Merge branch 'feature/certificate-template' into feature/est-simpleenroll

This commit is contained in:
Sheen Capadngan
2024-08-19 17:04:22 +08:00
199 changed files with 4510 additions and 989 deletions
+1 -1
View File
@@ -50,6 +50,6 @@ jobs:
CLI_TESTS_ENV_SLUG: ${{ secrets.CLI_TESTS_ENV_SLUG }} CLI_TESTS_ENV_SLUG: ${{ secrets.CLI_TESTS_ENV_SLUG }}
CLI_TESTS_USER_EMAIL: ${{ secrets.CLI_TESTS_USER_EMAIL }} CLI_TESTS_USER_EMAIL: ${{ secrets.CLI_TESTS_USER_EMAIL }}
CLI_TESTS_USER_PASSWORD: ${{ secrets.CLI_TESTS_USER_PASSWORD }} CLI_TESTS_USER_PASSWORD: ${{ secrets.CLI_TESTS_USER_PASSWORD }}
INFISICAL_VAULT_FILE_PASSPHRASE: ${{ secrets.CLI_TESTS_INFISICAL_VAULT_FILE_PASSPHRASE }} # INFISICAL_VAULT_FILE_PASSPHRASE: ${{ secrets.CLI_TESTS_INFISICAL_VAULT_FILE_PASSPHRASE }}
run: go test -v -count=1 ./test run: go test -v -count=1 ./test
+13
View File
@@ -15,3 +15,16 @@ up-prod:
down: down:
docker compose -f docker-compose.dev.yml down docker compose -f docker-compose.dev.yml down
reviewable-ui:
cd frontend && \
npm run lint:fix && \
npm run type:check
reviewable-api:
cd backend && \
npm run lint:fix && \
npm run type:check
reviewable: reviewable-ui reviewable-api
+73 -6
View File
@@ -25,6 +25,7 @@
"@fastify/swagger": "^8.14.0", "@fastify/swagger": "^8.14.0",
"@fastify/swagger-ui": "^2.1.0", "@fastify/swagger-ui": "^2.1.0",
"@node-saml/passport-saml": "^4.0.4", "@node-saml/passport-saml": "^4.0.4",
"@octokit/plugin-retry": "^5.0.5",
"@octokit/rest": "^20.0.2", "@octokit/rest": "^20.0.2",
"@octokit/webhooks-types": "^7.3.1", "@octokit/webhooks-types": "^7.3.1",
"@peculiar/asn1-schema": "^2.3.8", "@peculiar/asn1-schema": "^2.3.8",
@@ -76,6 +77,7 @@
"pkijs": "^3.2.4", "pkijs": "^3.2.4",
"posthog-node": "^3.6.2", "posthog-node": "^3.6.2",
"probot": "^13.0.0", "probot": "^13.0.0",
"safe-regex": "^2.1.1",
"smee-client": "^2.0.0", "smee-client": "^2.0.0",
"tedious": "^18.2.1", "tedious": "^18.2.1",
"tweetnacl": "^1.0.3", "tweetnacl": "^1.0.3",
@@ -107,6 +109,7 @@
"@types/picomatch": "^2.3.3", "@types/picomatch": "^2.3.3",
"@types/prompt-sync": "^4.2.3", "@types/prompt-sync": "^4.2.3",
"@types/resolve": "^1.20.6", "@types/resolve": "^1.20.6",
"@types/safe-regex": "^1.1.6",
"@types/uuid": "^9.0.7", "@types/uuid": "^9.0.7",
"@typescript-eslint/eslint-plugin": "^6.20.0", "@typescript-eslint/eslint-plugin": "^6.20.0",
"@typescript-eslint/parser": "^6.20.0", "@typescript-eslint/parser": "^6.20.0",
@@ -7824,19 +7827,45 @@
} }
}, },
"node_modules/@octokit/plugin-retry": { "node_modules/@octokit/plugin-retry": {
"version": "6.0.1", "version": "5.0.5",
"resolved": "https://registry.npmjs.org/@octokit/plugin-retry/-/plugin-retry-6.0.1.tgz", "resolved": "https://registry.npmjs.org/@octokit/plugin-retry/-/plugin-retry-5.0.5.tgz",
"integrity": "sha512-SKs+Tz9oj0g4p28qkZwl/topGcb0k0qPNX/i7vBKmDsjoeqnVfFUquqrE/O9oJY7+oLzdCtkiWSXLpLjvl6uog==", "integrity": "sha512-sB1RWMhSrre02Atv95K6bhESlJ/sPdZkK/wE/w1IdSCe0yM6FxSjksLa6T7aAvxvxlLKzQEC4KIiqpqyov1Tbg==",
"dependencies": { "dependencies": {
"@octokit/request-error": "^5.0.0", "@octokit/request-error": "^4.0.1",
"@octokit/types": "^12.0.0", "@octokit/types": "^10.0.0",
"bottleneck": "^2.15.3" "bottleneck": "^2.15.3"
}, },
"engines": { "engines": {
"node": ">= 18" "node": ">= 18"
}, },
"peerDependencies": { "peerDependencies": {
"@octokit/core": ">=5" "@octokit/core": ">=3"
}
},
"node_modules/@octokit/plugin-retry/node_modules/@octokit/openapi-types": {
"version": "18.1.1",
"resolved": "https://registry.npmjs.org/@octokit/openapi-types/-/openapi-types-18.1.1.tgz",
"integrity": "sha512-VRaeH8nCDtF5aXWnjPuEMIYf1itK/s3JYyJcWFJT8X9pSNnBtriDf7wlEWsGuhPLl4QIH4xM8fqTXDwJ3Mu6sw=="
},
"node_modules/@octokit/plugin-retry/node_modules/@octokit/request-error": {
"version": "4.0.2",
"resolved": "https://registry.npmjs.org/@octokit/request-error/-/request-error-4.0.2.tgz",
"integrity": "sha512-uqwUEmZw3x4I9DGYq9fODVAAvcLsPQv97NRycP6syEFu5916M189VnNBW2zANNwqg3OiligNcAey7P0SET843w==",
"dependencies": {
"@octokit/types": "^10.0.0",
"deprecation": "^2.0.0",
"once": "^1.4.0"
},
"engines": {
"node": ">= 18"
}
},
"node_modules/@octokit/plugin-retry/node_modules/@octokit/types": {
"version": "10.0.0",
"resolved": "https://registry.npmjs.org/@octokit/types/-/types-10.0.0.tgz",
"integrity": "sha512-Vm8IddVmhCgU1fxC1eyinpwqzXPEYu0NrYzD3YZjlGjyftdLBTeqNblRC0jmJmgxbJIsQlyogVeGnrNaaMVzIg==",
"dependencies": {
"@octokit/openapi-types": "^18.0.0"
} }
}, },
"node_modules/@octokit/plugin-throttling": { "node_modules/@octokit/plugin-throttling": {
@@ -9786,6 +9815,12 @@
"integrity": "sha512-A4STmOXPhMUtHH+S6ymgE2GiBSMqf4oTvcQZMcHzokuTLVYzXTB8ttjcgxOVaAp2lGwEdzZ0J+cRbbeevQj1UQ==", "integrity": "sha512-A4STmOXPhMUtHH+S6ymgE2GiBSMqf4oTvcQZMcHzokuTLVYzXTB8ttjcgxOVaAp2lGwEdzZ0J+cRbbeevQj1UQ==",
"dev": true "dev": true
}, },
"node_modules/@types/safe-regex": {
"version": "1.1.6",
"resolved": "https://registry.npmjs.org/@types/safe-regex/-/safe-regex-1.1.6.tgz",
"integrity": "sha512-CQ/uPB9fLOPKwDsrTeVbNIkwfUthTWOx0l6uIGwVFjZxv7e68pCW5gtTYFzdJi3EBJp8h8zYhJbTasAbX7gEMQ==",
"dev": true
},
"node_modules/@types/semver": { "node_modules/@types/semver": {
"version": "7.5.6", "version": "7.5.6",
"resolved": "https://registry.npmjs.org/@types/semver/-/semver-7.5.6.tgz", "resolved": "https://registry.npmjs.org/@types/semver/-/semver-7.5.6.tgz",
@@ -17432,6 +17467,22 @@
"node": ">=18" "node": ">=18"
} }
}, },
"node_modules/probot/node_modules/@octokit/plugin-retry": {
"version": "6.0.1",
"resolved": "https://registry.npmjs.org/@octokit/plugin-retry/-/plugin-retry-6.0.1.tgz",
"integrity": "sha512-SKs+Tz9oj0g4p28qkZwl/topGcb0k0qPNX/i7vBKmDsjoeqnVfFUquqrE/O9oJY7+oLzdCtkiWSXLpLjvl6uog==",
"dependencies": {
"@octokit/request-error": "^5.0.0",
"@octokit/types": "^12.0.0",
"bottleneck": "^2.15.3"
},
"engines": {
"node": ">= 18"
},
"peerDependencies": {
"@octokit/core": ">=5"
}
},
"node_modules/probot/node_modules/commander": { "node_modules/probot/node_modules/commander": {
"version": "11.1.0", "version": "11.1.0",
"resolved": "https://registry.npmjs.org/commander/-/commander-11.1.0.tgz", "resolved": "https://registry.npmjs.org/commander/-/commander-11.1.0.tgz",
@@ -17814,6 +17865,14 @@
"@babel/runtime": "^7.8.4" "@babel/runtime": "^7.8.4"
} }
}, },
"node_modules/regexp-tree": {
"version": "0.1.27",
"resolved": "https://registry.npmjs.org/regexp-tree/-/regexp-tree-0.1.27.tgz",
"integrity": "sha512-iETxpjK6YoRWJG5o6hXLwvjYAoW+FEZn9os0PD/b6AP6xQwsa/Y7lCVgIixBbUPMfhu+i2LtdeAqVTgGlQarfA==",
"bin": {
"regexp-tree": "bin/regexp-tree"
}
},
"node_modules/regexp.prototype.flags": { "node_modules/regexp.prototype.flags": {
"version": "1.5.1", "version": "1.5.1",
"resolved": "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.5.1.tgz", "resolved": "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.5.1.tgz",
@@ -18130,6 +18189,14 @@
} }
] ]
}, },
"node_modules/safe-regex": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/safe-regex/-/safe-regex-2.1.1.tgz",
"integrity": "sha512-rx+x8AMzKb5Q5lQ95Zoi6ZbJqwCLkqi3XuJXp5P3rT8OEc6sZCJG5AE5dU3lsgRr/F4Bs31jSlVN+j5KrsGu9A==",
"dependencies": {
"regexp-tree": "~0.1.1"
}
},
"node_modules/safe-regex-test": { "node_modules/safe-regex-test": {
"version": "1.0.0", "version": "1.0.0",
"resolved": "https://registry.npmjs.org/safe-regex-test/-/safe-regex-test-1.0.0.tgz", "resolved": "https://registry.npmjs.org/safe-regex-test/-/safe-regex-test-1.0.0.tgz",
+3
View File
@@ -78,6 +78,7 @@
"@types/picomatch": "^2.3.3", "@types/picomatch": "^2.3.3",
"@types/prompt-sync": "^4.2.3", "@types/prompt-sync": "^4.2.3",
"@types/resolve": "^1.20.6", "@types/resolve": "^1.20.6",
"@types/safe-regex": "^1.1.6",
"@types/uuid": "^9.0.7", "@types/uuid": "^9.0.7",
"@typescript-eslint/eslint-plugin": "^6.20.0", "@typescript-eslint/eslint-plugin": "^6.20.0",
"@typescript-eslint/parser": "^6.20.0", "@typescript-eslint/parser": "^6.20.0",
@@ -121,6 +122,7 @@
"@fastify/swagger": "^8.14.0", "@fastify/swagger": "^8.14.0",
"@fastify/swagger-ui": "^2.1.0", "@fastify/swagger-ui": "^2.1.0",
"@node-saml/passport-saml": "^4.0.4", "@node-saml/passport-saml": "^4.0.4",
"@octokit/plugin-retry": "^5.0.5",
"@octokit/rest": "^20.0.2", "@octokit/rest": "^20.0.2",
"@octokit/webhooks-types": "^7.3.1", "@octokit/webhooks-types": "^7.3.1",
"@peculiar/asn1-schema": "^2.3.8", "@peculiar/asn1-schema": "^2.3.8",
@@ -172,6 +174,7 @@
"pkijs": "^3.2.4", "pkijs": "^3.2.4",
"posthog-node": "^3.6.2", "posthog-node": "^3.6.2",
"probot": "^13.0.0", "probot": "^13.0.0",
"safe-regex": "^2.1.1",
"smee-client": "^2.0.0", "smee-client": "^2.0.0",
"tedious": "^18.2.1", "tedious": "^18.2.1",
"tweetnacl": "^1.0.3", "tweetnacl": "^1.0.3",
+20 -1
View File
@@ -7,14 +7,33 @@ const prompt = promptSync({
sigint: true sigint: true
}); });
type ComponentType = 1 | 2 | 3;
console.log(` console.log(`
Component List Component List
-------------- --------------
0. Exit
1. Service component 1. Service component
2. DAL component 2. DAL component
3. Router component 3. Router component
`); `);
const componentType = parseInt(prompt("Select a component: "), 10);
function getComponentType(): ComponentType {
while (true) {
const input = prompt("Select a component (0-3): ");
const componentType = parseInt(input, 10);
if (componentType === 0) {
console.log("Exiting the program. Goodbye!");
process.exit(0);
} else if (componentType === 1 || componentType === 2 || componentType === 3) {
return componentType;
} else {
console.log("Invalid input. Please enter 0, 1, 2, or 3.");
}
}
}
const componentType = getComponentType();
if (componentType === 1) { if (componentType === 1) {
const componentName = prompt("Enter service name: "); const componentName = prompt("Enter service name: ");
+4
View File
@@ -18,6 +18,7 @@ import { TOidcConfigServiceFactory } from "@app/ee/services/oidc/oidc-config-ser
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service"; import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service";
import { TProjectUserAdditionalPrivilegeServiceFactory } from "@app/ee/services/project-user-additional-privilege/project-user-additional-privilege-service"; import { TProjectUserAdditionalPrivilegeServiceFactory } from "@app/ee/services/project-user-additional-privilege/project-user-additional-privilege-service";
import { TRateLimitServiceFactory } from "@app/ee/services/rate-limit/rate-limit-service"; import { TRateLimitServiceFactory } from "@app/ee/services/rate-limit/rate-limit-service";
import { RateLimitConfiguration } from "@app/ee/services/rate-limit/rate-limit-types";
import { TSamlConfigServiceFactory } from "@app/ee/services/saml-config/saml-config-service"; import { TSamlConfigServiceFactory } from "@app/ee/services/saml-config/saml-config-service";
import { TScimServiceFactory } from "@app/ee/services/scim/scim-service"; import { TScimServiceFactory } from "@app/ee/services/scim/scim-service";
import { TSecretApprovalPolicyServiceFactory } from "@app/ee/services/secret-approval-policy/secret-approval-policy-service"; import { TSecretApprovalPolicyServiceFactory } from "@app/ee/services/secret-approval-policy/secret-approval-policy-service";
@@ -35,6 +36,7 @@ import { ActorAuthMethod, ActorType } from "@app/services/auth/auth-type";
import { TAuthTokenServiceFactory } from "@app/services/auth-token/auth-token-service"; import { TAuthTokenServiceFactory } from "@app/services/auth-token/auth-token-service";
import { TCertificateServiceFactory } from "@app/services/certificate/certificate-service"; import { TCertificateServiceFactory } from "@app/services/certificate/certificate-service";
import { TCertificateAuthorityServiceFactory } from "@app/services/certificate-authority/certificate-authority-service"; import { TCertificateAuthorityServiceFactory } from "@app/services/certificate-authority/certificate-authority-service";
import { TCertificateTemplateServiceFactory } from "@app/services/certificate-template/certificate-template-service";
import { TGroupProjectServiceFactory } from "@app/services/group-project/group-project-service"; import { TGroupProjectServiceFactory } from "@app/services/group-project/group-project-service";
import { TIdentityServiceFactory } from "@app/services/identity/identity-service"; import { TIdentityServiceFactory } from "@app/services/identity/identity-service";
import { TIdentityAccessTokenServiceFactory } from "@app/services/identity-access-token/identity-access-token-service"; import { TIdentityAccessTokenServiceFactory } from "@app/services/identity-access-token/identity-access-token-service";
@@ -91,6 +93,7 @@ declare module "fastify" {
id: string; id: string;
orgId: string; orgId: string;
}; };
rateLimits: RateLimitConfiguration;
// passport data // passport data
passportUser: { passportUser: {
isUserCompleted: string; isUserCompleted: string;
@@ -154,6 +157,7 @@ declare module "fastify" {
auditLog: TAuditLogServiceFactory; auditLog: TAuditLogServiceFactory;
auditLogStream: TAuditLogStreamServiceFactory; auditLogStream: TAuditLogStreamServiceFactory;
certificate: TCertificateServiceFactory; certificate: TCertificateServiceFactory;
certificateTemplate: TCertificateTemplateServiceFactory;
certificateAuthority: TCertificateAuthorityServiceFactory; certificateAuthority: TCertificateAuthorityServiceFactory;
certificateAuthorityCrl: TCertificateAuthorityCrlServiceFactory; certificateAuthorityCrl: TCertificateAuthorityCrlServiceFactory;
pkiCollection: TPkiCollectionServiceFactory; pkiCollection: TPkiCollectionServiceFactory;
+8
View File
@@ -53,6 +53,9 @@ import {
TCertificateSecretsUpdate, TCertificateSecretsUpdate,
TCertificatesInsert, TCertificatesInsert,
TCertificatesUpdate, TCertificatesUpdate,
TCertificateTemplates,
TCertificateTemplatesInsert,
TCertificateTemplatesUpdate,
TDynamicSecretLeases, TDynamicSecretLeases,
TDynamicSecretLeasesInsert, TDynamicSecretLeasesInsert,
TDynamicSecretLeasesUpdate, TDynamicSecretLeasesUpdate,
@@ -369,6 +372,11 @@ declare module "knex/types/tables" {
TCertificateAuthorityCrlUpdate TCertificateAuthorityCrlUpdate
>; >;
[TableName.Certificate]: KnexOriginal.CompositeTableType<TCertificates, TCertificatesInsert, TCertificatesUpdate>; [TableName.Certificate]: KnexOriginal.CompositeTableType<TCertificates, TCertificatesInsert, TCertificatesUpdate>;
[TableName.CertificateTemplate]: KnexOriginal.CompositeTableType<
TCertificateTemplates,
TCertificateTemplatesInsert,
TCertificateTemplatesUpdate
>;
[TableName.CertificateBody]: KnexOriginal.CompositeTableType< [TableName.CertificateBody]: KnexOriginal.CompositeTableType<
TCertificateBodies, TCertificateBodies,
TCertificateBodiesInsert, TCertificateBodiesInsert,
@@ -4,16 +4,19 @@ import { TableName } from "../schemas";
export async function up(knex: Knex): Promise<void> { export async function up(knex: Knex): Promise<void> {
if (await knex.schema.hasTable(TableName.CertificateAuthority)) { if (await knex.schema.hasTable(TableName.CertificateAuthority)) {
const hasActiveCaCertVersionColumn = await knex.schema.hasColumn( const hasActiveCaCertIdColumn = await knex.schema.hasColumn(TableName.CertificateAuthority, "activeCaCertId");
TableName.CertificateAuthority, if (!hasActiveCaCertIdColumn) {
"activeCaCertVersion"
);
if (!hasActiveCaCertVersionColumn) {
await knex.schema.alterTable(TableName.CertificateAuthority, (t) => { await knex.schema.alterTable(TableName.CertificateAuthority, (t) => {
t.integer("activeCaCertVersion").nullable(); t.uuid("activeCaCertId").nullable();
t.foreign("activeCaCertId").references("id").inTable(TableName.CertificateAuthorityCert);
}); });
await knex(TableName.CertificateAuthority).where("status", "active").update({ activeCaCertVersion: 1 }); await knex.raw(`
UPDATE "${TableName.CertificateAuthority}" ca
SET "activeCaCertId" = cac.id
FROM "${TableName.CertificateAuthorityCert}" cac
WHERE ca.id = cac."caId"
`);
} }
} }
@@ -22,7 +25,7 @@ export async function up(knex: Knex): Promise<void> {
if (!hasVersionColumn) { if (!hasVersionColumn) {
await knex.schema.alterTable(TableName.CertificateAuthorityCert, (t) => { await knex.schema.alterTable(TableName.CertificateAuthorityCert, (t) => {
t.integer("version").nullable(); t.integer("version").nullable();
// t.dropUnique(["caId"]); t.dropUnique(["caId"]);
}); });
await knex(TableName.CertificateAuthorityCert).update({ version: 1 }).whereNull("version"); await knex(TableName.CertificateAuthorityCert).update({ version: 1 }).whereNull("version");
@@ -54,18 +57,38 @@ export async function up(knex: Knex): Promise<void> {
} }
} }
// if (await knex.schema.hasTable(TableName.CertificateAuthoritySecret)) { if (await knex.schema.hasTable(TableName.CertificateAuthoritySecret)) {
// await knex.schema.alterTable(TableName.CertificateAuthoritySecret, (t) => { await knex.schema.alterTable(TableName.CertificateAuthoritySecret, (t) => {
// t.dropUnique(["caId"]); t.dropUnique(["caId"]);
// }); });
// } }
if (await knex.schema.hasTable(TableName.Certificate)) {
await knex.schema.alterTable(TableName.Certificate, (t) => {
t.uuid("caCertId").nullable();
t.foreign("caCertId").references("id").inTable(TableName.CertificateAuthorityCert);
});
await knex.raw(`
UPDATE "${TableName.Certificate}" cert
SET "caCertId" = (
SELECT caCert.id
FROM "${TableName.CertificateAuthorityCert}" caCert
WHERE caCert."caId" = cert."caId"
)
`);
await knex.schema.alterTable(TableName.Certificate, (t) => {
t.uuid("caCertId").notNullable().alter();
});
}
} }
export async function down(knex: Knex): Promise<void> { export async function down(knex: Knex): Promise<void> {
if (await knex.schema.hasTable(TableName.CertificateAuthority)) { if (await knex.schema.hasTable(TableName.CertificateAuthority)) {
if (await knex.schema.hasColumn(TableName.CertificateAuthority, "activeCaCertVersion")) { if (await knex.schema.hasColumn(TableName.CertificateAuthority, "activeCaCertId")) {
await knex.schema.alterTable(TableName.CertificateAuthority, (t) => { await knex.schema.alterTable(TableName.CertificateAuthority, (t) => {
t.dropColumn("activeCaCertVersion"); t.dropColumn("activeCaCertId");
}); });
} }
} }
@@ -83,4 +106,12 @@ export async function down(knex: Knex): Promise<void> {
}); });
} }
} }
if (await knex.schema.hasTable(TableName.Certificate)) {
if (await knex.schema.hasColumn(TableName.Certificate, "caCertId")) {
await knex.schema.alterTable(TableName.Certificate, (t) => {
t.dropColumn("caCertId");
});
}
}
} }
@@ -0,0 +1,21 @@
import { Knex } from "knex";
import { TableName } from "../schemas";
export async function up(knex: Knex): Promise<void> {
const hasCreationLimitCol = await knex.schema.hasColumn(TableName.RateLimit, "creationLimit");
await knex.schema.alterTable(TableName.RateLimit, (t) => {
if (hasCreationLimitCol) {
t.dropColumn("creationLimit");
}
});
}
export async function down(knex: Knex): Promise<void> {
const hasCreationLimitCol = await knex.schema.hasColumn(TableName.RateLimit, "creationLimit");
await knex.schema.alterTable(TableName.RateLimit, (t) => {
if (!hasCreationLimitCol) {
t.integer("creationLimit").defaultTo(30).notNullable();
}
});
}
@@ -0,0 +1,21 @@
import { Knex } from "knex";
import { TableName } from "../schemas";
export async function up(knex: Knex): Promise<void> {
const hasNameField = await knex.schema.hasColumn(TableName.SecretTag, "name");
if (hasNameField) {
await knex.schema.alterTable(TableName.SecretTag, (t) => {
t.dropColumn("name");
});
}
}
export async function down(knex: Knex): Promise<void> {
const hasNameField = await knex.schema.hasColumn(TableName.SecretTag, "name");
if (!hasNameField) {
await knex.schema.alterTable(TableName.SecretTag, (t) => {
t.string("name");
});
}
}
@@ -11,9 +11,12 @@ export async function up(knex: Knex): Promise<void> {
t.string("projectId").notNullable(); t.string("projectId").notNullable();
t.foreign("projectId").references("id").inTable(TableName.Project).onDelete("CASCADE"); t.foreign("projectId").references("id").inTable(TableName.Project).onDelete("CASCADE");
t.string("name").notNullable(); t.string("name").notNullable();
t.string("description").notNullable();
}); });
} }
await createOnUpdateTrigger(knex, TableName.PkiCollection);
if (!(await knex.schema.hasTable(TableName.PkiCollectionItem))) { if (!(await knex.schema.hasTable(TableName.PkiCollectionItem))) {
await knex.schema.createTable(TableName.PkiCollectionItem, (t) => { await knex.schema.createTable(TableName.PkiCollectionItem, (t) => {
t.uuid("id", { primaryKey: true }).defaultTo(knex.fn.uuid()); t.uuid("id", { primaryKey: true }).defaultTo(knex.fn.uuid());
@@ -27,6 +30,8 @@ export async function up(knex: Knex): Promise<void> {
}); });
} }
await createOnUpdateTrigger(knex, TableName.PkiCollectionItem);
if (!(await knex.schema.hasTable(TableName.PkiAlert))) { if (!(await knex.schema.hasTable(TableName.PkiAlert))) {
await knex.schema.createTable(TableName.PkiAlert, (t) => { await knex.schema.createTable(TableName.PkiAlert, (t) => {
t.uuid("id", { primaryKey: true }).defaultTo(knex.fn.uuid()); t.uuid("id", { primaryKey: true }).defaultTo(knex.fn.uuid());
@@ -42,7 +47,6 @@ export async function up(knex: Knex): Promise<void> {
}); });
} }
await createOnUpdateTrigger(knex, TableName.PkiCollection);
await createOnUpdateTrigger(knex, TableName.PkiAlert); await createOnUpdateTrigger(knex, TableName.PkiAlert);
} }
@@ -0,0 +1,55 @@
import { Knex } from "knex";
import { TableName } from "../schemas";
import { createOnUpdateTrigger, dropOnUpdateTrigger } from "../utils";
export async function up(knex: Knex): Promise<void> {
const hasCertificateTemplateTable = await knex.schema.hasTable(TableName.CertificateTemplate);
if (!hasCertificateTemplateTable) {
await knex.schema.createTable(TableName.CertificateTemplate, (tb) => {
tb.uuid("id", { primaryKey: true }).defaultTo(knex.fn.uuid());
tb.uuid("caId").notNullable();
tb.foreign("caId").references("id").inTable(TableName.CertificateAuthority).onDelete("CASCADE");
tb.uuid("pkiCollectionId");
tb.foreign("pkiCollectionId").references("id").inTable(TableName.PkiCollection).onDelete("SET NULL");
tb.string("name").notNullable();
tb.string("commonName").notNullable();
tb.string("subjectAlternativeName").notNullable();
tb.string("ttl").notNullable();
tb.timestamps(true, true, true);
});
await createOnUpdateTrigger(knex, TableName.CertificateTemplate);
}
const doesCertificateTableHaveTemplateId = await knex.schema.hasColumn(
TableName.Certificate,
"certificateTemplateId"
);
if (!doesCertificateTableHaveTemplateId) {
await knex.schema.alterTable(TableName.Certificate, (tb) => {
tb.uuid("certificateTemplateId");
tb.foreign("certificateTemplateId").references("id").inTable(TableName.CertificateTemplate).onDelete("SET NULL");
});
}
}
export async function down(knex: Knex): Promise<void> {
const doesCertificateTableHaveTemplateId = await knex.schema.hasColumn(
TableName.Certificate,
"certificateTemplateId"
);
if (doesCertificateTableHaveTemplateId) {
await knex.schema.alterTable(TableName.Certificate, (t) => {
t.dropColumn("certificateTemplateId");
});
}
const hasCertificateTemplateTable = await knex.schema.hasTable(TableName.CertificateTemplate);
if (hasCertificateTemplateTable) {
await knex.schema.dropTable(TableName.CertificateTemplate);
await dropOnUpdateTrigger(knex, TableName.CertificateTemplate);
}
}
@@ -28,7 +28,7 @@ export const CertificateAuthoritiesSchema = z.object({
keyAlgorithm: z.string(), keyAlgorithm: z.string(),
notBefore: z.date().nullable().optional(), notBefore: z.date().nullable().optional(),
notAfter: z.date().nullable().optional(), notAfter: z.date().nullable().optional(),
activeCaCertVersion: z.number().nullable().optional() activeCaCertId: z.string().uuid().nullable().optional()
}); });
export type TCertificateAuthorities = z.infer<typeof CertificateAuthoritiesSchema>; export type TCertificateAuthorities = z.infer<typeof CertificateAuthoritiesSchema>;
@@ -0,0 +1,24 @@
// Code generated by automation script, DO NOT EDIT.
// Automated by pulling database and generating zod schema
// To update. Just run npm run generate:schema
// Written by akhilmhdh.
import { z } from "zod";
import { TImmutableDBKeys } from "./models";
export const CertificateTemplatesSchema = z.object({
id: z.string().uuid(),
caId: z.string().uuid(),
pkiCollectionId: z.string().uuid().nullable().optional(),
name: z.string(),
commonName: z.string(),
subjectAlternativeName: z.string(),
ttl: z.string(),
createdAt: z.date(),
updatedAt: z.date()
});
export type TCertificateTemplates = z.infer<typeof CertificateTemplatesSchema>;
export type TCertificateTemplatesInsert = Omit<z.input<typeof CertificateTemplatesSchema>, TImmutableDBKeys>;
export type TCertificateTemplatesUpdate = Partial<Omit<z.input<typeof CertificateTemplatesSchema>, TImmutableDBKeys>>;
+3 -1
View File
@@ -20,7 +20,9 @@ export const CertificatesSchema = z.object({
notAfter: z.date(), notAfter: z.date(),
revokedAt: z.date().nullable().optional(), revokedAt: z.date().nullable().optional(),
revocationReason: z.number().nullable().optional(), revocationReason: z.number().nullable().optional(),
altNames: z.string().default("").nullable().optional() altNames: z.string().default("").nullable().optional(),
caCertId: z.string().uuid(),
certificateTemplateId: z.string().uuid().nullable().optional()
}); });
export type TCertificates = z.infer<typeof CertificatesSchema>; export type TCertificates = z.infer<typeof CertificatesSchema>;
+1
View File
@@ -14,6 +14,7 @@ export * from "./certificate-authority-crl";
export * from "./certificate-authority-secret"; export * from "./certificate-authority-secret";
export * from "./certificate-bodies"; export * from "./certificate-bodies";
export * from "./certificate-secrets"; export * from "./certificate-secrets";
export * from "./certificate-templates";
export * from "./certificates"; export * from "./certificates";
export * from "./dynamic-secret-leases"; export * from "./dynamic-secret-leases";
export * from "./dynamic-secrets"; export * from "./dynamic-secrets";
+1
View File
@@ -10,6 +10,7 @@ export enum TableName {
Certificate = "certificates", Certificate = "certificates",
CertificateBody = "certificate_bodies", CertificateBody = "certificate_bodies",
CertificateSecret = "certificate_secrets", CertificateSecret = "certificate_secrets",
CertificateTemplate = "certificate_templates",
PkiAlert = "pki_alerts", PkiAlert = "pki_alerts",
PkiCollection = "pki_collections", PkiCollection = "pki_collections",
PkiCollectionItem = "pki_collection_items", PkiCollectionItem = "pki_collection_items",
+2 -1
View File
@@ -12,7 +12,8 @@ export const PkiCollectionsSchema = z.object({
createdAt: z.date(), createdAt: z.date(),
updatedAt: z.date(), updatedAt: z.date(),
projectId: z.string(), projectId: z.string(),
name: z.string() name: z.string(),
description: z.string()
}); });
export type TPkiCollections = z.infer<typeof PkiCollectionsSchema>; export type TPkiCollections = z.infer<typeof PkiCollectionsSchema>;
-1
View File
@@ -15,7 +15,6 @@ export const RateLimitSchema = z.object({
authRateLimit: z.number().default(60), authRateLimit: z.number().default(60),
inviteUserRateLimit: z.number().default(30), inviteUserRateLimit: z.number().default(30),
mfaRateLimit: z.number().default(20), mfaRateLimit: z.number().default(20),
creationLimit: z.number().default(30),
publicEndpointLimit: z.number().default(30), publicEndpointLimit: z.number().default(30),
createdAt: z.date(), createdAt: z.date(),
updatedAt: z.date() updatedAt: z.date()
-1
View File
@@ -9,7 +9,6 @@ import { TImmutableDBKeys } from "./models";
export const SecretTagsSchema = z.object({ export const SecretTagsSchema = z.object({
id: z.string().uuid(), id: z.string().uuid(),
name: z.string(),
slug: z.string(), slug: z.string(),
color: z.string().nullable().optional(), color: z.string().nullable().optional(),
createdAt: z.date(), createdAt: z.date(),
@@ -131,7 +131,7 @@ export const registerDynamicSecretLeaseRouter = async (server: FastifyZodProvide
.default("/") .default("/")
.transform(removeTrailingSlash) .transform(removeTrailingSlash)
.describe(DYNAMIC_SECRET_LEASES.RENEW.path), .describe(DYNAMIC_SECRET_LEASES.RENEW.path),
environmentSlug: z.string().min(1).describe(DYNAMIC_SECRET_LEASES.RENEW.ttl) environmentSlug: z.string().min(1).describe(DYNAMIC_SECRET_LEASES.RENEW.environmentSlug)
}), }),
response: { response: {
200: z.object({ 200: z.object({
@@ -58,7 +58,6 @@ export const registerRateLimitRouter = async (server: FastifyZodProvider) => {
authRateLimit: z.number(), authRateLimit: z.number(),
inviteUserRateLimit: z.number(), inviteUserRateLimit: z.number(),
mfaRateLimit: z.number(), mfaRateLimit: z.number(),
creationLimit: z.number(),
publicEndpointLimit: z.number() publicEndpointLimit: z.number()
}), }),
response: { response: {
@@ -75,15 +75,16 @@ export const auditLogDALFactory = (db: TDbClient) => {
.del() .del()
.returning("id"); .returning("id");
numberOfRetryOnFailure = 0; // reset numberOfRetryOnFailure = 0; // reset
// eslint-disable-next-line no-await-in-loop
await new Promise((resolve) => {
setTimeout(resolve, 100); // time to breathe for db
});
} catch (error) { } catch (error) {
numberOfRetryOnFailure += 1; numberOfRetryOnFailure += 1;
logger.error(error, "Failed to delete audit log on pruning"); logger.error(error, "Failed to delete audit log on pruning");
} finally {
// eslint-disable-next-line no-await-in-loop
await new Promise((resolve) => {
setTimeout(resolve, 10); // time to breathe for db
});
} }
} while (deletedAuditLogIds.length > 0 && numberOfRetryOnFailure < MAX_RETRY_ON_FAILURE); } while (deletedAuditLogIds.length > 0 || numberOfRetryOnFailure < MAX_RETRY_ON_FAILURE);
}; };
return { ...auditLogOrm, pruneAuditLog, find }; return { ...auditLogOrm, pruneAuditLog, find };
@@ -1,5 +1,6 @@
import { ForbiddenError } from "@casl/ability"; import { ForbiddenError } from "@casl/ability";
import { getConfig } from "@app/lib/config/env";
import { BadRequestError } from "@app/lib/errors"; import { BadRequestError } from "@app/lib/errors";
import { TPermissionServiceFactory } from "../permission/permission-service"; import { TPermissionServiceFactory } from "../permission/permission-service";
@@ -61,6 +62,10 @@ export const auditLogServiceFactory = ({
}; };
const createAuditLog = async (data: TCreateAuditLogDTO) => { const createAuditLog = async (data: TCreateAuditLogDTO) => {
const appCfg = getConfig();
if (appCfg.DISABLE_AUDIT_LOG_GENERATION) {
return;
}
// add all cases in which project id or org id cannot be added // add all cases in which project id or org id cannot be added
if (data.event.type !== EventType.LOGIN_IDENTITY_UNIVERSAL_AUTH) { if (data.event.type !== EventType.LOGIN_IDENTITY_UNIVERSAL_AUTH) {
if (!data.projectId && !data.orgId) throw new BadRequestError({ message: "Must either project id or org id" }); if (!data.projectId && !data.orgId) throw new BadRequestError({ message: "Must either project id or org id" });
@@ -162,7 +162,11 @@ export enum EventType {
UPDATE_PROJECT_KMS = "update-project-kms", UPDATE_PROJECT_KMS = "update-project-kms",
GET_PROJECT_KMS_BACKUP = "get-project-kms-backup", GET_PROJECT_KMS_BACKUP = "get-project-kms-backup",
LOAD_PROJECT_KMS_BACKUP = "load-project-kms-backup", LOAD_PROJECT_KMS_BACKUP = "load-project-kms-backup",
ORG_ADMIN_ACCESS_PROJECT = "org-admin-accessed-project" ORG_ADMIN_ACCESS_PROJECT = "org-admin-accessed-project",
CREATE_CERTIFICATE_TEMPLATE = "create-certificate-template",
UPDATE_CERTIFICATE_TEMPLATE = "update-certificate-template",
DELETE_CERTIFICATE_TEMPLATE = "delete-certificate-template",
GET_CERTIFICATE_TEMPLATE = "get-certificate-template"
} }
interface UserActorMetadata { interface UserActorMetadata {
@@ -352,6 +356,7 @@ interface DeleteIntegrationEvent {
targetServiceId?: string; targetServiceId?: string;
path?: string; path?: string;
region?: string; region?: string;
shouldDeleteIntegrationSecrets?: boolean;
}; };
} }
@@ -1365,6 +1370,46 @@ interface LoadProjectKmsBackupEvent {
metadata: Record<string, string>; // no metadata yet metadata: Record<string, string>; // no metadata yet
} }
interface CreateCertificateTemplate {
type: EventType.CREATE_CERTIFICATE_TEMPLATE;
metadata: {
certificateTemplateId: string;
caId: string;
pkiCollectionId?: string;
name: string;
commonName: string;
subjectAlternativeName: string;
ttl: string;
};
}
interface GetCertificateTemplate {
type: EventType.GET_CERTIFICATE_TEMPLATE;
metadata: {
certificateTemplateId: string;
};
}
interface UpdateCertificateTemplate {
type: EventType.UPDATE_CERTIFICATE_TEMPLATE;
metadata: {
certificateTemplateId: string;
caId: string;
pkiCollectionId?: string;
name: string;
commonName: string;
subjectAlternativeName: string;
ttl: string;
};
}
interface DeleteCertificateTemplate {
type: EventType.DELETE_CERTIFICATE_TEMPLATE;
metadata: {
certificateTemplateId: string;
};
}
interface OrgAdminAccessProjectEvent { interface OrgAdminAccessProjectEvent {
type: EventType.ORG_ADMIN_ACCESS_PROJECT; type: EventType.ORG_ADMIN_ACCESS_PROJECT;
metadata: { metadata: {
@@ -1498,4 +1543,8 @@ export type Event =
| UpdateProjectKmsEvent | UpdateProjectKmsEvent
| GetProjectKmsBackupEvent | GetProjectKmsBackupEvent
| LoadProjectKmsBackupEvent | LoadProjectKmsBackupEvent
| OrgAdminAccessProjectEvent; | OrgAdminAccessProjectEvent
| CreateCertificateTemplate
| UpdateCertificateTemplate
| GetCertificateTemplate
| DeleteCertificateTemplate;
@@ -40,7 +40,12 @@ export const getDefaultOnPremFeatures = (): TFeatureSet => ({
secretRotation: true, secretRotation: true,
caCrl: false, caCrl: false,
instanceUserManagement: false, instanceUserManagement: false,
externalKms: false externalKms: false,
rateLimits: {
readLimit: 60,
writeLimit: 200,
secretsLimit: 40
}
}); });
export const setupLicenceRequestWithStore = (baseURL: string, refreshUrl: string, licenseKey: string) => { export const setupLicenceRequestWithStore = (baseURL: string, refreshUrl: string, licenseKey: string) => {
@@ -58,6 +58,11 @@ export type TFeatureSet = {
caCrl: false; caCrl: false;
instanceUserManagement: false; instanceUserManagement: false;
externalKms: false; externalKms: false;
rateLimits: {
readLimit: number;
writeLimit: number;
secretsLimit: number;
};
}; };
export type TOrgPlansTableDTO = { export type TOrgPlansTableDTO = {
@@ -30,6 +30,7 @@ export enum ProjectPermissionSub {
Identity = "identity", Identity = "identity",
CertificateAuthorities = "certificate-authorities", CertificateAuthorities = "certificate-authorities",
Certificates = "certificates", Certificates = "certificates",
CertificateTemplates = "certificate-templates",
PkiAlerts = "pki-alerts", PkiAlerts = "pki-alerts",
PkiCollections = "pki-collections", PkiCollections = "pki-collections",
Kms = "kms" Kms = "kms"
@@ -65,6 +66,7 @@ export type ProjectPermissionSet =
| [ProjectPermissionActions, ProjectPermissionSub.Identity] | [ProjectPermissionActions, ProjectPermissionSub.Identity]
| [ProjectPermissionActions, ProjectPermissionSub.CertificateAuthorities] | [ProjectPermissionActions, ProjectPermissionSub.CertificateAuthorities]
| [ProjectPermissionActions, ProjectPermissionSub.Certificates] | [ProjectPermissionActions, ProjectPermissionSub.Certificates]
| [ProjectPermissionActions, ProjectPermissionSub.CertificateTemplates]
| [ProjectPermissionActions, ProjectPermissionSub.PkiAlerts] | [ProjectPermissionActions, ProjectPermissionSub.PkiAlerts]
| [ProjectPermissionActions, ProjectPermissionSub.PkiCollections] | [ProjectPermissionActions, ProjectPermissionSub.PkiCollections]
| [ProjectPermissionActions.Delete, ProjectPermissionSub.Project] | [ProjectPermissionActions.Delete, ProjectPermissionSub.Project]
@@ -165,6 +167,11 @@ const buildAdminPermissionRules = () => {
can(ProjectPermissionActions.Edit, ProjectPermissionSub.Certificates); can(ProjectPermissionActions.Edit, ProjectPermissionSub.Certificates);
can(ProjectPermissionActions.Delete, ProjectPermissionSub.Certificates); can(ProjectPermissionActions.Delete, ProjectPermissionSub.Certificates);
can(ProjectPermissionActions.Read, ProjectPermissionSub.CertificateTemplates);
can(ProjectPermissionActions.Create, ProjectPermissionSub.CertificateTemplates);
can(ProjectPermissionActions.Edit, ProjectPermissionSub.CertificateTemplates);
can(ProjectPermissionActions.Delete, ProjectPermissionSub.CertificateTemplates);
can(ProjectPermissionActions.Read, ProjectPermissionSub.PkiAlerts); can(ProjectPermissionActions.Read, ProjectPermissionSub.PkiAlerts);
can(ProjectPermissionActions.Create, ProjectPermissionSub.PkiAlerts); can(ProjectPermissionActions.Create, ProjectPermissionSub.PkiAlerts);
can(ProjectPermissionActions.Edit, ProjectPermissionSub.PkiAlerts); can(ProjectPermissionActions.Edit, ProjectPermissionSub.PkiAlerts);
@@ -251,6 +258,8 @@ const buildMemberPermissionRules = () => {
can(ProjectPermissionActions.Edit, ProjectPermissionSub.Certificates); can(ProjectPermissionActions.Edit, ProjectPermissionSub.Certificates);
can(ProjectPermissionActions.Delete, ProjectPermissionSub.Certificates); can(ProjectPermissionActions.Delete, ProjectPermissionSub.Certificates);
can(ProjectPermissionActions.Read, ProjectPermissionSub.CertificateTemplates);
can(ProjectPermissionActions.Read, ProjectPermissionSub.PkiAlerts); can(ProjectPermissionActions.Read, ProjectPermissionSub.PkiAlerts);
can(ProjectPermissionActions.Read, ProjectPermissionSub.PkiCollections); can(ProjectPermissionActions.Read, ProjectPermissionSub.PkiCollections);
@@ -4,17 +4,16 @@ import { logger } from "@app/lib/logger";
import { TLicenseServiceFactory } from "../license/license-service"; import { TLicenseServiceFactory } from "../license/license-service";
import { TRateLimitDALFactory } from "./rate-limit-dal"; import { TRateLimitDALFactory } from "./rate-limit-dal";
import { TRateLimit, TRateLimitUpdateDTO } from "./rate-limit-types"; import { RateLimitConfiguration, TRateLimit, TRateLimitUpdateDTO } from "./rate-limit-types";
let rateLimitMaxConfiguration = { let rateLimitMaxConfiguration: RateLimitConfiguration = {
readLimit: 60, readLimit: 60,
publicEndpointLimit: 30, publicEndpointLimit: 30,
writeLimit: 200, writeLimit: 200,
secretsLimit: 60, secretsLimit: 60,
authRateLimit: 60, authRateLimit: 60,
inviteUserRateLimit: 30, inviteUserRateLimit: 30,
mfaRateLimit: 20, mfaRateLimit: 20
creationLimit: 30
}; };
Object.freeze(rateLimitMaxConfiguration); Object.freeze(rateLimitMaxConfiguration);
@@ -67,8 +66,7 @@ export const rateLimitServiceFactory = ({ rateLimitDAL, licenseService }: TRateL
secretsLimit: rateLimit.secretsRateLimit, secretsLimit: rateLimit.secretsRateLimit,
authRateLimit: rateLimit.authRateLimit, authRateLimit: rateLimit.authRateLimit,
inviteUserRateLimit: rateLimit.inviteUserRateLimit, inviteUserRateLimit: rateLimit.inviteUserRateLimit,
mfaRateLimit: rateLimit.mfaRateLimit, mfaRateLimit: rateLimit.mfaRateLimit
creationLimit: rateLimit.creationLimit
}; };
logger.info(`syncRateLimitConfiguration: rate limit configuration: %o`, newRateLimitMaxConfiguration); logger.info(`syncRateLimitConfiguration: rate limit configuration: %o`, newRateLimitMaxConfiguration);
@@ -5,7 +5,6 @@ export type TRateLimitUpdateDTO = {
authRateLimit: number; authRateLimit: number;
inviteUserRateLimit: number; inviteUserRateLimit: number;
mfaRateLimit: number; mfaRateLimit: number;
creationLimit: number;
publicEndpointLimit: number; publicEndpointLimit: number;
}; };
@@ -14,3 +13,13 @@ export type TRateLimit = {
createdAt: Date; createdAt: Date;
updatedAt: Date; updatedAt: Date;
} & TRateLimitUpdateDTO; } & TRateLimitUpdateDTO;
export type RateLimitConfiguration = {
readLimit: number;
publicEndpointLimit: number;
writeLimit: number;
secretsLimit: number;
authRateLimit: number;
inviteUserRateLimit: number;
mfaRateLimit: number;
};
@@ -8,6 +8,7 @@ import { removeTrailingSlash } from "@app/lib/fn";
import { containsGlobPatterns } from "@app/lib/picomatch"; import { containsGlobPatterns } from "@app/lib/picomatch";
import { TProjectEnvDALFactory } from "@app/services/project-env/project-env-dal"; import { TProjectEnvDALFactory } from "@app/services/project-env/project-env-dal";
import { TLicenseServiceFactory } from "../license/license-service";
import { TSecretApprovalPolicyApproverDALFactory } from "./secret-approval-policy-approver-dal"; import { TSecretApprovalPolicyApproverDALFactory } from "./secret-approval-policy-approver-dal";
import { TSecretApprovalPolicyDALFactory } from "./secret-approval-policy-dal"; import { TSecretApprovalPolicyDALFactory } from "./secret-approval-policy-dal";
import { import {
@@ -28,6 +29,7 @@ type TSecretApprovalPolicyServiceFactoryDep = {
secretApprovalPolicyDAL: TSecretApprovalPolicyDALFactory; secretApprovalPolicyDAL: TSecretApprovalPolicyDALFactory;
projectEnvDAL: Pick<TProjectEnvDALFactory, "findOne">; projectEnvDAL: Pick<TProjectEnvDALFactory, "findOne">;
secretApprovalPolicyApproverDAL: TSecretApprovalPolicyApproverDALFactory; secretApprovalPolicyApproverDAL: TSecretApprovalPolicyApproverDALFactory;
licenseService: Pick<TLicenseServiceFactory, "getPlan">;
}; };
export type TSecretApprovalPolicyServiceFactory = ReturnType<typeof secretApprovalPolicyServiceFactory>; export type TSecretApprovalPolicyServiceFactory = ReturnType<typeof secretApprovalPolicyServiceFactory>;
@@ -36,7 +38,8 @@ export const secretApprovalPolicyServiceFactory = ({
secretApprovalPolicyDAL, secretApprovalPolicyDAL,
permissionService, permissionService,
secretApprovalPolicyApproverDAL, secretApprovalPolicyApproverDAL,
projectEnvDAL projectEnvDAL,
licenseService
}: TSecretApprovalPolicyServiceFactoryDep) => { }: TSecretApprovalPolicyServiceFactoryDep) => {
const createSecretApprovalPolicy = async ({ const createSecretApprovalPolicy = async ({
name, name,
@@ -65,6 +68,15 @@ export const secretApprovalPolicyServiceFactory = ({
ProjectPermissionActions.Create, ProjectPermissionActions.Create,
ProjectPermissionSub.SecretApproval ProjectPermissionSub.SecretApproval
); );
const plan = await licenseService.getPlan(actorOrgId);
if (!plan.secretApproval) {
throw new BadRequestError({
message:
"Failed to create secret approval policy due to plan restriction. Upgrade plan to create secret approval policy."
});
}
const env = await projectEnvDAL.findOne({ slug: environment, projectId }); const env = await projectEnvDAL.findOne({ slug: environment, projectId });
if (!env) throw new BadRequestError({ message: "Environment not found" }); if (!env) throw new BadRequestError({ message: "Environment not found" });
@@ -115,6 +127,14 @@ export const secretApprovalPolicyServiceFactory = ({
); );
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Edit, ProjectPermissionSub.SecretApproval); ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Edit, ProjectPermissionSub.SecretApproval);
const plan = await licenseService.getPlan(actorOrgId);
if (!plan.secretApproval) {
throw new BadRequestError({
message:
"Failed to update secret approval policy due to plan restriction. Upgrade plan to update secret approval policy."
});
}
const updatedSap = await secretApprovalPolicyDAL.transaction(async (tx) => { const updatedSap = await secretApprovalPolicyDAL.transaction(async (tx) => {
const doc = await secretApprovalPolicyDAL.updateById( const doc = await secretApprovalPolicyDAL.updateById(
secretApprovalPolicy.id, secretApprovalPolicy.id,
@@ -167,6 +187,14 @@ export const secretApprovalPolicyServiceFactory = ({
ProjectPermissionSub.SecretApproval ProjectPermissionSub.SecretApproval
); );
const plan = await licenseService.getPlan(actorOrgId);
if (!plan.secretApproval) {
throw new BadRequestError({
message:
"Failed to update secret approval policy due to plan restriction. Upgrade plan to update secret approval policy."
});
}
await secretApprovalPolicyDAL.deleteById(secretPolicyId); await secretApprovalPolicyDAL.deleteById(secretPolicyId);
return sapPolicy; return sapPolicy;
}; };
@@ -81,15 +81,13 @@ export const secretApprovalRequestSecretDALFactory = (db: TDbClient) => {
.select({ .select({
secVerTagId: "secVerTag.id", secVerTagId: "secVerTag.id",
secVerTagColor: "secVerTag.color", secVerTagColor: "secVerTag.color",
secVerTagSlug: "secVerTag.slug", secVerTagSlug: "secVerTag.slug"
secVerTagName: "secVerTag.name"
}) })
.select( .select(
db.ref("id").withSchema(TableName.SecretTag).as("tagId"), db.ref("id").withSchema(TableName.SecretTag).as("tagId"),
db.ref("id").withSchema(TableName.SecretApprovalRequestSecretTag).as("tagJnId"), db.ref("id").withSchema(TableName.SecretApprovalRequestSecretTag).as("tagJnId"),
db.ref("color").withSchema(TableName.SecretTag).as("tagColor"), db.ref("color").withSchema(TableName.SecretTag).as("tagColor"),
db.ref("slug").withSchema(TableName.SecretTag).as("tagSlug"), db.ref("slug").withSchema(TableName.SecretTag).as("tagSlug")
db.ref("name").withSchema(TableName.SecretTag).as("tagName")
) )
.select( .select(
db.ref("secretBlindIndex").withSchema(TableName.Secret).as("orgSecBlindIndex"), db.ref("secretBlindIndex").withSchema(TableName.Secret).as("orgSecBlindIndex"),
@@ -124,9 +122,9 @@ export const secretApprovalRequestSecretDALFactory = (db: TDbClient) => {
{ {
key: "tagJnId", key: "tagJnId",
label: "tags" as const, label: "tags" as const,
mapper: ({ tagId: id, tagName: name, tagSlug: slug, tagColor: color }) => ({ mapper: ({ tagId: id, tagSlug: slug, tagColor: color }) => ({
id, id,
name, name: slug,
slug, slug,
color color
}) })
@@ -200,11 +198,11 @@ export const secretApprovalRequestSecretDALFactory = (db: TDbClient) => {
{ {
key: "secVerTagId", key: "secVerTagId",
label: "tags" as const, label: "tags" as const,
mapper: ({ secVerTagId: id, secVerTagName: name, secVerTagSlug: slug, secVerTagColor: color }) => ({ mapper: ({ secVerTagId: id, secVerTagSlug: slug, secVerTagColor: color }) => ({
// eslint-disable-next-line // eslint-disable-next-line
id, id,
// eslint-disable-next-line // eslint-disable-next-line
name, name: slug,
// eslint-disable-next-line // eslint-disable-next-line
slug, slug,
// eslint-disable-next-line // eslint-disable-next-line
@@ -262,15 +260,13 @@ export const secretApprovalRequestSecretDALFactory = (db: TDbClient) => {
.select({ .select({
secVerTagId: "secVerTag.id", secVerTagId: "secVerTag.id",
secVerTagColor: "secVerTag.color", secVerTagColor: "secVerTag.color",
secVerTagSlug: "secVerTag.slug", secVerTagSlug: "secVerTag.slug"
secVerTagName: "secVerTag.name"
}) })
.select( .select(
db.ref("id").withSchema(TableName.SecretTag).as("tagId"), db.ref("id").withSchema(TableName.SecretTag).as("tagId"),
db.ref("id").withSchema(TableName.SecretApprovalRequestSecretTagV2).as("tagJnId"), db.ref("id").withSchema(TableName.SecretApprovalRequestSecretTagV2).as("tagJnId"),
db.ref("color").withSchema(TableName.SecretTag).as("tagColor"), db.ref("color").withSchema(TableName.SecretTag).as("tagColor"),
db.ref("slug").withSchema(TableName.SecretTag).as("tagSlug"), db.ref("slug").withSchema(TableName.SecretTag).as("tagSlug")
db.ref("name").withSchema(TableName.SecretTag).as("tagName")
) )
.select( .select(
db.ref("version").withSchema(TableName.SecretV2).as("orgSecVersion"), db.ref("version").withSchema(TableName.SecretV2).as("orgSecVersion"),
@@ -292,9 +288,9 @@ export const secretApprovalRequestSecretDALFactory = (db: TDbClient) => {
{ {
key: "tagJnId", key: "tagJnId",
label: "tags" as const, label: "tags" as const,
mapper: ({ tagId: id, tagName: name, tagSlug: slug, tagColor: color }) => ({ mapper: ({ tagId: id, tagSlug: slug, tagColor: color }) => ({
id, id,
name, name: slug,
slug, slug,
color color
}) })
@@ -330,11 +326,11 @@ export const secretApprovalRequestSecretDALFactory = (db: TDbClient) => {
{ {
key: "secVerTagId", key: "secVerTagId",
label: "tags" as const, label: "tags" as const,
mapper: ({ secVerTagId: id, secVerTagName: name, secVerTagSlug: slug, secVerTagColor: color }) => ({ mapper: ({ secVerTagId: id, secVerTagSlug: slug, secVerTagColor: color }) => ({
// eslint-disable-next-line // eslint-disable-next-line
id, id,
// eslint-disable-next-line // eslint-disable-next-line
name, name: slug,
// eslint-disable-next-line // eslint-disable-next-line
slug, slug,
// eslint-disable-next-line // eslint-disable-next-line
@@ -50,6 +50,7 @@ import { TSecretVersionV2TagDALFactory } from "@app/services/secret-v2-bridge/se
import { SmtpTemplates, TSmtpService } from "@app/services/smtp/smtp-service"; import { SmtpTemplates, TSmtpService } from "@app/services/smtp/smtp-service";
import { TUserDALFactory } from "@app/services/user/user-dal"; import { TUserDALFactory } from "@app/services/user/user-dal";
import { TLicenseServiceFactory } from "../license/license-service";
import { TPermissionServiceFactory } from "../permission/permission-service"; import { TPermissionServiceFactory } from "../permission/permission-service";
import { ProjectPermissionActions, ProjectPermissionSub } from "../permission/project-permission"; import { ProjectPermissionActions, ProjectPermissionSub } from "../permission/project-permission";
import { TSecretSnapshotServiceFactory } from "../secret-snapshot/secret-snapshot-service"; import { TSecretSnapshotServiceFactory } from "../secret-snapshot/secret-snapshot-service";
@@ -97,6 +98,7 @@ type TSecretApprovalRequestServiceFactoryDep = {
>; >;
secretVersionV2BridgeDAL: Pick<TSecretVersionV2DALFactory, "insertMany" | "findLatestVersionMany">; secretVersionV2BridgeDAL: Pick<TSecretVersionV2DALFactory, "insertMany" | "findLatestVersionMany">;
secretVersionTagV2BridgeDAL: Pick<TSecretVersionV2TagDALFactory, "insertMany">; secretVersionTagV2BridgeDAL: Pick<TSecretVersionV2TagDALFactory, "insertMany">;
licenseService: Pick<TLicenseServiceFactory, "getPlan">;
}; };
export type TSecretApprovalRequestServiceFactory = ReturnType<typeof secretApprovalRequestServiceFactory>; export type TSecretApprovalRequestServiceFactory = ReturnType<typeof secretApprovalRequestServiceFactory>;
@@ -122,7 +124,8 @@ export const secretApprovalRequestServiceFactory = ({
kmsService, kmsService,
secretV2BridgeDAL, secretV2BridgeDAL,
secretVersionV2BridgeDAL, secretVersionV2BridgeDAL,
secretVersionTagV2BridgeDAL secretVersionTagV2BridgeDAL,
licenseService
}: TSecretApprovalRequestServiceFactoryDep) => { }: TSecretApprovalRequestServiceFactoryDep) => {
const requestCount = async ({ projectId, actor, actorId, actorOrgId, actorAuthMethod }: TApprovalRequestCountDTO) => { const requestCount = async ({ projectId, actor, actorId, actorOrgId, actorAuthMethod }: TApprovalRequestCountDTO) => {
if (actor === ActorType.SERVICE) throw new BadRequestError({ message: "Cannot use service token" }); if (actor === ActorType.SERVICE) throw new BadRequestError({ message: "Cannot use service token" });
@@ -224,12 +227,10 @@ export const secretApprovalRequestServiceFactory = ({
secretKey: el.key, secretKey: el.key,
id: el.id, id: el.id,
version: el.version, version: el.version,
secretValue: el.encryptedValue secretValue: el.encryptedValue ? secretManagerDecryptor({ cipherTextBlob: el.encryptedValue }).toString() : "",
? secretManagerDecryptor({ cipherTextBlob: el.encryptedValue }).toString()
: undefined,
secretComment: el.encryptedComment secretComment: el.encryptedComment
? secretManagerDecryptor({ cipherTextBlob: el.encryptedComment }).toString() ? secretManagerDecryptor({ cipherTextBlob: el.encryptedComment }).toString()
: undefined, : "",
secret: el.secret secret: el.secret
? { ? {
secretKey: el.secret.key, secretKey: el.secret.key,
@@ -237,10 +238,10 @@ export const secretApprovalRequestServiceFactory = ({
version: el.secret.version, version: el.secret.version,
secretValue: el.secret.encryptedValue secretValue: el.secret.encryptedValue
? secretManagerDecryptor({ cipherTextBlob: el.secret.encryptedValue }).toString() ? secretManagerDecryptor({ cipherTextBlob: el.secret.encryptedValue }).toString()
: undefined, : "",
secretComment: el.secret.encryptedComment secretComment: el.secret.encryptedComment
? secretManagerDecryptor({ cipherTextBlob: el.secret.encryptedComment }).toString() ? secretManagerDecryptor({ cipherTextBlob: el.secret.encryptedComment }).toString()
: undefined : ""
} }
: undefined, : undefined,
secretVersion: el.secretVersion secretVersion: el.secretVersion
@@ -250,10 +251,10 @@ export const secretApprovalRequestServiceFactory = ({
version: el.secretVersion.version, version: el.secretVersion.version,
secretValue: el.secretVersion.encryptedValue secretValue: el.secretVersion.encryptedValue
? secretManagerDecryptor({ cipherTextBlob: el.secretVersion.encryptedValue }).toString() ? secretManagerDecryptor({ cipherTextBlob: el.secretVersion.encryptedValue }).toString()
: undefined, : "",
secretComment: el.secretVersion.encryptedComment secretComment: el.secretVersion.encryptedComment
? secretManagerDecryptor({ cipherTextBlob: el.secretVersion.encryptedComment }).toString() ? secretManagerDecryptor({ cipherTextBlob: el.secretVersion.encryptedComment }).toString()
: undefined : ""
} }
: undefined : undefined
})); }));
@@ -297,6 +298,14 @@ export const secretApprovalRequestServiceFactory = ({
if (!secretApprovalRequest) throw new BadRequestError({ message: "Secret approval request not found" }); if (!secretApprovalRequest) throw new BadRequestError({ message: "Secret approval request not found" });
if (actor !== ActorType.USER) throw new BadRequestError({ message: "Must be a user" }); if (actor !== ActorType.USER) throw new BadRequestError({ message: "Must be a user" });
const plan = await licenseService.getPlan(actorOrgId);
if (!plan.secretApproval) {
throw new BadRequestError({
message:
"Failed to review secret approval request due to plan restriction. Upgrade plan to review secret approval request."
});
}
const { policy } = secretApprovalRequest; const { policy } = secretApprovalRequest;
const { hasRole } = await permissionService.getProjectPermission( const { hasRole } = await permissionService.getProjectPermission(
ActorType.USER, ActorType.USER,
@@ -347,6 +356,14 @@ export const secretApprovalRequestServiceFactory = ({
if (!secretApprovalRequest) throw new BadRequestError({ message: "Secret approval request not found" }); if (!secretApprovalRequest) throw new BadRequestError({ message: "Secret approval request not found" });
if (actor !== ActorType.USER) throw new BadRequestError({ message: "Must be a user" }); if (actor !== ActorType.USER) throw new BadRequestError({ message: "Must be a user" });
const plan = await licenseService.getPlan(actorOrgId);
if (!plan.secretApproval) {
throw new BadRequestError({
message:
"Failed to update secret approval request due to plan restriction. Upgrade plan to update secret approval request."
});
}
const { policy } = secretApprovalRequest; const { policy } = secretApprovalRequest;
const { hasRole } = await permissionService.getProjectPermission( const { hasRole } = await permissionService.getProjectPermission(
ActorType.USER, ActorType.USER,
@@ -388,6 +405,14 @@ export const secretApprovalRequestServiceFactory = ({
if (!secretApprovalRequest) throw new BadRequestError({ message: "Secret approval request not found" }); if (!secretApprovalRequest) throw new BadRequestError({ message: "Secret approval request not found" });
if (actor !== ActorType.USER) throw new BadRequestError({ message: "Must be a user" }); if (actor !== ActorType.USER) throw new BadRequestError({ message: "Must be a user" });
const plan = await licenseService.getPlan(actorOrgId);
if (!plan.secretApproval) {
throw new BadRequestError({
message:
"Failed to merge secret approval request due to plan restriction. Upgrade plan to merge secret approval request."
});
}
const { policy, folderId, projectId } = secretApprovalRequest; const { policy, folderId, projectId } = secretApprovalRequest;
const { hasRole } = await permissionService.getProjectPermission( const { hasRole } = await permissionService.getProjectPermission(
ActorType.USER, ActorType.USER,
@@ -257,7 +257,7 @@ export const secretReplicationServiceFactory = ({
secretDAL: secretV2BridgeDAL, secretDAL: secretV2BridgeDAL,
folderDAL, folderDAL,
secretImportDAL, secretImportDAL,
decryptor: (value) => (value ? secretManagerDecryptor({ cipherTextBlob: value }).toString() : undefined) decryptor: (value) => (value ? secretManagerDecryptor({ cipherTextBlob: value }).toString() : "")
}); });
// secrets that gets replicated across imports // secrets that gets replicated across imports
const sourceDecryptedLocalSecrets = sourceLocalSecrets.map((el) => ({ const sourceDecryptedLocalSecrets = sourceLocalSecrets.map((el) => ({
@@ -449,7 +449,7 @@ export const secretReplicationServiceFactory = ({
}); });
} }
if (locallyDeletedSecrets.length) { if (locallyDeletedSecrets.length) {
await secretDAL.delete( await secretV2BridgeDAL.delete(
{ {
$in: { $in: {
id: locallyDeletedSecrets.map(({ id }) => id) id: locallyDeletedSecrets.map(({ id }) => id)
@@ -164,10 +164,10 @@ export const secretSnapshotServiceFactory = ({
secretKey: el.key, secretKey: el.key,
secretValue: el.encryptedValue secretValue: el.encryptedValue
? secretManagerDecryptor({ cipherTextBlob: el.encryptedValue }).toString() ? secretManagerDecryptor({ cipherTextBlob: el.encryptedValue }).toString()
: undefined, : "",
secretComment: el.encryptedComment secretComment: el.encryptedComment
? secretManagerDecryptor({ cipherTextBlob: el.encryptedComment }).toString() ? secretManagerDecryptor({ cipherTextBlob: el.encryptedComment }).toString()
: undefined : ""
})) }))
}; };
} else { } else {
@@ -100,8 +100,7 @@ export const snapshotDALFactory = (db: TDbClient) => {
db.ref("id").withSchema(TableName.SecretTag).as("tagId"), db.ref("id").withSchema(TableName.SecretTag).as("tagId"),
db.ref("id").withSchema(TableName.SecretVersionTag).as("tagVersionId"), db.ref("id").withSchema(TableName.SecretVersionTag).as("tagVersionId"),
db.ref("color").withSchema(TableName.SecretTag).as("tagColor"), db.ref("color").withSchema(TableName.SecretTag).as("tagColor"),
db.ref("slug").withSchema(TableName.SecretTag).as("tagSlug"), db.ref("slug").withSchema(TableName.SecretTag).as("tagSlug")
db.ref("name").withSchema(TableName.SecretTag).as("tagName")
); );
return sqlNestRelationships({ return sqlNestRelationships({
data, data,
@@ -132,9 +131,9 @@ export const snapshotDALFactory = (db: TDbClient) => {
{ {
key: "tagVersionId", key: "tagVersionId",
label: "tags" as const, label: "tags" as const,
mapper: ({ tagId: id, tagName: name, tagSlug: slug, tagColor: color, tagVersionId: vId }) => ({ mapper: ({ tagId: id, tagSlug: slug, tagColor: color, tagVersionId: vId }) => ({
id, id,
name, name: slug,
slug, slug,
color, color,
vId vId
@@ -195,8 +194,7 @@ export const snapshotDALFactory = (db: TDbClient) => {
db.ref("id").withSchema(TableName.SecretTag).as("tagId"), db.ref("id").withSchema(TableName.SecretTag).as("tagId"),
db.ref("id").withSchema(TableName.SecretVersionV2Tag).as("tagVersionId"), db.ref("id").withSchema(TableName.SecretVersionV2Tag).as("tagVersionId"),
db.ref("color").withSchema(TableName.SecretTag).as("tagColor"), db.ref("color").withSchema(TableName.SecretTag).as("tagColor"),
db.ref("slug").withSchema(TableName.SecretTag).as("tagSlug"), db.ref("slug").withSchema(TableName.SecretTag).as("tagSlug")
db.ref("name").withSchema(TableName.SecretTag).as("tagName")
); );
return sqlNestRelationships({ return sqlNestRelationships({
data, data,
@@ -227,9 +225,9 @@ export const snapshotDALFactory = (db: TDbClient) => {
{ {
key: "tagVersionId", key: "tagVersionId",
label: "tags" as const, label: "tags" as const,
mapper: ({ tagId: id, tagName: name, tagSlug: slug, tagColor: color, tagVersionId: vId }) => ({ mapper: ({ tagId: id, tagSlug: slug, tagColor: color, tagVersionId: vId }) => ({
id, id,
name, name: slug,
slug, slug,
color, color,
vId vId
@@ -353,8 +351,7 @@ export const snapshotDALFactory = (db: TDbClient) => {
db.ref("id").withSchema(TableName.SecretTag).as("tagId"), db.ref("id").withSchema(TableName.SecretTag).as("tagId"),
db.ref("id").withSchema(TableName.SecretVersionTag).as("tagVersionId"), db.ref("id").withSchema(TableName.SecretVersionTag).as("tagVersionId"),
db.ref("color").withSchema(TableName.SecretTag).as("tagColor"), db.ref("color").withSchema(TableName.SecretTag).as("tagColor"),
db.ref("slug").withSchema(TableName.SecretTag).as("tagSlug"), db.ref("slug").withSchema(TableName.SecretTag).as("tagSlug")
db.ref("name").withSchema(TableName.SecretTag).as("tagName")
); );
const formated = sqlNestRelationships({ const formated = sqlNestRelationships({
@@ -377,9 +374,9 @@ export const snapshotDALFactory = (db: TDbClient) => {
{ {
key: "tagVersionId", key: "tagVersionId",
label: "tags" as const, label: "tags" as const,
mapper: ({ tagId: id, tagName: name, tagSlug: slug, tagColor: color, tagVersionId: vId }) => ({ mapper: ({ tagId: id, tagSlug: slug, tagColor: color, tagVersionId: vId }) => ({
id, id,
name, name: slug,
slug, slug,
color, color,
vId vId
@@ -508,8 +505,7 @@ export const snapshotDALFactory = (db: TDbClient) => {
db.ref("id").withSchema(TableName.SecretTag).as("tagId"), db.ref("id").withSchema(TableName.SecretTag).as("tagId"),
db.ref("id").withSchema(TableName.SecretVersionV2Tag).as("tagVersionId"), db.ref("id").withSchema(TableName.SecretVersionV2Tag).as("tagVersionId"),
db.ref("color").withSchema(TableName.SecretTag).as("tagColor"), db.ref("color").withSchema(TableName.SecretTag).as("tagColor"),
db.ref("slug").withSchema(TableName.SecretTag).as("tagSlug"), db.ref("slug").withSchema(TableName.SecretTag).as("tagSlug")
db.ref("name").withSchema(TableName.SecretTag).as("tagName")
); );
const formated = sqlNestRelationships({ const formated = sqlNestRelationships({
@@ -532,9 +528,9 @@ export const snapshotDALFactory = (db: TDbClient) => {
{ {
key: "tagVersionId", key: "tagVersionId",
label: "tags" as const, label: "tags" as const,
mapper: ({ tagId: id, tagName: name, tagSlug: slug, tagColor: color, tagVersionId: vId }) => ({ mapper: ({ tagId: id, tagSlug: slug, tagColor: color, tagVersionId: vId }) => ({
id, id,
name, name: slug,
slug, slug,
color, color,
vId vId
+22 -13
View File
@@ -5,17 +5,26 @@ import { Redlock, Settings } from "@app/lib/red-lock";
export type TKeyStoreFactory = ReturnType<typeof keyStoreFactory>; export type TKeyStoreFactory = ReturnType<typeof keyStoreFactory>;
// all the key prefixes used must be set here to avoid conflict // all the key prefixes used must be set here to avoid conflict
export enum KeyStorePrefixes { export const KeyStorePrefixes = {
SecretReplication = "secret-replication-import-lock", SecretReplication: "secret-replication-import-lock",
KmsProjectDataKeyCreation = "kms-project-data-key-creation-lock", KmsProjectDataKeyCreation: "kms-project-data-key-creation-lock",
KmsProjectKeyCreation = "kms-project-key-creation-lock", KmsProjectKeyCreation: "kms-project-key-creation-lock",
WaitUntilReadyKmsProjectDataKeyCreation = "wait-until-ready-kms-project-data-key-creation-", WaitUntilReadyKmsProjectDataKeyCreation: "wait-until-ready-kms-project-data-key-creation-",
WaitUntilReadyKmsProjectKeyCreation = "wait-until-ready-kms-project-key-creation-", WaitUntilReadyKmsProjectKeyCreation: "wait-until-ready-kms-project-key-creation-",
KmsOrgKeyCreation = "kms-org-key-creation-lock", KmsOrgKeyCreation: "kms-org-key-creation-lock",
KmsOrgDataKeyCreation = "kms-org-data-key-creation-lock", KmsOrgDataKeyCreation: "kms-org-data-key-creation-lock",
WaitUntilReadyKmsOrgKeyCreation = "wait-until-ready-kms-org-key-creation-", WaitUntilReadyKmsOrgKeyCreation: "wait-until-ready-kms-org-key-creation-",
WaitUntilReadyKmsOrgDataKeyCreation = "wait-until-ready-kms-org-data-key-creation-" WaitUntilReadyKmsOrgDataKeyCreation: "wait-until-ready-kms-org-data-key-creation-",
}
SyncSecretIntegrationLock: (projectId: string, environmentSlug: string, secretPath: string) =>
`sync-integration-mutex-${projectId}-${environmentSlug}-${secretPath}` as const,
SyncSecretIntegrationLastRunTimestamp: (projectId: string, environmentSlug: string, secretPath: string) =>
`sync-integration-last-run-${projectId}-${environmentSlug}-${secretPath}` as const
};
export const KeyStoreTtls = {
SetSyncSecretIntegrationLastRunTimestampInSeconds: 10
};
type TWaitTillReady = { type TWaitTillReady = {
key: string; key: string;
@@ -37,10 +46,10 @@ export const keyStoreFactory = (redisUrl: string) => {
const setItemWithExpiry = async ( const setItemWithExpiry = async (
key: string, key: string,
exp: number | string, expiryInSeconds: number | string,
value: string | number | Buffer, value: string | number | Buffer,
prefix?: string prefix?: string
) => redis.set(prefix ? `${prefix}:${key}` : key, value, "EX", exp); ) => redis.set(prefix ? `${prefix}:${key}` : key, value, "EX", expiryInSeconds);
const deleteItem = async (key: string) => redis.del(key); const deleteItem = async (key: string) => redis.del(key);
+35 -3
View File
@@ -596,7 +596,8 @@ export const RAW_SECRETS = {
"The slug of the project to list secrets from. This parameter is only applicable by machine identities.", "The slug of the project to list secrets from. This parameter is only applicable by machine identities.",
environment: "The slug of the environment to list secrets from.", environment: "The slug of the environment to list secrets from.",
secretPath: "The secret path to list secrets from.", secretPath: "The secret path to list secrets from.",
includeImports: "Weather to include imported secrets or not." includeImports: "Weather to include imported secrets or not.",
tagSlugs: "The comma separated tag slugs to filter secrets"
}, },
CREATE: { CREATE: {
secretName: "The name of the secret to create.", secretName: "The name of the secret to create.",
@@ -1088,6 +1089,8 @@ export const CERTIFICATE_AUTHORITIES = {
}, },
ISSUE_CERT: { ISSUE_CERT: {
caId: "The ID of the CA to issue the certificate from", caId: "The ID of the CA to issue the certificate from",
certificateTemplateId: "The ID of the certificate template to issue the certificate from",
pkiCollectionId: "The ID of the PKI collection to add the certificate to",
friendlyName: "A friendly name for the certificate", friendlyName: "A friendly name for the certificate",
commonName: "The common name (CN) for the certificate", commonName: "The common name (CN) for the certificate",
altNames: altNames:
@@ -1103,6 +1106,7 @@ export const CERTIFICATE_AUTHORITIES = {
}, },
SIGN_CERT: { SIGN_CERT: {
caId: "The ID of the CA to issue the certificate from", caId: "The ID of the CA to issue the certificate from",
pkiCollectionId: "The ID of the PKI collection to add the certificate to",
csr: "The pem-encoded CSR to sign with the CA to be used for certificate issuance", csr: "The pem-encoded CSR to sign with the CA to be used for certificate issuance",
friendlyName: "A friendly name for the certificate", friendlyName: "A friendly name for the certificate",
commonName: "The common name (CN) for the certificate", commonName: "The common name (CN) for the certificate",
@@ -1144,6 +1148,32 @@ export const CERTIFICATES = {
} }
}; };
export const CERTIFICATE_TEMPLATES = {
CREATE: {
caId: "The ID of the certificate authority to associate the template with",
pkiCollectionId: "The ID of the PKI collection to bind to the template",
name: "The name of the template",
commonName: "The regular expression string to use for validating common names",
subjectAlternativeName: "The regular expression string to use for validating subject alternative names",
ttl: "The max TTL for the template"
},
GET: {
certificateTemplateId: "The ID of the certificate template to get"
},
UPDATE: {
certificateTemplateId: "The ID of the certificate template to update",
caId: "The ID of the certificate authority to update the association with the template",
pkiCollectionId: "The ID of the PKI collection to update the binding to the template",
name: "The updated name of the template",
commonName: "The updated regular expression string for validating common names",
subjectAlternativeName: "The updated regular expression string for validating subject alternative names",
ttl: "The updated max TTL for the template"
},
DELETE: {
certificateTemplateId: "The ID of the certificate template to delete"
}
};
export const ALERTS = { export const ALERTS = {
CREATE: { CREATE: {
projectId: "The ID of the project to create the alert in", projectId: "The ID of the project to create the alert in",
@@ -1170,14 +1200,16 @@ export const ALERTS = {
export const PKI_COLLECTIONS = { export const PKI_COLLECTIONS = {
CREATE: { CREATE: {
projectId: "The ID of the project to create the PKI collection in", projectId: "The ID of the project to create the PKI collection in",
name: "The name of the PKI collection" name: "The name of the PKI collection",
description: "A description for the PKI collection"
}, },
GET: { GET: {
collectionId: "The ID of the PKI collection to get" collectionId: "The ID of the PKI collection to get"
}, },
UPDATE: { UPDATE: {
collectionId: "The ID of the PKI collection to update", collectionId: "The ID of the PKI collection to update",
name: "The name of the PKI collection to update to" name: "The name of the PKI collection to update to",
description: "The description for the PKI collection to update to"
}, },
DELETE: { DELETE: {
collectionId: "The ID of the PKI collection to delete" collectionId: "The ID of the PKI collection to delete"
+2 -1
View File
@@ -140,7 +140,8 @@ const envSchema = z
MAINTENANCE_MODE: zodStrBool.default("false"), MAINTENANCE_MODE: zodStrBool.default("false"),
CAPTCHA_SECRET: zpStr(z.string().optional()), CAPTCHA_SECRET: zpStr(z.string().optional()),
PLAIN_API_KEY: zpStr(z.string().optional()), PLAIN_API_KEY: zpStr(z.string().optional()),
PLAIN_WISH_LABEL_IDS: zpStr(z.string().optional()) PLAIN_WISH_LABEL_IDS: zpStr(z.string().optional()),
DISABLE_AUDIT_LOG_GENERATION: zodStrBool.default("false")
}) })
.transform((data) => ({ .transform((data) => ({
...data, ...data,
+6
View File
@@ -1,2 +1,8 @@
export const getLastMidnightDateISO = (last = 1) => export const getLastMidnightDateISO = (last = 1) =>
`${new Date(new Date().setDate(new Date().getDate() - last)).toISOString().slice(0, 10)}T00:00:00Z`; `${new Date(new Date().setDate(new Date().getDate() - last)).toISOString().slice(0, 10)}T00:00:00Z`;
export const getTimeDifferenceInSeconds = (lhsTimestamp: string, rhsTimestamp: string) => {
const lhs = new Date(lhsTimestamp);
const rhs = new Date(rhsTimestamp);
return Math.floor((Number(lhs) - Number(rhs)) / 1000);
};
+10
View File
@@ -128,6 +128,16 @@ export const ormify = <DbOps extends object, Tname extends keyof Tables>(db: Kne
throw new DatabaseError({ error, name: "Create" }); throw new DatabaseError({ error, name: "Create" });
} }
}, },
// This spilit the insert into multiple chunk
batchInsert: async (data: readonly Tables[Tname]["insert"][], tx?: Knex) => {
try {
if (!data.length) return [];
const res = await (tx || db).batchInsert(tableName, data as never).returning("*");
return res as Tables[Tname]["base"][];
} catch (error) {
throw new DatabaseError({ error, name: "batchInsert" });
}
},
upsert: async (data: readonly Tables[Tname]["insert"][], onConflictField: keyof Tables[Tname]["base"], tx?: Knex) => { upsert: async (data: readonly Tables[Tname]["insert"][], onConflictField: keyof Tables[Tname]["base"], tx?: Knex) => {
try { try {
if (!data.length) return []; if (!data.length) return [];
+6
View File
@@ -16,6 +16,7 @@ export enum QueueName {
// TODO(akhilmhdh): This will get removed later. For now this is kept to stop the repeatable queue // TODO(akhilmhdh): This will get removed later. For now this is kept to stop the repeatable queue
AuditLogPrune = "audit-log-prune", AuditLogPrune = "audit-log-prune",
DailyResourceCleanUp = "daily-resource-cleanup", DailyResourceCleanUp = "daily-resource-cleanup",
DailyExpiringPkiItemAlert = "daily-expiring-pki-item-alert",
TelemetryInstanceStats = "telemtry-self-hosted-stats", TelemetryInstanceStats = "telemtry-self-hosted-stats",
IntegrationSync = "sync-integrations", IntegrationSync = "sync-integrations",
SecretWebhook = "secret-webhook", SecretWebhook = "secret-webhook",
@@ -36,6 +37,7 @@ export enum QueueJobs {
// TODO(akhilmhdh): This will get removed later. For now this is kept to stop the repeatable queue // TODO(akhilmhdh): This will get removed later. For now this is kept to stop the repeatable queue
AuditLogPrune = "audit-log-prune-job", AuditLogPrune = "audit-log-prune-job",
DailyResourceCleanUp = "daily-resource-cleanup-job", DailyResourceCleanUp = "daily-resource-cleanup-job",
DailyExpiringPkiItemAlert = "daily-expiring-pki-item-alert",
SecWebhook = "secret-webhook-trigger", SecWebhook = "secret-webhook-trigger",
TelemetryInstanceStats = "telemetry-self-hosted-stats", TelemetryInstanceStats = "telemetry-self-hosted-stats",
IntegrationSync = "secret-integration-pull", IntegrationSync = "secret-integration-pull",
@@ -71,6 +73,10 @@ export type TQueueJobTypes = {
name: QueueJobs.DailyResourceCleanUp; name: QueueJobs.DailyResourceCleanUp;
payload: undefined; payload: undefined;
}; };
[QueueName.DailyExpiringPkiItemAlert]: {
name: QueueJobs.DailyExpiringPkiItemAlert;
payload: undefined;
};
[QueueName.AuditLogPrune]: { [QueueName.AuditLogPrune]: {
name: QueueJobs.AuditLogPrune; name: QueueJobs.AuditLogPrune;
payload: undefined; payload: undefined;
+14 -15
View File
@@ -1,7 +1,6 @@
import type { RateLimitOptions, RateLimitPluginOptions } from "@fastify/rate-limit"; import type { RateLimitOptions, RateLimitPluginOptions } from "@fastify/rate-limit";
import { Redis } from "ioredis"; import { Redis } from "ioredis";
import { getRateLimiterConfig } from "@app/ee/services/rate-limit/rate-limit-service";
import { getConfig } from "@app/lib/config/env"; import { getConfig } from "@app/lib/config/env";
export const globalRateLimiterCfg = (): RateLimitPluginOptions => { export const globalRateLimiterCfg = (): RateLimitPluginOptions => {
@@ -22,14 +21,16 @@ export const globalRateLimiterCfg = (): RateLimitPluginOptions => {
// GET endpoints // GET endpoints
export const readLimit: RateLimitOptions = { export const readLimit: RateLimitOptions = {
timeWindow: 60 * 1000, timeWindow: 60 * 1000,
max: () => getRateLimiterConfig().readLimit, hook: "preValidation",
max: (req) => req.rateLimits.readLimit,
keyGenerator: (req) => req.realIp keyGenerator: (req) => req.realIp
}; };
// POST, PATCH, PUT, DELETE endpoints // POST, PATCH, PUT, DELETE endpoints
export const writeLimit: RateLimitOptions = { export const writeLimit: RateLimitOptions = {
timeWindow: 60 * 1000, timeWindow: 60 * 1000,
max: () => getRateLimiterConfig().writeLimit, hook: "preValidation",
max: (req) => req.rateLimits.writeLimit,
keyGenerator: (req) => req.realIp keyGenerator: (req) => req.realIp
}; };
@@ -37,42 +38,40 @@ export const writeLimit: RateLimitOptions = {
export const secretsLimit: RateLimitOptions = { export const secretsLimit: RateLimitOptions = {
// secrets, folders, secret imports // secrets, folders, secret imports
timeWindow: 60 * 1000, timeWindow: 60 * 1000,
max: () => getRateLimiterConfig().secretsLimit, hook: "preValidation",
max: (req) => req.rateLimits.secretsLimit,
keyGenerator: (req) => req.realIp keyGenerator: (req) => req.realIp
}; };
export const authRateLimit: RateLimitOptions = { export const authRateLimit: RateLimitOptions = {
timeWindow: 60 * 1000, timeWindow: 60 * 1000,
max: () => getRateLimiterConfig().authRateLimit, hook: "preValidation",
max: (req) => req.rateLimits.authRateLimit,
keyGenerator: (req) => req.realIp keyGenerator: (req) => req.realIp
}; };
export const inviteUserRateLimit: RateLimitOptions = { export const inviteUserRateLimit: RateLimitOptions = {
timeWindow: 60 * 1000, timeWindow: 60 * 1000,
max: () => getRateLimiterConfig().inviteUserRateLimit, hook: "preValidation",
max: (req) => req.rateLimits.inviteUserRateLimit,
keyGenerator: (req) => req.realIp keyGenerator: (req) => req.realIp
}; };
export const mfaRateLimit: RateLimitOptions = { export const mfaRateLimit: RateLimitOptions = {
timeWindow: 60 * 1000, timeWindow: 60 * 1000,
max: () => getRateLimiterConfig().mfaRateLimit, hook: "preValidation",
max: (req) => req.rateLimits.mfaRateLimit,
keyGenerator: (req) => { keyGenerator: (req) => {
return req.headers.authorization?.split(" ")[1] || req.realIp; return req.headers.authorization?.split(" ")[1] || req.realIp;
} }
}; };
export const creationLimit: RateLimitOptions = {
// identity, project, org
timeWindow: 60 * 1000,
max: () => getRateLimiterConfig().creationLimit,
keyGenerator: (req) => req.realIp
};
// Public endpoints to avoid brute force attacks // Public endpoints to avoid brute force attacks
export const publicEndpointLimit: RateLimitOptions = { export const publicEndpointLimit: RateLimitOptions = {
// Read Shared Secrets // Read Shared Secrets
timeWindow: 60 * 1000, timeWindow: 60 * 1000,
max: () => getRateLimiterConfig().publicEndpointLimit, hook: "preValidation",
max: (req) => req.rateLimits.publicEndpointLimit,
keyGenerator: (req) => req.realIp keyGenerator: (req) => req.realIp
}; };
@@ -0,0 +1,38 @@
import fp from "fastify-plugin";
import { getRateLimiterConfig } from "@app/ee/services/rate-limit/rate-limit-service";
import { getConfig } from "@app/lib/config/env";
export const injectRateLimits = fp(async (server) => {
server.decorateRequest("rateLimits", null);
server.addHook("onRequest", async (req) => {
const appCfg = getConfig();
const instanceRateLimiterConfig = getRateLimiterConfig();
if (!req.auth?.orgId) {
// for public endpoints, we always use the instance-wide default rate limits
req.rateLimits = instanceRateLimiterConfig;
return;
}
const { rateLimits, customRateLimits } = await server.services.license.getPlan(req.auth.orgId);
if (customRateLimits && !appCfg.isCloud) {
// we do this because for self-hosted/dedicated instances, we want custom rate limits to be based on admin configuration
// note that the syncing of custom rate limit happens on the instanceRateLimiterConfig object
req.rateLimits = instanceRateLimiterConfig;
return;
}
// we're using the null coalescing operator in order to handle outdated licenses
req.rateLimits = {
readLimit: rateLimits?.readLimit ?? instanceRateLimiterConfig.readLimit,
writeLimit: rateLimits?.writeLimit ?? instanceRateLimiterConfig.writeLimit,
secretsLimit: rateLimits?.secretsLimit ?? instanceRateLimiterConfig.secretsLimit,
publicEndpointLimit: instanceRateLimiterConfig.publicEndpointLimit,
authRateLimit: instanceRateLimiterConfig.authRateLimit,
inviteUserRateLimit: instanceRateLimiterConfig.inviteUserRateLimit,
mfaRateLimit: instanceRateLimiterConfig.mfaRateLimit
};
});
});
+39 -6
View File
@@ -90,6 +90,8 @@ import { certificateAuthorityEstConfigDALFactory } from "@app/services/certifica
import { certificateAuthorityQueueFactory } from "@app/services/certificate-authority/certificate-authority-queue"; import { certificateAuthorityQueueFactory } from "@app/services/certificate-authority/certificate-authority-queue";
import { certificateAuthoritySecretDALFactory } from "@app/services/certificate-authority/certificate-authority-secret-dal"; import { certificateAuthoritySecretDALFactory } from "@app/services/certificate-authority/certificate-authority-secret-dal";
import { certificateAuthorityServiceFactory } from "@app/services/certificate-authority/certificate-authority-service"; import { certificateAuthorityServiceFactory } from "@app/services/certificate-authority/certificate-authority-service";
import { certificateTemplateDALFactory } from "@app/services/certificate-template/certificate-template-dal";
import { certificateTemplateServiceFactory } from "@app/services/certificate-template/certificate-template-service";
import { groupProjectDALFactory } from "@app/services/group-project/group-project-dal"; import { groupProjectDALFactory } from "@app/services/group-project/group-project-dal";
import { groupProjectMembershipRoleDALFactory } from "@app/services/group-project/group-project-membership-role-dal"; import { groupProjectMembershipRoleDALFactory } from "@app/services/group-project/group-project-membership-role-dal";
import { groupProjectServiceFactory } from "@app/services/group-project/group-project-service"; import { groupProjectServiceFactory } from "@app/services/group-project/group-project-service";
@@ -132,6 +134,7 @@ import { orgRoleServiceFactory } from "@app/services/org/org-role-service";
import { orgServiceFactory } from "@app/services/org/org-service"; import { orgServiceFactory } from "@app/services/org/org-service";
import { orgAdminServiceFactory } from "@app/services/org-admin/org-admin-service"; import { orgAdminServiceFactory } from "@app/services/org-admin/org-admin-service";
import { orgMembershipDALFactory } from "@app/services/org-membership/org-membership-dal"; import { orgMembershipDALFactory } from "@app/services/org-membership/org-membership-dal";
import { dailyExpiringPkiItemAlertQueueServiceFactory } from "@app/services/pki-alert/expiring-pki-item-alert-queue";
import { pkiAlertDALFactory } from "@app/services/pki-alert/pki-alert-dal"; import { pkiAlertDALFactory } from "@app/services/pki-alert/pki-alert-dal";
import { pkiAlertServiceFactory } from "@app/services/pki-alert/pki-alert-service"; import { pkiAlertServiceFactory } from "@app/services/pki-alert/pki-alert-service";
import { pkiCollectionDALFactory } from "@app/services/pki-collection/pki-collection-dal"; import { pkiCollectionDALFactory } from "@app/services/pki-collection/pki-collection-dal";
@@ -190,6 +193,7 @@ import { webhookServiceFactory } from "@app/services/webhook/webhook-service";
import { injectAuditLogInfo } from "../plugins/audit-log"; import { injectAuditLogInfo } from "../plugins/audit-log";
import { injectIdentity } from "../plugins/auth/inject-identity"; import { injectIdentity } from "../plugins/auth/inject-identity";
import { injectPermission } from "../plugins/auth/inject-permission"; import { injectPermission } from "../plugins/auth/inject-permission";
import { injectRateLimits } from "../plugins/inject-rate-limits";
import { registerSecretScannerGhApp } from "../plugins/secret-scanner"; import { registerSecretScannerGhApp } from "../plugins/secret-scanner";
import { registerCertificateEstRouter } from "./est/certificate-est-router"; import { registerCertificateEstRouter } from "./est/certificate-est-router";
import { registerV1Routes } from "./v1"; import { registerV1Routes } from "./v1";
@@ -362,7 +366,8 @@ export const registerRoutes = async (
projectEnvDAL, projectEnvDAL,
secretApprovalPolicyApproverDAL: sapApproverDAL, secretApprovalPolicyApproverDAL: sapApproverDAL,
permissionService, permissionService,
secretApprovalPolicyDAL secretApprovalPolicyDAL,
licenseService
}); });
const tokenService = tokenServiceFactory({ tokenDAL: authTokenDAL, userDAL, orgMembershipDAL }); const tokenService = tokenServiceFactory({ tokenDAL: authTokenDAL, userDAL, orgMembershipDAL });
@@ -587,6 +592,7 @@ export const registerRoutes = async (
const certificateAuthoritySecretDAL = certificateAuthoritySecretDALFactory(db); const certificateAuthoritySecretDAL = certificateAuthoritySecretDALFactory(db);
const certificateAuthorityCrlDAL = certificateAuthorityCrlDALFactory(db); const certificateAuthorityCrlDAL = certificateAuthorityCrlDALFactory(db);
const certificateAuthorityEstConfigDAL = certificateAuthorityEstConfigDALFactory(db); const certificateAuthorityEstConfigDAL = certificateAuthorityEstConfigDALFactory(db);
const certificateTemplateDAL = certificateTemplateDALFactory(db);
const certificateDAL = certificateDALFactory(db); const certificateDAL = certificateDALFactory(db);
const certificateBodyDAL = certificateBodyDALFactory(db); const certificateBodyDAL = certificateBodyDALFactory(db);
@@ -622,9 +628,12 @@ export const registerRoutes = async (
certificateAuthorityCertDAL, certificateAuthorityCertDAL,
certificateAuthoritySecretDAL, certificateAuthoritySecretDAL,
certificateAuthorityCrlDAL, certificateAuthorityCrlDAL,
certificateTemplateDAL,
certificateAuthorityQueue, certificateAuthorityQueue,
certificateDAL, certificateDAL,
certificateBodyDAL, certificateBodyDAL,
pkiCollectionDAL,
pkiCollectionItemDAL,
projectDAL, projectDAL,
kmsService, kmsService,
permissionService, permissionService,
@@ -640,6 +649,12 @@ export const registerRoutes = async (
licenseService licenseService
}); });
const certificateTemplateService = certificateTemplateServiceFactory({
certificateTemplateDAL,
certificateAuthorityDAL,
permissionService
});
const pkiAlertService = pkiAlertServiceFactory({ const pkiAlertService = pkiAlertServiceFactory({
pkiAlertDAL, pkiAlertDAL,
pkiCollectionDAL, pkiCollectionDAL,
@@ -677,7 +692,8 @@ export const registerRoutes = async (
identityProjectMembershipRoleDAL, identityProjectMembershipRoleDAL,
keyStore, keyStore,
kmsService, kmsService,
projectBotDAL projectBotDAL,
certificateTemplateDAL
}); });
const projectEnvService = projectEnvServiceFactory({ const projectEnvService = projectEnvServiceFactory({
@@ -740,6 +756,7 @@ export const registerRoutes = async (
kmsService kmsService
}); });
const secretQueueService = secretQueueFactory({ const secretQueueService = secretQueueFactory({
keyStore,
queueService, queueService,
secretDAL, secretDAL,
folderDAL, folderDAL,
@@ -825,7 +842,8 @@ export const registerRoutes = async (
secretVersionTagV2BridgeDAL, secretVersionTagV2BridgeDAL,
smtpService, smtpService,
projectEnvDAL, projectEnvDAL,
userDAL userDAL,
licenseService
}); });
const secretService = secretServiceFactory({ const secretService = secretServiceFactory({
@@ -926,8 +944,15 @@ export const registerRoutes = async (
folderDAL, folderDAL,
integrationDAL, integrationDAL,
integrationAuthDAL, integrationAuthDAL,
secretQueueService secretQueueService,
integrationAuthService,
projectBotService,
secretV2BridgeDAL,
secretImportDAL,
secretDAL,
kmsService
}); });
const serviceTokenService = serviceTokenServiceFactory({ const serviceTokenService = serviceTokenServiceFactory({
projectEnvDAL, projectEnvDAL,
serviceTokenDAL, serviceTokenDAL,
@@ -1054,13 +1079,18 @@ export const registerRoutes = async (
const dailyResourceCleanUp = dailyResourceCleanUpQueueServiceFactory({ const dailyResourceCleanUp = dailyResourceCleanUpQueueServiceFactory({
auditLogDAL, auditLogDAL,
queueService, queueService,
pkiAlertService,
secretVersionDAL, secretVersionDAL,
secretFolderVersionDAL: folderVersionDAL, secretFolderVersionDAL: folderVersionDAL,
snapshotDAL, snapshotDAL,
identityAccessTokenDAL, identityAccessTokenDAL,
secretSharingDAL, secretSharingDAL,
secretVersionV2DAL: secretVersionV2BridgeDAL secretVersionV2DAL: secretVersionV2BridgeDAL,
identityUniversalAuthClientSecretDAL: identityUaClientSecretDAL
});
const dailyExpiringPkiItemAlert = dailyExpiringPkiItemAlertQueueServiceFactory({
queueService,
pkiAlertService
}); });
const oidcService = oidcConfigServiceFactory({ const oidcService = oidcConfigServiceFactory({
@@ -1087,6 +1117,7 @@ export const registerRoutes = async (
await telemetryQueue.startTelemetryCheck(); await telemetryQueue.startTelemetryCheck();
await dailyResourceCleanUp.startCleanUp(); await dailyResourceCleanUp.startCleanUp();
await dailyExpiringPkiItemAlert.startSendingAlerts();
await kmsService.startService(); await kmsService.startService();
// inject all services // inject all services
@@ -1144,6 +1175,7 @@ export const registerRoutes = async (
auditLogStream: auditLogStreamService, auditLogStream: auditLogStreamService,
certificate: certificateService, certificate: certificateService,
certificateAuthority: certificateAuthorityService, certificateAuthority: certificateAuthorityService,
certificateTemplate: certificateTemplateService,
certificateAuthorityCrl: certificateAuthorityCrlService, certificateAuthorityCrl: certificateAuthorityCrlService,
pkiAlert: pkiAlertService, pkiAlert: pkiAlertService,
pkiCollection: pkiCollectionService, pkiCollection: pkiCollectionService,
@@ -1175,6 +1207,7 @@ export const registerRoutes = async (
await server.register(injectIdentity, { userDAL, serviceTokenDAL }); await server.register(injectIdentity, { userDAL, serviceTokenDAL });
await server.register(injectPermission); await server.register(injectPermission);
await server.register(injectRateLimits);
await server.register(injectAuditLogInfo); await server.register(injectAuditLogInfo);
server.route({ server.route({
@@ -63,8 +63,8 @@ export const secretRawSchema = z.object({
version: z.number(), version: z.number(),
type: z.string(), type: z.string(),
secretKey: z.string(), secretKey: z.string(),
secretValue: z.string().optional(), secretValue: z.string(),
secretComment: z.string().optional(), secretComment: z.string(),
secretReminderNote: z.string().nullable().optional(), secretReminderNote: z.string().nullable().optional(),
secretReminderRepeatDays: z.number().nullable().optional(), secretReminderRepeatDays: z.number().nullable().optional(),
skipMultilineEncoding: z.boolean().default(false).nullable().optional(), skipMultilineEncoding: z.boolean().default(false).nullable().optional(),
@@ -284,7 +284,7 @@ export const registerCaRouter = async (server: FastifyZodProvider) => {
}, },
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
schema: { schema: {
description: "Renew CA certificate for CA", description: "Perform CA certificate renewal",
params: z.object({ params: z.object({
caId: z.string().trim().describe(CERTIFICATE_AUTHORITIES.RENEW_CA_CERT.caId) caId: z.string().trim().describe(CERTIFICATE_AUTHORITIES.RENEW_CA_CERT.caId)
}), }),
@@ -557,6 +557,7 @@ export const registerCaRouter = async (server: FastifyZodProvider) => {
}), }),
body: z body: z
.object({ .object({
pkiCollectionId: z.string().trim().optional().describe(CERTIFICATE_AUTHORITIES.ISSUE_CERT.pkiCollectionId),
friendlyName: z.string().trim().optional().describe(CERTIFICATE_AUTHORITIES.ISSUE_CERT.friendlyName), friendlyName: z.string().trim().optional().describe(CERTIFICATE_AUTHORITIES.ISSUE_CERT.friendlyName),
commonName: z.string().trim().min(1).describe(CERTIFICATE_AUTHORITIES.ISSUE_CERT.commonName), commonName: z.string().trim().min(1).describe(CERTIFICATE_AUTHORITIES.ISSUE_CERT.commonName),
altNames: validateAltNamesField.describe(CERTIFICATE_AUTHORITIES.ISSUE_CERT.altNames), altNames: validateAltNamesField.describe(CERTIFICATE_AUTHORITIES.ISSUE_CERT.altNames),
@@ -636,6 +637,7 @@ export const registerCaRouter = async (server: FastifyZodProvider) => {
body: z body: z
.object({ .object({
csr: z.string().trim().min(1).describe(CERTIFICATE_AUTHORITIES.SIGN_CERT.csr), csr: z.string().trim().min(1).describe(CERTIFICATE_AUTHORITIES.SIGN_CERT.csr),
pkiCollectionId: z.string().trim().optional().describe(CERTIFICATE_AUTHORITIES.SIGN_CERT.pkiCollectionId),
friendlyName: z.string().trim().optional().describe(CERTIFICATE_AUTHORITIES.SIGN_CERT.friendlyName), friendlyName: z.string().trim().optional().describe(CERTIFICATE_AUTHORITIES.SIGN_CERT.friendlyName),
commonName: z.string().trim().min(1).optional().describe(CERTIFICATE_AUTHORITIES.SIGN_CERT.commonName), commonName: z.string().trim().min(1).optional().describe(CERTIFICATE_AUTHORITIES.SIGN_CERT.commonName),
altNames: validateAltNamesField.describe(CERTIFICATE_AUTHORITIES.SIGN_CERT.altNames), altNames: validateAltNamesField.describe(CERTIFICATE_AUTHORITIES.SIGN_CERT.altNames),
@@ -1,12 +1,17 @@
import ms from "ms";
import { z } from "zod"; import { z } from "zod";
import { CertificatesSchema } from "@app/db/schemas"; import { CertificatesSchema } from "@app/db/schemas";
import { EventType } from "@app/ee/services/audit-log/audit-log-types"; import { EventType } from "@app/ee/services/audit-log/audit-log-types";
import { CERTIFICATES } from "@app/lib/api-docs"; import { CERTIFICATE_AUTHORITIES, CERTIFICATES } from "@app/lib/api-docs";
import { readLimit, writeLimit } from "@app/server/config/rateLimiter"; import { readLimit, writeLimit } from "@app/server/config/rateLimiter";
import { verifyAuth } from "@app/server/plugins/auth/verify-auth"; import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
import { AuthMode } from "@app/services/auth/auth-type"; import { AuthMode } from "@app/services/auth/auth-type";
import { CrlReason } from "@app/services/certificate/certificate-types"; import { CrlReason } from "@app/services/certificate/certificate-types";
import {
validateAltNamesField,
validateCaDateField
} from "@app/services/certificate-authority/certificate-authority-validators";
export const registerCertRouter = async (server: FastifyZodProvider) => { export const registerCertRouter = async (server: FastifyZodProvider) => {
server.route({ server.route({
@@ -55,6 +60,185 @@ export const registerCertRouter = async (server: FastifyZodProvider) => {
} }
}); });
server.route({
method: "POST",
url: "/issue-certificate",
config: {
rateLimit: writeLimit
},
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
schema: {
description: "Issue certificate",
body: z
.object({
caId: z.string().trim().optional().describe(CERTIFICATE_AUTHORITIES.ISSUE_CERT.caId),
certificateTemplateId: z
.string()
.trim()
.optional()
.describe(CERTIFICATE_AUTHORITIES.ISSUE_CERT.certificateTemplateId),
pkiCollectionId: z.string().trim().optional().describe(CERTIFICATE_AUTHORITIES.SIGN_CERT.pkiCollectionId),
friendlyName: z.string().trim().optional().describe(CERTIFICATE_AUTHORITIES.ISSUE_CERT.friendlyName),
commonName: z.string().trim().min(1).describe(CERTIFICATE_AUTHORITIES.ISSUE_CERT.commonName),
altNames: validateAltNamesField.describe(CERTIFICATE_AUTHORITIES.ISSUE_CERT.altNames),
ttl: z
.string()
.refine((val) => ms(val) > 0, "TTL must be a positive number")
.describe(CERTIFICATE_AUTHORITIES.ISSUE_CERT.ttl),
notBefore: validateCaDateField.optional().describe(CERTIFICATE_AUTHORITIES.ISSUE_CERT.notBefore),
notAfter: validateCaDateField.optional().describe(CERTIFICATE_AUTHORITIES.ISSUE_CERT.notAfter)
})
.refine(
(data) => {
const { ttl, notAfter } = data;
return (ttl !== undefined && notAfter === undefined) || (ttl === undefined && notAfter !== undefined);
},
{
message: "Either ttl or notAfter must be present, but not both",
path: ["ttl", "notAfter"]
}
)
.refine(
(data) =>
(data.caId !== undefined && data.certificateTemplateId === undefined) ||
(data.caId === undefined && data.certificateTemplateId !== undefined),
{
message: "Either CA ID or Certificate Template ID must be present, but not both",
path: ["caId", "certificateTemplateId"]
}
),
response: {
200: z.object({
certificate: z.string().trim().describe(CERTIFICATE_AUTHORITIES.ISSUE_CERT.certificate),
issuingCaCertificate: z.string().trim().describe(CERTIFICATE_AUTHORITIES.ISSUE_CERT.issuingCaCertificate),
certificateChain: z.string().trim().describe(CERTIFICATE_AUTHORITIES.ISSUE_CERT.certificateChain),
privateKey: z.string().trim().describe(CERTIFICATE_AUTHORITIES.ISSUE_CERT.privateKey),
serialNumber: z.string().trim().describe(CERTIFICATE_AUTHORITIES.ISSUE_CERT.serialNumber)
})
}
},
handler: async (req) => {
const { certificate, certificateChain, issuingCaCertificate, privateKey, serialNumber, ca } =
await server.services.certificateAuthority.issueCertFromCa({
actor: req.permission.type,
actorId: req.permission.id,
actorAuthMethod: req.permission.authMethod,
actorOrgId: req.permission.orgId,
...req.body
});
await server.services.auditLog.createAuditLog({
...req.auditLogInfo,
projectId: ca.projectId,
event: {
type: EventType.ISSUE_CERT,
metadata: {
caId: ca.id,
dn: ca.dn,
serialNumber
}
}
});
return {
certificate,
certificateChain,
issuingCaCertificate,
privateKey,
serialNumber
};
}
});
server.route({
method: "POST",
url: "/sign-certificate",
config: {
rateLimit: writeLimit
},
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
schema: {
description: "Sign certificate",
body: z
.object({
caId: z.string().trim().optional().describe(CERTIFICATE_AUTHORITIES.SIGN_CERT.caId),
certificateTemplateId: z
.string()
.trim()
.optional()
.describe(CERTIFICATE_AUTHORITIES.ISSUE_CERT.certificateTemplateId),
pkiCollectionId: z.string().trim().optional().describe(CERTIFICATE_AUTHORITIES.SIGN_CERT.pkiCollectionId),
csr: z.string().trim().min(1).describe(CERTIFICATE_AUTHORITIES.SIGN_CERT.csr),
friendlyName: z.string().trim().optional().describe(CERTIFICATE_AUTHORITIES.SIGN_CERT.friendlyName),
commonName: z.string().trim().min(1).optional().describe(CERTIFICATE_AUTHORITIES.SIGN_CERT.commonName),
altNames: validateAltNamesField.describe(CERTIFICATE_AUTHORITIES.SIGN_CERT.altNames),
ttl: z
.string()
.refine((val) => ms(val) > 0, "TTL must be a positive number")
.describe(CERTIFICATE_AUTHORITIES.SIGN_CERT.ttl),
notBefore: validateCaDateField.optional().describe(CERTIFICATE_AUTHORITIES.SIGN_CERT.notBefore),
notAfter: validateCaDateField.optional().describe(CERTIFICATE_AUTHORITIES.SIGN_CERT.notAfter)
})
.refine(
(data) => {
const { ttl, notAfter } = data;
return (ttl !== undefined && notAfter === undefined) || (ttl === undefined && notAfter !== undefined);
},
{
message: "Either ttl or notAfter must be present, but not both",
path: ["ttl", "notAfter"]
}
)
.refine(
(data) =>
(data.caId !== undefined && data.certificateTemplateId === undefined) ||
(data.caId === undefined && data.certificateTemplateId !== undefined),
{
message: "Either CA ID or Certificate Template ID must be present, but not both",
path: ["caId", "certificateTemplateId"]
}
),
response: {
200: z.object({
certificate: z.string().trim().describe(CERTIFICATE_AUTHORITIES.SIGN_CERT.certificate),
issuingCaCertificate: z.string().trim().describe(CERTIFICATE_AUTHORITIES.ISSUE_CERT.issuingCaCertificate),
certificateChain: z.string().trim().describe(CERTIFICATE_AUTHORITIES.ISSUE_CERT.certificateChain),
serialNumber: z.string().trim().describe(CERTIFICATE_AUTHORITIES.ISSUE_CERT.serialNumber)
})
}
},
handler: async (req) => {
const { certificate, certificateChain, issuingCaCertificate, serialNumber, ca } =
await server.services.certificateAuthority.signCertFromCa({
actor: req.permission.type,
actorId: req.permission.id,
actorAuthMethod: req.permission.authMethod,
actorOrgId: req.permission.orgId,
...req.body
});
await server.services.auditLog.createAuditLog({
...req.auditLogInfo,
projectId: ca.projectId,
event: {
type: EventType.SIGN_CERT,
metadata: {
caId: ca.id,
dn: ca.dn,
serialNumber
}
}
});
return {
certificate,
certificateChain,
issuingCaCertificate,
serialNumber
};
}
});
server.route({ server.route({
method: "POST", method: "POST",
url: "/:serialNumber/revoke", url: "/:serialNumber/revoke",
@@ -0,0 +1,205 @@
import ms from "ms";
import { z } from "zod";
import { EventType } from "@app/ee/services/audit-log/audit-log-types";
import { CERTIFICATE_TEMPLATES } from "@app/lib/api-docs";
import { readLimit, writeLimit } from "@app/server/config/rateLimiter";
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
import { AuthMode } from "@app/services/auth/auth-type";
import { sanitizedCertificateTemplate } from "@app/services/certificate-template/certificate-template-schema";
import { validateTemplateRegexField } from "@app/services/certificate-template/certificate-template-validators";
export const registerCertificateTemplateRouter = async (server: FastifyZodProvider) => {
server.route({
method: "GET",
url: "/:certificateTemplateId",
config: {
rateLimit: readLimit
},
schema: {
params: z.object({
certificateTemplateId: z.string().describe(CERTIFICATE_TEMPLATES.GET.certificateTemplateId)
}),
response: {
200: sanitizedCertificateTemplate
}
},
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
handler: async (req) => {
const certificateTemplate = await server.services.certificateTemplate.getCertTemplate({
id: req.params.certificateTemplateId,
actor: req.permission.type,
actorId: req.permission.id,
actorAuthMethod: req.permission.authMethod,
actorOrgId: req.permission.orgId
});
await server.services.auditLog.createAuditLog({
...req.auditLogInfo,
projectId: certificateTemplate.projectId,
event: {
type: EventType.GET_CERTIFICATE_TEMPLATE,
metadata: {
certificateTemplateId: certificateTemplate.id
}
}
});
return certificateTemplate;
}
});
server.route({
method: "POST",
url: "/",
config: {
rateLimit: writeLimit
},
schema: {
body: z.object({
caId: z.string().describe(CERTIFICATE_TEMPLATES.CREATE.caId),
pkiCollectionId: z.string().optional().describe(CERTIFICATE_TEMPLATES.CREATE.pkiCollectionId),
name: z.string().min(1).describe(CERTIFICATE_TEMPLATES.CREATE.name),
commonName: validateTemplateRegexField.describe(CERTIFICATE_TEMPLATES.CREATE.commonName),
subjectAlternativeName: validateTemplateRegexField.describe(
CERTIFICATE_TEMPLATES.CREATE.subjectAlternativeName
),
ttl: z
.string()
.refine((val) => ms(val) > 0, "TTL must be a positive number")
.describe(CERTIFICATE_TEMPLATES.CREATE.ttl)
}),
response: {
200: sanitizedCertificateTemplate
}
},
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
handler: async (req) => {
const certificateTemplate = await server.services.certificateTemplate.createCertTemplate({
actor: req.permission.type,
actorId: req.permission.id,
actorAuthMethod: req.permission.authMethod,
actorOrgId: req.permission.orgId,
...req.body
});
await server.services.auditLog.createAuditLog({
...req.auditLogInfo,
projectId: certificateTemplate.projectId,
event: {
type: EventType.CREATE_CERTIFICATE_TEMPLATE,
metadata: {
certificateTemplateId: certificateTemplate.id,
caId: certificateTemplate.caId,
pkiCollectionId: certificateTemplate.pkiCollectionId as string,
name: certificateTemplate.name,
commonName: certificateTemplate.commonName,
subjectAlternativeName: certificateTemplate.subjectAlternativeName,
ttl: certificateTemplate.ttl
}
}
});
return certificateTemplate;
}
});
server.route({
method: "PATCH",
url: "/:certificateTemplateId",
config: {
rateLimit: writeLimit
},
schema: {
body: z.object({
caId: z.string().optional().describe(CERTIFICATE_TEMPLATES.UPDATE.caId),
pkiCollectionId: z.string().optional().describe(CERTIFICATE_TEMPLATES.UPDATE.pkiCollectionId),
name: z.string().min(1).optional().describe(CERTIFICATE_TEMPLATES.UPDATE.name),
commonName: validateTemplateRegexField.optional().describe(CERTIFICATE_TEMPLATES.UPDATE.commonName),
subjectAlternativeName: validateTemplateRegexField
.optional()
.describe(CERTIFICATE_TEMPLATES.UPDATE.subjectAlternativeName),
ttl: z
.string()
.refine((val) => ms(val) > 0, "TTL must be a positive number")
.optional()
.describe(CERTIFICATE_TEMPLATES.UPDATE.ttl)
}),
params: z.object({
certificateTemplateId: z.string().describe(CERTIFICATE_TEMPLATES.UPDATE.certificateTemplateId)
}),
response: {
200: sanitizedCertificateTemplate
}
},
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
handler: async (req) => {
const certificateTemplate = await server.services.certificateTemplate.updateCertTemplate({
...req.body,
id: req.params.certificateTemplateId,
actor: req.permission.type,
actorId: req.permission.id,
actorAuthMethod: req.permission.authMethod,
actorOrgId: req.permission.orgId
});
await server.services.auditLog.createAuditLog({
...req.auditLogInfo,
projectId: certificateTemplate.projectId,
event: {
type: EventType.UPDATE_CERTIFICATE_TEMPLATE,
metadata: {
certificateTemplateId: certificateTemplate.id,
caId: certificateTemplate.caId,
pkiCollectionId: certificateTemplate.pkiCollectionId as string,
name: certificateTemplate.name,
commonName: certificateTemplate.commonName,
subjectAlternativeName: certificateTemplate.subjectAlternativeName,
ttl: certificateTemplate.ttl
}
}
});
return certificateTemplate;
}
});
server.route({
method: "DELETE",
url: "/:certificateTemplateId",
config: {
rateLimit: writeLimit
},
schema: {
params: z.object({
certificateTemplateId: z.string().describe(CERTIFICATE_TEMPLATES.DELETE.certificateTemplateId)
}),
response: {
200: sanitizedCertificateTemplate
}
},
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
handler: async (req) => {
const certificateTemplate = await server.services.certificateTemplate.deleteCertTemplate({
id: req.params.certificateTemplateId,
actor: req.permission.type,
actorId: req.permission.id,
actorAuthMethod: req.permission.authMethod,
actorOrgId: req.permission.orgId
});
await server.services.auditLog.createAuditLog({
...req.auditLogInfo,
projectId: certificateTemplate.projectId,
event: {
type: EventType.DELETE_CERTIFICATE_TEMPLATE,
metadata: {
certificateTemplateId: certificateTemplate.id
}
}
});
return certificateTemplate;
}
});
};
@@ -3,7 +3,7 @@ import { z } from "zod";
import { IdentitiesSchema, IdentityOrgMembershipsSchema, OrgMembershipRole, OrgRolesSchema } from "@app/db/schemas"; import { IdentitiesSchema, IdentityOrgMembershipsSchema, OrgMembershipRole, OrgRolesSchema } from "@app/db/schemas";
import { EventType } from "@app/ee/services/audit-log/audit-log-types"; import { EventType } from "@app/ee/services/audit-log/audit-log-types";
import { IDENTITIES } from "@app/lib/api-docs"; import { IDENTITIES } from "@app/lib/api-docs";
import { creationLimit, readLimit, writeLimit } from "@app/server/config/rateLimiter"; import { readLimit, writeLimit } from "@app/server/config/rateLimiter";
import { getTelemetryDistinctId } from "@app/server/lib/telemetry"; import { getTelemetryDistinctId } from "@app/server/lib/telemetry";
import { verifyAuth } from "@app/server/plugins/auth/verify-auth"; import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
import { AuthMode } from "@app/services/auth/auth-type"; import { AuthMode } from "@app/services/auth/auth-type";
@@ -16,7 +16,7 @@ export const registerIdentityRouter = async (server: FastifyZodProvider) => {
method: "POST", method: "POST",
url: "/", url: "/",
config: { config: {
rateLimit: creationLimit rateLimit: writeLimit
}, },
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
schema: { schema: {
+4 -2
View File
@@ -3,6 +3,7 @@ import { registerAuthRoutes } from "./auth-router";
import { registerProjectBotRouter } from "./bot-router"; import { registerProjectBotRouter } from "./bot-router";
import { registerCaRouter } from "./certificate-authority-router"; import { registerCaRouter } from "./certificate-authority-router";
import { registerCertRouter } from "./certificate-router"; import { registerCertRouter } from "./certificate-router";
import { registerCertificateTemplateRouter } from "./certificate-template-router";
import { registerIdentityAccessTokenRouter } from "./identity-access-token-router"; import { registerIdentityAccessTokenRouter } from "./identity-access-token-router";
import { registerIdentityAwsAuthRouter } from "./identity-aws-iam-auth-router"; import { registerIdentityAwsAuthRouter } from "./identity-aws-iam-auth-router";
import { registerIdentityAzureAuthRouter } from "./identity-azure-auth-router"; import { registerIdentityAzureAuthRouter } from "./identity-azure-auth-router";
@@ -76,8 +77,9 @@ export const registerV1Routes = async (server: FastifyZodProvider) => {
async (pkiRouter) => { async (pkiRouter) => {
await pkiRouter.register(registerCaRouter, { prefix: "/ca" }); await pkiRouter.register(registerCaRouter, { prefix: "/ca" });
await pkiRouter.register(registerCertRouter, { prefix: "/certificates" }); await pkiRouter.register(registerCertRouter, { prefix: "/certificates" });
await server.register(registerPkiAlertRouter, { prefix: "/alerts" }); await pkiRouter.register(registerCertificateTemplateRouter, { prefix: "/certificate-templates" });
await server.register(registerPkiCollectionRouter, { prefix: "/collections" }); await pkiRouter.register(registerPkiAlertRouter, { prefix: "/alerts" });
await pkiRouter.register(registerPkiCollectionRouter, { prefix: "/collections" });
}, },
{ prefix: "/pki" } { prefix: "/pki" }
); );
@@ -170,6 +170,12 @@ export const registerIntegrationRouter = async (server: FastifyZodProvider) => {
params: z.object({ params: z.object({
integrationId: z.string().trim().describe(INTEGRATION.DELETE.integrationId) integrationId: z.string().trim().describe(INTEGRATION.DELETE.integrationId)
}), }),
querystring: z.object({
shouldDeleteIntegrationSecrets: z
.enum(["true", "false"])
.optional()
.transform((val) => val === "true")
}),
response: { response: {
200: z.object({ 200: z.object({
integration: IntegrationsSchema integration: IntegrationsSchema
@@ -183,7 +189,8 @@ export const registerIntegrationRouter = async (server: FastifyZodProvider) => {
actorAuthMethod: req.permission.authMethod, actorAuthMethod: req.permission.authMethod,
actor: req.permission.type, actor: req.permission.type,
actorOrgId: req.permission.orgId, actorOrgId: req.permission.orgId,
id: req.params.integrationId id: req.params.integrationId,
shouldDeleteIntegrationSecrets: req.query.shouldDeleteIntegrationSecrets
}); });
await server.services.auditLog.createAuditLog({ await server.services.auditLog.createAuditLog({
@@ -205,7 +212,8 @@ export const registerIntegrationRouter = async (server: FastifyZodProvider) => {
targetService: integration.targetService, targetService: integration.targetService,
targetServiceId: integration.targetServiceId, targetServiceId: integration.targetServiceId,
path: integration.path, path: integration.path,
region: integration.region region: integration.region,
shouldDeleteIntegrationSecrets: req.query.shouldDeleteIntegrationSecrets
// eslint-disable-next-line // eslint-disable-next-line
}) as any }) as any
} }
@@ -22,7 +22,11 @@ export const registerPkiAlertRouter = async (server: FastifyZodProvider) => {
pkiCollectionId: z.string().trim().describe(ALERTS.CREATE.pkiCollectionId), pkiCollectionId: z.string().trim().describe(ALERTS.CREATE.pkiCollectionId),
name: z.string().trim().describe(ALERTS.CREATE.name), name: z.string().trim().describe(ALERTS.CREATE.name),
alertBeforeDays: z.number().describe(ALERTS.CREATE.alertBeforeDays), alertBeforeDays: z.number().describe(ALERTS.CREATE.alertBeforeDays),
emails: z.array(z.string().trim().email({ message: "Invalid email address" })).describe(ALERTS.CREATE.emails) emails: z
.array(z.string().trim().email({ message: "Invalid email address" }))
.min(1, { message: "You must specify at least 1 email" })
.max(5, { message: "You can specify a maximum of 5 emails" })
.describe(ALERTS.CREATE.emails)
}), }),
response: { response: {
200: PkiAlertsSchema 200: PkiAlertsSchema
@@ -114,6 +118,8 @@ export const registerPkiAlertRouter = async (server: FastifyZodProvider) => {
pkiCollectionId: z.string().trim().optional().describe(ALERTS.UPDATE.pkiCollectionId), pkiCollectionId: z.string().trim().optional().describe(ALERTS.UPDATE.pkiCollectionId),
emails: z emails: z
.array(z.string().trim().email({ message: "Invalid email address" })) .array(z.string().trim().email({ message: "Invalid email address" }))
.min(1, { message: "You must specify at least 1 email" })
.max(5, { message: "You can specify a maximum of 5 emails" })
.optional() .optional()
.describe(ALERTS.UPDATE.emails) .describe(ALERTS.UPDATE.emails)
}), }),
@@ -20,7 +20,8 @@ export const registerPkiCollectionRouter = async (server: FastifyZodProvider) =>
description: "Create PKI collection", description: "Create PKI collection",
body: z.object({ body: z.object({
projectId: z.string().trim().describe(PKI_COLLECTIONS.CREATE.projectId), projectId: z.string().trim().describe(PKI_COLLECTIONS.CREATE.projectId),
name: z.string().trim().describe(PKI_COLLECTIONS.CREATE.name) name: z.string().trim().describe(PKI_COLLECTIONS.CREATE.name),
description: z.string().trim().default("").describe(PKI_COLLECTIONS.CREATE.description)
}), }),
response: { response: {
200: PkiCollectionsSchema 200: PkiCollectionsSchema
@@ -104,7 +105,8 @@ export const registerPkiCollectionRouter = async (server: FastifyZodProvider) =>
collectionId: z.string().trim().describe(PKI_COLLECTIONS.UPDATE.collectionId) collectionId: z.string().trim().describe(PKI_COLLECTIONS.UPDATE.collectionId)
}), }),
body: z.object({ body: z.object({
name: z.string().trim().optional().describe(PKI_COLLECTIONS.UPDATE.name) name: z.string().trim().optional().describe(PKI_COLLECTIONS.UPDATE.name),
description: z.string().trim().optional().describe(PKI_COLLECTIONS.UPDATE.description)
}), }),
response: { response: {
200: PkiCollectionsSchema 200: PkiCollectionsSchema
@@ -1,3 +1,4 @@
import slugify from "@sindresorhus/slugify";
import { z } from "zod"; import { z } from "zod";
import { SecretTagsSchema } from "@app/db/schemas"; import { SecretTagsSchema } from "@app/db/schemas";
@@ -49,7 +50,8 @@ export const registerSecretTagRouter = async (server: FastifyZodProvider) => {
}), }),
response: { response: {
200: z.object({ 200: z.object({
workspaceTag: SecretTagsSchema // akhilmhdh: for terraform backward compatiability
workspaceTag: SecretTagsSchema.extend({ name: z.string() })
}) })
} }
}, },
@@ -79,7 +81,8 @@ export const registerSecretTagRouter = async (server: FastifyZodProvider) => {
}), }),
response: { response: {
200: z.object({ 200: z.object({
workspaceTag: SecretTagsSchema // akhilmhdh: for terraform backward compatiability
workspaceTag: SecretTagsSchema.extend({ name: z.string() })
}) })
} }
}, },
@@ -108,8 +111,14 @@ export const registerSecretTagRouter = async (server: FastifyZodProvider) => {
projectId: z.string().trim().describe(SECRET_TAGS.CREATE.projectId) projectId: z.string().trim().describe(SECRET_TAGS.CREATE.projectId)
}), }),
body: z.object({ body: z.object({
name: z.string().trim().describe(SECRET_TAGS.CREATE.name), slug: z
slug: z.string().trim().describe(SECRET_TAGS.CREATE.slug), .string()
.toLowerCase()
.trim()
.describe(SECRET_TAGS.CREATE.slug)
.refine((v) => slugify(v) === v, {
message: "Invalid slug. Slug can only contain alphanumeric characters and hyphens."
}),
color: z.string().trim().describe(SECRET_TAGS.CREATE.color) color: z.string().trim().describe(SECRET_TAGS.CREATE.color)
}), }),
response: { response: {
@@ -144,8 +153,14 @@ export const registerSecretTagRouter = async (server: FastifyZodProvider) => {
tagId: z.string().trim().describe(SECRET_TAGS.UPDATE.tagId) tagId: z.string().trim().describe(SECRET_TAGS.UPDATE.tagId)
}), }),
body: z.object({ body: z.object({
name: z.string().trim().describe(SECRET_TAGS.UPDATE.name), slug: z
slug: z.string().trim().describe(SECRET_TAGS.UPDATE.slug), .string()
.toLowerCase()
.trim()
.describe(SECRET_TAGS.UPDATE.slug)
.refine((v) => slugify(v) === v, {
message: "Invalid slug. Slug can only contain alphanumeric characters and hyphens."
}),
color: z.string().trim().describe(SECRET_TAGS.UPDATE.color) color: z.string().trim().describe(SECRET_TAGS.UPDATE.color)
}), }),
response: { response: {
@@ -9,7 +9,7 @@ import {
UsersSchema UsersSchema
} from "@app/db/schemas"; } from "@app/db/schemas";
import { ORGANIZATIONS } from "@app/lib/api-docs"; import { ORGANIZATIONS } from "@app/lib/api-docs";
import { creationLimit, readLimit, writeLimit } from "@app/server/config/rateLimiter"; import { readLimit, writeLimit } from "@app/server/config/rateLimiter";
import { verifyAuth } from "@app/server/plugins/auth/verify-auth"; import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
import { ActorType, AuthMode } from "@app/services/auth/auth-type"; import { ActorType, AuthMode } from "@app/services/auth/auth-type";
@@ -307,7 +307,7 @@ export const registerOrgRouter = async (server: FastifyZodProvider) => {
method: "POST", method: "POST",
url: "/", url: "/",
config: { config: {
rateLimit: creationLimit rateLimit: writeLimit
}, },
schema: { schema: {
body: z.object({ body: z.object({
+33 -2
View File
@@ -10,11 +10,12 @@ import {
} from "@app/db/schemas"; } from "@app/db/schemas";
import { EventType } from "@app/ee/services/audit-log/audit-log-types"; import { EventType } from "@app/ee/services/audit-log/audit-log-types";
import { PROJECTS } from "@app/lib/api-docs"; import { PROJECTS } from "@app/lib/api-docs";
import { creationLimit, readLimit, writeLimit } from "@app/server/config/rateLimiter"; import { readLimit, writeLimit } from "@app/server/config/rateLimiter";
import { getTelemetryDistinctId } from "@app/server/lib/telemetry"; import { getTelemetryDistinctId } from "@app/server/lib/telemetry";
import { verifyAuth } from "@app/server/plugins/auth/verify-auth"; import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
import { AuthMode } from "@app/services/auth/auth-type"; import { AuthMode } from "@app/services/auth/auth-type";
import { CaStatus } from "@app/services/certificate-authority/certificate-authority-types"; import { CaStatus } from "@app/services/certificate-authority/certificate-authority-types";
import { sanitizedCertificateTemplate } from "@app/services/certificate-template/certificate-template-schema";
import { ProjectFilterType } from "@app/services/project/project-types"; import { ProjectFilterType } from "@app/services/project/project-types";
import { PostHogEventTypes } from "@app/services/telemetry/telemetry-types"; import { PostHogEventTypes } from "@app/services/telemetry/telemetry-types";
@@ -148,7 +149,7 @@ export const registerProjectRouter = async (server: FastifyZodProvider) => {
method: "POST", method: "POST",
url: "/", url: "/",
config: { config: {
rateLimit: creationLimit rateLimit: writeLimit
}, },
schema: { schema: {
description: "Create a new project", description: "Create a new project",
@@ -458,4 +459,34 @@ export const registerProjectRouter = async (server: FastifyZodProvider) => {
return { collections: pkiCollections }; return { collections: pkiCollections };
} }
}); });
server.route({
method: "GET",
url: "/:projectId/certificate-templates",
config: {
rateLimit: readLimit
},
schema: {
params: z.object({
projectId: z.string().trim()
}),
response: {
200: z.object({
certificateTemplates: sanitizedCertificateTemplate.array()
})
}
},
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
handler: async (req) => {
const { certificateTemplates } = await server.services.project.listProjectCertificateTemplates({
projectId: req.params.projectId,
actorId: req.permission.id,
actorOrgId: req.permission.orgId,
actorAuthMethod: req.permission.authMethod,
actor: req.permission.type
});
return { certificateTemplates };
}
});
}; };
+25 -17
View File
@@ -59,9 +59,10 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => {
tags: SecretTagsSchema.pick({ tags: SecretTagsSchema.pick({
id: true, id: true,
slug: true, slug: true,
name: true,
color: true color: true
}).array() })
.extend({ name: z.string() })
.array()
}) })
) )
}) })
@@ -116,16 +117,15 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => {
}), }),
response: { response: {
200: z.object({ 200: z.object({
secret: SecretsSchema.omit({ secretBlindIndex: true }).merge( secret: SecretsSchema.omit({ secretBlindIndex: true }).extend({
z.object({ tags: SecretTagsSchema.pick({
tags: SecretTagsSchema.pick({ id: true,
id: true, slug: true,
slug: true, color: true
name: true,
color: true
}).array()
}) })
) .extend({ name: z.string() })
.array()
})
}) })
} }
}, },
@@ -180,7 +180,13 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => {
.enum(["true", "false"]) .enum(["true", "false"])
.default("false") .default("false")
.transform((value) => value === "true") .transform((value) => value === "true")
.describe(RAW_SECRETS.LIST.includeImports) .describe(RAW_SECRETS.LIST.includeImports),
tagSlugs: z
.string()
.describe(RAW_SECRETS.LIST.tagSlugs)
.optional()
// split by comma and trim the strings
.transform((el) => (el ? el.split(",").map((i) => i.trim()) : []))
}), }),
response: { response: {
200: z.object({ 200: z.object({
@@ -190,9 +196,9 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => {
tags: SecretTagsSchema.pick({ tags: SecretTagsSchema.pick({
id: true, id: true,
slug: true, slug: true,
name: true,
color: true color: true
}) })
.extend({ name: z.string() })
.array() .array()
.optional() .optional()
}) })
@@ -251,7 +257,8 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => {
projectId: workspaceId, projectId: workspaceId,
path: secretPath, path: secretPath,
includeImports: req.query.include_imports, includeImports: req.query.include_imports,
recursive: req.query.recursive recursive: req.query.recursive,
tagSlugs: req.query.tagSlugs
}); });
await server.services.auditLog.createAuditLog({ await server.services.auditLog.createAuditLog({
@@ -325,9 +332,9 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => {
tags: SecretTagsSchema.pick({ tags: SecretTagsSchema.pick({
id: true, id: true,
slug: true, slug: true,
name: true,
color: true color: true
}) })
.extend({ name: z.string() })
.array() .array()
.optional() .optional()
}) })
@@ -731,9 +738,10 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => {
tags: SecretTagsSchema.pick({ tags: SecretTagsSchema.pick({
id: true, id: true,
slug: true, slug: true,
name: true,
color: true color: true
}).array() })
.extend({ name: z.string() })
.array()
}) })
.array(), .array(),
imports: z imports: z
@@ -1,7 +1,7 @@
import * as x509 from "@peculiar/x509"; import * as x509 from "@peculiar/x509";
import crypto from "crypto"; import crypto from "crypto";
import { BadRequestError } from "@app/lib/errors"; import { NotFoundError } from "@app/lib/errors";
import { getProjectKmsCertificateKeyId } from "@app/services/project/project-fns"; import { getProjectKmsCertificateKeyId } from "@app/services/project/project-fns";
import { CertKeyAlgorithm, CertStatus } from "../certificate/certificate-types"; import { CertKeyAlgorithm, CertStatus } from "../certificate/certificate-types";
@@ -106,10 +106,10 @@ export const getCaCredentials = async ({
kmsService kmsService
}: TGetCaCredentialsDTO) => { }: TGetCaCredentialsDTO) => {
const ca = await certificateAuthorityDAL.findById(caId); const ca = await certificateAuthorityDAL.findById(caId);
if (!ca) throw new BadRequestError({ message: "CA not found" }); if (!ca) throw new NotFoundError({ message: "CA not found" });
const caSecret = await certificateAuthoritySecretDAL.findOne({ caId }); const caSecret = await certificateAuthoritySecretDAL.findOne({ caId });
if (!caSecret) throw new BadRequestError({ message: "CA secret not found" }); if (!caSecret) throw new NotFoundError({ message: "CA secret not found" });
const keyId = await getProjectKmsCertificateKeyId({ const keyId = await getProjectKmsCertificateKeyId({
projectId: ca.projectId, projectId: ca.projectId,
@@ -158,7 +158,7 @@ export const getCaCertChains = async ({
kmsService kmsService
}: TGetCaCertChainsDTO) => { }: TGetCaCertChainsDTO) => {
const ca = await certificateAuthorityDAL.findById(caId); const ca = await certificateAuthorityDAL.findById(caId);
if (!ca) throw new BadRequestError({ message: "CA not found" }); if (!ca) throw new NotFoundError({ message: "CA not found" });
const keyId = await getProjectKmsCertificateKeyId({ const keyId = await getProjectKmsCertificateKeyId({
projectId: ca.projectId, projectId: ca.projectId,
@@ -195,19 +195,18 @@ export const getCaCertChains = async ({
/** /**
* Return the decrypted pem-encoded certificate and certificate chain * Return the decrypted pem-encoded certificate and certificate chain
* for CA with id [caId]. * corresponding to CA certificate with id [caCertId].
*/ */
export const getCaCertChain = async ({ export const getCaCertChain = async ({
caId, caCertId,
certificateAuthorityDAL, certificateAuthorityDAL,
certificateAuthorityCertDAL, certificateAuthorityCertDAL,
projectDAL, projectDAL,
kmsService kmsService
}: TGetCaCertChainDTO) => { }: TGetCaCertChainDTO) => {
const ca = await certificateAuthorityDAL.findById(caId); const caCert = await certificateAuthorityCertDAL.findById(caCertId);
if (!ca) throw new BadRequestError({ message: "CA not found" }); if (!caCert) throw new NotFoundError({ message: "CA certificate not found" });
const ca = await certificateAuthorityDAL.findById(caCert.caId);
const caCert = await certificateAuthorityCertDAL.findOne({ caId: ca.id });
const keyId = await getProjectKmsCertificateKeyId({ const keyId = await getProjectKmsCertificateKeyId({
projectId: ca.projectId, projectId: ca.projectId,
@@ -250,7 +249,7 @@ export const rebuildCaCrl = async ({
kmsService kmsService
}: TRebuildCaCrlDTO) => { }: TRebuildCaCrlDTO) => {
const ca = await certificateAuthorityDAL.findById(caId); const ca = await certificateAuthorityDAL.findById(caId);
if (!ca) throw new BadRequestError({ message: "CA not found" }); if (!ca) throw new NotFoundError({ message: "CA not found" });
const caSecret = await certificateAuthoritySecretDAL.findOne({ caId: ca.id }); const caSecret = await certificateAuthoritySecretDAL.findOne({ caId: ca.id });
@@ -6,6 +6,7 @@ import crypto, { KeyObject } from "crypto";
import ms from "ms"; import ms from "ms";
import { z } from "zod"; import { z } from "zod";
import { TCertificateAuthorities, TCertificateTemplates } from "@app/db/schemas";
import { TCertificateAuthorityEstConfigsUpdate } from "@app/db/schemas/certificate-authority-est-configs"; import { TCertificateAuthorityEstConfigsUpdate } from "@app/db/schemas/certificate-authority-est-configs";
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service"; import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service";
import { ProjectPermissionActions, ProjectPermissionSub } from "@app/ee/services/permission/project-permission"; import { ProjectPermissionActions, ProjectPermissionSub } from "@app/ee/services/permission/project-permission";
@@ -14,11 +15,15 @@ import { BadRequestError, NotFoundError } from "@app/lib/errors";
import { TCertificateBodyDALFactory } from "@app/services/certificate/certificate-body-dal"; import { TCertificateBodyDALFactory } from "@app/services/certificate/certificate-body-dal";
import { TCertificateDALFactory } from "@app/services/certificate/certificate-dal"; import { TCertificateDALFactory } from "@app/services/certificate/certificate-dal";
import { TKmsServiceFactory } from "@app/services/kms/kms-service"; import { TKmsServiceFactory } from "@app/services/kms/kms-service";
import { TPkiCollectionDALFactory } from "@app/services/pki-collection/pki-collection-dal";
import { TPkiCollectionItemDALFactory } from "@app/services/pki-collection/pki-collection-item-dal";
import { TProjectDALFactory } from "@app/services/project/project-dal"; import { TProjectDALFactory } from "@app/services/project/project-dal";
import { getProjectKmsCertificateKeyId } from "@app/services/project/project-fns"; import { getProjectKmsCertificateKeyId } from "@app/services/project/project-fns";
import { TCertificateAuthorityCrlDALFactory } from "../../ee/services/certificate-authority-crl/certificate-authority-crl-dal"; import { TCertificateAuthorityCrlDALFactory } from "../../ee/services/certificate-authority-crl/certificate-authority-crl-dal";
import { CertKeyAlgorithm, CertStatus } from "../certificate/certificate-types"; import { CertKeyAlgorithm, CertStatus } from "../certificate/certificate-types";
import { TCertificateTemplateDALFactory } from "../certificate-template/certificate-template-dal";
import { validateCertificateDetailsAgainstTemplate } from "../certificate-template/certificate-template-fns";
import { TCertificateAuthorityCertDALFactory } from "./certificate-authority-cert-dal"; import { TCertificateAuthorityCertDALFactory } from "./certificate-authority-cert-dal";
import { TCertificateAuthorityDALFactory } from "./certificate-authority-dal"; import { TCertificateAuthorityDALFactory } from "./certificate-authority-dal";
import { TCertificateAuthorityEstConfigDALFactory } from "./certificate-authority-est-config-dal"; import { TCertificateAuthorityEstConfigDALFactory } from "./certificate-authority-est-config-dal";
@@ -58,13 +63,19 @@ type TCertificateAuthorityServiceFactoryDep = {
TCertificateAuthorityDALFactory, TCertificateAuthorityDALFactory,
"transaction" | "create" | "findById" | "updateById" | "deleteById" | "findOne" "transaction" | "create" | "findById" | "updateById" | "deleteById" | "findOne"
>; >;
certificateAuthorityCertDAL: Pick<TCertificateAuthorityCertDALFactory, "create" | "findOne" | "transaction" | "find">; certificateAuthorityCertDAL: Pick<
TCertificateAuthorityCertDALFactory,
"create" | "findOne" | "transaction" | "find" | "findById"
>;
certificateAuthoritySecretDAL: Pick<TCertificateAuthoritySecretDALFactory, "create" | "findOne">; certificateAuthoritySecretDAL: Pick<TCertificateAuthoritySecretDALFactory, "create" | "findOne">;
certificateAuthorityCrlDAL: Pick<TCertificateAuthorityCrlDALFactory, "create" | "findOne" | "update">; certificateAuthorityCrlDAL: Pick<TCertificateAuthorityCrlDALFactory, "create" | "findOne" | "update">;
certificateTemplateDAL: Pick<TCertificateTemplateDALFactory, "getById">;
certificateAuthorityQueue: TCertificateAuthorityQueueFactory; // TODO: Pick certificateAuthorityQueue: TCertificateAuthorityQueueFactory; // TODO: Pick
certificateAuthorityEstConfigDAL: Pick<TCertificateAuthorityEstConfigDALFactory, "updateById" | "create" | "findOne">; certificateAuthorityEstConfigDAL: Pick<TCertificateAuthorityEstConfigDALFactory, "updateById" | "create" | "findOne">;
certificateDAL: Pick<TCertificateDALFactory, "transaction" | "create" | "find">; certificateDAL: Pick<TCertificateDALFactory, "transaction" | "create" | "find">;
certificateBodyDAL: Pick<TCertificateBodyDALFactory, "create">; certificateBodyDAL: Pick<TCertificateBodyDALFactory, "create">;
pkiCollectionDAL: Pick<TPkiCollectionDALFactory, "findById">;
pkiCollectionItemDAL: Pick<TPkiCollectionItemDALFactory, "create">;
projectDAL: Pick<TProjectDALFactory, "findProjectBySlug" | "findOne" | "updateById" | "findById" | "transaction">; projectDAL: Pick<TProjectDALFactory, "findProjectBySlug" | "findOne" | "updateById" | "findById" | "transaction">;
kmsService: Pick<TKmsServiceFactory, "generateKmsKey" | "encryptWithKmsKey" | "decryptWithKmsKey">; kmsService: Pick<TKmsServiceFactory, "generateKmsKey" | "encryptWithKmsKey" | "decryptWithKmsKey">;
permissionService: Pick<TPermissionServiceFactory, "getProjectPermission">; permissionService: Pick<TPermissionServiceFactory, "getProjectPermission">;
@@ -78,8 +89,11 @@ export const certificateAuthorityServiceFactory = ({
certificateAuthoritySecretDAL, certificateAuthoritySecretDAL,
certificateAuthorityEstConfigDAL, certificateAuthorityEstConfigDAL,
certificateAuthorityCrlDAL, certificateAuthorityCrlDAL,
certificateTemplateDAL,
certificateDAL, certificateDAL,
certificateBodyDAL, certificateBodyDAL,
pkiCollectionDAL,
pkiCollectionItemDAL,
projectDAL, projectDAL,
kmsService, kmsService,
permissionService permissionService
@@ -162,8 +176,7 @@ export const certificateAuthorityServiceFactory = ({
maxPathLength, maxPathLength,
notBefore: notBeforeDate, notBefore: notBeforeDate,
notAfter: notAfterDate, notAfter: notAfterDate,
serialNumber, serialNumber
activeCaCertVersion: 1
}) })
}, },
tx tx
@@ -222,7 +235,7 @@ export const certificateAuthorityServiceFactory = ({
plainText: Buffer.alloc(0) plainText: Buffer.alloc(0)
}); });
await certificateAuthorityCertDAL.create( const caCert = await certificateAuthorityCertDAL.create(
{ {
caId: ca.id, caId: ca.id,
encryptedCertificate, encryptedCertificate,
@@ -232,6 +245,14 @@ export const certificateAuthorityServiceFactory = ({
}, },
tx tx
); );
await certificateAuthorityDAL.updateById(
ca.id,
{
activeCaCertId: caCert.id
},
tx
);
} }
// create empty CRL // create empty CRL
@@ -356,9 +377,7 @@ export const certificateAuthorityServiceFactory = ({
); );
if (ca.type === CaType.ROOT) throw new BadRequestError({ message: "Root CA cannot generate CSR" }); if (ca.type === CaType.ROOT) throw new BadRequestError({ message: "Root CA cannot generate CSR" });
if (ca.activeCaCertId) throw new BadRequestError({ message: "CA already has a certificate installed" });
const [caCert] = await certificateAuthorityCertDAL.find({ caId: ca.id }, { sort: [["version", "desc"]] });
if (caCert) throw new BadRequestError({ message: "CA already has a certificate installed" });
const { caPrivateKey, caPublicKey } = await getCaCredentials({ const { caPrivateKey, caPublicKey } = await getCaCredentials({
caId, caId,
@@ -403,6 +422,8 @@ export const certificateAuthorityServiceFactory = ({
const ca = await certificateAuthorityDAL.findById(caId); const ca = await certificateAuthorityDAL.findById(caId);
if (!ca) throw new BadRequestError({ message: "CA not found" }); if (!ca) throw new BadRequestError({ message: "CA not found" });
if (!ca.activeCaCertId) throw new BadRequestError({ message: "CA does not have a certificate installed" });
const { permission } = await permissionService.getProjectPermission( const { permission } = await permissionService.getProjectPermission(
actor, actor,
actorId, actorId,
@@ -419,8 +440,7 @@ export const certificateAuthorityServiceFactory = ({
if (ca.status === CaStatus.DISABLED) throw new BadRequestError({ message: "CA is disabled" }); if (ca.status === CaStatus.DISABLED) throw new BadRequestError({ message: "CA is disabled" });
// get latest CA certificate // get latest CA certificate
const [caCert] = await certificateAuthorityCertDAL.find({ caId: ca.id }, { sort: [["version", "desc"]] }); const caCert = await certificateAuthorityCertDAL.findById(ca.activeCaCertId);
if (!caCert) throw new BadRequestError({ message: "CA does not have a certificate installed" });
const serialNumber = crypto.randomBytes(32).toString("hex"); const serialNumber = crypto.randomBytes(32).toString("hex");
@@ -498,13 +518,12 @@ export const certificateAuthorityServiceFactory = ({
}); });
await certificateAuthorityDAL.transaction(async (tx) => { await certificateAuthorityDAL.transaction(async (tx) => {
const newActiveCaCertVersion = caCert.version + 1; const newCaCert = await certificateAuthorityCertDAL.create(
await certificateAuthorityCertDAL.create(
{ {
caId: ca.id, caId: ca.id,
encryptedCertificate, encryptedCertificate,
encryptedCertificateChain, encryptedCertificateChain,
version: newActiveCaCertVersion, version: caCert.version + 1,
caSecretId: caSecret.id caSecretId: caSecret.id
}, },
tx tx
@@ -513,7 +532,7 @@ export const certificateAuthorityServiceFactory = ({
await certificateAuthorityDAL.updateById( await certificateAuthorityDAL.updateById(
ca.id, ca.id,
{ {
activeCaCertVersion: newActiveCaCertVersion, activeCaCertId: newCaCert.id,
notBefore: notBeforeDate, notBefore: notBeforeDate,
notAfter: new Date(notAfter) notAfter: new Date(notAfter)
}, },
@@ -542,10 +561,9 @@ export const certificateAuthorityServiceFactory = ({
}); });
// get latest parent CA certificate // get latest parent CA certificate
const [parentCaCert] = await certificateAuthorityCertDAL.find( if (!parentCa.activeCaCertId)
{ caId: parentCa.id }, throw new BadRequestError({ message: "Parent CA does not have a certificate installed" });
{ sort: [["version", "desc"]] } const parentCaCert = await certificateAuthorityCertDAL.findById(parentCa.activeCaCertId);
);
const decryptedParentCaCert = await kmsDecryptor({ const decryptedParentCaCert = await kmsDecryptor({
cipherTextBlob: parentCaCert.encryptedCertificate cipherTextBlob: parentCaCert.encryptedCertificate
@@ -590,7 +608,7 @@ export const certificateAuthorityServiceFactory = ({
const intermediateCert = await x509.X509CertificateGenerator.create({ const intermediateCert = await x509.X509CertificateGenerator.create({
serialNumber, serialNumber,
subject: csrObj.subject, subject: csrObj.subject,
issuer: caCertObj.subject, issuer: parentCaCertObj.subject,
notBefore: notBeforeDate, notBefore: notBeforeDate,
notAfter: new Date(notAfter), notAfter: new Date(notAfter),
signingKey: parentCaPrivateKey, signingKey: parentCaPrivateKey,
@@ -609,7 +627,7 @@ export const certificateAuthorityServiceFactory = ({
ca.maxPathLength === -1 || !ca.maxPathLength ? undefined : ca.maxPathLength, ca.maxPathLength === -1 || !ca.maxPathLength ? undefined : ca.maxPathLength,
true true
), ),
await x509.AuthorityKeyIdentifierExtension.create(caCertObj, false), await x509.AuthorityKeyIdentifierExtension.create(parentCaCertObj, false),
await x509.SubjectKeyIdentifierExtension.create(csrObj.publicKey) await x509.SubjectKeyIdentifierExtension.create(csrObj.publicKey)
] ]
}); });
@@ -619,7 +637,7 @@ export const certificateAuthorityServiceFactory = ({
}); });
const { caCert: parentCaCertificate, caCertChain: parentCaCertChain } = await getCaCertChain({ const { caCert: parentCaCertificate, caCertChain: parentCaCertChain } = await getCaCertChain({
caId: parentCa.id, caCertId: parentCa.activeCaCertId,
certificateAuthorityDAL, certificateAuthorityDAL,
certificateAuthorityCertDAL, certificateAuthorityCertDAL,
projectDAL, projectDAL,
@@ -633,13 +651,12 @@ export const certificateAuthorityServiceFactory = ({
}); });
await certificateAuthorityDAL.transaction(async (tx) => { await certificateAuthorityDAL.transaction(async (tx) => {
const newActiveCaCertVersion = caCert.version + 1; const newCaCert = await certificateAuthorityCertDAL.create(
await certificateAuthorityCertDAL.create(
{ {
caId: ca.id, caId: ca.id,
encryptedCertificate, encryptedCertificate,
encryptedCertificateChain, encryptedCertificateChain,
version: newActiveCaCertVersion, version: caCert.version + 1,
caSecretId: caSecret.id caSecretId: caSecret.id
}, },
tx tx
@@ -648,7 +665,7 @@ export const certificateAuthorityServiceFactory = ({
await certificateAuthorityDAL.updateById( await certificateAuthorityDAL.updateById(
ca.id, ca.id,
{ {
activeCaCertVersion: newActiveCaCertVersion, activeCaCertId: newCaCert.id,
notBefore: notBeforeDate, notBefore: notBeforeDate,
notAfter: new Date(notAfter) notAfter: new Date(notAfter)
}, },
@@ -707,11 +724,11 @@ export const certificateAuthorityServiceFactory = ({
/** /**
* Return current certificate and certificate chain for CA * Return current certificate and certificate chain for CA
* get latest?? ca cert
*/ */
const getCaCert = async ({ caId, actorId, actorAuthMethod, actor, actorOrgId }: TGetCaCertDTO) => { const getCaCert = async ({ caId, actorId, actorAuthMethod, actor, actorOrgId }: TGetCaCertDTO) => {
const ca = await certificateAuthorityDAL.findById(caId); const ca = await certificateAuthorityDAL.findById(caId);
if (!ca) throw new BadRequestError({ message: "CA not found" }); if (!ca) throw new BadRequestError({ message: "CA not found" });
if (!ca.activeCaCertId) throw new BadRequestError({ message: "CA does not have a certificate installed" });
const { permission } = await permissionService.getProjectPermission( const { permission } = await permissionService.getProjectPermission(
actor, actor,
@@ -727,7 +744,7 @@ export const certificateAuthorityServiceFactory = ({
); );
const { caCert, caCertChain, serialNumber } = await getCaCertChain({ const { caCert, caCertChain, serialNumber } = await getCaCertChain({
caId, caCertId: ca.activeCaCertId,
certificateAuthorityDAL, certificateAuthorityDAL,
certificateAuthorityCertDAL, certificateAuthorityCertDAL,
projectDAL, projectDAL,
@@ -773,9 +790,9 @@ export const certificateAuthorityServiceFactory = ({
); );
if (ca.status === CaStatus.DISABLED) throw new BadRequestError({ message: "CA is disabled" }); if (ca.status === CaStatus.DISABLED) throw new BadRequestError({ message: "CA is disabled" });
if (!ca.activeCaCertId) throw new BadRequestError({ message: "CA does not have a certificate installed" });
const [caCert] = await certificateAuthorityCertDAL.find({ caId: ca.id }, { sort: [["version", "desc"]] }); const caCert = await certificateAuthorityCertDAL.findById(ca.activeCaCertId);
if (!caCert) throw new BadRequestError({ message: "CA does not have a certificate installed" });
if (ca.notAfter && new Date() > new Date(ca.notAfter)) { if (ca.notAfter && new Date() > new Date(ca.notAfter)) {
throw new BadRequestError({ message: "CA is expired" }); throw new BadRequestError({ message: "CA is expired" });
@@ -863,7 +880,7 @@ export const certificateAuthorityServiceFactory = ({
}); });
const { caCert: issuingCaCertificate, caCertChain } = await getCaCertChain({ const { caCert: issuingCaCertificate, caCertChain } = await getCaCertChain({
caId, caCertId: ca.activeCaCertId,
certificateAuthorityDAL, certificateAuthorityDAL,
certificateAuthorityCertDAL, certificateAuthorityCertDAL,
projectDAL, projectDAL,
@@ -909,8 +926,7 @@ export const certificateAuthorityServiceFactory = ({
ProjectPermissionSub.CertificateAuthorities ProjectPermissionSub.CertificateAuthorities
); );
const [caCert] = await certificateAuthorityCertDAL.find({ caId: ca.id }, { sort: [["version", "desc"]] }); if (ca.activeCaCertId) throw new BadRequestError({ message: "CA has already imported a certificate" });
if (caCert) throw new BadRequestError({ message: "CA has already imported a certificate" });
const certObj = new x509.X509Certificate(certificate); const certObj = new x509.X509Certificate(certificate);
const maxPathLength = certObj.getExtension(x509.BasicConstraintsExtension)?.pathLength; const maxPathLength = certObj.getExtension(x509.BasicConstraintsExtension)?.pathLength;
@@ -976,7 +992,7 @@ export const certificateAuthorityServiceFactory = ({
} }
await certificateAuthorityCertDAL.transaction(async (tx) => { await certificateAuthorityCertDAL.transaction(async (tx) => {
await certificateAuthorityCertDAL.create( const newCaCert = await certificateAuthorityCertDAL.create(
{ {
caId: ca.id, caId: ca.id,
encryptedCertificate, encryptedCertificate,
@@ -995,7 +1011,8 @@ export const certificateAuthorityServiceFactory = ({
notBefore: new Date(certObj.notBefore), notBefore: new Date(certObj.notBefore),
notAfter: new Date(certObj.notAfter), notAfter: new Date(certObj.notAfter),
serialNumber: certObj.serialNumber, serialNumber: certObj.serialNumber,
parentCaId: parentCa?.id parentCaId: parentCa?.id,
activeCaCertId: newCaCert.id
}, },
tx tx
); );
@@ -1010,6 +1027,8 @@ export const certificateAuthorityServiceFactory = ({
*/ */
const issueCertFromCa = async ({ const issueCertFromCa = async ({
caId, caId,
certificateTemplateId,
pkiCollectionId,
friendlyName, friendlyName,
commonName, commonName,
altNames, altNames,
@@ -1021,8 +1040,27 @@ export const certificateAuthorityServiceFactory = ({
actor, actor,
actorOrgId actorOrgId
}: TIssueCertFromCaDTO) => { }: TIssueCertFromCaDTO) => {
const ca = await certificateAuthorityDAL.findById(caId); let ca: TCertificateAuthorities | undefined;
if (!ca) throw new BadRequestError({ message: "CA not found" }); let certificateTemplate: TCertificateTemplates | undefined;
let collectionId = pkiCollectionId;
if (caId) {
ca = await certificateAuthorityDAL.findById(caId);
} else if (certificateTemplateId) {
certificateTemplate = await certificateTemplateDAL.getById(certificateTemplateId);
if (!certificateTemplate) {
throw new NotFoundError({
message: "Certificate template not found"
});
}
collectionId = certificateTemplate.pkiCollectionId as string;
ca = await certificateAuthorityDAL.findById(certificateTemplate.caId);
}
if (!ca) {
throw new BadRequestError({ message: "CA not found" });
}
const { permission } = await permissionService.getProjectPermission( const { permission } = await permissionService.getProjectPermission(
actor, actor,
@@ -1035,14 +1073,20 @@ export const certificateAuthorityServiceFactory = ({
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Create, ProjectPermissionSub.Certificates); ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Create, ProjectPermissionSub.Certificates);
if (ca.status === CaStatus.DISABLED) throw new BadRequestError({ message: "CA is disabled" }); if (ca.status === CaStatus.DISABLED) throw new BadRequestError({ message: "CA is disabled" });
if (!ca.activeCaCertId) throw new BadRequestError({ message: "CA does not have a certificate installed" });
const [caCert] = await certificateAuthorityCertDAL.find({ caId: ca.id }, { sort: [["version", "desc"]] }); const caCert = await certificateAuthorityCertDAL.findById(ca.activeCaCertId);
if (!caCert) throw new BadRequestError({ message: "CA does not have a certificate installed" });
if (ca.notAfter && new Date() > new Date(ca.notAfter)) { if (ca.notAfter && new Date() > new Date(ca.notAfter)) {
throw new BadRequestError({ message: "CA is expired" }); throw new BadRequestError({ message: "CA is expired" });
} }
// check PKI collection
if (collectionId) {
const pkiCollection = await pkiCollectionDAL.findById(collectionId);
if (!pkiCollection) throw new NotFoundError({ message: "PKI collection not found" });
if (pkiCollection.projectId !== ca.projectId) throw new BadRequestError({ message: "Invalid PKI collection" });
}
const certificateManagerKmsId = await getProjectKmsCertificateKeyId({ const certificateManagerKmsId = await getProjectKmsCertificateKeyId({
projectId: ca.projectId, projectId: ca.projectId,
projectDAL, projectDAL,
@@ -1111,11 +1155,13 @@ export const certificateAuthorityServiceFactory = ({
await x509.SubjectKeyIdentifierExtension.create(csrObj.publicKey) await x509.SubjectKeyIdentifierExtension.create(csrObj.publicKey)
]; ];
let altNamesArray: {
type: "email" | "dns";
value: string;
}[] = [];
if (altNames) { if (altNames) {
const altNamesArray: { altNamesArray = altNames
type: "email" | "dns";
value: string;
}[] = altNames
.split(",") .split(",")
.map((name) => name.trim()) .map((name) => name.trim())
.map((altName) => { .map((altName) => {
@@ -1143,6 +1189,18 @@ export const certificateAuthorityServiceFactory = ({
extensions.push(altNamesExtension); extensions.push(altNamesExtension);
} }
if (certificateTemplate) {
validateCertificateDetailsAgainstTemplate(
{
commonName,
notBeforeDate,
notAfterDate,
altNames: altNamesArray.map((entry) => entry.value)
},
certificateTemplate
);
}
const serialNumber = crypto.randomBytes(32).toString("hex"); const serialNumber = crypto.randomBytes(32).toString("hex");
const leafCert = await x509.X509CertificateGenerator.create({ const leafCert = await x509.X509CertificateGenerator.create({
serialNumber, serialNumber,
@@ -1169,7 +1227,9 @@ export const certificateAuthorityServiceFactory = ({
await certificateDAL.transaction(async (tx) => { await certificateDAL.transaction(async (tx) => {
const cert = await certificateDAL.create( const cert = await certificateDAL.create(
{ {
caId: ca.id, caId: (ca as TCertificateAuthorities).id,
caCertId: caCert.id,
certificateTemplateId: certificateTemplate?.id,
status: CertStatus.ACTIVE, status: CertStatus.ACTIVE,
friendlyName: friendlyName || commonName, friendlyName: friendlyName || commonName,
commonName, commonName,
@@ -1189,11 +1249,21 @@ export const certificateAuthorityServiceFactory = ({
tx tx
); );
if (collectionId) {
await pkiCollectionItemDAL.create(
{
pkiCollectionId: collectionId,
certId: cert.id
},
tx
);
}
return cert; return cert;
}); });
const { caCert: issuingCaCertificate, caCertChain } = await getCaCertChain({ const { caCert: issuingCaCertificate, caCertChain } = await getCaCertChain({
caId: ca.id, caCertId: caCert.id,
certificateAuthorityDAL, certificateAuthorityDAL,
certificateAuthorityCertDAL, certificateAuthorityCertDAL,
projectDAL, projectDAL,
@@ -1215,8 +1285,41 @@ export const certificateAuthorityServiceFactory = ({
* Note: CSR is generated externally and submitted to Infisical. * Note: CSR is generated externally and submitted to Infisical.
*/ */
const signCertFromCa = async (dto: TSignCertFromCaDTO) => { const signCertFromCa = async (dto: TSignCertFromCaDTO) => {
const ca = await certificateAuthorityDAL.findById(dto.caId); let ca: TCertificateAuthorities | undefined;
if (!ca) throw new BadRequestError({ message: "CA not found" }); let certificateTemplate: TCertificateTemplates | undefined;
const {
caId,
certificateTemplateId,
csr,
pkiCollectionId,
friendlyName,
commonName,
altNames,
ttl,
notBefore,
notAfter
} = dto;
let collectionId = pkiCollectionId;
if (caId) {
ca = await certificateAuthorityDAL.findById(caId);
} else if (certificateTemplateId) {
certificateTemplate = await certificateTemplateDAL.getById(certificateTemplateId);
if (!certificateTemplate) {
throw new NotFoundError({
message: "Certificate template not found"
});
}
collectionId = certificateTemplate.pkiCollectionId as string;
ca = await certificateAuthorityDAL.findById(certificateTemplate.caId);
}
if (!ca) {
throw new BadRequestError({ message: "CA not found" });
}
if (!dto.isInternal) { if (!dto.isInternal) {
const { permission } = await permissionService.getProjectPermission( const { permission } = await permissionService.getProjectPermission(
@@ -1233,17 +1336,22 @@ export const certificateAuthorityServiceFactory = ({
); );
} }
const { csr, friendlyName, commonName, altNames, ttl, notBefore, notAfter } = dto;
if (ca.status === CaStatus.DISABLED) throw new BadRequestError({ message: "CA is disabled" }); if (ca.status === CaStatus.DISABLED) throw new BadRequestError({ message: "CA is disabled" });
if (!ca.activeCaCertId) throw new BadRequestError({ message: "CA does not have a certificate installed" });
const [caCert] = await certificateAuthorityCertDAL.find({ caId: ca.id }, { sort: [["version", "desc"]] }); const caCert = await certificateAuthorityCertDAL.findById(ca.activeCaCertId);
if (!caCert) throw new BadRequestError({ message: "CA does not have a certificate installed" });
if (ca.notAfter && new Date() > new Date(ca.notAfter)) { if (ca.notAfter && new Date() > new Date(ca.notAfter)) {
throw new BadRequestError({ message: "CA is expired" }); throw new BadRequestError({ message: "CA is expired" });
} }
// check PKI collection
if (pkiCollectionId) {
const pkiCollection = await pkiCollectionDAL.findById(pkiCollectionId);
if (!pkiCollection) throw new NotFoundError({ message: "PKI collection not found" });
if (pkiCollection.projectId !== ca.projectId) throw new BadRequestError({ message: "Invalid PKI collection" });
}
const certificateManagerKmsId = await getProjectKmsCertificateKeyId({ const certificateManagerKmsId = await getProjectKmsCertificateKeyId({
projectId: ca.projectId, projectId: ca.projectId,
projectDAL, projectDAL,
@@ -1311,11 +1419,12 @@ export const certificateAuthorityServiceFactory = ({
await x509.SubjectKeyIdentifierExtension.create(csrObj.publicKey) await x509.SubjectKeyIdentifierExtension.create(csrObj.publicKey)
]; ];
let altNamesArray: {
type: "email" | "dns";
value: string;
}[] = [];
if (altNames) { if (altNames) {
const altNamesArray: { altNamesArray = altNames
type: "email" | "dns";
value: string;
}[] = altNames
.split(",") .split(",")
.map((name) => name.trim()) .map((name) => name.trim())
.map((altName) => { .map((altName) => {
@@ -1343,6 +1452,18 @@ export const certificateAuthorityServiceFactory = ({
extensions.push(altNamesExtension); extensions.push(altNamesExtension);
} }
if (certificateTemplate) {
validateCertificateDetailsAgainstTemplate(
{
commonName: cn,
notBeforeDate,
notAfterDate,
altNames: altNamesArray.map((entry) => entry.value)
},
certificateTemplate
);
}
const serialNumber = crypto.randomBytes(32).toString("hex"); const serialNumber = crypto.randomBytes(32).toString("hex");
const leafCert = await x509.X509CertificateGenerator.create({ const leafCert = await x509.X509CertificateGenerator.create({
serialNumber, serialNumber,
@@ -1366,7 +1487,9 @@ export const certificateAuthorityServiceFactory = ({
await certificateDAL.transaction(async (tx) => { await certificateDAL.transaction(async (tx) => {
const cert = await certificateDAL.create( const cert = await certificateDAL.create(
{ {
caId: ca.id, caId: (ca as TCertificateAuthorities).id,
caCertId: caCert.id,
certificateTemplateId: certificateTemplate?.id,
status: CertStatus.ACTIVE, status: CertStatus.ACTIVE,
friendlyName: friendlyName || csrObj.subject, friendlyName: friendlyName || csrObj.subject,
commonName: cn, commonName: cn,
@@ -1386,11 +1509,21 @@ export const certificateAuthorityServiceFactory = ({
tx tx
); );
if (collectionId) {
await pkiCollectionItemDAL.create(
{
pkiCollectionId: collectionId,
certId: cert.id
},
tx
);
}
return cert; return cert;
}); });
const { caCert: issuingCaCertificate, caCertChain } = await getCaCertChain({ const { caCert: issuingCaCertificate, caCertChain } = await getCaCertChain({
caId: ca.id, caCertId: ca.activeCaCertId,
certificateAuthorityDAL, certificateAuthorityDAL,
certificateAuthorityCertDAL, certificateAuthorityCertDAL,
projectDAL, projectDAL,
@@ -86,7 +86,9 @@ export type TImportCertToCaDTO = {
} & Omit<TProjectPermission, "projectId">; } & Omit<TProjectPermission, "projectId">;
export type TIssueCertFromCaDTO = { export type TIssueCertFromCaDTO = {
caId: string; caId?: string;
certificateTemplateId?: string;
pkiCollectionId?: string;
friendlyName?: string; friendlyName?: string;
commonName: string; commonName: string;
altNames: string; altNames: string;
@@ -98,8 +100,10 @@ export type TIssueCertFromCaDTO = {
export type TSignCertFromCaDTO = export type TSignCertFromCaDTO =
| { | {
isInternal: true; isInternal: true;
caId: string; caId?: string;
csr: string; csr: string;
certificateTemplateId?: string;
pkiCollectionId?: string;
friendlyName?: string; friendlyName?: string;
commonName?: string; commonName?: string;
altNames: string; altNames: string;
@@ -109,8 +113,10 @@ export type TSignCertFromCaDTO =
} }
| ({ | ({
isInternal: false; isInternal: false;
caId: string; caId?: string;
csr: string; csr: string;
certificateTemplateId?: string;
pkiCollectionId?: string;
friendlyName?: string; friendlyName?: string;
commonName?: string; commonName?: string;
altNames: string; altNames: string;
@@ -145,9 +151,9 @@ export type TGetCaCertChainsDTO = {
}; };
export type TGetCaCertChainDTO = { export type TGetCaCertChainDTO = {
caId: string; caCertId: string;
certificateAuthorityDAL: Pick<TCertificateAuthorityDALFactory, "findById">; certificateAuthorityDAL: Pick<TCertificateAuthorityDALFactory, "findById">;
certificateAuthorityCertDAL: Pick<TCertificateAuthorityCertDALFactory, "findOne">; certificateAuthorityCertDAL: Pick<TCertificateAuthorityCertDALFactory, "findById">;
projectDAL: Pick<TProjectDALFactory, "findOne" | "updateById" | "transaction">; projectDAL: Pick<TProjectDALFactory, "findOne" | "updateById" | "transaction">;
kmsService: Pick<TKmsServiceFactory, "decryptWithKmsKey" | "generateKmsKey">; kmsService: Pick<TKmsServiceFactory, "decryptWithKmsKey" | "generateKmsKey">;
}; };
@@ -0,0 +1,57 @@
import { TDbClient } from "@app/db";
import { TableName } from "@app/db/schemas";
import { DatabaseError } from "@app/lib/errors";
import { ormify, selectAllTableCols } from "@app/lib/knex";
export type TCertificateTemplateDALFactory = ReturnType<typeof certificateTemplateDALFactory>;
export const certificateTemplateDALFactory = (db: TDbClient) => {
const certificateTemplateOrm = ormify(db, TableName.CertificateTemplate);
const getCertTemplatesByProjectId = async (projectId: string) => {
try {
const certTemplates = await db
.replicaNode()(TableName.CertificateTemplate)
.join(
TableName.CertificateAuthority,
`${TableName.CertificateAuthority}.id`,
`${TableName.CertificateTemplate}.caId`
)
.where(`${TableName.CertificateAuthority}.projectId`, "=", projectId)
.select(selectAllTableCols(TableName.CertificateTemplate))
.select(
db.ref("friendlyName").as("caName").withSchema(TableName.CertificateAuthority),
db.ref("projectId").withSchema(TableName.CertificateAuthority)
);
return certTemplates;
} catch (error) {
throw new DatabaseError({ error, name: "Get certificate templates by project ID" });
}
};
const getById = async (id: string) => {
try {
const certTemplate = await db
.replicaNode()(TableName.CertificateTemplate)
.join(
TableName.CertificateAuthority,
`${TableName.CertificateAuthority}.id`,
`${TableName.CertificateTemplate}.caId`
)
.where(`${TableName.CertificateTemplate}.id`, "=", id)
.select(selectAllTableCols(TableName.CertificateTemplate))
.select(
db.ref("projectId").withSchema(TableName.CertificateAuthority),
db.ref("friendlyName").as("caName").withSchema(TableName.CertificateAuthority)
)
.first();
return certTemplate;
} catch (error) {
throw new DatabaseError({ error, name: "Get certificate template by ID" });
}
};
return { ...certificateTemplateOrm, getCertTemplatesByProjectId, getById };
};
@@ -0,0 +1,36 @@
import ms from "ms";
import { TCertificateTemplates } from "@app/db/schemas";
import { BadRequestError } from "@app/lib/errors";
export const validateCertificateDetailsAgainstTemplate = (
cert: {
commonName: string;
notBeforeDate: Date;
notAfterDate: Date;
altNames: string[];
},
template: TCertificateTemplates
) => {
const commonNameRegex = new RegExp(template.commonName);
if (!commonNameRegex.test(cert.commonName)) {
throw new BadRequestError({
message: "Invalid common name based on template policy"
});
}
if (cert.notAfterDate.getTime() - cert.notBeforeDate.getTime() > ms(template.ttl)) {
throw new BadRequestError({
message: "Invalid validity date based on template policy"
});
}
const subjectAlternativeNameRegex = new RegExp(template.subjectAlternativeName);
cert.altNames.forEach((altName) => {
if (!subjectAlternativeNameRegex.test(altName)) {
throw new BadRequestError({
message: "Invalid subject alternative name based on template policy"
});
}
});
};
@@ -0,0 +1,18 @@
import z from "zod";
import { CertificateTemplatesSchema } from "@app/db/schemas";
export const sanitizedCertificateTemplate = CertificateTemplatesSchema.pick({
id: true,
caId: true,
name: true,
commonName: true,
subjectAlternativeName: true,
pkiCollectionId: true,
ttl: true
}).merge(
z.object({
projectId: z.string(),
caName: z.string()
})
);
@@ -0,0 +1,196 @@
import { ForbiddenError } from "@casl/ability";
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service";
import { ProjectPermissionActions, ProjectPermissionSub } from "@app/ee/services/permission/project-permission";
import { BadRequestError, NotFoundError } from "@app/lib/errors";
import { TCertificateAuthorityDALFactory } from "../certificate-authority/certificate-authority-dal";
import { TCertificateTemplateDALFactory } from "./certificate-template-dal";
import {
TCreateCertTemplateDTO,
TDeleteCertTemplateDTO,
TGetCertTemplateDTO,
TUpdateCertTemplateDTO
} from "./certificate-template-types";
type TCertificateTemplateServiceFactoryDep = {
certificateTemplateDAL: TCertificateTemplateDALFactory;
certificateAuthorityDAL: Pick<TCertificateAuthorityDALFactory, "findById">;
permissionService: Pick<TPermissionServiceFactory, "getProjectPermission">;
};
export type TCertificateTemplateServiceFactory = ReturnType<typeof certificateTemplateServiceFactory>;
export const certificateTemplateServiceFactory = ({
certificateTemplateDAL,
certificateAuthorityDAL,
permissionService
}: TCertificateTemplateServiceFactoryDep) => {
const createCertTemplate = async ({
caId,
pkiCollectionId,
name,
commonName,
subjectAlternativeName,
ttl,
actorId,
actorAuthMethod,
actor,
actorOrgId
}: TCreateCertTemplateDTO) => {
const ca = await certificateAuthorityDAL.findById(caId);
if (!ca) {
throw new NotFoundError({
message: "CA not found"
});
}
const { permission } = await permissionService.getProjectPermission(
actor,
actorId,
ca.projectId,
actorAuthMethod,
actorOrgId
);
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Create,
ProjectPermissionSub.CertificateTemplates
);
const { id } = await certificateTemplateDAL.create({
caId,
pkiCollectionId,
name,
commonName,
subjectAlternativeName,
ttl
});
const certificateTemplate = await certificateTemplateDAL.getById(id);
if (!certificateTemplate) {
throw new NotFoundError({
message: "Certificate template not found"
});
}
return certificateTemplate;
};
const updateCertTemplate = async ({
id,
caId,
pkiCollectionId,
name,
commonName,
subjectAlternativeName,
ttl,
actorId,
actorAuthMethod,
actor,
actorOrgId
}: TUpdateCertTemplateDTO) => {
const certTemplate = await certificateTemplateDAL.getById(id);
if (!certTemplate) {
throw new NotFoundError({
message: "Certificate template not found."
});
}
const { permission } = await permissionService.getProjectPermission(
actor,
actorId,
certTemplate.projectId,
actorAuthMethod,
actorOrgId
);
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Edit,
ProjectPermissionSub.CertificateTemplates
);
if (caId) {
const ca = await certificateAuthorityDAL.findById(caId);
if (!ca || ca.projectId !== certTemplate.projectId) {
throw new BadRequestError({
message: "Invalid CA"
});
}
}
await certificateTemplateDAL.updateById(certTemplate.id, {
caId,
pkiCollectionId,
commonName,
subjectAlternativeName,
name,
ttl
});
const updatedTemplate = await certificateTemplateDAL.getById(id);
if (!updatedTemplate) {
throw new NotFoundError({
message: "Certificate template not found"
});
}
return updatedTemplate;
};
const deleteCertTemplate = async ({ id, actorId, actorAuthMethod, actor, actorOrgId }: TDeleteCertTemplateDTO) => {
const certTemplate = await certificateTemplateDAL.getById(id);
if (!certTemplate) {
throw new NotFoundError({
message: "Certificate template not found."
});
}
const { permission } = await permissionService.getProjectPermission(
actor,
actorId,
certTemplate.projectId,
actorAuthMethod,
actorOrgId
);
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Delete,
ProjectPermissionSub.CertificateTemplates
);
await certificateTemplateDAL.deleteById(certTemplate.id);
return certTemplate;
};
const getCertTemplate = async ({ id, actorId, actorAuthMethod, actor, actorOrgId }: TGetCertTemplateDTO) => {
const certTemplate = await certificateTemplateDAL.getById(id);
if (!certTemplate) {
throw new NotFoundError({
message: "Certificate template not found."
});
}
const { permission } = await permissionService.getProjectPermission(
actor,
actorId,
certTemplate.projectId,
actorAuthMethod,
actorOrgId
);
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Read,
ProjectPermissionSub.CertificateTemplates
);
return certTemplate;
};
return {
createCertTemplate,
getCertTemplate,
deleteCertTemplate,
updateCertTemplate
};
};
@@ -0,0 +1,28 @@
import { TProjectPermission } from "@app/lib/types";
export type TCreateCertTemplateDTO = {
caId: string;
pkiCollectionId?: string;
name: string;
commonName: string;
subjectAlternativeName: string;
ttl: string;
} & Omit<TProjectPermission, "projectId">;
export type TUpdateCertTemplateDTO = {
id: string;
caId?: string;
pkiCollectionId?: string;
name?: string;
commonName?: string;
subjectAlternativeName?: string;
ttl?: string;
} & Omit<TProjectPermission, "projectId">;
export type TGetCertTemplateDTO = {
id: string;
} & Omit<TProjectPermission, "projectId">;
export type TDeleteCertTemplateDTO = {
id: string;
} & Omit<TProjectPermission, "projectId">;
@@ -0,0 +1,14 @@
import safe from "safe-regex";
import z from "zod";
export const validateTemplateRegexField = z
.string()
.min(1)
.max(100)
.regex(/^[a-zA-Z0-9 *@\-\\.\\]+$/, {
message: "Invalid pattern: only alphanumeric characters, spaces, *, ., @, -, and \\ are allowed."
})
// we ensure that the inputted pattern is computationally safe by limiting star height to 1
.refine((v) => safe(v), {
message: "Unsafe REGEX pattern"
});
@@ -21,7 +21,7 @@ type TCertificateServiceFactoryDep = {
certificateDAL: Pick<TCertificateDALFactory, "findOne" | "deleteById" | "update" | "find">; certificateDAL: Pick<TCertificateDALFactory, "findOne" | "deleteById" | "update" | "find">;
certificateBodyDAL: Pick<TCertificateBodyDALFactory, "findOne">; certificateBodyDAL: Pick<TCertificateBodyDALFactory, "findOne">;
certificateAuthorityDAL: Pick<TCertificateAuthorityDALFactory, "findById">; certificateAuthorityDAL: Pick<TCertificateAuthorityDALFactory, "findById">;
certificateAuthorityCertDAL: Pick<TCertificateAuthorityCertDALFactory, "findOne">; certificateAuthorityCertDAL: Pick<TCertificateAuthorityCertDALFactory, "findById">;
certificateAuthorityCrlDAL: Pick<TCertificateAuthorityCrlDALFactory, "update">; certificateAuthorityCrlDAL: Pick<TCertificateAuthorityCrlDALFactory, "update">;
certificateAuthoritySecretDAL: Pick<TCertificateAuthoritySecretDALFactory, "findOne">; certificateAuthoritySecretDAL: Pick<TCertificateAuthoritySecretDALFactory, "findOne">;
projectDAL: Pick<TProjectDALFactory, "findOne" | "updateById" | "findById" | "transaction">; projectDAL: Pick<TProjectDALFactory, "findOne" | "updateById" | "findById" | "transaction">;
@@ -180,7 +180,7 @@ export const certificateServiceFactory = ({
const certObj = new x509.X509Certificate(decryptedCert); const certObj = new x509.X509Certificate(decryptedCert);
const { caCert, caCertChain } = await getCaCertChain({ const { caCert, caCertChain } = await getCaCertChain({
caId: ca.id, caCertId: cert.caCertId,
certificateAuthorityDAL, certificateAuthorityDAL,
certificateAuthorityCertDAL, certificateAuthorityCertDAL,
projectDAL, projectDAL,
@@ -4,6 +4,7 @@ import { TDbClient } from "@app/db";
import { TableName } from "@app/db/schemas"; import { TableName } from "@app/db/schemas";
import { DatabaseError } from "@app/lib/errors"; import { DatabaseError } from "@app/lib/errors";
import { ormify } from "@app/lib/knex"; import { ormify } from "@app/lib/knex";
import { logger } from "@app/lib/logger";
export type TIdentityUaClientSecretDALFactory = ReturnType<typeof identityUaClientSecretDALFactory>; export type TIdentityUaClientSecretDALFactory = ReturnType<typeof identityUaClientSecretDALFactory>;
@@ -23,5 +24,55 @@ export const identityUaClientSecretDALFactory = (db: TDbClient) => {
} }
}; };
return { ...uaClientSecretOrm, incrementUsage }; const removeExpiredClientSecrets = async (tx?: Knex) => {
const BATCH_SIZE = 10000;
const MAX_RETRY_ON_FAILURE = 3;
let deletedClientSecret: { id: string }[] = [];
let numberOfRetryOnFailure = 0;
do {
try {
const findExpiredClientSecretQuery = (tx || db)(TableName.IdentityUaClientSecret)
.where({
isClientSecretRevoked: true
})
.orWhere((qb) => {
void qb
.where("clientSecretNumUses", ">", 0)
.andWhere(
"clientSecretNumUses",
">=",
db.ref("clientSecretNumUsesLimit").withSchema(TableName.IdentityUaClientSecret)
);
})
.orWhere((qb) => {
void qb
.where("clientSecretTTL", ">", 0)
.andWhereRaw(
`"${TableName.IdentityUaClientSecret}"."createdAt" + make_interval(secs => "${TableName.IdentityUaClientSecret}"."clientSecretTTL") < NOW()`
);
})
.select("id")
.limit(BATCH_SIZE);
// eslint-disable-next-line no-await-in-loop
deletedClientSecret = await (tx || db)(TableName.IdentityUaClientSecret)
.whereIn("id", findExpiredClientSecretQuery)
.del()
.returning("id");
numberOfRetryOnFailure = 0; // reset
} catch (error) {
numberOfRetryOnFailure += 1;
logger.error(error, "Failed to delete client secret on pruning");
} finally {
// eslint-disable-next-line no-await-in-loop
await new Promise((resolve) => {
setTimeout(resolve, 10); // time to breathe for db
});
}
} while (deletedClientSecret.length > 0 || numberOfRetryOnFailure < MAX_RETRY_ON_FAILURE);
};
return { ...uaClientSecretOrm, incrementUsage, removeExpiredClientSecrets };
}; };
@@ -0,0 +1,357 @@
import { retry } from "@octokit/plugin-retry";
import { Octokit } from "@octokit/rest";
import { TIntegrationAuths, TIntegrations } from "@app/db/schemas";
import { decryptSymmetric128BitHexKeyUTF8 } from "@app/lib/crypto";
import { BadRequestError, NotFoundError } from "@app/lib/errors";
import { logger } from "@app/lib/logger";
import { IntegrationMetadataSchema } from "../integration/integration-schema";
import { TKmsServiceFactory } from "../kms/kms-service";
import { KmsDataKey } from "../kms/kms-types";
import { TProjectBotServiceFactory } from "../project-bot/project-bot-service";
import { TSecretDALFactory } from "../secret/secret-dal";
import { TSecretFolderDALFactory } from "../secret-folder/secret-folder-dal";
import { TSecretImportDALFactory } from "../secret-import/secret-import-dal";
import { fnSecretsV2FromImports } from "../secret-import/secret-import-fns";
import { TSecretV2BridgeDALFactory } from "../secret-v2-bridge/secret-v2-bridge-dal";
import { TIntegrationAuthServiceFactory } from "./integration-auth-service";
import { Integrations } from "./integration-list";
const MAX_SYNC_SECRET_DEPTH = 5;
/**
* Return the secrets in a given [folderId] including secrets from
* nested imported folders recursively.
*/
const getIntegrationSecretsV2 = async (
dto: {
projectId: string;
environment: string;
folderId: string;
depth: number;
decryptor: (value: Buffer | null | undefined) => string;
},
secretV2BridgeDAL: Pick<TSecretV2BridgeDALFactory, "find" | "findByFolderId">,
folderDAL: Pick<TSecretFolderDALFactory, "findByManySecretPath">,
secretImportDAL: Pick<TSecretImportDALFactory, "find" | "findByFolderIds">
) => {
const content: Record<string, boolean> = {};
if (dto.depth > MAX_SYNC_SECRET_DEPTH) {
logger.info(
`getIntegrationSecrets: secret depth exceeded for [projectId=${dto.projectId}] [folderId=${dto.folderId}] [depth=${dto.depth}]`
);
return content;
}
// process secrets in current folder
const secrets = await secretV2BridgeDAL.findByFolderId(dto.folderId);
secrets.forEach((secret) => {
const secretKey = secret.key;
content[secretKey] = true;
});
// check if current folder has any imports from other folders
const secretImports = await secretImportDAL.find({ folderId: dto.folderId, isReplication: false });
// if no imports then return secrets in the current folder
if (!secretImports.length) return content;
const importedSecrets = await fnSecretsV2FromImports({
decryptor: dto.decryptor,
folderDAL,
secretDAL: secretV2BridgeDAL,
secretImportDAL,
allowedImports: secretImports
});
for (let i = importedSecrets.length - 1; i >= 0; i -= 1) {
for (let j = 0; j < importedSecrets[i].secrets.length; j += 1) {
const importedSecret = importedSecrets[i].secrets[j];
if (!content[importedSecret.key]) {
content[importedSecret.key] = true;
}
}
}
return content;
};
/**
* Return the secrets in a given [folderId] including secrets from
* nested imported folders recursively.
*/
const getIntegrationSecretsV1 = async (
dto: {
projectId: string;
environment: string;
folderId: string;
key: string;
depth: number;
},
secretDAL: Pick<TSecretDALFactory, "findByFolderId">,
folderDAL: Pick<TSecretFolderDALFactory, "findByManySecretPath">,
secretImportDAL: Pick<TSecretImportDALFactory, "find" | "findByFolderIds">
) => {
let content: Record<string, boolean> = {};
if (dto.depth > MAX_SYNC_SECRET_DEPTH) {
logger.info(
`getIntegrationSecrets: secret depth exceeded for [projectId=${dto.projectId}] [folderId=${dto.folderId}] [depth=${dto.depth}]`
);
return content;
}
// process secrets in current folder
const secrets = await secretDAL.findByFolderId(dto.folderId);
secrets.forEach((secret) => {
const secretKey = decryptSymmetric128BitHexKeyUTF8({
ciphertext: secret.secretKeyCiphertext,
iv: secret.secretKeyIV,
tag: secret.secretKeyTag,
key: dto.key
});
content[secretKey] = true;
});
// check if current folder has any imports from other folders
const secretImport = await secretImportDAL.find({ folderId: dto.folderId, isReplication: false });
// if no imports then return secrets in the current folder
if (!secretImport) return content;
const importedFolders = await folderDAL.findByManySecretPath(
secretImport.map(({ importEnv, importPath }) => ({
envId: importEnv.id,
secretPath: importPath
}))
);
for await (const folder of importedFolders) {
if (folder) {
// get secrets contained in each imported folder by recursively calling
// this function against the imported folder
const importedSecrets = await getIntegrationSecretsV1(
{
environment: dto.environment,
projectId: dto.projectId,
folderId: folder.id,
key: dto.key,
depth: dto.depth + 1
},
secretDAL,
folderDAL,
secretImportDAL
);
// add the imported secrets to the current folder secrets
content = { ...importedSecrets, ...content };
}
}
return content;
};
export const deleteGithubSecrets = async ({
integration,
secrets,
accessToken
}: {
integration: Omit<TIntegrations, "envId">;
secrets: Record<string, boolean>;
accessToken: string;
}) => {
interface GitHubSecret {
name: string;
created_at: string;
updated_at: string;
visibility?: "all" | "private" | "selected";
selected_repositories_url?: string | undefined;
}
const OctokitWithRetry = Octokit.plugin(retry);
const octokit = new OctokitWithRetry({
auth: accessToken
});
enum GithubScope {
Repo = "github-repo",
Org = "github-org",
Env = "github-env"
}
let encryptedGithubSecrets: GitHubSecret[];
switch (integration.scope) {
case GithubScope.Org: {
encryptedGithubSecrets = (
await octokit.request("GET /orgs/{org}/actions/secrets", {
org: integration.owner as string
})
).data.secrets;
break;
}
case GithubScope.Env: {
encryptedGithubSecrets = (
await octokit.request("GET /repositories/{repository_id}/environments/{environment_name}/secrets", {
repository_id: Number(integration.appId),
environment_name: integration.targetEnvironmentId as string
})
).data.secrets;
break;
}
default: {
encryptedGithubSecrets = (
await octokit.request("GET /repos/{owner}/{repo}/actions/secrets", {
owner: integration.owner as string,
repo: integration.app as string
})
).data.secrets;
break;
}
}
for await (const encryptedSecret of encryptedGithubSecrets) {
if (encryptedSecret.name in secrets) {
switch (integration.scope) {
case GithubScope.Org: {
await octokit.request("DELETE /orgs/{org}/actions/secrets/{secret_name}", {
org: integration.owner as string,
secret_name: encryptedSecret.name
});
break;
}
case GithubScope.Env: {
await octokit.request(
"DELETE /repositories/{repository_id}/environments/{environment_name}/secrets/{secret_name}",
{
repository_id: Number(integration.appId),
environment_name: integration.targetEnvironmentId as string,
secret_name: encryptedSecret.name
}
);
break;
}
default: {
await octokit.request("DELETE /repos/{owner}/{repo}/actions/secrets/{secret_name}", {
owner: integration.owner as string,
repo: integration.app as string,
secret_name: encryptedSecret.name
});
break;
}
}
// small delay to prevent hitting API rate limits
await new Promise((resolve) => {
setTimeout(resolve, 50);
});
}
}
};
export const deleteIntegrationSecrets = async ({
integration,
integrationAuth,
integrationAuthService,
projectBotService,
secretV2BridgeDAL,
folderDAL,
secretDAL,
secretImportDAL,
kmsService
}: {
integration: Omit<TIntegrations, "envId"> & {
projectId: string;
environment: {
id: string;
name: string;
slug: string;
};
secretPath: string;
};
integrationAuth: TIntegrationAuths;
integrationAuthService: Pick<TIntegrationAuthServiceFactory, "getIntegrationAccessToken" | "getIntegrationAuth">;
projectBotService: Pick<TProjectBotServiceFactory, "getBotKey">;
secretV2BridgeDAL: Pick<TSecretV2BridgeDALFactory, "find" | "findByFolderId">;
folderDAL: Pick<TSecretFolderDALFactory, "findByManySecretPath" | "findBySecretPath">;
secretImportDAL: Pick<TSecretImportDALFactory, "find" | "findByFolderIds">;
secretDAL: Pick<TSecretDALFactory, "findByFolderId">;
kmsService: Pick<TKmsServiceFactory, "createCipherPairWithDataKey">;
}) => {
const { shouldUseSecretV2Bridge, botKey } = await projectBotService.getBotKey(integration.projectId);
const { decryptor: secretManagerDecryptor } = await kmsService.createCipherPairWithDataKey({
type: KmsDataKey.SecretManager,
projectId: integration.projectId
});
const folder = await folderDAL.findBySecretPath(
integration.projectId,
integration.environment.slug,
integration.secretPath
);
if (!folder) {
throw new NotFoundError({
message: "Folder not found."
});
}
const { accessToken } = await integrationAuthService.getIntegrationAccessToken(
integrationAuth,
shouldUseSecretV2Bridge,
botKey
);
const secrets = shouldUseSecretV2Bridge
? await getIntegrationSecretsV2(
{
environment: integration.environment.id,
projectId: integration.projectId,
folderId: folder.id,
depth: 1,
decryptor: (value) => (value ? secretManagerDecryptor({ cipherTextBlob: value }).toString() : "")
},
secretV2BridgeDAL,
folderDAL,
secretImportDAL
)
: await getIntegrationSecretsV1(
{
environment: integration.environment.id,
projectId: integration.projectId,
folderId: folder.id,
key: botKey as string,
depth: 1
},
secretDAL,
folderDAL,
secretImportDAL
);
const suffixedSecrets: typeof secrets = {};
const metadata = IntegrationMetadataSchema.parse(integration.metadata);
if (metadata) {
Object.keys(secrets).forEach((key) => {
const prefix = metadata?.secretPrefix || "";
const suffix = metadata?.secretSuffix || "";
const newKey = prefix + key + suffix;
suffixedSecrets[newKey] = secrets[key];
});
}
switch (integration.integration) {
case Integrations.GITHUB: {
await deleteGithubSecrets({
integration,
accessToken,
secrets: Object.keys(suffixedSecrets).length !== 0 ? suffixedSecrets : secrets
});
break;
}
default:
throw new BadRequestError({
message: "Invalid integration"
});
}
};
@@ -538,19 +538,20 @@ const syncSecretsAWSParameterStore = async ({
integration, integration,
secrets, secrets,
accessId, accessId,
accessToken accessToken,
projectId
}: { }: {
integration: TIntegrations; integration: TIntegrations & { secretPath: string; environment: { slug: string } };
secrets: Record<string, { value: string; comment?: string }>; secrets: Record<string, { value: string; comment?: string }>;
accessId: string | null; accessId: string | null;
accessToken: string; accessToken: string;
projectId?: string;
}) => { }) => {
let response: { isSynced: boolean; syncMessage: string } | null = null; let response: { isSynced: boolean; syncMessage: string } | null = null;
if (!accessId) { if (!accessId) {
throw new Error("AWS access ID is required"); throw new Error("AWS access ID is required");
} }
const config = new AWS.Config({ const config = new AWS.Config({
region: integration.region as string, region: integration.region as string,
credentials: { credentials: {
@@ -567,7 +568,9 @@ const syncSecretsAWSParameterStore = async ({
const metadata = z.record(z.any()).parse(integration.metadata || {}); const metadata = z.record(z.any()).parse(integration.metadata || {});
const awsParameterStoreSecretsObj: Record<string, AWS.SSM.Parameter> = {}; const awsParameterStoreSecretsObj: Record<string, AWS.SSM.Parameter> = {};
logger.info(
`getIntegrationSecrets: integration sync triggered for ssm with [projectId=${projectId}] [environment=${integration.environment.slug}] [secretPath=${integration.secretPath}] [shouldDisableDelete=${metadata.shouldDisableDelete}]`
);
// now fetch all aws parameter store secrets // now fetch all aws parameter store secrets
let hasNext = true; let hasNext = true;
let nextToken: string | undefined; let nextToken: string | undefined;
@@ -594,6 +597,18 @@ const syncSecretsAWSParameterStore = async ({
nextToken = parameters.NextToken; nextToken = parameters.NextToken;
} }
logger.info(
`getIntegrationSecrets: all fetched keys from AWS SSM [projectId=${projectId}] [environment=${
integration.environment.slug
}] [secretPath=${integration.secretPath}] [awsParameterStoreSecretsObj=${Object.keys(
awsParameterStoreSecretsObj
).join(",")}]`
);
logger.info(
`getIntegrationSecrets: all secrets from Infisical to send to AWS SSM [projectId=${projectId}] [environment=${
integration.environment.slug
}] [secretPath=${integration.secretPath}] [secrets=${Object.keys(secrets).join(",")}]`
);
// Identify secrets to create // Identify secrets to create
// don't use Promise.all() and promise map here // don't use Promise.all() and promise map here
// it will cause rate limit // it will cause rate limit
@@ -603,24 +618,56 @@ const syncSecretsAWSParameterStore = async ({
// case: secret does not exist in AWS parameter store // case: secret does not exist in AWS parameter store
// -> create secret // -> create secret
if (secrets[key].value) { if (secrets[key].value) {
logger.info(
`getIntegrationSecrets: create secret in AWS SSM for [projectId=${projectId}] [environment=${integration.environment.slug}] [secretPath=${integration.secretPath}] [key=${key}]`
);
await ssm await ssm
.putParameter({ .putParameter({
Name: `${integration.path}${key}`, Name: `${integration.path}${key}`,
Type: "SecureString", Type: "SecureString",
Value: secrets[key].value, Value: secrets[key].value,
...(metadata.kmsKeyId && { KeyId: metadata.kmsKeyId }), ...(metadata.kmsKeyId && { KeyId: metadata.kmsKeyId }),
// Overwrite: true, Overwrite: true
Tags: metadata.secretAWSTag
? metadata.secretAWSTag.map((tag: { key: string; value: string }) => ({
Key: tag.key,
Value: tag.value
}))
: []
}) })
.promise(); .promise();
if (metadata.secretAWSTag?.length) {
try {
await ssm
.addTagsToResource({
ResourceType: "Parameter",
ResourceId: `${integration.path}${key}`,
Tags: metadata.secretAWSTag
? metadata.secretAWSTag.map((tag: { key: string; value: string }) => ({
Key: tag.key,
Value: tag.value
}))
: []
})
.promise();
} catch (err) {
logger.error(
err,
`getIntegrationSecrets: create secret in AWS SSM for failed [projectId=${projectId}] [environment=${integration.environment.slug}] [secretPath=${integration.secretPath}] [key=${key}]`
);
// eslint-disable-next-line @typescript-eslint/no-explicit-any
if ((err as any).code === "AccessDeniedException") {
logger.error(
`AWS Parameter Store Error [integration=${integration.id}]: double check AWS account permissions (refer to the Infisical docs)`
);
}
response = {
isSynced: false,
syncMessage: (err as AWSError)?.message || "Error syncing with AWS Parameter Store"
};
}
}
} }
// case: secret exists in AWS parameter store // case: secret exists in AWS parameter store
} else { } else {
logger.info(
`getIntegrationSecrets: update secret in AWS SSM for [projectId=${projectId}] [environment=${integration.environment.slug}] [secretPath=${integration.secretPath}] [key=${key}]`
);
// -> update secret // -> update secret
if (awsParameterStoreSecretsObj[key].Value !== secrets[key].value) { if (awsParameterStoreSecretsObj[key].Value !== secrets[key].value) {
await ssm await ssm
@@ -648,6 +695,10 @@ const syncSecretsAWSParameterStore = async ({
}) })
.promise(); .promise();
} catch (err) { } catch (err) {
logger.error(
err,
`getIntegrationSecrets: update secret in AWS SSM for failed [projectId=${projectId}] [environment=${integration.environment.slug}] [secretPath=${integration.secretPath}] [key=${key}]`
);
// eslint-disable-next-line @typescript-eslint/no-explicit-any // eslint-disable-next-line @typescript-eslint/no-explicit-any
if ((err as any).code === "AccessDeniedException") { if ((err as any).code === "AccessDeniedException") {
logger.error( logger.error(
@@ -670,9 +721,18 @@ const syncSecretsAWSParameterStore = async ({
} }
if (!metadata.shouldDisableDelete) { if (!metadata.shouldDisableDelete) {
logger.info(
`getIntegrationSecrets: inside of shouldDisableDelete AWS SSM [projectId=${projectId}] [environment=${integration.environment.slug}] [secretPath=${integration.secretPath}] [step=1]`
);
for (const key in awsParameterStoreSecretsObj) { for (const key in awsParameterStoreSecretsObj) {
if (Object.hasOwn(awsParameterStoreSecretsObj, key)) { if (Object.hasOwn(awsParameterStoreSecretsObj, key)) {
logger.info(
`getIntegrationSecrets: inside of shouldDisableDelete AWS SSM [projectId=${projectId}] [environment=${integration.environment.slug}] [secretPath=${integration.secretPath}] [key=${key}] [step=2]`
);
if (!(key in secrets)) { if (!(key in secrets)) {
logger.info(
`getIntegrationSecrets: inside of shouldDisableDelete AWS SSM [projectId=${projectId}] [environment=${integration.environment.slug}] [secretPath=${integration.secretPath}] [key=${key}] [step=3]`
);
// case: // case:
// -> delete secret // -> delete secret
await ssm await ssm
@@ -680,6 +740,9 @@ const syncSecretsAWSParameterStore = async ({
Name: awsParameterStoreSecretsObj[key].Name as string Name: awsParameterStoreSecretsObj[key].Name as string
}) })
.promise(); .promise();
logger.info(
`getIntegrationSecrets: inside of shouldDisableDelete AWS SSM [projectId=${projectId}] [environment=${integration.environment.slug}] [secretPath=${integration.secretPath}] [key=${key}] [step=4]`
);
} }
await new Promise((resolve) => { await new Promise((resolve) => {
setTimeout(resolve, 50); setTimeout(resolve, 50);
@@ -3656,7 +3719,8 @@ export const syncIntegrationSecrets = async ({
integration, integration,
secrets, secrets,
accessId, accessId,
accessToken accessToken,
projectId
}); });
break; break;
case Integrations.AWS_SECRET_MANAGER: case Integrations.AWS_SECRET_MANAGER:
@@ -6,8 +6,15 @@ import { BadRequestError } from "@app/lib/errors";
import { TProjectPermission } from "@app/lib/types"; import { TProjectPermission } from "@app/lib/types";
import { TIntegrationAuthDALFactory } from "../integration-auth/integration-auth-dal"; import { TIntegrationAuthDALFactory } from "../integration-auth/integration-auth-dal";
import { TIntegrationAuthServiceFactory } from "../integration-auth/integration-auth-service";
import { deleteIntegrationSecrets } from "../integration-auth/integration-delete-secret";
import { TKmsServiceFactory } from "../kms/kms-service";
import { TProjectBotServiceFactory } from "../project-bot/project-bot-service";
import { TSecretDALFactory } from "../secret/secret-dal";
import { TSecretQueueFactory } from "../secret/secret-queue"; import { TSecretQueueFactory } from "../secret/secret-queue";
import { TSecretFolderDALFactory } from "../secret-folder/secret-folder-dal"; import { TSecretFolderDALFactory } from "../secret-folder/secret-folder-dal";
import { TSecretImportDALFactory } from "../secret-import/secret-import-dal";
import { TSecretV2BridgeDALFactory } from "../secret-v2-bridge/secret-v2-bridge-dal";
import { TIntegrationDALFactory } from "./integration-dal"; import { TIntegrationDALFactory } from "./integration-dal";
import { import {
TCreateIntegrationDTO, TCreateIntegrationDTO,
@@ -19,9 +26,15 @@ import {
type TIntegrationServiceFactoryDep = { type TIntegrationServiceFactoryDep = {
integrationDAL: TIntegrationDALFactory; integrationDAL: TIntegrationDALFactory;
integrationAuthDAL: TIntegrationAuthDALFactory; integrationAuthDAL: TIntegrationAuthDALFactory;
folderDAL: Pick<TSecretFolderDALFactory, "findBySecretPath">; integrationAuthService: TIntegrationAuthServiceFactory;
folderDAL: Pick<TSecretFolderDALFactory, "findBySecretPath" | "findByManySecretPath">;
permissionService: Pick<TPermissionServiceFactory, "getProjectPermission">; permissionService: Pick<TPermissionServiceFactory, "getProjectPermission">;
projectBotService: TProjectBotServiceFactory;
secretQueueService: Pick<TSecretQueueFactory, "syncIntegrations">; secretQueueService: Pick<TSecretQueueFactory, "syncIntegrations">;
secretV2BridgeDAL: Pick<TSecretV2BridgeDALFactory, "find" | "findByFolderId">;
secretImportDAL: Pick<TSecretImportDALFactory, "find" | "findByFolderIds">;
kmsService: Pick<TKmsServiceFactory, "createCipherPairWithDataKey">;
secretDAL: Pick<TSecretDALFactory, "findByFolderId">;
}; };
export type TIntegrationServiceFactory = ReturnType<typeof integrationServiceFactory>; export type TIntegrationServiceFactory = ReturnType<typeof integrationServiceFactory>;
@@ -31,7 +44,13 @@ export const integrationServiceFactory = ({
integrationAuthDAL, integrationAuthDAL,
folderDAL, folderDAL,
permissionService, permissionService,
secretQueueService secretQueueService,
integrationAuthService,
projectBotService,
secretV2BridgeDAL,
secretImportDAL,
kmsService,
secretDAL
}: TIntegrationServiceFactoryDep) => { }: TIntegrationServiceFactoryDep) => {
const createIntegration = async ({ const createIntegration = async ({
app, app,
@@ -161,7 +180,14 @@ export const integrationServiceFactory = ({
return updatedIntegration; return updatedIntegration;
}; };
const deleteIntegration = async ({ actorId, id, actor, actorAuthMethod, actorOrgId }: TDeleteIntegrationDTO) => { const deleteIntegration = async ({
actorId,
id,
actor,
actorAuthMethod,
actorOrgId,
shouldDeleteIntegrationSecrets
}: TDeleteIntegrationDTO) => {
const integration = await integrationDAL.findById(id); const integration = await integrationDAL.findById(id);
if (!integration) throw new BadRequestError({ message: "Integration auth not found" }); if (!integration) throw new BadRequestError({ message: "Integration auth not found" });
@@ -174,6 +200,22 @@ export const integrationServiceFactory = ({
); );
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Delete, ProjectPermissionSub.Integrations); ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Delete, ProjectPermissionSub.Integrations);
const integrationAuth = await integrationAuthDAL.findById(integration.integrationAuthId);
if (shouldDeleteIntegrationSecrets) {
await deleteIntegrationSecrets({
integration,
integrationAuth,
projectBotService,
integrationAuthService,
secretV2BridgeDAL,
folderDAL,
secretImportDAL,
secretDAL,
kmsService
});
}
const deletedIntegration = await integrationDAL.transaction(async (tx) => { const deletedIntegration = await integrationDAL.transaction(async (tx) => {
// delete integration // delete integration
const deletedIntegrationResult = await integrationDAL.deleteById(id, tx); const deletedIntegrationResult = await integrationDAL.deleteById(id, tx);
@@ -63,6 +63,7 @@ export type TUpdateIntegrationDTO = {
export type TDeleteIntegrationDTO = { export type TDeleteIntegrationDTO = {
id: string; id: string;
shouldDeleteIntegrationSecrets?: boolean;
} & Omit<TProjectPermission, "projectId">; } & Omit<TProjectPermission, "projectId">;
export type TSyncIntegrationDTO = { export type TSyncIntegrationDTO = {
@@ -0,0 +1,48 @@
import { logger } from "@app/lib/logger";
import { QueueJobs, QueueName, TQueueServiceFactory } from "@app/queue";
import { TPkiAlertServiceFactory } from "@app/services/pki-alert/pki-alert-service";
type TDailyExpiringPkiItemAlertQueueServiceFactoryDep = {
queueService: TQueueServiceFactory;
pkiAlertService: Pick<TPkiAlertServiceFactory, "sendPkiItemExpiryNotices">;
};
export type TDailyExpiringPkiItemAlertQueueServiceFactory = ReturnType<
typeof dailyExpiringPkiItemAlertQueueServiceFactory
>;
export const dailyExpiringPkiItemAlertQueueServiceFactory = ({
queueService,
pkiAlertService
}: TDailyExpiringPkiItemAlertQueueServiceFactoryDep) => {
queueService.start(QueueName.DailyExpiringPkiItemAlert, async () => {
logger.info(`${QueueName.DailyExpiringPkiItemAlert}: queue task started`);
await pkiAlertService.sendPkiItemExpiryNotices();
logger.info(`${QueueName.DailyExpiringPkiItemAlert}: queue task completed`);
});
// we do a repeat cron job in utc timezone at 12 Midnight each day
const startSendingAlerts = async () => {
// clear previous job
await queueService.stopRepeatableJob(
QueueName.DailyExpiringPkiItemAlert,
QueueJobs.DailyExpiringPkiItemAlert,
{ pattern: "0 0 * * *", utc: true },
QueueName.DailyExpiringPkiItemAlert // just a job id
);
await queueService.queue(QueueName.DailyExpiringPkiItemAlert, QueueJobs.DailyExpiringPkiItemAlert, undefined, {
delay: 5000,
jobId: QueueName.DailyExpiringPkiItemAlert,
repeat: { pattern: "0 0 * * *", utc: true }
});
};
queueService.listen(QueueName.DailyExpiringPkiItemAlert, "failed", (_, err) => {
logger.error(err, `${QueueName.DailyExpiringPkiItemAlert}: Expiring PKI item alert failed`);
});
return {
startSendingAlerts
};
};
@@ -12,8 +12,11 @@ import { TPkiAlertDALFactory } from "./pki-alert-dal";
import { TCreateAlertDTO, TDeleteAlertDTO, TGetAlertByIdDTO, TUpdateAlertDTO } from "./pki-alert-types"; import { TCreateAlertDTO, TDeleteAlertDTO, TGetAlertByIdDTO, TUpdateAlertDTO } from "./pki-alert-types";
type TPkiAlertServiceFactoryDep = { type TPkiAlertServiceFactoryDep = {
pkiAlertDAL: TPkiAlertDALFactory; pkiAlertDAL: Pick<
pkiCollectionDAL: TPkiCollectionDALFactory; TPkiAlertDALFactory,
"create" | "findById" | "updateById" | "deleteById" | "getExpiringPkiCollectionItemsForAlerting"
>;
pkiCollectionDAL: Pick<TPkiCollectionDALFactory, "findById">;
permissionService: Pick<TPermissionServiceFactory, "getProjectPermission">; permissionService: Pick<TPermissionServiceFactory, "getProjectPermission">;
smtpService: Pick<TSmtpService, "sendMail">; smtpService: Pick<TSmtpService, "sendMail">;
}; };
@@ -81,7 +81,7 @@ export const pkiCollectionItemDALFactory = (db: TDbClient) => {
return parseInt((count as unknown as CountResult).count || "0", 10); return parseInt((count as unknown as CountResult).count || "0", 10);
} catch (error) { } catch (error) {
throw new DatabaseError({ error, name: "Count all project certificates" }); throw new DatabaseError({ error, name: "Count all PKI collection items" });
} }
}; };
@@ -22,10 +22,13 @@ import {
} from "./pki-collection-types"; } from "./pki-collection-types";
type TPkiCollectionServiceFactoryDep = { type TPkiCollectionServiceFactoryDep = {
pkiCollectionDAL: TPkiCollectionDALFactory; // TODO: Pick pkiCollectionDAL: Pick<TPkiCollectionDALFactory, "create" | "findById" | "updateById" | "deleteById">;
pkiCollectionItemDAL: TPkiCollectionItemDALFactory; pkiCollectionItemDAL: Pick<
certificateAuthorityDAL: TCertificateAuthorityDALFactory; TPkiCollectionItemDALFactory,
certificateDAL: TCertificateDALFactory; "findOne" | "create" | "deleteById" | "findPkiCollectionItems" | "countItemsInPkiCollection"
>;
certificateAuthorityDAL: Pick<TCertificateAuthorityDALFactory, "find" | "findOne">;
certificateDAL: Pick<TCertificateDALFactory, "find">;
permissionService: Pick<TPermissionServiceFactory, "getProjectPermission">; permissionService: Pick<TPermissionServiceFactory, "getProjectPermission">;
}; };
@@ -40,6 +43,7 @@ export const pkiCollectionServiceFactory = ({
}: TPkiCollectionServiceFactoryDep) => { }: TPkiCollectionServiceFactoryDep) => {
const createPkiCollection = async ({ const createPkiCollection = async ({
name, name,
description,
projectId, projectId,
actorId, actorId,
actorAuthMethod, actorAuthMethod,
@@ -61,7 +65,8 @@ export const pkiCollectionServiceFactory = ({
const pkiCollection = await pkiCollectionDAL.create({ const pkiCollection = await pkiCollectionDAL.create({
projectId, projectId,
name name,
description
}); });
return pkiCollection; return pkiCollection;
@@ -92,6 +97,7 @@ export const pkiCollectionServiceFactory = ({
const updatePkiCollection = async ({ const updatePkiCollection = async ({
collectionId, collectionId,
name, name,
description,
actorId, actorId,
actorAuthMethod, actorAuthMethod,
actor, actor,
@@ -110,7 +116,8 @@ export const pkiCollectionServiceFactory = ({
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Edit, ProjectPermissionSub.PkiCollections); ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Edit, ProjectPermissionSub.PkiCollections);
pkiCollection = await pkiCollectionDAL.updateById(collectionId, { pkiCollection = await pkiCollectionDAL.updateById(collectionId, {
name name,
description
}); });
return pkiCollection; return pkiCollection;
@@ -135,7 +142,7 @@ export const pkiCollectionServiceFactory = ({
); );
ForbiddenError.from(permission).throwUnlessCan( ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Create, ProjectPermissionActions.Delete,
ProjectPermissionSub.PkiCollections ProjectPermissionSub.PkiCollections
); );
pkiCollection = await pkiCollectionDAL.deleteById(collectionId); pkiCollection = await pkiCollectionDAL.deleteById(collectionId);
@@ -2,6 +2,7 @@ import { TProjectPermission } from "@app/lib/types";
export type TCreatePkiCollectionDTO = { export type TCreatePkiCollectionDTO = {
name: string; name: string;
description: string;
} & TProjectPermission; } & TProjectPermission;
export type TGetPkiCollectionByIdDTO = { export type TGetPkiCollectionByIdDTO = {
@@ -11,6 +12,7 @@ export type TGetPkiCollectionByIdDTO = {
export type TUpdatePkiCollectionDTO = { export type TUpdatePkiCollectionDTO = {
collectionId: string; collectionId: string;
name?: string; name?: string;
description?: string;
} & Omit<TProjectPermission, "projectId">; } & Omit<TProjectPermission, "projectId">;
export type TDeletePkiCollectionDTO = { export type TDeletePkiCollectionDTO = {
@@ -46,6 +46,7 @@ export const projectBotDALFactory = (db: TDbClient) => {
const doc = await db const doc = await db
.replicaNode()(TableName.ProjectMembership) .replicaNode()(TableName.ProjectMembership)
.where(`${TableName.ProjectMembership}.projectId` as "projectId", projectId) .where(`${TableName.ProjectMembership}.projectId` as "projectId", projectId)
.where(`${TableName.ProjectKeys}.projectId` as "projectId", projectId)
.where(`${TableName.Users}.isGhost` as "isGhost", false) .where(`${TableName.Users}.isGhost` as "isGhost", false)
.join(TableName.Users, `${TableName.ProjectMembership}.userId`, `${TableName.Users}.id`) .join(TableName.Users, `${TableName.ProjectMembership}.userId`, `${TableName.Users}.id`)
.join(TableName.ProjectKeys, `${TableName.ProjectMembership}.userId`, `${TableName.ProjectKeys}.receiverId`) .join(TableName.ProjectKeys, `${TableName.ProjectMembership}.userId`, `${TableName.ProjectKeys}.receiverId`)
@@ -16,6 +16,7 @@ import { TProjectPermission } from "@app/lib/types";
import { ActorType } from "../auth/auth-type"; import { ActorType } from "../auth/auth-type";
import { TCertificateDALFactory } from "../certificate/certificate-dal"; import { TCertificateDALFactory } from "../certificate/certificate-dal";
import { TCertificateAuthorityDALFactory } from "../certificate-authority/certificate-authority-dal"; import { TCertificateAuthorityDALFactory } from "../certificate-authority/certificate-authority-dal";
import { TCertificateTemplateDALFactory } from "../certificate-template/certificate-template-dal";
import { TIdentityOrgDALFactory } from "../identity/identity-org-dal"; import { TIdentityOrgDALFactory } from "../identity/identity-org-dal";
import { TIdentityProjectDALFactory } from "../identity-project/identity-project-dal"; import { TIdentityProjectDALFactory } from "../identity-project/identity-project-dal";
import { TIdentityProjectMembershipRoleDALFactory } from "../identity-project/identity-project-membership-role-dal"; import { TIdentityProjectMembershipRoleDALFactory } from "../identity-project/identity-project-membership-role-dal";
@@ -41,6 +42,7 @@ import {
TGetProjectKmsKey, TGetProjectKmsKey,
TListProjectAlertsDTO, TListProjectAlertsDTO,
TListProjectCasDTO, TListProjectCasDTO,
TListProjectCertificateTemplatesDTO,
TListProjectCertsDTO, TListProjectCertsDTO,
TLoadProjectKmsBackupDTO, TLoadProjectKmsBackupDTO,
TToggleProjectAutoCapitalizationDTO, TToggleProjectAutoCapitalizationDTO,
@@ -73,6 +75,7 @@ type TProjectServiceFactoryDep = {
projectUserMembershipRoleDAL: Pick<TProjectUserMembershipRoleDALFactory, "create">; projectUserMembershipRoleDAL: Pick<TProjectUserMembershipRoleDALFactory, "create">;
certificateAuthorityDAL: Pick<TCertificateAuthorityDALFactory, "find">; certificateAuthorityDAL: Pick<TCertificateAuthorityDALFactory, "find">;
certificateDAL: Pick<TCertificateDALFactory, "find" | "countCertificatesInProject">; certificateDAL: Pick<TCertificateDALFactory, "find" | "countCertificatesInProject">;
certificateTemplateDAL: Pick<TCertificateTemplateDALFactory, "getCertTemplatesByProjectId">;
pkiAlertDAL: Pick<TPkiAlertDALFactory, "find">; pkiAlertDAL: Pick<TPkiAlertDALFactory, "find">;
pkiCollectionDAL: Pick<TPkiCollectionDALFactory, "find">; pkiCollectionDAL: Pick<TPkiCollectionDALFactory, "find">;
permissionService: TPermissionServiceFactory; permissionService: TPermissionServiceFactory;
@@ -112,6 +115,7 @@ export const projectServiceFactory = ({
identityProjectMembershipRoleDAL, identityProjectMembershipRoleDAL,
certificateAuthorityDAL, certificateAuthorityDAL,
certificateDAL, certificateDAL,
certificateTemplateDAL,
pkiCollectionDAL, pkiCollectionDAL,
pkiAlertDAL, pkiAlertDAL,
keyStore, keyStore,
@@ -737,6 +741,36 @@ export const projectServiceFactory = ({
}; };
}; };
/**
* Return list of certificate templates for project
*/
const listProjectCertificateTemplates = async ({
projectId,
actorId,
actorOrgId,
actorAuthMethod,
actor
}: TListProjectCertificateTemplatesDTO) => {
const { permission } = await permissionService.getProjectPermission(
actor,
actorId,
projectId,
actorAuthMethod,
actorOrgId
);
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Read,
ProjectPermissionSub.CertificateTemplates
);
const certificateTemplates = await certificateTemplateDAL.getCertTemplatesByProjectId(projectId);
return {
certificateTemplates
};
};
const updateProjectKmsKey = async ({ const updateProjectKmsKey = async ({
projectId, projectId,
kms, kms,
@@ -857,6 +891,7 @@ export const projectServiceFactory = ({
listProjectCertificates, listProjectCertificates,
listProjectAlerts, listProjectAlerts,
listProjectPkiCollections, listProjectPkiCollections,
listProjectCertificateTemplates,
updateVersionLimit, updateVersionLimit,
updateAuditLogsRetention, updateAuditLogsRetention,
updateProjectKmsKey, updateProjectKmsKey,
@@ -117,3 +117,5 @@ export type TLoadProjectKmsBackupDTO = {
} & TProjectPermission; } & TProjectPermission;
export type TGetProjectKmsKey = TProjectPermission; export type TGetProjectKmsKey = TProjectPermission;
export type TListProjectCertificateTemplatesDTO = TProjectPermission;
@@ -2,9 +2,9 @@ import { TAuditLogDALFactory } from "@app/ee/services/audit-log/audit-log-dal";
import { TSnapshotDALFactory } from "@app/ee/services/secret-snapshot/snapshot-dal"; import { TSnapshotDALFactory } from "@app/ee/services/secret-snapshot/snapshot-dal";
import { logger } from "@app/lib/logger"; import { logger } from "@app/lib/logger";
import { QueueJobs, QueueName, TQueueServiceFactory } from "@app/queue"; import { QueueJobs, QueueName, TQueueServiceFactory } from "@app/queue";
import { TPkiAlertServiceFactory } from "@app/services/pki-alert/pki-alert-service";
import { TIdentityAccessTokenDALFactory } from "../identity-access-token/identity-access-token-dal"; import { TIdentityAccessTokenDALFactory } from "../identity-access-token/identity-access-token-dal";
import { TIdentityUaClientSecretDALFactory } from "../identity-ua/identity-ua-client-secret-dal";
import { TSecretVersionDALFactory } from "../secret/secret-version-dal"; import { TSecretVersionDALFactory } from "../secret/secret-version-dal";
import { TSecretFolderVersionDALFactory } from "../secret-folder/secret-folder-version-dal"; import { TSecretFolderVersionDALFactory } from "../secret-folder/secret-folder-version-dal";
import { TSecretSharingDALFactory } from "../secret-sharing/secret-sharing-dal"; import { TSecretSharingDALFactory } from "../secret-sharing/secret-sharing-dal";
@@ -13,13 +13,13 @@ import { TSecretVersionV2DALFactory } from "../secret-v2-bridge/secret-version-d
type TDailyResourceCleanUpQueueServiceFactoryDep = { type TDailyResourceCleanUpQueueServiceFactoryDep = {
auditLogDAL: Pick<TAuditLogDALFactory, "pruneAuditLog">; auditLogDAL: Pick<TAuditLogDALFactory, "pruneAuditLog">;
identityAccessTokenDAL: Pick<TIdentityAccessTokenDALFactory, "removeExpiredTokens">; identityAccessTokenDAL: Pick<TIdentityAccessTokenDALFactory, "removeExpiredTokens">;
identityUniversalAuthClientSecretDAL: Pick<TIdentityUaClientSecretDALFactory, "removeExpiredClientSecrets">;
secretVersionDAL: Pick<TSecretVersionDALFactory, "pruneExcessVersions">; secretVersionDAL: Pick<TSecretVersionDALFactory, "pruneExcessVersions">;
secretVersionV2DAL: Pick<TSecretVersionV2DALFactory, "pruneExcessVersions">; secretVersionV2DAL: Pick<TSecretVersionV2DALFactory, "pruneExcessVersions">;
secretFolderVersionDAL: Pick<TSecretFolderVersionDALFactory, "pruneExcessVersions">; secretFolderVersionDAL: Pick<TSecretFolderVersionDALFactory, "pruneExcessVersions">;
snapshotDAL: Pick<TSnapshotDALFactory, "pruneExcessSnapshots">; snapshotDAL: Pick<TSnapshotDALFactory, "pruneExcessSnapshots">;
secretSharingDAL: Pick<TSecretSharingDALFactory, "pruneExpiredSharedSecrets">; secretSharingDAL: Pick<TSecretSharingDALFactory, "pruneExpiredSharedSecrets">;
queueService: TQueueServiceFactory; queueService: TQueueServiceFactory;
pkiAlertService: Pick<TPkiAlertServiceFactory, "sendPkiItemExpiryNotices">;
}; };
export type TDailyResourceCleanUpQueueServiceFactory = ReturnType<typeof dailyResourceCleanUpQueueServiceFactory>; export type TDailyResourceCleanUpQueueServiceFactory = ReturnType<typeof dailyResourceCleanUpQueueServiceFactory>;
@@ -27,24 +27,24 @@ export type TDailyResourceCleanUpQueueServiceFactory = ReturnType<typeof dailyRe
export const dailyResourceCleanUpQueueServiceFactory = ({ export const dailyResourceCleanUpQueueServiceFactory = ({
auditLogDAL, auditLogDAL,
queueService, queueService,
pkiAlertService,
snapshotDAL, snapshotDAL,
secretVersionDAL, secretVersionDAL,
secretFolderVersionDAL, secretFolderVersionDAL,
identityAccessTokenDAL, identityAccessTokenDAL,
secretSharingDAL, secretSharingDAL,
secretVersionV2DAL secretVersionV2DAL,
identityUniversalAuthClientSecretDAL
}: TDailyResourceCleanUpQueueServiceFactoryDep) => { }: TDailyResourceCleanUpQueueServiceFactoryDep) => {
queueService.start(QueueName.DailyResourceCleanUp, async () => { queueService.start(QueueName.DailyResourceCleanUp, async () => {
logger.info(`${QueueName.DailyResourceCleanUp}: queue task started`); logger.info(`${QueueName.DailyResourceCleanUp}: queue task started`);
await auditLogDAL.pruneAuditLog(); await auditLogDAL.pruneAuditLog();
await identityAccessTokenDAL.removeExpiredTokens(); await identityAccessTokenDAL.removeExpiredTokens();
await identityUniversalAuthClientSecretDAL.removeExpiredClientSecrets();
await secretSharingDAL.pruneExpiredSharedSecrets(); await secretSharingDAL.pruneExpiredSharedSecrets();
await snapshotDAL.pruneExcessSnapshots(); await snapshotDAL.pruneExcessSnapshots();
await secretVersionDAL.pruneExcessVersions(); await secretVersionDAL.pruneExcessVersions();
await secretVersionV2DAL.pruneExcessVersions(); await secretVersionV2DAL.pruneExcessVersions();
await secretFolderVersionDAL.pruneExcessVersions(); await secretFolderVersionDAL.pruneExcessVersions();
await pkiAlertService.sendPkiItemExpiryNotices();
logger.info(`${QueueName.DailyResourceCleanUp}: queue task completed`); logger.info(`${QueueName.DailyResourceCleanUp}: queue task completed`);
}); });
@@ -36,8 +36,8 @@ type TSecretImportSecretsV2 = {
secretKey: string; secretKey: string;
// akhilmhdh: yes i know you can put ?. // akhilmhdh: yes i know you can put ?.
// But for somereason ts consider ? and undefined explicit as different just ts things // But for somereason ts consider ? and undefined explicit as different just ts things
secretValue: string | undefined; secretValue: string;
secretComment: string | undefined; secretComment: string;
})[]; })[];
}; };
@@ -157,7 +157,7 @@ export const fnSecretsV2FromImports = async ({
secretImportDAL: Pick<TSecretImportDALFactory, "findByFolderIds">; secretImportDAL: Pick<TSecretImportDALFactory, "findByFolderIds">;
depth?: number; depth?: number;
cyclicDetector?: Set<string>; cyclicDetector?: Set<string>;
decryptor: (value?: Buffer | null) => string | undefined; decryptor: (value?: Buffer | null) => string;
expandSecretReferences?: ( expandSecretReferences?: (
secrets: Record<string, { value?: string; comment?: string; skipMultilineEncoding?: boolean | null }> secrets: Record<string, { value?: string; comment?: string; skipMultilineEncoding?: boolean | null }>
) => Promise<Record<string, { value?: string; comment?: string; skipMultilineEncoding?: boolean | null }>>; ) => Promise<Record<string, { value?: string; comment?: string; skipMultilineEncoding?: boolean | null }>>;
@@ -231,6 +231,7 @@ export const fnSecretsV2FromImports = async ({
_id: item.id // The old Python SDK depends on the _id field being returned. We return this to keep the older Python SDK versions backwards compatible with the new Postgres backend. _id: item.id // The old Python SDK depends on the _id field being returned. We return this to keep the older Python SDK versions backwards compatible with the new Postgres backend.
})) }))
.concat(folderDeeperImportSecrets); .concat(folderDeeperImportSecrets);
return { return {
secretPath: importPath, secretPath: importPath,
environment: importEnv.slug, environment: importEnv.slug,
@@ -254,7 +255,7 @@ export const fnSecretsV2FromImports = async ({
}; };
return acc; return acc;
}, },
{} as Record<string, { value?: string; comment?: string; skipMultilineEncoding?: boolean | null }> {} as Record<string, { value: string; comment?: string; skipMultilineEncoding?: boolean | null }>
); );
// eslint-disable-next-line // eslint-disable-next-line
await expandSecretReferences(secretsGroupByKey); await expandSecretReferences(secretsGroupByKey);
@@ -507,7 +507,7 @@ export const secretImportServiceFactory = ({
folderDAL, folderDAL,
secretDAL: secretV2BridgeDAL, secretDAL: secretV2BridgeDAL,
secretImportDAL, secretImportDAL,
decryptor: (value) => (value ? secretManagerDecryptor({ cipherTextBlob: value }).toString() : undefined) decryptor: (value) => (value ? secretManagerDecryptor({ cipherTextBlob: value }).toString() : "")
}); });
return importedSecrets; return importedSecrets;
} }
@@ -51,7 +51,7 @@ export const secretTagDALFactory = (db: TDbClient) => {
...secretTagOrm, ...secretTagOrm,
saveTagsToSecret: secretJnTagOrm.insertMany, saveTagsToSecret: secretJnTagOrm.insertMany,
deleteTagsToSecret: secretJnTagOrm.delete, deleteTagsToSecret: secretJnTagOrm.delete,
saveTagsToSecretV2: secretV2JnTagOrm.insertMany, saveTagsToSecretV2: secretV2JnTagOrm.batchInsert,
deleteTagsToSecretV2: secretV2JnTagOrm.delete, deleteTagsToSecretV2: secretV2JnTagOrm.delete,
findSecretTagsByProjectId, findSecretTagsByProjectId,
deleteTagsManySecret, deleteTagsManySecret,
@@ -22,16 +22,7 @@ type TSecretTagServiceFactoryDep = {
export type TSecretTagServiceFactory = ReturnType<typeof secretTagServiceFactory>; export type TSecretTagServiceFactory = ReturnType<typeof secretTagServiceFactory>;
export const secretTagServiceFactory = ({ secretTagDAL, permissionService }: TSecretTagServiceFactoryDep) => { export const secretTagServiceFactory = ({ secretTagDAL, permissionService }: TSecretTagServiceFactoryDep) => {
const createTag = async ({ const createTag = async ({ slug, actor, color, actorId, actorOrgId, actorAuthMethod, projectId }: TCreateTagDTO) => {
name,
slug,
actor,
color,
actorId,
actorOrgId,
actorAuthMethod,
projectId
}: TCreateTagDTO) => {
const { permission } = await permissionService.getProjectPermission( const { permission } = await permissionService.getProjectPermission(
actor, actor,
actorId, actorId,
@@ -46,7 +37,6 @@ export const secretTagServiceFactory = ({ secretTagDAL, permissionService }: TSe
const newTag = await secretTagDAL.create({ const newTag = await secretTagDAL.create({
projectId, projectId,
name,
slug, slug,
color, color,
createdBy: actorId, createdBy: actorId,
@@ -55,7 +45,7 @@ export const secretTagServiceFactory = ({ secretTagDAL, permissionService }: TSe
return newTag; return newTag;
}; };
const updateTag = async ({ actorId, actor, actorOrgId, actorAuthMethod, id, name, color, slug }: TUpdateTagDTO) => { const updateTag = async ({ actorId, actor, actorOrgId, actorAuthMethod, id, color, slug }: TUpdateTagDTO) => {
const tag = await secretTagDAL.findById(id); const tag = await secretTagDAL.findById(id);
if (!tag) throw new BadRequestError({ message: "Tag doesn't exist" }); if (!tag) throw new BadRequestError({ message: "Tag doesn't exist" });
@@ -73,7 +63,7 @@ export const secretTagServiceFactory = ({ secretTagDAL, permissionService }: TSe
); );
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Edit, ProjectPermissionSub.Tags); ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Edit, ProjectPermissionSub.Tags);
const updatedTag = await secretTagDAL.updateById(tag.id, { name, color, slug }); const updatedTag = await secretTagDAL.updateById(tag.id, { color, slug });
return updatedTag; return updatedTag;
}; };
@@ -107,7 +97,7 @@ export const secretTagServiceFactory = ({ secretTagDAL, permissionService }: TSe
); );
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.Tags); ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.Tags);
return tag; return { ...tag, name: tag.slug };
}; };
const getTagBySlug = async ({ actorId, actor, actorOrgId, actorAuthMethod, slug, projectId }: TGetTagBySlugDTO) => { const getTagBySlug = async ({ actorId, actor, actorOrgId, actorAuthMethod, slug, projectId }: TGetTagBySlugDTO) => {
@@ -123,7 +113,7 @@ export const secretTagServiceFactory = ({ secretTagDAL, permissionService }: TSe
); );
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.Tags); ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.Tags);
return tag; return { ...tag, name: tag.slug };
}; };
const getProjectTags = async ({ actor, actorId, actorOrgId, actorAuthMethod, projectId }: TListProjectTagsDTO) => { const getProjectTags = async ({ actor, actorId, actorOrgId, actorAuthMethod, projectId }: TListProjectTagsDTO) => {
@@ -1,14 +1,12 @@
import { TProjectPermission } from "@app/lib/types"; import { TProjectPermission } from "@app/lib/types";
export type TCreateTagDTO = { export type TCreateTagDTO = {
name: string;
color: string; color: string;
slug: string; slug: string;
} & TProjectPermission; } & TProjectPermission;
export type TUpdateTagDTO = { export type TUpdateTagDTO = {
id: string; id: string;
name?: string;
slug?: string; slug?: string;
color?: string; color?: string;
} & Omit<TProjectPermission, "projectId">; } & Omit<TProjectPermission, "projectId">;
@@ -136,7 +136,6 @@ export const secretV2BridgeDALFactory = (db: TDbClient) => {
.select(db.ref("id").withSchema(TableName.SecretTag).as("tagId")) .select(db.ref("id").withSchema(TableName.SecretTag).as("tagId"))
.select(db.ref("color").withSchema(TableName.SecretTag).as("tagColor")) .select(db.ref("color").withSchema(TableName.SecretTag).as("tagColor"))
.select(db.ref("slug").withSchema(TableName.SecretTag).as("tagSlug")) .select(db.ref("slug").withSchema(TableName.SecretTag).as("tagSlug"))
.select(db.ref("name").withSchema(TableName.SecretTag).as("tagName"))
.orderBy("id", "asc"); .orderBy("id", "asc");
const data = sqlNestRelationships({ const data = sqlNestRelationships({
@@ -147,11 +146,11 @@ export const secretV2BridgeDALFactory = (db: TDbClient) => {
{ {
key: "tagId", key: "tagId",
label: "tags" as const, label: "tags" as const,
mapper: ({ tagId: id, tagColor: color, tagSlug: slug, tagName: name }) => ({ mapper: ({ tagId: id, tagColor: color, tagSlug: slug }) => ({
id, id,
color, color,
slug, slug,
name name: slug
}) })
} }
] ]
@@ -169,14 +168,13 @@ export const secretV2BridgeDALFactory = (db: TDbClient) => {
.where({ [`${TableName.SecretV2}Id` as const]: secretId }) .where({ [`${TableName.SecretV2}Id` as const]: secretId })
.select(db.ref("id").withSchema(TableName.SecretTag).as("tagId")) .select(db.ref("id").withSchema(TableName.SecretTag).as("tagId"))
.select(db.ref("color").withSchema(TableName.SecretTag).as("tagColor")) .select(db.ref("color").withSchema(TableName.SecretTag).as("tagColor"))
.select(db.ref("slug").withSchema(TableName.SecretTag).as("tagSlug")) .select(db.ref("slug").withSchema(TableName.SecretTag).as("tagSlug"));
.select(db.ref("name").withSchema(TableName.SecretTag).as("tagName"));
return tags.map((el) => ({ return tags.map((el) => ({
id: el.tagId, id: el.tagId,
color: el.tagColor, color: el.tagColor,
slug: el.tagSlug, slug: el.tagSlug,
name: el.tagName name: el.tagSlug
})); }));
} catch (error) { } catch (error) {
throw new DatabaseError({ error, name: "get secret tags" }); throw new DatabaseError({ error, name: "get secret tags" });
@@ -210,7 +208,6 @@ export const secretV2BridgeDALFactory = (db: TDbClient) => {
.select(db.ref("id").withSchema(TableName.SecretTag).as("tagId")) .select(db.ref("id").withSchema(TableName.SecretTag).as("tagId"))
.select(db.ref("color").withSchema(TableName.SecretTag).as("tagColor")) .select(db.ref("color").withSchema(TableName.SecretTag).as("tagColor"))
.select(db.ref("slug").withSchema(TableName.SecretTag).as("tagSlug")) .select(db.ref("slug").withSchema(TableName.SecretTag).as("tagSlug"))
.select(db.ref("name").withSchema(TableName.SecretTag).as("tagName"))
.orderBy("id", "asc"); .orderBy("id", "asc");
const data = sqlNestRelationships({ const data = sqlNestRelationships({
@@ -221,11 +218,11 @@ export const secretV2BridgeDALFactory = (db: TDbClient) => {
{ {
key: "tagId", key: "tagId",
label: "tags" as const, label: "tags" as const,
mapper: ({ tagId: id, tagColor: color, tagSlug: slug, tagName: name }) => ({ mapper: ({ tagId: id, tagColor: color, tagSlug: slug }) => ({
id, id,
color, color,
slug, slug,
name name: slug
}) })
} }
] ]
@@ -290,7 +287,7 @@ export const secretV2BridgeDALFactory = (db: TDbClient) => {
})) }))
); );
if (!newSecretReferences.length) return; if (!newSecretReferences.length) return;
const secretReferences = await (tx || db)(TableName.SecretReferenceV2).insert(newSecretReferences); const secretReferences = await (tx || db).batchInsert(TableName.SecretReferenceV2, newSecretReferences);
return secretReferences; return secretReferences;
} catch (error) { } catch (error) {
throw new DatabaseError({ error, name: "UpsertSecretReference" }); throw new DatabaseError({ error, name: "UpsertSecretReference" });
@@ -350,8 +347,7 @@ export const secretV2BridgeDALFactory = (db: TDbClient) => {
.select(selectAllTableCols(TableName.SecretV2)) .select(selectAllTableCols(TableName.SecretV2))
.select(db.ref("id").withSchema(TableName.SecretTag).as("tagId")) .select(db.ref("id").withSchema(TableName.SecretTag).as("tagId"))
.select(db.ref("color").withSchema(TableName.SecretTag).as("tagColor")) .select(db.ref("color").withSchema(TableName.SecretTag).as("tagColor"))
.select(db.ref("slug").withSchema(TableName.SecretTag).as("tagSlug")) .select(db.ref("slug").withSchema(TableName.SecretTag).as("tagSlug"));
.select(db.ref("name").withSchema(TableName.SecretTag).as("tagName"));
const docs = sqlNestRelationships({ const docs = sqlNestRelationships({
data: rawDocs, data: rawDocs,
key: "id", key: "id",
@@ -360,11 +356,11 @@ export const secretV2BridgeDALFactory = (db: TDbClient) => {
{ {
key: "tagId", key: "tagId",
label: "tags" as const, label: "tags" as const,
mapper: ({ tagId: id, tagColor: color, tagSlug: slug, tagName: name }) => ({ mapper: ({ tagId: id, tagColor: color, tagSlug: slug }) => ({
id, id,
color, color,
slug, slug,
name name: slug
}) })
} }
] ]
@@ -528,8 +528,8 @@ export const reshapeBridgeSecret = (
environment: string, environment: string,
secretPath: string, secretPath: string,
secret: Omit<TSecretsV2, "encryptedValue" | "encryptedComment"> & { secret: Omit<TSecretsV2, "encryptedValue" | "encryptedComment"> & {
value?: string; value: string;
comment?: string; comment: string;
tags?: { tags?: {
id: string; id: string;
slug: string; slug: string;
@@ -542,8 +542,8 @@ export const reshapeBridgeSecret = (
secretPath, secretPath,
workspace: workspaceId, workspace: workspaceId,
environment, environment,
secretValue: secret.value, secretValue: secret.value || "",
secretComment: secret.comment, secretComment: secret.comment || "",
version: secret.version, version: secret.version,
type: secret.type, type: secret.type,
_id: secret.id, _id: secret.id,
@@ -196,7 +196,7 @@ export const secretV2BridgeServiceFactory = ({
return reshapeBridgeSecret(projectId, environment, secretPath, { return reshapeBridgeSecret(projectId, environment, secretPath, {
...secret[0], ...secret[0],
value: inputSecret.secretValue, value: inputSecret.secretValue,
comment: inputSecret.secretComment comment: inputSecret.secretComment || ""
}); });
}; };
@@ -339,8 +339,8 @@ export const secretV2BridgeServiceFactory = ({
}); });
return reshapeBridgeSecret(projectId, environment, secretPath, { return reshapeBridgeSecret(projectId, environment, secretPath, {
...updatedSecret[0], ...updatedSecret[0],
value: inputSecret.secretValue, value: inputSecret.secretValue || "",
comment: inputSecret.secretComment comment: inputSecret.secretComment || ""
}); });
}; };
@@ -378,6 +378,18 @@ export const secretV2BridgeServiceFactory = ({
throw new BadRequestError({ message: "Must be user to delete personal secret" }); throw new BadRequestError({ message: "Must be user to delete personal secret" });
} }
const secretToDelete = await secretDAL.findOne({
key: inputSecret.secretName,
folderId,
...(inputSecret.type === SecretType.Shared
? {}
: {
type: SecretType.Personal,
userId: actorId
})
});
if (!secretToDelete) throw new NotFoundError({ message: "Secret not found" });
const deletedSecret = await secretDAL.transaction(async (tx) => const deletedSecret = await secretDAL.transaction(async (tx) =>
fnSecretBulkDelete({ fnSecretBulkDelete({
projectId, projectId,
@@ -412,10 +424,10 @@ export const secretV2BridgeServiceFactory = ({
...deletedSecret[0], ...deletedSecret[0],
value: deletedSecret[0].encryptedValue value: deletedSecret[0].encryptedValue
? secretManagerDecryptor({ cipherTextBlob: deletedSecret[0].encryptedValue }).toString() ? secretManagerDecryptor({ cipherTextBlob: deletedSecret[0].encryptedValue }).toString()
: undefined, : "",
comment: deletedSecret[0].encryptedComment comment: deletedSecret[0].encryptedComment
? secretManagerDecryptor({ cipherTextBlob: deletedSecret[0].encryptedComment }).toString() ? secretManagerDecryptor({ cipherTextBlob: deletedSecret[0].encryptedComment }).toString()
: undefined : ""
}); });
}; };
@@ -429,6 +441,7 @@ export const secretV2BridgeServiceFactory = ({
actorAuthMethod, actorAuthMethod,
includeImports, includeImports,
recursive, recursive,
tagSlugs = [],
expandSecretReferences: shouldExpandSecretReferences expandSecretReferences: shouldExpandSecretReferences
}: TGetSecretsDTO) => { }: TGetSecretsDTO) => {
const { permission } = await permissionService.getProjectPermission( const { permission } = await permissionService.getProjectPermission(
@@ -496,6 +509,9 @@ export const secretV2BridgeServiceFactory = ({
: "" : ""
}) })
); );
const filteredSecrets = tagSlugs.length
? decryptedSecrets.filter((secret) => Boolean(secret.tags?.find((el) => tagSlugs.includes(el.slug))))
: decryptedSecrets;
const expandSecretReferences = expandSecretReferencesFactory({ const expandSecretReferences = expandSecretReferencesFactory({
projectId, projectId,
folderDAL, folderDAL,
@@ -504,7 +520,7 @@ export const secretV2BridgeServiceFactory = ({
}); });
if (shouldExpandSecretReferences) { if (shouldExpandSecretReferences) {
const secretsGroupByPath = groupBy(decryptedSecrets, (i) => i.secretPath); const secretsGroupByPath = groupBy(filteredSecrets, (i) => i.secretPath);
for (const secretPathKey in secretsGroupByPath) { for (const secretPathKey in secretsGroupByPath) {
if (Object.hasOwn(secretsGroupByPath, secretPathKey)) { if (Object.hasOwn(secretsGroupByPath, secretPathKey)) {
const secretsGroupByKey = secretsGroupByPath[secretPathKey].reduce( const secretsGroupByKey = secretsGroupByPath[secretPathKey].reduce(
@@ -522,7 +538,7 @@ export const secretV2BridgeServiceFactory = ({
await expandSecretReferences(secretsGroupByKey); await expandSecretReferences(secretsGroupByKey);
secretsGroupByPath[secretPathKey].forEach((decryptedSecret) => { secretsGroupByPath[secretPathKey].forEach((decryptedSecret) => {
// eslint-disable-next-line no-param-reassign // eslint-disable-next-line no-param-reassign
decryptedSecret.secretValue = secretsGroupByKey[decryptedSecret.secretKey].value; decryptedSecret.secretValue = secretsGroupByKey[decryptedSecret.secretKey].value || "";
}); });
} }
} }
@@ -530,7 +546,7 @@ export const secretV2BridgeServiceFactory = ({
if (!includeImports) { if (!includeImports) {
return { return {
secrets: decryptedSecrets secrets: filteredSecrets
}; };
} }
@@ -554,11 +570,11 @@ export const secretV2BridgeServiceFactory = ({
folderDAL, folderDAL,
secretImportDAL, secretImportDAL,
expandSecretReferences, expandSecretReferences,
decryptor: (value) => (value ? secretManagerDecryptor({ cipherTextBlob: value }).toString() : undefined) decryptor: (value) => (value ? secretManagerDecryptor({ cipherTextBlob: value }).toString() : "")
}); });
return { return {
secrets: decryptedSecrets, secrets: filteredSecrets,
imports: importedSecrets imports: importedSecrets
}; };
}; };
@@ -654,7 +670,7 @@ export const secretV2BridgeServiceFactory = ({
secretDAL, secretDAL,
folderDAL, folderDAL,
secretImportDAL, secretImportDAL,
decryptor: (value) => (value ? secretManagerDecryptor({ cipherTextBlob: value }).toString() : undefined), decryptor: (value) => (value ? secretManagerDecryptor({ cipherTextBlob: value }).toString() : ""),
expandSecretReferences: shouldExpandSecretReferences ? expandSecretReferences : undefined expandSecretReferences: shouldExpandSecretReferences ? expandSecretReferences : undefined
}); });
@@ -662,12 +678,11 @@ export const secretV2BridgeServiceFactory = ({
for (let j = 0; j < importedSecrets[i].secrets.length; j += 1) { for (let j = 0; j < importedSecrets[i].secrets.length; j += 1) {
const importedSecret = importedSecrets[i].secrets[j]; const importedSecret = importedSecrets[i].secrets[j];
if (secretName === importedSecret.key) { if (secretName === importedSecret.key) {
return reshapeBridgeSecret( return reshapeBridgeSecret(projectId, importedSecrets[i].environment, importedSecrets[i].secretPath, {
projectId, ...importedSecret,
importedSecrets[i].environment, value: importedSecret.secretValue || "",
importedSecrets[i].secretPath, comment: importedSecret.secretComment || ""
importedSecret });
);
} }
} }
} }
@@ -676,7 +691,7 @@ export const secretV2BridgeServiceFactory = ({
let secretValue = secret.encryptedValue let secretValue = secret.encryptedValue
? secretManagerDecryptor({ cipherTextBlob: secret.encryptedValue }).toString() ? secretManagerDecryptor({ cipherTextBlob: secret.encryptedValue }).toString()
: undefined; : "";
if (shouldExpandSecretReferences && secretValue) { if (shouldExpandSecretReferences && secretValue) {
const secretReferenceExpandedRecord = { const secretReferenceExpandedRecord = {
[secret.key]: { value: secretValue } [secret.key]: { value: secretValue }
@@ -691,7 +706,7 @@ export const secretV2BridgeServiceFactory = ({
value: secretValue, value: secretValue,
comment: secret.encryptedComment comment: secret.encryptedComment
? secretManagerDecryptor({ cipherTextBlob: secret.encryptedComment }).toString() ? secretManagerDecryptor({ cipherTextBlob: secret.encryptedComment }).toString()
: undefined : ""
}); });
}; };
@@ -781,10 +796,8 @@ export const secretV2BridgeServiceFactory = ({
return newSecrets.map((el) => return newSecrets.map((el) =>
reshapeBridgeSecret(projectId, environment, secretPath, { reshapeBridgeSecret(projectId, environment, secretPath, {
...el, ...el,
value: el.encryptedValue ? secretManagerDecryptor({ cipherTextBlob: el.encryptedValue }).toString() : undefined, value: el.encryptedValue ? secretManagerDecryptor({ cipherTextBlob: el.encryptedValue }).toString() : "",
comment: el.encryptedComment comment: el.encryptedComment ? secretManagerDecryptor({ cipherTextBlob: el.encryptedComment }).toString() : ""
? secretManagerDecryptor({ cipherTextBlob: el.encryptedComment }).toString()
: undefined
}) })
); );
}; };
@@ -902,10 +915,8 @@ export const secretV2BridgeServiceFactory = ({
return secrets.map((el) => return secrets.map((el) =>
reshapeBridgeSecret(projectId, environment, secretPath, { reshapeBridgeSecret(projectId, environment, secretPath, {
...el, ...el,
value: el.encryptedValue ? secretManagerDecryptor({ cipherTextBlob: el.encryptedValue }).toString() : undefined, value: el.encryptedValue ? secretManagerDecryptor({ cipherTextBlob: el.encryptedValue }).toString() : "",
comment: el.encryptedComment comment: el.encryptedComment ? secretManagerDecryptor({ cipherTextBlob: el.encryptedComment }).toString() : ""
? secretManagerDecryptor({ cipherTextBlob: el.encryptedComment }).toString()
: undefined
}) })
); );
}; };
@@ -981,10 +992,8 @@ export const secretV2BridgeServiceFactory = ({
return secretsDeleted.map((el) => return secretsDeleted.map((el) =>
reshapeBridgeSecret(projectId, environment, secretPath, { reshapeBridgeSecret(projectId, environment, secretPath, {
...el, ...el,
value: el.encryptedValue ? secretManagerDecryptor({ cipherTextBlob: el.encryptedValue }).toString() : undefined, value: el.encryptedValue ? secretManagerDecryptor({ cipherTextBlob: el.encryptedValue }).toString() : "",
comment: el.encryptedComment comment: el.encryptedComment ? secretManagerDecryptor({ cipherTextBlob: el.encryptedComment }).toString() : ""
? secretManagerDecryptor({ cipherTextBlob: el.encryptedComment }).toString()
: undefined
}) })
); );
}; };
@@ -1020,10 +1029,8 @@ export const secretV2BridgeServiceFactory = ({
return secretVersions.map((el) => return secretVersions.map((el) =>
reshapeBridgeSecret(folder.projectId, folder.environment.envSlug, "/", { reshapeBridgeSecret(folder.projectId, folder.environment.envSlug, "/", {
...el, ...el,
value: el.encryptedValue ? secretManagerDecryptor({ cipherTextBlob: el.encryptedValue }).toString() : undefined, value: el.encryptedValue ? secretManagerDecryptor({ cipherTextBlob: el.encryptedValue }).toString() : "",
comment: el.encryptedComment comment: el.encryptedComment ? secretManagerDecryptor({ cipherTextBlob: el.encryptedComment }).toString() : ""
? secretManagerDecryptor({ cipherTextBlob: el.encryptedComment }).toString()
: undefined
}) })
); );
}; };
@@ -20,6 +20,7 @@ export type TGetSecretsDTO = {
environment: string; environment: string;
includeImports?: boolean; includeImports?: boolean;
recursive?: boolean; recursive?: boolean;
tagSlugs?: string[];
} & TProjectPermission; } & TProjectPermission;
export type TGetASecretDTO = { export type TGetASecretDTO = {
+9 -13
View File
@@ -123,7 +123,6 @@ export const secretDALFactory = (db: TDbClient) => {
.select(db.ref("id").withSchema(TableName.SecretTag).as("tagId")) .select(db.ref("id").withSchema(TableName.SecretTag).as("tagId"))
.select(db.ref("color").withSchema(TableName.SecretTag).as("tagColor")) .select(db.ref("color").withSchema(TableName.SecretTag).as("tagColor"))
.select(db.ref("slug").withSchema(TableName.SecretTag).as("tagSlug")) .select(db.ref("slug").withSchema(TableName.SecretTag).as("tagSlug"))
.select(db.ref("name").withSchema(TableName.SecretTag).as("tagName"))
.orderBy("id", "asc"); .orderBy("id", "asc");
const data = sqlNestRelationships({ const data = sqlNestRelationships({
data: secs, data: secs,
@@ -133,11 +132,11 @@ export const secretDALFactory = (db: TDbClient) => {
{ {
key: "tagId", key: "tagId",
label: "tags" as const, label: "tags" as const,
mapper: ({ tagId: id, tagColor: color, tagSlug: slug, tagName: name }) => ({ mapper: ({ tagId: id, tagColor: color, tagSlug: slug }) => ({
id, id,
color, color,
slug, slug,
name name: slug
}) })
} }
] ]
@@ -155,14 +154,13 @@ export const secretDALFactory = (db: TDbClient) => {
.where({ [`${TableName.Secret}Id` as const]: secretId }) .where({ [`${TableName.Secret}Id` as const]: secretId })
.select(db.ref("id").withSchema(TableName.SecretTag).as("tagId")) .select(db.ref("id").withSchema(TableName.SecretTag).as("tagId"))
.select(db.ref("color").withSchema(TableName.SecretTag).as("tagColor")) .select(db.ref("color").withSchema(TableName.SecretTag).as("tagColor"))
.select(db.ref("slug").withSchema(TableName.SecretTag).as("tagSlug")) .select(db.ref("slug").withSchema(TableName.SecretTag).as("tagSlug"));
.select(db.ref("name").withSchema(TableName.SecretTag).as("tagName"));
return tags.map((el) => ({ return tags.map((el) => ({
id: el.tagId, id: el.tagId,
color: el.tagColor, color: el.tagColor,
slug: el.tagSlug, slug: el.tagSlug,
name: el.tagName name: el.tagSlug
})); }));
} catch (error) { } catch (error) {
throw new DatabaseError({ error, name: "get secret tags" }); throw new DatabaseError({ error, name: "get secret tags" });
@@ -188,7 +186,6 @@ export const secretDALFactory = (db: TDbClient) => {
.select(db.ref("id").withSchema(TableName.SecretTag).as("tagId")) .select(db.ref("id").withSchema(TableName.SecretTag).as("tagId"))
.select(db.ref("color").withSchema(TableName.SecretTag).as("tagColor")) .select(db.ref("color").withSchema(TableName.SecretTag).as("tagColor"))
.select(db.ref("slug").withSchema(TableName.SecretTag).as("tagSlug")) .select(db.ref("slug").withSchema(TableName.SecretTag).as("tagSlug"))
.select(db.ref("name").withSchema(TableName.SecretTag).as("tagName"))
.orderBy("id", "asc"); .orderBy("id", "asc");
const data = sqlNestRelationships({ const data = sqlNestRelationships({
data: secs, data: secs,
@@ -198,11 +195,11 @@ export const secretDALFactory = (db: TDbClient) => {
{ {
key: "tagId", key: "tagId",
label: "tags" as const, label: "tags" as const,
mapper: ({ tagId: id, tagColor: color, tagSlug: slug, tagName: name }) => ({ mapper: ({ tagId: id, tagColor: color, tagSlug: slug }) => ({
id, id,
color, color,
slug, slug,
name name: slug
}) })
} }
] ]
@@ -318,8 +315,7 @@ export const secretDALFactory = (db: TDbClient) => {
.select(selectAllTableCols(TableName.Secret)) .select(selectAllTableCols(TableName.Secret))
.select(db.ref("id").withSchema(TableName.SecretTag).as("tagId")) .select(db.ref("id").withSchema(TableName.SecretTag).as("tagId"))
.select(db.ref("color").withSchema(TableName.SecretTag).as("tagColor")) .select(db.ref("color").withSchema(TableName.SecretTag).as("tagColor"))
.select(db.ref("slug").withSchema(TableName.SecretTag).as("tagSlug")) .select(db.ref("slug").withSchema(TableName.SecretTag).as("tagSlug"));
.select(db.ref("name").withSchema(TableName.SecretTag).as("tagName"));
const docs = sqlNestRelationships({ const docs = sqlNestRelationships({
data: rawDocs, data: rawDocs,
key: "id", key: "id",
@@ -328,11 +324,11 @@ export const secretDALFactory = (db: TDbClient) => {
{ {
key: "tagId", key: "tagId",
label: "tags" as const, label: "tags" as const,
mapper: ({ tagId: id, tagColor: color, tagSlug: slug, tagName: name }) => ({ mapper: ({ tagId: id, tagColor: color, tagSlug: slug }) => ({
id, id,
color, color,
slug, slug,
name name: slug
}) })
} }
] ]
+1 -2
View File
@@ -370,7 +370,6 @@ export const decryptSecretRaw = (
id: string; id: string;
slug: string; slug: string;
color?: string | null; color?: string | null;
name: string;
}[]; }[];
}, },
key: string key: string
@@ -412,7 +411,7 @@ export const decryptSecretRaw = (
_id: secret.id, _id: secret.id,
id: secret.id, id: secret.id,
user: secret.userId, user: secret.userId,
tags: secret.tags, tags: secret.tags?.map((el) => ({ ...el, name: el.slug })),
skipMultilineEncoding: secret.skipMultilineEncoding, skipMultilineEncoding: secret.skipMultilineEncoding,
secretReminderRepeatDays: secret.secretReminderRepeatDays, secretReminderRepeatDays: secret.secretReminderRepeatDays,
secretReminderNote: secret.secretReminderNote, secretReminderNote: secret.secretReminderNote,
+157 -106
View File
@@ -6,11 +6,12 @@ import { TSecretApprovalRequestDALFactory } from "@app/ee/services/secret-approv
import { TSecretRotationDALFactory } from "@app/ee/services/secret-rotation/secret-rotation-dal"; import { TSecretRotationDALFactory } from "@app/ee/services/secret-rotation/secret-rotation-dal";
import { TSnapshotDALFactory } from "@app/ee/services/secret-snapshot/snapshot-dal"; import { TSnapshotDALFactory } from "@app/ee/services/secret-snapshot/snapshot-dal";
import { TSnapshotSecretV2DALFactory } from "@app/ee/services/secret-snapshot/snapshot-secret-v2-dal"; import { TSnapshotSecretV2DALFactory } from "@app/ee/services/secret-snapshot/snapshot-secret-v2-dal";
import { KeyStorePrefixes, KeyStoreTtls, TKeyStoreFactory } from "@app/keystore/keystore";
import { getConfig } from "@app/lib/config/env"; import { getConfig } from "@app/lib/config/env";
import { decryptSymmetric128BitHexKeyUTF8 } from "@app/lib/crypto"; import { decryptSymmetric128BitHexKeyUTF8 } from "@app/lib/crypto";
import { daysToMillisecond, secondsToMillis } from "@app/lib/dates"; import { daysToMillisecond, secondsToMillis } from "@app/lib/dates";
import { BadRequestError } from "@app/lib/errors"; import { BadRequestError } from "@app/lib/errors";
import { groupBy, isSamePath, unique } from "@app/lib/fn"; import { getTimeDifferenceInSeconds, groupBy, isSamePath, unique } from "@app/lib/fn";
import { logger } from "@app/lib/logger"; import { logger } from "@app/lib/logger";
import { QueueJobs, QueueName, TQueueServiceFactory } from "@app/queue"; import { QueueJobs, QueueName, TQueueServiceFactory } from "@app/queue";
import { TProjectBotDALFactory } from "@app/services/project-bot/project-bot-dal"; import { TProjectBotDALFactory } from "@app/services/project-bot/project-bot-dal";
@@ -73,12 +74,13 @@ type TSecretQueueFactoryDep = {
secretVersionTagDAL: TSecretVersionTagDALFactory; secretVersionTagDAL: TSecretVersionTagDALFactory;
kmsService: Pick<TKmsServiceFactory, "createCipherPairWithDataKey">; kmsService: Pick<TKmsServiceFactory, "createCipherPairWithDataKey">;
secretV2BridgeDAL: TSecretV2BridgeDALFactory; secretV2BridgeDAL: TSecretV2BridgeDALFactory;
secretVersionV2BridgeDAL: Pick<TSecretVersionV2DALFactory, "insertMany" | "findLatestVersionMany">; secretVersionV2BridgeDAL: Pick<TSecretVersionV2DALFactory, "batchInsert" | "insertMany" | "findLatestVersionMany">;
secretVersionTagV2BridgeDAL: Pick<TSecretVersionV2TagDALFactory, "insertMany">; secretVersionTagV2BridgeDAL: Pick<TSecretVersionV2TagDALFactory, "insertMany" | "batchInsert">;
secretRotationDAL: Pick<TSecretRotationDALFactory, "secretOutputV2InsertMany" | "find">; secretRotationDAL: Pick<TSecretRotationDALFactory, "secretOutputV2InsertMany" | "find">;
secretApprovalRequestDAL: Pick<TSecretApprovalRequestDALFactory, "deleteByProjectId">; secretApprovalRequestDAL: Pick<TSecretApprovalRequestDALFactory, "deleteByProjectId">;
snapshotDAL: Pick<TSnapshotDALFactory, "findNSecretV1SnapshotByFolderId" | "deleteSnapshotsAboveLimit">; snapshotDAL: Pick<TSnapshotDALFactory, "findNSecretV1SnapshotByFolderId" | "deleteSnapshotsAboveLimit">;
snapshotSecretV2BridgeDAL: Pick<TSnapshotSecretV2DALFactory, "insertMany">; snapshotSecretV2BridgeDAL: Pick<TSnapshotSecretV2DALFactory, "insertMany" | "batchInsert">;
keyStore: Pick<TKeyStoreFactory, "acquireLock" | "setItemWithExpiry" | "getItem">;
}; };
export type TGetSecrets = { export type TGetSecrets = {
@@ -122,7 +124,8 @@ export const secretQueueFactory = ({
secretRotationDAL, secretRotationDAL,
snapshotDAL, snapshotDAL,
snapshotSecretV2BridgeDAL, snapshotSecretV2BridgeDAL,
secretApprovalRequestDAL secretApprovalRequestDAL,
keyStore
}: TSecretQueueFactoryDep) => { }: TSecretQueueFactoryDep) => {
const removeSecretReminder = async (dto: TRemoveSecretReminderDTO) => { const removeSecretReminder = async (dto: TRemoveSecretReminderDTO) => {
const appCfg = getConfig(); const appCfg = getConfig();
@@ -576,7 +579,6 @@ export const secretQueueFactory = ({
) )
); );
} }
const { shouldUseSecretV2Bridge, botKey } = await projectBotService.getBotKey(projectId); const { shouldUseSecretV2Bridge, botKey } = await projectBotService.getBotKey(projectId);
const { decryptor: secretManagerDecryptor } = await kmsService.createCipherPairWithDataKey({ const { decryptor: secretManagerDecryptor } = await kmsService.createCipherPairWithDataKey({
type: KmsDataKey.SecretManager, type: KmsDataKey.SecretManager,
@@ -641,108 +643,157 @@ export const secretQueueFactory = ({
`getIntegrationSecrets: secret integration sync started [jobId=${job.id}] [jobId=${job.id}] [projectId=${job.data.projectId}] [environment=${job.data.environment}] [secretPath=${job.data.secretPath}] [depth=${job.data.depth}]` `getIntegrationSecrets: secret integration sync started [jobId=${job.id}] [jobId=${job.id}] [projectId=${job.data.projectId}] [environment=${job.data.environment}] [secretPath=${job.data.secretPath}] [depth=${job.data.depth}]`
); );
const secrets = shouldUseSecretV2Bridge const lock = await keyStore.acquireLock(
? await getIntegrationSecretsV2({ [KeyStorePrefixes.SyncSecretIntegrationLock(projectId, environment, secretPath)],
environment, 10000,
projectId, {
folderId: folder.id, retryCount: 3,
depth: 1, retryDelay: 2000
decryptor: (value) => (value ? secretManagerDecryptor({ cipherTextBlob: value }).toString() : "") }
}) );
: await getIntegrationSecrets({ const lockAcquiredTime = new Date();
environment,
projectId,
folderId: folder.id,
key: botKey as string,
depth: 1
});
for (const integration of toBeSyncedIntegrations) { const lastRunSyncIntegrationTimestamp = await keyStore.getItem(
const integrationAuth = { KeyStorePrefixes.SyncSecretIntegrationLastRunTimestamp(projectId, environment, secretPath)
...integration.integrationAuth, );
createdAt: new Date(),
updatedAt: new Date(),
projectId: integration.projectId
};
const { accessToken, accessId } = await integrationAuthService.getIntegrationAccessToken( // check whether the integration should wait or not
integrationAuth, if (lastRunSyncIntegrationTimestamp) {
shouldUseSecretV2Bridge, const INTEGRATION_INTERVAL = 2000;
botKey const isStaleSyncIntegration = new Date(job.timestamp) < new Date(lastRunSyncIntegrationTimestamp);
if (isStaleSyncIntegration) {
logger.info(
`getIntegrationSecrets: secret integration sync stale [jobId=${job.id}] [jobId=${job.id}] [projectId=${job.data.projectId}] [environment=${job.data.environment}] [secretPath=${job.data.secretPath}] [depth=${job.data.depth}]`
);
return;
}
const timeDifferenceWithLastIntegration = getTimeDifferenceInSeconds(
lockAcquiredTime.toISOString(),
lastRunSyncIntegrationTimestamp
); );
let awsAssumeRoleArn = null; if (timeDifferenceWithLastIntegration < INTEGRATION_INTERVAL && timeDifferenceWithLastIntegration > 0)
if (shouldUseSecretV2Bridge) { await new Promise((resolve) => {
if (integrationAuth.encryptedAwsAssumeIamRoleArn) { setTimeout(resolve, 2000 - timeDifferenceWithLastIntegration * 1000);
awsAssumeRoleArn = secretManagerDecryptor({
cipherTextBlob: Buffer.from(integrationAuth.encryptedAwsAssumeIamRoleArn)
}).toString();
}
} else if (
integrationAuth.awsAssumeIamRoleArnTag &&
integrationAuth.awsAssumeIamRoleArnIV &&
integrationAuth.awsAssumeIamRoleArnCipherText
) {
awsAssumeRoleArn = decryptSymmetric128BitHexKeyUTF8({
ciphertext: integrationAuth.awsAssumeIamRoleArnCipherText,
iv: integrationAuth.awsAssumeIamRoleArnIV,
tag: integrationAuth.awsAssumeIamRoleArnTag,
key: botKey as string
}); });
}
const suffixedSecrets: typeof secrets = {};
const metadata = integration.metadata as Record<string, string>;
if (metadata) {
Object.keys(secrets).forEach((key) => {
const prefix = metadata?.secretPrefix || "";
const suffix = metadata?.secretSuffix || "";
const newKey = prefix + key + suffix;
suffixedSecrets[newKey] = secrets[key];
});
}
try {
// akhilmhdh: this needs to changed later to be more easier to use
// at present this is not at all extendable like to add a new parameter for just one integration need to modify multiple places
const response = await syncIntegrationSecrets({
createManySecretsRawFn,
updateManySecretsRawFn,
integrationDAL,
integration,
integrationAuth,
secrets: Object.keys(suffixedSecrets).length !== 0 ? suffixedSecrets : secrets,
accessId: accessId as string,
awsAssumeRoleArn,
accessToken,
projectId,
appendices: {
prefix: metadata?.secretPrefix || "",
suffix: metadata?.secretSuffix || ""
}
});
await integrationDAL.updateById(integration.id, {
lastSyncJobId: job.id,
lastUsed: new Date(),
syncMessage: response?.syncMessage ?? "",
isSynced: response?.isSynced ?? true
});
} catch (err) {
logger.info("Secret integration sync error: %o", err);
const message =
(err instanceof AxiosError ? JSON.stringify(err?.response?.data) : (err as Error)?.message) ||
"Unknown error occurred.";
await integrationDAL.updateById(integration.id, {
lastSyncJobId: job.id,
lastUsed: new Date(),
syncMessage: message,
isSynced: false
});
}
} }
// akhilmhdh: this try catch is for lock release
try {
const secrets = shouldUseSecretV2Bridge
? await getIntegrationSecretsV2({
environment,
projectId,
folderId: folder.id,
depth: 1,
decryptor: (value) => (value ? secretManagerDecryptor({ cipherTextBlob: value }).toString() : "")
})
: await getIntegrationSecrets({
environment,
projectId,
folderId: folder.id,
key: botKey as string,
depth: 1
});
for (const integration of toBeSyncedIntegrations) {
const integrationAuth = {
...integration.integrationAuth,
createdAt: new Date(),
updatedAt: new Date(),
projectId: integration.projectId
};
const { accessToken, accessId } = await integrationAuthService.getIntegrationAccessToken(
integrationAuth,
shouldUseSecretV2Bridge,
botKey
);
let awsAssumeRoleArn = null;
if (shouldUseSecretV2Bridge) {
if (integrationAuth.encryptedAwsAssumeIamRoleArn) {
awsAssumeRoleArn = secretManagerDecryptor({
cipherTextBlob: Buffer.from(integrationAuth.encryptedAwsAssumeIamRoleArn)
}).toString();
}
} else if (
integrationAuth.awsAssumeIamRoleArnTag &&
integrationAuth.awsAssumeIamRoleArnIV &&
integrationAuth.awsAssumeIamRoleArnCipherText
) {
awsAssumeRoleArn = decryptSymmetric128BitHexKeyUTF8({
ciphertext: integrationAuth.awsAssumeIamRoleArnCipherText,
iv: integrationAuth.awsAssumeIamRoleArnIV,
tag: integrationAuth.awsAssumeIamRoleArnTag,
key: botKey as string
});
}
const suffixedSecrets: typeof secrets = {};
const metadata = integration.metadata as Record<string, string>;
if (metadata) {
Object.keys(secrets).forEach((key) => {
const prefix = metadata?.secretPrefix || "";
const suffix = metadata?.secretSuffix || "";
const newKey = prefix + key + suffix;
suffixedSecrets[newKey] = secrets[key];
});
}
// akhilmhdh: this try catch is for catching integration error and saving it in db
try {
// akhilmhdh: this needs to changed later to be more easier to use
// at present this is not at all extendable like to add a new parameter for just one integration need to modify multiple places
const response = await syncIntegrationSecrets({
createManySecretsRawFn,
updateManySecretsRawFn,
integrationDAL,
integration,
integrationAuth,
secrets: Object.keys(suffixedSecrets).length !== 0 ? suffixedSecrets : secrets,
accessId: accessId as string,
awsAssumeRoleArn,
accessToken,
projectId,
appendices: {
prefix: metadata?.secretPrefix || "",
suffix: metadata?.secretSuffix || ""
}
});
await integrationDAL.updateById(integration.id, {
lastSyncJobId: job.id,
lastUsed: new Date(),
syncMessage: response?.syncMessage ?? "",
isSynced: response?.isSynced ?? true
});
} catch (err) {
logger.error(
err,
`Secret integration sync error [projectId=${job.data.projectId}] [environment=${job.data.environment}] [secretPath=${job.data.secretPath}]`
);
const message =
(err instanceof AxiosError ? JSON.stringify(err?.response?.data) : (err as Error)?.message) ||
"Unknown error occurred.";
await integrationDAL.updateById(integration.id, {
lastSyncJobId: job.id,
lastUsed: new Date(),
syncMessage: message,
isSynced: false
});
}
}
} finally {
await lock.release();
}
await keyStore.setItemWithExpiry(
KeyStorePrefixes.SyncSecretIntegrationLastRunTimestamp(projectId, environment, secretPath),
KeyStoreTtls.SetSyncSecretIntegrationLastRunTimestampInSeconds,
lockAcquiredTime.toISOString()
);
logger.info("Secret integration sync ended: %s", job.id); logger.info("Secret integration sync ended: %s", job.id);
}); });
@@ -828,7 +879,7 @@ export const secretQueueFactory = ({
secretId: string; secretId: string;
references: { environment: string; secretPath: string; secretKey: string }[]; references: { environment: string; secretPath: string; secretKey: string }[];
}[] = []; }[] = [];
await secretV2BridgeDAL.insertMany( await secretV2BridgeDAL.batchInsert(
projectV1Secrets.map((el) => { projectV1Secrets.map((el) => {
const key = decryptSymmetric128BitHexKeyUTF8({ const key = decryptSymmetric128BitHexKeyUTF8({
ciphertext: el.secretKeyCiphertext, ciphertext: el.secretKeyCiphertext,
@@ -1004,14 +1055,14 @@ export const secretQueueFactory = ({
const projectV3SecretVersions = Object.values(projectV3SecretVersionsGroupById); const projectV3SecretVersions = Object.values(projectV3SecretVersionsGroupById);
if (projectV3SecretVersions.length) { if (projectV3SecretVersions.length) {
await secretVersionV2BridgeDAL.insertMany(projectV3SecretVersions, tx); await secretVersionV2BridgeDAL.batchInsert(projectV3SecretVersions, tx);
} }
if (projectV3SecretVersionTags.length) { if (projectV3SecretVersionTags.length) {
await secretVersionTagV2BridgeDAL.insertMany(projectV3SecretVersionTags, tx); await secretVersionTagV2BridgeDAL.batchInsert(projectV3SecretVersionTags, tx);
} }
if (projectV3SnapshotSecrets.length) { if (projectV3SnapshotSecrets.length) {
await snapshotSecretV2BridgeDAL.insertMany(projectV3SnapshotSecrets, tx); await snapshotSecretV2BridgeDAL.batchInsert(projectV3SnapshotSecrets, tx);
} }
await snapshotDAL.deleteSnapshotsAboveLimit(folderId, SNAPSHOT_BATCH_SIZE, tx); await snapshotDAL.deleteSnapshotsAboveLimit(folderId, SNAPSHOT_BATCH_SIZE, tx);
} }
+11 -5
View File
@@ -964,7 +964,8 @@ export const secretServiceFactory = ({
environment, environment,
includeImports, includeImports,
expandSecretReferences, expandSecretReferences,
recursive recursive,
tagSlugs = []
}: TGetSecretsRawDTO) => { }: TGetSecretsRawDTO) => {
const { botKey, shouldUseSecretV2Bridge } = await projectBotService.getBotKey(projectId); const { botKey, shouldUseSecretV2Bridge } = await projectBotService.getBotKey(projectId);
if (shouldUseSecretV2Bridge) { if (shouldUseSecretV2Bridge) {
@@ -978,7 +979,8 @@ export const secretServiceFactory = ({
path, path,
recursive, recursive,
actorAuthMethod, actorAuthMethod,
includeImports includeImports,
tagSlugs
}); });
return { secrets, imports }; return { secrets, imports };
} }
@@ -998,6 +1000,9 @@ export const secretServiceFactory = ({
}); });
const decryptedSecrets = secrets.map((el) => decryptSecretRaw(el, botKey)); const decryptedSecrets = secrets.map((el) => decryptSecretRaw(el, botKey));
const filteredSecrets = tagSlugs.length
? decryptedSecrets.filter((secret) => Boolean(secret.tags?.find((el) => tagSlugs.includes(el.slug))))
: decryptedSecrets;
const processedImports = (imports || [])?.map(({ secrets: importedSecrets, ...el }) => { const processedImports = (imports || [])?.map(({ secrets: importedSecrets, ...el }) => {
const decryptedImportSecrets = importedSecrets.map((sec) => const decryptedImportSecrets = importedSecrets.map((sec) =>
decryptSecretRaw( decryptSecretRaw(
@@ -1106,14 +1111,14 @@ export const secretServiceFactory = ({
}; };
// expand secrets // expand secrets
await batchSecretsExpand(decryptedSecrets); await batchSecretsExpand(filteredSecrets);
// expand imports by batch // expand imports by batch
await Promise.all(processedImports.map((processedImport) => batchSecretsExpand(processedImport.secrets))); await Promise.all(processedImports.map((processedImport) => batchSecretsExpand(processedImport.secrets)));
} }
return { return {
secrets: decryptedSecrets, secrets: filteredSecrets,
imports: processedImports imports: processedImports
}; };
}; };
@@ -1149,6 +1154,7 @@ export const secretServiceFactory = ({
type, type,
secretName secretName
}); });
return secret; return secret;
} }
@@ -2081,7 +2087,7 @@ export const secretServiceFactory = ({
return { return {
...updatedSecret[0], ...updatedSecret[0],
tags: [...existingSecretTags, ...tags].map((t) => ({ id: t.id, slug: t.slug, name: t.name, color: t.color })) tags: [...existingSecretTags, ...tags].map((t) => ({ id: t.id, slug: t.slug, name: t.slug, color: t.color }))
}; };
}; };
@@ -149,6 +149,7 @@ export type TGetSecretsRawDTO = {
environment: string; environment: string;
includeImports?: boolean; includeImports?: boolean;
recursive?: boolean; recursive?: boolean;
tagSlugs?: string[];
} & TProjectPermission; } & TProjectPermission;
export type TGetASecretRawDTO = { export type TGetASecretRawDTO = {
+4
View File
@@ -404,6 +404,10 @@ func CallGetRawSecretsV3(httpClient *resty.Client, request GetRawSecretsV3Reques
SetQueryParam("environment", request.Environment). SetQueryParam("environment", request.Environment).
SetQueryParam("secretPath", request.SecretPath) SetQueryParam("secretPath", request.SecretPath)
if request.TagSlugs != "" {
req.SetQueryParam("tagSlugs", request.TagSlugs)
}
if request.IncludeImport { if request.IncludeImport {
req.SetQueryParam("include_imports", "true") req.SetQueryParam("include_imports", "true")
} }
+1
View File
@@ -574,6 +574,7 @@ type GetRawSecretsV3Request struct {
SecretPath string `json:"secretPath"` SecretPath string `json:"secretPath"`
IncludeImport bool `json:"include_imports"` IncludeImport bool `json:"include_imports"`
Recursive bool `json:"recursive"` Recursive bool `json:"recursive"`
TagSlugs string `json:"tagSlugs,omitempty"`
} }
type GetRawSecretsV3Response struct { type GetRawSecretsV3Response struct {
+1 -1
View File
@@ -312,7 +312,7 @@ func ParseAgentConfig(configFile []byte) (*Config, error) {
func secretTemplateFunction(accessToken string, existingEtag string, currentEtag *string) func(string, string, string) ([]models.SingleEnvironmentVariable, error) { func secretTemplateFunction(accessToken string, existingEtag string, currentEtag *string) func(string, string, string) ([]models.SingleEnvironmentVariable, error) {
return func(projectID, envSlug, secretPath string) ([]models.SingleEnvironmentVariable, error) { return func(projectID, envSlug, secretPath string) ([]models.SingleEnvironmentVariable, error) {
res, err := util.GetPlainTextSecretsV3(accessToken, projectID, envSlug, secretPath, false, false) res, err := util.GetPlainTextSecretsV3(accessToken, projectID, envSlug, secretPath, false, false, "")
if err != nil { if err != nil {
return nil, err return nil, err
} }
+12 -5
View File
@@ -14,6 +14,7 @@ import (
"github.com/Infisical/infisical-merge/packages/util" "github.com/Infisical/infisical-merge/packages/util"
"github.com/rs/zerolog/log" "github.com/rs/zerolog/log"
"github.com/spf13/cobra" "github.com/spf13/cobra"
"gopkg.in/yaml.v2"
) )
const ( const (
@@ -188,7 +189,7 @@ func formatEnvs(envs []models.SingleEnvironmentVariable, format string) (string,
case FormatCSV: case FormatCSV:
return formatAsCSV(envs), nil return formatAsCSV(envs), nil
case FormatYaml: case FormatYaml:
return formatAsYaml(envs), nil return formatAsYaml(envs)
default: default:
return "", fmt.Errorf("invalid format type: %s. Available format types are [%s]", format, []string{FormatDotenv, FormatJson, FormatCSV, FormatYaml, FormatDotEnvExport}) return "", fmt.Errorf("invalid format type: %s. Available format types are [%s]", format, []string{FormatDotenv, FormatJson, FormatCSV, FormatYaml, FormatDotEnvExport})
} }
@@ -224,12 +225,18 @@ func formatAsDotEnvExport(envs []models.SingleEnvironmentVariable) string {
return dotenv return dotenv
} }
func formatAsYaml(envs []models.SingleEnvironmentVariable) string { func formatAsYaml(envs []models.SingleEnvironmentVariable) (string, error) {
var dotenv string m := make(map[string]string)
for _, env := range envs { for _, env := range envs {
dotenv += fmt.Sprintf("%s: %s\n", env.Key, env.Value) m[env.Key] = env.Value
} }
return dotenv
yamlBytes, err := yaml.Marshal(m)
if err != nil {
return "", fmt.Errorf("failed to format environment variables as YAML: %w", err)
}
return string(yamlBytes), nil
} }
// Format environment variables as a JSON file // Format environment variables as a JSON file

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