mirror of
https://github.com/Infisical/infisical.git
synced 2025-07-25 14:07:47 +00:00
Compare commits
248 Commits
patch-mult
...
daniel/lea
Author | SHA1 | Date | |
---|---|---|---|
|
f121f8e828 | ||
|
54c8da8ab6 | ||
|
6e0dfc72e4 | ||
|
b226fdac9d | ||
|
3c36d5dbd2 | ||
|
a5f895ad91 | ||
|
9f66b9bb4d | ||
|
80e55a9341 | ||
|
5142d6f3c1 | ||
|
21c6160c84 | ||
|
8a2268956a | ||
|
df3c58bc2a | ||
|
db726128f1 | ||
|
ae177343d5 | ||
|
0342ba0890 | ||
|
a332019c25 | ||
|
8039b3f21e | ||
|
c9f7f6481f | ||
|
39df6ce086 | ||
|
de3e23ecfa | ||
|
17a79fb621 | ||
|
0ee792e84b | ||
|
116e940050 | ||
|
5d45237ea5 | ||
|
44928a2e3c | ||
|
ff912fc3b0 | ||
|
bde40e53e3 | ||
|
5211eb1ed6 | ||
|
96fffd3c03 | ||
|
56506b5a47 | ||
|
400b412196 | ||
|
2780414fcb | ||
|
b82524d65d | ||
|
c493f1d0f6 | ||
|
fb1b816be6 | ||
|
2645d4d158 | ||
|
61d60498a9 | ||
|
93f3395bde | ||
|
d6060781e4 | ||
|
345edb3f15 | ||
|
d4ef92787d | ||
|
b7326bf4c6 | ||
|
3dd024c90a | ||
|
dd6fb4232e | ||
|
3411185d60 | ||
|
2d8a2a6a3a | ||
|
5eeea767a3 | ||
|
2b4f5962e2 | ||
|
bf14bbfeee | ||
|
fa77dc01df | ||
|
ed5044a102 | ||
|
ec7fe013fd | ||
|
a26ad6cfb0 | ||
|
dd0399d12e | ||
|
8fca6b60b3 | ||
|
04456fe996 | ||
|
2605987289 | ||
|
7edcf5ff90 | ||
|
3947e3dabf | ||
|
fe6e5e09ac | ||
|
561992e5cf | ||
|
d69aab0b2c | ||
|
90dae62158 | ||
|
068eb9246d | ||
|
3472be480a | ||
|
df71ecffa0 | ||
|
68818beb38 | ||
|
e600b68684 | ||
|
b52aebfd92 | ||
|
c9e56e4e9f | ||
|
ef03e9bf3b | ||
|
08a77f6ddb | ||
|
bc3f21809e | ||
|
8686b4abd3 | ||
|
46b48cea63 | ||
|
44956c6a37 | ||
|
4de63b6140 | ||
|
5cee228f5f | ||
|
20fea1e25f | ||
|
d0ffb94bc7 | ||
|
d3932d8f08 | ||
|
d5658d374a | ||
|
810a58c836 | ||
|
9e24050f17 | ||
|
8d6f7babff | ||
|
7057d399bc | ||
|
c63d57f086 | ||
|
a9ce3789b0 | ||
|
023a0d99ab | ||
|
5aadc41a4a | ||
|
4f38352765 | ||
|
cf5e367aba | ||
|
da7da27572 | ||
|
a70043b80d | ||
|
b94db5d674 | ||
|
bd6a89fa9a | ||
|
81513e4a75 | ||
|
a28b458653 | ||
|
7ccf752e0c | ||
|
9977329741 | ||
|
2d10265d0d | ||
|
34338720e5 | ||
|
f5322abe85 | ||
|
cd030b0370 | ||
|
6c86db7d4e | ||
|
d48e7eca2d | ||
|
30f3dac35f | ||
|
0e5f0eefc1 | ||
|
2a005d2654 | ||
|
42425d91d5 | ||
|
a0770baff2 | ||
|
f101366bce | ||
|
76c468ecc7 | ||
|
dcf315a524 | ||
|
f8a4b6365c | ||
|
e27d273e8f | ||
|
30dc2d0fcb | ||
|
12e217d200 | ||
|
a3a1c9d2e5 | ||
|
0f266ebe9e | ||
|
506e0b1342 | ||
|
579948ea6d | ||
|
958ad8236a | ||
|
e6ed1231cd | ||
|
b06b8294e9 | ||
|
cb9dabe03f | ||
|
9197530b43 | ||
|
1eae7d0c30 | ||
|
cc8119766a | ||
|
928d5a5240 | ||
|
32dd478894 | ||
|
c3f7c1d46b | ||
|
89644703a0 | ||
|
d20b897f28 | ||
|
70e022826e | ||
|
b7f5fa2cec | ||
|
7b444e91a8 | ||
|
7626dbb96e | ||
|
869be3c273 | ||
|
9a2355fe63 | ||
|
3929a82099 | ||
|
40e5c6ef66 | ||
|
6c95e75d0d | ||
|
d6c9e6db75 | ||
|
76f87a7708 | ||
|
366f03080d | ||
|
dfdd8e95f9 | ||
|
87df5a2749 | ||
|
c4797ea060 | ||
|
6e011a0b52 | ||
|
05ed00834a | ||
|
38b0edf510 | ||
|
56b9506b39 | ||
|
ae34e015db | ||
|
7c42768cd8 | ||
|
b4a9e0e62d | ||
|
30606093f4 | ||
|
16862a3b33 | ||
|
e800a455c4 | ||
|
ba0de6afcf | ||
|
868d0345d6 | ||
|
bfc82105bd | ||
|
c8a3252c1a | ||
|
0bba1801b9 | ||
|
a61e92c49c | ||
|
985116c6f2 | ||
|
9945d249d6 | ||
|
8bc9a5efcd | ||
|
8329cbf299 | ||
|
9138ab8ed7 | ||
|
cf9169ad6f | ||
|
69b76aea64 | ||
|
c9a95023be | ||
|
9db5be1c91 | ||
|
a1b41ca454 | ||
|
6c252b4bfb | ||
|
aafddaa856 | ||
|
776f464bee | ||
|
104b0d6c60 | ||
|
e696bff004 | ||
|
d9c4c332ea | ||
|
120e482c6f | ||
|
abd4b411fa | ||
|
bf430925e4 | ||
|
3079cd72df | ||
|
7c9c65312b | ||
|
8a46cbd08f | ||
|
b48325b4ba | ||
|
fa05639592 | ||
|
9e4b248794 | ||
|
f6e44463c4 | ||
|
1a6b710138 | ||
|
43a3731b62 | ||
|
24b8b64d3b | ||
|
263d321d75 | ||
|
a6e71c98a6 | ||
|
0e86d5573a | ||
|
6c0ab43c97 | ||
|
d743537284 | ||
|
5df53a25fc | ||
|
b6c924ef37 | ||
|
931119f6ea | ||
|
4c0e04528e | ||
|
7fe7056af4 | ||
|
2bd9ad0137 | ||
|
2cbf471beb | ||
|
9072c6c567 | ||
|
ee152f2d20 | ||
|
f21a13f388 | ||
|
7ee440fa3f | ||
|
68a30f4212 | ||
|
4d830f1d1a | ||
|
cd6caab508 | ||
|
ab093dfc85 | ||
|
b8e9417466 | ||
|
4eb08c64d4 | ||
|
d76760fa9c | ||
|
4d8f94a9dc | ||
|
abd8d6aa8a | ||
|
9117067ab5 | ||
|
3a1168c7e8 | ||
|
31de0755a2 | ||
|
2937a46943 | ||
|
45fdd4ebc2 | ||
|
14229931ac | ||
|
526979fcec | ||
|
a0f507d2c9 | ||
|
a2a786f392 | ||
|
f9847f48b0 | ||
|
2f06168b29 | ||
|
b8516da90f | ||
|
26ea949a4e | ||
|
88a4fb84e6 | ||
|
a1e8f45a86 | ||
|
04dca9432d | ||
|
920b9a7dfa | ||
|
8fc4fd64f8 | ||
|
24f7ecc548 | ||
|
a5ca96f2df | ||
|
505ccdf8ea | ||
|
3897bd70fa | ||
|
4479e626c7 | ||
|
6640b55504 | ||
|
85f024c814 | ||
|
531fa634a2 | ||
|
772dd464f5 | ||
|
877b9a409e | ||
|
104a91647c |
@@ -35,7 +35,7 @@ jobs:
|
|||||||
echo "SECRET_SCANNING_GIT_APP_ID=793712" >> .env
|
echo "SECRET_SCANNING_GIT_APP_ID=793712" >> .env
|
||||||
echo "SECRET_SCANNING_PRIVATE_KEY=some-random" >> .env
|
echo "SECRET_SCANNING_PRIVATE_KEY=some-random" >> .env
|
||||||
echo "SECRET_SCANNING_WEBHOOK_SECRET=some-random" >> .env
|
echo "SECRET_SCANNING_WEBHOOK_SECRET=some-random" >> .env
|
||||||
docker run --name infisical-api -d -p 4000:4000 -e DB_CONNECTION_URI=$DB_CONNECTION_URI -e REDIS_URL=$REDIS_URL -e JWT_AUTH_SECRET=$JWT_AUTH_SECRET --env-file .env --entrypoint '/bin/sh' infisical-api -c "npm run migration:latest && ls && node dist/main.mjs"
|
docker run --name infisical-api -d -p 4000:4000 -e DB_CONNECTION_URI=$DB_CONNECTION_URI -e REDIS_URL=$REDIS_URL -e JWT_AUTH_SECRET=$JWT_AUTH_SECRET -e ENCRYPTION_KEY=$ENCRYPTION_KEY --env-file .env --entrypoint '/bin/sh' infisical-api -c "npm run migration:latest && ls && node dist/main.mjs"
|
||||||
env:
|
env:
|
||||||
REDIS_URL: redis://172.17.0.1:6379
|
REDIS_URL: redis://172.17.0.1:6379
|
||||||
DB_CONNECTION_URI: postgres://infisical:infisical@172.17.0.1:5432/infisical?sslmode=disable
|
DB_CONNECTION_URI: postgres://infisical:infisical@172.17.0.1:5432/infisical?sslmode=disable
|
||||||
@@ -47,7 +47,7 @@ jobs:
|
|||||||
- name: Wait for container to be stable and check logs
|
- name: Wait for container to be stable and check logs
|
||||||
run: |
|
run: |
|
||||||
SECONDS=0
|
SECONDS=0
|
||||||
r HEALTHY=0
|
HEALTHY=0
|
||||||
while [ $SECONDS -lt 60 ]; do
|
while [ $SECONDS -lt 60 ]; do
|
||||||
if docker ps | grep infisical-api | grep -q healthy; then
|
if docker ps | grep infisical-api | grep -q healthy; then
|
||||||
echo "Container is healthy."
|
echo "Container is healthy."
|
||||||
|
@@ -22,6 +22,9 @@ jobs:
|
|||||||
CLI_TESTS_SERVICE_TOKEN: ${{ secrets.CLI_TESTS_SERVICE_TOKEN }}
|
CLI_TESTS_SERVICE_TOKEN: ${{ secrets.CLI_TESTS_SERVICE_TOKEN }}
|
||||||
CLI_TESTS_PROJECT_ID: ${{ secrets.CLI_TESTS_PROJECT_ID }}
|
CLI_TESTS_PROJECT_ID: ${{ secrets.CLI_TESTS_PROJECT_ID }}
|
||||||
CLI_TESTS_ENV_SLUG: ${{ secrets.CLI_TESTS_ENV_SLUG }}
|
CLI_TESTS_ENV_SLUG: ${{ secrets.CLI_TESTS_ENV_SLUG }}
|
||||||
|
CLI_TESTS_USER_EMAIL: ${{ secrets.CLI_TESTS_USER_EMAIL }}
|
||||||
|
CLI_TESTS_USER_PASSWORD: ${{ secrets.CLI_TESTS_USER_PASSWORD }}
|
||||||
|
CLI_TESTS_INFISICAL_VAULT_FILE_PASSPHRASE: ${{ secrets.CLI_TESTS_INFISICAL_VAULT_FILE_PASSPHRASE }}
|
||||||
|
|
||||||
goreleaser:
|
goreleaser:
|
||||||
runs-on: ubuntu-20.04
|
runs-on: ubuntu-20.04
|
||||||
@@ -56,7 +59,7 @@ jobs:
|
|||||||
- uses: goreleaser/goreleaser-action@v4
|
- uses: goreleaser/goreleaser-action@v4
|
||||||
with:
|
with:
|
||||||
distribution: goreleaser-pro
|
distribution: goreleaser-pro
|
||||||
version: latest
|
version: v1.26.2-pro
|
||||||
args: release --clean
|
args: release --clean
|
||||||
env:
|
env:
|
||||||
GITHUB_TOKEN: ${{ secrets.GO_RELEASER_GITHUB_TOKEN }}
|
GITHUB_TOKEN: ${{ secrets.GO_RELEASER_GITHUB_TOKEN }}
|
||||||
|
10
.github/workflows/run-cli-tests.yml
vendored
10
.github/workflows/run-cli-tests.yml
vendored
@@ -20,7 +20,12 @@ on:
|
|||||||
required: true
|
required: true
|
||||||
CLI_TESTS_ENV_SLUG:
|
CLI_TESTS_ENV_SLUG:
|
||||||
required: true
|
required: true
|
||||||
|
CLI_TESTS_USER_EMAIL:
|
||||||
|
required: true
|
||||||
|
CLI_TESTS_USER_PASSWORD:
|
||||||
|
required: true
|
||||||
|
CLI_TESTS_INFISICAL_VAULT_FILE_PASSPHRASE:
|
||||||
|
required: true
|
||||||
jobs:
|
jobs:
|
||||||
test:
|
test:
|
||||||
defaults:
|
defaults:
|
||||||
@@ -43,5 +48,8 @@ jobs:
|
|||||||
CLI_TESTS_SERVICE_TOKEN: ${{ secrets.CLI_TESTS_SERVICE_TOKEN }}
|
CLI_TESTS_SERVICE_TOKEN: ${{ secrets.CLI_TESTS_SERVICE_TOKEN }}
|
||||||
CLI_TESTS_PROJECT_ID: ${{ secrets.CLI_TESTS_PROJECT_ID }}
|
CLI_TESTS_PROJECT_ID: ${{ secrets.CLI_TESTS_PROJECT_ID }}
|
||||||
CLI_TESTS_ENV_SLUG: ${{ secrets.CLI_TESTS_ENV_SLUG }}
|
CLI_TESTS_ENV_SLUG: ${{ secrets.CLI_TESTS_ENV_SLUG }}
|
||||||
|
CLI_TESTS_USER_EMAIL: ${{ secrets.CLI_TESTS_USER_EMAIL }}
|
||||||
|
CLI_TESTS_USER_PASSWORD: ${{ secrets.CLI_TESTS_USER_PASSWORD }}
|
||||||
|
INFISICAL_VAULT_FILE_PASSPHRASE: ${{ secrets.CLI_TESTS_INFISICAL_VAULT_FILE_PASSPHRASE }}
|
||||||
|
|
||||||
run: go test -v -count=1 ./test
|
run: go test -v -count=1 ./test
|
||||||
|
216
backend/package-lock.json
generated
216
backend/package-lock.json
generated
@@ -25,6 +25,8 @@
|
|||||||
"@node-saml/passport-saml": "^4.0.4",
|
"@node-saml/passport-saml": "^4.0.4",
|
||||||
"@octokit/rest": "^20.0.2",
|
"@octokit/rest": "^20.0.2",
|
||||||
"@octokit/webhooks-types": "^7.3.1",
|
"@octokit/webhooks-types": "^7.3.1",
|
||||||
|
"@peculiar/asn1-schema": "^2.3.8",
|
||||||
|
"@peculiar/x509": "^1.10.0",
|
||||||
"@serdnam/pino-cloudwatch-transport": "^1.0.4",
|
"@serdnam/pino-cloudwatch-transport": "^1.0.4",
|
||||||
"@sindresorhus/slugify": "^2.2.1",
|
"@sindresorhus/slugify": "^2.2.1",
|
||||||
"@ucast/mongo2js": "^1.3.4",
|
"@ucast/mongo2js": "^1.3.4",
|
||||||
@@ -36,6 +38,7 @@
|
|||||||
"bcrypt": "^5.1.1",
|
"bcrypt": "^5.1.1",
|
||||||
"bullmq": "^5.4.2",
|
"bullmq": "^5.4.2",
|
||||||
"cassandra-driver": "^4.7.2",
|
"cassandra-driver": "^4.7.2",
|
||||||
|
"cron": "^3.1.7",
|
||||||
"dotenv": "^16.4.1",
|
"dotenv": "^16.4.1",
|
||||||
"fastify": "^4.26.0",
|
"fastify": "^4.26.0",
|
||||||
"fastify-plugin": "^4.5.1",
|
"fastify-plugin": "^4.5.1",
|
||||||
@@ -2458,9 +2461,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@fastify/session": {
|
"node_modules/@fastify/session": {
|
||||||
"version": "10.7.0",
|
"version": "10.9.0",
|
||||||
"resolved": "https://registry.npmjs.org/@fastify/session/-/session-10.7.0.tgz",
|
"resolved": "https://registry.npmjs.org/@fastify/session/-/session-10.9.0.tgz",
|
||||||
"integrity": "sha512-ECA75gnyaxcyIukgyO2NGT3XdbLReNl/pTKrrkRfDc6pVqNtdptwwfx9KXrIMOfsO4B3m84eF3wZ9GgnebiZ4w==",
|
"integrity": "sha512-u/c42RuAaxCeEuRCAwK2+/SfGqKOd0NSyRzEvDwFBWySQoKUZQyb9OmmJSWJBbOP1OfaU2OsDrjbPbghE1l/YQ==",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"fastify-plugin": "^4.0.0",
|
"fastify-plugin": "^4.0.0",
|
||||||
"safe-stable-stringify": "^2.3.1"
|
"safe-stable-stringify": "^2.3.1"
|
||||||
@@ -3298,6 +3301,149 @@
|
|||||||
"resolved": "https://registry.npmjs.org/@octokit/webhooks-types/-/webhooks-types-7.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/@octokit/webhooks-types/-/webhooks-types-7.1.0.tgz",
|
||||||
"integrity": "sha512-y92CpG4kFFtBBjni8LHoV12IegJ+KFxLgKRengrVjKmGE5XMeCuGvlfRe75lTRrgXaG6XIWJlFpIDTlkoJsU8w=="
|
"integrity": "sha512-y92CpG4kFFtBBjni8LHoV12IegJ+KFxLgKRengrVjKmGE5XMeCuGvlfRe75lTRrgXaG6XIWJlFpIDTlkoJsU8w=="
|
||||||
},
|
},
|
||||||
|
"node_modules/@peculiar/asn1-cms": {
|
||||||
|
"version": "2.3.8",
|
||||||
|
"resolved": "https://registry.npmjs.org/@peculiar/asn1-cms/-/asn1-cms-2.3.8.tgz",
|
||||||
|
"integrity": "sha512-Wtk9R7yQxGaIaawHorWKP2OOOm/RZzamOmSWwaqGphIuU6TcKYih0slL6asZlSSZtVoYTrBfrddSOD/jTu9vuQ==",
|
||||||
|
"dependencies": {
|
||||||
|
"@peculiar/asn1-schema": "^2.3.8",
|
||||||
|
"@peculiar/asn1-x509": "^2.3.8",
|
||||||
|
"@peculiar/asn1-x509-attr": "^2.3.8",
|
||||||
|
"asn1js": "^3.0.5",
|
||||||
|
"tslib": "^2.6.2"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@peculiar/asn1-csr": {
|
||||||
|
"version": "2.3.8",
|
||||||
|
"resolved": "https://registry.npmjs.org/@peculiar/asn1-csr/-/asn1-csr-2.3.8.tgz",
|
||||||
|
"integrity": "sha512-ZmAaP2hfzgIGdMLcot8gHTykzoI+X/S53x1xoGbTmratETIaAbSWMiPGvZmXRA0SNEIydpMkzYtq4fQBxN1u1w==",
|
||||||
|
"dependencies": {
|
||||||
|
"@peculiar/asn1-schema": "^2.3.8",
|
||||||
|
"@peculiar/asn1-x509": "^2.3.8",
|
||||||
|
"asn1js": "^3.0.5",
|
||||||
|
"tslib": "^2.6.2"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@peculiar/asn1-ecc": {
|
||||||
|
"version": "2.3.8",
|
||||||
|
"resolved": "https://registry.npmjs.org/@peculiar/asn1-ecc/-/asn1-ecc-2.3.8.tgz",
|
||||||
|
"integrity": "sha512-Ah/Q15y3A/CtxbPibiLM/LKcMbnLTdUdLHUgdpB5f60sSvGkXzxJCu5ezGTFHogZXWNX3KSmYqilCrfdmBc6pQ==",
|
||||||
|
"dependencies": {
|
||||||
|
"@peculiar/asn1-schema": "^2.3.8",
|
||||||
|
"@peculiar/asn1-x509": "^2.3.8",
|
||||||
|
"asn1js": "^3.0.5",
|
||||||
|
"tslib": "^2.6.2"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@peculiar/asn1-pfx": {
|
||||||
|
"version": "2.3.8",
|
||||||
|
"resolved": "https://registry.npmjs.org/@peculiar/asn1-pfx/-/asn1-pfx-2.3.8.tgz",
|
||||||
|
"integrity": "sha512-XhdnCVznMmSmgy68B9pVxiZ1XkKoE1BjO4Hv+eUGiY1pM14msLsFZ3N7K46SoITIVZLq92kKkXpGiTfRjlNLyg==",
|
||||||
|
"dependencies": {
|
||||||
|
"@peculiar/asn1-cms": "^2.3.8",
|
||||||
|
"@peculiar/asn1-pkcs8": "^2.3.8",
|
||||||
|
"@peculiar/asn1-rsa": "^2.3.8",
|
||||||
|
"@peculiar/asn1-schema": "^2.3.8",
|
||||||
|
"asn1js": "^3.0.5",
|
||||||
|
"tslib": "^2.6.2"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@peculiar/asn1-pkcs8": {
|
||||||
|
"version": "2.3.8",
|
||||||
|
"resolved": "https://registry.npmjs.org/@peculiar/asn1-pkcs8/-/asn1-pkcs8-2.3.8.tgz",
|
||||||
|
"integrity": "sha512-rL8k2x59v8lZiwLRqdMMmOJ30GHt6yuHISFIuuWivWjAJjnxzZBVzMTQ72sknX5MeTSSvGwPmEFk2/N8+UztFQ==",
|
||||||
|
"dependencies": {
|
||||||
|
"@peculiar/asn1-schema": "^2.3.8",
|
||||||
|
"@peculiar/asn1-x509": "^2.3.8",
|
||||||
|
"asn1js": "^3.0.5",
|
||||||
|
"tslib": "^2.6.2"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@peculiar/asn1-pkcs9": {
|
||||||
|
"version": "2.3.8",
|
||||||
|
"resolved": "https://registry.npmjs.org/@peculiar/asn1-pkcs9/-/asn1-pkcs9-2.3.8.tgz",
|
||||||
|
"integrity": "sha512-+nONq5tcK7vm3qdY7ZKoSQGQjhJYMJbwJGbXLFOhmqsFIxEWyQPHyV99+wshOjpOjg0wUSSkEEzX2hx5P6EKeQ==",
|
||||||
|
"dependencies": {
|
||||||
|
"@peculiar/asn1-cms": "^2.3.8",
|
||||||
|
"@peculiar/asn1-pfx": "^2.3.8",
|
||||||
|
"@peculiar/asn1-pkcs8": "^2.3.8",
|
||||||
|
"@peculiar/asn1-schema": "^2.3.8",
|
||||||
|
"@peculiar/asn1-x509": "^2.3.8",
|
||||||
|
"@peculiar/asn1-x509-attr": "^2.3.8",
|
||||||
|
"asn1js": "^3.0.5",
|
||||||
|
"tslib": "^2.6.2"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@peculiar/asn1-rsa": {
|
||||||
|
"version": "2.3.8",
|
||||||
|
"resolved": "https://registry.npmjs.org/@peculiar/asn1-rsa/-/asn1-rsa-2.3.8.tgz",
|
||||||
|
"integrity": "sha512-ES/RVEHu8VMYXgrg3gjb1m/XG0KJWnV4qyZZ7mAg7rrF3VTmRbLxO8mk+uy0Hme7geSMebp+Wvi2U6RLLEs12Q==",
|
||||||
|
"dependencies": {
|
||||||
|
"@peculiar/asn1-schema": "^2.3.8",
|
||||||
|
"@peculiar/asn1-x509": "^2.3.8",
|
||||||
|
"asn1js": "^3.0.5",
|
||||||
|
"tslib": "^2.6.2"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@peculiar/asn1-schema": {
|
||||||
|
"version": "2.3.8",
|
||||||
|
"resolved": "https://registry.npmjs.org/@peculiar/asn1-schema/-/asn1-schema-2.3.8.tgz",
|
||||||
|
"integrity": "sha512-ULB1XqHKx1WBU/tTFIA+uARuRoBVZ4pNdOA878RDrRbBfBGcSzi5HBkdScC6ZbHn8z7L8gmKCgPC1LHRrP46tA==",
|
||||||
|
"dependencies": {
|
||||||
|
"asn1js": "^3.0.5",
|
||||||
|
"pvtsutils": "^1.3.5",
|
||||||
|
"tslib": "^2.6.2"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@peculiar/asn1-x509": {
|
||||||
|
"version": "2.3.8",
|
||||||
|
"resolved": "https://registry.npmjs.org/@peculiar/asn1-x509/-/asn1-x509-2.3.8.tgz",
|
||||||
|
"integrity": "sha512-voKxGfDU1c6r9mKiN5ZUsZWh3Dy1BABvTM3cimf0tztNwyMJPhiXY94eRTgsMQe6ViLfT6EoXxkWVzcm3mFAFw==",
|
||||||
|
"dependencies": {
|
||||||
|
"@peculiar/asn1-schema": "^2.3.8",
|
||||||
|
"asn1js": "^3.0.5",
|
||||||
|
"ipaddr.js": "^2.1.0",
|
||||||
|
"pvtsutils": "^1.3.5",
|
||||||
|
"tslib": "^2.6.2"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@peculiar/asn1-x509-attr": {
|
||||||
|
"version": "2.3.8",
|
||||||
|
"resolved": "https://registry.npmjs.org/@peculiar/asn1-x509-attr/-/asn1-x509-attr-2.3.8.tgz",
|
||||||
|
"integrity": "sha512-4Z8mSN95MOuX04Aku9BUyMdsMKtVQUqWnr627IheiWnwFoheUhX3R4Y2zh23M7m80r4/WG8MOAckRKc77IRv6g==",
|
||||||
|
"dependencies": {
|
||||||
|
"@peculiar/asn1-schema": "^2.3.8",
|
||||||
|
"@peculiar/asn1-x509": "^2.3.8",
|
||||||
|
"asn1js": "^3.0.5",
|
||||||
|
"tslib": "^2.6.2"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@peculiar/asn1-x509/node_modules/ipaddr.js": {
|
||||||
|
"version": "2.2.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-2.2.0.tgz",
|
||||||
|
"integrity": "sha512-Ag3wB2o37wslZS19hZqorUnrnzSkpOVy+IiiDEiTqNubEYpYuHWIf6K4psgN2ZWKExS4xhVCrRVfb/wfW8fWJA==",
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 10"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@peculiar/x509": {
|
||||||
|
"version": "1.10.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@peculiar/x509/-/x509-1.10.0.tgz",
|
||||||
|
"integrity": "sha512-gdH6H8gWjAYoM4Yr6wPnRbzU77nU7xq/jipqYyyv5/AHTrulN2Z5DlnOSq9jjKrB+Ya0D6YJ2cGGtwkWDK75jA==",
|
||||||
|
"dependencies": {
|
||||||
|
"@peculiar/asn1-cms": "^2.3.8",
|
||||||
|
"@peculiar/asn1-csr": "^2.3.8",
|
||||||
|
"@peculiar/asn1-ecc": "^2.3.8",
|
||||||
|
"@peculiar/asn1-pkcs9": "^2.3.8",
|
||||||
|
"@peculiar/asn1-rsa": "^2.3.8",
|
||||||
|
"@peculiar/asn1-schema": "^2.3.8",
|
||||||
|
"@peculiar/asn1-x509": "^2.3.8",
|
||||||
|
"pvtsutils": "^1.3.5",
|
||||||
|
"reflect-metadata": "^0.2.2",
|
||||||
|
"tslib": "^2.6.2",
|
||||||
|
"tsyringe": "^4.8.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@phc/format": {
|
"node_modules/@phc/format": {
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/@phc/format/-/format-1.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/@phc/format/-/format-1.0.0.tgz",
|
||||||
@@ -4806,6 +4952,11 @@
|
|||||||
"long": "*"
|
"long": "*"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@types/luxon": {
|
||||||
|
"version": "3.4.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/luxon/-/luxon-3.4.2.tgz",
|
||||||
|
"integrity": "sha512-TifLZlFudklWlMBfhubvgqTXRzLDI5pCbGa4P8a3wPyUQSW+1xQ5eDsreP9DWHX3tjq1ke96uYG/nwundroWcA=="
|
||||||
|
},
|
||||||
"node_modules/@types/mime": {
|
"node_modules/@types/mime": {
|
||||||
"version": "1.3.5",
|
"version": "1.3.5",
|
||||||
"resolved": "https://registry.npmjs.org/@types/mime/-/mime-1.3.5.tgz",
|
"resolved": "https://registry.npmjs.org/@types/mime/-/mime-1.3.5.tgz",
|
||||||
@@ -5948,6 +6099,19 @@
|
|||||||
"safer-buffer": "~2.1.0"
|
"safer-buffer": "~2.1.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/asn1js": {
|
||||||
|
"version": "3.0.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/asn1js/-/asn1js-3.0.5.tgz",
|
||||||
|
"integrity": "sha512-FVnvrKJwpt9LP2lAMl8qZswRNm3T4q9CON+bxldk2iwk3FFpuwhx2FfinyitizWHsVYyaY+y5JzDR0rCMV5yTQ==",
|
||||||
|
"dependencies": {
|
||||||
|
"pvtsutils": "^1.3.2",
|
||||||
|
"pvutils": "^1.1.3",
|
||||||
|
"tslib": "^2.4.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/assert-plus": {
|
"node_modules/assert-plus": {
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz",
|
||||||
@@ -6689,6 +6853,15 @@
|
|||||||
"integrity": "sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==",
|
"integrity": "sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
|
"node_modules/cron": {
|
||||||
|
"version": "3.1.7",
|
||||||
|
"resolved": "https://registry.npmjs.org/cron/-/cron-3.1.7.tgz",
|
||||||
|
"integrity": "sha512-tlBg7ARsAMQLzgwqVxy8AZl/qlTc5nibqYwtNGoCrd+cV+ugI+tvZC1oT/8dFH8W455YrywGykx/KMmAqOr7Jw==",
|
||||||
|
"dependencies": {
|
||||||
|
"@types/luxon": "~3.4.0",
|
||||||
|
"luxon": "~3.4.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/cron-parser": {
|
"node_modules/cron-parser": {
|
||||||
"version": "4.9.0",
|
"version": "4.9.0",
|
||||||
"resolved": "https://registry.npmjs.org/cron-parser/-/cron-parser-4.9.0.tgz",
|
"resolved": "https://registry.npmjs.org/cron-parser/-/cron-parser-4.9.0.tgz",
|
||||||
@@ -11702,6 +11875,22 @@
|
|||||||
"node": ">=6"
|
"node": ">=6"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/pvtsutils": {
|
||||||
|
"version": "1.3.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/pvtsutils/-/pvtsutils-1.3.5.tgz",
|
||||||
|
"integrity": "sha512-ARvb14YB9Nm2Xi6nBq1ZX6dAM0FsJnuk+31aUp4TrcZEdKUlSqOqsxJHUPJDNE3qiIp+iUPEIeR6Je/tgV7zsA==",
|
||||||
|
"dependencies": {
|
||||||
|
"tslib": "^2.6.1"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/pvutils": {
|
||||||
|
"version": "1.1.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/pvutils/-/pvutils-1.1.3.tgz",
|
||||||
|
"integrity": "sha512-pMpnA0qRdFp32b1sJl1wOJNxZLQ2cbQx+k6tjNtZ8CpvVhNqEPRgivZ2WOUev2YMajecdH7ctUPDvEe87nariQ==",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=6.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/qs": {
|
"node_modules/qs": {
|
||||||
"version": "6.11.0",
|
"version": "6.11.0",
|
||||||
"resolved": "https://registry.npmjs.org/qs/-/qs-6.11.0.tgz",
|
"resolved": "https://registry.npmjs.org/qs/-/qs-6.11.0.tgz",
|
||||||
@@ -11883,6 +12072,11 @@
|
|||||||
"node": ">=4"
|
"node": ">=4"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/reflect-metadata": {
|
||||||
|
"version": "0.2.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/reflect-metadata/-/reflect-metadata-0.2.2.tgz",
|
||||||
|
"integrity": "sha512-urBwgfrvVP/eAyXx4hluJivBKzuEbSQs9rKWCrCkbSxNv8mxPcUZKeuoF3Uy4mJl3Lwprp6yy5/39VWigZ4K6Q=="
|
||||||
|
},
|
||||||
"node_modules/regexp.prototype.flags": {
|
"node_modules/regexp.prototype.flags": {
|
||||||
"version": "1.5.1",
|
"version": "1.5.1",
|
||||||
"resolved": "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.5.1.tgz",
|
"resolved": "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.5.1.tgz",
|
||||||
@@ -13666,6 +13860,22 @@
|
|||||||
"fsevents": "~2.3.3"
|
"fsevents": "~2.3.3"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/tsyringe": {
|
||||||
|
"version": "4.8.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/tsyringe/-/tsyringe-4.8.0.tgz",
|
||||||
|
"integrity": "sha512-YB1FG+axdxADa3ncEtRnQCFq/M0lALGLxSZeVNbTU8NqhOVc51nnv2CISTcvc1kyv6EGPtXVr0v6lWeDxiijOA==",
|
||||||
|
"dependencies": {
|
||||||
|
"tslib": "^1.9.3"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 6.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/tsyringe/node_modules/tslib": {
|
||||||
|
"version": "1.14.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz",
|
||||||
|
"integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg=="
|
||||||
|
},
|
||||||
"node_modules/tweetnacl": {
|
"node_modules/tweetnacl": {
|
||||||
"version": "1.0.3",
|
"version": "1.0.3",
|
||||||
"resolved": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-1.0.3.tgz",
|
"resolved": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-1.0.3.tgz",
|
||||||
|
@@ -86,6 +86,8 @@
|
|||||||
"@node-saml/passport-saml": "^4.0.4",
|
"@node-saml/passport-saml": "^4.0.4",
|
||||||
"@octokit/rest": "^20.0.2",
|
"@octokit/rest": "^20.0.2",
|
||||||
"@octokit/webhooks-types": "^7.3.1",
|
"@octokit/webhooks-types": "^7.3.1",
|
||||||
|
"@peculiar/asn1-schema": "^2.3.8",
|
||||||
|
"@peculiar/x509": "^1.10.0",
|
||||||
"@serdnam/pino-cloudwatch-transport": "^1.0.4",
|
"@serdnam/pino-cloudwatch-transport": "^1.0.4",
|
||||||
"@sindresorhus/slugify": "^2.2.1",
|
"@sindresorhus/slugify": "^2.2.1",
|
||||||
"@ucast/mongo2js": "^1.3.4",
|
"@ucast/mongo2js": "^1.3.4",
|
||||||
@@ -97,6 +99,7 @@
|
|||||||
"bcrypt": "^5.1.1",
|
"bcrypt": "^5.1.1",
|
||||||
"bullmq": "^5.4.2",
|
"bullmq": "^5.4.2",
|
||||||
"cassandra-driver": "^4.7.2",
|
"cassandra-driver": "^4.7.2",
|
||||||
|
"cron": "^3.1.7",
|
||||||
"dotenv": "^16.4.1",
|
"dotenv": "^16.4.1",
|
||||||
"fastify": "^4.26.0",
|
"fastify": "^4.26.0",
|
||||||
"fastify-plugin": "^4.5.1",
|
"fastify-plugin": "^4.5.1",
|
||||||
|
8
backend/src/@types/fastify.d.ts
vendored
8
backend/src/@types/fastify.d.ts
vendored
@@ -6,6 +6,7 @@ import { TAccessApprovalRequestServiceFactory } from "@app/ee/services/access-ap
|
|||||||
import { TAuditLogServiceFactory } from "@app/ee/services/audit-log/audit-log-service";
|
import { TAuditLogServiceFactory } from "@app/ee/services/audit-log/audit-log-service";
|
||||||
import { TCreateAuditLogDTO } from "@app/ee/services/audit-log/audit-log-types";
|
import { TCreateAuditLogDTO } from "@app/ee/services/audit-log/audit-log-types";
|
||||||
import { TAuditLogStreamServiceFactory } from "@app/ee/services/audit-log-stream/audit-log-stream-service";
|
import { TAuditLogStreamServiceFactory } from "@app/ee/services/audit-log-stream/audit-log-stream-service";
|
||||||
|
import { TCertificateAuthorityCrlServiceFactory } from "@app/ee/services/certificate-authority-crl/certificate-authority-crl-service";
|
||||||
import { TDynamicSecretServiceFactory } from "@app/ee/services/dynamic-secret/dynamic-secret-service";
|
import { TDynamicSecretServiceFactory } from "@app/ee/services/dynamic-secret/dynamic-secret-service";
|
||||||
import { TDynamicSecretLeaseServiceFactory } from "@app/ee/services/dynamic-secret-lease/dynamic-secret-lease-service";
|
import { TDynamicSecretLeaseServiceFactory } from "@app/ee/services/dynamic-secret-lease/dynamic-secret-lease-service";
|
||||||
import { TGroupServiceFactory } from "@app/ee/services/group/group-service";
|
import { TGroupServiceFactory } from "@app/ee/services/group/group-service";
|
||||||
@@ -14,6 +15,7 @@ import { TLdapConfigServiceFactory } from "@app/ee/services/ldap-config/ldap-con
|
|||||||
import { TLicenseServiceFactory } from "@app/ee/services/license/license-service";
|
import { TLicenseServiceFactory } from "@app/ee/services/license/license-service";
|
||||||
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service";
|
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service";
|
||||||
import { TProjectUserAdditionalPrivilegeServiceFactory } from "@app/ee/services/project-user-additional-privilege/project-user-additional-privilege-service";
|
import { TProjectUserAdditionalPrivilegeServiceFactory } from "@app/ee/services/project-user-additional-privilege/project-user-additional-privilege-service";
|
||||||
|
import { TRateLimitServiceFactory } from "@app/ee/services/rate-limit/rate-limit-service";
|
||||||
import { TSamlConfigServiceFactory } from "@app/ee/services/saml-config/saml-config-service";
|
import { TSamlConfigServiceFactory } from "@app/ee/services/saml-config/saml-config-service";
|
||||||
import { TScimServiceFactory } from "@app/ee/services/scim/scim-service";
|
import { TScimServiceFactory } from "@app/ee/services/scim/scim-service";
|
||||||
import { TSecretApprovalPolicyServiceFactory } from "@app/ee/services/secret-approval-policy/secret-approval-policy-service";
|
import { TSecretApprovalPolicyServiceFactory } from "@app/ee/services/secret-approval-policy/secret-approval-policy-service";
|
||||||
@@ -29,6 +31,8 @@ import { TAuthPasswordFactory } from "@app/services/auth/auth-password-service";
|
|||||||
import { TAuthSignupFactory } from "@app/services/auth/auth-signup-service";
|
import { TAuthSignupFactory } from "@app/services/auth/auth-signup-service";
|
||||||
import { ActorAuthMethod, ActorType } from "@app/services/auth/auth-type";
|
import { ActorAuthMethod, ActorType } from "@app/services/auth/auth-type";
|
||||||
import { TAuthTokenServiceFactory } from "@app/services/auth-token/auth-token-service";
|
import { TAuthTokenServiceFactory } from "@app/services/auth-token/auth-token-service";
|
||||||
|
import { TCertificateServiceFactory } from "@app/services/certificate/certificate-service";
|
||||||
|
import { TCertificateAuthorityServiceFactory } from "@app/services/certificate-authority/certificate-authority-service";
|
||||||
import { TGroupProjectServiceFactory } from "@app/services/group-project/group-project-service";
|
import { TGroupProjectServiceFactory } from "@app/services/group-project/group-project-service";
|
||||||
import { TIdentityServiceFactory } from "@app/services/identity/identity-service";
|
import { TIdentityServiceFactory } from "@app/services/identity/identity-service";
|
||||||
import { TIdentityAccessTokenServiceFactory } from "@app/services/identity-access-token/identity-access-token-service";
|
import { TIdentityAccessTokenServiceFactory } from "@app/services/identity-access-token/identity-access-token-service";
|
||||||
@@ -137,6 +141,9 @@ declare module "fastify" {
|
|||||||
ldap: TLdapConfigServiceFactory;
|
ldap: TLdapConfigServiceFactory;
|
||||||
auditLog: TAuditLogServiceFactory;
|
auditLog: TAuditLogServiceFactory;
|
||||||
auditLogStream: TAuditLogStreamServiceFactory;
|
auditLogStream: TAuditLogStreamServiceFactory;
|
||||||
|
certificate: TCertificateServiceFactory;
|
||||||
|
certificateAuthority: TCertificateAuthorityServiceFactory;
|
||||||
|
certificateAuthorityCrl: TCertificateAuthorityCrlServiceFactory;
|
||||||
secretScanning: TSecretScanningServiceFactory;
|
secretScanning: TSecretScanningServiceFactory;
|
||||||
license: TLicenseServiceFactory;
|
license: TLicenseServiceFactory;
|
||||||
trustedIp: TTrustedIpServiceFactory;
|
trustedIp: TTrustedIpServiceFactory;
|
||||||
@@ -147,6 +154,7 @@ declare module "fastify" {
|
|||||||
projectUserAdditionalPrivilege: TProjectUserAdditionalPrivilegeServiceFactory;
|
projectUserAdditionalPrivilege: TProjectUserAdditionalPrivilegeServiceFactory;
|
||||||
identityProjectAdditionalPrivilege: TIdentityProjectAdditionalPrivilegeServiceFactory;
|
identityProjectAdditionalPrivilege: TIdentityProjectAdditionalPrivilegeServiceFactory;
|
||||||
secretSharing: TSecretSharingServiceFactory;
|
secretSharing: TSecretSharingServiceFactory;
|
||||||
|
rateLimit: TRateLimitServiceFactory;
|
||||||
};
|
};
|
||||||
// this is exclusive use for middlewares in which we need to inject data
|
// this is exclusive use for middlewares in which we need to inject data
|
||||||
// everywhere else access using service layer
|
// everywhere else access using service layer
|
||||||
|
56
backend/src/@types/knex.d.ts
vendored
56
backend/src/@types/knex.d.ts
vendored
@@ -32,6 +32,27 @@ import {
|
|||||||
TBackupPrivateKey,
|
TBackupPrivateKey,
|
||||||
TBackupPrivateKeyInsert,
|
TBackupPrivateKeyInsert,
|
||||||
TBackupPrivateKeyUpdate,
|
TBackupPrivateKeyUpdate,
|
||||||
|
TCertificateAuthorities,
|
||||||
|
TCertificateAuthoritiesInsert,
|
||||||
|
TCertificateAuthoritiesUpdate,
|
||||||
|
TCertificateAuthorityCerts,
|
||||||
|
TCertificateAuthorityCertsInsert,
|
||||||
|
TCertificateAuthorityCertsUpdate,
|
||||||
|
TCertificateAuthorityCrl,
|
||||||
|
TCertificateAuthorityCrlInsert,
|
||||||
|
TCertificateAuthorityCrlUpdate,
|
||||||
|
TCertificateAuthoritySecret,
|
||||||
|
TCertificateAuthoritySecretInsert,
|
||||||
|
TCertificateAuthoritySecretUpdate,
|
||||||
|
TCertificateBodies,
|
||||||
|
TCertificateBodiesInsert,
|
||||||
|
TCertificateBodiesUpdate,
|
||||||
|
TCertificates,
|
||||||
|
TCertificateSecrets,
|
||||||
|
TCertificateSecretsInsert,
|
||||||
|
TCertificateSecretsUpdate,
|
||||||
|
TCertificatesInsert,
|
||||||
|
TCertificatesUpdate,
|
||||||
TDynamicSecretLeases,
|
TDynamicSecretLeases,
|
||||||
TDynamicSecretLeasesInsert,
|
TDynamicSecretLeasesInsert,
|
||||||
TDynamicSecretLeasesUpdate,
|
TDynamicSecretLeasesUpdate,
|
||||||
@@ -149,6 +170,9 @@ import {
|
|||||||
TProjectUserMembershipRoles,
|
TProjectUserMembershipRoles,
|
||||||
TProjectUserMembershipRolesInsert,
|
TProjectUserMembershipRolesInsert,
|
||||||
TProjectUserMembershipRolesUpdate,
|
TProjectUserMembershipRolesUpdate,
|
||||||
|
TRateLimit,
|
||||||
|
TRateLimitInsert,
|
||||||
|
TRateLimitUpdate,
|
||||||
TSamlConfigs,
|
TSamlConfigs,
|
||||||
TSamlConfigsInsert,
|
TSamlConfigsInsert,
|
||||||
TSamlConfigsUpdate,
|
TSamlConfigsUpdate,
|
||||||
@@ -257,6 +281,37 @@ declare module "knex/types/tables" {
|
|||||||
interface Tables {
|
interface Tables {
|
||||||
[TableName.Users]: Knex.CompositeTableType<TUsers, TUsersInsert, TUsersUpdate>;
|
[TableName.Users]: Knex.CompositeTableType<TUsers, TUsersInsert, TUsersUpdate>;
|
||||||
[TableName.Groups]: Knex.CompositeTableType<TGroups, TGroupsInsert, TGroupsUpdate>;
|
[TableName.Groups]: Knex.CompositeTableType<TGroups, TGroupsInsert, TGroupsUpdate>;
|
||||||
|
[TableName.CertificateAuthority]: Knex.CompositeTableType<
|
||||||
|
TCertificateAuthorities,
|
||||||
|
TCertificateAuthoritiesInsert,
|
||||||
|
TCertificateAuthoritiesUpdate
|
||||||
|
>;
|
||||||
|
[TableName.CertificateAuthorityCert]: Knex.CompositeTableType<
|
||||||
|
TCertificateAuthorityCerts,
|
||||||
|
TCertificateAuthorityCertsInsert,
|
||||||
|
TCertificateAuthorityCertsUpdate
|
||||||
|
>;
|
||||||
|
[TableName.CertificateAuthoritySecret]: Knex.CompositeTableType<
|
||||||
|
TCertificateAuthoritySecret,
|
||||||
|
TCertificateAuthoritySecretInsert,
|
||||||
|
TCertificateAuthoritySecretUpdate
|
||||||
|
>;
|
||||||
|
[TableName.CertificateAuthorityCrl]: Knex.CompositeTableType<
|
||||||
|
TCertificateAuthorityCrl,
|
||||||
|
TCertificateAuthorityCrlInsert,
|
||||||
|
TCertificateAuthorityCrlUpdate
|
||||||
|
>;
|
||||||
|
[TableName.Certificate]: Knex.CompositeTableType<TCertificates, TCertificatesInsert, TCertificatesUpdate>;
|
||||||
|
[TableName.CertificateBody]: Knex.CompositeTableType<
|
||||||
|
TCertificateBodies,
|
||||||
|
TCertificateBodiesInsert,
|
||||||
|
TCertificateBodiesUpdate
|
||||||
|
>;
|
||||||
|
[TableName.CertificateSecret]: Knex.CompositeTableType<
|
||||||
|
TCertificateSecrets,
|
||||||
|
TCertificateSecretsInsert,
|
||||||
|
TCertificateSecretsUpdate
|
||||||
|
>;
|
||||||
[TableName.UserGroupMembership]: Knex.CompositeTableType<
|
[TableName.UserGroupMembership]: Knex.CompositeTableType<
|
||||||
TUserGroupMembership,
|
TUserGroupMembership,
|
||||||
TUserGroupMembershipInsert,
|
TUserGroupMembershipInsert,
|
||||||
@@ -343,6 +398,7 @@ declare module "knex/types/tables" {
|
|||||||
TSecretFolderVersionsUpdate
|
TSecretFolderVersionsUpdate
|
||||||
>;
|
>;
|
||||||
[TableName.SecretSharing]: Knex.CompositeTableType<TSecretSharing, TSecretSharingInsert, TSecretSharingUpdate>;
|
[TableName.SecretSharing]: Knex.CompositeTableType<TSecretSharing, TSecretSharingInsert, TSecretSharingUpdate>;
|
||||||
|
[TableName.RateLimit]: Knex.CompositeTableType<TRateLimit, TRateLimitInsert, TRateLimitUpdate>;
|
||||||
[TableName.SecretTag]: Knex.CompositeTableType<TSecretTags, TSecretTagsInsert, TSecretTagsUpdate>;
|
[TableName.SecretTag]: Knex.CompositeTableType<TSecretTags, TSecretTagsInsert, TSecretTagsUpdate>;
|
||||||
[TableName.SecretImport]: Knex.CompositeTableType<TSecretImports, TSecretImportsInsert, TSecretImportsUpdate>;
|
[TableName.SecretImport]: Knex.CompositeTableType<TSecretImports, TSecretImportsInsert, TSecretImportsUpdate>;
|
||||||
[TableName.Integration]: Knex.CompositeTableType<TIntegrations, TIntegrationsInsert, TIntegrationsUpdate>;
|
[TableName.Integration]: Knex.CompositeTableType<TIntegrations, TIntegrationsInsert, TIntegrationsUpdate>;
|
||||||
|
@@ -0,0 +1,21 @@
|
|||||||
|
import { Knex } from "knex";
|
||||||
|
|
||||||
|
import { TableName } from "../schemas";
|
||||||
|
|
||||||
|
export async function up(knex: Knex): Promise<void> {
|
||||||
|
const hasPitVersionLimitColumn = await knex.schema.hasColumn(TableName.Project, "pitVersionLimit");
|
||||||
|
await knex.schema.alterTable(TableName.Project, (tb) => {
|
||||||
|
if (!hasPitVersionLimitColumn) {
|
||||||
|
tb.integer("pitVersionLimit").notNullable().defaultTo(10);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function down(knex: Knex): Promise<void> {
|
||||||
|
const hasPitVersionLimitColumn = await knex.schema.hasColumn(TableName.Project, "pitVersionLimit");
|
||||||
|
await knex.schema.alterTable(TableName.Project, (tb) => {
|
||||||
|
if (hasPitVersionLimitColumn) {
|
||||||
|
tb.dropColumn("pitVersionLimit");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
@@ -0,0 +1,31 @@
|
|||||||
|
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.RateLimit))) {
|
||||||
|
await knex.schema.createTable(TableName.RateLimit, (t) => {
|
||||||
|
t.uuid("id", { primaryKey: true }).defaultTo(knex.fn.uuid());
|
||||||
|
t.integer("readRateLimit").defaultTo(600).notNullable();
|
||||||
|
t.integer("writeRateLimit").defaultTo(200).notNullable();
|
||||||
|
t.integer("secretsRateLimit").defaultTo(60).notNullable();
|
||||||
|
t.integer("authRateLimit").defaultTo(60).notNullable();
|
||||||
|
t.integer("inviteUserRateLimit").defaultTo(30).notNullable();
|
||||||
|
t.integer("mfaRateLimit").defaultTo(20).notNullable();
|
||||||
|
t.integer("creationLimit").defaultTo(30).notNullable();
|
||||||
|
t.integer("publicEndpointLimit").defaultTo(30).notNullable();
|
||||||
|
t.timestamps(true, true, true);
|
||||||
|
});
|
||||||
|
|
||||||
|
await createOnUpdateTrigger(knex, TableName.RateLimit);
|
||||||
|
|
||||||
|
// create init rate limit entry with defaults
|
||||||
|
await knex(TableName.RateLimit).insert({});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function down(knex: Knex): Promise<void> {
|
||||||
|
await knex.schema.dropTableIfExists(TableName.RateLimit);
|
||||||
|
await dropOnUpdateTrigger(knex, TableName.RateLimit);
|
||||||
|
}
|
@@ -0,0 +1,25 @@
|
|||||||
|
import { Knex } from "knex";
|
||||||
|
|
||||||
|
import { ActorType } from "@app/services/auth/auth-type";
|
||||||
|
|
||||||
|
import { TableName } from "../schemas";
|
||||||
|
|
||||||
|
export async function up(knex: Knex): Promise<void> {
|
||||||
|
const hasCreatedByActorType = await knex.schema.hasColumn(TableName.SecretTag, "createdByActorType");
|
||||||
|
await knex.schema.alterTable(TableName.SecretTag, (tb) => {
|
||||||
|
if (!hasCreatedByActorType) {
|
||||||
|
tb.string("createdByActorType").notNullable().defaultTo(ActorType.USER);
|
||||||
|
tb.dropForeign("createdBy");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function down(knex: Knex): Promise<void> {
|
||||||
|
const hasCreatedByActorType = await knex.schema.hasColumn(TableName.SecretTag, "createdByActorType");
|
||||||
|
await knex.schema.alterTable(TableName.SecretTag, (tb) => {
|
||||||
|
if (hasCreatedByActorType) {
|
||||||
|
tb.dropColumn("createdByActorType");
|
||||||
|
tb.foreign("createdBy").references("id").inTable(TableName.Users).onDelete("SET NULL");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
137
backend/src/db/migrations/20240614154212_certificate-mgmt.ts
Normal file
137
backend/src/db/migrations/20240614154212_certificate-mgmt.ts
Normal file
@@ -0,0 +1,137 @@
|
|||||||
|
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.Project)) {
|
||||||
|
const doesProjectCertificateKeyIdExist = await knex.schema.hasColumn(TableName.Project, "kmsCertificateKeyId");
|
||||||
|
await knex.schema.alterTable(TableName.Project, (t) => {
|
||||||
|
if (!doesProjectCertificateKeyIdExist) {
|
||||||
|
t.uuid("kmsCertificateKeyId").nullable();
|
||||||
|
t.foreign("kmsCertificateKeyId").references("id").inTable(TableName.KmsKey);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!(await knex.schema.hasTable(TableName.CertificateAuthority))) {
|
||||||
|
await knex.schema.createTable(TableName.CertificateAuthority, (t) => {
|
||||||
|
t.uuid("id", { primaryKey: true }).defaultTo(knex.fn.uuid());
|
||||||
|
t.timestamps(true, true, true);
|
||||||
|
t.uuid("parentCaId").nullable();
|
||||||
|
t.foreign("parentCaId").references("id").inTable(TableName.CertificateAuthority).onDelete("CASCADE");
|
||||||
|
t.string("projectId").notNullable();
|
||||||
|
t.foreign("projectId").references("id").inTable(TableName.Project).onDelete("CASCADE");
|
||||||
|
t.string("type").notNullable(); // root / intermediate
|
||||||
|
t.string("status").notNullable(); // active / pending-certificate
|
||||||
|
t.string("friendlyName").notNullable();
|
||||||
|
t.string("organization").notNullable();
|
||||||
|
t.string("ou").notNullable();
|
||||||
|
t.string("country").notNullable();
|
||||||
|
t.string("province").notNullable();
|
||||||
|
t.string("locality").notNullable();
|
||||||
|
t.string("commonName").notNullable();
|
||||||
|
t.string("dn").notNullable();
|
||||||
|
t.string("serialNumber").nullable().unique();
|
||||||
|
t.integer("maxPathLength").nullable();
|
||||||
|
t.string("keyAlgorithm").notNullable();
|
||||||
|
t.datetime("notBefore").nullable();
|
||||||
|
t.datetime("notAfter").nullable();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!(await knex.schema.hasTable(TableName.CertificateAuthorityCert))) {
|
||||||
|
// table to keep track of certificates belonging to CA
|
||||||
|
await knex.schema.createTable(TableName.CertificateAuthorityCert, (t) => {
|
||||||
|
t.uuid("id", { primaryKey: true }).defaultTo(knex.fn.uuid());
|
||||||
|
t.timestamps(true, true, true);
|
||||||
|
t.uuid("caId").notNullable().unique();
|
||||||
|
t.foreign("caId").references("id").inTable(TableName.CertificateAuthority).onDelete("CASCADE");
|
||||||
|
t.binary("encryptedCertificate").notNullable();
|
||||||
|
t.binary("encryptedCertificateChain").notNullable();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!(await knex.schema.hasTable(TableName.CertificateAuthoritySecret))) {
|
||||||
|
await knex.schema.createTable(TableName.CertificateAuthoritySecret, (t) => {
|
||||||
|
t.uuid("id", { primaryKey: true }).defaultTo(knex.fn.uuid());
|
||||||
|
t.timestamps(true, true, true);
|
||||||
|
t.uuid("caId").notNullable().unique();
|
||||||
|
t.foreign("caId").references("id").inTable(TableName.CertificateAuthority).onDelete("CASCADE");
|
||||||
|
t.binary("encryptedPrivateKey").notNullable();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!(await knex.schema.hasTable(TableName.CertificateAuthorityCrl))) {
|
||||||
|
await knex.schema.createTable(TableName.CertificateAuthorityCrl, (t) => {
|
||||||
|
t.uuid("id", { primaryKey: true }).defaultTo(knex.fn.uuid());
|
||||||
|
t.timestamps(true, true, true);
|
||||||
|
t.uuid("caId").notNullable().unique();
|
||||||
|
t.foreign("caId").references("id").inTable(TableName.CertificateAuthority).onDelete("CASCADE");
|
||||||
|
t.binary("encryptedCrl").notNullable();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!(await knex.schema.hasTable(TableName.Certificate))) {
|
||||||
|
await knex.schema.createTable(TableName.Certificate, (t) => {
|
||||||
|
t.uuid("id", { primaryKey: true }).defaultTo(knex.fn.uuid());
|
||||||
|
t.timestamps(true, true, true);
|
||||||
|
t.uuid("caId").notNullable();
|
||||||
|
t.foreign("caId").references("id").inTable(TableName.CertificateAuthority).onDelete("CASCADE");
|
||||||
|
t.string("status").notNullable(); // active / pending-certificate
|
||||||
|
t.string("serialNumber").notNullable().unique();
|
||||||
|
t.string("friendlyName").notNullable();
|
||||||
|
t.string("commonName").notNullable();
|
||||||
|
t.datetime("notBefore").notNullable();
|
||||||
|
t.datetime("notAfter").notNullable();
|
||||||
|
t.datetime("revokedAt").nullable();
|
||||||
|
t.integer("revocationReason").nullable(); // integer based on crl reason in RFC 5280
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!(await knex.schema.hasTable(TableName.CertificateBody))) {
|
||||||
|
await knex.schema.createTable(TableName.CertificateBody, (t) => {
|
||||||
|
t.uuid("id", { primaryKey: true }).defaultTo(knex.fn.uuid());
|
||||||
|
t.timestamps(true, true, true);
|
||||||
|
t.uuid("certId").notNullable().unique();
|
||||||
|
t.foreign("certId").references("id").inTable(TableName.Certificate).onDelete("CASCADE");
|
||||||
|
t.binary("encryptedCertificate").notNullable();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
await createOnUpdateTrigger(knex, TableName.CertificateAuthority);
|
||||||
|
await createOnUpdateTrigger(knex, TableName.CertificateAuthorityCert);
|
||||||
|
await createOnUpdateTrigger(knex, TableName.CertificateAuthoritySecret);
|
||||||
|
await createOnUpdateTrigger(knex, TableName.Certificate);
|
||||||
|
await createOnUpdateTrigger(knex, TableName.CertificateBody);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function down(knex: Knex): Promise<void> {
|
||||||
|
// project
|
||||||
|
if (await knex.schema.hasTable(TableName.Project)) {
|
||||||
|
const doesProjectCertificateKeyIdExist = await knex.schema.hasColumn(TableName.Project, "kmsCertificateKeyId");
|
||||||
|
await knex.schema.alterTable(TableName.Project, (t) => {
|
||||||
|
if (doesProjectCertificateKeyIdExist) t.dropColumn("kmsCertificateKeyId");
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// certificates
|
||||||
|
await knex.schema.dropTableIfExists(TableName.CertificateBody);
|
||||||
|
await dropOnUpdateTrigger(knex, TableName.CertificateBody);
|
||||||
|
|
||||||
|
await knex.schema.dropTableIfExists(TableName.Certificate);
|
||||||
|
await dropOnUpdateTrigger(knex, TableName.Certificate);
|
||||||
|
|
||||||
|
// certificate authorities
|
||||||
|
await knex.schema.dropTableIfExists(TableName.CertificateAuthoritySecret);
|
||||||
|
await dropOnUpdateTrigger(knex, TableName.CertificateAuthoritySecret);
|
||||||
|
|
||||||
|
await knex.schema.dropTableIfExists(TableName.CertificateAuthorityCrl);
|
||||||
|
await dropOnUpdateTrigger(knex, TableName.CertificateAuthorityCrl);
|
||||||
|
|
||||||
|
await knex.schema.dropTableIfExists(TableName.CertificateAuthorityCert);
|
||||||
|
await dropOnUpdateTrigger(knex, TableName.CertificateAuthorityCert);
|
||||||
|
|
||||||
|
await knex.schema.dropTableIfExists(TableName.CertificateAuthority);
|
||||||
|
await dropOnUpdateTrigger(knex, TableName.CertificateAuthority);
|
||||||
|
}
|
@@ -0,0 +1,27 @@
|
|||||||
|
import { Knex } from "knex";
|
||||||
|
|
||||||
|
import { TableName } from "../schemas";
|
||||||
|
|
||||||
|
export async function up(knex: Knex): Promise<void> {
|
||||||
|
const hasOrgIdColumn = await knex.schema.hasColumn(TableName.SecretSharing, "orgId");
|
||||||
|
const hasUserIdColumn = await knex.schema.hasColumn(TableName.SecretSharing, "userId");
|
||||||
|
|
||||||
|
if (await knex.schema.hasTable(TableName.SecretSharing)) {
|
||||||
|
await knex.schema.alterTable(TableName.SecretSharing, (t) => {
|
||||||
|
if (hasOrgIdColumn) t.uuid("orgId").nullable().alter();
|
||||||
|
if (hasUserIdColumn) t.uuid("userId").nullable().alter();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function down(knex: Knex): Promise<void> {
|
||||||
|
const hasOrgIdColumn = await knex.schema.hasColumn(TableName.SecretSharing, "orgId");
|
||||||
|
const hasUserIdColumn = await knex.schema.hasColumn(TableName.SecretSharing, "userId");
|
||||||
|
|
||||||
|
if (await knex.schema.hasTable(TableName.SecretSharing)) {
|
||||||
|
await knex.schema.alterTable(TableName.SecretSharing, (t) => {
|
||||||
|
if (hasOrgIdColumn) t.uuid("orgId").notNullable().alter();
|
||||||
|
if (hasUserIdColumn) t.uuid("userId").notNullable().alter();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
37
backend/src/db/schemas/certificate-authorities.ts
Normal file
37
backend/src/db/schemas/certificate-authorities.ts
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
// 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 CertificateAuthoritiesSchema = z.object({
|
||||||
|
id: z.string().uuid(),
|
||||||
|
createdAt: z.date(),
|
||||||
|
updatedAt: z.date(),
|
||||||
|
parentCaId: z.string().uuid().nullable().optional(),
|
||||||
|
projectId: z.string(),
|
||||||
|
type: z.string(),
|
||||||
|
status: z.string(),
|
||||||
|
friendlyName: z.string(),
|
||||||
|
organization: z.string(),
|
||||||
|
ou: z.string(),
|
||||||
|
country: z.string(),
|
||||||
|
province: z.string(),
|
||||||
|
locality: z.string(),
|
||||||
|
commonName: z.string(),
|
||||||
|
dn: z.string(),
|
||||||
|
serialNumber: z.string().nullable().optional(),
|
||||||
|
maxPathLength: z.number().nullable().optional(),
|
||||||
|
keyAlgorithm: z.string(),
|
||||||
|
notBefore: z.date().nullable().optional(),
|
||||||
|
notAfter: z.date().nullable().optional()
|
||||||
|
});
|
||||||
|
|
||||||
|
export type TCertificateAuthorities = z.infer<typeof CertificateAuthoritiesSchema>;
|
||||||
|
export type TCertificateAuthoritiesInsert = Omit<z.input<typeof CertificateAuthoritiesSchema>, TImmutableDBKeys>;
|
||||||
|
export type TCertificateAuthoritiesUpdate = Partial<
|
||||||
|
Omit<z.input<typeof CertificateAuthoritiesSchema>, TImmutableDBKeys>
|
||||||
|
>;
|
25
backend/src/db/schemas/certificate-authority-certs.ts
Normal file
25
backend/src/db/schemas/certificate-authority-certs.ts
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
// 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 { zodBuffer } from "@app/lib/zod";
|
||||||
|
|
||||||
|
import { TImmutableDBKeys } from "./models";
|
||||||
|
|
||||||
|
export const CertificateAuthorityCertsSchema = z.object({
|
||||||
|
id: z.string().uuid(),
|
||||||
|
createdAt: z.date(),
|
||||||
|
updatedAt: z.date(),
|
||||||
|
caId: z.string().uuid(),
|
||||||
|
encryptedCertificate: zodBuffer,
|
||||||
|
encryptedCertificateChain: zodBuffer
|
||||||
|
});
|
||||||
|
|
||||||
|
export type TCertificateAuthorityCerts = z.infer<typeof CertificateAuthorityCertsSchema>;
|
||||||
|
export type TCertificateAuthorityCertsInsert = Omit<z.input<typeof CertificateAuthorityCertsSchema>, TImmutableDBKeys>;
|
||||||
|
export type TCertificateAuthorityCertsUpdate = Partial<
|
||||||
|
Omit<z.input<typeof CertificateAuthorityCertsSchema>, TImmutableDBKeys>
|
||||||
|
>;
|
24
backend/src/db/schemas/certificate-authority-crl.ts
Normal file
24
backend/src/db/schemas/certificate-authority-crl.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 { zodBuffer } from "@app/lib/zod";
|
||||||
|
|
||||||
|
import { TImmutableDBKeys } from "./models";
|
||||||
|
|
||||||
|
export const CertificateAuthorityCrlSchema = z.object({
|
||||||
|
id: z.string().uuid(),
|
||||||
|
createdAt: z.date(),
|
||||||
|
updatedAt: z.date(),
|
||||||
|
caId: z.string().uuid(),
|
||||||
|
encryptedCrl: zodBuffer
|
||||||
|
});
|
||||||
|
|
||||||
|
export type TCertificateAuthorityCrl = z.infer<typeof CertificateAuthorityCrlSchema>;
|
||||||
|
export type TCertificateAuthorityCrlInsert = Omit<z.input<typeof CertificateAuthorityCrlSchema>, TImmutableDBKeys>;
|
||||||
|
export type TCertificateAuthorityCrlUpdate = Partial<
|
||||||
|
Omit<z.input<typeof CertificateAuthorityCrlSchema>, TImmutableDBKeys>
|
||||||
|
>;
|
27
backend/src/db/schemas/certificate-authority-secret.ts
Normal file
27
backend/src/db/schemas/certificate-authority-secret.ts
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
// 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 { zodBuffer } from "@app/lib/zod";
|
||||||
|
|
||||||
|
import { TImmutableDBKeys } from "./models";
|
||||||
|
|
||||||
|
export const CertificateAuthoritySecretSchema = z.object({
|
||||||
|
id: z.string().uuid(),
|
||||||
|
createdAt: z.date(),
|
||||||
|
updatedAt: z.date(),
|
||||||
|
caId: z.string().uuid(),
|
||||||
|
encryptedPrivateKey: zodBuffer
|
||||||
|
});
|
||||||
|
|
||||||
|
export type TCertificateAuthoritySecret = z.infer<typeof CertificateAuthoritySecretSchema>;
|
||||||
|
export type TCertificateAuthoritySecretInsert = Omit<
|
||||||
|
z.input<typeof CertificateAuthoritySecretSchema>,
|
||||||
|
TImmutableDBKeys
|
||||||
|
>;
|
||||||
|
export type TCertificateAuthoritySecretUpdate = Partial<
|
||||||
|
Omit<z.input<typeof CertificateAuthoritySecretSchema>, TImmutableDBKeys>
|
||||||
|
>;
|
22
backend/src/db/schemas/certificate-bodies.ts
Normal file
22
backend/src/db/schemas/certificate-bodies.ts
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
// 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 { zodBuffer } from "@app/lib/zod";
|
||||||
|
|
||||||
|
import { TImmutableDBKeys } from "./models";
|
||||||
|
|
||||||
|
export const CertificateBodiesSchema = z.object({
|
||||||
|
id: z.string().uuid(),
|
||||||
|
createdAt: z.date(),
|
||||||
|
updatedAt: z.date(),
|
||||||
|
certId: z.string().uuid(),
|
||||||
|
encryptedCertificate: zodBuffer
|
||||||
|
});
|
||||||
|
|
||||||
|
export type TCertificateBodies = z.infer<typeof CertificateBodiesSchema>;
|
||||||
|
export type TCertificateBodiesInsert = Omit<z.input<typeof CertificateBodiesSchema>, TImmutableDBKeys>;
|
||||||
|
export type TCertificateBodiesUpdate = Partial<Omit<z.input<typeof CertificateBodiesSchema>, TImmutableDBKeys>>;
|
21
backend/src/db/schemas/certificate-secrets.ts
Normal file
21
backend/src/db/schemas/certificate-secrets.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 CertificateSecretsSchema = z.object({
|
||||||
|
id: z.string().uuid(),
|
||||||
|
createdAt: z.date(),
|
||||||
|
updatedAt: z.date(),
|
||||||
|
certId: z.string().uuid(),
|
||||||
|
pk: z.string(),
|
||||||
|
sk: z.string()
|
||||||
|
});
|
||||||
|
|
||||||
|
export type TCertificateSecrets = z.infer<typeof CertificateSecretsSchema>;
|
||||||
|
export type TCertificateSecretsInsert = Omit<z.input<typeof CertificateSecretsSchema>, TImmutableDBKeys>;
|
||||||
|
export type TCertificateSecretsUpdate = Partial<Omit<z.input<typeof CertificateSecretsSchema>, TImmutableDBKeys>>;
|
27
backend/src/db/schemas/certificates.ts
Normal file
27
backend/src/db/schemas/certificates.ts
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
// 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 CertificatesSchema = z.object({
|
||||||
|
id: z.string().uuid(),
|
||||||
|
createdAt: z.date(),
|
||||||
|
updatedAt: z.date(),
|
||||||
|
caId: z.string().uuid(),
|
||||||
|
status: z.string(),
|
||||||
|
serialNumber: z.string(),
|
||||||
|
friendlyName: z.string(),
|
||||||
|
commonName: z.string(),
|
||||||
|
notBefore: z.date(),
|
||||||
|
notAfter: z.date(),
|
||||||
|
revokedAt: z.date().nullable().optional(),
|
||||||
|
revocationReason: z.number().nullable().optional()
|
||||||
|
});
|
||||||
|
|
||||||
|
export type TCertificates = z.infer<typeof CertificatesSchema>;
|
||||||
|
export type TCertificatesInsert = Omit<z.input<typeof CertificatesSchema>, TImmutableDBKeys>;
|
||||||
|
export type TCertificatesUpdate = Partial<Omit<z.input<typeof CertificatesSchema>, TImmutableDBKeys>>;
|
@@ -8,6 +8,13 @@ export * from "./audit-logs";
|
|||||||
export * from "./auth-token-sessions";
|
export * from "./auth-token-sessions";
|
||||||
export * from "./auth-tokens";
|
export * from "./auth-tokens";
|
||||||
export * from "./backup-private-key";
|
export * from "./backup-private-key";
|
||||||
|
export * from "./certificate-authorities";
|
||||||
|
export * from "./certificate-authority-certs";
|
||||||
|
export * from "./certificate-authority-crl";
|
||||||
|
export * from "./certificate-authority-secret";
|
||||||
|
export * from "./certificate-bodies";
|
||||||
|
export * from "./certificate-secrets";
|
||||||
|
export * from "./certificates";
|
||||||
export * from "./dynamic-secret-leases";
|
export * from "./dynamic-secret-leases";
|
||||||
export * from "./dynamic-secrets";
|
export * from "./dynamic-secrets";
|
||||||
export * from "./git-app-install-sessions";
|
export * from "./git-app-install-sessions";
|
||||||
@@ -48,6 +55,7 @@ export * from "./project-roles";
|
|||||||
export * from "./project-user-additional-privilege";
|
export * from "./project-user-additional-privilege";
|
||||||
export * from "./project-user-membership-roles";
|
export * from "./project-user-membership-roles";
|
||||||
export * from "./projects";
|
export * from "./projects";
|
||||||
|
export * from "./rate-limit";
|
||||||
export * from "./saml-configs";
|
export * from "./saml-configs";
|
||||||
export * from "./scim-tokens";
|
export * from "./scim-tokens";
|
||||||
export * from "./secret-approval-policies";
|
export * from "./secret-approval-policies";
|
||||||
|
@@ -2,6 +2,13 @@ import { z } from "zod";
|
|||||||
|
|
||||||
export enum TableName {
|
export enum TableName {
|
||||||
Users = "users",
|
Users = "users",
|
||||||
|
CertificateAuthority = "certificate_authorities",
|
||||||
|
CertificateAuthorityCert = "certificate_authority_certs",
|
||||||
|
CertificateAuthoritySecret = "certificate_authority_secret",
|
||||||
|
CertificateAuthorityCrl = "certificate_authority_crl",
|
||||||
|
Certificate = "certificates",
|
||||||
|
CertificateBody = "certificate_bodies",
|
||||||
|
CertificateSecret = "certificate_secrets",
|
||||||
Groups = "groups",
|
Groups = "groups",
|
||||||
GroupProjectMembership = "group_project_memberships",
|
GroupProjectMembership = "group_project_memberships",
|
||||||
GroupProjectMembershipRole = "group_project_membership_roles",
|
GroupProjectMembershipRole = "group_project_membership_roles",
|
||||||
@@ -18,6 +25,7 @@ export enum TableName {
|
|||||||
IncidentContact = "incident_contacts",
|
IncidentContact = "incident_contacts",
|
||||||
UserAction = "user_actions",
|
UserAction = "user_actions",
|
||||||
SuperAdmin = "super_admin",
|
SuperAdmin = "super_admin",
|
||||||
|
RateLimit = "rate_limit",
|
||||||
ApiKey = "api_keys",
|
ApiKey = "api_keys",
|
||||||
Project = "projects",
|
Project = "projects",
|
||||||
ProjectBot = "project_bots",
|
ProjectBot = "project_bots",
|
||||||
|
@@ -16,7 +16,9 @@ export const ProjectsSchema = z.object({
|
|||||||
createdAt: z.date(),
|
createdAt: z.date(),
|
||||||
updatedAt: z.date(),
|
updatedAt: z.date(),
|
||||||
version: z.number().default(1),
|
version: z.number().default(1),
|
||||||
upgradeStatus: z.string().nullable().optional()
|
upgradeStatus: z.string().nullable().optional(),
|
||||||
|
kmsCertificateKeyId: z.string().uuid().nullable().optional(),
|
||||||
|
pitVersionLimit: z.number().default(10)
|
||||||
});
|
});
|
||||||
|
|
||||||
export type TProjects = z.infer<typeof ProjectsSchema>;
|
export type TProjects = z.infer<typeof ProjectsSchema>;
|
||||||
|
26
backend/src/db/schemas/rate-limit.ts
Normal file
26
backend/src/db/schemas/rate-limit.ts
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
// 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 RateLimitSchema = z.object({
|
||||||
|
id: z.string().uuid(),
|
||||||
|
readRateLimit: z.number().default(600),
|
||||||
|
writeRateLimit: z.number().default(200),
|
||||||
|
secretsRateLimit: z.number().default(60),
|
||||||
|
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()
|
||||||
|
});
|
||||||
|
|
||||||
|
export type TRateLimit = z.infer<typeof RateLimitSchema>;
|
||||||
|
export type TRateLimitInsert = Omit<z.input<typeof RateLimitSchema>, TImmutableDBKeys>;
|
||||||
|
export type TRateLimitUpdate = Partial<Omit<z.input<typeof RateLimitSchema>, TImmutableDBKeys>>;
|
@@ -14,8 +14,8 @@ export const SecretSharingSchema = z.object({
|
|||||||
tag: z.string(),
|
tag: z.string(),
|
||||||
hashedHex: z.string(),
|
hashedHex: z.string(),
|
||||||
expiresAt: z.date(),
|
expiresAt: z.date(),
|
||||||
userId: z.string().uuid(),
|
userId: z.string().uuid().nullable().optional(),
|
||||||
orgId: z.string().uuid(),
|
orgId: z.string().uuid().nullable().optional(),
|
||||||
createdAt: z.date(),
|
createdAt: z.date(),
|
||||||
updatedAt: z.date(),
|
updatedAt: z.date(),
|
||||||
expiresAfterViews: z.number().nullable().optional()
|
expiresAfterViews: z.number().nullable().optional()
|
||||||
|
@@ -15,7 +15,8 @@ export const SecretTagsSchema = z.object({
|
|||||||
createdAt: z.date(),
|
createdAt: z.date(),
|
||||||
updatedAt: z.date(),
|
updatedAt: z.date(),
|
||||||
createdBy: z.string().uuid().nullable().optional(),
|
createdBy: z.string().uuid().nullable().optional(),
|
||||||
projectId: z.string()
|
projectId: z.string(),
|
||||||
|
createdByActorType: z.string().default("user")
|
||||||
});
|
});
|
||||||
|
|
||||||
export type TSecretTags = z.infer<typeof SecretTagsSchema>;
|
export type TSecretTags = z.infer<typeof SecretTagsSchema>;
|
||||||
|
86
backend/src/ee/routes/v1/certificate-authority-crl-router.ts
Normal file
86
backend/src/ee/routes/v1/certificate-authority-crl-router.ts
Normal file
@@ -0,0 +1,86 @@
|
|||||||
|
import { z } from "zod";
|
||||||
|
|
||||||
|
import { EventType } from "@app/ee/services/audit-log/audit-log-types";
|
||||||
|
import { CERTIFICATE_AUTHORITIES } from "@app/lib/api-docs";
|
||||||
|
import { readLimit } from "@app/server/config/rateLimiter";
|
||||||
|
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
|
||||||
|
import { AuthMode } from "@app/services/auth/auth-type";
|
||||||
|
|
||||||
|
export const registerCaCrlRouter = async (server: FastifyZodProvider) => {
|
||||||
|
server.route({
|
||||||
|
method: "GET",
|
||||||
|
url: "/:caId/crl",
|
||||||
|
config: {
|
||||||
|
rateLimit: readLimit
|
||||||
|
},
|
||||||
|
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
||||||
|
schema: {
|
||||||
|
description: "Get CRL of the CA",
|
||||||
|
params: z.object({
|
||||||
|
caId: z.string().trim().describe(CERTIFICATE_AUTHORITIES.GET_CRL.caId)
|
||||||
|
}),
|
||||||
|
response: {
|
||||||
|
200: z.object({
|
||||||
|
crl: z.string().describe(CERTIFICATE_AUTHORITIES.GET_CRL.crl)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
},
|
||||||
|
handler: async (req) => {
|
||||||
|
const { crl, ca } = await server.services.certificateAuthorityCrl.getCaCrl({
|
||||||
|
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_CRL,
|
||||||
|
metadata: {
|
||||||
|
caId: ca.id,
|
||||||
|
dn: ca.dn
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
crl
|
||||||
|
};
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// server.route({
|
||||||
|
// method: "GET",
|
||||||
|
// url: "/:caId/crl/rotate",
|
||||||
|
// config: {
|
||||||
|
// rateLimit: writeLimit
|
||||||
|
// },
|
||||||
|
// onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
||||||
|
// schema: {
|
||||||
|
// description: "Rotate CRL of the CA",
|
||||||
|
// params: z.object({
|
||||||
|
// caId: z.string().trim()
|
||||||
|
// }),
|
||||||
|
// response: {
|
||||||
|
// 200: z.object({
|
||||||
|
// message: z.string()
|
||||||
|
// })
|
||||||
|
// }
|
||||||
|
// },
|
||||||
|
// handler: async (req) => {
|
||||||
|
// await server.services.certificateAuthority.rotateCaCrl({
|
||||||
|
// caId: req.params.caId,
|
||||||
|
// actor: req.permission.type,
|
||||||
|
// actorId: req.permission.id,
|
||||||
|
// actorAuthMethod: req.permission.authMethod,
|
||||||
|
// actorOrgId: req.permission.orgId
|
||||||
|
// });
|
||||||
|
// return {
|
||||||
|
// message: "Successfully rotated CA CRL"
|
||||||
|
// };
|
||||||
|
// }
|
||||||
|
// });
|
||||||
|
};
|
@@ -1,6 +1,7 @@
|
|||||||
import { registerAccessApprovalPolicyRouter } from "./access-approval-policy-router";
|
import { registerAccessApprovalPolicyRouter } from "./access-approval-policy-router";
|
||||||
import { registerAccessApprovalRequestRouter } from "./access-approval-request-router";
|
import { registerAccessApprovalRequestRouter } from "./access-approval-request-router";
|
||||||
import { registerAuditLogStreamRouter } from "./audit-log-stream-router";
|
import { registerAuditLogStreamRouter } from "./audit-log-stream-router";
|
||||||
|
import { registerCaCrlRouter } from "./certificate-authority-crl-router";
|
||||||
import { registerDynamicSecretLeaseRouter } from "./dynamic-secret-lease-router";
|
import { registerDynamicSecretLeaseRouter } from "./dynamic-secret-lease-router";
|
||||||
import { registerDynamicSecretRouter } from "./dynamic-secret-router";
|
import { registerDynamicSecretRouter } from "./dynamic-secret-router";
|
||||||
import { registerGroupRouter } from "./group-router";
|
import { registerGroupRouter } from "./group-router";
|
||||||
@@ -10,6 +11,7 @@ import { registerLicenseRouter } from "./license-router";
|
|||||||
import { registerOrgRoleRouter } from "./org-role-router";
|
import { registerOrgRoleRouter } from "./org-role-router";
|
||||||
import { registerProjectRoleRouter } from "./project-role-router";
|
import { registerProjectRoleRouter } from "./project-role-router";
|
||||||
import { registerProjectRouter } from "./project-router";
|
import { registerProjectRouter } from "./project-router";
|
||||||
|
import { registerRateLimitRouter } from "./rate-limit-router";
|
||||||
import { registerSamlRouter } from "./saml-router";
|
import { registerSamlRouter } from "./saml-router";
|
||||||
import { registerScimRouter } from "./scim-router";
|
import { registerScimRouter } from "./scim-router";
|
||||||
import { registerSecretApprovalPolicyRouter } from "./secret-approval-policy-router";
|
import { registerSecretApprovalPolicyRouter } from "./secret-approval-policy-router";
|
||||||
@@ -45,6 +47,7 @@ export const registerV1EERoutes = async (server: FastifyZodProvider) => {
|
|||||||
|
|
||||||
await server.register(registerAccessApprovalPolicyRouter, { prefix: "/access-approvals/policies" });
|
await server.register(registerAccessApprovalPolicyRouter, { prefix: "/access-approvals/policies" });
|
||||||
await server.register(registerAccessApprovalRequestRouter, { prefix: "/access-approvals/requests" });
|
await server.register(registerAccessApprovalRequestRouter, { prefix: "/access-approvals/requests" });
|
||||||
|
await server.register(registerRateLimitRouter, { prefix: "/rate-limit" });
|
||||||
|
|
||||||
await server.register(
|
await server.register(
|
||||||
async (dynamicSecretRouter) => {
|
async (dynamicSecretRouter) => {
|
||||||
@@ -54,6 +57,13 @@ export const registerV1EERoutes = async (server: FastifyZodProvider) => {
|
|||||||
{ prefix: "/dynamic-secrets" }
|
{ prefix: "/dynamic-secrets" }
|
||||||
);
|
);
|
||||||
|
|
||||||
|
await server.register(
|
||||||
|
async (pkiRouter) => {
|
||||||
|
await pkiRouter.register(registerCaCrlRouter, { prefix: "/ca" });
|
||||||
|
},
|
||||||
|
{ prefix: "/pki" }
|
||||||
|
);
|
||||||
|
|
||||||
await server.register(registerSamlRouter, { prefix: "/sso" });
|
await server.register(registerSamlRouter, { prefix: "/sso" });
|
||||||
await server.register(registerScimRouter, { prefix: "/scim" });
|
await server.register(registerScimRouter, { prefix: "/scim" });
|
||||||
await server.register(registerLdapRouter, { prefix: "/ldap" });
|
await server.register(registerLdapRouter, { prefix: "/ldap" });
|
||||||
|
@@ -143,7 +143,8 @@ export const registerProjectRouter = async (server: FastifyZodProvider) => {
|
|||||||
actorAuthMethod: req.permission.authMethod,
|
actorAuthMethod: req.permission.authMethod,
|
||||||
projectId: req.params.workspaceId,
|
projectId: req.params.workspaceId,
|
||||||
...req.query,
|
...req.query,
|
||||||
startDate: req.query.endDate || getLastMidnightDateISO(),
|
endDate: req.query.endDate,
|
||||||
|
startDate: req.query.startDate || getLastMidnightDateISO(),
|
||||||
auditLogActor: req.query.actor,
|
auditLogActor: req.query.actor,
|
||||||
actor: req.permission.type
|
actor: req.permission.type
|
||||||
});
|
});
|
||||||
|
75
backend/src/ee/routes/v1/rate-limit-router.ts
Normal file
75
backend/src/ee/routes/v1/rate-limit-router.ts
Normal file
@@ -0,0 +1,75 @@
|
|||||||
|
import { z } from "zod";
|
||||||
|
|
||||||
|
import { RateLimitSchema } from "@app/db/schemas";
|
||||||
|
import { BadRequestError } from "@app/lib/errors";
|
||||||
|
import { readLimit } from "@app/server/config/rateLimiter";
|
||||||
|
import { verifySuperAdmin } from "@app/server/plugins/auth/superAdmin";
|
||||||
|
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
|
||||||
|
import { AuthMode } from "@app/services/auth/auth-type";
|
||||||
|
|
||||||
|
export const registerRateLimitRouter = async (server: FastifyZodProvider) => {
|
||||||
|
server.route({
|
||||||
|
method: "GET",
|
||||||
|
url: "/",
|
||||||
|
config: {
|
||||||
|
rateLimit: readLimit
|
||||||
|
},
|
||||||
|
schema: {
|
||||||
|
response: {
|
||||||
|
200: z.object({
|
||||||
|
rateLimit: RateLimitSchema
|
||||||
|
})
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onRequest: (req, res, done) => {
|
||||||
|
verifyAuth([AuthMode.JWT])(req, res, () => {
|
||||||
|
verifySuperAdmin(req, res, done);
|
||||||
|
});
|
||||||
|
},
|
||||||
|
handler: async () => {
|
||||||
|
const rateLimit = await server.services.rateLimit.getRateLimits();
|
||||||
|
if (!rateLimit) {
|
||||||
|
throw new BadRequestError({
|
||||||
|
name: "Get Rate Limit Error",
|
||||||
|
message: "Rate limit configuration does not exist."
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return { rateLimit };
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
server.route({
|
||||||
|
method: "PUT",
|
||||||
|
url: "/",
|
||||||
|
config: {
|
||||||
|
rateLimit: readLimit
|
||||||
|
},
|
||||||
|
onRequest: (req, res, done) => {
|
||||||
|
verifyAuth([AuthMode.JWT])(req, res, () => {
|
||||||
|
verifySuperAdmin(req, res, done);
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
schema: {
|
||||||
|
body: z.object({
|
||||||
|
readRateLimit: z.number(),
|
||||||
|
writeRateLimit: z.number(),
|
||||||
|
secretsRateLimit: z.number(),
|
||||||
|
authRateLimit: z.number(),
|
||||||
|
inviteUserRateLimit: z.number(),
|
||||||
|
mfaRateLimit: z.number(),
|
||||||
|
creationLimit: z.number(),
|
||||||
|
publicEndpointLimit: z.number()
|
||||||
|
}),
|
||||||
|
response: {
|
||||||
|
200: z.object({
|
||||||
|
rateLimit: RateLimitSchema
|
||||||
|
})
|
||||||
|
}
|
||||||
|
},
|
||||||
|
handler: async (req) => {
|
||||||
|
const rateLimit = await server.services.rateLimit.updateRateLimit(req.body);
|
||||||
|
return { rateLimit };
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
@@ -362,6 +362,7 @@ export const registerScimRouter = async (server: FastifyZodProvider) => {
|
|||||||
const groups = await req.server.services.scim.listScimGroups({
|
const groups = await req.server.services.scim.listScimGroups({
|
||||||
orgId: req.permission.orgId,
|
orgId: req.permission.orgId,
|
||||||
startIndex: req.query.startIndex,
|
startIndex: req.query.startIndex,
|
||||||
|
filter: req.query.filter,
|
||||||
limit: req.query.count
|
limit: req.query.count
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@@ -1,6 +1,7 @@
|
|||||||
import { nanoid } from "nanoid";
|
import { nanoid } from "nanoid";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
|
|
||||||
|
import { removeTrailingSlash } from "@app/lib/fn";
|
||||||
import { readLimit, writeLimit } from "@app/server/config/rateLimiter";
|
import { readLimit, writeLimit } from "@app/server/config/rateLimiter";
|
||||||
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
|
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
|
||||||
import { sapPubSchema } from "@app/server/routes/sanitizedSchemas";
|
import { sapPubSchema } from "@app/server/routes/sanitizedSchemas";
|
||||||
@@ -19,7 +20,11 @@ export const registerSecretApprovalPolicyRouter = async (server: FastifyZodProvi
|
|||||||
workspaceId: z.string(),
|
workspaceId: z.string(),
|
||||||
name: z.string().optional(),
|
name: z.string().optional(),
|
||||||
environment: z.string(),
|
environment: z.string(),
|
||||||
secretPath: z.string().optional().nullable(),
|
secretPath: z
|
||||||
|
.string()
|
||||||
|
.optional()
|
||||||
|
.nullable()
|
||||||
|
.transform((val) => (val ? removeTrailingSlash(val) : val)),
|
||||||
approvers: z.string().array().min(1),
|
approvers: z.string().array().min(1),
|
||||||
approvals: z.number().min(1).default(1)
|
approvals: z.number().min(1).default(1)
|
||||||
})
|
})
|
||||||
@@ -63,7 +68,11 @@ export const registerSecretApprovalPolicyRouter = async (server: FastifyZodProvi
|
|||||||
name: z.string().optional(),
|
name: z.string().optional(),
|
||||||
approvers: z.string().array().min(1),
|
approvers: z.string().array().min(1),
|
||||||
approvals: z.number().min(1).default(1),
|
approvals: z.number().min(1).default(1),
|
||||||
secretPath: z.string().optional().nullable()
|
secretPath: z
|
||||||
|
.string()
|
||||||
|
.optional()
|
||||||
|
.nullable()
|
||||||
|
.transform((val) => (val ? removeTrailingSlash(val) : val))
|
||||||
})
|
})
|
||||||
.refine((data) => data.approvals <= data.approvers.length, {
|
.refine((data) => data.approvals <= data.approvers.length, {
|
||||||
path: ["approvals"],
|
path: ["approvals"],
|
||||||
@@ -157,7 +166,7 @@ export const registerSecretApprovalPolicyRouter = async (server: FastifyZodProvi
|
|||||||
querystring: z.object({
|
querystring: z.object({
|
||||||
workspaceId: z.string().trim(),
|
workspaceId: z.string().trim(),
|
||||||
environment: z.string().trim(),
|
environment: z.string().trim(),
|
||||||
secretPath: z.string().trim()
|
secretPath: z.string().trim().transform(removeTrailingSlash)
|
||||||
}),
|
}),
|
||||||
response: {
|
response: {
|
||||||
200: z.object({
|
200: z.object({
|
||||||
|
@@ -1,5 +1,6 @@
|
|||||||
import { TProjectPermission } from "@app/lib/types";
|
import { TProjectPermission } from "@app/lib/types";
|
||||||
import { ActorType } from "@app/services/auth/auth-type";
|
import { ActorType } from "@app/services/auth/auth-type";
|
||||||
|
import { CaStatus } from "@app/services/certificate-authority/certificate-authority-types";
|
||||||
import { TIdentityTrustedIp } from "@app/services/identity/identity-types";
|
import { TIdentityTrustedIp } from "@app/services/identity/identity-types";
|
||||||
|
|
||||||
export type TListProjectAuditLogDTO = {
|
export type TListProjectAuditLogDTO = {
|
||||||
@@ -104,7 +105,21 @@ export enum EventType {
|
|||||||
SECRET_APPROVAL_MERGED = "secret-approval-merged",
|
SECRET_APPROVAL_MERGED = "secret-approval-merged",
|
||||||
SECRET_APPROVAL_REQUEST = "secret-approval-request",
|
SECRET_APPROVAL_REQUEST = "secret-approval-request",
|
||||||
SECRET_APPROVAL_CLOSED = "secret-approval-closed",
|
SECRET_APPROVAL_CLOSED = "secret-approval-closed",
|
||||||
SECRET_APPROVAL_REOPENED = "secret-approval-reopened"
|
SECRET_APPROVAL_REOPENED = "secret-approval-reopened",
|
||||||
|
CREATE_CA = "create-certificate-authority",
|
||||||
|
GET_CA = "get-certificate-authority",
|
||||||
|
UPDATE_CA = "update-certificate-authority",
|
||||||
|
DELETE_CA = "delete-certificate-authority",
|
||||||
|
GET_CA_CSR = "get-certificate-authority-csr",
|
||||||
|
GET_CA_CERT = "get-certificate-authority-cert",
|
||||||
|
SIGN_INTERMEDIATE = "sign-intermediate",
|
||||||
|
IMPORT_CA_CERT = "import-certificate-authority-cert",
|
||||||
|
GET_CA_CRL = "get-certificate-authority-crl",
|
||||||
|
ISSUE_CERT = "issue-cert",
|
||||||
|
GET_CERT = "get-cert",
|
||||||
|
DELETE_CERT = "delete-cert",
|
||||||
|
REVOKE_CERT = "revoke-cert",
|
||||||
|
GET_CERT_BODY = "get-cert-body"
|
||||||
}
|
}
|
||||||
|
|
||||||
interface UserActorMetadata {
|
interface UserActorMetadata {
|
||||||
@@ -843,6 +858,125 @@ interface SecretApprovalRequest {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface CreateCa {
|
||||||
|
type: EventType.CREATE_CA;
|
||||||
|
metadata: {
|
||||||
|
caId: string;
|
||||||
|
dn: string;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
interface GetCa {
|
||||||
|
type: EventType.GET_CA;
|
||||||
|
metadata: {
|
||||||
|
caId: string;
|
||||||
|
dn: string;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
interface UpdateCa {
|
||||||
|
type: EventType.UPDATE_CA;
|
||||||
|
metadata: {
|
||||||
|
caId: string;
|
||||||
|
dn: string;
|
||||||
|
status: CaStatus;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
interface DeleteCa {
|
||||||
|
type: EventType.DELETE_CA;
|
||||||
|
metadata: {
|
||||||
|
caId: string;
|
||||||
|
dn: string;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
interface GetCaCsr {
|
||||||
|
type: EventType.GET_CA_CSR;
|
||||||
|
metadata: {
|
||||||
|
caId: string;
|
||||||
|
dn: string;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
interface GetCaCert {
|
||||||
|
type: EventType.GET_CA_CERT;
|
||||||
|
metadata: {
|
||||||
|
caId: string;
|
||||||
|
dn: string;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
interface SignIntermediate {
|
||||||
|
type: EventType.SIGN_INTERMEDIATE;
|
||||||
|
metadata: {
|
||||||
|
caId: string;
|
||||||
|
dn: string;
|
||||||
|
serialNumber: string;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ImportCaCert {
|
||||||
|
type: EventType.IMPORT_CA_CERT;
|
||||||
|
metadata: {
|
||||||
|
caId: string;
|
||||||
|
dn: string;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
interface GetCaCrl {
|
||||||
|
type: EventType.GET_CA_CRL;
|
||||||
|
metadata: {
|
||||||
|
caId: string;
|
||||||
|
dn: string;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
interface IssueCert {
|
||||||
|
type: EventType.ISSUE_CERT;
|
||||||
|
metadata: {
|
||||||
|
caId: string;
|
||||||
|
dn: string;
|
||||||
|
serialNumber: string;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
interface GetCert {
|
||||||
|
type: EventType.GET_CERT;
|
||||||
|
metadata: {
|
||||||
|
certId: string;
|
||||||
|
cn: string;
|
||||||
|
serialNumber: string;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
interface DeleteCert {
|
||||||
|
type: EventType.DELETE_CERT;
|
||||||
|
metadata: {
|
||||||
|
certId: string;
|
||||||
|
cn: string;
|
||||||
|
serialNumber: string;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
interface RevokeCert {
|
||||||
|
type: EventType.REVOKE_CERT;
|
||||||
|
metadata: {
|
||||||
|
certId: string;
|
||||||
|
cn: string;
|
||||||
|
serialNumber: string;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
interface GetCertBody {
|
||||||
|
type: EventType.GET_CERT_BODY;
|
||||||
|
metadata: {
|
||||||
|
certId: string;
|
||||||
|
cn: string;
|
||||||
|
serialNumber: string;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
export type Event =
|
export type Event =
|
||||||
| GetSecretsEvent
|
| GetSecretsEvent
|
||||||
| GetSecretEvent
|
| GetSecretEvent
|
||||||
@@ -910,4 +1044,18 @@ export type Event =
|
|||||||
| SecretApprovalMerge
|
| SecretApprovalMerge
|
||||||
| SecretApprovalClosed
|
| SecretApprovalClosed
|
||||||
| SecretApprovalRequest
|
| SecretApprovalRequest
|
||||||
| SecretApprovalReopened;
|
| SecretApprovalReopened
|
||||||
|
| CreateCa
|
||||||
|
| GetCa
|
||||||
|
| UpdateCa
|
||||||
|
| DeleteCa
|
||||||
|
| GetCaCsr
|
||||||
|
| GetCaCert
|
||||||
|
| SignIntermediate
|
||||||
|
| ImportCaCert
|
||||||
|
| GetCaCrl
|
||||||
|
| IssueCert
|
||||||
|
| GetCert
|
||||||
|
| DeleteCert
|
||||||
|
| RevokeCert
|
||||||
|
| GetCertBody;
|
||||||
|
@@ -0,0 +1,10 @@
|
|||||||
|
import { TDbClient } from "@app/db";
|
||||||
|
import { TableName } from "@app/db/schemas";
|
||||||
|
import { ormify } from "@app/lib/knex";
|
||||||
|
|
||||||
|
export type TCertificateAuthorityCrlDALFactory = ReturnType<typeof certificateAuthorityCrlDALFactory>;
|
||||||
|
|
||||||
|
export const certificateAuthorityCrlDALFactory = (db: TDbClient) => {
|
||||||
|
const caCrlOrm = ormify(db, TableName.CertificateAuthorityCrl);
|
||||||
|
return caCrlOrm;
|
||||||
|
};
|
@@ -0,0 +1,172 @@
|
|||||||
|
import { ForbiddenError } from "@casl/ability";
|
||||||
|
import * as x509 from "@peculiar/x509";
|
||||||
|
|
||||||
|
import { TCertificateAuthorityCrlDALFactory } from "@app/ee/services/certificate-authority-crl/certificate-authority-crl-dal";
|
||||||
|
import { TLicenseServiceFactory } from "@app/ee/services/license/license-service";
|
||||||
|
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 { TCertificateAuthorityDALFactory } from "@app/services/certificate-authority/certificate-authority-dal";
|
||||||
|
import { TKmsServiceFactory } from "@app/services/kms/kms-service";
|
||||||
|
import { TProjectDALFactory } from "@app/services/project/project-dal";
|
||||||
|
import { getProjectKmsCertificateKeyId } from "@app/services/project/project-fns";
|
||||||
|
|
||||||
|
import { TGetCrl } from "./certificate-authority-crl-types";
|
||||||
|
|
||||||
|
type TCertificateAuthorityCrlServiceFactoryDep = {
|
||||||
|
certificateAuthorityDAL: Pick<TCertificateAuthorityDALFactory, "findById">;
|
||||||
|
certificateAuthorityCrlDAL: Pick<TCertificateAuthorityCrlDALFactory, "findOne">;
|
||||||
|
projectDAL: Pick<TProjectDALFactory, "findOne" | "updateById" | "transaction">;
|
||||||
|
kmsService: Pick<TKmsServiceFactory, "decrypt" | "generateKmsKey">;
|
||||||
|
permissionService: Pick<TPermissionServiceFactory, "getProjectPermission">;
|
||||||
|
licenseService: Pick<TLicenseServiceFactory, "getPlan">;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type TCertificateAuthorityCrlServiceFactory = ReturnType<typeof certificateAuthorityCrlServiceFactory>;
|
||||||
|
|
||||||
|
export const certificateAuthorityCrlServiceFactory = ({
|
||||||
|
certificateAuthorityDAL,
|
||||||
|
certificateAuthorityCrlDAL,
|
||||||
|
projectDAL,
|
||||||
|
kmsService,
|
||||||
|
permissionService,
|
||||||
|
licenseService
|
||||||
|
}: TCertificateAuthorityCrlServiceFactoryDep) => {
|
||||||
|
/**
|
||||||
|
* Return the Certificate Revocation List (CRL) for CA with id [caId]
|
||||||
|
*/
|
||||||
|
const getCaCrl = async ({ caId, actorId, actorAuthMethod, actor, actorOrgId }: TGetCrl) => {
|
||||||
|
const ca = await certificateAuthorityDAL.findById(caId);
|
||||||
|
if (!ca) throw new BadRequestError({ message: "CA not found" });
|
||||||
|
|
||||||
|
const { permission } = await permissionService.getProjectPermission(
|
||||||
|
actor,
|
||||||
|
actorId,
|
||||||
|
ca.projectId,
|
||||||
|
actorAuthMethod,
|
||||||
|
actorOrgId
|
||||||
|
);
|
||||||
|
|
||||||
|
ForbiddenError.from(permission).throwUnlessCan(
|
||||||
|
ProjectPermissionActions.Read,
|
||||||
|
ProjectPermissionSub.CertificateAuthorities
|
||||||
|
);
|
||||||
|
|
||||||
|
const plan = await licenseService.getPlan(actorOrgId);
|
||||||
|
if (!plan.caCrl)
|
||||||
|
throw new BadRequestError({
|
||||||
|
message:
|
||||||
|
"Failed to get CA certificate revocation list (CRL) due to plan restriction. Upgrade plan to get the CA CRL."
|
||||||
|
});
|
||||||
|
|
||||||
|
const caCrl = await certificateAuthorityCrlDAL.findOne({ caId: ca.id });
|
||||||
|
if (!caCrl) throw new BadRequestError({ message: "CRL not found" });
|
||||||
|
|
||||||
|
const keyId = await getProjectKmsCertificateKeyId({
|
||||||
|
projectId: ca.projectId,
|
||||||
|
projectDAL,
|
||||||
|
kmsService
|
||||||
|
});
|
||||||
|
|
||||||
|
const decryptedCrl = await kmsService.decrypt({
|
||||||
|
kmsId: keyId,
|
||||||
|
cipherTextBlob: caCrl.encryptedCrl
|
||||||
|
});
|
||||||
|
|
||||||
|
const crl = new x509.X509Crl(decryptedCrl);
|
||||||
|
|
||||||
|
const base64crl = crl.toString("base64");
|
||||||
|
const crlPem = `-----BEGIN X509 CRL-----\n${base64crl.match(/.{1,64}/g)?.join("\n")}\n-----END X509 CRL-----`;
|
||||||
|
|
||||||
|
return {
|
||||||
|
crl: crlPem,
|
||||||
|
ca
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
// const rotateCaCrl = async ({ caId, actorId, actorAuthMethod, actor, actorOrgId }: TRotateCrlDTO) => {
|
||||||
|
// const ca = await certificateAuthorityDAL.findById(caId);
|
||||||
|
// if (!ca) throw new BadRequestError({ message: "CA not found" });
|
||||||
|
|
||||||
|
// const { permission } = await permissionService.getProjectPermission(
|
||||||
|
// actor,
|
||||||
|
// actorId,
|
||||||
|
// ca.projectId,
|
||||||
|
// actorAuthMethod,
|
||||||
|
// actorOrgId
|
||||||
|
// );
|
||||||
|
|
||||||
|
// ForbiddenError.from(permission).throwUnlessCan(
|
||||||
|
// ProjectPermissionActions.Read,
|
||||||
|
// ProjectPermissionSub.CertificateAuthorities
|
||||||
|
// );
|
||||||
|
|
||||||
|
// const caSecret = await certificateAuthoritySecretDAL.findOne({ caId: ca.id });
|
||||||
|
|
||||||
|
// const alg = keyAlgorithmToAlgCfg(ca.keyAlgorithm as CertKeyAlgorithm);
|
||||||
|
|
||||||
|
// const keyId = await getProjectKmsCertificateKeyId({
|
||||||
|
// projectId: ca.projectId,
|
||||||
|
// projectDAL,
|
||||||
|
// kmsService
|
||||||
|
// });
|
||||||
|
|
||||||
|
// const privateKey = await kmsService.decrypt({
|
||||||
|
// kmsId: keyId,
|
||||||
|
// cipherTextBlob: caSecret.encryptedPrivateKey
|
||||||
|
// });
|
||||||
|
|
||||||
|
// const skObj = crypto.createPrivateKey({ key: privateKey, format: "der", type: "pkcs8" });
|
||||||
|
// const sk = await crypto.subtle.importKey("pkcs8", skObj.export({ format: "der", type: "pkcs8" }), alg, true, [
|
||||||
|
// "sign"
|
||||||
|
// ]);
|
||||||
|
|
||||||
|
// const revokedCerts = await certificateDAL.find({
|
||||||
|
// caId: ca.id,
|
||||||
|
// status: CertStatus.REVOKED
|
||||||
|
// });
|
||||||
|
|
||||||
|
// const crl = await x509.X509CrlGenerator.create({
|
||||||
|
// issuer: ca.dn,
|
||||||
|
// thisUpdate: new Date(),
|
||||||
|
// nextUpdate: new Date("2025/12/12"),
|
||||||
|
// entries: revokedCerts.map((revokedCert) => {
|
||||||
|
// return {
|
||||||
|
// serialNumber: revokedCert.serialNumber,
|
||||||
|
// revocationDate: new Date(revokedCert.revokedAt as Date),
|
||||||
|
// reason: revokedCert.revocationReason as number,
|
||||||
|
// invalidity: new Date("2022/01/01"),
|
||||||
|
// issuer: ca.dn
|
||||||
|
// };
|
||||||
|
// }),
|
||||||
|
// signingAlgorithm: alg,
|
||||||
|
// signingKey: sk
|
||||||
|
// });
|
||||||
|
|
||||||
|
// const { cipherTextBlob: encryptedCrl } = await kmsService.encrypt({
|
||||||
|
// kmsId: keyId,
|
||||||
|
// plainText: Buffer.from(new Uint8Array(crl.rawData))
|
||||||
|
// });
|
||||||
|
|
||||||
|
// await certificateAuthorityCrlDAL.update(
|
||||||
|
// {
|
||||||
|
// caId: ca.id
|
||||||
|
// },
|
||||||
|
// {
|
||||||
|
// encryptedCrl
|
||||||
|
// }
|
||||||
|
// );
|
||||||
|
|
||||||
|
// const base64crl = crl.toString("base64");
|
||||||
|
// const crlPem = `-----BEGIN X509 CRL-----\n${base64crl.match(/.{1,64}/g)?.join("\n")}\n-----END X509 CRL-----`;
|
||||||
|
|
||||||
|
// return {
|
||||||
|
// crl: crlPem
|
||||||
|
// };
|
||||||
|
// };
|
||||||
|
|
||||||
|
return {
|
||||||
|
getCaCrl
|
||||||
|
// rotateCaCrl
|
||||||
|
};
|
||||||
|
};
|
@@ -0,0 +1,5 @@
|
|||||||
|
import { TProjectPermission } from "@app/lib/types";
|
||||||
|
|
||||||
|
export type TGetCrl = {
|
||||||
|
caId: string;
|
||||||
|
} & Omit<TProjectPermission, "projectId">;
|
@@ -77,7 +77,7 @@ type TLdapConfigServiceFactoryDep = {
|
|||||||
>;
|
>;
|
||||||
userAliasDAL: Pick<TUserAliasDALFactory, "create" | "findOne">;
|
userAliasDAL: Pick<TUserAliasDALFactory, "create" | "findOne">;
|
||||||
permissionService: Pick<TPermissionServiceFactory, "getOrgPermission">;
|
permissionService: Pick<TPermissionServiceFactory, "getOrgPermission">;
|
||||||
licenseService: Pick<TLicenseServiceFactory, "getPlan">;
|
licenseService: Pick<TLicenseServiceFactory, "getPlan" | "updateSubscriptionOrgMemberCount">;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type TLdapConfigServiceFactory = ReturnType<typeof ldapConfigServiceFactory>;
|
export type TLdapConfigServiceFactory = ReturnType<typeof ldapConfigServiceFactory>;
|
||||||
@@ -510,6 +510,7 @@ export const ldapConfigServiceFactory = ({
|
|||||||
return newUserAlias;
|
return newUserAlias;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
await licenseService.updateSubscriptionOrgMemberCount(organization.id);
|
||||||
|
|
||||||
const user = await userDAL.transaction(async (tx) => {
|
const user = await userDAL.transaction(async (tx) => {
|
||||||
const newUser = await userDAL.findOne({ id: userAlias.userId }, tx);
|
const newUser = await userDAL.findOne({ id: userAlias.userId }, tx);
|
||||||
|
@@ -25,6 +25,7 @@ export const getDefaultOnPremFeatures = () => {
|
|||||||
trial_end: null,
|
trial_end: null,
|
||||||
has_used_trial: true,
|
has_used_trial: true,
|
||||||
secretApproval: false,
|
secretApproval: false,
|
||||||
secretRotation: true
|
secretRotation: true,
|
||||||
|
caCrl: false
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
@@ -34,7 +34,8 @@ export const getDefaultOnPremFeatures = (): TFeatureSet => ({
|
|||||||
trial_end: null,
|
trial_end: null,
|
||||||
has_used_trial: true,
|
has_used_trial: true,
|
||||||
secretApproval: false,
|
secretApproval: false,
|
||||||
secretRotation: true
|
secretRotation: true,
|
||||||
|
caCrl: false
|
||||||
});
|
});
|
||||||
|
|
||||||
export const setupLicenceRequestWithStore = (baseURL: string, refreshUrl: string, licenseKey: string) => {
|
export const setupLicenceRequestWithStore = (baseURL: string, refreshUrl: string, licenseKey: string) => {
|
||||||
|
@@ -575,6 +575,9 @@ export const licenseServiceFactory = ({
|
|||||||
getInstanceType() {
|
getInstanceType() {
|
||||||
return instanceType;
|
return instanceType;
|
||||||
},
|
},
|
||||||
|
get onPremFeatures() {
|
||||||
|
return onPremFeatures;
|
||||||
|
},
|
||||||
getPlan,
|
getPlan,
|
||||||
updateSubscriptionOrgMemberCount,
|
updateSubscriptionOrgMemberCount,
|
||||||
refreshPlan,
|
refreshPlan,
|
||||||
|
@@ -52,6 +52,7 @@ export type TFeatureSet = {
|
|||||||
has_used_trial: true;
|
has_used_trial: true;
|
||||||
secretApproval: false;
|
secretApproval: false;
|
||||||
secretRotation: true;
|
secretRotation: true;
|
||||||
|
caCrl: false;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type TOrgPlansTableDTO = {
|
export type TOrgPlansTableDTO = {
|
||||||
|
@@ -26,7 +26,9 @@ export enum ProjectPermissionSub {
|
|||||||
SecretRollback = "secret-rollback",
|
SecretRollback = "secret-rollback",
|
||||||
SecretApproval = "secret-approval",
|
SecretApproval = "secret-approval",
|
||||||
SecretRotation = "secret-rotation",
|
SecretRotation = "secret-rotation",
|
||||||
Identity = "identity"
|
Identity = "identity",
|
||||||
|
CertificateAuthorities = "certificate-authorities",
|
||||||
|
Certificates = "certificates"
|
||||||
}
|
}
|
||||||
|
|
||||||
type SubjectFields = {
|
type SubjectFields = {
|
||||||
@@ -53,6 +55,8 @@ export type ProjectPermissionSet =
|
|||||||
| [ProjectPermissionActions, ProjectPermissionSub.SecretApproval]
|
| [ProjectPermissionActions, ProjectPermissionSub.SecretApproval]
|
||||||
| [ProjectPermissionActions, ProjectPermissionSub.SecretRotation]
|
| [ProjectPermissionActions, ProjectPermissionSub.SecretRotation]
|
||||||
| [ProjectPermissionActions, ProjectPermissionSub.Identity]
|
| [ProjectPermissionActions, ProjectPermissionSub.Identity]
|
||||||
|
| [ProjectPermissionActions, ProjectPermissionSub.CertificateAuthorities]
|
||||||
|
| [ProjectPermissionActions, ProjectPermissionSub.Certificates]
|
||||||
| [ProjectPermissionActions.Delete, ProjectPermissionSub.Project]
|
| [ProjectPermissionActions.Delete, ProjectPermissionSub.Project]
|
||||||
| [ProjectPermissionActions.Edit, ProjectPermissionSub.Project]
|
| [ProjectPermissionActions.Edit, ProjectPermissionSub.Project]
|
||||||
| [ProjectPermissionActions.Read, ProjectPermissionSub.SecretRollback]
|
| [ProjectPermissionActions.Read, ProjectPermissionSub.SecretRollback]
|
||||||
@@ -139,6 +143,17 @@ const buildAdminPermissionRules = () => {
|
|||||||
can(ProjectPermissionActions.Edit, ProjectPermissionSub.IpAllowList);
|
can(ProjectPermissionActions.Edit, ProjectPermissionSub.IpAllowList);
|
||||||
can(ProjectPermissionActions.Delete, ProjectPermissionSub.IpAllowList);
|
can(ProjectPermissionActions.Delete, ProjectPermissionSub.IpAllowList);
|
||||||
|
|
||||||
|
// double check if all CRUD are needed for CA and Certificates
|
||||||
|
can(ProjectPermissionActions.Read, ProjectPermissionSub.CertificateAuthorities);
|
||||||
|
can(ProjectPermissionActions.Create, ProjectPermissionSub.CertificateAuthorities);
|
||||||
|
can(ProjectPermissionActions.Edit, ProjectPermissionSub.CertificateAuthorities);
|
||||||
|
can(ProjectPermissionActions.Delete, ProjectPermissionSub.CertificateAuthorities);
|
||||||
|
|
||||||
|
can(ProjectPermissionActions.Read, ProjectPermissionSub.Certificates);
|
||||||
|
can(ProjectPermissionActions.Create, ProjectPermissionSub.Certificates);
|
||||||
|
can(ProjectPermissionActions.Edit, ProjectPermissionSub.Certificates);
|
||||||
|
can(ProjectPermissionActions.Delete, ProjectPermissionSub.Certificates);
|
||||||
|
|
||||||
can(ProjectPermissionActions.Edit, ProjectPermissionSub.Project);
|
can(ProjectPermissionActions.Edit, ProjectPermissionSub.Project);
|
||||||
can(ProjectPermissionActions.Delete, ProjectPermissionSub.Project);
|
can(ProjectPermissionActions.Delete, ProjectPermissionSub.Project);
|
||||||
|
|
||||||
@@ -205,6 +220,14 @@ const buildMemberPermissionRules = () => {
|
|||||||
can(ProjectPermissionActions.Read, ProjectPermissionSub.AuditLogs);
|
can(ProjectPermissionActions.Read, ProjectPermissionSub.AuditLogs);
|
||||||
can(ProjectPermissionActions.Read, ProjectPermissionSub.IpAllowList);
|
can(ProjectPermissionActions.Read, ProjectPermissionSub.IpAllowList);
|
||||||
|
|
||||||
|
// double check if all CRUD are needed for CA and Certificates
|
||||||
|
can(ProjectPermissionActions.Read, ProjectPermissionSub.CertificateAuthorities);
|
||||||
|
|
||||||
|
can(ProjectPermissionActions.Read, ProjectPermissionSub.Certificates);
|
||||||
|
can(ProjectPermissionActions.Create, ProjectPermissionSub.Certificates);
|
||||||
|
can(ProjectPermissionActions.Edit, ProjectPermissionSub.Certificates);
|
||||||
|
can(ProjectPermissionActions.Delete, ProjectPermissionSub.Certificates);
|
||||||
|
|
||||||
return rules;
|
return rules;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -229,6 +252,8 @@ const buildViewerPermissionRules = () => {
|
|||||||
can(ProjectPermissionActions.Read, ProjectPermissionSub.Tags);
|
can(ProjectPermissionActions.Read, ProjectPermissionSub.Tags);
|
||||||
can(ProjectPermissionActions.Read, ProjectPermissionSub.AuditLogs);
|
can(ProjectPermissionActions.Read, ProjectPermissionSub.AuditLogs);
|
||||||
can(ProjectPermissionActions.Read, ProjectPermissionSub.IpAllowList);
|
can(ProjectPermissionActions.Read, ProjectPermissionSub.IpAllowList);
|
||||||
|
can(ProjectPermissionActions.Read, ProjectPermissionSub.CertificateAuthorities);
|
||||||
|
can(ProjectPermissionActions.Read, ProjectPermissionSub.Certificates);
|
||||||
|
|
||||||
return rules;
|
return rules;
|
||||||
};
|
};
|
||||||
|
7
backend/src/ee/services/rate-limit/rate-limit-dal.ts
Normal file
7
backend/src/ee/services/rate-limit/rate-limit-dal.ts
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
import { TDbClient } from "@app/db";
|
||||||
|
import { TableName } from "@app/db/schemas";
|
||||||
|
import { ormify } from "@app/lib/knex";
|
||||||
|
|
||||||
|
export type TRateLimitDALFactory = ReturnType<typeof rateLimitDALFactory>;
|
||||||
|
|
||||||
|
export const rateLimitDALFactory = (db: TDbClient) => ormify(db, TableName.RateLimit, {});
|
106
backend/src/ee/services/rate-limit/rate-limit-service.ts
Normal file
106
backend/src/ee/services/rate-limit/rate-limit-service.ts
Normal file
@@ -0,0 +1,106 @@
|
|||||||
|
import { CronJob } from "cron";
|
||||||
|
|
||||||
|
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";
|
||||||
|
|
||||||
|
let rateLimitMaxConfiguration = {
|
||||||
|
readLimit: 60,
|
||||||
|
publicEndpointLimit: 30,
|
||||||
|
writeLimit: 200,
|
||||||
|
secretsLimit: 60,
|
||||||
|
authRateLimit: 60,
|
||||||
|
inviteUserRateLimit: 30,
|
||||||
|
mfaRateLimit: 20,
|
||||||
|
creationLimit: 30
|
||||||
|
};
|
||||||
|
|
||||||
|
Object.freeze(rateLimitMaxConfiguration);
|
||||||
|
|
||||||
|
export const getRateLimiterConfig = () => {
|
||||||
|
return rateLimitMaxConfiguration;
|
||||||
|
};
|
||||||
|
|
||||||
|
type TRateLimitServiceFactoryDep = {
|
||||||
|
rateLimitDAL: TRateLimitDALFactory;
|
||||||
|
licenseService: Pick<TLicenseServiceFactory, "onPremFeatures">;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type TRateLimitServiceFactory = ReturnType<typeof rateLimitServiceFactory>;
|
||||||
|
|
||||||
|
export const rateLimitServiceFactory = ({ rateLimitDAL, licenseService }: TRateLimitServiceFactoryDep) => {
|
||||||
|
const DEFAULT_RATE_LIMIT_CONFIG_ID = "00000000-0000-0000-0000-000000000000";
|
||||||
|
|
||||||
|
const getRateLimits = async (): Promise<TRateLimit | undefined> => {
|
||||||
|
let rateLimit: TRateLimit;
|
||||||
|
|
||||||
|
try {
|
||||||
|
rateLimit = await rateLimitDAL.findOne({ id: DEFAULT_RATE_LIMIT_CONFIG_ID });
|
||||||
|
if (!rateLimit) {
|
||||||
|
// rate limit might not exist
|
||||||
|
rateLimit = await rateLimitDAL.create({
|
||||||
|
// @ts-expect-error id is kept as fixed because there should only be one rate limit config per instance
|
||||||
|
id: DEFAULT_RATE_LIMIT_CONFIG_ID
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return rateLimit;
|
||||||
|
} catch (err) {
|
||||||
|
logger.error("Error fetching rate limits %o", err);
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const updateRateLimit = async (updates: TRateLimitUpdateDTO): Promise<TRateLimit> => {
|
||||||
|
return rateLimitDAL.updateById(DEFAULT_RATE_LIMIT_CONFIG_ID, updates);
|
||||||
|
};
|
||||||
|
|
||||||
|
const syncRateLimitConfiguration = async () => {
|
||||||
|
try {
|
||||||
|
const rateLimit = await getRateLimits();
|
||||||
|
if (rateLimit) {
|
||||||
|
const newRateLimitMaxConfiguration: typeof rateLimitMaxConfiguration = {
|
||||||
|
readLimit: rateLimit.readRateLimit,
|
||||||
|
publicEndpointLimit: rateLimit.publicEndpointLimit,
|
||||||
|
writeLimit: rateLimit.writeRateLimit,
|
||||||
|
secretsLimit: rateLimit.secretsRateLimit,
|
||||||
|
authRateLimit: rateLimit.authRateLimit,
|
||||||
|
inviteUserRateLimit: rateLimit.inviteUserRateLimit,
|
||||||
|
mfaRateLimit: rateLimit.mfaRateLimit,
|
||||||
|
creationLimit: rateLimit.creationLimit
|
||||||
|
};
|
||||||
|
|
||||||
|
logger.info(`syncRateLimitConfiguration: rate limit configuration: %o`, newRateLimitMaxConfiguration);
|
||||||
|
Object.freeze(newRateLimitMaxConfiguration);
|
||||||
|
rateLimitMaxConfiguration = newRateLimitMaxConfiguration;
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
logger.error(`Error syncing rate limit configurations: %o`, error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const initializeBackgroundSync = async () => {
|
||||||
|
if (!licenseService.onPremFeatures.customRateLimits) {
|
||||||
|
logger.info("Current license does not support custom rate limit configuration");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.info("Setting up background sync process for rate limits");
|
||||||
|
// initial sync upon startup
|
||||||
|
await syncRateLimitConfiguration();
|
||||||
|
|
||||||
|
// sync rate limits configuration every 10 minutes
|
||||||
|
const job = new CronJob("*/10 * * * *", syncRateLimitConfiguration);
|
||||||
|
job.start();
|
||||||
|
|
||||||
|
return job;
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
getRateLimits,
|
||||||
|
updateRateLimit,
|
||||||
|
initializeBackgroundSync,
|
||||||
|
syncRateLimitConfiguration
|
||||||
|
};
|
||||||
|
};
|
16
backend/src/ee/services/rate-limit/rate-limit-types.ts
Normal file
16
backend/src/ee/services/rate-limit/rate-limit-types.ts
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
export type TRateLimitUpdateDTO = {
|
||||||
|
readRateLimit: number;
|
||||||
|
writeRateLimit: number;
|
||||||
|
secretsRateLimit: number;
|
||||||
|
authRateLimit: number;
|
||||||
|
inviteUserRateLimit: number;
|
||||||
|
mfaRateLimit: number;
|
||||||
|
creationLimit: number;
|
||||||
|
publicEndpointLimit: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type TRateLimit = {
|
||||||
|
id: string;
|
||||||
|
createdAt: Date;
|
||||||
|
updatedAt: Date;
|
||||||
|
} & TRateLimitUpdateDTO;
|
@@ -50,7 +50,7 @@ type TSamlConfigServiceFactoryDep = {
|
|||||||
orgMembershipDAL: Pick<TOrgMembershipDALFactory, "create">;
|
orgMembershipDAL: Pick<TOrgMembershipDALFactory, "create">;
|
||||||
orgBotDAL: Pick<TOrgBotDALFactory, "findOne" | "create" | "transaction">;
|
orgBotDAL: Pick<TOrgBotDALFactory, "findOne" | "create" | "transaction">;
|
||||||
permissionService: Pick<TPermissionServiceFactory, "getOrgPermission">;
|
permissionService: Pick<TPermissionServiceFactory, "getOrgPermission">;
|
||||||
licenseService: Pick<TLicenseServiceFactory, "getPlan">;
|
licenseService: Pick<TLicenseServiceFactory, "getPlan" | "updateSubscriptionOrgMemberCount">;
|
||||||
tokenService: Pick<TAuthTokenServiceFactory, "createTokenForUser">;
|
tokenService: Pick<TAuthTokenServiceFactory, "createTokenForUser">;
|
||||||
smtpService: Pick<TSmtpService, "sendMail">;
|
smtpService: Pick<TSmtpService, "sendMail">;
|
||||||
};
|
};
|
||||||
@@ -449,6 +449,7 @@ export const samlConfigServiceFactory = ({
|
|||||||
return newUser;
|
return newUser;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
await licenseService.updateSubscriptionOrgMemberCount(organization.id);
|
||||||
|
|
||||||
const isUserCompleted = Boolean(user.isAccepted);
|
const isUserCompleted = Boolean(user.isAccepted);
|
||||||
const providerAuthToken = jwt.sign(
|
const providerAuthToken = jwt.sign(
|
||||||
|
@@ -18,6 +18,20 @@ export const buildScimUserList = ({
|
|||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const parseScimFilter = (filterToParse: string | undefined) => {
|
||||||
|
if (!filterToParse) return {};
|
||||||
|
const [parsedName, parsedValue] = filterToParse.split("eq").map((s) => s.trim());
|
||||||
|
|
||||||
|
let attributeName = parsedName;
|
||||||
|
if (parsedName === "userName") {
|
||||||
|
attributeName = "email";
|
||||||
|
} else if (parsedName === "displayName") {
|
||||||
|
attributeName = "name";
|
||||||
|
}
|
||||||
|
|
||||||
|
return { [attributeName]: parsedValue.replace(/"/g, "") };
|
||||||
|
};
|
||||||
|
|
||||||
export const buildScimUser = ({
|
export const buildScimUser = ({
|
||||||
orgMembershipId,
|
orgMembershipId,
|
||||||
username,
|
username,
|
||||||
|
@@ -30,7 +30,7 @@ import { UserAliasType } from "@app/services/user-alias/user-alias-types";
|
|||||||
import { TLicenseServiceFactory } from "../license/license-service";
|
import { TLicenseServiceFactory } from "../license/license-service";
|
||||||
import { OrgPermissionActions, OrgPermissionSubjects } from "../permission/org-permission";
|
import { OrgPermissionActions, OrgPermissionSubjects } from "../permission/org-permission";
|
||||||
import { TPermissionServiceFactory } from "../permission/permission-service";
|
import { TPermissionServiceFactory } from "../permission/permission-service";
|
||||||
import { buildScimGroup, buildScimGroupList, buildScimUser, buildScimUserList } from "./scim-fns";
|
import { buildScimGroup, buildScimGroupList, buildScimUser, buildScimUserList, parseScimFilter } from "./scim-fns";
|
||||||
import {
|
import {
|
||||||
TCreateScimGroupDTO,
|
TCreateScimGroupDTO,
|
||||||
TCreateScimTokenDTO,
|
TCreateScimTokenDTO,
|
||||||
@@ -184,18 +184,6 @@ export const scimServiceFactory = ({
|
|||||||
status: 403
|
status: 403
|
||||||
});
|
});
|
||||||
|
|
||||||
const parseFilter = (filterToParse: string | undefined) => {
|
|
||||||
if (!filterToParse) return {};
|
|
||||||
const [parsedName, parsedValue] = filterToParse.split("eq").map((s) => s.trim());
|
|
||||||
|
|
||||||
let attributeName = parsedName;
|
|
||||||
if (parsedName === "userName") {
|
|
||||||
attributeName = "email";
|
|
||||||
}
|
|
||||||
|
|
||||||
return { [attributeName]: parsedValue.replace(/"/g, "") };
|
|
||||||
};
|
|
||||||
|
|
||||||
const findOpts = {
|
const findOpts = {
|
||||||
...(startIndex && { offset: startIndex - 1 }),
|
...(startIndex && { offset: startIndex - 1 }),
|
||||||
...(limit && { limit })
|
...(limit && { limit })
|
||||||
@@ -204,7 +192,7 @@ export const scimServiceFactory = ({
|
|||||||
const users = await orgDAL.findMembership(
|
const users = await orgDAL.findMembership(
|
||||||
{
|
{
|
||||||
[`${TableName.OrgMembership}.orgId` as "id"]: orgId,
|
[`${TableName.OrgMembership}.orgId` as "id"]: orgId,
|
||||||
...parseFilter(filter)
|
...parseScimFilter(filter)
|
||||||
},
|
},
|
||||||
findOpts
|
findOpts
|
||||||
);
|
);
|
||||||
@@ -391,7 +379,7 @@ export const scimServiceFactory = ({
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
await licenseService.updateSubscriptionOrgMemberCount(org.id);
|
||||||
return { user, orgMembership };
|
return { user, orgMembership };
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -557,7 +545,7 @@ export const scimServiceFactory = ({
|
|||||||
return {}; // intentionally return empty object upon success
|
return {}; // intentionally return empty object upon success
|
||||||
};
|
};
|
||||||
|
|
||||||
const listScimGroups = async ({ orgId, startIndex, limit }: TListScimGroupsDTO) => {
|
const listScimGroups = async ({ orgId, startIndex, limit, filter }: TListScimGroupsDTO) => {
|
||||||
const plan = await licenseService.getPlan(orgId);
|
const plan = await licenseService.getPlan(orgId);
|
||||||
if (!plan.groups)
|
if (!plan.groups)
|
||||||
throw new BadRequestError({
|
throw new BadRequestError({
|
||||||
@@ -580,7 +568,8 @@ export const scimServiceFactory = ({
|
|||||||
|
|
||||||
const groups = await groupDAL.findGroups(
|
const groups = await groupDAL.findGroups(
|
||||||
{
|
{
|
||||||
orgId
|
orgId,
|
||||||
|
...(filter && parseScimFilter(filter))
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
offset: startIndex - 1,
|
offset: startIndex - 1,
|
||||||
|
@@ -66,6 +66,7 @@ export type TDeleteScimUserDTO = {
|
|||||||
|
|
||||||
export type TListScimGroupsDTO = {
|
export type TListScimGroupsDTO = {
|
||||||
startIndex: number;
|
startIndex: number;
|
||||||
|
filter?: string;
|
||||||
limit: number;
|
limit: number;
|
||||||
orgId: string;
|
orgId: string;
|
||||||
};
|
};
|
||||||
|
@@ -4,6 +4,7 @@ import picomatch from "picomatch";
|
|||||||
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service";
|
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service";
|
||||||
import { ProjectPermissionActions, ProjectPermissionSub } from "@app/ee/services/permission/project-permission";
|
import { ProjectPermissionActions, ProjectPermissionSub } from "@app/ee/services/permission/project-permission";
|
||||||
import { BadRequestError } from "@app/lib/errors";
|
import { BadRequestError } from "@app/lib/errors";
|
||||||
|
import { removeTrailingSlash } from "@app/lib/fn";
|
||||||
import { containsGlobPatterns } from "@app/lib/picomatch";
|
import { containsGlobPatterns } from "@app/lib/picomatch";
|
||||||
import { TProjectEnvDALFactory } from "@app/services/project-env/project-env-dal";
|
import { TProjectEnvDALFactory } from "@app/services/project-env/project-env-dal";
|
||||||
import { TProjectMembershipDALFactory } from "@app/services/project-membership/project-membership-dal";
|
import { TProjectMembershipDALFactory } from "@app/services/project-membership/project-membership-dal";
|
||||||
@@ -207,7 +208,8 @@ export const secretApprovalPolicyServiceFactory = ({
|
|||||||
return sapPolicies;
|
return sapPolicies;
|
||||||
};
|
};
|
||||||
|
|
||||||
const getSecretApprovalPolicy = async (projectId: string, environment: string, secretPath: string) => {
|
const getSecretApprovalPolicy = async (projectId: string, environment: string, path: string) => {
|
||||||
|
const secretPath = removeTrailingSlash(path);
|
||||||
const env = await projectEnvDAL.findOne({ slug: environment, projectId });
|
const env = await projectEnvDAL.findOne({ slug: environment, projectId });
|
||||||
if (!env) throw new BadRequestError({ message: "Environment not found" });
|
if (!env) throw new BadRequestError({ message: "Environment not found" });
|
||||||
|
|
||||||
|
@@ -81,8 +81,7 @@ export const secretSnapshotServiceFactory = ({
|
|||||||
const folder = await folderDAL.findBySecretPath(projectId, environment, path);
|
const folder = await folderDAL.findBySecretPath(projectId, environment, path);
|
||||||
if (!folder) throw new BadRequestError({ message: "Folder not found" });
|
if (!folder) throw new BadRequestError({ message: "Folder not found" });
|
||||||
|
|
||||||
const count = await snapshotDAL.countOfSnapshotsByFolderId(folder.id);
|
return snapshotDAL.countOfSnapshotsByFolderId(folder.id);
|
||||||
return count;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const listSnapshots = async ({
|
const listSnapshots = async ({
|
||||||
|
@@ -1,3 +1,4 @@
|
|||||||
|
/* eslint-disable no-await-in-loop */
|
||||||
import { Knex } from "knex";
|
import { Knex } from "knex";
|
||||||
|
|
||||||
import { TDbClient } from "@app/db";
|
import { TDbClient } from "@app/db";
|
||||||
@@ -11,6 +12,7 @@ import {
|
|||||||
} from "@app/db/schemas";
|
} from "@app/db/schemas";
|
||||||
import { DatabaseError } from "@app/lib/errors";
|
import { DatabaseError } from "@app/lib/errors";
|
||||||
import { ormify, selectAllTableCols, sqlNestRelationships } from "@app/lib/knex";
|
import { ormify, selectAllTableCols, sqlNestRelationships } from "@app/lib/knex";
|
||||||
|
import { logger } from "@app/lib/logger";
|
||||||
|
|
||||||
export type TSnapshotDALFactory = ReturnType<typeof snapshotDALFactory>;
|
export type TSnapshotDALFactory = ReturnType<typeof snapshotDALFactory>;
|
||||||
|
|
||||||
@@ -325,12 +327,152 @@ export const snapshotDALFactory = (db: TDbClient) => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Prunes excess snapshots from the database to ensure only a specified number of recent snapshots are retained for each folder.
|
||||||
|
*
|
||||||
|
* This function operates in three main steps:
|
||||||
|
* 1. Pruning snapshots from root/non-versioned folders.
|
||||||
|
* 2. Pruning snapshots from versioned folders.
|
||||||
|
* 3. Removing orphaned snapshots that do not belong to any existing folder or folder version.
|
||||||
|
*
|
||||||
|
* The function processes snapshots in batches, determined by the `PRUNE_FOLDER_BATCH_SIZE` constant,
|
||||||
|
* to manage the large datasets without overwhelming the DB.
|
||||||
|
*
|
||||||
|
* Steps:
|
||||||
|
* - Fetch a batch of folder IDs.
|
||||||
|
* - For each batch, use a Common Table Expression (CTE) to rank snapshots within each folder by their creation date.
|
||||||
|
* - Identify and delete snapshots that exceed the project's point-in-time version limit (`pitVersionLimit`).
|
||||||
|
* - Repeat the process for versioned folders.
|
||||||
|
* - Finally, delete orphaned snapshots that do not have an associated folder.
|
||||||
|
*/
|
||||||
|
const pruneExcessSnapshots = async () => {
|
||||||
|
const PRUNE_FOLDER_BATCH_SIZE = 10000;
|
||||||
|
|
||||||
|
try {
|
||||||
|
let uuidOffset = "00000000-0000-0000-0000-000000000000";
|
||||||
|
// cleanup snapshots from root/non-versioned folders
|
||||||
|
// eslint-disable-next-line no-constant-condition, no-unreachable-loop
|
||||||
|
while (true) {
|
||||||
|
const folderBatch = await db(TableName.SecretFolder)
|
||||||
|
.where("id", ">", uuidOffset)
|
||||||
|
.where("isReserved", false)
|
||||||
|
.orderBy("id", "asc")
|
||||||
|
.limit(PRUNE_FOLDER_BATCH_SIZE)
|
||||||
|
.select("id");
|
||||||
|
|
||||||
|
const batchEntries = folderBatch.map((folder) => folder.id);
|
||||||
|
|
||||||
|
if (folderBatch.length) {
|
||||||
|
try {
|
||||||
|
logger.info(`Pruning snapshots in [range=${batchEntries[0]}:${batchEntries[batchEntries.length - 1]}]`);
|
||||||
|
await db(TableName.Snapshot)
|
||||||
|
.with("snapshot_cte", (qb) => {
|
||||||
|
void qb
|
||||||
|
.from(TableName.Snapshot)
|
||||||
|
.whereIn(`${TableName.Snapshot}.folderId`, batchEntries)
|
||||||
|
.select(
|
||||||
|
"folderId",
|
||||||
|
`${TableName.Snapshot}.id as id`,
|
||||||
|
db.raw(
|
||||||
|
`ROW_NUMBER() OVER (PARTITION BY ${TableName.Snapshot}."folderId" ORDER BY ${TableName.Snapshot}."createdAt" DESC) AS row_num`
|
||||||
|
)
|
||||||
|
);
|
||||||
|
})
|
||||||
|
.join(TableName.SecretFolder, `${TableName.SecretFolder}.id`, `${TableName.Snapshot}.folderId`)
|
||||||
|
.join(TableName.Environment, `${TableName.Environment}.id`, `${TableName.SecretFolder}.envId`)
|
||||||
|
.join(TableName.Project, `${TableName.Project}.id`, `${TableName.Environment}.projectId`)
|
||||||
|
.join("snapshot_cte", "snapshot_cte.id", `${TableName.Snapshot}.id`)
|
||||||
|
.whereNull(`${TableName.SecretFolder}.parentId`)
|
||||||
|
.whereRaw(`snapshot_cte.row_num > ${TableName.Project}."pitVersionLimit"`)
|
||||||
|
.delete();
|
||||||
|
} catch (err) {
|
||||||
|
logger.error(
|
||||||
|
`Failed to prune snapshots from root/non-versioned folders in range ${batchEntries[0]}:${
|
||||||
|
batchEntries[batchEntries.length - 1]
|
||||||
|
}`
|
||||||
|
);
|
||||||
|
} finally {
|
||||||
|
uuidOffset = batchEntries[batchEntries.length - 1];
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// cleanup snapshots from versioned folders
|
||||||
|
uuidOffset = "00000000-0000-0000-0000-000000000000";
|
||||||
|
// eslint-disable-next-line no-constant-condition
|
||||||
|
while (true) {
|
||||||
|
const folderBatch = await db(TableName.SecretFolderVersion)
|
||||||
|
.select("folderId")
|
||||||
|
.distinct("folderId")
|
||||||
|
.where("folderId", ">", uuidOffset)
|
||||||
|
.orderBy("folderId", "asc")
|
||||||
|
.limit(PRUNE_FOLDER_BATCH_SIZE);
|
||||||
|
|
||||||
|
const batchEntries = folderBatch.map((folder) => folder.folderId);
|
||||||
|
|
||||||
|
if (folderBatch.length) {
|
||||||
|
try {
|
||||||
|
logger.info(`Pruning snapshots in range ${batchEntries[0]}:${batchEntries[batchEntries.length - 1]}`);
|
||||||
|
await db(TableName.Snapshot)
|
||||||
|
.with("snapshot_cte", (qb) => {
|
||||||
|
void qb
|
||||||
|
.from(TableName.Snapshot)
|
||||||
|
.whereIn(`${TableName.Snapshot}.folderId`, batchEntries)
|
||||||
|
.select(
|
||||||
|
"folderId",
|
||||||
|
`${TableName.Snapshot}.id as id`,
|
||||||
|
db.raw(
|
||||||
|
`ROW_NUMBER() OVER (PARTITION BY ${TableName.Snapshot}."folderId" ORDER BY ${TableName.Snapshot}."createdAt" DESC) AS row_num`
|
||||||
|
)
|
||||||
|
);
|
||||||
|
})
|
||||||
|
.join(
|
||||||
|
TableName.SecretFolderVersion,
|
||||||
|
`${TableName.SecretFolderVersion}.folderId`,
|
||||||
|
`${TableName.Snapshot}.folderId`
|
||||||
|
)
|
||||||
|
.join(TableName.Environment, `${TableName.Environment}.id`, `${TableName.SecretFolderVersion}.envId`)
|
||||||
|
.join(TableName.Project, `${TableName.Project}.id`, `${TableName.Environment}.projectId`)
|
||||||
|
.join("snapshot_cte", "snapshot_cte.id", `${TableName.Snapshot}.id`)
|
||||||
|
.whereRaw(`snapshot_cte.row_num > ${TableName.Project}."pitVersionLimit"`)
|
||||||
|
.delete();
|
||||||
|
} catch (err) {
|
||||||
|
logger.error(
|
||||||
|
`Failed to prune snapshots from versioned folders in range ${batchEntries[0]}:${
|
||||||
|
batchEntries[batchEntries.length - 1]
|
||||||
|
}`
|
||||||
|
);
|
||||||
|
} finally {
|
||||||
|
uuidOffset = batchEntries[batchEntries.length - 1];
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// cleanup orphaned snapshots (those that don't belong to an existing folder and folder version)
|
||||||
|
await db(TableName.Snapshot)
|
||||||
|
.whereNotIn("folderId", (qb) => {
|
||||||
|
void qb
|
||||||
|
.select("folderId")
|
||||||
|
.from(TableName.SecretFolderVersion)
|
||||||
|
.union((qb1) => void qb1.select("id").from(TableName.SecretFolder));
|
||||||
|
})
|
||||||
|
.delete();
|
||||||
|
} catch (error) {
|
||||||
|
throw new DatabaseError({ error, name: "SnapshotPrune" });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
return {
|
return {
|
||||||
...secretSnapshotOrm,
|
...secretSnapshotOrm,
|
||||||
findById,
|
findById,
|
||||||
findLatestSnapshotByFolderId,
|
findLatestSnapshotByFolderId,
|
||||||
findRecursivelySnapshots,
|
findRecursivelySnapshots,
|
||||||
countOfSnapshotsByFolderId,
|
countOfSnapshotsByFolderId,
|
||||||
findSecretSnapshotDataById
|
findSecretSnapshotDataById,
|
||||||
|
pruneExcessSnapshots
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
@@ -343,7 +343,8 @@ export const RAW_SECRETS = {
|
|||||||
secretValue: "The value of the secret to create.",
|
secretValue: "The value of the secret to create.",
|
||||||
skipMultilineEncoding: "Skip multiline encoding for the secret value.",
|
skipMultilineEncoding: "Skip multiline encoding for the secret value.",
|
||||||
type: "The type of the secret to create.",
|
type: "The type of the secret to create.",
|
||||||
workspaceId: "The ID of the project to create the secret in."
|
workspaceId: "The ID of the project to create the secret in.",
|
||||||
|
tagIds: "The ID of the tags to be attached to the created secret."
|
||||||
},
|
},
|
||||||
GET: {
|
GET: {
|
||||||
secretName: "The name of the secret to get.",
|
secretName: "The name of the secret to get.",
|
||||||
@@ -364,7 +365,8 @@ export const RAW_SECRETS = {
|
|||||||
skipMultilineEncoding: "Skip multiline encoding for the secret value.",
|
skipMultilineEncoding: "Skip multiline encoding for the secret value.",
|
||||||
type: "The type of the secret to update.",
|
type: "The type of the secret to update.",
|
||||||
projectSlug: "The slug of the project to update the secret in.",
|
projectSlug: "The slug of the project to update the secret in.",
|
||||||
workspaceId: "The ID of the project to update the secret in."
|
workspaceId: "The ID of the project to update the secret in.",
|
||||||
|
tagIds: "The ID of the tags to be attached to the updated secret."
|
||||||
},
|
},
|
||||||
DELETE: {
|
DELETE: {
|
||||||
secretName: "The name of the secret to delete.",
|
secretName: "The name of the secret to delete.",
|
||||||
@@ -677,6 +679,8 @@ export const INTEGRATION = {
|
|||||||
secretAWSTag: "The tags for AWS secrets.",
|
secretAWSTag: "The tags for AWS secrets.",
|
||||||
kmsKeyId: "The ID of the encryption key from AWS KMS.",
|
kmsKeyId: "The ID of the encryption key from AWS KMS.",
|
||||||
shouldDisableDelete: "The flag to disable deletion of secrets in AWS Parameter Store.",
|
shouldDisableDelete: "The flag to disable deletion of secrets in AWS Parameter Store.",
|
||||||
|
shouldMaskSecrets: "Specifies if the secrets synced from Infisical to Gitlab should be marked as 'Masked'.",
|
||||||
|
shouldProtectSecrets: "Specifies if the secrets synced from Infisical to Gitlab should be marked as 'Protected'.",
|
||||||
shouldEnableDelete: "The flag to enable deletion of secrets"
|
shouldEnableDelete: "The flag to enable deletion of secrets"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -726,6 +730,102 @@ export const AUDIT_LOG_STREAMS = {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const CERTIFICATE_AUTHORITIES = {
|
||||||
|
CREATE: {
|
||||||
|
projectSlug: "Slug of the project to create the CA in.",
|
||||||
|
type: "The type of CA to create",
|
||||||
|
friendlyName: "A friendly name for the CA",
|
||||||
|
organization: "The organization (O) for the CA",
|
||||||
|
ou: "The organization unit (OU) for the CA",
|
||||||
|
country: "The country name (C) for the CA",
|
||||||
|
province: "The state of province name for the CA",
|
||||||
|
locality: "The locality name for the CA",
|
||||||
|
commonName: "The common name (CN) for the CA",
|
||||||
|
notBefore: "The date and time when the CA becomes valid in YYYY-MM-DDTHH:mm:ss.sssZ format",
|
||||||
|
notAfter: "The date and time when the CA expires in YYYY-MM-DDTHH:mm:ss.sssZ format",
|
||||||
|
maxPathLength:
|
||||||
|
"The maximum number of intermediate CAs that may follow this CA in the certificate / CA chain. A maxPathLength of -1 implies no path limit on the chain.",
|
||||||
|
keyAlgorithm:
|
||||||
|
"The type of public key algorithm and size, in bits, of the key pair for the CA; when you create an intermediate CA, you must use a key algorithm supported by the parent CA."
|
||||||
|
},
|
||||||
|
GET: {
|
||||||
|
caId: "The ID of the CA to get"
|
||||||
|
},
|
||||||
|
UPDATE: {
|
||||||
|
caId: "The ID of the CA to update",
|
||||||
|
status: "The status of the CA to update to. This can be one of active or disabled"
|
||||||
|
},
|
||||||
|
DELETE: {
|
||||||
|
caId: "The ID of the CA to delete"
|
||||||
|
},
|
||||||
|
GET_CSR: {
|
||||||
|
caId: "The ID of the CA to generate CSR from",
|
||||||
|
csr: "The generated CSR from the CA"
|
||||||
|
},
|
||||||
|
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"
|
||||||
|
},
|
||||||
|
SIGN_INTERMEDIATE: {
|
||||||
|
caId: "The ID of the CA to sign the intermediate certificate with",
|
||||||
|
csr: "The CSR to sign with the CA",
|
||||||
|
notBefore: "The date and time when the intermediate CA becomes valid in YYYY-MM-DDTHH:mm:ss.sssZ format",
|
||||||
|
notAfter: "The date and time when the intermediate CA expires in YYYY-MM-DDTHH:mm:ss.sssZ format",
|
||||||
|
maxPathLength:
|
||||||
|
"The maximum number of intermediate CAs that may follow this CA in the certificate / CA chain. A maxPathLength of -1 implies no path limit on the chain.",
|
||||||
|
certificate: "The signed intermediate certificate",
|
||||||
|
certificateChain: "The certificate chain of the intermediate certificate",
|
||||||
|
issuingCaCertificate: "The certificate of the issuing CA",
|
||||||
|
serialNumber: "The serial number of the intermediate certificate"
|
||||||
|
},
|
||||||
|
IMPORT_CERT: {
|
||||||
|
caId: "The ID of the CA to import the certificate for",
|
||||||
|
certificate: "The certificate body to import",
|
||||||
|
certificateChain: "The certificate chain to import"
|
||||||
|
},
|
||||||
|
ISSUE_CERT: {
|
||||||
|
caId: "The ID of the CA to issue the certificate from",
|
||||||
|
friendlyName: "A friendly name for the certificate",
|
||||||
|
commonName: "The common name (CN) for the certificate",
|
||||||
|
ttl: "The time to live for the certificate such as 1m, 1h, 1d, 1y, ...",
|
||||||
|
notBefore: "The date and time when the certificate becomes valid in YYYY-MM-DDTHH:mm:ss.sssZ format",
|
||||||
|
notAfter: "The date and time when the certificate expires in YYYY-MM-DDTHH:mm:ss.sssZ format",
|
||||||
|
certificate: "The issued certificate",
|
||||||
|
issuingCaCertificate: "The certificate of the issuing CA",
|
||||||
|
certificateChain: "The certificate chain of the issued certificate",
|
||||||
|
privateKey: "The private key of the issued certificate",
|
||||||
|
serialNumber: "The serial number of the issued certificate"
|
||||||
|
},
|
||||||
|
GET_CRL: {
|
||||||
|
caId: "The ID of the CA to get the certificate revocation list (CRL) for",
|
||||||
|
crl: "The certificate revocation list (CRL) of the CA"
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export const CERTIFICATES = {
|
||||||
|
GET: {
|
||||||
|
serialNumber: "The serial number of the certificate to get"
|
||||||
|
},
|
||||||
|
REVOKE: {
|
||||||
|
serialNumber:
|
||||||
|
"The serial number of the certificate to revoke. The revoked certificate will be added to the certificate revocation list (CRL) of the CA.",
|
||||||
|
revocationReason: "The reason for revoking the certificate.",
|
||||||
|
revokedAt: "The date and time when the certificate was revoked",
|
||||||
|
serialNumberRes: "The serial number of the revoked certificate."
|
||||||
|
},
|
||||||
|
DELETE: {
|
||||||
|
serialNumber: "The serial number of the certificate to delete"
|
||||||
|
},
|
||||||
|
GET_CERT: {
|
||||||
|
serialNumber: "The serial number of the certificate to get the certificate body and certificate chain for",
|
||||||
|
certificate: "The certificate body of the certificate",
|
||||||
|
certificateChain: "The certificate chain of the certificate",
|
||||||
|
serialNumberRes: "The serial number of the certificate"
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
export const PROJECT_ROLE = {
|
export const PROJECT_ROLE = {
|
||||||
CREATE: {
|
CREATE: {
|
||||||
projectSlug: "Slug of the project to create the role for.",
|
projectSlug: "Slug of the project to create the role for.",
|
||||||
|
@@ -39,7 +39,9 @@ const envSchema = z
|
|||||||
HTTPS_ENABLED: zodStrBool,
|
HTTPS_ENABLED: zodStrBool,
|
||||||
// smtp options
|
// smtp options
|
||||||
SMTP_HOST: zpStr(z.string().optional()),
|
SMTP_HOST: zpStr(z.string().optional()),
|
||||||
SMTP_SECURE: zodStrBool,
|
SMTP_IGNORE_TLS: zodStrBool.default("false"),
|
||||||
|
SMTP_REQUIRE_TLS: zodStrBool.default("true"),
|
||||||
|
SMTP_TLS_REJECT_UNAUTHORIZED: zodStrBool.default("true"),
|
||||||
SMTP_PORT: z.coerce.number().default(587),
|
SMTP_PORT: z.coerce.number().default(587),
|
||||||
SMTP_USERNAME: zpStr(z.string().optional()),
|
SMTP_USERNAME: zpStr(z.string().optional()),
|
||||||
SMTP_PASSWORD: zpStr(z.string().optional()),
|
SMTP_PASSWORD: zpStr(z.string().optional()),
|
||||||
@@ -153,13 +155,20 @@ export const initEnvConfig = (logger: Logger) => {
|
|||||||
return envCfg;
|
return envCfg;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const formatSmtpConfig = () => ({
|
export const formatSmtpConfig = () => {
|
||||||
host: envCfg.SMTP_HOST,
|
return {
|
||||||
port: envCfg.SMTP_PORT,
|
host: envCfg.SMTP_HOST,
|
||||||
auth:
|
port: envCfg.SMTP_PORT,
|
||||||
envCfg.SMTP_USERNAME && envCfg.SMTP_PASSWORD
|
auth:
|
||||||
? { user: envCfg.SMTP_USERNAME, pass: envCfg.SMTP_PASSWORD }
|
envCfg.SMTP_USERNAME && envCfg.SMTP_PASSWORD
|
||||||
: undefined,
|
? { user: envCfg.SMTP_USERNAME, pass: envCfg.SMTP_PASSWORD }
|
||||||
secure: envCfg.SMTP_SECURE,
|
: undefined,
|
||||||
from: `"${envCfg.SMTP_FROM_NAME}" <${envCfg.SMTP_FROM_ADDRESS}>`
|
secure: envCfg.SMTP_PORT === 465,
|
||||||
});
|
from: `"${envCfg.SMTP_FROM_NAME}" <${envCfg.SMTP_FROM_ADDRESS}>`,
|
||||||
|
ignoreTLS: envCfg.SMTP_IGNORE_TLS,
|
||||||
|
requireTLS: envCfg.SMTP_REQUIRE_TLS,
|
||||||
|
tls: {
|
||||||
|
rejectUnauthorized: envCfg.SMTP_TLS_REJECT_UNAUTHORIZED
|
||||||
|
}
|
||||||
|
};
|
||||||
|
};
|
||||||
|
@@ -23,6 +23,7 @@ export enum QueueName {
|
|||||||
SecretPushEventScan = "secret-push-event-scan",
|
SecretPushEventScan = "secret-push-event-scan",
|
||||||
UpgradeProjectToGhost = "upgrade-project-to-ghost",
|
UpgradeProjectToGhost = "upgrade-project-to-ghost",
|
||||||
DynamicSecretRevocation = "dynamic-secret-revocation",
|
DynamicSecretRevocation = "dynamic-secret-revocation",
|
||||||
|
CaCrlRotation = "ca-crl-rotation",
|
||||||
SecretReplication = "secret-replication",
|
SecretReplication = "secret-replication",
|
||||||
SecretSync = "secret-sync" // parent queue to push integration sync, webhook, and secret replication
|
SecretSync = "secret-sync" // parent queue to push integration sync, webhook, and secret replication
|
||||||
}
|
}
|
||||||
@@ -41,6 +42,7 @@ export enum QueueJobs {
|
|||||||
UpgradeProjectToGhost = "upgrade-project-to-ghost-job",
|
UpgradeProjectToGhost = "upgrade-project-to-ghost-job",
|
||||||
DynamicSecretRevocation = "dynamic-secret-revocation",
|
DynamicSecretRevocation = "dynamic-secret-revocation",
|
||||||
DynamicSecretPruning = "dynamic-secret-pruning",
|
DynamicSecretPruning = "dynamic-secret-pruning",
|
||||||
|
CaCrlRotation = "ca-crl-rotation-job",
|
||||||
SecretReplication = "secret-replication",
|
SecretReplication = "secret-replication",
|
||||||
SecretSync = "secret-sync" // parent queue to push integration sync, webhook, and secret replication
|
SecretSync = "secret-sync" // parent queue to push integration sync, webhook, and secret replication
|
||||||
}
|
}
|
||||||
@@ -55,7 +57,6 @@ export type TQueueJobTypes = {
|
|||||||
};
|
};
|
||||||
name: QueueJobs.SecretReminder;
|
name: QueueJobs.SecretReminder;
|
||||||
};
|
};
|
||||||
|
|
||||||
[QueueName.SecretRotation]: {
|
[QueueName.SecretRotation]: {
|
||||||
payload: { rotationId: string };
|
payload: { rotationId: string };
|
||||||
name: QueueJobs.SecretRotation;
|
name: QueueJobs.SecretRotation;
|
||||||
@@ -121,6 +122,12 @@ export type TQueueJobTypes = {
|
|||||||
dynamicSecretCfgId: string;
|
dynamicSecretCfgId: string;
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
[QueueName.CaCrlRotation]: {
|
||||||
|
name: QueueJobs.CaCrlRotation;
|
||||||
|
payload: {
|
||||||
|
caId: string;
|
||||||
|
};
|
||||||
|
};
|
||||||
[QueueName.SecretReplication]: {
|
[QueueName.SecretReplication]: {
|
||||||
name: QueueJobs.SecretReplication;
|
name: QueueJobs.SecretReplication;
|
||||||
payload: TSyncSecretsDTO;
|
payload: TSyncSecretsDTO;
|
||||||
|
@@ -71,6 +71,7 @@ export const main = async ({ db, smtp, logger, queue, keyStore }: TMain) => {
|
|||||||
if (appCfg.isProductionMode) {
|
if (appCfg.isProductionMode) {
|
||||||
await server.register<FastifyRateLimitOptions>(ratelimiter, globalRateLimiterCfg());
|
await server.register<FastifyRateLimitOptions>(ratelimiter, globalRateLimiterCfg());
|
||||||
}
|
}
|
||||||
|
|
||||||
await server.register(helmet, { contentSecurityPolicy: false });
|
await server.register(helmet, { contentSecurityPolicy: false });
|
||||||
|
|
||||||
await server.register(maintenanceMode);
|
await server.register(maintenanceMode);
|
||||||
|
@@ -5,7 +5,6 @@ import { createTransport } from "nodemailer";
|
|||||||
|
|
||||||
import { formatSmtpConfig, getConfig } from "@app/lib/config/env";
|
import { formatSmtpConfig, getConfig } from "@app/lib/config/env";
|
||||||
import { logger } from "@app/lib/logger";
|
import { logger } from "@app/lib/logger";
|
||||||
import { getTlsOption } from "@app/services/smtp/smtp-service";
|
|
||||||
import { getServerCfg } from "@app/services/super-admin/super-admin-service";
|
import { getServerCfg } from "@app/services/super-admin/super-admin-service";
|
||||||
|
|
||||||
type BootstrapOpt = {
|
type BootstrapOpt = {
|
||||||
@@ -44,7 +43,7 @@ export const bootstrapCheck = async ({ db }: BootstrapOpt) => {
|
|||||||
console.info("Testing smtp connection");
|
console.info("Testing smtp connection");
|
||||||
|
|
||||||
const smtpCfg = formatSmtpConfig();
|
const smtpCfg = formatSmtpConfig();
|
||||||
await createTransport({ ...smtpCfg, ...getTlsOption(smtpCfg.host, smtpCfg.secure) })
|
await createTransport(smtpCfg)
|
||||||
.verify()
|
.verify()
|
||||||
.then(async () => {
|
.then(async () => {
|
||||||
console.info("SMTP successfully connected");
|
console.info("SMTP successfully connected");
|
||||||
|
@@ -1,6 +1,7 @@
|
|||||||
import type { RateLimitOptions, RateLimitPluginOptions } from "@fastify/rate-limit";
|
import type { RateLimitOptions, RateLimitPluginOptions } from "@fastify/rate-limit";
|
||||||
import { Redis } from "ioredis";
|
import { Redis } from "ioredis";
|
||||||
|
|
||||||
|
import { getRateLimiterConfig } from "@app/ee/services/rate-limit/rate-limit-service";
|
||||||
import { getConfig } from "@app/lib/config/env";
|
import { getConfig } from "@app/lib/config/env";
|
||||||
|
|
||||||
export const globalRateLimiterCfg = (): RateLimitPluginOptions => {
|
export const globalRateLimiterCfg = (): RateLimitPluginOptions => {
|
||||||
@@ -21,14 +22,14 @@ export const globalRateLimiterCfg = (): RateLimitPluginOptions => {
|
|||||||
// GET endpoints
|
// GET endpoints
|
||||||
export const readLimit: RateLimitOptions = {
|
export const readLimit: RateLimitOptions = {
|
||||||
timeWindow: 60 * 1000,
|
timeWindow: 60 * 1000,
|
||||||
max: 600,
|
max: () => getRateLimiterConfig().readLimit,
|
||||||
keyGenerator: (req) => req.realIp
|
keyGenerator: (req) => req.realIp
|
||||||
};
|
};
|
||||||
|
|
||||||
// POST, PATCH, PUT, DELETE endpoints
|
// POST, PATCH, PUT, DELETE endpoints
|
||||||
export const writeLimit: RateLimitOptions = {
|
export const writeLimit: RateLimitOptions = {
|
||||||
timeWindow: 60 * 1000,
|
timeWindow: 60 * 1000,
|
||||||
max: 200, // (too low, FA having issues so increasing it - maidul)
|
max: () => getRateLimiterConfig().writeLimit,
|
||||||
keyGenerator: (req) => req.realIp
|
keyGenerator: (req) => req.realIp
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -36,25 +37,25 @@ export const writeLimit: RateLimitOptions = {
|
|||||||
export const secretsLimit: RateLimitOptions = {
|
export const secretsLimit: RateLimitOptions = {
|
||||||
// secrets, folders, secret imports
|
// secrets, folders, secret imports
|
||||||
timeWindow: 60 * 1000,
|
timeWindow: 60 * 1000,
|
||||||
max: 60,
|
max: () => getRateLimiterConfig().secretsLimit,
|
||||||
keyGenerator: (req) => req.realIp
|
keyGenerator: (req) => req.realIp
|
||||||
};
|
};
|
||||||
|
|
||||||
export const authRateLimit: RateLimitOptions = {
|
export const authRateLimit: RateLimitOptions = {
|
||||||
timeWindow: 60 * 1000,
|
timeWindow: 60 * 1000,
|
||||||
max: 60,
|
max: () => getRateLimiterConfig().authRateLimit,
|
||||||
keyGenerator: (req) => req.realIp
|
keyGenerator: (req) => req.realIp
|
||||||
};
|
};
|
||||||
|
|
||||||
export const inviteUserRateLimit: RateLimitOptions = {
|
export const inviteUserRateLimit: RateLimitOptions = {
|
||||||
timeWindow: 60 * 1000,
|
timeWindow: 60 * 1000,
|
||||||
max: 30,
|
max: () => getRateLimiterConfig().inviteUserRateLimit,
|
||||||
keyGenerator: (req) => req.realIp
|
keyGenerator: (req) => req.realIp
|
||||||
};
|
};
|
||||||
|
|
||||||
export const mfaRateLimit: RateLimitOptions = {
|
export const mfaRateLimit: RateLimitOptions = {
|
||||||
timeWindow: 60 * 1000,
|
timeWindow: 60 * 1000,
|
||||||
max: 20,
|
max: () => getRateLimiterConfig().mfaRateLimit,
|
||||||
keyGenerator: (req) => {
|
keyGenerator: (req) => {
|
||||||
return req.headers.authorization?.split(" ")[1] || req.realIp;
|
return req.headers.authorization?.split(" ")[1] || req.realIp;
|
||||||
}
|
}
|
||||||
@@ -63,14 +64,21 @@ export const mfaRateLimit: RateLimitOptions = {
|
|||||||
export const creationLimit: RateLimitOptions = {
|
export const creationLimit: RateLimitOptions = {
|
||||||
// identity, project, org
|
// identity, project, org
|
||||||
timeWindow: 60 * 1000,
|
timeWindow: 60 * 1000,
|
||||||
max: 30,
|
max: () => getRateLimiterConfig().creationLimit,
|
||||||
keyGenerator: (req) => req.realIp
|
keyGenerator: (req) => req.realIp
|
||||||
};
|
};
|
||||||
|
|
||||||
// Public endpoints to avoid brute force attacks
|
// Public endpoints to avoid brute force attacks
|
||||||
export const publicEndpointLimit: RateLimitOptions = {
|
export const publicEndpointLimit: RateLimitOptions = {
|
||||||
// Shared Secrets
|
// Read Shared Secrets
|
||||||
timeWindow: 60 * 1000,
|
timeWindow: 60 * 1000,
|
||||||
max: 30,
|
max: () => getRateLimiterConfig().publicEndpointLimit,
|
||||||
|
keyGenerator: (req) => req.realIp
|
||||||
|
};
|
||||||
|
|
||||||
|
export const publicSecretShareCreationLimit: RateLimitOptions = {
|
||||||
|
// Create Shared Secrets
|
||||||
|
timeWindow: 60 * 1000,
|
||||||
|
max: 5,
|
||||||
keyGenerator: (req) => req.realIp
|
keyGenerator: (req) => req.realIp
|
||||||
};
|
};
|
||||||
|
@@ -1,3 +1,4 @@
|
|||||||
|
import { CronJob } from "cron";
|
||||||
import { Knex } from "knex";
|
import { Knex } from "knex";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
|
|
||||||
@@ -13,6 +14,8 @@ import { auditLogQueueServiceFactory } from "@app/ee/services/audit-log/audit-lo
|
|||||||
import { auditLogServiceFactory } from "@app/ee/services/audit-log/audit-log-service";
|
import { auditLogServiceFactory } from "@app/ee/services/audit-log/audit-log-service";
|
||||||
import { auditLogStreamDALFactory } from "@app/ee/services/audit-log-stream/audit-log-stream-dal";
|
import { auditLogStreamDALFactory } from "@app/ee/services/audit-log-stream/audit-log-stream-dal";
|
||||||
import { auditLogStreamServiceFactory } from "@app/ee/services/audit-log-stream/audit-log-stream-service";
|
import { auditLogStreamServiceFactory } from "@app/ee/services/audit-log-stream/audit-log-stream-service";
|
||||||
|
import { certificateAuthorityCrlDALFactory } from "@app/ee/services/certificate-authority-crl/certificate-authority-crl-dal";
|
||||||
|
import { certificateAuthorityCrlServiceFactory } from "@app/ee/services/certificate-authority-crl/certificate-authority-crl-service";
|
||||||
import { dynamicSecretDALFactory } from "@app/ee/services/dynamic-secret/dynamic-secret-dal";
|
import { dynamicSecretDALFactory } from "@app/ee/services/dynamic-secret/dynamic-secret-dal";
|
||||||
import { dynamicSecretServiceFactory } from "@app/ee/services/dynamic-secret/dynamic-secret-service";
|
import { dynamicSecretServiceFactory } from "@app/ee/services/dynamic-secret/dynamic-secret-service";
|
||||||
import { buildDynamicSecretProviders } from "@app/ee/services/dynamic-secret/providers";
|
import { buildDynamicSecretProviders } from "@app/ee/services/dynamic-secret/providers";
|
||||||
@@ -33,6 +36,8 @@ import { permissionDALFactory } from "@app/ee/services/permission/permission-dal
|
|||||||
import { permissionServiceFactory } from "@app/ee/services/permission/permission-service";
|
import { permissionServiceFactory } from "@app/ee/services/permission/permission-service";
|
||||||
import { projectUserAdditionalPrivilegeDALFactory } from "@app/ee/services/project-user-additional-privilege/project-user-additional-privilege-dal";
|
import { projectUserAdditionalPrivilegeDALFactory } from "@app/ee/services/project-user-additional-privilege/project-user-additional-privilege-dal";
|
||||||
import { projectUserAdditionalPrivilegeServiceFactory } from "@app/ee/services/project-user-additional-privilege/project-user-additional-privilege-service";
|
import { projectUserAdditionalPrivilegeServiceFactory } from "@app/ee/services/project-user-additional-privilege/project-user-additional-privilege-service";
|
||||||
|
import { rateLimitDALFactory } from "@app/ee/services/rate-limit/rate-limit-dal";
|
||||||
|
import { rateLimitServiceFactory } from "@app/ee/services/rate-limit/rate-limit-service";
|
||||||
import { samlConfigDALFactory } from "@app/ee/services/saml-config/saml-config-dal";
|
import { samlConfigDALFactory } from "@app/ee/services/saml-config/saml-config-dal";
|
||||||
import { samlConfigServiceFactory } from "@app/ee/services/saml-config/saml-config-service";
|
import { samlConfigServiceFactory } from "@app/ee/services/saml-config/saml-config-service";
|
||||||
import { scimDALFactory } from "@app/ee/services/scim/scim-dal";
|
import { scimDALFactory } from "@app/ee/services/scim/scim-dal";
|
||||||
@@ -71,6 +76,14 @@ import { authPaswordServiceFactory } from "@app/services/auth/auth-password-serv
|
|||||||
import { authSignupServiceFactory } from "@app/services/auth/auth-signup-service";
|
import { authSignupServiceFactory } from "@app/services/auth/auth-signup-service";
|
||||||
import { tokenDALFactory } from "@app/services/auth-token/auth-token-dal";
|
import { tokenDALFactory } from "@app/services/auth-token/auth-token-dal";
|
||||||
import { tokenServiceFactory } from "@app/services/auth-token/auth-token-service";
|
import { tokenServiceFactory } from "@app/services/auth-token/auth-token-service";
|
||||||
|
import { certificateBodyDALFactory } from "@app/services/certificate/certificate-body-dal";
|
||||||
|
import { certificateDALFactory } from "@app/services/certificate/certificate-dal";
|
||||||
|
import { certificateServiceFactory } from "@app/services/certificate/certificate-service";
|
||||||
|
import { certificateAuthorityCertDALFactory } from "@app/services/certificate-authority/certificate-authority-cert-dal";
|
||||||
|
import { certificateAuthorityDALFactory } from "@app/services/certificate-authority/certificate-authority-dal";
|
||||||
|
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 { groupProjectDALFactory } from "@app/services/group-project/group-project-dal";
|
import { groupProjectDALFactory } from "@app/services/group-project/group-project-dal";
|
||||||
import { groupProjectMembershipRoleDALFactory } from "@app/services/group-project/group-project-membership-role-dal";
|
import { groupProjectMembershipRoleDALFactory } from "@app/services/group-project/group-project-membership-role-dal";
|
||||||
import { groupProjectServiceFactory } from "@app/services/group-project/group-project-service";
|
import { groupProjectServiceFactory } from "@app/services/group-project/group-project-service";
|
||||||
@@ -185,6 +198,7 @@ export const registerRoutes = async (
|
|||||||
const incidentContactDAL = incidentContactDALFactory(db);
|
const incidentContactDAL = incidentContactDALFactory(db);
|
||||||
const orgRoleDAL = orgRoleDALFactory(db);
|
const orgRoleDAL = orgRoleDALFactory(db);
|
||||||
const superAdminDAL = superAdminDALFactory(db);
|
const superAdminDAL = superAdminDALFactory(db);
|
||||||
|
const rateLimitDAL = rateLimitDALFactory(db);
|
||||||
const apiKeyDAL = apiKeyDALFactory(db);
|
const apiKeyDAL = apiKeyDALFactory(db);
|
||||||
|
|
||||||
const projectDAL = projectDALFactory(db);
|
const projectDAL = projectDALFactory(db);
|
||||||
@@ -444,6 +458,10 @@ export const registerRoutes = async (
|
|||||||
orgService,
|
orgService,
|
||||||
keyStore
|
keyStore
|
||||||
});
|
});
|
||||||
|
const rateLimitService = rateLimitServiceFactory({
|
||||||
|
rateLimitDAL,
|
||||||
|
licenseService
|
||||||
|
});
|
||||||
const apiKeyService = apiKeyServiceFactory({ apiKeyDAL, userDAL });
|
const apiKeyService = apiKeyServiceFactory({ apiKeyDAL, userDAL });
|
||||||
|
|
||||||
const secretScanningQueue = secretScanningQueueFactory({
|
const secretScanningQueue = secretScanningQueueFactory({
|
||||||
@@ -506,6 +524,58 @@ export const registerRoutes = async (
|
|||||||
projectUserMembershipRoleDAL
|
projectUserMembershipRoleDAL
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const certificateAuthorityDAL = certificateAuthorityDALFactory(db);
|
||||||
|
const certificateAuthorityCertDAL = certificateAuthorityCertDALFactory(db);
|
||||||
|
const certificateAuthoritySecretDAL = certificateAuthoritySecretDALFactory(db);
|
||||||
|
const certificateAuthorityCrlDAL = certificateAuthorityCrlDALFactory(db);
|
||||||
|
|
||||||
|
const certificateDAL = certificateDALFactory(db);
|
||||||
|
const certificateBodyDAL = certificateBodyDALFactory(db);
|
||||||
|
|
||||||
|
const certificateService = certificateServiceFactory({
|
||||||
|
certificateDAL,
|
||||||
|
certificateBodyDAL,
|
||||||
|
certificateAuthorityDAL,
|
||||||
|
certificateAuthorityCertDAL,
|
||||||
|
certificateAuthorityCrlDAL,
|
||||||
|
certificateAuthoritySecretDAL,
|
||||||
|
projectDAL,
|
||||||
|
kmsService,
|
||||||
|
permissionService
|
||||||
|
});
|
||||||
|
|
||||||
|
const certificateAuthorityQueue = certificateAuthorityQueueFactory({
|
||||||
|
certificateAuthorityCrlDAL,
|
||||||
|
certificateAuthorityDAL,
|
||||||
|
certificateAuthoritySecretDAL,
|
||||||
|
certificateDAL,
|
||||||
|
projectDAL,
|
||||||
|
kmsService,
|
||||||
|
queueService
|
||||||
|
});
|
||||||
|
|
||||||
|
const certificateAuthorityService = certificateAuthorityServiceFactory({
|
||||||
|
certificateAuthorityDAL,
|
||||||
|
certificateAuthorityCertDAL,
|
||||||
|
certificateAuthoritySecretDAL,
|
||||||
|
certificateAuthorityCrlDAL,
|
||||||
|
certificateAuthorityQueue,
|
||||||
|
certificateDAL,
|
||||||
|
certificateBodyDAL,
|
||||||
|
projectDAL,
|
||||||
|
kmsService,
|
||||||
|
permissionService
|
||||||
|
});
|
||||||
|
|
||||||
|
const certificateAuthorityCrlService = certificateAuthorityCrlServiceFactory({
|
||||||
|
certificateAuthorityDAL,
|
||||||
|
certificateAuthorityCrlDAL,
|
||||||
|
projectDAL,
|
||||||
|
kmsService,
|
||||||
|
permissionService,
|
||||||
|
licenseService
|
||||||
|
});
|
||||||
|
|
||||||
const projectService = projectServiceFactory({
|
const projectService = projectServiceFactory({
|
||||||
permissionService,
|
permissionService,
|
||||||
projectDAL,
|
projectDAL,
|
||||||
@@ -522,6 +592,8 @@ export const registerRoutes = async (
|
|||||||
projectMembershipDAL,
|
projectMembershipDAL,
|
||||||
folderDAL,
|
folderDAL,
|
||||||
licenseService,
|
licenseService,
|
||||||
|
certificateAuthorityDAL,
|
||||||
|
certificateDAL,
|
||||||
projectUserMembershipRoleDAL,
|
projectUserMembershipRoleDAL,
|
||||||
identityProjectMembershipRoleDAL,
|
identityProjectMembershipRoleDAL,
|
||||||
keyStore
|
keyStore
|
||||||
@@ -824,6 +896,9 @@ export const registerRoutes = async (
|
|||||||
const dailyResourceCleanUp = dailyResourceCleanUpQueueServiceFactory({
|
const dailyResourceCleanUp = dailyResourceCleanUpQueueServiceFactory({
|
||||||
auditLogDAL,
|
auditLogDAL,
|
||||||
queueService,
|
queueService,
|
||||||
|
secretVersionDAL,
|
||||||
|
secretFolderVersionDAL: folderVersionDAL,
|
||||||
|
snapshotDAL,
|
||||||
identityAccessTokenDAL,
|
identityAccessTokenDAL,
|
||||||
secretSharingDAL
|
secretSharingDAL
|
||||||
});
|
});
|
||||||
@@ -859,6 +934,7 @@ export const registerRoutes = async (
|
|||||||
secret: secretService,
|
secret: secretService,
|
||||||
secretReplication: secretReplicationService,
|
secretReplication: secretReplicationService,
|
||||||
secretTag: secretTagService,
|
secretTag: secretTagService,
|
||||||
|
rateLimit: rateLimitService,
|
||||||
folder: folderService,
|
folder: folderService,
|
||||||
secretImport: secretImportService,
|
secretImport: secretImportService,
|
||||||
projectBot: projectBotService,
|
projectBot: projectBotService,
|
||||||
@@ -886,6 +962,9 @@ export const registerRoutes = async (
|
|||||||
ldap: ldapService,
|
ldap: ldapService,
|
||||||
auditLog: auditLogService,
|
auditLog: auditLogService,
|
||||||
auditLogStream: auditLogStreamService,
|
auditLogStream: auditLogStreamService,
|
||||||
|
certificate: certificateService,
|
||||||
|
certificateAuthority: certificateAuthorityService,
|
||||||
|
certificateAuthorityCrl: certificateAuthorityCrlService,
|
||||||
secretScanning: secretScanningService,
|
secretScanning: secretScanningService,
|
||||||
license: licenseService,
|
license: licenseService,
|
||||||
trustedIp: trustedIpService,
|
trustedIp: trustedIpService,
|
||||||
@@ -897,6 +976,14 @@ export const registerRoutes = async (
|
|||||||
secretSharing: secretSharingService
|
secretSharing: secretSharingService
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const cronJobs: CronJob[] = [];
|
||||||
|
if (appCfg.isProductionMode) {
|
||||||
|
const rateLimitSyncJob = await rateLimitService.initializeBackgroundSync();
|
||||||
|
if (rateLimitSyncJob) {
|
||||||
|
cronJobs.push(rateLimitSyncJob);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
server.decorate<FastifyZodProvider["store"]>("store", {
|
server.decorate<FastifyZodProvider["store"]>("store", {
|
||||||
user: userDAL
|
user: userDAL
|
||||||
});
|
});
|
||||||
@@ -951,6 +1038,7 @@ export const registerRoutes = async (
|
|||||||
await server.register(registerV3Routes, { prefix: "/api/v3" });
|
await server.register(registerV3Routes, { prefix: "/api/v3" });
|
||||||
|
|
||||||
server.addHook("onClose", async () => {
|
server.addHook("onClose", async () => {
|
||||||
|
cronJobs.forEach((job) => job.stop());
|
||||||
await telemetryService.flushAll();
|
await telemetryService.flushAll();
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
515
backend/src/server/routes/v1/certificate-authority-router.ts
Normal file
515
backend/src/server/routes/v1/certificate-authority-router.ts
Normal file
@@ -0,0 +1,515 @@
|
|||||||
|
import ms from "ms";
|
||||||
|
import { z } from "zod";
|
||||||
|
|
||||||
|
import { CertificateAuthoritiesSchema } from "@app/db/schemas";
|
||||||
|
import { EventType } from "@app/ee/services/audit-log/audit-log-types";
|
||||||
|
import { CERTIFICATE_AUTHORITIES } 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 { CertKeyAlgorithm } from "@app/services/certificate/certificate-types";
|
||||||
|
import { CaStatus, CaType } from "@app/services/certificate-authority/certificate-authority-types";
|
||||||
|
import { validateCaDateField } from "@app/services/certificate-authority/certificate-authority-validators";
|
||||||
|
|
||||||
|
export const registerCaRouter = async (server: FastifyZodProvider) => {
|
||||||
|
server.route({
|
||||||
|
method: "POST",
|
||||||
|
url: "/",
|
||||||
|
config: {
|
||||||
|
rateLimit: writeLimit
|
||||||
|
},
|
||||||
|
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
||||||
|
schema: {
|
||||||
|
description: "Create CA",
|
||||||
|
body: z
|
||||||
|
.object({
|
||||||
|
projectSlug: z.string().trim().describe(CERTIFICATE_AUTHORITIES.CREATE.projectSlug),
|
||||||
|
type: z.nativeEnum(CaType).describe(CERTIFICATE_AUTHORITIES.CREATE.type),
|
||||||
|
friendlyName: z.string().optional().describe(CERTIFICATE_AUTHORITIES.CREATE.friendlyName),
|
||||||
|
commonName: z.string().trim().describe(CERTIFICATE_AUTHORITIES.CREATE.commonName),
|
||||||
|
organization: z.string().trim().describe(CERTIFICATE_AUTHORITIES.CREATE.organization),
|
||||||
|
ou: z.string().trim().describe(CERTIFICATE_AUTHORITIES.CREATE.ou),
|
||||||
|
country: z.string().trim().describe(CERTIFICATE_AUTHORITIES.CREATE.country),
|
||||||
|
province: z.string().trim().describe(CERTIFICATE_AUTHORITIES.CREATE.province),
|
||||||
|
locality: z.string().trim().describe(CERTIFICATE_AUTHORITIES.CREATE.locality),
|
||||||
|
// format: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Date#date_time_string_format
|
||||||
|
notBefore: validateCaDateField.optional().describe(CERTIFICATE_AUTHORITIES.CREATE.notBefore),
|
||||||
|
notAfter: validateCaDateField.optional().describe(CERTIFICATE_AUTHORITIES.CREATE.notAfter),
|
||||||
|
maxPathLength: z.number().min(-1).default(-1).describe(CERTIFICATE_AUTHORITIES.CREATE.maxPathLength),
|
||||||
|
keyAlgorithm: z
|
||||||
|
.nativeEnum(CertKeyAlgorithm)
|
||||||
|
.default(CertKeyAlgorithm.RSA_2048)
|
||||||
|
.describe(CERTIFICATE_AUTHORITIES.CREATE.keyAlgorithm)
|
||||||
|
})
|
||||||
|
.refine(
|
||||||
|
(data) => {
|
||||||
|
// Check that at least one of the specified fields is non-empty
|
||||||
|
return [data.commonName, data.organization, data.ou, data.country, data.province, data.locality].some(
|
||||||
|
(field) => field !== ""
|
||||||
|
);
|
||||||
|
},
|
||||||
|
{
|
||||||
|
message:
|
||||||
|
"At least one of the fields commonName, organization, ou, country, province, or locality must be non-empty",
|
||||||
|
path: []
|
||||||
|
}
|
||||||
|
),
|
||||||
|
response: {
|
||||||
|
200: z.object({
|
||||||
|
ca: CertificateAuthoritiesSchema
|
||||||
|
})
|
||||||
|
}
|
||||||
|
},
|
||||||
|
handler: async (req) => {
|
||||||
|
const ca = await server.services.certificateAuthority.createCa({
|
||||||
|
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.CREATE_CA,
|
||||||
|
metadata: {
|
||||||
|
caId: ca.id,
|
||||||
|
dn: ca.dn
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
ca
|
||||||
|
};
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
server.route({
|
||||||
|
method: "GET",
|
||||||
|
url: "/:caId",
|
||||||
|
config: {
|
||||||
|
rateLimit: readLimit
|
||||||
|
},
|
||||||
|
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
||||||
|
schema: {
|
||||||
|
description: "Get CA",
|
||||||
|
params: z.object({
|
||||||
|
caId: z.string().trim().describe(CERTIFICATE_AUTHORITIES.GET.caId)
|
||||||
|
}),
|
||||||
|
response: {
|
||||||
|
200: z.object({
|
||||||
|
ca: CertificateAuthoritiesSchema
|
||||||
|
})
|
||||||
|
}
|
||||||
|
},
|
||||||
|
handler: async (req) => {
|
||||||
|
const ca = await server.services.certificateAuthority.getCaById({
|
||||||
|
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,
|
||||||
|
metadata: {
|
||||||
|
caId: ca.id,
|
||||||
|
dn: ca.dn
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
ca
|
||||||
|
};
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
server.route({
|
||||||
|
method: "PATCH",
|
||||||
|
url: "/:caId",
|
||||||
|
config: {
|
||||||
|
rateLimit: readLimit
|
||||||
|
},
|
||||||
|
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
||||||
|
schema: {
|
||||||
|
description: "Update CA",
|
||||||
|
params: z.object({
|
||||||
|
caId: z.string().trim().describe(CERTIFICATE_AUTHORITIES.UPDATE.caId)
|
||||||
|
}),
|
||||||
|
body: z.object({
|
||||||
|
status: z.enum([CaStatus.ACTIVE, CaStatus.DISABLED]).optional().describe(CERTIFICATE_AUTHORITIES.UPDATE.status)
|
||||||
|
}),
|
||||||
|
response: {
|
||||||
|
200: z.object({
|
||||||
|
ca: CertificateAuthoritiesSchema
|
||||||
|
})
|
||||||
|
}
|
||||||
|
},
|
||||||
|
handler: async (req) => {
|
||||||
|
const ca = await server.services.certificateAuthority.updateCaById({
|
||||||
|
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.UPDATE_CA,
|
||||||
|
metadata: {
|
||||||
|
caId: ca.id,
|
||||||
|
dn: ca.dn,
|
||||||
|
status: ca.status as CaStatus
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
ca
|
||||||
|
};
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
server.route({
|
||||||
|
method: "DELETE",
|
||||||
|
url: "/:caId",
|
||||||
|
config: {
|
||||||
|
rateLimit: writeLimit
|
||||||
|
},
|
||||||
|
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
||||||
|
schema: {
|
||||||
|
description: "Delete CA",
|
||||||
|
params: z.object({
|
||||||
|
caId: z.string().trim().describe(CERTIFICATE_AUTHORITIES.DELETE.caId)
|
||||||
|
}),
|
||||||
|
response: {
|
||||||
|
200: z.object({
|
||||||
|
ca: CertificateAuthoritiesSchema
|
||||||
|
})
|
||||||
|
}
|
||||||
|
},
|
||||||
|
handler: async (req) => {
|
||||||
|
const ca = await server.services.certificateAuthority.deleteCaById({
|
||||||
|
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.DELETE_CA,
|
||||||
|
metadata: {
|
||||||
|
caId: ca.id,
|
||||||
|
dn: ca.dn
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
ca
|
||||||
|
};
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
server.route({
|
||||||
|
method: "GET",
|
||||||
|
url: "/:caId/csr",
|
||||||
|
config: {
|
||||||
|
rateLimit: readLimit
|
||||||
|
},
|
||||||
|
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
||||||
|
schema: {
|
||||||
|
description: "Get CA CSR",
|
||||||
|
params: z.object({
|
||||||
|
caId: z.string().trim().describe(CERTIFICATE_AUTHORITIES.GET_CSR.caId)
|
||||||
|
}),
|
||||||
|
response: {
|
||||||
|
200: z.object({
|
||||||
|
csr: z.string().describe(CERTIFICATE_AUTHORITIES.GET_CSR.csr)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
},
|
||||||
|
handler: async (req) => {
|
||||||
|
const { ca, csr } = await server.services.certificateAuthority.getCaCsr({
|
||||||
|
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_CSR,
|
||||||
|
metadata: {
|
||||||
|
caId: ca.id,
|
||||||
|
dn: ca.dn
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
csr
|
||||||
|
};
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
server.route({
|
||||||
|
method: "GET",
|
||||||
|
url: "/:caId/certificate",
|
||||||
|
config: {
|
||||||
|
rateLimit: readLimit
|
||||||
|
},
|
||||||
|
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
||||||
|
schema: {
|
||||||
|
description: "Get cert and cert chain of a CA",
|
||||||
|
params: z.object({
|
||||||
|
caId: z.string().trim().describe(CERTIFICATE_AUTHORITIES.GET_CERT.caId)
|
||||||
|
}),
|
||||||
|
response: {
|
||||||
|
200: z.object({
|
||||||
|
certificate: z.string().describe(CERTIFICATE_AUTHORITIES.GET_CERT.certificate),
|
||||||
|
certificateChain: z.string().describe(CERTIFICATE_AUTHORITIES.GET_CERT.certificateChain),
|
||||||
|
serialNumber: z.string().describe(CERTIFICATE_AUTHORITIES.GET_CERT.serialNumber)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
},
|
||||||
|
handler: async (req) => {
|
||||||
|
const { certificate, certificateChain, serialNumber, ca } = await server.services.certificateAuthority.getCaCert({
|
||||||
|
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_CERT,
|
||||||
|
metadata: {
|
||||||
|
caId: ca.id,
|
||||||
|
dn: ca.dn
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
certificate,
|
||||||
|
certificateChain,
|
||||||
|
serialNumber
|
||||||
|
};
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
server.route({
|
||||||
|
method: "POST",
|
||||||
|
url: "/:caId/sign-intermediate",
|
||||||
|
config: {
|
||||||
|
rateLimit: writeLimit
|
||||||
|
},
|
||||||
|
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
||||||
|
schema: {
|
||||||
|
description: "Create intermediate CA certificate from parent CA",
|
||||||
|
params: z.object({
|
||||||
|
caId: z.string().trim().describe(CERTIFICATE_AUTHORITIES.SIGN_INTERMEDIATE.caId)
|
||||||
|
}),
|
||||||
|
body: z.object({
|
||||||
|
csr: z.string().trim().describe(CERTIFICATE_AUTHORITIES.SIGN_INTERMEDIATE.csr),
|
||||||
|
notBefore: validateCaDateField.optional().describe(CERTIFICATE_AUTHORITIES.SIGN_INTERMEDIATE.notBefore),
|
||||||
|
notAfter: validateCaDateField.describe(CERTIFICATE_AUTHORITIES.SIGN_INTERMEDIATE.notAfter),
|
||||||
|
maxPathLength: z.number().min(-1).default(-1).describe(CERTIFICATE_AUTHORITIES.SIGN_INTERMEDIATE.maxPathLength)
|
||||||
|
}),
|
||||||
|
response: {
|
||||||
|
200: z.object({
|
||||||
|
certificate: z.string().trim().describe(CERTIFICATE_AUTHORITIES.SIGN_INTERMEDIATE.certificate),
|
||||||
|
certificateChain: z.string().trim().describe(CERTIFICATE_AUTHORITIES.SIGN_INTERMEDIATE.certificateChain),
|
||||||
|
issuingCaCertificate: z
|
||||||
|
.string()
|
||||||
|
.trim()
|
||||||
|
.describe(CERTIFICATE_AUTHORITIES.SIGN_INTERMEDIATE.issuingCaCertificate),
|
||||||
|
serialNumber: z.string().trim().describe(CERTIFICATE_AUTHORITIES.SIGN_INTERMEDIATE.serialNumber)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
},
|
||||||
|
handler: async (req) => {
|
||||||
|
const { certificate, certificateChain, issuingCaCertificate, serialNumber, ca } =
|
||||||
|
await server.services.certificateAuthority.signIntermediate({
|
||||||
|
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.SIGN_INTERMEDIATE,
|
||||||
|
metadata: {
|
||||||
|
caId: ca.id,
|
||||||
|
dn: ca.dn,
|
||||||
|
serialNumber
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
certificate,
|
||||||
|
certificateChain,
|
||||||
|
issuingCaCertificate,
|
||||||
|
serialNumber
|
||||||
|
};
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
server.route({
|
||||||
|
method: "POST",
|
||||||
|
url: "/:caId/import-certificate",
|
||||||
|
config: {
|
||||||
|
rateLimit: writeLimit
|
||||||
|
},
|
||||||
|
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
||||||
|
schema: {
|
||||||
|
description: "Import certificate and chain to CA",
|
||||||
|
params: z.object({
|
||||||
|
caId: z.string().trim().describe(CERTIFICATE_AUTHORITIES.IMPORT_CERT.caId)
|
||||||
|
}),
|
||||||
|
body: z.object({
|
||||||
|
certificate: z.string().trim().describe(CERTIFICATE_AUTHORITIES.IMPORT_CERT.certificate),
|
||||||
|
certificateChain: z.string().trim().describe(CERTIFICATE_AUTHORITIES.IMPORT_CERT.certificateChain)
|
||||||
|
}),
|
||||||
|
response: {
|
||||||
|
200: z.object({
|
||||||
|
message: z.string().trim(),
|
||||||
|
caId: z.string().trim()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
},
|
||||||
|
handler: async (req) => {
|
||||||
|
const { ca } = await server.services.certificateAuthority.importCertToCa({
|
||||||
|
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.IMPORT_CA_CERT,
|
||||||
|
metadata: {
|
||||||
|
caId: ca.id,
|
||||||
|
dn: ca.dn
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
message: "Successfully imported certificate to CA",
|
||||||
|
caId: req.params.caId
|
||||||
|
};
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
server.route({
|
||||||
|
method: "POST",
|
||||||
|
url: "/:caId/issue-certificate",
|
||||||
|
config: {
|
||||||
|
rateLimit: writeLimit
|
||||||
|
},
|
||||||
|
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
||||||
|
schema: {
|
||||||
|
description: "Issue certificate from CA",
|
||||||
|
params: z.object({
|
||||||
|
caId: z.string().trim().describe(CERTIFICATE_AUTHORITIES.ISSUE_CERT.caId)
|
||||||
|
}),
|
||||||
|
body: z
|
||||||
|
.object({
|
||||||
|
friendlyName: z.string().optional().describe(CERTIFICATE_AUTHORITIES.ISSUE_CERT.friendlyName),
|
||||||
|
commonName: z.string().trim().min(1).describe(CERTIFICATE_AUTHORITIES.ISSUE_CERT.commonName),
|
||||||
|
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"]
|
||||||
|
}
|
||||||
|
),
|
||||||
|
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({
|
||||||
|
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.ISSUE_CERT,
|
||||||
|
metadata: {
|
||||||
|
caId: ca.id,
|
||||||
|
dn: ca.dn,
|
||||||
|
serialNumber
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
certificate,
|
||||||
|
certificateChain,
|
||||||
|
issuingCaCertificate,
|
||||||
|
privateKey,
|
||||||
|
serialNumber
|
||||||
|
};
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
207
backend/src/server/routes/v1/certificate-router.ts
Normal file
207
backend/src/server/routes/v1/certificate-router.ts
Normal file
@@ -0,0 +1,207 @@
|
|||||||
|
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 { 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";
|
||||||
|
|
||||||
|
export const registerCertRouter = async (server: FastifyZodProvider) => {
|
||||||
|
server.route({
|
||||||
|
method: "GET",
|
||||||
|
url: "/:serialNumber",
|
||||||
|
config: {
|
||||||
|
rateLimit: readLimit
|
||||||
|
},
|
||||||
|
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
||||||
|
schema: {
|
||||||
|
description: "Get certificate",
|
||||||
|
params: z.object({
|
||||||
|
serialNumber: z.string().trim().describe(CERTIFICATES.GET.serialNumber)
|
||||||
|
}),
|
||||||
|
response: {
|
||||||
|
200: z.object({
|
||||||
|
certificate: CertificatesSchema
|
||||||
|
})
|
||||||
|
}
|
||||||
|
},
|
||||||
|
handler: async (req) => {
|
||||||
|
const { cert, ca } = await server.services.certificate.getCert({
|
||||||
|
serialNumber: req.params.serialNumber,
|
||||||
|
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_CERT,
|
||||||
|
metadata: {
|
||||||
|
certId: cert.id,
|
||||||
|
cn: cert.commonName,
|
||||||
|
serialNumber: cert.serialNumber
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
certificate: cert
|
||||||
|
};
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
server.route({
|
||||||
|
method: "POST",
|
||||||
|
url: "/:serialNumber/revoke",
|
||||||
|
config: {
|
||||||
|
rateLimit: writeLimit
|
||||||
|
},
|
||||||
|
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
||||||
|
schema: {
|
||||||
|
description: "Revoke",
|
||||||
|
params: z.object({
|
||||||
|
serialNumber: z.string().trim().describe(CERTIFICATES.REVOKE.serialNumber)
|
||||||
|
}),
|
||||||
|
body: z.object({
|
||||||
|
revocationReason: z.nativeEnum(CrlReason).describe(CERTIFICATES.REVOKE.revocationReason)
|
||||||
|
}),
|
||||||
|
response: {
|
||||||
|
200: z.object({
|
||||||
|
message: z.string().trim(),
|
||||||
|
serialNumber: z.string().trim().describe(CERTIFICATES.REVOKE.serialNumberRes),
|
||||||
|
revokedAt: z.date().describe(CERTIFICATES.REVOKE.revokedAt)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
},
|
||||||
|
handler: async (req) => {
|
||||||
|
const { revokedAt, cert, ca } = await server.services.certificate.revokeCert({
|
||||||
|
serialNumber: req.params.serialNumber,
|
||||||
|
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.REVOKE_CERT,
|
||||||
|
metadata: {
|
||||||
|
certId: cert.id,
|
||||||
|
cn: cert.commonName,
|
||||||
|
serialNumber: cert.serialNumber
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
message: "Successfully revoked certificate",
|
||||||
|
serialNumber: req.params.serialNumber,
|
||||||
|
revokedAt
|
||||||
|
};
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
server.route({
|
||||||
|
method: "DELETE",
|
||||||
|
url: "/:serialNumber",
|
||||||
|
config: {
|
||||||
|
rateLimit: writeLimit
|
||||||
|
},
|
||||||
|
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
||||||
|
schema: {
|
||||||
|
description: "Delete certificate",
|
||||||
|
params: z.object({
|
||||||
|
serialNumber: z.string().trim().describe(CERTIFICATES.DELETE.serialNumber)
|
||||||
|
}),
|
||||||
|
response: {
|
||||||
|
200: z.object({
|
||||||
|
certificate: CertificatesSchema
|
||||||
|
})
|
||||||
|
}
|
||||||
|
},
|
||||||
|
handler: async (req) => {
|
||||||
|
const { deletedCert, ca } = await server.services.certificate.deleteCert({
|
||||||
|
serialNumber: req.params.serialNumber,
|
||||||
|
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.DELETE_CERT,
|
||||||
|
metadata: {
|
||||||
|
certId: deletedCert.id,
|
||||||
|
cn: deletedCert.commonName,
|
||||||
|
serialNumber: deletedCert.serialNumber
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
certificate: deletedCert
|
||||||
|
};
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
server.route({
|
||||||
|
method: "GET",
|
||||||
|
url: "/:serialNumber/certificate",
|
||||||
|
config: {
|
||||||
|
rateLimit: readLimit
|
||||||
|
},
|
||||||
|
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
||||||
|
schema: {
|
||||||
|
description: "Get certificate body of certificate",
|
||||||
|
params: z.object({
|
||||||
|
serialNumber: z.string().trim().describe(CERTIFICATES.GET_CERT.serialNumber)
|
||||||
|
}),
|
||||||
|
response: {
|
||||||
|
200: z.object({
|
||||||
|
certificate: z.string().trim().describe(CERTIFICATES.GET_CERT.certificate),
|
||||||
|
certificateChain: z.string().trim().describe(CERTIFICATES.GET_CERT.certificateChain),
|
||||||
|
serialNumber: z.string().trim().describe(CERTIFICATES.GET_CERT.serialNumberRes)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
},
|
||||||
|
handler: async (req) => {
|
||||||
|
const { certificate, certificateChain, serialNumber, cert, ca } = await server.services.certificate.getCertBody({
|
||||||
|
serialNumber: req.params.serialNumber,
|
||||||
|
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.DELETE_CERT,
|
||||||
|
metadata: {
|
||||||
|
certId: cert.id,
|
||||||
|
cn: cert.commonName,
|
||||||
|
serialNumber: cert.serialNumber
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
certificate,
|
||||||
|
certificateChain,
|
||||||
|
serialNumber
|
||||||
|
};
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
@@ -1,6 +1,8 @@
|
|||||||
import { registerAdminRouter } from "./admin-router";
|
import { registerAdminRouter } from "./admin-router";
|
||||||
import { registerAuthRoutes } from "./auth-router";
|
import { registerAuthRoutes } from "./auth-router";
|
||||||
import { registerProjectBotRouter } from "./bot-router";
|
import { registerProjectBotRouter } from "./bot-router";
|
||||||
|
import { registerCaRouter } from "./certificate-authority-router";
|
||||||
|
import { registerCertRouter } from "./certificate-router";
|
||||||
import { registerIdentityAccessTokenRouter } from "./identity-access-token-router";
|
import { registerIdentityAccessTokenRouter } from "./identity-access-token-router";
|
||||||
import { registerIdentityAwsAuthRouter } from "./identity-aws-iam-auth-router";
|
import { registerIdentityAwsAuthRouter } from "./identity-aws-iam-auth-router";
|
||||||
import { registerIdentityAzureAuthRouter } from "./identity-azure-auth-router";
|
import { registerIdentityAzureAuthRouter } from "./identity-azure-auth-router";
|
||||||
@@ -61,6 +63,14 @@ export const registerV1Routes = async (server: FastifyZodProvider) => {
|
|||||||
{ prefix: "/workspace" }
|
{ prefix: "/workspace" }
|
||||||
);
|
);
|
||||||
|
|
||||||
|
await server.register(
|
||||||
|
async (pkiRouter) => {
|
||||||
|
await pkiRouter.register(registerCaRouter, { prefix: "/ca" });
|
||||||
|
await pkiRouter.register(registerCertRouter, { prefix: "/certificates" });
|
||||||
|
},
|
||||||
|
{ prefix: "/pki" }
|
||||||
|
);
|
||||||
|
|
||||||
await server.register(registerProjectBotRouter, { prefix: "/bot" });
|
await server.register(registerProjectBotRouter, { prefix: "/bot" });
|
||||||
await server.register(registerIntegrationRouter, { prefix: "/integration" });
|
await server.register(registerIntegrationRouter, { prefix: "/integration" });
|
||||||
await server.register(registerIntegrationAuthRouter, { prefix: "/integration-auth" });
|
await server.register(registerIntegrationAuthRouter, { prefix: "/integration-auth" });
|
||||||
|
@@ -309,4 +309,32 @@ export const registerProjectMembershipRouter = async (server: FastifyZodProvider
|
|||||||
return { membership };
|
return { membership };
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
server.route({
|
||||||
|
method: "DELETE",
|
||||||
|
url: "/:workspaceId/leave",
|
||||||
|
config: {
|
||||||
|
rateLimit: writeLimit
|
||||||
|
},
|
||||||
|
schema: {
|
||||||
|
params: z.object({
|
||||||
|
workspaceId: z.string().trim()
|
||||||
|
}),
|
||||||
|
response: {
|
||||||
|
200: z.object({
|
||||||
|
membership: ProjectMembershipsSchema
|
||||||
|
})
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
onRequest: verifyAuth([AuthMode.JWT]),
|
||||||
|
handler: async (req) => {
|
||||||
|
const membership = await server.services.projectMembership.leaveProject({
|
||||||
|
actorId: req.permission.id,
|
||||||
|
actor: req.permission.type,
|
||||||
|
projectId: req.params.workspaceId
|
||||||
|
});
|
||||||
|
return { membership };
|
||||||
|
}
|
||||||
|
});
|
||||||
};
|
};
|
||||||
|
@@ -334,6 +334,44 @@ export const registerProjectRouter = async (server: FastifyZodProvider) => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
server.route({
|
||||||
|
method: "PUT",
|
||||||
|
url: "/:workspaceSlug/version-limit",
|
||||||
|
config: {
|
||||||
|
rateLimit: writeLimit
|
||||||
|
},
|
||||||
|
schema: {
|
||||||
|
params: z.object({
|
||||||
|
workspaceSlug: z.string().trim()
|
||||||
|
}),
|
||||||
|
body: z.object({
|
||||||
|
pitVersionLimit: z.number().min(1).max(100)
|
||||||
|
}),
|
||||||
|
response: {
|
||||||
|
200: z.object({
|
||||||
|
message: z.string(),
|
||||||
|
workspace: ProjectsSchema
|
||||||
|
})
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onRequest: verifyAuth([AuthMode.JWT]),
|
||||||
|
handler: async (req) => {
|
||||||
|
const workspace = await server.services.project.updateVersionLimit({
|
||||||
|
actorId: req.permission.id,
|
||||||
|
actor: req.permission.type,
|
||||||
|
actorAuthMethod: req.permission.authMethod,
|
||||||
|
actorOrgId: req.permission.orgId,
|
||||||
|
pitVersionLimit: req.body.pitVersionLimit,
|
||||||
|
workspaceSlug: req.params.workspaceSlug
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
message: "Successfully changed workspace version limit",
|
||||||
|
workspace
|
||||||
|
};
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
server.route({
|
server.route({
|
||||||
method: "GET",
|
method: "GET",
|
||||||
url: "/:workspaceId/integrations",
|
url: "/:workspaceId/integrations",
|
||||||
|
@@ -1,7 +1,12 @@
|
|||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
|
|
||||||
import { SecretSharingSchema } from "@app/db/schemas";
|
import { SecretSharingSchema } from "@app/db/schemas";
|
||||||
import { publicEndpointLimit, readLimit, writeLimit } from "@app/server/config/rateLimiter";
|
import {
|
||||||
|
publicEndpointLimit,
|
||||||
|
publicSecretShareCreationLimit,
|
||||||
|
readLimit,
|
||||||
|
writeLimit
|
||||||
|
} from "@app/server/config/rateLimiter";
|
||||||
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
|
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
|
||||||
import { AuthMode } from "@app/services/auth/auth-type";
|
import { AuthMode } from "@app/services/auth/auth-type";
|
||||||
|
|
||||||
@@ -72,7 +77,7 @@ export const registerSecretSharingRouter = async (server: FastifyZodProvider) =>
|
|||||||
|
|
||||||
server.route({
|
server.route({
|
||||||
method: "POST",
|
method: "POST",
|
||||||
url: "/",
|
url: "/public",
|
||||||
config: {
|
config: {
|
||||||
rateLimit: writeLimit
|
rateLimit: writeLimit
|
||||||
},
|
},
|
||||||
@@ -82,9 +87,42 @@ export const registerSecretSharingRouter = async (server: FastifyZodProvider) =>
|
|||||||
iv: z.string(),
|
iv: z.string(),
|
||||||
tag: z.string(),
|
tag: z.string(),
|
||||||
hashedHex: z.string(),
|
hashedHex: z.string(),
|
||||||
expiresAt: z
|
expiresAt: z.string(),
|
||||||
.string()
|
expiresAfterViews: z.number()
|
||||||
.refine((date) => date === undefined || new Date(date) > new Date(), "Expires at should be a future date"),
|
}),
|
||||||
|
response: {
|
||||||
|
200: z.object({
|
||||||
|
id: z.string().uuid()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
},
|
||||||
|
handler: async (req) => {
|
||||||
|
const { encryptedValue, iv, tag, hashedHex, expiresAt, expiresAfterViews } = req.body;
|
||||||
|
const sharedSecret = await req.server.services.secretSharing.createPublicSharedSecret({
|
||||||
|
encryptedValue,
|
||||||
|
iv,
|
||||||
|
tag,
|
||||||
|
hashedHex,
|
||||||
|
expiresAt: new Date(expiresAt),
|
||||||
|
expiresAfterViews
|
||||||
|
});
|
||||||
|
return { id: sharedSecret.id };
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
server.route({
|
||||||
|
method: "POST",
|
||||||
|
url: "/",
|
||||||
|
config: {
|
||||||
|
rateLimit: publicSecretShareCreationLimit
|
||||||
|
},
|
||||||
|
schema: {
|
||||||
|
body: z.object({
|
||||||
|
encryptedValue: z.string(),
|
||||||
|
iv: z.string(),
|
||||||
|
tag: z.string(),
|
||||||
|
hashedHex: z.string(),
|
||||||
|
expiresAt: z.string(),
|
||||||
expiresAfterViews: z.number()
|
expiresAfterViews: z.number()
|
||||||
}),
|
}),
|
||||||
response: {
|
response: {
|
||||||
|
@@ -23,7 +23,7 @@ export const registerSecretTagRouter = async (server: FastifyZodProvider) => {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
onRequest: verifyAuth([AuthMode.JWT]),
|
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
||||||
handler: async (req) => {
|
handler: async (req) => {
|
||||||
const workspaceTags = await server.services.secretTag.getProjectTags({
|
const workspaceTags = await server.services.secretTag.getProjectTags({
|
||||||
actor: req.permission.type,
|
actor: req.permission.type,
|
||||||
@@ -57,7 +57,7 @@ export const registerSecretTagRouter = async (server: FastifyZodProvider) => {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
onRequest: verifyAuth([AuthMode.JWT]),
|
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
||||||
handler: async (req) => {
|
handler: async (req) => {
|
||||||
const workspaceTag = await server.services.secretTag.createTag({
|
const workspaceTag = await server.services.secretTag.createTag({
|
||||||
actor: req.permission.type,
|
actor: req.permission.type,
|
||||||
@@ -88,7 +88,7 @@ export const registerSecretTagRouter = async (server: FastifyZodProvider) => {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
onRequest: verifyAuth([AuthMode.JWT]),
|
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
||||||
handler: async (req) => {
|
handler: async (req) => {
|
||||||
const workspaceTag = await server.services.secretTag.deleteTag({
|
const workspaceTag = await server.services.secretTag.deleteTag({
|
||||||
actor: req.permission.type,
|
actor: req.permission.type,
|
||||||
|
@@ -1,13 +1,14 @@
|
|||||||
import slugify from "@sindresorhus/slugify";
|
import slugify from "@sindresorhus/slugify";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
|
|
||||||
import { ProjectKeysSchema, ProjectsSchema } from "@app/db/schemas";
|
import { CertificateAuthoritiesSchema, CertificatesSchema, ProjectKeysSchema, ProjectsSchema } from "@app/db/schemas";
|
||||||
import { EventType } from "@app/ee/services/audit-log/audit-log-types";
|
import { EventType } from "@app/ee/services/audit-log/audit-log-types";
|
||||||
import { PROJECTS } from "@app/lib/api-docs";
|
import { PROJECTS } from "@app/lib/api-docs";
|
||||||
import { creationLimit, readLimit, writeLimit } from "@app/server/config/rateLimiter";
|
import { creationLimit, readLimit, writeLimit } from "@app/server/config/rateLimiter";
|
||||||
import { getTelemetryDistinctId } from "@app/server/lib/telemetry";
|
import { getTelemetryDistinctId } from "@app/server/lib/telemetry";
|
||||||
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
|
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
|
||||||
import { AuthMode } from "@app/services/auth/auth-type";
|
import { AuthMode } from "@app/services/auth/auth-type";
|
||||||
|
import { CaStatus } from "@app/services/certificate-authority/certificate-authority-types";
|
||||||
import { ProjectFilterType } from "@app/services/project/project-types";
|
import { ProjectFilterType } from "@app/services/project/project-types";
|
||||||
import { PostHogEventTypes } from "@app/services/telemetry/telemetry-types";
|
import { PostHogEventTypes } from "@app/services/telemetry/telemetry-types";
|
||||||
|
|
||||||
@@ -307,4 +308,80 @@ export const registerProjectRouter = async (server: FastifyZodProvider) => {
|
|||||||
return project;
|
return project;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
server.route({
|
||||||
|
method: "GET",
|
||||||
|
url: "/:slug/cas",
|
||||||
|
config: {
|
||||||
|
rateLimit: readLimit
|
||||||
|
},
|
||||||
|
schema: {
|
||||||
|
params: z.object({
|
||||||
|
slug: slugSchema.describe("The slug of the project to list CAs.")
|
||||||
|
}),
|
||||||
|
querystring: z.object({
|
||||||
|
status: z.enum([CaStatus.ACTIVE, CaStatus.PENDING_CERTIFICATE]).optional()
|
||||||
|
}),
|
||||||
|
response: {
|
||||||
|
200: z.object({
|
||||||
|
cas: z.array(CertificateAuthoritiesSchema)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
||||||
|
handler: async (req) => {
|
||||||
|
const cas = await server.services.project.listProjectCas({
|
||||||
|
filter: {
|
||||||
|
slug: req.params.slug,
|
||||||
|
orgId: req.permission.orgId,
|
||||||
|
type: ProjectFilterType.SLUG
|
||||||
|
},
|
||||||
|
status: req.query.status,
|
||||||
|
actorId: req.permission.id,
|
||||||
|
actorOrgId: req.permission.orgId,
|
||||||
|
actorAuthMethod: req.permission.authMethod,
|
||||||
|
actor: req.permission.type
|
||||||
|
});
|
||||||
|
return { cas };
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
server.route({
|
||||||
|
method: "GET",
|
||||||
|
url: "/:slug/certificates",
|
||||||
|
config: {
|
||||||
|
rateLimit: readLimit
|
||||||
|
},
|
||||||
|
schema: {
|
||||||
|
params: z.object({
|
||||||
|
slug: slugSchema.describe("The slug of the project to list certificates.")
|
||||||
|
}),
|
||||||
|
querystring: z.object({
|
||||||
|
offset: z.coerce.number().min(0).max(100).default(0),
|
||||||
|
limit: z.coerce.number().min(1).max(100).default(25)
|
||||||
|
}),
|
||||||
|
response: {
|
||||||
|
200: z.object({
|
||||||
|
certificates: z.array(CertificatesSchema),
|
||||||
|
totalCount: z.number()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
||||||
|
handler: async (req) => {
|
||||||
|
const { certificates, totalCount } = await server.services.project.listProjectCertificates({
|
||||||
|
filter: {
|
||||||
|
slug: req.params.slug,
|
||||||
|
orgId: req.permission.orgId,
|
||||||
|
type: ProjectFilterType.SLUG
|
||||||
|
},
|
||||||
|
actorId: req.permission.id,
|
||||||
|
actorOrgId: req.permission.orgId,
|
||||||
|
actorAuthMethod: req.permission.authMethod,
|
||||||
|
actor: req.permission.type,
|
||||||
|
...req.query
|
||||||
|
});
|
||||||
|
return { certificates, totalCount };
|
||||||
|
}
|
||||||
|
});
|
||||||
};
|
};
|
||||||
|
@@ -306,7 +306,16 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => {
|
|||||||
}),
|
}),
|
||||||
response: {
|
response: {
|
||||||
200: z.object({
|
200: z.object({
|
||||||
secret: secretRawSchema
|
secret: secretRawSchema.extend({
|
||||||
|
tags: SecretTagsSchema.pick({
|
||||||
|
id: true,
|
||||||
|
slug: true,
|
||||||
|
name: true,
|
||||||
|
color: true
|
||||||
|
})
|
||||||
|
.array()
|
||||||
|
.optional()
|
||||||
|
})
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -404,6 +413,7 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => {
|
|||||||
.transform((val) => (val.at(-1) === "\n" ? `${val.trim()}\n` : val.trim()))
|
.transform((val) => (val.at(-1) === "\n" ? `${val.trim()}\n` : val.trim()))
|
||||||
.describe(RAW_SECRETS.CREATE.secretValue),
|
.describe(RAW_SECRETS.CREATE.secretValue),
|
||||||
secretComment: z.string().trim().optional().default("").describe(RAW_SECRETS.CREATE.secretComment),
|
secretComment: z.string().trim().optional().default("").describe(RAW_SECRETS.CREATE.secretComment),
|
||||||
|
tagIds: z.string().array().optional().describe(RAW_SECRETS.CREATE.tagIds),
|
||||||
skipMultilineEncoding: z.boolean().optional().describe(RAW_SECRETS.CREATE.skipMultilineEncoding),
|
skipMultilineEncoding: z.boolean().optional().describe(RAW_SECRETS.CREATE.skipMultilineEncoding),
|
||||||
type: z.nativeEnum(SecretType).default(SecretType.Shared).describe(RAW_SECRETS.CREATE.type)
|
type: z.nativeEnum(SecretType).default(SecretType.Shared).describe(RAW_SECRETS.CREATE.type)
|
||||||
}),
|
}),
|
||||||
@@ -427,7 +437,8 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => {
|
|||||||
type: req.body.type,
|
type: req.body.type,
|
||||||
secretValue: req.body.secretValue,
|
secretValue: req.body.secretValue,
|
||||||
skipMultilineEncoding: req.body.skipMultilineEncoding,
|
skipMultilineEncoding: req.body.skipMultilineEncoding,
|
||||||
secretComment: req.body.secretComment
|
secretComment: req.body.secretComment,
|
||||||
|
tagIds: req.body.tagIds
|
||||||
});
|
});
|
||||||
|
|
||||||
await server.services.auditLog.createAuditLog({
|
await server.services.auditLog.createAuditLog({
|
||||||
@@ -492,7 +503,8 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => {
|
|||||||
.transform(removeTrailingSlash)
|
.transform(removeTrailingSlash)
|
||||||
.describe(RAW_SECRETS.UPDATE.secretPath),
|
.describe(RAW_SECRETS.UPDATE.secretPath),
|
||||||
skipMultilineEncoding: z.boolean().optional().describe(RAW_SECRETS.UPDATE.skipMultilineEncoding),
|
skipMultilineEncoding: z.boolean().optional().describe(RAW_SECRETS.UPDATE.skipMultilineEncoding),
|
||||||
type: z.nativeEnum(SecretType).default(SecretType.Shared).describe(RAW_SECRETS.UPDATE.type)
|
type: z.nativeEnum(SecretType).default(SecretType.Shared).describe(RAW_SECRETS.UPDATE.type),
|
||||||
|
tagIds: z.string().array().optional().describe(RAW_SECRETS.UPDATE.tagIds)
|
||||||
}),
|
}),
|
||||||
response: {
|
response: {
|
||||||
200: z.object({
|
200: z.object({
|
||||||
@@ -513,7 +525,8 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => {
|
|||||||
secretName: req.params.secretName,
|
secretName: req.params.secretName,
|
||||||
type: req.body.type,
|
type: req.body.type,
|
||||||
secretValue: req.body.secretValue,
|
secretValue: req.body.secretValue,
|
||||||
skipMultilineEncoding: req.body.skipMultilineEncoding
|
skipMultilineEncoding: req.body.skipMultilineEncoding,
|
||||||
|
tagIds: req.body.tagIds
|
||||||
});
|
});
|
||||||
|
|
||||||
await server.services.auditLog.createAuditLog({
|
await server.services.auditLog.createAuditLog({
|
||||||
|
@@ -231,7 +231,7 @@ export const authSignupServiceFactory = ({
|
|||||||
|
|
||||||
const accessToken = jwt.sign(
|
const accessToken = jwt.sign(
|
||||||
{
|
{
|
||||||
authMethod: AuthMethod.EMAIL,
|
authMethod: authMethod || AuthMethod.EMAIL,
|
||||||
authTokenType: AuthTokenType.ACCESS_TOKEN,
|
authTokenType: AuthTokenType.ACCESS_TOKEN,
|
||||||
userId: updateduser.info.id,
|
userId: updateduser.info.id,
|
||||||
tokenVersionId: tokenSession.id,
|
tokenVersionId: tokenSession.id,
|
||||||
@@ -244,7 +244,7 @@ export const authSignupServiceFactory = ({
|
|||||||
|
|
||||||
const refreshToken = jwt.sign(
|
const refreshToken = jwt.sign(
|
||||||
{
|
{
|
||||||
authMethod: AuthMethod.EMAIL,
|
authMethod: authMethod || AuthMethod.EMAIL,
|
||||||
authTokenType: AuthTokenType.REFRESH_TOKEN,
|
authTokenType: AuthTokenType.REFRESH_TOKEN,
|
||||||
userId: updateduser.info.id,
|
userId: updateduser.info.id,
|
||||||
tokenVersionId: tokenSession.id,
|
tokenVersionId: tokenSession.id,
|
||||||
|
@@ -0,0 +1,10 @@
|
|||||||
|
import { TDbClient } from "@app/db";
|
||||||
|
import { TableName } from "@app/db/schemas";
|
||||||
|
import { ormify } from "@app/lib/knex";
|
||||||
|
|
||||||
|
export type TCertificateAuthorityCertDALFactory = ReturnType<typeof certificateAuthorityCertDALFactory>;
|
||||||
|
|
||||||
|
export const certificateAuthorityCertDALFactory = (db: TDbClient) => {
|
||||||
|
const caCertOrm = ormify(db, TableName.CertificateAuthorityCert);
|
||||||
|
return caCertOrm;
|
||||||
|
};
|
@@ -0,0 +1,48 @@
|
|||||||
|
import { TDbClient } from "@app/db";
|
||||||
|
import { TableName } from "@app/db/schemas";
|
||||||
|
import { DatabaseError } from "@app/lib/errors";
|
||||||
|
import { ormify } from "@app/lib/knex";
|
||||||
|
|
||||||
|
export type TCertificateAuthorityDALFactory = ReturnType<typeof certificateAuthorityDALFactory>;
|
||||||
|
|
||||||
|
export const certificateAuthorityDALFactory = (db: TDbClient) => {
|
||||||
|
const caOrm = ormify(db, TableName.CertificateAuthority);
|
||||||
|
|
||||||
|
// note: not used
|
||||||
|
const buildCertificateChain = async (caId: string) => {
|
||||||
|
try {
|
||||||
|
const result: {
|
||||||
|
caId: string;
|
||||||
|
parentCaId?: string;
|
||||||
|
encryptedCertificate: Buffer;
|
||||||
|
}[] = await db
|
||||||
|
.withRecursive("cte", (cte) => {
|
||||||
|
void cte
|
||||||
|
.select("ca.id as caId", "ca.parentCaId", "cert.encryptedCertificate")
|
||||||
|
.from({ ca: TableName.CertificateAuthority })
|
||||||
|
.leftJoin({ cert: TableName.CertificateAuthorityCert }, "ca.id", "cert.caId")
|
||||||
|
.where("ca.id", caId)
|
||||||
|
.unionAll((builder) => {
|
||||||
|
void builder
|
||||||
|
.select("ca.id as caId", "ca.parentCaId", "cert.encryptedCertificate")
|
||||||
|
.from({ ca: TableName.CertificateAuthority })
|
||||||
|
.leftJoin({ cert: TableName.CertificateAuthorityCert }, "ca.id", "cert.caId")
|
||||||
|
.innerJoin("cte", "cte.parentCaId", "ca.id");
|
||||||
|
});
|
||||||
|
})
|
||||||
|
.select("*")
|
||||||
|
.from("cte");
|
||||||
|
|
||||||
|
// Extract certificates and reverse the order to have the root CA at the end
|
||||||
|
const certChain: Buffer[] = result.map((row) => row.encryptedCertificate);
|
||||||
|
return certChain;
|
||||||
|
} catch (error) {
|
||||||
|
throw new DatabaseError({ error, name: "BuildCertificateChain" });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
...caOrm,
|
||||||
|
buildCertificateChain
|
||||||
|
};
|
||||||
|
};
|
@@ -0,0 +1,216 @@
|
|||||||
|
import * as x509 from "@peculiar/x509";
|
||||||
|
import crypto from "crypto";
|
||||||
|
|
||||||
|
import { BadRequestError } 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";
|
||||||
|
|
||||||
|
export const createDistinguishedName = (parts: TDNParts) => {
|
||||||
|
const dnParts = [];
|
||||||
|
if (parts.country) dnParts.push(`C=${parts.country}`);
|
||||||
|
if (parts.organization) dnParts.push(`O=${parts.organization}`);
|
||||||
|
if (parts.ou) dnParts.push(`OU=${parts.ou}`);
|
||||||
|
if (parts.province) dnParts.push(`ST=${parts.province}`);
|
||||||
|
if (parts.commonName) dnParts.push(`CN=${parts.commonName}`);
|
||||||
|
if (parts.locality) dnParts.push(`L=${parts.locality}`);
|
||||||
|
return dnParts.join(", ");
|
||||||
|
};
|
||||||
|
|
||||||
|
export const keyAlgorithmToAlgCfg = (keyAlgorithm: CertKeyAlgorithm) => {
|
||||||
|
switch (keyAlgorithm) {
|
||||||
|
case CertKeyAlgorithm.RSA_4096:
|
||||||
|
return {
|
||||||
|
name: "RSASSA-PKCS1-v1_5",
|
||||||
|
hash: "SHA-256",
|
||||||
|
publicExponent: new Uint8Array([1, 0, 1]),
|
||||||
|
modulusLength: 4096
|
||||||
|
};
|
||||||
|
case CertKeyAlgorithm.ECDSA_P256:
|
||||||
|
return {
|
||||||
|
name: "ECDSA",
|
||||||
|
namedCurve: "P-256",
|
||||||
|
hash: "SHA-256"
|
||||||
|
};
|
||||||
|
case CertKeyAlgorithm.ECDSA_P384:
|
||||||
|
return {
|
||||||
|
name: "ECDSA",
|
||||||
|
namedCurve: "P-384",
|
||||||
|
hash: "SHA-384"
|
||||||
|
};
|
||||||
|
default: {
|
||||||
|
// RSA_2048
|
||||||
|
return {
|
||||||
|
name: "RSASSA-PKCS1-v1_5",
|
||||||
|
hash: "SHA-256",
|
||||||
|
publicExponent: new Uint8Array([1, 0, 1]),
|
||||||
|
modulusLength: 2048
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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
|
||||||
|
*/
|
||||||
|
export const getCaCredentials = async ({
|
||||||
|
caId,
|
||||||
|
certificateAuthorityDAL,
|
||||||
|
certificateAuthoritySecretDAL,
|
||||||
|
projectDAL,
|
||||||
|
kmsService
|
||||||
|
}: TGetCaCredentialsDTO) => {
|
||||||
|
const ca = await certificateAuthorityDAL.findById(caId);
|
||||||
|
if (!ca) throw new BadRequestError({ message: "CA not found" });
|
||||||
|
|
||||||
|
const caSecret = await certificateAuthoritySecretDAL.findOne({ caId });
|
||||||
|
if (!caSecret) throw new BadRequestError({ message: "CA secret not found" });
|
||||||
|
|
||||||
|
const keyId = await getProjectKmsCertificateKeyId({
|
||||||
|
projectId: ca.projectId,
|
||||||
|
projectDAL,
|
||||||
|
kmsService
|
||||||
|
});
|
||||||
|
|
||||||
|
const decryptedPrivateKey = await kmsService.decrypt({
|
||||||
|
kmsId: keyId,
|
||||||
|
cipherTextBlob: caSecret.encryptedPrivateKey
|
||||||
|
});
|
||||||
|
|
||||||
|
const alg = keyAlgorithmToAlgCfg(ca.keyAlgorithm as CertKeyAlgorithm);
|
||||||
|
const skObj = crypto.createPrivateKey({ key: decryptedPrivateKey, format: "der", type: "pkcs8" });
|
||||||
|
const caPrivateKey = await crypto.subtle.importKey(
|
||||||
|
"pkcs8",
|
||||||
|
skObj.export({ format: "der", type: "pkcs8" }),
|
||||||
|
alg,
|
||||||
|
true,
|
||||||
|
["sign"]
|
||||||
|
);
|
||||||
|
|
||||||
|
const pkObj = crypto.createPublicKey(skObj);
|
||||||
|
const caPublicKey = await crypto.subtle.importKey("spki", pkObj.export({ format: "der", type: "spki" }), alg, true, [
|
||||||
|
"verify"
|
||||||
|
]);
|
||||||
|
|
||||||
|
return {
|
||||||
|
caPrivateKey,
|
||||||
|
caPublicKey
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return the decrypted pem-encoded certificate and certificate chain
|
||||||
|
* for CA with id [caId].
|
||||||
|
*/
|
||||||
|
export const getCaCertChain = async ({
|
||||||
|
caId,
|
||||||
|
certificateAuthorityDAL,
|
||||||
|
certificateAuthorityCertDAL,
|
||||||
|
projectDAL,
|
||||||
|
kmsService
|
||||||
|
}: TGetCaCertChainDTO) => {
|
||||||
|
const ca = await certificateAuthorityDAL.findById(caId);
|
||||||
|
if (!ca) throw new BadRequestError({ message: "CA not found" });
|
||||||
|
|
||||||
|
const caCert = await certificateAuthorityCertDAL.findOne({ caId: ca.id });
|
||||||
|
|
||||||
|
const keyId = await getProjectKmsCertificateKeyId({
|
||||||
|
projectId: ca.projectId,
|
||||||
|
projectDAL,
|
||||||
|
kmsService
|
||||||
|
});
|
||||||
|
|
||||||
|
const decryptedCaCert = await kmsService.decrypt({
|
||||||
|
kmsId: keyId,
|
||||||
|
cipherTextBlob: caCert.encryptedCertificate
|
||||||
|
});
|
||||||
|
|
||||||
|
const caCertObj = new x509.X509Certificate(decryptedCaCert);
|
||||||
|
|
||||||
|
const decryptedChain = await kmsService.decrypt({
|
||||||
|
kmsId: keyId,
|
||||||
|
cipherTextBlob: caCert.encryptedCertificateChain
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
caCert: caCertObj.toString("pem"),
|
||||||
|
caCertChain: decryptedChain.toString("utf-8"),
|
||||||
|
serialNumber: caCertObj.serialNumber
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Rebuilds the certificate revocation list (CRL)
|
||||||
|
* for CA with id [caId]
|
||||||
|
*/
|
||||||
|
export const rebuildCaCrl = async ({
|
||||||
|
caId,
|
||||||
|
certificateAuthorityDAL,
|
||||||
|
certificateAuthorityCrlDAL,
|
||||||
|
certificateAuthoritySecretDAL,
|
||||||
|
projectDAL,
|
||||||
|
certificateDAL,
|
||||||
|
kmsService
|
||||||
|
}: TRebuildCaCrlDTO) => {
|
||||||
|
const ca = await certificateAuthorityDAL.findById(caId);
|
||||||
|
if (!ca) throw new BadRequestError({ message: "CA not found" });
|
||||||
|
|
||||||
|
const caSecret = await certificateAuthoritySecretDAL.findOne({ caId: ca.id });
|
||||||
|
|
||||||
|
const alg = keyAlgorithmToAlgCfg(ca.keyAlgorithm as CertKeyAlgorithm);
|
||||||
|
|
||||||
|
const keyId = await getProjectKmsCertificateKeyId({
|
||||||
|
projectId: ca.projectId,
|
||||||
|
projectDAL,
|
||||||
|
kmsService
|
||||||
|
});
|
||||||
|
|
||||||
|
const privateKey = await kmsService.decrypt({
|
||||||
|
kmsId: keyId,
|
||||||
|
cipherTextBlob: caSecret.encryptedPrivateKey
|
||||||
|
});
|
||||||
|
|
||||||
|
const skObj = crypto.createPrivateKey({ key: privateKey, format: "der", type: "pkcs8" });
|
||||||
|
const sk = await crypto.subtle.importKey("pkcs8", skObj.export({ format: "der", type: "pkcs8" }), alg, true, [
|
||||||
|
"sign"
|
||||||
|
]);
|
||||||
|
|
||||||
|
const revokedCerts = await certificateDAL.find({
|
||||||
|
caId: ca.id,
|
||||||
|
status: CertStatus.REVOKED
|
||||||
|
});
|
||||||
|
|
||||||
|
const crl = await x509.X509CrlGenerator.create({
|
||||||
|
issuer: ca.dn,
|
||||||
|
thisUpdate: new Date(),
|
||||||
|
nextUpdate: new Date("2025/12/12"),
|
||||||
|
entries: revokedCerts.map((revokedCert) => {
|
||||||
|
return {
|
||||||
|
serialNumber: revokedCert.serialNumber,
|
||||||
|
revocationDate: new Date(revokedCert.revokedAt as Date),
|
||||||
|
reason: revokedCert.revocationReason as number,
|
||||||
|
invalidity: new Date("2022/01/01"),
|
||||||
|
issuer: ca.dn
|
||||||
|
};
|
||||||
|
}),
|
||||||
|
signingAlgorithm: alg,
|
||||||
|
signingKey: sk
|
||||||
|
});
|
||||||
|
|
||||||
|
const { cipherTextBlob: encryptedCrl } = await kmsService.encrypt({
|
||||||
|
kmsId: keyId,
|
||||||
|
plainText: Buffer.from(new Uint8Array(crl.rawData))
|
||||||
|
});
|
||||||
|
|
||||||
|
await certificateAuthorityCrlDAL.update(
|
||||||
|
{
|
||||||
|
caId: ca.id
|
||||||
|
},
|
||||||
|
{
|
||||||
|
encryptedCrl
|
||||||
|
}
|
||||||
|
);
|
||||||
|
};
|
@@ -0,0 +1,145 @@
|
|||||||
|
import * as x509 from "@peculiar/x509";
|
||||||
|
import crypto from "crypto";
|
||||||
|
|
||||||
|
import { getConfig } from "@app/lib/config/env";
|
||||||
|
import { daysToMillisecond, secondsToMillis } from "@app/lib/dates";
|
||||||
|
import { BadRequestError } from "@app/lib/errors";
|
||||||
|
import { logger } from "@app/lib/logger";
|
||||||
|
import { QueueJobs, QueueName, TQueueServiceFactory } from "@app/queue";
|
||||||
|
import { TCertificateDALFactory } from "@app/services/certificate/certificate-dal";
|
||||||
|
import { CertKeyAlgorithm, CertStatus } from "@app/services/certificate/certificate-types";
|
||||||
|
import { TKmsServiceFactory } from "@app/services/kms/kms-service";
|
||||||
|
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 { TCertificateAuthorityDALFactory } from "./certificate-authority-dal";
|
||||||
|
import { keyAlgorithmToAlgCfg } from "./certificate-authority-fns";
|
||||||
|
import { TCertificateAuthoritySecretDALFactory } from "./certificate-authority-secret-dal";
|
||||||
|
import { TRotateCaCrlTriggerDTO } from "./certificate-authority-types";
|
||||||
|
|
||||||
|
type TCertificateAuthorityQueueFactoryDep = {
|
||||||
|
// TODO: Pick
|
||||||
|
certificateAuthorityDAL: TCertificateAuthorityDALFactory;
|
||||||
|
certificateAuthorityCrlDAL: TCertificateAuthorityCrlDALFactory;
|
||||||
|
certificateAuthoritySecretDAL: TCertificateAuthoritySecretDALFactory;
|
||||||
|
certificateDAL: TCertificateDALFactory;
|
||||||
|
projectDAL: Pick<TProjectDALFactory, "findProjectBySlug" | "findOne" | "updateById" | "findById" | "transaction">;
|
||||||
|
kmsService: Pick<TKmsServiceFactory, "generateKmsKey" | "encrypt" | "decrypt">;
|
||||||
|
queueService: TQueueServiceFactory;
|
||||||
|
};
|
||||||
|
export type TCertificateAuthorityQueueFactory = ReturnType<typeof certificateAuthorityQueueFactory>;
|
||||||
|
|
||||||
|
export const certificateAuthorityQueueFactory = ({
|
||||||
|
certificateAuthorityCrlDAL,
|
||||||
|
certificateAuthorityDAL,
|
||||||
|
certificateAuthoritySecretDAL,
|
||||||
|
certificateDAL,
|
||||||
|
projectDAL,
|
||||||
|
kmsService,
|
||||||
|
queueService
|
||||||
|
}: TCertificateAuthorityQueueFactoryDep) => {
|
||||||
|
// TODO 1: auto-periodic rotation
|
||||||
|
// TODO 2: manual rotation
|
||||||
|
|
||||||
|
const setCaCrlRotationInterval = async ({ caId, rotationIntervalDays }: TRotateCaCrlTriggerDTO) => {
|
||||||
|
const appCfg = getConfig();
|
||||||
|
|
||||||
|
// query for config
|
||||||
|
// const caCrl = await certificateAuthorityCrlDAL.findOne({
|
||||||
|
// caId
|
||||||
|
// });
|
||||||
|
|
||||||
|
await queueService.queue(
|
||||||
|
// TODO: clarify queue + job naming
|
||||||
|
QueueName.CaCrlRotation,
|
||||||
|
QueueJobs.CaCrlRotation,
|
||||||
|
{
|
||||||
|
caId
|
||||||
|
},
|
||||||
|
{
|
||||||
|
jobId: `ca-crl-rotation-${caId}`,
|
||||||
|
repeat: {
|
||||||
|
// on prod it this will be in days, in development this will be second
|
||||||
|
every:
|
||||||
|
appCfg.NODE_ENV === "development"
|
||||||
|
? secondsToMillis(rotationIntervalDays)
|
||||||
|
: daysToMillisecond(rotationIntervalDays),
|
||||||
|
immediately: true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
queueService.start(QueueName.CaCrlRotation, async (job) => {
|
||||||
|
const { caId } = job.data;
|
||||||
|
logger.info(`secretReminderQueue.process: [secretDocument=${caId}]`);
|
||||||
|
|
||||||
|
const ca = await certificateAuthorityDAL.findById(caId);
|
||||||
|
if (!ca) throw new BadRequestError({ message: "CA not found" });
|
||||||
|
|
||||||
|
const caSecret = await certificateAuthoritySecretDAL.findOne({ caId: ca.id });
|
||||||
|
|
||||||
|
const alg = keyAlgorithmToAlgCfg(ca.keyAlgorithm as CertKeyAlgorithm);
|
||||||
|
|
||||||
|
const keyId = await getProjectKmsCertificateKeyId({
|
||||||
|
projectId: ca.projectId,
|
||||||
|
projectDAL,
|
||||||
|
kmsService
|
||||||
|
});
|
||||||
|
|
||||||
|
const privateKey = await kmsService.decrypt({
|
||||||
|
kmsId: keyId,
|
||||||
|
cipherTextBlob: caSecret.encryptedPrivateKey
|
||||||
|
});
|
||||||
|
|
||||||
|
const skObj = crypto.createPrivateKey({ key: privateKey, format: "der", type: "pkcs8" });
|
||||||
|
const sk = await crypto.subtle.importKey("pkcs8", skObj.export({ format: "der", type: "pkcs8" }), alg, true, [
|
||||||
|
"sign"
|
||||||
|
]);
|
||||||
|
|
||||||
|
const revokedCerts = await certificateDAL.find({
|
||||||
|
caId: ca.id,
|
||||||
|
status: CertStatus.REVOKED
|
||||||
|
});
|
||||||
|
|
||||||
|
const crl = await x509.X509CrlGenerator.create({
|
||||||
|
issuer: ca.dn,
|
||||||
|
thisUpdate: new Date(),
|
||||||
|
nextUpdate: new Date("2025/12/12"), // TODO: depends on configured rebuild interval
|
||||||
|
entries: revokedCerts.map((revokedCert) => {
|
||||||
|
return {
|
||||||
|
serialNumber: revokedCert.serialNumber,
|
||||||
|
revocationDate: new Date(revokedCert.revokedAt as Date),
|
||||||
|
reason: revokedCert.revocationReason as number,
|
||||||
|
invalidity: new Date("2022/01/01"),
|
||||||
|
issuer: ca.dn
|
||||||
|
};
|
||||||
|
}),
|
||||||
|
signingAlgorithm: alg,
|
||||||
|
signingKey: sk
|
||||||
|
});
|
||||||
|
|
||||||
|
const { cipherTextBlob: encryptedCrl } = await kmsService.encrypt({
|
||||||
|
kmsId: keyId,
|
||||||
|
plainText: Buffer.from(new Uint8Array(crl.rawData))
|
||||||
|
});
|
||||||
|
|
||||||
|
await certificateAuthorityCrlDAL.update(
|
||||||
|
{
|
||||||
|
caId: ca.id
|
||||||
|
},
|
||||||
|
{
|
||||||
|
encryptedCrl
|
||||||
|
}
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
queueService.listen(QueueName.CaCrlRotation, "failed", (job, err) => {
|
||||||
|
logger.error(err, "Failed to rotate CA CRL %s", job?.id);
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
setCaCrlRotationInterval
|
||||||
|
};
|
||||||
|
};
|
@@ -0,0 +1,10 @@
|
|||||||
|
import { TDbClient } from "@app/db";
|
||||||
|
import { TableName } from "@app/db/schemas";
|
||||||
|
import { ormify } from "@app/lib/knex";
|
||||||
|
|
||||||
|
export type TCertificateAuthoritySecretDALFactory = ReturnType<typeof certificateAuthoritySecretDALFactory>;
|
||||||
|
|
||||||
|
export const certificateAuthoritySecretDALFactory = (db: TDbClient) => {
|
||||||
|
const caSecretOrm = ormify(db, TableName.CertificateAuthoritySecret);
|
||||||
|
return caSecretOrm;
|
||||||
|
};
|
@@ -0,0 +1,821 @@
|
|||||||
|
/* eslint-disable no-bitwise */
|
||||||
|
import { ForbiddenError } from "@casl/ability";
|
||||||
|
import * as x509 from "@peculiar/x509";
|
||||||
|
import crypto, { KeyObject } from "crypto";
|
||||||
|
import ms from "ms";
|
||||||
|
|
||||||
|
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 { 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 { 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 { TCertificateAuthorityCertDALFactory } from "./certificate-authority-cert-dal";
|
||||||
|
import { TCertificateAuthorityDALFactory } from "./certificate-authority-dal";
|
||||||
|
import {
|
||||||
|
createDistinguishedName,
|
||||||
|
getCaCertChain,
|
||||||
|
getCaCredentials,
|
||||||
|
keyAlgorithmToAlgCfg
|
||||||
|
} from "./certificate-authority-fns";
|
||||||
|
import { TCertificateAuthorityQueueFactory } from "./certificate-authority-queue";
|
||||||
|
import { TCertificateAuthoritySecretDALFactory } from "./certificate-authority-secret-dal";
|
||||||
|
import {
|
||||||
|
CaStatus,
|
||||||
|
CaType,
|
||||||
|
TCreateCaDTO,
|
||||||
|
TDeleteCaDTO,
|
||||||
|
TGetCaCertDTO,
|
||||||
|
TGetCaCsrDTO,
|
||||||
|
TGetCaDTO,
|
||||||
|
TImportCertToCaDTO,
|
||||||
|
TIssueCertFromCaDTO,
|
||||||
|
TSignIntermediateDTO,
|
||||||
|
TUpdateCaDTO
|
||||||
|
} from "./certificate-authority-types";
|
||||||
|
|
||||||
|
type TCertificateAuthorityServiceFactoryDep = {
|
||||||
|
certificateAuthorityDAL: Pick<
|
||||||
|
TCertificateAuthorityDALFactory,
|
||||||
|
"transaction" | "create" | "findById" | "updateById" | "deleteById" | "findOne"
|
||||||
|
>;
|
||||||
|
certificateAuthorityCertDAL: Pick<TCertificateAuthorityCertDALFactory, "create" | "findOne" | "transaction">;
|
||||||
|
certificateAuthoritySecretDAL: Pick<TCertificateAuthoritySecretDALFactory, "create" | "findOne">;
|
||||||
|
certificateAuthorityCrlDAL: Pick<TCertificateAuthorityCrlDALFactory, "create" | "findOne" | "update">;
|
||||||
|
certificateAuthorityQueue: TCertificateAuthorityQueueFactory; // TODO: Pick
|
||||||
|
certificateDAL: Pick<TCertificateDALFactory, "transaction" | "create" | "find">;
|
||||||
|
certificateBodyDAL: Pick<TCertificateBodyDALFactory, "create">;
|
||||||
|
projectDAL: Pick<TProjectDALFactory, "findProjectBySlug" | "findOne" | "updateById" | "findById" | "transaction">;
|
||||||
|
kmsService: Pick<TKmsServiceFactory, "generateKmsKey" | "encrypt" | "decrypt">;
|
||||||
|
permissionService: Pick<TPermissionServiceFactory, "getProjectPermission">;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type TCertificateAuthorityServiceFactory = ReturnType<typeof certificateAuthorityServiceFactory>;
|
||||||
|
|
||||||
|
export const certificateAuthorityServiceFactory = ({
|
||||||
|
certificateAuthorityDAL,
|
||||||
|
certificateAuthorityCertDAL,
|
||||||
|
certificateAuthoritySecretDAL,
|
||||||
|
certificateAuthorityCrlDAL,
|
||||||
|
certificateDAL,
|
||||||
|
certificateBodyDAL,
|
||||||
|
projectDAL,
|
||||||
|
kmsService,
|
||||||
|
permissionService
|
||||||
|
}: TCertificateAuthorityServiceFactoryDep) => {
|
||||||
|
/**
|
||||||
|
* Generates new root or intermediate CA
|
||||||
|
*/
|
||||||
|
const createCa = async ({
|
||||||
|
projectSlug,
|
||||||
|
type,
|
||||||
|
friendlyName,
|
||||||
|
commonName,
|
||||||
|
organization,
|
||||||
|
ou,
|
||||||
|
country,
|
||||||
|
province,
|
||||||
|
locality,
|
||||||
|
notBefore,
|
||||||
|
notAfter,
|
||||||
|
maxPathLength,
|
||||||
|
keyAlgorithm,
|
||||||
|
actorId,
|
||||||
|
actorAuthMethod,
|
||||||
|
actor,
|
||||||
|
actorOrgId
|
||||||
|
}: TCreateCaDTO) => {
|
||||||
|
const project = await projectDAL.findProjectBySlug(projectSlug, actorOrgId);
|
||||||
|
if (!project) throw new BadRequestError({ message: "Project not found" });
|
||||||
|
|
||||||
|
const { permission } = await permissionService.getProjectPermission(
|
||||||
|
actor,
|
||||||
|
actorId,
|
||||||
|
project.id,
|
||||||
|
actorAuthMethod,
|
||||||
|
actorOrgId
|
||||||
|
);
|
||||||
|
|
||||||
|
ForbiddenError.from(permission).throwUnlessCan(
|
||||||
|
ProjectPermissionActions.Create,
|
||||||
|
ProjectPermissionSub.CertificateAuthorities
|
||||||
|
);
|
||||||
|
|
||||||
|
const dn = createDistinguishedName({
|
||||||
|
commonName,
|
||||||
|
organization,
|
||||||
|
ou,
|
||||||
|
country,
|
||||||
|
province,
|
||||||
|
locality
|
||||||
|
});
|
||||||
|
|
||||||
|
const alg = keyAlgorithmToAlgCfg(keyAlgorithm);
|
||||||
|
const keys = await crypto.subtle.generateKey(alg, true, ["sign", "verify"]);
|
||||||
|
|
||||||
|
const newCa = await certificateAuthorityDAL.transaction(async (tx) => {
|
||||||
|
const notBeforeDate = notBefore ? new Date(notBefore) : new Date();
|
||||||
|
|
||||||
|
// if undefined, set [notAfterDate] to 10 years from now
|
||||||
|
const notAfterDate = notAfter
|
||||||
|
? new Date(notAfter)
|
||||||
|
: new Date(new Date().setFullYear(new Date().getFullYear() + 10));
|
||||||
|
|
||||||
|
const serialNumber = crypto.randomBytes(32).toString("hex");
|
||||||
|
|
||||||
|
const ca = await certificateAuthorityDAL.create(
|
||||||
|
{
|
||||||
|
projectId: project.id,
|
||||||
|
type,
|
||||||
|
organization,
|
||||||
|
ou,
|
||||||
|
country,
|
||||||
|
province,
|
||||||
|
locality,
|
||||||
|
friendlyName: friendlyName || dn,
|
||||||
|
commonName,
|
||||||
|
status: type === CaType.ROOT ? CaStatus.ACTIVE : CaStatus.PENDING_CERTIFICATE,
|
||||||
|
dn,
|
||||||
|
keyAlgorithm,
|
||||||
|
...(type === CaType.ROOT && {
|
||||||
|
maxPathLength,
|
||||||
|
notBefore: notBeforeDate,
|
||||||
|
notAfter: notAfterDate,
|
||||||
|
serialNumber
|
||||||
|
})
|
||||||
|
},
|
||||||
|
tx
|
||||||
|
);
|
||||||
|
|
||||||
|
const keyId = await getProjectKmsCertificateKeyId({
|
||||||
|
projectId: project.id,
|
||||||
|
projectDAL,
|
||||||
|
kmsService
|
||||||
|
});
|
||||||
|
|
||||||
|
if (type === CaType.ROOT) {
|
||||||
|
// note: create self-signed cert only applicable for root CA
|
||||||
|
const cert = await x509.X509CertificateGenerator.createSelfSigned({
|
||||||
|
name: dn,
|
||||||
|
serialNumber,
|
||||||
|
notBefore: notBeforeDate,
|
||||||
|
notAfter: notAfterDate,
|
||||||
|
signingAlgorithm: alg,
|
||||||
|
keys,
|
||||||
|
extensions: [
|
||||||
|
new x509.BasicConstraintsExtension(true, maxPathLength === -1 ? undefined : 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(keys.publicKey)
|
||||||
|
]
|
||||||
|
});
|
||||||
|
|
||||||
|
const { cipherTextBlob: encryptedCertificate } = await kmsService.encrypt({
|
||||||
|
kmsId: keyId,
|
||||||
|
plainText: Buffer.from(new Uint8Array(cert.rawData))
|
||||||
|
});
|
||||||
|
|
||||||
|
const { cipherTextBlob: encryptedCertificateChain } = await kmsService.encrypt({
|
||||||
|
kmsId: keyId,
|
||||||
|
plainText: Buffer.alloc(0)
|
||||||
|
});
|
||||||
|
|
||||||
|
await certificateAuthorityCertDAL.create(
|
||||||
|
{
|
||||||
|
caId: ca.id,
|
||||||
|
encryptedCertificate,
|
||||||
|
encryptedCertificateChain
|
||||||
|
},
|
||||||
|
tx
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// create empty CRL
|
||||||
|
const crl = await x509.X509CrlGenerator.create({
|
||||||
|
issuer: ca.dn,
|
||||||
|
thisUpdate: new Date(),
|
||||||
|
nextUpdate: new Date("2025/12/12"), // TODO: change
|
||||||
|
entries: [],
|
||||||
|
signingAlgorithm: alg,
|
||||||
|
signingKey: keys.privateKey
|
||||||
|
});
|
||||||
|
|
||||||
|
const { cipherTextBlob: encryptedCrl } = await kmsService.encrypt({
|
||||||
|
kmsId: keyId,
|
||||||
|
plainText: Buffer.from(new Uint8Array(crl.rawData))
|
||||||
|
});
|
||||||
|
|
||||||
|
await certificateAuthorityCrlDAL.create(
|
||||||
|
{
|
||||||
|
caId: ca.id,
|
||||||
|
encryptedCrl
|
||||||
|
},
|
||||||
|
tx
|
||||||
|
);
|
||||||
|
|
||||||
|
// https://nodejs.org/api/crypto.html#static-method-keyobjectfromkey
|
||||||
|
const skObj = KeyObject.from(keys.privateKey);
|
||||||
|
|
||||||
|
const { cipherTextBlob: encryptedPrivateKey } = await kmsService.encrypt({
|
||||||
|
kmsId: keyId,
|
||||||
|
plainText: skObj.export({
|
||||||
|
type: "pkcs8",
|
||||||
|
format: "der"
|
||||||
|
})
|
||||||
|
});
|
||||||
|
|
||||||
|
await certificateAuthoritySecretDAL.create(
|
||||||
|
{
|
||||||
|
caId: ca.id,
|
||||||
|
encryptedPrivateKey
|
||||||
|
},
|
||||||
|
tx
|
||||||
|
);
|
||||||
|
|
||||||
|
return ca;
|
||||||
|
});
|
||||||
|
|
||||||
|
return newCa;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return CA with id [caId]
|
||||||
|
*/
|
||||||
|
const getCaById = async ({ caId, actorId, actorAuthMethod, actor, actorOrgId }: TGetCaDTO) => {
|
||||||
|
const ca = await certificateAuthorityDAL.findById(caId);
|
||||||
|
if (!ca) throw new BadRequestError({ message: "CA not found" });
|
||||||
|
|
||||||
|
const { permission } = await permissionService.getProjectPermission(
|
||||||
|
actor,
|
||||||
|
actorId,
|
||||||
|
ca.projectId,
|
||||||
|
actorAuthMethod,
|
||||||
|
actorOrgId
|
||||||
|
);
|
||||||
|
ForbiddenError.from(permission).throwUnlessCan(
|
||||||
|
ProjectPermissionActions.Read,
|
||||||
|
ProjectPermissionSub.CertificateAuthorities
|
||||||
|
);
|
||||||
|
|
||||||
|
return ca;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update CA with id [caId].
|
||||||
|
* Note: Used to enable/disable CA
|
||||||
|
*/
|
||||||
|
const updateCaById = async ({ caId, status, actorId, actorAuthMethod, actor, actorOrgId }: TUpdateCaDTO) => {
|
||||||
|
const ca = await certificateAuthorityDAL.findById(caId);
|
||||||
|
if (!ca) throw new BadRequestError({ message: "CA not found" });
|
||||||
|
|
||||||
|
const { permission } = await permissionService.getProjectPermission(
|
||||||
|
actor,
|
||||||
|
actorId,
|
||||||
|
ca.projectId,
|
||||||
|
actorAuthMethod,
|
||||||
|
actorOrgId
|
||||||
|
);
|
||||||
|
|
||||||
|
ForbiddenError.from(permission).throwUnlessCan(
|
||||||
|
ProjectPermissionActions.Edit,
|
||||||
|
ProjectPermissionSub.CertificateAuthorities
|
||||||
|
);
|
||||||
|
|
||||||
|
const updatedCa = await certificateAuthorityDAL.updateById(caId, { status });
|
||||||
|
|
||||||
|
return updatedCa;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Delete CA with id [caId]
|
||||||
|
*/
|
||||||
|
const deleteCaById = async ({ caId, actorId, actorAuthMethod, actor, actorOrgId }: TDeleteCaDTO) => {
|
||||||
|
const ca = await certificateAuthorityDAL.findById(caId);
|
||||||
|
if (!ca) throw new BadRequestError({ message: "CA not found" });
|
||||||
|
|
||||||
|
const { permission } = await permissionService.getProjectPermission(
|
||||||
|
actor,
|
||||||
|
actorId,
|
||||||
|
ca.projectId,
|
||||||
|
actorAuthMethod,
|
||||||
|
actorOrgId
|
||||||
|
);
|
||||||
|
|
||||||
|
ForbiddenError.from(permission).throwUnlessCan(
|
||||||
|
ProjectPermissionActions.Delete,
|
||||||
|
ProjectPermissionSub.CertificateAuthorities
|
||||||
|
);
|
||||||
|
|
||||||
|
const deletedCa = await certificateAuthorityDAL.deleteById(caId);
|
||||||
|
|
||||||
|
return deletedCa;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return certificate signing request (CSR) made with CA with id [caId]
|
||||||
|
*/
|
||||||
|
const getCaCsr = async ({ caId, actorId, actorAuthMethod, actor, actorOrgId }: TGetCaCsrDTO) => {
|
||||||
|
const ca = await certificateAuthorityDAL.findById(caId);
|
||||||
|
if (!ca) throw new BadRequestError({ message: "CA not found" });
|
||||||
|
|
||||||
|
const { permission } = await permissionService.getProjectPermission(
|
||||||
|
actor,
|
||||||
|
actorId,
|
||||||
|
ca.projectId,
|
||||||
|
actorAuthMethod,
|
||||||
|
actorOrgId
|
||||||
|
);
|
||||||
|
|
||||||
|
ForbiddenError.from(permission).throwUnlessCan(
|
||||||
|
ProjectPermissionActions.Create,
|
||||||
|
ProjectPermissionSub.CertificateAuthorities
|
||||||
|
);
|
||||||
|
|
||||||
|
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,
|
||||||
|
certificateAuthoritySecretDAL,
|
||||||
|
projectDAL,
|
||||||
|
kmsService
|
||||||
|
});
|
||||||
|
|
||||||
|
const alg = keyAlgorithmToAlgCfg(ca.keyAlgorithm as CertKeyAlgorithm);
|
||||||
|
|
||||||
|
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")]
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
csr: csrObj.toString("pem"),
|
||||||
|
ca
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return 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" });
|
||||||
|
|
||||||
|
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({
|
||||||
|
caId,
|
||||||
|
certificateAuthorityDAL,
|
||||||
|
certificateAuthorityCertDAL,
|
||||||
|
projectDAL,
|
||||||
|
kmsService
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
certificate: caCert,
|
||||||
|
certificateChain: caCertChain,
|
||||||
|
serialNumber,
|
||||||
|
ca
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Issue certificate to be imported back in for intermediate CA
|
||||||
|
*/
|
||||||
|
const signIntermediate = async ({
|
||||||
|
caId,
|
||||||
|
actorId,
|
||||||
|
actorAuthMethod,
|
||||||
|
actor,
|
||||||
|
actorOrgId,
|
||||||
|
csr,
|
||||||
|
notBefore,
|
||||||
|
notAfter,
|
||||||
|
maxPathLength
|
||||||
|
}: TSignIntermediateDTO) => {
|
||||||
|
const ca = await certificateAuthorityDAL.findById(caId);
|
||||||
|
if (!ca) throw new BadRequestError({ message: "CA not found" });
|
||||||
|
|
||||||
|
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" });
|
||||||
|
|
||||||
|
const alg = keyAlgorithmToAlgCfg(ca.keyAlgorithm as CertKeyAlgorithm);
|
||||||
|
|
||||||
|
const keyId = await getProjectKmsCertificateKeyId({
|
||||||
|
projectId: ca.projectId,
|
||||||
|
projectDAL,
|
||||||
|
kmsService
|
||||||
|
});
|
||||||
|
|
||||||
|
const caCert = await certificateAuthorityCertDAL.findOne({ caId: ca.id });
|
||||||
|
const decryptedCaCert = await kmsService.decrypt({
|
||||||
|
kmsId: keyId,
|
||||||
|
cipherTextBlob: caCert.encryptedCertificate
|
||||||
|
});
|
||||||
|
|
||||||
|
const caCertObj = new x509.X509Certificate(decryptedCaCert);
|
||||||
|
const csrObj = new x509.Pkcs10CertificateRequest(csr);
|
||||||
|
|
||||||
|
// check path length constraint
|
||||||
|
const caPathLength = caCertObj.getExtension(x509.BasicConstraintsExtension)?.pathLength;
|
||||||
|
if (caPathLength !== undefined) {
|
||||||
|
if (caPathLength === 0)
|
||||||
|
throw new BadRequestError({
|
||||||
|
message: "Failed to issue intermediate certificate due to CA path length constraint"
|
||||||
|
});
|
||||||
|
if (maxPathLength >= caPathLength || (maxPathLength === -1 && caPathLength !== -1))
|
||||||
|
throw new BadRequestError({
|
||||||
|
message: "The requested path length constraint exceeds the CA's allowed path length"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const notBeforeDate = notBefore ? new Date(notBefore) : new Date();
|
||||||
|
const notAfterDate = new Date(notAfter);
|
||||||
|
|
||||||
|
const caCertNotBeforeDate = new Date(caCertObj.notBefore);
|
||||||
|
const caCertNotAfterDate = new Date(caCertObj.notAfter);
|
||||||
|
|
||||||
|
// check not before constraint
|
||||||
|
if (notBeforeDate < caCertNotBeforeDate) {
|
||||||
|
throw new BadRequestError({ message: "notBefore date is before CA certificate's notBefore date" });
|
||||||
|
}
|
||||||
|
|
||||||
|
if (notBeforeDate > notAfterDate) throw new BadRequestError({ message: "notBefore date is after notAfter date" });
|
||||||
|
|
||||||
|
// check not after constraint
|
||||||
|
if (notAfterDate > caCertNotAfterDate) {
|
||||||
|
throw new BadRequestError({ message: "notAfter date is after CA certificate's notAfter date" });
|
||||||
|
}
|
||||||
|
|
||||||
|
const { caPrivateKey } = await getCaCredentials({
|
||||||
|
caId: ca.id,
|
||||||
|
certificateAuthorityDAL,
|
||||||
|
certificateAuthoritySecretDAL,
|
||||||
|
projectDAL,
|
||||||
|
kmsService
|
||||||
|
});
|
||||||
|
|
||||||
|
const serialNumber = crypto.randomBytes(32).toString("hex");
|
||||||
|
const intermediateCert = await x509.X509CertificateGenerator.create({
|
||||||
|
serialNumber,
|
||||||
|
subject: csrObj.subject,
|
||||||
|
issuer: caCertObj.subject,
|
||||||
|
notBefore: notBeforeDate,
|
||||||
|
notAfter: notAfterDate,
|
||||||
|
signingKey: caPrivateKey,
|
||||||
|
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, maxPathLength === -1 ? undefined : maxPathLength, true),
|
||||||
|
await x509.AuthorityKeyIdentifierExtension.create(caCertObj, false),
|
||||||
|
await x509.SubjectKeyIdentifierExtension.create(csrObj.publicKey)
|
||||||
|
]
|
||||||
|
});
|
||||||
|
|
||||||
|
const { caCert: issuingCaCertificate, caCertChain } = await getCaCertChain({
|
||||||
|
caId,
|
||||||
|
certificateAuthorityDAL,
|
||||||
|
certificateAuthorityCertDAL,
|
||||||
|
projectDAL,
|
||||||
|
kmsService
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
certificate: intermediateCert.toString("pem"),
|
||||||
|
issuingCaCertificate,
|
||||||
|
certificateChain: `${issuingCaCertificate}\n${caCertChain}`.trim(),
|
||||||
|
serialNumber: intermediateCert.serialNumber,
|
||||||
|
ca
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Import certificate for (un-installed) CA with id [caId].
|
||||||
|
* Note: Can be used to import an external certificate and certificate chain
|
||||||
|
* to be installed into the CA.
|
||||||
|
*/
|
||||||
|
const importCertToCa = async ({
|
||||||
|
caId,
|
||||||
|
actorId,
|
||||||
|
actorAuthMethod,
|
||||||
|
actor,
|
||||||
|
actorOrgId,
|
||||||
|
certificate,
|
||||||
|
certificateChain
|
||||||
|
}: TImportCertToCaDTO) => {
|
||||||
|
const ca = await certificateAuthorityDAL.findById(caId);
|
||||||
|
if (!ca) throw new BadRequestError({ message: "CA not found" });
|
||||||
|
|
||||||
|
const { permission } = await permissionService.getProjectPermission(
|
||||||
|
actor,
|
||||||
|
actorId,
|
||||||
|
ca.projectId,
|
||||||
|
actorAuthMethod,
|
||||||
|
actorOrgId
|
||||||
|
);
|
||||||
|
|
||||||
|
ForbiddenError.from(permission).throwUnlessCan(
|
||||||
|
ProjectPermissionActions.Create,
|
||||||
|
ProjectPermissionSub.CertificateAuthorities
|
||||||
|
);
|
||||||
|
|
||||||
|
const caCert = await certificateAuthorityCertDAL.findOne({ caId: ca.id });
|
||||||
|
if (caCert) throw new BadRequestError({ message: "CA has already imported a certificate" });
|
||||||
|
|
||||||
|
const certObj = new x509.X509Certificate(certificate);
|
||||||
|
const maxPathLength = certObj.getExtension(x509.BasicConstraintsExtension)?.pathLength;
|
||||||
|
|
||||||
|
// validate imported certificate and certificate chain
|
||||||
|
const certificates = certificateChain
|
||||||
|
.match(/-----BEGIN CERTIFICATE-----[\s\S]+?-----END CERTIFICATE-----/g)
|
||||||
|
?.map((cert) => new x509.X509Certificate(cert));
|
||||||
|
|
||||||
|
if (!certificates) throw new BadRequestError({ message: "Failed to parse certificate chain" });
|
||||||
|
|
||||||
|
const chain = new x509.X509ChainBuilder({
|
||||||
|
certificates
|
||||||
|
});
|
||||||
|
|
||||||
|
const chainItems = await chain.build(certObj);
|
||||||
|
|
||||||
|
// chain.build() implicitly verifies the chain
|
||||||
|
if (chainItems.length !== certificates.length + 1)
|
||||||
|
throw new BadRequestError({ message: "Invalid certificate chain" });
|
||||||
|
|
||||||
|
const parentCertObj = chainItems[1];
|
||||||
|
const parentCertSubject = parentCertObj.subject;
|
||||||
|
|
||||||
|
const parentCa = await certificateAuthorityDAL.findOne({
|
||||||
|
projectId: ca.projectId,
|
||||||
|
dn: parentCertSubject
|
||||||
|
});
|
||||||
|
|
||||||
|
const keyId = await getProjectKmsCertificateKeyId({
|
||||||
|
projectId: ca.projectId,
|
||||||
|
projectDAL,
|
||||||
|
kmsService
|
||||||
|
});
|
||||||
|
|
||||||
|
const { cipherTextBlob: encryptedCertificate } = await kmsService.encrypt({
|
||||||
|
kmsId: keyId,
|
||||||
|
plainText: Buffer.from(new Uint8Array(certObj.rawData))
|
||||||
|
});
|
||||||
|
|
||||||
|
const { cipherTextBlob: encryptedCertificateChain } = await kmsService.encrypt({
|
||||||
|
kmsId: keyId,
|
||||||
|
plainText: Buffer.from(certificateChain)
|
||||||
|
});
|
||||||
|
|
||||||
|
await certificateAuthorityCertDAL.transaction(async (tx) => {
|
||||||
|
await certificateAuthorityCertDAL.create(
|
||||||
|
{
|
||||||
|
caId: ca.id,
|
||||||
|
encryptedCertificate,
|
||||||
|
encryptedCertificateChain
|
||||||
|
},
|
||||||
|
tx
|
||||||
|
);
|
||||||
|
|
||||||
|
await certificateAuthorityDAL.updateById(
|
||||||
|
ca.id,
|
||||||
|
{
|
||||||
|
status: CaStatus.ACTIVE,
|
||||||
|
maxPathLength: maxPathLength === undefined ? -1 : maxPathLength,
|
||||||
|
notBefore: new Date(certObj.notBefore),
|
||||||
|
notAfter: new Date(certObj.notAfter),
|
||||||
|
serialNumber: certObj.serialNumber,
|
||||||
|
parentCaId: parentCa?.id
|
||||||
|
},
|
||||||
|
tx
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
return { ca };
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return new leaf certificate issued by CA with id [caId]
|
||||||
|
*/
|
||||||
|
const issueCertFromCa = async ({
|
||||||
|
caId,
|
||||||
|
friendlyName,
|
||||||
|
commonName,
|
||||||
|
ttl,
|
||||||
|
notBefore,
|
||||||
|
notAfter,
|
||||||
|
actorId,
|
||||||
|
actorAuthMethod,
|
||||||
|
actor,
|
||||||
|
actorOrgId
|
||||||
|
}: TIssueCertFromCaDTO) => {
|
||||||
|
const ca = await certificateAuthorityDAL.findById(caId);
|
||||||
|
if (!ca) throw new BadRequestError({ message: "CA not found" });
|
||||||
|
|
||||||
|
const { permission } = await permissionService.getProjectPermission(
|
||||||
|
actor,
|
||||||
|
actorId,
|
||||||
|
ca.projectId,
|
||||||
|
actorAuthMethod,
|
||||||
|
actorOrgId
|
||||||
|
);
|
||||||
|
|
||||||
|
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Create, ProjectPermissionSub.Certificates);
|
||||||
|
|
||||||
|
if (ca.status === CaStatus.DISABLED) throw new BadRequestError({ message: "CA is disabled" });
|
||||||
|
|
||||||
|
const caCert = await certificateAuthorityCertDAL.findOne({ caId: ca.id });
|
||||||
|
if (!caCert) throw new BadRequestError({ message: "CA does not have a certificate installed" });
|
||||||
|
|
||||||
|
const keyId = await getProjectKmsCertificateKeyId({
|
||||||
|
projectId: ca.projectId,
|
||||||
|
projectDAL,
|
||||||
|
kmsService
|
||||||
|
});
|
||||||
|
|
||||||
|
const decryptedCaCert = await kmsService.decrypt({
|
||||||
|
kmsId: keyId,
|
||||||
|
cipherTextBlob: caCert.encryptedCertificate
|
||||||
|
});
|
||||||
|
|
||||||
|
const caCertObj = new x509.X509Certificate(decryptedCaCert);
|
||||||
|
|
||||||
|
const notBeforeDate = notBefore ? new Date(notBefore) : new Date();
|
||||||
|
|
||||||
|
let notAfterDate = new Date(new Date().setFullYear(new Date().getFullYear() + 1));
|
||||||
|
if (notAfter) {
|
||||||
|
notAfterDate = new Date(notAfter);
|
||||||
|
} else if (ttl) {
|
||||||
|
notAfterDate = new Date(new Date().getTime() + ms(ttl));
|
||||||
|
}
|
||||||
|
|
||||||
|
const caCertNotBeforeDate = new Date(caCertObj.notBefore);
|
||||||
|
const caCertNotAfterDate = new Date(caCertObj.notAfter);
|
||||||
|
|
||||||
|
// check not before constraint
|
||||||
|
if (notBeforeDate < caCertNotBeforeDate) {
|
||||||
|
throw new BadRequestError({ message: "notBefore date is before CA certificate's notBefore date" });
|
||||||
|
}
|
||||||
|
|
||||||
|
if (notBeforeDate > notAfterDate) throw new BadRequestError({ message: "notBefore date is after notAfter date" });
|
||||||
|
|
||||||
|
// check not after constraint
|
||||||
|
if (notAfterDate > caCertNotAfterDate) {
|
||||||
|
throw new BadRequestError({ message: "notAfter date is after CA certificate's notAfter date" });
|
||||||
|
}
|
||||||
|
|
||||||
|
const alg = keyAlgorithmToAlgCfg(ca.keyAlgorithm as CertKeyAlgorithm);
|
||||||
|
const leafKeys = await crypto.subtle.generateKey(alg, true, ["sign", "verify"]);
|
||||||
|
|
||||||
|
const csrObj = await x509.Pkcs10CertificateRequestGenerator.create({
|
||||||
|
name: `CN=${commonName}`,
|
||||||
|
keys: leafKeys,
|
||||||
|
signingAlgorithm: alg,
|
||||||
|
extensions: [
|
||||||
|
// eslint-disable-next-line no-bitwise
|
||||||
|
new x509.KeyUsagesExtension(x509.KeyUsageFlags.digitalSignature | x509.KeyUsageFlags.keyEncipherment)
|
||||||
|
],
|
||||||
|
attributes: [new x509.ChallengePasswordAttribute("password")]
|
||||||
|
});
|
||||||
|
|
||||||
|
const { caPrivateKey } = await getCaCredentials({
|
||||||
|
caId: ca.id,
|
||||||
|
certificateAuthorityDAL,
|
||||||
|
certificateAuthoritySecretDAL,
|
||||||
|
projectDAL,
|
||||||
|
kmsService
|
||||||
|
});
|
||||||
|
|
||||||
|
const serialNumber = crypto.randomBytes(32).toString("hex");
|
||||||
|
const leafCert = await x509.X509CertificateGenerator.create({
|
||||||
|
serialNumber,
|
||||||
|
subject: csrObj.subject,
|
||||||
|
issuer: caCertObj.subject,
|
||||||
|
notBefore: notBeforeDate,
|
||||||
|
notAfter: notAfterDate,
|
||||||
|
signingKey: caPrivateKey,
|
||||||
|
publicKey: csrObj.publicKey,
|
||||||
|
signingAlgorithm: alg,
|
||||||
|
extensions: [
|
||||||
|
new x509.KeyUsagesExtension(x509.KeyUsageFlags.digitalSignature | x509.KeyUsageFlags.keyEncipherment, true),
|
||||||
|
new x509.BasicConstraintsExtension(false),
|
||||||
|
await x509.AuthorityKeyIdentifierExtension.create(caCertObj, false),
|
||||||
|
await x509.SubjectKeyIdentifierExtension.create(csrObj.publicKey)
|
||||||
|
]
|
||||||
|
});
|
||||||
|
|
||||||
|
const skLeafObj = KeyObject.from(leafKeys.privateKey);
|
||||||
|
const skLeaf = skLeafObj.export({ format: "pem", type: "pkcs8" }) as string;
|
||||||
|
|
||||||
|
const { cipherTextBlob: encryptedCertificate } = await kmsService.encrypt({
|
||||||
|
kmsId: keyId,
|
||||||
|
plainText: Buffer.from(new Uint8Array(leafCert.rawData))
|
||||||
|
});
|
||||||
|
|
||||||
|
await certificateDAL.transaction(async (tx) => {
|
||||||
|
const cert = await certificateDAL.create(
|
||||||
|
{
|
||||||
|
caId: ca.id,
|
||||||
|
status: CertStatus.ACTIVE,
|
||||||
|
friendlyName: friendlyName || commonName,
|
||||||
|
commonName,
|
||||||
|
serialNumber,
|
||||||
|
notBefore: notBeforeDate,
|
||||||
|
notAfter: notAfterDate
|
||||||
|
},
|
||||||
|
tx
|
||||||
|
);
|
||||||
|
|
||||||
|
await certificateBodyDAL.create(
|
||||||
|
{
|
||||||
|
certId: cert.id,
|
||||||
|
encryptedCertificate
|
||||||
|
},
|
||||||
|
tx
|
||||||
|
);
|
||||||
|
|
||||||
|
return cert;
|
||||||
|
});
|
||||||
|
|
||||||
|
const { caCert: issuingCaCertificate, caCertChain } = await getCaCertChain({
|
||||||
|
caId: ca.id,
|
||||||
|
certificateAuthorityDAL,
|
||||||
|
certificateAuthorityCertDAL,
|
||||||
|
projectDAL,
|
||||||
|
kmsService
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
certificate: leafCert.toString("pem"),
|
||||||
|
certificateChain: `${issuingCaCertificate}\n${caCertChain}`.trim(),
|
||||||
|
issuingCaCertificate,
|
||||||
|
privateKey: skLeaf,
|
||||||
|
serialNumber,
|
||||||
|
ca
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
createCa,
|
||||||
|
getCaById,
|
||||||
|
updateCaById,
|
||||||
|
deleteCaById,
|
||||||
|
getCaCsr,
|
||||||
|
getCaCert,
|
||||||
|
signIntermediate,
|
||||||
|
importCertToCa,
|
||||||
|
issueCertFromCa
|
||||||
|
};
|
||||||
|
};
|
@@ -0,0 +1,121 @@
|
|||||||
|
import { TProjectPermission } from "@app/lib/types";
|
||||||
|
import { TCertificateDALFactory } from "@app/services/certificate/certificate-dal";
|
||||||
|
import { TKmsServiceFactory } from "@app/services/kms/kms-service";
|
||||||
|
import { TProjectDALFactory } from "@app/services/project/project-dal";
|
||||||
|
|
||||||
|
import { TCertificateAuthorityCrlDALFactory } from "../../ee/services/certificate-authority-crl/certificate-authority-crl-dal";
|
||||||
|
import { CertKeyAlgorithm } from "../certificate/certificate-types";
|
||||||
|
import { TCertificateAuthorityCertDALFactory } from "./certificate-authority-cert-dal";
|
||||||
|
import { TCertificateAuthorityDALFactory } from "./certificate-authority-dal";
|
||||||
|
import { TCertificateAuthoritySecretDALFactory } from "./certificate-authority-secret-dal";
|
||||||
|
|
||||||
|
export enum CaType {
|
||||||
|
ROOT = "root",
|
||||||
|
INTERMEDIATE = "intermediate"
|
||||||
|
}
|
||||||
|
|
||||||
|
export enum CaStatus {
|
||||||
|
ACTIVE = "active",
|
||||||
|
DISABLED = "disabled",
|
||||||
|
PENDING_CERTIFICATE = "pending-certificate"
|
||||||
|
}
|
||||||
|
|
||||||
|
export type TCreateCaDTO = {
|
||||||
|
projectSlug: string;
|
||||||
|
type: CaType;
|
||||||
|
friendlyName?: string;
|
||||||
|
commonName: string;
|
||||||
|
organization: string;
|
||||||
|
ou: string;
|
||||||
|
country: string;
|
||||||
|
province: string;
|
||||||
|
locality: string;
|
||||||
|
notBefore?: string;
|
||||||
|
notAfter?: string;
|
||||||
|
maxPathLength: number;
|
||||||
|
keyAlgorithm: CertKeyAlgorithm;
|
||||||
|
} & Omit<TProjectPermission, "projectId">;
|
||||||
|
|
||||||
|
export type TGetCaDTO = {
|
||||||
|
caId: string;
|
||||||
|
} & Omit<TProjectPermission, "projectId">;
|
||||||
|
|
||||||
|
export type TUpdateCaDTO = {
|
||||||
|
caId: string;
|
||||||
|
status?: CaStatus;
|
||||||
|
} & Omit<TProjectPermission, "projectId">;
|
||||||
|
|
||||||
|
export type TDeleteCaDTO = {
|
||||||
|
caId: string;
|
||||||
|
} & Omit<TProjectPermission, "projectId">;
|
||||||
|
|
||||||
|
export type TGetCaCsrDTO = {
|
||||||
|
caId: string;
|
||||||
|
} & Omit<TProjectPermission, "projectId">;
|
||||||
|
|
||||||
|
export type TGetCaCertDTO = {
|
||||||
|
caId: string;
|
||||||
|
} & Omit<TProjectPermission, "projectId">;
|
||||||
|
|
||||||
|
export type TSignIntermediateDTO = {
|
||||||
|
caId: string;
|
||||||
|
csr: string;
|
||||||
|
notBefore?: string;
|
||||||
|
notAfter: string;
|
||||||
|
maxPathLength: number;
|
||||||
|
} & Omit<TProjectPermission, "projectId">;
|
||||||
|
|
||||||
|
export type TImportCertToCaDTO = {
|
||||||
|
caId: string;
|
||||||
|
certificate: string;
|
||||||
|
certificateChain: string;
|
||||||
|
} & Omit<TProjectPermission, "projectId">;
|
||||||
|
|
||||||
|
export type TIssueCertFromCaDTO = {
|
||||||
|
caId: string;
|
||||||
|
friendlyName?: string;
|
||||||
|
commonName: string;
|
||||||
|
ttl: string;
|
||||||
|
notBefore?: string;
|
||||||
|
notAfter?: string;
|
||||||
|
} & Omit<TProjectPermission, "projectId">;
|
||||||
|
|
||||||
|
export type TDNParts = {
|
||||||
|
commonName?: string;
|
||||||
|
organization?: string;
|
||||||
|
ou?: string;
|
||||||
|
country?: string;
|
||||||
|
province?: string;
|
||||||
|
locality?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type TGetCaCredentialsDTO = {
|
||||||
|
caId: string;
|
||||||
|
certificateAuthorityDAL: Pick<TCertificateAuthorityDALFactory, "findById">;
|
||||||
|
certificateAuthoritySecretDAL: Pick<TCertificateAuthoritySecretDALFactory, "findOne">;
|
||||||
|
projectDAL: Pick<TProjectDALFactory, "findOne" | "updateById" | "transaction">;
|
||||||
|
kmsService: Pick<TKmsServiceFactory, "decrypt" | "generateKmsKey">;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type TGetCaCertChainDTO = {
|
||||||
|
caId: string;
|
||||||
|
certificateAuthorityDAL: Pick<TCertificateAuthorityDALFactory, "findById">;
|
||||||
|
certificateAuthorityCertDAL: Pick<TCertificateAuthorityCertDALFactory, "findOne">;
|
||||||
|
projectDAL: Pick<TProjectDALFactory, "findOne" | "updateById" | "transaction">;
|
||||||
|
kmsService: Pick<TKmsServiceFactory, "decrypt" | "generateKmsKey">;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type TRebuildCaCrlDTO = {
|
||||||
|
caId: string;
|
||||||
|
certificateAuthorityDAL: Pick<TCertificateAuthorityDALFactory, "findById">;
|
||||||
|
certificateAuthorityCrlDAL: Pick<TCertificateAuthorityCrlDALFactory, "update">;
|
||||||
|
certificateAuthoritySecretDAL: Pick<TCertificateAuthoritySecretDALFactory, "findOne">;
|
||||||
|
projectDAL: Pick<TProjectDALFactory, "findOne" | "updateById" | "transaction">;
|
||||||
|
certificateDAL: Pick<TCertificateDALFactory, "find">;
|
||||||
|
kmsService: Pick<TKmsServiceFactory, "generateKmsKey" | "decrypt" | "encrypt">;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type TRotateCaCrlTriggerDTO = {
|
||||||
|
caId: string;
|
||||||
|
rotationIntervalDays: number;
|
||||||
|
};
|
@@ -0,0 +1,8 @@
|
|||||||
|
import { z } from "zod";
|
||||||
|
|
||||||
|
const isValidDate = (dateString: string) => {
|
||||||
|
const date = new Date(dateString);
|
||||||
|
return !Number.isNaN(date.getTime());
|
||||||
|
};
|
||||||
|
|
||||||
|
export const validateCaDateField = z.string().trim().refine(isValidDate, { message: "Invalid date format" });
|
10
backend/src/services/certificate/certificate-body-dal.ts
Normal file
10
backend/src/services/certificate/certificate-body-dal.ts
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
import { TDbClient } from "@app/db";
|
||||||
|
import { TableName } from "@app/db/schemas";
|
||||||
|
import { ormify } from "@app/lib/knex";
|
||||||
|
|
||||||
|
export type TCertificateBodyDALFactory = ReturnType<typeof certificateBodyDALFactory>;
|
||||||
|
|
||||||
|
export const certificateBodyDALFactory = (db: TDbClient) => {
|
||||||
|
const certificateBodyOrm = ormify(db, TableName.CertificateBody);
|
||||||
|
return certificateBodyOrm;
|
||||||
|
};
|
34
backend/src/services/certificate/certificate-dal.ts
Normal file
34
backend/src/services/certificate/certificate-dal.ts
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
import { TDbClient } from "@app/db";
|
||||||
|
import { TableName } from "@app/db/schemas";
|
||||||
|
import { DatabaseError } from "@app/lib/errors";
|
||||||
|
import { ormify } from "@app/lib/knex";
|
||||||
|
|
||||||
|
export type TCertificateDALFactory = ReturnType<typeof certificateDALFactory>;
|
||||||
|
|
||||||
|
export const certificateDALFactory = (db: TDbClient) => {
|
||||||
|
const certificateOrm = ormify(db, TableName.Certificate);
|
||||||
|
|
||||||
|
const countCertificatesInProject = async (projectId: string) => {
|
||||||
|
try {
|
||||||
|
interface CountResult {
|
||||||
|
count: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const count = await db(TableName.Certificate)
|
||||||
|
.join(TableName.CertificateAuthority, `${TableName.Certificate}.caId`, `${TableName.CertificateAuthority}.id`)
|
||||||
|
.join(TableName.Project, `${TableName.CertificateAuthority}.projectId`, `${TableName.Project}.id`)
|
||||||
|
.where(`${TableName.Project}.id`, projectId)
|
||||||
|
.count("*")
|
||||||
|
.first();
|
||||||
|
|
||||||
|
return parseInt((count as unknown as CountResult).count || "0", 10);
|
||||||
|
} catch (error) {
|
||||||
|
throw new DatabaseError({ error, name: "Count all project certificates" });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
...certificateOrm,
|
||||||
|
countCertificatesInProject
|
||||||
|
};
|
||||||
|
};
|
26
backend/src/services/certificate/certificate-fns.ts
Normal file
26
backend/src/services/certificate/certificate-fns.ts
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
import * as x509 from "@peculiar/x509";
|
||||||
|
|
||||||
|
import { CrlReason } from "./certificate-types";
|
||||||
|
|
||||||
|
export const revocationReasonToCrlCode = (crlReason: CrlReason) => {
|
||||||
|
switch (crlReason) {
|
||||||
|
case CrlReason.KEY_COMPROMISE:
|
||||||
|
return x509.X509CrlReason.keyCompromise;
|
||||||
|
case CrlReason.CA_COMPROMISE:
|
||||||
|
return x509.X509CrlReason.cACompromise;
|
||||||
|
case CrlReason.AFFILIATION_CHANGED:
|
||||||
|
return x509.X509CrlReason.affiliationChanged;
|
||||||
|
case CrlReason.SUPERSEDED:
|
||||||
|
return x509.X509CrlReason.superseded;
|
||||||
|
case CrlReason.CESSATION_OF_OPERATION:
|
||||||
|
return x509.X509CrlReason.cessationOfOperation;
|
||||||
|
case CrlReason.CERTIFICATE_HOLD:
|
||||||
|
return x509.X509CrlReason.certificateHold;
|
||||||
|
case CrlReason.PRIVILEGE_WITHDRAWN:
|
||||||
|
return x509.X509CrlReason.privilegeWithdrawn;
|
||||||
|
case CrlReason.A_A_COMPROMISE:
|
||||||
|
return x509.X509CrlReason.aACompromise;
|
||||||
|
default:
|
||||||
|
return x509.X509CrlReason.unspecified;
|
||||||
|
}
|
||||||
|
};
|
203
backend/src/services/certificate/certificate-service.ts
Normal file
203
backend/src/services/certificate/certificate-service.ts
Normal file
@@ -0,0 +1,203 @@
|
|||||||
|
import { ForbiddenError } from "@casl/ability";
|
||||||
|
import * as x509 from "@peculiar/x509";
|
||||||
|
|
||||||
|
import { TCertificateAuthorityCrlDALFactory } from "@app/ee/services/certificate-authority-crl/certificate-authority-crl-dal";
|
||||||
|
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service";
|
||||||
|
import { ProjectPermissionActions, ProjectPermissionSub } from "@app/ee/services/permission/project-permission";
|
||||||
|
import { TCertificateBodyDALFactory } from "@app/services/certificate/certificate-body-dal";
|
||||||
|
import { TCertificateDALFactory } from "@app/services/certificate/certificate-dal";
|
||||||
|
import { TCertificateAuthorityCertDALFactory } from "@app/services/certificate-authority/certificate-authority-cert-dal";
|
||||||
|
import { TCertificateAuthorityDALFactory } from "@app/services/certificate-authority/certificate-authority-dal";
|
||||||
|
import { TCertificateAuthoritySecretDALFactory } from "@app/services/certificate-authority/certificate-authority-secret-dal";
|
||||||
|
import { TKmsServiceFactory } from "@app/services/kms/kms-service";
|
||||||
|
import { TProjectDALFactory } from "@app/services/project/project-dal";
|
||||||
|
import { getProjectKmsCertificateKeyId } from "@app/services/project/project-fns";
|
||||||
|
|
||||||
|
import { getCaCertChain, rebuildCaCrl } from "../certificate-authority/certificate-authority-fns";
|
||||||
|
import { revocationReasonToCrlCode } from "./certificate-fns";
|
||||||
|
import { CertStatus, TDeleteCertDTO, TGetCertBodyDTO, TGetCertDTO, TRevokeCertDTO } from "./certificate-types";
|
||||||
|
|
||||||
|
type TCertificateServiceFactoryDep = {
|
||||||
|
certificateDAL: Pick<TCertificateDALFactory, "findOne" | "deleteById" | "update" | "find">;
|
||||||
|
certificateBodyDAL: Pick<TCertificateBodyDALFactory, "findOne">;
|
||||||
|
certificateAuthorityDAL: Pick<TCertificateAuthorityDALFactory, "findById">;
|
||||||
|
certificateAuthorityCertDAL: Pick<TCertificateAuthorityCertDALFactory, "findOne">;
|
||||||
|
certificateAuthorityCrlDAL: Pick<TCertificateAuthorityCrlDALFactory, "update">;
|
||||||
|
certificateAuthoritySecretDAL: Pick<TCertificateAuthoritySecretDALFactory, "findOne">;
|
||||||
|
projectDAL: Pick<TProjectDALFactory, "findOne" | "updateById" | "findById" | "transaction">;
|
||||||
|
kmsService: Pick<TKmsServiceFactory, "generateKmsKey" | "encrypt" | "decrypt">;
|
||||||
|
permissionService: Pick<TPermissionServiceFactory, "getProjectPermission">;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type TCertificateServiceFactory = ReturnType<typeof certificateServiceFactory>;
|
||||||
|
|
||||||
|
export const certificateServiceFactory = ({
|
||||||
|
certificateDAL,
|
||||||
|
certificateBodyDAL,
|
||||||
|
certificateAuthorityDAL,
|
||||||
|
certificateAuthorityCertDAL,
|
||||||
|
certificateAuthorityCrlDAL,
|
||||||
|
certificateAuthoritySecretDAL,
|
||||||
|
projectDAL,
|
||||||
|
kmsService,
|
||||||
|
permissionService
|
||||||
|
}: TCertificateServiceFactoryDep) => {
|
||||||
|
/**
|
||||||
|
* Return details for certificate with serial number [serialNumber]
|
||||||
|
*/
|
||||||
|
const getCert = async ({ serialNumber, actorId, actorAuthMethod, actor, actorOrgId }: TGetCertDTO) => {
|
||||||
|
const cert = await certificateDAL.findOne({ serialNumber });
|
||||||
|
const ca = await certificateAuthorityDAL.findById(cert.caId);
|
||||||
|
|
||||||
|
const { permission } = await permissionService.getProjectPermission(
|
||||||
|
actor,
|
||||||
|
actorId,
|
||||||
|
ca.projectId,
|
||||||
|
actorAuthMethod,
|
||||||
|
actorOrgId
|
||||||
|
);
|
||||||
|
|
||||||
|
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.Certificates);
|
||||||
|
|
||||||
|
return {
|
||||||
|
cert,
|
||||||
|
ca
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Delete certificate with serial number [serialNumber]
|
||||||
|
*/
|
||||||
|
const deleteCert = async ({ serialNumber, actorId, actorAuthMethod, actor, actorOrgId }: TDeleteCertDTO) => {
|
||||||
|
const cert = await certificateDAL.findOne({ serialNumber });
|
||||||
|
const ca = await certificateAuthorityDAL.findById(cert.caId);
|
||||||
|
|
||||||
|
const { permission } = await permissionService.getProjectPermission(
|
||||||
|
actor,
|
||||||
|
actorId,
|
||||||
|
ca.projectId,
|
||||||
|
actorAuthMethod,
|
||||||
|
actorOrgId
|
||||||
|
);
|
||||||
|
|
||||||
|
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Delete, ProjectPermissionSub.Certificates);
|
||||||
|
|
||||||
|
const deletedCert = await certificateDAL.deleteById(cert.id);
|
||||||
|
|
||||||
|
return {
|
||||||
|
deletedCert,
|
||||||
|
ca
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Revoke certificate with serial number [serialNumber].
|
||||||
|
* Note: Revoking a certificate adds it to the certificate revocation list (CRL)
|
||||||
|
* of its issuing CA
|
||||||
|
*/
|
||||||
|
const revokeCert = async ({
|
||||||
|
serialNumber,
|
||||||
|
revocationReason,
|
||||||
|
actorId,
|
||||||
|
actorAuthMethod,
|
||||||
|
actor,
|
||||||
|
actorOrgId
|
||||||
|
}: TRevokeCertDTO) => {
|
||||||
|
const cert = await certificateDAL.findOne({ serialNumber });
|
||||||
|
const ca = await certificateAuthorityDAL.findById(cert.caId);
|
||||||
|
|
||||||
|
const { permission } = await permissionService.getProjectPermission(
|
||||||
|
actor,
|
||||||
|
actorId,
|
||||||
|
ca.projectId,
|
||||||
|
actorAuthMethod,
|
||||||
|
actorOrgId
|
||||||
|
);
|
||||||
|
|
||||||
|
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Delete, ProjectPermissionSub.Certificates);
|
||||||
|
|
||||||
|
if (cert.status === CertStatus.REVOKED) throw new Error("Certificate already revoked");
|
||||||
|
|
||||||
|
const revokedAt = new Date();
|
||||||
|
await certificateDAL.update(
|
||||||
|
{
|
||||||
|
id: cert.id
|
||||||
|
},
|
||||||
|
{
|
||||||
|
status: CertStatus.REVOKED,
|
||||||
|
revokedAt,
|
||||||
|
revocationReason: revocationReasonToCrlCode(revocationReason)
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
// rebuild CRL (TODO: move to interval-based cron job)
|
||||||
|
await rebuildCaCrl({
|
||||||
|
caId: ca.id,
|
||||||
|
certificateAuthorityDAL,
|
||||||
|
certificateAuthorityCrlDAL,
|
||||||
|
certificateAuthoritySecretDAL,
|
||||||
|
projectDAL,
|
||||||
|
certificateDAL,
|
||||||
|
kmsService
|
||||||
|
});
|
||||||
|
|
||||||
|
return { revokedAt, cert, ca };
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return certificate body and certificate chain for certificate with
|
||||||
|
* serial number [serialNumber]
|
||||||
|
*/
|
||||||
|
const getCertBody = async ({ serialNumber, actorId, actorAuthMethod, actor, actorOrgId }: TGetCertBodyDTO) => {
|
||||||
|
const cert = await certificateDAL.findOne({ serialNumber });
|
||||||
|
const ca = await certificateAuthorityDAL.findById(cert.caId);
|
||||||
|
|
||||||
|
const { permission } = await permissionService.getProjectPermission(
|
||||||
|
actor,
|
||||||
|
actorId,
|
||||||
|
ca.projectId,
|
||||||
|
actorAuthMethod,
|
||||||
|
actorOrgId
|
||||||
|
);
|
||||||
|
|
||||||
|
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.Certificates);
|
||||||
|
|
||||||
|
const certBody = await certificateBodyDAL.findOne({ certId: cert.id });
|
||||||
|
|
||||||
|
const keyId = await getProjectKmsCertificateKeyId({
|
||||||
|
projectId: ca.projectId,
|
||||||
|
projectDAL,
|
||||||
|
kmsService
|
||||||
|
});
|
||||||
|
|
||||||
|
const decryptedCert = await kmsService.decrypt({
|
||||||
|
kmsId: keyId,
|
||||||
|
cipherTextBlob: certBody.encryptedCertificate
|
||||||
|
});
|
||||||
|
|
||||||
|
const certObj = new x509.X509Certificate(decryptedCert);
|
||||||
|
|
||||||
|
const { caCert, caCertChain } = await getCaCertChain({
|
||||||
|
caId: ca.id,
|
||||||
|
certificateAuthorityDAL,
|
||||||
|
certificateAuthorityCertDAL,
|
||||||
|
projectDAL,
|
||||||
|
kmsService
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
certificate: certObj.toString("pem"),
|
||||||
|
certificateChain: `${caCert}\n${caCertChain}`.trim(),
|
||||||
|
serialNumber: certObj.serialNumber,
|
||||||
|
cert,
|
||||||
|
ca
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
getCert,
|
||||||
|
deleteCert,
|
||||||
|
revokeCert,
|
||||||
|
getCertBody
|
||||||
|
};
|
||||||
|
};
|
43
backend/src/services/certificate/certificate-types.ts
Normal file
43
backend/src/services/certificate/certificate-types.ts
Normal file
@@ -0,0 +1,43 @@
|
|||||||
|
import { TProjectPermission } from "@app/lib/types";
|
||||||
|
|
||||||
|
export enum CertStatus {
|
||||||
|
ACTIVE = "active",
|
||||||
|
REVOKED = "revoked"
|
||||||
|
}
|
||||||
|
|
||||||
|
export enum CertKeyAlgorithm {
|
||||||
|
RSA_2048 = "RSA_2048",
|
||||||
|
RSA_4096 = "RSA_4096",
|
||||||
|
ECDSA_P256 = "EC_prime256v1",
|
||||||
|
ECDSA_P384 = "EC_secp384r1"
|
||||||
|
}
|
||||||
|
|
||||||
|
export enum CrlReason {
|
||||||
|
UNSPECIFIED = "UNSPECIFIED",
|
||||||
|
KEY_COMPROMISE = "KEY_COMPROMISE",
|
||||||
|
CA_COMPROMISE = "CA_COMPROMISE",
|
||||||
|
AFFILIATION_CHANGED = "AFFILIATION_CHANGED",
|
||||||
|
SUPERSEDED = "SUPERSEDED",
|
||||||
|
CESSATION_OF_OPERATION = "CESSATION_OF_OPERATION",
|
||||||
|
CERTIFICATE_HOLD = "CERTIFICATE_HOLD",
|
||||||
|
// REMOVE_FROM_CRL = "REMOVE_FROM_CRL",
|
||||||
|
PRIVILEGE_WITHDRAWN = "PRIVILEGE_WITHDRAWN",
|
||||||
|
A_A_COMPROMISE = "A_A_COMPROMISE"
|
||||||
|
}
|
||||||
|
|
||||||
|
export type TGetCertDTO = {
|
||||||
|
serialNumber: string;
|
||||||
|
} & Omit<TProjectPermission, "projectId">;
|
||||||
|
|
||||||
|
export type TDeleteCertDTO = {
|
||||||
|
serialNumber: string;
|
||||||
|
} & Omit<TProjectPermission, "projectId">;
|
||||||
|
|
||||||
|
export type TRevokeCertDTO = {
|
||||||
|
serialNumber: string;
|
||||||
|
revocationReason: CrlReason;
|
||||||
|
} & Omit<TProjectPermission, "projectId">;
|
||||||
|
|
||||||
|
export type TGetCertBodyDTO = {
|
||||||
|
serialNumber: string;
|
||||||
|
} & Omit<TProjectPermission, "projectId">;
|
@@ -1921,13 +1921,13 @@ const syncSecretsGitLab = async ({
|
|||||||
return allEnvVariables;
|
return allEnvVariables;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const metadata = IntegrationMetadataSchema.parse(integration.metadata);
|
||||||
const allEnvVariables = await getAllEnvVariables(integration?.appId as string, accessToken);
|
const allEnvVariables = await getAllEnvVariables(integration?.appId as string, accessToken);
|
||||||
const getSecretsRes: GitLabSecret[] = allEnvVariables
|
const getSecretsRes: GitLabSecret[] = allEnvVariables
|
||||||
.filter((secret: GitLabSecret) => secret.environment_scope === integration.targetEnvironment)
|
.filter((secret: GitLabSecret) => secret.environment_scope === integration.targetEnvironment)
|
||||||
.filter((gitLabSecret) => {
|
.filter((gitLabSecret) => {
|
||||||
let isValid = true;
|
let isValid = true;
|
||||||
|
|
||||||
const metadata = z.record(z.any()).parse(integration.metadata);
|
|
||||||
if (metadata.secretPrefix && !gitLabSecret.key.startsWith(metadata.secretPrefix)) {
|
if (metadata.secretPrefix && !gitLabSecret.key.startsWith(metadata.secretPrefix)) {
|
||||||
isValid = false;
|
isValid = false;
|
||||||
}
|
}
|
||||||
@@ -1947,8 +1947,8 @@ const syncSecretsGitLab = async ({
|
|||||||
{
|
{
|
||||||
key,
|
key,
|
||||||
value: secrets[key].value,
|
value: secrets[key].value,
|
||||||
protected: false,
|
protected: Boolean(metadata.shouldProtectSecrets),
|
||||||
masked: false,
|
masked: Boolean(metadata.shouldMaskSecrets),
|
||||||
raw: false,
|
raw: false,
|
||||||
environment_scope: integration.targetEnvironment
|
environment_scope: integration.targetEnvironment
|
||||||
},
|
},
|
||||||
@@ -1965,7 +1965,9 @@ const syncSecretsGitLab = async ({
|
|||||||
`${gitLabApiUrl}/v4/projects/${integration?.appId}/variables/${existingSecret.key}?filter[environment_scope]=${integration.targetEnvironment}`,
|
`${gitLabApiUrl}/v4/projects/${integration?.appId}/variables/${existingSecret.key}?filter[environment_scope]=${integration.targetEnvironment}`,
|
||||||
{
|
{
|
||||||
...existingSecret,
|
...existingSecret,
|
||||||
value: secrets[existingSecret.key].value
|
value: secrets[existingSecret.key].value,
|
||||||
|
protected: Boolean(metadata.shouldProtectSecrets),
|
||||||
|
masked: Boolean(metadata.shouldMaskSecrets)
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
headers: {
|
headers: {
|
||||||
|
@@ -31,5 +31,7 @@ export const IntegrationMetadataSchema = z.object({
|
|||||||
.describe(INTEGRATION.CREATE.metadata.secretAWSTag),
|
.describe(INTEGRATION.CREATE.metadata.secretAWSTag),
|
||||||
kmsKeyId: z.string().optional().describe(INTEGRATION.CREATE.metadata.kmsKeyId),
|
kmsKeyId: z.string().optional().describe(INTEGRATION.CREATE.metadata.kmsKeyId),
|
||||||
shouldDisableDelete: z.boolean().optional().describe(INTEGRATION.CREATE.metadata.shouldDisableDelete),
|
shouldDisableDelete: z.boolean().optional().describe(INTEGRATION.CREATE.metadata.shouldDisableDelete),
|
||||||
shouldEnableDelete: z.boolean().optional().describe(INTEGRATION.CREATE.metadata.shouldEnableDelete)
|
shouldEnableDelete: z.boolean().optional().describe(INTEGRATION.CREATE.metadata.shouldEnableDelete),
|
||||||
|
shouldMaskSecrets: z.boolean().optional().describe(INTEGRATION.CREATE.metadata.shouldMaskSecrets),
|
||||||
|
shouldProtectSecrets: z.boolean().optional().describe(INTEGRATION.CREATE.metadata.shouldProtectSecrets)
|
||||||
});
|
});
|
||||||
|
@@ -29,6 +29,8 @@ export type TCreateIntegrationDTO = {
|
|||||||
}[];
|
}[];
|
||||||
kmsKeyId?: string;
|
kmsKeyId?: string;
|
||||||
shouldDisableDelete?: boolean;
|
shouldDisableDelete?: boolean;
|
||||||
|
shouldMaskSecrets?: boolean;
|
||||||
|
shouldProtectSecrets?: boolean;
|
||||||
shouldEnableDelete?: boolean;
|
shouldEnableDelete?: boolean;
|
||||||
};
|
};
|
||||||
} & Omit<TProjectPermission, "projectId">;
|
} & Omit<TProjectPermission, "projectId">;
|
||||||
|
@@ -29,19 +29,22 @@ export const kmsServiceFactory = ({ kmsDAL, kmsRootConfigDAL, keyStore }: TKmsSe
|
|||||||
let ROOT_ENCRYPTION_KEY = Buffer.alloc(0);
|
let ROOT_ENCRYPTION_KEY = Buffer.alloc(0);
|
||||||
|
|
||||||
// this is used symmetric encryption
|
// this is used symmetric encryption
|
||||||
const generateKmsKey = async ({ scopeId, scopeType, isReserved = true }: TGenerateKMSDTO) => {
|
const generateKmsKey = async ({ scopeId, scopeType, isReserved = true, tx }: TGenerateKMSDTO) => {
|
||||||
const cipher = symmetricCipherService(SymmetricEncryption.AES_GCM_256);
|
const cipher = symmetricCipherService(SymmetricEncryption.AES_GCM_256);
|
||||||
const kmsKeyMaterial = randomSecureBytes(32);
|
const kmsKeyMaterial = randomSecureBytes(32);
|
||||||
const encryptedKeyMaterial = cipher.encrypt(kmsKeyMaterial, ROOT_ENCRYPTION_KEY);
|
const encryptedKeyMaterial = cipher.encrypt(kmsKeyMaterial, ROOT_ENCRYPTION_KEY);
|
||||||
|
|
||||||
const { encryptedKey, ...doc } = await kmsDAL.create({
|
const { encryptedKey, ...doc } = await kmsDAL.create(
|
||||||
version: 1,
|
{
|
||||||
encryptedKey: encryptedKeyMaterial,
|
version: 1,
|
||||||
encryptionAlgorithm: SymmetricEncryption.AES_GCM_256,
|
encryptedKey: encryptedKeyMaterial,
|
||||||
isReserved,
|
encryptionAlgorithm: SymmetricEncryption.AES_GCM_256,
|
||||||
orgId: scopeType === "org" ? scopeId : undefined,
|
isReserved,
|
||||||
projectId: scopeType === "project" ? scopeId : undefined
|
orgId: scopeType === "org" ? scopeId : undefined,
|
||||||
});
|
projectId: scopeType === "project" ? scopeId : undefined
|
||||||
|
},
|
||||||
|
tx
|
||||||
|
);
|
||||||
return doc;
|
return doc;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@@ -1,7 +1,10 @@
|
|||||||
|
import { Knex } from "knex";
|
||||||
|
|
||||||
export type TGenerateKMSDTO = {
|
export type TGenerateKMSDTO = {
|
||||||
scopeType: "project" | "org";
|
scopeType: "project" | "org";
|
||||||
scopeId: string;
|
scopeId: string;
|
||||||
isReserved?: boolean;
|
isReserved?: boolean;
|
||||||
|
tx?: Knex;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type TEncryptWithKmsDTO = {
|
export type TEncryptWithKmsDTO = {
|
||||||
|
@@ -336,6 +336,7 @@ export const orgServiceFactory = ({
|
|||||||
return org;
|
return org;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
await licenseService.updateSubscriptionOrgMemberCount(organization.id);
|
||||||
return organization;
|
return organization;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@@ -36,6 +36,7 @@ import {
|
|||||||
TDeleteProjectMembershipsDTO,
|
TDeleteProjectMembershipsDTO,
|
||||||
TGetProjectMembershipByUsernameDTO,
|
TGetProjectMembershipByUsernameDTO,
|
||||||
TGetProjectMembershipDTO,
|
TGetProjectMembershipDTO,
|
||||||
|
TLeaveProjectDTO,
|
||||||
TUpdateProjectMembershipDTO
|
TUpdateProjectMembershipDTO
|
||||||
} from "./project-membership-types";
|
} from "./project-membership-types";
|
||||||
import { TProjectUserMembershipRoleDALFactory } from "./project-user-membership-role-dal";
|
import { TProjectUserMembershipRoleDALFactory } from "./project-user-membership-role-dal";
|
||||||
@@ -531,6 +532,53 @@ export const projectMembershipServiceFactory = ({
|
|||||||
return memberships;
|
return memberships;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const leaveProject = async ({ projectId, actorId, actor }: TLeaveProjectDTO) => {
|
||||||
|
if (actor !== ActorType.USER) {
|
||||||
|
throw new BadRequestError({ message: "Only users can leave projects" });
|
||||||
|
}
|
||||||
|
|
||||||
|
const project = await projectDAL.findById(projectId);
|
||||||
|
if (!project) throw new BadRequestError({ message: "Project not found" });
|
||||||
|
|
||||||
|
if (project.version !== ProjectVersion.V2) {
|
||||||
|
throw new BadRequestError({
|
||||||
|
message: "Please ask your project administrator to upgrade the project before leaving."
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const projectMembers = await projectMembershipDAL.findAllProjectMembers(projectId);
|
||||||
|
|
||||||
|
if (!projectMembers?.length) {
|
||||||
|
throw new BadRequestError({ message: "Failed to find project members" });
|
||||||
|
}
|
||||||
|
|
||||||
|
if (projectMembers.length < 2) {
|
||||||
|
throw new BadRequestError({ message: "You cannot leave the project as you are the only member" });
|
||||||
|
}
|
||||||
|
|
||||||
|
const adminMembers = projectMembers.filter(
|
||||||
|
(member) => member.roles.map((r) => r.role).includes("admin") && member.userId !== actorId
|
||||||
|
);
|
||||||
|
if (!adminMembers.length) {
|
||||||
|
throw new BadRequestError({
|
||||||
|
message: "You cannot leave the project as you are the only admin. Promote another user to admin before leaving."
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const deletedMembership = (
|
||||||
|
await projectMembershipDAL.delete({
|
||||||
|
projectId: project.id,
|
||||||
|
userId: actorId
|
||||||
|
})
|
||||||
|
)?.[0];
|
||||||
|
|
||||||
|
if (!deletedMembership) {
|
||||||
|
throw new BadRequestError({ message: "Failed to leave project" });
|
||||||
|
}
|
||||||
|
|
||||||
|
return deletedMembership;
|
||||||
|
};
|
||||||
|
|
||||||
return {
|
return {
|
||||||
getProjectMemberships,
|
getProjectMemberships,
|
||||||
getProjectMembershipByUsername,
|
getProjectMembershipByUsername,
|
||||||
@@ -538,6 +586,7 @@ export const projectMembershipServiceFactory = ({
|
|||||||
addUsersToProjectNonE2EE,
|
addUsersToProjectNonE2EE,
|
||||||
deleteProjectMemberships,
|
deleteProjectMemberships,
|
||||||
deleteProjectMembership, // TODO: Remove this
|
deleteProjectMembership, // TODO: Remove this
|
||||||
addUsersToProject
|
addUsersToProject,
|
||||||
|
leaveProject
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
@@ -1,6 +1,7 @@
|
|||||||
import { TProjectPermission } from "@app/lib/types";
|
import { TProjectPermission } from "@app/lib/types";
|
||||||
|
|
||||||
export type TGetProjectMembershipDTO = TProjectPermission;
|
export type TGetProjectMembershipDTO = TProjectPermission;
|
||||||
|
export type TLeaveProjectDTO = Omit<TProjectPermission, "actorOrgId" | "actorAuthMethod">;
|
||||||
export enum ProjectUserMembershipTemporaryMode {
|
export enum ProjectUserMembershipTemporaryMode {
|
||||||
Relative = "relative"
|
Relative = "relative"
|
||||||
}
|
}
|
||||||
|
@@ -1,6 +1,9 @@
|
|||||||
import crypto from "crypto";
|
import crypto from "crypto";
|
||||||
|
|
||||||
import { decryptAsymmetric, encryptAsymmetric } from "@app/lib/crypto";
|
import { decryptAsymmetric, encryptAsymmetric } from "@app/lib/crypto";
|
||||||
|
import { BadRequestError } from "@app/lib/errors";
|
||||||
|
import { TKmsServiceFactory } from "@app/services/kms/kms-service";
|
||||||
|
import { TProjectDALFactory } from "@app/services/project/project-dal";
|
||||||
|
|
||||||
import { AddUserToWsDTO } from "./project-types";
|
import { AddUserToWsDTO } from "./project-types";
|
||||||
|
|
||||||
@@ -49,3 +52,44 @@ export const createProjectKey = ({ publicKey, privateKey, plainProjectKey }: TCr
|
|||||||
|
|
||||||
return { key: encryptedProjectKey, iv: encryptedProjectKeyIv };
|
return { key: encryptedProjectKey, iv: encryptedProjectKeyIv };
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const getProjectKmsCertificateKeyId = async ({
|
||||||
|
projectId,
|
||||||
|
projectDAL,
|
||||||
|
kmsService
|
||||||
|
}: {
|
||||||
|
projectId: string;
|
||||||
|
projectDAL: Pick<TProjectDALFactory, "findOne" | "updateById" | "transaction">;
|
||||||
|
kmsService: Pick<TKmsServiceFactory, "generateKmsKey">;
|
||||||
|
}) => {
|
||||||
|
const keyId = await projectDAL.transaction(async (tx) => {
|
||||||
|
const project = await projectDAL.findOne({ id: projectId }, tx);
|
||||||
|
if (!project) {
|
||||||
|
throw new BadRequestError({ message: "Project not found" });
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!project.kmsCertificateKeyId) {
|
||||||
|
// create default kms key for certificate service
|
||||||
|
const key = await kmsService.generateKmsKey({
|
||||||
|
scopeId: projectId,
|
||||||
|
scopeType: "project",
|
||||||
|
isReserved: true,
|
||||||
|
tx
|
||||||
|
});
|
||||||
|
|
||||||
|
await projectDAL.updateById(
|
||||||
|
projectId,
|
||||||
|
{
|
||||||
|
kmsCertificateKeyId: key.id
|
||||||
|
},
|
||||||
|
tx
|
||||||
|
);
|
||||||
|
|
||||||
|
return key.id;
|
||||||
|
}
|
||||||
|
|
||||||
|
return project.kmsCertificateKeyId;
|
||||||
|
});
|
||||||
|
|
||||||
|
return keyId;
|
||||||
|
};
|
||||||
|
@@ -16,6 +16,8 @@ import { alphaNumericNanoId } from "@app/lib/nanoid";
|
|||||||
import { TProjectPermission } from "@app/lib/types";
|
import { TProjectPermission } from "@app/lib/types";
|
||||||
|
|
||||||
import { ActorType } from "../auth/auth-type";
|
import { ActorType } from "../auth/auth-type";
|
||||||
|
import { TCertificateDALFactory } from "../certificate/certificate-dal";
|
||||||
|
import { TCertificateAuthorityDALFactory } from "../certificate-authority/certificate-authority-dal";
|
||||||
import { TIdentityOrgDALFactory } from "../identity/identity-org-dal";
|
import { TIdentityOrgDALFactory } from "../identity/identity-org-dal";
|
||||||
import { TIdentityProjectDALFactory } from "../identity-project/identity-project-dal";
|
import { TIdentityProjectDALFactory } from "../identity-project/identity-project-dal";
|
||||||
import { TIdentityProjectMembershipRoleDALFactory } from "../identity-project/identity-project-membership-role-dal";
|
import { TIdentityProjectMembershipRoleDALFactory } from "../identity-project/identity-project-membership-role-dal";
|
||||||
@@ -36,9 +38,12 @@ import {
|
|||||||
TCreateProjectDTO,
|
TCreateProjectDTO,
|
||||||
TDeleteProjectDTO,
|
TDeleteProjectDTO,
|
||||||
TGetProjectDTO,
|
TGetProjectDTO,
|
||||||
|
TListProjectCasDTO,
|
||||||
|
TListProjectCertsDTO,
|
||||||
TToggleProjectAutoCapitalizationDTO,
|
TToggleProjectAutoCapitalizationDTO,
|
||||||
TUpdateProjectDTO,
|
TUpdateProjectDTO,
|
||||||
TUpdateProjectNameDTO,
|
TUpdateProjectNameDTO,
|
||||||
|
TUpdateProjectVersionLimitDTO,
|
||||||
TUpgradeProjectDTO
|
TUpgradeProjectDTO
|
||||||
} from "./project-types";
|
} from "./project-types";
|
||||||
|
|
||||||
@@ -49,6 +54,7 @@ export const DEFAULT_PROJECT_ENVS = [
|
|||||||
];
|
];
|
||||||
|
|
||||||
type TProjectServiceFactoryDep = {
|
type TProjectServiceFactoryDep = {
|
||||||
|
// TODO: Pick
|
||||||
projectDAL: TProjectDALFactory;
|
projectDAL: TProjectDALFactory;
|
||||||
projectQueue: TProjectQueueFactory;
|
projectQueue: TProjectQueueFactory;
|
||||||
userDAL: TUserDALFactory;
|
userDAL: TUserDALFactory;
|
||||||
@@ -62,6 +68,8 @@ type TProjectServiceFactoryDep = {
|
|||||||
projectMembershipDAL: Pick<TProjectMembershipDALFactory, "create" | "findProjectGhostUser" | "findOne">;
|
projectMembershipDAL: Pick<TProjectMembershipDALFactory, "create" | "findProjectGhostUser" | "findOne">;
|
||||||
projectUserMembershipRoleDAL: Pick<TProjectUserMembershipRoleDALFactory, "create">;
|
projectUserMembershipRoleDAL: Pick<TProjectUserMembershipRoleDALFactory, "create">;
|
||||||
secretBlindIndexDAL: Pick<TSecretBlindIndexDALFactory, "create">;
|
secretBlindIndexDAL: Pick<TSecretBlindIndexDALFactory, "create">;
|
||||||
|
certificateAuthorityDAL: Pick<TCertificateAuthorityDALFactory, "find">;
|
||||||
|
certificateDAL: Pick<TCertificateDALFactory, "find" | "countCertificatesInProject">;
|
||||||
permissionService: TPermissionServiceFactory;
|
permissionService: TPermissionServiceFactory;
|
||||||
orgService: Pick<TOrgServiceFactory, "addGhostUser">;
|
orgService: Pick<TOrgServiceFactory, "addGhostUser">;
|
||||||
licenseService: Pick<TLicenseServiceFactory, "getPlan">;
|
licenseService: Pick<TLicenseServiceFactory, "getPlan">;
|
||||||
@@ -89,6 +97,8 @@ export const projectServiceFactory = ({
|
|||||||
licenseService,
|
licenseService,
|
||||||
projectUserMembershipRoleDAL,
|
projectUserMembershipRoleDAL,
|
||||||
identityProjectMembershipRoleDAL,
|
identityProjectMembershipRoleDAL,
|
||||||
|
certificateAuthorityDAL,
|
||||||
|
certificateDAL,
|
||||||
keyStore
|
keyStore
|
||||||
}: TProjectServiceFactoryDep) => {
|
}: TProjectServiceFactoryDep) => {
|
||||||
/*
|
/*
|
||||||
@@ -133,7 +143,8 @@ export const projectServiceFactory = ({
|
|||||||
name: workspaceName,
|
name: workspaceName,
|
||||||
orgId: organization.id,
|
orgId: organization.id,
|
||||||
slug: projectSlug || slugify(`${workspaceName}-${alphaNumericNanoId(4)}`),
|
slug: projectSlug || slugify(`${workspaceName}-${alphaNumericNanoId(4)}`),
|
||||||
version: ProjectVersion.V2
|
version: ProjectVersion.V2,
|
||||||
|
pitVersionLimit: 10
|
||||||
},
|
},
|
||||||
tx
|
tx
|
||||||
);
|
);
|
||||||
@@ -406,6 +417,35 @@ export const projectServiceFactory = ({
|
|||||||
return updatedProject;
|
return updatedProject;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const updateVersionLimit = async ({
|
||||||
|
actor,
|
||||||
|
actorId,
|
||||||
|
actorOrgId,
|
||||||
|
actorAuthMethod,
|
||||||
|
pitVersionLimit,
|
||||||
|
workspaceSlug
|
||||||
|
}: TUpdateProjectVersionLimitDTO) => {
|
||||||
|
const project = await projectDAL.findProjectBySlug(workspaceSlug, actorOrgId);
|
||||||
|
if (!project) {
|
||||||
|
throw new BadRequestError({
|
||||||
|
message: "Project not found"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const { hasRole } = await permissionService.getProjectPermission(
|
||||||
|
actor,
|
||||||
|
actorId,
|
||||||
|
project.id,
|
||||||
|
actorAuthMethod,
|
||||||
|
actorOrgId
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!hasRole(ProjectMembershipRole.Admin))
|
||||||
|
throw new BadRequestError({ message: "Only admins are allowed to take this action" });
|
||||||
|
|
||||||
|
return projectDAL.updateById(project.id, { pitVersionLimit });
|
||||||
|
};
|
||||||
|
|
||||||
const updateName = async ({
|
const updateName = async ({
|
||||||
projectId,
|
projectId,
|
||||||
actor,
|
actor,
|
||||||
@@ -492,6 +532,83 @@ export const projectServiceFactory = ({
|
|||||||
return project.upgradeStatus || null;
|
return project.upgradeStatus || null;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return list of CAs for project
|
||||||
|
*/
|
||||||
|
const listProjectCas = async ({
|
||||||
|
status,
|
||||||
|
actorId,
|
||||||
|
actorOrgId,
|
||||||
|
actorAuthMethod,
|
||||||
|
filter,
|
||||||
|
actor
|
||||||
|
}: TListProjectCasDTO) => {
|
||||||
|
const project = await projectDAL.findProjectByFilter(filter);
|
||||||
|
|
||||||
|
const { permission } = await permissionService.getProjectPermission(
|
||||||
|
actor,
|
||||||
|
actorId,
|
||||||
|
project.id,
|
||||||
|
actorAuthMethod,
|
||||||
|
actorOrgId
|
||||||
|
);
|
||||||
|
|
||||||
|
ForbiddenError.from(permission).throwUnlessCan(
|
||||||
|
ProjectPermissionActions.Read,
|
||||||
|
ProjectPermissionSub.CertificateAuthorities
|
||||||
|
);
|
||||||
|
|
||||||
|
const cas = await certificateAuthorityDAL.find({
|
||||||
|
projectId: project.id,
|
||||||
|
...(status && { status })
|
||||||
|
});
|
||||||
|
|
||||||
|
return cas;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return list of certificates for project
|
||||||
|
*/
|
||||||
|
const listProjectCertificates = async ({
|
||||||
|
offset,
|
||||||
|
limit,
|
||||||
|
actorId,
|
||||||
|
actorOrgId,
|
||||||
|
actorAuthMethod,
|
||||||
|
filter,
|
||||||
|
actor
|
||||||
|
}: TListProjectCertsDTO) => {
|
||||||
|
const project = await projectDAL.findProjectByFilter(filter);
|
||||||
|
|
||||||
|
const { permission } = await permissionService.getProjectPermission(
|
||||||
|
actor,
|
||||||
|
actorId,
|
||||||
|
project.id,
|
||||||
|
actorAuthMethod,
|
||||||
|
actorOrgId
|
||||||
|
);
|
||||||
|
|
||||||
|
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.Certificates);
|
||||||
|
|
||||||
|
const cas = await certificateAuthorityDAL.find({ projectId: project.id });
|
||||||
|
|
||||||
|
const certificates = await certificateDAL.find(
|
||||||
|
{
|
||||||
|
$in: {
|
||||||
|
caId: cas.map((ca) => ca.id)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{ offset, limit, sort: [["updatedAt", "desc"]] }
|
||||||
|
);
|
||||||
|
|
||||||
|
const count = await certificateDAL.countCertificatesInProject(project.id);
|
||||||
|
|
||||||
|
return {
|
||||||
|
certificates,
|
||||||
|
totalCount: count
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
return {
|
return {
|
||||||
createProject,
|
createProject,
|
||||||
deleteProject,
|
deleteProject,
|
||||||
@@ -501,6 +618,9 @@ export const projectServiceFactory = ({
|
|||||||
getAProject,
|
getAProject,
|
||||||
toggleAutoCapitalization,
|
toggleAutoCapitalization,
|
||||||
updateName,
|
updateName,
|
||||||
upgradeProject
|
upgradeProject,
|
||||||
|
listProjectCas,
|
||||||
|
listProjectCertificates,
|
||||||
|
updateVersionLimit
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
@@ -2,6 +2,7 @@ import { ProjectMembershipRole, TProjectKeys } from "@app/db/schemas";
|
|||||||
import { TProjectPermission } from "@app/lib/types";
|
import { TProjectPermission } from "@app/lib/types";
|
||||||
|
|
||||||
import { ActorAuthMethod, ActorType } from "../auth/auth-type";
|
import { ActorAuthMethod, ActorType } from "../auth/auth-type";
|
||||||
|
import { CaStatus } from "../certificate-authority/certificate-authority-types";
|
||||||
|
|
||||||
export enum ProjectFilterType {
|
export enum ProjectFilterType {
|
||||||
ID = "id",
|
ID = "id",
|
||||||
@@ -43,6 +44,11 @@ export type TToggleProjectAutoCapitalizationDTO = {
|
|||||||
autoCapitalization: boolean;
|
autoCapitalization: boolean;
|
||||||
} & TProjectPermission;
|
} & TProjectPermission;
|
||||||
|
|
||||||
|
export type TUpdateProjectVersionLimitDTO = {
|
||||||
|
pitVersionLimit: number;
|
||||||
|
workspaceSlug: string;
|
||||||
|
} & Omit<TProjectPermission, "projectId">;
|
||||||
|
|
||||||
export type TUpdateProjectNameDTO = {
|
export type TUpdateProjectNameDTO = {
|
||||||
name: string;
|
name: string;
|
||||||
} & TProjectPermission;
|
} & TProjectPermission;
|
||||||
@@ -75,3 +81,14 @@ export type AddUserToWsDTO = {
|
|||||||
userPublicKey: string;
|
userPublicKey: string;
|
||||||
}[];
|
}[];
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type TListProjectCasDTO = {
|
||||||
|
status?: CaStatus;
|
||||||
|
filter: Filter;
|
||||||
|
} & Omit<TProjectPermission, "projectId">;
|
||||||
|
|
||||||
|
export type TListProjectCertsDTO = {
|
||||||
|
filter: Filter;
|
||||||
|
offset: number;
|
||||||
|
limit: number;
|
||||||
|
} & Omit<TProjectPermission, "projectId">;
|
||||||
|
@@ -1,13 +1,19 @@
|
|||||||
import { TAuditLogDALFactory } from "@app/ee/services/audit-log/audit-log-dal";
|
import { TAuditLogDALFactory } from "@app/ee/services/audit-log/audit-log-dal";
|
||||||
|
import { TSnapshotDALFactory } from "@app/ee/services/secret-snapshot/snapshot-dal";
|
||||||
import { logger } from "@app/lib/logger";
|
import { logger } from "@app/lib/logger";
|
||||||
import { QueueJobs, QueueName, TQueueServiceFactory } from "@app/queue";
|
import { QueueJobs, QueueName, TQueueServiceFactory } from "@app/queue";
|
||||||
|
|
||||||
import { TIdentityAccessTokenDALFactory } from "../identity-access-token/identity-access-token-dal";
|
import { TIdentityAccessTokenDALFactory } from "../identity-access-token/identity-access-token-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";
|
import { TSecretSharingDALFactory } from "../secret-sharing/secret-sharing-dal";
|
||||||
|
|
||||||
type TDailyResourceCleanUpQueueServiceFactoryDep = {
|
type TDailyResourceCleanUpQueueServiceFactoryDep = {
|
||||||
auditLogDAL: Pick<TAuditLogDALFactory, "pruneAuditLog">;
|
auditLogDAL: Pick<TAuditLogDALFactory, "pruneAuditLog">;
|
||||||
identityAccessTokenDAL: Pick<TIdentityAccessTokenDALFactory, "removeExpiredTokens">;
|
identityAccessTokenDAL: Pick<TIdentityAccessTokenDALFactory, "removeExpiredTokens">;
|
||||||
|
secretVersionDAL: Pick<TSecretVersionDALFactory, "pruneExcessVersions">;
|
||||||
|
secretFolderVersionDAL: Pick<TSecretFolderVersionDALFactory, "pruneExcessVersions">;
|
||||||
|
snapshotDAL: Pick<TSnapshotDALFactory, "pruneExcessSnapshots">;
|
||||||
secretSharingDAL: Pick<TSecretSharingDALFactory, "pruneExpiredSharedSecrets">;
|
secretSharingDAL: Pick<TSecretSharingDALFactory, "pruneExpiredSharedSecrets">;
|
||||||
queueService: TQueueServiceFactory;
|
queueService: TQueueServiceFactory;
|
||||||
};
|
};
|
||||||
@@ -17,6 +23,9 @@ export type TDailyResourceCleanUpQueueServiceFactory = ReturnType<typeof dailyRe
|
|||||||
export const dailyResourceCleanUpQueueServiceFactory = ({
|
export const dailyResourceCleanUpQueueServiceFactory = ({
|
||||||
auditLogDAL,
|
auditLogDAL,
|
||||||
queueService,
|
queueService,
|
||||||
|
snapshotDAL,
|
||||||
|
secretVersionDAL,
|
||||||
|
secretFolderVersionDAL,
|
||||||
identityAccessTokenDAL,
|
identityAccessTokenDAL,
|
||||||
secretSharingDAL
|
secretSharingDAL
|
||||||
}: TDailyResourceCleanUpQueueServiceFactoryDep) => {
|
}: TDailyResourceCleanUpQueueServiceFactoryDep) => {
|
||||||
@@ -25,6 +34,9 @@ export const dailyResourceCleanUpQueueServiceFactory = ({
|
|||||||
await auditLogDAL.pruneAuditLog();
|
await auditLogDAL.pruneAuditLog();
|
||||||
await identityAccessTokenDAL.removeExpiredTokens();
|
await identityAccessTokenDAL.removeExpiredTokens();
|
||||||
await secretSharingDAL.pruneExpiredSharedSecrets();
|
await secretSharingDAL.pruneExpiredSharedSecrets();
|
||||||
|
await snapshotDAL.pruneExcessSnapshots();
|
||||||
|
await secretVersionDAL.pruneExcessVersions();
|
||||||
|
await secretFolderVersionDAL.pruneExcessVersions();
|
||||||
logger.info(`${QueueName.DailyResourceCleanUp}: queue task completed`);
|
logger.info(`${QueueName.DailyResourceCleanUp}: queue task completed`);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@@ -62,5 +62,32 @@ export const secretFolderVersionDALFactory = (db: TDbClient) => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
return { ...secretFolderVerOrm, findLatestFolderVersions, findLatestVersionByFolderId };
|
const pruneExcessVersions = async () => {
|
||||||
|
try {
|
||||||
|
await db(TableName.SecretFolderVersion)
|
||||||
|
.with("folder_cte", (qb) => {
|
||||||
|
void qb
|
||||||
|
.from(TableName.SecretFolderVersion)
|
||||||
|
.select(
|
||||||
|
"id",
|
||||||
|
"folderId",
|
||||||
|
db.raw(
|
||||||
|
`ROW_NUMBER() OVER (PARTITION BY ${TableName.SecretFolderVersion}."folderId" ORDER BY ${TableName.SecretFolderVersion}."createdAt" DESC) AS row_num`
|
||||||
|
)
|
||||||
|
);
|
||||||
|
})
|
||||||
|
.join(TableName.Environment, `${TableName.Environment}.id`, `${TableName.SecretFolderVersion}.envId`)
|
||||||
|
.join(TableName.Project, `${TableName.Project}.id`, `${TableName.Environment}.projectId`)
|
||||||
|
.join("folder_cte", "folder_cte.id", `${TableName.SecretFolderVersion}.id`)
|
||||||
|
.whereRaw(`folder_cte.row_num > ${TableName.Project}."pitVersionLimit"`)
|
||||||
|
.delete();
|
||||||
|
} catch (error) {
|
||||||
|
throw new DatabaseError({
|
||||||
|
error,
|
||||||
|
name: "Secret Folder Version Prune"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return { ...secretFolderVerOrm, findLatestFolderVersions, findLatestVersionByFolderId, pruneExcessVersions };
|
||||||
};
|
};
|
||||||
|
@@ -1,8 +1,13 @@
|
|||||||
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service";
|
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service";
|
||||||
import { UnauthorizedError } from "@app/lib/errors";
|
import { BadRequestError, UnauthorizedError } from "@app/lib/errors";
|
||||||
|
|
||||||
import { TSecretSharingDALFactory } from "./secret-sharing-dal";
|
import { TSecretSharingDALFactory } from "./secret-sharing-dal";
|
||||||
import { TCreateSharedSecretDTO, TDeleteSharedSecretDTO, TSharedSecretPermission } from "./secret-sharing-types";
|
import {
|
||||||
|
TCreatePublicSharedSecretDTO,
|
||||||
|
TCreateSharedSecretDTO,
|
||||||
|
TDeleteSharedSecretDTO,
|
||||||
|
TSharedSecretPermission
|
||||||
|
} from "./secret-sharing-types";
|
||||||
|
|
||||||
type TSecretSharingServiceFactoryDep = {
|
type TSecretSharingServiceFactoryDep = {
|
||||||
permissionService: Pick<TPermissionServiceFactory, "getOrgPermission">;
|
permissionService: Pick<TPermissionServiceFactory, "getOrgPermission">;
|
||||||
@@ -31,6 +36,24 @@ export const secretSharingServiceFactory = ({
|
|||||||
} = createSharedSecretInput;
|
} = createSharedSecretInput;
|
||||||
const { permission } = await permissionService.getOrgPermission(actor, actorId, orgId, actorAuthMethod, actorOrgId);
|
const { permission } = await permissionService.getOrgPermission(actor, actorId, orgId, actorAuthMethod, actorOrgId);
|
||||||
if (!permission) throw new UnauthorizedError({ name: "User not in org" });
|
if (!permission) throw new UnauthorizedError({ name: "User not in org" });
|
||||||
|
|
||||||
|
if (new Date(expiresAt) < new Date()) {
|
||||||
|
throw new BadRequestError({ message: "Expiration date cannot be in the past" });
|
||||||
|
}
|
||||||
|
|
||||||
|
// Limit Expiry Time to 1 month
|
||||||
|
const expiryTime = new Date(expiresAt).getTime();
|
||||||
|
const currentTime = new Date().getTime();
|
||||||
|
const thirtyDays = 30 * 24 * 60 * 60 * 1000;
|
||||||
|
if (expiryTime - currentTime > thirtyDays) {
|
||||||
|
throw new BadRequestError({ message: "Expiration date cannot be more than 30 days" });
|
||||||
|
}
|
||||||
|
|
||||||
|
// Limit Input ciphertext length to 13000 (equivalent to 10,000 characters of Plaintext)
|
||||||
|
if (encryptedValue.length > 13000) {
|
||||||
|
throw new BadRequestError({ message: "Shared secret value too long" });
|
||||||
|
}
|
||||||
|
|
||||||
const newSharedSecret = await secretSharingDAL.create({
|
const newSharedSecret = await secretSharingDAL.create({
|
||||||
encryptedValue,
|
encryptedValue,
|
||||||
iv,
|
iv,
|
||||||
@@ -44,6 +67,36 @@ export const secretSharingServiceFactory = ({
|
|||||||
return { id: newSharedSecret.id };
|
return { id: newSharedSecret.id };
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const createPublicSharedSecret = async (createSharedSecretInput: TCreatePublicSharedSecretDTO) => {
|
||||||
|
const { encryptedValue, iv, tag, hashedHex, expiresAt, expiresAfterViews } = createSharedSecretInput;
|
||||||
|
if (new Date(expiresAt) < new Date()) {
|
||||||
|
throw new BadRequestError({ message: "Expiration date cannot be in the past" });
|
||||||
|
}
|
||||||
|
|
||||||
|
// Limit Expiry Time to 1 month
|
||||||
|
const expiryTime = new Date(expiresAt).getTime();
|
||||||
|
const currentTime = new Date().getTime();
|
||||||
|
const thirtyDays = 30 * 24 * 60 * 60 * 1000;
|
||||||
|
if (expiryTime - currentTime > thirtyDays) {
|
||||||
|
throw new BadRequestError({ message: "Expiration date cannot exceed more than 30 days" });
|
||||||
|
}
|
||||||
|
|
||||||
|
// Limit Input ciphertext length to 13000 (equivalent to 10,000 characters of Plaintext)
|
||||||
|
if (encryptedValue.length > 13000) {
|
||||||
|
throw new BadRequestError({ message: "Shared secret value too long" });
|
||||||
|
}
|
||||||
|
|
||||||
|
const newSharedSecret = await secretSharingDAL.create({
|
||||||
|
encryptedValue,
|
||||||
|
iv,
|
||||||
|
tag,
|
||||||
|
hashedHex,
|
||||||
|
expiresAt,
|
||||||
|
expiresAfterViews
|
||||||
|
});
|
||||||
|
return { id: newSharedSecret.id };
|
||||||
|
};
|
||||||
|
|
||||||
const getSharedSecrets = async (getSharedSecretsInput: TSharedSecretPermission) => {
|
const getSharedSecrets = async (getSharedSecretsInput: TSharedSecretPermission) => {
|
||||||
const { actor, actorId, orgId, actorAuthMethod, actorOrgId } = getSharedSecretsInput;
|
const { actor, actorId, orgId, actorAuthMethod, actorOrgId } = getSharedSecretsInput;
|
||||||
const { permission } = await permissionService.getOrgPermission(actor, actorId, orgId, actorAuthMethod, actorOrgId);
|
const { permission } = await permissionService.getOrgPermission(actor, actorId, orgId, actorAuthMethod, actorOrgId);
|
||||||
@@ -54,6 +107,7 @@ export const secretSharingServiceFactory = ({
|
|||||||
|
|
||||||
const getActiveSharedSecretByIdAndHashedHex = async (sharedSecretId: string, hashedHex: string) => {
|
const getActiveSharedSecretByIdAndHashedHex = async (sharedSecretId: string, hashedHex: string) => {
|
||||||
const sharedSecret = await secretSharingDAL.findOne({ id: sharedSecretId, hashedHex });
|
const sharedSecret = await secretSharingDAL.findOne({ id: sharedSecretId, hashedHex });
|
||||||
|
if (!sharedSecret) return;
|
||||||
if (sharedSecret.expiresAt && sharedSecret.expiresAt < new Date()) {
|
if (sharedSecret.expiresAt && sharedSecret.expiresAt < new Date()) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -77,6 +131,7 @@ export const secretSharingServiceFactory = ({
|
|||||||
|
|
||||||
return {
|
return {
|
||||||
createSharedSecret,
|
createSharedSecret,
|
||||||
|
createPublicSharedSecret,
|
||||||
getSharedSecrets,
|
getSharedSecrets,
|
||||||
deleteSharedSecretById,
|
deleteSharedSecretById,
|
||||||
getActiveSharedSecretByIdAndHashedHex
|
getActiveSharedSecretByIdAndHashedHex
|
||||||
|
@@ -8,14 +8,16 @@ export type TSharedSecretPermission = {
|
|||||||
orgId: string;
|
orgId: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type TCreateSharedSecretDTO = {
|
export type TCreatePublicSharedSecretDTO = {
|
||||||
encryptedValue: string;
|
encryptedValue: string;
|
||||||
iv: string;
|
iv: string;
|
||||||
tag: string;
|
tag: string;
|
||||||
hashedHex: string;
|
hashedHex: string;
|
||||||
expiresAt: Date;
|
expiresAt: Date;
|
||||||
expiresAfterViews: number;
|
expiresAfterViews: number;
|
||||||
} & TSharedSecretPermission;
|
};
|
||||||
|
|
||||||
|
export type TCreateSharedSecretDTO = TSharedSecretPermission & TCreatePublicSharedSecretDTO;
|
||||||
|
|
||||||
export type TDeleteSharedSecretDTO = {
|
export type TDeleteSharedSecretDTO = {
|
||||||
sharedSecretId: string;
|
sharedSecretId: string;
|
||||||
|
@@ -42,7 +42,8 @@ export const secretTagServiceFactory = ({ secretTagDAL, permissionService }: TSe
|
|||||||
name,
|
name,
|
||||||
slug,
|
slug,
|
||||||
color,
|
color,
|
||||||
createdBy: actorId
|
createdBy: actorId,
|
||||||
|
createdByActorType: actor
|
||||||
});
|
});
|
||||||
return newTag;
|
return newTag;
|
||||||
};
|
};
|
||||||
|
@@ -311,6 +311,40 @@ export const secretDALFactory = (db: TDbClient) => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const findOneWithTags = async (filter: Partial<TSecrets>, tx?: Knex) => {
|
||||||
|
try {
|
||||||
|
const rawDocs = await (tx || db)(TableName.Secret)
|
||||||
|
.where(filter)
|
||||||
|
.leftJoin(TableName.JnSecretTag, `${TableName.Secret}.id`, `${TableName.JnSecretTag}.${TableName.Secret}Id`)
|
||||||
|
.leftJoin(TableName.SecretTag, `${TableName.JnSecretTag}.${TableName.SecretTag}Id`, `${TableName.SecretTag}.id`)
|
||||||
|
.select(selectAllTableCols(TableName.Secret))
|
||||||
|
.select(db.ref("id").withSchema(TableName.SecretTag).as("tagId"))
|
||||||
|
.select(db.ref("color").withSchema(TableName.SecretTag).as("tagColor"))
|
||||||
|
.select(db.ref("slug").withSchema(TableName.SecretTag).as("tagSlug"))
|
||||||
|
.select(db.ref("name").withSchema(TableName.SecretTag).as("tagName"));
|
||||||
|
const docs = sqlNestRelationships({
|
||||||
|
data: rawDocs,
|
||||||
|
key: "id",
|
||||||
|
parentMapper: (el) => ({ _id: el.id, ...SecretsSchema.parse(el) }),
|
||||||
|
childrenMapper: [
|
||||||
|
{
|
||||||
|
key: "tagId",
|
||||||
|
label: "tags" as const,
|
||||||
|
mapper: ({ tagId: id, tagColor: color, tagSlug: slug, tagName: name }) => ({
|
||||||
|
id,
|
||||||
|
color,
|
||||||
|
slug,
|
||||||
|
name
|
||||||
|
})
|
||||||
|
}
|
||||||
|
]
|
||||||
|
});
|
||||||
|
return docs?.[0];
|
||||||
|
} catch (error) {
|
||||||
|
throw new DatabaseError({ error, name: "FindOneWIthTags" });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
return {
|
return {
|
||||||
...secretOrm,
|
...secretOrm,
|
||||||
update,
|
update,
|
||||||
@@ -318,6 +352,7 @@ export const secretDALFactory = (db: TDbClient) => {
|
|||||||
deleteMany,
|
deleteMany,
|
||||||
bulkUpdateNoVersionIncrement,
|
bulkUpdateNoVersionIncrement,
|
||||||
getSecretTags,
|
getSecretTags,
|
||||||
|
findOneWithTags,
|
||||||
findByFolderId,
|
findByFolderId,
|
||||||
findByFolderIds,
|
findByFolderIds,
|
||||||
findByBlindIndexes,
|
findByBlindIndexes,
|
||||||
|
@@ -356,7 +356,17 @@ export const interpolateSecrets = ({ projectId, secretEncKey, secretDAL, folderD
|
|||||||
};
|
};
|
||||||
|
|
||||||
export const decryptSecretRaw = (
|
export const decryptSecretRaw = (
|
||||||
secret: TSecrets & { workspace: string; environment: string; secretPath: string },
|
secret: TSecrets & {
|
||||||
|
workspace: string;
|
||||||
|
environment: string;
|
||||||
|
secretPath: string;
|
||||||
|
tags?: {
|
||||||
|
id: string;
|
||||||
|
slug: string;
|
||||||
|
color?: string | null;
|
||||||
|
name: string;
|
||||||
|
}[];
|
||||||
|
},
|
||||||
key: string
|
key: string
|
||||||
) => {
|
) => {
|
||||||
const secretKey = decryptSymmetric128BitHexKeyUTF8({
|
const secretKey = decryptSymmetric128BitHexKeyUTF8({
|
||||||
@@ -396,6 +406,7 @@ export const decryptSecretRaw = (
|
|||||||
_id: secret.id,
|
_id: secret.id,
|
||||||
id: secret.id,
|
id: secret.id,
|
||||||
user: secret.userId,
|
user: secret.userId,
|
||||||
|
tags: secret.tags,
|
||||||
skipMultilineEncoding: secret.skipMultilineEncoding
|
skipMultilineEncoding: secret.skipMultilineEncoding
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
@@ -1,4 +1,6 @@
|
|||||||
/* eslint-disable no-await-in-loop */
|
/* eslint-disable no-await-in-loop */
|
||||||
|
import { AxiosError } from "axios";
|
||||||
|
|
||||||
import { getConfig } from "@app/lib/config/env";
|
import { getConfig } from "@app/lib/config/env";
|
||||||
import { decryptSymmetric128BitHexKeyUTF8 } from "@app/lib/crypto";
|
import { decryptSymmetric128BitHexKeyUTF8 } from "@app/lib/crypto";
|
||||||
import { daysToMillisecond, secondsToMillis } from "@app/lib/dates";
|
import { daysToMillisecond, secondsToMillis } from "@app/lib/dates";
|
||||||
@@ -570,11 +572,14 @@ export const secretQueueFactory = ({
|
|||||||
isSynced: true
|
isSynced: true
|
||||||
});
|
});
|
||||||
} catch (err: unknown) {
|
} catch (err: unknown) {
|
||||||
logger.info("Secret integration sync error:", err);
|
logger.info("Secret integration sync error: %o", err);
|
||||||
|
const message =
|
||||||
|
err instanceof AxiosError ? JSON.stringify((err as AxiosError)?.response?.data) : (err as Error)?.message;
|
||||||
|
|
||||||
await integrationDAL.updateById(integration.id, {
|
await integrationDAL.updateById(integration.id, {
|
||||||
lastSyncJobId: job.id,
|
lastSyncJobId: job.id,
|
||||||
lastUsed: new Date(),
|
lastUsed: new Date(),
|
||||||
syncMessage: (err as Error)?.message,
|
syncMessage: message,
|
||||||
isSynced: false
|
isSynced: false
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user