Merge branch 'main' into ENG-3041

This commit is contained in:
x032205
2025-07-03 19:55:32 -04:00
148 changed files with 3763 additions and 846 deletions
@@ -83,7 +83,7 @@ jobs:
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
goreleaser: goreleaser:
runs-on: ubuntu-latest runs-on: ubuntu-latest-8-cores
needs: [cli-integration-tests] needs: [cli-integration-tests]
steps: steps:
- uses: actions/checkout@v3 - uses: actions/checkout@v3
+2 -1
View File
@@ -110,7 +110,8 @@ export const initAuditLogDbConnection = ({
}, },
migrations: { migrations: {
tableName: "infisical_migrations" tableName: "infisical_migrations"
} },
pool: { min: 0, max: 10 }
}); });
// we add these overrides so that auditLogDb and the primary DB are interchangeable // we add these overrides so that auditLogDb and the primary DB are interchangeable
@@ -0,0 +1,21 @@
import { Knex } from "knex";
import { TableName } from "../schemas";
export async function up(knex: Knex): Promise<void> {
const hasColumn = await knex.schema.hasColumn(TableName.SuperAdmin, "encryptedEnvOverrides");
if (!hasColumn) {
await knex.schema.alterTable(TableName.SuperAdmin, (t) => {
t.binary("encryptedEnvOverrides").nullable();
});
}
}
export async function down(knex: Knex): Promise<void> {
const hasColumn = await knex.schema.hasColumn(TableName.SuperAdmin, "encryptedEnvOverrides");
if (hasColumn) {
await knex.schema.alterTable(TableName.SuperAdmin, (t) => {
t.dropColumn("encryptedEnvOverrides");
});
}
}
+2 -1
View File
@@ -34,7 +34,8 @@ export const SuperAdminSchema = z.object({
encryptedGitHubAppConnectionClientSecret: zodBuffer.nullable().optional(), encryptedGitHubAppConnectionClientSecret: zodBuffer.nullable().optional(),
encryptedGitHubAppConnectionSlug: zodBuffer.nullable().optional(), encryptedGitHubAppConnectionSlug: zodBuffer.nullable().optional(),
encryptedGitHubAppConnectionId: zodBuffer.nullable().optional(), encryptedGitHubAppConnectionId: zodBuffer.nullable().optional(),
encryptedGitHubAppConnectionPrivateKey: zodBuffer.nullable().optional() encryptedGitHubAppConnectionPrivateKey: zodBuffer.nullable().optional(),
encryptedEnvOverrides: zodBuffer.nullable().optional()
}); });
export type TSuperAdmin = z.infer<typeof SuperAdminSchema>; export type TSuperAdmin = z.infer<typeof SuperAdminSchema>;
+26 -3
View File
@@ -111,14 +111,37 @@ export const registerProjectRouter = async (server: FastifyZodProvider) => {
params: z.object({ params: z.object({
workspaceId: z.string().trim().describe(AUDIT_LOGS.EXPORT.projectId) workspaceId: z.string().trim().describe(AUDIT_LOGS.EXPORT.projectId)
}), }),
querystring: z.object({ querystring: z
.object({
eventType: z.nativeEnum(EventType).optional().describe(AUDIT_LOGS.EXPORT.eventType), eventType: z.nativeEnum(EventType).optional().describe(AUDIT_LOGS.EXPORT.eventType),
userAgentType: z.nativeEnum(UserAgentType).optional().describe(AUDIT_LOGS.EXPORT.userAgentType), userAgentType: z.nativeEnum(UserAgentType).optional().describe(AUDIT_LOGS.EXPORT.userAgentType),
startDate: z.string().datetime().optional().describe(AUDIT_LOGS.EXPORT.startDate), startDate: z.string().datetime().optional().describe(AUDIT_LOGS.EXPORT.startDate),
endDate: z.string().datetime().optional().describe(AUDIT_LOGS.EXPORT.endDate), endDate: z.string().datetime().optional().describe(AUDIT_LOGS.EXPORT.endDate),
offset: z.coerce.number().default(0).describe(AUDIT_LOGS.EXPORT.offset), offset: z.coerce.number().default(0).describe(AUDIT_LOGS.EXPORT.offset),
limit: z.coerce.number().default(20).describe(AUDIT_LOGS.EXPORT.limit), limit: z.coerce.number().max(1000).default(20).describe(AUDIT_LOGS.EXPORT.limit),
actor: z.string().optional().describe(AUDIT_LOGS.EXPORT.actor) actor: z.string().optional().describe(AUDIT_LOGS.EXPORT.actor)
})
.superRefine((el, ctx) => {
if (el.endDate && el.startDate) {
const startDate = new Date(el.startDate);
const endDate = new Date(el.endDate);
const maxAllowedDate = new Date(startDate);
maxAllowedDate.setMonth(maxAllowedDate.getMonth() + 3);
if (endDate < startDate) {
ctx.addIssue({
code: z.ZodIssueCode.custom,
path: ["endDate"],
message: "End date cannot be before start date"
});
}
if (endDate > maxAllowedDate) {
ctx.addIssue({
code: z.ZodIssueCode.custom,
path: ["endDate"],
message: "Dates must be within 3 months"
});
}
}
}), }),
response: { response: {
200: z.object({ 200: z.object({
@@ -161,7 +184,7 @@ export const registerProjectRouter = async (server: FastifyZodProvider) => {
filter: { filter: {
...req.query, ...req.query,
projectId: req.params.workspaceId, projectId: req.params.workspaceId,
endDate: req.query.endDate, endDate: req.query.endDate || new Date().toISOString(),
startDate: req.query.startDate || getLastMidnightDateISO(), startDate: req.query.startDate || getLastMidnightDateISO(),
auditLogActorId: req.query.actor, auditLogActorId: req.query.actor,
eventType: req.query.eventType ? [req.query.eventType] : undefined eventType: req.query.eventType ? [req.query.eventType] : undefined
@@ -30,10 +30,10 @@ type TFindQuery = {
actor?: string; actor?: string;
projectId?: string; projectId?: string;
environment?: string; environment?: string;
orgId?: string; orgId: string;
eventType?: string; eventType?: string;
startDate?: string; startDate: string;
endDate?: string; endDate: string;
userAgentType?: string; userAgentType?: string;
limit?: number; limit?: number;
offset?: number; offset?: number;
@@ -61,18 +61,15 @@ export const auditLogDALFactory = (db: TDbClient) => {
}, },
tx tx
) => { ) => {
if (!orgId && !projectId) {
throw new Error("Either orgId or projectId must be provided");
}
try { try {
// Find statements // Find statements
const sqlQuery = (tx || db.replicaNode())(TableName.AuditLog) const sqlQuery = (tx || db.replicaNode())(TableName.AuditLog)
.where(`${TableName.AuditLog}.orgId`, orgId)
.whereRaw(`"${TableName.AuditLog}"."createdAt" >= ?::timestamptz`, [startDate])
.andWhereRaw(`"${TableName.AuditLog}"."createdAt" < ?::timestamptz`, [endDate])
// eslint-disable-next-line func-names // eslint-disable-next-line func-names
.where(function () { .where(function () {
if (orgId) { if (projectId) {
void this.where(`${TableName.AuditLog}.orgId`, orgId);
} else if (projectId) {
void this.where(`${TableName.AuditLog}.projectId`, projectId); void this.where(`${TableName.AuditLog}.projectId`, projectId);
} }
}); });
@@ -135,14 +132,6 @@ export const auditLogDALFactory = (db: TDbClient) => {
void sqlQuery.whereIn("eventType", eventType); void sqlQuery.whereIn("eventType", eventType);
} }
// Filter by date range
if (startDate) {
void sqlQuery.whereRaw(`"${TableName.AuditLog}"."createdAt" >= ?::timestamptz`, [startDate]);
}
if (endDate) {
void sqlQuery.whereRaw(`"${TableName.AuditLog}"."createdAt" <= ?::timestamptz`, [endDate]);
}
// we timeout long running queries to prevent DB resource issues (2 minutes) // we timeout long running queries to prevent DB resource issues (2 minutes)
const docs = await sqlQuery.timeout(1000 * 120); const docs = await sqlQuery.timeout(1000 * 120);
@@ -174,6 +163,8 @@ export const auditLogDALFactory = (db: TDbClient) => {
try { try {
const findExpiredLogSubQuery = (tx || db)(TableName.AuditLog) const findExpiredLogSubQuery = (tx || db)(TableName.AuditLog)
.where("expiresAt", "<", today) .where("expiresAt", "<", today)
.where("createdAt", "<", today) // to use audit log partition
.orderBy(`${TableName.AuditLog}.createdAt`, "desc")
.select("id") .select("id")
.limit(AUDIT_LOG_PRUNE_BATCH_SIZE); .limit(AUDIT_LOG_PRUNE_BATCH_SIZE);
@@ -67,7 +67,8 @@ export const auditLogServiceFactory = ({
secretPath: filter.secretPath, secretPath: filter.secretPath,
secretKey: filter.secretKey, secretKey: filter.secretKey,
environment: filter.environment, environment: filter.environment,
...(filter.projectId ? { projectId: filter.projectId } : { orgId: actorOrgId }) orgId: actorOrgId,
...(filter.projectId ? { projectId: filter.projectId } : {})
}); });
return auditLogs.map(({ eventType: logEventType, actor: eActor, actorMetadata, eventMetadata, ...el }) => ({ return auditLogs.map(({ eventType: logEventType, actor: eActor, actorMetadata, eventMetadata, ...el }) => ({
@@ -56,8 +56,8 @@ export type TListProjectAuditLogDTO = {
eventType?: EventType[]; eventType?: EventType[];
offset?: number; offset?: number;
limit: number; limit: number;
endDate?: string; endDate: string;
startDate?: string; startDate: string;
projectId?: string; projectId?: string;
environment?: string; environment?: string;
auditLogActorId?: string; auditLogActorId?: string;
@@ -318,7 +318,7 @@ export const secretScanningV2QueueServiceFactory = async ({
}, },
{ {
batchSize: 1, batchSize: 1,
workerCount: 20, workerCount: 2,
pollingIntervalSeconds: 1 pollingIntervalSeconds: 1
} }
); );
@@ -539,7 +539,7 @@ export const secretScanningV2QueueServiceFactory = async ({
}, },
{ {
batchSize: 1, batchSize: 1,
workerCount: 20, workerCount: 2,
pollingIntervalSeconds: 1 pollingIntervalSeconds: 1
} }
); );
@@ -613,7 +613,7 @@ export const secretScanningV2QueueServiceFactory = async ({
}, },
{ {
batchSize: 1, batchSize: 1,
workerCount: 5, workerCount: 2,
pollingIntervalSeconds: 1 pollingIntervalSeconds: 1
} }
); );
+10
View File
@@ -2272,6 +2272,10 @@ export const AppConnections = {
BITBUCKET: { BITBUCKET: {
email: "The email used to access Bitbucket.", email: "The email used to access Bitbucket.",
apiToken: "The API token used to access Bitbucket." apiToken: "The API token used to access Bitbucket."
},
ZABBIX: {
apiToken: "The API Token used to access Zabbix.",
instanceUrl: "The Zabbix instance URL to connect with."
} }
} }
}; };
@@ -2461,6 +2465,12 @@ export const SecretSyncs = {
CLOUDFLARE_PAGES: { CLOUDFLARE_PAGES: {
projectName: "The name of the Cloudflare Pages project to sync secrets to.", projectName: "The name of the Cloudflare Pages project to sync secrets to.",
environment: "The environment of the Cloudflare Pages project to sync secrets to." environment: "The environment of the Cloudflare Pages project to sync secrets to."
},
ZABBIX: {
scope: "The Zabbix scope that secrets should be synced to.",
hostId: "The ID of the Zabbix host to sync secrets to.",
hostName: "The name of the Zabbix host to sync secrets to.",
macroType: "The type of macro to sync secrets to. (0: Text, 1: Secret)"
} }
} }
}; };
+110 -1
View File
@@ -2,6 +2,7 @@ import { z } from "zod";
import { QueueWorkerProfile } from "@app/lib/types"; import { QueueWorkerProfile } from "@app/lib/types";
import { BadRequestError } from "../errors";
import { removeTrailingSlash } from "../fn"; import { removeTrailingSlash } from "../fn";
import { CustomLogger } from "../logger/logger"; import { CustomLogger } from "../logger/logger";
import { zpStr } from "../zod"; import { zpStr } from "../zod";
@@ -341,8 +342,11 @@ const envSchema = z
export type TEnvConfig = Readonly<z.infer<typeof envSchema>>; export type TEnvConfig = Readonly<z.infer<typeof envSchema>>;
let envCfg: TEnvConfig; let envCfg: TEnvConfig;
let originalEnvConfig: TEnvConfig;
export const getConfig = () => envCfg; export const getConfig = () => envCfg;
export const getOriginalConfig = () => originalEnvConfig;
// cannot import singleton logger directly as it needs config to load various transport // cannot import singleton logger directly as it needs config to load various transport
export const initEnvConfig = (logger?: CustomLogger) => { export const initEnvConfig = (logger?: CustomLogger) => {
const parsedEnv = envSchema.safeParse(process.env); const parsedEnv = envSchema.safeParse(process.env);
@@ -352,10 +356,115 @@ export const initEnvConfig = (logger?: CustomLogger) => {
process.exit(-1); process.exit(-1);
} }
envCfg = Object.freeze(parsedEnv.data); const config = Object.freeze(parsedEnv.data);
envCfg = config;
if (!originalEnvConfig) {
originalEnvConfig = config;
}
return envCfg; return envCfg;
}; };
// A list of environment variables that can be overwritten
export const overwriteSchema: {
[key: string]: {
name: string;
fields: { key: keyof TEnvConfig; description?: string }[];
};
} = {
azure: {
name: "Azure",
fields: [
{
key: "INF_APP_CONNECTION_AZURE_CLIENT_ID",
description: "The Application (Client) ID of your Azure application."
},
{
key: "INF_APP_CONNECTION_AZURE_CLIENT_SECRET",
description: "The Client Secret of your Azure application."
}
]
},
google_sso: {
name: "Google SSO",
fields: [
{
key: "CLIENT_ID_GOOGLE_LOGIN",
description: "The Client ID of your GCP OAuth2 application."
},
{
key: "CLIENT_SECRET_GOOGLE_LOGIN",
description: "The Client Secret of your GCP OAuth2 application."
}
]
},
github_sso: {
name: "GitHub SSO",
fields: [
{
key: "CLIENT_ID_GITHUB_LOGIN",
description: "The Client ID of your GitHub OAuth application."
},
{
key: "CLIENT_SECRET_GITHUB_LOGIN",
description: "The Client Secret of your GitHub OAuth application."
}
]
},
gitlab_sso: {
name: "GitLab SSO",
fields: [
{
key: "CLIENT_ID_GITLAB_LOGIN",
description: "The Client ID of your GitLab application."
},
{
key: "CLIENT_SECRET_GITLAB_LOGIN",
description: "The Secret of your GitLab application."
},
{
key: "CLIENT_GITLAB_LOGIN_URL",
description:
"The URL of your self-hosted instance of GitLab where the OAuth application is registered. If no URL is passed in, this will default to https://gitlab.com."
}
]
}
};
export const overridableKeys = new Set(
Object.values(overwriteSchema).flatMap(({ fields }) => fields.map(({ key }) => key))
);
export const validateOverrides = (config: Record<string, string>) => {
const allowedOverrides = Object.fromEntries(
Object.entries(config).filter(([key]) => overridableKeys.has(key as keyof z.input<typeof envSchema>))
);
const tempEnv: Record<string, unknown> = { ...process.env, ...allowedOverrides };
const parsedResult = envSchema.safeParse(tempEnv);
if (!parsedResult.success) {
const errorDetails = parsedResult.error.issues
.map((issue) => `Key: "${issue.path.join(".")}", Error: ${issue.message}`)
.join("\n");
throw new BadRequestError({ message: errorDetails });
}
};
export const overrideEnvConfig = (config: Record<string, string>) => {
const allowedOverrides = Object.fromEntries(
Object.entries(config).filter(([key]) => overridableKeys.has(key as keyof z.input<typeof envSchema>))
);
const tempEnv: Record<string, unknown> = { ...process.env, ...allowedOverrides };
const parsedResult = envSchema.safeParse(tempEnv);
if (parsedResult.success) {
envCfg = Object.freeze(parsedResult.data);
}
};
export const formatSmtpConfig = () => { export const formatSmtpConfig = () => {
const tlsOptions: { const tlsOptions: {
rejectUnauthorized: boolean; rejectUnauthorized: boolean;
+11
View File
@@ -300,6 +300,7 @@ import { injectIdentity } from "../plugins/auth/inject-identity";
import { injectPermission } from "../plugins/auth/inject-permission"; import { injectPermission } from "../plugins/auth/inject-permission";
import { injectRateLimits } from "../plugins/inject-rate-limits"; import { injectRateLimits } from "../plugins/inject-rate-limits";
import { registerV1Routes } from "./v1"; import { registerV1Routes } from "./v1";
import { initializeOauthConfigSync } from "./v1/sso-router";
import { registerV2Routes } from "./v2"; import { registerV2Routes } from "./v2";
import { registerV3Routes } from "./v3"; import { registerV3Routes } from "./v3";
@@ -2046,6 +2047,16 @@ export const registerRoutes = async (
} }
} }
const configSyncJob = await superAdminService.initializeEnvConfigSync();
if (configSyncJob) {
cronJobs.push(configSyncJob);
}
const oauthConfigSyncJob = await initializeOauthConfigSync();
if (oauthConfigSyncJob) {
cronJobs.push(oauthConfigSyncJob);
}
server.decorate<FastifyZodProvider["store"]>("store", { server.decorate<FastifyZodProvider["store"]>("store", {
user: userDAL, user: userDAL,
kmipClient: kmipClientDAL kmipClient: kmipClientDAL
+43 -4
View File
@@ -8,7 +8,7 @@ import {
SuperAdminSchema, SuperAdminSchema,
UsersSchema UsersSchema
} from "@app/db/schemas"; } from "@app/db/schemas";
import { getConfig } from "@app/lib/config/env"; import { getConfig, overridableKeys } from "@app/lib/config/env";
import { BadRequestError } from "@app/lib/errors"; import { BadRequestError } from "@app/lib/errors";
import { invalidateCacheLimit, readLimit, writeLimit } from "@app/server/config/rateLimiter"; import { invalidateCacheLimit, readLimit, writeLimit } from "@app/server/config/rateLimiter";
import { getTelemetryDistinctId } from "@app/server/lib/telemetry"; import { getTelemetryDistinctId } from "@app/server/lib/telemetry";
@@ -42,7 +42,8 @@ export const registerAdminRouter = async (server: FastifyZodProvider) => {
encryptedGitHubAppConnectionClientSecret: true, encryptedGitHubAppConnectionClientSecret: true,
encryptedGitHubAppConnectionSlug: true, encryptedGitHubAppConnectionSlug: true,
encryptedGitHubAppConnectionId: true, encryptedGitHubAppConnectionId: true,
encryptedGitHubAppConnectionPrivateKey: true encryptedGitHubAppConnectionPrivateKey: true,
encryptedEnvOverrides: true
}).extend({ }).extend({
isMigrationModeOn: z.boolean(), isMigrationModeOn: z.boolean(),
defaultAuthOrgSlug: z.string().nullable(), defaultAuthOrgSlug: z.string().nullable(),
@@ -110,11 +111,14 @@ export const registerAdminRouter = async (server: FastifyZodProvider) => {
.refine((content) => DOMPurify.sanitize(content) === content, { .refine((content) => DOMPurify.sanitize(content) === content, {
message: "Page frame content contains unsafe HTML." message: "Page frame content contains unsafe HTML."
}) })
.optional() .optional(),
envOverrides: z.record(z.enum(Array.from(overridableKeys) as [string, ...string[]]), z.string()).optional()
}), }),
response: { response: {
200: z.object({ 200: z.object({
config: SuperAdminSchema.extend({ config: SuperAdminSchema.omit({
encryptedEnvOverrides: true
}).extend({
defaultAuthOrgSlug: z.string().nullable() defaultAuthOrgSlug: z.string().nullable()
}) })
}) })
@@ -381,6 +385,41 @@ export const registerAdminRouter = async (server: FastifyZodProvider) => {
} }
}); });
server.route({
method: "GET",
url: "/env-overrides",
config: {
rateLimit: readLimit
},
schema: {
response: {
200: z.record(
z.string(),
z.object({
name: z.string(),
fields: z
.object({
key: z.string(),
value: z.string(),
hasEnvEntry: z.boolean(),
description: z.string().optional()
})
.array()
})
)
}
},
onRequest: (req, res, done) => {
verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN])(req, res, () => {
verifySuperAdmin(req, res, done);
});
},
handler: async () => {
const envOverrides = await server.services.superAdmin.getEnvOverridesOrganized();
return envOverrides;
}
});
server.route({ server.route({
method: "DELETE", method: "DELETE",
url: "/user-management/users/:userId", url: "/user-management/users/:userId",
@@ -88,6 +88,7 @@ import {
SanitizedWindmillConnectionSchema, SanitizedWindmillConnectionSchema,
WindmillConnectionListItemSchema WindmillConnectionListItemSchema
} from "@app/services/app-connection/windmill"; } from "@app/services/app-connection/windmill";
import { SanitizedZabbixConnectionSchema, ZabbixConnectionListItemSchema } from "@app/services/app-connection/zabbix";
import { AuthMode } from "@app/services/auth/auth-type"; import { AuthMode } from "@app/services/auth/auth-type";
// can't use discriminated due to multiple schemas for certain apps // can't use discriminated due to multiple schemas for certain apps
@@ -121,7 +122,8 @@ const SanitizedAppConnectionSchema = z.union([
...SanitizedFlyioConnectionSchema.options, ...SanitizedFlyioConnectionSchema.options,
...SanitizedGitLabConnectionSchema.options, ...SanitizedGitLabConnectionSchema.options,
...SanitizedCloudflareConnectionSchema.options, ...SanitizedCloudflareConnectionSchema.options,
...SanitizedBitbucketConnectionSchema.options ...SanitizedBitbucketConnectionSchema.options,
...SanitizedZabbixConnectionSchema.options
]); ]);
const AppConnectionOptionsSchema = z.discriminatedUnion("app", [ const AppConnectionOptionsSchema = z.discriminatedUnion("app", [
@@ -154,7 +156,8 @@ const AppConnectionOptionsSchema = z.discriminatedUnion("app", [
FlyioConnectionListItemSchema, FlyioConnectionListItemSchema,
GitLabConnectionListItemSchema, GitLabConnectionListItemSchema,
CloudflareConnectionListItemSchema, CloudflareConnectionListItemSchema,
BitbucketConnectionListItemSchema BitbucketConnectionListItemSchema,
ZabbixConnectionListItemSchema
]); ]);
export const registerAppConnectionRouter = async (server: FastifyZodProvider) => { export const registerAppConnectionRouter = async (server: FastifyZodProvider) => {
@@ -30,6 +30,7 @@ import { registerTeamCityConnectionRouter } from "./teamcity-connection-router";
import { registerTerraformCloudConnectionRouter } from "./terraform-cloud-router"; import { registerTerraformCloudConnectionRouter } from "./terraform-cloud-router";
import { registerVercelConnectionRouter } from "./vercel-connection-router"; import { registerVercelConnectionRouter } from "./vercel-connection-router";
import { registerWindmillConnectionRouter } from "./windmill-connection-router"; import { registerWindmillConnectionRouter } from "./windmill-connection-router";
import { registerZabbixConnectionRouter } from "./zabbix-connection-router";
export * from "./app-connection-router"; export * from "./app-connection-router";
@@ -64,5 +65,6 @@ export const APP_CONNECTION_REGISTER_ROUTER_MAP: Record<AppConnection, (server:
[AppConnection.Flyio]: registerFlyioConnectionRouter, [AppConnection.Flyio]: registerFlyioConnectionRouter,
[AppConnection.GitLab]: registerGitLabConnectionRouter, [AppConnection.GitLab]: registerGitLabConnectionRouter,
[AppConnection.Cloudflare]: registerCloudflareConnectionRouter, [AppConnection.Cloudflare]: registerCloudflareConnectionRouter,
[AppConnection.Bitbucket]: registerBitbucketConnectionRouter [AppConnection.Bitbucket]: registerBitbucketConnectionRouter,
[AppConnection.Zabbix]: registerZabbixConnectionRouter
}; };
@@ -0,0 +1,51 @@
import z from "zod";
import { readLimit } from "@app/server/config/rateLimiter";
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
import { AppConnection } from "@app/services/app-connection/app-connection-enums";
import {
CreateZabbixConnectionSchema,
SanitizedZabbixConnectionSchema,
UpdateZabbixConnectionSchema
} from "@app/services/app-connection/zabbix";
import { AuthMode } from "@app/services/auth/auth-type";
import { registerAppConnectionEndpoints } from "./app-connection-endpoints";
export const registerZabbixConnectionRouter = async (server: FastifyZodProvider) => {
registerAppConnectionEndpoints({
app: AppConnection.Zabbix,
server,
sanitizedResponseSchema: SanitizedZabbixConnectionSchema,
createSchema: CreateZabbixConnectionSchema,
updateSchema: UpdateZabbixConnectionSchema
});
// The following endpoints are for internal Infisical App use only and not part of the public API
server.route({
method: "GET",
url: `/:connectionId/hosts`,
config: {
rateLimit: readLimit
},
schema: {
params: z.object({
connectionId: z.string().uuid()
}),
response: {
200: z
.object({
hostId: z.string(),
host: z.string()
})
.array()
}
},
onRequest: verifyAuth([AuthMode.JWT]),
handler: async (req) => {
const { connectionId } = req.params;
const hosts = await server.services.appConnection.zabbix.listHosts(connectionId, req.permission);
return hosts;
}
});
};
@@ -113,7 +113,8 @@ export const registerOrgRouter = async (server: FastifyZodProvider) => {
hide: false, hide: false,
tags: [ApiDocsTags.AuditLogs], tags: [ApiDocsTags.AuditLogs],
description: "Get all audit logs for an organization", description: "Get all audit logs for an organization",
querystring: z.object({ querystring: z
.object({
projectId: z.string().optional().describe(AUDIT_LOGS.EXPORT.projectId), projectId: z.string().optional().describe(AUDIT_LOGS.EXPORT.projectId),
environment: z.string().optional().describe(AUDIT_LOGS.EXPORT.environment), environment: z.string().optional().describe(AUDIT_LOGS.EXPORT.environment),
actorType: z.nativeEnum(ActorType).optional(), actorType: z.nativeEnum(ActorType).optional(),
@@ -123,7 +124,6 @@ export const registerOrgRouter = async (server: FastifyZodProvider) => {
.transform((val) => (!val ? val : removeTrailingSlash(val))) .transform((val) => (!val ? val : removeTrailingSlash(val)))
.describe(AUDIT_LOGS.EXPORT.secretPath), .describe(AUDIT_LOGS.EXPORT.secretPath),
secretKey: z.string().optional().describe(AUDIT_LOGS.EXPORT.secretKey), secretKey: z.string().optional().describe(AUDIT_LOGS.EXPORT.secretKey),
// eventType is split with , for multiple values, we need to transform it to array // eventType is split with , for multiple values, we need to transform it to array
eventType: z eventType: z
.string() .string()
@@ -155,10 +155,31 @@ export const registerOrgRouter = async (server: FastifyZodProvider) => {
startDate: z.string().datetime().optional().describe(AUDIT_LOGS.EXPORT.startDate), startDate: z.string().datetime().optional().describe(AUDIT_LOGS.EXPORT.startDate),
endDate: z.string().datetime().optional().describe(AUDIT_LOGS.EXPORT.endDate), endDate: z.string().datetime().optional().describe(AUDIT_LOGS.EXPORT.endDate),
offset: z.coerce.number().default(0).describe(AUDIT_LOGS.EXPORT.offset), offset: z.coerce.number().default(0).describe(AUDIT_LOGS.EXPORT.offset),
limit: z.coerce.number().default(20).describe(AUDIT_LOGS.EXPORT.limit), limit: z.coerce.number().max(1000).default(20).describe(AUDIT_LOGS.EXPORT.limit),
actor: z.string().optional().describe(AUDIT_LOGS.EXPORT.actor) actor: z.string().optional().describe(AUDIT_LOGS.EXPORT.actor)
})
.superRefine((el, ctx) => {
if (el.endDate && el.startDate) {
const startDate = new Date(el.startDate);
const endDate = new Date(el.endDate);
const maxAllowedDate = new Date(startDate);
maxAllowedDate.setMonth(maxAllowedDate.getMonth() + 3);
if (endDate < startDate) {
ctx.addIssue({
code: z.ZodIssueCode.custom,
path: ["endDate"],
message: "End date cannot be before start date"
});
}
if (endDate > maxAllowedDate) {
ctx.addIssue({
code: z.ZodIssueCode.custom,
path: ["endDate"],
message: "Dates must be within 3 months"
});
}
}
}), }),
response: { response: {
200: z.object({ 200: z.object({
auditLogs: AuditLogsSchema.omit({ auditLogs: AuditLogsSchema.omit({
@@ -188,14 +209,13 @@ export const registerOrgRouter = async (server: FastifyZodProvider) => {
const auditLogs = await server.services.auditLog.listAuditLogs({ const auditLogs = await server.services.auditLog.listAuditLogs({
filter: { filter: {
...req.query, ...req.query,
endDate: req.query.endDate, endDate: req.query.endDate || new Date().toISOString(),
projectId: req.query.projectId, projectId: req.query.projectId,
startDate: req.query.startDate || getLastMidnightDateISO(), startDate: req.query.startDate || getLastMidnightDateISO(),
auditLogActorId: req.query.actor, auditLogActorId: req.query.actor,
actorType: req.query.actorType, actorType: req.query.actorType,
eventType: req.query.eventType as EventType[] | undefined eventType: req.query.eventType as EventType[] | undefined
}, },
actorId: req.permission.id, actorId: req.permission.id,
actorOrgId: req.permission.orgId, actorOrgId: req.permission.orgId,
actorAuthMethod: req.permission.authMethod, actorAuthMethod: req.permission.authMethod,
@@ -22,6 +22,7 @@ import { registerTeamCitySyncRouter } from "./teamcity-sync-router";
import { registerTerraformCloudSyncRouter } from "./terraform-cloud-sync-router"; import { registerTerraformCloudSyncRouter } from "./terraform-cloud-sync-router";
import { registerVercelSyncRouter } from "./vercel-sync-router"; import { registerVercelSyncRouter } from "./vercel-sync-router";
import { registerWindmillSyncRouter } from "./windmill-sync-router"; import { registerWindmillSyncRouter } from "./windmill-sync-router";
import { registerZabbixSyncRouter } from "./zabbix-sync-router";
export * from "./secret-sync-router"; export * from "./secret-sync-router";
@@ -47,5 +48,6 @@ export const SECRET_SYNC_REGISTER_ROUTER_MAP: Record<SecretSync, (server: Fastif
[SecretSync.Render]: registerRenderSyncRouter, [SecretSync.Render]: registerRenderSyncRouter,
[SecretSync.Flyio]: registerFlyioSyncRouter, [SecretSync.Flyio]: registerFlyioSyncRouter,
[SecretSync.GitLab]: registerGitLabSyncRouter, [SecretSync.GitLab]: registerGitLabSyncRouter,
[SecretSync.CloudflarePages]: registerCloudflarePagesSyncRouter [SecretSync.CloudflarePages]: registerCloudflarePagesSyncRouter,
[SecretSync.Zabbix]: registerZabbixSyncRouter
}; };
@@ -39,6 +39,7 @@ import { TeamCitySyncListItemSchema, TeamCitySyncSchema } from "@app/services/se
import { TerraformCloudSyncListItemSchema, TerraformCloudSyncSchema } from "@app/services/secret-sync/terraform-cloud"; import { TerraformCloudSyncListItemSchema, TerraformCloudSyncSchema } from "@app/services/secret-sync/terraform-cloud";
import { VercelSyncListItemSchema, VercelSyncSchema } from "@app/services/secret-sync/vercel"; import { VercelSyncListItemSchema, VercelSyncSchema } from "@app/services/secret-sync/vercel";
import { WindmillSyncListItemSchema, WindmillSyncSchema } from "@app/services/secret-sync/windmill"; import { WindmillSyncListItemSchema, WindmillSyncSchema } from "@app/services/secret-sync/windmill";
import { ZabbixSyncListItemSchema, ZabbixSyncSchema } from "@app/services/secret-sync/zabbix";
const SecretSyncSchema = z.discriminatedUnion("destination", [ const SecretSyncSchema = z.discriminatedUnion("destination", [
AwsParameterStoreSyncSchema, AwsParameterStoreSyncSchema,
@@ -62,7 +63,8 @@ const SecretSyncSchema = z.discriminatedUnion("destination", [
RenderSyncSchema, RenderSyncSchema,
FlyioSyncSchema, FlyioSyncSchema,
GitLabSyncSchema, GitLabSyncSchema,
CloudflarePagesSyncSchema CloudflarePagesSyncSchema,
ZabbixSyncSchema
]); ]);
const SecretSyncOptionsSchema = z.discriminatedUnion("destination", [ const SecretSyncOptionsSchema = z.discriminatedUnion("destination", [
@@ -87,7 +89,8 @@ const SecretSyncOptionsSchema = z.discriminatedUnion("destination", [
RenderSyncListItemSchema, RenderSyncListItemSchema,
FlyioSyncListItemSchema, FlyioSyncListItemSchema,
GitLabSyncListItemSchema, GitLabSyncListItemSchema,
CloudflarePagesSyncListItemSchema CloudflarePagesSyncListItemSchema,
ZabbixSyncListItemSchema
]); ]);
export const registerSecretSyncRouter = async (server: FastifyZodProvider) => { export const registerSecretSyncRouter = async (server: FastifyZodProvider) => {
@@ -0,0 +1,13 @@
import { SecretSync } from "@app/services/secret-sync/secret-sync-enums";
import { CreateZabbixSyncSchema, UpdateZabbixSyncSchema, ZabbixSyncSchema } from "@app/services/secret-sync/zabbix";
import { registerSyncSecretsEndpoints } from "./secret-sync-endpoints";
export const registerZabbixSyncRouter = async (server: FastifyZodProvider) =>
registerSyncSecretsEndpoints({
destination: SecretSync.Zabbix,
server,
responseSchema: ZabbixSyncSchema,
createSchema: CreateZabbixSyncSchema,
updateSchema: UpdateZabbixSyncSchema
});
+49 -18
View File
@@ -9,6 +9,7 @@
import { Authenticator } from "@fastify/passport"; import { Authenticator } from "@fastify/passport";
import fastifySession from "@fastify/session"; import fastifySession from "@fastify/session";
import RedisStore from "connect-redis"; import RedisStore from "connect-redis";
import { CronJob } from "cron";
import { Strategy as GitLabStrategy } from "passport-gitlab2"; import { Strategy as GitLabStrategy } from "passport-gitlab2";
import { Strategy as GoogleStrategy } from "passport-google-oauth20"; import { Strategy as GoogleStrategy } from "passport-google-oauth20";
import { Strategy as OAuth2Strategy } from "passport-oauth2"; import { Strategy as OAuth2Strategy } from "passport-oauth2";
@@ -25,26 +26,13 @@ import { AuthMethod } from "@app/services/auth/auth-type";
import { OrgAuthMethod } from "@app/services/org/org-types"; import { OrgAuthMethod } from "@app/services/org/org-types";
import { getServerCfg } from "@app/services/super-admin/super-admin-service"; import { getServerCfg } from "@app/services/super-admin/super-admin-service";
export const registerSsoRouter = async (server: FastifyZodProvider) => {
const appCfg = getConfig();
const passport = new Authenticator({ key: "sso", userProperty: "passportUser" }); const passport = new Authenticator({ key: "sso", userProperty: "passportUser" });
const redisStore = new RedisStore({
client: server.redis,
prefix: "oauth-session:",
ttl: 600 // 10 minutes
});
await server.register(fastifySession, { let serverInstance: FastifyZodProvider | null = null;
secret: appCfg.COOKIE_SECRET_SIGN_KEY,
store: redisStore, export const registerOauthMiddlewares = (server: FastifyZodProvider) => {
cookie: { serverInstance = server;
secure: appCfg.HTTPS_ENABLED, const appCfg = getConfig();
sameSite: "lax" // we want cookies to be sent to Infisical in redirects originating from IDP server
}
});
await server.register(passport.initialize());
await server.register(passport.secureSession());
// passport oauth strategy for Google // passport oauth strategy for Google
const isGoogleOauthActive = Boolean(appCfg.CLIENT_ID_GOOGLE_LOGIN && appCfg.CLIENT_SECRET_GOOGLE_LOGIN); const isGoogleOauthActive = Boolean(appCfg.CLIENT_ID_GOOGLE_LOGIN && appCfg.CLIENT_SECRET_GOOGLE_LOGIN);
@@ -176,6 +164,49 @@ export const registerSsoRouter = async (server: FastifyZodProvider) => {
) )
); );
} }
};
export const refreshOauthConfig = () => {
if (!serverInstance) {
logger.warn("Cannot refresh OAuth config: server instance not available");
return;
}
logger.info("Refreshing OAuth configuration...");
registerOauthMiddlewares(serverInstance);
};
export const initializeOauthConfigSync = async () => {
logger.info("Setting up background sync process for oauth configuration");
// sync every 5 minutes
const job = new CronJob("*/5 * * * *", refreshOauthConfig);
job.start();
return job;
};
export const registerSsoRouter = async (server: FastifyZodProvider) => {
const appCfg = getConfig();
const redisStore = new RedisStore({
client: server.redis,
prefix: "oauth-session:",
ttl: 600 // 10 minutes
});
await server.register(fastifySession, {
secret: appCfg.COOKIE_SECRET_SIGN_KEY,
store: redisStore,
cookie: {
secure: appCfg.HTTPS_ENABLED,
sameSite: "lax" // we want cookies to be sent to Infisical in redirects originating from IDP server
}
});
await server.register(passport.initialize());
await server.register(passport.secureSession());
registerOauthMiddlewares(server);
server.route({ server.route({
url: "/redirect/google", url: "/redirect/google",
@@ -28,7 +28,8 @@ export enum AppConnection {
Flyio = "flyio", Flyio = "flyio",
GitLab = "gitlab", GitLab = "gitlab",
Cloudflare = "cloudflare", Cloudflare = "cloudflare",
Bitbucket = "bitbucket" Bitbucket = "bitbucket",
Zabbix = "zabbix"
} }
export enum AWSRegion { export enum AWSRegion {
@@ -110,6 +110,7 @@ import {
validateWindmillConnectionCredentials, validateWindmillConnectionCredentials,
WindmillConnectionMethod WindmillConnectionMethod
} from "./windmill"; } from "./windmill";
import { getZabbixConnectionListItem, validateZabbixConnectionCredentials, ZabbixConnectionMethod } from "./zabbix";
export const listAppConnectionOptions = () => { export const listAppConnectionOptions = () => {
return [ return [
@@ -142,7 +143,8 @@ export const listAppConnectionOptions = () => {
getFlyioConnectionListItem(), getFlyioConnectionListItem(),
getGitLabConnectionListItem(), getGitLabConnectionListItem(),
getCloudflareConnectionListItem(), getCloudflareConnectionListItem(),
getBitbucketConnectionListItem() getBitbucketConnectionListItem(),
getZabbixConnectionListItem()
].sort((a, b) => a.name.localeCompare(b.name)); ].sort((a, b) => a.name.localeCompare(b.name));
}; };
@@ -223,7 +225,8 @@ export const validateAppConnectionCredentials = async (
[AppConnection.Flyio]: validateFlyioConnectionCredentials as TAppConnectionCredentialsValidator, [AppConnection.Flyio]: validateFlyioConnectionCredentials as TAppConnectionCredentialsValidator,
[AppConnection.GitLab]: validateGitLabConnectionCredentials as TAppConnectionCredentialsValidator, [AppConnection.GitLab]: validateGitLabConnectionCredentials as TAppConnectionCredentialsValidator,
[AppConnection.Cloudflare]: validateCloudflareConnectionCredentials as TAppConnectionCredentialsValidator, [AppConnection.Cloudflare]: validateCloudflareConnectionCredentials as TAppConnectionCredentialsValidator,
[AppConnection.Bitbucket]: validateBitbucketConnectionCredentials as TAppConnectionCredentialsValidator [AppConnection.Bitbucket]: validateBitbucketConnectionCredentials as TAppConnectionCredentialsValidator,
[AppConnection.Zabbix]: validateZabbixConnectionCredentials as TAppConnectionCredentialsValidator
}; };
return VALIDATE_APP_CONNECTION_CREDENTIALS_MAP[appConnection.app](appConnection); return VALIDATE_APP_CONNECTION_CREDENTIALS_MAP[appConnection.app](appConnection);
@@ -261,6 +264,7 @@ export const getAppConnectionMethodName = (method: TAppConnection["method"]) =>
case OnePassConnectionMethod.ApiToken: case OnePassConnectionMethod.ApiToken:
case CloudflareConnectionMethod.APIToken: case CloudflareConnectionMethod.APIToken:
case BitbucketConnectionMethod.ApiToken: case BitbucketConnectionMethod.ApiToken:
case ZabbixConnectionMethod.ApiToken:
return "API Token"; return "API Token";
case PostgresConnectionMethod.UsernameAndPassword: case PostgresConnectionMethod.UsernameAndPassword:
case MsSqlConnectionMethod.UsernameAndPassword: case MsSqlConnectionMethod.UsernameAndPassword:
@@ -341,7 +345,8 @@ export const TRANSITION_CONNECTION_CREDENTIALS_TO_PLATFORM: Record<
[AppConnection.Flyio]: platformManagedCredentialsNotSupported, [AppConnection.Flyio]: platformManagedCredentialsNotSupported,
[AppConnection.GitLab]: platformManagedCredentialsNotSupported, [AppConnection.GitLab]: platformManagedCredentialsNotSupported,
[AppConnection.Cloudflare]: platformManagedCredentialsNotSupported, [AppConnection.Cloudflare]: platformManagedCredentialsNotSupported,
[AppConnection.Bitbucket]: platformManagedCredentialsNotSupported [AppConnection.Bitbucket]: platformManagedCredentialsNotSupported,
[AppConnection.Zabbix]: platformManagedCredentialsNotSupported
}; };
export const enterpriseAppCheck = async ( export const enterpriseAppCheck = async (
@@ -30,7 +30,8 @@ export const APP_CONNECTION_NAME_MAP: Record<AppConnection, string> = {
[AppConnection.Flyio]: "Fly.io", [AppConnection.Flyio]: "Fly.io",
[AppConnection.GitLab]: "GitLab", [AppConnection.GitLab]: "GitLab",
[AppConnection.Cloudflare]: "Cloudflare", [AppConnection.Cloudflare]: "Cloudflare",
[AppConnection.Bitbucket]: "Bitbucket" [AppConnection.Bitbucket]: "Bitbucket",
[AppConnection.Zabbix]: "Zabbix"
}; };
export const APP_CONNECTION_PLAN_MAP: Record<AppConnection, AppConnectionPlanType> = { export const APP_CONNECTION_PLAN_MAP: Record<AppConnection, AppConnectionPlanType> = {
@@ -63,5 +64,6 @@ export const APP_CONNECTION_PLAN_MAP: Record<AppConnection, AppConnectionPlanTyp
[AppConnection.Flyio]: AppConnectionPlanType.Regular, [AppConnection.Flyio]: AppConnectionPlanType.Regular,
[AppConnection.GitLab]: AppConnectionPlanType.Regular, [AppConnection.GitLab]: AppConnectionPlanType.Regular,
[AppConnection.Cloudflare]: AppConnectionPlanType.Regular, [AppConnection.Cloudflare]: AppConnectionPlanType.Regular,
[AppConnection.Bitbucket]: AppConnectionPlanType.Regular [AppConnection.Bitbucket]: AppConnectionPlanType.Regular,
[AppConnection.Zabbix]: AppConnectionPlanType.Regular
}; };
@@ -82,6 +82,8 @@ import { ValidateVercelConnectionCredentialsSchema } from "./vercel";
import { vercelConnectionService } from "./vercel/vercel-connection-service"; import { vercelConnectionService } from "./vercel/vercel-connection-service";
import { ValidateWindmillConnectionCredentialsSchema } from "./windmill"; import { ValidateWindmillConnectionCredentialsSchema } from "./windmill";
import { windmillConnectionService } from "./windmill/windmill-connection-service"; import { windmillConnectionService } from "./windmill/windmill-connection-service";
import { ValidateZabbixConnectionCredentialsSchema } from "./zabbix";
import { zabbixConnectionService } from "./zabbix/zabbix-connection-service";
export type TAppConnectionServiceFactoryDep = { export type TAppConnectionServiceFactoryDep = {
appConnectionDAL: TAppConnectionDALFactory; appConnectionDAL: TAppConnectionDALFactory;
@@ -122,7 +124,8 @@ const VALIDATE_APP_CONNECTION_CREDENTIALS_MAP: Record<AppConnection, TValidateAp
[AppConnection.Flyio]: ValidateFlyioConnectionCredentialsSchema, [AppConnection.Flyio]: ValidateFlyioConnectionCredentialsSchema,
[AppConnection.GitLab]: ValidateGitLabConnectionCredentialsSchema, [AppConnection.GitLab]: ValidateGitLabConnectionCredentialsSchema,
[AppConnection.Cloudflare]: ValidateCloudflareConnectionCredentialsSchema, [AppConnection.Cloudflare]: ValidateCloudflareConnectionCredentialsSchema,
[AppConnection.Bitbucket]: ValidateBitbucketConnectionCredentialsSchema [AppConnection.Bitbucket]: ValidateBitbucketConnectionCredentialsSchema,
[AppConnection.Zabbix]: ValidateZabbixConnectionCredentialsSchema
}; };
export const appConnectionServiceFactory = ({ export const appConnectionServiceFactory = ({
@@ -533,6 +536,7 @@ export const appConnectionServiceFactory = ({
flyio: flyioConnectionService(connectAppConnectionById), flyio: flyioConnectionService(connectAppConnectionById),
gitlab: gitlabConnectionService(connectAppConnectionById, appConnectionDAL, kmsService), gitlab: gitlabConnectionService(connectAppConnectionById, appConnectionDAL, kmsService),
cloudflare: cloudflareConnectionService(connectAppConnectionById), cloudflare: cloudflareConnectionService(connectAppConnectionById),
bitbucket: bitBucketConnectionService(connectAppConnectionById) bitbucket: bitBucketConnectionService(connectAppConnectionById),
zabbix: zabbixConnectionService(connectAppConnectionById)
}; };
}; };
@@ -171,6 +171,12 @@ import {
TWindmillConnectionConfig, TWindmillConnectionConfig,
TWindmillConnectionInput TWindmillConnectionInput
} from "./windmill"; } from "./windmill";
import {
TValidateZabbixConnectionCredentialsSchema,
TZabbixConnection,
TZabbixConnectionConfig,
TZabbixConnectionInput
} from "./zabbix";
export type TAppConnection = { id: string } & ( export type TAppConnection = { id: string } & (
| TAwsConnection | TAwsConnection
@@ -203,6 +209,7 @@ export type TAppConnection = { id: string } & (
| TGitLabConnection | TGitLabConnection
| TCloudflareConnection | TCloudflareConnection
| TBitbucketConnection | TBitbucketConnection
| TZabbixConnection
); );
export type TAppConnectionRaw = NonNullable<Awaited<ReturnType<TAppConnectionDALFactory["findById"]>>>; export type TAppConnectionRaw = NonNullable<Awaited<ReturnType<TAppConnectionDALFactory["findById"]>>>;
@@ -240,6 +247,7 @@ export type TAppConnectionInput = { id: string } & (
| TGitLabConnectionInput | TGitLabConnectionInput
| TCloudflareConnectionInput | TCloudflareConnectionInput
| TBitbucketConnectionInput | TBitbucketConnectionInput
| TZabbixConnectionInput
); );
export type TSqlConnectionInput = export type TSqlConnectionInput =
@@ -285,6 +293,7 @@ export type TAppConnectionConfig =
| TGitLabConnectionConfig | TGitLabConnectionConfig
| TCloudflareConnectionConfig | TCloudflareConnectionConfig
| TBitbucketConnectionConfig; | TBitbucketConnectionConfig;
| TZabbixConnectionConfig;
export type TValidateAppConnectionCredentialsSchema = export type TValidateAppConnectionCredentialsSchema =
| TValidateAwsConnectionCredentialsSchema | TValidateAwsConnectionCredentialsSchema
@@ -316,7 +325,8 @@ export type TValidateAppConnectionCredentialsSchema =
| TValidateFlyioConnectionCredentialsSchema | TValidateFlyioConnectionCredentialsSchema
| TValidateGitLabConnectionCredentialsSchema | TValidateGitLabConnectionCredentialsSchema
| TValidateCloudflareConnectionCredentialsSchema | TValidateCloudflareConnectionCredentialsSchema
| TValidateBitbucketConnectionCredentialsSchema; | TValidateBitbucketConnectionCredentialsSchema
| TValidateZabbixConnectionCredentialsSchema;
export type TListAwsConnectionKmsKeys = { export type TListAwsConnectionKmsKeys = {
connectionId: string; connectionId: string;
@@ -0,0 +1,4 @@
export * from "./zabbix-connection-enums";
export * from "./zabbix-connection-fns";
export * from "./zabbix-connection-schemas";
export * from "./zabbix-connection-types";
@@ -0,0 +1,3 @@
export enum ZabbixConnectionMethod {
ApiToken = "api-token"
}
@@ -0,0 +1,108 @@
import { AxiosError } from "axios";
import RE2 from "re2";
import { request } from "@app/lib/config/request";
import { BadRequestError } from "@app/lib/errors";
import { blockLocalAndPrivateIpAddresses } from "@app/lib/validator";
import { AppConnection } from "@app/services/app-connection/app-connection-enums";
import { ZabbixConnectionMethod } from "./zabbix-connection-enums";
import {
TZabbixConnection,
TZabbixConnectionConfig,
TZabbixHost,
TZabbixHostListResponse
} from "./zabbix-connection-types";
const TRAILING_SLASH_REGEX = new RE2("/+$");
export const getZabbixConnectionListItem = () => {
return {
name: "Zabbix" as const,
app: AppConnection.Zabbix as const,
methods: Object.values(ZabbixConnectionMethod) as [ZabbixConnectionMethod.ApiToken]
};
};
export const validateZabbixConnectionCredentials = async (config: TZabbixConnectionConfig) => {
const { apiToken, instanceUrl } = config.credentials;
await blockLocalAndPrivateIpAddresses(instanceUrl);
try {
const apiUrl = `${instanceUrl.replace(TRAILING_SLASH_REGEX, "")}/api_jsonrpc.php`;
const payload = {
jsonrpc: "2.0",
method: "authentication.get",
params: {
output: "extend"
},
id: 1
};
const response: { data: { error?: { message: string }; result?: string } } = await request.post(apiUrl, payload, {
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${apiToken}`
}
});
if (response.data.error) {
throw new BadRequestError({
message: response.data.error.message
});
}
return config.credentials;
} catch (error) {
if (error instanceof AxiosError) {
throw new BadRequestError({
message: `Failed to connect to Zabbix instance: ${error.message}`
});
}
throw error;
}
};
export const listZabbixHosts = async (appConnection: TZabbixConnection): Promise<TZabbixHost[]> => {
const { apiToken, instanceUrl } = appConnection.credentials;
await blockLocalAndPrivateIpAddresses(instanceUrl);
try {
const apiUrl = `${instanceUrl.replace(TRAILING_SLASH_REGEX, "")}/api_jsonrpc.php`;
const payload = {
jsonrpc: "2.0",
method: "host.get",
params: {
output: ["hostid", "host"],
sortfield: "host",
sortorder: "ASC"
},
id: 1
};
const response: { data: TZabbixHostListResponse } = await request.post(apiUrl, payload, {
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${apiToken}`
}
});
return response.data.result
? response.data.result.map((host) => ({
hostId: host.hostid,
host: host.host
}))
: [];
} catch (error: unknown) {
if (error instanceof AxiosError) {
throw new BadRequestError({
message: `Failed to validate credentials: ${error.message || "Unknown error"}`
});
}
throw new BadRequestError({
message: "Unable to validate connection: verify credentials"
});
}
};
@@ -0,0 +1,62 @@
import z from "zod";
import { AppConnections } from "@app/lib/api-docs";
import { AppConnection } from "@app/services/app-connection/app-connection-enums";
import {
BaseAppConnectionSchema,
GenericCreateAppConnectionFieldsSchema,
GenericUpdateAppConnectionFieldsSchema
} from "@app/services/app-connection/app-connection-schemas";
import { ZabbixConnectionMethod } from "./zabbix-connection-enums";
export const ZabbixConnectionApiTokenCredentialsSchema = z.object({
apiToken: z
.string()
.trim()
.min(1, "API Token required")
.max(1000)
.describe(AppConnections.CREDENTIALS.ZABBIX.apiToken),
instanceUrl: z.string().trim().url("Invalid Instance URL").describe(AppConnections.CREDENTIALS.ZABBIX.instanceUrl)
});
const BaseZabbixConnectionSchema = BaseAppConnectionSchema.extend({ app: z.literal(AppConnection.Zabbix) });
export const ZabbixConnectionSchema = BaseZabbixConnectionSchema.extend({
method: z.literal(ZabbixConnectionMethod.ApiToken),
credentials: ZabbixConnectionApiTokenCredentialsSchema
});
export const SanitizedZabbixConnectionSchema = z.discriminatedUnion("method", [
BaseZabbixConnectionSchema.extend({
method: z.literal(ZabbixConnectionMethod.ApiToken),
credentials: ZabbixConnectionApiTokenCredentialsSchema.pick({ instanceUrl: true })
})
]);
export const ValidateZabbixConnectionCredentialsSchema = z.discriminatedUnion("method", [
z.object({
method: z.literal(ZabbixConnectionMethod.ApiToken).describe(AppConnections.CREATE(AppConnection.Zabbix).method),
credentials: ZabbixConnectionApiTokenCredentialsSchema.describe(
AppConnections.CREATE(AppConnection.Zabbix).credentials
)
})
]);
export const CreateZabbixConnectionSchema = ValidateZabbixConnectionCredentialsSchema.and(
GenericCreateAppConnectionFieldsSchema(AppConnection.Zabbix)
);
export const UpdateZabbixConnectionSchema = z
.object({
credentials: ZabbixConnectionApiTokenCredentialsSchema.optional().describe(
AppConnections.UPDATE(AppConnection.Zabbix).credentials
)
})
.and(GenericUpdateAppConnectionFieldsSchema(AppConnection.Zabbix));
export const ZabbixConnectionListItemSchema = z.object({
name: z.literal("Zabbix"),
app: z.literal(AppConnection.Zabbix),
methods: z.nativeEnum(ZabbixConnectionMethod).array()
});
@@ -0,0 +1,30 @@
import { logger } from "@app/lib/logger";
import { OrgServiceActor } from "@app/lib/types";
import { AppConnection } from "../app-connection-enums";
import { listZabbixHosts } from "./zabbix-connection-fns";
import { TZabbixConnection } from "./zabbix-connection-types";
type TGetAppConnectionFunc = (
app: AppConnection,
connectionId: string,
actor: OrgServiceActor
) => Promise<TZabbixConnection>;
export const zabbixConnectionService = (getAppConnection: TGetAppConnectionFunc) => {
const listHosts = async (connectionId: string, actor: OrgServiceActor) => {
const appConnection = await getAppConnection(AppConnection.Zabbix, connectionId, actor);
try {
const hosts = await listZabbixHosts(appConnection);
return hosts;
} catch (error) {
logger.error(error, "Failed to establish connection with zabbix");
return [];
}
};
return {
listHosts
};
};
@@ -0,0 +1,33 @@
import z from "zod";
import { DiscriminativePick } from "@app/lib/types";
import { AppConnection } from "../app-connection-enums";
import {
CreateZabbixConnectionSchema,
ValidateZabbixConnectionCredentialsSchema,
ZabbixConnectionSchema
} from "./zabbix-connection-schemas";
export type TZabbixConnection = z.infer<typeof ZabbixConnectionSchema>;
export type TZabbixConnectionInput = z.infer<typeof CreateZabbixConnectionSchema> & {
app: AppConnection.Zabbix;
};
export type TValidateZabbixConnectionCredentialsSchema = typeof ValidateZabbixConnectionCredentialsSchema;
export type TZabbixConnectionConfig = DiscriminativePick<TZabbixConnectionInput, "method" | "app" | "credentials"> & {
orgId: string;
};
export type TZabbixHost = {
hostId: string;
host: string;
};
export type TZabbixHostListResponse = {
jsonrpc: string;
result: { hostid: string; host: string }[];
error?: { message: string };
};
@@ -47,7 +47,6 @@ export const dailyResourceCleanUpQueueServiceFactory = ({
queueService.start(QueueName.DailyResourceCleanUp, async () => { queueService.start(QueueName.DailyResourceCleanUp, async () => {
logger.info(`${QueueName.DailyResourceCleanUp}: queue task started`); logger.info(`${QueueName.DailyResourceCleanUp}: queue task started`);
await secretDAL.pruneSecretReminders(queueService); await secretDAL.pruneSecretReminders(queueService);
await auditLogDAL.pruneAuditLog();
await identityAccessTokenDAL.removeExpiredTokens(); await identityAccessTokenDAL.removeExpiredTokens();
await identityUniversalAuthClientSecretDAL.removeExpiredClientSecrets(); await identityUniversalAuthClientSecretDAL.removeExpiredClientSecrets();
await secretSharingDAL.pruneExpiredSharedSecrets(); await secretSharingDAL.pruneExpiredSharedSecrets();
@@ -58,6 +57,7 @@ export const dailyResourceCleanUpQueueServiceFactory = ({
await secretFolderVersionDAL.pruneExcessVersions(); await secretFolderVersionDAL.pruneExcessVersions();
await serviceTokenService.notifyExpiringTokens(); await serviceTokenService.notifyExpiringTokens();
await orgService.notifyInvitedUsers(); await orgService.notifyInvitedUsers();
await auditLogDAL.pruneAuditLog();
logger.info(`${QueueName.DailyResourceCleanUp}: queue task completed`); logger.info(`${QueueName.DailyResourceCleanUp}: queue task completed`);
}); });
@@ -2,7 +2,7 @@ import { ForbiddenError, subject } from "@casl/ability";
import path from "path"; import path from "path";
import { v4 as uuidv4, validate as uuidValidate } from "uuid"; import { v4 as uuidv4, validate as uuidValidate } from "uuid";
import { TSecretFolders, TSecretFoldersInsert } from "@app/db/schemas"; import { TProjectEnvironments, TSecretFolders, TSecretFoldersInsert } from "@app/db/schemas";
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service-types"; import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service-types";
import { ProjectPermissionActions, ProjectPermissionSub } from "@app/ee/services/permission/project-permission"; import { ProjectPermissionActions, ProjectPermissionSub } from "@app/ee/services/permission/project-permission";
import { TSecretApprovalPolicyServiceFactory } from "@app/ee/services/secret-approval-policy/secret-approval-policy-service"; import { TSecretApprovalPolicyServiceFactory } from "@app/ee/services/secret-approval-policy/secret-approval-policy-service";
@@ -469,15 +469,41 @@ export const secretFolderServiceFactory = ({
const $checkFolderPolicy = async ({ const $checkFolderPolicy = async ({
projectId, projectId,
environment, env,
parentId parentId,
idOrName
}: { }: {
projectId: string; projectId: string;
environment: string; env: TProjectEnvironments;
parentId: string; parentId: string;
idOrName: string;
}) => { }) => {
let targetFolder = await folderDAL
.findOne({
envId: env.id,
name: idOrName,
parentId,
isReserved: false
})
.catch(() => null);
if (!targetFolder && uuidValidate(idOrName)) {
targetFolder = await folderDAL
.findOne({
envId: env.id,
id: idOrName,
parentId,
isReserved: false
})
.catch(() => null);
}
if (!targetFolder) {
throw new NotFoundError({ message: `Target folder not found` });
}
// get environment root folder (as it's needed to get all folders under it) // get environment root folder (as it's needed to get all folders under it)
const rootFolder = await folderDAL.findBySecretPath(projectId, environment, "/"); const rootFolder = await folderDAL.findBySecretPath(projectId, env.slug, "/");
if (!rootFolder) throw new NotFoundError({ message: `Root folder not found` }); if (!rootFolder) throw new NotFoundError({ message: `Root folder not found` });
// get all folders under environment root folder // get all folders under environment root folder
const folderPaths = await folderDAL.findByEnvsDeep({ parentIds: [rootFolder.id] }); const folderPaths = await folderDAL.findByEnvsDeep({ parentIds: [rootFolder.id] });
@@ -492,7 +518,13 @@ export const secretFolderServiceFactory = ({
folderMap.get(normalizeKey(folder.parentId))?.push(folder); folderMap.get(normalizeKey(folder.parentId))?.push(folder);
} }
// Recursively collect all folders under the given parentId // Find the target folder in the folderPaths to get its full details
const targetFolderWithPath = folderPaths.find((f) => f.id === targetFolder!.id);
if (!targetFolderWithPath) {
throw new NotFoundError({ message: `Target folder path not found` });
}
// Recursively collect all folders under the target folder (descendants only)
const collectDescendants = ( const collectDescendants = (
id: string id: string
): (TSecretFolders & { path: string; depth: number; environment: string })[] => { ): (TSecretFolders & { path: string; depth: number; environment: string })[] => {
@@ -500,23 +532,31 @@ export const secretFolderServiceFactory = ({
return [...children, ...children.flatMap((child) => collectDescendants(child.id))]; return [...children, ...children.flatMap((child) => collectDescendants(child.id))];
}; };
const foldersUnderParent = collectDescendants(parentId); const targetFolderDescendants = collectDescendants(targetFolder.id);
const folderPolicyPaths = foldersUnderParent.map((folder) => ({ // Include the target folder itself plus all its descendants
const foldersToCheck = [targetFolderWithPath, ...targetFolderDescendants];
const folderPolicyPaths = foldersToCheck.map((folder) => ({
path: folder.path, path: folder.path,
id: folder.id id: folder.id
})); }));
// get secrets under the given folders // get secrets under the given folders
const secrets = await secretV2BridgeDAL.findByFolderIds({ folderIds: folderPolicyPaths.map((p) => p.id) }); const secrets = await secretV2BridgeDAL.findByFolderIds({
folderIds: folderPolicyPaths.map((p) => p.id)
});
for await (const folderPolicyPath of folderPolicyPaths) { for await (const folderPolicyPath of folderPolicyPaths) {
// eslint-disable-next-line no-continue // eslint-disable-next-line no-continue
if (!secrets.some((s) => s.folderId === folderPolicyPath.id)) continue; if (!secrets.some((s) => s.folderId === folderPolicyPath.id)) continue;
const policy = await secretApprovalPolicyService.getSecretApprovalPolicy( const policy = await secretApprovalPolicyService.getSecretApprovalPolicy(
projectId, projectId,
environment, env.slug,
folderPolicyPath.path folderPolicyPath.path
); );
// if there is a policy and there are secrets under the given folder, throw error // if there is a policy and there are secrets under the given folder, throw error
if (policy) { if (policy) {
throw new BadRequestError({ throw new BadRequestError({
@@ -560,20 +600,42 @@ export const secretFolderServiceFactory = ({
message: `Folder with path '${secretPath}' in environment with slug '${environment}' not found` message: `Folder with path '${secretPath}' in environment with slug '${environment}' not found`
}); });
await $checkFolderPolicy({ projectId, environment, parentId: parentFolder.id }); await $checkFolderPolicy({ projectId, env, parentId: parentFolder.id, idOrName });
let folderToDelete = await folderDAL
.findOne({
envId: env.id,
name: idOrName,
parentId: parentFolder.id,
isReserved: false
})
.catch(() => null);
if (!folderToDelete && uuidValidate(idOrName)) {
folderToDelete = await folderDAL
.findOne({
envId: env.id,
id: idOrName,
parentId: parentFolder.id,
isReserved: false
})
.catch(() => null);
}
if (!folderToDelete) {
throw new NotFoundError({ message: `Folder with ID '${idOrName}' not found` });
}
const [doc] = await folderDAL.delete( const [doc] = await folderDAL.delete(
{ {
envId: env.id, envId: env.id,
[uuidValidate(idOrName) ? "id" : "name"]: idOrName, id: folderToDelete.id,
parentId: parentFolder.id, parentId: parentFolder.id,
isReserved: false isReserved: false
}, },
tx tx
); );
if (!doc) throw new NotFoundError({ message: `Failed to delete folder with ID '${idOrName}', not found` });
const folderVersions = await folderVersionDAL.findLatestFolderVersions([doc.id], tx); const folderVersions = await folderVersionDAL.findLatestFolderVersions([doc.id], tx);
await folderCommitService.createCommit( await folderCommitService.createCommit(
@@ -20,7 +20,8 @@ export enum SecretSync {
Render = "render", Render = "render",
Flyio = "flyio", Flyio = "flyio",
GitLab = "gitlab", GitLab = "gitlab",
CloudflarePages = "cloudflare-pages" CloudflarePages = "cloudflare-pages",
Zabbix = "zabbix"
} }
export enum SecretSyncInitialSyncBehavior { export enum SecretSyncInitialSyncBehavior {
@@ -45,6 +45,7 @@ import { TEAMCITY_SYNC_LIST_OPTION, TeamCitySyncFns } from "./teamcity";
import { TERRAFORM_CLOUD_SYNC_LIST_OPTION, TerraformCloudSyncFns } from "./terraform-cloud"; import { TERRAFORM_CLOUD_SYNC_LIST_OPTION, TerraformCloudSyncFns } from "./terraform-cloud";
import { VERCEL_SYNC_LIST_OPTION, VercelSyncFns } from "./vercel"; import { VERCEL_SYNC_LIST_OPTION, VercelSyncFns } from "./vercel";
import { WINDMILL_SYNC_LIST_OPTION, WindmillSyncFns } from "./windmill"; import { WINDMILL_SYNC_LIST_OPTION, WindmillSyncFns } from "./windmill";
import { ZABBIX_SYNC_LIST_OPTION, ZabbixSyncFns } from "./zabbix";
const SECRET_SYNC_LIST_OPTIONS: Record<SecretSync, TSecretSyncListItem> = { const SECRET_SYNC_LIST_OPTIONS: Record<SecretSync, TSecretSyncListItem> = {
[SecretSync.AWSParameterStore]: AWS_PARAMETER_STORE_SYNC_LIST_OPTION, [SecretSync.AWSParameterStore]: AWS_PARAMETER_STORE_SYNC_LIST_OPTION,
@@ -68,7 +69,8 @@ const SECRET_SYNC_LIST_OPTIONS: Record<SecretSync, TSecretSyncListItem> = {
[SecretSync.Render]: RENDER_SYNC_LIST_OPTION, [SecretSync.Render]: RENDER_SYNC_LIST_OPTION,
[SecretSync.Flyio]: FLYIO_SYNC_LIST_OPTION, [SecretSync.Flyio]: FLYIO_SYNC_LIST_OPTION,
[SecretSync.GitLab]: GITLAB_SYNC_LIST_OPTION, [SecretSync.GitLab]: GITLAB_SYNC_LIST_OPTION,
[SecretSync.CloudflarePages]: CLOUDFLARE_PAGES_SYNC_LIST_OPTION [SecretSync.CloudflarePages]: CLOUDFLARE_PAGES_SYNC_LIST_OPTION,
[SecretSync.Zabbix]: ZABBIX_SYNC_LIST_OPTION
}; };
export const listSecretSyncOptions = () => { export const listSecretSyncOptions = () => {
@@ -236,6 +238,8 @@ export const SecretSyncFns = {
return GitLabSyncFns.syncSecrets(secretSync, schemaSecretMap, { appConnectionDAL, kmsService }); return GitLabSyncFns.syncSecrets(secretSync, schemaSecretMap, { appConnectionDAL, kmsService });
case SecretSync.CloudflarePages: case SecretSync.CloudflarePages:
return CloudflarePagesSyncFns.syncSecrets(secretSync, schemaSecretMap); return CloudflarePagesSyncFns.syncSecrets(secretSync, schemaSecretMap);
case SecretSync.Zabbix:
return ZabbixSyncFns.syncSecrets(secretSync, schemaSecretMap);
default: default:
throw new Error( throw new Error(
`Unhandled sync destination for sync secrets fns: ${(secretSync as TSecretSyncWithCredentials).destination}` `Unhandled sync destination for sync secrets fns: ${(secretSync as TSecretSyncWithCredentials).destination}`
@@ -328,6 +332,9 @@ export const SecretSyncFns = {
case SecretSync.CloudflarePages: case SecretSync.CloudflarePages:
secretMap = await CloudflarePagesSyncFns.getSecrets(secretSync); secretMap = await CloudflarePagesSyncFns.getSecrets(secretSync);
break; break;
case SecretSync.Zabbix:
secretMap = await ZabbixSyncFns.getSecrets(secretSync);
break;
default: default:
throw new Error( throw new Error(
`Unhandled sync destination for get secrets fns: ${(secretSync as TSecretSyncWithCredentials).destination}` `Unhandled sync destination for get secrets fns: ${(secretSync as TSecretSyncWithCredentials).destination}`
@@ -405,6 +412,8 @@ export const SecretSyncFns = {
return GitLabSyncFns.removeSecrets(secretSync, schemaSecretMap, { appConnectionDAL, kmsService }); return GitLabSyncFns.removeSecrets(secretSync, schemaSecretMap, { appConnectionDAL, kmsService });
case SecretSync.CloudflarePages: case SecretSync.CloudflarePages:
return CloudflarePagesSyncFns.removeSecrets(secretSync, schemaSecretMap); return CloudflarePagesSyncFns.removeSecrets(secretSync, schemaSecretMap);
case SecretSync.Zabbix:
return ZabbixSyncFns.removeSecrets(secretSync, schemaSecretMap);
default: default:
throw new Error( throw new Error(
`Unhandled sync destination for remove secrets fns: ${(secretSync as TSecretSyncWithCredentials).destination}` `Unhandled sync destination for remove secrets fns: ${(secretSync as TSecretSyncWithCredentials).destination}`
@@ -23,7 +23,8 @@ export const SECRET_SYNC_NAME_MAP: Record<SecretSync, string> = {
[SecretSync.Render]: "Render", [SecretSync.Render]: "Render",
[SecretSync.Flyio]: "Fly.io", [SecretSync.Flyio]: "Fly.io",
[SecretSync.GitLab]: "GitLab", [SecretSync.GitLab]: "GitLab",
[SecretSync.CloudflarePages]: "Cloudflare Pages" [SecretSync.CloudflarePages]: "Cloudflare Pages",
[SecretSync.Zabbix]: "Zabbix"
}; };
export const SECRET_SYNC_CONNECTION_MAP: Record<SecretSync, AppConnection> = { export const SECRET_SYNC_CONNECTION_MAP: Record<SecretSync, AppConnection> = {
@@ -48,7 +49,8 @@ export const SECRET_SYNC_CONNECTION_MAP: Record<SecretSync, AppConnection> = {
[SecretSync.Render]: AppConnection.Render, [SecretSync.Render]: AppConnection.Render,
[SecretSync.Flyio]: AppConnection.Flyio, [SecretSync.Flyio]: AppConnection.Flyio,
[SecretSync.GitLab]: AppConnection.GitLab, [SecretSync.GitLab]: AppConnection.GitLab,
[SecretSync.CloudflarePages]: AppConnection.Cloudflare [SecretSync.CloudflarePages]: AppConnection.Cloudflare,
[SecretSync.Zabbix]: AppConnection.Zabbix
}; };
export const SECRET_SYNC_PLAN_MAP: Record<SecretSync, SecretSyncPlanType> = { export const SECRET_SYNC_PLAN_MAP: Record<SecretSync, SecretSyncPlanType> = {
@@ -73,5 +75,6 @@ export const SECRET_SYNC_PLAN_MAP: Record<SecretSync, SecretSyncPlanType> = {
[SecretSync.Render]: SecretSyncPlanType.Regular, [SecretSync.Render]: SecretSyncPlanType.Regular,
[SecretSync.Flyio]: SecretSyncPlanType.Regular, [SecretSync.Flyio]: SecretSyncPlanType.Regular,
[SecretSync.GitLab]: SecretSyncPlanType.Regular, [SecretSync.GitLab]: SecretSyncPlanType.Regular,
[SecretSync.CloudflarePages]: SecretSyncPlanType.Regular [SecretSync.CloudflarePages]: SecretSyncPlanType.Regular,
[SecretSync.Zabbix]: SecretSyncPlanType.Regular
}; };
@@ -113,6 +113,7 @@ import {
TTerraformCloudSyncWithCredentials TTerraformCloudSyncWithCredentials
} from "./terraform-cloud"; } from "./terraform-cloud";
import { TVercelSync, TVercelSyncInput, TVercelSyncListItem, TVercelSyncWithCredentials } from "./vercel"; import { TVercelSync, TVercelSyncInput, TVercelSyncListItem, TVercelSyncWithCredentials } from "./vercel";
import { TZabbixSync, TZabbixSyncInput, TZabbixSyncListItem, TZabbixSyncWithCredentials } from "./zabbix";
export type TSecretSync = export type TSecretSync =
| TAwsParameterStoreSync | TAwsParameterStoreSync
@@ -136,7 +137,8 @@ export type TSecretSync =
| TRenderSync | TRenderSync
| TFlyioSync | TFlyioSync
| TGitLabSync | TGitLabSync
| TCloudflarePagesSync; | TCloudflarePagesSync
| TZabbixSync;
export type TSecretSyncWithCredentials = export type TSecretSyncWithCredentials =
| TAwsParameterStoreSyncWithCredentials | TAwsParameterStoreSyncWithCredentials
@@ -160,7 +162,8 @@ export type TSecretSyncWithCredentials =
| TRenderSyncWithCredentials | TRenderSyncWithCredentials
| TFlyioSyncWithCredentials | TFlyioSyncWithCredentials
| TGitLabSyncWithCredentials | TGitLabSyncWithCredentials
| TCloudflarePagesSyncWithCredentials; | TCloudflarePagesSyncWithCredentials
| TZabbixSyncWithCredentials;
export type TSecretSyncInput = export type TSecretSyncInput =
| TAwsParameterStoreSyncInput | TAwsParameterStoreSyncInput
@@ -184,7 +187,8 @@ export type TSecretSyncInput =
| TRenderSyncInput | TRenderSyncInput
| TFlyioSyncInput | TFlyioSyncInput
| TGitLabSyncInput | TGitLabSyncInput
| TCloudflarePagesSyncInput; | TCloudflarePagesSyncInput
| TZabbixSyncInput;
export type TSecretSyncListItem = export type TSecretSyncListItem =
| TAwsParameterStoreSyncListItem | TAwsParameterStoreSyncListItem
@@ -208,7 +212,8 @@ export type TSecretSyncListItem =
| TRenderSyncListItem | TRenderSyncListItem
| TFlyioSyncListItem | TFlyioSyncListItem
| TGitLabSyncListItem | TGitLabSyncListItem
| TCloudflarePagesSyncListItem; | TCloudflarePagesSyncListItem
| TZabbixSyncListItem;
export type TSyncOptionsConfig = { export type TSyncOptionsConfig = {
canImportSecrets: boolean; canImportSecrets: boolean;
@@ -0,0 +1,5 @@
export * from "./zabbix-sync-constants";
export * from "./zabbix-sync-enums";
export * from "./zabbix-sync-fns";
export * from "./zabbix-sync-schemas";
export * from "./zabbix-sync-types";
@@ -0,0 +1,10 @@
import { AppConnection } from "@app/services/app-connection/app-connection-enums";
import { SecretSync } from "@app/services/secret-sync/secret-sync-enums";
import { TSecretSyncListItem } from "@app/services/secret-sync/secret-sync-types";
export const ZABBIX_SYNC_LIST_OPTION: TSecretSyncListItem = {
name: "Zabbix",
destination: SecretSync.Zabbix,
connection: AppConnection.Zabbix,
canImportSecrets: true
};
@@ -0,0 +1,4 @@
export enum ZabbixSyncScope {
Global = "global",
Host = "host"
}
@@ -0,0 +1,285 @@
import RE2 from "re2";
import { request } from "@app/lib/config/request";
import { blockLocalAndPrivateIpAddresses } from "@app/lib/validator";
import { SecretSyncError } from "@app/services/secret-sync/secret-sync-errors";
import { matchesSchema } from "@app/services/secret-sync/secret-sync-fns";
import { TSecretMap } from "@app/services/secret-sync/secret-sync-types";
import {
TZabbixSecret,
TZabbixSyncWithCredentials,
ZabbixApiResponse,
ZabbixMacroCreateResponse,
ZabbixMacroDeleteResponse
} from "@app/services/secret-sync/zabbix/zabbix-sync-types";
import { ZabbixSyncScope } from "./zabbix-sync-enums";
const TRAILING_SLASH_REGEX = new RE2("/+$");
const MACRO_START_REGEX = new RE2("^\\{\\$");
const MACRO_END_REGEX = new RE2("\\}$");
const extractMacroKey = (macro: string): string => {
return macro.replace(MACRO_START_REGEX, "").replace(MACRO_END_REGEX, "");
};
// Helper function to handle Zabbix API responses and errors
const handleZabbixResponse = <T>(response: ZabbixApiResponse<T>): T => {
if (response.data.error) {
const errorMessage = response.data.error.data
? `${response.data.error.message}: ${response.data.error.data}`
: response.data.error.message;
throw new SecretSyncError({
error: new Error(`Zabbix API Error (${response.data.error.code}): ${errorMessage}`)
});
}
if (response.data.result === undefined) {
throw new SecretSyncError({
error: new Error("Zabbix API returned no result")
});
}
return response.data.result;
};
const listZabbixSecrets = async (apiToken: string, instanceUrl: string, hostId?: string): Promise<TZabbixSecret[]> => {
const apiUrl = `${instanceUrl.replace(TRAILING_SLASH_REGEX, "")}/api_jsonrpc.php`;
// - jsonrpc: Specifies the JSON-RPC protocol version.
// - method: The API method to call, in this case "usermacro.get" for retrieving user macros.
// - id: A unique identifier for the request. Required by JSON-RPC but not used by the API for logic. Typically set to any integer.
const payload = {
jsonrpc: "2.0" as const,
method: "usermacro.get",
params: hostId ? { output: "extend", hostids: hostId } : { output: "extend", globalmacro: true },
id: 1
};
try {
const response: ZabbixApiResponse<TZabbixSecret[]> = await request.post(apiUrl, payload, {
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${apiToken}`
}
});
return handleZabbixResponse(response) || [];
} catch (error) {
throw new SecretSyncError({
error: error instanceof Error ? error : new Error("Failed to list Zabbix secrets")
});
}
};
const putZabbixSecrets = async (
apiToken: string,
instanceUrl: string,
secretMap: TSecretMap,
destinationConfig: TZabbixSyncWithCredentials["destinationConfig"],
existingSecrets: TZabbixSecret[]
): Promise<void> => {
const apiUrl = `${instanceUrl.replace(TRAILING_SLASH_REGEX, "")}/api_jsonrpc.php`;
const hostId = destinationConfig.scope === ZabbixSyncScope.Host ? destinationConfig.hostId : undefined;
const existingMacroMap = new Map(existingSecrets.map((secret) => [secret.macro, secret]));
for (const [key, secret] of Object.entries(secretMap)) {
const macroKey = `{$${key.toUpperCase()}}`;
const existingMacro = existingMacroMap.get(macroKey);
try {
if (existingMacro) {
// Update existing macro
const updatePayload = {
jsonrpc: "2.0" as const,
method: hostId ? "usermacro.update" : "usermacro.updateglobal",
params: {
[hostId ? "hostmacroid" : "globalmacroid"]: existingMacro[hostId ? "hostmacroid" : "globalmacroid"],
value: secret.value,
type: destinationConfig.macroType,
description: secret.comment
},
id: 1
};
// eslint-disable-next-line no-await-in-loop
const response: ZabbixApiResponse<ZabbixMacroCreateResponse> = await request.post(apiUrl, updatePayload, {
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${apiToken}`
}
});
handleZabbixResponse(response);
} else {
// Create new macro
const createPayload = {
jsonrpc: "2.0" as const,
method: hostId ? "usermacro.create" : "usermacro.createglobal",
params: hostId
? {
hostid: hostId,
macro: macroKey,
value: secret.value,
type: destinationConfig.macroType,
description: secret.comment
}
: {
macro: macroKey,
value: secret.value,
type: destinationConfig.macroType,
description: secret.comment
},
id: 1
};
// eslint-disable-next-line no-await-in-loop
const response: ZabbixApiResponse<ZabbixMacroCreateResponse> = await request.post(apiUrl, createPayload, {
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${apiToken}`
}
});
handleZabbixResponse(response);
}
} catch (error) {
throw new SecretSyncError({
error: error instanceof Error ? error : new Error(`Failed to sync secret ${key}`)
});
}
}
};
const deleteZabbixSecrets = async (
apiToken: string,
instanceUrl: string,
keys: string[],
hostId?: string
): Promise<void> => {
if (keys.length === 0) return;
const apiUrl = `${instanceUrl.replace(TRAILING_SLASH_REGEX, "")}/api_jsonrpc.php`;
try {
// Get existing macros to find their IDs
const existingSecrets = await listZabbixSecrets(apiToken, instanceUrl, hostId);
const macroIds = existingSecrets
.filter((secret) => keys.includes(secret.macro))
.map((secret) => secret[hostId ? "hostmacroid" : "globalmacroid"])
.filter(Boolean);
if (macroIds.length === 0) return;
const payload = {
jsonrpc: "2.0" as const,
method: hostId ? "usermacro.delete" : "usermacro.deleteglobal",
params: macroIds,
id: 1
};
const response: ZabbixApiResponse<ZabbixMacroDeleteResponse> = await request.post(apiUrl, payload, {
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${apiToken}`
}
});
handleZabbixResponse(response);
} catch (error) {
throw new SecretSyncError({
error: error instanceof Error ? error : new Error("Failed to delete Zabbix secrets")
});
}
};
export const ZabbixSyncFns = {
syncSecrets: async (secretSync: TZabbixSyncWithCredentials, secretMap: TSecretMap) => {
const { connection, environment, destinationConfig } = secretSync;
const { apiToken, instanceUrl } = connection.credentials;
await blockLocalAndPrivateIpAddresses(instanceUrl);
const hostId = destinationConfig.scope === ZabbixSyncScope.Host ? destinationConfig.hostId : undefined;
let secrets: TZabbixSecret[] = [];
try {
secrets = await listZabbixSecrets(apiToken, instanceUrl, hostId);
} catch (error) {
throw new SecretSyncError({
error: error instanceof Error ? error : new Error("Failed to list Zabbix secrets")
});
}
try {
await putZabbixSecrets(apiToken, instanceUrl, secretMap, destinationConfig, secrets);
} catch (error) {
throw new SecretSyncError({
error: error instanceof Error ? error : new Error("Failed to sync secrets")
});
}
if (secretSync.syncOptions.disableSecretDeletion) return;
try {
const shapedSecretMapKeys = Object.keys(secretMap).map((key) => key.toUpperCase());
const keys = secrets
.filter(
(secret) =>
matchesSchema(secret.macro, environment?.slug || "", secretSync.syncOptions.keySchema) &&
!shapedSecretMapKeys.includes(extractMacroKey(secret.macro))
)
.map((secret) => secret.macro);
await deleteZabbixSecrets(apiToken, instanceUrl, keys, hostId);
} catch (error) {
throw new SecretSyncError({
error: error instanceof Error ? error : new Error("Failed to delete orphaned secrets")
});
}
},
removeSecrets: async (secretSync: TZabbixSyncWithCredentials, secretMap: TSecretMap) => {
const { connection, destinationConfig } = secretSync;
const { apiToken, instanceUrl } = connection.credentials;
await blockLocalAndPrivateIpAddresses(instanceUrl);
const hostId = destinationConfig.scope === ZabbixSyncScope.Host ? destinationConfig.hostId : undefined;
try {
const secrets = await listZabbixSecrets(apiToken, instanceUrl, hostId);
const shapedSecretMapKeys = Object.keys(secretMap).map((key) => key.toUpperCase());
const keys = secrets
.filter((secret) => shapedSecretMapKeys.includes(extractMacroKey(secret.macro)))
.map((secret) => secret.macro);
await deleteZabbixSecrets(apiToken, instanceUrl, keys, hostId);
} catch (error) {
throw new SecretSyncError({
error: error instanceof Error ? error : new Error("Failed to remove secrets")
});
}
},
getSecrets: async (secretSync: TZabbixSyncWithCredentials) => {
const { connection, destinationConfig } = secretSync;
const { apiToken, instanceUrl } = connection.credentials;
await blockLocalAndPrivateIpAddresses(instanceUrl);
const hostId = destinationConfig.scope === ZabbixSyncScope.Host ? destinationConfig.hostId : undefined;
try {
const secrets = await listZabbixSecrets(apiToken, instanceUrl, hostId);
return Object.fromEntries(
secrets.map((secret) => [
extractMacroKey(secret.macro),
{ value: secret.value ?? "", comment: secret.description }
])
);
} catch (error) {
throw new SecretSyncError({
error: error instanceof Error ? error : new Error("Failed to get secrets")
});
}
}
};
@@ -0,0 +1,67 @@
import { z } from "zod";
import { SecretSyncs } from "@app/lib/api-docs";
import { AppConnection } from "@app/services/app-connection/app-connection-enums";
import { SecretSync } from "@app/services/secret-sync/secret-sync-enums";
import {
BaseSecretSyncSchema,
GenericCreateSecretSyncFieldsSchema,
GenericUpdateSecretSyncFieldsSchema
} from "@app/services/secret-sync/secret-sync-schemas";
import { TSyncOptionsConfig } from "@app/services/secret-sync/secret-sync-types";
import { ZabbixSyncScope } from "./zabbix-sync-enums";
const ZabbixSyncDestinationConfigSchema = z.discriminatedUnion("scope", [
z.object({
scope: z.literal(ZabbixSyncScope.Host).describe(SecretSyncs.DESTINATION_CONFIG.ZABBIX.scope),
hostId: z.string().trim().min(1, "Host required").max(255).describe(SecretSyncs.DESTINATION_CONFIG.ZABBIX.hostId),
hostName: z
.string()
.trim()
.min(1, "Host name required")
.max(255)
.describe(SecretSyncs.DESTINATION_CONFIG.ZABBIX.hostName),
macroType: z
.number()
.min(0, "Macro type required")
.max(1, "Macro type required")
.describe(SecretSyncs.DESTINATION_CONFIG.ZABBIX.macroType)
}),
z.object({
scope: z.literal(ZabbixSyncScope.Global).describe(SecretSyncs.DESTINATION_CONFIG.ZABBIX.scope),
macroType: z
.number()
.min(0, "Macro type required")
.max(1, "Macro type required")
.describe(SecretSyncs.DESTINATION_CONFIG.ZABBIX.macroType)
})
]);
const ZabbixSyncOptionsConfig: TSyncOptionsConfig = { canImportSecrets: true };
export const ZabbixSyncSchema = BaseSecretSyncSchema(SecretSync.Zabbix, ZabbixSyncOptionsConfig).extend({
destination: z.literal(SecretSync.Zabbix),
destinationConfig: ZabbixSyncDestinationConfigSchema
});
export const CreateZabbixSyncSchema = GenericCreateSecretSyncFieldsSchema(
SecretSync.Zabbix,
ZabbixSyncOptionsConfig
).extend({
destinationConfig: ZabbixSyncDestinationConfigSchema
});
export const UpdateZabbixSyncSchema = GenericUpdateSecretSyncFieldsSchema(
SecretSync.Zabbix,
ZabbixSyncOptionsConfig
).extend({
destinationConfig: ZabbixSyncDestinationConfigSchema.optional()
});
export const ZabbixSyncListItemSchema = z.object({
name: z.literal("Zabbix"),
connection: z.literal(AppConnection.Zabbix),
destination: z.literal(SecretSync.Zabbix),
canImportSecrets: z.literal(true)
});
@@ -0,0 +1,75 @@
import { z } from "zod";
import { TZabbixConnection } from "@app/services/app-connection/zabbix";
import { CreateZabbixSyncSchema, ZabbixSyncListItemSchema, ZabbixSyncSchema } from "./zabbix-sync-schemas";
export type TZabbixSync = z.infer<typeof ZabbixSyncSchema>;
export type TZabbixSyncInput = z.infer<typeof CreateZabbixSyncSchema>;
export type TZabbixSyncListItem = z.infer<typeof ZabbixSyncListItemSchema>;
export type TZabbixSyncWithCredentials = TZabbixSync & {
connection: TZabbixConnection;
};
export type TZabbixSecret = {
macro: string;
value: string;
description?: string;
globalmacroid?: string;
hostmacroid?: string;
hostid?: string;
type: number;
automatic?: string;
};
export interface ZabbixApiResponse<T = unknown> {
data: {
jsonrpc: "2.0";
result?: T;
error?: {
code: number;
message: string;
data?: string;
};
id: number;
};
}
export interface ZabbixMacroCreateResponse {
hostmacroids?: string[];
globalmacroids?: string[];
}
export interface ZabbixMacroUpdateResponse {
hostmacroids?: string[];
globalmacroids?: string[];
}
export interface ZabbixMacroDeleteResponse {
hostmacroids?: string[];
globalmacroids?: string[];
}
export enum ZabbixMacroType {
TEXT = 0,
SECRET = 1
}
export interface ZabbixMacroInput {
hostid?: string;
macro: string;
value: string;
description?: string;
type?: ZabbixMacroType;
automatic?: "0" | "1";
}
export interface ZabbixMacroUpdate {
hostmacroid?: string;
globalmacroid?: string;
value?: string;
description?: string;
type?: ZabbixMacroType;
automatic?: "0" | "1";
}
@@ -5,7 +5,13 @@ import jwt from "jsonwebtoken";
import { IdentityAuthMethod, OrgMembershipRole, TSuperAdmin, TSuperAdminUpdate } from "@app/db/schemas"; import { IdentityAuthMethod, OrgMembershipRole, TSuperAdmin, TSuperAdminUpdate } from "@app/db/schemas";
import { TLicenseServiceFactory } from "@app/ee/services/license/license-service"; import { TLicenseServiceFactory } from "@app/ee/services/license/license-service";
import { PgSqlLock, TKeyStoreFactory } from "@app/keystore/keystore"; import { PgSqlLock, TKeyStoreFactory } from "@app/keystore/keystore";
import { getConfig } from "@app/lib/config/env"; import {
getConfig,
getOriginalConfig,
overrideEnvConfig,
overwriteSchema,
validateOverrides
} from "@app/lib/config/env";
import { infisicalSymmetricEncypt } from "@app/lib/crypto/encryption"; import { infisicalSymmetricEncypt } from "@app/lib/crypto/encryption";
import { generateUserSrpKeys, getUserPrivateKey } from "@app/lib/crypto/srp"; import { generateUserSrpKeys, getUserPrivateKey } from "@app/lib/crypto/srp";
import { BadRequestError, NotFoundError } from "@app/lib/errors"; import { BadRequestError, NotFoundError } from "@app/lib/errors";
@@ -33,6 +39,7 @@ import { TInvalidateCacheQueueFactory } from "./invalidate-cache-queue";
import { TSuperAdminDALFactory } from "./super-admin-dal"; import { TSuperAdminDALFactory } from "./super-admin-dal";
import { import {
CacheType, CacheType,
EnvOverrides,
LoginMethod, LoginMethod,
TAdminBootstrapInstanceDTO, TAdminBootstrapInstanceDTO,
TAdminGetIdentitiesDTO, TAdminGetIdentitiesDTO,
@@ -234,6 +241,45 @@ export const superAdminServiceFactory = ({
adminIntegrationsConfig = config; adminIntegrationsConfig = config;
}; };
const getEnvOverrides = async () => {
const serverCfg = await serverCfgDAL.findById(ADMIN_CONFIG_DB_UUID);
if (!serverCfg || !serverCfg.encryptedEnvOverrides) {
return {};
}
const decrypt = kmsService.decryptWithRootKey();
const overrides = JSON.parse(decrypt(serverCfg.encryptedEnvOverrides).toString()) as Record<string, string>;
return overrides;
};
const getEnvOverridesOrganized = async (): Promise<EnvOverrides> => {
const overrides = await getEnvOverrides();
const ogConfig = getOriginalConfig();
return Object.fromEntries(
Object.entries(overwriteSchema).map(([groupKey, groupDef]) => [
groupKey,
{
name: groupDef.name,
fields: groupDef.fields.map(({ key, description }) => ({
key,
description,
value: overrides[key] || "",
hasEnvEntry: !!(ogConfig as unknown as Record<string, string | undefined>)[key]
}))
}
])
);
};
const $syncEnvConfig = async () => {
const config = await getEnvOverrides();
overrideEnvConfig(config);
};
const updateServerCfg = async ( const updateServerCfg = async (
data: TSuperAdminUpdate & { data: TSuperAdminUpdate & {
slackClientId?: string; slackClientId?: string;
@@ -246,6 +292,7 @@ export const superAdminServiceFactory = ({
gitHubAppConnectionSlug?: string; gitHubAppConnectionSlug?: string;
gitHubAppConnectionId?: string; gitHubAppConnectionId?: string;
gitHubAppConnectionPrivateKey?: string; gitHubAppConnectionPrivateKey?: string;
envOverrides?: Record<string, string>;
}, },
userId: string userId: string
) => { ) => {
@@ -374,6 +421,17 @@ export const superAdminServiceFactory = ({
gitHubAppConnectionSettingsUpdated = true; gitHubAppConnectionSettingsUpdated = true;
} }
let envOverridesUpdated = false;
if (data.envOverrides !== undefined) {
// Verify input format
validateOverrides(data.envOverrides);
const encryptedEnvOverrides = encryptWithRoot(Buffer.from(JSON.stringify(data.envOverrides)));
updatedData.encryptedEnvOverrides = encryptedEnvOverrides;
updatedData.envOverrides = undefined;
envOverridesUpdated = true;
}
const updatedServerCfg = await serverCfgDAL.updateById(ADMIN_CONFIG_DB_UUID, updatedData); const updatedServerCfg = await serverCfgDAL.updateById(ADMIN_CONFIG_DB_UUID, updatedData);
await keyStore.setItemWithExpiry(ADMIN_CONFIG_KEY, ADMIN_CONFIG_KEY_EXP, JSON.stringify(updatedServerCfg)); await keyStore.setItemWithExpiry(ADMIN_CONFIG_KEY, ADMIN_CONFIG_KEY_EXP, JSON.stringify(updatedServerCfg));
@@ -382,6 +440,10 @@ export const superAdminServiceFactory = ({
await $syncAdminIntegrationConfig(); await $syncAdminIntegrationConfig();
} }
if (envOverridesUpdated) {
await $syncEnvConfig();
}
if ( if (
updatedServerCfg.encryptedMicrosoftTeamsAppId && updatedServerCfg.encryptedMicrosoftTeamsAppId &&
updatedServerCfg.encryptedMicrosoftTeamsClientSecret && updatedServerCfg.encryptedMicrosoftTeamsClientSecret &&
@@ -814,6 +876,18 @@ export const superAdminServiceFactory = ({
return job; return job;
}; };
const initializeEnvConfigSync = async () => {
logger.info("Setting up background sync process for environment overrides");
await $syncEnvConfig();
// sync every 5 minutes
const job = new CronJob("*/5 * * * *", $syncEnvConfig);
job.start();
return job;
};
return { return {
initServerCfg, initServerCfg,
updateServerCfg, updateServerCfg,
@@ -833,6 +907,9 @@ export const superAdminServiceFactory = ({
getOrganizations, getOrganizations,
deleteOrganization, deleteOrganization,
deleteOrganizationMembership, deleteOrganizationMembership,
initializeAdminIntegrationConfigSync initializeAdminIntegrationConfigSync,
initializeEnvConfigSync,
getEnvOverrides,
getEnvOverridesOrganized
}; };
}; };
@@ -1,3 +1,5 @@
import { TEnvConfig } from "@app/lib/config/env";
export type TAdminSignUpDTO = { export type TAdminSignUpDTO = {
email: string; email: string;
password: string; password: string;
@@ -74,3 +76,10 @@ export type TAdminIntegrationConfig = {
privateKey: string; privateKey: string;
}; };
}; };
export interface EnvOverrides {
[key: string]: {
name: string;
fields: { key: keyof TEnvConfig; value: string; hasEnvEntry: boolean; description?: string }[];
};
}
@@ -0,0 +1,4 @@
---
title: "Available"
openapi: "GET /api/v1/app-connections/zabbix/available"
---
@@ -0,0 +1,8 @@
---
title: "Create"
openapi: "POST /api/v1/app-connections/zabbix"
---
<Note>
Check out the configuration docs for [Zabbix Connections](/integrations/app-connections/zabbix) to learn how to obtain the required credentials.
</Note>
@@ -0,0 +1,4 @@
---
title: "Delete"
openapi: "DELETE /api/v1/app-connections/zabbix/{connectionId}"
---
@@ -0,0 +1,4 @@
---
title: "Get by ID"
openapi: "GET /api/v1/app-connections/zabbix/{connectionId}"
---
@@ -0,0 +1,4 @@
---
title: "Get by Name"
openapi: "GET /api/v1/app-connections/zabbix/connection-name/{connectionName}"
---
@@ -0,0 +1,4 @@
---
title: "List"
openapi: "GET /api/v1/app-connections/zabbix"
---
@@ -0,0 +1,8 @@
---
title: "Update"
openapi: "PATCH /api/v1/app-connections/zabbix/{connectionId}"
---
<Note>
Check out the configuration docs for [Zabbix Connections](/integrations/app-connections/zabbix) to learn how to obtain the required credentials.
</Note>
@@ -0,0 +1,4 @@
---
title: "Create"
openapi: "POST /api/v1/secret-syncs/zabbix"
---
@@ -0,0 +1,4 @@
---
title: "Delete"
openapi: "DELETE /api/v1/secret-syncs/zabbix/{syncId}"
---
@@ -0,0 +1,4 @@
---
title: "Get by ID"
openapi: "GET /api/v1/secret-syncs/zabbix/{syncId}"
---
@@ -0,0 +1,4 @@
---
title: "Get by Name"
openapi: "GET /api/v1/secret-syncs/zabbix/sync-name/{syncName}"
---
@@ -0,0 +1,4 @@
---
title: "Import Secrets"
openapi: "POST /api/v1/secret-syncs/zabbix/{syncId}/import-secrets"
---
@@ -0,0 +1,4 @@
---
title: "List"
openapi: "GET /api/v1/secret-syncs/zabbix"
---
@@ -0,0 +1,4 @@
---
title: "Remove Secrets"
openapi: "POST /api/v1/secret-syncs/zabbix/{syncId}/remove-secrets"
---
@@ -0,0 +1,4 @@
---
title: "Sync Secrets"
openapi: "POST /api/v1/secret-syncs/zabbix/{syncId}/sync-secrets"
---
@@ -0,0 +1,4 @@
---
title: "Update"
openapi: "PATCH /api/v1/secret-syncs/zabbix/{syncId}"
---
+30 -2
View File
@@ -490,7 +490,8 @@
"integrations/app-connections/teamcity", "integrations/app-connections/teamcity",
"integrations/app-connections/terraform-cloud", "integrations/app-connections/terraform-cloud",
"integrations/app-connections/vercel", "integrations/app-connections/vercel",
"integrations/app-connections/windmill" "integrations/app-connections/windmill",
"integrations/app-connections/zabbix"
] ]
} }
] ]
@@ -523,7 +524,8 @@
"integrations/secret-syncs/teamcity", "integrations/secret-syncs/teamcity",
"integrations/secret-syncs/terraform-cloud", "integrations/secret-syncs/terraform-cloud",
"integrations/secret-syncs/vercel", "integrations/secret-syncs/vercel",
"integrations/secret-syncs/windmill" "integrations/secret-syncs/windmill",
"integrations/secret-syncs/zabbix"
] ]
} }
] ]
@@ -1521,6 +1523,18 @@
"api-reference/endpoints/app-connections/windmill/update", "api-reference/endpoints/app-connections/windmill/update",
"api-reference/endpoints/app-connections/windmill/delete" "api-reference/endpoints/app-connections/windmill/delete"
] ]
},
{
"group": "Zabbix",
"pages": [
"api-reference/endpoints/app-connections/zabbix/list",
"api-reference/endpoints/app-connections/zabbix/available",
"api-reference/endpoints/app-connections/zabbix/get-by-id",
"api-reference/endpoints/app-connections/zabbix/get-by-name",
"api-reference/endpoints/app-connections/zabbix/create",
"api-reference/endpoints/app-connections/zabbix/update",
"api-reference/endpoints/app-connections/zabbix/delete"
]
} }
] ]
}, },
@@ -1827,6 +1841,20 @@
"api-reference/endpoints/secret-syncs/windmill/import-secrets", "api-reference/endpoints/secret-syncs/windmill/import-secrets",
"api-reference/endpoints/secret-syncs/windmill/remove-secrets" "api-reference/endpoints/secret-syncs/windmill/remove-secrets"
] ]
},
{
"group": "Zabbix",
"pages": [
"api-reference/endpoints/secret-syncs/zabbix/list",
"api-reference/endpoints/secret-syncs/zabbix/get-by-id",
"api-reference/endpoints/secret-syncs/zabbix/get-by-name",
"api-reference/endpoints/secret-syncs/zabbix/create",
"api-reference/endpoints/secret-syncs/zabbix/update",
"api-reference/endpoints/secret-syncs/zabbix/delete",
"api-reference/endpoints/secret-syncs/zabbix/sync-secrets",
"api-reference/endpoints/secret-syncs/zabbix/import-secrets",
"api-reference/endpoints/secret-syncs/zabbix/remove-secrets"
]
} }
] ]
}, },
Binary file not shown.

After

Width:  |  Height:  |  Size: 339 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 348 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 316 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 577 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 938 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 627 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 591 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 558 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 547 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 592 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 589 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 551 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 958 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 868 KiB

@@ -0,0 +1,101 @@
---
title: "Zabbix Connection"
description: "Learn how to configure a Zabbix Connection for Infisical."
---
Infisical supports the use of [API Tokens](https://www.zabbix.com/documentation/current/en/manual/web_interface/frontend_sections/users/api_tokens) to connect with Zabbix.
## Create Zabbix API Token
<Steps>
<Step title="Navigate to 'API Tokens'">
![Dashboard Page](/images/app-connections/zabbix/zabbix-dashboard.png)
</Step>
<Step title="Click 'Create API Token'">
![Click Create Token](/images/app-connections/zabbix/zabbix-api-token-list.png)
</Step>
<Step title="Provide Token Information">
Ensure that you give this token access to the correct app, then click 'Create Token'.
![Create Token Page](/images/app-connections/zabbix/zabbix-api-token-form.png)
</Step>
<Step title="Save Token">
After clicking 'Create Token', a modal containing your access token will appear. Save this token for later steps.
![Copy Token Modal](/images/app-connections/zabbix/zabbix-api-token-generated.png)
</Step>
</Steps>
## Create Zabbix Connection in Infisical
<Tabs>
<Tab title="Infisical UI">
<Steps>
<Step title="Navigate to App Connections">
In your Infisical dashboard, go to **Organization Settings** and select the [**App Connections**](https://app.infisical.com/organization/app-connections) tab.
![App Connections Tab](/images/app-connections/general/add-connection.png)
</Step>
<Step title="Select Zabbix Connection">
Click the **+ Add Connection** button and select the **Zabbix Connection** option from the available integrations.
![Select Zabbix Connection](/images/app-connections/zabbix/zabbix-app-connection-option.png)
</Step>
<Step title="Fill out the Zabbix Connection Modal">
Complete the Zabbix Connection form by entering:
- A descriptive name for the connection
- An optional description for future reference
- The Zabbix URL for your instance
- The API Token from earlier steps
![Zabbix Connection Modal](/images/app-connections/zabbix/zabbix-app-connection-form.png)
</Step>
<Step title="Connection Created">
After clicking Create, your **Zabbix Connection** is established and ready to use with your Infisical projects.
![Zabbix Connection Created](/images/app-connections/zabbix/zabbix-app-connection-generated.png)
</Step>
</Steps>
</Tab>
<Tab title="API">
To create a Zabbix Connection, make an API request to the [Create Zabbix Connection](/api-reference/endpoints/app-connections/zabbix/create) API endpoint.
### Sample request
```bash Request
curl --request POST \
--url https://app.infisical.com/api/v1/app-connections/zabbix \
--header 'Content-Type: application/json' \
--data '{
"name": "my-zabbix-connection",
"method": "api-token",
"credentials": {
"apiToken": "[API TOKEN]",
"instanceUrl": "https://zabbix.example.com"
}
}'
```
### Sample response
```bash Response
{
"appConnection": {
"id": "e5d18aca-86f7-4026-a95e-efb8aeb0d8e6",
"name": "my-zabbix-connection",
"description": null,
"version": 1,
"orgId": "6f03caa1-a5de-43ce-b127-95a145d3464c",
"createdAt": "2025-04-23T19:46:34.831Z",
"updatedAt": "2025-04-23T19:46:34.831Z",
"isPlatformManagedCredentials": false,
"credentialsHash": "7c2d371dec195f82a6a0d5b41c970a229cfcaf88e894a5b6395e2dbd0280661f",
"app": "zabbix",
"method": "api-token",
"credentials": {
"instanceUrl": "https://zabbix.example.com"
}
}
}
```
</Tab>
</Tabs>
+173
View File
@@ -0,0 +1,173 @@
---
title: "Zabbix Sync"
description: "Learn how to configure a Zabbix Sync for Infisical."
---
**Prerequisites:**
- Create a [Zabbix Connection](/integrations/app-connections/zabbix)
<Tabs>
<Tab title="Infisical UI">
<Steps>
<Step title="Add Sync">
Navigate to **Project** > **Integrations** and select the **Secret Syncs** tab. Click on the **Add Sync** button.
![Secret Syncs Tab](/images/secret-syncs/general/secret-sync-tab.png)
</Step>
<Step title="Select 'Zabbix'">
![Select Zabbix](/images/secret-syncs/zabbix/select-option.png)
</Step>
<Step title="Configure source">
Configure the **Source** from where secrets should be retrieved, then click **Next**.
![Configure Source](/images/secret-syncs/zabbix/configure-source.png)
- **Environment**: The project environment to retrieve secrets from.
- **Secret Path**: The folder path to retrieve secrets from.
<Tip>
If you need to sync secrets from multiple folder locations, check out [secret imports](/documentation/platform/secret-reference#secret-imports).
</Tip>
</Step>
<Step title="Configure destination">
Configure the **Destination** to where secrets should be deployed, then click **Next**.
![Configure Destination](/images/secret-syncs/zabbix/configure-destination.png)
- **Zabbix Connection**: The Zabbix Connection to authenticate with.
- **Scope**: The Zabbix scope to sync secrets to.
- **Global**: Secrets will be synced globally.
- **Host**: Secrets will be synced to the specified host.
- **Macro Type**: The type of macro to use when syncing secrets to Zabbix. Currently only **Text** and **Secret** macros are supported.
The remaining fields are determined by the selected **Scope**:
<AccordionGroup>
<Accordion title="Host">
- **Host**: The host to sync secrets to.
</Accordion>
</AccordionGroup>
</Step>
<Step title="Configure Sync Options">
Configure the **Sync Options** to specify how secrets should be synced, then click **Next**.
![Configure Options](/images/secret-syncs/zabbix/configure-sync-options.png)
- **Initial Sync Behavior**: Determines how Infisical should resolve the initial sync.
- **Overwrite Destination Secrets**: Removes any secrets at the destination endpoint not present in Infisical.
- **Import Secrets (Prioritize Infisical)**: Imports secrets from the destination endpoint before syncing, prioritizing values from Infisical over Zabbix when keys conflict.
- **Import Secrets (Prioritize Zabbix)**: Imports secrets from the destination endpoint before syncing, prioritizing values from Zabbix over Infisical when keys conflict.
- **Key Schema**: Template that determines how secret names are transformed when syncing, using `{{secretKey}}` as a placeholder for the original secret name and `{{environment}}` for the environment.
<Note>
We highly recommend using a Key Schema to ensure that Infisical only manages the specific keys you intend, keeping everything else untouched.
</Note>
- **Auto-Sync Enabled**: If enabled, secrets will automatically be synced from the source location when changes occur. Disable to enforce manual syncing only.
- **Disable Secret Deletion**: If enabled, Infisical will not remove secrets from the sync destination. Enable this option if you intend to manage some secrets manually outside of Infisical.
</Step>
<Step title="Configure details">
Configure the **Details** of your Zabbix Sync, then click **Next**.
![Configure Details](/images/secret-syncs/zabbix/configure-details.png)
- **Name**: The name of your sync. Must be slug-friendly.
- **Description**: An optional description for your sync.
</Step>
<Step title="Review configuration">
Review your Zabbix Sync configuration, then click **Create Sync**.
![Review Configuration](/images/secret-syncs/zabbix/review-configuration.png)
</Step>
<Step title="Sync created">
If enabled, your Zabbix Sync will begin syncing your secrets to the destination endpoint.
![Sync Created](/images/secret-syncs/zabbix/sync-created.png)
</Step>
</Steps>
</Tab>
<Tab title="API">
To create a **Zabbix Sync**, make an API request to the [Create Zabbix Sync](/api-reference/endpoints/secret-syncs/zabbix/create) API endpoint.
### Sample request
```bash Request
curl --request POST \
--url https://app.infisical.com/api/v1/secret-syncs/zabbix \
--header 'Content-Type: application/json' \
--data '{
"name": "my-zabbix-sync",
"projectId": "3c90c3cc-0d44-4b50-8888-8dd25736052a",
"description": "an example sync",
"connectionId": "3c90c3cc-0d44-4b50-8888-8dd25736052a",
"environment": "dev",
"secretPath": "/my-secrets",
"isEnabled": true,
"syncOptions": {
"initialSyncBehavior": "overwrite-destination",
"autoSyncEnabled": true,
"disableSecretDeletion": false
},
"destinationConfig": {
"scope": "host",
"hostId": "my-zabbix-host",
"hostName": "my-zabbix-host",
"macroType": 0
}
}'
```
### Sample response
```bash Response
{
"secretSync": {
"id": "3c90c3cc-0d44-4b50-8888-8dd25736052a",
"name": "my-zabbix-sync",
"description": "an example sync",
"isEnabled": true,
"version": 1,
"folderId": "3c90c3cc-0d44-4b50-8888-8dd25736052a",
"connectionId": "3c90c3cc-0d44-4b50-8888-8dd25736052a",
"createdAt": "2023-11-07T05:31:56Z",
"updatedAt": "2023-11-07T05:31:56Z",
"syncStatus": "succeeded",
"lastSyncJobId": "123",
"lastSyncMessage": null,
"lastSyncedAt": "2023-11-07T05:31:56Z",
"importStatus": null,
"lastImportJobId": null,
"lastImportMessage": null,
"lastImportedAt": null,
"removeStatus": null,
"lastRemoveJobId": null,
"lastRemoveMessage": null,
"lastRemovedAt": null,
"syncOptions": {
"initialSyncBehavior": "overwrite-destination",
"autoSyncEnabled": true,
"disableSecretDeletion": false
},
"projectId": "3c90c3cc-0d44-4b50-8888-8dd25736052a",
"connection": {
"app": "zabbix",
"name": "my-zabbix-connection",
"id": "3c90c3cc-0d44-4b50-8888-8dd25736052a"
},
"environment": {
"slug": "dev",
"name": "Development",
"id": "3c90c3cc-0d44-4b50-8888-8dd25736052a"
},
"folder": {
"id": "3c90c3cc-0d44-4b50-8888-8dd25736052a",
"path": "/my-secrets"
},
"destination": "zabbix",
"destinationConfig": {
"scope": "host",
"hostId": "my-zabbix-host",
"hostName": "my-zabbix-host",
"macroType": 0
}
}
}
```
</Tab>
</Tabs>
@@ -794,3 +794,9 @@ If export type is set to `otlp`, you will have to configure a value for `OTEL_EX
The TLS header used to propagate the client certificate from the load balancer The TLS header used to propagate the client certificate from the load balancer
to the server. to the server.
</ParamField> </ParamField>
## Environment Variable Overrides
If you can't directly access and modify environment variables, you can update them using the [Server Admin Console](/documentation/platform/admin-panel/server-admin).
![Environment Variables Overrides Page](../../images/self-hosting/configuration/overrides/page.png)
Binary file not shown.

After

Width:  |  Height:  |  Size: 80 KiB

@@ -65,6 +65,7 @@ export const createNotification = (
toast(<NotificationContent {...myProps} />, { toast(<NotificationContent {...myProps} />, {
position: "bottom-right", position: "bottom-right",
...toastProps, ...toastProps,
autoClose: toastProps.autoClose || 15000,
theme: "dark", theme: "dark",
type: myProps?.type || "info" type: myProps?.type || "info"
}); });
@@ -60,7 +60,7 @@ export const OnePassSyncFields = () => {
menuPlacement="top" menuPlacement="top"
isLoading={isVaultsLoading && Boolean(connectionId)} isLoading={isVaultsLoading && Boolean(connectionId)}
isDisabled={!connectionId} isDisabled={!connectionId}
value={vaults?.find((v) => v.id === value) ?? null} value={vaults?.find((v) => v.id === value) || null}
onChange={(option) => onChange((option as SingleValue<TOnePassVault>)?.id ?? null)} onChange={(option) => onChange((option as SingleValue<TOnePassVault>)?.id ?? null)}
options={vaults} options={vaults}
placeholder="Select a vault..." placeholder="Select a vault..."
@@ -25,6 +25,7 @@ import { TeamCitySyncFields } from "./TeamCitySyncFields";
import { TerraformCloudSyncFields } from "./TerraformCloudSyncFields"; import { TerraformCloudSyncFields } from "./TerraformCloudSyncFields";
import { VercelSyncFields } from "./VercelSyncFields"; import { VercelSyncFields } from "./VercelSyncFields";
import { WindmillSyncFields } from "./WindmillSyncFields"; import { WindmillSyncFields } from "./WindmillSyncFields";
import { ZabbixSyncFields } from "./ZabbixSyncFields";
export const SecretSyncDestinationFields = () => { export const SecretSyncDestinationFields = () => {
const { watch } = useFormContext<TSecretSyncForm>(); const { watch } = useFormContext<TSecretSyncForm>();
@@ -76,6 +77,8 @@ export const SecretSyncDestinationFields = () => {
return <GitLabSyncFields />; return <GitLabSyncFields />;
case SecretSync.CloudflarePages: case SecretSync.CloudflarePages:
return <CloudflarePagesSyncFields />; return <CloudflarePagesSyncFields />;
case SecretSync.Zabbix:
return <ZabbixSyncFields />;
default: default:
throw new Error(`Unhandled Destination Config Field: ${destination}`); throw new Error(`Unhandled Destination Config Field: ${destination}`);
} }
@@ -0,0 +1,147 @@
import { Controller, useFormContext, useWatch } from "react-hook-form";
import { SingleValue } from "react-select";
import { SecretSyncConnectionField } from "@app/components/secret-syncs/forms/SecretSyncConnectionField";
import { FilterableSelect, FormControl, Select, SelectItem } from "@app/components/v2";
import {
TZabbixHost,
useZabbixConnectionListHosts,
ZABBIX_SYNC_SCOPES,
ZabbixMacroType,
ZabbixSyncScope
} from "@app/hooks/api/appConnections/zabbix";
import { SecretSync } from "@app/hooks/api/secretSyncs";
import { TSecretSyncForm } from "../schemas";
export const ZabbixSyncFields = () => {
const { control, watch, setValue } = useFormContext<
TSecretSyncForm & { destination: SecretSync.Zabbix }
>();
const connectionId = useWatch({ name: "connection.id", control });
const currentScope = watch("destinationConfig.scope");
const { data: hosts = [], isPending: isHostsPending } = useZabbixConnectionListHosts(
connectionId,
{
enabled: Boolean(connectionId)
}
);
return (
<>
<SecretSyncConnectionField
onChange={() => {
setValue("destinationConfig.scope", ZabbixSyncScope.Global);
setValue("destinationConfig.hostId", "");
setValue("destinationConfig.hostName", "");
}}
/>
<Controller
name="destinationConfig.scope"
control={control}
render={({ field: { value, onChange }, fieldState: { error } }) => (
<FormControl
errorText={error?.message}
isError={Boolean(error?.message)}
label="Scope"
tooltipClassName="max-w-lg py-3"
tooltipText={
<div className="flex flex-col gap-3">
<p>
Specify how Infisical should manage secrets from Zabbix. The following options are
available:
</p>
<ul className="flex list-disc flex-col gap-3 pl-4">
{Object.values(ZABBIX_SYNC_SCOPES).map(({ name, description }) => {
return (
<li key={name}>
<p className="text-mineshaft-300">
<span className="font-medium text-bunker-200">{name}</span>: {description}
</p>
</li>
);
})}
</ul>
</div>
}
>
<Select
value={value}
onValueChange={(val) => {
onChange(val);
setValue("destinationConfig.hostId", "");
setValue("destinationConfig.hostName", "");
}}
className="w-full border border-mineshaft-500 capitalize"
position="popper"
placeholder="Select a scope..."
dropdownContainerClassName="max-w-none"
>
{Object.values(ZabbixSyncScope).map((scope) => (
<SelectItem className="capitalize" value={scope} key={scope}>
{scope.replace("-", " ")}
</SelectItem>
))}
</Select>
</FormControl>
)}
/>
{currentScope === ZabbixSyncScope.Host && (
<Controller
name="destinationConfig.hostId"
control={control}
render={({ field: { value, onChange }, fieldState: { error } }) => (
<FormControl isError={Boolean(error)} errorText={error?.message} label="Host">
<FilterableSelect
menuPlacement="top"
isLoading={isHostsPending && Boolean(connectionId)}
isDisabled={!connectionId}
value={hosts.find((host) => host.hostId === value) ?? null}
onChange={(option) => {
const selectedOption = option as SingleValue<TZabbixHost>;
onChange(selectedOption?.hostId ?? null);
if (selectedOption) {
setValue("destinationConfig.hostName", selectedOption.host);
} else {
setValue("destinationConfig.hostName", "");
}
}}
options={hosts}
placeholder="Select a host..."
getOptionLabel={(option) => option.host}
getOptionValue={(option) => option.hostId}
/>
</FormControl>
)}
/>
)}
<Controller
control={control}
name="destinationConfig.macroType"
defaultValue={ZabbixMacroType.Secret}
render={({ field: { onChange, value }, fieldState: { error } }) => (
<FormControl isError={Boolean(error)} errorText={error?.message} label="Macro Type">
<Select
value={String(value)}
onValueChange={(val) => onChange(parseInt(val, 10))}
className="w-full border border-mineshaft-500 capitalize"
position="popper"
placeholder="Select a macro type..."
dropdownContainerClassName="max-w-none"
>
<SelectItem value={String(ZabbixMacroType.Text)} key="text">
Text
</SelectItem>
<SelectItem value={String(ZabbixMacroType.Secret)} key="secret">
Secret
</SelectItem>
</Select>
</FormControl>
)}
/>
</>
);
};
@@ -23,6 +23,7 @@ export const SecretSyncOptionsFields = ({ hideInitialSync }: Props) => {
const { control, watch } = useFormContext<TSecretSyncForm>(); const { control, watch } = useFormContext<TSecretSyncForm>();
const destination = watch("destination"); const destination = watch("destination");
const currentSyncOption = watch("syncOptions");
const destinationName = SECRET_SYNC_MAP[destination].name; const destinationName = SECRET_SYNC_MAP[destination].name;
@@ -57,6 +58,7 @@ export const SecretSyncOptionsFields = ({ hideInitialSync }: Props) => {
case SecretSync.Flyio: case SecretSync.Flyio:
case SecretSync.GitLab: case SecretSync.GitLab:
case SecretSync.CloudflarePages: case SecretSync.CloudflarePages:
case SecretSync.Zabbix:
AdditionalSyncOptionsFieldsComponent = null; AdditionalSyncOptionsFieldsComponent = null;
break; break;
default: default:
@@ -127,8 +129,9 @@ export const SecretSyncOptionsFields = ({ hideInitialSync }: Props) => {
{!syncOption?.canImportSecrets && ( {!syncOption?.canImportSecrets && (
<p className="-mt-2.5 mb-2.5 text-xs text-yellow"> <p className="-mt-2.5 mb-2.5 text-xs text-yellow">
<FontAwesomeIcon className="mr-1" size="xs" icon={faTriangleExclamation} /> <FontAwesomeIcon className="mr-1" size="xs" icon={faTriangleExclamation} />
{destinationName} only supports overwriting destination secrets. Secrets not present {destinationName} only supports overwriting destination secrets.{" "}
in Infisical will be removed from the destination. {!currentSyncOption.disableSecretDeletion &&
"Secrets not present in Infisical will be removed from the destination."}
</p> </p>
)} )}
</> </>
@@ -35,6 +35,7 @@ import { TeamCitySyncReviewFields } from "./TeamCitySyncReviewFields";
import { TerraformCloudSyncReviewFields } from "./TerraformCloudSyncReviewFields"; import { TerraformCloudSyncReviewFields } from "./TerraformCloudSyncReviewFields";
import { VercelSyncReviewFields } from "./VercelSyncReviewFields"; import { VercelSyncReviewFields } from "./VercelSyncReviewFields";
import { WindmillSyncReviewFields } from "./WindmillSyncReviewFields"; import { WindmillSyncReviewFields } from "./WindmillSyncReviewFields";
import { ZabbixSyncReviewFields } from "./ZabbixSyncReviewFields";
export const SecretSyncReviewFields = () => { export const SecretSyncReviewFields = () => {
const { watch } = useFormContext<TSecretSyncForm>(); const { watch } = useFormContext<TSecretSyncForm>();
@@ -124,6 +125,9 @@ export const SecretSyncReviewFields = () => {
case SecretSync.CloudflarePages: case SecretSync.CloudflarePages:
DestinationFieldsComponent = <CloudflarePagesSyncReviewFields />; DestinationFieldsComponent = <CloudflarePagesSyncReviewFields />;
break; break;
case SecretSync.Zabbix:
DestinationFieldsComponent = <ZabbixSyncReviewFields />;
break;
default: default:
throw new Error(`Unhandled Destination Review Fields: ${destination}`); throw new Error(`Unhandled Destination Review Fields: ${destination}`);
} }
@@ -0,0 +1,31 @@
import { useFormContext } from "react-hook-form";
import { TSecretSyncForm } from "@app/components/secret-syncs/forms/schemas";
import { GenericFieldLabel } from "@app/components/v2";
import { ZabbixSyncScope } from "@app/hooks/api/appConnections/zabbix";
import { SecretSync } from "@app/hooks/api/secretSyncs";
const isTextMacro = (macroType: number) => macroType === 0;
export const ZabbixSyncReviewFields = () => {
const { watch } = useFormContext<TSecretSyncForm & { destination: SecretSync.Zabbix }>();
const scope = watch("destinationConfig.scope");
const hostId = watch("destinationConfig.hostId");
const hostName = watch("destinationConfig.hostName");
const macroType = watch("destinationConfig.macroType");
return (
<>
<GenericFieldLabel label="Scope">{scope}</GenericFieldLabel>
{scope === ZabbixSyncScope.Host && (
<>
<GenericFieldLabel label="Host ID">{hostId}</GenericFieldLabel>
<GenericFieldLabel label="Host Name">{hostName}</GenericFieldLabel>
</>
)}
<GenericFieldLabel label="Macro Type">
{isTextMacro(macroType) ? "Text" : "Secret"}
</GenericFieldLabel>
</>
);
};
@@ -22,6 +22,7 @@ import { TeamCitySyncDestinationSchema } from "./teamcity-sync-destination-schem
import { TerraformCloudSyncDestinationSchema } from "./terraform-cloud-destination-schema"; import { TerraformCloudSyncDestinationSchema } from "./terraform-cloud-destination-schema";
import { VercelSyncDestinationSchema } from "./vercel-sync-destination-schema"; import { VercelSyncDestinationSchema } from "./vercel-sync-destination-schema";
import { WindmillSyncDestinationSchema } from "./windmill-sync-destination-schema"; import { WindmillSyncDestinationSchema } from "./windmill-sync-destination-schema";
import { ZabbixSyncDestinationSchema } from "./zabbix-sync-destination-schema";
const SecretSyncUnionSchema = z.discriminatedUnion("destination", [ const SecretSyncUnionSchema = z.discriminatedUnion("destination", [
AwsParameterStoreSyncDestinationSchema, AwsParameterStoreSyncDestinationSchema,
@@ -45,7 +46,8 @@ const SecretSyncUnionSchema = z.discriminatedUnion("destination", [
RenderSyncDestinationSchema, RenderSyncDestinationSchema,
FlyioSyncDestinationSchema, FlyioSyncDestinationSchema,
GitlabSyncDestinationSchema, GitlabSyncDestinationSchema,
CloudflarePagesSyncDestinationSchema CloudflarePagesSyncDestinationSchema,
ZabbixSyncDestinationSchema
]); ]);
export const SecretSyncFormSchema = SecretSyncUnionSchema; export const SecretSyncFormSchema = SecretSyncUnionSchema;
@@ -0,0 +1,27 @@
import { z } from "zod";
import { BaseSecretSyncSchema } from "@app/components/secret-syncs/forms/schemas/base-secret-sync-schema";
import { ZabbixMacroType, ZabbixSyncScope } from "@app/hooks/api/appConnections/zabbix";
import { SecretSync } from "@app/hooks/api/secretSyncs";
export const ZabbixSyncDestinationSchema = BaseSecretSyncSchema().merge(
z.object({
destination: z.literal(SecretSync.Zabbix),
destinationConfig: z.discriminatedUnion("scope", [
z.object({
scope: z.literal(ZabbixSyncScope.Host),
hostId: z.string().trim().min(1, "Host ID required"),
hostName: z.string().trim().min(1, "Host name required"),
macroType: z.nativeEnum(ZabbixMacroType, {
errorMap: () => ({ message: "Macro type must be either 'text' or 'secret'" })
})
}),
z.object({
scope: z.literal(ZabbixSyncScope.Global),
macroType: z.nativeEnum(ZabbixMacroType, {
errorMap: () => ({ message: "Macro type must be either 'text' or 'secret'" })
})
})
])
})
);
@@ -0,0 +1,36 @@
import { MouseEventHandler } from "react";
export const HeaderResizer = ({
onMouseDown,
isActive,
scrollOffset,
heightOffset
}: {
onMouseDown: MouseEventHandler<HTMLDivElement>;
isActive: boolean;
scrollOffset: number;
heightOffset: number;
}) => {
return (
<>
<div
tabIndex={-1}
role="button"
className={`absolute left-0 z-40 h-0.5 w-full cursor-ns-resize hover:bg-blue-400/20 ${
isActive ? "bg-blue-400/75" : "bg-transparent"
}`}
onMouseDown={onMouseDown}
style={{
transform: "translateY(50%)",
top: heightOffset
}}
/>
<div
style={{ left: `calc(50% + ${scrollOffset}px)`, top: heightOffset }}
className="pointer-events-none absolute z-30 -translate-x-1/2"
>
<div className="h-1 w-8 rounded bg-gray-400 opacity-50" />
</div>
</>
);
};
@@ -0,0 +1,42 @@
export const HighlightText = ({
text,
highlight,
highlightClassName
}: {
text: string | undefined | null;
highlight: string;
highlightClassName?: string;
}) => {
if (!text) return null;
const searchTerm = highlight.toLowerCase().trim();
if (!searchTerm) return <span>{text}</span>;
const parts: React.ReactNode[] = [];
let lastIndex = 0;
const escapedSearchTerm = searchTerm.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
const regex = new RegExp(escapedSearchTerm, "gi");
text.replace(regex, (match: string, offset: number) => {
if (offset > lastIndex) {
parts.push(<span key={`pre-${lastIndex}`}>{text.substring(lastIndex, offset)}</span>);
}
parts.push(
<span key={`match-${offset}`} className={highlightClassName || "bg-yellow/30"}>
{match}
</span>
);
lastIndex = offset + match.length;
return match;
});
if (lastIndex < text.length) {
parts.push(<span key={`post-${lastIndex}`}>{text.substring(lastIndex)}</span>);
}
return parts;
};
@@ -0,0 +1 @@
export { HighlightText } from "./HighlightText";
+9 -3
View File
@@ -45,10 +45,14 @@ export const Table = ({ children, className }: TableProps): JSX.Element => (
export type THeadProps = { export type THeadProps = {
children: ReactNode; children: ReactNode;
className?: string; className?: string;
style?: React.CSSProperties;
}; };
export const THead = ({ children, className }: THeadProps): JSX.Element => ( export const THead = ({ children, className, style }: THeadProps): JSX.Element => (
<thead className={twMerge("bg-mineshaft-800 text-xs uppercase text-bunker-300", className)}> <thead
className={twMerge("bg-mineshaft-800 text-xs uppercase text-bunker-300", className)}
style={style}
>
{children} {children}
</thead> </thead>
); );
@@ -96,14 +100,16 @@ export const Tr = ({
export type ThProps = { export type ThProps = {
children?: ReactNode; children?: ReactNode;
className?: string; className?: string;
style?: React.CSSProperties;
}; };
export const Th = ({ children, className }: ThProps): JSX.Element => ( export const Th = ({ children, className, style }: ThProps): JSX.Element => (
<th <th
className={twMerge( className={twMerge(
"border-b-2 border-mineshaft-600 bg-mineshaft-800 px-5 pb-3.5 pt-4 font-semibold", "border-b-2 border-mineshaft-600 bg-mineshaft-800 px-5 pb-3.5 pt-4 font-semibold",
className className
)} )}
style={style}
> >
{children} {children}
</th> </th>
+5 -2
View File
@@ -37,7 +37,8 @@ import {
TeamCityConnectionMethod, TeamCityConnectionMethod,
TerraformCloudConnectionMethod, TerraformCloudConnectionMethod,
VercelConnectionMethod, VercelConnectionMethod,
WindmillConnectionMethod WindmillConnectionMethod,
ZabbixConnectionMethod
} from "@app/hooks/api/appConnections/types"; } from "@app/hooks/api/appConnections/types";
import { BitbucketConnectionMethod } from "@app/hooks/api/appConnections/types/bitbucket-connection"; import { BitbucketConnectionMethod } from "@app/hooks/api/appConnections/types/bitbucket-connection";
import { HerokuConnectionMethod } from "@app/hooks/api/appConnections/types/heroku-connection"; import { HerokuConnectionMethod } from "@app/hooks/api/appConnections/types/heroku-connection";
@@ -90,7 +91,8 @@ export const APP_CONNECTION_MAP: Record<
[AppConnection.Flyio]: { name: "Fly.io", image: "Flyio.svg" }, [AppConnection.Flyio]: { name: "Fly.io", image: "Flyio.svg" },
[AppConnection.Gitlab]: { name: "GitLab", image: "GitLab.png" }, [AppConnection.Gitlab]: { name: "GitLab", image: "GitLab.png" },
[AppConnection.Cloudflare]: { name: "Cloudflare", image: "Cloudflare.png" }, [AppConnection.Cloudflare]: { name: "Cloudflare", image: "Cloudflare.png" },
[AppConnection.Bitbucket]: { name: "Bitbucket", image: "Bitbucket.png" } [AppConnection.Bitbucket]: { name: "Bitbucket", image: "Bitbucket.png" },
[AppConnection.Zabbix]: { name: "Zabbix", image: "Zabbix.png" }
}; };
export const getAppConnectionMethodDetails = (method: TAppConnection["method"]) => { export const getAppConnectionMethodDetails = (method: TAppConnection["method"]) => {
@@ -123,6 +125,7 @@ export const getAppConnectionMethodDetails = (method: TAppConnection["method"])
case OnePassConnectionMethod.ApiToken: case OnePassConnectionMethod.ApiToken:
case CloudflareConnectionMethod.ApiToken: case CloudflareConnectionMethod.ApiToken:
case BitbucketConnectionMethod.ApiToken: case BitbucketConnectionMethod.ApiToken:
case ZabbixConnectionMethod.ApiToken:
return { name: "API Token", icon: faKey }; return { name: "API Token", icon: faKey };
case PostgresConnectionMethod.UsernameAndPassword: case PostgresConnectionMethod.UsernameAndPassword:
case MsSqlConnectionMethod.UsernameAndPassword: case MsSqlConnectionMethod.UsernameAndPassword:
+6 -1
View File
@@ -81,6 +81,10 @@ export const SECRET_SYNC_MAP: Record<SecretSync, { name: string; image: string }
[SecretSync.CloudflarePages]: { [SecretSync.CloudflarePages]: {
name: "Cloudflare Pages", name: "Cloudflare Pages",
image: "Cloudflare.png" image: "Cloudflare.png"
},
[SecretSync.Zabbix]: {
name: "Zabbix",
image: "Zabbix.png"
} }
}; };
@@ -106,7 +110,8 @@ export const SECRET_SYNC_CONNECTION_MAP: Record<SecretSync, AppConnection> = {
[SecretSync.Render]: AppConnection.Render, [SecretSync.Render]: AppConnection.Render,
[SecretSync.Flyio]: AppConnection.Flyio, [SecretSync.Flyio]: AppConnection.Flyio,
[SecretSync.GitLab]: AppConnection.Gitlab, [SecretSync.GitLab]: AppConnection.Gitlab,
[SecretSync.CloudflarePages]: AppConnection.Cloudflare [SecretSync.CloudflarePages]: AppConnection.Cloudflare,
[SecretSync.Zabbix]: AppConnection.Zabbix
}; };
export const SECRET_SYNC_INITIAL_SYNC_BEHAVIOR_MAP: Record< export const SECRET_SYNC_INITIAL_SYNC_BEHAVIOR_MAP: Record<
+13 -1
View File
@@ -10,6 +10,7 @@ import {
AdminGetUsersFilters, AdminGetUsersFilters,
AdminIntegrationsConfig, AdminIntegrationsConfig,
OrganizationWithProjects, OrganizationWithProjects,
TGetEnvOverrides,
TGetInvalidatingCacheStatus, TGetInvalidatingCacheStatus,
TGetServerRootKmsEncryptionDetails, TGetServerRootKmsEncryptionDetails,
TServerConfig TServerConfig
@@ -31,7 +32,8 @@ export const adminQueryKeys = {
getAdminSlackConfig: () => ["admin-slack-config"] as const, getAdminSlackConfig: () => ["admin-slack-config"] as const,
getServerEncryptionStrategies: () => ["server-encryption-strategies"] as const, getServerEncryptionStrategies: () => ["server-encryption-strategies"] as const,
getInvalidateCache: () => ["admin-invalidate-cache"] as const, getInvalidateCache: () => ["admin-invalidate-cache"] as const,
getAdminIntegrationsConfig: () => ["admin-integrations-config"] as const getAdminIntegrationsConfig: () => ["admin-integrations-config"] as const,
getEnvOverrides: () => ["env-overrides"] as const
}; };
export const fetchServerConfig = async () => { export const fetchServerConfig = async () => {
@@ -163,3 +165,13 @@ export const useGetInvalidatingCacheStatus = (enabled = true) => {
refetchInterval: (data) => (data ? 3000 : false) refetchInterval: (data) => (data ? 3000 : false)
}); });
}; };
export const useGetEnvOverrides = () => {
return useQuery({
queryKey: adminQueryKeys.getEnvOverrides(),
queryFn: async () => {
const { data } = await apiRequest.get<TGetEnvOverrides>("/api/v1/admin/env-overrides");
return data;
}
});
};
+9
View File
@@ -48,6 +48,7 @@ export type TServerConfig = {
authConsentContent?: string; authConsentContent?: string;
pageFrameContent?: string; pageFrameContent?: string;
invalidatingCache: boolean; invalidatingCache: boolean;
envOverrides?: Record<string, string>;
}; };
export type TUpdateServerConfigDTO = { export type TUpdateServerConfigDTO = {
@@ -61,6 +62,7 @@ export type TUpdateServerConfigDTO = {
gitHubAppConnectionSlug?: string; gitHubAppConnectionSlug?: string;
gitHubAppConnectionId?: string; gitHubAppConnectionId?: string;
gitHubAppConnectionPrivateKey?: string; gitHubAppConnectionPrivateKey?: string;
envOverrides?: Record<string, string>;
} & Partial<TServerConfig>; } & Partial<TServerConfig>;
export type TCreateAdminUserDTO = { export type TCreateAdminUserDTO = {
@@ -138,3 +140,10 @@ export type TInvalidateCacheDTO = {
export type TGetInvalidatingCacheStatus = { export type TGetInvalidatingCacheStatus = {
invalidating: boolean; invalidating: boolean;
}; };
export interface TGetEnvOverrides {
[key: string]: {
name: string;
fields: { key: string; value: string; hasEnvEntry: boolean; description?: string }[];
};
}

Some files were not shown because too many files have changed in this diff Show More