mirror of
https://github.com/awatertrevi/infisical.git
synced 2026-10-08 02:27:37 +00:00
merge deconflict
This commit is contained in:
@@ -22,3 +22,5 @@ frontend/src/components/secret-rotations-v2/ViewSecretRotationV2GeneratedCredent
|
|||||||
frontend/src/hooks/api/secretRotationsV2/types/index.ts:generic-api-key:28
|
frontend/src/hooks/api/secretRotationsV2/types/index.ts:generic-api-key:28
|
||||||
frontend/src/hooks/api/secretRotationsV2/types/index.ts:generic-api-key:65
|
frontend/src/hooks/api/secretRotationsV2/types/index.ts:generic-api-key:65
|
||||||
frontend/src/pages/secret-manager/SecretDashboardPage/components/SecretRotationListView/SecretRotationItem.tsx:generic-api-key:26
|
frontend/src/pages/secret-manager/SecretDashboardPage/components/SecretRotationListView/SecretRotationItem.tsx:generic-api-key:26
|
||||||
|
docs/documentation/platform/kms/overview.mdx:generic-api-key:281
|
||||||
|
docs/documentation/platform/kms/overview.mdx:generic-api-key:344
|
||||||
|
|||||||
+2
-1
@@ -8,7 +8,8 @@ RUN apt-get update && apt-get install -y \
|
|||||||
python3 \
|
python3 \
|
||||||
make \
|
make \
|
||||||
g++ \
|
g++ \
|
||||||
openssh-client
|
openssh-client \
|
||||||
|
openssl
|
||||||
|
|
||||||
# Install dependencies for TDS driver (required for SAP ASE dynamic secrets)
|
# Install dependencies for TDS driver (required for SAP ASE dynamic secrets)
|
||||||
RUN apt-get install -y \
|
RUN apt-get install -y \
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ RUN apt-get update && apt-get install -y \
|
|||||||
make \
|
make \
|
||||||
g++ \
|
g++ \
|
||||||
openssh-client \
|
openssh-client \
|
||||||
|
openssl \
|
||||||
curl \
|
curl \
|
||||||
pkg-config
|
pkg-config
|
||||||
|
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ export const mockKeyStore = (): TKeyStoreFactory => {
|
|||||||
store[key] = value;
|
store[key] = value;
|
||||||
return "OK";
|
return "OK";
|
||||||
},
|
},
|
||||||
|
setExpiry: async () => 0,
|
||||||
setItemWithExpiry: async (key, value) => {
|
setItemWithExpiry: async (key, value) => {
|
||||||
store[key] = value;
|
store[key] = value;
|
||||||
return "OK";
|
return "OK";
|
||||||
|
|||||||
Generated
+9
-13
@@ -132,7 +132,7 @@
|
|||||||
"@types/jsrp": "^0.2.6",
|
"@types/jsrp": "^0.2.6",
|
||||||
"@types/libsodium-wrappers": "^0.7.13",
|
"@types/libsodium-wrappers": "^0.7.13",
|
||||||
"@types/lodash.isequal": "^4.5.8",
|
"@types/lodash.isequal": "^4.5.8",
|
||||||
"@types/node": "^20.9.5",
|
"@types/node": "^20.17.30",
|
||||||
"@types/nodemailer": "^6.4.14",
|
"@types/nodemailer": "^6.4.14",
|
||||||
"@types/passport-github": "^1.1.12",
|
"@types/passport-github": "^1.1.12",
|
||||||
"@types/passport-google-oauth20": "^2.0.14",
|
"@types/passport-google-oauth20": "^2.0.14",
|
||||||
@@ -9753,11 +9753,12 @@
|
|||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/@types/node": {
|
"node_modules/@types/node": {
|
||||||
"version": "20.9.5",
|
"version": "20.17.30",
|
||||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-20.9.5.tgz",
|
"resolved": "https://registry.npmjs.org/@types/node/-/node-20.17.30.tgz",
|
||||||
"integrity": "sha512-Uq2xbNq0chGg+/WQEU0LJTSs/1nKxz6u1iemLcGomkSnKokbW1fbLqc3HOqCf2JP7KjlL4QkS7oZZTrOQHQYgQ==",
|
"integrity": "sha512-7zf4YyHA+jvBNfVrk2Gtvs6x7E8V+YDW05bNfG2XkWDJfYRXrTiP/DsB2zSYTaHX0bGIujTBQdMVAhb+j7mwpg==",
|
||||||
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"undici-types": "~5.26.4"
|
"undici-types": "~6.19.2"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@types/node-fetch": {
|
"node_modules/@types/node-fetch": {
|
||||||
@@ -20081,11 +20082,6 @@
|
|||||||
"undici-types": "~6.19.2"
|
"undici-types": "~6.19.2"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/scim-patch/node_modules/undici-types": {
|
|
||||||
"version": "6.19.8",
|
|
||||||
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.19.8.tgz",
|
|
||||||
"integrity": "sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw=="
|
|
||||||
},
|
|
||||||
"node_modules/scim2-parse-filter": {
|
"node_modules/scim2-parse-filter": {
|
||||||
"version": "0.2.10",
|
"version": "0.2.10",
|
||||||
"resolved": "https://registry.npmjs.org/scim2-parse-filter/-/scim2-parse-filter-0.2.10.tgz",
|
"resolved": "https://registry.npmjs.org/scim2-parse-filter/-/scim2-parse-filter-0.2.10.tgz",
|
||||||
@@ -22442,9 +22438,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/undici-types": {
|
"node_modules/undici-types": {
|
||||||
"version": "5.26.5",
|
"version": "6.19.8",
|
||||||
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz",
|
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.19.8.tgz",
|
||||||
"integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA=="
|
"integrity": "sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw=="
|
||||||
},
|
},
|
||||||
"node_modules/unicode-canonical-property-names-ecmascript": {
|
"node_modules/unicode-canonical-property-names-ecmascript": {
|
||||||
"version": "2.0.0",
|
"version": "2.0.0",
|
||||||
|
|||||||
@@ -89,7 +89,7 @@
|
|||||||
"@types/jsrp": "^0.2.6",
|
"@types/jsrp": "^0.2.6",
|
||||||
"@types/libsodium-wrappers": "^0.7.13",
|
"@types/libsodium-wrappers": "^0.7.13",
|
||||||
"@types/lodash.isequal": "^4.5.8",
|
"@types/lodash.isequal": "^4.5.8",
|
||||||
"@types/node": "^20.9.5",
|
"@types/node": "^20.17.30",
|
||||||
"@types/nodemailer": "^6.4.14",
|
"@types/nodemailer": "^6.4.14",
|
||||||
"@types/passport-github": "^1.1.12",
|
"@types/passport-github": "^1.1.12",
|
||||||
"@types/passport-google-oauth20": "^2.0.14",
|
"@types/passport-google-oauth20": "^2.0.14",
|
||||||
|
|||||||
@@ -0,0 +1,25 @@
|
|||||||
|
import { Knex } from "knex";
|
||||||
|
|
||||||
|
import { KmsKeyUsage } from "@app/services/kms/kms-types";
|
||||||
|
|
||||||
|
import { TableName } from "../schemas";
|
||||||
|
|
||||||
|
export async function up(knex: Knex): Promise<void> {
|
||||||
|
const hasKeyUsageColumn = await knex.schema.hasColumn(TableName.KmsKey, "keyUsage");
|
||||||
|
|
||||||
|
if (!hasKeyUsageColumn) {
|
||||||
|
await knex.schema.alterTable(TableName.KmsKey, (t) => {
|
||||||
|
t.string("keyUsage").notNullable().defaultTo(KmsKeyUsage.ENCRYPT_DECRYPT);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function down(knex: Knex): Promise<void> {
|
||||||
|
const hasKeyUsageColumn = await knex.schema.hasColumn(TableName.KmsKey, "keyUsage");
|
||||||
|
|
||||||
|
if (hasKeyUsageColumn) {
|
||||||
|
await knex.schema.alterTable(TableName.KmsKey, (t) => {
|
||||||
|
t.dropColumn("keyUsage");
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
import { Knex } from "knex";
|
||||||
|
|
||||||
|
import { TableName } from "../schemas";
|
||||||
|
|
||||||
|
export async function up(knex: Knex): Promise<void> {
|
||||||
|
if (!(await knex.schema.hasColumn(TableName.ResourceMetadata, "dynamicSecretId"))) {
|
||||||
|
await knex.schema.alterTable(TableName.ResourceMetadata, (tb) => {
|
||||||
|
tb.uuid("dynamicSecretId");
|
||||||
|
tb.foreign("dynamicSecretId").references("id").inTable(TableName.DynamicSecret).onDelete("CASCADE");
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function down(knex: Knex): Promise<void> {
|
||||||
|
if (await knex.schema.hasColumn(TableName.ResourceMetadata, "dynamicSecretId")) {
|
||||||
|
await knex.schema.alterTable(TableName.ResourceMetadata, (tb) => {
|
||||||
|
tb.dropColumn("dynamicSecretId");
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
+15
@@ -0,0 +1,15 @@
|
|||||||
|
import { Knex } from "knex";
|
||||||
|
|
||||||
|
import { TableName } from "../schemas";
|
||||||
|
|
||||||
|
export async function up(knex: Knex): Promise<void> {
|
||||||
|
await knex.schema.alterTable(TableName.Certificate, (t) => {
|
||||||
|
t.string("altNames", 4096).alter();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function down(knex: Knex): Promise<void> {
|
||||||
|
await knex.schema.alterTable(TableName.Certificate, (t) => {
|
||||||
|
t.string("altNames").alter(); // Defaults to varchar(255)
|
||||||
|
});
|
||||||
|
}
|
||||||
+15
@@ -0,0 +1,15 @@
|
|||||||
|
import { Knex } from "knex";
|
||||||
|
|
||||||
|
import { TableName } from "../schemas";
|
||||||
|
|
||||||
|
export async function up(knex: Knex): Promise<void> {
|
||||||
|
await knex.schema.alterTable(TableName.KmipOrgServerCertificates, (t) => {
|
||||||
|
t.string("altNames", 4096).alter();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function down(knex: Knex): Promise<void> {
|
||||||
|
await knex.schema.alterTable(TableName.KmipOrgServerCertificates, (t) => {
|
||||||
|
t.string("altNames").alter(); // Defaults to varchar(255)
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -16,7 +16,8 @@ export const KmsKeysSchema = z.object({
|
|||||||
name: z.string(),
|
name: z.string(),
|
||||||
createdAt: z.date(),
|
createdAt: z.date(),
|
||||||
updatedAt: z.date(),
|
updatedAt: z.date(),
|
||||||
projectId: z.string().nullable().optional()
|
projectId: z.string().nullable().optional(),
|
||||||
|
keyUsage: z.string().default("encrypt-decrypt")
|
||||||
});
|
});
|
||||||
|
|
||||||
export type TKmsKeys = z.infer<typeof KmsKeysSchema>;
|
export type TKmsKeys = z.infer<typeof KmsKeysSchema>;
|
||||||
|
|||||||
@@ -16,7 +16,8 @@ export const ResourceMetadataSchema = z.object({
|
|||||||
identityId: z.string().uuid().nullable().optional(),
|
identityId: z.string().uuid().nullable().optional(),
|
||||||
secretId: z.string().uuid().nullable().optional(),
|
secretId: z.string().uuid().nullable().optional(),
|
||||||
createdAt: z.date(),
|
createdAt: z.date(),
|
||||||
updatedAt: z.date()
|
updatedAt: z.date(),
|
||||||
|
dynamicSecretId: z.string().uuid().nullable().optional()
|
||||||
});
|
});
|
||||||
|
|
||||||
export type TResourceMetadata = z.infer<typeof ResourceMetadataSchema>;
|
export type TResourceMetadata = z.infer<typeof ResourceMetadataSchema>;
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ import { slugSchema } from "@app/server/lib/schemas";
|
|||||||
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
|
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
|
||||||
import { SanitizedDynamicSecretSchema } from "@app/server/routes/sanitizedSchemas";
|
import { SanitizedDynamicSecretSchema } from "@app/server/routes/sanitizedSchemas";
|
||||||
import { AuthMode } from "@app/services/auth/auth-type";
|
import { AuthMode } from "@app/services/auth/auth-type";
|
||||||
|
import { ResourceMetadataSchema } from "@app/services/resource-metadata/resource-metadata-schema";
|
||||||
|
|
||||||
export const registerDynamicSecretRouter = async (server: FastifyZodProvider) => {
|
export const registerDynamicSecretRouter = async (server: FastifyZodProvider) => {
|
||||||
server.route({
|
server.route({
|
||||||
@@ -48,7 +49,8 @@ export const registerDynamicSecretRouter = async (server: FastifyZodProvider) =>
|
|||||||
.nullable(),
|
.nullable(),
|
||||||
path: z.string().describe(DYNAMIC_SECRETS.CREATE.path).trim().default("/").transform(removeTrailingSlash),
|
path: z.string().describe(DYNAMIC_SECRETS.CREATE.path).trim().default("/").transform(removeTrailingSlash),
|
||||||
environmentSlug: z.string().describe(DYNAMIC_SECRETS.CREATE.environmentSlug).min(1),
|
environmentSlug: z.string().describe(DYNAMIC_SECRETS.CREATE.environmentSlug).min(1),
|
||||||
name: slugSchema({ min: 1, max: 64, field: "Name" }).describe(DYNAMIC_SECRETS.CREATE.name)
|
name: slugSchema({ min: 1, max: 64, field: "Name" }).describe(DYNAMIC_SECRETS.CREATE.name),
|
||||||
|
metadata: ResourceMetadataSchema.optional()
|
||||||
}),
|
}),
|
||||||
response: {
|
response: {
|
||||||
200: z.object({
|
200: z.object({
|
||||||
@@ -143,7 +145,8 @@ export const registerDynamicSecretRouter = async (server: FastifyZodProvider) =>
|
|||||||
ctx.addIssue({ code: z.ZodIssueCode.custom, message: "TTL must be less than a day" });
|
ctx.addIssue({ code: z.ZodIssueCode.custom, message: "TTL must be less than a day" });
|
||||||
})
|
})
|
||||||
.nullable(),
|
.nullable(),
|
||||||
newName: z.string().describe(DYNAMIC_SECRETS.UPDATE.newName).optional()
|
newName: z.string().describe(DYNAMIC_SECRETS.UPDATE.newName).optional(),
|
||||||
|
metadata: ResourceMetadataSchema.optional()
|
||||||
})
|
})
|
||||||
}),
|
}),
|
||||||
response: {
|
response: {
|
||||||
@@ -238,6 +241,7 @@ export const registerDynamicSecretRouter = async (server: FastifyZodProvider) =>
|
|||||||
name: req.params.name,
|
name: req.params.name,
|
||||||
...req.query
|
...req.query
|
||||||
});
|
});
|
||||||
|
|
||||||
return { dynamicSecret: dynamicSecretCfg };
|
return { dynamicSecret: dynamicSecretCfg };
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import z from "zod";
|
|||||||
|
|
||||||
import { KmsKeysSchema } from "@app/db/schemas";
|
import { KmsKeysSchema } from "@app/db/schemas";
|
||||||
import { EventType } from "@app/ee/services/audit-log/audit-log-types";
|
import { EventType } from "@app/ee/services/audit-log/audit-log-types";
|
||||||
import { SymmetricEncryption } from "@app/lib/crypto/cipher";
|
import { SymmetricKeyAlgorithm } from "@app/lib/crypto/cipher";
|
||||||
import { ForbiddenRequestError, NotFoundError } from "@app/lib/errors";
|
import { ForbiddenRequestError, NotFoundError } from "@app/lib/errors";
|
||||||
import { writeLimit } from "@app/server/config/rateLimiter";
|
import { writeLimit } from "@app/server/config/rateLimiter";
|
||||||
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
|
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
|
||||||
@@ -74,7 +74,7 @@ export const registerKmipSpecRouter = async (server: FastifyZodProvider) => {
|
|||||||
schema: {
|
schema: {
|
||||||
description: "KMIP endpoint for creating managed objects",
|
description: "KMIP endpoint for creating managed objects",
|
||||||
body: z.object({
|
body: z.object({
|
||||||
algorithm: z.nativeEnum(SymmetricEncryption)
|
algorithm: z.nativeEnum(SymmetricKeyAlgorithm)
|
||||||
}),
|
}),
|
||||||
response: {
|
response: {
|
||||||
200: KmsKeysSchema
|
200: KmsKeysSchema
|
||||||
@@ -433,7 +433,7 @@ export const registerKmipSpecRouter = async (server: FastifyZodProvider) => {
|
|||||||
body: z.object({
|
body: z.object({
|
||||||
key: z.string(),
|
key: z.string(),
|
||||||
name: z.string(),
|
name: z.string(),
|
||||||
algorithm: z.nativeEnum(SymmetricEncryption)
|
algorithm: z.nativeEnum(SymmetricKeyAlgorithm)
|
||||||
}),
|
}),
|
||||||
response: {
|
response: {
|
||||||
200: z.object({
|
200: z.object({
|
||||||
|
|||||||
@@ -136,11 +136,12 @@ export const registerOidcRouter = async (server: FastifyZodProvider) => {
|
|||||||
url: "/login/error",
|
url: "/login/error",
|
||||||
method: "GET",
|
method: "GET",
|
||||||
handler: async (req, res) => {
|
handler: async (req, res) => {
|
||||||
|
const failureMessage = req.session.get<any>("messages");
|
||||||
await req.session.destroy();
|
await req.session.destroy();
|
||||||
|
|
||||||
return res.status(500).send({
|
return res.status(500).send({
|
||||||
error: "Authentication error",
|
error: "Authentication error",
|
||||||
details: req.query
|
details: failureMessage ?? req.query
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -23,7 +23,8 @@ export const registerSecretRotationProviderRouter = async (server: FastifyZodPro
|
|||||||
title: z.string(),
|
title: z.string(),
|
||||||
image: z.string().optional(),
|
image: z.string().optional(),
|
||||||
description: z.string().optional(),
|
description: z.string().optional(),
|
||||||
template: z.any()
|
template: z.any(),
|
||||||
|
isDeprecated: z.boolean().optional()
|
||||||
})
|
})
|
||||||
.array()
|
.array()
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
|
|
||||||
import { SecretRotationOutputsSchema, SecretRotationsSchema } from "@app/db/schemas";
|
import { SecretRotationOutputsSchema, SecretRotationsSchema } from "@app/db/schemas";
|
||||||
import { BadRequestError } from "@app/lib/errors";
|
|
||||||
import { removeTrailingSlash } from "@app/lib/fn";
|
import { removeTrailingSlash } from "@app/lib/fn";
|
||||||
import { readLimit, writeLimit } from "@app/server/config/rateLimiter";
|
import { readLimit, writeLimit } from "@app/server/config/rateLimiter";
|
||||||
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
|
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
|
||||||
@@ -41,10 +40,16 @@ export const registerSecretRotationRouter = async (server: FastifyZodProvider) =
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
onRequest: verifyAuth([AuthMode.JWT]),
|
onRequest: verifyAuth([AuthMode.JWT]),
|
||||||
handler: async () => {
|
handler: async (req) => {
|
||||||
throw new BadRequestError({
|
const secretRotation = await server.services.secretRotation.createRotation({
|
||||||
message: `This version of Secret Rotations has been deprecated. Please see docs for new version.`
|
actor: req.permission.type,
|
||||||
|
actorAuthMethod: req.permission.authMethod,
|
||||||
|
actorId: req.permission.id,
|
||||||
|
actorOrgId: req.permission.orgId,
|
||||||
|
...req.body,
|
||||||
|
projectId: req.body.workspaceId
|
||||||
});
|
});
|
||||||
|
return { secretRotation };
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -12,7 +12,8 @@ import {
|
|||||||
import { SshCaStatus, SshCertType } from "@app/ee/services/ssh/ssh-certificate-authority-types";
|
import { SshCaStatus, SshCertType } from "@app/ee/services/ssh/ssh-certificate-authority-types";
|
||||||
import { SshCertKeyAlgorithm } from "@app/ee/services/ssh-certificate/ssh-certificate-types";
|
import { SshCertKeyAlgorithm } from "@app/ee/services/ssh-certificate/ssh-certificate-types";
|
||||||
import { SshCertTemplateStatus } from "@app/ee/services/ssh-certificate-template/ssh-certificate-template-types";
|
import { SshCertTemplateStatus } from "@app/ee/services/ssh-certificate-template/ssh-certificate-template-types";
|
||||||
import { SymmetricEncryption } from "@app/lib/crypto/cipher";
|
import { SymmetricKeyAlgorithm } from "@app/lib/crypto/cipher";
|
||||||
|
import { AsymmetricKeyAlgorithm, SigningAlgorithm } from "@app/lib/crypto/sign/types";
|
||||||
import { TProjectPermission } from "@app/lib/types";
|
import { TProjectPermission } from "@app/lib/types";
|
||||||
import { AppConnection } from "@app/services/app-connection/app-connection-enums";
|
import { AppConnection } from "@app/services/app-connection/app-connection-enums";
|
||||||
import { TCreateAppConnectionDTO, TUpdateAppConnectionDTO } from "@app/services/app-connection/app-connection-types";
|
import { TCreateAppConnectionDTO, TUpdateAppConnectionDTO } from "@app/services/app-connection/app-connection-types";
|
||||||
@@ -255,6 +256,11 @@ export enum EventType {
|
|||||||
GET_CMEK = "get-cmek",
|
GET_CMEK = "get-cmek",
|
||||||
CMEK_ENCRYPT = "cmek-encrypt",
|
CMEK_ENCRYPT = "cmek-encrypt",
|
||||||
CMEK_DECRYPT = "cmek-decrypt",
|
CMEK_DECRYPT = "cmek-decrypt",
|
||||||
|
CMEK_SIGN = "cmek-sign",
|
||||||
|
CMEK_VERIFY = "cmek-verify",
|
||||||
|
CMEK_LIST_SIGNING_ALGORITHMS = "cmek-list-signing-algorithms",
|
||||||
|
CMEK_GET_PUBLIC_KEY = "cmek-get-public-key",
|
||||||
|
|
||||||
UPDATE_EXTERNAL_GROUP_ORG_ROLE_MAPPINGS = "update-external-group-org-role-mapping",
|
UPDATE_EXTERNAL_GROUP_ORG_ROLE_MAPPINGS = "update-external-group-org-role-mapping",
|
||||||
GET_EXTERNAL_GROUP_ORG_ROLE_MAPPINGS = "get-external-group-org-role-mapping",
|
GET_EXTERNAL_GROUP_ORG_ROLE_MAPPINGS = "get-external-group-org-role-mapping",
|
||||||
GET_PROJECT_TEMPLATES = "get-project-templates",
|
GET_PROJECT_TEMPLATES = "get-project-templates",
|
||||||
@@ -1997,7 +2003,7 @@ interface CreateCmekEvent {
|
|||||||
keyId: string;
|
keyId: string;
|
||||||
name: string;
|
name: string;
|
||||||
description?: string;
|
description?: string;
|
||||||
encryptionAlgorithm: SymmetricEncryption;
|
encryptionAlgorithm: SymmetricKeyAlgorithm | AsymmetricKeyAlgorithm;
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2045,6 +2051,39 @@ interface CmekDecryptEvent {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface CmekSignEvent {
|
||||||
|
type: EventType.CMEK_SIGN;
|
||||||
|
metadata: {
|
||||||
|
keyId: string;
|
||||||
|
signingAlgorithm: SigningAlgorithm;
|
||||||
|
signature: string;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
interface CmekVerifyEvent {
|
||||||
|
type: EventType.CMEK_VERIFY;
|
||||||
|
metadata: {
|
||||||
|
keyId: string;
|
||||||
|
signingAlgorithm: SigningAlgorithm;
|
||||||
|
signature: string;
|
||||||
|
signatureValid: boolean;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
interface CmekListSigningAlgorithmsEvent {
|
||||||
|
type: EventType.CMEK_LIST_SIGNING_ALGORITHMS;
|
||||||
|
metadata: {
|
||||||
|
keyId: string;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
interface CmekGetPublicKeyEvent {
|
||||||
|
type: EventType.CMEK_GET_PUBLIC_KEY;
|
||||||
|
metadata: {
|
||||||
|
keyId: string;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
interface GetExternalGroupOrgRoleMappingsEvent {
|
interface GetExternalGroupOrgRoleMappingsEvent {
|
||||||
type: EventType.GET_EXTERNAL_GROUP_ORG_ROLE_MAPPINGS;
|
type: EventType.GET_EXTERNAL_GROUP_ORG_ROLE_MAPPINGS;
|
||||||
metadata?: Record<string, never>; // not needed, based off orgId
|
metadata?: Record<string, never>; // not needed, based off orgId
|
||||||
@@ -2639,6 +2678,10 @@ export type Event =
|
|||||||
| GetCmeksEvent
|
| GetCmeksEvent
|
||||||
| CmekEncryptEvent
|
| CmekEncryptEvent
|
||||||
| CmekDecryptEvent
|
| CmekDecryptEvent
|
||||||
|
| CmekSignEvent
|
||||||
|
| CmekVerifyEvent
|
||||||
|
| CmekListSigningAlgorithmsEvent
|
||||||
|
| CmekGetPublicKeyEvent
|
||||||
| GetExternalGroupOrgRoleMappingsEvent
|
| GetExternalGroupOrgRoleMappingsEvent
|
||||||
| UpdateExternalGroupOrgRoleMappingsEvent
|
| UpdateExternalGroupOrgRoleMappingsEvent
|
||||||
| GetProjectTemplatesEvent
|
| GetProjectTemplatesEvent
|
||||||
|
|||||||
@@ -78,10 +78,6 @@ export const dynamicSecretLeaseServiceFactory = ({
|
|||||||
actorOrgId,
|
actorOrgId,
|
||||||
actionProjectType: ActionProjectType.SecretManager
|
actionProjectType: ActionProjectType.SecretManager
|
||||||
});
|
});
|
||||||
ForbiddenError.from(permission).throwUnlessCan(
|
|
||||||
ProjectPermissionDynamicSecretActions.Lease,
|
|
||||||
subject(ProjectPermissionSub.DynamicSecrets, { environment: environmentSlug, secretPath: path })
|
|
||||||
);
|
|
||||||
|
|
||||||
const plan = await licenseService.getPlan(actorOrgId);
|
const plan = await licenseService.getPlan(actorOrgId);
|
||||||
if (!plan?.dynamicSecret) {
|
if (!plan?.dynamicSecret) {
|
||||||
@@ -102,6 +98,15 @@ export const dynamicSecretLeaseServiceFactory = ({
|
|||||||
message: `Dynamic secret with name '${name}' in folder with path '${path}' not found`
|
message: `Dynamic secret with name '${name}' in folder with path '${path}' not found`
|
||||||
});
|
});
|
||||||
|
|
||||||
|
ForbiddenError.from(permission).throwUnlessCan(
|
||||||
|
ProjectPermissionDynamicSecretActions.Lease,
|
||||||
|
subject(ProjectPermissionSub.DynamicSecrets, {
|
||||||
|
environment: environmentSlug,
|
||||||
|
secretPath: path,
|
||||||
|
metadata: dynamicSecretCfg.metadata
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
const totalLeasesTaken = await dynamicSecretLeaseDAL.countLeasesForDynamicSecret(dynamicSecretCfg.id);
|
const totalLeasesTaken = await dynamicSecretLeaseDAL.countLeasesForDynamicSecret(dynamicSecretCfg.id);
|
||||||
if (totalLeasesTaken >= appCfg.MAX_LEASE_LIMIT)
|
if (totalLeasesTaken >= appCfg.MAX_LEASE_LIMIT)
|
||||||
throw new BadRequestError({ message: `Max lease limit reached. Limit: ${appCfg.MAX_LEASE_LIMIT}` });
|
throw new BadRequestError({ message: `Max lease limit reached. Limit: ${appCfg.MAX_LEASE_LIMIT}` });
|
||||||
@@ -159,10 +164,6 @@ export const dynamicSecretLeaseServiceFactory = ({
|
|||||||
actorOrgId,
|
actorOrgId,
|
||||||
actionProjectType: ActionProjectType.SecretManager
|
actionProjectType: ActionProjectType.SecretManager
|
||||||
});
|
});
|
||||||
ForbiddenError.from(permission).throwUnlessCan(
|
|
||||||
ProjectPermissionDynamicSecretActions.Lease,
|
|
||||||
subject(ProjectPermissionSub.DynamicSecrets, { environment: environmentSlug, secretPath: path })
|
|
||||||
);
|
|
||||||
|
|
||||||
const { decryptor: secretManagerDecryptor } = await kmsService.createCipherPairWithDataKey({
|
const { decryptor: secretManagerDecryptor } = await kmsService.createCipherPairWithDataKey({
|
||||||
type: KmsDataKey.SecretManager,
|
type: KmsDataKey.SecretManager,
|
||||||
@@ -187,7 +188,25 @@ export const dynamicSecretLeaseServiceFactory = ({
|
|||||||
throw new NotFoundError({ message: `Dynamic secret lease with ID '${leaseId}' not found` });
|
throw new NotFoundError({ message: `Dynamic secret lease with ID '${leaseId}' not found` });
|
||||||
}
|
}
|
||||||
|
|
||||||
const dynamicSecretCfg = dynamicSecretLease.dynamicSecret;
|
const dynamicSecretCfg = await dynamicSecretDAL.findOne({
|
||||||
|
id: dynamicSecretLease.dynamicSecretId,
|
||||||
|
folderId: folder.id
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!dynamicSecretCfg)
|
||||||
|
throw new NotFoundError({
|
||||||
|
message: `Dynamic secret with ID '${dynamicSecretLease.dynamicSecretId}' not found`
|
||||||
|
});
|
||||||
|
|
||||||
|
ForbiddenError.from(permission).throwUnlessCan(
|
||||||
|
ProjectPermissionDynamicSecretActions.Lease,
|
||||||
|
subject(ProjectPermissionSub.DynamicSecrets, {
|
||||||
|
environment: environmentSlug,
|
||||||
|
secretPath: path,
|
||||||
|
metadata: dynamicSecretCfg.metadata
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
const selectedProvider = dynamicSecretProviders[dynamicSecretCfg.type as DynamicSecretProviders];
|
const selectedProvider = dynamicSecretProviders[dynamicSecretCfg.type as DynamicSecretProviders];
|
||||||
const decryptedStoredInput = JSON.parse(
|
const decryptedStoredInput = JSON.parse(
|
||||||
secretManagerDecryptor({ cipherTextBlob: Buffer.from(dynamicSecretCfg.encryptedInput) }).toString()
|
secretManagerDecryptor({ cipherTextBlob: Buffer.from(dynamicSecretCfg.encryptedInput) }).toString()
|
||||||
@@ -239,10 +258,6 @@ export const dynamicSecretLeaseServiceFactory = ({
|
|||||||
actorOrgId,
|
actorOrgId,
|
||||||
actionProjectType: ActionProjectType.SecretManager
|
actionProjectType: ActionProjectType.SecretManager
|
||||||
});
|
});
|
||||||
ForbiddenError.from(permission).throwUnlessCan(
|
|
||||||
ProjectPermissionDynamicSecretActions.Lease,
|
|
||||||
subject(ProjectPermissionSub.DynamicSecrets, { environment: environmentSlug, secretPath: path })
|
|
||||||
);
|
|
||||||
|
|
||||||
const { decryptor: secretManagerDecryptor } = await kmsService.createCipherPairWithDataKey({
|
const { decryptor: secretManagerDecryptor } = await kmsService.createCipherPairWithDataKey({
|
||||||
type: KmsDataKey.SecretManager,
|
type: KmsDataKey.SecretManager,
|
||||||
@@ -259,7 +274,25 @@ export const dynamicSecretLeaseServiceFactory = ({
|
|||||||
if (!dynamicSecretLease || dynamicSecretLease.dynamicSecret.folderId !== folder.id)
|
if (!dynamicSecretLease || dynamicSecretLease.dynamicSecret.folderId !== folder.id)
|
||||||
throw new NotFoundError({ message: `Dynamic secret lease with ID '${leaseId}' not found` });
|
throw new NotFoundError({ message: `Dynamic secret lease with ID '${leaseId}' not found` });
|
||||||
|
|
||||||
const dynamicSecretCfg = dynamicSecretLease.dynamicSecret;
|
const dynamicSecretCfg = await dynamicSecretDAL.findOne({
|
||||||
|
id: dynamicSecretLease.dynamicSecretId,
|
||||||
|
folderId: folder.id
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!dynamicSecretCfg)
|
||||||
|
throw new NotFoundError({
|
||||||
|
message: `Dynamic secret with ID '${dynamicSecretLease.dynamicSecretId}' not found`
|
||||||
|
});
|
||||||
|
|
||||||
|
ForbiddenError.from(permission).throwUnlessCan(
|
||||||
|
ProjectPermissionDynamicSecretActions.Lease,
|
||||||
|
subject(ProjectPermissionSub.DynamicSecrets, {
|
||||||
|
environment: environmentSlug,
|
||||||
|
secretPath: path,
|
||||||
|
metadata: dynamicSecretCfg.metadata
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
const selectedProvider = dynamicSecretProviders[dynamicSecretCfg.type as DynamicSecretProviders];
|
const selectedProvider = dynamicSecretProviders[dynamicSecretCfg.type as DynamicSecretProviders];
|
||||||
const decryptedStoredInput = JSON.parse(
|
const decryptedStoredInput = JSON.parse(
|
||||||
secretManagerDecryptor({ cipherTextBlob: Buffer.from(dynamicSecretCfg.encryptedInput) }).toString()
|
secretManagerDecryptor({ cipherTextBlob: Buffer.from(dynamicSecretCfg.encryptedInput) }).toString()
|
||||||
@@ -309,10 +342,6 @@ export const dynamicSecretLeaseServiceFactory = ({
|
|||||||
actorOrgId,
|
actorOrgId,
|
||||||
actionProjectType: ActionProjectType.SecretManager
|
actionProjectType: ActionProjectType.SecretManager
|
||||||
});
|
});
|
||||||
ForbiddenError.from(permission).throwUnlessCan(
|
|
||||||
ProjectPermissionDynamicSecretActions.Lease,
|
|
||||||
subject(ProjectPermissionSub.DynamicSecrets, { environment: environmentSlug, secretPath: path })
|
|
||||||
);
|
|
||||||
|
|
||||||
const folder = await folderDAL.findBySecretPath(projectId, environmentSlug, path);
|
const folder = await folderDAL.findBySecretPath(projectId, environmentSlug, path);
|
||||||
if (!folder)
|
if (!folder)
|
||||||
@@ -326,6 +355,15 @@ export const dynamicSecretLeaseServiceFactory = ({
|
|||||||
message: `Dynamic secret with name '${name}' in folder with path '${path}' not found`
|
message: `Dynamic secret with name '${name}' in folder with path '${path}' not found`
|
||||||
});
|
});
|
||||||
|
|
||||||
|
ForbiddenError.from(permission).throwUnlessCan(
|
||||||
|
ProjectPermissionDynamicSecretActions.Lease,
|
||||||
|
subject(ProjectPermissionSub.DynamicSecrets, {
|
||||||
|
environment: environmentSlug,
|
||||||
|
secretPath: path,
|
||||||
|
metadata: dynamicSecretCfg.metadata
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
const dynamicSecretLeases = await dynamicSecretLeaseDAL.find({ dynamicSecretId: dynamicSecretCfg.id });
|
const dynamicSecretLeases = await dynamicSecretLeaseDAL.find({ dynamicSecretId: dynamicSecretCfg.id });
|
||||||
return dynamicSecretLeases;
|
return dynamicSecretLeases;
|
||||||
};
|
};
|
||||||
@@ -352,10 +390,6 @@ export const dynamicSecretLeaseServiceFactory = ({
|
|||||||
actorOrgId,
|
actorOrgId,
|
||||||
actionProjectType: ActionProjectType.SecretManager
|
actionProjectType: ActionProjectType.SecretManager
|
||||||
});
|
});
|
||||||
ForbiddenError.from(permission).throwUnlessCan(
|
|
||||||
ProjectPermissionDynamicSecretActions.Lease,
|
|
||||||
subject(ProjectPermissionSub.DynamicSecrets, { environment: environmentSlug, secretPath: path })
|
|
||||||
);
|
|
||||||
|
|
||||||
const folder = await folderDAL.findBySecretPath(projectId, environmentSlug, path);
|
const folder = await folderDAL.findBySecretPath(projectId, environmentSlug, path);
|
||||||
if (!folder) throw new NotFoundError({ message: `Folder with path '${path}' not found` });
|
if (!folder) throw new NotFoundError({ message: `Folder with path '${path}' not found` });
|
||||||
@@ -364,6 +398,25 @@ export const dynamicSecretLeaseServiceFactory = ({
|
|||||||
if (!dynamicSecretLease)
|
if (!dynamicSecretLease)
|
||||||
throw new NotFoundError({ message: `Dynamic secret lease with ID '${leaseId}' not found` });
|
throw new NotFoundError({ message: `Dynamic secret lease with ID '${leaseId}' not found` });
|
||||||
|
|
||||||
|
const dynamicSecretCfg = await dynamicSecretDAL.findOne({
|
||||||
|
id: dynamicSecretLease.dynamicSecretId,
|
||||||
|
folderId: folder.id
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!dynamicSecretCfg)
|
||||||
|
throw new NotFoundError({
|
||||||
|
message: `Dynamic secret with ID '${dynamicSecretLease.dynamicSecretId}' not found`
|
||||||
|
});
|
||||||
|
|
||||||
|
ForbiddenError.from(permission).throwUnlessCan(
|
||||||
|
ProjectPermissionDynamicSecretActions.Lease,
|
||||||
|
subject(ProjectPermissionSub.DynamicSecrets, {
|
||||||
|
environment: environmentSlug,
|
||||||
|
secretPath: path,
|
||||||
|
metadata: dynamicSecretCfg.metadata
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
return dynamicSecretLease;
|
return dynamicSecretLease;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -1,9 +1,17 @@
|
|||||||
import { Knex } from "knex";
|
import { Knex } from "knex";
|
||||||
|
|
||||||
import { TDbClient } from "@app/db";
|
import { TDbClient } from "@app/db";
|
||||||
import { TableName } from "@app/db/schemas";
|
import { TableName, TDynamicSecrets } from "@app/db/schemas";
|
||||||
import { DatabaseError } from "@app/lib/errors";
|
import { DatabaseError } from "@app/lib/errors";
|
||||||
import { ormify, selectAllTableCols } from "@app/lib/knex";
|
import {
|
||||||
|
buildFindFilter,
|
||||||
|
ormify,
|
||||||
|
prependTableNameToFindFilter,
|
||||||
|
selectAllTableCols,
|
||||||
|
sqlNestRelationships,
|
||||||
|
TFindFilter,
|
||||||
|
TFindOpt
|
||||||
|
} from "@app/lib/knex";
|
||||||
import { OrderByDirection } from "@app/lib/types";
|
import { OrderByDirection } from "@app/lib/types";
|
||||||
import { SecretsOrderBy } from "@app/services/secret/secret-types";
|
import { SecretsOrderBy } from "@app/services/secret/secret-types";
|
||||||
|
|
||||||
@@ -12,6 +20,86 @@ export type TDynamicSecretDALFactory = ReturnType<typeof dynamicSecretDALFactory
|
|||||||
export const dynamicSecretDALFactory = (db: TDbClient) => {
|
export const dynamicSecretDALFactory = (db: TDbClient) => {
|
||||||
const orm = ormify(db, TableName.DynamicSecret);
|
const orm = ormify(db, TableName.DynamicSecret);
|
||||||
|
|
||||||
|
const findOne = async (filter: TFindFilter<TDynamicSecrets>, tx?: Knex) => {
|
||||||
|
const query = (tx || db.replicaNode())(TableName.DynamicSecret)
|
||||||
|
.leftJoin(
|
||||||
|
TableName.ResourceMetadata,
|
||||||
|
`${TableName.ResourceMetadata}.dynamicSecretId`,
|
||||||
|
`${TableName.DynamicSecret}.id`
|
||||||
|
)
|
||||||
|
.select(selectAllTableCols(TableName.DynamicSecret))
|
||||||
|
.select(
|
||||||
|
db.ref("id").withSchema(TableName.ResourceMetadata).as("metadataId"),
|
||||||
|
db.ref("key").withSchema(TableName.ResourceMetadata).as("metadataKey"),
|
||||||
|
db.ref("value").withSchema(TableName.ResourceMetadata).as("metadataValue")
|
||||||
|
)
|
||||||
|
.where(prependTableNameToFindFilter(TableName.DynamicSecret, filter));
|
||||||
|
|
||||||
|
const docs = sqlNestRelationships({
|
||||||
|
data: await query,
|
||||||
|
key: "id",
|
||||||
|
parentMapper: (el) => el,
|
||||||
|
childrenMapper: [
|
||||||
|
{
|
||||||
|
key: "metadataId",
|
||||||
|
label: "metadata" as const,
|
||||||
|
mapper: ({ metadataKey, metadataValue, metadataId }) => ({
|
||||||
|
id: metadataId,
|
||||||
|
key: metadataKey,
|
||||||
|
value: metadataValue
|
||||||
|
})
|
||||||
|
}
|
||||||
|
]
|
||||||
|
});
|
||||||
|
|
||||||
|
return docs[0];
|
||||||
|
};
|
||||||
|
|
||||||
|
const findWithMetadata = async (
|
||||||
|
filter: TFindFilter<TDynamicSecrets>,
|
||||||
|
{ offset, limit, sort, tx }: TFindOpt<TDynamicSecrets> = {}
|
||||||
|
) => {
|
||||||
|
const query = (tx || db.replicaNode())(TableName.DynamicSecret)
|
||||||
|
.leftJoin(
|
||||||
|
TableName.ResourceMetadata,
|
||||||
|
`${TableName.ResourceMetadata}.dynamicSecretId`,
|
||||||
|
`${TableName.DynamicSecret}.id`
|
||||||
|
)
|
||||||
|
.select(selectAllTableCols(TableName.DynamicSecret))
|
||||||
|
.select(
|
||||||
|
db.ref("id").withSchema(TableName.ResourceMetadata).as("metadataId"),
|
||||||
|
db.ref("key").withSchema(TableName.ResourceMetadata).as("metadataKey"),
|
||||||
|
db.ref("value").withSchema(TableName.ResourceMetadata).as("metadataValue")
|
||||||
|
)
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-misused-promises
|
||||||
|
.where(buildFindFilter(filter));
|
||||||
|
|
||||||
|
if (limit) void query.limit(limit);
|
||||||
|
if (offset) void query.offset(offset);
|
||||||
|
if (sort) {
|
||||||
|
void query.orderBy(sort.map(([column, order, nulls]) => ({ column: column as string, order, nulls })));
|
||||||
|
}
|
||||||
|
|
||||||
|
const docs = sqlNestRelationships({
|
||||||
|
data: await query,
|
||||||
|
key: "id",
|
||||||
|
parentMapper: (el) => el,
|
||||||
|
childrenMapper: [
|
||||||
|
{
|
||||||
|
key: "metadataId",
|
||||||
|
label: "metadata" as const,
|
||||||
|
mapper: ({ metadataKey, metadataValue, metadataId }) => ({
|
||||||
|
id: metadataId,
|
||||||
|
key: metadataKey,
|
||||||
|
value: metadataValue
|
||||||
|
})
|
||||||
|
}
|
||||||
|
]
|
||||||
|
});
|
||||||
|
|
||||||
|
return docs;
|
||||||
|
};
|
||||||
|
|
||||||
// find dynamic secrets for multiple environments (folder IDs are cross env, thus need to rank for pagination)
|
// find dynamic secrets for multiple environments (folder IDs are cross env, thus need to rank for pagination)
|
||||||
const listDynamicSecretsByFolderIds = async (
|
const listDynamicSecretsByFolderIds = async (
|
||||||
{
|
{
|
||||||
@@ -39,18 +127,27 @@ export const dynamicSecretDALFactory = (db: TDbClient) => {
|
|||||||
void bd.whereILike(`${TableName.DynamicSecret}.name`, `%${search}%`);
|
void bd.whereILike(`${TableName.DynamicSecret}.name`, `%${search}%`);
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
.leftJoin(
|
||||||
|
TableName.ResourceMetadata,
|
||||||
|
`${TableName.ResourceMetadata}.dynamicSecretId`,
|
||||||
|
`${TableName.DynamicSecret}.id`
|
||||||
|
)
|
||||||
.leftJoin(TableName.SecretFolder, `${TableName.SecretFolder}.id`, `${TableName.DynamicSecret}.folderId`)
|
.leftJoin(TableName.SecretFolder, `${TableName.SecretFolder}.id`, `${TableName.DynamicSecret}.folderId`)
|
||||||
.leftJoin(TableName.Environment, `${TableName.SecretFolder}.envId`, `${TableName.Environment}.id`)
|
.leftJoin(TableName.Environment, `${TableName.SecretFolder}.envId`, `${TableName.Environment}.id`)
|
||||||
.select(
|
.select(
|
||||||
selectAllTableCols(TableName.DynamicSecret),
|
selectAllTableCols(TableName.DynamicSecret),
|
||||||
db.ref("slug").withSchema(TableName.Environment).as("environment"),
|
db.ref("slug").withSchema(TableName.Environment).as("environment"),
|
||||||
db.raw(`DENSE_RANK() OVER (ORDER BY ${TableName.DynamicSecret}."name" ${orderDirection}) as rank`)
|
db.raw(`DENSE_RANK() OVER (ORDER BY ${TableName.DynamicSecret}."name" ${orderDirection}) as rank`),
|
||||||
|
db.ref("id").withSchema(TableName.ResourceMetadata).as("metadataId"),
|
||||||
|
db.ref("key").withSchema(TableName.ResourceMetadata).as("metadataKey"),
|
||||||
|
db.ref("value").withSchema(TableName.ResourceMetadata).as("metadataValue")
|
||||||
)
|
)
|
||||||
.orderBy(`${TableName.DynamicSecret}.${orderBy}`, orderDirection);
|
.orderBy(`${TableName.DynamicSecret}.${orderBy}`, orderDirection);
|
||||||
|
|
||||||
|
let queryWithLimit;
|
||||||
if (limit) {
|
if (limit) {
|
||||||
const rankOffset = offset + 1;
|
const rankOffset = offset + 1;
|
||||||
return await (tx || db)
|
queryWithLimit = (tx || db.replicaNode())
|
||||||
.with("w", query)
|
.with("w", query)
|
||||||
.select("*")
|
.select("*")
|
||||||
.from<Awaited<typeof query>[number]>("w")
|
.from<Awaited<typeof query>[number]>("w")
|
||||||
@@ -58,7 +155,22 @@ export const dynamicSecretDALFactory = (db: TDbClient) => {
|
|||||||
.andWhere("w.rank", "<", rankOffset + limit);
|
.andWhere("w.rank", "<", rankOffset + limit);
|
||||||
}
|
}
|
||||||
|
|
||||||
const dynamicSecrets = await query;
|
const dynamicSecrets = sqlNestRelationships({
|
||||||
|
data: await (queryWithLimit || query),
|
||||||
|
key: "id",
|
||||||
|
parentMapper: (el) => el,
|
||||||
|
childrenMapper: [
|
||||||
|
{
|
||||||
|
key: "metadataId",
|
||||||
|
label: "metadata" as const,
|
||||||
|
mapper: ({ metadataKey, metadataValue, metadataId }) => ({
|
||||||
|
id: metadataId,
|
||||||
|
key: metadataKey,
|
||||||
|
value: metadataValue
|
||||||
|
})
|
||||||
|
}
|
||||||
|
]
|
||||||
|
});
|
||||||
|
|
||||||
return dynamicSecrets;
|
return dynamicSecrets;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -66,5 +178,5 @@ export const dynamicSecretDALFactory = (db: TDbClient) => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
return { ...orm, listDynamicSecretsByFolderIds };
|
return { ...orm, listDynamicSecretsByFolderIds, findOne, findWithMetadata };
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -42,7 +42,7 @@ export const verifyHostInputValidity = async (host: string, isGateway = false) =
|
|||||||
inputHostIps.push(...resolvedIps);
|
inputHostIps.push(...resolvedIps);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!isGateway && !appCfg.DYNAMIC_SECRET_ALLOW_INTERNAL_IP) {
|
if (!isGateway && !(appCfg.DYNAMIC_SECRET_ALLOW_INTERNAL_IP || appCfg.ALLOW_INTERNAL_IP_CONNECTIONS)) {
|
||||||
const isInternalIp = inputHostIps.some((el) => isPrivateIp(el));
|
const isInternalIp = inputHostIps.some((el) => isPrivateIp(el));
|
||||||
if (isInternalIp) throw new BadRequestError({ message: "Invalid db host" });
|
if (isInternalIp) throw new BadRequestError({ message: "Invalid db host" });
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ import { OrderByDirection, OrgServiceActor } from "@app/lib/types";
|
|||||||
import { TKmsServiceFactory } from "@app/services/kms/kms-service";
|
import { TKmsServiceFactory } from "@app/services/kms/kms-service";
|
||||||
import { KmsDataKey } from "@app/services/kms/kms-types";
|
import { KmsDataKey } from "@app/services/kms/kms-types";
|
||||||
import { TProjectDALFactory } from "@app/services/project/project-dal";
|
import { TProjectDALFactory } from "@app/services/project/project-dal";
|
||||||
|
import { TResourceMetadataDALFactory } from "@app/services/resource-metadata/resource-metadata-dal";
|
||||||
import { TSecretFolderDALFactory } from "@app/services/secret-folder/secret-folder-dal";
|
import { TSecretFolderDALFactory } from "@app/services/secret-folder/secret-folder-dal";
|
||||||
|
|
||||||
import { TDynamicSecretLeaseDALFactory } from "../dynamic-secret-lease/dynamic-secret-lease-dal";
|
import { TDynamicSecretLeaseDALFactory } from "../dynamic-secret-lease/dynamic-secret-lease-dal";
|
||||||
@@ -46,6 +47,7 @@ type TDynamicSecretServiceFactoryDep = {
|
|||||||
permissionService: Pick<TPermissionServiceFactory, "getProjectPermission">;
|
permissionService: Pick<TPermissionServiceFactory, "getProjectPermission">;
|
||||||
kmsService: Pick<TKmsServiceFactory, "createCipherPairWithDataKey">;
|
kmsService: Pick<TKmsServiceFactory, "createCipherPairWithDataKey">;
|
||||||
projectGatewayDAL: Pick<TProjectGatewayDALFactory, "findOne">;
|
projectGatewayDAL: Pick<TProjectGatewayDALFactory, "findOne">;
|
||||||
|
resourceMetadataDAL: Pick<TResourceMetadataDALFactory, "insertMany" | "delete">;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type TDynamicSecretServiceFactory = ReturnType<typeof dynamicSecretServiceFactory>;
|
export type TDynamicSecretServiceFactory = ReturnType<typeof dynamicSecretServiceFactory>;
|
||||||
@@ -60,7 +62,8 @@ export const dynamicSecretServiceFactory = ({
|
|||||||
dynamicSecretQueueService,
|
dynamicSecretQueueService,
|
||||||
projectDAL,
|
projectDAL,
|
||||||
kmsService,
|
kmsService,
|
||||||
projectGatewayDAL
|
projectGatewayDAL,
|
||||||
|
resourceMetadataDAL
|
||||||
}: TDynamicSecretServiceFactoryDep) => {
|
}: TDynamicSecretServiceFactoryDep) => {
|
||||||
const create = async ({
|
const create = async ({
|
||||||
path,
|
path,
|
||||||
@@ -73,7 +76,8 @@ export const dynamicSecretServiceFactory = ({
|
|||||||
projectSlug,
|
projectSlug,
|
||||||
actorOrgId,
|
actorOrgId,
|
||||||
defaultTTL,
|
defaultTTL,
|
||||||
actorAuthMethod
|
actorAuthMethod,
|
||||||
|
metadata
|
||||||
}: TCreateDynamicSecretDTO) => {
|
}: TCreateDynamicSecretDTO) => {
|
||||||
const project = await projectDAL.findProjectBySlug(projectSlug, actorOrgId);
|
const project = await projectDAL.findProjectBySlug(projectSlug, actorOrgId);
|
||||||
if (!project) throw new NotFoundError({ message: `Project with slug '${projectSlug}' not found` });
|
if (!project) throw new NotFoundError({ message: `Project with slug '${projectSlug}' not found` });
|
||||||
@@ -87,9 +91,10 @@ export const dynamicSecretServiceFactory = ({
|
|||||||
actorOrgId,
|
actorOrgId,
|
||||||
actionProjectType: ActionProjectType.SecretManager
|
actionProjectType: ActionProjectType.SecretManager
|
||||||
});
|
});
|
||||||
|
|
||||||
ForbiddenError.from(permission).throwUnlessCan(
|
ForbiddenError.from(permission).throwUnlessCan(
|
||||||
ProjectPermissionDynamicSecretActions.CreateRootCredential,
|
ProjectPermissionDynamicSecretActions.CreateRootCredential,
|
||||||
subject(ProjectPermissionSub.DynamicSecrets, { environment: environmentSlug, secretPath: path })
|
subject(ProjectPermissionSub.DynamicSecrets, { environment: environmentSlug, secretPath: path, metadata })
|
||||||
);
|
);
|
||||||
|
|
||||||
const plan = await licenseService.getPlan(actorOrgId);
|
const plan = await licenseService.getPlan(actorOrgId);
|
||||||
@@ -131,16 +136,36 @@ export const dynamicSecretServiceFactory = ({
|
|||||||
projectId
|
projectId
|
||||||
});
|
});
|
||||||
|
|
||||||
const dynamicSecretCfg = await dynamicSecretDAL.create({
|
const dynamicSecretCfg = await dynamicSecretDAL.transaction(async (tx) => {
|
||||||
type: provider.type,
|
const cfg = await dynamicSecretDAL.create(
|
||||||
version: 1,
|
{
|
||||||
encryptedInput: secretManagerEncryptor({ plainText: Buffer.from(JSON.stringify(inputs)) }).cipherTextBlob,
|
type: provider.type,
|
||||||
maxTTL,
|
version: 1,
|
||||||
defaultTTL,
|
encryptedInput: secretManagerEncryptor({ plainText: Buffer.from(JSON.stringify(inputs)) }).cipherTextBlob,
|
||||||
folderId: folder.id,
|
maxTTL,
|
||||||
name,
|
defaultTTL,
|
||||||
projectGatewayId: selectedGatewayId
|
folderId: folder.id,
|
||||||
|
name,
|
||||||
|
projectGatewayId: selectedGatewayId
|
||||||
|
},
|
||||||
|
tx
|
||||||
|
);
|
||||||
|
|
||||||
|
if (metadata) {
|
||||||
|
await resourceMetadataDAL.insertMany(
|
||||||
|
metadata.map(({ key, value }) => ({
|
||||||
|
key,
|
||||||
|
value,
|
||||||
|
dynamicSecretId: cfg.id,
|
||||||
|
orgId: actorOrgId
|
||||||
|
})),
|
||||||
|
tx
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return cfg;
|
||||||
});
|
});
|
||||||
|
|
||||||
return dynamicSecretCfg;
|
return dynamicSecretCfg;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -156,7 +181,8 @@ export const dynamicSecretServiceFactory = ({
|
|||||||
actorId,
|
actorId,
|
||||||
newName,
|
newName,
|
||||||
actorOrgId,
|
actorOrgId,
|
||||||
actorAuthMethod
|
actorAuthMethod,
|
||||||
|
metadata
|
||||||
}: TUpdateDynamicSecretDTO) => {
|
}: TUpdateDynamicSecretDTO) => {
|
||||||
const project = await projectDAL.findProjectBySlug(projectSlug, actorOrgId);
|
const project = await projectDAL.findProjectBySlug(projectSlug, actorOrgId);
|
||||||
if (!project) throw new NotFoundError({ message: `Project with slug '${projectSlug}' not found` });
|
if (!project) throw new NotFoundError({ message: `Project with slug '${projectSlug}' not found` });
|
||||||
@@ -171,10 +197,6 @@ export const dynamicSecretServiceFactory = ({
|
|||||||
actorOrgId,
|
actorOrgId,
|
||||||
actionProjectType: ActionProjectType.SecretManager
|
actionProjectType: ActionProjectType.SecretManager
|
||||||
});
|
});
|
||||||
ForbiddenError.from(permission).throwUnlessCan(
|
|
||||||
ProjectPermissionDynamicSecretActions.EditRootCredential,
|
|
||||||
subject(ProjectPermissionSub.DynamicSecrets, { environment: environmentSlug, secretPath: path })
|
|
||||||
);
|
|
||||||
|
|
||||||
const plan = await licenseService.getPlan(actorOrgId);
|
const plan = await licenseService.getPlan(actorOrgId);
|
||||||
if (!plan?.dynamicSecret) {
|
if (!plan?.dynamicSecret) {
|
||||||
@@ -193,6 +215,27 @@ export const dynamicSecretServiceFactory = ({
|
|||||||
message: `Dynamic secret with name '${name}' in folder '${folder.path}' not found`
|
message: `Dynamic secret with name '${name}' in folder '${folder.path}' not found`
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
ForbiddenError.from(permission).throwUnlessCan(
|
||||||
|
ProjectPermissionDynamicSecretActions.EditRootCredential,
|
||||||
|
subject(ProjectPermissionSub.DynamicSecrets, {
|
||||||
|
environment: environmentSlug,
|
||||||
|
secretPath: path,
|
||||||
|
metadata: dynamicSecretCfg.metadata
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
if (metadata) {
|
||||||
|
ForbiddenError.from(permission).throwUnlessCan(
|
||||||
|
ProjectPermissionDynamicSecretActions.EditRootCredential,
|
||||||
|
subject(ProjectPermissionSub.DynamicSecrets, {
|
||||||
|
environment: environmentSlug,
|
||||||
|
secretPath: path,
|
||||||
|
metadata
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
if (newName) {
|
if (newName) {
|
||||||
const existingDynamicSecret = await dynamicSecretDAL.findOne({ name: newName, folderId: folder.id });
|
const existingDynamicSecret = await dynamicSecretDAL.findOne({ name: newName, folderId: folder.id });
|
||||||
if (existingDynamicSecret)
|
if (existingDynamicSecret)
|
||||||
@@ -231,14 +274,41 @@ export const dynamicSecretServiceFactory = ({
|
|||||||
const isConnected = await selectedProvider.validateConnection(newInput);
|
const isConnected = await selectedProvider.validateConnection(newInput);
|
||||||
if (!isConnected) throw new BadRequestError({ message: "Provider connection failed" });
|
if (!isConnected) throw new BadRequestError({ message: "Provider connection failed" });
|
||||||
|
|
||||||
const updatedDynamicCfg = await dynamicSecretDAL.updateById(dynamicSecretCfg.id, {
|
const updatedDynamicCfg = await dynamicSecretDAL.transaction(async (tx) => {
|
||||||
encryptedInput: secretManagerEncryptor({ plainText: Buffer.from(JSON.stringify(updatedInput)) }).cipherTextBlob,
|
const cfg = await dynamicSecretDAL.updateById(
|
||||||
maxTTL,
|
dynamicSecretCfg.id,
|
||||||
defaultTTL,
|
{
|
||||||
name: newName ?? name,
|
encryptedInput: secretManagerEncryptor({ plainText: Buffer.from(JSON.stringify(updatedInput)) })
|
||||||
status: null,
|
.cipherTextBlob,
|
||||||
statusDetails: null,
|
maxTTL,
|
||||||
projectGatewayId: selectedGatewayId
|
defaultTTL,
|
||||||
|
name: newName ?? name,
|
||||||
|
status: null,
|
||||||
|
projectGatewayId: selectedGatewayId
|
||||||
|
},
|
||||||
|
tx
|
||||||
|
);
|
||||||
|
|
||||||
|
if (metadata) {
|
||||||
|
await resourceMetadataDAL.delete(
|
||||||
|
{
|
||||||
|
dynamicSecretId: cfg.id
|
||||||
|
},
|
||||||
|
tx
|
||||||
|
);
|
||||||
|
|
||||||
|
await resourceMetadataDAL.insertMany(
|
||||||
|
metadata.map(({ key, value }) => ({
|
||||||
|
key,
|
||||||
|
value,
|
||||||
|
dynamicSecretId: cfg.id,
|
||||||
|
orgId: actorOrgId
|
||||||
|
})),
|
||||||
|
tx
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return cfg;
|
||||||
});
|
});
|
||||||
|
|
||||||
return updatedDynamicCfg;
|
return updatedDynamicCfg;
|
||||||
@@ -268,10 +338,6 @@ export const dynamicSecretServiceFactory = ({
|
|||||||
actorOrgId,
|
actorOrgId,
|
||||||
actionProjectType: ActionProjectType.SecretManager
|
actionProjectType: ActionProjectType.SecretManager
|
||||||
});
|
});
|
||||||
ForbiddenError.from(permission).throwUnlessCan(
|
|
||||||
ProjectPermissionDynamicSecretActions.DeleteRootCredential,
|
|
||||||
subject(ProjectPermissionSub.DynamicSecrets, { environment: environmentSlug, secretPath: path })
|
|
||||||
);
|
|
||||||
|
|
||||||
const folder = await folderDAL.findBySecretPath(projectId, environmentSlug, path);
|
const folder = await folderDAL.findBySecretPath(projectId, environmentSlug, path);
|
||||||
if (!folder)
|
if (!folder)
|
||||||
@@ -282,6 +348,15 @@ export const dynamicSecretServiceFactory = ({
|
|||||||
throw new NotFoundError({ message: `Dynamic secret with name '${name}' in folder '${folder.path}' not found` });
|
throw new NotFoundError({ message: `Dynamic secret with name '${name}' in folder '${folder.path}' not found` });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
ForbiddenError.from(permission).throwUnlessCan(
|
||||||
|
ProjectPermissionDynamicSecretActions.DeleteRootCredential,
|
||||||
|
subject(ProjectPermissionSub.DynamicSecrets, {
|
||||||
|
environment: environmentSlug,
|
||||||
|
secretPath: path,
|
||||||
|
metadata: dynamicSecretCfg.metadata
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
const leases = await dynamicSecretLeaseDAL.find({ dynamicSecretId: dynamicSecretCfg.id });
|
const leases = await dynamicSecretLeaseDAL.find({ dynamicSecretId: dynamicSecretCfg.id });
|
||||||
// when not forced we check with the external system to first remove the things
|
// when not forced we check with the external system to first remove the things
|
||||||
// we introduce a forced concept because consider the external lease got deleted by some other external like a human or another system
|
// we introduce a forced concept because consider the external lease got deleted by some other external like a human or another system
|
||||||
@@ -329,14 +404,6 @@ export const dynamicSecretServiceFactory = ({
|
|||||||
actorOrgId,
|
actorOrgId,
|
||||||
actionProjectType: ActionProjectType.SecretManager
|
actionProjectType: ActionProjectType.SecretManager
|
||||||
});
|
});
|
||||||
ForbiddenError.from(permission).throwUnlessCan(
|
|
||||||
ProjectPermissionDynamicSecretActions.ReadRootCredential,
|
|
||||||
subject(ProjectPermissionSub.DynamicSecrets, { environment: environmentSlug, secretPath: path })
|
|
||||||
);
|
|
||||||
ForbiddenError.from(permission).throwUnlessCan(
|
|
||||||
ProjectPermissionDynamicSecretActions.EditRootCredential,
|
|
||||||
subject(ProjectPermissionSub.DynamicSecrets, { environment: environmentSlug, secretPath: path })
|
|
||||||
);
|
|
||||||
|
|
||||||
const folder = await folderDAL.findBySecretPath(projectId, environmentSlug, path);
|
const folder = await folderDAL.findBySecretPath(projectId, environmentSlug, path);
|
||||||
if (!folder)
|
if (!folder)
|
||||||
@@ -346,6 +413,25 @@ export const dynamicSecretServiceFactory = ({
|
|||||||
if (!dynamicSecretCfg) {
|
if (!dynamicSecretCfg) {
|
||||||
throw new NotFoundError({ message: `Dynamic secret with name '${name} in folder '${path}' not found` });
|
throw new NotFoundError({ message: `Dynamic secret with name '${name} in folder '${path}' not found` });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
ForbiddenError.from(permission).throwUnlessCan(
|
||||||
|
ProjectPermissionDynamicSecretActions.ReadRootCredential,
|
||||||
|
subject(ProjectPermissionSub.DynamicSecrets, {
|
||||||
|
environment: environmentSlug,
|
||||||
|
secretPath: path,
|
||||||
|
metadata: dynamicSecretCfg.metadata
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
ForbiddenError.from(permission).throwUnlessCan(
|
||||||
|
ProjectPermissionDynamicSecretActions.EditRootCredential,
|
||||||
|
subject(ProjectPermissionSub.DynamicSecrets, {
|
||||||
|
environment: environmentSlug,
|
||||||
|
secretPath: path,
|
||||||
|
metadata: dynamicSecretCfg.metadata
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
const { decryptor: secretManagerDecryptor } = await kmsService.createCipherPairWithDataKey({
|
const { decryptor: secretManagerDecryptor } = await kmsService.createCipherPairWithDataKey({
|
||||||
type: KmsDataKey.SecretManager,
|
type: KmsDataKey.SecretManager,
|
||||||
projectId
|
projectId
|
||||||
@@ -356,6 +442,7 @@ export const dynamicSecretServiceFactory = ({
|
|||||||
) as object;
|
) as object;
|
||||||
const selectedProvider = dynamicSecretProviders[dynamicSecretCfg.type as DynamicSecretProviders];
|
const selectedProvider = dynamicSecretProviders[dynamicSecretCfg.type as DynamicSecretProviders];
|
||||||
const providerInputs = (await selectedProvider.validateProviderInputs(decryptedStoredInput)) as object;
|
const providerInputs = (await selectedProvider.validateProviderInputs(decryptedStoredInput)) as object;
|
||||||
|
|
||||||
return { ...dynamicSecretCfg, inputs: providerInputs };
|
return { ...dynamicSecretCfg, inputs: providerInputs };
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -426,7 +513,7 @@ export const dynamicSecretServiceFactory = ({
|
|||||||
});
|
});
|
||||||
ForbiddenError.from(permission).throwUnlessCan(
|
ForbiddenError.from(permission).throwUnlessCan(
|
||||||
ProjectPermissionDynamicSecretActions.ReadRootCredential,
|
ProjectPermissionDynamicSecretActions.ReadRootCredential,
|
||||||
subject(ProjectPermissionSub.DynamicSecrets, { environment: environmentSlug, secretPath: path })
|
ProjectPermissionSub.DynamicSecrets
|
||||||
);
|
);
|
||||||
|
|
||||||
const folder = await folderDAL.findBySecretPath(projectId, environmentSlug, path);
|
const folder = await folderDAL.findBySecretPath(projectId, environmentSlug, path);
|
||||||
@@ -473,16 +560,12 @@ export const dynamicSecretServiceFactory = ({
|
|||||||
actorOrgId,
|
actorOrgId,
|
||||||
actionProjectType: ActionProjectType.SecretManager
|
actionProjectType: ActionProjectType.SecretManager
|
||||||
});
|
});
|
||||||
ForbiddenError.from(permission).throwUnlessCan(
|
|
||||||
ProjectPermissionDynamicSecretActions.ReadRootCredential,
|
|
||||||
subject(ProjectPermissionSub.DynamicSecrets, { environment: environmentSlug, secretPath: path })
|
|
||||||
);
|
|
||||||
|
|
||||||
const folder = await folderDAL.findBySecretPath(projectId, environmentSlug, path);
|
const folder = await folderDAL.findBySecretPath(projectId, environmentSlug, path);
|
||||||
if (!folder)
|
if (!folder)
|
||||||
throw new NotFoundError({ message: `Folder with path '${path}' in environment '${environmentSlug}' not found` });
|
throw new NotFoundError({ message: `Folder with path '${path}' in environment '${environmentSlug}' not found` });
|
||||||
|
|
||||||
const dynamicSecretCfg = await dynamicSecretDAL.find(
|
const dynamicSecretCfg = await dynamicSecretDAL.findWithMetadata(
|
||||||
{ folderId: folder.id, $search: search ? { name: `%${search}%` } : undefined },
|
{ folderId: folder.id, $search: search ? { name: `%${search}%` } : undefined },
|
||||||
{
|
{
|
||||||
limit,
|
limit,
|
||||||
@@ -490,7 +573,17 @@ export const dynamicSecretServiceFactory = ({
|
|||||||
sort: orderBy ? [[orderBy, orderDirection]] : undefined
|
sort: orderBy ? [[orderBy, orderDirection]] : undefined
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
return dynamicSecretCfg;
|
|
||||||
|
return dynamicSecretCfg.filter((dynamicSecret) => {
|
||||||
|
return permission.can(
|
||||||
|
ProjectPermissionDynamicSecretActions.ReadRootCredential,
|
||||||
|
subject(ProjectPermissionSub.DynamicSecrets, {
|
||||||
|
environment: environmentSlug,
|
||||||
|
secretPath: path,
|
||||||
|
metadata: dynamicSecret.metadata
|
||||||
|
})
|
||||||
|
);
|
||||||
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const listDynamicSecretsByFolderIds = async (
|
const listDynamicSecretsByFolderIds = async (
|
||||||
@@ -542,24 +635,14 @@ export const dynamicSecretServiceFactory = ({
|
|||||||
isInternal,
|
isInternal,
|
||||||
...params
|
...params
|
||||||
}: TListDynamicSecretsMultiEnvDTO) => {
|
}: TListDynamicSecretsMultiEnvDTO) => {
|
||||||
if (!isInternal) {
|
const { permission } = await permissionService.getProjectPermission({
|
||||||
const { permission } = await permissionService.getProjectPermission({
|
actor,
|
||||||
actor,
|
actorId,
|
||||||
actorId,
|
projectId,
|
||||||
projectId,
|
actorAuthMethod,
|
||||||
actorAuthMethod,
|
actorOrgId,
|
||||||
actorOrgId,
|
actionProjectType: ActionProjectType.SecretManager
|
||||||
actionProjectType: ActionProjectType.SecretManager
|
});
|
||||||
});
|
|
||||||
|
|
||||||
// verify user has access to each env in request
|
|
||||||
environmentSlugs.forEach((environmentSlug) =>
|
|
||||||
ForbiddenError.from(permission).throwUnlessCan(
|
|
||||||
ProjectPermissionDynamicSecretActions.ReadRootCredential,
|
|
||||||
subject(ProjectPermissionSub.DynamicSecrets, { environment: environmentSlug, secretPath: path })
|
|
||||||
)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const folders = await folderDAL.findBySecretPathMultiEnv(projectId, environmentSlugs, path);
|
const folders = await folderDAL.findBySecretPathMultiEnv(projectId, environmentSlugs, path);
|
||||||
if (!folders.length)
|
if (!folders.length)
|
||||||
@@ -572,7 +655,16 @@ export const dynamicSecretServiceFactory = ({
|
|||||||
...params
|
...params
|
||||||
});
|
});
|
||||||
|
|
||||||
return dynamicSecretCfg;
|
return dynamicSecretCfg.filter((dynamicSecret) => {
|
||||||
|
return permission.can(
|
||||||
|
ProjectPermissionDynamicSecretActions.ReadRootCredential,
|
||||||
|
subject(ProjectPermissionSub.DynamicSecrets, {
|
||||||
|
environment: dynamicSecret.environment,
|
||||||
|
secretPath: path,
|
||||||
|
metadata: dynamicSecret.metadata
|
||||||
|
})
|
||||||
|
);
|
||||||
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const fetchAzureEntraIdUsers = async ({
|
const fetchAzureEntraIdUsers = async ({
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
|
|
||||||
import { OrderByDirection, TProjectPermission } from "@app/lib/types";
|
import { OrderByDirection, TProjectPermission } from "@app/lib/types";
|
||||||
|
import { ResourceMetadataDTO } from "@app/services/resource-metadata/resource-metadata-schema";
|
||||||
import { SecretsOrderBy } from "@app/services/secret/secret-types";
|
import { SecretsOrderBy } from "@app/services/secret/secret-types";
|
||||||
|
|
||||||
import { DynamicSecretProviderSchema } from "./providers/models";
|
import { DynamicSecretProviderSchema } from "./providers/models";
|
||||||
@@ -20,6 +21,7 @@ export type TCreateDynamicSecretDTO = {
|
|||||||
environmentSlug: string;
|
environmentSlug: string;
|
||||||
name: string;
|
name: string;
|
||||||
projectSlug: string;
|
projectSlug: string;
|
||||||
|
metadata?: ResourceMetadataDTO;
|
||||||
} & Omit<TProjectPermission, "projectId">;
|
} & Omit<TProjectPermission, "projectId">;
|
||||||
|
|
||||||
export type TUpdateDynamicSecretDTO = {
|
export type TUpdateDynamicSecretDTO = {
|
||||||
@@ -31,6 +33,7 @@ export type TUpdateDynamicSecretDTO = {
|
|||||||
environmentSlug: string;
|
environmentSlug: string;
|
||||||
inputs?: TProvider["inputs"];
|
inputs?: TProvider["inputs"];
|
||||||
projectSlug: string;
|
projectSlug: string;
|
||||||
|
metadata?: ResourceMetadataDTO;
|
||||||
} & Omit<TProjectPermission, "projectId">;
|
} & Omit<TProjectPermission, "projectId">;
|
||||||
|
|
||||||
export type TDeleteDynamicSecretDTO = {
|
export type TDeleteDynamicSecretDTO = {
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ import { BadRequestError, InternalServerError, NotFoundError } from "@app/lib/er
|
|||||||
import { alphaNumericNanoId } from "@app/lib/nanoid";
|
import { alphaNumericNanoId } from "@app/lib/nanoid";
|
||||||
import { TKmsKeyDALFactory } from "@app/services/kms/kms-key-dal";
|
import { TKmsKeyDALFactory } from "@app/services/kms/kms-key-dal";
|
||||||
import { TKmsServiceFactory } from "@app/services/kms/kms-service";
|
import { TKmsServiceFactory } from "@app/services/kms/kms-service";
|
||||||
import { KmsDataKey } from "@app/services/kms/kms-types";
|
import { KmsDataKey, KmsKeyUsage } from "@app/services/kms/kms-types";
|
||||||
|
|
||||||
import { TLicenseServiceFactory } from "../license/license-service";
|
import { TLicenseServiceFactory } from "../license/license-service";
|
||||||
import { OrgPermissionActions, OrgPermissionSubjects } from "../permission/org-permission";
|
import { OrgPermissionActions, OrgPermissionSubjects } from "../permission/org-permission";
|
||||||
@@ -115,6 +115,7 @@ export const externalKmsServiceFactory = ({
|
|||||||
{
|
{
|
||||||
isReserved: false,
|
isReserved: false,
|
||||||
description,
|
description,
|
||||||
|
keyUsage: KmsKeyUsage.ENCRYPT_DECRYPT,
|
||||||
name: kmsName,
|
name: kmsName,
|
||||||
orgId: actorOrgId
|
orgId: actorOrgId
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -92,7 +92,7 @@ export const GcpKmsProviderFactory = async ({ inputs }: GcpKmsProviderArgs): Pro
|
|||||||
plaintext: data
|
plaintext: data
|
||||||
});
|
});
|
||||||
if (!encryptedText[0].ciphertext) throw new Error("encryption failed");
|
if (!encryptedText[0].ciphertext) throw new Error("encryption failed");
|
||||||
return { encryptedBlob: Buffer.from(encryptedText[0].ciphertext) };
|
return { encryptedBlob: Buffer.from(encryptedText[0].ciphertext as Uint8Array) };
|
||||||
};
|
};
|
||||||
|
|
||||||
const decrypt = async (encryptedBlob: Buffer) => {
|
const decrypt = async (encryptedBlob: Buffer) => {
|
||||||
@@ -101,7 +101,7 @@ export const GcpKmsProviderFactory = async ({ inputs }: GcpKmsProviderArgs): Pro
|
|||||||
ciphertext: encryptedBlob
|
ciphertext: encryptedBlob
|
||||||
});
|
});
|
||||||
if (!decryptedText[0].plaintext) throw new Error("decryption failed");
|
if (!decryptedText[0].plaintext) throw new Error("decryption failed");
|
||||||
return { data: Buffer.from(decryptedText[0].plaintext) };
|
return { data: Buffer.from(decryptedText[0].plaintext as Uint8Array) };
|
||||||
};
|
};
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
|||||||
@@ -258,7 +258,7 @@ export const hsmServiceFactory = ({ hsmModule: { isInitialized, pkcs11 }, envCon
|
|||||||
const decrypt: {
|
const decrypt: {
|
||||||
(encryptedBlob: Buffer, providedSession: pkcs11js.Handle): Promise<Buffer>;
|
(encryptedBlob: Buffer, providedSession: pkcs11js.Handle): Promise<Buffer>;
|
||||||
(encryptedBlob: Buffer): Promise<Buffer>;
|
(encryptedBlob: Buffer): Promise<Buffer>;
|
||||||
} = async (encryptedBlob: Buffer, providedSession?: pkcs11js.Handle) => {
|
} = async (encryptedBlob: Buffer, providedSession?: pkcs11js.Handle): Promise<Buffer> => {
|
||||||
if (!pkcs11 || !isInitialized) {
|
if (!pkcs11 || !isInitialized) {
|
||||||
throw new Error("PKCS#11 module is not initialized");
|
throw new Error("PKCS#11 module is not initialized");
|
||||||
}
|
}
|
||||||
@@ -309,10 +309,10 @@ export const hsmServiceFactory = ({ hsmModule: { isInitialized, pkcs11 }, envCon
|
|||||||
|
|
||||||
pkcs11.C_DecryptInit(sessionHandle, decryptMechanism, aesKey);
|
pkcs11.C_DecryptInit(sessionHandle, decryptMechanism, aesKey);
|
||||||
|
|
||||||
const tempBuffer = Buffer.alloc(encryptedData.length);
|
const tempBuffer: Buffer = Buffer.alloc(encryptedData.length);
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
|
||||||
const decryptedData = pkcs11.C_Decrypt(sessionHandle, encryptedData, tempBuffer);
|
const decryptedData = pkcs11.C_Decrypt(sessionHandle, encryptedData, tempBuffer);
|
||||||
|
|
||||||
// Create a new buffer from the decrypted data
|
|
||||||
return Buffer.from(decryptedData);
|
return Buffer.from(decryptedData);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error(error, "HSM: Failed to perform decryption");
|
logger.error(error, "HSM: Failed to perform decryption");
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import { ForbiddenError } from "@casl/ability";
|
|||||||
import { BadRequestError, ForbiddenRequestError, NotFoundError } from "@app/lib/errors";
|
import { BadRequestError, ForbiddenRequestError, NotFoundError } from "@app/lib/errors";
|
||||||
import { TKmsKeyDALFactory } from "@app/services/kms/kms-key-dal";
|
import { TKmsKeyDALFactory } from "@app/services/kms/kms-key-dal";
|
||||||
import { TKmsServiceFactory } from "@app/services/kms/kms-service";
|
import { TKmsServiceFactory } from "@app/services/kms/kms-service";
|
||||||
|
import { KmsKeyUsage } from "@app/services/kms/kms-types";
|
||||||
import { TProjectDALFactory } from "@app/services/project/project-dal";
|
import { TProjectDALFactory } from "@app/services/project/project-dal";
|
||||||
|
|
||||||
import { OrgPermissionKmipActions, OrgPermissionSubjects } from "../permission/org-permission";
|
import { OrgPermissionKmipActions, OrgPermissionSubjects } from "../permission/org-permission";
|
||||||
@@ -403,6 +404,7 @@ export const kmipOperationServiceFactory = ({
|
|||||||
algorithm,
|
algorithm,
|
||||||
isReserved: false,
|
isReserved: false,
|
||||||
projectId,
|
projectId,
|
||||||
|
keyUsage: KmsKeyUsage.ENCRYPT_DECRYPT,
|
||||||
orgId: project.orgId
|
orgId: project.orgId
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { SymmetricEncryption } from "@app/lib/crypto/cipher";
|
import { SymmetricKeyAlgorithm } from "@app/lib/crypto/cipher";
|
||||||
import { OrderByDirection, TOrgPermission, TProjectPermission } from "@app/lib/types";
|
import { OrderByDirection, TOrgPermission, TProjectPermission } from "@app/lib/types";
|
||||||
import { CertKeyAlgorithm } from "@app/services/certificate/certificate-types";
|
import { CertKeyAlgorithm } from "@app/services/certificate/certificate-types";
|
||||||
|
|
||||||
@@ -49,7 +49,7 @@ type KmipOperationBaseDTO = {
|
|||||||
} & Omit<TOrgPermission, "orgId">;
|
} & Omit<TOrgPermission, "orgId">;
|
||||||
|
|
||||||
export type TKmipCreateDTO = {
|
export type TKmipCreateDTO = {
|
||||||
algorithm: SymmetricEncryption;
|
algorithm: SymmetricKeyAlgorithm;
|
||||||
} & KmipOperationBaseDTO;
|
} & KmipOperationBaseDTO;
|
||||||
|
|
||||||
export type TKmipGetDTO = {
|
export type TKmipGetDTO = {
|
||||||
@@ -77,7 +77,7 @@ export type TKmipLocateDTO = KmipOperationBaseDTO;
|
|||||||
export type TKmipRegisterDTO = {
|
export type TKmipRegisterDTO = {
|
||||||
name: string;
|
name: string;
|
||||||
key: string;
|
key: string;
|
||||||
algorithm: SymmetricEncryption;
|
algorithm: SymmetricKeyAlgorithm;
|
||||||
} & KmipOperationBaseDTO;
|
} & KmipOperationBaseDTO;
|
||||||
|
|
||||||
export type TSetupOrgKmipDTO = {
|
export type TSetupOrgKmipDTO = {
|
||||||
|
|||||||
@@ -32,7 +32,9 @@ export enum ProjectPermissionCmekActions {
|
|||||||
Edit = "edit",
|
Edit = "edit",
|
||||||
Delete = "delete",
|
Delete = "delete",
|
||||||
Encrypt = "encrypt",
|
Encrypt = "encrypt",
|
||||||
Decrypt = "decrypt"
|
Decrypt = "decrypt",
|
||||||
|
Sign = "sign",
|
||||||
|
Verify = "verify"
|
||||||
}
|
}
|
||||||
|
|
||||||
export enum ProjectPermissionDynamicSecretActions {
|
export enum ProjectPermissionDynamicSecretActions {
|
||||||
@@ -153,6 +155,10 @@ export type SecretFolderSubjectFields = {
|
|||||||
export type DynamicSecretSubjectFields = {
|
export type DynamicSecretSubjectFields = {
|
||||||
environment: string;
|
environment: string;
|
||||||
secretPath: string;
|
secretPath: string;
|
||||||
|
metadata?: {
|
||||||
|
key: string;
|
||||||
|
value: string;
|
||||||
|
}[];
|
||||||
};
|
};
|
||||||
|
|
||||||
export type SecretImportSubjectFields = {
|
export type SecretImportSubjectFields = {
|
||||||
@@ -282,6 +288,42 @@ const SecretConditionV1Schema = z
|
|||||||
})
|
})
|
||||||
.partial();
|
.partial();
|
||||||
|
|
||||||
|
const DynamicSecretConditionV2Schema = z
|
||||||
|
.object({
|
||||||
|
environment: z.union([
|
||||||
|
z.string(),
|
||||||
|
z
|
||||||
|
.object({
|
||||||
|
[PermissionConditionOperators.$EQ]: PermissionConditionSchema[PermissionConditionOperators.$EQ],
|
||||||
|
[PermissionConditionOperators.$NEQ]: PermissionConditionSchema[PermissionConditionOperators.$NEQ],
|
||||||
|
[PermissionConditionOperators.$IN]: PermissionConditionSchema[PermissionConditionOperators.$IN]
|
||||||
|
})
|
||||||
|
.partial()
|
||||||
|
]),
|
||||||
|
secretPath: SECRET_PATH_PERMISSION_OPERATOR_SCHEMA,
|
||||||
|
metadata: z.object({
|
||||||
|
[PermissionConditionOperators.$ELEMENTMATCH]: z
|
||||||
|
.object({
|
||||||
|
key: z
|
||||||
|
.object({
|
||||||
|
[PermissionConditionOperators.$EQ]: PermissionConditionSchema[PermissionConditionOperators.$EQ],
|
||||||
|
[PermissionConditionOperators.$NEQ]: PermissionConditionSchema[PermissionConditionOperators.$NEQ],
|
||||||
|
[PermissionConditionOperators.$IN]: PermissionConditionSchema[PermissionConditionOperators.$IN]
|
||||||
|
})
|
||||||
|
.partial(),
|
||||||
|
value: z
|
||||||
|
.object({
|
||||||
|
[PermissionConditionOperators.$EQ]: PermissionConditionSchema[PermissionConditionOperators.$EQ],
|
||||||
|
[PermissionConditionOperators.$NEQ]: PermissionConditionSchema[PermissionConditionOperators.$NEQ],
|
||||||
|
[PermissionConditionOperators.$IN]: PermissionConditionSchema[PermissionConditionOperators.$IN]
|
||||||
|
})
|
||||||
|
.partial()
|
||||||
|
})
|
||||||
|
.partial()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.partial();
|
||||||
|
|
||||||
const SecretConditionV2Schema = z
|
const SecretConditionV2Schema = z
|
||||||
.object({
|
.object({
|
||||||
environment: z.union([
|
environment: z.union([
|
||||||
@@ -579,7 +621,7 @@ export const ProjectPermissionV2Schema = z.discriminatedUnion("subject", [
|
|||||||
action: CASL_ACTION_SCHEMA_NATIVE_ENUM(ProjectPermissionDynamicSecretActions).describe(
|
action: CASL_ACTION_SCHEMA_NATIVE_ENUM(ProjectPermissionDynamicSecretActions).describe(
|
||||||
"Describe what action an entity can take."
|
"Describe what action an entity can take."
|
||||||
),
|
),
|
||||||
conditions: SecretConditionV1Schema.describe(
|
conditions: DynamicSecretConditionV2Schema.describe(
|
||||||
"When specified, only matching conditions will be allowed to access given resource."
|
"When specified, only matching conditions will be allowed to access given resource."
|
||||||
).optional()
|
).optional()
|
||||||
}),
|
}),
|
||||||
@@ -732,7 +774,9 @@ const buildAdminPermissionRules = () => {
|
|||||||
ProjectPermissionCmekActions.Delete,
|
ProjectPermissionCmekActions.Delete,
|
||||||
ProjectPermissionCmekActions.Read,
|
ProjectPermissionCmekActions.Read,
|
||||||
ProjectPermissionCmekActions.Encrypt,
|
ProjectPermissionCmekActions.Encrypt,
|
||||||
ProjectPermissionCmekActions.Decrypt
|
ProjectPermissionCmekActions.Decrypt,
|
||||||
|
ProjectPermissionCmekActions.Sign,
|
||||||
|
ProjectPermissionCmekActions.Verify
|
||||||
],
|
],
|
||||||
ProjectPermissionSub.Cmek
|
ProjectPermissionSub.Cmek
|
||||||
);
|
);
|
||||||
@@ -935,7 +979,9 @@ const buildMemberPermissionRules = () => {
|
|||||||
ProjectPermissionCmekActions.Delete,
|
ProjectPermissionCmekActions.Delete,
|
||||||
ProjectPermissionCmekActions.Read,
|
ProjectPermissionCmekActions.Read,
|
||||||
ProjectPermissionCmekActions.Encrypt,
|
ProjectPermissionCmekActions.Encrypt,
|
||||||
ProjectPermissionCmekActions.Decrypt
|
ProjectPermissionCmekActions.Decrypt,
|
||||||
|
ProjectPermissionCmekActions.Sign,
|
||||||
|
ProjectPermissionCmekActions.Verify
|
||||||
],
|
],
|
||||||
ProjectPermissionSub.Cmek
|
ProjectPermissionSub.Cmek
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -113,7 +113,13 @@ type TSecretApprovalRequestServiceFactoryDep = {
|
|||||||
kmsService: Pick<TKmsServiceFactory, "createCipherPairWithDataKey" | "encryptWithInputKey" | "decryptWithInputKey">;
|
kmsService: Pick<TKmsServiceFactory, "createCipherPairWithDataKey" | "encryptWithInputKey" | "decryptWithInputKey">;
|
||||||
secretV2BridgeDAL: Pick<
|
secretV2BridgeDAL: Pick<
|
||||||
TSecretV2BridgeDALFactory,
|
TSecretV2BridgeDALFactory,
|
||||||
"insertMany" | "upsertSecretReferences" | "findBySecretKeys" | "bulkUpdate" | "deleteMany" | "find"
|
| "insertMany"
|
||||||
|
| "upsertSecretReferences"
|
||||||
|
| "findBySecretKeys"
|
||||||
|
| "bulkUpdate"
|
||||||
|
| "deleteMany"
|
||||||
|
| "find"
|
||||||
|
| "invalidateSecretCacheByProjectId"
|
||||||
>;
|
>;
|
||||||
secretVersionV2BridgeDAL: Pick<TSecretVersionV2DALFactory, "insertMany" | "findLatestVersionMany">;
|
secretVersionV2BridgeDAL: Pick<TSecretVersionV2DALFactory, "insertMany" | "findLatestVersionMany">;
|
||||||
secretVersionTagV2BridgeDAL: Pick<TSecretVersionV2TagDALFactory, "insertMany">;
|
secretVersionTagV2BridgeDAL: Pick<TSecretVersionV2TagDALFactory, "insertMany">;
|
||||||
@@ -864,6 +870,7 @@ export const secretApprovalRequestServiceFactory = ({
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
await secretV2BridgeDAL.invalidateSecretCacheByProjectId(projectId);
|
||||||
await snapshotService.performSnapshot(folderId);
|
await snapshotService.performSnapshot(folderId);
|
||||||
const [folder] = await folderDAL.findSecretPathByFolderIds(projectId, [folderId]);
|
const [folder] = await folderDAL.findSecretPathByFolderIds(projectId, [folderId]);
|
||||||
if (!folder) {
|
if (!folder) {
|
||||||
|
|||||||
@@ -45,7 +45,14 @@ type TSecretReplicationServiceFactoryDep = {
|
|||||||
secretVersionDAL: Pick<TSecretVersionDALFactory, "find" | "insertMany" | "update" | "findLatestVersionMany">;
|
secretVersionDAL: Pick<TSecretVersionDALFactory, "find" | "insertMany" | "update" | "findLatestVersionMany">;
|
||||||
secretV2BridgeDAL: Pick<
|
secretV2BridgeDAL: Pick<
|
||||||
TSecretV2BridgeDALFactory,
|
TSecretV2BridgeDALFactory,
|
||||||
"find" | "findBySecretKeys" | "insertMany" | "bulkUpdate" | "delete" | "upsertSecretReferences" | "transaction"
|
| "find"
|
||||||
|
| "findBySecretKeys"
|
||||||
|
| "insertMany"
|
||||||
|
| "bulkUpdate"
|
||||||
|
| "delete"
|
||||||
|
| "upsertSecretReferences"
|
||||||
|
| "transaction"
|
||||||
|
| "invalidateSecretCacheByProjectId"
|
||||||
>;
|
>;
|
||||||
secretVersionV2BridgeDAL: Pick<
|
secretVersionV2BridgeDAL: Pick<
|
||||||
TSecretVersionV2DALFactory,
|
TSecretVersionV2DALFactory,
|
||||||
@@ -260,6 +267,7 @@ export const secretReplicationServiceFactory = ({
|
|||||||
const sourceLocalSecrets = await secretV2BridgeDAL.find({ folderId: folder.id, type: SecretType.Shared });
|
const sourceLocalSecrets = await secretV2BridgeDAL.find({ folderId: folder.id, type: SecretType.Shared });
|
||||||
const sourceSecretImports = await secretImportDAL.find({ folderId: folder.id });
|
const sourceSecretImports = await secretImportDAL.find({ folderId: folder.id });
|
||||||
const sourceImportedSecrets = await fnSecretsV2FromImports({
|
const sourceImportedSecrets = await fnSecretsV2FromImports({
|
||||||
|
projectId,
|
||||||
secretImports: sourceSecretImports,
|
secretImports: sourceSecretImports,
|
||||||
secretDAL: secretV2BridgeDAL,
|
secretDAL: secretV2BridgeDAL,
|
||||||
folderDAL,
|
folderDAL,
|
||||||
@@ -497,6 +505,7 @@ export const secretReplicationServiceFactory = ({
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
await secretV2BridgeDAL.invalidateSecretCacheByProjectId(projectId);
|
||||||
await secretQueueService.syncSecrets({
|
await secretQueueService.syncSecrets({
|
||||||
projectId,
|
projectId,
|
||||||
orgId,
|
orgId,
|
||||||
|
|||||||
@@ -91,7 +91,7 @@ export type TSecretRotationV2ServiceFactoryDep = {
|
|||||||
folderDAL: Pick<TSecretFolderDALFactory, "findBySecretPath" | "findBySecretPathMultiEnv">;
|
folderDAL: Pick<TSecretFolderDALFactory, "findBySecretPath" | "findBySecretPathMultiEnv">;
|
||||||
secretV2BridgeDAL: Pick<
|
secretV2BridgeDAL: Pick<
|
||||||
TSecretV2BridgeDALFactory,
|
TSecretV2BridgeDALFactory,
|
||||||
"bulkUpdate" | "insertMany" | "deleteMany" | "upsertSecretReferences" | "find"
|
"bulkUpdate" | "insertMany" | "deleteMany" | "upsertSecretReferences" | "find" | "invalidateSecretCacheByProjectId"
|
||||||
>;
|
>;
|
||||||
secretVersionV2BridgeDAL: Pick<TSecretVersionV2DALFactory, "insertMany">;
|
secretVersionV2BridgeDAL: Pick<TSecretVersionV2DALFactory, "insertMany">;
|
||||||
secretVersionTagV2BridgeDAL: Pick<TSecretVersionV2TagDALFactory, "insertMany">;
|
secretVersionTagV2BridgeDAL: Pick<TSecretVersionV2TagDALFactory, "insertMany">;
|
||||||
@@ -529,6 +529,7 @@ export const secretRotationV2ServiceFactory = ({
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
await secretV2BridgeDAL.invalidateSecretCacheByProjectId(projectId);
|
||||||
await snapshotService.performSnapshot(folder.id);
|
await snapshotService.performSnapshot(folder.id);
|
||||||
await secretQueueService.syncSecrets({
|
await secretQueueService.syncSecrets({
|
||||||
orgId: connection.orgId,
|
orgId: connection.orgId,
|
||||||
@@ -665,6 +666,7 @@ export const secretRotationV2ServiceFactory = ({
|
|||||||
});
|
});
|
||||||
|
|
||||||
if (secretsMappingUpdated) {
|
if (secretsMappingUpdated) {
|
||||||
|
await secretV2BridgeDAL.invalidateSecretCacheByProjectId(projectId);
|
||||||
await snapshotService.performSnapshot(folder.id);
|
await snapshotService.performSnapshot(folder.id);
|
||||||
await secretQueueService.syncSecrets({
|
await secretQueueService.syncSecrets({
|
||||||
orgId: connection.orgId,
|
orgId: connection.orgId,
|
||||||
@@ -796,6 +798,7 @@ export const secretRotationV2ServiceFactory = ({
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (deleteSecrets) {
|
if (deleteSecrets) {
|
||||||
|
await secretV2BridgeDAL.invalidateSecretCacheByProjectId(projectId);
|
||||||
await snapshotService.performSnapshot(folder.id);
|
await snapshotService.performSnapshot(folder.id);
|
||||||
await secretQueueService.syncSecrets({
|
await secretQueueService.syncSecrets({
|
||||||
orgId: connection.orgId,
|
orgId: connection.orgId,
|
||||||
@@ -958,6 +961,7 @@ export const secretRotationV2ServiceFactory = ({
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
await secretV2BridgeDAL.invalidateSecretCacheByProjectId(projectId);
|
||||||
await snapshotService.performSnapshot(folder.id);
|
await snapshotService.performSnapshot(folder.id);
|
||||||
await secretQueueService.syncSecrets({
|
await secretQueueService.syncSecrets({
|
||||||
orgId: connection.orgId,
|
orgId: connection.orgId,
|
||||||
|
|||||||
+3
-1
@@ -48,7 +48,7 @@ type TSecretRotationQueueFactoryDep = {
|
|||||||
secretRotationDAL: TSecretRotationDALFactory;
|
secretRotationDAL: TSecretRotationDALFactory;
|
||||||
projectBotService: Pick<TProjectBotServiceFactory, "getBotKey">;
|
projectBotService: Pick<TProjectBotServiceFactory, "getBotKey">;
|
||||||
secretDAL: Pick<TSecretDALFactory, "bulkUpdate" | "find">;
|
secretDAL: Pick<TSecretDALFactory, "bulkUpdate" | "find">;
|
||||||
secretV2BridgeDAL: Pick<TSecretV2BridgeDALFactory, "bulkUpdate" | "find">;
|
secretV2BridgeDAL: Pick<TSecretV2BridgeDALFactory, "bulkUpdate" | "find" | "invalidateSecretCacheByProjectId">;
|
||||||
secretVersionDAL: Pick<TSecretVersionDALFactory, "insertMany" | "findLatestVersionMany">;
|
secretVersionDAL: Pick<TSecretVersionDALFactory, "insertMany" | "findLatestVersionMany">;
|
||||||
secretVersionV2BridgeDAL: Pick<TSecretVersionV2DALFactory, "insertMany" | "findLatestVersionMany">;
|
secretVersionV2BridgeDAL: Pick<TSecretVersionV2DALFactory, "insertMany" | "findLatestVersionMany">;
|
||||||
telemetryService: Pick<TTelemetryServiceFactory, "sendPostHogEvents">;
|
telemetryService: Pick<TTelemetryServiceFactory, "sendPostHogEvents">;
|
||||||
@@ -339,6 +339,8 @@ export const secretRotationQueueFactory = ({
|
|||||||
tx
|
tx
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
await secretV2BridgeDAL.invalidateSecretCacheByProjectId(secretRotation.projectId);
|
||||||
} else {
|
} else {
|
||||||
if (!botKey)
|
if (!botKey)
|
||||||
throw new NotFoundError({
|
throw new NotFoundError({
|
||||||
|
|||||||
@@ -127,6 +127,13 @@ export const secretRotationServiceFactory = ({
|
|||||||
});
|
});
|
||||||
if (selectedSecrets.length !== Object.values(outputs).length)
|
if (selectedSecrets.length !== Object.values(outputs).length)
|
||||||
throw new NotFoundError({ message: `Secrets not found in folder with ID '${folder.id}'` });
|
throw new NotFoundError({ message: `Secrets not found in folder with ID '${folder.id}'` });
|
||||||
|
const rotatedSecrets = selectedSecrets.filter(({ isRotatedSecret }) => isRotatedSecret);
|
||||||
|
if (rotatedSecrets.length)
|
||||||
|
throw new BadRequestError({
|
||||||
|
message: `Selected secrets are already used for rotation: ${rotatedSecrets
|
||||||
|
.map((secret) => secret.key)
|
||||||
|
.join(", ")}`
|
||||||
|
});
|
||||||
} else {
|
} else {
|
||||||
const selectedSecrets = await secretDAL.find({
|
const selectedSecrets = await secretDAL.find({
|
||||||
folderId: folder.id,
|
folderId: folder.id,
|
||||||
|
|||||||
@@ -18,7 +18,8 @@ export const rotationTemplates: TSecretRotationProviderTemplate[] = [
|
|||||||
title: "PostgreSQL",
|
title: "PostgreSQL",
|
||||||
image: "postgres.png",
|
image: "postgres.png",
|
||||||
description: "Rotate PostgreSQL/CockroachDB user credentials",
|
description: "Rotate PostgreSQL/CockroachDB user credentials",
|
||||||
template: POSTGRES_TEMPLATE
|
template: POSTGRES_TEMPLATE,
|
||||||
|
isDeprecated: true
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "mysql",
|
name: "mysql",
|
||||||
@@ -32,7 +33,8 @@ export const rotationTemplates: TSecretRotationProviderTemplate[] = [
|
|||||||
title: "Microsoft SQL Server",
|
title: "Microsoft SQL Server",
|
||||||
image: "mssqlserver.png",
|
image: "mssqlserver.png",
|
||||||
description: "Rotate Microsoft SQL server user credentials",
|
description: "Rotate Microsoft SQL server user credentials",
|
||||||
template: MSSQL_TEMPLATE
|
template: MSSQL_TEMPLATE,
|
||||||
|
isDeprecated: true
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "aws-iam",
|
name: "aws-iam",
|
||||||
|
|||||||
@@ -50,6 +50,7 @@ export type TSecretRotationProviderTemplate = {
|
|||||||
image?: string;
|
image?: string;
|
||||||
description?: string;
|
description?: string;
|
||||||
template: THttpProviderTemplate | TDbProviderTemplate | TAwsProviderTemplate;
|
template: THttpProviderTemplate | TDbProviderTemplate | TAwsProviderTemplate;
|
||||||
|
isDeprecated?: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type THttpProviderTemplate = {
|
export type THttpProviderTemplate = {
|
||||||
|
|||||||
@@ -77,6 +77,8 @@ export const keyStoreFactory = (redisUrl: string) => {
|
|||||||
|
|
||||||
const incrementBy = async (key: string, value: number) => redis.incrby(key, value);
|
const incrementBy = async (key: string, value: number) => redis.incrby(key, value);
|
||||||
|
|
||||||
|
const setExpiry = async (key: string, expiryInSeconds: number) => redis.expire(key, expiryInSeconds);
|
||||||
|
|
||||||
const waitTillReady = async ({
|
const waitTillReady = async ({
|
||||||
key,
|
key,
|
||||||
waitingCb,
|
waitingCb,
|
||||||
@@ -103,6 +105,7 @@ export const keyStoreFactory = (redisUrl: string) => {
|
|||||||
return {
|
return {
|
||||||
setItem,
|
setItem,
|
||||||
getItem,
|
getItem,
|
||||||
|
setExpiry,
|
||||||
setItemWithExpiry,
|
setItemWithExpiry,
|
||||||
deleteItem,
|
deleteItem,
|
||||||
incrementBy,
|
incrementBy,
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ export const inMemoryKeyStore = (): TKeyStoreFactory => {
|
|||||||
store[key] = value;
|
store[key] = value;
|
||||||
return "OK";
|
return "OK";
|
||||||
},
|
},
|
||||||
|
setExpiry: async () => 0,
|
||||||
setItemWithExpiry: async (key, value) => {
|
setItemWithExpiry: async (key, value) => {
|
||||||
store[key] = value;
|
store[key] = value;
|
||||||
return "OK";
|
return "OK";
|
||||||
|
|||||||
@@ -1672,7 +1672,8 @@ export const KMS = {
|
|||||||
projectId: "The ID of the project to create the key in.",
|
projectId: "The ID of the project to create the key in.",
|
||||||
name: "The name of the key to be created. Must be slug-friendly.",
|
name: "The name of the key to be created. Must be slug-friendly.",
|
||||||
description: "An optional description of the key.",
|
description: "An optional description of the key.",
|
||||||
encryptionAlgorithm: "The algorithm to use when performing cryptographic operations with the key."
|
encryptionAlgorithm: "The algorithm to use when performing cryptographic operations with the key.",
|
||||||
|
type: "The type of key to be created, either encrypt-decrypt or sign-verify, based on your intended use for the key."
|
||||||
},
|
},
|
||||||
UPDATE_KEY: {
|
UPDATE_KEY: {
|
||||||
keyId: "The ID of the key to be updated.",
|
keyId: "The ID of the key to be updated.",
|
||||||
@@ -1705,6 +1706,28 @@ export const KMS = {
|
|||||||
DECRYPT: {
|
DECRYPT: {
|
||||||
keyId: "The ID of the key to decrypt the data with.",
|
keyId: "The ID of the key to decrypt the data with.",
|
||||||
ciphertext: "The ciphertext to be decrypted (base64 encoded)."
|
ciphertext: "The ciphertext to be decrypted (base64 encoded)."
|
||||||
|
},
|
||||||
|
|
||||||
|
LIST_SIGNING_ALGORITHMS: {
|
||||||
|
keyId: "The ID of the key to list the signing algorithms for. The key must be for signing and verifying."
|
||||||
|
},
|
||||||
|
|
||||||
|
GET_PUBLIC_KEY: {
|
||||||
|
keyId: "The ID of the key to get the public key for. The key must be for signing and verifying."
|
||||||
|
},
|
||||||
|
|
||||||
|
SIGN: {
|
||||||
|
keyId: "The ID of the key to sign the data with.",
|
||||||
|
data: "The data in string format to be signed (base64 encoded).",
|
||||||
|
isDigest:
|
||||||
|
"Whether the data is already digested or not. Please be aware that if you are passing a digest the algorithm used to create the digest must match the signing algorithm used to sign the digest.",
|
||||||
|
signingAlgorithm: "The algorithm to use when performing cryptographic operations with the key."
|
||||||
|
},
|
||||||
|
VERIFY: {
|
||||||
|
keyId: "The ID of the key to verify the data with.",
|
||||||
|
data: "The data in string format to be verified (base64 encoded). For data larger than 4096 bytes you must first create a digest of the data and then pass the digest in the data parameter.",
|
||||||
|
signature: "The signature to be verified (base64 encoded).",
|
||||||
|
isDigest: "Whether the data is already digested or not."
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -1775,6 +1798,9 @@ export const AppConnections = {
|
|||||||
sslRejectUnauthorized: "Whether or not to reject unauthorized SSL certificates.",
|
sslRejectUnauthorized: "Whether or not to reject unauthorized SSL certificates.",
|
||||||
sslCertificate: "The SSL certificate to use for connection."
|
sslCertificate: "The SSL certificate to use for connection."
|
||||||
},
|
},
|
||||||
|
TERRAFORM_CLOUD: {
|
||||||
|
apiToken: "The API token to use to connect with Terraform Cloud."
|
||||||
|
},
|
||||||
VERCEL: {
|
VERCEL: {
|
||||||
apiToken: "The API token used to authenticate with Vercel."
|
apiToken: "The API token used to authenticate with Vercel."
|
||||||
},
|
},
|
||||||
@@ -1901,6 +1927,15 @@ export const SecretSyncs = {
|
|||||||
env: "The ID of the Humanitec environment to sync secrets to.",
|
env: "The ID of the Humanitec environment to sync secrets to.",
|
||||||
scope: "The Humanitec scope that secrets should be synced to."
|
scope: "The Humanitec scope that secrets should be synced to."
|
||||||
},
|
},
|
||||||
|
TERRAFORM_CLOUD: {
|
||||||
|
org: "The ID of the Terraform Cloud org to sync secrets to.",
|
||||||
|
variableSetName: "The name of the Terraform Cloud Variable Set to sync secrets to.",
|
||||||
|
variableSetId: "The ID of the Terraform Cloud Variable Set to sync secrets to.",
|
||||||
|
workspaceName: "The name of the Terraform Cloud workspace to sync secrets to.",
|
||||||
|
workspaceId: "The ID of the Terraform Cloud workspace to sync secrets to.",
|
||||||
|
scope: "The Terraform Cloud scope that secrets should be synced to.",
|
||||||
|
category: "The Terraform Cloud category that secrets should be synced to."
|
||||||
|
},
|
||||||
VERCEL: {
|
VERCEL: {
|
||||||
app: "The ID of the Vercel app to sync secrets to.",
|
app: "The ID of the Vercel app to sync secrets to.",
|
||||||
appName: "The name of the Vercel app to sync secrets to.",
|
appName: "The name of the Vercel app to sync secrets to.",
|
||||||
|
|||||||
@@ -24,5 +24,6 @@ export enum PermissionConditionOperators {
|
|||||||
$IN = "$in",
|
$IN = "$in",
|
||||||
$EQ = "$eq",
|
$EQ = "$eq",
|
||||||
$NEQ = "$ne",
|
$NEQ = "$ne",
|
||||||
$GLOB = "$glob"
|
$GLOB = "$glob",
|
||||||
|
$ELEMENTMATCH = "$elemMatch"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -197,6 +197,7 @@ const envSchema = z
|
|||||||
/* ----------------------------------------------------------------------------- */
|
/* ----------------------------------------------------------------------------- */
|
||||||
|
|
||||||
/* App Connections ----------------------------------------------------------------------------- */
|
/* App Connections ----------------------------------------------------------------------------- */
|
||||||
|
ALLOW_INTERNAL_IP_CONNECTIONS: zodStrBool.default("false"),
|
||||||
|
|
||||||
// aws
|
// aws
|
||||||
INF_APP_CONNECTION_AWS_ACCESS_KEY_ID: zpStr(z.string().optional()),
|
INF_APP_CONNECTION_AWS_ACCESS_KEY_ID: zpStr(z.string().optional()),
|
||||||
|
|||||||
@@ -0,0 +1,10 @@
|
|||||||
|
import crypto from "node:crypto";
|
||||||
|
|
||||||
|
export const generateCacheKeyFromData = (data: unknown) =>
|
||||||
|
crypto
|
||||||
|
.createHash("md5")
|
||||||
|
.update(JSON.stringify(data))
|
||||||
|
.digest("base64")
|
||||||
|
.replace(/\+/g, "-")
|
||||||
|
.replace(/\//g, "_")
|
||||||
|
.replace(/=/g, "");
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
import crypto from "crypto";
|
import crypto from "crypto";
|
||||||
|
|
||||||
import { SymmetricEncryption, TSymmetricEncryptionFns } from "./types";
|
import { SymmetricKeyAlgorithm, TSymmetricEncryptionFns } from "./types";
|
||||||
|
|
||||||
const getIvLength = () => {
|
const getIvLength = () => {
|
||||||
return 12;
|
return 12;
|
||||||
@@ -10,7 +10,9 @@ const getTagLength = () => {
|
|||||||
return 16;
|
return 16;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const symmetricCipherService = (type: SymmetricEncryption): TSymmetricEncryptionFns => {
|
export const symmetricCipherService = (
|
||||||
|
type: SymmetricKeyAlgorithm.AES_GCM_128 | SymmetricKeyAlgorithm.AES_GCM_256
|
||||||
|
): TSymmetricEncryptionFns => {
|
||||||
const IV_LENGTH = getIvLength();
|
const IV_LENGTH = getIvLength();
|
||||||
const TAG_LENGTH = getTagLength();
|
const TAG_LENGTH = getTagLength();
|
||||||
|
|
||||||
|
|||||||
@@ -1,2 +1,2 @@
|
|||||||
export { symmetricCipherService } from "./cipher";
|
export { symmetricCipherService } from "./cipher";
|
||||||
export { SymmetricEncryption } from "./types";
|
export { AllowedEncryptionKeyAlgorithms, SymmetricKeyAlgorithm } from "./types";
|
||||||
|
|||||||
@@ -1,7 +1,18 @@
|
|||||||
export enum SymmetricEncryption {
|
import { z } from "zod";
|
||||||
|
|
||||||
|
import { AsymmetricKeyAlgorithm } from "../sign/types";
|
||||||
|
|
||||||
|
// Supported symmetric encrypt/decrypt algorithms
|
||||||
|
export enum SymmetricKeyAlgorithm {
|
||||||
AES_GCM_256 = "aes-256-gcm",
|
AES_GCM_256 = "aes-256-gcm",
|
||||||
AES_GCM_128 = "aes-128-gcm"
|
AES_GCM_128 = "aes-128-gcm"
|
||||||
}
|
}
|
||||||
|
export const SymmetricKeyAlgorithmEnum = z.enum(Object.values(SymmetricKeyAlgorithm) as [string, ...string[]]).options;
|
||||||
|
|
||||||
|
export const AllowedEncryptionKeyAlgorithms = z.enum([
|
||||||
|
...Object.values(SymmetricKeyAlgorithm),
|
||||||
|
...Object.values(AsymmetricKeyAlgorithm)
|
||||||
|
] as [string, ...string[]]).options;
|
||||||
|
|
||||||
export type TSymmetricEncryptionFns = {
|
export type TSymmetricEncryptionFns = {
|
||||||
encrypt: (text: Buffer, key: Buffer) => Buffer;
|
encrypt: (text: Buffer, key: Buffer) => Buffer;
|
||||||
|
|||||||
@@ -0,0 +1,2 @@
|
|||||||
|
export { signingService } from "./signing";
|
||||||
|
export { AsymmetricKeyAlgorithm, SigningAlgorithm } from "./types";
|
||||||
@@ -0,0 +1,564 @@
|
|||||||
|
import { execFile } from "child_process";
|
||||||
|
import crypto from "crypto";
|
||||||
|
import fs from "fs/promises";
|
||||||
|
import path from "path";
|
||||||
|
import { promisify } from "util";
|
||||||
|
|
||||||
|
import { BadRequestError } from "@app/lib/errors";
|
||||||
|
import { cleanTemporaryDirectory, createTemporaryDirectory, writeToTemporaryFile } from "@app/lib/files";
|
||||||
|
import { logger } from "@app/lib/logger";
|
||||||
|
|
||||||
|
import { AsymmetricKeyAlgorithm, SigningAlgorithm, TAsymmetricSignVerifyFns } from "./types";
|
||||||
|
|
||||||
|
const execFileAsync = promisify(execFile);
|
||||||
|
|
||||||
|
interface SigningParams {
|
||||||
|
hashAlgorithm: SupportedHashAlgorithm;
|
||||||
|
padding?: number;
|
||||||
|
saltLength?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
enum SupportedHashAlgorithm {
|
||||||
|
SHA256 = "sha256",
|
||||||
|
SHA384 = "sha384",
|
||||||
|
SHA512 = "sha512"
|
||||||
|
}
|
||||||
|
|
||||||
|
const COMMAND_TIMEOUT = 15_000;
|
||||||
|
|
||||||
|
const SHA256_DIGEST_LENGTH = 32;
|
||||||
|
const SHA384_DIGEST_LENGTH = 48;
|
||||||
|
const SHA512_DIGEST_LENGTH = 64;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Service for cryptographic signing and verification operations using asymmetric keys
|
||||||
|
*
|
||||||
|
* @param algorithm The key algorithm itself. The signing algorithm is supplied in the individual sign/verify functions.
|
||||||
|
* @returns Object with sign and verify functions
|
||||||
|
*/
|
||||||
|
export const signingService = (algorithm: AsymmetricKeyAlgorithm): TAsymmetricSignVerifyFns => {
|
||||||
|
const $getSigningParams = (signingAlgorithm: SigningAlgorithm): SigningParams => {
|
||||||
|
switch (signingAlgorithm) {
|
||||||
|
// RSA PSS
|
||||||
|
case SigningAlgorithm.RSASSA_PSS_SHA_512:
|
||||||
|
return {
|
||||||
|
hashAlgorithm: SupportedHashAlgorithm.SHA512,
|
||||||
|
padding: crypto.constants.RSA_PKCS1_PSS_PADDING,
|
||||||
|
saltLength: SHA512_DIGEST_LENGTH
|
||||||
|
};
|
||||||
|
case SigningAlgorithm.RSASSA_PSS_SHA_256:
|
||||||
|
return {
|
||||||
|
hashAlgorithm: SupportedHashAlgorithm.SHA256,
|
||||||
|
padding: crypto.constants.RSA_PKCS1_PSS_PADDING,
|
||||||
|
saltLength: SHA256_DIGEST_LENGTH
|
||||||
|
};
|
||||||
|
case SigningAlgorithm.RSASSA_PSS_SHA_384:
|
||||||
|
return {
|
||||||
|
hashAlgorithm: SupportedHashAlgorithm.SHA384,
|
||||||
|
padding: crypto.constants.RSA_PKCS1_PSS_PADDING,
|
||||||
|
saltLength: SHA384_DIGEST_LENGTH
|
||||||
|
};
|
||||||
|
|
||||||
|
// RSA PKCS#1 v1.5
|
||||||
|
case SigningAlgorithm.RSASSA_PKCS1_V1_5_SHA_512:
|
||||||
|
return {
|
||||||
|
hashAlgorithm: SupportedHashAlgorithm.SHA512,
|
||||||
|
padding: crypto.constants.RSA_PKCS1_PADDING
|
||||||
|
};
|
||||||
|
case SigningAlgorithm.RSASSA_PKCS1_V1_5_SHA_384:
|
||||||
|
return {
|
||||||
|
hashAlgorithm: SupportedHashAlgorithm.SHA384,
|
||||||
|
padding: crypto.constants.RSA_PKCS1_PADDING
|
||||||
|
};
|
||||||
|
case SigningAlgorithm.RSASSA_PKCS1_V1_5_SHA_256:
|
||||||
|
return {
|
||||||
|
hashAlgorithm: SupportedHashAlgorithm.SHA256,
|
||||||
|
padding: crypto.constants.RSA_PKCS1_PADDING
|
||||||
|
};
|
||||||
|
|
||||||
|
// ECDSA
|
||||||
|
case SigningAlgorithm.ECDSA_SHA_256:
|
||||||
|
return { hashAlgorithm: SupportedHashAlgorithm.SHA256 };
|
||||||
|
case SigningAlgorithm.ECDSA_SHA_384:
|
||||||
|
return { hashAlgorithm: SupportedHashAlgorithm.SHA384 };
|
||||||
|
case SigningAlgorithm.ECDSA_SHA_512:
|
||||||
|
return { hashAlgorithm: SupportedHashAlgorithm.SHA512 };
|
||||||
|
|
||||||
|
default:
|
||||||
|
throw new Error(`Unsupported signing algorithm: ${signingAlgorithm as string}`);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const $getEcCurveName = (keyAlgorithm: AsymmetricKeyAlgorithm): { full: string; short: string } => {
|
||||||
|
// We will support more in the future
|
||||||
|
switch (keyAlgorithm) {
|
||||||
|
case AsymmetricKeyAlgorithm.ECC_NIST_P256:
|
||||||
|
return {
|
||||||
|
full: "prime256v1",
|
||||||
|
short: "p256"
|
||||||
|
};
|
||||||
|
default:
|
||||||
|
throw new Error(`Unsupported EC curve: ${keyAlgorithm}`);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const $validateAlgorithmWithKeyType = (signingAlgorithm: SigningAlgorithm) => {
|
||||||
|
const isRsaKey = algorithm.startsWith("RSA");
|
||||||
|
const isEccKey = algorithm.startsWith("ECC");
|
||||||
|
|
||||||
|
const isRsaAlgorithm = signingAlgorithm.startsWith("RSASSA");
|
||||||
|
const isEccAlgorithm = signingAlgorithm.startsWith("ECDSA");
|
||||||
|
|
||||||
|
if (isRsaKey && !isRsaAlgorithm) {
|
||||||
|
throw new BadRequestError({ message: `KMS RSA key cannot be used with ${signingAlgorithm}` });
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isEccKey && !isEccAlgorithm) {
|
||||||
|
throw new BadRequestError({ message: `KMS ECC key cannot be used with ${signingAlgorithm}` });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const $signRsaDigest = async (
|
||||||
|
digest: Buffer,
|
||||||
|
privateKey: Buffer,
|
||||||
|
hashAlgorithm: SupportedHashAlgorithm,
|
||||||
|
signingAlgorithm: SigningAlgorithm
|
||||||
|
) => {
|
||||||
|
const tempDir = await createTemporaryDirectory("kms-rsa-sign");
|
||||||
|
const digestPath = path.join(tempDir, "digest.bin");
|
||||||
|
const sigPath = path.join(tempDir, "signature.bin");
|
||||||
|
const keyPath = path.join(tempDir, "key.pem");
|
||||||
|
|
||||||
|
try {
|
||||||
|
await writeToTemporaryFile(digestPath, digest);
|
||||||
|
await writeToTemporaryFile(keyPath, privateKey);
|
||||||
|
|
||||||
|
const { stderr } = await execFileAsync(
|
||||||
|
"openssl",
|
||||||
|
[
|
||||||
|
"pkeyutl",
|
||||||
|
"-sign",
|
||||||
|
"-in",
|
||||||
|
digestPath,
|
||||||
|
"-inkey",
|
||||||
|
keyPath,
|
||||||
|
"-pkeyopt",
|
||||||
|
`digest:${hashAlgorithm}`,
|
||||||
|
"-out",
|
||||||
|
sigPath
|
||||||
|
],
|
||||||
|
{
|
||||||
|
maxBuffer: 10 * 1024 * 1024,
|
||||||
|
timeout: COMMAND_TIMEOUT
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
if (stderr) {
|
||||||
|
logger.error(stderr, "KMS: Failed to sign RSA digest");
|
||||||
|
throw new BadRequestError({
|
||||||
|
message: "Failed to sign RSA digest due to signing error"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
const signature = await fs.readFile(sigPath);
|
||||||
|
|
||||||
|
if (!signature) {
|
||||||
|
throw new BadRequestError({
|
||||||
|
message:
|
||||||
|
"No signature was created. Make sure you are using an appropriate signing algorithm that uses the same hashing algorithm as the one used to create the digest."
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return signature;
|
||||||
|
} catch (err) {
|
||||||
|
logger.error(err, "KMS: Failed to sign RSA digest");
|
||||||
|
throw new BadRequestError({
|
||||||
|
message: `Failed to sign RSA digest with ${signingAlgorithm} due to signing error. Ensure that your digest is hashed with ${hashAlgorithm.toUpperCase()}.`
|
||||||
|
});
|
||||||
|
} finally {
|
||||||
|
await cleanTemporaryDirectory(tempDir);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const $signEccDigest = async (
|
||||||
|
digest: Buffer,
|
||||||
|
privateKey: Buffer,
|
||||||
|
hashAlgorithm: SupportedHashAlgorithm,
|
||||||
|
signingAlgorithm: SigningAlgorithm
|
||||||
|
) => {
|
||||||
|
const tempDir = await createTemporaryDirectory("ecc-sign");
|
||||||
|
const digestPath = path.join(tempDir, "digest.bin");
|
||||||
|
const keyPath = path.join(tempDir, "key.pem");
|
||||||
|
const sigPath = path.join(tempDir, "signature.bin");
|
||||||
|
|
||||||
|
try {
|
||||||
|
await writeToTemporaryFile(digestPath, digest);
|
||||||
|
await writeToTemporaryFile(keyPath, privateKey);
|
||||||
|
|
||||||
|
const { stderr } = await execFileAsync(
|
||||||
|
"openssl",
|
||||||
|
[
|
||||||
|
"pkeyutl",
|
||||||
|
"-sign",
|
||||||
|
"-in",
|
||||||
|
digestPath,
|
||||||
|
"-inkey",
|
||||||
|
keyPath,
|
||||||
|
"-pkeyopt",
|
||||||
|
`digest:${hashAlgorithm}`,
|
||||||
|
"-out",
|
||||||
|
sigPath
|
||||||
|
],
|
||||||
|
{
|
||||||
|
maxBuffer: 10 * 1024 * 1024,
|
||||||
|
timeout: COMMAND_TIMEOUT
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
if (stderr) {
|
||||||
|
logger.error(stderr, "KMS: Failed to sign ECC digest");
|
||||||
|
throw new BadRequestError({
|
||||||
|
message: "Failed to sign ECC digest due to signing error"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const signature = await fs.readFile(sigPath);
|
||||||
|
|
||||||
|
if (!signature) {
|
||||||
|
throw new BadRequestError({
|
||||||
|
message:
|
||||||
|
"No signature was created. Make sure you are using an appropriate signing algorithm that uses the same hashing algorithm as the one used to create the digest."
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return signature;
|
||||||
|
} catch (err) {
|
||||||
|
logger.error(err, "KMS: Failed to sign ECC digest");
|
||||||
|
throw new BadRequestError({
|
||||||
|
message: `Failed to sign ECC digest with ${signingAlgorithm} due to signing error. Ensure that your digest is hashed with ${hashAlgorithm.toUpperCase()}.`
|
||||||
|
});
|
||||||
|
} finally {
|
||||||
|
await cleanTemporaryDirectory(tempDir);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const $verifyEccDigest = async (
|
||||||
|
digest: Buffer,
|
||||||
|
signature: Buffer,
|
||||||
|
publicKey: Buffer,
|
||||||
|
hashAlgorithm: SupportedHashAlgorithm
|
||||||
|
) => {
|
||||||
|
const tempDir = await createTemporaryDirectory("ecc-signature-verification");
|
||||||
|
const publicKeyFile = path.join(tempDir, "public-key.pem");
|
||||||
|
const sigFile = path.join(tempDir, "signature.sig");
|
||||||
|
const digestFile = path.join(tempDir, "digest.bin");
|
||||||
|
|
||||||
|
try {
|
||||||
|
await writeToTemporaryFile(publicKeyFile, publicKey);
|
||||||
|
await writeToTemporaryFile(sigFile, signature);
|
||||||
|
await writeToTemporaryFile(digestFile, digest);
|
||||||
|
|
||||||
|
await execFileAsync(
|
||||||
|
"openssl",
|
||||||
|
[
|
||||||
|
"pkeyutl",
|
||||||
|
"-verify",
|
||||||
|
"-in",
|
||||||
|
digestFile,
|
||||||
|
"-inkey",
|
||||||
|
publicKeyFile,
|
||||||
|
"-pubin", // Important for EC public keys
|
||||||
|
"-sigfile",
|
||||||
|
sigFile,
|
||||||
|
"-pkeyopt",
|
||||||
|
`digest:${hashAlgorithm}`
|
||||||
|
],
|
||||||
|
{ timeout: COMMAND_TIMEOUT }
|
||||||
|
);
|
||||||
|
|
||||||
|
return true;
|
||||||
|
} catch (error) {
|
||||||
|
const err = error as { stderr: string };
|
||||||
|
|
||||||
|
if (
|
||||||
|
!err?.stderr?.toLowerCase()?.includes("signature verification failure") &&
|
||||||
|
!err?.stderr?.toLowerCase()?.includes("bad signature")
|
||||||
|
) {
|
||||||
|
logger.error(error, "KMS: Failed to verify ECC signature");
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
} finally {
|
||||||
|
await cleanTemporaryDirectory(tempDir);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const $verifyRsaDigest = async (
|
||||||
|
digest: Buffer,
|
||||||
|
signature: Buffer,
|
||||||
|
publicKey: Buffer,
|
||||||
|
hashAlgorithm: SupportedHashAlgorithm
|
||||||
|
) => {
|
||||||
|
const tempDir = await createTemporaryDirectory("kms-signature-verification");
|
||||||
|
const publicKeyFile = path.join(tempDir, "public-key.pub");
|
||||||
|
const signatureFile = path.join(tempDir, "signature.sig");
|
||||||
|
const digestFile = path.join(tempDir, "digest.bin");
|
||||||
|
|
||||||
|
try {
|
||||||
|
await writeToTemporaryFile(publicKeyFile, publicKey);
|
||||||
|
await writeToTemporaryFile(signatureFile, signature);
|
||||||
|
await writeToTemporaryFile(digestFile, digest);
|
||||||
|
|
||||||
|
await execFileAsync(
|
||||||
|
"openssl",
|
||||||
|
[
|
||||||
|
"pkeyutl",
|
||||||
|
"-verify",
|
||||||
|
"-in",
|
||||||
|
digestFile,
|
||||||
|
"-inkey",
|
||||||
|
publicKeyFile,
|
||||||
|
"-pubin",
|
||||||
|
"-sigfile",
|
||||||
|
signatureFile,
|
||||||
|
"-pkeyopt",
|
||||||
|
`digest:${hashAlgorithm}`
|
||||||
|
],
|
||||||
|
{ timeout: COMMAND_TIMEOUT }
|
||||||
|
);
|
||||||
|
|
||||||
|
// it'll throw if the verification was not successful
|
||||||
|
return true;
|
||||||
|
} catch (error) {
|
||||||
|
const err = error as { stdout: string };
|
||||||
|
|
||||||
|
if (!err?.stdout?.toLowerCase()?.includes("signature verification failure")) {
|
||||||
|
logger.error(error, "KMS: Failed to verify signature");
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
} finally {
|
||||||
|
await cleanTemporaryDirectory(tempDir);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const verifyDigestFunctionsMap: Record<
|
||||||
|
AsymmetricKeyAlgorithm,
|
||||||
|
(data: Buffer, signature: Buffer, publicKey: Buffer, hashAlgorithm: SupportedHashAlgorithm) => Promise<boolean>
|
||||||
|
> = {
|
||||||
|
[AsymmetricKeyAlgorithm.ECC_NIST_P256]: $verifyEccDigest,
|
||||||
|
[AsymmetricKeyAlgorithm.RSA_4096]: $verifyRsaDigest
|
||||||
|
};
|
||||||
|
|
||||||
|
const signDigestFunctionsMap: Record<
|
||||||
|
AsymmetricKeyAlgorithm,
|
||||||
|
(
|
||||||
|
data: Buffer,
|
||||||
|
privateKey: Buffer,
|
||||||
|
hashAlgorithm: SupportedHashAlgorithm,
|
||||||
|
signingAlgorithm: SigningAlgorithm
|
||||||
|
) => Promise<Buffer>
|
||||||
|
> = {
|
||||||
|
[AsymmetricKeyAlgorithm.ECC_NIST_P256]: $signEccDigest,
|
||||||
|
[AsymmetricKeyAlgorithm.RSA_4096]: $signRsaDigest
|
||||||
|
};
|
||||||
|
|
||||||
|
const sign = async (
|
||||||
|
data: Buffer,
|
||||||
|
privateKey: Buffer,
|
||||||
|
signingAlgorithm: SigningAlgorithm,
|
||||||
|
isDigest: boolean
|
||||||
|
): Promise<Buffer> => {
|
||||||
|
$validateAlgorithmWithKeyType(signingAlgorithm);
|
||||||
|
|
||||||
|
const { hashAlgorithm, padding, saltLength } = $getSigningParams(signingAlgorithm);
|
||||||
|
|
||||||
|
if (isDigest) {
|
||||||
|
if (signingAlgorithm.startsWith("RSASSA_PSS")) {
|
||||||
|
throw new BadRequestError({
|
||||||
|
message: "RSA PSS does not support digested input"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const signFunction = signDigestFunctionsMap[algorithm];
|
||||||
|
|
||||||
|
if (!signFunction) {
|
||||||
|
throw new BadRequestError({
|
||||||
|
message: `Digested input is not supported for key algorithm ${algorithm}`
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const signature = await signFunction(data, privateKey, hashAlgorithm, signingAlgorithm);
|
||||||
|
return signature;
|
||||||
|
}
|
||||||
|
|
||||||
|
const privateKeyObject = crypto.createPrivateKey({
|
||||||
|
key: privateKey,
|
||||||
|
format: "pem",
|
||||||
|
type: "pkcs8"
|
||||||
|
});
|
||||||
|
|
||||||
|
// For RSA signatures
|
||||||
|
if (signingAlgorithm.startsWith("RSA")) {
|
||||||
|
const signer = crypto.createSign(hashAlgorithm);
|
||||||
|
signer.update(data);
|
||||||
|
|
||||||
|
return signer.sign({
|
||||||
|
key: privateKeyObject,
|
||||||
|
padding,
|
||||||
|
...(signingAlgorithm.includes("PSS") ? { saltLength } : {})
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if (signingAlgorithm.startsWith("ECDSA")) {
|
||||||
|
// For ECDSA signatures
|
||||||
|
const signer = crypto.createSign(hashAlgorithm);
|
||||||
|
signer.update(data);
|
||||||
|
return signer.sign({
|
||||||
|
key: privateKeyObject,
|
||||||
|
dsaEncoding: "der"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
throw new BadRequestError({
|
||||||
|
message: `Signing algorithm ${signingAlgorithm} not implemented`
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const verify = async (
|
||||||
|
data: Buffer,
|
||||||
|
signature: Buffer,
|
||||||
|
publicKey: Buffer,
|
||||||
|
signingAlgorithm: SigningAlgorithm,
|
||||||
|
isDigest: boolean
|
||||||
|
): Promise<boolean> => {
|
||||||
|
try {
|
||||||
|
$validateAlgorithmWithKeyType(signingAlgorithm);
|
||||||
|
|
||||||
|
const { hashAlgorithm, padding, saltLength } = $getSigningParams(signingAlgorithm);
|
||||||
|
|
||||||
|
if (isDigest) {
|
||||||
|
if (signingAlgorithm.startsWith("RSASSA_PSS")) {
|
||||||
|
throw new BadRequestError({
|
||||||
|
message: "RSA PSS does not support digested input"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const verifyFunction = verifyDigestFunctionsMap[algorithm];
|
||||||
|
|
||||||
|
if (!verifyFunction) {
|
||||||
|
throw new BadRequestError({
|
||||||
|
message: `Digested input is not supported for key algorithm ${algorithm}`
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const signatureValid = await verifyFunction(data, signature, publicKey, hashAlgorithm);
|
||||||
|
|
||||||
|
return signatureValid;
|
||||||
|
}
|
||||||
|
|
||||||
|
const publicKeyObject = crypto.createPublicKey({
|
||||||
|
key: publicKey,
|
||||||
|
format: "der",
|
||||||
|
type: "spki"
|
||||||
|
});
|
||||||
|
|
||||||
|
// For RSA signatures
|
||||||
|
if (signingAlgorithm.startsWith("RSA")) {
|
||||||
|
const verifier = crypto.createVerify(hashAlgorithm);
|
||||||
|
verifier.update(data);
|
||||||
|
|
||||||
|
return verifier.verify(
|
||||||
|
{
|
||||||
|
key: publicKeyObject,
|
||||||
|
padding,
|
||||||
|
...(signingAlgorithm.includes("PSS") ? { saltLength } : {})
|
||||||
|
},
|
||||||
|
signature
|
||||||
|
);
|
||||||
|
}
|
||||||
|
// For ECDSA signatures
|
||||||
|
if (signingAlgorithm.startsWith("ECDSA")) {
|
||||||
|
const verifier = crypto.createVerify(hashAlgorithm);
|
||||||
|
verifier.update(data);
|
||||||
|
return verifier.verify(
|
||||||
|
{
|
||||||
|
key: publicKeyObject,
|
||||||
|
dsaEncoding: "der"
|
||||||
|
},
|
||||||
|
signature
|
||||||
|
);
|
||||||
|
}
|
||||||
|
throw new BadRequestError({
|
||||||
|
message: `Verification for algorithm ${signingAlgorithm} not implemented`
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
if (error instanceof BadRequestError) {
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
logger.error(error, "KMS: Failed to verify signature");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const generateAsymmetricPrivateKey = async () => {
|
||||||
|
const { privateKey } = await new Promise<{ privateKey: string }>((resolve, reject) => {
|
||||||
|
if (algorithm.startsWith("RSA")) {
|
||||||
|
crypto.generateKeyPair(
|
||||||
|
"rsa",
|
||||||
|
{
|
||||||
|
modulusLength: Number(algorithm.split("_")[1]),
|
||||||
|
publicKeyEncoding: { type: "spki", format: "pem" },
|
||||||
|
privateKeyEncoding: { type: "pkcs8", format: "pem" }
|
||||||
|
},
|
||||||
|
(err, _, pk) => {
|
||||||
|
if (err) {
|
||||||
|
reject(err);
|
||||||
|
} else {
|
||||||
|
resolve({ privateKey: pk });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
const { full: namedCurve } = $getEcCurveName(algorithm);
|
||||||
|
|
||||||
|
crypto.generateKeyPair(
|
||||||
|
"ec",
|
||||||
|
{
|
||||||
|
namedCurve,
|
||||||
|
publicKeyEncoding: { type: "spki", format: "pem" },
|
||||||
|
privateKeyEncoding: { type: "pkcs8", format: "pem" }
|
||||||
|
},
|
||||||
|
(err, _, pk) => {
|
||||||
|
if (err) {
|
||||||
|
reject(err);
|
||||||
|
} else {
|
||||||
|
resolve({
|
||||||
|
privateKey: pk
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return Buffer.from(privateKey);
|
||||||
|
};
|
||||||
|
|
||||||
|
const getPublicKeyFromPrivateKey = (privateKey: Buffer) => {
|
||||||
|
const privateKeyObj = crypto.createPrivateKey({
|
||||||
|
key: privateKey,
|
||||||
|
format: "pem",
|
||||||
|
type: "pkcs8"
|
||||||
|
});
|
||||||
|
|
||||||
|
const publicKey = crypto.createPublicKey(privateKeyObj).export({
|
||||||
|
type: "spki",
|
||||||
|
format: "der"
|
||||||
|
});
|
||||||
|
|
||||||
|
return publicKey;
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
sign,
|
||||||
|
verify,
|
||||||
|
generateAsymmetricPrivateKey,
|
||||||
|
getPublicKeyFromPrivateKey
|
||||||
|
};
|
||||||
|
};
|
||||||
@@ -0,0 +1,45 @@
|
|||||||
|
import { z } from "zod";
|
||||||
|
|
||||||
|
export type TAsymmetricSignVerifyFns = {
|
||||||
|
sign: (data: Buffer, key: Buffer, signingAlgorithm: SigningAlgorithm, isDigest: boolean) => Promise<Buffer>;
|
||||||
|
verify: (
|
||||||
|
data: Buffer,
|
||||||
|
signature: Buffer,
|
||||||
|
key: Buffer,
|
||||||
|
signingAlgorithm: SigningAlgorithm,
|
||||||
|
isDigest: boolean
|
||||||
|
) => Promise<boolean>;
|
||||||
|
generateAsymmetricPrivateKey: () => Promise<Buffer>;
|
||||||
|
getPublicKeyFromPrivateKey: (privateKey: Buffer) => Buffer;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Supported asymmetric key types
|
||||||
|
export enum AsymmetricKeyAlgorithm {
|
||||||
|
RSA_4096 = "RSA_4096",
|
||||||
|
ECC_NIST_P256 = "ECC_NIST_P256"
|
||||||
|
}
|
||||||
|
|
||||||
|
export const AsymmetricKeyAlgorithmEnum = z.enum(
|
||||||
|
Object.values(AsymmetricKeyAlgorithm) as [string, ...string[]]
|
||||||
|
).options;
|
||||||
|
|
||||||
|
export enum SigningAlgorithm {
|
||||||
|
// RSA PSS algorithms
|
||||||
|
// These are NOT deterministic and include randomness.
|
||||||
|
// This means that the output signature is different each time for the same input.
|
||||||
|
RSASSA_PSS_SHA_512 = "RSASSA_PSS_SHA_512",
|
||||||
|
RSASSA_PSS_SHA_384 = "RSASSA_PSS_SHA_384",
|
||||||
|
RSASSA_PSS_SHA_256 = "RSASSA_PSS_SHA_256",
|
||||||
|
|
||||||
|
// RSA PKCS#1 v1.5 algorithms
|
||||||
|
// These are deterministic and the output is the same each time for the same input.
|
||||||
|
RSASSA_PKCS1_V1_5_SHA_512 = "RSASSA_PKCS1_V1_5_SHA_512",
|
||||||
|
RSASSA_PKCS1_V1_5_SHA_384 = "RSASSA_PKCS1_V1_5_SHA_384",
|
||||||
|
RSASSA_PKCS1_V1_5_SHA_256 = "RSASSA_PKCS1_V1_5_SHA_256",
|
||||||
|
|
||||||
|
// ECDSA algorithms
|
||||||
|
// None of these are deterministic and include randomness like RSA PSS.
|
||||||
|
ECDSA_SHA_512 = "ECDSA_SHA_512",
|
||||||
|
ECDSA_SHA_384 = "ECDSA_SHA_384",
|
||||||
|
ECDSA_SHA_256 = "ECDSA_SHA_256"
|
||||||
|
}
|
||||||
@@ -0,0 +1,35 @@
|
|||||||
|
import crypto from "crypto";
|
||||||
|
import fs from "fs/promises";
|
||||||
|
import os from "os";
|
||||||
|
import path from "path";
|
||||||
|
|
||||||
|
import { logger } from "@app/lib/logger";
|
||||||
|
|
||||||
|
const baseDir = path.join(os.tmpdir(), "infisical");
|
||||||
|
const randomPath = () => `${crypto.randomBytes(32).toString("hex")}`;
|
||||||
|
|
||||||
|
export const createTemporaryDirectory = async (name: string) => {
|
||||||
|
const tempDirPath = path.join(baseDir, `${name}-${randomPath()}`);
|
||||||
|
await fs.mkdir(tempDirPath, { recursive: true });
|
||||||
|
|
||||||
|
return tempDirPath;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const removeTemporaryBaseDirectory = async () => {
|
||||||
|
await fs.rm(baseDir, { force: true, recursive: true }).catch((err) => {
|
||||||
|
logger.error(err, `Failed to remove temporary base directory [path=${baseDir}]`);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
export const cleanTemporaryDirectory = async (dirPath: string) => {
|
||||||
|
await fs.rm(dirPath, { recursive: true, force: true }).catch((err) => {
|
||||||
|
logger.error(err, `Failed to cleanup temporary directory [path=${dirPath}]`);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
export const writeToTemporaryFile = async (tempDirPath: string, data: string | Buffer) => {
|
||||||
|
await fs.writeFile(tempDirPath, data, { mode: 0o600 }).catch((err) => {
|
||||||
|
logger.error(err, `Failed to write to temporary file [path=${tempDirPath}]`);
|
||||||
|
throw err;
|
||||||
|
});
|
||||||
|
};
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
export * from "./files";
|
||||||
@@ -41,6 +41,18 @@ export type RequiredKeys<T> = {
|
|||||||
[K in keyof T]-?: undefined extends T[K] ? never : K;
|
[K in keyof T]-?: undefined extends T[K] ? never : K;
|
||||||
}[keyof T];
|
}[keyof T];
|
||||||
|
|
||||||
|
export type BufferKeysToString<T> = {
|
||||||
|
[K in keyof T]: T[K] extends Buffer
|
||||||
|
? string
|
||||||
|
: T[K] extends Buffer | null
|
||||||
|
? string | null
|
||||||
|
: T[K] extends Buffer | undefined
|
||||||
|
? string | undefined
|
||||||
|
: T[K] extends Buffer | null | undefined
|
||||||
|
? string | null | undefined
|
||||||
|
: T[K];
|
||||||
|
};
|
||||||
|
|
||||||
export type PickRequired<T> = Pick<T, RequiredKeys<T>>;
|
export type PickRequired<T> = Pick<T, RequiredKeys<T>>;
|
||||||
|
|
||||||
export type DiscriminativePick<T, K extends keyof T> = T extends unknown ? Pick<T, K> : never;
|
export type DiscriminativePick<T, K extends keyof T> = T extends unknown ? Pick<T, K> : never;
|
||||||
|
|||||||
@@ -2,10 +2,16 @@ import dns from "node:dns/promises";
|
|||||||
|
|
||||||
import { isIPv4 } from "net";
|
import { isIPv4 } from "net";
|
||||||
|
|
||||||
|
import { getConfig } from "@app/lib/config/env";
|
||||||
|
|
||||||
import { BadRequestError } from "../errors";
|
import { BadRequestError } from "../errors";
|
||||||
import { isPrivateIp } from "../ip/ipRange";
|
import { isPrivateIp } from "../ip/ipRange";
|
||||||
|
|
||||||
export const blockLocalAndPrivateIpAddresses = async (url: string) => {
|
export const blockLocalAndPrivateIpAddresses = async (url: string) => {
|
||||||
|
const appCfg = getConfig();
|
||||||
|
|
||||||
|
if (appCfg.isDevelopmentMode) return;
|
||||||
|
|
||||||
const validUrl = new URL(url);
|
const validUrl = new URL(url);
|
||||||
const inputHostIps: string[] = [];
|
const inputHostIps: string[] = [];
|
||||||
if (isIPv4(validUrl.host)) {
|
if (isIPv4(validUrl.host)) {
|
||||||
@@ -18,7 +24,8 @@ export const blockLocalAndPrivateIpAddresses = async (url: string) => {
|
|||||||
inputHostIps.push(...resolvedIps);
|
inputHostIps.push(...resolvedIps);
|
||||||
}
|
}
|
||||||
const isInternalIp = inputHostIps.some((el) => isPrivateIp(el));
|
const isInternalIp = inputHostIps.some((el) => isPrivateIp(el));
|
||||||
if (isInternalIp) throw new BadRequestError({ message: "Local IPs not allowed as URL" });
|
if (isInternalIp && !appCfg.ALLOW_INTERNAL_IP_CONNECTIONS)
|
||||||
|
throw new BadRequestError({ message: "Local IPs not allowed as URL" });
|
||||||
};
|
};
|
||||||
|
|
||||||
type FQDNOptions = {
|
type FQDNOptions = {
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import { runMigrations } from "./auto-start-migrations";
|
|||||||
import { initAuditLogDbConnection, initDbConnection } from "./db";
|
import { initAuditLogDbConnection, initDbConnection } from "./db";
|
||||||
import { keyStoreFactory } from "./keystore/keystore";
|
import { keyStoreFactory } from "./keystore/keystore";
|
||||||
import { formatSmtpConfig, initEnvConfig } from "./lib/config/env";
|
import { formatSmtpConfig, initEnvConfig } from "./lib/config/env";
|
||||||
|
import { removeTemporaryBaseDirectory } from "./lib/files";
|
||||||
import { initLogger } from "./lib/logger";
|
import { initLogger } from "./lib/logger";
|
||||||
import { queueServiceFactory } from "./queue";
|
import { queueServiceFactory } from "./queue";
|
||||||
import { main } from "./server/app";
|
import { main } from "./server/app";
|
||||||
@@ -21,6 +22,8 @@ const run = async () => {
|
|||||||
const logger = initLogger();
|
const logger = initLogger();
|
||||||
const envConfig = initEnvConfig(logger);
|
const envConfig = initEnvConfig(logger);
|
||||||
|
|
||||||
|
await removeTemporaryBaseDirectory();
|
||||||
|
|
||||||
const db = initDbConnection({
|
const db = initDbConnection({
|
||||||
dbConnectionUri: envConfig.DB_CONNECTION_URI,
|
dbConnectionUri: envConfig.DB_CONNECTION_URI,
|
||||||
dbRootCert: envConfig.DB_ROOT_CERT,
|
dbRootCert: envConfig.DB_ROOT_CERT,
|
||||||
@@ -71,6 +74,7 @@ const run = async () => {
|
|||||||
process.on("SIGINT", async () => {
|
process.on("SIGINT", async () => {
|
||||||
await server.close();
|
await server.close();
|
||||||
await db.destroy();
|
await db.destroy();
|
||||||
|
await removeTemporaryBaseDirectory();
|
||||||
hsmModule.finalize();
|
hsmModule.finalize();
|
||||||
process.exit(0);
|
process.exit(0);
|
||||||
});
|
});
|
||||||
@@ -79,6 +83,7 @@ const run = async () => {
|
|||||||
process.on("SIGTERM", async () => {
|
process.on("SIGTERM", async () => {
|
||||||
await server.close();
|
await server.close();
|
||||||
await db.destroy();
|
await db.destroy();
|
||||||
|
await removeTemporaryBaseDirectory();
|
||||||
hsmModule.finalize();
|
hsmModule.finalize();
|
||||||
process.exit(0);
|
process.exit(0);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -315,7 +315,7 @@ export const registerRoutes = async (
|
|||||||
const secretVersionTagDAL = secretVersionTagDALFactory(db);
|
const secretVersionTagDAL = secretVersionTagDALFactory(db);
|
||||||
const secretBlindIndexDAL = secretBlindIndexDALFactory(db);
|
const secretBlindIndexDAL = secretBlindIndexDALFactory(db);
|
||||||
|
|
||||||
const secretV2BridgeDAL = secretV2BridgeDALFactory(db);
|
const secretV2BridgeDAL = secretV2BridgeDALFactory({ db, keyStore });
|
||||||
const secretVersionV2BridgeDAL = secretVersionV2BridgeDALFactory(db);
|
const secretVersionV2BridgeDAL = secretVersionV2BridgeDALFactory(db);
|
||||||
const secretVersionTagV2BridgeDAL = secretVersionV2TagBridgeDALFactory(db);
|
const secretVersionTagV2BridgeDAL = secretVersionV2TagBridgeDALFactory(db);
|
||||||
|
|
||||||
@@ -1391,7 +1391,8 @@ export const registerRoutes = async (
|
|||||||
permissionService,
|
permissionService,
|
||||||
licenseService,
|
licenseService,
|
||||||
kmsService,
|
kmsService,
|
||||||
projectGatewayDAL
|
projectGatewayDAL,
|
||||||
|
resourceMetadataDAL
|
||||||
});
|
});
|
||||||
|
|
||||||
const dynamicSecretLeaseService = dynamicSecretLeaseServiceFactory({
|
const dynamicSecretLeaseService = dynamicSecretLeaseServiceFactory({
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ import {
|
|||||||
UsersSchema
|
UsersSchema
|
||||||
} from "@app/db/schemas";
|
} from "@app/db/schemas";
|
||||||
import { ProjectPermissionActions, ProjectPermissionSub } from "@app/ee/services/permission/project-permission";
|
import { ProjectPermissionActions, ProjectPermissionSub } from "@app/ee/services/permission/project-permission";
|
||||||
|
import { ResourceMetadataSchema } from "@app/services/resource-metadata/resource-metadata-schema";
|
||||||
|
|
||||||
import { UnpackedPermissionSchema } from "./sanitizedSchema/permission";
|
import { UnpackedPermissionSchema } from "./sanitizedSchema/permission";
|
||||||
|
|
||||||
@@ -232,7 +233,11 @@ export const SanitizedDynamicSecretSchema = DynamicSecretsSchema.omit({
|
|||||||
inputIV: true,
|
inputIV: true,
|
||||||
inputTag: true,
|
inputTag: true,
|
||||||
algorithm: true
|
algorithm: true
|
||||||
});
|
}).merge(
|
||||||
|
z.object({
|
||||||
|
metadata: ResourceMetadataSchema.optional()
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
export const SanitizedAuditLogStreamSchema = z.object({
|
export const SanitizedAuditLogStreamSchema = z.object({
|
||||||
id: z.string(),
|
id: z.string(),
|
||||||
|
|||||||
@@ -32,6 +32,10 @@ import {
|
|||||||
PostgresConnectionListItemSchema,
|
PostgresConnectionListItemSchema,
|
||||||
SanitizedPostgresConnectionSchema
|
SanitizedPostgresConnectionSchema
|
||||||
} from "@app/services/app-connection/postgres";
|
} from "@app/services/app-connection/postgres";
|
||||||
|
import {
|
||||||
|
SanitizedTerraformCloudConnectionSchema,
|
||||||
|
TerraformCloudConnectionListItemSchema
|
||||||
|
} from "@app/services/app-connection/terraform-cloud";
|
||||||
import { SanitizedVercelConnectionSchema, VercelConnectionListItemSchema } from "@app/services/app-connection/vercel";
|
import { SanitizedVercelConnectionSchema, VercelConnectionListItemSchema } from "@app/services/app-connection/vercel";
|
||||||
import { AuthMode } from "@app/services/auth/auth-type";
|
import { AuthMode } from "@app/services/auth/auth-type";
|
||||||
|
|
||||||
@@ -44,6 +48,7 @@ const SanitizedAppConnectionSchema = z.union([
|
|||||||
...SanitizedAzureAppConfigurationConnectionSchema.options,
|
...SanitizedAzureAppConfigurationConnectionSchema.options,
|
||||||
...SanitizedDatabricksConnectionSchema.options,
|
...SanitizedDatabricksConnectionSchema.options,
|
||||||
...SanitizedHumanitecConnectionSchema.options,
|
...SanitizedHumanitecConnectionSchema.options,
|
||||||
|
...SanitizedTerraformCloudConnectionSchema.options,
|
||||||
...SanitizedVercelConnectionSchema.options,
|
...SanitizedVercelConnectionSchema.options,
|
||||||
...SanitizedPostgresConnectionSchema.options,
|
...SanitizedPostgresConnectionSchema.options,
|
||||||
...SanitizedMsSqlConnectionSchema.options,
|
...SanitizedMsSqlConnectionSchema.options,
|
||||||
@@ -59,6 +64,7 @@ const AppConnectionOptionsSchema = z.discriminatedUnion("app", [
|
|||||||
AzureAppConfigurationConnectionListItemSchema,
|
AzureAppConfigurationConnectionListItemSchema,
|
||||||
DatabricksConnectionListItemSchema,
|
DatabricksConnectionListItemSchema,
|
||||||
HumanitecConnectionListItemSchema,
|
HumanitecConnectionListItemSchema,
|
||||||
|
TerraformCloudConnectionListItemSchema,
|
||||||
VercelConnectionListItemSchema,
|
VercelConnectionListItemSchema,
|
||||||
PostgresConnectionListItemSchema,
|
PostgresConnectionListItemSchema,
|
||||||
MsSqlConnectionListItemSchema,
|
MsSqlConnectionListItemSchema,
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ import { registerGitHubConnectionRouter } from "./github-connection-router";
|
|||||||
import { registerHumanitecConnectionRouter } from "./humanitec-connection-router";
|
import { registerHumanitecConnectionRouter } from "./humanitec-connection-router";
|
||||||
import { registerMsSqlConnectionRouter } from "./mssql-connection-router";
|
import { registerMsSqlConnectionRouter } from "./mssql-connection-router";
|
||||||
import { registerPostgresConnectionRouter } from "./postgres-connection-router";
|
import { registerPostgresConnectionRouter } from "./postgres-connection-router";
|
||||||
|
import { registerTerraformCloudConnectionRouter } from "./terraform-cloud-router";
|
||||||
import { registerVercelConnectionRouter } from "./vercel-connection-router";
|
import { registerVercelConnectionRouter } from "./vercel-connection-router";
|
||||||
|
|
||||||
export * from "./app-connection-router";
|
export * from "./app-connection-router";
|
||||||
@@ -24,6 +25,7 @@ export const APP_CONNECTION_REGISTER_ROUTER_MAP: Record<AppConnection, (server:
|
|||||||
[AppConnection.AzureAppConfiguration]: registerAzureAppConfigurationConnectionRouter,
|
[AppConnection.AzureAppConfiguration]: registerAzureAppConfigurationConnectionRouter,
|
||||||
[AppConnection.Databricks]: registerDatabricksConnectionRouter,
|
[AppConnection.Databricks]: registerDatabricksConnectionRouter,
|
||||||
[AppConnection.Humanitec]: registerHumanitecConnectionRouter,
|
[AppConnection.Humanitec]: registerHumanitecConnectionRouter,
|
||||||
|
[AppConnection.TerraformCloud]: registerTerraformCloudConnectionRouter,
|
||||||
[AppConnection.Vercel]: registerVercelConnectionRouter,
|
[AppConnection.Vercel]: registerVercelConnectionRouter,
|
||||||
[AppConnection.Postgres]: registerPostgresConnectionRouter,
|
[AppConnection.Postgres]: registerPostgresConnectionRouter,
|
||||||
[AppConnection.MsSql]: registerMsSqlConnectionRouter,
|
[AppConnection.MsSql]: registerMsSqlConnectionRouter,
|
||||||
|
|||||||
@@ -0,0 +1,69 @@
|
|||||||
|
import z from "zod";
|
||||||
|
|
||||||
|
import { readLimit } from "@app/server/config/rateLimiter";
|
||||||
|
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
|
||||||
|
import { AppConnection } from "@app/services/app-connection/app-connection-enums";
|
||||||
|
import {
|
||||||
|
CreateTerraformCloudConnectionSchema,
|
||||||
|
SanitizedTerraformCloudConnectionSchema,
|
||||||
|
TTerraformCloudOrganization,
|
||||||
|
UpdateTerraformCloudConnectionSchema
|
||||||
|
} from "@app/services/app-connection/terraform-cloud";
|
||||||
|
import { AuthMode } from "@app/services/auth/auth-type";
|
||||||
|
|
||||||
|
import { registerAppConnectionEndpoints } from "./app-connection-endpoints";
|
||||||
|
|
||||||
|
export const registerTerraformCloudConnectionRouter = async (server: FastifyZodProvider) => {
|
||||||
|
registerAppConnectionEndpoints({
|
||||||
|
app: AppConnection.TerraformCloud,
|
||||||
|
server,
|
||||||
|
sanitizedResponseSchema: SanitizedTerraformCloudConnectionSchema,
|
||||||
|
createSchema: CreateTerraformCloudConnectionSchema,
|
||||||
|
updateSchema: UpdateTerraformCloudConnectionSchema
|
||||||
|
});
|
||||||
|
|
||||||
|
// The below endpoints are not exposed and for Infisical App use
|
||||||
|
server.route({
|
||||||
|
method: "GET",
|
||||||
|
url: `/:connectionId/organizations`,
|
||||||
|
config: {
|
||||||
|
rateLimit: readLimit
|
||||||
|
},
|
||||||
|
schema: {
|
||||||
|
params: z.object({
|
||||||
|
connectionId: z.string().uuid()
|
||||||
|
}),
|
||||||
|
response: {
|
||||||
|
200: z
|
||||||
|
.object({
|
||||||
|
id: z.string(),
|
||||||
|
name: z.string(),
|
||||||
|
variableSets: z
|
||||||
|
.object({
|
||||||
|
id: z.string(),
|
||||||
|
name: z.string(),
|
||||||
|
description: z.string().optional(),
|
||||||
|
global: z.boolean().optional()
|
||||||
|
})
|
||||||
|
.array(),
|
||||||
|
workspaces: z
|
||||||
|
.object({
|
||||||
|
id: z.string(),
|
||||||
|
name: z.string()
|
||||||
|
})
|
||||||
|
.array()
|
||||||
|
})
|
||||||
|
.array()
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onRequest: verifyAuth([AuthMode.JWT]),
|
||||||
|
handler: async (req) => {
|
||||||
|
const { connectionId } = req.params;
|
||||||
|
|
||||||
|
const organizations: TTerraformCloudOrganization[] =
|
||||||
|
await server.services.appConnection.terraformCloud.listOrganizations(connectionId, req.permission);
|
||||||
|
|
||||||
|
return organizations;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
@@ -4,13 +4,15 @@ import { InternalKmsSchema, KmsKeysSchema } from "@app/db/schemas";
|
|||||||
import { EventType } from "@app/ee/services/audit-log/audit-log-types";
|
import { EventType } from "@app/ee/services/audit-log/audit-log-types";
|
||||||
import { KMS } from "@app/lib/api-docs";
|
import { KMS } from "@app/lib/api-docs";
|
||||||
import { getBase64SizeInBytes, isBase64 } from "@app/lib/base64";
|
import { getBase64SizeInBytes, isBase64 } from "@app/lib/base64";
|
||||||
import { SymmetricEncryption } from "@app/lib/crypto/cipher";
|
import { AllowedEncryptionKeyAlgorithms, SymmetricKeyAlgorithm } from "@app/lib/crypto/cipher";
|
||||||
|
import { AsymmetricKeyAlgorithm, SigningAlgorithm } from "@app/lib/crypto/sign";
|
||||||
import { OrderByDirection } from "@app/lib/types";
|
import { OrderByDirection } from "@app/lib/types";
|
||||||
import { readLimit, writeLimit } from "@app/server/config/rateLimiter";
|
import { readLimit, writeLimit } from "@app/server/config/rateLimiter";
|
||||||
import { slugSchema } from "@app/server/lib/schemas";
|
import { slugSchema } from "@app/server/lib/schemas";
|
||||||
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
|
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
|
||||||
import { AuthMode } from "@app/services/auth/auth-type";
|
import { AuthMode } from "@app/services/auth/auth-type";
|
||||||
import { CmekOrderBy } from "@app/services/cmek/cmek-types";
|
import { CmekOrderBy, TCmekKeyEncryptionAlgorithm } from "@app/services/cmek/cmek-types";
|
||||||
|
import { KmsKeyUsage } from "@app/services/kms/kms-types";
|
||||||
|
|
||||||
const keyNameSchema = slugSchema({ min: 1, max: 32, field: "Name" });
|
const keyNameSchema = slugSchema({ min: 1, max: 32, field: "Name" });
|
||||||
const keyDescriptionSchema = z.string().trim().max(500).optional();
|
const keyDescriptionSchema = z.string().trim().max(500).optional();
|
||||||
@@ -45,16 +47,46 @@ export const registerCmekRouter = async (server: FastifyZodProvider) => {
|
|||||||
},
|
},
|
||||||
schema: {
|
schema: {
|
||||||
description: "Create KMS key",
|
description: "Create KMS key",
|
||||||
body: z.object({
|
body: z
|
||||||
projectId: z.string().describe(KMS.CREATE_KEY.projectId),
|
.object({
|
||||||
name: keyNameSchema.describe(KMS.CREATE_KEY.name),
|
projectId: z.string().describe(KMS.CREATE_KEY.projectId),
|
||||||
description: keyDescriptionSchema.describe(KMS.CREATE_KEY.description),
|
name: keyNameSchema.describe(KMS.CREATE_KEY.name),
|
||||||
encryptionAlgorithm: z
|
description: keyDescriptionSchema.describe(KMS.CREATE_KEY.description),
|
||||||
.nativeEnum(SymmetricEncryption)
|
keyUsage: z
|
||||||
.optional()
|
.nativeEnum(KmsKeyUsage)
|
||||||
.default(SymmetricEncryption.AES_GCM_256)
|
.optional()
|
||||||
.describe(KMS.CREATE_KEY.encryptionAlgorithm) // eventually will support others
|
.default(KmsKeyUsage.ENCRYPT_DECRYPT)
|
||||||
}),
|
.describe(KMS.CREATE_KEY.type),
|
||||||
|
encryptionAlgorithm: z
|
||||||
|
.enum(AllowedEncryptionKeyAlgorithms)
|
||||||
|
.optional()
|
||||||
|
.default(SymmetricKeyAlgorithm.AES_GCM_256)
|
||||||
|
.describe(KMS.CREATE_KEY.encryptionAlgorithm)
|
||||||
|
})
|
||||||
|
.superRefine((data, ctx) => {
|
||||||
|
if (
|
||||||
|
data.keyUsage === KmsKeyUsage.ENCRYPT_DECRYPT &&
|
||||||
|
!Object.values(SymmetricKeyAlgorithm).includes(data.encryptionAlgorithm as SymmetricKeyAlgorithm)
|
||||||
|
) {
|
||||||
|
ctx.addIssue({
|
||||||
|
code: z.ZodIssueCode.custom,
|
||||||
|
message: `encryptionAlgorithm must be a valid symmetric encryption algorithm. Valid options are: ${Object.values(
|
||||||
|
SymmetricKeyAlgorithm
|
||||||
|
).join(", ")}`
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if (
|
||||||
|
data.keyUsage === KmsKeyUsage.SIGN_VERIFY &&
|
||||||
|
!Object.values(AsymmetricKeyAlgorithm).includes(data.encryptionAlgorithm as AsymmetricKeyAlgorithm)
|
||||||
|
) {
|
||||||
|
ctx.addIssue({
|
||||||
|
code: z.ZodIssueCode.custom,
|
||||||
|
message: `encryptionAlgorithm must be a valid asymmetric sign-verify algorithm. Valid options are: ${Object.values(
|
||||||
|
AsymmetricKeyAlgorithm
|
||||||
|
).join(", ")}`
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}),
|
||||||
response: {
|
response: {
|
||||||
200: z.object({
|
200: z.object({
|
||||||
key: CmekSchema
|
key: CmekSchema
|
||||||
@@ -64,12 +96,19 @@ export const registerCmekRouter = 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 {
|
const {
|
||||||
body: { projectId, name, description, encryptionAlgorithm },
|
body: { projectId, name, description, encryptionAlgorithm, keyUsage },
|
||||||
permission
|
permission
|
||||||
} = req;
|
} = req;
|
||||||
|
|
||||||
const cmek = await server.services.cmek.createCmek(
|
const cmek = await server.services.cmek.createCmek(
|
||||||
{ orgId: permission.orgId, projectId, name, description, encryptionAlgorithm },
|
{
|
||||||
|
orgId: permission.orgId,
|
||||||
|
projectId,
|
||||||
|
name,
|
||||||
|
description,
|
||||||
|
encryptionAlgorithm: encryptionAlgorithm as TCmekKeyEncryptionAlgorithm,
|
||||||
|
keyUsage
|
||||||
|
},
|
||||||
permission
|
permission
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -82,7 +121,7 @@ export const registerCmekRouter = async (server: FastifyZodProvider) => {
|
|||||||
keyId: cmek.id,
|
keyId: cmek.id,
|
||||||
name,
|
name,
|
||||||
description,
|
description,
|
||||||
encryptionAlgorithm
|
encryptionAlgorithm: encryptionAlgorithm as TCmekKeyEncryptionAlgorithm
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -126,7 +165,7 @@ export const registerCmekRouter = async (server: FastifyZodProvider) => {
|
|||||||
|
|
||||||
await server.services.auditLog.createAuditLog({
|
await server.services.auditLog.createAuditLog({
|
||||||
...req.auditLogInfo,
|
...req.auditLogInfo,
|
||||||
orgId: permission.orgId,
|
projectId: cmek.projectId!,
|
||||||
event: {
|
event: {
|
||||||
type: EventType.UPDATE_CMEK,
|
type: EventType.UPDATE_CMEK,
|
||||||
metadata: {
|
metadata: {
|
||||||
@@ -169,7 +208,7 @@ export const registerCmekRouter = async (server: FastifyZodProvider) => {
|
|||||||
|
|
||||||
await server.services.auditLog.createAuditLog({
|
await server.services.auditLog.createAuditLog({
|
||||||
...req.auditLogInfo,
|
...req.auditLogInfo,
|
||||||
orgId: permission.orgId,
|
projectId: cmek.projectId!,
|
||||||
event: {
|
event: {
|
||||||
type: EventType.DELETE_CMEK,
|
type: EventType.DELETE_CMEK,
|
||||||
metadata: {
|
metadata: {
|
||||||
@@ -282,7 +321,7 @@ export const registerCmekRouter = async (server: FastifyZodProvider) => {
|
|||||||
rateLimit: readLimit
|
rateLimit: readLimit
|
||||||
},
|
},
|
||||||
schema: {
|
schema: {
|
||||||
description: "Get KMS key by Name",
|
description: "Get KMS key by name",
|
||||||
params: z.object({
|
params: z.object({
|
||||||
keyName: slugSchema({ field: "Key name" }).describe(KMS.GET_KEY_BY_NAME.keyName)
|
keyName: slugSchema({ field: "Key name" }).describe(KMS.GET_KEY_BY_NAME.keyName)
|
||||||
}),
|
}),
|
||||||
@@ -349,11 +388,11 @@ export const registerCmekRouter = async (server: FastifyZodProvider) => {
|
|||||||
permission
|
permission
|
||||||
} = req;
|
} = req;
|
||||||
|
|
||||||
const ciphertext = await server.services.cmek.cmekEncrypt({ keyId, plaintext }, permission);
|
const { ciphertext, projectId } = await server.services.cmek.cmekEncrypt({ keyId, plaintext }, permission);
|
||||||
|
|
||||||
await server.services.auditLog.createAuditLog({
|
await server.services.auditLog.createAuditLog({
|
||||||
...req.auditLogInfo,
|
...req.auditLogInfo,
|
||||||
orgId: permission.orgId,
|
projectId,
|
||||||
event: {
|
event: {
|
||||||
type: EventType.CMEK_ENCRYPT,
|
type: EventType.CMEK_ENCRYPT,
|
||||||
metadata: {
|
metadata: {
|
||||||
@@ -366,6 +405,198 @@ export const registerCmekRouter = async (server: FastifyZodProvider) => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
server.route({
|
||||||
|
method: "GET",
|
||||||
|
url: "/keys/:keyId/public-key",
|
||||||
|
config: {
|
||||||
|
rateLimit: readLimit
|
||||||
|
},
|
||||||
|
schema: {
|
||||||
|
description:
|
||||||
|
"Get the public key for a KMS key that is used for signing and verifying data. This endpoint is only available for asymmetric keys.",
|
||||||
|
params: z.object({
|
||||||
|
keyId: z.string().uuid().describe(KMS.GET_PUBLIC_KEY.keyId)
|
||||||
|
}),
|
||||||
|
response: {
|
||||||
|
200: z.object({
|
||||||
|
publicKey: z.string()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
||||||
|
handler: async (req) => {
|
||||||
|
const {
|
||||||
|
params: { keyId },
|
||||||
|
permission
|
||||||
|
} = req;
|
||||||
|
|
||||||
|
const { publicKey, projectId } = await server.services.cmek.getPublicKey({ keyId }, permission);
|
||||||
|
|
||||||
|
await server.services.auditLog.createAuditLog({
|
||||||
|
...req.auditLogInfo,
|
||||||
|
projectId,
|
||||||
|
event: {
|
||||||
|
type: EventType.CMEK_GET_PUBLIC_KEY,
|
||||||
|
metadata: {
|
||||||
|
keyId
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return { publicKey };
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
server.route({
|
||||||
|
method: "GET",
|
||||||
|
url: "/keys/:keyId/signing-algorithms",
|
||||||
|
config: {
|
||||||
|
rateLimit: readLimit
|
||||||
|
},
|
||||||
|
schema: {
|
||||||
|
description: "List all available signing algorithms for a KMS key",
|
||||||
|
params: z.object({
|
||||||
|
keyId: z.string().uuid().describe(KMS.LIST_SIGNING_ALGORITHMS.keyId)
|
||||||
|
}),
|
||||||
|
response: {
|
||||||
|
200: z.object({
|
||||||
|
signingAlgorithms: z.array(z.nativeEnum(SigningAlgorithm))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
||||||
|
handler: async (req) => {
|
||||||
|
const { keyId } = req.params;
|
||||||
|
|
||||||
|
const { signingAlgorithms, projectId } = await server.services.cmek.listSigningAlgorithms(
|
||||||
|
{ keyId },
|
||||||
|
req.permission
|
||||||
|
);
|
||||||
|
|
||||||
|
await server.services.auditLog.createAuditLog({
|
||||||
|
...req.auditLogInfo,
|
||||||
|
projectId,
|
||||||
|
event: {
|
||||||
|
type: EventType.CMEK_LIST_SIGNING_ALGORITHMS,
|
||||||
|
metadata: {
|
||||||
|
keyId
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return { signingAlgorithms };
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
server.route({
|
||||||
|
method: "POST",
|
||||||
|
url: "/keys/:keyId/sign",
|
||||||
|
config: {
|
||||||
|
rateLimit: writeLimit
|
||||||
|
},
|
||||||
|
schema: {
|
||||||
|
description: "Sign data with a KMS key.",
|
||||||
|
params: z.object({
|
||||||
|
keyId: z.string().uuid().describe(KMS.SIGN.keyId)
|
||||||
|
}),
|
||||||
|
body: z.object({
|
||||||
|
signingAlgorithm: z.nativeEnum(SigningAlgorithm),
|
||||||
|
isDigest: z.boolean().optional().default(false).describe(KMS.SIGN.isDigest),
|
||||||
|
data: base64Schema.describe(KMS.SIGN.data)
|
||||||
|
}),
|
||||||
|
response: {
|
||||||
|
200: z.object({
|
||||||
|
signature: z.string(),
|
||||||
|
keyId: z.string().uuid(),
|
||||||
|
signingAlgorithm: z.nativeEnum(SigningAlgorithm)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
||||||
|
handler: async (req) => {
|
||||||
|
const {
|
||||||
|
params: { keyId: inputKeyId },
|
||||||
|
body: { data, signingAlgorithm, isDigest },
|
||||||
|
permission
|
||||||
|
} = req;
|
||||||
|
|
||||||
|
const { projectId, ...result } = await server.services.cmek.cmekSign(
|
||||||
|
{ keyId: inputKeyId, data, signingAlgorithm, isDigest },
|
||||||
|
permission
|
||||||
|
);
|
||||||
|
|
||||||
|
await server.services.auditLog.createAuditLog({
|
||||||
|
...req.auditLogInfo,
|
||||||
|
projectId,
|
||||||
|
event: {
|
||||||
|
type: EventType.CMEK_SIGN,
|
||||||
|
metadata: {
|
||||||
|
keyId: inputKeyId,
|
||||||
|
signingAlgorithm,
|
||||||
|
signature: result.signature
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
server.route({
|
||||||
|
method: "POST",
|
||||||
|
url: "/keys/:keyId/verify",
|
||||||
|
config: {
|
||||||
|
rateLimit: writeLimit
|
||||||
|
},
|
||||||
|
schema: {
|
||||||
|
description: "Verify data signatures with a KMS key.",
|
||||||
|
params: z.object({
|
||||||
|
keyId: z.string().uuid().describe(KMS.VERIFY.keyId)
|
||||||
|
}),
|
||||||
|
body: z.object({
|
||||||
|
isDigest: z.boolean().optional().default(false).describe(KMS.VERIFY.isDigest),
|
||||||
|
data: base64Schema.describe(KMS.VERIFY.data),
|
||||||
|
signature: base64Schema.describe(KMS.VERIFY.signature),
|
||||||
|
signingAlgorithm: z.nativeEnum(SigningAlgorithm)
|
||||||
|
}),
|
||||||
|
response: {
|
||||||
|
200: z.object({
|
||||||
|
signatureValid: z.boolean(),
|
||||||
|
keyId: z.string().uuid(),
|
||||||
|
signingAlgorithm: z.nativeEnum(SigningAlgorithm)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
||||||
|
handler: async (req) => {
|
||||||
|
const {
|
||||||
|
params: { keyId },
|
||||||
|
body: { data, signature, signingAlgorithm, isDigest },
|
||||||
|
permission
|
||||||
|
} = req;
|
||||||
|
|
||||||
|
const { projectId, ...result } = await server.services.cmek.cmekVerify(
|
||||||
|
{ keyId, data, signature, signingAlgorithm, isDigest },
|
||||||
|
permission
|
||||||
|
);
|
||||||
|
|
||||||
|
await server.services.auditLog.createAuditLog({
|
||||||
|
...req.auditLogInfo,
|
||||||
|
projectId,
|
||||||
|
event: {
|
||||||
|
type: EventType.CMEK_VERIFY,
|
||||||
|
metadata: {
|
||||||
|
keyId,
|
||||||
|
signatureValid: result.signatureValid,
|
||||||
|
signingAlgorithm,
|
||||||
|
signature
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
server.route({
|
server.route({
|
||||||
method: "POST",
|
method: "POST",
|
||||||
url: "/keys/:keyId/decrypt",
|
url: "/keys/:keyId/decrypt",
|
||||||
@@ -394,11 +625,11 @@ export const registerCmekRouter = async (server: FastifyZodProvider) => {
|
|||||||
permission
|
permission
|
||||||
} = req;
|
} = req;
|
||||||
|
|
||||||
const plaintext = await server.services.cmek.cmekDecrypt({ keyId, ciphertext }, permission);
|
const { plaintext, projectId } = await server.services.cmek.cmekDecrypt({ keyId, ciphertext }, permission);
|
||||||
|
|
||||||
await server.services.auditLog.createAuditLog({
|
await server.services.auditLog.createAuditLog({
|
||||||
...req.auditLogInfo,
|
...req.auditLogInfo,
|
||||||
orgId: permission.orgId,
|
projectId,
|
||||||
event: {
|
event: {
|
||||||
type: EventType.CMEK_DECRYPT,
|
type: EventType.CMEK_DECRYPT,
|
||||||
metadata: {
|
metadata: {
|
||||||
|
|||||||
@@ -1,13 +1,9 @@
|
|||||||
import { ForbiddenError, subject } from "@casl/ability";
|
import { ForbiddenError } from "@casl/ability";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
|
|
||||||
import { ActionProjectType, SecretFoldersSchema, SecretImportsSchema } from "@app/db/schemas";
|
import { SecretFoldersSchema, SecretImportsSchema } from "@app/db/schemas";
|
||||||
import { EventType, UserAgentType } from "@app/ee/services/audit-log/audit-log-types";
|
import { EventType, UserAgentType } from "@app/ee/services/audit-log/audit-log-types";
|
||||||
import {
|
import { ProjectPermissionSecretActions } from "@app/ee/services/permission/project-permission";
|
||||||
ProjectPermissionDynamicSecretActions,
|
|
||||||
ProjectPermissionSecretActions,
|
|
||||||
ProjectPermissionSub
|
|
||||||
} from "@app/ee/services/permission/project-permission";
|
|
||||||
import { SecretRotationV2Schema } from "@app/ee/services/secret-rotation-v2/secret-rotation-v2-union-schema";
|
import { SecretRotationV2Schema } from "@app/ee/services/secret-rotation-v2/secret-rotation-v2-union-schema";
|
||||||
import { DASHBOARD } from "@app/lib/api-docs";
|
import { DASHBOARD } from "@app/lib/api-docs";
|
||||||
import { BadRequestError } from "@app/lib/errors";
|
import { BadRequestError } from "@app/lib/errors";
|
||||||
@@ -142,6 +138,34 @@ export const registerDashboardRouter = async (server: FastifyZodProvider) => {
|
|||||||
})
|
})
|
||||||
.array()
|
.array()
|
||||||
.optional(),
|
.optional(),
|
||||||
|
importedByEnvs: z
|
||||||
|
.object({
|
||||||
|
environment: z.string(),
|
||||||
|
importedBy: z
|
||||||
|
.object({
|
||||||
|
environment: z.object({
|
||||||
|
name: z.string(),
|
||||||
|
slug: z.string()
|
||||||
|
}),
|
||||||
|
folders: z
|
||||||
|
.object({
|
||||||
|
name: z.string(),
|
||||||
|
isImported: z.boolean(),
|
||||||
|
secrets: z
|
||||||
|
.object({
|
||||||
|
secretId: z.string(),
|
||||||
|
referencedSecretKey: z.string()
|
||||||
|
})
|
||||||
|
.array()
|
||||||
|
.optional()
|
||||||
|
})
|
||||||
|
.array()
|
||||||
|
})
|
||||||
|
.array()
|
||||||
|
.optional()
|
||||||
|
})
|
||||||
|
.array()
|
||||||
|
.optional(),
|
||||||
totalFolderCount: z.number().optional(),
|
totalFolderCount: z.number().optional(),
|
||||||
totalDynamicSecretCount: z.number().optional(),
|
totalDynamicSecretCount: z.number().optional(),
|
||||||
totalSecretCount: z.number().optional(),
|
totalSecretCount: z.number().optional(),
|
||||||
@@ -289,24 +313,7 @@ export const registerDashboardRouter = async (server: FastifyZodProvider) => {
|
|||||||
totalCount: totalFolderCount ?? 0
|
totalCount: totalFolderCount ?? 0
|
||||||
};
|
};
|
||||||
|
|
||||||
const { permission } = await server.services.permission.getProjectPermission({
|
if (includeDynamicSecrets) {
|
||||||
actor: req.permission.type,
|
|
||||||
actorId: req.permission.id,
|
|
||||||
projectId,
|
|
||||||
actorAuthMethod: req.permission.authMethod,
|
|
||||||
actorOrgId: req.permission.orgId,
|
|
||||||
actionProjectType: ActionProjectType.SecretManager
|
|
||||||
});
|
|
||||||
|
|
||||||
const allowedDynamicSecretEnvironments = // filter envs user has access to
|
|
||||||
environments.filter((environment) =>
|
|
||||||
permission.can(
|
|
||||||
ProjectPermissionDynamicSecretActions.Lease,
|
|
||||||
subject(ProjectPermissionSub.DynamicSecrets, { environment, secretPath })
|
|
||||||
)
|
|
||||||
);
|
|
||||||
|
|
||||||
if (includeDynamicSecrets && allowedDynamicSecretEnvironments.length) {
|
|
||||||
// this is the unique count, ie duplicate secrets across envs only count as 1
|
// this is the unique count, ie duplicate secrets across envs only count as 1
|
||||||
totalDynamicSecretCount = await server.services.dynamicSecret.getCountMultiEnv({
|
totalDynamicSecretCount = await server.services.dynamicSecret.getCountMultiEnv({
|
||||||
actor: req.permission.type,
|
actor: req.permission.type,
|
||||||
@@ -315,7 +322,7 @@ export const registerDashboardRouter = async (server: FastifyZodProvider) => {
|
|||||||
actorOrgId: req.permission.orgId,
|
actorOrgId: req.permission.orgId,
|
||||||
projectId,
|
projectId,
|
||||||
search,
|
search,
|
||||||
environmentSlugs: allowedDynamicSecretEnvironments,
|
environmentSlugs: environments,
|
||||||
path: secretPath,
|
path: secretPath,
|
||||||
isInternal: true
|
isInternal: true
|
||||||
});
|
});
|
||||||
@@ -330,7 +337,7 @@ export const registerDashboardRouter = async (server: FastifyZodProvider) => {
|
|||||||
search,
|
search,
|
||||||
orderBy,
|
orderBy,
|
||||||
orderDirection,
|
orderDirection,
|
||||||
environmentSlugs: allowedDynamicSecretEnvironments,
|
environmentSlugs: environments,
|
||||||
path: secretPath,
|
path: secretPath,
|
||||||
limit: remainingLimit,
|
limit: remainingLimit,
|
||||||
offset: adjustedOffset,
|
offset: adjustedOffset,
|
||||||
@@ -471,6 +478,28 @@ export const registerDashboardRouter = async (server: FastifyZodProvider) => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const importedByEnvs = [];
|
||||||
|
|
||||||
|
for await (const environment of environments) {
|
||||||
|
const importedBy = await server.services.secretImport.getFolderIsImportedBy({
|
||||||
|
path: secretPath,
|
||||||
|
environment,
|
||||||
|
projectId,
|
||||||
|
actor: req.permission.type,
|
||||||
|
actorId: req.permission.id,
|
||||||
|
actorAuthMethod: req.permission.authMethod,
|
||||||
|
actorOrgId: req.permission.orgId,
|
||||||
|
secrets: secrets?.filter((s) => s.environment === environment)
|
||||||
|
});
|
||||||
|
|
||||||
|
if (importedBy) {
|
||||||
|
importedByEnvs.push({
|
||||||
|
environment,
|
||||||
|
importedBy
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
folders,
|
folders,
|
||||||
dynamicSecrets,
|
dynamicSecrets,
|
||||||
@@ -482,6 +511,7 @@ export const registerDashboardRouter = async (server: FastifyZodProvider) => {
|
|||||||
totalImportCount,
|
totalImportCount,
|
||||||
totalSecretCount,
|
totalSecretCount,
|
||||||
totalSecretRotationCount,
|
totalSecretRotationCount,
|
||||||
|
importedByEnvs,
|
||||||
totalCount:
|
totalCount:
|
||||||
(totalFolderCount ?? 0) +
|
(totalFolderCount ?? 0) +
|
||||||
(totalDynamicSecretCount ?? 0) +
|
(totalDynamicSecretCount ?? 0) +
|
||||||
@@ -575,6 +605,28 @@ export const registerDashboardRouter = async (server: FastifyZodProvider) => {
|
|||||||
totalFolderCount: z.number().optional(),
|
totalFolderCount: z.number().optional(),
|
||||||
totalDynamicSecretCount: z.number().optional(),
|
totalDynamicSecretCount: z.number().optional(),
|
||||||
totalSecretCount: z.number().optional(),
|
totalSecretCount: z.number().optional(),
|
||||||
|
importedBy: z
|
||||||
|
.object({
|
||||||
|
environment: z.object({
|
||||||
|
name: z.string(),
|
||||||
|
slug: z.string()
|
||||||
|
}),
|
||||||
|
folders: z
|
||||||
|
.object({
|
||||||
|
name: z.string(),
|
||||||
|
isImported: z.boolean(),
|
||||||
|
secrets: z
|
||||||
|
.object({
|
||||||
|
secretId: z.string(),
|
||||||
|
referencedSecretKey: z.string()
|
||||||
|
})
|
||||||
|
.array()
|
||||||
|
.optional()
|
||||||
|
})
|
||||||
|
.array()
|
||||||
|
})
|
||||||
|
.array()
|
||||||
|
.optional(),
|
||||||
totalSecretRotationCount: z.number().optional(),
|
totalSecretRotationCount: z.number().optional(),
|
||||||
totalCount: z.number()
|
totalCount: z.number()
|
||||||
})
|
})
|
||||||
@@ -835,6 +887,17 @@ export const registerDashboardRouter = async (server: FastifyZodProvider) => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const importedBy = await server.services.secretImport.getFolderIsImportedBy({
|
||||||
|
path: secretPath,
|
||||||
|
environment,
|
||||||
|
projectId,
|
||||||
|
actor: req.permission.type,
|
||||||
|
actorId: req.permission.id,
|
||||||
|
actorAuthMethod: req.permission.authMethod,
|
||||||
|
actorOrgId: req.permission.orgId,
|
||||||
|
secrets
|
||||||
|
});
|
||||||
|
|
||||||
if (secrets?.length || secretRotations?.length) {
|
if (secrets?.length || secretRotations?.length) {
|
||||||
const secretCount =
|
const secretCount =
|
||||||
(secrets?.length ?? 0) +
|
(secrets?.length ?? 0) +
|
||||||
@@ -880,6 +943,7 @@ export const registerDashboardRouter = async (server: FastifyZodProvider) => {
|
|||||||
totalDynamicSecretCount,
|
totalDynamicSecretCount,
|
||||||
totalSecretCount,
|
totalSecretCount,
|
||||||
totalSecretRotationCount,
|
totalSecretRotationCount,
|
||||||
|
importedBy,
|
||||||
totalCount:
|
totalCount:
|
||||||
(totalImportCount ?? 0) +
|
(totalImportCount ?? 0) +
|
||||||
(totalFolderCount ?? 0) +
|
(totalFolderCount ?? 0) +
|
||||||
|
|||||||
@@ -31,6 +31,7 @@ export const registerIntegrationAuthRouter = async (server: FastifyZodProvider)
|
|||||||
.object({
|
.object({
|
||||||
name: z.string(),
|
name: z.string(),
|
||||||
slug: z.string(),
|
slug: z.string(),
|
||||||
|
syncSlug: z.string().optional(),
|
||||||
clientSlug: z.string().optional(),
|
clientSlug: z.string().optional(),
|
||||||
image: z.string(),
|
image: z.string(),
|
||||||
isAvailable: z.boolean().optional(),
|
isAvailable: z.boolean().optional(),
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import { registerDatabricksSyncRouter } from "./databricks-sync-router";
|
|||||||
import { registerGcpSyncRouter } from "./gcp-sync-router";
|
import { registerGcpSyncRouter } from "./gcp-sync-router";
|
||||||
import { registerGitHubSyncRouter } from "./github-sync-router";
|
import { registerGitHubSyncRouter } from "./github-sync-router";
|
||||||
import { registerHumanitecSyncRouter } from "./humanitec-sync-router";
|
import { registerHumanitecSyncRouter } from "./humanitec-sync-router";
|
||||||
|
import { registerTerraformCloudSyncRouter } from "./terraform-cloud-sync-router";
|
||||||
import { registerVercelSyncRouter } from "./vercel-sync-router";
|
import { registerVercelSyncRouter } from "./vercel-sync-router";
|
||||||
|
|
||||||
export * from "./secret-sync-router";
|
export * from "./secret-sync-router";
|
||||||
@@ -22,6 +23,7 @@ export const SECRET_SYNC_REGISTER_ROUTER_MAP: Record<SecretSync, (server: Fastif
|
|||||||
[SecretSync.AzureAppConfiguration]: registerAzureAppConfigurationSyncRouter,
|
[SecretSync.AzureAppConfiguration]: registerAzureAppConfigurationSyncRouter,
|
||||||
[SecretSync.Databricks]: registerDatabricksSyncRouter,
|
[SecretSync.Databricks]: registerDatabricksSyncRouter,
|
||||||
[SecretSync.Humanitec]: registerHumanitecSyncRouter,
|
[SecretSync.Humanitec]: registerHumanitecSyncRouter,
|
||||||
|
[SecretSync.TerraformCloud]: registerTerraformCloudSyncRouter,
|
||||||
[SecretSync.Camunda]: registerCamundaSyncRouter,
|
[SecretSync.Camunda]: registerCamundaSyncRouter,
|
||||||
[SecretSync.Vercel]: registerVercelSyncRouter
|
[SecretSync.Vercel]: registerVercelSyncRouter
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -23,6 +23,7 @@ import { DatabricksSyncListItemSchema, DatabricksSyncSchema } from "@app/service
|
|||||||
import { GcpSyncListItemSchema, GcpSyncSchema } from "@app/services/secret-sync/gcp";
|
import { GcpSyncListItemSchema, GcpSyncSchema } from "@app/services/secret-sync/gcp";
|
||||||
import { GitHubSyncListItemSchema, GitHubSyncSchema } from "@app/services/secret-sync/github";
|
import { GitHubSyncListItemSchema, GitHubSyncSchema } from "@app/services/secret-sync/github";
|
||||||
import { HumanitecSyncListItemSchema, HumanitecSyncSchema } from "@app/services/secret-sync/humanitec";
|
import { HumanitecSyncListItemSchema, HumanitecSyncSchema } from "@app/services/secret-sync/humanitec";
|
||||||
|
import { TerraformCloudSyncListItemSchema, TerraformCloudSyncSchema } from "@app/services/secret-sync/terraform-cloud";
|
||||||
import { VercelSyncListItemSchema, VercelSyncSchema } from "@app/services/secret-sync/vercel";
|
import { VercelSyncListItemSchema, VercelSyncSchema } from "@app/services/secret-sync/vercel";
|
||||||
|
|
||||||
const SecretSyncSchema = z.discriminatedUnion("destination", [
|
const SecretSyncSchema = z.discriminatedUnion("destination", [
|
||||||
@@ -34,6 +35,7 @@ const SecretSyncSchema = z.discriminatedUnion("destination", [
|
|||||||
AzureAppConfigurationSyncSchema,
|
AzureAppConfigurationSyncSchema,
|
||||||
DatabricksSyncSchema,
|
DatabricksSyncSchema,
|
||||||
HumanitecSyncSchema,
|
HumanitecSyncSchema,
|
||||||
|
TerraformCloudSyncSchema,
|
||||||
CamundaSyncSchema,
|
CamundaSyncSchema,
|
||||||
VercelSyncSchema
|
VercelSyncSchema
|
||||||
]);
|
]);
|
||||||
@@ -47,6 +49,7 @@ const SecretSyncOptionsSchema = z.discriminatedUnion("destination", [
|
|||||||
AzureAppConfigurationSyncListItemSchema,
|
AzureAppConfigurationSyncListItemSchema,
|
||||||
DatabricksSyncListItemSchema,
|
DatabricksSyncListItemSchema,
|
||||||
HumanitecSyncListItemSchema,
|
HumanitecSyncListItemSchema,
|
||||||
|
TerraformCloudSyncListItemSchema,
|
||||||
CamundaSyncListItemSchema,
|
CamundaSyncListItemSchema,
|
||||||
VercelSyncListItemSchema
|
VercelSyncListItemSchema
|
||||||
]);
|
]);
|
||||||
|
|||||||
@@ -0,0 +1,17 @@
|
|||||||
|
import { SecretSync } from "@app/services/secret-sync/secret-sync-enums";
|
||||||
|
import {
|
||||||
|
CreateTerraformCloudSyncSchema,
|
||||||
|
TerraformCloudSyncSchema,
|
||||||
|
UpdateTerraformCloudSyncSchema
|
||||||
|
} from "@app/services/secret-sync/terraform-cloud";
|
||||||
|
|
||||||
|
import { registerSyncSecretsEndpoints } from "./secret-sync-endpoints";
|
||||||
|
|
||||||
|
export const registerTerraformCloudSyncRouter = async (server: FastifyZodProvider) =>
|
||||||
|
registerSyncSecretsEndpoints({
|
||||||
|
destination: SecretSync.TerraformCloud,
|
||||||
|
server,
|
||||||
|
responseSchema: TerraformCloudSyncSchema,
|
||||||
|
createSchema: CreateTerraformCloudSyncSchema,
|
||||||
|
updateSchema: UpdateTerraformCloudSyncSchema
|
||||||
|
});
|
||||||
@@ -108,7 +108,7 @@ export const registerSsoRouter = async (server: FastifyZodProvider) => {
|
|||||||
const { email } = ghEmails.filter((gitHubEmail) => gitHubEmail.primary)[0];
|
const { email } = ghEmails.filter((gitHubEmail) => gitHubEmail.primary)[0];
|
||||||
const { isUserCompleted, providerAuthToken } = await server.services.login.oauth2Login({
|
const { isUserCompleted, providerAuthToken } = await server.services.login.oauth2Login({
|
||||||
email,
|
email,
|
||||||
firstName: profile.displayName,
|
firstName: profile.displayName || profile.username || "",
|
||||||
lastName: "",
|
lastName: "",
|
||||||
authMethod: AuthMethod.GITHUB,
|
authMethod: AuthMethod.GITHUB,
|
||||||
callbackPort
|
callbackPort
|
||||||
@@ -145,7 +145,7 @@ export const registerSsoRouter = async (server: FastifyZodProvider) => {
|
|||||||
const email = profile.emails[0].value;
|
const email = profile.emails[0].value;
|
||||||
const { isUserCompleted, providerAuthToken } = await server.services.login.oauth2Login({
|
const { isUserCompleted, providerAuthToken } = await server.services.login.oauth2Login({
|
||||||
email,
|
email,
|
||||||
firstName: profile.displayName,
|
firstName: profile.displayName || profile.username || "",
|
||||||
lastName: "",
|
lastName: "",
|
||||||
authMethod: AuthMethod.GITLAB,
|
authMethod: AuthMethod.GITLAB,
|
||||||
callbackPort
|
callbackPort
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ export enum AppConnection {
|
|||||||
AzureKeyVault = "azure-key-vault",
|
AzureKeyVault = "azure-key-vault",
|
||||||
AzureAppConfiguration = "azure-app-configuration",
|
AzureAppConfiguration = "azure-app-configuration",
|
||||||
Humanitec = "humanitec",
|
Humanitec = "humanitec",
|
||||||
|
TerraformCloud = "terraform-cloud",
|
||||||
Vercel = "vercel",
|
Vercel = "vercel",
|
||||||
Postgres = "postgres",
|
Postgres = "postgres",
|
||||||
MsSql = "mssql",
|
MsSql = "mssql",
|
||||||
|
|||||||
@@ -43,6 +43,11 @@ import {
|
|||||||
} from "./humanitec";
|
} from "./humanitec";
|
||||||
import { getMsSqlConnectionListItem, MsSqlConnectionMethod } from "./mssql";
|
import { getMsSqlConnectionListItem, MsSqlConnectionMethod } from "./mssql";
|
||||||
import { getPostgresConnectionListItem, PostgresConnectionMethod } from "./postgres";
|
import { getPostgresConnectionListItem, PostgresConnectionMethod } from "./postgres";
|
||||||
|
import {
|
||||||
|
getTerraformCloudConnectionListItem,
|
||||||
|
TerraformCloudConnectionMethod,
|
||||||
|
validateTerraformCloudConnectionCredentials
|
||||||
|
} from "./terraform-cloud";
|
||||||
import { VercelConnectionMethod } from "./vercel";
|
import { VercelConnectionMethod } from "./vercel";
|
||||||
import { getVercelConnectionListItem, validateVercelConnectionCredentials } from "./vercel/vercel-connection-fns";
|
import { getVercelConnectionListItem, validateVercelConnectionCredentials } from "./vercel/vercel-connection-fns";
|
||||||
|
|
||||||
@@ -55,6 +60,7 @@ export const listAppConnectionOptions = () => {
|
|||||||
getAzureAppConfigurationConnectionListItem(),
|
getAzureAppConfigurationConnectionListItem(),
|
||||||
getDatabricksConnectionListItem(),
|
getDatabricksConnectionListItem(),
|
||||||
getHumanitecConnectionListItem(),
|
getHumanitecConnectionListItem(),
|
||||||
|
getTerraformCloudConnectionListItem(),
|
||||||
getVercelConnectionListItem(),
|
getVercelConnectionListItem(),
|
||||||
getPostgresConnectionListItem(),
|
getPostgresConnectionListItem(),
|
||||||
getMsSqlConnectionListItem(),
|
getMsSqlConnectionListItem(),
|
||||||
@@ -121,6 +127,7 @@ export const validateAppConnectionCredentials = async (
|
|||||||
[AppConnection.MsSql]: validateSqlConnectionCredentials as TAppConnectionCredentialsValidator,
|
[AppConnection.MsSql]: validateSqlConnectionCredentials as TAppConnectionCredentialsValidator,
|
||||||
[AppConnection.Camunda]: validateCamundaConnectionCredentials as TAppConnectionCredentialsValidator,
|
[AppConnection.Camunda]: validateCamundaConnectionCredentials as TAppConnectionCredentialsValidator,
|
||||||
[AppConnection.Vercel]: validateVercelConnectionCredentials as TAppConnectionCredentialsValidator,
|
[AppConnection.Vercel]: validateVercelConnectionCredentials as TAppConnectionCredentialsValidator,
|
||||||
|
[AppConnection.TerraformCloud]: validateTerraformCloudConnectionCredentials as TAppConnectionCredentialsValidator,
|
||||||
[AppConnection.Auth0]: validateAuth0ConnectionCredentials as TAppConnectionCredentialsValidator
|
[AppConnection.Auth0]: validateAuth0ConnectionCredentials as TAppConnectionCredentialsValidator
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -146,6 +153,7 @@ export const getAppConnectionMethodName = (method: TAppConnection["method"]) =>
|
|||||||
case CamundaConnectionMethod.ClientCredentials:
|
case CamundaConnectionMethod.ClientCredentials:
|
||||||
return "Client Credentials";
|
return "Client Credentials";
|
||||||
case HumanitecConnectionMethod.ApiToken:
|
case HumanitecConnectionMethod.ApiToken:
|
||||||
|
case TerraformCloudConnectionMethod.ApiToken:
|
||||||
case VercelConnectionMethod.ApiToken:
|
case VercelConnectionMethod.ApiToken:
|
||||||
return "API Token";
|
return "API Token";
|
||||||
case PostgresConnectionMethod.UsernameAndPassword:
|
case PostgresConnectionMethod.UsernameAndPassword:
|
||||||
@@ -193,6 +201,7 @@ export const TRANSITION_CONNECTION_CREDENTIALS_TO_PLATFORM: Record<
|
|||||||
[AppConnection.Humanitec]: platformManagedCredentialsNotSupported,
|
[AppConnection.Humanitec]: platformManagedCredentialsNotSupported,
|
||||||
[AppConnection.Postgres]: transferSqlConnectionCredentialsToPlatform as TAppConnectionTransitionCredentialsToPlatform,
|
[AppConnection.Postgres]: transferSqlConnectionCredentialsToPlatform as TAppConnectionTransitionCredentialsToPlatform,
|
||||||
[AppConnection.MsSql]: transferSqlConnectionCredentialsToPlatform as TAppConnectionTransitionCredentialsToPlatform,
|
[AppConnection.MsSql]: transferSqlConnectionCredentialsToPlatform as TAppConnectionTransitionCredentialsToPlatform,
|
||||||
|
[AppConnection.TerraformCloud]: platformManagedCredentialsNotSupported,
|
||||||
[AppConnection.Camunda]: platformManagedCredentialsNotSupported,
|
[AppConnection.Camunda]: platformManagedCredentialsNotSupported,
|
||||||
[AppConnection.Vercel]: platformManagedCredentialsNotSupported,
|
[AppConnection.Vercel]: platformManagedCredentialsNotSupported,
|
||||||
[AppConnection.Auth0]: platformManagedCredentialsNotSupported
|
[AppConnection.Auth0]: platformManagedCredentialsNotSupported
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ export const APP_CONNECTION_NAME_MAP: Record<AppConnection, string> = {
|
|||||||
[AppConnection.AzureAppConfiguration]: "Azure App Configuration",
|
[AppConnection.AzureAppConfiguration]: "Azure App Configuration",
|
||||||
[AppConnection.Databricks]: "Databricks",
|
[AppConnection.Databricks]: "Databricks",
|
||||||
[AppConnection.Humanitec]: "Humanitec",
|
[AppConnection.Humanitec]: "Humanitec",
|
||||||
|
[AppConnection.TerraformCloud]: "Terraform Cloud",
|
||||||
[AppConnection.Vercel]: "Vercel",
|
[AppConnection.Vercel]: "Vercel",
|
||||||
[AppConnection.Postgres]: "PostgreSQL",
|
[AppConnection.Postgres]: "PostgreSQL",
|
||||||
[AppConnection.MsSql]: "Microsoft SQL Server",
|
[AppConnection.MsSql]: "Microsoft SQL Server",
|
||||||
|
|||||||
@@ -45,6 +45,8 @@ import { ValidateHumanitecConnectionCredentialsSchema } from "./humanitec";
|
|||||||
import { humanitecConnectionService } from "./humanitec/humanitec-connection-service";
|
import { humanitecConnectionService } from "./humanitec/humanitec-connection-service";
|
||||||
import { ValidateMsSqlConnectionCredentialsSchema } from "./mssql";
|
import { ValidateMsSqlConnectionCredentialsSchema } from "./mssql";
|
||||||
import { ValidatePostgresConnectionCredentialsSchema } from "./postgres";
|
import { ValidatePostgresConnectionCredentialsSchema } from "./postgres";
|
||||||
|
import { ValidateTerraformCloudConnectionCredentialsSchema } from "./terraform-cloud";
|
||||||
|
import { terraformCloudConnectionService } from "./terraform-cloud/terraform-cloud-connection-service";
|
||||||
import { ValidateVercelConnectionCredentialsSchema } from "./vercel";
|
import { ValidateVercelConnectionCredentialsSchema } from "./vercel";
|
||||||
import { vercelConnectionService } from "./vercel/vercel-connection-service";
|
import { vercelConnectionService } from "./vercel/vercel-connection-service";
|
||||||
|
|
||||||
@@ -64,6 +66,7 @@ const VALIDATE_APP_CONNECTION_CREDENTIALS_MAP: Record<AppConnection, TValidateAp
|
|||||||
[AppConnection.AzureAppConfiguration]: ValidateAzureAppConfigurationConnectionCredentialsSchema,
|
[AppConnection.AzureAppConfiguration]: ValidateAzureAppConfigurationConnectionCredentialsSchema,
|
||||||
[AppConnection.Databricks]: ValidateDatabricksConnectionCredentialsSchema,
|
[AppConnection.Databricks]: ValidateDatabricksConnectionCredentialsSchema,
|
||||||
[AppConnection.Humanitec]: ValidateHumanitecConnectionCredentialsSchema,
|
[AppConnection.Humanitec]: ValidateHumanitecConnectionCredentialsSchema,
|
||||||
|
[AppConnection.TerraformCloud]: ValidateTerraformCloudConnectionCredentialsSchema,
|
||||||
[AppConnection.Vercel]: ValidateVercelConnectionCredentialsSchema,
|
[AppConnection.Vercel]: ValidateVercelConnectionCredentialsSchema,
|
||||||
[AppConnection.Postgres]: ValidatePostgresConnectionCredentialsSchema,
|
[AppConnection.Postgres]: ValidatePostgresConnectionCredentialsSchema,
|
||||||
[AppConnection.MsSql]: ValidateMsSqlConnectionCredentialsSchema,
|
[AppConnection.MsSql]: ValidateMsSqlConnectionCredentialsSchema,
|
||||||
@@ -440,6 +443,7 @@ export const appConnectionServiceFactory = ({
|
|||||||
databricks: databricksConnectionService(connectAppConnectionById, appConnectionDAL, kmsService),
|
databricks: databricksConnectionService(connectAppConnectionById, appConnectionDAL, kmsService),
|
||||||
aws: awsConnectionService(connectAppConnectionById),
|
aws: awsConnectionService(connectAppConnectionById),
|
||||||
humanitec: humanitecConnectionService(connectAppConnectionById),
|
humanitec: humanitecConnectionService(connectAppConnectionById),
|
||||||
|
terraformCloud: terraformCloudConnectionService(connectAppConnectionById),
|
||||||
camunda: camundaConnectionService(connectAppConnectionById, appConnectionDAL, kmsService),
|
camunda: camundaConnectionService(connectAppConnectionById, appConnectionDAL, kmsService),
|
||||||
vercel: vercelConnectionService(connectAppConnectionById),
|
vercel: vercelConnectionService(connectAppConnectionById),
|
||||||
auth0: auth0ConnectionService(connectAppConnectionById, appConnectionDAL, kmsService)
|
auth0: auth0ConnectionService(connectAppConnectionById, appConnectionDAL, kmsService)
|
||||||
|
|||||||
@@ -63,6 +63,12 @@ import {
|
|||||||
TPostgresConnectionInput,
|
TPostgresConnectionInput,
|
||||||
TValidatePostgresConnectionCredentialsSchema
|
TValidatePostgresConnectionCredentialsSchema
|
||||||
} from "./postgres";
|
} from "./postgres";
|
||||||
|
import {
|
||||||
|
TTerraformCloudConnection,
|
||||||
|
TTerraformCloudConnectionConfig,
|
||||||
|
TTerraformCloudConnectionInput,
|
||||||
|
TValidateTerraformCloudConnectionCredentialsSchema
|
||||||
|
} from "./terraform-cloud";
|
||||||
import {
|
import {
|
||||||
TValidateVercelConnectionCredentialsSchema,
|
TValidateVercelConnectionCredentialsSchema,
|
||||||
TVercelConnection,
|
TVercelConnection,
|
||||||
@@ -78,6 +84,7 @@ export type TAppConnection = { id: string } & (
|
|||||||
| TAzureAppConfigurationConnection
|
| TAzureAppConfigurationConnection
|
||||||
| TDatabricksConnection
|
| TDatabricksConnection
|
||||||
| THumanitecConnection
|
| THumanitecConnection
|
||||||
|
| TTerraformCloudConnection
|
||||||
| TVercelConnection
|
| TVercelConnection
|
||||||
| TPostgresConnection
|
| TPostgresConnection
|
||||||
| TMsSqlConnection
|
| TMsSqlConnection
|
||||||
@@ -97,6 +104,7 @@ export type TAppConnectionInput = { id: string } & (
|
|||||||
| TAzureAppConfigurationConnectionInput
|
| TAzureAppConfigurationConnectionInput
|
||||||
| TDatabricksConnectionInput
|
| TDatabricksConnectionInput
|
||||||
| THumanitecConnectionInput
|
| THumanitecConnectionInput
|
||||||
|
| TTerraformCloudConnectionInput
|
||||||
| TVercelConnectionInput
|
| TVercelConnectionInput
|
||||||
| TPostgresConnectionInput
|
| TPostgresConnectionInput
|
||||||
| TMsSqlConnectionInput
|
| TMsSqlConnectionInput
|
||||||
@@ -123,6 +131,7 @@ export type TAppConnectionConfig =
|
|||||||
| TAzureAppConfigurationConnectionConfig
|
| TAzureAppConfigurationConnectionConfig
|
||||||
| TDatabricksConnectionConfig
|
| TDatabricksConnectionConfig
|
||||||
| THumanitecConnectionConfig
|
| THumanitecConnectionConfig
|
||||||
|
| TTerraformCloudConnectionConfig
|
||||||
| TSqlConnectionConfig
|
| TSqlConnectionConfig
|
||||||
| TCamundaConnectionConfig
|
| TCamundaConnectionConfig
|
||||||
| TVercelConnectionConfig
|
| TVercelConnectionConfig
|
||||||
@@ -140,6 +149,7 @@ export type TValidateAppConnectionCredentialsSchema =
|
|||||||
| TValidateMsSqlConnectionCredentialsSchema
|
| TValidateMsSqlConnectionCredentialsSchema
|
||||||
| TValidateCamundaConnectionCredentialsSchema
|
| TValidateCamundaConnectionCredentialsSchema
|
||||||
| TValidateVercelConnectionCredentialsSchema
|
| TValidateVercelConnectionCredentialsSchema
|
||||||
|
| TValidateTerraformCloudConnectionCredentialsSchema
|
||||||
| TValidateAuth0ConnectionCredentialsSchema;
|
| TValidateAuth0ConnectionCredentialsSchema;
|
||||||
|
|
||||||
export type TListAwsConnectionKmsKeys = {
|
export type TListAwsConnectionKmsKeys = {
|
||||||
|
|||||||
@@ -0,0 +1,4 @@
|
|||||||
|
export * from "./terraform-cloud-connection-enums";
|
||||||
|
export * from "./terraform-cloud-connection-fns";
|
||||||
|
export * from "./terraform-cloud-connection-schemas";
|
||||||
|
export * from "./terraform-cloud-connection-types";
|
||||||
+3
@@ -0,0 +1,3 @@
|
|||||||
|
export enum TerraformCloudConnectionMethod {
|
||||||
|
ApiToken = "api-token"
|
||||||
|
}
|
||||||
+135
@@ -0,0 +1,135 @@
|
|||||||
|
import { AxiosError, AxiosResponse } from "axios";
|
||||||
|
|
||||||
|
import { request } from "@app/lib/config/request";
|
||||||
|
import { BadRequestError, InternalServerError } from "@app/lib/errors";
|
||||||
|
import { AppConnection } from "@app/services/app-connection/app-connection-enums";
|
||||||
|
import { IntegrationUrls } from "@app/services/integration-auth/integration-list";
|
||||||
|
|
||||||
|
import { TerraformCloudConnectionMethod } from "./terraform-cloud-connection-enums";
|
||||||
|
import {
|
||||||
|
TTerraformCloudConnection,
|
||||||
|
TTerraformCloudConnectionConfig,
|
||||||
|
TTerraformCloudOrganization,
|
||||||
|
TTerraformCloudVariableSet,
|
||||||
|
TTerraformCloudWorkspace
|
||||||
|
} from "./terraform-cloud-connection-types";
|
||||||
|
|
||||||
|
export const getTerraformCloudConnectionListItem = () => {
|
||||||
|
return {
|
||||||
|
name: "Terraform Cloud" as const,
|
||||||
|
app: AppConnection.TerraformCloud as const,
|
||||||
|
methods: Object.values(TerraformCloudConnectionMethod) as [TerraformCloudConnectionMethod.ApiToken]
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export const validateTerraformCloudConnectionCredentials = async (config: TTerraformCloudConnectionConfig) => {
|
||||||
|
const { credentials: inputCredentials } = config;
|
||||||
|
|
||||||
|
let response: AxiosResponse<{ data: TTerraformCloudOrganization[] }> | null = null;
|
||||||
|
|
||||||
|
try {
|
||||||
|
response = await request.get<{ data: TTerraformCloudOrganization[] }>(
|
||||||
|
`${IntegrationUrls.TERRAFORM_CLOUD_API_URL}/api/v2/organizations`,
|
||||||
|
{
|
||||||
|
headers: {
|
||||||
|
Authorization: `Bearer ${inputCredentials.apiToken}`,
|
||||||
|
"Content-Type": "application/vnd.api+json"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
} catch (error: unknown) {
|
||||||
|
if (error instanceof AxiosError) {
|
||||||
|
throw new BadRequestError({
|
||||||
|
message: `Failed to validate credentials: ${error.message || "Unknown error"}`
|
||||||
|
});
|
||||||
|
}
|
||||||
|
throw new BadRequestError({
|
||||||
|
message: "Unable to validate connection - verify credentials"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!response?.data) {
|
||||||
|
throw new InternalServerError({
|
||||||
|
message: "Failed to get organizations: Response was empty"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return inputCredentials;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const listOrganizations = async (
|
||||||
|
appConnection: TTerraformCloudConnection
|
||||||
|
): Promise<TTerraformCloudOrganization[]> => {
|
||||||
|
const {
|
||||||
|
credentials: { apiToken }
|
||||||
|
} = appConnection;
|
||||||
|
|
||||||
|
const headers = {
|
||||||
|
Authorization: `Bearer ${apiToken}`,
|
||||||
|
"Content-Type": "application/vnd.api+json"
|
||||||
|
};
|
||||||
|
|
||||||
|
const fetchAllPages = async <T>(url: string): Promise<T[]> => {
|
||||||
|
let results: T[] = [];
|
||||||
|
let nextUrl: string | null = url;
|
||||||
|
|
||||||
|
while (nextUrl) {
|
||||||
|
// eslint-disable-next-line no-await-in-loop
|
||||||
|
const res: AxiosResponse<{ data: T[]; links?: { next?: string } }> = await request.get(nextUrl, { headers });
|
||||||
|
results = results.concat(res.data.data);
|
||||||
|
nextUrl = res.data.links?.next || null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return results;
|
||||||
|
};
|
||||||
|
|
||||||
|
const orgEntities = await fetchAllPages<{ id: string; attributes: { name: string } }>(
|
||||||
|
`${IntegrationUrls.TERRAFORM_CLOUD_API_URL}/api/v2/organizations`
|
||||||
|
);
|
||||||
|
|
||||||
|
const orgsWithVariableSetsAndWorkspaces: TTerraformCloudOrganization[] = [];
|
||||||
|
|
||||||
|
const variableSetPromises = orgEntities.map((org) =>
|
||||||
|
fetchAllPages<{ id: string; attributes: { name: string; description?: string; global?: boolean } }>(
|
||||||
|
`${IntegrationUrls.TERRAFORM_CLOUD_API_URL}/api/v2/organizations/${org.id}/varsets`
|
||||||
|
).catch(() => [])
|
||||||
|
);
|
||||||
|
|
||||||
|
const workspacePromises = orgEntities.map((org) =>
|
||||||
|
fetchAllPages<{ id: string; attributes: { name: string } }>(
|
||||||
|
`${IntegrationUrls.TERRAFORM_CLOUD_API_URL}/api/v2/organizations/${org.id}/workspaces`
|
||||||
|
).catch(() => [])
|
||||||
|
);
|
||||||
|
|
||||||
|
const [variableSetResults, workspaceResults] = await Promise.all([
|
||||||
|
Promise.all(variableSetPromises),
|
||||||
|
Promise.all(workspacePromises)
|
||||||
|
]);
|
||||||
|
|
||||||
|
for (let i = 0; i < orgEntities.length; i += 1) {
|
||||||
|
const org = orgEntities[i];
|
||||||
|
const variableSetsData = variableSetResults[i];
|
||||||
|
const workspacesData = workspaceResults[i];
|
||||||
|
|
||||||
|
const variableSets: TTerraformCloudVariableSet[] = variableSetsData.map((varSet) => ({
|
||||||
|
id: varSet.id,
|
||||||
|
name: varSet.attributes.name,
|
||||||
|
description: varSet.attributes.description,
|
||||||
|
global: varSet.attributes.global
|
||||||
|
}));
|
||||||
|
|
||||||
|
const workspaces: TTerraformCloudWorkspace[] = workspacesData.map((workspace) => ({
|
||||||
|
id: workspace.id,
|
||||||
|
name: workspace.attributes.name
|
||||||
|
}));
|
||||||
|
|
||||||
|
orgsWithVariableSetsAndWorkspaces.push({
|
||||||
|
id: org.id,
|
||||||
|
name: org.attributes.name,
|
||||||
|
variableSets,
|
||||||
|
workspaces
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return orgsWithVariableSetsAndWorkspaces;
|
||||||
|
};
|
||||||
+60
@@ -0,0 +1,60 @@
|
|||||||
|
import z from "zod";
|
||||||
|
|
||||||
|
import { AppConnections } from "@app/lib/api-docs";
|
||||||
|
import { AppConnection } from "@app/services/app-connection/app-connection-enums";
|
||||||
|
import {
|
||||||
|
BaseAppConnectionSchema,
|
||||||
|
GenericCreateAppConnectionFieldsSchema,
|
||||||
|
GenericUpdateAppConnectionFieldsSchema
|
||||||
|
} from "@app/services/app-connection/app-connection-schemas";
|
||||||
|
|
||||||
|
import { TerraformCloudConnectionMethod } from "./terraform-cloud-connection-enums";
|
||||||
|
|
||||||
|
export const TerraformCloudConnectionAccessTokenCredentialsSchema = z.object({
|
||||||
|
apiToken: z.string().trim().min(1, "API Token required").describe(AppConnections.CREDENTIALS.TERRAFORM_CLOUD.apiToken)
|
||||||
|
});
|
||||||
|
|
||||||
|
const BaseTerraformCloudConnectionSchema = BaseAppConnectionSchema.extend({
|
||||||
|
app: z.literal(AppConnection.TerraformCloud)
|
||||||
|
});
|
||||||
|
|
||||||
|
export const TerraformCloudConnectionSchema = BaseTerraformCloudConnectionSchema.extend({
|
||||||
|
method: z.literal(TerraformCloudConnectionMethod.ApiToken),
|
||||||
|
credentials: TerraformCloudConnectionAccessTokenCredentialsSchema
|
||||||
|
});
|
||||||
|
|
||||||
|
export const SanitizedTerraformCloudConnectionSchema = z.discriminatedUnion("method", [
|
||||||
|
BaseTerraformCloudConnectionSchema.extend({
|
||||||
|
method: z.literal(TerraformCloudConnectionMethod.ApiToken),
|
||||||
|
credentials: TerraformCloudConnectionAccessTokenCredentialsSchema.pick({})
|
||||||
|
})
|
||||||
|
]);
|
||||||
|
|
||||||
|
export const ValidateTerraformCloudConnectionCredentialsSchema = z.discriminatedUnion("method", [
|
||||||
|
z.object({
|
||||||
|
method: z
|
||||||
|
.literal(TerraformCloudConnectionMethod.ApiToken)
|
||||||
|
.describe(AppConnections?.CREATE(AppConnection.TerraformCloud).method),
|
||||||
|
credentials: TerraformCloudConnectionAccessTokenCredentialsSchema.describe(
|
||||||
|
AppConnections.CREATE(AppConnection.TerraformCloud).credentials
|
||||||
|
)
|
||||||
|
})
|
||||||
|
]);
|
||||||
|
|
||||||
|
export const CreateTerraformCloudConnectionSchema = ValidateTerraformCloudConnectionCredentialsSchema.and(
|
||||||
|
GenericCreateAppConnectionFieldsSchema(AppConnection.TerraformCloud)
|
||||||
|
);
|
||||||
|
|
||||||
|
export const UpdateTerraformCloudConnectionSchema = z
|
||||||
|
.object({
|
||||||
|
credentials: TerraformCloudConnectionAccessTokenCredentialsSchema.optional().describe(
|
||||||
|
AppConnections.UPDATE(AppConnection.TerraformCloud).credentials
|
||||||
|
)
|
||||||
|
})
|
||||||
|
.and(GenericUpdateAppConnectionFieldsSchema(AppConnection.TerraformCloud));
|
||||||
|
|
||||||
|
export const TerraformCloudConnectionListItemSchema = z.object({
|
||||||
|
name: z.literal("Terraform Cloud"),
|
||||||
|
app: z.literal(AppConnection.TerraformCloud),
|
||||||
|
methods: z.nativeEnum(TerraformCloudConnectionMethod).array()
|
||||||
|
});
|
||||||
+29
@@ -0,0 +1,29 @@
|
|||||||
|
import { logger } from "@app/lib/logger";
|
||||||
|
import { OrgServiceActor } from "@app/lib/types";
|
||||||
|
|
||||||
|
import { AppConnection } from "../app-connection-enums";
|
||||||
|
import { listOrganizations as getTerraformCloudOrganizations } from "./terraform-cloud-connection-fns";
|
||||||
|
import { TTerraformCloudConnection } from "./terraform-cloud-connection-types";
|
||||||
|
|
||||||
|
type TGetAppConnectionFunc = (
|
||||||
|
app: AppConnection,
|
||||||
|
connectionId: string,
|
||||||
|
actor: OrgServiceActor
|
||||||
|
) => Promise<TTerraformCloudConnection>;
|
||||||
|
|
||||||
|
export const terraformCloudConnectionService = (getAppConnection: TGetAppConnectionFunc) => {
|
||||||
|
const listOrganizations = async (connectionId: string, actor: OrgServiceActor) => {
|
||||||
|
const appConnection = await getAppConnection(AppConnection.TerraformCloud, connectionId, actor);
|
||||||
|
try {
|
||||||
|
const organizations = await getTerraformCloudOrganizations(appConnection);
|
||||||
|
return organizations;
|
||||||
|
} catch (error) {
|
||||||
|
logger.error(error, "Failed to establish connection with Terraform Cloud");
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
listOrganizations
|
||||||
|
};
|
||||||
|
};
|
||||||
+45
@@ -0,0 +1,45 @@
|
|||||||
|
import z from "zod";
|
||||||
|
|
||||||
|
import { DiscriminativePick } from "@app/lib/types";
|
||||||
|
|
||||||
|
import { AppConnection } from "../app-connection-enums";
|
||||||
|
import {
|
||||||
|
CreateTerraformCloudConnectionSchema,
|
||||||
|
TerraformCloudConnectionSchema,
|
||||||
|
ValidateTerraformCloudConnectionCredentialsSchema
|
||||||
|
} from "./terraform-cloud-connection-schemas";
|
||||||
|
|
||||||
|
export type TTerraformCloudConnection = z.infer<typeof TerraformCloudConnectionSchema>;
|
||||||
|
|
||||||
|
export type TTerraformCloudConnectionInput = z.infer<typeof CreateTerraformCloudConnectionSchema> & {
|
||||||
|
app: AppConnection.TerraformCloud;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type TValidateTerraformCloudConnectionCredentialsSchema =
|
||||||
|
typeof ValidateTerraformCloudConnectionCredentialsSchema;
|
||||||
|
|
||||||
|
export type TTerraformCloudConnectionConfig = DiscriminativePick<
|
||||||
|
TTerraformCloudConnectionInput,
|
||||||
|
"method" | "app" | "credentials"
|
||||||
|
> & {
|
||||||
|
orgId: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type TTerraformCloudVariableSet = {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
description?: string;
|
||||||
|
global?: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type TTerraformCloudWorkspace = {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type TTerraformCloudOrganization = {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
variableSets: TTerraformCloudVariableSet[];
|
||||||
|
workspaces: TTerraformCloudWorkspace[];
|
||||||
|
};
|
||||||
@@ -3,12 +3,18 @@ import { ForbiddenError } from "@casl/ability";
|
|||||||
import { ActionProjectType, ProjectType } from "@app/db/schemas";
|
import { ActionProjectType, ProjectType } from "@app/db/schemas";
|
||||||
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service";
|
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service";
|
||||||
import { ProjectPermissionCmekActions, ProjectPermissionSub } from "@app/ee/services/permission/project-permission";
|
import { ProjectPermissionCmekActions, ProjectPermissionSub } from "@app/ee/services/permission/project-permission";
|
||||||
|
import { SigningAlgorithm } from "@app/lib/crypto/sign";
|
||||||
import { DatabaseErrorCode } from "@app/lib/error-codes";
|
import { DatabaseErrorCode } from "@app/lib/error-codes";
|
||||||
import { BadRequestError, DatabaseError, NotFoundError } from "@app/lib/errors";
|
import { BadRequestError, DatabaseError, NotFoundError } from "@app/lib/errors";
|
||||||
import { OrgServiceActor } from "@app/lib/types";
|
import { OrgServiceActor } from "@app/lib/types";
|
||||||
import {
|
import {
|
||||||
TCmekDecryptDTO,
|
TCmekDecryptDTO,
|
||||||
TCmekEncryptDTO,
|
TCmekEncryptDTO,
|
||||||
|
TCmekGetPublicKeyDTO,
|
||||||
|
TCmekKeyEncryptionAlgorithm,
|
||||||
|
TCmekListSigningAlgorithmsDTO,
|
||||||
|
TCmekSignDTO,
|
||||||
|
TCmekVerifyDTO,
|
||||||
TCreateCmekDTO,
|
TCreateCmekDTO,
|
||||||
TListCmeksByProjectIdDTO,
|
TListCmeksByProjectIdDTO,
|
||||||
TUpdabteCmekByIdDTO
|
TUpdabteCmekByIdDTO
|
||||||
@@ -16,6 +22,7 @@ import {
|
|||||||
import { TKmsKeyDALFactory } from "@app/services/kms/kms-key-dal";
|
import { TKmsKeyDALFactory } from "@app/services/kms/kms-key-dal";
|
||||||
import { TKmsServiceFactory } from "@app/services/kms/kms-service";
|
import { TKmsServiceFactory } from "@app/services/kms/kms-service";
|
||||||
|
|
||||||
|
import { KmsKeyUsage } from "../kms/kms-types";
|
||||||
import { TProjectDALFactory } from "../project/project-dal";
|
import { TProjectDALFactory } from "../project/project-dal";
|
||||||
|
|
||||||
type TCmekServiceFactoryDep = {
|
type TCmekServiceFactoryDep = {
|
||||||
@@ -221,7 +228,151 @@ export const cmekServiceFactory = ({ kmsService, kmsDAL, permissionService, proj
|
|||||||
|
|
||||||
const { cipherTextBlob } = await encrypt({ plainText: Buffer.from(plaintext, "base64") });
|
const { cipherTextBlob } = await encrypt({ plainText: Buffer.from(plaintext, "base64") });
|
||||||
|
|
||||||
return cipherTextBlob.toString("base64");
|
return {
|
||||||
|
ciphertext: cipherTextBlob.toString("base64"),
|
||||||
|
projectId: key.projectId
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
const listSigningAlgorithms = async ({ keyId }: TCmekListSigningAlgorithmsDTO, actor: OrgServiceActor) => {
|
||||||
|
const key = await kmsDAL.findCmekById(keyId);
|
||||||
|
|
||||||
|
if (!key) throw new NotFoundError({ message: `Key with ID "${keyId}" not found` });
|
||||||
|
if (!key.projectId || key.isReserved) throw new BadRequestError({ message: "Key is not customer managed" });
|
||||||
|
if (key.isDisabled) throw new BadRequestError({ message: "Key is disabled" });
|
||||||
|
|
||||||
|
const { permission } = await permissionService.getProjectPermission({
|
||||||
|
actor: actor.type,
|
||||||
|
actorId: actor.id,
|
||||||
|
projectId: key.projectId,
|
||||||
|
actorAuthMethod: actor.authMethod,
|
||||||
|
actorOrgId: actor.orgId,
|
||||||
|
actionProjectType: ActionProjectType.KMS
|
||||||
|
});
|
||||||
|
|
||||||
|
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionCmekActions.Read, ProjectPermissionSub.Cmek);
|
||||||
|
|
||||||
|
if (key.keyUsage !== KmsKeyUsage.SIGN_VERIFY) {
|
||||||
|
throw new BadRequestError({ message: `Key with ID '${keyId}' is not intended for signing` });
|
||||||
|
}
|
||||||
|
|
||||||
|
const encryptionAlgorithm = key.encryptionAlgorithm as TCmekKeyEncryptionAlgorithm;
|
||||||
|
|
||||||
|
const algos = [
|
||||||
|
{
|
||||||
|
keyAlgorithm: "rsa",
|
||||||
|
signingAlgorithms: Object.values(SigningAlgorithm).filter((algorithm) =>
|
||||||
|
algorithm.toLowerCase().startsWith("rsa")
|
||||||
|
)
|
||||||
|
},
|
||||||
|
{
|
||||||
|
keyAlgorithm: "ecc",
|
||||||
|
signingAlgorithms: Object.values(SigningAlgorithm).filter((algorithm) =>
|
||||||
|
algorithm.toLowerCase().startsWith("ecdsa")
|
||||||
|
)
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
const selectedAlgorithm = algos.find((algo) => encryptionAlgorithm.toLowerCase().startsWith(algo.keyAlgorithm));
|
||||||
|
|
||||||
|
if (!selectedAlgorithm) {
|
||||||
|
throw new BadRequestError({ message: `Unsupported encryption algorithm: ${encryptionAlgorithm}` });
|
||||||
|
}
|
||||||
|
|
||||||
|
return { signingAlgorithms: selectedAlgorithm.signingAlgorithms, projectId: key.projectId };
|
||||||
|
};
|
||||||
|
|
||||||
|
const getPublicKey = async ({ keyId }: TCmekGetPublicKeyDTO, actor: OrgServiceActor) => {
|
||||||
|
const key = await kmsDAL.findCmekById(keyId);
|
||||||
|
|
||||||
|
if (!key) throw new NotFoundError({ message: `Key with ID "${keyId}" not found` });
|
||||||
|
if (!key.projectId || key.isReserved) throw new BadRequestError({ message: "Key is not customer managed" });
|
||||||
|
if (key.isDisabled) throw new BadRequestError({ message: "Key is disabled" });
|
||||||
|
|
||||||
|
const { permission } = await permissionService.getProjectPermission({
|
||||||
|
actor: actor.type,
|
||||||
|
actorId: actor.id,
|
||||||
|
projectId: key.projectId,
|
||||||
|
actorAuthMethod: actor.authMethod,
|
||||||
|
actorOrgId: actor.orgId,
|
||||||
|
actionProjectType: ActionProjectType.KMS
|
||||||
|
});
|
||||||
|
|
||||||
|
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionCmekActions.Read, ProjectPermissionSub.Cmek);
|
||||||
|
|
||||||
|
const publicKey = await kmsService.getPublicKey({ kmsId: keyId });
|
||||||
|
return { publicKey: publicKey.toString("base64"), projectId: key.projectId };
|
||||||
|
};
|
||||||
|
|
||||||
|
const cmekSign = async ({ keyId, data, signingAlgorithm, isDigest }: TCmekSignDTO, actor: OrgServiceActor) => {
|
||||||
|
const key = await kmsDAL.findCmekById(keyId);
|
||||||
|
|
||||||
|
if (!key) throw new NotFoundError({ message: `Key with ID "${keyId}" not found` });
|
||||||
|
|
||||||
|
if (!key.projectId || key.isReserved) throw new BadRequestError({ message: "Key is not customer managed" });
|
||||||
|
|
||||||
|
if (key.isDisabled) throw new BadRequestError({ message: "Key is disabled" });
|
||||||
|
|
||||||
|
const { permission } = await permissionService.getProjectPermission({
|
||||||
|
actor: actor.type,
|
||||||
|
actorId: actor.id,
|
||||||
|
projectId: key.projectId,
|
||||||
|
actorAuthMethod: actor.authMethod,
|
||||||
|
actorOrgId: actor.orgId,
|
||||||
|
actionProjectType: ActionProjectType.KMS
|
||||||
|
});
|
||||||
|
|
||||||
|
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionCmekActions.Sign, ProjectPermissionSub.Cmek);
|
||||||
|
|
||||||
|
const sign = await kmsService.signWithKmsKey({ kmsId: keyId });
|
||||||
|
|
||||||
|
const { signature, algorithm } = await sign({ data: Buffer.from(data, "base64"), signingAlgorithm, isDigest });
|
||||||
|
|
||||||
|
return {
|
||||||
|
signature: signature.toString("base64"),
|
||||||
|
keyId: key.id,
|
||||||
|
projectId: key.projectId,
|
||||||
|
signingAlgorithm: algorithm
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
const cmekVerify = async (
|
||||||
|
{ keyId, data, signature, signingAlgorithm, isDigest }: TCmekVerifyDTO,
|
||||||
|
actor: OrgServiceActor
|
||||||
|
) => {
|
||||||
|
const key = await kmsDAL.findCmekById(keyId);
|
||||||
|
|
||||||
|
if (!key) throw new NotFoundError({ message: `Key with ID "${keyId}" not found` });
|
||||||
|
|
||||||
|
if (!key.projectId || key.isReserved) throw new BadRequestError({ message: "Key is not customer managed" });
|
||||||
|
|
||||||
|
if (key.isDisabled) throw new BadRequestError({ message: "Key is disabled" });
|
||||||
|
|
||||||
|
const { permission } = await permissionService.getProjectPermission({
|
||||||
|
actor: actor.type,
|
||||||
|
actorId: actor.id,
|
||||||
|
projectId: key.projectId,
|
||||||
|
actorAuthMethod: actor.authMethod,
|
||||||
|
actorOrgId: actor.orgId,
|
||||||
|
actionProjectType: ActionProjectType.KMS
|
||||||
|
});
|
||||||
|
|
||||||
|
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionCmekActions.Verify, ProjectPermissionSub.Cmek);
|
||||||
|
|
||||||
|
const verify = await kmsService.verifyWithKmsKey({ kmsId: keyId, signingAlgorithm });
|
||||||
|
|
||||||
|
const { signatureValid, algorithm } = await verify({
|
||||||
|
isDigest,
|
||||||
|
data: Buffer.from(data, "base64"),
|
||||||
|
signature: Buffer.from(signature, "base64")
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
signatureValid,
|
||||||
|
keyId: key.id,
|
||||||
|
projectId: key.projectId,
|
||||||
|
signingAlgorithm: algorithm
|
||||||
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
const cmekDecrypt = async ({ keyId, ciphertext }: TCmekDecryptDTO, actor: OrgServiceActor) => {
|
const cmekDecrypt = async ({ keyId, ciphertext }: TCmekDecryptDTO, actor: OrgServiceActor) => {
|
||||||
@@ -248,7 +399,10 @@ export const cmekServiceFactory = ({ kmsService, kmsDAL, permissionService, proj
|
|||||||
|
|
||||||
const plaintextBlob = await decrypt({ cipherTextBlob: Buffer.from(ciphertext, "base64") });
|
const plaintextBlob = await decrypt({ cipherTextBlob: Buffer.from(ciphertext, "base64") });
|
||||||
|
|
||||||
return plaintextBlob.toString("base64");
|
return {
|
||||||
|
plaintext: plaintextBlob.toString("base64"),
|
||||||
|
projectId: key.projectId
|
||||||
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
return {
|
return {
|
||||||
@@ -259,6 +413,10 @@ export const cmekServiceFactory = ({ kmsService, kmsDAL, permissionService, proj
|
|||||||
cmekEncrypt,
|
cmekEncrypt,
|
||||||
cmekDecrypt,
|
cmekDecrypt,
|
||||||
findCmekById,
|
findCmekById,
|
||||||
findCmekByName
|
findCmekByName,
|
||||||
|
cmekSign,
|
||||||
|
cmekVerify,
|
||||||
|
listSigningAlgorithms,
|
||||||
|
getPublicKey
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,12 +1,18 @@
|
|||||||
import { SymmetricEncryption } from "@app/lib/crypto/cipher";
|
import { SymmetricKeyAlgorithm } from "@app/lib/crypto/cipher";
|
||||||
|
import { AsymmetricKeyAlgorithm, SigningAlgorithm } from "@app/lib/crypto/sign";
|
||||||
import { OrderByDirection } from "@app/lib/types";
|
import { OrderByDirection } from "@app/lib/types";
|
||||||
|
|
||||||
|
import { KmsKeyUsage } from "../kms/kms-types";
|
||||||
|
|
||||||
|
export type TCmekKeyEncryptionAlgorithm = SymmetricKeyAlgorithm | AsymmetricKeyAlgorithm;
|
||||||
|
|
||||||
export type TCreateCmekDTO = {
|
export type TCreateCmekDTO = {
|
||||||
orgId: string;
|
orgId: string;
|
||||||
projectId: string;
|
projectId: string;
|
||||||
name: string;
|
name: string;
|
||||||
description?: string;
|
description?: string;
|
||||||
encryptionAlgorithm: SymmetricEncryption;
|
encryptionAlgorithm: TCmekKeyEncryptionAlgorithm;
|
||||||
|
keyUsage: KmsKeyUsage;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type TUpdabteCmekByIdDTO = {
|
export type TUpdabteCmekByIdDTO = {
|
||||||
@@ -38,3 +44,26 @@ export type TCmekDecryptDTO = {
|
|||||||
export enum CmekOrderBy {
|
export enum CmekOrderBy {
|
||||||
Name = "name"
|
Name = "name"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type TCmekListSigningAlgorithmsDTO = {
|
||||||
|
keyId: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type TCmekGetPublicKeyDTO = {
|
||||||
|
keyId: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type TCmekSignDTO = {
|
||||||
|
keyId: string;
|
||||||
|
data: string;
|
||||||
|
signingAlgorithm: SigningAlgorithm;
|
||||||
|
isDigest: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type TCmekVerifyDTO = {
|
||||||
|
keyId: string;
|
||||||
|
data: string;
|
||||||
|
signature: string;
|
||||||
|
signingAlgorithm: SigningAlgorithm;
|
||||||
|
isDigest: boolean;
|
||||||
|
};
|
||||||
|
|||||||
@@ -50,7 +50,7 @@ const getIntegrationSecretsV2 = async (
|
|||||||
}
|
}
|
||||||
|
|
||||||
// process secrets in current folder
|
// process secrets in current folder
|
||||||
const secrets = await secretV2BridgeDAL.findByFolderId(dto.folderId);
|
const secrets = await secretV2BridgeDAL.findByFolderId({ folderId: dto.folderId, projectId: dto.projectId });
|
||||||
|
|
||||||
secrets.forEach((secret) => {
|
secrets.forEach((secret) => {
|
||||||
const secretKey = secret.key;
|
const secretKey = secret.key;
|
||||||
@@ -63,6 +63,7 @@ const getIntegrationSecretsV2 = async (
|
|||||||
// if no imports then return secrets in the current folder
|
// if no imports then return secrets in the current folder
|
||||||
if (!secretImports.length) return content;
|
if (!secretImports.length) return content;
|
||||||
const importedSecrets = await fnSecretsV2FromImports({
|
const importedSecrets = await fnSecretsV2FromImports({
|
||||||
|
projectId: dto.projectId,
|
||||||
decryptor: dto.decryptor,
|
decryptor: dto.decryptor,
|
||||||
folderDAL,
|
folderDAL,
|
||||||
secretDAL: secretV2BridgeDAL,
|
secretDAL: secretV2BridgeDAL,
|
||||||
|
|||||||
@@ -195,6 +195,7 @@ export const getIntegrationOptions = async () => {
|
|||||||
{
|
{
|
||||||
name: "AWS Secrets Manager",
|
name: "AWS Secrets Manager",
|
||||||
slug: "aws-secret-manager",
|
slug: "aws-secret-manager",
|
||||||
|
syncSlug: "aws-secrets-manager",
|
||||||
image: "Amazon Web Services.png",
|
image: "Amazon Web Services.png",
|
||||||
isAvailable: true,
|
isAvailable: true,
|
||||||
type: "custom",
|
type: "custom",
|
||||||
|
|||||||
@@ -1,13 +1,55 @@
|
|||||||
import { SymmetricEncryption } from "@app/lib/crypto/cipher";
|
import { SymmetricKeyAlgorithm } from "@app/lib/crypto/cipher";
|
||||||
|
import { AsymmetricKeyAlgorithm } from "@app/lib/crypto/sign";
|
||||||
|
import { BadRequestError } from "@app/lib/errors";
|
||||||
|
|
||||||
|
import { KmsKeyUsage } from "./kms-types";
|
||||||
|
|
||||||
export const KMS_ROOT_CONFIG_UUID = "00000000-0000-0000-0000-000000000000";
|
export const KMS_ROOT_CONFIG_UUID = "00000000-0000-0000-0000-000000000000";
|
||||||
|
|
||||||
export const getByteLengthForAlgorithm = (encryptionAlgorithm: SymmetricEncryption) => {
|
export const getByteLengthForSymmetricEncryptionAlgorithm = (encryptionAlgorithm: SymmetricKeyAlgorithm) => {
|
||||||
switch (encryptionAlgorithm) {
|
switch (encryptionAlgorithm) {
|
||||||
case SymmetricEncryption.AES_GCM_128:
|
case SymmetricKeyAlgorithm.AES_GCM_128:
|
||||||
return 16;
|
return 16;
|
||||||
case SymmetricEncryption.AES_GCM_256:
|
case SymmetricKeyAlgorithm.AES_GCM_256:
|
||||||
default:
|
default:
|
||||||
return 32;
|
return 32;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const verifyKeyTypeAndAlgorithm = (
|
||||||
|
keyUsage: KmsKeyUsage,
|
||||||
|
algorithm: SymmetricKeyAlgorithm | AsymmetricKeyAlgorithm,
|
||||||
|
extra?: {
|
||||||
|
forceType?: KmsKeyUsage;
|
||||||
|
}
|
||||||
|
) => {
|
||||||
|
if (extra?.forceType && keyUsage !== extra.forceType) {
|
||||||
|
throw new BadRequestError({
|
||||||
|
message: `Unsupported key type, expected ${extra.forceType} but got ${keyUsage}`
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (keyUsage === KmsKeyUsage.ENCRYPT_DECRYPT) {
|
||||||
|
if (!Object.values(SymmetricKeyAlgorithm).includes(algorithm as SymmetricKeyAlgorithm)) {
|
||||||
|
throw new BadRequestError({
|
||||||
|
message: `Unsupported encryption algorithm for encrypt/decrypt key: ${algorithm as string}`
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (keyUsage === KmsKeyUsage.SIGN_VERIFY) {
|
||||||
|
if (!Object.values(AsymmetricKeyAlgorithm).includes(algorithm as AsymmetricKeyAlgorithm)) {
|
||||||
|
throw new BadRequestError({
|
||||||
|
message: `Unsupported sign/verify algorithm for sign/verify key: ${algorithm as string}`
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new BadRequestError({
|
||||||
|
message: `Unsupported key type: ${keyUsage as string}`
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|||||||
@@ -15,12 +15,17 @@ import { THsmServiceFactory } from "@app/ee/services/hsm/hsm-service";
|
|||||||
import { KeyStorePrefixes, PgSqlLock, TKeyStoreFactory } from "@app/keystore/keystore";
|
import { KeyStorePrefixes, PgSqlLock, TKeyStoreFactory } from "@app/keystore/keystore";
|
||||||
import { TEnvConfig } from "@app/lib/config/env";
|
import { TEnvConfig } from "@app/lib/config/env";
|
||||||
import { randomSecureBytes } from "@app/lib/crypto";
|
import { randomSecureBytes } from "@app/lib/crypto";
|
||||||
import { symmetricCipherService, SymmetricEncryption } from "@app/lib/crypto/cipher";
|
import { symmetricCipherService, SymmetricKeyAlgorithm } from "@app/lib/crypto/cipher";
|
||||||
import { generateHash } from "@app/lib/crypto/encryption";
|
import { generateHash } from "@app/lib/crypto/encryption";
|
||||||
|
import { AsymmetricKeyAlgorithm, signingService } from "@app/lib/crypto/sign";
|
||||||
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, KMS_ROOT_CONFIG_UUID } from "@app/services/kms/kms-fns";
|
import {
|
||||||
|
getByteLengthForSymmetricEncryptionAlgorithm,
|
||||||
|
KMS_ROOT_CONFIG_UUID,
|
||||||
|
verifyKeyTypeAndAlgorithm
|
||||||
|
} 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";
|
||||||
@@ -29,6 +34,7 @@ import { TKmsKeyDALFactory } from "./kms-key-dal";
|
|||||||
import { TKmsRootConfigDALFactory } from "./kms-root-config-dal";
|
import { TKmsRootConfigDALFactory } from "./kms-root-config-dal";
|
||||||
import {
|
import {
|
||||||
KmsDataKey,
|
KmsDataKey,
|
||||||
|
KmsKeyUsage,
|
||||||
KmsType,
|
KmsType,
|
||||||
RootKeyEncryptionStrategy,
|
RootKeyEncryptionStrategy,
|
||||||
TDecryptWithKeyDTO,
|
TDecryptWithKeyDTO,
|
||||||
@@ -38,8 +44,11 @@ import {
|
|||||||
TEncryptWithKmsDTO,
|
TEncryptWithKmsDTO,
|
||||||
TGenerateKMSDTO,
|
TGenerateKMSDTO,
|
||||||
TGetKeyMaterialDTO,
|
TGetKeyMaterialDTO,
|
||||||
|
TGetPublicKeyDTO,
|
||||||
TImportKeyMaterialDTO,
|
TImportKeyMaterialDTO,
|
||||||
TUpdateProjectSecretManagerKmsKeyDTO
|
TSignWithKmsDTO,
|
||||||
|
TUpdateProjectSecretManagerKmsKeyDTO,
|
||||||
|
TVerifyWithKmsDTO
|
||||||
} from "./kms-types";
|
} from "./kms-types";
|
||||||
|
|
||||||
type TKmsServiceFactoryDep = {
|
type TKmsServiceFactoryDep = {
|
||||||
@@ -83,19 +92,42 @@ export const kmsServiceFactory = ({
|
|||||||
tx,
|
tx,
|
||||||
name,
|
name,
|
||||||
projectId,
|
projectId,
|
||||||
encryptionAlgorithm = SymmetricEncryption.AES_GCM_256,
|
encryptionAlgorithm = SymmetricKeyAlgorithm.AES_GCM_256,
|
||||||
|
keyUsage = KmsKeyUsage.ENCRYPT_DECRYPT,
|
||||||
description
|
description
|
||||||
}: TGenerateKMSDTO) => {
|
}: TGenerateKMSDTO) => {
|
||||||
const cipher = symmetricCipherService(SymmetricEncryption.AES_GCM_256);
|
// daniel: ensure that the key type (sign/encrypt) and the encryption algorithm are compatible.
|
||||||
|
verifyKeyTypeAndAlgorithm(keyUsage, encryptionAlgorithm);
|
||||||
|
|
||||||
const kmsKeyMaterial = randomSecureBytes(getByteLengthForAlgorithm(encryptionAlgorithm));
|
let kmsKeyMaterial: Buffer | null = null;
|
||||||
|
if (keyUsage === KmsKeyUsage.ENCRYPT_DECRYPT) {
|
||||||
|
kmsKeyMaterial = randomSecureBytes(
|
||||||
|
getByteLengthForSymmetricEncryptionAlgorithm(encryptionAlgorithm as SymmetricKeyAlgorithm)
|
||||||
|
);
|
||||||
|
} else if (keyUsage === KmsKeyUsage.SIGN_VERIFY) {
|
||||||
|
const { generateAsymmetricPrivateKey, getPublicKeyFromPrivateKey } = signingService(
|
||||||
|
encryptionAlgorithm as AsymmetricKeyAlgorithm
|
||||||
|
);
|
||||||
|
kmsKeyMaterial = await generateAsymmetricPrivateKey();
|
||||||
|
|
||||||
|
// daniel: safety check to ensure we're able to extract the public key from the private key before we proceed to key creation
|
||||||
|
getPublicKeyFromPrivateKey(kmsKeyMaterial);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!kmsKeyMaterial) {
|
||||||
|
throw new BadRequestError({
|
||||||
|
message: `Invalid KMS key type. No key material was created for key usage '${keyUsage}' using algorithm '${encryptionAlgorithm}'`
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const cipher = symmetricCipherService(SymmetricKeyAlgorithm.AES_GCM_256);
|
||||||
const encryptedKeyMaterial = cipher.encrypt(kmsKeyMaterial, ROOT_ENCRYPTION_KEY);
|
const encryptedKeyMaterial = cipher.encrypt(kmsKeyMaterial, ROOT_ENCRYPTION_KEY);
|
||||||
const sanitizedName = name ? slugify(name) : 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(
|
||||||
{
|
{
|
||||||
name: sanitizedName,
|
name: sanitizedName,
|
||||||
|
keyUsage,
|
||||||
orgId,
|
orgId,
|
||||||
isReserved,
|
isReserved,
|
||||||
projectId,
|
projectId,
|
||||||
@@ -115,6 +147,7 @@ export const kmsServiceFactory = ({
|
|||||||
);
|
);
|
||||||
return kmsDoc;
|
return kmsDoc;
|
||||||
};
|
};
|
||||||
|
|
||||||
if (tx) return dbQuery(tx);
|
if (tx) return dbQuery(tx);
|
||||||
const doc = await kmsDAL.transaction(async (tx2) => dbQuery(tx2));
|
const doc = await kmsDAL.transaction(async (tx2) => dbQuery(tx2));
|
||||||
return doc;
|
return doc;
|
||||||
@@ -134,7 +167,7 @@ export const kmsServiceFactory = ({
|
|||||||
*/
|
*/
|
||||||
const encryptWithInputKey = async ({ key }: Omit<TEncryptionWithKeyDTO, "plainText">) => {
|
const encryptWithInputKey = async ({ key }: Omit<TEncryptionWithKeyDTO, "plainText">) => {
|
||||||
// akhilmhdh: as more encryption are added do a check here on kmsDoc.encryptionAlgorithm
|
// akhilmhdh: as more encryption are added do a check here on kmsDoc.encryptionAlgorithm
|
||||||
const cipher = symmetricCipherService(SymmetricEncryption.AES_GCM_256);
|
const cipher = symmetricCipherService(SymmetricKeyAlgorithm.AES_GCM_256);
|
||||||
return ({ plainText }: Pick<TEncryptWithKmsDTO, "plainText">) => {
|
return ({ plainText }: Pick<TEncryptWithKmsDTO, "plainText">) => {
|
||||||
const encryptedPlainTextBlob = cipher.encrypt(plainText, key);
|
const encryptedPlainTextBlob = cipher.encrypt(plainText, key);
|
||||||
// Buffer#1 encrypted text + Buffer#2 version number
|
// Buffer#1 encrypted text + Buffer#2 version number
|
||||||
@@ -149,7 +182,7 @@ export const kmsServiceFactory = ({
|
|||||||
* This can be even later exposed directly as api for encryption as function
|
* This can be even later exposed directly as api for encryption as function
|
||||||
*/
|
*/
|
||||||
const decryptWithInputKey = async ({ key }: Omit<TDecryptWithKeyDTO, "cipherTextBlob">) => {
|
const decryptWithInputKey = async ({ key }: Omit<TDecryptWithKeyDTO, "cipherTextBlob">) => {
|
||||||
const cipher = symmetricCipherService(SymmetricEncryption.AES_GCM_256);
|
const cipher = symmetricCipherService(SymmetricKeyAlgorithm.AES_GCM_256);
|
||||||
|
|
||||||
return ({ cipherTextBlob: versionedCipherTextBlob }: Pick<TDecryptWithKeyDTO, "cipherTextBlob">) => {
|
return ({ cipherTextBlob: versionedCipherTextBlob }: Pick<TDecryptWithKeyDTO, "cipherTextBlob">) => {
|
||||||
const cipherTextBlob = versionedCipherTextBlob.subarray(0, -KMS_VERSION_BLOB_LENGTH);
|
const cipherTextBlob = versionedCipherTextBlob.subarray(0, -KMS_VERSION_BLOB_LENGTH);
|
||||||
@@ -227,7 +260,7 @@ export const kmsServiceFactory = ({
|
|||||||
};
|
};
|
||||||
|
|
||||||
const encryptWithRootKey = () => {
|
const encryptWithRootKey = () => {
|
||||||
const cipher = symmetricCipherService(SymmetricEncryption.AES_GCM_256);
|
const cipher = symmetricCipherService(SymmetricKeyAlgorithm.AES_GCM_256);
|
||||||
|
|
||||||
return (plainTextBuffer: Buffer) => {
|
return (plainTextBuffer: Buffer) => {
|
||||||
const encryptedBuffer = cipher.encrypt(plainTextBuffer, ROOT_ENCRYPTION_KEY);
|
const encryptedBuffer = cipher.encrypt(plainTextBuffer, ROOT_ENCRYPTION_KEY);
|
||||||
@@ -236,7 +269,7 @@ export const kmsServiceFactory = ({
|
|||||||
};
|
};
|
||||||
|
|
||||||
const decryptWithRootKey = () => {
|
const decryptWithRootKey = () => {
|
||||||
const cipher = symmetricCipherService(SymmetricEncryption.AES_GCM_256);
|
const cipher = symmetricCipherService(SymmetricKeyAlgorithm.AES_GCM_256);
|
||||||
|
|
||||||
return (cipherTextBuffer: Buffer) => {
|
return (cipherTextBuffer: Buffer) => {
|
||||||
return cipher.decrypt(cipherTextBuffer, ROOT_ENCRYPTION_KEY);
|
return cipher.decrypt(cipherTextBuffer, ROOT_ENCRYPTION_KEY);
|
||||||
@@ -315,9 +348,14 @@ export const kmsServiceFactory = ({
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const encryptionAlgorithm = kmsDoc.internalKms?.encryptionAlgorithm as SymmetricKeyAlgorithm;
|
||||||
|
verifyKeyTypeAndAlgorithm(kmsDoc.keyUsage as KmsKeyUsage, encryptionAlgorithm, {
|
||||||
|
forceType: KmsKeyUsage.ENCRYPT_DECRYPT
|
||||||
|
});
|
||||||
|
|
||||||
// internal KMS
|
// internal KMS
|
||||||
const keyCipher = symmetricCipherService(SymmetricEncryption.AES_GCM_256);
|
const keyCipher = symmetricCipherService(SymmetricKeyAlgorithm.AES_GCM_256);
|
||||||
const dataCipher = symmetricCipherService(kmsDoc.internalKms?.encryptionAlgorithm as SymmetricEncryption);
|
const dataCipher = symmetricCipherService(encryptionAlgorithm);
|
||||||
const kmsKey = keyCipher.decrypt(kmsDoc.internalKms?.encryptedKey as Buffer, ROOT_ENCRYPTION_KEY);
|
const kmsKey = keyCipher.decrypt(kmsDoc.internalKms?.encryptedKey as Buffer, ROOT_ENCRYPTION_KEY);
|
||||||
|
|
||||||
return ({ cipherTextBlob: versionedCipherTextBlob }: Pick<TDecryptWithKmsDTO, "cipherTextBlob">) => {
|
return ({ cipherTextBlob: versionedCipherTextBlob }: Pick<TDecryptWithKmsDTO, "cipherTextBlob">) => {
|
||||||
@@ -345,19 +383,22 @@ export const kmsServiceFactory = ({
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
const keyCipher = symmetricCipherService(SymmetricEncryption.AES_GCM_256);
|
const keyCipher = symmetricCipherService(SymmetricKeyAlgorithm.AES_GCM_256);
|
||||||
const kmsKey = keyCipher.decrypt(kmsDoc.internalKms?.encryptedKey as Buffer, ROOT_ENCRYPTION_KEY);
|
const kmsKey = keyCipher.decrypt(kmsDoc.internalKms?.encryptedKey as Buffer, ROOT_ENCRYPTION_KEY);
|
||||||
|
|
||||||
return kmsKey;
|
return kmsKey;
|
||||||
};
|
};
|
||||||
|
|
||||||
const importKeyMaterial = async (
|
const importKeyMaterial = async (
|
||||||
{ key, algorithm, name, isReserved, projectId, orgId }: TImportKeyMaterialDTO,
|
{ key, algorithm, name, isReserved, projectId, orgId, keyUsage }: TImportKeyMaterialDTO,
|
||||||
tx?: Knex
|
tx?: Knex
|
||||||
) => {
|
) => {
|
||||||
const cipher = symmetricCipherService(SymmetricEncryption.AES_GCM_256);
|
// daniel: currently we only support imports for encrypt/decrypt keys
|
||||||
|
verifyKeyTypeAndAlgorithm(keyUsage, algorithm, { forceType: KmsKeyUsage.ENCRYPT_DECRYPT });
|
||||||
|
|
||||||
const expectedByteLength = getByteLengthForAlgorithm(algorithm);
|
const cipher = symmetricCipherService(SymmetricKeyAlgorithm.AES_GCM_256);
|
||||||
|
|
||||||
|
const expectedByteLength = getByteLengthForSymmetricEncryptionAlgorithm(algorithm as SymmetricKeyAlgorithm);
|
||||||
if (key.byteLength !== expectedByteLength) {
|
if (key.byteLength !== expectedByteLength) {
|
||||||
throw new BadRequestError({
|
throw new BadRequestError({
|
||||||
message: `Invalid key length for ${algorithm}. Expected ${expectedByteLength} bytes but got ${key.byteLength} bytes`
|
message: `Invalid key length for ${algorithm}. Expected ${expectedByteLength} bytes but got ${key.byteLength} bytes`
|
||||||
@@ -370,6 +411,7 @@ export const kmsServiceFactory = ({
|
|||||||
const kmsDoc = await kmsDAL.create(
|
const kmsDoc = await kmsDAL.create(
|
||||||
{
|
{
|
||||||
name: sanitizedName,
|
name: sanitizedName,
|
||||||
|
keyUsage: KmsKeyUsage.ENCRYPT_DECRYPT,
|
||||||
orgId,
|
orgId,
|
||||||
isReserved,
|
isReserved,
|
||||||
projectId
|
projectId
|
||||||
@@ -393,6 +435,74 @@ export const kmsServiceFactory = ({
|
|||||||
return doc;
|
return doc;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const getPublicKey = async ({ kmsId }: TGetPublicKeyDTO) => {
|
||||||
|
const kmsDoc = await kmsDAL.findByIdWithAssociatedKms(kmsId);
|
||||||
|
if (!kmsDoc) {
|
||||||
|
throw new NotFoundError({ message: `KMS with ID '${kmsId}' not found` });
|
||||||
|
}
|
||||||
|
|
||||||
|
const encryptionAlgorithm = kmsDoc.internalKms?.encryptionAlgorithm as AsymmetricKeyAlgorithm;
|
||||||
|
|
||||||
|
verifyKeyTypeAndAlgorithm(kmsDoc.keyUsage as KmsKeyUsage, encryptionAlgorithm, {
|
||||||
|
forceType: KmsKeyUsage.SIGN_VERIFY
|
||||||
|
});
|
||||||
|
|
||||||
|
const keyCipher = symmetricCipherService(SymmetricKeyAlgorithm.AES_GCM_256);
|
||||||
|
const kmsKey = keyCipher.decrypt(kmsDoc.internalKms?.encryptedKey as Buffer, ROOT_ENCRYPTION_KEY);
|
||||||
|
|
||||||
|
return signingService(encryptionAlgorithm).getPublicKeyFromPrivateKey(kmsKey);
|
||||||
|
};
|
||||||
|
|
||||||
|
const signWithKmsKey = async ({ kmsId }: Pick<TSignWithKmsDTO, "kmsId">) => {
|
||||||
|
const kmsDoc = await kmsDAL.findByIdWithAssociatedKms(kmsId);
|
||||||
|
if (!kmsDoc) {
|
||||||
|
throw new NotFoundError({ message: `KMS with ID '${kmsId}' not found` });
|
||||||
|
}
|
||||||
|
|
||||||
|
const encryptionAlgorithm = kmsDoc.internalKms?.encryptionAlgorithm as AsymmetricKeyAlgorithm;
|
||||||
|
verifyKeyTypeAndAlgorithm(kmsDoc.keyUsage as KmsKeyUsage, encryptionAlgorithm, {
|
||||||
|
forceType: KmsKeyUsage.SIGN_VERIFY
|
||||||
|
});
|
||||||
|
|
||||||
|
const keyCipher = symmetricCipherService(SymmetricKeyAlgorithm.AES_GCM_256);
|
||||||
|
const { sign } = signingService(encryptionAlgorithm);
|
||||||
|
return async ({
|
||||||
|
data,
|
||||||
|
signingAlgorithm,
|
||||||
|
isDigest
|
||||||
|
}: Pick<TSignWithKmsDTO, "data" | "signingAlgorithm" | "isDigest">) => {
|
||||||
|
const kmsKey = keyCipher.decrypt(kmsDoc.internalKms?.encryptedKey as Buffer, ROOT_ENCRYPTION_KEY);
|
||||||
|
const signature = await sign(data, kmsKey, signingAlgorithm, isDigest);
|
||||||
|
|
||||||
|
return Promise.resolve({ signature, algorithm: signingAlgorithm });
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
const verifyWithKmsKey = async ({
|
||||||
|
kmsId,
|
||||||
|
signingAlgorithm
|
||||||
|
}: Pick<TVerifyWithKmsDTO, "kmsId" | "signingAlgorithm">) => {
|
||||||
|
const kmsDoc = await kmsDAL.findByIdWithAssociatedKms(kmsId);
|
||||||
|
if (!kmsDoc) {
|
||||||
|
throw new NotFoundError({ message: `KMS with ID '${kmsId}' not found` });
|
||||||
|
}
|
||||||
|
|
||||||
|
const encryptionAlgorithm = kmsDoc.internalKms?.encryptionAlgorithm as AsymmetricKeyAlgorithm;
|
||||||
|
verifyKeyTypeAndAlgorithm(kmsDoc.keyUsage as KmsKeyUsage, encryptionAlgorithm, {
|
||||||
|
forceType: KmsKeyUsage.SIGN_VERIFY
|
||||||
|
});
|
||||||
|
|
||||||
|
const keyCipher = symmetricCipherService(SymmetricKeyAlgorithm.AES_GCM_256);
|
||||||
|
const { verify, getPublicKeyFromPrivateKey } = signingService(encryptionAlgorithm);
|
||||||
|
return async ({ data, signature, isDigest }: Pick<TVerifyWithKmsDTO, "data" | "signature" | "isDigest">) => {
|
||||||
|
const kmsKey = keyCipher.decrypt(kmsDoc.internalKms?.encryptedKey as Buffer, ROOT_ENCRYPTION_KEY);
|
||||||
|
|
||||||
|
const publicKey = getPublicKeyFromPrivateKey(kmsKey);
|
||||||
|
const signatureValid = await verify(data, signature, publicKey, signingAlgorithm, isDigest);
|
||||||
|
return Promise.resolve({ signatureValid, algorithm: signingAlgorithm });
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
const encryptWithKmsKey = async ({ kmsId }: Omit<TEncryptWithKmsDTO, "plainText">, tx?: Knex) => {
|
const encryptWithKmsKey = async ({ kmsId }: Omit<TEncryptWithKmsDTO, "plainText">, tx?: Knex) => {
|
||||||
const kmsDoc = await kmsDAL.findByIdWithAssociatedKms(kmsId, tx);
|
const kmsDoc = await kmsDAL.findByIdWithAssociatedKms(kmsId, tx);
|
||||||
if (!kmsDoc) {
|
if (!kmsDoc) {
|
||||||
@@ -453,9 +563,14 @@ export const kmsServiceFactory = ({
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const encryptionAlgorithm = kmsDoc.internalKms?.encryptionAlgorithm as SymmetricKeyAlgorithm;
|
||||||
|
verifyKeyTypeAndAlgorithm(kmsDoc.keyUsage as KmsKeyUsage, encryptionAlgorithm, {
|
||||||
|
forceType: KmsKeyUsage.ENCRYPT_DECRYPT
|
||||||
|
});
|
||||||
|
|
||||||
// internal KMS
|
// internal KMS
|
||||||
const keyCipher = symmetricCipherService(SymmetricEncryption.AES_GCM_256);
|
const keyCipher = symmetricCipherService(SymmetricKeyAlgorithm.AES_GCM_256);
|
||||||
const dataCipher = symmetricCipherService(kmsDoc.internalKms?.encryptionAlgorithm as SymmetricEncryption);
|
const dataCipher = symmetricCipherService(encryptionAlgorithm);
|
||||||
return ({ plainText }: Pick<TEncryptWithKmsDTO, "plainText">) => {
|
return ({ plainText }: Pick<TEncryptWithKmsDTO, "plainText">) => {
|
||||||
const kmsKey = keyCipher.decrypt(kmsDoc.internalKms?.encryptedKey as Buffer, ROOT_ENCRYPTION_KEY);
|
const kmsKey = keyCipher.decrypt(kmsDoc.internalKms?.encryptedKey as Buffer, ROOT_ENCRYPTION_KEY);
|
||||||
const encryptedPlainTextBlob = dataCipher.encrypt(plainText, kmsKey);
|
const encryptedPlainTextBlob = dataCipher.encrypt(plainText, kmsKey);
|
||||||
@@ -729,7 +844,7 @@ export const kmsServiceFactory = ({
|
|||||||
|
|
||||||
// case 2: root key is encrypted with software encryption
|
// case 2: root key is encrypted with software encryption
|
||||||
if (kmsRootConfig.encryptionStrategy === RootKeyEncryptionStrategy.Software) {
|
if (kmsRootConfig.encryptionStrategy === RootKeyEncryptionStrategy.Software) {
|
||||||
const cipher = symmetricCipherService(SymmetricEncryption.AES_GCM_256);
|
const cipher = symmetricCipherService(SymmetricKeyAlgorithm.AES_GCM_256);
|
||||||
const encryptionKeyBuffer = $getBasicEncryptionKey();
|
const encryptionKeyBuffer = $getBasicEncryptionKey();
|
||||||
|
|
||||||
return cipher.decrypt(kmsRootConfig.encryptedRootKey, encryptionKeyBuffer);
|
return cipher.decrypt(kmsRootConfig.encryptedRootKey, encryptionKeyBuffer);
|
||||||
@@ -749,7 +864,7 @@ export const kmsServiceFactory = ({
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (strategy === RootKeyEncryptionStrategy.Software) {
|
if (strategy === RootKeyEncryptionStrategy.Software) {
|
||||||
const cipher = symmetricCipherService(SymmetricEncryption.AES_GCM_256);
|
const cipher = symmetricCipherService(SymmetricKeyAlgorithm.AES_GCM_256);
|
||||||
const encryptionKeyBuffer = $getBasicEncryptionKey();
|
const encryptionKeyBuffer = $getBasicEncryptionKey();
|
||||||
|
|
||||||
return cipher.encrypt(plainKeyBuffer, encryptionKeyBuffer);
|
return cipher.encrypt(plainKeyBuffer, encryptionKeyBuffer);
|
||||||
@@ -765,7 +880,7 @@ export const kmsServiceFactory = ({
|
|||||||
const createCipherPairWithDataKey = async (encryptionContext: TEncryptWithKmsDataKeyDTO, trx?: Knex) => {
|
const createCipherPairWithDataKey = async (encryptionContext: TEncryptWithKmsDataKeyDTO, trx?: Knex) => {
|
||||||
const dataKey = await $getDataKey(encryptionContext, trx);
|
const dataKey = await $getDataKey(encryptionContext, trx);
|
||||||
|
|
||||||
const cipher = symmetricCipherService(SymmetricEncryption.AES_GCM_256);
|
const cipher = symmetricCipherService(SymmetricKeyAlgorithm.AES_GCM_256);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
encryptor: ({ plainText }: Pick<TEncryptWithKmsDTO, "plainText">) => {
|
encryptor: ({ plainText }: Pick<TEncryptWithKmsDTO, "plainText">) => {
|
||||||
@@ -966,6 +1081,7 @@ export const kmsServiceFactory = ({
|
|||||||
const decryptedRootKey = await $decryptRootKey(kmsRootConfig);
|
const decryptedRootKey = await $decryptRootKey(kmsRootConfig);
|
||||||
|
|
||||||
logger.info("KMS: Loading ROOT Key into Memory.");
|
logger.info("KMS: Loading ROOT Key into Memory.");
|
||||||
|
|
||||||
ROOT_ENCRYPTION_KEY = decryptedRootKey;
|
ROOT_ENCRYPTION_KEY = decryptedRootKey;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -1014,6 +1130,9 @@ export const kmsServiceFactory = ({
|
|||||||
getKmsById,
|
getKmsById,
|
||||||
createCipherPairWithDataKey,
|
createCipherPairWithDataKey,
|
||||||
getKeyMaterial,
|
getKeyMaterial,
|
||||||
importKeyMaterial
|
importKeyMaterial,
|
||||||
|
signWithKmsKey,
|
||||||
|
verifyWithKmsKey,
|
||||||
|
getPublicKey
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { Knex } from "knex";
|
import { Knex } from "knex";
|
||||||
|
|
||||||
import { SymmetricEncryption } from "@app/lib/crypto/cipher";
|
import { SymmetricKeyAlgorithm } from "@app/lib/crypto/cipher";
|
||||||
|
import { AsymmetricKeyAlgorithm, SigningAlgorithm } from "@app/lib/crypto/sign/types";
|
||||||
|
|
||||||
export enum KmsDataKey {
|
export enum KmsDataKey {
|
||||||
Organization,
|
Organization,
|
||||||
@@ -13,6 +14,11 @@ export enum KmsType {
|
|||||||
Internal = "internal"
|
Internal = "internal"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export enum KmsKeyUsage {
|
||||||
|
ENCRYPT_DECRYPT = "encrypt-decrypt",
|
||||||
|
SIGN_VERIFY = "sign-verify"
|
||||||
|
}
|
||||||
|
|
||||||
export type TEncryptWithKmsDataKeyDTO =
|
export type TEncryptWithKmsDataKeyDTO =
|
||||||
| { type: KmsDataKey.Organization; orgId: string }
|
| { type: KmsDataKey.Organization; orgId: string }
|
||||||
| { type: KmsDataKey.SecretManager; projectId: string };
|
| { type: KmsDataKey.SecretManager; projectId: string };
|
||||||
@@ -25,7 +31,8 @@ export type TEncryptWithKmsDataKeyDTO =
|
|||||||
export type TGenerateKMSDTO = {
|
export type TGenerateKMSDTO = {
|
||||||
orgId: string;
|
orgId: string;
|
||||||
projectId?: string;
|
projectId?: string;
|
||||||
encryptionAlgorithm?: SymmetricEncryption;
|
encryptionAlgorithm?: SymmetricKeyAlgorithm | AsymmetricKeyAlgorithm;
|
||||||
|
keyUsage?: KmsKeyUsage;
|
||||||
isReserved?: boolean;
|
isReserved?: boolean;
|
||||||
name?: string;
|
name?: string;
|
||||||
description?: string;
|
description?: string;
|
||||||
@@ -37,6 +44,25 @@ export type TEncryptWithKmsDTO = {
|
|||||||
plainText: Buffer;
|
plainText: Buffer;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type TGetPublicKeyDTO = {
|
||||||
|
kmsId: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type TSignWithKmsDTO = {
|
||||||
|
kmsId: string;
|
||||||
|
data: Buffer;
|
||||||
|
signingAlgorithm: SigningAlgorithm;
|
||||||
|
isDigest: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type TVerifyWithKmsDTO = {
|
||||||
|
kmsId: string;
|
||||||
|
data: Buffer;
|
||||||
|
signature: Buffer;
|
||||||
|
signingAlgorithm: SigningAlgorithm;
|
||||||
|
isDigest: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
export type TEncryptionWithKeyDTO = {
|
export type TEncryptionWithKeyDTO = {
|
||||||
key: Buffer;
|
key: Buffer;
|
||||||
plainText: Buffer;
|
plainText: Buffer;
|
||||||
@@ -67,9 +93,10 @@ export type TGetKeyMaterialDTO = {
|
|||||||
|
|
||||||
export type TImportKeyMaterialDTO = {
|
export type TImportKeyMaterialDTO = {
|
||||||
key: Buffer;
|
key: Buffer;
|
||||||
algorithm: SymmetricEncryption;
|
algorithm: SymmetricKeyAlgorithm;
|
||||||
name?: string;
|
name?: string;
|
||||||
isReserved: boolean;
|
isReserved: boolean;
|
||||||
projectId: string;
|
projectId: string;
|
||||||
orgId: string;
|
orgId: string;
|
||||||
|
keyUsage: KmsKeyUsage;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -5,6 +5,8 @@ import { TableName, TSecretImports } from "@app/db/schemas";
|
|||||||
import { DatabaseError } from "@app/lib/errors";
|
import { DatabaseError } from "@app/lib/errors";
|
||||||
import { ormify } from "@app/lib/knex";
|
import { ormify } from "@app/lib/knex";
|
||||||
|
|
||||||
|
import { EnvironmentInfo, FolderInfo, FolderResult, SecretResult } from "./secret-import-types";
|
||||||
|
|
||||||
export type TSecretImportDALFactory = ReturnType<typeof secretImportDALFactory>;
|
export type TSecretImportDALFactory = ReturnType<typeof secretImportDALFactory>;
|
||||||
|
|
||||||
export const secretImportDALFactory = (db: TDbClient) => {
|
export const secretImportDALFactory = (db: TDbClient) => {
|
||||||
@@ -169,6 +171,136 @@ export const secretImportDALFactory = (db: TDbClient) => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const getFolderIsImportedBy = async (
|
||||||
|
secretPath: string,
|
||||||
|
environmentId: string,
|
||||||
|
environment: string,
|
||||||
|
projectId: string,
|
||||||
|
tx?: Knex
|
||||||
|
) => {
|
||||||
|
try {
|
||||||
|
const folderImports = await (tx || db.replicaNode())(TableName.SecretImport)
|
||||||
|
.where({ importPath: secretPath, importEnv: environmentId })
|
||||||
|
.join(TableName.SecretFolder, `${TableName.SecretImport}.folderId`, `${TableName.SecretFolder}.id`)
|
||||||
|
.join(TableName.Environment, `${TableName.SecretFolder}.envId`, `${TableName.Environment}.id`)
|
||||||
|
.select(
|
||||||
|
db.ref("name").withSchema(TableName.Environment).as("envName"),
|
||||||
|
db.ref("slug").withSchema(TableName.Environment).as("envSlug"),
|
||||||
|
db.ref("name").withSchema(TableName.SecretFolder).as("folderName"),
|
||||||
|
db.ref("id").withSchema(TableName.SecretFolder).as("folderId")
|
||||||
|
);
|
||||||
|
|
||||||
|
const secretReferences = await (tx || db.replicaNode())(TableName.SecretReferenceV2)
|
||||||
|
.where({ secretPath, environment })
|
||||||
|
.join(TableName.SecretV2, `${TableName.SecretReferenceV2}.secretId`, `${TableName.SecretV2}.id`)
|
||||||
|
.join(TableName.SecretFolder, `${TableName.SecretV2}.folderId`, `${TableName.SecretFolder}.id`)
|
||||||
|
.join(TableName.Environment, `${TableName.SecretFolder}.envId`, `${TableName.Environment}.id`)
|
||||||
|
.where(`${TableName.Environment}.projectId`, projectId)
|
||||||
|
.where(`${TableName.SecretFolder}.isReserved`, false)
|
||||||
|
.select(
|
||||||
|
db.ref("key").withSchema(TableName.SecretV2).as("secretId"),
|
||||||
|
db.ref("name").withSchema(TableName.SecretFolder).as("folderName"),
|
||||||
|
db.ref("name").withSchema(TableName.Environment).as("envName"),
|
||||||
|
db.ref("slug").withSchema(TableName.Environment).as("envSlug"),
|
||||||
|
db.ref("id").withSchema(TableName.SecretFolder).as("folderId"),
|
||||||
|
db.ref("secretKey").withSchema(TableName.SecretReferenceV2).as("referencedSecretKey")
|
||||||
|
);
|
||||||
|
|
||||||
|
const folderResults = folderImports.map(({ envName, envSlug, folderName, folderId }) => ({
|
||||||
|
envName,
|
||||||
|
envSlug,
|
||||||
|
folderName,
|
||||||
|
folderId
|
||||||
|
}));
|
||||||
|
|
||||||
|
const secretResults = secretReferences.map(
|
||||||
|
({ envName, envSlug, secretId, folderName, folderId, referencedSecretKey }) => ({
|
||||||
|
envName,
|
||||||
|
envSlug,
|
||||||
|
secretId,
|
||||||
|
folderName,
|
||||||
|
folderId,
|
||||||
|
referencedSecretKey
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
type ResultItem = FolderResult | SecretResult;
|
||||||
|
const allResults: ResultItem[] = [...folderResults, ...secretResults];
|
||||||
|
|
||||||
|
type EnvFolderMap = {
|
||||||
|
[envName: string]: {
|
||||||
|
envSlug: string;
|
||||||
|
folders: {
|
||||||
|
[folderName: string]: {
|
||||||
|
secrets: {
|
||||||
|
secretId: string;
|
||||||
|
referencedSecretKey: string;
|
||||||
|
}[];
|
||||||
|
folderId: string;
|
||||||
|
folderImported: boolean;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
const groupedByEnv = allResults.reduce<EnvFolderMap>((acc, item) => {
|
||||||
|
const env = item.envName;
|
||||||
|
const folder = item.folderName;
|
||||||
|
const { envSlug } = item;
|
||||||
|
|
||||||
|
const updatedAcc = { ...acc };
|
||||||
|
|
||||||
|
if (!updatedAcc[env]) {
|
||||||
|
updatedAcc[env] = {
|
||||||
|
envSlug,
|
||||||
|
folders: {}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!updatedAcc[env].folders[folder]) {
|
||||||
|
updatedAcc[env].folders[folder] = { secrets: [], folderId: item.folderId, folderImported: false };
|
||||||
|
}
|
||||||
|
|
||||||
|
if ("secretId" in item && item.secretId) {
|
||||||
|
updatedAcc[env].folders[folder].secrets = [
|
||||||
|
...updatedAcc[env].folders[folder].secrets,
|
||||||
|
{ secretId: item.secretId, referencedSecretKey: item.referencedSecretKey }
|
||||||
|
];
|
||||||
|
} else {
|
||||||
|
updatedAcc[env].folders[folder].folderImported = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return updatedAcc;
|
||||||
|
}, {});
|
||||||
|
|
||||||
|
const formattedResult: EnvironmentInfo[] = Object.keys(groupedByEnv).map((envName) => {
|
||||||
|
const envData = groupedByEnv[envName];
|
||||||
|
|
||||||
|
const folders: FolderInfo[] = Object.keys(envData.folders).map((folderName) => {
|
||||||
|
const folderData = envData.folders[folderName];
|
||||||
|
const hasSecrets = folderData.secrets.length > 0;
|
||||||
|
|
||||||
|
return {
|
||||||
|
folderName,
|
||||||
|
folderId: folderData.folderId,
|
||||||
|
folderImported: folderData.folderImported,
|
||||||
|
...(hasSecrets && { secrets: folderData.secrets })
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
envName,
|
||||||
|
envSlug: envData.envSlug,
|
||||||
|
folders
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
return formattedResult;
|
||||||
|
} catch (error) {
|
||||||
|
throw new DatabaseError({ error, name: "GetSecretImportsAndReferences" });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
return {
|
return {
|
||||||
...secretImportOrm,
|
...secretImportOrm,
|
||||||
find,
|
find,
|
||||||
@@ -176,6 +308,7 @@ export const secretImportDALFactory = (db: TDbClient) => {
|
|||||||
findByFolderIds,
|
findByFolderIds,
|
||||||
findLastImportPosition,
|
findLastImportPosition,
|
||||||
updateAllPosition,
|
updateAllPosition,
|
||||||
getProjectImportCount
|
getProjectImportCount,
|
||||||
|
getFolderIsImportedBy
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -159,7 +159,8 @@ export const fnSecretsV2FromImports = async ({
|
|||||||
decryptor,
|
decryptor,
|
||||||
expandSecretReferences,
|
expandSecretReferences,
|
||||||
hasSecretAccess,
|
hasSecretAccess,
|
||||||
viewSecretValue
|
viewSecretValue,
|
||||||
|
projectId
|
||||||
}: {
|
}: {
|
||||||
secretImports: (Omit<TSecretImports, "importEnv"> & {
|
secretImports: (Omit<TSecretImports, "importEnv"> & {
|
||||||
importEnv: { id: string; slug: string; name: string };
|
importEnv: { id: string; slug: string; name: string };
|
||||||
@@ -176,6 +177,7 @@ export const fnSecretsV2FromImports = async ({
|
|||||||
environment: string;
|
environment: string;
|
||||||
}) => Promise<string | undefined>;
|
}) => Promise<string | undefined>;
|
||||||
hasSecretAccess: (environment: string, secretPath: string, secretName: string, secretTagSlugs: string[]) => boolean;
|
hasSecretAccess: (environment: string, secretPath: string, secretName: string, secretTagSlugs: string[]) => boolean;
|
||||||
|
projectId: string;
|
||||||
}) => {
|
}) => {
|
||||||
const cyclicDetector = new Set();
|
const cyclicDetector = new Set();
|
||||||
const stack: {
|
const stack: {
|
||||||
@@ -216,7 +218,8 @@ export const fnSecretsV2FromImports = async ({
|
|||||||
type: SecretType.Shared
|
type: SecretType.Shared
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
sort: [["id", "asc"]]
|
sort: [["id", "asc"]],
|
||||||
|
useCache: { projectId }
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
const importedSecretsGroupByFolderId = groupBy(importedSecrets, (i) => i.folderId);
|
const importedSecretsGroupByFolderId = groupBy(importedSecrets, (i) => i.folderId);
|
||||||
|
|||||||
@@ -27,6 +27,7 @@ import { decryptSecretRaw } from "../secret/secret-fns";
|
|||||||
import { TSecretQueueFactory } from "../secret/secret-queue";
|
import { TSecretQueueFactory } from "../secret/secret-queue";
|
||||||
import { TSecretFolderDALFactory } from "../secret-folder/secret-folder-dal";
|
import { TSecretFolderDALFactory } from "../secret-folder/secret-folder-dal";
|
||||||
import { TSecretV2BridgeDALFactory } from "../secret-v2-bridge/secret-v2-bridge-dal";
|
import { TSecretV2BridgeDALFactory } from "../secret-v2-bridge/secret-v2-bridge-dal";
|
||||||
|
import { recursivelyGetSecretPaths } from "../secret-v2-bridge/secret-v2-bridge-fns";
|
||||||
import { TSecretImportDALFactory } from "./secret-import-dal";
|
import { TSecretImportDALFactory } from "./secret-import-dal";
|
||||||
import { fnSecretsFromImports, fnSecretsV2FromImports } from "./secret-import-fns";
|
import { fnSecretsFromImports, fnSecretsV2FromImports } from "./secret-import-fns";
|
||||||
import {
|
import {
|
||||||
@@ -43,7 +44,7 @@ type TSecretImportServiceFactoryDep = {
|
|||||||
secretImportDAL: TSecretImportDALFactory;
|
secretImportDAL: TSecretImportDALFactory;
|
||||||
folderDAL: TSecretFolderDALFactory;
|
folderDAL: TSecretFolderDALFactory;
|
||||||
secretDAL: Pick<TSecretDALFactory, "find">;
|
secretDAL: Pick<TSecretDALFactory, "find">;
|
||||||
secretV2BridgeDAL: Pick<TSecretV2BridgeDALFactory, "find">;
|
secretV2BridgeDAL: Pick<TSecretV2BridgeDALFactory, "find" | "invalidateSecretCacheByProjectId">;
|
||||||
projectBotService: Pick<TProjectBotServiceFactory, "getBotKey">;
|
projectBotService: Pick<TProjectBotServiceFactory, "getBotKey">;
|
||||||
projectDAL: Pick<TProjectDALFactory, "checkProjectUpgradeStatus">;
|
projectDAL: Pick<TProjectDALFactory, "checkProjectUpgradeStatus">;
|
||||||
projectEnvDAL: TProjectEnvDALFactory;
|
projectEnvDAL: TProjectEnvDALFactory;
|
||||||
@@ -184,6 +185,7 @@ export const secretImportServiceFactory = ({
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
await secretV2BridgeDAL.invalidateSecretCacheByProjectId(projectId);
|
||||||
return { ...secImport, importEnv };
|
return { ...secImport, importEnv };
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -281,6 +283,8 @@ export const secretImportServiceFactory = ({
|
|||||||
);
|
);
|
||||||
return doc;
|
return doc;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
await secretV2BridgeDAL.invalidateSecretCacheByProjectId(projectId);
|
||||||
return { ...updatedSecImport, importEnv: importedEnv };
|
return { ...updatedSecImport, importEnv: importedEnv };
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -355,6 +359,7 @@ export const secretImportServiceFactory = ({
|
|||||||
actorId
|
actorId
|
||||||
});
|
});
|
||||||
|
|
||||||
|
await secretV2BridgeDAL.invalidateSecretCacheByProjectId(projectId);
|
||||||
return secImport;
|
return secImport;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -693,6 +698,7 @@ export const secretImportServiceFactory = ({
|
|||||||
projectId
|
projectId
|
||||||
});
|
});
|
||||||
const importedSecrets = await fnSecretsV2FromImports({
|
const importedSecrets = await fnSecretsV2FromImports({
|
||||||
|
projectId,
|
||||||
secretImports,
|
secretImports,
|
||||||
folderDAL,
|
folderDAL,
|
||||||
viewSecretValue: true,
|
viewSecretValue: true,
|
||||||
@@ -793,6 +799,136 @@ export const secretImportServiceFactory = ({
|
|||||||
return secImportsArrays.flat();
|
return secImportsArrays.flat();
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const getFolderIsImportedBy = async ({
|
||||||
|
path: secretPath,
|
||||||
|
environment,
|
||||||
|
projectId,
|
||||||
|
actor,
|
||||||
|
actorId,
|
||||||
|
actorAuthMethod,
|
||||||
|
actorOrgId,
|
||||||
|
secrets
|
||||||
|
}: TGetSecretImportsDTO & {
|
||||||
|
secrets: { secretKey: string; secretValue: string }[] | undefined;
|
||||||
|
}) => {
|
||||||
|
const { permission } = await permissionService.getProjectPermission({
|
||||||
|
actor,
|
||||||
|
actorId,
|
||||||
|
projectId,
|
||||||
|
actorAuthMethod,
|
||||||
|
actorOrgId,
|
||||||
|
actionProjectType: ActionProjectType.SecretManager
|
||||||
|
});
|
||||||
|
ForbiddenError.from(permission).throwUnlessCan(
|
||||||
|
ProjectPermissionActions.Read,
|
||||||
|
subject(ProjectPermissionSub.SecretImports, { environment, secretPath })
|
||||||
|
);
|
||||||
|
|
||||||
|
const folder = await folderDAL.findBySecretPath(projectId, environment, secretPath);
|
||||||
|
if (!folder) return [];
|
||||||
|
|
||||||
|
const importedBy = await secretImportDAL.getFolderIsImportedBy(secretPath, folder.envId, environment, projectId);
|
||||||
|
const deepPaths: { path: string; folderId: string }[] = [];
|
||||||
|
|
||||||
|
await Promise.all(
|
||||||
|
importedBy.map(async (el) => {
|
||||||
|
const envDeepPaths = await recursivelyGetSecretPaths({
|
||||||
|
folderDAL,
|
||||||
|
projectEnvDAL,
|
||||||
|
projectId,
|
||||||
|
environment: el.envSlug,
|
||||||
|
currentPath: "/"
|
||||||
|
});
|
||||||
|
deepPaths.push(...envDeepPaths);
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
const result = importedBy.map((el) => ({
|
||||||
|
environment: {
|
||||||
|
name: el.envName,
|
||||||
|
slug: el.envSlug
|
||||||
|
},
|
||||||
|
folders: el.folders.map((folderItem) => ({
|
||||||
|
folderId: folderItem.folderId,
|
||||||
|
isImported: folderItem.folderImported,
|
||||||
|
secrets: folderItem.secrets,
|
||||||
|
name: deepPaths.find((p) => p.folderId === folderItem.folderId)?.path || `...${folderItem.folderName}`
|
||||||
|
}))
|
||||||
|
}));
|
||||||
|
|
||||||
|
// Special case for same folder references as these do not have an entry on the references table
|
||||||
|
const locallyReferenced =
|
||||||
|
secrets
|
||||||
|
?.filter((secret) => {
|
||||||
|
return secrets.some(
|
||||||
|
(otherSecret) =>
|
||||||
|
otherSecret.secretKey !== secret.secretKey && secret.secretValue.includes(`\${${otherSecret.secretKey}}`)
|
||||||
|
);
|
||||||
|
})
|
||||||
|
.flatMap((secret) => {
|
||||||
|
return secrets
|
||||||
|
.filter(
|
||||||
|
(otherSecret) =>
|
||||||
|
otherSecret.secretKey !== secret.secretKey &&
|
||||||
|
secret.secretValue.includes(`\${${otherSecret.secretKey}}`)
|
||||||
|
)
|
||||||
|
.map((otherSecret) => ({
|
||||||
|
secretId: secret.secretKey,
|
||||||
|
referencedSecretKey: otherSecret.secretKey
|
||||||
|
}));
|
||||||
|
}) || [];
|
||||||
|
if (locallyReferenced.length > 0) {
|
||||||
|
const existingEnvIndex = result.findIndex((item) => item.environment.slug === environment);
|
||||||
|
|
||||||
|
if (existingEnvIndex >= 0) {
|
||||||
|
const existingFolderIndex = result[existingEnvIndex].folders.findIndex(
|
||||||
|
(folderItem) => folderItem.name === secretPath
|
||||||
|
);
|
||||||
|
|
||||||
|
if (existingFolderIndex >= 0) {
|
||||||
|
if (!result[existingEnvIndex].folders[existingFolderIndex].secrets) {
|
||||||
|
result[existingEnvIndex].folders[existingFolderIndex].secrets = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
const existingSecrets = result[existingEnvIndex].folders[existingFolderIndex].secrets || [];
|
||||||
|
locallyReferenced.forEach((ref) => {
|
||||||
|
if (
|
||||||
|
!existingSecrets.some(
|
||||||
|
(s) => s.secretId === ref.secretId && s.referencedSecretKey === ref.referencedSecretKey
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
existingSecrets.push(ref);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
result[existingEnvIndex].folders.push({
|
||||||
|
folderId: folder.id,
|
||||||
|
isImported: false,
|
||||||
|
secrets: locallyReferenced,
|
||||||
|
name: secretPath
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
result.push({
|
||||||
|
environment: {
|
||||||
|
slug: environment,
|
||||||
|
name: environment
|
||||||
|
},
|
||||||
|
folders: [
|
||||||
|
{
|
||||||
|
folderId: folder.id,
|
||||||
|
isImported: false,
|
||||||
|
secrets: locallyReferenced,
|
||||||
|
name: secretPath
|
||||||
|
}
|
||||||
|
]
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
};
|
||||||
|
|
||||||
return {
|
return {
|
||||||
createImport,
|
createImport,
|
||||||
updateImport,
|
updateImport,
|
||||||
@@ -805,6 +941,7 @@ export const secretImportServiceFactory = ({
|
|||||||
getProjectImportCount,
|
getProjectImportCount,
|
||||||
fnSecretsFromImports,
|
fnSecretsFromImports,
|
||||||
getProjectImportMultiEnvCount,
|
getProjectImportMultiEnvCount,
|
||||||
getImportsMultiEnv
|
getImportsMultiEnv,
|
||||||
|
getFolderIsImportedBy
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -45,3 +45,29 @@ export type TGetSecretsFromImportDTO = {
|
|||||||
environment: string;
|
environment: string;
|
||||||
path: string;
|
path: string;
|
||||||
} & TProjectPermission;
|
} & TProjectPermission;
|
||||||
|
|
||||||
|
export type FolderResult = {
|
||||||
|
envName: string;
|
||||||
|
folderName: string;
|
||||||
|
folderId: string;
|
||||||
|
envSlug: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type SecretResult = {
|
||||||
|
secretId: string;
|
||||||
|
referencedSecretKey: string;
|
||||||
|
} & FolderResult;
|
||||||
|
|
||||||
|
export type FolderInfo = {
|
||||||
|
folderName: string;
|
||||||
|
secrets?: { secretId: string; referencedSecretKey: string }[];
|
||||||
|
folderId: string;
|
||||||
|
folderImported: boolean;
|
||||||
|
envSlug?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type EnvironmentInfo = {
|
||||||
|
envName: string;
|
||||||
|
envSlug: string;
|
||||||
|
folders: FolderInfo[];
|
||||||
|
};
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ export enum SecretSync {
|
|||||||
AzureAppConfiguration = "azure-app-configuration",
|
AzureAppConfiguration = "azure-app-configuration",
|
||||||
Databricks = "databricks",
|
Databricks = "databricks",
|
||||||
Humanitec = "humanitec",
|
Humanitec = "humanitec",
|
||||||
|
TerraformCloud = "terraform-cloud",
|
||||||
Camunda = "camunda",
|
Camunda = "camunda",
|
||||||
Vercel = "vercel"
|
Vercel = "vercel"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -27,6 +27,7 @@ import { GCP_SYNC_LIST_OPTION } from "./gcp";
|
|||||||
import { GcpSyncFns } from "./gcp/gcp-sync-fns";
|
import { GcpSyncFns } from "./gcp/gcp-sync-fns";
|
||||||
import { HUMANITEC_SYNC_LIST_OPTION } from "./humanitec";
|
import { HUMANITEC_SYNC_LIST_OPTION } from "./humanitec";
|
||||||
import { HumanitecSyncFns } from "./humanitec/humanitec-sync-fns";
|
import { HumanitecSyncFns } from "./humanitec/humanitec-sync-fns";
|
||||||
|
import { TERRAFORM_CLOUD_SYNC_LIST_OPTION, TerraformCloudSyncFns } from "./terraform-cloud";
|
||||||
import { VERCEL_SYNC_LIST_OPTION, VercelSyncFns } from "./vercel";
|
import { VERCEL_SYNC_LIST_OPTION, VercelSyncFns } from "./vercel";
|
||||||
|
|
||||||
const SECRET_SYNC_LIST_OPTIONS: Record<SecretSync, TSecretSyncListItem> = {
|
const SECRET_SYNC_LIST_OPTIONS: Record<SecretSync, TSecretSyncListItem> = {
|
||||||
@@ -38,6 +39,7 @@ const SECRET_SYNC_LIST_OPTIONS: Record<SecretSync, TSecretSyncListItem> = {
|
|||||||
[SecretSync.AzureAppConfiguration]: AZURE_APP_CONFIGURATION_SYNC_LIST_OPTION,
|
[SecretSync.AzureAppConfiguration]: AZURE_APP_CONFIGURATION_SYNC_LIST_OPTION,
|
||||||
[SecretSync.Databricks]: DATABRICKS_SYNC_LIST_OPTION,
|
[SecretSync.Databricks]: DATABRICKS_SYNC_LIST_OPTION,
|
||||||
[SecretSync.Humanitec]: HUMANITEC_SYNC_LIST_OPTION,
|
[SecretSync.Humanitec]: HUMANITEC_SYNC_LIST_OPTION,
|
||||||
|
[SecretSync.TerraformCloud]: TERRAFORM_CLOUD_SYNC_LIST_OPTION,
|
||||||
[SecretSync.Camunda]: CAMUNDA_SYNC_LIST_OPTION,
|
[SecretSync.Camunda]: CAMUNDA_SYNC_LIST_OPTION,
|
||||||
[SecretSync.Vercel]: VERCEL_SYNC_LIST_OPTION
|
[SecretSync.Vercel]: VERCEL_SYNC_LIST_OPTION
|
||||||
};
|
};
|
||||||
@@ -125,6 +127,8 @@ export const SecretSyncFns = {
|
|||||||
}).syncSecrets(secretSync, secretMap);
|
}).syncSecrets(secretSync, secretMap);
|
||||||
case SecretSync.Humanitec:
|
case SecretSync.Humanitec:
|
||||||
return HumanitecSyncFns.syncSecrets(secretSync, secretMap);
|
return HumanitecSyncFns.syncSecrets(secretSync, secretMap);
|
||||||
|
case SecretSync.TerraformCloud:
|
||||||
|
return TerraformCloudSyncFns.syncSecrets(secretSync, secretMap);
|
||||||
case SecretSync.Camunda:
|
case SecretSync.Camunda:
|
||||||
return camundaSyncFactory({
|
return camundaSyncFactory({
|
||||||
appConnectionDAL,
|
appConnectionDAL,
|
||||||
@@ -176,6 +180,9 @@ export const SecretSyncFns = {
|
|||||||
case SecretSync.Humanitec:
|
case SecretSync.Humanitec:
|
||||||
secretMap = await HumanitecSyncFns.getSecrets(secretSync);
|
secretMap = await HumanitecSyncFns.getSecrets(secretSync);
|
||||||
break;
|
break;
|
||||||
|
case SecretSync.TerraformCloud:
|
||||||
|
secretMap = await TerraformCloudSyncFns.getSecrets(secretSync);
|
||||||
|
break;
|
||||||
case SecretSync.Camunda:
|
case SecretSync.Camunda:
|
||||||
secretMap = await camundaSyncFactory({
|
secretMap = await camundaSyncFactory({
|
||||||
appConnectionDAL,
|
appConnectionDAL,
|
||||||
@@ -227,6 +234,8 @@ export const SecretSyncFns = {
|
|||||||
}).removeSecrets(secretSync, secretMap);
|
}).removeSecrets(secretSync, secretMap);
|
||||||
case SecretSync.Humanitec:
|
case SecretSync.Humanitec:
|
||||||
return HumanitecSyncFns.removeSecrets(secretSync, secretMap);
|
return HumanitecSyncFns.removeSecrets(secretSync, secretMap);
|
||||||
|
case SecretSync.TerraformCloud:
|
||||||
|
return TerraformCloudSyncFns.removeSecrets(secretSync, secretMap);
|
||||||
case SecretSync.Camunda:
|
case SecretSync.Camunda:
|
||||||
return camundaSyncFactory({
|
return camundaSyncFactory({
|
||||||
appConnectionDAL,
|
appConnectionDAL,
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ export const SECRET_SYNC_NAME_MAP: Record<SecretSync, string> = {
|
|||||||
[SecretSync.AzureAppConfiguration]: "Azure App Configuration",
|
[SecretSync.AzureAppConfiguration]: "Azure App Configuration",
|
||||||
[SecretSync.Databricks]: "Databricks",
|
[SecretSync.Databricks]: "Databricks",
|
||||||
[SecretSync.Humanitec]: "Humanitec",
|
[SecretSync.Humanitec]: "Humanitec",
|
||||||
|
[SecretSync.TerraformCloud]: "Terraform Cloud",
|
||||||
[SecretSync.Camunda]: "Camunda",
|
[SecretSync.Camunda]: "Camunda",
|
||||||
[SecretSync.Vercel]: "Vercel"
|
[SecretSync.Vercel]: "Vercel"
|
||||||
};
|
};
|
||||||
@@ -23,6 +24,7 @@ export const SECRET_SYNC_CONNECTION_MAP: Record<SecretSync, AppConnection> = {
|
|||||||
[SecretSync.AzureAppConfiguration]: AppConnection.AzureAppConfiguration,
|
[SecretSync.AzureAppConfiguration]: AppConnection.AzureAppConfiguration,
|
||||||
[SecretSync.Databricks]: AppConnection.Databricks,
|
[SecretSync.Databricks]: AppConnection.Databricks,
|
||||||
[SecretSync.Humanitec]: AppConnection.Humanitec,
|
[SecretSync.Humanitec]: AppConnection.Humanitec,
|
||||||
|
[SecretSync.TerraformCloud]: AppConnection.TerraformCloud,
|
||||||
[SecretSync.Camunda]: AppConnection.Camunda,
|
[SecretSync.Camunda]: AppConnection.Camunda,
|
||||||
[SecretSync.Vercel]: AppConnection.Vercel
|
[SecretSync.Vercel]: AppConnection.Vercel
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -213,7 +213,7 @@ export const secretSyncQueueFactory = ({
|
|||||||
canExpandValue: () => true
|
canExpandValue: () => true
|
||||||
});
|
});
|
||||||
|
|
||||||
const secrets = await secretV2BridgeDAL.findByFolderId(folderId);
|
const secrets = await secretV2BridgeDAL.findByFolderId({ folderId, projectId });
|
||||||
|
|
||||||
await Promise.allSettled(
|
await Promise.allSettled(
|
||||||
secrets.map(async (secret) => {
|
secrets.map(async (secret) => {
|
||||||
@@ -243,6 +243,7 @@ export const secretSyncQueueFactory = ({
|
|||||||
|
|
||||||
if (secretImports.length) {
|
if (secretImports.length) {
|
||||||
const importedSecrets = await fnSecretsV2FromImports({
|
const importedSecrets = await fnSecretsV2FromImports({
|
||||||
|
projectId,
|
||||||
decryptor: decryptSecretValue,
|
decryptor: decryptSecretValue,
|
||||||
folderDAL,
|
folderDAL,
|
||||||
secretDAL: secretV2BridgeDAL,
|
secretDAL: secretV2BridgeDAL,
|
||||||
|
|||||||
@@ -55,6 +55,12 @@ import {
|
|||||||
THumanitecSyncListItem,
|
THumanitecSyncListItem,
|
||||||
THumanitecSyncWithCredentials
|
THumanitecSyncWithCredentials
|
||||||
} from "./humanitec";
|
} from "./humanitec";
|
||||||
|
import {
|
||||||
|
TTerraformCloudSync,
|
||||||
|
TTerraformCloudSyncInput,
|
||||||
|
TTerraformCloudSyncListItem,
|
||||||
|
TTerraformCloudSyncWithCredentials
|
||||||
|
} from "./terraform-cloud";
|
||||||
import { TVercelSync, TVercelSyncInput, TVercelSyncListItem, TVercelSyncWithCredentials } from "./vercel";
|
import { TVercelSync, TVercelSyncInput, TVercelSyncListItem, TVercelSyncWithCredentials } from "./vercel";
|
||||||
|
|
||||||
export type TSecretSync =
|
export type TSecretSync =
|
||||||
@@ -66,6 +72,7 @@ export type TSecretSync =
|
|||||||
| TAzureAppConfigurationSync
|
| TAzureAppConfigurationSync
|
||||||
| TDatabricksSync
|
| TDatabricksSync
|
||||||
| THumanitecSync
|
| THumanitecSync
|
||||||
|
| TTerraformCloudSync
|
||||||
| TCamundaSync
|
| TCamundaSync
|
||||||
| TVercelSync;
|
| TVercelSync;
|
||||||
|
|
||||||
@@ -78,6 +85,7 @@ export type TSecretSyncWithCredentials =
|
|||||||
| TAzureAppConfigurationSyncWithCredentials
|
| TAzureAppConfigurationSyncWithCredentials
|
||||||
| TDatabricksSyncWithCredentials
|
| TDatabricksSyncWithCredentials
|
||||||
| THumanitecSyncWithCredentials
|
| THumanitecSyncWithCredentials
|
||||||
|
| TTerraformCloudSyncWithCredentials
|
||||||
| TCamundaSyncWithCredentials
|
| TCamundaSyncWithCredentials
|
||||||
| TVercelSyncWithCredentials;
|
| TVercelSyncWithCredentials;
|
||||||
|
|
||||||
@@ -90,6 +98,7 @@ export type TSecretSyncInput =
|
|||||||
| TAzureAppConfigurationSyncInput
|
| TAzureAppConfigurationSyncInput
|
||||||
| TDatabricksSyncInput
|
| TDatabricksSyncInput
|
||||||
| THumanitecSyncInput
|
| THumanitecSyncInput
|
||||||
|
| TTerraformCloudSyncInput
|
||||||
| TCamundaSyncInput
|
| TCamundaSyncInput
|
||||||
| TVercelSyncInput;
|
| TVercelSyncInput;
|
||||||
|
|
||||||
@@ -102,6 +111,7 @@ export type TSecretSyncListItem =
|
|||||||
| TAzureAppConfigurationSyncListItem
|
| TAzureAppConfigurationSyncListItem
|
||||||
| TDatabricksSyncListItem
|
| TDatabricksSyncListItem
|
||||||
| THumanitecSyncListItem
|
| THumanitecSyncListItem
|
||||||
|
| TTerraformCloudSyncListItem
|
||||||
| TCamundaSyncListItem
|
| TCamundaSyncListItem
|
||||||
| TVercelSyncListItem;
|
| TVercelSyncListItem;
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,5 @@
|
|||||||
|
export * from "./terraform-cloud-sync-constants";
|
||||||
|
export * from "./terraform-cloud-sync-enums";
|
||||||
|
export * from "./terraform-cloud-sync-fns";
|
||||||
|
export * from "./terraform-cloud-sync-schemas";
|
||||||
|
export * from "./terraform-cloud-sync-types";
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
import { AppConnection } from "@app/services/app-connection/app-connection-enums";
|
||||||
|
import { SecretSync } from "@app/services/secret-sync/secret-sync-enums";
|
||||||
|
import { TSecretSyncListItem } from "@app/services/secret-sync/secret-sync-types";
|
||||||
|
|
||||||
|
export const TERRAFORM_CLOUD_SYNC_LIST_OPTION: TSecretSyncListItem = {
|
||||||
|
name: "Terraform Cloud",
|
||||||
|
destination: SecretSync.TerraformCloud,
|
||||||
|
connection: AppConnection.TerraformCloud,
|
||||||
|
canImportSecrets: false
|
||||||
|
};
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
export enum TerraformCloudSyncScope {
|
||||||
|
VariableSet = "variable-set",
|
||||||
|
Workspace = "workspace"
|
||||||
|
}
|
||||||
|
|
||||||
|
export enum TerraformCloudSyncCategory {
|
||||||
|
Environment = "env",
|
||||||
|
Terraform = "terraform"
|
||||||
|
}
|
||||||
@@ -0,0 +1,253 @@
|
|||||||
|
/* eslint-disable no-await-in-loop */
|
||||||
|
import { AxiosResponse } from "axios";
|
||||||
|
|
||||||
|
import { request } from "@app/lib/config/request";
|
||||||
|
import { IntegrationUrls } from "@app/services/integration-auth/integration-list";
|
||||||
|
import { SecretSyncError } from "@app/services/secret-sync/secret-sync-errors";
|
||||||
|
import { TSecretMap } from "@app/services/secret-sync/secret-sync-types";
|
||||||
|
|
||||||
|
import { SECRET_SYNC_NAME_MAP } from "../secret-sync-maps";
|
||||||
|
import { TerraformCloudSyncScope } from "./terraform-cloud-sync-enums";
|
||||||
|
import {
|
||||||
|
TerraformCloudApiResponse,
|
||||||
|
TerraformCloudApiVariable,
|
||||||
|
TerraformCloudVariable,
|
||||||
|
TTerraformCloudSyncWithCredentials
|
||||||
|
} from "./terraform-cloud-sync-types";
|
||||||
|
|
||||||
|
const getTerraformCloudVariables = async (
|
||||||
|
secretSync: TTerraformCloudSyncWithCredentials
|
||||||
|
): Promise<TerraformCloudVariable[]> => {
|
||||||
|
const {
|
||||||
|
destinationConfig,
|
||||||
|
connection: {
|
||||||
|
credentials: { apiToken }
|
||||||
|
}
|
||||||
|
} = secretSync;
|
||||||
|
|
||||||
|
let url: string;
|
||||||
|
let source: TerraformCloudVariable["source"];
|
||||||
|
|
||||||
|
if (destinationConfig.scope === TerraformCloudSyncScope.VariableSet) {
|
||||||
|
url = `${IntegrationUrls.TERRAFORM_CLOUD_API_URL}/api/v2/varsets/${destinationConfig.variableSetId}/relationships/vars`;
|
||||||
|
source = "varset";
|
||||||
|
} else {
|
||||||
|
url = `${IntegrationUrls.TERRAFORM_CLOUD_API_URL}/api/v2/workspaces/${destinationConfig.workspaceId}/vars`;
|
||||||
|
source = "workspace";
|
||||||
|
}
|
||||||
|
|
||||||
|
const headers = {
|
||||||
|
Authorization: `Bearer ${apiToken}`,
|
||||||
|
"Content-Type": "application/vnd.api+json"
|
||||||
|
};
|
||||||
|
|
||||||
|
const fetchAllPages = async (): Promise<TerraformCloudApiVariable[]> => {
|
||||||
|
let results: TerraformCloudApiVariable[] = [];
|
||||||
|
let nextUrl: string | null = url;
|
||||||
|
|
||||||
|
while (nextUrl) {
|
||||||
|
const res: AxiosResponse<TerraformCloudApiResponse<TerraformCloudApiVariable[]>> = await request.get<
|
||||||
|
TerraformCloudApiResponse<TerraformCloudApiVariable[]>
|
||||||
|
>(nextUrl, {
|
||||||
|
headers
|
||||||
|
});
|
||||||
|
|
||||||
|
if (res.data?.data) {
|
||||||
|
results = results.concat(res.data.data);
|
||||||
|
}
|
||||||
|
|
||||||
|
nextUrl = res.data?.links?.next ?? null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return results;
|
||||||
|
};
|
||||||
|
|
||||||
|
const allVariableData = await fetchAllPages();
|
||||||
|
|
||||||
|
const variables: TerraformCloudVariable[] = allVariableData.map((variable) => ({
|
||||||
|
id: variable.id,
|
||||||
|
key: variable.attributes.key,
|
||||||
|
value: variable.attributes.value || "",
|
||||||
|
sensitive: variable.attributes.sensitive,
|
||||||
|
description: variable.attributes.description || "",
|
||||||
|
category: variable.attributes.category,
|
||||||
|
source
|
||||||
|
}));
|
||||||
|
|
||||||
|
return variables;
|
||||||
|
};
|
||||||
|
|
||||||
|
const deleteVariable = async (
|
||||||
|
secretSync: TTerraformCloudSyncWithCredentials,
|
||||||
|
variable: TerraformCloudVariable
|
||||||
|
): Promise<void> => {
|
||||||
|
const {
|
||||||
|
destinationConfig,
|
||||||
|
connection: {
|
||||||
|
credentials: { apiToken }
|
||||||
|
}
|
||||||
|
} = secretSync;
|
||||||
|
|
||||||
|
try {
|
||||||
|
let url;
|
||||||
|
|
||||||
|
if (destinationConfig.scope === TerraformCloudSyncScope.VariableSet) {
|
||||||
|
url = `${IntegrationUrls.TERRAFORM_CLOUD_API_URL}/api/v2/varsets/${destinationConfig.variableSetId}/relationships/vars/${variable.id}`;
|
||||||
|
} else {
|
||||||
|
url = `${IntegrationUrls.TERRAFORM_CLOUD_API_URL}/api/v2/workspaces/${destinationConfig.workspaceId}/vars/${variable.id}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
await request.delete(url, {
|
||||||
|
headers: {
|
||||||
|
Authorization: `Bearer ${apiToken}`,
|
||||||
|
"Content-Type": "application/vnd.api+json"
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
throw new SecretSyncError({
|
||||||
|
error,
|
||||||
|
secretKey: variable.key
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const createVariable = async (
|
||||||
|
secretSync: TTerraformCloudSyncWithCredentials,
|
||||||
|
secretMap: TSecretMap,
|
||||||
|
key: string
|
||||||
|
): Promise<void> => {
|
||||||
|
try {
|
||||||
|
const {
|
||||||
|
destinationConfig,
|
||||||
|
connection: {
|
||||||
|
credentials: { apiToken }
|
||||||
|
}
|
||||||
|
} = secretSync;
|
||||||
|
|
||||||
|
let url;
|
||||||
|
|
||||||
|
if (destinationConfig.scope === TerraformCloudSyncScope.VariableSet) {
|
||||||
|
url = `${IntegrationUrls.TERRAFORM_CLOUD_API_URL}/api/v2/varsets/${destinationConfig.variableSetId}/relationships/vars`;
|
||||||
|
} else {
|
||||||
|
url = `${IntegrationUrls.TERRAFORM_CLOUD_API_URL}/api/v2/workspaces/${destinationConfig.workspaceId}/vars`;
|
||||||
|
}
|
||||||
|
|
||||||
|
await request.post(
|
||||||
|
url,
|
||||||
|
{
|
||||||
|
data: {
|
||||||
|
type: "vars",
|
||||||
|
attributes: {
|
||||||
|
key,
|
||||||
|
value: secretMap[key].value,
|
||||||
|
description: secretMap[key].comment || "",
|
||||||
|
category: secretSync.destinationConfig.category,
|
||||||
|
sensitive: true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
headers: {
|
||||||
|
Authorization: `Bearer ${apiToken}`,
|
||||||
|
"Content-Type": "application/vnd.api+json"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
} catch (error) {
|
||||||
|
throw new SecretSyncError({
|
||||||
|
error,
|
||||||
|
secretKey: key
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const updateVariable = async (
|
||||||
|
secretSync: TTerraformCloudSyncWithCredentials,
|
||||||
|
secretMap: TSecretMap,
|
||||||
|
variable: TerraformCloudVariable
|
||||||
|
): Promise<void> => {
|
||||||
|
try {
|
||||||
|
const {
|
||||||
|
destinationConfig,
|
||||||
|
connection: {
|
||||||
|
credentials: { apiToken }
|
||||||
|
}
|
||||||
|
} = secretSync;
|
||||||
|
|
||||||
|
let url;
|
||||||
|
|
||||||
|
if (destinationConfig.scope === TerraformCloudSyncScope.VariableSet) {
|
||||||
|
url = `${IntegrationUrls.TERRAFORM_CLOUD_API_URL}/api/v2/varsets/${destinationConfig.variableSetId}/relationships/vars/${variable.id}`;
|
||||||
|
} else {
|
||||||
|
url = `${IntegrationUrls.TERRAFORM_CLOUD_API_URL}/api/v2/workspaces/${destinationConfig.workspaceId}/vars/${variable.id}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
await request.patch(
|
||||||
|
url,
|
||||||
|
{
|
||||||
|
data: {
|
||||||
|
type: "vars",
|
||||||
|
id: variable.id,
|
||||||
|
attributes: {
|
||||||
|
value: secretMap[variable.key].value,
|
||||||
|
description: secretMap[variable.key].comment || "",
|
||||||
|
category: secretSync.destinationConfig.category
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
headers: {
|
||||||
|
Authorization: `Bearer ${apiToken}`,
|
||||||
|
"Content-Type": "application/vnd.api+json"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
} catch (error) {
|
||||||
|
throw new SecretSyncError({
|
||||||
|
error,
|
||||||
|
secretKey: variable.key
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export const TerraformCloudSyncFns = {
|
||||||
|
syncSecrets: async (secretSync: TTerraformCloudSyncWithCredentials, secretMap: TSecretMap): Promise<void> => {
|
||||||
|
const terraformCloudVariables = await getTerraformCloudVariables(secretSync);
|
||||||
|
const terraformCloudVariablesMap = new Map<string, TerraformCloudVariable>(
|
||||||
|
terraformCloudVariables.map((v) => [v.key, v])
|
||||||
|
);
|
||||||
|
|
||||||
|
const secretKeys = Object.keys(secretMap);
|
||||||
|
for (const key of secretKeys) {
|
||||||
|
const existingVariable = terraformCloudVariablesMap.get(key);
|
||||||
|
|
||||||
|
if (!existingVariable) {
|
||||||
|
await createVariable(secretSync, secretMap, key);
|
||||||
|
} else {
|
||||||
|
await updateVariable(secretSync, secretMap, existingVariable);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (secretSync.syncOptions.disableSecretDeletion) return;
|
||||||
|
|
||||||
|
for (const terraformCloudVariable of terraformCloudVariables) {
|
||||||
|
if (!Object.prototype.hasOwnProperty.call(secretMap, terraformCloudVariable.key)) {
|
||||||
|
await deleteVariable(secretSync, terraformCloudVariable);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
getSecrets: async (secretSync: TTerraformCloudSyncWithCredentials): Promise<TSecretMap> => {
|
||||||
|
throw new Error(`${SECRET_SYNC_NAME_MAP[secretSync.destination]} does not support importing secrets.`);
|
||||||
|
},
|
||||||
|
|
||||||
|
removeSecrets: async (secretSync: TTerraformCloudSyncWithCredentials, secretMap: TSecretMap): Promise<void> => {
|
||||||
|
const terraformCloudVariables = await getTerraformCloudVariables(secretSync);
|
||||||
|
|
||||||
|
for (const variable of terraformCloudVariables) {
|
||||||
|
if (Object.prototype.hasOwnProperty.call(secretMap, variable.key)) {
|
||||||
|
await deleteVariable(secretSync, variable);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -0,0 +1,77 @@
|
|||||||
|
import { z } from "zod";
|
||||||
|
|
||||||
|
import { SecretSyncs } from "@app/lib/api-docs";
|
||||||
|
import { AppConnection } from "@app/services/app-connection/app-connection-enums";
|
||||||
|
import { SecretSync } from "@app/services/secret-sync/secret-sync-enums";
|
||||||
|
import {
|
||||||
|
BaseSecretSyncSchema,
|
||||||
|
GenericCreateSecretSyncFieldsSchema,
|
||||||
|
GenericUpdateSecretSyncFieldsSchema
|
||||||
|
} from "@app/services/secret-sync/secret-sync-schemas";
|
||||||
|
import { TSyncOptionsConfig } from "@app/services/secret-sync/secret-sync-types";
|
||||||
|
import {
|
||||||
|
TerraformCloudSyncCategory,
|
||||||
|
TerraformCloudSyncScope
|
||||||
|
} from "@app/services/secret-sync/terraform-cloud/terraform-cloud-sync-enums";
|
||||||
|
|
||||||
|
const TerraformCloudSyncDestinationConfigSchema = z.discriminatedUnion("scope", [
|
||||||
|
z.object({
|
||||||
|
scope: z
|
||||||
|
.literal(TerraformCloudSyncScope.VariableSet)
|
||||||
|
.describe(SecretSyncs.DESTINATION_CONFIG.TERRAFORM_CLOUD.scope),
|
||||||
|
org: z.string().min(1, "Org ID is required").describe(SecretSyncs.DESTINATION_CONFIG.TERRAFORM_CLOUD.org),
|
||||||
|
variableSetName: z
|
||||||
|
.string()
|
||||||
|
.min(1, "Variable set name is required")
|
||||||
|
.describe(SecretSyncs.DESTINATION_CONFIG.TERRAFORM_CLOUD.variableSetName),
|
||||||
|
variableSetId: z
|
||||||
|
.string()
|
||||||
|
.min(1, "Variable set ID is required")
|
||||||
|
.describe(SecretSyncs.DESTINATION_CONFIG.TERRAFORM_CLOUD.variableSetId),
|
||||||
|
category: z.nativeEnum(TerraformCloudSyncCategory).describe(SecretSyncs.DESTINATION_CONFIG.TERRAFORM_CLOUD.category)
|
||||||
|
}),
|
||||||
|
z.object({
|
||||||
|
scope: z.literal(TerraformCloudSyncScope.Workspace).describe(SecretSyncs.DESTINATION_CONFIG.TERRAFORM_CLOUD.scope),
|
||||||
|
org: z.string().min(1, "Org ID is required").describe(SecretSyncs.DESTINATION_CONFIG.TERRAFORM_CLOUD.org),
|
||||||
|
workspaceName: z
|
||||||
|
.string()
|
||||||
|
.min(1, "Workspace name is required")
|
||||||
|
.describe(SecretSyncs.DESTINATION_CONFIG.TERRAFORM_CLOUD.workspaceName),
|
||||||
|
workspaceId: z
|
||||||
|
.string()
|
||||||
|
.min(1, "Workspace ID is required")
|
||||||
|
.describe(SecretSyncs.DESTINATION_CONFIG.TERRAFORM_CLOUD.workspaceId),
|
||||||
|
category: z.nativeEnum(TerraformCloudSyncCategory).describe(SecretSyncs.DESTINATION_CONFIG.TERRAFORM_CLOUD.category)
|
||||||
|
})
|
||||||
|
]);
|
||||||
|
|
||||||
|
const TerraformCloudSyncOptionsConfig: TSyncOptionsConfig = { canImportSecrets: false };
|
||||||
|
|
||||||
|
export const TerraformCloudSyncSchema = BaseSecretSyncSchema(
|
||||||
|
SecretSync.TerraformCloud,
|
||||||
|
TerraformCloudSyncOptionsConfig
|
||||||
|
).extend({
|
||||||
|
destination: z.literal(SecretSync.TerraformCloud),
|
||||||
|
destinationConfig: TerraformCloudSyncDestinationConfigSchema
|
||||||
|
});
|
||||||
|
|
||||||
|
export const CreateTerraformCloudSyncSchema = GenericCreateSecretSyncFieldsSchema(
|
||||||
|
SecretSync.TerraformCloud,
|
||||||
|
TerraformCloudSyncOptionsConfig
|
||||||
|
).extend({
|
||||||
|
destinationConfig: TerraformCloudSyncDestinationConfigSchema
|
||||||
|
});
|
||||||
|
|
||||||
|
export const UpdateTerraformCloudSyncSchema = GenericUpdateSecretSyncFieldsSchema(
|
||||||
|
SecretSync.TerraformCloud,
|
||||||
|
TerraformCloudSyncOptionsConfig
|
||||||
|
).extend({
|
||||||
|
destinationConfig: TerraformCloudSyncDestinationConfigSchema.optional()
|
||||||
|
});
|
||||||
|
|
||||||
|
export const TerraformCloudSyncListItemSchema = z.object({
|
||||||
|
name: z.literal("Terraform Cloud"),
|
||||||
|
connection: z.literal(AppConnection.TerraformCloud),
|
||||||
|
destination: z.literal(SecretSync.TerraformCloud),
|
||||||
|
canImportSecrets: z.literal(false)
|
||||||
|
});
|
||||||
@@ -0,0 +1,77 @@
|
|||||||
|
import z from "zod";
|
||||||
|
|
||||||
|
import { TTerraformCloudConnection } from "@app/services/app-connection/terraform-cloud";
|
||||||
|
|
||||||
|
import {
|
||||||
|
CreateTerraformCloudSyncSchema,
|
||||||
|
TerraformCloudSyncListItemSchema,
|
||||||
|
TerraformCloudSyncSchema
|
||||||
|
} from "./terraform-cloud-sync-schemas";
|
||||||
|
|
||||||
|
export type TTerraformCloudSyncListItem = z.infer<typeof TerraformCloudSyncListItemSchema>;
|
||||||
|
|
||||||
|
export type TTerraformCloudSync = z.infer<typeof TerraformCloudSyncSchema>;
|
||||||
|
|
||||||
|
export type TTerraformCloudSyncInput = z.infer<typeof CreateTerraformCloudSyncSchema>;
|
||||||
|
|
||||||
|
export type TTerraformCloudSyncWithCredentials = TTerraformCloudSync & {
|
||||||
|
connection: TTerraformCloudConnection;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type TerraformCloudApiVariable = {
|
||||||
|
id: string;
|
||||||
|
type: string;
|
||||||
|
attributes: {
|
||||||
|
key: string;
|
||||||
|
value: string | null;
|
||||||
|
sensitive: boolean;
|
||||||
|
category: "terraform" | "env";
|
||||||
|
hcl: boolean;
|
||||||
|
description: string | null;
|
||||||
|
};
|
||||||
|
relationships: {
|
||||||
|
workspace?: {
|
||||||
|
data: {
|
||||||
|
id: string;
|
||||||
|
type: string;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
project?: {
|
||||||
|
data: {
|
||||||
|
id: string;
|
||||||
|
type: string;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export type TerraformCloudVariable = {
|
||||||
|
id: string;
|
||||||
|
key: string;
|
||||||
|
value: string;
|
||||||
|
sensitive: boolean;
|
||||||
|
description: string;
|
||||||
|
category: "terraform" | "env";
|
||||||
|
source: "varset" | "workspace";
|
||||||
|
};
|
||||||
|
|
||||||
|
export type TerraformCloudApiResponse<T> = {
|
||||||
|
data: T;
|
||||||
|
included?: unknown[];
|
||||||
|
links?: {
|
||||||
|
self?: string;
|
||||||
|
first?: string;
|
||||||
|
prev?: string;
|
||||||
|
next?: string;
|
||||||
|
last?: string;
|
||||||
|
};
|
||||||
|
meta?: {
|
||||||
|
pagination?: {
|
||||||
|
current_page: number;
|
||||||
|
prev_page: number | null;
|
||||||
|
next_page: number | null;
|
||||||
|
total_pages: number;
|
||||||
|
total_count: number;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
@@ -2,7 +2,10 @@ import { Knex } from "knex";
|
|||||||
import { validate as uuidValidate } from "uuid";
|
import { validate as uuidValidate } from "uuid";
|
||||||
|
|
||||||
import { TDbClient } from "@app/db";
|
import { TDbClient } from "@app/db";
|
||||||
import { SecretsV2Schema, SecretType, TableName, TSecretsV2, TSecretsV2Update } from "@app/db/schemas";
|
import { ProjectType, SecretsV2Schema, SecretType, TableName, TSecretsV2, TSecretsV2Update } from "@app/db/schemas";
|
||||||
|
import { TKeyStoreFactory } from "@app/keystore/keystore";
|
||||||
|
import { getConfig } from "@app/lib/config/env";
|
||||||
|
import { generateCacheKeyFromData } from "@app/lib/crypto/cache";
|
||||||
import { BadRequestError, DatabaseError, NotFoundError } from "@app/lib/errors";
|
import { BadRequestError, DatabaseError, NotFoundError } from "@app/lib/errors";
|
||||||
import {
|
import {
|
||||||
buildFindFilter,
|
buildFindFilter,
|
||||||
@@ -12,15 +15,67 @@ import {
|
|||||||
TFindFilter,
|
TFindFilter,
|
||||||
TFindOpt
|
TFindOpt
|
||||||
} from "@app/lib/knex";
|
} from "@app/lib/knex";
|
||||||
import { OrderByDirection } from "@app/lib/types";
|
import { BufferKeysToString, OrderByDirection } from "@app/lib/types";
|
||||||
import { SecretsOrderBy } from "@app/services/secret/secret-types";
|
import { SecretsOrderBy } from "@app/services/secret/secret-types";
|
||||||
import { TFindSecretsByFolderIdsFilter } from "@app/services/secret-v2-bridge/secret-v2-bridge-types";
|
import type { TFindSecretsByFolderIdsFilter } from "@app/services/secret-v2-bridge/secret-v2-bridge-types";
|
||||||
|
|
||||||
|
export const SecretDalCacheKeys = {
|
||||||
|
get productKey() {
|
||||||
|
const { INFISICAL_PLATFORM_VERSION } = getConfig();
|
||||||
|
return `${ProjectType.SecretManager}:${INFISICAL_PLATFORM_VERSION || 0}`;
|
||||||
|
},
|
||||||
|
getSecretDalVersion: (projectId: string) => {
|
||||||
|
return `${SecretDalCacheKeys.productKey}:${projectId}:${TableName.SecretV2}-dal-version`;
|
||||||
|
},
|
||||||
|
findByFolderIds: (
|
||||||
|
projectId: string,
|
||||||
|
version: number,
|
||||||
|
{ useCache, tx, ...cacheKey }: Parameters<TSecretV2BridgeDALFactory["findByFolderIds"]>[0]
|
||||||
|
) => {
|
||||||
|
return `${SecretDalCacheKeys.productKey}:${projectId}:${
|
||||||
|
TableName.SecretV2
|
||||||
|
}-dal:v${version}:find-by-folder-ids:${generateCacheKeyFromData(cacheKey)}`;
|
||||||
|
},
|
||||||
|
findByFolderId: (
|
||||||
|
projectId: string,
|
||||||
|
version: number,
|
||||||
|
{ useCache, tx, ...cacheKey }: Parameters<TSecretV2BridgeDALFactory["findByFolderId"]>[0]
|
||||||
|
) => {
|
||||||
|
return `${SecretDalCacheKeys.productKey}:${projectId}:${
|
||||||
|
TableName.SecretV2
|
||||||
|
}-dal:v${version}:find-by-folder-id:${generateCacheKeyFromData(cacheKey)}`;
|
||||||
|
},
|
||||||
|
find: (projectId: string, version: number, ...args: Parameters<TSecretV2BridgeDALFactory["find"]>) => {
|
||||||
|
const [filter, opts] = args;
|
||||||
|
delete opts?.tx;
|
||||||
|
delete opts?.useCache;
|
||||||
|
return `${SecretDalCacheKeys.productKey}:${projectId}:${
|
||||||
|
TableName.SecretV2
|
||||||
|
}-dal:v${version}:find:${generateCacheKeyFromData({
|
||||||
|
filter,
|
||||||
|
opts
|
||||||
|
})}`;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
export type TSecretV2BridgeDALFactory = ReturnType<typeof secretV2BridgeDALFactory>;
|
export type TSecretV2BridgeDALFactory = ReturnType<typeof secretV2BridgeDALFactory>;
|
||||||
|
interface TSecretV2DalArg {
|
||||||
|
db: TDbClient;
|
||||||
|
keyStore: TKeyStoreFactory;
|
||||||
|
}
|
||||||
|
|
||||||
export const secretV2BridgeDALFactory = (db: TDbClient) => {
|
const SECRET_DAL_TTL = 5 * 60;
|
||||||
|
const SECRET_DAL_VERSION_TTL = 15 * 60;
|
||||||
|
const MAX_SECRET_CACHE_BYTES = 25 * 1024 * 1024;
|
||||||
|
export const secretV2BridgeDALFactory = ({ db, keyStore }: TSecretV2DalArg) => {
|
||||||
const secretOrm = ormify(db, TableName.SecretV2);
|
const secretOrm = ormify(db, TableName.SecretV2);
|
||||||
|
|
||||||
|
const invalidateSecretCacheByProjectId = async (projectId: string) => {
|
||||||
|
const secretDalVersionKey = SecretDalCacheKeys.getSecretDalVersion(projectId);
|
||||||
|
await keyStore.incrementBy(secretDalVersionKey, 1);
|
||||||
|
await keyStore.setExpiry(secretDalVersionKey, SECRET_DAL_VERSION_TTL);
|
||||||
|
};
|
||||||
|
|
||||||
const findOne = async (filter: Partial<TSecretsV2>, tx?: Knex) => {
|
const findOne = async (filter: Partial<TSecretsV2>, tx?: Knex) => {
|
||||||
try {
|
try {
|
||||||
const docs = await (tx || db)(TableName.SecretV2)
|
const docs = await (tx || db)(TableName.SecretV2)
|
||||||
@@ -73,8 +128,35 @@ export const secretV2BridgeDALFactory = (db: TDbClient) => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const find = async (filter: TFindFilter<TSecretsV2>, { offset, limit, sort, tx }: TFindOpt<TSecretsV2> = {}) => {
|
const find = async (
|
||||||
|
filter: TFindFilter<TSecretsV2>,
|
||||||
|
opts: TFindOpt<TSecretsV2> & { useCache?: { projectId: string } } = {}
|
||||||
|
) => {
|
||||||
|
const { offset, limit, sort, tx, useCache } = opts;
|
||||||
try {
|
try {
|
||||||
|
let secretDalVersion = 0;
|
||||||
|
if (useCache) {
|
||||||
|
const cachedSecretDalVersion = await keyStore.getItem(
|
||||||
|
SecretDalCacheKeys.getSecretDalVersion(useCache.projectId)
|
||||||
|
);
|
||||||
|
secretDalVersion = Number(cachedSecretDalVersion || 0);
|
||||||
|
const cacheKey = SecretDalCacheKeys.find(useCache.projectId, secretDalVersion, filter, opts);
|
||||||
|
const cachedSecrets = await keyStore.getItem(cacheKey);
|
||||||
|
if (cachedSecrets) {
|
||||||
|
await keyStore.setExpiry(cacheKey, SECRET_DAL_TTL);
|
||||||
|
|
||||||
|
const unsanitizedSecrets = JSON.parse(cachedSecrets) as BufferKeysToString<(typeof data)[number]>[];
|
||||||
|
const sanitizedSecrets = unsanitizedSecrets.map((el) => {
|
||||||
|
const encryptedValue = el.encryptedValue ? Buffer.from(el.encryptedValue, "base64") : null;
|
||||||
|
const encryptedComment = el.encryptedComment ? Buffer.from(el.encryptedComment, "base64") : null;
|
||||||
|
const createdAt = new Date(el.createdAt);
|
||||||
|
const updatedAt = new Date(el.updatedAt);
|
||||||
|
return { ...el, encryptedComment, encryptedValue, createdAt, updatedAt };
|
||||||
|
});
|
||||||
|
return sanitizedSecrets;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const query = (tx || db)(TableName.SecretV2)
|
const query = (tx || db)(TableName.SecretV2)
|
||||||
// eslint-disable-next-line @typescript-eslint/no-misused-promises
|
// eslint-disable-next-line @typescript-eslint/no-misused-promises
|
||||||
.where(buildFindFilter(filter))
|
.where(buildFindFilter(filter))
|
||||||
@@ -142,6 +224,23 @@ export const secretV2BridgeDALFactory = (db: TDbClient) => {
|
|||||||
}
|
}
|
||||||
]
|
]
|
||||||
});
|
});
|
||||||
|
|
||||||
|
if (useCache) {
|
||||||
|
const cachedSecrets = data.map((el) => {
|
||||||
|
const encryptedValue = el.encryptedValue ? el.encryptedValue.toString("base64") : null;
|
||||||
|
const encryptedComment = el.encryptedComment ? el.encryptedComment.toString("base64") : null;
|
||||||
|
return { ...el, encryptedValue, encryptedComment };
|
||||||
|
});
|
||||||
|
const cache = JSON.stringify(cachedSecrets);
|
||||||
|
if (Buffer.byteLength(cache, "utf8") < MAX_SECRET_CACHE_BYTES) {
|
||||||
|
await keyStore.setItemWithExpiry(
|
||||||
|
SecretDalCacheKeys.find(useCache.projectId, secretDalVersion, filter, opts),
|
||||||
|
SECRET_DAL_TTL,
|
||||||
|
cache
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return data;
|
return data;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
throw new DatabaseError({ error, name: `${TableName.SecretV2}: Find` });
|
throw new DatabaseError({ error, name: `${TableName.SecretV2}: Find` });
|
||||||
@@ -246,14 +345,43 @@ export const secretV2BridgeDALFactory = (db: TDbClient) => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const findByFolderId = async (folderId: string, userId?: string, tx?: Knex) => {
|
const findByFolderId = async (dto: {
|
||||||
|
folderId: string;
|
||||||
|
userId?: string;
|
||||||
|
tx?: Knex;
|
||||||
|
projectId: string;
|
||||||
|
useCache?: boolean;
|
||||||
|
}) => {
|
||||||
try {
|
try {
|
||||||
// check if not uui then userId id is null (corner case because service token's ID is not UUI in effort to keep backwards compatibility from mongo)
|
const { folderId, tx, projectId } = dto;
|
||||||
|
let { userId } = dto;
|
||||||
|
// check if not uui then userId id is null (corner case because service token's ID is not UUI in effort to keep backwards compatibility from mongo
|
||||||
if (userId && !uuidValidate(userId)) {
|
if (userId && !uuidValidate(userId)) {
|
||||||
// eslint-disable-next-line
|
// eslint-disable-next-line
|
||||||
userId = undefined;
|
userId = undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const cachedSecretDalVersion = await keyStore.getItem(SecretDalCacheKeys.getSecretDalVersion(projectId));
|
||||||
|
const secretDalVersion = Number(cachedSecretDalVersion || 0);
|
||||||
|
|
||||||
|
if (dto.useCache) {
|
||||||
|
const cacheKey = SecretDalCacheKeys.findByFolderId(projectId, secretDalVersion, dto);
|
||||||
|
const cachedSecrets = await keyStore.getItem(cacheKey);
|
||||||
|
if (cachedSecrets) {
|
||||||
|
await keyStore.setExpiry(cacheKey, SECRET_DAL_TTL);
|
||||||
|
|
||||||
|
const unsanitizedSecrets = JSON.parse(cachedSecrets) as BufferKeysToString<(typeof data)[number]>[];
|
||||||
|
const sanitizedSecrets = unsanitizedSecrets.map((el) => {
|
||||||
|
const encryptedValue = el.encryptedValue ? Buffer.from(el.encryptedValue, "base64") : null;
|
||||||
|
const encryptedComment = el.encryptedComment ? Buffer.from(el.encryptedComment, "base64") : null;
|
||||||
|
const createdAt = new Date(el.createdAt);
|
||||||
|
const updatedAt = new Date(el.updatedAt);
|
||||||
|
return { ...el, encryptedComment, encryptedValue, createdAt, updatedAt };
|
||||||
|
});
|
||||||
|
return sanitizedSecrets;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const secs = await (tx || db.replicaNode())(TableName.SecretV2)
|
const secs = await (tx || db.replicaNode())(TableName.SecretV2)
|
||||||
.where({ folderId })
|
.where({ folderId })
|
||||||
.where((bd) => {
|
.where((bd) => {
|
||||||
@@ -309,6 +437,22 @@ export const secretV2BridgeDALFactory = (db: TDbClient) => {
|
|||||||
}
|
}
|
||||||
]
|
]
|
||||||
});
|
});
|
||||||
|
if (dto.useCache) {
|
||||||
|
const newCachedSecrets = data.map((el) => {
|
||||||
|
const encryptedValue = el.encryptedValue ? el.encryptedValue.toString("base64") : null;
|
||||||
|
const encryptedComment = el.encryptedComment ? el.encryptedComment.toString("base64") : null;
|
||||||
|
return { ...el, encryptedValue, encryptedComment };
|
||||||
|
});
|
||||||
|
const cache = JSON.stringify(newCachedSecrets);
|
||||||
|
|
||||||
|
if (Buffer.byteLength(cache, "utf8") < MAX_SECRET_CACHE_BYTES) {
|
||||||
|
await keyStore.setItemWithExpiry(
|
||||||
|
SecretDalCacheKeys.findByFolderId(projectId, secretDalVersion, dto),
|
||||||
|
SECRET_DAL_TTL,
|
||||||
|
cache
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
return data;
|
return data;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
throw new DatabaseError({ error, name: "get all secret" });
|
throw new DatabaseError({ error, name: "get all secret" });
|
||||||
@@ -394,12 +538,16 @@ export const secretV2BridgeDALFactory = (db: TDbClient) => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const findByFolderIds = async (
|
const findByFolderIds = async (dto: {
|
||||||
folderIds: string[],
|
folderIds: string[];
|
||||||
userId?: string,
|
userId?: string;
|
||||||
tx?: Knex,
|
tx?: Knex;
|
||||||
filters?: TFindSecretsByFolderIdsFilter
|
projectId: string;
|
||||||
) => {
|
filters?: TFindSecretsByFolderIdsFilter;
|
||||||
|
useCache?: boolean;
|
||||||
|
}) => {
|
||||||
|
const { folderIds, tx, filters, useCache, projectId } = dto;
|
||||||
|
let { userId } = dto;
|
||||||
try {
|
try {
|
||||||
// check if not uui then userId id is null (corner case because service token's ID is not UUI in effort to keep backwards compatibility from mongo)
|
// check if not uui then userId id is null (corner case because service token's ID is not UUI in effort to keep backwards compatibility from mongo)
|
||||||
if (userId && !uuidValidate(userId)) {
|
if (userId && !uuidValidate(userId)) {
|
||||||
@@ -407,6 +555,26 @@ export const secretV2BridgeDALFactory = (db: TDbClient) => {
|
|||||||
userId = undefined;
|
userId = undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const cachedSecretDalVersion = await keyStore.getItem(SecretDalCacheKeys.getSecretDalVersion(projectId));
|
||||||
|
const secretDalVersion = Number(cachedSecretDalVersion || 0);
|
||||||
|
if (useCache) {
|
||||||
|
const cacheKey = SecretDalCacheKeys.findByFolderIds(projectId, secretDalVersion, dto);
|
||||||
|
const cachedSecrets = await keyStore.getItem(cacheKey);
|
||||||
|
if (cachedSecrets) {
|
||||||
|
await keyStore.setExpiry(cacheKey, SECRET_DAL_TTL);
|
||||||
|
|
||||||
|
const unsanitizedSecrets = JSON.parse(cachedSecrets) as BufferKeysToString<(typeof data)[number]>[];
|
||||||
|
const sanitizedSecrets = unsanitizedSecrets.map((el) => {
|
||||||
|
const encryptedValue = el.encryptedValue ? Buffer.from(el.encryptedValue, "base64") : null;
|
||||||
|
const encryptedComment = el.encryptedComment ? Buffer.from(el.encryptedComment, "base64") : null;
|
||||||
|
const createdAt = new Date(el.createdAt);
|
||||||
|
const updatedAt = new Date(el.updatedAt);
|
||||||
|
return { ...el, encryptedComment, encryptedValue, createdAt, updatedAt };
|
||||||
|
});
|
||||||
|
return sanitizedSecrets;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const query = (tx || db.replicaNode())(TableName.SecretV2)
|
const query = (tx || db.replicaNode())(TableName.SecretV2)
|
||||||
.whereIn(`${TableName.SecretV2}.folderId`, folderIds)
|
.whereIn(`${TableName.SecretV2}.folderId`, folderIds)
|
||||||
.where((bd) => {
|
.where((bd) => {
|
||||||
@@ -532,6 +700,22 @@ export const secretV2BridgeDALFactory = (db: TDbClient) => {
|
|||||||
}
|
}
|
||||||
]
|
]
|
||||||
});
|
});
|
||||||
|
if (useCache) {
|
||||||
|
const cachedSecrets = data.map((el) => {
|
||||||
|
const encryptedValue = el.encryptedValue ? el.encryptedValue.toString("base64") : null;
|
||||||
|
const encryptedComment = el.encryptedComment ? el.encryptedComment.toString("base64") : null;
|
||||||
|
return { ...el, encryptedValue, encryptedComment };
|
||||||
|
});
|
||||||
|
const cache = JSON.stringify(cachedSecrets);
|
||||||
|
|
||||||
|
if (Buffer.byteLength(cache, "utf8") < MAX_SECRET_CACHE_BYTES) {
|
||||||
|
await keyStore.setItemWithExpiry(
|
||||||
|
SecretDalCacheKeys.findByFolderIds(projectId, secretDalVersion, dto),
|
||||||
|
SECRET_DAL_TTL,
|
||||||
|
cache
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return data;
|
return data;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -724,6 +908,7 @@ export const secretV2BridgeDALFactory = (db: TDbClient) => {
|
|||||||
findAllProjectSecretValues,
|
findAllProjectSecretValues,
|
||||||
countByFolderIds,
|
countByFolderIds,
|
||||||
findOne,
|
findOne,
|
||||||
find
|
find,
|
||||||
|
invalidateSecretCacheByProjectId
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -501,7 +501,7 @@ export const expandSecretReferencesFactory = ({
|
|||||||
|
|
||||||
const folder = await folderDAL.findBySecretPath(projectId, environment, secretPath);
|
const folder = await folderDAL.findBySecretPath(projectId, environment, secretPath);
|
||||||
if (!folder) return { value: "", tags: [] };
|
if (!folder) return { value: "", tags: [] };
|
||||||
const secrets = await secretDAL.findByFolderId(folder.id);
|
const secrets = await secretDAL.findByFolderId({ folderId: folder.id, projectId, useCache: true });
|
||||||
|
|
||||||
const decryptedSecret = secrets.reduce<Record<string, { value: string; tags: string[] }>>((prev, secret) => {
|
const decryptedSecret = secrets.reduce<Record<string, { value: string; tags: string[] }>>((prev, secret) => {
|
||||||
// eslint-disable-next-line no-param-reassign
|
// eslint-disable-next-line no-param-reassign
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user