mirror of
https://github.com/awatertrevi/infisical.git
synced 2026-10-06 07:26:45 +00:00
Merge pull request #3093 from Infisical/cmek-additions
Improvement: CMEK Additions and Normalization
This commit is contained in:
@@ -1,4 +1,5 @@
|
|||||||
import { Knex } from "knex";
|
import { Knex } from "knex";
|
||||||
|
|
||||||
import { TableName } from "../schemas";
|
import { TableName } from "../schemas";
|
||||||
|
|
||||||
export async function up(knex: Knex): Promise<void> {
|
export async function up(knex: Knex): Promise<void> {
|
||||||
|
|||||||
@@ -0,0 +1,27 @@
|
|||||||
|
import { Knex } from "knex";
|
||||||
|
|
||||||
|
import { TableName } from "@app/db/schemas";
|
||||||
|
|
||||||
|
export async function up(knex: Knex): Promise<void> {
|
||||||
|
if (await knex.schema.hasTable(TableName.KmsKey)) {
|
||||||
|
const hasSlugCol = await knex.schema.hasColumn(TableName.KmsKey, "slug");
|
||||||
|
|
||||||
|
if (hasSlugCol) {
|
||||||
|
await knex.schema.alterTable(TableName.KmsKey, (t) => {
|
||||||
|
t.dropColumn("slug");
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function down(knex: Knex): Promise<void> {
|
||||||
|
if (await knex.schema.hasTable(TableName.KmsKey)) {
|
||||||
|
const hasSlugCol = await knex.schema.hasColumn(TableName.KmsKey, "slug");
|
||||||
|
|
||||||
|
if (!hasSlugCol) {
|
||||||
|
await knex.schema.alterTable(TableName.KmsKey, (t) => {
|
||||||
|
t.string("slug", 32);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -16,8 +16,7 @@ export const KmsKeysSchema = z.object({
|
|||||||
name: z.string(),
|
name: z.string(),
|
||||||
createdAt: z.date(),
|
createdAt: z.date(),
|
||||||
updatedAt: z.date(),
|
updatedAt: z.date(),
|
||||||
projectId: z.string().nullable().optional(),
|
projectId: z.string().nullable().optional()
|
||||||
slug: z.string().nullable().optional()
|
|
||||||
});
|
});
|
||||||
|
|
||||||
export type TKmsKeys = z.infer<typeof KmsKeysSchema>;
|
export type TKmsKeys = z.infer<typeof KmsKeysSchema>;
|
||||||
|
|||||||
@@ -223,6 +223,7 @@ export enum EventType {
|
|||||||
UPDATE_CMEK = "update-cmek",
|
UPDATE_CMEK = "update-cmek",
|
||||||
DELETE_CMEK = "delete-cmek",
|
DELETE_CMEK = "delete-cmek",
|
||||||
GET_CMEKS = "get-cmeks",
|
GET_CMEKS = "get-cmeks",
|
||||||
|
GET_CMEK = "get-cmek",
|
||||||
CMEK_ENCRYPT = "cmek-encrypt",
|
CMEK_ENCRYPT = "cmek-encrypt",
|
||||||
CMEK_DECRYPT = "cmek-decrypt",
|
CMEK_DECRYPT = "cmek-decrypt",
|
||||||
UPDATE_EXTERNAL_GROUP_ORG_ROLE_MAPPINGS = "update-external-group-org-role-mapping",
|
UPDATE_EXTERNAL_GROUP_ORG_ROLE_MAPPINGS = "update-external-group-org-role-mapping",
|
||||||
@@ -1847,6 +1848,13 @@ interface GetCmeksEvent {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface GetCmekEvent {
|
||||||
|
type: EventType.GET_CMEK;
|
||||||
|
metadata: {
|
||||||
|
keyId: string;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
interface CmekEncryptEvent {
|
interface CmekEncryptEvent {
|
||||||
type: EventType.CMEK_ENCRYPT;
|
type: EventType.CMEK_ENCRYPT;
|
||||||
metadata: {
|
metadata: {
|
||||||
@@ -2237,6 +2245,7 @@ export type Event =
|
|||||||
| CreateCmekEvent
|
| CreateCmekEvent
|
||||||
| UpdateCmekEvent
|
| UpdateCmekEvent
|
||||||
| DeleteCmekEvent
|
| DeleteCmekEvent
|
||||||
|
| GetCmekEvent
|
||||||
| GetCmeksEvent
|
| GetCmeksEvent
|
||||||
| CmekEncryptEvent
|
| CmekEncryptEvent
|
||||||
| CmekDecryptEvent
|
| CmekDecryptEvent
|
||||||
|
|||||||
@@ -1593,6 +1593,13 @@ export const KMS = {
|
|||||||
orderDirection: "The direction to order keys in.",
|
orderDirection: "The direction to order keys in.",
|
||||||
search: "The text string to filter key names by."
|
search: "The text string to filter key names by."
|
||||||
},
|
},
|
||||||
|
GET_KEY_BY_ID: {
|
||||||
|
keyId: "The ID of the KMS key to retrieve."
|
||||||
|
},
|
||||||
|
GET_KEY_BY_NAME: {
|
||||||
|
keyName: "The name of the KMS key to retrieve.",
|
||||||
|
projectId: "The ID of the project the key belongs to."
|
||||||
|
},
|
||||||
ENCRYPT: {
|
ENCRYPT: {
|
||||||
keyId: "The ID of the key to encrypt the data with.",
|
keyId: "The ID of the key to encrypt the data with.",
|
||||||
plaintext: "The plaintext to be encrypted (base64 encoded)."
|
plaintext: "The plaintext to be encrypted (base64 encoded)."
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import { buildDynamicKnexQuery, TKnexDynamicOperator } from "./dynamic";
|
|||||||
|
|
||||||
export * from "./connection";
|
export * from "./connection";
|
||||||
export * from "./join";
|
export * from "./join";
|
||||||
|
export * from "./prependTableNameToFindFilter";
|
||||||
export * from "./select";
|
export * from "./select";
|
||||||
|
|
||||||
export const withTransaction = <K extends object>(db: Knex, dal: K) => ({
|
export const withTransaction = <K extends object>(db: Knex, dal: K) => ({
|
||||||
|
|||||||
@@ -0,0 +1,13 @@
|
|||||||
|
import { TableName } from "@app/db/schemas";
|
||||||
|
import { buildFindFilter } from "@app/lib/knex/index";
|
||||||
|
|
||||||
|
type TFindFilterParameters = Parameters<typeof buildFindFilter<object>>[0];
|
||||||
|
|
||||||
|
export const prependTableNameToFindFilter = (tableName: TableName, filterObj: object): TFindFilterParameters =>
|
||||||
|
Object.fromEntries(
|
||||||
|
Object.entries(filterObj).map(([key, value]) =>
|
||||||
|
key.startsWith("$")
|
||||||
|
? [key, prependTableNameToFindFilter(tableName, value as object)]
|
||||||
|
: [`${tableName}.${key}`, value]
|
||||||
|
)
|
||||||
|
);
|
||||||
@@ -15,6 +15,10 @@ import { CmekOrderBy } from "@app/services/cmek/cmek-types";
|
|||||||
const keyNameSchema = slugSchema({ min: 1, max: 32, field: "Name" });
|
const keyNameSchema = slugSchema({ min: 1, max: 32, field: "Name" });
|
||||||
const keyDescriptionSchema = z.string().trim().max(500).optional();
|
const keyDescriptionSchema = z.string().trim().max(500).optional();
|
||||||
|
|
||||||
|
const CmekSchema = KmsKeysSchema.merge(InternalKmsSchema.pick({ version: true, encryptionAlgorithm: true })).omit({
|
||||||
|
isReserved: true
|
||||||
|
});
|
||||||
|
|
||||||
const base64Schema = z.string().superRefine((val, ctx) => {
|
const base64Schema = z.string().superRefine((val, ctx) => {
|
||||||
if (!isBase64(val)) {
|
if (!isBase64(val)) {
|
||||||
ctx.addIssue({
|
ctx.addIssue({
|
||||||
@@ -53,7 +57,7 @@ export const registerCmekRouter = async (server: FastifyZodProvider) => {
|
|||||||
}),
|
}),
|
||||||
response: {
|
response: {
|
||||||
200: z.object({
|
200: z.object({
|
||||||
key: KmsKeysSchema
|
key: CmekSchema
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -106,7 +110,7 @@ export const registerCmekRouter = async (server: FastifyZodProvider) => {
|
|||||||
}),
|
}),
|
||||||
response: {
|
response: {
|
||||||
200: z.object({
|
200: z.object({
|
||||||
key: KmsKeysSchema
|
key: CmekSchema
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -150,7 +154,7 @@ export const registerCmekRouter = async (server: FastifyZodProvider) => {
|
|||||||
}),
|
}),
|
||||||
response: {
|
response: {
|
||||||
200: z.object({
|
200: z.object({
|
||||||
key: KmsKeysSchema
|
key: CmekSchema
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -201,7 +205,7 @@ export const registerCmekRouter = async (server: FastifyZodProvider) => {
|
|||||||
}),
|
}),
|
||||||
response: {
|
response: {
|
||||||
200: z.object({
|
200: z.object({
|
||||||
keys: KmsKeysSchema.merge(InternalKmsSchema.pick({ version: true, encryptionAlgorithm: true })).array(),
|
keys: CmekSchema.array(),
|
||||||
totalCount: z.number()
|
totalCount: z.number()
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -230,6 +234,92 @@ export const registerCmekRouter = async (server: FastifyZodProvider) => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
server.route({
|
||||||
|
method: "GET",
|
||||||
|
url: "/keys/:keyId",
|
||||||
|
config: {
|
||||||
|
rateLimit: readLimit
|
||||||
|
},
|
||||||
|
schema: {
|
||||||
|
description: "Get KMS key by ID",
|
||||||
|
params: z.object({
|
||||||
|
keyId: z.string().uuid().describe(KMS.GET_KEY_BY_ID.keyId)
|
||||||
|
}),
|
||||||
|
response: {
|
||||||
|
200: z.object({
|
||||||
|
key: CmekSchema
|
||||||
|
})
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
||||||
|
handler: async (req) => {
|
||||||
|
const {
|
||||||
|
params: { keyId },
|
||||||
|
permission
|
||||||
|
} = req;
|
||||||
|
|
||||||
|
const key = await server.services.cmek.findCmekById(keyId, permission);
|
||||||
|
|
||||||
|
await server.services.auditLog.createAuditLog({
|
||||||
|
...req.auditLogInfo,
|
||||||
|
projectId: key.projectId!,
|
||||||
|
event: {
|
||||||
|
type: EventType.GET_CMEK,
|
||||||
|
metadata: {
|
||||||
|
keyId: key.id
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return { key };
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
server.route({
|
||||||
|
method: "GET",
|
||||||
|
url: "/keys/key-name/:keyName",
|
||||||
|
config: {
|
||||||
|
rateLimit: readLimit
|
||||||
|
},
|
||||||
|
schema: {
|
||||||
|
description: "Get KMS key by Name",
|
||||||
|
params: z.object({
|
||||||
|
keyName: slugSchema({ field: "Key name" }).describe(KMS.GET_KEY_BY_NAME.keyName)
|
||||||
|
}),
|
||||||
|
querystring: z.object({
|
||||||
|
projectId: z.string().min(1, "Project ID is required").describe(KMS.GET_KEY_BY_NAME.projectId)
|
||||||
|
}),
|
||||||
|
response: {
|
||||||
|
200: z.object({
|
||||||
|
key: CmekSchema
|
||||||
|
})
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
||||||
|
handler: async (req) => {
|
||||||
|
const {
|
||||||
|
params: { keyName },
|
||||||
|
query: { projectId },
|
||||||
|
permission
|
||||||
|
} = req;
|
||||||
|
|
||||||
|
const key = await server.services.cmek.findCmekByName(keyName, projectId, permission);
|
||||||
|
|
||||||
|
await server.services.auditLog.createAuditLog({
|
||||||
|
...req.auditLogInfo,
|
||||||
|
projectId: key.projectId!,
|
||||||
|
event: {
|
||||||
|
type: EventType.GET_CMEK,
|
||||||
|
metadata: {
|
||||||
|
keyId: key.id
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return { key };
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
// encrypt data
|
// encrypt data
|
||||||
server.route({
|
server.route({
|
||||||
method: "POST",
|
method: "POST",
|
||||||
|
|||||||
@@ -3,7 +3,8 @@ import { ForbiddenError } from "@casl/ability";
|
|||||||
import { ActionProjectType, ProjectType } from "@app/db/schemas";
|
import { ActionProjectType, ProjectType } from "@app/db/schemas";
|
||||||
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service";
|
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service";
|
||||||
import { ProjectPermissionCmekActions, ProjectPermissionSub } from "@app/ee/services/permission/project-permission";
|
import { ProjectPermissionCmekActions, ProjectPermissionSub } from "@app/ee/services/permission/project-permission";
|
||||||
import { BadRequestError, NotFoundError } from "@app/lib/errors";
|
import { DatabaseErrorCode } from "@app/lib/error-codes";
|
||||||
|
import { BadRequestError, DatabaseError, NotFoundError } from "@app/lib/errors";
|
||||||
import { OrgServiceActor } from "@app/lib/types";
|
import { OrgServiceActor } from "@app/lib/types";
|
||||||
import {
|
import {
|
||||||
TCmekDecryptDTO,
|
TCmekDecryptDTO,
|
||||||
@@ -44,17 +45,31 @@ export const cmekServiceFactory = ({ kmsService, kmsDAL, permissionService, proj
|
|||||||
});
|
});
|
||||||
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionCmekActions.Create, ProjectPermissionSub.Cmek);
|
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionCmekActions.Create, ProjectPermissionSub.Cmek);
|
||||||
|
|
||||||
const cmek = await kmsService.generateKmsKey({
|
try {
|
||||||
...dto,
|
const cmek = await kmsService.generateKmsKey({
|
||||||
projectId,
|
...dto,
|
||||||
isReserved: false
|
projectId,
|
||||||
});
|
isReserved: false
|
||||||
|
});
|
||||||
|
|
||||||
return cmek;
|
return {
|
||||||
|
...cmek,
|
||||||
|
version: 1,
|
||||||
|
encryptionAlgorithm: dto.encryptionAlgorithm
|
||||||
|
};
|
||||||
|
} catch (err) {
|
||||||
|
if (err instanceof DatabaseError && (err.error as { code: string })?.code === DatabaseErrorCode.UniqueViolation) {
|
||||||
|
throw new BadRequestError({
|
||||||
|
message: `A KMS key with the name "${dto.name}" already exists for the project with ID "${projectId}"`
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const updateCmekById = async ({ keyId, ...data }: TUpdabteCmekByIdDTO, actor: OrgServiceActor) => {
|
const updateCmekById = async ({ keyId, ...data }: TUpdabteCmekByIdDTO, actor: OrgServiceActor) => {
|
||||||
const key = await kmsDAL.findById(keyId);
|
const key = await kmsDAL.findCmekById(keyId);
|
||||||
|
|
||||||
if (!key) throw new NotFoundError({ message: `Key with ID ${keyId} not found` });
|
if (!key) throw new NotFoundError({ message: `Key with ID ${keyId} not found` });
|
||||||
|
|
||||||
@@ -71,13 +86,27 @@ export const cmekServiceFactory = ({ kmsService, kmsDAL, permissionService, proj
|
|||||||
|
|
||||||
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionCmekActions.Edit, ProjectPermissionSub.Cmek);
|
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionCmekActions.Edit, ProjectPermissionSub.Cmek);
|
||||||
|
|
||||||
const cmek = await kmsDAL.updateById(keyId, data);
|
try {
|
||||||
|
const cmek = await kmsDAL.updateById(keyId, data);
|
||||||
|
|
||||||
return cmek;
|
return {
|
||||||
|
...cmek,
|
||||||
|
version: key.version,
|
||||||
|
encryptionAlgorithm: key.encryptionAlgorithm
|
||||||
|
};
|
||||||
|
} catch (err) {
|
||||||
|
if (err instanceof DatabaseError && (err.error as { code: string })?.code === DatabaseErrorCode.UniqueViolation) {
|
||||||
|
throw new BadRequestError({
|
||||||
|
message: `A KMS key with the name "${data.name!}" already exists for the project with ID "${key.projectId}"`
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const deleteCmekById = async (keyId: string, actor: OrgServiceActor) => {
|
const deleteCmekById = async (keyId: string, actor: OrgServiceActor) => {
|
||||||
const key = await kmsDAL.findById(keyId);
|
const key = await kmsDAL.findCmekById(keyId);
|
||||||
|
|
||||||
if (!key) throw new NotFoundError({ message: `Key with ID ${keyId} not found` });
|
if (!key) throw new NotFoundError({ message: `Key with ID ${keyId} not found` });
|
||||||
|
|
||||||
@@ -94,9 +123,9 @@ export const cmekServiceFactory = ({ kmsService, kmsDAL, permissionService, proj
|
|||||||
|
|
||||||
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionCmekActions.Delete, ProjectPermissionSub.Cmek);
|
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionCmekActions.Delete, ProjectPermissionSub.Cmek);
|
||||||
|
|
||||||
const cmek = kmsDAL.deleteById(keyId);
|
await kmsDAL.deleteById(keyId);
|
||||||
|
|
||||||
return cmek;
|
return key;
|
||||||
};
|
};
|
||||||
|
|
||||||
const listCmeksByProjectId = async (
|
const listCmeksByProjectId = async (
|
||||||
@@ -120,15 +149,58 @@ export const cmekServiceFactory = ({ kmsService, kmsDAL, permissionService, proj
|
|||||||
|
|
||||||
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionCmekActions.Read, ProjectPermissionSub.Cmek);
|
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionCmekActions.Read, ProjectPermissionSub.Cmek);
|
||||||
|
|
||||||
const { keys: cmeks, totalCount } = await kmsDAL.findKmsKeysByProjectId({ projectId, ...filters });
|
const { keys: cmeks, totalCount } = await kmsDAL.listCmeksByProjectId({ projectId, ...filters });
|
||||||
|
|
||||||
return { cmeks, totalCount };
|
return { cmeks, totalCount };
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const findCmekById = async (keyId: string, actor: OrgServiceActor) => {
|
||||||
|
const key = await kmsDAL.findCmekById(keyId);
|
||||||
|
|
||||||
|
if (!key) throw new NotFoundError({ message: `Key with ID "${keyId}" not found` });
|
||||||
|
|
||||||
|
if (!key.projectId || key.isReserved) throw new BadRequestError({ message: "Key is not customer managed" });
|
||||||
|
|
||||||
|
const { permission } = await permissionService.getProjectPermission({
|
||||||
|
actor: actor.type,
|
||||||
|
actorId: actor.id,
|
||||||
|
projectId: key.projectId,
|
||||||
|
actorAuthMethod: actor.authMethod,
|
||||||
|
actorOrgId: actor.orgId,
|
||||||
|
actionProjectType: ActionProjectType.KMS
|
||||||
|
});
|
||||||
|
|
||||||
|
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionCmekActions.Read, ProjectPermissionSub.Cmek);
|
||||||
|
|
||||||
|
return key;
|
||||||
|
};
|
||||||
|
|
||||||
|
const findCmekByName = async (keyName: string, projectId: string, actor: OrgServiceActor) => {
|
||||||
|
const key = await kmsDAL.findCmekByName(keyName, projectId);
|
||||||
|
|
||||||
|
if (!key)
|
||||||
|
throw new NotFoundError({ message: `Key with name "${keyName}" not found for project with ID "${projectId}"` });
|
||||||
|
|
||||||
|
if (!key.projectId || key.isReserved) throw new BadRequestError({ message: "Key is not customer managed" });
|
||||||
|
|
||||||
|
const { permission } = await permissionService.getProjectPermission({
|
||||||
|
actor: actor.type,
|
||||||
|
actorId: actor.id,
|
||||||
|
projectId: key.projectId,
|
||||||
|
actorAuthMethod: actor.authMethod,
|
||||||
|
actorOrgId: actor.orgId,
|
||||||
|
actionProjectType: ActionProjectType.KMS
|
||||||
|
});
|
||||||
|
|
||||||
|
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionCmekActions.Read, ProjectPermissionSub.Cmek);
|
||||||
|
|
||||||
|
return key;
|
||||||
|
};
|
||||||
|
|
||||||
const cmekEncrypt = async ({ keyId, plaintext }: TCmekEncryptDTO, actor: OrgServiceActor) => {
|
const cmekEncrypt = async ({ keyId, plaintext }: TCmekEncryptDTO, actor: OrgServiceActor) => {
|
||||||
const key = await kmsDAL.findById(keyId);
|
const key = await kmsDAL.findById(keyId);
|
||||||
|
|
||||||
if (!key) throw new NotFoundError({ message: `Key with ID ${keyId} not found` });
|
if (!key) throw new NotFoundError({ message: `Key with ID "${keyId}" not found` });
|
||||||
|
|
||||||
if (!key.projectId || key.isReserved) throw new BadRequestError({ message: "Key is not customer managed" });
|
if (!key.projectId || key.isReserved) throw new BadRequestError({ message: "Key is not customer managed" });
|
||||||
|
|
||||||
@@ -155,7 +227,7 @@ export const cmekServiceFactory = ({ kmsService, kmsDAL, permissionService, proj
|
|||||||
const cmekDecrypt = async ({ keyId, ciphertext }: TCmekDecryptDTO, actor: OrgServiceActor) => {
|
const cmekDecrypt = async ({ keyId, ciphertext }: TCmekDecryptDTO, actor: OrgServiceActor) => {
|
||||||
const key = await kmsDAL.findById(keyId);
|
const key = await kmsDAL.findById(keyId);
|
||||||
|
|
||||||
if (!key) throw new NotFoundError({ message: `Key with ID ${keyId} not found` });
|
if (!key) throw new NotFoundError({ message: `Key with ID "${keyId}" not found` });
|
||||||
|
|
||||||
if (!key.projectId || key.isReserved) throw new BadRequestError({ message: "Key is not customer managed" });
|
if (!key.projectId || key.isReserved) throw new BadRequestError({ message: "Key is not customer managed" });
|
||||||
|
|
||||||
@@ -185,6 +257,8 @@ export const cmekServiceFactory = ({ kmsService, kmsDAL, permissionService, proj
|
|||||||
deleteCmekById,
|
deleteCmekById,
|
||||||
listCmeksByProjectId,
|
listCmeksByProjectId,
|
||||||
cmekEncrypt,
|
cmekEncrypt,
|
||||||
cmekDecrypt
|
cmekDecrypt,
|
||||||
|
findCmekById,
|
||||||
|
findCmekByName
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -3,12 +3,32 @@ import { Knex } from "knex";
|
|||||||
import { TDbClient } from "@app/db";
|
import { TDbClient } from "@app/db";
|
||||||
import { KmsKeysSchema, TableName, TInternalKms, TKmsKeys } from "@app/db/schemas";
|
import { KmsKeysSchema, TableName, TInternalKms, TKmsKeys } from "@app/db/schemas";
|
||||||
import { DatabaseError } from "@app/lib/errors";
|
import { DatabaseError } from "@app/lib/errors";
|
||||||
import { ormify, selectAllTableCols } from "@app/lib/knex";
|
import { buildFindFilter, ormify, prependTableNameToFindFilter, selectAllTableCols } from "@app/lib/knex";
|
||||||
import { OrderByDirection } from "@app/lib/types";
|
import { OrderByDirection } from "@app/lib/types";
|
||||||
import { CmekOrderBy, TListCmeksByProjectIdDTO } from "@app/services/cmek/cmek-types";
|
import { CmekOrderBy, TListCmeksByProjectIdDTO } from "@app/services/cmek/cmek-types";
|
||||||
|
|
||||||
export type TKmsKeyDALFactory = ReturnType<typeof kmskeyDALFactory>;
|
export type TKmsKeyDALFactory = ReturnType<typeof kmskeyDALFactory>;
|
||||||
|
|
||||||
|
type TCmekFindFilter = Parameters<typeof buildFindFilter<TKmsKeys>>[0];
|
||||||
|
|
||||||
|
const baseCmekQuery = ({ filter, db, tx }: { db: TDbClient; filter?: TCmekFindFilter; tx?: Knex }) => {
|
||||||
|
const query = (tx || db.replicaNode())(TableName.KmsKey)
|
||||||
|
.where(`${TableName.KmsKey}.isReserved`, false)
|
||||||
|
.join(TableName.InternalKms, `${TableName.InternalKms}.kmsKeyId`, `${TableName.KmsKey}.id`)
|
||||||
|
.select(
|
||||||
|
selectAllTableCols(TableName.KmsKey),
|
||||||
|
db.ref("encryptionAlgorithm").withSchema(TableName.InternalKms),
|
||||||
|
db.ref("version").withSchema(TableName.InternalKms)
|
||||||
|
);
|
||||||
|
|
||||||
|
if (filter) {
|
||||||
|
/* eslint-disable @typescript-eslint/no-misused-promises */
|
||||||
|
void query.where(buildFindFilter(prependTableNameToFindFilter(TableName.KmsKey, filter)));
|
||||||
|
}
|
||||||
|
|
||||||
|
return query;
|
||||||
|
};
|
||||||
|
|
||||||
export const kmskeyDALFactory = (db: TDbClient) => {
|
export const kmskeyDALFactory = (db: TDbClient) => {
|
||||||
const kmsOrm = ormify(db, TableName.KmsKey);
|
const kmsOrm = ormify(db, TableName.KmsKey);
|
||||||
|
|
||||||
@@ -73,7 +93,7 @@ export const kmskeyDALFactory = (db: TDbClient) => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const findKmsKeysByProjectId = async (
|
const listCmeksByProjectId = async (
|
||||||
{
|
{
|
||||||
projectId,
|
projectId,
|
||||||
offset = 0,
|
offset = 0,
|
||||||
@@ -92,6 +112,7 @@ export const kmskeyDALFactory = (db: TDbClient) => {
|
|||||||
void qb.whereILike("name", `%${search}%`);
|
void qb.whereILike("name", `%${search}%`);
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
.where(`${TableName.KmsKey}.isReserved`, false)
|
||||||
.join(TableName.InternalKms, `${TableName.InternalKms}.kmsKeyId`, `${TableName.KmsKey}.id`)
|
.join(TableName.InternalKms, `${TableName.InternalKms}.kmsKeyId`, `${TableName.KmsKey}.id`)
|
||||||
.select<
|
.select<
|
||||||
(TKmsKeys &
|
(TKmsKeys &
|
||||||
@@ -118,5 +139,33 @@ export const kmskeyDALFactory = (db: TDbClient) => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
return { ...kmsOrm, findByIdWithAssociatedKms, findKmsKeysByProjectId };
|
const findCmekById = async (id: string, tx?: Knex) => {
|
||||||
|
try {
|
||||||
|
const key = await baseCmekQuery({
|
||||||
|
filter: { id },
|
||||||
|
db,
|
||||||
|
tx
|
||||||
|
}).first();
|
||||||
|
|
||||||
|
return key;
|
||||||
|
} catch (error) {
|
||||||
|
throw new DatabaseError({ error, name: "Find by ID - KMS Key" });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const findCmekByName = async (keyName: string, projectId: string, tx?: Knex) => {
|
||||||
|
try {
|
||||||
|
const key = await baseCmekQuery({
|
||||||
|
filter: { name: keyName, projectId },
|
||||||
|
db,
|
||||||
|
tx
|
||||||
|
}).first();
|
||||||
|
|
||||||
|
return key;
|
||||||
|
} catch (error) {
|
||||||
|
throw new DatabaseError({ error, name: "Find by Name - KMS Key" });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return { ...kmsOrm, findByIdWithAssociatedKms, listCmeksByProjectId, findCmekById, findCmekByName };
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import { TDbClient } from "@app/db";
|
|||||||
import { TableName } from "@app/db/schemas";
|
import { TableName } from "@app/db/schemas";
|
||||||
import { TSecretSyncs } from "@app/db/schemas/secret-syncs";
|
import { TSecretSyncs } from "@app/db/schemas/secret-syncs";
|
||||||
import { DatabaseError } from "@app/lib/errors";
|
import { DatabaseError } from "@app/lib/errors";
|
||||||
import { buildFindFilter, ormify, selectAllTableCols } from "@app/lib/knex";
|
import { buildFindFilter, ormify, prependTableNameToFindFilter, selectAllTableCols } from "@app/lib/knex";
|
||||||
import { TSecretFolderDALFactory } from "@app/services/secret-folder/secret-folder-dal";
|
import { TSecretFolderDALFactory } from "@app/services/secret-folder/secret-folder-dal";
|
||||||
|
|
||||||
export type TSecretSyncDALFactory = ReturnType<typeof secretSyncDALFactory>;
|
export type TSecretSyncDALFactory = ReturnType<typeof secretSyncDALFactory>;
|
||||||
@@ -34,17 +34,9 @@ const baseSecretSyncQuery = ({ filter, db, tx }: { db: TDbClient; filter?: Secre
|
|||||||
db.ref("updatedAt").withSchema(TableName.AppConnection).as("connectionUpdatedAt")
|
db.ref("updatedAt").withSchema(TableName.AppConnection).as("connectionUpdatedAt")
|
||||||
);
|
);
|
||||||
|
|
||||||
// prepends table name to filter keys to avoid ambiguous col references, skipping utility filters like $in, etc.
|
|
||||||
const prependTableName = (filterObj: object): SecretSyncFindFilter =>
|
|
||||||
Object.fromEntries(
|
|
||||||
Object.entries(filterObj).map(([key, value]) =>
|
|
||||||
key.startsWith("$") ? [key, prependTableName(value as object)] : [`${TableName.SecretSync}.${key}`, value]
|
|
||||||
)
|
|
||||||
);
|
|
||||||
|
|
||||||
if (filter) {
|
if (filter) {
|
||||||
/* eslint-disable @typescript-eslint/no-misused-promises */
|
/* eslint-disable @typescript-eslint/no-misused-promises */
|
||||||
void query.where(buildFindFilter(prependTableName(filter)));
|
void query.where(buildFindFilter(prependTableNameToFindFilter(TableName.SecretSync, filter)));
|
||||||
}
|
}
|
||||||
|
|
||||||
return query;
|
return query;
|
||||||
|
|||||||
@@ -0,0 +1,4 @@
|
|||||||
|
---
|
||||||
|
title: "Get Key by ID"
|
||||||
|
openapi: "Get /api/v1/kms/keys/{keyId}"
|
||||||
|
---
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
---
|
||||||
|
title: "Get Key by Name"
|
||||||
|
openapi: "Get /api/v1/kms/keys/key-name/{keyName}"
|
||||||
|
---
|
||||||
@@ -1039,6 +1039,8 @@
|
|||||||
"group": "Keys",
|
"group": "Keys",
|
||||||
"pages": [
|
"pages": [
|
||||||
"api-reference/endpoints/kms/keys/list",
|
"api-reference/endpoints/kms/keys/list",
|
||||||
|
"api-reference/endpoints/kms/keys/get-by-id",
|
||||||
|
"api-reference/endpoints/kms/keys/get-by-name",
|
||||||
"api-reference/endpoints/kms/keys/create",
|
"api-reference/endpoints/kms/keys/create",
|
||||||
"api-reference/endpoints/kms/keys/update",
|
"api-reference/endpoints/kms/keys/update",
|
||||||
"api-reference/endpoints/kms/keys/delete",
|
"api-reference/endpoints/kms/keys/delete",
|
||||||
|
|||||||
@@ -92,6 +92,7 @@ export const eventToNameMap: { [K in EventType]: string } = {
|
|||||||
[EventType.UPDATE_CMEK]: "Update KMS key",
|
[EventType.UPDATE_CMEK]: "Update KMS key",
|
||||||
[EventType.DELETE_CMEK]: "Delete KMS key",
|
[EventType.DELETE_CMEK]: "Delete KMS key",
|
||||||
[EventType.GET_CMEKS]: "List KMS keys",
|
[EventType.GET_CMEKS]: "List KMS keys",
|
||||||
|
[EventType.GET_CMEK]: "Get KMS key",
|
||||||
[EventType.CMEK_ENCRYPT]: "Encrypt with KMS key",
|
[EventType.CMEK_ENCRYPT]: "Encrypt with KMS key",
|
||||||
[EventType.CMEK_DECRYPT]: "Decrypt with KMS key",
|
[EventType.CMEK_DECRYPT]: "Decrypt with KMS key",
|
||||||
[EventType.UPDATE_EXTERNAL_GROUP_ORG_ROLE_MAPPINGS]:
|
[EventType.UPDATE_EXTERNAL_GROUP_ORG_ROLE_MAPPINGS]:
|
||||||
|
|||||||
@@ -106,6 +106,7 @@ export enum EventType {
|
|||||||
UPDATE_CMEK = "update-cmek",
|
UPDATE_CMEK = "update-cmek",
|
||||||
DELETE_CMEK = "delete-cmek",
|
DELETE_CMEK = "delete-cmek",
|
||||||
GET_CMEKS = "get-cmeks",
|
GET_CMEKS = "get-cmeks",
|
||||||
|
GET_CMEK = "get-cmek",
|
||||||
CMEK_ENCRYPT = "cmek-encrypt",
|
CMEK_ENCRYPT = "cmek-encrypt",
|
||||||
CMEK_DECRYPT = "cmek-decrypt",
|
CMEK_DECRYPT = "cmek-decrypt",
|
||||||
UPDATE_EXTERNAL_GROUP_ORG_ROLE_MAPPINGS = "update-external-group-org-role-mapping",
|
UPDATE_EXTERNAL_GROUP_ORG_ROLE_MAPPINGS = "update-external-group-org-role-mapping",
|
||||||
|
|||||||
@@ -284,7 +284,8 @@ export const CmekTable = () => {
|
|||||||
<IconButton
|
<IconButton
|
||||||
ariaLabel="copy icon"
|
ariaLabel="copy icon"
|
||||||
colorSchema="secondary"
|
colorSchema="secondary"
|
||||||
className="group/copy duration:0 invisible relative ml-3 group-hover:visible"
|
size="xs"
|
||||||
|
className="group/copy duration:0 invisible relative ml-3 rounded-md group-hover:visible"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
navigator.clipboard.writeText(id);
|
navigator.clipboard.writeText(id);
|
||||||
setCopyCipherText("Copied");
|
setCopyCipherText("Copied");
|
||||||
@@ -299,112 +300,116 @@ export const CmekTable = () => {
|
|||||||
<Badge variant={variant}>{label}</Badge>
|
<Badge variant={variant}>{label}</Badge>
|
||||||
</Td>
|
</Td>
|
||||||
<Td>{version}</Td>
|
<Td>{version}</Td>
|
||||||
<Td className="flex justify-end">
|
<Td>
|
||||||
<DropdownMenu>
|
<div className="flex justify-end">
|
||||||
<DropdownMenuTrigger asChild>
|
<DropdownMenu>
|
||||||
<IconButton
|
<DropdownMenuTrigger asChild>
|
||||||
variant="plain"
|
<IconButton
|
||||||
colorSchema="primary"
|
variant="plain"
|
||||||
className="ml-4 p-0 data-[state=open]:text-primary-400"
|
colorSchema="primary"
|
||||||
ariaLabel="More options"
|
className="ml-4 p-0 data-[state=open]:text-primary-400"
|
||||||
>
|
ariaLabel="More options"
|
||||||
<FontAwesomeIcon size="lg" icon={faEllipsis} />
|
>
|
||||||
</IconButton>
|
<FontAwesomeIcon size="lg" icon={faEllipsis} />
|
||||||
</DropdownMenuTrigger>
|
</IconButton>
|
||||||
<DropdownMenuContent className="min-w-[160px]">
|
</DropdownMenuTrigger>
|
||||||
<Tooltip
|
<DropdownMenuContent className="min-w-[160px]">
|
||||||
content={
|
<Tooltip
|
||||||
// eslint-disable-next-line no-nested-ternary
|
content={
|
||||||
cannotEncryptData
|
// eslint-disable-next-line no-nested-ternary
|
||||||
? "Access Restricted"
|
cannotEncryptData
|
||||||
: isDisabled
|
? "Access Restricted"
|
||||||
? "Key Disabled"
|
: isDisabled
|
||||||
: ""
|
? "Key Disabled"
|
||||||
}
|
: ""
|
||||||
position="left"
|
}
|
||||||
>
|
position="left"
|
||||||
<div>
|
>
|
||||||
<DropdownMenuItem
|
<div>
|
||||||
onClick={() => handlePopUpOpen("encryptData", cmek)}
|
<DropdownMenuItem
|
||||||
icon={<FontAwesomeIcon icon={faLock} />}
|
onClick={() => handlePopUpOpen("encryptData", cmek)}
|
||||||
iconPos="left"
|
icon={<FontAwesomeIcon icon={faLock} />}
|
||||||
isDisabled={cannotEncryptData || isDisabled}
|
iconPos="left"
|
||||||
>
|
isDisabled={cannotEncryptData || isDisabled}
|
||||||
Encrypt Data
|
>
|
||||||
</DropdownMenuItem>
|
Encrypt Data
|
||||||
</div>
|
</DropdownMenuItem>
|
||||||
</Tooltip>
|
</div>
|
||||||
<Tooltip
|
</Tooltip>
|
||||||
content={
|
<Tooltip
|
||||||
// eslint-disable-next-line no-nested-ternary
|
content={
|
||||||
cannotDecryptData
|
// eslint-disable-next-line no-nested-ternary
|
||||||
? "Access Restricted"
|
cannotDecryptData
|
||||||
: isDisabled
|
? "Access Restricted"
|
||||||
? "Key Disabled"
|
: isDisabled
|
||||||
: ""
|
? "Key Disabled"
|
||||||
}
|
: ""
|
||||||
position="left"
|
}
|
||||||
>
|
position="left"
|
||||||
<div>
|
>
|
||||||
<DropdownMenuItem
|
<div>
|
||||||
onClick={() => handlePopUpOpen("decryptData", cmek)}
|
<DropdownMenuItem
|
||||||
icon={<FontAwesomeIcon icon={faLockOpen} />}
|
onClick={() => handlePopUpOpen("decryptData", cmek)}
|
||||||
iconPos="left"
|
icon={<FontAwesomeIcon icon={faLockOpen} />}
|
||||||
isDisabled={cannotDecryptData || isDisabled}
|
iconPos="left"
|
||||||
>
|
isDisabled={cannotDecryptData || isDisabled}
|
||||||
Decrypt Data
|
>
|
||||||
</DropdownMenuItem>
|
Decrypt Data
|
||||||
</div>
|
</DropdownMenuItem>
|
||||||
</Tooltip>
|
</div>
|
||||||
<Tooltip
|
</Tooltip>
|
||||||
content={cannotEditKey ? "Access Restricted" : ""}
|
<Tooltip
|
||||||
position="left"
|
content={cannotEditKey ? "Access Restricted" : ""}
|
||||||
>
|
position="left"
|
||||||
<div>
|
>
|
||||||
<DropdownMenuItem
|
<div>
|
||||||
onClick={() => handlePopUpOpen("upsertKey", cmek)}
|
<DropdownMenuItem
|
||||||
icon={<FontAwesomeIcon icon={faEdit} />}
|
onClick={() => handlePopUpOpen("upsertKey", cmek)}
|
||||||
iconPos="left"
|
icon={<FontAwesomeIcon icon={faEdit} />}
|
||||||
isDisabled={cannotEditKey}
|
iconPos="left"
|
||||||
>
|
isDisabled={cannotEditKey}
|
||||||
Edit Key
|
>
|
||||||
</DropdownMenuItem>
|
Edit Key
|
||||||
</div>
|
</DropdownMenuItem>
|
||||||
</Tooltip>
|
</div>
|
||||||
<Tooltip
|
</Tooltip>
|
||||||
content={cannotEditKey ? "Access Restricted" : ""}
|
<Tooltip
|
||||||
position="left"
|
content={cannotEditKey ? "Access Restricted" : ""}
|
||||||
>
|
position="left"
|
||||||
<div>
|
>
|
||||||
<DropdownMenuItem
|
<div>
|
||||||
onClick={() => handleDisableCmek(cmek)}
|
<DropdownMenuItem
|
||||||
icon={
|
onClick={() => handleDisableCmek(cmek)}
|
||||||
<FontAwesomeIcon icon={isDisabled ? faCheckCircle : faCancel} />
|
icon={
|
||||||
}
|
<FontAwesomeIcon
|
||||||
iconPos="left"
|
icon={isDisabled ? faCheckCircle : faCancel}
|
||||||
isDisabled={cannotEditKey}
|
/>
|
||||||
>
|
}
|
||||||
{isDisabled ? "Enable" : "Disable"} Key
|
iconPos="left"
|
||||||
</DropdownMenuItem>
|
isDisabled={cannotEditKey}
|
||||||
</div>
|
>
|
||||||
</Tooltip>
|
{isDisabled ? "Enable" : "Disable"} Key
|
||||||
<Tooltip
|
</DropdownMenuItem>
|
||||||
content={cannotDeleteKey ? "Access Restricted" : ""}
|
</div>
|
||||||
position="left"
|
</Tooltip>
|
||||||
>
|
<Tooltip
|
||||||
<div>
|
content={cannotDeleteKey ? "Access Restricted" : ""}
|
||||||
<DropdownMenuItem
|
position="left"
|
||||||
onClick={() => handlePopUpOpen("deleteKey", cmek)}
|
>
|
||||||
icon={<FontAwesomeIcon icon={faTrash} />}
|
<div>
|
||||||
iconPos="left"
|
<DropdownMenuItem
|
||||||
isDisabled={cannotDeleteKey}
|
onClick={() => handlePopUpOpen("deleteKey", cmek)}
|
||||||
>
|
icon={<FontAwesomeIcon icon={faTrash} />}
|
||||||
Delete Key
|
iconPos="left"
|
||||||
</DropdownMenuItem>
|
isDisabled={cannotDeleteKey}
|
||||||
</div>
|
>
|
||||||
</Tooltip>
|
Delete Key
|
||||||
</DropdownMenuContent>
|
</DropdownMenuItem>
|
||||||
</DropdownMenu>
|
</div>
|
||||||
|
</Tooltip>
|
||||||
|
</DropdownMenuContent>
|
||||||
|
</DropdownMenu>
|
||||||
|
</div>
|
||||||
</Td>
|
</Td>
|
||||||
</Tr>
|
</Tr>
|
||||||
);
|
);
|
||||||
|
|||||||
Reference in New Issue
Block a user