Merge pull request #2497 from scott-ray-wilson/kms-feature

Feature: KMS MVP
This commit is contained in:
Scott Wilson
2024-10-03 15:15:08 -07:00
committed by GitHub
87 changed files with 2685 additions and 230 deletions
+5
View File
@@ -73,6 +73,11 @@ We're on a mission to make security tooling more accessible to everyone, not jus
- **[Infisical PKI Issuer for Kubernetes](https://infisical.com/docs/documentation/platform/pki/pki-issuer)**: Deliver TLS certificates to your Kubernetes workloads with automatic renewal. - **[Infisical PKI Issuer for Kubernetes](https://infisical.com/docs/documentation/platform/pki/pki-issuer)**: Deliver TLS certificates to your Kubernetes workloads with automatic renewal.
- **[Enrollment over Secure Transport](https://infisical.com/docs/documentation/platform/pki/est)**: Enroll and manage certificates via EST protocol. - **[Enrollment over Secure Transport](https://infisical.com/docs/documentation/platform/pki/est)**: Enroll and manage certificates via EST protocol.
### Key Management (KMS):
- **[Cryptograhic Keys](https://infisical.com/docs/documentation/platform/kms)**: Centrally manage keys across projects through a user-friendly interface or via the API.
- **[Encrypt and Decrypt Data](https://infisical.com/docs/documentation/platform/kms#guide-to-encrypting-data)**: Use symmetric keys to encrypt and decrypt data.
### General Platform: ### General Platform:
- **Authentication Methods**: Authenticate machine identities with Infisical using a cloud-native or platform agnostic authentication method ([Kubernetes Auth](https://infisical.com/docs/documentation/platform/identities/kubernetes-auth), [GCP Auth](https://infisical.com/docs/documentation/platform/identities/gcp-auth), [Azure Auth](https://infisical.com/docs/documentation/platform/identities/azure-auth), [AWS Auth](https://infisical.com/docs/documentation/platform/identities/aws-auth), [OIDC Auth](https://infisical.com/docs/documentation/platform/identities/oidc-auth/general), [Universal Auth](https://infisical.com/docs/documentation/platform/identities/universal-auth)). - **Authentication Methods**: Authenticate machine identities with Infisical using a cloud-native or platform agnostic authentication method ([Kubernetes Auth](https://infisical.com/docs/documentation/platform/identities/kubernetes-auth), [GCP Auth](https://infisical.com/docs/documentation/platform/identities/gcp-auth), [Azure Auth](https://infisical.com/docs/documentation/platform/identities/azure-auth), [AWS Auth](https://infisical.com/docs/documentation/platform/identities/aws-auth), [OIDC Auth](https://infisical.com/docs/documentation/platform/identities/oidc-auth/general), [Universal Auth](https://infisical.com/docs/documentation/platform/identities/universal-auth)).
- **[Access Controls](https://infisical.com/docs/documentation/platform/access-controls/overview)**: Define advanced authorization controls for users and machine identities with [RBAC](https://infisical.com/docs/documentation/platform/access-controls/role-based-access-controls), [additional privileges](https://infisical.com/docs/documentation/platform/access-controls/additional-privileges), [temporary access](https://infisical.com/docs/documentation/platform/access-controls/temporary-access), [access requests](https://infisical.com/docs/documentation/platform/access-controls/access-requests), [approval workflows](https://infisical.com/docs/documentation/platform/pr-workflows), and more. - **[Access Controls](https://infisical.com/docs/documentation/platform/access-controls/overview)**: Define advanced authorization controls for users and machine identities with [RBAC](https://infisical.com/docs/documentation/platform/access-controls/role-based-access-controls), [additional privileges](https://infisical.com/docs/documentation/platform/access-controls/additional-privileges), [temporary access](https://infisical.com/docs/documentation/platform/access-controls/temporary-access), [access requests](https://infisical.com/docs/documentation/platform/access-controls/access-requests), [approval workflows](https://infisical.com/docs/documentation/platform/pr-workflows), and more.
+2
View File
@@ -38,6 +38,7 @@ import { TAuthTokenServiceFactory } from "@app/services/auth-token/auth-token-se
import { TCertificateServiceFactory } from "@app/services/certificate/certificate-service"; import { TCertificateServiceFactory } from "@app/services/certificate/certificate-service";
import { TCertificateAuthorityServiceFactory } from "@app/services/certificate-authority/certificate-authority-service"; import { TCertificateAuthorityServiceFactory } from "@app/services/certificate-authority/certificate-authority-service";
import { TCertificateTemplateServiceFactory } from "@app/services/certificate-template/certificate-template-service"; import { TCertificateTemplateServiceFactory } from "@app/services/certificate-template/certificate-template-service";
import { TCmekServiceFactory } from "@app/services/cmek/cmek-service";
import { TExternalMigrationServiceFactory } from "@app/services/external-migration/external-migration-service"; import { TExternalMigrationServiceFactory } from "@app/services/external-migration/external-migration-service";
import { TGroupProjectServiceFactory } from "@app/services/group-project/group-project-service"; import { TGroupProjectServiceFactory } from "@app/services/group-project/group-project-service";
import { TIdentityServiceFactory } from "@app/services/identity/identity-service"; import { TIdentityServiceFactory } from "@app/services/identity/identity-service";
@@ -182,6 +183,7 @@ declare module "fastify" {
orgAdmin: TOrgAdminServiceFactory; orgAdmin: TOrgAdminServiceFactory;
slack: TSlackServiceFactory; slack: TSlackServiceFactory;
workflowIntegration: TWorkflowIntegrationServiceFactory; workflowIntegration: TWorkflowIntegrationServiceFactory;
cmek: TCmekServiceFactory;
migration: TExternalMigrationServiceFactory; migration: TExternalMigrationServiceFactory;
}; };
// this is exclusive use for middlewares in which we need to inject data // this is exclusive use for middlewares in which we need to inject data
@@ -0,0 +1,46 @@
import { Knex } from "knex";
import { dropConstraintIfExists } from "@app/db/migrations/utils/dropConstraintIfExists";
import { TableName } from "@app/db/schemas";
export async function up(knex: Knex): Promise<void> {
if (await knex.schema.hasTable(TableName.KmsKey)) {
const hasOrgId = await knex.schema.hasColumn(TableName.KmsKey, "orgId");
const hasSlug = await knex.schema.hasColumn(TableName.KmsKey, "slug");
// drop constraint if exists (won't exist if rolled back, see below)
await dropConstraintIfExists(TableName.KmsKey, "kms_keys_orgid_slug_unique", knex);
// projectId for CMEK functionality
await knex.schema.alterTable(TableName.KmsKey, (table) => {
table.string("projectId").nullable().references("id").inTable(TableName.Project).onDelete("CASCADE");
if (hasOrgId) {
table.unique(["orgId", "projectId", "slug"]);
}
if (hasSlug) {
table.renameColumn("slug", "name");
}
});
}
}
export async function down(knex: Knex): Promise<void> {
if (await knex.schema.hasTable(TableName.KmsKey)) {
const hasOrgId = await knex.schema.hasColumn(TableName.KmsKey, "orgId");
const hasName = await knex.schema.hasColumn(TableName.KmsKey, "name");
// remove projectId for CMEK functionality
await knex.schema.alterTable(TableName.KmsKey, (table) => {
if (hasName) {
table.renameColumn("name", "slug");
}
if (hasOrgId) {
table.dropUnique(["orgId", "projectId", "slug"]);
}
table.dropColumn("projectId");
});
}
}
@@ -0,0 +1,6 @@
import { Knex } from "knex";
import { TableName } from "@app/db/schemas";
export const dropConstraintIfExists = (tableName: TableName, constraintName: string, knex: Knex) =>
knex.raw(`ALTER TABLE ${tableName} DROP CONSTRAINT IF EXISTS ${constraintName};`);
+1 -1
View File
@@ -54,7 +54,7 @@ export const getSecretManagerDataKey = async (knex: Knex, projectId: string) =>
} else { } else {
const [kmsDoc] = await knex(TableName.KmsKey) const [kmsDoc] = await knex(TableName.KmsKey)
.insert({ .insert({
slug: slugify(alphaNumericNanoId(8).toLowerCase()), name: slugify(alphaNumericNanoId(8).toLowerCase()),
orgId: project.orgId, orgId: project.orgId,
isReserved: false isReserved: false
}) })
+3 -2
View File
@@ -13,9 +13,10 @@ export const KmsKeysSchema = z.object({
isDisabled: z.boolean().default(false).nullable().optional(), isDisabled: z.boolean().default(false).nullable().optional(),
isReserved: z.boolean().default(true).nullable().optional(), isReserved: z.boolean().default(true).nullable().optional(),
orgId: z.string().uuid(), orgId: z.string().uuid(),
slug: z.string(), name: z.string(),
createdAt: z.date(), createdAt: z.date(),
updatedAt: z.date() updatedAt: z.date(),
projectId: z.string().nullable().optional()
}); });
export type TKmsKeys = z.infer<typeof KmsKeysSchema>; export type TKmsKeys = z.infer<typeof KmsKeysSchema>;
+13 -13
View File
@@ -26,7 +26,7 @@ const sanitizedExternalSchemaForGetAll = KmsKeysSchema.pick({
isDisabled: true, isDisabled: true,
createdAt: true, createdAt: true,
updatedAt: true, updatedAt: true,
slug: true name: true
}) })
.extend({ .extend({
externalKms: ExternalKmsSchema.pick({ externalKms: ExternalKmsSchema.pick({
@@ -57,7 +57,7 @@ export const registerExternalKmsRouter = async (server: FastifyZodProvider) => {
}, },
schema: { schema: {
body: z.object({ body: z.object({
slug: z.string().min(1).trim().toLowerCase(), name: z.string().min(1).trim().toLowerCase(),
description: z.string().trim().optional(), description: z.string().trim().optional(),
provider: ExternalKmsInputSchema provider: ExternalKmsInputSchema
}), }),
@@ -74,7 +74,7 @@ export const registerExternalKmsRouter = async (server: FastifyZodProvider) => {
actorId: req.permission.id, actorId: req.permission.id,
actorAuthMethod: req.permission.authMethod, actorAuthMethod: req.permission.authMethod,
actorOrgId: req.permission.orgId, actorOrgId: req.permission.orgId,
slug: req.body.slug, name: req.body.name,
provider: req.body.provider, provider: req.body.provider,
description: req.body.description description: req.body.description
}); });
@@ -87,7 +87,7 @@ export const registerExternalKmsRouter = async (server: FastifyZodProvider) => {
metadata: { metadata: {
kmsId: externalKms.id, kmsId: externalKms.id,
provider: req.body.provider.type, provider: req.body.provider.type,
slug: req.body.slug, name: req.body.name,
description: req.body.description description: req.body.description
} }
} }
@@ -108,7 +108,7 @@ export const registerExternalKmsRouter = async (server: FastifyZodProvider) => {
id: z.string().trim().min(1) id: z.string().trim().min(1)
}), }),
body: z.object({ body: z.object({
slug: 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(),
provider: ExternalKmsInputUpdateSchema provider: ExternalKmsInputUpdateSchema
}), }),
@@ -125,7 +125,7 @@ export const registerExternalKmsRouter = async (server: FastifyZodProvider) => {
actorId: req.permission.id, actorId: req.permission.id,
actorAuthMethod: req.permission.authMethod, actorAuthMethod: req.permission.authMethod,
actorOrgId: req.permission.orgId, actorOrgId: req.permission.orgId,
slug: req.body.slug, name: req.body.name,
provider: req.body.provider, provider: req.body.provider,
description: req.body.description, description: req.body.description,
id: req.params.id id: req.params.id
@@ -139,7 +139,7 @@ export const registerExternalKmsRouter = async (server: FastifyZodProvider) => {
metadata: { metadata: {
kmsId: externalKms.id, kmsId: externalKms.id,
provider: req.body.provider.type, provider: req.body.provider.type,
slug: req.body.slug, name: req.body.name,
description: req.body.description description: req.body.description
} }
} }
@@ -182,7 +182,7 @@ export const registerExternalKmsRouter = async (server: FastifyZodProvider) => {
type: EventType.DELETE_KMS, type: EventType.DELETE_KMS,
metadata: { metadata: {
kmsId: externalKms.id, kmsId: externalKms.id,
slug: externalKms.slug name: externalKms.name
} }
} }
}); });
@@ -224,7 +224,7 @@ export const registerExternalKmsRouter = async (server: FastifyZodProvider) => {
type: EventType.GET_KMS, type: EventType.GET_KMS,
metadata: { metadata: {
kmsId: externalKms.id, kmsId: externalKms.id,
slug: externalKms.slug name: externalKms.name
} }
} }
}); });
@@ -260,13 +260,13 @@ export const registerExternalKmsRouter = async (server: FastifyZodProvider) => {
server.route({ server.route({
method: "GET", method: "GET",
url: "/slug/:slug", url: "/name/:name",
config: { config: {
rateLimit: readLimit rateLimit: readLimit
}, },
schema: { schema: {
params: z.object({ params: z.object({
slug: z.string().trim().min(1) name: z.string().trim().min(1)
}), }),
response: { response: {
200: z.object({ 200: z.object({
@@ -276,12 +276,12 @@ export const registerExternalKmsRouter = async (server: FastifyZodProvider) => {
}, },
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
handler: async (req) => { handler: async (req) => {
const externalKms = await server.services.externalKms.findBySlug({ const externalKms = await server.services.externalKms.findByName({
actor: req.permission.type, actor: req.permission.type,
actorId: req.permission.id, actorId: req.permission.id,
actorAuthMethod: req.permission.authMethod, actorAuthMethod: req.permission.authMethod,
actorOrgId: req.permission.orgId, actorOrgId: req.permission.orgId,
slug: req.params.slug name: req.params.name
}); });
return { externalKms }; return { externalKms };
} }
+4 -4
View File
@@ -203,7 +203,7 @@ export const registerProjectRouter = async (server: FastifyZodProvider) => {
200: z.object({ 200: z.object({
secretManagerKmsKey: z.object({ secretManagerKmsKey: z.object({
id: z.string(), id: z.string(),
slug: z.string(), name: z.string(),
isExternal: z.boolean() isExternal: z.boolean()
}) })
}) })
@@ -243,7 +243,7 @@ export const registerProjectRouter = async (server: FastifyZodProvider) => {
200: z.object({ 200: z.object({
secretManagerKmsKey: z.object({ secretManagerKmsKey: z.object({
id: z.string(), id: z.string(),
slug: z.string(), name: z.string(),
isExternal: z.boolean() isExternal: z.boolean()
}) })
}) })
@@ -268,7 +268,7 @@ export const registerProjectRouter = async (server: FastifyZodProvider) => {
metadata: { metadata: {
secretManagerKmsKey: { secretManagerKmsKey: {
id: secretManagerKmsKey.id, id: secretManagerKmsKey.id,
slug: secretManagerKmsKey.slug name: secretManagerKmsKey.name
} }
} }
} }
@@ -336,7 +336,7 @@ export const registerProjectRouter = async (server: FastifyZodProvider) => {
200: z.object({ 200: z.object({
secretManagerKmsKey: z.object({ secretManagerKmsKey: z.object({
id: z.string(), id: z.string(),
slug: z.string(), name: z.string(),
isExternal: z.boolean() isExternal: z.boolean()
}) })
}) })
@@ -1,3 +1,4 @@
import { SymmetricEncryption } from "@app/lib/crypto/cipher";
import { TProjectPermission } from "@app/lib/types"; import { TProjectPermission } from "@app/lib/types";
import { ActorType } from "@app/services/auth/auth-type"; import { ActorType } from "@app/services/auth/auth-type";
import { CaStatus } from "@app/services/certificate-authority/certificate-authority-types"; import { CaStatus } from "@app/services/certificate-authority/certificate-authority-types";
@@ -182,7 +183,13 @@ export enum EventType {
DELETE_SLACK_INTEGRATION = "delete-slack-integration", DELETE_SLACK_INTEGRATION = "delete-slack-integration",
GET_PROJECT_SLACK_CONFIG = "get-project-slack-config", GET_PROJECT_SLACK_CONFIG = "get-project-slack-config",
UPDATE_PROJECT_SLACK_CONFIG = "update-project-slack-config", UPDATE_PROJECT_SLACK_CONFIG = "update-project-slack-config",
INTEGRATION_SYNCED = "integration-synced" INTEGRATION_SYNCED = "integration-synced",
CREATE_CMEK = "create-cmek",
UPDATE_CMEK = "update-cmek",
DELETE_CMEK = "delete-cmek",
GET_CMEKS = "get-cmeks",
CMEK_ENCRYPT = "cmek-encrypt",
CMEK_DECRYPT = "cmek-decrypt"
} }
interface UserActorMetadata { interface UserActorMetadata {
@@ -1350,7 +1357,7 @@ interface CreateKmsEvent {
metadata: { metadata: {
kmsId: string; kmsId: string;
provider: string; provider: string;
slug: string; name: string;
description?: string; description?: string;
}; };
} }
@@ -1359,7 +1366,7 @@ interface DeleteKmsEvent {
type: EventType.DELETE_KMS; type: EventType.DELETE_KMS;
metadata: { metadata: {
kmsId: string; kmsId: string;
slug: string; name: string;
}; };
} }
@@ -1368,7 +1375,7 @@ interface UpdateKmsEvent {
metadata: { metadata: {
kmsId: string; kmsId: string;
provider: string; provider: string;
slug?: string; name?: string;
description?: string; description?: string;
}; };
} }
@@ -1377,7 +1384,7 @@ interface GetKmsEvent {
type: EventType.GET_KMS; type: EventType.GET_KMS;
metadata: { metadata: {
kmsId: string; kmsId: string;
slug: string; name: string;
}; };
} }
@@ -1386,7 +1393,7 @@ interface UpdateProjectKmsEvent {
metadata: { metadata: {
secretManagerKmsKey: { secretManagerKmsKey: {
id: string; id: string;
slug: string; name: string;
}; };
}; };
} }
@@ -1541,6 +1548,53 @@ interface IntegrationSyncedEvent {
}; };
} }
interface CreateCmekEvent {
type: EventType.CREATE_CMEK;
metadata: {
keyId: string;
name: string;
description?: string;
encryptionAlgorithm: SymmetricEncryption;
};
}
interface DeleteCmekEvent {
type: EventType.DELETE_CMEK;
metadata: {
keyId: string;
};
}
interface UpdateCmekEvent {
type: EventType.UPDATE_CMEK;
metadata: {
keyId: string;
name?: string;
description?: string;
};
}
interface GetCmeksEvent {
type: EventType.GET_CMEKS;
metadata: {
keyIds: string[];
};
}
interface CmekEncryptEvent {
type: EventType.CMEK_ENCRYPT;
metadata: {
keyId: string;
};
}
interface CmekDecryptEvent {
type: EventType.CMEK_DECRYPT;
metadata: {
keyId: string;
};
}
export type Event = export type Event =
| GetSecretsEvent | GetSecretsEvent
| GetSecretEvent | GetSecretEvent
@@ -1680,4 +1734,10 @@ export type Event =
| GetSlackIntegration | GetSlackIntegration
| UpdateProjectSlackConfig | UpdateProjectSlackConfig
| GetProjectSlackConfig | GetProjectSlackConfig
| IntegrationSyncedEvent; | IntegrationSyncedEvent
| CreateCmekEvent
| UpdateCmekEvent
| DeleteCmekEvent
| GetCmeksEvent
| CmekEncryptEvent
| CmekDecryptEvent;
@@ -30,7 +30,7 @@ export const externalKmsDALFactory = (db: TDbClient) => {
isDisabled: el.isDisabled, isDisabled: el.isDisabled,
isReserved: el.isReserved, isReserved: el.isReserved,
orgId: el.orgId, orgId: el.orgId,
slug: el.slug, name: el.name,
createdAt: el.createdAt, createdAt: el.createdAt,
updatedAt: el.updatedAt, updatedAt: el.updatedAt,
externalKms: { externalKms: {
@@ -43,7 +43,7 @@ export const externalKmsServiceFactory = ({
provider, provider,
description, description,
actor, actor,
slug, name,
actorId, actorId,
actorOrgId, actorOrgId,
actorAuthMethod actorAuthMethod
@@ -64,7 +64,7 @@ export const externalKmsServiceFactory = ({
}); });
} }
const kmsSlug = slug ? slugify(slug) : slugify(alphaNumericNanoId(8).toLowerCase()); const kmsName = name ? slugify(name) : slugify(alphaNumericNanoId(8).toLowerCase());
let sanitizedProviderInput = ""; let sanitizedProviderInput = "";
switch (provider.type) { switch (provider.type) {
@@ -96,7 +96,7 @@ export const externalKmsServiceFactory = ({
{ {
isReserved: false, isReserved: false,
description, description,
slug: kmsSlug, name: kmsName,
orgId: actorOrgId orgId: actorOrgId
}, },
tx tx
@@ -120,7 +120,7 @@ export const externalKmsServiceFactory = ({
description, description,
actor, actor,
id: kmsId, id: kmsId,
slug, name,
actorId, actorId,
actorOrgId, actorOrgId,
actorAuthMethod actorAuthMethod
@@ -142,7 +142,7 @@ export const externalKmsServiceFactory = ({
}); });
} }
const kmsSlug = slug ? slugify(slug) : undefined; const kmsName = name ? slugify(name) : undefined;
const externalKmsDoc = await externalKmsDAL.findOne({ kmsKeyId: kmsDoc.id }); const externalKmsDoc = await externalKmsDAL.findOne({ kmsKeyId: kmsDoc.id });
if (!externalKmsDoc) throw new NotFoundError({ message: "External kms not found" }); if (!externalKmsDoc) throw new NotFoundError({ message: "External kms not found" });
@@ -188,7 +188,7 @@ export const externalKmsServiceFactory = ({
kmsDoc.id, kmsDoc.id,
{ {
description, description,
slug: kmsSlug name: kmsName
}, },
tx tx
); );
@@ -280,14 +280,14 @@ export const externalKmsServiceFactory = ({
} }
}; };
const findBySlug = async ({ const findByName = async ({
actor, actor,
actorId, actorId,
actorOrgId, actorOrgId,
actorAuthMethod, actorAuthMethod,
slug: kmsSlug name: kmsName
}: TGetExternalKmsBySlugDTO) => { }: TGetExternalKmsBySlugDTO) => {
const kmsDoc = await kmsDAL.findOne({ slug: kmsSlug, orgId: actorOrgId }); const kmsDoc = await kmsDAL.findOne({ name: kmsName, orgId: actorOrgId });
const { permission } = await permissionService.getOrgPermission( const { permission } = await permissionService.getOrgPermission(
actor, actor,
actorId, actorId,
@@ -327,6 +327,6 @@ export const externalKmsServiceFactory = ({
deleteById, deleteById,
list, list,
findById, findById,
findBySlug findByName
}; };
}; };
@@ -3,14 +3,14 @@ import { TOrgPermission } from "@app/lib/types";
import { TExternalKmsInputSchema, TExternalKmsInputUpdateSchema } from "./providers/model"; import { TExternalKmsInputSchema, TExternalKmsInputUpdateSchema } from "./providers/model";
export type TCreateExternalKmsDTO = { export type TCreateExternalKmsDTO = {
slug?: string; name?: string;
description?: string; description?: string;
provider: TExternalKmsInputSchema; provider: TExternalKmsInputSchema;
} & Omit<TOrgPermission, "orgId">; } & Omit<TOrgPermission, "orgId">;
export type TUpdateExternalKmsDTO = { export type TUpdateExternalKmsDTO = {
id: string; id: string;
slug?: string; name?: string;
description?: string; description?: string;
provider?: TExternalKmsInputUpdateSchema; provider?: TExternalKmsInputUpdateSchema;
} & Omit<TOrgPermission, "orgId">; } & Omit<TOrgPermission, "orgId">;
@@ -26,5 +26,5 @@ export type TGetExternalKmsByIdDTO = {
} & Omit<TOrgPermission, "orgId">; } & Omit<TOrgPermission, "orgId">;
export type TGetExternalKmsBySlugDTO = { export type TGetExternalKmsBySlugDTO = {
slug: string; name: string;
} & Omit<TOrgPermission, "orgId">; } & Omit<TOrgPermission, "orgId">;
@@ -14,6 +14,15 @@ export enum ProjectPermissionActions {
Delete = "delete" Delete = "delete"
} }
export enum ProjectPermissionCmekActions {
Read = "read",
Create = "create",
Edit = "edit",
Delete = "delete",
Encrypt = "encrypt",
Decrypt = "decrypt"
}
export enum ProjectPermissionSub { export enum ProjectPermissionSub {
Role = "role", Role = "role",
Member = "member", Member = "member",
@@ -38,7 +47,8 @@ export enum ProjectPermissionSub {
CertificateTemplates = "certificate-templates", CertificateTemplates = "certificate-templates",
PkiAlerts = "pki-alerts", PkiAlerts = "pki-alerts",
PkiCollections = "pki-collections", PkiCollections = "pki-collections",
Kms = "kms" Kms = "kms",
Cmek = "cmek"
} }
export type SecretSubjectFields = { export type SecretSubjectFields = {
@@ -95,6 +105,7 @@ export type ProjectPermissionSet =
| [ProjectPermissionActions, ProjectPermissionSub.CertificateTemplates] | [ProjectPermissionActions, ProjectPermissionSub.CertificateTemplates]
| [ProjectPermissionActions, ProjectPermissionSub.PkiAlerts] | [ProjectPermissionActions, ProjectPermissionSub.PkiAlerts]
| [ProjectPermissionActions, ProjectPermissionSub.PkiCollections] | [ProjectPermissionActions, ProjectPermissionSub.PkiCollections]
| [ProjectPermissionCmekActions, ProjectPermissionSub.Cmek]
| [ProjectPermissionActions.Delete, ProjectPermissionSub.Project] | [ProjectPermissionActions.Delete, ProjectPermissionSub.Project]
| [ProjectPermissionActions.Edit, ProjectPermissionSub.Project] | [ProjectPermissionActions.Edit, ProjectPermissionSub.Project]
| [ProjectPermissionActions.Read, ProjectPermissionSub.SecretRollback] | [ProjectPermissionActions.Read, ProjectPermissionSub.SecretRollback]
@@ -282,6 +293,12 @@ export const ProjectPermissionSchema = z.discriminatedUnion("subject", [
action: CASL_ACTION_SCHEMA_ENUM([ProjectPermissionActions.Read]).describe( action: CASL_ACTION_SCHEMA_ENUM([ProjectPermissionActions.Read]).describe(
"Describe what action an entity can take." "Describe what action an entity can take."
) )
}),
z.object({
subject: z.literal(ProjectPermissionSub.Cmek).describe("The entity this permission pertains to."),
action: CASL_ACTION_SCHEMA_NATIVE_ENUM(ProjectPermissionCmekActions).describe(
"Describe what action an entity can take."
)
}) })
]); ]);
@@ -325,6 +342,17 @@ const buildAdminPermissionRules = () => {
can([ProjectPermissionActions.Edit, ProjectPermissionActions.Delete], ProjectPermissionSub.Project); can([ProjectPermissionActions.Edit, ProjectPermissionActions.Delete], ProjectPermissionSub.Project);
can([ProjectPermissionActions.Read, ProjectPermissionActions.Create], ProjectPermissionSub.SecretRollback); can([ProjectPermissionActions.Read, ProjectPermissionActions.Create], ProjectPermissionSub.SecretRollback);
can([ProjectPermissionActions.Edit], ProjectPermissionSub.Kms); can([ProjectPermissionActions.Edit], ProjectPermissionSub.Kms);
can(
[
ProjectPermissionCmekActions.Create,
ProjectPermissionCmekActions.Edit,
ProjectPermissionCmekActions.Delete,
ProjectPermissionCmekActions.Read,
ProjectPermissionCmekActions.Encrypt,
ProjectPermissionCmekActions.Decrypt
],
ProjectPermissionSub.Cmek
);
return rules; return rules;
}; };
@@ -444,6 +472,18 @@ const buildMemberPermissionRules = () => {
can([ProjectPermissionActions.Read], ProjectPermissionSub.PkiAlerts); can([ProjectPermissionActions.Read], ProjectPermissionSub.PkiAlerts);
can([ProjectPermissionActions.Read], ProjectPermissionSub.PkiCollections); can([ProjectPermissionActions.Read], ProjectPermissionSub.PkiCollections);
can(
[
ProjectPermissionCmekActions.Create,
ProjectPermissionCmekActions.Edit,
ProjectPermissionCmekActions.Delete,
ProjectPermissionCmekActions.Read,
ProjectPermissionCmekActions.Encrypt,
ProjectPermissionCmekActions.Decrypt
],
ProjectPermissionSub.Cmek
);
return rules; return rules;
}; };
@@ -470,6 +510,7 @@ const buildViewerPermissionRules = () => {
can(ProjectPermissionActions.Read, ProjectPermissionSub.IpAllowList); can(ProjectPermissionActions.Read, ProjectPermissionSub.IpAllowList);
can(ProjectPermissionActions.Read, ProjectPermissionSub.CertificateAuthorities); can(ProjectPermissionActions.Read, ProjectPermissionSub.CertificateAuthorities);
can(ProjectPermissionActions.Read, ProjectPermissionSub.Certificates); can(ProjectPermissionActions.Read, ProjectPermissionSub.Certificates);
can(ProjectPermissionCmekActions.Read, ProjectPermissionSub.Cmek);
return rules; return rules;
}; };
+34
View File
@@ -1347,3 +1347,37 @@ export const PROJECT_ROLE = {
projectSlug: "The slug of the project to list the roles of." projectSlug: "The slug of the project to list the roles of."
} }
}; };
export const KMS = {
CREATE_KEY: {
projectId: "The ID of the project to create the key in.",
name: "The name of the key to be created. Must be slug-friendly.",
description: "An optional description of the key.",
encryptionAlgorithm: "The algorithm to use when performing cryptographic operations with the key."
},
UPDATE_KEY: {
keyId: "The ID of the key to be updated.",
name: "The updated name of this key. Must be slug-friendly.",
description: "The updated description of this key.",
isDisabled: "The flag to enable or disable this key."
},
DELETE_KEY: {
keyId: "The ID of the key to be deleted."
},
LIST_KEYS: {
projectId: "The ID of the project to list keys from.",
offset: "The offset to start from. If you enter 10, it will start from the 10th key.",
limit: "The number of keys to return.",
orderBy: "The column to order keys by.",
orderDirection: "The direction to order keys in.",
search: "The text string to filter key names by."
},
ENCRYPT: {
keyId: "The ID of the key to encrypt the data with.",
plaintext: "The plaintext to be encrypted (base64 encoded)."
},
DECRYPT: {
keyId: "The ID of the key to decrypt the data with.",
ciphertext: "The ciphertext to be decrypted (base64 encoded)."
}
};
+28
View File
@@ -0,0 +1,28 @@
// Credit: https://github.com/miguelmota/is-base64
export const isBase64 = (
v: string,
opts = { allowEmpty: false, mimeRequired: false, allowMime: true, paddingRequired: false }
) => {
if (opts.allowEmpty === false && v === "") {
return false;
}
let regex = "(?:[A-Za-z0-9+\\/]{4})*(?:[A-Za-z0-9+\\/]{2}==|[A-Za-z0-9+/]{3}=)?";
const mimeRegex = "(data:\\w+\\/[a-zA-Z\\+\\-\\.]+;base64,)";
if (opts.mimeRequired === true) {
regex = mimeRegex + regex;
} else if (opts.allowMime === true) {
regex = `${mimeRegex}?${regex}`;
}
if (opts.paddingRequired === false) {
regex = "(?:[A-Za-z0-9+\\/]{4})*(?:[A-Za-z0-9+\\/]{2}(==)?|[A-Za-z0-9+\\/]{3}=?)?";
}
return new RegExp(`^${regex}$`, "gi").test(v);
};
export const getBase64SizeInBytes = (base64String: string) => {
return Buffer.from(base64String, "base64").length;
};
+8
View File
@@ -96,6 +96,7 @@ import { certificateAuthorityServiceFactory } from "@app/services/certificate-au
import { certificateTemplateDALFactory } from "@app/services/certificate-template/certificate-template-dal"; import { certificateTemplateDALFactory } from "@app/services/certificate-template/certificate-template-dal";
import { certificateTemplateEstConfigDALFactory } from "@app/services/certificate-template/certificate-template-est-config-dal"; import { certificateTemplateEstConfigDALFactory } from "@app/services/certificate-template/certificate-template-est-config-dal";
import { certificateTemplateServiceFactory } from "@app/services/certificate-template/certificate-template-service"; import { certificateTemplateServiceFactory } from "@app/services/certificate-template/certificate-template-service";
import { cmekServiceFactory } from "@app/services/cmek/cmek-service";
import { externalMigrationServiceFactory } from "@app/services/external-migration/external-migration-service"; import { externalMigrationServiceFactory } from "@app/services/external-migration/external-migration-service";
import { groupProjectDALFactory } from "@app/services/group-project/group-project-dal"; import { groupProjectDALFactory } from "@app/services/group-project/group-project-dal";
import { groupProjectMembershipRoleDALFactory } from "@app/services/group-project/group-project-membership-role-dal"; import { groupProjectMembershipRoleDALFactory } from "@app/services/group-project/group-project-membership-role-dal";
@@ -1194,6 +1195,12 @@ export const registerRoutes = async (
workflowIntegrationDAL workflowIntegrationDAL
}); });
const cmekService = cmekServiceFactory({
kmsDAL,
kmsService,
permissionService
});
const migrationService = externalMigrationServiceFactory({ const migrationService = externalMigrationServiceFactory({
projectService, projectService,
orgService, orgService,
@@ -1283,6 +1290,7 @@ export const registerRoutes = async (
secretSharing: secretSharingService, secretSharing: secretSharingService,
userEngagement: userEngagementService, userEngagement: userEngagementService,
externalKms: externalKmsService, externalKms: externalKmsService,
cmek: cmekService,
orgAdmin: orgAdminService, orgAdmin: orgAdminService,
slack: slackService, slack: slackService,
workflowIntegration: workflowIntegrationService, workflowIntegration: workflowIntegrationService,
+331
View File
@@ -0,0 +1,331 @@
import slugify from "@sindresorhus/slugify";
import { z } from "zod";
import { InternalKmsSchema, KmsKeysSchema } from "@app/db/schemas";
import { EventType } from "@app/ee/services/audit-log/audit-log-types";
import { KMS } from "@app/lib/api-docs";
import { getBase64SizeInBytes, isBase64 } from "@app/lib/base64";
import { SymmetricEncryption } from "@app/lib/crypto/cipher";
import { OrderByDirection } from "@app/lib/types";
import { readLimit, writeLimit } from "@app/server/config/rateLimiter";
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
import { AuthMode } from "@app/services/auth/auth-type";
import { CmekOrderBy } from "@app/services/cmek/cmek-types";
const keyNameSchema = z
.string()
.trim()
.min(1)
.max(32)
.toLowerCase()
.refine((v) => slugify(v) === v, {
message: "Name must be slug friendly"
});
const keyDescriptionSchema = z.string().trim().max(500).optional();
const base64Schema = z.string().superRefine((val, ctx) => {
if (!isBase64(val)) {
ctx.addIssue({
code: z.ZodIssueCode.custom,
message: "plaintext must be base64 encoded"
});
}
if (getBase64SizeInBytes(val) > 4096) {
ctx.addIssue({
code: z.ZodIssueCode.custom,
message: "data cannot exceed 4096 bytes"
});
}
});
export const registerCmekRouter = async (server: FastifyZodProvider) => {
// create encryption key
server.route({
method: "POST",
url: "/keys",
config: {
rateLimit: writeLimit
},
schema: {
description: "Create KMS key",
body: z.object({
projectId: z.string().describe(KMS.CREATE_KEY.projectId),
name: keyNameSchema.describe(KMS.CREATE_KEY.name),
description: keyDescriptionSchema.describe(KMS.CREATE_KEY.description),
encryptionAlgorithm: z
.nativeEnum(SymmetricEncryption)
.optional()
.default(SymmetricEncryption.AES_GCM_256)
.describe(KMS.CREATE_KEY.encryptionAlgorithm) // eventually will support others
}),
response: {
200: z.object({
key: KmsKeysSchema
})
}
},
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
handler: async (req) => {
const {
body: { projectId, name, description, encryptionAlgorithm },
permission
} = req;
const cmek = await server.services.cmek.createCmek(
{ orgId: permission.orgId, projectId, name, description, encryptionAlgorithm },
permission
);
await server.services.auditLog.createAuditLog({
...req.auditLogInfo,
projectId,
event: {
type: EventType.CREATE_CMEK,
metadata: {
keyId: cmek.id,
name,
description,
encryptionAlgorithm
}
}
});
return { key: cmek };
}
});
// update KMS key
server.route({
method: "PATCH",
url: "/keys/:keyId",
config: {
rateLimit: writeLimit
},
schema: {
description: "Update KMS key",
params: z.object({
keyId: z.string().uuid().describe(KMS.UPDATE_KEY.keyId)
}),
body: z.object({
name: keyNameSchema.optional().describe(KMS.UPDATE_KEY.name),
isDisabled: z.boolean().optional().describe(KMS.UPDATE_KEY.isDisabled),
description: keyDescriptionSchema.describe(KMS.UPDATE_KEY.description)
}),
response: {
200: z.object({
key: KmsKeysSchema
})
}
},
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
handler: async (req) => {
const {
params: { keyId },
body,
permission
} = req;
const cmek = await server.services.cmek.updateCmekById({ keyId, ...body }, permission);
await server.services.auditLog.createAuditLog({
...req.auditLogInfo,
orgId: permission.orgId,
event: {
type: EventType.UPDATE_CMEK,
metadata: {
keyId,
...body
}
}
});
return { key: cmek };
}
});
// delete KMS key
server.route({
method: "DELETE",
url: "/keys/:keyId",
config: {
rateLimit: writeLimit
},
schema: {
description: "Delete KMS key",
params: z.object({
keyId: z.string().uuid().describe(KMS.DELETE_KEY.keyId)
}),
response: {
200: z.object({
key: KmsKeysSchema
})
}
},
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
handler: async (req) => {
const {
params: { keyId },
permission
} = req;
const cmek = await server.services.cmek.deleteCmekById(keyId, permission);
await server.services.auditLog.createAuditLog({
...req.auditLogInfo,
orgId: permission.orgId,
event: {
type: EventType.DELETE_CMEK,
metadata: {
keyId
}
}
});
return { key: cmek };
}
});
// list KMS keys
server.route({
method: "GET",
url: "/keys",
config: {
rateLimit: readLimit
},
schema: {
description: "List KMS keys",
querystring: z.object({
projectId: z.string().describe(KMS.LIST_KEYS.projectId),
offset: z.coerce.number().min(0).optional().default(0).describe(KMS.LIST_KEYS.offset),
limit: z.coerce.number().min(1).max(100).optional().default(100).describe(KMS.LIST_KEYS.limit),
orderBy: z.nativeEnum(CmekOrderBy).optional().default(CmekOrderBy.Name).describe(KMS.LIST_KEYS.orderBy),
orderDirection: z
.nativeEnum(OrderByDirection)
.optional()
.default(OrderByDirection.ASC)
.describe(KMS.LIST_KEYS.orderDirection),
search: z.string().trim().optional().describe(KMS.LIST_KEYS.search)
}),
response: {
200: z.object({
keys: KmsKeysSchema.merge(InternalKmsSchema.pick({ version: true, encryptionAlgorithm: true })).array(),
totalCount: z.number()
})
}
},
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
handler: async (req) => {
const {
query: { projectId, ...dto },
permission
} = req;
const { cmeks, totalCount } = await server.services.cmek.listCmeksByProjectId({ projectId, ...dto }, permission);
await server.services.auditLog.createAuditLog({
...req.auditLogInfo,
projectId,
event: {
type: EventType.GET_CMEKS,
metadata: {
keyIds: cmeks.map((key) => key.id)
}
}
});
return { keys: cmeks, totalCount };
}
});
// encrypt data
server.route({
method: "POST",
url: "/keys/:keyId/encrypt",
config: {
rateLimit: writeLimit
},
schema: {
description: "Encrypt data with KMS key",
params: z.object({
keyId: z.string().uuid().describe(KMS.ENCRYPT.keyId)
}),
body: z.object({
plaintext: base64Schema.describe(KMS.ENCRYPT.plaintext)
}),
response: {
200: z.object({
ciphertext: z.string()
})
}
},
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
handler: async (req) => {
const {
params: { keyId },
body: { plaintext },
permission
} = req;
const ciphertext = await server.services.cmek.cmekEncrypt({ keyId, plaintext }, permission);
await server.services.auditLog.createAuditLog({
...req.auditLogInfo,
orgId: permission.orgId,
event: {
type: EventType.CMEK_ENCRYPT,
metadata: {
keyId
}
}
});
return { ciphertext };
}
});
server.route({
method: "POST",
url: "/keys/:keyId/decrypt",
config: {
rateLimit: writeLimit
},
schema: {
description: "Decrypt data with KMS key",
params: z.object({
keyId: z.string().uuid().describe(KMS.ENCRYPT.keyId)
}),
body: z.object({
ciphertext: base64Schema.describe(KMS.ENCRYPT.plaintext)
}),
response: {
200: z.object({
plaintext: z.string()
})
}
},
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
handler: async (req) => {
const {
params: { keyId },
body: { ciphertext },
permission
} = req;
const plaintext = await server.services.cmek.cmekDecrypt({ keyId, ciphertext }, permission);
await server.services.auditLog.createAuditLog({
...req.auditLogInfo,
orgId: permission.orgId,
event: {
type: EventType.CMEK_DECRYPT,
metadata: {
keyId
}
}
});
return { plaintext };
}
});
};
+2 -1
View File
@@ -1,3 +1,4 @@
import { registerCmekRouter } from "@app/server/routes/v1/cmek-router";
import { registerDashboardRouter } from "@app/server/routes/v1/dashboard-router"; import { registerDashboardRouter } from "@app/server/routes/v1/dashboard-router";
import { registerAdminRouter } from "./admin-router"; import { registerAdminRouter } from "./admin-router";
@@ -103,6 +104,6 @@ export const registerV1Routes = async (server: FastifyZodProvider) => {
await server.register(registerIdentityRouter, { prefix: "/identities" }); await server.register(registerIdentityRouter, { prefix: "/identities" });
await server.register(registerSecretSharingRouter, { prefix: "/secret-sharing" }); await server.register(registerSecretSharingRouter, { prefix: "/secret-sharing" });
await server.register(registerUserEngagementRouter, { prefix: "/user-engagement" }); await server.register(registerUserEngagementRouter, { prefix: "/user-engagement" });
await server.register(registerDashboardRouter, { prefix: "/dashboard" }); await server.register(registerDashboardRouter, { prefix: "/dashboard" });
await server.register(registerCmekRouter, { prefix: "/kms" });
}; };
+169
View File
@@ -0,0 +1,169 @@
import { ForbiddenError } from "@casl/ability";
import { FastifyRequest } from "fastify";
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service";
import { ProjectPermissionCmekActions, ProjectPermissionSub } from "@app/ee/services/permission/project-permission";
import { BadRequestError, NotFoundError } from "@app/lib/errors";
import {
TCmekDecryptDTO,
TCmekEncryptDTO,
TCreateCmekDTO,
TListCmeksByProjectIdDTO,
TUpdabteCmekByIdDTO
} from "@app/services/cmek/cmek-types";
import { TKmsKeyDALFactory } from "@app/services/kms/kms-key-dal";
import { TKmsServiceFactory } from "@app/services/kms/kms-service";
type TCmekServiceFactoryDep = {
kmsService: TKmsServiceFactory;
kmsDAL: TKmsKeyDALFactory;
permissionService: TPermissionServiceFactory;
};
export type TCmekServiceFactory = ReturnType<typeof cmekServiceFactory>;
export const cmekServiceFactory = ({ kmsService, kmsDAL, permissionService }: TCmekServiceFactoryDep) => {
const createCmek = async ({ projectId, ...dto }: TCreateCmekDTO, actor: FastifyRequest["permission"]) => {
const { permission } = await permissionService.getProjectPermission(
actor.type,
actor.id,
projectId,
actor.authMethod,
actor.orgId
);
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionCmekActions.Create, ProjectPermissionSub.Cmek);
const cmek = await kmsService.generateKmsKey({
...dto,
projectId,
isReserved: false
});
return cmek;
};
const updateCmekById = async ({ keyId, ...data }: TUpdabteCmekByIdDTO, actor: FastifyRequest["permission"]) => {
const key = await kmsDAL.findById(keyId);
if (!key) throw new NotFoundError({ message: "Key not found" });
if (!key.projectId || key.isReserved) throw new BadRequestError({ message: "Key is not customer managed" });
const { permission } = await permissionService.getProjectPermission(
actor.type,
actor.id,
key.projectId,
actor.authMethod,
actor.orgId
);
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionCmekActions.Edit, ProjectPermissionSub.Cmek);
const cmek = await kmsDAL.updateById(keyId, data);
return cmek;
};
const deleteCmekById = async (keyId: string, actor: FastifyRequest["permission"]) => {
const key = await kmsDAL.findById(keyId);
if (!key) throw new NotFoundError({ message: "Key not found" });
if (!key.projectId || key.isReserved) throw new BadRequestError({ message: "Key is not customer managed" });
const { permission } = await permissionService.getProjectPermission(
actor.type,
actor.id,
key.projectId,
actor.authMethod,
actor.orgId
);
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionCmekActions.Delete, ProjectPermissionSub.Cmek);
const cmek = kmsDAL.deleteById(keyId);
return cmek;
};
const listCmeksByProjectId = async (
{ projectId, ...filters }: TListCmeksByProjectIdDTO,
actor: FastifyRequest["permission"]
) => {
const { permission } = await permissionService.getProjectPermission(
actor.type,
actor.id,
projectId,
actor.authMethod,
actor.orgId
);
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionCmekActions.Read, ProjectPermissionSub.Cmek);
const { keys: cmeks, totalCount } = await kmsDAL.findKmsKeysByProjectId({ projectId, ...filters });
return { cmeks, totalCount };
};
const cmekEncrypt = async ({ keyId, plaintext }: TCmekEncryptDTO, actor: FastifyRequest["permission"]) => {
const key = await kmsDAL.findById(keyId);
if (!key) throw new NotFoundError({ message: "Key not found" });
if (!key.projectId || key.isReserved) throw new BadRequestError({ message: "Key is not customer managed" });
if (key.isDisabled) throw new BadRequestError({ message: "Key is disabled" });
const { permission } = await permissionService.getProjectPermission(
actor.type,
actor.id,
key.projectId,
actor.authMethod,
actor.orgId
);
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionCmekActions.Encrypt, ProjectPermissionSub.Cmek);
const encrypt = await kmsService.encryptWithKmsKey({ kmsId: keyId });
const { cipherTextBlob } = await encrypt({ plainText: Buffer.from(plaintext, "base64") });
return cipherTextBlob.toString("base64");
};
const cmekDecrypt = async ({ keyId, ciphertext }: TCmekDecryptDTO, actor: FastifyRequest["permission"]) => {
const key = await kmsDAL.findById(keyId);
if (!key) throw new NotFoundError({ message: "Key not found" });
if (!key.projectId || key.isReserved) throw new BadRequestError({ message: "Key is not customer managed" });
if (key.isDisabled) throw new BadRequestError({ message: "Key is disabled" });
const { permission } = await permissionService.getProjectPermission(
actor.type,
actor.id,
key.projectId,
actor.authMethod,
actor.orgId
);
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionCmekActions.Decrypt, ProjectPermissionSub.Cmek);
const decrypt = await kmsService.decryptWithKmsKey({ kmsId: keyId });
const plaintextBlob = await decrypt({ cipherTextBlob: Buffer.from(ciphertext, "base64") });
return plaintextBlob.toString("base64");
};
return {
createCmek,
updateCmekById,
deleteCmekById,
listCmeksByProjectId,
cmekEncrypt,
cmekDecrypt
};
};
+40
View File
@@ -0,0 +1,40 @@
import { SymmetricEncryption } from "@app/lib/crypto/cipher";
import { OrderByDirection } from "@app/lib/types";
export type TCreateCmekDTO = {
orgId: string;
projectId: string;
name: string;
description?: string;
encryptionAlgorithm: SymmetricEncryption;
};
export type TUpdabteCmekByIdDTO = {
keyId: string;
name?: string;
isDisabled?: boolean;
description?: string;
};
export type TListCmeksByProjectIdDTO = {
projectId: string;
offset?: number;
limit?: number;
orderBy?: CmekOrderBy;
orderDirection?: OrderByDirection;
search?: string;
};
export type TCmekEncryptDTO = {
keyId: string;
plaintext: string;
};
export type TCmekDecryptDTO = {
keyId: string;
ciphertext: string;
};
export enum CmekOrderBy {
Name = "name"
}
@@ -5,7 +5,7 @@ import { TableName, TIdentityOrgMemberships } from "@app/db/schemas";
import { DatabaseError } from "@app/lib/errors"; import { DatabaseError } from "@app/lib/errors";
import { ormify, selectAllTableCols, sqlNestRelationships } from "@app/lib/knex"; import { ormify, selectAllTableCols, sqlNestRelationships } from "@app/lib/knex";
import { OrderByDirection } from "@app/lib/types"; import { OrderByDirection } from "@app/lib/types";
import { TListOrgIdentitiesByOrgIdDTO } from "@app/services/identity/identity-types"; import { OrgIdentityOrderBy, TListOrgIdentitiesByOrgIdDTO } from "@app/services/identity/identity-types";
export type TIdentityOrgDALFactory = ReturnType<typeof identityOrgDALFactory>; export type TIdentityOrgDALFactory = ReturnType<typeof identityOrgDALFactory>;
@@ -33,7 +33,7 @@ export const identityOrgDALFactory = (db: TDbClient) => {
{ {
limit, limit,
offset = 0, offset = 0,
orderBy, orderBy = OrgIdentityOrderBy.Name,
orderDirection = OrderByDirection.ASC, orderDirection = OrderByDirection.ASC,
search, search,
...filter ...filter
@@ -43,12 +43,16 @@ export const identityOrgDALFactory = (db: TDbClient) => {
) => { ) => {
try { try {
const paginatedFetchIdentity = (tx || db.replicaNode())(TableName.Identity) const paginatedFetchIdentity = (tx || db.replicaNode())(TableName.Identity)
.where((queryBuilder) => { .as(TableName.Identity)
if (limit) { .orderBy(`${TableName.Identity}.${orderBy}`, orderDirection);
void queryBuilder.offset(offset).limit(limit);
} if (search?.length) {
}) void paginatedFetchIdentity.whereILike(`${TableName.Identity}.name`, `%${search}%`);
.as(TableName.Identity); }
if (limit) {
void paginatedFetchIdentity.offset(offset).limit(limit);
}
const query = (tx || db.replicaNode())(TableName.IdentityOrgMembership) const query = (tx || db.replicaNode())(TableName.IdentityOrgMembership)
.where(filter) .where(filter)
@@ -78,24 +82,8 @@ export const identityOrgDALFactory = (db: TDbClient) => {
db.ref("id").withSchema(TableName.IdentityMetadata).as("metadataId"), db.ref("id").withSchema(TableName.IdentityMetadata).as("metadataId"),
db.ref("key").withSchema(TableName.IdentityMetadata).as("metadataKey"), db.ref("key").withSchema(TableName.IdentityMetadata).as("metadataKey"),
db.ref("value").withSchema(TableName.IdentityMetadata).as("metadataValue") db.ref("value").withSchema(TableName.IdentityMetadata).as("metadataValue")
); )
.orderBy(`${TableName.Identity}.${orderBy}`, orderDirection);
if (orderBy) {
switch (orderBy) {
case "name":
void query.orderBy(`${TableName.Identity}.${orderBy}`, orderDirection);
break;
case "role":
void query.orderBy(`${TableName.IdentityOrgMembership}.${orderBy}`, orderDirection);
break;
default:
// do nothing
}
}
if (search?.length) {
void query.whereILike(`${TableName.Identity}.name`, `%${search}%`);
}
const docs = await query; const docs = await query;
const formattedDocs = sqlNestRelationships({ const formattedDocs = sqlNestRelationships({
@@ -41,6 +41,6 @@ export type TListOrgIdentitiesByOrgIdDTO = {
} & TOrgPermission; } & TOrgPermission;
export enum OrgIdentityOrderBy { export enum OrgIdentityOrderBy {
Name = "name", Name = "name"
Role = "role" // Role = "role"
} }
+11
View File
@@ -0,0 +1,11 @@
import { SymmetricEncryption } from "@app/lib/crypto/cipher";
export const getByteLengthForAlgorithm = (encryptionAlgorithm: SymmetricEncryption) => {
switch (encryptionAlgorithm) {
case SymmetricEncryption.AES_GCM_128:
return 16;
case SymmetricEncryption.AES_GCM_256:
default:
return 32;
}
};
+49 -2
View File
@@ -1,9 +1,11 @@
import { Knex } from "knex"; import { Knex } from "knex";
import { TDbClient } from "@app/db"; import { TDbClient } from "@app/db";
import { KmsKeysSchema, TableName } 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 { ormify, selectAllTableCols } from "@app/lib/knex";
import { OrderByDirection } from "@app/lib/types";
import { CmekOrderBy, TListCmeksByProjectIdDTO } from "@app/services/cmek/cmek-types";
export type TKmsKeyDALFactory = ReturnType<typeof kmskeyDALFactory>; export type TKmsKeyDALFactory = ReturnType<typeof kmskeyDALFactory>;
@@ -71,5 +73,50 @@ export const kmskeyDALFactory = (db: TDbClient) => {
} }
}; };
return { ...kmsOrm, findByIdWithAssociatedKms }; const findKmsKeysByProjectId = async (
{
projectId,
offset = 0,
limit,
orderBy = CmekOrderBy.Name,
orderDirection = OrderByDirection.ASC,
search
}: TListCmeksByProjectIdDTO,
tx?: Knex
) => {
try {
const query = (tx || db.replicaNode())(TableName.KmsKey)
.where("projectId", projectId)
.where((qb) => {
if (search) {
void qb.whereILike("name", `%${search}%`);
}
})
.join(TableName.InternalKms, `${TableName.InternalKms}.kmsKeyId`, `${TableName.KmsKey}.id`)
.select<
(TKmsKeys &
Pick<TInternalKms, "version" | "encryptionAlgorithm"> & {
total_count: number;
})[]
>(
selectAllTableCols(TableName.KmsKey),
db.raw(`count(*) OVER() as total_count`),
db.ref("encryptionAlgorithm").withSchema(TableName.InternalKms),
db.ref("version").withSchema(TableName.InternalKms)
)
.orderBy(orderBy, orderDirection);
if (limit) {
void query.limit(limit).offset(offset);
}
const data = await query;
return { keys: data, totalCount: Number(data?.[0]?.total_count ?? 0) };
} catch (error) {
throw new DatabaseError({ error, name: "Find kms keys by project id" });
}
};
return { ...kmsOrm, findByIdWithAssociatedKms, findKmsKeysByProjectId };
}; };
+29 -15
View File
@@ -17,6 +17,7 @@ import { generateHash } from "@app/lib/crypto/encryption";
import { BadRequestError, ForbiddenRequestError, NotFoundError } from "@app/lib/errors"; import { BadRequestError, ForbiddenRequestError, NotFoundError } from "@app/lib/errors";
import { logger } from "@app/lib/logger"; import { logger } from "@app/lib/logger";
import { alphaNumericNanoId } from "@app/lib/nanoid"; import { alphaNumericNanoId } from "@app/lib/nanoid";
import { getByteLengthForAlgorithm } from "@app/services/kms/kms-fns";
import { TOrgDALFactory } from "../org/org-dal"; import { TOrgDALFactory } from "../org/org-dal";
import { TProjectDALFactory } from "../project/project-dal"; import { TProjectDALFactory } from "../project/project-dal";
@@ -71,17 +72,29 @@ export const kmsServiceFactory = ({
* This function is responsibile for generating the infisical internal KMS for various entities * This function is responsibile for generating the infisical internal KMS for various entities
* Like for secret manager, cert manager or for organization * Like for secret manager, cert manager or for organization
*/ */
const generateKmsKey = async ({ orgId, isReserved = true, tx, slug }: TGenerateKMSDTO) => { const generateKmsKey = async ({
orgId,
isReserved = true,
tx,
name,
projectId,
encryptionAlgorithm = SymmetricEncryption.AES_GCM_256,
description
}: TGenerateKMSDTO) => {
const cipher = symmetricCipherService(SymmetricEncryption.AES_GCM_256); const cipher = symmetricCipherService(SymmetricEncryption.AES_GCM_256);
const kmsKeyMaterial = randomSecureBytes(32);
const kmsKeyMaterial = randomSecureBytes(getByteLengthForAlgorithm(encryptionAlgorithm));
const encryptedKeyMaterial = cipher.encrypt(kmsKeyMaterial, ROOT_ENCRYPTION_KEY); const encryptedKeyMaterial = cipher.encrypt(kmsKeyMaterial, ROOT_ENCRYPTION_KEY);
const sanitizedSlug = slug ? slugify(slug) : slugify(alphaNumericNanoId(8).toLowerCase()); const sanitizedName = name ? slugify(name) : slugify(alphaNumericNanoId(8).toLowerCase());
const dbQuery = async (db: Knex) => { const dbQuery = async (db: Knex) => {
const kmsDoc = await kmsDAL.create( const kmsDoc = await kmsDAL.create(
{ {
slug: sanitizedSlug, name: sanitizedName,
orgId, orgId,
isReserved isReserved,
projectId,
description
}, },
db db
); );
@@ -90,7 +103,7 @@ export const kmsServiceFactory = ({
{ {
version: 1, version: 1,
encryptedKey: encryptedKeyMaterial, encryptedKey: encryptedKeyMaterial,
encryptionAlgorithm: SymmetricEncryption.AES_GCM_256, encryptionAlgorithm,
kmsKeyId: kmsDoc.id kmsKeyId: kmsDoc.id
}, },
db db
@@ -286,12 +299,13 @@ export const kmsServiceFactory = ({
} }
// internal KMS // internal KMS
const cipher = symmetricCipherService(SymmetricEncryption.AES_GCM_256); const keyCipher = symmetricCipherService(SymmetricEncryption.AES_GCM_256);
const kmsKey = cipher.decrypt(kmsDoc.internalKms?.encryptedKey as Buffer, ROOT_ENCRYPTION_KEY); const dataCipher = symmetricCipherService(kmsDoc.internalKms?.encryptionAlgorithm as SymmetricEncryption);
const kmsKey = keyCipher.decrypt(kmsDoc.internalKms?.encryptedKey as Buffer, ROOT_ENCRYPTION_KEY);
return ({ cipherTextBlob: versionedCipherTextBlob }: Pick<TDecryptWithKmsDTO, "cipherTextBlob">) => { return ({ cipherTextBlob: versionedCipherTextBlob }: Pick<TDecryptWithKmsDTO, "cipherTextBlob">) => {
const cipherTextBlob = versionedCipherTextBlob.subarray(0, -KMS_VERSION_BLOB_LENGTH); const cipherTextBlob = versionedCipherTextBlob.subarray(0, -KMS_VERSION_BLOB_LENGTH);
const decryptedBlob = cipher.decrypt(cipherTextBlob, kmsKey); const decryptedBlob = dataCipher.decrypt(cipherTextBlob, kmsKey);
return Promise.resolve(decryptedBlob); return Promise.resolve(decryptedBlob);
}; };
}; };
@@ -347,11 +361,11 @@ export const kmsServiceFactory = ({
} }
// internal KMS // internal KMS
// akhilmhdh: as more encryption are added do a check here on kmsDoc.encryptionAlgorithm const keyCipher = symmetricCipherService(SymmetricEncryption.AES_GCM_256);
const cipher = symmetricCipherService(SymmetricEncryption.AES_GCM_256); const dataCipher = symmetricCipherService(kmsDoc.internalKms?.encryptionAlgorithm as SymmetricEncryption);
return ({ plainText }: Pick<TEncryptWithKmsDTO, "plainText">) => { return ({ plainText }: Pick<TEncryptWithKmsDTO, "plainText">) => {
const kmsKey = cipher.decrypt(kmsDoc.internalKms?.encryptedKey as Buffer, ROOT_ENCRYPTION_KEY); const kmsKey = keyCipher.decrypt(kmsDoc.internalKms?.encryptedKey as Buffer, ROOT_ENCRYPTION_KEY);
const encryptedPlainTextBlob = cipher.encrypt(plainText, kmsKey); const encryptedPlainTextBlob = dataCipher.encrypt(plainText, kmsKey);
// Buffer#1 encrypted text + Buffer#2 version number // Buffer#1 encrypted text + Buffer#2 version number
const versionBlob = Buffer.from(KMS_VERSION, "utf8"); // length is 3 const versionBlob = Buffer.from(KMS_VERSION, "utf8"); // length is 3
@@ -767,8 +781,8 @@ export const kmsServiceFactory = ({
message: "KMS not found" message: "KMS not found"
}); });
} }
const { id, slug, orgId, isExternal } = kms; const { id, name, orgId, isExternal } = kms;
return { id, slug, orgId, isExternal }; return { id, name, orgId, isExternal };
}; };
// akhilmhdh: a copy of this is made in migrations/utils/kms // akhilmhdh: a copy of this is made in migrations/utils/kms
+6 -1
View File
@@ -1,5 +1,7 @@
import { Knex } from "knex"; import { Knex } from "knex";
import { SymmetricEncryption } from "@app/lib/crypto/cipher";
export enum KmsDataKey { export enum KmsDataKey {
Organization, Organization,
SecretManager SecretManager
@@ -22,8 +24,11 @@ export type TEncryptWithKmsDataKeyDTO =
export type TGenerateKMSDTO = { export type TGenerateKMSDTO = {
orgId: string; orgId: string;
projectId?: string;
encryptionAlgorithm?: SymmetricEncryption;
isReserved?: boolean; isReserved?: boolean;
slug?: string; name?: string;
description?: string;
tx?: Knex; tx?: Knex;
}; };
@@ -0,0 +1,4 @@
---
title: "Create Key"
openapi: "POST /api/v1/kms/keys"
---
@@ -0,0 +1,4 @@
---
title: "Decrypt Data"
openapi: "POST /api/v1/kms/keys/{keyId}/decrypt"
---
@@ -0,0 +1,4 @@
---
title: "Delete Key"
openapi: "DELETE /api/v1/kms/keys/{keyId}"
---
@@ -0,0 +1,4 @@
---
title: "Encrypt Data"
openapi: "POST /api/v1/kms/keys/{keyId}/encrypt"
---
@@ -0,0 +1,4 @@
---
title: "List Keys"
openapi: "Get /api/v1/kms/keys"
---
@@ -0,0 +1,4 @@
---
title: "Update Key"
openapi: "PATCH /api/v1/kms/keys/{keyId}"
---
@@ -7,7 +7,7 @@ description: "Learn how to authenticate with Infisical for EC2 instances, Lambda
## Diagram ## Diagram
The following sequence digram illustrates the AWS Auth workflow for authenticating AWS IAM principals with Infisical. The following sequence diagram illustrates the AWS Auth workflow for authenticating AWS IAM principals with Infisical.
```mermaid ```mermaid
sequenceDiagram sequenceDiagram
@@ -7,7 +7,7 @@ description: "Learn how to authenticate with Infisical for services on Azure"
## Diagram ## Diagram
The following sequence digram illustrates the Azure Auth workflow for authenticating Azure [service principals](https://learn.microsoft.com/en-us/entra/identity-platform/app-objects-and-service-principals?tabs=browser) with Infisical. The following sequence diagram illustrates the Azure Auth workflow for authenticating Azure [service principals](https://learn.microsoft.com/en-us/entra/identity-platform/app-objects-and-service-principals?tabs=browser) with Infisical.
```mermaid ```mermaid
sequenceDiagram sequenceDiagram
@@ -13,7 +13,7 @@ description: "Learn how to authenticate with Infisical for services on Google Cl
## Diagram ## Diagram
The following sequence digram illustrates the GCP ID Token Auth workflow for authenticating GCP resources with Infisical. The following sequence diagram illustrates the GCP ID Token Auth workflow for authenticating GCP resources with Infisical.
```mermaid ```mermaid
sequenceDiagram sequenceDiagram
@@ -182,7 +182,7 @@ access the Infisical API using the GCP ID Token authentication method.
## Diagram ## Diagram
The following sequence digram illustrates the GCP IAM Auth workflow for authenticating GCP IAM service accounts with Infisical. The following sequence diagram illustrates the GCP IAM Auth workflow for authenticating GCP IAM service accounts with Infisical.
```mermaid ```mermaid
sequenceDiagram sequenceDiagram
@@ -7,7 +7,7 @@ description: "Learn how to authenticate with Infisical in Kubernetes"
## Diagram ## Diagram
The following sequence digram illustrates the Kubernetes Auth workflow for authenticating applications running in pods with Infisical. The following sequence diagram illustrates the Kubernetes Auth workflow for authenticating applications running in pods with Infisical.
```mermaid ```mermaid
sequenceDiagram sequenceDiagram
@@ -7,7 +7,7 @@ description: "Learn how to authenticate to Infisical from any platform or enviro
## Diagram ## Diagram
The following sequence digram illustrates the Token Auth workflow for authenticating clients with Infisical. The following sequence diagram illustrates the Token Auth workflow for authenticating clients with Infisical.
```mermaid ```mermaid
sequenceDiagram sequenceDiagram
@@ -7,7 +7,7 @@ description: "Learn how to authenticate to Infisical from any platform or enviro
## Diagram ## Diagram
The following sequence digram illustrates the Universal Auth workflow for authenticating clients with Infisical. The following sequence diagram illustrates the Universal Auth workflow for authenticating clients with Infisical.
```mermaid ```mermaid
sequenceDiagram sequenceDiagram
@@ -1,5 +1,5 @@
--- ---
title: "Key Management Service (KMS)" title: "Key Management Service (KMS) Configuration"
sidebarTitle: "Overview" sidebarTitle: "Overview"
description: "Learn how to configure your project's encryption" description: "Learn how to configure your project's encryption"
--- ---
@@ -25,4 +25,4 @@ For existing projects, you can configure the KMS from the Project Settings page.
## External KMS ## External KMS
Infisical supports the use of external KMS solutions to enhance security and compliance. You can configure your project to use services like [AWS Key Management Service](./aws-kms) for managing encryption. Infisical supports the use of external KMS solutions to enhance security and compliance. You can configure your project to use services like [AWS Key Management Service](./aws-kms) for managing encryption.
+208
View File
@@ -0,0 +1,208 @@
---
title: "Key Management Service (KMS)"
sidebarTitle: "Key Management (KMS)"
description: "Learn how to manage and use cryptographic keys with Infisical."
---
## Concept
Infisical can be used as a Key Management System (KMS), referred to as Infisical KMS, to centralize management of keys to be used for cryptographic operations like encryption/decryption.
<Note>
Keys managed in KMS are not extractable from the platform. Additionally, data
is never stored when performing cryptographic operations.
</Note>
## Workflow
The typical workflow for using Infisical KMS consists of the following steps:
1. Creating a KMS key. As part of this step, you specify a name for the key and the encryption algorithm meant to be used for it (e.g. `AES-GCM-128`, `AES-GCM-256`).
2. Encryption: To encrypt data, you would make a request to the Infisical KMS API endpoint, specifying the base64-encoded plaintext and the intended key to use for encryption; the API would return the base64-encoded ciphertext.
3. Decryption: To decrypt data, you would make a request to the Infisical KMS API endpoint, specifying the base64-encoded ciphertext and the intended key to use for decryption; the API would return the base64-encoded plaintext.
<Note>
Note that this workflow can be executed via the Infisical UI or manually such
as via API.
</Note>
## Guide to Encrypting Data
In the following steps, we explore how to generate a key and use it to encrypt data.
<Tabs>
<Tab title="Infisical UI">
<Steps>
<Step title="Creating a KMS key">
Navigate to Project > Key Management and tap on the **Add Key** button.
![kms add key button](/images/platform/kms/infisical-kms/kms-add-key.png)
Specify your key details. Here's some guidance on each field:
- Name: A slug-friendly name for the key.
- Type: The encryption algorithm associated with the key (e.g. `AES-GCM-256`).
- Description: An optional description of what the intended usage is for the key.
![kms add key modal](/images/platform/kms/infisical-kms/kms-add-key-modal.png)
</Step>
<Step title="Encrypting data with the KMS key">
Once your key is generated, open the options menu for the newly created key and select encrypt data.
![kms key options](/images/platform/kms/infisical-kms/kms-key-options.png)
Populate the text area with your data and tap on the Encrypt button.
![kms encrypt data](/images/platform/kms/infisical-kms/kms-encrypt-data.png)
<Note>
If your data is already Base64 encoded make sure to toggle the respective switch on to avoid
redundant encoding.
</Note>
Copy and store the encrypted data.
![kms encrypted data](/images/platform/kms/infisical-kms/kms-encrypted-data.png)
</Step>
</Steps>
</Tab>
<Tab title="API">
<Steps>
<Step title="Creating a KMS key">
To create a cryptographic key, make an API request to the [Create KMS
Key](/api-reference/endpoints/kms/keys/create) API endpoint.
### Sample request
```bash Request
curl --request POST \
--url https://app.infisical.com/api/v1/kms/keys \
--header 'Content-Type: application/json' \
--data '{
"projectId": "<project-id>",
"name": "my-secret-key",
"description": "...",
"encryptionAlgorithm": "aes-256-gcm"
}'
```
### Sample response
```bash Response
{
"key": {
"id": "<key-id>",
"description": "...",
"isDisabled": false,
"isReserved": false,
"orgId": "<org-id>",
"name": "my-secret-key",
"createdAt": "2023-11-07T05:31:56Z",
"updatedAt": "2023-11-07T05:31:56Z",
"projectId": "<project-id>"
}
}
```
</Step>
<Step title="Encrypting data with the KMS key">
To encrypt data, make an API request to the [Encrypt
Data](/api-reference/endpoints/kms/keys/encrypt) API endpoint,
specifying the key to use.
<Note>
Make sure your data is Base64 encoded
</Note>
### Sample request
```bash Request
curl --request POST \
--url https://app.infisical.com/api/v1/kms/keys/<key-id>/encrypt \
--header 'Content-Type: application/json' \
--data '{
"plaintext": "lUFHM5Ggwo6TOfpuN1S==" // base64 encoded plaintext
}'
```
### Sample response
```bash Response
{
"ciphertext": "HwFHwSFHwlMF6TOfp==" // base64 encoded ciphertext
}
```
</Step>
</Steps>
</Tab>
</Tabs>
## Guide to Decrypting Data
In the following steps, we explore how to use decrypt data using an existing key in Infisical KMS.
<Tabs>
<Tab title="Infisical UI">
<Steps>
<Step title="Accessing your key">
Navigate to Project > Key Management and open the options menu for the key used to encrypt the data
you want to decrypt.
![kms key options](/images/platform/kms/infisical-kms/kms-decrypt-options.png)
</Step>
<Step title="Decrypting your data">
Paste your encrypted data into the text area and tap on the Decrypt button. Optionally, if your data was
originally plain text, enable the decode Base64 switch.
![kms decrypt data](/images/platform/kms/infisical-kms/kms-decrypt-data.png)
Your decrypted data will be displayed and can be copied for use.
![kms decrypted data](/images/platform/kms/infisical-kms/kms-decrypted-data.png)
</Step>
</Steps>
</Tab>
<Tab title="API">
<Steps>
<Step title="Decrypting data">
To decrypt data, make an API request to the [Decrypt
Data](/api-reference/endpoints/kms/keys/decrypt) API endpoint,
specifying the key to use.
### Sample request
```bash Request
curl --request POST \
--url https://app.infisical.com/api/v1/kms/keys/<key-id>/decrypt \
--header 'Content-Type: application/json' \
--data '{
"ciphertext": "HwFHwSFHwlMF6TOfp==" // base64 encoded ciphertext
}'
```
### Sample response
```bash Response
{
"plaintext": "lUFHM5Ggwo6TOfpuN1S==" // base64 encoded plaintext
}
```
</Step>
</Steps>
</Tab>
</Tabs>
## FAQ
<AccordionGroup>
<Accordion title="Is my data stored in Infisical KMS?">
No. Infisical's KMS only provides cryptographic services and does not store
any encrypted or decrypted data.
</Accordion>
<Accordion title="Can key material be accessed outside of Infisical KMS?">
No. Infisical's KMS will never expose your keys, encrypted or decrypted, to
external sources.
</Accordion>
<Accordion title="What algorithms does Infisical KMS support?">
Currently, Infisical only supports `AES-128-GCM` and `AES-256-GCM` for
encryption operations. We anticipate supporting more algorithms and
cryptographic operations in the coming months.
</Accordion>
</AccordionGroup>
Binary file not shown.

After

Width:  |  Height:  |  Size: 535 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 702 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 624 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 942 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 585 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 558 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 586 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 734 KiB

+25 -8
View File
@@ -113,6 +113,15 @@
"documentation/platform/pki/alerting" "documentation/platform/pki/alerting"
] ]
}, },
"documentation/platform/kms",
{
"group": "KMS Configuration",
"pages": [
"documentation/platform/kms-configuration/overview",
"documentation/platform/kms-configuration/aws-kms",
"documentation/platform/kms-configuration/aws-hsm"
]
},
{ {
"group": "Identities", "group": "Identities",
"pages": [ "pages": [
@@ -172,14 +181,6 @@
"documentation/platform/dynamic-secrets/ldap" "documentation/platform/dynamic-secrets/ldap"
] ]
}, },
{
"group": "Key Management (KMS)",
"pages": [
"documentation/platform/kms/overview",
"documentation/platform/kms/aws-kms",
"documentation/platform/kms/aws-hsm"
]
},
{ {
"group": "Workflow Integrations", "group": "Workflow Integrations",
"pages": [ "pages": [
@@ -790,6 +791,22 @@
} }
] ]
}, },
{
"group": "Infisical KMS",
"pages": [
{
"group": "Keys",
"pages": [
"api-reference/endpoints/kms/keys/list",
"api-reference/endpoints/kms/keys/create",
"api-reference/endpoints/kms/keys/update",
"api-reference/endpoints/kms/keys/delete",
"api-reference/endpoints/kms/keys/encrypt",
"api-reference/endpoints/kms/keys/decrypt"
]
}
]
},
{ {
"group": "Internals", "group": "Internals",
"pages": [ "pages": [
@@ -86,13 +86,15 @@ export const DropdownMenuItem = <T extends ElementType = "button">({
icon, icon,
as: Item = "button", as: Item = "button",
iconPos = "left", iconPos = "left",
isDisabled = false,
...props ...props
}: DropdownMenuItemProps<T> & ComponentPropsWithRef<T>) => ( }: DropdownMenuItemProps<T> & ComponentPropsWithRef<T> & { isDisabled?: boolean }) => (
<DropdownMenuPrimitive.Item <DropdownMenuPrimitive.Item
{...props} {...props}
className={twMerge( className={twMerge(
"block cursor-pointer rounded-sm px-4 py-2 font-inter text-xs text-mineshaft-200 outline-none data-[highlighted]:bg-mineshaft-700", "block cursor-pointer rounded-sm px-4 py-2 font-inter text-xs text-mineshaft-200 outline-none data-[highlighted]:bg-mineshaft-700",
className className,
isDisabled ? "pointer-events-none opacity-50" : ""
)} )}
> >
<Item type="button" role="menuitem" className="flex w-full items-center" ref={inputRef}> <Item type="button" role="menuitem" className="flex w-full items-center" ref={inputRef}>
@@ -49,7 +49,7 @@ export const Pagination = ({
return ( return (
<div <div
className={twMerge( className={twMerge(
"flex w-full items-center justify-end bg-mineshaft-800 py-3 px-4 text-white", "flex w-full items-center justify-end border-t border-mineshaft-600 bg-mineshaft-800 py-3 px-4 text-white",
className className
)} )}
> >
+3 -1
View File
@@ -8,6 +8,7 @@ export type SwitchProps = Omit<SwitchPrimitive.SwitchProps, "checked" | "disable
isChecked?: boolean; isChecked?: boolean;
isRequired?: boolean; isRequired?: boolean;
isDisabled?: boolean; isDisabled?: boolean;
containerClassName?: string;
}; };
export const Switch = ({ export const Switch = ({
@@ -17,9 +18,10 @@ export const Switch = ({
isChecked, isChecked,
isDisabled, isDisabled,
isRequired, isRequired,
containerClassName,
...props ...props
}: SwitchProps): JSX.Element => ( }: SwitchProps): JSX.Element => (
<div className="flex items-center font-inter text-bunker-300"> <div className={twMerge("flex items-center font-inter text-bunker-300", containerClassName)}>
<label className="text-sm" htmlFor={id}> <label className="text-sm" htmlFor={id}>
{children} {children}
{isRequired && <span className="pl-1 text-red">*</span>} {isRequired && <span className="pl-1 text-red">*</span>}
+30 -25
View File
@@ -27,35 +27,40 @@ export const Tooltip = ({
isDisabled, isDisabled,
position = "top", position = "top",
...props ...props
}: TooltipProps) => ( }: TooltipProps) =>
<TooltipPrimitive.Root // just render children if tooltip content is empty
delayDuration={50} content ? (
open={isOpen} <TooltipPrimitive.Root
defaultOpen={defaultOpen} delayDuration={50}
onOpenChange={onOpenChange} open={isOpen}
> defaultOpen={defaultOpen}
<TooltipPrimitive.Trigger asChild={asChild}>{children}</TooltipPrimitive.Trigger> onOpenChange={onOpenChange}
<TooltipPrimitive.Content >
side={position} <TooltipPrimitive.Trigger asChild={asChild}>{children}</TooltipPrimitive.Trigger>
align="center" <TooltipPrimitive.Content
sideOffset={5} side={position}
{...props} align="center"
className={twMerge( sideOffset={5}
`z-50 max-w-[15rem] select-none rounded-md border border-mineshaft-600 bg-mineshaft-800 py-2 px-4 text-sm font-light text-bunker-200 shadow-md {...props}
className={twMerge(
`z-50 max-w-[15rem] select-none rounded-md border border-mineshaft-600 bg-mineshaft-800 py-2 px-4 text-sm font-light text-bunker-200 shadow-md
data-[state=delayed-open]:data-[side=top]:animate-slideDownAndFade data-[state=delayed-open]:data-[side=top]:animate-slideDownAndFade
data-[state=delayed-open]:data-[side=right]:animate-slideLeftAndFade data-[state=delayed-open]:data-[side=right]:animate-slideLeftAndFade
data-[state=delayed-open]:data-[side=left]:animate-slideRightAndFade data-[state=delayed-open]:data-[side=left]:animate-slideRightAndFade
data-[state=delayed-open]:data-[side=bottom]:animate-slideUpAndFade data-[state=delayed-open]:data-[side=bottom]:animate-slideUpAndFade
`, `,
isDisabled && "!hidden", isDisabled && "!hidden",
center && "text-center", center && "text-center",
className className
)} )}
> >
{content} {content}
<TooltipPrimitive.Arrow width={11} height={5} className="fill-mineshaft-600" /> <TooltipPrimitive.Arrow width={11} height={5} className="fill-mineshaft-600" />
</TooltipPrimitive.Content> </TooltipPrimitive.Content>
</TooltipPrimitive.Root> </TooltipPrimitive.Root>
); ) : (
// eslint-disable-next-line react/jsx-no-useless-fragment
<>{children}</>
);
export const TooltipProvider = TooltipPrimitive.Provider; export const TooltipProvider = TooltipPrimitive.Provider;
@@ -1,3 +1,7 @@
export { ProjectPermissionProvider, useProjectPermission } from "./ProjectPermissionContext"; export { ProjectPermissionProvider, useProjectPermission } from "./ProjectPermissionContext";
export type { ProjectPermissionSet, TProjectPermission } from "./types"; export type { ProjectPermissionSet, TProjectPermission } from "./types";
export { ProjectPermissionActions, ProjectPermissionSub } from "./types"; export {
ProjectPermissionActions,
ProjectPermissionCmekActions,
ProjectPermissionSub
} from "./types";
@@ -7,6 +7,15 @@ export enum ProjectPermissionActions {
Delete = "delete" Delete = "delete"
} }
export enum ProjectPermissionCmekActions {
Read = "read",
Create = "create",
Edit = "edit",
Delete = "delete",
Encrypt = "encrypt",
Decrypt = "decrypt"
}
export enum PermissionConditionOperators { export enum PermissionConditionOperators {
$IN = "$in", $IN = "$in",
$ALL = "$all", $ALL = "$all",
@@ -55,7 +64,8 @@ export enum ProjectPermissionSub {
CertificateTemplates = "certificate-templates", CertificateTemplates = "certificate-templates",
PkiAlerts = "pki-alerts", PkiAlerts = "pki-alerts",
PkiCollections = "pki-collections", PkiCollections = "pki-collections",
Kms = "kms" Kms = "kms",
Cmek = "cmek"
} }
type SubjectFields = { type SubjectFields = {
@@ -97,6 +107,7 @@ export type ProjectPermissionSet =
| [ProjectPermissionActions.Delete, ProjectPermissionSub.Workspace] | [ProjectPermissionActions.Delete, ProjectPermissionSub.Workspace]
| [ProjectPermissionActions.Edit, ProjectPermissionSub.Workspace] | [ProjectPermissionActions.Edit, ProjectPermissionSub.Workspace]
| [ProjectPermissionActions.Read, ProjectPermissionSub.SecretRollback] | [ProjectPermissionActions.Read, ProjectPermissionSub.SecretRollback]
| [ProjectPermissionActions.Create, ProjectPermissionSub.SecretRollback]; | [ProjectPermissionActions.Create, ProjectPermissionSub.SecretRollback]
| [ProjectPermissionCmekActions, ProjectPermissionSub.Cmek];
export type TProjectPermission = MongoAbility<ProjectPermissionSet>; export type TProjectPermission = MongoAbility<ProjectPermissionSet>;
+1
View File
@@ -10,6 +10,7 @@ export {
export type { TProjectPermission } from "./ProjectPermissionContext"; export type { TProjectPermission } from "./ProjectPermissionContext";
export { export {
ProjectPermissionActions, ProjectPermissionActions,
ProjectPermissionCmekActions,
ProjectPermissionProvider, ProjectPermissionProvider,
ProjectPermissionSub, ProjectPermissionSub,
useProjectPermission useProjectPermission
+3
View File
@@ -0,0 +1,3 @@
export * from "./mutations";
export * from "./queries";
export * from "./types";
@@ -0,0 +1,90 @@
import { useMutation, useQueryClient } from "@tanstack/react-query";
import { encodeBase64 } from "tweetnacl-util";
import { apiRequest } from "@app/config/request";
import { cmekKeys } from "@app/hooks/api/cmeks/queries";
import {
TCmekDecrypt,
TCmekDecryptResponse,
TCmekEncrypt,
TCmekEncryptResponse,
TCreateCmek,
TDeleteCmek,
TUpdateCmek
} from "@app/hooks/api/cmeks/types";
export const useCreateCmek = () => {
const queryClient = useQueryClient();
return useMutation({
mutationFn: async (payload: TCreateCmek) => {
const { data } = await apiRequest.post("/api/v1/kms/keys", payload);
return data;
},
onSuccess: (_, { projectId }) => {
queryClient.invalidateQueries(cmekKeys.getCmeksByProjectId({ projectId }));
}
});
};
export const useUpdateCmek = () => {
const queryClient = useQueryClient();
return useMutation({
mutationFn: async ({ keyId, name, description, isDisabled }: TUpdateCmek) => {
const { data } = await apiRequest.patch(`/api/v1/kms/keys/${keyId}`, {
name,
description,
isDisabled
});
return data;
},
onSuccess: (_, { projectId }) => {
queryClient.invalidateQueries(cmekKeys.getCmeksByProjectId({ projectId }));
}
});
};
export const useDeleteCmek = () => {
const queryClient = useQueryClient();
return useMutation({
mutationFn: async ({ keyId }: TDeleteCmek) => {
const { data } = await apiRequest.delete(`/api/v1/kms/keys/${keyId}`);
return data;
},
onSuccess: (_, { projectId }) => {
queryClient.invalidateQueries(cmekKeys.getCmeksByProjectId({ projectId }));
}
});
};
export const useCmekEncrypt = () => {
return useMutation({
mutationFn: async ({ keyId, plaintext, isBase64Encoded }: TCmekEncrypt) => {
const { data } = await apiRequest.post<TCmekEncryptResponse>(
`/api/v1/kms/keys/${keyId}/encrypt`,
{
plaintext: isBase64Encoded ? plaintext : encodeBase64(Buffer.from(plaintext))
}
);
return data;
}
});
};
export const useCmekDecrypt = () => {
return useMutation({
mutationFn: async ({ keyId, ciphertext }: TCmekDecrypt) => {
const { data } = await apiRequest.post<TCmekDecryptResponse>(
`/api/v1/kms/keys/${keyId}/decrypt`,
{
ciphertext
}
);
return data;
}
});
};
+53
View File
@@ -0,0 +1,53 @@
import { useQuery, UseQueryOptions } from "@tanstack/react-query";
import { apiRequest } from "@app/config/request";
import { CmekOrderBy, TListProjectCmeksDTO, TProjectCmeksList } from "@app/hooks/api/cmeks/types";
import { OrderByDirection } from "@app/hooks/api/generic/types";
export const cmekKeys = {
all: ["cmek"] as const,
lists: () => [...cmekKeys.all, "list"] as const,
getCmeksByProjectId: ({ projectId, ...filters }: TListProjectCmeksDTO) =>
[...cmekKeys.lists(), projectId, filters] as const
};
export const useGetCmeksByProjectId = (
{
projectId,
offset = 0,
limit = 100,
orderBy = CmekOrderBy.Name,
orderDirection = OrderByDirection.ASC,
search = ""
}: TListProjectCmeksDTO,
options?: Omit<
UseQueryOptions<
TProjectCmeksList,
unknown,
TProjectCmeksList,
ReturnType<typeof cmekKeys.getCmeksByProjectId>
>,
"queryKey" | "queryFn"
>
) => {
return useQuery({
queryKey: cmekKeys.getCmeksByProjectId({
projectId,
offset,
limit,
orderBy,
orderDirection,
search
}),
queryFn: async () => {
const { data } = await apiRequest.get<TProjectCmeksList>("/api/v1/kms/keys", {
params: { projectId, offset, limit, search, orderBy, orderDirection }
});
return data;
},
enabled: Boolean(projectId) && (options?.enabled ?? true),
keepPreviousData: true,
...options
});
};
+58
View File
@@ -0,0 +1,58 @@
import { OrderByDirection } from "@app/hooks/api/generic/types";
export type TCmek = {
id: string;
name: string;
description?: string;
encryptionAlgorithm: EncryptionAlgorithm;
projectId: string;
isDisabled: boolean;
isReserved: boolean;
orgId: string;
version: number;
createdAt: string;
updatedAt: string;
};
type ProjectRef = { projectId: string };
type KeyRef = { keyId: string };
export type TCreateCmek = Pick<TCmek, "name" | "description" | "encryptionAlgorithm"> & ProjectRef;
export type TUpdateCmek = KeyRef &
Partial<Pick<TCmek, "name" | "description" | "isDisabled">> &
ProjectRef;
export type TDeleteCmek = KeyRef & ProjectRef;
export type TCmekEncrypt = KeyRef & { plaintext: string; isBase64Encoded?: boolean };
export type TCmekDecrypt = KeyRef & { ciphertext: string };
export type TProjectCmeksList = {
keys: TCmek[];
totalCount: number;
};
export type TListProjectCmeksDTO = {
projectId: string;
offset?: number;
limit?: number;
orderBy?: CmekOrderBy;
orderDirection?: OrderByDirection;
search?: string;
};
export type TCmekEncryptResponse = {
ciphertext: string;
};
export type TCmekDecryptResponse = {
plaintext: string;
};
export enum CmekOrderBy {
Name = "name"
}
export enum EncryptionAlgorithm {
AES_GCM_256 = "aes-256-gcm",
AES_GCM_128 = "aes-128-gcm"
}
+4 -4
View File
@@ -8,9 +8,9 @@ import { AddExternalKmsType, KmsType } from "./types";
export const useAddExternalKms = (orgId: string) => { export const useAddExternalKms = (orgId: string) => {
const queryClient = useQueryClient(); const queryClient = useQueryClient();
return useMutation({ return useMutation({
mutationFn: async ({ slug, description, provider }: AddExternalKmsType) => { mutationFn: async ({ name, description, provider }: AddExternalKmsType) => {
const { data } = await apiRequest.post("/api/v1/external-kms", { const { data } = await apiRequest.post("/api/v1/external-kms", {
slug, name,
description, description,
provider provider
}); });
@@ -28,14 +28,14 @@ export const useUpdateExternalKms = (orgId: string) => {
return useMutation({ return useMutation({
mutationFn: async ({ mutationFn: async ({
kmsId, kmsId,
slug, name,
description, description,
provider provider
}: { }: {
kmsId: string; kmsId: string;
} & AddExternalKmsType) => { } & AddExternalKmsType) => {
const { data } = await apiRequest.patch(`/api/v1/external-kms/${kmsId}`, { const { data } = await apiRequest.patch(`/api/v1/external-kms/${kmsId}`, {
slug, name,
description, description,
provider provider
}); });
+1 -1
View File
@@ -46,7 +46,7 @@ export const useGetActiveProjectKms = (projectId: string) => {
} = await apiRequest.get<{ } = await apiRequest.get<{
secretManagerKmsKey: { secretManagerKmsKey: {
id: string; id: string;
slug: string; name: string;
isExternal: string; isExternal: string;
}; };
}>(`/api/v1/workspace/${projectId}/kms`); }>(`/api/v1/workspace/${projectId}/kms`);
+3 -3
View File
@@ -5,7 +5,7 @@ export type Kms = {
id: string; id: string;
description: string; description: string;
orgId: string; orgId: string;
slug: string; name: string;
external: { external: {
id: string; id: string;
status: string; status: string;
@@ -21,7 +21,7 @@ export type KmsListEntry = {
isDisabled: boolean; isDisabled: boolean;
createdAt: string; createdAt: string;
updatedAt: string; updatedAt: string;
slug: string; name: string;
externalKms: { externalKms: {
provider: string; provider: string;
status: string; status: string;
@@ -88,7 +88,7 @@ export const ExternalKmsInputSchema = z.discriminatedUnion("type", [
]); ]);
export const AddExternalKmsSchema = z.object({ export const AddExternalKmsSchema = z.object({
slug: z name: z
.string() .string()
.trim() .trim()
.min(1) .min(1)
+2 -2
View File
@@ -122,6 +122,6 @@ export type TOrgIdentitiesList = {
}; };
export enum OrgIdentityOrderBy { export enum OrgIdentityOrderBy {
Name = "name", Name = "name"
Role = "role" // Role = "role"
} }
+1
View File
@@ -1,5 +1,6 @@
export { useDebounce } from "./useDebounce"; export { useDebounce } from "./useDebounce";
export { useLeaveConfirm } from "./useLeaveConfirm"; export { useLeaveConfirm } from "./useLeaveConfirm";
export { usePagination } from "./usePagination";
export { usePersistentState } from "./usePersistentState"; export { usePersistentState } from "./usePersistentState";
export { usePopUp } from "./usePopUp"; export { usePopUp } from "./usePopUp";
export { useSyntaxHighlight } from "./useSyntaxHighlight"; export { useSyntaxHighlight } from "./useSyntaxHighlight";
+31
View File
@@ -0,0 +1,31 @@
import { useState } from "react";
import { OrderByDirection } from "@app/hooks/api/generic/types";
import { useDebounce } from "@app/hooks/useDebounce";
export const usePagination = <T extends string>(initialOrderBy: T) => {
const [page, setPage] = useState(1);
const [perPage, setPerPage] = useState(20);
const [orderDirection, setOrderDirection] = useState(OrderByDirection.ASC);
const [orderBy, setOrderBy] = useState<T>(initialOrderBy);
const [search, setSearch] = useState("");
const [debouncedSearch] = useDebounce(search);
const offset = (page - 1) * perPage;
return {
offset,
limit: perPage,
page,
setPage,
perPage,
setPerPage,
orderDirection,
setOrderDirection,
debouncedSearch,
search,
setSearch,
orderBy,
setOrderBy
};
};
+11 -1
View File
@@ -630,6 +630,16 @@ export const AppLayout = ({ children }: LayoutProps) => {
</MenuItem> </MenuItem>
</a> </a>
</Link> </Link>
<Link href={`/project/${currentWorkspace?.id}/kms`} passHref>
<a>
<MenuItem
isSelected={router.asPath === `/project/${currentWorkspace?.id}/kms`}
icon="system-outline-90-lock-closed"
>
Key Management
</MenuItem>
</a>
</Link>
<Link href={`/project/${currentWorkspace?.id}/members`} passHref> <Link href={`/project/${currentWorkspace?.id}/members`} passHref>
<a> <a>
<MenuItem <MenuItem
@@ -942,7 +952,7 @@ export const AppLayout = ({ children }: LayoutProps) => {
</SelectItem> </SelectItem>
{externalKmsList?.map((kms) => ( {externalKmsList?.map((kms) => (
<SelectItem value={kms.id} key={`kms-${kms.id}`}> <SelectItem value={kms.id} key={`kms-${kms.id}`}>
{kms.slug} {kms.name}
</SelectItem> </SelectItem>
))} ))}
</Select> </Select>
@@ -1101,7 +1101,7 @@ const OrganizationPage = () => {
</SelectItem> </SelectItem>
{externalKmsList?.map((kms) => ( {externalKmsList?.map((kms) => (
<SelectItem value={kms.id} key={`kms-${kms.id}`}> <SelectItem value={kms.id} key={`kms-${kms.id}`}>
{kms.slug} {kms.name}
</SelectItem> </SelectItem>
))} ))}
</Select> </Select>
@@ -0,0 +1,23 @@
import { useTranslation } from "react-i18next";
import Head from "next/head";
import { KmsPage } from "@app/views/Project/KmsPage";
const KMS = () => {
const { t } = useTranslation();
return (
<div className="h-full bg-bunker-800">
<Head>
<title>{t("common.head-title", { title: "KMS" })}</title>
<link rel="icon" href="/infisical.ico" />
<meta property="og:image" content="/images/message.png" />
</Head>
<KmsPage />
</div>
);
};
export default KMS;
KMS.requireAuth = true;
@@ -1,4 +1,4 @@
import { useEffect, useState } from "react"; import { useEffect } from "react";
import { useRouter } from "next/router"; import { useRouter } from "next/router";
import { import {
faArrowDown, faArrowDown,
@@ -34,7 +34,7 @@ import {
Tr Tr
} from "@app/components/v2"; } from "@app/components/v2";
import { OrgPermissionActions, OrgPermissionSubjects, useOrganization } from "@app/context"; import { OrgPermissionActions, OrgPermissionSubjects, useOrganization } from "@app/context";
import { useDebounce } from "@app/hooks"; import { usePagination } from "@app/hooks";
import { useGetIdentityMembershipOrgs, useGetOrgRoles, useUpdateIdentity } from "@app/hooks/api"; import { useGetIdentityMembershipOrgs, useGetOrgRoles, useUpdateIdentity } from "@app/hooks/api";
import { OrderByDirection } from "@app/hooks/api/generic/types"; import { OrderByDirection } from "@app/hooks/api/generic/types";
import { OrgIdentityOrderBy } from "@app/hooks/api/organization/types"; import { OrgIdentityOrderBy } from "@app/hooks/api/organization/types";
@@ -50,28 +50,35 @@ type Props = {
) => void; ) => void;
}; };
const INIT_PER_PAGE = 20;
export const IdentityTable = ({ handlePopUpOpen }: Props) => { export const IdentityTable = ({ handlePopUpOpen }: Props) => {
const router = useRouter(); const router = useRouter();
const { currentOrg } = useOrganization(); const { currentOrg } = useOrganization();
const [page, setPage] = useState(1);
const [perPage, setPerPage] = useState(INIT_PER_PAGE); const {
const [orderDirection, setOrderDirection] = useState(OrderByDirection.ASC); offset,
const [orderBy, setOrderBy] = useState(OrgIdentityOrderBy.Name); limit,
const [search, setSearch] = useState(""); orderBy,
const [debouncedSearch] = useDebounce(search); setOrderBy,
orderDirection,
setOrderDirection,
search,
debouncedSearch,
setPage,
setSearch,
perPage,
page,
setPerPage
} = usePagination<OrgIdentityOrderBy>(OrgIdentityOrderBy.Name);
const organizationId = currentOrg?.id || ""; const organizationId = currentOrg?.id || "";
const { mutateAsync: updateMutateAsync } = useUpdateIdentity(); const { mutateAsync: updateMutateAsync } = useUpdateIdentity();
const offset = (page - 1) * perPage;
const { data, isLoading, isFetching } = useGetIdentityMembershipOrgs( const { data, isLoading, isFetching } = useGetIdentityMembershipOrgs(
{ {
organizationId, organizationId,
offset, offset,
limit: perPage, limit,
orderDirection, orderDirection,
orderBy, orderBy,
search: debouncedSearch search: debouncedSearch
@@ -79,10 +86,11 @@ export const IdentityTable = ({ handlePopUpOpen }: Props) => {
{ keepPreviousData: true } { keepPreviousData: true }
); );
const { totalCount = 0 } = data ?? {};
useEffect(() => { useEffect(() => {
// reset page if no longer valid // reset page if no longer valid
if (data && data.totalCount < offset) setPage(1); if (totalCount <= offset) setPage(1);
}, [data?.totalCount]); }, [totalCount]);
const { data: roles } = useGetOrgRoles(organizationId); const { data: roles } = useGetOrgRoles(organizationId);
@@ -155,7 +163,8 @@ export const IdentityTable = ({ handlePopUpOpen }: Props) => {
</IconButton> </IconButton>
</div> </div>
</Th> </Th>
<Th> <Th>Role</Th>
{/* <Th>
<div className="flex items-center"> <div className="flex items-center">
Role Role
<IconButton <IconButton
@@ -174,7 +183,7 @@ export const IdentityTable = ({ handlePopUpOpen }: Props) => {
/> />
</IconButton> </IconButton>
</div> </div>
</Th> </Th> */}
<Th className="w-16">{isFetching ? <Spinner size="xs" /> : null}</Th> <Th className="w-16">{isFetching ? <Spinner size="xs" /> : null}</Th>
</Tr> </Tr>
</THead> </THead>
@@ -277,9 +286,9 @@ export const IdentityTable = ({ handlePopUpOpen }: Props) => {
})} })}
</TBody> </TBody>
</Table> </Table>
{!isLoading && data && data.totalCount > 0 && ( {!isLoading && data && totalCount > 0 && (
<Pagination <Pagination
count={data.totalCount} count={totalCount}
page={page} page={page}
perPage={perPage} perPage={perPage}
onChangePage={(newPage) => setPage(newPage)} onChangePage={(newPage) => setPage(newPage)}
@@ -2,7 +2,7 @@ import { Tab, TabList, TabPanel, Tabs } from "@app/components/v2";
import { ProjectPermissionActions, ProjectPermissionSub } from "@app/context"; import { ProjectPermissionActions, ProjectPermissionSub } from "@app/context";
import { withProjectPermission } from "@app/hoc"; import { withProjectPermission } from "@app/hoc";
import { CaTab, CertificatesTab,PkiAlertsTab } from "./components"; import { CaTab, CertificatesTab, PkiAlertsTab } from "./components";
enum TabSections { enum TabSections {
Ca = "certificate-authorities", Ca = "certificate-authorities",
@@ -36,5 +36,5 @@ export const CertificatesPage = withProjectPermission(
</div> </div>
); );
}, },
{ action: ProjectPermissionActions.Read, subject: ProjectPermissionSub.AuditLogs } { action: ProjectPermissionActions.Read, subject: ProjectPermissionSub.Certificates }
); );
@@ -0,0 +1,169 @@
import { useEffect, useState } from "react";
import { useForm } from "react-hook-form";
import { faCheckCircle, faCopy, faInfoCircle, faLockOpen } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { zodResolver } from "@hookform/resolvers/zod";
import { decodeBase64 } from "tweetnacl-util";
import { z } from "zod";
import { createNotification } from "@app/components/notifications";
import {
Button,
FormControl,
Modal,
ModalClose,
ModalContent,
Switch,
TextArea,
Tooltip
} from "@app/components/v2";
import { useTimedReset } from "@app/hooks";
import { TCmek, useCmekDecrypt } from "@app/hooks/api/cmeks";
const formSchema = z.object({
ciphertext: z.string()
});
export type FormData = z.infer<typeof formSchema>;
type Props = {
isOpen: boolean;
onOpenChange: (isOpen: boolean) => void;
cmek: TCmek;
};
type FormProps = Pick<Props, "cmek">;
const DecryptForm = ({ cmek }: FormProps) => {
const cmekDecrypt = useCmekDecrypt();
const [shouldDecode, setShouldDecode] = useState(false);
const [plaintext, setPlaintext] = useState("");
const {
handleSubmit,
register,
formState: { isSubmitting, errors }
} = useForm<FormData>({
resolver: zodResolver(formSchema)
});
const [copyCiphertext, isCopyingCiphertext, setCopyCipherText] = useTimedReset<string>({
initialState: "Copy to Clipboard"
});
const handleDecryptData = async (formData: FormData) => {
try {
const data = await cmekDecrypt.mutateAsync({ ...formData, keyId: cmek.id });
createNotification({
text: "Successfully decrypted data",
type: "success"
});
setPlaintext(
shouldDecode ? Buffer.from(decodeBase64(data.plaintext)).toString("utf8") : data.plaintext
);
} catch (err) {
console.error(err);
createNotification({
text: "Failed to decrypt data",
type: "error"
});
}
};
useEffect(() => {
const text = cmekDecrypt.data?.plaintext;
if (!text) return;
setPlaintext(shouldDecode ? Buffer.from(decodeBase64(text)).toString("utf8") : text);
}, [shouldDecode]);
const handleCopyToClipboard = () => {
navigator.clipboard.writeText(plaintext ?? "");
setCopyCipherText("Copied to Clipboard");
};
return (
<form onSubmit={handleSubmit(handleDecryptData)}>
{plaintext ? (
<FormControl label="Decrypted Data (plaintext)">
<TextArea
className="max-h-[20rem] min-h-[10rem] min-w-full max-w-full"
isDisabled
value={plaintext}
/>
</FormControl>
) : (
<FormControl
label="Encrypted Data (ciphertext)"
errorText={errors.ciphertext?.message}
isError={Boolean(errors.ciphertext)}
>
<TextArea
{...register("ciphertext")}
className="max-h-[20rem] min-h-[10rem] min-w-full max-w-full"
/>
</FormControl>
)}
<Switch
id="decode-base-64"
isChecked={shouldDecode}
onCheckedChange={setShouldDecode}
containerClassName="mb-6 ml-0.5 -mt-2.5"
>
Decode Base64{" "}
<Tooltip content="Toggle this switch on if your data was originally plain text.">
<FontAwesomeIcon icon={faInfoCircle} className=" text-mineshaft-400" />
</Tooltip>
</Switch>
<div className="flex items-center">
<Button
className={`mr-4 ${plaintext ? "w-44" : ""}`}
size="sm"
leftIcon={
// eslint-disable-next-line no-nested-ternary
plaintext ? (
isCopyingCiphertext ? (
<FontAwesomeIcon icon={faCheckCircle} />
) : (
<FontAwesomeIcon icon={faCopy} />
)
) : (
<FontAwesomeIcon icon={faLockOpen} />
)
}
onClick={plaintext ? handleCopyToClipboard : undefined}
type={plaintext ? "button" : "submit"}
isLoading={isSubmitting}
isDisabled={isSubmitting}
>
{plaintext ? copyCiphertext : "Decrypt"}
</Button>
<ModalClose asChild>
<Button colorSchema="secondary" variant="plain">
{plaintext ? "Close" : "Cancel"}
</Button>
</ModalClose>
</div>
</form>
);
};
export const CmekDecryptModal = ({ isOpen, onOpenChange, cmek }: Props) => {
return (
<Modal isOpen={isOpen} onOpenChange={onOpenChange}>
<ModalContent
subTitle={
<>
Decrypt ciphertext using <span className="font-bold">{cmek?.name}</span>. Returns Base64
encoded plaintext.
</>
}
title="Decrypt Data"
>
<DecryptForm cmek={cmek} />
</ModalContent>
</Modal>
);
};
@@ -0,0 +1,169 @@
import { Controller, useForm } from "react-hook-form";
import { faCheckCircle, faCopy, faInfoCircle, faLock } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { zodResolver } from "@hookform/resolvers/zod";
import { z } from "zod";
import { createNotification } from "@app/components/notifications";
import {
Button,
FormControl,
Modal,
ModalClose,
ModalContent,
Switch,
TextArea,
Tooltip
} from "@app/components/v2";
import { useTimedReset } from "@app/hooks";
import { TCmek, useCmekEncrypt } from "@app/hooks/api/cmeks";
const formSchema = z.object({
plaintext: z.string(),
isBase64Encoded: z.boolean()
});
export type FormData = z.infer<typeof formSchema>;
type Props = {
isOpen: boolean;
onOpenChange: (isOpen: boolean) => void;
cmek: TCmek;
};
type FormProps = Pick<Props, "cmek">;
const EncryptForm = ({ cmek }: FormProps) => {
const cmekEncrypt = useCmekEncrypt();
const {
handleSubmit,
register,
control,
formState: { isSubmitting, errors }
} = useForm<FormData>({
resolver: zodResolver(formSchema),
defaultValues: {
isBase64Encoded: false
}
});
const [copyCiphertext, isCopyingCiphertext, setCopyCipherText] = useTimedReset<string>({
initialState: "Copy to Clipboard"
});
const handleEncryptData = async (formData: FormData) => {
try {
await cmekEncrypt.mutateAsync({ ...formData, keyId: cmek.id });
createNotification({
text: "Successfully encrypted data",
type: "success"
});
} catch (err) {
console.error(err);
createNotification({
text: "Failed to encrypt data",
type: "error"
});
}
};
const ciphertext = cmekEncrypt.data?.ciphertext;
const handleCopyToClipboard = () => {
navigator.clipboard.writeText(ciphertext ?? "");
setCopyCipherText("Copied to Clipboard");
};
return (
<form onSubmit={handleSubmit(handleEncryptData)}>
{ciphertext ? (
<FormControl label="Encrypted Data (Ciphertext)">
<TextArea
className="max-h-[20rem] min-h-[10rem] min-w-full max-w-full"
isDisabled
value={cmekEncrypt.data?.ciphertext}
/>
</FormControl>
) : (
<>
<FormControl
label="Data (Plaintext)"
errorText={errors.plaintext?.message}
isError={Boolean(errors.plaintext)}
>
<TextArea
{...register("plaintext")}
className="max-h-[20rem] min-h-[10rem] min-w-full max-w-full"
/>
</FormControl>
<Controller
control={control}
name="isBase64Encoded"
render={({ field: { onChange, value } }) => (
<Switch
id="encode-base-64"
isChecked={value}
onCheckedChange={onChange}
containerClassName="mb-6 ml-0.5 -mt-2.5"
>
Data is Base64 encoded{" "}
<Tooltip content="Toggle this switch on if your data is already Base64 encoded to avoid redundant encoding.">
<FontAwesomeIcon icon={faInfoCircle} className=" text-mineshaft-400" />
</Tooltip>
</Switch>
)}
/>
</>
)}
<div className="flex items-center">
<Button
className={`mr-4 ${ciphertext ? "w-44" : ""}`}
size="sm"
leftIcon={
// eslint-disable-next-line no-nested-ternary
ciphertext ? (
isCopyingCiphertext ? (
<FontAwesomeIcon icon={faCheckCircle} />
) : (
<FontAwesomeIcon icon={faCopy} />
)
) : (
<FontAwesomeIcon icon={faLock} />
)
}
onClick={ciphertext ? handleCopyToClipboard : undefined}
type={ciphertext ? "button" : "submit"}
isLoading={isSubmitting}
isDisabled={isSubmitting}
>
{ciphertext ? copyCiphertext : "Encrypt"}
</Button>
<ModalClose asChild>
<Button colorSchema="secondary" variant="plain">
{ciphertext ? "Close" : "Cancel"}
</Button>
</ModalClose>
</div>
</form>
);
};
export const CmekEncryptModal = ({ isOpen, onOpenChange, cmek }: Props) => {
return (
<Modal isOpen={isOpen} onOpenChange={onOpenChange}>
<ModalContent
title="Encrypt Data"
subTitle={
<>
Encrypt data using <span className="font-bold">{cmek?.name}</span>. Returns Base64
encoded ciphertext.
</>
}
>
<EncryptForm cmek={cmek} />
</ModalContent>
</Modal>
);
};
@@ -0,0 +1,158 @@
import { Controller, useForm } from "react-hook-form";
import { zodResolver } from "@hookform/resolvers/zod";
import slugify from "@sindresorhus/slugify";
import { z } from "zod";
import { createNotification } from "@app/components/notifications";
import {
Button,
FormControl,
Input,
Modal,
ModalClose,
ModalContent,
Select,
SelectItem,
TextArea
} from "@app/components/v2";
import { useWorkspace } from "@app/context";
import { EncryptionAlgorithm, TCmek, useCreateCmek, useUpdateCmek } from "@app/hooks/api/cmeks";
const formSchema = z.object({
name: z
.string()
.min(1)
.toLowerCase()
.max(32)
.refine((v) => slugify(v) === v, {
message: "Name must be in slug format"
}),
description: z.string().max(500).optional(),
encryptionAlgorithm: z.nativeEnum(EncryptionAlgorithm)
});
export type FormData = z.infer<typeof formSchema>;
type Props = {
isOpen: boolean;
onOpenChange: (isOpen: boolean) => void;
cmek?: TCmek | null;
};
type FormProps = Pick<Props, "cmek"> & {
onComplete: () => void;
};
const CmekForm = ({ onComplete, cmek }: FormProps) => {
const createCmek = useCreateCmek();
const updateCmek = useUpdateCmek();
const { currentWorkspace } = useWorkspace();
const projectId = currentWorkspace?.id!;
const isUpdate = !!cmek;
const {
control,
handleSubmit,
register,
formState: { isSubmitting, errors }
} = useForm<FormData>({
resolver: zodResolver(formSchema),
defaultValues: {
name: cmek?.name,
description: cmek?.description,
encryptionAlgorithm: EncryptionAlgorithm.AES_GCM_256
}
});
const handleCreateCmek = async ({ encryptionAlgorithm, name, description }: FormData) => {
const mutation = isUpdate
? updateCmek.mutateAsync({ keyId: cmek.id, projectId, name, description })
: createCmek.mutateAsync({
projectId,
encryptionAlgorithm,
name,
description
});
try {
await mutation;
createNotification({
text: `Successfully ${isUpdate ? "updated" : "added"} key`,
type: "success"
});
onComplete();
} catch (err) {
console.error(err);
createNotification({
text: `Failed to ${isUpdate ? "update" : "add"} key`,
type: "error"
});
}
};
return (
<form onSubmit={handleSubmit(handleCreateCmek)}>
<FormControl
helperText="Name must be slug-friendly"
errorText={errors.name?.message}
isError={Boolean(errors.name?.message)}
label="Name"
>
<Input autoFocus placeholder="my-secret-key" {...register("name")} />
</FormControl>
{!isUpdate && (
<Controller
control={control}
name="encryptionAlgorithm"
render={({ field: { onChange, ...field }, fieldState: { error } }) => (
<FormControl label="Algorithm" errorText={error?.message} isError={Boolean(error)}>
<Select defaultValue={field.value} onValueChange={onChange} className="w-full">
{Object.entries(EncryptionAlgorithm)?.map(([key, value]) => (
<SelectItem value={value} key={`source-environment-${key}`}>
{key.replaceAll("_", "-")}
</SelectItem>
))}
</Select>
</FormControl>
)}
/>
)}
<FormControl
label="Description (optional)"
errorText={errors.description?.message}
isError={Boolean(errors.description?.message)}
>
<TextArea
className="max-h-[20rem] min-h-[10rem] min-w-full max-w-full"
{...register("description")}
/>
</FormControl>
<div className="flex items-center">
<Button
className="mr-4"
size="sm"
type="submit"
isLoading={isSubmitting}
isDisabled={isSubmitting}
>
{isUpdate ? "Update" : "Add"} Key
</Button>
<ModalClose asChild>
<Button colorSchema="secondary" variant="plain">
Cancel
</Button>
</ModalClose>
</div>
</form>
);
};
export const CmekModal = ({ isOpen, onOpenChange, cmek }: Props) => {
return (
<Modal isOpen={isOpen} onOpenChange={onOpenChange}>
<ModalContent title={`${cmek ? "Update" : "Add"} Key`}>
<CmekForm onComplete={() => onOpenChange(false)} cmek={cmek} />
</ModalContent>
</Modal>
);
};
@@ -0,0 +1,427 @@
import { useEffect } from "react";
import Link from "next/link";
import {
faArrowDown,
faArrowUp,
faArrowUpRightFromSquare,
faCancel,
faCheckCircle,
faEdit,
faEllipsis,
faInfoCircle,
faKey,
faLock,
faLockOpen,
faMagnifyingGlass,
faPlus,
faTrash
} from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { motion } from "framer-motion";
import { createNotification } from "@app/components/notifications";
import { ProjectPermissionCan } from "@app/components/permissions";
import {
Badge,
Button,
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger,
EmptyState,
IconButton,
Input,
Pagination,
Spinner,
Table,
TableContainer,
TableSkeleton,
TBody,
Td,
Th,
THead,
Tooltip,
Tr
} from "@app/components/v2";
import { BadgeProps } from "@app/components/v2/Badge/Badge";
import {
ProjectPermissionActions,
ProjectPermissionCmekActions,
ProjectPermissionSub,
useProjectPermission,
useWorkspace
} from "@app/context";
import { usePagination, usePopUp } from "@app/hooks";
import { useGetCmeksByProjectId, useUpdateCmek } from "@app/hooks/api/cmeks";
import { CmekOrderBy, TCmek } from "@app/hooks/api/cmeks/types";
import { OrderByDirection } from "@app/hooks/api/generic/types";
import { CmekDecryptModal } from "./CmekDecryptModal";
import { CmekEncryptModal } from "./CmekEncryptModal";
import { CmekModal } from "./CmekModal";
import { DeleteCmekModal } from "./DeleteCmekModal";
const getStatusBadgeProps = (
isDisabled: boolean
): { variant: BadgeProps["variant"]; label: string } => {
if (isDisabled) {
return {
variant: "danger",
label: "Disabled"
};
}
return {
variant: "success",
label: "Active"
};
};
export const CmekTable = () => {
const { currentWorkspace } = useWorkspace();
const { permission } = useProjectPermission();
const projectId = currentWorkspace?.id ?? "";
const {
offset,
limit,
orderBy,
orderDirection,
setOrderDirection,
search,
debouncedSearch,
setPage,
setSearch,
perPage,
page,
setPerPage
} = usePagination(CmekOrderBy.Name);
const { data, isLoading, isFetching } = useGetCmeksByProjectId({
projectId,
offset,
limit,
search: debouncedSearch,
orderBy,
orderDirection
});
const { keys = [], totalCount = 0 } = data ?? {};
useEffect(() => {
// reset page if no longer valid
if (totalCount <= offset) setPage(1);
}, [totalCount]);
const { popUp, handlePopUpOpen, handlePopUpToggle } = usePopUp([
"upsertKey",
"deleteKey",
"encryptData",
"decryptData"
] as const);
const handleSort = () => {
setOrderDirection((prev) =>
prev === OrderByDirection.ASC ? OrderByDirection.DESC : OrderByDirection.ASC
);
};
const updateCmek = useUpdateCmek();
const handleDisableCmek = async ({ id: keyId, isDisabled }: TCmek) => {
try {
await updateCmek.mutateAsync({
keyId,
projectId,
isDisabled: !isDisabled
});
createNotification({
text: `Key successfully ${isDisabled ? "enabled" : "disabled"}`,
type: "success"
});
} catch (err) {
console.error(err);
const error = err as any;
const text =
error?.response?.data?.message ?? `Failed to ${isDisabled ? "enable" : "disable"} key`;
createNotification({
text,
type: "error"
});
}
};
const cannotEditKey = permission.cannot(
ProjectPermissionCmekActions.Edit,
ProjectPermissionSub.Cmek
);
const cannotDeleteKey = permission.cannot(
ProjectPermissionCmekActions.Delete,
ProjectPermissionSub.Cmek
);
const cannotEncryptData = permission.cannot(
ProjectPermissionCmekActions.Encrypt,
ProjectPermissionSub.Cmek
);
const cannotDecryptData = permission.cannot(
ProjectPermissionCmekActions.Decrypt,
ProjectPermissionSub.Cmek
);
return (
<motion.div
key="kms-keys-tab"
transition={{ duration: 0.15 }}
initial={{ opacity: 0, translateX: 30 }}
animate={{ opacity: 1, translateX: 0 }}
exit={{ opacity: 0, translateX: 30 }}
>
<div className="mb-6 rounded-lg border border-mineshaft-600 bg-mineshaft-900 p-4">
<div className="mb-4 flex items-center justify-between">
<p className="whitespace-nowrap text-xl font-semibold text-mineshaft-100">Keys</p>
<div className="flex w-full justify-end pr-4">
<Link href="https://infisical.com/docs/documentation/platform/kms/infisical-kms">
<span className="w-max cursor-pointer rounded-md border border-mineshaft-500 bg-mineshaft-600 px-4 py-2 text-mineshaft-200 duration-200 hover:border-primary/40 hover:bg-primary/10 hover:text-white">
Documentation{" "}
<FontAwesomeIcon
icon={faArrowUpRightFromSquare}
className="mb-[0.06rem] ml-1 text-xs"
/>
</span>
</Link>
</div>
<ProjectPermissionCan I={ProjectPermissionActions.Create} a={ProjectPermissionSub.Cmek}>
{(isAllowed) => (
<Button
colorSchema="primary"
type="submit"
leftIcon={<FontAwesomeIcon icon={faPlus} />}
onClick={() => handlePopUpOpen("upsertKey", null)}
isDisabled={!isAllowed}
>
Add Key
</Button>
)}
</ProjectPermissionCan>
</div>
<Input
containerClassName="mb-4"
value={search}
onChange={(e) => setSearch(e.target.value)}
leftIcon={<FontAwesomeIcon icon={faMagnifyingGlass} />}
placeholder="Search keys by name..."
/>
<TableContainer>
<Table>
<THead>
<Tr className="h-14">
<Th className="w-1/3">
<div className="flex items-center">
Name
<IconButton
variant="plain"
className="ml-2"
ariaLabel="sort"
onClick={handleSort}
>
<FontAwesomeIcon
icon={orderDirection === OrderByDirection.DESC ? faArrowUp : faArrowDown}
/>
</IconButton>
</div>
</Th>
<Th>Algorithm</Th>
<Th>Status</Th>
<Th>Version</Th>
<Th className="w-16">{isFetching ? <Spinner size="xs" /> : null}</Th>
</Tr>
</THead>
<TBody>
{isLoading && <TableSkeleton columns={4} innerKey="project-keys" />}
{!isLoading &&
keys.length > 0 &&
keys.map((cmek) => {
const { name, id, version, description, encryptionAlgorithm, isDisabled } = cmek;
const { variant, label } = getStatusBadgeProps(isDisabled);
return (
<Tr className="group h-10 hover:bg-mineshaft-700" key={`st-v3-${id}`}>
<Td>
<div className="flex items-center gap-2">
{name}
{description && (
<Tooltip content={description}>
<FontAwesomeIcon
icon={faInfoCircle}
className=" text-mineshaft-400"
/>
</Tooltip>
)}
</div>
</Td>
<Td className="uppercase">{encryptionAlgorithm}</Td>
<Td>
<Badge variant={variant}>{label}</Badge>
</Td>
<Td>{version}</Td>
<Td className="flex justify-end">
<DropdownMenu>
<DropdownMenuTrigger asChild>
<IconButton
variant="plain"
colorSchema="primary"
className="ml-4 p-0 data-[state=open]:text-primary-400"
ariaLabel="More options"
>
<FontAwesomeIcon size="lg" icon={faEllipsis} />
</IconButton>
</DropdownMenuTrigger>
<DropdownMenuContent className="min-w-[160px]">
<Tooltip
content={
// eslint-disable-next-line no-nested-ternary
cannotEncryptData
? "Access Restricted"
: isDisabled
? "Key Disabled"
: ""
}
position="left"
>
<div>
<DropdownMenuItem
onClick={() => handlePopUpOpen("encryptData", cmek)}
icon={<FontAwesomeIcon icon={faLock} />}
iconPos="left"
isDisabled={cannotEncryptData || isDisabled}
>
Encrypt Data
</DropdownMenuItem>
</div>
</Tooltip>
<Tooltip
content={
// eslint-disable-next-line no-nested-ternary
cannotDecryptData
? "Access Restricted"
: isDisabled
? "Key Disabled"
: ""
}
position="left"
>
<div>
<DropdownMenuItem
onClick={() => handlePopUpOpen("decryptData", cmek)}
icon={<FontAwesomeIcon icon={faLockOpen} />}
iconPos="left"
isDisabled={cannotDecryptData || isDisabled}
>
Decrypt Data
</DropdownMenuItem>
</div>
</Tooltip>
<Tooltip
content={cannotEditKey ? "Access Restricted" : ""}
position="left"
>
<div>
<DropdownMenuItem
onClick={() => handlePopUpOpen("upsertKey", cmek)}
icon={<FontAwesomeIcon icon={faEdit} />}
iconPos="left"
isDisabled={cannotEditKey}
>
Edit Key
</DropdownMenuItem>
</div>
</Tooltip>
<Tooltip
content={cannotEditKey ? "Access Restricted" : ""}
position="left"
>
<div>
<DropdownMenuItem
onClick={() => handleDisableCmek(cmek)}
icon={
<FontAwesomeIcon icon={isDisabled ? faCheckCircle : faCancel} />
}
iconPos="left"
isDisabled={cannotEditKey}
>
{isDisabled ? "Enable" : "Disable"} Key
</DropdownMenuItem>
</div>
</Tooltip>
<Tooltip
content={cannotDeleteKey ? "Access Restricted" : ""}
position="left"
>
<div>
<DropdownMenuItem
onClick={() => handlePopUpOpen("deleteKey", cmek)}
icon={<FontAwesomeIcon icon={faTrash} />}
iconPos="left"
isDisabled={cannotDeleteKey}
>
Delete Key
</DropdownMenuItem>
</div>
</Tooltip>
</DropdownMenuContent>
</DropdownMenu>
</Td>
</Tr>
);
})}
</TBody>
</Table>
{!isLoading && totalCount > 0 && (
<Pagination
count={totalCount}
page={page}
perPage={perPage}
onChangePage={(newPage) => setPage(newPage)}
onChangePerPage={(newPerPage) => setPerPage(newPerPage)}
/>
)}
{!isLoading && keys.length === 0 && (
<EmptyState
title={
debouncedSearch.trim().length > 0
? "No keys match search filter"
: "No keys have been added to this project"
}
icon={faKey}
/>
)}
</TableContainer>
<DeleteCmekModal
isOpen={popUp.deleteKey.isOpen}
onOpenChange={(isOpen) => handlePopUpToggle("deleteKey", isOpen)}
cmek={popUp.deleteKey.data as TCmek}
/>
<CmekModal
isOpen={popUp.upsertKey.isOpen}
onOpenChange={(isOpen) => handlePopUpToggle("upsertKey", isOpen)}
cmek={popUp.upsertKey.data as TCmek | null}
/>
<CmekEncryptModal
isOpen={popUp.encryptData.isOpen}
onOpenChange={(isOpen) => handlePopUpToggle("encryptData", isOpen)}
cmek={popUp.encryptData.data as TCmek}
/>
<CmekDecryptModal
isOpen={popUp.decryptData.isOpen}
onOpenChange={(isOpen) => handlePopUpToggle("decryptData", isOpen)}
cmek={popUp.decryptData.data as TCmek}
/>
</div>
</motion.div>
);
};
@@ -0,0 +1,52 @@
import { createNotification } from "@app/components/notifications";
import { DeleteActionModal } from "@app/components/v2";
import { TCmek, useDeleteCmek } from "@app/hooks/api/cmeks";
type Props = {
cmek: TCmek;
isOpen: boolean;
onOpenChange: (isOpen: boolean) => void;
};
export const DeleteCmekModal = ({ isOpen, onOpenChange, cmek }: Props) => {
const deleteCmek = useDeleteCmek();
if (!cmek) return null;
const { id: keyId, projectId, name } = cmek;
const handleDeleteCmek = async () => {
try {
await deleteCmek.mutateAsync({
keyId,
projectId
});
createNotification({
text: "Key successfully deleted",
type: "success"
});
onOpenChange(false);
} catch (err) {
console.error(err);
const error = err as any;
const text = error?.response?.data?.message ?? "Failed to delete key";
createNotification({
text,
type: "error"
});
}
};
return (
<DeleteActionModal
isOpen={isOpen}
title={`Are you sure want to delete ${name}?`}
onChange={onOpenChange}
deleteKey="confirm"
onDeleteApproved={handleDeleteCmek}
/>
);
};
@@ -0,0 +1 @@
export * from "./CmekTable";
@@ -0,0 +1,24 @@
import { ProjectPermissionCmekActions, ProjectPermissionSub } from "@app/context";
import { withProjectPermission } from "@app/hoc";
import { CmekTable } from "./components";
export const KmsPage = withProjectPermission(
() => {
return (
<div className="container mx-auto flex flex-col justify-between bg-bunker-800 text-white">
<div className="mx-auto mb-6 w-full max-w-7xl py-6 px-6">
<p className="mr-4 text-3xl font-semibold text-white">Key Management System</p>
<p className="text-md mb-4 text-bunker-300">
Manage keys and perform cryptographic operations.
</p>
<CmekTable />
</div>
</div>
);
},
{
action: ProjectPermissionCmekActions.Read,
subject: ProjectPermissionSub.Cmek
}
);
@@ -1,4 +1,4 @@
import { useEffect, useState } from "react"; import { useEffect } from "react";
import Link from "next/link"; import Link from "next/link";
import { import {
faArrowDown, faArrowDown,
@@ -44,7 +44,7 @@ import {
} from "@app/components/v2"; } from "@app/components/v2";
import { ProjectPermissionActions, ProjectPermissionSub, useWorkspace } from "@app/context"; import { ProjectPermissionActions, ProjectPermissionSub, useWorkspace } from "@app/context";
import { withProjectPermission } from "@app/hoc"; import { withProjectPermission } from "@app/hoc";
import { useDebounce } from "@app/hooks"; import { usePagination } from "@app/hooks";
import { useDeleteIdentityFromWorkspace, useGetWorkspaceIdentityMemberships } from "@app/hooks/api"; import { useDeleteIdentityFromWorkspace, useGetWorkspaceIdentityMemberships } from "@app/hooks/api";
import { OrderByDirection } from "@app/hooks/api/generic/types"; import { OrderByDirection } from "@app/hooks/api/generic/types";
import { IdentityMembership } from "@app/hooks/api/identities/types"; import { IdentityMembership } from "@app/hooks/api/identities/types";
@@ -56,7 +56,7 @@ import { IdentityModal } from "./components/IdentityModal";
import { IdentityRoleForm } from "./components/IdentityRoleForm"; import { IdentityRoleForm } from "./components/IdentityRoleForm";
const MAX_ROLES_TO_BE_SHOWN_IN_TABLE = 2; const MAX_ROLES_TO_BE_SHOWN_IN_TABLE = 2;
const INIT_PER_PAGE = 20;
const formatRoleName = (role: string, customRoleName?: string) => { const formatRoleName = (role: string, customRoleName?: string) => {
if (role === ProjectMembershipRole.Custom) return customRoleName; if (role === ProjectMembershipRole.Custom) return customRoleName;
if (role === ProjectMembershipRole.Member) return "Developer"; if (role === ProjectMembershipRole.Member) return "Developer";
@@ -67,27 +67,43 @@ export const IdentityTab = withProjectPermission(
() => { () => {
const { currentWorkspace } = useWorkspace(); const { currentWorkspace } = useWorkspace();
const [page, setPage] = useState(1); const {
const [perPage, setPerPage] = useState(INIT_PER_PAGE); offset,
const [orderDirection, setOrderDirection] = useState(OrderByDirection.ASC); limit,
const [orderBy, setOrderBy] = useState(ProjectIdentityOrderBy.Name); orderBy,
const [search, setSearch] = useState(""); setOrderBy,
const [debouncedSearch] = useDebounce(search); orderDirection,
setOrderDirection,
search,
debouncedSearch,
setPage,
setSearch,
perPage,
page,
setPerPage
} = usePagination(ProjectIdentityOrderBy.Name);
const workspaceId = currentWorkspace?.id ?? ""; const workspaceId = currentWorkspace?.id ?? "";
const offset = (page - 1) * perPage;
const { data, isLoading, isFetching } = useGetWorkspaceIdentityMemberships( const { data, isLoading, isFetching } = useGetWorkspaceIdentityMemberships(
{ {
workspaceId: currentWorkspace?.id || "", workspaceId: currentWorkspace?.id || "",
offset, offset,
limit: perPage, limit,
orderDirection, orderDirection,
orderBy, orderBy,
search: debouncedSearch search: debouncedSearch
}, },
{ keepPreviousData: true } { keepPreviousData: true }
); );
const { totalCount = 0 } = data ?? {};
useEffect(() => {
// reset page if no longer valid
if (totalCount <= offset) setPage(1);
}, [totalCount]);
const { mutateAsync: deleteMutateAsync } = useDeleteIdentityFromWorkspace(); const { mutateAsync: deleteMutateAsync } = useDeleteIdentityFromWorkspace();
const { popUp, handlePopUpOpen, handlePopUpClose, handlePopUpToggle } = usePopUp([ const { popUp, handlePopUpOpen, handlePopUpClose, handlePopUpToggle } = usePopUp([
@@ -122,11 +138,6 @@ export const IdentityTab = withProjectPermission(
} }
}; };
useEffect(() => {
// reset page if no longer valid
if (data && data.totalCount < offset) setPage(1);
}, [data?.totalCount]);
const handleSort = (column: ProjectIdentityOrderBy) => { const handleSort = (column: ProjectIdentityOrderBy) => {
if (column === orderBy) { if (column === orderBy) {
setOrderDirection((prev) => setOrderDirection((prev) =>
@@ -369,9 +380,9 @@ export const IdentityTab = withProjectPermission(
})} })}
</TBody> </TBody>
</Table> </Table>
{!isLoading && data && data.totalCount > 0 && ( {!isLoading && data && totalCount > 0 && (
<Pagination <Pagination
count={data.totalCount} count={totalCount}
page={page} page={page}
perPage={perPage} perPage={perPage}
onChangePage={(newPage) => setPage(newPage)} onChangePage={(newPage) => setPage(newPage)}
@@ -1,6 +1,10 @@
import { z } from "zod"; import { z } from "zod";
import { ProjectPermissionActions, ProjectPermissionSub } from "@app/context"; import {
ProjectPermissionActions,
ProjectPermissionCmekActions,
ProjectPermissionSub
} from "@app/context";
import { import {
PermissionConditionOperators, PermissionConditionOperators,
TPermissionCondition, TPermissionCondition,
@@ -15,6 +19,15 @@ const GeneralPolicyActionSchema = z.object({
create: z.boolean().optional() create: z.boolean().optional()
}); });
const CmekPolicyActionSchema = z.object({
read: z.boolean().optional(),
edit: z.boolean().optional(),
delete: z.boolean().optional(),
create: z.boolean().optional(),
encrypt: z.boolean().optional(),
decrypt: z.boolean().optional()
});
const SecretFolderPolicyActionSchema = z.object({ const SecretFolderPolicyActionSchema = z.object({
read: z.boolean().optional() read: z.boolean().optional()
}); });
@@ -88,7 +101,8 @@ export const formSchema = z.object({
[ProjectPermissionSub.Workspace]: WorkspacePolicyActionSchema.array().default([]), [ProjectPermissionSub.Workspace]: WorkspacePolicyActionSchema.array().default([]),
[ProjectPermissionSub.Tags]: GeneralPolicyActionSchema.array().default([]), [ProjectPermissionSub.Tags]: GeneralPolicyActionSchema.array().default([]),
[ProjectPermissionSub.SecretRotation]: GeneralPolicyActionSchema.array().default([]), [ProjectPermissionSub.SecretRotation]: GeneralPolicyActionSchema.array().default([]),
[ProjectPermissionSub.Kms]: GeneralPolicyActionSchema.array().default([]) [ProjectPermissionSub.Kms]: GeneralPolicyActionSchema.array().default([]),
[ProjectPermissionSub.Cmek]: CmekPolicyActionSchema.array().default([])
}) })
.partial() .partial()
.optional() .optional()
@@ -119,7 +133,7 @@ const convertCaslConditionToFormOperator = (caslConditions: TPermissionCondition
return formConditions; return formConditions;
}; };
// convert role permission to form compatiable data structure // convert role permission to form compatible data structure
export const rolePermission2Form = (permissions: TProjectPermission[] = []) => { export const rolePermission2Form = (permissions: TProjectPermission[] = []) => {
const formVal: Partial<TFormSchema["permissions"]> = {}; const formVal: Partial<TFormSchema["permissions"]> = {};
@@ -198,6 +212,23 @@ export const rolePermission2Form = (permissions: TProjectPermission[] = []) => {
// from above statement we are sure it won't be undefined // from above statement we are sure it won't be undefined
if (canRead) formVal[subject as ProjectPermissionSub.Member]![0].read = true; if (canRead) formVal[subject as ProjectPermissionSub.Member]![0].read = true;
} else if (subject === ProjectPermissionSub.Cmek) {
const canRead = action.includes(ProjectPermissionCmekActions.Read);
const canEdit = action.includes(ProjectPermissionCmekActions.Edit);
const canDelete = action.includes(ProjectPermissionCmekActions.Delete);
const canCreate = action.includes(ProjectPermissionCmekActions.Create);
const canEncrypt = action.includes(ProjectPermissionCmekActions.Encrypt);
const canDecrypt = action.includes(ProjectPermissionCmekActions.Decrypt);
if (!formVal[subject]) formVal[subject] = [{}];
// from above statement we are sure it won't be undefined
if (canRead) formVal[subject]![0].read = true;
if (canEdit) formVal[subject]![0].edit = true;
if (canCreate) formVal[subject]![0].create = true;
if (canDelete) formVal[subject]![0].delete = true;
if (canEncrypt) formVal[subject]![0].encrypt = true;
if (canDecrypt) formVal[subject]![0].decrypt = true;
} }
}); });
return formVal; return formVal;
@@ -277,8 +308,19 @@ export const PROJECT_PERMISSION_OBJECT: TProjectPermissionObject = {
title: "Secret Folders", title: "Secret Folders",
actions: [{ label: "Read Only", value: "read" }] actions: [{ label: "Read Only", value: "read" }]
}, },
[ProjectPermissionSub.Kms]: { [ProjectPermissionSub.Cmek]: {
title: "KMS", title: "KMS",
actions: [
{ label: "Read", value: "read" },
{ label: "Create", value: "create" },
{ label: "Modify", value: "edit" },
{ label: "Remove", value: "delete" },
{ label: "Encrypt", value: "encrypt" },
{ label: "Decrypt", value: "decrypt" }
]
},
[ProjectPermissionSub.Kms]: {
title: "Project KMS Configuration",
actions: [{ label: "Modify", value: "edit" }] actions: [{ label: "Modify", value: "edit" }]
}, },
[ProjectPermissionSub.Integrations]: { [ProjectPermissionSub.Integrations]: {
@@ -16,7 +16,7 @@ import {
useProjectPermission, useProjectPermission,
useWorkspace useWorkspace
} from "@app/context"; } from "@app/context";
import { useDebounce, usePopUp } from "@app/hooks"; import { useDebounce, usePagination, usePopUp } from "@app/hooks";
import { import {
useGetImportedSecretsSingleEnv, useGetImportedSecretsSingleEnv,
useGetSecretApprovalPolicyOfABoard, useGetSecretApprovalPolicyOfABoard,
@@ -25,6 +25,7 @@ import {
useGetWsTags useGetWsTags
} from "@app/hooks/api"; } from "@app/hooks/api";
import { useGetProjectSecretsDetails } from "@app/hooks/api/dashboard"; import { useGetProjectSecretsDetails } from "@app/hooks/api/dashboard";
import { DashboardSecretsOrderBy } from "@app/hooks/api/dashboard/types";
import { OrderByDirection } from "@app/hooks/api/generic/types"; import { OrderByDirection } from "@app/hooks/api/generic/types";
import { DynamicSecretListView } from "@app/views/SecretMainPage/components/DynamicSecretListView"; import { DynamicSecretListView } from "@app/views/SecretMainPage/components/DynamicSecretListView";
import { FolderListView } from "@app/views/SecretMainPage/components/FolderListView"; import { FolderListView } from "@app/views/SecretMainPage/components/FolderListView";
@@ -47,7 +48,6 @@ const LOADER_TEXT = [
"Getting secret import links..." "Getting secret import links..."
]; ];
const INIT_PER_PAGE = 20;
export const SecretMainPage = () => { export const SecretMainPage = () => {
const { t } = useTranslation(); const { t } = useTranslation();
const { currentWorkspace, isLoading: isWorkspaceLoading } = useWorkspace(); const { currentWorkspace, isLoading: isWorkspaceLoading } = useWorkspace();
@@ -55,11 +55,18 @@ export const SecretMainPage = () => {
const { permission } = useProjectPermission(); const { permission } = useProjectPermission();
const [isVisible, setIsVisible] = useState(false); const [isVisible, setIsVisible] = useState(false);
const [orderDirection, setOrderDirection] = useState<OrderByDirection>(OrderByDirection.ASC);
const [page, setPage] = useState(1); const {
const [perPage, setPerPage] = useState(INIT_PER_PAGE); offset,
const paginationOffset = (page - 1) * perPage; limit,
orderDirection,
setOrderDirection,
setPage,
perPage,
page,
setPerPage,
orderBy
} = usePagination<DashboardSecretsOrderBy>(DashboardSecretsOrderBy.Name);
const [snapshotId, setSnapshotId] = useState<string | null>(null); const [snapshotId, setSnapshotId] = useState<string | null>(null);
const isRollbackMode = Boolean(snapshotId); const isRollbackMode = Boolean(snapshotId);
@@ -129,8 +136,9 @@ export const SecretMainPage = () => {
environment, environment,
projectId: workspaceId, projectId: workspaceId,
secretPath, secretPath,
offset: paginationOffset, offset,
limit: perPage, limit,
orderBy,
search: debouncedSearchFilter, search: debouncedSearchFilter,
orderDirection, orderDirection,
includeImports: canReadSecret && filter.include.import, includeImports: canReadSecret && filter.include.import,
@@ -152,6 +160,11 @@ export const SecretMainPage = () => {
totalCount = 0 totalCount = 0
} = data ?? {}; } = data ?? {};
useEffect(() => {
// reset page if no longer valid
if (totalCount <= offset) setPage(1);
}, [totalCount]);
// fetch imported secrets to show user the overriden ones // fetch imported secrets to show user the overriden ones
const { data: importedSecrets } = useGetImportedSecretsSingleEnv({ const { data: importedSecrets } = useGetImportedSecretsSingleEnv({
projectId: workspaceId, projectId: workspaceId,
@@ -253,11 +266,6 @@ export const SecretMainPage = () => {
handlePopUpClose("snapshots"); handlePopUpClose("snapshots");
}, []); }, []);
useEffect(() => {
// reset page if no longer valid
if (totalCount < paginationOffset) setPage(1);
}, [totalCount]);
useEffect(() => { useEffect(() => {
// restore filters for path if set // restore filters for path if set
const restore = filterHistory.get(secretPath); const restore = filterHistory.get(secretPath);
@@ -54,7 +54,7 @@ import {
useProjectPermission, useProjectPermission,
useWorkspace useWorkspace
} from "@app/context"; } from "@app/context";
import { useDebounce, usePopUp } from "@app/hooks"; import { useDebounce, usePagination, usePopUp } from "@app/hooks";
import { import {
useCreateFolder, useCreateFolder,
useCreateSecretV3, useCreateSecretV3,
@@ -95,7 +95,6 @@ enum RowType {
type Filter = { type Filter = {
[key in RowType]: boolean; [key in RowType]: boolean;
}; };
const INIT_PER_PAGE = 20;
const DEFAULT_FILTER_STATE = { const DEFAULT_FILTER_STATE = {
[RowType.Folder]: true, [RowType.Folder]: true,
@@ -111,7 +110,6 @@ export const SecretOverviewPage = () => {
// coz when overflow the table goes to the right // coz when overflow the table goes to the right
const parentTableRef = useRef<HTMLTableElement>(null); const parentTableRef = useRef<HTMLTableElement>(null);
const [expandableTableWidth, setExpandableTableWidth] = useState(0); const [expandableTableWidth, setExpandableTableWidth] = useState(0);
const [orderDirection, setOrderDirection] = useState<OrderByDirection>(OrderByDirection.ASC);
const { permission } = useProjectPermission(); const { permission } = useProjectPermission();
useEffect(() => { useEffect(() => {
@@ -142,8 +140,17 @@ export const SecretOverviewPage = () => {
[EntryType.SECRET]: {} [EntryType.SECRET]: {}
}); });
const [page, setPage] = useState(1); const {
const [perPage, setPerPage] = useState(INIT_PER_PAGE); offset,
limit,
orderDirection,
setOrderDirection,
setPage,
perPage,
page,
setPerPage,
orderBy
} = usePagination<DashboardSecretsOrderBy>(DashboardSecretsOrderBy.Name);
const toggleSelectedEntry = useCallback( const toggleSelectedEntry = useCallback(
(type: EntryType, key: string) => { (type: EntryType, key: string) => {
@@ -208,21 +215,19 @@ export const SecretOverviewPage = () => {
environments: userAvailableEnvs.map(({ slug }) => slug) environments: userAvailableEnvs.map(({ slug }) => slug)
}); });
const paginationOffset = (page - 1) * perPage;
const { isLoading: isOverviewLoading, data: overview } = useGetProjectSecretsOverview( const { isLoading: isOverviewLoading, data: overview } = useGetProjectSecretsOverview(
{ {
projectId: workspaceId, projectId: workspaceId,
environments: visibleEnvs.map((env) => env.slug), environments: visibleEnvs.map((env) => env.slug),
secretPath, secretPath,
orderDirection, orderDirection,
orderBy: DashboardSecretsOrderBy.Name, orderBy,
includeFolders: filter.folder, includeFolders: filter.folder,
includeDynamicSecrets: filter.dynamic, includeDynamicSecrets: filter.dynamic,
includeSecrets: filter.secret, includeSecrets: filter.secret,
search: debouncedSearchFilter, search: debouncedSearchFilter,
limit: perPage, limit,
offset: paginationOffset offset
}, },
{ enabled: isProjectV3 } { enabled: isProjectV3 }
); );
@@ -231,12 +236,17 @@ export const SecretOverviewPage = () => {
secrets, secrets,
folders, folders,
dynamicSecrets, dynamicSecrets,
totalCount = 0,
totalFolderCount, totalFolderCount,
totalSecretCount, totalSecretCount,
totalDynamicSecretCount totalDynamicSecretCount,
totalCount = 0
} = overview ?? {}; } = overview ?? {};
useEffect(() => {
// reset page if no longer valid
if (totalCount <= offset) setPage(1);
}, [totalCount]);
const { folderNames, getFolderByNameAndEnv, isFolderPresentInEnv } = useFolderOverview(folders); const { folderNames, getFolderByNameAndEnv, isFolderPresentInEnv } = useFolderOverview(folders);
const { dynamicSecretNames, isDynamicSecretPresentInEnv } = const { dynamicSecretNames, isDynamicSecretPresentInEnv } =
@@ -530,11 +540,6 @@ export const SecretOverviewPage = () => {
} }
}; };
useEffect(() => {
// reset page if no longer valid
if (totalCount < paginationOffset) setPage(1);
}, [totalCount]);
const handleToggleRowType = useCallback( const handleToggleRowType = useCallback(
(rowType: RowType) => (rowType: RowType) =>
setFilter((state) => { setFilter((state) => {
@@ -61,7 +61,7 @@ export const AwsKmsForm = ({ onCompleted, onCancel, kms }: Props) => {
} = useForm<AddExternalKmsType>({ } = useForm<AddExternalKmsType>({
resolver: zodResolver(AddExternalKmsSchema), resolver: zodResolver(AddExternalKmsSchema),
defaultValues: { defaultValues: {
slug: kms?.slug, name: kms?.name,
description: kms?.description ?? "", description: kms?.description ?? "",
provider: { provider: {
type: ExternalKmsProvider.AWS, type: ExternalKmsProvider.AWS,
@@ -89,12 +89,12 @@ export const AwsKmsForm = ({ onCompleted, onCancel, kms }: Props) => {
const selectedAwsAuthType = watch("provider.inputs.credential.type"); const selectedAwsAuthType = watch("provider.inputs.credential.type");
const handleAddAwsKms = async (data: AddExternalKmsType) => { const handleAddAwsKms = async (data: AddExternalKmsType) => {
const { slug, description, provider } = data; const { name, description, provider } = data;
try { try {
if (kms) { if (kms) {
await updateAwsExternalKms({ await updateAwsExternalKms({
kmsId: kms.id, kmsId: kms.id,
slug, name,
description, description,
provider provider
}); });
@@ -105,7 +105,7 @@ export const AwsKmsForm = ({ onCompleted, onCancel, kms }: Props) => {
}); });
} else { } else {
await addAwsExternalKms({ await addAwsExternalKms({
slug, name,
description, description,
provider provider
}); });
@@ -126,7 +126,7 @@ export const AwsKmsForm = ({ onCompleted, onCancel, kms }: Props) => {
<form onSubmit={handleSubmit(handleAddAwsKms)} autoComplete="off"> <form onSubmit={handleSubmit(handleAddAwsKms)} autoComplete="off">
<Controller <Controller
control={control} control={control}
name="slug" name="name"
render={({ field, fieldState: { error } }) => ( render={({ field, fieldState: { error } }) => (
<FormControl label="Alias" errorText={error?.message} isError={Boolean(error)}> <FormControl label="Alias" errorText={error?.message} isError={Boolean(error)}>
<Input placeholder="" {...field} /> <Input placeholder="" {...field} />
@@ -122,7 +122,7 @@ export const OrgEncryptionTab = withPermission(
)} )}
<div className="ml-2">{kms.externalKms.provider.toUpperCase()}</div> <div className="ml-2">{kms.externalKms.provider.toUpperCase()}</div>
</Td> </Td>
<Td>{kms.slug}</Td> <Td>{kms.name}</Td>
<Td> <Td>
<DropdownMenu> <DropdownMenu>
<DropdownMenuTrigger asChild className="rounded-lg"> <DropdownMenuTrigger asChild className="rounded-lg">
@@ -170,7 +170,7 @@ export const OrgEncryptionTab = withPermission(
onClick={(e) => { onClick={(e) => {
e.stopPropagation(); e.stopPropagation();
handlePopUpOpen("removeExternalKms", { handlePopUpOpen("removeExternalKms", {
slug: kms.slug, name: kms.name,
kmsId: kms.id, kmsId: kms.id,
provider: kms.externalKms.provider provider: kms.externalKms.provider
}); });
@@ -307,7 +307,7 @@ export const EncryptionTab = () => {
</SelectItem> </SelectItem>
{externalKmsList?.map((kms) => ( {externalKmsList?.map((kms) => (
<SelectItem value={kms.id} key={`kms-${kms.id}`}> <SelectItem value={kms.id} key={`kms-${kms.id}`}>
{kms.slug} {kms.name}
</SelectItem> </SelectItem>
))} ))}
</Select> </Select>