fix: kms endpoints to rerutn credentialsHash

This commit is contained in:
Piyush Gupta
2025-12-05 02:49:20 +05:30
parent bd149940a5
commit f14b03b6ac
12 changed files with 481 additions and 194 deletions
@@ -3,35 +3,27 @@ import { z } from "zod";
import { ExternalKmsSchema, KmsKeysSchema } from "@app/db/schemas"; import { ExternalKmsSchema, KmsKeysSchema } from "@app/db/schemas";
import { EventType } from "@app/ee/services/audit-log/audit-log-types"; import { EventType } from "@app/ee/services/audit-log/audit-log-types";
import { import {
ExternalKmsAwsSchema,
ExternalKmsGcpSchema,
KmsProviders, KmsProviders,
SanitizedExternalKmsAwsSchema,
SanitizedExternalKmsGcpSchema,
TExternalKmsInputSchema, TExternalKmsInputSchema,
TExternalKmsInputUpdateSchema TExternalKmsInputUpdateSchema
} from "@app/ee/services/external-kms/providers/model"; } from "@app/ee/services/external-kms/providers/model";
import { crypto } from "@app/lib/crypto/cryptography";
import { BadRequestError } from "@app/lib/errors"; import { BadRequestError } from "@app/lib/errors";
import { readLimit, writeLimit } from "@app/server/config/rateLimiter"; import { readLimit, writeLimit } from "@app/server/config/rateLimiter";
import { verifyAuth } from "@app/server/plugins/auth/verify-auth"; import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
import { AuthMode } from "@app/services/auth/auth-type"; import { AuthMode } from "@app/services/auth/auth-type";
const sanitizedExternalSchema = KmsKeysSchema.extend({ const sanitizedExternalSchema = KmsKeysSchema.extend({
external: ExternalKmsSchema.pick({ externalKms: ExternalKmsSchema.pick({
id: true,
status: true,
statusDetails: true,
provider: true
})
});
const sanitizedExternalSchemaForGetById = KmsKeysSchema.extend({
external: ExternalKmsSchema.pick({
id: true, id: true,
status: true, status: true,
statusDetails: true, statusDetails: true,
provider: true provider: true
}).extend({ }).extend({
// for GCP, we don't return the credential object as it is sensitive data that should not be exposed configuration: z.union([SanitizedExternalKmsAwsSchema, SanitizedExternalKmsGcpSchema]),
providerInput: z.union([ExternalKmsAwsSchema, ExternalKmsGcpSchema.pick({ gcpRegion: true, keyName: true })]) credentialsHash: z.string().optional()
}) })
}); });
@@ -59,9 +51,7 @@ export const registerExternalKmsEndpoints = <
id: z.string().trim().min(1) id: z.string().trim().min(1)
}), }),
response: { response: {
200: z.object({ 200: sanitizedExternalSchema
externalKms: sanitizedExternalSchemaForGetById
})
} }
}, },
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
@@ -93,7 +83,16 @@ export const registerExternalKmsEndpoints = <
} }
}); });
return { externalKms }; const {
external: { providerInput: configuration, ...externalKmsData },
...rest
} = externalKms;
const credentialsHash = crypto.nativeCrypto
.createHash("sha256")
.update(externalKmsData.encryptedProviderInputs)
.digest("hex");
return { ...rest, externalKms: { ...externalKmsData, configuration, credentialsHash } };
} }
}); });
@@ -110,9 +109,7 @@ export const registerExternalKmsEndpoints = <
configuration: createSchema configuration: createSchema
}), }),
response: { response: {
200: z.object({ 200: sanitizedExternalSchema
externalKms: sanitizedExternalSchema
})
} }
}, },
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
@@ -152,7 +149,15 @@ export const registerExternalKmsEndpoints = <
} }
}); });
return { externalKms }; 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 } };
} }
}); });
@@ -169,12 +174,10 @@ export const registerExternalKmsEndpoints = <
body: z.object({ body: z.object({
name: z.string().min(1).trim().toLowerCase().optional(), name: z.string().min(1).trim().toLowerCase().optional(),
description: z.string().trim().optional(), description: z.string().trim().optional(),
configuration: updateSchema configuration: updateSchema.optional()
}), }),
response: { response: {
200: z.object({ 200: sanitizedExternalSchema
externalKms: sanitizedExternalSchema
})
} }
}, },
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
@@ -215,7 +218,15 @@ export const registerExternalKmsEndpoints = <
} }
}); });
return { externalKms }; 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 } };
} }
}); });
@@ -230,9 +241,7 @@ export const registerExternalKmsEndpoints = <
id: z.string().trim().min(1) id: z.string().trim().min(1)
}), }),
response: { response: {
200: z.object({ 200: sanitizedExternalSchema
externalKms: sanitizedExternalSchema
})
} }
}, },
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
@@ -264,7 +273,16 @@ export const registerExternalKmsEndpoints = <
} }
}); });
return { externalKms }; const {
external: { providerInput: configuration, ...externalKmsData },
...rest
} = externalKms;
const credentialsHash = crypto.nativeCrypto
.createHash("sha256")
.update(externalKmsData.encryptedProviderInputs)
.digest("hex");
return { ...rest, externalKms: { ...externalKmsData, configuration, credentialsHash } };
} }
}); });
}; };
@@ -24,7 +24,13 @@ import {
} from "./external-kms-types"; } from "./external-kms-types";
import { AwsKmsProviderFactory } from "./providers/aws-kms"; import { AwsKmsProviderFactory } from "./providers/aws-kms";
import { GcpKmsProviderFactory } from "./providers/gcp-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 = { type TExternalKmsServiceFactoryDep = {
externalKmsDAL: TExternalKmsDALFactory; externalKmsDAL: TExternalKmsDALFactory;
@@ -72,6 +78,7 @@ export const externalKmsServiceFactory = ({
const kmsName = name ? slugify(name) : slugify(alphaNumericNanoId(8).toLowerCase()); const kmsName = name ? slugify(name) : slugify(alphaNumericNanoId(8).toLowerCase());
let sanitizedProviderInput = ""; let sanitizedProviderInput = "";
let sanitizedProviderInputObject: TExternalKmsAwsSchema | TExternalKmsGcpSchema;
switch (provider.type) { switch (provider.type) {
case KmsProviders.Aws: case KmsProviders.Aws:
{ {
@@ -88,6 +95,7 @@ export const externalKmsServiceFactory = ({
try { try {
// if missing kms key this generate a new kms key id and returns new provider input // if missing kms key this generate a new kms key id and returns new provider input
const newProviderInput = await externalKms.generateInputKmsKey(); const newProviderInput = await externalKms.generateInputKmsKey();
sanitizedProviderInputObject = newProviderInput;
sanitizedProviderInput = JSON.stringify(newProviderInput); sanitizedProviderInput = JSON.stringify(newProviderInput);
await externalKms.validateConnection(); await externalKms.validateConnection();
@@ -109,6 +117,7 @@ export const externalKmsServiceFactory = ({
const externalKms = await GcpKmsProviderFactory({ inputs: provider.inputs }); const externalKms = await GcpKmsProviderFactory({ inputs: provider.inputs });
try { try {
await externalKms.validateConnection(); await externalKms.validateConnection();
sanitizedProviderInputObject = provider.inputs;
sanitizedProviderInput = JSON.stringify(provider.inputs); sanitizedProviderInput = JSON.stringify(provider.inputs);
} catch (error) { } catch (error) {
if (error instanceof BadRequestError) { if (error instanceof BadRequestError) {
@@ -155,7 +164,10 @@ export const externalKmsServiceFactory = ({
}, },
tx tx
); );
return { ...kms, external: externalKmsCfg }; return {
...kms,
external: { ...externalKmsCfg, providerInput: sanitizedProviderInputObject }
};
}); });
return externalKms; return externalKms;
@@ -195,6 +207,7 @@ export const externalKmsServiceFactory = ({
if (!externalKmsDoc) throw new NotFoundError({ message: `External KMS with ID '${kmsId}' not found` }); if (!externalKmsDoc) throw new NotFoundError({ message: `External KMS with ID '${kmsId}' not found` });
let sanitizedProviderInput = ""; let sanitizedProviderInput = "";
let sanitizedProviderInputObject: TExternalKmsAwsSchema | TExternalKmsGcpSchema;
const { encryptor: orgDataKeyEncryptor, decryptor: orgDataKeyDecryptor } = const { encryptor: orgDataKeyEncryptor, decryptor: orgDataKeyDecryptor } =
await kmsService.createCipherPairWithDataKey({ await kmsService.createCipherPairWithDataKey({
type: KmsDataKey.Organization, type: KmsDataKey.Organization,
@@ -215,6 +228,7 @@ export const externalKmsServiceFactory = ({
const externalKms = await AwsKmsProviderFactory({ inputs: updatedProviderInput }); const externalKms = await AwsKmsProviderFactory({ inputs: updatedProviderInput });
try { try {
await externalKms.validateConnection(); await externalKms.validateConnection();
sanitizedProviderInputObject = updatedProviderInput;
sanitizedProviderInput = JSON.stringify(updatedProviderInput); sanitizedProviderInput = JSON.stringify(updatedProviderInput);
} catch (error) { } catch (error) {
if (error instanceof BadRequestError) { if (error instanceof BadRequestError) {
@@ -238,6 +252,7 @@ export const externalKmsServiceFactory = ({
const externalKms = await GcpKmsProviderFactory({ inputs: updatedProviderInput }); const externalKms = await GcpKmsProviderFactory({ inputs: updatedProviderInput });
try { try {
await externalKms.validateConnection(); await externalKms.validateConnection();
sanitizedProviderInputObject = updatedProviderInput;
sanitizedProviderInput = JSON.stringify(updatedProviderInput); sanitizedProviderInput = JSON.stringify(updatedProviderInput);
} catch (error) { } catch (error) {
if (error instanceof BadRequestError) { if (error instanceof BadRequestError) {
@@ -266,14 +281,17 @@ export const externalKmsServiceFactory = ({
} }
const externalKms = await externalKmsDAL.transaction(async (tx) => { const externalKms = await externalKmsDAL.transaction(async (tx) => {
const kms = await kmsDAL.updateById( let kms = kmsDoc;
kmsDoc.id, if (kmsName || description) {
{ kms = await kmsDAL.updateById(
description, kmsDoc.id,
name: kmsName {
}, description,
tx name: kmsName
); },
tx
);
}
if (encryptedProviderInputs) { if (encryptedProviderInputs) {
const externalKmsCfg = await externalKmsDAL.updateById( const externalKmsCfg = await externalKmsDAL.updateById(
externalKmsDoc.id, externalKmsDoc.id,
@@ -282,9 +300,9 @@ export const externalKmsServiceFactory = ({
}, },
tx tx
); );
return { ...kms, external: externalKmsCfg }; return { ...kms, external: { ...externalKmsCfg, providerInput: sanitizedProviderInputObject } };
} }
return { ...kms, external: externalKmsDoc }; return { ...kms, external: { ...externalKmsDoc, providerInput: sanitizedProviderInputObject } };
}); });
return externalKms; return externalKms;
@@ -305,9 +323,40 @@ export const externalKmsServiceFactory = ({
const externalKmsDoc = await externalKmsDAL.findOne({ kmsKeyId: kmsDoc.id }); const externalKmsDoc = await externalKmsDAL.findOne({ kmsKeyId: kmsDoc.id });
if (!externalKmsDoc) throw new NotFoundError({ message: `External KMS with ID '${kmsId}' not found` }); 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 externalKms = await externalKmsDAL.transaction(async (tx) => {
const kms = await kmsDAL.deleteById(kmsDoc.id, tx); const kms = await kmsDAL.deleteById(kmsDoc.id, tx);
return { ...kms, external: externalKmsDoc }; return { ...kms, external: { ...externalKmsDoc, providerInput: decryptedProviderInputObject } };
}); });
return externalKms; return externalKms;
@@ -19,27 +19,31 @@ export enum KmsGcpKeyFetchAuthType {
Kms = "kmsId" 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 furthur 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({ export const ExternalKmsAwsSchema = z.object({
credential: z credential: z
.discriminatedUnion("type", [ .discriminatedUnion("type", [
z.object({ z.object({
type: z.literal(KmsAwsCredentialType.AccessKey), type: z.literal(KmsAwsCredentialType.AccessKey),
data: z.object({ data: AwsConnectionAccessTokenCredentialsSchema
accessKey: z.string().trim().min(1).describe("AWS user account access key"),
secretKey: z.string().trim().min(1).describe("AWS user account secret key")
})
}), }),
z.object({ z.object({
type: z.literal(KmsAwsCredentialType.AssumeRole), type: z.literal(KmsAwsCredentialType.AssumeRole),
data: z.object({ data: AwsConnectionAssumeRoleCredentialsSchema
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")
})
}) })
]) ])
.describe("AWS credential information to connect"), .describe("AWS credential information to connect"),
@@ -52,6 +56,19 @@ export const ExternalKmsAwsSchema = z.object({
}); });
export type TExternalKmsAwsSchema = z.infer<typeof ExternalKmsAwsSchema>; export type TExternalKmsAwsSchema = z.infer<typeof ExternalKmsAwsSchema>;
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({})
})
])
});
export const ExternalKmsGcpCredentialSchema = z.object({ export const ExternalKmsGcpCredentialSchema = z.object({
type: z.literal(KmsGcpCredentialType.ServiceAccount), type: z.literal(KmsGcpCredentialType.ServiceAccount),
project_id: z.string().min(1), project_id: z.string().min(1),
@@ -75,6 +92,8 @@ export const ExternalKmsGcpSchema = z.object({
}); });
export type TExternalKmsGcpSchema = z.infer<typeof ExternalKmsGcpSchema>; export type TExternalKmsGcpSchema = z.infer<typeof ExternalKmsGcpSchema>;
export const SanitizedExternalKmsGcpSchema = ExternalKmsGcpSchema.pick({ gcpRegion: true, keyName: true });
const ExternalKmsGcpClientSchema = ExternalKmsGcpSchema.pick({ gcpRegion: true }).extend({ const ExternalKmsGcpClientSchema = ExternalKmsGcpSchema.pick({ gcpRegion: true }).extend({
credential: ExternalKmsGcpCredentialSchema credential: ExternalKmsGcpCredentialSchema
}); });
+7 -8
View File
@@ -15,12 +15,12 @@ import {
export const useAddExternalKms = (orgId: string) => { export const useAddExternalKms = (orgId: string) => {
const queryClient = useQueryClient(); const queryClient = useQueryClient();
return useMutation({ return useMutation({
mutationFn: async ({ name, description, provider }: AddExternalKmsType) => { mutationFn: async ({ name, description, configuration }: AddExternalKmsType) => {
const providerPath = provider.type === ExternalKmsProvider.Aws ? "aws" : "gcp"; const providerPath = configuration.type === ExternalKmsProvider.Aws ? "aws" : "gcp";
const { data } = await apiRequest.post(`/api/v1/external-kms/${providerPath}`, { const { data } = await apiRequest.post(`/api/v1/external-kms/${providerPath}`, {
name, name,
description, description,
configuration: provider.inputs configuration: configuration.inputs
}); });
return data; return data;
@@ -31,22 +31,21 @@ export const useAddExternalKms = (orgId: string) => {
}); });
}; };
export const useUpdateExternalKms = (orgId: string) => { export const useUpdateExternalKms = (orgId: string, provider: ExternalKmsProvider) => {
const queryClient = useQueryClient(); const queryClient = useQueryClient();
return useMutation({ return useMutation({
mutationFn: async ({ mutationFn: async ({
kmsId, kmsId,
name, name,
description, description,
provider configuration
}: { }: {
kmsId: string; kmsId: string;
} & UpdateExternalKmsType) => { } & UpdateExternalKmsType) => {
const providerPath = provider.type === ExternalKmsProvider.Aws ? "aws" : "gcp"; const { data } = await apiRequest.patch(`/api/v1/external-kms/${provider}/${kmsId}`, {
const { data } = await apiRequest.patch(`/api/v1/external-kms/${providerPath}/${kmsId}`, {
name, name,
description, description,
configuration: provider.inputs configuration: configuration?.inputs
}); });
return data; return data;
+2 -4
View File
@@ -34,10 +34,8 @@ export const useGetExternalKmsById = ({
queryKey: kmsKeys.getExternalKmsById(kmsId), queryKey: kmsKeys.getExternalKmsById(kmsId),
enabled: Boolean(kmsId), enabled: Boolean(kmsId),
queryFn: async () => { queryFn: async () => {
const { const { data } = await apiRequest.get<Kms>(`/api/v1/external-kms/${provider}/${kmsId}`);
data: { externalKms } return data;
} = await apiRequest.get<{ externalKms: Kms }>(`/api/v1/external-kms/${provider}/${kmsId}`);
return externalKms;
} }
}); });
}; };
+7 -5
View File
@@ -8,12 +8,13 @@ export type Kms = {
description: string; description: string;
orgId: string; orgId: string;
name: string; name: string;
external: { externalKms: {
id: string; id: string;
status: string; status: string;
statusDetails: string; statusDetails: string;
provider: string; provider: string;
providerInput: Record<string, any>; configuration: Record<string, any>;
credentialsHash?: string;
}; };
}; };
@@ -123,7 +124,7 @@ export const ExternalKmsInputSchema = z.discriminatedUnion("type", [
export const AddExternalKmsSchema = z.object({ export const AddExternalKmsSchema = z.object({
name: slugSchema({ min: 1, field: "Alias" }), name: slugSchema({ min: 1, field: "Alias" }),
description: z.string().trim().optional(), description: z.string().trim().optional(),
provider: ExternalKmsInputSchema configuration: ExternalKmsInputSchema
}); });
export type AddExternalKmsType = z.infer<typeof AddExternalKmsSchema>; export type AddExternalKmsType = z.infer<typeof AddExternalKmsSchema>;
@@ -144,9 +145,10 @@ export const UpdateExternalKmsSchema = z.object({
.min(1) .min(1)
.refine((v) => slugify(v) === v, { .refine((v) => slugify(v) === v, {
message: "Alias must be a valid slug" message: "Alias must be a valid slug"
}), })
.optional(),
description: z.string().trim().optional(), description: z.string().trim().optional(),
provider: ExternalKmsUpdateInputSchema configuration: ExternalKmsUpdateInputSchema.optional()
}); });
export type UpdateExternalKmsType = z.infer<typeof UpdateExternalKmsSchema>; export type UpdateExternalKmsType = z.infer<typeof UpdateExternalKmsSchema>;
@@ -50,9 +50,10 @@ type Props = {
onCompleted: () => void; onCompleted: () => void;
onCancel: () => void; onCancel: () => void;
kms?: Kms; kms?: Kms;
mode?: "full" | "credentials";
}; };
export const AwsKmsForm = ({ onCompleted, onCancel, kms }: Props) => { export const AwsKmsForm = ({ onCompleted, onCancel, kms, mode = "full" }: Props) => {
const { const {
control, control,
handleSubmit, handleSubmit,
@@ -64,20 +65,20 @@ export const AwsKmsForm = ({ onCompleted, onCancel, kms }: Props) => {
defaultValues: { defaultValues: {
name: kms?.name, name: kms?.name,
description: kms?.description ?? "", description: kms?.description ?? "",
provider: { configuration: {
type: ExternalKmsProvider.Aws, type: ExternalKmsProvider.Aws,
inputs: { inputs: {
credential: { credential: {
type: kms?.external?.providerInput?.credential?.type, type: kms?.externalKms?.configuration?.credential?.type,
data: { data: {
accessKey: kms?.external?.providerInput?.credential?.data?.accessKey, accessKey: kms?.externalKms?.configuration?.credential?.data?.accessKey,
secretKey: kms?.external?.providerInput?.credential?.data?.secretKey, secretKey: kms?.externalKms?.configuration?.credential?.data?.secretKey,
assumeRoleArn: kms?.external?.providerInput?.credential?.data?.assumeRoleArn, assumeRoleArn: kms?.externalKms?.configuration?.credential?.data?.assumeRoleArn,
externalId: kms?.external?.providerInput?.credential?.data?.externalId externalId: kms?.externalKms?.configuration?.credential?.data?.externalId
} }
}, },
awsRegion: kms?.external?.providerInput?.awsRegion, awsRegion: kms?.externalKms?.configuration?.awsRegion,
kmsKeyId: kms?.external?.providerInput?.kmsKeyId kmsKeyId: kms?.externalKms?.configuration?.kmsKeyId
} }
} }
} }
@@ -85,30 +86,42 @@ export const AwsKmsForm = ({ onCompleted, onCancel, kms }: Props) => {
const { currentOrg } = useOrganization(); const { currentOrg } = useOrganization();
const { mutateAsync: addAwsExternalKms } = useAddExternalKms(currentOrg.id); 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 handleAwsKmsFormSubmit = async (data: AddExternalKmsType) => {
const { name, description, provider } = data; const { name, description, configuration } = data;
try { try {
if (kms) { if (kms) {
await updateAwsExternalKms({ if (mode === "credentials") {
kmsId: kms.id, await updateAwsExternalKms({
name, kmsId: kms.id,
description, configuration
provider });
}); } else {
await updateAwsExternalKms({
kmsId: kms.id,
name,
description
});
}
createNotification({ createNotification({
text: "Successfully updated AWS External KMS", text:
mode === "credentials"
? "Successfully updated AWS External KMS credentials"
: "Successfully updated AWS External KMS",
type: "success" type: "success"
}); });
} else { } else {
await addAwsExternalKms({ await addAwsExternalKms({
name, name,
description, description,
provider configuration
}); });
createNotification({ createNotification({
@@ -125,27 +138,31 @@ export const AwsKmsForm = ({ onCompleted, onCancel, kms }: Props) => {
return ( return (
<form onSubmit={handleSubmit(handleAwsKmsFormSubmit)} autoComplete="off"> <form onSubmit={handleSubmit(handleAwsKmsFormSubmit)} autoComplete="off">
{mode === "full" && (
<>
<Controller
control={control}
name="name"
render={({ field, fieldState: { error } }) => (
<FormControl label="Alias" errorText={error?.message} isError={Boolean(error)}>
<Input placeholder="" {...field} />
</FormControl>
)}
/>
<Controller
control={control}
name="description"
render={({ field, fieldState: { error } }) => (
<FormControl label="Description" errorText={error?.message} isError={Boolean(error)}>
<Input placeholder="" {...field} />
</FormControl>
)}
/>
</>
)}
<Controller <Controller
control={control} control={control}
name="name" name="configuration.inputs.credential.type"
render={({ field, fieldState: { error } }) => (
<FormControl label="Alias" errorText={error?.message} isError={Boolean(error)}>
<Input placeholder="" {...field} />
</FormControl>
)}
/>
<Controller
control={control}
name="description"
render={({ field, fieldState: { error } }) => (
<FormControl label="Description" errorText={error?.message} isError={Boolean(error)}>
<Input placeholder="" {...field} />
</FormControl>
)}
/>
<Controller
control={control}
name="provider.inputs.credential.type"
defaultValue={KmsAwsCredentialType.AssumeRole} defaultValue={KmsAwsCredentialType.AssumeRole}
render={({ field: { onChange, ...field }, fieldState: { error } }) => ( render={({ field: { onChange, ...field }, fieldState: { error } }) => (
<FormControl <FormControl
@@ -157,10 +174,10 @@ export const AwsKmsForm = ({ onCompleted, onCancel, kms }: Props) => {
defaultValue={field.value} defaultValue={field.value}
{...field} {...field}
onValueChange={(e) => { onValueChange={(e) => {
setValue("provider.inputs.credential.data.accessKey", ""); setValue("configuration.inputs.credential.data.accessKey", "");
setValue("provider.inputs.credential.data.secretKey", ""); setValue("configuration.inputs.credential.data.secretKey", "");
setValue("provider.inputs.credential.data.assumeRoleArn", ""); setValue("configuration.inputs.credential.data.assumeRoleArn", "");
setValue("provider.inputs.credential.data.externalId", ""); setValue("configuration.inputs.credential.data.externalId", "");
onChange(e); onChange(e);
}} }}
@@ -177,7 +194,7 @@ export const AwsKmsForm = ({ onCompleted, onCancel, kms }: Props) => {
<> <>
<Controller <Controller
control={control} control={control}
name="provider.inputs.credential.data.accessKey" name="configuration.inputs.credential.data.accessKey"
render={({ field, fieldState: { error } }) => ( render={({ field, fieldState: { error } }) => (
<FormControl <FormControl
label="Access Key ID" label="Access Key ID"
@@ -190,7 +207,7 @@ export const AwsKmsForm = ({ onCompleted, onCancel, kms }: Props) => {
/> />
<Controller <Controller
control={control} control={control}
name="provider.inputs.credential.data.secretKey" name="configuration.inputs.credential.data.secretKey"
render={({ field, fieldState: { error } }) => ( render={({ field, fieldState: { error } }) => (
<FormControl <FormControl
label="Secret Access Key" label="Secret Access Key"
@@ -206,7 +223,7 @@ export const AwsKmsForm = ({ onCompleted, onCancel, kms }: Props) => {
<> <>
<Controller <Controller
control={control} control={control}
name="provider.inputs.credential.data.assumeRoleArn" name="configuration.inputs.credential.data.assumeRoleArn"
render={({ field, fieldState: { error } }) => ( render={({ field, fieldState: { error } }) => (
<FormControl <FormControl
label="IAM Role ARN For Role Assumption" label="IAM Role ARN For Role Assumption"
@@ -219,7 +236,7 @@ export const AwsKmsForm = ({ onCompleted, onCancel, kms }: Props) => {
/> />
<Controller <Controller
control={control} control={control}
name="provider.inputs.credential.data.externalId" name="configuration.inputs.credential.data.externalId"
render={({ field, fieldState: { error } }) => ( render={({ field, fieldState: { error } }) => (
<FormControl <FormControl
label="Assume Role External ID" label="Assume Role External ID"
@@ -234,7 +251,7 @@ export const AwsKmsForm = ({ onCompleted, onCancel, kms }: Props) => {
)} )}
<Controller <Controller
control={control} control={control}
name="provider.inputs.awsRegion" name="configuration.inputs.awsRegion"
render={({ field: { onChange, ...field }, fieldState: { error } }) => ( render={({ field: { onChange, ...field }, fieldState: { error } }) => (
<FormControl label="AWS Region" errorText={error?.message} isError={Boolean(error)}> <FormControl label="AWS Region" errorText={error?.message} isError={Boolean(error)}>
<Select <Select
@@ -254,7 +271,7 @@ export const AwsKmsForm = ({ onCompleted, onCancel, kms }: Props) => {
/> />
<Controller <Controller
control={control} control={control}
name="provider.inputs.kmsKeyId" name="configuration.inputs.kmsKeyId"
render={({ field, fieldState: { error } }) => ( render={({ field, fieldState: { error } }) => (
<FormControl label="AWS KMS Key ID" errorText={error?.message} isError={Boolean(error)}> <FormControl label="AWS KMS Key ID" errorText={error?.message} isError={Boolean(error)}>
<Input placeholder="" {...field} /> <Input placeholder="" {...field} />
@@ -263,7 +280,7 @@ export const AwsKmsForm = ({ onCompleted, onCancel, kms }: Props) => {
/> />
<div className="mt-6 flex items-center space-x-4"> <div className="mt-6 flex items-center space-x-4">
<Button type="submit" isLoading={isSubmitting}> <Button type="submit" isLoading={isSubmitting}>
Save {mode === "credentials" ? "Update Credentials" : "Save"}
</Button> </Button>
<Button variant="outline_bg" onClick={onCancel}> <Button variant="outline_bg" onClick={onCancel}>
Cancel Cancel
@@ -12,22 +12,33 @@ type Props = {
onOpenChange: (state: boolean) => void; onOpenChange: (state: boolean) => void;
}; };
export const UpdateExternalKmsForm = ({ isOpen, kmsId, provider, onOpenChange }: Props) => { export const EditExternalKmsCredentialsModal = ({
isOpen,
kmsId,
provider,
onOpenChange
}: Props) => {
const { data: externalKms, isPending } = useGetExternalKmsById({ kmsId, provider }); const { data: externalKms, isPending } = useGetExternalKmsById({ kmsId, provider });
return ( return (
<Modal isOpen={isOpen} onOpenChange={onOpenChange}> <Modal isOpen={isOpen} onOpenChange={onOpenChange}>
<ModalContent title="Edit configuration" bodyClassName="overflow-visible"> <ModalContent
title="Edit Credentials"
subTitle="Update the credentials for this KMS."
bodyClassName="overflow-visible"
>
{isPending && <ContentLoader />} {isPending && <ContentLoader />}
{externalKms?.external?.provider === ExternalKmsProvider.Aws && ( {externalKms?.externalKms?.provider === ExternalKmsProvider.Aws && (
<AwsKmsForm <AwsKmsForm
kms={externalKms} kms={externalKms}
mode="credentials"
onCancel={() => onOpenChange(false)} onCancel={() => onOpenChange(false)}
onCompleted={() => onOpenChange(false)} onCompleted={() => onOpenChange(false)}
/> />
)} )}
{externalKms?.external?.provider === ExternalKmsProvider.Gcp && ( {externalKms?.externalKms?.provider === ExternalKmsProvider.Gcp && (
<GcpKmsForm <GcpKmsForm
kms={externalKms} kms={externalKms}
mode="credentials"
onCancel={() => onOpenChange(false)} onCancel={() => onOpenChange(false)}
onCompleted={() => onOpenChange(false)} onCompleted={() => onOpenChange(false)}
/> />
@@ -0,0 +1,108 @@
import { FormProvider, useForm } from "react-hook-form";
import { zodResolver } from "@hookform/resolvers/zod";
import { z } from "zod";
import { createNotification } from "@app/components/notifications";
import {
Button,
ContentLoader,
FormControl,
Input,
Modal,
ModalClose,
ModalContent
} from "@app/components/v2";
import { useOrganization } from "@app/context";
import { useGetExternalKmsById, useUpdateExternalKms } from "@app/hooks/api";
import { ExternalKmsProvider, Kms } from "@app/hooks/api/kms/types";
type Props = {
isOpen: boolean;
onOpenChange: (isOpen: boolean) => void;
kmsId: string;
provider: ExternalKmsProvider;
};
const formSchema = z.object({
name: z.string().min(1).trim(),
description: z.string().trim().optional()
});
type FormData = z.infer<typeof formSchema>;
type ContentProps = { kms: Kms; provider: ExternalKmsProvider; onComplete: () => void };
const Content = ({ kms, onComplete, provider }: ContentProps) => {
const { currentOrg } = useOrganization();
const { mutateAsync: updateExternalKms, isPending } = useUpdateExternalKms(
currentOrg.id,
provider
);
const form = useForm<FormData>({
resolver: zodResolver(formSchema),
defaultValues: {
name: kms.name,
description: kms.description ?? ""
}
});
const {
handleSubmit,
formState: { isDirty }
} = form;
const onSubmit = async (formData: FormData) => {
if (!kms) return;
await updateExternalKms({
kmsId: kms.id,
name: formData.name,
description: formData.description
});
createNotification({
text: "Successfully updated KMS details",
type: "success"
});
onComplete();
};
return (
<FormProvider {...form}>
<form onSubmit={handleSubmit(onSubmit)}>
<FormControl label="Alias">
<Input {...form.register("name")} />
</FormControl>
<FormControl label="Description">
<Input {...form.register("description")} />
</FormControl>
<div className="mt-6 flex items-center space-x-4">
<Button type="submit" isLoading={isPending} isDisabled={!isDirty}>
Update Details
</Button>
<ModalClose asChild>
<Button variant="outline_bg">Cancel</Button>
</ModalClose>
</div>
</form>
</FormProvider>
);
};
export const EditExternalKmsDetailsModal = ({ isOpen, onOpenChange, kmsId, provider }: Props) => {
const { data: kms, isPending } = useGetExternalKmsById({ kmsId, provider });
return (
<Modal isOpen={isOpen} onOpenChange={onOpenChange}>
<ModalContent
className="max-w-2xl"
title="Edit KMS Details"
subTitle="Update the name and description for this KMS."
>
{isPending && <ContentLoader />}
{kms && <Content kms={kms} provider={provider} onComplete={() => onOpenChange(false)} />}
</ModalContent>
</Modal>
);
};
@@ -22,7 +22,9 @@ import { UsePopUpState } from "@app/hooks/usePopUp";
type Props = { type Props = {
kms: KmsListEntry; kms: KmsListEntry;
handlePopUpOpen: ( handlePopUpOpen: (
popUpName: keyof UsePopUpState<["editExternalKms", "removeExternalKms", "upgradePlan"]>, popUpName: keyof UsePopUpState<
["editExternalKmsDetails", "editExternalKmsCredentials", "removeExternalKms", "upgradePlan"]
>,
data?: { data?: {
kmsId?: string; kmsId?: string;
name?: string; name?: string;
@@ -104,28 +106,52 @@ export const ExternalKmsItem = ({ kms, handlePopUpOpen, subscription }: Props) =
<DropdownMenuContent align="start" className="p-1"> <DropdownMenuContent align="start" className="p-1">
<OrgPermissionCan I={OrgPermissionActions.Edit} an={OrgPermissionSubjects.Kms}> <OrgPermissionCan I={OrgPermissionActions.Edit} an={OrgPermissionSubjects.Kms}>
{(isAllowed) => ( {(isAllowed) => (
<DropdownMenuItem <>
disabled={!isAllowed} <DropdownMenuItem
className={twMerge( disabled={!isAllowed}
!isAllowed && "pointer-events-none cursor-not-allowed opacity-50" className={twMerge(
)} !isAllowed && "pointer-events-none cursor-not-allowed opacity-50"
onClick={(e) => { )}
e.stopPropagation(); onClick={(e) => {
if (subscription && !subscription?.externalKms) { e.stopPropagation();
handlePopUpOpen("upgradePlan", { if (subscription && !subscription?.externalKms) {
isEnterpriseFeature: true handlePopUpOpen("upgradePlan", {
}); isEnterpriseFeature: true
return; });
} return;
}
handlePopUpOpen("editExternalKms", { handlePopUpOpen("editExternalKmsDetails", {
kmsId: kms.id, kmsId: kms.id,
provider: kms.externalKms.provider provider: kms.externalKms.provider
}); });
}} }}
> >
Edit Edit Details
</DropdownMenuItem> </DropdownMenuItem>
<DropdownMenuItem
disabled={!isAllowed}
className={twMerge(
!isAllowed && "pointer-events-none cursor-not-allowed opacity-50"
)}
onClick={(e) => {
e.stopPropagation();
if (subscription && !subscription?.externalKms) {
handlePopUpOpen("upgradePlan", {
isEnterpriseFeature: true
});
return;
}
handlePopUpOpen("editExternalKmsCredentials", {
kmsId: kms.id,
provider: kms.externalKms.provider
});
}}
>
Edit Credentials
</DropdownMenuItem>
</>
)} )}
</OrgPermissionCan> </OrgPermissionCan>
<OrgPermissionCan I={OrgPermissionActions.Delete} an={OrgPermissionSubjects.Kms}> <OrgPermissionCan I={OrgPermissionActions.Delete} an={OrgPermissionSubjects.Kms}>
@@ -24,6 +24,7 @@ type Props = {
onCompleted: () => void; onCompleted: () => void;
onCancel: () => void; onCancel: () => void;
kms?: Kms; kms?: Kms;
mode?: "full" | "credentials";
}; };
const GCP_REGIONS = [ const GCP_REGIONS = [
@@ -76,7 +77,7 @@ const formatOptionLabel = ({ value, label }: { value: string; label: string }) =
</div> </div>
); );
export const GcpKmsForm = ({ onCompleted, onCancel, kms }: Props) => { export const GcpKmsForm = ({ onCompleted, onCancel, kms, mode = "full" }: Props) => {
const [isCredentialValid, setIsCredentialValid] = useState<boolean>(false); const [isCredentialValid, setIsCredentialValid] = useState<boolean>(false);
const [keys, setKeys] = useState<{ value: string; label: string }[]>([]); const [keys, setKeys] = useState<{ value: string; label: string }[]>([]);
@@ -98,9 +99,9 @@ export const GcpKmsForm = ({ onCompleted, onCancel, kms }: Props) => {
gcpRegion: kms gcpRegion: kms
? { ? {
label: 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, : undefined,
keyObject: undefined keyObject: undefined
@@ -109,7 +110,10 @@ export const GcpKmsForm = ({ onCompleted, onCancel, kms }: Props) => {
const { currentOrg } = useOrganization(); const { currentOrg } = useOrganization();
const { mutateAsync: addGcpExternalKms } = useAddExternalKms(currentOrg.id); 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 } = const { mutateAsync: fetchGcpKeys, isPending: isFetchGcpKeysLoading } =
useExternalKmsFetchGcpKeys(currentOrg?.id); useExternalKmsFetchGcpKeys(currentOrg?.id);
@@ -152,21 +156,39 @@ export const GcpKmsForm = ({ onCompleted, onCancel, kms }: Props) => {
try { try {
if (kms) { if (kms) {
await updateGcpExternalKms({ if (mode === "credentials") {
kmsId: kms.id, await updateGcpExternalKms({
name, kmsId: kms.id,
description, name: kms.name,
provider: { description: kms.description,
type: ExternalKmsProvider.Gcp, configuration: {
inputs: { type: ExternalKmsProvider.Gcp,
gcpRegion, inputs: {
keyName: keyObject?.value gcpRegion,
keyName: keyObject?.value
}
} }
} });
}); } else {
await updateGcpExternalKms({
kmsId: kms.id,
name,
description,
configuration: {
type: ExternalKmsProvider.Gcp,
inputs: {
gcpRegion,
keyName: keyObject?.value
}
}
});
}
createNotification({ createNotification({
text: "Successfully updated GCP External KMS", text:
mode === "credentials"
? "Successfully updated GCP External KMS configuration"
: "Successfully updated GCP External KMS",
type: "success" type: "success"
}); });
} else { } else {
@@ -177,7 +199,7 @@ export const GcpKmsForm = ({ onCompleted, onCancel, kms }: Props) => {
await addGcpExternalKms({ await addGcpExternalKms({
name, name,
description, description,
provider: { configuration: {
type: ExternalKmsProvider.Gcp, type: ExternalKmsProvider.Gcp,
inputs: { inputs: {
gcpRegion, gcpRegion,
@@ -231,7 +253,9 @@ export const GcpKmsForm = ({ onCompleted, onCancel, kms }: Props) => {
setKeys(returnedKeys); setKeys(returnedKeys);
if (kms) { 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) { if (existingKey) {
setValue("keyObject", existingKey); setValue("keyObject", existingKey);
} }
@@ -260,24 +284,28 @@ export const GcpKmsForm = ({ onCompleted, onCancel, kms }: Props) => {
return ( return (
<form onSubmit={handleSubmit(handleGcpKmsFormSubmit)} autoComplete="off"> <form onSubmit={handleSubmit(handleGcpKmsFormSubmit)} autoComplete="off">
<Controller {mode === "full" && (
control={control} <>
name="name" <Controller
render={({ field, fieldState: { error } }) => ( control={control}
<FormControl label="Alias" errorText={error?.message} isError={Boolean(error)}> name="name"
<Input placeholder="" {...field} /> render={({ field, fieldState: { error } }) => (
</FormControl> <FormControl label="Alias" errorText={error?.message} isError={Boolean(error)}>
)} <Input placeholder="" {...field} />
/> </FormControl>
<Controller )}
control={control} />
name="description" <Controller
render={({ field, fieldState: { error } }) => ( control={control}
<FormControl label="Description" errorText={error?.message} isError={Boolean(error)}> name="description"
<Input placeholder="" {...field} /> render={({ field, fieldState: { error } }) => (
</FormControl> <FormControl label="Description" errorText={error?.message} isError={Boolean(error)}>
)} <Input placeholder="" {...field} />
/> </FormControl>
)}
/>
</>
)}
<Controller <Controller
control={control} control={control}
name="gcpRegion" name="gcpRegion"
@@ -350,7 +378,7 @@ export const GcpKmsForm = ({ onCompleted, onCancel, kms }: Props) => {
)} )}
<div className="mt-6 flex items-center space-x-4"> <div className="mt-6 flex items-center space-x-4">
<Button type="submit" isLoading={isSubmitting}> <Button type="submit" isLoading={isSubmitting}>
Save {mode === "credentials" ? "Update Configuration" : "Save"}
</Button> </Button>
<Button variant="outline_bg" onClick={onCancel}> <Button variant="outline_bg" onClick={onCancel}>
Cancel Cancel
@@ -28,8 +28,9 @@ import { useGetExternalKmsList, useRemoveExternalKms } from "@app/hooks/api";
import { ExternalKmsProvider } from "@app/hooks/api/kms/types"; import { ExternalKmsProvider } from "@app/hooks/api/kms/types";
import { AddExternalKmsForm } from "./AddExternalKmsForm"; import { AddExternalKmsForm } from "./AddExternalKmsForm";
import { EditExternalKmsCredentialsModal } from "./EditExternalKmsCredentialsModal";
import { EditExternalKmsDetailsModal } from "./EditExternalKmsDetailsModal";
import { ExternalKmsItem } from "./ExternalKmsItem"; import { ExternalKmsItem } from "./ExternalKmsItem";
import { UpdateExternalKmsForm } from "./UpdateExternalKmsForm";
export const OrgEncryptionTab = withPermission( export const OrgEncryptionTab = withPermission(
() => { () => {
@@ -39,7 +40,8 @@ export const OrgEncryptionTab = withPermission(
const { popUp, handlePopUpOpen, handlePopUpToggle } = usePopUp([ const { popUp, handlePopUpOpen, handlePopUpToggle } = usePopUp([
"upgradePlan", "upgradePlan",
"addExternalKms", "addExternalKms",
"editExternalKms", "editExternalKmsDetails",
"editExternalKmsCredentials",
"removeExternalKms" "removeExternalKms"
] as const); ] as const);
const { data: externalKmsList, isPending: isExternalKmsListLoading } = const { data: externalKmsList, isPending: isExternalKmsListLoading } =
@@ -130,11 +132,21 @@ export const OrgEncryptionTab = withPermission(
isOpen={popUp.addExternalKms.isOpen} isOpen={popUp.addExternalKms.isOpen}
onToggle={(state) => handlePopUpToggle("addExternalKms", state)} onToggle={(state) => handlePopUpToggle("addExternalKms", state)}
/> />
<UpdateExternalKmsForm <EditExternalKmsDetailsModal
isOpen={popUp.editExternalKms.isOpen} isOpen={popUp.editExternalKmsDetails.isOpen}
kmsId={(popUp.editExternalKms.data as { kmsId: string })?.kmsId} kmsId={(popUp.editExternalKmsDetails.data as { kmsId: string })?.kmsId}
provider={(popUp.editExternalKms.data as { provider: ExternalKmsProvider })?.provider} provider={
onOpenChange={(state) => handlePopUpToggle("editExternalKms", state)} (popUp.editExternalKmsDetails.data as { provider: ExternalKmsProvider })?.provider
}
onOpenChange={(state) => handlePopUpToggle("editExternalKmsDetails", state)}
/>
<EditExternalKmsCredentialsModal
isOpen={popUp.editExternalKmsCredentials.isOpen}
kmsId={(popUp.editExternalKmsCredentials.data as { kmsId: string })?.kmsId}
provider={
(popUp.editExternalKmsCredentials.data as { provider: ExternalKmsProvider })?.provider
}
onOpenChange={(state) => handlePopUpToggle("editExternalKmsCredentials", state)}
/> />
<DeleteActionModal <DeleteActionModal
isOpen={popUp.removeExternalKms.isOpen} isOpen={popUp.removeExternalKms.isOpen}