Merge remote-tracking branch 'origin/main' into misc/move-audit-logs-to-dedicated

This commit is contained in:
Sheen Capadngan
2024-10-04 22:16:46 +08:00
135 changed files with 4314 additions and 581 deletions
+5
View File
@@ -73,6 +73,11 @@ We're on a mission to make security tooling more accessible to everyone, not jus
- **[Infisical PKI Issuer for Kubernetes](https://infisical.com/docs/documentation/platform/pki/pki-issuer)**: Deliver TLS certificates to your Kubernetes workloads with automatic renewal. - **[Infisical PKI Issuer for Kubernetes](https://infisical.com/docs/documentation/platform/pki/pki-issuer)**: Deliver TLS certificates to your Kubernetes workloads with automatic renewal.
- **[Enrollment over Secure Transport](https://infisical.com/docs/documentation/platform/pki/est)**: Enroll and manage certificates via EST protocol. - **[Enrollment over Secure Transport](https://infisical.com/docs/documentation/platform/pki/est)**: Enroll and manage certificates via EST protocol.
### Key Management (KMS):
- **[Cryptograhic Keys](https://infisical.com/docs/documentation/platform/kms)**: Centrally manage keys across projects through a user-friendly interface or via the API.
- **[Encrypt and Decrypt Data](https://infisical.com/docs/documentation/platform/kms#guide-to-encrypting-data)**: Use symmetric keys to encrypt and decrypt data.
### General Platform: ### General Platform:
- **Authentication Methods**: Authenticate machine identities with Infisical using a cloud-native or platform agnostic authentication method ([Kubernetes Auth](https://infisical.com/docs/documentation/platform/identities/kubernetes-auth), [GCP Auth](https://infisical.com/docs/documentation/platform/identities/gcp-auth), [Azure Auth](https://infisical.com/docs/documentation/platform/identities/azure-auth), [AWS Auth](https://infisical.com/docs/documentation/platform/identities/aws-auth), [OIDC Auth](https://infisical.com/docs/documentation/platform/identities/oidc-auth/general), [Universal Auth](https://infisical.com/docs/documentation/platform/identities/universal-auth)). - **Authentication Methods**: Authenticate machine identities with Infisical using a cloud-native or platform agnostic authentication method ([Kubernetes Auth](https://infisical.com/docs/documentation/platform/identities/kubernetes-auth), [GCP Auth](https://infisical.com/docs/documentation/platform/identities/gcp-auth), [Azure Auth](https://infisical.com/docs/documentation/platform/identities/azure-auth), [AWS Auth](https://infisical.com/docs/documentation/platform/identities/aws-auth), [OIDC Auth](https://infisical.com/docs/documentation/platform/identities/oidc-auth/general), [Universal Auth](https://infisical.com/docs/documentation/platform/identities/universal-auth)).
- **[Access Controls](https://infisical.com/docs/documentation/platform/access-controls/overview)**: Define advanced authorization controls for users and machine identities with [RBAC](https://infisical.com/docs/documentation/platform/access-controls/role-based-access-controls), [additional privileges](https://infisical.com/docs/documentation/platform/access-controls/additional-privileges), [temporary access](https://infisical.com/docs/documentation/platform/access-controls/temporary-access), [access requests](https://infisical.com/docs/documentation/platform/access-controls/access-requests), [approval workflows](https://infisical.com/docs/documentation/platform/pr-workflows), and more. - **[Access Controls](https://infisical.com/docs/documentation/platform/access-controls/overview)**: Define advanced authorization controls for users and machine identities with [RBAC](https://infisical.com/docs/documentation/platform/access-controls/role-based-access-controls), [additional privileges](https://infisical.com/docs/documentation/platform/access-controls/additional-privileges), [temporary access](https://infisical.com/docs/documentation/platform/access-controls/temporary-access), [access requests](https://infisical.com/docs/documentation/platform/access-controls/access-requests), [approval workflows](https://infisical.com/docs/documentation/platform/pr-workflows), and more.
+7
View File
@@ -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",
+1
View File
@@ -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",
+2
View File
@@ -38,6 +38,7 @@ import { TAuthTokenServiceFactory } from "@app/services/auth-token/auth-token-se
import { TCertificateServiceFactory } from "@app/services/certificate/certificate-service"; import { TCertificateServiceFactory } from "@app/services/certificate/certificate-service";
import { TCertificateAuthorityServiceFactory } from "@app/services/certificate-authority/certificate-authority-service"; import { TCertificateAuthorityServiceFactory } from "@app/services/certificate-authority/certificate-authority-service";
import { TCertificateTemplateServiceFactory } from "@app/services/certificate-template/certificate-template-service"; import { TCertificateTemplateServiceFactory } from "@app/services/certificate-template/certificate-template-service";
import { TCmekServiceFactory } from "@app/services/cmek/cmek-service";
import { TExternalMigrationServiceFactory } from "@app/services/external-migration/external-migration-service"; import { TExternalMigrationServiceFactory } from "@app/services/external-migration/external-migration-service";
import { TGroupProjectServiceFactory } from "@app/services/group-project/group-project-service"; import { TGroupProjectServiceFactory } from "@app/services/group-project/group-project-service";
import { TIdentityServiceFactory } from "@app/services/identity/identity-service"; import { TIdentityServiceFactory } from "@app/services/identity/identity-service";
@@ -182,6 +183,7 @@ declare module "fastify" {
orgAdmin: TOrgAdminServiceFactory; orgAdmin: TOrgAdminServiceFactory;
slack: TSlackServiceFactory; slack: TSlackServiceFactory;
workflowIntegration: TWorkflowIntegrationServiceFactory; workflowIntegration: TWorkflowIntegrationServiceFactory;
cmek: TCmekServiceFactory;
migration: TExternalMigrationServiceFactory; migration: TExternalMigrationServiceFactory;
}; };
// this is exclusive use for middlewares in which we need to inject data // this is exclusive use for middlewares in which we need to inject data
+4
View File
@@ -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};`);
+1 -1
View File
@@ -54,7 +54,7 @@ export const getSecretManagerDataKey = async (knex: Knex, projectId: string) =>
} else { } else {
const [kmsDoc] = await knex(TableName.KmsKey) const [kmsDoc] = await knex(TableName.KmsKey)
.insert({ .insert({
slug: slugify(alphaNumericNanoId(8).toLowerCase()), name: slugify(alphaNumericNanoId(8).toLowerCase()),
orgId: project.orgId, orgId: project.orgId,
isReserved: false isReserved: false
}) })
+3 -2
View File
@@ -13,9 +13,10 @@ export const KmsKeysSchema = z.object({
isDisabled: z.boolean().default(false).nullable().optional(), isDisabled: z.boolean().default(false).nullable().optional(),
isReserved: z.boolean().default(true).nullable().optional(), isReserved: z.boolean().default(true).nullable().optional(),
orgId: z.string().uuid(), orgId: z.string().uuid(),
slug: z.string(), name: z.string(),
createdAt: z.date(), createdAt: z.date(),
updatedAt: z.date() updatedAt: z.date(),
projectId: z.string().nullable().optional()
}); });
export type TKmsKeys = z.infer<typeof KmsKeysSchema>; export type TKmsKeys = z.infer<typeof KmsKeysSchema>;
+9 -5
View File
@@ -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>;
+13 -13
View File
@@ -26,7 +26,7 @@ const sanitizedExternalSchemaForGetAll = KmsKeysSchema.pick({
isDisabled: true, isDisabled: true,
createdAt: true, createdAt: true,
updatedAt: true, updatedAt: true,
slug: true name: true
}) })
.extend({ .extend({
externalKms: ExternalKmsSchema.pick({ externalKms: ExternalKmsSchema.pick({
@@ -57,7 +57,7 @@ export const registerExternalKmsRouter = async (server: FastifyZodProvider) => {
}, },
schema: { schema: {
body: z.object({ body: z.object({
slug: z.string().min(1).trim().toLowerCase(), name: z.string().min(1).trim().toLowerCase(),
description: z.string().trim().optional(), description: z.string().trim().optional(),
provider: ExternalKmsInputSchema provider: ExternalKmsInputSchema
}), }),
@@ -74,7 +74,7 @@ export const registerExternalKmsRouter = async (server: FastifyZodProvider) => {
actorId: req.permission.id, actorId: req.permission.id,
actorAuthMethod: req.permission.authMethod, actorAuthMethod: req.permission.authMethod,
actorOrgId: req.permission.orgId, actorOrgId: req.permission.orgId,
slug: req.body.slug, name: req.body.name,
provider: req.body.provider, provider: req.body.provider,
description: req.body.description description: req.body.description
}); });
@@ -87,7 +87,7 @@ export const registerExternalKmsRouter = async (server: FastifyZodProvider) => {
metadata: { metadata: {
kmsId: externalKms.id, kmsId: externalKms.id,
provider: req.body.provider.type, provider: req.body.provider.type,
slug: req.body.slug, name: req.body.name,
description: req.body.description description: req.body.description
} }
} }
@@ -108,7 +108,7 @@ export const registerExternalKmsRouter = async (server: FastifyZodProvider) => {
id: z.string().trim().min(1) id: z.string().trim().min(1)
}), }),
body: z.object({ body: z.object({
slug: z.string().min(1).trim().toLowerCase().optional(), name: z.string().min(1).trim().toLowerCase().optional(),
description: z.string().trim().optional(), description: z.string().trim().optional(),
provider: ExternalKmsInputUpdateSchema provider: ExternalKmsInputUpdateSchema
}), }),
@@ -125,7 +125,7 @@ export const registerExternalKmsRouter = async (server: FastifyZodProvider) => {
actorId: req.permission.id, actorId: req.permission.id,
actorAuthMethod: req.permission.authMethod, actorAuthMethod: req.permission.authMethod,
actorOrgId: req.permission.orgId, actorOrgId: req.permission.orgId,
slug: req.body.slug, name: req.body.name,
provider: req.body.provider, provider: req.body.provider,
description: req.body.description, description: req.body.description,
id: req.params.id id: req.params.id
@@ -139,7 +139,7 @@ export const registerExternalKmsRouter = async (server: FastifyZodProvider) => {
metadata: { metadata: {
kmsId: externalKms.id, kmsId: externalKms.id,
provider: req.body.provider.type, provider: req.body.provider.type,
slug: req.body.slug, name: req.body.name,
description: req.body.description description: req.body.description
} }
} }
@@ -182,7 +182,7 @@ export const registerExternalKmsRouter = async (server: FastifyZodProvider) => {
type: EventType.DELETE_KMS, type: EventType.DELETE_KMS,
metadata: { metadata: {
kmsId: externalKms.id, kmsId: externalKms.id,
slug: externalKms.slug name: externalKms.name
} }
} }
}); });
@@ -224,7 +224,7 @@ export const registerExternalKmsRouter = async (server: FastifyZodProvider) => {
type: EventType.GET_KMS, type: EventType.GET_KMS,
metadata: { metadata: {
kmsId: externalKms.id, kmsId: externalKms.id,
slug: externalKms.slug name: externalKms.name
} }
} }
}); });
@@ -260,13 +260,13 @@ export const registerExternalKmsRouter = async (server: FastifyZodProvider) => {
server.route({ server.route({
method: "GET", method: "GET",
url: "/slug/:slug", url: "/name/:name",
config: { config: {
rateLimit: readLimit rateLimit: readLimit
}, },
schema: { schema: {
params: z.object({ params: z.object({
slug: z.string().trim().min(1) name: z.string().trim().min(1)
}), }),
response: { response: {
200: z.object({ 200: z.object({
@@ -276,12 +276,12 @@ export const registerExternalKmsRouter = async (server: FastifyZodProvider) => {
}, },
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
handler: async (req) => { handler: async (req) => {
const externalKms = await server.services.externalKms.findBySlug({ const externalKms = await server.services.externalKms.findByName({
actor: req.permission.type, actor: req.permission.type,
actorId: req.permission.id, actorId: req.permission.id,
actorAuthMethod: req.permission.authMethod, actorAuthMethod: req.permission.authMethod,
actorOrgId: req.permission.orgId, actorOrgId: req.permission.orgId,
slug: req.params.slug name: req.params.name
}); });
return { externalKms }; return { externalKms };
} }
+4 -4
View File
@@ -203,7 +203,7 @@ export const registerProjectRouter = async (server: FastifyZodProvider) => {
200: z.object({ 200: z.object({
secretManagerKmsKey: z.object({ secretManagerKmsKey: z.object({
id: z.string(), id: z.string(),
slug: z.string(), name: z.string(),
isExternal: z.boolean() isExternal: z.boolean()
}) })
}) })
@@ -243,7 +243,7 @@ export const registerProjectRouter = async (server: FastifyZodProvider) => {
200: z.object({ 200: z.object({
secretManagerKmsKey: z.object({ secretManagerKmsKey: z.object({
id: z.string(), id: z.string(),
slug: z.string(), name: z.string(),
isExternal: z.boolean() isExternal: z.boolean()
}) })
}) })
@@ -268,7 +268,7 @@ export const registerProjectRouter = async (server: FastifyZodProvider) => {
metadata: { metadata: {
secretManagerKmsKey: { secretManagerKmsKey: {
id: secretManagerKmsKey.id, id: secretManagerKmsKey.id,
slug: secretManagerKmsKey.slug name: secretManagerKmsKey.name
} }
} }
} }
@@ -336,7 +336,7 @@ export const registerProjectRouter = async (server: FastifyZodProvider) => {
200: z.object({ 200: z.object({
secretManagerKmsKey: z.object({ secretManagerKmsKey: z.object({
id: z.string(), id: z.string(),
slug: z.string(), name: z.string(),
isExternal: z.boolean() isExternal: z.boolean()
}) })
}) })
@@ -1,3 +1,4 @@
import { SymmetricEncryption } from "@app/lib/crypto/cipher";
import { TProjectPermission } from "@app/lib/types"; import { TProjectPermission } from "@app/lib/types";
import { ActorType } from "@app/services/auth/auth-type"; import { ActorType } from "@app/services/auth/auth-type";
import { CaStatus } from "@app/services/certificate-authority/certificate-authority-types"; import { CaStatus } from "@app/services/certificate-authority/certificate-authority-types";
@@ -182,7 +183,13 @@ export enum EventType {
DELETE_SLACK_INTEGRATION = "delete-slack-integration", DELETE_SLACK_INTEGRATION = "delete-slack-integration",
GET_PROJECT_SLACK_CONFIG = "get-project-slack-config", GET_PROJECT_SLACK_CONFIG = "get-project-slack-config",
UPDATE_PROJECT_SLACK_CONFIG = "update-project-slack-config", UPDATE_PROJECT_SLACK_CONFIG = "update-project-slack-config",
INTEGRATION_SYNCED = "integration-synced" INTEGRATION_SYNCED = "integration-synced",
CREATE_CMEK = "create-cmek",
UPDATE_CMEK = "update-cmek",
DELETE_CMEK = "delete-cmek",
GET_CMEKS = "get-cmeks",
CMEK_ENCRYPT = "cmek-encrypt",
CMEK_DECRYPT = "cmek-decrypt"
} }
interface UserActorMetadata { interface UserActorMetadata {
@@ -1350,7 +1357,7 @@ interface CreateKmsEvent {
metadata: { metadata: {
kmsId: string; kmsId: string;
provider: string; provider: string;
slug: string; name: string;
description?: string; description?: string;
}; };
} }
@@ -1359,7 +1366,7 @@ interface DeleteKmsEvent {
type: EventType.DELETE_KMS; type: EventType.DELETE_KMS;
metadata: { metadata: {
kmsId: string; kmsId: string;
slug: string; name: string;
}; };
} }
@@ -1368,7 +1375,7 @@ interface UpdateKmsEvent {
metadata: { metadata: {
kmsId: string; kmsId: string;
provider: string; provider: string;
slug?: string; name?: string;
description?: string; description?: string;
}; };
} }
@@ -1377,7 +1384,7 @@ interface GetKmsEvent {
type: EventType.GET_KMS; type: EventType.GET_KMS;
metadata: { metadata: {
kmsId: string; kmsId: string;
slug: string; name: string;
}; };
} }
@@ -1386,7 +1393,7 @@ interface UpdateProjectKmsEvent {
metadata: { metadata: {
secretManagerKmsKey: { secretManagerKmsKey: {
id: string; id: string;
slug: string; name: string;
}; };
}; };
} }
@@ -1541,6 +1548,53 @@ interface IntegrationSyncedEvent {
}; };
} }
interface CreateCmekEvent {
type: EventType.CREATE_CMEK;
metadata: {
keyId: string;
name: string;
description?: string;
encryptionAlgorithm: SymmetricEncryption;
};
}
interface DeleteCmekEvent {
type: EventType.DELETE_CMEK;
metadata: {
keyId: string;
};
}
interface UpdateCmekEvent {
type: EventType.UPDATE_CMEK;
metadata: {
keyId: string;
name?: string;
description?: string;
};
}
interface GetCmeksEvent {
type: EventType.GET_CMEKS;
metadata: {
keyIds: string[];
};
}
interface CmekEncryptEvent {
type: EventType.CMEK_ENCRYPT;
metadata: {
keyId: string;
};
}
interface CmekDecryptEvent {
type: EventType.CMEK_DECRYPT;
metadata: {
keyId: string;
};
}
export type Event = export type Event =
| GetSecretsEvent | GetSecretsEvent
| GetSecretEvent | GetSecretEvent
@@ -1680,4 +1734,10 @@ export type Event =
| GetSlackIntegration | GetSlackIntegration
| UpdateProjectSlackConfig | UpdateProjectSlackConfig
| GetProjectSlackConfig | GetProjectSlackConfig
| IntegrationSyncedEvent; | IntegrationSyncedEvent
| CreateCmekEvent
| UpdateCmekEvent
| DeleteCmekEvent
| GetCmeksEvent
| CmekEncryptEvent
| CmekDecryptEvent;
@@ -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;
}; };
+3
View File
@@ -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) =>
+34
View File
@@ -1347,3 +1347,37 @@ export const PROJECT_ROLE = {
projectSlug: "The slug of the project to list the roles of." projectSlug: "The slug of the project to list the roles of."
} }
}; };
export const KMS = {
CREATE_KEY: {
projectId: "The ID of the project to create the key in.",
name: "The name of the key to be created. Must be slug-friendly.",
description: "An optional description of the key.",
encryptionAlgorithm: "The algorithm to use when performing cryptographic operations with the key."
},
UPDATE_KEY: {
keyId: "The ID of the key to be updated.",
name: "The updated name of this key. Must be slug-friendly.",
description: "The updated description of this key.",
isDisabled: "The flag to enable or disable this key."
},
DELETE_KEY: {
keyId: "The ID of the key to be deleted."
},
LIST_KEYS: {
projectId: "The ID of the project to list keys from.",
offset: "The offset to start from. If you enter 10, it will start from the 10th key.",
limit: "The number of keys to return.",
orderBy: "The column to order keys by.",
orderDirection: "The direction to order keys in.",
search: "The text string to filter key names by."
},
ENCRYPT: {
keyId: "The ID of the key to encrypt the data with.",
plaintext: "The plaintext to be encrypted (base64 encoded)."
},
DECRYPT: {
keyId: "The ID of the key to decrypt the data with.",
ciphertext: "The ciphertext to be decrypted (base64 encoded)."
}
};
+28
View File
@@ -0,0 +1,28 @@
// Credit: https://github.com/miguelmota/is-base64
export const isBase64 = (
v: string,
opts = { allowEmpty: false, mimeRequired: false, allowMime: true, paddingRequired: false }
) => {
if (opts.allowEmpty === false && v === "") {
return false;
}
let regex = "(?:[A-Za-z0-9+\\/]{4})*(?:[A-Za-z0-9+\\/]{2}==|[A-Za-z0-9+/]{3}=)?";
const mimeRegex = "(data:\\w+\\/[a-zA-Z\\+\\-\\.]+;base64,)";
if (opts.mimeRequired === true) {
regex = mimeRegex + regex;
} else if (opts.allowMime === true) {
regex = `${mimeRegex}?${regex}`;
}
if (opts.paddingRequired === false) {
regex = "(?:[A-Za-z0-9+\\/]{4})*(?:[A-Za-z0-9+\\/]{2}(==)?|[A-Za-z0-9+\\/]{3}=?)?";
}
return new RegExp(`^${regex}$`, "gi").test(v);
};
export const getBase64SizeInBytes = (base64String: string) => {
return Buffer.from(base64String, "base64").length;
};
+13 -2
View File
@@ -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.
+11 -1
View File
@@ -96,6 +96,7 @@ import { certificateAuthorityServiceFactory } from "@app/services/certificate-au
import { certificateTemplateDALFactory } from "@app/services/certificate-template/certificate-template-dal"; import { certificateTemplateDALFactory } from "@app/services/certificate-template/certificate-template-dal";
import { certificateTemplateEstConfigDALFactory } from "@app/services/certificate-template/certificate-template-est-config-dal"; import { certificateTemplateEstConfigDALFactory } from "@app/services/certificate-template/certificate-template-est-config-dal";
import { certificateTemplateServiceFactory } from "@app/services/certificate-template/certificate-template-service"; import { certificateTemplateServiceFactory } from "@app/services/certificate-template/certificate-template-service";
import { cmekServiceFactory } from "@app/services/cmek/cmek-service";
import { externalMigrationServiceFactory } from "@app/services/external-migration/external-migration-service"; import { externalMigrationServiceFactory } from "@app/services/external-migration/external-migration-service";
import { groupProjectDALFactory } from "@app/services/group-project/group-project-dal"; import { groupProjectDALFactory } from "@app/services/group-project/group-project-dal";
import { groupProjectMembershipRoleDALFactory } from "@app/services/group-project/group-project-membership-role-dal"; import { groupProjectMembershipRoleDALFactory } from "@app/services/group-project/group-project-membership-role-dal";
@@ -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,
+331
View File
@@ -0,0 +1,331 @@
import slugify from "@sindresorhus/slugify";
import { z } from "zod";
import { InternalKmsSchema, KmsKeysSchema } from "@app/db/schemas";
import { EventType } from "@app/ee/services/audit-log/audit-log-types";
import { KMS } from "@app/lib/api-docs";
import { getBase64SizeInBytes, isBase64 } from "@app/lib/base64";
import { SymmetricEncryption } from "@app/lib/crypto/cipher";
import { OrderByDirection } from "@app/lib/types";
import { readLimit, writeLimit } from "@app/server/config/rateLimiter";
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
import { AuthMode } from "@app/services/auth/auth-type";
import { CmekOrderBy } from "@app/services/cmek/cmek-types";
const keyNameSchema = z
.string()
.trim()
.min(1)
.max(32)
.toLowerCase()
.refine((v) => slugify(v) === v, {
message: "Name must be slug friendly"
});
const keyDescriptionSchema = z.string().trim().max(500).optional();
const base64Schema = z.string().superRefine((val, ctx) => {
if (!isBase64(val)) {
ctx.addIssue({
code: z.ZodIssueCode.custom,
message: "plaintext must be base64 encoded"
});
}
if (getBase64SizeInBytes(val) > 4096) {
ctx.addIssue({
code: z.ZodIssueCode.custom,
message: "data cannot exceed 4096 bytes"
});
}
});
export const registerCmekRouter = async (server: FastifyZodProvider) => {
// create encryption key
server.route({
method: "POST",
url: "/keys",
config: {
rateLimit: writeLimit
},
schema: {
description: "Create KMS key",
body: z.object({
projectId: z.string().describe(KMS.CREATE_KEY.projectId),
name: keyNameSchema.describe(KMS.CREATE_KEY.name),
description: keyDescriptionSchema.describe(KMS.CREATE_KEY.description),
encryptionAlgorithm: z
.nativeEnum(SymmetricEncryption)
.optional()
.default(SymmetricEncryption.AES_GCM_256)
.describe(KMS.CREATE_KEY.encryptionAlgorithm) // eventually will support others
}),
response: {
200: z.object({
key: KmsKeysSchema
})
}
},
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
handler: async (req) => {
const {
body: { projectId, name, description, encryptionAlgorithm },
permission
} = req;
const cmek = await server.services.cmek.createCmek(
{ orgId: permission.orgId, projectId, name, description, encryptionAlgorithm },
permission
);
await server.services.auditLog.createAuditLog({
...req.auditLogInfo,
projectId,
event: {
type: EventType.CREATE_CMEK,
metadata: {
keyId: cmek.id,
name,
description,
encryptionAlgorithm
}
}
});
return { key: cmek };
}
});
// update KMS key
server.route({
method: "PATCH",
url: "/keys/:keyId",
config: {
rateLimit: writeLimit
},
schema: {
description: "Update KMS key",
params: z.object({
keyId: z.string().uuid().describe(KMS.UPDATE_KEY.keyId)
}),
body: z.object({
name: keyNameSchema.optional().describe(KMS.UPDATE_KEY.name),
isDisabled: z.boolean().optional().describe(KMS.UPDATE_KEY.isDisabled),
description: keyDescriptionSchema.describe(KMS.UPDATE_KEY.description)
}),
response: {
200: z.object({
key: KmsKeysSchema
})
}
},
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
handler: async (req) => {
const {
params: { keyId },
body,
permission
} = req;
const cmek = await server.services.cmek.updateCmekById({ keyId, ...body }, permission);
await server.services.auditLog.createAuditLog({
...req.auditLogInfo,
orgId: permission.orgId,
event: {
type: EventType.UPDATE_CMEK,
metadata: {
keyId,
...body
}
}
});
return { key: cmek };
}
});
// delete KMS key
server.route({
method: "DELETE",
url: "/keys/:keyId",
config: {
rateLimit: writeLimit
},
schema: {
description: "Delete KMS key",
params: z.object({
keyId: z.string().uuid().describe(KMS.DELETE_KEY.keyId)
}),
response: {
200: z.object({
key: KmsKeysSchema
})
}
},
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
handler: async (req) => {
const {
params: { keyId },
permission
} = req;
const cmek = await server.services.cmek.deleteCmekById(keyId, permission);
await server.services.auditLog.createAuditLog({
...req.auditLogInfo,
orgId: permission.orgId,
event: {
type: EventType.DELETE_CMEK,
metadata: {
keyId
}
}
});
return { key: cmek };
}
});
// list KMS keys
server.route({
method: "GET",
url: "/keys",
config: {
rateLimit: readLimit
},
schema: {
description: "List KMS keys",
querystring: z.object({
projectId: z.string().describe(KMS.LIST_KEYS.projectId),
offset: z.coerce.number().min(0).optional().default(0).describe(KMS.LIST_KEYS.offset),
limit: z.coerce.number().min(1).max(100).optional().default(100).describe(KMS.LIST_KEYS.limit),
orderBy: z.nativeEnum(CmekOrderBy).optional().default(CmekOrderBy.Name).describe(KMS.LIST_KEYS.orderBy),
orderDirection: z
.nativeEnum(OrderByDirection)
.optional()
.default(OrderByDirection.ASC)
.describe(KMS.LIST_KEYS.orderDirection),
search: z.string().trim().optional().describe(KMS.LIST_KEYS.search)
}),
response: {
200: z.object({
keys: KmsKeysSchema.merge(InternalKmsSchema.pick({ version: true, encryptionAlgorithm: true })).array(),
totalCount: z.number()
})
}
},
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
handler: async (req) => {
const {
query: { projectId, ...dto },
permission
} = req;
const { cmeks, totalCount } = await server.services.cmek.listCmeksByProjectId({ projectId, ...dto }, permission);
await server.services.auditLog.createAuditLog({
...req.auditLogInfo,
projectId,
event: {
type: EventType.GET_CMEKS,
metadata: {
keyIds: cmeks.map((key) => key.id)
}
}
});
return { keys: cmeks, totalCount };
}
});
// encrypt data
server.route({
method: "POST",
url: "/keys/:keyId/encrypt",
config: {
rateLimit: writeLimit
},
schema: {
description: "Encrypt data with KMS key",
params: z.object({
keyId: z.string().uuid().describe(KMS.ENCRYPT.keyId)
}),
body: z.object({
plaintext: base64Schema.describe(KMS.ENCRYPT.plaintext)
}),
response: {
200: z.object({
ciphertext: z.string()
})
}
},
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
handler: async (req) => {
const {
params: { keyId },
body: { plaintext },
permission
} = req;
const ciphertext = await server.services.cmek.cmekEncrypt({ keyId, plaintext }, permission);
await server.services.auditLog.createAuditLog({
...req.auditLogInfo,
orgId: permission.orgId,
event: {
type: EventType.CMEK_ENCRYPT,
metadata: {
keyId
}
}
});
return { ciphertext };
}
});
server.route({
method: "POST",
url: "/keys/:keyId/decrypt",
config: {
rateLimit: writeLimit
},
schema: {
description: "Decrypt data with KMS key",
params: z.object({
keyId: z.string().uuid().describe(KMS.ENCRYPT.keyId)
}),
body: z.object({
ciphertext: base64Schema.describe(KMS.ENCRYPT.plaintext)
}),
response: {
200: z.object({
plaintext: z.string()
})
}
},
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
handler: async (req) => {
const {
params: { keyId },
body: { ciphertext },
permission
} = req;
const plaintext = await server.services.cmek.cmekDecrypt({ keyId, ciphertext }, permission);
await server.services.auditLog.createAuditLog({
...req.auditLogInfo,
orgId: permission.orgId,
event: {
type: EventType.CMEK_DECRYPT,
metadata: {
keyId
}
}
});
return { plaintext };
}
});
};
+2 -1
View File
@@ -1,3 +1,4 @@
import { registerCmekRouter } from "@app/server/routes/v1/cmek-router";
import { registerDashboardRouter } from "@app/server/routes/v1/dashboard-router"; import { registerDashboardRouter } from "@app/server/routes/v1/dashboard-router";
import { registerAdminRouter } from "./admin-router"; import { registerAdminRouter } from "./admin-router";
@@ -103,6 +104,6 @@ export const registerV1Routes = async (server: FastifyZodProvider) => {
await server.register(registerIdentityRouter, { prefix: "/identities" }); await server.register(registerIdentityRouter, { prefix: "/identities" });
await server.register(registerSecretSharingRouter, { prefix: "/secret-sharing" }); await server.register(registerSecretSharingRouter, { prefix: "/secret-sharing" });
await server.register(registerUserEngagementRouter, { prefix: "/user-engagement" }); await server.register(registerUserEngagementRouter, { prefix: "/user-engagement" });
await server.register(registerDashboardRouter, { prefix: "/dashboard" }); await server.register(registerDashboardRouter, { prefix: "/dashboard" });
await server.register(registerCmekRouter, { prefix: "/kms" });
}; };
@@ -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
+169
View File
@@ -0,0 +1,169 @@
import { ForbiddenError } from "@casl/ability";
import { FastifyRequest } from "fastify";
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service";
import { ProjectPermissionCmekActions, ProjectPermissionSub } from "@app/ee/services/permission/project-permission";
import { BadRequestError, NotFoundError } from "@app/lib/errors";
import {
TCmekDecryptDTO,
TCmekEncryptDTO,
TCreateCmekDTO,
TListCmeksByProjectIdDTO,
TUpdabteCmekByIdDTO
} from "@app/services/cmek/cmek-types";
import { TKmsKeyDALFactory } from "@app/services/kms/kms-key-dal";
import { TKmsServiceFactory } from "@app/services/kms/kms-service";
type TCmekServiceFactoryDep = {
kmsService: TKmsServiceFactory;
kmsDAL: TKmsKeyDALFactory;
permissionService: TPermissionServiceFactory;
};
export type TCmekServiceFactory = ReturnType<typeof cmekServiceFactory>;
export const cmekServiceFactory = ({ kmsService, kmsDAL, permissionService }: TCmekServiceFactoryDep) => {
const createCmek = async ({ projectId, ...dto }: TCreateCmekDTO, actor: FastifyRequest["permission"]) => {
const { permission } = await permissionService.getProjectPermission(
actor.type,
actor.id,
projectId,
actor.authMethod,
actor.orgId
);
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionCmekActions.Create, ProjectPermissionSub.Cmek);
const cmek = await kmsService.generateKmsKey({
...dto,
projectId,
isReserved: false
});
return cmek;
};
const updateCmekById = async ({ keyId, ...data }: TUpdabteCmekByIdDTO, actor: FastifyRequest["permission"]) => {
const key = await kmsDAL.findById(keyId);
if (!key) throw new NotFoundError({ message: "Key not found" });
if (!key.projectId || key.isReserved) throw new BadRequestError({ message: "Key is not customer managed" });
const { permission } = await permissionService.getProjectPermission(
actor.type,
actor.id,
key.projectId,
actor.authMethod,
actor.orgId
);
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionCmekActions.Edit, ProjectPermissionSub.Cmek);
const cmek = await kmsDAL.updateById(keyId, data);
return cmek;
};
const deleteCmekById = async (keyId: string, actor: FastifyRequest["permission"]) => {
const key = await kmsDAL.findById(keyId);
if (!key) throw new NotFoundError({ message: "Key not found" });
if (!key.projectId || key.isReserved) throw new BadRequestError({ message: "Key is not customer managed" });
const { permission } = await permissionService.getProjectPermission(
actor.type,
actor.id,
key.projectId,
actor.authMethod,
actor.orgId
);
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionCmekActions.Delete, ProjectPermissionSub.Cmek);
const cmek = kmsDAL.deleteById(keyId);
return cmek;
};
const listCmeksByProjectId = async (
{ projectId, ...filters }: TListCmeksByProjectIdDTO,
actor: FastifyRequest["permission"]
) => {
const { permission } = await permissionService.getProjectPermission(
actor.type,
actor.id,
projectId,
actor.authMethod,
actor.orgId
);
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionCmekActions.Read, ProjectPermissionSub.Cmek);
const { keys: cmeks, totalCount } = await kmsDAL.findKmsKeysByProjectId({ projectId, ...filters });
return { cmeks, totalCount };
};
const cmekEncrypt = async ({ keyId, plaintext }: TCmekEncryptDTO, actor: FastifyRequest["permission"]) => {
const key = await kmsDAL.findById(keyId);
if (!key) throw new NotFoundError({ message: "Key not found" });
if (!key.projectId || key.isReserved) throw new BadRequestError({ message: "Key is not customer managed" });
if (key.isDisabled) throw new BadRequestError({ message: "Key is disabled" });
const { permission } = await permissionService.getProjectPermission(
actor.type,
actor.id,
key.projectId,
actor.authMethod,
actor.orgId
);
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionCmekActions.Encrypt, ProjectPermissionSub.Cmek);
const encrypt = await kmsService.encryptWithKmsKey({ kmsId: keyId });
const { cipherTextBlob } = await encrypt({ plainText: Buffer.from(plaintext, "base64") });
return cipherTextBlob.toString("base64");
};
const cmekDecrypt = async ({ keyId, ciphertext }: TCmekDecryptDTO, actor: FastifyRequest["permission"]) => {
const key = await kmsDAL.findById(keyId);
if (!key) throw new NotFoundError({ message: "Key not found" });
if (!key.projectId || key.isReserved) throw new BadRequestError({ message: "Key is not customer managed" });
if (key.isDisabled) throw new BadRequestError({ message: "Key is disabled" });
const { permission } = await permissionService.getProjectPermission(
actor.type,
actor.id,
key.projectId,
actor.authMethod,
actor.orgId
);
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionCmekActions.Decrypt, ProjectPermissionSub.Cmek);
const decrypt = await kmsService.decryptWithKmsKey({ kmsId: keyId });
const plaintextBlob = await decrypt({ cipherTextBlob: Buffer.from(ciphertext, "base64") });
return plaintextBlob.toString("base64");
};
return {
createCmek,
updateCmekById,
deleteCmekById,
listCmeksByProjectId,
cmekEncrypt,
cmekDecrypt
};
};
+40
View File
@@ -0,0 +1,40 @@
import { SymmetricEncryption } from "@app/lib/crypto/cipher";
import { OrderByDirection } from "@app/lib/types";
export type TCreateCmekDTO = {
orgId: string;
projectId: string;
name: string;
description?: string;
encryptionAlgorithm: SymmetricEncryption;
};
export type TUpdabteCmekByIdDTO = {
keyId: string;
name?: string;
isDisabled?: boolean;
description?: string;
};
export type TListCmeksByProjectIdDTO = {
projectId: string;
offset?: number;
limit?: number;
orderBy?: CmekOrderBy;
orderDirection?: OrderByDirection;
search?: string;
};
export type TCmekEncryptDTO = {
keyId: string;
plaintext: string;
};
export type TCmekDecryptDTO = {
keyId: string;
ciphertext: string;
};
export enum CmekOrderBy {
Name = "name"
}
@@ -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;
+11
View File
@@ -0,0 +1,11 @@
import { SymmetricEncryption } from "@app/lib/crypto/cipher";
export const getByteLengthForAlgorithm = (encryptionAlgorithm: SymmetricEncryption) => {
switch (encryptionAlgorithm) {
case SymmetricEncryption.AES_GCM_128:
return 16;
case SymmetricEncryption.AES_GCM_256:
default:
return 32;
}
};
+49 -2
View File
@@ -1,9 +1,11 @@
import { Knex } from "knex"; import { Knex } from "knex";
import { TDbClient } from "@app/db"; import { TDbClient } from "@app/db";
import { KmsKeysSchema, TableName } from "@app/db/schemas"; import { KmsKeysSchema, TableName, TInternalKms, TKmsKeys } from "@app/db/schemas";
import { DatabaseError } from "@app/lib/errors"; import { DatabaseError } from "@app/lib/errors";
import { ormify, selectAllTableCols } from "@app/lib/knex"; import { ormify, selectAllTableCols } from "@app/lib/knex";
import { OrderByDirection } from "@app/lib/types";
import { CmekOrderBy, TListCmeksByProjectIdDTO } from "@app/services/cmek/cmek-types";
export type TKmsKeyDALFactory = ReturnType<typeof kmskeyDALFactory>; export type TKmsKeyDALFactory = ReturnType<typeof kmskeyDALFactory>;
@@ -71,5 +73,50 @@ export const kmskeyDALFactory = (db: TDbClient) => {
} }
}; };
return { ...kmsOrm, findByIdWithAssociatedKms }; const findKmsKeysByProjectId = async (
{
projectId,
offset = 0,
limit,
orderBy = CmekOrderBy.Name,
orderDirection = OrderByDirection.ASC,
search
}: TListCmeksByProjectIdDTO,
tx?: Knex
) => {
try {
const query = (tx || db.replicaNode())(TableName.KmsKey)
.where("projectId", projectId)
.where((qb) => {
if (search) {
void qb.whereILike("name", `%${search}%`);
}
})
.join(TableName.InternalKms, `${TableName.InternalKms}.kmsKeyId`, `${TableName.KmsKey}.id`)
.select<
(TKmsKeys &
Pick<TInternalKms, "version" | "encryptionAlgorithm"> & {
total_count: number;
})[]
>(
selectAllTableCols(TableName.KmsKey),
db.raw(`count(*) OVER() as total_count`),
db.ref("encryptionAlgorithm").withSchema(TableName.InternalKms),
db.ref("version").withSchema(TableName.InternalKms)
)
.orderBy(orderBy, orderDirection);
if (limit) {
void query.limit(limit).offset(offset);
}
const data = await query;
return { keys: data, totalCount: Number(data?.[0]?.total_count ?? 0) };
} catch (error) {
throw new DatabaseError({ error, name: "Find kms keys by project id" });
}
};
return { ...kmsOrm, findByIdWithAssociatedKms, findKmsKeysByProjectId };
}; };
+37 -23
View File
@@ -17,6 +17,7 @@ import { generateHash } from "@app/lib/crypto/encryption";
import { BadRequestError, ForbiddenRequestError, NotFoundError } from "@app/lib/errors"; import { BadRequestError, ForbiddenRequestError, NotFoundError } from "@app/lib/errors";
import { logger } from "@app/lib/logger"; import { logger } from "@app/lib/logger";
import { alphaNumericNanoId } from "@app/lib/nanoid"; import { alphaNumericNanoId } from "@app/lib/nanoid";
import { getByteLengthForAlgorithm } from "@app/services/kms/kms-fns";
import { TOrgDALFactory } from "../org/org-dal"; import { TOrgDALFactory } from "../org/org-dal";
import { TProjectDALFactory } from "../project/project-dal"; import { TProjectDALFactory } from "../project/project-dal";
@@ -71,17 +72,29 @@ export const kmsServiceFactory = ({
* This function is responsibile for generating the infisical internal KMS for various entities * This function is responsibile for generating the infisical internal KMS for various entities
* Like for secret manager, cert manager or for organization * Like for secret manager, cert manager or for organization
*/ */
const generateKmsKey = async ({ orgId, isReserved = true, tx, slug }: TGenerateKMSDTO) => { const generateKmsKey = async ({
orgId,
isReserved = true,
tx,
name,
projectId,
encryptionAlgorithm = SymmetricEncryption.AES_GCM_256,
description
}: TGenerateKMSDTO) => {
const cipher = symmetricCipherService(SymmetricEncryption.AES_GCM_256); const cipher = symmetricCipherService(SymmetricEncryption.AES_GCM_256);
const kmsKeyMaterial = randomSecureBytes(32);
const kmsKeyMaterial = randomSecureBytes(getByteLengthForAlgorithm(encryptionAlgorithm));
const encryptedKeyMaterial = cipher.encrypt(kmsKeyMaterial, ROOT_ENCRYPTION_KEY); const encryptedKeyMaterial = cipher.encrypt(kmsKeyMaterial, ROOT_ENCRYPTION_KEY);
const sanitizedSlug = slug ? slugify(slug) : slugify(alphaNumericNanoId(8).toLowerCase()); const sanitizedName = name ? slugify(name) : slugify(alphaNumericNanoId(8).toLowerCase());
const dbQuery = async (db: Knex) => { const dbQuery = async (db: Knex) => {
const kmsDoc = await kmsDAL.create( const kmsDoc = await kmsDAL.create(
{ {
slug: sanitizedSlug, name: sanitizedName,
orgId, orgId,
isReserved isReserved,
projectId,
description
}, },
db db
); );
@@ -90,7 +103,7 @@ export const kmsServiceFactory = ({
{ {
version: 1, version: 1,
encryptedKey: encryptedKeyMaterial, encryptedKey: encryptedKeyMaterial,
encryptionAlgorithm: SymmetricEncryption.AES_GCM_256, encryptionAlgorithm,
kmsKeyId: kmsDoc.id kmsKeyId: kmsDoc.id
}, },
db db
@@ -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
+6 -1
View File
@@ -1,5 +1,7 @@
import { Knex } from "knex"; import { Knex } from "knex";
import { SymmetricEncryption } from "@app/lib/crypto/cipher";
export enum KmsDataKey { export enum KmsDataKey {
Organization, Organization,
SecretManager SecretManager
@@ -22,8 +24,11 @@ export type TEncryptWithKmsDataKeyDTO =
export type TGenerateKMSDTO = { export type TGenerateKMSDTO = {
orgId: string; orgId: string;
projectId?: string;
encryptionAlgorithm?: SymmetricEncryption;
isReserved?: boolean; isReserved?: boolean;
slug?: string; name?: string;
description?: string;
tx?: Knex; tx?: Knex;
}; };
@@ -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;
}; };
+3 -5
View File
@@ -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">
![Add Dynamic Secret Button](../../../images/platform/dynamic-secrets/add-dynamic-secret-button.png)
</Step>
<Step title="Select 'LDAP'">
![Dynamic Secret Modal](../../../images/platform/dynamic-secrets/dynamic-secret-ldap-select.png)
</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.
![Dynamic Secret](/images/platform/dynamic-secrets/dynamic-secret-generate-redis.png)
![Dynamic Secret](/images/platform/dynamic-secrets/dynamic-secret-lease-empty-redis.png)
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.
![Provision Lease](/images/platform/dynamic-secrets/provision-lease.png)
<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.
![Provision Lease](/images/platform/dynamic-secrets/dynamic-secret-ldap-lease.png)
</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"
--- ---
+208
View File
@@ -0,0 +1,208 @@
---
title: "Key Management Service (KMS)"
sidebarTitle: "Key Management (KMS)"
description: "Learn how to manage and use cryptographic keys with Infisical."
---
## Concept
Infisical can be used as a Key Management System (KMS), referred to as Infisical KMS, to centralize management of keys to be used for cryptographic operations like encryption/decryption.
<Note>
Keys managed in KMS are not extractable from the platform. Additionally, data
is never stored when performing cryptographic operations.
</Note>
## Workflow
The typical workflow for using Infisical KMS consists of the following steps:
1. Creating a KMS key. As part of this step, you specify a name for the key and the encryption algorithm meant to be used for it (e.g. `AES-GCM-128`, `AES-GCM-256`).
2. Encryption: To encrypt data, you would make a request to the Infisical KMS API endpoint, specifying the base64-encoded plaintext and the intended key to use for encryption; the API would return the base64-encoded ciphertext.
3. Decryption: To decrypt data, you would make a request to the Infisical KMS API endpoint, specifying the base64-encoded ciphertext and the intended key to use for decryption; the API would return the base64-encoded plaintext.
<Note>
Note that this workflow can be executed via the Infisical UI or manually such
as via API.
</Note>
## Guide to Encrypting Data
In the following steps, we explore how to generate a key and use it to encrypt data.
<Tabs>
<Tab title="Infisical UI">
<Steps>
<Step title="Creating a KMS key">
Navigate to Project > Key Management and tap on the **Add Key** button.
![kms add key button](/images/platform/kms/infisical-kms/kms-add-key.png)
Specify your key details. Here's some guidance on each field:
- Name: A slug-friendly name for the key.
- Type: The encryption algorithm associated with the key (e.g. `AES-GCM-256`).
- Description: An optional description of what the intended usage is for the key.
![kms add key modal](/images/platform/kms/infisical-kms/kms-add-key-modal.png)
</Step>
<Step title="Encrypting data with the KMS key">
Once your key is generated, open the options menu for the newly created key and select encrypt data.
![kms key options](/images/platform/kms/infisical-kms/kms-key-options.png)
Populate the text area with your data and tap on the Encrypt button.
![kms encrypt data](/images/platform/kms/infisical-kms/kms-encrypt-data.png)
<Note>
If your data is already Base64 encoded make sure to toggle the respective switch on to avoid
redundant encoding.
</Note>
Copy and store the encrypted data.
![kms encrypted data](/images/platform/kms/infisical-kms/kms-encrypted-data.png)
</Step>
</Steps>
</Tab>
<Tab title="API">
<Steps>
<Step title="Creating a KMS key">
To create a cryptographic key, make an API request to the [Create KMS
Key](/api-reference/endpoints/kms/keys/create) API endpoint.
### Sample request
```bash Request
curl --request POST \
--url https://app.infisical.com/api/v1/kms/keys \
--header 'Content-Type: application/json' \
--data '{
"projectId": "<project-id>",
"name": "my-secret-key",
"description": "...",
"encryptionAlgorithm": "aes-256-gcm"
}'
```
### Sample response
```bash Response
{
"key": {
"id": "<key-id>",
"description": "...",
"isDisabled": false,
"isReserved": false,
"orgId": "<org-id>",
"name": "my-secret-key",
"createdAt": "2023-11-07T05:31:56Z",
"updatedAt": "2023-11-07T05:31:56Z",
"projectId": "<project-id>"
}
}
```
</Step>
<Step title="Encrypting data with the KMS key">
To encrypt data, make an API request to the [Encrypt
Data](/api-reference/endpoints/kms/keys/encrypt) API endpoint,
specifying the key to use.
<Note>
Make sure your data is Base64 encoded
</Note>
### Sample request
```bash Request
curl --request POST \
--url https://app.infisical.com/api/v1/kms/keys/<key-id>/encrypt \
--header 'Content-Type: application/json' \
--data '{
"plaintext": "lUFHM5Ggwo6TOfpuN1S==" // base64 encoded plaintext
}'
```
### Sample response
```bash Response
{
"ciphertext": "HwFHwSFHwlMF6TOfp==" // base64 encoded ciphertext
}
```
</Step>
</Steps>
</Tab>
</Tabs>
## Guide to Decrypting Data
In the following steps, we explore how to use decrypt data using an existing key in Infisical KMS.
<Tabs>
<Tab title="Infisical UI">
<Steps>
<Step title="Accessing your key">
Navigate to Project > Key Management and open the options menu for the key used to encrypt the data
you want to decrypt.
![kms key options](/images/platform/kms/infisical-kms/kms-decrypt-options.png)
</Step>
<Step title="Decrypting your data">
Paste your encrypted data into the text area and tap on the Decrypt button. Optionally, if your data was
originally plain text, enable the decode Base64 switch.
![kms decrypt data](/images/platform/kms/infisical-kms/kms-decrypt-data.png)
Your decrypted data will be displayed and can be copied for use.
![kms decrypted data](/images/platform/kms/infisical-kms/kms-decrypted-data.png)
</Step>
</Steps>
</Tab>
<Tab title="API">
<Steps>
<Step title="Decrypting data">
To decrypt data, make an API request to the [Decrypt
Data](/api-reference/endpoints/kms/keys/decrypt) API endpoint,
specifying the key to use.
### Sample request
```bash Request
curl --request POST \
--url https://app.infisical.com/api/v1/kms/keys/<key-id>/decrypt \
--header 'Content-Type: application/json' \
--data '{
"ciphertext": "HwFHwSFHwlMF6TOfp==" // base64 encoded ciphertext
}'
```
### Sample response
```bash Response
{
"plaintext": "lUFHM5Ggwo6TOfpuN1S==" // base64 encoded plaintext
}
```
</Step>
</Steps>
</Tab>
</Tabs>
## FAQ
<AccordionGroup>
<Accordion title="Is my data stored in Infisical KMS?">
No. Infisical's KMS only provides cryptographic services and does not store
any encrypted or decrypted data.
</Accordion>
<Accordion title="Can key material be accessed outside of Infisical KMS?">
No. Infisical's KMS will never expose your keys, encrypted or decrypted, to
external sources.
</Accordion>
<Accordion title="What algorithms does Infisical KMS support?">
Currently, Infisical only supports `AES-128-GCM` and `AES-256-GCM` for
encryption operations. We anticipate supporting more algorithms and
cryptographic operations in the coming months.
</Accordion>
</AccordionGroup>
Binary file not shown.

After

Width:  |  Height:  |  Size: 104 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 490 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 535 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 702 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 624 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 942 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 585 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 558 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 586 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 734 KiB

+27 -9
View File
@@ -113,6 +113,15 @@
"documentation/platform/pki/alerting" "documentation/platform/pki/alerting"
] ]
}, },
"documentation/platform/kms",
{
"group": "KMS Configuration",
"pages": [
"documentation/platform/kms-configuration/overview",
"documentation/platform/kms-configuration/aws-kms",
"documentation/platform/kms-configuration/aws-hsm"
]
},
{ {
"group": "Identities", "group": "Identities",
"pages": [ "pages": [
@@ -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 || "";
+3 -1
View File
@@ -8,6 +8,7 @@ export type SwitchProps = Omit<SwitchPrimitive.SwitchProps, "checked" | "disable
isChecked?: boolean; isChecked?: boolean;
isRequired?: boolean; isRequired?: boolean;
isDisabled?: boolean; isDisabled?: boolean;
containerClassName?: string;
}; };
export const Switch = ({ export const Switch = ({
@@ -17,9 +18,10 @@ export const Switch = ({
isChecked, isChecked,
isDisabled, isDisabled,
isRequired, isRequired,
containerClassName,
...props ...props
}: SwitchProps): JSX.Element => ( }: SwitchProps): JSX.Element => (
<div className="flex items-center font-inter text-bunker-300"> <div className={twMerge("flex items-center font-inter text-bunker-300", containerClassName)}>
<label className="text-sm" htmlFor={id}> <label className="text-sm" htmlFor={id}>
{children} {children}
{isRequired && <span className="pl-1 text-red">*</span>} {isRequired && <span className="pl-1 text-red">*</span>}
+30 -25
View File
@@ -27,35 +27,40 @@ export const Tooltip = ({
isDisabled, isDisabled,
position = "top", position = "top",
...props ...props
}: TooltipProps) => ( }: TooltipProps) =>
<TooltipPrimitive.Root // just render children if tooltip content is empty
delayDuration={50} content ? (
open={isOpen} <TooltipPrimitive.Root
defaultOpen={defaultOpen} delayDuration={50}
onOpenChange={onOpenChange} open={isOpen}
> defaultOpen={defaultOpen}
<TooltipPrimitive.Trigger asChild={asChild}>{children}</TooltipPrimitive.Trigger> onOpenChange={onOpenChange}
<TooltipPrimitive.Content >
side={position} <TooltipPrimitive.Trigger asChild={asChild}>{children}</TooltipPrimitive.Trigger>
align="center" <TooltipPrimitive.Content
sideOffset={5} side={position}
{...props} align="center"
className={twMerge( sideOffset={5}
`z-50 max-w-[15rem] select-none rounded-md border border-mineshaft-600 bg-mineshaft-800 py-2 px-4 text-sm font-light text-bunker-200 shadow-md {...props}
className={twMerge(
`z-50 max-w-[15rem] select-none rounded-md border border-mineshaft-600 bg-mineshaft-800 py-2 px-4 text-sm font-light text-bunker-200 shadow-md
data-[state=delayed-open]:data-[side=top]:animate-slideDownAndFade data-[state=delayed-open]:data-[side=top]:animate-slideDownAndFade
data-[state=delayed-open]:data-[side=right]:animate-slideLeftAndFade data-[state=delayed-open]:data-[side=right]:animate-slideLeftAndFade
data-[state=delayed-open]:data-[side=left]:animate-slideRightAndFade data-[state=delayed-open]:data-[side=left]:animate-slideRightAndFade
data-[state=delayed-open]:data-[side=bottom]:animate-slideUpAndFade data-[state=delayed-open]:data-[side=bottom]:animate-slideUpAndFade
`, `,
isDisabled && "!hidden", isDisabled && "!hidden",
center && "text-center", center && "text-center",
className className
)} )}
> >
{content} {content}
<TooltipPrimitive.Arrow width={11} height={5} className="fill-mineshaft-600" /> <TooltipPrimitive.Arrow width={11} height={5} className="fill-mineshaft-600" />
</TooltipPrimitive.Content> </TooltipPrimitive.Content>
</TooltipPrimitive.Root> </TooltipPrimitive.Root>
); ) : (
// eslint-disable-next-line react/jsx-no-useless-fragment
<>{children}</>
);
export const TooltipProvider = TooltipPrimitive.Provider; export const TooltipProvider = TooltipPrimitive.Provider;
@@ -1,3 +1,7 @@
export { ProjectPermissionProvider, useProjectPermission } from "./ProjectPermissionContext"; export { ProjectPermissionProvider, useProjectPermission } from "./ProjectPermissionContext";
export type { ProjectPermissionSet, TProjectPermission } from "./types"; export type { ProjectPermissionSet, TProjectPermission } from "./types";
export { ProjectPermissionActions, ProjectPermissionSub } from "./types"; export {
ProjectPermissionActions,
ProjectPermissionCmekActions,
ProjectPermissionSub
} from "./types";
@@ -7,6 +7,15 @@ export enum ProjectPermissionActions {
Delete = "delete" Delete = "delete"
} }
export enum ProjectPermissionCmekActions {
Read = "read",
Create = "create",
Edit = "edit",
Delete = "delete",
Encrypt = "encrypt",
Decrypt = "decrypt"
}
export enum PermissionConditionOperators { export enum PermissionConditionOperators {
$IN = "$in", $IN = "$in",
$ALL = "$all", $ALL = "$all",
@@ -55,7 +64,8 @@ export enum ProjectPermissionSub {
CertificateTemplates = "certificate-templates", CertificateTemplates = "certificate-templates",
PkiAlerts = "pki-alerts", PkiAlerts = "pki-alerts",
PkiCollections = "pki-collections", PkiCollections = "pki-collections",
Kms = "kms" Kms = "kms",
Cmek = "cmek"
} }
type SubjectFields = { type SubjectFields = {
@@ -97,6 +107,7 @@ export type ProjectPermissionSet =
| [ProjectPermissionActions.Delete, ProjectPermissionSub.Workspace] | [ProjectPermissionActions.Delete, ProjectPermissionSub.Workspace]
| [ProjectPermissionActions.Edit, ProjectPermissionSub.Workspace] | [ProjectPermissionActions.Edit, ProjectPermissionSub.Workspace]
| [ProjectPermissionActions.Read, ProjectPermissionSub.SecretRollback] | [ProjectPermissionActions.Read, ProjectPermissionSub.SecretRollback]
| [ProjectPermissionActions.Create, ProjectPermissionSub.SecretRollback]; | [ProjectPermissionActions.Create, ProjectPermissionSub.SecretRollback]
| [ProjectPermissionCmekActions, ProjectPermissionSub.Cmek];
export type TProjectPermission = MongoAbility<ProjectPermissionSet>; export type TProjectPermission = MongoAbility<ProjectPermissionSet>;
+1
View File
@@ -10,6 +10,7 @@ export {
export type { TProjectPermission } from "./ProjectPermissionContext"; export type { TProjectPermission } from "./ProjectPermissionContext";
export { export {
ProjectPermissionActions, ProjectPermissionActions,
ProjectPermissionCmekActions,
ProjectPermissionProvider, ProjectPermissionProvider,
ProjectPermissionSub, ProjectPermissionSub,
useProjectPermission useProjectPermission
+3
View File
@@ -0,0 +1,3 @@
export * from "./mutations";
export * from "./queries";
export * from "./types";
@@ -0,0 +1,90 @@
import { useMutation, useQueryClient } from "@tanstack/react-query";
import { encodeBase64 } from "tweetnacl-util";
import { apiRequest } from "@app/config/request";
import { cmekKeys } from "@app/hooks/api/cmeks/queries";
import {
TCmekDecrypt,
TCmekDecryptResponse,
TCmekEncrypt,
TCmekEncryptResponse,
TCreateCmek,
TDeleteCmek,
TUpdateCmek
} from "@app/hooks/api/cmeks/types";
export const useCreateCmek = () => {
const queryClient = useQueryClient();
return useMutation({
mutationFn: async (payload: TCreateCmek) => {
const { data } = await apiRequest.post("/api/v1/kms/keys", payload);
return data;
},
onSuccess: (_, { projectId }) => {
queryClient.invalidateQueries(cmekKeys.getCmeksByProjectId({ projectId }));
}
});
};
export const useUpdateCmek = () => {
const queryClient = useQueryClient();
return useMutation({
mutationFn: async ({ keyId, name, description, isDisabled }: TUpdateCmek) => {
const { data } = await apiRequest.patch(`/api/v1/kms/keys/${keyId}`, {
name,
description,
isDisabled
});
return data;
},
onSuccess: (_, { projectId }) => {
queryClient.invalidateQueries(cmekKeys.getCmeksByProjectId({ projectId }));
}
});
};
export const useDeleteCmek = () => {
const queryClient = useQueryClient();
return useMutation({
mutationFn: async ({ keyId }: TDeleteCmek) => {
const { data } = await apiRequest.delete(`/api/v1/kms/keys/${keyId}`);
return data;
},
onSuccess: (_, { projectId }) => {
queryClient.invalidateQueries(cmekKeys.getCmeksByProjectId({ projectId }));
}
});
};
export const useCmekEncrypt = () => {
return useMutation({
mutationFn: async ({ keyId, plaintext, isBase64Encoded }: TCmekEncrypt) => {
const { data } = await apiRequest.post<TCmekEncryptResponse>(
`/api/v1/kms/keys/${keyId}/encrypt`,
{
plaintext: isBase64Encoded ? plaintext : encodeBase64(Buffer.from(plaintext))
}
);
return data;
}
});
};
export const useCmekDecrypt = () => {
return useMutation({
mutationFn: async ({ keyId, ciphertext }: TCmekDecrypt) => {
const { data } = await apiRequest.post<TCmekDecryptResponse>(
`/api/v1/kms/keys/${keyId}/decrypt`,
{
ciphertext
}
);
return data;
}
});
};
+53
View File
@@ -0,0 +1,53 @@
import { useQuery, UseQueryOptions } from "@tanstack/react-query";
import { apiRequest } from "@app/config/request";
import { CmekOrderBy, TListProjectCmeksDTO, TProjectCmeksList } from "@app/hooks/api/cmeks/types";
import { OrderByDirection } from "@app/hooks/api/generic/types";
export const cmekKeys = {
all: ["cmek"] as const,
lists: () => [...cmekKeys.all, "list"] as const,
getCmeksByProjectId: ({ projectId, ...filters }: TListProjectCmeksDTO) =>
[...cmekKeys.lists(), projectId, filters] as const
};
export const useGetCmeksByProjectId = (
{
projectId,
offset = 0,
limit = 100,
orderBy = CmekOrderBy.Name,
orderDirection = OrderByDirection.ASC,
search = ""
}: TListProjectCmeksDTO,
options?: Omit<
UseQueryOptions<
TProjectCmeksList,
unknown,
TProjectCmeksList,
ReturnType<typeof cmekKeys.getCmeksByProjectId>
>,
"queryKey" | "queryFn"
>
) => {
return useQuery({
queryKey: cmekKeys.getCmeksByProjectId({
projectId,
offset,
limit,
orderBy,
orderDirection,
search
}),
queryFn: async () => {
const { data } = await apiRequest.get<TProjectCmeksList>("/api/v1/kms/keys", {
params: { projectId, offset, limit, search, orderBy, orderDirection }
});
return data;
},
enabled: Boolean(projectId) && (options?.enabled ?? true),
keepPreviousData: true,
...options
});
};
+58
View File
@@ -0,0 +1,58 @@
import { OrderByDirection } from "@app/hooks/api/generic/types";
export type TCmek = {
id: string;
name: string;
description?: string;
encryptionAlgorithm: EncryptionAlgorithm;
projectId: string;
isDisabled: boolean;
isReserved: boolean;
orgId: string;
version: number;
createdAt: string;
updatedAt: string;
};
type ProjectRef = { projectId: string };
type KeyRef = { keyId: string };
export type TCreateCmek = Pick<TCmek, "name" | "description" | "encryptionAlgorithm"> & ProjectRef;
export type TUpdateCmek = KeyRef &
Partial<Pick<TCmek, "name" | "description" | "isDisabled">> &
ProjectRef;
export type TDeleteCmek = KeyRef & ProjectRef;
export type TCmekEncrypt = KeyRef & { plaintext: string; isBase64Encoded?: boolean };
export type TCmekDecrypt = KeyRef & { ciphertext: string };
export type TProjectCmeksList = {
keys: TCmek[];
totalCount: number;
};
export type TListProjectCmeksDTO = {
projectId: string;
offset?: number;
limit?: number;
orderBy?: CmekOrderBy;
orderDirection?: OrderByDirection;
search?: string;
};
export type TCmekEncryptResponse = {
ciphertext: string;
};
export type TCmekDecryptResponse = {
plaintext: string;
};
export enum CmekOrderBy {
Name = "name"
}
export enum EncryptionAlgorithm {
AES_GCM_256 = "aes-256-gcm",
AES_GCM_128 = "aes-128-gcm"
}
+16 -2
View File
@@ -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;
+4 -4
View File
@@ -8,9 +8,9 @@ import { AddExternalKmsType, KmsType } from "./types";
export const useAddExternalKms = (orgId: string) => { export const useAddExternalKms = (orgId: string) => {
const queryClient = useQueryClient(); const queryClient = useQueryClient();
return useMutation({ return useMutation({
mutationFn: async ({ slug, description, provider }: AddExternalKmsType) => { mutationFn: async ({ name, description, provider }: AddExternalKmsType) => {
const { data } = await apiRequest.post("/api/v1/external-kms", { const { data } = await apiRequest.post("/api/v1/external-kms", {
slug, name,
description, description,
provider provider
}); });
@@ -28,14 +28,14 @@ export const useUpdateExternalKms = (orgId: string) => {
return useMutation({ return useMutation({
mutationFn: async ({ mutationFn: async ({
kmsId, kmsId,
slug, name,
description, description,
provider provider
}: { }: {
kmsId: string; kmsId: string;
} & AddExternalKmsType) => { } & AddExternalKmsType) => {
const { data } = await apiRequest.patch(`/api/v1/external-kms/${kmsId}`, { const { data } = await apiRequest.patch(`/api/v1/external-kms/${kmsId}`, {
slug, name,
description, description,
provider provider
}); });
+1 -1
View File
@@ -46,7 +46,7 @@ export const useGetActiveProjectKms = (projectId: string) => {
} = await apiRequest.get<{ } = await apiRequest.get<{
secretManagerKmsKey: { secretManagerKmsKey: {
id: string; id: string;
slug: string; name: string;
isExternal: string; isExternal: string;
}; };
}>(`/api/v1/workspace/${projectId}/kms`); }>(`/api/v1/workspace/${projectId}/kms`);
+3 -3
View File
@@ -5,7 +5,7 @@ export type Kms = {
id: string; id: string;
description: string; description: string;
orgId: string; orgId: string;
slug: string; name: string;
external: { external: {
id: string; id: string;
status: string; status: string;
@@ -21,7 +21,7 @@ export type KmsListEntry = {
isDisabled: boolean; isDisabled: boolean;
createdAt: string; createdAt: string;
updatedAt: string; updatedAt: string;
slug: string; name: string;
externalKms: { externalKms: {
provider: string; provider: string;
status: string; status: string;
@@ -88,7 +88,7 @@ export const ExternalKmsInputSchema = z.discriminatedUnion("type", [
]); ]);
export const AddExternalKmsSchema = z.object({ export const AddExternalKmsSchema = z.object({
slug: z name: z
.string() .string()
.trim() .trim()
.min(1) .min(1)
+2 -2
View File
@@ -122,6 +122,6 @@ export type TOrgIdentitiesList = {
}; };
export enum OrgIdentityOrderBy { export enum OrgIdentityOrderBy {
Name = "name", Name = "name"
Role = "role" // Role = "role"
} }
@@ -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
View File
@@ -1,5 +1,6 @@
export { useDebounce } from "./useDebounce"; export { useDebounce } from "./useDebounce";
export { useLeaveConfirm } from "./useLeaveConfirm"; export { useLeaveConfirm } from "./useLeaveConfirm";
export { usePagination } from "./usePagination";
export { usePersistentState } from "./usePersistentState"; export { usePersistentState } from "./usePersistentState";
export { usePopUp } from "./usePopUp"; export { usePopUp } from "./usePopUp";
export { useSyntaxHighlight } from "./useSyntaxHighlight"; export { useSyntaxHighlight } from "./useSyntaxHighlight";
+6 -3
View File
@@ -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];
}; };
+31
View File
@@ -0,0 +1,31 @@
import { useState } from "react";
import { OrderByDirection } from "@app/hooks/api/generic/types";
import { useDebounce } from "@app/hooks/useDebounce";
export const usePagination = <T extends string>(initialOrderBy: T) => {
const [page, setPage] = useState(1);
const [perPage, setPerPage] = useState(20);
const [orderDirection, setOrderDirection] = useState(OrderByDirection.ASC);
const [orderBy, setOrderBy] = useState<T>(initialOrderBy);
const [search, setSearch] = useState("");
const [debouncedSearch] = useDebounce(search);
const offset = (page - 1) * perPage;
return {
offset,
limit: perPage,
page,
setPage,
perPage,
setPerPage,
orderDirection,
setOrderDirection,
debouncedSearch,
search,
setSearch,
orderBy,
setOrderBy
};
};
+11 -1
View File
@@ -630,6 +630,16 @@ export const AppLayout = ({ children }: LayoutProps) => {
</MenuItem> </MenuItem>
</a> </a>
</Link> </Link>
<Link href={`/project/${currentWorkspace?.id}/kms`} passHref>
<a>
<MenuItem
isSelected={router.asPath === `/project/${currentWorkspace?.id}/kms`}
icon="system-outline-90-lock-closed"
>
Key Management
</MenuItem>
</a>
</Link>
<Link href={`/project/${currentWorkspace?.id}/members`} passHref> <Link href={`/project/${currentWorkspace?.id}/members`} passHref>
<a> <a>
<MenuItem <MenuItem
@@ -942,7 +952,7 @@ export const AppLayout = ({ children }: LayoutProps) => {
</SelectItem> </SelectItem>
{externalKmsList?.map((kms) => ( {externalKmsList?.map((kms) => (
<SelectItem value={kms.id} key={`kms-${kms.id}`}> <SelectItem value={kms.id} key={`kms-${kms.id}`}>
{kms.slug} {kms.name}
</SelectItem> </SelectItem>
))} ))}
</Select> </Select>
@@ -1101,7 +1101,7 @@ const OrganizationPage = () => {
</SelectItem> </SelectItem>
{externalKmsList?.map((kms) => ( {externalKmsList?.map((kms) => (
<SelectItem value={kms.id} key={`kms-${kms.id}`}> <SelectItem value={kms.id} key={`kms-${kms.id}`}>
{kms.slug} {kms.name}
</SelectItem> </SelectItem>
))} ))}
</Select> </Select>

Some files were not shown because too many files have changed in this diff Show More