mirror of
https://github.com/Infisical/infisical.git
synced 2025-07-15 09:42:14 +00:00
Compare commits
94 Commits
native-int
...
infisical/
Author | SHA1 | Date | |
---|---|---|---|
ae1ee25687 | |||
4650ba9fdd | |||
e7742afcd3 | |||
927eb0407d | |||
17ddb79def | |||
5ef5a5a107 | |||
9ae0880f50 | |||
3814c65f38 | |||
3fa98e2a8d | |||
c6b21491db | |||
b2fae5c439 | |||
f16e96759f | |||
5eb9a1a667 | |||
03ad6f822a | |||
98447e9402 | |||
0f7e8585dc | |||
8568d1f6fe | |||
27198869d8 | |||
f27050a1c3 | |||
d33b06dd8a | |||
9475c1671e | |||
0f710b1ccc | |||
71c55d5a53 | |||
32bca651df | |||
82533f49ca | |||
b08b53b77d | |||
862ed4f4e7 | |||
7b9254d09a | |||
c6305045e3 | |||
24bf9f7a2a | |||
8d4fa0bdb9 | |||
2642f7501d | |||
68ba807b43 | |||
499ff3635b | |||
78fc8a693d | |||
78687984b7 | |||
31a4bcafbe | |||
ac8b3aca60 | |||
4ea0cc62e3 | |||
bdab16f64b | |||
9d0020fa4e | |||
3c07204532 | |||
c0926bec69 | |||
b9d74e0aed | |||
f3078040fc | |||
f2fead7a51 | |||
3483ed85ff | |||
3c58bf890d | |||
dc219b8e9f | |||
85627eb825 | |||
f1e30fd06b | |||
fcc6f812d5 | |||
7c38932878 | |||
e339b81bf1 | |||
b9bfe19b64 | |||
966ca1a3c6 | |||
65f78c556f | |||
581e4b35f9 | |||
4a9e24884d | |||
5bc8e4729f | |||
f33a777fae | |||
8a870131e9 | |||
041fac7f42 | |||
d97057b43b | |||
5ce738bba0 | |||
19b0cd9735 | |||
1ec87fae75 | |||
aec131543f | |||
aeaa5babab | |||
07898414a3 | |||
f15b30ff85 | |||
894633143d | |||
8ee2b54182 | |||
ac0f4aa8bd | |||
b121ec891f | |||
ab566bcbe4 | |||
8fa8117fa1 | |||
041d585f19 | |||
939b77b050 | |||
224b167000 | |||
83206aad93 | |||
e1a11c37e3 | |||
cd83efb060 | |||
53b5497271 | |||
15130a433c | |||
a0bf03b2ae | |||
c7416c825c | |||
fe172e39bf | |||
fda77fe464 | |||
c4c065ea9e | |||
c6ca668db9 | |||
4d8598a019 | |||
a9da2d6241 | |||
4420985669 |
@ -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:65
|
||||
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
|
||||
|
@ -50,7 +50,7 @@ We're on a mission to make security tooling more accessible to everyone, not jus
|
||||
- **[Dashboard](https://infisical.com/docs/documentation/platform/project)**: Manage secrets across projects and environments (e.g. development, production, etc.) through a user-friendly interface.
|
||||
- **[Native Integrations](https://infisical.com/docs/integrations/overview)**: Sync secrets to platforms like [GitHub](https://infisical.com/docs/integrations/cicd/githubactions), [Vercel](https://infisical.com/docs/integrations/cloud/vercel), [AWS](https://infisical.com/docs/integrations/cloud/aws-secret-manager), and use tools like [Terraform](https://infisical.com/docs/integrations/frameworks/terraform), [Ansible](https://infisical.com/docs/integrations/platforms/ansible), and more.
|
||||
- **[Secret versioning](https://infisical.com/docs/documentation/platform/secret-versioning)** and **[Point-in-Time Recovery](https://infisical.com/docs/documentation/platform/pit-recovery)**: Keep track of every secret and project state; roll back when needed.
|
||||
- **[Secret Rotation](https://infisical.com/docs/documentation/platform/secret-rotation/overview)**: Rotate secrets at regular intervals for services like [PostgreSQL](https://infisical.com/docs/documentation/platform/secret-rotation/postgres), [MySQL](https://infisical.com/docs/documentation/platform/secret-rotation/mysql), [AWS IAM](https://infisical.com/docs/documentation/platform/secret-rotation/aws-iam), and more.
|
||||
- **[Secret Rotation](https://infisical.com/docs/documentation/platform/secret-rotation/overview)**: Rotate secrets at regular intervals for services like [PostgreSQL](https://infisical.com/docs/documentation/platform/secret-rotation/postgres-credentials), [MySQL](https://infisical.com/docs/documentation/platform/secret-rotation/mysql), [AWS IAM](https://infisical.com/docs/documentation/platform/secret-rotation/aws-iam), and more.
|
||||
- **[Dynamic Secrets](https://infisical.com/docs/documentation/platform/dynamic-secrets/overview)**: Generate ephemeral secrets on-demand for services like [PostgreSQL](https://infisical.com/docs/documentation/platform/dynamic-secrets/postgresql), [MySQL](https://infisical.com/docs/documentation/platform/dynamic-secrets/mysql), [RabbitMQ](https://infisical.com/docs/documentation/platform/dynamic-secrets/rabbit-mq), and more.
|
||||
- **[Secret Scanning and Leak Prevention](https://infisical.com/docs/cli/scanning-overview)**: Prevent secrets from leaking to git.
|
||||
- **[Infisical Kubernetes Operator](https://infisical.com/docs/documentation/getting-started/kubernetes)**: Deliver secrets to your Kubernetes workloads and automatically reload deployments.
|
||||
|
@ -8,7 +8,8 @@ RUN apt-get update && apt-get install -y \
|
||||
python3 \
|
||||
make \
|
||||
g++ \
|
||||
openssh-client
|
||||
openssh-client \
|
||||
openssl
|
||||
|
||||
# Install dependencies for TDS driver (required for SAP ASE dynamic secrets)
|
||||
RUN apt-get install -y \
|
||||
|
@ -19,6 +19,7 @@ RUN apt-get update && apt-get install -y \
|
||||
make \
|
||||
g++ \
|
||||
openssh-client \
|
||||
openssl \
|
||||
curl \
|
||||
pkg-config
|
||||
|
||||
|
22
backend/package-lock.json
generated
22
backend/package-lock.json
generated
@ -132,7 +132,7 @@
|
||||
"@types/jsrp": "^0.2.6",
|
||||
"@types/libsodium-wrappers": "^0.7.13",
|
||||
"@types/lodash.isequal": "^4.5.8",
|
||||
"@types/node": "^20.9.5",
|
||||
"@types/node": "^20.17.30",
|
||||
"@types/nodemailer": "^6.4.14",
|
||||
"@types/passport-github": "^1.1.12",
|
||||
"@types/passport-google-oauth20": "^2.0.14",
|
||||
@ -9753,11 +9753,12 @@
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/node": {
|
||||
"version": "20.9.5",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-20.9.5.tgz",
|
||||
"integrity": "sha512-Uq2xbNq0chGg+/WQEU0LJTSs/1nKxz6u1iemLcGomkSnKokbW1fbLqc3HOqCf2JP7KjlL4QkS7oZZTrOQHQYgQ==",
|
||||
"version": "20.17.30",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-20.17.30.tgz",
|
||||
"integrity": "sha512-7zf4YyHA+jvBNfVrk2Gtvs6x7E8V+YDW05bNfG2XkWDJfYRXrTiP/DsB2zSYTaHX0bGIujTBQdMVAhb+j7mwpg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"undici-types": "~5.26.4"
|
||||
"undici-types": "~6.19.2"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/node-fetch": {
|
||||
@ -20081,11 +20082,6 @@
|
||||
"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": {
|
||||
"version": "0.2.10",
|
||||
"resolved": "https://registry.npmjs.org/scim2-parse-filter/-/scim2-parse-filter-0.2.10.tgz",
|
||||
@ -22442,9 +22438,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/undici-types": {
|
||||
"version": "5.26.5",
|
||||
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz",
|
||||
"integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA=="
|
||||
"version": "6.19.8",
|
||||
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.19.8.tgz",
|
||||
"integrity": "sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw=="
|
||||
},
|
||||
"node_modules/unicode-canonical-property-names-ecmascript": {
|
||||
"version": "2.0.0",
|
||||
|
@ -89,7 +89,7 @@
|
||||
"@types/jsrp": "^0.2.6",
|
||||
"@types/libsodium-wrappers": "^0.7.13",
|
||||
"@types/lodash.isequal": "^4.5.8",
|
||||
"@types/node": "^20.9.5",
|
||||
"@types/node": "^20.17.30",
|
||||
"@types/nodemailer": "^6.4.14",
|
||||
"@types/passport-github": "^1.1.12",
|
||||
"@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");
|
||||
});
|
||||
}
|
||||
}
|
@ -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)
|
||||
});
|
||||
}
|
@ -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)
|
||||
});
|
||||
}
|
@ -0,0 +1,21 @@
|
||||
import { Knex } from "knex";
|
||||
|
||||
import { OIDCJWTSignatureAlgorithm } from "@app/ee/services/oidc/oidc-config-types";
|
||||
|
||||
import { TableName } from "../schemas";
|
||||
|
||||
export async function up(knex: Knex): Promise<void> {
|
||||
if (!(await knex.schema.hasColumn(TableName.OidcConfig, "jwtSignatureAlgorithm"))) {
|
||||
await knex.schema.alterTable(TableName.OidcConfig, (t) => {
|
||||
t.string("jwtSignatureAlgorithm").defaultTo(OIDCJWTSignatureAlgorithm.RS256).notNullable();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export async function down(knex: Knex): Promise<void> {
|
||||
if (await knex.schema.hasColumn(TableName.OidcConfig, "jwtSignatureAlgorithm")) {
|
||||
await knex.schema.alterTable(TableName.OidcConfig, (t) => {
|
||||
t.dropColumn("jwtSignatureAlgorithm");
|
||||
});
|
||||
}
|
||||
}
|
@ -16,7 +16,8 @@ export const KmsKeysSchema = z.object({
|
||||
name: z.string(),
|
||||
createdAt: 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>;
|
||||
|
@ -30,9 +30,10 @@ export const OidcConfigsSchema = z.object({
|
||||
updatedAt: z.date(),
|
||||
orgId: z.string().uuid(),
|
||||
lastUsed: z.date().nullable().optional(),
|
||||
manageGroupMemberships: z.boolean().default(false),
|
||||
encryptedOidcClientId: zodBuffer,
|
||||
encryptedOidcClientSecret: zodBuffer
|
||||
encryptedOidcClientSecret: zodBuffer,
|
||||
manageGroupMemberships: z.boolean().default(false),
|
||||
jwtSignatureAlgorithm: z.string().default("RS256")
|
||||
});
|
||||
|
||||
export type TOidcConfigs = z.infer<typeof OidcConfigsSchema>;
|
||||
|
@ -16,7 +16,8 @@ export const ResourceMetadataSchema = z.object({
|
||||
identityId: z.string().uuid().nullable().optional(),
|
||||
secretId: z.string().uuid().nullable().optional(),
|
||||
createdAt: z.date(),
|
||||
updatedAt: z.date()
|
||||
updatedAt: z.date(),
|
||||
dynamicSecretId: z.string().uuid().nullable().optional()
|
||||
});
|
||||
|
||||
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 { SanitizedDynamicSecretSchema } from "@app/server/routes/sanitizedSchemas";
|
||||
import { AuthMode } from "@app/services/auth/auth-type";
|
||||
import { ResourceMetadataSchema } from "@app/services/resource-metadata/resource-metadata-schema";
|
||||
|
||||
export const registerDynamicSecretRouter = async (server: FastifyZodProvider) => {
|
||||
server.route({
|
||||
@ -48,7 +49,8 @@ export const registerDynamicSecretRouter = async (server: FastifyZodProvider) =>
|
||||
.nullable(),
|
||||
path: z.string().describe(DYNAMIC_SECRETS.CREATE.path).trim().default("/").transform(removeTrailingSlash),
|
||||
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: {
|
||||
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" });
|
||||
})
|
||||
.nullable(),
|
||||
newName: z.string().describe(DYNAMIC_SECRETS.UPDATE.newName).optional()
|
||||
newName: z.string().describe(DYNAMIC_SECRETS.UPDATE.newName).optional(),
|
||||
metadata: ResourceMetadataSchema.optional()
|
||||
})
|
||||
}),
|
||||
response: {
|
||||
@ -238,6 +241,7 @@ export const registerDynamicSecretRouter = async (server: FastifyZodProvider) =>
|
||||
name: req.params.name,
|
||||
...req.query
|
||||
});
|
||||
|
||||
return { dynamicSecret: dynamicSecretCfg };
|
||||
}
|
||||
});
|
||||
|
@ -2,7 +2,7 @@ import z from "zod";
|
||||
|
||||
import { KmsKeysSchema } from "@app/db/schemas";
|
||||
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 { writeLimit } from "@app/server/config/rateLimiter";
|
||||
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
|
||||
@ -74,7 +74,7 @@ export const registerKmipSpecRouter = async (server: FastifyZodProvider) => {
|
||||
schema: {
|
||||
description: "KMIP endpoint for creating managed objects",
|
||||
body: z.object({
|
||||
algorithm: z.nativeEnum(SymmetricEncryption)
|
||||
algorithm: z.nativeEnum(SymmetricKeyAlgorithm)
|
||||
}),
|
||||
response: {
|
||||
200: KmsKeysSchema
|
||||
@ -433,7 +433,7 @@ export const registerKmipSpecRouter = async (server: FastifyZodProvider) => {
|
||||
body: z.object({
|
||||
key: z.string(),
|
||||
name: z.string(),
|
||||
algorithm: z.nativeEnum(SymmetricEncryption)
|
||||
algorithm: z.nativeEnum(SymmetricKeyAlgorithm)
|
||||
}),
|
||||
response: {
|
||||
200: z.object({
|
||||
|
@ -12,7 +12,7 @@ import RedisStore from "connect-redis";
|
||||
import { z } from "zod";
|
||||
|
||||
import { OidcConfigsSchema } from "@app/db/schemas";
|
||||
import { OIDCConfigurationType } from "@app/ee/services/oidc/oidc-config-types";
|
||||
import { OIDCConfigurationType, OIDCJWTSignatureAlgorithm } from "@app/ee/services/oidc/oidc-config-types";
|
||||
import { getConfig } from "@app/lib/config/env";
|
||||
import { authRateLimit, readLimit, writeLimit } from "@app/server/config/rateLimiter";
|
||||
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
|
||||
@ -30,7 +30,8 @@ const SanitizedOidcConfigSchema = OidcConfigsSchema.pick({
|
||||
orgId: true,
|
||||
isActive: true,
|
||||
allowedEmailDomains: true,
|
||||
manageGroupMemberships: true
|
||||
manageGroupMemberships: true,
|
||||
jwtSignatureAlgorithm: true
|
||||
});
|
||||
|
||||
export const registerOidcRouter = async (server: FastifyZodProvider) => {
|
||||
@ -136,11 +137,12 @@ export const registerOidcRouter = async (server: FastifyZodProvider) => {
|
||||
url: "/login/error",
|
||||
method: "GET",
|
||||
handler: async (req, res) => {
|
||||
const failureMessage = req.session.get<any>("messages");
|
||||
await req.session.destroy();
|
||||
|
||||
return res.status(500).send({
|
||||
error: "Authentication error",
|
||||
details: req.query
|
||||
details: failureMessage ?? req.query
|
||||
});
|
||||
}
|
||||
});
|
||||
@ -169,7 +171,8 @@ export const registerOidcRouter = async (server: FastifyZodProvider) => {
|
||||
isActive: true,
|
||||
orgId: true,
|
||||
allowedEmailDomains: true,
|
||||
manageGroupMemberships: true
|
||||
manageGroupMemberships: true,
|
||||
jwtSignatureAlgorithm: true
|
||||
}).extend({
|
||||
clientId: z.string(),
|
||||
clientSecret: z.string()
|
||||
@ -224,7 +227,8 @@ export const registerOidcRouter = async (server: FastifyZodProvider) => {
|
||||
clientId: z.string().trim(),
|
||||
clientSecret: z.string().trim(),
|
||||
isActive: z.boolean(),
|
||||
manageGroupMemberships: z.boolean().optional()
|
||||
manageGroupMemberships: z.boolean().optional(),
|
||||
jwtSignatureAlgorithm: z.nativeEnum(OIDCJWTSignatureAlgorithm).optional()
|
||||
})
|
||||
.partial()
|
||||
.merge(z.object({ orgSlug: z.string() })),
|
||||
@ -291,7 +295,11 @@ export const registerOidcRouter = async (server: FastifyZodProvider) => {
|
||||
clientSecret: z.string().trim(),
|
||||
isActive: z.boolean(),
|
||||
orgSlug: z.string().trim(),
|
||||
manageGroupMemberships: z.boolean().optional().default(false)
|
||||
manageGroupMemberships: z.boolean().optional().default(false),
|
||||
jwtSignatureAlgorithm: z
|
||||
.nativeEnum(OIDCJWTSignatureAlgorithm)
|
||||
.optional()
|
||||
.default(OIDCJWTSignatureAlgorithm.RS256)
|
||||
})
|
||||
.superRefine((data, ctx) => {
|
||||
if (data.configurationType === OIDCConfigurationType.CUSTOM) {
|
||||
|
@ -23,7 +23,8 @@ export const registerSecretRotationProviderRouter = async (server: FastifyZodPro
|
||||
title: z.string(),
|
||||
image: z.string().optional(),
|
||||
description: z.string().optional(),
|
||||
template: z.any()
|
||||
template: z.any(),
|
||||
isDeprecated: z.boolean().optional()
|
||||
})
|
||||
.array()
|
||||
})
|
||||
|
@ -1,7 +1,6 @@
|
||||
import { z } from "zod";
|
||||
|
||||
import { SecretRotationOutputsSchema, SecretRotationsSchema } from "@app/db/schemas";
|
||||
import { BadRequestError } from "@app/lib/errors";
|
||||
import { removeTrailingSlash } from "@app/lib/fn";
|
||||
import { readLimit, writeLimit } from "@app/server/config/rateLimiter";
|
||||
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
|
||||
@ -41,10 +40,16 @@ export const registerSecretRotationRouter = async (server: FastifyZodProvider) =
|
||||
}
|
||||
},
|
||||
onRequest: verifyAuth([AuthMode.JWT]),
|
||||
handler: async () => {
|
||||
throw new BadRequestError({
|
||||
message: `This version of Secret Rotations has been deprecated. Please see docs for new version.`
|
||||
handler: async (req) => {
|
||||
const secretRotation = await server.services.secretRotation.createRotation({
|
||||
actor: req.permission.type,
|
||||
actorAuthMethod: req.permission.authMethod,
|
||||
actorId: req.permission.id,
|
||||
actorOrgId: req.permission.orgId,
|
||||
...req.body,
|
||||
projectId: req.body.workspaceId
|
||||
});
|
||||
return { secretRotation };
|
||||
}
|
||||
});
|
||||
|
||||
|
@ -0,0 +1,19 @@
|
||||
import {
|
||||
Auth0ClientSecretRotationGeneratedCredentialsSchema,
|
||||
Auth0ClientSecretRotationSchema,
|
||||
CreateAuth0ClientSecretRotationSchema,
|
||||
UpdateAuth0ClientSecretRotationSchema
|
||||
} from "@app/ee/services/secret-rotation-v2/auth0-client-secret";
|
||||
import { SecretRotation } from "@app/ee/services/secret-rotation-v2/secret-rotation-v2-enums";
|
||||
|
||||
import { registerSecretRotationEndpoints } from "./secret-rotation-v2-endpoints";
|
||||
|
||||
export const registerAuth0ClientSecretRotationRouter = async (server: FastifyZodProvider) =>
|
||||
registerSecretRotationEndpoints({
|
||||
type: SecretRotation.Auth0ClientSecret,
|
||||
server,
|
||||
responseSchema: Auth0ClientSecretRotationSchema,
|
||||
createSchema: CreateAuth0ClientSecretRotationSchema,
|
||||
updateSchema: UpdateAuth0ClientSecretRotationSchema,
|
||||
generatedCredentialsSchema: Auth0ClientSecretRotationGeneratedCredentialsSchema
|
||||
});
|
@ -1,5 +1,6 @@
|
||||
import { SecretRotation } from "@app/ee/services/secret-rotation-v2/secret-rotation-v2-enums";
|
||||
|
||||
import { registerAuth0ClientSecretRotationRouter } from "./auth0-client-secret-rotation-router";
|
||||
import { registerMsSqlCredentialsRotationRouter } from "./mssql-credentials-rotation-router";
|
||||
import { registerPostgresCredentialsRotationRouter } from "./postgres-credentials-rotation-router";
|
||||
|
||||
@ -10,5 +11,6 @@ export const SECRET_ROTATION_REGISTER_ROUTER_MAP: Record<
|
||||
(server: FastifyZodProvider) => Promise<void>
|
||||
> = {
|
||||
[SecretRotation.PostgresCredentials]: registerPostgresCredentialsRotationRouter,
|
||||
[SecretRotation.MsSqlCredentials]: registerMsSqlCredentialsRotationRouter
|
||||
[SecretRotation.MsSqlCredentials]: registerMsSqlCredentialsRotationRouter,
|
||||
[SecretRotation.Auth0ClientSecret]: registerAuth0ClientSecretRotationRouter
|
||||
};
|
||||
|
@ -1,6 +1,7 @@
|
||||
import { z } from "zod";
|
||||
|
||||
import { EventType } from "@app/ee/services/audit-log/audit-log-types";
|
||||
import { Auth0ClientSecretRotationListItemSchema } from "@app/ee/services/secret-rotation-v2/auth0-client-secret";
|
||||
import { MsSqlCredentialsRotationListItemSchema } from "@app/ee/services/secret-rotation-v2/mssql-credentials";
|
||||
import { PostgresCredentialsRotationListItemSchema } from "@app/ee/services/secret-rotation-v2/postgres-credentials";
|
||||
import { SecretRotationV2Schema } from "@app/ee/services/secret-rotation-v2/secret-rotation-v2-union-schema";
|
||||
@ -11,7 +12,8 @@ import { AuthMode } from "@app/services/auth/auth-type";
|
||||
|
||||
const SecretRotationV2OptionsSchema = z.discriminatedUnion("type", [
|
||||
PostgresCredentialsRotationListItemSchema,
|
||||
MsSqlCredentialsRotationListItemSchema
|
||||
MsSqlCredentialsRotationListItemSchema,
|
||||
Auth0ClientSecretRotationListItemSchema
|
||||
]);
|
||||
|
||||
export const registerSecretRotationV2Router = async (server: FastifyZodProvider) => {
|
||||
|
@ -12,7 +12,8 @@ import {
|
||||
import { SshCaStatus, SshCertType } from "@app/ee/services/ssh/ssh-certificate-authority-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 { 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 { AppConnection } from "@app/services/app-connection/app-connection-enums";
|
||||
import { TCreateAppConnectionDTO, TUpdateAppConnectionDTO } from "@app/services/app-connection/app-connection-types";
|
||||
@ -255,6 +256,11 @@ export enum EventType {
|
||||
GET_CMEK = "get-cmek",
|
||||
CMEK_ENCRYPT = "cmek-encrypt",
|
||||
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",
|
||||
GET_EXTERNAL_GROUP_ORG_ROLE_MAPPINGS = "get-external-group-org-role-mapping",
|
||||
GET_PROJECT_TEMPLATES = "get-project-templates",
|
||||
@ -1997,7 +2003,7 @@ interface CreateCmekEvent {
|
||||
keyId: string;
|
||||
name: 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 {
|
||||
type: EventType.GET_EXTERNAL_GROUP_ORG_ROLE_MAPPINGS;
|
||||
metadata?: Record<string, never>; // not needed, based off orgId
|
||||
@ -2639,6 +2678,10 @@ export type Event =
|
||||
| GetCmeksEvent
|
||||
| CmekEncryptEvent
|
||||
| CmekDecryptEvent
|
||||
| CmekSignEvent
|
||||
| CmekVerifyEvent
|
||||
| CmekListSigningAlgorithmsEvent
|
||||
| CmekGetPublicKeyEvent
|
||||
| GetExternalGroupOrgRoleMappingsEvent
|
||||
| UpdateExternalGroupOrgRoleMappingsEvent
|
||||
| GetProjectTemplatesEvent
|
||||
|
@ -78,10 +78,6 @@ export const dynamicSecretLeaseServiceFactory = ({
|
||||
actorOrgId,
|
||||
actionProjectType: ActionProjectType.SecretManager
|
||||
});
|
||||
ForbiddenError.from(permission).throwUnlessCan(
|
||||
ProjectPermissionDynamicSecretActions.Lease,
|
||||
subject(ProjectPermissionSub.DynamicSecrets, { environment: environmentSlug, secretPath: path })
|
||||
);
|
||||
|
||||
const plan = await licenseService.getPlan(actorOrgId);
|
||||
if (!plan?.dynamicSecret) {
|
||||
@ -102,6 +98,15 @@ export const dynamicSecretLeaseServiceFactory = ({
|
||||
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);
|
||||
if (totalLeasesTaken >= 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,
|
||||
actionProjectType: ActionProjectType.SecretManager
|
||||
});
|
||||
ForbiddenError.from(permission).throwUnlessCan(
|
||||
ProjectPermissionDynamicSecretActions.Lease,
|
||||
subject(ProjectPermissionSub.DynamicSecrets, { environment: environmentSlug, secretPath: path })
|
||||
);
|
||||
|
||||
const { decryptor: secretManagerDecryptor } = await kmsService.createCipherPairWithDataKey({
|
||||
type: KmsDataKey.SecretManager,
|
||||
@ -187,7 +188,25 @@ export const dynamicSecretLeaseServiceFactory = ({
|
||||
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 decryptedStoredInput = JSON.parse(
|
||||
secretManagerDecryptor({ cipherTextBlob: Buffer.from(dynamicSecretCfg.encryptedInput) }).toString()
|
||||
@ -239,10 +258,6 @@ export const dynamicSecretLeaseServiceFactory = ({
|
||||
actorOrgId,
|
||||
actionProjectType: ActionProjectType.SecretManager
|
||||
});
|
||||
ForbiddenError.from(permission).throwUnlessCan(
|
||||
ProjectPermissionDynamicSecretActions.Lease,
|
||||
subject(ProjectPermissionSub.DynamicSecrets, { environment: environmentSlug, secretPath: path })
|
||||
);
|
||||
|
||||
const { decryptor: secretManagerDecryptor } = await kmsService.createCipherPairWithDataKey({
|
||||
type: KmsDataKey.SecretManager,
|
||||
@ -259,7 +274,25 @@ export const dynamicSecretLeaseServiceFactory = ({
|
||||
if (!dynamicSecretLease || dynamicSecretLease.dynamicSecret.folderId !== folder.id)
|
||||
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 decryptedStoredInput = JSON.parse(
|
||||
secretManagerDecryptor({ cipherTextBlob: Buffer.from(dynamicSecretCfg.encryptedInput) }).toString()
|
||||
@ -309,10 +342,6 @@ export const dynamicSecretLeaseServiceFactory = ({
|
||||
actorOrgId,
|
||||
actionProjectType: ActionProjectType.SecretManager
|
||||
});
|
||||
ForbiddenError.from(permission).throwUnlessCan(
|
||||
ProjectPermissionDynamicSecretActions.Lease,
|
||||
subject(ProjectPermissionSub.DynamicSecrets, { environment: environmentSlug, secretPath: path })
|
||||
);
|
||||
|
||||
const folder = await folderDAL.findBySecretPath(projectId, environmentSlug, path);
|
||||
if (!folder)
|
||||
@ -326,6 +355,15 @@ export const dynamicSecretLeaseServiceFactory = ({
|
||||
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 });
|
||||
return dynamicSecretLeases;
|
||||
};
|
||||
@ -352,10 +390,6 @@ export const dynamicSecretLeaseServiceFactory = ({
|
||||
actorOrgId,
|
||||
actionProjectType: ActionProjectType.SecretManager
|
||||
});
|
||||
ForbiddenError.from(permission).throwUnlessCan(
|
||||
ProjectPermissionDynamicSecretActions.Lease,
|
||||
subject(ProjectPermissionSub.DynamicSecrets, { environment: environmentSlug, secretPath: path })
|
||||
);
|
||||
|
||||
const folder = await folderDAL.findBySecretPath(projectId, environmentSlug, path);
|
||||
if (!folder) throw new NotFoundError({ message: `Folder with path '${path}' not found` });
|
||||
@ -364,6 +398,25 @@ export const dynamicSecretLeaseServiceFactory = ({
|
||||
if (!dynamicSecretLease)
|
||||
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;
|
||||
};
|
||||
|
||||
|
@ -1,9 +1,17 @@
|
||||
import { Knex } from "knex";
|
||||
|
||||
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 { 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 { SecretsOrderBy } from "@app/services/secret/secret-types";
|
||||
|
||||
@ -12,6 +20,86 @@ export type TDynamicSecretDALFactory = ReturnType<typeof dynamicSecretDALFactory
|
||||
export const dynamicSecretDALFactory = (db: TDbClient) => {
|
||||
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)
|
||||
const listDynamicSecretsByFolderIds = async (
|
||||
{
|
||||
@ -39,18 +127,27 @@ export const dynamicSecretDALFactory = (db: TDbClient) => {
|
||||
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.Environment, `${TableName.SecretFolder}.envId`, `${TableName.Environment}.id`)
|
||||
.select(
|
||||
selectAllTableCols(TableName.DynamicSecret),
|
||||
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);
|
||||
|
||||
let queryWithLimit;
|
||||
if (limit) {
|
||||
const rankOffset = offset + 1;
|
||||
return await (tx || db)
|
||||
queryWithLimit = (tx || db.replicaNode())
|
||||
.with("w", query)
|
||||
.select("*")
|
||||
.from<Awaited<typeof query>[number]>("w")
|
||||
@ -58,7 +155,22 @@ export const dynamicSecretDALFactory = (db: TDbClient) => {
|
||||
.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;
|
||||
} 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);
|
||||
}
|
||||
|
||||
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));
|
||||
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 { KmsDataKey } from "@app/services/kms/kms-types";
|
||||
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 { TDynamicSecretLeaseDALFactory } from "../dynamic-secret-lease/dynamic-secret-lease-dal";
|
||||
@ -46,6 +47,7 @@ type TDynamicSecretServiceFactoryDep = {
|
||||
permissionService: Pick<TPermissionServiceFactory, "getProjectPermission">;
|
||||
kmsService: Pick<TKmsServiceFactory, "createCipherPairWithDataKey">;
|
||||
projectGatewayDAL: Pick<TProjectGatewayDALFactory, "findOne">;
|
||||
resourceMetadataDAL: Pick<TResourceMetadataDALFactory, "insertMany" | "delete">;
|
||||
};
|
||||
|
||||
export type TDynamicSecretServiceFactory = ReturnType<typeof dynamicSecretServiceFactory>;
|
||||
@ -60,7 +62,8 @@ export const dynamicSecretServiceFactory = ({
|
||||
dynamicSecretQueueService,
|
||||
projectDAL,
|
||||
kmsService,
|
||||
projectGatewayDAL
|
||||
projectGatewayDAL,
|
||||
resourceMetadataDAL
|
||||
}: TDynamicSecretServiceFactoryDep) => {
|
||||
const create = async ({
|
||||
path,
|
||||
@ -73,7 +76,8 @@ export const dynamicSecretServiceFactory = ({
|
||||
projectSlug,
|
||||
actorOrgId,
|
||||
defaultTTL,
|
||||
actorAuthMethod
|
||||
actorAuthMethod,
|
||||
metadata
|
||||
}: TCreateDynamicSecretDTO) => {
|
||||
const project = await projectDAL.findProjectBySlug(projectSlug, actorOrgId);
|
||||
if (!project) throw new NotFoundError({ message: `Project with slug '${projectSlug}' not found` });
|
||||
@ -87,9 +91,10 @@ export const dynamicSecretServiceFactory = ({
|
||||
actorOrgId,
|
||||
actionProjectType: ActionProjectType.SecretManager
|
||||
});
|
||||
|
||||
ForbiddenError.from(permission).throwUnlessCan(
|
||||
ProjectPermissionDynamicSecretActions.CreateRootCredential,
|
||||
subject(ProjectPermissionSub.DynamicSecrets, { environment: environmentSlug, secretPath: path })
|
||||
subject(ProjectPermissionSub.DynamicSecrets, { environment: environmentSlug, secretPath: path, metadata })
|
||||
);
|
||||
|
||||
const plan = await licenseService.getPlan(actorOrgId);
|
||||
@ -131,16 +136,36 @@ export const dynamicSecretServiceFactory = ({
|
||||
projectId
|
||||
});
|
||||
|
||||
const dynamicSecretCfg = await dynamicSecretDAL.create({
|
||||
type: provider.type,
|
||||
version: 1,
|
||||
encryptedInput: secretManagerEncryptor({ plainText: Buffer.from(JSON.stringify(inputs)) }).cipherTextBlob,
|
||||
maxTTL,
|
||||
defaultTTL,
|
||||
folderId: folder.id,
|
||||
name,
|
||||
projectGatewayId: selectedGatewayId
|
||||
const dynamicSecretCfg = await dynamicSecretDAL.transaction(async (tx) => {
|
||||
const cfg = await dynamicSecretDAL.create(
|
||||
{
|
||||
type: provider.type,
|
||||
version: 1,
|
||||
encryptedInput: secretManagerEncryptor({ plainText: Buffer.from(JSON.stringify(inputs)) }).cipherTextBlob,
|
||||
maxTTL,
|
||||
defaultTTL,
|
||||
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;
|
||||
};
|
||||
|
||||
@ -156,7 +181,8 @@ export const dynamicSecretServiceFactory = ({
|
||||
actorId,
|
||||
newName,
|
||||
actorOrgId,
|
||||
actorAuthMethod
|
||||
actorAuthMethod,
|
||||
metadata
|
||||
}: TUpdateDynamicSecretDTO) => {
|
||||
const project = await projectDAL.findProjectBySlug(projectSlug, actorOrgId);
|
||||
if (!project) throw new NotFoundError({ message: `Project with slug '${projectSlug}' not found` });
|
||||
@ -171,10 +197,6 @@ export const dynamicSecretServiceFactory = ({
|
||||
actorOrgId,
|
||||
actionProjectType: ActionProjectType.SecretManager
|
||||
});
|
||||
ForbiddenError.from(permission).throwUnlessCan(
|
||||
ProjectPermissionDynamicSecretActions.EditRootCredential,
|
||||
subject(ProjectPermissionSub.DynamicSecrets, { environment: environmentSlug, secretPath: path })
|
||||
);
|
||||
|
||||
const plan = await licenseService.getPlan(actorOrgId);
|
||||
if (!plan?.dynamicSecret) {
|
||||
@ -193,6 +215,27 @@ export const dynamicSecretServiceFactory = ({
|
||||
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) {
|
||||
const existingDynamicSecret = await dynamicSecretDAL.findOne({ name: newName, folderId: folder.id });
|
||||
if (existingDynamicSecret)
|
||||
@ -231,14 +274,41 @@ export const dynamicSecretServiceFactory = ({
|
||||
const isConnected = await selectedProvider.validateConnection(newInput);
|
||||
if (!isConnected) throw new BadRequestError({ message: "Provider connection failed" });
|
||||
|
||||
const updatedDynamicCfg = await dynamicSecretDAL.updateById(dynamicSecretCfg.id, {
|
||||
encryptedInput: secretManagerEncryptor({ plainText: Buffer.from(JSON.stringify(updatedInput)) }).cipherTextBlob,
|
||||
maxTTL,
|
||||
defaultTTL,
|
||||
name: newName ?? name,
|
||||
status: null,
|
||||
statusDetails: null,
|
||||
projectGatewayId: selectedGatewayId
|
||||
const updatedDynamicCfg = await dynamicSecretDAL.transaction(async (tx) => {
|
||||
const cfg = await dynamicSecretDAL.updateById(
|
||||
dynamicSecretCfg.id,
|
||||
{
|
||||
encryptedInput: secretManagerEncryptor({ plainText: Buffer.from(JSON.stringify(updatedInput)) })
|
||||
.cipherTextBlob,
|
||||
maxTTL,
|
||||
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;
|
||||
@ -268,10 +338,6 @@ export const dynamicSecretServiceFactory = ({
|
||||
actorOrgId,
|
||||
actionProjectType: ActionProjectType.SecretManager
|
||||
});
|
||||
ForbiddenError.from(permission).throwUnlessCan(
|
||||
ProjectPermissionDynamicSecretActions.DeleteRootCredential,
|
||||
subject(ProjectPermissionSub.DynamicSecrets, { environment: environmentSlug, secretPath: path })
|
||||
);
|
||||
|
||||
const folder = await folderDAL.findBySecretPath(projectId, environmentSlug, path);
|
||||
if (!folder)
|
||||
@ -282,6 +348,15 @@ export const dynamicSecretServiceFactory = ({
|
||||
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 });
|
||||
// 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
|
||||
@ -329,14 +404,6 @@ export const dynamicSecretServiceFactory = ({
|
||||
actorOrgId,
|
||||
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);
|
||||
if (!folder)
|
||||
@ -346,6 +413,25 @@ export const dynamicSecretServiceFactory = ({
|
||||
if (!dynamicSecretCfg) {
|
||||
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({
|
||||
type: KmsDataKey.SecretManager,
|
||||
projectId
|
||||
@ -356,6 +442,7 @@ export const dynamicSecretServiceFactory = ({
|
||||
) as object;
|
||||
const selectedProvider = dynamicSecretProviders[dynamicSecretCfg.type as DynamicSecretProviders];
|
||||
const providerInputs = (await selectedProvider.validateProviderInputs(decryptedStoredInput)) as object;
|
||||
|
||||
return { ...dynamicSecretCfg, inputs: providerInputs };
|
||||
};
|
||||
|
||||
@ -426,7 +513,7 @@ export const dynamicSecretServiceFactory = ({
|
||||
});
|
||||
ForbiddenError.from(permission).throwUnlessCan(
|
||||
ProjectPermissionDynamicSecretActions.ReadRootCredential,
|
||||
subject(ProjectPermissionSub.DynamicSecrets, { environment: environmentSlug, secretPath: path })
|
||||
ProjectPermissionSub.DynamicSecrets
|
||||
);
|
||||
|
||||
const folder = await folderDAL.findBySecretPath(projectId, environmentSlug, path);
|
||||
@ -473,16 +560,12 @@ export const dynamicSecretServiceFactory = ({
|
||||
actorOrgId,
|
||||
actionProjectType: ActionProjectType.SecretManager
|
||||
});
|
||||
ForbiddenError.from(permission).throwUnlessCan(
|
||||
ProjectPermissionDynamicSecretActions.ReadRootCredential,
|
||||
subject(ProjectPermissionSub.DynamicSecrets, { environment: environmentSlug, secretPath: path })
|
||||
);
|
||||
|
||||
const folder = await folderDAL.findBySecretPath(projectId, environmentSlug, path);
|
||||
if (!folder)
|
||||
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 },
|
||||
{
|
||||
limit,
|
||||
@ -490,7 +573,17 @@ export const dynamicSecretServiceFactory = ({
|
||||
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 (
|
||||
@ -542,24 +635,14 @@ export const dynamicSecretServiceFactory = ({
|
||||
isInternal,
|
||||
...params
|
||||
}: TListDynamicSecretsMultiEnvDTO) => {
|
||||
if (!isInternal) {
|
||||
const { permission } = await permissionService.getProjectPermission({
|
||||
actor,
|
||||
actorId,
|
||||
projectId,
|
||||
actorAuthMethod,
|
||||
actorOrgId,
|
||||
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 { permission } = await permissionService.getProjectPermission({
|
||||
actor,
|
||||
actorId,
|
||||
projectId,
|
||||
actorAuthMethod,
|
||||
actorOrgId,
|
||||
actionProjectType: ActionProjectType.SecretManager
|
||||
});
|
||||
|
||||
const folders = await folderDAL.findBySecretPathMultiEnv(projectId, environmentSlugs, path);
|
||||
if (!folders.length)
|
||||
@ -572,7 +655,16 @@ export const dynamicSecretServiceFactory = ({
|
||||
...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 ({
|
||||
|
@ -1,6 +1,7 @@
|
||||
import { z } from "zod";
|
||||
|
||||
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 { DynamicSecretProviderSchema } from "./providers/models";
|
||||
@ -20,6 +21,7 @@ export type TCreateDynamicSecretDTO = {
|
||||
environmentSlug: string;
|
||||
name: string;
|
||||
projectSlug: string;
|
||||
metadata?: ResourceMetadataDTO;
|
||||
} & Omit<TProjectPermission, "projectId">;
|
||||
|
||||
export type TUpdateDynamicSecretDTO = {
|
||||
@ -31,6 +33,7 @@ export type TUpdateDynamicSecretDTO = {
|
||||
environmentSlug: string;
|
||||
inputs?: TProvider["inputs"];
|
||||
projectSlug: string;
|
||||
metadata?: ResourceMetadataDTO;
|
||||
} & Omit<TProjectPermission, "projectId">;
|
||||
|
||||
export type TDeleteDynamicSecretDTO = {
|
||||
|
@ -7,7 +7,7 @@ import { BadRequestError, InternalServerError, NotFoundError } from "@app/lib/er
|
||||
import { alphaNumericNanoId } from "@app/lib/nanoid";
|
||||
import { TKmsKeyDALFactory } from "@app/services/kms/kms-key-dal";
|
||||
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 { OrgPermissionActions, OrgPermissionSubjects } from "../permission/org-permission";
|
||||
@ -115,6 +115,7 @@ export const externalKmsServiceFactory = ({
|
||||
{
|
||||
isReserved: false,
|
||||
description,
|
||||
keyUsage: KmsKeyUsage.ENCRYPT_DECRYPT,
|
||||
name: kmsName,
|
||||
orgId: actorOrgId
|
||||
},
|
||||
|
@ -92,7 +92,7 @@ export const GcpKmsProviderFactory = async ({ inputs }: GcpKmsProviderArgs): Pro
|
||||
plaintext: data
|
||||
});
|
||||
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) => {
|
||||
@ -101,7 +101,7 @@ export const GcpKmsProviderFactory = async ({ inputs }: GcpKmsProviderArgs): Pro
|
||||
ciphertext: encryptedBlob
|
||||
});
|
||||
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 {
|
||||
|
@ -258,7 +258,7 @@ export const hsmServiceFactory = ({ hsmModule: { isInitialized, pkcs11 }, envCon
|
||||
const decrypt: {
|
||||
(encryptedBlob: Buffer, providedSession: pkcs11js.Handle): Promise<Buffer>;
|
||||
(encryptedBlob: Buffer): Promise<Buffer>;
|
||||
} = async (encryptedBlob: Buffer, providedSession?: pkcs11js.Handle) => {
|
||||
} = async (encryptedBlob: Buffer, providedSession?: pkcs11js.Handle): Promise<Buffer> => {
|
||||
if (!pkcs11 || !isInitialized) {
|
||||
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);
|
||||
|
||||
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);
|
||||
|
||||
// Create a new buffer from the decrypted data
|
||||
return Buffer.from(decryptedData);
|
||||
} catch (error) {
|
||||
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 { TKmsKeyDALFactory } from "@app/services/kms/kms-key-dal";
|
||||
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 { OrgPermissionKmipActions, OrgPermissionSubjects } from "../permission/org-permission";
|
||||
@ -403,6 +404,7 @@ export const kmipOperationServiceFactory = ({
|
||||
algorithm,
|
||||
isReserved: false,
|
||||
projectId,
|
||||
keyUsage: KmsKeyUsage.ENCRYPT_DECRYPT,
|
||||
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 { CertKeyAlgorithm } from "@app/services/certificate/certificate-types";
|
||||
|
||||
@ -49,7 +49,7 @@ type KmipOperationBaseDTO = {
|
||||
} & Omit<TOrgPermission, "orgId">;
|
||||
|
||||
export type TKmipCreateDTO = {
|
||||
algorithm: SymmetricEncryption;
|
||||
algorithm: SymmetricKeyAlgorithm;
|
||||
} & KmipOperationBaseDTO;
|
||||
|
||||
export type TKmipGetDTO = {
|
||||
@ -77,7 +77,7 @@ export type TKmipLocateDTO = KmipOperationBaseDTO;
|
||||
export type TKmipRegisterDTO = {
|
||||
name: string;
|
||||
key: string;
|
||||
algorithm: SymmetricEncryption;
|
||||
algorithm: SymmetricKeyAlgorithm;
|
||||
} & KmipOperationBaseDTO;
|
||||
|
||||
export type TSetupOrgKmipDTO = {
|
||||
|
@ -165,7 +165,8 @@ export const oidcConfigServiceFactory = ({
|
||||
allowedEmailDomains: oidcCfg.allowedEmailDomains,
|
||||
clientId,
|
||||
clientSecret,
|
||||
manageGroupMemberships: oidcCfg.manageGroupMemberships
|
||||
manageGroupMemberships: oidcCfg.manageGroupMemberships,
|
||||
jwtSignatureAlgorithm: oidcCfg.jwtSignatureAlgorithm
|
||||
};
|
||||
};
|
||||
|
||||
@ -481,7 +482,8 @@ export const oidcConfigServiceFactory = ({
|
||||
userinfoEndpoint,
|
||||
clientId,
|
||||
clientSecret,
|
||||
manageGroupMemberships
|
||||
manageGroupMemberships,
|
||||
jwtSignatureAlgorithm
|
||||
}: TUpdateOidcCfgDTO) => {
|
||||
const org = await orgDAL.findOne({
|
||||
slug: orgSlug
|
||||
@ -536,7 +538,8 @@ export const oidcConfigServiceFactory = ({
|
||||
jwksUri,
|
||||
isActive,
|
||||
lastUsed: null,
|
||||
manageGroupMemberships
|
||||
manageGroupMemberships,
|
||||
jwtSignatureAlgorithm
|
||||
};
|
||||
|
||||
if (clientId !== undefined) {
|
||||
@ -569,7 +572,8 @@ export const oidcConfigServiceFactory = ({
|
||||
userinfoEndpoint,
|
||||
clientId,
|
||||
clientSecret,
|
||||
manageGroupMemberships
|
||||
manageGroupMemberships,
|
||||
jwtSignatureAlgorithm
|
||||
}: TCreateOidcCfgDTO) => {
|
||||
const org = await orgDAL.findOne({
|
||||
slug: orgSlug
|
||||
@ -613,6 +617,7 @@ export const oidcConfigServiceFactory = ({
|
||||
userinfoEndpoint,
|
||||
orgId: org.id,
|
||||
manageGroupMemberships,
|
||||
jwtSignatureAlgorithm,
|
||||
encryptedOidcClientId: encryptor({ plainText: Buffer.from(clientId) }).cipherTextBlob,
|
||||
encryptedOidcClientSecret: encryptor({ plainText: Buffer.from(clientSecret) }).cipherTextBlob
|
||||
});
|
||||
@ -676,7 +681,8 @@ export const oidcConfigServiceFactory = ({
|
||||
const client = new issuer.Client({
|
||||
client_id: oidcCfg.clientId,
|
||||
client_secret: oidcCfg.clientSecret,
|
||||
redirect_uris: [`${appCfg.SITE_URL}/api/v1/sso/oidc/callback`]
|
||||
redirect_uris: [`${appCfg.SITE_URL}/api/v1/sso/oidc/callback`],
|
||||
id_token_signed_response_alg: oidcCfg.jwtSignatureAlgorithm
|
||||
});
|
||||
|
||||
const strategy = new OpenIdStrategy(
|
||||
|
@ -5,6 +5,12 @@ export enum OIDCConfigurationType {
|
||||
DISCOVERY_URL = "discoveryURL"
|
||||
}
|
||||
|
||||
export enum OIDCJWTSignatureAlgorithm {
|
||||
RS256 = "RS256",
|
||||
HS256 = "HS256",
|
||||
RS512 = "RS512"
|
||||
}
|
||||
|
||||
export type TOidcLoginDTO = {
|
||||
externalId: string;
|
||||
email: string;
|
||||
@ -40,6 +46,7 @@ export type TCreateOidcCfgDTO = {
|
||||
isActive: boolean;
|
||||
orgSlug: string;
|
||||
manageGroupMemberships: boolean;
|
||||
jwtSignatureAlgorithm: OIDCJWTSignatureAlgorithm;
|
||||
} & TGenericPermission;
|
||||
|
||||
export type TUpdateOidcCfgDTO = Partial<{
|
||||
@ -56,5 +63,6 @@ export type TUpdateOidcCfgDTO = Partial<{
|
||||
isActive: boolean;
|
||||
orgSlug: string;
|
||||
manageGroupMemberships: boolean;
|
||||
jwtSignatureAlgorithm: OIDCJWTSignatureAlgorithm;
|
||||
}> &
|
||||
TGenericPermission;
|
||||
|
@ -32,7 +32,9 @@ export enum ProjectPermissionCmekActions {
|
||||
Edit = "edit",
|
||||
Delete = "delete",
|
||||
Encrypt = "encrypt",
|
||||
Decrypt = "decrypt"
|
||||
Decrypt = "decrypt",
|
||||
Sign = "sign",
|
||||
Verify = "verify"
|
||||
}
|
||||
|
||||
export enum ProjectPermissionDynamicSecretActions {
|
||||
@ -153,6 +155,10 @@ export type SecretFolderSubjectFields = {
|
||||
export type DynamicSecretSubjectFields = {
|
||||
environment: string;
|
||||
secretPath: string;
|
||||
metadata?: {
|
||||
key: string;
|
||||
value: string;
|
||||
}[];
|
||||
};
|
||||
|
||||
export type SecretImportSubjectFields = {
|
||||
@ -282,6 +288,42 @@ const SecretConditionV1Schema = z
|
||||
})
|
||||
.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
|
||||
.object({
|
||||
environment: z.union([
|
||||
@ -579,7 +621,7 @@ export const ProjectPermissionV2Schema = z.discriminatedUnion("subject", [
|
||||
action: CASL_ACTION_SCHEMA_NATIVE_ENUM(ProjectPermissionDynamicSecretActions).describe(
|
||||
"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."
|
||||
).optional()
|
||||
}),
|
||||
@ -732,7 +774,9 @@ const buildAdminPermissionRules = () => {
|
||||
ProjectPermissionCmekActions.Delete,
|
||||
ProjectPermissionCmekActions.Read,
|
||||
ProjectPermissionCmekActions.Encrypt,
|
||||
ProjectPermissionCmekActions.Decrypt
|
||||
ProjectPermissionCmekActions.Decrypt,
|
||||
ProjectPermissionCmekActions.Sign,
|
||||
ProjectPermissionCmekActions.Verify
|
||||
],
|
||||
ProjectPermissionSub.Cmek
|
||||
);
|
||||
@ -935,7 +979,9 @@ const buildMemberPermissionRules = () => {
|
||||
ProjectPermissionCmekActions.Delete,
|
||||
ProjectPermissionCmekActions.Read,
|
||||
ProjectPermissionCmekActions.Encrypt,
|
||||
ProjectPermissionCmekActions.Decrypt
|
||||
ProjectPermissionCmekActions.Decrypt,
|
||||
ProjectPermissionCmekActions.Sign,
|
||||
ProjectPermissionCmekActions.Verify
|
||||
],
|
||||
ProjectPermissionSub.Cmek
|
||||
);
|
||||
|
@ -0,0 +1,15 @@
|
||||
import { SecretRotation } from "@app/ee/services/secret-rotation-v2/secret-rotation-v2-enums";
|
||||
import { TSecretRotationV2ListItem } from "@app/ee/services/secret-rotation-v2/secret-rotation-v2-types";
|
||||
import { AppConnection } from "@app/services/app-connection/app-connection-enums";
|
||||
|
||||
export const AUTH0_CLIENT_SECRET_ROTATION_LIST_OPTION: TSecretRotationV2ListItem = {
|
||||
name: "Auth0 Client Secret",
|
||||
type: SecretRotation.Auth0ClientSecret,
|
||||
connection: AppConnection.Auth0,
|
||||
template: {
|
||||
secretsMapping: {
|
||||
clientId: "AUTH0_CLIENT_ID",
|
||||
clientSecret: "AUTH0_CLIENT_SECRET"
|
||||
}
|
||||
}
|
||||
};
|
@ -0,0 +1,104 @@
|
||||
import {
|
||||
TAuth0ClientSecretRotationGeneratedCredentials,
|
||||
TAuth0ClientSecretRotationWithConnection
|
||||
} from "@app/ee/services/secret-rotation-v2/auth0-client-secret/auth0-client-secret-rotation-types";
|
||||
import {
|
||||
TRotationFactory,
|
||||
TRotationFactoryGetSecretsPayload,
|
||||
TRotationFactoryIssueCredentials,
|
||||
TRotationFactoryRevokeCredentials,
|
||||
TRotationFactoryRotateCredentials
|
||||
} from "@app/ee/services/secret-rotation-v2/secret-rotation-v2-types";
|
||||
import { request } from "@app/lib/config/request";
|
||||
import { blockLocalAndPrivateIpAddresses } from "@app/lib/validator";
|
||||
import { getAuth0ConnectionAccessToken } from "@app/services/app-connection/auth0";
|
||||
|
||||
import { generatePassword } from "../shared/utils";
|
||||
|
||||
export const auth0ClientSecretRotationFactory: TRotationFactory<
|
||||
TAuth0ClientSecretRotationWithConnection,
|
||||
TAuth0ClientSecretRotationGeneratedCredentials
|
||||
> = (secretRotation, appConnectionDAL, kmsService) => {
|
||||
const {
|
||||
connection,
|
||||
parameters: { clientId },
|
||||
secretsMapping
|
||||
} = secretRotation;
|
||||
|
||||
const $rotateClientSecret = async () => {
|
||||
const accessToken = await getAuth0ConnectionAccessToken(connection, appConnectionDAL, kmsService);
|
||||
const { audience } = connection.credentials;
|
||||
await blockLocalAndPrivateIpAddresses(audience);
|
||||
const clientSecret = generatePassword();
|
||||
|
||||
await request.request({
|
||||
method: "PATCH",
|
||||
url: `${audience}clients/${clientId}`,
|
||||
headers: { authorization: `Bearer ${accessToken}` },
|
||||
data: {
|
||||
client_secret: clientSecret
|
||||
}
|
||||
});
|
||||
|
||||
return { clientId, clientSecret };
|
||||
};
|
||||
|
||||
const issueCredentials: TRotationFactoryIssueCredentials<TAuth0ClientSecretRotationGeneratedCredentials> = async (
|
||||
callback
|
||||
) => {
|
||||
const credentials = await $rotateClientSecret();
|
||||
|
||||
return callback(credentials);
|
||||
};
|
||||
|
||||
const revokeCredentials: TRotationFactoryRevokeCredentials<TAuth0ClientSecretRotationGeneratedCredentials> = async (
|
||||
_,
|
||||
callback
|
||||
) => {
|
||||
const accessToken = await getAuth0ConnectionAccessToken(connection, appConnectionDAL, kmsService);
|
||||
const { audience } = connection.credentials;
|
||||
await blockLocalAndPrivateIpAddresses(audience);
|
||||
|
||||
// we just trigger an auth0 rotation to negate our credentials
|
||||
await request.request({
|
||||
method: "POST",
|
||||
url: `${audience}clients/${clientId}/rotate-secret`,
|
||||
headers: { authorization: `Bearer ${accessToken}` }
|
||||
});
|
||||
|
||||
return callback();
|
||||
};
|
||||
|
||||
const rotateCredentials: TRotationFactoryRotateCredentials<TAuth0ClientSecretRotationGeneratedCredentials> = async (
|
||||
_,
|
||||
callback
|
||||
) => {
|
||||
const credentials = await $rotateClientSecret();
|
||||
|
||||
return callback(credentials);
|
||||
};
|
||||
|
||||
const getSecretsPayload: TRotationFactoryGetSecretsPayload<TAuth0ClientSecretRotationGeneratedCredentials> = (
|
||||
generatedCredentials
|
||||
) => {
|
||||
const secrets = [
|
||||
{
|
||||
key: secretsMapping.clientId,
|
||||
value: generatedCredentials.clientId
|
||||
},
|
||||
{
|
||||
key: secretsMapping.clientSecret,
|
||||
value: generatedCredentials.clientSecret
|
||||
}
|
||||
];
|
||||
|
||||
return secrets;
|
||||
};
|
||||
|
||||
return {
|
||||
issueCredentials,
|
||||
revokeCredentials,
|
||||
rotateCredentials,
|
||||
getSecretsPayload
|
||||
};
|
||||
};
|
@ -0,0 +1,67 @@
|
||||
import { z } from "zod";
|
||||
|
||||
import { SecretRotation } from "@app/ee/services/secret-rotation-v2/secret-rotation-v2-enums";
|
||||
import {
|
||||
BaseCreateSecretRotationSchema,
|
||||
BaseSecretRotationSchema,
|
||||
BaseUpdateSecretRotationSchema
|
||||
} from "@app/ee/services/secret-rotation-v2/secret-rotation-v2-schemas";
|
||||
import { SecretRotations } from "@app/lib/api-docs";
|
||||
import { SecretNameSchema } from "@app/server/lib/schemas";
|
||||
import { AppConnection } from "@app/services/app-connection/app-connection-enums";
|
||||
|
||||
export const Auth0ClientSecretRotationGeneratedCredentialsSchema = z
|
||||
.object({
|
||||
clientId: z.string(),
|
||||
clientSecret: z.string()
|
||||
})
|
||||
.array()
|
||||
.min(1)
|
||||
.max(2);
|
||||
|
||||
const Auth0ClientSecretRotationParametersSchema = z.object({
|
||||
clientId: z
|
||||
.string()
|
||||
.trim()
|
||||
.min(1, "Client ID Required")
|
||||
.describe(SecretRotations.PARAMETERS.AUTH0_CLIENT_SECRET.clientId)
|
||||
});
|
||||
|
||||
const Auth0ClientSecretRotationSecretsMappingSchema = z.object({
|
||||
clientId: SecretNameSchema.describe(SecretRotations.SECRETS_MAPPING.AUTH0_CLIENT_SECRET.clientId),
|
||||
clientSecret: SecretNameSchema.describe(SecretRotations.SECRETS_MAPPING.AUTH0_CLIENT_SECRET.clientSecret)
|
||||
});
|
||||
|
||||
export const Auth0ClientSecretRotationTemplateSchema = z.object({
|
||||
secretsMapping: z.object({
|
||||
clientId: z.string(),
|
||||
clientSecret: z.string()
|
||||
})
|
||||
});
|
||||
|
||||
export const Auth0ClientSecretRotationSchema = BaseSecretRotationSchema(SecretRotation.Auth0ClientSecret).extend({
|
||||
type: z.literal(SecretRotation.Auth0ClientSecret),
|
||||
parameters: Auth0ClientSecretRotationParametersSchema,
|
||||
secretsMapping: Auth0ClientSecretRotationSecretsMappingSchema
|
||||
});
|
||||
|
||||
export const CreateAuth0ClientSecretRotationSchema = BaseCreateSecretRotationSchema(
|
||||
SecretRotation.Auth0ClientSecret
|
||||
).extend({
|
||||
parameters: Auth0ClientSecretRotationParametersSchema,
|
||||
secretsMapping: Auth0ClientSecretRotationSecretsMappingSchema
|
||||
});
|
||||
|
||||
export const UpdateAuth0ClientSecretRotationSchema = BaseUpdateSecretRotationSchema(
|
||||
SecretRotation.Auth0ClientSecret
|
||||
).extend({
|
||||
parameters: Auth0ClientSecretRotationParametersSchema.optional(),
|
||||
secretsMapping: Auth0ClientSecretRotationSecretsMappingSchema.optional()
|
||||
});
|
||||
|
||||
export const Auth0ClientSecretRotationListItemSchema = z.object({
|
||||
name: z.literal("Auth0 Client Secret"),
|
||||
connection: z.literal(AppConnection.Auth0),
|
||||
type: z.literal(SecretRotation.Auth0ClientSecret),
|
||||
template: Auth0ClientSecretRotationTemplateSchema
|
||||
});
|
@ -0,0 +1,24 @@
|
||||
import { z } from "zod";
|
||||
|
||||
import { TAuth0Connection } from "@app/services/app-connection/auth0";
|
||||
|
||||
import {
|
||||
Auth0ClientSecretRotationGeneratedCredentialsSchema,
|
||||
Auth0ClientSecretRotationListItemSchema,
|
||||
Auth0ClientSecretRotationSchema,
|
||||
CreateAuth0ClientSecretRotationSchema
|
||||
} from "./auth0-client-secret-rotation-schemas";
|
||||
|
||||
export type TAuth0ClientSecretRotation = z.infer<typeof Auth0ClientSecretRotationSchema>;
|
||||
|
||||
export type TAuth0ClientSecretRotationInput = z.infer<typeof CreateAuth0ClientSecretRotationSchema>;
|
||||
|
||||
export type TAuth0ClientSecretRotationListItem = z.infer<typeof Auth0ClientSecretRotationListItemSchema>;
|
||||
|
||||
export type TAuth0ClientSecretRotationWithConnection = TAuth0ClientSecretRotation & {
|
||||
connection: TAuth0Connection;
|
||||
};
|
||||
|
||||
export type TAuth0ClientSecretRotationGeneratedCredentials = z.infer<
|
||||
typeof Auth0ClientSecretRotationGeneratedCredentialsSchema
|
||||
>;
|
@ -0,0 +1,3 @@
|
||||
export * from "./auth0-client-secret-rotation-constants";
|
||||
export * from "./auth0-client-secret-rotation-schemas";
|
||||
export * from "./auth0-client-secret-rotation-types";
|
@ -1,6 +1,7 @@
|
||||
export enum SecretRotation {
|
||||
PostgresCredentials = "postgres-credentials",
|
||||
MsSqlCredentials = "mssql-credentials"
|
||||
MsSqlCredentials = "mssql-credentials",
|
||||
Auth0ClientSecret = "auth0-client-secret"
|
||||
}
|
||||
|
||||
export enum SecretRotationStatus {
|
||||
|
@ -3,6 +3,7 @@ import { AxiosError } from "axios";
|
||||
import { getConfig } from "@app/lib/config/env";
|
||||
import { KmsDataKey } from "@app/services/kms/kms-types";
|
||||
|
||||
import { AUTH0_CLIENT_SECRET_ROTATION_LIST_OPTION } from "./auth0-client-secret";
|
||||
import { MSSQL_CREDENTIALS_ROTATION_LIST_OPTION } from "./mssql-credentials";
|
||||
import { POSTGRES_CREDENTIALS_ROTATION_LIST_OPTION } from "./postgres-credentials";
|
||||
import { SecretRotation, SecretRotationStatus } from "./secret-rotation-v2-enums";
|
||||
@ -16,7 +17,8 @@ import {
|
||||
|
||||
const SECRET_ROTATION_LIST_OPTIONS: Record<SecretRotation, TSecretRotationV2ListItem> = {
|
||||
[SecretRotation.PostgresCredentials]: POSTGRES_CREDENTIALS_ROTATION_LIST_OPTION,
|
||||
[SecretRotation.MsSqlCredentials]: MSSQL_CREDENTIALS_ROTATION_LIST_OPTION
|
||||
[SecretRotation.MsSqlCredentials]: MSSQL_CREDENTIALS_ROTATION_LIST_OPTION,
|
||||
[SecretRotation.Auth0ClientSecret]: AUTH0_CLIENT_SECRET_ROTATION_LIST_OPTION
|
||||
};
|
||||
|
||||
export const listSecretRotationOptions = () => {
|
||||
|
@ -3,10 +3,12 @@ import { AppConnection } from "@app/services/app-connection/app-connection-enums
|
||||
|
||||
export const SECRET_ROTATION_NAME_MAP: Record<SecretRotation, string> = {
|
||||
[SecretRotation.PostgresCredentials]: "PostgreSQL Credentials",
|
||||
[SecretRotation.MsSqlCredentials]: "Microsoft SQL Sever Credentials"
|
||||
[SecretRotation.MsSqlCredentials]: "Microsoft SQL Sever Credentials",
|
||||
[SecretRotation.Auth0ClientSecret]: "Auth0 Client Secret"
|
||||
};
|
||||
|
||||
export const SECRET_ROTATION_CONNECTION_MAP: Record<SecretRotation, AppConnection> = {
|
||||
[SecretRotation.PostgresCredentials]: AppConnection.Postgres,
|
||||
[SecretRotation.MsSqlCredentials]: AppConnection.MsSql
|
||||
[SecretRotation.MsSqlCredentials]: AppConnection.MsSql,
|
||||
[SecretRotation.Auth0ClientSecret]: AppConnection.Auth0
|
||||
};
|
||||
|
@ -13,6 +13,7 @@ import {
|
||||
ProjectPermissionSecretRotationActions,
|
||||
ProjectPermissionSub
|
||||
} from "@app/ee/services/permission/project-permission";
|
||||
import { auth0ClientSecretRotationFactory } from "@app/ee/services/secret-rotation-v2/auth0-client-secret/auth0-client-secret-rotation-fns";
|
||||
import { SecretRotation, SecretRotationStatus } from "@app/ee/services/secret-rotation-v2/secret-rotation-v2-enums";
|
||||
import {
|
||||
calculateNextRotationAt,
|
||||
@ -41,6 +42,7 @@ import {
|
||||
TRotationFactory,
|
||||
TSecretRotationRotateGeneratedCredentials,
|
||||
TSecretRotationV2,
|
||||
TSecretRotationV2GeneratedCredentials,
|
||||
TSecretRotationV2Raw,
|
||||
TSecretRotationV2WithConnection,
|
||||
TUpdateSecretRotationV2DTO
|
||||
@ -53,6 +55,7 @@ import { DatabaseErrorCode } from "@app/lib/error-codes";
|
||||
import { BadRequestError, DatabaseError, InternalServerError, NotFoundError } from "@app/lib/errors";
|
||||
import { OrderByDirection, OrgServiceActor } from "@app/lib/types";
|
||||
import { QueueJobs, TQueueServiceFactory } from "@app/queue";
|
||||
import { TAppConnectionDALFactory } from "@app/services/app-connection/app-connection-dal";
|
||||
import { decryptAppConnection } from "@app/services/app-connection/app-connection-fns";
|
||||
import { TAppConnectionServiceFactory } from "@app/services/app-connection/app-connection-service";
|
||||
import { ActorType } from "@app/services/auth/auth-type";
|
||||
@ -97,15 +100,21 @@ export type TSecretRotationV2ServiceFactoryDep = {
|
||||
secretQueueService: Pick<TSecretQueueFactory, "syncSecrets" | "removeSecretReminder">;
|
||||
snapshotService: Pick<TSecretSnapshotServiceFactory, "performSnapshot">;
|
||||
queueService: Pick<TQueueServiceFactory, "queuePg">;
|
||||
appConnectionDAL: Pick<TAppConnectionDALFactory, "updateById">;
|
||||
};
|
||||
|
||||
export type TSecretRotationV2ServiceFactory = ReturnType<typeof secretRotationV2ServiceFactory>;
|
||||
|
||||
const MAX_GENERATED_CREDENTIALS_LENGTH = 2;
|
||||
|
||||
const SECRET_ROTATION_FACTORY_MAP: Record<SecretRotation, TRotationFactory> = {
|
||||
[SecretRotation.PostgresCredentials]: sqlCredentialsRotationFactory,
|
||||
[SecretRotation.MsSqlCredentials]: sqlCredentialsRotationFactory
|
||||
type TRotationFactoryImplementation = TRotationFactory<
|
||||
TSecretRotationV2WithConnection,
|
||||
TSecretRotationV2GeneratedCredentials
|
||||
>;
|
||||
const SECRET_ROTATION_FACTORY_MAP: Record<SecretRotation, TRotationFactoryImplementation> = {
|
||||
[SecretRotation.PostgresCredentials]: sqlCredentialsRotationFactory as TRotationFactoryImplementation,
|
||||
[SecretRotation.MsSqlCredentials]: sqlCredentialsRotationFactory as TRotationFactoryImplementation,
|
||||
[SecretRotation.Auth0ClientSecret]: auth0ClientSecretRotationFactory as TRotationFactoryImplementation
|
||||
};
|
||||
|
||||
export const secretRotationV2ServiceFactory = ({
|
||||
@ -125,7 +134,8 @@ export const secretRotationV2ServiceFactory = ({
|
||||
secretQueueService,
|
||||
snapshotService,
|
||||
keyStore,
|
||||
queueService
|
||||
queueService,
|
||||
appConnectionDAL
|
||||
}: TSecretRotationV2ServiceFactoryDep) => {
|
||||
const $queueSendSecretRotationStatusNotification = async (secretRotation: TSecretRotationV2Raw) => {
|
||||
const appCfg = getConfig();
|
||||
@ -429,11 +439,15 @@ export const secretRotationV2ServiceFactory = ({
|
||||
// validates permission to connect and app is valid for rotation type
|
||||
const connection = await appConnectionService.connectAppConnectionById(typeApp, payload.connectionId, actor);
|
||||
|
||||
const rotationFactory = SECRET_ROTATION_FACTORY_MAP[payload.type]({
|
||||
parameters: payload.parameters,
|
||||
secretsMapping,
|
||||
connection
|
||||
} as TSecretRotationV2WithConnection);
|
||||
const rotationFactory = SECRET_ROTATION_FACTORY_MAP[payload.type](
|
||||
{
|
||||
parameters: payload.parameters,
|
||||
secretsMapping,
|
||||
connection
|
||||
} as TSecretRotationV2WithConnection,
|
||||
appConnectionDAL,
|
||||
kmsService
|
||||
);
|
||||
|
||||
try {
|
||||
const currentTime = new Date();
|
||||
@ -441,7 +455,7 @@ export const secretRotationV2ServiceFactory = ({
|
||||
// callback structure to support transactional rollback when possible
|
||||
const secretRotation = await rotationFactory.issueCredentials(async (newCredentials) => {
|
||||
const encryptedGeneratedCredentials = await encryptSecretRotationCredentials({
|
||||
generatedCredentials: [newCredentials],
|
||||
generatedCredentials: [newCredentials] as TSecretRotationV2GeneratedCredentials,
|
||||
projectId,
|
||||
kmsService
|
||||
});
|
||||
@ -740,32 +754,37 @@ export const secretRotationV2ServiceFactory = ({
|
||||
message: `Secret Rotation with ID "${rotationId}" is not configured for ${SECRET_ROTATION_NAME_MAP[type]}`
|
||||
});
|
||||
|
||||
const deleteTransaction = secretRotationV2DAL.transaction(async (tx) => {
|
||||
if (deleteSecrets) {
|
||||
await fnSecretBulkDelete({
|
||||
secretDAL: secretV2BridgeDAL,
|
||||
secretQueueService,
|
||||
inputSecrets: Object.values(secretsMapping as TSecretRotationV2["secretsMapping"]).map((secretKey) => ({
|
||||
secretKey,
|
||||
type: SecretType.Shared
|
||||
})),
|
||||
projectId,
|
||||
folderId,
|
||||
actorId: actor.id, // not actually used since rotated secrets are shared
|
||||
tx
|
||||
});
|
||||
}
|
||||
const deleteTransaction = async () =>
|
||||
secretRotationV2DAL.transaction(async (tx) => {
|
||||
if (deleteSecrets) {
|
||||
await fnSecretBulkDelete({
|
||||
secretDAL: secretV2BridgeDAL,
|
||||
secretQueueService,
|
||||
inputSecrets: Object.values(secretsMapping as TSecretRotationV2["secretsMapping"]).map((secretKey) => ({
|
||||
secretKey,
|
||||
type: SecretType.Shared
|
||||
})),
|
||||
projectId,
|
||||
folderId,
|
||||
actorId: actor.id, // not actually used since rotated secrets are shared
|
||||
tx
|
||||
});
|
||||
}
|
||||
|
||||
return secretRotationV2DAL.deleteById(rotationId, tx);
|
||||
});
|
||||
return secretRotationV2DAL.deleteById(rotationId, tx);
|
||||
});
|
||||
|
||||
if (revokeGeneratedCredentials) {
|
||||
const appConnection = await decryptAppConnection(connection, kmsService);
|
||||
|
||||
const rotationFactory = SECRET_ROTATION_FACTORY_MAP[type]({
|
||||
...secretRotation,
|
||||
connection: appConnection
|
||||
} as TSecretRotationV2WithConnection);
|
||||
const rotationFactory = SECRET_ROTATION_FACTORY_MAP[type](
|
||||
{
|
||||
...secretRotation,
|
||||
connection: appConnection
|
||||
} as TSecretRotationV2WithConnection,
|
||||
appConnectionDAL,
|
||||
kmsService
|
||||
);
|
||||
|
||||
const generatedCredentials = await decryptSecretRotationCredentials({
|
||||
encryptedGeneratedCredentials,
|
||||
@ -773,9 +792,9 @@ export const secretRotationV2ServiceFactory = ({
|
||||
kmsService
|
||||
});
|
||||
|
||||
await rotationFactory.revokeCredentials(generatedCredentials, async () => deleteTransaction);
|
||||
await rotationFactory.revokeCredentials(generatedCredentials, deleteTransaction);
|
||||
} else {
|
||||
await deleteTransaction;
|
||||
await deleteTransaction();
|
||||
}
|
||||
|
||||
if (deleteSecrets) {
|
||||
@ -840,10 +859,14 @@ export const secretRotationV2ServiceFactory = ({
|
||||
|
||||
const inactiveCredentials = generatedCredentials[inactiveIndex];
|
||||
|
||||
const rotationFactory = SECRET_ROTATION_FACTORY_MAP[type as SecretRotation]({
|
||||
...secretRotation,
|
||||
connection: appConnection
|
||||
} as TSecretRotationV2WithConnection);
|
||||
const rotationFactory = SECRET_ROTATION_FACTORY_MAP[type as SecretRotation](
|
||||
{
|
||||
...secretRotation,
|
||||
connection: appConnection
|
||||
} as TSecretRotationV2WithConnection,
|
||||
appConnectionDAL,
|
||||
kmsService
|
||||
);
|
||||
|
||||
const updatedRotation = await rotationFactory.rotateCredentials(inactiveCredentials, async (newCredentials) => {
|
||||
const updatedCredentials = [...generatedCredentials];
|
||||
@ -851,7 +874,7 @@ export const secretRotationV2ServiceFactory = ({
|
||||
|
||||
const encryptedUpdatedCredentials = await encryptSecretRotationCredentials({
|
||||
projectId,
|
||||
generatedCredentials: updatedCredentials,
|
||||
generatedCredentials: updatedCredentials as TSecretRotationV2GeneratedCredentials,
|
||||
kmsService
|
||||
});
|
||||
|
||||
|
@ -1,8 +1,17 @@
|
||||
import { AuditLogInfo } from "@app/ee/services/audit-log/audit-log-types";
|
||||
import { TSqlCredentialsRotationGeneratedCredentials } from "@app/ee/services/secret-rotation-v2/shared/sql-credentials/sql-credentials-rotation-types";
|
||||
import { OrderByDirection } from "@app/lib/types";
|
||||
import { TAppConnectionDALFactory } from "@app/services/app-connection/app-connection-dal";
|
||||
import { TKmsServiceFactory } from "@app/services/kms/kms-service";
|
||||
import { SecretsOrderBy } from "@app/services/secret/secret-types";
|
||||
|
||||
import {
|
||||
TAuth0ClientSecretRotation,
|
||||
TAuth0ClientSecretRotationGeneratedCredentials,
|
||||
TAuth0ClientSecretRotationInput,
|
||||
TAuth0ClientSecretRotationListItem,
|
||||
TAuth0ClientSecretRotationWithConnection
|
||||
} from "./auth0-client-secret";
|
||||
import {
|
||||
TMsSqlCredentialsRotation,
|
||||
TMsSqlCredentialsRotationInput,
|
||||
@ -18,17 +27,26 @@ import {
|
||||
import { TSecretRotationV2DALFactory } from "./secret-rotation-v2-dal";
|
||||
import { SecretRotation } from "./secret-rotation-v2-enums";
|
||||
|
||||
export type TSecretRotationV2 = TPostgresCredentialsRotation | TMsSqlCredentialsRotation;
|
||||
export type TSecretRotationV2 = TPostgresCredentialsRotation | TMsSqlCredentialsRotation | TAuth0ClientSecretRotation;
|
||||
|
||||
export type TSecretRotationV2WithConnection =
|
||||
| TPostgresCredentialsRotationWithConnection
|
||||
| TMsSqlCredentialsRotationWithConnection;
|
||||
| TMsSqlCredentialsRotationWithConnection
|
||||
| TAuth0ClientSecretRotationWithConnection;
|
||||
|
||||
export type TSecretRotationV2GeneratedCredentials = TSqlCredentialsRotationGeneratedCredentials;
|
||||
export type TSecretRotationV2GeneratedCredentials =
|
||||
| TSqlCredentialsRotationGeneratedCredentials
|
||||
| TAuth0ClientSecretRotationGeneratedCredentials;
|
||||
|
||||
export type TSecretRotationV2Input = TPostgresCredentialsRotationInput | TMsSqlCredentialsRotationInput;
|
||||
export type TSecretRotationV2Input =
|
||||
| TPostgresCredentialsRotationInput
|
||||
| TMsSqlCredentialsRotationInput
|
||||
| TAuth0ClientSecretRotationInput;
|
||||
|
||||
export type TSecretRotationV2ListItem = TPostgresCredentialsRotationListItem | TMsSqlCredentialsRotationListItem;
|
||||
export type TSecretRotationV2ListItem =
|
||||
| TPostgresCredentialsRotationListItem
|
||||
| TMsSqlCredentialsRotationListItem
|
||||
| TAuth0ClientSecretRotationListItem;
|
||||
|
||||
export type TSecretRotationV2Raw = NonNullable<Awaited<ReturnType<TSecretRotationV2DALFactory["findById"]>>>;
|
||||
|
||||
@ -129,27 +147,34 @@ export type TSecretRotationSendNotificationJobPayload = {
|
||||
// transactional behavior. By passing in the rotation mutation, if this mutation fails we can roll back the
|
||||
// third party credential changes (when supported), preventing credentials getting out of sync
|
||||
|
||||
export type TRotationFactoryIssueCredentials = (
|
||||
callback: (newCredentials: TSecretRotationV2GeneratedCredentials[number]) => Promise<TSecretRotationV2Raw>
|
||||
export type TRotationFactoryIssueCredentials<T extends TSecretRotationV2GeneratedCredentials> = (
|
||||
callback: (newCredentials: T[number]) => Promise<TSecretRotationV2Raw>
|
||||
) => Promise<TSecretRotationV2Raw>;
|
||||
|
||||
export type TRotationFactoryRevokeCredentials = (
|
||||
generatedCredentials: TSecretRotationV2GeneratedCredentials,
|
||||
export type TRotationFactoryRevokeCredentials<T extends TSecretRotationV2GeneratedCredentials> = (
|
||||
generatedCredentials: T,
|
||||
callback: () => Promise<TSecretRotationV2Raw>
|
||||
) => Promise<TSecretRotationV2Raw>;
|
||||
|
||||
export type TRotationFactoryRotateCredentials = (
|
||||
credentialsToRevoke: TSecretRotationV2GeneratedCredentials[number] | undefined,
|
||||
callback: (newCredentials: TSecretRotationV2GeneratedCredentials[number]) => Promise<TSecretRotationV2Raw>
|
||||
export type TRotationFactoryRotateCredentials<T extends TSecretRotationV2GeneratedCredentials> = (
|
||||
credentialsToRevoke: T[number] | undefined,
|
||||
callback: (newCredentials: T[number]) => Promise<TSecretRotationV2Raw>
|
||||
) => Promise<TSecretRotationV2Raw>;
|
||||
|
||||
export type TRotationFactoryGetSecretsPayload = (
|
||||
generatedCredentials: TSecretRotationV2GeneratedCredentials[number]
|
||||
export type TRotationFactoryGetSecretsPayload<T extends TSecretRotationV2GeneratedCredentials> = (
|
||||
generatedCredentials: T[number]
|
||||
) => { key: string; value: string }[];
|
||||
|
||||
export type TRotationFactory = (secretRotation: TSecretRotationV2WithConnection) => {
|
||||
issueCredentials: TRotationFactoryIssueCredentials;
|
||||
revokeCredentials: TRotationFactoryRevokeCredentials;
|
||||
rotateCredentials: TRotationFactoryRotateCredentials;
|
||||
getSecretsPayload: TRotationFactoryGetSecretsPayload;
|
||||
export type TRotationFactory<
|
||||
T extends TSecretRotationV2WithConnection,
|
||||
C extends TSecretRotationV2GeneratedCredentials
|
||||
> = (
|
||||
secretRotation: T,
|
||||
appConnectionDAL: Pick<TAppConnectionDALFactory, "updateById">,
|
||||
kmsService: Pick<TKmsServiceFactory, "createCipherPairWithDataKey">
|
||||
) => {
|
||||
issueCredentials: TRotationFactoryIssueCredentials<C>;
|
||||
revokeCredentials: TRotationFactoryRevokeCredentials<C>;
|
||||
rotateCredentials: TRotationFactoryRotateCredentials<C>;
|
||||
getSecretsPayload: TRotationFactoryGetSecretsPayload<C>;
|
||||
};
|
||||
|
@ -1,9 +1,11 @@
|
||||
import { z } from "zod";
|
||||
|
||||
import { Auth0ClientSecretRotationSchema } from "@app/ee/services/secret-rotation-v2/auth0-client-secret";
|
||||
import { MsSqlCredentialsRotationSchema } from "@app/ee/services/secret-rotation-v2/mssql-credentials";
|
||||
import { PostgresCredentialsRotationSchema } from "@app/ee/services/secret-rotation-v2/postgres-credentials";
|
||||
|
||||
export const SecretRotationV2Schema = z.discriminatedUnion("type", [
|
||||
PostgresCredentialsRotationSchema,
|
||||
MsSqlCredentialsRotationSchema
|
||||
MsSqlCredentialsRotationSchema,
|
||||
Auth0ClientSecretRotationSchema
|
||||
]);
|
||||
|
@ -1,6 +1,5 @@
|
||||
import { randomInt } from "crypto";
|
||||
|
||||
import {
|
||||
TRotationFactory,
|
||||
TRotationFactoryGetSecretsPayload,
|
||||
TRotationFactoryIssueCredentials,
|
||||
TRotationFactoryRevokeCredentials,
|
||||
@ -8,94 +7,12 @@ import {
|
||||
} from "@app/ee/services/secret-rotation-v2/secret-rotation-v2-types";
|
||||
import { getSqlConnectionClient, SQL_CONNECTION_ALTER_LOGIN_STATEMENT } from "@app/services/app-connection/shared/sql";
|
||||
|
||||
import { generatePassword } from "../utils";
|
||||
import {
|
||||
TSqlCredentialsRotationGeneratedCredentials,
|
||||
TSqlCredentialsRotationWithConnection
|
||||
} from "./sql-credentials-rotation-types";
|
||||
|
||||
const DEFAULT_PASSWORD_REQUIREMENTS = {
|
||||
length: 48,
|
||||
required: {
|
||||
lowercase: 1,
|
||||
uppercase: 1,
|
||||
digits: 1,
|
||||
symbols: 0
|
||||
},
|
||||
allowedSymbols: "-_.~!*"
|
||||
};
|
||||
|
||||
const generatePassword = () => {
|
||||
try {
|
||||
const { length, required, allowedSymbols } = DEFAULT_PASSWORD_REQUIREMENTS;
|
||||
|
||||
const chars = {
|
||||
lowercase: "abcdefghijklmnopqrstuvwxyz",
|
||||
uppercase: "ABCDEFGHIJKLMNOPQRSTUVWXYZ",
|
||||
digits: "0123456789",
|
||||
symbols: allowedSymbols || "-_.~!*"
|
||||
};
|
||||
|
||||
const parts: string[] = [];
|
||||
|
||||
if (required.lowercase > 0) {
|
||||
parts.push(
|
||||
...Array(required.lowercase)
|
||||
.fill(0)
|
||||
.map(() => chars.lowercase[randomInt(chars.lowercase.length)])
|
||||
);
|
||||
}
|
||||
|
||||
if (required.uppercase > 0) {
|
||||
parts.push(
|
||||
...Array(required.uppercase)
|
||||
.fill(0)
|
||||
.map(() => chars.uppercase[randomInt(chars.uppercase.length)])
|
||||
);
|
||||
}
|
||||
|
||||
if (required.digits > 0) {
|
||||
parts.push(
|
||||
...Array(required.digits)
|
||||
.fill(0)
|
||||
.map(() => chars.digits[randomInt(chars.digits.length)])
|
||||
);
|
||||
}
|
||||
|
||||
if (required.symbols > 0) {
|
||||
parts.push(
|
||||
...Array(required.symbols)
|
||||
.fill(0)
|
||||
.map(() => chars.symbols[randomInt(chars.symbols.length)])
|
||||
);
|
||||
}
|
||||
|
||||
const requiredTotal = Object.values(required).reduce<number>((a, b) => a + b, 0);
|
||||
const remainingLength = Math.max(length - requiredTotal, 0);
|
||||
|
||||
const allowedChars = Object.entries(chars)
|
||||
.filter(([key]) => required[key as keyof typeof required] > 0)
|
||||
.map(([, value]) => value)
|
||||
.join("");
|
||||
|
||||
parts.push(
|
||||
...Array(remainingLength)
|
||||
.fill(0)
|
||||
.map(() => allowedChars[randomInt(allowedChars.length)])
|
||||
);
|
||||
|
||||
// shuffle the array to mix up the characters
|
||||
for (let i = parts.length - 1; i > 0; i -= 1) {
|
||||
const j = randomInt(i + 1);
|
||||
[parts[i], parts[j]] = [parts[j], parts[i]];
|
||||
}
|
||||
|
||||
return parts.join("");
|
||||
} catch (error: unknown) {
|
||||
const message = error instanceof Error ? error.message : "Unknown error";
|
||||
throw new Error(`Failed to generate password: ${message}`);
|
||||
}
|
||||
};
|
||||
|
||||
const redactPasswords = (e: unknown, credentials: TSqlCredentialsRotationGeneratedCredentials) => {
|
||||
const error = e as Error;
|
||||
|
||||
@ -110,7 +27,10 @@ const redactPasswords = (e: unknown, credentials: TSqlCredentialsRotationGenerat
|
||||
return redactedMessage;
|
||||
};
|
||||
|
||||
export const sqlCredentialsRotationFactory = (secretRotation: TSqlCredentialsRotationWithConnection) => {
|
||||
export const sqlCredentialsRotationFactory: TRotationFactory<
|
||||
TSqlCredentialsRotationWithConnection,
|
||||
TSqlCredentialsRotationGeneratedCredentials
|
||||
> = (secretRotation) => {
|
||||
const {
|
||||
connection,
|
||||
parameters: { username1, username2 },
|
||||
@ -118,7 +38,7 @@ export const sqlCredentialsRotationFactory = (secretRotation: TSqlCredentialsRot
|
||||
secretsMapping
|
||||
} = secretRotation;
|
||||
|
||||
const validateCredentials = async (credentials: TSqlCredentialsRotationGeneratedCredentials[number]) => {
|
||||
const $validateCredentials = async (credentials: TSqlCredentialsRotationGeneratedCredentials[number]) => {
|
||||
const client = await getSqlConnectionClient({
|
||||
...connection,
|
||||
credentials: {
|
||||
@ -136,7 +56,9 @@ export const sqlCredentialsRotationFactory = (secretRotation: TSqlCredentialsRot
|
||||
}
|
||||
};
|
||||
|
||||
const issueCredentials: TRotationFactoryIssueCredentials = async (callback) => {
|
||||
const issueCredentials: TRotationFactoryIssueCredentials<TSqlCredentialsRotationGeneratedCredentials> = async (
|
||||
callback
|
||||
) => {
|
||||
const client = await getSqlConnectionClient(connection);
|
||||
|
||||
// For SQL, since we get existing users, we change both their passwords
|
||||
@ -159,13 +81,16 @@ export const sqlCredentialsRotationFactory = (secretRotation: TSqlCredentialsRot
|
||||
}
|
||||
|
||||
for await (const credentials of credentialsSet) {
|
||||
await validateCredentials(credentials);
|
||||
await $validateCredentials(credentials);
|
||||
}
|
||||
|
||||
return callback(credentialsSet[0]);
|
||||
};
|
||||
|
||||
const revokeCredentials: TRotationFactoryRevokeCredentials = async (credentialsToRevoke, callback) => {
|
||||
const revokeCredentials: TRotationFactoryRevokeCredentials<TSqlCredentialsRotationGeneratedCredentials> = async (
|
||||
credentialsToRevoke,
|
||||
callback
|
||||
) => {
|
||||
const client = await getSqlConnectionClient(connection);
|
||||
|
||||
const revokedCredentials = credentialsToRevoke.map(({ username }) => ({ username, password: generatePassword() }));
|
||||
@ -186,7 +111,10 @@ export const sqlCredentialsRotationFactory = (secretRotation: TSqlCredentialsRot
|
||||
return callback();
|
||||
};
|
||||
|
||||
const rotateCredentials: TRotationFactoryRotateCredentials = async (_, callback) => {
|
||||
const rotateCredentials: TRotationFactoryRotateCredentials<TSqlCredentialsRotationGeneratedCredentials> = async (
|
||||
_,
|
||||
callback
|
||||
) => {
|
||||
const client = await getSqlConnectionClient(connection);
|
||||
|
||||
// generate new password for the next active user
|
||||
@ -200,12 +128,14 @@ export const sqlCredentialsRotationFactory = (secretRotation: TSqlCredentialsRot
|
||||
await client.destroy();
|
||||
}
|
||||
|
||||
await validateCredentials(credentials);
|
||||
await $validateCredentials(credentials);
|
||||
|
||||
return callback(credentials);
|
||||
};
|
||||
|
||||
const getSecretsPayload: TRotationFactoryGetSecretsPayload = (generatedCredentials) => {
|
||||
const getSecretsPayload: TRotationFactoryGetSecretsPayload<TSqlCredentialsRotationGeneratedCredentials> = (
|
||||
generatedCredentials
|
||||
) => {
|
||||
const { username, password } = secretsMapping;
|
||||
|
||||
const secrets = [
|
||||
@ -226,7 +156,6 @@ export const sqlCredentialsRotationFactory = (secretRotation: TSqlCredentialsRot
|
||||
issueCredentials,
|
||||
revokeCredentials,
|
||||
rotateCredentials,
|
||||
getSecretsPayload,
|
||||
validateCredentials
|
||||
getSecretsPayload
|
||||
};
|
||||
};
|
||||
|
@ -0,0 +1,84 @@
|
||||
import { randomInt } from "crypto";
|
||||
|
||||
const DEFAULT_PASSWORD_REQUIREMENTS = {
|
||||
length: 48,
|
||||
required: {
|
||||
lowercase: 1,
|
||||
uppercase: 1,
|
||||
digits: 1,
|
||||
symbols: 0
|
||||
},
|
||||
allowedSymbols: "-_.~!*"
|
||||
};
|
||||
|
||||
export const generatePassword = () => {
|
||||
try {
|
||||
const { length, required, allowedSymbols } = DEFAULT_PASSWORD_REQUIREMENTS;
|
||||
|
||||
const chars = {
|
||||
lowercase: "abcdefghijklmnopqrstuvwxyz",
|
||||
uppercase: "ABCDEFGHIJKLMNOPQRSTUVWXYZ",
|
||||
digits: "0123456789",
|
||||
symbols: allowedSymbols || "-_.~!*"
|
||||
};
|
||||
|
||||
const parts: string[] = [];
|
||||
|
||||
if (required.lowercase > 0) {
|
||||
parts.push(
|
||||
...Array(required.lowercase)
|
||||
.fill(0)
|
||||
.map(() => chars.lowercase[randomInt(chars.lowercase.length)])
|
||||
);
|
||||
}
|
||||
|
||||
if (required.uppercase > 0) {
|
||||
parts.push(
|
||||
...Array(required.uppercase)
|
||||
.fill(0)
|
||||
.map(() => chars.uppercase[randomInt(chars.uppercase.length)])
|
||||
);
|
||||
}
|
||||
|
||||
if (required.digits > 0) {
|
||||
parts.push(
|
||||
...Array(required.digits)
|
||||
.fill(0)
|
||||
.map(() => chars.digits[randomInt(chars.digits.length)])
|
||||
);
|
||||
}
|
||||
|
||||
if (required.symbols > 0) {
|
||||
parts.push(
|
||||
...Array(required.symbols)
|
||||
.fill(0)
|
||||
.map(() => chars.symbols[randomInt(chars.symbols.length)])
|
||||
);
|
||||
}
|
||||
|
||||
const requiredTotal = Object.values(required).reduce<number>((a, b) => a + b, 0);
|
||||
const remainingLength = Math.max(length - requiredTotal, 0);
|
||||
|
||||
const allowedChars = Object.entries(chars)
|
||||
.filter(([key]) => required[key as keyof typeof required] > 0)
|
||||
.map(([, value]) => value)
|
||||
.join("");
|
||||
|
||||
parts.push(
|
||||
...Array(remainingLength)
|
||||
.fill(0)
|
||||
.map(() => allowedChars[randomInt(allowedChars.length)])
|
||||
);
|
||||
|
||||
// shuffle the array to mix up the characters
|
||||
for (let i = parts.length - 1; i > 0; i -= 1) {
|
||||
const j = randomInt(i + 1);
|
||||
[parts[i], parts[j]] = [parts[j], parts[i]];
|
||||
}
|
||||
|
||||
return parts.join("");
|
||||
} catch (error: unknown) {
|
||||
const message = error instanceof Error ? error.message : "Unknown error";
|
||||
throw new Error(`Failed to generate password: ${message}`);
|
||||
}
|
||||
};
|
@ -127,6 +127,13 @@ export const secretRotationServiceFactory = ({
|
||||
});
|
||||
if (selectedSecrets.length !== Object.values(outputs).length)
|
||||
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 {
|
||||
const selectedSecrets = await secretDAL.find({
|
||||
folderId: folder.id,
|
||||
|
@ -18,7 +18,8 @@ export const rotationTemplates: TSecretRotationProviderTemplate[] = [
|
||||
title: "PostgreSQL",
|
||||
image: "postgres.png",
|
||||
description: "Rotate PostgreSQL/CockroachDB user credentials",
|
||||
template: POSTGRES_TEMPLATE
|
||||
template: POSTGRES_TEMPLATE,
|
||||
isDeprecated: true
|
||||
},
|
||||
{
|
||||
name: "mysql",
|
||||
@ -32,7 +33,8 @@ export const rotationTemplates: TSecretRotationProviderTemplate[] = [
|
||||
title: "Microsoft SQL Server",
|
||||
image: "mssqlserver.png",
|
||||
description: "Rotate Microsoft SQL server user credentials",
|
||||
template: MSSQL_TEMPLATE
|
||||
template: MSSQL_TEMPLATE,
|
||||
isDeprecated: true
|
||||
},
|
||||
{
|
||||
name: "aws-iam",
|
||||
|
@ -50,6 +50,7 @@ export type TSecretRotationProviderTemplate = {
|
||||
image?: string;
|
||||
description?: string;
|
||||
template: THttpProviderTemplate | TDbProviderTemplate | TAwsProviderTemplate;
|
||||
isDeprecated?: boolean;
|
||||
};
|
||||
|
||||
export type THttpProviderTemplate = {
|
||||
|
@ -1672,7 +1672,8 @@ export const KMS = {
|
||||
projectId: "The ID of the project to create the key in.",
|
||||
name: "The name of the key to be created. Must be slug-friendly.",
|
||||
description: "An optional description of the key.",
|
||||
encryptionAlgorithm: "The algorithm to use when performing cryptographic operations with the key."
|
||||
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: {
|
||||
keyId: "The ID of the key to be updated.",
|
||||
@ -1705,6 +1706,28 @@ export const KMS = {
|
||||
DECRYPT: {
|
||||
keyId: "The ID of the key to decrypt the data with.",
|
||||
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."
|
||||
}
|
||||
};
|
||||
|
||||
@ -1759,6 +1782,12 @@ export const AppConnections = {
|
||||
connectionId: `The ID of the ${APP_CONNECTION_NAME_MAP[app]} Connection to be deleted.`
|
||||
}),
|
||||
CREDENTIALS: {
|
||||
AUTH0_CONNECTION: {
|
||||
domain: "The domain of the Auth0 instance to connect to.",
|
||||
clientId: "Your Auth0 application's Client ID.",
|
||||
clientSecret: "Your Auth0 application's Client Secret.",
|
||||
audience: "The unique identifier of the target API you want to access."
|
||||
},
|
||||
SQL_CONNECTION: {
|
||||
host: "The hostname of the database server.",
|
||||
port: "The port number of the database.",
|
||||
@ -1974,12 +2003,19 @@ export const SecretRotations = {
|
||||
"The username of the first login to rotate passwords for. This user must already exists in your database.",
|
||||
username2:
|
||||
"The username of the second login to rotate passwords for. This user must already exists in your database."
|
||||
},
|
||||
AUTH0_CLIENT_SECRET: {
|
||||
clientId: "The client ID of the Auth0 Application to rotate the client secret for."
|
||||
}
|
||||
},
|
||||
SECRETS_MAPPING: {
|
||||
SQL_CREDENTIALS: {
|
||||
username: "The name of the secret that the active username will be mapped to.",
|
||||
password: "The name of the secret that the generated password will be mapped to."
|
||||
},
|
||||
AUTH0_CLIENT_SECRET: {
|
||||
clientId: "The name of the secret that the client ID will be mapped to.",
|
||||
clientSecret: "The name of the secret that the rotated client secret will be mapped to."
|
||||
}
|
||||
}
|
||||
};
|
||||
|
@ -24,5 +24,6 @@ export enum PermissionConditionOperators {
|
||||
$IN = "$in",
|
||||
$EQ = "$eq",
|
||||
$NEQ = "$ne",
|
||||
$GLOB = "$glob"
|
||||
$GLOB = "$glob",
|
||||
$ELEMENTMATCH = "$elemMatch"
|
||||
}
|
||||
|
@ -197,6 +197,7 @@ const envSchema = z
|
||||
/* ----------------------------------------------------------------------------- */
|
||||
|
||||
/* App Connections ----------------------------------------------------------------------------- */
|
||||
ALLOW_INTERNAL_IP_CONNECTIONS: zodStrBool.default("false"),
|
||||
|
||||
// aws
|
||||
INF_APP_CONNECTION_AWS_ACCESS_KEY_ID: zpStr(z.string().optional()),
|
||||
|
@ -1,6 +1,6 @@
|
||||
import crypto from "crypto";
|
||||
|
||||
import { SymmetricEncryption, TSymmetricEncryptionFns } from "./types";
|
||||
import { SymmetricKeyAlgorithm, TSymmetricEncryptionFns } from "./types";
|
||||
|
||||
const getIvLength = () => {
|
||||
return 12;
|
||||
@ -10,7 +10,9 @@ const getTagLength = () => {
|
||||
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 TAG_LENGTH = getTagLength();
|
||||
|
||||
|
@ -1,2 +1,2 @@
|
||||
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_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 = {
|
||||
encrypt: (text: Buffer, key: Buffer) => Buffer;
|
||||
|
2
backend/src/lib/crypto/sign/index.ts
Normal file
2
backend/src/lib/crypto/sign/index.ts
Normal file
@ -0,0 +1,2 @@
|
||||
export { signingService } from "./signing";
|
||||
export { AsymmetricKeyAlgorithm, SigningAlgorithm } from "./types";
|
564
backend/src/lib/crypto/sign/signing.ts
Normal file
564
backend/src/lib/crypto/sign/signing.ts
Normal file
@ -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
|
||||
};
|
||||
};
|
45
backend/src/lib/crypto/sign/types.ts
Normal file
45
backend/src/lib/crypto/sign/types.ts
Normal file
@ -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"
|
||||
}
|
35
backend/src/lib/files/files.ts
Normal file
35
backend/src/lib/files/files.ts
Normal file
@ -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;
|
||||
});
|
||||
};
|
1
backend/src/lib/files/index.ts
Normal file
1
backend/src/lib/files/index.ts
Normal file
@ -0,0 +1 @@
|
||||
export * from "./files";
|
@ -2,10 +2,16 @@ import dns from "node:dns/promises";
|
||||
|
||||
import { isIPv4 } from "net";
|
||||
|
||||
import { getConfig } from "@app/lib/config/env";
|
||||
|
||||
import { BadRequestError } from "../errors";
|
||||
import { isPrivateIp } from "../ip/ipRange";
|
||||
|
||||
export const blockLocalAndPrivateIpAddresses = async (url: string) => {
|
||||
const appCfg = getConfig();
|
||||
|
||||
if (appCfg.isDevelopmentMode) return;
|
||||
|
||||
const validUrl = new URL(url);
|
||||
const inputHostIps: string[] = [];
|
||||
if (isIPv4(validUrl.host)) {
|
||||
@ -18,7 +24,8 @@ export const blockLocalAndPrivateIpAddresses = async (url: string) => {
|
||||
inputHostIps.push(...resolvedIps);
|
||||
}
|
||||
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 = {
|
||||
|
@ -9,6 +9,7 @@ import { runMigrations } from "./auto-start-migrations";
|
||||
import { initAuditLogDbConnection, initDbConnection } from "./db";
|
||||
import { keyStoreFactory } from "./keystore/keystore";
|
||||
import { formatSmtpConfig, initEnvConfig } from "./lib/config/env";
|
||||
import { removeTemporaryBaseDirectory } from "./lib/files";
|
||||
import { initLogger } from "./lib/logger";
|
||||
import { queueServiceFactory } from "./queue";
|
||||
import { main } from "./server/app";
|
||||
@ -21,6 +22,8 @@ const run = async () => {
|
||||
const logger = initLogger();
|
||||
const envConfig = initEnvConfig(logger);
|
||||
|
||||
await removeTemporaryBaseDirectory();
|
||||
|
||||
const db = initDbConnection({
|
||||
dbConnectionUri: envConfig.DB_CONNECTION_URI,
|
||||
dbRootCert: envConfig.DB_ROOT_CERT,
|
||||
@ -71,6 +74,7 @@ const run = async () => {
|
||||
process.on("SIGINT", async () => {
|
||||
await server.close();
|
||||
await db.destroy();
|
||||
await removeTemporaryBaseDirectory();
|
||||
hsmModule.finalize();
|
||||
process.exit(0);
|
||||
});
|
||||
@ -79,6 +83,7 @@ const run = async () => {
|
||||
process.on("SIGTERM", async () => {
|
||||
await server.close();
|
||||
await db.destroy();
|
||||
await removeTemporaryBaseDirectory();
|
||||
hsmModule.finalize();
|
||||
process.exit(0);
|
||||
});
|
||||
|
@ -1391,7 +1391,8 @@ export const registerRoutes = async (
|
||||
permissionService,
|
||||
licenseService,
|
||||
kmsService,
|
||||
projectGatewayDAL
|
||||
projectGatewayDAL,
|
||||
resourceMetadataDAL
|
||||
});
|
||||
|
||||
const dynamicSecretLeaseService = dynamicSecretLeaseServiceFactory({
|
||||
@ -1548,7 +1549,8 @@ export const registerRoutes = async (
|
||||
resourceMetadataDAL,
|
||||
snapshotService,
|
||||
secretQueueService,
|
||||
queueService
|
||||
queueService,
|
||||
appConnectionDAL
|
||||
});
|
||||
|
||||
await secretRotationV2QueueServiceFactory({
|
||||
|
@ -11,6 +11,7 @@ import {
|
||||
UsersSchema
|
||||
} from "@app/db/schemas";
|
||||
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";
|
||||
|
||||
@ -232,7 +233,11 @@ export const SanitizedDynamicSecretSchema = DynamicSecretsSchema.omit({
|
||||
inputIV: true,
|
||||
inputTag: true,
|
||||
algorithm: true
|
||||
});
|
||||
}).merge(
|
||||
z.object({
|
||||
metadata: ResourceMetadataSchema.optional()
|
||||
})
|
||||
);
|
||||
|
||||
export const SanitizedAuditLogStreamSchema = z.object({
|
||||
id: z.string(),
|
||||
|
@ -3,6 +3,7 @@ import { z } from "zod";
|
||||
import { EventType } from "@app/ee/services/audit-log/audit-log-types";
|
||||
import { readLimit } from "@app/server/config/rateLimiter";
|
||||
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
|
||||
import { Auth0ConnectionListItemSchema, SanitizedAuth0ConnectionSchema } from "@app/services/app-connection/auth0";
|
||||
import { AwsConnectionListItemSchema, SanitizedAwsConnectionSchema } from "@app/services/app-connection/aws";
|
||||
import {
|
||||
AzureAppConfigurationConnectionListItemSchema,
|
||||
@ -51,7 +52,8 @@ const SanitizedAppConnectionSchema = z.union([
|
||||
...SanitizedVercelConnectionSchema.options,
|
||||
...SanitizedPostgresConnectionSchema.options,
|
||||
...SanitizedMsSqlConnectionSchema.options,
|
||||
...SanitizedCamundaConnectionSchema.options
|
||||
...SanitizedCamundaConnectionSchema.options,
|
||||
...SanitizedAuth0ConnectionSchema.options
|
||||
]);
|
||||
|
||||
const AppConnectionOptionsSchema = z.discriminatedUnion("app", [
|
||||
@ -66,7 +68,8 @@ const AppConnectionOptionsSchema = z.discriminatedUnion("app", [
|
||||
VercelConnectionListItemSchema,
|
||||
PostgresConnectionListItemSchema,
|
||||
MsSqlConnectionListItemSchema,
|
||||
CamundaConnectionListItemSchema
|
||||
CamundaConnectionListItemSchema,
|
||||
Auth0ConnectionListItemSchema
|
||||
]);
|
||||
|
||||
export const registerAppConnectionRouter = async (server: FastifyZodProvider) => {
|
||||
|
@ -0,0 +1,51 @@
|
||||
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 {
|
||||
CreateAuth0ConnectionSchema,
|
||||
SanitizedAuth0ConnectionSchema,
|
||||
UpdateAuth0ConnectionSchema
|
||||
} from "@app/services/app-connection/auth0";
|
||||
import { AuthMode } from "@app/services/auth/auth-type";
|
||||
|
||||
import { registerAppConnectionEndpoints } from "./app-connection-endpoints";
|
||||
|
||||
export const registerAuth0ConnectionRouter = async (server: FastifyZodProvider) => {
|
||||
registerAppConnectionEndpoints({
|
||||
app: AppConnection.Auth0,
|
||||
server,
|
||||
sanitizedResponseSchema: SanitizedAuth0ConnectionSchema,
|
||||
createSchema: CreateAuth0ConnectionSchema,
|
||||
updateSchema: UpdateAuth0ConnectionSchema
|
||||
});
|
||||
|
||||
// The below endpoints are not exposed and for Infisical App use
|
||||
|
||||
server.route({
|
||||
method: "GET",
|
||||
url: `/:connectionId/clients`,
|
||||
config: {
|
||||
rateLimit: readLimit
|
||||
},
|
||||
schema: {
|
||||
params: z.object({
|
||||
connectionId: z.string().uuid()
|
||||
}),
|
||||
response: {
|
||||
200: z.object({
|
||||
clients: z.object({ name: z.string(), id: z.string() }).array()
|
||||
})
|
||||
}
|
||||
},
|
||||
onRequest: verifyAuth([AuthMode.JWT]),
|
||||
handler: async (req) => {
|
||||
const { connectionId } = req.params;
|
||||
|
||||
const clients = await server.services.appConnection.auth0.listClients(connectionId, req.permission);
|
||||
|
||||
return { clients };
|
||||
}
|
||||
});
|
||||
};
|
@ -1,3 +1,4 @@
|
||||
import { registerAuth0ConnectionRouter } from "@app/server/routes/v1/app-connection-routers/auth0-connection-router";
|
||||
import { AppConnection } from "@app/services/app-connection/app-connection-enums";
|
||||
|
||||
import { registerAwsConnectionRouter } from "./aws-connection-router";
|
||||
@ -28,5 +29,6 @@ export const APP_CONNECTION_REGISTER_ROUTER_MAP: Record<AppConnection, (server:
|
||||
[AppConnection.Vercel]: registerVercelConnectionRouter,
|
||||
[AppConnection.Postgres]: registerPostgresConnectionRouter,
|
||||
[AppConnection.MsSql]: registerMsSqlConnectionRouter,
|
||||
[AppConnection.Camunda]: registerCamundaConnectionRouter
|
||||
[AppConnection.Camunda]: registerCamundaConnectionRouter,
|
||||
[AppConnection.Auth0]: registerAuth0ConnectionRouter
|
||||
};
|
||||
|
@ -4,13 +4,15 @@ import { InternalKmsSchema, KmsKeysSchema } from "@app/db/schemas";
|
||||
import { EventType } from "@app/ee/services/audit-log/audit-log-types";
|
||||
import { KMS } from "@app/lib/api-docs";
|
||||
import { getBase64SizeInBytes, isBase64 } from "@app/lib/base64";
|
||||
import { SymmetricEncryption } from "@app/lib/crypto/cipher";
|
||||
import { AllowedEncryptionKeyAlgorithms, SymmetricKeyAlgorithm } from "@app/lib/crypto/cipher";
|
||||
import { AsymmetricKeyAlgorithm, SigningAlgorithm } from "@app/lib/crypto/sign";
|
||||
import { OrderByDirection } from "@app/lib/types";
|
||||
import { readLimit, writeLimit } from "@app/server/config/rateLimiter";
|
||||
import { slugSchema } from "@app/server/lib/schemas";
|
||||
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
|
||||
import { AuthMode } from "@app/services/auth/auth-type";
|
||||
import { CmekOrderBy } from "@app/services/cmek/cmek-types";
|
||||
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 keyDescriptionSchema = z.string().trim().max(500).optional();
|
||||
@ -45,16 +47,46 @@ export const registerCmekRouter = async (server: FastifyZodProvider) => {
|
||||
},
|
||||
schema: {
|
||||
description: "Create KMS key",
|
||||
body: z.object({
|
||||
projectId: z.string().describe(KMS.CREATE_KEY.projectId),
|
||||
name: keyNameSchema.describe(KMS.CREATE_KEY.name),
|
||||
description: keyDescriptionSchema.describe(KMS.CREATE_KEY.description),
|
||||
encryptionAlgorithm: z
|
||||
.nativeEnum(SymmetricEncryption)
|
||||
.optional()
|
||||
.default(SymmetricEncryption.AES_GCM_256)
|
||||
.describe(KMS.CREATE_KEY.encryptionAlgorithm) // eventually will support others
|
||||
}),
|
||||
body: z
|
||||
.object({
|
||||
projectId: z.string().describe(KMS.CREATE_KEY.projectId),
|
||||
name: keyNameSchema.describe(KMS.CREATE_KEY.name),
|
||||
description: keyDescriptionSchema.describe(KMS.CREATE_KEY.description),
|
||||
keyUsage: z
|
||||
.nativeEnum(KmsKeyUsage)
|
||||
.optional()
|
||||
.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: {
|
||||
200: z.object({
|
||||
key: CmekSchema
|
||||
@ -64,12 +96,19 @@ export const registerCmekRouter = async (server: FastifyZodProvider) => {
|
||||
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
||||
handler: async (req) => {
|
||||
const {
|
||||
body: { projectId, name, description, encryptionAlgorithm },
|
||||
body: { projectId, name, description, encryptionAlgorithm, keyUsage },
|
||||
permission
|
||||
} = req;
|
||||
|
||||
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
|
||||
);
|
||||
|
||||
@ -82,7 +121,7 @@ export const registerCmekRouter = async (server: FastifyZodProvider) => {
|
||||
keyId: cmek.id,
|
||||
name,
|
||||
description,
|
||||
encryptionAlgorithm
|
||||
encryptionAlgorithm: encryptionAlgorithm as TCmekKeyEncryptionAlgorithm
|
||||
}
|
||||
}
|
||||
});
|
||||
@ -126,7 +165,7 @@ export const registerCmekRouter = async (server: FastifyZodProvider) => {
|
||||
|
||||
await server.services.auditLog.createAuditLog({
|
||||
...req.auditLogInfo,
|
||||
orgId: permission.orgId,
|
||||
projectId: cmek.projectId!,
|
||||
event: {
|
||||
type: EventType.UPDATE_CMEK,
|
||||
metadata: {
|
||||
@ -169,7 +208,7 @@ export const registerCmekRouter = async (server: FastifyZodProvider) => {
|
||||
|
||||
await server.services.auditLog.createAuditLog({
|
||||
...req.auditLogInfo,
|
||||
orgId: permission.orgId,
|
||||
projectId: cmek.projectId!,
|
||||
event: {
|
||||
type: EventType.DELETE_CMEK,
|
||||
metadata: {
|
||||
@ -282,7 +321,7 @@ export const registerCmekRouter = async (server: FastifyZodProvider) => {
|
||||
rateLimit: readLimit
|
||||
},
|
||||
schema: {
|
||||
description: "Get KMS key by Name",
|
||||
description: "Get KMS key by name",
|
||||
params: z.object({
|
||||
keyName: slugSchema({ field: "Key name" }).describe(KMS.GET_KEY_BY_NAME.keyName)
|
||||
}),
|
||||
@ -349,11 +388,11 @@ export const registerCmekRouter = async (server: FastifyZodProvider) => {
|
||||
permission
|
||||
} = 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({
|
||||
...req.auditLogInfo,
|
||||
orgId: permission.orgId,
|
||||
projectId,
|
||||
event: {
|
||||
type: EventType.CMEK_ENCRYPT,
|
||||
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({
|
||||
method: "POST",
|
||||
url: "/keys/:keyId/decrypt",
|
||||
@ -394,11 +625,11 @@ export const registerCmekRouter = async (server: FastifyZodProvider) => {
|
||||
permission
|
||||
} = 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({
|
||||
...req.auditLogInfo,
|
||||
orgId: permission.orgId,
|
||||
projectId,
|
||||
event: {
|
||||
type: EventType.CMEK_DECRYPT,
|
||||
metadata: {
|
||||
|
@ -1,13 +1,9 @@
|
||||
import { ForbiddenError, subject } from "@casl/ability";
|
||||
import { ForbiddenError } from "@casl/ability";
|
||||
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 {
|
||||
ProjectPermissionDynamicSecretActions,
|
||||
ProjectPermissionSecretActions,
|
||||
ProjectPermissionSub
|
||||
} from "@app/ee/services/permission/project-permission";
|
||||
import { ProjectPermissionSecretActions } from "@app/ee/services/permission/project-permission";
|
||||
import { SecretRotationV2Schema } from "@app/ee/services/secret-rotation-v2/secret-rotation-v2-union-schema";
|
||||
import { DASHBOARD } from "@app/lib/api-docs";
|
||||
import { BadRequestError } from "@app/lib/errors";
|
||||
@ -142,6 +138,34 @@ export const registerDashboardRouter = async (server: FastifyZodProvider) => {
|
||||
})
|
||||
.array()
|
||||
.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(),
|
||||
totalDynamicSecretCount: z.number().optional(),
|
||||
totalSecretCount: z.number().optional(),
|
||||
@ -289,24 +313,7 @@ export const registerDashboardRouter = async (server: FastifyZodProvider) => {
|
||||
totalCount: totalFolderCount ?? 0
|
||||
};
|
||||
|
||||
const { permission } = await server.services.permission.getProjectPermission({
|
||||
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) {
|
||||
if (includeDynamicSecrets) {
|
||||
// this is the unique count, ie duplicate secrets across envs only count as 1
|
||||
totalDynamicSecretCount = await server.services.dynamicSecret.getCountMultiEnv({
|
||||
actor: req.permission.type,
|
||||
@ -315,7 +322,7 @@ export const registerDashboardRouter = async (server: FastifyZodProvider) => {
|
||||
actorOrgId: req.permission.orgId,
|
||||
projectId,
|
||||
search,
|
||||
environmentSlugs: allowedDynamicSecretEnvironments,
|
||||
environmentSlugs: environments,
|
||||
path: secretPath,
|
||||
isInternal: true
|
||||
});
|
||||
@ -330,7 +337,7 @@ export const registerDashboardRouter = async (server: FastifyZodProvider) => {
|
||||
search,
|
||||
orderBy,
|
||||
orderDirection,
|
||||
environmentSlugs: allowedDynamicSecretEnvironments,
|
||||
environmentSlugs: environments,
|
||||
path: secretPath,
|
||||
limit: remainingLimit,
|
||||
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 {
|
||||
folders,
|
||||
dynamicSecrets,
|
||||
@ -482,6 +511,7 @@ export const registerDashboardRouter = async (server: FastifyZodProvider) => {
|
||||
totalImportCount,
|
||||
totalSecretCount,
|
||||
totalSecretRotationCount,
|
||||
importedByEnvs,
|
||||
totalCount:
|
||||
(totalFolderCount ?? 0) +
|
||||
(totalDynamicSecretCount ?? 0) +
|
||||
@ -575,6 +605,28 @@ export const registerDashboardRouter = async (server: FastifyZodProvider) => {
|
||||
totalFolderCount: z.number().optional(),
|
||||
totalDynamicSecretCount: 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(),
|
||||
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) {
|
||||
const secretCount =
|
||||
(secrets?.length ?? 0) +
|
||||
@ -880,6 +943,7 @@ export const registerDashboardRouter = async (server: FastifyZodProvider) => {
|
||||
totalDynamicSecretCount,
|
||||
totalSecretCount,
|
||||
totalSecretRotationCount,
|
||||
importedBy,
|
||||
totalCount:
|
||||
(totalImportCount ?? 0) +
|
||||
(totalFolderCount ?? 0) +
|
||||
|
@ -39,17 +39,19 @@ export const registerSecretFolderRouter = async (server: FastifyZodProvider) =>
|
||||
.string()
|
||||
.trim()
|
||||
.default("/")
|
||||
.transform(prefixWithSlash)
|
||||
.transform(prefixWithSlash) // Transformations get skipped if path is undefined
|
||||
.transform(removeTrailingSlash)
|
||||
.describe(FOLDERS.CREATE.path),
|
||||
.describe(FOLDERS.CREATE.path)
|
||||
.optional(),
|
||||
// backward compatiability with cli
|
||||
directory: z
|
||||
.string()
|
||||
.trim()
|
||||
.default("/")
|
||||
.transform(prefixWithSlash)
|
||||
.transform(prefixWithSlash) // Transformations get skipped if directory is undefined
|
||||
.transform(removeTrailingSlash)
|
||||
.describe(FOLDERS.CREATE.directory),
|
||||
.describe(FOLDERS.CREATE.directory)
|
||||
.optional(),
|
||||
description: z.string().optional().nullable().describe(FOLDERS.CREATE.description)
|
||||
}),
|
||||
response: {
|
||||
@ -60,7 +62,7 @@ export const registerSecretFolderRouter = async (server: FastifyZodProvider) =>
|
||||
},
|
||||
onRequest: verifyAuth([AuthMode.JWT, AuthMode.API_KEY, AuthMode.SERVICE_TOKEN, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
||||
handler: async (req) => {
|
||||
const path = req.body.path || req.body.directory;
|
||||
const path = req.body.path || req.body.directory || "/";
|
||||
const folder = await server.services.folder.createFolder({
|
||||
actorId: req.permission.id,
|
||||
actor: req.permission.type,
|
||||
@ -120,17 +122,19 @@ export const registerSecretFolderRouter = async (server: FastifyZodProvider) =>
|
||||
.string()
|
||||
.trim()
|
||||
.default("/")
|
||||
.transform(prefixWithSlash)
|
||||
.transform(prefixWithSlash) // Transformations get skipped if path is undefined
|
||||
.transform(removeTrailingSlash)
|
||||
.describe(FOLDERS.UPDATE.path),
|
||||
.describe(FOLDERS.UPDATE.path)
|
||||
.optional(),
|
||||
// backward compatiability with cli
|
||||
directory: z
|
||||
.string()
|
||||
.trim()
|
||||
.default("/")
|
||||
.transform(prefixWithSlash)
|
||||
.transform(prefixWithSlash) // Transformations get skipped if directory is undefined
|
||||
.transform(removeTrailingSlash)
|
||||
.describe(FOLDERS.UPDATE.directory),
|
||||
.describe(FOLDERS.UPDATE.directory)
|
||||
.optional(),
|
||||
description: z.string().optional().nullable().describe(FOLDERS.UPDATE.description)
|
||||
}),
|
||||
response: {
|
||||
@ -141,7 +145,7 @@ export const registerSecretFolderRouter = async (server: FastifyZodProvider) =>
|
||||
},
|
||||
onRequest: verifyAuth([AuthMode.JWT, AuthMode.API_KEY, AuthMode.SERVICE_TOKEN, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
||||
handler: async (req) => {
|
||||
const path = req.body.path || req.body.directory;
|
||||
const path = req.body.path || req.body.directory || "/";
|
||||
const { folder, old } = await server.services.folder.updateFolder({
|
||||
actorId: req.permission.id,
|
||||
actor: req.permission.type,
|
||||
@ -271,17 +275,19 @@ export const registerSecretFolderRouter = async (server: FastifyZodProvider) =>
|
||||
.string()
|
||||
.trim()
|
||||
.default("/")
|
||||
.transform(prefixWithSlash)
|
||||
.transform(prefixWithSlash) // Transformations get skipped if path is undefined
|
||||
.transform(removeTrailingSlash)
|
||||
.describe(FOLDERS.DELETE.path),
|
||||
.describe(FOLDERS.DELETE.path)
|
||||
.optional(),
|
||||
// keep this here as cli need directory
|
||||
directory: z
|
||||
.string()
|
||||
.trim()
|
||||
.default("/")
|
||||
.transform(prefixWithSlash)
|
||||
.transform(prefixWithSlash) // Transformations get skipped if directory is undefined
|
||||
.transform(removeTrailingSlash)
|
||||
.describe(FOLDERS.DELETE.directory)
|
||||
.optional()
|
||||
}),
|
||||
response: {
|
||||
200: z.object({
|
||||
@ -291,7 +297,7 @@ export const registerSecretFolderRouter = async (server: FastifyZodProvider) =>
|
||||
},
|
||||
onRequest: verifyAuth([AuthMode.JWT, AuthMode.API_KEY, AuthMode.SERVICE_TOKEN, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
||||
handler: async (req) => {
|
||||
const path = req.body.path || req.body.directory;
|
||||
const path = req.body.path || req.body.directory || "/";
|
||||
const folder = await server.services.folder.deleteFolder({
|
||||
actorId: req.permission.id,
|
||||
actor: req.permission.type,
|
||||
@ -339,18 +345,18 @@ export const registerSecretFolderRouter = async (server: FastifyZodProvider) =>
|
||||
path: z
|
||||
.string()
|
||||
.trim()
|
||||
.default("/")
|
||||
.transform(prefixWithSlash)
|
||||
.transform(prefixWithSlash) // Transformations get skipped if path is undefined
|
||||
.transform(removeTrailingSlash)
|
||||
.describe(FOLDERS.LIST.path),
|
||||
.describe(FOLDERS.LIST.path)
|
||||
.optional(),
|
||||
// backward compatiability with cli
|
||||
directory: z
|
||||
.string()
|
||||
.trim()
|
||||
.default("/")
|
||||
.transform(prefixWithSlash)
|
||||
.transform(prefixWithSlash) // Transformations get skipped if directory is undefined
|
||||
.transform(removeTrailingSlash)
|
||||
.describe(FOLDERS.LIST.directory),
|
||||
.describe(FOLDERS.LIST.directory)
|
||||
.optional(),
|
||||
recursive: booleanSchema.default(false).describe(FOLDERS.LIST.recursive)
|
||||
}),
|
||||
response: {
|
||||
@ -363,7 +369,7 @@ export const registerSecretFolderRouter = async (server: FastifyZodProvider) =>
|
||||
},
|
||||
onRequest: verifyAuth([AuthMode.JWT, AuthMode.API_KEY, AuthMode.SERVICE_TOKEN, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
||||
handler: async (req) => {
|
||||
const path = req.query.path || req.query.directory;
|
||||
const path = req.query.path || req.query.directory || "/";
|
||||
const folders = await server.services.folder.getFolders({
|
||||
actorId: req.permission.id,
|
||||
actor: req.permission.type,
|
||||
|
@ -108,7 +108,7 @@ export const registerSsoRouter = async (server: FastifyZodProvider) => {
|
||||
const { email } = ghEmails.filter((gitHubEmail) => gitHubEmail.primary)[0];
|
||||
const { isUserCompleted, providerAuthToken } = await server.services.login.oauth2Login({
|
||||
email,
|
||||
firstName: profile.displayName,
|
||||
firstName: profile.displayName || profile.username || "",
|
||||
lastName: "",
|
||||
authMethod: AuthMethod.GITHUB,
|
||||
callbackPort
|
||||
@ -145,7 +145,7 @@ export const registerSsoRouter = async (server: FastifyZodProvider) => {
|
||||
const email = profile.emails[0].value;
|
||||
const { isUserCompleted, providerAuthToken } = await server.services.login.oauth2Login({
|
||||
email,
|
||||
firstName: profile.displayName,
|
||||
firstName: profile.displayName || profile.username || "",
|
||||
lastName: "",
|
||||
authMethod: AuthMethod.GITLAB,
|
||||
callbackPort
|
||||
|
@ -10,7 +10,8 @@ export enum AppConnection {
|
||||
Vercel = "vercel",
|
||||
Postgres = "postgres",
|
||||
MsSql = "mssql",
|
||||
Camunda = "camunda"
|
||||
Camunda = "camunda",
|
||||
Auth0 = "auth0"
|
||||
}
|
||||
|
||||
export enum AWSRegion {
|
||||
|
@ -16,6 +16,7 @@ import {
|
||||
TAppConnectionCredentialsValidator,
|
||||
TAppConnectionTransitionCredentialsToPlatform
|
||||
} from "./app-connection-types";
|
||||
import { Auth0ConnectionMethod, getAuth0ConnectionListItem, validateAuth0ConnectionCredentials } from "./auth0";
|
||||
import { AwsConnectionMethod, getAwsConnectionListItem, validateAwsConnectionCredentials } from "./aws";
|
||||
import {
|
||||
AzureAppConfigurationConnectionMethod,
|
||||
@ -63,7 +64,8 @@ export const listAppConnectionOptions = () => {
|
||||
getVercelConnectionListItem(),
|
||||
getPostgresConnectionListItem(),
|
||||
getMsSqlConnectionListItem(),
|
||||
getCamundaConnectionListItem()
|
||||
getCamundaConnectionListItem(),
|
||||
getAuth0ConnectionListItem()
|
||||
].sort((a, b) => a.name.localeCompare(b.name));
|
||||
};
|
||||
|
||||
@ -109,25 +111,28 @@ export const decryptAppConnectionCredentials = async ({
|
||||
return JSON.parse(decryptedPlainTextBlob.toString()) as TAppConnection["credentials"];
|
||||
};
|
||||
|
||||
const VALIDATE_APP_CONNECTION_CREDENTIALS_MAP: Record<AppConnection, TAppConnectionCredentialsValidator> = {
|
||||
[AppConnection.AWS]: validateAwsConnectionCredentials as TAppConnectionCredentialsValidator,
|
||||
[AppConnection.Databricks]: validateDatabricksConnectionCredentials as TAppConnectionCredentialsValidator,
|
||||
[AppConnection.GitHub]: validateGitHubConnectionCredentials as TAppConnectionCredentialsValidator,
|
||||
[AppConnection.GCP]: validateGcpConnectionCredentials as TAppConnectionCredentialsValidator,
|
||||
[AppConnection.AzureKeyVault]: validateAzureKeyVaultConnectionCredentials as TAppConnectionCredentialsValidator,
|
||||
[AppConnection.AzureAppConfiguration]:
|
||||
validateAzureAppConfigurationConnectionCredentials as TAppConnectionCredentialsValidator,
|
||||
[AppConnection.Humanitec]: validateHumanitecConnectionCredentials as TAppConnectionCredentialsValidator,
|
||||
[AppConnection.Postgres]: validateSqlConnectionCredentials as TAppConnectionCredentialsValidator,
|
||||
[AppConnection.MsSql]: validateSqlConnectionCredentials as TAppConnectionCredentialsValidator,
|
||||
[AppConnection.TerraformCloud]: validateTerraformCloudConnectionCredentials as TAppConnectionCredentialsValidator,
|
||||
[AppConnection.Camunda]: validateCamundaConnectionCredentials as TAppConnectionCredentialsValidator,
|
||||
[AppConnection.Vercel]: validateVercelConnectionCredentials as TAppConnectionCredentialsValidator
|
||||
};
|
||||
|
||||
export const validateAppConnectionCredentials = async (
|
||||
appConnection: TAppConnectionConfig
|
||||
): Promise<TAppConnection["credentials"]> => VALIDATE_APP_CONNECTION_CREDENTIALS_MAP[appConnection.app](appConnection);
|
||||
): Promise<TAppConnection["credentials"]> => {
|
||||
const VALIDATE_APP_CONNECTION_CREDENTIALS_MAP: Record<AppConnection, TAppConnectionCredentialsValidator> = {
|
||||
[AppConnection.AWS]: validateAwsConnectionCredentials as TAppConnectionCredentialsValidator,
|
||||
[AppConnection.Databricks]: validateDatabricksConnectionCredentials as TAppConnectionCredentialsValidator,
|
||||
[AppConnection.GitHub]: validateGitHubConnectionCredentials as TAppConnectionCredentialsValidator,
|
||||
[AppConnection.GCP]: validateGcpConnectionCredentials as TAppConnectionCredentialsValidator,
|
||||
[AppConnection.AzureKeyVault]: validateAzureKeyVaultConnectionCredentials as TAppConnectionCredentialsValidator,
|
||||
[AppConnection.AzureAppConfiguration]:
|
||||
validateAzureAppConfigurationConnectionCredentials as TAppConnectionCredentialsValidator,
|
||||
[AppConnection.Humanitec]: validateHumanitecConnectionCredentials as TAppConnectionCredentialsValidator,
|
||||
[AppConnection.Postgres]: validateSqlConnectionCredentials as TAppConnectionCredentialsValidator,
|
||||
[AppConnection.MsSql]: validateSqlConnectionCredentials as TAppConnectionCredentialsValidator,
|
||||
[AppConnection.Camunda]: validateCamundaConnectionCredentials as TAppConnectionCredentialsValidator,
|
||||
[AppConnection.Vercel]: validateVercelConnectionCredentials as TAppConnectionCredentialsValidator,
|
||||
[AppConnection.TerraformCloud]: validateTerraformCloudConnectionCredentials as TAppConnectionCredentialsValidator,
|
||||
[AppConnection.Auth0]: validateAuth0ConnectionCredentials as TAppConnectionCredentialsValidator
|
||||
};
|
||||
|
||||
return VALIDATE_APP_CONNECTION_CREDENTIALS_MAP[appConnection.app](appConnection);
|
||||
};
|
||||
|
||||
export const getAppConnectionMethodName = (method: TAppConnection["method"]) => {
|
||||
switch (method) {
|
||||
@ -154,6 +159,8 @@ export const getAppConnectionMethodName = (method: TAppConnection["method"]) =>
|
||||
case PostgresConnectionMethod.UsernameAndPassword:
|
||||
case MsSqlConnectionMethod.UsernameAndPassword:
|
||||
return "Username & Password";
|
||||
case Auth0ConnectionMethod.ClientCredentials:
|
||||
return "Client Credentials";
|
||||
default:
|
||||
// eslint-disable-next-line @typescript-eslint/restrict-template-expressions
|
||||
throw new Error(`Unhandled App Connection Method: ${method}`);
|
||||
@ -196,5 +203,6 @@ export const TRANSITION_CONNECTION_CREDENTIALS_TO_PLATFORM: Record<
|
||||
[AppConnection.MsSql]: transferSqlConnectionCredentialsToPlatform as TAppConnectionTransitionCredentialsToPlatform,
|
||||
[AppConnection.TerraformCloud]: platformManagedCredentialsNotSupported,
|
||||
[AppConnection.Camunda]: platformManagedCredentialsNotSupported,
|
||||
[AppConnection.Vercel]: platformManagedCredentialsNotSupported
|
||||
[AppConnection.Vercel]: platformManagedCredentialsNotSupported,
|
||||
[AppConnection.Auth0]: platformManagedCredentialsNotSupported
|
||||
};
|
||||
|
@ -12,5 +12,6 @@ export const APP_CONNECTION_NAME_MAP: Record<AppConnection, string> = {
|
||||
[AppConnection.Vercel]: "Vercel",
|
||||
[AppConnection.Postgres]: "PostgreSQL",
|
||||
[AppConnection.MsSql]: "Microsoft SQL Server",
|
||||
[AppConnection.Camunda]: "Camunda"
|
||||
[AppConnection.Camunda]: "Camunda",
|
||||
[AppConnection.Auth0]: "Auth0"
|
||||
};
|
||||
|
@ -14,6 +14,7 @@ import {
|
||||
TRANSITION_CONNECTION_CREDENTIALS_TO_PLATFORM,
|
||||
validateAppConnectionCredentials
|
||||
} from "@app/services/app-connection/app-connection-fns";
|
||||
import { auth0ConnectionService } from "@app/services/app-connection/auth0/auth0-connection-service";
|
||||
import { TKmsServiceFactory } from "@app/services/kms/kms-service";
|
||||
|
||||
import { TAppConnectionDALFactory } from "./app-connection-dal";
|
||||
@ -27,6 +28,7 @@ import {
|
||||
TUpdateAppConnectionDTO,
|
||||
TValidateAppConnectionCredentialsSchema
|
||||
} from "./app-connection-types";
|
||||
import { ValidateAuth0ConnectionCredentialsSchema } from "./auth0";
|
||||
import { ValidateAwsConnectionCredentialsSchema } from "./aws";
|
||||
import { awsConnectionService } from "./aws/aws-connection-service";
|
||||
import { ValidateAzureAppConfigurationConnectionCredentialsSchema } from "./azure-app-configuration";
|
||||
@ -68,7 +70,8 @@ const VALIDATE_APP_CONNECTION_CREDENTIALS_MAP: Record<AppConnection, TValidateAp
|
||||
[AppConnection.Vercel]: ValidateVercelConnectionCredentialsSchema,
|
||||
[AppConnection.Postgres]: ValidatePostgresConnectionCredentialsSchema,
|
||||
[AppConnection.MsSql]: ValidateMsSqlConnectionCredentialsSchema,
|
||||
[AppConnection.Camunda]: ValidateCamundaConnectionCredentialsSchema
|
||||
[AppConnection.Camunda]: ValidateCamundaConnectionCredentialsSchema,
|
||||
[AppConnection.Auth0]: ValidateAuth0ConnectionCredentialsSchema
|
||||
};
|
||||
|
||||
export const appConnectionServiceFactory = ({
|
||||
@ -442,6 +445,7 @@ export const appConnectionServiceFactory = ({
|
||||
humanitec: humanitecConnectionService(connectAppConnectionById),
|
||||
terraformCloud: terraformCloudConnectionService(connectAppConnectionById),
|
||||
camunda: camundaConnectionService(connectAppConnectionById, appConnectionDAL, kmsService),
|
||||
vercel: vercelConnectionService(connectAppConnectionById)
|
||||
vercel: vercelConnectionService(connectAppConnectionById),
|
||||
auth0: auth0ConnectionService(connectAppConnectionById, appConnectionDAL, kmsService)
|
||||
};
|
||||
};
|
||||
|
@ -3,6 +3,12 @@ import { TSqlConnectionConfig } from "@app/services/app-connection/shared/sql/sq
|
||||
import { SecretSync } from "@app/services/secret-sync/secret-sync-enums";
|
||||
|
||||
import { AWSRegion } from "./app-connection-enums";
|
||||
import {
|
||||
TAuth0Connection,
|
||||
TAuth0ConnectionConfig,
|
||||
TAuth0ConnectionInput,
|
||||
TValidateAuth0ConnectionCredentialsSchema
|
||||
} from "./auth0";
|
||||
import {
|
||||
TAwsConnection,
|
||||
TAwsConnectionConfig,
|
||||
@ -83,6 +89,7 @@ export type TAppConnection = { id: string } & (
|
||||
| TPostgresConnection
|
||||
| TMsSqlConnection
|
||||
| TCamundaConnection
|
||||
| TAuth0Connection
|
||||
);
|
||||
|
||||
export type TAppConnectionRaw = NonNullable<Awaited<ReturnType<TAppConnectionDALFactory["findById"]>>>;
|
||||
@ -102,6 +109,7 @@ export type TAppConnectionInput = { id: string } & (
|
||||
| TPostgresConnectionInput
|
||||
| TMsSqlConnectionInput
|
||||
| TCamundaConnectionInput
|
||||
| TAuth0ConnectionInput
|
||||
);
|
||||
|
||||
export type TSqlConnectionInput = TPostgresConnectionInput | TMsSqlConnectionInput;
|
||||
@ -124,9 +132,10 @@ export type TAppConnectionConfig =
|
||||
| TDatabricksConnectionConfig
|
||||
| THumanitecConnectionConfig
|
||||
| TTerraformCloudConnectionConfig
|
||||
| TVercelConnectionConfig
|
||||
| TSqlConnectionConfig
|
||||
| TCamundaConnectionConfig;
|
||||
| TCamundaConnectionConfig
|
||||
| TVercelConnectionConfig
|
||||
| TAuth0ConnectionConfig;
|
||||
|
||||
export type TValidateAppConnectionCredentialsSchema =
|
||||
| TValidateAwsConnectionCredentialsSchema
|
||||
@ -139,8 +148,9 @@ export type TValidateAppConnectionCredentialsSchema =
|
||||
| TValidatePostgresConnectionCredentialsSchema
|
||||
| TValidateMsSqlConnectionCredentialsSchema
|
||||
| TValidateCamundaConnectionCredentialsSchema
|
||||
| TValidateVercelConnectionCredentialsSchema
|
||||
| TValidateTerraformCloudConnectionCredentialsSchema
|
||||
| TValidateVercelConnectionCredentialsSchema;
|
||||
| TValidateAuth0ConnectionCredentialsSchema;
|
||||
|
||||
export type TListAwsConnectionKmsKeys = {
|
||||
connectionId: string;
|
||||
|
@ -0,0 +1,3 @@
|
||||
export enum Auth0ConnectionMethod {
|
||||
ClientCredentials = "client-credentials"
|
||||
}
|
@ -0,0 +1,97 @@
|
||||
import { request } from "@app/lib/config/request";
|
||||
import { BadRequestError } from "@app/lib/errors";
|
||||
import { removeTrailingSlash } from "@app/lib/fn";
|
||||
import { blockLocalAndPrivateIpAddresses } from "@app/lib/validator";
|
||||
import { TAppConnectionDALFactory } from "@app/services/app-connection/app-connection-dal";
|
||||
import { AppConnection } from "@app/services/app-connection/app-connection-enums";
|
||||
import { encryptAppConnectionCredentials } from "@app/services/app-connection/app-connection-fns";
|
||||
import { TKmsServiceFactory } from "@app/services/kms/kms-service";
|
||||
|
||||
import { Auth0ConnectionMethod } from "./auth0-connection-enums";
|
||||
import { TAuth0AccessTokenResponse, TAuth0Connection, TAuth0ConnectionConfig } from "./auth0-connection-types";
|
||||
|
||||
export const getAuth0ConnectionListItem = () => {
|
||||
return {
|
||||
name: "Auth0" as const,
|
||||
app: AppConnection.Auth0 as const,
|
||||
methods: Object.values(Auth0ConnectionMethod) as [Auth0ConnectionMethod.ClientCredentials]
|
||||
};
|
||||
};
|
||||
|
||||
const authorizeAuth0Connection = async ({
|
||||
clientId,
|
||||
clientSecret,
|
||||
domain,
|
||||
audience
|
||||
}: TAuth0ConnectionConfig["credentials"]) => {
|
||||
const instanceUrl = domain.startsWith("http") ? domain : `https://${domain}`;
|
||||
await blockLocalAndPrivateIpAddresses(instanceUrl);
|
||||
|
||||
const { data } = await request.request<TAuth0AccessTokenResponse>({
|
||||
method: "POST",
|
||||
url: `${removeTrailingSlash(instanceUrl)}/oauth/token`,
|
||||
headers: { "content-type": "application/x-www-form-urlencoded" },
|
||||
data: new URLSearchParams({
|
||||
grant_type: "client_credentials", // this will need to be resolved if we support methods other than client credentials
|
||||
client_id: clientId,
|
||||
client_secret: clientSecret,
|
||||
audience
|
||||
})
|
||||
});
|
||||
|
||||
if (data.token_type !== "Bearer") {
|
||||
throw new Error(`Unhandled token type: ${data.token_type}`);
|
||||
}
|
||||
|
||||
return {
|
||||
accessToken: data.access_token,
|
||||
// cap token lifespan to 10 minutes
|
||||
expiresAt: Math.min(data.expires_in * 1000, 600000) + Date.now()
|
||||
};
|
||||
};
|
||||
|
||||
export const getAuth0ConnectionAccessToken = async (
|
||||
{ id, orgId, credentials }: TAuth0Connection,
|
||||
appConnectionDAL: Pick<TAppConnectionDALFactory, "updateById">,
|
||||
kmsService: Pick<TKmsServiceFactory, "createCipherPairWithDataKey">
|
||||
) => {
|
||||
const { expiresAt, accessToken } = credentials;
|
||||
|
||||
// get new token if expired or less than 5 minutes until expiry
|
||||
if (Date.now() < expiresAt - 300000) {
|
||||
return accessToken;
|
||||
}
|
||||
|
||||
const authData = await authorizeAuth0Connection(credentials);
|
||||
|
||||
const updatedCredentials: TAuth0Connection["credentials"] = {
|
||||
...credentials,
|
||||
...authData
|
||||
};
|
||||
|
||||
const encryptedCredentials = await encryptAppConnectionCredentials({
|
||||
credentials: updatedCredentials,
|
||||
orgId,
|
||||
kmsService
|
||||
});
|
||||
|
||||
await appConnectionDAL.updateById(id, { encryptedCredentials });
|
||||
|
||||
return authData.accessToken;
|
||||
};
|
||||
|
||||
export const validateAuth0ConnectionCredentials = async ({ credentials }: TAuth0ConnectionConfig) => {
|
||||
try {
|
||||
const { accessToken, expiresAt } = await authorizeAuth0Connection(credentials);
|
||||
|
||||
return {
|
||||
...credentials,
|
||||
accessToken,
|
||||
expiresAt
|
||||
};
|
||||
} catch (e: unknown) {
|
||||
throw new BadRequestError({
|
||||
message: (e as Error).message ?? `Unable to validate connection: verify credentials`
|
||||
});
|
||||
}
|
||||
};
|
@ -0,0 +1,94 @@
|
||||
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 { Auth0ConnectionMethod } from "./auth0-connection-enums";
|
||||
|
||||
export const Auth0ConnectionClientCredentialsInputCredentialsSchema = z.object({
|
||||
domain: z.string().trim().min(1, "Domain required").describe(AppConnections.CREDENTIALS.AUTH0_CONNECTION.domain),
|
||||
clientId: z
|
||||
.string()
|
||||
.trim()
|
||||
.min(1, "Client ID required")
|
||||
.describe(AppConnections.CREDENTIALS.AUTH0_CONNECTION.clientId),
|
||||
clientSecret: z
|
||||
.string()
|
||||
.trim()
|
||||
.min(1, "Client Secret required")
|
||||
.describe(AppConnections.CREDENTIALS.AUTH0_CONNECTION.clientSecret),
|
||||
audience: z
|
||||
.string()
|
||||
.trim()
|
||||
.url()
|
||||
.min(1, "Audience required")
|
||||
.describe(AppConnections.CREDENTIALS.AUTH0_CONNECTION.audience)
|
||||
});
|
||||
|
||||
const Auth0ConnectionClientCredentialsOutputCredentialsSchema = z
|
||||
.object({
|
||||
accessToken: z.string(),
|
||||
expiresAt: z.number()
|
||||
})
|
||||
.merge(Auth0ConnectionClientCredentialsInputCredentialsSchema);
|
||||
|
||||
const BaseAuth0ConnectionSchema = BaseAppConnectionSchema.extend({
|
||||
app: z.literal(AppConnection.Auth0)
|
||||
});
|
||||
|
||||
export const Auth0ConnectionSchema = z.intersection(
|
||||
BaseAuth0ConnectionSchema,
|
||||
z.discriminatedUnion("method", [
|
||||
z.object({
|
||||
method: z.literal(Auth0ConnectionMethod.ClientCredentials),
|
||||
credentials: Auth0ConnectionClientCredentialsOutputCredentialsSchema
|
||||
})
|
||||
])
|
||||
);
|
||||
|
||||
export const SanitizedAuth0ConnectionSchema = z.discriminatedUnion("method", [
|
||||
BaseAuth0ConnectionSchema.extend({
|
||||
method: z.literal(Auth0ConnectionMethod.ClientCredentials),
|
||||
credentials: Auth0ConnectionClientCredentialsInputCredentialsSchema.pick({
|
||||
domain: true,
|
||||
clientId: true,
|
||||
audience: true
|
||||
})
|
||||
})
|
||||
]);
|
||||
|
||||
export const ValidateAuth0ConnectionCredentialsSchema = z.discriminatedUnion("method", [
|
||||
z.object({
|
||||
method: z
|
||||
.literal(Auth0ConnectionMethod.ClientCredentials)
|
||||
.describe(AppConnections.CREATE(AppConnection.Auth0).method),
|
||||
credentials: Auth0ConnectionClientCredentialsInputCredentialsSchema.describe(
|
||||
AppConnections.CREATE(AppConnection.Auth0).credentials
|
||||
)
|
||||
})
|
||||
]);
|
||||
|
||||
export const CreateAuth0ConnectionSchema = ValidateAuth0ConnectionCredentialsSchema.and(
|
||||
GenericCreateAppConnectionFieldsSchema(AppConnection.Auth0)
|
||||
);
|
||||
|
||||
export const UpdateAuth0ConnectionSchema = z
|
||||
.object({
|
||||
credentials: Auth0ConnectionClientCredentialsInputCredentialsSchema.optional().describe(
|
||||
AppConnections.UPDATE(AppConnection.Auth0).credentials
|
||||
)
|
||||
})
|
||||
.and(GenericUpdateAppConnectionFieldsSchema(AppConnection.Auth0));
|
||||
|
||||
export const Auth0ConnectionListItemSchema = z.object({
|
||||
name: z.literal("Auth0"),
|
||||
app: z.literal(AppConnection.Auth0),
|
||||
// the below is preferable but currently breaks with our zod to json schema parser
|
||||
// methods: z.tuple([z.literal(AwsConnectionMethod.ServicePrincipal), z.literal(AwsConnectionMethod.AccessKey)]),
|
||||
methods: z.nativeEnum(Auth0ConnectionMethod).array()
|
||||
});
|
@ -0,0 +1,71 @@
|
||||
import { request } from "@app/lib/config/request";
|
||||
import { OrgServiceActor } from "@app/lib/types";
|
||||
import { blockLocalAndPrivateIpAddresses } from "@app/lib/validator";
|
||||
import { TAppConnectionDALFactory } from "@app/services/app-connection/app-connection-dal";
|
||||
import { AppConnection } from "@app/services/app-connection/app-connection-enums";
|
||||
import { getAuth0ConnectionAccessToken } from "@app/services/app-connection/auth0/auth0-connection-fns";
|
||||
import { TKmsServiceFactory } from "@app/services/kms/kms-service";
|
||||
|
||||
import { TAuth0Connection, TAuth0ListClient, TAuth0ListClientsResponse } from "./auth0-connection-types";
|
||||
|
||||
type TGetAppConnectionFunc = (
|
||||
app: AppConnection,
|
||||
connectionId: string,
|
||||
actor: OrgServiceActor
|
||||
) => Promise<TAuth0Connection>;
|
||||
|
||||
const listAuth0Clients = async (
|
||||
appConnection: TAuth0Connection,
|
||||
appConnectionDAL: Pick<TAppConnectionDALFactory, "updateById">,
|
||||
kmsService: Pick<TKmsServiceFactory, "createCipherPairWithDataKey">
|
||||
) => {
|
||||
const accessToken = await getAuth0ConnectionAccessToken(appConnection, appConnectionDAL, kmsService);
|
||||
|
||||
const { audience, clientId: connectionClientId } = appConnection.credentials;
|
||||
await blockLocalAndPrivateIpAddresses(audience);
|
||||
|
||||
const clients: TAuth0ListClient[] = [];
|
||||
let hasMore = true;
|
||||
let page = 0;
|
||||
|
||||
while (hasMore) {
|
||||
// eslint-disable-next-line no-await-in-loop
|
||||
const { data: clientsPage } = await request.get<TAuth0ListClientsResponse>(`${audience}clients`, {
|
||||
headers: {
|
||||
Authorization: `Bearer ${accessToken}`,
|
||||
"Accept-Encoding": "application/json"
|
||||
},
|
||||
params: {
|
||||
include_totals: true,
|
||||
per_page: 100,
|
||||
page
|
||||
}
|
||||
});
|
||||
|
||||
clients.push(...clientsPage.clients);
|
||||
page += 1;
|
||||
hasMore = clientsPage.total > clients.length;
|
||||
}
|
||||
|
||||
return (
|
||||
clients.filter((client) => client.client_id !== connectionClientId && client.name !== "All Applications") ?? []
|
||||
);
|
||||
};
|
||||
|
||||
export const auth0ConnectionService = (
|
||||
getAppConnection: TGetAppConnectionFunc,
|
||||
appConnectionDAL: Pick<TAppConnectionDALFactory, "updateById">,
|
||||
kmsService: Pick<TKmsServiceFactory, "createCipherPairWithDataKey">
|
||||
) => {
|
||||
const listClients = async (connectionId: string, actor: OrgServiceActor) => {
|
||||
const appConnection = await getAppConnection(AppConnection.Auth0, connectionId, actor);
|
||||
|
||||
const clients = await listAuth0Clients(appConnection, appConnectionDAL, kmsService);
|
||||
|
||||
return clients.map((client) => ({ id: client.client_id, name: client.name }));
|
||||
};
|
||||
|
||||
return {
|
||||
listClients
|
||||
};
|
||||
};
|
@ -0,0 +1,39 @@
|
||||
import { z } from "zod";
|
||||
|
||||
import { DiscriminativePick } from "@app/lib/types";
|
||||
import { AppConnection } from "@app/services/app-connection/app-connection-enums";
|
||||
|
||||
import {
|
||||
Auth0ConnectionSchema,
|
||||
CreateAuth0ConnectionSchema,
|
||||
ValidateAuth0ConnectionCredentialsSchema
|
||||
} from "./auth0-connection-schemas";
|
||||
|
||||
export type TAuth0Connection = z.infer<typeof Auth0ConnectionSchema>;
|
||||
|
||||
export type TAuth0ConnectionInput = z.infer<typeof CreateAuth0ConnectionSchema> & {
|
||||
app: AppConnection.Auth0;
|
||||
};
|
||||
|
||||
export type TValidateAuth0ConnectionCredentialsSchema = typeof ValidateAuth0ConnectionCredentialsSchema;
|
||||
|
||||
export type TAuth0ConnectionConfig = DiscriminativePick<TAuth0Connection, "method" | "app" | "credentials"> & {
|
||||
orgId: string;
|
||||
};
|
||||
|
||||
export type TAuth0AccessTokenResponse = {
|
||||
access_token: string;
|
||||
expires_in: number;
|
||||
scope: string;
|
||||
token_type: string;
|
||||
};
|
||||
|
||||
export type TAuth0ListClient = {
|
||||
name: string;
|
||||
client_id: string;
|
||||
};
|
||||
|
||||
export type TAuth0ListClientsResponse = {
|
||||
total: number;
|
||||
clients: TAuth0ListClient[];
|
||||
};
|
4
backend/src/services/app-connection/auth0/index.ts
Normal file
4
backend/src/services/app-connection/auth0/index.ts
Normal file
@ -0,0 +1,4 @@
|
||||
export * from "./auth0-connection-enums";
|
||||
export * from "./auth0-connection-fns";
|
||||
export * from "./auth0-connection-schemas";
|
||||
export * from "./auth0-connection-types";
|
@ -1,6 +1,7 @@
|
||||
import { request } from "@app/lib/config/request";
|
||||
import { BadRequestError } from "@app/lib/errors";
|
||||
import { removeTrailingSlash } from "@app/lib/fn";
|
||||
import { blockLocalAndPrivateIpAddresses } from "@app/lib/validator";
|
||||
import { TAppConnectionDALFactory } from "@app/services/app-connection/app-connection-dal";
|
||||
import { AppConnection } from "@app/services/app-connection/app-connection-enums";
|
||||
import { encryptAppConnectionCredentials } from "@app/services/app-connection/app-connection-fns";
|
||||
@ -26,6 +27,8 @@ const authorizeDatabricksConnection = async ({
|
||||
clientSecret,
|
||||
workspaceUrl
|
||||
}: Pick<TDatabricksConnection["credentials"], "workspaceUrl" | "clientId" | "clientSecret">) => {
|
||||
await blockLocalAndPrivateIpAddresses(workspaceUrl);
|
||||
|
||||
const { data } = await request.post<TAuthorizeDatabricksConnection>(
|
||||
`${removeTrailingSlash(workspaceUrl)}/oidc/v1/token`,
|
||||
"grant_type=client_credentials&scope=all-apis",
|
||||
|
@ -16,7 +16,7 @@ export const DatabricksConnectionServicePrincipalInputCredentialsSchema = z.obje
|
||||
workspaceUrl: z.string().trim().url().min(1, "Workspace URL required")
|
||||
});
|
||||
|
||||
export const DatabricksConnectionServicePrincipalOutputCredentialsSchema = z
|
||||
const DatabricksConnectionServicePrincipalOutputCredentialsSchema = z
|
||||
.object({
|
||||
accessToken: z.string(),
|
||||
expiresAt: z.number()
|
||||
|
@ -3,12 +3,18 @@ import { ForbiddenError } from "@casl/ability";
|
||||
import { ActionProjectType, ProjectType } from "@app/db/schemas";
|
||||
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service";
|
||||
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 { BadRequestError, DatabaseError, NotFoundError } from "@app/lib/errors";
|
||||
import { OrgServiceActor } from "@app/lib/types";
|
||||
import {
|
||||
TCmekDecryptDTO,
|
||||
TCmekEncryptDTO,
|
||||
TCmekGetPublicKeyDTO,
|
||||
TCmekKeyEncryptionAlgorithm,
|
||||
TCmekListSigningAlgorithmsDTO,
|
||||
TCmekSignDTO,
|
||||
TCmekVerifyDTO,
|
||||
TCreateCmekDTO,
|
||||
TListCmeksByProjectIdDTO,
|
||||
TUpdabteCmekByIdDTO
|
||||
@ -16,6 +22,7 @@ import {
|
||||
import { TKmsKeyDALFactory } from "@app/services/kms/kms-key-dal";
|
||||
import { TKmsServiceFactory } from "@app/services/kms/kms-service";
|
||||
|
||||
import { KmsKeyUsage } from "../kms/kms-types";
|
||||
import { TProjectDALFactory } from "../project/project-dal";
|
||||
|
||||
type TCmekServiceFactoryDep = {
|
||||
@ -221,7 +228,151 @@ export const cmekServiceFactory = ({ kmsService, kmsDAL, permissionService, proj
|
||||
|
||||
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) => {
|
||||
@ -248,7 +399,10 @@ export const cmekServiceFactory = ({ kmsService, kmsDAL, permissionService, proj
|
||||
|
||||
const plaintextBlob = await decrypt({ cipherTextBlob: Buffer.from(ciphertext, "base64") });
|
||||
|
||||
return plaintextBlob.toString("base64");
|
||||
return {
|
||||
plaintext: plaintextBlob.toString("base64"),
|
||||
projectId: key.projectId
|
||||
};
|
||||
};
|
||||
|
||||
return {
|
||||
@ -259,6 +413,10 @@ export const cmekServiceFactory = ({ kmsService, kmsDAL, permissionService, proj
|
||||
cmekEncrypt,
|
||||
cmekDecrypt,
|
||||
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 { KmsKeyUsage } from "../kms/kms-types";
|
||||
|
||||
export type TCmekKeyEncryptionAlgorithm = SymmetricKeyAlgorithm | AsymmetricKeyAlgorithm;
|
||||
|
||||
export type TCreateCmekDTO = {
|
||||
orgId: string;
|
||||
projectId: string;
|
||||
name: string;
|
||||
description?: string;
|
||||
encryptionAlgorithm: SymmetricEncryption;
|
||||
encryptionAlgorithm: TCmekKeyEncryptionAlgorithm;
|
||||
keyUsage: KmsKeyUsage;
|
||||
};
|
||||
|
||||
export type TUpdabteCmekByIdDTO = {
|
||||
@ -38,3 +44,26 @@ export type TCmekDecryptDTO = {
|
||||
export enum CmekOrderBy {
|
||||
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;
|
||||
};
|
||||
|
@ -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 getByteLengthForAlgorithm = (encryptionAlgorithm: SymmetricEncryption) => {
|
||||
export const getByteLengthForSymmetricEncryptionAlgorithm = (encryptionAlgorithm: SymmetricKeyAlgorithm) => {
|
||||
switch (encryptionAlgorithm) {
|
||||
case SymmetricEncryption.AES_GCM_128:
|
||||
case SymmetricKeyAlgorithm.AES_GCM_128:
|
||||
return 16;
|
||||
case SymmetricEncryption.AES_GCM_256:
|
||||
case SymmetricKeyAlgorithm.AES_GCM_256:
|
||||
default:
|
||||
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 { TEnvConfig } from "@app/lib/config/env";
|
||||
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 { AsymmetricKeyAlgorithm, signingService } from "@app/lib/crypto/sign";
|
||||
import { BadRequestError, ForbiddenRequestError, NotFoundError } from "@app/lib/errors";
|
||||
import { logger } from "@app/lib/logger";
|
||||
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 { TProjectDALFactory } from "../project/project-dal";
|
||||
@ -29,6 +34,7 @@ import { TKmsKeyDALFactory } from "./kms-key-dal";
|
||||
import { TKmsRootConfigDALFactory } from "./kms-root-config-dal";
|
||||
import {
|
||||
KmsDataKey,
|
||||
KmsKeyUsage,
|
||||
KmsType,
|
||||
RootKeyEncryptionStrategy,
|
||||
TDecryptWithKeyDTO,
|
||||
@ -38,8 +44,11 @@ import {
|
||||
TEncryptWithKmsDTO,
|
||||
TGenerateKMSDTO,
|
||||
TGetKeyMaterialDTO,
|
||||
TGetPublicKeyDTO,
|
||||
TImportKeyMaterialDTO,
|
||||
TUpdateProjectSecretManagerKmsKeyDTO
|
||||
TSignWithKmsDTO,
|
||||
TUpdateProjectSecretManagerKmsKeyDTO,
|
||||
TVerifyWithKmsDTO
|
||||
} from "./kms-types";
|
||||
|
||||
type TKmsServiceFactoryDep = {
|
||||
@ -83,19 +92,42 @@ export const kmsServiceFactory = ({
|
||||
tx,
|
||||
name,
|
||||
projectId,
|
||||
encryptionAlgorithm = SymmetricEncryption.AES_GCM_256,
|
||||
encryptionAlgorithm = SymmetricKeyAlgorithm.AES_GCM_256,
|
||||
keyUsage = KmsKeyUsage.ENCRYPT_DECRYPT,
|
||||
description
|
||||
}: 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 sanitizedName = name ? slugify(name) : slugify(alphaNumericNanoId(8).toLowerCase());
|
||||
const dbQuery = async (db: Knex) => {
|
||||
const kmsDoc = await kmsDAL.create(
|
||||
{
|
||||
name: sanitizedName,
|
||||
keyUsage,
|
||||
orgId,
|
||||
isReserved,
|
||||
projectId,
|
||||
@ -115,6 +147,7 @@ export const kmsServiceFactory = ({
|
||||
);
|
||||
return kmsDoc;
|
||||
};
|
||||
|
||||
if (tx) return dbQuery(tx);
|
||||
const doc = await kmsDAL.transaction(async (tx2) => dbQuery(tx2));
|
||||
return doc;
|
||||
@ -134,7 +167,7 @@ export const kmsServiceFactory = ({
|
||||
*/
|
||||
const encryptWithInputKey = async ({ key }: Omit<TEncryptionWithKeyDTO, "plainText">) => {
|
||||
// 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">) => {
|
||||
const encryptedPlainTextBlob = cipher.encrypt(plainText, key);
|
||||
// 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
|
||||
*/
|
||||
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">) => {
|
||||
const cipherTextBlob = versionedCipherTextBlob.subarray(0, -KMS_VERSION_BLOB_LENGTH);
|
||||
@ -227,7 +260,7 @@ export const kmsServiceFactory = ({
|
||||
};
|
||||
|
||||
const encryptWithRootKey = () => {
|
||||
const cipher = symmetricCipherService(SymmetricEncryption.AES_GCM_256);
|
||||
const cipher = symmetricCipherService(SymmetricKeyAlgorithm.AES_GCM_256);
|
||||
|
||||
return (plainTextBuffer: Buffer) => {
|
||||
const encryptedBuffer = cipher.encrypt(plainTextBuffer, ROOT_ENCRYPTION_KEY);
|
||||
@ -236,7 +269,7 @@ export const kmsServiceFactory = ({
|
||||
};
|
||||
|
||||
const decryptWithRootKey = () => {
|
||||
const cipher = symmetricCipherService(SymmetricEncryption.AES_GCM_256);
|
||||
const cipher = symmetricCipherService(SymmetricKeyAlgorithm.AES_GCM_256);
|
||||
|
||||
return (cipherTextBuffer: Buffer) => {
|
||||
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
|
||||
const keyCipher = symmetricCipherService(SymmetricEncryption.AES_GCM_256);
|
||||
const dataCipher = symmetricCipherService(kmsDoc.internalKms?.encryptionAlgorithm as SymmetricEncryption);
|
||||
const keyCipher = symmetricCipherService(SymmetricKeyAlgorithm.AES_GCM_256);
|
||||
const dataCipher = symmetricCipherService(encryptionAlgorithm);
|
||||
const kmsKey = keyCipher.decrypt(kmsDoc.internalKms?.encryptedKey as Buffer, ROOT_ENCRYPTION_KEY);
|
||||
|
||||
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);
|
||||
|
||||
return kmsKey;
|
||||
};
|
||||
|
||||
const importKeyMaterial = async (
|
||||
{ key, algorithm, name, isReserved, projectId, orgId }: TImportKeyMaterialDTO,
|
||||
{ key, algorithm, name, isReserved, projectId, orgId, keyUsage }: TImportKeyMaterialDTO,
|
||||
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) {
|
||||
throw new BadRequestError({
|
||||
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(
|
||||
{
|
||||
name: sanitizedName,
|
||||
keyUsage: KmsKeyUsage.ENCRYPT_DECRYPT,
|
||||
orgId,
|
||||
isReserved,
|
||||
projectId
|
||||
@ -393,6 +435,74 @@ export const kmsServiceFactory = ({
|
||||
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 kmsDoc = await kmsDAL.findByIdWithAssociatedKms(kmsId, tx);
|
||||
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
|
||||
const keyCipher = symmetricCipherService(SymmetricEncryption.AES_GCM_256);
|
||||
const dataCipher = symmetricCipherService(kmsDoc.internalKms?.encryptionAlgorithm as SymmetricEncryption);
|
||||
const keyCipher = symmetricCipherService(SymmetricKeyAlgorithm.AES_GCM_256);
|
||||
const dataCipher = symmetricCipherService(encryptionAlgorithm);
|
||||
return ({ plainText }: Pick<TEncryptWithKmsDTO, "plainText">) => {
|
||||
const kmsKey = keyCipher.decrypt(kmsDoc.internalKms?.encryptedKey as Buffer, ROOT_ENCRYPTION_KEY);
|
||||
const encryptedPlainTextBlob = dataCipher.encrypt(plainText, kmsKey);
|
||||
@ -729,7 +844,7 @@ export const kmsServiceFactory = ({
|
||||
|
||||
// case 2: root key is encrypted with software encryption
|
||||
if (kmsRootConfig.encryptionStrategy === RootKeyEncryptionStrategy.Software) {
|
||||
const cipher = symmetricCipherService(SymmetricEncryption.AES_GCM_256);
|
||||
const cipher = symmetricCipherService(SymmetricKeyAlgorithm.AES_GCM_256);
|
||||
const encryptionKeyBuffer = $getBasicEncryptionKey();
|
||||
|
||||
return cipher.decrypt(kmsRootConfig.encryptedRootKey, encryptionKeyBuffer);
|
||||
@ -749,7 +864,7 @@ export const kmsServiceFactory = ({
|
||||
}
|
||||
|
||||
if (strategy === RootKeyEncryptionStrategy.Software) {
|
||||
const cipher = symmetricCipherService(SymmetricEncryption.AES_GCM_256);
|
||||
const cipher = symmetricCipherService(SymmetricKeyAlgorithm.AES_GCM_256);
|
||||
const encryptionKeyBuffer = $getBasicEncryptionKey();
|
||||
|
||||
return cipher.encrypt(plainKeyBuffer, encryptionKeyBuffer);
|
||||
@ -765,7 +880,7 @@ export const kmsServiceFactory = ({
|
||||
const createCipherPairWithDataKey = async (encryptionContext: TEncryptWithKmsDataKeyDTO, trx?: Knex) => {
|
||||
const dataKey = await $getDataKey(encryptionContext, trx);
|
||||
|
||||
const cipher = symmetricCipherService(SymmetricEncryption.AES_GCM_256);
|
||||
const cipher = symmetricCipherService(SymmetricKeyAlgorithm.AES_GCM_256);
|
||||
|
||||
return {
|
||||
encryptor: ({ plainText }: Pick<TEncryptWithKmsDTO, "plainText">) => {
|
||||
@ -966,6 +1081,7 @@ export const kmsServiceFactory = ({
|
||||
const decryptedRootKey = await $decryptRootKey(kmsRootConfig);
|
||||
|
||||
logger.info("KMS: Loading ROOT Key into Memory.");
|
||||
|
||||
ROOT_ENCRYPTION_KEY = decryptedRootKey;
|
||||
};
|
||||
|
||||
@ -1014,6 +1130,9 @@ export const kmsServiceFactory = ({
|
||||
getKmsById,
|
||||
createCipherPairWithDataKey,
|
||||
getKeyMaterial,
|
||||
importKeyMaterial
|
||||
importKeyMaterial,
|
||||
signWithKmsKey,
|
||||
verifyWithKmsKey,
|
||||
getPublicKey
|
||||
};
|
||||
};
|
||||
|
@ -1,6 +1,7 @@
|
||||
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 {
|
||||
Organization,
|
||||
@ -13,6 +14,11 @@ export enum KmsType {
|
||||
Internal = "internal"
|
||||
}
|
||||
|
||||
export enum KmsKeyUsage {
|
||||
ENCRYPT_DECRYPT = "encrypt-decrypt",
|
||||
SIGN_VERIFY = "sign-verify"
|
||||
}
|
||||
|
||||
export type TEncryptWithKmsDataKeyDTO =
|
||||
| { type: KmsDataKey.Organization; orgId: string }
|
||||
| { type: KmsDataKey.SecretManager; projectId: string };
|
||||
@ -25,7 +31,8 @@ export type TEncryptWithKmsDataKeyDTO =
|
||||
export type TGenerateKMSDTO = {
|
||||
orgId: string;
|
||||
projectId?: string;
|
||||
encryptionAlgorithm?: SymmetricEncryption;
|
||||
encryptionAlgorithm?: SymmetricKeyAlgorithm | AsymmetricKeyAlgorithm;
|
||||
keyUsage?: KmsKeyUsage;
|
||||
isReserved?: boolean;
|
||||
name?: string;
|
||||
description?: string;
|
||||
@ -37,6 +44,25 @@ export type TEncryptWithKmsDTO = {
|
||||
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 = {
|
||||
key: Buffer;
|
||||
plainText: Buffer;
|
||||
@ -67,9 +93,10 @@ export type TGetKeyMaterialDTO = {
|
||||
|
||||
export type TImportKeyMaterialDTO = {
|
||||
key: Buffer;
|
||||
algorithm: SymmetricEncryption;
|
||||
algorithm: SymmetricKeyAlgorithm;
|
||||
name?: string;
|
||||
isReserved: boolean;
|
||||
projectId: string;
|
||||
orgId: string;
|
||||
keyUsage: KmsKeyUsage;
|
||||
};
|
||||
|
@ -1,11 +1,11 @@
|
||||
import { Knex } from "knex";
|
||||
|
||||
import { ProjectType, TProjectKeys, SortDirection } from "@app/db/schemas";
|
||||
import { ProjectType, SortDirection, TProjectKeys } from "@app/db/schemas";
|
||||
import { TSshCertificateAuthorityDALFactory } from "@app/ee/services/ssh/ssh-certificate-authority-dal";
|
||||
import { TSshCertificateAuthoritySecretDALFactory } from "@app/ee/services/ssh/ssh-certificate-authority-secret-dal";
|
||||
import { OrgServiceActor, TProjectPermission } from "@app/lib/types";
|
||||
import { TKmsServiceFactory } from "@app/services/kms/kms-service";
|
||||
import { TProjectSshConfigDALFactory } from "@app/services/project/project-ssh-config-dal";
|
||||
import { OrgServiceActor, TProjectPermission } from "@app/lib/types";
|
||||
|
||||
import { ActorAuthMethod, ActorType } from "../auth/auth-type";
|
||||
|
||||
|
@ -5,6 +5,8 @@ import { TableName, TSecretImports } from "@app/db/schemas";
|
||||
import { DatabaseError } from "@app/lib/errors";
|
||||
import { ormify } from "@app/lib/knex";
|
||||
|
||||
import { EnvironmentInfo, FolderInfo, FolderResult, SecretResult } from "./secret-import-types";
|
||||
|
||||
export type TSecretImportDALFactory = ReturnType<typeof secretImportDALFactory>;
|
||||
|
||||
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 {
|
||||
...secretImportOrm,
|
||||
find,
|
||||
@ -176,6 +308,7 @@ export const secretImportDALFactory = (db: TDbClient) => {
|
||||
findByFolderIds,
|
||||
findLastImportPosition,
|
||||
updateAllPosition,
|
||||
getProjectImportCount
|
||||
getProjectImportCount,
|
||||
getFolderIsImportedBy
|
||||
};
|
||||
};
|
||||
|
@ -27,6 +27,7 @@ import { decryptSecretRaw } from "../secret/secret-fns";
|
||||
import { TSecretQueueFactory } from "../secret/secret-queue";
|
||||
import { TSecretFolderDALFactory } from "../secret-folder/secret-folder-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 { fnSecretsFromImports, fnSecretsV2FromImports } from "./secret-import-fns";
|
||||
import {
|
||||
@ -798,6 +799,140 @@ export const secretImportServiceFactory = ({
|
||||
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
|
||||
});
|
||||
if (
|
||||
permission.cannot(
|
||||
ProjectPermissionActions.Read,
|
||||
subject(ProjectPermissionSub.SecretImports, { environment, secretPath })
|
||||
)
|
||||
) {
|
||||
return [];
|
||||
}
|
||||
|
||||
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 {
|
||||
createImport,
|
||||
updateImport,
|
||||
@ -810,6 +945,7 @@ export const secretImportServiceFactory = ({
|
||||
getProjectImportCount,
|
||||
fnSecretsFromImports,
|
||||
getProjectImportMultiEnvCount,
|
||||
getImportsMultiEnv
|
||||
getImportsMultiEnv,
|
||||
getFolderIsImportedBy
|
||||
};
|
||||
};
|
||||
|
@ -45,3 +45,29 @@ export type TGetSecretsFromImportDTO = {
|
||||
environment: string;
|
||||
path: string;
|
||||
} & 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[];
|
||||
};
|
||||
|
@ -0,0 +1,4 @@
|
||||
---
|
||||
title: "Available"
|
||||
openapi: "GET /api/v1/app-connections/auth0/available"
|
||||
---
|
@ -0,0 +1,9 @@
|
||||
---
|
||||
title: "Create"
|
||||
openapi: "POST /api/v1/app-connections/auth0"
|
||||
---
|
||||
|
||||
<Note>
|
||||
Check out the configuration docs for [Auth0 Connections](/integrations/app-connections/auth0) to learn how to obtain the
|
||||
required credentials.
|
||||
</Note>
|
@ -0,0 +1,4 @@
|
||||
---
|
||||
title: "Delete"
|
||||
openapi: "DELETE /api/v1/app-connections/auth0/{connectionId}"
|
||||
---
|
@ -0,0 +1,4 @@
|
||||
---
|
||||
title: "Get by ID"
|
||||
openapi: "GET /api/v1/app-connections/auth0/{connectionId}"
|
||||
---
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user