diff --git a/backend/src/@types/fastify.d.ts b/backend/src/@types/fastify.d.ts index c02347038..1cb11545c 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 2dad77392..27f6b894c 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 000000000..ae63fbfe4 --- /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 9bcfdd49f..737193836 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 000000000..a42d94a98 --- /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 000000000..eb8f31bfb --- /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 000000000..e75d76413 --- /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 000000000..66e5dcbd6 --- /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 3ead85530..3c5d1cad8 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 5f931440c..0567c76ef 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 000000000..45ab5044b --- /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 000000000..c9899c98e --- /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 e124c1f45..b7bd8b7d4 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 000000000..989b53324 --- /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 000000000..2650ebad0 --- /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 000000000..80af88e1c --- /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 000000000..66c3a1d46 --- /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 000000000..a6567fafd --- /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 000000000..98626aad1 --- /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 000000000..c7ae6b728 --- /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 000000000..a259a79b8 --- /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 c54daa3a6..cab19a530 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 0242377b0..2b4dbe49e 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 c72008057..03d1747d4 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 657e9ce3a..4389c3866 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 6503165f6..1e247dcde 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 10da81bf5..155b52cfb 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/server/routes/v1/auth-router.ts b/backend/src/server/routes/v1/auth-router.ts index 46f856105..04ca958c6 100644 --- a/backend/src/server/routes/v1/auth-router.ts +++ b/backend/src/server/routes/v1/auth-router.ts @@ -2,10 +2,9 @@ import jwt from "jsonwebtoken"; import { z } from "zod"; import { getConfig } from "@app/lib/config/env"; -import { NotFoundError, UnauthorizedError } from "@app/lib/errors"; import { authRateLimit, writeLimit } from "@app/server/config/rateLimiter"; import { verifyAuth } from "@app/server/plugins/auth/verify-auth"; -import { AuthMode, AuthModeRefreshJwtTokenPayload, AuthTokenType } from "@app/services/auth/auth-type"; +import { AuthMode, AuthTokenType } from "@app/services/auth/auth-type"; export const registerAuthRoutes = async (server: FastifyZodProvider) => { server.route({ @@ -21,18 +20,19 @@ export const registerAuthRoutes = async (server: FastifyZodProvider) => { }) } }, - onRequest: verifyAuth([AuthMode.JWT], { requireOrg: false }), handler: async (req, res) => { + const { decodedToken } = await server.services.authToken.validateRefreshToken(req.cookies.jid); const appCfg = getConfig(); - if (req.auth.authMode === AuthMode.JWT) { - await server.services.login.logout(req.permission.id, req.auth.tokenVersionId); - } + + await server.services.login.logout(decodedToken.userId, decodedToken.tokenVersionId); + void res.cookie("jid", "", { httpOnly: true, path: "/", sameSite: "strict", secure: appCfg.HTTPS_ENABLED }); + return { message: "Successfully logged out" }; } }); @@ -69,37 +69,8 @@ export const registerAuthRoutes = async (server: FastifyZodProvider) => { } }, handler: async (req) => { - const refreshToken = req.cookies.jid; + const { decodedToken, tokenVersion } = await server.services.authToken.validateRefreshToken(req.cookies.jid); const appCfg = getConfig(); - if (!refreshToken) - throw new NotFoundError({ - name: "AuthTokenNotFound", - message: "Failed to find refresh token" - }); - - const decodedToken = jwt.verify(refreshToken, appCfg.AUTH_SECRET) as AuthModeRefreshJwtTokenPayload; - if (decodedToken.authTokenType !== AuthTokenType.REFRESH_TOKEN) - throw new UnauthorizedError({ - message: "The token provided is not a refresh token", - name: "InvalidToken" - }); - - const tokenVersion = await server.services.authToken.getUserTokenSessionById( - decodedToken.tokenVersionId, - decodedToken.userId - ); - if (!tokenVersion) - throw new UnauthorizedError({ - message: "Valid token version not found", - name: "InvalidToken" - }); - - if (decodedToken.refreshVersion !== tokenVersion.refreshVersion) { - throw new UnauthorizedError({ - message: "Token version mismatch", - name: "InvalidToken" - }); - } const token = jwt.sign( { diff --git a/backend/src/services/auth-token/auth-token-service.ts b/backend/src/services/auth-token/auth-token-service.ts index d15fa4543..2468e3c8a 100644 --- a/backend/src/services/auth-token/auth-token-service.ts +++ b/backend/src/services/auth-token/auth-token-service.ts @@ -1,6 +1,7 @@ import crypto from "node:crypto"; import bcrypt from "bcrypt"; +import jwt from "jsonwebtoken"; import { Knex } from "knex"; import { TAuthTokens, TAuthTokenSessions } from "@app/db/schemas"; @@ -8,7 +9,7 @@ import { getConfig } from "@app/lib/config/env"; import { ForbiddenRequestError, NotFoundError, UnauthorizedError } from "@app/lib/errors"; import { TOrgMembershipDALFactory } from "@app/services/org-membership/org-membership-dal"; -import { AuthModeJwtTokenPayload } from "../auth/auth-type"; +import { AuthModeJwtTokenPayload, AuthModeRefreshJwtTokenPayload, AuthTokenType } from "../auth/auth-type"; import { TUserDALFactory } from "../user/user-dal"; import { TTokenDALFactory } from "./auth-token-dal"; import { TCreateTokenForUserDTO, TIssueAuthTokenDTO, TokenType, TValidateTokenForUserDTO } from "./auth-token-types"; @@ -150,6 +151,40 @@ export const tokenServiceFactory = ({ tokenDAL, userDAL, orgMembershipDAL }: TAu const revokeAllMySessions = async (userId: string) => tokenDAL.deleteTokenSession({ userId }); + const validateRefreshToken = async (refreshToken?: string) => { + const appCfg = getConfig(); + if (!refreshToken) + throw new NotFoundError({ + name: "AuthTokenNotFound", + message: "Failed to find refresh token" + }); + + const decodedToken = jwt.verify(refreshToken, appCfg.AUTH_SECRET) as AuthModeRefreshJwtTokenPayload; + + if (decodedToken.authTokenType !== AuthTokenType.REFRESH_TOKEN) + throw new UnauthorizedError({ + message: "The token provided is not a refresh token", + name: "InvalidToken" + }); + + const tokenVersion = await getUserTokenSessionById(decodedToken.tokenVersionId, decodedToken.userId); + + if (!tokenVersion) + throw new UnauthorizedError({ + message: "Valid token version not found", + name: "InvalidToken" + }); + + if (decodedToken.refreshVersion !== tokenVersion.refreshVersion) { + throw new UnauthorizedError({ + message: "Token version mismatch", + name: "InvalidToken" + }); + } + + return { decodedToken, tokenVersion }; + }; + // to parse jwt identity in inject identity plugin const fnValidateJwtIdentity = async (token: AuthModeJwtTokenPayload) => { const session = await tokenDAL.findOneTokenSession({ @@ -188,6 +223,7 @@ export const tokenServiceFactory = ({ tokenDAL, userDAL, orgMembershipDAL }: TAu clearTokenSessionById, getTokenSessionByUser, revokeAllMySessions, + validateRefreshToken, fnValidateJwtIdentity, getUserTokenSessionById }; diff --git a/backend/src/services/auth/auth-type.ts b/backend/src/services/auth/auth-type.ts index 05412a73a..497414a60 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 16e7dcf49..1840652ca 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 1768a5011..45ad5963c 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/integration-auth/integration-sync-secret.ts b/backend/src/services/integration-auth/integration-sync-secret.ts index 249307f8d..93318762b 100644 --- a/backend/src/services/integration-auth/integration-sync-secret.ts +++ b/backend/src/services/integration-auth/integration-sync-secret.ts @@ -2257,7 +2257,9 @@ const syncSecretsFlyio = async ({ } `; - await request.post( + type TFlyioErrors = { message: string }[]; + + const setSecretsResp = await request.post<{ errors?: TFlyioErrors }>( IntegrationUrls.FLYIO_API_URL, { query: SetSecrets, @@ -2279,6 +2281,10 @@ const syncSecretsFlyio = async ({ } ); + if (setSecretsResp.data.errors?.length) { + throw new Error(JSON.stringify(setSecretsResp.data.errors)); + } + // get secrets interface FlyioSecret { name: string; diff --git a/backend/src/services/kms/kms-key-dal.ts b/backend/src/services/kms/kms-key-dal.ts index 69a175471..a0dd12191 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 babba4cb2..cfd64a89a 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 f655d4b5d..8be0b29fc 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 637bd904d..b96772c91 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 79fe1fd7f..67a3d3275 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 000000000..b0c397895 --- /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/identities/oidc-auth/terraform-cloud.mdx b/docs/documentation/platform/identities/oidc-auth/terraform-cloud.mdx new file mode 100644 index 000000000..5105981ec --- /dev/null +++ b/docs/documentation/platform/identities/oidc-auth/terraform-cloud.mdx @@ -0,0 +1,87 @@ +--- +title: "Terraform Cloud" +description: "How to authenticate with Infisical from Terraform Cloud using OIDC." +--- + +This guide will walk you through setting up Terraform Cloud to inject a [workload identity token](https://developer.hashicorp.com/terraform/cloud-docs/workspaces/dynamic-provider-credentials/workload-identity-tokens) and use it for OIDC-based authentication with the Infisical Terraform provider. You'll start by creating a machine identity in Infisical, then configure Terraform Cloud to pass the injected token into your Terraform runs. + + + + Follow the instructions [in this documentation](/documentation/platform/identities/oidc-auth/general) to create a machine identity with OIDC auth. Infisical OIDC configuration values for Terraform Cloud: + 1. Set the OIDC Discovery URL to https://app.terraform.io. + 2. Set the Issuer to https://app.terraform.io. + 3. Configure the Audience to match the value you will use for **TFC_WORKLOAD_IDENTITY_AUDIENCE** in Terraform Cloud for the next step. + + + To view all possible claims available from Terraform cloud, visit [HashiCorp’s documentation](https://developer.hashicorp.com/terraform/cloud-docs/workspaces/dynamic-provider-credentials/workload-identity-tokens#token-structure). + + + + + + + 1. **Navigate to your workspace** in Terraform Cloud. + 2. **Add a workspace variable** named `TFC_WORKLOAD_IDENTITY_AUDIENCE`: + - **Key**: `TFC_WORKLOAD_IDENTITY_AUDIENCE` + - **Value**: For example, `my-infisical-audience` + - **Category**: Environment + + > **Important**: + > - The presence of `TFC_WORKLOAD_IDENTITY_AUDIENCE` is required for Terraform Cloud to inject a token. + > - If you are self-hosting HCP Terraform agents, ensure they are **v1.7.0 or above**. + + Once set, Terraform Cloud will inject a workload identity token into the run environment as `TFC_WORKLOAD_IDENTITY_TOKEN`. + + + If you need multiple tokens (each with a different audience), create additional variables: + + ``` + TFC_WORKLOAD_IDENTITY_AUDIENCE_[YOUR_TAG_HERE] + ``` + + For example: + - `TFC_WORKLOAD_IDENTITY_AUDIENCE_INFISICAL` + - `TFC_WORKLOAD_IDENTITY_AUDIENCE_OTHER_SERVICE` + + Terraform Cloud will then inject: + - `TFC_WORKLOAD_IDENTITY_TOKEN_INFISICAL` + - `TFC_WORKLOAD_IDENTITY_TOKEN_OTHER_SERVICE` + + > **Note**: + > - The `[YOUR_TAG_HERE]` can only contain letters, numbers, and underscores. + > - You **cannot** use the reserved keyword `TYPE`. + > - Generating multiple tokens requires **v1.12.0 or later** if you are self-hosting agents. + + + + + If you are running on self-hosted HCP Terraform agents, you must use v1.7.0 or later to enable token injection. If you need to generate multiple tokens, you must use v1.12.0 or later. + + + + In your Terraform configuration, reference the injected token by name. For example: + + ```hcl + provider "infisical" { + host = "https://app.infisical.com" + + auth = { + oidc = { + identity_id = "" + # This must match the environment variable Terraform injects: + token_environment_variable_name = "TFC_WORKLOAD_IDENTITY_TOKEN" + } + } + } + ``` + + - **`host`**: Defaults to `https://app.infisical.com`. Override if using a self-hosted Infisical instance. + - **`identity_id`**: The OIDC identity ID from Infisical. + - **`token_environment_variable_name`**: Must match the injected variable name from Terraform Cloud. If using single token, use `TFC_WORKLOAD_IDENTITY_TOKEN`. If using multiple tokens, choose the one you want to use (e.g., `TFC_WORKLOAD_IDENTITY_TOKEN_INFISICAL`). + + + 1. Run a plan and apply in Terraform Cloud. + 2. Verify the Infisical provider authenticates successfully without issues. If you run into authentication errors, double-check the Infisical identity has the correct roles/permissions in Infisical. + + + diff --git a/docs/documentation/platform/kms/kmip.mdx b/docs/documentation/platform/kms/kmip.mdx new file mode 100644 index 000000000..1e025d51a --- /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/documentation/setup/networking.mdx b/docs/documentation/setup/networking.mdx new file mode 100644 index 000000000..4a666b73c --- /dev/null +++ b/docs/documentation/setup/networking.mdx @@ -0,0 +1,36 @@ +--- +title: "Networking" +sidebarTitle: "Networking" +description: "Network configuration details for Infisical Cloud" +--- + +## Overview + +When integrating your infrastructure with Infisical Cloud, you may need to configure network access controls. This page provides the IP addresses that Infisical uses to communicate with your services. + +## Egress IP Addresses + +Infisical Cloud operates from two regions: US and EU. If your infrastructure has strict network policies, you may need to allow traffic from Infisical by adding the following IP addresses to your ingress rules. These are the egress IPs Infisical uses when making outbound requests to your services. + +### US Region + +To allow connections from Infisical US, add these IP addresses to your ingress rules: + +- `3.213.63.16` +- `54.164.68.7` + +### EU Region + +To allow connections from Infisical EU, add these IP addresses to your ingress rules: + +- `3.77.89.19` +- `3.125.209.189` + +## Common Use Cases + +You may need to allow Infisical’s egress IPs if your services require inbound connections for: + +- Secret rotation - When Infisical needs to send requests to your systems to automatically rotate credentials +- Dynamic secrets - When Infisical generates and manages temporary credentials for your cloud services +- Secret integrations - When syncing secrets with third-party services like Azure Key Vault +- Native authentication with machine identities - When using methods like Kubernetes authentication 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 000000000..498fc2012 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 000000000..93d4c7f84 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 000000000..45eb8c830 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 000000000..d6f9fc2ad 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 000000000..40b49f570 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 000000000..5038c2a87 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 000000000..794a91d42 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 000000000..5e6a5145d 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 000000000..ed8a0988c 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 000000000..6fc6dd7ce 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 000000000..5a77878e5 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 b62af6533..62e40e1c9 100644 --- a/docs/mint.json +++ b/docs/mint.json @@ -85,6 +85,10 @@ "documentation/guides/microsoft-power-apps", "documentation/guides/organization-structure" ] + }, + { + "group": "Setup", + "pages": ["documentation/setup/networking"] } ] }, @@ -120,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" ] }, { @@ -232,7 +237,8 @@ "documentation/platform/identities/oidc-auth/general", "documentation/platform/identities/oidc-auth/github", "documentation/platform/identities/oidc-auth/circleci", - "documentation/platform/identities/oidc-auth/gitlab" + "documentation/platform/identities/oidc-auth/gitlab", + "documentation/platform/identities/oidc-auth/terraform-cloud" ] }, "documentation/platform/mfa", @@ -633,7 +639,8 @@ "api-reference/endpoints/oidc-auth/attach", "api-reference/endpoints/oidc-auth/retrieve", "api-reference/endpoints/oidc-auth/update", - "api-reference/endpoints/oidc-auth/revoke" + "api-reference/endpoints/oidc-auth/revoke", + "integrations/frameworks/terraform-cloud" ] }, { diff --git a/docs/sdks/languages/go.mdx b/docs/sdks/languages/go.mdx index 18fabf64e..bbe8febca 100644 --- a/docs/sdks/languages/go.mdx +++ b/docs/sdks/languages/go.mdx @@ -4,8 +4,6 @@ sidebarTitle: "Go" icon: "golang" --- - - If you're working with Go Lang, the official [Infisical Go SDK](https://github.com/infisical/go-sdk) package is the easiest way to fetch and work with secrets for your application. - [Package](https://pkg.go.dev/github.com/infisical/go-sdk) @@ -57,7 +55,9 @@ func main() { This example demonstrates how to use the Infisical Go SDK in a simple Go application. The application retrieves a secret named `API_KEY` from the `dev` environment of the `YOUR_PROJECT_ID` project. - We do not recommend hardcoding your [Machine Identity Tokens](/platform/identities/overview). Setting it as an environment variable would be best. + We do not recommend hardcoding your [Machine Identity + Tokens](/platform/identities/overview). Setting it as an environment variable + would be best. # Installation @@ -95,6 +95,10 @@ client := infisical.NewInfisicalClient(context.Background(), infisical.Config{ Whether or not to suppress logs such as warnings from the token refreshing process. Defaults to false if not specified. + + + Defines how long certain responses should be cached in memory, in seconds. When set to a positive value, responses from specific methods (like secret fetching) will be cached for this duration. Set to 0 to disable caching. + @@ -140,6 +144,7 @@ Call `.Auth().UniversalAuthLogin()` with empty arguments to use the following en - `INFISICAL_UNIVERSAL_AUTH_CLIENT_SECRET` - Your machine identity client secret. **Using the SDK directly** + ```go _, err := client.Auth().UniversalAuthLogin("CLIENT_ID", "CLIENT_SECRET") @@ -150,9 +155,12 @@ if err != nil { ``` #### GCP ID Token Auth + - Please note that this authentication method will only work if you're running your application on Google Cloud Platform. - Please [read more](/documentation/platform/identities/gcp-auth) about this authentication method. + Please note that this authentication method will only work if you're running + your application on Google Cloud Platform. Please [read + more](/documentation/platform/identities/gcp-auth) about this authentication + method. **Using environment variables** @@ -162,6 +170,7 @@ Call `.Auth().GcpIdTokenAuthLogin()` with empty arguments to use the following e - `INFISICAL_GCP_AUTH_IDENTITY_ID` - Your Infisical Machine Identity ID. **Using the SDK directly** + ```go _, err := client.Auth().GcpIdTokenAuthLogin("YOUR_MACHINE_IDENTITY_ID") @@ -181,6 +190,7 @@ Call `.Auth().GcpIamAuthLogin()` with empty arguments to use the following envir - `INFISICAL_GCP_IAM_SERVICE_ACCOUNT_KEY_FILE_PATH` - The path to your GCP service account key file. **Using the SDK directly** + ```go _, err = client.Auth().GcpIamAuthLogin("MACHINE_IDENTITY_ID", "SERVICE_ACCOUNT_KEY_FILE_PATH") @@ -191,9 +201,12 @@ if err != nil { ``` #### AWS IAM Auth + - Please note that this authentication method will only work if you're running your application on AWS. - Please [read more](/documentation/platform/identities/aws-auth) about this authentication method. + Please note that this authentication method will only work if you're running + your application on AWS. Please [read + more](/documentation/platform/identities/aws-auth) about this authentication + method. **Using environment variables** @@ -203,6 +216,7 @@ Call `.Auth().AwsIamAuthLogin()` with empty arguments to use the following envir - `INFISICAL_AWS_IAM_AUTH_IDENTITY_ID` - Your Infisical Machine Identity ID. **Using the SDK directly** + ```go _, err = client.Auth().AwsIamAuthLogin("MACHINE_IDENTITY_ID") @@ -212,11 +226,13 @@ if err != nil { } ``` - #### Azure Auth + - Please note that this authentication method will only work if you're running your application on Azure. - Please [read more](/documentation/platform/identities/azure-auth) about this authentication method. + Please note that this authentication method will only work if you're running + your application on Azure. Please [read + more](/documentation/platform/identities/azure-auth) about this authentication + method. **Using environment variables** @@ -226,6 +242,7 @@ Call `.Auth().AzureAuthLogin()` with empty arguments to use the following enviro - `INFISICAL_AZURE_AUTH_IDENTITY_ID` - Your Infisical Machine Identity ID. **Using the SDK directly** + ```go _, err = client.Auth().AzureAuthLogin("MACHINE_IDENTITY_ID") @@ -236,9 +253,12 @@ if err != nil { ``` #### Kubernetes Auth + - Please note that this authentication method will only work if you're running your application on Kubernetes. - Please [read more](/documentation/platform/identities/kubernetes-auth) about this authentication method. + Please note that this authentication method will only work if you're running + your application on Kubernetes. Please [read + more](/documentation/platform/identities/kubernetes-auth) about this + authentication method. **Using environment variables** @@ -249,6 +269,7 @@ Call `.Auth().KubernetesAuthLogin()` with empty arguments to use the following e - `INFISICAL_KUBERNETES_SERVICE_ACCOUNT_TOKEN_PATH_ENV_NAME` - The environment variable name that contains the path to the service account token. This is optional and will default to `/var/run/secrets/kubernetes.io/serviceaccount/token`. **Using the SDK directly** + ```go // Service account token path will default to /var/run/secrets/kubernetes.io/serviceaccount/token if empty value is passed _, err = client.Auth().KubernetesAuthLogin("MACHINE_IDENTITY_ID", "SERVICE_ACCOUNT_TOKEN_PATH") @@ -262,6 +283,7 @@ if err != nil { ## Working With Secrets ### List Secrets + `client.Secrets().List(options)` Retrieve all secrets within the Infisical project and environment that client is connected to. @@ -311,7 +333,9 @@ secrets, err := client.Secrets().List(infisical.ListSecretsOptions{ ### + ### Retrieve Secret + `client.Secrets().Retrieve(options)` Retrieve a secret from Infisical. By default `Secrets().Retrieve()` fetches and returns a shared secret. @@ -327,27 +351,31 @@ secret, err := client.Secrets().Retrieve(infisical.RetrieveSecretOptions{ ### Parameters - - - The key of the secret to retrieve. - - - The project ID where the secret lives in. - - - The slug name (dev, prod, etc) of the environment from where secrets should be fetched from. - - - The path from where secret should be fetched from. - - - The type of the secret. Valid options are "shared" or "personal". If not specified, the default value is "shared". - - + + + The key of the secret to retrieve. + + + The project ID where the secret lives in. + + + The slug name (dev, prod, etc) of the environment from where secrets + should be fetched from. + + + The path from where secret should be fetched from. + + + The type of the secret. Valid options are "shared" or "personal". If not + specified, the default value is "shared". + + ### + ### Create Secret + `client.Secrets().Create(options)` Create a new secret in Infisical. @@ -363,36 +391,38 @@ secret, err := client.Secrets().Create(infisical.CreateSecretOptions{ }) ``` - ### Parameters - - - The key of the secret to create. - - - The value of the secret. - - - A comment for the secret. - - - The project ID where the secret lives in. - - - The slug name (dev, prod, etc) of the environment from where secrets should be fetched from. - - - The path from where secret should be created. - - - The type of the secret. Valid options are "shared" or "personal". If not specified, the default value is "shared". - - + + + The key of the secret to create. + + + The value of the secret. + + + A comment for the secret. + + + The project ID where the secret lives in. + + + The slug name (dev, prod, etc) of the environment from where secrets + should be fetched from. + + + The path from where secret should be created. + + + The type of the secret. Valid options are "shared" or "personal". If not + specified, the default value is "shared". + + ### + ### Update Secret `client.Secrets().Update(options)` @@ -412,33 +442,42 @@ secret, err := client.Secrets().Update(infisical.UpdateSecretOptions{ ### Parameters - - - The key of the secret to update. - - - The new value of the secret. - - - Whether or not to skip multiline encoding for the new secret value. - - - The project ID where the secret lives in. - - - The slug name (dev, prod, etc) of the environment from where secrets should be fetched from. - - - The path from where secret should be updated. - - - The type of the secret. Valid options are "shared" or "personal". If not specified, the default value is "shared". - - + + + The key of the secret to update. + + + The new value of the secret. + + + Whether or not to skip multiline encoding for the new secret value. + + + The project ID where the secret lives in. + + + The slug name (dev, prod, etc) of the environment from where secrets + should be fetched from. + + + The path from where secret should be updated. + + + The type of the secret. Valid options are "shared" or "personal". If not + specified, the default value is "shared". + + ### + ### Delete Secret + `client.Secrets().Delete(options)` Delete a secret in Infisical. @@ -454,30 +493,33 @@ secret, err := client.Secrets().Delete(infisical.DeleteSecretOptions{ ### Parameters - - - The key of the secret to update. - - - The project ID where the secret lives in. - - - The slug name (dev, prod, etc) of the environment from where secrets should be fetched from. - - - The path from where secret should be deleted. - - - The type of the secret. Valid options are "shared" or "personal". If not specified, the default value is "shared". - - + + + The key of the secret to update. + + + The project ID where the secret lives in. + + + The slug name (dev, prod, etc) of the environment from where secrets + should be fetched from. + + + The path from where secret should be deleted. + + + The type of the secret. Valid options are "shared" or "personal". If not + specified, the default value is "shared". + + ## Working With folders - ### + ### List Folders + `client.Folders().List(options)` Retrieve all within the Infisical project and environment that client is connected to. @@ -510,7 +552,9 @@ folders, err := client.Folders().List(infisical.ListFoldersOptions{ ### + ### Create Folder + `client.Folders().Create(options)` Create a new folder in Infisical. @@ -527,25 +571,27 @@ folder, err := client.Folders().Create(infisical.CreateFolderOptions{ ### Parameters - - - The ID of the project where the folder will be created. - - - The slug name (dev, prod, etc) of the environment where the folder will be created. - - - The path to create the folder in. The root path is `/`. - - - The name of the folder to create. - - + + + The ID of the project where the folder will be created. + + + The slug name (dev, prod, etc) of the environment where the folder will be + created. + + + The path to create the folder in. The root path is `/`. + + + The name of the folder to create. + + - ### + ### Update Folder + `client.Folders().Update(options)` Update an existing folder in Infisical. @@ -563,27 +609,30 @@ folder, err := client.Folders().Update(infisical.UpdateFolderOptions{ ### Parameters - - - The ID of the project where the folder will be updated. - - - The slug name (dev, prod, etc) of the environment from where the folder lives in. - - - The path from where the folder should be updated. - - - The ID of the folder to update. - - - The new name of the folder. - - + + + The ID of the project where the folder will be updated. + + + The slug name (dev, prod, etc) of the environment from where the folder + lives in. + + + The path from where the folder should be updated. + + + The ID of the folder to update. + + + The new name of the folder. + + ### + ### Delete Folder + `client.Folders().Delete(options)` Delete a folder in Infisical. @@ -620,6 +669,5 @@ deletedFolder, err := client.Folders().Delete(infisical.DeleteFolderOptions{ The path from where the folder should be deleted. + - - diff --git a/frontend/public/lotties/key-user.json b/frontend/public/lotties/key-user.json new file mode 100644 index 000000000..268b10a6f --- /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 6fc2a7d00..3a9c926a2 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 ea5003c8c..46329abc6 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 009bc9451..69bcd4f99 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 279e69889..d368a949f 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 70d00dd74..fb9d8c385 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 000000000..50adf4fe2 --- /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 a51ca7902..39c351e38 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 a83a8a429..ed25c6e4a 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 338671cab..8acaa215a 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 000000000..f4e57d7cc --- /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 000000000..af86c1fe0 --- /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 000000000..f734420a4 --- /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 000000000..c406a44d1 --- /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 c67b3f8c3..3129dd508 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/hooks/api/users/queries.tsx b/frontend/src/hooks/api/users/queries.tsx index 88486ee48..06cde4d34 100644 --- a/frontend/src/hooks/api/users/queries.tsx +++ b/frontend/src/hooks/api/users/queries.tsx @@ -3,6 +3,7 @@ import { AxiosError } from "axios"; import { apiRequest } from "@app/config/request"; import { SessionStorageKeys } from "@app/const"; +import { queryClient as qc } from "@app/hooks/api/reactQuery"; import { APIKeyDataV2 } from "../apiKeys/types"; import { MfaMethod } from "../auth/types"; @@ -27,7 +28,6 @@ import { export const fetchUserDetails = async () => { const { data } = await apiRequest.get<{ user: User & UserEnc }>("/api/v1/user"); - return data.user; }; @@ -278,30 +278,33 @@ export const useRegisterUserAction = () => { }); }; -export const useLogoutUser = (keepQueryClient?: boolean) => { - const queryClient = useQueryClient(); - return useMutation({ - mutationFn: async () => { - await apiRequest.post("/api/v1/auth/logout"); - }, - onSuccess: () => { - setAuthToken(""); - // Delete the cookie by not setting a value; Alternatively clear the local storage - localStorage.removeItem("protectedKey"); - localStorage.removeItem("protectedKeyIV"); - localStorage.removeItem("protectedKeyTag"); - localStorage.removeItem("publicKey"); - localStorage.removeItem("encryptedPrivateKey"); - localStorage.removeItem("iv"); - localStorage.removeItem("tag"); - localStorage.removeItem("PRIVATE_KEY"); - localStorage.removeItem("orgData.id"); - sessionStorage.removeItem(SessionStorageKeys.CLI_TERMINAL_TOKEN); +export const logoutUser = async () => { + await apiRequest.post("/api/v1/auth/logout"); +}; - if (!keepQueryClient) { - queryClient.clear(); - } - } +// Utility function to clear session storage and query cache +export const clearSession = (keepQueryClient?: boolean) => { + setAuthToken(""); // Clear authentication token + localStorage.removeItem("protectedKey"); + localStorage.removeItem("protectedKeyIV"); + localStorage.removeItem("protectedKeyTag"); + localStorage.removeItem("publicKey"); + localStorage.removeItem("encryptedPrivateKey"); + localStorage.removeItem("iv"); + localStorage.removeItem("tag"); + localStorage.removeItem("PRIVATE_KEY"); + localStorage.removeItem("orgData.id"); + sessionStorage.removeItem(SessionStorageKeys.CLI_TERMINAL_TOKEN); + + if (!keepQueryClient) { + qc.clear(); // Clear React Query cache + } +}; + +export const useLogoutUser = (keepQueryClient?: boolean) => { + return useMutation({ + mutationFn: logoutUser, + onSuccess: () => clearSession(keepQueryClient) }); }; diff --git a/frontend/src/layouts/ProjectLayout/ProjectLayout.tsx b/frontend/src/layouts/ProjectLayout/ProjectLayout.tsx index d2d49822f..e6b39369f 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 && ( { console.log("Error - Not logged in yet"); } }; + if (isLoggedIn()) { handleRedirects(); } diff --git a/frontend/src/pages/auth/SignUpInvitePage/SignUpInvitePage.tsx b/frontend/src/pages/auth/SignUpInvitePage/SignUpInvitePage.tsx index 3252bc9f9..d3106e341 100644 --- a/frontend/src/pages/auth/SignUpInvitePage/SignUpInvitePage.tsx +++ b/frontend/src/pages/auth/SignUpInvitePage/SignUpInvitePage.tsx @@ -250,20 +250,24 @@ export const SignupInvitePage = () => { setStep(2); } else { const redirectExistingUser = async () => { - const { token: mfaToken, isMfaEnabled } = await selectOrganization({ - organizationId - }); + try { + const { token: mfaToken, isMfaEnabled } = await selectOrganization({ + organizationId + }); - if (isMfaEnabled) { - SecurityClient.setMfaToken(mfaToken); - toggleShowMfa.on(); - setMfaSuccessCallback(() => redirectExistingUser); - return; + if (isMfaEnabled) { + SecurityClient.setMfaToken(mfaToken); + toggleShowMfa.on(); + setMfaSuccessCallback(() => redirectExistingUser); + return; + } + + // user will be redirected to dashboard + // if not logged in gets kicked out to login + await navigateUserToOrg(navigate, organizationId); + } catch (err) { + navigate({ to: "/login" }); } - - // user will be redirected to dashboard - // if not logged in gets kicked out to login - await navigateUserToOrg(navigate, organizationId); }; await redirectExistingUser(); diff --git a/frontend/src/pages/kms/KmipPage/KmipPage.tsx b/frontend/src/pages/kms/KmipPage/KmipPage.tsx new file mode 100644 index 000000000..6297c5eef --- /dev/null +++ b/frontend/src/pages/kms/KmipPage/KmipPage.tsx @@ -0,0 +1,36 @@ +import { Helmet } from "react-helmet"; +import { useTranslation } from "react-i18next"; + +import { ProjectPermissionCan } from "@app/components/permissions"; +import { PageHeader } from "@app/components/v2"; +import { ProjectPermissionKmipActions, ProjectPermissionSub } from "@app/context"; + +import { KmipClientTable } from "./components/KmipClientTable"; + +export const KmipPage = () => { + 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 000000000..2cb56e7f9 --- /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 000000000..d9f833543 --- /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 000000000..4f8744682 --- /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 000000000..4bab2c10f --- /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 ( +
+ + + + +