improvement: finish address changes
@@ -73,6 +73,12 @@ 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)**: Manage and perform cryptographic operations with personally managed keys.
|
||||||
|
- **[Encrypt and Decrypt Data](https://infisical.com/docs/documentation/platform/kms#guide-to-encrypting-data)**: Use symmetric keys to encrypt and decrypt data.
|
||||||
|
- **[Audit Trail](https://infisical.com/docs/documentation/platform/kms)**: Maintain detailed logs of all key-related activities for compliance and security analysis.
|
||||||
|
|
||||||
### 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.
|
||||||
|
|||||||
@@ -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,44 @@
|
|||||||
|
import { Knex } from "knex";
|
||||||
|
|
||||||
|
import { TableName } from "@app/db/schemas";
|
||||||
|
|
||||||
|
export async function up(knex: Knex): Promise<void> {
|
||||||
|
if (await knex.schema.hasTable(TableName.KmsKey)) {
|
||||||
|
const hasOrgId = await knex.schema.hasColumn(TableName.KmsKey, "orgId");
|
||||||
|
const hasSlug = await knex.schema.hasColumn(TableName.KmsKey, "slug");
|
||||||
|
|
||||||
|
// 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.dropUnique(["orgId", "slug"]); // prevents using the same key name in different projects so swapping constraint
|
||||||
|
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 (hasOrgId) {
|
||||||
|
table.dropUnique(["orgId", "projectId", "slug"]);
|
||||||
|
table.unique(["orgId", "slug"]);
|
||||||
|
}
|
||||||
|
table.dropColumn("projectId");
|
||||||
|
|
||||||
|
if (hasName) {
|
||||||
|
table.renameColumn("name", "slug");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -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>;
|
||||||
|
|||||||
@@ -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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -266,7 +266,7 @@ export const registerExternalKmsRouter = async (server: FastifyZodProvider) => {
|
|||||||
},
|
},
|
||||||
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({
|
||||||
@@ -281,7 +281,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.params.slug
|
name: req.params.name
|
||||||
});
|
});
|
||||||
return { externalKms };
|
return { externalKms };
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
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">;
|
||||||
|
|||||||
@@ -2,13 +2,24 @@ import { CreateKeyCommand, DecryptCommand, DescribeKeyCommand, EncryptCommand, K
|
|||||||
import { AssumeRoleCommand, STSClient } from "@aws-sdk/client-sts";
|
import { AssumeRoleCommand, STSClient } from "@aws-sdk/client-sts";
|
||||||
import { randomUUID } from "crypto";
|
import { randomUUID } from "crypto";
|
||||||
|
|
||||||
|
import { getConfig } from "@app/lib/config/env";
|
||||||
|
|
||||||
import { ExternalKmsAwsSchema, KmsAwsCredentialType, TExternalKmsAwsSchema, TExternalKmsProviderFns } from "./model";
|
import { ExternalKmsAwsSchema, KmsAwsCredentialType, TExternalKmsAwsSchema, TExternalKmsProviderFns } from "./model";
|
||||||
|
|
||||||
const getAwsKmsClient = async (providerInputs: TExternalKmsAwsSchema) => {
|
const getAwsKmsClient = async (providerInputs: TExternalKmsAwsSchema) => {
|
||||||
|
const appCfg = getConfig();
|
||||||
|
|
||||||
if (providerInputs.credential.type === KmsAwsCredentialType.AssumeRole) {
|
if (providerInputs.credential.type === KmsAwsCredentialType.AssumeRole) {
|
||||||
const awsCredential = providerInputs.credential.data;
|
const awsCredential = providerInputs.credential.data;
|
||||||
const stsClient = new STSClient({
|
const stsClient = new STSClient({
|
||||||
region: providerInputs.awsRegion
|
region: providerInputs.awsRegion,
|
||||||
|
credentials:
|
||||||
|
appCfg.CLIENT_ID_AWS_INTEGRATION && appCfg.CLIENT_SECRET_AWS_INTEGRATION
|
||||||
|
? {
|
||||||
|
accessKeyId: appCfg.CLIENT_ID_AWS_INTEGRATION,
|
||||||
|
secretAccessKey: appCfg.CLIENT_SECRET_AWS_INTEGRATION
|
||||||
|
}
|
||||||
|
: undefined
|
||||||
});
|
});
|
||||||
const command = new AssumeRoleCommand({
|
const command = new AssumeRoleCommand({
|
||||||
RoleArn: awsCredential.assumeRoleArn,
|
RoleArn: awsCredential.assumeRoleArn,
|
||||||
|
|||||||
@@ -40,7 +40,7 @@ export const getDefaultOnPremFeatures = (): TFeatureSet => ({
|
|||||||
secretRotation: true,
|
secretRotation: true,
|
||||||
caCrl: false,
|
caCrl: false,
|
||||||
instanceUserManagement: false,
|
instanceUserManagement: false,
|
||||||
externalKms: false,
|
externalKms: true,
|
||||||
rateLimits: {
|
rateLimits: {
|
||||||
readLimit: 60,
|
readLimit: 60,
|
||||||
writeLimit: 200,
|
writeLimit: 200,
|
||||||
|
|||||||
@@ -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;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -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)."
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|||||||
@@ -0,0 +1,40 @@
|
|||||||
|
// 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) => {
|
||||||
|
// Remove data URI scheme if present
|
||||||
|
const base64 = base64String.replace(/^data:.*?;base64,/, "");
|
||||||
|
|
||||||
|
// Remove padding characters
|
||||||
|
const withoutPadding = base64.replace(/=+$/, "");
|
||||||
|
|
||||||
|
// Calculate bits: each base64 character represents 6 bits
|
||||||
|
const totalBits = withoutPadding.length * 6;
|
||||||
|
|
||||||
|
// Convert bits to bytes (8 bits = 1 byte)
|
||||||
|
const bytes = totalBits / 8;
|
||||||
|
|
||||||
|
return bytes;
|
||||||
|
};
|
||||||
@@ -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";
|
||||||
@@ -1192,6 +1193,12 @@ export const registerRoutes = async (
|
|||||||
workflowIntegrationDAL
|
workflowIntegrationDAL
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const cmekService = cmekServiceFactory({
|
||||||
|
kmsDAL,
|
||||||
|
kmsService,
|
||||||
|
permissionService
|
||||||
|
});
|
||||||
|
|
||||||
const migrationService = externalMigrationServiceFactory({
|
const migrationService = externalMigrationServiceFactory({
|
||||||
projectService,
|
projectService,
|
||||||
orgService,
|
orgService,
|
||||||
@@ -1281,6 +1288,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,
|
||||||
|
|||||||
@@ -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) > 6144) {
|
||||||
|
ctx.addIssue({
|
||||||
|
code: z.ZodIssueCode.custom,
|
||||||
|
message: "data cannot exceed 6144 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]),
|
||||||
|
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]),
|
||||||
|
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]),
|
||||||
|
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]),
|
||||||
|
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]),
|
||||||
|
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]),
|
||||||
|
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 };
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
@@ -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" });
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -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
|
||||||
|
};
|
||||||
|
};
|
||||||
@@ -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"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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;
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -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 };
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -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";
|
||||||
@@ -66,22 +67,43 @@ export const kmsServiceFactory = ({
|
|||||||
}: TKmsServiceFactoryDep) => {
|
}: TKmsServiceFactoryDep) => {
|
||||||
let ROOT_ENCRYPTION_KEY = Buffer.alloc(0);
|
let ROOT_ENCRYPTION_KEY = Buffer.alloc(0);
|
||||||
|
|
||||||
|
const $getRootEncryptionKey = (encryptionAlgorithm: SymmetricEncryption) => {
|
||||||
|
switch (encryptionAlgorithm) {
|
||||||
|
case SymmetricEncryption.AES_GCM_128:
|
||||||
|
return ROOT_ENCRYPTION_KEY.subarray(0, 16); // taking first 128bits
|
||||||
|
case SymmetricEncryption.AES_GCM_256:
|
||||||
|
default:
|
||||||
|
return ROOT_ENCRYPTION_KEY;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* Generate KMS Key
|
* Generate KMS Key
|
||||||
* 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 ({
|
||||||
const cipher = symmetricCipherService(SymmetricEncryption.AES_GCM_256);
|
orgId,
|
||||||
const kmsKeyMaterial = randomSecureBytes(32);
|
isReserved = true,
|
||||||
const encryptedKeyMaterial = cipher.encrypt(kmsKeyMaterial, ROOT_ENCRYPTION_KEY);
|
tx,
|
||||||
const sanitizedSlug = slug ? slugify(slug) : slugify(alphaNumericNanoId(8).toLowerCase());
|
name,
|
||||||
|
projectId,
|
||||||
|
encryptionAlgorithm = SymmetricEncryption.AES_GCM_256,
|
||||||
|
description
|
||||||
|
}: TGenerateKMSDTO) => {
|
||||||
|
const cipher = symmetricCipherService(encryptionAlgorithm);
|
||||||
|
|
||||||
|
const kmsKeyMaterial = randomSecureBytes(getByteLengthForAlgorithm(encryptionAlgorithm));
|
||||||
|
const encryptedKeyMaterial = cipher.encrypt(kmsKeyMaterial, $getRootEncryptionKey(encryptionAlgorithm));
|
||||||
|
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 +112,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,8 +308,13 @@ export const kmsServiceFactory = ({
|
|||||||
}
|
}
|
||||||
|
|
||||||
// internal KMS
|
// internal KMS
|
||||||
const cipher = symmetricCipherService(SymmetricEncryption.AES_GCM_256);
|
const encryptionAlgorithm =
|
||||||
const kmsKey = cipher.decrypt(kmsDoc.internalKms?.encryptedKey as Buffer, ROOT_ENCRYPTION_KEY);
|
(kmsDoc.internalKms?.encryptionAlgorithm as SymmetricEncryption) ?? SymmetricEncryption.AES_GCM_256;
|
||||||
|
const cipher = symmetricCipherService(encryptionAlgorithm);
|
||||||
|
const kmsKey = cipher.decrypt(
|
||||||
|
kmsDoc.internalKms?.encryptedKey as Buffer,
|
||||||
|
$getRootEncryptionKey(encryptionAlgorithm)
|
||||||
|
);
|
||||||
|
|
||||||
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);
|
||||||
@@ -347,10 +374,14 @@ export const kmsServiceFactory = ({
|
|||||||
}
|
}
|
||||||
|
|
||||||
// internal KMS
|
// internal KMS
|
||||||
// akhilmhdh: as more encryption are added do a check here on kmsDoc.encryptionAlgorithm
|
const encryptionAlgorithm =
|
||||||
const cipher = symmetricCipherService(SymmetricEncryption.AES_GCM_256);
|
(kmsDoc.internalKms?.encryptionAlgorithm as SymmetricEncryption) ?? SymmetricEncryption.AES_GCM_256;
|
||||||
|
const cipher = symmetricCipherService(encryptionAlgorithm);
|
||||||
return ({ plainText }: Pick<TEncryptWithKmsDTO, "plainText">) => {
|
return ({ plainText }: Pick<TEncryptWithKmsDTO, "plainText">) => {
|
||||||
const kmsKey = cipher.decrypt(kmsDoc.internalKms?.encryptedKey as Buffer, ROOT_ENCRYPTION_KEY);
|
const kmsKey = cipher.decrypt(
|
||||||
|
kmsDoc.internalKms?.encryptedKey as Buffer,
|
||||||
|
$getRootEncryptionKey(encryptionAlgorithm)
|
||||||
|
);
|
||||||
const encryptedPlainTextBlob = cipher.encrypt(plainText, kmsKey);
|
const encryptedPlainTextBlob = cipher.encrypt(plainText, kmsKey);
|
||||||
|
|
||||||
// Buffer#1 encrypted text + Buffer#2 version number
|
// Buffer#1 encrypted text + Buffer#2 version number
|
||||||
@@ -767,8 +798,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
|
||||||
|
|||||||
@@ -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}"
|
||||||
|
---
|
||||||
@@ -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"
|
||||||
---
|
---
|
||||||
@@ -26,3 +26,8 @@ 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.
|
||||||
|
|
||||||
|
## Infisical KMS
|
||||||
|
|
||||||
|
Infisical exposes it's internal KMS solution, [Infisical KMS](../kms), enabling you to create and manage keys to perform cryptographic operations with.
|
||||||
|
|
||||||
@@ -0,0 +1,200 @@
|
|||||||
|
---
|
||||||
|
title: "Key Management Service (KMS)"
|
||||||
|
sidebarTitle: "Key Management (KMS)"
|
||||||
|
description: "Learn how to manage and use cryptographic keys with Infisical."
|
||||||
|
---
|
||||||
|
|
||||||
|
## Introduction
|
||||||
|
|
||||||
|
Infisical's <strong>Key Management System (KMS)</strong> allows you to create, store and manage cryptographic keys.
|
||||||
|
These keys can be used to perform cryptographic operations such as data encryption. You can access
|
||||||
|
Infisical's KMS from the [project](./project) sidebar.
|
||||||
|
|
||||||
|
## Features
|
||||||
|
|
||||||
|
1. <strong>Centralized Key Storage:</strong> Securely store all your organization's cryptographic keys in one location.
|
||||||
|
2. <strong>Encryption and
|
||||||
|
Decryption:</strong> Provide on-demand encryption and decryption services without exposing the keys to
|
||||||
|
external applications.
|
||||||
|
3. <strong>Audit
|
||||||
|
Trails:</strong> Maintain detailed logs of all key-related activities for compliance and security analysis.
|
||||||
|
|
||||||
|
<Note>
|
||||||
|
Your keys will never be used or viewable outside of Infisical KMS.
|
||||||
|
In addition, no data is stored when performing cryptographic operations.
|
||||||
|
</Note>
|
||||||
|
|
||||||
|
## Guide to Encrypting Data
|
||||||
|
|
||||||
|
In the following steps, we'll explore how to generate a cryptographic key and encrypt data.
|
||||||
|
|
||||||
|
<Tabs>
|
||||||
|
<Tab title="Infisical UI">
|
||||||
|
<Steps>
|
||||||
|
<Step title="Creating a key">
|
||||||
|
Navigate to Project > Key Management and tap on the Add Key button.
|
||||||
|

|
||||||
|
|
||||||
|
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 this key. By default symmetric `AES-GCM-256` is selected,
|
||||||
|
but
|
||||||
|
Infisical will continue to add more options down the road.
|
||||||
|
- Description: An optional description of what this key is used for.
|
||||||
|
|
||||||
|

|
||||||
|
</Step>
|
||||||
|
<Step title="Encrypting your data">
|
||||||
|
Once your key is generated, open the options menu for the newly created key and select encrypt data.
|
||||||
|

|
||||||
|
|
||||||
|
Populate the text area with your data and tap on the Encrypt button.
|
||||||
|

|
||||||
|
|
||||||
|
<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.
|
||||||
|

|
||||||
|
</Step>
|
||||||
|
</Steps>
|
||||||
|
</Tab>
|
||||||
|
<Tab title="API">
|
||||||
|
<Steps>
|
||||||
|
<Step title="Creating a 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">
|
||||||
|
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'll explore how to decrypt data.
|
||||||
|
|
||||||
|
<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.
|
||||||
|

|
||||||
|
|
||||||
|
|
||||||
|
</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.
|
||||||
|

|
||||||
|
|
||||||
|
Your decrypted data will be displayed and can be copied for use.
|
||||||
|

|
||||||
|
|
||||||
|
</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>
|
||||||
|
</AccordionGroup>
|
||||||
|
After Width: | Height: | Size: 535 KiB |
|
After Width: | Height: | Size: 702 KiB |
|
After Width: | Height: | Size: 624 KiB |
|
After Width: | Height: | Size: 942 KiB |
|
After Width: | Height: | Size: 585 KiB |
|
After Width: | Height: | Size: 558 KiB |
|
After Width: | Height: | Size: 586 KiB |
|
After Width: | Height: | Size: 734 KiB |
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "Infisical",
|
"name": "Infisical",
|
||||||
"openapi": "https://app.infisical.com/api/docs/json",
|
"openapi": "http://localhost:8080/api/docs/json",
|
||||||
"logo": {
|
"logo": {
|
||||||
"dark": "/logo/dark.svg",
|
"dark": "/logo/dark.svg",
|
||||||
"light": "/logo/light.svg",
|
"light": "/logo/light.svg",
|
||||||
@@ -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": [
|
||||||
@@ -171,14 +180,6 @@
|
|||||||
"documentation/platform/dynamic-secrets/azure-entra-id"
|
"documentation/platform/dynamic-secrets/azure-entra-id"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
|
||||||
"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": [
|
||||||
@@ -789,6 +790,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
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -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>}
|
||||||
|
|||||||
@@ -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>;
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
@@ -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
|
||||||
|
});
|
||||||
|
};
|
||||||
@@ -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"
|
||||||
|
}
|
||||||
@@ -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
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -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`);
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -122,6 +122,6 @@ export type TOrgIdentitiesList = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export enum OrgIdentityOrderBy {
|
export enum OrgIdentityOrderBy {
|
||||||
Name = "name",
|
Name = "name"
|
||||||
Role = "role"
|
// Role = "role"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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";
|
||||||
|
|||||||
@@ -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
|
||||||
|
};
|
||||||
|
};
|
||||||
@@ -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>
|
||||||
|
|||||||