diff --git a/backend/src/lib/api-docs/constants.ts b/backend/src/lib/api-docs/constants.ts index be460b4b4..ef2f4f241 100644 --- a/backend/src/lib/api-docs/constants.ts +++ b/backend/src/lib/api-docs/constants.ts @@ -2234,6 +2234,10 @@ export const AppConnections = { accessToken: "The Access Token used to access GitLab.", code: "The OAuth code to use to connect with GitLab.", accessTokenType: "The type of token used to connect with GitLab." + }, + ZABBIX: { + apiToken: "The API Token used to access Zabbix.", + instanceUrl: "The Zabbix instance URL to connect with." } } }; @@ -2422,6 +2426,12 @@ export const SecretSyncs = { CLOUDFLARE_PAGES: { projectName: "The name 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)" } } }; diff --git a/backend/src/server/routes/v1/app-connection-routers/app-connection-router.ts b/backend/src/server/routes/v1/app-connection-routers/app-connection-router.ts index 6160828f4..032dd939e 100644 --- a/backend/src/server/routes/v1/app-connection-routers/app-connection-router.ts +++ b/backend/src/server/routes/v1/app-connection-routers/app-connection-router.ts @@ -84,6 +84,7 @@ import { SanitizedWindmillConnectionSchema, WindmillConnectionListItemSchema } from "@app/services/app-connection/windmill"; +import { SanitizedZabbixConnectionSchema, ZabbixConnectionListItemSchema } from "@app/services/app-connection/zabbix"; import { AuthMode } from "@app/services/auth/auth-type"; // can't use discriminated due to multiple schemas for certain apps @@ -116,7 +117,8 @@ const SanitizedAppConnectionSchema = z.union([ ...SanitizedRenderConnectionSchema.options, ...SanitizedFlyioConnectionSchema.options, ...SanitizedGitLabConnectionSchema.options, - ...SanitizedCloudflareConnectionSchema.options + ...SanitizedCloudflareConnectionSchema.options, + ...SanitizedZabbixConnectionSchema.options ]); const AppConnectionOptionsSchema = z.discriminatedUnion("app", [ @@ -148,7 +150,8 @@ const AppConnectionOptionsSchema = z.discriminatedUnion("app", [ RenderConnectionListItemSchema, FlyioConnectionListItemSchema, GitLabConnectionListItemSchema, - CloudflareConnectionListItemSchema + CloudflareConnectionListItemSchema, + ZabbixConnectionListItemSchema ]); export const registerAppConnectionRouter = async (server: FastifyZodProvider) => { diff --git a/backend/src/server/routes/v1/app-connection-routers/index.ts b/backend/src/server/routes/v1/app-connection-routers/index.ts index cd4ccd728..35958ddf8 100644 --- a/backend/src/server/routes/v1/app-connection-routers/index.ts +++ b/backend/src/server/routes/v1/app-connection-routers/index.ts @@ -29,6 +29,7 @@ import { registerTeamCityConnectionRouter } from "./teamcity-connection-router"; import { registerTerraformCloudConnectionRouter } from "./terraform-cloud-router"; import { registerVercelConnectionRouter } from "./vercel-connection-router"; import { registerWindmillConnectionRouter } from "./windmill-connection-router"; +import { registerZabbixConnectionRouter } from "./zabbix-connection-router"; export * from "./app-connection-router"; @@ -62,5 +63,6 @@ export const APP_CONNECTION_REGISTER_ROUTER_MAP: Record { + 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; + } + }); +}; diff --git a/backend/src/server/routes/v1/secret-sync-routers/index.ts b/backend/src/server/routes/v1/secret-sync-routers/index.ts index 4675a1a40..67e1ac720 100644 --- a/backend/src/server/routes/v1/secret-sync-routers/index.ts +++ b/backend/src/server/routes/v1/secret-sync-routers/index.ts @@ -22,6 +22,7 @@ import { registerTeamCitySyncRouter } from "./teamcity-sync-router"; import { registerTerraformCloudSyncRouter } from "./terraform-cloud-sync-router"; import { registerVercelSyncRouter } from "./vercel-sync-router"; import { registerWindmillSyncRouter } from "./windmill-sync-router"; +import { registerZabbixSyncRouter } from "./zabbix-sync-router"; export * from "./secret-sync-router"; @@ -47,5 +48,6 @@ export const SECRET_SYNC_REGISTER_ROUTER_MAP: Record { diff --git a/backend/src/server/routes/v1/secret-sync-routers/zabbix-sync-router.ts b/backend/src/server/routes/v1/secret-sync-routers/zabbix-sync-router.ts new file mode 100644 index 000000000..cfd029623 --- /dev/null +++ b/backend/src/server/routes/v1/secret-sync-routers/zabbix-sync-router.ts @@ -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 + }); diff --git a/backend/src/services/app-connection/app-connection-enums.ts b/backend/src/services/app-connection/app-connection-enums.ts index 11b84b5ad..8e71f2285 100644 --- a/backend/src/services/app-connection/app-connection-enums.ts +++ b/backend/src/services/app-connection/app-connection-enums.ts @@ -27,7 +27,8 @@ export enum AppConnection { Render = "render", Flyio = "flyio", GitLab = "gitlab", - Cloudflare = "cloudflare" + Cloudflare = "cloudflare", + Zabbix = "zabbix" } export enum AWSRegion { diff --git a/backend/src/services/app-connection/app-connection-fns.ts b/backend/src/services/app-connection/app-connection-fns.ts index 78f6b99b5..cfaf97c3b 100644 --- a/backend/src/services/app-connection/app-connection-fns.ts +++ b/backend/src/services/app-connection/app-connection-fns.ts @@ -105,6 +105,7 @@ import { validateWindmillConnectionCredentials, WindmillConnectionMethod } from "./windmill"; +import { getZabbixConnectionListItem, validateZabbixConnectionCredentials, ZabbixConnectionMethod } from "./zabbix"; export const listAppConnectionOptions = () => { return [ @@ -136,7 +137,8 @@ export const listAppConnectionOptions = () => { getRenderConnectionListItem(), getFlyioConnectionListItem(), getGitLabConnectionListItem(), - getCloudflareConnectionListItem() + getCloudflareConnectionListItem(), + getZabbixConnectionListItem() ].sort((a, b) => a.name.localeCompare(b.name)); }; @@ -216,7 +218,8 @@ export const validateAppConnectionCredentials = async ( [AppConnection.Render]: validateRenderConnectionCredentials as TAppConnectionCredentialsValidator, [AppConnection.Flyio]: validateFlyioConnectionCredentials as TAppConnectionCredentialsValidator, [AppConnection.GitLab]: validateGitLabConnectionCredentials as TAppConnectionCredentialsValidator, - [AppConnection.Cloudflare]: validateCloudflareConnectionCredentials as TAppConnectionCredentialsValidator + [AppConnection.Cloudflare]: validateCloudflareConnectionCredentials as TAppConnectionCredentialsValidator, + [AppConnection.Zabbix]: validateZabbixConnectionCredentials as TAppConnectionCredentialsValidator }; return VALIDATE_APP_CONNECTION_CREDENTIALS_MAP[appConnection.app](appConnection); @@ -253,6 +256,7 @@ export const getAppConnectionMethodName = (method: TAppConnection["method"]) => case VercelConnectionMethod.ApiToken: case OnePassConnectionMethod.ApiToken: case CloudflareConnectionMethod.APIToken: + case ZabbixConnectionMethod.ApiToken: return "API Token"; case PostgresConnectionMethod.UsernameAndPassword: case MsSqlConnectionMethod.UsernameAndPassword: @@ -332,7 +336,8 @@ export const TRANSITION_CONNECTION_CREDENTIALS_TO_PLATFORM: Record< [AppConnection.Render]: platformManagedCredentialsNotSupported, [AppConnection.Flyio]: platformManagedCredentialsNotSupported, [AppConnection.GitLab]: platformManagedCredentialsNotSupported, - [AppConnection.Cloudflare]: platformManagedCredentialsNotSupported + [AppConnection.Cloudflare]: platformManagedCredentialsNotSupported, + [AppConnection.Zabbix]: platformManagedCredentialsNotSupported }; export const enterpriseAppCheck = async ( diff --git a/backend/src/services/app-connection/app-connection-maps.ts b/backend/src/services/app-connection/app-connection-maps.ts index 9c0a3b5b8..342e39d71 100644 --- a/backend/src/services/app-connection/app-connection-maps.ts +++ b/backend/src/services/app-connection/app-connection-maps.ts @@ -29,7 +29,8 @@ export const APP_CONNECTION_NAME_MAP: Record = { [AppConnection.Render]: "Render", [AppConnection.Flyio]: "Fly.io", [AppConnection.GitLab]: "GitLab", - [AppConnection.Cloudflare]: "Cloudflare" + [AppConnection.Cloudflare]: "Cloudflare", + [AppConnection.Zabbix]: "Zabbix" }; export const APP_CONNECTION_PLAN_MAP: Record = { @@ -61,5 +62,6 @@ export const APP_CONNECTION_PLAN_MAP: Record>>; @@ -232,6 +239,7 @@ export type TAppConnectionInput = { id: string } & ( | TFlyioConnectionInput | TGitLabConnectionInput | TCloudflareConnectionInput + | TZabbixConnectionInput ); export type TSqlConnectionInput = @@ -275,7 +283,8 @@ export type TAppConnectionConfig = | TRenderConnectionConfig | TFlyioConnectionConfig | TGitLabConnectionConfig - | TCloudflareConnectionConfig; + | TCloudflareConnectionConfig + | TZabbixConnectionConfig; export type TValidateAppConnectionCredentialsSchema = | TValidateAwsConnectionCredentialsSchema @@ -306,7 +315,8 @@ export type TValidateAppConnectionCredentialsSchema = | TValidateRenderConnectionCredentialsSchema | TValidateFlyioConnectionCredentialsSchema | TValidateGitLabConnectionCredentialsSchema - | TValidateCloudflareConnectionCredentialsSchema; + | TValidateCloudflareConnectionCredentialsSchema + | TValidateZabbixConnectionCredentialsSchema; export type TListAwsConnectionKmsKeys = { connectionId: string; diff --git a/backend/src/services/app-connection/zabbix/index.ts b/backend/src/services/app-connection/zabbix/index.ts new file mode 100644 index 000000000..0de17bde7 --- /dev/null +++ b/backend/src/services/app-connection/zabbix/index.ts @@ -0,0 +1,4 @@ +export * from "./zabbix-connection-enums"; +export * from "./zabbix-connection-fns"; +export * from "./zabbix-connection-schemas"; +export * from "./zabbix-connection-types"; diff --git a/backend/src/services/app-connection/zabbix/zabbix-connection-enums.ts b/backend/src/services/app-connection/zabbix/zabbix-connection-enums.ts new file mode 100644 index 000000000..690d7c609 --- /dev/null +++ b/backend/src/services/app-connection/zabbix/zabbix-connection-enums.ts @@ -0,0 +1,3 @@ +export enum ZabbixConnectionMethod { + ApiToken = "api-token" +} diff --git a/backend/src/services/app-connection/zabbix/zabbix-connection-fns.ts b/backend/src/services/app-connection/zabbix/zabbix-connection-fns.ts new file mode 100644 index 000000000..17839ee68 --- /dev/null +++ b/backend/src/services/app-connection/zabbix/zabbix-connection-fns.ts @@ -0,0 +1,105 @@ +import { AxiosError } from "axios"; + +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"; + +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(/\/$/, "")}/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 => { + const { apiToken, instanceUrl } = appConnection.credentials; + await blockLocalAndPrivateIpAddresses(instanceUrl); + + try { + const apiUrl = `${instanceUrl.replace(/\/$/, "")}/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" + }); + } +}; diff --git a/backend/src/services/app-connection/zabbix/zabbix-connection-schemas.ts b/backend/src/services/app-connection/zabbix/zabbix-connection-schemas.ts new file mode 100644 index 000000000..23bffd859 --- /dev/null +++ b/backend/src/services/app-connection/zabbix/zabbix-connection-schemas.ts @@ -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() +}); diff --git a/backend/src/services/app-connection/zabbix/zabbix-connection-service.ts b/backend/src/services/app-connection/zabbix/zabbix-connection-service.ts new file mode 100644 index 000000000..e8c8f8018 --- /dev/null +++ b/backend/src/services/app-connection/zabbix/zabbix-connection-service.ts @@ -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; + +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 + }; +}; diff --git a/backend/src/services/app-connection/zabbix/zabbix-connection-types.ts b/backend/src/services/app-connection/zabbix/zabbix-connection-types.ts new file mode 100644 index 000000000..08b4c685f --- /dev/null +++ b/backend/src/services/app-connection/zabbix/zabbix-connection-types.ts @@ -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; + +export type TZabbixConnectionInput = z.infer & { + app: AppConnection.Zabbix; +}; + +export type TValidateZabbixConnectionCredentialsSchema = typeof ValidateZabbixConnectionCredentialsSchema; + +export type TZabbixConnectionConfig = DiscriminativePick & { + orgId: string; +}; + +export type TZabbixHost = { + hostId: string; + host: string; +}; + +export type TZabbixHostListResponse = { + jsonrpc: string; + result: { hostid: string; host: string }[]; + error?: { message: string }; +}; diff --git a/backend/src/services/secret-sync/secret-sync-enums.ts b/backend/src/services/secret-sync/secret-sync-enums.ts index b70b37caf..62730c3da 100644 --- a/backend/src/services/secret-sync/secret-sync-enums.ts +++ b/backend/src/services/secret-sync/secret-sync-enums.ts @@ -20,7 +20,8 @@ export enum SecretSync { Render = "render", Flyio = "flyio", GitLab = "gitlab", - CloudflarePages = "cloudflare-pages" + CloudflarePages = "cloudflare-pages", + Zabbix = "zabbix" } export enum SecretSyncInitialSyncBehavior { diff --git a/backend/src/services/secret-sync/secret-sync-fns.ts b/backend/src/services/secret-sync/secret-sync-fns.ts index 9d0513a2c..d058d355e 100644 --- a/backend/src/services/secret-sync/secret-sync-fns.ts +++ b/backend/src/services/secret-sync/secret-sync-fns.ts @@ -45,6 +45,7 @@ import { TEAMCITY_SYNC_LIST_OPTION, TeamCitySyncFns } from "./teamcity"; import { TERRAFORM_CLOUD_SYNC_LIST_OPTION, TerraformCloudSyncFns } from "./terraform-cloud"; import { VERCEL_SYNC_LIST_OPTION, VercelSyncFns } from "./vercel"; import { WINDMILL_SYNC_LIST_OPTION, WindmillSyncFns } from "./windmill"; +import { ZABBIX_SYNC_LIST_OPTION, ZabbixSyncFns } from "./zabbix"; const SECRET_SYNC_LIST_OPTIONS: Record = { [SecretSync.AWSParameterStore]: AWS_PARAMETER_STORE_SYNC_LIST_OPTION, @@ -68,7 +69,8 @@ const SECRET_SYNC_LIST_OPTIONS: Record = { [SecretSync.Render]: RENDER_SYNC_LIST_OPTION, [SecretSync.Flyio]: FLYIO_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 = () => { @@ -236,6 +238,8 @@ export const SecretSyncFns = { return GitLabSyncFns.syncSecrets(secretSync, schemaSecretMap, { appConnectionDAL, kmsService }); case SecretSync.CloudflarePages: return CloudflarePagesSyncFns.syncSecrets(secretSync, schemaSecretMap); + case SecretSync.Zabbix: + return ZabbixSyncFns.syncSecrets(secretSync, schemaSecretMap); default: throw new Error( `Unhandled sync destination for sync secrets fns: ${(secretSync as TSecretSyncWithCredentials).destination}` @@ -328,6 +332,9 @@ export const SecretSyncFns = { case SecretSync.CloudflarePages: secretMap = await CloudflarePagesSyncFns.getSecrets(secretSync); break; + case SecretSync.Zabbix: + secretMap = await ZabbixSyncFns.getSecrets(secretSync); + break; default: throw new Error( `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 }); case SecretSync.CloudflarePages: return CloudflarePagesSyncFns.removeSecrets(secretSync, schemaSecretMap); + case SecretSync.Zabbix: + return ZabbixSyncFns.removeSecrets(secretSync, schemaSecretMap); default: throw new Error( `Unhandled sync destination for remove secrets fns: ${(secretSync as TSecretSyncWithCredentials).destination}` diff --git a/backend/src/services/secret-sync/secret-sync-maps.ts b/backend/src/services/secret-sync/secret-sync-maps.ts index 1dc0ea6c0..25df5d0b4 100644 --- a/backend/src/services/secret-sync/secret-sync-maps.ts +++ b/backend/src/services/secret-sync/secret-sync-maps.ts @@ -23,7 +23,8 @@ export const SECRET_SYNC_NAME_MAP: Record = { [SecretSync.Render]: "Render", [SecretSync.Flyio]: "Fly.io", [SecretSync.GitLab]: "GitLab", - [SecretSync.CloudflarePages]: "Cloudflare Pages" + [SecretSync.CloudflarePages]: "Cloudflare Pages", + [SecretSync.Zabbix]: "Zabbix" }; export const SECRET_SYNC_CONNECTION_MAP: Record = { @@ -48,7 +49,8 @@ export const SECRET_SYNC_CONNECTION_MAP: Record = { [SecretSync.Render]: AppConnection.Render, [SecretSync.Flyio]: AppConnection.Flyio, [SecretSync.GitLab]: AppConnection.GitLab, - [SecretSync.CloudflarePages]: AppConnection.Cloudflare + [SecretSync.CloudflarePages]: AppConnection.Cloudflare, + [SecretSync.Zabbix]: AppConnection.Zabbix }; export const SECRET_SYNC_PLAN_MAP: Record = { @@ -73,5 +75,6 @@ export const SECRET_SYNC_PLAN_MAP: Record = { [SecretSync.Render]: SecretSyncPlanType.Regular, [SecretSync.Flyio]: SecretSyncPlanType.Regular, [SecretSync.GitLab]: SecretSyncPlanType.Regular, - [SecretSync.CloudflarePages]: SecretSyncPlanType.Regular + [SecretSync.CloudflarePages]: SecretSyncPlanType.Regular, + [SecretSync.Zabbix]: SecretSyncPlanType.Regular }; diff --git a/backend/src/services/secret-sync/secret-sync-types.ts b/backend/src/services/secret-sync/secret-sync-types.ts index a31183280..b076ea9c4 100644 --- a/backend/src/services/secret-sync/secret-sync-types.ts +++ b/backend/src/services/secret-sync/secret-sync-types.ts @@ -113,6 +113,7 @@ import { TTerraformCloudSyncWithCredentials } from "./terraform-cloud"; import { TVercelSync, TVercelSyncInput, TVercelSyncListItem, TVercelSyncWithCredentials } from "./vercel"; +import { TZabbixSync, TZabbixSyncInput, TZabbixSyncListItem, TZabbixSyncWithCredentials } from "./zabbix"; export type TSecretSync = | TAwsParameterStoreSync @@ -136,7 +137,8 @@ export type TSecretSync = | TRenderSync | TFlyioSync | TGitLabSync - | TCloudflarePagesSync; + | TCloudflarePagesSync + | TZabbixSync; export type TSecretSyncWithCredentials = | TAwsParameterStoreSyncWithCredentials @@ -160,7 +162,8 @@ export type TSecretSyncWithCredentials = | TRenderSyncWithCredentials | TFlyioSyncWithCredentials | TGitLabSyncWithCredentials - | TCloudflarePagesSyncWithCredentials; + | TCloudflarePagesSyncWithCredentials + | TZabbixSyncWithCredentials; export type TSecretSyncInput = | TAwsParameterStoreSyncInput @@ -184,7 +187,8 @@ export type TSecretSyncInput = | TRenderSyncInput | TFlyioSyncInput | TGitLabSyncInput - | TCloudflarePagesSyncInput; + | TCloudflarePagesSyncInput + | TZabbixSyncInput; export type TSecretSyncListItem = | TAwsParameterStoreSyncListItem @@ -208,7 +212,8 @@ export type TSecretSyncListItem = | TRenderSyncListItem | TFlyioSyncListItem | TGitLabSyncListItem - | TCloudflarePagesSyncListItem; + | TCloudflarePagesSyncListItem + | TZabbixSyncListItem; export type TSyncOptionsConfig = { canImportSecrets: boolean; diff --git a/backend/src/services/secret-sync/zabbix/index.ts b/backend/src/services/secret-sync/zabbix/index.ts new file mode 100644 index 000000000..a49d8e14c --- /dev/null +++ b/backend/src/services/secret-sync/zabbix/index.ts @@ -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"; diff --git a/backend/src/services/secret-sync/zabbix/zabbix-sync-constants.ts b/backend/src/services/secret-sync/zabbix/zabbix-sync-constants.ts new file mode 100644 index 000000000..51c1ca793 --- /dev/null +++ b/backend/src/services/secret-sync/zabbix/zabbix-sync-constants.ts @@ -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 +}; diff --git a/backend/src/services/secret-sync/zabbix/zabbix-sync-enums.ts b/backend/src/services/secret-sync/zabbix/zabbix-sync-enums.ts new file mode 100644 index 000000000..8f4c8c5d6 --- /dev/null +++ b/backend/src/services/secret-sync/zabbix/zabbix-sync-enums.ts @@ -0,0 +1,4 @@ +export enum ZabbixSyncScope { + Global = "global", + Host = "host" +} diff --git a/backend/src/services/secret-sync/zabbix/zabbix-sync-fns.ts b/backend/src/services/secret-sync/zabbix/zabbix-sync-fns.ts new file mode 100644 index 000000000..a0e8a9e4f --- /dev/null +++ b/backend/src/services/secret-sync/zabbix/zabbix-sync-fns.ts @@ -0,0 +1,266 @@ +import { request } from "@app/lib/config/request"; +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"; + +// Helper function to handle Zabbix API responses and errors +const handleZabbixResponse = (response: ZabbixApiResponse): 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 => { + const apiUrl = `${instanceUrl.replace(/\/$/, "")}/api_jsonrpc.php`; + + 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 = 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 => { + const apiUrl = `${instanceUrl.replace(/\/$/, "")}/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 = 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 = 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 => { + if (keys.length === 0) return; + + const apiUrl = `${instanceUrl.replace(/\/$/, "")}/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 = 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; + 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(secret.macro.replace(/^\{\$/, "").replace(/\}$/, "")) + ) + .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; + 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(secret.macro.replace(/^\{\$/, "").replace(/\}$/, ""))) + .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; + const hostId = destinationConfig.scope === ZabbixSyncScope.Host ? destinationConfig.hostId : undefined; + + try { + const secrets = await listZabbixSecrets(apiToken, instanceUrl, hostId); + return Object.fromEntries( + secrets.map((secret) => [ + secret.macro.replace(/^\{\$/, "").replace(/\}$/, ""), + { value: secret.value ?? "", comment: secret.description } + ]) + ); + } catch (error) { + throw new SecretSyncError({ + error: error instanceof Error ? error : new Error("Failed to get secrets") + }); + } + } +}; diff --git a/backend/src/services/secret-sync/zabbix/zabbix-sync-schemas.ts b/backend/src/services/secret-sync/zabbix/zabbix-sync-schemas.ts new file mode 100644 index 000000000..94a729cb6 --- /dev/null +++ b/backend/src/services/secret-sync/zabbix/zabbix-sync-schemas.ts @@ -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) +}); diff --git a/backend/src/services/secret-sync/zabbix/zabbix-sync-types.ts b/backend/src/services/secret-sync/zabbix/zabbix-sync-types.ts new file mode 100644 index 000000000..9640394d9 --- /dev/null +++ b/backend/src/services/secret-sync/zabbix/zabbix-sync-types.ts @@ -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; +export type TZabbixSyncInput = z.infer; +export type TZabbixSyncListItem = z.infer; + +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 { + 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"; +} diff --git a/docs/api-reference/endpoints/app-connections/zabbix/available.mdx b/docs/api-reference/endpoints/app-connections/zabbix/available.mdx new file mode 100644 index 000000000..49488471e --- /dev/null +++ b/docs/api-reference/endpoints/app-connections/zabbix/available.mdx @@ -0,0 +1,4 @@ +--- +title: "Available" +openapi: "GET /api/v1/app-connections/zabbix/available" +--- diff --git a/docs/api-reference/endpoints/app-connections/zabbix/create.mdx b/docs/api-reference/endpoints/app-connections/zabbix/create.mdx new file mode 100644 index 000000000..a11b01309 --- /dev/null +++ b/docs/api-reference/endpoints/app-connections/zabbix/create.mdx @@ -0,0 +1,8 @@ +--- +title: "Create" +openapi: "POST /api/v1/app-connections/zabbix" +--- + + + Check out the configuration docs for [Zabbix Connections](/integrations/app-connections/zabbix) to learn how to obtain the required credentials. + diff --git a/docs/api-reference/endpoints/app-connections/zabbix/delete.mdx b/docs/api-reference/endpoints/app-connections/zabbix/delete.mdx new file mode 100644 index 000000000..95b34e814 --- /dev/null +++ b/docs/api-reference/endpoints/app-connections/zabbix/delete.mdx @@ -0,0 +1,4 @@ +--- +title: "Delete" +openapi: "DELETE /api/v1/app-connections/zabbix/{connectionId}" +--- diff --git a/docs/api-reference/endpoints/app-connections/zabbix/get-by-id.mdx b/docs/api-reference/endpoints/app-connections/zabbix/get-by-id.mdx new file mode 100644 index 000000000..46306b035 --- /dev/null +++ b/docs/api-reference/endpoints/app-connections/zabbix/get-by-id.mdx @@ -0,0 +1,4 @@ +--- +title: "Get by ID" +openapi: "GET /api/v1/app-connections/zabbix/{connectionId}" +--- diff --git a/docs/api-reference/endpoints/app-connections/zabbix/get-by-name.mdx b/docs/api-reference/endpoints/app-connections/zabbix/get-by-name.mdx new file mode 100644 index 000000000..692c69fc7 --- /dev/null +++ b/docs/api-reference/endpoints/app-connections/zabbix/get-by-name.mdx @@ -0,0 +1,4 @@ +--- +title: "Get by Name" +openapi: "GET /api/v1/app-connections/zabbix/connection-name/{connectionName}" +--- diff --git a/docs/api-reference/endpoints/app-connections/zabbix/list.mdx b/docs/api-reference/endpoints/app-connections/zabbix/list.mdx new file mode 100644 index 000000000..bc1c7df2b --- /dev/null +++ b/docs/api-reference/endpoints/app-connections/zabbix/list.mdx @@ -0,0 +1,4 @@ +--- +title: "List" +openapi: "GET /api/v1/app-connections/zabbix" +--- diff --git a/docs/api-reference/endpoints/app-connections/zabbix/update.mdx b/docs/api-reference/endpoints/app-connections/zabbix/update.mdx new file mode 100644 index 000000000..aa408c9ee --- /dev/null +++ b/docs/api-reference/endpoints/app-connections/zabbix/update.mdx @@ -0,0 +1,8 @@ +--- +title: "Update" +openapi: "PATCH /api/v1/app-connections/zabbix/{connectionId}" +--- + + + Check out the configuration docs for [Zabbix Connections](/integrations/app-connections/zabbix) to learn how to obtain the required credentials. + diff --git a/docs/api-reference/endpoints/secret-syncs/zabbix/create.mdx b/docs/api-reference/endpoints/secret-syncs/zabbix/create.mdx new file mode 100644 index 000000000..1d15bd7d5 --- /dev/null +++ b/docs/api-reference/endpoints/secret-syncs/zabbix/create.mdx @@ -0,0 +1,4 @@ +--- +title: "Create" +openapi: "POST /api/v1/secret-syncs/zabbix" +--- diff --git a/docs/api-reference/endpoints/secret-syncs/zabbix/delete.mdx b/docs/api-reference/endpoints/secret-syncs/zabbix/delete.mdx new file mode 100644 index 000000000..dc7345298 --- /dev/null +++ b/docs/api-reference/endpoints/secret-syncs/zabbix/delete.mdx @@ -0,0 +1,4 @@ +--- +title: "Delete" +openapi: "DELETE /api/v1/secret-syncs/zabbix/{syncId}" +--- diff --git a/docs/api-reference/endpoints/secret-syncs/zabbix/get-by-id.mdx b/docs/api-reference/endpoints/secret-syncs/zabbix/get-by-id.mdx new file mode 100644 index 000000000..79e787d04 --- /dev/null +++ b/docs/api-reference/endpoints/secret-syncs/zabbix/get-by-id.mdx @@ -0,0 +1,4 @@ +--- +title: "Get by ID" +openapi: "GET /api/v1/secret-syncs/zabbix/{syncId}" +--- diff --git a/docs/api-reference/endpoints/secret-syncs/zabbix/get-by-name.mdx b/docs/api-reference/endpoints/secret-syncs/zabbix/get-by-name.mdx new file mode 100644 index 000000000..2b364c699 --- /dev/null +++ b/docs/api-reference/endpoints/secret-syncs/zabbix/get-by-name.mdx @@ -0,0 +1,4 @@ +--- +title: "Get by Name" +openapi: "GET /api/v1/secret-syncs/zabbix/sync-name/{syncName}" +--- diff --git a/docs/api-reference/endpoints/secret-syncs/zabbix/import-secrets.mdx b/docs/api-reference/endpoints/secret-syncs/zabbix/import-secrets.mdx new file mode 100644 index 000000000..716f3c8d5 --- /dev/null +++ b/docs/api-reference/endpoints/secret-syncs/zabbix/import-secrets.mdx @@ -0,0 +1,4 @@ +--- +title: "Import Secrets" +openapi: "POST /api/v1/secret-syncs/zabbix/{syncId}/import-secrets" +--- diff --git a/docs/api-reference/endpoints/secret-syncs/zabbix/list.mdx b/docs/api-reference/endpoints/secret-syncs/zabbix/list.mdx new file mode 100644 index 000000000..d6d4bdef5 --- /dev/null +++ b/docs/api-reference/endpoints/secret-syncs/zabbix/list.mdx @@ -0,0 +1,4 @@ +--- +title: "List" +openapi: "GET /api/v1/secret-syncs/zabbix" +--- diff --git a/docs/api-reference/endpoints/secret-syncs/zabbix/remove-secrets.mdx b/docs/api-reference/endpoints/secret-syncs/zabbix/remove-secrets.mdx new file mode 100644 index 000000000..8fb422544 --- /dev/null +++ b/docs/api-reference/endpoints/secret-syncs/zabbix/remove-secrets.mdx @@ -0,0 +1,4 @@ +--- +title: "Remove Secrets" +openapi: "POST /api/v1/secret-syncs/zabbix/{syncId}/remove-secrets" +--- diff --git a/docs/api-reference/endpoints/secret-syncs/zabbix/sync-secrets.mdx b/docs/api-reference/endpoints/secret-syncs/zabbix/sync-secrets.mdx new file mode 100644 index 000000000..9751ad721 --- /dev/null +++ b/docs/api-reference/endpoints/secret-syncs/zabbix/sync-secrets.mdx @@ -0,0 +1,4 @@ +--- +title: "Sync Secrets" +openapi: "POST /api/v1/secret-syncs/zabbix/{syncId}/sync-secrets" +--- diff --git a/docs/api-reference/endpoints/secret-syncs/zabbix/update.mdx b/docs/api-reference/endpoints/secret-syncs/zabbix/update.mdx new file mode 100644 index 000000000..ea7582143 --- /dev/null +++ b/docs/api-reference/endpoints/secret-syncs/zabbix/update.mdx @@ -0,0 +1,4 @@ +--- +title: "Update" +openapi: "PATCH /api/v1/secret-syncs/zabbix/{syncId}" +--- diff --git a/docs/docs.json b/docs/docs.json index e0d0db73a..b611e6522 100644 --- a/docs/docs.json +++ b/docs/docs.json @@ -489,7 +489,8 @@ "integrations/app-connections/teamcity", "integrations/app-connections/terraform-cloud", "integrations/app-connections/vercel", - "integrations/app-connections/windmill" + "integrations/app-connections/windmill", + "integrations/app-connections/zabbix" ] } ] @@ -522,7 +523,8 @@ "integrations/secret-syncs/teamcity", "integrations/secret-syncs/terraform-cloud", "integrations/secret-syncs/vercel", - "integrations/secret-syncs/windmill" + "integrations/secret-syncs/windmill", + "integrations/secret-syncs/zabbix" ] } ] @@ -1510,6 +1512,18 @@ "api-reference/endpoints/app-connections/windmill/update", "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" + ] } ] }, @@ -1816,6 +1830,20 @@ "api-reference/endpoints/secret-syncs/windmill/import-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" + ] } ] }, diff --git a/docs/images/app-connections/zabbix/zabbit-app-connection-form.png b/docs/images/app-connections/zabbix/zabbit-app-connection-form.png new file mode 100644 index 000000000..90ac10f5f Binary files /dev/null and b/docs/images/app-connections/zabbix/zabbit-app-connection-form.png differ diff --git a/docs/images/app-connections/zabbix/zabbit-app-connection-generated.png b/docs/images/app-connections/zabbix/zabbit-app-connection-generated.png new file mode 100644 index 000000000..87cf99a20 Binary files /dev/null and b/docs/images/app-connections/zabbix/zabbit-app-connection-generated.png differ diff --git a/docs/images/app-connections/zabbix/zabbit-app-connection-option.png b/docs/images/app-connections/zabbix/zabbit-app-connection-option.png new file mode 100644 index 000000000..4cd01571c Binary files /dev/null and b/docs/images/app-connections/zabbix/zabbit-app-connection-option.png differ diff --git a/docs/images/app-connections/zabbix/zabbix-api-token-list.png b/docs/images/app-connections/zabbix/zabbix-api-token-list.png new file mode 100644 index 000000000..d549cc576 Binary files /dev/null and b/docs/images/app-connections/zabbix/zabbix-api-token-list.png differ diff --git a/docs/images/app-connections/zabbix/zabbix-dashboard.png b/docs/images/app-connections/zabbix/zabbix-dashboard.png new file mode 100644 index 000000000..1f80f2e1b Binary files /dev/null and b/docs/images/app-connections/zabbix/zabbix-dashboard.png differ diff --git a/docs/images/app-connections/zabbix/zabibix-api-token-form.png b/docs/images/app-connections/zabbix/zabibix-api-token-form.png new file mode 100644 index 000000000..471ecc4a4 Binary files /dev/null and b/docs/images/app-connections/zabbix/zabibix-api-token-form.png differ diff --git a/docs/images/app-connections/zabbix/zabibix-api-token-generated.png b/docs/images/app-connections/zabbix/zabibix-api-token-generated.png new file mode 100644 index 000000000..f05dd279e Binary files /dev/null and b/docs/images/app-connections/zabbix/zabibix-api-token-generated.png differ diff --git a/docs/images/secret-syncs/zabbix/configure-destination.png b/docs/images/secret-syncs/zabbix/configure-destination.png new file mode 100644 index 000000000..deb9ad993 Binary files /dev/null and b/docs/images/secret-syncs/zabbix/configure-destination.png differ diff --git a/docs/images/secret-syncs/zabbix/configure-details.png b/docs/images/secret-syncs/zabbix/configure-details.png new file mode 100644 index 000000000..c454de858 Binary files /dev/null and b/docs/images/secret-syncs/zabbix/configure-details.png differ diff --git a/docs/images/secret-syncs/zabbix/configure-source.png b/docs/images/secret-syncs/zabbix/configure-source.png new file mode 100644 index 000000000..82b2630b4 Binary files /dev/null and b/docs/images/secret-syncs/zabbix/configure-source.png differ diff --git a/docs/images/secret-syncs/zabbix/configure-sync-options.png b/docs/images/secret-syncs/zabbix/configure-sync-options.png new file mode 100644 index 000000000..ad54ce8ad Binary files /dev/null and b/docs/images/secret-syncs/zabbix/configure-sync-options.png differ diff --git a/docs/images/secret-syncs/zabbix/review-configuration.png b/docs/images/secret-syncs/zabbix/review-configuration.png new file mode 100644 index 000000000..25b0fbf8f Binary files /dev/null and b/docs/images/secret-syncs/zabbix/review-configuration.png differ diff --git a/docs/images/secret-syncs/zabbix/select-option.png b/docs/images/secret-syncs/zabbix/select-option.png new file mode 100644 index 000000000..9ebf248a0 Binary files /dev/null and b/docs/images/secret-syncs/zabbix/select-option.png differ diff --git a/docs/images/secret-syncs/zabbix/sync-created.png b/docs/images/secret-syncs/zabbix/sync-created.png new file mode 100644 index 000000000..e9924c82f Binary files /dev/null and b/docs/images/secret-syncs/zabbix/sync-created.png differ diff --git a/docs/integrations/app-connections/zabbix.mdx b/docs/integrations/app-connections/zabbix.mdx new file mode 100644 index 000000000..e8bb09dd9 --- /dev/null +++ b/docs/integrations/app-connections/zabbix.mdx @@ -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 + + + + ![Dashboard Page](/images/app-connections/zabbix/zabbix-dashboard.png) + + + ![Click Create Token](/images/app-connections/zabbix/zabbix-api-token-list.png) + + + 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) + + + 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) + + + +## Create Zabbix Connection in Infisical + + + + + + 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) + + + 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) + + + 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-modal.png) + + + 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) + + + + + 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", + } + } + } + ``` + + diff --git a/docs/integrations/secret-syncs/zabbix.mdx b/docs/integrations/secret-syncs/zabbix.mdx new file mode 100644 index 000000000..e89e2ec02 --- /dev/null +++ b/docs/integrations/secret-syncs/zabbix.mdx @@ -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) + + + + + + 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) + + + ![Select Zabbix](/images/secret-syncs/zabbix/select-option.png) + + + 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. + + + If you need to sync secrets from multiple folder locations, check out [secret imports](/documentation/platform/secret-reference#secret-imports). + + + + 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. + The remaining fields are determined by the selected **Scope**: + + + - **Host**: The host to sync secrets to. + + + + + Configure the **Sync Options** to specify how secrets should be synced, then click **Next**. + + ![Configure Options](/images/secret-syncs/zabbix/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. + + We highly recommend using a Key Schema to ensure that Infisical only manages the specific keys you intend, keeping everything else untouched. + + - **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. + + + 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. + + + Review your Zabbix Sync configuration, then click **Create Sync**. + + ![Review Configuration](/images/secret-syncs/zabbix/review-configuration.png) + + + If enabled, your Zabbix Sync will begin syncing your secrets to the destination endpoint. + + ![Sync Created](/images/secret-syncs/zabbix/sync-created.png) + + + + + 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 + } + } + } + ``` + + diff --git a/docs/mint.json b/docs/mint.json index 2dcb233d3..ed1dca7f5 100644 --- a/docs/mint.json +++ b/docs/mint.json @@ -522,7 +522,8 @@ "integrations/app-connections/teamcity", "integrations/app-connections/terraform-cloud", "integrations/app-connections/vercel", - "integrations/app-connections/windmill" + "integrations/app-connections/windmill", + "integrations/app-connections/zabbix" ] } ] @@ -554,7 +555,8 @@ "integrations/secret-syncs/teamcity", "integrations/secret-syncs/terraform-cloud", "integrations/secret-syncs/vercel", - "integrations/secret-syncs/windmill" + "integrations/secret-syncs/windmill", + "integrations/secret-syncs/zabbix" ] } ] @@ -1498,6 +1500,18 @@ "api-reference/endpoints/app-connections/windmill/update", "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" + ] } ] }, @@ -1791,6 +1805,20 @@ "api-reference/endpoints/secret-syncs/windmill/import-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" + ] } ] }, diff --git a/frontend/public/images/integrations/Zabbix.png b/frontend/public/images/integrations/Zabbix.png new file mode 100644 index 000000000..3ac67d2b4 Binary files /dev/null and b/frontend/public/images/integrations/Zabbix.png differ diff --git a/frontend/src/components/secret-syncs/forms/SecretSyncDestinationFields/SecretSyncDestinationFields.tsx b/frontend/src/components/secret-syncs/forms/SecretSyncDestinationFields/SecretSyncDestinationFields.tsx index b6074d270..da8686cc1 100644 --- a/frontend/src/components/secret-syncs/forms/SecretSyncDestinationFields/SecretSyncDestinationFields.tsx +++ b/frontend/src/components/secret-syncs/forms/SecretSyncDestinationFields/SecretSyncDestinationFields.tsx @@ -25,6 +25,7 @@ import { TeamCitySyncFields } from "./TeamCitySyncFields"; import { TerraformCloudSyncFields } from "./TerraformCloudSyncFields"; import { VercelSyncFields } from "./VercelSyncFields"; import { WindmillSyncFields } from "./WindmillSyncFields"; +import { ZabbixSyncFields } from "./ZabbixSyncFields"; export const SecretSyncDestinationFields = () => { const { watch } = useFormContext(); @@ -76,6 +77,8 @@ export const SecretSyncDestinationFields = () => { return ; case SecretSync.CloudflarePages: return ; + case SecretSync.Zabbix: + return ; default: throw new Error(`Unhandled Destination Config Field: ${destination}`); } diff --git a/frontend/src/components/secret-syncs/forms/SecretSyncDestinationFields/ZabbixSyncFields.tsx b/frontend/src/components/secret-syncs/forms/SecretSyncDestinationFields/ZabbixSyncFields.tsx new file mode 100644 index 000000000..45ab7bbaa --- /dev/null +++ b/frontend/src/components/secret-syncs/forms/SecretSyncDestinationFields/ZabbixSyncFields.tsx @@ -0,0 +1,146 @@ +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, + 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 ( + <> + { + setValue("destinationConfig.scope", ZabbixSyncScope.Global); + setValue("destinationConfig.hostId", ""); + setValue("destinationConfig.hostName", ""); + }} + /> + ( + +

+ Specify how Infisical should manage secrets from Zabbix. The following options are + available: +

+
    + {Object.values(ZABBIX_SYNC_SCOPES).map(({ name, description }) => { + return ( +
  • +

    + {name}: {description} +

    +
  • + ); + })} +
+ + } + > + +
+ )} + /> + {currentScope === ZabbixSyncScope.Host && ( + ( + + host.hostId === value) ?? null} + onChange={(option) => { + const selectedOption = option as SingleValue; + 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} + /> + + )} + /> + )} + ( + + + + )} + /> + + ); +}; diff --git a/frontend/src/components/secret-syncs/forms/SecretSyncOptionsFields/SecretSyncOptionsFields.tsx b/frontend/src/components/secret-syncs/forms/SecretSyncOptionsFields/SecretSyncOptionsFields.tsx index a61c2a6b8..b5a1a2603 100644 --- a/frontend/src/components/secret-syncs/forms/SecretSyncOptionsFields/SecretSyncOptionsFields.tsx +++ b/frontend/src/components/secret-syncs/forms/SecretSyncOptionsFields/SecretSyncOptionsFields.tsx @@ -57,6 +57,7 @@ export const SecretSyncOptionsFields = ({ hideInitialSync }: Props) => { case SecretSync.Flyio: case SecretSync.GitLab: case SecretSync.CloudflarePages: + case SecretSync.Zabbix: AdditionalSyncOptionsFieldsComponent = null; break; default: diff --git a/frontend/src/components/secret-syncs/forms/SecretSyncReviewFields/SecretSyncReviewFields.tsx b/frontend/src/components/secret-syncs/forms/SecretSyncReviewFields/SecretSyncReviewFields.tsx index fb639e91b..e2ffb9fa6 100644 --- a/frontend/src/components/secret-syncs/forms/SecretSyncReviewFields/SecretSyncReviewFields.tsx +++ b/frontend/src/components/secret-syncs/forms/SecretSyncReviewFields/SecretSyncReviewFields.tsx @@ -35,6 +35,7 @@ import { TeamCitySyncReviewFields } from "./TeamCitySyncReviewFields"; import { TerraformCloudSyncReviewFields } from "./TerraformCloudSyncReviewFields"; import { VercelSyncReviewFields } from "./VercelSyncReviewFields"; import { WindmillSyncReviewFields } from "./WindmillSyncReviewFields"; +import { ZabbixSyncReviewFields } from "./ZabbixSyncReviewFields"; export const SecretSyncReviewFields = () => { const { watch } = useFormContext(); @@ -124,6 +125,9 @@ export const SecretSyncReviewFields = () => { case SecretSync.CloudflarePages: DestinationFieldsComponent = ; break; + case SecretSync.Zabbix: + DestinationFieldsComponent = ; + break; default: throw new Error(`Unhandled Destination Review Fields: ${destination}`); } diff --git a/frontend/src/components/secret-syncs/forms/SecretSyncReviewFields/ZabbixSyncReviewFields.tsx b/frontend/src/components/secret-syncs/forms/SecretSyncReviewFields/ZabbixSyncReviewFields.tsx new file mode 100644 index 000000000..ecdaf3ac3 --- /dev/null +++ b/frontend/src/components/secret-syncs/forms/SecretSyncReviewFields/ZabbixSyncReviewFields.tsx @@ -0,0 +1,29 @@ +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"; + +export const ZabbixSyncReviewFields = () => { + const { watch } = useFormContext(); + const scope = watch("destinationConfig.scope"); + const hostId = watch("destinationConfig.hostId"); + const hostName = watch("destinationConfig.hostName"); + const macroType = watch("destinationConfig.macroType"); + + return ( + <> + {scope} + {scope === ZabbixSyncScope.Host && ( + <> + {hostId} + {hostName} + + )} + + {macroType === 0 ? "Text" : "Secret"} + + + ); +}; diff --git a/frontend/src/components/secret-syncs/forms/schemas/secret-sync-schema.ts b/frontend/src/components/secret-syncs/forms/schemas/secret-sync-schema.ts index 5d15492eb..331768e7f 100644 --- a/frontend/src/components/secret-syncs/forms/schemas/secret-sync-schema.ts +++ b/frontend/src/components/secret-syncs/forms/schemas/secret-sync-schema.ts @@ -22,6 +22,7 @@ import { TeamCitySyncDestinationSchema } from "./teamcity-sync-destination-schem import { TerraformCloudSyncDestinationSchema } from "./terraform-cloud-destination-schema"; import { VercelSyncDestinationSchema } from "./vercel-sync-destination-schema"; import { WindmillSyncDestinationSchema } from "./windmill-sync-destination-schema"; +import { ZabbixSyncDestinationSchema } from "./zabbix-sync-destination-schema"; const SecretSyncUnionSchema = z.discriminatedUnion("destination", [ AwsParameterStoreSyncDestinationSchema, @@ -45,7 +46,8 @@ const SecretSyncUnionSchema = z.discriminatedUnion("destination", [ RenderSyncDestinationSchema, FlyioSyncDestinationSchema, GitlabSyncDestinationSchema, - CloudflarePagesSyncDestinationSchema + CloudflarePagesSyncDestinationSchema, + ZabbixSyncDestinationSchema ]); export const SecretSyncFormSchema = SecretSyncUnionSchema; diff --git a/frontend/src/components/secret-syncs/forms/schemas/zabbix-sync-destination-schema.ts b/frontend/src/components/secret-syncs/forms/schemas/zabbix-sync-destination-schema.ts new file mode 100644 index 000000000..ae3a279a1 --- /dev/null +++ b/frontend/src/components/secret-syncs/forms/schemas/zabbix-sync-destination-schema.ts @@ -0,0 +1,23 @@ +import { z } from "zod"; + +import { BaseSecretSyncSchema } from "@app/components/secret-syncs/forms/schemas/base-secret-sync-schema"; +import { 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.number().min(0, "Macro type required").max(1, "Macro type required") + }), + z.object({ + scope: z.literal(ZabbixSyncScope.Global), + macroType: z.number().min(0, "Macro type required").max(1, "Macro type required") + }) + ]) + }) +); diff --git a/frontend/src/helpers/appConnections.ts b/frontend/src/helpers/appConnections.ts index e1fd9e365..321555f45 100644 --- a/frontend/src/helpers/appConnections.ts +++ b/frontend/src/helpers/appConnections.ts @@ -37,7 +37,8 @@ import { TeamCityConnectionMethod, TerraformCloudConnectionMethod, VercelConnectionMethod, - WindmillConnectionMethod + WindmillConnectionMethod, + ZabbixConnectionMethod } from "@app/hooks/api/appConnections/types"; import { HerokuConnectionMethod } from "@app/hooks/api/appConnections/types/heroku-connection"; import { OCIConnectionMethod } from "@app/hooks/api/appConnections/types/oci-connection"; @@ -88,7 +89,8 @@ export const APP_CONNECTION_MAP: Record< [AppConnection.Render]: { name: "Render", image: "Render.png" }, [AppConnection.Flyio]: { name: "Fly.io", image: "Flyio.svg" }, [AppConnection.Gitlab]: { name: "GitLab", image: "GitLab.png" }, - [AppConnection.Cloudflare]: { name: "Cloudflare", image: "Cloudflare.png" } + [AppConnection.Cloudflare]: { name: "Cloudflare", image: "Cloudflare.png" }, + [AppConnection.Zabbix]: { name: "Zabbix", image: "Zabbix.png" } }; export const getAppConnectionMethodDetails = (method: TAppConnection["method"]) => { @@ -120,6 +122,7 @@ export const getAppConnectionMethodDetails = (method: TAppConnection["method"]) case VercelConnectionMethod.ApiToken: case OnePassConnectionMethod.ApiToken: case CloudflareConnectionMethod.ApiToken: + case ZabbixConnectionMethod.ApiToken: return { name: "API Token", icon: faKey }; case PostgresConnectionMethod.UsernameAndPassword: case MsSqlConnectionMethod.UsernameAndPassword: diff --git a/frontend/src/helpers/secretSyncs.ts b/frontend/src/helpers/secretSyncs.ts index d6395397d..e42898036 100644 --- a/frontend/src/helpers/secretSyncs.ts +++ b/frontend/src/helpers/secretSyncs.ts @@ -81,6 +81,10 @@ export const SECRET_SYNC_MAP: Record = { [SecretSync.Render]: AppConnection.Render, [SecretSync.Flyio]: AppConnection.Flyio, [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< diff --git a/frontend/src/hooks/api/appConnections/enums.ts b/frontend/src/hooks/api/appConnections/enums.ts index 8097720a7..38de5b1b3 100644 --- a/frontend/src/hooks/api/appConnections/enums.ts +++ b/frontend/src/hooks/api/appConnections/enums.ts @@ -27,5 +27,6 @@ export enum AppConnection { Render = "render", Flyio = "flyio", Gitlab = "gitlab", - Cloudflare = "cloudflare" + Cloudflare = "cloudflare", + Zabbix = "zabbix" } diff --git a/frontend/src/hooks/api/appConnections/types/app-options.ts b/frontend/src/hooks/api/appConnections/types/app-options.ts index b71370da7..049f75471 100644 --- a/frontend/src/hooks/api/appConnections/types/app-options.ts +++ b/frontend/src/hooks/api/appConnections/types/app-options.ts @@ -132,6 +132,10 @@ export type TCloudflareConnectionOption = TAppConnectionOptionBase & { app: AppConnection.Cloudflare; }; +export type TZabbixConnectionOption = TAppConnectionOptionBase & { + app: AppConnection.Zabbix; +}; + export type TAppConnectionOption = | TAwsConnectionOption | TGitHubConnectionOption @@ -159,7 +163,8 @@ export type TAppConnectionOption = | TRenderConnectionOption | TFlyioConnectionOption | TGitlabConnectionOption - | TCloudflareConnectionOption; + | TCloudflareConnectionOption + | TZabbixConnectionOption; export type TAppConnectionOptionMap = { [AppConnection.AWS]: TAwsConnectionOption; @@ -191,4 +196,5 @@ export type TAppConnectionOptionMap = { [AppConnection.Flyio]: TFlyioConnectionOption; [AppConnection.Gitlab]: TGitlabConnectionOption; [AppConnection.Cloudflare]: TCloudflareConnectionOption; + [AppConnection.Zabbix]: TZabbixConnectionOption; }; diff --git a/frontend/src/hooks/api/appConnections/types/index.ts b/frontend/src/hooks/api/appConnections/types/index.ts index 2eaebb45a..3bcf4fe5c 100644 --- a/frontend/src/hooks/api/appConnections/types/index.ts +++ b/frontend/src/hooks/api/appConnections/types/index.ts @@ -29,6 +29,7 @@ import { TTeamCityConnection } from "./teamcity-connection"; import { TTerraformCloudConnection } from "./terraform-cloud-connection"; import { TVercelConnection } from "./vercel-connection"; import { TWindmillConnection } from "./windmill-connection"; +import { TZabbixConnection } from "./zabbix-connection"; export * from "./1password-connection"; export * from "./auth0-connection"; @@ -59,6 +60,7 @@ export * from "./teamcity-connection"; export * from "./terraform-cloud-connection"; export * from "./vercel-connection"; export * from "./windmill-connection"; +export * from "./zabbix-connection"; export type TAppConnection = | TAwsConnection @@ -89,7 +91,8 @@ export type TAppConnection = | TRenderConnection | TFlyioConnection | TGitLabConnection - | TCloudflareConnection; + | TCloudflareConnection + | TZabbixConnection; export type TAvailableAppConnection = Pick; @@ -146,4 +149,5 @@ export type TAppConnectionMap = { [AppConnection.Flyio]: TFlyioConnection; [AppConnection.Gitlab]: TGitLabConnection; [AppConnection.Cloudflare]: TCloudflareConnection; + [AppConnection.Zabbix]: TZabbixConnection; }; diff --git a/frontend/src/hooks/api/appConnections/types/zabbix-connection.ts b/frontend/src/hooks/api/appConnections/types/zabbix-connection.ts new file mode 100644 index 000000000..b8eaa9636 --- /dev/null +++ b/frontend/src/hooks/api/appConnections/types/zabbix-connection.ts @@ -0,0 +1,14 @@ +import { AppConnection } from "@app/hooks/api/appConnections/enums"; +import { TRootAppConnection } from "@app/hooks/api/appConnections/types/root-connection"; + +export enum ZabbixConnectionMethod { + ApiToken = "api-token" +} + +export type TZabbixConnection = TRootAppConnection & { app: AppConnection.Zabbix } & { + method: ZabbixConnectionMethod.ApiToken; + credentials: { + apiToken: string; + instanceUrl: string; + }; +}; diff --git a/frontend/src/hooks/api/appConnections/zabbix/index.ts b/frontend/src/hooks/api/appConnections/zabbix/index.ts new file mode 100644 index 000000000..2c1906d36 --- /dev/null +++ b/frontend/src/hooks/api/appConnections/zabbix/index.ts @@ -0,0 +1,2 @@ +export * from "./queries"; +export * from "./types"; diff --git a/frontend/src/hooks/api/appConnections/zabbix/queries.tsx b/frontend/src/hooks/api/appConnections/zabbix/queries.tsx new file mode 100644 index 000000000..5c7d1cc19 --- /dev/null +++ b/frontend/src/hooks/api/appConnections/zabbix/queries.tsx @@ -0,0 +1,36 @@ +import { useQuery, UseQueryOptions } from "@tanstack/react-query"; + +import { apiRequest } from "@app/config/request"; + +import { appConnectionKeys } from "../queries"; +import { TZabbixHost } from "./types"; + +const zabbixConnectionKeys = { + all: [...appConnectionKeys.all, "zabbix"] as const, + listHosts: (connectionId: string) => [...zabbixConnectionKeys.all, "hosts", connectionId] as const +}; + +export const useZabbixConnectionListHosts = ( + connectionId: string, + options?: Omit< + UseQueryOptions< + TZabbixHost[], + unknown, + TZabbixHost[], + ReturnType + >, + "queryKey" | "queryFn" + > +) => { + return useQuery({ + queryKey: zabbixConnectionKeys.listHosts(connectionId), + queryFn: async () => { + const { data } = await apiRequest.get( + `/api/v1/app-connections/zabbix/${connectionId}/hosts` + ); + + return data; + }, + ...options + }); +}; diff --git a/frontend/src/hooks/api/appConnections/zabbix/types.ts b/frontend/src/hooks/api/appConnections/zabbix/types.ts new file mode 100644 index 000000000..c5bc1424c --- /dev/null +++ b/frontend/src/hooks/api/appConnections/zabbix/types.ts @@ -0,0 +1,20 @@ +export type TZabbixHost = { + host: string; + hostId: string; +}; + +export enum ZabbixSyncScope { + Host = "host", + Global = "global" +} + +export const ZABBIX_SYNC_SCOPES = { + [ZabbixSyncScope.Host]: { + name: "Host", + description: "Sync secrets to a specific host in Zabbix." + }, + [ZabbixSyncScope.Global]: { + name: "Global", + description: "Sync secrets to a global scope in Zabbix." + } +}; diff --git a/frontend/src/hooks/api/secretSyncs/enums.ts b/frontend/src/hooks/api/secretSyncs/enums.ts index 66834ced5..ab79f73bb 100644 --- a/frontend/src/hooks/api/secretSyncs/enums.ts +++ b/frontend/src/hooks/api/secretSyncs/enums.ts @@ -20,7 +20,8 @@ export enum SecretSync { Render = "render", Flyio = "flyio", GitLab = "gitlab", - CloudflarePages = "cloudflare-pages" + CloudflarePages = "cloudflare-pages", + Zabbix = "zabbix" } export enum SecretSyncStatus { diff --git a/frontend/src/hooks/api/secretSyncs/types/index.ts b/frontend/src/hooks/api/secretSyncs/types/index.ts index 3e254e9aa..33119b8a3 100644 --- a/frontend/src/hooks/api/secretSyncs/types/index.ts +++ b/frontend/src/hooks/api/secretSyncs/types/index.ts @@ -23,6 +23,7 @@ import { TTeamCitySync } from "./teamcity-sync"; import { TTerraformCloudSync } from "./terraform-cloud-sync"; import { TVercelSync } from "./vercel-sync"; import { TWindmillSync } from "./windmill-sync"; +import { TZabbixSync } from "./zabbix-sync"; export type TSecretSyncOption = { name: string; @@ -53,7 +54,8 @@ export type TSecretSync = | TRenderSync | TFlyioSync | TGitLabSync - | TCloudflarePagesSync; + | TCloudflarePagesSync + | TZabbixSync; export type TListSecretSyncs = { secretSyncs: TSecretSync[] }; diff --git a/frontend/src/hooks/api/secretSyncs/types/zabbix-sync.ts b/frontend/src/hooks/api/secretSyncs/types/zabbix-sync.ts new file mode 100644 index 000000000..cb8a4bac6 --- /dev/null +++ b/frontend/src/hooks/api/secretSyncs/types/zabbix-sync.ts @@ -0,0 +1,25 @@ +import { AppConnection } from "@app/hooks/api/appConnections/enums"; +import { SecretSync } from "@app/hooks/api/secretSyncs"; +import { TRootSecretSync } from "@app/hooks/api/secretSyncs/types/root-sync"; + +import { ZabbixSyncScope } from "../../appConnections/zabbix"; + +export type TZabbixSync = TRootSecretSync & { + destination: SecretSync.Zabbix; + destinationConfig: + | { + scope: ZabbixSyncScope.Host; + hostId: string; + hostName: string; + macroType: number; + } + | { + scope: ZabbixSyncScope.Global; + macroType: number; + }; + connection: { + app: AppConnection.Zabbix; + name: string; + id: string; + }; +}; diff --git a/frontend/src/pages/organization/AppConnections/AppConnectionsPage/components/AppConnectionForm/AppConnectionForm.tsx b/frontend/src/pages/organization/AppConnections/AppConnectionsPage/components/AppConnectionForm/AppConnectionForm.tsx index 52abfee5d..140857110 100644 --- a/frontend/src/pages/organization/AppConnections/AppConnectionsPage/components/AppConnectionForm/AppConnectionForm.tsx +++ b/frontend/src/pages/organization/AppConnections/AppConnectionsPage/components/AppConnectionForm/AppConnectionForm.tsx @@ -38,6 +38,7 @@ import { TeamCityConnectionForm } from "./TeamCityConnectionForm"; import { TerraformCloudConnectionForm } from "./TerraformCloudConnectionForm"; import { VercelConnectionForm } from "./VercelConnectionForm"; import { WindmillConnectionForm } from "./WindmillConnectionForm"; +import { ZabbixConnectionForm } from "./ZabbixConnectionForm"; type FormProps = { onComplete: (appConnection: TAppConnection) => void; @@ -134,6 +135,8 @@ const CreateForm = ({ app, onComplete }: CreateFormProps) => { return ; case AppConnection.Cloudflare: return ; + case AppConnection.Zabbix: + return ; default: throw new Error(`Unhandled App ${app}`); } @@ -228,6 +231,8 @@ const UpdateForm = ({ appConnection, onComplete }: UpdateFormProps) => { return ; case AppConnection.Cloudflare: return ; + case AppConnection.Zabbix: + return ; default: throw new Error(`Unhandled App ${(appConnection as TAppConnection).app}`); } diff --git a/frontend/src/pages/organization/AppConnections/AppConnectionsPage/components/AppConnectionForm/ZabbixConnectionForm.tsx b/frontend/src/pages/organization/AppConnections/AppConnectionsPage/components/AppConnectionForm/ZabbixConnectionForm.tsx new file mode 100644 index 000000000..93746a8c6 --- /dev/null +++ b/frontend/src/pages/organization/AppConnections/AppConnectionsPage/components/AppConnectionForm/ZabbixConnectionForm.tsx @@ -0,0 +1,149 @@ +import { Controller, FormProvider, useForm } from "react-hook-form"; +import { zodResolver } from "@hookform/resolvers/zod"; +import { z } from "zod"; + +import { + Button, + FormControl, + Input, + ModalClose, + SecretInput, + Select, + SelectItem +} from "@app/components/v2"; +import { APP_CONNECTION_MAP, getAppConnectionMethodDetails } from "@app/helpers/appConnections"; +import { TZabbixConnection, ZabbixConnectionMethod } from "@app/hooks/api/appConnections"; +import { AppConnection } from "@app/hooks/api/appConnections/enums"; + +import { + genericAppConnectionFieldsSchema, + GenericAppConnectionsFields +} from "./GenericAppConnectionFields"; + +type Props = { + appConnection?: TZabbixConnection; + onSubmit: (formData: FormData) => void; +}; + +const rootSchema = genericAppConnectionFieldsSchema.extend({ + app: z.literal(AppConnection.Zabbix) +}); + +const formSchema = z.discriminatedUnion("method", [ + rootSchema.extend({ + method: z.literal(ZabbixConnectionMethod.ApiToken), + credentials: z.object({ + apiToken: z.string().trim().min(1, "API Token required"), + instanceUrl: z.string().trim().url("Invalid instance URL") + }) + }) +]); + +type FormData = z.infer; + +export const ZabbixConnectionForm = ({ appConnection, onSubmit }: Props) => { + const isUpdate = Boolean(appConnection); + + const form = useForm({ + resolver: zodResolver(formSchema), + defaultValues: appConnection ?? { + app: AppConnection.Zabbix, + method: ZabbixConnectionMethod.ApiToken + } + }); + + const { + handleSubmit, + control, + formState: { isSubmitting, isDirty } + } = form; + + return ( + +
+ {!isUpdate && } + ( + + + + )} + /> + ( + + + + )} + /> + ( + + onChange(e.target.value)} + /> + + )} + /> +
+ + + + +
+ +
+ ); +}; diff --git a/frontend/src/pages/secret-manager/IntegrationsListPage/components/SecretSyncsTab/SecretSyncTable/SecretSyncDestinationCol/SecretSyncDestinationCol.tsx b/frontend/src/pages/secret-manager/IntegrationsListPage/components/SecretSyncsTab/SecretSyncTable/SecretSyncDestinationCol/SecretSyncDestinationCol.tsx index 01064ef1d..c980df5bd 100644 --- a/frontend/src/pages/secret-manager/IntegrationsListPage/components/SecretSyncsTab/SecretSyncTable/SecretSyncDestinationCol/SecretSyncDestinationCol.tsx +++ b/frontend/src/pages/secret-manager/IntegrationsListPage/components/SecretSyncsTab/SecretSyncTable/SecretSyncDestinationCol/SecretSyncDestinationCol.tsx @@ -22,6 +22,7 @@ import { TeamCitySyncDestinationCol } from "./TeamCitySyncDestinationCol"; import { TerraformCloudSyncDestinationCol } from "./TerraformCloudSyncDestinationCol"; import { VercelSyncDestinationCol } from "./VercelSyncDestinationCol"; import { WindmillSyncDestinationCol } from "./WindmillSyncDestinationCol"; +import { ZabbixSyncDestinationCol } from "./ZabbixSyncDestinationCol"; type Props = { secretSync: TSecretSync; @@ -73,6 +74,8 @@ export const SecretSyncDestinationCol = ({ secretSync }: Props) => { return ; case SecretSync.CloudflarePages: return ; + case SecretSync.Zabbix: + return ; default: throw new Error( `Unhandled Secret Sync Destination Col: ${(secretSync as TSecretSync).destination}` diff --git a/frontend/src/pages/secret-manager/IntegrationsListPage/components/SecretSyncsTab/SecretSyncTable/SecretSyncDestinationCol/ZabbixSyncDestinationCol.tsx b/frontend/src/pages/secret-manager/IntegrationsListPage/components/SecretSyncsTab/SecretSyncTable/SecretSyncDestinationCol/ZabbixSyncDestinationCol.tsx new file mode 100644 index 000000000..849e64d55 --- /dev/null +++ b/frontend/src/pages/secret-manager/IntegrationsListPage/components/SecretSyncsTab/SecretSyncTable/SecretSyncDestinationCol/ZabbixSyncDestinationCol.tsx @@ -0,0 +1,14 @@ +import { TZabbixSync } from "@app/hooks/api/secretSyncs/types/zabbix-sync"; + +import { getSecretSyncDestinationColValues } from "../helpers"; +import { SecretSyncTableCell } from "../SecretSyncTableCell"; + +type Props = { + secretSync: TZabbixSync; +}; + +export const ZabbixSyncDestinationCol = ({ secretSync }: Props) => { + const { primaryText, secondaryText } = getSecretSyncDestinationColValues(secretSync); + + return ; +}; diff --git a/frontend/src/pages/secret-manager/IntegrationsListPage/components/SecretSyncsTab/SecretSyncTable/helpers/index.ts b/frontend/src/pages/secret-manager/IntegrationsListPage/components/SecretSyncsTab/SecretSyncTable/helpers/index.ts index 26c9144d7..1787162e3 100644 --- a/frontend/src/pages/secret-manager/IntegrationsListPage/components/SecretSyncsTab/SecretSyncTable/helpers/index.ts +++ b/frontend/src/pages/secret-manager/IntegrationsListPage/components/SecretSyncsTab/SecretSyncTable/helpers/index.ts @@ -1,4 +1,5 @@ import { TerraformCloudSyncScope } from "@app/hooks/api/appConnections/terraform-cloud"; +import { ZabbixSyncScope } from "@app/hooks/api/appConnections/zabbix"; import { SecretSync, TSecretSync } from "@app/hooks/api/secretSyncs"; import { GcpSyncScope } from "@app/hooks/api/secretSyncs/types/gcp-sync"; import { @@ -144,6 +145,17 @@ export const getSecretSyncDestinationColValues = (secretSync: TSecretSync) => { primaryText = destinationConfig.projectName; secondaryText = destinationConfig.environment; break; + case SecretSync.Zabbix: + if (destinationConfig.scope === ZabbixSyncScope.Host) { + primaryText = destinationConfig.hostName; + secondaryText = destinationConfig.hostId; + } else if (destinationConfig.scope === ZabbixSyncScope.Global) { + primaryText = "Global"; + secondaryText = ""; + } else { + throw new Error(`Unhandled Zabbix Scope Destination Col Values ${destination}`); + } + break; default: throw new Error(`Unhandled Destination Col Values ${destination}`); } diff --git a/frontend/src/pages/secret-manager/SecretSyncDetailsByIDPage/components/SecretSyncDestinationSection/SecretSyncDestinatonSection.tsx b/frontend/src/pages/secret-manager/SecretSyncDetailsByIDPage/components/SecretSyncDestinationSection/SecretSyncDestinatonSection.tsx index f49b75b52..15c1b61f1 100644 --- a/frontend/src/pages/secret-manager/SecretSyncDetailsByIDPage/components/SecretSyncDestinationSection/SecretSyncDestinatonSection.tsx +++ b/frontend/src/pages/secret-manager/SecretSyncDetailsByIDPage/components/SecretSyncDestinationSection/SecretSyncDestinatonSection.tsx @@ -33,6 +33,7 @@ import { TeamCitySyncDestinationSection } from "./TeamCitySyncDestinationSection import { TerraformCloudSyncDestinationSection } from "./TerraformCloudSyncDestinationSection"; import { VercelSyncDestinationSection } from "./VercelSyncDestinationSection"; import { WindmillSyncDestinationSection } from "./WindmillSyncDestinationSection"; +import { ZabbixSyncDestinationSection } from "./ZabbixSyncDestinationSection"; type Props = { secretSync: TSecretSync; @@ -114,6 +115,9 @@ export const SecretSyncDestinationSection = ({ secretSync, onEditDestination }: case SecretSync.CloudflarePages: DestinationComponents = ; break; + case SecretSync.Zabbix: + DestinationComponents = ; + break; default: throw new Error(`Unhandled Destination Section components: ${destination}`); } diff --git a/frontend/src/pages/secret-manager/SecretSyncDetailsByIDPage/components/SecretSyncDestinationSection/ZabbixSyncDestinationSection.tsx b/frontend/src/pages/secret-manager/SecretSyncDetailsByIDPage/components/SecretSyncDestinationSection/ZabbixSyncDestinationSection.tsx new file mode 100644 index 000000000..4110697c5 --- /dev/null +++ b/frontend/src/pages/secret-manager/SecretSyncDetailsByIDPage/components/SecretSyncDestinationSection/ZabbixSyncDestinationSection.tsx @@ -0,0 +1,31 @@ +import { GenericFieldLabel } from "@app/components/secret-syncs"; +import { ZabbixSyncScope } from "@app/hooks/api/appConnections/zabbix"; +import { TZabbixSync } from "@app/hooks/api/secretSyncs/types/zabbix-sync"; + +type Props = { + secretSync: TZabbixSync; +}; + +export const ZabbixSyncDestinationSection = ({ secretSync }: Props) => { + const { + destinationConfig: { macroType } + } = secretSync; + + return ( + <> + {secretSync.destinationConfig.scope === ZabbixSyncScope.Host && ( + <> + + {secretSync.destinationConfig.hostName} + + + {secretSync.destinationConfig.hostId} + + + )} + + {macroType === 0 ? "Text" : "Secret"} + + + ); +}; diff --git a/frontend/src/pages/secret-manager/SecretSyncDetailsByIDPage/components/SecretSyncOptionsSection/SecretSyncOptionsSection.tsx b/frontend/src/pages/secret-manager/SecretSyncDetailsByIDPage/components/SecretSyncOptionsSection/SecretSyncOptionsSection.tsx index a8406d13d..225800783 100644 --- a/frontend/src/pages/secret-manager/SecretSyncDetailsByIDPage/components/SecretSyncOptionsSection/SecretSyncOptionsSection.tsx +++ b/frontend/src/pages/secret-manager/SecretSyncDetailsByIDPage/components/SecretSyncOptionsSection/SecretSyncOptionsSection.tsx @@ -60,6 +60,7 @@ export const SecretSyncOptionsSection = ({ secretSync, onEditOptions }: Props) = case SecretSync.Flyio: case SecretSync.GitLab: case SecretSync.CloudflarePages: + case SecretSync.Zabbix: AdditionalSyncOptionsComponent = null; break; default: