mirror of
https://github.com/Infisical/infisical.git
synced 2025-07-05 04:29:09 +00:00
Compare commits
475 Commits
doc/add-ab
...
misc/add-e
Author | SHA1 | Date | |
---|---|---|---|
e5947fcab9 | |||
f7cf2bb78f | |||
ff24e76a32 | |||
6ac802b6c9 | |||
ff92e00503 | |||
b20474c505 | |||
e19ffc91c6 | |||
61eb66efca | |||
15999daa24 | |||
ec31211bca | |||
0ecf6044d9 | |||
6c512f47bf | |||
33b135f02c | |||
eed7cc6408 | |||
440ada464f | |||
6b7abbbeb9 | |||
3944e20a5b | |||
2079913511 | |||
049f0f56a0 | |||
9ad725fd6c | |||
9a954c8f15 | |||
81a64d081c | |||
43804f62e6 | |||
67089af17a | |||
d83240749f | |||
36144d8c42 | |||
4478dc8659 | |||
510ddf2b1a | |||
5363f8c6ff | |||
7d9de6acba | |||
bac944133a | |||
f059d65b45 | |||
c487b2b34a | |||
015a193330 | |||
8e20531b40 | |||
d91add2e7b | |||
8ead2aa774 | |||
1b2128e3cc | |||
6d72524896 | |||
1ec11d5963 | |||
ad6f285b59 | |||
d4842dd273 | |||
78f83cb478 | |||
c8a871de7c | |||
64c0951df3 | |||
c185414a3c | |||
f9695741f1 | |||
b7c4b11260 | |||
81f3613393 | |||
a7fe79c046 | |||
ed6306747a | |||
64569ab44b | |||
9eb89bb46d | |||
c4da1ce32d | |||
2d1d6f5ce8 | |||
add97c9b38 | |||
768ba4f4dc | |||
18c32d872c | |||
1fd40ab6ab | |||
9d258f57ce | |||
45ccbaf4c9 | |||
6ef358b172 | |||
838c1af448 | |||
8de7261c9a | |||
67b1b79fe3 | |||
31477f4d2b | |||
f200372d74 | |||
f955b68519 | |||
9269b63943 | |||
8f96653273 | |||
7dffc08eba | |||
126b0ce7e7 | |||
0b71f7f297 | |||
e53439d586 | |||
c86e508817 | |||
6426b85c1e | |||
cc7d0d752f | |||
b89212a0c9 | |||
d4c69d8e5d | |||
3d6da1e548 | |||
7e46fe8148 | |||
3756a1901d | |||
9c8adf75ec | |||
f461eaa432 | |||
a1fbc140ee | |||
ea27870ce3 | |||
48943b4d78 | |||
fd1afc2cbe | |||
6905029455 | |||
e89fb33981 | |||
2ef77c737a | |||
0f31fa3128 | |||
1da5a5f417 | |||
5ebf142e3e | |||
94d7d2b029 | |||
e39d1a0530 | |||
4c5f3859d6 | |||
16866d46bf | |||
4f4764dfcd | |||
bdceea4c91 | |||
32fa6866e4 | |||
b4faef797c | |||
08732cab62 | |||
81d5f639ae | |||
25b83d4b86 | |||
155e59e571 | |||
8fbd3f2fce | |||
a500f00a49 | |||
6842f7aa8b | |||
ad207786e2 | |||
ace8c37c25 | |||
f15e61dbd9 | |||
4c82408b51 | |||
8146dcef16 | |||
2e90addbc5 | |||
427201a634 | |||
0b55ac141c | |||
aecfa268ae | |||
fdfc020efc | |||
62aa80a104 | |||
cf9d8035bd | |||
d0c9f1ca53 | |||
2ecc7424d9 | |||
c04b97c689 | |||
7600a86dfc | |||
8924eaf251 | |||
82e9504285 | |||
c4e10df754 | |||
ce60e96008 | |||
930b59cb4f | |||
ec363a5ad4 | |||
c0de4ae3ee | |||
de7e92ccfc | |||
522d81ae1a | |||
ef22b39421 | |||
02153ffb32 | |||
1d14cdf334 | |||
39b323dd9c | |||
b0b55344ce | |||
d9d62384e7 | |||
76f34501dc | |||
7415bb93b8 | |||
7a1c08a7f2 | |||
568aadef75 | |||
84f9eb5f9f | |||
87ac723fcb | |||
a6dab47552 | |||
79d8a9debb | |||
08bac83bcc | |||
46c90f03f0 | |||
d7722f7587 | |||
a42bcb3393 | |||
192dba04a5 | |||
0cc3240956 | |||
667580546b | |||
9fd662b7f7 | |||
a56cbbc02f | |||
dc30465afb | |||
f1caab2d00 | |||
1d186b1950 | |||
9cf5908cc1 | |||
f1b6c3764f | |||
4e6c860c69 | |||
eda9ed257e | |||
38cf43176e | |||
f5c7943f2f | |||
3c59f7f350 | |||
84cc7bcd6c | |||
159c27ac67 | |||
de5a432745 | |||
387780aa94 | |||
3887ce800b | |||
1a06b3e1f5 | |||
5f0dd31334 | |||
7e14c58931 | |||
627e17b3ae | |||
39b7a4a111 | |||
e7c512999e | |||
c19016e6e6 | |||
20477ce2b0 | |||
e04b2220be | |||
edf6a37fe5 | |||
f5749e326a | |||
75e0a68b68 | |||
71b8e3dbce | |||
4dc56033b1 | |||
ed37b99756 | |||
6fa41a609b | |||
e46f10292c | |||
acb22cdf36 | |||
c9da8477c8 | |||
5e4b478b74 | |||
765be2d99d | |||
719a18c218 | |||
16d3bbb67a | |||
872a3fe48d | |||
c7414e00f9 | |||
ad1dd55b8b | |||
497761a0e5 | |||
483fb458dd | |||
17cf602a65 | |||
23f6f5dfd4 | |||
b9b76579ac | |||
761965696b | |||
ace2500885 | |||
4eff7d8ea5 | |||
c4512ae111 | |||
78c349c09a | |||
09df440613 | |||
a8fc0e540a | |||
46ce46b5a0 | |||
dc88115d43 | |||
955657e172 | |||
f1ba64aa66 | |||
d74197aeb4 | |||
97567d06d4 | |||
3986df8e8a | |||
3fcd84b592 | |||
29e39b558b | |||
9458c8b04f | |||
3b95c5d859 | |||
de8f315211 | |||
9960d58e1b | |||
0057404562 | |||
47ca1b3011 | |||
716cd090c4 | |||
e870bb3ade | |||
98c9e98082 | |||
a814f459ab | |||
66817a40db | |||
20bd2ca71c | |||
004a8b71a2 | |||
f0fce3086e | |||
a9e7db6fc0 | |||
2bd681d58f | |||
51fef3ce60 | |||
df9e7bf6ee | |||
04479bb70a | |||
cdc90411e5 | |||
dcb05a3093 | |||
b055cda64d | |||
f68602280e | |||
f9483afe95 | |||
d742534f6a | |||
99eb8eb8ed | |||
1dea024880 | |||
699e03c1a9 | |||
f6372249b4 | |||
0f42fcd688 | |||
2e02f8bea8 | |||
8203158c63 | |||
ada04ed4fc | |||
cc9cc70125 | |||
045debeaf3 | |||
3fb8ad2fac | |||
795d9e4413 | |||
67f2e4671a | |||
cbe3acde74 | |||
de480b5771 | |||
07b93c5cec | |||
77431b4719 | |||
50610945be | |||
57f54440d6 | |||
9711e73a06 | |||
214f837041 | |||
58ebebb162 | |||
65ddddb6de | |||
a55b26164a | |||
6cd448b8a5 | |||
c48c9ae628 | |||
7003ad608a | |||
104edca6f1 | |||
75345d91c0 | |||
abc2ffca57 | |||
b7640f2d03 | |||
2ee4d68fd0 | |||
3ca931acf1 | |||
7f6715643d | |||
8e311658d4 | |||
9116acd37b | |||
0513307d98 | |||
28c2f1874e | |||
efc3b6d474 | |||
07e1d1b130 | |||
7f76779124 | |||
30bcf1f204 | |||
706feafbf2 | |||
fc4e3f1f72 | |||
dcd5f20325 | |||
58f3e116a3 | |||
7bc5aad8ec | |||
a16dc3aef6 | |||
da7746c639 | |||
cd5b6da541 | |||
2dda7180a9 | |||
30ccfbfc8e | |||
aa76924ee6 | |||
d8f679e72d | |||
bf6cfbac7a | |||
8e82813894 | |||
df21a1fb81 | |||
bdbb6346cb | |||
ea9da6d2a8 | |||
3c2c70912f | |||
b607429b99 | |||
16c1516979 | |||
f5dbbaf1fd | |||
2a292455ef | |||
4d040706a9 | |||
5183f76397 | |||
4b3efb43b0 | |||
96046726b2 | |||
a86a951acc | |||
5e70860160 | |||
abbd427ee2 | |||
8fd5fdbc6a | |||
77e1ccc8d7 | |||
711cc438f6 | |||
8447190bf8 | |||
12b447425b | |||
9cb1a31287 | |||
b00413817d | |||
2a8bd74e88 | |||
f28f4f7561 | |||
f0b05c683b | |||
3e8f02a4f9 | |||
50ee60a3ea | |||
21bdecdf2a | |||
bf09461416 | |||
1ff615913c | |||
281cedf1a2 | |||
a8d847f139 | |||
2a0c0590f1 | |||
2e6d525d27 | |||
7fd4249d00 | |||
90cfc44592 | |||
8c403780c2 | |||
b69c091f2f | |||
4a66395ce6 | |||
8c18753e3f | |||
85c5d69c36 | |||
94fe577046 | |||
a0a579834c | |||
b5575f4c20 | |||
f98f212ecf | |||
b331a4a708 | |||
e351a16b5a | |||
2cfca823f2 | |||
a8398a7009 | |||
8c054cedfc | |||
24d4f8100c | |||
08f23e2d3c | |||
d1ad605ac4 | |||
9dd5857ff5 | |||
babbacdc96 | |||
76427f43f7 | |||
3badcea95b | |||
1a4c0fe8d9 | |||
04f6864abc | |||
fcbe0f59d2 | |||
e95b6fdeaa | |||
5391bcd3b2 | |||
48fd9e2a56 | |||
7b5926d865 | |||
034123bcdf | |||
f3786788fd | |||
c406f6d78d | |||
eb66295dd4 | |||
798215e84c | |||
53f7491441 | |||
53f6ab118b | |||
0f5a1b13a6 | |||
5c606fe45f | |||
bbf60169eb | |||
e004be22e3 | |||
016cb4a7ba | |||
9bfc2a5dd2 | |||
72dbef97fb | |||
f376eaae13 | |||
026f883d21 | |||
e42f860261 | |||
08ec8c9b73 | |||
1512d4f496 | |||
9f7b42ad91 | |||
3045477c32 | |||
be4adc2759 | |||
4eba80905a | |||
b023bc7442 | |||
a0029ab469 | |||
53605c3880 | |||
e5bca5b5df | |||
4091bc19e9 | |||
23bd048bb9 | |||
17a4674821 | |||
ec9631107d | |||
3fa450b9a7 | |||
3b9c62c366 | |||
cb3d171d48 | |||
c29841fbcf | |||
fcccf1bd8d | |||
4382825162 | |||
f80ef1dcc8 | |||
7abf3e3642 | |||
82ef35bd08 | |||
4eb668b5a5 | |||
18edea9f26 | |||
787c091948 | |||
ff269b1063 | |||
ca0636cb25 | |||
b995358b7e | |||
7aaf0f4ed3 | |||
68646bcdf8 | |||
9989ceb6d1 | |||
95d7ba5f22 | |||
2aa6fdf983 | |||
be5a32a5d6 | |||
f009cd329b | |||
e2778864e2 | |||
ea7375b2c6 | |||
d42566c335 | |||
45cbd9f006 | |||
8580602ea7 | |||
7ff75cdfab | |||
bd8c8871c0 | |||
d5aa13b277 | |||
428dc5d371 | |||
f1facf1f2c | |||
31dc36d4e2 | |||
51f29e5357 | |||
30f0f174d1 | |||
3e7110f334 | |||
e6af7a6fb9 | |||
de420fd02c | |||
41a3ca149d | |||
da38d1a261 | |||
b0d8c8fb23 | |||
d84bac5fba | |||
44f74e4d12 | |||
c16a4e00d8 | |||
11f2719842 | |||
f8153dd896 | |||
b104f8c07d | |||
746687e5b5 | |||
080b1e1550 | |||
38a6fd140c | |||
19d66abc38 | |||
e61c0be6db | |||
917573931f | |||
929a41065c | |||
9b44972e77 | |||
17e576511b | |||
afd444cad6 | |||
55b1fbdf52 | |||
46ca5c8efa | |||
f7406ea8f8 | |||
f34370cb9d | |||
78718cd299 | |||
1307fa49d4 | |||
a7ca242f5d | |||
c6b3b24312 | |||
8520029958 | |||
7905017121 | |||
4bbe80c083 | |||
d65ae2c61b | |||
84c534ef70 | |||
ce4c5d8ea1 | |||
617aa2f533 | |||
e9dd3340bf | |||
1c2b4e91ba | |||
c54eafc128 | |||
757942aefc | |||
1d57629036 | |||
8061066e27 | |||
c993b1bbe3 | |||
2cbf33ac14 |
@ -112,4 +112,11 @@ INF_APP_CONNECTION_GCP_SERVICE_ACCOUNT_CREDENTIAL=
|
|||||||
|
|
||||||
# azure app connection
|
# azure app connection
|
||||||
INF_APP_CONNECTION_AZURE_CLIENT_ID=
|
INF_APP_CONNECTION_AZURE_CLIENT_ID=
|
||||||
INF_APP_CONNECTION_AZURE_CLIENT_SECRET=
|
INF_APP_CONNECTION_AZURE_CLIENT_SECRET=
|
||||||
|
|
||||||
|
# datadog
|
||||||
|
SHOULD_USE_DATADOG_TRACER=
|
||||||
|
DATADOG_PROFILING_ENABLED=
|
||||||
|
DATADOG_ENV=
|
||||||
|
DATADOG_SERVICE=
|
||||||
|
DATADOG_HOSTNAME=
|
||||||
|
3
.envrc
Normal file
3
.envrc
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
# Learn more at https://direnv.net
|
||||||
|
# We instruct direnv to use our Nix flake for a consistent development environment.
|
||||||
|
use flake
|
@ -32,10 +32,23 @@ jobs:
|
|||||||
run: touch .env && docker compose -f docker-compose.dev.yml up -d db redis
|
run: touch .env && docker compose -f docker-compose.dev.yml up -d db redis
|
||||||
- name: Start the server
|
- name: Start the server
|
||||||
run: |
|
run: |
|
||||||
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 -e ENCRYPTION_KEY=$ENCRYPTION_KEY --env-file .env --entrypoint '/bin/sh' infisical-api -c "npm run migration:latest && ls && node dist/main.mjs"
|
|
||||||
|
echo "Examining built image:"
|
||||||
|
docker image inspect infisical-api | grep -A 5 "Entrypoint"
|
||||||
|
|
||||||
|
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 \
|
||||||
|
infisical-api
|
||||||
|
|
||||||
|
echo "Container status right after creation:"
|
||||||
|
docker ps -a | grep infisical-api
|
||||||
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
|
||||||
@ -43,35 +56,48 @@ jobs:
|
|||||||
ENCRYPTION_KEY: 4bnfe4e407b8921c104518903515b218
|
ENCRYPTION_KEY: 4bnfe4e407b8921c104518903515b218
|
||||||
- uses: actions/setup-go@v5
|
- uses: actions/setup-go@v5
|
||||||
with:
|
with:
|
||||||
go-version: '1.21.5'
|
go-version: "1.21.5"
|
||||||
- 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
|
||||||
HEALTHY=0
|
HEALTHY=0
|
||||||
while [ $SECONDS -lt 60 ]; do
|
while [ $SECONDS -lt 60 ]; do
|
||||||
if docker ps | grep infisical-api | grep -q healthy; then
|
# Check if container is running
|
||||||
echo "Container is healthy."
|
if docker ps | grep infisical-api; then
|
||||||
HEALTHY=1
|
# Try to access the API endpoint
|
||||||
|
if curl -s -f http://localhost:4000/api/docs/json > /dev/null 2>&1; then
|
||||||
|
echo "API endpoint is responding. Container seems healthy."
|
||||||
|
HEALTHY=1
|
||||||
|
break
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
echo "Container is not running!"
|
||||||
|
docker ps -a | grep infisical-api
|
||||||
break
|
break
|
||||||
fi
|
fi
|
||||||
|
|
||||||
echo "Waiting for container to be healthy... ($SECONDS seconds elapsed)"
|
echo "Waiting for container to be healthy... ($SECONDS seconds elapsed)"
|
||||||
|
sleep 5
|
||||||
docker logs infisical-api
|
SECONDS=$((SECONDS+5))
|
||||||
|
|
||||||
sleep 2
|
|
||||||
SECONDS=$((SECONDS+2))
|
|
||||||
done
|
done
|
||||||
|
|
||||||
if [ $HEALTHY -ne 1 ]; then
|
if [ $HEALTHY -ne 1 ]; then
|
||||||
echo "Container did not become healthy in time"
|
echo "Container did not become healthy in time"
|
||||||
|
echo "Container status:"
|
||||||
|
docker ps -a | grep infisical-api
|
||||||
|
echo "Container logs (if any):"
|
||||||
|
docker logs infisical-api || echo "No logs available"
|
||||||
|
echo "Container inspection:"
|
||||||
|
docker inspect infisical-api | grep -A 5 "State"
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
- name: Install openapi-diff
|
- name: Install openapi-diff
|
||||||
run: go install github.com/tufin/oasdiff@latest
|
run: go install github.com/oasdiff/oasdiff@latest
|
||||||
- name: Running OpenAPI Spec diff action
|
- name: Running OpenAPI Spec diff action
|
||||||
run: oasdiff breaking https://app.infisical.com/api/docs/json http://localhost:4000/api/docs/json --fail-on ERR
|
run: oasdiff breaking https://app.infisical.com/api/docs/json http://localhost:4000/api/docs/json --fail-on ERR
|
||||||
- name: cleanup
|
- name: cleanup
|
||||||
|
if: always()
|
||||||
run: |
|
run: |
|
||||||
docker compose -f "docker-compose.dev.yml" down
|
docker compose -f "docker-compose.dev.yml" down
|
||||||
docker stop infisical-api
|
docker stop infisical-api || true
|
||||||
docker remove infisical-api
|
docker rm infisical-api || true
|
@ -26,7 +26,7 @@ jobs:
|
|||||||
CLI_TESTS_INFISICAL_VAULT_FILE_PASSPHRASE: ${{ secrets.CLI_TESTS_INFISICAL_VAULT_FILE_PASSPHRASE }}
|
CLI_TESTS_INFISICAL_VAULT_FILE_PASSPHRASE: ${{ secrets.CLI_TESTS_INFISICAL_VAULT_FILE_PASSPHRASE }}
|
||||||
|
|
||||||
npm-release:
|
npm-release:
|
||||||
runs-on: ubuntu-20.04
|
runs-on: ubuntu-latest
|
||||||
env:
|
env:
|
||||||
working-directory: ./npm
|
working-directory: ./npm
|
||||||
needs:
|
needs:
|
||||||
@ -83,7 +83,7 @@ jobs:
|
|||||||
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
|
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
|
||||||
|
|
||||||
goreleaser:
|
goreleaser:
|
||||||
runs-on: ubuntu-20.04
|
runs-on: ubuntu-latest
|
||||||
needs: [cli-integration-tests]
|
needs: [cli-integration-tests]
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v3
|
- uses: actions/checkout@v3
|
||||||
@ -103,11 +103,12 @@ jobs:
|
|||||||
go-version: ">=1.19.3"
|
go-version: ">=1.19.3"
|
||||||
cache: true
|
cache: true
|
||||||
cache-dependency-path: cli/go.sum
|
cache-dependency-path: cli/go.sum
|
||||||
- name: libssl1.1 => libssl1.0-dev for OSXCross
|
- name: Setup for libssl1.0-dev
|
||||||
run: |
|
run: |
|
||||||
echo 'deb http://security.ubuntu.com/ubuntu bionic-security main' | sudo tee -a /etc/apt/sources.list
|
echo 'deb http://security.ubuntu.com/ubuntu bionic-security main' | sudo tee -a /etc/apt/sources.list
|
||||||
sudo apt update && apt-cache policy libssl1.0-dev
|
sudo apt-key adv --keyserver keyserver.ubuntu.com --recv-keys 3B4FE6ACC0B21F32
|
||||||
sudo apt-get install libssl1.0-dev
|
sudo apt update
|
||||||
|
sudo apt-get install -y libssl1.0-dev
|
||||||
- name: OSXCross for CGO Support
|
- name: OSXCross for CGO Support
|
||||||
run: |
|
run: |
|
||||||
mkdir ../../osxcross
|
mkdir ../../osxcross
|
||||||
|
8
.github/workflows/run-backend-tests.yml
vendored
8
.github/workflows/run-backend-tests.yml
vendored
@ -34,7 +34,10 @@ jobs:
|
|||||||
working-directory: backend
|
working-directory: backend
|
||||||
- name: Start postgres and redis
|
- name: Start postgres and redis
|
||||||
run: touch .env && docker compose -f docker-compose.dev.yml up -d db redis
|
run: touch .env && docker compose -f docker-compose.dev.yml up -d db redis
|
||||||
- name: Start integration test
|
- name: Run unit test
|
||||||
|
run: npm run test:unit
|
||||||
|
working-directory: backend
|
||||||
|
- name: Run integration test
|
||||||
run: npm run test:e2e
|
run: npm run test:e2e
|
||||||
working-directory: backend
|
working-directory: backend
|
||||||
env:
|
env:
|
||||||
@ -44,4 +47,5 @@ jobs:
|
|||||||
ENCRYPTION_KEY: 4bnfe4e407b8921c104518903515b218
|
ENCRYPTION_KEY: 4bnfe4e407b8921c104518903515b218
|
||||||
- name: cleanup
|
- name: cleanup
|
||||||
run: |
|
run: |
|
||||||
docker compose -f "docker-compose.dev.yml" down
|
docker compose -f "docker-compose.dev.yml" down
|
||||||
|
|
||||||
|
@ -161,6 +161,9 @@ COPY --from=backend-runner /app /backend
|
|||||||
|
|
||||||
COPY --from=frontend-runner /app ./backend/frontend-build
|
COPY --from=frontend-runner /app ./backend/frontend-build
|
||||||
|
|
||||||
|
ARG INFISICAL_PLATFORM_VERSION
|
||||||
|
ENV INFISICAL_PLATFORM_VERSION $INFISICAL_PLATFORM_VERSION
|
||||||
|
|
||||||
ENV PORT 8080
|
ENV PORT 8080
|
||||||
ENV HOST=0.0.0.0
|
ENV HOST=0.0.0.0
|
||||||
ENV HTTPS_ENABLED false
|
ENV HTTPS_ENABLED false
|
||||||
|
@ -3,13 +3,10 @@ ARG POSTHOG_API_KEY=posthog-api-key
|
|||||||
ARG INTERCOM_ID=intercom-id
|
ARG INTERCOM_ID=intercom-id
|
||||||
ARG CAPTCHA_SITE_KEY=captcha-site-key
|
ARG CAPTCHA_SITE_KEY=captcha-site-key
|
||||||
|
|
||||||
FROM node:20-alpine AS base
|
FROM node:20-slim AS base
|
||||||
|
|
||||||
FROM base AS frontend-dependencies
|
FROM base AS frontend-dependencies
|
||||||
|
|
||||||
# Check https://github.com/nodejs/docker-node/tree/b4117f9333da4138b03a546ec926ef50a31506c3#nodealpine to understand why libc6-compat might be needed.
|
|
||||||
RUN apk add --no-cache libc6-compat
|
|
||||||
|
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
|
|
||||||
COPY frontend/package.json frontend/package-lock.json ./
|
COPY frontend/package.json frontend/package-lock.json ./
|
||||||
@ -45,8 +42,8 @@ RUN npm run build
|
|||||||
FROM base AS frontend-runner
|
FROM base AS frontend-runner
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
|
|
||||||
RUN addgroup --system --gid 1001 nodejs
|
RUN groupadd --system --gid 1001 nodejs
|
||||||
RUN adduser --system --uid 1001 non-root-user
|
RUN useradd --system --uid 1001 --gid nodejs non-root-user
|
||||||
|
|
||||||
COPY --from=frontend-builder --chown=non-root-user:nodejs /app/dist ./
|
COPY --from=frontend-builder --chown=non-root-user:nodejs /app/dist ./
|
||||||
|
|
||||||
@ -56,21 +53,23 @@ USER non-root-user
|
|||||||
## BACKEND
|
## BACKEND
|
||||||
##
|
##
|
||||||
FROM base AS backend-build
|
FROM base AS backend-build
|
||||||
RUN addgroup --system --gid 1001 nodejs \
|
|
||||||
&& adduser --system --uid 1001 non-root-user
|
|
||||||
|
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
|
|
||||||
# Install all required dependencies for build
|
# Install all required dependencies for build
|
||||||
RUN apk --update add \
|
RUN apt-get update && apt-get install -y \
|
||||||
python3 \
|
python3 \
|
||||||
make \
|
make \
|
||||||
g++ \
|
g++ \
|
||||||
unixodbc \
|
unixodbc \
|
||||||
freetds \
|
freetds-bin \
|
||||||
unixodbc-dev \
|
unixodbc-dev \
|
||||||
libc-dev \
|
libc-dev \
|
||||||
freetds-dev
|
freetds-dev \
|
||||||
|
&& rm -rf /var/lib/apt/lists/*
|
||||||
|
|
||||||
|
RUN groupadd --system --gid 1001 nodejs
|
||||||
|
RUN useradd --system --uid 1001 --gid nodejs non-root-user
|
||||||
|
|
||||||
COPY backend/package*.json ./
|
COPY backend/package*.json ./
|
||||||
RUN npm ci --only-production
|
RUN npm ci --only-production
|
||||||
@ -86,18 +85,19 @@ FROM base AS backend-runner
|
|||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
|
|
||||||
# Install all required dependencies for runtime
|
# Install all required dependencies for runtime
|
||||||
RUN apk --update add \
|
RUN apt-get update && apt-get install -y \
|
||||||
python3 \
|
python3 \
|
||||||
make \
|
make \
|
||||||
g++ \
|
g++ \
|
||||||
unixodbc \
|
unixodbc \
|
||||||
freetds \
|
freetds-bin \
|
||||||
unixodbc-dev \
|
unixodbc-dev \
|
||||||
libc-dev \
|
libc-dev \
|
||||||
freetds-dev
|
freetds-dev \
|
||||||
|
&& rm -rf /var/lib/apt/lists/*
|
||||||
|
|
||||||
# Configure ODBC
|
# Configure ODBC
|
||||||
RUN printf "[FreeTDS]\nDescription = FreeTDS Driver\nDriver = /usr/lib/libtdsodbc.so\nSetup = /usr/lib/libtdsodbc.so\nFileUsage = 1\n" > /etc/odbcinst.ini
|
RUN printf "[FreeTDS]\nDescription = FreeTDS Driver\nDriver = /usr/lib/x86_64-linux-gnu/odbc/libtdsodbc.so\nSetup = /usr/lib/x86_64-linux-gnu/odbc/libtdsS.so\nFileUsage = 1\n" > /etc/odbcinst.ini
|
||||||
|
|
||||||
COPY backend/package*.json ./
|
COPY backend/package*.json ./
|
||||||
RUN npm ci --only-production
|
RUN npm ci --only-production
|
||||||
@ -109,34 +109,36 @@ RUN mkdir frontend-build
|
|||||||
# Production stage
|
# Production stage
|
||||||
FROM base AS production
|
FROM base AS production
|
||||||
|
|
||||||
RUN apk add --upgrade --no-cache ca-certificates
|
RUN apt-get update && apt-get install -y \
|
||||||
RUN apk add --no-cache bash curl && curl -1sLf \
|
ca-certificates \
|
||||||
'https://dl.cloudsmith.io/public/infisical/infisical-cli/setup.alpine.sh' | bash \
|
bash \
|
||||||
&& apk add infisical=0.31.1 && apk add --no-cache git
|
curl \
|
||||||
|
git \
|
||||||
WORKDIR /
|
|
||||||
|
|
||||||
# Install all required runtime dependencies
|
|
||||||
RUN apk --update add \
|
|
||||||
python3 \
|
python3 \
|
||||||
make \
|
make \
|
||||||
g++ \
|
g++ \
|
||||||
unixodbc \
|
unixodbc \
|
||||||
freetds \
|
freetds-bin \
|
||||||
unixodbc-dev \
|
unixodbc-dev \
|
||||||
libc-dev \
|
libc-dev \
|
||||||
freetds-dev \
|
freetds-dev \
|
||||||
bash \
|
wget \
|
||||||
curl \
|
openssh-client \
|
||||||
git \
|
&& rm -rf /var/lib/apt/lists/*
|
||||||
openssh
|
|
||||||
|
# Install Infisical CLI
|
||||||
|
RUN curl -1sLf 'https://dl.cloudsmith.io/public/infisical/infisical-cli/setup.deb.sh' | bash \
|
||||||
|
&& apt-get update && apt-get install -y infisical=0.31.1 \
|
||||||
|
&& rm -rf /var/lib/apt/lists/*
|
||||||
|
|
||||||
|
WORKDIR /
|
||||||
|
|
||||||
# Configure ODBC in production
|
# Configure ODBC in production
|
||||||
RUN printf "[FreeTDS]\nDescription = FreeTDS Driver\nDriver = /usr/lib/libtdsodbc.so\nSetup = /usr/lib/libtdsodbc.so\nFileUsage = 1\n" > /etc/odbcinst.ini
|
RUN printf "[FreeTDS]\nDescription = FreeTDS Driver\nDriver = /usr/lib/x86_64-linux-gnu/odbc/libtdsodbc.so\nSetup = /usr/lib/x86_64-linux-gnu/odbc/libtdsS.so\nFileUsage = 1\n" > /etc/odbcinst.ini
|
||||||
|
|
||||||
# Setup user permissions
|
# Setup user permissions
|
||||||
RUN addgroup --system --gid 1001 nodejs \
|
RUN groupadd --system --gid 1001 nodejs \
|
||||||
&& adduser --system --uid 1001 non-root-user
|
&& useradd --system --uid 1001 --gid nodejs non-root-user
|
||||||
|
|
||||||
# Give non-root-user permission to update SSL certs
|
# Give non-root-user permission to update SSL certs
|
||||||
RUN chown -R non-root-user /etc/ssl/certs
|
RUN chown -R non-root-user /etc/ssl/certs
|
||||||
@ -154,11 +156,11 @@ ENV INTERCOM_ID=$INTERCOM_ID
|
|||||||
ARG CAPTCHA_SITE_KEY
|
ARG CAPTCHA_SITE_KEY
|
||||||
ENV CAPTCHA_SITE_KEY=$CAPTCHA_SITE_KEY
|
ENV CAPTCHA_SITE_KEY=$CAPTCHA_SITE_KEY
|
||||||
|
|
||||||
|
|
||||||
COPY --from=backend-runner /app /backend
|
COPY --from=backend-runner /app /backend
|
||||||
|
|
||||||
COPY --from=frontend-runner /app ./backend/frontend-build
|
COPY --from=frontend-runner /app ./backend/frontend-build
|
||||||
|
|
||||||
|
ARG INFISICAL_PLATFORM_VERSION
|
||||||
|
ENV INFISICAL_PLATFORM_VERSION $INFISICAL_PLATFORM_VERSION
|
||||||
|
|
||||||
ENV PORT 8080
|
ENV PORT 8080
|
||||||
ENV HOST=0.0.0.0
|
ENV HOST=0.0.0.0
|
||||||
@ -166,6 +168,7 @@ ENV HTTPS_ENABLED false
|
|||||||
ENV NODE_ENV production
|
ENV NODE_ENV production
|
||||||
ENV STANDALONE_BUILD true
|
ENV STANDALONE_BUILD true
|
||||||
ENV STANDALONE_MODE true
|
ENV STANDALONE_MODE true
|
||||||
|
|
||||||
WORKDIR /backend
|
WORKDIR /backend
|
||||||
|
|
||||||
ENV TELEMETRY_ENABLED true
|
ENV TELEMETRY_ENABLED true
|
||||||
|
@ -1,23 +1,22 @@
|
|||||||
# Build stage
|
# Build stage
|
||||||
FROM node:20-alpine AS build
|
FROM node:20-slim AS build
|
||||||
|
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
|
|
||||||
# Required for pkcs11js
|
# Required for pkcs11js
|
||||||
RUN apk --update add \
|
RUN apt-get update && apt-get install -y \
|
||||||
python3 \
|
python3 \
|
||||||
make \
|
make \
|
||||||
g++ \
|
g++ \
|
||||||
openssh
|
openssh-client
|
||||||
|
|
||||||
# install dependencies for TDS driver (required for SAP ASE dynamic secrets)
|
# Install dependencies for TDS driver (required for SAP ASE dynamic secrets)
|
||||||
RUN apk add --no-cache \
|
RUN apt-get install -y \
|
||||||
unixodbc \
|
unixodbc \
|
||||||
freetds \
|
freetds-bin \
|
||||||
|
freetds-dev \
|
||||||
unixodbc-dev \
|
unixodbc-dev \
|
||||||
libc-dev \
|
libc-dev
|
||||||
freetds-dev
|
|
||||||
|
|
||||||
|
|
||||||
COPY package*.json ./
|
COPY package*.json ./
|
||||||
RUN npm ci --only-production
|
RUN npm ci --only-production
|
||||||
@ -26,36 +25,36 @@ COPY . .
|
|||||||
RUN npm run build
|
RUN npm run build
|
||||||
|
|
||||||
# Production stage
|
# Production stage
|
||||||
FROM node:20-alpine
|
FROM node:20-slim
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
|
|
||||||
ENV npm_config_cache /home/node/.npm
|
ENV npm_config_cache /home/node/.npm
|
||||||
|
|
||||||
COPY package*.json ./
|
COPY package*.json ./
|
||||||
|
|
||||||
RUN apk --update add \
|
RUN apt-get update && apt-get install -y \
|
||||||
python3 \
|
python3 \
|
||||||
make \
|
make \
|
||||||
g++
|
g++
|
||||||
|
|
||||||
# install dependencies for TDS driver (required for SAP ASE dynamic secrets)
|
# Install dependencies for TDS driver (required for SAP ASE dynamic secrets)
|
||||||
RUN apk add --no-cache \
|
RUN apt-get install -y \
|
||||||
unixodbc \
|
unixodbc \
|
||||||
freetds \
|
freetds-bin \
|
||||||
|
freetds-dev \
|
||||||
unixodbc-dev \
|
unixodbc-dev \
|
||||||
libc-dev \
|
libc-dev
|
||||||
freetds-dev
|
|
||||||
|
|
||||||
|
RUN printf "[FreeTDS]\nDescription = FreeTDS Driver\nDriver = /usr/lib/x86_64-linux-gnu/odbc/libtdsodbc.so\nSetup = /usr/lib/x86_64-linux-gnu/odbc/libtdsodbc.so\nFileUsage = 1\n" > /etc/odbcinst.ini
|
||||||
RUN printf "[FreeTDS]\nDescription = FreeTDS Driver\nDriver = /usr/lib/libtdsodbc.so\nSetup = /usr/lib/libtdsodbc.so\nFileUsage = 1\n" > /etc/odbcinst.ini
|
|
||||||
|
|
||||||
RUN npm ci --only-production && npm cache clean --force
|
RUN npm ci --only-production && npm cache clean --force
|
||||||
|
|
||||||
COPY --from=build /app .
|
COPY --from=build /app .
|
||||||
|
|
||||||
RUN apk add --no-cache bash curl && curl -1sLf \
|
# Install Infisical CLI
|
||||||
'https://dl.cloudsmith.io/public/infisical/infisical-cli/setup.alpine.sh' | bash \
|
RUN apt-get install -y curl bash && \
|
||||||
&& apk add infisical=0.8.1 && apk add --no-cache git
|
curl -1sLf 'https://dl.cloudsmith.io/public/infisical/infisical-cli/setup.deb.sh' | bash && \
|
||||||
|
apt-get update && apt-get install -y infisical=0.8.1 git
|
||||||
|
|
||||||
HEALTHCHECK --interval=10s --timeout=3s --start-period=10s \
|
HEALTHCHECK --interval=10s --timeout=3s --start-period=10s \
|
||||||
CMD node healthcheck.js
|
CMD node healthcheck.js
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
FROM node:20-alpine
|
FROM node:20-slim
|
||||||
|
|
||||||
# ? Setup a test SoftHSM module. In production a real HSM is used.
|
# ? Setup a test SoftHSM module. In production a real HSM is used.
|
||||||
|
|
||||||
@ -7,32 +7,32 @@ ARG SOFTHSM2_VERSION=2.5.0
|
|||||||
ENV SOFTHSM2_VERSION=${SOFTHSM2_VERSION} \
|
ENV SOFTHSM2_VERSION=${SOFTHSM2_VERSION} \
|
||||||
SOFTHSM2_SOURCES=/tmp/softhsm2
|
SOFTHSM2_SOURCES=/tmp/softhsm2
|
||||||
|
|
||||||
# install build dependencies including python3 (required for pkcs11js and partially TDS driver)
|
# Install build dependencies including python3 (required for pkcs11js and partially TDS driver)
|
||||||
RUN apk --update add \
|
RUN apt-get update && apt-get install -y \
|
||||||
alpine-sdk \
|
build-essential \
|
||||||
autoconf \
|
autoconf \
|
||||||
automake \
|
automake \
|
||||||
git \
|
git \
|
||||||
libtool \
|
libtool \
|
||||||
openssl-dev \
|
libssl-dev \
|
||||||
python3 \
|
python3 \
|
||||||
make \
|
make \
|
||||||
g++ \
|
g++ \
|
||||||
openssh
|
openssh-client \
|
||||||
|
curl \
|
||||||
|
pkg-config
|
||||||
|
|
||||||
# install dependencies for TDS driver (required for SAP ASE dynamic secrets)
|
# Install dependencies for TDS driver (required for SAP ASE dynamic secrets)
|
||||||
RUN apk add --no-cache \
|
RUN apt-get install -y \
|
||||||
unixodbc \
|
unixodbc \
|
||||||
freetds \
|
|
||||||
unixodbc-dev \
|
unixodbc-dev \
|
||||||
libc-dev \
|
freetds-dev \
|
||||||
freetds-dev
|
freetds-bin \
|
||||||
|
tdsodbc
|
||||||
|
|
||||||
|
RUN printf "[FreeTDS]\nDescription = FreeTDS Driver\nDriver = /usr/lib/x86_64-linux-gnu/odbc/libtdsodbc.so\nSetup = /usr/lib/x86_64-linux-gnu/odbc/libtdsodbc.so\nFileUsage = 1\n" > /etc/odbcinst.ini
|
||||||
|
|
||||||
RUN printf "[FreeTDS]\nDescription = FreeTDS Driver\nDriver = /usr/lib/libtdsodbc.so\nSetup = /usr/lib/libtdsodbc.so\nFileUsage = 1\n" > /etc/odbcinst.ini
|
# Build and install SoftHSM2
|
||||||
|
|
||||||
# build and install SoftHSM2
|
|
||||||
|
|
||||||
RUN git clone https://github.com/opendnssec/SoftHSMv2.git ${SOFTHSM2_SOURCES}
|
RUN git clone https://github.com/opendnssec/SoftHSMv2.git ${SOFTHSM2_SOURCES}
|
||||||
WORKDIR ${SOFTHSM2_SOURCES}
|
WORKDIR ${SOFTHSM2_SOURCES}
|
||||||
|
|
||||||
@ -45,16 +45,18 @@ RUN git checkout ${SOFTHSM2_VERSION} -b ${SOFTHSM2_VERSION} \
|
|||||||
WORKDIR /root
|
WORKDIR /root
|
||||||
RUN rm -fr ${SOFTHSM2_SOURCES}
|
RUN rm -fr ${SOFTHSM2_SOURCES}
|
||||||
|
|
||||||
# install pkcs11-tool
|
# Install pkcs11-tool
|
||||||
RUN apk --update add opensc
|
RUN apt-get install -y opensc
|
||||||
|
|
||||||
RUN softhsm2-util --init-token --slot 0 --label "auth-app" --pin 1234 --so-pin 0000
|
RUN mkdir -p /etc/softhsm2/tokens && \
|
||||||
|
softhsm2-util --init-token --slot 0 --label "auth-app" --pin 1234 --so-pin 0000
|
||||||
|
|
||||||
# ? App setup
|
# ? App setup
|
||||||
|
|
||||||
RUN apk add --no-cache bash curl && curl -1sLf \
|
# Install Infisical CLI
|
||||||
'https://dl.cloudsmith.io/public/infisical/infisical-cli/setup.alpine.sh' | bash \
|
RUN curl -1sLf 'https://dl.cloudsmith.io/public/infisical/infisical-cli/setup.deb.sh' | bash && \
|
||||||
&& apk add infisical=0.8.1 && apk add --no-cache git
|
apt-get update && \
|
||||||
|
apt-get install -y infisical=0.8.1
|
||||||
|
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
|
|
||||||
|
@ -120,4 +120,3 @@ export default {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
2405
backend/package-lock.json
generated
2405
backend/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@ -40,6 +40,7 @@
|
|||||||
"type:check": "tsc --noEmit",
|
"type:check": "tsc --noEmit",
|
||||||
"lint:fix": "eslint --fix --ext js,ts ./src",
|
"lint:fix": "eslint --fix --ext js,ts ./src",
|
||||||
"lint": "eslint 'src/**/*.ts'",
|
"lint": "eslint 'src/**/*.ts'",
|
||||||
|
"test:unit": "vitest run -c vitest.unit.config.ts",
|
||||||
"test:e2e": "vitest run -c vitest.e2e.config.ts --bail=1",
|
"test:e2e": "vitest run -c vitest.e2e.config.ts --bail=1",
|
||||||
"test:e2e-watch": "vitest -c vitest.e2e.config.ts --bail=1",
|
"test:e2e-watch": "vitest -c vitest.e2e.config.ts --bail=1",
|
||||||
"test:e2e-coverage": "vitest run --coverage -c vitest.e2e.config.ts",
|
"test:e2e-coverage": "vitest run --coverage -c vitest.e2e.config.ts",
|
||||||
@ -60,9 +61,17 @@
|
|||||||
"migration:status": "npm run auditlog-migration:status && knex --knexfile ./dist/db/knexfile.mjs --client pg migrate:status",
|
"migration:status": "npm run auditlog-migration:status && knex --knexfile ./dist/db/knexfile.mjs --client pg migrate:status",
|
||||||
"migration:rollback": "npm run auditlog-migration:rollback && knex --knexfile ./dist/db/knexfile.mjs migrate:rollback",
|
"migration:rollback": "npm run auditlog-migration:rollback && knex --knexfile ./dist/db/knexfile.mjs migrate:rollback",
|
||||||
"migration:unlock": "npm run auditlog-migration:unlock && knex --knexfile ./dist/db/knexfile.mjs migrate:unlock",
|
"migration:unlock": "npm run auditlog-migration:unlock && knex --knexfile ./dist/db/knexfile.mjs migrate:unlock",
|
||||||
|
"migration:up-dev": "knex --knexfile ./src/db/knexfile.ts --client pg migrate:up",
|
||||||
|
"migration:down-dev": "knex --knexfile ./src/db/knexfile.ts --client pg migrate:down",
|
||||||
|
"migration:list-dev": "knex --knexfile ./src/db/knexfile.ts --client pg migrate:list",
|
||||||
|
"migration:latest-dev": "knex --knexfile ./src/db/knexfile.ts --client pg migrate:latest",
|
||||||
|
"migration:status-dev": "knex --knexfile ./src/db/knexfile.ts --client pg migrate:status",
|
||||||
|
"migration:rollback-dev": "knex --knexfile ./src/db/knexfile.ts migrate:rollback",
|
||||||
|
"migration:unlock-dev": "knex --knexfile ./src/db/knexfile.ts migrate:unlock",
|
||||||
"migrate:org": "tsx ./scripts/migrate-organization.ts",
|
"migrate:org": "tsx ./scripts/migrate-organization.ts",
|
||||||
"seed:new": "tsx ./scripts/create-seed-file.ts",
|
"seed:new": "tsx ./scripts/create-seed-file.ts",
|
||||||
"seed": "knex --knexfile ./dist/db/knexfile.ts --client pg seed:run",
|
"seed": "knex --knexfile ./dist/db/knexfile.ts --client pg seed:run",
|
||||||
|
"seed-dev": "knex --knexfile ./src/db/knexfile.ts --client pg seed:run",
|
||||||
"db:reset": "npm run migration:rollback -- --all && npm run migration:latest"
|
"db:reset": "npm run migration:rollback -- --all && npm run migration:latest"
|
||||||
},
|
},
|
||||||
"keywords": [],
|
"keywords": [],
|
||||||
@ -138,17 +147,18 @@
|
|||||||
"@fastify/swagger": "^8.14.0",
|
"@fastify/swagger": "^8.14.0",
|
||||||
"@fastify/swagger-ui": "^2.1.0",
|
"@fastify/swagger-ui": "^2.1.0",
|
||||||
"@google-cloud/kms": "^4.5.0",
|
"@google-cloud/kms": "^4.5.0",
|
||||||
"@node-saml/passport-saml": "^4.0.4",
|
"@infisical/quic": "^1.0.8",
|
||||||
|
"@node-saml/passport-saml": "^5.0.1",
|
||||||
"@octokit/auth-app": "^7.1.1",
|
"@octokit/auth-app": "^7.1.1",
|
||||||
"@octokit/plugin-retry": "^5.0.5",
|
"@octokit/plugin-retry": "^5.0.5",
|
||||||
"@octokit/rest": "^20.0.2",
|
"@octokit/rest": "^20.0.2",
|
||||||
"@octokit/webhooks-types": "^7.3.1",
|
"@octokit/webhooks-types": "^7.3.1",
|
||||||
"@octopusdeploy/api-client": "^3.4.1",
|
"@octopusdeploy/api-client": "^3.4.1",
|
||||||
"@opentelemetry/api": "^1.9.0",
|
"@opentelemetry/api": "^1.9.0",
|
||||||
"@opentelemetry/auto-instrumentations-node": "^0.53.0",
|
|
||||||
"@opentelemetry/exporter-metrics-otlp-proto": "^0.55.0",
|
"@opentelemetry/exporter-metrics-otlp-proto": "^0.55.0",
|
||||||
"@opentelemetry/exporter-prometheus": "^0.55.0",
|
"@opentelemetry/exporter-prometheus": "^0.55.0",
|
||||||
"@opentelemetry/instrumentation": "^0.55.0",
|
"@opentelemetry/instrumentation": "^0.55.0",
|
||||||
|
"@opentelemetry/instrumentation-http": "^0.57.2",
|
||||||
"@opentelemetry/resources": "^1.28.0",
|
"@opentelemetry/resources": "^1.28.0",
|
||||||
"@opentelemetry/sdk-metrics": "^1.28.0",
|
"@opentelemetry/sdk-metrics": "^1.28.0",
|
||||||
"@opentelemetry/semantic-conventions": "^1.27.0",
|
"@opentelemetry/semantic-conventions": "^1.27.0",
|
||||||
@ -169,6 +179,7 @@
|
|||||||
"cassandra-driver": "^4.7.2",
|
"cassandra-driver": "^4.7.2",
|
||||||
"connect-redis": "^7.1.1",
|
"connect-redis": "^7.1.1",
|
||||||
"cron": "^3.1.7",
|
"cron": "^3.1.7",
|
||||||
|
"dd-trace": "^5.40.0",
|
||||||
"dotenv": "^16.4.1",
|
"dotenv": "^16.4.1",
|
||||||
"fastify": "^4.28.1",
|
"fastify": "^4.28.1",
|
||||||
"fastify-plugin": "^4.5.1",
|
"fastify-plugin": "^4.5.1",
|
||||||
@ -177,6 +188,7 @@
|
|||||||
"handlebars": "^4.7.8",
|
"handlebars": "^4.7.8",
|
||||||
"hdb": "^0.19.10",
|
"hdb": "^0.19.10",
|
||||||
"ioredis": "^5.3.2",
|
"ioredis": "^5.3.2",
|
||||||
|
"isomorphic-dompurify": "^2.22.0",
|
||||||
"jmespath": "^0.16.0",
|
"jmespath": "^0.16.0",
|
||||||
"jsonwebtoken": "^9.0.2",
|
"jsonwebtoken": "^9.0.2",
|
||||||
"jsrp": "^0.2.4",
|
"jsrp": "^0.2.4",
|
||||||
|
@ -1,7 +0,0 @@
|
|||||||
import "@fastify/request-context";
|
|
||||||
|
|
||||||
declare module "@fastify/request-context" {
|
|
||||||
interface RequestContextData {
|
|
||||||
reqId: string;
|
|
||||||
}
|
|
||||||
}
|
|
8
backend/src/@types/fastify.d.ts
vendored
8
backend/src/@types/fastify.d.ts
vendored
@ -13,6 +13,7 @@ import { TCertificateEstServiceFactory } from "@app/ee/services/certificate-est/
|
|||||||
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 { TExternalKmsServiceFactory } from "@app/ee/services/external-kms/external-kms-service";
|
import { TExternalKmsServiceFactory } from "@app/ee/services/external-kms/external-kms-service";
|
||||||
|
import { TGatewayServiceFactory } from "@app/ee/services/gateway/gateway-service";
|
||||||
import { TGroupServiceFactory } from "@app/ee/services/group/group-service";
|
import { TGroupServiceFactory } from "@app/ee/services/group/group-service";
|
||||||
import { TIdentityProjectAdditionalPrivilegeServiceFactory } from "@app/ee/services/identity-project-additional-privilege/identity-project-additional-privilege-service";
|
import { TIdentityProjectAdditionalPrivilegeServiceFactory } from "@app/ee/services/identity-project-additional-privilege/identity-project-additional-privilege-service";
|
||||||
import { TIdentityProjectAdditionalPrivilegeV2ServiceFactory } from "@app/ee/services/identity-project-additional-privilege-v2/identity-project-additional-privilege-v2-service";
|
import { TIdentityProjectAdditionalPrivilegeV2ServiceFactory } from "@app/ee/services/identity-project-additional-privilege-v2/identity-project-additional-privilege-v2-service";
|
||||||
@ -99,6 +100,12 @@ import { TWorkflowIntegrationServiceFactory } from "@app/services/workflow-integ
|
|||||||
declare module "@fastify/request-context" {
|
declare module "@fastify/request-context" {
|
||||||
interface RequestContextData {
|
interface RequestContextData {
|
||||||
reqId: string;
|
reqId: string;
|
||||||
|
identityAuthInfo?: {
|
||||||
|
identityId: string;
|
||||||
|
oidc?: {
|
||||||
|
claims: Record<string, string>;
|
||||||
|
};
|
||||||
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -228,6 +235,7 @@ declare module "fastify" {
|
|||||||
secretSync: TSecretSyncServiceFactory;
|
secretSync: TSecretSyncServiceFactory;
|
||||||
kmip: TKmipServiceFactory;
|
kmip: TKmipServiceFactory;
|
||||||
kmipOperation: TKmipOperationServiceFactory;
|
kmipOperation: TKmipOperationServiceFactory;
|
||||||
|
gateway: TGatewayServiceFactory;
|
||||||
};
|
};
|
||||||
// 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
|
||||||
|
20
backend/src/@types/knex.d.ts
vendored
20
backend/src/@types/knex.d.ts
vendored
@ -68,6 +68,9 @@ import {
|
|||||||
TExternalKms,
|
TExternalKms,
|
||||||
TExternalKmsInsert,
|
TExternalKmsInsert,
|
||||||
TExternalKmsUpdate,
|
TExternalKmsUpdate,
|
||||||
|
TGateways,
|
||||||
|
TGatewaysInsert,
|
||||||
|
TGatewaysUpdate,
|
||||||
TGitAppInstallSessions,
|
TGitAppInstallSessions,
|
||||||
TGitAppInstallSessionsInsert,
|
TGitAppInstallSessionsInsert,
|
||||||
TGitAppInstallSessionsUpdate,
|
TGitAppInstallSessionsUpdate,
|
||||||
@ -179,6 +182,9 @@ import {
|
|||||||
TOrgBots,
|
TOrgBots,
|
||||||
TOrgBotsInsert,
|
TOrgBotsInsert,
|
||||||
TOrgBotsUpdate,
|
TOrgBotsUpdate,
|
||||||
|
TOrgGatewayConfig,
|
||||||
|
TOrgGatewayConfigInsert,
|
||||||
|
TOrgGatewayConfigUpdate,
|
||||||
TOrgMemberships,
|
TOrgMemberships,
|
||||||
TOrgMembershipsInsert,
|
TOrgMembershipsInsert,
|
||||||
TOrgMembershipsUpdate,
|
TOrgMembershipsUpdate,
|
||||||
@ -200,6 +206,9 @@ import {
|
|||||||
TProjectEnvironments,
|
TProjectEnvironments,
|
||||||
TProjectEnvironmentsInsert,
|
TProjectEnvironmentsInsert,
|
||||||
TProjectEnvironmentsUpdate,
|
TProjectEnvironmentsUpdate,
|
||||||
|
TProjectGateways,
|
||||||
|
TProjectGatewaysInsert,
|
||||||
|
TProjectGatewaysUpdate,
|
||||||
TProjectKeys,
|
TProjectKeys,
|
||||||
TProjectKeysInsert,
|
TProjectKeysInsert,
|
||||||
TProjectKeysUpdate,
|
TProjectKeysUpdate,
|
||||||
@ -930,5 +939,16 @@ declare module "knex/types/tables" {
|
|||||||
TKmipClientCertificatesInsert,
|
TKmipClientCertificatesInsert,
|
||||||
TKmipClientCertificatesUpdate
|
TKmipClientCertificatesUpdate
|
||||||
>;
|
>;
|
||||||
|
[TableName.Gateway]: KnexOriginal.CompositeTableType<TGateways, TGatewaysInsert, TGatewaysUpdate>;
|
||||||
|
[TableName.ProjectGateway]: KnexOriginal.CompositeTableType<
|
||||||
|
TProjectGateways,
|
||||||
|
TProjectGatewaysInsert,
|
||||||
|
TProjectGatewaysUpdate
|
||||||
|
>;
|
||||||
|
[TableName.OrgGatewayConfig]: KnexOriginal.CompositeTableType<
|
||||||
|
TOrgGatewayConfig,
|
||||||
|
TOrgGatewayConfigInsert,
|
||||||
|
TOrgGatewayConfigUpdate
|
||||||
|
>;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -39,7 +39,7 @@ export default {
|
|||||||
},
|
},
|
||||||
migrations: {
|
migrations: {
|
||||||
tableName: "infisical_migrations",
|
tableName: "infisical_migrations",
|
||||||
loadExtensions: [".mjs"]
|
loadExtensions: [".mjs", ".ts"]
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
production: {
|
production: {
|
||||||
@ -64,7 +64,7 @@ export default {
|
|||||||
},
|
},
|
||||||
migrations: {
|
migrations: {
|
||||||
tableName: "infisical_migrations",
|
tableName: "infisical_migrations",
|
||||||
loadExtensions: [".mjs"]
|
loadExtensions: [".mjs", ".ts"]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} as Knex.Config;
|
} as Knex.Config;
|
||||||
|
115
backend/src/db/migrations/20250212191958_create-gateway.ts
Normal file
115
backend/src/db/migrations/20250212191958_create-gateway.ts
Normal file
@ -0,0 +1,115 @@
|
|||||||
|
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.OrgGatewayConfig))) {
|
||||||
|
await knex.schema.createTable(TableName.OrgGatewayConfig, (t) => {
|
||||||
|
t.uuid("id", { primaryKey: true }).defaultTo(knex.fn.uuid());
|
||||||
|
t.string("rootCaKeyAlgorithm").notNullable();
|
||||||
|
|
||||||
|
t.datetime("rootCaIssuedAt").notNullable();
|
||||||
|
t.datetime("rootCaExpiration").notNullable();
|
||||||
|
t.string("rootCaSerialNumber").notNullable();
|
||||||
|
t.binary("encryptedRootCaCertificate").notNullable();
|
||||||
|
t.binary("encryptedRootCaPrivateKey").notNullable();
|
||||||
|
|
||||||
|
t.datetime("clientCaIssuedAt").notNullable();
|
||||||
|
t.datetime("clientCaExpiration").notNullable();
|
||||||
|
t.string("clientCaSerialNumber");
|
||||||
|
t.binary("encryptedClientCaCertificate").notNullable();
|
||||||
|
t.binary("encryptedClientCaPrivateKey").notNullable();
|
||||||
|
|
||||||
|
t.string("clientCertSerialNumber").notNullable();
|
||||||
|
t.string("clientCertKeyAlgorithm").notNullable();
|
||||||
|
t.datetime("clientCertIssuedAt").notNullable();
|
||||||
|
t.datetime("clientCertExpiration").notNullable();
|
||||||
|
t.binary("encryptedClientCertificate").notNullable();
|
||||||
|
t.binary("encryptedClientPrivateKey").notNullable();
|
||||||
|
|
||||||
|
t.datetime("gatewayCaIssuedAt").notNullable();
|
||||||
|
t.datetime("gatewayCaExpiration").notNullable();
|
||||||
|
t.string("gatewayCaSerialNumber").notNullable();
|
||||||
|
t.binary("encryptedGatewayCaCertificate").notNullable();
|
||||||
|
t.binary("encryptedGatewayCaPrivateKey").notNullable();
|
||||||
|
|
||||||
|
t.uuid("orgId").notNullable();
|
||||||
|
t.foreign("orgId").references("id").inTable(TableName.Organization).onDelete("CASCADE");
|
||||||
|
t.unique("orgId");
|
||||||
|
t.timestamps(true, true, true);
|
||||||
|
});
|
||||||
|
|
||||||
|
await createOnUpdateTrigger(knex, TableName.OrgGatewayConfig);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!(await knex.schema.hasTable(TableName.Gateway))) {
|
||||||
|
await knex.schema.createTable(TableName.Gateway, (t) => {
|
||||||
|
t.uuid("id", { primaryKey: true }).defaultTo(knex.fn.uuid());
|
||||||
|
|
||||||
|
t.string("name").notNullable();
|
||||||
|
t.string("serialNumber").notNullable();
|
||||||
|
t.string("keyAlgorithm").notNullable();
|
||||||
|
t.datetime("issuedAt").notNullable();
|
||||||
|
t.datetime("expiration").notNullable();
|
||||||
|
t.datetime("heartbeat");
|
||||||
|
|
||||||
|
t.binary("relayAddress").notNullable();
|
||||||
|
|
||||||
|
t.uuid("orgGatewayRootCaId").notNullable();
|
||||||
|
t.foreign("orgGatewayRootCaId").references("id").inTable(TableName.OrgGatewayConfig).onDelete("CASCADE");
|
||||||
|
|
||||||
|
t.uuid("identityId").notNullable();
|
||||||
|
t.foreign("identityId").references("id").inTable(TableName.Identity).onDelete("CASCADE");
|
||||||
|
|
||||||
|
t.timestamps(true, true, true);
|
||||||
|
});
|
||||||
|
|
||||||
|
await createOnUpdateTrigger(knex, TableName.Gateway);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!(await knex.schema.hasTable(TableName.ProjectGateway))) {
|
||||||
|
await knex.schema.createTable(TableName.ProjectGateway, (t) => {
|
||||||
|
t.uuid("id", { primaryKey: true }).defaultTo(knex.fn.uuid());
|
||||||
|
|
||||||
|
t.string("projectId").notNullable();
|
||||||
|
t.foreign("projectId").references("id").inTable(TableName.Project).onDelete("CASCADE");
|
||||||
|
|
||||||
|
t.uuid("gatewayId").notNullable();
|
||||||
|
t.foreign("gatewayId").references("id").inTable(TableName.Gateway).onDelete("CASCADE");
|
||||||
|
|
||||||
|
t.timestamps(true, true, true);
|
||||||
|
});
|
||||||
|
|
||||||
|
await createOnUpdateTrigger(knex, TableName.ProjectGateway);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (await knex.schema.hasTable(TableName.DynamicSecret)) {
|
||||||
|
const doesGatewayColExist = await knex.schema.hasColumn(TableName.DynamicSecret, "projectGatewayId");
|
||||||
|
await knex.schema.alterTable(TableName.DynamicSecret, (t) => {
|
||||||
|
// not setting a foreign constraint so that cascade effects are not triggered
|
||||||
|
if (!doesGatewayColExist) {
|
||||||
|
t.uuid("projectGatewayId");
|
||||||
|
t.foreign("projectGatewayId").references("id").inTable(TableName.ProjectGateway);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function down(knex: Knex): Promise<void> {
|
||||||
|
if (await knex.schema.hasTable(TableName.DynamicSecret)) {
|
||||||
|
const doesGatewayColExist = await knex.schema.hasColumn(TableName.DynamicSecret, "projectGatewayId");
|
||||||
|
await knex.schema.alterTable(TableName.DynamicSecret, (t) => {
|
||||||
|
if (doesGatewayColExist) t.dropColumn("projectGatewayId");
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
await knex.schema.dropTableIfExists(TableName.ProjectGateway);
|
||||||
|
await dropOnUpdateTrigger(knex, TableName.ProjectGateway);
|
||||||
|
|
||||||
|
await knex.schema.dropTableIfExists(TableName.Gateway);
|
||||||
|
await dropOnUpdateTrigger(knex, TableName.Gateway);
|
||||||
|
|
||||||
|
await knex.schema.dropTableIfExists(TableName.OrgGatewayConfig);
|
||||||
|
await dropOnUpdateTrigger(knex, TableName.OrgGatewayConfig);
|
||||||
|
}
|
25
backend/src/db/migrations/20250226021631_secret-requests.ts
Normal file
25
backend/src/db/migrations/20250226021631_secret-requests.ts
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
import { Knex } from "knex";
|
||||||
|
|
||||||
|
import { SecretSharingType } from "@app/services/secret-sharing/secret-sharing-types";
|
||||||
|
|
||||||
|
import { TableName } from "../schemas";
|
||||||
|
|
||||||
|
export async function up(knex: Knex): Promise<void> {
|
||||||
|
const hasSharingTypeColumn = await knex.schema.hasColumn(TableName.SecretSharing, "type");
|
||||||
|
|
||||||
|
await knex.schema.alterTable(TableName.SecretSharing, (table) => {
|
||||||
|
if (!hasSharingTypeColumn) {
|
||||||
|
table.string("type", 32).defaultTo(SecretSharingType.Share).notNullable();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function down(knex: Knex): Promise<void> {
|
||||||
|
const hasSharingTypeColumn = await knex.schema.hasColumn(TableName.SecretSharing, "type");
|
||||||
|
|
||||||
|
await knex.schema.alterTable(TableName.SecretSharing, (table) => {
|
||||||
|
if (hasSharingTypeColumn) {
|
||||||
|
table.dropColumn("type");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
@ -0,0 +1,31 @@
|
|||||||
|
import { Knex } from "knex";
|
||||||
|
|
||||||
|
import { TableName } from "../schemas";
|
||||||
|
|
||||||
|
export async function up(knex: Knex): Promise<void> {
|
||||||
|
const hasAuthConsentContentCol = await knex.schema.hasColumn(TableName.SuperAdmin, "authConsentContent");
|
||||||
|
const hasPageFrameContentCol = await knex.schema.hasColumn(TableName.SuperAdmin, "pageFrameContent");
|
||||||
|
if (await knex.schema.hasTable(TableName.SuperAdmin)) {
|
||||||
|
await knex.schema.alterTable(TableName.SuperAdmin, (t) => {
|
||||||
|
if (!hasAuthConsentContentCol) {
|
||||||
|
t.text("authConsentContent");
|
||||||
|
}
|
||||||
|
if (!hasPageFrameContentCol) {
|
||||||
|
t.text("pageFrameContent");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function down(knex: Knex): Promise<void> {
|
||||||
|
const hasAuthConsentContentCol = await knex.schema.hasColumn(TableName.SuperAdmin, "authConsentContent");
|
||||||
|
const hasPageFrameContentCol = await knex.schema.hasColumn(TableName.SuperAdmin, "pageFrameContent");
|
||||||
|
await knex.schema.alterTable(TableName.SuperAdmin, (t) => {
|
||||||
|
if (hasAuthConsentContentCol) {
|
||||||
|
t.dropColumn("authConsentContent");
|
||||||
|
}
|
||||||
|
if (hasPageFrameContentCol) {
|
||||||
|
t.dropColumn("pageFrameContent");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
@ -0,0 +1,35 @@
|
|||||||
|
import { Knex } from "knex";
|
||||||
|
|
||||||
|
import { TableName } from "@app/db/schemas";
|
||||||
|
|
||||||
|
export async function up(knex: Knex): Promise<void> {
|
||||||
|
for await (const tableName of [
|
||||||
|
TableName.SecretV2,
|
||||||
|
TableName.SecretVersionV2,
|
||||||
|
TableName.SecretApprovalRequestSecretV2
|
||||||
|
]) {
|
||||||
|
const hasReminderNoteCol = await knex.schema.hasColumn(tableName, "reminderNote");
|
||||||
|
|
||||||
|
if (hasReminderNoteCol) {
|
||||||
|
await knex.schema.alterTable(tableName, (t) => {
|
||||||
|
t.string("reminderNote", 1024).alter();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function down(knex: Knex): Promise<void> {
|
||||||
|
for await (const tableName of [
|
||||||
|
TableName.SecretV2,
|
||||||
|
TableName.SecretVersionV2,
|
||||||
|
TableName.SecretApprovalRequestSecretV2
|
||||||
|
]) {
|
||||||
|
const hasReminderNoteCol = await knex.schema.hasColumn(tableName, "reminderNote");
|
||||||
|
|
||||||
|
if (hasReminderNoteCol) {
|
||||||
|
await knex.schema.alterTable(tableName, (t) => {
|
||||||
|
t.string("reminderNote").alter();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,23 @@
|
|||||||
|
import { Knex } from "knex";
|
||||||
|
|
||||||
|
import { TableName } from "@app/db/schemas";
|
||||||
|
|
||||||
|
export async function up(knex: Knex): Promise<void> {
|
||||||
|
const hasProjectDescription = await knex.schema.hasColumn(TableName.SecretFolder, "description");
|
||||||
|
|
||||||
|
if (!hasProjectDescription) {
|
||||||
|
await knex.schema.alterTable(TableName.SecretFolder, (t) => {
|
||||||
|
t.string("description");
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function down(knex: Knex): Promise<void> {
|
||||||
|
const hasProjectDescription = await knex.schema.hasColumn(TableName.SecretFolder, "description");
|
||||||
|
|
||||||
|
if (hasProjectDescription) {
|
||||||
|
await knex.schema.alterTable(TableName.SecretFolder, (t) => {
|
||||||
|
t.dropColumn("description");
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,19 @@
|
|||||||
|
import { Knex } from "knex";
|
||||||
|
|
||||||
|
import { TableName } from "../schemas";
|
||||||
|
|
||||||
|
export async function up(knex: Knex): Promise<void> {
|
||||||
|
if (!(await knex.schema.hasColumn(TableName.SecretApprovalRequestReviewer, "comment"))) {
|
||||||
|
await knex.schema.alterTable(TableName.SecretApprovalRequestReviewer, (t) => {
|
||||||
|
t.string("comment");
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function down(knex: Knex): Promise<void> {
|
||||||
|
if (await knex.schema.hasColumn(TableName.SecretApprovalRequestReviewer, "comment")) {
|
||||||
|
await knex.schema.alterTable(TableName.SecretApprovalRequestReviewer, (t) => {
|
||||||
|
t.dropColumn("comment");
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,45 @@
|
|||||||
|
import { Knex } from "knex";
|
||||||
|
|
||||||
|
import { TableName } from "@app/db/schemas";
|
||||||
|
|
||||||
|
export async function up(knex: Knex): Promise<void> {
|
||||||
|
if (await knex.schema.hasTable(TableName.SecretVersionV2)) {
|
||||||
|
const hasSecretVersionV2UserActorId = await knex.schema.hasColumn(TableName.SecretVersionV2, "userActorId");
|
||||||
|
const hasSecretVersionV2IdentityActorId = await knex.schema.hasColumn(TableName.SecretVersionV2, "identityActorId");
|
||||||
|
const hasSecretVersionV2ActorType = await knex.schema.hasColumn(TableName.SecretVersionV2, "actorType");
|
||||||
|
|
||||||
|
await knex.schema.alterTable(TableName.SecretVersionV2, (t) => {
|
||||||
|
if (!hasSecretVersionV2UserActorId) {
|
||||||
|
t.uuid("userActorId");
|
||||||
|
t.foreign("userActorId").references("id").inTable(TableName.Users);
|
||||||
|
}
|
||||||
|
if (!hasSecretVersionV2IdentityActorId) {
|
||||||
|
t.uuid("identityActorId");
|
||||||
|
t.foreign("identityActorId").references("id").inTable(TableName.Identity);
|
||||||
|
}
|
||||||
|
if (!hasSecretVersionV2ActorType) {
|
||||||
|
t.string("actorType");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function down(knex: Knex): Promise<void> {
|
||||||
|
if (await knex.schema.hasTable(TableName.SecretVersionV2)) {
|
||||||
|
const hasSecretVersionV2UserActorId = await knex.schema.hasColumn(TableName.SecretVersionV2, "userActorId");
|
||||||
|
const hasSecretVersionV2IdentityActorId = await knex.schema.hasColumn(TableName.SecretVersionV2, "identityActorId");
|
||||||
|
const hasSecretVersionV2ActorType = await knex.schema.hasColumn(TableName.SecretVersionV2, "actorType");
|
||||||
|
|
||||||
|
await knex.schema.alterTable(TableName.SecretVersionV2, (t) => {
|
||||||
|
if (hasSecretVersionV2UserActorId) {
|
||||||
|
t.dropColumn("userActorId");
|
||||||
|
}
|
||||||
|
if (hasSecretVersionV2IdentityActorId) {
|
||||||
|
t.dropColumn("identityActorId");
|
||||||
|
}
|
||||||
|
if (hasSecretVersionV2ActorType) {
|
||||||
|
t.dropColumn("actorType");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,32 @@
|
|||||||
|
import { Knex } from "knex";
|
||||||
|
|
||||||
|
import { TableName } from "../schemas";
|
||||||
|
|
||||||
|
export async function up(knex: Knex): Promise<void> {
|
||||||
|
if (await knex.schema.hasTable(TableName.Organization)) {
|
||||||
|
const hasSecretShareToAnyoneCol = await knex.schema.hasColumn(
|
||||||
|
TableName.Organization,
|
||||||
|
"allowSecretSharingOutsideOrganization"
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!hasSecretShareToAnyoneCol) {
|
||||||
|
await knex.schema.alterTable(TableName.Organization, (t) => {
|
||||||
|
t.boolean("allowSecretSharingOutsideOrganization").defaultTo(true);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function down(knex: Knex): Promise<void> {
|
||||||
|
if (await knex.schema.hasTable(TableName.Organization)) {
|
||||||
|
const hasSecretShareToAnyoneCol = await knex.schema.hasColumn(
|
||||||
|
TableName.Organization,
|
||||||
|
"allowSecretSharingOutsideOrganization"
|
||||||
|
);
|
||||||
|
if (hasSecretShareToAnyoneCol) {
|
||||||
|
await knex.schema.alterTable(TableName.Organization, (t) => {
|
||||||
|
t.dropColumn("allowSecretSharingOutsideOrganization");
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,21 @@
|
|||||||
|
import { Knex } from "knex";
|
||||||
|
|
||||||
|
import { TableName } from "../schemas";
|
||||||
|
|
||||||
|
export async function up(knex: Knex): Promise<void> {
|
||||||
|
const hasMappingField = await knex.schema.hasColumn(TableName.IdentityOidcAuth, "claimMetadataMapping");
|
||||||
|
if (!hasMappingField) {
|
||||||
|
await knex.schema.alterTable(TableName.IdentityOidcAuth, (t) => {
|
||||||
|
t.jsonb("claimMetadataMapping");
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function down(knex: Knex): Promise<void> {
|
||||||
|
const hasMappingField = await knex.schema.hasColumn(TableName.IdentityOidcAuth, "claimMetadataMapping");
|
||||||
|
if (hasMappingField) {
|
||||||
|
await knex.schema.alterTable(TableName.IdentityOidcAuth, (t) => {
|
||||||
|
t.dropColumn("claimMetadataMapping");
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,19 @@
|
|||||||
|
import { Knex } from "knex";
|
||||||
|
|
||||||
|
import { TableName } from "../schemas/models";
|
||||||
|
|
||||||
|
export async function up(knex: Knex): Promise<void> {
|
||||||
|
if (!(await knex.schema.hasColumn(TableName.SuperAdmin, "adminIdentityIds"))) {
|
||||||
|
await knex.schema.alterTable(TableName.SuperAdmin, (t) => {
|
||||||
|
t.specificType("adminIdentityIds", "text[]");
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function down(knex: Knex): Promise<void> {
|
||||||
|
if (await knex.schema.hasColumn(TableName.SuperAdmin, "adminIdentityIds")) {
|
||||||
|
await knex.schema.alterTable(TableName.SuperAdmin, (t) => {
|
||||||
|
t.dropColumn("adminIdentityIds");
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
@ -26,7 +26,8 @@ export const DynamicSecretsSchema = z.object({
|
|||||||
statusDetails: z.string().nullable().optional(),
|
statusDetails: z.string().nullable().optional(),
|
||||||
createdAt: z.date(),
|
createdAt: z.date(),
|
||||||
updatedAt: z.date(),
|
updatedAt: z.date(),
|
||||||
encryptedInput: zodBuffer
|
encryptedInput: zodBuffer,
|
||||||
|
projectGatewayId: z.string().uuid().nullable().optional()
|
||||||
});
|
});
|
||||||
|
|
||||||
export type TDynamicSecrets = z.infer<typeof DynamicSecretsSchema>;
|
export type TDynamicSecrets = z.infer<typeof DynamicSecretsSchema>;
|
||||||
|
29
backend/src/db/schemas/gateways.ts
Normal file
29
backend/src/db/schemas/gateways.ts
Normal file
@ -0,0 +1,29 @@
|
|||||||
|
// 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 GatewaysSchema = z.object({
|
||||||
|
id: z.string().uuid(),
|
||||||
|
name: z.string(),
|
||||||
|
serialNumber: z.string(),
|
||||||
|
keyAlgorithm: z.string(),
|
||||||
|
issuedAt: z.date(),
|
||||||
|
expiration: z.date(),
|
||||||
|
heartbeat: z.date().nullable().optional(),
|
||||||
|
relayAddress: zodBuffer,
|
||||||
|
orgGatewayRootCaId: z.string().uuid(),
|
||||||
|
identityId: z.string().uuid(),
|
||||||
|
createdAt: z.date(),
|
||||||
|
updatedAt: z.date()
|
||||||
|
});
|
||||||
|
|
||||||
|
export type TGateways = z.infer<typeof GatewaysSchema>;
|
||||||
|
export type TGatewaysInsert = Omit<z.input<typeof GatewaysSchema>, TImmutableDBKeys>;
|
||||||
|
export type TGatewaysUpdate = Partial<Omit<z.input<typeof GatewaysSchema>, TImmutableDBKeys>>;
|
@ -26,7 +26,8 @@ export const IdentityOidcAuthsSchema = z.object({
|
|||||||
boundSubject: z.string().nullable().optional(),
|
boundSubject: z.string().nullable().optional(),
|
||||||
createdAt: z.date(),
|
createdAt: z.date(),
|
||||||
updatedAt: z.date(),
|
updatedAt: z.date(),
|
||||||
encryptedCaCertificate: zodBuffer.nullable().optional()
|
encryptedCaCertificate: zodBuffer.nullable().optional(),
|
||||||
|
claimMetadataMapping: z.unknown().nullable().optional()
|
||||||
});
|
});
|
||||||
|
|
||||||
export type TIdentityOidcAuths = z.infer<typeof IdentityOidcAuthsSchema>;
|
export type TIdentityOidcAuths = z.infer<typeof IdentityOidcAuthsSchema>;
|
||||||
|
@ -20,6 +20,7 @@ export * from "./certificates";
|
|||||||
export * from "./dynamic-secret-leases";
|
export * from "./dynamic-secret-leases";
|
||||||
export * from "./dynamic-secrets";
|
export * from "./dynamic-secrets";
|
||||||
export * from "./external-kms";
|
export * from "./external-kms";
|
||||||
|
export * from "./gateways";
|
||||||
export * from "./git-app-install-sessions";
|
export * from "./git-app-install-sessions";
|
||||||
export * from "./git-app-org";
|
export * from "./git-app-org";
|
||||||
export * from "./group-project-membership-roles";
|
export * from "./group-project-membership-roles";
|
||||||
@ -57,6 +58,7 @@ export * from "./ldap-group-maps";
|
|||||||
export * from "./models";
|
export * from "./models";
|
||||||
export * from "./oidc-configs";
|
export * from "./oidc-configs";
|
||||||
export * from "./org-bots";
|
export * from "./org-bots";
|
||||||
|
export * from "./org-gateway-config";
|
||||||
export * from "./org-memberships";
|
export * from "./org-memberships";
|
||||||
export * from "./org-roles";
|
export * from "./org-roles";
|
||||||
export * from "./organizations";
|
export * from "./organizations";
|
||||||
@ -65,6 +67,7 @@ export * from "./pki-collection-items";
|
|||||||
export * from "./pki-collections";
|
export * from "./pki-collections";
|
||||||
export * from "./project-bots";
|
export * from "./project-bots";
|
||||||
export * from "./project-environments";
|
export * from "./project-environments";
|
||||||
|
export * from "./project-gateways";
|
||||||
export * from "./project-keys";
|
export * from "./project-keys";
|
||||||
export * from "./project-memberships";
|
export * from "./project-memberships";
|
||||||
export * from "./project-roles";
|
export * from "./project-roles";
|
||||||
|
@ -113,6 +113,10 @@ export enum TableName {
|
|||||||
SecretApprovalRequestSecretTagV2 = "secret_approval_request_secret_tags_v2",
|
SecretApprovalRequestSecretTagV2 = "secret_approval_request_secret_tags_v2",
|
||||||
SnapshotSecretV2 = "secret_snapshot_secrets_v2",
|
SnapshotSecretV2 = "secret_snapshot_secrets_v2",
|
||||||
ProjectSplitBackfillIds = "project_split_backfill_ids",
|
ProjectSplitBackfillIds = "project_split_backfill_ids",
|
||||||
|
// Gateway
|
||||||
|
OrgGatewayConfig = "org_gateway_config",
|
||||||
|
Gateway = "gateways",
|
||||||
|
ProjectGateway = "project_gateways",
|
||||||
// junction tables with tags
|
// junction tables with tags
|
||||||
SecretV2JnTag = "secret_v2_tag_junction",
|
SecretV2JnTag = "secret_v2_tag_junction",
|
||||||
JnSecretTag = "secret_tag_junction",
|
JnSecretTag = "secret_tag_junction",
|
||||||
|
43
backend/src/db/schemas/org-gateway-config.ts
Normal file
43
backend/src/db/schemas/org-gateway-config.ts
Normal file
@ -0,0 +1,43 @@
|
|||||||
|
// 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 OrgGatewayConfigSchema = z.object({
|
||||||
|
id: z.string().uuid(),
|
||||||
|
rootCaKeyAlgorithm: z.string(),
|
||||||
|
rootCaIssuedAt: z.date(),
|
||||||
|
rootCaExpiration: z.date(),
|
||||||
|
rootCaSerialNumber: z.string(),
|
||||||
|
encryptedRootCaCertificate: zodBuffer,
|
||||||
|
encryptedRootCaPrivateKey: zodBuffer,
|
||||||
|
clientCaIssuedAt: z.date(),
|
||||||
|
clientCaExpiration: z.date(),
|
||||||
|
clientCaSerialNumber: z.string().nullable().optional(),
|
||||||
|
encryptedClientCaCertificate: zodBuffer,
|
||||||
|
encryptedClientCaPrivateKey: zodBuffer,
|
||||||
|
clientCertSerialNumber: z.string(),
|
||||||
|
clientCertKeyAlgorithm: z.string(),
|
||||||
|
clientCertIssuedAt: z.date(),
|
||||||
|
clientCertExpiration: z.date(),
|
||||||
|
encryptedClientCertificate: zodBuffer,
|
||||||
|
encryptedClientPrivateKey: zodBuffer,
|
||||||
|
gatewayCaIssuedAt: z.date(),
|
||||||
|
gatewayCaExpiration: z.date(),
|
||||||
|
gatewayCaSerialNumber: z.string(),
|
||||||
|
encryptedGatewayCaCertificate: zodBuffer,
|
||||||
|
encryptedGatewayCaPrivateKey: zodBuffer,
|
||||||
|
orgId: z.string().uuid(),
|
||||||
|
createdAt: z.date(),
|
||||||
|
updatedAt: z.date()
|
||||||
|
});
|
||||||
|
|
||||||
|
export type TOrgGatewayConfig = z.infer<typeof OrgGatewayConfigSchema>;
|
||||||
|
export type TOrgGatewayConfigInsert = Omit<z.input<typeof OrgGatewayConfigSchema>, TImmutableDBKeys>;
|
||||||
|
export type TOrgGatewayConfigUpdate = Partial<Omit<z.input<typeof OrgGatewayConfigSchema>, TImmutableDBKeys>>;
|
@ -22,7 +22,8 @@ export const OrganizationsSchema = z.object({
|
|||||||
kmsEncryptedDataKey: zodBuffer.nullable().optional(),
|
kmsEncryptedDataKey: zodBuffer.nullable().optional(),
|
||||||
defaultMembershipRole: z.string().default("member"),
|
defaultMembershipRole: z.string().default("member"),
|
||||||
enforceMfa: z.boolean().default(false),
|
enforceMfa: z.boolean().default(false),
|
||||||
selectedMfaMethod: z.string().nullable().optional()
|
selectedMfaMethod: z.string().nullable().optional(),
|
||||||
|
allowSecretSharingOutsideOrganization: z.boolean().default(true).nullable().optional()
|
||||||
});
|
});
|
||||||
|
|
||||||
export type TOrganizations = z.infer<typeof OrganizationsSchema>;
|
export type TOrganizations = z.infer<typeof OrganizationsSchema>;
|
||||||
|
20
backend/src/db/schemas/project-gateways.ts
Normal file
20
backend/src/db/schemas/project-gateways.ts
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
// 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 ProjectGatewaysSchema = z.object({
|
||||||
|
id: z.string().uuid(),
|
||||||
|
projectId: z.string(),
|
||||||
|
gatewayId: z.string().uuid(),
|
||||||
|
createdAt: z.date(),
|
||||||
|
updatedAt: z.date()
|
||||||
|
});
|
||||||
|
|
||||||
|
export type TProjectGateways = z.infer<typeof ProjectGatewaysSchema>;
|
||||||
|
export type TProjectGatewaysInsert = Omit<z.input<typeof ProjectGatewaysSchema>, TImmutableDBKeys>;
|
||||||
|
export type TProjectGatewaysUpdate = Partial<Omit<z.input<typeof ProjectGatewaysSchema>, TImmutableDBKeys>>;
|
@ -13,7 +13,8 @@ export const SecretApprovalRequestsReviewersSchema = z.object({
|
|||||||
requestId: z.string().uuid(),
|
requestId: z.string().uuid(),
|
||||||
createdAt: z.date(),
|
createdAt: z.date(),
|
||||||
updatedAt: z.date(),
|
updatedAt: z.date(),
|
||||||
reviewerUserId: z.string().uuid()
|
reviewerUserId: z.string().uuid(),
|
||||||
|
comment: z.string().nullable().optional()
|
||||||
});
|
});
|
||||||
|
|
||||||
export type TSecretApprovalRequestsReviewers = z.infer<typeof SecretApprovalRequestsReviewersSchema>;
|
export type TSecretApprovalRequestsReviewers = z.infer<typeof SecretApprovalRequestsReviewersSchema>;
|
||||||
|
@ -15,7 +15,8 @@ export const SecretFoldersSchema = z.object({
|
|||||||
updatedAt: z.date(),
|
updatedAt: z.date(),
|
||||||
envId: z.string().uuid(),
|
envId: z.string().uuid(),
|
||||||
parentId: z.string().uuid().nullable().optional(),
|
parentId: z.string().uuid().nullable().optional(),
|
||||||
isReserved: z.boolean().default(false).nullable().optional()
|
isReserved: z.boolean().default(false).nullable().optional(),
|
||||||
|
description: z.string().nullable().optional()
|
||||||
});
|
});
|
||||||
|
|
||||||
export type TSecretFolders = z.infer<typeof SecretFoldersSchema>;
|
export type TSecretFolders = z.infer<typeof SecretFoldersSchema>;
|
||||||
|
@ -26,7 +26,8 @@ export const SecretSharingSchema = z.object({
|
|||||||
lastViewedAt: z.date().nullable().optional(),
|
lastViewedAt: z.date().nullable().optional(),
|
||||||
password: z.string().nullable().optional(),
|
password: z.string().nullable().optional(),
|
||||||
encryptedSecret: zodBuffer.nullable().optional(),
|
encryptedSecret: zodBuffer.nullable().optional(),
|
||||||
identifier: z.string().nullable().optional()
|
identifier: z.string().nullable().optional(),
|
||||||
|
type: z.string().default("share")
|
||||||
});
|
});
|
||||||
|
|
||||||
export type TSecretSharing = z.infer<typeof SecretSharingSchema>;
|
export type TSecretSharing = z.infer<typeof SecretSharingSchema>;
|
||||||
|
@ -25,7 +25,10 @@ export const SecretVersionsV2Schema = z.object({
|
|||||||
folderId: z.string().uuid(),
|
folderId: z.string().uuid(),
|
||||||
userId: z.string().uuid().nullable().optional(),
|
userId: z.string().uuid().nullable().optional(),
|
||||||
createdAt: z.date(),
|
createdAt: z.date(),
|
||||||
updatedAt: z.date()
|
updatedAt: z.date(),
|
||||||
|
userActorId: z.string().uuid().nullable().optional(),
|
||||||
|
identityActorId: z.string().uuid().nullable().optional(),
|
||||||
|
actorType: z.string().nullable().optional()
|
||||||
});
|
});
|
||||||
|
|
||||||
export type TSecretVersionsV2 = z.infer<typeof SecretVersionsV2Schema>;
|
export type TSecretVersionsV2 = z.infer<typeof SecretVersionsV2Schema>;
|
||||||
|
@ -23,7 +23,10 @@ export const SuperAdminSchema = z.object({
|
|||||||
defaultAuthOrgId: z.string().uuid().nullable().optional(),
|
defaultAuthOrgId: z.string().uuid().nullable().optional(),
|
||||||
enabledLoginMethods: z.string().array().nullable().optional(),
|
enabledLoginMethods: z.string().array().nullable().optional(),
|
||||||
encryptedSlackClientId: zodBuffer.nullable().optional(),
|
encryptedSlackClientId: zodBuffer.nullable().optional(),
|
||||||
encryptedSlackClientSecret: zodBuffer.nullable().optional()
|
encryptedSlackClientSecret: zodBuffer.nullable().optional(),
|
||||||
|
authConsentContent: z.string().nullable().optional(),
|
||||||
|
pageFrameContent: z.string().nullable().optional(),
|
||||||
|
adminIdentityIds: z.string().array().nullable().optional()
|
||||||
});
|
});
|
||||||
|
|
||||||
export type TSuperAdmin = z.infer<typeof SuperAdminSchema>;
|
export type TSuperAdmin = z.infer<typeof SuperAdminSchema>;
|
||||||
|
@ -1,10 +1,10 @@
|
|||||||
import ms from "ms";
|
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
|
|
||||||
import { DynamicSecretLeasesSchema } from "@app/db/schemas";
|
import { DynamicSecretLeasesSchema } from "@app/db/schemas";
|
||||||
import { DYNAMIC_SECRET_LEASES } from "@app/lib/api-docs";
|
import { DYNAMIC_SECRET_LEASES } from "@app/lib/api-docs";
|
||||||
import { daysToMillisecond } from "@app/lib/dates";
|
import { daysToMillisecond } from "@app/lib/dates";
|
||||||
import { removeTrailingSlash } from "@app/lib/fn";
|
import { removeTrailingSlash } from "@app/lib/fn";
|
||||||
|
import { ms } from "@app/lib/ms";
|
||||||
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 { SanitizedDynamicSecretSchema } from "@app/server/routes/sanitizedSchemas";
|
import { SanitizedDynamicSecretSchema } from "@app/server/routes/sanitizedSchemas";
|
||||||
|
@ -1,4 +1,3 @@
|
|||||||
import ms from "ms";
|
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
|
|
||||||
import { DynamicSecretLeasesSchema } from "@app/db/schemas";
|
import { DynamicSecretLeasesSchema } from "@app/db/schemas";
|
||||||
@ -6,6 +5,7 @@ import { DynamicSecretProviderSchema } from "@app/ee/services/dynamic-secret/pro
|
|||||||
import { DYNAMIC_SECRETS } from "@app/lib/api-docs";
|
import { DYNAMIC_SECRETS } from "@app/lib/api-docs";
|
||||||
import { daysToMillisecond } from "@app/lib/dates";
|
import { daysToMillisecond } from "@app/lib/dates";
|
||||||
import { removeTrailingSlash } from "@app/lib/fn";
|
import { removeTrailingSlash } from "@app/lib/fn";
|
||||||
|
import { ms } from "@app/lib/ms";
|
||||||
import { readLimit, writeLimit } from "@app/server/config/rateLimiter";
|
import { readLimit, writeLimit } from "@app/server/config/rateLimiter";
|
||||||
import { slugSchema } from "@app/server/lib/schemas";
|
import { slugSchema } from "@app/server/lib/schemas";
|
||||||
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
|
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
|
||||||
|
265
backend/src/ee/routes/v1/gateway-router.ts
Normal file
265
backend/src/ee/routes/v1/gateway-router.ts
Normal file
@ -0,0 +1,265 @@
|
|||||||
|
import { z } from "zod";
|
||||||
|
|
||||||
|
import { GatewaysSchema } from "@app/db/schemas";
|
||||||
|
import { isValidIp } from "@app/lib/ip";
|
||||||
|
import { readLimit, writeLimit } from "@app/server/config/rateLimiter";
|
||||||
|
import { slugSchema } from "@app/server/lib/schemas";
|
||||||
|
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
|
||||||
|
import { AuthMode } from "@app/services/auth/auth-type";
|
||||||
|
|
||||||
|
const SanitizedGatewaySchema = GatewaysSchema.pick({
|
||||||
|
id: true,
|
||||||
|
identityId: true,
|
||||||
|
name: true,
|
||||||
|
createdAt: true,
|
||||||
|
updatedAt: true,
|
||||||
|
issuedAt: true,
|
||||||
|
serialNumber: true,
|
||||||
|
heartbeat: true
|
||||||
|
});
|
||||||
|
|
||||||
|
const isValidRelayAddress = (relayAddress: string) => {
|
||||||
|
const [ip, port] = relayAddress.split(":");
|
||||||
|
return isValidIp(ip) && Number(port) <= 65535 && Number(port) >= 40000;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const registerGatewayRouter = async (server: FastifyZodProvider) => {
|
||||||
|
server.route({
|
||||||
|
method: "POST",
|
||||||
|
url: "/register-identity",
|
||||||
|
config: {
|
||||||
|
rateLimit: writeLimit
|
||||||
|
},
|
||||||
|
schema: {
|
||||||
|
response: {
|
||||||
|
200: z.object({
|
||||||
|
turnServerUsername: z.string(),
|
||||||
|
turnServerPassword: z.string(),
|
||||||
|
turnServerRealm: z.string(),
|
||||||
|
turnServerAddress: z.string(),
|
||||||
|
infisicalStaticIp: z.string().optional()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onRequest: verifyAuth([AuthMode.IDENTITY_ACCESS_TOKEN]),
|
||||||
|
handler: async (req) => {
|
||||||
|
const relayDetails = await server.services.gateway.getGatewayRelayDetails(
|
||||||
|
req.permission.id,
|
||||||
|
req.permission.orgId,
|
||||||
|
req.permission.authMethod
|
||||||
|
);
|
||||||
|
return relayDetails;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
server.route({
|
||||||
|
method: "POST",
|
||||||
|
url: "/exchange-cert",
|
||||||
|
config: {
|
||||||
|
rateLimit: writeLimit
|
||||||
|
},
|
||||||
|
schema: {
|
||||||
|
body: z.object({
|
||||||
|
relayAddress: z.string().refine(isValidRelayAddress, { message: "Invalid relay address" })
|
||||||
|
}),
|
||||||
|
response: {
|
||||||
|
200: z.object({
|
||||||
|
serialNumber: z.string(),
|
||||||
|
privateKey: z.string(),
|
||||||
|
certificate: z.string(),
|
||||||
|
certificateChain: z.string()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onRequest: verifyAuth([AuthMode.IDENTITY_ACCESS_TOKEN]),
|
||||||
|
handler: async (req) => {
|
||||||
|
const gatewayCertificates = await server.services.gateway.exchangeAllocatedRelayAddress({
|
||||||
|
identityOrg: req.permission.orgId,
|
||||||
|
identityId: req.permission.id,
|
||||||
|
relayAddress: req.body.relayAddress,
|
||||||
|
identityOrgAuthMethod: req.permission.authMethod
|
||||||
|
});
|
||||||
|
return gatewayCertificates;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
server.route({
|
||||||
|
method: "POST",
|
||||||
|
url: "/heartbeat",
|
||||||
|
config: {
|
||||||
|
rateLimit: writeLimit
|
||||||
|
},
|
||||||
|
schema: {
|
||||||
|
response: {
|
||||||
|
200: z.object({
|
||||||
|
message: z.string()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onRequest: verifyAuth([AuthMode.IDENTITY_ACCESS_TOKEN]),
|
||||||
|
handler: async (req) => {
|
||||||
|
await server.services.gateway.heartbeat({
|
||||||
|
orgPermission: req.permission
|
||||||
|
});
|
||||||
|
return { message: "Successfully registered heartbeat" };
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
server.route({
|
||||||
|
method: "GET",
|
||||||
|
url: "/",
|
||||||
|
config: {
|
||||||
|
rateLimit: readLimit
|
||||||
|
},
|
||||||
|
schema: {
|
||||||
|
querystring: z.object({
|
||||||
|
projectId: z.string().optional()
|
||||||
|
}),
|
||||||
|
response: {
|
||||||
|
200: z.object({
|
||||||
|
gateways: SanitizedGatewaySchema.extend({
|
||||||
|
identity: z.object({
|
||||||
|
name: z.string(),
|
||||||
|
id: z.string()
|
||||||
|
}),
|
||||||
|
projects: z
|
||||||
|
.object({
|
||||||
|
name: z.string(),
|
||||||
|
id: z.string(),
|
||||||
|
slug: z.string()
|
||||||
|
})
|
||||||
|
.array()
|
||||||
|
}).array()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onRequest: verifyAuth([AuthMode.IDENTITY_ACCESS_TOKEN, AuthMode.JWT]),
|
||||||
|
handler: async (req) => {
|
||||||
|
const gateways = await server.services.gateway.listGateways({
|
||||||
|
orgPermission: req.permission
|
||||||
|
});
|
||||||
|
return { gateways };
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
server.route({
|
||||||
|
method: "GET",
|
||||||
|
url: "/projects/:projectId",
|
||||||
|
config: {
|
||||||
|
rateLimit: readLimit
|
||||||
|
},
|
||||||
|
schema: {
|
||||||
|
params: z.object({
|
||||||
|
projectId: z.string()
|
||||||
|
}),
|
||||||
|
response: {
|
||||||
|
200: z.object({
|
||||||
|
gateways: SanitizedGatewaySchema.extend({
|
||||||
|
identity: z.object({
|
||||||
|
name: z.string(),
|
||||||
|
id: z.string()
|
||||||
|
}),
|
||||||
|
projectGatewayId: z.string()
|
||||||
|
}).array()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onRequest: verifyAuth([AuthMode.IDENTITY_ACCESS_TOKEN, AuthMode.JWT]),
|
||||||
|
handler: async (req) => {
|
||||||
|
const gateways = await server.services.gateway.getProjectGateways({
|
||||||
|
projectId: req.params.projectId,
|
||||||
|
projectPermission: req.permission
|
||||||
|
});
|
||||||
|
return { gateways };
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
server.route({
|
||||||
|
method: "GET",
|
||||||
|
url: "/:id",
|
||||||
|
config: {
|
||||||
|
rateLimit: readLimit
|
||||||
|
},
|
||||||
|
schema: {
|
||||||
|
params: z.object({
|
||||||
|
id: z.string()
|
||||||
|
}),
|
||||||
|
response: {
|
||||||
|
200: z.object({
|
||||||
|
gateway: SanitizedGatewaySchema.extend({
|
||||||
|
identity: z.object({
|
||||||
|
name: z.string(),
|
||||||
|
id: z.string()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onRequest: verifyAuth([AuthMode.IDENTITY_ACCESS_TOKEN, AuthMode.JWT]),
|
||||||
|
handler: async (req) => {
|
||||||
|
const gateway = await server.services.gateway.getGatewayById({
|
||||||
|
orgPermission: req.permission,
|
||||||
|
id: req.params.id
|
||||||
|
});
|
||||||
|
return { gateway };
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
server.route({
|
||||||
|
method: "PATCH",
|
||||||
|
url: "/:id",
|
||||||
|
config: {
|
||||||
|
rateLimit: writeLimit
|
||||||
|
},
|
||||||
|
schema: {
|
||||||
|
params: z.object({
|
||||||
|
id: z.string()
|
||||||
|
}),
|
||||||
|
body: z.object({
|
||||||
|
name: slugSchema({ field: "name" }).optional(),
|
||||||
|
projectIds: z.string().array().optional()
|
||||||
|
}),
|
||||||
|
response: {
|
||||||
|
200: z.object({
|
||||||
|
gateway: SanitizedGatewaySchema
|
||||||
|
})
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onRequest: verifyAuth([AuthMode.IDENTITY_ACCESS_TOKEN, AuthMode.JWT]),
|
||||||
|
handler: async (req) => {
|
||||||
|
const gateway = await server.services.gateway.updateGatewayById({
|
||||||
|
orgPermission: req.permission,
|
||||||
|
id: req.params.id,
|
||||||
|
name: req.body.name,
|
||||||
|
projectIds: req.body.projectIds
|
||||||
|
});
|
||||||
|
return { gateway };
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
server.route({
|
||||||
|
method: "DELETE",
|
||||||
|
url: "/:id",
|
||||||
|
config: {
|
||||||
|
rateLimit: writeLimit
|
||||||
|
},
|
||||||
|
schema: {
|
||||||
|
params: z.object({
|
||||||
|
id: z.string()
|
||||||
|
}),
|
||||||
|
response: {
|
||||||
|
200: z.object({
|
||||||
|
gateway: SanitizedGatewaySchema
|
||||||
|
})
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onRequest: verifyAuth([AuthMode.IDENTITY_ACCESS_TOKEN, AuthMode.JWT]),
|
||||||
|
handler: async (req) => {
|
||||||
|
const gateway = await server.services.gateway.deleteGatewayById({
|
||||||
|
orgPermission: req.permission,
|
||||||
|
id: req.params.id
|
||||||
|
});
|
||||||
|
return { gateway };
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
@ -1,11 +1,11 @@
|
|||||||
import slugify from "@sindresorhus/slugify";
|
import slugify from "@sindresorhus/slugify";
|
||||||
import ms from "ms";
|
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
|
|
||||||
import { IdentityProjectAdditionalPrivilegeTemporaryMode } from "@app/ee/services/identity-project-additional-privilege/identity-project-additional-privilege-types";
|
import { IdentityProjectAdditionalPrivilegeTemporaryMode } from "@app/ee/services/identity-project-additional-privilege/identity-project-additional-privilege-types";
|
||||||
import { backfillPermissionV1SchemaToV2Schema } from "@app/ee/services/permission/project-permission";
|
import { backfillPermissionV1SchemaToV2Schema } from "@app/ee/services/permission/project-permission";
|
||||||
import { IDENTITY_ADDITIONAL_PRIVILEGE } from "@app/lib/api-docs";
|
import { IDENTITY_ADDITIONAL_PRIVILEGE } from "@app/lib/api-docs";
|
||||||
import { UnauthorizedError } from "@app/lib/errors";
|
import { UnauthorizedError } from "@app/lib/errors";
|
||||||
|
import { ms } from "@app/lib/ms";
|
||||||
import { alphaNumericNanoId } from "@app/lib/nanoid";
|
import { alphaNumericNanoId } from "@app/lib/nanoid";
|
||||||
import { readLimit, writeLimit } from "@app/server/config/rateLimiter";
|
import { readLimit, writeLimit } from "@app/server/config/rateLimiter";
|
||||||
import { slugSchema } from "@app/server/lib/schemas";
|
import { slugSchema } from "@app/server/lib/schemas";
|
||||||
|
@ -7,6 +7,7 @@ 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 { registerExternalKmsRouter } from "./external-kms-router";
|
import { registerExternalKmsRouter } from "./external-kms-router";
|
||||||
|
import { registerGatewayRouter } from "./gateway-router";
|
||||||
import { registerGroupRouter } from "./group-router";
|
import { registerGroupRouter } from "./group-router";
|
||||||
import { registerIdentityProjectAdditionalPrivilegeRouter } from "./identity-project-additional-privilege-router";
|
import { registerIdentityProjectAdditionalPrivilegeRouter } from "./identity-project-additional-privilege-router";
|
||||||
import { registerKmipRouter } from "./kmip-router";
|
import { registerKmipRouter } from "./kmip-router";
|
||||||
@ -67,6 +68,8 @@ export const registerV1EERoutes = async (server: FastifyZodProvider) => {
|
|||||||
{ prefix: "/dynamic-secrets" }
|
{ prefix: "/dynamic-secrets" }
|
||||||
);
|
);
|
||||||
|
|
||||||
|
await server.register(registerGatewayRouter, { prefix: "/gateways" });
|
||||||
|
|
||||||
await server.register(
|
await server.register(
|
||||||
async (pkiRouter) => {
|
async (pkiRouter) => {
|
||||||
await pkiRouter.register(registerCaCrlRouter, { prefix: "/crl" });
|
await pkiRouter.register(registerCaCrlRouter, { prefix: "/crl" });
|
||||||
|
@ -1,10 +1,10 @@
|
|||||||
import ms from "ms";
|
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
|
|
||||||
import { KmipClientsSchema } from "@app/db/schemas";
|
import { KmipClientsSchema } 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 { KmipPermission } from "@app/ee/services/kmip/kmip-enum";
|
import { KmipPermission } from "@app/ee/services/kmip/kmip-enum";
|
||||||
import { KmipClientOrderBy } from "@app/ee/services/kmip/kmip-types";
|
import { KmipClientOrderBy } from "@app/ee/services/kmip/kmip-types";
|
||||||
|
import { ms } from "@app/lib/ms";
|
||||||
import { OrderByDirection } from "@app/lib/types";
|
import { OrderByDirection } from "@app/lib/types";
|
||||||
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";
|
||||||
|
@ -25,7 +25,7 @@ type TSAMLConfig = {
|
|||||||
callbackUrl: string;
|
callbackUrl: string;
|
||||||
entryPoint: string;
|
entryPoint: string;
|
||||||
issuer: string;
|
issuer: string;
|
||||||
cert: string;
|
idpCert: string;
|
||||||
audience: string;
|
audience: string;
|
||||||
wantAuthnResponseSigned?: boolean;
|
wantAuthnResponseSigned?: boolean;
|
||||||
wantAssertionsSigned?: boolean;
|
wantAssertionsSigned?: boolean;
|
||||||
@ -72,7 +72,7 @@ export const registerSamlRouter = async (server: FastifyZodProvider) => {
|
|||||||
callbackUrl: `${appCfg.SITE_URL}/api/v1/sso/saml2/${ssoConfig.id}`,
|
callbackUrl: `${appCfg.SITE_URL}/api/v1/sso/saml2/${ssoConfig.id}`,
|
||||||
entryPoint: ssoConfig.entryPoint,
|
entryPoint: ssoConfig.entryPoint,
|
||||||
issuer: ssoConfig.issuer,
|
issuer: ssoConfig.issuer,
|
||||||
cert: ssoConfig.cert,
|
idpCert: ssoConfig.cert,
|
||||||
audience: appCfg.SITE_URL || ""
|
audience: appCfg.SITE_URL || ""
|
||||||
};
|
};
|
||||||
if (ssoConfig.authProvider === SamlProviders.JUMPCLOUD_SAML) {
|
if (ssoConfig.authProvider === SamlProviders.JUMPCLOUD_SAML) {
|
||||||
@ -302,15 +302,21 @@ export const registerSamlRouter = async (server: FastifyZodProvider) => {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
handler: async (req) => {
|
handler: async (req) => {
|
||||||
const saml = await server.services.saml.createSamlCfg({
|
const { isActive, authProvider, issuer, entryPoint, cert } = req.body;
|
||||||
actor: req.permission.type,
|
const { permission } = req;
|
||||||
actorId: req.permission.id,
|
|
||||||
actorAuthMethod: req.permission.authMethod,
|
return server.services.saml.createSamlCfg({
|
||||||
actorOrgId: req.permission.orgId,
|
isActive,
|
||||||
orgId: req.body.organizationId,
|
authProvider,
|
||||||
...req.body
|
issuer,
|
||||||
|
entryPoint,
|
||||||
|
idpCert: cert,
|
||||||
|
actor: permission.type,
|
||||||
|
actorId: permission.id,
|
||||||
|
actorAuthMethod: permission.authMethod,
|
||||||
|
actorOrgId: permission.orgId,
|
||||||
|
orgId: req.body.organizationId
|
||||||
});
|
});
|
||||||
return saml;
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -337,15 +343,21 @@ export const registerSamlRouter = async (server: FastifyZodProvider) => {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
handler: async (req) => {
|
handler: async (req) => {
|
||||||
const saml = await server.services.saml.updateSamlCfg({
|
const { isActive, authProvider, issuer, entryPoint, cert } = req.body;
|
||||||
actor: req.permission.type,
|
const { permission } = req;
|
||||||
actorId: req.permission.id,
|
|
||||||
actorAuthMethod: req.permission.authMethod,
|
return server.services.saml.updateSamlCfg({
|
||||||
actorOrgId: req.permission.orgId,
|
isActive,
|
||||||
orgId: req.body.organizationId,
|
authProvider,
|
||||||
...req.body
|
issuer,
|
||||||
|
entryPoint,
|
||||||
|
idpCert: cert,
|
||||||
|
actor: permission.type,
|
||||||
|
actorId: permission.id,
|
||||||
|
actorAuthMethod: permission.authMethod,
|
||||||
|
actorOrgId: permission.orgId,
|
||||||
|
orgId: req.body.organizationId
|
||||||
});
|
});
|
||||||
return saml;
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
@ -1,16 +1,11 @@
|
|||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
|
|
||||||
import {
|
import { SecretApprovalRequestsReviewersSchema, SecretApprovalRequestsSchema, UsersSchema } from "@app/db/schemas";
|
||||||
SecretApprovalRequestsReviewersSchema,
|
|
||||||
SecretApprovalRequestsSchema,
|
|
||||||
SecretTagsSchema,
|
|
||||||
UsersSchema
|
|
||||||
} 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 { ApprovalStatus, RequestState } from "@app/ee/services/secret-approval-request/secret-approval-request-types";
|
import { ApprovalStatus, RequestState } from "@app/ee/services/secret-approval-request/secret-approval-request-types";
|
||||||
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 { secretRawSchema } from "@app/server/routes/sanitizedSchemas";
|
import { SanitizedTagSchema, secretRawSchema } from "@app/server/routes/sanitizedSchemas";
|
||||||
import { AuthMode } from "@app/services/auth/auth-type";
|
import { AuthMode } from "@app/services/auth/auth-type";
|
||||||
import { ResourceMetadataSchema } from "@app/services/resource-metadata/resource-metadata-schema";
|
import { ResourceMetadataSchema } from "@app/services/resource-metadata/resource-metadata-schema";
|
||||||
|
|
||||||
@ -159,7 +154,8 @@ export const registerSecretApprovalRequestRouter = async (server: FastifyZodProv
|
|||||||
id: z.string()
|
id: z.string()
|
||||||
}),
|
}),
|
||||||
body: z.object({
|
body: z.object({
|
||||||
status: z.enum([ApprovalStatus.APPROVED, ApprovalStatus.REJECTED])
|
status: z.enum([ApprovalStatus.APPROVED, ApprovalStatus.REJECTED]),
|
||||||
|
comment: z.string().optional()
|
||||||
}),
|
}),
|
||||||
response: {
|
response: {
|
||||||
200: z.object({
|
200: z.object({
|
||||||
@ -175,8 +171,25 @@ export const registerSecretApprovalRequestRouter = async (server: FastifyZodProv
|
|||||||
actorAuthMethod: req.permission.authMethod,
|
actorAuthMethod: req.permission.authMethod,
|
||||||
actorOrgId: req.permission.orgId,
|
actorOrgId: req.permission.orgId,
|
||||||
approvalId: req.params.id,
|
approvalId: req.params.id,
|
||||||
status: req.body.status
|
status: req.body.status,
|
||||||
|
comment: req.body.comment
|
||||||
});
|
});
|
||||||
|
|
||||||
|
await server.services.auditLog.createAuditLog({
|
||||||
|
...req.auditLogInfo,
|
||||||
|
orgId: req.permission.orgId,
|
||||||
|
projectId: review.projectId,
|
||||||
|
event: {
|
||||||
|
type: EventType.SECRET_APPROVAL_REQUEST_REVIEW,
|
||||||
|
metadata: {
|
||||||
|
secretApprovalRequestId: review.requestId,
|
||||||
|
reviewedBy: review.reviewerUserId,
|
||||||
|
status: review.status as ApprovalStatus,
|
||||||
|
comment: review.comment || ""
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
return { review };
|
return { review };
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@ -232,15 +245,6 @@ export const registerSecretApprovalRequestRouter = async (server: FastifyZodProv
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
const tagSchema = SecretTagsSchema.pick({
|
|
||||||
id: true,
|
|
||||||
slug: true,
|
|
||||||
name: true,
|
|
||||||
color: true
|
|
||||||
})
|
|
||||||
.array()
|
|
||||||
.optional();
|
|
||||||
|
|
||||||
server.route({
|
server.route({
|
||||||
method: "GET",
|
method: "GET",
|
||||||
url: "/:id",
|
url: "/:id",
|
||||||
@ -268,13 +272,13 @@ export const registerSecretApprovalRequestRouter = async (server: FastifyZodProv
|
|||||||
environment: z.string(),
|
environment: z.string(),
|
||||||
statusChangedByUser: approvalRequestUser.optional(),
|
statusChangedByUser: approvalRequestUser.optional(),
|
||||||
committerUser: approvalRequestUser,
|
committerUser: approvalRequestUser,
|
||||||
reviewers: approvalRequestUser.extend({ status: z.string() }).array(),
|
reviewers: approvalRequestUser.extend({ status: z.string(), comment: z.string().optional() }).array(),
|
||||||
secretPath: z.string(),
|
secretPath: z.string(),
|
||||||
commits: secretRawSchema
|
commits: secretRawSchema
|
||||||
.omit({ _id: true, environment: true, workspace: true, type: true, version: true })
|
.omit({ _id: true, environment: true, workspace: true, type: true, version: true })
|
||||||
.extend({
|
.extend({
|
||||||
op: z.string(),
|
op: z.string(),
|
||||||
tags: tagSchema,
|
tags: SanitizedTagSchema.array().optional(),
|
||||||
secretMetadata: ResourceMetadataSchema.nullish(),
|
secretMetadata: ResourceMetadataSchema.nullish(),
|
||||||
secret: z
|
secret: z
|
||||||
.object({
|
.object({
|
||||||
@ -293,7 +297,7 @@ export const registerSecretApprovalRequestRouter = async (server: FastifyZodProv
|
|||||||
secretKey: z.string(),
|
secretKey: z.string(),
|
||||||
secretValue: z.string().optional(),
|
secretValue: z.string().optional(),
|
||||||
secretComment: z.string().optional(),
|
secretComment: z.string().optional(),
|
||||||
tags: tagSchema,
|
tags: SanitizedTagSchema.array().optional(),
|
||||||
secretMetadata: ResourceMetadataSchema.nullish()
|
secretMetadata: ResourceMetadataSchema.nullish()
|
||||||
})
|
})
|
||||||
.optional()
|
.optional()
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
import z from "zod";
|
import z from "zod";
|
||||||
|
|
||||||
import { ProjectPermissionActions } from "@app/ee/services/permission/project-permission";
|
import { ProjectPermissionSecretActions } from "@app/ee/services/permission/project-permission";
|
||||||
import { RAW_SECRETS } from "@app/lib/api-docs";
|
import { RAW_SECRETS } from "@app/lib/api-docs";
|
||||||
import { removeTrailingSlash } from "@app/lib/fn";
|
import { removeTrailingSlash } from "@app/lib/fn";
|
||||||
import { readLimit } from "@app/server/config/rateLimiter";
|
import { readLimit } from "@app/server/config/rateLimiter";
|
||||||
@ -9,7 +9,7 @@ import { AuthMode } from "@app/services/auth/auth-type";
|
|||||||
|
|
||||||
const AccessListEntrySchema = z
|
const AccessListEntrySchema = z
|
||||||
.object({
|
.object({
|
||||||
allowedActions: z.nativeEnum(ProjectPermissionActions).array(),
|
allowedActions: z.nativeEnum(ProjectPermissionSecretActions).array(),
|
||||||
id: z.string(),
|
id: z.string(),
|
||||||
membershipId: z.string(),
|
membershipId: z.string(),
|
||||||
name: z.string()
|
name: z.string()
|
||||||
|
@ -22,7 +22,11 @@ export const registerSecretVersionRouter = async (server: FastifyZodProvider) =>
|
|||||||
}),
|
}),
|
||||||
response: {
|
response: {
|
||||||
200: z.object({
|
200: z.object({
|
||||||
secretVersions: secretRawSchema.array()
|
secretVersions: secretRawSchema
|
||||||
|
.extend({
|
||||||
|
secretValueHidden: z.boolean()
|
||||||
|
})
|
||||||
|
.array()
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@ -37,6 +41,7 @@ export const registerSecretVersionRouter = async (server: FastifyZodProvider) =>
|
|||||||
offset: req.query.offset,
|
offset: req.query.offset,
|
||||||
secretId: req.params.secretId
|
secretId: req.params.secretId
|
||||||
});
|
});
|
||||||
|
|
||||||
return { secretVersions };
|
return { secretVersions };
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
@ -1,10 +1,10 @@
|
|||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
|
|
||||||
import { SecretSnapshotsSchema, SecretTagsSchema } from "@app/db/schemas";
|
import { SecretSnapshotsSchema } from "@app/db/schemas";
|
||||||
import { PROJECTS } from "@app/lib/api-docs";
|
import { PROJECTS } from "@app/lib/api-docs";
|
||||||
import { readLimit, writeLimit } from "@app/server/config/rateLimiter";
|
import { readLimit, writeLimit } from "@app/server/config/rateLimiter";
|
||||||
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
|
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
|
||||||
import { secretRawSchema } from "@app/server/routes/sanitizedSchemas";
|
import { SanitizedTagSchema, secretRawSchema } from "@app/server/routes/sanitizedSchemas";
|
||||||
import { AuthMode } from "@app/services/auth/auth-type";
|
import { AuthMode } from "@app/services/auth/auth-type";
|
||||||
|
|
||||||
export const registerSnapshotRouter = async (server: FastifyZodProvider) => {
|
export const registerSnapshotRouter = async (server: FastifyZodProvider) => {
|
||||||
@ -31,13 +31,9 @@ export const registerSnapshotRouter = async (server: FastifyZodProvider) => {
|
|||||||
secretVersions: secretRawSchema
|
secretVersions: secretRawSchema
|
||||||
.omit({ _id: true, environment: true, workspace: true, type: true })
|
.omit({ _id: true, environment: true, workspace: true, type: true })
|
||||||
.extend({
|
.extend({
|
||||||
|
secretValueHidden: z.boolean(),
|
||||||
secretId: z.string(),
|
secretId: z.string(),
|
||||||
tags: SecretTagsSchema.pick({
|
tags: SanitizedTagSchema.array()
|
||||||
id: true,
|
|
||||||
slug: true,
|
|
||||||
name: true,
|
|
||||||
color: true
|
|
||||||
}).array()
|
|
||||||
})
|
})
|
||||||
.array(),
|
.array(),
|
||||||
folderVersion: z.object({ id: z.string(), name: z.string() }).array(),
|
folderVersion: z.object({ id: z.string(), name: z.string() }).array(),
|
||||||
@ -56,6 +52,7 @@ export const registerSnapshotRouter = async (server: FastifyZodProvider) => {
|
|||||||
actorOrgId: req.permission.orgId,
|
actorOrgId: req.permission.orgId,
|
||||||
id: req.params.secretSnapshotId
|
id: req.params.secretSnapshotId
|
||||||
});
|
});
|
||||||
|
|
||||||
return { secretSnapshot };
|
return { secretSnapshot };
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
@ -1,9 +1,9 @@
|
|||||||
import ms from "ms";
|
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
|
|
||||||
import { EventType } from "@app/ee/services/audit-log/audit-log-types";
|
import { EventType } from "@app/ee/services/audit-log/audit-log-types";
|
||||||
import { SshCertType } from "@app/ee/services/ssh/ssh-certificate-authority-types";
|
import { SshCertType } from "@app/ee/services/ssh/ssh-certificate-authority-types";
|
||||||
import { SSH_CERTIFICATE_AUTHORITIES } from "@app/lib/api-docs";
|
import { SSH_CERTIFICATE_AUTHORITIES } from "@app/lib/api-docs";
|
||||||
|
import { ms } from "@app/lib/ms";
|
||||||
import { writeLimit } from "@app/server/config/rateLimiter";
|
import { 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";
|
||||||
|
@ -1,5 +1,4 @@
|
|||||||
import slugify from "@sindresorhus/slugify";
|
import slugify from "@sindresorhus/slugify";
|
||||||
import ms from "ms";
|
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
|
|
||||||
import { EventType } from "@app/ee/services/audit-log/audit-log-types";
|
import { EventType } from "@app/ee/services/audit-log/audit-log-types";
|
||||||
@ -10,6 +9,7 @@ import {
|
|||||||
isValidUserPattern
|
isValidUserPattern
|
||||||
} from "@app/ee/services/ssh-certificate-template/ssh-certificate-template-validators";
|
} from "@app/ee/services/ssh-certificate-template/ssh-certificate-template-validators";
|
||||||
import { SSH_CERTIFICATE_TEMPLATES } from "@app/lib/api-docs";
|
import { SSH_CERTIFICATE_TEMPLATES } from "@app/lib/api-docs";
|
||||||
|
import { ms } from "@app/lib/ms";
|
||||||
import { readLimit, writeLimit } from "@app/server/config/rateLimiter";
|
import { readLimit, writeLimit } from "@app/server/config/rateLimiter";
|
||||||
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
|
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
|
||||||
import { AuthMode } from "@app/services/auth/auth-type";
|
import { AuthMode } from "@app/services/auth/auth-type";
|
||||||
|
@ -1,10 +1,11 @@
|
|||||||
import slugify from "@sindresorhus/slugify";
|
import slugify from "@sindresorhus/slugify";
|
||||||
import ms from "ms";
|
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
|
|
||||||
|
import { checkForInvalidPermissionCombination } from "@app/ee/services/permission/permission-fns";
|
||||||
import { ProjectPermissionV2Schema } from "@app/ee/services/permission/project-permission";
|
import { ProjectPermissionV2Schema } from "@app/ee/services/permission/project-permission";
|
||||||
import { ProjectUserAdditionalPrivilegeTemporaryMode } from "@app/ee/services/project-user-additional-privilege/project-user-additional-privilege-types";
|
import { ProjectUserAdditionalPrivilegeTemporaryMode } from "@app/ee/services/project-user-additional-privilege/project-user-additional-privilege-types";
|
||||||
import { PROJECT_USER_ADDITIONAL_PRIVILEGE } from "@app/lib/api-docs";
|
import { PROJECT_USER_ADDITIONAL_PRIVILEGE } from "@app/lib/api-docs";
|
||||||
|
import { ms } from "@app/lib/ms";
|
||||||
import { alphaNumericNanoId } from "@app/lib/nanoid";
|
import { alphaNumericNanoId } from "@app/lib/nanoid";
|
||||||
import { readLimit, writeLimit } from "@app/server/config/rateLimiter";
|
import { readLimit, writeLimit } from "@app/server/config/rateLimiter";
|
||||||
import { slugSchema } from "@app/server/lib/schemas";
|
import { slugSchema } from "@app/server/lib/schemas";
|
||||||
@ -23,7 +24,9 @@ export const registerUserAdditionalPrivilegeRouter = async (server: FastifyZodPr
|
|||||||
body: z.object({
|
body: z.object({
|
||||||
projectMembershipId: z.string().min(1).describe(PROJECT_USER_ADDITIONAL_PRIVILEGE.CREATE.projectMembershipId),
|
projectMembershipId: z.string().min(1).describe(PROJECT_USER_ADDITIONAL_PRIVILEGE.CREATE.projectMembershipId),
|
||||||
slug: slugSchema({ min: 1, max: 60 }).optional().describe(PROJECT_USER_ADDITIONAL_PRIVILEGE.CREATE.slug),
|
slug: slugSchema({ min: 1, max: 60 }).optional().describe(PROJECT_USER_ADDITIONAL_PRIVILEGE.CREATE.slug),
|
||||||
permissions: ProjectPermissionV2Schema.array().describe(PROJECT_USER_ADDITIONAL_PRIVILEGE.CREATE.permissions),
|
permissions: ProjectPermissionV2Schema.array()
|
||||||
|
.describe(PROJECT_USER_ADDITIONAL_PRIVILEGE.CREATE.permissions)
|
||||||
|
.refine(checkForInvalidPermissionCombination),
|
||||||
type: z.discriminatedUnion("isTemporary", [
|
type: z.discriminatedUnion("isTemporary", [
|
||||||
z.object({
|
z.object({
|
||||||
isTemporary: z.literal(false)
|
isTemporary: z.literal(false)
|
||||||
@ -81,7 +84,8 @@ export const registerUserAdditionalPrivilegeRouter = async (server: FastifyZodPr
|
|||||||
slug: slugSchema({ min: 1, max: 60 }).describe(PROJECT_USER_ADDITIONAL_PRIVILEGE.UPDATE.slug),
|
slug: slugSchema({ min: 1, max: 60 }).describe(PROJECT_USER_ADDITIONAL_PRIVILEGE.UPDATE.slug),
|
||||||
permissions: ProjectPermissionV2Schema.array()
|
permissions: ProjectPermissionV2Schema.array()
|
||||||
.optional()
|
.optional()
|
||||||
.describe(PROJECT_USER_ADDITIONAL_PRIVILEGE.UPDATE.permissions),
|
.describe(PROJECT_USER_ADDITIONAL_PRIVILEGE.UPDATE.permissions)
|
||||||
|
.refine(checkForInvalidPermissionCombination),
|
||||||
type: z.discriminatedUnion("isTemporary", [
|
type: z.discriminatedUnion("isTemporary", [
|
||||||
z.object({ isTemporary: z.literal(false).describe(PROJECT_USER_ADDITIONAL_PRIVILEGE.UPDATE.isTemporary) }),
|
z.object({ isTemporary: z.literal(false).describe(PROJECT_USER_ADDITIONAL_PRIVILEGE.UPDATE.isTemporary) }),
|
||||||
z.object({
|
z.object({
|
||||||
|
@ -1,10 +1,11 @@
|
|||||||
import slugify from "@sindresorhus/slugify";
|
import slugify from "@sindresorhus/slugify";
|
||||||
import ms from "ms";
|
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
|
|
||||||
import { IdentityProjectAdditionalPrivilegeTemporaryMode } from "@app/ee/services/identity-project-additional-privilege-v2/identity-project-additional-privilege-v2-types";
|
import { IdentityProjectAdditionalPrivilegeTemporaryMode } from "@app/ee/services/identity-project-additional-privilege-v2/identity-project-additional-privilege-v2-types";
|
||||||
|
import { checkForInvalidPermissionCombination } from "@app/ee/services/permission/permission-fns";
|
||||||
import { ProjectPermissionV2Schema } from "@app/ee/services/permission/project-permission";
|
import { ProjectPermissionV2Schema } from "@app/ee/services/permission/project-permission";
|
||||||
import { IDENTITY_ADDITIONAL_PRIVILEGE_V2 } from "@app/lib/api-docs";
|
import { IDENTITY_ADDITIONAL_PRIVILEGE_V2 } from "@app/lib/api-docs";
|
||||||
|
import { ms } from "@app/lib/ms";
|
||||||
import { alphaNumericNanoId } from "@app/lib/nanoid";
|
import { alphaNumericNanoId } from "@app/lib/nanoid";
|
||||||
import { readLimit, writeLimit } from "@app/server/config/rateLimiter";
|
import { readLimit, writeLimit } from "@app/server/config/rateLimiter";
|
||||||
import { slugSchema } from "@app/server/lib/schemas";
|
import { slugSchema } from "@app/server/lib/schemas";
|
||||||
@ -30,7 +31,9 @@ export const registerIdentityProjectAdditionalPrivilegeRouter = async (server: F
|
|||||||
identityId: z.string().min(1).describe(IDENTITY_ADDITIONAL_PRIVILEGE_V2.CREATE.identityId),
|
identityId: z.string().min(1).describe(IDENTITY_ADDITIONAL_PRIVILEGE_V2.CREATE.identityId),
|
||||||
projectId: z.string().min(1).describe(IDENTITY_ADDITIONAL_PRIVILEGE_V2.CREATE.projectId),
|
projectId: z.string().min(1).describe(IDENTITY_ADDITIONAL_PRIVILEGE_V2.CREATE.projectId),
|
||||||
slug: slugSchema({ min: 1, max: 60 }).optional().describe(IDENTITY_ADDITIONAL_PRIVILEGE_V2.CREATE.slug),
|
slug: slugSchema({ min: 1, max: 60 }).optional().describe(IDENTITY_ADDITIONAL_PRIVILEGE_V2.CREATE.slug),
|
||||||
permissions: ProjectPermissionV2Schema.array().describe(IDENTITY_ADDITIONAL_PRIVILEGE_V2.CREATE.permission),
|
permissions: ProjectPermissionV2Schema.array()
|
||||||
|
.describe(IDENTITY_ADDITIONAL_PRIVILEGE_V2.CREATE.permission)
|
||||||
|
.refine(checkForInvalidPermissionCombination),
|
||||||
type: z.discriminatedUnion("isTemporary", [
|
type: z.discriminatedUnion("isTemporary", [
|
||||||
z.object({
|
z.object({
|
||||||
isTemporary: z.literal(false)
|
isTemporary: z.literal(false)
|
||||||
@ -94,7 +97,8 @@ export const registerIdentityProjectAdditionalPrivilegeRouter = async (server: F
|
|||||||
slug: slugSchema({ min: 1, max: 60 }).describe(IDENTITY_ADDITIONAL_PRIVILEGE_V2.UPDATE.slug),
|
slug: slugSchema({ min: 1, max: 60 }).describe(IDENTITY_ADDITIONAL_PRIVILEGE_V2.UPDATE.slug),
|
||||||
permissions: ProjectPermissionV2Schema.array()
|
permissions: ProjectPermissionV2Schema.array()
|
||||||
.optional()
|
.optional()
|
||||||
.describe(IDENTITY_ADDITIONAL_PRIVILEGE_V2.UPDATE.privilegePermission),
|
.describe(IDENTITY_ADDITIONAL_PRIVILEGE_V2.UPDATE.privilegePermission)
|
||||||
|
.refine(checkForInvalidPermissionCombination),
|
||||||
type: z.discriminatedUnion("isTemporary", [
|
type: z.discriminatedUnion("isTemporary", [
|
||||||
z.object({ isTemporary: z.literal(false).describe(IDENTITY_ADDITIONAL_PRIVILEGE_V2.UPDATE.isTemporary) }),
|
z.object({ isTemporary: z.literal(false).describe(IDENTITY_ADDITIONAL_PRIVILEGE_V2.UPDATE.isTemporary) }),
|
||||||
z.object({
|
z.object({
|
||||||
|
@ -2,6 +2,7 @@ import { packRules } from "@casl/ability/extra";
|
|||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
|
|
||||||
import { ProjectMembershipRole, ProjectRolesSchema } from "@app/db/schemas";
|
import { ProjectMembershipRole, ProjectRolesSchema } from "@app/db/schemas";
|
||||||
|
import { checkForInvalidPermissionCombination } from "@app/ee/services/permission/permission-fns";
|
||||||
import { ProjectPermissionV2Schema } from "@app/ee/services/permission/project-permission";
|
import { ProjectPermissionV2Schema } from "@app/ee/services/permission/project-permission";
|
||||||
import { PROJECT_ROLE } from "@app/lib/api-docs";
|
import { PROJECT_ROLE } from "@app/lib/api-docs";
|
||||||
import { readLimit, writeLimit } from "@app/server/config/rateLimiter";
|
import { readLimit, writeLimit } from "@app/server/config/rateLimiter";
|
||||||
@ -37,7 +38,9 @@ export const registerProjectRoleRouter = async (server: FastifyZodProvider) => {
|
|||||||
.describe(PROJECT_ROLE.CREATE.slug),
|
.describe(PROJECT_ROLE.CREATE.slug),
|
||||||
name: z.string().min(1).trim().describe(PROJECT_ROLE.CREATE.name),
|
name: z.string().min(1).trim().describe(PROJECT_ROLE.CREATE.name),
|
||||||
description: z.string().trim().nullish().describe(PROJECT_ROLE.CREATE.description),
|
description: z.string().trim().nullish().describe(PROJECT_ROLE.CREATE.description),
|
||||||
permissions: ProjectPermissionV2Schema.array().describe(PROJECT_ROLE.CREATE.permissions)
|
permissions: ProjectPermissionV2Schema.array()
|
||||||
|
.describe(PROJECT_ROLE.CREATE.permissions)
|
||||||
|
.refine(checkForInvalidPermissionCombination)
|
||||||
}),
|
}),
|
||||||
response: {
|
response: {
|
||||||
200: z.object({
|
200: z.object({
|
||||||
@ -92,7 +95,10 @@ export const registerProjectRoleRouter = async (server: FastifyZodProvider) => {
|
|||||||
.describe(PROJECT_ROLE.UPDATE.slug),
|
.describe(PROJECT_ROLE.UPDATE.slug),
|
||||||
name: z.string().trim().optional().describe(PROJECT_ROLE.UPDATE.name),
|
name: z.string().trim().optional().describe(PROJECT_ROLE.UPDATE.name),
|
||||||
description: z.string().trim().nullish().describe(PROJECT_ROLE.UPDATE.description),
|
description: z.string().trim().nullish().describe(PROJECT_ROLE.UPDATE.description),
|
||||||
permissions: ProjectPermissionV2Schema.array().describe(PROJECT_ROLE.UPDATE.permissions).optional()
|
permissions: ProjectPermissionV2Schema.array()
|
||||||
|
.describe(PROJECT_ROLE.UPDATE.permissions)
|
||||||
|
.optional()
|
||||||
|
.superRefine(checkForInvalidPermissionCombination)
|
||||||
}),
|
}),
|
||||||
response: {
|
response: {
|
||||||
200: z.object({
|
200: z.object({
|
||||||
|
@ -1,9 +1,10 @@
|
|||||||
import slugify from "@sindresorhus/slugify";
|
import slugify from "@sindresorhus/slugify";
|
||||||
import ms from "ms";
|
import msFn from "ms";
|
||||||
|
|
||||||
import { ActionProjectType, ProjectMembershipRole } from "@app/db/schemas";
|
import { ActionProjectType, ProjectMembershipRole } from "@app/db/schemas";
|
||||||
import { getConfig } from "@app/lib/config/env";
|
import { getConfig } from "@app/lib/config/env";
|
||||||
import { BadRequestError, ForbiddenRequestError, NotFoundError } from "@app/lib/errors";
|
import { BadRequestError, ForbiddenRequestError, NotFoundError } from "@app/lib/errors";
|
||||||
|
import { ms } from "@app/lib/ms";
|
||||||
import { alphaNumericNanoId } from "@app/lib/nanoid";
|
import { alphaNumericNanoId } from "@app/lib/nanoid";
|
||||||
import { TKmsServiceFactory } from "@app/services/kms/kms-service";
|
import { TKmsServiceFactory } from "@app/services/kms/kms-service";
|
||||||
import { TProjectDALFactory } from "@app/services/project/project-dal";
|
import { TProjectDALFactory } from "@app/services/project/project-dal";
|
||||||
@ -246,7 +247,7 @@ export const accessApprovalRequestServiceFactory = ({
|
|||||||
requesterEmail: requestedByUser.email,
|
requesterEmail: requestedByUser.email,
|
||||||
isTemporary,
|
isTemporary,
|
||||||
...(isTemporary && {
|
...(isTemporary && {
|
||||||
expiresIn: ms(ms(temporaryRange || ""), { long: true })
|
expiresIn: msFn(ms(temporaryRange || ""), { long: true })
|
||||||
}),
|
}),
|
||||||
secretPath,
|
secretPath,
|
||||||
environment: envSlug,
|
environment: envSlug,
|
||||||
|
@ -22,6 +22,7 @@ import {
|
|||||||
} from "@app/services/secret-sync/secret-sync-types";
|
} from "@app/services/secret-sync/secret-sync-types";
|
||||||
|
|
||||||
import { KmipPermission } from "../kmip/kmip-enum";
|
import { KmipPermission } from "../kmip/kmip-enum";
|
||||||
|
import { ApprovalStatus } from "../secret-approval-request/secret-approval-request-types";
|
||||||
|
|
||||||
export type TListProjectAuditLogDTO = {
|
export type TListProjectAuditLogDTO = {
|
||||||
filter: {
|
filter: {
|
||||||
@ -165,6 +166,7 @@ export enum EventType {
|
|||||||
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",
|
||||||
|
SECRET_APPROVAL_REQUEST_REVIEW = "secret-approval-request-review",
|
||||||
SIGN_SSH_KEY = "sign-ssh-key",
|
SIGN_SSH_KEY = "sign-ssh-key",
|
||||||
ISSUE_SSH_CREDS = "issue-ssh-creds",
|
ISSUE_SSH_CREDS = "issue-ssh-creds",
|
||||||
CREATE_SSH_CA = "create-ssh-certificate-authority",
|
CREATE_SSH_CA = "create-ssh-certificate-authority",
|
||||||
@ -250,6 +252,7 @@ export enum EventType {
|
|||||||
UPDATE_APP_CONNECTION = "update-app-connection",
|
UPDATE_APP_CONNECTION = "update-app-connection",
|
||||||
DELETE_APP_CONNECTION = "delete-app-connection",
|
DELETE_APP_CONNECTION = "delete-app-connection",
|
||||||
CREATE_SHARED_SECRET = "create-shared-secret",
|
CREATE_SHARED_SECRET = "create-shared-secret",
|
||||||
|
CREATE_SECRET_REQUEST = "create-secret-request",
|
||||||
DELETE_SHARED_SECRET = "delete-shared-secret",
|
DELETE_SHARED_SECRET = "delete-shared-secret",
|
||||||
READ_SHARED_SECRET = "read-shared-secret",
|
READ_SHARED_SECRET = "read-shared-secret",
|
||||||
GET_SECRET_SYNCS = "get-secret-syncs",
|
GET_SECRET_SYNCS = "get-secret-syncs",
|
||||||
@ -975,6 +978,7 @@ interface AddIdentityOidcAuthEvent {
|
|||||||
boundIssuer: string;
|
boundIssuer: string;
|
||||||
boundAudiences: string;
|
boundAudiences: string;
|
||||||
boundClaims: Record<string, string>;
|
boundClaims: Record<string, string>;
|
||||||
|
claimMetadataMapping: Record<string, string>;
|
||||||
boundSubject: string;
|
boundSubject: string;
|
||||||
accessTokenTTL: number;
|
accessTokenTTL: number;
|
||||||
accessTokenMaxTTL: number;
|
accessTokenMaxTTL: number;
|
||||||
@ -999,6 +1003,7 @@ interface UpdateIdentityOidcAuthEvent {
|
|||||||
boundIssuer?: string;
|
boundIssuer?: string;
|
||||||
boundAudiences?: string;
|
boundAudiences?: string;
|
||||||
boundClaims?: Record<string, string>;
|
boundClaims?: Record<string, string>;
|
||||||
|
claimMetadataMapping?: Record<string, string>;
|
||||||
boundSubject?: string;
|
boundSubject?: string;
|
||||||
accessTokenTTL?: number;
|
accessTokenTTL?: number;
|
||||||
accessTokenMaxTTL?: number;
|
accessTokenMaxTTL?: number;
|
||||||
@ -1141,6 +1146,7 @@ interface CreateFolderEvent {
|
|||||||
folderId: string;
|
folderId: string;
|
||||||
folderName: string;
|
folderName: string;
|
||||||
folderPath: string;
|
folderPath: string;
|
||||||
|
description?: string;
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1312,6 +1318,16 @@ interface SecretApprovalRequest {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface SecretApprovalRequestReview {
|
||||||
|
type: EventType.SECRET_APPROVAL_REQUEST_REVIEW;
|
||||||
|
metadata: {
|
||||||
|
secretApprovalRequestId: string;
|
||||||
|
reviewedBy: string;
|
||||||
|
status: ApprovalStatus;
|
||||||
|
comment: string;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
interface SignSshKey {
|
interface SignSshKey {
|
||||||
type: EventType.SIGN_SSH_KEY;
|
type: EventType.SIGN_SSH_KEY;
|
||||||
metadata: {
|
metadata: {
|
||||||
@ -2020,6 +2036,15 @@ interface CreateSharedSecretEvent {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface CreateSecretRequestEvent {
|
||||||
|
type: EventType.CREATE_SECRET_REQUEST;
|
||||||
|
metadata: {
|
||||||
|
id: string;
|
||||||
|
accessType: string;
|
||||||
|
name?: string;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
interface DeleteSharedSecretEvent {
|
interface DeleteSharedSecretEvent {
|
||||||
type: EventType.DELETE_SHARED_SECRET;
|
type: EventType.DELETE_SHARED_SECRET;
|
||||||
metadata: {
|
metadata: {
|
||||||
@ -2470,4 +2495,6 @@ export type Event =
|
|||||||
| KmipOperationActivateEvent
|
| KmipOperationActivateEvent
|
||||||
| KmipOperationRevokeEvent
|
| KmipOperationRevokeEvent
|
||||||
| KmipOperationLocateEvent
|
| KmipOperationLocateEvent
|
||||||
| KmipOperationRegisterEvent;
|
| KmipOperationRegisterEvent
|
||||||
|
| CreateSecretRequestEvent
|
||||||
|
| SecretApprovalRequestReview;
|
||||||
|
@ -1,5 +1,4 @@
|
|||||||
import { ForbiddenError, subject } from "@casl/ability";
|
import { ForbiddenError, subject } from "@casl/ability";
|
||||||
import ms from "ms";
|
|
||||||
|
|
||||||
import { ActionProjectType } from "@app/db/schemas";
|
import { ActionProjectType } from "@app/db/schemas";
|
||||||
import { TLicenseServiceFactory } from "@app/ee/services/license/license-service";
|
import { TLicenseServiceFactory } from "@app/ee/services/license/license-service";
|
||||||
@ -11,6 +10,7 @@ import {
|
|||||||
import { getConfig } from "@app/lib/config/env";
|
import { getConfig } from "@app/lib/config/env";
|
||||||
import { BadRequestError, NotFoundError } from "@app/lib/errors";
|
import { BadRequestError, NotFoundError } from "@app/lib/errors";
|
||||||
import { logger } from "@app/lib/logger";
|
import { logger } from "@app/lib/logger";
|
||||||
|
import { ms } from "@app/lib/ms";
|
||||||
import { TKmsServiceFactory } from "@app/services/kms/kms-service";
|
import { TKmsServiceFactory } from "@app/services/kms/kms-service";
|
||||||
import { KmsDataKey } from "@app/services/kms/kms-types";
|
import { KmsDataKey } from "@app/services/kms/kms-types";
|
||||||
import { TProjectDALFactory } from "@app/services/project/project-dal";
|
import { TProjectDALFactory } from "@app/services/project/project-dal";
|
||||||
|
@ -1,20 +1,31 @@
|
|||||||
|
import crypto from "node:crypto";
|
||||||
|
|
||||||
import { getConfig } from "@app/lib/config/env";
|
import { getConfig } from "@app/lib/config/env";
|
||||||
import { BadRequestError } from "@app/lib/errors";
|
import { BadRequestError } from "@app/lib/errors";
|
||||||
import { getDbConnectionHost } from "@app/lib/knex";
|
import { getDbConnectionHost } from "@app/lib/knex";
|
||||||
|
|
||||||
export const verifyHostInputValidity = (host: string) => {
|
export const verifyHostInputValidity = (host: string, isGateway = false) => {
|
||||||
const appCfg = getConfig();
|
const appCfg = getConfig();
|
||||||
const dbHost = appCfg.DB_HOST || getDbConnectionHost(appCfg.DB_CONNECTION_URI);
|
const dbHost = appCfg.DB_HOST || getDbConnectionHost(appCfg.DB_CONNECTION_URI);
|
||||||
|
// no need for validation when it's dev
|
||||||
|
if (appCfg.NODE_ENV === "development") return;
|
||||||
|
|
||||||
|
if (host === "host.docker.internal") throw new BadRequestError({ message: "Invalid db host" });
|
||||||
|
|
||||||
if (
|
if (
|
||||||
appCfg.isCloud &&
|
appCfg.isCloud &&
|
||||||
|
!isGateway &&
|
||||||
// localhost
|
// localhost
|
||||||
// internal ips
|
// internal ips
|
||||||
(host === "host.docker.internal" || host.match(/^10\.\d+\.\d+\.\d+/) || host.match(/^192\.168\.\d+\.\d+/))
|
(host.match(/^10\.\d+\.\d+\.\d+/) || host.match(/^192\.168\.\d+\.\d+/))
|
||||||
)
|
)
|
||||||
throw new BadRequestError({ message: "Invalid db host" });
|
throw new BadRequestError({ message: "Invalid db host" });
|
||||||
|
|
||||||
if (host === "localhost" || host === "127.0.0.1" || dbHost === host) {
|
if (
|
||||||
|
host === "localhost" ||
|
||||||
|
host === "127.0.0.1" ||
|
||||||
|
(dbHost?.length === host.length && crypto.timingSafeEqual(Buffer.from(dbHost || ""), Buffer.from(host)))
|
||||||
|
) {
|
||||||
throw new BadRequestError({ message: "Invalid db host" });
|
throw new BadRequestError({ message: "Invalid db host" });
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
@ -16,6 +16,7 @@ import { TSecretFolderDALFactory } from "@app/services/secret-folder/secret-fold
|
|||||||
|
|
||||||
import { TDynamicSecretLeaseDALFactory } from "../dynamic-secret-lease/dynamic-secret-lease-dal";
|
import { TDynamicSecretLeaseDALFactory } from "../dynamic-secret-lease/dynamic-secret-lease-dal";
|
||||||
import { TDynamicSecretLeaseQueueServiceFactory } from "../dynamic-secret-lease/dynamic-secret-lease-queue";
|
import { TDynamicSecretLeaseQueueServiceFactory } from "../dynamic-secret-lease/dynamic-secret-lease-queue";
|
||||||
|
import { TProjectGatewayDALFactory } from "../gateway/project-gateway-dal";
|
||||||
import { TDynamicSecretDALFactory } from "./dynamic-secret-dal";
|
import { TDynamicSecretDALFactory } from "./dynamic-secret-dal";
|
||||||
import {
|
import {
|
||||||
DynamicSecretStatus,
|
DynamicSecretStatus,
|
||||||
@ -44,6 +45,7 @@ type TDynamicSecretServiceFactoryDep = {
|
|||||||
projectDAL: Pick<TProjectDALFactory, "findProjectBySlug">;
|
projectDAL: Pick<TProjectDALFactory, "findProjectBySlug">;
|
||||||
permissionService: Pick<TPermissionServiceFactory, "getProjectPermission">;
|
permissionService: Pick<TPermissionServiceFactory, "getProjectPermission">;
|
||||||
kmsService: Pick<TKmsServiceFactory, "createCipherPairWithDataKey">;
|
kmsService: Pick<TKmsServiceFactory, "createCipherPairWithDataKey">;
|
||||||
|
projectGatewayDAL: Pick<TProjectGatewayDALFactory, "findOne">;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type TDynamicSecretServiceFactory = ReturnType<typeof dynamicSecretServiceFactory>;
|
export type TDynamicSecretServiceFactory = ReturnType<typeof dynamicSecretServiceFactory>;
|
||||||
@ -57,7 +59,8 @@ export const dynamicSecretServiceFactory = ({
|
|||||||
permissionService,
|
permissionService,
|
||||||
dynamicSecretQueueService,
|
dynamicSecretQueueService,
|
||||||
projectDAL,
|
projectDAL,
|
||||||
kmsService
|
kmsService,
|
||||||
|
projectGatewayDAL
|
||||||
}: TDynamicSecretServiceFactoryDep) => {
|
}: TDynamicSecretServiceFactoryDep) => {
|
||||||
const create = async ({
|
const create = async ({
|
||||||
path,
|
path,
|
||||||
@ -108,6 +111,18 @@ export const dynamicSecretServiceFactory = ({
|
|||||||
const selectedProvider = dynamicSecretProviders[provider.type];
|
const selectedProvider = dynamicSecretProviders[provider.type];
|
||||||
const inputs = await selectedProvider.validateProviderInputs(provider.inputs);
|
const inputs = await selectedProvider.validateProviderInputs(provider.inputs);
|
||||||
|
|
||||||
|
let selectedGatewayId: string | null = null;
|
||||||
|
if (inputs && typeof inputs === "object" && "projectGatewayId" in inputs && inputs.projectGatewayId) {
|
||||||
|
const projectGatewayId = inputs.projectGatewayId as string;
|
||||||
|
|
||||||
|
const projectGateway = await projectGatewayDAL.findOne({ id: projectGatewayId, projectId });
|
||||||
|
if (!projectGateway)
|
||||||
|
throw new NotFoundError({
|
||||||
|
message: `Project gateway with ${projectGatewayId} not found`
|
||||||
|
});
|
||||||
|
selectedGatewayId = projectGateway.id;
|
||||||
|
}
|
||||||
|
|
||||||
const isConnected = await selectedProvider.validateConnection(provider.inputs);
|
const isConnected = await selectedProvider.validateConnection(provider.inputs);
|
||||||
if (!isConnected) throw new BadRequestError({ message: "Provider connection failed" });
|
if (!isConnected) throw new BadRequestError({ message: "Provider connection failed" });
|
||||||
|
|
||||||
@ -123,7 +138,8 @@ export const dynamicSecretServiceFactory = ({
|
|||||||
maxTTL,
|
maxTTL,
|
||||||
defaultTTL,
|
defaultTTL,
|
||||||
folderId: folder.id,
|
folderId: folder.id,
|
||||||
name
|
name,
|
||||||
|
projectGatewayId: selectedGatewayId
|
||||||
});
|
});
|
||||||
return dynamicSecretCfg;
|
return dynamicSecretCfg;
|
||||||
};
|
};
|
||||||
@ -195,6 +211,23 @@ export const dynamicSecretServiceFactory = ({
|
|||||||
const newInput = { ...decryptedStoredInput, ...(inputs || {}) };
|
const newInput = { ...decryptedStoredInput, ...(inputs || {}) };
|
||||||
const updatedInput = await selectedProvider.validateProviderInputs(newInput);
|
const updatedInput = await selectedProvider.validateProviderInputs(newInput);
|
||||||
|
|
||||||
|
let selectedGatewayId: string | null = null;
|
||||||
|
if (
|
||||||
|
updatedInput &&
|
||||||
|
typeof updatedInput === "object" &&
|
||||||
|
"projectGatewayId" in updatedInput &&
|
||||||
|
updatedInput?.projectGatewayId
|
||||||
|
) {
|
||||||
|
const projectGatewayId = updatedInput.projectGatewayId as string;
|
||||||
|
|
||||||
|
const projectGateway = await projectGatewayDAL.findOne({ id: projectGatewayId, projectId });
|
||||||
|
if (!projectGateway)
|
||||||
|
throw new NotFoundError({
|
||||||
|
message: `Project gateway with ${projectGatewayId} not found`
|
||||||
|
});
|
||||||
|
selectedGatewayId = projectGateway.id;
|
||||||
|
}
|
||||||
|
|
||||||
const isConnected = await selectedProvider.validateConnection(newInput);
|
const isConnected = await selectedProvider.validateConnection(newInput);
|
||||||
if (!isConnected) throw new BadRequestError({ message: "Provider connection failed" });
|
if (!isConnected) throw new BadRequestError({ message: "Provider connection failed" });
|
||||||
|
|
||||||
@ -204,7 +237,8 @@ export const dynamicSecretServiceFactory = ({
|
|||||||
defaultTTL,
|
defaultTTL,
|
||||||
name: newName ?? name,
|
name: newName ?? name,
|
||||||
status: null,
|
status: null,
|
||||||
statusDetails: null
|
statusDetails: null,
|
||||||
|
projectGatewayId: selectedGatewayId
|
||||||
});
|
});
|
||||||
|
|
||||||
return updatedDynamicCfg;
|
return updatedDynamicCfg;
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
import { SnowflakeProvider } from "@app/ee/services/dynamic-secret/providers/snowflake";
|
import { SnowflakeProvider } from "@app/ee/services/dynamic-secret/providers/snowflake";
|
||||||
|
|
||||||
|
import { TGatewayServiceFactory } from "../../gateway/gateway-service";
|
||||||
import { AwsElastiCacheDatabaseProvider } from "./aws-elasticache";
|
import { AwsElastiCacheDatabaseProvider } from "./aws-elasticache";
|
||||||
import { AwsIamProvider } from "./aws-iam";
|
import { AwsIamProvider } from "./aws-iam";
|
||||||
import { AzureEntraIDProvider } from "./azure-entra-id";
|
import { AzureEntraIDProvider } from "./azure-entra-id";
|
||||||
@ -16,8 +17,14 @@ import { SapHanaProvider } from "./sap-hana";
|
|||||||
import { SqlDatabaseProvider } from "./sql-database";
|
import { SqlDatabaseProvider } from "./sql-database";
|
||||||
import { TotpProvider } from "./totp";
|
import { TotpProvider } from "./totp";
|
||||||
|
|
||||||
export const buildDynamicSecretProviders = (): Record<DynamicSecretProviders, TDynamicProviderFns> => ({
|
type TBuildDynamicSecretProviderDTO = {
|
||||||
[DynamicSecretProviders.SqlDatabase]: SqlDatabaseProvider(),
|
gatewayService: Pick<TGatewayServiceFactory, "fnGetGatewayClientTls">;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const buildDynamicSecretProviders = ({
|
||||||
|
gatewayService
|
||||||
|
}: TBuildDynamicSecretProviderDTO): Record<DynamicSecretProviders, TDynamicProviderFns> => ({
|
||||||
|
[DynamicSecretProviders.SqlDatabase]: SqlDatabaseProvider({ gatewayService }),
|
||||||
[DynamicSecretProviders.Cassandra]: CassandraProvider(),
|
[DynamicSecretProviders.Cassandra]: CassandraProvider(),
|
||||||
[DynamicSecretProviders.AwsIam]: AwsIamProvider(),
|
[DynamicSecretProviders.AwsIam]: AwsIamProvider(),
|
||||||
[DynamicSecretProviders.Redis]: RedisDatabaseProvider(),
|
[DynamicSecretProviders.Redis]: RedisDatabaseProvider(),
|
||||||
|
@ -1,5 +1,16 @@
|
|||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
|
|
||||||
|
export type PasswordRequirements = {
|
||||||
|
length: number;
|
||||||
|
required: {
|
||||||
|
lowercase: number;
|
||||||
|
uppercase: number;
|
||||||
|
digits: number;
|
||||||
|
symbols: number;
|
||||||
|
};
|
||||||
|
allowedSymbols?: string;
|
||||||
|
};
|
||||||
|
|
||||||
export enum SqlProviders {
|
export enum SqlProviders {
|
||||||
Postgres = "postgres",
|
Postgres = "postgres",
|
||||||
MySQL = "mysql2",
|
MySQL = "mysql2",
|
||||||
@ -100,10 +111,33 @@ export const DynamicSecretSqlDBSchema = z.object({
|
|||||||
database: z.string().trim(),
|
database: z.string().trim(),
|
||||||
username: z.string().trim(),
|
username: z.string().trim(),
|
||||||
password: z.string().trim(),
|
password: z.string().trim(),
|
||||||
|
passwordRequirements: z
|
||||||
|
.object({
|
||||||
|
length: z.number().min(1).max(250),
|
||||||
|
required: z
|
||||||
|
.object({
|
||||||
|
lowercase: z.number().min(0),
|
||||||
|
uppercase: z.number().min(0),
|
||||||
|
digits: z.number().min(0),
|
||||||
|
symbols: z.number().min(0)
|
||||||
|
})
|
||||||
|
.refine((data) => {
|
||||||
|
const total = Object.values(data).reduce((sum, count) => sum + count, 0);
|
||||||
|
return total <= 250;
|
||||||
|
}, "Sum of required characters cannot exceed 250"),
|
||||||
|
allowedSymbols: z.string().optional()
|
||||||
|
})
|
||||||
|
.refine((data) => {
|
||||||
|
const total = Object.values(data.required).reduce((sum, count) => sum + count, 0);
|
||||||
|
return total <= data.length;
|
||||||
|
}, "Sum of required characters cannot exceed the total length")
|
||||||
|
.optional()
|
||||||
|
.describe("Password generation requirements"),
|
||||||
creationStatement: z.string().trim(),
|
creationStatement: z.string().trim(),
|
||||||
revocationStatement: z.string().trim(),
|
revocationStatement: z.string().trim(),
|
||||||
renewStatement: z.string().trim().optional(),
|
renewStatement: z.string().trim().optional(),
|
||||||
ca: z.string().optional()
|
ca: z.string().optional(),
|
||||||
|
projectGatewayId: z.string().nullable().optional()
|
||||||
});
|
});
|
||||||
|
|
||||||
export const DynamicSecretCassandraSchema = z.object({
|
export const DynamicSecretCassandraSchema = z.object({
|
||||||
|
@ -1,21 +1,106 @@
|
|||||||
|
import { randomInt } from "crypto";
|
||||||
import handlebars from "handlebars";
|
import handlebars from "handlebars";
|
||||||
import knex from "knex";
|
import knex from "knex";
|
||||||
import { customAlphabet } from "nanoid";
|
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
|
|
||||||
|
import { withGatewayProxy } from "@app/lib/gateway";
|
||||||
import { alphaNumericNanoId } from "@app/lib/nanoid";
|
import { alphaNumericNanoId } from "@app/lib/nanoid";
|
||||||
|
|
||||||
|
import { TGatewayServiceFactory } from "../../gateway/gateway-service";
|
||||||
import { verifyHostInputValidity } from "../dynamic-secret-fns";
|
import { verifyHostInputValidity } from "../dynamic-secret-fns";
|
||||||
import { DynamicSecretSqlDBSchema, SqlProviders, TDynamicProviderFns } from "./models";
|
import { DynamicSecretSqlDBSchema, PasswordRequirements, SqlProviders, TDynamicProviderFns } from "./models";
|
||||||
|
|
||||||
const EXTERNAL_REQUEST_TIMEOUT = 10 * 1000;
|
const EXTERNAL_REQUEST_TIMEOUT = 10 * 1000;
|
||||||
|
|
||||||
const generatePassword = (provider: SqlProviders) => {
|
const DEFAULT_PASSWORD_REQUIREMENTS = {
|
||||||
// oracle has limit of 48 password length
|
length: 48,
|
||||||
const size = provider === SqlProviders.Oracle ? 30 : 48;
|
required: {
|
||||||
|
lowercase: 1,
|
||||||
|
uppercase: 1,
|
||||||
|
digits: 1,
|
||||||
|
symbols: 0
|
||||||
|
},
|
||||||
|
allowedSymbols: "-_.~!*"
|
||||||
|
};
|
||||||
|
|
||||||
const charset = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-_.~!*";
|
const ORACLE_PASSWORD_REQUIREMENTS = {
|
||||||
return customAlphabet(charset, 48)(size);
|
...DEFAULT_PASSWORD_REQUIREMENTS,
|
||||||
|
length: 30
|
||||||
|
};
|
||||||
|
|
||||||
|
const generatePassword = (provider: SqlProviders, requirements?: PasswordRequirements) => {
|
||||||
|
const defaultReqs = provider === SqlProviders.Oracle ? ORACLE_PASSWORD_REQUIREMENTS : DEFAULT_PASSWORD_REQUIREMENTS;
|
||||||
|
const finalReqs = requirements || defaultReqs;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const { length, required, allowedSymbols } = finalReqs;
|
||||||
|
|
||||||
|
const chars = {
|
||||||
|
lowercase: "abcdefghijklmnopqrstuvwxyz",
|
||||||
|
uppercase: "ABCDEFGHIJKLMNOPQRSTUVWXYZ",
|
||||||
|
digits: "0123456789",
|
||||||
|
symbols: allowedSymbols || "-_.~!*"
|
||||||
|
};
|
||||||
|
|
||||||
|
const parts: string[] = [];
|
||||||
|
|
||||||
|
if (required.lowercase > 0) {
|
||||||
|
parts.push(
|
||||||
|
...Array(required.lowercase)
|
||||||
|
.fill(0)
|
||||||
|
.map(() => chars.lowercase[randomInt(chars.lowercase.length)])
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (required.uppercase > 0) {
|
||||||
|
parts.push(
|
||||||
|
...Array(required.uppercase)
|
||||||
|
.fill(0)
|
||||||
|
.map(() => chars.uppercase[randomInt(chars.uppercase.length)])
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (required.digits > 0) {
|
||||||
|
parts.push(
|
||||||
|
...Array(required.digits)
|
||||||
|
.fill(0)
|
||||||
|
.map(() => chars.digits[randomInt(chars.digits.length)])
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (required.symbols > 0) {
|
||||||
|
parts.push(
|
||||||
|
...Array(required.symbols)
|
||||||
|
.fill(0)
|
||||||
|
.map(() => chars.symbols[randomInt(chars.symbols.length)])
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const requiredTotal = Object.values(required).reduce<number>((a, b) => a + b, 0);
|
||||||
|
const remainingLength = Math.max(length - requiredTotal, 0);
|
||||||
|
|
||||||
|
const allowedChars = Object.entries(chars)
|
||||||
|
.filter(([key]) => required[key as keyof typeof required] > 0)
|
||||||
|
.map(([, value]) => value)
|
||||||
|
.join("");
|
||||||
|
|
||||||
|
parts.push(
|
||||||
|
...Array(remainingLength)
|
||||||
|
.fill(0)
|
||||||
|
.map(() => allowedChars[randomInt(allowedChars.length)])
|
||||||
|
);
|
||||||
|
|
||||||
|
// shuffle the array to mix up the characters
|
||||||
|
for (let i = parts.length - 1; i > 0; i -= 1) {
|
||||||
|
const j = randomInt(i + 1);
|
||||||
|
[parts[i], parts[j]] = [parts[j], parts[i]];
|
||||||
|
}
|
||||||
|
|
||||||
|
return parts.join("");
|
||||||
|
} catch (error: unknown) {
|
||||||
|
const message = error instanceof Error ? error.message : "Unknown error";
|
||||||
|
throw new Error(`Failed to generate password: ${message}`);
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const generateUsername = (provider: SqlProviders) => {
|
const generateUsername = (provider: SqlProviders) => {
|
||||||
@ -25,10 +110,14 @@ const generateUsername = (provider: SqlProviders) => {
|
|||||||
return alphaNumericNanoId(32);
|
return alphaNumericNanoId(32);
|
||||||
};
|
};
|
||||||
|
|
||||||
export const SqlDatabaseProvider = (): TDynamicProviderFns => {
|
type TSqlDatabaseProviderDTO = {
|
||||||
|
gatewayService: Pick<TGatewayServiceFactory, "fnGetGatewayClientTls">;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const SqlDatabaseProvider = ({ gatewayService }: TSqlDatabaseProviderDTO): TDynamicProviderFns => {
|
||||||
const validateProviderInputs = async (inputs: unknown) => {
|
const validateProviderInputs = async (inputs: unknown) => {
|
||||||
const providerInputs = await DynamicSecretSqlDBSchema.parseAsync(inputs);
|
const providerInputs = await DynamicSecretSqlDBSchema.parseAsync(inputs);
|
||||||
verifyHostInputValidity(providerInputs.host);
|
verifyHostInputValidity(providerInputs.host, Boolean(providerInputs.projectGatewayId));
|
||||||
return providerInputs;
|
return providerInputs;
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -45,7 +134,6 @@ export const SqlDatabaseProvider = (): TDynamicProviderFns => {
|
|||||||
user: providerInputs.username,
|
user: providerInputs.username,
|
||||||
password: providerInputs.password,
|
password: providerInputs.password,
|
||||||
ssl,
|
ssl,
|
||||||
pool: { min: 0, max: 1 },
|
|
||||||
// @ts-expect-error this is because of knexjs type signature issue. This is directly passed to driver
|
// @ts-expect-error this is because of knexjs type signature issue. This is directly passed to driver
|
||||||
// https://github.com/knex/knex/blob/b6507a7129d2b9fafebf5f831494431e64c6a8a0/lib/dialects/mssql/index.js#L66
|
// https://github.com/knex/knex/blob/b6507a7129d2b9fafebf5f831494431e64c6a8a0/lib/dialects/mssql/index.js#L66
|
||||||
// https://github.com/tediousjs/tedious/blob/ebb023ed90969a7ec0e4b036533ad52739d921f7/test/config.ci.ts#L19
|
// https://github.com/tediousjs/tedious/blob/ebb023ed90969a7ec0e4b036533ad52739d921f7/test/config.ci.ts#L19
|
||||||
@ -61,61 +149,112 @@ export const SqlDatabaseProvider = (): TDynamicProviderFns => {
|
|||||||
return db;
|
return db;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const gatewayProxyWrapper = async (
|
||||||
|
providerInputs: z.infer<typeof DynamicSecretSqlDBSchema>,
|
||||||
|
gatewayCallback: (host: string, port: number) => Promise<void>
|
||||||
|
) => {
|
||||||
|
const relayDetails = await gatewayService.fnGetGatewayClientTls(providerInputs.projectGatewayId as string);
|
||||||
|
const [relayHost, relayPort] = relayDetails.relayAddress.split(":");
|
||||||
|
await withGatewayProxy(
|
||||||
|
async (port) => {
|
||||||
|
await gatewayCallback("localhost", port);
|
||||||
|
},
|
||||||
|
{
|
||||||
|
targetHost: providerInputs.host,
|
||||||
|
targetPort: providerInputs.port,
|
||||||
|
relayHost,
|
||||||
|
relayPort: Number(relayPort),
|
||||||
|
identityId: relayDetails.identityId,
|
||||||
|
orgId: relayDetails.orgId,
|
||||||
|
tlsOptions: {
|
||||||
|
ca: relayDetails.certChain,
|
||||||
|
cert: relayDetails.certificate,
|
||||||
|
key: relayDetails.privateKey.toString()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
const validateConnection = async (inputs: unknown) => {
|
const validateConnection = async (inputs: unknown) => {
|
||||||
const providerInputs = await validateProviderInputs(inputs);
|
const providerInputs = await validateProviderInputs(inputs);
|
||||||
const db = await $getClient(providerInputs);
|
let isConnected = false;
|
||||||
// oracle needs from keyword
|
const gatewayCallback = async (host = providerInputs.host, port = providerInputs.port) => {
|
||||||
const testStatement = providerInputs.client === SqlProviders.Oracle ? "SELECT 1 FROM DUAL" : "SELECT 1";
|
const db = await $getClient({ ...providerInputs, port, host });
|
||||||
|
// oracle needs from keyword
|
||||||
|
const testStatement = providerInputs.client === SqlProviders.Oracle ? "SELECT 1 FROM DUAL" : "SELECT 1";
|
||||||
|
|
||||||
const isConnected = await db.raw(testStatement).then(() => true);
|
isConnected = await db.raw(testStatement).then(() => true);
|
||||||
await db.destroy();
|
await db.destroy();
|
||||||
|
};
|
||||||
|
|
||||||
|
if (providerInputs.projectGatewayId) {
|
||||||
|
await gatewayProxyWrapper(providerInputs, gatewayCallback);
|
||||||
|
} else {
|
||||||
|
await gatewayCallback();
|
||||||
|
}
|
||||||
return isConnected;
|
return isConnected;
|
||||||
};
|
};
|
||||||
|
|
||||||
const create = async (inputs: unknown, expireAt: number) => {
|
const create = async (inputs: unknown, expireAt: number) => {
|
||||||
const providerInputs = await validateProviderInputs(inputs);
|
const providerInputs = await validateProviderInputs(inputs);
|
||||||
const db = await $getClient(providerInputs);
|
|
||||||
|
|
||||||
const username = generateUsername(providerInputs.client);
|
const username = generateUsername(providerInputs.client);
|
||||||
const password = generatePassword(providerInputs.client);
|
const password = generatePassword(providerInputs.client, providerInputs.passwordRequirements);
|
||||||
const { database } = providerInputs;
|
const gatewayCallback = async (host = providerInputs.host, port = providerInputs.port) => {
|
||||||
const expiration = new Date(expireAt).toISOString();
|
const db = await $getClient({ ...providerInputs, port, host });
|
||||||
|
try {
|
||||||
|
const { database } = providerInputs;
|
||||||
|
const expiration = new Date(expireAt).toISOString();
|
||||||
|
|
||||||
const creationStatement = handlebars.compile(providerInputs.creationStatement, { noEscape: true })({
|
const creationStatement = handlebars.compile(providerInputs.creationStatement, { noEscape: true })({
|
||||||
username,
|
username,
|
||||||
password,
|
password,
|
||||||
expiration,
|
expiration,
|
||||||
database
|
database
|
||||||
});
|
});
|
||||||
|
|
||||||
const queries = creationStatement.toString().split(";").filter(Boolean);
|
const queries = creationStatement.toString().split(";").filter(Boolean);
|
||||||
await db.transaction(async (tx) => {
|
await db.transaction(async (tx) => {
|
||||||
for (const query of queries) {
|
for (const query of queries) {
|
||||||
// eslint-disable-next-line
|
// eslint-disable-next-line
|
||||||
await tx.raw(query);
|
await tx.raw(query);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} finally {
|
||||||
|
await db.destroy();
|
||||||
}
|
}
|
||||||
});
|
};
|
||||||
await db.destroy();
|
if (providerInputs.projectGatewayId) {
|
||||||
|
await gatewayProxyWrapper(providerInputs, gatewayCallback);
|
||||||
|
} else {
|
||||||
|
await gatewayCallback();
|
||||||
|
}
|
||||||
return { entityId: username, data: { DB_USERNAME: username, DB_PASSWORD: password } };
|
return { entityId: username, data: { DB_USERNAME: username, DB_PASSWORD: password } };
|
||||||
};
|
};
|
||||||
|
|
||||||
const revoke = async (inputs: unknown, entityId: string) => {
|
const revoke = async (inputs: unknown, entityId: string) => {
|
||||||
const providerInputs = await validateProviderInputs(inputs);
|
const providerInputs = await validateProviderInputs(inputs);
|
||||||
const db = await $getClient(providerInputs);
|
|
||||||
|
|
||||||
const username = entityId;
|
const username = entityId;
|
||||||
const { database } = providerInputs;
|
const { database } = providerInputs;
|
||||||
|
const gatewayCallback = async (host = providerInputs.host, port = providerInputs.port) => {
|
||||||
const revokeStatement = handlebars.compile(providerInputs.revocationStatement)({ username, database });
|
const db = await $getClient({ ...providerInputs, port, host });
|
||||||
const queries = revokeStatement.toString().split(";").filter(Boolean);
|
try {
|
||||||
await db.transaction(async (tx) => {
|
const revokeStatement = handlebars.compile(providerInputs.revocationStatement)({ username, database });
|
||||||
for (const query of queries) {
|
const queries = revokeStatement.toString().split(";").filter(Boolean);
|
||||||
// eslint-disable-next-line
|
await db.transaction(async (tx) => {
|
||||||
await tx.raw(query);
|
for (const query of queries) {
|
||||||
|
// eslint-disable-next-line
|
||||||
|
await tx.raw(query);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} finally {
|
||||||
|
await db.destroy();
|
||||||
}
|
}
|
||||||
});
|
};
|
||||||
|
if (providerInputs.projectGatewayId) {
|
||||||
await db.destroy();
|
await gatewayProxyWrapper(providerInputs, gatewayCallback);
|
||||||
|
} else {
|
||||||
|
await gatewayCallback();
|
||||||
|
}
|
||||||
return { entityId: username };
|
return { entityId: username };
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -123,28 +262,35 @@ export const SqlDatabaseProvider = (): TDynamicProviderFns => {
|
|||||||
const providerInputs = await validateProviderInputs(inputs);
|
const providerInputs = await validateProviderInputs(inputs);
|
||||||
if (!providerInputs.renewStatement) return { entityId };
|
if (!providerInputs.renewStatement) return { entityId };
|
||||||
|
|
||||||
const db = await $getClient(providerInputs);
|
const gatewayCallback = async (host = providerInputs.host, port = providerInputs.port) => {
|
||||||
|
const db = await $getClient({ ...providerInputs, port, host });
|
||||||
|
const expiration = new Date(expireAt).toISOString();
|
||||||
|
const { database } = providerInputs;
|
||||||
|
|
||||||
const expiration = new Date(expireAt).toISOString();
|
const renewStatement = handlebars.compile(providerInputs.renewStatement)({
|
||||||
const { database } = providerInputs;
|
username: entityId,
|
||||||
|
expiration,
|
||||||
const renewStatement = handlebars.compile(providerInputs.renewStatement)({
|
database
|
||||||
username: entityId,
|
|
||||||
expiration,
|
|
||||||
database
|
|
||||||
});
|
|
||||||
|
|
||||||
if (renewStatement) {
|
|
||||||
const queries = renewStatement.toString().split(";").filter(Boolean);
|
|
||||||
await db.transaction(async (tx) => {
|
|
||||||
for (const query of queries) {
|
|
||||||
// eslint-disable-next-line
|
|
||||||
await tx.raw(query);
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
try {
|
||||||
|
if (renewStatement) {
|
||||||
|
const queries = renewStatement.toString().split(";").filter(Boolean);
|
||||||
|
await db.transaction(async (tx) => {
|
||||||
|
for (const query of queries) {
|
||||||
|
// eslint-disable-next-line
|
||||||
|
await tx.raw(query);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
await db.destroy();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
if (providerInputs.projectGatewayId) {
|
||||||
|
await gatewayProxyWrapper(providerInputs, gatewayCallback);
|
||||||
|
} else {
|
||||||
|
await gatewayCallback();
|
||||||
}
|
}
|
||||||
|
|
||||||
await db.destroy();
|
|
||||||
return { entityId };
|
return { entityId };
|
||||||
};
|
};
|
||||||
|
|
||||||
|
86
backend/src/ee/services/gateway/gateway-dal.ts
Normal file
86
backend/src/ee/services/gateway/gateway-dal.ts
Normal file
@ -0,0 +1,86 @@
|
|||||||
|
import { Knex } from "knex";
|
||||||
|
|
||||||
|
import { TDbClient } from "@app/db";
|
||||||
|
import { GatewaysSchema, TableName, TGateways } from "@app/db/schemas";
|
||||||
|
import { DatabaseError } from "@app/lib/errors";
|
||||||
|
import {
|
||||||
|
buildFindFilter,
|
||||||
|
ormify,
|
||||||
|
selectAllTableCols,
|
||||||
|
sqlNestRelationships,
|
||||||
|
TFindFilter,
|
||||||
|
TFindOpt
|
||||||
|
} from "@app/lib/knex";
|
||||||
|
|
||||||
|
export type TGatewayDALFactory = ReturnType<typeof gatewayDALFactory>;
|
||||||
|
|
||||||
|
export const gatewayDALFactory = (db: TDbClient) => {
|
||||||
|
const orm = ormify(db, TableName.Gateway);
|
||||||
|
|
||||||
|
const find = async (filter: TFindFilter<TGateways>, { offset, limit, sort, tx }: TFindOpt<TGateways> = {}) => {
|
||||||
|
try {
|
||||||
|
const query = (tx || db)(TableName.Gateway)
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-misused-promises
|
||||||
|
.where(buildFindFilter(filter))
|
||||||
|
.join(TableName.Identity, `${TableName.Identity}.id`, `${TableName.Gateway}.identityId`)
|
||||||
|
.leftJoin(TableName.ProjectGateway, `${TableName.ProjectGateway}.gatewayId`, `${TableName.Gateway}.id`)
|
||||||
|
.leftJoin(TableName.Project, `${TableName.Project}.id`, `${TableName.ProjectGateway}.projectId`)
|
||||||
|
.select(selectAllTableCols(TableName.Gateway))
|
||||||
|
.select(
|
||||||
|
db.ref("name").withSchema(TableName.Identity).as("identityName"),
|
||||||
|
db.ref("name").withSchema(TableName.Project).as("projectName"),
|
||||||
|
db.ref("slug").withSchema(TableName.Project).as("projectSlug"),
|
||||||
|
db.ref("id").withSchema(TableName.Project).as("projectId")
|
||||||
|
);
|
||||||
|
if (limit) void query.limit(limit);
|
||||||
|
if (offset) void query.offset(offset);
|
||||||
|
if (sort) {
|
||||||
|
void query.orderBy(sort.map(([column, order, nulls]) => ({ column: column as string, order, nulls })));
|
||||||
|
}
|
||||||
|
|
||||||
|
const docs = await query;
|
||||||
|
return sqlNestRelationships({
|
||||||
|
data: docs,
|
||||||
|
key: "id",
|
||||||
|
parentMapper: (data) => ({
|
||||||
|
...GatewaysSchema.parse(data),
|
||||||
|
identity: { id: data.identityId, name: data.identityName }
|
||||||
|
}),
|
||||||
|
childrenMapper: [
|
||||||
|
{
|
||||||
|
key: "projectId",
|
||||||
|
label: "projects" as const,
|
||||||
|
mapper: ({ projectId, projectName, projectSlug }) => ({
|
||||||
|
id: projectId,
|
||||||
|
name: projectName,
|
||||||
|
slug: projectSlug
|
||||||
|
})
|
||||||
|
}
|
||||||
|
]
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
throw new DatabaseError({ error, name: `${TableName.Gateway}: Find` });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const findByProjectId = async (projectId: string, tx?: Knex) => {
|
||||||
|
try {
|
||||||
|
const query = (tx || db)(TableName.Gateway)
|
||||||
|
.join(TableName.Identity, `${TableName.Identity}.id`, `${TableName.Gateway}.identityId`)
|
||||||
|
.join(TableName.ProjectGateway, `${TableName.ProjectGateway}.gatewayId`, `${TableName.Gateway}.id`)
|
||||||
|
.select(selectAllTableCols(TableName.Gateway))
|
||||||
|
.select(
|
||||||
|
db.ref("name").withSchema(TableName.Identity).as("identityName"),
|
||||||
|
db.ref("id").withSchema(TableName.ProjectGateway).as("projectGatewayId")
|
||||||
|
)
|
||||||
|
.where({ [`${TableName.ProjectGateway}.projectId` as "projectId"]: projectId });
|
||||||
|
|
||||||
|
const docs = await query;
|
||||||
|
return docs.map((el) => ({ ...el, identity: { id: el.identityId, name: el.identityName } }));
|
||||||
|
} catch (error) {
|
||||||
|
throw new DatabaseError({ error, name: `${TableName.Gateway}: Find by project id` });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return { ...orm, find, findByProjectId };
|
||||||
|
};
|
652
backend/src/ee/services/gateway/gateway-service.ts
Normal file
652
backend/src/ee/services/gateway/gateway-service.ts
Normal file
@ -0,0 +1,652 @@
|
|||||||
|
import crypto from "node:crypto";
|
||||||
|
|
||||||
|
import { ForbiddenError } from "@casl/ability";
|
||||||
|
import * as x509 from "@peculiar/x509";
|
||||||
|
import { z } from "zod";
|
||||||
|
|
||||||
|
import { ActionProjectType } from "@app/db/schemas";
|
||||||
|
import { KeyStorePrefixes, PgSqlLock, TKeyStoreFactory } from "@app/keystore/keystore";
|
||||||
|
import { getConfig } from "@app/lib/config/env";
|
||||||
|
import { BadRequestError, NotFoundError } from "@app/lib/errors";
|
||||||
|
import { pingGatewayAndVerify } from "@app/lib/gateway";
|
||||||
|
import { alphaNumericNanoId } from "@app/lib/nanoid";
|
||||||
|
import { getTurnCredentials } from "@app/lib/turn/credentials";
|
||||||
|
import { ActorAuthMethod, ActorType } from "@app/services/auth/auth-type";
|
||||||
|
import { CertExtendedKeyUsage, CertKeyAlgorithm, CertKeyUsage } from "@app/services/certificate/certificate-types";
|
||||||
|
import {
|
||||||
|
createSerialNumber,
|
||||||
|
keyAlgorithmToAlgCfg
|
||||||
|
} from "@app/services/certificate-authority/certificate-authority-fns";
|
||||||
|
import { TKmsServiceFactory } from "@app/services/kms/kms-service";
|
||||||
|
import { KmsDataKey } from "@app/services/kms/kms-types";
|
||||||
|
|
||||||
|
import { TLicenseServiceFactory } from "../license/license-service";
|
||||||
|
import { OrgPermissionGatewayActions, OrgPermissionSubjects } from "../permission/org-permission";
|
||||||
|
import { TPermissionServiceFactory } from "../permission/permission-service";
|
||||||
|
import { TGatewayDALFactory } from "./gateway-dal";
|
||||||
|
import {
|
||||||
|
TExchangeAllocatedRelayAddressDTO,
|
||||||
|
TGetGatewayByIdDTO,
|
||||||
|
TGetProjectGatewayByIdDTO,
|
||||||
|
THeartBeatDTO,
|
||||||
|
TListGatewaysDTO,
|
||||||
|
TUpdateGatewayByIdDTO
|
||||||
|
} from "./gateway-types";
|
||||||
|
import { TOrgGatewayConfigDALFactory } from "./org-gateway-config-dal";
|
||||||
|
import { TProjectGatewayDALFactory } from "./project-gateway-dal";
|
||||||
|
|
||||||
|
type TGatewayServiceFactoryDep = {
|
||||||
|
gatewayDAL: TGatewayDALFactory;
|
||||||
|
projectGatewayDAL: TProjectGatewayDALFactory;
|
||||||
|
orgGatewayConfigDAL: Pick<TOrgGatewayConfigDALFactory, "findOne" | "create" | "transaction" | "findById">;
|
||||||
|
licenseService: Pick<TLicenseServiceFactory, "onPremFeatures" | "getPlan">;
|
||||||
|
kmsService: Pick<TKmsServiceFactory, "createCipherPairWithDataKey" | "decryptWithRootKey">;
|
||||||
|
permissionService: Pick<TPermissionServiceFactory, "getOrgPermission" | "getProjectPermission">;
|
||||||
|
keyStore: Pick<TKeyStoreFactory, "getItem" | "setItemWithExpiry">;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type TGatewayServiceFactory = ReturnType<typeof gatewayServiceFactory>;
|
||||||
|
const TURN_SERVER_CREDENTIALS_SCHEMA = z.object({
|
||||||
|
username: z.string(),
|
||||||
|
password: z.string()
|
||||||
|
});
|
||||||
|
|
||||||
|
export const gatewayServiceFactory = ({
|
||||||
|
gatewayDAL,
|
||||||
|
licenseService,
|
||||||
|
kmsService,
|
||||||
|
permissionService,
|
||||||
|
orgGatewayConfigDAL,
|
||||||
|
keyStore,
|
||||||
|
projectGatewayDAL
|
||||||
|
}: TGatewayServiceFactoryDep) => {
|
||||||
|
const $validateOrgAccessToGateway = async (orgId: string, actorId: string, actorAuthMethod: ActorAuthMethod) => {
|
||||||
|
// if (!licenseService.onPremFeatures.gateway) {
|
||||||
|
// throw new BadRequestError({
|
||||||
|
// message:
|
||||||
|
// "Gateway handshake failed due to instance plan restrictions. Please upgrade your instance to Infisical's Enterprise plan."
|
||||||
|
// });
|
||||||
|
// }
|
||||||
|
const orgLicensePlan = await licenseService.getPlan(orgId);
|
||||||
|
if (!orgLicensePlan.gateway) {
|
||||||
|
throw new BadRequestError({
|
||||||
|
message:
|
||||||
|
"Gateway handshake failed due to organization plan restrictions. Please upgrade your instance to Infisical's Enterprise plan."
|
||||||
|
});
|
||||||
|
}
|
||||||
|
const { permission } = await permissionService.getOrgPermission(
|
||||||
|
ActorType.IDENTITY,
|
||||||
|
actorId,
|
||||||
|
orgId,
|
||||||
|
actorAuthMethod,
|
||||||
|
orgId
|
||||||
|
);
|
||||||
|
ForbiddenError.from(permission).throwUnlessCan(
|
||||||
|
OrgPermissionGatewayActions.CreateGateways,
|
||||||
|
OrgPermissionSubjects.Gateway
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const getGatewayRelayDetails = async (actorId: string, actorOrgId: string, actorAuthMethod: ActorAuthMethod) => {
|
||||||
|
const TURN_CRED_EXPIRY = 10 * 60; // 10 minutes
|
||||||
|
|
||||||
|
const envCfg = getConfig();
|
||||||
|
await $validateOrgAccessToGateway(actorOrgId, actorId, actorAuthMethod);
|
||||||
|
const { encryptor, decryptor } = await kmsService.createCipherPairWithDataKey({
|
||||||
|
type: KmsDataKey.Organization,
|
||||||
|
orgId: actorOrgId
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!envCfg.GATEWAY_RELAY_AUTH_SECRET || !envCfg.GATEWAY_RELAY_ADDRESS || !envCfg.GATEWAY_RELAY_REALM) {
|
||||||
|
throw new BadRequestError({
|
||||||
|
message: "Gateway handshake failed due to missing instance configuration."
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
let turnServerUsername = "";
|
||||||
|
let turnServerPassword = "";
|
||||||
|
// keep it in redis for 5mins to avoid generating so many credentials
|
||||||
|
const previousCredential = await keyStore.getItem(KeyStorePrefixes.GatewayIdentityCredential(actorId));
|
||||||
|
if (previousCredential) {
|
||||||
|
const el = await TURN_SERVER_CREDENTIALS_SCHEMA.parseAsync(
|
||||||
|
JSON.parse(decryptor({ cipherTextBlob: Buffer.from(previousCredential, "hex") }).toString())
|
||||||
|
);
|
||||||
|
turnServerUsername = el.username;
|
||||||
|
turnServerPassword = el.password;
|
||||||
|
} else {
|
||||||
|
const el = getTurnCredentials(actorId, envCfg.GATEWAY_RELAY_AUTH_SECRET);
|
||||||
|
await keyStore.setItemWithExpiry(
|
||||||
|
KeyStorePrefixes.GatewayIdentityCredential(actorId),
|
||||||
|
TURN_CRED_EXPIRY,
|
||||||
|
encryptor({
|
||||||
|
plainText: Buffer.from(JSON.stringify({ username: el.username, password: el.password }))
|
||||||
|
}).cipherTextBlob.toString("hex")
|
||||||
|
);
|
||||||
|
turnServerUsername = el.username;
|
||||||
|
turnServerPassword = el.password;
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
turnServerUsername,
|
||||||
|
turnServerPassword,
|
||||||
|
turnServerRealm: envCfg.GATEWAY_RELAY_REALM,
|
||||||
|
turnServerAddress: envCfg.GATEWAY_RELAY_ADDRESS,
|
||||||
|
infisicalStaticIp: envCfg.GATEWAY_INFISICAL_STATIC_IP_ADDRESS
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
const exchangeAllocatedRelayAddress = async ({
|
||||||
|
identityId,
|
||||||
|
identityOrg,
|
||||||
|
relayAddress,
|
||||||
|
identityOrgAuthMethod
|
||||||
|
}: TExchangeAllocatedRelayAddressDTO) => {
|
||||||
|
await $validateOrgAccessToGateway(identityOrg, identityId, identityOrgAuthMethod);
|
||||||
|
const { encryptor: orgKmsEncryptor, decryptor: orgKmsDecryptor } = await kmsService.createCipherPairWithDataKey({
|
||||||
|
type: KmsDataKey.Organization,
|
||||||
|
orgId: identityOrg
|
||||||
|
});
|
||||||
|
|
||||||
|
const orgGatewayConfig = await orgGatewayConfigDAL.transaction(async (tx) => {
|
||||||
|
await tx.raw("SELECT pg_advisory_xact_lock(?)", [PgSqlLock.OrgGatewayRootCaInit(identityOrg)]);
|
||||||
|
const existingGatewayConfig = await orgGatewayConfigDAL.findOne({ orgId: identityOrg });
|
||||||
|
if (existingGatewayConfig) return existingGatewayConfig;
|
||||||
|
|
||||||
|
const alg = keyAlgorithmToAlgCfg(CertKeyAlgorithm.RSA_2048);
|
||||||
|
// generate root CA
|
||||||
|
const rootCaKeys = await crypto.subtle.generateKey(alg, true, ["sign", "verify"]);
|
||||||
|
const rootCaSerialNumber = createSerialNumber();
|
||||||
|
const rootCaSkObj = crypto.KeyObject.from(rootCaKeys.privateKey);
|
||||||
|
const rootCaIssuedAt = new Date();
|
||||||
|
const rootCaKeyAlgorithm = CertKeyAlgorithm.RSA_2048;
|
||||||
|
const rootCaExpiration = new Date(new Date().setFullYear(2045));
|
||||||
|
const rootCaCert = await x509.X509CertificateGenerator.createSelfSigned({
|
||||||
|
name: `O=${identityOrg},CN=Infisical Gateway Root CA`,
|
||||||
|
serialNumber: rootCaSerialNumber,
|
||||||
|
notBefore: rootCaIssuedAt,
|
||||||
|
notAfter: rootCaExpiration,
|
||||||
|
signingAlgorithm: alg,
|
||||||
|
keys: rootCaKeys,
|
||||||
|
extensions: [
|
||||||
|
// eslint-disable-next-line no-bitwise
|
||||||
|
new x509.KeyUsagesExtension(x509.KeyUsageFlags.keyCertSign | x509.KeyUsageFlags.cRLSign, true),
|
||||||
|
await x509.SubjectKeyIdentifierExtension.create(rootCaKeys.publicKey)
|
||||||
|
]
|
||||||
|
});
|
||||||
|
|
||||||
|
// generate client ca
|
||||||
|
const clientCaSerialNumber = createSerialNumber();
|
||||||
|
const clientCaIssuedAt = new Date();
|
||||||
|
const clientCaExpiration = new Date(new Date().setFullYear(2045));
|
||||||
|
const clientCaKeys = await crypto.subtle.generateKey(alg, true, ["sign", "verify"]);
|
||||||
|
const clientCaSkObj = crypto.KeyObject.from(clientCaKeys.privateKey);
|
||||||
|
|
||||||
|
const clientCaCert = await x509.X509CertificateGenerator.create({
|
||||||
|
serialNumber: clientCaSerialNumber,
|
||||||
|
subject: `O=${identityOrg},CN=Client Intermediate CA`,
|
||||||
|
issuer: rootCaCert.subject,
|
||||||
|
notBefore: clientCaIssuedAt,
|
||||||
|
notAfter: clientCaExpiration,
|
||||||
|
signingKey: rootCaKeys.privateKey,
|
||||||
|
publicKey: clientCaKeys.publicKey,
|
||||||
|
signingAlgorithm: alg,
|
||||||
|
extensions: [
|
||||||
|
new x509.KeyUsagesExtension(
|
||||||
|
// eslint-disable-next-line no-bitwise
|
||||||
|
x509.KeyUsageFlags.keyCertSign |
|
||||||
|
x509.KeyUsageFlags.cRLSign |
|
||||||
|
x509.KeyUsageFlags.digitalSignature |
|
||||||
|
x509.KeyUsageFlags.keyEncipherment,
|
||||||
|
true
|
||||||
|
),
|
||||||
|
new x509.BasicConstraintsExtension(true, 0, true),
|
||||||
|
await x509.AuthorityKeyIdentifierExtension.create(rootCaCert, false),
|
||||||
|
await x509.SubjectKeyIdentifierExtension.create(clientCaKeys.publicKey)
|
||||||
|
]
|
||||||
|
});
|
||||||
|
|
||||||
|
const clientKeys = await crypto.subtle.generateKey(alg, true, ["sign", "verify"]);
|
||||||
|
const clientCertSerialNumber = createSerialNumber();
|
||||||
|
const clientCert = await x509.X509CertificateGenerator.create({
|
||||||
|
serialNumber: clientCertSerialNumber,
|
||||||
|
subject: `O=${identityOrg},OU=gateway-client,CN=cloud`,
|
||||||
|
issuer: clientCaCert.subject,
|
||||||
|
notAfter: clientCaExpiration,
|
||||||
|
notBefore: clientCaIssuedAt,
|
||||||
|
signingKey: clientCaKeys.privateKey,
|
||||||
|
publicKey: clientKeys.publicKey,
|
||||||
|
signingAlgorithm: alg,
|
||||||
|
extensions: [
|
||||||
|
new x509.BasicConstraintsExtension(false),
|
||||||
|
await x509.AuthorityKeyIdentifierExtension.create(clientCaCert, false),
|
||||||
|
await x509.SubjectKeyIdentifierExtension.create(clientKeys.publicKey),
|
||||||
|
new x509.CertificatePolicyExtension(["2.5.29.32.0"]), // anyPolicy
|
||||||
|
new x509.KeyUsagesExtension(
|
||||||
|
// eslint-disable-next-line no-bitwise
|
||||||
|
x509.KeyUsageFlags[CertKeyUsage.DIGITAL_SIGNATURE] |
|
||||||
|
x509.KeyUsageFlags[CertKeyUsage.KEY_ENCIPHERMENT] |
|
||||||
|
x509.KeyUsageFlags[CertKeyUsage.KEY_AGREEMENT],
|
||||||
|
true
|
||||||
|
),
|
||||||
|
new x509.ExtendedKeyUsageExtension([x509.ExtendedKeyUsage[CertExtendedKeyUsage.CLIENT_AUTH]], true)
|
||||||
|
]
|
||||||
|
});
|
||||||
|
const clientSkObj = crypto.KeyObject.from(clientKeys.privateKey);
|
||||||
|
|
||||||
|
// generate gateway ca
|
||||||
|
const gatewayCaSerialNumber = createSerialNumber();
|
||||||
|
const gatewayCaIssuedAt = new Date();
|
||||||
|
const gatewayCaExpiration = new Date(new Date().setFullYear(2045));
|
||||||
|
const gatewayCaKeys = await crypto.subtle.generateKey(alg, true, ["sign", "verify"]);
|
||||||
|
const gatewayCaSkObj = crypto.KeyObject.from(gatewayCaKeys.privateKey);
|
||||||
|
const gatewayCaCert = await x509.X509CertificateGenerator.create({
|
||||||
|
serialNumber: gatewayCaSerialNumber,
|
||||||
|
subject: `O=${identityOrg},CN=Gateway CA`,
|
||||||
|
issuer: rootCaCert.subject,
|
||||||
|
notBefore: gatewayCaIssuedAt,
|
||||||
|
notAfter: gatewayCaExpiration,
|
||||||
|
signingKey: rootCaKeys.privateKey,
|
||||||
|
publicKey: gatewayCaKeys.publicKey,
|
||||||
|
signingAlgorithm: alg,
|
||||||
|
extensions: [
|
||||||
|
new x509.KeyUsagesExtension(
|
||||||
|
// eslint-disable-next-line no-bitwise
|
||||||
|
x509.KeyUsageFlags.keyCertSign |
|
||||||
|
x509.KeyUsageFlags.cRLSign |
|
||||||
|
x509.KeyUsageFlags.digitalSignature |
|
||||||
|
x509.KeyUsageFlags.keyEncipherment,
|
||||||
|
true
|
||||||
|
),
|
||||||
|
new x509.BasicConstraintsExtension(true, 0, true),
|
||||||
|
await x509.AuthorityKeyIdentifierExtension.create(rootCaCert, false),
|
||||||
|
await x509.SubjectKeyIdentifierExtension.create(gatewayCaKeys.publicKey)
|
||||||
|
]
|
||||||
|
});
|
||||||
|
|
||||||
|
return orgGatewayConfigDAL.create({
|
||||||
|
orgId: identityOrg,
|
||||||
|
rootCaIssuedAt,
|
||||||
|
rootCaExpiration,
|
||||||
|
rootCaSerialNumber,
|
||||||
|
rootCaKeyAlgorithm,
|
||||||
|
encryptedRootCaPrivateKey: orgKmsEncryptor({
|
||||||
|
plainText: rootCaSkObj.export({
|
||||||
|
type: "pkcs8",
|
||||||
|
format: "der"
|
||||||
|
})
|
||||||
|
}).cipherTextBlob,
|
||||||
|
encryptedRootCaCertificate: orgKmsEncryptor({ plainText: Buffer.from(rootCaCert.rawData) }).cipherTextBlob,
|
||||||
|
|
||||||
|
clientCaIssuedAt,
|
||||||
|
clientCaExpiration,
|
||||||
|
clientCaSerialNumber,
|
||||||
|
encryptedClientCaPrivateKey: orgKmsEncryptor({
|
||||||
|
plainText: clientCaSkObj.export({
|
||||||
|
type: "pkcs8",
|
||||||
|
format: "der"
|
||||||
|
})
|
||||||
|
}).cipherTextBlob,
|
||||||
|
encryptedClientCaCertificate: orgKmsEncryptor({
|
||||||
|
plainText: Buffer.from(clientCaCert.rawData)
|
||||||
|
}).cipherTextBlob,
|
||||||
|
|
||||||
|
clientCertIssuedAt: clientCaIssuedAt,
|
||||||
|
clientCertExpiration: clientCaExpiration,
|
||||||
|
clientCertKeyAlgorithm: CertKeyAlgorithm.RSA_2048,
|
||||||
|
clientCertSerialNumber,
|
||||||
|
encryptedClientPrivateKey: orgKmsEncryptor({
|
||||||
|
plainText: clientSkObj.export({
|
||||||
|
type: "pkcs8",
|
||||||
|
format: "der"
|
||||||
|
})
|
||||||
|
}).cipherTextBlob,
|
||||||
|
encryptedClientCertificate: orgKmsEncryptor({
|
||||||
|
plainText: Buffer.from(clientCert.rawData)
|
||||||
|
}).cipherTextBlob,
|
||||||
|
|
||||||
|
gatewayCaIssuedAt,
|
||||||
|
gatewayCaExpiration,
|
||||||
|
gatewayCaSerialNumber,
|
||||||
|
encryptedGatewayCaPrivateKey: orgKmsEncryptor({
|
||||||
|
plainText: gatewayCaSkObj.export({
|
||||||
|
type: "pkcs8",
|
||||||
|
format: "der"
|
||||||
|
})
|
||||||
|
}).cipherTextBlob,
|
||||||
|
encryptedGatewayCaCertificate: orgKmsEncryptor({
|
||||||
|
plainText: Buffer.from(gatewayCaCert.rawData)
|
||||||
|
}).cipherTextBlob
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
const rootCaCert = new x509.X509Certificate(
|
||||||
|
orgKmsDecryptor({
|
||||||
|
cipherTextBlob: orgGatewayConfig.encryptedRootCaCertificate
|
||||||
|
})
|
||||||
|
);
|
||||||
|
const clientCaCert = new x509.X509Certificate(
|
||||||
|
orgKmsDecryptor({
|
||||||
|
cipherTextBlob: orgGatewayConfig.encryptedClientCaCertificate
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
const gatewayCaAlg = keyAlgorithmToAlgCfg(orgGatewayConfig.rootCaKeyAlgorithm as CertKeyAlgorithm);
|
||||||
|
const gatewayCaSkObj = crypto.createPrivateKey({
|
||||||
|
key: orgKmsDecryptor({ cipherTextBlob: orgGatewayConfig.encryptedGatewayCaPrivateKey }),
|
||||||
|
format: "der",
|
||||||
|
type: "pkcs8"
|
||||||
|
});
|
||||||
|
const gatewayCaCert = new x509.X509Certificate(
|
||||||
|
orgKmsDecryptor({
|
||||||
|
cipherTextBlob: orgGatewayConfig.encryptedGatewayCaCertificate
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
const gatewayCaPrivateKey = await crypto.subtle.importKey(
|
||||||
|
"pkcs8",
|
||||||
|
gatewayCaSkObj.export({ format: "der", type: "pkcs8" }),
|
||||||
|
gatewayCaAlg,
|
||||||
|
true,
|
||||||
|
["sign"]
|
||||||
|
);
|
||||||
|
|
||||||
|
const alg = keyAlgorithmToAlgCfg(CertKeyAlgorithm.RSA_2048);
|
||||||
|
const gatewayKeys = await crypto.subtle.generateKey(alg, true, ["sign", "verify"]);
|
||||||
|
const certIssuedAt = new Date();
|
||||||
|
// then need to periodically init
|
||||||
|
const certExpireAt = new Date(new Date().setMonth(new Date().getMonth() + 1));
|
||||||
|
|
||||||
|
const extensions: x509.Extension[] = [
|
||||||
|
new x509.BasicConstraintsExtension(false),
|
||||||
|
await x509.AuthorityKeyIdentifierExtension.create(gatewayCaCert, false),
|
||||||
|
await x509.SubjectKeyIdentifierExtension.create(gatewayKeys.publicKey),
|
||||||
|
new x509.CertificatePolicyExtension(["2.5.29.32.0"]), // anyPolicy
|
||||||
|
new x509.KeyUsagesExtension(
|
||||||
|
// eslint-disable-next-line no-bitwise
|
||||||
|
x509.KeyUsageFlags[CertKeyUsage.DIGITAL_SIGNATURE] | x509.KeyUsageFlags[CertKeyUsage.KEY_ENCIPHERMENT],
|
||||||
|
true
|
||||||
|
),
|
||||||
|
new x509.ExtendedKeyUsageExtension([x509.ExtendedKeyUsage[CertExtendedKeyUsage.SERVER_AUTH]], true),
|
||||||
|
// san
|
||||||
|
new x509.SubjectAlternativeNameExtension([{ type: "ip", value: relayAddress.split(":")[0] }], false)
|
||||||
|
];
|
||||||
|
|
||||||
|
const serialNumber = createSerialNumber();
|
||||||
|
const privateKey = crypto.KeyObject.from(gatewayKeys.privateKey);
|
||||||
|
const gatewayCertificate = await x509.X509CertificateGenerator.create({
|
||||||
|
serialNumber,
|
||||||
|
subject: `CN=${identityId},O=${identityOrg},OU=Gateway`,
|
||||||
|
issuer: gatewayCaCert.subject,
|
||||||
|
notBefore: certIssuedAt,
|
||||||
|
notAfter: certExpireAt,
|
||||||
|
signingKey: gatewayCaPrivateKey,
|
||||||
|
publicKey: gatewayKeys.publicKey,
|
||||||
|
signingAlgorithm: alg,
|
||||||
|
extensions
|
||||||
|
});
|
||||||
|
|
||||||
|
const appCfg = getConfig();
|
||||||
|
// just for local development
|
||||||
|
const formatedRelayAddress =
|
||||||
|
appCfg.NODE_ENV === "development" ? relayAddress.replace("127.0.0.1", "host.docker.internal") : relayAddress;
|
||||||
|
|
||||||
|
await gatewayDAL.transaction(async (tx) => {
|
||||||
|
await tx.raw("SELECT pg_advisory_xact_lock(?)", [PgSqlLock.OrgGatewayCertExchange(identityOrg)]);
|
||||||
|
const existingGateway = await gatewayDAL.findOne({ identityId, orgGatewayRootCaId: orgGatewayConfig.id });
|
||||||
|
|
||||||
|
if (existingGateway) {
|
||||||
|
return gatewayDAL.updateById(existingGateway.id, {
|
||||||
|
keyAlgorithm: CertKeyAlgorithm.RSA_2048,
|
||||||
|
issuedAt: certIssuedAt,
|
||||||
|
expiration: certExpireAt,
|
||||||
|
serialNumber,
|
||||||
|
relayAddress: orgKmsEncryptor({
|
||||||
|
plainText: Buffer.from(formatedRelayAddress)
|
||||||
|
}).cipherTextBlob
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return gatewayDAL.create({
|
||||||
|
keyAlgorithm: CertKeyAlgorithm.RSA_2048,
|
||||||
|
issuedAt: certIssuedAt,
|
||||||
|
expiration: certExpireAt,
|
||||||
|
serialNumber,
|
||||||
|
relayAddress: orgKmsEncryptor({
|
||||||
|
plainText: Buffer.from(formatedRelayAddress)
|
||||||
|
}).cipherTextBlob,
|
||||||
|
identityId,
|
||||||
|
orgGatewayRootCaId: orgGatewayConfig.id,
|
||||||
|
name: `gateway-${alphaNumericNanoId(6).toLowerCase()}`
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
const gatewayCertificateChain = `${clientCaCert.toString("pem")}\n${rootCaCert.toString("pem")}`.trim();
|
||||||
|
|
||||||
|
return {
|
||||||
|
serialNumber,
|
||||||
|
privateKey: privateKey.export({ format: "pem", type: "pkcs8" }) as string,
|
||||||
|
certificate: gatewayCertificate.toString("pem"),
|
||||||
|
certificateChain: gatewayCertificateChain
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
const heartbeat = async ({ orgPermission }: THeartBeatDTO) => {
|
||||||
|
await $validateOrgAccessToGateway(orgPermission.orgId, orgPermission.id, orgPermission.authMethod);
|
||||||
|
const orgGatewayConfig = await orgGatewayConfigDAL.findOne({ orgId: orgPermission.orgId });
|
||||||
|
if (!orgGatewayConfig) throw new NotFoundError({ message: `Identity with ID ${orgPermission.id} not found.` });
|
||||||
|
|
||||||
|
const [gateway] = await gatewayDAL.find({ identityId: orgPermission.id, orgGatewayRootCaId: orgGatewayConfig.id });
|
||||||
|
if (!gateway) throw new NotFoundError({ message: `Gateway with ID ${orgPermission.id} not found.` });
|
||||||
|
|
||||||
|
const { decryptor: orgKmsDecryptor } = await kmsService.createCipherPairWithDataKey({
|
||||||
|
type: KmsDataKey.Organization,
|
||||||
|
orgId: orgGatewayConfig.orgId
|
||||||
|
});
|
||||||
|
|
||||||
|
const rootCaCert = new x509.X509Certificate(
|
||||||
|
orgKmsDecryptor({
|
||||||
|
cipherTextBlob: orgGatewayConfig.encryptedRootCaCertificate
|
||||||
|
})
|
||||||
|
);
|
||||||
|
const gatewayCaCert = new x509.X509Certificate(
|
||||||
|
orgKmsDecryptor({
|
||||||
|
cipherTextBlob: orgGatewayConfig.encryptedGatewayCaCertificate
|
||||||
|
})
|
||||||
|
);
|
||||||
|
const clientCert = new x509.X509Certificate(
|
||||||
|
orgKmsDecryptor({
|
||||||
|
cipherTextBlob: orgGatewayConfig.encryptedClientCertificate
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
const privateKey = crypto
|
||||||
|
.createPrivateKey({
|
||||||
|
key: orgKmsDecryptor({ cipherTextBlob: orgGatewayConfig.encryptedClientPrivateKey }),
|
||||||
|
format: "der",
|
||||||
|
type: "pkcs8"
|
||||||
|
})
|
||||||
|
.export({ type: "pkcs8", format: "pem" });
|
||||||
|
|
||||||
|
const relayAddress = orgKmsDecryptor({ cipherTextBlob: gateway.relayAddress }).toString();
|
||||||
|
const [relayHost, relayPort] = relayAddress.split(":");
|
||||||
|
|
||||||
|
await pingGatewayAndVerify({
|
||||||
|
relayHost,
|
||||||
|
relayPort: Number(relayPort),
|
||||||
|
tlsOptions: {
|
||||||
|
key: privateKey.toString(),
|
||||||
|
ca: `${gatewayCaCert.toString("pem")}\n${rootCaCert.toString("pem")}`.trim(),
|
||||||
|
cert: clientCert.toString("pem")
|
||||||
|
},
|
||||||
|
identityId: orgPermission.id,
|
||||||
|
orgId: orgPermission.orgId
|
||||||
|
});
|
||||||
|
|
||||||
|
await gatewayDAL.updateById(gateway.id, { heartbeat: new Date() });
|
||||||
|
};
|
||||||
|
|
||||||
|
const listGateways = async ({ orgPermission }: TListGatewaysDTO) => {
|
||||||
|
const { permission } = await permissionService.getOrgPermission(
|
||||||
|
orgPermission.type,
|
||||||
|
orgPermission.id,
|
||||||
|
orgPermission.orgId,
|
||||||
|
orgPermission.authMethod,
|
||||||
|
orgPermission.orgId
|
||||||
|
);
|
||||||
|
ForbiddenError.from(permission).throwUnlessCan(
|
||||||
|
OrgPermissionGatewayActions.ListGateways,
|
||||||
|
OrgPermissionSubjects.Gateway
|
||||||
|
);
|
||||||
|
const orgGatewayConfig = await orgGatewayConfigDAL.findOne({ orgId: orgPermission.orgId });
|
||||||
|
if (!orgGatewayConfig) return [];
|
||||||
|
|
||||||
|
const gateways = await gatewayDAL.find({
|
||||||
|
orgGatewayRootCaId: orgGatewayConfig.id
|
||||||
|
});
|
||||||
|
return gateways;
|
||||||
|
};
|
||||||
|
|
||||||
|
const getGatewayById = async ({ orgPermission, id }: TGetGatewayByIdDTO) => {
|
||||||
|
const { permission } = await permissionService.getOrgPermission(
|
||||||
|
orgPermission.type,
|
||||||
|
orgPermission.id,
|
||||||
|
orgPermission.orgId,
|
||||||
|
orgPermission.authMethod,
|
||||||
|
orgPermission.orgId
|
||||||
|
);
|
||||||
|
ForbiddenError.from(permission).throwUnlessCan(
|
||||||
|
OrgPermissionGatewayActions.ListGateways,
|
||||||
|
OrgPermissionSubjects.Gateway
|
||||||
|
);
|
||||||
|
const orgGatewayConfig = await orgGatewayConfigDAL.findOne({ orgId: orgPermission.orgId });
|
||||||
|
if (!orgGatewayConfig) throw new NotFoundError({ message: `Gateway with ID ${id} not found.` });
|
||||||
|
|
||||||
|
const [gateway] = await gatewayDAL.find({ id, orgGatewayRootCaId: orgGatewayConfig.id });
|
||||||
|
if (!gateway) throw new NotFoundError({ message: `Gateway with ID ${id} not found.` });
|
||||||
|
return gateway;
|
||||||
|
};
|
||||||
|
|
||||||
|
const updateGatewayById = async ({ orgPermission, id, name, projectIds }: TUpdateGatewayByIdDTO) => {
|
||||||
|
const { permission } = await permissionService.getOrgPermission(
|
||||||
|
orgPermission.type,
|
||||||
|
orgPermission.id,
|
||||||
|
orgPermission.orgId,
|
||||||
|
orgPermission.authMethod,
|
||||||
|
orgPermission.orgId
|
||||||
|
);
|
||||||
|
ForbiddenError.from(permission).throwUnlessCan(
|
||||||
|
OrgPermissionGatewayActions.EditGateways,
|
||||||
|
OrgPermissionSubjects.Gateway
|
||||||
|
);
|
||||||
|
const orgGatewayConfig = await orgGatewayConfigDAL.findOne({ orgId: orgPermission.orgId });
|
||||||
|
if (!orgGatewayConfig) throw new NotFoundError({ message: `Gateway with ID ${id} not found.` });
|
||||||
|
|
||||||
|
const [gateway] = await gatewayDAL.update({ id, orgGatewayRootCaId: orgGatewayConfig.id }, { name });
|
||||||
|
if (!gateway) throw new NotFoundError({ message: `Gateway with ID ${id} not found.` });
|
||||||
|
if (projectIds) {
|
||||||
|
await projectGatewayDAL.transaction(async (tx) => {
|
||||||
|
await projectGatewayDAL.delete({ gatewayId: gateway.id }, tx);
|
||||||
|
await projectGatewayDAL.insertMany(
|
||||||
|
projectIds.map((el) => ({ gatewayId: gateway.id, projectId: el })),
|
||||||
|
tx
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return gateway;
|
||||||
|
};
|
||||||
|
|
||||||
|
const deleteGatewayById = async ({ orgPermission, id }: TGetGatewayByIdDTO) => {
|
||||||
|
const { permission } = await permissionService.getOrgPermission(
|
||||||
|
orgPermission.type,
|
||||||
|
orgPermission.id,
|
||||||
|
orgPermission.orgId,
|
||||||
|
orgPermission.authMethod,
|
||||||
|
orgPermission.orgId
|
||||||
|
);
|
||||||
|
ForbiddenError.from(permission).throwUnlessCan(
|
||||||
|
OrgPermissionGatewayActions.DeleteGateways,
|
||||||
|
OrgPermissionSubjects.Gateway
|
||||||
|
);
|
||||||
|
const orgGatewayConfig = await orgGatewayConfigDAL.findOne({ orgId: orgPermission.orgId });
|
||||||
|
if (!orgGatewayConfig) throw new NotFoundError({ message: `Gateway with ID ${id} not found.` });
|
||||||
|
|
||||||
|
const [gateway] = await gatewayDAL.delete({ id, orgGatewayRootCaId: orgGatewayConfig.id });
|
||||||
|
if (!gateway) throw new NotFoundError({ message: `Gateway with ID ${id} not found.` });
|
||||||
|
return gateway;
|
||||||
|
};
|
||||||
|
|
||||||
|
const getProjectGateways = async ({ projectId, projectPermission }: TGetProjectGatewayByIdDTO) => {
|
||||||
|
await permissionService.getProjectPermission({
|
||||||
|
projectId,
|
||||||
|
actor: projectPermission.type,
|
||||||
|
actorId: projectPermission.id,
|
||||||
|
actorOrgId: projectPermission.orgId,
|
||||||
|
actorAuthMethod: projectPermission.authMethod,
|
||||||
|
actionProjectType: ActionProjectType.Any
|
||||||
|
});
|
||||||
|
|
||||||
|
const gateways = await gatewayDAL.findByProjectId(projectId);
|
||||||
|
return gateways;
|
||||||
|
};
|
||||||
|
|
||||||
|
// this has no permission check and used for dynamic secrets directly
|
||||||
|
// assumes permission check is already done
|
||||||
|
const fnGetGatewayClientTls = async (projectGatewayId: string) => {
|
||||||
|
const projectGateway = await projectGatewayDAL.findById(projectGatewayId);
|
||||||
|
if (!projectGateway) throw new NotFoundError({ message: `Project gateway with ID ${projectGatewayId} not found.` });
|
||||||
|
|
||||||
|
const { gatewayId } = projectGateway;
|
||||||
|
const gateway = await gatewayDAL.findById(gatewayId);
|
||||||
|
if (!gateway) throw new NotFoundError({ message: `Gateway with ID ${gatewayId} not found.` });
|
||||||
|
|
||||||
|
const orgGatewayConfig = await orgGatewayConfigDAL.findById(gateway.orgGatewayRootCaId);
|
||||||
|
const { decryptor: orgKmsDecryptor } = await kmsService.createCipherPairWithDataKey({
|
||||||
|
type: KmsDataKey.Organization,
|
||||||
|
orgId: orgGatewayConfig.orgId
|
||||||
|
});
|
||||||
|
|
||||||
|
const rootCaCert = new x509.X509Certificate(
|
||||||
|
orgKmsDecryptor({
|
||||||
|
cipherTextBlob: orgGatewayConfig.encryptedRootCaCertificate
|
||||||
|
})
|
||||||
|
);
|
||||||
|
const gatewayCaCert = new x509.X509Certificate(
|
||||||
|
orgKmsDecryptor({
|
||||||
|
cipherTextBlob: orgGatewayConfig.encryptedGatewayCaCertificate
|
||||||
|
})
|
||||||
|
);
|
||||||
|
const clientCert = new x509.X509Certificate(
|
||||||
|
orgKmsDecryptor({
|
||||||
|
cipherTextBlob: orgGatewayConfig.encryptedClientCertificate
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
const clientSkObj = crypto.createPrivateKey({
|
||||||
|
key: orgKmsDecryptor({ cipherTextBlob: orgGatewayConfig.encryptedClientPrivateKey }),
|
||||||
|
format: "der",
|
||||||
|
type: "pkcs8"
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
relayAddress: orgKmsDecryptor({ cipherTextBlob: gateway.relayAddress }).toString(),
|
||||||
|
privateKey: clientSkObj.export({ type: "pkcs8", format: "pem" }),
|
||||||
|
certificate: clientCert.toString("pem"),
|
||||||
|
certChain: `${gatewayCaCert.toString("pem")}\n${rootCaCert.toString("pem")}`.trim(),
|
||||||
|
identityId: gateway.identityId,
|
||||||
|
orgId: orgGatewayConfig.orgId
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
getGatewayRelayDetails,
|
||||||
|
exchangeAllocatedRelayAddress,
|
||||||
|
listGateways,
|
||||||
|
getGatewayById,
|
||||||
|
updateGatewayById,
|
||||||
|
deleteGatewayById,
|
||||||
|
getProjectGateways,
|
||||||
|
fnGetGatewayClientTls,
|
||||||
|
heartbeat
|
||||||
|
};
|
||||||
|
};
|
39
backend/src/ee/services/gateway/gateway-types.ts
Normal file
39
backend/src/ee/services/gateway/gateway-types.ts
Normal file
@ -0,0 +1,39 @@
|
|||||||
|
import { OrgServiceActor } from "@app/lib/types";
|
||||||
|
import { ActorAuthMethod } from "@app/services/auth/auth-type";
|
||||||
|
|
||||||
|
export type TExchangeAllocatedRelayAddressDTO = {
|
||||||
|
identityId: string;
|
||||||
|
identityOrg: string;
|
||||||
|
identityOrgAuthMethod: ActorAuthMethod;
|
||||||
|
relayAddress: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type TListGatewaysDTO = {
|
||||||
|
orgPermission: OrgServiceActor;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type TGetGatewayByIdDTO = {
|
||||||
|
id: string;
|
||||||
|
orgPermission: OrgServiceActor;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type TUpdateGatewayByIdDTO = {
|
||||||
|
id: string;
|
||||||
|
name?: string;
|
||||||
|
projectIds?: string[];
|
||||||
|
orgPermission: OrgServiceActor;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type TDeleteGatewayByIdDTO = {
|
||||||
|
id: string;
|
||||||
|
orgPermission: OrgServiceActor;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type TGetProjectGatewayByIdDTO = {
|
||||||
|
projectId: string;
|
||||||
|
projectPermission: OrgServiceActor;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type THeartBeatDTO = {
|
||||||
|
orgPermission: OrgServiceActor;
|
||||||
|
};
|
10
backend/src/ee/services/gateway/org-gateway-config-dal.ts
Normal file
10
backend/src/ee/services/gateway/org-gateway-config-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 TOrgGatewayConfigDALFactory = ReturnType<typeof orgGatewayConfigDALFactory>;
|
||||||
|
|
||||||
|
export const orgGatewayConfigDALFactory = (db: TDbClient) => {
|
||||||
|
const orm = ormify(db, TableName.OrgGatewayConfig);
|
||||||
|
return orm;
|
||||||
|
};
|
10
backend/src/ee/services/gateway/project-gateway-dal.ts
Normal file
10
backend/src/ee/services/gateway/project-gateway-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 TProjectGatewayDALFactory = ReturnType<typeof projectGatewayDALFactory>;
|
||||||
|
|
||||||
|
export const projectGatewayDALFactory = (db: TDbClient) => {
|
||||||
|
const orm = ormify(db, TableName.ProjectGateway);
|
||||||
|
return orm;
|
||||||
|
};
|
@ -3,7 +3,7 @@ import slugify from "@sindresorhus/slugify";
|
|||||||
|
|
||||||
import { OrgMembershipRole, TOrgRoles } from "@app/db/schemas";
|
import { OrgMembershipRole, TOrgRoles } from "@app/db/schemas";
|
||||||
import { TOidcConfigDALFactory } from "@app/ee/services/oidc/oidc-config-dal";
|
import { TOidcConfigDALFactory } from "@app/ee/services/oidc/oidc-config-dal";
|
||||||
import { isAtLeastAsPrivileged } from "@app/lib/casl";
|
import { validatePermissionBoundary } from "@app/lib/casl/boundary";
|
||||||
import { BadRequestError, ForbiddenRequestError, NotFoundError, UnauthorizedError } from "@app/lib/errors";
|
import { BadRequestError, ForbiddenRequestError, NotFoundError, UnauthorizedError } from "@app/lib/errors";
|
||||||
import { alphaNumericNanoId } from "@app/lib/nanoid";
|
import { alphaNumericNanoId } from "@app/lib/nanoid";
|
||||||
import { TGroupProjectDALFactory } from "@app/services/group-project/group-project-dal";
|
import { TGroupProjectDALFactory } from "@app/services/group-project/group-project-dal";
|
||||||
@ -87,9 +87,14 @@ export const groupServiceFactory = ({
|
|||||||
actorOrgId
|
actorOrgId
|
||||||
);
|
);
|
||||||
const isCustomRole = Boolean(customRole);
|
const isCustomRole = Boolean(customRole);
|
||||||
const hasRequiredPriviledges = isAtLeastAsPrivileged(permission, rolePermission);
|
|
||||||
if (!hasRequiredPriviledges)
|
const permissionBoundary = validatePermissionBoundary(permission, rolePermission);
|
||||||
throw new ForbiddenRequestError({ message: "Failed to create a more privileged group" });
|
if (!permissionBoundary.isValid)
|
||||||
|
throw new ForbiddenRequestError({
|
||||||
|
name: "PermissionBoundaryError",
|
||||||
|
message: "Failed to create a more privileged group",
|
||||||
|
details: { missingPermissions: permissionBoundary.missingPermissions }
|
||||||
|
});
|
||||||
|
|
||||||
const group = await groupDAL.transaction(async (tx) => {
|
const group = await groupDAL.transaction(async (tx) => {
|
||||||
const existingGroup = await groupDAL.findOne({ orgId: actorOrgId, name }, tx);
|
const existingGroup = await groupDAL.findOne({ orgId: actorOrgId, name }, tx);
|
||||||
@ -156,9 +161,13 @@ export const groupServiceFactory = ({
|
|||||||
);
|
);
|
||||||
|
|
||||||
const isCustomRole = Boolean(customOrgRole);
|
const isCustomRole = Boolean(customOrgRole);
|
||||||
const hasRequiredNewRolePermission = isAtLeastAsPrivileged(permission, rolePermission);
|
const permissionBoundary = validatePermissionBoundary(permission, rolePermission);
|
||||||
if (!hasRequiredNewRolePermission)
|
if (!permissionBoundary.isValid)
|
||||||
throw new ForbiddenRequestError({ message: "Failed to create a more privileged group" });
|
throw new ForbiddenRequestError({
|
||||||
|
name: "PermissionBoundaryError",
|
||||||
|
message: "Failed to update a more privileged group",
|
||||||
|
details: { missingPermissions: permissionBoundary.missingPermissions }
|
||||||
|
});
|
||||||
if (isCustomRole) customRole = customOrgRole;
|
if (isCustomRole) customRole = customOrgRole;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -329,9 +338,13 @@ export const groupServiceFactory = ({
|
|||||||
const { permission: groupRolePermission } = await permissionService.getOrgPermissionByRole(group.role, actorOrgId);
|
const { permission: groupRolePermission } = await permissionService.getOrgPermissionByRole(group.role, actorOrgId);
|
||||||
|
|
||||||
// check if user has broader or equal to privileges than group
|
// check if user has broader or equal to privileges than group
|
||||||
const hasRequiredPrivileges = isAtLeastAsPrivileged(permission, groupRolePermission);
|
const permissionBoundary = validatePermissionBoundary(permission, groupRolePermission);
|
||||||
if (!hasRequiredPrivileges)
|
if (!permissionBoundary.isValid)
|
||||||
throw new ForbiddenRequestError({ message: "Failed to add user to more privileged group" });
|
throw new ForbiddenRequestError({
|
||||||
|
name: "PermissionBoundaryError",
|
||||||
|
message: "Failed to add user to more privileged group",
|
||||||
|
details: { missingPermissions: permissionBoundary.missingPermissions }
|
||||||
|
});
|
||||||
|
|
||||||
const user = await userDAL.findOne({ username });
|
const user = await userDAL.findOne({ username });
|
||||||
if (!user) throw new NotFoundError({ message: `Failed to find user with username ${username}` });
|
if (!user) throw new NotFoundError({ message: `Failed to find user with username ${username}` });
|
||||||
@ -396,9 +409,13 @@ export const groupServiceFactory = ({
|
|||||||
const { permission: groupRolePermission } = await permissionService.getOrgPermissionByRole(group.role, actorOrgId);
|
const { permission: groupRolePermission } = await permissionService.getOrgPermissionByRole(group.role, actorOrgId);
|
||||||
|
|
||||||
// check if user has broader or equal to privileges than group
|
// check if user has broader or equal to privileges than group
|
||||||
const hasRequiredPrivileges = isAtLeastAsPrivileged(permission, groupRolePermission);
|
const permissionBoundary = validatePermissionBoundary(permission, groupRolePermission);
|
||||||
if (!hasRequiredPrivileges)
|
if (!permissionBoundary.isValid)
|
||||||
throw new ForbiddenRequestError({ message: "Failed to delete user from more privileged group" });
|
throw new ForbiddenRequestError({
|
||||||
|
name: "PermissionBoundaryError",
|
||||||
|
message: "Failed to delete user from more privileged group",
|
||||||
|
details: { missingPermissions: permissionBoundary.missingPermissions }
|
||||||
|
});
|
||||||
|
|
||||||
const user = await userDAL.findOne({ username });
|
const user = await userDAL.findOne({ username });
|
||||||
if (!user) throw new NotFoundError({ message: `Failed to find user with username ${username}` });
|
if (!user) throw new NotFoundError({ message: `Failed to find user with username ${username}` });
|
||||||
|
@ -1,10 +1,10 @@
|
|||||||
import { ForbiddenError, subject } from "@casl/ability";
|
import { ForbiddenError, subject } from "@casl/ability";
|
||||||
import { packRules } from "@casl/ability/extra";
|
import { packRules } from "@casl/ability/extra";
|
||||||
import ms from "ms";
|
|
||||||
|
|
||||||
import { ActionProjectType, TableName } from "@app/db/schemas";
|
import { ActionProjectType, TableName } from "@app/db/schemas";
|
||||||
import { isAtLeastAsPrivileged } from "@app/lib/casl";
|
import { validatePermissionBoundary } from "@app/lib/casl/boundary";
|
||||||
import { BadRequestError, ForbiddenRequestError, NotFoundError } from "@app/lib/errors";
|
import { BadRequestError, ForbiddenRequestError, NotFoundError } from "@app/lib/errors";
|
||||||
|
import { ms } from "@app/lib/ms";
|
||||||
import { unpackPermissions } from "@app/server/routes/sanitizedSchema/permission";
|
import { unpackPermissions } from "@app/server/routes/sanitizedSchema/permission";
|
||||||
import { ActorType } from "@app/services/auth/auth-type";
|
import { ActorType } from "@app/services/auth/auth-type";
|
||||||
import { TIdentityProjectDALFactory } from "@app/services/identity-project/identity-project-dal";
|
import { TIdentityProjectDALFactory } from "@app/services/identity-project/identity-project-dal";
|
||||||
@ -79,9 +79,13 @@ export const identityProjectAdditionalPrivilegeV2ServiceFactory = ({
|
|||||||
// we need to validate that the privilege given is not higher than the assigning users permission
|
// we need to validate that the privilege given is not higher than the assigning users permission
|
||||||
// @ts-expect-error this is expected error because of one being really accurate rule definition other being a bit more broader. Both are valid casl rules
|
// @ts-expect-error this is expected error because of one being really accurate rule definition other being a bit more broader. Both are valid casl rules
|
||||||
targetIdentityPermission.update(targetIdentityPermission.rules.concat(customPermission));
|
targetIdentityPermission.update(targetIdentityPermission.rules.concat(customPermission));
|
||||||
const hasRequiredPriviledges = isAtLeastAsPrivileged(permission, targetIdentityPermission);
|
const permissionBoundary = validatePermissionBoundary(permission, targetIdentityPermission);
|
||||||
if (!hasRequiredPriviledges)
|
if (!permissionBoundary.isValid)
|
||||||
throw new ForbiddenRequestError({ message: "Failed to update more privileged identity" });
|
throw new ForbiddenRequestError({
|
||||||
|
name: "PermissionBoundaryError",
|
||||||
|
message: "Failed to update more privileged identity",
|
||||||
|
details: { missingPermissions: permissionBoundary.missingPermissions }
|
||||||
|
});
|
||||||
|
|
||||||
const existingSlug = await identityProjectAdditionalPrivilegeDAL.findOne({
|
const existingSlug = await identityProjectAdditionalPrivilegeDAL.findOne({
|
||||||
slug,
|
slug,
|
||||||
@ -161,9 +165,13 @@ export const identityProjectAdditionalPrivilegeV2ServiceFactory = ({
|
|||||||
// we need to validate that the privilege given is not higher than the assigning users permission
|
// we need to validate that the privilege given is not higher than the assigning users permission
|
||||||
// @ts-expect-error this is expected error because of one being really accurate rule definition other being a bit more broader. Both are valid casl rules
|
// @ts-expect-error this is expected error because of one being really accurate rule definition other being a bit more broader. Both are valid casl rules
|
||||||
targetIdentityPermission.update(targetIdentityPermission.rules.concat(data.permissions || []));
|
targetIdentityPermission.update(targetIdentityPermission.rules.concat(data.permissions || []));
|
||||||
const hasRequiredPriviledges = isAtLeastAsPrivileged(permission, targetIdentityPermission);
|
const permissionBoundary = validatePermissionBoundary(permission, targetIdentityPermission);
|
||||||
if (!hasRequiredPriviledges)
|
if (!permissionBoundary.isValid)
|
||||||
throw new ForbiddenRequestError({ message: "Failed to update more privileged identity" });
|
throw new ForbiddenRequestError({
|
||||||
|
name: "PermissionBoundaryError",
|
||||||
|
message: "Failed to update more privileged identity",
|
||||||
|
details: { missingPermissions: permissionBoundary.missingPermissions }
|
||||||
|
});
|
||||||
|
|
||||||
if (data?.slug) {
|
if (data?.slug) {
|
||||||
const existingSlug = await identityProjectAdditionalPrivilegeDAL.findOne({
|
const existingSlug = await identityProjectAdditionalPrivilegeDAL.findOne({
|
||||||
@ -239,9 +247,13 @@ export const identityProjectAdditionalPrivilegeV2ServiceFactory = ({
|
|||||||
actorOrgId,
|
actorOrgId,
|
||||||
actionProjectType: ActionProjectType.Any
|
actionProjectType: ActionProjectType.Any
|
||||||
});
|
});
|
||||||
const hasRequiredPriviledges = isAtLeastAsPrivileged(permission, identityRolePermission);
|
const permissionBoundary = validatePermissionBoundary(permission, identityRolePermission);
|
||||||
if (!hasRequiredPriviledges)
|
if (!permissionBoundary.isValid)
|
||||||
throw new ForbiddenRequestError({ message: "Failed to update more privileged identity" });
|
throw new ForbiddenRequestError({
|
||||||
|
name: "PermissionBoundaryError",
|
||||||
|
message: "Failed to update more privileged identity",
|
||||||
|
details: { missingPermissions: permissionBoundary.missingPermissions }
|
||||||
|
});
|
||||||
|
|
||||||
const deletedPrivilege = await identityProjectAdditionalPrivilegeDAL.deleteById(identityPrivilege.id);
|
const deletedPrivilege = await identityProjectAdditionalPrivilegeDAL.deleteById(identityPrivilege.id);
|
||||||
return {
|
return {
|
||||||
|
@ -1,10 +1,10 @@
|
|||||||
import { ForbiddenError, MongoAbility, RawRuleOf, subject } from "@casl/ability";
|
import { ForbiddenError, MongoAbility, RawRuleOf, subject } from "@casl/ability";
|
||||||
import { PackRule, packRules, unpackRules } from "@casl/ability/extra";
|
import { PackRule, packRules, unpackRules } from "@casl/ability/extra";
|
||||||
import ms from "ms";
|
|
||||||
|
|
||||||
import { ActionProjectType } from "@app/db/schemas";
|
import { ActionProjectType } from "@app/db/schemas";
|
||||||
import { isAtLeastAsPrivileged } from "@app/lib/casl";
|
import { validatePermissionBoundary } from "@app/lib/casl/boundary";
|
||||||
import { BadRequestError, ForbiddenRequestError, NotFoundError } from "@app/lib/errors";
|
import { BadRequestError, ForbiddenRequestError, NotFoundError } from "@app/lib/errors";
|
||||||
|
import { ms } from "@app/lib/ms";
|
||||||
import { UnpackedPermissionSchema } from "@app/server/routes/sanitizedSchema/permission";
|
import { UnpackedPermissionSchema } from "@app/server/routes/sanitizedSchema/permission";
|
||||||
import { ActorType } from "@app/services/auth/auth-type";
|
import { ActorType } from "@app/services/auth/auth-type";
|
||||||
import { TIdentityProjectDALFactory } from "@app/services/identity-project/identity-project-dal";
|
import { TIdentityProjectDALFactory } from "@app/services/identity-project/identity-project-dal";
|
||||||
@ -88,9 +88,13 @@ export const identityProjectAdditionalPrivilegeServiceFactory = ({
|
|||||||
// we need to validate that the privilege given is not higher than the assigning users permission
|
// we need to validate that the privilege given is not higher than the assigning users permission
|
||||||
// @ts-expect-error this is expected error because of one being really accurate rule definition other being a bit more broader. Both are valid casl rules
|
// @ts-expect-error this is expected error because of one being really accurate rule definition other being a bit more broader. Both are valid casl rules
|
||||||
targetIdentityPermission.update(targetIdentityPermission.rules.concat(customPermission));
|
targetIdentityPermission.update(targetIdentityPermission.rules.concat(customPermission));
|
||||||
const hasRequiredPriviledges = isAtLeastAsPrivileged(permission, targetIdentityPermission);
|
const permissionBoundary = validatePermissionBoundary(permission, targetIdentityPermission);
|
||||||
if (!hasRequiredPriviledges)
|
if (!permissionBoundary.isValid)
|
||||||
throw new ForbiddenRequestError({ message: "Failed to update more privileged identity" });
|
throw new ForbiddenRequestError({
|
||||||
|
name: "PermissionBoundaryError",
|
||||||
|
message: "Failed to update more privileged identity",
|
||||||
|
details: { missingPermissions: permissionBoundary.missingPermissions }
|
||||||
|
});
|
||||||
|
|
||||||
const existingSlug = await identityProjectAdditionalPrivilegeDAL.findOne({
|
const existingSlug = await identityProjectAdditionalPrivilegeDAL.findOne({
|
||||||
slug,
|
slug,
|
||||||
@ -172,9 +176,13 @@ export const identityProjectAdditionalPrivilegeServiceFactory = ({
|
|||||||
// we need to validate that the privilege given is not higher than the assigning users permission
|
// we need to validate that the privilege given is not higher than the assigning users permission
|
||||||
// @ts-expect-error this is expected error because of one being really accurate rule definition other being a bit more broader. Both are valid casl rules
|
// @ts-expect-error this is expected error because of one being really accurate rule definition other being a bit more broader. Both are valid casl rules
|
||||||
targetIdentityPermission.update(targetIdentityPermission.rules.concat(data.permissions || []));
|
targetIdentityPermission.update(targetIdentityPermission.rules.concat(data.permissions || []));
|
||||||
const hasRequiredPriviledges = isAtLeastAsPrivileged(permission, targetIdentityPermission);
|
const permissionBoundary = validatePermissionBoundary(permission, targetIdentityPermission);
|
||||||
if (!hasRequiredPriviledges)
|
if (!permissionBoundary.isValid)
|
||||||
throw new ForbiddenRequestError({ message: "Failed to update more privileged identity" });
|
throw new ForbiddenRequestError({
|
||||||
|
name: "PermissionBoundaryError",
|
||||||
|
message: "Failed to update more privileged identity",
|
||||||
|
details: { missingPermissions: permissionBoundary.missingPermissions }
|
||||||
|
});
|
||||||
|
|
||||||
const identityPrivilege = await identityProjectAdditionalPrivilegeDAL.findOne({
|
const identityPrivilege = await identityProjectAdditionalPrivilegeDAL.findOne({
|
||||||
slug,
|
slug,
|
||||||
@ -268,9 +276,13 @@ export const identityProjectAdditionalPrivilegeServiceFactory = ({
|
|||||||
actorOrgId,
|
actorOrgId,
|
||||||
actionProjectType: ActionProjectType.Any
|
actionProjectType: ActionProjectType.Any
|
||||||
});
|
});
|
||||||
const hasRequiredPriviledges = isAtLeastAsPrivileged(permission, identityRolePermission);
|
const permissionBoundary = validatePermissionBoundary(permission, identityRolePermission);
|
||||||
if (!hasRequiredPriviledges)
|
if (!permissionBoundary.isValid)
|
||||||
throw new ForbiddenRequestError({ message: "Failed to edit more privileged identity" });
|
throw new ForbiddenRequestError({
|
||||||
|
name: "PermissionBoundaryError",
|
||||||
|
message: "Failed to edit more privileged identity",
|
||||||
|
details: { missingPermissions: permissionBoundary.missingPermissions }
|
||||||
|
});
|
||||||
|
|
||||||
const identityPrivilege = await identityProjectAdditionalPrivilegeDAL.findOne({
|
const identityPrivilege = await identityProjectAdditionalPrivilegeDAL.findOne({
|
||||||
slug,
|
slug,
|
||||||
|
@ -1,11 +1,11 @@
|
|||||||
import { ForbiddenError } from "@casl/ability";
|
import { ForbiddenError } from "@casl/ability";
|
||||||
import * as x509 from "@peculiar/x509";
|
import * as x509 from "@peculiar/x509";
|
||||||
import crypto, { KeyObject } from "crypto";
|
import crypto, { KeyObject } from "crypto";
|
||||||
import ms from "ms";
|
|
||||||
|
|
||||||
import { ActionProjectType } from "@app/db/schemas";
|
import { ActionProjectType } from "@app/db/schemas";
|
||||||
import { BadRequestError, InternalServerError, NotFoundError } from "@app/lib/errors";
|
import { BadRequestError, InternalServerError, NotFoundError } from "@app/lib/errors";
|
||||||
import { isValidHostname, isValidIp } from "@app/lib/ip";
|
import { isValidHostname, isValidIp } from "@app/lib/ip";
|
||||||
|
import { ms } from "@app/lib/ms";
|
||||||
import { constructPemChainFromCerts } from "@app/services/certificate/certificate-fns";
|
import { constructPemChainFromCerts } from "@app/services/certificate/certificate-fns";
|
||||||
import { CertExtendedKeyUsage, CertKeyAlgorithm, CertKeyUsage } from "@app/services/certificate/certificate-types";
|
import { CertExtendedKeyUsage, CertKeyAlgorithm, CertKeyUsage } from "@app/services/certificate/certificate-types";
|
||||||
import {
|
import {
|
||||||
|
@ -51,7 +51,8 @@ export const getDefaultOnPremFeatures = (): TFeatureSet => ({
|
|||||||
pkiEst: false,
|
pkiEst: false,
|
||||||
enforceMfa: false,
|
enforceMfa: false,
|
||||||
projectTemplates: false,
|
projectTemplates: false,
|
||||||
kmip: false
|
kmip: false,
|
||||||
|
gateway: false
|
||||||
});
|
});
|
||||||
|
|
||||||
export const setupLicenseRequestWithStore = (baseURL: string, refreshUrl: string, licenseKey: string) => {
|
export const setupLicenseRequestWithStore = (baseURL: string, refreshUrl: string, licenseKey: string) => {
|
||||||
|
@ -50,7 +50,7 @@ export type TLicenseServiceFactory = ReturnType<typeof licenseServiceFactory>;
|
|||||||
const LICENSE_SERVER_CLOUD_LOGIN = "/api/auth/v1/license-server-login";
|
const LICENSE_SERVER_CLOUD_LOGIN = "/api/auth/v1/license-server-login";
|
||||||
const LICENSE_SERVER_ON_PREM_LOGIN = "/api/auth/v1/license-login";
|
const LICENSE_SERVER_ON_PREM_LOGIN = "/api/auth/v1/license-login";
|
||||||
|
|
||||||
const LICENSE_SERVER_CLOUD_PLAN_TTL = 30; // 30 second
|
const LICENSE_SERVER_CLOUD_PLAN_TTL = 5 * 60; // 5 mins
|
||||||
const FEATURE_CACHE_KEY = (orgId: string) => `infisical-cloud-plan-${orgId}`;
|
const FEATURE_CACHE_KEY = (orgId: string) => `infisical-cloud-plan-${orgId}`;
|
||||||
|
|
||||||
export const licenseServiceFactory = ({
|
export const licenseServiceFactory = ({
|
||||||
@ -142,7 +142,10 @@ export const licenseServiceFactory = ({
|
|||||||
try {
|
try {
|
||||||
if (instanceType === InstanceType.Cloud) {
|
if (instanceType === InstanceType.Cloud) {
|
||||||
const cachedPlan = await keyStore.getItem(FEATURE_CACHE_KEY(orgId));
|
const cachedPlan = await keyStore.getItem(FEATURE_CACHE_KEY(orgId));
|
||||||
if (cachedPlan) return JSON.parse(cachedPlan) as TFeatureSet;
|
if (cachedPlan) {
|
||||||
|
logger.info(`getPlan: plan fetched from cache [orgId=${orgId}] [projectId=${projectId}]`);
|
||||||
|
return JSON.parse(cachedPlan) as TFeatureSet;
|
||||||
|
}
|
||||||
|
|
||||||
const org = await orgDAL.findOrgById(orgId);
|
const org = await orgDAL.findOrgById(orgId);
|
||||||
if (!org) throw new NotFoundError({ message: `Organization with ID '${orgId}' not found` });
|
if (!org) throw new NotFoundError({ message: `Organization with ID '${orgId}' not found` });
|
||||||
@ -170,6 +173,8 @@ export const licenseServiceFactory = ({
|
|||||||
JSON.stringify(onPremFeatures)
|
JSON.stringify(onPremFeatures)
|
||||||
);
|
);
|
||||||
return onPremFeatures;
|
return onPremFeatures;
|
||||||
|
} finally {
|
||||||
|
logger.info(`getPlan: Process done for [orgId=${orgId}] [projectId=${projectId}]`);
|
||||||
}
|
}
|
||||||
return onPremFeatures;
|
return onPremFeatures;
|
||||||
};
|
};
|
||||||
|
@ -69,6 +69,7 @@ export type TFeatureSet = {
|
|||||||
enforceMfa: boolean;
|
enforceMfa: boolean;
|
||||||
projectTemplates: false;
|
projectTemplates: false;
|
||||||
kmip: false;
|
kmip: false;
|
||||||
|
gateway: false;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type TOrgPlansTableDTO = {
|
export type TOrgPlansTableDTO = {
|
||||||
|
@ -32,6 +32,18 @@ export enum OrgPermissionAdminConsoleAction {
|
|||||||
AccessAllProjects = "access-all-projects"
|
AccessAllProjects = "access-all-projects"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export enum OrgPermissionSecretShareAction {
|
||||||
|
ManageSettings = "manage-settings"
|
||||||
|
}
|
||||||
|
|
||||||
|
export enum OrgPermissionGatewayActions {
|
||||||
|
// is there a better word for this. This mean can an identity be a gateway
|
||||||
|
CreateGateways = "create-gateways",
|
||||||
|
ListGateways = "list-gateways",
|
||||||
|
EditGateways = "edit-gateways",
|
||||||
|
DeleteGateways = "delete-gateways"
|
||||||
|
}
|
||||||
|
|
||||||
export enum OrgPermissionSubjects {
|
export enum OrgPermissionSubjects {
|
||||||
Workspace = "workspace",
|
Workspace = "workspace",
|
||||||
Role = "role",
|
Role = "role",
|
||||||
@ -50,7 +62,9 @@ export enum OrgPermissionSubjects {
|
|||||||
AuditLogs = "audit-logs",
|
AuditLogs = "audit-logs",
|
||||||
ProjectTemplates = "project-templates",
|
ProjectTemplates = "project-templates",
|
||||||
AppConnections = "app-connections",
|
AppConnections = "app-connections",
|
||||||
Kmip = "kmip"
|
Kmip = "kmip",
|
||||||
|
Gateway = "gateway",
|
||||||
|
SecretShare = "secret-share"
|
||||||
}
|
}
|
||||||
|
|
||||||
export type AppConnectionSubjectFields = {
|
export type AppConnectionSubjectFields = {
|
||||||
@ -73,6 +87,7 @@ export type OrgPermissionSet =
|
|||||||
| [OrgPermissionActions, OrgPermissionSubjects.Kms]
|
| [OrgPermissionActions, OrgPermissionSubjects.Kms]
|
||||||
| [OrgPermissionActions, OrgPermissionSubjects.AuditLogs]
|
| [OrgPermissionActions, OrgPermissionSubjects.AuditLogs]
|
||||||
| [OrgPermissionActions, OrgPermissionSubjects.ProjectTemplates]
|
| [OrgPermissionActions, OrgPermissionSubjects.ProjectTemplates]
|
||||||
|
| [OrgPermissionGatewayActions, OrgPermissionSubjects.Gateway]
|
||||||
| [
|
| [
|
||||||
OrgPermissionAppConnectionActions,
|
OrgPermissionAppConnectionActions,
|
||||||
(
|
(
|
||||||
@ -81,7 +96,8 @@ export type OrgPermissionSet =
|
|||||||
)
|
)
|
||||||
]
|
]
|
||||||
| [OrgPermissionAdminConsoleAction, OrgPermissionSubjects.AdminConsole]
|
| [OrgPermissionAdminConsoleAction, OrgPermissionSubjects.AdminConsole]
|
||||||
| [OrgPermissionKmipActions, OrgPermissionSubjects.Kmip];
|
| [OrgPermissionKmipActions, OrgPermissionSubjects.Kmip]
|
||||||
|
| [OrgPermissionSecretShareAction, OrgPermissionSubjects.SecretShare];
|
||||||
|
|
||||||
const AppConnectionConditionSchema = z
|
const AppConnectionConditionSchema = z
|
||||||
.object({
|
.object({
|
||||||
@ -175,11 +191,23 @@ export const OrgPermissionSchema = z.discriminatedUnion("subject", [
|
|||||||
"Describe what action an entity can take."
|
"Describe what action an entity can take."
|
||||||
)
|
)
|
||||||
}),
|
}),
|
||||||
|
z.object({
|
||||||
|
subject: z.literal(OrgPermissionSubjects.SecretShare).describe("The entity this permission pertains to."),
|
||||||
|
action: CASL_ACTION_SCHEMA_NATIVE_ENUM(OrgPermissionSecretShareAction).describe(
|
||||||
|
"Describe what action an entity can take."
|
||||||
|
)
|
||||||
|
}),
|
||||||
z.object({
|
z.object({
|
||||||
subject: z.literal(OrgPermissionSubjects.Kmip).describe("The entity this permission pertains to."),
|
subject: z.literal(OrgPermissionSubjects.Kmip).describe("The entity this permission pertains to."),
|
||||||
action: CASL_ACTION_SCHEMA_NATIVE_ENUM(OrgPermissionKmipActions).describe(
|
action: CASL_ACTION_SCHEMA_NATIVE_ENUM(OrgPermissionKmipActions).describe(
|
||||||
"Describe what action an entity can take."
|
"Describe what action an entity can take."
|
||||||
)
|
)
|
||||||
|
}),
|
||||||
|
z.object({
|
||||||
|
subject: z.literal(OrgPermissionSubjects.Gateway).describe("The entity this permission pertains to."),
|
||||||
|
action: CASL_ACTION_SCHEMA_NATIVE_ENUM(OrgPermissionGatewayActions).describe(
|
||||||
|
"Describe what action an entity can take."
|
||||||
|
)
|
||||||
})
|
})
|
||||||
]);
|
]);
|
||||||
|
|
||||||
@ -264,6 +292,11 @@ const buildAdminPermission = () => {
|
|||||||
can(OrgPermissionAppConnectionActions.Delete, OrgPermissionSubjects.AppConnections);
|
can(OrgPermissionAppConnectionActions.Delete, OrgPermissionSubjects.AppConnections);
|
||||||
can(OrgPermissionAppConnectionActions.Connect, OrgPermissionSubjects.AppConnections);
|
can(OrgPermissionAppConnectionActions.Connect, OrgPermissionSubjects.AppConnections);
|
||||||
|
|
||||||
|
can(OrgPermissionGatewayActions.ListGateways, OrgPermissionSubjects.Gateway);
|
||||||
|
can(OrgPermissionGatewayActions.CreateGateways, OrgPermissionSubjects.Gateway);
|
||||||
|
can(OrgPermissionGatewayActions.EditGateways, OrgPermissionSubjects.Gateway);
|
||||||
|
can(OrgPermissionGatewayActions.DeleteGateways, OrgPermissionSubjects.Gateway);
|
||||||
|
|
||||||
can(OrgPermissionAdminConsoleAction.AccessAllProjects, OrgPermissionSubjects.AdminConsole);
|
can(OrgPermissionAdminConsoleAction.AccessAllProjects, OrgPermissionSubjects.AdminConsole);
|
||||||
|
|
||||||
can(OrgPermissionKmipActions.Setup, OrgPermissionSubjects.Kmip);
|
can(OrgPermissionKmipActions.Setup, OrgPermissionSubjects.Kmip);
|
||||||
@ -271,6 +304,8 @@ const buildAdminPermission = () => {
|
|||||||
// the proxy assignment is temporary in order to prevent "more privilege" error during role assignment to MI
|
// the proxy assignment is temporary in order to prevent "more privilege" error during role assignment to MI
|
||||||
can(OrgPermissionKmipActions.Proxy, OrgPermissionSubjects.Kmip);
|
can(OrgPermissionKmipActions.Proxy, OrgPermissionSubjects.Kmip);
|
||||||
|
|
||||||
|
can(OrgPermissionSecretShareAction.ManageSettings, OrgPermissionSubjects.SecretShare);
|
||||||
|
|
||||||
return rules;
|
return rules;
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -300,6 +335,8 @@ const buildMemberPermission = () => {
|
|||||||
can(OrgPermissionActions.Read, OrgPermissionSubjects.AuditLogs);
|
can(OrgPermissionActions.Read, OrgPermissionSubjects.AuditLogs);
|
||||||
|
|
||||||
can(OrgPermissionAppConnectionActions.Connect, OrgPermissionSubjects.AppConnections);
|
can(OrgPermissionAppConnectionActions.Connect, OrgPermissionSubjects.AppConnections);
|
||||||
|
can(OrgPermissionGatewayActions.ListGateways, OrgPermissionSubjects.Gateway);
|
||||||
|
can(OrgPermissionGatewayActions.CreateGateways, OrgPermissionSubjects.Gateway);
|
||||||
|
|
||||||
return rules;
|
return rules;
|
||||||
};
|
};
|
||||||
|
@ -1,7 +1,109 @@
|
|||||||
|
/* eslint-disable no-nested-ternary */
|
||||||
|
import { ForbiddenError, MongoAbility, PureAbility, subject } from "@casl/ability";
|
||||||
|
import { z } from "zod";
|
||||||
|
|
||||||
import { TOrganizations } from "@app/db/schemas";
|
import { TOrganizations } from "@app/db/schemas";
|
||||||
import { ForbiddenRequestError, UnauthorizedError } from "@app/lib/errors";
|
import { BadRequestError, ForbiddenRequestError, UnauthorizedError } from "@app/lib/errors";
|
||||||
import { ActorAuthMethod, AuthMethod } from "@app/services/auth/auth-type";
|
import { ActorAuthMethod, AuthMethod } from "@app/services/auth/auth-type";
|
||||||
|
|
||||||
|
import {
|
||||||
|
ProjectPermissionSecretActions,
|
||||||
|
ProjectPermissionSet,
|
||||||
|
ProjectPermissionSub,
|
||||||
|
ProjectPermissionV2Schema,
|
||||||
|
SecretSubjectFields
|
||||||
|
} from "./project-permission";
|
||||||
|
|
||||||
|
export function throwIfMissingSecretReadValueOrDescribePermission(
|
||||||
|
permission: MongoAbility<ProjectPermissionSet> | PureAbility,
|
||||||
|
action: Extract<
|
||||||
|
ProjectPermissionSecretActions,
|
||||||
|
ProjectPermissionSecretActions.ReadValue | ProjectPermissionSecretActions.DescribeSecret
|
||||||
|
>,
|
||||||
|
subjectFields?: SecretSubjectFields
|
||||||
|
) {
|
||||||
|
try {
|
||||||
|
if (subjectFields) {
|
||||||
|
ForbiddenError.from(permission).throwUnlessCan(
|
||||||
|
ProjectPermissionSecretActions.DescribeAndReadValue,
|
||||||
|
subject(ProjectPermissionSub.Secrets, subjectFields)
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
ForbiddenError.from(permission).throwUnlessCan(
|
||||||
|
ProjectPermissionSecretActions.DescribeAndReadValue,
|
||||||
|
ProjectPermissionSub.Secrets
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
if (subjectFields) {
|
||||||
|
ForbiddenError.from(permission).throwUnlessCan(action, subject(ProjectPermissionSub.Secrets, subjectFields));
|
||||||
|
} else {
|
||||||
|
ForbiddenError.from(permission).throwUnlessCan(action, ProjectPermissionSub.Secrets);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function hasSecretReadValueOrDescribePermission(
|
||||||
|
permission: MongoAbility<ProjectPermissionSet>,
|
||||||
|
action: Extract<
|
||||||
|
ProjectPermissionSecretActions,
|
||||||
|
ProjectPermissionSecretActions.DescribeSecret | ProjectPermissionSecretActions.ReadValue
|
||||||
|
>,
|
||||||
|
subjectFields?: SecretSubjectFields
|
||||||
|
) {
|
||||||
|
let canNewPermission = false;
|
||||||
|
let canOldPermission = false;
|
||||||
|
|
||||||
|
if (subjectFields) {
|
||||||
|
canNewPermission = permission.can(action, subject(ProjectPermissionSub.Secrets, subjectFields));
|
||||||
|
canOldPermission = permission.can(
|
||||||
|
ProjectPermissionSecretActions.DescribeAndReadValue,
|
||||||
|
subject(ProjectPermissionSub.Secrets, subjectFields)
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
canNewPermission = permission.can(action, ProjectPermissionSub.Secrets);
|
||||||
|
canOldPermission = permission.can(
|
||||||
|
ProjectPermissionSecretActions.DescribeAndReadValue,
|
||||||
|
ProjectPermissionSub.Secrets
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return canNewPermission || canOldPermission;
|
||||||
|
}
|
||||||
|
|
||||||
|
const OptionalArrayPermissionSchema = ProjectPermissionV2Schema.array().optional();
|
||||||
|
export function checkForInvalidPermissionCombination(permissions: z.infer<typeof OptionalArrayPermissionSchema>) {
|
||||||
|
if (!permissions) return;
|
||||||
|
|
||||||
|
for (const permission of permissions) {
|
||||||
|
if (permission.subject === ProjectPermissionSub.Secrets) {
|
||||||
|
if (permission.action.includes(ProjectPermissionSecretActions.DescribeAndReadValue)) {
|
||||||
|
const hasReadValue = permission.action.includes(ProjectPermissionSecretActions.ReadValue);
|
||||||
|
const hasDescribeSecret = permission.action.includes(ProjectPermissionSecretActions.DescribeSecret);
|
||||||
|
|
||||||
|
// eslint-disable-next-line no-continue
|
||||||
|
if (!hasReadValue && !hasDescribeSecret) continue;
|
||||||
|
|
||||||
|
const hasBothDescribeAndReadValue = hasReadValue && hasDescribeSecret;
|
||||||
|
|
||||||
|
throw new BadRequestError({
|
||||||
|
message: `You have selected Read, and ${
|
||||||
|
hasBothDescribeAndReadValue
|
||||||
|
? "both Read Value and Describe Secret"
|
||||||
|
: hasReadValue
|
||||||
|
? "Read Value"
|
||||||
|
: hasDescribeSecret
|
||||||
|
? "Describe Secret"
|
||||||
|
: ""
|
||||||
|
}. You cannot select Read Value or Describe Secret if you have selected Read. The Read permission is a legacy action which has been replaced by Describe Secret and Read Value.`
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
function isAuthMethodSaml(actorAuthMethod: ActorAuthMethod) {
|
function isAuthMethodSaml(actorAuthMethod: ActorAuthMethod) {
|
||||||
if (!actorAuthMethod) return false;
|
if (!actorAuthMethod) return false;
|
||||||
|
|
||||||
@ -29,12 +131,12 @@ function validateOrgSSO(actorAuthMethod: ActorAuthMethod, isOrgSsoEnforced: TOrg
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const escapeHandlebarsMissingMetadata = (obj: Record<string, string>) => {
|
const escapeHandlebarsMissingDict = (obj: Record<string, string>, key: string) => {
|
||||||
const handler = {
|
const handler = {
|
||||||
get(target: Record<string, string>, prop: string) {
|
get(target: Record<string, string>, prop: string) {
|
||||||
if (!(prop in target)) {
|
if (!Object.hasOwn(target, prop)) {
|
||||||
// eslint-disable-next-line no-param-reassign
|
// eslint-disable-next-line no-param-reassign
|
||||||
target[prop] = `{{identity.metadata.${prop}}}`; // Add missing key as an "own" property
|
target[prop] = `{{${key}.${prop}}}`; // Add missing key as an "own" property
|
||||||
}
|
}
|
||||||
return target[prop];
|
return target[prop];
|
||||||
}
|
}
|
||||||
@ -43,4 +145,4 @@ const escapeHandlebarsMissingMetadata = (obj: Record<string, string>) => {
|
|||||||
return new Proxy(obj, handler);
|
return new Proxy(obj, handler);
|
||||||
};
|
};
|
||||||
|
|
||||||
export { escapeHandlebarsMissingMetadata, isAuthMethodSaml, validateOrgSSO };
|
export { escapeHandlebarsMissingDict, isAuthMethodSaml, validateOrgSSO };
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
import { createMongoAbility, MongoAbility, RawRuleOf } from "@casl/ability";
|
import { createMongoAbility, MongoAbility, RawRuleOf } from "@casl/ability";
|
||||||
import { PackRule, unpackRules } from "@casl/ability/extra";
|
import { PackRule, unpackRules } from "@casl/ability/extra";
|
||||||
|
import { requestContext } from "@fastify/request-context";
|
||||||
import { MongoQuery } from "@ucast/mongo2js";
|
import { MongoQuery } from "@ucast/mongo2js";
|
||||||
import handlebars from "handlebars";
|
import handlebars from "handlebars";
|
||||||
|
|
||||||
@ -22,7 +23,7 @@ import { TServiceTokenDALFactory } from "@app/services/service-token/service-tok
|
|||||||
|
|
||||||
import { orgAdminPermissions, orgMemberPermissions, orgNoAccessPermissions, OrgPermissionSet } from "./org-permission";
|
import { orgAdminPermissions, orgMemberPermissions, orgNoAccessPermissions, OrgPermissionSet } from "./org-permission";
|
||||||
import { TPermissionDALFactory } from "./permission-dal";
|
import { TPermissionDALFactory } from "./permission-dal";
|
||||||
import { escapeHandlebarsMissingMetadata, validateOrgSSO } from "./permission-fns";
|
import { escapeHandlebarsMissingDict, validateOrgSSO } from "./permission-fns";
|
||||||
import {
|
import {
|
||||||
TBuildOrgPermissionDTO,
|
TBuildOrgPermissionDTO,
|
||||||
TBuildProjectPermissionDTO,
|
TBuildProjectPermissionDTO,
|
||||||
@ -243,20 +244,22 @@ export const permissionServiceFactory = ({
|
|||||||
|
|
||||||
const rules = buildProjectPermissionRules(rolePermissions.concat(additionalPrivileges));
|
const rules = buildProjectPermissionRules(rolePermissions.concat(additionalPrivileges));
|
||||||
const templatedRules = handlebars.compile(JSON.stringify(rules), { data: false });
|
const templatedRules = handlebars.compile(JSON.stringify(rules), { data: false });
|
||||||
const metadataKeyValuePair = escapeHandlebarsMissingMetadata(
|
const metadataKeyValuePair = escapeHandlebarsMissingDict(
|
||||||
objectify(
|
objectify(
|
||||||
userProjectPermission.metadata,
|
userProjectPermission.metadata,
|
||||||
(i) => i.key,
|
(i) => i.key,
|
||||||
(i) => i.value
|
(i) => i.value
|
||||||
)
|
),
|
||||||
|
"identity.metadata"
|
||||||
);
|
);
|
||||||
|
const templateValue = {
|
||||||
|
id: userProjectPermission.userId,
|
||||||
|
username: userProjectPermission.username,
|
||||||
|
metadata: metadataKeyValuePair
|
||||||
|
};
|
||||||
const interpolateRules = templatedRules(
|
const interpolateRules = templatedRules(
|
||||||
{
|
{
|
||||||
identity: {
|
identity: templateValue
|
||||||
id: userProjectPermission.userId,
|
|
||||||
username: userProjectPermission.username,
|
|
||||||
metadata: metadataKeyValuePair
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
{ data: false }
|
{ data: false }
|
||||||
);
|
);
|
||||||
@ -317,21 +320,26 @@ export const permissionServiceFactory = ({
|
|||||||
|
|
||||||
const rules = buildProjectPermissionRules(rolePermissions.concat(additionalPrivileges));
|
const rules = buildProjectPermissionRules(rolePermissions.concat(additionalPrivileges));
|
||||||
const templatedRules = handlebars.compile(JSON.stringify(rules), { data: false });
|
const templatedRules = handlebars.compile(JSON.stringify(rules), { data: false });
|
||||||
const metadataKeyValuePair = escapeHandlebarsMissingMetadata(
|
const unescapedIdentityAuthInfo = requestContext.get("identityAuthInfo");
|
||||||
objectify(
|
const unescapedMetadata = objectify(
|
||||||
identityProjectPermission.metadata,
|
identityProjectPermission.metadata,
|
||||||
(i) => i.key,
|
(i) => i.key,
|
||||||
(i) => i.value
|
(i) => i.value
|
||||||
)
|
|
||||||
);
|
);
|
||||||
|
const identityAuthInfo =
|
||||||
|
unescapedIdentityAuthInfo?.identityId === identityId && unescapedIdentityAuthInfo
|
||||||
|
? escapeHandlebarsMissingDict(unescapedIdentityAuthInfo as never, "identity.auth")
|
||||||
|
: {};
|
||||||
|
const metadataKeyValuePair = escapeHandlebarsMissingDict(unescapedMetadata, "identity.metadata");
|
||||||
|
const templateValue = {
|
||||||
|
id: identityProjectPermission.identityId,
|
||||||
|
username: identityProjectPermission.username,
|
||||||
|
metadata: metadataKeyValuePair,
|
||||||
|
auth: identityAuthInfo
|
||||||
|
};
|
||||||
const interpolateRules = templatedRules(
|
const interpolateRules = templatedRules(
|
||||||
{
|
{
|
||||||
identity: {
|
identity: templateValue
|
||||||
id: identityProjectPermission.identityId,
|
|
||||||
username: identityProjectPermission.username,
|
|
||||||
metadata: metadataKeyValuePair
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
{ data: false }
|
{ data: false }
|
||||||
);
|
);
|
||||||
@ -424,20 +432,22 @@ export const permissionServiceFactory = ({
|
|||||||
|
|
||||||
const rules = buildProjectPermissionRules(rolePermissions.concat(additionalPrivileges));
|
const rules = buildProjectPermissionRules(rolePermissions.concat(additionalPrivileges));
|
||||||
const templatedRules = handlebars.compile(JSON.stringify(rules), { data: false });
|
const templatedRules = handlebars.compile(JSON.stringify(rules), { data: false });
|
||||||
const metadataKeyValuePair = escapeHandlebarsMissingMetadata(
|
const metadataKeyValuePair = escapeHandlebarsMissingDict(
|
||||||
objectify(
|
objectify(
|
||||||
userProjectPermission.metadata,
|
userProjectPermission.metadata,
|
||||||
(i) => i.key,
|
(i) => i.key,
|
||||||
(i) => i.value
|
(i) => i.value
|
||||||
)
|
),
|
||||||
|
"identity.metadata"
|
||||||
);
|
);
|
||||||
|
const templateValue = {
|
||||||
|
id: userProjectPermission.userId,
|
||||||
|
username: userProjectPermission.username,
|
||||||
|
metadata: metadataKeyValuePair
|
||||||
|
};
|
||||||
const interpolateRules = templatedRules(
|
const interpolateRules = templatedRules(
|
||||||
{
|
{
|
||||||
identity: {
|
identity: templateValue
|
||||||
id: userProjectPermission.userId,
|
|
||||||
username: userProjectPermission.username,
|
|
||||||
metadata: metadataKeyValuePair
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
{ data: false }
|
{ data: false }
|
||||||
);
|
);
|
||||||
@ -469,21 +479,22 @@ export const permissionServiceFactory = ({
|
|||||||
|
|
||||||
const rules = buildProjectPermissionRules(rolePermissions.concat(additionalPrivileges));
|
const rules = buildProjectPermissionRules(rolePermissions.concat(additionalPrivileges));
|
||||||
const templatedRules = handlebars.compile(JSON.stringify(rules), { data: false });
|
const templatedRules = handlebars.compile(JSON.stringify(rules), { data: false });
|
||||||
const metadataKeyValuePair = escapeHandlebarsMissingMetadata(
|
const metadataKeyValuePair = escapeHandlebarsMissingDict(
|
||||||
objectify(
|
objectify(
|
||||||
identityProjectPermission.metadata,
|
identityProjectPermission.metadata,
|
||||||
(i) => i.key,
|
(i) => i.key,
|
||||||
(i) => i.value
|
(i) => i.value
|
||||||
)
|
),
|
||||||
|
"identity.metadata"
|
||||||
);
|
);
|
||||||
|
const templateValue = {
|
||||||
|
id: identityProjectPermission.identityId,
|
||||||
|
username: identityProjectPermission.username,
|
||||||
|
metadata: metadataKeyValuePair
|
||||||
|
};
|
||||||
const interpolateRules = templatedRules(
|
const interpolateRules = templatedRules(
|
||||||
{
|
{
|
||||||
identity: {
|
identity: templateValue
|
||||||
id: identityProjectPermission.identityId,
|
|
||||||
username: identityProjectPermission.username,
|
|
||||||
metadata: metadataKeyValuePair
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
{ data: false }
|
{ data: false }
|
||||||
);
|
);
|
||||||
|
@ -5,22 +5,6 @@ import { PermissionConditionOperators } from "@app/lib/casl";
|
|||||||
|
|
||||||
export const PermissionConditionSchema = {
|
export const PermissionConditionSchema = {
|
||||||
[PermissionConditionOperators.$IN]: z.string().trim().min(1).array(),
|
[PermissionConditionOperators.$IN]: z.string().trim().min(1).array(),
|
||||||
[PermissionConditionOperators.$ALL]: z.string().trim().min(1).array(),
|
|
||||||
[PermissionConditionOperators.$REGEX]: z
|
|
||||||
.string()
|
|
||||||
.min(1)
|
|
||||||
.refine(
|
|
||||||
(el) => {
|
|
||||||
try {
|
|
||||||
// eslint-disable-next-line no-new
|
|
||||||
new RegExp(el);
|
|
||||||
return true;
|
|
||||||
} catch {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{ message: "Invalid regex pattern" }
|
|
||||||
),
|
|
||||||
[PermissionConditionOperators.$EQ]: z.string().min(1),
|
[PermissionConditionOperators.$EQ]: z.string().min(1),
|
||||||
[PermissionConditionOperators.$NEQ]: z.string().min(1),
|
[PermissionConditionOperators.$NEQ]: z.string().min(1),
|
||||||
[PermissionConditionOperators.$GLOB]: z
|
[PermissionConditionOperators.$GLOB]: z
|
||||||
|
@ -17,6 +17,15 @@ export enum ProjectPermissionActions {
|
|||||||
Delete = "delete"
|
Delete = "delete"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export enum ProjectPermissionSecretActions {
|
||||||
|
DescribeAndReadValue = "read",
|
||||||
|
DescribeSecret = "describeSecret",
|
||||||
|
ReadValue = "readValue",
|
||||||
|
Create = "create",
|
||||||
|
Edit = "edit",
|
||||||
|
Delete = "delete"
|
||||||
|
}
|
||||||
|
|
||||||
export enum ProjectPermissionCmekActions {
|
export enum ProjectPermissionCmekActions {
|
||||||
Read = "read",
|
Read = "read",
|
||||||
Create = "create",
|
Create = "create",
|
||||||
@ -115,7 +124,7 @@ export type IdentityManagementSubjectFields = {
|
|||||||
|
|
||||||
export type ProjectPermissionSet =
|
export type ProjectPermissionSet =
|
||||||
| [
|
| [
|
||||||
ProjectPermissionActions,
|
ProjectPermissionSecretActions,
|
||||||
ProjectPermissionSub.Secrets | (ForcedSubject<ProjectPermissionSub.Secrets> & SecretSubjectFields)
|
ProjectPermissionSub.Secrets | (ForcedSubject<ProjectPermissionSub.Secrets> & SecretSubjectFields)
|
||||||
]
|
]
|
||||||
| [
|
| [
|
||||||
@ -429,6 +438,7 @@ const GeneralPermissionSchema = [
|
|||||||
})
|
})
|
||||||
];
|
];
|
||||||
|
|
||||||
|
// Do not update this schema anymore, as it's kept purely for backwards compatability. Update V2 schema only.
|
||||||
export const ProjectPermissionV1Schema = z.discriminatedUnion("subject", [
|
export const ProjectPermissionV1Schema = z.discriminatedUnion("subject", [
|
||||||
z.object({
|
z.object({
|
||||||
subject: z.literal(ProjectPermissionSub.Secrets).describe("The entity this permission pertains to."),
|
subject: z.literal(ProjectPermissionSub.Secrets).describe("The entity this permission pertains to."),
|
||||||
@ -460,7 +470,7 @@ export const ProjectPermissionV2Schema = z.discriminatedUnion("subject", [
|
|||||||
z.object({
|
z.object({
|
||||||
subject: z.literal(ProjectPermissionSub.Secrets).describe("The entity this permission pertains to."),
|
subject: z.literal(ProjectPermissionSub.Secrets).describe("The entity this permission pertains to."),
|
||||||
inverted: z.boolean().optional().describe("Whether rule allows or forbids."),
|
inverted: z.boolean().optional().describe("Whether rule allows or forbids."),
|
||||||
action: CASL_ACTION_SCHEMA_NATIVE_ENUM(ProjectPermissionActions).describe(
|
action: CASL_ACTION_SCHEMA_NATIVE_ENUM(ProjectPermissionSecretActions).describe(
|
||||||
"Describe what action an entity can take."
|
"Describe what action an entity can take."
|
||||||
),
|
),
|
||||||
conditions: SecretConditionV2Schema.describe(
|
conditions: SecretConditionV2Schema.describe(
|
||||||
@ -517,7 +527,6 @@ const buildAdminPermissionRules = () => {
|
|||||||
|
|
||||||
// Admins get full access to everything
|
// Admins get full access to everything
|
||||||
[
|
[
|
||||||
ProjectPermissionSub.Secrets,
|
|
||||||
ProjectPermissionSub.SecretFolders,
|
ProjectPermissionSub.SecretFolders,
|
||||||
ProjectPermissionSub.SecretImports,
|
ProjectPermissionSub.SecretImports,
|
||||||
ProjectPermissionSub.SecretApproval,
|
ProjectPermissionSub.SecretApproval,
|
||||||
@ -550,10 +559,22 @@ const buildAdminPermissionRules = () => {
|
|||||||
ProjectPermissionActions.Create,
|
ProjectPermissionActions.Create,
|
||||||
ProjectPermissionActions.Delete
|
ProjectPermissionActions.Delete
|
||||||
],
|
],
|
||||||
el as ProjectPermissionSub
|
el
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
can(
|
||||||
|
[
|
||||||
|
ProjectPermissionSecretActions.DescribeAndReadValue,
|
||||||
|
ProjectPermissionSecretActions.DescribeSecret,
|
||||||
|
ProjectPermissionSecretActions.ReadValue,
|
||||||
|
ProjectPermissionSecretActions.Create,
|
||||||
|
ProjectPermissionSecretActions.Edit,
|
||||||
|
ProjectPermissionSecretActions.Delete
|
||||||
|
],
|
||||||
|
ProjectPermissionSub.Secrets
|
||||||
|
);
|
||||||
|
|
||||||
can(
|
can(
|
||||||
[
|
[
|
||||||
ProjectPermissionDynamicSecretActions.ReadRootCredential,
|
ProjectPermissionDynamicSecretActions.ReadRootCredential,
|
||||||
@ -613,10 +634,12 @@ const buildMemberPermissionRules = () => {
|
|||||||
|
|
||||||
can(
|
can(
|
||||||
[
|
[
|
||||||
ProjectPermissionActions.Read,
|
ProjectPermissionSecretActions.DescribeAndReadValue,
|
||||||
ProjectPermissionActions.Edit,
|
ProjectPermissionSecretActions.DescribeSecret,
|
||||||
ProjectPermissionActions.Create,
|
ProjectPermissionSecretActions.ReadValue,
|
||||||
ProjectPermissionActions.Delete
|
ProjectPermissionSecretActions.Edit,
|
||||||
|
ProjectPermissionSecretActions.Create,
|
||||||
|
ProjectPermissionSecretActions.Delete
|
||||||
],
|
],
|
||||||
ProjectPermissionSub.Secrets
|
ProjectPermissionSub.Secrets
|
||||||
);
|
);
|
||||||
@ -788,7 +811,9 @@ export const projectMemberPermissions = buildMemberPermissionRules();
|
|||||||
const buildViewerPermissionRules = () => {
|
const buildViewerPermissionRules = () => {
|
||||||
const { can, rules } = new AbilityBuilder<MongoAbility<ProjectPermissionSet>>(createMongoAbility);
|
const { can, rules } = new AbilityBuilder<MongoAbility<ProjectPermissionSet>>(createMongoAbility);
|
||||||
|
|
||||||
can(ProjectPermissionActions.Read, ProjectPermissionSub.Secrets);
|
can(ProjectPermissionSecretActions.DescribeAndReadValue, ProjectPermissionSub.Secrets);
|
||||||
|
can(ProjectPermissionSecretActions.DescribeSecret, ProjectPermissionSub.Secrets);
|
||||||
|
can(ProjectPermissionSecretActions.ReadValue, ProjectPermissionSub.Secrets);
|
||||||
can(ProjectPermissionActions.Read, ProjectPermissionSub.SecretFolders);
|
can(ProjectPermissionActions.Read, ProjectPermissionSub.SecretFolders);
|
||||||
can(ProjectPermissionDynamicSecretActions.ReadRootCredential, ProjectPermissionSub.DynamicSecrets);
|
can(ProjectPermissionDynamicSecretActions.ReadRootCredential, ProjectPermissionSub.DynamicSecrets);
|
||||||
can(ProjectPermissionActions.Read, ProjectPermissionSub.SecretImports);
|
can(ProjectPermissionActions.Read, ProjectPermissionSub.SecretImports);
|
||||||
@ -837,7 +862,6 @@ export const buildServiceTokenProjectPermission = (
|
|||||||
(subject) => {
|
(subject) => {
|
||||||
if (canWrite) {
|
if (canWrite) {
|
||||||
can(ProjectPermissionActions.Edit, subject, {
|
can(ProjectPermissionActions.Edit, subject, {
|
||||||
// TODO: @Akhi
|
|
||||||
// @ts-expect-error type
|
// @ts-expect-error type
|
||||||
secretPath: { $glob: secretPath },
|
secretPath: { $glob: secretPath },
|
||||||
environment
|
environment
|
||||||
@ -916,7 +940,17 @@ export const backfillPermissionV1SchemaToV2Schema = (
|
|||||||
subject: ProjectPermissionSub.SecretImports as const
|
subject: ProjectPermissionSub.SecretImports as const
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
const secretPolicies = secretSubjects.map(({ subject, ...el }) => ({
|
||||||
|
subject: ProjectPermissionSub.Secrets as const,
|
||||||
|
...el,
|
||||||
|
action:
|
||||||
|
el.action.includes(ProjectPermissionActions.Read) && !el.action.includes(ProjectPermissionSecretActions.ReadValue)
|
||||||
|
? el.action.concat(ProjectPermissionSecretActions.ReadValue)
|
||||||
|
: el.action
|
||||||
|
}));
|
||||||
|
|
||||||
const secretFolderPolicies = secretSubjects
|
const secretFolderPolicies = secretSubjects
|
||||||
|
|
||||||
.map(({ subject, ...el }) => ({
|
.map(({ subject, ...el }) => ({
|
||||||
...el,
|
...el,
|
||||||
// read permission is not needed anymore
|
// read permission is not needed anymore
|
||||||
@ -958,6 +992,7 @@ export const backfillPermissionV1SchemaToV2Schema = (
|
|||||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||||
// @ts-ignore-error this is valid ts
|
// @ts-ignore-error this is valid ts
|
||||||
secretImportPolicies,
|
secretImportPolicies,
|
||||||
|
secretPolicies,
|
||||||
dynamicSecretPolicies,
|
dynamicSecretPolicies,
|
||||||
hasReadOnlyFolder.length ? [] : secretFolderPolicies
|
hasReadOnlyFolder.length ? [] : secretFolderPolicies
|
||||||
);
|
);
|
||||||
|
@ -1,10 +1,10 @@
|
|||||||
import { ForbiddenError, MongoAbility, RawRuleOf } from "@casl/ability";
|
import { ForbiddenError, MongoAbility, RawRuleOf } from "@casl/ability";
|
||||||
import { PackRule, packRules, unpackRules } from "@casl/ability/extra";
|
import { PackRule, packRules, unpackRules } from "@casl/ability/extra";
|
||||||
import ms from "ms";
|
|
||||||
|
|
||||||
import { ActionProjectType, TableName } from "@app/db/schemas";
|
import { ActionProjectType, TableName } from "@app/db/schemas";
|
||||||
import { isAtLeastAsPrivileged } from "@app/lib/casl";
|
import { validatePermissionBoundary } from "@app/lib/casl/boundary";
|
||||||
import { BadRequestError, ForbiddenRequestError, NotFoundError } from "@app/lib/errors";
|
import { BadRequestError, ForbiddenRequestError, NotFoundError } from "@app/lib/errors";
|
||||||
|
import { ms } from "@app/lib/ms";
|
||||||
import { UnpackedPermissionSchema } from "@app/server/routes/sanitizedSchema/permission";
|
import { UnpackedPermissionSchema } from "@app/server/routes/sanitizedSchema/permission";
|
||||||
import { ActorType } from "@app/services/auth/auth-type";
|
import { ActorType } from "@app/services/auth/auth-type";
|
||||||
import { TProjectMembershipDALFactory } from "@app/services/project-membership/project-membership-dal";
|
import { TProjectMembershipDALFactory } from "@app/services/project-membership/project-membership-dal";
|
||||||
@ -76,9 +76,13 @@ export const projectUserAdditionalPrivilegeServiceFactory = ({
|
|||||||
// we need to validate that the privilege given is not higher than the assigning users permission
|
// we need to validate that the privilege given is not higher than the assigning users permission
|
||||||
// @ts-expect-error this is expected error because of one being really accurate rule definition other being a bit more broader. Both are valid casl rules
|
// @ts-expect-error this is expected error because of one being really accurate rule definition other being a bit more broader. Both are valid casl rules
|
||||||
targetUserPermission.update(targetUserPermission.rules.concat(customPermission));
|
targetUserPermission.update(targetUserPermission.rules.concat(customPermission));
|
||||||
const hasRequiredPriviledges = isAtLeastAsPrivileged(permission, targetUserPermission);
|
const permissionBoundary = validatePermissionBoundary(permission, targetUserPermission);
|
||||||
if (!hasRequiredPriviledges)
|
if (!permissionBoundary.isValid)
|
||||||
throw new ForbiddenRequestError({ message: "Failed to update more privileged identity" });
|
throw new ForbiddenRequestError({
|
||||||
|
name: "PermissionBoundaryError",
|
||||||
|
message: "Failed to update more privileged user",
|
||||||
|
details: { missingPermissions: permissionBoundary.missingPermissions }
|
||||||
|
});
|
||||||
|
|
||||||
const existingSlug = await projectUserAdditionalPrivilegeDAL.findOne({
|
const existingSlug = await projectUserAdditionalPrivilegeDAL.findOne({
|
||||||
slug,
|
slug,
|
||||||
@ -163,9 +167,13 @@ export const projectUserAdditionalPrivilegeServiceFactory = ({
|
|||||||
// we need to validate that the privilege given is not higher than the assigning users permission
|
// we need to validate that the privilege given is not higher than the assigning users permission
|
||||||
// @ts-expect-error this is expected error because of one being really accurate rule definition other being a bit more broader. Both are valid casl rules
|
// @ts-expect-error this is expected error because of one being really accurate rule definition other being a bit more broader. Both are valid casl rules
|
||||||
targetUserPermission.update(targetUserPermission.rules.concat(dto.permissions || []));
|
targetUserPermission.update(targetUserPermission.rules.concat(dto.permissions || []));
|
||||||
const hasRequiredPriviledges = isAtLeastAsPrivileged(permission, targetUserPermission);
|
const permissionBoundary = validatePermissionBoundary(permission, targetUserPermission);
|
||||||
if (!hasRequiredPriviledges)
|
if (!permissionBoundary.isValid)
|
||||||
throw new ForbiddenRequestError({ message: "Failed to update more privileged identity" });
|
throw new ForbiddenRequestError({
|
||||||
|
name: "PermissionBoundaryError",
|
||||||
|
message: "Failed to update more privileged identity",
|
||||||
|
details: { missingPermissions: permissionBoundary.missingPermissions }
|
||||||
|
});
|
||||||
|
|
||||||
if (dto?.slug) {
|
if (dto?.slug) {
|
||||||
const existingSlug = await projectUserAdditionalPrivilegeDAL.findOne({
|
const existingSlug = await projectUserAdditionalPrivilegeDAL.findOne({
|
||||||
|
@ -63,7 +63,7 @@ export const samlConfigServiceFactory = ({
|
|||||||
kmsService
|
kmsService
|
||||||
}: TSamlConfigServiceFactoryDep) => {
|
}: TSamlConfigServiceFactoryDep) => {
|
||||||
const createSamlCfg = async ({
|
const createSamlCfg = async ({
|
||||||
cert,
|
idpCert,
|
||||||
actor,
|
actor,
|
||||||
actorAuthMethod,
|
actorAuthMethod,
|
||||||
actorOrgId,
|
actorOrgId,
|
||||||
@ -93,9 +93,9 @@ export const samlConfigServiceFactory = ({
|
|||||||
orgId,
|
orgId,
|
||||||
authProvider,
|
authProvider,
|
||||||
isActive,
|
isActive,
|
||||||
encryptedSamlIssuer: encryptor({ plainText: Buffer.from(issuer) }).cipherTextBlob,
|
encryptedSamlCertificate: encryptor({ plainText: Buffer.from(idpCert) }).cipherTextBlob,
|
||||||
encryptedSamlEntryPoint: encryptor({ plainText: Buffer.from(entryPoint) }).cipherTextBlob,
|
encryptedSamlEntryPoint: encryptor({ plainText: Buffer.from(entryPoint) }).cipherTextBlob,
|
||||||
encryptedSamlCertificate: encryptor({ plainText: Buffer.from(cert) }).cipherTextBlob
|
encryptedSamlIssuer: encryptor({ plainText: Buffer.from(issuer) }).cipherTextBlob
|
||||||
});
|
});
|
||||||
|
|
||||||
return samlConfig;
|
return samlConfig;
|
||||||
@ -106,7 +106,7 @@ export const samlConfigServiceFactory = ({
|
|||||||
actor,
|
actor,
|
||||||
actorOrgId,
|
actorOrgId,
|
||||||
actorAuthMethod,
|
actorAuthMethod,
|
||||||
cert,
|
idpCert,
|
||||||
actorId,
|
actorId,
|
||||||
issuer,
|
issuer,
|
||||||
isActive,
|
isActive,
|
||||||
@ -136,8 +136,8 @@ export const samlConfigServiceFactory = ({
|
|||||||
updateQuery.encryptedSamlIssuer = encryptor({ plainText: Buffer.from(issuer) }).cipherTextBlob;
|
updateQuery.encryptedSamlIssuer = encryptor({ plainText: Buffer.from(issuer) }).cipherTextBlob;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (cert !== undefined) {
|
if (idpCert !== undefined) {
|
||||||
updateQuery.encryptedSamlCertificate = encryptor({ plainText: Buffer.from(cert) }).cipherTextBlob;
|
updateQuery.encryptedSamlCertificate = encryptor({ plainText: Buffer.from(idpCert) }).cipherTextBlob;
|
||||||
}
|
}
|
||||||
|
|
||||||
const [ssoConfig] = await samlConfigDAL.update({ orgId }, updateQuery);
|
const [ssoConfig] = await samlConfigDAL.update({ orgId }, updateQuery);
|
||||||
|
@ -15,7 +15,7 @@ export type TCreateSamlCfgDTO = {
|
|||||||
isActive: boolean;
|
isActive: boolean;
|
||||||
entryPoint: string;
|
entryPoint: string;
|
||||||
issuer: string;
|
issuer: string;
|
||||||
cert: string;
|
idpCert: string;
|
||||||
} & TOrgPermission;
|
} & TOrgPermission;
|
||||||
|
|
||||||
export type TUpdateSamlCfgDTO = Partial<{
|
export type TUpdateSamlCfgDTO = Partial<{
|
||||||
@ -23,7 +23,7 @@ export type TUpdateSamlCfgDTO = Partial<{
|
|||||||
isActive: boolean;
|
isActive: boolean;
|
||||||
entryPoint: string;
|
entryPoint: string;
|
||||||
issuer: string;
|
issuer: string;
|
||||||
cert: string;
|
idpCert: string;
|
||||||
}> &
|
}> &
|
||||||
TOrgPermission;
|
TOrgPermission;
|
||||||
|
|
||||||
|
@ -100,6 +100,7 @@ export const secretApprovalRequestDALFactory = (db: TDbClient) => {
|
|||||||
tx.ref("lastName").withSchema("committerUser").as("committerUserLastName"),
|
tx.ref("lastName").withSchema("committerUser").as("committerUserLastName"),
|
||||||
tx.ref("reviewerUserId").withSchema(TableName.SecretApprovalRequestReviewer),
|
tx.ref("reviewerUserId").withSchema(TableName.SecretApprovalRequestReviewer),
|
||||||
tx.ref("status").withSchema(TableName.SecretApprovalRequestReviewer).as("reviewerStatus"),
|
tx.ref("status").withSchema(TableName.SecretApprovalRequestReviewer).as("reviewerStatus"),
|
||||||
|
tx.ref("comment").withSchema(TableName.SecretApprovalRequestReviewer).as("reviewerComment"),
|
||||||
tx.ref("email").withSchema("secretApprovalReviewerUser").as("reviewerEmail"),
|
tx.ref("email").withSchema("secretApprovalReviewerUser").as("reviewerEmail"),
|
||||||
tx.ref("username").withSchema("secretApprovalReviewerUser").as("reviewerUsername"),
|
tx.ref("username").withSchema("secretApprovalReviewerUser").as("reviewerUsername"),
|
||||||
tx.ref("firstName").withSchema("secretApprovalReviewerUser").as("reviewerFirstName"),
|
tx.ref("firstName").withSchema("secretApprovalReviewerUser").as("reviewerFirstName"),
|
||||||
@ -162,8 +163,10 @@ export const secretApprovalRequestDALFactory = (db: TDbClient) => {
|
|||||||
reviewerEmail: email,
|
reviewerEmail: email,
|
||||||
reviewerLastName: lastName,
|
reviewerLastName: lastName,
|
||||||
reviewerUsername: username,
|
reviewerUsername: username,
|
||||||
reviewerFirstName: firstName
|
reviewerFirstName: firstName,
|
||||||
}) => (userId ? { userId, status, email, firstName, lastName, username } : undefined)
|
reviewerComment: comment
|
||||||
|
}) =>
|
||||||
|
userId ? { userId, status, email, firstName, lastName, username, comment: comment ?? "" } : undefined
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: "approverUserId",
|
key: "approverUserId",
|
||||||
|
@ -6,6 +6,7 @@ import {
|
|||||||
SecretEncryptionAlgo,
|
SecretEncryptionAlgo,
|
||||||
SecretKeyEncoding,
|
SecretKeyEncoding,
|
||||||
SecretType,
|
SecretType,
|
||||||
|
TableName,
|
||||||
TSecretApprovalRequestsSecretsInsert,
|
TSecretApprovalRequestsSecretsInsert,
|
||||||
TSecretApprovalRequestsSecretsV2Insert
|
TSecretApprovalRequestsSecretsV2Insert
|
||||||
} from "@app/db/schemas";
|
} from "@app/db/schemas";
|
||||||
@ -57,8 +58,9 @@ import { SmtpTemplates, TSmtpService } from "@app/services/smtp/smtp-service";
|
|||||||
import { TUserDALFactory } from "@app/services/user/user-dal";
|
import { TUserDALFactory } from "@app/services/user/user-dal";
|
||||||
|
|
||||||
import { TLicenseServiceFactory } from "../license/license-service";
|
import { TLicenseServiceFactory } from "../license/license-service";
|
||||||
|
import { throwIfMissingSecretReadValueOrDescribePermission } from "../permission/permission-fns";
|
||||||
import { TPermissionServiceFactory } from "../permission/permission-service";
|
import { TPermissionServiceFactory } from "../permission/permission-service";
|
||||||
import { ProjectPermissionActions, ProjectPermissionSub } from "../permission/project-permission";
|
import { ProjectPermissionSecretActions, ProjectPermissionSub } from "../permission/project-permission";
|
||||||
import { TSecretApprovalPolicyDALFactory } from "../secret-approval-policy/secret-approval-policy-dal";
|
import { TSecretApprovalPolicyDALFactory } from "../secret-approval-policy/secret-approval-policy-dal";
|
||||||
import { TSecretSnapshotServiceFactory } from "../secret-snapshot/secret-snapshot-service";
|
import { TSecretSnapshotServiceFactory } from "../secret-snapshot/secret-snapshot-service";
|
||||||
import { TSecretApprovalRequestDALFactory } from "./secret-approval-request-dal";
|
import { TSecretApprovalRequestDALFactory } from "./secret-approval-request-dal";
|
||||||
@ -88,7 +90,12 @@ type TSecretApprovalRequestServiceFactoryDep = {
|
|||||||
secretDAL: TSecretDALFactory;
|
secretDAL: TSecretDALFactory;
|
||||||
secretTagDAL: Pick<
|
secretTagDAL: Pick<
|
||||||
TSecretTagDALFactory,
|
TSecretTagDALFactory,
|
||||||
"findManyTagsById" | "saveTagsToSecret" | "deleteTagsManySecret" | "saveTagsToSecretV2" | "deleteTagsToSecretV2"
|
| "findManyTagsById"
|
||||||
|
| "saveTagsToSecret"
|
||||||
|
| "deleteTagsManySecret"
|
||||||
|
| "saveTagsToSecretV2"
|
||||||
|
| "deleteTagsToSecretV2"
|
||||||
|
| "find"
|
||||||
>;
|
>;
|
||||||
secretBlindIndexDAL: Pick<TSecretBlindIndexDALFactory, "findOne">;
|
secretBlindIndexDAL: Pick<TSecretBlindIndexDALFactory, "findOne">;
|
||||||
snapshotService: Pick<TSecretSnapshotServiceFactory, "performSnapshot">;
|
snapshotService: Pick<TSecretSnapshotServiceFactory, "performSnapshot">;
|
||||||
@ -106,7 +113,7 @@ type TSecretApprovalRequestServiceFactoryDep = {
|
|||||||
kmsService: Pick<TKmsServiceFactory, "createCipherPairWithDataKey" | "encryptWithInputKey" | "decryptWithInputKey">;
|
kmsService: Pick<TKmsServiceFactory, "createCipherPairWithDataKey" | "encryptWithInputKey" | "decryptWithInputKey">;
|
||||||
secretV2BridgeDAL: Pick<
|
secretV2BridgeDAL: Pick<
|
||||||
TSecretV2BridgeDALFactory,
|
TSecretV2BridgeDALFactory,
|
||||||
"insertMany" | "upsertSecretReferences" | "findBySecretKeys" | "bulkUpdate" | "deleteMany"
|
"insertMany" | "upsertSecretReferences" | "findBySecretKeys" | "bulkUpdate" | "deleteMany" | "find"
|
||||||
>;
|
>;
|
||||||
secretVersionV2BridgeDAL: Pick<TSecretVersionV2DALFactory, "insertMany" | "findLatestVersionMany">;
|
secretVersionV2BridgeDAL: Pick<TSecretVersionV2DALFactory, "insertMany" | "findLatestVersionMany">;
|
||||||
secretVersionTagV2BridgeDAL: Pick<TSecretVersionV2TagDALFactory, "insertMany">;
|
secretVersionTagV2BridgeDAL: Pick<TSecretVersionV2TagDALFactory, "insertMany">;
|
||||||
@ -320,6 +327,7 @@ export const secretApprovalRequestServiceFactory = ({
|
|||||||
approvalId,
|
approvalId,
|
||||||
actor,
|
actor,
|
||||||
status,
|
status,
|
||||||
|
comment,
|
||||||
actorId,
|
actorId,
|
||||||
actorAuthMethod,
|
actorAuthMethod,
|
||||||
actorOrgId
|
actorOrgId
|
||||||
@ -372,15 +380,18 @@ export const secretApprovalRequestServiceFactory = ({
|
|||||||
return secretApprovalRequestReviewerDAL.create(
|
return secretApprovalRequestReviewerDAL.create(
|
||||||
{
|
{
|
||||||
status,
|
status,
|
||||||
|
comment,
|
||||||
requestId: secretApprovalRequest.id,
|
requestId: secretApprovalRequest.id,
|
||||||
reviewerUserId: actorId
|
reviewerUserId: actorId
|
||||||
},
|
},
|
||||||
tx
|
tx
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
return secretApprovalRequestReviewerDAL.updateById(review.id, { status }, tx);
|
|
||||||
|
return secretApprovalRequestReviewerDAL.updateById(review.id, { status, comment }, tx);
|
||||||
});
|
});
|
||||||
return reviewStatus;
|
|
||||||
|
return { ...reviewStatus, projectId: secretApprovalRequest.projectId };
|
||||||
};
|
};
|
||||||
|
|
||||||
const updateApprovalStatus = async ({
|
const updateApprovalStatus = async ({
|
||||||
@ -499,7 +510,7 @@ export const secretApprovalRequestServiceFactory = ({
|
|||||||
if (!hasMinApproval && !isSoftEnforcement)
|
if (!hasMinApproval && !isSoftEnforcement)
|
||||||
throw new BadRequestError({ message: "Doesn't have minimum approvals needed" });
|
throw new BadRequestError({ message: "Doesn't have minimum approvals needed" });
|
||||||
|
|
||||||
const { botKey, shouldUseSecretV2Bridge } = await projectBotService.getBotKey(projectId);
|
const { botKey, shouldUseSecretV2Bridge, project } = await projectBotService.getBotKey(projectId);
|
||||||
let mergeStatus;
|
let mergeStatus;
|
||||||
if (shouldUseSecretV2Bridge) {
|
if (shouldUseSecretV2Bridge) {
|
||||||
// this cycle if for bridged secrets
|
// this cycle if for bridged secrets
|
||||||
@ -857,7 +868,6 @@ export const secretApprovalRequestServiceFactory = ({
|
|||||||
|
|
||||||
if (isSoftEnforcement) {
|
if (isSoftEnforcement) {
|
||||||
const cfg = getConfig();
|
const cfg = getConfig();
|
||||||
const project = await projectDAL.findProjectById(projectId);
|
|
||||||
const env = await projectEnvDAL.findOne({ id: policy.envId });
|
const env = await projectEnvDAL.findOne({ id: policy.envId });
|
||||||
const requestedByUser = await userDAL.findOne({ id: actorId });
|
const requestedByUser = await userDAL.findOne({ id: actorId });
|
||||||
const approverUsers = await userDAL.find({
|
const approverUsers = await userDAL.find({
|
||||||
@ -909,10 +919,11 @@ export const secretApprovalRequestServiceFactory = ({
|
|||||||
actorOrgId,
|
actorOrgId,
|
||||||
actionProjectType: ActionProjectType.SecretManager
|
actionProjectType: ActionProjectType.SecretManager
|
||||||
});
|
});
|
||||||
ForbiddenError.from(permission).throwUnlessCan(
|
|
||||||
ProjectPermissionActions.Read,
|
throwIfMissingSecretReadValueOrDescribePermission(permission, ProjectPermissionSecretActions.ReadValue, {
|
||||||
subject(ProjectPermissionSub.Secrets, { environment, secretPath })
|
environment,
|
||||||
);
|
secretPath
|
||||||
|
});
|
||||||
|
|
||||||
await projectDAL.checkProjectUpgradeStatus(projectId);
|
await projectDAL.checkProjectUpgradeStatus(projectId);
|
||||||
|
|
||||||
@ -997,6 +1008,7 @@ export const secretApprovalRequestServiceFactory = ({
|
|||||||
: keyName2BlindIndex[secretName];
|
: keyName2BlindIndex[secretName];
|
||||||
// add tags
|
// add tags
|
||||||
if (tagIds?.length) commitTagIds[keyName2BlindIndex[secretName]] = tagIds;
|
if (tagIds?.length) commitTagIds[keyName2BlindIndex[secretName]] = tagIds;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
...latestSecretVersions[secretId],
|
...latestSecretVersions[secretId],
|
||||||
...el,
|
...el,
|
||||||
@ -1152,7 +1164,8 @@ export const secretApprovalRequestServiceFactory = ({
|
|||||||
environment: env.name,
|
environment: env.name,
|
||||||
secretPath,
|
secretPath,
|
||||||
projectId,
|
projectId,
|
||||||
requestId: secretApprovalRequest.id
|
requestId: secretApprovalRequest.id,
|
||||||
|
secretKeys: [...new Set(Object.values(data).flatMap((arr) => arr?.map((item) => item.secretName) ?? []))]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@ -1294,7 +1307,7 @@ export const secretApprovalRequestServiceFactory = ({
|
|||||||
secretMetadata
|
secretMetadata
|
||||||
}) => {
|
}) => {
|
||||||
const secretId = updatingSecretsGroupByKey[secretKey][0].id;
|
const secretId = updatingSecretsGroupByKey[secretKey][0].id;
|
||||||
if (tagIds?.length) commitTagIds[secretKey] = tagIds;
|
if (tagIds?.length) commitTagIds[newSecretName ?? secretKey] = tagIds;
|
||||||
return {
|
return {
|
||||||
...latestSecretVersions[secretId],
|
...latestSecretVersions[secretId],
|
||||||
secretMetadata,
|
secretMetadata,
|
||||||
@ -1323,17 +1336,48 @@ export const secretApprovalRequestServiceFactory = ({
|
|||||||
// deleted secrets
|
// deleted secrets
|
||||||
const deletedSecrets = data[SecretOperations.Delete];
|
const deletedSecrets = data[SecretOperations.Delete];
|
||||||
if (deletedSecrets && deletedSecrets.length) {
|
if (deletedSecrets && deletedSecrets.length) {
|
||||||
const secretsToDeleteInDB = await secretV2BridgeDAL.findBySecretKeys(
|
const secretsToDeleteInDB = await secretV2BridgeDAL.find({
|
||||||
folderId,
|
folderId,
|
||||||
deletedSecrets.map((el) => ({
|
$complex: {
|
||||||
key: el.secretKey,
|
operator: "and",
|
||||||
type: SecretType.Shared
|
value: [
|
||||||
}))
|
{
|
||||||
);
|
operator: "or",
|
||||||
|
value: deletedSecrets.map((el) => ({
|
||||||
|
operator: "and",
|
||||||
|
value: [
|
||||||
|
{
|
||||||
|
operator: "eq",
|
||||||
|
field: `${TableName.SecretV2}.key` as "key",
|
||||||
|
value: el.secretKey
|
||||||
|
},
|
||||||
|
{
|
||||||
|
operator: "eq",
|
||||||
|
field: "type",
|
||||||
|
value: SecretType.Shared
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
});
|
||||||
if (secretsToDeleteInDB.length !== deletedSecrets.length)
|
if (secretsToDeleteInDB.length !== deletedSecrets.length)
|
||||||
throw new NotFoundError({
|
throw new NotFoundError({
|
||||||
message: `Secret does not exist: ${secretsToDeleteInDB.map((el) => el.key).join(",")}`
|
message: `Secret does not exist: ${secretsToDeleteInDB.map((el) => el.key).join(",")}`
|
||||||
});
|
});
|
||||||
|
secretsToDeleteInDB.forEach((el) => {
|
||||||
|
ForbiddenError.from(permission).throwUnlessCan(
|
||||||
|
ProjectPermissionSecretActions.Delete,
|
||||||
|
subject(ProjectPermissionSub.Secrets, {
|
||||||
|
environment,
|
||||||
|
secretPath,
|
||||||
|
secretName: el.key,
|
||||||
|
secretTags: el.tags?.map((i) => i.slug)
|
||||||
|
})
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
const secretsGroupedByKey = groupBy(secretsToDeleteInDB, (i) => i.key);
|
const secretsGroupedByKey = groupBy(secretsToDeleteInDB, (i) => i.key);
|
||||||
const deletedSecretIds = deletedSecrets.map((el) => secretsGroupedByKey[el.secretKey][0].id);
|
const deletedSecretIds = deletedSecrets.map((el) => secretsGroupedByKey[el.secretKey][0].id);
|
||||||
const latestSecretVersions = await secretVersionV2BridgeDAL.findLatestVersionMany(folderId, deletedSecretIds);
|
const latestSecretVersions = await secretVersionV2BridgeDAL.findLatestVersionMany(folderId, deletedSecretIds);
|
||||||
@ -1359,9 +1403,9 @@ export const secretApprovalRequestServiceFactory = ({
|
|||||||
const tagsGroupById = groupBy(tags, (i) => i.id);
|
const tagsGroupById = groupBy(tags, (i) => i.id);
|
||||||
|
|
||||||
commits.forEach((commit) => {
|
commits.forEach((commit) => {
|
||||||
let action = ProjectPermissionActions.Create;
|
let action = ProjectPermissionSecretActions.Create;
|
||||||
if (commit.op === SecretOperations.Update) action = ProjectPermissionActions.Edit;
|
if (commit.op === SecretOperations.Update) action = ProjectPermissionSecretActions.Edit;
|
||||||
if (commit.op === SecretOperations.Delete) action = ProjectPermissionActions.Delete;
|
if (commit.op === SecretOperations.Delete) return; // we do the validation on top
|
||||||
|
|
||||||
ForbiddenError.from(permission).throwUnlessCan(
|
ForbiddenError.from(permission).throwUnlessCan(
|
||||||
action,
|
action,
|
||||||
@ -1452,7 +1496,8 @@ export const secretApprovalRequestServiceFactory = ({
|
|||||||
environment: env.name,
|
environment: env.name,
|
||||||
secretPath,
|
secretPath,
|
||||||
projectId,
|
projectId,
|
||||||
requestId: secretApprovalRequest.id
|
requestId: secretApprovalRequest.id,
|
||||||
|
secretKeys: [...new Set(Object.values(data).flatMap((arr) => arr?.map((item) => item.secretKey) ?? []))]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
@ -80,6 +80,7 @@ export type TStatusChangeDTO = {
|
|||||||
export type TReviewRequestDTO = {
|
export type TReviewRequestDTO = {
|
||||||
approvalId: string;
|
approvalId: string;
|
||||||
status: ApprovalStatus;
|
status: ApprovalStatus;
|
||||||
|
comment?: string;
|
||||||
} & Omit<TProjectPermission, "projectId">;
|
} & Omit<TProjectPermission, "projectId">;
|
||||||
|
|
||||||
export type TApprovalRequestCountDTO = TProjectPermission;
|
export type TApprovalRequestCountDTO = TProjectPermission;
|
||||||
|
@ -265,6 +265,7 @@ export const secretReplicationServiceFactory = ({
|
|||||||
folderDAL,
|
folderDAL,
|
||||||
secretImportDAL,
|
secretImportDAL,
|
||||||
decryptor: (value) => (value ? secretManagerDecryptor({ cipherTextBlob: value }).toString() : ""),
|
decryptor: (value) => (value ? secretManagerDecryptor({ cipherTextBlob: value }).toString() : ""),
|
||||||
|
viewSecretValue: true,
|
||||||
hasSecretAccess: () => true
|
hasSecretAccess: () => true
|
||||||
});
|
});
|
||||||
// secrets that gets replicated across imports
|
// secrets that gets replicated across imports
|
||||||
|
@ -13,6 +13,7 @@ import { NotFoundError } from "@app/lib/errors";
|
|||||||
import { logger } from "@app/lib/logger";
|
import { logger } from "@app/lib/logger";
|
||||||
import { alphaNumericNanoId } from "@app/lib/nanoid";
|
import { alphaNumericNanoId } from "@app/lib/nanoid";
|
||||||
import { QueueJobs, QueueName, TQueueServiceFactory } from "@app/queue";
|
import { QueueJobs, QueueName, TQueueServiceFactory } from "@app/queue";
|
||||||
|
import { ActorType } from "@app/services/auth/auth-type";
|
||||||
import { TKmsServiceFactory } from "@app/services/kms/kms-service";
|
import { TKmsServiceFactory } from "@app/services/kms/kms-service";
|
||||||
import { KmsDataKey } from "@app/services/kms/kms-types";
|
import { KmsDataKey } from "@app/services/kms/kms-types";
|
||||||
import { TProjectBotServiceFactory } from "@app/services/project-bot/project-bot-service";
|
import { TProjectBotServiceFactory } from "@app/services/project-bot/project-bot-service";
|
||||||
@ -332,6 +333,7 @@ export const secretRotationQueueFactory = ({
|
|||||||
await secretVersionV2BridgeDAL.insertMany(
|
await secretVersionV2BridgeDAL.insertMany(
|
||||||
updatedSecrets.map(({ id, updatedAt, createdAt, ...el }) => ({
|
updatedSecrets.map(({ id, updatedAt, createdAt, ...el }) => ({
|
||||||
...el,
|
...el,
|
||||||
|
actorType: ActorType.PLATFORM,
|
||||||
secretId: id
|
secretId: id
|
||||||
})),
|
})),
|
||||||
tx
|
tx
|
||||||
|
@ -15,7 +15,11 @@ import { TSecretV2BridgeDALFactory } from "@app/services/secret-v2-bridge/secret
|
|||||||
|
|
||||||
import { TLicenseServiceFactory } from "../license/license-service";
|
import { TLicenseServiceFactory } from "../license/license-service";
|
||||||
import { TPermissionServiceFactory } from "../permission/permission-service";
|
import { TPermissionServiceFactory } from "../permission/permission-service";
|
||||||
import { ProjectPermissionActions, ProjectPermissionSub } from "../permission/project-permission";
|
import {
|
||||||
|
ProjectPermissionActions,
|
||||||
|
ProjectPermissionSecretActions,
|
||||||
|
ProjectPermissionSub
|
||||||
|
} from "../permission/project-permission";
|
||||||
import { TSecretRotationDALFactory } from "./secret-rotation-dal";
|
import { TSecretRotationDALFactory } from "./secret-rotation-dal";
|
||||||
import { TSecretRotationQueueFactory } from "./secret-rotation-queue";
|
import { TSecretRotationQueueFactory } from "./secret-rotation-queue";
|
||||||
import { TSecretRotationEncData } from "./secret-rotation-queue/secret-rotation-queue-types";
|
import { TSecretRotationEncData } from "./secret-rotation-queue/secret-rotation-queue-types";
|
||||||
@ -106,7 +110,7 @@ export const secretRotationServiceFactory = ({
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
ForbiddenError.from(permission).throwUnlessCan(
|
ForbiddenError.from(permission).throwUnlessCan(
|
||||||
ProjectPermissionActions.Edit,
|
ProjectPermissionSecretActions.Edit,
|
||||||
subject(ProjectPermissionSub.Secrets, { environment, secretPath })
|
subject(ProjectPermissionSub.Secrets, { environment, secretPath })
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@ -1,16 +1,18 @@
|
|||||||
/* eslint-disable @typescript-eslint/no-unsafe-assignment,@typescript-eslint/no-unsafe-member-access,@typescript-eslint/no-unsafe-argument */
|
/* eslint-disable @typescript-eslint/no-unsafe-assignment,@typescript-eslint/no-unsafe-member-access,@typescript-eslint/no-unsafe-argument */
|
||||||
// akhilmhdh: I did this, quite strange bug with eslint. Everything do have a type stil has this error
|
// akhilmhdh: I did this, quite strange bug with eslint. Everything do have a type stil has this error
|
||||||
import { ForbiddenError, subject } from "@casl/ability";
|
import { ForbiddenError } from "@casl/ability";
|
||||||
|
|
||||||
import { ActionProjectType, TableName, TSecretTagJunctionInsert, TSecretV2TagJunctionInsert } from "@app/db/schemas";
|
import { ActionProjectType, TableName, TSecretTagJunctionInsert, TSecretV2TagJunctionInsert } from "@app/db/schemas";
|
||||||
import { decryptSymmetric128BitHexKeyUTF8 } from "@app/lib/crypto";
|
import { decryptSymmetric128BitHexKeyUTF8 } from "@app/lib/crypto";
|
||||||
import { InternalServerError, NotFoundError } from "@app/lib/errors";
|
import { InternalServerError, NotFoundError } from "@app/lib/errors";
|
||||||
import { groupBy } from "@app/lib/fn";
|
import { groupBy } from "@app/lib/fn";
|
||||||
import { logger } from "@app/lib/logger";
|
import { logger } from "@app/lib/logger";
|
||||||
|
import { ActorType } from "@app/services/auth/auth-type";
|
||||||
import { TKmsServiceFactory } from "@app/services/kms/kms-service";
|
import { TKmsServiceFactory } from "@app/services/kms/kms-service";
|
||||||
import { KmsDataKey } from "@app/services/kms/kms-types";
|
import { KmsDataKey } from "@app/services/kms/kms-types";
|
||||||
import { TProjectBotServiceFactory } from "@app/services/project-bot/project-bot-service";
|
import { TProjectBotServiceFactory } from "@app/services/project-bot/project-bot-service";
|
||||||
import { TSecretDALFactory } from "@app/services/secret/secret-dal";
|
import { TSecretDALFactory } from "@app/services/secret/secret-dal";
|
||||||
|
import { INFISICAL_SECRET_VALUE_HIDDEN_MASK } from "@app/services/secret/secret-fns";
|
||||||
import { TSecretVersionDALFactory } from "@app/services/secret/secret-version-dal";
|
import { TSecretVersionDALFactory } from "@app/services/secret/secret-version-dal";
|
||||||
import { TSecretVersionTagDALFactory } from "@app/services/secret/secret-version-tag-dal";
|
import { TSecretVersionTagDALFactory } from "@app/services/secret/secret-version-tag-dal";
|
||||||
import { TSecretFolderDALFactory } from "@app/services/secret-folder/secret-folder-dal";
|
import { TSecretFolderDALFactory } from "@app/services/secret-folder/secret-folder-dal";
|
||||||
@ -21,8 +23,16 @@ import { TSecretVersionV2DALFactory } from "@app/services/secret-v2-bridge/secre
|
|||||||
import { TSecretVersionV2TagDALFactory } from "@app/services/secret-v2-bridge/secret-version-tag-dal";
|
import { TSecretVersionV2TagDALFactory } from "@app/services/secret-v2-bridge/secret-version-tag-dal";
|
||||||
|
|
||||||
import { TLicenseServiceFactory } from "../license/license-service";
|
import { TLicenseServiceFactory } from "../license/license-service";
|
||||||
|
import {
|
||||||
|
hasSecretReadValueOrDescribePermission,
|
||||||
|
throwIfMissingSecretReadValueOrDescribePermission
|
||||||
|
} from "../permission/permission-fns";
|
||||||
import { TPermissionServiceFactory } from "../permission/permission-service";
|
import { TPermissionServiceFactory } from "../permission/permission-service";
|
||||||
import { ProjectPermissionActions, ProjectPermissionSub } from "../permission/project-permission";
|
import {
|
||||||
|
ProjectPermissionActions,
|
||||||
|
ProjectPermissionSecretActions,
|
||||||
|
ProjectPermissionSub
|
||||||
|
} from "../permission/project-permission";
|
||||||
import {
|
import {
|
||||||
TGetSnapshotDataDTO,
|
TGetSnapshotDataDTO,
|
||||||
TProjectSnapshotCountDTO,
|
TProjectSnapshotCountDTO,
|
||||||
@ -96,10 +106,10 @@ export const secretSnapshotServiceFactory = ({
|
|||||||
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.SecretRollback);
|
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.SecretRollback);
|
||||||
|
|
||||||
// We need to check if the user has access to the secrets in the folder. If we don't do this, a user could theoretically access snapshot secret values even if they don't have read access to the secrets in the folder.
|
// We need to check if the user has access to the secrets in the folder. If we don't do this, a user could theoretically access snapshot secret values even if they don't have read access to the secrets in the folder.
|
||||||
ForbiddenError.from(permission).throwUnlessCan(
|
throwIfMissingSecretReadValueOrDescribePermission(permission, ProjectPermissionSecretActions.DescribeSecret, {
|
||||||
ProjectPermissionActions.Read,
|
environment,
|
||||||
subject(ProjectPermissionSub.Secrets, { environment, secretPath: path })
|
secretPath: path
|
||||||
);
|
});
|
||||||
|
|
||||||
const folder = await folderDAL.findBySecretPath(projectId, environment, path);
|
const folder = await folderDAL.findBySecretPath(projectId, environment, path);
|
||||||
if (!folder) {
|
if (!folder) {
|
||||||
@ -133,10 +143,10 @@ export const secretSnapshotServiceFactory = ({
|
|||||||
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.SecretRollback);
|
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.SecretRollback);
|
||||||
|
|
||||||
// We need to check if the user has access to the secrets in the folder. If we don't do this, a user could theoretically access snapshot secret values even if they don't have read access to the secrets in the folder.
|
// We need to check if the user has access to the secrets in the folder. If we don't do this, a user could theoretically access snapshot secret values even if they don't have read access to the secrets in the folder.
|
||||||
ForbiddenError.from(permission).throwUnlessCan(
|
throwIfMissingSecretReadValueOrDescribePermission(permission, ProjectPermissionSecretActions.DescribeSecret, {
|
||||||
ProjectPermissionActions.Read,
|
environment,
|
||||||
subject(ProjectPermissionSub.Secrets, { environment, secretPath: path })
|
secretPath: path
|
||||||
);
|
});
|
||||||
|
|
||||||
const folder = await folderDAL.findBySecretPath(projectId, environment, path);
|
const folder = await folderDAL.findBySecretPath(projectId, environment, path);
|
||||||
if (!folder)
|
if (!folder)
|
||||||
@ -161,6 +171,7 @@ export const secretSnapshotServiceFactory = ({
|
|||||||
});
|
});
|
||||||
|
|
||||||
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.SecretRollback);
|
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.SecretRollback);
|
||||||
|
|
||||||
const shouldUseBridge = snapshot.projectVersion === 3;
|
const shouldUseBridge = snapshot.projectVersion === 3;
|
||||||
let snapshotDetails;
|
let snapshotDetails;
|
||||||
if (shouldUseBridge) {
|
if (shouldUseBridge) {
|
||||||
@ -169,68 +180,112 @@ export const secretSnapshotServiceFactory = ({
|
|||||||
projectId: snapshot.projectId
|
projectId: snapshot.projectId
|
||||||
});
|
});
|
||||||
const encryptedSnapshotDetails = await snapshotDAL.findSecretSnapshotV2DataById(id);
|
const encryptedSnapshotDetails = await snapshotDAL.findSecretSnapshotV2DataById(id);
|
||||||
|
|
||||||
|
const fullFolderPath = await getFullFolderPath({
|
||||||
|
folderDAL,
|
||||||
|
folderId: encryptedSnapshotDetails.folderId,
|
||||||
|
envId: encryptedSnapshotDetails.environment.id
|
||||||
|
});
|
||||||
|
|
||||||
snapshotDetails = {
|
snapshotDetails = {
|
||||||
...encryptedSnapshotDetails,
|
...encryptedSnapshotDetails,
|
||||||
secretVersions: encryptedSnapshotDetails.secretVersions.map((el) => ({
|
secretVersions: encryptedSnapshotDetails.secretVersions.map((el) => {
|
||||||
...el,
|
const canReadValue = hasSecretReadValueOrDescribePermission(
|
||||||
secretKey: el.key,
|
permission,
|
||||||
secretValue: el.encryptedValue
|
ProjectPermissionSecretActions.ReadValue,
|
||||||
? secretManagerDecryptor({ cipherTextBlob: el.encryptedValue }).toString()
|
{
|
||||||
: "",
|
environment: encryptedSnapshotDetails.environment.slug,
|
||||||
secretComment: el.encryptedComment
|
secretPath: fullFolderPath,
|
||||||
? secretManagerDecryptor({ cipherTextBlob: el.encryptedComment }).toString()
|
secretName: el.key,
|
||||||
: ""
|
secretTags: el.tags.length ? el.tags.map((tag) => tag.slug) : undefined
|
||||||
}))
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
let secretValue = "";
|
||||||
|
if (canReadValue) {
|
||||||
|
secretValue = el.encryptedValue
|
||||||
|
? secretManagerDecryptor({ cipherTextBlob: el.encryptedValue }).toString()
|
||||||
|
: "";
|
||||||
|
} else {
|
||||||
|
secretValue = INFISICAL_SECRET_VALUE_HIDDEN_MASK;
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
...el,
|
||||||
|
secretKey: el.key,
|
||||||
|
secretValueHidden: !canReadValue,
|
||||||
|
secretValue,
|
||||||
|
secretComment: el.encryptedComment
|
||||||
|
? secretManagerDecryptor({ cipherTextBlob: el.encryptedComment }).toString()
|
||||||
|
: ""
|
||||||
|
};
|
||||||
|
})
|
||||||
};
|
};
|
||||||
} else {
|
} else {
|
||||||
const encryptedSnapshotDetails = await snapshotDAL.findSecretSnapshotDataById(id);
|
const encryptedSnapshotDetails = await snapshotDAL.findSecretSnapshotDataById(id);
|
||||||
|
|
||||||
|
const fullFolderPath = await getFullFolderPath({
|
||||||
|
folderDAL,
|
||||||
|
folderId: encryptedSnapshotDetails.folderId,
|
||||||
|
envId: encryptedSnapshotDetails.environment.id
|
||||||
|
});
|
||||||
|
|
||||||
const { botKey } = await projectBotService.getBotKey(snapshot.projectId);
|
const { botKey } = await projectBotService.getBotKey(snapshot.projectId);
|
||||||
if (!botKey)
|
if (!botKey)
|
||||||
throw new NotFoundError({ message: `Project bot key not found for project with ID '${snapshot.projectId}'` });
|
throw new NotFoundError({ message: `Project bot key not found for project with ID '${snapshot.projectId}'` });
|
||||||
snapshotDetails = {
|
snapshotDetails = {
|
||||||
...encryptedSnapshotDetails,
|
...encryptedSnapshotDetails,
|
||||||
secretVersions: encryptedSnapshotDetails.secretVersions.map((el) => ({
|
secretVersions: encryptedSnapshotDetails.secretVersions.map((el) => {
|
||||||
...el,
|
const secretKey = decryptSymmetric128BitHexKeyUTF8({
|
||||||
secretKey: decryptSymmetric128BitHexKeyUTF8({
|
|
||||||
ciphertext: el.secretKeyCiphertext,
|
ciphertext: el.secretKeyCiphertext,
|
||||||
iv: el.secretKeyIV,
|
iv: el.secretKeyIV,
|
||||||
tag: el.secretKeyTag,
|
tag: el.secretKeyTag,
|
||||||
key: botKey
|
key: botKey
|
||||||
}),
|
});
|
||||||
secretValue: decryptSymmetric128BitHexKeyUTF8({
|
|
||||||
ciphertext: el.secretValueCiphertext,
|
const canReadValue = hasSecretReadValueOrDescribePermission(
|
||||||
iv: el.secretValueIV,
|
permission,
|
||||||
tag: el.secretValueTag,
|
ProjectPermissionSecretActions.ReadValue,
|
||||||
key: botKey
|
{
|
||||||
}),
|
environment: encryptedSnapshotDetails.environment.slug,
|
||||||
secretComment:
|
secretPath: fullFolderPath,
|
||||||
el.secretCommentTag && el.secretCommentIV && el.secretCommentCiphertext
|
secretName: secretKey,
|
||||||
? decryptSymmetric128BitHexKeyUTF8({
|
secretTags: el.tags.length ? el.tags.map((tag) => tag.slug) : undefined
|
||||||
ciphertext: el.secretCommentCiphertext,
|
}
|
||||||
iv: el.secretCommentIV,
|
);
|
||||||
tag: el.secretCommentTag,
|
|
||||||
key: botKey
|
let secretValue = "";
|
||||||
})
|
|
||||||
: ""
|
if (canReadValue) {
|
||||||
}))
|
secretValue = decryptSymmetric128BitHexKeyUTF8({
|
||||||
|
ciphertext: el.secretValueCiphertext,
|
||||||
|
iv: el.secretValueIV,
|
||||||
|
tag: el.secretValueTag,
|
||||||
|
key: botKey
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
secretValue = INFISICAL_SECRET_VALUE_HIDDEN_MASK;
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
...el,
|
||||||
|
secretKey,
|
||||||
|
secretValueHidden: !canReadValue,
|
||||||
|
secretValue,
|
||||||
|
secretComment:
|
||||||
|
el.secretCommentTag && el.secretCommentIV && el.secretCommentCiphertext
|
||||||
|
? decryptSymmetric128BitHexKeyUTF8({
|
||||||
|
ciphertext: el.secretCommentCiphertext,
|
||||||
|
iv: el.secretCommentIV,
|
||||||
|
tag: el.secretCommentTag,
|
||||||
|
key: botKey
|
||||||
|
})
|
||||||
|
: ""
|
||||||
|
};
|
||||||
|
})
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
const fullFolderPath = await getFullFolderPath({
|
|
||||||
folderDAL,
|
|
||||||
folderId: snapshotDetails.folderId,
|
|
||||||
envId: snapshotDetails.environment.id
|
|
||||||
});
|
|
||||||
|
|
||||||
// We need to check if the user has access to the secrets in the folder. If we don't do this, a user could theoretically access snapshot secret values even if they don't have read access to the secrets in the folder.
|
|
||||||
ForbiddenError.from(permission).throwUnlessCan(
|
|
||||||
ProjectPermissionActions.Read,
|
|
||||||
subject(ProjectPermissionSub.Secrets, {
|
|
||||||
environment: snapshotDetails.environment.slug,
|
|
||||||
secretPath: fullFolderPath
|
|
||||||
})
|
|
||||||
);
|
|
||||||
|
|
||||||
return snapshotDetails;
|
return snapshotDetails;
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -370,7 +425,21 @@ export const secretSnapshotServiceFactory = ({
|
|||||||
const secrets = await secretV2BridgeDAL.insertMany(
|
const secrets = await secretV2BridgeDAL.insertMany(
|
||||||
rollbackSnaps.flatMap(({ secretVersions, folderId }) =>
|
rollbackSnaps.flatMap(({ secretVersions, folderId }) =>
|
||||||
secretVersions.map(
|
secretVersions.map(
|
||||||
({ latestSecretVersion, version, updatedAt, createdAt, secretId, envId, id, tags, ...el }) => ({
|
({
|
||||||
|
latestSecretVersion,
|
||||||
|
version,
|
||||||
|
updatedAt,
|
||||||
|
createdAt,
|
||||||
|
secretId,
|
||||||
|
envId,
|
||||||
|
id,
|
||||||
|
tags,
|
||||||
|
// exclude the bottom fields from the secret - they are for versioning only.
|
||||||
|
userActorId,
|
||||||
|
identityActorId,
|
||||||
|
actorType,
|
||||||
|
...el
|
||||||
|
}) => ({
|
||||||
...el,
|
...el,
|
||||||
id: secretId,
|
id: secretId,
|
||||||
version: deletedTopLevelSecsGroupById[secretId] ? latestSecretVersion + 1 : latestSecretVersion,
|
version: deletedTopLevelSecsGroupById[secretId] ? latestSecretVersion + 1 : latestSecretVersion,
|
||||||
@ -401,8 +470,18 @@ export const secretSnapshotServiceFactory = ({
|
|||||||
})),
|
})),
|
||||||
tx
|
tx
|
||||||
);
|
);
|
||||||
|
const userActorId = actor === ActorType.USER ? actorId : undefined;
|
||||||
|
const identityActorId = actor !== ActorType.USER ? actorId : undefined;
|
||||||
|
const actorType = actor || ActorType.PLATFORM;
|
||||||
|
|
||||||
const secretVersions = await secretVersionV2BridgeDAL.insertMany(
|
const secretVersions = await secretVersionV2BridgeDAL.insertMany(
|
||||||
secrets.map(({ id, updatedAt, createdAt, ...el }) => ({ ...el, secretId: id })),
|
secrets.map(({ id, updatedAt, createdAt, ...el }) => ({
|
||||||
|
...el,
|
||||||
|
secretId: id,
|
||||||
|
userActorId,
|
||||||
|
identityActorId,
|
||||||
|
actorType
|
||||||
|
})),
|
||||||
tx
|
tx
|
||||||
);
|
);
|
||||||
await secretVersionV2TagBridgeDAL.insertMany(
|
await secretVersionV2TagBridgeDAL.insertMany(
|
||||||
|
@ -1,10 +1,10 @@
|
|||||||
import { ForbiddenError } from "@casl/ability";
|
import { ForbiddenError } from "@casl/ability";
|
||||||
import ms from "ms";
|
|
||||||
|
|
||||||
import { ActionProjectType } from "@app/db/schemas";
|
import { ActionProjectType } from "@app/db/schemas";
|
||||||
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, NotFoundError } from "@app/lib/errors";
|
import { BadRequestError, NotFoundError } from "@app/lib/errors";
|
||||||
|
import { ms } from "@app/lib/ms";
|
||||||
|
|
||||||
import { TSshCertificateAuthorityDALFactory } from "../ssh/ssh-certificate-authority-dal";
|
import { TSshCertificateAuthorityDALFactory } from "../ssh/ssh-certificate-authority-dal";
|
||||||
import { TSshCertificateTemplateDALFactory } from "./ssh-certificate-template-dal";
|
import { TSshCertificateTemplateDALFactory } from "./ssh-certificate-template-dal";
|
||||||
|
@ -6,7 +6,6 @@ export const sanitizedSshCertificate = SshCertificatesSchema.pick({
|
|||||||
sshCertificateTemplateId: true,
|
sshCertificateTemplateId: true,
|
||||||
serialNumber: true,
|
serialNumber: true,
|
||||||
certType: true,
|
certType: true,
|
||||||
publicKey: true,
|
|
||||||
principals: true,
|
principals: true,
|
||||||
keyId: true,
|
keyId: true,
|
||||||
notBefore: true,
|
notBefore: true,
|
||||||
|
@ -1,13 +1,13 @@
|
|||||||
import { execFile } from "child_process";
|
import { execFile } from "child_process";
|
||||||
import crypto from "crypto";
|
import crypto from "crypto";
|
||||||
import { promises as fs } from "fs";
|
import { promises as fs } from "fs";
|
||||||
import ms from "ms";
|
|
||||||
import os from "os";
|
import os from "os";
|
||||||
import path from "path";
|
import path from "path";
|
||||||
import { promisify } from "util";
|
import { promisify } from "util";
|
||||||
|
|
||||||
import { TSshCertificateTemplates } from "@app/db/schemas";
|
import { TSshCertificateTemplates } from "@app/db/schemas";
|
||||||
import { BadRequestError } from "@app/lib/errors";
|
import { BadRequestError } from "@app/lib/errors";
|
||||||
|
import { ms } from "@app/lib/ms";
|
||||||
import { CertKeyAlgorithm } from "@app/services/certificate/certificate-types";
|
import { CertKeyAlgorithm } from "@app/services/certificate/certificate-types";
|
||||||
|
|
||||||
import {
|
import {
|
||||||
|
@ -1,12 +1,15 @@
|
|||||||
import { Redis } from "ioredis";
|
import { Redis } from "ioredis";
|
||||||
|
|
||||||
|
import { pgAdvisoryLockHashText } from "@app/lib/crypto/hashtext";
|
||||||
import { Redlock, Settings } from "@app/lib/red-lock";
|
import { Redlock, Settings } from "@app/lib/red-lock";
|
||||||
|
|
||||||
export enum PgSqlLock {
|
export const PgSqlLock = {
|
||||||
BootUpMigration = 2023,
|
BootUpMigration: 2023,
|
||||||
SuperAdminInit = 2024,
|
SuperAdminInit: 2024,
|
||||||
KmsRootKeyInit = 2025
|
KmsRootKeyInit: 2025,
|
||||||
}
|
OrgGatewayRootCaInit: (orgId: string) => pgAdvisoryLockHashText(`org-gateway-root-ca:${orgId}`),
|
||||||
|
OrgGatewayCertExchange: (orgId: string) => pgAdvisoryLockHashText(`org-gateway-cert-exchange:${orgId}`)
|
||||||
|
} as const;
|
||||||
|
|
||||||
export type TKeyStoreFactory = ReturnType<typeof keyStoreFactory>;
|
export type TKeyStoreFactory = ReturnType<typeof keyStoreFactory>;
|
||||||
|
|
||||||
@ -33,7 +36,8 @@ export const KeyStorePrefixes = {
|
|||||||
SecretSyncLastRunTimestamp: (syncId: string) => `secret-sync-last-run-${syncId}` as const,
|
SecretSyncLastRunTimestamp: (syncId: string) => `secret-sync-last-run-${syncId}` as const,
|
||||||
IdentityAccessTokenStatusUpdate: (identityAccessTokenId: string) =>
|
IdentityAccessTokenStatusUpdate: (identityAccessTokenId: string) =>
|
||||||
`identity-access-token-status:${identityAccessTokenId}`,
|
`identity-access-token-status:${identityAccessTokenId}`,
|
||||||
ServiceTokenStatusUpdate: (serviceTokenId: string) => `service-token-status:${serviceTokenId}`
|
ServiceTokenStatusUpdate: (serviceTokenId: string) => `service-token-status:${serviceTokenId}`,
|
||||||
|
GatewayIdentityCredential: (identityId: string) => `gateway-credentials:${identityId}`
|
||||||
};
|
};
|
||||||
|
|
||||||
export const KeyStoreTtls = {
|
export const KeyStoreTtls = {
|
||||||
|
@ -329,6 +329,7 @@ export const OIDC_AUTH = {
|
|||||||
boundIssuer: "The unique identifier of the identity provider issuing the JWT.",
|
boundIssuer: "The unique identifier of the identity provider issuing the JWT.",
|
||||||
boundAudiences: "The list of intended recipients.",
|
boundAudiences: "The list of intended recipients.",
|
||||||
boundClaims: "The attributes that should be present in the JWT for it to be valid.",
|
boundClaims: "The attributes that should be present in the JWT for it to be valid.",
|
||||||
|
claimMetadataMapping: "The attributes that should be present in the permission metadata from the JWT.",
|
||||||
boundSubject: "The expected principal that is the subject of the JWT.",
|
boundSubject: "The expected principal that is the subject of the JWT.",
|
||||||
accessTokenTrustedIps: "The IPs or CIDR ranges that access tokens can be used from.",
|
accessTokenTrustedIps: "The IPs or CIDR ranges that access tokens can be used from.",
|
||||||
accessTokenTTL: "The lifetime for an access token in seconds.",
|
accessTokenTTL: "The lifetime for an access token in seconds.",
|
||||||
@ -342,6 +343,7 @@ export const OIDC_AUTH = {
|
|||||||
boundIssuer: "The new unique identifier of the identity provider issuing the JWT.",
|
boundIssuer: "The new unique identifier of the identity provider issuing the JWT.",
|
||||||
boundAudiences: "The new list of intended recipients.",
|
boundAudiences: "The new list of intended recipients.",
|
||||||
boundClaims: "The new attributes that should be present in the JWT for it to be valid.",
|
boundClaims: "The new attributes that should be present in the JWT for it to be valid.",
|
||||||
|
claimMetadataMapping: "The new attributes that should be present in the permission metadata from the JWT.",
|
||||||
boundSubject: "The new expected principal that is the subject of the JWT.",
|
boundSubject: "The new expected principal that is the subject of the JWT.",
|
||||||
accessTokenTrustedIps: "The new IPs or CIDR ranges that access tokens can be used from.",
|
accessTokenTrustedIps: "The new IPs or CIDR ranges that access tokens can be used from.",
|
||||||
accessTokenTTL: "The new lifetime for an access token in seconds.",
|
accessTokenTTL: "The new lifetime for an access token in seconds.",
|
||||||
@ -459,7 +461,8 @@ export const PROJECTS = {
|
|||||||
workspaceId: "The ID of the project to update.",
|
workspaceId: "The ID of the project to update.",
|
||||||
name: "The new name of the project.",
|
name: "The new name of the project.",
|
||||||
projectDescription: "An optional description label for the project.",
|
projectDescription: "An optional description label for the project.",
|
||||||
autoCapitalization: "Disable or enable auto-capitalization for the project."
|
autoCapitalization: "Disable or enable auto-capitalization for the project.",
|
||||||
|
slug: "An optional slug for the project. (must be unique within the organization)"
|
||||||
},
|
},
|
||||||
GET_KEY: {
|
GET_KEY: {
|
||||||
workspaceId: "The ID of the project to get the key from."
|
workspaceId: "The ID of the project to get the key from."
|
||||||
@ -638,7 +641,8 @@ export const FOLDERS = {
|
|||||||
environment: "The slug of the environment to create the folder in.",
|
environment: "The slug of the environment to create the folder in.",
|
||||||
name: "The name of the folder to create.",
|
name: "The name of the folder to create.",
|
||||||
path: "The path of the folder to create.",
|
path: "The path of the folder to create.",
|
||||||
directory: "The directory of the folder to create. (Deprecated in favor of path)"
|
directory: "The directory of the folder to create. (Deprecated in favor of path)",
|
||||||
|
description: "An optional description label for the folder."
|
||||||
},
|
},
|
||||||
UPDATE: {
|
UPDATE: {
|
||||||
folderId: "The ID of the folder to update.",
|
folderId: "The ID of the folder to update.",
|
||||||
@ -647,7 +651,8 @@ export const FOLDERS = {
|
|||||||
path: "The path of the folder to update.",
|
path: "The path of the folder to update.",
|
||||||
directory: "The new directory of the folder to update. (Deprecated in favor of path)",
|
directory: "The new directory of the folder to update. (Deprecated in favor of path)",
|
||||||
projectSlug: "The slug of the project where the folder is located.",
|
projectSlug: "The slug of the project where the folder is located.",
|
||||||
workspaceId: "The ID of the project where the folder is located."
|
workspaceId: "The ID of the project where the folder is located.",
|
||||||
|
description: "An optional description label for the folder."
|
||||||
},
|
},
|
||||||
DELETE: {
|
DELETE: {
|
||||||
folderIdOrName: "The ID or name of the folder to delete.",
|
folderIdOrName: "The ID or name of the folder to delete.",
|
||||||
@ -664,6 +669,7 @@ export const SECRETS = {
|
|||||||
secretPath: "The path of the secret to attach tags to.",
|
secretPath: "The path of the secret to attach tags to.",
|
||||||
type: "The type of the secret to attach tags to. (shared/personal)",
|
type: "The type of the secret to attach tags to. (shared/personal)",
|
||||||
environment: "The slug of the environment where the secret is located",
|
environment: "The slug of the environment where the secret is located",
|
||||||
|
viewSecretValue: "Whether or not to retrieve the secret value.",
|
||||||
projectSlug: "The slug of the project where the secret is located.",
|
projectSlug: "The slug of the project where the secret is located.",
|
||||||
tagSlugs: "An array of existing tag slugs to attach to the secret."
|
tagSlugs: "An array of existing tag slugs to attach to the secret."
|
||||||
},
|
},
|
||||||
@ -687,6 +693,7 @@ export const RAW_SECRETS = {
|
|||||||
"The slug of the project to list secrets from. This parameter is only applicable by machine identities.",
|
"The slug of the project to list secrets from. This parameter is only applicable by machine identities.",
|
||||||
environment: "The slug of the environment to list secrets from.",
|
environment: "The slug of the environment to list secrets from.",
|
||||||
secretPath: "The secret path to list secrets from.",
|
secretPath: "The secret path to list secrets from.",
|
||||||
|
viewSecretValue: "Whether or not to retrieve the secret value.",
|
||||||
includeImports: "Weather to include imported secrets or not.",
|
includeImports: "Weather to include imported secrets or not.",
|
||||||
tagSlugs: "The comma separated tag slugs to filter secrets.",
|
tagSlugs: "The comma separated tag slugs to filter secrets.",
|
||||||
metadataFilter:
|
metadataFilter:
|
||||||
@ -715,6 +722,7 @@ export const RAW_SECRETS = {
|
|||||||
secretPath: "The path of the secret to get.",
|
secretPath: "The path of the secret to get.",
|
||||||
version: "The version of the secret to get.",
|
version: "The version of the secret to get.",
|
||||||
type: "The type of the secret to get.",
|
type: "The type of the secret to get.",
|
||||||
|
viewSecretValue: "Whether or not to retrieve the secret value.",
|
||||||
includeImports: "Weather to include imported secrets or not."
|
includeImports: "Weather to include imported secrets or not."
|
||||||
},
|
},
|
||||||
UPDATE: {
|
UPDATE: {
|
||||||
@ -1719,7 +1727,8 @@ export const SecretSyncs = {
|
|||||||
SYNC_OPTIONS: (destination: SecretSync) => {
|
SYNC_OPTIONS: (destination: SecretSync) => {
|
||||||
const destinationName = SECRET_SYNC_NAME_MAP[destination];
|
const destinationName = SECRET_SYNC_NAME_MAP[destination];
|
||||||
return {
|
return {
|
||||||
initialSyncBehavior: `Specify how Infisical should resolve the initial sync to the ${destinationName} destination.`
|
initialSyncBehavior: `Specify how Infisical should resolve the initial sync to the ${destinationName} destination.`,
|
||||||
|
disableSecretDeletion: `Enable this flag to prevent removal of secrets from the ${destinationName} destination when syncing.`
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
ADDITIONAL_SYNC_OPTIONS: {
|
ADDITIONAL_SYNC_OPTIONS: {
|
||||||
@ -1765,6 +1774,12 @@ export const SecretSyncs = {
|
|||||||
},
|
},
|
||||||
DATABRICKS: {
|
DATABRICKS: {
|
||||||
scope: "The Databricks secret scope that secrets should be synced to."
|
scope: "The Databricks secret scope that secrets should be synced to."
|
||||||
|
},
|
||||||
|
HUMANITEC: {
|
||||||
|
app: "The ID of the Humanitec app to sync secrets to.",
|
||||||
|
org: "The ID of the Humanitec org to sync secrets to.",
|
||||||
|
env: "The ID of the Humanitec environment to sync secrets to.",
|
||||||
|
scope: "The Humanitec scope that secrets should be synced to."
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
669
backend/src/lib/casl/boundary.test.ts
Normal file
669
backend/src/lib/casl/boundary.test.ts
Normal file
@ -0,0 +1,669 @@
|
|||||||
|
import { createMongoAbility } from "@casl/ability";
|
||||||
|
|
||||||
|
import { PermissionConditionOperators } from ".";
|
||||||
|
import { validatePermissionBoundary } from "./boundary";
|
||||||
|
|
||||||
|
describe("Validate Permission Boundary Function", () => {
|
||||||
|
test.each([
|
||||||
|
{
|
||||||
|
title: "child with equal privilege",
|
||||||
|
parentPermission: createMongoAbility([
|
||||||
|
{
|
||||||
|
action: ["create", "edit", "delete", "read"],
|
||||||
|
subject: "secrets"
|
||||||
|
}
|
||||||
|
]),
|
||||||
|
childPermission: createMongoAbility([
|
||||||
|
{
|
||||||
|
action: ["create", "edit", "delete", "read"],
|
||||||
|
subject: "secrets"
|
||||||
|
}
|
||||||
|
]),
|
||||||
|
expectValid: true,
|
||||||
|
missingPermissions: []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "child with less privilege",
|
||||||
|
parentPermission: createMongoAbility([
|
||||||
|
{
|
||||||
|
action: ["create", "edit", "delete", "read"],
|
||||||
|
subject: "secrets"
|
||||||
|
}
|
||||||
|
]),
|
||||||
|
childPermission: createMongoAbility([
|
||||||
|
{
|
||||||
|
action: ["create", "edit"],
|
||||||
|
subject: "secrets"
|
||||||
|
}
|
||||||
|
]),
|
||||||
|
expectValid: true,
|
||||||
|
missingPermissions: []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "child with more privilege",
|
||||||
|
parentPermission: createMongoAbility([
|
||||||
|
{
|
||||||
|
action: ["create"],
|
||||||
|
subject: "secrets"
|
||||||
|
}
|
||||||
|
]),
|
||||||
|
childPermission: createMongoAbility([
|
||||||
|
{
|
||||||
|
action: ["create", "edit"],
|
||||||
|
subject: "secrets"
|
||||||
|
}
|
||||||
|
]),
|
||||||
|
expectValid: false,
|
||||||
|
missingPermissions: [{ action: "edit", subject: "secrets" }]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "parent with multiple and child with multiple",
|
||||||
|
parentPermission: createMongoAbility([
|
||||||
|
{
|
||||||
|
action: ["create"],
|
||||||
|
subject: "secrets"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
action: ["create", "edit"],
|
||||||
|
subject: "members"
|
||||||
|
}
|
||||||
|
]),
|
||||||
|
childPermission: createMongoAbility([
|
||||||
|
{
|
||||||
|
action: ["create"],
|
||||||
|
subject: "members"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
action: ["create"],
|
||||||
|
subject: "secrets"
|
||||||
|
}
|
||||||
|
]),
|
||||||
|
expectValid: true,
|
||||||
|
missingPermissions: []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "Child with no access",
|
||||||
|
parentPermission: createMongoAbility([
|
||||||
|
{
|
||||||
|
action: ["create"],
|
||||||
|
subject: "secrets"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
action: ["create", "edit"],
|
||||||
|
subject: "members"
|
||||||
|
}
|
||||||
|
]),
|
||||||
|
childPermission: createMongoAbility([]),
|
||||||
|
expectValid: true,
|
||||||
|
missingPermissions: []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "Parent and child disjoint set",
|
||||||
|
parentPermission: createMongoAbility([
|
||||||
|
{
|
||||||
|
action: ["create", "edit", "delete", "read"],
|
||||||
|
subject: "secrets",
|
||||||
|
conditions: {
|
||||||
|
environment: { [PermissionConditionOperators.$EQ]: "dev" }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]),
|
||||||
|
childPermission: createMongoAbility([
|
||||||
|
{
|
||||||
|
action: ["create", "edit", "delete", "read"],
|
||||||
|
subject: "secrets",
|
||||||
|
conditions: {
|
||||||
|
secretPath: { [PermissionConditionOperators.$EQ]: "dev" }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]),
|
||||||
|
expectValid: false,
|
||||||
|
missingPermissions: ["create", "edit", "delete", "read"].map((el) => ({
|
||||||
|
action: el,
|
||||||
|
subject: "secrets",
|
||||||
|
conditions: {
|
||||||
|
secretPath: { [PermissionConditionOperators.$EQ]: "dev" }
|
||||||
|
}
|
||||||
|
}))
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "Parent with inverted rules",
|
||||||
|
parentPermission: createMongoAbility([
|
||||||
|
{
|
||||||
|
action: ["create", "edit", "delete", "read"],
|
||||||
|
subject: "secrets",
|
||||||
|
conditions: {
|
||||||
|
environment: { [PermissionConditionOperators.$EQ]: "dev" }
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
action: "read",
|
||||||
|
subject: "secrets",
|
||||||
|
inverted: true,
|
||||||
|
conditions: {
|
||||||
|
environment: { [PermissionConditionOperators.$EQ]: "dev" },
|
||||||
|
secretPath: { [PermissionConditionOperators.$GLOB]: "/hello/**" }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]),
|
||||||
|
childPermission: createMongoAbility([
|
||||||
|
{
|
||||||
|
action: "read",
|
||||||
|
subject: "secrets",
|
||||||
|
conditions: {
|
||||||
|
environment: { [PermissionConditionOperators.$EQ]: "dev" },
|
||||||
|
secretPath: { [PermissionConditionOperators.$EQ]: "/" }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]),
|
||||||
|
expectValid: true,
|
||||||
|
missingPermissions: []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "Parent with inverted rules - child accessing invalid one",
|
||||||
|
parentPermission: createMongoAbility([
|
||||||
|
{
|
||||||
|
action: ["create", "edit", "delete", "read"],
|
||||||
|
subject: "secrets",
|
||||||
|
conditions: {
|
||||||
|
environment: { [PermissionConditionOperators.$EQ]: "dev" }
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
action: "read",
|
||||||
|
subject: "secrets",
|
||||||
|
inverted: true,
|
||||||
|
conditions: {
|
||||||
|
environment: { [PermissionConditionOperators.$EQ]: "dev" },
|
||||||
|
secretPath: { [PermissionConditionOperators.$GLOB]: "/hello/**" }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]),
|
||||||
|
childPermission: createMongoAbility([
|
||||||
|
{
|
||||||
|
action: "read",
|
||||||
|
subject: "secrets",
|
||||||
|
conditions: {
|
||||||
|
environment: { [PermissionConditionOperators.$EQ]: "dev" },
|
||||||
|
secretPath: { [PermissionConditionOperators.$EQ]: "/hello/world" }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]),
|
||||||
|
expectValid: false,
|
||||||
|
missingPermissions: [
|
||||||
|
{
|
||||||
|
action: "read",
|
||||||
|
subject: "secrets",
|
||||||
|
conditions: {
|
||||||
|
environment: { [PermissionConditionOperators.$EQ]: "dev" },
|
||||||
|
secretPath: { [PermissionConditionOperators.$EQ]: "/hello/world" }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
])("Check permission: $title", ({ parentPermission, childPermission, expectValid, missingPermissions }) => {
|
||||||
|
const permissionBoundary = validatePermissionBoundary(parentPermission, childPermission);
|
||||||
|
if (expectValid) {
|
||||||
|
expect(permissionBoundary.isValid).toBeTruthy();
|
||||||
|
} else {
|
||||||
|
expect(permissionBoundary.isValid).toBeFalsy();
|
||||||
|
expect(permissionBoundary.missingPermissions).toEqual(expect.arrayContaining(missingPermissions));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("Validate Permission Boundary: Checking Parent $eq operator", () => {
|
||||||
|
const parentPermission = createMongoAbility([
|
||||||
|
{
|
||||||
|
action: ["create", "read"],
|
||||||
|
subject: "secrets",
|
||||||
|
conditions: {
|
||||||
|
environment: { [PermissionConditionOperators.$EQ]: "dev" }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]);
|
||||||
|
|
||||||
|
test.each([
|
||||||
|
{
|
||||||
|
operator: PermissionConditionOperators.$EQ,
|
||||||
|
childPermission: createMongoAbility([
|
||||||
|
{
|
||||||
|
action: ["create"],
|
||||||
|
subject: "secrets",
|
||||||
|
conditions: {
|
||||||
|
environment: { [PermissionConditionOperators.$EQ]: "dev" }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
])
|
||||||
|
},
|
||||||
|
{
|
||||||
|
operator: PermissionConditionOperators.$IN,
|
||||||
|
childPermission: createMongoAbility([
|
||||||
|
{
|
||||||
|
action: ["create"],
|
||||||
|
subject: "secrets",
|
||||||
|
conditions: {
|
||||||
|
environment: { [PermissionConditionOperators.$IN]: ["dev"] }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
])
|
||||||
|
},
|
||||||
|
{
|
||||||
|
operator: PermissionConditionOperators.$GLOB,
|
||||||
|
childPermission: createMongoAbility([
|
||||||
|
{
|
||||||
|
action: ["create"],
|
||||||
|
subject: "secrets",
|
||||||
|
conditions: {
|
||||||
|
environment: { [PermissionConditionOperators.$GLOB]: "dev" }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
])
|
||||||
|
}
|
||||||
|
])("Child $operator truthy cases", ({ childPermission }) => {
|
||||||
|
const permissionBoundary = validatePermissionBoundary(parentPermission, childPermission);
|
||||||
|
expect(permissionBoundary.isValid).toBeTruthy();
|
||||||
|
});
|
||||||
|
|
||||||
|
test.each([
|
||||||
|
{
|
||||||
|
operator: PermissionConditionOperators.$EQ,
|
||||||
|
childPermission: createMongoAbility([
|
||||||
|
{
|
||||||
|
action: ["create"],
|
||||||
|
subject: "secrets",
|
||||||
|
conditions: {
|
||||||
|
environment: { [PermissionConditionOperators.$EQ]: "prod" }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
])
|
||||||
|
},
|
||||||
|
{
|
||||||
|
operator: PermissionConditionOperators.$IN,
|
||||||
|
childPermission: createMongoAbility([
|
||||||
|
{
|
||||||
|
action: ["create"],
|
||||||
|
subject: "secrets",
|
||||||
|
conditions: {
|
||||||
|
environment: { [PermissionConditionOperators.$IN]: ["dev", "prod"] }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
])
|
||||||
|
},
|
||||||
|
{
|
||||||
|
operator: PermissionConditionOperators.$GLOB,
|
||||||
|
childPermission: createMongoAbility([
|
||||||
|
{
|
||||||
|
action: ["create"],
|
||||||
|
subject: "secrets",
|
||||||
|
conditions: {
|
||||||
|
environment: { [PermissionConditionOperators.$GLOB]: "dev**" }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
])
|
||||||
|
},
|
||||||
|
{
|
||||||
|
operator: PermissionConditionOperators.$NEQ,
|
||||||
|
childPermission: createMongoAbility([
|
||||||
|
{
|
||||||
|
action: ["create"],
|
||||||
|
subject: "secrets",
|
||||||
|
conditions: {
|
||||||
|
environment: { [PermissionConditionOperators.$GLOB]: "staging" }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
])
|
||||||
|
}
|
||||||
|
])("Child $operator falsy cases", ({ childPermission }) => {
|
||||||
|
const permissionBoundary = validatePermissionBoundary(parentPermission, childPermission);
|
||||||
|
expect(permissionBoundary.isValid).toBeFalsy();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("Validate Permission Boundary: Checking Parent $neq operator", () => {
|
||||||
|
const parentPermission = createMongoAbility([
|
||||||
|
{
|
||||||
|
action: ["create", "read"],
|
||||||
|
subject: "secrets",
|
||||||
|
conditions: {
|
||||||
|
secretPath: { [PermissionConditionOperators.$NEQ]: "/hello" }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]);
|
||||||
|
|
||||||
|
test.each([
|
||||||
|
{
|
||||||
|
operator: PermissionConditionOperators.$EQ,
|
||||||
|
childPermission: createMongoAbility([
|
||||||
|
{
|
||||||
|
action: ["create"],
|
||||||
|
subject: "secrets",
|
||||||
|
conditions: {
|
||||||
|
secretPath: { [PermissionConditionOperators.$EQ]: "/" }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
])
|
||||||
|
},
|
||||||
|
{
|
||||||
|
operator: PermissionConditionOperators.$NEQ,
|
||||||
|
childPermission: createMongoAbility([
|
||||||
|
{
|
||||||
|
action: ["create"],
|
||||||
|
subject: "secrets",
|
||||||
|
conditions: {
|
||||||
|
secretPath: { [PermissionConditionOperators.$NEQ]: "/hello" }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
])
|
||||||
|
},
|
||||||
|
{
|
||||||
|
operator: PermissionConditionOperators.$IN,
|
||||||
|
childPermission: createMongoAbility([
|
||||||
|
{
|
||||||
|
action: ["create"],
|
||||||
|
subject: "secrets",
|
||||||
|
conditions: {
|
||||||
|
secretPath: { [PermissionConditionOperators.$IN]: ["/", "/staging"] }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
])
|
||||||
|
},
|
||||||
|
{
|
||||||
|
operator: PermissionConditionOperators.$GLOB,
|
||||||
|
childPermission: createMongoAbility([
|
||||||
|
{
|
||||||
|
action: ["create"],
|
||||||
|
subject: "secrets",
|
||||||
|
conditions: {
|
||||||
|
secretPath: { [PermissionConditionOperators.$GLOB]: "/dev**" }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
])
|
||||||
|
}
|
||||||
|
])("Child $operator truthy cases", ({ childPermission }) => {
|
||||||
|
const permissionBoundary = validatePermissionBoundary(parentPermission, childPermission);
|
||||||
|
expect(permissionBoundary.isValid).toBeTruthy();
|
||||||
|
});
|
||||||
|
|
||||||
|
test.each([
|
||||||
|
{
|
||||||
|
operator: PermissionConditionOperators.$EQ,
|
||||||
|
childPermission: createMongoAbility([
|
||||||
|
{
|
||||||
|
action: ["create"],
|
||||||
|
subject: "secrets",
|
||||||
|
conditions: {
|
||||||
|
secretPath: { [PermissionConditionOperators.$EQ]: "/hello" }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
])
|
||||||
|
},
|
||||||
|
{
|
||||||
|
operator: PermissionConditionOperators.$NEQ,
|
||||||
|
childPermission: createMongoAbility([
|
||||||
|
{
|
||||||
|
action: ["create"],
|
||||||
|
subject: "secrets",
|
||||||
|
conditions: {
|
||||||
|
secretPath: { [PermissionConditionOperators.$NEQ]: "/" }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
])
|
||||||
|
},
|
||||||
|
{
|
||||||
|
operator: PermissionConditionOperators.$IN,
|
||||||
|
childPermission: createMongoAbility([
|
||||||
|
{
|
||||||
|
action: ["create"],
|
||||||
|
subject: "secrets",
|
||||||
|
conditions: {
|
||||||
|
secretPath: { [PermissionConditionOperators.$IN]: ["/", "/hello"] }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
])
|
||||||
|
},
|
||||||
|
{
|
||||||
|
operator: PermissionConditionOperators.$GLOB,
|
||||||
|
childPermission: createMongoAbility([
|
||||||
|
{
|
||||||
|
action: ["create"],
|
||||||
|
subject: "secrets",
|
||||||
|
conditions: {
|
||||||
|
secretPath: { [PermissionConditionOperators.$GLOB]: "/hello**" }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
])
|
||||||
|
}
|
||||||
|
])("Child $operator falsy cases", ({ childPermission }) => {
|
||||||
|
const permissionBoundary = validatePermissionBoundary(parentPermission, childPermission);
|
||||||
|
expect(permissionBoundary.isValid).toBeFalsy();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("Validate Permission Boundary: Checking Parent $IN operator", () => {
|
||||||
|
const parentPermission = createMongoAbility([
|
||||||
|
{
|
||||||
|
action: ["edit"],
|
||||||
|
subject: "secrets",
|
||||||
|
conditions: {
|
||||||
|
environment: { [PermissionConditionOperators.$IN]: ["dev", "staging"] }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]);
|
||||||
|
|
||||||
|
test.each([
|
||||||
|
{
|
||||||
|
operator: PermissionConditionOperators.$EQ,
|
||||||
|
childPermission: createMongoAbility([
|
||||||
|
{
|
||||||
|
action: ["edit"],
|
||||||
|
subject: "secrets",
|
||||||
|
conditions: {
|
||||||
|
environment: { [PermissionConditionOperators.$EQ]: "dev" }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
])
|
||||||
|
},
|
||||||
|
{
|
||||||
|
operator: PermissionConditionOperators.$IN,
|
||||||
|
childPermission: createMongoAbility([
|
||||||
|
{
|
||||||
|
action: ["edit"],
|
||||||
|
subject: "secrets",
|
||||||
|
conditions: {
|
||||||
|
environment: { [PermissionConditionOperators.$IN]: ["dev"] }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
])
|
||||||
|
},
|
||||||
|
{
|
||||||
|
operator: `${PermissionConditionOperators.$IN} - 2`,
|
||||||
|
childPermission: createMongoAbility([
|
||||||
|
{
|
||||||
|
action: ["edit"],
|
||||||
|
subject: "secrets",
|
||||||
|
conditions: {
|
||||||
|
environment: { [PermissionConditionOperators.$IN]: ["dev", "staging"] }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
])
|
||||||
|
},
|
||||||
|
{
|
||||||
|
operator: PermissionConditionOperators.$GLOB,
|
||||||
|
childPermission: createMongoAbility([
|
||||||
|
{
|
||||||
|
action: ["edit"],
|
||||||
|
subject: "secrets",
|
||||||
|
conditions: {
|
||||||
|
environment: { [PermissionConditionOperators.$GLOB]: "dev" }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
])
|
||||||
|
}
|
||||||
|
])("Child $operator truthy cases", ({ childPermission }) => {
|
||||||
|
const permissionBoundary = validatePermissionBoundary(parentPermission, childPermission);
|
||||||
|
expect(permissionBoundary.isValid).toBeTruthy();
|
||||||
|
});
|
||||||
|
|
||||||
|
test.each([
|
||||||
|
{
|
||||||
|
operator: PermissionConditionOperators.$EQ,
|
||||||
|
childPermission: createMongoAbility([
|
||||||
|
{
|
||||||
|
action: ["edit"],
|
||||||
|
subject: "secrets",
|
||||||
|
conditions: {
|
||||||
|
environment: { [PermissionConditionOperators.$EQ]: "prod" }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
])
|
||||||
|
},
|
||||||
|
{
|
||||||
|
operator: PermissionConditionOperators.$NEQ,
|
||||||
|
childPermission: createMongoAbility([
|
||||||
|
{
|
||||||
|
action: ["edit"],
|
||||||
|
subject: "secrets",
|
||||||
|
conditions: {
|
||||||
|
environment: { [PermissionConditionOperators.$NEQ]: "dev" }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
])
|
||||||
|
},
|
||||||
|
{
|
||||||
|
operator: PermissionConditionOperators.$IN,
|
||||||
|
childPermission: createMongoAbility([
|
||||||
|
{
|
||||||
|
action: ["edit"],
|
||||||
|
subject: "secrets",
|
||||||
|
conditions: {
|
||||||
|
environment: { [PermissionConditionOperators.$IN]: ["dev", "prod"] }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
])
|
||||||
|
},
|
||||||
|
{
|
||||||
|
operator: PermissionConditionOperators.$GLOB,
|
||||||
|
childPermission: createMongoAbility([
|
||||||
|
{
|
||||||
|
action: ["edit"],
|
||||||
|
subject: "secrets",
|
||||||
|
conditions: {
|
||||||
|
environment: { [PermissionConditionOperators.$GLOB]: "dev**" }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
])
|
||||||
|
}
|
||||||
|
])("Child $operator falsy cases", ({ childPermission }) => {
|
||||||
|
const permissionBoundary = validatePermissionBoundary(parentPermission, childPermission);
|
||||||
|
expect(permissionBoundary.isValid).toBeFalsy();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("Validate Permission Boundary: Checking Parent $GLOB operator", () => {
|
||||||
|
const parentPermission = createMongoAbility([
|
||||||
|
{
|
||||||
|
action: ["create", "read"],
|
||||||
|
subject: "secrets",
|
||||||
|
conditions: {
|
||||||
|
secretPath: { [PermissionConditionOperators.$GLOB]: "/hello/**" }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]);
|
||||||
|
|
||||||
|
test.each([
|
||||||
|
{
|
||||||
|
operator: PermissionConditionOperators.$EQ,
|
||||||
|
childPermission: createMongoAbility([
|
||||||
|
{
|
||||||
|
action: ["create"],
|
||||||
|
subject: "secrets",
|
||||||
|
conditions: {
|
||||||
|
secretPath: { [PermissionConditionOperators.$EQ]: "/hello/world" }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
])
|
||||||
|
},
|
||||||
|
{
|
||||||
|
operator: PermissionConditionOperators.$IN,
|
||||||
|
childPermission: createMongoAbility([
|
||||||
|
{
|
||||||
|
action: ["create"],
|
||||||
|
subject: "secrets",
|
||||||
|
conditions: {
|
||||||
|
secretPath: { [PermissionConditionOperators.$IN]: ["/hello/world", "/hello/world2"] }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
])
|
||||||
|
},
|
||||||
|
{
|
||||||
|
operator: PermissionConditionOperators.$GLOB,
|
||||||
|
childPermission: createMongoAbility([
|
||||||
|
{
|
||||||
|
action: ["create"],
|
||||||
|
subject: "secrets",
|
||||||
|
conditions: {
|
||||||
|
secretPath: { [PermissionConditionOperators.$GLOB]: "/hello/**/world" }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
])
|
||||||
|
}
|
||||||
|
])("Child $operator truthy cases", ({ childPermission }) => {
|
||||||
|
const permissionBoundary = validatePermissionBoundary(parentPermission, childPermission);
|
||||||
|
expect(permissionBoundary.isValid).toBeTruthy();
|
||||||
|
});
|
||||||
|
|
||||||
|
test.each([
|
||||||
|
{
|
||||||
|
operator: PermissionConditionOperators.$EQ,
|
||||||
|
childPermission: createMongoAbility([
|
||||||
|
{
|
||||||
|
action: ["create"],
|
||||||
|
subject: "secrets",
|
||||||
|
conditions: {
|
||||||
|
secretPath: { [PermissionConditionOperators.$EQ]: "/print" }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
])
|
||||||
|
},
|
||||||
|
{
|
||||||
|
operator: PermissionConditionOperators.$NEQ,
|
||||||
|
childPermission: createMongoAbility([
|
||||||
|
{
|
||||||
|
action: ["create"],
|
||||||
|
subject: "secrets",
|
||||||
|
conditions: {
|
||||||
|
secretPath: { [PermissionConditionOperators.$NEQ]: "/hello/world" }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
])
|
||||||
|
},
|
||||||
|
{
|
||||||
|
operator: PermissionConditionOperators.$IN,
|
||||||
|
childPermission: createMongoAbility([
|
||||||
|
{
|
||||||
|
action: ["create"],
|
||||||
|
subject: "secrets",
|
||||||
|
conditions: {
|
||||||
|
secretPath: { [PermissionConditionOperators.$IN]: ["/", "/hello"] }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
])
|
||||||
|
},
|
||||||
|
{
|
||||||
|
operator: PermissionConditionOperators.$GLOB,
|
||||||
|
childPermission: createMongoAbility([
|
||||||
|
{
|
||||||
|
action: ["create"],
|
||||||
|
subject: "secrets",
|
||||||
|
conditions: {
|
||||||
|
secretPath: { [PermissionConditionOperators.$GLOB]: "/hello**" }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
])
|
||||||
|
}
|
||||||
|
])("Child $operator falsy cases", ({ childPermission }) => {
|
||||||
|
const permissionBoundary = validatePermissionBoundary(parentPermission, childPermission);
|
||||||
|
expect(permissionBoundary.isValid).toBeFalsy();
|
||||||
|
});
|
||||||
|
});
|
249
backend/src/lib/casl/boundary.ts
Normal file
249
backend/src/lib/casl/boundary.ts
Normal file
@ -0,0 +1,249 @@
|
|||||||
|
import { MongoAbility } from "@casl/ability";
|
||||||
|
import { MongoQuery } from "@ucast/mongo2js";
|
||||||
|
import picomatch from "picomatch";
|
||||||
|
|
||||||
|
import { PermissionConditionOperators } from "./index";
|
||||||
|
|
||||||
|
type TMissingPermission = {
|
||||||
|
action: string;
|
||||||
|
subject: string;
|
||||||
|
conditions?: MongoQuery;
|
||||||
|
};
|
||||||
|
|
||||||
|
type TPermissionConditionShape = {
|
||||||
|
[PermissionConditionOperators.$EQ]: string;
|
||||||
|
[PermissionConditionOperators.$NEQ]: string;
|
||||||
|
[PermissionConditionOperators.$GLOB]: string;
|
||||||
|
[PermissionConditionOperators.$IN]: string[];
|
||||||
|
};
|
||||||
|
|
||||||
|
const getPermissionSetID = (action: string, subject: string) => `${action}:${subject}`;
|
||||||
|
const invertTheOperation = (shouldInvert: boolean, operation: boolean) => (shouldInvert ? !operation : operation);
|
||||||
|
const formatConditionOperator = (condition: TPermissionConditionShape | string) => {
|
||||||
|
return (
|
||||||
|
typeof condition === "string" ? { [PermissionConditionOperators.$EQ]: condition } : condition
|
||||||
|
) as TPermissionConditionShape;
|
||||||
|
};
|
||||||
|
|
||||||
|
const isOperatorsASubset = (parentSet: TPermissionConditionShape, subset: TPermissionConditionShape) => {
|
||||||
|
// we compute each operator against each other in left hand side and right hand side
|
||||||
|
if (subset[PermissionConditionOperators.$EQ] || subset[PermissionConditionOperators.$NEQ]) {
|
||||||
|
const subsetOperatorValue = subset[PermissionConditionOperators.$EQ] || subset[PermissionConditionOperators.$NEQ];
|
||||||
|
const isInverted = !subset[PermissionConditionOperators.$EQ];
|
||||||
|
if (
|
||||||
|
parentSet[PermissionConditionOperators.$EQ] &&
|
||||||
|
invertTheOperation(isInverted, parentSet[PermissionConditionOperators.$EQ] !== subsetOperatorValue)
|
||||||
|
) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (
|
||||||
|
parentSet[PermissionConditionOperators.$NEQ] &&
|
||||||
|
invertTheOperation(isInverted, parentSet[PermissionConditionOperators.$NEQ] === subsetOperatorValue)
|
||||||
|
) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (
|
||||||
|
parentSet[PermissionConditionOperators.$IN] &&
|
||||||
|
invertTheOperation(isInverted, !parentSet[PermissionConditionOperators.$IN].includes(subsetOperatorValue))
|
||||||
|
) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
// ne and glob cannot match each other
|
||||||
|
if (parentSet[PermissionConditionOperators.$GLOB] && isInverted) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (
|
||||||
|
parentSet[PermissionConditionOperators.$GLOB] &&
|
||||||
|
!picomatch.isMatch(subsetOperatorValue, parentSet[PermissionConditionOperators.$GLOB], { strictSlashes: false })
|
||||||
|
) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (subset[PermissionConditionOperators.$IN]) {
|
||||||
|
const subsetOperatorValue = subset[PermissionConditionOperators.$IN];
|
||||||
|
if (
|
||||||
|
parentSet[PermissionConditionOperators.$EQ] &&
|
||||||
|
(subsetOperatorValue.length !== 1 || subsetOperatorValue[0] !== parentSet[PermissionConditionOperators.$EQ])
|
||||||
|
) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (
|
||||||
|
parentSet[PermissionConditionOperators.$NEQ] &&
|
||||||
|
subsetOperatorValue.includes(parentSet[PermissionConditionOperators.$NEQ])
|
||||||
|
) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (
|
||||||
|
parentSet[PermissionConditionOperators.$IN] &&
|
||||||
|
!subsetOperatorValue.every((el) => parentSet[PermissionConditionOperators.$IN].includes(el))
|
||||||
|
) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (
|
||||||
|
parentSet[PermissionConditionOperators.$GLOB] &&
|
||||||
|
!subsetOperatorValue.every((el) =>
|
||||||
|
picomatch.isMatch(el, parentSet[PermissionConditionOperators.$GLOB], {
|
||||||
|
strictSlashes: false
|
||||||
|
})
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (subset[PermissionConditionOperators.$GLOB]) {
|
||||||
|
const subsetOperatorValue = subset[PermissionConditionOperators.$GLOB];
|
||||||
|
const { isGlob } = picomatch.scan(subsetOperatorValue);
|
||||||
|
// if it's glob, all other fixed operators would make this superset because glob is powerful. like eq
|
||||||
|
// example: $in [dev, prod] => glob: dev** could mean anything starting with dev: thus is bigger
|
||||||
|
if (
|
||||||
|
isGlob &&
|
||||||
|
Object.keys(parentSet).some(
|
||||||
|
(el) => el !== PermissionConditionOperators.$GLOB && el !== PermissionConditionOperators.$NEQ
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
parentSet[PermissionConditionOperators.$EQ] &&
|
||||||
|
parentSet[PermissionConditionOperators.$EQ] !== subsetOperatorValue
|
||||||
|
) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (
|
||||||
|
parentSet[PermissionConditionOperators.$NEQ] &&
|
||||||
|
picomatch.isMatch(parentSet[PermissionConditionOperators.$NEQ], subsetOperatorValue, {
|
||||||
|
strictSlashes: false
|
||||||
|
})
|
||||||
|
) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
// if parent set is IN, glob cannot be used for children - It's a bigger scope
|
||||||
|
if (
|
||||||
|
parentSet[PermissionConditionOperators.$IN] &&
|
||||||
|
!parentSet[PermissionConditionOperators.$IN].includes(subsetOperatorValue)
|
||||||
|
) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (
|
||||||
|
parentSet[PermissionConditionOperators.$GLOB] &&
|
||||||
|
!picomatch.isMatch(subsetOperatorValue, parentSet[PermissionConditionOperators.$GLOB], {
|
||||||
|
strictSlashes: false
|
||||||
|
})
|
||||||
|
) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
};
|
||||||
|
|
||||||
|
const isSubsetForSamePermissionSubjectAction = (
|
||||||
|
parentSetRules: ReturnType<MongoAbility["possibleRulesFor"]>,
|
||||||
|
subsetRules: ReturnType<MongoAbility["possibleRulesFor"]>,
|
||||||
|
appendToMissingPermission: (condition?: MongoQuery) => void
|
||||||
|
) => {
|
||||||
|
const isMissingConditionInParent = parentSetRules.every((el) => !el.conditions);
|
||||||
|
if (isMissingConditionInParent) return true;
|
||||||
|
|
||||||
|
// all subset rules must pass in comparison to parent rul
|
||||||
|
return subsetRules.every((subsetRule) => {
|
||||||
|
const subsetRuleConditions = subsetRule.conditions as Record<string, TPermissionConditionShape | string>;
|
||||||
|
// compare subset rule with all parent rules
|
||||||
|
const isSubsetOfNonInvertedParentSet = parentSetRules
|
||||||
|
.filter((el) => !el.inverted)
|
||||||
|
.some((parentSetRule) => {
|
||||||
|
// get conditions and iterate
|
||||||
|
const parentSetRuleConditions = parentSetRule?.conditions as Record<string, TPermissionConditionShape | string>;
|
||||||
|
if (!parentSetRuleConditions) return true;
|
||||||
|
return Object.keys(parentSetRuleConditions).every((parentConditionField) => {
|
||||||
|
// if parent condition is missing then it's never a subset
|
||||||
|
if (!subsetRuleConditions?.[parentConditionField]) return false;
|
||||||
|
|
||||||
|
// standardize the conditions plain string operator => $eq function
|
||||||
|
const parentRuleConditionOperators = formatConditionOperator(parentSetRuleConditions[parentConditionField]);
|
||||||
|
const selectedSubsetRuleCondition = subsetRuleConditions?.[parentConditionField];
|
||||||
|
const subsetRuleConditionOperators = formatConditionOperator(selectedSubsetRuleCondition);
|
||||||
|
return isOperatorsASubset(parentRuleConditionOperators, subsetRuleConditionOperators);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
const invertedParentSetRules = parentSetRules.filter((el) => el.inverted);
|
||||||
|
const isNotSubsetOfInvertedParentSet = invertedParentSetRules.length
|
||||||
|
? !invertedParentSetRules.some((parentSetRule) => {
|
||||||
|
// get conditions and iterate
|
||||||
|
const parentSetRuleConditions = parentSetRule?.conditions as Record<
|
||||||
|
string,
|
||||||
|
TPermissionConditionShape | string
|
||||||
|
>;
|
||||||
|
if (!parentSetRuleConditions) return true;
|
||||||
|
return Object.keys(parentSetRuleConditions).every((parentConditionField) => {
|
||||||
|
// if parent condition is missing then it's never a subset
|
||||||
|
if (!subsetRuleConditions?.[parentConditionField]) return false;
|
||||||
|
|
||||||
|
// standardize the conditions plain string operator => $eq function
|
||||||
|
const parentRuleConditionOperators = formatConditionOperator(parentSetRuleConditions[parentConditionField]);
|
||||||
|
const selectedSubsetRuleCondition = subsetRuleConditions?.[parentConditionField];
|
||||||
|
const subsetRuleConditionOperators = formatConditionOperator(selectedSubsetRuleCondition);
|
||||||
|
return isOperatorsASubset(parentRuleConditionOperators, subsetRuleConditionOperators);
|
||||||
|
});
|
||||||
|
})
|
||||||
|
: true;
|
||||||
|
const isSubset = isSubsetOfNonInvertedParentSet && isNotSubsetOfInvertedParentSet;
|
||||||
|
if (!isSubset) {
|
||||||
|
appendToMissingPermission(subsetRule.conditions);
|
||||||
|
}
|
||||||
|
return isSubset;
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
export const validatePermissionBoundary = (parentSetPermissions: MongoAbility, subsetPermissions: MongoAbility) => {
|
||||||
|
const checkedPermissionRules = new Set<string>();
|
||||||
|
const missingPermissions: TMissingPermission[] = [];
|
||||||
|
|
||||||
|
subsetPermissions.rules.forEach((subsetPermissionRules) => {
|
||||||
|
const subsetPermissionSubject = subsetPermissionRules.subject.toString();
|
||||||
|
let subsetPermissionActions: string[] = [];
|
||||||
|
|
||||||
|
// actions can be string or string[]
|
||||||
|
if (typeof subsetPermissionRules.action === "string") {
|
||||||
|
subsetPermissionActions.push(subsetPermissionRules.action);
|
||||||
|
} else {
|
||||||
|
subsetPermissionRules.action.forEach((subsetPermissionAction) => {
|
||||||
|
subsetPermissionActions.push(subsetPermissionAction);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// if action is already processed ignore
|
||||||
|
subsetPermissionActions = subsetPermissionActions.filter(
|
||||||
|
(el) => !checkedPermissionRules.has(getPermissionSetID(el, subsetPermissionSubject))
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!subsetPermissionActions.length) return;
|
||||||
|
subsetPermissionActions.forEach((subsetPermissionAction) => {
|
||||||
|
const parentSetRulesOfSubset = parentSetPermissions.possibleRulesFor(
|
||||||
|
subsetPermissionAction,
|
||||||
|
subsetPermissionSubject
|
||||||
|
);
|
||||||
|
const nonInveretedOnes = parentSetRulesOfSubset.filter((el) => !el.inverted);
|
||||||
|
if (!nonInveretedOnes.length) {
|
||||||
|
missingPermissions.push({ action: subsetPermissionAction, subject: subsetPermissionSubject });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const subsetRules = subsetPermissions.possibleRulesFor(subsetPermissionAction, subsetPermissionSubject);
|
||||||
|
isSubsetForSamePermissionSubjectAction(parentSetRulesOfSubset, subsetRules, (conditions) => {
|
||||||
|
missingPermissions.push({ action: subsetPermissionAction, subject: subsetPermissionSubject, conditions });
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
subsetPermissionActions.forEach((el) =>
|
||||||
|
checkedPermissionRules.add(getPermissionSetID(el, subsetPermissionSubject))
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
if (missingPermissions.length) {
|
||||||
|
return { isValid: false as const, missingPermissions };
|
||||||
|
}
|
||||||
|
|
||||||
|
return { isValid: true };
|
||||||
|
};
|
@ -1,5 +1,5 @@
|
|||||||
/* eslint-disable @typescript-eslint/no-unsafe-assignment */
|
/* eslint-disable @typescript-eslint/no-unsafe-assignment */
|
||||||
import { buildMongoQueryMatcher, MongoAbility } from "@casl/ability";
|
import { buildMongoQueryMatcher } from "@casl/ability";
|
||||||
import { FieldCondition, FieldInstruction, JsInterpreter } from "@ucast/mongo2js";
|
import { FieldCondition, FieldInstruction, JsInterpreter } from "@ucast/mongo2js";
|
||||||
import picomatch from "picomatch";
|
import picomatch from "picomatch";
|
||||||
|
|
||||||
@ -20,45 +20,8 @@ const glob: JsInterpreter<FieldCondition<string>> = (node, object, context) => {
|
|||||||
|
|
||||||
export const conditionsMatcher = buildMongoQueryMatcher({ $glob }, { glob });
|
export const conditionsMatcher = buildMongoQueryMatcher({ $glob }, { glob });
|
||||||
|
|
||||||
/**
|
|
||||||
* Extracts and formats permissions from a CASL Ability object or a raw permission set.
|
|
||||||
*/
|
|
||||||
const extractPermissions = (ability: MongoAbility) => {
|
|
||||||
const permissions: string[] = [];
|
|
||||||
ability.rules.forEach((permission) => {
|
|
||||||
if (typeof permission.action === "string") {
|
|
||||||
permissions.push(`${permission.action}_${permission.subject as string}`);
|
|
||||||
} else {
|
|
||||||
permission.action.forEach((permissionAction) => {
|
|
||||||
permissions.push(`${permissionAction}_${permission.subject as string}`);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
return permissions;
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Compares two sets of permissions to determine if the first set is at least as privileged as the second set.
|
|
||||||
* The function checks if all permissions in the second set are contained within the first set and if the first set has equal or more permissions.
|
|
||||||
*
|
|
||||||
*/
|
|
||||||
export const isAtLeastAsPrivileged = (permissions1: MongoAbility, permissions2: MongoAbility) => {
|
|
||||||
const set1 = new Set(extractPermissions(permissions1));
|
|
||||||
const set2 = new Set(extractPermissions(permissions2));
|
|
||||||
|
|
||||||
for (const perm of set2) {
|
|
||||||
if (!set1.has(perm)) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return set1.size >= set2.size;
|
|
||||||
};
|
|
||||||
|
|
||||||
export enum PermissionConditionOperators {
|
export enum PermissionConditionOperators {
|
||||||
$IN = "$in",
|
$IN = "$in",
|
||||||
$ALL = "$all",
|
|
||||||
$REGEX = "$regex",
|
|
||||||
$EQ = "$eq",
|
$EQ = "$eq",
|
||||||
$NEQ = "$ne",
|
$NEQ = "$ne",
|
||||||
$GLOB = "$glob"
|
$GLOB = "$glob"
|
||||||
|
@ -24,6 +24,7 @@ const databaseReadReplicaSchema = z
|
|||||||
|
|
||||||
const envSchema = z
|
const envSchema = z
|
||||||
.object({
|
.object({
|
||||||
|
INFISICAL_PLATFORM_VERSION: zpStr(z.string().optional()),
|
||||||
PORT: z.coerce.number().default(IS_PACKAGED ? 8080 : 4000),
|
PORT: z.coerce.number().default(IS_PACKAGED ? 8080 : 4000),
|
||||||
DISABLE_SECRET_SCANNING: z
|
DISABLE_SECRET_SCANNING: z
|
||||||
.enum(["true", "false"])
|
.enum(["true", "false"])
|
||||||
@ -184,6 +185,14 @@ const envSchema = z
|
|||||||
USE_PG_QUEUE: zodStrBool.default("false"),
|
USE_PG_QUEUE: zodStrBool.default("false"),
|
||||||
SHOULD_INIT_PG_QUEUE: zodStrBool.default("false"),
|
SHOULD_INIT_PG_QUEUE: zodStrBool.default("false"),
|
||||||
|
|
||||||
|
/* Gateway----------------------------------------------------------------------------- */
|
||||||
|
GATEWAY_INFISICAL_STATIC_IP_ADDRESS: zpStr(z.string().optional()),
|
||||||
|
GATEWAY_RELAY_ADDRESS: zpStr(z.string().optional()),
|
||||||
|
GATEWAY_RELAY_REALM: zpStr(z.string().optional()),
|
||||||
|
GATEWAY_RELAY_AUTH_SECRET: zpStr(z.string().optional()),
|
||||||
|
|
||||||
|
/* ----------------------------------------------------------------------------- */
|
||||||
|
|
||||||
/* App Connections ----------------------------------------------------------------------------- */
|
/* App Connections ----------------------------------------------------------------------------- */
|
||||||
|
|
||||||
// aws
|
// aws
|
||||||
@ -208,6 +217,13 @@ const envSchema = z
|
|||||||
INF_APP_CONNECTION_AZURE_CLIENT_ID: zpStr(z.string().optional()),
|
INF_APP_CONNECTION_AZURE_CLIENT_ID: zpStr(z.string().optional()),
|
||||||
INF_APP_CONNECTION_AZURE_CLIENT_SECRET: zpStr(z.string().optional()),
|
INF_APP_CONNECTION_AZURE_CLIENT_SECRET: zpStr(z.string().optional()),
|
||||||
|
|
||||||
|
// datadog
|
||||||
|
SHOULD_USE_DATADOG_TRACER: zodStrBool.default("false"),
|
||||||
|
DATADOG_PROFILING_ENABLED: zodStrBool.default("false"),
|
||||||
|
DATADOG_ENV: zpStr(z.string().optional().default("prod")),
|
||||||
|
DATADOG_SERVICE: zpStr(z.string().optional().default("infisical-core")),
|
||||||
|
DATADOG_HOSTNAME: zpStr(z.string().optional()),
|
||||||
|
|
||||||
/* CORS ----------------------------------------------------------------------------- */
|
/* CORS ----------------------------------------------------------------------------- */
|
||||||
|
|
||||||
CORS_ALLOWED_ORIGINS: zpStr(
|
CORS_ALLOWED_ORIGINS: zpStr(
|
||||||
|
29
backend/src/lib/crypto/hashtext.ts
Normal file
29
backend/src/lib/crypto/hashtext.ts
Normal file
@ -0,0 +1,29 @@
|
|||||||
|
// used for postgres lock
|
||||||
|
// this is something postgres does under the hood
|
||||||
|
// convert any string to a unique number
|
||||||
|
export const hashtext = (text: string) => {
|
||||||
|
// Convert text to UTF8 bytes array for consistent behavior with PostgreSQL
|
||||||
|
const encoder = new TextEncoder();
|
||||||
|
const bytes = encoder.encode(text);
|
||||||
|
|
||||||
|
// Implementation of hash_any
|
||||||
|
let result = 0;
|
||||||
|
|
||||||
|
for (let i = 0; i < bytes.length; i += 1) {
|
||||||
|
// eslint-disable-next-line no-bitwise
|
||||||
|
result = ((result << 5) + result) ^ bytes[i];
|
||||||
|
// Keep within 32-bit integer range
|
||||||
|
// eslint-disable-next-line no-bitwise
|
||||||
|
result >>>= 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Convert to signed 32-bit integer like PostgreSQL
|
||||||
|
// eslint-disable-next-line no-bitwise
|
||||||
|
return result | 0;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const pgAdvisoryLockHashText = (text: string) => {
|
||||||
|
const hash = hashtext(text);
|
||||||
|
// Ensure positive value within PostgreSQL integer range
|
||||||
|
return Math.abs(hash) % 2 ** 31;
|
||||||
|
};
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user