diff --git a/backend/src/ee/routes/v1/external-kms-router.ts b/backend/src/ee/routes/v1/external-kms-router.ts index a48e28e3d..b46b525fe 100644 --- a/backend/src/ee/routes/v1/external-kms-router.ts +++ b/backend/src/ee/routes/v1/external-kms-router.ts @@ -4,15 +4,10 @@ import { ExternalKmsSchema, KmsKeysSchema } from "@app/db/schemas"; import { EventType } from "@app/ee/services/audit-log/audit-log-types"; import { ExternalKmsAwsSchema, - ExternalKmsGcpCredentialSchema, ExternalKmsGcpSchema, ExternalKmsInputSchema, - ExternalKmsInputUpdateSchema, - KmsGcpKeyFetchAuthType, - KmsProviders, - TExternalKmsGcpCredentialSchema + ExternalKmsInputUpdateSchema } from "@app/ee/services/external-kms/providers/model"; -import { NotFoundError } from "@app/lib/errors"; 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"; @@ -293,67 +288,4 @@ export const registerExternalKmsRouter = async (server: FastifyZodProvider) => { return { externalKms }; } }); - - server.route({ - method: "POST", - url: "/gcp/keys", - config: { - rateLimit: writeLimit - }, - schema: { - body: z.discriminatedUnion("authMethod", [ - z.object({ - authMethod: z.literal(KmsGcpKeyFetchAuthType.Credential), - region: z.string().trim().min(1), - credential: ExternalKmsGcpCredentialSchema - }), - z.object({ - authMethod: z.literal(KmsGcpKeyFetchAuthType.Kms), - region: z.string().trim().min(1), - kmsId: z.string().trim().min(1) - }) - ]), - response: { - 200: z.object({ - keys: z.string().array() - }) - } - }, - onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), - handler: async (req) => { - const { region, authMethod } = req.body; - let credentialJson: TExternalKmsGcpCredentialSchema | undefined; - - if (authMethod === KmsGcpKeyFetchAuthType.Credential) { - credentialJson = req.body.credential; - } else if (authMethod === KmsGcpKeyFetchAuthType.Kms) { - const externalKms = await server.services.externalKms.findById({ - actor: req.permission.type, - actorId: req.permission.id, - actorAuthMethod: req.permission.authMethod, - actorOrgId: req.permission.orgId, - id: req.body.kmsId - }); - - if (!externalKms || externalKms.external.provider !== KmsProviders.Gcp) { - throw new NotFoundError({ message: "KMS not found or not of type GCP" }); - } - - credentialJson = externalKms.external.providerInput.credential as TExternalKmsGcpCredentialSchema; - } - - if (!credentialJson) { - throw new NotFoundError({ - message: "Something went wrong while fetching the GCP credential, please check inputs and try again" - }); - } - - const results = await server.services.externalKms.fetchGcpKeys({ - credential: credentialJson, - gcpRegion: region - }); - - return results; - } - }); }; diff --git a/backend/src/ee/routes/v1/external-kms-routers/aws-kms-router.ts b/backend/src/ee/routes/v1/external-kms-routers/aws-kms-router.ts new file mode 100644 index 000000000..518b7947e --- /dev/null +++ b/backend/src/ee/routes/v1/external-kms-routers/aws-kms-router.ts @@ -0,0 +1,12 @@ +import { ExternalKmsAwsSchema, KmsProviders } from "@app/ee/services/external-kms/providers/model"; + +import { registerExternalKmsEndpoints } from "./external-kms-endpoints"; + +export const registerAwsKmsRouter = async (server: FastifyZodProvider) => { + registerExternalKmsEndpoints({ + server, + provider: KmsProviders.Aws, + createSchema: ExternalKmsAwsSchema, + updateSchema: ExternalKmsAwsSchema.partial() + }); +}; diff --git a/backend/src/ee/routes/v1/external-kms-routers/external-kms-endpoints.ts b/backend/src/ee/routes/v1/external-kms-routers/external-kms-endpoints.ts new file mode 100644 index 000000000..47b4947f2 --- /dev/null +++ b/backend/src/ee/routes/v1/external-kms-routers/external-kms-endpoints.ts @@ -0,0 +1,288 @@ +import { z } from "zod"; + +import { ExternalKmsSchema, KmsKeysSchema } from "@app/db/schemas"; +import { EventType } from "@app/ee/services/audit-log/audit-log-types"; +import { + KmsProviders, + SanitizedExternalKmsAwsSchema, + SanitizedExternalKmsGcpSchema, + TExternalKmsInputSchema, + TExternalKmsInputUpdateSchema +} from "@app/ee/services/external-kms/providers/model"; +import { crypto } from "@app/lib/crypto/cryptography"; +import { BadRequestError } from "@app/lib/errors"; +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"; + +const sanitizedExternalSchema = KmsKeysSchema.extend({ + externalKms: ExternalKmsSchema.pick({ + id: true, + status: true, + statusDetails: true, + provider: true + }).extend({ + configuration: z.union([SanitizedExternalKmsAwsSchema, SanitizedExternalKmsGcpSchema]), + credentialsHash: z.string().optional() + }) +}); + +export const registerExternalKmsEndpoints = < + T extends { type: KmsProviders; inputs: TExternalKmsInputSchema["inputs"] } +>({ + server, + provider, + createSchema, + updateSchema +}: { + server: FastifyZodProvider; + provider: T["type"]; + createSchema: z.ZodType; + updateSchema: z.ZodType>; +}) => { + server.route({ + method: "GET", + url: "/:id", + config: { + rateLimit: readLimit + }, + schema: { + params: z.object({ + id: z.string().trim().min(1) + }), + response: { + 200: sanitizedExternalSchema + } + }, + onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), + handler: async (req) => { + const externalKms = await server.services.externalKms.findById({ + actor: req.permission.type, + actorId: req.permission.id, + actorAuthMethod: req.permission.authMethod, + actorOrgId: req.permission.orgId, + id: req.params.id + }); + + // Validate that the KMS is of the expected provider type + if (externalKms.external.provider !== provider) { + throw new BadRequestError({ + message: `KMS provider mismatch. Expected ${provider}, got ${externalKms.external.provider}` + }); + } + + await server.services.auditLog.createAuditLog({ + ...req.auditLogInfo, + orgId: req.permission.orgId, + event: { + type: EventType.GET_KMS, + metadata: { + kmsId: externalKms.id, + name: externalKms.name + } + } + }); + + const { + external: { providerInput: configuration, ...externalKmsData }, + ...rest + } = externalKms; + + const credentialsHash = crypto.nativeCrypto + .createHash("sha256") + .update(externalKmsData.encryptedProviderInputs) + .digest("hex"); + return { ...rest, externalKms: { ...externalKmsData, configuration, credentialsHash } }; + } + }); + + server.route({ + method: "POST", + url: "/", + config: { + rateLimit: writeLimit + }, + schema: { + body: z.object({ + name: z.string().min(1).trim().toLowerCase(), + description: z.string().trim().optional(), + configuration: createSchema + }), + response: { + 200: sanitizedExternalSchema + } + }, + onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), + handler: async (req) => { + const { name, description, configuration } = req.body as { + name: string; + description?: string; + configuration: T["inputs"]; + }; + + const providerInput = { + type: provider, + inputs: configuration + } as TExternalKmsInputSchema; + + const externalKms = await server.services.externalKms.create({ + actor: req.permission.type, + actorId: req.permission.id, + actorAuthMethod: req.permission.authMethod, + actorOrgId: req.permission.orgId, + name, + provider: providerInput, + description + }); + + await server.services.auditLog.createAuditLog({ + ...req.auditLogInfo, + orgId: req.permission.orgId, + event: { + type: EventType.CREATE_KMS, + metadata: { + kmsId: externalKms.id, + provider, + name, + description + } + } + }); + + const { + external: { providerInput: externalKmsConfiguration, ...externalKmsData }, + ...rest + } = externalKms; + const credentialsHash = crypto.nativeCrypto + .createHash("sha256") + .update(externalKmsData.encryptedProviderInputs) + .digest("hex"); + return { ...rest, externalKms: { ...externalKmsData, configuration: externalKmsConfiguration, credentialsHash } }; + } + }); + + server.route({ + method: "PATCH", + url: "/:id", + config: { + rateLimit: writeLimit + }, + schema: { + params: z.object({ + id: z.string().trim().min(1) + }), + body: z.object({ + name: z.string().min(1).trim().toLowerCase().optional(), + description: z.string().trim().optional(), + configuration: updateSchema.optional() + }), + response: { + 200: sanitizedExternalSchema + } + }, + onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), + handler: async (req) => { + const { name, description, configuration } = req.body as { + name?: string; + description?: string; + configuration: Partial; + }; + + const providerInput = { + type: provider, + inputs: configuration + } as TExternalKmsInputUpdateSchema; + + const externalKms = await server.services.externalKms.updateById({ + actor: req.permission.type, + actorId: req.permission.id, + actorAuthMethod: req.permission.authMethod, + actorOrgId: req.permission.orgId, + name, + provider: providerInput, + description, + id: req.params.id + }); + + await server.services.auditLog.createAuditLog({ + ...req.auditLogInfo, + orgId: req.permission.orgId, + event: { + type: EventType.UPDATE_KMS, + metadata: { + kmsId: externalKms.id, + provider, + name, + description + } + } + }); + + const { + external: { providerInput: externalKmsConfiguration, ...externalKmsData }, + ...rest + } = externalKms; + const credentialsHash = crypto.nativeCrypto + .createHash("sha256") + .update(externalKmsData.encryptedProviderInputs) + .digest("hex"); + return { ...rest, externalKms: { ...externalKmsData, configuration: externalKmsConfiguration, credentialsHash } }; + } + }); + + server.route({ + method: "DELETE", + url: "/:id", + config: { + rateLimit: writeLimit + }, + schema: { + params: z.object({ + id: z.string().trim().min(1) + }), + response: { + 200: sanitizedExternalSchema + } + }, + onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), + handler: async (req) => { + const externalKms = await server.services.externalKms.deleteById({ + actor: req.permission.type, + actorId: req.permission.id, + actorAuthMethod: req.permission.authMethod, + actorOrgId: req.permission.orgId, + id: req.params.id + }); + + // Validate that the KMS is of the expected provider type + if (externalKms.external.provider !== provider) { + throw new BadRequestError({ + message: `KMS provider mismatch. Expected ${provider}, got ${externalKms.external.provider}` + }); + } + + await server.services.auditLog.createAuditLog({ + ...req.auditLogInfo, + orgId: req.permission.orgId, + event: { + type: EventType.DELETE_KMS, + metadata: { + kmsId: externalKms.id, + name: externalKms.name + } + } + }); + + const { + external: { providerInput: configuration, ...externalKmsData }, + ...rest + } = externalKms; + const credentialsHash = crypto.nativeCrypto + .createHash("sha256") + .update(externalKmsData.encryptedProviderInputs) + .digest("hex"); + + return { ...rest, externalKms: { ...externalKmsData, configuration, credentialsHash } }; + } + }); +}; diff --git a/backend/src/ee/routes/v1/external-kms-routers/gcp-kms-router.ts b/backend/src/ee/routes/v1/external-kms-routers/gcp-kms-router.ts new file mode 100644 index 000000000..97b600c10 --- /dev/null +++ b/backend/src/ee/routes/v1/external-kms-routers/gcp-kms-router.ts @@ -0,0 +1,88 @@ +import { z } from "zod"; + +import { + ExternalKmsGcpCredentialSchema, + ExternalKmsGcpSchema, + KmsGcpKeyFetchAuthType, + KmsProviders, + TExternalKmsGcpCredentialSchema +} from "@app/ee/services/external-kms/providers/model"; +import { NotFoundError } from "@app/lib/errors"; +import { writeLimit } from "@app/server/config/rateLimiter"; +import { verifyAuth } from "@app/server/plugins/auth/verify-auth"; +import { AuthMode } from "@app/services/auth/auth-type"; + +import { registerExternalKmsEndpoints } from "./external-kms-endpoints"; + +export const registerGcpKmsRouter = async (server: FastifyZodProvider) => { + registerExternalKmsEndpoints({ + server, + provider: KmsProviders.Gcp, + createSchema: ExternalKmsGcpSchema, + updateSchema: ExternalKmsGcpSchema.partial() + }); + + server.route({ + method: "POST", + url: "/keys", + config: { + rateLimit: writeLimit + }, + schema: { + body: z.discriminatedUnion("authMethod", [ + z.object({ + authMethod: z.literal(KmsGcpKeyFetchAuthType.Credential), + region: z.string().trim().min(1), + credential: ExternalKmsGcpCredentialSchema + }), + z.object({ + authMethod: z.literal(KmsGcpKeyFetchAuthType.Kms), + region: z.string().trim().min(1), + kmsId: z.string().trim().min(1) + }) + ]), + response: { + 200: z.object({ + keys: z.string().array() + }) + } + }, + onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), + handler: async (req) => { + const { region, authMethod } = req.body; + let credentialJson: TExternalKmsGcpCredentialSchema | undefined; + + if (authMethod === KmsGcpKeyFetchAuthType.Credential && "credential" in req.body) { + credentialJson = req.body.credential; + } else if (authMethod === KmsGcpKeyFetchAuthType.Kms && "kmsId" in req.body) { + const externalKms = await server.services.externalKms.findById({ + actor: req.permission.type, + actorId: req.permission.id, + actorAuthMethod: req.permission.authMethod, + actorOrgId: req.permission.orgId, + id: req.body.kmsId + }); + + if (!externalKms || externalKms.external.provider !== KmsProviders.Gcp) { + throw new NotFoundError({ message: "KMS not found or not of type GCP" }); + } + + const providerInput = externalKms.external.providerInput as { credential: TExternalKmsGcpCredentialSchema }; + credentialJson = providerInput.credential; + } + + if (!credentialJson) { + throw new NotFoundError({ + message: "Something went wrong while fetching the GCP credential, please check inputs and try again" + }); + } + + const results = await server.services.externalKms.fetchGcpKeys({ + credential: credentialJson, + gcpRegion: region + }); + + return results; + } + }); +}; diff --git a/backend/src/ee/routes/v1/external-kms-routers/index.ts b/backend/src/ee/routes/v1/external-kms-routers/index.ts new file mode 100644 index 000000000..da70b0f59 --- /dev/null +++ b/backend/src/ee/routes/v1/external-kms-routers/index.ts @@ -0,0 +1,9 @@ +import { KmsProviders } from "@app/ee/services/external-kms/providers/model"; + +import { registerAwsKmsRouter } from "./aws-kms-router"; +import { registerGcpKmsRouter } from "./gcp-kms-router"; + +export const EXTERNAL_KMS_REGISTER_ROUTER_MAP: Record Promise> = { + [KmsProviders.Aws]: registerAwsKmsRouter, + [KmsProviders.Gcp]: registerGcpKmsRouter +}; diff --git a/backend/src/ee/routes/v1/index.ts b/backend/src/ee/routes/v1/index.ts index 367c2833c..bc3a4f602 100644 --- a/backend/src/ee/routes/v1/index.ts +++ b/backend/src/ee/routes/v1/index.ts @@ -12,6 +12,8 @@ import { registerDynamicSecretLeaseRouter } from "./dynamic-secret-lease-router" import { registerKubernetesDynamicSecretLeaseRouter } from "./dynamic-secret-lease-routers/kubernetes-lease-router"; import { registerDynamicSecretRouter } from "./dynamic-secret-router"; import { registerExternalKmsRouter } from "./external-kms-router"; + +import { EXTERNAL_KMS_REGISTER_ROUTER_MAP } from "./external-kms-routers"; import { registerGatewayRouter } from "./gateway-router"; import { registerGithubOrgSyncRouter } from "./github-org-sync-router"; import { registerGroupRouter } from "./group-router"; @@ -162,9 +164,19 @@ export const registerV1EERoutes = async (server: FastifyZodProvider) => { { prefix: "/additional-privilege" } ); - await server.register(registerExternalKmsRouter, { - prefix: "/external-kms" - }); + await server.register( + async (externalKmsRouter) => { + await externalKmsRouter.register(registerExternalKmsRouter); + + // Provider-specific endpoints + await Promise.all( + Object.entries(EXTERNAL_KMS_REGISTER_ROUTER_MAP).map(([provider, router]) => + externalKmsRouter.register(router, { prefix: `/${provider}` }) + ) + ); + }, + { prefix: "/external-kms" } + ); await server.register(registerIdentityTemplateRouter, { prefix: "/identity-templates" }); await server.register(registerProjectTemplateRouter, { prefix: "/project-templates" }); diff --git a/backend/src/ee/services/external-kms/external-kms-service.ts b/backend/src/ee/services/external-kms/external-kms-service.ts index 9614f3298..eb595ee02 100644 --- a/backend/src/ee/services/external-kms/external-kms-service.ts +++ b/backend/src/ee/services/external-kms/external-kms-service.ts @@ -24,7 +24,13 @@ import { } from "./external-kms-types"; import { AwsKmsProviderFactory } from "./providers/aws-kms"; import { GcpKmsProviderFactory } from "./providers/gcp-kms"; -import { ExternalKmsAwsSchema, ExternalKmsGcpSchema, KmsProviders, TExternalKmsGcpSchema } from "./providers/model"; +import { + ExternalKmsAwsSchema, + ExternalKmsGcpSchema, + KmsProviders, + TExternalKmsAwsSchema, + TExternalKmsGcpSchema +} from "./providers/model"; type TExternalKmsServiceFactoryDep = { externalKmsDAL: TExternalKmsDALFactory; @@ -72,6 +78,7 @@ export const externalKmsServiceFactory = ({ const kmsName = name ? slugify(name) : slugify(alphaNumericNanoId(8).toLowerCase()); let sanitizedProviderInput = ""; + let sanitizedProviderInputObject: TExternalKmsAwsSchema | TExternalKmsGcpSchema; switch (provider.type) { case KmsProviders.Aws: { @@ -88,9 +95,18 @@ export const externalKmsServiceFactory = ({ try { // if missing kms key this generate a new kms key id and returns new provider input const newProviderInput = await externalKms.generateInputKmsKey(); + sanitizedProviderInputObject = newProviderInput; sanitizedProviderInput = JSON.stringify(newProviderInput); await externalKms.validateConnection(); + } catch (error) { + if (error instanceof BadRequestError) { + throw error; + } + + throw new BadRequestError({ + message: error instanceof Error ? `AWS error: ${error.message}` : "Failed to validate AWS connection" + }); } finally { await externalKms.cleanup(); } @@ -101,7 +117,16 @@ export const externalKmsServiceFactory = ({ const externalKms = await GcpKmsProviderFactory({ inputs: provider.inputs }); try { await externalKms.validateConnection(); + sanitizedProviderInputObject = provider.inputs; sanitizedProviderInput = JSON.stringify(provider.inputs); + } catch (error) { + if (error instanceof BadRequestError) { + throw error; + } + + throw new BadRequestError({ + message: error instanceof Error ? `GCP error: ${error.message}` : "Failed to validate GCP connection" + }); } finally { await externalKms.cleanup(); } @@ -139,7 +164,10 @@ export const externalKmsServiceFactory = ({ }, tx ); - return { ...kms, external: externalKmsCfg }; + return { + ...kms, + external: { ...externalKmsCfg, providerInput: sanitizedProviderInputObject } + }; }); return externalKms; @@ -179,6 +207,7 @@ export const externalKmsServiceFactory = ({ if (!externalKmsDoc) throw new NotFoundError({ message: `External KMS with ID '${kmsId}' not found` }); let sanitizedProviderInput = ""; + let sanitizedProviderInputObject: TExternalKmsAwsSchema | TExternalKmsGcpSchema; const { encryptor: orgDataKeyEncryptor, decryptor: orgDataKeyDecryptor } = await kmsService.createCipherPairWithDataKey({ type: KmsDataKey.Organization, @@ -199,7 +228,16 @@ export const externalKmsServiceFactory = ({ const externalKms = await AwsKmsProviderFactory({ inputs: updatedProviderInput }); try { await externalKms.validateConnection(); + sanitizedProviderInputObject = updatedProviderInput; sanitizedProviderInput = JSON.stringify(updatedProviderInput); + } catch (error) { + if (error instanceof BadRequestError) { + throw error; + } + + throw new BadRequestError({ + message: error instanceof Error ? `AWS error: ${error.message}` : "Failed to validate AWS connection" + }); } finally { await externalKms.cleanup(); } @@ -214,7 +252,16 @@ export const externalKmsServiceFactory = ({ const externalKms = await GcpKmsProviderFactory({ inputs: updatedProviderInput }); try { await externalKms.validateConnection(); + sanitizedProviderInputObject = updatedProviderInput; sanitizedProviderInput = JSON.stringify(updatedProviderInput); + } catch (error) { + if (error instanceof BadRequestError) { + throw error; + } + + throw new BadRequestError({ + message: error instanceof Error ? `GCP error: ${error.message}` : "Failed to validate GCP connection" + }); } finally { await externalKms.cleanup(); } @@ -234,14 +281,17 @@ export const externalKmsServiceFactory = ({ } const externalKms = await externalKmsDAL.transaction(async (tx) => { - const kms = await kmsDAL.updateById( - kmsDoc.id, - { - description, - name: kmsName - }, - tx - ); + let kms = kmsDoc; + if (kmsName || description) { + kms = await kmsDAL.updateById( + kmsDoc.id, + { + description, + name: kmsName + }, + tx + ); + } if (encryptedProviderInputs) { const externalKmsCfg = await externalKmsDAL.updateById( externalKmsDoc.id, @@ -250,9 +300,9 @@ export const externalKmsServiceFactory = ({ }, tx ); - return { ...kms, external: externalKmsCfg }; + return { ...kms, external: { ...externalKmsCfg, providerInput: sanitizedProviderInputObject } }; } - return { ...kms, external: externalKmsDoc }; + return { ...kms, external: { ...externalKmsDoc, providerInput: sanitizedProviderInputObject } }; }); return externalKms; @@ -273,9 +323,40 @@ export const externalKmsServiceFactory = ({ const externalKmsDoc = await externalKmsDAL.findOne({ kmsKeyId: kmsDoc.id }); if (!externalKmsDoc) throw new NotFoundError({ message: `External KMS with ID '${kmsId}' not found` }); + let decryptedProviderInputObject: TExternalKmsAwsSchema | TExternalKmsGcpSchema; + + const { decryptor: orgDataKeyDecryptor } = await kmsService.createCipherPairWithDataKey({ + type: KmsDataKey.Organization, + orgId: actorOrgId + }); + + const decryptedProviderInputBlob = orgDataKeyDecryptor({ + cipherTextBlob: externalKmsDoc.encryptedProviderInputs + }); + + switch (externalKmsDoc.provider) { + case KmsProviders.Aws: { + const decryptedProviderInput = await ExternalKmsAwsSchema.parseAsync( + JSON.parse(decryptedProviderInputBlob.toString()) + ); + decryptedProviderInputObject = decryptedProviderInput; + break; + } + case KmsProviders.Gcp: { + const decryptedProviderInput = await ExternalKmsGcpSchema.parseAsync( + JSON.parse(decryptedProviderInputBlob.toString()) + ); + + decryptedProviderInputObject = decryptedProviderInput; + break; + } + default: + break; + } + const externalKms = await externalKmsDAL.transaction(async (tx) => { const kms = await kmsDAL.deleteById(kmsDoc.id, tx); - return { ...kms, external: externalKmsDoc }; + return { ...kms, external: { ...externalKmsDoc, providerInput: decryptedProviderInputObject } }; }); return externalKms; @@ -393,6 +474,14 @@ export const externalKmsServiceFactory = ({ const externalKms = await GcpKmsProviderFactory({ inputs: { credential, gcpRegion, keyName: "" } }); try { return await externalKms.getKeysList(); + } catch (error) { + if (error instanceof BadRequestError) { + throw error; + } + + throw new BadRequestError({ + message: error instanceof Error ? `GCP error: ${error.message}` : "Failed to fetch GCP keys" + }); } finally { await externalKms.cleanup(); } diff --git a/backend/src/ee/services/external-kms/providers/aws-kms.ts b/backend/src/ee/services/external-kms/providers/aws-kms.ts index 2c248992f..82e95f360 100644 --- a/backend/src/ee/services/external-kms/providers/aws-kms.ts +++ b/backend/src/ee/services/external-kms/providers/aws-kms.ts @@ -3,6 +3,7 @@ import { AssumeRoleCommand, STSClient } from "@aws-sdk/client-sts"; import { CustomAWSHasher } from "@app/lib/aws/hashing"; import { crypto } from "@app/lib/crypto/cryptography"; +import { BadRequestError } from "@app/lib/errors"; import { ExternalKmsAwsSchema, KmsAwsCredentialType, TExternalKmsAwsSchema, TExternalKmsProviderFns } from "./model"; @@ -22,7 +23,7 @@ const getAwsKmsClient = async (providerInputs: TExternalKmsAwsSchema) => { }); const response = await stsClient.send(command); if (!response.Credentials?.AccessKeyId || !response.Credentials?.SecretAccessKey) - throw new Error("Failed to assume role"); + throw new BadRequestError({ message: "Failed to assume role" }); const kmsClient = new KMSClient({ region: providerInputs.awsRegion, @@ -67,7 +68,7 @@ export const AwsKmsProviderFactory = async ({ inputs }: AwsKmsProviderArgs): Pro const command = new CreateKeyCommand({ Tags: [{ TagKey: "author", TagValue: "infisical" }] }); const kmsKey = await awsClient.send(command); - if (!kmsKey.KeyMetadata?.KeyId) throw new Error("Failed to generate kms key"); + if (!kmsKey.KeyMetadata?.KeyId) throw new BadRequestError({ message: "Failed to generate kms key" }); const updatedProviderInputs = await ExternalKmsAwsSchema.parseAsync({ ...providerInputs, diff --git a/backend/src/ee/services/external-kms/providers/model.ts b/backend/src/ee/services/external-kms/providers/model.ts index 6cb78a34e..08a9a3fc7 100644 --- a/backend/src/ee/services/external-kms/providers/model.ts +++ b/backend/src/ee/services/external-kms/providers/model.ts @@ -19,27 +19,31 @@ export enum KmsGcpKeyFetchAuthType { Kms = "kmsId" } +const AwsConnectionAssumeRoleCredentialsSchema = z.object({ + assumeRoleArn: z.string().trim().min(1).describe("AWS user role to be assumed by infisical"), + externalId: z + .string() + .trim() + .min(1) + .optional() + .describe("AWS assume role external id for further security in authentication") +}); + +const AwsConnectionAccessTokenCredentialsSchema = z.object({ + accessKey: z.string().trim().min(1).describe("AWS user account access key"), + secretKey: z.string().trim().min(1).describe("AWS user account secret key") +}); + export const ExternalKmsAwsSchema = z.object({ credential: z .discriminatedUnion("type", [ z.object({ type: z.literal(KmsAwsCredentialType.AccessKey), - data: z.object({ - accessKey: z.string().trim().min(1).describe("AWS user account access key"), - secretKey: z.string().trim().min(1).describe("AWS user account secret key") - }) + data: AwsConnectionAccessTokenCredentialsSchema }), z.object({ type: z.literal(KmsAwsCredentialType.AssumeRole), - data: z.object({ - assumeRoleArn: z.string().trim().min(1).describe("AWS user role to be assumed by infisical"), - externalId: z - .string() - .trim() - .min(1) - .optional() - .describe("AWS assume role external id for furthur security in authentication") - }) + data: AwsConnectionAssumeRoleCredentialsSchema }) ]) .describe("AWS credential information to connect"), @@ -52,6 +56,22 @@ export const ExternalKmsAwsSchema = z.object({ }); export type TExternalKmsAwsSchema = z.infer; +export const SanitizedExternalKmsAwsSchema = ExternalKmsAwsSchema.extend({ + credential: z.discriminatedUnion("type", [ + z.object({ + type: z.literal(KmsAwsCredentialType.AccessKey), + data: AwsConnectionAccessTokenCredentialsSchema.pick({ accessKey: true }) + }), + z.object({ + type: z.literal(KmsAwsCredentialType.AssumeRole), + data: AwsConnectionAssumeRoleCredentialsSchema.pick({ + assumeRoleArn: true, + externalId: true + }) + }) + ]) +}); + export const ExternalKmsGcpCredentialSchema = z.object({ type: z.literal(KmsGcpCredentialType.ServiceAccount), project_id: z.string().min(1), @@ -75,6 +95,8 @@ export const ExternalKmsGcpSchema = z.object({ }); export type TExternalKmsGcpSchema = z.infer; +export const SanitizedExternalKmsGcpSchema = ExternalKmsGcpSchema.pick({ gcpRegion: true, keyName: true }); + const ExternalKmsGcpClientSchema = ExternalKmsGcpSchema.pick({ gcpRegion: true }).extend({ credential: ExternalKmsGcpCredentialSchema }); diff --git a/backend/src/services/kms/kms-service.ts b/backend/src/services/kms/kms-service.ts index 8f868978d..a63f0d41d 100644 --- a/backend/src/services/kms/kms-service.ts +++ b/backend/src/services/kms/kms-service.ts @@ -253,7 +253,7 @@ export const kmsServiceFactory = ({ } if (!org.kmsDefaultKeyId) { - throw new Error("Invalid organization KMS"); + throw new BadRequestError({ message: "Invalid organization KMS" }); } return org.kmsDefaultKeyId; @@ -292,7 +292,7 @@ export const kmsServiceFactory = ({ let externalKms: TExternalKmsProviderFns; if (!kmsDoc.orgKms.id || !kmsDoc.orgKms.encryptedDataKey) { - throw new Error("Invalid organization KMS"); + throw new BadRequestError({ message: "Invalid organization KMS" }); } // The idea is external kms connection info is encrypted by an org default KMS @@ -338,7 +338,7 @@ export const kmsServiceFactory = ({ break; } default: - throw new Error("Invalid KMS provider."); + throw new BadRequestError({ message: "Invalid KMS provider." }); } return async ({ cipherTextBlob }: Pick) => { @@ -509,7 +509,7 @@ export const kmsServiceFactory = ({ if (kmsDoc.externalKms) { let externalKms: TExternalKmsProviderFns; if (!kmsDoc.orgKms.id || !kmsDoc.orgKms.encryptedDataKey) { - throw new Error("Invalid organization KMS"); + throw new BadRequestError({ message: "Invalid organization KMS" }); } const orgKmsDecryptor = await decryptWithKmsKey({ @@ -550,7 +550,7 @@ export const kmsServiceFactory = ({ break; } default: - throw new Error("Invalid KMS provider."); + throw new BadRequestError({ message: "Invalid KMS provider." }); } return async ({ plainText }: Pick) => { @@ -651,7 +651,7 @@ export const kmsServiceFactory = ({ } if (!org.kmsEncryptedDataKey) { - throw new Error("Invalid organization KMS"); + throw new BadRequestError({ message: "Invalid organization KMS" }); } const kmsDecryptor = await decryptWithKmsKey({ @@ -723,7 +723,7 @@ export const kmsServiceFactory = ({ } if (!project.kmsSecretManagerKeyId) { - throw new Error("Missing project KMS key ID"); + throw new BadRequestError({ message: "Missing project KMS key ID" }); } return project.kmsSecretManagerKeyId; @@ -832,9 +832,10 @@ export const kmsServiceFactory = ({ const isBase64 = !envConfig.ENCRYPTION_KEY; if (!encryptionKey) - throw new Error( - "Root encryption key not found for KMS service. Did you set the ENCRYPTION_KEY or ROOT_ENCRYPTION_KEY environment variables?" - ); + throw new BadRequestError({ + message: + "Root encryption key not found for KMS service. Did you set the ENCRYPTION_KEY or ROOT_ENCRYPTION_KEY environment variables?" + }); const encryptionKeyBuffer = Buffer.from(encryptionKey, isBase64 ? "base64" : "utf8"); @@ -846,7 +847,9 @@ export const kmsServiceFactory = ({ if (kmsRootConfig.encryptionStrategy === RootKeyEncryptionStrategy.HSM) { const hsmIsActive = await hsmService.isActive(); if (!hsmIsActive) { - throw new Error("Unable to decrypt root KMS key. HSM service is inactive. Did you configure the HSM?"); + throw new BadRequestError({ + message: "Unable to decrypt root KMS key. HSM service is inactive. Did you configure the HSM?" + }); } const decryptedKey = await hsmService.decrypt(kmsRootConfig.encryptedRootKey); @@ -861,14 +864,16 @@ export const kmsServiceFactory = ({ return cipher.decrypt(kmsRootConfig.encryptedRootKey, encryptionKeyBuffer); } - throw new Error(`Invalid root key encryption strategy: ${kmsRootConfig.encryptionStrategy}`); + throw new BadRequestError({ message: `Invalid root key encryption strategy: ${kmsRootConfig.encryptionStrategy}` }); }; const $encryptRootKey = async (plainKeyBuffer: Buffer, strategy: RootKeyEncryptionStrategy) => { if (strategy === RootKeyEncryptionStrategy.HSM) { const hsmIsActive = await hsmService.isActive(); if (!hsmIsActive) { - throw new Error("Unable to encrypt root KMS key. HSM service is inactive. Did you configure the HSM?"); + throw new BadRequestError({ + message: "Unable to encrypt root KMS key. HSM service is inactive. Did you configure the HSM?" + }); } const encrypted = await hsmService.encrypt(plainKeyBuffer); return encrypted; @@ -882,7 +887,7 @@ export const kmsServiceFactory = ({ } // eslint-disable-next-line @typescript-eslint/restrict-template-expressions - throw new Error(`Invalid root key encryption strategy: ${strategy}`); + throw new BadRequestError({ message: `Invalid root key encryption strategy: ${strategy}` }); }; // by keeping the decrypted data key in inner scope @@ -1130,7 +1135,7 @@ export const kmsServiceFactory = ({ if (!encryptedRootKey) { logger.error("KMS: Failed to re-encrypt ROOT Key with selected strategy"); - throw new Error("Failed to re-encrypt ROOT Key with selected strategy"); + throw new BadRequestError({ message: "Failed to re-encrypt ROOT Key with selected strategy" }); } await kmsRootConfigDAL.updateById(KMS_ROOT_CONFIG_UUID, { diff --git a/frontend/src/hooks/api/kms/mutations.tsx b/frontend/src/hooks/api/kms/mutations.tsx index 4fb0a5af5..534e9fd5d 100644 --- a/frontend/src/hooks/api/kms/mutations.tsx +++ b/frontend/src/hooks/api/kms/mutations.tsx @@ -6,6 +6,7 @@ import { kmsKeys } from "./queries"; import { AddExternalKmsType, ExternalKmsGcpSchemaType, + ExternalKmsProvider, KmsGcpKeyFetchAuthType, KmsType, UpdateExternalKmsType @@ -14,11 +15,12 @@ import { export const useAddExternalKms = (orgId: string) => { const queryClient = useQueryClient(); return useMutation({ - mutationFn: async ({ name, description, provider }: AddExternalKmsType) => { - const { data } = await apiRequest.post("/api/v1/external-kms", { + mutationFn: async ({ name, description, configuration }: AddExternalKmsType) => { + const providerPath = configuration.type === ExternalKmsProvider.Aws ? "aws" : "gcp"; + const { data } = await apiRequest.post(`/api/v1/external-kms/${providerPath}`, { name, description, - provider + configuration: configuration.inputs }); return data; @@ -29,21 +31,21 @@ export const useAddExternalKms = (orgId: string) => { }); }; -export const useUpdateExternalKms = (orgId: string) => { +export const useUpdateExternalKms = (orgId: string, provider: ExternalKmsProvider) => { const queryClient = useQueryClient(); return useMutation({ mutationFn: async ({ kmsId, name, description, - provider + configuration }: { kmsId: string; } & UpdateExternalKmsType) => { - const { data } = await apiRequest.patch(`/api/v1/external-kms/${kmsId}`, { + const { data } = await apiRequest.patch(`/api/v1/external-kms/${provider}/${kmsId}`, { name, description, - provider + configuration: configuration?.inputs }); return data; @@ -58,8 +60,8 @@ export const useUpdateExternalKms = (orgId: string) => { export const useRemoveExternalKms = (orgId: string) => { const queryClient = useQueryClient(); return useMutation({ - mutationFn: async (kmsId: string) => { - const { data } = await apiRequest.delete(`/api/v1/external-kms/${kmsId}`); + mutationFn: async ({ kmsId, provider }: { kmsId: string; provider: ExternalKmsProvider }) => { + const { data } = await apiRequest.delete(`/api/v1/external-kms/${provider}/${kmsId}`); return data; }, @@ -130,11 +132,19 @@ export const useExternalKmsFetchGcpKeys = (orgId: string) => { ); } - const { data } = await apiRequest.post("/api/v1/external-kms/gcp/keys", { - authMethod: credential ? KmsGcpKeyFetchAuthType.Credential : KmsGcpKeyFetchAuthType.Kms, - region: gcpRegion, - ...rest - }); + const requestBody = credential + ? { + authMethod: KmsGcpKeyFetchAuthType.Credential, + region: gcpRegion, + credential + } + : { + authMethod: KmsGcpKeyFetchAuthType.Kms, + region: gcpRegion, + kmsId + }; + + const { data } = await apiRequest.post("/api/v1/external-kms/gcp/keys", requestBody); return data; }, diff --git a/frontend/src/hooks/api/kms/queries.tsx b/frontend/src/hooks/api/kms/queries.tsx index 97d25376c..4342c1881 100644 --- a/frontend/src/hooks/api/kms/queries.tsx +++ b/frontend/src/hooks/api/kms/queries.tsx @@ -2,7 +2,7 @@ import { useQuery } from "@tanstack/react-query"; import { apiRequest } from "@app/config/request"; -import { Kms, KmsListEntry } from "./types"; +import { ExternalKmsProvider, Kms, KmsListEntry } from "./types"; export const kmsKeys = { getExternalKmsList: (orgId: string) => ["get-all-external-kms", { orgId }], @@ -23,15 +23,19 @@ export const useGetExternalKmsList = (orgId: string, { enabled }: { enabled?: bo }); }; -export const useGetExternalKmsById = (kmsId: string) => { +export const useGetExternalKmsById = ({ + kmsId, + provider +}: { + kmsId: string; + provider: ExternalKmsProvider; +}) => { return useQuery({ queryKey: kmsKeys.getExternalKmsById(kmsId), enabled: Boolean(kmsId), queryFn: async () => { - const { - data: { externalKms } - } = await apiRequest.get<{ externalKms: Kms }>(`/api/v1/external-kms/${kmsId}`); - return externalKms; + const { data } = await apiRequest.get(`/api/v1/external-kms/${provider}/${kmsId}`); + return data; } }); }; diff --git a/frontend/src/hooks/api/kms/types.ts b/frontend/src/hooks/api/kms/types.ts index 73b821b1a..32c2fa32a 100644 --- a/frontend/src/hooks/api/kms/types.ts +++ b/frontend/src/hooks/api/kms/types.ts @@ -8,12 +8,13 @@ export type Kms = { description: string; orgId: string; name: string; - external: { + externalKms: { id: string; status: string; statusDetails: string; provider: string; - providerInput: Record; + configuration: Record; + credentialsHash?: string; }; }; @@ -123,14 +124,14 @@ export const ExternalKmsInputSchema = z.discriminatedUnion("type", [ export const AddExternalKmsSchema = z.object({ name: slugSchema({ min: 1, field: "Alias" }), description: z.string().trim().optional(), - provider: ExternalKmsInputSchema + configuration: ExternalKmsInputSchema }); export type AddExternalKmsType = z.infer; // we need separate schema for update because the credential field is not required on GCP export const ExternalKmsUpdateInputSchema = z.discriminatedUnion("type", [ - z.object({ type: z.literal(ExternalKmsProvider.Aws), inputs: ExternalKmsAwsSchema }), + z.object({ type: z.literal(ExternalKmsProvider.Aws), inputs: ExternalKmsAwsSchema.partial() }), z.object({ type: z.literal(ExternalKmsProvider.Gcp), inputs: ExternalKmsGcpSchema.pick({ gcpRegion: true, keyName: true }) @@ -144,9 +145,10 @@ export const UpdateExternalKmsSchema = z.object({ .min(1) .refine((v) => slugify(v) === v, { message: "Alias must be a valid slug" - }), + }) + .optional(), description: z.string().trim().optional(), - provider: ExternalKmsUpdateInputSchema + configuration: ExternalKmsUpdateInputSchema.optional() }); export type UpdateExternalKmsType = z.infer; diff --git a/frontend/src/pages/organization/SettingsPage/components/OrgEncryptionTab/AwsKmsForm.tsx b/frontend/src/pages/organization/SettingsPage/components/OrgEncryptionTab/AwsKmsForm.tsx index 251cebcbe..fe063d88a 100644 --- a/frontend/src/pages/organization/SettingsPage/components/OrgEncryptionTab/AwsKmsForm.tsx +++ b/frontend/src/pages/organization/SettingsPage/components/OrgEncryptionTab/AwsKmsForm.tsx @@ -11,7 +11,8 @@ import { AddExternalKmsType, ExternalKmsProvider, Kms, - KmsAwsCredentialType + KmsAwsCredentialType, + UpdateExternalKmsSchema } from "@app/hooks/api/kms/types"; const AWS_REGIONS = [ @@ -50,9 +51,12 @@ type Props = { onCompleted: () => void; onCancel: () => void; kms?: Kms; + mode?: "full" | "credentials" | "details"; }; -export const AwsKmsForm = ({ onCompleted, onCancel, kms }: Props) => { +export const AwsKmsForm = ({ onCompleted, onCancel, kms, mode = "full" }: Props) => { + const validationSchema = kms ? UpdateExternalKmsSchema : AddExternalKmsSchema; + const { control, handleSubmit, @@ -60,24 +64,35 @@ export const AwsKmsForm = ({ onCompleted, onCancel, kms }: Props) => { setValue, formState: { isSubmitting } } = useForm({ - resolver: zodResolver(AddExternalKmsSchema), + resolver: zodResolver(validationSchema), defaultValues: { name: kms?.name, description: kms?.description ?? "", - provider: { + configuration: { type: ExternalKmsProvider.Aws, inputs: { - credential: { - type: kms?.external?.providerInput?.credential?.type, - data: { - accessKey: kms?.external?.providerInput?.credential?.data?.accessKey, - secretKey: kms?.external?.providerInput?.credential?.data?.secretKey, - assumeRoleArn: kms?.external?.providerInput?.credential?.data?.assumeRoleArn, - externalId: kms?.external?.providerInput?.credential?.data?.externalId - } - }, - awsRegion: kms?.external?.providerInput?.awsRegion, - kmsKeyId: kms?.external?.providerInput?.kmsKeyId + ...(mode !== "details" && + kms?.externalKms?.configuration?.credential?.type && + kms.externalKms.configuration.credential.data + ? { + credential: { + type: kms.externalKms.configuration.credential.type, + data: { + accessKey: kms.externalKms.configuration.credential.data?.accessKey ?? "", + secretKey: kms.externalKms.configuration.credential.data?.secretKey ?? "", + assumeRoleArn: + kms.externalKms.configuration.credential.data?.assumeRoleArn ?? "", + externalId: kms.externalKms.configuration.credential.data?.externalId ?? "" + } + } + } + : {}), + ...(mode !== "credentials" + ? { + awsRegion: kms?.externalKms?.configuration?.awsRegion ?? "", + kmsKeyId: kms?.externalKms?.configuration?.kmsKeyId ?? "" + } + : {}) } } } @@ -85,30 +100,59 @@ export const AwsKmsForm = ({ onCompleted, onCancel, kms }: Props) => { const { currentOrg } = useOrganization(); const { mutateAsync: addAwsExternalKms } = useAddExternalKms(currentOrg.id); - const { mutateAsync: updateAwsExternalKms } = useUpdateExternalKms(currentOrg.id); + const { mutateAsync: updateAwsExternalKms } = useUpdateExternalKms( + currentOrg.id, + ExternalKmsProvider.Aws + ); - const selectedAwsAuthType = watch("provider.inputs.credential.type"); + const selectedAwsAuthType = watch("configuration.inputs.credential.type"); const handleAwsKmsFormSubmit = async (data: AddExternalKmsType) => { - const { name, description, provider } = data; + const { name, description, configuration } = data; try { if (kms) { - await updateAwsExternalKms({ - kmsId: kms.id, - name, - description, - provider - }); + if (configuration.type !== ExternalKmsProvider.Aws) { + throw new Error("Invalid configuration type"); + } + const awsInputs = configuration.inputs; + + if (mode === "credentials") { + await updateAwsExternalKms({ + kmsId: kms.id, + configuration: { + type: ExternalKmsProvider.Aws, + inputs: { + credential: { ...awsInputs.credential } + } + } + }); + } else { + await updateAwsExternalKms({ + kmsId: kms.id, + name, + description, + configuration: { + type: ExternalKmsProvider.Aws, + inputs: { + awsRegion: awsInputs.awsRegion, + kmsKeyId: awsInputs.kmsKeyId + } + } + }); + } createNotification({ - text: "Successfully updated AWS External KMS", + text: + mode === "credentials" + ? "Successfully updated AWS External KMS credentials" + : "Successfully updated AWS External KMS Details", type: "success" }); } else { await addAwsExternalKms({ name, description, - provider + configuration }); createNotification({ @@ -125,104 +169,149 @@ export const AwsKmsForm = ({ onCompleted, onCancel, kms }: Props) => { return (
- ( - - - - )} - /> - ( - - - - )} - /> - ( - - - - )} - /> - - {selectedAwsAuthType === KmsAwsCredentialType.AccessKey ? ( + {(mode === "full" || mode === "details") && ( <> ( - + )} /> ( - - + + )} /> - ) : ( + )} + {(mode === "full" || mode === "credentials") && ( <> ( + name="configuration.inputs.credential.type" + defaultValue={KmsAwsCredentialType.AssumeRole} + render={({ field: { onChange, ...field }, fieldState: { error } }) => ( - + + + )} + /> + + {selectedAwsAuthType === KmsAwsCredentialType.AccessKey ? ( + <> + ( + + + + )} + /> + ( + + + + )} + /> + + ) : ( + <> + ( + + + + )} + /> + ( + + + + )} + /> + + )} + + )} + {(mode === "full" || mode === "details") && ( + <> + ( + + )} /> ( @@ -232,38 +321,9 @@ export const AwsKmsForm = ({ onCompleted, onCancel, kms }: Props) => { /> )} - ( - - - - )} - /> - ( - - - - )} - />
); -export const GcpKmsForm = ({ onCompleted, onCancel, kms }: Props) => { +export const GcpKmsForm = ({ onCompleted, onCancel, kms, mode = "full" }: Props) => { const [isCredentialValid, setIsCredentialValid] = useState(false); const [keys, setKeys] = useState<{ value: string; label: string }[]>([]); @@ -88,7 +89,7 @@ export const GcpKmsForm = ({ onCompleted, onCancel, kms }: Props) => { getValues, resetField, setValue, - formState: { isSubmitting } + formState: { isSubmitting, isDirty, isValid } } = useForm({ resolver: zodResolver(AddExternalKmsGcpFormSchema), defaultValues: { @@ -98,9 +99,9 @@ export const GcpKmsForm = ({ onCompleted, onCancel, kms }: Props) => { gcpRegion: kms ? { label: - GCP_REGIONS.find((r) => r.value === kms.external.providerInput.gcpRegion)?.label ?? + GCP_REGIONS.find((r) => r.value === kms.externalKms.configuration.gcpRegion)?.label ?? "", - value: kms.external.providerInput.gcpRegion + value: kms.externalKms.configuration.gcpRegion } : undefined, keyObject: undefined @@ -109,7 +110,11 @@ export const GcpKmsForm = ({ onCompleted, onCancel, kms }: Props) => { const { currentOrg } = useOrganization(); const { mutateAsync: addGcpExternalKms } = useAddExternalKms(currentOrg.id); - const { mutateAsync: updateGcpExternalKms } = useUpdateExternalKms(currentOrg.id); + const { mutateAsync: updateGcpExternalKms } = useUpdateExternalKms( + currentOrg.id, + ExternalKmsProvider.Gcp + ); + const { mutateAsync: fetchGcpKeys, isPending: isFetchGcpKeysLoading } = useExternalKmsFetchGcpKeys(currentOrg?.id); @@ -140,36 +145,62 @@ export const GcpKmsForm = ({ onCompleted, onCancel, kms }: Props) => { // handles the form submission const handleGcpKmsFormSubmit = async (data: AddExternalKmsGcpFormSchemaType) => { - const { name, description, gcpRegion: gcpRegionObject, keyObject } = data; - const gcpRegion = gcpRegionObject.value; - if (!keys.find((k) => k.value === keyObject?.value)) { - setError("keyObject", { - message: "Please select a valid key." - }); - resetField("keyObject"); - return; - } + const { name, description, formType, gcpRegion: gcpRegionObject, keyObject } = data; try { if (kms) { - await updateGcpExternalKms({ - kmsId: kms.id, - name, - description, - provider: { - type: ExternalKmsProvider.Gcp, - inputs: { - gcpRegion, - keyName: keyObject?.value - } + if (formType === "updateGcpKms") { + const gcpRegion = gcpRegionObject?.value; + if (!gcpRegion) { + setError("gcpRegion", { + message: "Please select a GCP region." + }); + return; } - }); - createNotification({ - text: "Successfully updated GCP External KMS", - type: "success" - }); - } else { + if (keyObject && !keys.find((k) => k.value === keyObject.value)) { + setError("keyObject", { + message: "Please select a valid key." + }); + resetField("keyObject"); + return; + } + + await updateGcpExternalKms({ + kmsId: kms.id, + name, + description, + configuration: { + type: ExternalKmsProvider.Gcp, + inputs: { + gcpRegion, + keyName: keyObject?.value ?? kms.externalKms.configuration.keyName + } + } + }); + + createNotification({ + text: "Successfully updated GCP External KMS Details", + type: "success" + }); + } + } else if (formType === "newGcpKms") { + const gcpRegion = gcpRegionObject?.value; + if (!gcpRegion) { + setError("gcpRegion", { + message: "Please select a GCP region." + }); + return; + } + + if (!keys.find((k) => k.value === keyObject?.value)) { + setError("keyObject", { + message: "Please select a valid key." + }); + resetField("keyObject"); + return; + } + const credentialJson = await getCredentialFileJson(); if (!credentialJson) { return; @@ -177,11 +208,11 @@ export const GcpKmsForm = ({ onCompleted, onCancel, kms }: Props) => { await addGcpExternalKms({ name, description, - provider: { + configuration: { type: ExternalKmsProvider.Gcp, inputs: { gcpRegion, - keyName: keyObject?.value, + keyName: keyObject?.value ?? "", credential: credentialJson } } @@ -208,8 +239,9 @@ export const GcpKmsForm = ({ onCompleted, onCancel, kms }: Props) => { if (!kms && !credentialJson) { return; } - const gcpRegion = getValues("gcpRegion").value; - if (!gcpRegion.length) { + const gcpRegionObject = getValues("gcpRegion"); + const gcpRegion = gcpRegionObject?.value; + if (!gcpRegion) { setError("gcpRegion", { message: "Please select a GCP region to fetch GCP Keys." }); @@ -231,7 +263,9 @@ export const GcpKmsForm = ({ onCompleted, onCancel, kms }: Props) => { setKeys(returnedKeys); if (kms) { - const existingKey = returnedKeys.find((k) => k.value === kms.external.providerInput.keyName); + const existingKey = returnedKeys.find( + (k) => k.value === kms.externalKms.configuration.keyName + ); if (existingKey) { setValue("keyObject", existingKey); } @@ -260,96 +294,104 @@ export const GcpKmsForm = ({ onCompleted, onCancel, kms }: Props) => { return ( - ( - - - - )} - /> - ( - - - - )} - /> - ( - - { - resetField("keyObject"); - field.onChange(e); - fetchGCPKeys(); - }} - formatOptionLabel={formatOptionLabel} + {(mode === "full" || mode === "details") && ( + <> + ( + + + + )} + /> + ( + + + + )} + /> + ( + + { + resetField("keyObject"); + field.onChange(e); + fetchGCPKeys(); + }} + formatOptionLabel={formatOptionLabel} + /> + + )} + /> + {!kms && ( + ( + + { + onChange(e.target.files); + fetchGCPKeys(); + }} + /> + + )} /> - - )} - /> - {!kms && ( - ( - - { - onChange(e.target.files); - fetchGCPKeys(); - }} - /> - )} - /> + ( + + + + )} + /> + )} - ( - - - - )} - /> - {kms && ( + {kms && mode === "credentials" && ( To change your GCP credentials, create a new external KMS and assign it to project you want to use it with. )}
-