Merge remote-tracking branch 'origin/main' into misc/move-audit-logs-to-dedicated
@@ -73,6 +73,11 @@ We're on a mission to make security tooling more accessible to everyone, not jus
|
|||||||
- **[Infisical PKI Issuer for Kubernetes](https://infisical.com/docs/documentation/platform/pki/pki-issuer)**: Deliver TLS certificates to your Kubernetes workloads with automatic renewal.
|
- **[Infisical PKI Issuer for Kubernetes](https://infisical.com/docs/documentation/platform/pki/pki-issuer)**: Deliver TLS certificates to your Kubernetes workloads with automatic renewal.
|
||||||
- **[Enrollment over Secure Transport](https://infisical.com/docs/documentation/platform/pki/est)**: Enroll and manage certificates via EST protocol.
|
- **[Enrollment over Secure Transport](https://infisical.com/docs/documentation/platform/pki/est)**: Enroll and manage certificates via EST protocol.
|
||||||
|
|
||||||
|
### Key Management (KMS):
|
||||||
|
|
||||||
|
- **[Cryptograhic Keys](https://infisical.com/docs/documentation/platform/kms)**: Centrally manage keys across projects through a user-friendly interface or via the API.
|
||||||
|
- **[Encrypt and Decrypt Data](https://infisical.com/docs/documentation/platform/kms#guide-to-encrypting-data)**: Use symmetric keys to encrypt and decrypt data.
|
||||||
|
|
||||||
### General Platform:
|
### General Platform:
|
||||||
- **Authentication Methods**: Authenticate machine identities with Infisical using a cloud-native or platform agnostic authentication method ([Kubernetes Auth](https://infisical.com/docs/documentation/platform/identities/kubernetes-auth), [GCP Auth](https://infisical.com/docs/documentation/platform/identities/gcp-auth), [Azure Auth](https://infisical.com/docs/documentation/platform/identities/azure-auth), [AWS Auth](https://infisical.com/docs/documentation/platform/identities/aws-auth), [OIDC Auth](https://infisical.com/docs/documentation/platform/identities/oidc-auth/general), [Universal Auth](https://infisical.com/docs/documentation/platform/identities/universal-auth)).
|
- **Authentication Methods**: Authenticate machine identities with Infisical using a cloud-native or platform agnostic authentication method ([Kubernetes Auth](https://infisical.com/docs/documentation/platform/identities/kubernetes-auth), [GCP Auth](https://infisical.com/docs/documentation/platform/identities/gcp-auth), [Azure Auth](https://infisical.com/docs/documentation/platform/identities/azure-auth), [AWS Auth](https://infisical.com/docs/documentation/platform/identities/aws-auth), [OIDC Auth](https://infisical.com/docs/documentation/platform/identities/oidc-auth/general), [Universal Auth](https://infisical.com/docs/documentation/platform/identities/universal-auth)).
|
||||||
- **[Access Controls](https://infisical.com/docs/documentation/platform/access-controls/overview)**: Define advanced authorization controls for users and machine identities with [RBAC](https://infisical.com/docs/documentation/platform/access-controls/role-based-access-controls), [additional privileges](https://infisical.com/docs/documentation/platform/access-controls/additional-privileges), [temporary access](https://infisical.com/docs/documentation/platform/access-controls/temporary-access), [access requests](https://infisical.com/docs/documentation/platform/access-controls/access-requests), [approval workflows](https://infisical.com/docs/documentation/platform/pr-workflows), and more.
|
- **[Access Controls](https://infisical.com/docs/documentation/platform/access-controls/overview)**: Define advanced authorization controls for users and machine identities with [RBAC](https://infisical.com/docs/documentation/platform/access-controls/role-based-access-controls), [additional privileges](https://infisical.com/docs/documentation/platform/access-controls/additional-privileges), [temporary access](https://infisical.com/docs/documentation/platform/access-controls/temporary-access), [access requests](https://infisical.com/docs/documentation/platform/access-controls/access-requests), [approval workflows](https://infisical.com/docs/documentation/platform/pr-workflows), and more.
|
||||||
|
|||||||
@@ -61,6 +61,7 @@
|
|||||||
"jwks-rsa": "^3.1.0",
|
"jwks-rsa": "^3.1.0",
|
||||||
"knex": "^3.0.1",
|
"knex": "^3.0.1",
|
||||||
"ldapjs": "^3.0.7",
|
"ldapjs": "^3.0.7",
|
||||||
|
"ldif": "^0.5.1",
|
||||||
"libsodium-wrappers": "^0.7.13",
|
"libsodium-wrappers": "^0.7.13",
|
||||||
"lodash.isequal": "^4.5.0",
|
"lodash.isequal": "^4.5.0",
|
||||||
"mongodb": "^6.8.1",
|
"mongodb": "^6.8.1",
|
||||||
@@ -13017,6 +13018,12 @@
|
|||||||
"verror": "^1.10.1"
|
"verror": "^1.10.1"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/ldif": {
|
||||||
|
"version": "0.5.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/ldif/-/ldif-0.5.1.tgz",
|
||||||
|
"integrity": "sha512-8s46m/r2lSFO2+DqMxqWiJ10iiL4tuR5LC/KndV+E5//OAOzOx5s3HS5O34PJ5+kyaCA+K2oCaEPaDRfXUnQow==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/leven": {
|
"node_modules/leven": {
|
||||||
"version": "2.1.0",
|
"version": "2.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/leven/-/leven-2.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/leven/-/leven-2.1.0.tgz",
|
||||||
|
|||||||
@@ -165,6 +165,7 @@
|
|||||||
"jwks-rsa": "^3.1.0",
|
"jwks-rsa": "^3.1.0",
|
||||||
"knex": "^3.0.1",
|
"knex": "^3.0.1",
|
||||||
"ldapjs": "^3.0.7",
|
"ldapjs": "^3.0.7",
|
||||||
|
"ldif": "0.5.1",
|
||||||
"libsodium-wrappers": "^0.7.13",
|
"libsodium-wrappers": "^0.7.13",
|
||||||
"lodash.isequal": "^4.5.0",
|
"lodash.isequal": "^4.5.0",
|
||||||
"mongodb": "^6.8.1",
|
"mongodb": "^6.8.1",
|
||||||
|
|||||||
@@ -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,4 @@
|
|||||||
|
declare module "ldif" {
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Untyped, the function returns `any`.
|
||||||
|
function parse(input: string, ...args: any[]): any;
|
||||||
|
}
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
import { Knex } from "knex";
|
||||||
|
|
||||||
|
import { TableName } from "../schemas";
|
||||||
|
|
||||||
|
export async function up(knex: Knex): Promise<void> {
|
||||||
|
if (await knex.schema.hasTable(TableName.SecretSharing)) {
|
||||||
|
await knex.schema.alterTable(TableName.SecretSharing, (t) => {
|
||||||
|
t.string("iv").nullable().alter();
|
||||||
|
t.string("tag").nullable().alter();
|
||||||
|
t.string("encryptedValue").nullable().alter();
|
||||||
|
|
||||||
|
t.binary("encryptedSecret").nullable();
|
||||||
|
t.string("hashedHex").nullable().alter();
|
||||||
|
|
||||||
|
t.string("identifier", 64).nullable();
|
||||||
|
t.unique("identifier");
|
||||||
|
t.index("identifier");
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function down(knex: Knex): Promise<void> {
|
||||||
|
if (await knex.schema.hasTable(TableName.SecretSharing)) {
|
||||||
|
await knex.schema.alterTable(TableName.SecretSharing, (t) => {
|
||||||
|
t.dropColumn("encryptedSecret");
|
||||||
|
|
||||||
|
t.dropColumn("identifier");
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,46 @@
|
|||||||
|
import { Knex } from "knex";
|
||||||
|
|
||||||
|
import { dropConstraintIfExists } from "@app/db/migrations/utils/dropConstraintIfExists";
|
||||||
|
import { TableName } from "@app/db/schemas";
|
||||||
|
|
||||||
|
export async function up(knex: Knex): Promise<void> {
|
||||||
|
if (await knex.schema.hasTable(TableName.KmsKey)) {
|
||||||
|
const hasOrgId = await knex.schema.hasColumn(TableName.KmsKey, "orgId");
|
||||||
|
const hasSlug = await knex.schema.hasColumn(TableName.KmsKey, "slug");
|
||||||
|
|
||||||
|
// drop constraint if exists (won't exist if rolled back, see below)
|
||||||
|
await dropConstraintIfExists(TableName.KmsKey, "kms_keys_orgid_slug_unique", knex);
|
||||||
|
|
||||||
|
// projectId for CMEK functionality
|
||||||
|
await knex.schema.alterTable(TableName.KmsKey, (table) => {
|
||||||
|
table.string("projectId").nullable().references("id").inTable(TableName.Project).onDelete("CASCADE");
|
||||||
|
|
||||||
|
if (hasOrgId) {
|
||||||
|
table.unique(["orgId", "projectId", "slug"]);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (hasSlug) {
|
||||||
|
table.renameColumn("slug", "name");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function down(knex: Knex): Promise<void> {
|
||||||
|
if (await knex.schema.hasTable(TableName.KmsKey)) {
|
||||||
|
const hasOrgId = await knex.schema.hasColumn(TableName.KmsKey, "orgId");
|
||||||
|
const hasName = await knex.schema.hasColumn(TableName.KmsKey, "name");
|
||||||
|
|
||||||
|
// remove projectId for CMEK functionality
|
||||||
|
await knex.schema.alterTable(TableName.KmsKey, (table) => {
|
||||||
|
if (hasName) {
|
||||||
|
table.renameColumn("name", "slug");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (hasOrgId) {
|
||||||
|
table.dropUnique(["orgId", "projectId", "slug"]);
|
||||||
|
}
|
||||||
|
table.dropColumn("projectId");
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
import { Knex } from "knex";
|
||||||
|
|
||||||
|
import { TableName } from "@app/db/schemas";
|
||||||
|
|
||||||
|
export const dropConstraintIfExists = (tableName: TableName, constraintName: string, knex: Knex) =>
|
||||||
|
knex.raw(`ALTER TABLE ${tableName} DROP CONSTRAINT IF EXISTS ${constraintName};`);
|
||||||
@@ -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>;
|
||||||
|
|||||||
@@ -5,14 +5,16 @@
|
|||||||
|
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
|
|
||||||
|
import { zodBuffer } from "@app/lib/zod";
|
||||||
|
|
||||||
import { TImmutableDBKeys } from "./models";
|
import { TImmutableDBKeys } from "./models";
|
||||||
|
|
||||||
export const SecretSharingSchema = z.object({
|
export const SecretSharingSchema = z.object({
|
||||||
id: z.string().uuid(),
|
id: z.string().uuid(),
|
||||||
encryptedValue: z.string(),
|
encryptedValue: z.string().nullable().optional(),
|
||||||
iv: z.string(),
|
iv: z.string().nullable().optional(),
|
||||||
tag: z.string(),
|
tag: z.string().nullable().optional(),
|
||||||
hashedHex: z.string(),
|
hashedHex: z.string().nullable().optional(),
|
||||||
expiresAt: z.date(),
|
expiresAt: z.date(),
|
||||||
userId: z.string().uuid().nullable().optional(),
|
userId: z.string().uuid().nullable().optional(),
|
||||||
orgId: z.string().uuid().nullable().optional(),
|
orgId: z.string().uuid().nullable().optional(),
|
||||||
@@ -22,7 +24,9 @@ export const SecretSharingSchema = z.object({
|
|||||||
accessType: z.string().default("anyone"),
|
accessType: z.string().default("anyone"),
|
||||||
name: z.string().nullable().optional(),
|
name: z.string().nullable().optional(),
|
||||||
lastViewedAt: z.date().nullable().optional(),
|
lastViewedAt: z.date().nullable().optional(),
|
||||||
password: z.string().nullable().optional()
|
password: z.string().nullable().optional(),
|
||||||
|
encryptedSecret: zodBuffer.nullable().optional(),
|
||||||
|
identifier: z.string().nullable().optional()
|
||||||
});
|
});
|
||||||
|
|
||||||
export type TSecretSharing = z.infer<typeof SecretSharingSchema>;
|
export type TSecretSharing = z.infer<typeof SecretSharingSchema>;
|
||||||
|
|||||||
@@ -26,7 +26,7 @@ const sanitizedExternalSchemaForGetAll = KmsKeysSchema.pick({
|
|||||||
isDisabled: true,
|
isDisabled: true,
|
||||||
createdAt: true,
|
createdAt: true,
|
||||||
updatedAt: true,
|
updatedAt: true,
|
||||||
slug: true
|
name: true
|
||||||
})
|
})
|
||||||
.extend({
|
.extend({
|
||||||
externalKms: ExternalKmsSchema.pick({
|
externalKms: ExternalKmsSchema.pick({
|
||||||
@@ -57,7 +57,7 @@ export const registerExternalKmsRouter = async (server: FastifyZodProvider) => {
|
|||||||
},
|
},
|
||||||
schema: {
|
schema: {
|
||||||
body: z.object({
|
body: z.object({
|
||||||
slug: z.string().min(1).trim().toLowerCase(),
|
name: z.string().min(1).trim().toLowerCase(),
|
||||||
description: z.string().trim().optional(),
|
description: z.string().trim().optional(),
|
||||||
provider: ExternalKmsInputSchema
|
provider: ExternalKmsInputSchema
|
||||||
}),
|
}),
|
||||||
@@ -74,7 +74,7 @@ export const registerExternalKmsRouter = async (server: FastifyZodProvider) => {
|
|||||||
actorId: req.permission.id,
|
actorId: req.permission.id,
|
||||||
actorAuthMethod: req.permission.authMethod,
|
actorAuthMethod: req.permission.authMethod,
|
||||||
actorOrgId: req.permission.orgId,
|
actorOrgId: req.permission.orgId,
|
||||||
slug: req.body.slug,
|
name: req.body.name,
|
||||||
provider: req.body.provider,
|
provider: req.body.provider,
|
||||||
description: req.body.description
|
description: req.body.description
|
||||||
});
|
});
|
||||||
@@ -87,7 +87,7 @@ export const registerExternalKmsRouter = async (server: FastifyZodProvider) => {
|
|||||||
metadata: {
|
metadata: {
|
||||||
kmsId: externalKms.id,
|
kmsId: externalKms.id,
|
||||||
provider: req.body.provider.type,
|
provider: req.body.provider.type,
|
||||||
slug: req.body.slug,
|
name: req.body.name,
|
||||||
description: req.body.description
|
description: req.body.description
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -108,7 +108,7 @@ export const registerExternalKmsRouter = async (server: FastifyZodProvider) => {
|
|||||||
id: z.string().trim().min(1)
|
id: z.string().trim().min(1)
|
||||||
}),
|
}),
|
||||||
body: z.object({
|
body: z.object({
|
||||||
slug: z.string().min(1).trim().toLowerCase().optional(),
|
name: z.string().min(1).trim().toLowerCase().optional(),
|
||||||
description: z.string().trim().optional(),
|
description: z.string().trim().optional(),
|
||||||
provider: ExternalKmsInputUpdateSchema
|
provider: ExternalKmsInputUpdateSchema
|
||||||
}),
|
}),
|
||||||
@@ -125,7 +125,7 @@ export const registerExternalKmsRouter = async (server: FastifyZodProvider) => {
|
|||||||
actorId: req.permission.id,
|
actorId: req.permission.id,
|
||||||
actorAuthMethod: req.permission.authMethod,
|
actorAuthMethod: req.permission.authMethod,
|
||||||
actorOrgId: req.permission.orgId,
|
actorOrgId: req.permission.orgId,
|
||||||
slug: req.body.slug,
|
name: req.body.name,
|
||||||
provider: req.body.provider,
|
provider: req.body.provider,
|
||||||
description: req.body.description,
|
description: req.body.description,
|
||||||
id: req.params.id
|
id: req.params.id
|
||||||
@@ -139,7 +139,7 @@ export const registerExternalKmsRouter = async (server: FastifyZodProvider) => {
|
|||||||
metadata: {
|
metadata: {
|
||||||
kmsId: externalKms.id,
|
kmsId: externalKms.id,
|
||||||
provider: req.body.provider.type,
|
provider: req.body.provider.type,
|
||||||
slug: req.body.slug,
|
name: req.body.name,
|
||||||
description: req.body.description
|
description: req.body.description
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -182,7 +182,7 @@ export const registerExternalKmsRouter = async (server: FastifyZodProvider) => {
|
|||||||
type: EventType.DELETE_KMS,
|
type: EventType.DELETE_KMS,
|
||||||
metadata: {
|
metadata: {
|
||||||
kmsId: externalKms.id,
|
kmsId: externalKms.id,
|
||||||
slug: externalKms.slug
|
name: externalKms.name
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -224,7 +224,7 @@ export const registerExternalKmsRouter = async (server: FastifyZodProvider) => {
|
|||||||
type: EventType.GET_KMS,
|
type: EventType.GET_KMS,
|
||||||
metadata: {
|
metadata: {
|
||||||
kmsId: externalKms.id,
|
kmsId: externalKms.id,
|
||||||
slug: externalKms.slug
|
name: externalKms.name
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -260,13 +260,13 @@ export const registerExternalKmsRouter = async (server: FastifyZodProvider) => {
|
|||||||
|
|
||||||
server.route({
|
server.route({
|
||||||
method: "GET",
|
method: "GET",
|
||||||
url: "/slug/:slug",
|
url: "/name/:name",
|
||||||
config: {
|
config: {
|
||||||
rateLimit: readLimit
|
rateLimit: readLimit
|
||||||
},
|
},
|
||||||
schema: {
|
schema: {
|
||||||
params: z.object({
|
params: z.object({
|
||||||
slug: z.string().trim().min(1)
|
name: z.string().trim().min(1)
|
||||||
}),
|
}),
|
||||||
response: {
|
response: {
|
||||||
200: z.object({
|
200: z.object({
|
||||||
@@ -276,12 +276,12 @@ export const registerExternalKmsRouter = async (server: FastifyZodProvider) => {
|
|||||||
},
|
},
|
||||||
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
||||||
handler: async (req) => {
|
handler: async (req) => {
|
||||||
const externalKms = await server.services.externalKms.findBySlug({
|
const externalKms = await server.services.externalKms.findByName({
|
||||||
actor: req.permission.type,
|
actor: req.permission.type,
|
||||||
actorId: req.permission.id,
|
actorId: req.permission.id,
|
||||||
actorAuthMethod: req.permission.authMethod,
|
actorAuthMethod: req.permission.authMethod,
|
||||||
actorOrgId: req.permission.orgId,
|
actorOrgId: req.permission.orgId,
|
||||||
slug: req.params.slug
|
name: req.params.name
|
||||||
});
|
});
|
||||||
return { externalKms };
|
return { externalKms };
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import { AwsIamProvider } from "./aws-iam";
|
|||||||
import { AzureEntraIDProvider } from "./azure-entra-id";
|
import { AzureEntraIDProvider } from "./azure-entra-id";
|
||||||
import { CassandraProvider } from "./cassandra";
|
import { CassandraProvider } from "./cassandra";
|
||||||
import { ElasticSearchProvider } from "./elastic-search";
|
import { ElasticSearchProvider } from "./elastic-search";
|
||||||
|
import { LdapProvider } from "./ldap";
|
||||||
import { DynamicSecretProviders } from "./models";
|
import { DynamicSecretProviders } from "./models";
|
||||||
import { MongoAtlasProvider } from "./mongo-atlas";
|
import { MongoAtlasProvider } from "./mongo-atlas";
|
||||||
import { MongoDBProvider } from "./mongo-db";
|
import { MongoDBProvider } from "./mongo-db";
|
||||||
@@ -20,5 +21,6 @@ export const buildDynamicSecretProviders = () => ({
|
|||||||
[DynamicSecretProviders.MongoDB]: MongoDBProvider(),
|
[DynamicSecretProviders.MongoDB]: MongoDBProvider(),
|
||||||
[DynamicSecretProviders.ElasticSearch]: ElasticSearchProvider(),
|
[DynamicSecretProviders.ElasticSearch]: ElasticSearchProvider(),
|
||||||
[DynamicSecretProviders.RabbitMq]: RabbitMqProvider(),
|
[DynamicSecretProviders.RabbitMq]: RabbitMqProvider(),
|
||||||
[DynamicSecretProviders.AzureEntraID]: AzureEntraIDProvider()
|
[DynamicSecretProviders.AzureEntraID]: AzureEntraIDProvider(),
|
||||||
|
[DynamicSecretProviders.Ldap]: LdapProvider()
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -0,0 +1,235 @@
|
|||||||
|
import { compile } from "handlebars";
|
||||||
|
import ldapjs from "ldapjs";
|
||||||
|
import ldif from "ldif";
|
||||||
|
import { customAlphabet } from "nanoid";
|
||||||
|
import { z } from "zod";
|
||||||
|
|
||||||
|
import { BadRequestError } from "@app/lib/errors";
|
||||||
|
import { alphaNumericNanoId } from "@app/lib/nanoid";
|
||||||
|
|
||||||
|
import { LdapSchema, TDynamicProviderFns } from "./models";
|
||||||
|
|
||||||
|
const generatePassword = () => {
|
||||||
|
const charset = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-_.~!*$#";
|
||||||
|
return customAlphabet(charset, 64)();
|
||||||
|
};
|
||||||
|
|
||||||
|
const encodePassword = (password?: string) => {
|
||||||
|
const quotedPassword = `"${password}"`;
|
||||||
|
const utf16lePassword = Buffer.from(quotedPassword, "utf16le");
|
||||||
|
const base64Password = utf16lePassword.toString("base64");
|
||||||
|
return base64Password;
|
||||||
|
};
|
||||||
|
|
||||||
|
const generateUsername = () => {
|
||||||
|
return alphaNumericNanoId(20);
|
||||||
|
};
|
||||||
|
|
||||||
|
const generateLDIF = ({
|
||||||
|
username,
|
||||||
|
password,
|
||||||
|
ldifTemplate
|
||||||
|
}: {
|
||||||
|
username: string;
|
||||||
|
password?: string;
|
||||||
|
ldifTemplate: string;
|
||||||
|
}): string => {
|
||||||
|
const data = {
|
||||||
|
Username: username,
|
||||||
|
Password: password,
|
||||||
|
EncodedPassword: encodePassword(password)
|
||||||
|
};
|
||||||
|
|
||||||
|
const renderTemplate = compile(ldifTemplate);
|
||||||
|
const renderedLdif = renderTemplate(data);
|
||||||
|
|
||||||
|
return renderedLdif;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const LdapProvider = (): TDynamicProviderFns => {
|
||||||
|
const validateProviderInputs = async (inputs: unknown) => {
|
||||||
|
const providerInputs = await LdapSchema.parseAsync(inputs);
|
||||||
|
return providerInputs;
|
||||||
|
};
|
||||||
|
|
||||||
|
const getClient = async (providerInputs: z.infer<typeof LdapSchema>): Promise<ldapjs.Client> => {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
const client = ldapjs.createClient({
|
||||||
|
url: providerInputs.url,
|
||||||
|
tlsOptions: {
|
||||||
|
ca: providerInputs.ca ? providerInputs.ca : null,
|
||||||
|
rejectUnauthorized: !!providerInputs.ca
|
||||||
|
},
|
||||||
|
reconnect: true,
|
||||||
|
bindDN: providerInputs.binddn,
|
||||||
|
bindCredentials: providerInputs.bindpass
|
||||||
|
});
|
||||||
|
|
||||||
|
client.on("error", (err: Error) => {
|
||||||
|
client.unbind();
|
||||||
|
reject(new BadRequestError({ message: err.message }));
|
||||||
|
});
|
||||||
|
|
||||||
|
client.bind(providerInputs.binddn, providerInputs.bindpass, (err) => {
|
||||||
|
if (err) {
|
||||||
|
client.unbind();
|
||||||
|
reject(new BadRequestError({ message: err.message }));
|
||||||
|
} else {
|
||||||
|
resolve(client);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const validateConnection = async (inputs: unknown) => {
|
||||||
|
const providerInputs = await validateProviderInputs(inputs);
|
||||||
|
const client = await getClient(providerInputs);
|
||||||
|
return client.connected;
|
||||||
|
};
|
||||||
|
|
||||||
|
const executeLdif = async (client: ldapjs.Client, ldif_file: string) => {
|
||||||
|
type TEntry = {
|
||||||
|
dn: string;
|
||||||
|
type: string;
|
||||||
|
|
||||||
|
changes: {
|
||||||
|
operation?: string;
|
||||||
|
attribute: {
|
||||||
|
attribute: string;
|
||||||
|
};
|
||||||
|
value: {
|
||||||
|
value: string;
|
||||||
|
};
|
||||||
|
values: {
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Untyped, can be any for ldapjs.Change.modification.values
|
||||||
|
value: any;
|
||||||
|
}[];
|
||||||
|
}[];
|
||||||
|
};
|
||||||
|
|
||||||
|
let parsedEntries: TEntry[];
|
||||||
|
|
||||||
|
try {
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
|
||||||
|
parsedEntries = ldif.parse(ldif_file).entries as TEntry[];
|
||||||
|
} catch (err) {
|
||||||
|
throw new BadRequestError({
|
||||||
|
message: "Invalid LDIF format, refer to the documentation at Dynamic secrets > LDAP > LDIF Entries."
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const dnArray: string[] = [];
|
||||||
|
|
||||||
|
for await (const entry of parsedEntries) {
|
||||||
|
const { dn } = entry;
|
||||||
|
let responseDn: string;
|
||||||
|
|
||||||
|
if (entry.type === "add") {
|
||||||
|
const attributes: Record<string, string | string[]> = {};
|
||||||
|
|
||||||
|
entry.changes.forEach((change) => {
|
||||||
|
const attrName = change.attribute.attribute;
|
||||||
|
const attrValue = change.value.value;
|
||||||
|
|
||||||
|
attributes[attrName] = Array.isArray(attrValue) ? attrValue : [attrValue];
|
||||||
|
});
|
||||||
|
|
||||||
|
responseDn = await new Promise((resolve, reject) => {
|
||||||
|
client.add(dn, attributes, (err) => {
|
||||||
|
if (err) {
|
||||||
|
reject(new BadRequestError({ message: err.message }));
|
||||||
|
} else {
|
||||||
|
resolve(dn);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
} else if (entry.type === "modify") {
|
||||||
|
const changes: ldapjs.Change[] = [];
|
||||||
|
|
||||||
|
entry.changes.forEach((change) => {
|
||||||
|
changes.push(
|
||||||
|
new ldapjs.Change({
|
||||||
|
operation: change.operation || "replace",
|
||||||
|
modification: {
|
||||||
|
type: change.attribute.attribute,
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-unsafe-return
|
||||||
|
values: change.values.map((value) => value.value)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
responseDn = await new Promise((resolve, reject) => {
|
||||||
|
client.modify(dn, changes, (err) => {
|
||||||
|
if (err) {
|
||||||
|
reject(new BadRequestError({ message: err.message }));
|
||||||
|
} else {
|
||||||
|
resolve(dn);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
} else if (entry.type === "delete") {
|
||||||
|
responseDn = await new Promise((resolve, reject) => {
|
||||||
|
client.del(dn, (err) => {
|
||||||
|
if (err) {
|
||||||
|
reject(new BadRequestError({ message: err.message }));
|
||||||
|
} else {
|
||||||
|
resolve(dn);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
client.unbind();
|
||||||
|
throw new BadRequestError({ message: `Unsupported operation type ${entry.type}` });
|
||||||
|
}
|
||||||
|
|
||||||
|
dnArray.push(responseDn);
|
||||||
|
}
|
||||||
|
client.unbind();
|
||||||
|
return dnArray;
|
||||||
|
};
|
||||||
|
|
||||||
|
const create = async (inputs: unknown) => {
|
||||||
|
const providerInputs = await validateProviderInputs(inputs);
|
||||||
|
const client = await getClient(providerInputs);
|
||||||
|
|
||||||
|
const username = generateUsername();
|
||||||
|
const password = generatePassword();
|
||||||
|
const generatedLdif = generateLDIF({ username, password, ldifTemplate: providerInputs.creationLdif });
|
||||||
|
|
||||||
|
try {
|
||||||
|
const dnArray = await executeLdif(client, generatedLdif);
|
||||||
|
|
||||||
|
return { entityId: username, data: { DN_ARRAY: dnArray, USERNAME: username, PASSWORD: password } };
|
||||||
|
} catch (err) {
|
||||||
|
if (providerInputs.rollbackLdif) {
|
||||||
|
const rollbackLdif = generateLDIF({ username, password, ldifTemplate: providerInputs.rollbackLdif });
|
||||||
|
await executeLdif(client, rollbackLdif);
|
||||||
|
}
|
||||||
|
throw new BadRequestError({ message: (err as Error).message });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const revoke = async (inputs: unknown, entityId: string) => {
|
||||||
|
const providerInputs = await validateProviderInputs(inputs);
|
||||||
|
const connection = await getClient(providerInputs);
|
||||||
|
const revocationLdif = generateLDIF({ username: entityId, ldifTemplate: providerInputs.revocationLdif });
|
||||||
|
|
||||||
|
await executeLdif(connection, revocationLdif);
|
||||||
|
|
||||||
|
return { entityId };
|
||||||
|
};
|
||||||
|
|
||||||
|
const renew = async (inputs: unknown, entityId: string) => {
|
||||||
|
// Do nothing
|
||||||
|
return { entityId };
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
validateProviderInputs,
|
||||||
|
validateConnection,
|
||||||
|
create,
|
||||||
|
revoke,
|
||||||
|
renew
|
||||||
|
};
|
||||||
|
};
|
||||||
@@ -174,6 +174,17 @@ export const AzureEntraIDSchema = z.object({
|
|||||||
clientSecret: z.string().trim().min(1)
|
clientSecret: z.string().trim().min(1)
|
||||||
});
|
});
|
||||||
|
|
||||||
|
export const LdapSchema = z.object({
|
||||||
|
url: z.string().trim().min(1),
|
||||||
|
binddn: z.string().trim().min(1),
|
||||||
|
bindpass: z.string().trim().min(1),
|
||||||
|
ca: z.string().optional(),
|
||||||
|
|
||||||
|
creationLdif: z.string().min(1),
|
||||||
|
revocationLdif: z.string().min(1),
|
||||||
|
rollbackLdif: z.string().optional()
|
||||||
|
});
|
||||||
|
|
||||||
export enum DynamicSecretProviders {
|
export enum DynamicSecretProviders {
|
||||||
SqlDatabase = "sql-database",
|
SqlDatabase = "sql-database",
|
||||||
Cassandra = "cassandra",
|
Cassandra = "cassandra",
|
||||||
@@ -184,7 +195,8 @@ export enum DynamicSecretProviders {
|
|||||||
ElasticSearch = "elastic-search",
|
ElasticSearch = "elastic-search",
|
||||||
MongoDB = "mongo-db",
|
MongoDB = "mongo-db",
|
||||||
RabbitMq = "rabbit-mq",
|
RabbitMq = "rabbit-mq",
|
||||||
AzureEntraID = "azure-entra-id"
|
AzureEntraID = "azure-entra-id",
|
||||||
|
Ldap = "ldap"
|
||||||
}
|
}
|
||||||
|
|
||||||
export const DynamicSecretProviderSchema = z.discriminatedUnion("type", [
|
export const DynamicSecretProviderSchema = z.discriminatedUnion("type", [
|
||||||
@@ -197,7 +209,8 @@ export const DynamicSecretProviderSchema = z.discriminatedUnion("type", [
|
|||||||
z.object({ type: z.literal(DynamicSecretProviders.ElasticSearch), inputs: DynamicSecretElasticSearchSchema }),
|
z.object({ type: z.literal(DynamicSecretProviders.ElasticSearch), inputs: DynamicSecretElasticSearchSchema }),
|
||||||
z.object({ type: z.literal(DynamicSecretProviders.MongoDB), inputs: DynamicSecretMongoDBSchema }),
|
z.object({ type: z.literal(DynamicSecretProviders.MongoDB), inputs: DynamicSecretMongoDBSchema }),
|
||||||
z.object({ type: z.literal(DynamicSecretProviders.RabbitMq), inputs: DynamicSecretRabbitMqSchema }),
|
z.object({ type: z.literal(DynamicSecretProviders.RabbitMq), inputs: DynamicSecretRabbitMqSchema }),
|
||||||
z.object({ type: z.literal(DynamicSecretProviders.AzureEntraID), inputs: AzureEntraIDSchema })
|
z.object({ type: z.literal(DynamicSecretProviders.AzureEntraID), inputs: AzureEntraIDSchema }),
|
||||||
|
z.object({ type: z.literal(DynamicSecretProviders.Ldap), inputs: LdapSchema })
|
||||||
]);
|
]);
|
||||||
|
|
||||||
export type TDynamicProviderFns = {
|
export type TDynamicProviderFns = {
|
||||||
|
|||||||
@@ -30,7 +30,7 @@ export const externalKmsDALFactory = (db: TDbClient) => {
|
|||||||
isDisabled: el.isDisabled,
|
isDisabled: el.isDisabled,
|
||||||
isReserved: el.isReserved,
|
isReserved: el.isReserved,
|
||||||
orgId: el.orgId,
|
orgId: el.orgId,
|
||||||
slug: el.slug,
|
name: el.name,
|
||||||
createdAt: el.createdAt,
|
createdAt: el.createdAt,
|
||||||
updatedAt: el.updatedAt,
|
updatedAt: el.updatedAt,
|
||||||
externalKms: {
|
externalKms: {
|
||||||
|
|||||||
@@ -43,7 +43,7 @@ export const externalKmsServiceFactory = ({
|
|||||||
provider,
|
provider,
|
||||||
description,
|
description,
|
||||||
actor,
|
actor,
|
||||||
slug,
|
name,
|
||||||
actorId,
|
actorId,
|
||||||
actorOrgId,
|
actorOrgId,
|
||||||
actorAuthMethod
|
actorAuthMethod
|
||||||
@@ -64,7 +64,7 @@ export const externalKmsServiceFactory = ({
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
const kmsSlug = slug ? slugify(slug) : slugify(alphaNumericNanoId(8).toLowerCase());
|
const kmsName = name ? slugify(name) : slugify(alphaNumericNanoId(8).toLowerCase());
|
||||||
|
|
||||||
let sanitizedProviderInput = "";
|
let sanitizedProviderInput = "";
|
||||||
switch (provider.type) {
|
switch (provider.type) {
|
||||||
@@ -96,7 +96,7 @@ export const externalKmsServiceFactory = ({
|
|||||||
{
|
{
|
||||||
isReserved: false,
|
isReserved: false,
|
||||||
description,
|
description,
|
||||||
slug: kmsSlug,
|
name: kmsName,
|
||||||
orgId: actorOrgId
|
orgId: actorOrgId
|
||||||
},
|
},
|
||||||
tx
|
tx
|
||||||
@@ -120,7 +120,7 @@ export const externalKmsServiceFactory = ({
|
|||||||
description,
|
description,
|
||||||
actor,
|
actor,
|
||||||
id: kmsId,
|
id: kmsId,
|
||||||
slug,
|
name,
|
||||||
actorId,
|
actorId,
|
||||||
actorOrgId,
|
actorOrgId,
|
||||||
actorAuthMethod
|
actorAuthMethod
|
||||||
@@ -142,7 +142,7 @@ export const externalKmsServiceFactory = ({
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
const kmsSlug = slug ? slugify(slug) : undefined;
|
const kmsName = name ? slugify(name) : undefined;
|
||||||
|
|
||||||
const externalKmsDoc = await externalKmsDAL.findOne({ kmsKeyId: kmsDoc.id });
|
const externalKmsDoc = await externalKmsDAL.findOne({ kmsKeyId: kmsDoc.id });
|
||||||
if (!externalKmsDoc) throw new NotFoundError({ message: "External kms not found" });
|
if (!externalKmsDoc) throw new NotFoundError({ message: "External kms not found" });
|
||||||
@@ -188,7 +188,7 @@ export const externalKmsServiceFactory = ({
|
|||||||
kmsDoc.id,
|
kmsDoc.id,
|
||||||
{
|
{
|
||||||
description,
|
description,
|
||||||
slug: kmsSlug
|
name: kmsName
|
||||||
},
|
},
|
||||||
tx
|
tx
|
||||||
);
|
);
|
||||||
@@ -280,14 +280,14 @@ export const externalKmsServiceFactory = ({
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const findBySlug = async ({
|
const findByName = async ({
|
||||||
actor,
|
actor,
|
||||||
actorId,
|
actorId,
|
||||||
actorOrgId,
|
actorOrgId,
|
||||||
actorAuthMethod,
|
actorAuthMethod,
|
||||||
slug: kmsSlug
|
name: kmsName
|
||||||
}: TGetExternalKmsBySlugDTO) => {
|
}: TGetExternalKmsBySlugDTO) => {
|
||||||
const kmsDoc = await kmsDAL.findOne({ slug: kmsSlug, orgId: actorOrgId });
|
const kmsDoc = await kmsDAL.findOne({ name: kmsName, orgId: actorOrgId });
|
||||||
const { permission } = await permissionService.getOrgPermission(
|
const { permission } = await permissionService.getOrgPermission(
|
||||||
actor,
|
actor,
|
||||||
actorId,
|
actorId,
|
||||||
@@ -327,6 +327,6 @@ export const externalKmsServiceFactory = ({
|
|||||||
deleteById,
|
deleteById,
|
||||||
list,
|
list,
|
||||||
findById,
|
findById,
|
||||||
findBySlug
|
findByName
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -3,14 +3,14 @@ import { TOrgPermission } from "@app/lib/types";
|
|||||||
import { TExternalKmsInputSchema, TExternalKmsInputUpdateSchema } from "./providers/model";
|
import { TExternalKmsInputSchema, TExternalKmsInputUpdateSchema } from "./providers/model";
|
||||||
|
|
||||||
export type TCreateExternalKmsDTO = {
|
export type TCreateExternalKmsDTO = {
|
||||||
slug?: string;
|
name?: string;
|
||||||
description?: string;
|
description?: string;
|
||||||
provider: TExternalKmsInputSchema;
|
provider: TExternalKmsInputSchema;
|
||||||
} & Omit<TOrgPermission, "orgId">;
|
} & Omit<TOrgPermission, "orgId">;
|
||||||
|
|
||||||
export type TUpdateExternalKmsDTO = {
|
export type TUpdateExternalKmsDTO = {
|
||||||
id: string;
|
id: string;
|
||||||
slug?: string;
|
name?: string;
|
||||||
description?: string;
|
description?: string;
|
||||||
provider?: TExternalKmsInputUpdateSchema;
|
provider?: TExternalKmsInputUpdateSchema;
|
||||||
} & Omit<TOrgPermission, "orgId">;
|
} & Omit<TOrgPermission, "orgId">;
|
||||||
@@ -26,5 +26,5 @@ export type TGetExternalKmsByIdDTO = {
|
|||||||
} & Omit<TOrgPermission, "orgId">;
|
} & Omit<TOrgPermission, "orgId">;
|
||||||
|
|
||||||
export type TGetExternalKmsBySlugDTO = {
|
export type TGetExternalKmsBySlugDTO = {
|
||||||
slug: string;
|
name: string;
|
||||||
} & Omit<TOrgPermission, "orgId">;
|
} & Omit<TOrgPermission, "orgId">;
|
||||||
|
|||||||
@@ -14,6 +14,15 @@ export enum ProjectPermissionActions {
|
|||||||
Delete = "delete"
|
Delete = "delete"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export enum ProjectPermissionCmekActions {
|
||||||
|
Read = "read",
|
||||||
|
Create = "create",
|
||||||
|
Edit = "edit",
|
||||||
|
Delete = "delete",
|
||||||
|
Encrypt = "encrypt",
|
||||||
|
Decrypt = "decrypt"
|
||||||
|
}
|
||||||
|
|
||||||
export enum ProjectPermissionSub {
|
export enum ProjectPermissionSub {
|
||||||
Role = "role",
|
Role = "role",
|
||||||
Member = "member",
|
Member = "member",
|
||||||
@@ -38,7 +47,8 @@ export enum ProjectPermissionSub {
|
|||||||
CertificateTemplates = "certificate-templates",
|
CertificateTemplates = "certificate-templates",
|
||||||
PkiAlerts = "pki-alerts",
|
PkiAlerts = "pki-alerts",
|
||||||
PkiCollections = "pki-collections",
|
PkiCollections = "pki-collections",
|
||||||
Kms = "kms"
|
Kms = "kms",
|
||||||
|
Cmek = "cmek"
|
||||||
}
|
}
|
||||||
|
|
||||||
export type SecretSubjectFields = {
|
export type SecretSubjectFields = {
|
||||||
@@ -95,6 +105,7 @@ export type ProjectPermissionSet =
|
|||||||
| [ProjectPermissionActions, ProjectPermissionSub.CertificateTemplates]
|
| [ProjectPermissionActions, ProjectPermissionSub.CertificateTemplates]
|
||||||
| [ProjectPermissionActions, ProjectPermissionSub.PkiAlerts]
|
| [ProjectPermissionActions, ProjectPermissionSub.PkiAlerts]
|
||||||
| [ProjectPermissionActions, ProjectPermissionSub.PkiCollections]
|
| [ProjectPermissionActions, ProjectPermissionSub.PkiCollections]
|
||||||
|
| [ProjectPermissionCmekActions, ProjectPermissionSub.Cmek]
|
||||||
| [ProjectPermissionActions.Delete, ProjectPermissionSub.Project]
|
| [ProjectPermissionActions.Delete, ProjectPermissionSub.Project]
|
||||||
| [ProjectPermissionActions.Edit, ProjectPermissionSub.Project]
|
| [ProjectPermissionActions.Edit, ProjectPermissionSub.Project]
|
||||||
| [ProjectPermissionActions.Read, ProjectPermissionSub.SecretRollback]
|
| [ProjectPermissionActions.Read, ProjectPermissionSub.SecretRollback]
|
||||||
@@ -282,6 +293,12 @@ export const ProjectPermissionSchema = z.discriminatedUnion("subject", [
|
|||||||
action: CASL_ACTION_SCHEMA_ENUM([ProjectPermissionActions.Read]).describe(
|
action: CASL_ACTION_SCHEMA_ENUM([ProjectPermissionActions.Read]).describe(
|
||||||
"Describe what action an entity can take."
|
"Describe what action an entity can take."
|
||||||
)
|
)
|
||||||
|
}),
|
||||||
|
z.object({
|
||||||
|
subject: z.literal(ProjectPermissionSub.Cmek).describe("The entity this permission pertains to."),
|
||||||
|
action: CASL_ACTION_SCHEMA_NATIVE_ENUM(ProjectPermissionCmekActions).describe(
|
||||||
|
"Describe what action an entity can take."
|
||||||
|
)
|
||||||
})
|
})
|
||||||
]);
|
]);
|
||||||
|
|
||||||
@@ -325,6 +342,17 @@ const buildAdminPermissionRules = () => {
|
|||||||
can([ProjectPermissionActions.Edit, ProjectPermissionActions.Delete], ProjectPermissionSub.Project);
|
can([ProjectPermissionActions.Edit, ProjectPermissionActions.Delete], ProjectPermissionSub.Project);
|
||||||
can([ProjectPermissionActions.Read, ProjectPermissionActions.Create], ProjectPermissionSub.SecretRollback);
|
can([ProjectPermissionActions.Read, ProjectPermissionActions.Create], ProjectPermissionSub.SecretRollback);
|
||||||
can([ProjectPermissionActions.Edit], ProjectPermissionSub.Kms);
|
can([ProjectPermissionActions.Edit], ProjectPermissionSub.Kms);
|
||||||
|
can(
|
||||||
|
[
|
||||||
|
ProjectPermissionCmekActions.Create,
|
||||||
|
ProjectPermissionCmekActions.Edit,
|
||||||
|
ProjectPermissionCmekActions.Delete,
|
||||||
|
ProjectPermissionCmekActions.Read,
|
||||||
|
ProjectPermissionCmekActions.Encrypt,
|
||||||
|
ProjectPermissionCmekActions.Decrypt
|
||||||
|
],
|
||||||
|
ProjectPermissionSub.Cmek
|
||||||
|
);
|
||||||
return rules;
|
return rules;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -444,6 +472,18 @@ const buildMemberPermissionRules = () => {
|
|||||||
can([ProjectPermissionActions.Read], ProjectPermissionSub.PkiAlerts);
|
can([ProjectPermissionActions.Read], ProjectPermissionSub.PkiAlerts);
|
||||||
can([ProjectPermissionActions.Read], ProjectPermissionSub.PkiCollections);
|
can([ProjectPermissionActions.Read], ProjectPermissionSub.PkiCollections);
|
||||||
|
|
||||||
|
can(
|
||||||
|
[
|
||||||
|
ProjectPermissionCmekActions.Create,
|
||||||
|
ProjectPermissionCmekActions.Edit,
|
||||||
|
ProjectPermissionCmekActions.Delete,
|
||||||
|
ProjectPermissionCmekActions.Read,
|
||||||
|
ProjectPermissionCmekActions.Encrypt,
|
||||||
|
ProjectPermissionCmekActions.Decrypt
|
||||||
|
],
|
||||||
|
ProjectPermissionSub.Cmek
|
||||||
|
);
|
||||||
|
|
||||||
return rules;
|
return rules;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -470,6 +510,7 @@ const buildViewerPermissionRules = () => {
|
|||||||
can(ProjectPermissionActions.Read, ProjectPermissionSub.IpAllowList);
|
can(ProjectPermissionActions.Read, ProjectPermissionSub.IpAllowList);
|
||||||
can(ProjectPermissionActions.Read, ProjectPermissionSub.CertificateAuthorities);
|
can(ProjectPermissionActions.Read, ProjectPermissionSub.CertificateAuthorities);
|
||||||
can(ProjectPermissionActions.Read, ProjectPermissionSub.Certificates);
|
can(ProjectPermissionActions.Read, ProjectPermissionSub.Certificates);
|
||||||
|
can(ProjectPermissionCmekActions.Read, ProjectPermissionSub.Cmek);
|
||||||
|
|
||||||
return rules;
|
return rules;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -16,6 +16,9 @@ export const KeyStorePrefixes = {
|
|||||||
WaitUntilReadyKmsOrgKeyCreation: "wait-until-ready-kms-org-key-creation-",
|
WaitUntilReadyKmsOrgKeyCreation: "wait-until-ready-kms-org-key-creation-",
|
||||||
WaitUntilReadyKmsOrgDataKeyCreation: "wait-until-ready-kms-org-data-key-creation-",
|
WaitUntilReadyKmsOrgDataKeyCreation: "wait-until-ready-kms-org-data-key-creation-",
|
||||||
|
|
||||||
|
WaitUntilReadyProjectEnvironmentOperation: (projectId: string) =>
|
||||||
|
`wait-until-ready-project-environments-operation-${projectId}`,
|
||||||
|
ProjectEnvironmentLock: (projectId: string) => `project-environment-lock-${projectId}` as const,
|
||||||
SyncSecretIntegrationLock: (projectId: string, environmentSlug: string, secretPath: string) =>
|
SyncSecretIntegrationLock: (projectId: string, environmentSlug: string, secretPath: string) =>
|
||||||
`sync-integration-mutex-${projectId}-${environmentSlug}-${secretPath}` as const,
|
`sync-integration-mutex-${projectId}-${environmentSlug}-${secretPath}` as const,
|
||||||
SyncSecretIntegrationLastRunTimestamp: (projectId: string, environmentSlug: string, secretPath: string) =>
|
SyncSecretIntegrationLastRunTimestamp: (projectId: string, environmentSlug: string, secretPath: string) =>
|
||||||
|
|||||||
@@ -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,28 @@
|
|||||||
|
// Credit: https://github.com/miguelmota/is-base64
|
||||||
|
export const isBase64 = (
|
||||||
|
v: string,
|
||||||
|
opts = { allowEmpty: false, mimeRequired: false, allowMime: true, paddingRequired: false }
|
||||||
|
) => {
|
||||||
|
if (opts.allowEmpty === false && v === "") {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
let regex = "(?:[A-Za-z0-9+\\/]{4})*(?:[A-Za-z0-9+\\/]{2}==|[A-Za-z0-9+/]{3}=)?";
|
||||||
|
const mimeRegex = "(data:\\w+\\/[a-zA-Z\\+\\-\\.]+;base64,)";
|
||||||
|
|
||||||
|
if (opts.mimeRequired === true) {
|
||||||
|
regex = mimeRegex + regex;
|
||||||
|
} else if (opts.allowMime === true) {
|
||||||
|
regex = `${mimeRegex}?${regex}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (opts.paddingRequired === false) {
|
||||||
|
regex = "(?:[A-Za-z0-9+\\/]{4})*(?:[A-Za-z0-9+\\/]{2}(==)?|[A-Za-z0-9+\\/]{3}=?)?";
|
||||||
|
}
|
||||||
|
|
||||||
|
return new RegExp(`^${regex}$`, "gi").test(v);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getBase64SizeInBytes = (base64String: string) => {
|
||||||
|
return Buffer.from(base64String, "base64").length;
|
||||||
|
};
|
||||||
@@ -23,8 +23,19 @@ export const conditionsMatcher = buildMongoQueryMatcher({ $glob }, { glob });
|
|||||||
/**
|
/**
|
||||||
* Extracts and formats permissions from a CASL Ability object or a raw permission set.
|
* Extracts and formats permissions from a CASL Ability object or a raw permission set.
|
||||||
*/
|
*/
|
||||||
const extractPermissions = (ability: MongoAbility) =>
|
const extractPermissions = (ability: MongoAbility) => {
|
||||||
ability.rules.map((permission) => `${permission.action as string}_${permission.subject as string}`);
|
const permissions: string[] = [];
|
||||||
|
ability.rules.forEach((permission) => {
|
||||||
|
if (typeof permission.action === "string") {
|
||||||
|
permissions.push(`${permission.action}_${permission.subject as string}`);
|
||||||
|
} else {
|
||||||
|
permission.action.forEach((permissionAction) => {
|
||||||
|
permissions.push(`${permissionAction}_${permission.subject as string}`);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return permissions;
|
||||||
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Compares two sets of permissions to determine if the first set is at least as privileged as the second set.
|
* Compares two sets of permissions to determine if the first set is at least as privileged as the second set.
|
||||||
|
|||||||
@@ -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";
|
||||||
@@ -749,6 +750,7 @@ export const registerRoutes = async (
|
|||||||
const projectEnvService = projectEnvServiceFactory({
|
const projectEnvService = projectEnvServiceFactory({
|
||||||
permissionService,
|
permissionService,
|
||||||
projectEnvDAL,
|
projectEnvDAL,
|
||||||
|
keyStore,
|
||||||
licenseService,
|
licenseService,
|
||||||
projectDAL,
|
projectDAL,
|
||||||
folderDAL
|
folderDAL
|
||||||
@@ -924,7 +926,8 @@ export const registerRoutes = async (
|
|||||||
const secretSharingService = secretSharingServiceFactory({
|
const secretSharingService = secretSharingServiceFactory({
|
||||||
permissionService,
|
permissionService,
|
||||||
secretSharingDAL,
|
secretSharingDAL,
|
||||||
orgDAL
|
orgDAL,
|
||||||
|
kmsService
|
||||||
});
|
});
|
||||||
|
|
||||||
const accessApprovalPolicyService = accessApprovalPolicyServiceFactory({
|
const accessApprovalPolicyService = accessApprovalPolicyServiceFactory({
|
||||||
@@ -1193,6 +1196,12 @@ export const registerRoutes = async (
|
|||||||
workflowIntegrationDAL
|
workflowIntegrationDAL
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const cmekService = cmekServiceFactory({
|
||||||
|
kmsDAL,
|
||||||
|
kmsService,
|
||||||
|
permissionService
|
||||||
|
});
|
||||||
|
|
||||||
const migrationService = externalMigrationServiceFactory({
|
const migrationService = externalMigrationServiceFactory({
|
||||||
projectService,
|
projectService,
|
||||||
orgService,
|
orgService,
|
||||||
@@ -1282,6 +1291,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) > 4096) {
|
||||||
|
ctx.addIssue({
|
||||||
|
code: z.ZodIssueCode.custom,
|
||||||
|
message: "data cannot exceed 4096 bytes"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
export const registerCmekRouter = async (server: FastifyZodProvider) => {
|
||||||
|
// create encryption key
|
||||||
|
server.route({
|
||||||
|
method: "POST",
|
||||||
|
url: "/keys",
|
||||||
|
config: {
|
||||||
|
rateLimit: writeLimit
|
||||||
|
},
|
||||||
|
schema: {
|
||||||
|
description: "Create KMS key",
|
||||||
|
body: z.object({
|
||||||
|
projectId: z.string().describe(KMS.CREATE_KEY.projectId),
|
||||||
|
name: keyNameSchema.describe(KMS.CREATE_KEY.name),
|
||||||
|
description: keyDescriptionSchema.describe(KMS.CREATE_KEY.description),
|
||||||
|
encryptionAlgorithm: z
|
||||||
|
.nativeEnum(SymmetricEncryption)
|
||||||
|
.optional()
|
||||||
|
.default(SymmetricEncryption.AES_GCM_256)
|
||||||
|
.describe(KMS.CREATE_KEY.encryptionAlgorithm) // eventually will support others
|
||||||
|
}),
|
||||||
|
response: {
|
||||||
|
200: z.object({
|
||||||
|
key: KmsKeysSchema
|
||||||
|
})
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
||||||
|
handler: async (req) => {
|
||||||
|
const {
|
||||||
|
body: { projectId, name, description, encryptionAlgorithm },
|
||||||
|
permission
|
||||||
|
} = req;
|
||||||
|
|
||||||
|
const cmek = await server.services.cmek.createCmek(
|
||||||
|
{ orgId: permission.orgId, projectId, name, description, encryptionAlgorithm },
|
||||||
|
permission
|
||||||
|
);
|
||||||
|
|
||||||
|
await server.services.auditLog.createAuditLog({
|
||||||
|
...req.auditLogInfo,
|
||||||
|
projectId,
|
||||||
|
event: {
|
||||||
|
type: EventType.CREATE_CMEK,
|
||||||
|
metadata: {
|
||||||
|
keyId: cmek.id,
|
||||||
|
name,
|
||||||
|
description,
|
||||||
|
encryptionAlgorithm
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return { key: cmek };
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// update KMS key
|
||||||
|
server.route({
|
||||||
|
method: "PATCH",
|
||||||
|
url: "/keys/:keyId",
|
||||||
|
config: {
|
||||||
|
rateLimit: writeLimit
|
||||||
|
},
|
||||||
|
schema: {
|
||||||
|
description: "Update KMS key",
|
||||||
|
params: z.object({
|
||||||
|
keyId: z.string().uuid().describe(KMS.UPDATE_KEY.keyId)
|
||||||
|
}),
|
||||||
|
body: z.object({
|
||||||
|
name: keyNameSchema.optional().describe(KMS.UPDATE_KEY.name),
|
||||||
|
isDisabled: z.boolean().optional().describe(KMS.UPDATE_KEY.isDisabled),
|
||||||
|
description: keyDescriptionSchema.describe(KMS.UPDATE_KEY.description)
|
||||||
|
}),
|
||||||
|
response: {
|
||||||
|
200: z.object({
|
||||||
|
key: KmsKeysSchema
|
||||||
|
})
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
||||||
|
handler: async (req) => {
|
||||||
|
const {
|
||||||
|
params: { keyId },
|
||||||
|
body,
|
||||||
|
permission
|
||||||
|
} = req;
|
||||||
|
|
||||||
|
const cmek = await server.services.cmek.updateCmekById({ keyId, ...body }, permission);
|
||||||
|
|
||||||
|
await server.services.auditLog.createAuditLog({
|
||||||
|
...req.auditLogInfo,
|
||||||
|
orgId: permission.orgId,
|
||||||
|
event: {
|
||||||
|
type: EventType.UPDATE_CMEK,
|
||||||
|
metadata: {
|
||||||
|
keyId,
|
||||||
|
...body
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return { key: cmek };
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// delete KMS key
|
||||||
|
server.route({
|
||||||
|
method: "DELETE",
|
||||||
|
url: "/keys/:keyId",
|
||||||
|
config: {
|
||||||
|
rateLimit: writeLimit
|
||||||
|
},
|
||||||
|
schema: {
|
||||||
|
description: "Delete KMS key",
|
||||||
|
params: z.object({
|
||||||
|
keyId: z.string().uuid().describe(KMS.DELETE_KEY.keyId)
|
||||||
|
}),
|
||||||
|
response: {
|
||||||
|
200: z.object({
|
||||||
|
key: KmsKeysSchema
|
||||||
|
})
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
||||||
|
handler: async (req) => {
|
||||||
|
const {
|
||||||
|
params: { keyId },
|
||||||
|
permission
|
||||||
|
} = req;
|
||||||
|
|
||||||
|
const cmek = await server.services.cmek.deleteCmekById(keyId, permission);
|
||||||
|
|
||||||
|
await server.services.auditLog.createAuditLog({
|
||||||
|
...req.auditLogInfo,
|
||||||
|
orgId: permission.orgId,
|
||||||
|
event: {
|
||||||
|
type: EventType.DELETE_CMEK,
|
||||||
|
metadata: {
|
||||||
|
keyId
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return { key: cmek };
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// list KMS keys
|
||||||
|
server.route({
|
||||||
|
method: "GET",
|
||||||
|
url: "/keys",
|
||||||
|
config: {
|
||||||
|
rateLimit: readLimit
|
||||||
|
},
|
||||||
|
schema: {
|
||||||
|
description: "List KMS keys",
|
||||||
|
querystring: z.object({
|
||||||
|
projectId: z.string().describe(KMS.LIST_KEYS.projectId),
|
||||||
|
offset: z.coerce.number().min(0).optional().default(0).describe(KMS.LIST_KEYS.offset),
|
||||||
|
limit: z.coerce.number().min(1).max(100).optional().default(100).describe(KMS.LIST_KEYS.limit),
|
||||||
|
orderBy: z.nativeEnum(CmekOrderBy).optional().default(CmekOrderBy.Name).describe(KMS.LIST_KEYS.orderBy),
|
||||||
|
orderDirection: z
|
||||||
|
.nativeEnum(OrderByDirection)
|
||||||
|
.optional()
|
||||||
|
.default(OrderByDirection.ASC)
|
||||||
|
.describe(KMS.LIST_KEYS.orderDirection),
|
||||||
|
search: z.string().trim().optional().describe(KMS.LIST_KEYS.search)
|
||||||
|
}),
|
||||||
|
response: {
|
||||||
|
200: z.object({
|
||||||
|
keys: KmsKeysSchema.merge(InternalKmsSchema.pick({ version: true, encryptionAlgorithm: true })).array(),
|
||||||
|
totalCount: z.number()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
||||||
|
handler: async (req) => {
|
||||||
|
const {
|
||||||
|
query: { projectId, ...dto },
|
||||||
|
permission
|
||||||
|
} = req;
|
||||||
|
|
||||||
|
const { cmeks, totalCount } = await server.services.cmek.listCmeksByProjectId({ projectId, ...dto }, permission);
|
||||||
|
|
||||||
|
await server.services.auditLog.createAuditLog({
|
||||||
|
...req.auditLogInfo,
|
||||||
|
projectId,
|
||||||
|
event: {
|
||||||
|
type: EventType.GET_CMEKS,
|
||||||
|
metadata: {
|
||||||
|
keyIds: cmeks.map((key) => key.id)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return { keys: cmeks, totalCount };
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// encrypt data
|
||||||
|
server.route({
|
||||||
|
method: "POST",
|
||||||
|
url: "/keys/:keyId/encrypt",
|
||||||
|
config: {
|
||||||
|
rateLimit: writeLimit
|
||||||
|
},
|
||||||
|
schema: {
|
||||||
|
description: "Encrypt data with KMS key",
|
||||||
|
params: z.object({
|
||||||
|
keyId: z.string().uuid().describe(KMS.ENCRYPT.keyId)
|
||||||
|
}),
|
||||||
|
body: z.object({
|
||||||
|
plaintext: base64Schema.describe(KMS.ENCRYPT.plaintext)
|
||||||
|
}),
|
||||||
|
response: {
|
||||||
|
200: z.object({
|
||||||
|
ciphertext: z.string()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
||||||
|
handler: async (req) => {
|
||||||
|
const {
|
||||||
|
params: { keyId },
|
||||||
|
body: { plaintext },
|
||||||
|
permission
|
||||||
|
} = req;
|
||||||
|
|
||||||
|
const ciphertext = await server.services.cmek.cmekEncrypt({ keyId, plaintext }, permission);
|
||||||
|
|
||||||
|
await server.services.auditLog.createAuditLog({
|
||||||
|
...req.auditLogInfo,
|
||||||
|
orgId: permission.orgId,
|
||||||
|
event: {
|
||||||
|
type: EventType.CMEK_ENCRYPT,
|
||||||
|
metadata: {
|
||||||
|
keyId
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return { ciphertext };
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
server.route({
|
||||||
|
method: "POST",
|
||||||
|
url: "/keys/:keyId/decrypt",
|
||||||
|
config: {
|
||||||
|
rateLimit: writeLimit
|
||||||
|
},
|
||||||
|
schema: {
|
||||||
|
description: "Decrypt data with KMS key",
|
||||||
|
params: z.object({
|
||||||
|
keyId: z.string().uuid().describe(KMS.ENCRYPT.keyId)
|
||||||
|
}),
|
||||||
|
body: z.object({
|
||||||
|
ciphertext: base64Schema.describe(KMS.ENCRYPT.plaintext)
|
||||||
|
}),
|
||||||
|
response: {
|
||||||
|
200: z.object({
|
||||||
|
plaintext: z.string()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
||||||
|
handler: async (req) => {
|
||||||
|
const {
|
||||||
|
params: { keyId },
|
||||||
|
body: { ciphertext },
|
||||||
|
permission
|
||||||
|
} = req;
|
||||||
|
|
||||||
|
const plaintext = await server.services.cmek.cmekDecrypt({ keyId, ciphertext }, permission);
|
||||||
|
|
||||||
|
await server.services.auditLog.createAuditLog({
|
||||||
|
...req.auditLogInfo,
|
||||||
|
orgId: permission.orgId,
|
||||||
|
event: {
|
||||||
|
type: EventType.CMEK_DECRYPT,
|
||||||
|
metadata: {
|
||||||
|
keyId
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return { plaintext };
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
@@ -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" });
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -131,9 +131,9 @@ export const registerIntegrationRouter = async (server: FastifyZodProvider) => {
|
|||||||
.default("/")
|
.default("/")
|
||||||
.transform(removeTrailingSlash)
|
.transform(removeTrailingSlash)
|
||||||
.describe(INTEGRATION.UPDATE.secretPath),
|
.describe(INTEGRATION.UPDATE.secretPath),
|
||||||
targetEnvironment: z.string().trim().describe(INTEGRATION.UPDATE.targetEnvironment),
|
targetEnvironment: z.string().trim().optional().describe(INTEGRATION.UPDATE.targetEnvironment),
|
||||||
owner: z.string().trim().describe(INTEGRATION.UPDATE.owner),
|
owner: z.string().trim().optional().describe(INTEGRATION.UPDATE.owner),
|
||||||
environment: z.string().trim().describe(INTEGRATION.UPDATE.environment),
|
environment: z.string().trim().optional().describe(INTEGRATION.UPDATE.environment),
|
||||||
metadata: IntegrationMetadataSchema.optional()
|
metadata: IntegrationMetadataSchema.optional()
|
||||||
}),
|
}),
|
||||||
response: {
|
response: {
|
||||||
|
|||||||
@@ -55,10 +55,10 @@ export const registerSecretSharingRouter = async (server: FastifyZodProvider) =>
|
|||||||
},
|
},
|
||||||
schema: {
|
schema: {
|
||||||
params: z.object({
|
params: z.object({
|
||||||
id: z.string().uuid()
|
id: z.string()
|
||||||
}),
|
}),
|
||||||
body: z.object({
|
body: z.object({
|
||||||
hashedHex: z.string().min(1),
|
hashedHex: z.string().min(1).optional(),
|
||||||
password: z.string().optional()
|
password: z.string().optional()
|
||||||
}),
|
}),
|
||||||
response: {
|
response: {
|
||||||
@@ -73,7 +73,8 @@ export const registerSecretSharingRouter = async (server: FastifyZodProvider) =>
|
|||||||
accessType: true
|
accessType: true
|
||||||
})
|
})
|
||||||
.extend({
|
.extend({
|
||||||
orgName: z.string().optional()
|
orgName: z.string().optional(),
|
||||||
|
secretValue: z.string().optional()
|
||||||
})
|
})
|
||||||
.optional()
|
.optional()
|
||||||
})
|
})
|
||||||
@@ -99,17 +100,14 @@ export const registerSecretSharingRouter = async (server: FastifyZodProvider) =>
|
|||||||
},
|
},
|
||||||
schema: {
|
schema: {
|
||||||
body: z.object({
|
body: z.object({
|
||||||
encryptedValue: z.string(),
|
secretValue: z.string().max(10_000),
|
||||||
password: z.string().optional(),
|
password: z.string().optional(),
|
||||||
hashedHex: z.string(),
|
|
||||||
iv: z.string(),
|
|
||||||
tag: z.string(),
|
|
||||||
expiresAt: z.string(),
|
expiresAt: z.string(),
|
||||||
expiresAfterViews: z.number().min(1).optional()
|
expiresAfterViews: z.number().min(1).optional()
|
||||||
}),
|
}),
|
||||||
response: {
|
response: {
|
||||||
200: z.object({
|
200: z.object({
|
||||||
id: z.string().uuid()
|
id: z.string()
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -132,17 +130,14 @@ export const registerSecretSharingRouter = async (server: FastifyZodProvider) =>
|
|||||||
body: z.object({
|
body: z.object({
|
||||||
name: z.string().max(50).optional(),
|
name: z.string().max(50).optional(),
|
||||||
password: z.string().optional(),
|
password: z.string().optional(),
|
||||||
encryptedValue: z.string(),
|
secretValue: z.string(),
|
||||||
hashedHex: z.string(),
|
|
||||||
iv: z.string(),
|
|
||||||
tag: z.string(),
|
|
||||||
expiresAt: z.string(),
|
expiresAt: z.string(),
|
||||||
expiresAfterViews: z.number().min(1).optional(),
|
expiresAfterViews: z.number().min(1).optional(),
|
||||||
accessType: z.nativeEnum(SecretSharingAccessType).default(SecretSharingAccessType.Organization)
|
accessType: z.nativeEnum(SecretSharingAccessType).default(SecretSharingAccessType.Organization)
|
||||||
}),
|
}),
|
||||||
response: {
|
response: {
|
||||||
200: z.object({
|
200: z.object({
|
||||||
id: z.string().uuid()
|
id: z.string()
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -168,7 +163,7 @@ export const registerSecretSharingRouter = async (server: FastifyZodProvider) =>
|
|||||||
},
|
},
|
||||||
schema: {
|
schema: {
|
||||||
params: z.object({
|
params: z.object({
|
||||||
sharedSecretId: z.string().uuid()
|
sharedSecretId: z.string()
|
||||||
}),
|
}),
|
||||||
response: {
|
response: {
|
||||||
200: SecretSharingSchema
|
200: SecretSharingSchema
|
||||||
|
|||||||
@@ -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"
|
||||||
|
}
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
import { ForbiddenError } from "@casl/ability";
|
import { ForbiddenError } from "@casl/ability";
|
||||||
import axios from "axios";
|
import axios, { AxiosError } from "axios";
|
||||||
import https from "https";
|
import https from "https";
|
||||||
import jwt from "jsonwebtoken";
|
import jwt from "jsonwebtoken";
|
||||||
|
|
||||||
@@ -107,32 +107,54 @@ export const identityKubernetesAuthServiceFactory = ({
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
const { data }: { data: TCreateTokenReviewResponse } = await axios.post(
|
const { data } = await axios
|
||||||
`${identityKubernetesAuth.kubernetesHost}/apis/authentication.k8s.io/v1/tokenreviews`,
|
.post<TCreateTokenReviewResponse>(
|
||||||
{
|
`${identityKubernetesAuth.kubernetesHost}/apis/authentication.k8s.io/v1/tokenreviews`,
|
||||||
apiVersion: "authentication.k8s.io/v1",
|
{
|
||||||
kind: "TokenReview",
|
apiVersion: "authentication.k8s.io/v1",
|
||||||
spec: {
|
kind: "TokenReview",
|
||||||
token: serviceAccountJwt
|
spec: {
|
||||||
}
|
token: serviceAccountJwt
|
||||||
},
|
}
|
||||||
{
|
|
||||||
headers: {
|
|
||||||
"Content-Type": "application/json",
|
|
||||||
Authorization: `Bearer ${tokenReviewerJwt}`
|
|
||||||
},
|
},
|
||||||
httpsAgent: new https.Agent({
|
{
|
||||||
ca: caCert,
|
headers: {
|
||||||
rejectUnauthorized: !!caCert
|
"Content-Type": "application/json",
|
||||||
})
|
Authorization: `Bearer ${tokenReviewerJwt}`
|
||||||
}
|
},
|
||||||
);
|
|
||||||
|
|
||||||
if ("error" in data.status) throw new UnauthorizedError({ message: data.status.error });
|
// if ca cert, rejectUnauthorized: true
|
||||||
|
httpsAgent: new https.Agent({
|
||||||
|
ca: caCert,
|
||||||
|
rejectUnauthorized: !!caCert
|
||||||
|
})
|
||||||
|
}
|
||||||
|
)
|
||||||
|
.catch((err) => {
|
||||||
|
if (err instanceof AxiosError) {
|
||||||
|
if (err.response) {
|
||||||
|
const { message } = err?.response?.data as unknown as { message?: string };
|
||||||
|
|
||||||
|
if (message) {
|
||||||
|
throw new UnauthorizedError({
|
||||||
|
message,
|
||||||
|
name: "KubernetesTokenReviewRequestError"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
throw err;
|
||||||
|
});
|
||||||
|
|
||||||
|
if ("error" in data.status)
|
||||||
|
throw new UnauthorizedError({ message: data.status.error, name: "KubernetesTokenReviewError" });
|
||||||
|
|
||||||
// check the response to determine if the token is valid
|
// check the response to determine if the token is valid
|
||||||
if (!(data.status && data.status.authenticated))
|
if (!(data.status && data.status.authenticated))
|
||||||
throw new UnauthorizedError({ message: "Kubernetes token not authenticated" });
|
throw new UnauthorizedError({
|
||||||
|
message: "Kubernetes token not authenticated",
|
||||||
|
name: "KubernetesTokenReviewError"
|
||||||
|
});
|
||||||
|
|
||||||
const { namespace: targetNamespace, name: targetName } = extractK8sUsername(data.status.user.username);
|
const { namespace: targetNamespace, name: targetName } = extractK8sUsername(data.status.user.username);
|
||||||
|
|
||||||
|
|||||||
@@ -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"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -150,12 +150,17 @@ export const integrationServiceFactory = ({
|
|||||||
);
|
);
|
||||||
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Edit, ProjectPermissionSub.Integrations);
|
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Edit, ProjectPermissionSub.Integrations);
|
||||||
|
|
||||||
ForbiddenError.from(permission).throwUnlessCan(
|
const newEnvironment = environment || integration.environment.slug;
|
||||||
ProjectPermissionActions.Read,
|
const newSecretPath = secretPath || integration.secretPath;
|
||||||
subject(ProjectPermissionSub.Secrets, { environment, secretPath })
|
|
||||||
);
|
|
||||||
|
|
||||||
const folder = await folderDAL.findBySecretPath(integration.projectId, environment, secretPath);
|
if (environment || secretPath) {
|
||||||
|
ForbiddenError.from(permission).throwUnlessCan(
|
||||||
|
ProjectPermissionActions.Read,
|
||||||
|
subject(ProjectPermissionSub.Secrets, { environment: newEnvironment, secretPath: newSecretPath })
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const folder = await folderDAL.findBySecretPath(integration.projectId, newEnvironment, newSecretPath);
|
||||||
if (!folder) throw new NotFoundError({ message: "Folder path not found" });
|
if (!folder) throw new NotFoundError({ message: "Folder path not found" });
|
||||||
|
|
||||||
const updatedIntegration = await integrationDAL.updateById(id, {
|
const updatedIntegration = await integrationDAL.updateById(id, {
|
||||||
@@ -174,7 +179,7 @@ export const integrationServiceFactory = ({
|
|||||||
|
|
||||||
await secretQueueService.syncIntegrations({
|
await secretQueueService.syncIntegrations({
|
||||||
environment: folder.environment.slug,
|
environment: folder.environment.slug,
|
||||||
secretPath,
|
secretPath: newSecretPath,
|
||||||
projectId: folder.projectId
|
projectId: folder.projectId
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -184,6 +189,12 @@ export const integrationServiceFactory = ({
|
|||||||
const getIntegration = async ({ id, actor, actorAuthMethod, actorId, actorOrgId }: TGetIntegrationDTO) => {
|
const getIntegration = async ({ id, actor, actorAuthMethod, actorId, actorOrgId }: TGetIntegrationDTO) => {
|
||||||
const integration = await integrationDAL.findById(id);
|
const integration = await integrationDAL.findById(id);
|
||||||
|
|
||||||
|
if (!integration) {
|
||||||
|
throw new NotFoundError({
|
||||||
|
message: "Integration not found"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
const { permission } = await permissionService.getProjectPermission(
|
const { permission } = await permissionService.getProjectPermission(
|
||||||
actor,
|
actor,
|
||||||
actorId,
|
actorId,
|
||||||
|
|||||||
@@ -48,10 +48,10 @@ export type TUpdateIntegrationDTO = {
|
|||||||
app?: string;
|
app?: string;
|
||||||
appId?: string;
|
appId?: string;
|
||||||
isActive?: boolean;
|
isActive?: boolean;
|
||||||
secretPath: string;
|
secretPath?: string;
|
||||||
targetEnvironment: string;
|
targetEnvironment?: string;
|
||||||
owner: string;
|
owner?: string;
|
||||||
environment: string;
|
environment?: string;
|
||||||
metadata?: {
|
metadata?: {
|
||||||
secretPrefix?: string;
|
secretPrefix?: string;
|
||||||
secretSuffix?: string;
|
secretSuffix?: string;
|
||||||
|
|||||||
@@ -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";
|
||||||
@@ -71,17 +72,29 @@ export const kmsServiceFactory = ({
|
|||||||
* This function is responsibile for generating the infisical internal KMS for various entities
|
* This function is responsibile for generating the infisical internal KMS for various entities
|
||||||
* Like for secret manager, cert manager or for organization
|
* Like for secret manager, cert manager or for organization
|
||||||
*/
|
*/
|
||||||
const generateKmsKey = async ({ orgId, isReserved = true, tx, slug }: TGenerateKMSDTO) => {
|
const generateKmsKey = async ({
|
||||||
|
orgId,
|
||||||
|
isReserved = true,
|
||||||
|
tx,
|
||||||
|
name,
|
||||||
|
projectId,
|
||||||
|
encryptionAlgorithm = SymmetricEncryption.AES_GCM_256,
|
||||||
|
description
|
||||||
|
}: TGenerateKMSDTO) => {
|
||||||
const cipher = symmetricCipherService(SymmetricEncryption.AES_GCM_256);
|
const cipher = symmetricCipherService(SymmetricEncryption.AES_GCM_256);
|
||||||
const kmsKeyMaterial = randomSecureBytes(32);
|
|
||||||
|
const kmsKeyMaterial = randomSecureBytes(getByteLengthForAlgorithm(encryptionAlgorithm));
|
||||||
|
|
||||||
const encryptedKeyMaterial = cipher.encrypt(kmsKeyMaterial, ROOT_ENCRYPTION_KEY);
|
const encryptedKeyMaterial = cipher.encrypt(kmsKeyMaterial, ROOT_ENCRYPTION_KEY);
|
||||||
const sanitizedSlug = slug ? slugify(slug) : slugify(alphaNumericNanoId(8).toLowerCase());
|
const sanitizedName = name ? slugify(name) : slugify(alphaNumericNanoId(8).toLowerCase());
|
||||||
const dbQuery = async (db: Knex) => {
|
const dbQuery = async (db: Knex) => {
|
||||||
const kmsDoc = await kmsDAL.create(
|
const kmsDoc = await kmsDAL.create(
|
||||||
{
|
{
|
||||||
slug: sanitizedSlug,
|
name: sanitizedName,
|
||||||
orgId,
|
orgId,
|
||||||
isReserved
|
isReserved,
|
||||||
|
projectId,
|
||||||
|
description
|
||||||
},
|
},
|
||||||
db
|
db
|
||||||
);
|
);
|
||||||
@@ -90,7 +103,7 @@ export const kmsServiceFactory = ({
|
|||||||
{
|
{
|
||||||
version: 1,
|
version: 1,
|
||||||
encryptedKey: encryptedKeyMaterial,
|
encryptedKey: encryptedKeyMaterial,
|
||||||
encryptionAlgorithm: SymmetricEncryption.AES_GCM_256,
|
encryptionAlgorithm,
|
||||||
kmsKeyId: kmsDoc.id
|
kmsKeyId: kmsDoc.id
|
||||||
},
|
},
|
||||||
db
|
db
|
||||||
@@ -208,20 +221,20 @@ export const kmsServiceFactory = ({
|
|||||||
return org.kmsDefaultKeyId;
|
return org.kmsDefaultKeyId;
|
||||||
};
|
};
|
||||||
|
|
||||||
const encryptWithRootKey = async () => {
|
const encryptWithRootKey = () => {
|
||||||
const cipher = symmetricCipherService(SymmetricEncryption.AES_GCM_256);
|
const cipher = symmetricCipherService(SymmetricEncryption.AES_GCM_256);
|
||||||
return ({ plainText }: { plainText: Buffer }) => {
|
|
||||||
const encryptedPlainTextBlob = cipher.encrypt(plainText, ROOT_ENCRYPTION_KEY);
|
|
||||||
|
|
||||||
return Promise.resolve({ cipherTextBlob: encryptedPlainTextBlob });
|
return (plainTextBuffer: Buffer) => {
|
||||||
|
const encryptedBuffer = cipher.encrypt(plainTextBuffer, ROOT_ENCRYPTION_KEY);
|
||||||
|
return encryptedBuffer;
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
const decryptWithRootKey = async () => {
|
const decryptWithRootKey = () => {
|
||||||
const cipher = symmetricCipherService(SymmetricEncryption.AES_GCM_256);
|
const cipher = symmetricCipherService(SymmetricEncryption.AES_GCM_256);
|
||||||
return ({ cipherTextBlob }: { cipherTextBlob: Buffer }) => {
|
|
||||||
const decryptedBlob = cipher.decrypt(cipherTextBlob, ROOT_ENCRYPTION_KEY);
|
return (cipherTextBuffer: Buffer) => {
|
||||||
return Promise.resolve(decryptedBlob);
|
return cipher.decrypt(cipherTextBuffer, ROOT_ENCRYPTION_KEY);
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -286,12 +299,13 @@ export const kmsServiceFactory = ({
|
|||||||
}
|
}
|
||||||
|
|
||||||
// internal KMS
|
// internal KMS
|
||||||
const cipher = symmetricCipherService(SymmetricEncryption.AES_GCM_256);
|
const keyCipher = symmetricCipherService(SymmetricEncryption.AES_GCM_256);
|
||||||
const kmsKey = cipher.decrypt(kmsDoc.internalKms?.encryptedKey as Buffer, ROOT_ENCRYPTION_KEY);
|
const dataCipher = symmetricCipherService(kmsDoc.internalKms?.encryptionAlgorithm as SymmetricEncryption);
|
||||||
|
const kmsKey = keyCipher.decrypt(kmsDoc.internalKms?.encryptedKey as Buffer, ROOT_ENCRYPTION_KEY);
|
||||||
|
|
||||||
return ({ cipherTextBlob: versionedCipherTextBlob }: Pick<TDecryptWithKmsDTO, "cipherTextBlob">) => {
|
return ({ cipherTextBlob: versionedCipherTextBlob }: Pick<TDecryptWithKmsDTO, "cipherTextBlob">) => {
|
||||||
const cipherTextBlob = versionedCipherTextBlob.subarray(0, -KMS_VERSION_BLOB_LENGTH);
|
const cipherTextBlob = versionedCipherTextBlob.subarray(0, -KMS_VERSION_BLOB_LENGTH);
|
||||||
const decryptedBlob = cipher.decrypt(cipherTextBlob, kmsKey);
|
const decryptedBlob = dataCipher.decrypt(cipherTextBlob, kmsKey);
|
||||||
return Promise.resolve(decryptedBlob);
|
return Promise.resolve(decryptedBlob);
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
@@ -347,11 +361,11 @@ export const kmsServiceFactory = ({
|
|||||||
}
|
}
|
||||||
|
|
||||||
// internal KMS
|
// internal KMS
|
||||||
// akhilmhdh: as more encryption are added do a check here on kmsDoc.encryptionAlgorithm
|
const keyCipher = symmetricCipherService(SymmetricEncryption.AES_GCM_256);
|
||||||
const cipher = symmetricCipherService(SymmetricEncryption.AES_GCM_256);
|
const dataCipher = symmetricCipherService(kmsDoc.internalKms?.encryptionAlgorithm as SymmetricEncryption);
|
||||||
return ({ plainText }: Pick<TEncryptWithKmsDTO, "plainText">) => {
|
return ({ plainText }: Pick<TEncryptWithKmsDTO, "plainText">) => {
|
||||||
const kmsKey = cipher.decrypt(kmsDoc.internalKms?.encryptedKey as Buffer, ROOT_ENCRYPTION_KEY);
|
const kmsKey = keyCipher.decrypt(kmsDoc.internalKms?.encryptedKey as Buffer, ROOT_ENCRYPTION_KEY);
|
||||||
const encryptedPlainTextBlob = cipher.encrypt(plainText, kmsKey);
|
const encryptedPlainTextBlob = dataCipher.encrypt(plainText, kmsKey);
|
||||||
|
|
||||||
// Buffer#1 encrypted text + Buffer#2 version number
|
// Buffer#1 encrypted text + Buffer#2 version number
|
||||||
const versionBlob = Buffer.from(KMS_VERSION, "utf8"); // length is 3
|
const versionBlob = Buffer.from(KMS_VERSION, "utf8"); // length is 3
|
||||||
@@ -767,8 +781,8 @@ export const kmsServiceFactory = ({
|
|||||||
message: "KMS not found"
|
message: "KMS not found"
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
const { id, slug, orgId, isExternal } = kms;
|
const { id, name, orgId, isExternal } = kms;
|
||||||
return { id, slug, orgId, isExternal };
|
return { id, name, orgId, isExternal };
|
||||||
};
|
};
|
||||||
|
|
||||||
// akhilmhdh: a copy of this is made in migrations/utils/kms
|
// akhilmhdh: a copy of this is made in migrations/utils/kms
|
||||||
|
|||||||
@@ -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;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -3,7 +3,9 @@ import { ForbiddenError } from "@casl/ability";
|
|||||||
import { TLicenseServiceFactory } from "@app/ee/services/license/license-service";
|
import { TLicenseServiceFactory } from "@app/ee/services/license/license-service";
|
||||||
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service";
|
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service";
|
||||||
import { ProjectPermissionActions, ProjectPermissionSub } from "@app/ee/services/permission/project-permission";
|
import { ProjectPermissionActions, ProjectPermissionSub } from "@app/ee/services/permission/project-permission";
|
||||||
|
import { KeyStorePrefixes, TKeyStoreFactory } from "@app/keystore/keystore";
|
||||||
import { BadRequestError, NotFoundError } from "@app/lib/errors";
|
import { BadRequestError, NotFoundError } from "@app/lib/errors";
|
||||||
|
import { logger } from "@app/lib/logger";
|
||||||
|
|
||||||
import { TProjectDALFactory } from "../project/project-dal";
|
import { TProjectDALFactory } from "../project/project-dal";
|
||||||
import { TSecretFolderDALFactory } from "../secret-folder/secret-folder-dal";
|
import { TSecretFolderDALFactory } from "../secret-folder/secret-folder-dal";
|
||||||
@@ -16,6 +18,7 @@ type TProjectEnvServiceFactoryDep = {
|
|||||||
projectDAL: Pick<TProjectDALFactory, "findById">;
|
projectDAL: Pick<TProjectDALFactory, "findById">;
|
||||||
permissionService: Pick<TPermissionServiceFactory, "getProjectPermission">;
|
permissionService: Pick<TPermissionServiceFactory, "getProjectPermission">;
|
||||||
licenseService: Pick<TLicenseServiceFactory, "getPlan">;
|
licenseService: Pick<TLicenseServiceFactory, "getPlan">;
|
||||||
|
keyStore: Pick<TKeyStoreFactory, "acquireLock" | "setItemWithExpiry" | "getItem" | "waitTillReady">;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type TProjectEnvServiceFactory = ReturnType<typeof projectEnvServiceFactory>;
|
export type TProjectEnvServiceFactory = ReturnType<typeof projectEnvServiceFactory>;
|
||||||
@@ -24,6 +27,7 @@ export const projectEnvServiceFactory = ({
|
|||||||
projectEnvDAL,
|
projectEnvDAL,
|
||||||
permissionService,
|
permissionService,
|
||||||
licenseService,
|
licenseService,
|
||||||
|
keyStore,
|
||||||
projectDAL,
|
projectDAL,
|
||||||
folderDAL
|
folderDAL
|
||||||
}: TProjectEnvServiceFactoryDep) => {
|
}: TProjectEnvServiceFactoryDep) => {
|
||||||
@@ -45,32 +49,56 @@ export const projectEnvServiceFactory = ({
|
|||||||
);
|
);
|
||||||
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Create, ProjectPermissionSub.Environments);
|
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Create, ProjectPermissionSub.Environments);
|
||||||
|
|
||||||
const envs = await projectEnvDAL.find({ projectId });
|
const lock = await keyStore
|
||||||
const existingEnv = envs.find(({ slug: envSlug }) => envSlug === slug);
|
.acquireLock([KeyStorePrefixes.ProjectEnvironmentLock(projectId)], 5000)
|
||||||
if (existingEnv)
|
.catch(() => null);
|
||||||
throw new BadRequestError({
|
|
||||||
message: "Environment with slug already exist",
|
try {
|
||||||
name: "CreateEnvironment"
|
if (!lock) {
|
||||||
|
await keyStore.waitTillReady({
|
||||||
|
key: KeyStorePrefixes.WaitUntilReadyProjectEnvironmentOperation(projectId),
|
||||||
|
keyCheckCb: (val) => val === "true",
|
||||||
|
waitingCb: () => logger.debug("Create project environment. Waiting for "),
|
||||||
|
delay: 500
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const envs = await projectEnvDAL.find({ projectId });
|
||||||
|
const existingEnv = envs.find(({ slug: envSlug }) => envSlug === slug);
|
||||||
|
if (existingEnv)
|
||||||
|
throw new BadRequestError({
|
||||||
|
message: "Environment with slug already exist",
|
||||||
|
name: "CreateEnvironment"
|
||||||
|
});
|
||||||
|
|
||||||
|
const project = await projectDAL.findById(projectId);
|
||||||
|
const plan = await licenseService.getPlan(project.orgId);
|
||||||
|
if (plan.environmentLimit !== null && envs.length >= plan.environmentLimit) {
|
||||||
|
// case: limit imposed on number of environments allowed
|
||||||
|
// case: number of environments used exceeds the number of environments allowed
|
||||||
|
throw new BadRequestError({
|
||||||
|
message:
|
||||||
|
"Failed to create environment due to environment limit reached. Upgrade plan to create more environments."
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const env = await projectEnvDAL.transaction(async (tx) => {
|
||||||
|
const lastPos = await projectEnvDAL.findLastEnvPosition(projectId, tx);
|
||||||
|
const doc = await projectEnvDAL.create({ slug, name, projectId, position: lastPos + 1 }, tx);
|
||||||
|
await folderDAL.create({ name: "root", parentId: null, envId: doc.id, version: 1 }, tx);
|
||||||
|
return doc;
|
||||||
});
|
});
|
||||||
|
|
||||||
const project = await projectDAL.findById(projectId);
|
await keyStore.setItemWithExpiry(
|
||||||
const plan = await licenseService.getPlan(project.orgId);
|
KeyStorePrefixes.WaitUntilReadyProjectEnvironmentOperation(projectId),
|
||||||
if (plan.environmentLimit !== null && envs.length >= plan.environmentLimit) {
|
10,
|
||||||
// case: limit imposed on number of environments allowed
|
"true"
|
||||||
// case: number of environments used exceeds the number of environments allowed
|
);
|
||||||
throw new BadRequestError({
|
|
||||||
message:
|
return env;
|
||||||
"Failed to create environment due to environment limit reached. Upgrade plan to create more environments."
|
} finally {
|
||||||
});
|
await lock?.release();
|
||||||
}
|
}
|
||||||
|
|
||||||
const env = await projectEnvDAL.transaction(async (tx) => {
|
|
||||||
const lastPos = await projectEnvDAL.findLastEnvPosition(projectId, tx);
|
|
||||||
const doc = await projectEnvDAL.create({ slug, name, projectId, position: lastPos + 1 }, tx);
|
|
||||||
await folderDAL.create({ name: "root", parentId: null, envId: doc.id, version: 1 }, tx);
|
|
||||||
return doc;
|
|
||||||
});
|
|
||||||
return env;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const updateEnvironment = async ({
|
const updateEnvironment = async ({
|
||||||
@@ -93,26 +121,50 @@ export const projectEnvServiceFactory = ({
|
|||||||
);
|
);
|
||||||
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Edit, ProjectPermissionSub.Environments);
|
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Edit, ProjectPermissionSub.Environments);
|
||||||
|
|
||||||
const oldEnv = await projectEnvDAL.findOne({ id, projectId });
|
const lock = await keyStore
|
||||||
if (!oldEnv) throw new NotFoundError({ message: "Environment not found" });
|
.acquireLock([KeyStorePrefixes.ProjectEnvironmentLock(projectId)], 5000)
|
||||||
|
.catch(() => null);
|
||||||
|
|
||||||
if (slug) {
|
try {
|
||||||
const existingEnv = await projectEnvDAL.findOne({ slug, projectId });
|
if (!lock) {
|
||||||
if (existingEnv && existingEnv.id !== id) {
|
await keyStore.waitTillReady({
|
||||||
throw new BadRequestError({
|
key: KeyStorePrefixes.WaitUntilReadyProjectEnvironmentOperation(projectId),
|
||||||
message: "Environment with slug already exist",
|
keyCheckCb: (val) => val === "true",
|
||||||
name: "UpdateEnvironment"
|
waitingCb: () => logger.debug("Update project environment. Waiting for project environment update"),
|
||||||
|
delay: 500
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
const env = await projectEnvDAL.transaction(async (tx) => {
|
const oldEnv = await projectEnvDAL.findOne({ id, projectId });
|
||||||
if (position) {
|
if (!oldEnv) throw new NotFoundError({ message: "Environment not found", name: "UpdateEnvironment" });
|
||||||
await projectEnvDAL.updateAllPosition(projectId, oldEnv.position, position, tx);
|
|
||||||
|
if (slug) {
|
||||||
|
const existingEnv = await projectEnvDAL.findOne({ slug, projectId });
|
||||||
|
if (existingEnv && existingEnv.id !== id) {
|
||||||
|
throw new BadRequestError({
|
||||||
|
message: "Environment with slug already exist",
|
||||||
|
name: "UpdateEnvironment"
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return projectEnvDAL.updateById(oldEnv.id, { name, slug, position }, tx);
|
|
||||||
});
|
const env = await projectEnvDAL.transaction(async (tx) => {
|
||||||
return { environment: env, old: oldEnv };
|
if (position) {
|
||||||
|
await projectEnvDAL.updateAllPosition(projectId, oldEnv.position, position, tx);
|
||||||
|
}
|
||||||
|
return projectEnvDAL.updateById(oldEnv.id, { name, slug, position }, tx);
|
||||||
|
});
|
||||||
|
|
||||||
|
await keyStore.setItemWithExpiry(
|
||||||
|
KeyStorePrefixes.WaitUntilReadyProjectEnvironmentOperation(projectId),
|
||||||
|
10,
|
||||||
|
"true"
|
||||||
|
);
|
||||||
|
|
||||||
|
return { environment: env, old: oldEnv };
|
||||||
|
} finally {
|
||||||
|
await lock?.release();
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const deleteEnvironment = async ({ projectId, actor, actorId, actorOrgId, actorAuthMethod, id }: TDeleteEnvDTO) => {
|
const deleteEnvironment = async ({ projectId, actor, actorId, actorOrgId, actorAuthMethod, id }: TDeleteEnvDTO) => {
|
||||||
@@ -125,18 +177,42 @@ export const projectEnvServiceFactory = ({
|
|||||||
);
|
);
|
||||||
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Delete, ProjectPermissionSub.Environments);
|
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Delete, ProjectPermissionSub.Environments);
|
||||||
|
|
||||||
const env = await projectEnvDAL.transaction(async (tx) => {
|
const lock = await keyStore
|
||||||
const [doc] = await projectEnvDAL.delete({ id, projectId }, tx);
|
.acquireLock([KeyStorePrefixes.ProjectEnvironmentLock(projectId)], 5000)
|
||||||
if (!doc)
|
.catch(() => null);
|
||||||
throw new NotFoundError({
|
|
||||||
message: "Env doesn't exist",
|
|
||||||
name: "DeleteEnvironment"
|
|
||||||
});
|
|
||||||
|
|
||||||
await projectEnvDAL.updateAllPosition(projectId, doc.position, -1, tx);
|
try {
|
||||||
return doc;
|
if (!lock) {
|
||||||
});
|
await keyStore.waitTillReady({
|
||||||
return env;
|
key: KeyStorePrefixes.WaitUntilReadyProjectEnvironmentOperation(projectId),
|
||||||
|
keyCheckCb: (val) => val === "true",
|
||||||
|
waitingCb: () => logger.debug("Delete project environment. Waiting for "),
|
||||||
|
delay: 500
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const env = await projectEnvDAL.transaction(async (tx) => {
|
||||||
|
const [doc] = await projectEnvDAL.delete({ id, projectId }, tx);
|
||||||
|
if (!doc)
|
||||||
|
throw new NotFoundError({
|
||||||
|
message: "Environment doesn't exist",
|
||||||
|
name: "DeleteEnvironment"
|
||||||
|
});
|
||||||
|
|
||||||
|
await projectEnvDAL.updateAllPosition(projectId, doc.position, -1, tx);
|
||||||
|
return doc;
|
||||||
|
});
|
||||||
|
|
||||||
|
await keyStore.setItemWithExpiry(
|
||||||
|
KeyStorePrefixes.WaitUntilReadyProjectEnvironmentOperation(projectId),
|
||||||
|
10,
|
||||||
|
"true"
|
||||||
|
);
|
||||||
|
|
||||||
|
return env;
|
||||||
|
} finally {
|
||||||
|
await lock?.release();
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const getEnvironmentById = async ({ projectId, actor, actorId, actorOrgId, actorAuthMethod, id }: TGetEnvDTO) => {
|
const getEnvironmentById = async ({ projectId, actor, actorId, actorOrgId, actorAuthMethod, id }: TGetEnvDTO) => {
|
||||||
|
|||||||
@@ -1,10 +1,14 @@
|
|||||||
|
import crypto from "node:crypto";
|
||||||
|
|
||||||
import bcrypt from "bcrypt";
|
import bcrypt from "bcrypt";
|
||||||
|
import { z } from "zod";
|
||||||
|
|
||||||
import { TSecretSharing } from "@app/db/schemas";
|
import { TSecretSharing } from "@app/db/schemas";
|
||||||
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service";
|
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service";
|
||||||
import { BadRequestError, ForbiddenRequestError, NotFoundError, UnauthorizedError } from "@app/lib/errors";
|
import { BadRequestError, ForbiddenRequestError, NotFoundError, UnauthorizedError } from "@app/lib/errors";
|
||||||
import { SecretSharingAccessType } from "@app/lib/types";
|
import { SecretSharingAccessType } from "@app/lib/types";
|
||||||
|
|
||||||
|
import { TKmsServiceFactory } from "../kms/kms-service";
|
||||||
import { TOrgDALFactory } from "../org/org-dal";
|
import { TOrgDALFactory } from "../org/org-dal";
|
||||||
import { TSecretSharingDALFactory } from "./secret-sharing-dal";
|
import { TSecretSharingDALFactory } from "./secret-sharing-dal";
|
||||||
import {
|
import {
|
||||||
@@ -19,14 +23,18 @@ type TSecretSharingServiceFactoryDep = {
|
|||||||
permissionService: Pick<TPermissionServiceFactory, "getOrgPermission">;
|
permissionService: Pick<TPermissionServiceFactory, "getOrgPermission">;
|
||||||
secretSharingDAL: TSecretSharingDALFactory;
|
secretSharingDAL: TSecretSharingDALFactory;
|
||||||
orgDAL: TOrgDALFactory;
|
orgDAL: TOrgDALFactory;
|
||||||
|
kmsService: TKmsServiceFactory;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type TSecretSharingServiceFactory = ReturnType<typeof secretSharingServiceFactory>;
|
export type TSecretSharingServiceFactory = ReturnType<typeof secretSharingServiceFactory>;
|
||||||
|
|
||||||
|
const isUuidV4 = (uuid: string) => z.string().uuid().safeParse(uuid).success;
|
||||||
|
|
||||||
export const secretSharingServiceFactory = ({
|
export const secretSharingServiceFactory = ({
|
||||||
permissionService,
|
permissionService,
|
||||||
secretSharingDAL,
|
secretSharingDAL,
|
||||||
orgDAL
|
orgDAL,
|
||||||
|
kmsService
|
||||||
}: TSecretSharingServiceFactoryDep) => {
|
}: TSecretSharingServiceFactoryDep) => {
|
||||||
const createSharedSecret = async ({
|
const createSharedSecret = async ({
|
||||||
actor,
|
actor,
|
||||||
@@ -34,10 +42,7 @@ export const secretSharingServiceFactory = ({
|
|||||||
orgId,
|
orgId,
|
||||||
actorAuthMethod,
|
actorAuthMethod,
|
||||||
actorOrgId,
|
actorOrgId,
|
||||||
encryptedValue,
|
secretValue,
|
||||||
hashedHex,
|
|
||||||
iv,
|
|
||||||
tag,
|
|
||||||
name,
|
name,
|
||||||
password,
|
password,
|
||||||
accessType,
|
accessType,
|
||||||
@@ -59,19 +64,25 @@ export const secretSharingServiceFactory = ({
|
|||||||
throw new BadRequestError({ message: "Expiration date cannot be more than 30 days" });
|
throw new BadRequestError({ message: "Expiration date cannot be more than 30 days" });
|
||||||
}
|
}
|
||||||
|
|
||||||
// Limit Input ciphertext length to 13000 (equivalent to 10,000 characters of Plaintext)
|
if (secretValue.length > 10_000) {
|
||||||
if (encryptedValue.length > 13000) {
|
|
||||||
throw new BadRequestError({ message: "Shared secret value too long" });
|
throw new BadRequestError({ message: "Shared secret value too long" });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const encryptWithRoot = kmsService.encryptWithRootKey();
|
||||||
|
|
||||||
|
const encryptedSecret = encryptWithRoot(Buffer.from(secretValue));
|
||||||
|
|
||||||
|
const id = crypto.randomBytes(32).toString("hex");
|
||||||
const hashedPassword = password ? await bcrypt.hash(password, 10) : null;
|
const hashedPassword = password ? await bcrypt.hash(password, 10) : null;
|
||||||
|
|
||||||
const newSharedSecret = await secretSharingDAL.create({
|
const newSharedSecret = await secretSharingDAL.create({
|
||||||
|
identifier: id,
|
||||||
|
iv: null,
|
||||||
|
tag: null,
|
||||||
|
encryptedValue: null,
|
||||||
|
encryptedSecret,
|
||||||
name,
|
name,
|
||||||
password: hashedPassword,
|
password: hashedPassword,
|
||||||
encryptedValue,
|
|
||||||
hashedHex,
|
|
||||||
iv,
|
|
||||||
tag,
|
|
||||||
expiresAt: new Date(expiresAt),
|
expiresAt: new Date(expiresAt),
|
||||||
expiresAfterViews,
|
expiresAfterViews,
|
||||||
userId: actorId,
|
userId: actorId,
|
||||||
@@ -79,15 +90,14 @@ export const secretSharingServiceFactory = ({
|
|||||||
accessType
|
accessType
|
||||||
});
|
});
|
||||||
|
|
||||||
return { id: newSharedSecret.id };
|
const idToReturn = `${Buffer.from(newSharedSecret.identifier!, "hex").toString("base64url")}`;
|
||||||
|
|
||||||
|
return { id: idToReturn };
|
||||||
};
|
};
|
||||||
|
|
||||||
const createPublicSharedSecret = async ({
|
const createPublicSharedSecret = async ({
|
||||||
password,
|
password,
|
||||||
encryptedValue,
|
secretValue,
|
||||||
hashedHex,
|
|
||||||
iv,
|
|
||||||
tag,
|
|
||||||
expiresAt,
|
expiresAt,
|
||||||
expiresAfterViews,
|
expiresAfterViews,
|
||||||
accessType
|
accessType
|
||||||
@@ -104,24 +114,25 @@ export const secretSharingServiceFactory = ({
|
|||||||
throw new BadRequestError({ message: "Expiration date cannot exceed more than 30 days" });
|
throw new BadRequestError({ message: "Expiration date cannot exceed more than 30 days" });
|
||||||
}
|
}
|
||||||
|
|
||||||
// Limit Input ciphertext length to 13000 (equivalent to 10,000 characters of Plaintext)
|
const encryptWithRoot = kmsService.encryptWithRootKey();
|
||||||
if (encryptedValue.length > 13000) {
|
const encryptedSecret = encryptWithRoot(Buffer.from(secretValue));
|
||||||
throw new BadRequestError({ message: "Shared secret value too long" });
|
|
||||||
}
|
|
||||||
|
|
||||||
|
const id = crypto.randomBytes(32).toString("hex");
|
||||||
const hashedPassword = password ? await bcrypt.hash(password, 10) : null;
|
const hashedPassword = password ? await bcrypt.hash(password, 10) : null;
|
||||||
|
|
||||||
const newSharedSecret = await secretSharingDAL.create({
|
const newSharedSecret = await secretSharingDAL.create({
|
||||||
|
identifier: id,
|
||||||
|
encryptedValue: null,
|
||||||
|
iv: null,
|
||||||
|
tag: null,
|
||||||
|
encryptedSecret,
|
||||||
password: hashedPassword,
|
password: hashedPassword,
|
||||||
encryptedValue,
|
|
||||||
hashedHex,
|
|
||||||
iv,
|
|
||||||
tag,
|
|
||||||
expiresAt: new Date(expiresAt),
|
expiresAt: new Date(expiresAt),
|
||||||
expiresAfterViews,
|
expiresAfterViews,
|
||||||
accessType
|
accessType
|
||||||
});
|
});
|
||||||
|
|
||||||
return { id: newSharedSecret.id };
|
return { id: `${Buffer.from(newSharedSecret.identifier!, "hex").toString("base64url")}` };
|
||||||
};
|
};
|
||||||
|
|
||||||
const getSharedSecrets = async ({
|
const getSharedSecrets = async ({
|
||||||
@@ -162,25 +173,30 @@ export const secretSharingServiceFactory = ({
|
|||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
const $decrementSecretViewCount = async (sharedSecret: TSecretSharing, sharedSecretId: string) => {
|
const $decrementSecretViewCount = async (sharedSecret: TSecretSharing) => {
|
||||||
const { expiresAfterViews } = sharedSecret;
|
const { expiresAfterViews } = sharedSecret;
|
||||||
|
|
||||||
if (expiresAfterViews) {
|
if (expiresAfterViews) {
|
||||||
// decrement view count if view count expiry set
|
// decrement view count if view count expiry set
|
||||||
await secretSharingDAL.updateById(sharedSecretId, { $decr: { expiresAfterViews: 1 } });
|
await secretSharingDAL.updateById(sharedSecret.id, { $decr: { expiresAfterViews: 1 } });
|
||||||
}
|
}
|
||||||
|
|
||||||
await secretSharingDAL.updateById(sharedSecretId, {
|
await secretSharingDAL.updateById(sharedSecret.id, {
|
||||||
lastViewedAt: new Date()
|
lastViewedAt: new Date()
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
/** Get's passwordless secret. validates all secret's requested (must be fresh). */
|
/** Get's password-less secret. validates all secret's requested (must be fresh). */
|
||||||
const getSharedSecretById = async ({ sharedSecretId, hashedHex, orgId, password }: TGetActiveSharedSecretByIdDTO) => {
|
const getSharedSecretById = async ({ sharedSecretId, hashedHex, orgId, password }: TGetActiveSharedSecretByIdDTO) => {
|
||||||
const sharedSecret = await secretSharingDAL.findOne({
|
const sharedSecret = isUuidV4(sharedSecretId)
|
||||||
id: sharedSecretId,
|
? await secretSharingDAL.findOne({
|
||||||
hashedHex
|
id: sharedSecretId,
|
||||||
});
|
hashedHex
|
||||||
|
})
|
||||||
|
: await secretSharingDAL.findOne({
|
||||||
|
identifier: Buffer.from(sharedSecretId, "base64url").toString("hex")
|
||||||
|
});
|
||||||
|
|
||||||
if (!sharedSecret)
|
if (!sharedSecret)
|
||||||
throw new NotFoundError({
|
throw new NotFoundError({
|
||||||
message: "Shared secret not found"
|
message: "Shared secret not found"
|
||||||
@@ -222,13 +238,23 @@ export const secretSharingServiceFactory = ({
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// If encryptedSecret is set, we know that this secret has been encrypted using KMS, and we can therefore do server-side decryption.
|
||||||
|
let decryptedSecretValue: Buffer | undefined;
|
||||||
|
if (sharedSecret.encryptedSecret) {
|
||||||
|
const decryptWithRoot = kmsService.decryptWithRootKey();
|
||||||
|
decryptedSecretValue = decryptWithRoot(sharedSecret.encryptedSecret);
|
||||||
|
}
|
||||||
|
|
||||||
// decrement when we are sure the user will view secret.
|
// decrement when we are sure the user will view secret.
|
||||||
await $decrementSecretViewCount(sharedSecret, sharedSecretId);
|
await $decrementSecretViewCount(sharedSecret);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
isPasswordProtected,
|
isPasswordProtected,
|
||||||
secret: {
|
secret: {
|
||||||
...sharedSecret,
|
...sharedSecret,
|
||||||
|
...(decryptedSecretValue && {
|
||||||
|
secretValue: Buffer.from(decryptedSecretValue).toString()
|
||||||
|
}),
|
||||||
orgName:
|
orgName:
|
||||||
sharedSecret.accessType === SecretSharingAccessType.Organization && orgId === sharedSecret.orgId
|
sharedSecret.accessType === SecretSharingAccessType.Organization && orgId === sharedSecret.orgId
|
||||||
? orgName
|
? orgName
|
||||||
@@ -241,7 +267,16 @@ export const secretSharingServiceFactory = ({
|
|||||||
const { actor, actorId, orgId, actorAuthMethod, actorOrgId, sharedSecretId } = deleteSharedSecretInput;
|
const { actor, actorId, orgId, actorAuthMethod, actorOrgId, sharedSecretId } = deleteSharedSecretInput;
|
||||||
const { permission } = await permissionService.getOrgPermission(actor, actorId, orgId, actorAuthMethod, actorOrgId);
|
const { permission } = await permissionService.getOrgPermission(actor, actorId, orgId, actorAuthMethod, actorOrgId);
|
||||||
if (!permission) throw new ForbiddenRequestError({ name: "User does not belong to the specified organization" });
|
if (!permission) throw new ForbiddenRequestError({ name: "User does not belong to the specified organization" });
|
||||||
|
|
||||||
|
const sharedSecret = isUuidV4(sharedSecretId)
|
||||||
|
? await secretSharingDAL.findById(sharedSecretId)
|
||||||
|
: await secretSharingDAL.findOne({ identifier: sharedSecretId });
|
||||||
|
|
||||||
const deletedSharedSecret = await secretSharingDAL.deleteById(sharedSecretId);
|
const deletedSharedSecret = await secretSharingDAL.deleteById(sharedSecretId);
|
||||||
|
|
||||||
|
if (sharedSecret.orgId && sharedSecret.orgId !== orgId)
|
||||||
|
throw new ForbiddenRequestError({ message: "User does not have permission to delete shared secret" });
|
||||||
|
|
||||||
return deletedSharedSecret;
|
return deletedSharedSecret;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -19,10 +19,7 @@ export type TSharedSecretPermission = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export type TCreatePublicSharedSecretDTO = {
|
export type TCreatePublicSharedSecretDTO = {
|
||||||
encryptedValue: string;
|
secretValue: string;
|
||||||
hashedHex: string;
|
|
||||||
iv: string;
|
|
||||||
tag: string;
|
|
||||||
expiresAt: string;
|
expiresAt: string;
|
||||||
expiresAfterViews?: number;
|
expiresAfterViews?: number;
|
||||||
password?: string;
|
password?: string;
|
||||||
@@ -31,7 +28,7 @@ export type TCreatePublicSharedSecretDTO = {
|
|||||||
|
|
||||||
export type TGetActiveSharedSecretByIdDTO = {
|
export type TGetActiveSharedSecretByIdDTO = {
|
||||||
sharedSecretId: string;
|
sharedSecretId: string;
|
||||||
hashedHex: string;
|
hashedHex?: string;
|
||||||
orgId?: string;
|
orgId?: string;
|
||||||
password?: string;
|
password?: string;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -141,16 +141,14 @@ export const slackServiceFactory = ({
|
|||||||
let slackClientId = appCfg.WORKFLOW_SLACK_CLIENT_ID as string;
|
let slackClientId = appCfg.WORKFLOW_SLACK_CLIENT_ID as string;
|
||||||
let slackClientSecret = appCfg.WORKFLOW_SLACK_CLIENT_SECRET as string;
|
let slackClientSecret = appCfg.WORKFLOW_SLACK_CLIENT_SECRET as string;
|
||||||
|
|
||||||
const decrypt = await kmsService.decryptWithRootKey();
|
const decrypt = kmsService.decryptWithRootKey();
|
||||||
|
|
||||||
if (serverCfg.encryptedSlackClientId) {
|
if (serverCfg.encryptedSlackClientId) {
|
||||||
slackClientId = (await decrypt({ cipherTextBlob: Buffer.from(serverCfg.encryptedSlackClientId) })).toString();
|
slackClientId = decrypt(Buffer.from(serverCfg.encryptedSlackClientId)).toString();
|
||||||
}
|
}
|
||||||
|
|
||||||
if (serverCfg.encryptedSlackClientSecret) {
|
if (serverCfg.encryptedSlackClientSecret) {
|
||||||
slackClientSecret = (
|
slackClientSecret = decrypt(Buffer.from(serverCfg.encryptedSlackClientSecret)).toString();
|
||||||
await decrypt({ cipherTextBlob: Buffer.from(serverCfg.encryptedSlackClientSecret) })
|
|
||||||
).toString();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!slackClientId || !slackClientSecret) {
|
if (!slackClientId || !slackClientSecret) {
|
||||||
|
|||||||
@@ -122,20 +122,16 @@ export const superAdminServiceFactory = ({
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const encryptWithRoot = await kmsService.encryptWithRootKey();
|
const encryptWithRoot = kmsService.encryptWithRootKey();
|
||||||
if (data.slackClientId) {
|
if (data.slackClientId) {
|
||||||
const { cipherTextBlob: encryptedClientId } = await encryptWithRoot({
|
const encryptedClientId = encryptWithRoot(Buffer.from(data.slackClientId));
|
||||||
plainText: Buffer.from(data.slackClientId)
|
|
||||||
});
|
|
||||||
|
|
||||||
updatedData.encryptedSlackClientId = encryptedClientId;
|
updatedData.encryptedSlackClientId = encryptedClientId;
|
||||||
updatedData.slackClientId = undefined;
|
updatedData.slackClientId = undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (data.slackClientSecret) {
|
if (data.slackClientSecret) {
|
||||||
const { cipherTextBlob: encryptedClientSecret } = await encryptWithRoot({
|
const encryptedClientSecret = encryptWithRoot(Buffer.from(data.slackClientSecret));
|
||||||
plainText: Buffer.from(data.slackClientSecret)
|
|
||||||
});
|
|
||||||
|
|
||||||
updatedData.encryptedSlackClientSecret = encryptedClientSecret;
|
updatedData.encryptedSlackClientSecret = encryptedClientSecret;
|
||||||
updatedData.slackClientSecret = undefined;
|
updatedData.slackClientSecret = undefined;
|
||||||
@@ -270,14 +266,14 @@ export const superAdminServiceFactory = ({
|
|||||||
let clientId = "";
|
let clientId = "";
|
||||||
let clientSecret = "";
|
let clientSecret = "";
|
||||||
|
|
||||||
const decrypt = await kmsService.decryptWithRootKey();
|
const decrypt = kmsService.decryptWithRootKey();
|
||||||
|
|
||||||
if (serverCfg.encryptedSlackClientId) {
|
if (serverCfg.encryptedSlackClientId) {
|
||||||
clientId = (await decrypt({ cipherTextBlob: serverCfg.encryptedSlackClientId })).toString();
|
clientId = decrypt(serverCfg.encryptedSlackClientId).toString();
|
||||||
}
|
}
|
||||||
|
|
||||||
if (serverCfg.encryptedSlackClientSecret) {
|
if (serverCfg.encryptedSlackClientSecret) {
|
||||||
clientSecret = (await decrypt({ cipherTextBlob: serverCfg.encryptedSlackClientSecret })).toString();
|
clientSecret = decrypt(serverCfg.encryptedSlackClientSecret).toString();
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
|||||||
@@ -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}"
|
||||||
|
---
|
||||||
@@ -0,0 +1,168 @@
|
|||||||
|
---
|
||||||
|
title: "LDAP"
|
||||||
|
description: "Learn how to dynamically generate user credentials via LDAP."
|
||||||
|
---
|
||||||
|
|
||||||
|
The Infisical LDAP dynamic secret allows you to generate user credentials on demand via LDAP. The integration is general to any LDAP implementation but has been tested with OpenLDAP and Active directory as of now.
|
||||||
|
|
||||||
|
## Prerequisites
|
||||||
|
|
||||||
|
1. Create a user with the necessary permissions to create users in your LDAP server.
|
||||||
|
2. Ensure your LDAP server is reachable via Infisical instance.
|
||||||
|
|
||||||
|
## Set up Dynamic Secrets with LDAP
|
||||||
|
|
||||||
|
<Steps>
|
||||||
|
<Step title="Open Secret Overview Dashboard">
|
||||||
|
Open the Secret Overview dashboard and select the environment in which you would like to add a dynamic secret.
|
||||||
|
</Step>
|
||||||
|
<Step title="Click on the 'Add Dynamic Secret' button">
|
||||||
|

|
||||||
|
</Step>
|
||||||
|
<Step title="Select 'LDAP'">
|
||||||
|

|
||||||
|
</Step>
|
||||||
|
|
||||||
|
<Step title="Provide the inputs for dynamic secret parameters">
|
||||||
|
<ParamField path="Secret Name" type="string" required>
|
||||||
|
Name by which you want the secret to be referenced
|
||||||
|
</ParamField>
|
||||||
|
|
||||||
|
<ParamField path="Default TTL" type="string" required>
|
||||||
|
Default time-to-live for a generated secret (it is possible to modify this value when a secret is generate)
|
||||||
|
</ParamField>
|
||||||
|
|
||||||
|
<ParamField path="Max TTL" type="string" required>
|
||||||
|
Maximum time-to-live for a generated secret.
|
||||||
|
</ParamField>
|
||||||
|
|
||||||
|
<ParamField path="URL" type="string" required>
|
||||||
|
LDAP url to connect to. _(Example: ldap://your-ldap-ip:389 or ldaps://domain:636)_
|
||||||
|
</ParamField>
|
||||||
|
|
||||||
|
<ParamField path="BIND DN" type="string" required>
|
||||||
|
DN to bind to. This should have permissions to create a new users.
|
||||||
|
</ParamField>
|
||||||
|
|
||||||
|
<ParamField path="BIND Password" type="string" required>
|
||||||
|
Password for the given DN.
|
||||||
|
</ParamField>
|
||||||
|
|
||||||
|
<ParamField path="CA" type="text">
|
||||||
|
CA certificate to use for TLS in case of a secure connection.
|
||||||
|
</ParamField>
|
||||||
|
|
||||||
|
<ParamField path="Creation LDIF" type="text" required>
|
||||||
|
LDIF to run while creating a user in LDAP. This can include extra steps to assign the user to groups or set permissions.
|
||||||
|
Here `{{Username}}`, `{{Password}}` and `{{EncodedPassword}}` are templatized variables for the username and password generated by the dynamic secret.
|
||||||
|
|
||||||
|
`{{EncodedPassword}}` is the encoded password required for the `unicodePwd` field in Active Directory as described [here](https://learn.microsoft.com/en-us/troubleshoot/windows-server/active-directory/change-windows-active-directory-user-password).
|
||||||
|
|
||||||
|
**OpenLDAP** Example:
|
||||||
|
```
|
||||||
|
dn: uid={{Username}},dc=infisical,dc=com
|
||||||
|
changetype: add
|
||||||
|
objectClass: top
|
||||||
|
objectClass: person
|
||||||
|
objectClass: organizationalPerson
|
||||||
|
objectClass: inetOrgPerson
|
||||||
|
cn: John Doe
|
||||||
|
sn: Doe
|
||||||
|
uid: jdoe
|
||||||
|
mail: [email protected]
|
||||||
|
userPassword: {{Password}}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Active Directory** Example:
|
||||||
|
```
|
||||||
|
dn: CN={{Username}},OU=Test Create,DC=infisical,DC=com
|
||||||
|
changetype: add
|
||||||
|
objectClass: top
|
||||||
|
objectClass: person
|
||||||
|
objectClass: organizationalPerson
|
||||||
|
objectClass: user
|
||||||
|
userPrincipalName: {{Username}}@infisical.com
|
||||||
|
sAMAccountName: {{Username}}
|
||||||
|
unicodePwd::{{EncodedPassword}}
|
||||||
|
userAccountControl: 66048
|
||||||
|
|
||||||
|
dn: CN=test-group,OU=Test Create,DC=infisical,DC=com
|
||||||
|
changetype: modify
|
||||||
|
add: member
|
||||||
|
member: CN={{Username}},OU=Test Create,DC=infisical,DC=com
|
||||||
|
-
|
||||||
|
```
|
||||||
|
</ParamField>
|
||||||
|
|
||||||
|
<ParamField path="Revocation LDIF" type="text" required>
|
||||||
|
LDIF to run while revoking a user in LDAP. This can include extra steps to remove the user from groups or set permissions.
|
||||||
|
Here `{{Username}}` is a templatized variable for the username generated by the dynamic secret.
|
||||||
|
|
||||||
|
**OpenLDAP / Active Directory** Example:
|
||||||
|
```
|
||||||
|
dn: CN={{Username}},OU=Test Create,DC=infisical,DC=com
|
||||||
|
changetype: delete
|
||||||
|
```
|
||||||
|
</ParamField>
|
||||||
|
|
||||||
|
<ParamField path="Rollback LDIF" type="text">
|
||||||
|
LDIF to run incase Creation LDIF fails midway.
|
||||||
|
|
||||||
|
For the creation example shown above, if the user is created successfully but not added to a group, this LDIF can be used to remove the user.
|
||||||
|
Here `{{Username}}`, `{{Password}}` and `{{EncodedPassword}}` are templatized variables for the username generated by the dynamic secret.
|
||||||
|
|
||||||
|
**OpenLDAP / Active Directory** Example:
|
||||||
|
```
|
||||||
|
dn: CN={{Username}},OU=Test Create,DC=infisical,DC=com
|
||||||
|
changetype: delete
|
||||||
|
```
|
||||||
|
</ParamField>
|
||||||
|
|
||||||
|
</Step>
|
||||||
|
|
||||||
|
<Step title="Click `Submit`">
|
||||||
|
After submitting the form, you will see a dynamic secret created in the dashboard.
|
||||||
|
</Step>
|
||||||
|
<Step title="Generate dynamic secrets">
|
||||||
|
Once you've successfully configured the dynamic secret, you're ready to generate on-demand credentials.
|
||||||
|
To do this, simply click on the 'Generate' button which appears when hovering over the dynamic secret item.
|
||||||
|
Alternatively, you can initiate the creation of a new lease by selecting 'New Lease' from the dynamic secret lease list section.
|
||||||
|
|
||||||
|

|
||||||
|

|
||||||
|
|
||||||
|
When generating these secrets, it's important to specify a Time-to-Live (TTL) duration. This will dictate how long the credentials are valid for.
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
<Tip>
|
||||||
|
Ensure that the TTL for the lease fall within the maximum TTL defined when configuring the dynamic secret.
|
||||||
|
</Tip>
|
||||||
|
|
||||||
|
|
||||||
|
Once you click the `Submit` button, a new secret lease will be generated and the credentials from it will be shown to you with an array of DN's altered depending on the Creation LDIF.
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
</Step>
|
||||||
|
|
||||||
|
</Steps>
|
||||||
|
|
||||||
|
## Active Directory Integration
|
||||||
|
|
||||||
|
- Passwords in Active Directory are set using the `unicodePwd` field. This must be proceeded by two colons `::` as shown in the example. [Source](https://learn.microsoft.com/en-us/troubleshoot/windows-server/active-directory/change-windows-active-directory-user-password)
|
||||||
|
- Active directory uses the `userAccountControl` field to enable account. [Read More](https://learn.microsoft.com/en-us/troubleshoot/windows-server/active-directory/useraccountcontrol-manipulate-account-properties)
|
||||||
|
- `userAccountControl` set to `512` enables a user.
|
||||||
|
- To disable AD's password expiration for this dynamic user account. The `userAccountControl` value for this is: `65536`.
|
||||||
|
- Since `userAccountControl` flag is cumulative set it to `512 + 65536` = `66048` to do both.
|
||||||
|
- Active Directory does not permit direct modification of a user's `memberOf` attribute. The member attribute of a group and the `memberOf` attribute of a user are [linked attributes](https://learn.microsoft.com/en-us/windows/win32/ad/linked-attributes), where the member attribute represents the forward link, which can be modified. In the context of AD group membership, the group's `member` attribute serves as the forward link. Therefore, to add a newly created dynamic user to a group, a modification request must be issued to the desired group, updating its membership to include the new user.
|
||||||
|
|
||||||
|
## LDIF Entries
|
||||||
|
|
||||||
|
User account management is handled through **LDIF entries**.
|
||||||
|
|
||||||
|
#### Things to Remember
|
||||||
|
|
||||||
|
- **No trailing spaces:** Ensure there are no trailing spaces on any line, including blank lines.
|
||||||
|
- **Empty lines before modify blocks:** Every modify block must be preceded by an empty line.
|
||||||
|
- **Multiple modifications:** You can define multiple modifications for a DN within a single modify block. Each modification should end with a single dash (`-`).
|
||||||
@@ -7,7 +7,7 @@ description: "Learn how to authenticate with Infisical for EC2 instances, Lambda
|
|||||||
|
|
||||||
## Diagram
|
## Diagram
|
||||||
|
|
||||||
The following sequence digram illustrates the AWS Auth workflow for authenticating AWS IAM principals with Infisical.
|
The following sequence diagram illustrates the AWS Auth workflow for authenticating AWS IAM principals with Infisical.
|
||||||
|
|
||||||
```mermaid
|
```mermaid
|
||||||
sequenceDiagram
|
sequenceDiagram
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ description: "Learn how to authenticate with Infisical for services on Azure"
|
|||||||
|
|
||||||
## Diagram
|
## Diagram
|
||||||
|
|
||||||
The following sequence digram illustrates the Azure Auth workflow for authenticating Azure [service principals](https://learn.microsoft.com/en-us/entra/identity-platform/app-objects-and-service-principals?tabs=browser) with Infisical.
|
The following sequence diagram illustrates the Azure Auth workflow for authenticating Azure [service principals](https://learn.microsoft.com/en-us/entra/identity-platform/app-objects-and-service-principals?tabs=browser) with Infisical.
|
||||||
|
|
||||||
```mermaid
|
```mermaid
|
||||||
sequenceDiagram
|
sequenceDiagram
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ description: "Learn how to authenticate with Infisical for services on Google Cl
|
|||||||
|
|
||||||
## Diagram
|
## Diagram
|
||||||
|
|
||||||
The following sequence digram illustrates the GCP ID Token Auth workflow for authenticating GCP resources with Infisical.
|
The following sequence diagram illustrates the GCP ID Token Auth workflow for authenticating GCP resources with Infisical.
|
||||||
|
|
||||||
```mermaid
|
```mermaid
|
||||||
sequenceDiagram
|
sequenceDiagram
|
||||||
@@ -182,7 +182,7 @@ access the Infisical API using the GCP ID Token authentication method.
|
|||||||
|
|
||||||
## Diagram
|
## Diagram
|
||||||
|
|
||||||
The following sequence digram illustrates the GCP IAM Auth workflow for authenticating GCP IAM service accounts with Infisical.
|
The following sequence diagram illustrates the GCP IAM Auth workflow for authenticating GCP IAM service accounts with Infisical.
|
||||||
|
|
||||||
```mermaid
|
```mermaid
|
||||||
sequenceDiagram
|
sequenceDiagram
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ description: "Learn how to authenticate with Infisical in Kubernetes"
|
|||||||
|
|
||||||
## Diagram
|
## Diagram
|
||||||
|
|
||||||
The following sequence digram illustrates the Kubernetes Auth workflow for authenticating applications running in pods with Infisical.
|
The following sequence diagram illustrates the Kubernetes Auth workflow for authenticating applications running in pods with Infisical.
|
||||||
|
|
||||||
```mermaid
|
```mermaid
|
||||||
sequenceDiagram
|
sequenceDiagram
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ description: "Learn how to authenticate to Infisical from any platform or enviro
|
|||||||
|
|
||||||
## Diagram
|
## Diagram
|
||||||
|
|
||||||
The following sequence digram illustrates the Token Auth workflow for authenticating clients with Infisical.
|
The following sequence diagram illustrates the Token Auth workflow for authenticating clients with Infisical.
|
||||||
|
|
||||||
```mermaid
|
```mermaid
|
||||||
sequenceDiagram
|
sequenceDiagram
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ description: "Learn how to authenticate to Infisical from any platform or enviro
|
|||||||
|
|
||||||
## Diagram
|
## Diagram
|
||||||
|
|
||||||
The following sequence digram illustrates the Universal Auth workflow for authenticating clients with Infisical.
|
The following sequence diagram illustrates the Universal Auth workflow for authenticating clients with Infisical.
|
||||||
|
|
||||||
```mermaid
|
```mermaid
|
||||||
sequenceDiagram
|
sequenceDiagram
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
---
|
---
|
||||||
title: "Key Management Service (KMS)"
|
title: "Key Management Service (KMS) Configuration"
|
||||||
sidebarTitle: "Overview"
|
sidebarTitle: "Overview"
|
||||||
description: "Learn how to configure your project's encryption"
|
description: "Learn how to configure your project's encryption"
|
||||||
---
|
---
|
||||||
@@ -0,0 +1,208 @@
|
|||||||
|
---
|
||||||
|
title: "Key Management Service (KMS)"
|
||||||
|
sidebarTitle: "Key Management (KMS)"
|
||||||
|
description: "Learn how to manage and use cryptographic keys with Infisical."
|
||||||
|
---
|
||||||
|
|
||||||
|
## Concept
|
||||||
|
|
||||||
|
Infisical can be used as a Key Management System (KMS), referred to as Infisical KMS, to centralize management of keys to be used for cryptographic operations like encryption/decryption.
|
||||||
|
|
||||||
|
<Note>
|
||||||
|
Keys managed in KMS are not extractable from the platform. Additionally, data
|
||||||
|
is never stored when performing cryptographic operations.
|
||||||
|
</Note>
|
||||||
|
|
||||||
|
## Workflow
|
||||||
|
|
||||||
|
The typical workflow for using Infisical KMS consists of the following steps:
|
||||||
|
|
||||||
|
1. Creating a KMS key. As part of this step, you specify a name for the key and the encryption algorithm meant to be used for it (e.g. `AES-GCM-128`, `AES-GCM-256`).
|
||||||
|
2. Encryption: To encrypt data, you would make a request to the Infisical KMS API endpoint, specifying the base64-encoded plaintext and the intended key to use for encryption; the API would return the base64-encoded ciphertext.
|
||||||
|
3. Decryption: To decrypt data, you would make a request to the Infisical KMS API endpoint, specifying the base64-encoded ciphertext and the intended key to use for decryption; the API would return the base64-encoded plaintext.
|
||||||
|
|
||||||
|
<Note>
|
||||||
|
Note that this workflow can be executed via the Infisical UI or manually such
|
||||||
|
as via API.
|
||||||
|
</Note>
|
||||||
|
|
||||||
|
## Guide to Encrypting Data
|
||||||
|
|
||||||
|
In the following steps, we explore how to generate a key and use it to encrypt data.
|
||||||
|
|
||||||
|
<Tabs>
|
||||||
|
<Tab title="Infisical UI">
|
||||||
|
<Steps>
|
||||||
|
<Step title="Creating a KMS key">
|
||||||
|
Navigate to Project > Key Management and tap on the **Add Key** button.
|
||||||
|

|
||||||
|
|
||||||
|
Specify your key details. Here's some guidance on each field:
|
||||||
|
|
||||||
|
- Name: A slug-friendly name for the key.
|
||||||
|
- Type: The encryption algorithm associated with the key (e.g. `AES-GCM-256`).
|
||||||
|
- Description: An optional description of what the intended usage is for the key.
|
||||||
|
|
||||||
|

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

|
||||||
|
|
||||||
|
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 KMS key">
|
||||||
|
To create a cryptographic key, make an API request to the [Create KMS
|
||||||
|
Key](/api-reference/endpoints/kms/keys/create) API endpoint.
|
||||||
|
|
||||||
|
### Sample request
|
||||||
|
|
||||||
|
```bash Request
|
||||||
|
curl --request POST \
|
||||||
|
--url https://app.infisical.com/api/v1/kms/keys \
|
||||||
|
--header 'Content-Type: application/json' \
|
||||||
|
--data '{
|
||||||
|
"projectId": "<project-id>",
|
||||||
|
"name": "my-secret-key",
|
||||||
|
"description": "...",
|
||||||
|
"encryptionAlgorithm": "aes-256-gcm"
|
||||||
|
}'
|
||||||
|
```
|
||||||
|
|
||||||
|
### Sample response
|
||||||
|
|
||||||
|
```bash Response
|
||||||
|
{
|
||||||
|
"key": {
|
||||||
|
"id": "<key-id>",
|
||||||
|
"description": "...",
|
||||||
|
"isDisabled": false,
|
||||||
|
"isReserved": false,
|
||||||
|
"orgId": "<org-id>",
|
||||||
|
"name": "my-secret-key",
|
||||||
|
"createdAt": "2023-11-07T05:31:56Z",
|
||||||
|
"updatedAt": "2023-11-07T05:31:56Z",
|
||||||
|
"projectId": "<project-id>"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
</Step>
|
||||||
|
<Step title="Encrypting data with the KMS key">
|
||||||
|
To encrypt data, make an API request to the [Encrypt
|
||||||
|
Data](/api-reference/endpoints/kms/keys/encrypt) API endpoint,
|
||||||
|
specifying the key to use.
|
||||||
|
|
||||||
|
<Note>
|
||||||
|
Make sure your data is Base64 encoded
|
||||||
|
</Note>
|
||||||
|
|
||||||
|
### Sample request
|
||||||
|
|
||||||
|
```bash Request
|
||||||
|
curl --request POST \
|
||||||
|
--url https://app.infisical.com/api/v1/kms/keys/<key-id>/encrypt \
|
||||||
|
--header 'Content-Type: application/json' \
|
||||||
|
--data '{
|
||||||
|
"plaintext": "lUFHM5Ggwo6TOfpuN1S==" // base64 encoded plaintext
|
||||||
|
}'
|
||||||
|
```
|
||||||
|
|
||||||
|
### Sample response
|
||||||
|
|
||||||
|
```bash Response
|
||||||
|
{
|
||||||
|
"ciphertext": "HwFHwSFHwlMF6TOfp==" // base64 encoded ciphertext
|
||||||
|
}
|
||||||
|
```
|
||||||
|
</Step>
|
||||||
|
</Steps>
|
||||||
|
</Tab>
|
||||||
|
|
||||||
|
</Tabs>
|
||||||
|
|
||||||
|
## Guide to Decrypting Data
|
||||||
|
|
||||||
|
In the following steps, we explore how to use decrypt data using an existing key in Infisical KMS.
|
||||||
|
|
||||||
|
<Tabs>
|
||||||
|
<Tab title="Infisical UI">
|
||||||
|
<Steps>
|
||||||
|
<Step title="Accessing your key">
|
||||||
|
Navigate to Project > Key Management and open the options menu for the key used to encrypt the data
|
||||||
|
you want to decrypt.
|
||||||
|

|
||||||
|
|
||||||
|
</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>
|
||||||
|
<Accordion title="What algorithms does Infisical KMS support?">
|
||||||
|
Currently, Infisical only supports `AES-128-GCM` and `AES-256-GCM` for
|
||||||
|
encryption operations. We anticipate supporting more algorithms and
|
||||||
|
cryptographic operations in the coming months.
|
||||||
|
</Accordion>
|
||||||
|
</AccordionGroup>
|
||||||
|
After Width: | Height: | Size: 104 KiB |
|
After Width: | Height: | Size: 490 KiB |
|
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 |
@@ -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": [
|
||||||
@@ -168,15 +177,8 @@
|
|||||||
"documentation/platform/dynamic-secrets/aws-iam",
|
"documentation/platform/dynamic-secrets/aws-iam",
|
||||||
"documentation/platform/dynamic-secrets/mongo-atlas",
|
"documentation/platform/dynamic-secrets/mongo-atlas",
|
||||||
"documentation/platform/dynamic-secrets/mongo-db",
|
"documentation/platform/dynamic-secrets/mongo-db",
|
||||||
"documentation/platform/dynamic-secrets/azure-entra-id"
|
"documentation/platform/dynamic-secrets/azure-entra-id",
|
||||||
]
|
"documentation/platform/dynamic-secrets/ldap"
|
||||||
},
|
|
||||||
{
|
|
||||||
"group": "Key Management (KMS)",
|
|
||||||
"pages": [
|
|
||||||
"documentation/platform/kms/overview",
|
|
||||||
"documentation/platform/kms/aws-kms",
|
|
||||||
"documentation/platform/kms/aws-hsm"
|
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -789,6 +791,22 @@
|
|||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"group": "Infisical KMS",
|
||||||
|
"pages": [
|
||||||
|
{
|
||||||
|
"group": "Keys",
|
||||||
|
"pages": [
|
||||||
|
"api-reference/endpoints/kms/keys/list",
|
||||||
|
"api-reference/endpoints/kms/keys/create",
|
||||||
|
"api-reference/endpoints/kms/keys/update",
|
||||||
|
"api-reference/endpoints/kms/keys/delete",
|
||||||
|
"api-reference/endpoints/kms/keys/encrypt",
|
||||||
|
"api-reference/endpoints/kms/keys/decrypt"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"group": "Internals",
|
"group": "Internals",
|
||||||
"pages": [
|
"pages": [
|
||||||
|
|||||||
@@ -86,13 +86,15 @@ export const DropdownMenuItem = <T extends ElementType = "button">({
|
|||||||
icon,
|
icon,
|
||||||
as: Item = "button",
|
as: Item = "button",
|
||||||
iconPos = "left",
|
iconPos = "left",
|
||||||
|
isDisabled = false,
|
||||||
...props
|
...props
|
||||||
}: DropdownMenuItemProps<T> & ComponentPropsWithRef<T>) => (
|
}: DropdownMenuItemProps<T> & ComponentPropsWithRef<T> & { isDisabled?: boolean }) => (
|
||||||
<DropdownMenuPrimitive.Item
|
<DropdownMenuPrimitive.Item
|
||||||
{...props}
|
{...props}
|
||||||
className={twMerge(
|
className={twMerge(
|
||||||
"block cursor-pointer rounded-sm px-4 py-2 font-inter text-xs text-mineshaft-200 outline-none data-[highlighted]:bg-mineshaft-700",
|
"block cursor-pointer rounded-sm px-4 py-2 font-inter text-xs text-mineshaft-200 outline-none data-[highlighted]:bg-mineshaft-700",
|
||||||
className
|
className,
|
||||||
|
isDisabled ? "pointer-events-none opacity-50" : ""
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<Item type="button" role="menuitem" className="flex w-full items-center" ref={inputRef}>
|
<Item type="button" role="menuitem" className="flex w-full items-center" ref={inputRef}>
|
||||||
|
|||||||
@@ -77,7 +77,7 @@ export const InfisicalSecretInput = forwardRef<HTMLTextAreaElement, Props>(
|
|||||||
const { currentWorkspace } = useWorkspace();
|
const { currentWorkspace } = useWorkspace();
|
||||||
const workspaceId = currentWorkspace?.id || "";
|
const workspaceId = currentWorkspace?.id || "";
|
||||||
|
|
||||||
const debouncedValue = useDebounce(value, 500);
|
const [debouncedValue] = useDebounce(value, 500);
|
||||||
|
|
||||||
const [highlightedIndex, setHighlightedIndex] = useState(-1);
|
const [highlightedIndex, setHighlightedIndex] = useState(-1);
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -33,7 +33,7 @@ export const SecretPathInput = ({
|
|||||||
const [suggestions, setSuggestions] = useState<string[]>([]);
|
const [suggestions, setSuggestions] = useState<string[]>([]);
|
||||||
const [isInputFocused, setIsInputFocus] = useState(false);
|
const [isInputFocused, setIsInputFocus] = useState(false);
|
||||||
const [highlightedIndex, setHighlightedIndex] = useState(-1);
|
const [highlightedIndex, setHighlightedIndex] = useState(-1);
|
||||||
const debouncedInputValue = useDebounce(inputValue, 200);
|
const [debouncedInputValue] = useDebounce(inputValue, 200);
|
||||||
|
|
||||||
const { currentWorkspace } = useWorkspace();
|
const { currentWorkspace } = useWorkspace();
|
||||||
const workspaceId = currentWorkspace?.id || "";
|
const workspaceId = currentWorkspace?.id || "";
|
||||||
|
|||||||
@@ -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"
|
||||||
|
}
|
||||||
@@ -25,7 +25,8 @@ export enum DynamicSecretProviders {
|
|||||||
ElasticSearch = "elastic-search",
|
ElasticSearch = "elastic-search",
|
||||||
MongoDB = "mongo-db",
|
MongoDB = "mongo-db",
|
||||||
RabbitMq = "rabbit-mq",
|
RabbitMq = "rabbit-mq",
|
||||||
AzureEntraId = "azure-entra-id"
|
AzureEntraId = "azure-entra-id",
|
||||||
|
Ldap = "ldap"
|
||||||
}
|
}
|
||||||
|
|
||||||
export enum SqlProviders {
|
export enum SqlProviders {
|
||||||
@@ -188,7 +189,20 @@ export type TDynamicSecretProvider =
|
|||||||
applicationId: string;
|
applicationId: string;
|
||||||
clientSecret: string;
|
clientSecret: string;
|
||||||
};
|
};
|
||||||
};
|
}
|
||||||
|
| {
|
||||||
|
type: DynamicSecretProviders.Ldap;
|
||||||
|
inputs: {
|
||||||
|
url: string;
|
||||||
|
binddn: string;
|
||||||
|
bindpass: string;
|
||||||
|
ca?: string | undefined;
|
||||||
|
creationLdif: string;
|
||||||
|
revocationLdif: string;
|
||||||
|
rollbackLdif?: string;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
;
|
||||||
|
|
||||||
export type TCreateDynamicSecretDTO = {
|
export type TCreateDynamicSecretDTO = {
|
||||||
projectSlug: string;
|
projectSlug: string;
|
||||||
|
|||||||
@@ -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"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,13 +3,21 @@ import { useMutation, useQueryClient } from "@tanstack/react-query";
|
|||||||
import { apiRequest } from "@app/config/request";
|
import { apiRequest } from "@app/config/request";
|
||||||
|
|
||||||
import { secretSharingKeys } from "./queries";
|
import { secretSharingKeys } from "./queries";
|
||||||
import { TCreateSharedSecretRequest, TDeleteSharedSecretRequest, TSharedSecret } from "./types";
|
import {
|
||||||
|
TCreatedSharedSecret,
|
||||||
|
TCreateSharedSecretRequest,
|
||||||
|
TDeleteSharedSecretRequest,
|
||||||
|
TSharedSecret
|
||||||
|
} from "./types";
|
||||||
|
|
||||||
export const useCreateSharedSecret = () => {
|
export const useCreateSharedSecret = () => {
|
||||||
const queryClient = useQueryClient();
|
const queryClient = useQueryClient();
|
||||||
return useMutation({
|
return useMutation({
|
||||||
mutationFn: async (inputData: TCreateSharedSecretRequest) => {
|
mutationFn: async (inputData: TCreateSharedSecretRequest) => {
|
||||||
const { data } = await apiRequest.post<TSharedSecret>("/api/v1/secret-sharing", inputData);
|
const { data } = await apiRequest.post<TCreatedSharedSecret>(
|
||||||
|
"/api/v1/secret-sharing",
|
||||||
|
inputData
|
||||||
|
);
|
||||||
return data;
|
return data;
|
||||||
},
|
},
|
||||||
onSuccess: () => queryClient.invalidateQueries(secretSharingKeys.allSharedSecrets())
|
onSuccess: () => queryClient.invalidateQueries(secretSharingKeys.allSharedSecrets())
|
||||||
@@ -20,7 +28,7 @@ export const useCreatePublicSharedSecret = () => {
|
|||||||
const queryClient = useQueryClient();
|
const queryClient = useQueryClient();
|
||||||
return useMutation({
|
return useMutation({
|
||||||
mutationFn: async (inputData: TCreateSharedSecretRequest) => {
|
mutationFn: async (inputData: TCreateSharedSecretRequest) => {
|
||||||
const { data } = await apiRequest.post<TSharedSecret>(
|
const { data } = await apiRequest.post<TCreatedSharedSecret>(
|
||||||
"/api/v1/secret-sharing/public",
|
"/api/v1/secret-sharing/public",
|
||||||
inputData
|
inputData
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ export const secretSharingKeys = {
|
|||||||
allSharedSecrets: () => ["sharedSecrets"] as const,
|
allSharedSecrets: () => ["sharedSecrets"] as const,
|
||||||
specificSharedSecrets: ({ offset, limit }: { offset: number; limit: number }) =>
|
specificSharedSecrets: ({ offset, limit }: { offset: number; limit: number }) =>
|
||||||
[...secretSharingKeys.allSharedSecrets(), { offset, limit }] as const,
|
[...secretSharingKeys.allSharedSecrets(), { offset, limit }] as const,
|
||||||
getSecretById: (arg: { id: string; hashedHex: string; password?: string }) => [
|
getSecretById: (arg: { id: string; hashedHex: string | null; password?: string }) => [
|
||||||
"shared-secret",
|
"shared-secret",
|
||||||
arg
|
arg
|
||||||
]
|
]
|
||||||
@@ -46,7 +46,7 @@ export const useGetActiveSharedSecretById = ({
|
|||||||
password
|
password
|
||||||
}: {
|
}: {
|
||||||
sharedSecretId: string;
|
sharedSecretId: string;
|
||||||
hashedHex: string;
|
hashedHex: string | null;
|
||||||
password?: string;
|
password?: string;
|
||||||
}) => {
|
}) => {
|
||||||
return useQuery<TViewSharedSecretResponse>(
|
return useQuery<TViewSharedSecretResponse>(
|
||||||
@@ -55,7 +55,7 @@ export const useGetActiveSharedSecretById = ({
|
|||||||
const { data } = await apiRequest.post<TViewSharedSecretResponse>(
|
const { data } = await apiRequest.post<TViewSharedSecretResponse>(
|
||||||
`/api/v1/secret-sharing/public/${sharedSecretId}`,
|
`/api/v1/secret-sharing/public/${sharedSecretId}`,
|
||||||
{
|
{
|
||||||
hashedHex,
|
...(hashedHex && { hashedHex }),
|
||||||
password
|
password
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
@@ -63,7 +63,7 @@ export const useGetActiveSharedSecretById = ({
|
|||||||
return data;
|
return data;
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
enabled: Boolean(sharedSecretId) && Boolean(hashedHex)
|
enabled: Boolean(sharedSecretId)
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -13,13 +13,14 @@ export type TSharedSecret = {
|
|||||||
tag: string;
|
tag: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type TCreatedSharedSecret = {
|
||||||
|
id: string;
|
||||||
|
};
|
||||||
|
|
||||||
export type TCreateSharedSecretRequest = {
|
export type TCreateSharedSecretRequest = {
|
||||||
name?: string;
|
name?: string;
|
||||||
password?: string;
|
password?: string;
|
||||||
encryptedValue: string;
|
secretValue: string;
|
||||||
hashedHex: string;
|
|
||||||
iv: string;
|
|
||||||
tag: string;
|
|
||||||
expiresAt: Date;
|
expiresAt: Date;
|
||||||
expiresAfterViews?: number;
|
expiresAfterViews?: number;
|
||||||
accessType?: SecretSharingAccessType;
|
accessType?: SecretSharingAccessType;
|
||||||
@@ -28,6 +29,7 @@ export type TCreateSharedSecretRequest = {
|
|||||||
export type TViewSharedSecretResponse = {
|
export type TViewSharedSecretResponse = {
|
||||||
isPasswordProtected: boolean;
|
isPasswordProtected: boolean;
|
||||||
secret: {
|
secret: {
|
||||||
|
secretValue?: string;
|
||||||
encryptedValue: string;
|
encryptedValue: string;
|
||||||
iv: string;
|
iv: string;
|
||||||
tag: string;
|
tag: string;
|
||||||
@@ -44,4 +46,3 @@ export enum SecretSharingAccessType {
|
|||||||
Anyone = "anyone",
|
Anyone = "anyone",
|
||||||
Organization = "organization"
|
Organization = "organization"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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";
|
||||||
|
|||||||
@@ -1,7 +1,10 @@
|
|||||||
import { useEffect, useState } from "react";
|
import { Dispatch, SetStateAction, useEffect, useState } from "react";
|
||||||
|
|
||||||
// Ref: https://usehooks.com/useDebounce/
|
// Ref: https://usehooks.com/useDebounce/
|
||||||
export const useDebounce = <T extends unknown>(value: T, delay = 500): T => {
|
export const useDebounce = <T extends unknown>(
|
||||||
|
value: T,
|
||||||
|
delay = 500
|
||||||
|
): [T, Dispatch<SetStateAction<T>>] => {
|
||||||
// State and setters for debounced value
|
// State and setters for debounced value
|
||||||
const [debouncedValue, setDebouncedValue] = useState(value);
|
const [debouncedValue, setDebouncedValue] = useState(value);
|
||||||
|
|
||||||
@@ -22,5 +25,5 @@ export const useDebounce = <T extends unknown>(value: T, delay = 500): T => {
|
|||||||
[value, delay] // Only re-call effect if value or delay changes
|
[value, delay] // Only re-call effect if value or delay changes
|
||||||
);
|
);
|
||||||
|
|
||||||
return debouncedValue;
|
return [debouncedValue, setDebouncedValue];
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||