mirror of
https://github.com/Infisical/infisical.git
synced 2025-07-05 04:29:09 +00:00
Compare commits
334 Commits
patch-mult
...
patch-ldap
Author | SHA1 | Date | |
---|---|---|---|
91634fbe76 | |||
4962a63888 | |||
9e9de9f527 | |||
6af4a06c02 | |||
fe6dc248b6 | |||
7d380f9b43 | |||
76c8410081 | |||
6df90fa825 | |||
c042bafba3 | |||
8067df821e | |||
1906896e56 | |||
a8ccfd9c92 | |||
32609b95a0 | |||
08d3436217 | |||
2ae45dc1cc | |||
44a898fb15 | |||
4d194052b5 | |||
1d622bb121 | |||
5c149c6ac6 | |||
c19f8839ff | |||
c6c71a04e8 | |||
d47c586a52 | |||
88156c8cd8 | |||
27d5d90d02 | |||
07ca1ed424 | |||
18c5dd3cbd | |||
467e3aab56 | |||
577b432861 | |||
dda6b1d233 | |||
e83f31249a | |||
3142d36ea1 | |||
9506b60d02 | |||
ed25b82113 | |||
83bd97fc70 | |||
1d5115972b | |||
d26521be0b | |||
473f8137fd | |||
719d0ea30f | |||
aaef339e21 | |||
e3beeb68eb | |||
d0c76ae4b4 | |||
a5cf6f40c7 | |||
f121f8e828 | |||
54c8da8ab6 | |||
6e0dfc72e4 | |||
b226fdac9d | |||
3c36d5dbd2 | |||
a5f895ad91 | |||
9f66b9bb4d | |||
80e55a9341 | |||
5142d6f3c1 | |||
c8677ac548 | |||
992cc03eca | |||
f0e7c459e2 | |||
29d0694a16 | |||
f13930bc6b | |||
0d5514834d | |||
b495156444 | |||
65a2b0116b | |||
8ef2501407 | |||
21c6160c84 | |||
8a2268956a | |||
df3c58bc2a | |||
2675aa6969 | |||
6bad13738f | |||
dbae6968c9 | |||
e019f3811b | |||
db726128f1 | |||
24935f4e07 | |||
1835777832 | |||
cb237831c7 | |||
49d2ea6f2e | |||
3b2a2d1a73 | |||
f490fb6616 | |||
c4f9a3b31e | |||
afcf15df55 | |||
bf8aee25fe | |||
ebdfe31c17 | |||
e65ce932dd | |||
ae177343d5 | |||
0342ba0890 | |||
c119f506fd | |||
93638baba7 | |||
bad97774c4 | |||
68f5be2ff1 | |||
0b54099789 | |||
9b2a2eda0c | |||
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 | |||
ccef9646c6 | |||
458639e93d | |||
35998e98cf | |||
e19b67f9a2 | |||
f41ec46a35 | |||
33aa9ea1a7 | |||
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 | |||
21bd468307 | |||
e95109c446 | |||
76c468ecc7 | |||
dcf315a524 | |||
f8a4b6365c | |||
e27d273e8f | |||
30dc2d0fcb | |||
93d5180dfc | |||
a9bec84d27 | |||
e3f87382a3 | |||
736f067178 | |||
f3ea7b3dfd | |||
777dfd5f58 | |||
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
|
||||||
|
31
README.md
31
README.md
@ -48,25 +48,26 @@
|
|||||||
|
|
||||||
## Introduction
|
## Introduction
|
||||||
|
|
||||||
**[Infisical](https://infisical.com)** is the open source secret management platform that teams use to centralize their secrets like API keys, database credentials, and configurations.
|
**[Infisical](https://infisical.com)** is the open source secret management platform that teams use to centralize their application configuration and secrets like API keys and database credentials as well as manage their internal PKI.
|
||||||
|
|
||||||
We're on a mission to make secret management more accessible to everyone, not just security teams, and that means redesigning the entire developer experience from ground up.
|
We're on a mission to make security tooling more accessible to everyone, not just security teams, and that means redesigning the entire developer experience from ground up.
|
||||||
|
|
||||||
## Features
|
## Features
|
||||||
|
|
||||||
- **[User-friendly dashboard](https://infisical.com/docs/documentation/platform/project)** to manage secrets across projects and environments (e.g. development, production, etc.).
|
- **[User-friendly dashboard](https://infisical.com/docs/documentation/platform/project)** to manage secrets across projects and environments (e.g. development, production, etc.).
|
||||||
- **[Client SDKs](https://infisical.com/docs/sdks/overview)** to fetch secrets for your apps and infrastructure on demand.
|
- **[Client SDKs](https://infisical.com/docs/sdks/overview)** to fetch secrets for your apps and infrastructure on demand.
|
||||||
- **[Infisical CLI](https://infisical.com/docs/cli/overview)** to fetch and inject secrets into any framework in local development and CI/CD.
|
- **[Infisical CLI](https://infisical.com/docs/cli/overview)** to fetch and inject secrets into any framework in local development and CI/CD.
|
||||||
- **[Infisical API](https://infisical.com/docs/api-reference/overview/introduction)** to perform CRUD operation on secrets, users, projects, and any other resource in Infisical.
|
- **[Infisical API](https://infisical.com/docs/api-reference/overview/introduction)** to perform CRUD operation on secrets, users, projects, and any other resource in Infisical.
|
||||||
- **[Native integrations](https://infisical.com/docs/integrations/overview)** with platforms like [GitHub](https://infisical.com/docs/integrations/cicd/githubactions), [Vercel](https://infisical.com/docs/integrations/cloud/vercel), [AWS](https://infisical.com/docs/integrations/cloud/aws-secret-manager), and tools like [Terraform](https://infisical.com/docs/integrations/frameworks/terraform), [Ansible](https://infisical.com/docs/integrations/platforms/ansible), and more.
|
- **[Native integrations](https://infisical.com/docs/integrations/overview)** with platforms like [GitHub](https://infisical.com/docs/integrations/cicd/githubactions), [Vercel](https://infisical.com/docs/integrations/cloud/vercel), [AWS](https://infisical.com/docs/integrations/cloud/aws-secret-manager), and tools like [Terraform](https://infisical.com/docs/integrations/frameworks/terraform), [Ansible](https://infisical.com/docs/integrations/platforms/ansible), and more.
|
||||||
- **[Infisical Kubernetes operator](https://infisical.com/docs/documentation/getting-started/kubernetes)** to managed secrets in k8s, automatically reload deployments, and more.
|
- **[Infisical Kubernetes operator](https://infisical.com/docs/documentation/getting-started/kubernetes)** to managed secrets in k8s, automatically reload deployments, and more.
|
||||||
- **[Infisical Agent](https://infisical.com/docs/infisical-agent/overview)** to inject secrets into your applications without modifying any code logic.
|
- **[Infisical Agent](https://infisical.com/docs/infisical-agent/overview)** to inject secrets into your applications without modifying any code logic.
|
||||||
- **[Self-hosting and on-prem](https://infisical.com/docs/self-hosting/overview)** to get complete control over your data.
|
- **[Self-hosting and on-prem](https://infisical.com/docs/self-hosting/overview)** to get complete control over your data.
|
||||||
- **[Secret versioning](https://infisical.com/docs/documentation/platform/secret-versioning)** and **[Point-in-Time Recovery](https://infisical.com/docs/documentation/platform/pit-recovery)** to version every secret and project state.
|
- **[Secret versioning](https://infisical.com/docs/documentation/platform/secret-versioning)** and **[Point-in-Time Recovery](https://infisical.com/docs/documentation/platform/pit-recovery)** to version every secret and project state.
|
||||||
- **[Audit logs](https://infisical.com/docs/documentation/platform/audit-logs)** to record every action taken in a project.
|
- **[Audit logs](https://infisical.com/docs/documentation/platform/audit-logs)** to record every action taken in a project.
|
||||||
- **[Role-based Access Controls](https://infisical.com/docs/documentation/platform/role-based-access-controls)** to create permission sets on any resource in Infisica and assign those to user or machine identities.
|
- **[Role-based Access Controls](https://infisical.com/docs/documentation/platform/role-based-access-controls)** to create permission sets on any resource in Infisica and assign those to user or machine identities.
|
||||||
- **[Simple on-premise deployments](https://infisical.com/docs/self-hosting/overview)** to AWS, Digital Ocean, and more.
|
- **[Simple on-premise deployments](https://infisical.com/docs/self-hosting/overview)** to AWS, Digital Ocean, and more.
|
||||||
- **[Secret Scanning and Leak Prevention](https://infisical.com/docs/cli/scanning-overview)** to prevent secrets from leaking to git.
|
- **[Internal PKI](https://infisical.com/docs/documentation/platform/pki/private-ca)** to create Private CA hierarchies and start issuing and managing X.509 digital certificates.
|
||||||
|
- **[Secret Scanning and Leak Prevention](https://infisical.com/docs/cli/scanning-overview)** to prevent secrets from leaking to git.
|
||||||
|
|
||||||
And much more.
|
And much more.
|
||||||
|
|
||||||
@ -74,9 +75,9 @@ And much more.
|
|||||||
|
|
||||||
Check out the [Quickstart Guides](https://infisical.com/docs/getting-started/introduction)
|
Check out the [Quickstart Guides](https://infisical.com/docs/getting-started/introduction)
|
||||||
|
|
||||||
| Use Infisical Cloud | Deploy Infisical on premise |
|
| Use Infisical Cloud | Deploy Infisical on premise |
|
||||||
| ------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
| ------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------ |
|
||||||
| The fastest and most reliable way to <br> get started with Infisical is signing up <br> for free to [Infisical Cloud](https://app.infisical.com/login). | <br> View all [deployment options](https://infisical.com/docs/self-hosting/overview) |
|
| The fastest and most reliable way to <br> get started with Infisical is signing up <br> for free to [Infisical Cloud](https://app.infisical.com/login). | <br> View all [deployment options](https://infisical.com/docs/self-hosting/overview) |
|
||||||
|
|
||||||
### Run Infisical locally
|
### Run Infisical locally
|
||||||
|
|
||||||
|
230
backend/package-lock.json
generated
230
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",
|
||||||
@ -6295,12 +6459,12 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/braces": {
|
"node_modules/braces": {
|
||||||
"version": "3.0.2",
|
"version": "3.0.3",
|
||||||
"resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz",
|
||||||
"integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==",
|
"integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"fill-range": "^7.0.1"
|
"fill-range": "^7.1.1"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=8"
|
"node": ">=8"
|
||||||
@ -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",
|
||||||
@ -7942,9 +8115,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/fill-range": {
|
"node_modules/fill-range": {
|
||||||
"version": "7.0.1",
|
"version": "7.1.1",
|
||||||
"resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz",
|
||||||
"integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==",
|
"integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"to-regex-range": "^5.0.1"
|
"to-regex-range": "^5.0.1"
|
||||||
@ -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,61 @@
|
|||||||
|
import { Knex } from "knex";
|
||||||
|
|
||||||
|
import { TableName } from "../schemas";
|
||||||
|
|
||||||
|
export async function up(knex: Knex): Promise<void> {
|
||||||
|
const doesPasswordFieldExist = await knex.schema.hasColumn(TableName.UserEncryptionKey, "hashedPassword");
|
||||||
|
const doesPrivateKeyFieldExist = await knex.schema.hasColumn(
|
||||||
|
TableName.UserEncryptionKey,
|
||||||
|
"serverEncryptedPrivateKey"
|
||||||
|
);
|
||||||
|
const doesPrivateKeyIVFieldExist = await knex.schema.hasColumn(
|
||||||
|
TableName.UserEncryptionKey,
|
||||||
|
"serverEncryptedPrivateKeyIV"
|
||||||
|
);
|
||||||
|
const doesPrivateKeyTagFieldExist = await knex.schema.hasColumn(
|
||||||
|
TableName.UserEncryptionKey,
|
||||||
|
"serverEncryptedPrivateKeyTag"
|
||||||
|
);
|
||||||
|
const doesPrivateKeyEncodingFieldExist = await knex.schema.hasColumn(
|
||||||
|
TableName.UserEncryptionKey,
|
||||||
|
"serverEncryptedPrivateKeyEncoding"
|
||||||
|
);
|
||||||
|
if (await knex.schema.hasTable(TableName.UserEncryptionKey)) {
|
||||||
|
await knex.schema.alterTable(TableName.UserEncryptionKey, (t) => {
|
||||||
|
if (!doesPasswordFieldExist) t.string("hashedPassword");
|
||||||
|
if (!doesPrivateKeyFieldExist) t.text("serverEncryptedPrivateKey");
|
||||||
|
if (!doesPrivateKeyIVFieldExist) t.text("serverEncryptedPrivateKeyIV");
|
||||||
|
if (!doesPrivateKeyTagFieldExist) t.text("serverEncryptedPrivateKeyTag");
|
||||||
|
if (!doesPrivateKeyEncodingFieldExist) t.text("serverEncryptedPrivateKeyEncoding");
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function down(knex: Knex): Promise<void> {
|
||||||
|
const doesPasswordFieldExist = await knex.schema.hasColumn(TableName.UserEncryptionKey, "hashedPassword");
|
||||||
|
const doesPrivateKeyFieldExist = await knex.schema.hasColumn(
|
||||||
|
TableName.UserEncryptionKey,
|
||||||
|
"serverEncryptedPrivateKey"
|
||||||
|
);
|
||||||
|
const doesPrivateKeyIVFieldExist = await knex.schema.hasColumn(
|
||||||
|
TableName.UserEncryptionKey,
|
||||||
|
"serverEncryptedPrivateKeyIV"
|
||||||
|
);
|
||||||
|
const doesPrivateKeyTagFieldExist = await knex.schema.hasColumn(
|
||||||
|
TableName.UserEncryptionKey,
|
||||||
|
"serverEncryptedPrivateKeyTag"
|
||||||
|
);
|
||||||
|
const doesPrivateKeyEncodingFieldExist = await knex.schema.hasColumn(
|
||||||
|
TableName.UserEncryptionKey,
|
||||||
|
"serverEncryptedPrivateKeyEncoding"
|
||||||
|
);
|
||||||
|
if (await knex.schema.hasTable(TableName.UserEncryptionKey)) {
|
||||||
|
await knex.schema.alterTable(TableName.UserEncryptionKey, (t) => {
|
||||||
|
if (doesPasswordFieldExist) t.dropColumn("hashedPassword");
|
||||||
|
if (doesPrivateKeyFieldExist) t.dropColumn("serverEncryptedPrivateKey");
|
||||||
|
if (doesPrivateKeyIVFieldExist) t.dropColumn("serverEncryptedPrivateKeyIV");
|
||||||
|
if (doesPrivateKeyTagFieldExist) t.dropColumn("serverEncryptedPrivateKeyTag");
|
||||||
|
if (doesPrivateKeyEncodingFieldExist) t.dropColumn("serverEncryptedPrivateKeyEncoding");
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
@ -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>;
|
||||||
|
@ -21,7 +21,12 @@ export const UserEncryptionKeysSchema = z.object({
|
|||||||
tag: z.string(),
|
tag: z.string(),
|
||||||
salt: z.string(),
|
salt: z.string(),
|
||||||
verifier: z.string(),
|
verifier: z.string(),
|
||||||
userId: z.string().uuid()
|
userId: z.string().uuid(),
|
||||||
|
hashedPassword: z.string().nullable().optional(),
|
||||||
|
serverEncryptedPrivateKey: z.string().nullable().optional(),
|
||||||
|
serverEncryptedPrivateKeyIV: z.string().nullable().optional(),
|
||||||
|
serverEncryptedPrivateKeyTag: z.string().nullable().optional(),
|
||||||
|
serverEncryptedPrivateKeyEncoding: z.string().nullable().optional()
|
||||||
});
|
});
|
||||||
|
|
||||||
export type TUserEncryptionKeys = z.infer<typeof UserEncryptionKeysSchema>;
|
export type TUserEncryptionKeys = z.infer<typeof UserEncryptionKeysSchema>;
|
||||||
|
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" });
|
||||||
|
@ -53,7 +53,7 @@ export const registerLdapRouter = async (server: FastifyZodProvider) => {
|
|||||||
// eslint-disable-next-line
|
// eslint-disable-next-line
|
||||||
async (req: IncomingMessage, user, cb) => {
|
async (req: IncomingMessage, user, cb) => {
|
||||||
try {
|
try {
|
||||||
if (!user.email) throw new BadRequestError({ message: "Invalid request. Missing email." });
|
if (!user.mail) throw new BadRequestError({ message: "Invalid request. Missing mail attribute on user." });
|
||||||
const ldapConfig = (req as unknown as FastifyRequest).ldapConfig as TLDAPConfig;
|
const ldapConfig = (req as unknown as FastifyRequest).ldapConfig as TLDAPConfig;
|
||||||
|
|
||||||
let groups: { dn: string; cn: string }[] | undefined;
|
let groups: { dn: string; cn: string }[] | undefined;
|
||||||
|
@ -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">;
|
@ -23,6 +23,8 @@ import {
|
|||||||
} from "@app/lib/crypto/encryption";
|
} from "@app/lib/crypto/encryption";
|
||||||
import { BadRequestError } from "@app/lib/errors";
|
import { BadRequestError } from "@app/lib/errors";
|
||||||
import { AuthMethod, AuthTokenType } from "@app/services/auth/auth-type";
|
import { AuthMethod, AuthTokenType } from "@app/services/auth/auth-type";
|
||||||
|
import { TAuthTokenServiceFactory } from "@app/services/auth-token/auth-token-service";
|
||||||
|
import { TokenType } from "@app/services/auth-token/auth-token-types";
|
||||||
import { TGroupProjectDALFactory } from "@app/services/group-project/group-project-dal";
|
import { TGroupProjectDALFactory } from "@app/services/group-project/group-project-dal";
|
||||||
import { TOrgBotDALFactory } from "@app/services/org/org-bot-dal";
|
import { TOrgBotDALFactory } from "@app/services/org/org-bot-dal";
|
||||||
import { TOrgDALFactory } from "@app/services/org/org-dal";
|
import { TOrgDALFactory } from "@app/services/org/org-dal";
|
||||||
@ -30,6 +32,7 @@ import { TOrgMembershipDALFactory } from "@app/services/org-membership/org-membe
|
|||||||
import { TProjectDALFactory } from "@app/services/project/project-dal";
|
import { TProjectDALFactory } from "@app/services/project/project-dal";
|
||||||
import { TProjectBotDALFactory } from "@app/services/project-bot/project-bot-dal";
|
import { TProjectBotDALFactory } from "@app/services/project-bot/project-bot-dal";
|
||||||
import { TProjectKeyDALFactory } from "@app/services/project-key/project-key-dal";
|
import { TProjectKeyDALFactory } from "@app/services/project-key/project-key-dal";
|
||||||
|
import { SmtpTemplates, TSmtpService } 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";
|
||||||
import { TUserDALFactory } from "@app/services/user/user-dal";
|
import { TUserDALFactory } from "@app/services/user/user-dal";
|
||||||
import { normalizeUsername } from "@app/services/user/user-fns";
|
import { normalizeUsername } from "@app/services/user/user-fns";
|
||||||
@ -73,11 +76,19 @@ type TLdapConfigServiceFactoryDep = {
|
|||||||
>;
|
>;
|
||||||
userDAL: Pick<
|
userDAL: Pick<
|
||||||
TUserDALFactory,
|
TUserDALFactory,
|
||||||
"create" | "findOne" | "transaction" | "updateById" | "findUserEncKeyByUserIdsBatch" | "find"
|
| "create"
|
||||||
|
| "findOne"
|
||||||
|
| "transaction"
|
||||||
|
| "updateById"
|
||||||
|
| "findUserEncKeyByUserIdsBatch"
|
||||||
|
| "find"
|
||||||
|
| "findUserEncKeyByUserId"
|
||||||
>;
|
>;
|
||||||
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">;
|
||||||
|
tokenService: Pick<TAuthTokenServiceFactory, "createTokenForUser">;
|
||||||
|
smtpService: Pick<TSmtpService, "sendMail">;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type TLdapConfigServiceFactory = ReturnType<typeof ldapConfigServiceFactory>;
|
export type TLdapConfigServiceFactory = ReturnType<typeof ldapConfigServiceFactory>;
|
||||||
@ -97,7 +108,9 @@ export const ldapConfigServiceFactory = ({
|
|||||||
userDAL,
|
userDAL,
|
||||||
userAliasDAL,
|
userAliasDAL,
|
||||||
permissionService,
|
permissionService,
|
||||||
licenseService
|
licenseService,
|
||||||
|
tokenService,
|
||||||
|
smtpService
|
||||||
}: TLdapConfigServiceFactoryDep) => {
|
}: TLdapConfigServiceFactoryDep) => {
|
||||||
const createLdapCfg = async ({
|
const createLdapCfg = async ({
|
||||||
actor,
|
actor,
|
||||||
@ -488,7 +501,7 @@ export const ldapConfigServiceFactory = ({
|
|||||||
if (!orgMembership) {
|
if (!orgMembership) {
|
||||||
await orgMembershipDAL.create(
|
await orgMembershipDAL.create(
|
||||||
{
|
{
|
||||||
userId: userAlias.userId,
|
userId: newUser.id,
|
||||||
inviteEmail: email,
|
inviteEmail: email,
|
||||||
orgId,
|
orgId,
|
||||||
role: OrgMembershipRole.Member,
|
role: OrgMembershipRole.Member,
|
||||||
@ -510,6 +523,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);
|
||||||
@ -591,12 +605,14 @@ export const ldapConfigServiceFactory = ({
|
|||||||
});
|
});
|
||||||
|
|
||||||
const isUserCompleted = Boolean(user.isAccepted);
|
const isUserCompleted = Boolean(user.isAccepted);
|
||||||
|
const userEnc = await userDAL.findUserEncKeyByUserId(user.id);
|
||||||
|
|
||||||
const providerAuthToken = jwt.sign(
|
const providerAuthToken = jwt.sign(
|
||||||
{
|
{
|
||||||
authTokenType: AuthTokenType.PROVIDER_TOKEN,
|
authTokenType: AuthTokenType.PROVIDER_TOKEN,
|
||||||
userId: user.id,
|
userId: user.id,
|
||||||
username: user.username,
|
username: user.username,
|
||||||
|
hasExchangedPrivateKey: Boolean(userEnc?.serverEncryptedPrivateKey),
|
||||||
...(user.email && { email: user.email, isEmailVerified: user.isEmailVerified }),
|
...(user.email && { email: user.email, isEmailVerified: user.isEmailVerified }),
|
||||||
firstName,
|
firstName,
|
||||||
lastName,
|
lastName,
|
||||||
@ -618,6 +634,22 @@ export const ldapConfigServiceFactory = ({
|
|||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
|
if (user.email && !user.isEmailVerified) {
|
||||||
|
const token = await tokenService.createTokenForUser({
|
||||||
|
type: TokenType.TOKEN_EMAIL_VERIFICATION,
|
||||||
|
userId: user.id
|
||||||
|
});
|
||||||
|
|
||||||
|
await smtpService.sendMail({
|
||||||
|
template: SmtpTemplates.EmailVerification,
|
||||||
|
subjectLine: "Infisical confirmation code",
|
||||||
|
recipients: [user.email],
|
||||||
|
substitutions: {
|
||||||
|
code: token
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
return { isUserCompleted, providerAuthToken };
|
return { isUserCompleted, providerAuthToken };
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -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;
|
@ -41,7 +41,10 @@ import { TCreateSamlCfgDTO, TGetSamlCfgDTO, TSamlLoginDTO, TUpdateSamlCfgDTO } f
|
|||||||
|
|
||||||
type TSamlConfigServiceFactoryDep = {
|
type TSamlConfigServiceFactoryDep = {
|
||||||
samlConfigDAL: Pick<TSamlConfigDALFactory, "create" | "findOne" | "update" | "findById">;
|
samlConfigDAL: Pick<TSamlConfigDALFactory, "create" | "findOne" | "update" | "findById">;
|
||||||
userDAL: Pick<TUserDALFactory, "create" | "findOne" | "transaction" | "updateById" | "findById">;
|
userDAL: Pick<
|
||||||
|
TUserDALFactory,
|
||||||
|
"create" | "findOne" | "transaction" | "updateById" | "findById" | "findUserEncKeyByUserId"
|
||||||
|
>;
|
||||||
userAliasDAL: Pick<TUserAliasDALFactory, "create" | "findOne">;
|
userAliasDAL: Pick<TUserAliasDALFactory, "create" | "findOne">;
|
||||||
orgDAL: Pick<
|
orgDAL: Pick<
|
||||||
TOrgDALFactory,
|
TOrgDALFactory,
|
||||||
@ -50,7 +53,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,8 +452,10 @@ export const samlConfigServiceFactory = ({
|
|||||||
return newUser;
|
return newUser;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
await licenseService.updateSubscriptionOrgMemberCount(organization.id);
|
||||||
|
|
||||||
const isUserCompleted = Boolean(user.isAccepted);
|
const isUserCompleted = Boolean(user.isAccepted);
|
||||||
|
const userEnc = await userDAL.findUserEncKeyByUserId(user.id);
|
||||||
const providerAuthToken = jwt.sign(
|
const providerAuthToken = jwt.sign(
|
||||||
{
|
{
|
||||||
authTokenType: AuthTokenType.PROVIDER_TOKEN,
|
authTokenType: AuthTokenType.PROVIDER_TOKEN,
|
||||||
@ -463,6 +468,7 @@ export const samlConfigServiceFactory = ({
|
|||||||
organizationId: organization.id,
|
organizationId: organization.id,
|
||||||
organizationSlug: organization.slug,
|
organizationSlug: organization.slug,
|
||||||
authMethod: authProvider,
|
authMethod: authProvider,
|
||||||
|
hasExchangedPrivateKey: Boolean(userEnc?.serverEncryptedPrivateKey),
|
||||||
authType: UserAliasType.SAML,
|
authType: UserAliasType.SAML,
|
||||||
isUserCompleted,
|
isUserCompleted,
|
||||||
...(relayState
|
...(relayState
|
||||||
|
@ -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,151 @@ 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 current folders.
|
||||||
|
* 2. Pruning snapshots from non-current folders (versioned ones).
|
||||||
|
* 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 current 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`)
|
||||||
|
.whereRaw(`snapshot_cte.row_num > ${TableName.Project}."pitVersionLimit"`)
|
||||||
|
.delete();
|
||||||
|
} catch (err) {
|
||||||
|
logger.error(
|
||||||
|
`Failed to prune snapshots from current folders in range ${batchEntries[0]}:${
|
||||||
|
batchEntries[batchEntries.length - 1]
|
||||||
|
}`
|
||||||
|
);
|
||||||
|
} finally {
|
||||||
|
uuidOffset = batchEntries[batchEntries.length - 1];
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// cleanup snapshots from non-current 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 non-current 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.",
|
||||||
@ -506,12 +508,27 @@ export const SECRET_TAGS = {
|
|||||||
LIST: {
|
LIST: {
|
||||||
projectId: "The ID of the project to list tags from."
|
projectId: "The ID of the project to list tags from."
|
||||||
},
|
},
|
||||||
|
GET_TAG_BY_ID: {
|
||||||
|
projectId: "The ID of the project to get tags from.",
|
||||||
|
tagId: "The ID of the tag to get details"
|
||||||
|
},
|
||||||
|
GET_TAG_BY_SLUG: {
|
||||||
|
projectId: "The ID of the project to get tags from.",
|
||||||
|
tagSlug: "The slug of the tag to get details"
|
||||||
|
},
|
||||||
CREATE: {
|
CREATE: {
|
||||||
projectId: "The ID of the project to create the tag in.",
|
projectId: "The ID of the project to create the tag in.",
|
||||||
name: "The name of the tag to create.",
|
name: "The name of the tag to create.",
|
||||||
slug: "The slug of the tag to create.",
|
slug: "The slug of the tag to create.",
|
||||||
color: "The color of the tag to create."
|
color: "The color of the tag to create."
|
||||||
},
|
},
|
||||||
|
UPDATE: {
|
||||||
|
projectId: "The ID of the project to update the tag in.",
|
||||||
|
tagId: "The ID of the tag to get details",
|
||||||
|
name: "The name of the tag to update.",
|
||||||
|
slug: "The slug of the tag to update.",
|
||||||
|
color: "The color of the tag to update."
|
||||||
|
},
|
||||||
DELETE: {
|
DELETE: {
|
||||||
tagId: "The ID of the tag to delete.",
|
tagId: "The ID of the tag to delete.",
|
||||||
projectId: "The ID of the project to delete the tag from."
|
projectId: "The ID of the project to delete the tag from."
|
||||||
@ -677,6 +694,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 +745,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.",
|
||||||
|
@ -29,7 +29,7 @@ const envSchema = z
|
|||||||
DB_USER: zpStr(z.string().describe("Postgres database username").optional()),
|
DB_USER: zpStr(z.string().describe("Postgres database username").optional()),
|
||||||
DB_PASSWORD: zpStr(z.string().describe("Postgres database password").optional()),
|
DB_PASSWORD: zpStr(z.string().describe("Postgres database password").optional()),
|
||||||
DB_NAME: zpStr(z.string().describe("Postgres database name").optional()),
|
DB_NAME: zpStr(z.string().describe("Postgres database name").optional()),
|
||||||
|
BCRYPT_SALT_ROUND: z.number().default(12),
|
||||||
NODE_ENV: z.enum(["development", "test", "production"]).default("production"),
|
NODE_ENV: z.enum(["development", "test", "production"]).default("production"),
|
||||||
SALT_ROUNDS: z.coerce.number().default(10),
|
SALT_ROUNDS: z.coerce.number().default(10),
|
||||||
INITIAL_ORGANIZATION_NAME: zpStr(z.string().optional()),
|
INITIAL_ORGANIZATION_NAME: zpStr(z.string().optional()),
|
||||||
@ -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
|
||||||
|
}
|
||||||
|
};
|
||||||
|
};
|
||||||
|
@ -6,7 +6,7 @@ import tweetnacl from "tweetnacl-util";
|
|||||||
|
|
||||||
import { TUserEncryptionKeys } from "@app/db/schemas";
|
import { TUserEncryptionKeys } from "@app/db/schemas";
|
||||||
|
|
||||||
import { decryptSymmetric, encryptAsymmetric, encryptSymmetric } from "./encryption";
|
import { decryptSymmetric128BitHexKeyUTF8, encryptAsymmetric, encryptSymmetric } from "./encryption";
|
||||||
|
|
||||||
export const generateSrpServerKey = async (salt: string, verifier: string) => {
|
export const generateSrpServerKey = async (salt: string, verifier: string) => {
|
||||||
// eslint-disable-next-line new-cap
|
// eslint-disable-next-line new-cap
|
||||||
@ -97,7 +97,13 @@ export const generateUserSrpKeys = async (email: string, password: string) => {
|
|||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
export const getUserPrivateKey = async (password: string, user: TUserEncryptionKeys) => {
|
export const getUserPrivateKey = async (
|
||||||
|
password: string,
|
||||||
|
user: Pick<
|
||||||
|
TUserEncryptionKeys,
|
||||||
|
"protectedKeyTag" | "protectedKey" | "protectedKeyIV" | "encryptedPrivateKey" | "iv" | "salt" | "tag"
|
||||||
|
>
|
||||||
|
) => {
|
||||||
const derivedKey = await argon2.hash(password, {
|
const derivedKey = await argon2.hash(password, {
|
||||||
salt: Buffer.from(user.salt),
|
salt: Buffer.from(user.salt),
|
||||||
memoryCost: 65536,
|
memoryCost: 65536,
|
||||||
@ -108,17 +114,18 @@ export const getUserPrivateKey = async (password: string, user: TUserEncryptionK
|
|||||||
raw: true
|
raw: true
|
||||||
});
|
});
|
||||||
if (!derivedKey) throw new Error("Failed to derive key from password");
|
if (!derivedKey) throw new Error("Failed to derive key from password");
|
||||||
const key = decryptSymmetric({
|
const key = decryptSymmetric128BitHexKeyUTF8({
|
||||||
ciphertext: user.protectedKey!,
|
ciphertext: user.protectedKey as string,
|
||||||
iv: user.protectedKeyIV!,
|
iv: user.protectedKeyIV as string,
|
||||||
tag: user.protectedKeyTag!,
|
tag: user.protectedKeyTag as string,
|
||||||
key: derivedKey.toString("base64")
|
key: derivedKey
|
||||||
});
|
});
|
||||||
const privateKey = decryptSymmetric({
|
|
||||||
|
const privateKey = decryptSymmetric128BitHexKeyUTF8({
|
||||||
ciphertext: user.encryptedPrivateKey,
|
ciphertext: user.encryptedPrivateKey,
|
||||||
iv: user.iv,
|
iv: user.iv,
|
||||||
tag: user.tag,
|
tag: user.tag,
|
||||||
key
|
key: Buffer.from(key, "hex")
|
||||||
});
|
});
|
||||||
return privateKey;
|
return privateKey;
|
||||||
};
|
};
|
||||||
|
@ -59,6 +59,18 @@ export class BadRequestError extends Error {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export class NotFoundError extends Error {
|
||||||
|
name: string;
|
||||||
|
|
||||||
|
error: unknown;
|
||||||
|
|
||||||
|
constructor({ name, error, message }: { message?: string; name?: string; error?: unknown }) {
|
||||||
|
super(message ?? "The requested entity is not found");
|
||||||
|
this.name = name || "NotFound";
|
||||||
|
this.error = error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export class DisableRotationErrors extends Error {
|
export class DisableRotationErrors extends Error {
|
||||||
name: string;
|
name: string;
|
||||||
|
|
||||||
|
@ -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
|
||||||
};
|
};
|
||||||
|
@ -6,6 +6,7 @@ import {
|
|||||||
BadRequestError,
|
BadRequestError,
|
||||||
DatabaseError,
|
DatabaseError,
|
||||||
InternalServerError,
|
InternalServerError,
|
||||||
|
NotFoundError,
|
||||||
ScimRequestError,
|
ScimRequestError,
|
||||||
UnauthorizedError
|
UnauthorizedError
|
||||||
} from "@app/lib/errors";
|
} from "@app/lib/errors";
|
||||||
@ -15,6 +16,8 @@ export const fastifyErrHandler = fastifyPlugin(async (server: FastifyZodProvider
|
|||||||
req.log.error(error);
|
req.log.error(error);
|
||||||
if (error instanceof BadRequestError) {
|
if (error instanceof BadRequestError) {
|
||||||
void res.status(400).send({ statusCode: 400, message: error.message, error: error.name });
|
void res.status(400).send({ statusCode: 400, message: error.message, error: error.name });
|
||||||
|
} else if (error instanceof NotFoundError) {
|
||||||
|
void res.status(404).send({ statusCode: 404, message: error.message, error: error.name });
|
||||||
} else if (error instanceof UnauthorizedError) {
|
} else if (error instanceof UnauthorizedError) {
|
||||||
void res.status(403).send({ statusCode: 403, message: error.message, error: error.name });
|
void res.status(403).send({ statusCode: 403, message: error.message, error: error.name });
|
||||||
} else if (error instanceof DatabaseError || error instanceof InternalServerError) {
|
} else if (error instanceof DatabaseError || error instanceof InternalServerError) {
|
||||||
|
@ -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);
|
||||||
@ -378,7 +392,9 @@ export const registerRoutes = async (
|
|||||||
userDAL,
|
userDAL,
|
||||||
userAliasDAL,
|
userAliasDAL,
|
||||||
permissionService,
|
permissionService,
|
||||||
licenseService
|
licenseService,
|
||||||
|
tokenService,
|
||||||
|
smtpService
|
||||||
});
|
});
|
||||||
|
|
||||||
const telemetryService = telemetryServiceFactory({
|
const telemetryService = telemetryServiceFactory({
|
||||||
@ -444,6 +460,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 +526,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 +594,8 @@ export const registerRoutes = async (
|
|||||||
projectMembershipDAL,
|
projectMembershipDAL,
|
||||||
folderDAL,
|
folderDAL,
|
||||||
licenseService,
|
licenseService,
|
||||||
|
certificateAuthorityDAL,
|
||||||
|
certificateDAL,
|
||||||
projectUserMembershipRoleDAL,
|
projectUserMembershipRoleDAL,
|
||||||
identityProjectMembershipRoleDAL,
|
identityProjectMembershipRoleDAL,
|
||||||
keyStore
|
keyStore
|
||||||
@ -824,6 +898,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 +936,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 +964,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 +978,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 +1040,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();
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
@ -79,6 +79,7 @@ export const registerAdminRouter = async (server: FastifyZodProvider) => {
|
|||||||
schema: {
|
schema: {
|
||||||
body: z.object({
|
body: z.object({
|
||||||
email: z.string().email().trim(),
|
email: z.string().email().trim(),
|
||||||
|
password: z.string().trim(),
|
||||||
firstName: z.string().trim(),
|
firstName: z.string().trim(),
|
||||||
lastName: z.string().trim().optional(),
|
lastName: z.string().trim().optional(),
|
||||||
protectedKey: z.string().trim(),
|
protectedKey: z.string().trim(),
|
||||||
|
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
|
||||||
|
};
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
@ -198,7 +198,7 @@ export const registerIdentityKubernetesRouter = async (server: FastifyZodProvide
|
|||||||
}),
|
}),
|
||||||
response: {
|
response: {
|
||||||
200: z.object({
|
200: z.object({
|
||||||
identityKubernetesAuth: IdentityKubernetesAuthsSchema
|
identityKubernetesAuth: IdentityKubernetesAuthResponseSchema
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
@ -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" });
|
||||||
|
@ -51,7 +51,8 @@ export const registerPasswordRouter = async (server: FastifyZodProvider) => {
|
|||||||
encryptedPrivateKeyIV: z.string().trim(),
|
encryptedPrivateKeyIV: z.string().trim(),
|
||||||
encryptedPrivateKeyTag: z.string().trim(),
|
encryptedPrivateKeyTag: z.string().trim(),
|
||||||
salt: z.string().trim(),
|
salt: z.string().trim(),
|
||||||
verifier: z.string().trim()
|
verifier: z.string().trim(),
|
||||||
|
password: z.string().trim()
|
||||||
}),
|
}),
|
||||||
response: {
|
response: {
|
||||||
200: z.object({
|
200: z.object({
|
||||||
|
@ -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,
|
||||||
@ -36,6 +36,67 @@ export const registerSecretTagRouter = async (server: FastifyZodProvider) => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
server.route({
|
||||||
|
method: "GET",
|
||||||
|
url: "/:projectId/tags/:tagId",
|
||||||
|
config: {
|
||||||
|
rateLimit: readLimit
|
||||||
|
},
|
||||||
|
schema: {
|
||||||
|
params: z.object({
|
||||||
|
projectId: z.string().trim().describe(SECRET_TAGS.GET_TAG_BY_ID.projectId),
|
||||||
|
tagId: z.string().trim().describe(SECRET_TAGS.GET_TAG_BY_ID.tagId)
|
||||||
|
}),
|
||||||
|
response: {
|
||||||
|
200: z.object({
|
||||||
|
workspaceTag: SecretTagsSchema
|
||||||
|
})
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
||||||
|
handler: async (req) => {
|
||||||
|
const workspaceTag = await server.services.secretTag.getTagById({
|
||||||
|
actor: req.permission.type,
|
||||||
|
actorId: req.permission.id,
|
||||||
|
actorAuthMethod: req.permission.authMethod,
|
||||||
|
actorOrgId: req.permission.orgId,
|
||||||
|
id: req.params.tagId
|
||||||
|
});
|
||||||
|
return { workspaceTag };
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
server.route({
|
||||||
|
method: "GET",
|
||||||
|
url: "/:projectId/tags/slug/:tagSlug",
|
||||||
|
config: {
|
||||||
|
rateLimit: readLimit
|
||||||
|
},
|
||||||
|
schema: {
|
||||||
|
params: z.object({
|
||||||
|
projectId: z.string().trim().describe(SECRET_TAGS.GET_TAG_BY_SLUG.projectId),
|
||||||
|
tagSlug: z.string().trim().describe(SECRET_TAGS.GET_TAG_BY_SLUG.tagSlug)
|
||||||
|
}),
|
||||||
|
response: {
|
||||||
|
200: z.object({
|
||||||
|
workspaceTag: SecretTagsSchema
|
||||||
|
})
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
||||||
|
handler: async (req) => {
|
||||||
|
const workspaceTag = await server.services.secretTag.getTagBySlug({
|
||||||
|
actor: req.permission.type,
|
||||||
|
actorId: req.permission.id,
|
||||||
|
actorAuthMethod: req.permission.authMethod,
|
||||||
|
actorOrgId: req.permission.orgId,
|
||||||
|
slug: req.params.tagSlug,
|
||||||
|
projectId: req.params.projectId
|
||||||
|
});
|
||||||
|
return { workspaceTag };
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
server.route({
|
server.route({
|
||||||
method: "POST",
|
method: "POST",
|
||||||
url: "/:projectId/tags",
|
url: "/:projectId/tags",
|
||||||
@ -57,7 +118,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,
|
||||||
@ -71,6 +132,42 @@ export const registerSecretTagRouter = async (server: FastifyZodProvider) => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
server.route({
|
||||||
|
method: "PATCH",
|
||||||
|
url: "/:projectId/tags/:tagId",
|
||||||
|
config: {
|
||||||
|
rateLimit: writeLimit
|
||||||
|
},
|
||||||
|
schema: {
|
||||||
|
params: z.object({
|
||||||
|
projectId: z.string().trim().describe(SECRET_TAGS.UPDATE.projectId),
|
||||||
|
tagId: z.string().trim().describe(SECRET_TAGS.UPDATE.tagId)
|
||||||
|
}),
|
||||||
|
body: z.object({
|
||||||
|
name: z.string().trim().describe(SECRET_TAGS.UPDATE.name),
|
||||||
|
slug: z.string().trim().describe(SECRET_TAGS.UPDATE.slug),
|
||||||
|
color: z.string().trim().describe(SECRET_TAGS.UPDATE.color)
|
||||||
|
}),
|
||||||
|
response: {
|
||||||
|
200: z.object({
|
||||||
|
workspaceTag: SecretTagsSchema
|
||||||
|
})
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
||||||
|
handler: async (req) => {
|
||||||
|
const workspaceTag = await server.services.secretTag.updateTag({
|
||||||
|
actor: req.permission.type,
|
||||||
|
actorId: req.permission.id,
|
||||||
|
actorAuthMethod: req.permission.authMethod,
|
||||||
|
actorOrgId: req.permission.orgId,
|
||||||
|
...req.body,
|
||||||
|
id: req.params.tagId
|
||||||
|
});
|
||||||
|
return { workspaceTag };
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
server.route({
|
server.route({
|
||||||
method: "DELETE",
|
method: "DELETE",
|
||||||
url: "/:projectId/tags/:tagId",
|
url: "/:projectId/tags/:tagId",
|
||||||
@ -88,7 +185,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,
|
||||||
|
@ -259,4 +259,50 @@ export const registerSsoRouter = async (server: FastifyZodProvider) => {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
server.route({
|
||||||
|
url: "/token-exchange",
|
||||||
|
method: "POST",
|
||||||
|
schema: {
|
||||||
|
body: z.object({
|
||||||
|
providerAuthToken: z.string(),
|
||||||
|
email: z.string()
|
||||||
|
})
|
||||||
|
},
|
||||||
|
handler: async (req, res) => {
|
||||||
|
const userAgent = req.headers["user-agent"];
|
||||||
|
if (!userAgent) throw new Error("user agent header is required");
|
||||||
|
|
||||||
|
const data = await server.services.login.oauth2TokenExchange({
|
||||||
|
email: req.body.email,
|
||||||
|
ip: req.realIp,
|
||||||
|
userAgent,
|
||||||
|
providerAuthToken: req.body.providerAuthToken
|
||||||
|
});
|
||||||
|
|
||||||
|
if (data.isMfaEnabled) {
|
||||||
|
return { mfaEnabled: true, token: data.token } as const; // for discriminated union
|
||||||
|
}
|
||||||
|
|
||||||
|
void res.setCookie("jid", data.token.refresh, {
|
||||||
|
httpOnly: true,
|
||||||
|
path: "/",
|
||||||
|
sameSite: "strict",
|
||||||
|
secure: appCfg.HTTPS_ENABLED
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
mfaEnabled: false,
|
||||||
|
encryptionVersion: data.user.encryptionVersion,
|
||||||
|
token: data.token.access,
|
||||||
|
publicKey: data.user.publicKey,
|
||||||
|
encryptedPrivateKey: data.user.encryptedPrivateKey,
|
||||||
|
iv: data.user.iv,
|
||||||
|
tag: data.user.tag,
|
||||||
|
protectedKey: data.user.protectedKey || null,
|
||||||
|
protectedKeyIV: data.user.protectedKeyIV || null,
|
||||||
|
protectedKeyTag: data.user.protectedKeyTag || null
|
||||||
|
} as const;
|
||||||
|
}
|
||||||
|
});
|
||||||
};
|
};
|
||||||
|
@ -19,7 +19,23 @@ export const registerUserRouter = async (server: FastifyZodProvider) => {
|
|||||||
schema: {
|
schema: {
|
||||||
response: {
|
response: {
|
||||||
200: z.object({
|
200: z.object({
|
||||||
user: UsersSchema.merge(UserEncryptionKeysSchema.omit({ verifier: true }))
|
user: UsersSchema.merge(
|
||||||
|
UserEncryptionKeysSchema.pick({
|
||||||
|
clientPublicKey: true,
|
||||||
|
serverPrivateKey: true,
|
||||||
|
encryptionVersion: true,
|
||||||
|
protectedKey: true,
|
||||||
|
protectedKeyIV: true,
|
||||||
|
protectedKeyTag: true,
|
||||||
|
publicKey: true,
|
||||||
|
encryptedPrivateKey: true,
|
||||||
|
iv: true,
|
||||||
|
tag: true,
|
||||||
|
salt: true,
|
||||||
|
verifier: true,
|
||||||
|
userId: true
|
||||||
|
})
|
||||||
|
)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@ -30,6 +46,26 @@ export const registerUserRouter = async (server: FastifyZodProvider) => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
server.route({
|
||||||
|
method: "GET",
|
||||||
|
url: "/private-key",
|
||||||
|
config: {
|
||||||
|
rateLimit: readLimit
|
||||||
|
},
|
||||||
|
schema: {
|
||||||
|
response: {
|
||||||
|
200: z.object({
|
||||||
|
privateKey: z.string()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onRequest: verifyAuth([AuthMode.JWT], { requireOrg: false }),
|
||||||
|
handler: async (req) => {
|
||||||
|
const privateKey = await server.services.user.getUserPrivateKey(req.permission.id);
|
||||||
|
return { privateKey };
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
server.route({
|
server.route({
|
||||||
method: "GET",
|
method: "GET",
|
||||||
url: "/:userId/unlock",
|
url: "/:userId/unlock",
|
||||||
|
@ -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 };
|
||||||
|
}
|
||||||
|
});
|
||||||
};
|
};
|
||||||
|
@ -255,7 +255,23 @@ export const registerUserRouter = async (server: FastifyZodProvider) => {
|
|||||||
description: "Retrieve the current user on the request",
|
description: "Retrieve the current user on the request",
|
||||||
response: {
|
response: {
|
||||||
200: z.object({
|
200: z.object({
|
||||||
user: UsersSchema.merge(UserEncryptionKeysSchema.omit({ verifier: true }))
|
user: UsersSchema.merge(
|
||||||
|
UserEncryptionKeysSchema.pick({
|
||||||
|
clientPublicKey: true,
|
||||||
|
serverPrivateKey: true,
|
||||||
|
encryptionVersion: true,
|
||||||
|
protectedKey: true,
|
||||||
|
protectedKeyIV: true,
|
||||||
|
protectedKeyTag: true,
|
||||||
|
publicKey: true,
|
||||||
|
encryptedPrivateKey: true,
|
||||||
|
iv: true,
|
||||||
|
tag: true,
|
||||||
|
salt: true,
|
||||||
|
verifier: true,
|
||||||
|
userId: true
|
||||||
|
})
|
||||||
|
)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
@ -81,7 +81,8 @@ export const registerLoginRouter = async (server: FastifyZodProvider) => {
|
|||||||
email: z.string().trim(),
|
email: z.string().trim(),
|
||||||
providerAuthToken: z.string().trim().optional(),
|
providerAuthToken: z.string().trim().optional(),
|
||||||
clientProof: z.string().trim(),
|
clientProof: z.string().trim(),
|
||||||
captchaToken: z.string().trim().optional()
|
captchaToken: z.string().trim().optional(),
|
||||||
|
password: z.string().optional()
|
||||||
}),
|
}),
|
||||||
response: {
|
response: {
|
||||||
200: z.discriminatedUnion("mfaEnabled", [
|
200: z.discriminatedUnion("mfaEnabled", [
|
||||||
@ -112,7 +113,8 @@ export const registerLoginRouter = async (server: FastifyZodProvider) => {
|
|||||||
ip: req.realIp,
|
ip: req.realIp,
|
||||||
userAgent,
|
userAgent,
|
||||||
providerAuthToken: req.body.providerAuthToken,
|
providerAuthToken: req.body.providerAuthToken,
|
||||||
clientProof: req.body.clientProof
|
clientProof: req.body.clientProof,
|
||||||
|
password: req.body.password
|
||||||
});
|
});
|
||||||
|
|
||||||
if (data.isMfaEnabled) {
|
if (data.isMfaEnabled) {
|
||||||
|
@ -8,7 +8,7 @@ import {
|
|||||||
SecretType,
|
SecretType,
|
||||||
ServiceTokenScopes
|
ServiceTokenScopes
|
||||||
} from "@app/db/schemas";
|
} from "@app/db/schemas";
|
||||||
import { EventType } from "@app/ee/services/audit-log/audit-log-types";
|
import { EventType, UserAgentType } from "@app/ee/services/audit-log/audit-log-types";
|
||||||
import { RAW_SECRETS, SECRETS } from "@app/lib/api-docs";
|
import { RAW_SECRETS, SECRETS } from "@app/lib/api-docs";
|
||||||
import { BadRequestError } from "@app/lib/errors";
|
import { BadRequestError } from "@app/lib/errors";
|
||||||
import { removeTrailingSlash } from "@app/lib/fn";
|
import { removeTrailingSlash } from "@app/lib/fn";
|
||||||
@ -259,18 +259,20 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
await server.services.telemetry.sendPostHogEvents({
|
if (getUserAgentType(req.headers["user-agent"]) !== UserAgentType.K8_OPERATOR) {
|
||||||
event: PostHogEventTypes.SecretPulled,
|
await server.services.telemetry.sendPostHogEvents({
|
||||||
distinctId: getTelemetryDistinctId(req),
|
event: PostHogEventTypes.SecretPulled,
|
||||||
properties: {
|
distinctId: getTelemetryDistinctId(req),
|
||||||
numberOfSecrets: secrets.length,
|
properties: {
|
||||||
workspaceId,
|
numberOfSecrets: secrets.length,
|
||||||
environment,
|
workspaceId,
|
||||||
secretPath: req.query.secretPath,
|
environment,
|
||||||
channel: getUserAgentType(req.headers["user-agent"]),
|
secretPath: req.query.secretPath,
|
||||||
...req.auditLogInfo
|
channel: getUserAgentType(req.headers["user-agent"]),
|
||||||
}
|
...req.auditLogInfo
|
||||||
});
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
return { secrets, imports };
|
return { secrets, imports };
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@ -306,7 +308,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()
|
||||||
|
})
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@ -358,18 +369,20 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
await server.services.telemetry.sendPostHogEvents({
|
if (getUserAgentType(req.headers["user-agent"]) !== UserAgentType.K8_OPERATOR) {
|
||||||
event: PostHogEventTypes.SecretPulled,
|
await server.services.telemetry.sendPostHogEvents({
|
||||||
distinctId: getTelemetryDistinctId(req),
|
event: PostHogEventTypes.SecretPulled,
|
||||||
properties: {
|
distinctId: getTelemetryDistinctId(req),
|
||||||
numberOfSecrets: 1,
|
properties: {
|
||||||
workspaceId: secret.workspace,
|
numberOfSecrets: 1,
|
||||||
environment,
|
workspaceId: secret.workspace,
|
||||||
secretPath: req.query.secretPath,
|
environment,
|
||||||
channel: getUserAgentType(req.headers["user-agent"]),
|
secretPath: req.query.secretPath,
|
||||||
...req.auditLogInfo
|
channel: getUserAgentType(req.headers["user-agent"]),
|
||||||
}
|
...req.auditLogInfo
|
||||||
});
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
return { secret };
|
return { secret };
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@ -404,6 +417,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 +441,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 +507,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 +529,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({
|
||||||
@ -710,24 +727,22 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
// TODO: Move to telemetry plugin
|
// TODO: Move to telemetry plugin
|
||||||
let shouldRecordK8Event = false;
|
// let shouldRecordK8Event = false;
|
||||||
if (req.headers["user-agent"] === "k8-operatoer") {
|
// if (req.headers["user-agent"] === "k8-operatoer") {
|
||||||
const randomNumber = Math.random();
|
// const randomNumber = Math.random();
|
||||||
if (randomNumber > 0.95) {
|
// if (randomNumber > 0.95) {
|
||||||
shouldRecordK8Event = true;
|
// shouldRecordK8Event = true;
|
||||||
}
|
// }
|
||||||
}
|
// }
|
||||||
|
|
||||||
const shouldCapture =
|
const shouldCapture =
|
||||||
req.query.workspaceId !== "650e71fbae3e6c8572f436d4" &&
|
req.query.workspaceId !== "650e71fbae3e6c8572f436d4" && req.headers["user-agent"] !== "k8-operator";
|
||||||
(req.headers["user-agent"] !== "k8-operator" || shouldRecordK8Event);
|
|
||||||
const approximateNumberTotalSecrets = secrets.length * 20;
|
|
||||||
if (shouldCapture) {
|
if (shouldCapture) {
|
||||||
await server.services.telemetry.sendPostHogEvents({
|
await server.services.telemetry.sendPostHogEvents({
|
||||||
event: PostHogEventTypes.SecretPulled,
|
event: PostHogEventTypes.SecretPulled,
|
||||||
distinctId: getTelemetryDistinctId(req),
|
distinctId: getTelemetryDistinctId(req),
|
||||||
properties: {
|
properties: {
|
||||||
numberOfSecrets: shouldRecordK8Event ? approximateNumberTotalSecrets : secrets.length,
|
numberOfSecrets: secrets.length,
|
||||||
workspaceId: req.query.workspaceId,
|
workspaceId: req.query.workspaceId,
|
||||||
environment: req.query.environment,
|
environment: req.query.environment,
|
||||||
secretPath: req.query.secretPath,
|
secretPath: req.query.secretPath,
|
||||||
@ -804,18 +819,20 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
await server.services.telemetry.sendPostHogEvents({
|
if (getUserAgentType(req.headers["user-agent"]) !== UserAgentType.K8_OPERATOR) {
|
||||||
event: PostHogEventTypes.SecretPulled,
|
await server.services.telemetry.sendPostHogEvents({
|
||||||
distinctId: getTelemetryDistinctId(req),
|
event: PostHogEventTypes.SecretPulled,
|
||||||
properties: {
|
distinctId: getTelemetryDistinctId(req),
|
||||||
numberOfSecrets: 1,
|
properties: {
|
||||||
workspaceId: req.query.workspaceId,
|
numberOfSecrets: 1,
|
||||||
environment: req.query.environment,
|
workspaceId: req.query.workspaceId,
|
||||||
secretPath: req.query.secretPath,
|
environment: req.query.environment,
|
||||||
channel: getUserAgentType(req.headers["user-agent"]),
|
secretPath: req.query.secretPath,
|
||||||
...req.auditLogInfo
|
channel: getUserAgentType(req.headers["user-agent"]),
|
||||||
}
|
...req.auditLogInfo
|
||||||
});
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
return { secret };
|
return { secret };
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
@ -102,7 +102,8 @@ export const registerSignupRouter = async (server: FastifyZodProvider) => {
|
|||||||
verifier: z.string().trim(),
|
verifier: z.string().trim(),
|
||||||
organizationName: z.string().trim().min(1),
|
organizationName: z.string().trim().min(1),
|
||||||
providerAuthToken: z.string().trim().optional().nullish(),
|
providerAuthToken: z.string().trim().optional().nullish(),
|
||||||
attributionSource: z.string().trim().optional()
|
attributionSource: z.string().trim().optional(),
|
||||||
|
password: z.string()
|
||||||
}),
|
}),
|
||||||
response: {
|
response: {
|
||||||
200: z.object({
|
200: z.object({
|
||||||
@ -167,6 +168,7 @@ export const registerSignupRouter = async (server: FastifyZodProvider) => {
|
|||||||
schema: {
|
schema: {
|
||||||
body: z.object({
|
body: z.object({
|
||||||
email: z.string().email().trim(),
|
email: z.string().email().trim(),
|
||||||
|
password: z.string(),
|
||||||
firstName: z.string().trim(),
|
firstName: z.string().trim(),
|
||||||
lastName: z.string().trim().optional(),
|
lastName: z.string().trim().optional(),
|
||||||
protectedKey: z.string().trim(),
|
protectedKey: z.string().trim(),
|
||||||
|
@ -15,10 +15,10 @@ export const validateProviderAuthToken = (providerToken: string, username?: stri
|
|||||||
if (decodedToken.username !== username) throw new Error("Invalid auth credentials");
|
if (decodedToken.username !== username) throw new Error("Invalid auth credentials");
|
||||||
|
|
||||||
if (decodedToken.organizationId) {
|
if (decodedToken.organizationId) {
|
||||||
return { orgId: decodedToken.organizationId, authMethod: decodedToken.authMethod };
|
return { orgId: decodedToken.organizationId, authMethod: decodedToken.authMethod, userName: decodedToken.username };
|
||||||
}
|
}
|
||||||
|
|
||||||
return { authMethod: decodedToken.authMethod, orgId: null };
|
return { authMethod: decodedToken.authMethod, orgId: null, userName: decodedToken.username };
|
||||||
};
|
};
|
||||||
|
|
||||||
export const validateSignUpAuthorization = (token: string, userId: string, validate = true) => {
|
export const validateSignUpAuthorization = (token: string, userId: string, validate = true) => {
|
||||||
|
@ -1,3 +1,4 @@
|
|||||||
|
import bcrypt from "bcrypt";
|
||||||
import jwt from "jsonwebtoken";
|
import jwt from "jsonwebtoken";
|
||||||
|
|
||||||
import { TUsers, UserDeviceSchema } from "@app/db/schemas";
|
import { TUsers, UserDeviceSchema } from "@app/db/schemas";
|
||||||
@ -5,6 +6,8 @@ import { isAuthMethodSaml } from "@app/ee/services/permission/permission-fns";
|
|||||||
import { getConfig } from "@app/lib/config/env";
|
import { getConfig } from "@app/lib/config/env";
|
||||||
import { request } from "@app/lib/config/request";
|
import { request } from "@app/lib/config/request";
|
||||||
import { generateSrpServerKey, srpCheckClientProof } from "@app/lib/crypto";
|
import { generateSrpServerKey, srpCheckClientProof } from "@app/lib/crypto";
|
||||||
|
import { infisicalSymmetricEncypt } from "@app/lib/crypto/encryption";
|
||||||
|
import { getUserPrivateKey } from "@app/lib/crypto/srp";
|
||||||
import { BadRequestError, DatabaseError, UnauthorizedError } from "@app/lib/errors";
|
import { BadRequestError, DatabaseError, UnauthorizedError } from "@app/lib/errors";
|
||||||
import { getServerCfg } from "@app/services/super-admin/super-admin-service";
|
import { getServerCfg } from "@app/services/super-admin/super-admin-service";
|
||||||
|
|
||||||
@ -19,6 +22,7 @@ import {
|
|||||||
TLoginClientProofDTO,
|
TLoginClientProofDTO,
|
||||||
TLoginGenServerPublicKeyDTO,
|
TLoginGenServerPublicKeyDTO,
|
||||||
TOauthLoginDTO,
|
TOauthLoginDTO,
|
||||||
|
TOauthTokenExchangeDTO,
|
||||||
TVerifyMfaTokenDTO
|
TVerifyMfaTokenDTO
|
||||||
} from "./auth-login-type";
|
} from "./auth-login-type";
|
||||||
import { AuthMethod, AuthModeJwtTokenPayload, AuthModeMfaJwtTokenPayload, AuthTokenType } from "./auth-type";
|
import { AuthMethod, AuthModeJwtTokenPayload, AuthModeMfaJwtTokenPayload, AuthTokenType } from "./auth-type";
|
||||||
@ -101,7 +105,7 @@ export const authLoginServiceFactory = ({
|
|||||||
user: TUsers;
|
user: TUsers;
|
||||||
ip: string;
|
ip: string;
|
||||||
userAgent: string;
|
userAgent: string;
|
||||||
organizationId: string | undefined;
|
organizationId?: string;
|
||||||
authMethod: AuthMethod;
|
authMethod: AuthMethod;
|
||||||
}) => {
|
}) => {
|
||||||
const cfg = getConfig();
|
const cfg = getConfig();
|
||||||
@ -178,7 +182,8 @@ export const authLoginServiceFactory = ({
|
|||||||
ip,
|
ip,
|
||||||
userAgent,
|
userAgent,
|
||||||
providerAuthToken,
|
providerAuthToken,
|
||||||
captchaToken
|
captchaToken,
|
||||||
|
password
|
||||||
}: TLoginClientProofDTO) => {
|
}: TLoginClientProofDTO) => {
|
||||||
const appCfg = getConfig();
|
const appCfg = getConfig();
|
||||||
|
|
||||||
@ -248,14 +253,29 @@ export const authLoginServiceFactory = ({
|
|||||||
throw new Error("Failed to authenticate. Try again?");
|
throw new Error("Failed to authenticate. Try again?");
|
||||||
}
|
}
|
||||||
|
|
||||||
await userDAL.updateUserEncryptionByUserId(userEnc.userId, {
|
|
||||||
serverPrivateKey: null,
|
|
||||||
clientPublicKey: null
|
|
||||||
});
|
|
||||||
|
|
||||||
await userDAL.updateById(userEnc.userId, {
|
await userDAL.updateById(userEnc.userId, {
|
||||||
consecutiveFailedPasswordAttempts: 0
|
consecutiveFailedPasswordAttempts: 0
|
||||||
});
|
});
|
||||||
|
// from password decrypt the private key
|
||||||
|
if (password) {
|
||||||
|
const privateKey = await getUserPrivateKey(password, userEnc);
|
||||||
|
const hashedPassword = await bcrypt.hash(password, cfg.BCRYPT_SALT_ROUND);
|
||||||
|
const { iv, tag, ciphertext, encoding } = infisicalSymmetricEncypt(privateKey);
|
||||||
|
await userDAL.updateUserEncryptionByUserId(userEnc.userId, {
|
||||||
|
serverPrivateKey: null,
|
||||||
|
clientPublicKey: null,
|
||||||
|
hashedPassword,
|
||||||
|
serverEncryptedPrivateKey: ciphertext,
|
||||||
|
serverEncryptedPrivateKeyIV: iv,
|
||||||
|
serverEncryptedPrivateKeyTag: tag,
|
||||||
|
serverEncryptedPrivateKeyEncoding: encoding
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
await userDAL.updateUserEncryptionByUserId(userEnc.userId, {
|
||||||
|
serverPrivateKey: null,
|
||||||
|
clientPublicKey: null
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
// send multi factor auth token if they it enabled
|
// send multi factor auth token if they it enabled
|
||||||
if (userEnc.isMfaEnabled && userEnc.email) {
|
if (userEnc.isMfaEnabled && userEnc.email) {
|
||||||
@ -499,8 +519,14 @@ export const authLoginServiceFactory = ({
|
|||||||
authMethods: [authMethod],
|
authMethods: [authMethod],
|
||||||
isGhost: false
|
isGhost: false
|
||||||
});
|
});
|
||||||
|
} else {
|
||||||
|
const isLinkingRequired = !user?.authMethods?.includes(authMethod);
|
||||||
|
if (isLinkingRequired) {
|
||||||
|
user = await userDAL.updateById(user.id, { authMethods: [...(user.authMethods || []), authMethod] });
|
||||||
|
}
|
||||||
}
|
}
|
||||||
const isLinkingRequired = !user?.authMethods?.includes(authMethod);
|
|
||||||
|
const userEnc = await userDAL.findUserEncKeyByUserId(user.id);
|
||||||
const isUserCompleted = user.isAccepted;
|
const isUserCompleted = user.isAccepted;
|
||||||
const providerAuthToken = jwt.sign(
|
const providerAuthToken = jwt.sign(
|
||||||
{
|
{
|
||||||
@ -511,9 +537,9 @@ export const authLoginServiceFactory = ({
|
|||||||
isEmailVerified: user.isEmailVerified,
|
isEmailVerified: user.isEmailVerified,
|
||||||
firstName: user.firstName,
|
firstName: user.firstName,
|
||||||
lastName: user.lastName,
|
lastName: user.lastName,
|
||||||
|
hasExchangedPrivateKey: Boolean(userEnc?.serverEncryptedPrivateKey),
|
||||||
authMethod,
|
authMethod,
|
||||||
isUserCompleted,
|
isUserCompleted,
|
||||||
isLinkingRequired,
|
|
||||||
...(callbackPort
|
...(callbackPort
|
||||||
? {
|
? {
|
||||||
callbackPort
|
callbackPort
|
||||||
@ -525,10 +551,71 @@ export const authLoginServiceFactory = ({
|
|||||||
expiresIn: appCfg.JWT_PROVIDER_AUTH_LIFETIME
|
expiresIn: appCfg.JWT_PROVIDER_AUTH_LIFETIME
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
return { isUserCompleted, providerAuthToken };
|
return { isUserCompleted, providerAuthToken };
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handles OAuth2 token exchange for user login with private key handoff.
|
||||||
|
*
|
||||||
|
* The process involves exchanging a provider's authorization token for an Infisical access token.
|
||||||
|
* The provider token is returned to the client, who then sends it back to obtain the Infisical access token.
|
||||||
|
*
|
||||||
|
* This approach is used instead of directly sending the access token for the following reasons:
|
||||||
|
* 1. To facilitate easier logic changes from SRP OAuth to simple OAuth.
|
||||||
|
* 2. To avoid attaching the access token to the URL, which could be logged. The provider token has a very short lifespan, reducing security risks.
|
||||||
|
*/
|
||||||
|
const oauth2TokenExchange = async ({ userAgent, ip, providerAuthToken, email }: TOauthTokenExchangeDTO) => {
|
||||||
|
const decodedProviderToken = validateProviderAuthToken(providerAuthToken, email);
|
||||||
|
|
||||||
|
const appCfg = getConfig();
|
||||||
|
const { authMethod, userName } = decodedProviderToken;
|
||||||
|
if (!userName) throw new BadRequestError({ message: "Missing user name" });
|
||||||
|
const organizationId =
|
||||||
|
(isAuthMethodSaml(authMethod) || authMethod === AuthMethod.LDAP) && decodedProviderToken.orgId
|
||||||
|
? decodedProviderToken.orgId
|
||||||
|
: undefined;
|
||||||
|
|
||||||
|
const userEnc = await userDAL.findUserEncKeyByUsername({
|
||||||
|
username: email
|
||||||
|
});
|
||||||
|
if (!userEnc) throw new BadRequestError({ message: "Invalid token" });
|
||||||
|
if (!userEnc.serverEncryptedPrivateKey)
|
||||||
|
throw new BadRequestError({ message: "Key handoff incomplete. Please try logging in again." });
|
||||||
|
// send multi factor auth token if they it enabled
|
||||||
|
if (userEnc.isMfaEnabled && userEnc.email) {
|
||||||
|
enforceUserLockStatus(Boolean(userEnc.isLocked), userEnc.temporaryLockDateEnd);
|
||||||
|
|
||||||
|
const mfaToken = jwt.sign(
|
||||||
|
{
|
||||||
|
authMethod,
|
||||||
|
authTokenType: AuthTokenType.MFA_TOKEN,
|
||||||
|
userId: userEnc.userId
|
||||||
|
},
|
||||||
|
appCfg.AUTH_SECRET,
|
||||||
|
{
|
||||||
|
expiresIn: appCfg.JWT_MFA_LIFETIME
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
await sendUserMfaCode({
|
||||||
|
userId: userEnc.userId,
|
||||||
|
email: userEnc.email
|
||||||
|
});
|
||||||
|
|
||||||
|
return { isMfaEnabled: true, token: mfaToken } as const;
|
||||||
|
}
|
||||||
|
|
||||||
|
const token = await generateUserTokens({
|
||||||
|
user: { ...userEnc, id: userEnc.userId },
|
||||||
|
ip,
|
||||||
|
userAgent,
|
||||||
|
authMethod,
|
||||||
|
organizationId
|
||||||
|
});
|
||||||
|
|
||||||
|
return { token, isMfaEnabled: false, user: userEnc } as const;
|
||||||
|
};
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* logout user by incrementing the version by 1 meaning any old session will become invalid
|
* logout user by incrementing the version by 1 meaning any old session will become invalid
|
||||||
* as there number is behind
|
* as there number is behind
|
||||||
@ -542,6 +629,7 @@ export const authLoginServiceFactory = ({
|
|||||||
loginExchangeClientProof,
|
loginExchangeClientProof,
|
||||||
logout,
|
logout,
|
||||||
oauth2Login,
|
oauth2Login,
|
||||||
|
oauth2TokenExchange,
|
||||||
resendMfaToken,
|
resendMfaToken,
|
||||||
verifyMfaToken,
|
verifyMfaToken,
|
||||||
selectOrganization,
|
selectOrganization,
|
||||||
|
@ -13,6 +13,7 @@ export type TLoginClientProofDTO = {
|
|||||||
ip: string;
|
ip: string;
|
||||||
userAgent: string;
|
userAgent: string;
|
||||||
captchaToken?: string;
|
captchaToken?: string;
|
||||||
|
password?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type TVerifyMfaTokenDTO = {
|
export type TVerifyMfaTokenDTO = {
|
||||||
@ -31,3 +32,10 @@ export type TOauthLoginDTO = {
|
|||||||
authMethod: AuthMethod;
|
authMethod: AuthMethod;
|
||||||
callbackPort?: string;
|
callbackPort?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type TOauthTokenExchangeDTO = {
|
||||||
|
providerAuthToken: string;
|
||||||
|
ip: string;
|
||||||
|
userAgent: string;
|
||||||
|
email: string;
|
||||||
|
};
|
||||||
|
@ -1,3 +1,4 @@
|
|||||||
|
import bcrypt from "bcrypt";
|
||||||
import jwt from "jsonwebtoken";
|
import jwt from "jsonwebtoken";
|
||||||
|
|
||||||
import { SecretEncryptionAlgo, SecretKeyEncoding } from "@app/db/schemas";
|
import { SecretEncryptionAlgo, SecretKeyEncoding } from "@app/db/schemas";
|
||||||
@ -57,7 +58,8 @@ export const authPaswordServiceFactory = ({
|
|||||||
encryptedPrivateKeyTag,
|
encryptedPrivateKeyTag,
|
||||||
salt,
|
salt,
|
||||||
verifier,
|
verifier,
|
||||||
tokenVersionId
|
tokenVersionId,
|
||||||
|
password
|
||||||
}: TChangePasswordDTO) => {
|
}: TChangePasswordDTO) => {
|
||||||
const userEnc = await userDAL.findUserEncKeyByUserId(userId);
|
const userEnc = await userDAL.findUserEncKeyByUserId(userId);
|
||||||
if (!userEnc) throw new Error("Failed to find user");
|
if (!userEnc) throw new Error("Failed to find user");
|
||||||
@ -76,6 +78,8 @@ export const authPaswordServiceFactory = ({
|
|||||||
);
|
);
|
||||||
if (!isValidClientProof) throw new Error("Failed to authenticate. Try again?");
|
if (!isValidClientProof) throw new Error("Failed to authenticate. Try again?");
|
||||||
|
|
||||||
|
const appCfg = getConfig();
|
||||||
|
const hashedPassword = await bcrypt.hash(password, appCfg.BCRYPT_SALT_ROUND);
|
||||||
await userDAL.updateUserEncryptionByUserId(userId, {
|
await userDAL.updateUserEncryptionByUserId(userId, {
|
||||||
encryptionVersion: 2,
|
encryptionVersion: 2,
|
||||||
protectedKey,
|
protectedKey,
|
||||||
@ -87,7 +91,8 @@ export const authPaswordServiceFactory = ({
|
|||||||
salt,
|
salt,
|
||||||
verifier,
|
verifier,
|
||||||
serverPrivateKey: null,
|
serverPrivateKey: null,
|
||||||
clientPublicKey: null
|
clientPublicKey: null,
|
||||||
|
hashedPassword
|
||||||
});
|
});
|
||||||
|
|
||||||
if (tokenVersionId) {
|
if (tokenVersionId) {
|
||||||
|
@ -10,6 +10,7 @@ export type TChangePasswordDTO = {
|
|||||||
salt: string;
|
salt: string;
|
||||||
verifier: string;
|
verifier: string;
|
||||||
tokenVersionId?: string;
|
tokenVersionId?: string;
|
||||||
|
password: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type TResetPasswordViaBackupKeyDTO = {
|
export type TResetPasswordViaBackupKeyDTO = {
|
||||||
|
@ -1,3 +1,4 @@
|
|||||||
|
import bcrypt from "bcrypt";
|
||||||
import jwt from "jsonwebtoken";
|
import jwt from "jsonwebtoken";
|
||||||
|
|
||||||
import { OrgMembershipStatus, TableName } from "@app/db/schemas";
|
import { OrgMembershipStatus, TableName } from "@app/db/schemas";
|
||||||
@ -6,6 +7,8 @@ import { TUserGroupMembershipDALFactory } from "@app/ee/services/group/user-grou
|
|||||||
import { TLicenseServiceFactory } from "@app/ee/services/license/license-service";
|
import { TLicenseServiceFactory } from "@app/ee/services/license/license-service";
|
||||||
import { isAuthMethodSaml } from "@app/ee/services/permission/permission-fns";
|
import { isAuthMethodSaml } from "@app/ee/services/permission/permission-fns";
|
||||||
import { getConfig } from "@app/lib/config/env";
|
import { getConfig } from "@app/lib/config/env";
|
||||||
|
import { infisicalSymmetricEncypt } from "@app/lib/crypto/encryption";
|
||||||
|
import { getUserPrivateKey } from "@app/lib/crypto/srp";
|
||||||
import { BadRequestError } from "@app/lib/errors";
|
import { BadRequestError } from "@app/lib/errors";
|
||||||
import { isDisposableEmail } from "@app/lib/validator";
|
import { isDisposableEmail } from "@app/lib/validator";
|
||||||
import { TGroupProjectDALFactory } from "@app/services/group-project/group-project-dal";
|
import { TGroupProjectDALFactory } from "@app/services/group-project/group-project-dal";
|
||||||
@ -119,6 +122,7 @@ export const authSignupServiceFactory = ({
|
|||||||
|
|
||||||
const completeEmailAccountSignup = async ({
|
const completeEmailAccountSignup = async ({
|
||||||
email,
|
email,
|
||||||
|
password,
|
||||||
firstName,
|
firstName,
|
||||||
lastName,
|
lastName,
|
||||||
providerAuthToken,
|
providerAuthToken,
|
||||||
@ -137,6 +141,7 @@ export const authSignupServiceFactory = ({
|
|||||||
userAgent,
|
userAgent,
|
||||||
authorization
|
authorization
|
||||||
}: TCompleteAccountSignupDTO) => {
|
}: TCompleteAccountSignupDTO) => {
|
||||||
|
const appCfg = getConfig();
|
||||||
const user = await userDAL.findOne({ username: email });
|
const user = await userDAL.findOne({ username: email });
|
||||||
if (!user || (user && user.isAccepted)) {
|
if (!user || (user && user.isAccepted)) {
|
||||||
throw new Error("Failed to complete account for complete user");
|
throw new Error("Failed to complete account for complete user");
|
||||||
@ -152,6 +157,17 @@ export const authSignupServiceFactory = ({
|
|||||||
validateSignUpAuthorization(authorization, user.id);
|
validateSignUpAuthorization(authorization, user.id);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const hashedPassword = await bcrypt.hash(password, appCfg.BCRYPT_SALT_ROUND);
|
||||||
|
const privateKey = await getUserPrivateKey(password, {
|
||||||
|
salt,
|
||||||
|
protectedKey,
|
||||||
|
protectedKeyIV,
|
||||||
|
protectedKeyTag,
|
||||||
|
encryptedPrivateKey,
|
||||||
|
iv: encryptedPrivateKeyIV,
|
||||||
|
tag: encryptedPrivateKeyTag
|
||||||
|
});
|
||||||
|
const { tag, encoding, ciphertext, iv } = infisicalSymmetricEncypt(privateKey);
|
||||||
const updateduser = await authDAL.transaction(async (tx) => {
|
const updateduser = await authDAL.transaction(async (tx) => {
|
||||||
const us = await userDAL.updateById(user.id, { firstName, lastName, isAccepted: true }, tx);
|
const us = await userDAL.updateById(user.id, { firstName, lastName, isAccepted: true }, tx);
|
||||||
if (!us) throw new Error("User not found");
|
if (!us) throw new Error("User not found");
|
||||||
@ -166,7 +182,12 @@ export const authSignupServiceFactory = ({
|
|||||||
protectedKeyTag,
|
protectedKeyTag,
|
||||||
encryptedPrivateKey,
|
encryptedPrivateKey,
|
||||||
iv: encryptedPrivateKeyIV,
|
iv: encryptedPrivateKeyIV,
|
||||||
tag: encryptedPrivateKeyTag
|
tag: encryptedPrivateKeyTag,
|
||||||
|
hashedPassword,
|
||||||
|
serverEncryptedPrivateKeyEncoding: encoding,
|
||||||
|
serverEncryptedPrivateKeyTag: tag,
|
||||||
|
serverEncryptedPrivateKeyIV: iv,
|
||||||
|
serverEncryptedPrivateKey: ciphertext
|
||||||
},
|
},
|
||||||
tx
|
tx
|
||||||
);
|
);
|
||||||
@ -227,11 +248,10 @@ export const authSignupServiceFactory = ({
|
|||||||
userId: updateduser.info.id
|
userId: updateduser.info.id
|
||||||
});
|
});
|
||||||
if (!tokenSession) throw new Error("Failed to create token");
|
if (!tokenSession) throw new Error("Failed to create token");
|
||||||
const appCfg = getConfig();
|
|
||||||
|
|
||||||
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 +264,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,
|
||||||
@ -265,6 +285,7 @@ export const authSignupServiceFactory = ({
|
|||||||
ip,
|
ip,
|
||||||
salt,
|
salt,
|
||||||
email,
|
email,
|
||||||
|
password,
|
||||||
verifier,
|
verifier,
|
||||||
firstName,
|
firstName,
|
||||||
publicKey,
|
publicKey,
|
||||||
@ -295,6 +316,18 @@ export const authSignupServiceFactory = ({
|
|||||||
name: "complete account invite"
|
name: "complete account invite"
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const appCfg = getConfig();
|
||||||
|
const hashedPassword = await bcrypt.hash(password, appCfg.BCRYPT_SALT_ROUND);
|
||||||
|
const privateKey = await getUserPrivateKey(password, {
|
||||||
|
salt,
|
||||||
|
protectedKey,
|
||||||
|
protectedKeyIV,
|
||||||
|
protectedKeyTag,
|
||||||
|
encryptedPrivateKey,
|
||||||
|
iv: encryptedPrivateKeyIV,
|
||||||
|
tag: encryptedPrivateKeyTag
|
||||||
|
});
|
||||||
|
const { tag, encoding, ciphertext, iv } = infisicalSymmetricEncypt(privateKey);
|
||||||
const updateduser = await authDAL.transaction(async (tx) => {
|
const updateduser = await authDAL.transaction(async (tx) => {
|
||||||
const us = await userDAL.updateById(user.id, { firstName, lastName, isAccepted: true }, tx);
|
const us = await userDAL.updateById(user.id, { firstName, lastName, isAccepted: true }, tx);
|
||||||
if (!us) throw new Error("User not found");
|
if (!us) throw new Error("User not found");
|
||||||
@ -310,7 +343,12 @@ export const authSignupServiceFactory = ({
|
|||||||
protectedKeyTag,
|
protectedKeyTag,
|
||||||
encryptedPrivateKey,
|
encryptedPrivateKey,
|
||||||
iv: encryptedPrivateKeyIV,
|
iv: encryptedPrivateKeyIV,
|
||||||
tag: encryptedPrivateKeyTag
|
tag: encryptedPrivateKeyTag,
|
||||||
|
hashedPassword,
|
||||||
|
serverEncryptedPrivateKeyEncoding: encoding,
|
||||||
|
serverEncryptedPrivateKeyTag: tag,
|
||||||
|
serverEncryptedPrivateKeyIV: iv,
|
||||||
|
serverEncryptedPrivateKey: ciphertext
|
||||||
},
|
},
|
||||||
tx
|
tx
|
||||||
);
|
);
|
||||||
@ -343,7 +381,6 @@ export const authSignupServiceFactory = ({
|
|||||||
userId: updateduser.info.id
|
userId: updateduser.info.id
|
||||||
});
|
});
|
||||||
if (!tokenSession) throw new Error("Failed to create token");
|
if (!tokenSession) throw new Error("Failed to create token");
|
||||||
const appCfg = getConfig();
|
|
||||||
|
|
||||||
const accessToken = jwt.sign(
|
const accessToken = jwt.sign(
|
||||||
{
|
{
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
export type TCompleteAccountSignupDTO = {
|
export type TCompleteAccountSignupDTO = {
|
||||||
email: string;
|
email: string;
|
||||||
|
password: string;
|
||||||
firstName: string;
|
firstName: string;
|
||||||
lastName?: string;
|
lastName?: string;
|
||||||
protectedKey: string;
|
protectedKey: string;
|
||||||
@ -21,6 +22,7 @@ export type TCompleteAccountSignupDTO = {
|
|||||||
|
|
||||||
export type TCompleteAccountInviteDTO = {
|
export type TCompleteAccountInviteDTO = {
|
||||||
email: string;
|
email: string;
|
||||||
|
password: string;
|
||||||
firstName: string;
|
firstName: string;
|
||||||
lastName?: string;
|
lastName?: string;
|
||||||
protectedKey: string;
|
protectedKey: string;
|
||||||
|
@ -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;
|
||||||
|
}
|
||||||
|
};
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user