diff --git a/backend/src/@types/fastify.d.ts b/backend/src/@types/fastify.d.ts index c023470385..1cb11545cf 100644 --- a/backend/src/@types/fastify.d.ts +++ b/backend/src/@types/fastify.d.ts @@ -16,6 +16,9 @@ import { TExternalKmsServiceFactory } from "@app/ee/services/external-kms/extern import { TGroupServiceFactory } from "@app/ee/services/group/group-service"; import { TIdentityProjectAdditionalPrivilegeServiceFactory } from "@app/ee/services/identity-project-additional-privilege/identity-project-additional-privilege-service"; import { TIdentityProjectAdditionalPrivilegeV2ServiceFactory } from "@app/ee/services/identity-project-additional-privilege-v2/identity-project-additional-privilege-v2-service"; +import { TKmipClientDALFactory } from "@app/ee/services/kmip/kmip-client-dal"; +import { TKmipOperationServiceFactory } from "@app/ee/services/kmip/kmip-operation-service"; +import { TKmipServiceFactory } from "@app/ee/services/kmip/kmip-service"; import { TLdapConfigServiceFactory } from "@app/ee/services/ldap-config/ldap-config-service"; import { TLicenseServiceFactory } from "@app/ee/services/license/license-service"; import { TOidcConfigServiceFactory } from "@app/ee/services/oidc/oidc-config-service"; @@ -126,6 +129,11 @@ declare module "fastify" { isUserCompleted: string; providerAuthToken: string; }; + kmipUser: { + projectId: string; + clientId: string; + name: string; + }; auditLogInfo: Pick; ssoConfig: Awaited>; ldapConfig: Awaited>; @@ -218,11 +226,14 @@ declare module "fastify" { totp: TTotpServiceFactory; appConnection: TAppConnectionServiceFactory; secretSync: TSecretSyncServiceFactory; + kmip: TKmipServiceFactory; + kmipOperation: TKmipOperationServiceFactory; }; // this is exclusive use for middlewares in which we need to inject data // everywhere else access using service layer store: { user: Pick; + kmipClient: Pick; }; } } diff --git a/backend/src/@types/knex.d.ts b/backend/src/@types/knex.d.ts index 2dad77392b..27f6b894c0 100644 --- a/backend/src/@types/knex.d.ts +++ b/backend/src/@types/knex.d.ts @@ -143,6 +143,18 @@ import { TInternalKms, TInternalKmsInsert, TInternalKmsUpdate, + TKmipClientCertificates, + TKmipClientCertificatesInsert, + TKmipClientCertificatesUpdate, + TKmipClients, + TKmipClientsInsert, + TKmipClientsUpdate, + TKmipOrgConfigs, + TKmipOrgConfigsInsert, + TKmipOrgConfigsUpdate, + TKmipOrgServerCertificates, + TKmipOrgServerCertificatesInsert, + TKmipOrgServerCertificatesUpdate, TKmsKeys, TKmsKeysInsert, TKmsKeysUpdate, @@ -902,5 +914,21 @@ declare module "knex/types/tables" { TAppConnectionsUpdate >; [TableName.SecretSync]: KnexOriginal.CompositeTableType; + [TableName.KmipClient]: KnexOriginal.CompositeTableType; + [TableName.KmipOrgConfig]: KnexOriginal.CompositeTableType< + TKmipOrgConfigs, + TKmipOrgConfigsInsert, + TKmipOrgConfigsUpdate + >; + [TableName.KmipOrgServerCertificates]: KnexOriginal.CompositeTableType< + TKmipOrgServerCertificates, + TKmipOrgServerCertificatesInsert, + TKmipOrgServerCertificatesUpdate + >; + [TableName.KmipClientCertificates]: KnexOriginal.CompositeTableType< + TKmipClientCertificates, + TKmipClientCertificatesInsert, + TKmipClientCertificatesUpdate + >; } } diff --git a/backend/src/db/migrations/20250203141127_add-kmip.ts b/backend/src/db/migrations/20250203141127_add-kmip.ts new file mode 100644 index 0000000000..ae63fbfe41 --- /dev/null +++ b/backend/src/db/migrations/20250203141127_add-kmip.ts @@ -0,0 +1,108 @@ +import { Knex } from "knex"; + +import { TableName } from "../schemas"; +import { createOnUpdateTrigger, dropOnUpdateTrigger } from "../utils"; + +export async function up(knex: Knex): Promise { + const hasKmipClientTable = await knex.schema.hasTable(TableName.KmipClient); + if (!hasKmipClientTable) { + await knex.schema.createTable(TableName.KmipClient, (t) => { + t.uuid("id", { primaryKey: true }).defaultTo(knex.fn.uuid()); + t.string("name").notNullable(); + t.specificType("permissions", "text[]"); + t.string("description"); + t.string("projectId").notNullable(); + t.foreign("projectId").references("id").inTable(TableName.Project).onDelete("CASCADE"); + }); + } + + const hasKmipOrgPkiConfig = await knex.schema.hasTable(TableName.KmipOrgConfig); + if (!hasKmipOrgPkiConfig) { + await knex.schema.createTable(TableName.KmipOrgConfig, (t) => { + t.uuid("id", { primaryKey: true }).defaultTo(knex.fn.uuid()); + + t.uuid("orgId").notNullable(); + t.foreign("orgId").references("id").inTable(TableName.Organization).onDelete("CASCADE"); + t.unique("orgId"); + + t.string("caKeyAlgorithm").notNullable(); + + t.datetime("rootCaIssuedAt").notNullable(); + t.datetime("rootCaExpiration").notNullable(); + t.string("rootCaSerialNumber").notNullable(); + t.binary("encryptedRootCaCertificate").notNullable(); + t.binary("encryptedRootCaPrivateKey").notNullable(); + + t.datetime("serverIntermediateCaIssuedAt").notNullable(); + t.datetime("serverIntermediateCaExpiration").notNullable(); + t.string("serverIntermediateCaSerialNumber"); + t.binary("encryptedServerIntermediateCaCertificate").notNullable(); + t.binary("encryptedServerIntermediateCaChain").notNullable(); + t.binary("encryptedServerIntermediateCaPrivateKey").notNullable(); + + t.datetime("clientIntermediateCaIssuedAt").notNullable(); + t.datetime("clientIntermediateCaExpiration").notNullable(); + t.string("clientIntermediateCaSerialNumber").notNullable(); + t.binary("encryptedClientIntermediateCaCertificate").notNullable(); + t.binary("encryptedClientIntermediateCaChain").notNullable(); + t.binary("encryptedClientIntermediateCaPrivateKey").notNullable(); + + t.timestamps(true, true, true); + }); + + await createOnUpdateTrigger(knex, TableName.KmipOrgConfig); + } + + const hasKmipOrgServerCertTable = await knex.schema.hasTable(TableName.KmipOrgServerCertificates); + if (!hasKmipOrgServerCertTable) { + await knex.schema.createTable(TableName.KmipOrgServerCertificates, (t) => { + t.uuid("id", { primaryKey: true }).defaultTo(knex.fn.uuid()); + t.uuid("orgId").notNullable(); + t.foreign("orgId").references("id").inTable(TableName.Organization).onDelete("CASCADE"); + t.string("commonName").notNullable(); + t.string("altNames").notNullable(); + t.string("serialNumber").notNullable(); + t.string("keyAlgorithm").notNullable(); + t.datetime("issuedAt").notNullable(); + t.datetime("expiration").notNullable(); + t.binary("encryptedCertificate").notNullable(); + t.binary("encryptedChain").notNullable(); + }); + } + + const hasKmipClientCertTable = await knex.schema.hasTable(TableName.KmipClientCertificates); + if (!hasKmipClientCertTable) { + await knex.schema.createTable(TableName.KmipClientCertificates, (t) => { + t.uuid("id", { primaryKey: true }).defaultTo(knex.fn.uuid()); + t.uuid("kmipClientId").notNullable(); + t.foreign("kmipClientId").references("id").inTable(TableName.KmipClient).onDelete("CASCADE"); + t.string("serialNumber").notNullable(); + t.string("keyAlgorithm").notNullable(); + t.datetime("issuedAt").notNullable(); + t.datetime("expiration").notNullable(); + }); + } +} + +export async function down(knex: Knex): Promise { + const hasKmipOrgPkiConfig = await knex.schema.hasTable(TableName.KmipOrgConfig); + if (hasKmipOrgPkiConfig) { + await knex.schema.dropTable(TableName.KmipOrgConfig); + await dropOnUpdateTrigger(knex, TableName.KmipOrgConfig); + } + + const hasKmipOrgServerCertTable = await knex.schema.hasTable(TableName.KmipOrgServerCertificates); + if (hasKmipOrgServerCertTable) { + await knex.schema.dropTable(TableName.KmipOrgServerCertificates); + } + + const hasKmipClientCertTable = await knex.schema.hasTable(TableName.KmipClientCertificates); + if (hasKmipClientCertTable) { + await knex.schema.dropTable(TableName.KmipClientCertificates); + } + + const hasKmipClientTable = await knex.schema.hasTable(TableName.KmipClient); + if (hasKmipClientTable) { + await knex.schema.dropTable(TableName.KmipClient); + } +} diff --git a/backend/src/db/schemas/index.ts b/backend/src/db/schemas/index.ts index 9bcfdd49fa..7371938365 100644 --- a/backend/src/db/schemas/index.ts +++ b/backend/src/db/schemas/index.ts @@ -45,6 +45,10 @@ export * from "./incident-contacts"; export * from "./integration-auths"; export * from "./integrations"; export * from "./internal-kms"; +export * from "./kmip-client-certificates"; +export * from "./kmip-clients"; +export * from "./kmip-org-configs"; +export * from "./kmip-org-server-certificates"; export * from "./kms-key-versions"; export * from "./kms-keys"; export * from "./kms-root-config"; diff --git a/backend/src/db/schemas/kmip-client-certificates.ts b/backend/src/db/schemas/kmip-client-certificates.ts new file mode 100644 index 0000000000..a42d94a982 --- /dev/null +++ b/backend/src/db/schemas/kmip-client-certificates.ts @@ -0,0 +1,23 @@ +// Code generated by automation script, DO NOT EDIT. +// Automated by pulling database and generating zod schema +// To update. Just run npm run generate:schema +// Written by akhilmhdh. + +import { z } from "zod"; + +import { TImmutableDBKeys } from "./models"; + +export const KmipClientCertificatesSchema = z.object({ + id: z.string().uuid(), + kmipClientId: z.string().uuid(), + serialNumber: z.string(), + keyAlgorithm: z.string(), + issuedAt: z.date(), + expiration: z.date() +}); + +export type TKmipClientCertificates = z.infer; +export type TKmipClientCertificatesInsert = Omit, TImmutableDBKeys>; +export type TKmipClientCertificatesUpdate = Partial< + Omit, TImmutableDBKeys> +>; diff --git a/backend/src/db/schemas/kmip-clients.ts b/backend/src/db/schemas/kmip-clients.ts new file mode 100644 index 0000000000..eb8f31bfb0 --- /dev/null +++ b/backend/src/db/schemas/kmip-clients.ts @@ -0,0 +1,20 @@ +// Code generated by automation script, DO NOT EDIT. +// Automated by pulling database and generating zod schema +// To update. Just run npm run generate:schema +// Written by akhilmhdh. + +import { z } from "zod"; + +import { TImmutableDBKeys } from "./models"; + +export const KmipClientsSchema = z.object({ + id: z.string().uuid(), + name: z.string(), + permissions: z.string().array().nullable().optional(), + description: z.string().nullable().optional(), + projectId: z.string() +}); + +export type TKmipClients = z.infer; +export type TKmipClientsInsert = Omit, TImmutableDBKeys>; +export type TKmipClientsUpdate = Partial, TImmutableDBKeys>>; diff --git a/backend/src/db/schemas/kmip-org-configs.ts b/backend/src/db/schemas/kmip-org-configs.ts new file mode 100644 index 0000000000..e75d764130 --- /dev/null +++ b/backend/src/db/schemas/kmip-org-configs.ts @@ -0,0 +1,39 @@ +// Code generated by automation script, DO NOT EDIT. +// Automated by pulling database and generating zod schema +// To update. Just run npm run generate:schema +// Written by akhilmhdh. + +import { z } from "zod"; + +import { zodBuffer } from "@app/lib/zod"; + +import { TImmutableDBKeys } from "./models"; + +export const KmipOrgConfigsSchema = z.object({ + id: z.string().uuid(), + orgId: z.string().uuid(), + caKeyAlgorithm: z.string(), + rootCaIssuedAt: z.date(), + rootCaExpiration: z.date(), + rootCaSerialNumber: z.string(), + encryptedRootCaCertificate: zodBuffer, + encryptedRootCaPrivateKey: zodBuffer, + serverIntermediateCaIssuedAt: z.date(), + serverIntermediateCaExpiration: z.date(), + serverIntermediateCaSerialNumber: z.string().nullable().optional(), + encryptedServerIntermediateCaCertificate: zodBuffer, + encryptedServerIntermediateCaChain: zodBuffer, + encryptedServerIntermediateCaPrivateKey: zodBuffer, + clientIntermediateCaIssuedAt: z.date(), + clientIntermediateCaExpiration: z.date(), + clientIntermediateCaSerialNumber: z.string(), + encryptedClientIntermediateCaCertificate: zodBuffer, + encryptedClientIntermediateCaChain: zodBuffer, + encryptedClientIntermediateCaPrivateKey: zodBuffer, + createdAt: z.date(), + updatedAt: z.date() +}); + +export type TKmipOrgConfigs = z.infer; +export type TKmipOrgConfigsInsert = Omit, TImmutableDBKeys>; +export type TKmipOrgConfigsUpdate = Partial, TImmutableDBKeys>>; diff --git a/backend/src/db/schemas/kmip-org-server-certificates.ts b/backend/src/db/schemas/kmip-org-server-certificates.ts new file mode 100644 index 0000000000..66e5dcbd64 --- /dev/null +++ b/backend/src/db/schemas/kmip-org-server-certificates.ts @@ -0,0 +1,29 @@ +// Code generated by automation script, DO NOT EDIT. +// Automated by pulling database and generating zod schema +// To update. Just run npm run generate:schema +// Written by akhilmhdh. + +import { z } from "zod"; + +import { zodBuffer } from "@app/lib/zod"; + +import { TImmutableDBKeys } from "./models"; + +export const KmipOrgServerCertificatesSchema = z.object({ + id: z.string().uuid(), + orgId: z.string().uuid(), + commonName: z.string(), + altNames: z.string(), + serialNumber: z.string(), + keyAlgorithm: z.string(), + issuedAt: z.date(), + expiration: z.date(), + encryptedCertificate: zodBuffer, + encryptedChain: zodBuffer +}); + +export type TKmipOrgServerCertificates = z.infer; +export type TKmipOrgServerCertificatesInsert = Omit, TImmutableDBKeys>; +export type TKmipOrgServerCertificatesUpdate = Partial< + Omit, TImmutableDBKeys> +>; diff --git a/backend/src/db/schemas/models.ts b/backend/src/db/schemas/models.ts index 3ead855305..3c5d1cad8c 100644 --- a/backend/src/db/schemas/models.ts +++ b/backend/src/db/schemas/models.ts @@ -132,7 +132,11 @@ export enum TableName { SlackIntegrations = "slack_integrations", ProjectSlackConfigs = "project_slack_configs", AppConnection = "app_connections", - SecretSync = "secret_syncs" + SecretSync = "secret_syncs", + KmipClient = "kmip_clients", + KmipOrgConfig = "kmip_org_configs", + KmipOrgServerCertificates = "kmip_org_server_certificates", + KmipClientCertificates = "kmip_client_certificates" } export type TImmutableDBKeys = "id" | "createdAt" | "updatedAt"; diff --git a/backend/src/ee/routes/v1/index.ts b/backend/src/ee/routes/v1/index.ts index 5f931440c7..0567c76ef0 100644 --- a/backend/src/ee/routes/v1/index.ts +++ b/backend/src/ee/routes/v1/index.ts @@ -9,6 +9,8 @@ import { registerDynamicSecretRouter } from "./dynamic-secret-router"; import { registerExternalKmsRouter } from "./external-kms-router"; import { registerGroupRouter } from "./group-router"; import { registerIdentityProjectAdditionalPrivilegeRouter } from "./identity-project-additional-privilege-router"; +import { registerKmipRouter } from "./kmip-router"; +import { registerKmipSpecRouter } from "./kmip-spec-router"; import { registerLdapRouter } from "./ldap-router"; import { registerLicenseRouter } from "./license-router"; import { registerOidcRouter } from "./oidc-router"; @@ -110,4 +112,12 @@ export const registerV1EERoutes = async (server: FastifyZodProvider) => { }); await server.register(registerProjectTemplateRouter, { prefix: "/project-templates" }); + + await server.register( + async (kmipRouter) => { + await kmipRouter.register(registerKmipRouter); + await kmipRouter.register(registerKmipSpecRouter, { prefix: "/spec" }); + }, + { prefix: "/kmip" } + ); }; diff --git a/backend/src/ee/routes/v1/kmip-router.ts b/backend/src/ee/routes/v1/kmip-router.ts new file mode 100644 index 0000000000..45ab5044ba --- /dev/null +++ b/backend/src/ee/routes/v1/kmip-router.ts @@ -0,0 +1,428 @@ +import ms from "ms"; +import { z } from "zod"; + +import { KmipClientsSchema } from "@app/db/schemas"; +import { EventType } from "@app/ee/services/audit-log/audit-log-types"; +import { KmipPermission } from "@app/ee/services/kmip/kmip-enum"; +import { KmipClientOrderBy } from "@app/ee/services/kmip/kmip-types"; +import { OrderByDirection } from "@app/lib/types"; +import { readLimit, writeLimit } from "@app/server/config/rateLimiter"; +import { verifyAuth } from "@app/server/plugins/auth/verify-auth"; +import { AuthMode } from "@app/services/auth/auth-type"; +import { CertKeyAlgorithm } from "@app/services/certificate/certificate-types"; +import { validateAltNamesField } from "@app/services/certificate-authority/certificate-authority-validators"; + +const KmipClientResponseSchema = KmipClientsSchema.pick({ + projectId: true, + name: true, + id: true, + description: true, + permissions: true +}); + +export const registerKmipRouter = async (server: FastifyZodProvider) => { + server.route({ + method: "POST", + url: "/clients", + config: { + rateLimit: writeLimit + }, + schema: { + body: z.object({ + projectId: z.string(), + name: z.string().trim().min(1), + description: z.string().optional(), + permissions: z.nativeEnum(KmipPermission).array() + }), + response: { + 200: KmipClientResponseSchema + } + }, + onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), + handler: async (req) => { + const kmipClient = await server.services.kmip.createKmipClient({ + actor: req.permission.type, + actorId: req.permission.id, + actorAuthMethod: req.permission.authMethod, + actorOrgId: req.permission.orgId, + ...req.body + }); + + await server.services.auditLog.createAuditLog({ + ...req.auditLogInfo, + orgId: req.permission.orgId, + projectId: kmipClient.projectId, + event: { + type: EventType.CREATE_KMIP_CLIENT, + metadata: { + id: kmipClient.id, + name: kmipClient.name, + permissions: (kmipClient.permissions ?? []) as KmipPermission[] + } + } + }); + + return kmipClient; + } + }); + + server.route({ + method: "PATCH", + url: "/clients/:id", + config: { + rateLimit: writeLimit + }, + schema: { + params: z.object({ + id: z.string() + }), + body: z.object({ + name: z.string().trim().min(1), + description: z.string().optional(), + permissions: z.nativeEnum(KmipPermission).array() + }), + response: { + 200: KmipClientResponseSchema + } + }, + onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), + handler: async (req) => { + const kmipClient = await server.services.kmip.updateKmipClient({ + actor: req.permission.type, + actorId: req.permission.id, + actorAuthMethod: req.permission.authMethod, + actorOrgId: req.permission.orgId, + ...req.params, + ...req.body + }); + + await server.services.auditLog.createAuditLog({ + ...req.auditLogInfo, + orgId: req.permission.orgId, + projectId: kmipClient.projectId, + event: { + type: EventType.UPDATE_KMIP_CLIENT, + metadata: { + id: kmipClient.id, + name: kmipClient.name, + permissions: (kmipClient.permissions ?? []) as KmipPermission[] + } + } + }); + + return kmipClient; + } + }); + + server.route({ + method: "DELETE", + url: "/clients/:id", + config: { + rateLimit: writeLimit + }, + schema: { + params: z.object({ + id: z.string() + }), + response: { + 200: KmipClientResponseSchema + } + }, + onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), + handler: async (req) => { + const kmipClient = await server.services.kmip.deleteKmipClient({ + actor: req.permission.type, + actorId: req.permission.id, + actorAuthMethod: req.permission.authMethod, + actorOrgId: req.permission.orgId, + ...req.params + }); + + await server.services.auditLog.createAuditLog({ + ...req.auditLogInfo, + orgId: req.permission.orgId, + projectId: kmipClient.projectId, + event: { + type: EventType.DELETE_KMIP_CLIENT, + metadata: { + id: kmipClient.id + } + } + }); + + return kmipClient; + } + }); + + server.route({ + method: "GET", + url: "/clients/:id", + config: { + rateLimit: readLimit + }, + schema: { + params: z.object({ + id: z.string() + }), + response: { + 200: KmipClientResponseSchema + } + }, + onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), + handler: async (req) => { + const kmipClient = await server.services.kmip.getKmipClient({ + actor: req.permission.type, + actorId: req.permission.id, + actorAuthMethod: req.permission.authMethod, + actorOrgId: req.permission.orgId, + ...req.params + }); + + await server.services.auditLog.createAuditLog({ + ...req.auditLogInfo, + orgId: req.permission.orgId, + projectId: kmipClient.projectId, + event: { + type: EventType.GET_KMIP_CLIENT, + metadata: { + id: kmipClient.id + } + } + }); + + return kmipClient; + } + }); + + server.route({ + method: "GET", + url: "/clients", + config: { + rateLimit: readLimit + }, + schema: { + description: "List KMIP clients", + querystring: z.object({ + projectId: z.string(), + offset: z.coerce.number().min(0).optional().default(0), + limit: z.coerce.number().min(1).max(100).optional().default(100), + orderBy: z.nativeEnum(KmipClientOrderBy).optional().default(KmipClientOrderBy.Name), + orderDirection: z.nativeEnum(OrderByDirection).optional().default(OrderByDirection.ASC), + search: z.string().trim().optional() + }), + response: { + 200: z.object({ + kmipClients: KmipClientResponseSchema.array(), + totalCount: z.number() + }) + } + }, + onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), + handler: async (req) => { + const { kmipClients, totalCount } = await server.services.kmip.listKmipClientsByProjectId({ + actor: req.permission.type, + actorId: req.permission.id, + actorAuthMethod: req.permission.authMethod, + actorOrgId: req.permission.orgId, + ...req.query + }); + + await server.services.auditLog.createAuditLog({ + ...req.auditLogInfo, + projectId: req.query.projectId, + event: { + type: EventType.GET_KMIP_CLIENTS, + metadata: { + ids: kmipClients.map((key) => key.id) + } + } + }); + + return { kmipClients, totalCount }; + } + }); + + server.route({ + method: "POST", + url: "/clients/:id/certificates", + config: { + rateLimit: writeLimit + }, + schema: { + params: z.object({ + id: z.string() + }), + body: z.object({ + keyAlgorithm: z.nativeEnum(CertKeyAlgorithm), + ttl: z.string().refine((val) => ms(val) > 0, "TTL must be a positive number") + }), + response: { + 200: z.object({ + serialNumber: z.string(), + certificateChain: z.string(), + certificate: z.string(), + privateKey: z.string() + }) + } + }, + onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), + handler: async (req) => { + const certificate = await server.services.kmip.createKmipClientCertificate({ + actor: req.permission.type, + actorId: req.permission.id, + actorAuthMethod: req.permission.authMethod, + actorOrgId: req.permission.orgId, + clientId: req.params.id, + ...req.body + }); + + await server.services.auditLog.createAuditLog({ + ...req.auditLogInfo, + orgId: req.permission.orgId, + projectId: certificate.projectId, + event: { + type: EventType.CREATE_KMIP_CLIENT_CERTIFICATE, + metadata: { + clientId: req.params.id, + serialNumber: certificate.serialNumber, + ttl: req.body.ttl, + keyAlgorithm: req.body.keyAlgorithm + } + } + }); + + return certificate; + } + }); + + server.route({ + method: "POST", + url: "/", + config: { + rateLimit: writeLimit + }, + schema: { + body: z.object({ + caKeyAlgorithm: z.nativeEnum(CertKeyAlgorithm) + }), + response: { + 200: z.object({ + serverCertificateChain: z.string(), + clientCertificateChain: z.string() + }) + } + }, + onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), + handler: async (req) => { + const chains = await server.services.kmip.setupOrgKmip({ + actor: req.permission.type, + actorId: req.permission.id, + actorAuthMethod: req.permission.authMethod, + actorOrgId: req.permission.orgId, + ...req.body + }); + + await server.services.auditLog.createAuditLog({ + ...req.auditLogInfo, + orgId: req.permission.orgId, + event: { + type: EventType.SETUP_KMIP, + metadata: { + keyAlgorithm: req.body.caKeyAlgorithm + } + } + }); + + return chains; + } + }); + + server.route({ + method: "GET", + url: "/", + config: { + rateLimit: readLimit + }, + schema: { + response: { + 200: z.object({ + serverCertificateChain: z.string(), + clientCertificateChain: z.string() + }) + } + }, + onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), + handler: async (req) => { + const kmip = await server.services.kmip.getOrgKmip({ + actor: req.permission.type, + actorId: req.permission.id, + actorAuthMethod: req.permission.authMethod, + actorOrgId: req.permission.orgId + }); + + await server.services.auditLog.createAuditLog({ + ...req.auditLogInfo, + orgId: req.permission.orgId, + event: { + type: EventType.GET_KMIP, + metadata: { + id: kmip.id + } + } + }); + + return kmip; + } + }); + + server.route({ + method: "POST", + url: "/server-registration", + config: { + rateLimit: writeLimit + }, + schema: { + body: z.object({ + hostnamesOrIps: validateAltNamesField, + commonName: z.string().trim().min(1).optional(), + keyAlgorithm: z.nativeEnum(CertKeyAlgorithm).optional().default(CertKeyAlgorithm.RSA_2048), + ttl: z.string().refine((val) => ms(val) > 0, "TTL must be a positive number") + }), + response: { + 200: z.object({ + clientCertificateChain: z.string(), + certificateChain: z.string(), + certificate: z.string(), + privateKey: z.string() + }) + } + }, + onRequest: verifyAuth([AuthMode.IDENTITY_ACCESS_TOKEN]), + handler: async (req) => { + const configs = await server.services.kmip.registerServer({ + actor: req.permission.type, + actorId: req.permission.id, + actorAuthMethod: req.permission.authMethod, + actorOrgId: req.permission.orgId, + ...req.body + }); + + await server.services.auditLog.createAuditLog({ + ...req.auditLogInfo, + orgId: req.permission.orgId, + event: { + type: EventType.REGISTER_KMIP_SERVER, + metadata: { + serverCertificateSerialNumber: configs.serverCertificateSerialNumber, + hostnamesOrIps: req.body.hostnamesOrIps, + commonName: req.body.commonName ?? "kmip-server", + keyAlgorithm: req.body.keyAlgorithm, + ttl: req.body.ttl + } + } + }); + + return configs; + } + }); +}; diff --git a/backend/src/ee/routes/v1/kmip-spec-router.ts b/backend/src/ee/routes/v1/kmip-spec-router.ts new file mode 100644 index 0000000000..c9899c98e8 --- /dev/null +++ b/backend/src/ee/routes/v1/kmip-spec-router.ts @@ -0,0 +1,477 @@ +import z from "zod"; + +import { KmsKeysSchema } from "@app/db/schemas"; +import { EventType } from "@app/ee/services/audit-log/audit-log-types"; +import { SymmetricEncryption } from "@app/lib/crypto/cipher"; +import { ForbiddenRequestError, NotFoundError } from "@app/lib/errors"; +import { writeLimit } from "@app/server/config/rateLimiter"; +import { verifyAuth } from "@app/server/plugins/auth/verify-auth"; +import { ActorType, AuthMode } from "@app/services/auth/auth-type"; + +export const registerKmipSpecRouter = async (server: FastifyZodProvider) => { + server.decorateRequest("kmipUser", null); + + server.addHook("onRequest", async (req) => { + const clientId = req.headers["x-kmip-client-id"] as string; + const projectId = req.headers["x-kmip-project-id"] as string; + const clientCertSerialNumber = req.headers["x-kmip-client-certificate-serial-number"] as string; + const serverCertSerialNumber = req.headers["x-kmip-server-certificate-serial-number"] as string; + + if (!serverCertSerialNumber) { + throw new ForbiddenRequestError({ + message: "Missing server certificate serial number from request" + }); + } + + if (!clientCertSerialNumber) { + throw new ForbiddenRequestError({ + message: "Missing client certificate serial number from request" + }); + } + + if (!clientId) { + throw new ForbiddenRequestError({ + message: "Missing client ID from request" + }); + } + + if (!projectId) { + throw new ForbiddenRequestError({ + message: "Missing project ID from request" + }); + } + + // TODO: assert that server certificate used is not revoked + // TODO: assert that client certificate used is not revoked + + const kmipClient = await server.store.kmipClient.findByProjectAndClientId(projectId, clientId); + + if (!kmipClient) { + throw new NotFoundError({ + message: "KMIP client cannot be found." + }); + } + + if (kmipClient.orgId !== req.permission.orgId) { + throw new ForbiddenRequestError({ + message: "Client specified in the request does not belong in the organization" + }); + } + + req.kmipUser = { + projectId, + clientId, + name: kmipClient.name + }; + }); + + server.route({ + method: "POST", + url: "/create", + config: { + rateLimit: writeLimit + }, + schema: { + description: "KMIP endpoint for creating managed objects", + body: z.object({ + algorithm: z.nativeEnum(SymmetricEncryption) + }), + response: { + 200: KmsKeysSchema + } + }, + onRequest: verifyAuth([AuthMode.IDENTITY_ACCESS_TOKEN]), + handler: async (req) => { + const object = await server.services.kmipOperation.create({ + ...req.kmipUser, + actor: req.permission.type, + actorId: req.permission.id, + actorAuthMethod: req.permission.authMethod, + actorOrgId: req.permission.orgId, + algorithm: req.body.algorithm + }); + + await server.services.auditLog.createAuditLog({ + projectId: req.kmipUser.projectId, + actor: { + type: ActorType.KMIP_CLIENT, + metadata: { + clientId: req.kmipUser.clientId, + name: req.kmipUser.name + } + }, + event: { + type: EventType.KMIP_OPERATION_CREATE, + metadata: { + id: object.id, + algorithm: req.body.algorithm + } + } + }); + + return object; + } + }); + + server.route({ + method: "POST", + url: "/get", + config: { + rateLimit: writeLimit + }, + schema: { + description: "KMIP endpoint for getting managed objects", + body: z.object({ + id: z.string() + }), + response: { + 200: z.object({ + id: z.string(), + value: z.string(), + algorithm: z.string() + }) + } + }, + onRequest: verifyAuth([AuthMode.IDENTITY_ACCESS_TOKEN]), + handler: async (req) => { + const object = await server.services.kmipOperation.get({ + ...req.kmipUser, + actor: req.permission.type, + actorId: req.permission.id, + actorAuthMethod: req.permission.authMethod, + actorOrgId: req.permission.orgId, + id: req.body.id + }); + + await server.services.auditLog.createAuditLog({ + projectId: req.kmipUser.projectId, + actor: { + type: ActorType.KMIP_CLIENT, + metadata: { + clientId: req.kmipUser.clientId, + name: req.kmipUser.name + } + }, + event: { + type: EventType.KMIP_OPERATION_GET, + metadata: { + id: object.id + } + } + }); + + return object; + } + }); + + server.route({ + method: "POST", + url: "/get-attributes", + config: { + rateLimit: writeLimit + }, + schema: { + description: "KMIP endpoint for getting attributes of managed object", + body: z.object({ + id: z.string() + }), + response: { + 200: z.object({ + id: z.string(), + algorithm: z.string(), + isActive: z.boolean(), + createdAt: z.date(), + updatedAt: z.date() + }) + } + }, + onRequest: verifyAuth([AuthMode.IDENTITY_ACCESS_TOKEN]), + handler: async (req) => { + const object = await server.services.kmipOperation.getAttributes({ + ...req.kmipUser, + actor: req.permission.type, + actorId: req.permission.id, + actorAuthMethod: req.permission.authMethod, + actorOrgId: req.permission.orgId, + id: req.body.id + }); + + await server.services.auditLog.createAuditLog({ + projectId: req.kmipUser.projectId, + actor: { + type: ActorType.KMIP_CLIENT, + metadata: { + clientId: req.kmipUser.clientId, + name: req.kmipUser.name + } + }, + event: { + type: EventType.KMIP_OPERATION_GET_ATTRIBUTES, + metadata: { + id: object.id + } + } + }); + + return object; + } + }); + + server.route({ + method: "POST", + url: "/destroy", + config: { + rateLimit: writeLimit + }, + schema: { + description: "KMIP endpoint for destroying managed objects", + body: z.object({ + id: z.string() + }), + response: { + 200: z.object({ + id: z.string() + }) + } + }, + onRequest: verifyAuth([AuthMode.IDENTITY_ACCESS_TOKEN]), + handler: async (req) => { + const object = await server.services.kmipOperation.destroy({ + ...req.kmipUser, + actor: req.permission.type, + actorId: req.permission.id, + actorAuthMethod: req.permission.authMethod, + actorOrgId: req.permission.orgId, + id: req.body.id + }); + + await server.services.auditLog.createAuditLog({ + projectId: req.kmipUser.projectId, + actor: { + type: ActorType.KMIP_CLIENT, + metadata: { + clientId: req.kmipUser.clientId, + name: req.kmipUser.name + } + }, + event: { + type: EventType.KMIP_OPERATION_DESTROY, + metadata: { + id: object.id + } + } + }); + + return object; + } + }); + + server.route({ + method: "POST", + url: "/activate", + config: { + rateLimit: writeLimit + }, + schema: { + description: "KMIP endpoint for activating managed object", + body: z.object({ + id: z.string() + }), + response: { + 200: z.object({ + id: z.string(), + isActive: z.boolean() + }) + } + }, + onRequest: verifyAuth([AuthMode.IDENTITY_ACCESS_TOKEN]), + handler: async (req) => { + const object = await server.services.kmipOperation.activate({ + ...req.kmipUser, + actor: req.permission.type, + actorId: req.permission.id, + actorAuthMethod: req.permission.authMethod, + actorOrgId: req.permission.orgId, + id: req.body.id + }); + + await server.services.auditLog.createAuditLog({ + projectId: req.kmipUser.projectId, + actor: { + type: ActorType.KMIP_CLIENT, + metadata: { + clientId: req.kmipUser.clientId, + name: req.kmipUser.name + } + }, + event: { + type: EventType.KMIP_OPERATION_ACTIVATE, + metadata: { + id: object.id + } + } + }); + + return object; + } + }); + + server.route({ + method: "POST", + url: "/revoke", + config: { + rateLimit: writeLimit + }, + schema: { + description: "KMIP endpoint for revoking managed object", + body: z.object({ + id: z.string() + }), + response: { + 200: z.object({ + id: z.string(), + updatedAt: z.date() + }) + } + }, + onRequest: verifyAuth([AuthMode.IDENTITY_ACCESS_TOKEN]), + handler: async (req) => { + const object = await server.services.kmipOperation.revoke({ + ...req.kmipUser, + actor: req.permission.type, + actorId: req.permission.id, + actorAuthMethod: req.permission.authMethod, + actorOrgId: req.permission.orgId, + id: req.body.id + }); + + await server.services.auditLog.createAuditLog({ + projectId: req.kmipUser.projectId, + actor: { + type: ActorType.KMIP_CLIENT, + metadata: { + clientId: req.kmipUser.clientId, + name: req.kmipUser.name + } + }, + event: { + type: EventType.KMIP_OPERATION_REVOKE, + metadata: { + id: object.id + } + } + }); + + return object; + } + }); + + server.route({ + method: "POST", + url: "/locate", + config: { + rateLimit: writeLimit + }, + schema: { + description: "KMIP endpoint for locating managed objects", + response: { + 200: z.object({ + objects: z + .object({ + id: z.string(), + name: z.string(), + isActive: z.boolean(), + algorithm: z.string(), + createdAt: z.date(), + updatedAt: z.date() + }) + .array() + }) + } + }, + onRequest: verifyAuth([AuthMode.IDENTITY_ACCESS_TOKEN]), + handler: async (req) => { + const objects = await server.services.kmipOperation.locate({ + ...req.kmipUser, + actor: req.permission.type, + actorId: req.permission.id, + actorAuthMethod: req.permission.authMethod, + actorOrgId: req.permission.orgId + }); + + await server.services.auditLog.createAuditLog({ + projectId: req.kmipUser.projectId, + actor: { + type: ActorType.KMIP_CLIENT, + metadata: { + clientId: req.kmipUser.clientId, + name: req.kmipUser.name + } + }, + event: { + type: EventType.KMIP_OPERATION_LOCATE, + metadata: { + ids: objects.map((obj) => obj.id) + } + } + }); + + return { + objects + }; + } + }); + + server.route({ + method: "POST", + url: "/register", + config: { + rateLimit: writeLimit + }, + schema: { + description: "KMIP endpoint for registering managed object", + body: z.object({ + key: z.string(), + name: z.string(), + algorithm: z.nativeEnum(SymmetricEncryption) + }), + response: { + 200: z.object({ + id: z.string() + }) + } + }, + onRequest: verifyAuth([AuthMode.IDENTITY_ACCESS_TOKEN]), + handler: async (req) => { + const object = await server.services.kmipOperation.register({ + ...req.kmipUser, + ...req.body, + actor: req.permission.type, + actorId: req.permission.id, + actorAuthMethod: req.permission.authMethod, + actorOrgId: req.permission.orgId + }); + + await server.services.auditLog.createAuditLog({ + projectId: req.kmipUser.projectId, + actor: { + type: ActorType.KMIP_CLIENT, + metadata: { + clientId: req.kmipUser.clientId, + name: req.kmipUser.name + } + }, + event: { + type: EventType.KMIP_OPERATION_REGISTER, + metadata: { + id: object.id, + algorithm: req.body.algorithm, + name: object.name + } + } + }); + + return object; + } + }); +}; diff --git a/backend/src/ee/services/audit-log/audit-log-types.ts b/backend/src/ee/services/audit-log/audit-log-types.ts index e124c1f451..b7bd8b7d44 100644 --- a/backend/src/ee/services/audit-log/audit-log-types.ts +++ b/backend/src/ee/services/audit-log/audit-log-types.ts @@ -21,6 +21,8 @@ import { TUpdateSecretSyncDTO } from "@app/services/secret-sync/secret-sync-types"; +import { KmipPermission } from "../kmip/kmip-enum"; + export type TListProjectAuditLogDTO = { filter: { userAgentType?: UserAgentType; @@ -39,7 +41,14 @@ export type TListProjectAuditLogDTO = { export type TCreateAuditLogDTO = { event: Event; - actor: UserActor | IdentityActor | ServiceActor | ScimClientActor | PlatformActor | UnknownUserActor; + actor: + | UserActor + | IdentityActor + | ServiceActor + | ScimClientActor + | PlatformActor + | UnknownUserActor + | KmipClientActor; orgId?: string; projectId?: string; } & BaseAuthData; @@ -252,7 +261,26 @@ export enum EventType { SECRET_SYNC_IMPORT_SECRETS = "secret-sync-import-secrets", SECRET_SYNC_REMOVE_SECRETS = "secret-sync-remove-secrets", OIDC_GROUP_MEMBERSHIP_MAPPING_ASSIGN_USER = "oidc-group-membership-mapping-assign-user", - OIDC_GROUP_MEMBERSHIP_MAPPING_REMOVE_USER = "oidc-group-membership-mapping-remove-user" + OIDC_GROUP_MEMBERSHIP_MAPPING_REMOVE_USER = "oidc-group-membership-mapping-remove-user", + CREATE_KMIP_CLIENT = "create-kmip-client", + UPDATE_KMIP_CLIENT = "update-kmip-client", + DELETE_KMIP_CLIENT = "delete-kmip-client", + GET_KMIP_CLIENT = "get-kmip-client", + GET_KMIP_CLIENTS = "get-kmip-clients", + CREATE_KMIP_CLIENT_CERTIFICATE = "create-kmip-client-certificate", + + SETUP_KMIP = "setup-kmip", + GET_KMIP = "get-kmip", + REGISTER_KMIP_SERVER = "register-kmip-server", + + KMIP_OPERATION_CREATE = "kmip-operation-create", + KMIP_OPERATION_GET = "kmip-operation-get", + KMIP_OPERATION_DESTROY = "kmip-operation-destroy", + KMIP_OPERATION_GET_ATTRIBUTES = "kmip-operation-get-attributes", + KMIP_OPERATION_ACTIVATE = "kmip-operation-activate", + KMIP_OPERATION_REVOKE = "kmip-operation-revoke", + KMIP_OPERATION_LOCATE = "kmip-operation-locate", + KMIP_OPERATION_REGISTER = "kmip-operation-register" } interface UserActorMetadata { @@ -275,6 +303,11 @@ interface ScimClientActorMetadata {} interface PlatformActorMetadata {} +interface KmipClientActorMetadata { + clientId: string; + name: string; +} + interface UnknownUserActorMetadata {} export interface UserActor { @@ -292,6 +325,11 @@ export interface PlatformActor { metadata: PlatformActorMetadata; } +export interface KmipClientActor { + type: ActorType.KMIP_CLIENT; + metadata: KmipClientActorMetadata; +} + export interface UnknownUserActor { type: ActorType.UNKNOWN_USER; metadata: UnknownUserActorMetadata; @@ -307,7 +345,7 @@ export interface ScimClientActor { metadata: ScimClientActorMetadata; } -export type Actor = UserActor | ServiceActor | IdentityActor | ScimClientActor | PlatformActor; +export type Actor = UserActor | ServiceActor | IdentityActor | ScimClientActor | PlatformActor | KmipClientActor; interface GetSecretsEvent { type: EventType.GET_SECRETS; @@ -2091,6 +2129,139 @@ interface OidcGroupMembershipMappingRemoveUserEvent { }; } +interface CreateKmipClientEvent { + type: EventType.CREATE_KMIP_CLIENT; + metadata: { + name: string; + id: string; + permissions: KmipPermission[]; + }; +} + +interface UpdateKmipClientEvent { + type: EventType.UPDATE_KMIP_CLIENT; + metadata: { + name: string; + id: string; + permissions: KmipPermission[]; + }; +} + +interface DeleteKmipClientEvent { + type: EventType.DELETE_KMIP_CLIENT; + metadata: { + id: string; + }; +} + +interface GetKmipClientEvent { + type: EventType.GET_KMIP_CLIENT; + metadata: { + id: string; + }; +} + +interface GetKmipClientsEvent { + type: EventType.GET_KMIP_CLIENTS; + metadata: { + ids: string[]; + }; +} + +interface CreateKmipClientCertificateEvent { + type: EventType.CREATE_KMIP_CLIENT_CERTIFICATE; + metadata: { + clientId: string; + ttl: string; + keyAlgorithm: string; + serialNumber: string; + }; +} + +interface KmipOperationGetEvent { + type: EventType.KMIP_OPERATION_GET; + metadata: { + id: string; + }; +} + +interface KmipOperationDestroyEvent { + type: EventType.KMIP_OPERATION_DESTROY; + metadata: { + id: string; + }; +} + +interface KmipOperationCreateEvent { + type: EventType.KMIP_OPERATION_CREATE; + metadata: { + id: string; + algorithm: string; + }; +} + +interface KmipOperationGetAttributesEvent { + type: EventType.KMIP_OPERATION_GET_ATTRIBUTES; + metadata: { + id: string; + }; +} + +interface KmipOperationActivateEvent { + type: EventType.KMIP_OPERATION_ACTIVATE; + metadata: { + id: string; + }; +} + +interface KmipOperationRevokeEvent { + type: EventType.KMIP_OPERATION_REVOKE; + metadata: { + id: string; + }; +} + +interface KmipOperationLocateEvent { + type: EventType.KMIP_OPERATION_LOCATE; + metadata: { + ids: string[]; + }; +} + +interface KmipOperationRegisterEvent { + type: EventType.KMIP_OPERATION_REGISTER; + metadata: { + id: string; + algorithm: string; + name: string; + }; +} + +interface SetupKmipEvent { + type: EventType.SETUP_KMIP; + metadata: { + keyAlgorithm: CertKeyAlgorithm; + }; +} + +interface GetKmipEvent { + type: EventType.GET_KMIP; + metadata: { + id: string; + }; +} + +interface RegisterKmipServerEvent { + type: EventType.REGISTER_KMIP_SERVER; + metadata: { + serverCertificateSerialNumber: string; + hostnamesOrIps: string; + commonName: string; + keyAlgorithm: CertKeyAlgorithm; + ttl: string; + }; +} + export type Event = | GetSecretsEvent | GetSecretEvent @@ -2282,4 +2453,21 @@ export type Event = | SecretSyncImportSecretsEvent | SecretSyncRemoveSecretsEvent | OidcGroupMembershipMappingAssignUserEvent - | OidcGroupMembershipMappingRemoveUserEvent; + | OidcGroupMembershipMappingRemoveUserEvent + | CreateKmipClientEvent + | UpdateKmipClientEvent + | DeleteKmipClientEvent + | GetKmipClientEvent + | GetKmipClientsEvent + | CreateKmipClientCertificateEvent + | SetupKmipEvent + | GetKmipEvent + | RegisterKmipServerEvent + | KmipOperationGetEvent + | KmipOperationDestroyEvent + | KmipOperationCreateEvent + | KmipOperationGetAttributesEvent + | KmipOperationActivateEvent + | KmipOperationRevokeEvent + | KmipOperationLocateEvent + | KmipOperationRegisterEvent; diff --git a/backend/src/ee/services/kmip/kmip-client-certificate-dal.ts b/backend/src/ee/services/kmip/kmip-client-certificate-dal.ts new file mode 100644 index 0000000000..989b533248 --- /dev/null +++ b/backend/src/ee/services/kmip/kmip-client-certificate-dal.ts @@ -0,0 +1,11 @@ +import { TDbClient } from "@app/db"; +import { TableName } from "@app/db/schemas"; +import { ormify } from "@app/lib/knex"; + +export type TKmipClientCertificateDALFactory = ReturnType; + +export const kmipClientCertificateDALFactory = (db: TDbClient) => { + const kmipClientCertOrm = ormify(db, TableName.KmipClientCertificates); + + return kmipClientCertOrm; +}; diff --git a/backend/src/ee/services/kmip/kmip-client-dal.ts b/backend/src/ee/services/kmip/kmip-client-dal.ts new file mode 100644 index 0000000000..2650ebad07 --- /dev/null +++ b/backend/src/ee/services/kmip/kmip-client-dal.ts @@ -0,0 +1,86 @@ +import { Knex } from "knex"; + +import { TDbClient } from "@app/db"; +import { TableName, TKmipClients } from "@app/db/schemas"; +import { DatabaseError } from "@app/lib/errors"; +import { ormify, selectAllTableCols } from "@app/lib/knex"; +import { OrderByDirection } from "@app/lib/types"; + +import { KmipClientOrderBy } from "./kmip-types"; + +export type TKmipClientDALFactory = ReturnType; + +export const kmipClientDALFactory = (db: TDbClient) => { + const kmipClientOrm = ormify(db, TableName.KmipClient); + + const findByProjectAndClientId = async (projectId: string, clientId: string) => { + try { + const client = await db + .replicaNode()(TableName.KmipClient) + .join(TableName.Project, `${TableName.Project}.id`, `${TableName.KmipClient}.projectId`) + .join(TableName.Organization, `${TableName.Organization}.id`, `${TableName.Project}.orgId`) + .where({ + [`${TableName.KmipClient}.projectId` as "projectId"]: projectId, + [`${TableName.KmipClient}.id` as "id"]: clientId + }) + .select(selectAllTableCols(TableName.KmipClient)) + .select(db.ref("id").withSchema(TableName.Organization).as("orgId")) + .first(); + + return client; + } catch (error) { + throw new DatabaseError({ error, name: "Find by project and client ID" }); + } + }; + + const findByProjectId = async ( + { + projectId, + offset = 0, + limit, + orderBy = KmipClientOrderBy.Name, + orderDirection = OrderByDirection.ASC, + search + }: { + projectId: string; + offset?: number; + limit?: number; + orderBy?: KmipClientOrderBy; + orderDirection?: OrderByDirection; + search?: string; + }, + tx?: Knex + ) => { + try { + const query = (tx || db.replicaNode())(TableName.KmipClient) + .where("projectId", projectId) + .where((qb) => { + if (search) { + void qb.whereILike("name", `%${search}%`); + } + }) + .select< + (TKmipClients & { + total_count: number; + })[] + >(selectAllTableCols(TableName.KmipClient), db.raw(`count(*) OVER() as total_count`)) + .orderBy(orderBy, orderDirection); + + if (limit) { + void query.limit(limit).offset(offset); + } + + const data = await query; + + return { kmipClients: data, totalCount: Number(data?.[0]?.total_count ?? 0) }; + } catch (error) { + throw new DatabaseError({ error, name: "Find KMIP clients by project id" }); + } + }; + + return { + ...kmipClientOrm, + findByProjectId, + findByProjectAndClientId + }; +}; diff --git a/backend/src/ee/services/kmip/kmip-enum.ts b/backend/src/ee/services/kmip/kmip-enum.ts new file mode 100644 index 0000000000..80af88e1c4 --- /dev/null +++ b/backend/src/ee/services/kmip/kmip-enum.ts @@ -0,0 +1,11 @@ +export enum KmipPermission { + Create = "create", + Locate = "locate", + Check = "check", + Get = "get", + GetAttributes = "get-attributes", + Activate = "activate", + Revoke = "revoke", + Destroy = "destroy", + Register = "register" +} diff --git a/backend/src/ee/services/kmip/kmip-operation-service.ts b/backend/src/ee/services/kmip/kmip-operation-service.ts new file mode 100644 index 0000000000..66c3a1d46b --- /dev/null +++ b/backend/src/ee/services/kmip/kmip-operation-service.ts @@ -0,0 +1,422 @@ +import { ForbiddenError } from "@casl/ability"; + +import { BadRequestError, ForbiddenRequestError, NotFoundError } from "@app/lib/errors"; +import { TKmsKeyDALFactory } from "@app/services/kms/kms-key-dal"; +import { TKmsServiceFactory } from "@app/services/kms/kms-service"; +import { TProjectDALFactory } from "@app/services/project/project-dal"; + +import { OrgPermissionKmipActions, OrgPermissionSubjects } from "../permission/org-permission"; +import { TPermissionServiceFactory } from "../permission/permission-service"; +import { TKmipClientDALFactory } from "./kmip-client-dal"; +import { KmipPermission } from "./kmip-enum"; +import { + TKmipCreateDTO, + TKmipDestroyDTO, + TKmipGetAttributesDTO, + TKmipGetDTO, + TKmipLocateDTO, + TKmipRegisterDTO, + TKmipRevokeDTO +} from "./kmip-types"; + +type TKmipOperationServiceFactoryDep = { + kmsService: TKmsServiceFactory; + kmsDAL: TKmsKeyDALFactory; + kmipClientDAL: TKmipClientDALFactory; + projectDAL: Pick; + permissionService: Pick; +}; + +export type TKmipOperationServiceFactory = ReturnType; + +export const kmipOperationServiceFactory = ({ + kmsService, + kmsDAL, + projectDAL, + kmipClientDAL, + permissionService +}: TKmipOperationServiceFactoryDep) => { + const create = async ({ + projectId, + clientId, + algorithm, + actor, + actorId, + actorAuthMethod, + actorOrgId + }: TKmipCreateDTO) => { + const { permission } = await permissionService.getOrgPermission( + actor, + actorId, + actorOrgId, + actorAuthMethod, + actorOrgId + ); + + ForbiddenError.from(permission).throwUnlessCan(OrgPermissionKmipActions.Proxy, OrgPermissionSubjects.Kmip); + + const kmipClient = await kmipClientDAL.findOne({ + id: clientId, + projectId + }); + + if (!kmipClient.permissions?.includes(KmipPermission.Create)) { + throw new ForbiddenRequestError({ + message: "Client does not have sufficient permission to perform KMIP create" + }); + } + + const kmsKey = await kmsService.generateKmsKey({ + encryptionAlgorithm: algorithm, + orgId: actorOrgId, + projectId, + isReserved: false + }); + + return kmsKey; + }; + + const destroy = async ({ projectId, id, clientId, actor, actorId, actorOrgId, actorAuthMethod }: TKmipDestroyDTO) => { + const { permission } = await permissionService.getOrgPermission( + actor, + actorId, + actorOrgId, + actorAuthMethod, + actorOrgId + ); + + ForbiddenError.from(permission).throwUnlessCan(OrgPermissionKmipActions.Proxy, OrgPermissionSubjects.Kmip); + + const kmipClient = await kmipClientDAL.findOne({ + id: clientId, + projectId + }); + + if (!kmipClient.permissions?.includes(KmipPermission.Destroy)) { + throw new ForbiddenRequestError({ + message: "Client does not have sufficient permission to perform KMIP destroy" + }); + } + + const key = await kmsDAL.findOne({ + id, + projectId + }); + + if (!key) { + throw new NotFoundError({ message: `Key with ID ${id} not found` }); + } + + if (key.isReserved) { + throw new BadRequestError({ message: "Cannot destroy reserved keys" }); + } + + const completeKeyDetails = await kmsDAL.findByIdWithAssociatedKms(id); + if (!completeKeyDetails.internalKms) { + throw new BadRequestError({ + message: "Cannot destroy external keys" + }); + } + + if (!completeKeyDetails.isDisabled) { + throw new BadRequestError({ + message: "Cannot destroy active keys" + }); + } + + const kms = kmsDAL.deleteById(id); + + return kms; + }; + + const get = async ({ projectId, id, clientId, actor, actorId, actorAuthMethod, actorOrgId }: TKmipGetDTO) => { + const { permission } = await permissionService.getOrgPermission( + actor, + actorId, + actorOrgId, + actorAuthMethod, + actorOrgId + ); + + ForbiddenError.from(permission).throwUnlessCan(OrgPermissionKmipActions.Proxy, OrgPermissionSubjects.Kmip); + + const kmipClient = await kmipClientDAL.findOne({ + id: clientId, + projectId + }); + + if (!kmipClient.permissions?.includes(KmipPermission.Get)) { + throw new ForbiddenRequestError({ + message: "Client does not have sufficient permission to perform KMIP get" + }); + } + + const key = await kmsDAL.findOne({ + id, + projectId + }); + + if (!key) { + throw new NotFoundError({ message: `Key with ID ${id} not found` }); + } + + if (key.isReserved) { + throw new BadRequestError({ message: "Cannot get reserved keys" }); + } + + const completeKeyDetails = await kmsDAL.findByIdWithAssociatedKms(id); + + if (!completeKeyDetails.internalKms) { + throw new BadRequestError({ + message: "Cannot get external keys" + }); + } + + const kmsKey = await kmsService.getKeyMaterial({ + kmsId: key.id + }); + + return { + id: key.id, + value: kmsKey.toString("base64"), + algorithm: completeKeyDetails.internalKms.encryptionAlgorithm, + isActive: !key.isDisabled, + createdAt: key.createdAt, + updatedAt: key.updatedAt + }; + }; + + const activate = async ({ projectId, id, clientId, actor, actorId, actorAuthMethod, actorOrgId }: TKmipGetDTO) => { + const { permission } = await permissionService.getOrgPermission( + actor, + actorId, + actorOrgId, + actorAuthMethod, + actorOrgId + ); + + ForbiddenError.from(permission).throwUnlessCan(OrgPermissionKmipActions.Proxy, OrgPermissionSubjects.Kmip); + + const kmipClient = await kmipClientDAL.findOne({ + id: clientId, + projectId + }); + + if (!kmipClient.permissions?.includes(KmipPermission.Activate)) { + throw new ForbiddenRequestError({ + message: "Client does not have sufficient permission to perform KMIP activate" + }); + } + + const key = await kmsDAL.findOne({ + id, + projectId + }); + + if (!key) { + throw new NotFoundError({ message: `Key with ID ${id} not found` }); + } + + return { + id: key.id, + isActive: !key.isDisabled + }; + }; + + const revoke = async ({ projectId, id, clientId, actor, actorId, actorAuthMethod, actorOrgId }: TKmipRevokeDTO) => { + const { permission } = await permissionService.getOrgPermission( + actor, + actorId, + actorOrgId, + actorAuthMethod, + actorOrgId + ); + + ForbiddenError.from(permission).throwUnlessCan(OrgPermissionKmipActions.Proxy, OrgPermissionSubjects.Kmip); + + const kmipClient = await kmipClientDAL.findOne({ + id: clientId, + projectId + }); + + if (!kmipClient.permissions?.includes(KmipPermission.Revoke)) { + throw new ForbiddenRequestError({ + message: "Client does not have sufficient permission to perform KMIP revoke" + }); + } + + const key = await kmsDAL.findOne({ + id, + projectId + }); + + if (!key) { + throw new NotFoundError({ message: `Key with ID ${id} not found` }); + } + + if (key.isReserved) { + throw new BadRequestError({ message: "Cannot revoke reserved keys" }); + } + + const completeKeyDetails = await kmsDAL.findByIdWithAssociatedKms(id); + + if (!completeKeyDetails.internalKms) { + throw new BadRequestError({ + message: "Cannot revoke external keys" + }); + } + + const revokedKey = await kmsDAL.updateById(key.id, { + isDisabled: true + }); + + return { + id: key.id, + updatedAt: revokedKey.updatedAt + }; + }; + + const getAttributes = async ({ + projectId, + id, + clientId, + actor, + actorId, + actorAuthMethod, + actorOrgId + }: TKmipGetAttributesDTO) => { + const { permission } = await permissionService.getOrgPermission( + actor, + actorId, + actorOrgId, + actorAuthMethod, + actorOrgId + ); + + ForbiddenError.from(permission).throwUnlessCan(OrgPermissionKmipActions.Proxy, OrgPermissionSubjects.Kmip); + + const kmipClient = await kmipClientDAL.findOne({ + id: clientId, + projectId + }); + + if (!kmipClient.permissions?.includes(KmipPermission.GetAttributes)) { + throw new ForbiddenRequestError({ + message: "Client does not have sufficient permission to perform KMIP get attributes" + }); + } + + const key = await kmsDAL.findOne({ + id, + projectId + }); + + if (!key) { + throw new NotFoundError({ message: `Key with ID ${id} not found` }); + } + + if (key.isReserved) { + throw new BadRequestError({ message: "Cannot get reserved keys" }); + } + + const completeKeyDetails = await kmsDAL.findByIdWithAssociatedKms(id); + + if (!completeKeyDetails.internalKms) { + throw new BadRequestError({ + message: "Cannot get external keys" + }); + } + + return { + id: key.id, + algorithm: completeKeyDetails.internalKms.encryptionAlgorithm, + isActive: !key.isDisabled, + createdAt: key.createdAt, + updatedAt: key.updatedAt + }; + }; + + const locate = async ({ projectId, clientId, actor, actorId, actorAuthMethod, actorOrgId }: TKmipLocateDTO) => { + const { permission } = await permissionService.getOrgPermission( + actor, + actorId, + actorOrgId, + actorAuthMethod, + actorOrgId + ); + + ForbiddenError.from(permission).throwUnlessCan(OrgPermissionKmipActions.Proxy, OrgPermissionSubjects.Kmip); + + const kmipClient = await kmipClientDAL.findOne({ + id: clientId, + projectId + }); + + if (!kmipClient.permissions?.includes(KmipPermission.Locate)) { + throw new ForbiddenRequestError({ + message: "Client does not have sufficient permission to perform KMIP locate" + }); + } + + const keys = await kmsDAL.findProjectCmeks(projectId); + + return keys; + }; + + const register = async ({ + projectId, + clientId, + key, + algorithm, + name, + actor, + actorId, + actorAuthMethod, + actorOrgId + }: TKmipRegisterDTO) => { + const { permission } = await permissionService.getOrgPermission( + actor, + actorId, + actorOrgId, + actorAuthMethod, + actorOrgId + ); + + ForbiddenError.from(permission).throwUnlessCan(OrgPermissionKmipActions.Proxy, OrgPermissionSubjects.Kmip); + + const kmipClient = await kmipClientDAL.findOne({ + id: clientId, + projectId + }); + + if (!kmipClient.permissions?.includes(KmipPermission.Register)) { + throw new ForbiddenRequestError({ + message: "Client does not have sufficient permission to perform KMIP register" + }); + } + + const project = await projectDAL.findById(projectId); + + const kmsKey = await kmsService.importKeyMaterial({ + name, + key: Buffer.from(key, "base64"), + algorithm, + isReserved: false, + projectId, + orgId: project.orgId + }); + + return kmsKey; + }; + + return { + create, + get, + activate, + getAttributes, + destroy, + revoke, + locate, + register + }; +}; diff --git a/backend/src/ee/services/kmip/kmip-org-config-dal.ts b/backend/src/ee/services/kmip/kmip-org-config-dal.ts new file mode 100644 index 0000000000..a6567fafd7 --- /dev/null +++ b/backend/src/ee/services/kmip/kmip-org-config-dal.ts @@ -0,0 +1,11 @@ +import { TDbClient } from "@app/db"; +import { TableName } from "@app/db/schemas"; +import { ormify } from "@app/lib/knex"; + +export type TKmipOrgConfigDALFactory = ReturnType; + +export const kmipOrgConfigDALFactory = (db: TDbClient) => { + const kmipOrgConfigOrm = ormify(db, TableName.KmipOrgConfig); + + return kmipOrgConfigOrm; +}; diff --git a/backend/src/ee/services/kmip/kmip-org-server-certificate-dal.ts b/backend/src/ee/services/kmip/kmip-org-server-certificate-dal.ts new file mode 100644 index 0000000000..98626aad1a --- /dev/null +++ b/backend/src/ee/services/kmip/kmip-org-server-certificate-dal.ts @@ -0,0 +1,11 @@ +import { TDbClient } from "@app/db"; +import { TableName } from "@app/db/schemas"; +import { ormify } from "@app/lib/knex"; + +export type TKmipOrgServerCertificateDALFactory = ReturnType; + +export const kmipOrgServerCertificateDALFactory = (db: TDbClient) => { + const kmipOrgServerCertificateOrm = ormify(db, TableName.KmipOrgServerCertificates); + + return kmipOrgServerCertificateOrm; +}; diff --git a/backend/src/ee/services/kmip/kmip-service.ts b/backend/src/ee/services/kmip/kmip-service.ts new file mode 100644 index 0000000000..c7ae6b7287 --- /dev/null +++ b/backend/src/ee/services/kmip/kmip-service.ts @@ -0,0 +1,817 @@ +import { ForbiddenError } from "@casl/ability"; +import * as x509 from "@peculiar/x509"; +import crypto, { KeyObject } from "crypto"; +import ms from "ms"; + +import { ActionProjectType } from "@app/db/schemas"; +import { BadRequestError, InternalServerError, NotFoundError } from "@app/lib/errors"; +import { isValidHostname, isValidIp } from "@app/lib/ip"; +import { constructPemChainFromCerts } from "@app/services/certificate/certificate-fns"; +import { CertExtendedKeyUsage, CertKeyAlgorithm, CertKeyUsage } from "@app/services/certificate/certificate-types"; +import { + createSerialNumber, + keyAlgorithmToAlgCfg +} from "@app/services/certificate-authority/certificate-authority-fns"; +import { TKmsServiceFactory } from "@app/services/kms/kms-service"; +import { KmsDataKey } from "@app/services/kms/kms-types"; + +import { TLicenseServiceFactory } from "../license/license-service"; +import { OrgPermissionKmipActions, OrgPermissionSubjects } from "../permission/org-permission"; +import { TPermissionServiceFactory } from "../permission/permission-service"; +import { ProjectPermissionKmipActions, ProjectPermissionSub } from "../permission/project-permission"; +import { TKmipClientCertificateDALFactory } from "./kmip-client-certificate-dal"; +import { TKmipClientDALFactory } from "./kmip-client-dal"; +import { TKmipOrgConfigDALFactory } from "./kmip-org-config-dal"; +import { TKmipOrgServerCertificateDALFactory } from "./kmip-org-server-certificate-dal"; +import { + TCreateKmipClientCertificateDTO, + TCreateKmipClientDTO, + TDeleteKmipClientDTO, + TGenerateOrgKmipServerCertificateDTO, + TGetKmipClientDTO, + TGetOrgKmipDTO, + TListKmipClientsByProjectIdDTO, + TRegisterServerDTO, + TSetupOrgKmipDTO, + TUpdateKmipClientDTO +} from "./kmip-types"; + +type TKmipServiceFactoryDep = { + kmipClientDAL: TKmipClientDALFactory; + kmipClientCertificateDAL: TKmipClientCertificateDALFactory; + kmipOrgServerCertificateDAL: TKmipOrgServerCertificateDALFactory; + permissionService: Pick; + kmsService: Pick; + kmipOrgConfigDAL: TKmipOrgConfigDALFactory; + licenseService: Pick; +}; + +export type TKmipServiceFactory = ReturnType; + +export const kmipServiceFactory = ({ + kmipClientDAL, + permissionService, + kmipClientCertificateDAL, + kmipOrgConfigDAL, + kmsService, + kmipOrgServerCertificateDAL, + licenseService +}: TKmipServiceFactoryDep) => { + const createKmipClient = async ({ + actor, + actorId, + actorOrgId, + actorAuthMethod, + projectId, + name, + description, + permissions + }: TCreateKmipClientDTO) => { + const { permission } = await permissionService.getProjectPermission({ + actor, + actorId, + projectId, + actorAuthMethod, + actorOrgId, + actionProjectType: ActionProjectType.KMS + }); + + ForbiddenError.from(permission).throwUnlessCan( + ProjectPermissionKmipActions.CreateClients, + ProjectPermissionSub.Kmip + ); + + const plan = await licenseService.getPlan(actorOrgId); + if (!plan.kmip) + throw new BadRequestError({ + message: "Failed to create KMIP client. Upgrade your plan to enterprise." + }); + + const kmipClient = await kmipClientDAL.create({ + projectId, + name, + description, + permissions + }); + + return kmipClient; + }; + + const updateKmipClient = async ({ + actor, + actorId, + actorOrgId, + actorAuthMethod, + name, + description, + permissions, + id + }: TUpdateKmipClientDTO) => { + const kmipClient = await kmipClientDAL.findById(id); + + if (!kmipClient) { + throw new NotFoundError({ + message: `KMIP client with ID ${id} does not exist` + }); + } + + const plan = await licenseService.getPlan(actorOrgId); + if (!plan.kmip) + throw new BadRequestError({ + message: "Failed to update KMIP client. Upgrade your plan to enterprise." + }); + + const { permission } = await permissionService.getProjectPermission({ + actor, + actorId, + projectId: kmipClient.projectId, + actorAuthMethod, + actorOrgId, + actionProjectType: ActionProjectType.KMS + }); + + ForbiddenError.from(permission).throwUnlessCan( + ProjectPermissionKmipActions.UpdateClients, + ProjectPermissionSub.Kmip + ); + + const updatedKmipClient = await kmipClientDAL.updateById(id, { + name, + description, + permissions + }); + + return updatedKmipClient; + }; + + const deleteKmipClient = async ({ actor, actorId, actorOrgId, actorAuthMethod, id }: TDeleteKmipClientDTO) => { + const kmipClient = await kmipClientDAL.findById(id); + + if (!kmipClient) { + throw new NotFoundError({ + message: `KMIP client with ID ${id} does not exist` + }); + } + + const { permission } = await permissionService.getProjectPermission({ + actor, + actorId, + projectId: kmipClient.projectId, + actorAuthMethod, + actorOrgId, + actionProjectType: ActionProjectType.KMS + }); + + ForbiddenError.from(permission).throwUnlessCan( + ProjectPermissionKmipActions.DeleteClients, + ProjectPermissionSub.Kmip + ); + + const plan = await licenseService.getPlan(actorOrgId); + if (!plan.kmip) + throw new BadRequestError({ + message: "Failed to delete KMIP client. Upgrade your plan to enterprise." + }); + + const deletedKmipClient = await kmipClientDAL.deleteById(id); + + return deletedKmipClient; + }; + + const getKmipClient = async ({ actor, actorId, actorOrgId, actorAuthMethod, id }: TGetKmipClientDTO) => { + const kmipClient = await kmipClientDAL.findById(id); + + if (!kmipClient) { + throw new NotFoundError({ + message: `KMIP client with ID ${id} does not exist` + }); + } + + const { permission } = await permissionService.getProjectPermission({ + actor, + actorId, + projectId: kmipClient.projectId, + actorAuthMethod, + actorOrgId, + actionProjectType: ActionProjectType.KMS + }); + + ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionKmipActions.ReadClients, ProjectPermissionSub.Kmip); + + return kmipClient; + }; + + const listKmipClientsByProjectId = async ({ + actor, + actorId, + actorOrgId, + actorAuthMethod, + projectId, + ...rest + }: TListKmipClientsByProjectIdDTO) => { + const { permission } = await permissionService.getProjectPermission({ + actor, + actorId, + projectId, + actorAuthMethod, + actorOrgId, + actionProjectType: ActionProjectType.KMS + }); + + ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionKmipActions.ReadClients, ProjectPermissionSub.Kmip); + + return kmipClientDAL.findByProjectId({ projectId, ...rest }); + }; + + const createKmipClientCertificate = async ({ + actor, + actorId, + actorOrgId, + actorAuthMethod, + ttl, + keyAlgorithm, + clientId + }: TCreateKmipClientCertificateDTO) => { + const kmipClient = await kmipClientDAL.findById(clientId); + + if (!kmipClient) { + throw new NotFoundError({ + message: `KMIP client with ID ${clientId} does not exist` + }); + } + + const plan = await licenseService.getPlan(actorOrgId); + if (!plan.kmip) + throw new BadRequestError({ + message: "Failed to create KMIP client. Upgrade your plan to enterprise." + }); + + const { permission } = await permissionService.getProjectPermission({ + actor, + actorId, + projectId: kmipClient.projectId, + actorAuthMethod, + actorOrgId, + actionProjectType: ActionProjectType.KMS + }); + + ForbiddenError.from(permission).throwUnlessCan( + ProjectPermissionKmipActions.GenerateClientCertificates, + ProjectPermissionSub.Kmip + ); + + const kmipConfig = await kmipOrgConfigDAL.findOne({ + orgId: actorOrgId + }); + + if (!kmipConfig) { + throw new InternalServerError({ + message: "KMIP has not been configured for the organization" + }); + } + + const { decryptor } = await kmsService.createCipherPairWithDataKey({ + type: KmsDataKey.Organization, + orgId: actorOrgId + }); + + const caCertObj = new x509.X509Certificate( + decryptor({ cipherTextBlob: kmipConfig.encryptedClientIntermediateCaCertificate }) + ); + + const notBeforeDate = new Date(); + const notAfterDate = new Date(new Date().getTime() + ms(ttl)); + + const caCertNotBeforeDate = new Date(caCertObj.notBefore); + const caCertNotAfterDate = new Date(caCertObj.notAfter); + + // check not before constraint + if (notBeforeDate < caCertNotBeforeDate) { + throw new BadRequestError({ message: "notBefore date is before CA certificate's notBefore date" }); + } + + if (notBeforeDate > notAfterDate) throw new BadRequestError({ message: "notBefore date is after notAfter date" }); + + // check not after constraint + if (notAfterDate > caCertNotAfterDate) { + throw new BadRequestError({ message: "notAfter date is after CA certificate's notAfter date" }); + } + + const alg = keyAlgorithmToAlgCfg(keyAlgorithm); + const leafKeys = await crypto.subtle.generateKey(alg, true, ["sign", "verify"]); + + const extensions: x509.Extension[] = [ + new x509.BasicConstraintsExtension(false), + await x509.AuthorityKeyIdentifierExtension.create(caCertObj, false), + await x509.SubjectKeyIdentifierExtension.create(leafKeys.publicKey), + new x509.CertificatePolicyExtension(["2.5.29.32.0"]), // anyPolicy + new x509.KeyUsagesExtension( + // eslint-disable-next-line no-bitwise + x509.KeyUsageFlags[CertKeyUsage.DIGITAL_SIGNATURE] | + x509.KeyUsageFlags[CertKeyUsage.KEY_ENCIPHERMENT] | + x509.KeyUsageFlags[CertKeyUsage.KEY_AGREEMENT], + true + ), + new x509.ExtendedKeyUsageExtension([x509.ExtendedKeyUsage[CertExtendedKeyUsage.CLIENT_AUTH]], true) + ]; + + const caAlg = keyAlgorithmToAlgCfg(kmipConfig.caKeyAlgorithm as CertKeyAlgorithm); + + const caSkObj = crypto.createPrivateKey({ + key: decryptor({ cipherTextBlob: kmipConfig.encryptedClientIntermediateCaPrivateKey }), + format: "der", + type: "pkcs8" + }); + + const caPrivateKey = await crypto.subtle.importKey( + "pkcs8", + caSkObj.export({ format: "der", type: "pkcs8" }), + caAlg, + true, + ["sign"] + ); + + const serialNumber = createSerialNumber(); + const leafCert = await x509.X509CertificateGenerator.create({ + serialNumber, + subject: `OU=${kmipClient.projectId},CN=${clientId}`, + issuer: caCertObj.subject, + notBefore: notBeforeDate, + notAfter: notAfterDate, + signingKey: caPrivateKey, + publicKey: leafKeys.publicKey, + signingAlgorithm: alg, + extensions + }); + + const skLeafObj = KeyObject.from(leafKeys.privateKey); + + const rootCaCert = new x509.X509Certificate(decryptor({ cipherTextBlob: kmipConfig.encryptedRootCaCertificate })); + const serverIntermediateCaCert = new x509.X509Certificate( + decryptor({ cipherTextBlob: kmipConfig.encryptedServerIntermediateCaCertificate }) + ); + + await kmipClientCertificateDAL.create({ + kmipClientId: clientId, + keyAlgorithm, + issuedAt: notBeforeDate, + expiration: notAfterDate, + serialNumber + }); + + return { + serialNumber, + privateKey: skLeafObj.export({ format: "pem", type: "pkcs8" }) as string, + certificate: leafCert.toString("pem"), + certificateChain: constructPemChainFromCerts([serverIntermediateCaCert, rootCaCert]), + projectId: kmipClient.projectId + }; + }; + + const getServerCertificateBySerialNumber = async (orgId: string, serialNumber: string) => { + const serverCert = await kmipOrgServerCertificateDAL.findOne({ + serialNumber, + orgId + }); + + if (!serverCert) { + throw new NotFoundError({ + message: "Server certificate not found" + }); + } + + const { decryptor } = await kmsService.createCipherPairWithDataKey({ + type: KmsDataKey.Organization, + orgId + }); + + const parsedCertificate = new x509.X509Certificate(decryptor({ cipherTextBlob: serverCert.encryptedCertificate })); + + return { + publicKey: parsedCertificate.publicKey.toString("pem"), + keyAlgorithm: serverCert.keyAlgorithm as CertKeyAlgorithm + }; + }; + + const setupOrgKmip = async ({ caKeyAlgorithm, actorOrgId, actor, actorId, actorAuthMethod }: TSetupOrgKmipDTO) => { + const { permission } = await permissionService.getOrgPermission( + actor, + actorId, + actorOrgId, + actorAuthMethod, + actorOrgId + ); + ForbiddenError.from(permission).throwUnlessCan(OrgPermissionKmipActions.Setup, OrgPermissionSubjects.Kmip); + + const kmipConfig = await kmipOrgConfigDAL.findOne({ + orgId: actorOrgId + }); + + if (kmipConfig) { + throw new BadRequestError({ + message: "KMIP has already been configured for the organization" + }); + } + + const plan = await licenseService.getPlan(actorOrgId); + if (!plan.kmip) + throw new BadRequestError({ + message: "Failed to setup KMIP. Upgrade your plan to enterprise." + }); + + const alg = keyAlgorithmToAlgCfg(caKeyAlgorithm); + + // generate root CA + const rootCaSerialNumber = createSerialNumber(); + const rootCaKeys = await crypto.subtle.generateKey(alg, true, ["sign", "verify"]); + const rootCaSkObj = KeyObject.from(rootCaKeys.privateKey); + const rootCaIssuedAt = new Date(); + const rootCaExpiration = new Date(new Date().setFullYear(new Date().getFullYear() + 20)); + + const rootCaCert = await x509.X509CertificateGenerator.createSelfSigned({ + name: `CN=KMIP Root CA,OU=${actorOrgId}`, + serialNumber: rootCaSerialNumber, + notBefore: rootCaIssuedAt, + notAfter: rootCaExpiration, + signingAlgorithm: alg, + keys: rootCaKeys, + extensions: [ + // eslint-disable-next-line no-bitwise + new x509.KeyUsagesExtension(x509.KeyUsageFlags.keyCertSign | x509.KeyUsageFlags.cRLSign, true), + await x509.SubjectKeyIdentifierExtension.create(rootCaKeys.publicKey) + ] + }); + + // generate intermediate server CA + const serverIntermediateCaSerialNumber = createSerialNumber(); + const serverIntermediateCaIssuedAt = new Date(); + const serverIntermediateCaExpiration = new Date(new Date().setFullYear(new Date().getFullYear() + 10)); + const serverIntermediateCaKeys = await crypto.subtle.generateKey(alg, true, ["sign", "verify"]); + const serverIntermediateCaSkObj = KeyObject.from(serverIntermediateCaKeys.privateKey); + + const serverIntermediateCaCert = await x509.X509CertificateGenerator.create({ + serialNumber: serverIntermediateCaSerialNumber, + subject: `CN=KMIP Server Intermediate CA,OU=${actorOrgId}`, + issuer: rootCaCert.subject, + notBefore: serverIntermediateCaIssuedAt, + notAfter: serverIntermediateCaExpiration, + signingKey: rootCaKeys.privateKey, + publicKey: serverIntermediateCaKeys.publicKey, + signingAlgorithm: alg, + extensions: [ + new x509.KeyUsagesExtension( + // eslint-disable-next-line no-bitwise + x509.KeyUsageFlags.keyCertSign | + x509.KeyUsageFlags.cRLSign | + x509.KeyUsageFlags.digitalSignature | + x509.KeyUsageFlags.keyEncipherment, + true + ), + new x509.BasicConstraintsExtension(true, 0, true), + await x509.AuthorityKeyIdentifierExtension.create(rootCaCert, false), + await x509.SubjectKeyIdentifierExtension.create(serverIntermediateCaKeys.publicKey) + ] + }); + + // generate intermediate client CA + const clientIntermediateCaSerialNumber = createSerialNumber(); + const clientIntermediateCaIssuedAt = new Date(); + const clientIntermediateCaExpiration = new Date(new Date().setFullYear(new Date().getFullYear() + 10)); + const clientIntermediateCaKeys = await crypto.subtle.generateKey(alg, true, ["sign", "verify"]); + const clientIntermediateCaSkObj = KeyObject.from(clientIntermediateCaKeys.privateKey); + + const clientIntermediateCaCert = await x509.X509CertificateGenerator.create({ + serialNumber: clientIntermediateCaSerialNumber, + subject: `CN=KMIP Client Intermediate CA,OU=${actorOrgId}`, + issuer: rootCaCert.subject, + notBefore: clientIntermediateCaIssuedAt, + notAfter: clientIntermediateCaExpiration, + signingKey: rootCaKeys.privateKey, + publicKey: clientIntermediateCaKeys.publicKey, + signingAlgorithm: alg, + extensions: [ + new x509.KeyUsagesExtension( + // eslint-disable-next-line no-bitwise + x509.KeyUsageFlags.keyCertSign | + x509.KeyUsageFlags.cRLSign | + x509.KeyUsageFlags.digitalSignature | + x509.KeyUsageFlags.keyEncipherment, + true + ), + new x509.BasicConstraintsExtension(true, 0, true), + await x509.AuthorityKeyIdentifierExtension.create(rootCaCert, false), + await x509.SubjectKeyIdentifierExtension.create(clientIntermediateCaKeys.publicKey) + ] + }); + + const { encryptor } = await kmsService.createCipherPairWithDataKey({ + type: KmsDataKey.Organization, + orgId: actorOrgId + }); + + await kmipOrgConfigDAL.create({ + orgId: actorOrgId, + caKeyAlgorithm, + rootCaIssuedAt, + rootCaExpiration, + rootCaSerialNumber, + encryptedRootCaCertificate: encryptor({ plainText: Buffer.from(rootCaCert.rawData) }).cipherTextBlob, + encryptedRootCaPrivateKey: encryptor({ + plainText: rootCaSkObj.export({ + type: "pkcs8", + format: "der" + }) + }).cipherTextBlob, + serverIntermediateCaIssuedAt, + serverIntermediateCaExpiration, + serverIntermediateCaSerialNumber, + encryptedServerIntermediateCaCertificate: encryptor({ + plainText: Buffer.from(new Uint8Array(serverIntermediateCaCert.rawData)) + }).cipherTextBlob, + encryptedServerIntermediateCaChain: encryptor({ plainText: Buffer.from(rootCaCert.toString("pem")) }) + .cipherTextBlob, + encryptedServerIntermediateCaPrivateKey: encryptor({ + plainText: serverIntermediateCaSkObj.export({ + type: "pkcs8", + format: "der" + }) + }).cipherTextBlob, + clientIntermediateCaIssuedAt, + clientIntermediateCaExpiration, + clientIntermediateCaSerialNumber, + encryptedClientIntermediateCaCertificate: encryptor({ + plainText: Buffer.from(new Uint8Array(clientIntermediateCaCert.rawData)) + }).cipherTextBlob, + encryptedClientIntermediateCaChain: encryptor({ plainText: Buffer.from(rootCaCert.toString("pem")) }) + .cipherTextBlob, + encryptedClientIntermediateCaPrivateKey: encryptor({ + plainText: clientIntermediateCaSkObj.export({ + type: "pkcs8", + format: "der" + }) + }).cipherTextBlob + }); + + return { + serverCertificateChain: constructPemChainFromCerts([serverIntermediateCaCert, rootCaCert]), + clientCertificateChain: constructPemChainFromCerts([clientIntermediateCaCert, rootCaCert]) + }; + }; + + const getOrgKmip = async ({ actorOrgId, actor, actorId, actorAuthMethod }: TGetOrgKmipDTO) => { + await permissionService.getOrgPermission(actor, actorId, actorOrgId, actorAuthMethod, actorOrgId); + + const kmipConfig = await kmipOrgConfigDAL.findOne({ + orgId: actorOrgId + }); + + if (!kmipConfig) { + throw new BadRequestError({ + message: "KMIP has not been configured for the organization" + }); + } + + const { decryptor } = await kmsService.createCipherPairWithDataKey({ + type: KmsDataKey.Organization, + orgId: actorOrgId + }); + + const rootCaCert = new x509.X509Certificate(decryptor({ cipherTextBlob: kmipConfig.encryptedRootCaCertificate })); + const serverIntermediateCaCert = new x509.X509Certificate( + decryptor({ cipherTextBlob: kmipConfig.encryptedServerIntermediateCaCertificate }) + ); + + const clientIntermediateCaCert = new x509.X509Certificate( + decryptor({ cipherTextBlob: kmipConfig.encryptedClientIntermediateCaCertificate }) + ); + + return { + id: kmipConfig.id, + serverCertificateChain: constructPemChainFromCerts([serverIntermediateCaCert, rootCaCert]), + clientCertificateChain: constructPemChainFromCerts([clientIntermediateCaCert, rootCaCert]) + }; + }; + + const generateOrgKmipServerCertificate = async ({ + orgId, + ttl, + commonName, + altNames, + keyAlgorithm + }: TGenerateOrgKmipServerCertificateDTO) => { + const kmipOrgConfig = await kmipOrgConfigDAL.findOne({ + orgId + }); + + if (!kmipOrgConfig) { + throw new BadRequestError({ + message: "KMIP has not been configured for the organization" + }); + } + + const plan = await licenseService.getPlan(orgId); + if (!plan.kmip) + throw new BadRequestError({ + message: "Failed to generate KMIP server certificate. Upgrade your plan to enterprise." + }); + + const { decryptor, encryptor } = await kmsService.createCipherPairWithDataKey({ + type: KmsDataKey.Organization, + orgId + }); + + const caCertObj = new x509.X509Certificate( + decryptor({ cipherTextBlob: kmipOrgConfig.encryptedServerIntermediateCaCertificate }) + ); + + const notBeforeDate = new Date(); + const notAfterDate = new Date(new Date().getTime() + ms(ttl)); + + const caCertNotBeforeDate = new Date(caCertObj.notBefore); + const caCertNotAfterDate = new Date(caCertObj.notAfter); + + // check not before constraint + if (notBeforeDate < caCertNotBeforeDate) { + throw new BadRequestError({ message: "notBefore date is before CA certificate's notBefore date" }); + } + + if (notBeforeDate > notAfterDate) throw new BadRequestError({ message: "notBefore date is after notAfter date" }); + + // check not after constraint + if (notAfterDate > caCertNotAfterDate) { + throw new BadRequestError({ message: "notAfter date is after CA certificate's notAfter date" }); + } + + const alg = keyAlgorithmToAlgCfg(keyAlgorithm); + const leafKeys = await crypto.subtle.generateKey(alg, true, ["sign", "verify"]); + + const extensions: x509.Extension[] = [ + new x509.BasicConstraintsExtension(false), + await x509.AuthorityKeyIdentifierExtension.create(caCertObj, false), + await x509.SubjectKeyIdentifierExtension.create(leafKeys.publicKey), + new x509.CertificatePolicyExtension(["2.5.29.32.0"]), // anyPolicy + new x509.KeyUsagesExtension( + // eslint-disable-next-line no-bitwise + x509.KeyUsageFlags[CertKeyUsage.DIGITAL_SIGNATURE] | x509.KeyUsageFlags[CertKeyUsage.KEY_ENCIPHERMENT], + true + ), + new x509.ExtendedKeyUsageExtension([x509.ExtendedKeyUsage[CertExtendedKeyUsage.SERVER_AUTH]], true) + ]; + + const altNamesArray: { + type: "email" | "dns" | "ip"; + value: string; + }[] = altNames + .split(",") + .map((name) => name.trim()) + .map((altName) => { + if (isValidHostname(altName)) { + return { + type: "dns", + value: altName + }; + } + + if (isValidIp(altName)) { + return { + type: "ip", + value: altName + }; + } + + throw new Error(`Invalid altName: ${altName}`); + }); + + const altNamesExtension = new x509.SubjectAlternativeNameExtension(altNamesArray, false); + extensions.push(altNamesExtension); + + const caAlg = keyAlgorithmToAlgCfg(kmipOrgConfig.caKeyAlgorithm as CertKeyAlgorithm); + + const decryptedCaCertChain = decryptor({ + cipherTextBlob: kmipOrgConfig.encryptedServerIntermediateCaChain + }).toString("utf-8"); + + const caSkObj = crypto.createPrivateKey({ + key: decryptor({ cipherTextBlob: kmipOrgConfig.encryptedServerIntermediateCaPrivateKey }), + format: "der", + type: "pkcs8" + }); + + const caPrivateKey = await crypto.subtle.importKey( + "pkcs8", + caSkObj.export({ format: "der", type: "pkcs8" }), + caAlg, + true, + ["sign"] + ); + + const serialNumber = createSerialNumber(); + const leafCert = await x509.X509CertificateGenerator.create({ + serialNumber, + subject: `CN=${commonName}`, + issuer: caCertObj.subject, + notBefore: notBeforeDate, + notAfter: notAfterDate, + signingKey: caPrivateKey, + publicKey: leafKeys.publicKey, + signingAlgorithm: alg, + extensions + }); + + const skLeafObj = KeyObject.from(leafKeys.privateKey); + const certificateChain = `${caCertObj.toString("pem")}\n${decryptedCaCertChain}`.trim(); + + await kmipOrgServerCertificateDAL.create({ + orgId, + keyAlgorithm, + issuedAt: notBeforeDate, + expiration: notAfterDate, + serialNumber, + commonName, + altNames, + encryptedCertificate: encryptor({ plainText: Buffer.from(new Uint8Array(leafCert.rawData)) }).cipherTextBlob, + encryptedChain: encryptor({ plainText: Buffer.from(certificateChain) }).cipherTextBlob + }); + + return { + serialNumber, + privateKey: skLeafObj.export({ format: "pem", type: "pkcs8" }) as string, + certificate: leafCert.toString("pem"), + certificateChain + }; + }; + + const registerServer = async ({ + actorOrgId, + actor, + actorId, + actorAuthMethod, + ttl, + commonName, + keyAlgorithm, + hostnamesOrIps + }: TRegisterServerDTO) => { + const { permission } = await permissionService.getOrgPermission( + actor, + actorId, + actorOrgId, + actorAuthMethod, + actorOrgId + ); + + ForbiddenError.from(permission).throwUnlessCan(OrgPermissionKmipActions.Proxy, OrgPermissionSubjects.Kmip); + + const kmipConfig = await kmipOrgConfigDAL.findOne({ + orgId: actorOrgId + }); + + if (!kmipConfig) { + throw new BadRequestError({ + message: "KMIP has not been configured for the organization" + }); + } + + const plan = await licenseService.getPlan(actorOrgId); + if (!plan.kmip) + throw new BadRequestError({ + message: "Failed to register KMIP server. Upgrade your plan to enterprise." + }); + + const { privateKey, certificate, certificateChain, serialNumber } = await generateOrgKmipServerCertificate({ + orgId: actorOrgId, + commonName: commonName ?? "kmip-server", + altNames: hostnamesOrIps, + keyAlgorithm: keyAlgorithm ?? (kmipConfig.caKeyAlgorithm as CertKeyAlgorithm), + ttl + }); + + const { clientCertificateChain } = await getOrgKmip({ + actor, + actorAuthMethod, + actorId, + actorOrgId + }); + + return { + serverCertificateSerialNumber: serialNumber, + clientCertificateChain, + privateKey, + certificate, + certificateChain + }; + }; + + return { + createKmipClient, + updateKmipClient, + deleteKmipClient, + getKmipClient, + listKmipClientsByProjectId, + createKmipClientCertificate, + setupOrgKmip, + generateOrgKmipServerCertificate, + getOrgKmip, + getServerCertificateBySerialNumber, + registerServer + }; +}; diff --git a/backend/src/ee/services/kmip/kmip-types.ts b/backend/src/ee/services/kmip/kmip-types.ts new file mode 100644 index 0000000000..a259a79b83 --- /dev/null +++ b/backend/src/ee/services/kmip/kmip-types.ts @@ -0,0 +1,102 @@ +import { SymmetricEncryption } from "@app/lib/crypto/cipher"; +import { OrderByDirection, TOrgPermission, TProjectPermission } from "@app/lib/types"; +import { CertKeyAlgorithm } from "@app/services/certificate/certificate-types"; + +import { KmipPermission } from "./kmip-enum"; + +export type TCreateKmipClientCertificateDTO = { + clientId: string; + keyAlgorithm: CertKeyAlgorithm; + ttl: string; +} & Omit; + +export type TCreateKmipClientDTO = { + name: string; + description?: string; + permissions: KmipPermission[]; +} & TProjectPermission; + +export type TUpdateKmipClientDTO = { + id: string; + name?: string; + description?: string; + permissions?: KmipPermission[]; +} & Omit; + +export type TDeleteKmipClientDTO = { + id: string; +} & Omit; + +export type TGetKmipClientDTO = { + id: string; +} & Omit; + +export enum KmipClientOrderBy { + Name = "name" +} + +export type TListKmipClientsByProjectIdDTO = { + offset?: number; + limit?: number; + orderBy?: KmipClientOrderBy; + orderDirection?: OrderByDirection; + search?: string; +} & TProjectPermission; + +type KmipOperationBaseDTO = { + clientId: string; + projectId: string; +} & Omit; + +export type TKmipCreateDTO = { + algorithm: SymmetricEncryption; +} & KmipOperationBaseDTO; + +export type TKmipGetDTO = { + id: string; +} & KmipOperationBaseDTO; + +export type TKmipGetAttributesDTO = { + id: string; +} & KmipOperationBaseDTO; + +export type TKmipDestroyDTO = { + id: string; +} & KmipOperationBaseDTO; + +export type TKmipActivateDTO = { + id: string; +} & KmipOperationBaseDTO; + +export type TKmipRevokeDTO = { + id: string; +} & KmipOperationBaseDTO; + +export type TKmipLocateDTO = KmipOperationBaseDTO; + +export type TKmipRegisterDTO = { + name: string; + key: string; + algorithm: SymmetricEncryption; +} & KmipOperationBaseDTO; + +export type TSetupOrgKmipDTO = { + caKeyAlgorithm: CertKeyAlgorithm; +} & Omit; + +export type TGetOrgKmipDTO = Omit; + +export type TGenerateOrgKmipServerCertificateDTO = { + commonName: string; + altNames: string; + keyAlgorithm: CertKeyAlgorithm; + ttl: string; + orgId: string; +}; + +export type TRegisterServerDTO = { + hostnamesOrIps: string; + commonName?: string; + keyAlgorithm?: CertKeyAlgorithm; + ttl: string; +} & Omit; diff --git a/backend/src/ee/services/license/license-fns.ts b/backend/src/ee/services/license/license-fns.ts index c54daa3a64..cab19a5300 100644 --- a/backend/src/ee/services/license/license-fns.ts +++ b/backend/src/ee/services/license/license-fns.ts @@ -50,7 +50,8 @@ export const getDefaultOnPremFeatures = (): TFeatureSet => ({ }, pkiEst: false, enforceMfa: false, - projectTemplates: false + projectTemplates: false, + kmip: false }); export const setupLicenseRequestWithStore = (baseURL: string, refreshUrl: string, licenseKey: string) => { diff --git a/backend/src/ee/services/license/license-types.ts b/backend/src/ee/services/license/license-types.ts index 0242377b04..2b4dbe49e7 100644 --- a/backend/src/ee/services/license/license-types.ts +++ b/backend/src/ee/services/license/license-types.ts @@ -68,6 +68,7 @@ export type TFeatureSet = { pkiEst: boolean; enforceMfa: boolean; projectTemplates: false; + kmip: false; }; export type TOrgPlansTableDTO = { diff --git a/backend/src/ee/services/permission/org-permission.ts b/backend/src/ee/services/permission/org-permission.ts index c72008057f..03d1747d46 100644 --- a/backend/src/ee/services/permission/org-permission.ts +++ b/backend/src/ee/services/permission/org-permission.ts @@ -23,6 +23,11 @@ export enum OrgPermissionAppConnectionActions { Connect = "connect" } +export enum OrgPermissionKmipActions { + Proxy = "proxy", + Setup = "setup" +} + export enum OrgPermissionAdminConsoleAction { AccessAllProjects = "access-all-projects" } @@ -44,7 +49,8 @@ export enum OrgPermissionSubjects { AdminConsole = "organization-admin-console", AuditLogs = "audit-logs", ProjectTemplates = "project-templates", - AppConnections = "app-connections" + AppConnections = "app-connections", + Kmip = "kmip" } export type AppConnectionSubjectFields = { @@ -74,7 +80,8 @@ export type OrgPermissionSet = | (ForcedSubject & AppConnectionSubjectFields) ) ] - | [OrgPermissionAdminConsoleAction, OrgPermissionSubjects.AdminConsole]; + | [OrgPermissionAdminConsoleAction, OrgPermissionSubjects.AdminConsole] + | [OrgPermissionKmipActions, OrgPermissionSubjects.Kmip]; const AppConnectionConditionSchema = z .object({ @@ -167,6 +174,12 @@ export const OrgPermissionSchema = z.discriminatedUnion("subject", [ action: CASL_ACTION_SCHEMA_NATIVE_ENUM(OrgPermissionAdminConsoleAction).describe( "Describe what action an entity can take." ) + }), + z.object({ + subject: z.literal(OrgPermissionSubjects.Kmip).describe("The entity this permission pertains to."), + action: CASL_ACTION_SCHEMA_NATIVE_ENUM(OrgPermissionKmipActions).describe( + "Describe what action an entity can take." + ) }) ]); @@ -253,6 +266,11 @@ const buildAdminPermission = () => { can(OrgPermissionAdminConsoleAction.AccessAllProjects, OrgPermissionSubjects.AdminConsole); + can(OrgPermissionKmipActions.Setup, OrgPermissionSubjects.Kmip); + + // the proxy assignment is temporary in order to prevent "more privilege" error during role assignment to MI + can(OrgPermissionKmipActions.Proxy, OrgPermissionSubjects.Kmip); + return rules; }; diff --git a/backend/src/ee/services/permission/project-permission.ts b/backend/src/ee/services/permission/project-permission.ts index 657e9ce3a5..4389c38666 100644 --- a/backend/src/ee/services/permission/project-permission.ts +++ b/backend/src/ee/services/permission/project-permission.ts @@ -44,6 +44,14 @@ export enum ProjectPermissionSecretSyncActions { RemoveSecrets = "remove-secrets" } +export enum ProjectPermissionKmipActions { + CreateClients = "create-clients", + UpdateClients = "update-clients", + DeleteClients = "delete-clients", + ReadClients = "read-clients", + GenerateClientCertificates = "generate-client-certificates" +} + export enum ProjectPermissionSub { Role = "role", Member = "member", @@ -75,7 +83,8 @@ export enum ProjectPermissionSub { PkiCollections = "pki-collections", Kms = "kms", Cmek = "cmek", - SecretSyncs = "secret-syncs" + SecretSyncs = "secret-syncs", + Kmip = "kmip" } export type SecretSubjectFields = { @@ -156,6 +165,7 @@ export type ProjectPermissionSet = | [ProjectPermissionActions, ProjectPermissionSub.PkiAlerts] | [ProjectPermissionActions, ProjectPermissionSub.PkiCollections] | [ProjectPermissionSecretSyncActions, ProjectPermissionSub.SecretSyncs] + | [ProjectPermissionKmipActions, ProjectPermissionSub.Kmip] | [ProjectPermissionCmekActions, ProjectPermissionSub.Cmek] | [ProjectPermissionActions.Delete, ProjectPermissionSub.Project] | [ProjectPermissionActions.Edit, ProjectPermissionSub.Project] @@ -410,6 +420,12 @@ const GeneralPermissionSchema = [ action: CASL_ACTION_SCHEMA_NATIVE_ENUM(ProjectPermissionSecretSyncActions).describe( "Describe what action an entity can take." ) + }), + z.object({ + subject: z.literal(ProjectPermissionSub.Kmip).describe("The entity this permission pertains to."), + action: CASL_ACTION_SCHEMA_NATIVE_ENUM(ProjectPermissionKmipActions).describe( + "Describe what action an entity can take." + ) }) ]; @@ -575,6 +591,18 @@ const buildAdminPermissionRules = () => { ], ProjectPermissionSub.SecretSyncs ); + + can( + [ + ProjectPermissionKmipActions.CreateClients, + ProjectPermissionKmipActions.UpdateClients, + ProjectPermissionKmipActions.DeleteClients, + ProjectPermissionKmipActions.ReadClients, + ProjectPermissionKmipActions.GenerateClientCertificates + ], + ProjectPermissionSub.Kmip + ); + return rules; }; diff --git a/backend/src/lib/ip/index.ts b/backend/src/lib/ip/index.ts index 6503165f66..1e247dcde8 100644 --- a/backend/src/lib/ip/index.ts +++ b/backend/src/lib/ip/index.ts @@ -103,6 +103,16 @@ export const isValidIpOrCidr = (ip: string): boolean => { return false; }; +export const isValidIp = (ip: string) => { + return net.isIPv4(ip) || net.isIPv6(ip); +}; + +export const isValidHostname = (name: string) => { + const hostnameRegex = /^(?!:\/\/)(\*\.)?([a-zA-Z0-9-_]{1,63}\.?)+(?!:\/\/)([a-zA-Z]{2,63})$/; + + return hostnameRegex.test(name); +}; + export type TIp = { ipAddress: string; type: IPType; diff --git a/backend/src/server/routes/index.ts b/backend/src/server/routes/index.ts index 10da81bf57..155b52cfbf 100644 --- a/backend/src/server/routes/index.ts +++ b/backend/src/server/routes/index.ts @@ -35,6 +35,12 @@ import { HsmModule } from "@app/ee/services/hsm/hsm-types"; import { identityProjectAdditionalPrivilegeDALFactory } from "@app/ee/services/identity-project-additional-privilege/identity-project-additional-privilege-dal"; import { identityProjectAdditionalPrivilegeServiceFactory } from "@app/ee/services/identity-project-additional-privilege/identity-project-additional-privilege-service"; import { identityProjectAdditionalPrivilegeV2ServiceFactory } from "@app/ee/services/identity-project-additional-privilege-v2/identity-project-additional-privilege-v2-service"; +import { kmipClientCertificateDALFactory } from "@app/ee/services/kmip/kmip-client-certificate-dal"; +import { kmipClientDALFactory } from "@app/ee/services/kmip/kmip-client-dal"; +import { kmipOperationServiceFactory } from "@app/ee/services/kmip/kmip-operation-service"; +import { kmipOrgConfigDALFactory } from "@app/ee/services/kmip/kmip-org-config-dal"; +import { kmipOrgServerCertificateDALFactory } from "@app/ee/services/kmip/kmip-org-server-certificate-dal"; +import { kmipServiceFactory } from "@app/ee/services/kmip/kmip-service"; import { ldapConfigDALFactory } from "@app/ee/services/ldap-config/ldap-config-dal"; import { ldapConfigServiceFactory } from "@app/ee/services/ldap-config/ldap-config-service"; import { ldapGroupMapDALFactory } from "@app/ee/services/ldap-config/ldap-group-map-dal"; @@ -382,6 +388,10 @@ export const registerRoutes = async ( const projectTemplateDAL = projectTemplateDALFactory(db); const resourceMetadataDAL = resourceMetadataDALFactory(db); + const kmipClientDAL = kmipClientDALFactory(db); + const kmipClientCertificateDAL = kmipClientCertificateDALFactory(db); + const kmipOrgConfigDAL = kmipOrgConfigDALFactory(db); + const kmipOrgServerCertificateDAL = kmipOrgServerCertificateDALFactory(db); const permissionService = permissionServiceFactory({ permissionDAL, @@ -1429,6 +1439,24 @@ export const registerRoutes = async ( keyStore }); + const kmipService = kmipServiceFactory({ + kmipClientDAL, + permissionService, + kmipClientCertificateDAL, + kmipOrgConfigDAL, + kmsService, + kmipOrgServerCertificateDAL, + licenseService + }); + + const kmipOperationService = kmipOperationServiceFactory({ + kmsService, + kmsDAL, + projectDAL, + kmipClientDAL, + permissionService + }); + await superAdminService.initServerCfg(); // setup the communication with license key server @@ -1527,7 +1555,9 @@ export const registerRoutes = async ( projectTemplate: projectTemplateService, totp: totpService, appConnection: appConnectionService, - secretSync: secretSyncService + secretSync: secretSyncService, + kmip: kmipService, + kmipOperation: kmipOperationService }); const cronJobs: CronJob[] = []; @@ -1539,7 +1569,8 @@ export const registerRoutes = async ( } server.decorate("store", { - user: userDAL + user: userDAL, + kmipClient: kmipClientDAL }); await server.register(injectIdentity, { userDAL, serviceTokenDAL }); diff --git a/backend/src/services/auth/auth-type.ts b/backend/src/services/auth/auth-type.ts index 05412a73a0..497414a60b 100644 --- a/backend/src/services/auth/auth-type.ts +++ b/backend/src/services/auth/auth-type.ts @@ -35,6 +35,7 @@ export enum AuthMode { export enum ActorType { // would extend to AWS, Azure, ... PLATFORM = "platform", // Useful for when we want to perform logging on automated actions such as integration syncs. + KMIP_CLIENT = "kmipClient", USER = "user", // userIdentity SERVICE = "service", IDENTITY = "identity", diff --git a/backend/src/services/certificate-authority/certificate-authority-validators.ts b/backend/src/services/certificate-authority/certificate-authority-validators.ts index 16e7dcf49d..1840652cad 100644 --- a/backend/src/services/certificate-authority/certificate-authority-validators.ts +++ b/backend/src/services/certificate-authority/certificate-authority-validators.ts @@ -1,5 +1,7 @@ import { z } from "zod"; +import { isValidIp } from "@app/lib/ip"; + const isValidDate = (dateString: string) => { const date = new Date(dateString); return !Number.isNaN(date.getTime()); @@ -25,7 +27,7 @@ export const validateAltNamesField = z if (data === "") return true; // Split and validate each alt name return data.split(", ").every((name) => { - return hostnameRegex.test(name) || z.string().email().safeParse(name).success; + return hostnameRegex.test(name) || z.string().email().safeParse(name).success || isValidIp(name); }); }, { diff --git a/backend/src/services/certificate/certificate-fns.ts b/backend/src/services/certificate/certificate-fns.ts index 1768a50117..45ad5963c8 100644 --- a/backend/src/services/certificate/certificate-fns.ts +++ b/backend/src/services/certificate/certificate-fns.ts @@ -40,3 +40,9 @@ export const isCertChainValid = async (certificates: x509.X509Certificate[]) => // chain.build() implicitly verifies the chain return chainItems.length === certificates.length; }; + +export const constructPemChainFromCerts = (certificates: x509.X509Certificate[]) => + certificates + .map((cert) => cert.toString("pem")) + .join("\n") + .trim(); diff --git a/backend/src/services/kms/kms-key-dal.ts b/backend/src/services/kms/kms-key-dal.ts index 69a1754710..a0dd12191d 100644 --- a/backend/src/services/kms/kms-key-dal.ts +++ b/backend/src/services/kms/kms-key-dal.ts @@ -93,6 +93,32 @@ export const kmskeyDALFactory = (db: TDbClient) => { } }; + const findProjectCmeks = async (projectId: string, tx?: Knex) => { + try { + const result = await (tx || db.replicaNode())(TableName.KmsKey) + .where({ + [`${TableName.KmsKey}.projectId` as "projectId"]: projectId, + [`${TableName.KmsKey}.isReserved` as "isReserved"]: false + }) + .join(TableName.Organization, `${TableName.KmsKey}.orgId`, `${TableName.Organization}.id`) + .join(TableName.InternalKms, `${TableName.KmsKey}.id`, `${TableName.InternalKms}.kmsKeyId`) + .select(selectAllTableCols(TableName.KmsKey)) + .select( + db.ref("encryptionAlgorithm").withSchema(TableName.InternalKms).as("internalKmsEncryptionAlgorithm"), + db.ref("version").withSchema(TableName.InternalKms).as("internalKmsVersion") + ); + + return result.map((entry) => ({ + ...KmsKeysSchema.parse(entry), + isActive: !entry.isDisabled, + algorithm: entry.internalKmsEncryptionAlgorithm, + version: entry.internalKmsVersion + })); + } catch (error) { + throw new DatabaseError({ error, name: "Find project cmeks" }); + } + }; + const listCmeksByProjectId = async ( { projectId, @@ -167,5 +193,5 @@ export const kmskeyDALFactory = (db: TDbClient) => { } }; - return { ...kmsOrm, findByIdWithAssociatedKms, listCmeksByProjectId, findCmekById, findCmekByName }; + return { ...kmsOrm, findByIdWithAssociatedKms, listCmeksByProjectId, findCmekById, findCmekByName, findProjectCmeks }; }; diff --git a/backend/src/services/kms/kms-service.ts b/backend/src/services/kms/kms-service.ts index babba4cb22..cfd64a89a9 100644 --- a/backend/src/services/kms/kms-service.ts +++ b/backend/src/services/kms/kms-service.ts @@ -37,6 +37,8 @@ import { TEncryptWithKmsDataKeyDTO, TEncryptWithKmsDTO, TGenerateKMSDTO, + TGetKeyMaterialDTO, + TImportKeyMaterialDTO, TUpdateProjectSecretManagerKmsKeyDTO } from "./kms-types"; @@ -325,6 +327,72 @@ export const kmsServiceFactory = ({ }; }; + const getKeyMaterial = async ({ kmsId }: TGetKeyMaterialDTO) => { + const kmsDoc = await kmsDAL.findByIdWithAssociatedKms(kmsId); + if (!kmsDoc) { + throw new NotFoundError({ message: `KMS with ID '${kmsId}' not found` }); + } + + if (kmsDoc.isReserved) { + throw new BadRequestError({ + message: "Cannot get key material for reserved key" + }); + } + + if (kmsDoc.externalKms) { + throw new BadRequestError({ + message: "Cannot get key material for external key" + }); + } + + const keyCipher = symmetricCipherService(SymmetricEncryption.AES_GCM_256); + const kmsKey = keyCipher.decrypt(kmsDoc.internalKms?.encryptedKey as Buffer, ROOT_ENCRYPTION_KEY); + + return kmsKey; + }; + + const importKeyMaterial = async ( + { key, algorithm, name, isReserved, projectId, orgId }: TImportKeyMaterialDTO, + tx?: Knex + ) => { + const cipher = symmetricCipherService(SymmetricEncryption.AES_GCM_256); + + const expectedByteLength = getByteLengthForAlgorithm(algorithm); + if (key.byteLength !== expectedByteLength) { + throw new BadRequestError({ + message: `Invalid key length for ${algorithm}. Expected ${expectedByteLength} bytes but got ${key.byteLength} bytes` + }); + } + + const encryptedKeyMaterial = cipher.encrypt(key, ROOT_ENCRYPTION_KEY); + const sanitizedName = name ? slugify(name) : slugify(alphaNumericNanoId(8).toLowerCase()); + const dbQuery = async (db: Knex) => { + const kmsDoc = await kmsDAL.create( + { + name: sanitizedName, + orgId, + isReserved, + projectId + }, + db + ); + + await internalKmsDAL.create( + { + version: 1, + encryptedKey: encryptedKeyMaterial, + encryptionAlgorithm: algorithm, + kmsKeyId: kmsDoc.id + }, + db + ); + return kmsDoc; + }; + if (tx) return dbQuery(tx); + const doc = await kmsDAL.transaction(async (tx2) => dbQuery(tx2)); + return doc; + }; + const encryptWithKmsKey = async ({ kmsId }: Omit, tx?: Knex) => { const kmsDoc = await kmsDAL.findByIdWithAssociatedKms(kmsId, tx); if (!kmsDoc) { @@ -944,6 +1012,8 @@ export const kmsServiceFactory = ({ getProjectKeyBackup, loadProjectKeyBackup, getKmsById, - createCipherPairWithDataKey + createCipherPairWithDataKey, + getKeyMaterial, + importKeyMaterial }; }; diff --git a/backend/src/services/kms/kms-types.ts b/backend/src/services/kms/kms-types.ts index f655d4b5d1..8be0b29fc8 100644 --- a/backend/src/services/kms/kms-types.ts +++ b/backend/src/services/kms/kms-types.ts @@ -61,3 +61,15 @@ export enum RootKeyEncryptionStrategy { Software = "SOFTWARE", HSM = "HSM" } +export type TGetKeyMaterialDTO = { + kmsId: string; +}; + +export type TImportKeyMaterialDTO = { + key: Buffer; + algorithm: SymmetricEncryption; + name?: string; + isReserved: boolean; + projectId: string; + orgId: string; +}; diff --git a/cli/go.mod b/cli/go.mod index 637bd904d3..b96772c917 100644 --- a/cli/go.mod +++ b/cli/go.mod @@ -10,7 +10,8 @@ require ( github.com/fatih/semgroup v1.2.0 github.com/gitleaks/go-gitdiff v0.8.0 github.com/h2non/filetype v1.1.3 - github.com/infisical/go-sdk v0.4.7 + github.com/infisical/go-sdk v0.4.8 + github.com/infisical/infisical-kmip v0.3.5 github.com/mattn/go-isatty v0.0.20 github.com/muesli/ansi v0.0.0-20221106050444-61f0cd9a192a github.com/muesli/mango-cobra v1.2.0 @@ -65,6 +66,9 @@ require ( github.com/google/s2a-go v0.1.7 // indirect github.com/googleapis/enterprise-certificate-proxy v0.3.2 // indirect github.com/googleapis/gax-go/v2 v2.12.5 // indirect + github.com/gosimple/slug v1.15.0 // indirect + github.com/gosimple/unidecode v1.0.1 // indirect + github.com/hashicorp/golang-lru/v2 v2.0.7 // indirect github.com/hashicorp/hcl v1.0.0 // indirect github.com/lucasb-eyer/go-colorful v1.2.0 // indirect github.com/magiconair/properties v1.8.5 // indirect @@ -77,6 +81,7 @@ require ( github.com/muesli/termenv v0.15.2 // indirect github.com/oklog/ulid v1.3.1 // indirect github.com/pelletier/go-toml v1.9.3 // indirect + github.com/pkg/errors v0.9.1 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect github.com/rivo/uniseg v0.2.0 // indirect github.com/spf13/afero v1.6.0 // indirect @@ -91,12 +96,12 @@ require ( go.opentelemetry.io/otel v1.24.0 // indirect go.opentelemetry.io/otel/metric v1.24.0 // indirect go.opentelemetry.io/otel/trace v1.24.0 // indirect - golang.org/x/net v0.27.0 // indirect + golang.org/x/net v0.33.0 // indirect golang.org/x/oauth2 v0.21.0 // indirect golang.org/x/sync v0.10.0 // indirect golang.org/x/sys v0.28.0 // indirect golang.org/x/text v0.21.0 // indirect - golang.org/x/time v0.5.0 // indirect + golang.org/x/time v0.6.0 // indirect google.golang.org/api v0.188.0 // indirect google.golang.org/genproto/googleapis/api v0.0.0-20240701130421-f6361c86f094 // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20240708141625-4ad9e859172b // indirect @@ -108,7 +113,7 @@ require ( require ( github.com/fatih/color v1.17.0 - github.com/go-resty/resty/v2 v2.13.1 + github.com/go-resty/resty/v2 v2.16.5 github.com/inconshreveable/mousetrap v1.0.1 // indirect github.com/jedib0t/go-pretty v4.3.0+incompatible github.com/manifoldco/promptui v0.9.0 diff --git a/cli/go.sum b/cli/go.sum index 79fe1fd7f6..67a3d3275e 100644 --- a/cli/go.sum +++ b/cli/go.sum @@ -152,8 +152,8 @@ github.com/go-openapi/errors v0.20.2 h1:dxy7PGTqEh94zj2E3h1cUmQQWiM1+aeCROfAr02E github.com/go-openapi/errors v0.20.2/go.mod h1:cM//ZKUKyO06HSwqAelJ5NsEMMcpa6VpXe8DOa1Mi1M= github.com/go-openapi/strfmt v0.21.3 h1:xwhj5X6CjXEZZHMWy1zKJxvW9AfHC9pkyUjLvHtKG7o= github.com/go-openapi/strfmt v0.21.3/go.mod h1:k+RzNO0Da+k3FrrynSNN8F7n/peCmQQqbbXjtDfvmGg= -github.com/go-resty/resty/v2 v2.13.1 h1:x+LHXBI2nMB1vqndymf26quycC4aggYJ7DECYbiz03g= -github.com/go-resty/resty/v2 v2.13.1/go.mod h1:GznXlLxkq6Nh4sU59rPmUw3VtgpO3aS96ORAI6Q7d+0= +github.com/go-resty/resty/v2 v2.16.5 h1:hBKqmWrr7uRc3euHVqmh1HTHcKn99Smr7o5spptdhTM= +github.com/go-resty/resty/v2 v2.16.5/go.mod h1:hkJtXbA2iKHzJheXYvQ8snQES5ZLGKMwQ07xAwp/fiA= github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= github.com/godbus/dbus/v5 v5.1.0 h1:4KLkAxT3aOY8Li4FRJe/KvhoNFFxo0m6fNuFUO8QJUk= github.com/godbus/dbus/v5 v5.1.0/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= @@ -237,6 +237,10 @@ github.com/googleapis/gax-go/v2 v2.12.5 h1:8gw9KZK8TiVKB6q3zHY3SBzLnrGp6HQjyfYBY github.com/googleapis/gax-go/v2 v2.12.5/go.mod h1:BUDKcWo+RaKq5SC9vVYL0wLADa3VcfswbOMMRmB9H3E= github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1 h1:EGx4pi6eqNxGaHF6qqu48+N2wcFQ5qg5FXgOdqsJ5d8= github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= +github.com/gosimple/slug v1.15.0 h1:wRZHsRrRcs6b0XnxMUBM6WK1U1Vg5B0R7VkIf1Xzobo= +github.com/gosimple/slug v1.15.0/go.mod h1:UiRaFH+GEilHstLUmcBgWcI42viBN7mAb818JrYOeFQ= +github.com/gosimple/unidecode v1.0.1 h1:hZzFTMMqSswvf0LBJZCZgThIZrpDHFXux9KeGmn6T/o= +github.com/gosimple/unidecode v1.0.1/go.mod h1:CP0Cr1Y1kogOtx0bJblKzsVWrqYaqfNOnHzpgWw4Awc= github.com/grpc-ecosystem/grpc-gateway v1.16.0/go.mod h1:BDjrQk3hbvj6Nolgz8mAMFbcEtjT1g+wF4CSlocrBnw= github.com/h2non/filetype v1.1.3 h1:FKkx9QbD7HR/zjK1Ia5XiBsq9zdLi5Kf3zGyFTAFkGg= github.com/h2non/filetype v1.1.3/go.mod h1:319b3zT68BvV+WRj7cwy856M2ehB3HqNOt6sy1HndBY= @@ -255,6 +259,8 @@ github.com/hashicorp/go-uuid v1.0.1/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/b github.com/hashicorp/go.net v0.0.1/go.mod h1:hjKkEWcCURg++eb33jQU7oqQcI9XDCnUzHA0oac0k90= github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= +github.com/hashicorp/golang-lru/v2 v2.0.7 h1:a+bsQ5rvGLjzHuww6tVxozPZFVghXaHOwFs4luLUK2k= +github.com/hashicorp/golang-lru/v2 v2.0.7/go.mod h1:QeFd9opnmA6QUJc5vARoKUSoFhyfM2/ZepoAG6RGpeM= github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4= github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= github.com/hashicorp/logutils v1.0.0/go.mod h1:QIAnNjmIWmVIIkWDTG1z5v++HQmx9WQRO+LraFDTW64= @@ -265,8 +271,10 @@ github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1: github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= github.com/inconshreveable/mousetrap v1.0.1 h1:U3uMjPSQEBMNp1lFxmllqCPM6P5u/Xq7Pgzkat/bFNc= github.com/inconshreveable/mousetrap v1.0.1/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= -github.com/infisical/go-sdk v0.4.7 h1:+cxIdDfciMh0Syxbxbqjhvz9/ShnN1equ2zqlVQYGtw= -github.com/infisical/go-sdk v0.4.7/go.mod h1:6fWzAwTPIoKU49mQ2Oxu+aFnJu9n7k2JcNrZjzhHM2M= +github.com/infisical/go-sdk v0.4.8 h1:aphRnaauC5//PkP1ZbY9RSK2RiT1LjPS5o4CbX0x5OQ= +github.com/infisical/go-sdk v0.4.8/go.mod h1:bMO9xSaBeXkDBhTIM4FkkREAfw2V8mv5Bm7lvo4+uDk= +github.com/infisical/infisical-kmip v0.3.5 h1:QM3s0e18B+mYv3a9HQNjNAlbwZJBzXq5BAJM2scIeiE= +github.com/infisical/infisical-kmip v0.3.5/go.mod h1:bO1M4YtKyutNg1bREPmlyZspC5duSR7hyQ3lPmLzrIs= github.com/jedib0t/go-pretty v4.3.0+incompatible h1:CGs8AVhEKg/n9YbUenWmNStRW2PHJzaeDodcfvRAbIo= github.com/jedib0t/go-pretty v4.3.0+incompatible/go.mod h1:XemHduiw8R651AF9Pt4FwCTKeG3oo7hrHJAoznj9nag= github.com/json-iterator/go v1.1.11/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= @@ -340,6 +348,7 @@ github.com/pelletier/go-toml v1.9.3/go.mod h1:u1nR/EPcESfeI/szUZKdtJ0xRNbUoANCko github.com/petar-dambovaliev/aho-corasick v0.0.0-20211021192214-5ab2d9280aa9 h1:lL+y4Xv20pVlCGyLzNHRC0I0rIHhIL1lTvHizoS/dU8= github.com/petar-dambovaliev/aho-corasick v0.0.0-20211021192214-5ab2d9280aa9/go.mod h1:EHPiTAKtiFmrMldLUNswFwfZ2eJIYBHktdaUTZxYWRw= github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/sftp v1.10.1/go.mod h1:lYOWFsE0bwd1+KfKJaKeuokY15vzFx25BLbzYYoAxZI= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= @@ -413,7 +422,6 @@ github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9de github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= github.com/yuin/goldmark v1.4.0/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= -github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= go.etcd.io/etcd/api/v3 v3.5.0/go.mod h1:cbVKeC6lCfl7j/8jBhAK6aIYO9XOjdptoxU/nLQcPvs= go.etcd.io/etcd/client/pkg/v3 v3.5.0/go.mod h1:IJHfcCEKxYu1Os13ZdwCwIUTUVGYTSAM3YSwc9/Ac1g= go.etcd.io/etcd/client/v2 v2.305.0/go.mod h1:h9puh54ZTgAKtEbut2oe9P4L/oqKCVB6xsXlzd7alYQ= @@ -448,11 +456,8 @@ golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8U golang.org/x/crypto v0.0.0-20190820162420-60c769a6c586/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= -golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.0.0-20211215165025-cf75a172585e/go.mod h1:P+XmwS30IXTQdn5tA2iutPOUgjI07+tq3H3K9MVA1s8= golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= -golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU= -golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8= golang.org/x/crypto v0.31.0 h1:ihbySMvVjLAeSH1IbfcRTkD/iNscyz8rGzjF/E5hV6U= golang.org/x/crypto v0.31.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= @@ -490,8 +495,6 @@ golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.4.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.4.1/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= -golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= -golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20181023162649-9b4f9f5ad519/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= @@ -530,13 +533,8 @@ golang.org/x/net v0.0.0-20210316092652-d523dce5a7f4/go.mod h1:RBQZq4jEuRlivfhVLd golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM= golang.org/x/net v0.0.0-20210805182204-aaa1db679c0d/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= -golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= -golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= -golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg= -golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44= -golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM= -golang.org/x/net v0.27.0 h1:5K3Njcw06/l2y9vpGCSdcxWOYHOUk3dVNGDXN+FvAys= -golang.org/x/net v0.27.0/go.mod h1:dDi0PyhWNoiUOrAS8uXv/vnScO4wnHQO4mj9fn/RytE= +golang.org/x/net v0.33.0 h1:74SYHlV8BIgHIFC/LrYkOGIwL19eTYXQ5wc6TBuO36I= +golang.org/x/net v0.33.0/go.mod h1:HXLR5J+9DxmrqMwG9qjGCxZ+zKXxBru04zlTvWlWuN4= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= @@ -562,8 +560,6 @@ golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJ golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.10.0 h1:3NQrjDixjgGwUOCaF8w2+VYHv0Ve/vGYSbdkTa98gmQ= golang.org/x/sync v0.10.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sys v0.0.0-20180823144017-11551d06cbcc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= @@ -612,22 +608,11 @@ golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210809222454-d867a43fc93e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220310020820-b874c991c1a5/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= -golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.28.0 h1:Fksou7UEQUWlKvIdsqzJmUmCX3cZuD2+P3XyyzwMhlA= golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= -golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= -golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= -golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo= -golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk= -golang.org/x/term v0.20.0/go.mod h1:8UkIAJTvZgivsXaD6/pH6U9ecQzZ45awqEOzuCvwpFY= golang.org/x/term v0.27.0 h1:WP60Sv1nlK1T6SupCHbXzSaN0b9wUmsPoRS9b61A23Q= golang.org/x/term v0.27.0/go.mod h1:iMsnZpn0cago0GOrHO2+Y7u7JPn5AylBrcoWkElMTSM= golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= @@ -639,17 +624,13 @@ golang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.5/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= -golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= -golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= -golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= -golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= golang.org/x/text v0.21.0 h1:zyQAAkrwaneQ066sspRyJaG9VNi/YJ1NfzcGB3hZ/qo= golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ= golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= -golang.org/x/time v0.5.0 h1:o7cqy6amK/52YcAKIPlM3a+Fpj35zvRj2TP+e1xFSfk= -golang.org/x/time v0.5.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= +golang.org/x/time v0.6.0 h1:eTDhh4ZXt5Qf0augr54TN6suAUudPcawVZeIAPU7D4U= +golang.org/x/time v0.6.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= @@ -702,8 +683,6 @@ golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4f golang.org/x/tools v0.1.0/go.mod h1:xkSsbof2nBLbhDlRMhhhyNLN/zl3eTqcnHD5viDpcZ0= golang.org/x/tools v0.1.2/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= golang.org/x/tools v0.1.7/go.mod h1:LGqMHiF4EqQNHR1JncWGqT5BVaXmza+X+BDGol+dOxo= -golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= -golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= diff --git a/cli/packages/cmd/kmip.go b/cli/packages/cmd/kmip.go new file mode 100644 index 0000000000..b0c3978957 --- /dev/null +++ b/cli/packages/cmd/kmip.go @@ -0,0 +1,103 @@ +/* +Copyright (c) 2023 Infisical Inc. +*/ +package cmd + +import ( + "fmt" + + "github.com/Infisical/infisical-merge/packages/config" + "github.com/Infisical/infisical-merge/packages/util" + kmip "github.com/infisical/infisical-kmip" + "github.com/spf13/cobra" +) + +var kmipCmd = &cobra.Command{ + Example: `infisical kmip`, + Short: "Used to manage KMIP servers", + Use: "kmip", + DisableFlagsInUseLine: true, + Args: cobra.NoArgs, +} + +var kmipStartCmd = &cobra.Command{ + Example: `infisical kmip start`, + Short: "Used to start a KMIP server", + Use: "start", + DisableFlagsInUseLine: true, + Args: cobra.NoArgs, + Run: startKmipServer, +} + +func startKmipServer(cmd *cobra.Command, args []string) { + listenAddr, err := cmd.Flags().GetString("listen-address") + if err != nil { + util.HandleError(err, "Unable to parse flag") + } + + identityAuthMethod, err := cmd.Flags().GetString("identity-auth-method") + if err != nil { + util.HandleError(err, "Unable to parse flag") + } + + authMethodValid, strategy := util.IsAuthMethodValid(identityAuthMethod, false) + if !authMethodValid { + util.PrintErrorMessageAndExit(fmt.Sprintf("Invalid login method: %s", identityAuthMethod)) + } + + var identityClientId string + var identityClientSecret string + + if strategy == util.AuthStrategy.UNIVERSAL_AUTH { + identityClientId, err = util.GetCmdFlagOrEnv(cmd, "identity-client-id", util.INFISICAL_UNIVERSAL_AUTH_CLIENT_ID_NAME) + + if err != nil { + util.HandleError(err, "Unable to parse identity client ID") + } + + identityClientSecret, err = util.GetCmdFlagOrEnv(cmd, "identity-client-secret", util.INFISICAL_UNIVERSAL_AUTH_CLIENT_SECRET_NAME) + if err != nil { + util.HandleError(err, "Unable to parse identity client secret") + } + } else { + util.PrintErrorMessageAndExit(fmt.Sprintf("Unsupported login method: %s", identityAuthMethod)) + } + + serverName, err := cmd.Flags().GetString("server-name") + if err != nil { + util.HandleError(err, "Unable to parse flag") + } + + certificateTTL, err := cmd.Flags().GetString("certificate-ttl") + if err != nil { + util.HandleError(err, "Unable to parse flag") + } + + hostnamesOrIps, err := cmd.Flags().GetString("hostnames-or-ips") + if err != nil { + util.HandleError(err, "Unable to parse flag") + } + + kmip.StartServer(kmip.ServerConfig{ + Addr: listenAddr, + InfisicalBaseAPIURL: config.INFISICAL_URL, + IdentityClientId: identityClientId, + IdentityClientSecret: identityClientSecret, + ServerName: serverName, + CertificateTTL: certificateTTL, + HostnamesOrIps: hostnamesOrIps, + }) +} + +func init() { + kmipStartCmd.Flags().String("listen-address", "localhost:5696", "The address for the KMIP server to listen on. Defaults to localhost:5696") + kmipStartCmd.Flags().String("identity-auth-method", string(util.AuthStrategy.UNIVERSAL_AUTH), "The auth method to use for authenticating the machine identity. Defaults to universal-auth.") + kmipStartCmd.Flags().String("identity-client-id", "", "Universal auth client ID of machine identity") + kmipStartCmd.Flags().String("identity-client-secret", "", "Universal auth client secret of machine identity") + kmipStartCmd.Flags().String("server-name", "kmip-server", "The name of the KMIP server") + kmipStartCmd.Flags().String("certificate-ttl", "1y", "The TTL duration for the server certificate") + kmipStartCmd.Flags().String("hostnames-or-ips", "", "Comma-separated list of hostnames or IPs") + + kmipCmd.AddCommand(kmipStartCmd) + rootCmd.AddCommand(kmipCmd) +} diff --git a/docs/documentation/platform/kms/kmip.mdx b/docs/documentation/platform/kms/kmip.mdx new file mode 100644 index 0000000000..1e025d51ad --- /dev/null +++ b/docs/documentation/platform/kms/kmip.mdx @@ -0,0 +1,142 @@ +--- +title: "KMIP Integration" +description: "Learn more about integrating with Infisical KMS using KMIP (Key Management Interoperability Protocol)." +--- + + + KMIP integration is an Enterprise-only feature. Please reach out to + sales@infisical.com if you have any questions. + + +## Overview + +Infisical KMS provides **Key Management Interoperability Protocol (KMIP)** support, enabling seamless integration with KMIP-compatible clients. This allows for enhanced key management across various applications that support the **KMIP 1.4 protocol**. + +## Supported Operations + +The Infisical KMIP server supports the following operations for **symmetric keys**: + +- **Create** - Generate symmetric keys. +- **Register** - Register externally created keys. +- **Locate** - Find keys using attributes. +- **Get** - Retrieve keys securely. +- **Activate** - Enable keys for usage. +- **Revoke** - Revoke existing keys. +- **Destroy** - Permanently remove keys. +- **Get Attributes** - Retrieve metadata associated with keys. +- **Query** - Query server capabilities and supported operations. + +## Benefits of KMIP Integration + +Integrating Infisical KMS with KMIP-compatible clients provides the following benefits: + +- **Standardized Key Management**: Allows interoperability with security and cryptographic applications that support KMIP. +- **Enterprise-Grade Security**: Utilizes Infisical’s encryption mechanisms to securely store and manage keys. +- **Centralized Key Management**: Enables a unified approach for managing cryptographic keys across multiple environments. + +## Compatibility + +Infisical KMIP supports **KMIP versions 1.0 to 1.4**, ensuring compatibility with a wide range of clients and security tools. + +## Secure Communication & Authorization + +KMIP client-server communication is secured using **mutual TLS (mTLS)**, ensuring strong identity verification and encrypted data exchange via **PKI certificates**. Each KMIP entity must possess valid certificates signed by a trusted Root CA to establish trust. +For strong isolation, each Infisical organization has its own KMIP PKI (Public Key Infrastructure), ensuring that cryptographic operations and certificate authorities remain separate across organizations. + +Infisical KMS enforces a **two-layer authorization model** for KMIP operations: + +1. **KMIP Server Authorization** – The KMIP server, acting as a proxy, must have the `proxy KMIP` permission to forward client requests to Infisical KMS. This is done using a **machine identity** attached to the KMIP server. +2. **KMIP Client Authorization** – Clients must have the necessary KMIP-level permissions to perform specific key management operations. + +By combining **mTLS for secure communication** and **machine identity-based proxying**, Infisical KMS ensures **strong authentication, controlled access, and centralized key management** for KMIP operations. + +## Setup Instructions + +### Setup KMIP for your organization + + + + From there, press Setup KMIP. + ![KMIP org navigate](/images/platform/kms/kmip/kmip-org-setup-navigation.png) + + + In the modal, select the desired key algorithm to use for the KMIP PKI of your organization. Press continue. + ![KMIP org PKI setup](/images/platform/kms/kmip/kmip-org-setup-modal.png) + + This generates the KMIP PKI for your organization. After this, you can proceed to setting up your KMIP server. + + + + +### Deploying and Configuring the KMIP Server + +Follow these steps to configure and deploy a KMIP server. + + + + Configure a [machine identity](https://infisical.com/docs/documentation/platform/identities/machine-identities#machine-identities) for the KMIP server to use. + ![KMIP create machine identity](/images/platform/kms/kmip/kmip-create-mi.png) + + Create a custom organization role and give it the **Proxy KMIP** permission. + ![KMIP create custom role](/images/platform/kms/kmip/kmip-create-custom-role.png) + ![KMIP assign proxy to role](/images/platform/kms/kmip/kmip-assign-custom-role-proxy.png) + + Assign the machine identity to the custom organization role. This allows the machine identity to serve KMIP client requests and forward them from your KMIP server to Infisical. + ![KMIP assign role to machine identity](/images/platform/kms/kmip/kmip-assign-mi-to-role.png) + + + + + To deploy the KMIP server, use the Infisical CLI’s `kmip start` command. + Before proceeding, make sure you have the [Infisical CLI installed](https://infisical.com/docs/cli/overview). + + Once installed, launch the KMIP server with the following command: + + ```bash + infisical kmip start \ + --identity-client-id= \ # This can be set by defining the INFISICAL_UNIVERSAL_AUTH_CLIENT_ID ENV variable + --identity-client-secret= \ # This can be set by defining the INFISICAL_UNIVERSAL_AUTH_CLIENT_SECRET ENV variable + --domain=https://app.infisical.com \ + --hostnames-or-ips="my-kmip-server.com" + ``` + + The following flags are available for the `infisical kmip start` command:: + - **listen-address** (default: localhost:5696): The address the KMIP server listens on. + - **identity-auth-method** (default: universal-auth): The authentication method for the machine identity. + - **identity-client-id**: The client ID of the machine identity. This can be set by defining the `INFISICAL_UNIVERSAL_AUTH_CLIENT_ID` ENV variable. + - **identity-client-secret**: The client secret of the machine identity. This can be set by defining the `INFISICAL_UNIVERSAL_AUTH_CLIENT_SECRET` ENV variable. + - **server-name** (default: "kmip-server"): The name of the KMIP server. + - **certificate-ttl** (default: "1y"): The duration for which the server certificate is valid. + - **hostnames-or-ips:** A comma-separated list of hostnames or IPs the KMIP server will use (required). + + + + +### Add and Configure KMIP Clients + + + + From there, press Add KMIP Client + ![KMIP client overview](/images/platform/kms/kmip/kmip-client-overview.png) + + + In the modal, provide the details of your client. The selected permissions determine what KMIP operations can be performed in your KMS project. + ![KMIP client modal](/images/platform/kms/kmip/kmip-client-modal.png) + + + Once the KMIP client is created, you will have to generate a client certificate. + Press Generate Certificate. + ![KMIP generate client cert](/images/platform/kms/kmip/kmip-client-generate-cert.png) + + Provide the desired TTL and key algorithm to use and press Generate Client Certificate. + ![KMIP client cert config](/images/platform/kms/kmip/kmip-client-cert-config-modal.png) + + Configure your KMIP clients to use the generated client certificate, certificate chain and private key. + ![KMIP client cert modal](/images/platform/kms/kmip/kmip-client-certificate-modal.png) + + + + +## Additional Resources + +- [KMIP 1.4 Specification](http://docs.oasis-open.org/kmip/spec/v1.4/os/kmip-spec-v1.4-os.html) diff --git a/docs/images/platform/kms/kmip/kmip-assign-custom-role-proxy.png b/docs/images/platform/kms/kmip/kmip-assign-custom-role-proxy.png new file mode 100644 index 0000000000..498fc20128 Binary files /dev/null and b/docs/images/platform/kms/kmip/kmip-assign-custom-role-proxy.png differ diff --git a/docs/images/platform/kms/kmip/kmip-assign-mi-to-role.png b/docs/images/platform/kms/kmip/kmip-assign-mi-to-role.png new file mode 100644 index 0000000000..93d4c7f84f Binary files /dev/null and b/docs/images/platform/kms/kmip/kmip-assign-mi-to-role.png differ diff --git a/docs/images/platform/kms/kmip/kmip-client-cert-config-modal.png b/docs/images/platform/kms/kmip/kmip-client-cert-config-modal.png new file mode 100644 index 0000000000..45eb8c830b Binary files /dev/null and b/docs/images/platform/kms/kmip/kmip-client-cert-config-modal.png differ diff --git a/docs/images/platform/kms/kmip/kmip-client-certificate-modal.png b/docs/images/platform/kms/kmip/kmip-client-certificate-modal.png new file mode 100644 index 0000000000..d6f9fc2ad8 Binary files /dev/null and b/docs/images/platform/kms/kmip/kmip-client-certificate-modal.png differ diff --git a/docs/images/platform/kms/kmip/kmip-client-generate-cert.png b/docs/images/platform/kms/kmip/kmip-client-generate-cert.png new file mode 100644 index 0000000000..40b49f5707 Binary files /dev/null and b/docs/images/platform/kms/kmip/kmip-client-generate-cert.png differ diff --git a/docs/images/platform/kms/kmip/kmip-client-modal.png b/docs/images/platform/kms/kmip/kmip-client-modal.png new file mode 100644 index 0000000000..5038c2a877 Binary files /dev/null and b/docs/images/platform/kms/kmip/kmip-client-modal.png differ diff --git a/docs/images/platform/kms/kmip/kmip-client-overview.png b/docs/images/platform/kms/kmip/kmip-client-overview.png new file mode 100644 index 0000000000..794a91d429 Binary files /dev/null and b/docs/images/platform/kms/kmip/kmip-client-overview.png differ diff --git a/docs/images/platform/kms/kmip/kmip-create-custom-role.png b/docs/images/platform/kms/kmip/kmip-create-custom-role.png new file mode 100644 index 0000000000..5e6a5145dd Binary files /dev/null and b/docs/images/platform/kms/kmip/kmip-create-custom-role.png differ diff --git a/docs/images/platform/kms/kmip/kmip-create-mi.png b/docs/images/platform/kms/kmip/kmip-create-mi.png new file mode 100644 index 0000000000..ed8a0988c9 Binary files /dev/null and b/docs/images/platform/kms/kmip/kmip-create-mi.png differ diff --git a/docs/images/platform/kms/kmip/kmip-org-setup-modal.png b/docs/images/platform/kms/kmip/kmip-org-setup-modal.png new file mode 100644 index 0000000000..6fc6dd7ce8 Binary files /dev/null and b/docs/images/platform/kms/kmip/kmip-org-setup-modal.png differ diff --git a/docs/images/platform/kms/kmip/kmip-org-setup-navigation.png b/docs/images/platform/kms/kmip/kmip-org-setup-navigation.png new file mode 100644 index 0000000000..5a77878e5a Binary files /dev/null and b/docs/images/platform/kms/kmip/kmip-org-setup-navigation.png differ diff --git a/docs/mint.json b/docs/mint.json index cd1a9120f8..62e40e1c98 100644 --- a/docs/mint.json +++ b/docs/mint.json @@ -124,7 +124,8 @@ "pages": [ "documentation/platform/kms/overview", "documentation/platform/kms/hsm-integration", - "documentation/platform/kms/kubernetes-encryption" + "documentation/platform/kms/kubernetes-encryption", + "documentation/platform/kms/kmip" ] }, { diff --git a/frontend/public/lotties/key-user.json b/frontend/public/lotties/key-user.json new file mode 100644 index 0000000000..268b10a6f6 --- /dev/null +++ b/frontend/public/lotties/key-user.json @@ -0,0 +1 @@ +{"v":"5.7.5","fr":100,"ip":0,"op":254,"w":512,"h":512,"nm":"Comp 1","ddd":0,"metadata":{},"assets":[],"layers":[{"ddd":0,"ind":12345679,"ty":4,"nm":"Group Layer 8","sr":1,"ks":{"p":{"a":0,"k":[373.76,475.53049180327866,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[52.459016393442624,52.459016393442624,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ty":"gr","it":[{"ty":"gr","it":[{"ty":"sh","d":1,"ks":{"a":0,"k":{"c":true,"v":[[220.741,37.184],[225.501,35.896],[228.749,32.36800000000001],[229.981,27.216000000000008],[228.749,22.12],[225.501,18.592],[220.741,17.304],[215.981,18.592],[212.677,22.12],[211.501,27.216000000000008],[212.677,32.36800000000001],[215.981,35.896],[220.741,37.184],[220.741,37.184],[220.741,37.184]],"i":[[0,0],[-1.380999999999972,0.8586999999999989],[-0.7839999999999918,1.493299999999991],[0,1.903999999999996],[0.8220000000000027,1.493299999999991],[1.382000000000062,0.8586999999999989],[1.79200000000003,0],[1.418999999999983,-0.8586999999999989],[0.8220000000000027,-1.493300000000005],[0,-1.904000000000011],[-0.7839999999999918,-1.5307000000000102],[-1.380999999999972,-0.8586999999999989],[-1.754000000000019,0],[0,0],[0,0]],"o":[[1.79200000000003,0],[1.382000000000062,-0.8586999999999989],[0.8220000000000027,-1.5307000000000102],[0,-1.904000000000011],[-0.7839999999999918,-1.493300000000005],[-1.380999999999972,-0.8586999999999989],[-1.754000000000019,0],[-1.380999999999972,0.8586999999999989],[-0.7839999999999918,1.493299999999991],[0,1.903999999999996],[0.8220000000000027,1.493299999999991],[1.418999999999983,0.8586999999999989],[0,0],[0,0],[0,0]]}}},{"ty":"tm","s":{"a":0,"k":0,"ix":2},"e":{"a":0,"k":100,"ix":2},"o":{"a":0,"k":0,"ix":2},"m":1},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}}]},{"ty":"gr","it":[{"ty":"sh","d":1,"ks":{"a":0,"k":{"c":true,"v":[[221.357,43.06400000000001],[214.917,41.608],[210.49300000000005,37.408],[211.221,36.232],[211.221,42.392],[205.173,42.392],[205.173,0],[211.501,0],[211.501,18.36800000000001],[210.49300000000005,16.912000000000006],[214.973,12.88],[221.357,11.424000000000007],[229.085,13.49600000000001],[234.51700000000005,19.152],[236.533,27.216000000000008],[234.51700000000005,35.28],[229.141,40.992],[221.357,43.06400000000001],[221.357,43.06400000000001],[221.357,43.06400000000001]],"i":[[0,0],[1.942000000000007,0.9706999999999937],[1.045999999999935,1.829300000000003],[-0.2426666666666506,0.3919999999999959],[0,-2.053333333333327],[2.015999999999963,0],[0,14.13066666666667],[-2.109333333333325,0],[0,-6.122666666666674],[0.3360000000000127,0.4853333333333296],[-1.865999999999985,0.9707000000000079],[-2.38900000000001,0],[-2.277000000000044,-1.3813000000000102],[-1.30600000000004,-2.389300000000006],[0,-2.986699999999999],[1.343999999999937,-2.389300000000006],[2.27800000000002,-1.4187000000000012],[2.912000000000035,0],[0,0],[0,0]],"o":[[-2.351999999999975,0],[-1.903999999999996,-0.9707000000000079],[0.2426666666666506,-0.3919999999999959],[0,2.053333333333327],[-2.015999999999963,0],[0,-14.13066666666667],[2.109333333333325,0],[0,6.122666666666667],[-0.3360000000000127,-0.4853333333333296],[1.120000000000005,-1.717300000000009],[1.867000000000075,-0.9706999999999937],[2.875,0],[2.314999999999941,1.381299999999996],[1.343999999999937,2.389300000000006],[0,2.986699999999999],[-1.30600000000004,2.389300000000006],[-2.27699999999993,1.381299999999996],[0,0],[0,0],[0,0]]}}},{"ty":"tm","s":{"a":0,"k":0,"ix":2},"e":{"a":0,"k":100,"ix":2},"o":{"a":0,"k":0,"ix":2},"m":1},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}}]},{"ty":"gr","it":[{"ty":"sh","d":1,"ks":{"a":0,"k":{"c":true,"v":[[181.87,43.06400000000001],[176.438,42],[172.854,38.976],[171.566,34.384],[172.63,29.960000000000008],[176.046,26.656000000000006],[181.814,24.752],[192.342,23.016000000000005],[192.342,28],[183.046,29.624],[179.35,31.248],[178.174,34.16],[179.462,37.016000000000005],[182.878,38.08],[187.35799999999995,36.96000000000001],[190.38199999999995,33.992],[191.446,29.792],[191.446,22.008],[189.766,18.36800000000001],[185.398,16.912000000000006],[180.974,18.256],[178.23,21.616],[172.966,18.98400000000001],[175.71,15.064000000000007],[180.134,12.376],[185.566,11.424000000000007],[191.894,12.768],[196.206,16.52],[197.774,22.008],[197.774,42.392],[191.726,42.392],[191.726,36.904],[193.014,37.072],[190.27,40.264],[186.518,42.336],[181.87,43.06400000000001],[181.87,43.06400000000001],[181.87,43.06400000000001]],"i":[[0,0],[1.567999999999984,0.7092999999999989],[0.8589999999999236,1.2693000000000012],[0,1.7547],[-0.7089999999999463,1.306699999999992],[-1.530000000000086,0.8960000000000008],[-2.313999999999965,0.3733000000000004],[-3.509333333333302,0.5786666666666633],[0,-1.661333333333332],[3.098666666666645,-0.541333333333327],[0.7839999999999918,-0.784000000000006],[0,-1.194699999999997],[-0.8579999999999472,-0.7467000000000041],[-1.381000000000085,0],[-1.268999999999892,0.7466999999999899],[-0.7089999999999463,1.2319999999999993],[0,1.530699999999996],[0,2.594666666666669],[1.120000000000005,0.9332999999999885],[1.829999999999927,0],[1.269999999999982,-0.8960000000000008],[0.5979999999999563,-1.381299999999996],[1.754666666666708,0.8773333333333255],[-1.269000000000005,1.11999999999999],[-1.680000000000064,0.6346999999999952],[-1.903999999999996,0],[-1.828999999999951,-0.8960000000000008],[-1.008000000000038,-1.6053],[0,-2.090699999999998],[0,-6.794666666666672],[2.015999999999963,0],[0,1.829333333333338],[-0.4293333333333749,-0.05599999999999739],[1.120000000000005,-0.8959999999999866],[1.418999999999983,-0.4852999999999952],[1.717999999999961,0],[0,0],[0,0]],"o":[[-2.052999999999997,0],[-1.529999999999973,-0.7467000000000041],[-0.8580000000000609,-1.306699999999992],[0,-1.642700000000005],[0.7469999999999573,-1.306700000000006],[1.530999999999949,-0.8960000000000008],[3.509333333333302,-0.5786666666666633],[0,1.661333333333332],[-3.098666666666645,0.541333333333327],[-1.680000000000064,0.2987000000000108],[-0.7839999999999918,0.7467000000000041],[0,1.157300000000006],[0.8959999999999582,0.7092999999999989],[1.717999999999961,0],[1.307000000000016,-0.7467000000000041],[0.7100000000000364,-1.2693000000000012],[0,-2.594666666666669],[0,-1.493299999999991],[-1.081999999999994,-0.9707000000000079],[-1.680000000000064,0],[-1.232000000000085,0.8586999999999989],[-1.754666666666708,-0.8773333333333255],[0.5599999999999454,-1.493300000000005],[1.269999999999982,-1.157300000000006],[1.717999999999961,-0.6347000000000094],[2.389999999999986,0],[1.866999999999962,0.8960000000000008],[1.045999999999935,1.568000000000012],[0,6.794666666666672],[-2.015999999999963,0],[0,-1.829333333333338],[0.4293333333333749,0.05599999999999739],[-0.70900000000006,1.2319999999999993],[-1.081999999999994,0.8960000000000008],[-1.380999999999972,0.4853000000000094],[0,0],[0,0],[0,0]]}}},{"ty":"tm","s":{"a":0,"k":0,"ix":2},"e":{"a":0,"k":100,"ix":2},"o":{"a":0,"k":0,"ix":2},"m":1},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}}]},{"ty":"gr","it":[{"ty":"sh","d":1,"ks":{"a":0,"k":{"c":true,"v":[[159.072,42.392],[159.072,0],[165.4,0],[165.4,42.392],[159.072,42.392],[159.072,42.392],[159.072,42.392]],"i":[[0,0],[0,14.13066666666667],[-2.109333333333325,0],[0,-14.13066666666667],[2.109333333333325,0],[0,0],[0,0]],"o":[[0,-14.13066666666667],[2.109333333333325,0],[0,14.13066666666667],[-2.109333333333325,0],[0,0],[0,0],[0,0]]}}},{"ty":"tm","s":{"a":0,"k":0,"ix":2},"e":{"a":0,"k":100,"ix":2},"o":{"a":0,"k":0,"ix":2},"m":1},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}}]},{"ty":"gr","it":[{"ty":"sh","d":1,"ks":{"a":0,"k":{"c":true,"v":[[138.952,43.06400000000001],[130.888,40.992],[125.456,35.28],[123.496,27.16],[125.456,19.040000000000006],[130.832,13.49600000000001],[138.448,11.424000000000007],[144.552,12.600000000000009],[149.088,15.848],[151.888,20.49600000000001],[152.896,26.096],[152.84,27.608],[152.616,29.064000000000007],[128.48,29.064000000000007],[128.48,24.024],[149.032,24.024],[146.008,26.320000000000007],[145.616,21.448000000000008],[142.816,18.032],[138.448,16.744],[133.968,18.032],[130.944,21.616],[130.104,27.216000000000008],[130.944,32.592],[134.192,36.176],[139.008,37.464],[143.65599999999995,36.232],[146.736,33.040000000000006],[151.888,35.56],[149.088,39.42400000000001],[144.60799999999995,42.11200000000001],[138.952,43.06400000000001],[138.952,43.06400000000001],[138.952,43.06400000000001]],"i":[[0,0],[2.351999999999975,1.381299999999996],[1.307000000000016,2.389300000000006],[0,2.986699999999999],[-1.307000000000016,2.35199999999999],[-2.240000000000009,1.343999999999994],[-2.836999999999989,0],[-1.79200000000003,-0.784000000000006],[-1.231999999999971,-1.381299999999996],[-0.6349999999999909,-1.754700000000014],[0,-1.978700000000003],[0.03699999999992087,-0.5227000000000004],[0.1119999999999663,-0.4480000000000075],[8.04533333333336,0],[0,1.680000000000007],[-6.850666666666712,0],[1.008000000000038,-0.7653333333333308],[0.6349999999999909,1.4187000000000012],[1.269000000000005,0.8213000000000079],[1.680000000000064,0],[1.307000000000016,-0.8586999999999989],[0.70900000000006,-1.567999999999998],[-0.1490000000000009,-2.202700000000007],[-0.7469999999999573,-1.530699999999996],[-1.380999999999972,-0.8586999999999989],[-1.79200000000003,0],[-1.268999999999892,0.8213000000000079],[-0.7469999999999573,1.306699999999992],[-1.717333333333386,-0.8400000000000034],[1.269000000000005,-1.157300000000006],[1.755000000000109,-0.6720000000000113],[2.052999999999997,0],[0,0],[0,0]],"o":[[-3.024000000000001,0],[-2.315000000000055,-1.4187000000000012],[-1.307000000000016,-2.426699999999997],[0,-3.061299999999989],[1.343999999999937,-2.352000000000004],[2.240000000000009,-1.3813000000000102],[2.277000000000044,0],[1.79200000000003,0.7839999999999918],[1.232000000000085,1.344000000000008],[0.6720000000000255,1.7547],[0,0.4852999999999952],[-0.03700000000003456,0.5227000000000004],[-8.04533333333336,0],[0,-1.680000000000007],[6.850666666666712,0],[-1.008000000000038,0.7653333333333308],[0.3729999999999336,-1.829300000000003],[-0.59699999999998,-1.456000000000003],[-1.232000000000085,-0.8586999999999989],[-1.67999999999995,0],[-1.306999999999903,0.8213000000000079],[-0.7089999999999463,1.530699999999996],[-0.1870000000000118,2.053299999999993],[0.7839999999999918,1.53070000000001],[1.418999999999983,0.8586999999999989],[1.828999999999951,0],[1.307000000000016,-0.8212999999999937],[1.717333333333386,0.8400000000000034],[-0.59699999999998,1.4187000000000012],[-1.231999999999971,1.11999999999999],[-1.716999999999985,0.6346999999999952],[0,0],[0,0],[0,0]]}}},{"ty":"tm","s":{"a":0,"k":0,"ix":2},"e":{"a":0,"k":100,"ix":2},"o":{"a":0,"k":0,"ix":2},"m":1},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}}]},{"ty":"gr","it":[{"ty":"sh","d":1,"ks":{"a":0,"k":{"c":true,"v":[[111.001,7.951999999999998],[111.001,0.6720000000000041],[117.329,0.6720000000000041],[117.329,7.951999999999998],[111.001,7.951999999999998],[111.001,7.951999999999998],[111.001,7.951999999999998]],"i":[[0,0],[0,2.426666666666662],[-2.109333333333325,0],[0,-2.426666666666662],[2.109333333333325,0],[0,0],[0,0]],"o":[[0,-2.426666666666662],[2.109333333333325,0],[0,2.426666666666662],[-2.109333333333325,0],[0,0],[0,0],[0,0]]}}},{"ty":"tm","s":{"a":0,"k":0,"ix":2},"e":{"a":0,"k":100,"ix":2},"o":{"a":0,"k":0,"ix":2},"m":1},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}}]},{"ty":"gr","it":[{"ty":"sh","d":1,"ks":{"a":0,"k":{"c":true,"v":[[111.001,42.392],[111.001,12.096],[117.329,12.096],[117.329,42.392],[111.001,42.392],[111.001,42.392],[111.001,42.392]],"i":[[0,0],[0,10.09866666666667],[-2.109333333333325,0],[0,-10.09866666666667],[2.109333333333325,0],[0,0],[0,0]],"o":[[0,-10.09866666666666],[2.109333333333325,0],[0,10.09866666666666],[-2.109333333333325,0],[0,0],[0,0],[0,0]]}}},{"ty":"tm","s":{"a":0,"k":0,"ix":2},"e":{"a":0,"k":100,"ix":2},"o":{"a":0,"k":0,"ix":2},"m":1},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}}]},{"ty":"gr","it":[{"ty":"sh","d":1,"ks":{"a":0,"k":{"c":true,"v":[[101.41,42.72800000000001],[94.01800000000003,40.040000000000006],[91.38599999999997,32.48],[91.38599999999997,17.808000000000007],[86.06600000000003,17.808000000000007],[86.06600000000003,12.096],[86.90599999999995,12.096],[90.21000000000004,10.864],[91.38599999999997,7.504000000000005],[91.38599999999997,5.152000000000001],[97.71400000000006,5.152000000000001],[97.71400000000006,12.096],[104.602,12.096],[104.602,17.808000000000007],[97.71400000000006,17.808000000000007],[97.71400000000006,32.2],[98.21799999999996,34.888000000000005],[99.84199999999998,36.568],[102.754,37.128],[103.76200000000006,37.072],[104.826,36.96000000000001],[104.826,42.392],[103.09,42.616],[101.41,42.72800000000001],[101.41,42.72800000000001],[101.41,42.72800000000001]],"i":[[0,0],[1.754999999999995,1.792000000000002],[0,3.248000000000005],[0,4.890666666666661],[1.773333333333312,0],[0,1.903999999999996],[-0.2799999999999727,0],[-0.7839999999999918,0.8212999999999937],[0,1.4187000000000012],[0,0.7839999999999989],[-2.109333333333325,0],[0,-2.314666666666668],[-2.295999999999935,0],[0,-1.903999999999996],[2.295999999999935,0],[0,-4.797333333333327],[-0.3360000000000127,-0.7467000000000041],[-0.7469999999999573,-0.4106999999999914],[-1.19500000000005,0],[-0.3730000000000473,0.03730000000000189],[-0.3360000000000127,0.03729999999998768],[0,-1.810666666666663],[0.6349999999999909,-0.07469999999999288],[0.4850000000000136,0],[0,0],[0,0]],"o":[[-3.173000000000002,0],[-1.754999999999995,-1.792000000000002],[0,-4.890666666666661],[-1.773333333333312,0],[0,-1.903999999999996],[0.2799999999999727,0],[1.419000000000096,0],[0.7839999999999918,-0.8213000000000079],[0,-0.7839999999999989],[2.109333333333325,0],[0,2.314666666666668],[2.295999999999935,0],[0,1.903999999999996],[-2.295999999999935,0],[0,4.797333333333327],[0,1.045299999999997],[0.3360000000000127,0.7092999999999989],[0.7470000000000709,0.3733000000000004],[0.2989999999999782,0],[0.3729999999999336,-0.03730000000000189],[0,1.810666666666663],[-0.5230000000000246,0.0747000000000071],[-0.6349999999999909,0.0747000000000071],[0,0],[0,0],[0,0]]}}},{"ty":"tm","s":{"a":0,"k":0,"ix":2},"e":{"a":0,"k":100,"ix":2},"o":{"a":0,"k":0,"ix":2},"m":1},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}}]},{"ty":"gr","it":[{"ty":"sh","d":1,"ks":{"a":0,"k":{"c":true,"v":[[78.71499999999997,42.72800000000001],[71.32299999999998,40.040000000000006],[68.69100000000003,32.48],[68.69100000000003,17.808000000000007],[63.37099999999998,17.808000000000007],[63.37099999999998,12.096],[64.21100000000001,12.096],[67.51499999999999,10.864],[68.69100000000003,7.504000000000005],[68.69100000000003,5.152000000000001],[75.019,5.152000000000001],[75.019,12.096],[81.90700000000004,12.096],[81.90700000000004,17.808000000000007],[75.019,17.808000000000007],[75.019,32.2],[75.52300000000002,34.888000000000005],[77.14699999999999,36.568],[80.05900000000003,37.128],[81.06700000000001,37.072],[82.13099999999997,36.96000000000001],[82.13099999999997,42.392],[80.39499999999998,42.616],[78.71499999999997,42.72800000000001],[78.71499999999997,42.72800000000001],[78.71499999999997,42.72800000000001]],"i":[[0,0],[1.754000000000019,1.792000000000002],[0,3.248000000000005],[0,4.890666666666661],[1.773333333333369,0],[0,1.903999999999996],[-0.2800000000000296,0],[-0.7839999999999918,0.8212999999999937],[0,1.4187000000000012],[0,0.7839999999999989],[-2.109333333333325,0],[0,-2.314666666666668],[-2.295999999999992,0],[0,-1.903999999999996],[2.295999999999992,0],[0,-4.797333333333327],[-0.3360000000000127,-0.7467000000000041],[-0.7470000000000141,-0.4106999999999914],[-1.19500000000005,0],[-0.3740000000000236,0.03730000000000189],[-0.3360000000000127,0.03729999999998768],[0,-1.810666666666663],[0.6340000000000146,-0.07469999999999288],[0.4850000000000136,0],[0,0],[0,0]],"o":[[-3.173999999999978,0],[-1.754999999999995,-1.792000000000002],[0,-4.890666666666661],[-1.773333333333369,0],[0,-1.903999999999996],[0.2800000000000296,0],[1.418000000000006,0],[0.7839999999999918,-0.8213000000000079],[0,-0.7839999999999989],[2.109333333333325,0],[0,2.314666666666668],[2.295999999999992,0],[0,1.903999999999996],[-2.295999999999992,0],[0,4.797333333333327],[0,1.045299999999997],[0.3359999999999559,0.7092999999999989],[0.7460000000000377,0.3733000000000004],[0.297999999999945,0],[0.3730000000000473,-0.03730000000000189],[0,1.810666666666663],[-0.5230000000000246,0.0747000000000071],[-0.6349999999999909,0.0747000000000071],[0,0],[0,0],[0,0]]}}},{"ty":"tm","s":{"a":0,"k":0,"ix":2},"e":{"a":0,"k":100,"ix":2},"o":{"a":0,"k":0,"ix":2},"m":1},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}}]},{"ty":"gr","it":[{"ty":"sh","d":1,"ks":{"a":0,"k":{"c":true,"v":[[44.18799999999999,37.184],[48.94799999999998,35.896],[52.19600000000003,32.36800000000001],[53.428,27.216000000000008],[52.19600000000003,22.12],[48.94799999999998,18.592],[44.18799999999999,17.304],[39.428,18.592],[36.12400000000002,22.12],[34.94799999999998,27.216000000000008],[36.12400000000002,32.36800000000001],[39.428,35.896],[44.18799999999999,37.184],[44.18799999999999,37.184],[44.18799999999999,37.184]],"i":[[0,0],[-1.381999999999948,0.8586999999999989],[-0.7840000000000487,1.493299999999991],[0,1.903999999999996],[0.8209999999999695,1.493299999999991],[1.381000000000029,0.8586999999999989],[1.79200000000003,0],[1.418000000000006,-0.8586999999999989],[0.8209999999999695,-1.493300000000005],[0,-1.904000000000011],[-0.7840000000000487,-1.5307000000000102],[-1.382000000000005,-0.8586999999999989],[-1.754999999999995,0],[0,0],[0,0]],"o":[[1.79200000000003,0],[1.381000000000029,-0.8586999999999989],[0.8209999999999695,-1.5307000000000102],[0,-1.904000000000011],[-0.7840000000000487,-1.493300000000005],[-1.381999999999948,-0.8586999999999989],[-1.754999999999995,0],[-1.382000000000005,0.8586999999999989],[-0.7840000000000487,1.493299999999991],[0,1.903999999999996],[0.8209999999999695,1.493299999999991],[1.418000000000006,0.8586999999999989],[0,0],[0,0],[0,0]]}}},{"ty":"tm","s":{"a":0,"k":0,"ix":2},"e":{"a":0,"k":100,"ix":2},"o":{"a":0,"k":0,"ix":2},"m":1},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}}]},{"ty":"gr","it":[{"ty":"sh","d":1,"ks":{"a":0,"k":{"c":true,"v":[[44.18799999999999,43.06400000000001],[36.18000000000001,40.992],[30.46800000000002,35.336],[28.33999999999997,27.216000000000008],[30.46800000000002,19.096],[36.18000000000001,13.49600000000001],[44.18799999999999,11.424000000000007],[52.19600000000003,13.49600000000001],[57.85199999999998,19.096],[59.98000000000002,27.216000000000008],[57.85199999999998,35.392],[52.139999999999986,41.048],[44.18799999999999,43.06400000000001],[44.18799999999999,43.06400000000001],[44.18799999999999,43.06400000000001]],"i":[[0,0],[2.425999999999988,1.381299999999996],[1.418000000000006,2.389300000000006],[0,3.024000000000001],[-1.41900000000004,2.352000000000004],[-2.389999999999986,1.343999999999994],[-2.949999999999989,0],[-2.352000000000032,-1.3813000000000102],[-1.381999999999948,-2.389300000000006],[0,-3.061300000000003],[1.418000000000006,-2.389299999999992],[2.38900000000001,-1.381299999999996],[2.912000000000035,0],[0,0],[0,0]],"o":[[-2.911999999999978,0],[-2.389999999999986,-1.381299999999996],[-1.41900000000004,-2.389299999999992],[0,-3.061300000000003],[1.418000000000006,-2.389300000000006],[2.38900000000001,-1.3813000000000102],[2.98599999999999,0],[2.388999999999953,1.343999999999994],[1.418000000000006,2.352000000000004],[0,3.061299999999989],[-1.418999999999983,2.389300000000006],[-2.389999999999986,1.343999999999994],[0,0],[0,0],[0,0]]}}},{"ty":"tm","s":{"a":0,"k":0,"ix":2},"e":{"a":0,"k":100,"ix":2},"o":{"a":0,"k":0,"ix":2},"m":1},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}}]},{"ty":"gr","it":[{"ty":"sh","d":1,"ks":{"a":0,"k":{"c":true,"v":[[0,42.392],[0,0.6720000000000041],[6.608000000000004,0.6720000000000041],[6.608000000000004,36.512],[24.639999999999986,36.512],[24.639999999999986,42.392],[0,42.392],[0,42.392],[0,42.392]],"i":[[0,0],[0,13.90666666666666],[-2.202666666666687,0],[0,-11.94666666666666],[-6.01066666666668,0],[0,-1.959999999999994],[8.21333333333331,0],[0,0],[0,0]],"o":[[0,-13.90666666666667],[2.202666666666687,0],[0,11.94666666666667],[6.01066666666668,0],[0,1.959999999999994],[-8.21333333333331,0],[0,0],[0,0],[0,0]]}}},{"ty":"tm","s":{"a":0,"k":0,"ix":2},"e":{"a":0,"k":100,"ix":2},"o":{"a":0,"k":0,"ix":2},"m":1},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}}]},{"ty":"fl","c":{"a":0,"k":[1,1,1],"ix":2},"o":{"a":0,"k":100,"ix":2},"r":1,"bm":0},{"ty":"tr","p":{"a":0,"k":[98.08047485351562,-21.67217254638672],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[99.99999403953552,99.99999403953552],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}}]},{"ty":"gr","it":[{"ty":"gr","it":[{"ty":"sh","d":1,"ks":{"a":0,"k":{"c":true,"v":[[246.681,42.392],[246.681,0],[253.009,0],[253.009,18.032],[252.001,17.248],[255.585,12.936000000000007],[261.297,11.424000000000007],[267.23299999999995,12.88],[271.265,16.912000000000006],[272.721,22.792],[272.721,42.392],[266.449,42.392],[266.449,24.528000000000006],[265.553,20.664],[263.201,18.2],[259.729,17.304],[256.25699999999995,18.2],[253.849,20.664],[253.009,24.528000000000006],[253.009,42.392],[246.681,42.392],[246.681,42.392],[246.681,42.392]],"i":[[0,0],[0,14.13066666666667],[-2.109333333333325,0],[0,-6.010666666666665],[0.3360000000000127,0.2613333333333259],[-1.643000000000029,0.9706999999999937],[-2.166000000000054,0],[-1.717999999999961,-0.9706999999999937],[-0.9710000000000036,-1.717300000000009],[0,-2.202699999999993],[0,-6.533333333333331],[2.090666666666721,0],[0,5.954666666666668],[0.59699999999998,1.045299999999997],[1.007999999999925,0.5600000000000023],[1.305999999999926,0],[1.045000000000073,-0.5973000000000042],[0.59699999999998,-1.082700000000003],[0,-1.493300000000005],[0,-5.954666666666668],[2.109333333333325,0],[0,0],[0,0]],"o":[[0,-14.13066666666667],[2.109333333333325,0],[0,6.010666666666665],[-0.3360000000000127,-0.2613333333333259],[0.7459999999999809,-1.903999999999996],[1.641999999999967,-1.0080000000000098],[2.240000000000009,0],[1.717000000000098,0.9707000000000079],[0.9700000000000273,1.717299999999994],[0,6.533333333333331],[-2.090666666666721,0],[0,-5.954666666666668],[0,-1.5307000000000102],[-0.5599999999999454,-1.082700000000003],[-1.008000000000038,-0.5973000000000042],[-1.270000000000095,0],[-1.007999999999953,0.5600000000000023],[-0.5600000000000023,1.082700000000003],[0,5.954666666666668],[-2.109333333333325,0],[0,0],[0,0],[0,0]]}}},{"ty":"tm","s":{"a":0,"k":0,"ix":2},"e":{"a":0,"k":100,"ix":2},"o":{"a":0,"k":0,"ix":2},"m":1},{"ty":"tr","p":{"a":0,"k":[-354.5325317382812,-77.50520324707031],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}}]},{"ty":"gr","it":[{"ty":"sh","d":1,"ks":{"a":0,"k":{"c":true,"v":[[237.089,42.72800000000001],[229.697,40.040000000000006],[227.065,32.48],[227.065,17.808000000000007],[221.745,17.808000000000007],[221.745,12.096],[222.585,12.096],[225.889,10.864],[227.065,7.504000000000005],[227.065,5.152000000000001],[233.393,5.152000000000001],[233.393,12.096],[240.281,12.096],[240.281,17.808000000000007],[233.393,17.808000000000007],[233.393,32.2],[233.897,34.888000000000005],[235.521,36.568],[238.433,37.128],[239.441,37.072],[240.505,36.96000000000001],[240.505,42.392],[238.769,42.616],[237.089,42.72800000000001],[237.089,42.72800000000001],[237.089,42.72800000000001]],"i":[[0,0],[1.755000000000052,1.792000000000002],[0,3.248000000000005],[0,4.890666666666661],[1.773333333333369,0],[0,1.903999999999996],[-0.2800000000000296,0],[-0.7839999999999918,0.8212999999999937],[0,1.4187000000000012],[0,0.7839999999999989],[-2.109333333333325,0],[0,-2.314666666666668],[-2.295999999999992,0],[0,-1.903999999999996],[2.295999999999992,0],[0,-4.797333333333327],[-0.3360000000000127,-0.7467000000000041],[-0.7459999999999809,-0.4106999999999914],[-1.194000000000017,0],[-0.3729999999999905,0.03730000000000189],[-0.3360000000000127,0.03729999999998768],[0,-1.810666666666663],[0.6350000000000477,-0.07469999999999288],[0.48599999999999,0],[0,0],[0,0]],"o":[[-3.173000000000002,0],[-1.753999999999962,-1.792000000000002],[0,-4.890666666666661],[-1.773333333333369,0],[0,-1.903999999999996],[0.2800000000000296,0],[1.418999999999983,0],[0.7839999999999918,-0.8213000000000079],[0,-0.7839999999999989],[2.109333333333325,0],[0,2.314666666666668],[2.295999999999992,0],[0,1.903999999999996],[-2.295999999999992,0],[0,4.797333333333327],[0,1.045299999999997],[0.3359999999999559,0.7092999999999989],[0.7470000000000141,0.3733000000000004],[0.2989999999999782,0],[0.3740000000000236,-0.03730000000000189],[0,1.810666666666663],[-0.5220000000000482,0.0747000000000071],[-0.6339999999999577,0.0747000000000071],[0,0],[0,0],[0,0]]}}},{"ty":"tm","s":{"a":0,"k":0,"ix":2},"e":{"a":0,"k":100,"ix":2},"o":{"a":0,"k":0,"ix":2},"m":1},{"ty":"tr","p":{"a":0,"k":[-354.5325317382812,-77.50520324707031],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}}]},{"ty":"gr","it":[{"ty":"sh","d":1,"ks":{"a":0,"k":{"c":true,"v":[[210.259,7.951999999999998],[210.259,0.6720000000000041],[216.587,0.6720000000000041],[216.587,7.951999999999998],[210.259,7.951999999999998],[210.259,7.951999999999998],[210.259,7.951999999999998]],"i":[[0,0],[0,2.426666666666662],[-2.109333333333325,0],[0,-2.426666666666662],[2.109333333333325,0],[0,0],[0,0]],"o":[[0,-2.426666666666662],[2.109333333333325,0],[0,2.426666666666662],[-2.109333333333325,0],[0,0],[0,0],[0,0]]}}},{"ty":"tm","s":{"a":0,"k":0,"ix":2},"e":{"a":0,"k":100,"ix":2},"o":{"a":0,"k":0,"ix":2},"m":1},{"ty":"tr","p":{"a":0,"k":[-354.5325317382812,-77.50520324707031],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}}]},{"ty":"gr","it":[{"ty":"sh","d":1,"ks":{"a":0,"k":{"c":true,"v":[[210.259,42.392],[210.259,12.096],[216.587,12.096],[216.587,42.392],[210.259,42.392],[210.259,42.392],[210.259,42.392]],"i":[[0,0],[0,10.09866666666667],[-2.109333333333325,0],[0,-10.09866666666667],[2.109333333333325,0],[0,0],[0,0]],"o":[[0,-10.09866666666666],[2.109333333333325,0],[0,10.09866666666666],[-2.109333333333325,0],[0,0],[0,0],[0,0]]}}},{"ty":"tm","s":{"a":0,"k":0,"ix":2},"e":{"a":0,"k":100,"ix":2},"o":{"a":0,"k":0,"ix":2},"m":1},{"ty":"tr","p":{"a":0,"k":[-354.5325317382812,-77.50520324707031],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}}]},{"ty":"gr","it":[{"ty":"sh","d":1,"ks":{"a":0,"k":{"c":true,"v":[[169.688,42.392],[159.272,12.096],[165.992,12.096],[173.944,36.232],[171.592,36.232],[179.712,12.096],[185.48,12.096],[193.544,36.232],[191.192,36.232],[199.2,12.096],[205.92,12.096],[195.448,42.392],[189.736,42.392],[181.56,17.696],[183.632,17.696],[175.456,42.392],[169.688,42.392],[169.688,42.392],[169.688,42.392]],"i":[[0,0],[3.47199999999998,10.09866666666667],[-2.240000000000009,0],[-2.650666666666666,-8.045333333333332],[0.7839999999999918,0],[-2.706666666666649,8.045333333333332],[-1.922666666666657,0],[-2.687999999999988,-8.045333333333332],[0.7839999999999918,0],[-2.669333333333327,8.045333333333332],[-2.240000000000009,0],[3.490666666666641,-10.09866666666667],[1.903999999999996,0],[2.725333333333367,8.232],[-0.6906666666666865,0],[2.72533333333331,-8.232],[1.922666666666657,0],[0,0],[0,0]],"o":[[-3.47199999999998,-10.09866666666666],[2.240000000000009,0],[2.650666666666666,8.045333333333332],[-0.7839999999999918,0],[2.706666666666649,-8.045333333333332],[1.922666666666657,0],[2.687999999999988,8.045333333333332],[-0.7839999999999918,0],[2.669333333333327,-8.045333333333332],[2.240000000000009,0],[-3.490666666666641,10.09866666666666],[-1.903999999999996,0],[-2.725333333333367,-8.232],[0.6906666666666865,0],[-2.72533333333331,8.232],[-1.922666666666657,0],[0,0],[0,0],[0,0]]}}},{"ty":"tm","s":{"a":0,"k":0,"ix":2},"e":{"a":0,"k":100,"ix":2},"o":{"a":0,"k":0,"ix":2},"m":1},{"ty":"tr","p":{"a":0,"k":[-354.5325317382812,-77.50520324707031],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}}]},{"ty":"fl","c":{"a":0,"k":[1,1,1],"ix":2},"o":{"a":0,"k":100,"ix":2},"r":1,"bm":0},{"ty":"tr","p":{"a":0,"k":[155.86146545410156,56.001014709472656],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[99.99999403953552,99.99999403953552],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}}]},{"ty":"gr","it":[{"ty":"gr","it":[{"ty":"sh","d":1,"ks":{"a":0,"k":{"c":true,"v":[[132.444,43.06400000000001],[124.38,40.992],[118.948,35.28],[116.988,27.16],[118.948,19.040000000000006],[124.324,13.49600000000001],[131.94,11.424000000000007],[138.044,12.600000000000009],[142.58,15.848],[145.38,20.49600000000001],[146.388,26.096],[146.332,27.608],[146.108,29.064000000000007],[121.972,29.064000000000007],[121.972,24.024],[142.524,24.024],[139.5,26.320000000000007],[139.108,21.448000000000008],[136.308,18.032],[131.94,16.744],[127.46,18.032],[124.436,21.616],[123.596,27.216000000000008],[124.436,32.592],[127.684,36.176],[132.5,37.464],[137.148,36.232],[140.228,33.040000000000006],[145.38,35.56],[142.58,39.42400000000001],[138.1,42.11200000000001],[132.444,43.06400000000001],[132.444,43.06400000000001],[132.444,43.06400000000001]],"i":[[0,0],[2.351999999999975,1.381299999999996],[1.305999999999983,2.389300000000006],[0,2.986699999999999],[-1.307000000000016,2.35199999999999],[-2.240000000000009,1.343999999999994],[-2.838000000000022,0],[-1.79200000000003,-0.784000000000006],[-1.232000000000028,-1.381299999999996],[-0.6350000000000477,-1.754700000000014],[0,-1.978700000000003],[0.03699999999997772,-0.5227000000000004],[0.1120000000000232,-0.4480000000000075],[8.045333333333303,0],[0,1.680000000000007],[-6.850666666666655,0],[1.007999999999981,-0.7653333333333308],[0.6340000000000146,1.4187000000000012],[1.269000000000005,0.8213000000000079],[1.67999999999995,0],[1.305999999999983,-0.8586999999999989],[0.7090000000000032,-1.567999999999998],[-0.1499999999999773,-2.202700000000007],[-0.7470000000000141,-1.530699999999996],[-1.382000000000005,-0.8586999999999989],[-1.791999999999973,0],[-1.269999999999982,0.8213000000000079],[-0.7469999999999573,1.306699999999992],[-1.717333333333329,-0.8400000000000034],[1.269000000000005,-1.157300000000006],[1.754000000000019,-0.6720000000000113],[2.052999999999997,0],[0,0],[0,0]],"o":[[-3.024000000000001,0],[-2.314999999999998,-1.4187000000000012],[-1.307000000000016,-2.426699999999997],[0,-3.061299999999989],[1.343999999999994,-2.352000000000004],[2.240000000000009,-1.3813000000000102],[2.276999999999987,0],[1.791999999999973,0.7839999999999918],[1.231999999999971,1.344000000000008],[0.6719999999999686,1.7547],[0,0.4852999999999952],[-0.03800000000001091,0.5227000000000004],[-8.045333333333303,0],[0,-1.680000000000007],[6.850666666666655,0],[-1.007999999999981,0.7653333333333308],[0.3730000000000473,-1.829300000000003],[-0.5979999999999563,-1.456000000000003],[-1.232000000000028,-0.8586999999999989],[-1.680000000000007,0],[-1.307000000000016,0.8213000000000079],[-0.7100000000000364,1.530699999999996],[-0.186999999999955,2.053299999999993],[0.7839999999999918,1.53070000000001],[1.418000000000006,0.8586999999999989],[1.829000000000008,0],[1.305999999999983,-0.8212999999999937],[1.717333333333329,0.8400000000000034],[-0.5980000000000132,1.4187000000000012],[-1.232000000000028,1.11999999999999],[-1.718000000000018,0.6346999999999952],[0,0],[0,0],[0,0]]}}},{"ty":"tm","s":{"a":0,"k":0,"ix":2},"e":{"a":0,"k":100,"ix":2},"o":{"a":0,"k":0,"ix":2},"m":1},{"ty":"tr","p":{"a":0,"k":[-211.7300415039062,-77.67320251464844],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}}]},{"ty":"gr","it":[{"ty":"sh","d":1,"ks":{"a":0,"k":{"c":true,"v":[[95.32,37.184],[100.024,35.896],[103.328,32.36800000000001],[104.56,27.216000000000008],[103.328,22.12],[100.024,18.592],[95.32,17.304],[90.56,18.592],[87.256,22.12],[86.08000000000001,27.216000000000008],[87.256,32.36800000000001],[90.50399999999999,35.896],[95.32,37.184],[95.32,37.184],[95.32,37.184]],"i":[[0,0],[-1.381,0.8586999999999989],[-0.7839999999999918,1.493299999999991],[0,1.903999999999996],[0.820999999999998,1.493299999999991],[1.419000000000011,0.8586999999999989],[1.754999999999995,0],[1.418999999999983,-0.8586999999999989],[0.7839999999999918,-1.493300000000005],[0,-1.904000000000011],[-0.7839999999999918,-1.5307000000000102],[-1.381,-0.8586999999999989],[-1.792000000000002,0],[0,0],[0,0]],"o":[[1.754999999999995,0],[1.419000000000011,-0.8586999999999989],[0.820999999999998,-1.5307000000000102],[0,-1.904000000000011],[-0.7839999999999918,-1.493300000000005],[-1.381,-0.8586999999999989],[-1.754999999999995,0],[-1.419000000000011,0.8586999999999989],[-0.7839999999999918,1.493299999999991],[0,1.903999999999996],[0.7839999999999918,1.493299999999991],[1.419000000000011,0.8586999999999989],[0,0],[0,0],[0,0]]}}},{"ty":"tm","s":{"a":0,"k":0,"ix":2},"e":{"a":0,"k":100,"ix":2},"o":{"a":0,"k":0,"ix":2},"m":1},{"ty":"tr","p":{"a":0,"k":[-211.7300415039062,-77.67320251464844],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}}]},{"ty":"gr","it":[{"ty":"sh","d":1,"ks":{"a":0,"k":{"c":true,"v":[[94.70400000000001,43.06400000000001],[86.864,40.992],[81.43199999999999,35.28],[79.47200000000001,27.216000000000008],[81.488,19.152],[86.91999999999999,13.49600000000001],[94.648,11.424000000000007],[101.088,12.88],[105.512,16.912000000000006],[104.56,18.36800000000001],[104.56,0],[110.832,0],[110.832,42.392],[104.84,42.392],[104.84,36.232],[105.568,37.408],[101.088,41.608],[94.70400000000001,43.06400000000001],[94.70400000000001,43.06400000000001],[94.70400000000001,43.06400000000001]],"i":[[0,0],[2.314999999999998,1.381299999999996],[1.344000000000023,2.389300000000006],[0,2.986699999999999],[-1.343999999999994,2.389300000000006],[-2.276999999999987,1.381299999999996],[-2.875,0],[-1.8669999999999902,-0.9706999999999937],[-1.082999999999998,-1.717300000000009],[0.3173333333333233,-0.4853333333333296],[0,6.122666666666674],[-2.090666666666664,0],[0,-14.13066666666667],[1.99733333333333,0],[0,2.053333333333327],[-0.242666666666679,-0.3919999999999959],[1.941000000000003,-0.9707000000000079],[2.314999999999998,0],[0,0],[0,0]],"o":[[-2.912000000000006,0],[-2.277000000000015,-1.4187000000000012],[-1.306999999999988,-2.389300000000006],[0,-2.986699999999999],[1.343999999999994,-2.389300000000006],[2.277000000000015,-1.3813000000000102],[2.426999999999992,0],[1.867000000000019,0.9707000000000079],[-0.3173333333333233,0.4853333333333296],[0,-6.122666666666674],[2.090666666666664,0],[0,14.13066666666667],[-1.99733333333333,0],[0,-2.053333333333327],[0.242666666666679,0.3919999999999959],[-1.045000000000016,1.829300000000003],[-1.941000000000003,0.9706999999999937],[0,0],[0,0],[0,0]]}}},{"ty":"tm","s":{"a":0,"k":0,"ix":2},"e":{"a":0,"k":100,"ix":2},"o":{"a":0,"k":0,"ix":2},"m":1},{"ty":"tr","p":{"a":0,"k":[-211.7300415039062,-77.67320251464844],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}}]},{"ty":"gr","it":[{"ty":"sh","d":1,"ks":{"a":0,"k":{"c":true,"v":[[57.40100000000001,43.06400000000001],[51.968999999999994,42],[48.38499999999999,38.976],[47.09700000000001,34.384],[48.161,29.960000000000008],[51.577,26.656000000000006],[57.345,24.752],[67.87299999999999,23.016000000000005],[67.87299999999999,28],[58.577,29.624],[54.881,31.248],[53.70500000000001,34.16],[54.992999999999995,37.016000000000005],[58.40899999999999,38.08],[62.88900000000001,36.96000000000001],[65.91300000000001,33.992],[66.977,29.792],[66.977,22.008],[65.297,18.36800000000001],[60.929,16.912000000000006],[56.505,18.256],[53.761,21.616],[48.496999999999986,18.98400000000001],[51.240999999999985,15.064000000000007],[55.66499999999999,12.376],[61.09700000000001,11.424000000000007],[67.42500000000001,12.768],[71.737,16.52],[73.305,22.008],[73.305,42.392],[67.257,42.392],[67.257,36.904],[68.54499999999999,37.072],[65.80099999999999,40.264],[62.04900000000001,42.336],[57.40100000000001,43.06400000000001],[57.40100000000001,43.06400000000001],[57.40100000000001,43.06400000000001]],"i":[[0,0],[1.568000000000012,0.7092999999999989],[0.8590000000000089,1.2693000000000012],[0,1.7547],[-0.7090000000000032,1.306699999999992],[-1.531000000000006,0.8960000000000008],[-2.314999999999998,0.3733000000000004],[-3.509333333333331,0.5786666666666633],[0,-1.661333333333332],[3.098666666666674,-0.541333333333327],[0.7839999999999918,-0.784000000000006],[0,-1.194699999999997],[-0.8590000000000089,-0.7467000000000041],[-1.381,0],[-1.269000000000005,0.7466999999999899],[-0.7090000000000032,1.2319999999999993],[0,1.530699999999996],[0,2.594666666666669],[1.120000000000005,0.9332999999999885],[1.829000000000008,0],[1.269000000000005,-0.8960000000000008],[0.5970000000000084,-1.381299999999996],[1.754666666666679,0.8773333333333255],[-1.268999999999977,1.11999999999999],[-1.680000000000007,0.6346999999999952],[-1.903999999999996,0],[-1.829000000000008,-0.8960000000000008],[-1.0080000000000098,-1.6053],[0,-2.090699999999998],[0,-6.794666666666672],[2.015999999999991,0],[0,1.829333333333338],[-0.429333333333318,-0.05599999999999739],[1.120000000000005,-0.8959999999999866],[1.418999999999983,-0.4852999999999952],[1.716999999999985,0],[0,0],[0,0]],"o":[[-2.053000000000026,0],[-1.531000000000006,-0.7467000000000041],[-0.8589999999999804,-1.306699999999992],[0,-1.642700000000005],[0.7469999999999857,-1.306700000000006],[1.531000000000006,-0.8960000000000008],[3.509333333333331,-0.5786666666666633],[0,1.661333333333332],[-3.098666666666674,0.541333333333327],[-1.680000000000007,0.2987000000000108],[-0.7839999999999918,0.7467000000000041],[0,1.157300000000006],[0.896000000000015,0.7092999999999989],[1.717000000000013,0],[1.306999999999988,-0.7467000000000041],[0.7089999999999748,-1.2693000000000012],[0,-2.594666666666669],[0,-1.493299999999991],[-1.082999999999998,-0.9707000000000079],[-1.680000000000007,0],[-1.2319999999999993,0.8586999999999989],[-1.754666666666679,-0.8773333333333255],[0.5600000000000023,-1.493300000000005],[1.269000000000005,-1.157300000000006],[1.717000000000013,-0.6347000000000094],[2.388999999999982,0],[1.86699999999999,0.8960000000000008],[1.045000000000016,1.568000000000012],[0,6.794666666666672],[-2.015999999999991,0],[0,-1.829333333333338],[0.429333333333318,0.05599999999999739],[-0.7089999999999748,1.2319999999999993],[-1.082999999999998,0.8960000000000008],[-1.381,0.4853000000000094],[0,0],[0,0],[0,0]]}}},{"ty":"tm","s":{"a":0,"k":0,"ix":2},"e":{"a":0,"k":100,"ix":2},"o":{"a":0,"k":0,"ix":2},"m":1},{"ty":"tr","p":{"a":0,"k":[-211.7300415039062,-77.67320251464844],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}}]},{"ty":"gr","it":[{"ty":"sh","d":1,"ks":{"a":0,"k":{"c":true,"v":[[0,42.392],[0,0.6720000000000041],[6.159999999999997,0.6720000000000041],[21.84,22.400000000000006],[18.75999999999999,22.400000000000006],[34.16,0.6720000000000041],[40.31999999999999,0.6720000000000041],[40.31999999999999,42.392],[33.768,42.392],[33.768,8.456000000000003],[36.232,9.128],[20.49600000000001,30.632000000000005],[19.824000000000012,30.632000000000005],[4.424000000000007,9.128],[6.608000000000004,8.456000000000003],[6.608000000000004,42.392],[0,42.392],[0,42.392],[0,42.392]],"i":[[0,0],[0,13.90666666666666],[-2.053333333333342,0],[-5.226666666666659,-7.242666666666665],[1.026666666666671,0],[-5.133333333333326,7.242666666666672],[-2.053333333333342,0],[0,-13.90666666666667],[2.183999999999997,0],[0,11.312],[-0.8213333333333424,-0.2240000000000038],[5.245333333333321,-7.168000000000006],[0.2239999999999895,0],[5.133333333333326,7.168000000000006],[-0.7280000000000086,0.2240000000000038],[0,-11.312],[2.202666666666659,0],[0,0],[0,0]],"o":[[0,-13.90666666666667],[2.053333333333342,0],[5.226666666666659,7.242666666666672],[-1.026666666666671,0],[5.133333333333326,-7.242666666666665],[2.053333333333342,0],[0,13.90666666666666],[-2.183999999999997,0],[0,-11.312],[0.8213333333333424,0.2240000000000038],[-5.245333333333321,7.168000000000006],[-0.2239999999999895,0],[-5.133333333333326,-7.168000000000006],[0.7280000000000086,-0.2240000000000038],[0,11.312],[-2.202666666666659,0],[0,0],[0,0],[0,0]]}}},{"ty":"tm","s":{"a":0,"k":0,"ix":2},"e":{"a":0,"k":100,"ix":2},"o":{"a":0,"k":0,"ix":2},"m":1},{"ty":"tr","p":{"a":0,"k":[-211.7300415039062,-77.67320251464844],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}}]},{"ty":"fl","c":{"a":0,"k":[1,1,1],"ix":2},"o":{"a":0,"k":100,"ix":2},"r":1,"bm":0},{"ty":"tr","p":{"a":0,"k":[2.91259765625,56.001014709472656],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[99.99999403953552,99.99999403953552],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}}]},{"ty":"gr","it":[{"ty":"rc","d":1,"s":{"a":0,"k":[702.6863719370097,144],"ix":2},"p":{"a":0,"k":[0,0],"ix":2},"r":{"a":0,"k":72,"ix":2}},{"ty":"fl","c":{"a":0,"k":[0,0,0],"ix":2},"o":{"a":0,"k":100,"ix":2},"r":1,"bm":0},{"ty":"tm","s":{"a":0,"k":0,"ix":2},"e":{"a":0,"k":100,"ix":2},"o":{"a":0,"k":0,"ix":2},"m":1},{"ty":"tr","p":{"a":0,"k":[56.54167175292969,-0.000022762338630855083],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[99.99999403953552,99.99999403953552],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":80,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}}]},{"ty":"tr","p":{"a":0,"k":[122.0000003294881,25.00000012138912],"ix":2},"a":{"a":0,"k":[56.54167175292969,-0.00002288818359375],"ix":2},"s":{"a":0,"k":[34.403572049765366,34.403572049765366],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}}]}],"ip":0,"op":255,"st":0,"bm":0},{"ddd":0,"ind":1,"ty":4,"nm":"key-920.svg 1","sr":1,"ks":{"p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ty":"gr","it":[{"ty":"gr","nm":"SVG","it":[{"ty":"gr","nm":"Path","it":[{"ty":"sh","d":1,"ks":{"a":0,"k":{"c":true,"v":[[22.252499999999987,14.641499999999992],[18.446499999999986,16.21549999999999],[18.446499999999986,23.827499999999993],[26.058499999999988,23.827499999999993],[26.058499999999988,16.215499999999995],[22.252499999999987,14.641499999999992],[22.252499999999987,14.641499999999992]],"i":[[0,0],[1.0489999999999995,-1.0489999999999995],[-2.099,-2.0980000000000025],[-2.1000000000000014,2.099],[2.099,2.097999999999999],[1.3779999999999966,0],[0,0]],"o":[[-1.3790000000000013,0],[-2.099,2.099],[2.099,2.097999999999999],[2.099,-2.099],[-1.0500000000000043,-1.049000000000003],[0,0],[0,0]]}}},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}}]},{"ty":"gr","nm":"Path","it":[{"ty":"sh","d":1,"ks":{"a":0,"k":{"c":true,"v":[[22.252499999999987,28.381499999999992],[16.33849999999999,25.93549999999999],[16.33849999999999,14.107499999999991],[28.16649999999999,14.107499999999991],[28.16649999999999,25.93549999999999],[22.252499999999987,28.381499999999992],[22.252499999999987,28.381499999999992]],"i":[[0,0],[1.629999999999999,1.6300000000000026],[-3.2609999999999992,3.2609999999999992],[-3.2609999999999992,-3.26],[3.2609999999999992,-3.2609999999999992],[2.1409999999999982,0],[0,0]],"o":[[-2.1419999999999995,0],[-3.2609999999999992,-3.2609999999999992],[3.2609999999999992,-3.26],[3.2609999999999992,3.2609999999999992],[-1.6310000000000038,1.6310000000000002],[0,0],[0,0]]}}},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}}]},{"ty":"gr","nm":"Path","it":[{"ty":"sh","d":1,"ks":{"a":0,"k":{"c":true,"v":[[68.07149999999999,71.6065],[70.06249999999999,72.3885],[70.99949999999998,74.8385],[70.59849999999999,79.04849999999999],[79.01449999999998,87.44749999999999],[87.01849999999999,86.56649999999999],[87.01849999999999,76.95249999999999],[50.197499999999984,40.14549999999999],[50.66749999999998,39.18849999999999],[45.89349999999998,10.334499999999991],[10.33349999999998,10.334499999999991],[10.33349999999998,45.894499999999994],[40.19849999999998,50.15149999999999],[41.17849999999998,49.61249999999999],[46.033499999999975,54.46649999999999],[50.018499999999975,53.61949999999999],[52.70949999999998,54.42049999999999],[53.512499999999974,57.11149999999999],[52.673499999999976,61.125499999999995],[57.10449999999997,65.53750000000001],[61.73249999999997,64.87150000000001],[64.12249999999997,65.60350000000001],[65.09449999999997,67.90450000000001],[64.93349999999997,72.16050000000001],[67.54249999999996,71.65550000000002],[68.07149999999999,71.6065],[68.07149999999999,71.6065]],"i":[[0,0],[-0.5529999999999973,-0.5090000000000003],[0.08899999999999864,-0.9260000000000019],[0.13366666666667015,-1.403333333333336],[-2.805333333333337,-2.799666666666667],[-2.6680000000000064,0.29366666666666674],[0,3.204666666666668],[12.27366666666667,12.268999999999998],[-0.1566666666666663,0.3190000000000026],[7.597999999999999,7.599],[9.803,-9.803],[-9.804,-9.804000000000002],[-9.736,5.341000000000001],[-0.326666666666668,0.17966666666666953],[-1.6183333333333323,-1.618000000000002],[-1.3283333333333331,0.28233333333333377],[-0.7060000000000031,-0.7070000000000007],[0.20400000000000063,-0.9780000000000015],[0.27966666666666384,-1.338000000000001],[-1.4769999999999968,-1.4706666666666734],[-1.542666666666669,0.2219999999999942],[-0.6510000000000034,-0.5879999999999939],[0.03300000000000125,-0.875],[0.05366666666667186,-1.4186666666666667],[-0.8696666666666601,0.16833333333333655],[-0.17499999999999716,0],[0,0]],"o":[[0.7339999999999947,0],[0.6839999999999975,0.6310000000000002],[-0.13366666666667015,1.403333333333336],[2.805333333333337,2.799666666666667],[2.6680000000000064,-0.29366666666666674],[0,-3.204666666666668],[-12.27366666666667,-12.268999999999991],[0.1566666666666663,-0.3190000000000026],[4.743000000000002,-9.66],[-9.804000000000002,-9.804],[-9.804,9.804000000000002],[7.849,7.848999999999997],[0.326666666666668,-0.17966666666666953],[1.6183333333333323,1.618000000000002],[1.3283333333333331,-0.28233333333333377],[0.9789999999999992,-0.2079999999999984],[0.7070000000000007,0.7070000000000007],[-0.27966666666666384,1.338000000000001],[1.4769999999999968,1.4706666666666663],[1.542666666666669,-0.2219999999999942],[0.8699999999999974,-0.12300000000000466],[0.6500000000000057,0.5859999999999985],[-0.05366666666667186,1.4186666666666667],[0.8696666666666601,-0.16833333333333655],[0.17600000000003035,-0.03300000000001546],[0,0],[0,0]]}}},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}}]},{"ty":"gr","nm":"Path","it":[{"ty":"sh","d":1,"ks":{"a":0,"k":{"c":true,"v":[[78.99149999999999,90.4215],[76.90749999999998,89.5565],[68.48249999999999,81.13250000000001],[67.63149999999999,78.76750000000001],[68.03249999999998,74.55550000000001],[65.42349999999999,75.0635],[62.96649999999999,74.38550000000001],[61.95349999999999,72.04750000000001],[62.11449999999999,67.79150000000001],[57.49549999999999,68.47950000000002],[54.99549999999999,67.64350000000002],[50.555499999999995,63.20450000000001],[49.75449999999999,60.51050000000001],[50.59349999999999,56.499500000000005],[46.616499999999995,57.374500000000005],[43.924499999999995,56.5735],[40.634499999999996,53.283500000000004],[8.224499999999999,48.0005],[8.224499999999999,8.224499999999999],[48.0005,8.224499999999999],[53.7975,39.527499999999996],[89.1365,74.86550000000001],[90.0005,76.95250000000001],[90.0005,86.59350000000002],[87.3685,89.52750000000002],[79.31349999999999,90.40250000000002],[78.99149999999999,90.4215],[78.99149999999999,90.4215]],"i":[[0,0],[0.5580000000000069,0.5579999999999927],[2.808333333333337,2.8079999999999927],[-0.08299999999999841,0.8780000000000001],[-0.13366666666667015,1.4039999999999964],[0.8696666666666601,-0.16933333333332712],[0.6780000000000044,0.5900000000000034],[-0.03399999999999892,0.8969999999999914],[-0.05366666666666475,1.4186666666666667],[1.539666666666669,-0.2293333333333294],[0.652000000000001,0.6529999999999916],[1.4799999999999969,1.4796666666666738],[-0.20599999999999596,0.9770000000000039],[-0.27966666666666384,1.3370000000000033],[1.3256666666666632,-0.2916666666666643],[0.7070000000000007,0.7070000000000007],[1.096666666666664,1.096666666666664],[8.514999999999997,8.515999999999998],[-10.965999999999998,10.966000000000005],[-10.966000000000008,-10.966],[4.715000000000003,-10.600999999999996],[-11.779666666666671,-11.779333333333348],[0,-0.7890000000000015],[0,-3.2136666666666684],[1.5,-0.1629999999999967],[2.6850000000000023,-0.2916666666666714],[0.10699999999999932,0],[0,0]],"o":[[-0.7800000000000011,0],[-2.808333333333337,-2.8079999999999927],[-0.6239999999999952,-0.6260000000000048],[0.13366666666667015,-1.4039999999999964],[-0.8696666666666601,0.16933333333332712],[-0.8790000000000049,0.15699999999999648],[-0.6769999999999996,-0.5889999999999986],[0.05366666666666475,-1.4186666666666667],[-1.539666666666669,0.2293333333333294],[-0.9140000000000015,0.132000000000005],[-1.4799999999999969,-1.4796666666666596],[-0.7079999999999984,-0.7100000000000009],[0.27966666666666384,-1.3370000000000033],[-1.3256666666666632,0.2916666666666643],[-0.9810000000000016,0.20700000000000074],[-1.096666666666664,-1.096666666666664],[-10.721,5.331000000000003],[-10.965999999999998,-10.966000000000001],[10.965,-10.966],[8.246000000000002,8.247],[11.779666666666671,11.779333333333334],[0.5570000000000022,0.5570000000000022],[0,3.2136666666666684],[0,1.5090000000000003],[-2.6850000000000023,0.2916666666666714],[-0.10699999999999932,0.012999999999976808],[0,0],[0,0]]}}},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}}]},{"ty":"st","c":{"a":0,"k":[1,1,1],"ix":2},"o":{"a":0,"k":100,"ix":2},"w":{"a":0,"k":2,"ix":2},"lc":1,"lj":1,"ml":4},{"ty":"fl","c":{"a":0,"k":[1,1,1],"ix":2},"o":{"a":0,"k":100,"ix":2},"r":1,"bm":0},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}}]},{"ty":"tr","p":{"a":0,"k":[128.41790771484375,127.32901763916016],"ix":2},"a":{"a":0,"k":[45.00025177001953,45.210750579833984],"ix":2},"s":{"a":0,"k":[280.9999942779541,280.9999942779541],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}}]},{"ty":"tr","p":{"a":0,"k":[53.66798400878906,226.4100341796875],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[106.49479392662286,106.49479392662285],"ix":2},"r":{"a":0,"k":-45,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}}]}],"ip":0,"op":255,"st":0,"bm":0},{"ddd":0,"ind":2,"ty":4,"nm":"Animation - 1739985524883.json","sr":1,"ks":{"p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ty":"gr","it":[{"ty":"gr","nm":"Group 1","it":[{"ty":"sh","d":1,"ks":{"a":0,"k":{"c":true,"v":[[61.405,0.002],[0.003,61.404],[-61.405,0.002],[0.003,-61.404],[61.405,0.002]],"i":[[0,0],[33.91,0],[0,33.91],[-33.915,0],[0,-33.911]],"o":[[0,33.91],[-33.915,0],[0,-33.911],[33.91,0],[0,0]]}}},{"ty":"st","c":{"a":0,"k":[1,1,1],"ix":2},"o":{"a":0,"k":100,"ix":2},"w":{"a":0,"k":32,"ix":2},"lc":2,"lj":1,"ml":4},{"ty":"tr","p":{"a":0,"k":[121.405,121.405],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}}]},{"ty":"tr","p":{"a":1,"k":[{"t":137,"s":[291.0716552734375,587.243005324116],"i":{"x":[0.75],"y":[0.75]},"o":{"x":[0.25],"y":[0.25]},"ti":[0,46.5],"to":[0,-61.167]},{"t":160,"s":[291.0716552734375,220.24300532411598],"i":{"x":[0.75],"y":[0.75]},"o":{"x":[0.25],"y":[0.25]},"ti":[0,-4.833],"to":[0,-46.5]},{"t":177,"s":[291.0716552734375,308.243005324116],"i":{"x":[0.75],"y":[0.75]},"o":{"x":[0.25],"y":[0.25]},"ti":[0,6.667],"to":[0,4.833]},{"t":194,"s":[291.0716552734375,249.24300532411598],"i":{"x":[0.75],"y":[0.75]},"o":{"x":[0.25],"y":[0.25]},"ti":[0,-1.167],"to":[0,-6.667]},{"t":210,"s":[291.0716552734375,268.243005324116],"i":{"x":[0.75],"y":[0.75]},"o":{"x":[0.25],"y":[0.25]},"ti":[0,2],"to":[0,1.167]},{"t":227,"s":[291.0716552734375,256.243005324116],"i":{"x":[0.75],"y":[0.75]},"o":{"x":[0.25],"y":[0.25]}}],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[66.06338024139404,66.06338024139404],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}}]},{"ty":"gr","nm":"avatar_body","it":[{"ty":"sh","d":1,"ks":{"a":0,"k":{"c":false,"v":[[-72.67896713876723,36.34080483698845],[0,-36.34080483698845],[72.67896713876723,36.34080483698845]],"i":[[0,0],[-40.140770468473434,0],[0,-40.140770468473434]],"o":[[0,-40.140770468473434],[40.138127933263775,0],[0,0]]}}},{"ty":"tm","s":{"a":0,"k":0,"ix":2},"e":{"a":1,"k":[{"t":110,"s":[0],"i":{"x":[0.75],"y":[0.75]},"o":{"x":[0.25],"y":[0.25]}},{"t":133,"s":[100],"i":{"x":[0.75],"y":[0.75]},"o":{"x":[0.25],"y":[0.25]}}],"ix":2},"o":{"a":0,"k":0,"ix":2},"m":1},{"ty":"st","c":{"a":0,"k":[1,1,1],"ix":2},"o":{"a":0,"k":100,"ix":2},"w":{"a":0,"k":32,"ix":2},"lc":2,"lj":1,"ml":4},{"ty":"tr","p":{"a":0,"k":[371.27587890625,447.1807861328125],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}}]},{"ty":"gr","nm":"line_01","it":[{"ty":"sh","d":1,"ks":{"a":0,"k":{"c":false,"v":[[-0.0024501811066720562,256.3418873988745],[-137.86066505745657,256.3418873988745],[-239.29448760201967,163.07350668959697],[-239.29448760201967,-163.07350668959697],[-137.86066505745657,-256.3418873988745],[137.86066505745657,-256.3418873988745],[239.29448760201967,-163.07350668959697]],"i":[[0,0],[0,0],[0,51.508991585593975],[0,0],[-56.02339100405656,0],[0,0],[0,-51.51349746772542]],"o":[[0,0],[-56.02339100405656,0],[0,0],[0,-51.51349746772542],[0,0],[56.018490641843215,0],[0,0]]}}},{"ty":"tm","s":{"a":0,"k":0,"ix":2},"e":{"a":1,"k":[{"t":0,"s":[0],"i":{"x":[0.75],"y":[0.75]},"o":{"x":[0.25],"y":[0.25]}},{"t":47,"s":[100],"i":{"x":[0.75],"y":[0.75]},"o":{"x":[0.25],"y":[0.25]}}],"ix":2},"o":{"a":0,"k":205,"ix":2},"m":1},{"ty":"st","c":{"a":0,"k":[1,1,1],"ix":2},"o":{"a":0,"k":100,"ix":2},"w":{"a":0,"k":32,"ix":2},"lc":2,"lj":1,"ml":4},{"ty":"tr","p":{"a":0,"k":[219.6200408935547,227.17970275878906],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}}]},{"ty":"tr","p":{"a":0,"k":[65.90681457519531,58.5899658203125],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[81.9148063659668,81.9148063659668],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}}]}],"ip":0,"op":255,"st":0,"bm":0}],"markers":[]} \ No newline at end of file diff --git a/frontend/src/components/v2/Menu/Menu.tsx b/frontend/src/components/v2/Menu/Menu.tsx index 6fc2a7d00c..3a9c926a2e 100644 --- a/frontend/src/components/v2/Menu/Menu.tsx +++ b/frontend/src/components/v2/Menu/Menu.tsx @@ -1,5 +1,5 @@ import { ComponentPropsWithRef, ElementType, ReactNode, Ref, useRef } from "react"; -import { DotLottie, DotLottieReact } from "@lottiefiles/dotlottie-react"; +import { DotLottie, DotLottieReact, Mode } from "@lottiefiles/dotlottie-react"; import { twMerge } from "tailwind-merge"; export type MenuProps = { @@ -16,6 +16,7 @@ export type MenuItemProps = { as?: T; children: ReactNode; icon?: string; + iconMode?: Mode; description?: ReactNode; isDisabled?: boolean; isSelected?: boolean; @@ -26,6 +27,7 @@ export type MenuItemProps = { export const MenuItem = ({ children, icon, + iconMode, className, isDisabled, isSelected, @@ -62,6 +64,7 @@ export const MenuItem = ({ dotLottieRefCallback={(el) => { iconRef.current = el; }} + mode={iconMode} src={`/lotties/${icon}.json`} loop className="h-full w-full" diff --git a/frontend/src/context/OrgPermissionContext/types.ts b/frontend/src/context/OrgPermissionContext/types.ts index ea5003c8ce..46329abc64 100644 --- a/frontend/src/context/OrgPermissionContext/types.ts +++ b/frontend/src/context/OrgPermissionContext/types.ts @@ -24,7 +24,8 @@ export enum OrgPermissionSubjects { AdminConsole = "organization-admin-console", AuditLogs = "audit-logs", ProjectTemplates = "project-templates", - AppConnections = "app-connections" + AppConnections = "app-connections", + Kmip = "kmip" } export enum OrgPermissionAdminConsoleAction { @@ -39,6 +40,11 @@ export enum OrgPermissionAppConnectionActions { Connect = "connect" } +export enum OrgPermissionKmipActions { + Proxy = "proxy", + Setup = "setup" +} + export type AppConnectionSubjectFields = { connectionId: string; }; @@ -61,7 +67,8 @@ export type OrgPermissionSet = | [OrgPermissionAdminConsoleAction, OrgPermissionSubjects.AdminConsole] | [OrgPermissionActions, OrgPermissionSubjects.AuditLogs] | [OrgPermissionActions, OrgPermissionSubjects.ProjectTemplates] - | [OrgPermissionAppConnectionActions, OrgPermissionSubjects.AppConnections]; + | [OrgPermissionAppConnectionActions, OrgPermissionSubjects.AppConnections] + | [OrgPermissionKmipActions, OrgPermissionSubjects.Kmip]; // TODO(scott): add back once org UI refactored // | [ // OrgPermissionAppConnectionActions, diff --git a/frontend/src/context/ProjectPermissionContext/index.tsx b/frontend/src/context/ProjectPermissionContext/index.tsx index 009bc9451b..69bcd4f992 100644 --- a/frontend/src/context/ProjectPermissionContext/index.tsx +++ b/frontend/src/context/ProjectPermissionContext/index.tsx @@ -4,5 +4,6 @@ export { ProjectPermissionActions, ProjectPermissionCmekActions, ProjectPermissionDynamicSecretActions, + ProjectPermissionKmipActions, ProjectPermissionSub } from "./types"; diff --git a/frontend/src/context/ProjectPermissionContext/types.ts b/frontend/src/context/ProjectPermissionContext/types.ts index 279e69889a..d368a949f5 100644 --- a/frontend/src/context/ProjectPermissionContext/types.ts +++ b/frontend/src/context/ProjectPermissionContext/types.ts @@ -24,6 +24,14 @@ export enum ProjectPermissionCmekActions { Decrypt = "decrypt" } +export enum ProjectPermissionKmipActions { + CreateClients = "create-clients", + UpdateClients = "update-clients", + DeleteClients = "delete-clients", + ReadClients = "read-clients", + GenerateClientCertificates = "generate-client-certificates" +} + export enum ProjectPermissionSecretSyncActions { Read = "read", Create = "create", @@ -102,7 +110,8 @@ export enum ProjectPermissionSub { PkiCollections = "pki-collections", Kms = "kms", Cmek = "cmek", - SecretSyncs = "secret-syncs" + SecretSyncs = "secret-syncs", + Kmip = "kmip" } export type SecretSubjectFields = { @@ -190,5 +199,7 @@ export type ProjectPermissionSet = | [ProjectPermissionActions.Read, ProjectPermissionSub.SecretRollback] | [ProjectPermissionActions.Create, ProjectPermissionSub.SecretRollback] | [ProjectPermissionCmekActions, ProjectPermissionSub.Cmek] - | [ProjectPermissionActions.Edit, ProjectPermissionSub.Kms]; + | [ProjectPermissionActions.Edit, ProjectPermissionSub.Kms] + | [ProjectPermissionKmipActions, ProjectPermissionSub.Kmip]; + export type TProjectPermission = MongoAbility; diff --git a/frontend/src/context/index.tsx b/frontend/src/context/index.tsx index 70d00dd744..fb9d8c3851 100644 --- a/frontend/src/context/index.tsx +++ b/frontend/src/context/index.tsx @@ -10,6 +10,7 @@ export { ProjectPermissionActions, ProjectPermissionCmekActions, ProjectPermissionDynamicSecretActions, + ProjectPermissionKmipActions, ProjectPermissionSub, useProjectPermission } from "./ProjectPermissionContext"; diff --git a/frontend/src/helpers/download.ts b/frontend/src/helpers/download.ts new file mode 100644 index 0000000000..50adf4fe23 --- /dev/null +++ b/frontend/src/helpers/download.ts @@ -0,0 +1,6 @@ +import FileSaver from "file-saver"; + +export const downloadTxtFile = (filename: string, content: string) => { + const blob = new Blob([content], { type: "text/plain;charset=utf-8" }); + FileSaver.saveAs(blob, filename); +}; diff --git a/frontend/src/hooks/api/auditLogs/constants.tsx b/frontend/src/hooks/api/auditLogs/constants.tsx index a51ca79020..39c351e385 100644 --- a/frontend/src/hooks/api/auditLogs/constants.tsx +++ b/frontend/src/hooks/api/auditLogs/constants.tsx @@ -121,7 +121,24 @@ export const eventToNameMap: { [K in EventType]: string } = { [EventType.OIDC_GROUP_MEMBERSHIP_MAPPING_ASSIGN_USER]: "OIDC group membership mapping assigned user to groups", [EventType.OIDC_GROUP_MEMBERSHIP_MAPPING_REMOVE_USER]: - "OIDC group membership mapping removed user from groups" + "OIDC group membership mapping removed user from groups", + [EventType.CREATE_KMIP_CLIENT]: "Create KMIP client", + [EventType.UPDATE_KMIP_CLIENT]: "Update KMIP client", + [EventType.DELETE_KMIP_CLIENT]: "Delete KMIP client", + [EventType.GET_KMIP_CLIENT]: "Get KMIP client", + [EventType.GET_KMIP_CLIENTS]: "Get KMIP clients", + [EventType.CREATE_KMIP_CLIENT_CERTIFICATE]: "Create KMIP client certificate", + [EventType.SETUP_KMIP]: "Setup KMIP configuration", + [EventType.GET_KMIP]: "Get KMIP configuration", + [EventType.REGISTER_KMIP_SERVER]: "Register KMIP server", + [EventType.KMIP_OPERATION_CREATE]: "KMIP operation create", + [EventType.KMIP_OPERATION_GET]: "KMIP operation get", + [EventType.KMIP_OPERATION_DESTROY]: "KMIP operation destroy", + [EventType.KMIP_OPERATION_GET_ATTRIBUTES]: "KMIP operation get attributes", + [EventType.KMIP_OPERATION_ACTIVATE]: "KMIP operation activate", + [EventType.KMIP_OPERATION_REVOKE]: "KMIP operation revoke", + [EventType.KMIP_OPERATION_LOCATE]: "KMIP operation locate", + [EventType.KMIP_OPERATION_REGISTER]: "KMIP operation register" }; export const userAgentTTypeoNameMap: { [K in UserAgentType]: string } = { diff --git a/frontend/src/hooks/api/auditLogs/enums.tsx b/frontend/src/hooks/api/auditLogs/enums.tsx index a83a8a429f..ed25c6e4ad 100644 --- a/frontend/src/hooks/api/auditLogs/enums.tsx +++ b/frontend/src/hooks/api/auditLogs/enums.tsx @@ -1,5 +1,6 @@ export enum ActorType { PLATFORM = "platform", + KMIP_CLIENT = "kmipClient", USER = "user", SERVICE = "service", IDENTITY = "identity", @@ -132,5 +133,22 @@ export enum EventType { SECRET_SYNC_IMPORT_SECRETS = "secret-sync-import-secrets", SECRET_SYNC_REMOVE_SECRETS = "secret-sync-remove-secrets", OIDC_GROUP_MEMBERSHIP_MAPPING_ASSIGN_USER = "oidc-group-membership-mapping-assign-user", - OIDC_GROUP_MEMBERSHIP_MAPPING_REMOVE_USER = "oidc-group-membership-mapping-remove-user" + OIDC_GROUP_MEMBERSHIP_MAPPING_REMOVE_USER = "oidc-group-membership-mapping-remove-user", + CREATE_KMIP_CLIENT = "create-kmip-client", + UPDATE_KMIP_CLIENT = "update-kmip-client", + DELETE_KMIP_CLIENT = "delete-kmip-client", + GET_KMIP_CLIENT = "get-kmip-client", + GET_KMIP_CLIENTS = "get-kmip-clients", + CREATE_KMIP_CLIENT_CERTIFICATE = "create-kmip-client-certificate", + SETUP_KMIP = "setup-kmip", + GET_KMIP = "get-kmip", + REGISTER_KMIP_SERVER = "register-kmip-server", + KMIP_OPERATION_CREATE = "kmip-operation-create", + KMIP_OPERATION_GET = "kmip-operation-get", + KMIP_OPERATION_DESTROY = "kmip-operation-destroy", + KMIP_OPERATION_GET_ATTRIBUTES = "kmip-operation-get-attributes", + KMIP_OPERATION_ACTIVATE = "kmip-operation-activate", + KMIP_OPERATION_REVOKE = "kmip-operation-revoke", + KMIP_OPERATION_LOCATE = "kmip-operation-locate", + KMIP_OPERATION_REGISTER = "kmip-operation-register" } diff --git a/frontend/src/hooks/api/auditLogs/types.tsx b/frontend/src/hooks/api/auditLogs/types.tsx index 338671cab5..8acaa215ac 100644 --- a/frontend/src/hooks/api/auditLogs/types.tsx +++ b/frontend/src/hooks/api/auditLogs/types.tsx @@ -30,6 +30,10 @@ interface IdentityActorMetadata { identityId: string; name: string; } +interface KmipClientActorMetadata { + clientId: string; + name: string; +} interface UserActor { type: ActorType.USER; @@ -51,11 +55,22 @@ export interface PlatformActor { metadata: object; } +export interface KmipClientActor { + type: ActorType.KMIP_CLIENT; + metadata: KmipClientActorMetadata; +} + export interface UnknownUserActor { type: ActorType.UNKNOWN_USER; } -export type Actor = UserActor | ServiceActor | IdentityActor | PlatformActor | UnknownUserActor; +export type Actor = + | UserActor + | ServiceActor + | IdentityActor + | PlatformActor + | UnknownUserActor + | KmipClientActor; interface GetSecretsEvent { type: EventType.GET_SECRETS; diff --git a/frontend/src/hooks/api/kmip/index.ts b/frontend/src/hooks/api/kmip/index.ts new file mode 100644 index 0000000000..f4e57d7cc9 --- /dev/null +++ b/frontend/src/hooks/api/kmip/index.ts @@ -0,0 +1,2 @@ +export * from "./mutation"; +export * from "./queries"; diff --git a/frontend/src/hooks/api/kmip/mutation.ts b/frontend/src/hooks/api/kmip/mutation.ts new file mode 100644 index 0000000000..af86c1fe0e --- /dev/null +++ b/frontend/src/hooks/api/kmip/mutation.ts @@ -0,0 +1,90 @@ +import { useMutation, useQueryClient } from "@tanstack/react-query"; + +import { apiRequest } from "@app/config/request"; + +import { kmipKeys } from "./queries"; +import { + KmipClientCertificate, + TCreateKmipClient, + TDeleteKmipClient, + TGenerateKmipClientCertificate, + TSetupOrgKmipDTO, + TUpdateKmipClient +} from "./types"; + +export const useCreateKmipClient = () => { + const queryClient = useQueryClient(); + return useMutation({ + mutationFn: async (payload: TCreateKmipClient) => { + const { data } = await apiRequest.post("/api/v1/kmip/clients", payload); + + return data; + }, + onSuccess: (_, { projectId }) => { + queryClient.invalidateQueries({ + queryKey: kmipKeys.getKmipClientsByProjectId({ projectId }) + }); + } + }); +}; + +export const useUpdateKmipClient = () => { + const queryClient = useQueryClient(); + return useMutation({ + mutationFn: async ({ id, name, description, permissions }: TUpdateKmipClient) => { + const { data } = await apiRequest.patch(`/api/v1/kmip/clients/${id}`, { + name, + description, + permissions + }); + + return data; + }, + onSuccess: (_, { projectId }) => { + queryClient.invalidateQueries({ + queryKey: kmipKeys.getKmipClientsByProjectId({ projectId }) + }); + } + }); +}; + +export const useDeleteKmipClients = () => { + const queryClient = useQueryClient(); + return useMutation({ + mutationFn: async ({ id }: TDeleteKmipClient) => { + const { data } = await apiRequest.delete(`/api/v1/kmip/clients/${id}`); + + return data; + }, + onSuccess: (_, { projectId }) => { + queryClient.invalidateQueries({ + queryKey: kmipKeys.getKmipClientsByProjectId({ projectId }) + }); + } + }); +}; + +export const useGenerateKmipClientCertificate = () => { + return useMutation({ + mutationFn: async (payload: TGenerateKmipClientCertificate) => { + const { data } = await apiRequest.post( + `/api/v1/kmip/clients/${payload.clientId}/certificates`, + payload + ); + + return data; + } + }); +}; + +export const useSetupOrgKmip = (orgId: string) => { + const queryClient = useQueryClient(); + return useMutation({ + mutationFn: async (payload: TSetupOrgKmipDTO) => { + await apiRequest.post("/api/v1/kmip", payload); + }, + onSuccess: () => { + queryClient.invalidateQueries({ queryKey: kmipKeys.getOrgKmip(orgId) }); + } + }); +}; diff --git a/frontend/src/hooks/api/kmip/queries.tsx b/frontend/src/hooks/api/kmip/queries.tsx new file mode 100644 index 0000000000..f734420a4f --- /dev/null +++ b/frontend/src/hooks/api/kmip/queries.tsx @@ -0,0 +1,69 @@ +import { useQuery, UseQueryOptions } from "@tanstack/react-query"; + +import { apiRequest } from "@app/config/request"; + +import { OrderByDirection } from "../generic/types"; +import { + KmipClientOrderBy, + OrgKmipConfig, + TListProjectKmipClientsDTO, + TProjectKmipClientList +} from "./types"; + +export const kmipKeys = { + getKmipClientsByProjectId: ({ projectId, ...filters }: TListProjectKmipClientsDTO) => + [projectId, filters] as const, + getOrgKmip: (orgId: string) => [{ orgId }, "org-kmip-config"] as const +}; + +export const useGetKmipClientsByProjectId = ( + { + projectId, + offset = 0, + limit = 100, + orderBy = KmipClientOrderBy.Name, + orderDirection = OrderByDirection.ASC, + search = "" + }: TListProjectKmipClientsDTO, + options?: Omit< + UseQueryOptions< + TProjectKmipClientList, + unknown, + TProjectKmipClientList, + ReturnType + >, + "queryKey" | "queryFn" + > +) => { + return useQuery({ + queryKey: kmipKeys.getKmipClientsByProjectId({ + projectId, + offset, + limit, + orderBy, + orderDirection, + search + }), + queryFn: async () => { + const { data } = await apiRequest.get("/api/v1/kmip/clients", { + params: { projectId, offset, limit, search, orderBy, orderDirection } + }); + + return data; + }, + enabled: Boolean(projectId) && (options?.enabled ?? true), + placeholderData: (previousData) => previousData, + ...options + }); +}; + +export const useGetOrgKmipConfig = (orgId: string) => { + return useQuery({ + queryKey: kmipKeys.getOrgKmip(orgId), + queryFn: async () => { + const { data } = await apiRequest.get("/api/v1/kmip"); + + return data; + } + }); +}; diff --git a/frontend/src/hooks/api/kmip/types.ts b/frontend/src/hooks/api/kmip/types.ts new file mode 100644 index 0000000000..c406a44d13 --- /dev/null +++ b/frontend/src/hooks/api/kmip/types.ts @@ -0,0 +1,81 @@ +import { CertKeyAlgorithm } from "../certificates/enums"; +import { OrderByDirection } from "../generic/types"; + +export enum KmipPermission { + Create = "create", + Locate = "locate", + Check = "check", + Get = "get", + GetAttributes = "get-attributes", + Activate = "activate", + Revoke = "revoke", + Destroy = "destroy", + Register = "register" +} + +export type TKmipClient = { + id: string; + name: string; + description?: string; + permissions: KmipPermission[]; + projectId: string; +}; + +type ProjectRef = { projectId: string }; +type KeyRef = { id: string }; + +export type TCreateKmipClient = Pick & + ProjectRef; + +export type TUpdateKmipClient = KeyRef & + Partial> & + ProjectRef; + +export type TProjectKmipClientList = { + kmipClients: TKmipClient[]; + totalCount: number; +}; + +export type TGenerateKmipClientCertificate = { + keyAlgorithm: CertKeyAlgorithm; + ttl: string; + clientId: string; +}; + +export type KmipClientCertificate = { + serialNumber: string; + certificate: string; + certificateChain: string; + privateKey: string; +}; + +export type TDeleteKmipClient = KeyRef & ProjectRef; + +export type TListProjectKmipClientsDTO = { + projectId: string; + offset?: number; + limit?: number; + orderBy?: KmipClientOrderBy; + orderDirection?: OrderByDirection; + search?: string; +}; + +export enum KmipClientOrderBy { + Name = "name" +} + +export type OrgKmipConfig = { + serverCertificateChain: string; + clientCertificateChain: string; +}; + +export type TSetupOrgKmipDTO = { + caKeyAlgorithm: CertKeyAlgorithm; +}; + +export type OrgKmipServerCert = { + serialNumber: string; + certificate: string; + certificateChain: string; + privateKey: string; +}; diff --git a/frontend/src/hooks/api/subscriptions/types.ts b/frontend/src/hooks/api/subscriptions/types.ts index c67b3f8c34..3129dd508e 100644 --- a/frontend/src/hooks/api/subscriptions/types.ts +++ b/frontend/src/hooks/api/subscriptions/types.ts @@ -46,4 +46,5 @@ export type SubscriptionPlan = { pkiEst: boolean; enforceMfa: boolean; projectTemplates: boolean; + kmip: boolean; }; diff --git a/frontend/src/layouts/ProjectLayout/ProjectLayout.tsx b/frontend/src/layouts/ProjectLayout/ProjectLayout.tsx index d2d49822f0..e6b39369fc 100644 --- a/frontend/src/layouts/ProjectLayout/ProjectLayout.tsx +++ b/frontend/src/layouts/ProjectLayout/ProjectLayout.tsx @@ -105,6 +105,20 @@ export const ProjectLayout = () => { )} )} + {isCmek && ( + + {({ isActive }) => ( + + KMIP + + )} + + )} {isSSH && ( { + const { t } = useTranslation(); + + return ( +
+ + {t("common.head-title", { title: "KMS" })} + +
+
+ + + + +
+
+
+ ); +}; diff --git a/frontend/src/pages/kms/KmipPage/components/CreateKmipClientCertificateModal.tsx b/frontend/src/pages/kms/KmipPage/components/CreateKmipClientCertificateModal.tsx new file mode 100644 index 0000000000..2cb56e7f9f --- /dev/null +++ b/frontend/src/pages/kms/KmipPage/components/CreateKmipClientCertificateModal.tsx @@ -0,0 +1,147 @@ +import { Controller, useForm } from "react-hook-form"; +import { zodResolver } from "@hookform/resolvers/zod"; +import { z } from "zod"; + +import { createNotification } from "@app/components/notifications"; +import { + Button, + FormControl, + Input, + Modal, + ModalClose, + ModalContent, + Select, + SelectItem +} from "@app/components/v2"; +import { certKeyAlgorithms } from "@app/hooks/api/certificates/constants"; +import { CertKeyAlgorithm } from "@app/hooks/api/certificates/enums"; +import { useGenerateKmipClientCertificate } from "@app/hooks/api/kmip"; +import { KmipClientCertificate, TKmipClient } from "@app/hooks/api/kmip/types"; + +const formSchema = z.object({ + keyAlgorithm: z.nativeEnum(CertKeyAlgorithm), + ttl: z.string() +}); + +export type FormData = z.infer; + +type Props = { + isOpen: boolean; + onOpenChange: (isOpen: boolean) => void; + kmipClient?: TKmipClient | null; + displayNewClientCertificate: (certificate: KmipClientCertificate) => void; +}; + +type FormProps = Pick & { + onComplete: () => void; +}; + +const KmipClientCertificateForm = ({ + displayNewClientCertificate, + kmipClient, + onComplete +}: FormProps) => { + const { mutateAsync: createKmipClientCertificate } = useGenerateKmipClientCertificate(); + + const { + control, + handleSubmit, + formState: { isSubmitting } + } = useForm({ + resolver: zodResolver(formSchema) + }); + + const handleKmipClientSubmit = async (payload: FormData) => { + if (!kmipClient) { + return; + } + + const certificate = await createKmipClientCertificate({ + ...payload, + clientId: kmipClient?.id + }); + + createNotification({ + text: "Successfully created KMIP client certificate", + type: "success" + }); + + displayNewClientCertificate(certificate); + onComplete(); + }; + + return ( +
+ ( + + + + )} + /> + ( + + + + )} + /> +
+ + + + +
+ + ); +}; + +export const CreateKmipClientCertificateModal = ({ + isOpen, + onOpenChange, + kmipClient, + displayNewClientCertificate +}: Props) => { + return ( + + + onOpenChange(false)} + displayNewClientCertificate={displayNewClientCertificate} + kmipClient={kmipClient} + /> + + + ); +}; diff --git a/frontend/src/pages/kms/KmipPage/components/DeleteKmipClientModal.tsx b/frontend/src/pages/kms/KmipPage/components/DeleteKmipClientModal.tsx new file mode 100644 index 0000000000..d9f833543b --- /dev/null +++ b/frontend/src/pages/kms/KmipPage/components/DeleteKmipClientModal.tsx @@ -0,0 +1,53 @@ +import { createNotification } from "@app/components/notifications"; +import { DeleteActionModal } from "@app/components/v2"; +import { useDeleteKmipClients } from "@app/hooks/api/kmip"; +import { TKmipClient } from "@app/hooks/api/kmip/types"; + +type Props = { + kmipClient: TKmipClient; + isOpen: boolean; + onOpenChange: (isOpen: boolean) => void; +}; + +export const DeleteKmipClientModal = ({ isOpen, onOpenChange, kmipClient }: Props) => { + const deleteKmipClients = useDeleteKmipClients(); + + if (!kmipClient) return null; + + const { id, projectId, name } = kmipClient; + + const handleDeleteKmipClient = async () => { + try { + await deleteKmipClients.mutateAsync({ + id, + projectId + }); + + createNotification({ + text: "KMIP client successfully deleted", + type: "success" + }); + + onOpenChange(false); + } catch (err) { + console.error(err); + const error = err as any; + const text = error?.response?.data?.message ?? "Failed to delete KMIP client"; + + createNotification({ + text, + type: "error" + }); + } + }; + + return ( + + ); +}; diff --git a/frontend/src/pages/kms/KmipPage/components/KmipClientCertificateModal.tsx b/frontend/src/pages/kms/KmipPage/components/KmipClientCertificateModal.tsx new file mode 100644 index 0000000000..4f87446825 --- /dev/null +++ b/frontend/src/pages/kms/KmipPage/components/KmipClientCertificateModal.tsx @@ -0,0 +1,19 @@ +import { Modal, ModalContent } from "@app/components/v2"; +import { KmipClientCertificate } from "@app/hooks/api/kmip/types"; +import { CertificateContent } from "@app/pages/cert-manager/CertificatesPage/components/CertificatesTab/components/CertificateContent"; + +type Props = { + isOpen: boolean; + onOpenChange: (isOpen: boolean) => void; + certificate: KmipClientCertificate; +}; + +export const KmipClientCertificateModal = ({ isOpen, onOpenChange, certificate }: Props) => { + return ( + + + + + + ); +}; diff --git a/frontend/src/pages/kms/KmipPage/components/KmipClientModal.tsx b/frontend/src/pages/kms/KmipPage/components/KmipClientModal.tsx new file mode 100644 index 0000000000..4bab2c10ff --- /dev/null +++ b/frontend/src/pages/kms/KmipPage/components/KmipClientModal.tsx @@ -0,0 +1,194 @@ +import { Controller, useForm } from "react-hook-form"; +import { zodResolver } from "@hookform/resolvers/zod"; +import { z } from "zod"; + +import { createNotification } from "@app/components/notifications"; +import { + Button, + Checkbox, + FormControl, + Input, + Modal, + ModalClose, + ModalContent, + TextArea +} from "@app/components/v2"; +import { useWorkspace } from "@app/context"; +import { useCreateKmipClient, useUpdateKmipClient } from "@app/hooks/api/kmip"; +import { KmipPermission, TKmipClient } from "@app/hooks/api/kmip/types"; + +const KMIP_PERMISSIONS_OPTIONS = [ + { value: KmipPermission.Check, label: "Check" }, + { value: KmipPermission.Create, label: "Create" }, + { value: KmipPermission.Get, label: "Get" }, + { value: KmipPermission.Locate, label: "Locate" }, + { value: KmipPermission.Destroy, label: "Destroy" }, + { value: KmipPermission.Activate, label: "Activate" }, + { value: KmipPermission.Revoke, label: "Revoke" }, + { value: KmipPermission.GetAttributes, label: "Get Attributes" }, + { value: KmipPermission.Register, label: "Register" } +] as const; + +const formSchema = z.object({ + name: z.string().trim().min(1), + description: z.string().max(500).optional(), + permissions: z.object({ + [KmipPermission.Check]: z.boolean().optional(), + [KmipPermission.Create]: z.boolean().optional(), + [KmipPermission.Get]: z.boolean().optional(), + [KmipPermission.Locate]: z.boolean().optional(), + [KmipPermission.Destroy]: z.boolean().optional(), + [KmipPermission.Activate]: z.boolean().optional(), + [KmipPermission.GetAttributes]: z.boolean().optional(), + [KmipPermission.Revoke]: z.boolean().optional(), + [KmipPermission.Register]: z.boolean().optional() + }) +}); + +export type FormData = z.infer; + +type Props = { + isOpen: boolean; + onOpenChange: (isOpen: boolean) => void; + kmipClient?: TKmipClient | null; +}; + +type FormProps = Pick & { + onComplete: () => void; +}; + +const KmipClientForm = ({ onComplete, kmipClient }: FormProps) => { + const createKmipClient = useCreateKmipClient(); + const updateKmipClient = useUpdateKmipClient(); + const { currentWorkspace } = useWorkspace(); + const projectId = currentWorkspace.id; + const isUpdate = !!kmipClient; + + const { + control, + handleSubmit, + register, + formState: { isSubmitting, errors } + } = useForm({ + resolver: zodResolver(formSchema), + defaultValues: { + name: kmipClient?.name, + description: kmipClient?.description, + permissions: Object.fromEntries((kmipClient?.permissions || []).map((name) => [name, true])) + } + }); + + const handleKmipClientSubmit = async ({ permissions, name, description }: FormData) => { + const mutation = isUpdate + ? updateKmipClient.mutateAsync({ + id: kmipClient.id, + projectId, + name, + description, + permissions: Object.entries(permissions) + .filter(([, value]) => value) + .map(([key]) => key as KmipPermission) + }) + : createKmipClient.mutateAsync({ + projectId, + name, + description, + permissions: Object.entries(permissions) + .filter(([, value]) => value) + .map(([key]) => key as KmipPermission) + }); + + try { + await mutation; + createNotification({ + text: `Successfully ${isUpdate ? "updated" : "added"} KMIP client`, + type: "success" + }); + onComplete(); + } catch (err) { + console.error(err); + createNotification({ + text: `Failed to ${isUpdate ? "update" : "add"} KMIP client`, + type: "error" + }); + } + }; + + return ( +
+ + + + +