feat(KMS): New external KMS support for Google GCP KMS (#2825)

* feat(KMS): New external KMS support for Google GCP KMS
This commit is contained in:
McPizza
2024-12-03 18:14:42 +01:00
committed by GitHub
parent 7728a4793b
commit 5ceb30f43f
30 changed files with 1095 additions and 49 deletions
+1
View File
@@ -1,5 +1,6 @@
export {
useAddExternalKms,
useExternalKmsFetchGcpKeys,
useLoadProjectKmsBackup,
useRemoveExternalKms,
useUpdateExternalKms,
+49 -2
View File
@@ -3,7 +3,13 @@ import { useMutation, useQueryClient } from "@tanstack/react-query";
import { apiRequest } from "@app/config/request";
import { kmsKeys } from "./queries";
import { AddExternalKmsType, KmsType } from "./types";
import {
AddExternalKmsType,
ExternalKmsGcpSchemaType,
KmsGcpKeyFetchAuthType,
KmsType,
UpdateExternalKmsType
} from "./types";
export const useAddExternalKms = (orgId: string) => {
const queryClient = useQueryClient();
@@ -33,7 +39,7 @@ export const useUpdateExternalKms = (orgId: string) => {
provider
}: {
kmsId: string;
} & AddExternalKmsType) => {
} & UpdateExternalKmsType) => {
const { data } = await apiRequest.patch(`/api/v1/external-kms/${kmsId}`, {
name,
description,
@@ -96,3 +102,44 @@ export const useLoadProjectKmsBackup = (projectId: string) => {
}
});
};
export const useExternalKmsFetchGcpKeys = (orgId: string) => {
const queryClient = useQueryClient();
return useMutation({
mutationFn: async ({
gcpRegion,
...rest
}: Pick<ExternalKmsGcpSchemaType, "gcpRegion"> &
(
| (Pick<ExternalKmsGcpSchemaType, KmsGcpKeyFetchAuthType.Credential> & {
[KmsGcpKeyFetchAuthType.Kms]?: never;
})
| {
[KmsGcpKeyFetchAuthType.Kms]: string;
[KmsGcpKeyFetchAuthType.Credential]?: never;
}
)): Promise<{ keys: string[] }> => {
const {
[KmsGcpKeyFetchAuthType.Credential]: credential,
[KmsGcpKeyFetchAuthType.Kms]: kmsId
} = rest;
if ((credential && kmsId) || (!credential && !kmsId)) {
throw new Error(
`Either '${KmsGcpKeyFetchAuthType.Credential}' or '${KmsGcpKeyFetchAuthType.Kms}' must be provided, but not both.`
);
}
const { data } = await apiRequest.post("/api/v1/external-kms/gcp/keys", {
authMethod: credential ? KmsGcpKeyFetchAuthType.Credential : KmsGcpKeyFetchAuthType.Kms,
region: gcpRegion,
...rest
});
return data;
},
onSuccess: () => {
queryClient.invalidateQueries(kmsKeys.getExternalKmsList(orgId));
}
});
};
+101 -2
View File
@@ -35,7 +35,8 @@ export enum KmsType {
}
export enum ExternalKmsProvider {
AWS = "aws"
Aws = "aws",
Gcp = "gcp"
}
export const INTERNAL_KMS_KEY_ID = "internal";
@@ -44,6 +45,10 @@ export enum KmsAwsCredentialType {
AssumeRole = "assume-role",
AccessKey = "access-key"
}
// Google uses snake_case for their enum values and we need to match that
export enum KmsGcpCredentialType {
ServiceAccount = "service_account"
}
export const ExternalKmsAwsSchema = z.object({
credential: z
@@ -83,8 +88,34 @@ export const ExternalKmsAwsSchema = z.object({
)
});
export const ExternalKmsGcpCredentialSchema = z.object({
type: z.literal(KmsGcpCredentialType.ServiceAccount),
project_id: z.string().min(1),
private_key_id: z.string().min(1),
private_key: z.string().min(1),
client_email: z.string().min(1),
client_id: z.string().min(1),
auth_uri: z.string().min(1),
token_uri: z.string().min(1),
auth_provider_x509_cert_url: z.string().min(1),
client_x509_cert_url: z.string().min(1),
universe_domain: z.string().min(1)
});
export type ExternalKmsGcpCredentialSchemaType = z.infer<typeof ExternalKmsGcpCredentialSchema>;
export const ExternalKmsGcpSchema = z.object({
credential: ExternalKmsGcpCredentialSchema.describe(
"GCP Service Account JSON credential to connect"
),
gcpRegion: z.string().min(1).trim().describe("GCP region where the KMS key is located"),
keyName: z.string().min(1).trim().describe("GCP key name")
});
export type ExternalKmsGcpSchemaType = z.infer<typeof ExternalKmsGcpSchema>;
export const ExternalKmsInputSchema = z.discriminatedUnion("type", [
z.object({ type: z.literal(ExternalKmsProvider.AWS), inputs: ExternalKmsAwsSchema })
z.object({ type: z.literal(ExternalKmsProvider.Aws), inputs: ExternalKmsAwsSchema }),
z.object({ type: z.literal(ExternalKmsProvider.Gcp), inputs: ExternalKmsGcpSchema })
]);
export const AddExternalKmsSchema = z.object({
@@ -100,3 +131,71 @@ export const AddExternalKmsSchema = z.object({
});
export type AddExternalKmsType = z.infer<typeof AddExternalKmsSchema>;
// 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.Gcp),
inputs: ExternalKmsGcpSchema.pick({ gcpRegion: true, keyName: true })
})
]);
export const UpdateExternalKmsSchema = z.object({
name: z
.string()
.trim()
.min(1)
.refine((v) => slugify(v) === v, {
message: "Alias must be a valid slug"
}),
description: z.string().trim().optional(),
provider: ExternalKmsUpdateInputSchema
});
export type UpdateExternalKmsType = z.infer<typeof UpdateExternalKmsSchema>;
const GCP_CREDENTIAL_MAX_FILE_SIZE = 8 * 1024; // 8KB
const GCP_CREDENTIAL_ACCEPTED_FILE_TYPES = ["application/json"];
const AddExternalKmsGcpFormSchemaStandardInputs = z.object({
keyObject: z
.object({ label: z.string().trim(), value: z.string().trim() })
.describe("GCP key name"),
gcpRegion: z.object({ label: z.string().trim(), value: z.string().trim() }).describe("GCP Region")
});
export const AddExternalKmsGcpFormSchema = z.discriminatedUnion("formType", [
z
.object({
formType: z.literal("newGcpKms"),
// `FileList` is a browser-only (window-specific) type, so we need to handle it differently on the server to avoid SSR errors
credentialFile:
typeof window === "undefined"
? z.any()
: z
.instanceof(FileList)
.refine((files) => files?.length === 1, "Image is required.")
.refine(
(files) => files?.[0]?.size <= GCP_CREDENTIAL_MAX_FILE_SIZE,
"Max file size is 8KB."
)
.refine(
(files) => GCP_CREDENTIAL_ACCEPTED_FILE_TYPES.includes(files?.[0]?.type),
"Only .json files are accepted."
)
})
.merge(AddExternalKmsGcpFormSchemaStandardInputs)
.merge(AddExternalKmsSchema.pick({ name: true, description: true })),
z
.object({ formType: z.literal("updateGcpKms") })
.merge(AddExternalKmsGcpFormSchemaStandardInputs)
.merge(AddExternalKmsSchema.pick({ name: true, description: true }))
]);
export type AddExternalKmsGcpFormSchemaType = z.infer<typeof AddExternalKmsGcpFormSchema>;
export enum KmsGcpKeyFetchAuthType {
Credential = "credential",
Kms = "kmsId"
}