mirror of
https://github.com/Infisical/infisical.git
synced 2025-04-07 08:38:28 +00:00
Compare commits
197 Commits
daniel/fix
...
install-ex
Author | SHA1 | Date | |
---|---|---|---|
af4428acec | |||
61370cc6b2 | |||
7b4b802a9b | |||
95cf3cf6cc | |||
d021b414cf | |||
bed75c36dd | |||
04cb499f0f | |||
189a610f52 | |||
00039ba0e4 | |||
abdcb95a8f | |||
47ea4ae9a6 | |||
903b2c3dc6 | |||
c795b3b3a0 | |||
0d8ff1828e | |||
30d6af7760 | |||
44b42359da | |||
38373722e3 | |||
7ec68ca9a1 | |||
a49d5b121b | |||
901ff7a605 | |||
ba4aa15c92 | |||
a00103aa1e | |||
0c17cc3577 | |||
51d84a47b9 | |||
d529670a52 | |||
ed0463e3e4 | |||
20db0a255c | |||
6fe1d77375 | |||
f90855e7a5 | |||
97f5c33aea | |||
34c2200269 | |||
69925721cc | |||
0961d2f1c6 | |||
b9bd518aa6 | |||
692c9b5d9c | |||
32046ca880 | |||
590dbbcb04 | |||
27d2af4979 | |||
a1e6c6f7d5 | |||
cc94a3366a | |||
6a6c084b8a | |||
7baa3b4cbe | |||
6cab7504fc | |||
ca3d8c5594 | |||
28a2a6c41a | |||
05efd95472 | |||
fa31f87479 | |||
b176f13392 | |||
f4384bb01e | |||
856c2423be | |||
4570de09ae | |||
4feff5b4ca | |||
6081e2927e | |||
0b42f29916 | |||
b60d0992f4 | |||
a8a68f600c | |||
742f5f6621 | |||
f993e4aa5c | |||
bb6416acb7 | |||
f3cd7efe0e | |||
2b16c19b70 | |||
943b540383 | |||
e180021aa6 | |||
8e08c443ad | |||
dae26daeeb | |||
170f8d9add | |||
8d41ef198a | |||
69d60a227a | |||
c8eefcfbf9 | |||
53cec754cc | |||
5db3e177eb | |||
3fcc3ccff4 | |||
df07d7b6d7 | |||
28a655bef1 | |||
5f2cd04f46 | |||
897ce1f267 | |||
6afc17b84b | |||
9017a5e838 | |||
cb8e4d884e | |||
16807c3dd6 | |||
61791e385c | |||
bbd7bfb0f5 | |||
4de8c48b2c | |||
a4bbe2c612 | |||
541a2e7d05 | |||
ea4e51d826 | |||
3bc920c593 | |||
f4244c6d4d | |||
e1b9965f01 | |||
705b4f7513 | |||
df38c761ad | |||
32a84471f2 | |||
fc4a20caf2 | |||
ea14df2cbd | |||
6bd6cac366 | |||
45294253aa | |||
635fbdc80b | |||
d20c48b7cf | |||
1fc18fe23b | |||
99403e122b | |||
5176e70437 | |||
82b2b0af97 | |||
e313c866a2 | |||
2d81606049 | |||
718f4ef129 | |||
a42f3b3763 | |||
f7d882a6fc | |||
385afdfcf8 | |||
281d703cc3 | |||
6f56ed5474 | |||
809e4eeba1 | |||
254446c895 | |||
bb52e2beb4 | |||
2739b08e59 | |||
ba5e877a3b | |||
d2752216f6 | |||
d91fb0db02 | |||
556e4d62c4 | |||
4892eea009 | |||
09c6fcb73b | |||
79181a1e3d | |||
bb934ef7b1 | |||
cd9316537d | |||
942e5f2f65 | |||
353d231a4e | |||
68e05b7198 | |||
4f998e3940 | |||
1248840dc8 | |||
64c8125e4b | |||
1690a9429c | |||
c109fbab3e | |||
15fb01089b | |||
6f4be3e25a | |||
8d33647739 | |||
d1c142e5b1 | |||
bb1cad0c5b | |||
2a1cfe15b4 | |||
881d70bc64 | |||
14c1b4f07b | |||
3028bdd424 | |||
902a0b0ed4 | |||
ba92192537 | |||
26ed8df73c | |||
c1decab912 | |||
216c073290 | |||
8626bce632 | |||
c5a2b0321f | |||
5af53d3398 | |||
8da8c6a66c | |||
88a4390ea0 | |||
c70d0a577c | |||
1070954bdd | |||
587a4a1120 | |||
cc689d3178 | |||
e6848828f2 | |||
c8b93e4467 | |||
0bca24bb00 | |||
c563ada50f | |||
26d1616e22 | |||
5fd071d1de | |||
a6ac78356b | |||
e4a2137991 | |||
9721d7a15e | |||
93db5c4555 | |||
ad4393fdef | |||
cd06e4e7f3 | |||
711a4179ce | |||
b4a2a477d3 | |||
8e53a1b171 | |||
0f23b7e1d3 | |||
33193a47ae | |||
1ad286ca87 | |||
be7c11a3f5 | |||
b97bbe5beb | |||
cf5260b383 | |||
13e0dd8e0f | |||
55a6740714 | |||
891cb06de0 | |||
02e8f20cbf | |||
dbe771dba0 | |||
273fd6c98f | |||
d5f4ce4376 | |||
18aac6508b | |||
85653a90d5 | |||
879ef2c178 | |||
8777cfe680 | |||
2b630f75aa | |||
91cee20cc8 | |||
4249ec6030 | |||
e7a95e6af2 | |||
a9f04a3c1f | |||
3d380710ee | |||
2177ec6bcc | |||
070eb2aacd | |||
e619cfa313 | |||
c3038e3ca1 | |||
ff0e7feeee |
2
.github/workflows/run-cli-tests.yml
vendored
2
.github/workflows/run-cli-tests.yml
vendored
@ -50,6 +50,6 @@ jobs:
|
||||
CLI_TESTS_ENV_SLUG: ${{ secrets.CLI_TESTS_ENV_SLUG }}
|
||||
CLI_TESTS_USER_EMAIL: ${{ secrets.CLI_TESTS_USER_EMAIL }}
|
||||
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
|
||||
|
13
Makefile
13
Makefile
@ -15,3 +15,16 @@ up-prod:
|
||||
|
||||
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
|
||||
|
||||
|
79
backend/package-lock.json
generated
79
backend/package-lock.json
generated
@ -25,6 +25,7 @@
|
||||
"@fastify/swagger": "^8.14.0",
|
||||
"@fastify/swagger-ui": "^2.1.0",
|
||||
"@node-saml/passport-saml": "^4.0.4",
|
||||
"@octokit/plugin-retry": "^5.0.5",
|
||||
"@octokit/rest": "^20.0.2",
|
||||
"@octokit/webhooks-types": "^7.3.1",
|
||||
"@peculiar/asn1-schema": "^2.3.8",
|
||||
@ -75,6 +76,7 @@
|
||||
"pino": "^8.16.2",
|
||||
"posthog-node": "^3.6.2",
|
||||
"probot": "^13.0.0",
|
||||
"safe-regex": "^2.1.1",
|
||||
"smee-client": "^2.0.0",
|
||||
"tedious": "^18.2.1",
|
||||
"tweetnacl": "^1.0.3",
|
||||
@ -106,6 +108,7 @@
|
||||
"@types/picomatch": "^2.3.3",
|
||||
"@types/prompt-sync": "^4.2.3",
|
||||
"@types/resolve": "^1.20.6",
|
||||
"@types/safe-regex": "^1.1.6",
|
||||
"@types/uuid": "^9.0.7",
|
||||
"@typescript-eslint/eslint-plugin": "^6.20.0",
|
||||
"@typescript-eslint/parser": "^6.20.0",
|
||||
@ -7812,19 +7815,45 @@
|
||||
}
|
||||
},
|
||||
"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==",
|
||||
"version": "5.0.5",
|
||||
"resolved": "https://registry.npmjs.org/@octokit/plugin-retry/-/plugin-retry-5.0.5.tgz",
|
||||
"integrity": "sha512-sB1RWMhSrre02Atv95K6bhESlJ/sPdZkK/wE/w1IdSCe0yM6FxSjksLa6T7aAvxvxlLKzQEC4KIiqpqyov1Tbg==",
|
||||
"dependencies": {
|
||||
"@octokit/request-error": "^5.0.0",
|
||||
"@octokit/types": "^12.0.0",
|
||||
"@octokit/request-error": "^4.0.1",
|
||||
"@octokit/types": "^10.0.0",
|
||||
"bottleneck": "^2.15.3"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 18"
|
||||
},
|
||||
"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": {
|
||||
@ -9774,6 +9803,12 @@
|
||||
"integrity": "sha512-A4STmOXPhMUtHH+S6ymgE2GiBSMqf4oTvcQZMcHzokuTLVYzXTB8ttjcgxOVaAp2lGwEdzZ0J+cRbbeevQj1UQ==",
|
||||
"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": {
|
||||
"version": "7.5.6",
|
||||
"resolved": "https://registry.npmjs.org/@types/semver/-/semver-7.5.6.tgz",
|
||||
@ -17396,6 +17431,22 @@
|
||||
"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": {
|
||||
"version": "11.1.0",
|
||||
"resolved": "https://registry.npmjs.org/commander/-/commander-11.1.0.tgz",
|
||||
@ -17778,6 +17829,14 @@
|
||||
"@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": {
|
||||
"version": "1.5.1",
|
||||
"resolved": "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.5.1.tgz",
|
||||
@ -18094,6 +18153,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": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/safe-regex-test/-/safe-regex-test-1.0.0.tgz",
|
||||
|
@ -78,6 +78,7 @@
|
||||
"@types/picomatch": "^2.3.3",
|
||||
"@types/prompt-sync": "^4.2.3",
|
||||
"@types/resolve": "^1.20.6",
|
||||
"@types/safe-regex": "^1.1.6",
|
||||
"@types/uuid": "^9.0.7",
|
||||
"@typescript-eslint/eslint-plugin": "^6.20.0",
|
||||
"@typescript-eslint/parser": "^6.20.0",
|
||||
@ -121,6 +122,7 @@
|
||||
"@fastify/swagger": "^8.14.0",
|
||||
"@fastify/swagger-ui": "^2.1.0",
|
||||
"@node-saml/passport-saml": "^4.0.4",
|
||||
"@octokit/plugin-retry": "^5.0.5",
|
||||
"@octokit/rest": "^20.0.2",
|
||||
"@octokit/webhooks-types": "^7.3.1",
|
||||
"@peculiar/asn1-schema": "^2.3.8",
|
||||
@ -171,6 +173,7 @@
|
||||
"pino": "^8.16.2",
|
||||
"posthog-node": "^3.6.2",
|
||||
"probot": "^13.0.0",
|
||||
"safe-regex": "^2.1.1",
|
||||
"smee-client": "^2.0.0",
|
||||
"tedious": "^18.2.1",
|
||||
"tweetnacl": "^1.0.3",
|
||||
|
@ -7,14 +7,33 @@ const prompt = promptSync({
|
||||
sigint: true
|
||||
});
|
||||
|
||||
type ComponentType = 1 | 2 | 3;
|
||||
|
||||
console.log(`
|
||||
Component List
|
||||
--------------
|
||||
0. Exit
|
||||
1. Service component
|
||||
2. DAL 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) {
|
||||
const componentName = prompt("Enter service name: ");
|
||||
|
10
backend/src/@types/fastify.d.ts
vendored
10
backend/src/@types/fastify.d.ts
vendored
@ -18,6 +18,7 @@ import { TOidcConfigServiceFactory } from "@app/ee/services/oidc/oidc-config-ser
|
||||
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 { 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 { TScimServiceFactory } from "@app/ee/services/scim/scim-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 { TCertificateServiceFactory } from "@app/services/certificate/certificate-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 { TIdentityServiceFactory } from "@app/services/identity/identity-service";
|
||||
import { TIdentityAccessTokenServiceFactory } from "@app/services/identity-access-token/identity-access-token-service";
|
||||
@ -50,6 +52,9 @@ import { TIntegrationServiceFactory } from "@app/services/integration/integratio
|
||||
import { TIntegrationAuthServiceFactory } from "@app/services/integration-auth/integration-auth-service";
|
||||
import { TOrgRoleServiceFactory } from "@app/services/org/org-role-service";
|
||||
import { TOrgServiceFactory } from "@app/services/org/org-service";
|
||||
import { TOrgAdminServiceFactory } from "@app/services/org-admin/org-admin-service";
|
||||
import { TPkiAlertServiceFactory } from "@app/services/pki-alert/pki-alert-service";
|
||||
import { TPkiCollectionServiceFactory } from "@app/services/pki-collection/pki-collection-service";
|
||||
import { TProjectServiceFactory } from "@app/services/project/project-service";
|
||||
import { TProjectBotServiceFactory } from "@app/services/project-bot/project-bot-service";
|
||||
import { TProjectEnvServiceFactory } from "@app/services/project-env/project-env-service";
|
||||
@ -88,6 +93,7 @@ declare module "fastify" {
|
||||
id: string;
|
||||
orgId: string;
|
||||
};
|
||||
rateLimits: RateLimitConfiguration;
|
||||
// passport data
|
||||
passportUser: {
|
||||
isUserCompleted: string;
|
||||
@ -113,6 +119,7 @@ declare module "fastify" {
|
||||
group: TGroupServiceFactory;
|
||||
groupProject: TGroupProjectServiceFactory;
|
||||
apiKey: TApiKeyServiceFactory;
|
||||
pkiAlert: TPkiAlertServiceFactory;
|
||||
project: TProjectServiceFactory;
|
||||
projectMembership: TProjectMembershipServiceFactory;
|
||||
projectEnv: TProjectEnvServiceFactory;
|
||||
@ -150,8 +157,10 @@ declare module "fastify" {
|
||||
auditLog: TAuditLogServiceFactory;
|
||||
auditLogStream: TAuditLogStreamServiceFactory;
|
||||
certificate: TCertificateServiceFactory;
|
||||
certificateTemplate: TCertificateTemplateServiceFactory;
|
||||
certificateAuthority: TCertificateAuthorityServiceFactory;
|
||||
certificateAuthorityCrl: TCertificateAuthorityCrlServiceFactory;
|
||||
pkiCollection: TPkiCollectionServiceFactory;
|
||||
secretScanning: TSecretScanningServiceFactory;
|
||||
license: TLicenseServiceFactory;
|
||||
trustedIp: TTrustedIpServiceFactory;
|
||||
@ -165,6 +174,7 @@ declare module "fastify" {
|
||||
rateLimit: TRateLimitServiceFactory;
|
||||
userEngagement: TUserEngagementServiceFactory;
|
||||
externalKms: TExternalKmsServiceFactory;
|
||||
orgAdmin: TOrgAdminServiceFactory;
|
||||
};
|
||||
// this is exclusive use for middlewares in which we need to inject data
|
||||
// everywhere else access using service layer
|
||||
|
28
backend/src/@types/knex.d.ts
vendored
28
backend/src/@types/knex.d.ts
vendored
@ -53,6 +53,9 @@ import {
|
||||
TCertificateSecretsUpdate,
|
||||
TCertificatesInsert,
|
||||
TCertificatesUpdate,
|
||||
TCertificateTemplates,
|
||||
TCertificateTemplatesInsert,
|
||||
TCertificateTemplatesUpdate,
|
||||
TDynamicSecretLeases,
|
||||
TDynamicSecretLeasesInsert,
|
||||
TDynamicSecretLeasesUpdate,
|
||||
@ -161,6 +164,15 @@ import {
|
||||
TOrgRoles,
|
||||
TOrgRolesInsert,
|
||||
TOrgRolesUpdate,
|
||||
TPkiAlerts,
|
||||
TPkiAlertsInsert,
|
||||
TPkiAlertsUpdate,
|
||||
TPkiCollectionItems,
|
||||
TPkiCollectionItemsInsert,
|
||||
TPkiCollectionItemsUpdate,
|
||||
TPkiCollections,
|
||||
TPkiCollectionsInsert,
|
||||
TPkiCollectionsUpdate,
|
||||
TProjectBots,
|
||||
TProjectBotsInsert,
|
||||
TProjectBotsUpdate,
|
||||
@ -355,6 +367,11 @@ declare module "knex/types/tables" {
|
||||
TCertificateAuthorityCrlUpdate
|
||||
>;
|
||||
[TableName.Certificate]: KnexOriginal.CompositeTableType<TCertificates, TCertificatesInsert, TCertificatesUpdate>;
|
||||
[TableName.CertificateTemplate]: KnexOriginal.CompositeTableType<
|
||||
TCertificateTemplates,
|
||||
TCertificateTemplatesInsert,
|
||||
TCertificateTemplatesUpdate
|
||||
>;
|
||||
[TableName.CertificateBody]: KnexOriginal.CompositeTableType<
|
||||
TCertificateBodies,
|
||||
TCertificateBodiesInsert,
|
||||
@ -365,6 +382,17 @@ declare module "knex/types/tables" {
|
||||
TCertificateSecretsInsert,
|
||||
TCertificateSecretsUpdate
|
||||
>;
|
||||
[TableName.PkiAlert]: KnexOriginal.CompositeTableType<TPkiAlerts, TPkiAlertsInsert, TPkiAlertsUpdate>;
|
||||
[TableName.PkiCollection]: KnexOriginal.CompositeTableType<
|
||||
TPkiCollections,
|
||||
TPkiCollectionsInsert,
|
||||
TPkiCollectionsUpdate
|
||||
>;
|
||||
[TableName.PkiCollectionItem]: KnexOriginal.CompositeTableType<
|
||||
TPkiCollectionItems,
|
||||
TPkiCollectionItemsInsert,
|
||||
TPkiCollectionItemsUpdate
|
||||
>;
|
||||
[TableName.UserGroupMembership]: KnexOriginal.CompositeTableType<
|
||||
TUserGroupMembership,
|
||||
TUserGroupMembershipInsert,
|
||||
|
117
backend/src/db/migrations/20240802181855_ca-cert-version.ts
Normal file
117
backend/src/db/migrations/20240802181855_ca-cert-version.ts
Normal file
@ -0,0 +1,117 @@
|
||||
import { Knex } from "knex";
|
||||
|
||||
import { TableName } from "../schemas";
|
||||
|
||||
export async function up(knex: Knex): Promise<void> {
|
||||
if (await knex.schema.hasTable(TableName.CertificateAuthority)) {
|
||||
const hasActiveCaCertIdColumn = await knex.schema.hasColumn(TableName.CertificateAuthority, "activeCaCertId");
|
||||
if (!hasActiveCaCertIdColumn) {
|
||||
await knex.schema.alterTable(TableName.CertificateAuthority, (t) => {
|
||||
t.uuid("activeCaCertId").nullable();
|
||||
t.foreign("activeCaCertId").references("id").inTable(TableName.CertificateAuthorityCert);
|
||||
});
|
||||
|
||||
await knex.raw(`
|
||||
UPDATE "${TableName.CertificateAuthority}" ca
|
||||
SET "activeCaCertId" = cac.id
|
||||
FROM "${TableName.CertificateAuthorityCert}" cac
|
||||
WHERE ca.id = cac."caId"
|
||||
`);
|
||||
}
|
||||
}
|
||||
|
||||
if (await knex.schema.hasTable(TableName.CertificateAuthorityCert)) {
|
||||
const hasVersionColumn = await knex.schema.hasColumn(TableName.CertificateAuthorityCert, "version");
|
||||
if (!hasVersionColumn) {
|
||||
await knex.schema.alterTable(TableName.CertificateAuthorityCert, (t) => {
|
||||
t.integer("version").nullable();
|
||||
t.dropUnique(["caId"]);
|
||||
});
|
||||
|
||||
await knex(TableName.CertificateAuthorityCert).update({ version: 1 }).whereNull("version");
|
||||
|
||||
await knex.schema.alterTable(TableName.CertificateAuthorityCert, (t) => {
|
||||
t.integer("version").notNullable().alter();
|
||||
});
|
||||
}
|
||||
|
||||
const hasCaSecretIdColumn = await knex.schema.hasColumn(TableName.CertificateAuthorityCert, "caSecretId");
|
||||
if (!hasCaSecretIdColumn) {
|
||||
await knex.schema.alterTable(TableName.CertificateAuthorityCert, (t) => {
|
||||
t.uuid("caSecretId").nullable();
|
||||
t.foreign("caSecretId").references("id").inTable(TableName.CertificateAuthoritySecret).onDelete("CASCADE");
|
||||
});
|
||||
|
||||
await knex.raw(`
|
||||
UPDATE "${TableName.CertificateAuthorityCert}" cert
|
||||
SET "caSecretId" = (
|
||||
SELECT sec.id
|
||||
FROM "${TableName.CertificateAuthoritySecret}" sec
|
||||
WHERE sec."caId" = cert."caId"
|
||||
)
|
||||
`);
|
||||
|
||||
await knex.schema.alterTable(TableName.CertificateAuthorityCert, (t) => {
|
||||
t.uuid("caSecretId").notNullable().alter();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (await knex.schema.hasTable(TableName.CertificateAuthoritySecret)) {
|
||||
await knex.schema.alterTable(TableName.CertificateAuthoritySecret, (t) => {
|
||||
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> {
|
||||
if (await knex.schema.hasTable(TableName.CertificateAuthority)) {
|
||||
if (await knex.schema.hasColumn(TableName.CertificateAuthority, "activeCaCertId")) {
|
||||
await knex.schema.alterTable(TableName.CertificateAuthority, (t) => {
|
||||
t.dropColumn("activeCaCertId");
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (await knex.schema.hasTable(TableName.CertificateAuthorityCert)) {
|
||||
if (await knex.schema.hasColumn(TableName.CertificateAuthorityCert, "version")) {
|
||||
await knex.schema.alterTable(TableName.CertificateAuthorityCert, (t) => {
|
||||
t.dropColumn("version");
|
||||
});
|
||||
}
|
||||
|
||||
if (await knex.schema.hasColumn(TableName.CertificateAuthorityCert, "caSecretId")) {
|
||||
await knex.schema.alterTable(TableName.CertificateAuthorityCert, (t) => {
|
||||
t.dropColumn("caSecretId");
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
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();
|
||||
}
|
||||
});
|
||||
}
|
21
backend/src/db/migrations/20240806185442_drop-tag-name.ts
Normal file
21
backend/src/db/migrations/20240806185442_drop-tag-name.ts
Normal file
@ -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");
|
||||
});
|
||||
}
|
||||
}
|
62
backend/src/db/migrations/20240818024923_cert-alerting.ts
Normal file
62
backend/src/db/migrations/20240818024923_cert-alerting.ts
Normal file
@ -0,0 +1,62 @@
|
||||
import { Knex } from "knex";
|
||||
|
||||
import { TableName } from "../schemas";
|
||||
import { createOnUpdateTrigger, dropOnUpdateTrigger } from "../utils";
|
||||
|
||||
export async function up(knex: Knex): Promise<void> {
|
||||
if (!(await knex.schema.hasTable(TableName.PkiCollection))) {
|
||||
await knex.schema.createTable(TableName.PkiCollection, (t) => {
|
||||
t.uuid("id", { primaryKey: true }).defaultTo(knex.fn.uuid());
|
||||
t.timestamps(true, true, true);
|
||||
t.string("projectId").notNullable();
|
||||
t.foreign("projectId").references("id").inTable(TableName.Project).onDelete("CASCADE");
|
||||
t.string("name").notNullable();
|
||||
t.string("description").notNullable();
|
||||
});
|
||||
}
|
||||
|
||||
await createOnUpdateTrigger(knex, TableName.PkiCollection);
|
||||
|
||||
if (!(await knex.schema.hasTable(TableName.PkiCollectionItem))) {
|
||||
await knex.schema.createTable(TableName.PkiCollectionItem, (t) => {
|
||||
t.uuid("id", { primaryKey: true }).defaultTo(knex.fn.uuid());
|
||||
t.timestamps(true, true, true);
|
||||
t.uuid("pkiCollectionId").notNullable();
|
||||
t.foreign("pkiCollectionId").references("id").inTable(TableName.PkiCollection).onDelete("CASCADE");
|
||||
t.uuid("caId").nullable();
|
||||
t.foreign("caId").references("id").inTable(TableName.CertificateAuthority).onDelete("CASCADE");
|
||||
t.uuid("certId").nullable();
|
||||
t.foreign("certId").references("id").inTable(TableName.Certificate).onDelete("CASCADE");
|
||||
});
|
||||
}
|
||||
|
||||
await createOnUpdateTrigger(knex, TableName.PkiCollectionItem);
|
||||
|
||||
if (!(await knex.schema.hasTable(TableName.PkiAlert))) {
|
||||
await knex.schema.createTable(TableName.PkiAlert, (t) => {
|
||||
t.uuid("id", { primaryKey: true }).defaultTo(knex.fn.uuid());
|
||||
t.timestamps(true, true, true);
|
||||
t.string("projectId").notNullable();
|
||||
t.foreign("projectId").references("id").inTable(TableName.Project).onDelete("CASCADE");
|
||||
t.uuid("pkiCollectionId").notNullable();
|
||||
t.foreign("pkiCollectionId").references("id").inTable(TableName.PkiCollection).onDelete("CASCADE");
|
||||
t.string("name").notNullable();
|
||||
t.integer("alertBeforeDays").notNullable();
|
||||
t.string("recipientEmails").notNullable();
|
||||
t.unique(["name", "projectId"]);
|
||||
});
|
||||
}
|
||||
|
||||
await createOnUpdateTrigger(knex, TableName.PkiAlert);
|
||||
}
|
||||
|
||||
export async function down(knex: Knex): Promise<void> {
|
||||
await knex.schema.dropTableIfExists(TableName.PkiAlert);
|
||||
await dropOnUpdateTrigger(knex, TableName.PkiAlert);
|
||||
|
||||
await knex.schema.dropTableIfExists(TableName.PkiCollectionItem);
|
||||
await dropOnUpdateTrigger(knex, TableName.PkiCollectionItem);
|
||||
|
||||
await knex.schema.dropTableIfExists(TableName.PkiCollection);
|
||||
await dropOnUpdateTrigger(knex, TableName.PkiCollection);
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
@ -27,7 +27,8 @@ export const CertificateAuthoritiesSchema = z.object({
|
||||
maxPathLength: z.number().nullable().optional(),
|
||||
keyAlgorithm: z.string(),
|
||||
notBefore: z.date().nullable().optional(),
|
||||
notAfter: z.date().nullable().optional()
|
||||
notAfter: z.date().nullable().optional(),
|
||||
activeCaCertId: z.string().uuid().nullable().optional()
|
||||
});
|
||||
|
||||
export type TCertificateAuthorities = z.infer<typeof CertificateAuthoritiesSchema>;
|
||||
|
@ -15,7 +15,9 @@ export const CertificateAuthorityCertsSchema = z.object({
|
||||
updatedAt: z.date(),
|
||||
caId: z.string().uuid(),
|
||||
encryptedCertificate: zodBuffer,
|
||||
encryptedCertificateChain: zodBuffer
|
||||
encryptedCertificateChain: zodBuffer,
|
||||
version: z.number(),
|
||||
caSecretId: z.string().uuid()
|
||||
});
|
||||
|
||||
export type TCertificateAuthorityCerts = z.infer<typeof CertificateAuthorityCertsSchema>;
|
||||
|
24
backend/src/db/schemas/certificate-templates.ts
Normal file
24
backend/src/db/schemas/certificate-templates.ts
Normal file
@ -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>>;
|
@ -20,7 +20,9 @@ export const CertificatesSchema = z.object({
|
||||
notAfter: z.date(),
|
||||
revokedAt: z.date().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>;
|
||||
|
@ -14,6 +14,7 @@ export * from "./certificate-authority-crl";
|
||||
export * from "./certificate-authority-secret";
|
||||
export * from "./certificate-bodies";
|
||||
export * from "./certificate-secrets";
|
||||
export * from "./certificate-templates";
|
||||
export * from "./certificates";
|
||||
export * from "./dynamic-secret-leases";
|
||||
export * from "./dynamic-secrets";
|
||||
@ -52,6 +53,9 @@ export * from "./org-bots";
|
||||
export * from "./org-memberships";
|
||||
export * from "./org-roles";
|
||||
export * from "./organizations";
|
||||
export * from "./pki-alerts";
|
||||
export * from "./pki-collection-items";
|
||||
export * from "./pki-collections";
|
||||
export * from "./project-bots";
|
||||
export * from "./project-environments";
|
||||
export * from "./project-keys";
|
||||
|
@ -9,6 +9,10 @@ export enum TableName {
|
||||
Certificate = "certificates",
|
||||
CertificateBody = "certificate_bodies",
|
||||
CertificateSecret = "certificate_secrets",
|
||||
CertificateTemplate = "certificate_templates",
|
||||
PkiAlert = "pki_alerts",
|
||||
PkiCollection = "pki_collections",
|
||||
PkiCollectionItem = "pki_collection_items",
|
||||
Groups = "groups",
|
||||
GroupProjectMembership = "group_project_memberships",
|
||||
GroupProjectMembershipRole = "group_project_membership_roles",
|
||||
|
23
backend/src/db/schemas/pki-alerts.ts
Normal file
23
backend/src/db/schemas/pki-alerts.ts
Normal file
@ -0,0 +1,23 @@
|
||||
// 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 PkiAlertsSchema = z.object({
|
||||
id: z.string().uuid(),
|
||||
createdAt: z.date(),
|
||||
updatedAt: z.date(),
|
||||
projectId: z.string(),
|
||||
pkiCollectionId: z.string().uuid(),
|
||||
name: z.string(),
|
||||
alertBeforeDays: z.number(),
|
||||
recipientEmails: z.string()
|
||||
});
|
||||
|
||||
export type TPkiAlerts = z.infer<typeof PkiAlertsSchema>;
|
||||
export type TPkiAlertsInsert = Omit<z.input<typeof PkiAlertsSchema>, TImmutableDBKeys>;
|
||||
export type TPkiAlertsUpdate = Partial<Omit<z.input<typeof PkiAlertsSchema>, TImmutableDBKeys>>;
|
21
backend/src/db/schemas/pki-collection-items.ts
Normal file
21
backend/src/db/schemas/pki-collection-items.ts
Normal file
@ -0,0 +1,21 @@
|
||||
// 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 PkiCollectionItemsSchema = z.object({
|
||||
id: z.string().uuid(),
|
||||
createdAt: z.date(),
|
||||
updatedAt: z.date(),
|
||||
pkiCollectionId: z.string().uuid(),
|
||||
caId: z.string().uuid().nullable().optional(),
|
||||
certId: z.string().uuid().nullable().optional()
|
||||
});
|
||||
|
||||
export type TPkiCollectionItems = z.infer<typeof PkiCollectionItemsSchema>;
|
||||
export type TPkiCollectionItemsInsert = Omit<z.input<typeof PkiCollectionItemsSchema>, TImmutableDBKeys>;
|
||||
export type TPkiCollectionItemsUpdate = Partial<Omit<z.input<typeof PkiCollectionItemsSchema>, TImmutableDBKeys>>;
|
21
backend/src/db/schemas/pki-collections.ts
Normal file
21
backend/src/db/schemas/pki-collections.ts
Normal file
@ -0,0 +1,21 @@
|
||||
// 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 PkiCollectionsSchema = z.object({
|
||||
id: z.string().uuid(),
|
||||
createdAt: z.date(),
|
||||
updatedAt: z.date(),
|
||||
projectId: z.string(),
|
||||
name: z.string(),
|
||||
description: z.string()
|
||||
});
|
||||
|
||||
export type TPkiCollections = z.infer<typeof PkiCollectionsSchema>;
|
||||
export type TPkiCollectionsInsert = Omit<z.input<typeof PkiCollectionsSchema>, TImmutableDBKeys>;
|
||||
export type TPkiCollectionsUpdate = Partial<Omit<z.input<typeof PkiCollectionsSchema>, TImmutableDBKeys>>;
|
@ -15,7 +15,6 @@ export const RateLimitSchema = z.object({
|
||||
authRateLimit: z.number().default(60),
|
||||
inviteUserRateLimit: z.number().default(30),
|
||||
mfaRateLimit: z.number().default(20),
|
||||
creationLimit: z.number().default(30),
|
||||
publicEndpointLimit: z.number().default(30),
|
||||
createdAt: z.date(),
|
||||
updatedAt: z.date()
|
||||
|
@ -9,7 +9,6 @@ import { TImmutableDBKeys } from "./models";
|
||||
|
||||
export const SecretTagsSchema = z.object({
|
||||
id: z.string().uuid(),
|
||||
name: z.string(),
|
||||
slug: z.string(),
|
||||
color: z.string().nullable().optional(),
|
||||
createdAt: z.date(),
|
||||
|
@ -131,7 +131,7 @@ export const registerDynamicSecretLeaseRouter = async (server: FastifyZodProvide
|
||||
.default("/")
|
||||
.transform(removeTrailingSlash)
|
||||
.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: {
|
||||
200: z.object({
|
||||
|
@ -58,7 +58,6 @@ export const registerRateLimitRouter = async (server: FastifyZodProvider) => {
|
||||
authRateLimit: z.number(),
|
||||
inviteUserRateLimit: z.number(),
|
||||
mfaRateLimit: z.number(),
|
||||
creationLimit: z.number(),
|
||||
publicEndpointLimit: z.number()
|
||||
}),
|
||||
response: {
|
||||
|
@ -75,15 +75,16 @@ export const auditLogDALFactory = (db: TDbClient) => {
|
||||
.del()
|
||||
.returning("id");
|
||||
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) {
|
||||
numberOfRetryOnFailure += 1;
|
||||
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 };
|
||||
|
@ -1,5 +1,6 @@
|
||||
import { ForbiddenError } from "@casl/ability";
|
||||
|
||||
import { getConfig } from "@app/lib/config/env";
|
||||
import { BadRequestError } from "@app/lib/errors";
|
||||
|
||||
import { TPermissionServiceFactory } from "../permission/permission-service";
|
||||
@ -61,6 +62,10 @@ export const auditLogServiceFactory = ({
|
||||
};
|
||||
|
||||
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
|
||||
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" });
|
||||
|
@ -2,6 +2,7 @@ import { TProjectPermission } from "@app/lib/types";
|
||||
import { ActorType } from "@app/services/auth/auth-type";
|
||||
import { CaStatus } from "@app/services/certificate-authority/certificate-authority-types";
|
||||
import { TIdentityTrustedIp } from "@app/services/identity/identity-types";
|
||||
import { PkiItemType } from "@app/services/pki-collection/pki-collection-types";
|
||||
|
||||
export type TListProjectAuditLogDTO = {
|
||||
auditLogActor?: string;
|
||||
@ -130,7 +131,9 @@ export enum EventType {
|
||||
GET_CA = "get-certificate-authority",
|
||||
UPDATE_CA = "update-certificate-authority",
|
||||
DELETE_CA = "delete-certificate-authority",
|
||||
RENEW_CA = "renew-certificate-authority",
|
||||
GET_CA_CSR = "get-certificate-authority-csr",
|
||||
GET_CA_CERTS = "get-certificate-authority-certs",
|
||||
GET_CA_CERT = "get-certificate-authority-cert",
|
||||
SIGN_INTERMEDIATE = "sign-intermediate",
|
||||
IMPORT_CA_CERT = "import-certificate-authority-cert",
|
||||
@ -141,13 +144,29 @@ export enum EventType {
|
||||
DELETE_CERT = "delete-cert",
|
||||
REVOKE_CERT = "revoke-cert",
|
||||
GET_CERT_BODY = "get-cert-body",
|
||||
CREATE_PKI_ALERT = "create-pki-alert",
|
||||
GET_PKI_ALERT = "get-pki-alert",
|
||||
UPDATE_PKI_ALERT = "update-pki-alert",
|
||||
DELETE_PKI_ALERT = "delete-pki-alert",
|
||||
CREATE_PKI_COLLECTION = "create-pki-collection",
|
||||
GET_PKI_COLLECTION = "get-pki-collection",
|
||||
UPDATE_PKI_COLLECTION = "update-pki-collection",
|
||||
DELETE_PKI_COLLECTION = "delete-pki-collection",
|
||||
GET_PKI_COLLECTION_ITEMS = "get-pki-collection-items",
|
||||
ADD_PKI_COLLECTION_ITEM = "add-pki-collection-item",
|
||||
DELETE_PKI_COLLECTION_ITEM = "delete-pki-collection-item",
|
||||
CREATE_KMS = "create-kms",
|
||||
UPDATE_KMS = "update-kms",
|
||||
DELETE_KMS = "delete-kms",
|
||||
GET_KMS = "get-kms",
|
||||
UPDATE_PROJECT_KMS = "update-project-kms",
|
||||
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",
|
||||
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 {
|
||||
@ -337,6 +356,7 @@ interface DeleteIntegrationEvent {
|
||||
targetServiceId?: string;
|
||||
path?: string;
|
||||
region?: string;
|
||||
shouldDeleteIntegrationSecrets?: boolean;
|
||||
};
|
||||
}
|
||||
|
||||
@ -1094,6 +1114,14 @@ interface DeleteCa {
|
||||
};
|
||||
}
|
||||
|
||||
interface RenewCa {
|
||||
type: EventType.RENEW_CA;
|
||||
metadata: {
|
||||
caId: string;
|
||||
dn: string;
|
||||
};
|
||||
}
|
||||
|
||||
interface GetCaCsr {
|
||||
type: EventType.GET_CA_CSR;
|
||||
metadata: {
|
||||
@ -1102,6 +1130,14 @@ interface GetCaCsr {
|
||||
};
|
||||
}
|
||||
|
||||
interface GetCaCerts {
|
||||
type: EventType.GET_CA_CERTS;
|
||||
metadata: {
|
||||
caId: string;
|
||||
dn: string;
|
||||
};
|
||||
}
|
||||
|
||||
interface GetCaCert {
|
||||
type: EventType.GET_CA_CERT;
|
||||
metadata: {
|
||||
@ -1189,6 +1225,95 @@ interface GetCertBody {
|
||||
};
|
||||
}
|
||||
|
||||
interface CreatePkiAlert {
|
||||
type: EventType.CREATE_PKI_ALERT;
|
||||
metadata: {
|
||||
pkiAlertId: string;
|
||||
pkiCollectionId: string;
|
||||
name: string;
|
||||
alertBeforeDays: number;
|
||||
recipientEmails: string;
|
||||
};
|
||||
}
|
||||
interface GetPkiAlert {
|
||||
type: EventType.GET_PKI_ALERT;
|
||||
metadata: {
|
||||
pkiAlertId: string;
|
||||
};
|
||||
}
|
||||
|
||||
interface UpdatePkiAlert {
|
||||
type: EventType.UPDATE_PKI_ALERT;
|
||||
metadata: {
|
||||
pkiAlertId: string;
|
||||
pkiCollectionId?: string;
|
||||
name?: string;
|
||||
alertBeforeDays?: number;
|
||||
recipientEmails?: string;
|
||||
};
|
||||
}
|
||||
interface DeletePkiAlert {
|
||||
type: EventType.DELETE_PKI_ALERT;
|
||||
metadata: {
|
||||
pkiAlertId: string;
|
||||
};
|
||||
}
|
||||
|
||||
interface CreatePkiCollection {
|
||||
type: EventType.CREATE_PKI_COLLECTION;
|
||||
metadata: {
|
||||
pkiCollectionId: string;
|
||||
name: string;
|
||||
};
|
||||
}
|
||||
|
||||
interface GetPkiCollection {
|
||||
type: EventType.GET_PKI_COLLECTION;
|
||||
metadata: {
|
||||
pkiCollectionId: string;
|
||||
};
|
||||
}
|
||||
|
||||
interface UpdatePkiCollection {
|
||||
type: EventType.UPDATE_PKI_COLLECTION;
|
||||
metadata: {
|
||||
pkiCollectionId: string;
|
||||
name?: string;
|
||||
};
|
||||
}
|
||||
|
||||
interface DeletePkiCollection {
|
||||
type: EventType.DELETE_PKI_COLLECTION;
|
||||
metadata: {
|
||||
pkiCollectionId: string;
|
||||
};
|
||||
}
|
||||
|
||||
interface GetPkiCollectionItems {
|
||||
type: EventType.GET_PKI_COLLECTION_ITEMS;
|
||||
metadata: {
|
||||
pkiCollectionId: string;
|
||||
};
|
||||
}
|
||||
|
||||
interface AddPkiCollectionItem {
|
||||
type: EventType.ADD_PKI_COLLECTION_ITEM;
|
||||
metadata: {
|
||||
pkiCollectionItemId: string;
|
||||
pkiCollectionId: string;
|
||||
type: PkiItemType;
|
||||
itemId: string;
|
||||
};
|
||||
}
|
||||
|
||||
interface DeletePkiCollectionItem {
|
||||
type: EventType.DELETE_PKI_COLLECTION_ITEM;
|
||||
metadata: {
|
||||
pkiCollectionItemId: string;
|
||||
pkiCollectionId: string;
|
||||
};
|
||||
}
|
||||
|
||||
interface CreateKmsEvent {
|
||||
type: EventType.CREATE_KMS;
|
||||
metadata: {
|
||||
@ -1245,6 +1370,56 @@ interface LoadProjectKmsBackupEvent {
|
||||
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 {
|
||||
type: EventType.ORG_ADMIN_ACCESS_PROJECT;
|
||||
metadata: {
|
||||
userId: string;
|
||||
username: string;
|
||||
email: string;
|
||||
projectId: string;
|
||||
}; // no metadata yet
|
||||
}
|
||||
|
||||
export type Event =
|
||||
| GetSecretsEvent
|
||||
| GetSecretEvent
|
||||
@ -1337,7 +1512,9 @@ export type Event =
|
||||
| GetCa
|
||||
| UpdateCa
|
||||
| DeleteCa
|
||||
| RenewCa
|
||||
| GetCaCsr
|
||||
| GetCaCerts
|
||||
| GetCaCert
|
||||
| SignIntermediate
|
||||
| ImportCaCert
|
||||
@ -1348,10 +1525,26 @@ export type Event =
|
||||
| DeleteCert
|
||||
| RevokeCert
|
||||
| GetCertBody
|
||||
| CreatePkiAlert
|
||||
| GetPkiAlert
|
||||
| UpdatePkiAlert
|
||||
| DeletePkiAlert
|
||||
| CreatePkiCollection
|
||||
| GetPkiCollection
|
||||
| UpdatePkiCollection
|
||||
| DeletePkiCollection
|
||||
| GetPkiCollectionItems
|
||||
| AddPkiCollectionItem
|
||||
| DeletePkiCollectionItem
|
||||
| CreateKmsEvent
|
||||
| UpdateKmsEvent
|
||||
| DeleteKmsEvent
|
||||
| GetKmsEvent
|
||||
| UpdateProjectKmsEvent
|
||||
| GetProjectKmsBackupEvent
|
||||
| LoadProjectKmsBackupEvent;
|
||||
| LoadProjectKmsBackupEvent
|
||||
| OrgAdminAccessProjectEvent
|
||||
| CreateCertificateTemplate
|
||||
| UpdateCertificateTemplate
|
||||
| GetCertificateTemplate
|
||||
| DeleteCertificateTemplate;
|
||||
|
@ -40,7 +40,12 @@ export const getDefaultOnPremFeatures = (): TFeatureSet => ({
|
||||
secretRotation: true,
|
||||
caCrl: false,
|
||||
instanceUserManagement: false,
|
||||
externalKms: false
|
||||
externalKms: false,
|
||||
rateLimits: {
|
||||
readLimit: 60,
|
||||
writeLimit: 200,
|
||||
secretsLimit: 40
|
||||
}
|
||||
});
|
||||
|
||||
export const setupLicenceRequestWithStore = (baseURL: string, refreshUrl: string, licenseKey: string) => {
|
||||
|
@ -58,6 +58,11 @@ export type TFeatureSet = {
|
||||
caCrl: false;
|
||||
instanceUserManagement: false;
|
||||
externalKms: false;
|
||||
rateLimits: {
|
||||
readLimit: number;
|
||||
writeLimit: number;
|
||||
secretsLimit: number;
|
||||
};
|
||||
};
|
||||
|
||||
export type TOrgPlansTableDTO = {
|
||||
|
@ -9,6 +9,10 @@ export enum OrgPermissionActions {
|
||||
Delete = "delete"
|
||||
}
|
||||
|
||||
export enum OrgPermissionAdminConsoleAction {
|
||||
AccessAllProjects = "access-all-projects"
|
||||
}
|
||||
|
||||
export enum OrgPermissionSubjects {
|
||||
Workspace = "workspace",
|
||||
Role = "role",
|
||||
@ -22,7 +26,8 @@ export enum OrgPermissionSubjects {
|
||||
Billing = "billing",
|
||||
SecretScanning = "secret-scanning",
|
||||
Identity = "identity",
|
||||
Kms = "kms"
|
||||
Kms = "kms",
|
||||
AdminConsole = "organization-admin-console"
|
||||
}
|
||||
|
||||
export type OrgPermissionSet =
|
||||
@ -39,7 +44,8 @@ export type OrgPermissionSet =
|
||||
| [OrgPermissionActions, OrgPermissionSubjects.SecretScanning]
|
||||
| [OrgPermissionActions, OrgPermissionSubjects.Billing]
|
||||
| [OrgPermissionActions, OrgPermissionSubjects.Identity]
|
||||
| [OrgPermissionActions, OrgPermissionSubjects.Kms];
|
||||
| [OrgPermissionActions, OrgPermissionSubjects.Kms]
|
||||
| [OrgPermissionAdminConsoleAction, OrgPermissionSubjects.AdminConsole];
|
||||
|
||||
const buildAdminPermission = () => {
|
||||
const { can, build } = new AbilityBuilder<MongoAbility<OrgPermissionSet>>(createMongoAbility);
|
||||
@ -107,6 +113,8 @@ const buildAdminPermission = () => {
|
||||
can(OrgPermissionActions.Edit, OrgPermissionSubjects.Kms);
|
||||
can(OrgPermissionActions.Delete, OrgPermissionSubjects.Kms);
|
||||
|
||||
can(OrgPermissionAdminConsoleAction.AccessAllProjects, OrgPermissionSubjects.AdminConsole);
|
||||
|
||||
return build({ conditionsMatcher });
|
||||
};
|
||||
|
||||
|
@ -30,6 +30,9 @@ export enum ProjectPermissionSub {
|
||||
Identity = "identity",
|
||||
CertificateAuthorities = "certificate-authorities",
|
||||
Certificates = "certificates",
|
||||
CertificateTemplates = "certificate-templates",
|
||||
PkiAlerts = "pki-alerts",
|
||||
PkiCollections = "pki-collections",
|
||||
Kms = "kms"
|
||||
}
|
||||
|
||||
@ -63,6 +66,9 @@ export type ProjectPermissionSet =
|
||||
| [ProjectPermissionActions, ProjectPermissionSub.Identity]
|
||||
| [ProjectPermissionActions, ProjectPermissionSub.CertificateAuthorities]
|
||||
| [ProjectPermissionActions, ProjectPermissionSub.Certificates]
|
||||
| [ProjectPermissionActions, ProjectPermissionSub.CertificateTemplates]
|
||||
| [ProjectPermissionActions, ProjectPermissionSub.PkiAlerts]
|
||||
| [ProjectPermissionActions, ProjectPermissionSub.PkiCollections]
|
||||
| [ProjectPermissionActions.Delete, ProjectPermissionSub.Project]
|
||||
| [ProjectPermissionActions.Edit, ProjectPermissionSub.Project]
|
||||
| [ProjectPermissionActions.Read, ProjectPermissionSub.SecretRollback]
|
||||
@ -161,6 +167,21 @@ const buildAdminPermissionRules = () => {
|
||||
can(ProjectPermissionActions.Edit, 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.Create, ProjectPermissionSub.PkiAlerts);
|
||||
can(ProjectPermissionActions.Edit, ProjectPermissionSub.PkiAlerts);
|
||||
can(ProjectPermissionActions.Delete, ProjectPermissionSub.PkiAlerts);
|
||||
|
||||
can(ProjectPermissionActions.Read, ProjectPermissionSub.PkiCollections);
|
||||
can(ProjectPermissionActions.Create, ProjectPermissionSub.PkiCollections);
|
||||
can(ProjectPermissionActions.Edit, ProjectPermissionSub.PkiCollections);
|
||||
can(ProjectPermissionActions.Delete, ProjectPermissionSub.PkiCollections);
|
||||
|
||||
can(ProjectPermissionActions.Edit, ProjectPermissionSub.Project);
|
||||
can(ProjectPermissionActions.Delete, ProjectPermissionSub.Project);
|
||||
|
||||
@ -237,6 +258,11 @@ const buildMemberPermissionRules = () => {
|
||||
can(ProjectPermissionActions.Edit, ProjectPermissionSub.Certificates);
|
||||
can(ProjectPermissionActions.Delete, ProjectPermissionSub.Certificates);
|
||||
|
||||
can(ProjectPermissionActions.Read, ProjectPermissionSub.CertificateTemplates);
|
||||
|
||||
can(ProjectPermissionActions.Read, ProjectPermissionSub.PkiAlerts);
|
||||
can(ProjectPermissionActions.Read, ProjectPermissionSub.PkiCollections);
|
||||
|
||||
return rules;
|
||||
};
|
||||
|
||||
|
@ -4,17 +4,16 @@ import { logger } from "@app/lib/logger";
|
||||
|
||||
import { TLicenseServiceFactory } from "../license/license-service";
|
||||
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,
|
||||
publicEndpointLimit: 30,
|
||||
writeLimit: 200,
|
||||
secretsLimit: 60,
|
||||
authRateLimit: 60,
|
||||
inviteUserRateLimit: 30,
|
||||
mfaRateLimit: 20,
|
||||
creationLimit: 30
|
||||
mfaRateLimit: 20
|
||||
};
|
||||
|
||||
Object.freeze(rateLimitMaxConfiguration);
|
||||
@ -67,8 +66,7 @@ export const rateLimitServiceFactory = ({ rateLimitDAL, licenseService }: TRateL
|
||||
secretsLimit: rateLimit.secretsRateLimit,
|
||||
authRateLimit: rateLimit.authRateLimit,
|
||||
inviteUserRateLimit: rateLimit.inviteUserRateLimit,
|
||||
mfaRateLimit: rateLimit.mfaRateLimit,
|
||||
creationLimit: rateLimit.creationLimit
|
||||
mfaRateLimit: rateLimit.mfaRateLimit
|
||||
};
|
||||
|
||||
logger.info(`syncRateLimitConfiguration: rate limit configuration: %o`, newRateLimitMaxConfiguration);
|
||||
|
@ -5,7 +5,6 @@ export type TRateLimitUpdateDTO = {
|
||||
authRateLimit: number;
|
||||
inviteUserRateLimit: number;
|
||||
mfaRateLimit: number;
|
||||
creationLimit: number;
|
||||
publicEndpointLimit: number;
|
||||
};
|
||||
|
||||
@ -14,3 +13,13 @@ export type TRateLimit = {
|
||||
createdAt: Date;
|
||||
updatedAt: Date;
|
||||
} & 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 { TProjectEnvDALFactory } from "@app/services/project-env/project-env-dal";
|
||||
|
||||
import { TLicenseServiceFactory } from "../license/license-service";
|
||||
import { TSecretApprovalPolicyApproverDALFactory } from "./secret-approval-policy-approver-dal";
|
||||
import { TSecretApprovalPolicyDALFactory } from "./secret-approval-policy-dal";
|
||||
import {
|
||||
@ -28,6 +29,7 @@ type TSecretApprovalPolicyServiceFactoryDep = {
|
||||
secretApprovalPolicyDAL: TSecretApprovalPolicyDALFactory;
|
||||
projectEnvDAL: Pick<TProjectEnvDALFactory, "findOne">;
|
||||
secretApprovalPolicyApproverDAL: TSecretApprovalPolicyApproverDALFactory;
|
||||
licenseService: Pick<TLicenseServiceFactory, "getPlan">;
|
||||
};
|
||||
|
||||
export type TSecretApprovalPolicyServiceFactory = ReturnType<typeof secretApprovalPolicyServiceFactory>;
|
||||
@ -36,7 +38,8 @@ export const secretApprovalPolicyServiceFactory = ({
|
||||
secretApprovalPolicyDAL,
|
||||
permissionService,
|
||||
secretApprovalPolicyApproverDAL,
|
||||
projectEnvDAL
|
||||
projectEnvDAL,
|
||||
licenseService
|
||||
}: TSecretApprovalPolicyServiceFactoryDep) => {
|
||||
const createSecretApprovalPolicy = async ({
|
||||
name,
|
||||
@ -65,6 +68,15 @@ export const secretApprovalPolicyServiceFactory = ({
|
||||
ProjectPermissionActions.Create,
|
||||
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 });
|
||||
if (!env) throw new BadRequestError({ message: "Environment not found" });
|
||||
|
||||
@ -115,6 +127,14 @@ export const secretApprovalPolicyServiceFactory = ({
|
||||
);
|
||||
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 doc = await secretApprovalPolicyDAL.updateById(
|
||||
secretApprovalPolicy.id,
|
||||
@ -167,6 +187,14 @@ export const secretApprovalPolicyServiceFactory = ({
|
||||
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);
|
||||
return sapPolicy;
|
||||
};
|
||||
|
@ -81,15 +81,13 @@ export const secretApprovalRequestSecretDALFactory = (db: TDbClient) => {
|
||||
.select({
|
||||
secVerTagId: "secVerTag.id",
|
||||
secVerTagColor: "secVerTag.color",
|
||||
secVerTagSlug: "secVerTag.slug",
|
||||
secVerTagName: "secVerTag.name"
|
||||
secVerTagSlug: "secVerTag.slug"
|
||||
})
|
||||
.select(
|
||||
db.ref("id").withSchema(TableName.SecretTag).as("tagId"),
|
||||
db.ref("id").withSchema(TableName.SecretApprovalRequestSecretTag).as("tagJnId"),
|
||||
db.ref("color").withSchema(TableName.SecretTag).as("tagColor"),
|
||||
db.ref("slug").withSchema(TableName.SecretTag).as("tagSlug"),
|
||||
db.ref("name").withSchema(TableName.SecretTag).as("tagName")
|
||||
db.ref("slug").withSchema(TableName.SecretTag).as("tagSlug")
|
||||
)
|
||||
.select(
|
||||
db.ref("secretBlindIndex").withSchema(TableName.Secret).as("orgSecBlindIndex"),
|
||||
@ -124,9 +122,9 @@ export const secretApprovalRequestSecretDALFactory = (db: TDbClient) => {
|
||||
{
|
||||
key: "tagJnId",
|
||||
label: "tags" as const,
|
||||
mapper: ({ tagId: id, tagName: name, tagSlug: slug, tagColor: color }) => ({
|
||||
mapper: ({ tagId: id, tagSlug: slug, tagColor: color }) => ({
|
||||
id,
|
||||
name,
|
||||
name: slug,
|
||||
slug,
|
||||
color
|
||||
})
|
||||
@ -200,11 +198,11 @@ export const secretApprovalRequestSecretDALFactory = (db: TDbClient) => {
|
||||
{
|
||||
key: "secVerTagId",
|
||||
label: "tags" as const,
|
||||
mapper: ({ secVerTagId: id, secVerTagName: name, secVerTagSlug: slug, secVerTagColor: color }) => ({
|
||||
mapper: ({ secVerTagId: id, secVerTagSlug: slug, secVerTagColor: color }) => ({
|
||||
// eslint-disable-next-line
|
||||
id,
|
||||
// eslint-disable-next-line
|
||||
name,
|
||||
name: slug,
|
||||
// eslint-disable-next-line
|
||||
slug,
|
||||
// eslint-disable-next-line
|
||||
@ -262,15 +260,13 @@ export const secretApprovalRequestSecretDALFactory = (db: TDbClient) => {
|
||||
.select({
|
||||
secVerTagId: "secVerTag.id",
|
||||
secVerTagColor: "secVerTag.color",
|
||||
secVerTagSlug: "secVerTag.slug",
|
||||
secVerTagName: "secVerTag.name"
|
||||
secVerTagSlug: "secVerTag.slug"
|
||||
})
|
||||
.select(
|
||||
db.ref("id").withSchema(TableName.SecretTag).as("tagId"),
|
||||
db.ref("id").withSchema(TableName.SecretApprovalRequestSecretTagV2).as("tagJnId"),
|
||||
db.ref("color").withSchema(TableName.SecretTag).as("tagColor"),
|
||||
db.ref("slug").withSchema(TableName.SecretTag).as("tagSlug"),
|
||||
db.ref("name").withSchema(TableName.SecretTag).as("tagName")
|
||||
db.ref("slug").withSchema(TableName.SecretTag).as("tagSlug")
|
||||
)
|
||||
.select(
|
||||
db.ref("version").withSchema(TableName.SecretV2).as("orgSecVersion"),
|
||||
@ -292,9 +288,9 @@ export const secretApprovalRequestSecretDALFactory = (db: TDbClient) => {
|
||||
{
|
||||
key: "tagJnId",
|
||||
label: "tags" as const,
|
||||
mapper: ({ tagId: id, tagName: name, tagSlug: slug, tagColor: color }) => ({
|
||||
mapper: ({ tagId: id, tagSlug: slug, tagColor: color }) => ({
|
||||
id,
|
||||
name,
|
||||
name: slug,
|
||||
slug,
|
||||
color
|
||||
})
|
||||
@ -330,11 +326,11 @@ export const secretApprovalRequestSecretDALFactory = (db: TDbClient) => {
|
||||
{
|
||||
key: "secVerTagId",
|
||||
label: "tags" as const,
|
||||
mapper: ({ secVerTagId: id, secVerTagName: name, secVerTagSlug: slug, secVerTagColor: color }) => ({
|
||||
mapper: ({ secVerTagId: id, secVerTagSlug: slug, secVerTagColor: color }) => ({
|
||||
// eslint-disable-next-line
|
||||
id,
|
||||
// eslint-disable-next-line
|
||||
name,
|
||||
name: slug,
|
||||
// eslint-disable-next-line
|
||||
slug,
|
||||
// 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 { TUserDALFactory } from "@app/services/user/user-dal";
|
||||
|
||||
import { TLicenseServiceFactory } from "../license/license-service";
|
||||
import { TPermissionServiceFactory } from "../permission/permission-service";
|
||||
import { ProjectPermissionActions, ProjectPermissionSub } from "../permission/project-permission";
|
||||
import { TSecretSnapshotServiceFactory } from "../secret-snapshot/secret-snapshot-service";
|
||||
@ -97,6 +98,7 @@ type TSecretApprovalRequestServiceFactoryDep = {
|
||||
>;
|
||||
secretVersionV2BridgeDAL: Pick<TSecretVersionV2DALFactory, "insertMany" | "findLatestVersionMany">;
|
||||
secretVersionTagV2BridgeDAL: Pick<TSecretVersionV2TagDALFactory, "insertMany">;
|
||||
licenseService: Pick<TLicenseServiceFactory, "getPlan">;
|
||||
};
|
||||
|
||||
export type TSecretApprovalRequestServiceFactory = ReturnType<typeof secretApprovalRequestServiceFactory>;
|
||||
@ -122,7 +124,8 @@ export const secretApprovalRequestServiceFactory = ({
|
||||
kmsService,
|
||||
secretV2BridgeDAL,
|
||||
secretVersionV2BridgeDAL,
|
||||
secretVersionTagV2BridgeDAL
|
||||
secretVersionTagV2BridgeDAL,
|
||||
licenseService
|
||||
}: TSecretApprovalRequestServiceFactoryDep) => {
|
||||
const requestCount = async ({ projectId, actor, actorId, actorOrgId, actorAuthMethod }: TApprovalRequestCountDTO) => {
|
||||
if (actor === ActorType.SERVICE) throw new BadRequestError({ message: "Cannot use service token" });
|
||||
@ -224,12 +227,10 @@ export const secretApprovalRequestServiceFactory = ({
|
||||
secretKey: el.key,
|
||||
id: el.id,
|
||||
version: el.version,
|
||||
secretValue: el.encryptedValue
|
||||
? secretManagerDecryptor({ cipherTextBlob: el.encryptedValue }).toString()
|
||||
: undefined,
|
||||
secretValue: el.encryptedValue ? secretManagerDecryptor({ cipherTextBlob: el.encryptedValue }).toString() : "",
|
||||
secretComment: el.encryptedComment
|
||||
? secretManagerDecryptor({ cipherTextBlob: el.encryptedComment }).toString()
|
||||
: undefined,
|
||||
: "",
|
||||
secret: el.secret
|
||||
? {
|
||||
secretKey: el.secret.key,
|
||||
@ -237,10 +238,10 @@ export const secretApprovalRequestServiceFactory = ({
|
||||
version: el.secret.version,
|
||||
secretValue: el.secret.encryptedValue
|
||||
? secretManagerDecryptor({ cipherTextBlob: el.secret.encryptedValue }).toString()
|
||||
: undefined,
|
||||
: "",
|
||||
secretComment: el.secret.encryptedComment
|
||||
? secretManagerDecryptor({ cipherTextBlob: el.secret.encryptedComment }).toString()
|
||||
: undefined
|
||||
: ""
|
||||
}
|
||||
: undefined,
|
||||
secretVersion: el.secretVersion
|
||||
@ -250,10 +251,10 @@ export const secretApprovalRequestServiceFactory = ({
|
||||
version: el.secretVersion.version,
|
||||
secretValue: el.secretVersion.encryptedValue
|
||||
? secretManagerDecryptor({ cipherTextBlob: el.secretVersion.encryptedValue }).toString()
|
||||
: undefined,
|
||||
: "",
|
||||
secretComment: el.secretVersion.encryptedComment
|
||||
? secretManagerDecryptor({ cipherTextBlob: el.secretVersion.encryptedComment }).toString()
|
||||
: undefined
|
||||
: ""
|
||||
}
|
||||
: undefined
|
||||
}));
|
||||
@ -297,6 +298,14 @@ export const secretApprovalRequestServiceFactory = ({
|
||||
if (!secretApprovalRequest) throw new BadRequestError({ message: "Secret approval request not found" });
|
||||
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 { hasRole } = await permissionService.getProjectPermission(
|
||||
ActorType.USER,
|
||||
@ -347,6 +356,14 @@ export const secretApprovalRequestServiceFactory = ({
|
||||
if (!secretApprovalRequest) throw new BadRequestError({ message: "Secret approval request not found" });
|
||||
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 { hasRole } = await permissionService.getProjectPermission(
|
||||
ActorType.USER,
|
||||
@ -388,6 +405,14 @@ export const secretApprovalRequestServiceFactory = ({
|
||||
if (!secretApprovalRequest) throw new BadRequestError({ message: "Secret approval request not found" });
|
||||
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 { hasRole } = await permissionService.getProjectPermission(
|
||||
ActorType.USER,
|
||||
|
@ -257,7 +257,7 @@ export const secretReplicationServiceFactory = ({
|
||||
secretDAL: secretV2BridgeDAL,
|
||||
folderDAL,
|
||||
secretImportDAL,
|
||||
decryptor: (value) => (value ? secretManagerDecryptor({ cipherTextBlob: value }).toString() : undefined)
|
||||
decryptor: (value) => (value ? secretManagerDecryptor({ cipherTextBlob: value }).toString() : "")
|
||||
});
|
||||
// secrets that gets replicated across imports
|
||||
const sourceDecryptedLocalSecrets = sourceLocalSecrets.map((el) => ({
|
||||
@ -449,7 +449,7 @@ export const secretReplicationServiceFactory = ({
|
||||
});
|
||||
}
|
||||
if (locallyDeletedSecrets.length) {
|
||||
await secretDAL.delete(
|
||||
await secretV2BridgeDAL.delete(
|
||||
{
|
||||
$in: {
|
||||
id: locallyDeletedSecrets.map(({ id }) => id)
|
||||
|
@ -164,10 +164,10 @@ export const secretSnapshotServiceFactory = ({
|
||||
secretKey: el.key,
|
||||
secretValue: el.encryptedValue
|
||||
? secretManagerDecryptor({ cipherTextBlob: el.encryptedValue }).toString()
|
||||
: undefined,
|
||||
: "",
|
||||
secretComment: el.encryptedComment
|
||||
? secretManagerDecryptor({ cipherTextBlob: el.encryptedComment }).toString()
|
||||
: undefined
|
||||
: ""
|
||||
}))
|
||||
};
|
||||
} else {
|
||||
|
@ -100,8 +100,7 @@ export const snapshotDALFactory = (db: TDbClient) => {
|
||||
db.ref("id").withSchema(TableName.SecretTag).as("tagId"),
|
||||
db.ref("id").withSchema(TableName.SecretVersionTag).as("tagVersionId"),
|
||||
db.ref("color").withSchema(TableName.SecretTag).as("tagColor"),
|
||||
db.ref("slug").withSchema(TableName.SecretTag).as("tagSlug"),
|
||||
db.ref("name").withSchema(TableName.SecretTag).as("tagName")
|
||||
db.ref("slug").withSchema(TableName.SecretTag).as("tagSlug")
|
||||
);
|
||||
return sqlNestRelationships({
|
||||
data,
|
||||
@ -132,9 +131,9 @@ export const snapshotDALFactory = (db: TDbClient) => {
|
||||
{
|
||||
key: "tagVersionId",
|
||||
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,
|
||||
name,
|
||||
name: slug,
|
||||
slug,
|
||||
color,
|
||||
vId
|
||||
@ -195,8 +194,7 @@ export const snapshotDALFactory = (db: TDbClient) => {
|
||||
db.ref("id").withSchema(TableName.SecretTag).as("tagId"),
|
||||
db.ref("id").withSchema(TableName.SecretVersionV2Tag).as("tagVersionId"),
|
||||
db.ref("color").withSchema(TableName.SecretTag).as("tagColor"),
|
||||
db.ref("slug").withSchema(TableName.SecretTag).as("tagSlug"),
|
||||
db.ref("name").withSchema(TableName.SecretTag).as("tagName")
|
||||
db.ref("slug").withSchema(TableName.SecretTag).as("tagSlug")
|
||||
);
|
||||
return sqlNestRelationships({
|
||||
data,
|
||||
@ -227,9 +225,9 @@ export const snapshotDALFactory = (db: TDbClient) => {
|
||||
{
|
||||
key: "tagVersionId",
|
||||
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,
|
||||
name,
|
||||
name: slug,
|
||||
slug,
|
||||
color,
|
||||
vId
|
||||
@ -353,8 +351,7 @@ export const snapshotDALFactory = (db: TDbClient) => {
|
||||
db.ref("id").withSchema(TableName.SecretTag).as("tagId"),
|
||||
db.ref("id").withSchema(TableName.SecretVersionTag).as("tagVersionId"),
|
||||
db.ref("color").withSchema(TableName.SecretTag).as("tagColor"),
|
||||
db.ref("slug").withSchema(TableName.SecretTag).as("tagSlug"),
|
||||
db.ref("name").withSchema(TableName.SecretTag).as("tagName")
|
||||
db.ref("slug").withSchema(TableName.SecretTag).as("tagSlug")
|
||||
);
|
||||
|
||||
const formated = sqlNestRelationships({
|
||||
@ -377,9 +374,9 @@ export const snapshotDALFactory = (db: TDbClient) => {
|
||||
{
|
||||
key: "tagVersionId",
|
||||
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,
|
||||
name,
|
||||
name: slug,
|
||||
slug,
|
||||
color,
|
||||
vId
|
||||
@ -508,8 +505,7 @@ export const snapshotDALFactory = (db: TDbClient) => {
|
||||
db.ref("id").withSchema(TableName.SecretTag).as("tagId"),
|
||||
db.ref("id").withSchema(TableName.SecretVersionV2Tag).as("tagVersionId"),
|
||||
db.ref("color").withSchema(TableName.SecretTag).as("tagColor"),
|
||||
db.ref("slug").withSchema(TableName.SecretTag).as("tagSlug"),
|
||||
db.ref("name").withSchema(TableName.SecretTag).as("tagName")
|
||||
db.ref("slug").withSchema(TableName.SecretTag).as("tagSlug")
|
||||
);
|
||||
|
||||
const formated = sqlNestRelationships({
|
||||
@ -532,9 +528,9 @@ export const snapshotDALFactory = (db: TDbClient) => {
|
||||
{
|
||||
key: "tagVersionId",
|
||||
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,
|
||||
name,
|
||||
name: slug,
|
||||
slug,
|
||||
color,
|
||||
vId
|
||||
|
@ -5,17 +5,26 @@ import { Redlock, Settings } from "@app/lib/red-lock";
|
||||
export type TKeyStoreFactory = ReturnType<typeof keyStoreFactory>;
|
||||
|
||||
// all the key prefixes used must be set here to avoid conflict
|
||||
export enum KeyStorePrefixes {
|
||||
SecretReplication = "secret-replication-import-lock",
|
||||
KmsProjectDataKeyCreation = "kms-project-data-key-creation-lock",
|
||||
KmsProjectKeyCreation = "kms-project-key-creation-lock",
|
||||
WaitUntilReadyKmsProjectDataKeyCreation = "wait-until-ready-kms-project-data-key-creation-",
|
||||
WaitUntilReadyKmsProjectKeyCreation = "wait-until-ready-kms-project-key-creation-",
|
||||
KmsOrgKeyCreation = "kms-org-key-creation-lock",
|
||||
KmsOrgDataKeyCreation = "kms-org-data-key-creation-lock",
|
||||
WaitUntilReadyKmsOrgKeyCreation = "wait-until-ready-kms-org-key-creation-",
|
||||
WaitUntilReadyKmsOrgDataKeyCreation = "wait-until-ready-kms-org-data-key-creation-"
|
||||
}
|
||||
export const KeyStorePrefixes = {
|
||||
SecretReplication: "secret-replication-import-lock",
|
||||
KmsProjectDataKeyCreation: "kms-project-data-key-creation-lock",
|
||||
KmsProjectKeyCreation: "kms-project-key-creation-lock",
|
||||
WaitUntilReadyKmsProjectDataKeyCreation: "wait-until-ready-kms-project-data-key-creation-",
|
||||
WaitUntilReadyKmsProjectKeyCreation: "wait-until-ready-kms-project-key-creation-",
|
||||
KmsOrgKeyCreation: "kms-org-key-creation-lock",
|
||||
KmsOrgDataKeyCreation: "kms-org-data-key-creation-lock",
|
||||
WaitUntilReadyKmsOrgKeyCreation: "wait-until-ready-kms-org-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 = {
|
||||
key: string;
|
||||
@ -37,10 +46,10 @@ export const keyStoreFactory = (redisUrl: string) => {
|
||||
|
||||
const setItemWithExpiry = async (
|
||||
key: string,
|
||||
exp: number | string,
|
||||
expiryInSeconds: number | string,
|
||||
value: string | number | Buffer,
|
||||
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);
|
||||
|
||||
|
@ -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.",
|
||||
environment: "The slug of the environment 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: {
|
||||
secretName: "The name of the secret to create.",
|
||||
@ -1048,12 +1049,27 @@ export const CERTIFICATE_AUTHORITIES = {
|
||||
caId: "The ID of the CA to generate CSR from",
|
||||
csr: "The generated CSR from the CA"
|
||||
},
|
||||
RENEW_CA_CERT: {
|
||||
caId: "The ID of the CA to renew the CA certificate for",
|
||||
type: "The type of behavior to use for the renewal operation. Currently Infisical is only able to renew a CA certificate with the same key pair.",
|
||||
notAfter: "The expiry date and time for the renewed CA certificate in YYYY-MM-DDTHH:mm:ss.sssZ format",
|
||||
certificate: "The renewed CA certificate body",
|
||||
certificateChain: "The certificate chain of the CA",
|
||||
serialNumber: "The serial number of the renewed CA certificate"
|
||||
},
|
||||
GET_CERT: {
|
||||
caId: "The ID of the CA to get the certificate body and certificate chain from",
|
||||
certificate: "The certificate body of the CA",
|
||||
certificateChain: "The certificate chain of the CA",
|
||||
serialNumber: "The serial number of the CA certificate"
|
||||
},
|
||||
GET_CA_CERTS: {
|
||||
caId: "The ID of the CA to get the CA certificates for",
|
||||
certificate: "The certificate body of the CA certificate",
|
||||
certificateChain: "The certificate chain of the CA certificate",
|
||||
serialNumber: "The serial number of the CA certificate",
|
||||
version: "The version of the CA certificate. The version is incremented for each CA renewal operation."
|
||||
},
|
||||
SIGN_INTERMEDIATE: {
|
||||
caId: "The ID of the CA to sign the intermediate certificate with",
|
||||
csr: "The pem-encoded CSR to sign with the CA",
|
||||
@ -1073,6 +1089,8 @@ export const CERTIFICATE_AUTHORITIES = {
|
||||
},
|
||||
ISSUE_CERT: {
|
||||
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",
|
||||
commonName: "The common name (CN) for the certificate",
|
||||
altNames:
|
||||
@ -1088,6 +1106,7 @@ export const CERTIFICATE_AUTHORITIES = {
|
||||
},
|
||||
SIGN_CERT: {
|
||||
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",
|
||||
friendlyName: "A friendly name for the certificate",
|
||||
commonName: "The common name (CN) for the certificate",
|
||||
@ -1129,6 +1148,91 @@ 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 = {
|
||||
CREATE: {
|
||||
projectId: "The ID of the project to create the alert in",
|
||||
pkiCollectionId: "The ID of the PKI collection to bind to the alert",
|
||||
name: "The name of the alert",
|
||||
alertBeforeDays: "The number of days before the certificate expires to trigger the alert",
|
||||
emails: "The email addresses to send the alert email to"
|
||||
},
|
||||
GET: {
|
||||
alertId: "The ID of the alert to get"
|
||||
},
|
||||
UPDATE: {
|
||||
alertId: "The ID of the alert to update",
|
||||
name: "The name of the alert to update to",
|
||||
alertBeforeDays: "The number of days before the certificate expires to trigger the alert to update to",
|
||||
pkiCollectionId: "The ID of the PKI collection to bind to the alert to update to",
|
||||
emails: "The email addresses to send the alert email to update to"
|
||||
},
|
||||
DELETE: {
|
||||
alertId: "The ID of the alert to delete"
|
||||
}
|
||||
};
|
||||
|
||||
export const PKI_COLLECTIONS = {
|
||||
CREATE: {
|
||||
projectId: "The ID of the project to create the PKI collection in",
|
||||
name: "The name of the PKI collection",
|
||||
description: "A description for the PKI collection"
|
||||
},
|
||||
GET: {
|
||||
collectionId: "The ID of the PKI collection to get"
|
||||
},
|
||||
UPDATE: {
|
||||
collectionId: "The ID of the PKI collection to update",
|
||||
name: "The name of the PKI collection to update to",
|
||||
description: "The description for the PKI collection to update to"
|
||||
},
|
||||
DELETE: {
|
||||
collectionId: "The ID of the PKI collection to delete"
|
||||
},
|
||||
LIST_ITEMS: {
|
||||
collectionId: "The ID of the PKI collection to list items from",
|
||||
type: "The type of the PKI collection item to list",
|
||||
offset: "The offset to start from",
|
||||
limit: "The number of items to return"
|
||||
},
|
||||
ADD_ITEM: {
|
||||
collectionId: "The ID of the PKI collection to add the item to",
|
||||
type: "The type of the PKI collection item to add",
|
||||
itemId: "The resource ID of the PKI collection item to add"
|
||||
},
|
||||
DELETE_ITEM: {
|
||||
collectionId: "The ID of the PKI collection to delete the item from",
|
||||
collectionItemId: "The ID of the PKI collection item to delete",
|
||||
type: "The type of the deleted PKI collection item",
|
||||
itemId: "The resource ID of the deleted PKI collection item"
|
||||
}
|
||||
};
|
||||
|
||||
export const PROJECT_ROLE = {
|
||||
CREATE: {
|
||||
projectSlug: "Slug of the project to create the role for.",
|
||||
|
@ -140,7 +140,8 @@ const envSchema = z
|
||||
MAINTENANCE_MODE: zodStrBool.default("false"),
|
||||
CAPTCHA_SECRET: 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) => ({
|
||||
...data,
|
||||
|
@ -1,2 +1,8 @@
|
||||
export const getLastMidnightDateISO = (last = 1) =>
|
||||
`${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);
|
||||
};
|
||||
|
@ -19,23 +19,43 @@ export const withTransaction = <K extends object>(db: Knex, dal: K) => ({
|
||||
|
||||
export type TFindFilter<R extends object = object> = Partial<R> & {
|
||||
$in?: Partial<{ [k in keyof R]: R[k][] }>;
|
||||
$search?: Partial<{ [k in keyof R]: R[k] }>;
|
||||
};
|
||||
export const buildFindFilter =
|
||||
<R extends object = object>({ $in, ...filter }: TFindFilter<R>) =>
|
||||
<R extends object = object>({ $in, $search, ...filter }: TFindFilter<R>) =>
|
||||
(bd: Knex.QueryBuilder<R, R>) => {
|
||||
void bd.where(filter);
|
||||
if ($in) {
|
||||
Object.entries($in).forEach(([key, val]) => {
|
||||
void bd.whereIn(key as never, val as never);
|
||||
if (val) {
|
||||
void bd.whereIn(key as never, val as never);
|
||||
}
|
||||
});
|
||||
}
|
||||
if ($search) {
|
||||
Object.entries($search).forEach(([key, val]) => {
|
||||
if (val) {
|
||||
void bd.whereILike(key as never, val as never);
|
||||
}
|
||||
});
|
||||
}
|
||||
return bd;
|
||||
};
|
||||
|
||||
export type TFindOpt<R extends object = object> = {
|
||||
export type TFindReturn<TQuery extends Knex.QueryBuilder, TCount extends boolean = false> = Array<
|
||||
Awaited<TQuery>[0] &
|
||||
(TCount extends true
|
||||
? {
|
||||
count: string;
|
||||
}
|
||||
: unknown)
|
||||
>;
|
||||
|
||||
export type TFindOpt<R extends object = object, TCount extends boolean = boolean> = {
|
||||
limit?: number;
|
||||
offset?: number;
|
||||
sort?: Array<[keyof R, "asc" | "desc"] | [keyof R, "asc" | "desc", "first" | "last"]>;
|
||||
count?: TCount;
|
||||
tx?: Knex;
|
||||
};
|
||||
|
||||
@ -66,18 +86,22 @@ export const ormify = <DbOps extends object, Tname extends keyof Tables>(db: Kne
|
||||
throw new DatabaseError({ error, name: "Find one" });
|
||||
}
|
||||
},
|
||||
find: async (
|
||||
find: async <TCount extends boolean = false>(
|
||||
filter: TFindFilter<Tables[Tname]["base"]>,
|
||||
{ offset, limit, sort, tx }: TFindOpt<Tables[Tname]["base"]> = {}
|
||||
{ offset, limit, sort, count, tx }: TFindOpt<Tables[Tname]["base"], TCount> = {}
|
||||
) => {
|
||||
try {
|
||||
const query = (tx || db.replicaNode())(tableName).where(buildFindFilter(filter));
|
||||
if (count) {
|
||||
void query.select(db.raw("COUNT(*) OVER() AS count"));
|
||||
void query.select("*");
|
||||
}
|
||||
if (limit) void query.limit(limit);
|
||||
if (offset) void query.offset(offset);
|
||||
if (sort) {
|
||||
void query.orderBy(sort.map(([column, order, nulls]) => ({ column: column as string, order, nulls })));
|
||||
}
|
||||
const res = await query;
|
||||
const res = (await query) as TFindReturn<typeof query, TCount>;
|
||||
return res;
|
||||
} catch (error) {
|
||||
throw new DatabaseError({ error, name: "Find one" });
|
||||
@ -104,6 +128,16 @@ export const ormify = <DbOps extends object, Tname extends keyof Tables>(db: Kne
|
||||
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) => {
|
||||
try {
|
||||
if (!data.length) return [];
|
||||
|
@ -16,6 +16,7 @@ export enum QueueName {
|
||||
// TODO(akhilmhdh): This will get removed later. For now this is kept to stop the repeatable queue
|
||||
AuditLogPrune = "audit-log-prune",
|
||||
DailyResourceCleanUp = "daily-resource-cleanup",
|
||||
DailyExpiringPkiItemAlert = "daily-expiring-pki-item-alert",
|
||||
TelemetryInstanceStats = "telemtry-self-hosted-stats",
|
||||
IntegrationSync = "sync-integrations",
|
||||
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
|
||||
AuditLogPrune = "audit-log-prune-job",
|
||||
DailyResourceCleanUp = "daily-resource-cleanup-job",
|
||||
DailyExpiringPkiItemAlert = "daily-expiring-pki-item-alert",
|
||||
SecWebhook = "secret-webhook-trigger",
|
||||
TelemetryInstanceStats = "telemetry-self-hosted-stats",
|
||||
IntegrationSync = "secret-integration-pull",
|
||||
@ -71,6 +73,10 @@ export type TQueueJobTypes = {
|
||||
name: QueueJobs.DailyResourceCleanUp;
|
||||
payload: undefined;
|
||||
};
|
||||
[QueueName.DailyExpiringPkiItemAlert]: {
|
||||
name: QueueJobs.DailyExpiringPkiItemAlert;
|
||||
payload: undefined;
|
||||
};
|
||||
[QueueName.AuditLogPrune]: {
|
||||
name: QueueJobs.AuditLogPrune;
|
||||
payload: undefined;
|
||||
|
@ -1,7 +1,6 @@
|
||||
import type { RateLimitOptions, RateLimitPluginOptions } from "@fastify/rate-limit";
|
||||
import { Redis } from "ioredis";
|
||||
|
||||
import { getRateLimiterConfig } from "@app/ee/services/rate-limit/rate-limit-service";
|
||||
import { getConfig } from "@app/lib/config/env";
|
||||
|
||||
export const globalRateLimiterCfg = (): RateLimitPluginOptions => {
|
||||
@ -22,14 +21,16 @@ export const globalRateLimiterCfg = (): RateLimitPluginOptions => {
|
||||
// GET endpoints
|
||||
export const readLimit: RateLimitOptions = {
|
||||
timeWindow: 60 * 1000,
|
||||
max: () => getRateLimiterConfig().readLimit,
|
||||
hook: "preValidation",
|
||||
max: (req) => req.rateLimits.readLimit,
|
||||
keyGenerator: (req) => req.realIp
|
||||
};
|
||||
|
||||
// POST, PATCH, PUT, DELETE endpoints
|
||||
export const writeLimit: RateLimitOptions = {
|
||||
timeWindow: 60 * 1000,
|
||||
max: () => getRateLimiterConfig().writeLimit,
|
||||
hook: "preValidation",
|
||||
max: (req) => req.rateLimits.writeLimit,
|
||||
keyGenerator: (req) => req.realIp
|
||||
};
|
||||
|
||||
@ -37,42 +38,40 @@ export const writeLimit: RateLimitOptions = {
|
||||
export const secretsLimit: RateLimitOptions = {
|
||||
// secrets, folders, secret imports
|
||||
timeWindow: 60 * 1000,
|
||||
max: () => getRateLimiterConfig().secretsLimit,
|
||||
hook: "preValidation",
|
||||
max: (req) => req.rateLimits.secretsLimit,
|
||||
keyGenerator: (req) => req.realIp
|
||||
};
|
||||
|
||||
export const authRateLimit: RateLimitOptions = {
|
||||
timeWindow: 60 * 1000,
|
||||
max: () => getRateLimiterConfig().authRateLimit,
|
||||
hook: "preValidation",
|
||||
max: (req) => req.rateLimits.authRateLimit,
|
||||
keyGenerator: (req) => req.realIp
|
||||
};
|
||||
|
||||
export const inviteUserRateLimit: RateLimitOptions = {
|
||||
timeWindow: 60 * 1000,
|
||||
max: () => getRateLimiterConfig().inviteUserRateLimit,
|
||||
hook: "preValidation",
|
||||
max: (req) => req.rateLimits.inviteUserRateLimit,
|
||||
keyGenerator: (req) => req.realIp
|
||||
};
|
||||
|
||||
export const mfaRateLimit: RateLimitOptions = {
|
||||
timeWindow: 60 * 1000,
|
||||
max: () => getRateLimiterConfig().mfaRateLimit,
|
||||
hook: "preValidation",
|
||||
max: (req) => req.rateLimits.mfaRateLimit,
|
||||
keyGenerator: (req) => {
|
||||
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
|
||||
export const publicEndpointLimit: RateLimitOptions = {
|
||||
// Read Shared Secrets
|
||||
timeWindow: 60 * 1000,
|
||||
max: () => getRateLimiterConfig().publicEndpointLimit,
|
||||
hook: "preValidation",
|
||||
max: (req) => req.rateLimits.publicEndpointLimit,
|
||||
keyGenerator: (req) => req.realIp
|
||||
};
|
||||
|
||||
|
38
backend/src/server/plugins/inject-rate-limits.ts
Normal file
38
backend/src/server/plugins/inject-rate-limits.ts
Normal file
@ -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
|
||||
};
|
||||
});
|
||||
});
|
@ -89,6 +89,8 @@ import { certificateAuthorityDALFactory } from "@app/services/certificate-author
|
||||
import { certificateAuthorityQueueFactory } from "@app/services/certificate-authority/certificate-authority-queue";
|
||||
import { certificateAuthoritySecretDALFactory } from "@app/services/certificate-authority/certificate-authority-secret-dal";
|
||||
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 { groupProjectMembershipRoleDALFactory } from "@app/services/group-project/group-project-membership-role-dal";
|
||||
import { groupProjectServiceFactory } from "@app/services/group-project/group-project-service";
|
||||
@ -129,7 +131,14 @@ import { orgDALFactory } from "@app/services/org/org-dal";
|
||||
import { orgRoleDALFactory } from "@app/services/org/org-role-dal";
|
||||
import { orgRoleServiceFactory } from "@app/services/org/org-role-service";
|
||||
import { orgServiceFactory } from "@app/services/org/org-service";
|
||||
import { orgAdminServiceFactory } from "@app/services/org-admin/org-admin-service";
|
||||
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 { pkiAlertServiceFactory } from "@app/services/pki-alert/pki-alert-service";
|
||||
import { pkiCollectionDALFactory } from "@app/services/pki-collection/pki-collection-dal";
|
||||
import { pkiCollectionItemDALFactory } from "@app/services/pki-collection/pki-collection-item-dal";
|
||||
import { pkiCollectionServiceFactory } from "@app/services/pki-collection/pki-collection-service";
|
||||
import { projectDALFactory } from "@app/services/project/project-dal";
|
||||
import { projectQueueFactory } from "@app/services/project/project-queue";
|
||||
import { projectServiceFactory } from "@app/services/project/project-service";
|
||||
@ -183,6 +192,7 @@ import { webhookServiceFactory } from "@app/services/webhook/webhook-service";
|
||||
import { injectAuditLogInfo } from "../plugins/audit-log";
|
||||
import { injectIdentity } from "../plugins/auth/inject-identity";
|
||||
import { injectPermission } from "../plugins/auth/inject-permission";
|
||||
import { injectRateLimits } from "../plugins/inject-rate-limits";
|
||||
import { registerSecretScannerGhApp } from "../plugins/secret-scanner";
|
||||
import { registerV1Routes } from "./v1";
|
||||
import { registerV2Routes } from "./v2";
|
||||
@ -354,7 +364,8 @@ export const registerRoutes = async (
|
||||
projectEnvDAL,
|
||||
secretApprovalPolicyApproverDAL: sapApproverDAL,
|
||||
permissionService,
|
||||
secretApprovalPolicyDAL
|
||||
secretApprovalPolicyDAL,
|
||||
licenseService
|
||||
});
|
||||
const tokenService = tokenServiceFactory({ tokenDAL: authTokenDAL, userDAL, orgMembershipDAL });
|
||||
|
||||
@ -498,6 +509,16 @@ export const registerRoutes = async (
|
||||
keyStore,
|
||||
licenseService
|
||||
});
|
||||
const orgAdminService = orgAdminServiceFactory({
|
||||
projectDAL,
|
||||
permissionService,
|
||||
projectUserMembershipRoleDAL,
|
||||
userDAL,
|
||||
projectBotDAL,
|
||||
projectKeyDAL,
|
||||
projectMembershipDAL
|
||||
});
|
||||
|
||||
const rateLimitService = rateLimitServiceFactory({
|
||||
rateLimitDAL,
|
||||
licenseService
|
||||
@ -568,10 +589,15 @@ export const registerRoutes = async (
|
||||
const certificateAuthorityCertDAL = certificateAuthorityCertDALFactory(db);
|
||||
const certificateAuthoritySecretDAL = certificateAuthoritySecretDALFactory(db);
|
||||
const certificateAuthorityCrlDAL = certificateAuthorityCrlDALFactory(db);
|
||||
const certificateTemplateDAL = certificateTemplateDALFactory(db);
|
||||
|
||||
const certificateDAL = certificateDALFactory(db);
|
||||
const certificateBodyDAL = certificateBodyDALFactory(db);
|
||||
|
||||
const pkiAlertDAL = pkiAlertDALFactory(db);
|
||||
const pkiCollectionDAL = pkiCollectionDALFactory(db);
|
||||
const pkiCollectionItemDAL = pkiCollectionItemDALFactory(db);
|
||||
|
||||
const certificateService = certificateServiceFactory({
|
||||
certificateDAL,
|
||||
certificateBodyDAL,
|
||||
@ -599,9 +625,12 @@ export const registerRoutes = async (
|
||||
certificateAuthorityCertDAL,
|
||||
certificateAuthoritySecretDAL,
|
||||
certificateAuthorityCrlDAL,
|
||||
certificateTemplateDAL,
|
||||
certificateAuthorityQueue,
|
||||
certificateDAL,
|
||||
certificateBodyDAL,
|
||||
pkiCollectionDAL,
|
||||
pkiCollectionItemDAL,
|
||||
projectDAL,
|
||||
kmsService,
|
||||
permissionService
|
||||
@ -616,6 +645,27 @@ export const registerRoutes = async (
|
||||
licenseService
|
||||
});
|
||||
|
||||
const certificateTemplateService = certificateTemplateServiceFactory({
|
||||
certificateTemplateDAL,
|
||||
certificateAuthorityDAL,
|
||||
permissionService
|
||||
});
|
||||
|
||||
const pkiAlertService = pkiAlertServiceFactory({
|
||||
pkiAlertDAL,
|
||||
pkiCollectionDAL,
|
||||
permissionService,
|
||||
smtpService
|
||||
});
|
||||
|
||||
const pkiCollectionService = pkiCollectionServiceFactory({
|
||||
pkiCollectionDAL,
|
||||
pkiCollectionItemDAL,
|
||||
certificateAuthorityDAL,
|
||||
certificateDAL,
|
||||
permissionService
|
||||
});
|
||||
|
||||
const projectService = projectServiceFactory({
|
||||
permissionService,
|
||||
projectDAL,
|
||||
@ -632,11 +682,14 @@ export const registerRoutes = async (
|
||||
licenseService,
|
||||
certificateAuthorityDAL,
|
||||
certificateDAL,
|
||||
pkiAlertDAL,
|
||||
pkiCollectionDAL,
|
||||
projectUserMembershipRoleDAL,
|
||||
identityProjectMembershipRoleDAL,
|
||||
keyStore,
|
||||
kmsService,
|
||||
projectBotDAL
|
||||
projectBotDAL,
|
||||
certificateTemplateDAL
|
||||
});
|
||||
|
||||
const projectEnvService = projectEnvServiceFactory({
|
||||
@ -699,6 +752,7 @@ export const registerRoutes = async (
|
||||
kmsService
|
||||
});
|
||||
const secretQueueService = secretQueueFactory({
|
||||
keyStore,
|
||||
queueService,
|
||||
secretDAL,
|
||||
folderDAL,
|
||||
@ -784,7 +838,8 @@ export const registerRoutes = async (
|
||||
secretVersionTagV2BridgeDAL,
|
||||
smtpService,
|
||||
projectEnvDAL,
|
||||
userDAL
|
||||
userDAL,
|
||||
licenseService
|
||||
});
|
||||
|
||||
const secretService = secretServiceFactory({
|
||||
@ -885,8 +940,15 @@ export const registerRoutes = async (
|
||||
folderDAL,
|
||||
integrationDAL,
|
||||
integrationAuthDAL,
|
||||
secretQueueService
|
||||
secretQueueService,
|
||||
integrationAuthService,
|
||||
projectBotService,
|
||||
secretV2BridgeDAL,
|
||||
secretImportDAL,
|
||||
secretDAL,
|
||||
kmsService
|
||||
});
|
||||
|
||||
const serviceTokenService = serviceTokenServiceFactory({
|
||||
projectEnvDAL,
|
||||
serviceTokenDAL,
|
||||
@ -1018,7 +1080,13 @@ export const registerRoutes = async (
|
||||
snapshotDAL,
|
||||
identityAccessTokenDAL,
|
||||
secretSharingDAL,
|
||||
secretVersionV2DAL: secretVersionV2BridgeDAL
|
||||
secretVersionV2DAL: secretVersionV2BridgeDAL,
|
||||
identityUniversalAuthClientSecretDAL: identityUaClientSecretDAL
|
||||
});
|
||||
|
||||
const dailyExpiringPkiItemAlert = dailyExpiringPkiItemAlertQueueServiceFactory({
|
||||
queueService,
|
||||
pkiAlertService
|
||||
});
|
||||
|
||||
const oidcService = oidcConfigServiceFactory({
|
||||
@ -1045,6 +1113,7 @@ export const registerRoutes = async (
|
||||
|
||||
await telemetryQueue.startTelemetryCheck();
|
||||
await dailyResourceCleanUp.startCleanUp();
|
||||
await dailyExpiringPkiItemAlert.startSendingAlerts();
|
||||
await kmsService.startService();
|
||||
|
||||
// inject all services
|
||||
@ -1102,7 +1171,10 @@ export const registerRoutes = async (
|
||||
auditLogStream: auditLogStreamService,
|
||||
certificate: certificateService,
|
||||
certificateAuthority: certificateAuthorityService,
|
||||
certificateTemplate: certificateTemplateService,
|
||||
certificateAuthorityCrl: certificateAuthorityCrlService,
|
||||
pkiAlert: pkiAlertService,
|
||||
pkiCollection: pkiCollectionService,
|
||||
secretScanning: secretScanningService,
|
||||
license: licenseService,
|
||||
trustedIp: trustedIpService,
|
||||
@ -1113,7 +1185,8 @@ export const registerRoutes = async (
|
||||
identityProjectAdditionalPrivilege: identityProjectAdditionalPrivilegeService,
|
||||
secretSharing: secretSharingService,
|
||||
userEngagement: userEngagementService,
|
||||
externalKms: externalKmsService
|
||||
externalKms: externalKmsService,
|
||||
orgAdmin: orgAdminService
|
||||
});
|
||||
|
||||
const cronJobs: CronJob[] = [];
|
||||
@ -1130,6 +1203,7 @@ export const registerRoutes = async (
|
||||
|
||||
await server.register(injectIdentity, { userDAL, serviceTokenDAL });
|
||||
await server.register(injectPermission);
|
||||
await server.register(injectRateLimits);
|
||||
await server.register(injectAuditLogInfo);
|
||||
|
||||
server.route({
|
||||
|
@ -63,8 +63,8 @@ export const secretRawSchema = z.object({
|
||||
version: z.number(),
|
||||
type: z.string(),
|
||||
secretKey: z.string(),
|
||||
secretValue: z.string().optional(),
|
||||
secretComment: z.string().optional(),
|
||||
secretValue: z.string(),
|
||||
secretComment: z.string(),
|
||||
secretReminderNote: z.string().nullable().optional(),
|
||||
secretReminderRepeatDays: z.number().nullable().optional(),
|
||||
skipMultilineEncoding: z.boolean().default(false).nullable().optional(),
|
||||
|
@ -8,7 +8,7 @@ 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 { CertKeyAlgorithm } from "@app/services/certificate/certificate-types";
|
||||
import { CaStatus, CaType } from "@app/services/certificate-authority/certificate-authority-types";
|
||||
import { CaRenewalType, CaStatus, CaType } from "@app/services/certificate-authority/certificate-authority-types";
|
||||
import {
|
||||
validateAltNamesField,
|
||||
validateCaDateField
|
||||
@ -275,15 +275,118 @@ export const registerCaRouter = async (server: FastifyZodProvider) => {
|
||||
}
|
||||
});
|
||||
|
||||
server.route({
|
||||
method: "POST",
|
||||
url: "/:caId/renew",
|
||||
config: {
|
||||
rateLimit: writeLimit
|
||||
},
|
||||
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
||||
schema: {
|
||||
description: "Perform CA certificate renewal",
|
||||
params: z.object({
|
||||
caId: z.string().trim().describe(CERTIFICATE_AUTHORITIES.RENEW_CA_CERT.caId)
|
||||
}),
|
||||
body: z.object({
|
||||
type: z.nativeEnum(CaRenewalType).describe(CERTIFICATE_AUTHORITIES.RENEW_CA_CERT.type),
|
||||
notAfter: validateCaDateField.describe(CERTIFICATE_AUTHORITIES.RENEW_CA_CERT.notAfter)
|
||||
}),
|
||||
response: {
|
||||
200: z.object({
|
||||
certificate: z.string().trim().describe(CERTIFICATE_AUTHORITIES.RENEW_CA_CERT.certificate),
|
||||
certificateChain: z.string().trim().describe(CERTIFICATE_AUTHORITIES.RENEW_CA_CERT.certificateChain),
|
||||
serialNumber: z.string().trim().describe(CERTIFICATE_AUTHORITIES.RENEW_CA_CERT.serialNumber)
|
||||
})
|
||||
}
|
||||
},
|
||||
handler: async (req) => {
|
||||
const { certificate, certificateChain, serialNumber, ca } =
|
||||
await server.services.certificateAuthority.renewCaCert({
|
||||
caId: req.params.caId,
|
||||
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.RENEW_CA,
|
||||
metadata: {
|
||||
caId: ca.id,
|
||||
dn: ca.dn
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return {
|
||||
certificate,
|
||||
certificateChain,
|
||||
serialNumber
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
server.route({
|
||||
method: "GET",
|
||||
url: "/:caId/certificate",
|
||||
url: "/:caId/ca-certificates",
|
||||
config: {
|
||||
rateLimit: readLimit
|
||||
},
|
||||
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
||||
schema: {
|
||||
description: "Get cert and cert chain of a CA",
|
||||
description: "Get list of past and current CA certificates for a CA",
|
||||
params: z.object({
|
||||
caId: z.string().trim().describe(CERTIFICATE_AUTHORITIES.GET_CA_CERTS.caId)
|
||||
}),
|
||||
response: {
|
||||
200: z.array(
|
||||
z.object({
|
||||
certificate: z.string().describe(CERTIFICATE_AUTHORITIES.GET_CA_CERTS.certificate),
|
||||
certificateChain: z.string().describe(CERTIFICATE_AUTHORITIES.GET_CA_CERTS.certificateChain),
|
||||
serialNumber: z.string().describe(CERTIFICATE_AUTHORITIES.GET_CA_CERTS.serialNumber),
|
||||
version: z.number().describe(CERTIFICATE_AUTHORITIES.GET_CA_CERTS.version)
|
||||
})
|
||||
)
|
||||
}
|
||||
},
|
||||
handler: async (req) => {
|
||||
const { caCerts, ca } = await server.services.certificateAuthority.getCaCerts({
|
||||
caId: req.params.caId,
|
||||
actor: req.permission.type,
|
||||
actorId: req.permission.id,
|
||||
actorAuthMethod: req.permission.authMethod,
|
||||
actorOrgId: req.permission.orgId
|
||||
});
|
||||
|
||||
await server.services.auditLog.createAuditLog({
|
||||
...req.auditLogInfo,
|
||||
projectId: ca.projectId,
|
||||
event: {
|
||||
type: EventType.GET_CA_CERTS,
|
||||
metadata: {
|
||||
caId: ca.id,
|
||||
dn: ca.dn
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return caCerts;
|
||||
}
|
||||
});
|
||||
|
||||
server.route({
|
||||
method: "GET",
|
||||
url: "/:caId/certificate", // TODO: consider updating endpoint structure considering CA certificates
|
||||
config: {
|
||||
rateLimit: readLimit
|
||||
},
|
||||
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
||||
schema: {
|
||||
description: "Get current CA cert and cert chain of a CA",
|
||||
params: z.object({
|
||||
caId: z.string().trim().describe(CERTIFICATE_AUTHORITIES.GET_CERT.caId)
|
||||
}),
|
||||
@ -453,6 +556,7 @@ export const registerCaRouter = async (server: FastifyZodProvider) => {
|
||||
}),
|
||||
body: z
|
||||
.object({
|
||||
pkiCollectionId: z.string().trim().optional().describe(CERTIFICATE_AUTHORITIES.ISSUE_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),
|
||||
@ -532,6 +636,7 @@ export const registerCaRouter = async (server: FastifyZodProvider) => {
|
||||
body: z
|
||||
.object({
|
||||
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),
|
||||
commonName: z.string().trim().min(1).optional().describe(CERTIFICATE_AUTHORITIES.SIGN_CERT.commonName),
|
||||
altNames: validateAltNamesField.describe(CERTIFICATE_AUTHORITIES.SIGN_CERT.altNames),
|
||||
|
@ -1,12 +1,17 @@
|
||||
import ms from "ms";
|
||||
import { z } from "zod";
|
||||
|
||||
import { CertificatesSchema } from "@app/db/schemas";
|
||||
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 { verifyAuth } from "@app/server/plugins/auth/verify-auth";
|
||||
import { AuthMode } from "@app/services/auth/auth-type";
|
||||
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) => {
|
||||
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({
|
||||
method: "POST",
|
||||
url: "/:serialNumber/revoke",
|
||||
|
205
backend/src/server/routes/v1/certificate-template-router.ts
Normal file
205
backend/src/server/routes/v1/certificate-template-router.ts
Normal file
@ -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 { EventType } from "@app/ee/services/audit-log/audit-log-types";
|
||||
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 { verifyAuth } from "@app/server/plugins/auth/verify-auth";
|
||||
import { AuthMode } from "@app/services/auth/auth-type";
|
||||
@ -16,7 +16,7 @@ export const registerIdentityRouter = async (server: FastifyZodProvider) => {
|
||||
method: "POST",
|
||||
url: "/",
|
||||
config: {
|
||||
rateLimit: creationLimit
|
||||
rateLimit: writeLimit
|
||||
},
|
||||
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
||||
schema: {
|
||||
|
@ -3,6 +3,7 @@ import { registerAuthRoutes } from "./auth-router";
|
||||
import { registerProjectBotRouter } from "./bot-router";
|
||||
import { registerCaRouter } from "./certificate-authority-router";
|
||||
import { registerCertRouter } from "./certificate-router";
|
||||
import { registerCertificateTemplateRouter } from "./certificate-template-router";
|
||||
import { registerIdentityAccessTokenRouter } from "./identity-access-token-router";
|
||||
import { registerIdentityAwsAuthRouter } from "./identity-aws-iam-auth-router";
|
||||
import { registerIdentityAzureAuthRouter } from "./identity-azure-auth-router";
|
||||
@ -15,8 +16,11 @@ import { registerIdentityUaRouter } from "./identity-universal-auth-router";
|
||||
import { registerIntegrationAuthRouter } from "./integration-auth-router";
|
||||
import { registerIntegrationRouter } from "./integration-router";
|
||||
import { registerInviteOrgRouter } from "./invite-org-router";
|
||||
import { registerOrgAdminRouter } from "./org-admin-router";
|
||||
import { registerOrgRouter } from "./organization-router";
|
||||
import { registerPasswordRouter } from "./password-router";
|
||||
import { registerPkiAlertRouter } from "./pki-alert-router";
|
||||
import { registerPkiCollectionRouter } from "./pki-collection-router";
|
||||
import { registerProjectEnvRouter } from "./project-env-router";
|
||||
import { registerProjectKeyRouter } from "./project-key-router";
|
||||
import { registerProjectMembershipRouter } from "./project-membership-router";
|
||||
@ -50,6 +54,7 @@ export const registerV1Routes = async (server: FastifyZodProvider) => {
|
||||
await server.register(registerPasswordRouter, { prefix: "/password" });
|
||||
await server.register(registerOrgRouter, { prefix: "/organization" });
|
||||
await server.register(registerAdminRouter, { prefix: "/admin" });
|
||||
await server.register(registerOrgAdminRouter, { prefix: "/organization-admin" });
|
||||
await server.register(registerUserRouter, { prefix: "/user" });
|
||||
await server.register(registerInviteOrgRouter, { prefix: "/invite-org" });
|
||||
await server.register(registerUserActionRouter, { prefix: "/user-action" });
|
||||
@ -72,6 +77,9 @@ export const registerV1Routes = async (server: FastifyZodProvider) => {
|
||||
async (pkiRouter) => {
|
||||
await pkiRouter.register(registerCaRouter, { prefix: "/ca" });
|
||||
await pkiRouter.register(registerCertRouter, { prefix: "/certificates" });
|
||||
await pkiRouter.register(registerCertificateTemplateRouter, { prefix: "/certificate-templates" });
|
||||
await pkiRouter.register(registerPkiAlertRouter, { prefix: "/alerts" });
|
||||
await pkiRouter.register(registerPkiCollectionRouter, { prefix: "/collections" });
|
||||
},
|
||||
{ prefix: "/pki" }
|
||||
);
|
||||
|
@ -170,6 +170,12 @@ export const registerIntegrationRouter = async (server: FastifyZodProvider) => {
|
||||
params: z.object({
|
||||
integrationId: z.string().trim().describe(INTEGRATION.DELETE.integrationId)
|
||||
}),
|
||||
querystring: z.object({
|
||||
shouldDeleteIntegrationSecrets: z
|
||||
.enum(["true", "false"])
|
||||
.optional()
|
||||
.transform((val) => val === "true")
|
||||
}),
|
||||
response: {
|
||||
200: z.object({
|
||||
integration: IntegrationsSchema
|
||||
@ -183,7 +189,8 @@ export const registerIntegrationRouter = async (server: FastifyZodProvider) => {
|
||||
actorAuthMethod: req.permission.authMethod,
|
||||
actor: req.permission.type,
|
||||
actorOrgId: req.permission.orgId,
|
||||
id: req.params.integrationId
|
||||
id: req.params.integrationId,
|
||||
shouldDeleteIntegrationSecrets: req.query.shouldDeleteIntegrationSecrets
|
||||
});
|
||||
|
||||
await server.services.auditLog.createAuditLog({
|
||||
@ -205,7 +212,8 @@ export const registerIntegrationRouter = async (server: FastifyZodProvider) => {
|
||||
targetService: integration.targetService,
|
||||
targetServiceId: integration.targetServiceId,
|
||||
path: integration.path,
|
||||
region: integration.region
|
||||
region: integration.region,
|
||||
shouldDeleteIntegrationSecrets: req.query.shouldDeleteIntegrationSecrets
|
||||
// eslint-disable-next-line
|
||||
}) as any
|
||||
}
|
||||
|
90
backend/src/server/routes/v1/org-admin-router.ts
Normal file
90
backend/src/server/routes/v1/org-admin-router.ts
Normal file
@ -0,0 +1,90 @@
|
||||
import { z } from "zod";
|
||||
|
||||
import { ProjectMembershipsSchema } from "@app/db/schemas";
|
||||
import { EventType } from "@app/ee/services/audit-log/audit-log-types";
|
||||
import { readLimit } from "@app/server/config/rateLimiter";
|
||||
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
|
||||
import { AuthMode } from "@app/services/auth/auth-type";
|
||||
|
||||
import { SanitizedProjectSchema } from "../sanitizedSchemas";
|
||||
|
||||
export const registerOrgAdminRouter = async (server: FastifyZodProvider) => {
|
||||
server.route({
|
||||
method: "GET",
|
||||
url: "/projects",
|
||||
config: {
|
||||
rateLimit: readLimit
|
||||
},
|
||||
schema: {
|
||||
querystring: z.object({
|
||||
search: z.string().optional(),
|
||||
offset: z.coerce.number().default(0),
|
||||
limit: z.coerce.number().max(100).default(50)
|
||||
}),
|
||||
response: {
|
||||
200: z.object({
|
||||
projects: SanitizedProjectSchema.array(),
|
||||
count: z.coerce.number()
|
||||
})
|
||||
}
|
||||
},
|
||||
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
||||
handler: async (req) => {
|
||||
const { projects, count } = await server.services.orgAdmin.listOrgProjects({
|
||||
limit: req.query.limit,
|
||||
offset: req.query.offset,
|
||||
search: req.query.search,
|
||||
actorOrgId: req.permission.orgId,
|
||||
actorAuthMethod: req.permission.authMethod,
|
||||
actorId: req.permission.id,
|
||||
actor: req.permission.type
|
||||
});
|
||||
return { projects, count };
|
||||
}
|
||||
});
|
||||
|
||||
server.route({
|
||||
method: "POST",
|
||||
url: "/projects/:projectId/grant-admin-access",
|
||||
config: {
|
||||
rateLimit: readLimit
|
||||
},
|
||||
schema: {
|
||||
params: z.object({
|
||||
projectId: z.string()
|
||||
}),
|
||||
response: {
|
||||
200: z.object({
|
||||
membership: ProjectMembershipsSchema
|
||||
})
|
||||
}
|
||||
},
|
||||
onRequest: verifyAuth([AuthMode.JWT]),
|
||||
handler: async (req) => {
|
||||
const { membership } = await server.services.orgAdmin.grantProjectAdminAccess({
|
||||
actorOrgId: req.permission.orgId,
|
||||
actorAuthMethod: req.permission.authMethod,
|
||||
actorId: req.permission.id,
|
||||
actor: req.permission.type,
|
||||
projectId: req.params.projectId
|
||||
});
|
||||
if (req.auth.authMode === AuthMode.JWT) {
|
||||
await server.services.auditLog.createAuditLog({
|
||||
...req.auditLogInfo,
|
||||
projectId: req.params.projectId,
|
||||
event: {
|
||||
type: EventType.ORG_ADMIN_ACCESS_PROJECT,
|
||||
metadata: {
|
||||
projectId: req.params.projectId,
|
||||
username: req.auth.user.username,
|
||||
email: req.auth.user.email || "",
|
||||
userId: req.auth.userId
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return { membership };
|
||||
}
|
||||
});
|
||||
};
|
198
backend/src/server/routes/v1/pki-alert-router.ts
Normal file
198
backend/src/server/routes/v1/pki-alert-router.ts
Normal file
@ -0,0 +1,198 @@
|
||||
import { z } from "zod";
|
||||
|
||||
import { PkiAlertsSchema } from "@app/db/schemas";
|
||||
import { EventType } from "@app/ee/services/audit-log/audit-log-types";
|
||||
import { ALERTS } 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";
|
||||
|
||||
export const registerPkiAlertRouter = async (server: FastifyZodProvider) => {
|
||||
server.route({
|
||||
method: "POST",
|
||||
url: "/",
|
||||
config: {
|
||||
rateLimit: writeLimit
|
||||
},
|
||||
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
||||
schema: {
|
||||
description: "Create PKI alert",
|
||||
body: z.object({
|
||||
projectId: z.string().trim().describe(ALERTS.CREATE.projectId),
|
||||
pkiCollectionId: z.string().trim().describe(ALERTS.CREATE.pkiCollectionId),
|
||||
name: z.string().trim().describe(ALERTS.CREATE.name),
|
||||
alertBeforeDays: z.number().describe(ALERTS.CREATE.alertBeforeDays),
|
||||
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: {
|
||||
200: PkiAlertsSchema
|
||||
}
|
||||
},
|
||||
handler: async (req) => {
|
||||
const alert = await server.services.pkiAlert.createPkiAlert({
|
||||
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: alert.projectId,
|
||||
event: {
|
||||
type: EventType.CREATE_PKI_ALERT,
|
||||
metadata: {
|
||||
pkiAlertId: alert.id,
|
||||
pkiCollectionId: alert.pkiCollectionId,
|
||||
name: alert.name,
|
||||
alertBeforeDays: alert.alertBeforeDays,
|
||||
recipientEmails: alert.recipientEmails
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return alert;
|
||||
}
|
||||
});
|
||||
|
||||
server.route({
|
||||
method: "GET",
|
||||
url: "/:alertId",
|
||||
config: {
|
||||
rateLimit: readLimit
|
||||
},
|
||||
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
||||
schema: {
|
||||
description: "Get PKI alert",
|
||||
params: z.object({
|
||||
alertId: z.string().trim().describe(ALERTS.GET.alertId)
|
||||
}),
|
||||
response: {
|
||||
200: PkiAlertsSchema
|
||||
}
|
||||
},
|
||||
handler: async (req) => {
|
||||
const alert = await server.services.pkiAlert.getPkiAlertById({
|
||||
alertId: req.params.alertId,
|
||||
actor: req.permission.type,
|
||||
actorId: req.permission.id,
|
||||
actorAuthMethod: req.permission.authMethod,
|
||||
actorOrgId: req.permission.orgId
|
||||
});
|
||||
|
||||
await server.services.auditLog.createAuditLog({
|
||||
...req.auditLogInfo,
|
||||
projectId: alert.projectId,
|
||||
event: {
|
||||
type: EventType.GET_PKI_ALERT,
|
||||
metadata: {
|
||||
pkiAlertId: alert.id
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return alert;
|
||||
}
|
||||
});
|
||||
|
||||
server.route({
|
||||
method: "PATCH",
|
||||
url: "/:alertId",
|
||||
config: {
|
||||
rateLimit: readLimit
|
||||
},
|
||||
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
||||
schema: {
|
||||
description: "Update PKI alert",
|
||||
params: z.object({
|
||||
alertId: z.string().trim().describe(ALERTS.UPDATE.alertId)
|
||||
}),
|
||||
body: z.object({
|
||||
name: z.string().trim().optional().describe(ALERTS.UPDATE.name),
|
||||
alertBeforeDays: z.number().optional().describe(ALERTS.UPDATE.alertBeforeDays),
|
||||
pkiCollectionId: z.string().trim().optional().describe(ALERTS.UPDATE.pkiCollectionId),
|
||||
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" })
|
||||
.optional()
|
||||
.describe(ALERTS.UPDATE.emails)
|
||||
}),
|
||||
response: {
|
||||
200: PkiAlertsSchema
|
||||
}
|
||||
},
|
||||
handler: async (req) => {
|
||||
const alert = await server.services.pkiAlert.updatePkiAlert({
|
||||
alertId: req.params.alertId,
|
||||
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: alert.projectId,
|
||||
event: {
|
||||
type: EventType.UPDATE_PKI_ALERT,
|
||||
metadata: {
|
||||
pkiAlertId: alert.id,
|
||||
pkiCollectionId: alert.pkiCollectionId,
|
||||
name: alert.name,
|
||||
alertBeforeDays: alert.alertBeforeDays,
|
||||
recipientEmails: alert.recipientEmails
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return alert;
|
||||
}
|
||||
});
|
||||
|
||||
server.route({
|
||||
method: "DELETE",
|
||||
url: "/:alertId",
|
||||
config: {
|
||||
rateLimit: writeLimit
|
||||
},
|
||||
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
||||
schema: {
|
||||
description: "Delete PKI alert",
|
||||
params: z.object({
|
||||
alertId: z.string().trim().describe(ALERTS.DELETE.alertId)
|
||||
}),
|
||||
response: {
|
||||
200: PkiAlertsSchema
|
||||
}
|
||||
},
|
||||
handler: async (req) => {
|
||||
const alert = await server.services.pkiAlert.deletePkiAlert({
|
||||
alertId: req.params.alertId,
|
||||
actor: req.permission.type,
|
||||
actorId: req.permission.id,
|
||||
actorAuthMethod: req.permission.authMethod,
|
||||
actorOrgId: req.permission.orgId
|
||||
});
|
||||
|
||||
await server.services.auditLog.createAuditLog({
|
||||
...req.auditLogInfo,
|
||||
projectId: alert.projectId,
|
||||
event: {
|
||||
type: EventType.DELETE_PKI_ALERT,
|
||||
metadata: {
|
||||
pkiAlertId: alert.id
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return alert;
|
||||
}
|
||||
});
|
||||
};
|
338
backend/src/server/routes/v1/pki-collection-router.ts
Normal file
338
backend/src/server/routes/v1/pki-collection-router.ts
Normal file
@ -0,0 +1,338 @@
|
||||
import { z } from "zod";
|
||||
|
||||
import { PkiCollectionItemsSchema, PkiCollectionsSchema } from "@app/db/schemas";
|
||||
import { EventType } from "@app/ee/services/audit-log/audit-log-types";
|
||||
import { PKI_COLLECTIONS } 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 { PkiItemType } from "@app/services/pki-collection/pki-collection-types";
|
||||
|
||||
export const registerPkiCollectionRouter = async (server: FastifyZodProvider) => {
|
||||
server.route({
|
||||
method: "POST",
|
||||
url: "/",
|
||||
config: {
|
||||
rateLimit: writeLimit
|
||||
},
|
||||
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
||||
schema: {
|
||||
description: "Create PKI collection",
|
||||
body: z.object({
|
||||
projectId: z.string().trim().describe(PKI_COLLECTIONS.CREATE.projectId),
|
||||
name: z.string().trim().describe(PKI_COLLECTIONS.CREATE.name),
|
||||
description: z.string().trim().default("").describe(PKI_COLLECTIONS.CREATE.description)
|
||||
}),
|
||||
response: {
|
||||
200: PkiCollectionsSchema
|
||||
}
|
||||
},
|
||||
handler: async (req) => {
|
||||
const pkiCollection = await server.services.pkiCollection.createPkiCollection({
|
||||
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: pkiCollection.projectId,
|
||||
event: {
|
||||
type: EventType.CREATE_PKI_COLLECTION,
|
||||
metadata: {
|
||||
pkiCollectionId: pkiCollection.id,
|
||||
name: pkiCollection.name
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return pkiCollection;
|
||||
}
|
||||
});
|
||||
|
||||
server.route({
|
||||
method: "GET",
|
||||
url: "/:collectionId",
|
||||
config: {
|
||||
rateLimit: readLimit
|
||||
},
|
||||
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
||||
schema: {
|
||||
description: "Get PKI collection",
|
||||
params: z.object({
|
||||
collectionId: z.string().trim().describe(PKI_COLLECTIONS.GET.collectionId)
|
||||
}),
|
||||
response: {
|
||||
200: PkiCollectionsSchema
|
||||
}
|
||||
},
|
||||
handler: async (req) => {
|
||||
const pkiCollection = await server.services.pkiCollection.getPkiCollectionById({
|
||||
collectionId: req.params.collectionId,
|
||||
actor: req.permission.type,
|
||||
actorId: req.permission.id,
|
||||
actorAuthMethod: req.permission.authMethod,
|
||||
actorOrgId: req.permission.orgId
|
||||
});
|
||||
|
||||
await server.services.auditLog.createAuditLog({
|
||||
...req.auditLogInfo,
|
||||
projectId: pkiCollection.projectId,
|
||||
event: {
|
||||
type: EventType.GET_PKI_COLLECTION,
|
||||
metadata: {
|
||||
pkiCollectionId: pkiCollection.id
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return pkiCollection;
|
||||
}
|
||||
});
|
||||
|
||||
server.route({
|
||||
method: "PATCH",
|
||||
url: "/:collectionId",
|
||||
config: {
|
||||
rateLimit: writeLimit
|
||||
},
|
||||
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
||||
schema: {
|
||||
description: "Update PKI collection",
|
||||
params: z.object({
|
||||
collectionId: z.string().trim().describe(PKI_COLLECTIONS.UPDATE.collectionId)
|
||||
}),
|
||||
body: z.object({
|
||||
name: z.string().trim().optional().describe(PKI_COLLECTIONS.UPDATE.name),
|
||||
description: z.string().trim().optional().describe(PKI_COLLECTIONS.UPDATE.description)
|
||||
}),
|
||||
response: {
|
||||
200: PkiCollectionsSchema
|
||||
}
|
||||
},
|
||||
handler: async (req) => {
|
||||
const pkiCollection = await server.services.pkiCollection.updatePkiCollection({
|
||||
collectionId: req.params.collectionId,
|
||||
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: pkiCollection.projectId,
|
||||
event: {
|
||||
type: EventType.UPDATE_PKI_COLLECTION,
|
||||
metadata: {
|
||||
pkiCollectionId: pkiCollection.id,
|
||||
name: pkiCollection.name
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return pkiCollection;
|
||||
}
|
||||
});
|
||||
|
||||
server.route({
|
||||
method: "DELETE",
|
||||
url: "/:collectionId",
|
||||
config: {
|
||||
rateLimit: writeLimit
|
||||
},
|
||||
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
||||
schema: {
|
||||
description: "Delete PKI collection",
|
||||
params: z.object({
|
||||
collectionId: z.string().trim().describe(PKI_COLLECTIONS.DELETE.collectionId)
|
||||
}),
|
||||
response: {
|
||||
200: PkiCollectionsSchema
|
||||
}
|
||||
},
|
||||
handler: async (req) => {
|
||||
const pkiCollection = await server.services.pkiCollection.deletePkiCollection({
|
||||
collectionId: req.params.collectionId,
|
||||
actor: req.permission.type,
|
||||
actorId: req.permission.id,
|
||||
actorAuthMethod: req.permission.authMethod,
|
||||
actorOrgId: req.permission.orgId
|
||||
});
|
||||
|
||||
await server.services.auditLog.createAuditLog({
|
||||
...req.auditLogInfo,
|
||||
projectId: pkiCollection.projectId,
|
||||
event: {
|
||||
type: EventType.DELETE_PKI_COLLECTION,
|
||||
metadata: {
|
||||
pkiCollectionId: pkiCollection.id
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return pkiCollection;
|
||||
}
|
||||
});
|
||||
|
||||
server.route({
|
||||
method: "GET",
|
||||
url: "/:collectionId/items",
|
||||
config: {
|
||||
rateLimit: writeLimit
|
||||
},
|
||||
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
||||
schema: {
|
||||
description: "Get items in PKI collection",
|
||||
params: z.object({
|
||||
collectionId: z.string().trim().describe(PKI_COLLECTIONS.LIST_ITEMS.collectionId)
|
||||
}),
|
||||
querystring: z.object({
|
||||
type: z.nativeEnum(PkiItemType).optional().describe(PKI_COLLECTIONS.LIST_ITEMS.type),
|
||||
offset: z.coerce.number().min(0).max(100).default(0).describe(PKI_COLLECTIONS.LIST_ITEMS.offset),
|
||||
limit: z.coerce.number().min(1).max(100).default(25).describe(PKI_COLLECTIONS.LIST_ITEMS.limit)
|
||||
}),
|
||||
response: {
|
||||
200: z.object({
|
||||
collectionItems: z.array(
|
||||
PkiCollectionItemsSchema.omit({ caId: true, certId: true }).extend({
|
||||
type: z.nativeEnum(PkiItemType),
|
||||
itemId: z.string().trim(),
|
||||
notBefore: z.date(),
|
||||
notAfter: z.date(),
|
||||
friendlyName: z.string().trim()
|
||||
})
|
||||
),
|
||||
totalCount: z.number()
|
||||
})
|
||||
}
|
||||
},
|
||||
handler: async (req) => {
|
||||
const { pkiCollection, pkiCollectionItems, totalCount } =
|
||||
await server.services.pkiCollection.getPkiCollectionItems({
|
||||
collectionId: req.params.collectionId,
|
||||
actor: req.permission.type,
|
||||
actorId: req.permission.id,
|
||||
actorAuthMethod: req.permission.authMethod,
|
||||
actorOrgId: req.permission.orgId,
|
||||
...req.query
|
||||
});
|
||||
|
||||
await server.services.auditLog.createAuditLog({
|
||||
...req.auditLogInfo,
|
||||
projectId: pkiCollection.projectId,
|
||||
event: {
|
||||
type: EventType.GET_PKI_COLLECTION_ITEMS,
|
||||
metadata: {
|
||||
pkiCollectionId: pkiCollection.id
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return {
|
||||
collectionItems: pkiCollectionItems,
|
||||
totalCount
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
server.route({
|
||||
method: "POST",
|
||||
url: "/:collectionId/items",
|
||||
config: {
|
||||
rateLimit: writeLimit
|
||||
},
|
||||
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
||||
schema: {
|
||||
description: "Add item to PKI collection",
|
||||
params: z.object({
|
||||
collectionId: z.string().trim().describe(PKI_COLLECTIONS.ADD_ITEM.collectionId)
|
||||
}),
|
||||
body: z.object({
|
||||
type: z.nativeEnum(PkiItemType).describe(PKI_COLLECTIONS.ADD_ITEM.type),
|
||||
itemId: z.string().trim().describe(PKI_COLLECTIONS.ADD_ITEM.itemId)
|
||||
}),
|
||||
response: {
|
||||
200: PkiCollectionItemsSchema.omit({ caId: true, certId: true }).extend({
|
||||
type: z.nativeEnum(PkiItemType).describe(PKI_COLLECTIONS.ADD_ITEM.type),
|
||||
itemId: z.string().trim().describe(PKI_COLLECTIONS.ADD_ITEM.itemId)
|
||||
})
|
||||
}
|
||||
},
|
||||
handler: async (req) => {
|
||||
const { pkiCollection, pkiCollectionItem } = await server.services.pkiCollection.addItemToPkiCollection({
|
||||
collectionId: req.params.collectionId,
|
||||
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: pkiCollection.projectId,
|
||||
event: {
|
||||
type: EventType.ADD_PKI_COLLECTION_ITEM,
|
||||
metadata: {
|
||||
pkiCollectionId: pkiCollection.id,
|
||||
pkiCollectionItemId: pkiCollectionItem.id,
|
||||
type: pkiCollectionItem.type,
|
||||
itemId: pkiCollectionItem.itemId
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return pkiCollectionItem;
|
||||
}
|
||||
});
|
||||
|
||||
server.route({
|
||||
method: "DELETE",
|
||||
url: "/:collectionId/items/:collectionItemId",
|
||||
config: {
|
||||
rateLimit: writeLimit
|
||||
},
|
||||
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
||||
schema: {
|
||||
description: "Remove item from PKI collection",
|
||||
params: z.object({
|
||||
collectionId: z.string().trim().describe(PKI_COLLECTIONS.DELETE_ITEM.collectionId),
|
||||
collectionItemId: z.string().trim().describe(PKI_COLLECTIONS.DELETE_ITEM.collectionItemId)
|
||||
}),
|
||||
response: {
|
||||
200: PkiCollectionItemsSchema.omit({ caId: true, certId: true }).extend({
|
||||
type: z.nativeEnum(PkiItemType).describe(PKI_COLLECTIONS.DELETE_ITEM.type),
|
||||
itemId: z.string().trim().describe(PKI_COLLECTIONS.DELETE_ITEM.itemId)
|
||||
})
|
||||
}
|
||||
},
|
||||
handler: async (req) => {
|
||||
const { pkiCollection, pkiCollectionItem } = await server.services.pkiCollection.removeItemFromPkiCollection({
|
||||
collectionId: req.params.collectionId,
|
||||
itemId: req.params.collectionItemId,
|
||||
actor: req.permission.type,
|
||||
actorId: req.permission.id,
|
||||
actorAuthMethod: req.permission.authMethod,
|
||||
actorOrgId: req.permission.orgId
|
||||
});
|
||||
|
||||
await server.services.auditLog.createAuditLog({
|
||||
...req.auditLogInfo,
|
||||
projectId: pkiCollection.projectId,
|
||||
event: {
|
||||
type: EventType.DELETE_PKI_COLLECTION_ITEM,
|
||||
metadata: {
|
||||
pkiCollectionId: pkiCollection.id,
|
||||
pkiCollectionItemId: pkiCollectionItem.id
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return pkiCollectionItem;
|
||||
}
|
||||
});
|
||||
};
|
@ -1,3 +1,4 @@
|
||||
import slugify from "@sindresorhus/slugify";
|
||||
import { z } from "zod";
|
||||
|
||||
import { SecretTagsSchema } from "@app/db/schemas";
|
||||
@ -49,7 +50,8 @@ export const registerSecretTagRouter = async (server: FastifyZodProvider) => {
|
||||
}),
|
||||
response: {
|
||||
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: {
|
||||
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)
|
||||
}),
|
||||
body: z.object({
|
||||
name: z.string().trim().describe(SECRET_TAGS.CREATE.name),
|
||||
slug: z.string().trim().describe(SECRET_TAGS.CREATE.slug),
|
||||
slug: z
|
||||
.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)
|
||||
}),
|
||||
response: {
|
||||
@ -144,8 +153,14 @@ export const registerSecretTagRouter = async (server: FastifyZodProvider) => {
|
||||
tagId: z.string().trim().describe(SECRET_TAGS.UPDATE.tagId)
|
||||
}),
|
||||
body: z.object({
|
||||
name: z.string().trim().describe(SECRET_TAGS.UPDATE.name),
|
||||
slug: z.string().trim().describe(SECRET_TAGS.UPDATE.slug),
|
||||
slug: z
|
||||
.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)
|
||||
}),
|
||||
response: {
|
||||
|
@ -9,7 +9,7 @@ import {
|
||||
UsersSchema
|
||||
} from "@app/db/schemas";
|
||||
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 { ActorType, AuthMode } from "@app/services/auth/auth-type";
|
||||
|
||||
@ -307,7 +307,7 @@ export const registerOrgRouter = async (server: FastifyZodProvider) => {
|
||||
method: "POST",
|
||||
url: "/",
|
||||
config: {
|
||||
rateLimit: creationLimit
|
||||
rateLimit: writeLimit
|
||||
},
|
||||
schema: {
|
||||
body: z.object({
|
||||
|
@ -1,14 +1,21 @@
|
||||
import slugify from "@sindresorhus/slugify";
|
||||
import { z } from "zod";
|
||||
|
||||
import { CertificateAuthoritiesSchema, CertificatesSchema, ProjectKeysSchema } from "@app/db/schemas";
|
||||
import {
|
||||
CertificateAuthoritiesSchema,
|
||||
CertificatesSchema,
|
||||
PkiAlertsSchema,
|
||||
PkiCollectionsSchema,
|
||||
ProjectKeysSchema
|
||||
} from "@app/db/schemas";
|
||||
import { EventType } from "@app/ee/services/audit-log/audit-log-types";
|
||||
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 { verifyAuth } from "@app/server/plugins/auth/verify-auth";
|
||||
import { AuthMode } from "@app/services/auth/auth-type";
|
||||
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 { PostHogEventTypes } from "@app/services/telemetry/telemetry-types";
|
||||
|
||||
@ -142,7 +149,7 @@ export const registerProjectRouter = async (server: FastifyZodProvider) => {
|
||||
method: "POST",
|
||||
url: "/",
|
||||
config: {
|
||||
rateLimit: creationLimit
|
||||
rateLimit: writeLimit
|
||||
},
|
||||
schema: {
|
||||
description: "Create a new project",
|
||||
@ -392,4 +399,94 @@ export const registerProjectRouter = async (server: FastifyZodProvider) => {
|
||||
return { certificates, totalCount };
|
||||
}
|
||||
});
|
||||
|
||||
server.route({
|
||||
method: "GET",
|
||||
url: "/:projectId/pki-alerts",
|
||||
config: {
|
||||
rateLimit: readLimit
|
||||
},
|
||||
schema: {
|
||||
params: z.object({
|
||||
projectId: z.string().trim()
|
||||
}),
|
||||
response: {
|
||||
200: z.object({
|
||||
alerts: z.array(PkiAlertsSchema)
|
||||
})
|
||||
}
|
||||
},
|
||||
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
||||
handler: async (req) => {
|
||||
const { alerts } = await server.services.project.listProjectAlerts({
|
||||
projectId: req.params.projectId,
|
||||
actorId: req.permission.id,
|
||||
actorOrgId: req.permission.orgId,
|
||||
actorAuthMethod: req.permission.authMethod,
|
||||
actor: req.permission.type
|
||||
});
|
||||
|
||||
return { alerts };
|
||||
}
|
||||
});
|
||||
|
||||
server.route({
|
||||
method: "GET",
|
||||
url: "/:projectId/pki-collections",
|
||||
config: {
|
||||
rateLimit: readLimit
|
||||
},
|
||||
schema: {
|
||||
params: z.object({
|
||||
projectId: z.string().trim()
|
||||
}),
|
||||
response: {
|
||||
200: z.object({
|
||||
collections: z.array(PkiCollectionsSchema)
|
||||
})
|
||||
}
|
||||
},
|
||||
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
||||
handler: async (req) => {
|
||||
const { pkiCollections } = await server.services.project.listProjectPkiCollections({
|
||||
projectId: req.params.projectId,
|
||||
actorId: req.permission.id,
|
||||
actorOrgId: req.permission.orgId,
|
||||
actorAuthMethod: req.permission.authMethod,
|
||||
actor: req.permission.type
|
||||
});
|
||||
|
||||
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 };
|
||||
}
|
||||
});
|
||||
};
|
||||
|
@ -59,9 +59,10 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => {
|
||||
tags: SecretTagsSchema.pick({
|
||||
id: true,
|
||||
slug: true,
|
||||
name: true,
|
||||
color: true
|
||||
}).array()
|
||||
})
|
||||
.extend({ name: z.string() })
|
||||
.array()
|
||||
})
|
||||
)
|
||||
})
|
||||
@ -116,16 +117,15 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => {
|
||||
}),
|
||||
response: {
|
||||
200: z.object({
|
||||
secret: SecretsSchema.omit({ secretBlindIndex: true }).merge(
|
||||
z.object({
|
||||
tags: SecretTagsSchema.pick({
|
||||
id: true,
|
||||
slug: true,
|
||||
name: true,
|
||||
color: true
|
||||
}).array()
|
||||
secret: SecretsSchema.omit({ secretBlindIndex: true }).extend({
|
||||
tags: SecretTagsSchema.pick({
|
||||
id: true,
|
||||
slug: true,
|
||||
color: true
|
||||
})
|
||||
)
|
||||
.extend({ name: z.string() })
|
||||
.array()
|
||||
})
|
||||
})
|
||||
}
|
||||
},
|
||||
@ -180,7 +180,13 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => {
|
||||
.enum(["true", "false"])
|
||||
.default("false")
|
||||
.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: {
|
||||
200: z.object({
|
||||
@ -190,9 +196,9 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => {
|
||||
tags: SecretTagsSchema.pick({
|
||||
id: true,
|
||||
slug: true,
|
||||
name: true,
|
||||
color: true
|
||||
})
|
||||
.extend({ name: z.string() })
|
||||
.array()
|
||||
.optional()
|
||||
})
|
||||
@ -251,7 +257,8 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => {
|
||||
projectId: workspaceId,
|
||||
path: secretPath,
|
||||
includeImports: req.query.include_imports,
|
||||
recursive: req.query.recursive
|
||||
recursive: req.query.recursive,
|
||||
tagSlugs: req.query.tagSlugs
|
||||
});
|
||||
|
||||
await server.services.auditLog.createAuditLog({
|
||||
@ -325,9 +332,9 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => {
|
||||
tags: SecretTagsSchema.pick({
|
||||
id: true,
|
||||
slug: true,
|
||||
name: true,
|
||||
color: true
|
||||
})
|
||||
.extend({ name: z.string() })
|
||||
.array()
|
||||
.optional()
|
||||
})
|
||||
@ -731,9 +738,10 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => {
|
||||
tags: SecretTagsSchema.pick({
|
||||
id: true,
|
||||
slug: true,
|
||||
name: true,
|
||||
color: true
|
||||
}).array()
|
||||
})
|
||||
.extend({ name: z.string() })
|
||||
.array()
|
||||
})
|
||||
.array(),
|
||||
imports: z
|
||||
|
@ -1,11 +1,17 @@
|
||||
import * as x509 from "@peculiar/x509";
|
||||
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 { CertKeyAlgorithm, CertStatus } from "../certificate/certificate-types";
|
||||
import { TDNParts, TGetCaCertChainDTO, TGetCaCredentialsDTO, TRebuildCaCrlDTO } from "./certificate-authority-types";
|
||||
import {
|
||||
TDNParts,
|
||||
TGetCaCertChainDTO,
|
||||
TGetCaCertChainsDTO,
|
||||
TGetCaCredentialsDTO,
|
||||
TRebuildCaCrlDTO
|
||||
} from "./certificate-authority-types";
|
||||
|
||||
export const createDistinguishedName = (parts: TDNParts) => {
|
||||
const dnParts = [];
|
||||
@ -89,6 +95,8 @@ export const keyAlgorithmToAlgCfg = (keyAlgorithm: CertKeyAlgorithm) => {
|
||||
* Return the public and private key of CA with id [caId]
|
||||
* Note: credentials are returned as crypto.webcrypto.CryptoKey
|
||||
* suitable for use with @peculiar/x509 module
|
||||
*
|
||||
* TODO: Update to get latest CA Secret once support for CA renewal with new key pair is added
|
||||
*/
|
||||
export const getCaCredentials = async ({
|
||||
caId,
|
||||
@ -98,10 +106,10 @@ export const getCaCredentials = async ({
|
||||
kmsService
|
||||
}: TGetCaCredentialsDTO) => {
|
||||
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 });
|
||||
if (!caSecret) throw new BadRequestError({ message: "CA secret not found" });
|
||||
if (!caSecret) throw new NotFoundError({ message: "CA secret not found" });
|
||||
|
||||
const keyId = await getProjectKmsCertificateKeyId({
|
||||
projectId: ca.projectId,
|
||||
@ -132,26 +140,73 @@ export const getCaCredentials = async ({
|
||||
]);
|
||||
|
||||
return {
|
||||
caSecret,
|
||||
caPrivateKey,
|
||||
caPublicKey
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Return the decrypted pem-encoded certificate and certificate chain
|
||||
* Return the list of decrypted pem-encoded certificates and certificate chains
|
||||
* for CA with id [caId].
|
||||
*/
|
||||
export const getCaCertChain = async ({
|
||||
export const getCaCertChains = async ({
|
||||
caId,
|
||||
certificateAuthorityDAL,
|
||||
certificateAuthorityCertDAL,
|
||||
projectDAL,
|
||||
kmsService
|
||||
}: TGetCaCertChainDTO) => {
|
||||
}: TGetCaCertChainsDTO) => {
|
||||
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 caCert = await certificateAuthorityCertDAL.findOne({ caId: ca.id });
|
||||
const keyId = await getProjectKmsCertificateKeyId({
|
||||
projectId: ca.projectId,
|
||||
projectDAL,
|
||||
kmsService
|
||||
});
|
||||
|
||||
const kmsDecryptor = await kmsService.decryptWithKmsKey({
|
||||
kmsId: keyId
|
||||
});
|
||||
|
||||
const caCerts = await certificateAuthorityCertDAL.find({ caId: ca.id }, { sort: [["version", "asc"]] });
|
||||
|
||||
const decryptedChains = await Promise.all(
|
||||
caCerts.map(async (caCert) => {
|
||||
const decryptedCaCert = await kmsDecryptor({
|
||||
cipherTextBlob: caCert.encryptedCertificate
|
||||
});
|
||||
const caCertObj = new x509.X509Certificate(decryptedCaCert);
|
||||
const decryptedChain = await kmsDecryptor({
|
||||
cipherTextBlob: caCert.encryptedCertificateChain
|
||||
});
|
||||
return {
|
||||
certificate: caCertObj.toString("pem"),
|
||||
certificateChain: decryptedChain.toString("utf-8"),
|
||||
serialNumber: caCertObj.serialNumber,
|
||||
version: caCert.version
|
||||
};
|
||||
})
|
||||
);
|
||||
|
||||
return decryptedChains;
|
||||
};
|
||||
|
||||
/**
|
||||
* Return the decrypted pem-encoded certificate and certificate chain
|
||||
* corresponding to CA certificate with id [caCertId].
|
||||
*/
|
||||
export const getCaCertChain = async ({
|
||||
caCertId,
|
||||
certificateAuthorityDAL,
|
||||
certificateAuthorityCertDAL,
|
||||
projectDAL,
|
||||
kmsService
|
||||
}: TGetCaCertChainDTO) => {
|
||||
const caCert = await certificateAuthorityCertDAL.findById(caCertId);
|
||||
if (!caCert) throw new NotFoundError({ message: "CA certificate not found" });
|
||||
const ca = await certificateAuthorityDAL.findById(caCert.caId);
|
||||
|
||||
const keyId = await getProjectKmsCertificateKeyId({
|
||||
projectId: ca.projectId,
|
||||
@ -194,7 +249,7 @@ export const rebuildCaCrl = async ({
|
||||
kmsService
|
||||
}: TRebuildCaCrlDTO) => {
|
||||
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 });
|
||||
|
||||
|
@ -5,22 +5,28 @@ import crypto, { KeyObject } from "crypto";
|
||||
import ms from "ms";
|
||||
import { z } from "zod";
|
||||
|
||||
import { TCertificateAuthorities, TCertificateTemplates } from "@app/db/schemas";
|
||||
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service";
|
||||
import { ProjectPermissionActions, ProjectPermissionSub } from "@app/ee/services/permission/project-permission";
|
||||
import { BadRequestError } from "@app/lib/errors";
|
||||
import { BadRequestError, NotFoundError } from "@app/lib/errors";
|
||||
import { TCertificateBodyDALFactory } from "@app/services/certificate/certificate-body-dal";
|
||||
import { TCertificateDALFactory } from "@app/services/certificate/certificate-dal";
|
||||
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 { getProjectKmsCertificateKeyId } from "@app/services/project/project-fns";
|
||||
|
||||
import { TCertificateAuthorityCrlDALFactory } from "../../ee/services/certificate-authority-crl/certificate-authority-crl-dal";
|
||||
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 { TCertificateAuthorityDALFactory } from "./certificate-authority-dal";
|
||||
import {
|
||||
createDistinguishedName,
|
||||
getCaCertChain,
|
||||
getCaCertChain, // TODO: consider rename
|
||||
getCaCertChains,
|
||||
getCaCredentials,
|
||||
keyAlgorithmToAlgCfg,
|
||||
parseDistinguishedName
|
||||
@ -33,10 +39,12 @@ import {
|
||||
TCreateCaDTO,
|
||||
TDeleteCaDTO,
|
||||
TGetCaCertDTO,
|
||||
TGetCaCertsDTO,
|
||||
TGetCaCsrDTO,
|
||||
TGetCaDTO,
|
||||
TImportCertToCaDTO,
|
||||
TIssueCertFromCaDTO,
|
||||
TRenewCaCertDTO,
|
||||
TSignCertFromCaDTO,
|
||||
TSignIntermediateDTO,
|
||||
TUpdateCaDTO
|
||||
@ -48,12 +56,18 @@ type TCertificateAuthorityServiceFactoryDep = {
|
||||
TCertificateAuthorityDALFactory,
|
||||
"transaction" | "create" | "findById" | "updateById" | "deleteById" | "findOne"
|
||||
>;
|
||||
certificateAuthorityCertDAL: Pick<TCertificateAuthorityCertDALFactory, "create" | "findOne" | "transaction">;
|
||||
certificateAuthorityCertDAL: Pick<
|
||||
TCertificateAuthorityCertDALFactory,
|
||||
"create" | "findOne" | "transaction" | "find" | "findById"
|
||||
>;
|
||||
certificateAuthoritySecretDAL: Pick<TCertificateAuthoritySecretDALFactory, "create" | "findOne">;
|
||||
certificateAuthorityCrlDAL: Pick<TCertificateAuthorityCrlDALFactory, "create" | "findOne" | "update">;
|
||||
certificateTemplateDAL: Pick<TCertificateTemplateDALFactory, "getById">;
|
||||
certificateAuthorityQueue: TCertificateAuthorityQueueFactory; // TODO: Pick
|
||||
certificateDAL: Pick<TCertificateDALFactory, "transaction" | "create" | "find">;
|
||||
certificateBodyDAL: Pick<TCertificateBodyDALFactory, "create">;
|
||||
pkiCollectionDAL: Pick<TPkiCollectionDALFactory, "findById">;
|
||||
pkiCollectionItemDAL: Pick<TPkiCollectionItemDALFactory, "create">;
|
||||
projectDAL: Pick<TProjectDALFactory, "findProjectBySlug" | "findOne" | "updateById" | "findById" | "transaction">;
|
||||
kmsService: Pick<TKmsServiceFactory, "generateKmsKey" | "encryptWithKmsKey" | "decryptWithKmsKey">;
|
||||
permissionService: Pick<TPermissionServiceFactory, "getProjectPermission">;
|
||||
@ -66,8 +80,11 @@ export const certificateAuthorityServiceFactory = ({
|
||||
certificateAuthorityCertDAL,
|
||||
certificateAuthoritySecretDAL,
|
||||
certificateAuthorityCrlDAL,
|
||||
certificateTemplateDAL,
|
||||
certificateDAL,
|
||||
certificateBodyDAL,
|
||||
pkiCollectionDAL,
|
||||
pkiCollectionItemDAL,
|
||||
projectDAL,
|
||||
kmsService,
|
||||
permissionService
|
||||
@ -165,6 +182,24 @@ export const certificateAuthorityServiceFactory = ({
|
||||
kmsId: certificateManagerKmsId
|
||||
});
|
||||
|
||||
// https://nodejs.org/api/crypto.html#static-method-keyobjectfromkey
|
||||
const skObj = KeyObject.from(keys.privateKey);
|
||||
|
||||
const { cipherTextBlob: encryptedPrivateKey } = await kmsEncryptor({
|
||||
plainText: skObj.export({
|
||||
type: "pkcs8",
|
||||
format: "der"
|
||||
})
|
||||
});
|
||||
|
||||
const caSecret = await certificateAuthoritySecretDAL.create(
|
||||
{
|
||||
caId: ca.id,
|
||||
encryptedPrivateKey
|
||||
},
|
||||
tx
|
||||
);
|
||||
|
||||
if (type === CaType.ROOT) {
|
||||
// note: create self-signed cert only applicable for root CA
|
||||
const cert = await x509.X509CertificateGenerator.createSelfSigned({
|
||||
@ -191,11 +226,21 @@ export const certificateAuthorityServiceFactory = ({
|
||||
plainText: Buffer.alloc(0)
|
||||
});
|
||||
|
||||
await certificateAuthorityCertDAL.create(
|
||||
const caCert = await certificateAuthorityCertDAL.create(
|
||||
{
|
||||
caId: ca.id,
|
||||
encryptedCertificate,
|
||||
encryptedCertificateChain
|
||||
encryptedCertificateChain,
|
||||
version: 1,
|
||||
caSecretId: caSecret.id
|
||||
},
|
||||
tx
|
||||
);
|
||||
|
||||
await certificateAuthorityDAL.updateById(
|
||||
ca.id,
|
||||
{
|
||||
activeCaCertId: caCert.id
|
||||
},
|
||||
tx
|
||||
);
|
||||
@ -223,24 +268,6 @@ export const certificateAuthorityServiceFactory = ({
|
||||
tx
|
||||
);
|
||||
|
||||
// https://nodejs.org/api/crypto.html#static-method-keyobjectfromkey
|
||||
const skObj = KeyObject.from(keys.privateKey);
|
||||
|
||||
const { cipherTextBlob: encryptedPrivateKey } = await kmsEncryptor({
|
||||
plainText: skObj.export({
|
||||
type: "pkcs8",
|
||||
format: "der"
|
||||
})
|
||||
});
|
||||
|
||||
await certificateAuthoritySecretDAL.create(
|
||||
{
|
||||
caId: ca.id,
|
||||
encryptedPrivateKey
|
||||
},
|
||||
tx
|
||||
);
|
||||
|
||||
return ca;
|
||||
});
|
||||
|
||||
@ -342,9 +369,6 @@ export const certificateAuthorityServiceFactory = ({
|
||||
|
||||
if (ca.type === CaType.ROOT) throw new BadRequestError({ message: "Root CA cannot generate CSR" });
|
||||
|
||||
const caCert = await certificateAuthorityCertDAL.findOne({ caId: ca.id });
|
||||
if (caCert) throw new BadRequestError({ message: "CA already has a certificate installed" });
|
||||
|
||||
const { caPrivateKey, caPublicKey } = await getCaCredentials({
|
||||
caId,
|
||||
certificateAuthorityDAL,
|
||||
@ -381,9 +405,284 @@ export const certificateAuthorityServiceFactory = ({
|
||||
};
|
||||
|
||||
/**
|
||||
* Return certificate and certificate chain for CA
|
||||
* Renew certificate for CA with id [caId]
|
||||
* Note 1: This CA renewal method is only applicable to CAs with internal parent CAs
|
||||
* Note 2: Currently implements CA renewal with same key-pair only
|
||||
*/
|
||||
const getCaCert = async ({ caId, actorId, actorAuthMethod, actor, actorOrgId }: TGetCaCertDTO) => {
|
||||
const renewCaCert = async ({ caId, notAfter, actorId, actorAuthMethod, actor, actorOrgId }: TRenewCaCertDTO) => {
|
||||
const ca = await certificateAuthorityDAL.findById(caId);
|
||||
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(
|
||||
actor,
|
||||
actorId,
|
||||
ca.projectId,
|
||||
actorAuthMethod,
|
||||
actorOrgId
|
||||
);
|
||||
|
||||
ForbiddenError.from(permission).throwUnlessCan(
|
||||
ProjectPermissionActions.Create,
|
||||
ProjectPermissionSub.CertificateAuthorities
|
||||
);
|
||||
|
||||
if (ca.status === CaStatus.DISABLED) throw new BadRequestError({ message: "CA is disabled" });
|
||||
|
||||
// get latest CA certificate
|
||||
const caCert = await certificateAuthorityCertDAL.findById(ca.activeCaCertId);
|
||||
|
||||
const serialNumber = crypto.randomBytes(32).toString("hex");
|
||||
|
||||
const certificateManagerKmsId = await getProjectKmsCertificateKeyId({
|
||||
projectId: ca.projectId,
|
||||
projectDAL,
|
||||
kmsService
|
||||
});
|
||||
|
||||
const kmsEncryptor = await kmsService.encryptWithKmsKey({
|
||||
kmsId: certificateManagerKmsId
|
||||
});
|
||||
|
||||
const { caPrivateKey, caPublicKey, caSecret } = await getCaCredentials({
|
||||
caId: ca.id,
|
||||
certificateAuthorityDAL,
|
||||
certificateAuthoritySecretDAL,
|
||||
projectDAL,
|
||||
kmsService
|
||||
});
|
||||
|
||||
const alg = keyAlgorithmToAlgCfg(ca.keyAlgorithm as CertKeyAlgorithm);
|
||||
|
||||
const kmsDecryptor = await kmsService.decryptWithKmsKey({
|
||||
kmsId: certificateManagerKmsId
|
||||
});
|
||||
const decryptedCaCert = await kmsDecryptor({
|
||||
cipherTextBlob: caCert.encryptedCertificate
|
||||
});
|
||||
|
||||
const caCertObj = new x509.X509Certificate(decryptedCaCert);
|
||||
|
||||
let certificate = "";
|
||||
let certificateChain = "";
|
||||
|
||||
switch (ca.type) {
|
||||
case CaType.ROOT: {
|
||||
if (new Date(notAfter) <= new Date(caCertObj.notAfter)) {
|
||||
throw new BadRequestError({
|
||||
message:
|
||||
"New Root CA certificate must have notAfter date that is greater than the current certificate notAfter date"
|
||||
});
|
||||
}
|
||||
|
||||
const notBeforeDate = new Date();
|
||||
const cert = await x509.X509CertificateGenerator.createSelfSigned({
|
||||
name: ca.dn,
|
||||
serialNumber,
|
||||
notBefore: notBeforeDate,
|
||||
notAfter: new Date(notAfter),
|
||||
signingAlgorithm: alg,
|
||||
keys: {
|
||||
privateKey: caPrivateKey,
|
||||
publicKey: caPublicKey
|
||||
},
|
||||
extensions: [
|
||||
new x509.BasicConstraintsExtension(
|
||||
true,
|
||||
ca.maxPathLength === -1 || !ca.maxPathLength ? undefined : ca.maxPathLength,
|
||||
true
|
||||
),
|
||||
new x509.ExtendedKeyUsageExtension(["1.2.3.4.5.6.7", "2.3.4.5.6.7.8"], true),
|
||||
// eslint-disable-next-line no-bitwise
|
||||
new x509.KeyUsagesExtension(x509.KeyUsageFlags.keyCertSign | x509.KeyUsageFlags.cRLSign, true),
|
||||
await x509.SubjectKeyIdentifierExtension.create(caPublicKey)
|
||||
]
|
||||
});
|
||||
|
||||
const { cipherTextBlob: encryptedCertificate } = await kmsEncryptor({
|
||||
plainText: Buffer.from(new Uint8Array(cert.rawData))
|
||||
});
|
||||
|
||||
const { cipherTextBlob: encryptedCertificateChain } = await kmsEncryptor({
|
||||
plainText: Buffer.alloc(0)
|
||||
});
|
||||
|
||||
await certificateAuthorityDAL.transaction(async (tx) => {
|
||||
const newCaCert = await certificateAuthorityCertDAL.create(
|
||||
{
|
||||
caId: ca.id,
|
||||
encryptedCertificate,
|
||||
encryptedCertificateChain,
|
||||
version: caCert.version + 1,
|
||||
caSecretId: caSecret.id
|
||||
},
|
||||
tx
|
||||
);
|
||||
|
||||
await certificateAuthorityDAL.updateById(
|
||||
ca.id,
|
||||
{
|
||||
activeCaCertId: newCaCert.id,
|
||||
notBefore: notBeforeDate,
|
||||
notAfter: new Date(notAfter)
|
||||
},
|
||||
tx
|
||||
);
|
||||
});
|
||||
|
||||
certificate = cert.toString("pem");
|
||||
break;
|
||||
}
|
||||
case CaType.INTERMEDIATE: {
|
||||
if (!ca.parentCaId) {
|
||||
// TODO: look into optimal way to support renewal of intermediate CA with external parent CA
|
||||
throw new BadRequestError({
|
||||
message: "Failed to renew intermediate CA certificate with external parent CA"
|
||||
});
|
||||
}
|
||||
|
||||
const parentCa = await certificateAuthorityDAL.findById(ca.parentCaId);
|
||||
const { caPrivateKey: parentCaPrivateKey } = await getCaCredentials({
|
||||
caId: parentCa.id,
|
||||
certificateAuthorityDAL,
|
||||
certificateAuthoritySecretDAL,
|
||||
projectDAL,
|
||||
kmsService
|
||||
});
|
||||
|
||||
// get latest parent CA certificate
|
||||
if (!parentCa.activeCaCertId)
|
||||
throw new BadRequestError({ message: "Parent CA does not have a certificate installed" });
|
||||
const parentCaCert = await certificateAuthorityCertDAL.findById(parentCa.activeCaCertId);
|
||||
|
||||
const decryptedParentCaCert = await kmsDecryptor({
|
||||
cipherTextBlob: parentCaCert.encryptedCertificate
|
||||
});
|
||||
|
||||
const parentCaCertObj = new x509.X509Certificate(decryptedParentCaCert);
|
||||
|
||||
if (new Date(notAfter) <= new Date(caCertObj.notAfter)) {
|
||||
throw new BadRequestError({
|
||||
message:
|
||||
"New Intermediate CA certificate must have notAfter date that is greater than the current certificate notAfter date"
|
||||
});
|
||||
}
|
||||
|
||||
if (new Date(notAfter) > new Date(parentCaCertObj.notAfter)) {
|
||||
throw new BadRequestError({
|
||||
message:
|
||||
"New Intermediate CA certificate must have notAfter date that is equal to or smaller than the notAfter date of the parent CA certificate current certificate notAfter date"
|
||||
});
|
||||
}
|
||||
|
||||
const csrObj = await x509.Pkcs10CertificateRequestGenerator.create({
|
||||
name: ca.dn,
|
||||
keys: {
|
||||
privateKey: caPrivateKey,
|
||||
publicKey: caPublicKey
|
||||
},
|
||||
signingAlgorithm: alg,
|
||||
extensions: [
|
||||
// eslint-disable-next-line no-bitwise
|
||||
new x509.KeyUsagesExtension(
|
||||
x509.KeyUsageFlags.keyCertSign |
|
||||
x509.KeyUsageFlags.cRLSign |
|
||||
x509.KeyUsageFlags.digitalSignature |
|
||||
x509.KeyUsageFlags.keyEncipherment
|
||||
)
|
||||
],
|
||||
attributes: [new x509.ChallengePasswordAttribute("password")]
|
||||
});
|
||||
|
||||
const notBeforeDate = new Date();
|
||||
const intermediateCert = await x509.X509CertificateGenerator.create({
|
||||
serialNumber,
|
||||
subject: csrObj.subject,
|
||||
issuer: parentCaCertObj.subject,
|
||||
notBefore: notBeforeDate,
|
||||
notAfter: new Date(notAfter),
|
||||
signingKey: parentCaPrivateKey,
|
||||
publicKey: csrObj.publicKey,
|
||||
signingAlgorithm: alg,
|
||||
extensions: [
|
||||
new x509.KeyUsagesExtension(
|
||||
x509.KeyUsageFlags.keyCertSign |
|
||||
x509.KeyUsageFlags.cRLSign |
|
||||
x509.KeyUsageFlags.digitalSignature |
|
||||
x509.KeyUsageFlags.keyEncipherment,
|
||||
true
|
||||
),
|
||||
new x509.BasicConstraintsExtension(
|
||||
true,
|
||||
ca.maxPathLength === -1 || !ca.maxPathLength ? undefined : ca.maxPathLength,
|
||||
true
|
||||
),
|
||||
await x509.AuthorityKeyIdentifierExtension.create(parentCaCertObj, false),
|
||||
await x509.SubjectKeyIdentifierExtension.create(csrObj.publicKey)
|
||||
]
|
||||
});
|
||||
|
||||
const { cipherTextBlob: encryptedCertificate } = await kmsEncryptor({
|
||||
plainText: Buffer.from(new Uint8Array(intermediateCert.rawData))
|
||||
});
|
||||
|
||||
const { caCert: parentCaCertificate, caCertChain: parentCaCertChain } = await getCaCertChain({
|
||||
caCertId: parentCa.activeCaCertId,
|
||||
certificateAuthorityDAL,
|
||||
certificateAuthorityCertDAL,
|
||||
projectDAL,
|
||||
kmsService
|
||||
});
|
||||
|
||||
certificateChain = `${parentCaCertificate}\n${parentCaCertChain}`.trim();
|
||||
|
||||
const { cipherTextBlob: encryptedCertificateChain } = await kmsEncryptor({
|
||||
plainText: Buffer.from(certificateChain)
|
||||
});
|
||||
|
||||
await certificateAuthorityDAL.transaction(async (tx) => {
|
||||
const newCaCert = await certificateAuthorityCertDAL.create(
|
||||
{
|
||||
caId: ca.id,
|
||||
encryptedCertificate,
|
||||
encryptedCertificateChain,
|
||||
version: caCert.version + 1,
|
||||
caSecretId: caSecret.id
|
||||
},
|
||||
tx
|
||||
);
|
||||
|
||||
await certificateAuthorityDAL.updateById(
|
||||
ca.id,
|
||||
{
|
||||
activeCaCertId: newCaCert.id,
|
||||
notBefore: notBeforeDate,
|
||||
notAfter: new Date(notAfter)
|
||||
},
|
||||
tx
|
||||
);
|
||||
});
|
||||
|
||||
certificate = intermediateCert.toString("pem");
|
||||
break;
|
||||
}
|
||||
default: {
|
||||
throw new BadRequestError({
|
||||
message: "Unrecognized CA type"
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
certificate,
|
||||
certificateChain,
|
||||
serialNumber,
|
||||
ca
|
||||
};
|
||||
};
|
||||
|
||||
const getCaCerts = async ({ caId, actorId, actorAuthMethod, actor, actorOrgId }: TGetCaCertsDTO) => {
|
||||
const ca = await certificateAuthorityDAL.findById(caId);
|
||||
if (!ca) throw new BadRequestError({ message: "CA not found" });
|
||||
|
||||
@ -400,7 +699,7 @@ export const certificateAuthorityServiceFactory = ({
|
||||
ProjectPermissionSub.CertificateAuthorities
|
||||
);
|
||||
|
||||
const { caCert, caCertChain, serialNumber } = await getCaCertChain({
|
||||
const caCertChains = await getCaCertChains({
|
||||
caId,
|
||||
certificateAuthorityDAL,
|
||||
certificateAuthorityCertDAL,
|
||||
@ -408,6 +707,41 @@ export const certificateAuthorityServiceFactory = ({
|
||||
kmsService
|
||||
});
|
||||
|
||||
return {
|
||||
ca,
|
||||
caCerts: caCertChains
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Return current certificate and certificate chain for CA
|
||||
*/
|
||||
const getCaCert = async ({ caId, actorId, actorAuthMethod, actor, actorOrgId }: TGetCaCertDTO) => {
|
||||
const ca = await certificateAuthorityDAL.findById(caId);
|
||||
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(
|
||||
actor,
|
||||
actorId,
|
||||
ca.projectId,
|
||||
actorAuthMethod,
|
||||
actorOrgId
|
||||
);
|
||||
|
||||
ForbiddenError.from(permission).throwUnlessCan(
|
||||
ProjectPermissionActions.Read,
|
||||
ProjectPermissionSub.CertificateAuthorities
|
||||
);
|
||||
|
||||
const { caCert, caCertChain, serialNumber } = await getCaCertChain({
|
||||
caCertId: ca.activeCaCertId,
|
||||
certificateAuthorityDAL,
|
||||
certificateAuthorityCertDAL,
|
||||
projectDAL,
|
||||
kmsService
|
||||
});
|
||||
|
||||
return {
|
||||
certificate: caCert,
|
||||
certificateChain: caCertChain,
|
||||
@ -447,6 +781,13 @@ export const certificateAuthorityServiceFactory = ({
|
||||
);
|
||||
|
||||
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.findById(ca.activeCaCertId);
|
||||
|
||||
if (ca.notAfter && new Date() > new Date(ca.notAfter)) {
|
||||
throw new BadRequestError({ message: "CA is expired" });
|
||||
}
|
||||
|
||||
const alg = keyAlgorithmToAlgCfg(ca.keyAlgorithm as CertKeyAlgorithm);
|
||||
|
||||
@ -459,7 +800,6 @@ export const certificateAuthorityServiceFactory = ({
|
||||
kmsId: certificateManagerKmsId
|
||||
});
|
||||
|
||||
const caCert = await certificateAuthorityCertDAL.findOne({ caId: ca.id });
|
||||
const decryptedCaCert = await kmsDecryptor({
|
||||
cipherTextBlob: caCert.encryptedCertificate
|
||||
});
|
||||
@ -531,7 +871,7 @@ export const certificateAuthorityServiceFactory = ({
|
||||
});
|
||||
|
||||
const { caCert: issuingCaCertificate, caCertChain } = await getCaCertChain({
|
||||
caId,
|
||||
caCertId: ca.activeCaCertId,
|
||||
certificateAuthorityDAL,
|
||||
certificateAuthorityCertDAL,
|
||||
projectDAL,
|
||||
@ -548,9 +888,9 @@ export const certificateAuthorityServiceFactory = ({
|
||||
};
|
||||
|
||||
/**
|
||||
* Import certificate for (un-installed) CA with id [caId].
|
||||
* Import certificate for CA with id [caId].
|
||||
* Note: Can be used to import an external certificate and certificate chain
|
||||
* to be installed into the CA.
|
||||
* to be into an installed or uninstalled CA.
|
||||
*/
|
||||
const importCertToCa = async ({
|
||||
caId,
|
||||
@ -577,8 +917,18 @@ export const certificateAuthorityServiceFactory = ({
|
||||
ProjectPermissionSub.CertificateAuthorities
|
||||
);
|
||||
|
||||
const caCert = await certificateAuthorityCertDAL.findOne({ caId: ca.id });
|
||||
if (caCert) throw new BadRequestError({ message: "CA has already imported a certificate" });
|
||||
if (ca.parentCaId) {
|
||||
/**
|
||||
* re-evaluate in the future if we should allow users to import a new CA certificate for an intermediate
|
||||
* CA chained to an internal parent CA. Doing so would allow users to re-chain the CA to a different
|
||||
* internal CA.
|
||||
*/
|
||||
throw new BadRequestError({
|
||||
message: "Cannot import certificate to intermediate CA chained to internal parent CA"
|
||||
});
|
||||
}
|
||||
|
||||
const caCert = ca.activeCaCertId ? await certificateAuthorityCertDAL.findById(ca.activeCaCertId) : undefined;
|
||||
|
||||
const certObj = new x509.X509Certificate(certificate);
|
||||
const maxPathLength = certObj.getExtension(x509.BasicConstraintsExtension)?.pathLength;
|
||||
@ -625,12 +975,32 @@ export const certificateAuthorityServiceFactory = ({
|
||||
plainText: Buffer.from(certificateChain)
|
||||
});
|
||||
|
||||
// TODO: validate that latest key-pair of CA is used to sign the certificate
|
||||
// once renewal with new key pair is supported
|
||||
const { caSecret, caPublicKey } = await getCaCredentials({
|
||||
caId: ca.id,
|
||||
certificateAuthorityDAL,
|
||||
certificateAuthoritySecretDAL,
|
||||
projectDAL,
|
||||
kmsService
|
||||
});
|
||||
|
||||
const isCaAndCertPublicKeySame = Buffer.from(await crypto.subtle.exportKey("spki", caPublicKey)).equals(
|
||||
Buffer.from(certObj.publicKey.rawData)
|
||||
);
|
||||
|
||||
if (!isCaAndCertPublicKeySame) {
|
||||
throw new BadRequestError({ message: "CA and certificate public key do not match" });
|
||||
}
|
||||
|
||||
await certificateAuthorityCertDAL.transaction(async (tx) => {
|
||||
await certificateAuthorityCertDAL.create(
|
||||
const newCaCert = await certificateAuthorityCertDAL.create(
|
||||
{
|
||||
caId: ca.id,
|
||||
encryptedCertificate,
|
||||
encryptedCertificateChain
|
||||
encryptedCertificateChain,
|
||||
version: caCert ? caCert.version + 1 : 1,
|
||||
caSecretId: caSecret.id
|
||||
},
|
||||
tx
|
||||
);
|
||||
@ -643,7 +1013,8 @@ export const certificateAuthorityServiceFactory = ({
|
||||
notBefore: new Date(certObj.notBefore),
|
||||
notAfter: new Date(certObj.notAfter),
|
||||
serialNumber: certObj.serialNumber,
|
||||
parentCaId: parentCa?.id
|
||||
parentCaId: parentCa?.id,
|
||||
activeCaCertId: newCaCert.id
|
||||
},
|
||||
tx
|
||||
);
|
||||
@ -658,6 +1029,8 @@ export const certificateAuthorityServiceFactory = ({
|
||||
*/
|
||||
const issueCertFromCa = async ({
|
||||
caId,
|
||||
certificateTemplateId,
|
||||
pkiCollectionId,
|
||||
friendlyName,
|
||||
commonName,
|
||||
altNames,
|
||||
@ -669,8 +1042,27 @@ export const certificateAuthorityServiceFactory = ({
|
||||
actor,
|
||||
actorOrgId
|
||||
}: TIssueCertFromCaDTO) => {
|
||||
const ca = await certificateAuthorityDAL.findById(caId);
|
||||
if (!ca) throw new BadRequestError({ message: "CA not found" });
|
||||
let ca: TCertificateAuthorities | undefined;
|
||||
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(
|
||||
actor,
|
||||
@ -683,9 +1075,19 @@ export const certificateAuthorityServiceFactory = ({
|
||||
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Create, ProjectPermissionSub.Certificates);
|
||||
|
||||
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.findById(ca.activeCaCertId);
|
||||
|
||||
const caCert = await certificateAuthorityCertDAL.findOne({ caId: ca.id });
|
||||
if (!caCert) throw new BadRequestError({ message: "CA does not have a certificate installed" });
|
||||
if (ca.notAfter && new Date() > new Date(ca.notAfter)) {
|
||||
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({
|
||||
projectId: ca.projectId,
|
||||
@ -755,11 +1157,13 @@ export const certificateAuthorityServiceFactory = ({
|
||||
await x509.SubjectKeyIdentifierExtension.create(csrObj.publicKey)
|
||||
];
|
||||
|
||||
let altNamesArray: {
|
||||
type: "email" | "dns";
|
||||
value: string;
|
||||
}[] = [];
|
||||
|
||||
if (altNames) {
|
||||
const altNamesArray: {
|
||||
type: "email" | "dns";
|
||||
value: string;
|
||||
}[] = altNames
|
||||
altNamesArray = altNames
|
||||
.split(",")
|
||||
.map((name) => name.trim())
|
||||
.map((altName) => {
|
||||
@ -787,6 +1191,18 @@ export const certificateAuthorityServiceFactory = ({
|
||||
extensions.push(altNamesExtension);
|
||||
}
|
||||
|
||||
if (certificateTemplate) {
|
||||
validateCertificateDetailsAgainstTemplate(
|
||||
{
|
||||
commonName,
|
||||
notBeforeDate,
|
||||
notAfterDate,
|
||||
altNames: altNamesArray.map((entry) => entry.value)
|
||||
},
|
||||
certificateTemplate
|
||||
);
|
||||
}
|
||||
|
||||
const serialNumber = crypto.randomBytes(32).toString("hex");
|
||||
const leafCert = await x509.X509CertificateGenerator.create({
|
||||
serialNumber,
|
||||
@ -813,7 +1229,9 @@ export const certificateAuthorityServiceFactory = ({
|
||||
await certificateDAL.transaction(async (tx) => {
|
||||
const cert = await certificateDAL.create(
|
||||
{
|
||||
caId: ca.id,
|
||||
caId: (ca as TCertificateAuthorities).id,
|
||||
caCertId: caCert.id,
|
||||
certificateTemplateId: certificateTemplate?.id,
|
||||
status: CertStatus.ACTIVE,
|
||||
friendlyName: friendlyName || commonName,
|
||||
commonName,
|
||||
@ -833,11 +1251,21 @@ export const certificateAuthorityServiceFactory = ({
|
||||
tx
|
||||
);
|
||||
|
||||
if (collectionId) {
|
||||
await pkiCollectionItemDAL.create(
|
||||
{
|
||||
pkiCollectionId: collectionId,
|
||||
certId: cert.id
|
||||
},
|
||||
tx
|
||||
);
|
||||
}
|
||||
|
||||
return cert;
|
||||
});
|
||||
|
||||
const { caCert: issuingCaCertificate, caCertChain } = await getCaCertChain({
|
||||
caId: ca.id,
|
||||
caCertId: caCert.id,
|
||||
certificateAuthorityDAL,
|
||||
certificateAuthorityCertDAL,
|
||||
projectDAL,
|
||||
@ -860,7 +1288,9 @@ export const certificateAuthorityServiceFactory = ({
|
||||
*/
|
||||
const signCertFromCa = async ({
|
||||
caId,
|
||||
certificateTemplateId,
|
||||
csr,
|
||||
pkiCollectionId,
|
||||
friendlyName,
|
||||
commonName,
|
||||
altNames,
|
||||
@ -872,8 +1302,27 @@ export const certificateAuthorityServiceFactory = ({
|
||||
actor,
|
||||
actorOrgId
|
||||
}: TSignCertFromCaDTO) => {
|
||||
const ca = await certificateAuthorityDAL.findById(caId);
|
||||
if (!ca) throw new BadRequestError({ message: "CA not found" });
|
||||
let ca: TCertificateAuthorities | undefined;
|
||||
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(
|
||||
actor,
|
||||
@ -886,9 +1335,20 @@ export const certificateAuthorityServiceFactory = ({
|
||||
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Create, ProjectPermissionSub.Certificates);
|
||||
|
||||
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.findOne({ caId: ca.id });
|
||||
if (!caCert) throw new BadRequestError({ message: "CA does not have a certificate installed" });
|
||||
const caCert = await certificateAuthorityCertDAL.findById(ca.activeCaCertId);
|
||||
|
||||
if (ca.notAfter && new Date() > new Date(ca.notAfter)) {
|
||||
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({
|
||||
projectId: ca.projectId,
|
||||
@ -957,11 +1417,12 @@ export const certificateAuthorityServiceFactory = ({
|
||||
await x509.SubjectKeyIdentifierExtension.create(csrObj.publicKey)
|
||||
];
|
||||
|
||||
let altNamesArray: {
|
||||
type: "email" | "dns";
|
||||
value: string;
|
||||
}[] = [];
|
||||
if (altNames) {
|
||||
const altNamesArray: {
|
||||
type: "email" | "dns";
|
||||
value: string;
|
||||
}[] = altNames
|
||||
altNamesArray = altNames
|
||||
.split(",")
|
||||
.map((name) => name.trim())
|
||||
.map((altName) => {
|
||||
@ -989,6 +1450,18 @@ export const certificateAuthorityServiceFactory = ({
|
||||
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 leafCert = await x509.X509CertificateGenerator.create({
|
||||
serialNumber,
|
||||
@ -1012,7 +1485,9 @@ export const certificateAuthorityServiceFactory = ({
|
||||
await certificateDAL.transaction(async (tx) => {
|
||||
const cert = await certificateDAL.create(
|
||||
{
|
||||
caId: ca.id,
|
||||
caId: (ca as TCertificateAuthorities).id,
|
||||
caCertId: caCert.id,
|
||||
certificateTemplateId: certificateTemplate?.id,
|
||||
status: CertStatus.ACTIVE,
|
||||
friendlyName: friendlyName || csrObj.subject,
|
||||
commonName: cn,
|
||||
@ -1032,11 +1507,21 @@ export const certificateAuthorityServiceFactory = ({
|
||||
tx
|
||||
);
|
||||
|
||||
if (collectionId) {
|
||||
await pkiCollectionItemDAL.create(
|
||||
{
|
||||
pkiCollectionId: collectionId,
|
||||
certId: cert.id
|
||||
},
|
||||
tx
|
||||
);
|
||||
}
|
||||
|
||||
return cert;
|
||||
});
|
||||
|
||||
const { caCert: issuingCaCertificate, caCertChain } = await getCaCertChain({
|
||||
caId: ca.id,
|
||||
caCertId: ca.activeCaCertId,
|
||||
certificateAuthorityDAL,
|
||||
certificateAuthorityCertDAL,
|
||||
projectDAL,
|
||||
@ -1058,6 +1543,8 @@ export const certificateAuthorityServiceFactory = ({
|
||||
updateCaById,
|
||||
deleteCaById,
|
||||
getCaCsr,
|
||||
renewCaCert,
|
||||
getCaCerts,
|
||||
getCaCert,
|
||||
signIntermediate,
|
||||
importCertToCa,
|
||||
|
@ -20,6 +20,10 @@ export enum CaStatus {
|
||||
PENDING_CERTIFICATE = "pending-certificate"
|
||||
}
|
||||
|
||||
export enum CaRenewalType {
|
||||
EXISTING = "existing"
|
||||
}
|
||||
|
||||
export type TCreateCaDTO = {
|
||||
projectSlug: string;
|
||||
type: CaType;
|
||||
@ -53,6 +57,16 @@ export type TGetCaCsrDTO = {
|
||||
caId: string;
|
||||
} & Omit<TProjectPermission, "projectId">;
|
||||
|
||||
export type TRenewCaCertDTO = {
|
||||
caId: string;
|
||||
notAfter: string;
|
||||
type: CaRenewalType;
|
||||
} & Omit<TProjectPermission, "projectId">;
|
||||
|
||||
export type TGetCaCertsDTO = {
|
||||
caId: string;
|
||||
} & Omit<TProjectPermission, "projectId">;
|
||||
|
||||
export type TGetCaCertDTO = {
|
||||
caId: string;
|
||||
} & Omit<TProjectPermission, "projectId">;
|
||||
@ -72,7 +86,9 @@ export type TImportCertToCaDTO = {
|
||||
} & Omit<TProjectPermission, "projectId">;
|
||||
|
||||
export type TIssueCertFromCaDTO = {
|
||||
caId: string;
|
||||
caId?: string;
|
||||
certificateTemplateId?: string;
|
||||
pkiCollectionId?: string;
|
||||
friendlyName?: string;
|
||||
commonName: string;
|
||||
altNames: string;
|
||||
@ -82,8 +98,10 @@ export type TIssueCertFromCaDTO = {
|
||||
} & Omit<TProjectPermission, "projectId">;
|
||||
|
||||
export type TSignCertFromCaDTO = {
|
||||
caId: string;
|
||||
caId?: string;
|
||||
csr: string;
|
||||
certificateTemplateId?: string;
|
||||
pkiCollectionId?: string;
|
||||
friendlyName?: string;
|
||||
commonName?: string;
|
||||
altNames: string;
|
||||
@ -109,10 +127,18 @@ export type TGetCaCredentialsDTO = {
|
||||
kmsService: Pick<TKmsServiceFactory, "decryptWithKmsKey" | "generateKmsKey">;
|
||||
};
|
||||
|
||||
export type TGetCaCertChainDTO = {
|
||||
export type TGetCaCertChainsDTO = {
|
||||
caId: string;
|
||||
certificateAuthorityDAL: Pick<TCertificateAuthorityDALFactory, "findById">;
|
||||
certificateAuthorityCertDAL: Pick<TCertificateAuthorityCertDALFactory, "findOne">;
|
||||
certificateAuthorityCertDAL: Pick<TCertificateAuthorityCertDALFactory, "find">;
|
||||
projectDAL: Pick<TProjectDALFactory, "findOne" | "updateById" | "transaction">;
|
||||
kmsService: Pick<TKmsServiceFactory, "decryptWithKmsKey" | "generateKmsKey">;
|
||||
};
|
||||
|
||||
export type TGetCaCertChainDTO = {
|
||||
caCertId: string;
|
||||
certificateAuthorityDAL: Pick<TCertificateAuthorityDALFactory, "findById">;
|
||||
certificateAuthorityCertDAL: Pick<TCertificateAuthorityCertDALFactory, "findById">;
|
||||
projectDAL: Pick<TProjectDALFactory, "findOne" | "updateById" | "transaction">;
|
||||
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">;
|
||||
certificateBodyDAL: Pick<TCertificateBodyDALFactory, "findOne">;
|
||||
certificateAuthorityDAL: Pick<TCertificateAuthorityDALFactory, "findById">;
|
||||
certificateAuthorityCertDAL: Pick<TCertificateAuthorityCertDALFactory, "findOne">;
|
||||
certificateAuthorityCertDAL: Pick<TCertificateAuthorityCertDALFactory, "findById">;
|
||||
certificateAuthorityCrlDAL: Pick<TCertificateAuthorityCrlDALFactory, "update">;
|
||||
certificateAuthoritySecretDAL: Pick<TCertificateAuthoritySecretDALFactory, "findOne">;
|
||||
projectDAL: Pick<TProjectDALFactory, "findOne" | "updateById" | "findById" | "transaction">;
|
||||
@ -180,7 +180,7 @@ export const certificateServiceFactory = ({
|
||||
const certObj = new x509.X509Certificate(decryptedCert);
|
||||
|
||||
const { caCert, caCertChain } = await getCaCertChain({
|
||||
caId: ca.id,
|
||||
caCertId: cert.caCertId,
|
||||
certificateAuthorityDAL,
|
||||
certificateAuthorityCertDAL,
|
||||
projectDAL,
|
||||
|
@ -4,6 +4,7 @@ import { TDbClient } from "@app/db";
|
||||
import { TableName } from "@app/db/schemas";
|
||||
import { DatabaseError } from "@app/lib/errors";
|
||||
import { ormify } from "@app/lib/knex";
|
||||
import { logger } from "@app/lib/logger";
|
||||
|
||||
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,
|
||||
secrets,
|
||||
accessId,
|
||||
accessToken
|
||||
accessToken,
|
||||
projectId
|
||||
}: {
|
||||
integration: TIntegrations;
|
||||
integration: TIntegrations & { secretPath: string; environment: { slug: string } };
|
||||
secrets: Record<string, { value: string; comment?: string }>;
|
||||
accessId: string | null;
|
||||
accessToken: string;
|
||||
projectId?: string;
|
||||
}) => {
|
||||
let response: { isSynced: boolean; syncMessage: string } | null = null;
|
||||
|
||||
if (!accessId) {
|
||||
throw new Error("AWS access ID is required");
|
||||
}
|
||||
|
||||
const config = new AWS.Config({
|
||||
region: integration.region as string,
|
||||
credentials: {
|
||||
@ -567,7 +568,9 @@ const syncSecretsAWSParameterStore = async ({
|
||||
|
||||
const metadata = z.record(z.any()).parse(integration.metadata || {});
|
||||
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
|
||||
let hasNext = true;
|
||||
let nextToken: string | undefined;
|
||||
@ -594,6 +597,18 @@ const syncSecretsAWSParameterStore = async ({
|
||||
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
|
||||
// don't use Promise.all() and promise map here
|
||||
// it will cause rate limit
|
||||
@ -603,24 +618,56 @@ const syncSecretsAWSParameterStore = async ({
|
||||
// case: secret does not exist in AWS parameter store
|
||||
// -> create secret
|
||||
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
|
||||
.putParameter({
|
||||
Name: `${integration.path}${key}`,
|
||||
Type: "SecureString",
|
||||
Value: secrets[key].value,
|
||||
...(metadata.kmsKeyId && { KeyId: metadata.kmsKeyId }),
|
||||
// Overwrite: true,
|
||||
Tags: metadata.secretAWSTag
|
||||
? metadata.secretAWSTag.map((tag: { key: string; value: string }) => ({
|
||||
Key: tag.key,
|
||||
Value: tag.value
|
||||
}))
|
||||
: []
|
||||
Overwrite: true
|
||||
})
|
||||
.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
|
||||
} else {
|
||||
logger.info(
|
||||
`getIntegrationSecrets: update secret in AWS SSM for [projectId=${projectId}] [environment=${integration.environment.slug}] [secretPath=${integration.secretPath}] [key=${key}]`
|
||||
);
|
||||
// -> update secret
|
||||
if (awsParameterStoreSecretsObj[key].Value !== secrets[key].value) {
|
||||
await ssm
|
||||
@ -648,6 +695,10 @@ const syncSecretsAWSParameterStore = async ({
|
||||
})
|
||||
.promise();
|
||||
} 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
|
||||
if ((err as any).code === "AccessDeniedException") {
|
||||
logger.error(
|
||||
@ -670,9 +721,18 @@ const syncSecretsAWSParameterStore = async ({
|
||||
}
|
||||
|
||||
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) {
|
||||
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)) {
|
||||
logger.info(
|
||||
`getIntegrationSecrets: inside of shouldDisableDelete AWS SSM [projectId=${projectId}] [environment=${integration.environment.slug}] [secretPath=${integration.secretPath}] [key=${key}] [step=3]`
|
||||
);
|
||||
// case:
|
||||
// -> delete secret
|
||||
await ssm
|
||||
@ -680,6 +740,9 @@ const syncSecretsAWSParameterStore = async ({
|
||||
Name: awsParameterStoreSecretsObj[key].Name as string
|
||||
})
|
||||
.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) => {
|
||||
setTimeout(resolve, 50);
|
||||
@ -3656,7 +3719,8 @@ export const syncIntegrationSecrets = async ({
|
||||
integration,
|
||||
secrets,
|
||||
accessId,
|
||||
accessToken
|
||||
accessToken,
|
||||
projectId
|
||||
});
|
||||
break;
|
||||
case Integrations.AWS_SECRET_MANAGER:
|
||||
|
@ -6,8 +6,15 @@ import { BadRequestError } from "@app/lib/errors";
|
||||
import { TProjectPermission } from "@app/lib/types";
|
||||
|
||||
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 { 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 {
|
||||
TCreateIntegrationDTO,
|
||||
@ -19,9 +26,15 @@ import {
|
||||
type TIntegrationServiceFactoryDep = {
|
||||
integrationDAL: TIntegrationDALFactory;
|
||||
integrationAuthDAL: TIntegrationAuthDALFactory;
|
||||
folderDAL: Pick<TSecretFolderDALFactory, "findBySecretPath">;
|
||||
integrationAuthService: TIntegrationAuthServiceFactory;
|
||||
folderDAL: Pick<TSecretFolderDALFactory, "findBySecretPath" | "findByManySecretPath">;
|
||||
permissionService: Pick<TPermissionServiceFactory, "getProjectPermission">;
|
||||
projectBotService: TProjectBotServiceFactory;
|
||||
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>;
|
||||
@ -31,7 +44,13 @@ export const integrationServiceFactory = ({
|
||||
integrationAuthDAL,
|
||||
folderDAL,
|
||||
permissionService,
|
||||
secretQueueService
|
||||
secretQueueService,
|
||||
integrationAuthService,
|
||||
projectBotService,
|
||||
secretV2BridgeDAL,
|
||||
secretImportDAL,
|
||||
kmsService,
|
||||
secretDAL
|
||||
}: TIntegrationServiceFactoryDep) => {
|
||||
const createIntegration = async ({
|
||||
app,
|
||||
@ -161,7 +180,14 @@ export const integrationServiceFactory = ({
|
||||
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);
|
||||
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);
|
||||
|
||||
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) => {
|
||||
// delete integration
|
||||
const deletedIntegrationResult = await integrationDAL.deleteById(id, tx);
|
||||
|
@ -63,6 +63,7 @@ export type TUpdateIntegrationDTO = {
|
||||
|
||||
export type TDeleteIntegrationDTO = {
|
||||
id: string;
|
||||
shouldDeleteIntegrationSecrets?: boolean;
|
||||
} & Omit<TProjectPermission, "projectId">;
|
||||
|
||||
export type TSyncIntegrationDTO = {
|
||||
|
5
backend/src/services/org-admin/org-admin-dal.ts
Normal file
5
backend/src/services/org-admin/org-admin-dal.ts
Normal file
@ -0,0 +1,5 @@
|
||||
export type TOrgAdminDALFactory = ReturnType<typeof orgAdminDALFactory>;
|
||||
|
||||
export const orgAdminDALFactory = () => {
|
||||
return {};
|
||||
};
|
191
backend/src/services/org-admin/org-admin-service.ts
Normal file
191
backend/src/services/org-admin/org-admin-service.ts
Normal file
@ -0,0 +1,191 @@
|
||||
import { ForbiddenError } from "@casl/ability";
|
||||
|
||||
import { ProjectMembershipRole, ProjectVersion, SecretKeyEncoding } from "@app/db/schemas";
|
||||
import { OrgPermissionAdminConsoleAction, OrgPermissionSubjects } from "@app/ee/services/permission/org-permission";
|
||||
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service";
|
||||
import { infisicalSymmetricDecrypt } from "@app/lib/crypto/encryption";
|
||||
import { BadRequestError } from "@app/lib/errors";
|
||||
|
||||
import { TProjectDALFactory } from "../project/project-dal";
|
||||
import { assignWorkspaceKeysToMembers } from "../project/project-fns";
|
||||
import { TProjectBotDALFactory } from "../project-bot/project-bot-dal";
|
||||
import { TProjectKeyDALFactory } from "../project-key/project-key-dal";
|
||||
import { TProjectMembershipDALFactory } from "../project-membership/project-membership-dal";
|
||||
import { TProjectUserMembershipRoleDALFactory } from "../project-membership/project-user-membership-role-dal";
|
||||
import { TUserDALFactory } from "../user/user-dal";
|
||||
import { TAccessProjectDTO, TListOrgProjectsDTO } from "./org-admin-types";
|
||||
|
||||
type TOrgAdminServiceFactoryDep = {
|
||||
permissionService: Pick<TPermissionServiceFactory, "getOrgPermission">;
|
||||
projectDAL: Pick<TProjectDALFactory, "find" | "findById" | "findProjectGhostUser">;
|
||||
projectMembershipDAL: Pick<TProjectMembershipDALFactory, "findOne" | "create" | "transaction" | "delete">;
|
||||
projectKeyDAL: Pick<TProjectKeyDALFactory, "findLatestProjectKey" | "create">;
|
||||
projectBotDAL: Pick<TProjectBotDALFactory, "findOne">;
|
||||
userDAL: Pick<TUserDALFactory, "findUserEncKeyByUserId">;
|
||||
projectUserMembershipRoleDAL: Pick<TProjectUserMembershipRoleDALFactory, "create" | "delete">;
|
||||
};
|
||||
|
||||
export type TOrgAdminServiceFactory = ReturnType<typeof orgAdminServiceFactory>;
|
||||
|
||||
export const orgAdminServiceFactory = ({
|
||||
permissionService,
|
||||
projectDAL,
|
||||
projectMembershipDAL,
|
||||
projectKeyDAL,
|
||||
projectBotDAL,
|
||||
userDAL,
|
||||
projectUserMembershipRoleDAL
|
||||
}: TOrgAdminServiceFactoryDep) => {
|
||||
const listOrgProjects = async ({
|
||||
actor,
|
||||
limit,
|
||||
actorId,
|
||||
offset,
|
||||
search,
|
||||
actorOrgId,
|
||||
actorAuthMethod
|
||||
}: TListOrgProjectsDTO) => {
|
||||
const { permission } = await permissionService.getOrgPermission(
|
||||
actor,
|
||||
actorId,
|
||||
actorOrgId,
|
||||
actorAuthMethod,
|
||||
actorOrgId
|
||||
);
|
||||
ForbiddenError.from(permission).throwUnlessCan(
|
||||
OrgPermissionAdminConsoleAction.AccessAllProjects,
|
||||
OrgPermissionSubjects.AdminConsole
|
||||
);
|
||||
const projects = await projectDAL.find(
|
||||
{
|
||||
orgId: actorOrgId,
|
||||
$search: {
|
||||
name: search ? `%${search}%` : undefined
|
||||
}
|
||||
},
|
||||
{ offset, limit, sort: [["name", "asc"]], count: true }
|
||||
);
|
||||
|
||||
const count = projects?.[0]?.count ? parseInt(projects?.[0]?.count, 10) : 0;
|
||||
return { projects, count };
|
||||
};
|
||||
|
||||
const grantProjectAdminAccess = async ({
|
||||
actor,
|
||||
actorId,
|
||||
actorOrgId,
|
||||
actorAuthMethod,
|
||||
projectId
|
||||
}: TAccessProjectDTO) => {
|
||||
const { permission, membership } = await permissionService.getOrgPermission(
|
||||
actor,
|
||||
actorId,
|
||||
actorOrgId,
|
||||
actorAuthMethod,
|
||||
actorOrgId
|
||||
);
|
||||
ForbiddenError.from(permission).throwUnlessCan(
|
||||
OrgPermissionAdminConsoleAction.AccessAllProjects,
|
||||
OrgPermissionSubjects.AdminConsole
|
||||
);
|
||||
|
||||
const project = await projectDAL.findById(projectId);
|
||||
if (!project) throw new BadRequestError({ message: "Project not found" });
|
||||
|
||||
if (project.version === ProjectVersion.V1) {
|
||||
throw new BadRequestError({ message: "Please upgrade your project on your dashboard" });
|
||||
}
|
||||
|
||||
// check already there exist a membership if there return it
|
||||
const projectMembership = await projectMembershipDAL.findOne({
|
||||
projectId,
|
||||
userId: actorId
|
||||
});
|
||||
if (projectMembership) {
|
||||
// reset and make the user admin
|
||||
await projectMembershipDAL.transaction(async (tx) => {
|
||||
await projectUserMembershipRoleDAL.delete({ projectMembershipId: projectMembership.id }, tx);
|
||||
await projectUserMembershipRoleDAL.create(
|
||||
{
|
||||
projectMembershipId: projectMembership.id,
|
||||
role: ProjectMembershipRole.Admin
|
||||
},
|
||||
tx
|
||||
);
|
||||
});
|
||||
return { isExistingMember: true, membership: projectMembership };
|
||||
}
|
||||
|
||||
// missing membership thus add admin back as admin to project
|
||||
const ghostUser = await projectDAL.findProjectGhostUser(projectId);
|
||||
if (!ghostUser) {
|
||||
throw new BadRequestError({
|
||||
message: "Failed to find sudo user"
|
||||
});
|
||||
}
|
||||
|
||||
const ghostUserLatestKey = await projectKeyDAL.findLatestProjectKey(ghostUser.id, projectId);
|
||||
if (!ghostUserLatestKey) {
|
||||
throw new BadRequestError({
|
||||
message: "Failed to find sudo user latest key"
|
||||
});
|
||||
}
|
||||
|
||||
const bot = await projectBotDAL.findOne({ projectId });
|
||||
if (!bot) {
|
||||
throw new BadRequestError({
|
||||
message: "Failed to find bot"
|
||||
});
|
||||
}
|
||||
|
||||
const botPrivateKey = infisicalSymmetricDecrypt({
|
||||
keyEncoding: bot.keyEncoding as SecretKeyEncoding,
|
||||
iv: bot.iv,
|
||||
tag: bot.tag,
|
||||
ciphertext: bot.encryptedPrivateKey
|
||||
});
|
||||
|
||||
const userEncryptionKey = await userDAL.findUserEncKeyByUserId(actorId);
|
||||
if (!userEncryptionKey) throw new BadRequestError({ message: "user encryption key not found" });
|
||||
const [newWsMember] = assignWorkspaceKeysToMembers({
|
||||
decryptKey: ghostUserLatestKey,
|
||||
userPrivateKey: botPrivateKey,
|
||||
members: [
|
||||
{
|
||||
orgMembershipId: membership.id,
|
||||
projectMembershipRole: ProjectMembershipRole.Admin,
|
||||
userPublicKey: userEncryptionKey.publicKey
|
||||
}
|
||||
]
|
||||
});
|
||||
|
||||
const updatedMembership = await projectMembershipDAL.transaction(async (tx) => {
|
||||
const newProjectMembership = await projectMembershipDAL.create(
|
||||
{
|
||||
projectId,
|
||||
userId: actorId
|
||||
},
|
||||
tx
|
||||
);
|
||||
await projectUserMembershipRoleDAL.create(
|
||||
{ projectMembershipId: newProjectMembership.id, role: ProjectMembershipRole.Admin },
|
||||
tx
|
||||
);
|
||||
|
||||
await projectKeyDAL.create(
|
||||
{
|
||||
encryptedKey: newWsMember.workspaceEncryptedKey,
|
||||
nonce: newWsMember.workspaceEncryptedNonce,
|
||||
senderId: ghostUser.id,
|
||||
receiverId: actorId,
|
||||
projectId
|
||||
},
|
||||
tx
|
||||
);
|
||||
return newProjectMembership;
|
||||
});
|
||||
return { isExistingMember: false, membership: updatedMembership };
|
||||
};
|
||||
|
||||
return { listOrgProjects, grantProjectAdminAccess };
|
||||
};
|
11
backend/src/services/org-admin/org-admin-types.ts
Normal file
11
backend/src/services/org-admin/org-admin-types.ts
Normal file
@ -0,0 +1,11 @@
|
||||
import { TOrgPermission } from "@app/lib/types";
|
||||
|
||||
export type TListOrgProjectsDTO = {
|
||||
limit?: number;
|
||||
offset?: number;
|
||||
search?: string;
|
||||
} & Omit<TOrgPermission, "orgId">;
|
||||
|
||||
export type TAccessProjectDTO = {
|
||||
projectId: string;
|
||||
} & Omit<TOrgPermission, "orgId">;
|
@ -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
|
||||
};
|
||||
};
|
84
backend/src/services/pki-alert/pki-alert-dal.ts
Normal file
84
backend/src/services/pki-alert/pki-alert-dal.ts
Normal file
@ -0,0 +1,84 @@
|
||||
import { TDbClient } from "@app/db";
|
||||
import { TableName } from "@app/db/schemas";
|
||||
import { DatabaseError } from "@app/lib/errors";
|
||||
import { ormify } from "@app/lib/knex";
|
||||
|
||||
import { PkiItemType } from "../pki-collection/pki-collection-types";
|
||||
|
||||
export type TPkiAlertDALFactory = ReturnType<typeof pkiAlertDALFactory>;
|
||||
|
||||
export const pkiAlertDALFactory = (db: TDbClient) => {
|
||||
const pkiAlertOrm = ormify(db, TableName.PkiAlert);
|
||||
|
||||
const getExpiringPkiCollectionItemsForAlerting = async () => {
|
||||
try {
|
||||
type AlertItem = {
|
||||
type: PkiItemType;
|
||||
id: string; // id of the CA or certificate
|
||||
expiryDate: Date;
|
||||
serialNumber: string;
|
||||
friendlyName: string;
|
||||
pkiCollectionId: string;
|
||||
alertId: string;
|
||||
alertName: string;
|
||||
alertBeforeDays: number;
|
||||
recipientEmails: string;
|
||||
};
|
||||
|
||||
// gets CAs and certificates as part of PKI collection items
|
||||
const combinedQuery = db
|
||||
.replicaNode()
|
||||
.select(
|
||||
db.raw("? as type", [PkiItemType.CA]),
|
||||
`${PkiItemType.CA}.id`,
|
||||
`${PkiItemType.CA}.notAfter as expiryDate`,
|
||||
`${PkiItemType.CA}.serialNumber`,
|
||||
`${PkiItemType.CA}.friendlyName`,
|
||||
"pci.pkiCollectionId"
|
||||
)
|
||||
.from(`${TableName.CertificateAuthority} as ${PkiItemType.CA}`)
|
||||
.join(`${TableName.PkiCollectionItem} as pci`, `${PkiItemType.CA}.id`, "pci.caId")
|
||||
.unionAll((qb) => {
|
||||
void qb
|
||||
.select(
|
||||
db.raw("? as type", [PkiItemType.CERTIFICATE]),
|
||||
`${PkiItemType.CERTIFICATE}.id`,
|
||||
`${PkiItemType.CERTIFICATE}.notAfter as expiryDate`,
|
||||
`${PkiItemType.CERTIFICATE}.serialNumber`,
|
||||
`${PkiItemType.CERTIFICATE}.friendlyName`,
|
||||
"pci.pkiCollectionId"
|
||||
)
|
||||
.from(`${TableName.Certificate} as ${PkiItemType.CERTIFICATE}`)
|
||||
.join(`${TableName.PkiCollectionItem} as pci`, `${PkiItemType.CERTIFICATE}.id`, "pci.certId");
|
||||
});
|
||||
|
||||
/**
|
||||
* Gets alerts to send based on alertBeforeDays on PKI alerts connected to PKI collection items
|
||||
* Note: Results are clamped to 1-day window to avoid sending multiple alerts for the same item
|
||||
*/
|
||||
const alertQuery = db
|
||||
.replicaNode()
|
||||
.select("combined.*", "pa.id as alertId", "pa.name as alertName", "pa.alertBeforeDays", "pa.recipientEmails")
|
||||
.from(db.raw("(?) as combined", [combinedQuery]))
|
||||
.join(`${TableName.PkiAlert} as pa`, "combined.pkiCollectionId", "pa.pkiCollectionId")
|
||||
.whereRaw(
|
||||
`
|
||||
combined."expiryDate" <= CURRENT_TIMESTAMP + (pa."alertBeforeDays" * INTERVAL '1 day')
|
||||
AND combined."expiryDate" > CURRENT_TIMESTAMP + ((pa."alertBeforeDays" - 1) * INTERVAL '1 day')
|
||||
`
|
||||
)
|
||||
.orderBy("combined.expiryDate");
|
||||
|
||||
const results = (await alertQuery) as AlertItem[];
|
||||
|
||||
return results;
|
||||
} catch (error) {
|
||||
throw new DatabaseError({ error, name: "Get expiring PKI collection items for alerting" });
|
||||
}
|
||||
};
|
||||
|
||||
return {
|
||||
getExpiringPkiCollectionItemsForAlerting,
|
||||
...pkiAlertOrm
|
||||
};
|
||||
};
|
182
backend/src/services/pki-alert/pki-alert-service.ts
Normal file
182
backend/src/services/pki-alert/pki-alert-service.ts
Normal file
@ -0,0 +1,182 @@
|
||||
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 { NotFoundError, UnauthorizedError } from "@app/lib/errors";
|
||||
import { groupBy } from "@app/lib/fn";
|
||||
import { TPkiCollectionDALFactory } from "@app/services/pki-collection/pki-collection-dal";
|
||||
import { pkiItemTypeToNameMap } from "@app/services/pki-collection/pki-collection-types";
|
||||
import { SmtpTemplates, TSmtpService } from "@app/services/smtp/smtp-service";
|
||||
|
||||
import { TPkiAlertDALFactory } from "./pki-alert-dal";
|
||||
import { TCreateAlertDTO, TDeleteAlertDTO, TGetAlertByIdDTO, TUpdateAlertDTO } from "./pki-alert-types";
|
||||
|
||||
type TPkiAlertServiceFactoryDep = {
|
||||
pkiAlertDAL: Pick<
|
||||
TPkiAlertDALFactory,
|
||||
"create" | "findById" | "updateById" | "deleteById" | "getExpiringPkiCollectionItemsForAlerting"
|
||||
>;
|
||||
pkiCollectionDAL: Pick<TPkiCollectionDALFactory, "findById">;
|
||||
permissionService: Pick<TPermissionServiceFactory, "getProjectPermission">;
|
||||
smtpService: Pick<TSmtpService, "sendMail">;
|
||||
};
|
||||
|
||||
export type TPkiAlertServiceFactory = ReturnType<typeof pkiAlertServiceFactory>;
|
||||
|
||||
export const pkiAlertServiceFactory = ({
|
||||
pkiAlertDAL,
|
||||
pkiCollectionDAL,
|
||||
permissionService,
|
||||
smtpService
|
||||
}: TPkiAlertServiceFactoryDep) => {
|
||||
const sendPkiItemExpiryNotices = async () => {
|
||||
const allAlertItems = await pkiAlertDAL.getExpiringPkiCollectionItemsForAlerting();
|
||||
|
||||
const flattenedResults = allAlertItems.flatMap(({ recipientEmails, ...item }) =>
|
||||
recipientEmails.split(",").map((email) => ({
|
||||
...item,
|
||||
recipientEmail: email.trim()
|
||||
}))
|
||||
);
|
||||
|
||||
const groupedByEmail = groupBy(flattenedResults, (item) => item.recipientEmail);
|
||||
|
||||
for await (const [email, items] of Object.entries(groupedByEmail)) {
|
||||
const groupedByAlert = groupBy(items, (item) => item.alertId);
|
||||
for await (const [, alertItems] of Object.entries(groupedByAlert)) {
|
||||
await smtpService.sendMail({
|
||||
recipients: [email],
|
||||
subjectLine: `Infisical CA/Certificate expiration notice: ${alertItems[0].alertName}`,
|
||||
substitutions: {
|
||||
alertName: alertItems[0].alertName,
|
||||
alertBeforeDays: items[0].alertBeforeDays,
|
||||
items: alertItems.map((alertItem) => ({
|
||||
...alertItem,
|
||||
type: pkiItemTypeToNameMap[alertItem.type],
|
||||
expiryDate: new Date(alertItem.expiryDate).toString()
|
||||
}))
|
||||
},
|
||||
template: SmtpTemplates.PkiExpirationAlert
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const createPkiAlert = async ({
|
||||
projectId,
|
||||
name,
|
||||
pkiCollectionId,
|
||||
alertBeforeDays,
|
||||
emails,
|
||||
actorId,
|
||||
actorAuthMethod,
|
||||
actor,
|
||||
actorOrgId
|
||||
}: TCreateAlertDTO) => {
|
||||
const { permission } = await permissionService.getProjectPermission(
|
||||
actor,
|
||||
actorId,
|
||||
projectId,
|
||||
actorAuthMethod,
|
||||
actorOrgId
|
||||
);
|
||||
|
||||
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Create, ProjectPermissionSub.PkiAlerts);
|
||||
|
||||
const pkiCollection = await pkiCollectionDAL.findById(pkiCollectionId);
|
||||
if (!pkiCollection) throw new NotFoundError({ message: "PKI collection not found" });
|
||||
if (pkiCollection.projectId !== projectId)
|
||||
throw new UnauthorizedError({ message: "PKI collection not found in project" });
|
||||
|
||||
const alert = await pkiAlertDAL.create({
|
||||
projectId,
|
||||
pkiCollectionId,
|
||||
name,
|
||||
alertBeforeDays,
|
||||
recipientEmails: emails.join(",")
|
||||
});
|
||||
return alert;
|
||||
};
|
||||
|
||||
const getPkiAlertById = async ({ alertId, actorId, actorAuthMethod, actor, actorOrgId }: TGetAlertByIdDTO) => {
|
||||
const alert = await pkiAlertDAL.findById(alertId);
|
||||
if (!alert) throw new NotFoundError({ message: "Alert not found" });
|
||||
|
||||
const { permission } = await permissionService.getProjectPermission(
|
||||
actor,
|
||||
actorId,
|
||||
alert.projectId,
|
||||
actorAuthMethod,
|
||||
actorOrgId
|
||||
);
|
||||
|
||||
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.PkiAlerts);
|
||||
return alert;
|
||||
};
|
||||
|
||||
const updatePkiAlert = async ({
|
||||
alertId,
|
||||
name,
|
||||
pkiCollectionId,
|
||||
alertBeforeDays,
|
||||
emails,
|
||||
actorId,
|
||||
actorAuthMethod,
|
||||
actor,
|
||||
actorOrgId
|
||||
}: TUpdateAlertDTO) => {
|
||||
let alert = await pkiAlertDAL.findById(alertId);
|
||||
if (!alert) throw new NotFoundError({ message: "Alert not found" });
|
||||
|
||||
const { permission } = await permissionService.getProjectPermission(
|
||||
actor,
|
||||
actorId,
|
||||
alert.projectId,
|
||||
actorAuthMethod,
|
||||
actorOrgId
|
||||
);
|
||||
|
||||
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Edit, ProjectPermissionSub.PkiAlerts);
|
||||
|
||||
if (pkiCollectionId) {
|
||||
const pkiCollection = await pkiCollectionDAL.findById(pkiCollectionId);
|
||||
if (!pkiCollection) throw new NotFoundError({ message: "PKI collection not found" });
|
||||
if (pkiCollection.projectId !== alert.projectId)
|
||||
throw new UnauthorizedError({ message: "PKI collection not found in project" });
|
||||
}
|
||||
|
||||
alert = await pkiAlertDAL.updateById(alertId, {
|
||||
name,
|
||||
alertBeforeDays,
|
||||
...(pkiCollectionId && { pkiCollectionId }),
|
||||
...(emails && { recipientEmails: emails.join(",") })
|
||||
});
|
||||
|
||||
return alert;
|
||||
};
|
||||
|
||||
const deletePkiAlert = async ({ alertId, actorId, actorAuthMethod, actor, actorOrgId }: TDeleteAlertDTO) => {
|
||||
let alert = await pkiAlertDAL.findById(alertId);
|
||||
if (!alert) throw new NotFoundError({ message: "Alert not found" });
|
||||
|
||||
const { permission } = await permissionService.getProjectPermission(
|
||||
actor,
|
||||
actorId,
|
||||
alert.projectId,
|
||||
actorAuthMethod,
|
||||
actorOrgId
|
||||
);
|
||||
|
||||
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Delete, ProjectPermissionSub.PkiAlerts);
|
||||
alert = await pkiAlertDAL.deleteById(alertId);
|
||||
return alert;
|
||||
};
|
||||
|
||||
return {
|
||||
sendPkiItemExpiryNotices,
|
||||
createPkiAlert,
|
||||
getPkiAlertById,
|
||||
updatePkiAlert,
|
||||
deletePkiAlert
|
||||
};
|
||||
};
|
24
backend/src/services/pki-alert/pki-alert-types.ts
Normal file
24
backend/src/services/pki-alert/pki-alert-types.ts
Normal file
@ -0,0 +1,24 @@
|
||||
import { TProjectPermission } from "@app/lib/types";
|
||||
|
||||
export type TCreateAlertDTO = {
|
||||
name: string;
|
||||
pkiCollectionId: string;
|
||||
alertBeforeDays: number;
|
||||
emails: string[];
|
||||
} & TProjectPermission;
|
||||
|
||||
export type TGetAlertByIdDTO = {
|
||||
alertId: string;
|
||||
} & Omit<TProjectPermission, "projectId">;
|
||||
|
||||
export type TUpdateAlertDTO = {
|
||||
alertId: string;
|
||||
name?: string;
|
||||
pkiCollectionId?: string;
|
||||
alertBeforeDays?: number;
|
||||
emails?: string[];
|
||||
} & Omit<TProjectPermission, "projectId">;
|
||||
|
||||
export type TDeleteAlertDTO = {
|
||||
alertId: string;
|
||||
} & Omit<TProjectPermission, "projectId">;
|
13
backend/src/services/pki-collection/pki-collection-dal.ts
Normal file
13
backend/src/services/pki-collection/pki-collection-dal.ts
Normal file
@ -0,0 +1,13 @@
|
||||
import { TDbClient } from "@app/db";
|
||||
import { TableName } from "@app/db/schemas";
|
||||
import { ormify } from "@app/lib/knex";
|
||||
|
||||
export type TPkiCollectionDALFactory = ReturnType<typeof pkiCollectionDALFactory>;
|
||||
|
||||
export const pkiCollectionDALFactory = (db: TDbClient) => {
|
||||
const pkiCollectionOrm = ormify(db, TableName.PkiCollection);
|
||||
|
||||
return {
|
||||
...pkiCollectionOrm
|
||||
};
|
||||
};
|
30
backend/src/services/pki-collection/pki-collection-fns.ts
Normal file
30
backend/src/services/pki-collection/pki-collection-fns.ts
Normal file
@ -0,0 +1,30 @@
|
||||
import { TPkiCollectionItems } from "@app/db/schemas";
|
||||
|
||||
import { PkiItemType } from "./pki-collection-types";
|
||||
|
||||
/**
|
||||
* Transforms a PKI Collection Item from the database to the expected API response format
|
||||
*/
|
||||
export const transformPkiCollectionItem = (pkiCollectionItem: TPkiCollectionItems) => {
|
||||
let type: PkiItemType;
|
||||
let itemId: string;
|
||||
|
||||
if (pkiCollectionItem.caId) {
|
||||
type = PkiItemType.CA;
|
||||
itemId = pkiCollectionItem.caId;
|
||||
} else if (pkiCollectionItem.certId) {
|
||||
type = PkiItemType.CERTIFICATE;
|
||||
itemId = pkiCollectionItem.certId;
|
||||
} else {
|
||||
throw new Error("Invalid PKI Collection Item: must have either caId or certId");
|
||||
}
|
||||
|
||||
return {
|
||||
id: pkiCollectionItem.id,
|
||||
pkiCollectionId: pkiCollectionItem.pkiCollectionId,
|
||||
type,
|
||||
itemId,
|
||||
createdAt: pkiCollectionItem.createdAt,
|
||||
updatedAt: pkiCollectionItem.updatedAt
|
||||
};
|
||||
};
|
@ -0,0 +1,93 @@
|
||||
import { TDbClient } from "@app/db";
|
||||
import { TableName, TPkiCollectionItems } from "@app/db/schemas";
|
||||
import { DatabaseError } from "@app/lib/errors";
|
||||
import { ormify } from "@app/lib/knex";
|
||||
|
||||
import { PkiItemType } from "./pki-collection-types";
|
||||
|
||||
export type TPkiCollectionItemDALFactory = ReturnType<typeof pkiCollectionItemDALFactory>;
|
||||
|
||||
export const pkiCollectionItemDALFactory = (db: TDbClient) => {
|
||||
const pkiCollectionItemOrm = ormify(db, TableName.PkiCollectionItem);
|
||||
|
||||
const findPkiCollectionItems = async ({
|
||||
collectionId,
|
||||
type,
|
||||
offset,
|
||||
limit
|
||||
}: {
|
||||
collectionId: string;
|
||||
type?: PkiItemType;
|
||||
offset?: number;
|
||||
limit?: number;
|
||||
}) => {
|
||||
try {
|
||||
const query = db
|
||||
.replicaNode()(TableName.PkiCollectionItem)
|
||||
.select(
|
||||
"pki_collection_items.*",
|
||||
db.raw(
|
||||
`COALESCE("${TableName.CertificateAuthority}"."notBefore", "${TableName.Certificate}"."notBefore") as "notBefore"`
|
||||
),
|
||||
db.raw(
|
||||
`COALESCE("${TableName.CertificateAuthority}"."notAfter", "${TableName.Certificate}"."notAfter") as "notAfter"`
|
||||
),
|
||||
db.raw(
|
||||
`COALESCE("${TableName.CertificateAuthority}"."friendlyName", "${TableName.Certificate}"."friendlyName") as "friendlyName"`
|
||||
)
|
||||
)
|
||||
.leftJoin(
|
||||
TableName.CertificateAuthority,
|
||||
`${TableName.PkiCollectionItem}.caId`,
|
||||
`${TableName.CertificateAuthority}.id`
|
||||
)
|
||||
.leftJoin(TableName.Certificate, `${TableName.PkiCollectionItem}.certId`, `${TableName.Certificate}.id`)
|
||||
.where((builder) => {
|
||||
void builder.where(`${TableName.PkiCollectionItem}.pkiCollectionId`, collectionId);
|
||||
if (type === PkiItemType.CA) {
|
||||
void builder.whereNull(`${TableName.PkiCollectionItem}.certId`);
|
||||
} else if (type === PkiItemType.CERTIFICATE) {
|
||||
void builder.whereNull(`${TableName.PkiCollectionItem}.caId`);
|
||||
}
|
||||
});
|
||||
|
||||
if (offset) {
|
||||
void query.offset(offset);
|
||||
}
|
||||
if (limit) {
|
||||
void query.limit(limit);
|
||||
}
|
||||
|
||||
void query.orderBy(`${TableName.PkiCollectionItem}.createdAt`, "desc");
|
||||
|
||||
const result = await query;
|
||||
return result as (TPkiCollectionItems & { notAfter: Date; notBefore: Date; friendlyName: string })[];
|
||||
} catch (error) {
|
||||
throw new DatabaseError({ error, name: "Find all PKI collection items" });
|
||||
}
|
||||
};
|
||||
|
||||
const countItemsInPkiCollection = async (collectionId: string) => {
|
||||
try {
|
||||
interface CountResult {
|
||||
count: string;
|
||||
}
|
||||
|
||||
const query = db
|
||||
.replicaNode()(TableName.PkiCollectionItem)
|
||||
.where(`${TableName.PkiCollectionItem}.pkiCollectionId`, collectionId);
|
||||
|
||||
const count = await query.count("*").first();
|
||||
|
||||
return parseInt((count as unknown as CountResult).count || "0", 10);
|
||||
} catch (error) {
|
||||
throw new DatabaseError({ error, name: "Count all PKI collection items" });
|
||||
}
|
||||
};
|
||||
|
||||
return {
|
||||
...pkiCollectionItemOrm,
|
||||
findPkiCollectionItems,
|
||||
countItemsInPkiCollection
|
||||
};
|
||||
};
|
331
backend/src/services/pki-collection/pki-collection-service.ts
Normal file
331
backend/src/services/pki-collection/pki-collection-service.ts
Normal file
@ -0,0 +1,331 @@
|
||||
import { ForbiddenError } from "@casl/ability";
|
||||
|
||||
import { TPkiCollectionItems } from "@app/db/schemas";
|
||||
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 { TCertificateDALFactory } from "@app/services/certificate/certificate-dal";
|
||||
import { TCertificateAuthorityDALFactory } from "@app/services/certificate-authority/certificate-authority-dal";
|
||||
|
||||
import { TPkiCollectionDALFactory } from "./pki-collection-dal";
|
||||
import { transformPkiCollectionItem } from "./pki-collection-fns";
|
||||
import { TPkiCollectionItemDALFactory } from "./pki-collection-item-dal";
|
||||
import {
|
||||
PkiItemType,
|
||||
TAddItemToPkiCollectionDTO,
|
||||
TCreatePkiCollectionDTO,
|
||||
TDeletePkiCollectionDTO,
|
||||
TGetPkiCollectionByIdDTO,
|
||||
TGetPkiCollectionItems,
|
||||
TRemoveItemFromPkiCollectionDTO,
|
||||
TUpdatePkiCollectionDTO
|
||||
} from "./pki-collection-types";
|
||||
|
||||
type TPkiCollectionServiceFactoryDep = {
|
||||
pkiCollectionDAL: Pick<TPkiCollectionDALFactory, "create" | "findById" | "updateById" | "deleteById">;
|
||||
pkiCollectionItemDAL: Pick<
|
||||
TPkiCollectionItemDALFactory,
|
||||
"findOne" | "create" | "deleteById" | "findPkiCollectionItems" | "countItemsInPkiCollection"
|
||||
>;
|
||||
certificateAuthorityDAL: Pick<TCertificateAuthorityDALFactory, "find" | "findOne">;
|
||||
certificateDAL: Pick<TCertificateDALFactory, "find">;
|
||||
permissionService: Pick<TPermissionServiceFactory, "getProjectPermission">;
|
||||
};
|
||||
|
||||
export type TPkiCollectionServiceFactory = ReturnType<typeof pkiCollectionServiceFactory>;
|
||||
|
||||
export const pkiCollectionServiceFactory = ({
|
||||
pkiCollectionDAL,
|
||||
pkiCollectionItemDAL,
|
||||
certificateAuthorityDAL,
|
||||
certificateDAL,
|
||||
permissionService
|
||||
}: TPkiCollectionServiceFactoryDep) => {
|
||||
const createPkiCollection = async ({
|
||||
name,
|
||||
description,
|
||||
projectId,
|
||||
actorId,
|
||||
actorAuthMethod,
|
||||
actor,
|
||||
actorOrgId
|
||||
}: TCreatePkiCollectionDTO) => {
|
||||
const { permission } = await permissionService.getProjectPermission(
|
||||
actor,
|
||||
actorId,
|
||||
projectId,
|
||||
actorAuthMethod,
|
||||
actorOrgId
|
||||
);
|
||||
|
||||
ForbiddenError.from(permission).throwUnlessCan(
|
||||
ProjectPermissionActions.Create,
|
||||
ProjectPermissionSub.PkiCollections
|
||||
);
|
||||
|
||||
const pkiCollection = await pkiCollectionDAL.create({
|
||||
projectId,
|
||||
name,
|
||||
description
|
||||
});
|
||||
|
||||
return pkiCollection;
|
||||
};
|
||||
|
||||
const getPkiCollectionById = async ({
|
||||
collectionId,
|
||||
actorId,
|
||||
actorAuthMethod,
|
||||
actor,
|
||||
actorOrgId
|
||||
}: TGetPkiCollectionByIdDTO) => {
|
||||
const pkiCollection = await pkiCollectionDAL.findById(collectionId);
|
||||
if (!pkiCollection) throw new NotFoundError({ message: "PKI collection not found" });
|
||||
|
||||
const { permission } = await permissionService.getProjectPermission(
|
||||
actor,
|
||||
actorId,
|
||||
pkiCollection.projectId,
|
||||
actorAuthMethod,
|
||||
actorOrgId
|
||||
);
|
||||
|
||||
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.PkiCollections);
|
||||
return pkiCollection;
|
||||
};
|
||||
|
||||
const updatePkiCollection = async ({
|
||||
collectionId,
|
||||
name,
|
||||
description,
|
||||
actorId,
|
||||
actorAuthMethod,
|
||||
actor,
|
||||
actorOrgId
|
||||
}: TUpdatePkiCollectionDTO) => {
|
||||
let pkiCollection = await pkiCollectionDAL.findById(collectionId);
|
||||
if (!pkiCollection) throw new NotFoundError({ message: "PKI collection not found" });
|
||||
|
||||
const { permission } = await permissionService.getProjectPermission(
|
||||
actor,
|
||||
actorId,
|
||||
pkiCollection.projectId,
|
||||
actorAuthMethod,
|
||||
actorOrgId
|
||||
);
|
||||
|
||||
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Edit, ProjectPermissionSub.PkiCollections);
|
||||
pkiCollection = await pkiCollectionDAL.updateById(collectionId, {
|
||||
name,
|
||||
description
|
||||
});
|
||||
|
||||
return pkiCollection;
|
||||
};
|
||||
|
||||
const deletePkiCollection = async ({
|
||||
collectionId,
|
||||
actorId,
|
||||
actorAuthMethod,
|
||||
actor,
|
||||
actorOrgId
|
||||
}: TDeletePkiCollectionDTO) => {
|
||||
let pkiCollection = await pkiCollectionDAL.findById(collectionId);
|
||||
if (!pkiCollection) throw new NotFoundError({ message: "PKI collection not found" });
|
||||
|
||||
const { permission } = await permissionService.getProjectPermission(
|
||||
actor,
|
||||
actorId,
|
||||
pkiCollection.projectId,
|
||||
actorAuthMethod,
|
||||
actorOrgId
|
||||
);
|
||||
|
||||
ForbiddenError.from(permission).throwUnlessCan(
|
||||
ProjectPermissionActions.Delete,
|
||||
ProjectPermissionSub.PkiCollections
|
||||
);
|
||||
pkiCollection = await pkiCollectionDAL.deleteById(collectionId);
|
||||
return pkiCollection;
|
||||
};
|
||||
|
||||
const getPkiCollectionItems = async ({
|
||||
collectionId,
|
||||
type,
|
||||
offset = 0,
|
||||
limit = 25,
|
||||
actorId,
|
||||
actorAuthMethod,
|
||||
actor,
|
||||
actorOrgId
|
||||
}: TGetPkiCollectionItems) => {
|
||||
const pkiCollection = await pkiCollectionDAL.findById(collectionId);
|
||||
if (!pkiCollection) throw new NotFoundError({ message: "PKI collection not found" });
|
||||
|
||||
const { permission } = await permissionService.getProjectPermission(
|
||||
actor,
|
||||
actorId,
|
||||
pkiCollection.projectId,
|
||||
actorAuthMethod,
|
||||
actorOrgId
|
||||
);
|
||||
|
||||
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.PkiCollections);
|
||||
|
||||
const pkiCollectionItems = await pkiCollectionItemDAL.findPkiCollectionItems({
|
||||
collectionId,
|
||||
type,
|
||||
offset,
|
||||
limit
|
||||
});
|
||||
|
||||
const count = await pkiCollectionItemDAL.countItemsInPkiCollection(collectionId);
|
||||
|
||||
return {
|
||||
pkiCollection,
|
||||
pkiCollectionItems: pkiCollectionItems.map((p) => ({
|
||||
...transformPkiCollectionItem(p),
|
||||
notBefore: p.notBefore,
|
||||
notAfter: p.notAfter,
|
||||
friendlyName: p.friendlyName
|
||||
})),
|
||||
totalCount: count
|
||||
};
|
||||
};
|
||||
|
||||
const addItemToPkiCollection = async ({
|
||||
collectionId,
|
||||
actorId,
|
||||
actorAuthMethod,
|
||||
actor,
|
||||
actorOrgId,
|
||||
type,
|
||||
itemId
|
||||
}: TAddItemToPkiCollectionDTO) => {
|
||||
const pkiCollection = await pkiCollectionDAL.findById(collectionId);
|
||||
if (!pkiCollection) throw new NotFoundError({ message: "PKI collection not found" });
|
||||
|
||||
const { permission } = await permissionService.getProjectPermission(
|
||||
actor,
|
||||
actorId,
|
||||
pkiCollection.projectId,
|
||||
actorAuthMethod,
|
||||
actorOrgId
|
||||
);
|
||||
|
||||
ForbiddenError.from(permission).throwUnlessCan(
|
||||
ProjectPermissionActions.Create,
|
||||
ProjectPermissionSub.PkiCollections
|
||||
);
|
||||
|
||||
let pkiCollectionItem: TPkiCollectionItems;
|
||||
switch (type) {
|
||||
case PkiItemType.CA: {
|
||||
// validate that CA has not already been added to PKI collection
|
||||
const isCaAdded = await pkiCollectionItemDAL.findOne({
|
||||
pkiCollectionId: collectionId,
|
||||
caId: itemId
|
||||
});
|
||||
|
||||
if (isCaAdded) throw new BadRequestError({ message: "CA is already part of the PKI collection" });
|
||||
|
||||
// validate that there exists a CA in same project as PKI collection
|
||||
const ca = await certificateAuthorityDAL.findOne({
|
||||
id: itemId,
|
||||
projectId: pkiCollection.projectId
|
||||
});
|
||||
|
||||
if (!ca) throw new NotFoundError({ message: "CA not found" });
|
||||
|
||||
pkiCollectionItem = await pkiCollectionItemDAL.create({
|
||||
pkiCollectionId: collectionId,
|
||||
caId: itemId
|
||||
});
|
||||
break;
|
||||
}
|
||||
case PkiItemType.CERTIFICATE: {
|
||||
// validate that certificate has not already been added to PKI collection
|
||||
const isCertAdded = await pkiCollectionItemDAL.findOne({
|
||||
pkiCollectionId: collectionId,
|
||||
certId: itemId
|
||||
});
|
||||
if (isCertAdded) throw new BadRequestError({ message: "Certificate already part of the PKI collection" });
|
||||
|
||||
// validate that there exists a certificate in same project as PKI collection
|
||||
const cas = await certificateAuthorityDAL.find({ projectId: pkiCollection.projectId });
|
||||
|
||||
// TODO: consider making this more efficient
|
||||
const [certificate] = await certificateDAL.find({
|
||||
$in: {
|
||||
caId: cas.map((ca) => ca.id)
|
||||
},
|
||||
id: itemId
|
||||
});
|
||||
if (!certificate) throw new NotFoundError({ message: "Certificate not found" });
|
||||
|
||||
pkiCollectionItem = await pkiCollectionItemDAL.create({
|
||||
pkiCollectionId: collectionId,
|
||||
certId: itemId
|
||||
});
|
||||
break;
|
||||
}
|
||||
default: {
|
||||
throw new BadRequestError({ message: "Invalid PKI item type" });
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
pkiCollection,
|
||||
pkiCollectionItem: transformPkiCollectionItem(pkiCollectionItem)
|
||||
};
|
||||
};
|
||||
|
||||
const removeItemFromPkiCollection = async ({
|
||||
collectionId,
|
||||
actorId,
|
||||
actorAuthMethod,
|
||||
actor,
|
||||
actorOrgId,
|
||||
itemId
|
||||
}: TRemoveItemFromPkiCollectionDTO) => {
|
||||
const pkiCollection = await pkiCollectionDAL.findById(collectionId);
|
||||
if (!pkiCollection) throw new NotFoundError({ message: "PKI collection not found" });
|
||||
|
||||
let pkiCollectionItem = await pkiCollectionItemDAL.findOne({
|
||||
pkiCollectionId: collectionId,
|
||||
id: itemId
|
||||
});
|
||||
|
||||
if (!pkiCollectionItem) throw new NotFoundError({ message: "PKI collection item not found" });
|
||||
|
||||
const { permission } = await permissionService.getProjectPermission(
|
||||
actor,
|
||||
actorId,
|
||||
pkiCollection.projectId,
|
||||
actorAuthMethod,
|
||||
actorOrgId
|
||||
);
|
||||
|
||||
ForbiddenError.from(permission).throwUnlessCan(
|
||||
ProjectPermissionActions.Delete,
|
||||
ProjectPermissionSub.PkiCollections
|
||||
);
|
||||
|
||||
pkiCollectionItem = await pkiCollectionItemDAL.deleteById(itemId);
|
||||
|
||||
return {
|
||||
pkiCollection,
|
||||
pkiCollectionItem: transformPkiCollectionItem(pkiCollectionItem)
|
||||
};
|
||||
};
|
||||
|
||||
return {
|
||||
createPkiCollection,
|
||||
getPkiCollectionById,
|
||||
updatePkiCollection,
|
||||
deletePkiCollection,
|
||||
getPkiCollectionItems,
|
||||
addItemToPkiCollection,
|
||||
removeItemFromPkiCollection
|
||||
};
|
||||
};
|
48
backend/src/services/pki-collection/pki-collection-types.ts
Normal file
48
backend/src/services/pki-collection/pki-collection-types.ts
Normal file
@ -0,0 +1,48 @@
|
||||
import { TProjectPermission } from "@app/lib/types";
|
||||
|
||||
export type TCreatePkiCollectionDTO = {
|
||||
name: string;
|
||||
description: string;
|
||||
} & TProjectPermission;
|
||||
|
||||
export type TGetPkiCollectionByIdDTO = {
|
||||
collectionId: string;
|
||||
} & Omit<TProjectPermission, "projectId">;
|
||||
|
||||
export type TUpdatePkiCollectionDTO = {
|
||||
collectionId: string;
|
||||
name?: string;
|
||||
description?: string;
|
||||
} & Omit<TProjectPermission, "projectId">;
|
||||
|
||||
export type TDeletePkiCollectionDTO = {
|
||||
collectionId: string;
|
||||
} & Omit<TProjectPermission, "projectId">;
|
||||
|
||||
export enum PkiItemType {
|
||||
CERTIFICATE = "certificate",
|
||||
CA = "ca"
|
||||
}
|
||||
|
||||
export const pkiItemTypeToNameMap: { [K in PkiItemType]: string } = {
|
||||
[PkiItemType.CA]: "CA",
|
||||
[PkiItemType.CERTIFICATE]: "Certificate"
|
||||
};
|
||||
|
||||
export type TGetPkiCollectionItems = {
|
||||
collectionId: string;
|
||||
type?: PkiItemType;
|
||||
offset: number;
|
||||
limit: number;
|
||||
} & Omit<TProjectPermission, "projectId">;
|
||||
|
||||
export type TAddItemToPkiCollectionDTO = {
|
||||
collectionId: string;
|
||||
type: PkiItemType;
|
||||
itemId: string;
|
||||
} & Omit<TProjectPermission, "projectId">;
|
||||
|
||||
export type TRemoveItemFromPkiCollectionDTO = {
|
||||
collectionId: string;
|
||||
itemId: string;
|
||||
} & Omit<TProjectPermission, "projectId">;
|
@ -46,6 +46,7 @@ export const projectBotDALFactory = (db: TDbClient) => {
|
||||
const doc = await db
|
||||
.replicaNode()(TableName.ProjectMembership)
|
||||
.where(`${TableName.ProjectMembership}.projectId` as "projectId", projectId)
|
||||
.where(`${TableName.ProjectKeys}.projectId` as "projectId", projectId)
|
||||
.where(`${TableName.Users}.isGhost` as "isGhost", false)
|
||||
.join(TableName.Users, `${TableName.ProjectMembership}.userId`, `${TableName.Users}.id`)
|
||||
.join(TableName.ProjectKeys, `${TableName.ProjectMembership}.userId`, `${TableName.ProjectKeys}.receiverId`)
|
||||
|
@ -66,10 +66,10 @@ export const getBotKeyFnFactory = (
|
||||
await projectBotDAL.create({
|
||||
name: "Infisical Bot (Ghost)",
|
||||
projectId,
|
||||
isActive: true,
|
||||
tag,
|
||||
iv,
|
||||
encryptedPrivateKey: ciphertext,
|
||||
isActive: true,
|
||||
publicKey: botKey.publicKey,
|
||||
algorithm,
|
||||
keyEncoding: encoding,
|
||||
@ -80,6 +80,12 @@ export const getBotKeyFnFactory = (
|
||||
} else {
|
||||
await projectBotDAL.updateById(bot.id, {
|
||||
isActive: true,
|
||||
tag,
|
||||
iv,
|
||||
encryptedPrivateKey: ciphertext,
|
||||
publicKey: botKey.publicKey,
|
||||
algorithm,
|
||||
keyEncoding: encoding,
|
||||
encryptedProjectKey: encryptedWorkspaceKey.ciphertext,
|
||||
encryptedProjectKeyNonce: encryptedWorkspaceKey.nonce,
|
||||
senderId: projectV1Keys.userId
|
||||
@ -89,7 +95,6 @@ export const getBotKeyFnFactory = (
|
||||
}
|
||||
|
||||
const botPrivateKey = getBotPrivateKey({ bot });
|
||||
|
||||
const botKey = decryptAsymmetric({
|
||||
ciphertext: bot.encryptedProjectKey,
|
||||
privateKey: botPrivateKey,
|
||||
|
@ -256,7 +256,6 @@ export const projectMembershipServiceFactory = ({
|
||||
}
|
||||
|
||||
const bot = await projectBotDAL.findOne({ projectId });
|
||||
|
||||
if (!bot) {
|
||||
throw new BadRequestError({
|
||||
message: "Failed to find bot"
|
||||
|
@ -16,12 +16,15 @@ import { TProjectPermission } from "@app/lib/types";
|
||||
import { ActorType } from "../auth/auth-type";
|
||||
import { TCertificateDALFactory } from "../certificate/certificate-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 { TIdentityProjectDALFactory } from "../identity-project/identity-project-dal";
|
||||
import { TIdentityProjectMembershipRoleDALFactory } from "../identity-project/identity-project-membership-role-dal";
|
||||
import { TKmsServiceFactory } from "../kms/kms-service";
|
||||
import { TOrgDALFactory } from "../org/org-dal";
|
||||
import { TOrgServiceFactory } from "../org/org-service";
|
||||
import { TPkiAlertDALFactory } from "../pki-alert/pki-alert-dal";
|
||||
import { TPkiCollectionDALFactory } from "../pki-collection/pki-collection-dal";
|
||||
import { TProjectBotDALFactory } from "../project-bot/project-bot-dal";
|
||||
import { TProjectEnvDALFactory } from "../project-env/project-env-dal";
|
||||
import { TProjectKeyDALFactory } from "../project-key/project-key-dal";
|
||||
@ -37,7 +40,9 @@ import {
|
||||
TDeleteProjectDTO,
|
||||
TGetProjectDTO,
|
||||
TGetProjectKmsKey,
|
||||
TListProjectAlertsDTO,
|
||||
TListProjectCasDTO,
|
||||
TListProjectCertificateTemplatesDTO,
|
||||
TListProjectCertsDTO,
|
||||
TLoadProjectKmsBackupDTO,
|
||||
TToggleProjectAutoCapitalizationDTO,
|
||||
@ -70,6 +75,9 @@ type TProjectServiceFactoryDep = {
|
||||
projectUserMembershipRoleDAL: Pick<TProjectUserMembershipRoleDALFactory, "create">;
|
||||
certificateAuthorityDAL: Pick<TCertificateAuthorityDALFactory, "find">;
|
||||
certificateDAL: Pick<TCertificateDALFactory, "find" | "countCertificatesInProject">;
|
||||
certificateTemplateDAL: Pick<TCertificateTemplateDALFactory, "getCertTemplatesByProjectId">;
|
||||
pkiAlertDAL: Pick<TPkiAlertDALFactory, "find">;
|
||||
pkiCollectionDAL: Pick<TPkiCollectionDALFactory, "find">;
|
||||
permissionService: TPermissionServiceFactory;
|
||||
orgService: Pick<TOrgServiceFactory, "addGhostUser">;
|
||||
licenseService: Pick<TLicenseServiceFactory, "getPlan">;
|
||||
@ -107,6 +115,9 @@ export const projectServiceFactory = ({
|
||||
identityProjectMembershipRoleDAL,
|
||||
certificateAuthorityDAL,
|
||||
certificateDAL,
|
||||
certificateTemplateDAL,
|
||||
pkiCollectionDAL,
|
||||
pkiAlertDAL,
|
||||
keyStore,
|
||||
kmsService,
|
||||
projectBotDAL
|
||||
@ -676,6 +687,90 @@ export const projectServiceFactory = ({
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Return list of (PKI) alerts configured for project
|
||||
*/
|
||||
const listProjectAlerts = async ({
|
||||
projectId,
|
||||
actor,
|
||||
actorId,
|
||||
actorAuthMethod,
|
||||
actorOrgId
|
||||
}: TListProjectAlertsDTO) => {
|
||||
const { permission } = await permissionService.getProjectPermission(
|
||||
actor,
|
||||
actorId,
|
||||
projectId,
|
||||
actorAuthMethod,
|
||||
actorOrgId
|
||||
);
|
||||
|
||||
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.PkiAlerts);
|
||||
|
||||
const alerts = await pkiAlertDAL.find({ projectId });
|
||||
|
||||
return {
|
||||
alerts
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Return list of PKI collections for project
|
||||
*/
|
||||
const listProjectPkiCollections = async ({
|
||||
projectId,
|
||||
actor,
|
||||
actorId,
|
||||
actorAuthMethod,
|
||||
actorOrgId
|
||||
}: TListProjectAlertsDTO) => {
|
||||
const { permission } = await permissionService.getProjectPermission(
|
||||
actor,
|
||||
actorId,
|
||||
projectId,
|
||||
actorAuthMethod,
|
||||
actorOrgId
|
||||
);
|
||||
|
||||
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.PkiCollections);
|
||||
|
||||
const pkiCollections = await pkiCollectionDAL.find({ projectId });
|
||||
|
||||
return {
|
||||
pkiCollections
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* 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 ({
|
||||
projectId,
|
||||
kms,
|
||||
@ -794,6 +889,9 @@ export const projectServiceFactory = ({
|
||||
upgradeProject,
|
||||
listProjectCas,
|
||||
listProjectCertificates,
|
||||
listProjectAlerts,
|
||||
listProjectPkiCollections,
|
||||
listProjectCertificateTemplates,
|
||||
updateVersionLimit,
|
||||
updateAuditLogsRetention,
|
||||
updateProjectKmsKey,
|
||||
|
@ -106,6 +106,8 @@ export type TListProjectCertsDTO = {
|
||||
commonName?: string;
|
||||
} & Omit<TProjectPermission, "projectId">;
|
||||
|
||||
export type TListProjectAlertsDTO = TProjectPermission;
|
||||
|
||||
export type TUpdateProjectKmsDTO = {
|
||||
kms: { type: KmsType.Internal } | { type: KmsType.External; kmsId: string };
|
||||
} & TProjectPermission;
|
||||
@ -115,3 +117,5 @@ export type TLoadProjectKmsBackupDTO = {
|
||||
} & TProjectPermission;
|
||||
|
||||
export type TGetProjectKmsKey = TProjectPermission;
|
||||
|
||||
export type TListProjectCertificateTemplatesDTO = TProjectPermission;
|
||||
|
@ -4,6 +4,7 @@ import { logger } from "@app/lib/logger";
|
||||
import { QueueJobs, QueueName, TQueueServiceFactory } from "@app/queue";
|
||||
|
||||
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 { TSecretFolderVersionDALFactory } from "../secret-folder/secret-folder-version-dal";
|
||||
import { TSecretSharingDALFactory } from "../secret-sharing/secret-sharing-dal";
|
||||
@ -12,6 +13,7 @@ import { TSecretVersionV2DALFactory } from "../secret-v2-bridge/secret-version-d
|
||||
type TDailyResourceCleanUpQueueServiceFactoryDep = {
|
||||
auditLogDAL: Pick<TAuditLogDALFactory, "pruneAuditLog">;
|
||||
identityAccessTokenDAL: Pick<TIdentityAccessTokenDALFactory, "removeExpiredTokens">;
|
||||
identityUniversalAuthClientSecretDAL: Pick<TIdentityUaClientSecretDALFactory, "removeExpiredClientSecrets">;
|
||||
secretVersionDAL: Pick<TSecretVersionDALFactory, "pruneExcessVersions">;
|
||||
secretVersionV2DAL: Pick<TSecretVersionV2DALFactory, "pruneExcessVersions">;
|
||||
secretFolderVersionDAL: Pick<TSecretFolderVersionDALFactory, "pruneExcessVersions">;
|
||||
@ -30,12 +32,14 @@ export const dailyResourceCleanUpQueueServiceFactory = ({
|
||||
secretFolderVersionDAL,
|
||||
identityAccessTokenDAL,
|
||||
secretSharingDAL,
|
||||
secretVersionV2DAL
|
||||
secretVersionV2DAL,
|
||||
identityUniversalAuthClientSecretDAL
|
||||
}: TDailyResourceCleanUpQueueServiceFactoryDep) => {
|
||||
queueService.start(QueueName.DailyResourceCleanUp, async () => {
|
||||
logger.info(`${QueueName.DailyResourceCleanUp}: queue task started`);
|
||||
await auditLogDAL.pruneAuditLog();
|
||||
await identityAccessTokenDAL.removeExpiredTokens();
|
||||
await identityUniversalAuthClientSecretDAL.removeExpiredClientSecrets();
|
||||
await secretSharingDAL.pruneExpiredSharedSecrets();
|
||||
await snapshotDAL.pruneExcessSnapshots();
|
||||
await secretVersionDAL.pruneExcessVersions();
|
||||
|
@ -36,8 +36,8 @@ type TSecretImportSecretsV2 = {
|
||||
secretKey: string;
|
||||
// akhilmhdh: yes i know you can put ?.
|
||||
// But for somereason ts consider ? and undefined explicit as different just ts things
|
||||
secretValue: string | undefined;
|
||||
secretComment: string | undefined;
|
||||
secretValue: string;
|
||||
secretComment: string;
|
||||
})[];
|
||||
};
|
||||
|
||||
@ -157,7 +157,7 @@ export const fnSecretsV2FromImports = async ({
|
||||
secretImportDAL: Pick<TSecretImportDALFactory, "findByFolderIds">;
|
||||
depth?: number;
|
||||
cyclicDetector?: Set<string>;
|
||||
decryptor: (value?: Buffer | null) => string | undefined;
|
||||
decryptor: (value?: Buffer | null) => string;
|
||||
expandSecretReferences?: (
|
||||
secrets: 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.
|
||||
}))
|
||||
.concat(folderDeeperImportSecrets);
|
||||
|
||||
return {
|
||||
secretPath: importPath,
|
||||
environment: importEnv.slug,
|
||||
@ -254,7 +255,7 @@ export const fnSecretsV2FromImports = async ({
|
||||
};
|
||||
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
|
||||
await expandSecretReferences(secretsGroupByKey);
|
||||
|
@ -507,7 +507,7 @@ export const secretImportServiceFactory = ({
|
||||
folderDAL,
|
||||
secretDAL: secretV2BridgeDAL,
|
||||
secretImportDAL,
|
||||
decryptor: (value) => (value ? secretManagerDecryptor({ cipherTextBlob: value }).toString() : undefined)
|
||||
decryptor: (value) => (value ? secretManagerDecryptor({ cipherTextBlob: value }).toString() : "")
|
||||
});
|
||||
return importedSecrets;
|
||||
}
|
||||
|
@ -51,7 +51,7 @@ export const secretTagDALFactory = (db: TDbClient) => {
|
||||
...secretTagOrm,
|
||||
saveTagsToSecret: secretJnTagOrm.insertMany,
|
||||
deleteTagsToSecret: secretJnTagOrm.delete,
|
||||
saveTagsToSecretV2: secretV2JnTagOrm.insertMany,
|
||||
saveTagsToSecretV2: secretV2JnTagOrm.batchInsert,
|
||||
deleteTagsToSecretV2: secretV2JnTagOrm.delete,
|
||||
findSecretTagsByProjectId,
|
||||
deleteTagsManySecret,
|
||||
|
@ -22,16 +22,7 @@ type TSecretTagServiceFactoryDep = {
|
||||
export type TSecretTagServiceFactory = ReturnType<typeof secretTagServiceFactory>;
|
||||
|
||||
export const secretTagServiceFactory = ({ secretTagDAL, permissionService }: TSecretTagServiceFactoryDep) => {
|
||||
const createTag = async ({
|
||||
name,
|
||||
slug,
|
||||
actor,
|
||||
color,
|
||||
actorId,
|
||||
actorOrgId,
|
||||
actorAuthMethod,
|
||||
projectId
|
||||
}: TCreateTagDTO) => {
|
||||
const createTag = async ({ slug, actor, color, actorId, actorOrgId, actorAuthMethod, projectId }: TCreateTagDTO) => {
|
||||
const { permission } = await permissionService.getProjectPermission(
|
||||
actor,
|
||||
actorId,
|
||||
@ -46,7 +37,6 @@ export const secretTagServiceFactory = ({ secretTagDAL, permissionService }: TSe
|
||||
|
||||
const newTag = await secretTagDAL.create({
|
||||
projectId,
|
||||
name,
|
||||
slug,
|
||||
color,
|
||||
createdBy: actorId,
|
||||
@ -55,7 +45,7 @@ export const secretTagServiceFactory = ({ secretTagDAL, permissionService }: TSe
|
||||
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);
|
||||
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);
|
||||
|
||||
const updatedTag = await secretTagDAL.updateById(tag.id, { name, color, slug });
|
||||
const updatedTag = await secretTagDAL.updateById(tag.id, { color, slug });
|
||||
return updatedTag;
|
||||
};
|
||||
|
||||
@ -107,7 +97,7 @@ export const secretTagServiceFactory = ({ secretTagDAL, permissionService }: TSe
|
||||
);
|
||||
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) => {
|
||||
@ -123,7 +113,7 @@ export const secretTagServiceFactory = ({ secretTagDAL, permissionService }: TSe
|
||||
);
|
||||
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.Tags);
|
||||
|
||||
return tag;
|
||||
return { ...tag, name: tag.slug };
|
||||
};
|
||||
|
||||
const getProjectTags = async ({ actor, actorId, actorOrgId, actorAuthMethod, projectId }: TListProjectTagsDTO) => {
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user