mirror of
https://github.com/Infisical/infisical.git
synced 2025-09-06 06:00:42 +00:00
Compare commits
494 Commits
secret-ove
...
secret-syn
Author | SHA1 | Date | |
---|---|---|---|
|
8368fa2d29 | ||
|
cea43d497d | ||
|
3700597ba7 | ||
|
65f0597bd8 | ||
|
5b3cae7255 | ||
|
a4ff6340f8 | ||
|
bfb2486204 | ||
|
c29b5e37f3 | ||
|
e666409026 | ||
|
ecfc8b5f87 | ||
|
a6b4939ea5 | ||
|
640dccadb7 | ||
|
3ebd5305c2 | ||
|
8d1c0b432b | ||
|
be588c2653 | ||
|
f7828ed458 | ||
|
b40bb72643 | ||
|
4f1cd69bcc | ||
|
4d4b4c13c3 | ||
|
c8bf9049de | ||
|
ab91863c77 | ||
|
6db4c614af | ||
|
21e2db2963 | ||
|
da0d4a31b1 | ||
|
b7d3ddff21 | ||
|
a3c6b1134b | ||
|
d931725930 | ||
|
6702498028 | ||
|
b650b142f7 | ||
|
19a5f52d20 | ||
|
e51c5256a0 | ||
|
3bb0c9b3ad | ||
|
41404148e1 | ||
|
e04e11f597 | ||
|
5fffa17c30 | ||
|
3fa6154517 | ||
|
1d5cdb4000 | ||
|
a1b53855bb | ||
|
b447ccd3f0 | ||
|
2058afb3e0 | ||
|
dc0a7d3a70 | ||
|
53618a4bd8 | ||
|
d6ca2cdc2e | ||
|
acf3bdc5a3 | ||
|
533d9cea38 | ||
|
82faf3a797 | ||
|
ece0af7787 | ||
|
6bccb1e5eb | ||
|
dc23abdb86 | ||
|
8d3be92d09 | ||
|
1e7f0f8a39 | ||
|
c99a4b7cc8 | ||
|
e3838643e5 | ||
|
5bd961735d | ||
|
1147cfcea4 | ||
|
abb577e4e9 | ||
|
29dd49d696 | ||
|
0f76003f77 | ||
|
1c4dfbe028 | ||
|
65be2e7f7b | ||
|
cf64c89ea3 | ||
|
d934f03597 | ||
|
e051cfd146 | ||
|
be30327dc9 | ||
|
f9784f15ed | ||
|
8e42fdaf5b | ||
|
2a52463585 | ||
|
20287973b1 | ||
|
7f958e6d89 | ||
|
e7138f1be9 | ||
|
01fba20872 | ||
|
696a70577a | ||
|
8ba61e8293 | ||
|
5944642278 | ||
|
f5434b5cba | ||
|
1159b74bdb | ||
|
bc4885b098 | ||
|
97be78a107 | ||
|
4b42f7b1b5 | ||
|
3de7fec650 | ||
|
7bc6697801 | ||
|
34c6d254a0 | ||
|
a0da2f2d4c | ||
|
c7987772e3 | ||
|
07a55bb943 | ||
|
7894bd8ae1 | ||
|
5eee99e9ac | ||
|
4485d7f757 | ||
|
d3c3f3a17e | ||
|
999588b06e | ||
|
37153cd8cf | ||
|
4547ed7aeb | ||
|
e8ef0191d6 | ||
|
7d74dce82b | ||
|
aae6a3f9af | ||
|
43dd45de29 | ||
|
13b20806ba | ||
|
49b5ab8126 | ||
|
c99d5c210c | ||
|
fc6778dd89 | ||
|
2f68ff1629 | ||
|
cde7673a23 | ||
|
1165b05e8a | ||
|
8884c0e6bd | ||
|
0762de93d6 | ||
|
af2f21fe93 | ||
|
dcd588007c | ||
|
8d6461b01d | ||
|
f52dbaa2f2 | ||
|
0c92764409 | ||
|
976317e71b | ||
|
7b52d60036 | ||
|
83479a091e | ||
|
4e2592960d | ||
|
8d5b6a17b1 | ||
|
8945bc0dc1 | ||
|
bceaac844f | ||
|
2f375d6b65 | ||
|
8f00bab61c | ||
|
ec12acfcdf | ||
|
34a8301617 | ||
|
1b22438c46 | ||
|
8ffff7e779 | ||
|
a349dda4bc | ||
|
f63ee39f3d | ||
|
f550a2ae3f | ||
|
725e55f7e5 | ||
|
f59efc1948 | ||
|
f52e90a5c1 | ||
|
2fda307b67 | ||
|
ff7b530252 | ||
|
10cfbe0c74 | ||
|
4da24bfa39 | ||
|
8123be4c14 | ||
|
9a98192b9b | ||
|
991ee20ec7 | ||
|
dc48281e6a | ||
|
b3002d784e | ||
|
c782493704 | ||
|
6c7062fa16 | ||
|
5c632db282 | ||
|
817daecc6c | ||
|
461deef0d5 | ||
|
7748e03612 | ||
|
2389c64e69 | ||
|
de5ad47f77 | ||
|
57c667f0b1 | ||
|
15d3638612 | ||
|
e0161cd06f | ||
|
ebd3b5c9d1 | ||
|
7c12fa3a4c | ||
|
0af53e82da | ||
|
f0c080187e | ||
|
47118bcf19 | ||
|
bb1975491f | ||
|
52bbe25fc5 | ||
|
28cc919ff7 | ||
|
5136dbc543 | ||
|
bceddab89f | ||
|
6d5bed756a | ||
|
5c21ac3182 | ||
|
6204b181e7 | ||
|
06de9d06c9 | ||
|
3cceec86c8 | ||
|
ff043f990f | ||
|
bb14231d71 | ||
|
9e177c1e45 | ||
|
5aeb823c9e | ||
|
ef6f79f7a6 | ||
|
43752e1888 | ||
|
d587e779f5 | ||
|
d985b84577 | ||
|
f9a9565630 | ||
|
05ba0abadd | ||
|
fff9a96204 | ||
|
bd72129d8c | ||
|
bf10b2f58a | ||
|
d24f5a57a8 | ||
|
166104e523 | ||
|
a7847f177c | ||
|
48e5f550e9 | ||
|
4a4a7fd325 | ||
|
91b8ed8015 | ||
|
6cf978b593 | ||
|
68fbb399fc | ||
|
97366f6e95 | ||
|
c83d4af7a3 | ||
|
f78556c85f | ||
|
13aa380cac | ||
|
c35c937c63 | ||
|
b10752acb5 | ||
|
eb9b75d930 | ||
|
f2a9a57c95 | ||
|
6384fa6dba | ||
|
c34ec8de09 | ||
|
ef8a7f1233 | ||
|
273a7b9657 | ||
|
a3b6fa9a53 | ||
|
f60dd528e8 | ||
|
8ffef1da8e | ||
|
f352f98374 | ||
|
91a76f50ca | ||
|
ea4bb0a062 | ||
|
3d6be7b1b2 | ||
|
09db98db50 | ||
|
a37f1eb1f8 | ||
|
2113abcfdc | ||
|
ea2707651c | ||
|
12558e8614 | ||
|
b986ff9a21 | ||
|
106833328b | ||
|
987f87e562 | ||
|
4d06d5cbb0 | ||
|
bad934de48 | ||
|
90b93fbd15 | ||
|
c2db2a0bc7 | ||
|
b0d24de008 | ||
|
0473fb0ddb | ||
|
4ccb5dc9b0 | ||
|
930425d5dc | ||
|
f77a53bd8e | ||
|
4bd61e5607 | ||
|
aa4dbfa073 | ||
|
b479406ba0 | ||
|
7cf9d933da | ||
|
ca2825ba95 | ||
|
b8fa4d5255 | ||
|
0d3cb2d41a | ||
|
e0d19d7b65 | ||
|
f5a0d8be78 | ||
|
c7ae7be493 | ||
|
18881749fd | ||
|
8a72023e80 | ||
|
41a3ac6bd4 | ||
|
fa54c406dc | ||
|
2fb5cc1712 | ||
|
b352428032 | ||
|
1a2eef3ba6 | ||
|
914bb3d389 | ||
|
0c562150f5 | ||
|
6fde132804 | ||
|
be70bfa33f | ||
|
7758e5dbfa | ||
|
22fca374f2 | ||
|
799721782a | ||
|
86d430f911 | ||
|
94039ca509 | ||
|
c8f124e4c5 | ||
|
2501c57030 | ||
|
7c28ee844e | ||
|
d5390fcafc | ||
|
1b40f5d475 | ||
|
3cec1b4021 | ||
|
97b2c534a7 | ||
|
d71362ccc3 | ||
|
e4d90eb055 | ||
|
55607a4886 | ||
|
97dac1da94 | ||
|
f9f989c8af | ||
|
385c75c543 | ||
|
f16dca45d9 | ||
|
118c28df54 | ||
|
249b2933da | ||
|
272336092d | ||
|
6f05a6d82c | ||
|
84ebdb8503 | ||
|
b464941fbc | ||
|
77e8d8a86d | ||
|
c61dd1ee6e | ||
|
9db8573e72 | ||
|
ce8653e908 | ||
|
fd4cdc2769 | ||
|
90a1cc9330 | ||
|
78bfd0922a | ||
|
458dcd31c1 | ||
|
372537f0b6 | ||
|
e173ff3828 | ||
|
2baadf60d1 | ||
|
e13fc93bac | ||
|
60b3f5c7c6 | ||
|
6b14fbcce2 | ||
|
86fbe5cc24 | ||
|
3f7862a345 | ||
|
9661458469 | ||
|
c7c1eb0f5f | ||
|
a1e48a1795 | ||
|
d14e80b771 | ||
|
0264d37d9b | ||
|
11a1604e14 | ||
|
f788dee398 | ||
|
88120ed45e | ||
|
d6a377416d | ||
|
dbbd58ffb7 | ||
|
5d2beb3604 | ||
|
ec65e0e29c | ||
|
b819848058 | ||
|
1b0ef540fe | ||
|
4496241002 | ||
|
52e32484ce | ||
|
8b497699d4 | ||
|
be73f62226 | ||
|
102620ff09 | ||
|
994ee88852 | ||
|
770e25b895 | ||
|
fcf3bdb440 | ||
|
89c11b5541 | ||
|
5f764904e2 | ||
|
1a75384dba | ||
|
50f434cd80 | ||
|
d879cfd90c | ||
|
c2cea8cffc | ||
|
ca1f5eaca3 | ||
|
04086376ea | ||
|
364027a88a | ||
|
ca110d11b0 | ||
|
4e8f404f16 | ||
|
22abb78f48 | ||
|
24f11406e1 | ||
|
d5d67c82b2 | ||
|
35cfcf1f0f | ||
|
368e00ea71 | ||
|
2c8cfeb826 | ||
|
23237dd055 | ||
|
70d22f90ec | ||
|
e10aec3170 | ||
|
0b11dcd627 | ||
|
d88a473b47 | ||
|
4f52400887 | ||
|
34eb9f475a | ||
|
902a0b0c56 | ||
|
d1e8ae3c98 | ||
|
5c9243d691 | ||
|
35d1eabf49 | ||
|
b6902160ce | ||
|
fbfc51ee93 | ||
|
9e6294786f | ||
|
9d92ffce95 | ||
|
9193418f8b | ||
|
847c50d2d4 | ||
|
efa043c3d2 | ||
|
352ef050c3 | ||
|
b6b9fb6ef5 | ||
|
7e94791635 | ||
|
eedc5f533e | ||
|
fc5d42baf0 | ||
|
b95c35620a | ||
|
fa867e5068 | ||
|
8851faec65 | ||
|
47fb666dc7 | ||
|
569edd2852 | ||
|
676ebaf3c2 | ||
|
adb3185042 | ||
|
8da0a4d846 | ||
|
eebf080e3c | ||
|
02ee418763 | ||
|
97be31f11e | ||
|
667cceebc0 | ||
|
1ad02e2da6 | ||
|
93445d96b3 | ||
|
e105a5f7da | ||
|
72b80e1fd7 | ||
|
6429adfaf6 | ||
|
fd89b3c702 | ||
|
50e40e8bcf | ||
|
6100086338 | ||
|
000dd6c223 | ||
|
389e2e1fb7 | ||
|
88fcbcadd4 | ||
|
faca20c00c | ||
|
69c3687add | ||
|
1645534b54 | ||
|
dca0b0c614 | ||
|
60dc1d1e00 | ||
|
2d68f9aa16 | ||
|
e694293ebe | ||
|
ef6f5ecc4b | ||
|
56f5249925 | ||
|
df5b3fa8dc | ||
|
035ac0fe8d | ||
|
c12408eb81 | ||
|
13194296c6 | ||
|
be20a507ac | ||
|
63cf36c722 | ||
|
4dcd3ed06c | ||
|
59cffe8cfb | ||
|
fa61867a72 | ||
|
f3694ca730 | ||
|
8fcd6d9997 | ||
|
1b32de5c5b | ||
|
522795871e | ||
|
5c63955fde | ||
|
d7f3892b73 | ||
|
33af2fb2b8 | ||
|
45ff9a50b6 | ||
|
81cdfb9861 | ||
|
e1e553ce23 | ||
|
e7a6f46f56 | ||
|
b51d997e26 | ||
|
23f6fbe9fc | ||
|
c1fb5d8998 | ||
|
0cb21082c7 | ||
|
4e3613ac6e | ||
|
6be65f7a56 | ||
|
63cb484313 | ||
|
aa3af1672a | ||
|
33fe11e0fd | ||
|
d924a4bccc | ||
|
3fc7a71bc7 | ||
|
986fe2fe23 | ||
|
08f7e530b0 | ||
|
e9f5055481 | ||
|
35055955e2 | ||
|
c188e7cd2b | ||
|
7d2ded6235 | ||
|
c568f40954 | ||
|
28f87b8b27 | ||
|
aab1a0297e | ||
|
dd0f5cebd2 | ||
|
1b29a4564a | ||
|
9e3c0c8583 | ||
|
3e803debb4 | ||
|
16ebe0f8e7 | ||
|
e8eb1b5f8b | ||
|
6e37b9f969 | ||
|
899b7fe024 | ||
|
098a8b81be | ||
|
d3d0d44778 | ||
|
e852cd8b4a | ||
|
830a2f9581 | ||
|
dc4db40936 | ||
|
0beff3cc1c | ||
|
67abcbfe7a | ||
|
5a3325fc53 | ||
|
3dde786621 | ||
|
da6b233db1 | ||
|
6958f1cfbd | ||
|
adf7a88d67 | ||
|
b8cd836225 | ||
|
6826b1c242 | ||
|
fc772e6b89 | ||
|
35012fde03 | ||
|
6e14b2f793 | ||
|
5a3aa3d608 | ||
|
95b327de50 | ||
|
a3c36f82f3 | ||
|
42612da57d | ||
|
f63c07d538 | ||
|
98a08d136e | ||
|
6c74b875f3 | ||
|
793cd4c144 | ||
|
dc0cc4c29d | ||
|
c8108ff49a | ||
|
6dd639be60 | ||
|
ebe05661d3 | ||
|
4f0007faa5 | ||
|
806165b9e9 | ||
|
ec0be1166f | ||
|
899d01237c | ||
|
9fde0a5787 | ||
|
ff5dbe74fd | ||
|
24004084f2 | ||
|
0e401ece73 | ||
|
c4e1651df7 | ||
|
514c7596db | ||
|
9fbdede82c | ||
|
e519637e89 | ||
|
ba393b0498 | ||
|
4150f81d83 | ||
|
a45bba8537 | ||
|
fe7e8e7240 | ||
|
cf54365022 | ||
|
4b9e57ae61 | ||
|
eb27983990 | ||
|
fa311b032c | ||
|
71651f85fe | ||
|
d28d3449de | ||
|
14ffa59530 | ||
|
9ee2581659 | ||
|
2deff0ef55 | ||
|
4312378589 | ||
|
d749a9621f | ||
|
9686d14e7f | ||
|
4f26365c21 | ||
|
c974df104e | ||
|
e88fdc957e | ||
|
de2c1c5560 | ||
|
2cbd66e804 | ||
|
4704774c63 | ||
|
4a55ecbe12 | ||
|
1e29d550be | ||
|
0c98d9187d | ||
|
e106a6dceb | ||
|
2d3b1b18d2 | ||
|
d5dd2e8bfd |
123
.github/workflows/build-docker-image-to-prod.yml
vendored
123
.github/workflows/build-docker-image-to-prod.yml
vendored
@@ -1,123 +0,0 @@
|
||||
name: Release production images (frontend, backend)
|
||||
on:
|
||||
push:
|
||||
tags:
|
||||
- "infisical/v*.*.*"
|
||||
- "!infisical/v*.*.*-postgres"
|
||||
|
||||
jobs:
|
||||
backend-image:
|
||||
name: Build backend image
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Extract version from tag
|
||||
id: extract_version
|
||||
run: echo "::set-output name=version::${GITHUB_REF_NAME#infisical/}"
|
||||
- name: ☁️ Checkout source
|
||||
uses: actions/checkout@v3
|
||||
- name: 📦 Install dependencies to test all dependencies
|
||||
run: npm ci --only-production
|
||||
working-directory: backend
|
||||
# - name: 🧪 Run tests
|
||||
# run: npm run test:ci
|
||||
# working-directory: backend
|
||||
- name: Save commit hashes for tag
|
||||
id: commit
|
||||
uses: pr-mpt/actions-commit-hash@v2
|
||||
- name: 🔧 Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v2
|
||||
- name: 🐋 Login to Docker Hub
|
||||
uses: docker/login-action@v2
|
||||
with:
|
||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
- name: Set up Depot CLI
|
||||
uses: depot/setup-action@v1
|
||||
- name: 📦 Build backend and export to Docker
|
||||
uses: depot/build-push-action@v1
|
||||
with:
|
||||
project: 64mmf0n610
|
||||
token: ${{ secrets.DEPOT_PROJECT_TOKEN }}
|
||||
load: true
|
||||
context: backend
|
||||
tags: infisical/infisical:test
|
||||
platforms: linux/amd64,linux/arm64
|
||||
- name: ⏻ Spawn backend container and dependencies
|
||||
run: |
|
||||
docker compose -f .github/resources/docker-compose.be-test.yml up --wait --quiet-pull
|
||||
- name: 🧪 Test backend image
|
||||
run: |
|
||||
./.github/resources/healthcheck.sh infisical-backend-test
|
||||
- name: ⏻ Shut down backend container and dependencies
|
||||
run: |
|
||||
docker compose -f .github/resources/docker-compose.be-test.yml down
|
||||
- name: 🏗️ Build backend and push
|
||||
uses: depot/build-push-action@v1
|
||||
with:
|
||||
project: 64mmf0n610
|
||||
token: ${{ secrets.DEPOT_PROJECT_TOKEN }}
|
||||
push: true
|
||||
context: backend
|
||||
tags: |
|
||||
infisical/backend:${{ steps.commit.outputs.short }}
|
||||
infisical/backend:latest
|
||||
infisical/backend:${{ steps.extract_version.outputs.version }}
|
||||
platforms: linux/amd64,linux/arm64
|
||||
|
||||
frontend-image:
|
||||
name: Build frontend image
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Extract version from tag
|
||||
id: extract_version
|
||||
run: echo "::set-output name=version::${GITHUB_REF_NAME#infisical/}"
|
||||
- name: ☁️ Checkout source
|
||||
uses: actions/checkout@v3
|
||||
- name: Save commit hashes for tag
|
||||
id: commit
|
||||
uses: pr-mpt/actions-commit-hash@v2
|
||||
- name: 🔧 Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v2
|
||||
- name: 🐋 Login to Docker Hub
|
||||
uses: docker/login-action@v2
|
||||
with:
|
||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
- name: Set up Depot CLI
|
||||
uses: depot/setup-action@v1
|
||||
- name: 📦 Build frontend and export to Docker
|
||||
uses: depot/build-push-action@v1
|
||||
with:
|
||||
load: true
|
||||
token: ${{ secrets.DEPOT_PROJECT_TOKEN }}
|
||||
project: 64mmf0n610
|
||||
context: frontend
|
||||
tags: infisical/frontend:test
|
||||
platforms: linux/amd64,linux/arm64
|
||||
build-args: |
|
||||
POSTHOG_API_KEY=${{ secrets.PUBLIC_POSTHOG_API_KEY }}
|
||||
NEXT_INFISICAL_PLATFORM_VERSION=${{ steps.extract_version.outputs.version }}
|
||||
- name: ⏻ Spawn frontend container
|
||||
run: |
|
||||
docker run -d --rm --name infisical-frontend-test infisical/frontend:test
|
||||
- name: 🧪 Test frontend image
|
||||
run: |
|
||||
./.github/resources/healthcheck.sh infisical-frontend-test
|
||||
- name: ⏻ Shut down frontend container
|
||||
run: |
|
||||
docker stop infisical-frontend-test
|
||||
- name: 🏗️ Build frontend and push
|
||||
uses: depot/build-push-action@v1
|
||||
with:
|
||||
project: 64mmf0n610
|
||||
push: true
|
||||
token: ${{ secrets.DEPOT_PROJECT_TOKEN }}
|
||||
context: frontend
|
||||
tags: |
|
||||
infisical/frontend:${{ steps.commit.outputs.short }}
|
||||
infisical/frontend:latest
|
||||
infisical/frontend:${{ steps.extract_version.outputs.version }}
|
||||
platforms: linux/amd64,linux/arm64
|
||||
build-args: |
|
||||
POSTHOG_API_KEY=${{ secrets.PUBLIC_POSTHOG_API_KEY }}
|
||||
NEXT_INFISICAL_PLATFORM_VERSION=${{ steps.extract_version.outputs.version }}
|
82
.github/workflows/nightly-tag-generation.yml
vendored
Normal file
82
.github/workflows/nightly-tag-generation.yml
vendored
Normal file
@@ -0,0 +1,82 @@
|
||||
name: Generate Nightly Tag
|
||||
|
||||
on:
|
||||
schedule:
|
||||
- cron: '0 0 * * *' # Run daily at midnight UTC
|
||||
workflow_dispatch: # Allow manual triggering for testing
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
|
||||
jobs:
|
||||
create-nightly-tag:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0 # Fetch all history for tags
|
||||
token: ${{ secrets.GO_RELEASER_GITHUB_TOKEN }}
|
||||
|
||||
- name: Configure Git
|
||||
run: |
|
||||
git config user.name "github-actions[bot]"
|
||||
git config user.email "github-actions[bot]@users.noreply.github.com"
|
||||
|
||||
- name: Generate nightly tag
|
||||
run: |
|
||||
# Get the latest infisical production tag
|
||||
LATEST_STABLE_TAG=$(git tag --list | grep "^v[0-9].*$" | grep -v "nightly" | sort -V | tail -n1)
|
||||
|
||||
if [ -z "$LATEST_STABLE_TAG" ]; then
|
||||
echo "No infisical production tags found, using v0.1.0"
|
||||
LATEST_STABLE_TAG="v0.1.0"
|
||||
fi
|
||||
|
||||
echo "Latest production tag: $LATEST_STABLE_TAG"
|
||||
|
||||
# Get current date in YYYYMMDD format
|
||||
DATE=$(date +%Y%m%d)
|
||||
|
||||
# Base nightly tag name
|
||||
BASE_TAG="${LATEST_STABLE_TAG}-nightly-${DATE}"
|
||||
|
||||
# Check if this exact tag already exists
|
||||
if git tag --list | grep -q "^${BASE_TAG}$"; then
|
||||
echo "Base tag ${BASE_TAG} already exists, finding next increment"
|
||||
|
||||
# Find existing tags for this date and get the highest increment
|
||||
EXISTING_TAGS=$(git tag --list | grep "^${BASE_TAG}" | grep -E '\.[0-9]+$' || true)
|
||||
|
||||
if [ -z "$EXISTING_TAGS" ]; then
|
||||
# No incremental tags exist, create .1
|
||||
NIGHTLY_TAG="${BASE_TAG}.1"
|
||||
else
|
||||
# Find the highest increment
|
||||
HIGHEST_INCREMENT=$(echo "$EXISTING_TAGS" | sed "s|^${BASE_TAG}\.||" | sort -n | tail -n1)
|
||||
NEXT_INCREMENT=$((HIGHEST_INCREMENT + 1))
|
||||
NIGHTLY_TAG="${BASE_TAG}.${NEXT_INCREMENT}"
|
||||
fi
|
||||
else
|
||||
# Base tag doesn't exist, use it
|
||||
NIGHTLY_TAG="$BASE_TAG"
|
||||
fi
|
||||
|
||||
echo "Generated nightly tag: $NIGHTLY_TAG"
|
||||
echo "NIGHTLY_TAG=$NIGHTLY_TAG" >> $GITHUB_ENV
|
||||
echo "LATEST_PRODUCTION_TAG=$LATEST_STABLE_TAG" >> $GITHUB_ENV
|
||||
|
||||
git tag "$NIGHTLY_TAG"
|
||||
git push origin "$NIGHTLY_TAG"
|
||||
echo "✅ Created and pushed nightly tag: $NIGHTLY_TAG"
|
||||
|
||||
- name: Create GitHub Release
|
||||
uses: softprops/action-gh-release@v2
|
||||
with:
|
||||
tag_name: ${{ env.NIGHTLY_TAG }}
|
||||
name: ${{ env.NIGHTLY_TAG }}
|
||||
draft: false
|
||||
prerelease: true
|
||||
generate_release_notes: true
|
||||
make_latest: false
|
@@ -2,7 +2,9 @@ name: Release standalone docker image
|
||||
on:
|
||||
push:
|
||||
tags:
|
||||
- "infisical/v*.*.*-postgres"
|
||||
- "v*.*.*"
|
||||
- "v*.*.*-nightly-*"
|
||||
- "v*.*.*-nightly-*.*"
|
||||
|
||||
jobs:
|
||||
infisical-tests:
|
||||
@@ -17,7 +19,7 @@ jobs:
|
||||
steps:
|
||||
- name: Extract version from tag
|
||||
id: extract_version
|
||||
run: echo "::set-output name=version::${GITHUB_REF_NAME#infisical/}"
|
||||
run: echo "::set-output name=version::${GITHUB_REF_NAME}"
|
||||
- name: ☁️ Checkout source
|
||||
uses: actions/checkout@v3
|
||||
with:
|
||||
@@ -53,7 +55,7 @@ jobs:
|
||||
push: true
|
||||
context: .
|
||||
tags: |
|
||||
infisical/infisical:latest-postgres
|
||||
infisical/infisical:latest
|
||||
infisical/infisical:${{ steps.commit.outputs.short }}
|
||||
infisical/infisical:${{ steps.extract_version.outputs.version }}
|
||||
platforms: linux/amd64,linux/arm64
|
||||
@@ -69,7 +71,7 @@ jobs:
|
||||
steps:
|
||||
- name: Extract version from tag
|
||||
id: extract_version
|
||||
run: echo "::set-output name=version::${GITHUB_REF_NAME#infisical/}"
|
||||
run: echo "::set-output name=version::${GITHUB_REF_NAME}"
|
||||
- name: ☁️ Checkout source
|
||||
uses: actions/checkout@v3
|
||||
with:
|
||||
@@ -105,7 +107,7 @@ jobs:
|
||||
push: true
|
||||
context: .
|
||||
tags: |
|
||||
infisical/infisical-fips:latest-postgres
|
||||
infisical/infisical-fips:latest
|
||||
infisical/infisical-fips:${{ steps.commit.outputs.short }}
|
||||
infisical/infisical-fips:${{ steps.extract_version.outputs.version }}
|
||||
platforms: linux/amd64,linux/arm64
|
||||
|
@@ -44,10 +44,7 @@ jobs:
|
||||
|
||||
- name: Generate Helm Chart
|
||||
working-directory: k8-operator
|
||||
run: make helm
|
||||
|
||||
- name: Update Helm Chart Version
|
||||
run: ./k8-operator/scripts/update-version.sh ${{ steps.extract_version.outputs.version }}
|
||||
run: make helm VERSION=${{ steps.extract_version.outputs.version }}
|
||||
|
||||
- name: Debug - Check file changes
|
||||
run: |
|
||||
|
15
.github/workflows/run-backend-tests.yml
vendored
15
.github/workflows/run-backend-tests.yml
vendored
@@ -16,6 +16,16 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 15
|
||||
steps:
|
||||
|
||||
|
||||
- name: Free up disk space
|
||||
run: |
|
||||
sudo rm -rf /usr/share/dotnet
|
||||
sudo rm -rf /opt/ghc
|
||||
sudo rm -rf "/usr/local/share/boost"
|
||||
sudo rm -rf "$AGENT_TOOLSDIRECTORY"
|
||||
docker system prune -af
|
||||
|
||||
- name: ☁️ Checkout source
|
||||
uses: actions/checkout@v3
|
||||
- uses: KengoTODA/actions-setup-docker-compose@v1
|
||||
@@ -34,6 +44,8 @@ jobs:
|
||||
working-directory: backend
|
||||
- name: Start postgres and redis
|
||||
run: touch .env && docker compose -f docker-compose.dev.yml up -d db redis
|
||||
- name: Start Secret Rotation testing databases
|
||||
run: docker compose -f docker-compose.e2e-dbs.yml up -d --wait --wait-timeout 300
|
||||
- name: Run unit test
|
||||
run: npm run test:unit
|
||||
working-directory: backend
|
||||
@@ -41,6 +53,9 @@ jobs:
|
||||
run: npm run test:e2e
|
||||
working-directory: backend
|
||||
env:
|
||||
E2E_TEST_ORACLE_DB_19_HOST: ${{ secrets.E2E_TEST_ORACLE_DB_19_HOST }}
|
||||
E2E_TEST_ORACLE_DB_19_USERNAME: ${{ secrets.E2E_TEST_ORACLE_DB_19_USERNAME }}
|
||||
E2E_TEST_ORACLE_DB_19_PASSWORD: ${{ secrets.E2E_TEST_ORACLE_DB_19_PASSWORD }}
|
||||
REDIS_URL: redis://172.17.0.1:6379
|
||||
DB_CONNECTION_URI: postgres://infisical:infisical@172.17.0.1:5432/infisical?sslmode=disable
|
||||
AUTH_SECRET: something-random
|
||||
|
@@ -50,3 +50,4 @@ docs/integrations/app-connections/zabbix.mdx:generic-api-key:91
|
||||
docs/integrations/app-connections/bitbucket.mdx:generic-api-key:123
|
||||
docs/integrations/app-connections/railway.mdx:generic-api-key:156
|
||||
.github/workflows/validate-db-schemas.yml:generic-api-key:21
|
||||
k8-operator/config/samples/universalAuthIdentitySecret.yaml:generic-api-key:8
|
||||
|
@@ -1,34 +0,0 @@
|
||||
import { TQueueServiceFactory } from "@app/queue";
|
||||
|
||||
export const mockQueue = (): TQueueServiceFactory => {
|
||||
const queues: Record<string, unknown> = {};
|
||||
const workers: Record<string, unknown> = {};
|
||||
const job: Record<string, unknown> = {};
|
||||
const events: Record<string, unknown> = {};
|
||||
|
||||
return {
|
||||
queue: async (name, jobData) => {
|
||||
job[name] = jobData;
|
||||
},
|
||||
queuePg: async () => {},
|
||||
schedulePg: async () => {},
|
||||
initialize: async () => {},
|
||||
shutdown: async () => undefined,
|
||||
stopRepeatableJob: async () => true,
|
||||
start: (name, jobFn) => {
|
||||
queues[name] = jobFn;
|
||||
workers[name] = jobFn;
|
||||
},
|
||||
startPg: async () => {},
|
||||
listen: (name, event) => {
|
||||
events[name] = event;
|
||||
},
|
||||
getRepeatableJobs: async () => [],
|
||||
getDelayedJobs: async () => [],
|
||||
clearQueue: async () => {},
|
||||
stopJobById: async () => {},
|
||||
stopJobByIdPg: async () => {},
|
||||
stopRepeatableJobByJobId: async () => true,
|
||||
stopRepeatableJobByKey: async () => true
|
||||
};
|
||||
};
|
726
backend/e2e-test/routes/v3/secret-rotations.spec.ts
Normal file
726
backend/e2e-test/routes/v3/secret-rotations.spec.ts
Normal file
@@ -0,0 +1,726 @@
|
||||
/* eslint-disable no-promise-executor-return */
|
||||
/* eslint-disable no-await-in-loop */
|
||||
import knex from "knex";
|
||||
import { v4 as uuidv4 } from "uuid";
|
||||
|
||||
import { seedData1 } from "@app/db/seed-data";
|
||||
|
||||
enum SecretRotationType {
|
||||
OracleDb = "oracledb",
|
||||
MySQL = "mysql",
|
||||
Postgres = "postgres"
|
||||
}
|
||||
|
||||
type TGenericSqlCredentials = {
|
||||
host: string;
|
||||
port: number;
|
||||
username: string;
|
||||
password: string;
|
||||
database: string;
|
||||
};
|
||||
|
||||
type TSecretMapping = {
|
||||
username: string;
|
||||
password: string;
|
||||
};
|
||||
|
||||
type TDatabaseUserCredentials = {
|
||||
username: string;
|
||||
};
|
||||
|
||||
const formatSqlUsername = (username: string) => `${username}_${uuidv4().slice(0, 8).replace(/-/g, "").toUpperCase()}`;
|
||||
|
||||
const getSecretValue = async (secretKey: string) => {
|
||||
const passwordSecret = await testServer.inject({
|
||||
url: `/api/v3/secrets/raw/${secretKey}`,
|
||||
method: "GET",
|
||||
query: {
|
||||
workspaceId: seedData1.projectV3.id,
|
||||
environment: seedData1.environment.slug
|
||||
},
|
||||
headers: {
|
||||
authorization: `Bearer ${jwtAuthToken}`
|
||||
}
|
||||
});
|
||||
|
||||
expect(passwordSecret.statusCode).toBe(200);
|
||||
expect(passwordSecret.json().secret).toBeDefined();
|
||||
|
||||
const passwordSecretJson = JSON.parse(passwordSecret.payload);
|
||||
|
||||
return passwordSecretJson.secret.secretValue as string;
|
||||
};
|
||||
|
||||
const deleteSecretRotation = async (id: string, type: SecretRotationType) => {
|
||||
const res = await testServer.inject({
|
||||
method: "DELETE",
|
||||
query: {
|
||||
deleteSecrets: "true",
|
||||
revokeGeneratedCredentials: "true"
|
||||
},
|
||||
url: `/api/v2/secret-rotations/${type}-credentials/${id}`,
|
||||
headers: {
|
||||
authorization: `Bearer ${jwtAuthToken}`
|
||||
}
|
||||
});
|
||||
|
||||
expect(res.statusCode).toBe(200);
|
||||
};
|
||||
|
||||
const deleteAppConnection = async (id: string, type: SecretRotationType) => {
|
||||
const res = await testServer.inject({
|
||||
method: "DELETE",
|
||||
url: `/api/v1/app-connections/${type}/${id}`,
|
||||
headers: {
|
||||
authorization: `Bearer ${jwtAuthToken}`
|
||||
}
|
||||
});
|
||||
|
||||
expect(res.statusCode).toBe(200);
|
||||
};
|
||||
|
||||
const createOracleDBAppConnection = async (credentials: TGenericSqlCredentials) => {
|
||||
const createOracleDBAppConnectionReqBody = {
|
||||
credentials: {
|
||||
database: credentials.database,
|
||||
host: credentials.host,
|
||||
username: credentials.username,
|
||||
password: credentials.password,
|
||||
port: credentials.port,
|
||||
sslEnabled: true,
|
||||
sslRejectUnauthorized: true
|
||||
},
|
||||
name: `oracle-db-${uuidv4()}`,
|
||||
description: "Test OracleDB App Connection",
|
||||
gatewayId: null,
|
||||
isPlatformManagedCredentials: false,
|
||||
method: "username-and-password"
|
||||
};
|
||||
|
||||
const res = await testServer.inject({
|
||||
method: "POST",
|
||||
url: `/api/v1/app-connections/oracledb`,
|
||||
headers: {
|
||||
authorization: `Bearer ${jwtAuthToken}`
|
||||
},
|
||||
body: createOracleDBAppConnectionReqBody
|
||||
});
|
||||
|
||||
const json = JSON.parse(res.payload);
|
||||
|
||||
expect(res.statusCode).toBe(200);
|
||||
expect(json.appConnection).toBeDefined();
|
||||
|
||||
return json.appConnection.id as string;
|
||||
};
|
||||
|
||||
const createMySQLAppConnection = async (credentials: TGenericSqlCredentials) => {
|
||||
const createMySQLAppConnectionReqBody = {
|
||||
name: `mysql-test-${uuidv4()}`,
|
||||
description: "test-mysql",
|
||||
gatewayId: null,
|
||||
method: "username-and-password",
|
||||
credentials: {
|
||||
host: credentials.host,
|
||||
port: credentials.port,
|
||||
database: credentials.database,
|
||||
username: credentials.username,
|
||||
password: credentials.password,
|
||||
sslEnabled: false,
|
||||
sslRejectUnauthorized: true
|
||||
}
|
||||
};
|
||||
|
||||
const res = await testServer.inject({
|
||||
method: "POST",
|
||||
url: `/api/v1/app-connections/mysql`,
|
||||
headers: {
|
||||
authorization: `Bearer ${jwtAuthToken}`
|
||||
},
|
||||
body: createMySQLAppConnectionReqBody
|
||||
});
|
||||
|
||||
const json = JSON.parse(res.payload);
|
||||
|
||||
expect(res.statusCode).toBe(200);
|
||||
expect(json.appConnection).toBeDefined();
|
||||
|
||||
return json.appConnection.id as string;
|
||||
};
|
||||
|
||||
const createPostgresAppConnection = async (credentials: TGenericSqlCredentials) => {
|
||||
const createPostgresAppConnectionReqBody = {
|
||||
credentials: {
|
||||
host: credentials.host,
|
||||
port: credentials.port,
|
||||
database: credentials.database,
|
||||
username: credentials.username,
|
||||
password: credentials.password,
|
||||
sslEnabled: false,
|
||||
sslRejectUnauthorized: true
|
||||
},
|
||||
name: `postgres-test-${uuidv4()}`,
|
||||
description: "test-postgres",
|
||||
gatewayId: null,
|
||||
method: "username-and-password"
|
||||
};
|
||||
|
||||
const res = await testServer.inject({
|
||||
method: "POST",
|
||||
url: `/api/v1/app-connections/postgres`,
|
||||
headers: {
|
||||
authorization: `Bearer ${jwtAuthToken}`
|
||||
},
|
||||
body: createPostgresAppConnectionReqBody
|
||||
});
|
||||
|
||||
const json = JSON.parse(res.payload);
|
||||
|
||||
expect(res.statusCode).toBe(200);
|
||||
expect(json.appConnection).toBeDefined();
|
||||
|
||||
return json.appConnection.id as string;
|
||||
};
|
||||
|
||||
const createOracleInfisicalUsers = async (
|
||||
credentials: TGenericSqlCredentials,
|
||||
userCredentials: TDatabaseUserCredentials[]
|
||||
) => {
|
||||
const client = knex({
|
||||
client: "oracledb",
|
||||
connection: {
|
||||
database: credentials.database,
|
||||
port: credentials.port,
|
||||
host: credentials.host,
|
||||
user: credentials.username,
|
||||
password: credentials.password,
|
||||
connectionTimeoutMillis: 10000,
|
||||
ssl: {
|
||||
// @ts-expect-error - this is a valid property for the ssl object
|
||||
sslServerDNMatch: true
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
for await (const { username } of userCredentials) {
|
||||
// check if user exists, and if it does, don't create it
|
||||
const existingUser = await client.raw(`SELECT * FROM all_users WHERE username = '${username}'`);
|
||||
|
||||
if (!existingUser.length) {
|
||||
await client.raw(`CREATE USER ${username} IDENTIFIED BY "temporary_password"`);
|
||||
}
|
||||
await client.raw(`GRANT ALL PRIVILEGES TO ${username} WITH ADMIN OPTION`);
|
||||
}
|
||||
|
||||
await client.destroy();
|
||||
};
|
||||
|
||||
const createMySQLInfisicalUsers = async (
|
||||
credentials: TGenericSqlCredentials,
|
||||
userCredentials: TDatabaseUserCredentials[]
|
||||
) => {
|
||||
const client = knex({
|
||||
client: "mysql2",
|
||||
connection: {
|
||||
database: credentials.database,
|
||||
port: credentials.port,
|
||||
host: credentials.host,
|
||||
user: credentials.username,
|
||||
password: credentials.password,
|
||||
connectionTimeoutMillis: 10000
|
||||
}
|
||||
});
|
||||
|
||||
// Fix: Ensure root has GRANT OPTION privileges
|
||||
try {
|
||||
await client.raw("GRANT ALL PRIVILEGES ON *.* TO 'root'@'%' WITH GRANT OPTION;");
|
||||
await client.raw("FLUSH PRIVILEGES;");
|
||||
} catch (error) {
|
||||
// Ignore if already has privileges
|
||||
}
|
||||
|
||||
for await (const { username } of userCredentials) {
|
||||
// check if user exists, and if it does, dont create it
|
||||
|
||||
const existingUser = await client.raw(`SELECT * FROM mysql.user WHERE user = '${username}'`);
|
||||
|
||||
if (!existingUser[0].length) {
|
||||
await client.raw(`CREATE USER '${username}'@'%' IDENTIFIED BY 'temporary_password';`);
|
||||
}
|
||||
|
||||
await client.raw(`GRANT ALL PRIVILEGES ON \`${credentials.database}\`.* TO '${username}'@'%';`);
|
||||
await client.raw("FLUSH PRIVILEGES;");
|
||||
}
|
||||
|
||||
await client.destroy();
|
||||
};
|
||||
|
||||
const createPostgresInfisicalUsers = async (
|
||||
credentials: TGenericSqlCredentials,
|
||||
userCredentials: TDatabaseUserCredentials[]
|
||||
) => {
|
||||
const client = knex({
|
||||
client: "pg",
|
||||
connection: {
|
||||
database: credentials.database,
|
||||
port: credentials.port,
|
||||
host: credentials.host,
|
||||
user: credentials.username,
|
||||
password: credentials.password,
|
||||
connectionTimeoutMillis: 10000
|
||||
}
|
||||
});
|
||||
|
||||
for await (const { username } of userCredentials) {
|
||||
// check if user exists, and if it does, don't create it
|
||||
const existingUser = await client.raw("SELECT * FROM pg_catalog.pg_user WHERE usename = ?", [username]);
|
||||
|
||||
if (!existingUser.rows.length) {
|
||||
await client.raw(`CREATE USER "${username}" WITH PASSWORD 'temporary_password'`);
|
||||
}
|
||||
|
||||
await client.raw("GRANT ALL PRIVILEGES ON DATABASE ?? TO ??", [credentials.database, username]);
|
||||
}
|
||||
|
||||
await client.destroy();
|
||||
};
|
||||
|
||||
const createOracleDBSecretRotation = async (
|
||||
appConnectionId: string,
|
||||
credentials: TGenericSqlCredentials,
|
||||
userCredentials: TDatabaseUserCredentials[],
|
||||
secretMapping: TSecretMapping
|
||||
) => {
|
||||
const now = new Date();
|
||||
const rotationTime = new Date(now.getTime() - 2 * 60 * 1000); // 2 minutes ago
|
||||
|
||||
await createOracleInfisicalUsers(credentials, userCredentials);
|
||||
|
||||
const createOracleDBSecretRotationReqBody = {
|
||||
parameters: userCredentials.reduce(
|
||||
(acc, user, index) => {
|
||||
acc[`username${index + 1}`] = user.username;
|
||||
return acc;
|
||||
},
|
||||
{} as Record<string, string>
|
||||
),
|
||||
secretsMapping: {
|
||||
username: secretMapping.username,
|
||||
password: secretMapping.password
|
||||
},
|
||||
name: `test-oracle-${uuidv4()}`,
|
||||
description: "Test OracleDB Secret Rotation",
|
||||
secretPath: "/",
|
||||
isAutoRotationEnabled: true,
|
||||
rotationInterval: 5, // 5 seconds for testing
|
||||
rotateAtUtc: {
|
||||
hours: rotationTime.getUTCHours(),
|
||||
minutes: rotationTime.getUTCMinutes()
|
||||
},
|
||||
connectionId: appConnectionId,
|
||||
environment: seedData1.environment.slug,
|
||||
projectId: seedData1.projectV3.id
|
||||
};
|
||||
|
||||
const res = await testServer.inject({
|
||||
method: "POST",
|
||||
url: `/api/v2/secret-rotations/oracledb-credentials`,
|
||||
headers: {
|
||||
authorization: `Bearer ${jwtAuthToken}`
|
||||
},
|
||||
body: createOracleDBSecretRotationReqBody
|
||||
});
|
||||
|
||||
expect(res.statusCode).toBe(200);
|
||||
expect(res.json().secretRotation).toBeDefined();
|
||||
|
||||
return res;
|
||||
};
|
||||
|
||||
const createMySQLSecretRotation = async (
|
||||
appConnectionId: string,
|
||||
credentials: TGenericSqlCredentials,
|
||||
userCredentials: TDatabaseUserCredentials[],
|
||||
secretMapping: TSecretMapping
|
||||
) => {
|
||||
const now = new Date();
|
||||
const rotationTime = new Date(now.getTime() - 2 * 60 * 1000); // 2 minutes ago
|
||||
|
||||
await createMySQLInfisicalUsers(credentials, userCredentials);
|
||||
|
||||
const createMySQLSecretRotationReqBody = {
|
||||
parameters: userCredentials.reduce(
|
||||
(acc, user, index) => {
|
||||
acc[`username${index + 1}`] = user.username;
|
||||
return acc;
|
||||
},
|
||||
{} as Record<string, string>
|
||||
),
|
||||
secretsMapping: {
|
||||
username: secretMapping.username,
|
||||
password: secretMapping.password
|
||||
},
|
||||
name: `test-mysql-rotation-${uuidv4()}`,
|
||||
description: "Test MySQL Secret Rotation",
|
||||
secretPath: "/",
|
||||
isAutoRotationEnabled: true,
|
||||
rotationInterval: 5,
|
||||
rotateAtUtc: {
|
||||
hours: rotationTime.getUTCHours(),
|
||||
minutes: rotationTime.getUTCMinutes()
|
||||
},
|
||||
connectionId: appConnectionId,
|
||||
environment: seedData1.environment.slug,
|
||||
projectId: seedData1.projectV3.id
|
||||
};
|
||||
|
||||
const res = await testServer.inject({
|
||||
method: "POST",
|
||||
url: `/api/v2/secret-rotations/mysql-credentials`,
|
||||
headers: {
|
||||
authorization: `Bearer ${jwtAuthToken}`
|
||||
},
|
||||
body: createMySQLSecretRotationReqBody
|
||||
});
|
||||
|
||||
expect(res.statusCode).toBe(200);
|
||||
expect(res.json().secretRotation).toBeDefined();
|
||||
|
||||
return res;
|
||||
};
|
||||
|
||||
const createPostgresSecretRotation = async (
|
||||
appConnectionId: string,
|
||||
credentials: TGenericSqlCredentials,
|
||||
userCredentials: TDatabaseUserCredentials[],
|
||||
secretMapping: TSecretMapping
|
||||
) => {
|
||||
const now = new Date();
|
||||
const rotationTime = new Date(now.getTime() - 2 * 60 * 1000); // 2 minutes ago
|
||||
|
||||
await createPostgresInfisicalUsers(credentials, userCredentials);
|
||||
|
||||
const createPostgresSecretRotationReqBody = {
|
||||
parameters: userCredentials.reduce(
|
||||
(acc, user, index) => {
|
||||
acc[`username${index + 1}`] = user.username;
|
||||
return acc;
|
||||
},
|
||||
{} as Record<string, string>
|
||||
),
|
||||
secretsMapping: {
|
||||
username: secretMapping.username,
|
||||
password: secretMapping.password
|
||||
},
|
||||
name: `test-postgres-rotation-${uuidv4()}`,
|
||||
description: "Test Postgres Secret Rotation",
|
||||
secretPath: "/",
|
||||
isAutoRotationEnabled: true,
|
||||
rotationInterval: 5,
|
||||
rotateAtUtc: {
|
||||
hours: rotationTime.getUTCHours(),
|
||||
minutes: rotationTime.getUTCMinutes()
|
||||
},
|
||||
connectionId: appConnectionId,
|
||||
environment: seedData1.environment.slug,
|
||||
projectId: seedData1.projectV3.id
|
||||
};
|
||||
|
||||
const res = await testServer.inject({
|
||||
method: "POST",
|
||||
url: `/api/v2/secret-rotations/postgres-credentials`,
|
||||
headers: {
|
||||
authorization: `Bearer ${jwtAuthToken}`
|
||||
},
|
||||
body: createPostgresSecretRotationReqBody
|
||||
});
|
||||
|
||||
expect(res.statusCode).toBe(200);
|
||||
expect(res.json().secretRotation).toBeDefined();
|
||||
|
||||
return res;
|
||||
};
|
||||
|
||||
describe("Secret Rotations", async () => {
|
||||
const testCases = [
|
||||
{
|
||||
type: SecretRotationType.MySQL,
|
||||
name: "MySQL (8.4.6) Secret Rotation",
|
||||
dbCredentials: {
|
||||
database: "mysql-test",
|
||||
host: "127.0.0.1",
|
||||
username: "root",
|
||||
password: "mysql-test",
|
||||
port: 3306
|
||||
},
|
||||
secretMapping: {
|
||||
username: formatSqlUsername("MYSQL_USERNAME"),
|
||||
password: formatSqlUsername("MYSQL_PASSWORD")
|
||||
},
|
||||
userCredentials: [
|
||||
{
|
||||
username: formatSqlUsername("MYSQL_USER_1")
|
||||
},
|
||||
{
|
||||
username: formatSqlUsername("MYSQL_USER_2")
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
type: SecretRotationType.MySQL,
|
||||
name: "MySQL (8.0.29) Secret Rotation",
|
||||
dbCredentials: {
|
||||
database: "mysql-test",
|
||||
host: "127.0.0.1",
|
||||
username: "root",
|
||||
password: "mysql-test",
|
||||
port: 3307
|
||||
},
|
||||
secretMapping: {
|
||||
username: formatSqlUsername("MYSQL_USERNAME"),
|
||||
password: formatSqlUsername("MYSQL_PASSWORD")
|
||||
},
|
||||
userCredentials: [
|
||||
{
|
||||
username: formatSqlUsername("MYSQL_USER_1")
|
||||
},
|
||||
{
|
||||
username: formatSqlUsername("MYSQL_USER_2")
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
type: SecretRotationType.MySQL,
|
||||
name: "MySQL (5.7.31) Secret Rotation",
|
||||
dbCredentials: {
|
||||
database: "mysql-test",
|
||||
host: "127.0.0.1",
|
||||
username: "root",
|
||||
password: "mysql-test",
|
||||
port: 3308
|
||||
},
|
||||
secretMapping: {
|
||||
username: formatSqlUsername("MYSQL_USERNAME"),
|
||||
password: formatSqlUsername("MYSQL_PASSWORD")
|
||||
},
|
||||
userCredentials: [
|
||||
{
|
||||
username: formatSqlUsername("MYSQL_USER_1")
|
||||
},
|
||||
{
|
||||
username: formatSqlUsername("MYSQL_USER_2")
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
type: SecretRotationType.OracleDb,
|
||||
name: "OracleDB (23.8) Secret Rotation",
|
||||
dbCredentials: {
|
||||
database: "FREEPDB1",
|
||||
host: "127.0.0.1",
|
||||
username: "system",
|
||||
password: "pdb-password",
|
||||
port: 1521
|
||||
},
|
||||
secretMapping: {
|
||||
username: formatSqlUsername("ORACLEDB_USERNAME"),
|
||||
password: formatSqlUsername("ORACLEDB_PASSWORD")
|
||||
},
|
||||
userCredentials: [
|
||||
{
|
||||
username: formatSqlUsername("INFISICAL_USER_1")
|
||||
},
|
||||
{
|
||||
username: formatSqlUsername("INFISICAL_USER_2")
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
type: SecretRotationType.OracleDb,
|
||||
name: "OracleDB (19.3) Secret Rotation",
|
||||
skippable: true,
|
||||
dbCredentials: {
|
||||
password: process.env.E2E_TEST_ORACLE_DB_19_PASSWORD!,
|
||||
host: process.env.E2E_TEST_ORACLE_DB_19_HOST!,
|
||||
username: process.env.E2E_TEST_ORACLE_DB_19_USERNAME!,
|
||||
port: 1521,
|
||||
database: "ORCLPDB1"
|
||||
},
|
||||
secretMapping: {
|
||||
username: formatSqlUsername("ORACLEDB_USERNAME"),
|
||||
password: formatSqlUsername("ORACLEDB_PASSWORD")
|
||||
},
|
||||
userCredentials: [
|
||||
{
|
||||
username: formatSqlUsername("INFISICAL_USER_1")
|
||||
},
|
||||
{
|
||||
username: formatSqlUsername("INFISICAL_USER_2")
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
type: SecretRotationType.Postgres,
|
||||
name: "Postgres (17) Secret Rotation",
|
||||
dbCredentials: {
|
||||
database: "postgres-test",
|
||||
host: "127.0.0.1",
|
||||
username: "postgres-test",
|
||||
password: "postgres-test",
|
||||
port: 5433
|
||||
},
|
||||
secretMapping: {
|
||||
username: formatSqlUsername("POSTGRES_USERNAME"),
|
||||
password: formatSqlUsername("POSTGRES_PASSWORD")
|
||||
},
|
||||
userCredentials: [
|
||||
{
|
||||
username: formatSqlUsername("INFISICAL_USER_1")
|
||||
},
|
||||
{
|
||||
username: formatSqlUsername("INFISICAL_USER_2")
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
type: SecretRotationType.Postgres,
|
||||
name: "Postgres (16) Secret Rotation",
|
||||
dbCredentials: {
|
||||
database: "postgres-test",
|
||||
host: "127.0.0.1",
|
||||
username: "postgres-test",
|
||||
password: "postgres-test",
|
||||
port: 5434
|
||||
},
|
||||
secretMapping: {
|
||||
username: formatSqlUsername("POSTGRES_USERNAME"),
|
||||
password: formatSqlUsername("POSTGRES_PASSWORD")
|
||||
},
|
||||
userCredentials: [
|
||||
{
|
||||
username: formatSqlUsername("INFISICAL_USER_1")
|
||||
},
|
||||
{
|
||||
username: formatSqlUsername("INFISICAL_USER_2")
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
type: SecretRotationType.Postgres,
|
||||
name: "Postgres (10.12) Secret Rotation",
|
||||
dbCredentials: {
|
||||
database: "postgres-test",
|
||||
host: "127.0.0.1",
|
||||
username: "postgres-test",
|
||||
password: "postgres-test",
|
||||
port: 5435
|
||||
},
|
||||
secretMapping: {
|
||||
username: formatSqlUsername("POSTGRES_USERNAME"),
|
||||
password: formatSqlUsername("POSTGRES_PASSWORD")
|
||||
},
|
||||
userCredentials: [
|
||||
{
|
||||
username: formatSqlUsername("INFISICAL_USER_1")
|
||||
},
|
||||
{
|
||||
username: formatSqlUsername("INFISICAL_USER_2")
|
||||
}
|
||||
]
|
||||
}
|
||||
] as {
|
||||
skippable?: boolean;
|
||||
type: SecretRotationType;
|
||||
name: string;
|
||||
dbCredentials: TGenericSqlCredentials;
|
||||
secretMapping: TSecretMapping;
|
||||
userCredentials: TDatabaseUserCredentials[];
|
||||
}[];
|
||||
|
||||
const createAppConnectionMap = {
|
||||
[SecretRotationType.OracleDb]: createOracleDBAppConnection,
|
||||
[SecretRotationType.MySQL]: createMySQLAppConnection,
|
||||
[SecretRotationType.Postgres]: createPostgresAppConnection
|
||||
};
|
||||
|
||||
const createRotationMap = {
|
||||
[SecretRotationType.OracleDb]: createOracleDBSecretRotation,
|
||||
[SecretRotationType.MySQL]: createMySQLSecretRotation,
|
||||
[SecretRotationType.Postgres]: createPostgresSecretRotation
|
||||
};
|
||||
|
||||
const appConnectionIds: { id: string; type: SecretRotationType }[] = [];
|
||||
const secretRotationIds: { id: string; type: SecretRotationType }[] = [];
|
||||
|
||||
afterAll(async () => {
|
||||
for (const { id, type } of secretRotationIds) {
|
||||
await deleteSecretRotation(id, type);
|
||||
}
|
||||
|
||||
for (const { id, type } of appConnectionIds) {
|
||||
await deleteAppConnection(id, type);
|
||||
}
|
||||
});
|
||||
|
||||
testCases.forEach(({ skippable, dbCredentials, secretMapping, userCredentials, type, name }) => {
|
||||
const shouldSkip = () => {
|
||||
if (skippable) {
|
||||
if (type === SecretRotationType.OracleDb) {
|
||||
if (!process.env.E2E_TEST_ORACLE_DB_19_HOST) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
};
|
||||
|
||||
if (shouldSkip()) {
|
||||
test.skip(`Skipping Secret Rotation for ${type} (${name}) because E2E_TEST_ORACLE_DB_19_HOST is not set`);
|
||||
} else {
|
||||
test.concurrent(
|
||||
`Create secret rotation for ${name}`,
|
||||
async () => {
|
||||
const appConnectionId = await createAppConnectionMap[type](dbCredentials);
|
||||
|
||||
if (appConnectionId) {
|
||||
appConnectionIds.push({ id: appConnectionId, type });
|
||||
}
|
||||
|
||||
const res = await createRotationMap[type](appConnectionId, dbCredentials, userCredentials, secretMapping);
|
||||
|
||||
const resJson = JSON.parse(res.payload);
|
||||
|
||||
if (resJson.secretRotation) {
|
||||
secretRotationIds.push({ id: resJson.secretRotation.id, type });
|
||||
}
|
||||
|
||||
const startSecretValue = await getSecretValue(secretMapping.password);
|
||||
expect(startSecretValue).toBeDefined();
|
||||
|
||||
let attempts = 0;
|
||||
while (attempts < 60) {
|
||||
const currentSecretValue = await getSecretValue(secretMapping.password);
|
||||
|
||||
if (currentSecretValue !== startSecretValue) {
|
||||
break;
|
||||
}
|
||||
|
||||
attempts += 1;
|
||||
await new Promise((resolve) => setTimeout(resolve, 2_500));
|
||||
}
|
||||
|
||||
if (attempts >= 60) {
|
||||
throw new Error("Secret rotation failed to rotate after 60 attempts");
|
||||
}
|
||||
|
||||
const finalSecretValue = await getSecretValue(secretMapping.password);
|
||||
expect(finalSecretValue).not.toBe(startSecretValue);
|
||||
},
|
||||
{
|
||||
timeout: 300_000
|
||||
}
|
||||
);
|
||||
}
|
||||
});
|
||||
});
|
@@ -18,6 +18,7 @@ import { keyStoreFactory } from "@app/keystore/keystore";
|
||||
import { initializeHsmModule } from "@app/ee/services/hsm/hsm-fns";
|
||||
import { buildRedisFromConfig } from "@app/lib/config/redis";
|
||||
import { superAdminDALFactory } from "@app/services/super-admin/super-admin-dal";
|
||||
import { bootstrapCheck } from "@app/server/boot-strap-check";
|
||||
|
||||
dotenv.config({ path: path.join(__dirname, "../../.env.test"), debug: true });
|
||||
export default {
|
||||
@@ -63,6 +64,8 @@ export default {
|
||||
const queue = queueServiceFactory(envCfg, { dbConnectionUrl: envCfg.DB_CONNECTION_URI });
|
||||
const keyStore = keyStoreFactory(envCfg);
|
||||
|
||||
await queue.initialize();
|
||||
|
||||
const hsmModule = initializeHsmModule(envCfg);
|
||||
hsmModule.initialize();
|
||||
|
||||
@@ -78,9 +81,13 @@ export default {
|
||||
envConfig: envCfg
|
||||
});
|
||||
|
||||
await bootstrapCheck({ db });
|
||||
|
||||
// @ts-expect-error type
|
||||
globalThis.testServer = server;
|
||||
// @ts-expect-error type
|
||||
globalThis.testQueue = queue;
|
||||
// @ts-expect-error type
|
||||
globalThis.testSuperAdminDAL = superAdminDAL;
|
||||
// @ts-expect-error type
|
||||
globalThis.jwtAuthToken = crypto.jwt().sign(
|
||||
@@ -105,6 +112,8 @@ export default {
|
||||
// custom setup
|
||||
return {
|
||||
async teardown() {
|
||||
// @ts-expect-error type
|
||||
await globalThis.testQueue.shutdown();
|
||||
// @ts-expect-error type
|
||||
await globalThis.testServer.close();
|
||||
// @ts-expect-error type
|
||||
@@ -112,7 +121,9 @@ export default {
|
||||
// @ts-expect-error type
|
||||
delete globalThis.testSuperAdminDAL;
|
||||
// @ts-expect-error type
|
||||
delete globalThis.jwtToken;
|
||||
delete globalThis.jwtAuthToken;
|
||||
// @ts-expect-error type
|
||||
delete globalThis.testQueue;
|
||||
// called after all tests with this env have been run
|
||||
await db.migrate.rollback(
|
||||
{
|
||||
|
479
backend/package-lock.json
generated
479
backend/package-lock.json
generated
@@ -38,6 +38,7 @@
|
||||
"@octokit/core": "^5.2.1",
|
||||
"@octokit/plugin-paginate-graphql": "^4.0.1",
|
||||
"@octokit/plugin-retry": "^5.0.5",
|
||||
"@octokit/request": "8.4.1",
|
||||
"@octokit/rest": "^20.0.2",
|
||||
"@octokit/webhooks-types": "^7.3.1",
|
||||
"@octopusdeploy/api-client": "^3.4.1",
|
||||
@@ -62,6 +63,7 @@
|
||||
"argon2": "^0.31.2",
|
||||
"aws-sdk": "^2.1553.0",
|
||||
"axios": "^1.11.0",
|
||||
"axios-ntlm": "^1.4.4",
|
||||
"axios-retry": "^4.0.0",
|
||||
"bcrypt": "^5.1.1",
|
||||
"botbuilder": "^4.23.2",
|
||||
@@ -9777,18 +9779,6 @@
|
||||
"node": ">= 18"
|
||||
}
|
||||
},
|
||||
"node_modules/@octokit/auth-app/node_modules/@octokit/endpoint": {
|
||||
"version": "10.1.1",
|
||||
"resolved": "https://registry.npmjs.org/@octokit/endpoint/-/endpoint-10.1.1.tgz",
|
||||
"integrity": "sha512-JYjh5rMOwXMJyUpj028cu0Gbp7qe/ihxfJMLc8VZBMMqSwLgOxDI1911gV4Enl1QSavAQNJcwmwBF9M0VvLh6Q==",
|
||||
"dependencies": {
|
||||
"@octokit/types": "^13.0.0",
|
||||
"universal-user-agent": "^7.0.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 18"
|
||||
}
|
||||
},
|
||||
"node_modules/@octokit/auth-app/node_modules/@octokit/openapi-types": {
|
||||
"version": "22.2.0",
|
||||
"resolved": "https://registry.npmjs.org/@octokit/openapi-types/-/openapi-types-22.2.0.tgz",
|
||||
@@ -9835,11 +9825,6 @@
|
||||
"node": "14 || >=16.14"
|
||||
}
|
||||
},
|
||||
"node_modules/@octokit/auth-app/node_modules/universal-user-agent": {
|
||||
"version": "7.0.2",
|
||||
"resolved": "https://registry.npmjs.org/universal-user-agent/-/universal-user-agent-7.0.2.tgz",
|
||||
"integrity": "sha512-0JCqzSKnStlRRQfCdowvqy3cy0Dvtlb8xecj/H8JFZuCze4rwjPZQOgvFvn0Ws/usCHQFGpyr+pB9adaGwXn4Q=="
|
||||
},
|
||||
"node_modules/@octokit/auth-oauth-app": {
|
||||
"version": "8.1.1",
|
||||
"resolved": "https://registry.npmjs.org/@octokit/auth-oauth-app/-/auth-oauth-app-8.1.1.tgz",
|
||||
@@ -9855,18 +9840,6 @@
|
||||
"node": ">= 18"
|
||||
}
|
||||
},
|
||||
"node_modules/@octokit/auth-oauth-app/node_modules/@octokit/endpoint": {
|
||||
"version": "10.1.1",
|
||||
"resolved": "https://registry.npmjs.org/@octokit/endpoint/-/endpoint-10.1.1.tgz",
|
||||
"integrity": "sha512-JYjh5rMOwXMJyUpj028cu0Gbp7qe/ihxfJMLc8VZBMMqSwLgOxDI1911gV4Enl1QSavAQNJcwmwBF9M0VvLh6Q==",
|
||||
"dependencies": {
|
||||
"@octokit/types": "^13.0.0",
|
||||
"universal-user-agent": "^7.0.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 18"
|
||||
}
|
||||
},
|
||||
"node_modules/@octokit/auth-oauth-app/node_modules/@octokit/openapi-types": {
|
||||
"version": "22.2.0",
|
||||
"resolved": "https://registry.npmjs.org/@octokit/openapi-types/-/openapi-types-22.2.0.tgz",
|
||||
@@ -9905,11 +9878,6 @@
|
||||
"@octokit/openapi-types": "^22.2.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@octokit/auth-oauth-app/node_modules/universal-user-agent": {
|
||||
"version": "7.0.2",
|
||||
"resolved": "https://registry.npmjs.org/universal-user-agent/-/universal-user-agent-7.0.2.tgz",
|
||||
"integrity": "sha512-0JCqzSKnStlRRQfCdowvqy3cy0Dvtlb8xecj/H8JFZuCze4rwjPZQOgvFvn0Ws/usCHQFGpyr+pB9adaGwXn4Q=="
|
||||
},
|
||||
"node_modules/@octokit/auth-oauth-device": {
|
||||
"version": "7.1.1",
|
||||
"resolved": "https://registry.npmjs.org/@octokit/auth-oauth-device/-/auth-oauth-device-7.1.1.tgz",
|
||||
@@ -9924,18 +9892,6 @@
|
||||
"node": ">= 18"
|
||||
}
|
||||
},
|
||||
"node_modules/@octokit/auth-oauth-device/node_modules/@octokit/endpoint": {
|
||||
"version": "10.1.1",
|
||||
"resolved": "https://registry.npmjs.org/@octokit/endpoint/-/endpoint-10.1.1.tgz",
|
||||
"integrity": "sha512-JYjh5rMOwXMJyUpj028cu0Gbp7qe/ihxfJMLc8VZBMMqSwLgOxDI1911gV4Enl1QSavAQNJcwmwBF9M0VvLh6Q==",
|
||||
"dependencies": {
|
||||
"@octokit/types": "^13.0.0",
|
||||
"universal-user-agent": "^7.0.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 18"
|
||||
}
|
||||
},
|
||||
"node_modules/@octokit/auth-oauth-device/node_modules/@octokit/openapi-types": {
|
||||
"version": "22.2.0",
|
||||
"resolved": "https://registry.npmjs.org/@octokit/openapi-types/-/openapi-types-22.2.0.tgz",
|
||||
@@ -9974,11 +9930,6 @@
|
||||
"@octokit/openapi-types": "^22.2.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@octokit/auth-oauth-device/node_modules/universal-user-agent": {
|
||||
"version": "7.0.2",
|
||||
"resolved": "https://registry.npmjs.org/universal-user-agent/-/universal-user-agent-7.0.2.tgz",
|
||||
"integrity": "sha512-0JCqzSKnStlRRQfCdowvqy3cy0Dvtlb8xecj/H8JFZuCze4rwjPZQOgvFvn0Ws/usCHQFGpyr+pB9adaGwXn4Q=="
|
||||
},
|
||||
"node_modules/@octokit/auth-oauth-user": {
|
||||
"version": "5.1.1",
|
||||
"resolved": "https://registry.npmjs.org/@octokit/auth-oauth-user/-/auth-oauth-user-5.1.1.tgz",
|
||||
@@ -9994,18 +9945,6 @@
|
||||
"node": ">= 18"
|
||||
}
|
||||
},
|
||||
"node_modules/@octokit/auth-oauth-user/node_modules/@octokit/endpoint": {
|
||||
"version": "10.1.1",
|
||||
"resolved": "https://registry.npmjs.org/@octokit/endpoint/-/endpoint-10.1.1.tgz",
|
||||
"integrity": "sha512-JYjh5rMOwXMJyUpj028cu0Gbp7qe/ihxfJMLc8VZBMMqSwLgOxDI1911gV4Enl1QSavAQNJcwmwBF9M0VvLh6Q==",
|
||||
"dependencies": {
|
||||
"@octokit/types": "^13.0.0",
|
||||
"universal-user-agent": "^7.0.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 18"
|
||||
}
|
||||
},
|
||||
"node_modules/@octokit/auth-oauth-user/node_modules/@octokit/openapi-types": {
|
||||
"version": "22.2.0",
|
||||
"resolved": "https://registry.npmjs.org/@octokit/openapi-types/-/openapi-types-22.2.0.tgz",
|
||||
@@ -10044,11 +9983,6 @@
|
||||
"@octokit/openapi-types": "^22.2.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@octokit/auth-oauth-user/node_modules/universal-user-agent": {
|
||||
"version": "7.0.2",
|
||||
"resolved": "https://registry.npmjs.org/universal-user-agent/-/universal-user-agent-7.0.2.tgz",
|
||||
"integrity": "sha512-0JCqzSKnStlRRQfCdowvqy3cy0Dvtlb8xecj/H8JFZuCze4rwjPZQOgvFvn0Ws/usCHQFGpyr+pB9adaGwXn4Q=="
|
||||
},
|
||||
"node_modules/@octokit/auth-token": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/@octokit/auth-token/-/auth-token-4.0.0.tgz",
|
||||
@@ -10102,32 +10036,38 @@
|
||||
"@octokit/openapi-types": "^24.2.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@octokit/core/node_modules/universal-user-agent": {
|
||||
"version": "6.0.1",
|
||||
"resolved": "https://registry.npmjs.org/universal-user-agent/-/universal-user-agent-6.0.1.tgz",
|
||||
"integrity": "sha512-yCzhz6FN2wU1NiiQRogkTQszlQSlpWaw8SvVegAc+bDxbzHgh1vX8uIe8OYyMH6DwH+sdTJsgMl36+mSMdRJIQ==",
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/@octokit/endpoint": {
|
||||
"version": "9.0.6",
|
||||
"resolved": "https://registry.npmjs.org/@octokit/endpoint/-/endpoint-9.0.6.tgz",
|
||||
"integrity": "sha512-H1fNTMA57HbkFESSt3Y9+FBICv+0jFceJFPWDePYlR/iMGrwM5ph+Dd4XRQs+8X+PUFURLQgX9ChPfhJ/1uNQw==",
|
||||
"version": "10.1.4",
|
||||
"resolved": "https://registry.npmjs.org/@octokit/endpoint/-/endpoint-10.1.4.tgz",
|
||||
"integrity": "sha512-OlYOlZIsfEVZm5HCSR8aSg02T2lbUWOsCQoPKfTXJwDzcHQBrVBGdGXb89dv2Kw2ToZaRtudp8O3ZIYoaOjKlA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@octokit/types": "^13.1.0",
|
||||
"universal-user-agent": "^6.0.0"
|
||||
"@octokit/types": "^14.0.0",
|
||||
"universal-user-agent": "^7.0.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 18"
|
||||
}
|
||||
},
|
||||
"node_modules/@octokit/endpoint/node_modules/@octokit/openapi-types": {
|
||||
"version": "24.2.0",
|
||||
"resolved": "https://registry.npmjs.org/@octokit/openapi-types/-/openapi-types-24.2.0.tgz",
|
||||
"integrity": "sha512-9sIH3nSUttelJSXUrmGzl7QUBFul0/mB8HRYl3fOlgHbIWG+WnYDXU3v/2zMtAvuzZ/ed00Ei6on975FhBfzrg==",
|
||||
"version": "25.1.0",
|
||||
"resolved": "https://registry.npmjs.org/@octokit/openapi-types/-/openapi-types-25.1.0.tgz",
|
||||
"integrity": "sha512-idsIggNXUKkk0+BExUn1dQ92sfysJrje03Q0bv0e+KPLrvyqZF8MnBpFz8UNfYDwB3Ie7Z0TByjWfzxt7vseaA==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@octokit/endpoint/node_modules/@octokit/types": {
|
||||
"version": "13.10.0",
|
||||
"resolved": "https://registry.npmjs.org/@octokit/types/-/types-13.10.0.tgz",
|
||||
"integrity": "sha512-ifLaO34EbbPj0Xgro4G5lP5asESjwHracYJvVaPIyXMuiuXLlhic3S47cBdTb+jfODkTE5YtGCLt3Ay3+J97sA==",
|
||||
"version": "14.1.0",
|
||||
"resolved": "https://registry.npmjs.org/@octokit/types/-/types-14.1.0.tgz",
|
||||
"integrity": "sha512-1y6DgTy8Jomcpu33N+p5w58l6xyt55Ar2I91RPiIA0xCJBXyUAhXCcmZaDWSANiha7R9a6qJJ2CRomGPZ6f46g==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@octokit/openapi-types": "^24.2.0"
|
||||
"@octokit/openapi-types": "^25.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@octokit/graphql": {
|
||||
@@ -10159,6 +10099,12 @@
|
||||
"@octokit/openapi-types": "^24.2.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@octokit/graphql/node_modules/universal-user-agent": {
|
||||
"version": "6.0.1",
|
||||
"resolved": "https://registry.npmjs.org/universal-user-agent/-/universal-user-agent-6.0.1.tgz",
|
||||
"integrity": "sha512-yCzhz6FN2wU1NiiQRogkTQszlQSlpWaw8SvVegAc+bDxbzHgh1vX8uIe8OYyMH6DwH+sdTJsgMl36+mSMdRJIQ==",
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/@octokit/oauth-authorization-url": {
|
||||
"version": "7.1.1",
|
||||
"resolved": "https://registry.npmjs.org/@octokit/oauth-authorization-url/-/oauth-authorization-url-7.1.1.tgz",
|
||||
@@ -10181,18 +10127,6 @@
|
||||
"node": ">= 18"
|
||||
}
|
||||
},
|
||||
"node_modules/@octokit/oauth-methods/node_modules/@octokit/endpoint": {
|
||||
"version": "10.1.1",
|
||||
"resolved": "https://registry.npmjs.org/@octokit/endpoint/-/endpoint-10.1.1.tgz",
|
||||
"integrity": "sha512-JYjh5rMOwXMJyUpj028cu0Gbp7qe/ihxfJMLc8VZBMMqSwLgOxDI1911gV4Enl1QSavAQNJcwmwBF9M0VvLh6Q==",
|
||||
"dependencies": {
|
||||
"@octokit/types": "^13.0.0",
|
||||
"universal-user-agent": "^7.0.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 18"
|
||||
}
|
||||
},
|
||||
"node_modules/@octokit/oauth-methods/node_modules/@octokit/openapi-types": {
|
||||
"version": "22.2.0",
|
||||
"resolved": "https://registry.npmjs.org/@octokit/openapi-types/-/openapi-types-22.2.0.tgz",
|
||||
@@ -10231,11 +10165,6 @@
|
||||
"@octokit/openapi-types": "^22.2.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@octokit/oauth-methods/node_modules/universal-user-agent": {
|
||||
"version": "7.0.2",
|
||||
"resolved": "https://registry.npmjs.org/universal-user-agent/-/universal-user-agent-7.0.2.tgz",
|
||||
"integrity": "sha512-0JCqzSKnStlRRQfCdowvqy3cy0Dvtlb8xecj/H8JFZuCze4rwjPZQOgvFvn0Ws/usCHQFGpyr+pB9adaGwXn4Q=="
|
||||
},
|
||||
"node_modules/@octokit/openapi-types": {
|
||||
"version": "19.1.0",
|
||||
"resolved": "https://registry.npmjs.org/@octokit/openapi-types/-/openapi-types-19.1.0.tgz",
|
||||
@@ -10376,31 +10305,54 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@octokit/request-error/node_modules/@octokit/openapi-types": {
|
||||
"version": "22.2.0",
|
||||
"resolved": "https://registry.npmjs.org/@octokit/openapi-types/-/openapi-types-22.2.0.tgz",
|
||||
"integrity": "sha512-QBhVjcUa9W7Wwhm6DBFu6ZZ+1/t/oYxqc2tp81Pi41YNuJinbFRx8B133qVOrAaBbF7D/m0Et6f9/pZt9Rc+tg=="
|
||||
"version": "24.2.0",
|
||||
"resolved": "https://registry.npmjs.org/@octokit/openapi-types/-/openapi-types-24.2.0.tgz",
|
||||
"integrity": "sha512-9sIH3nSUttelJSXUrmGzl7QUBFul0/mB8HRYl3fOlgHbIWG+WnYDXU3v/2zMtAvuzZ/ed00Ei6on975FhBfzrg==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@octokit/request-error/node_modules/@octokit/types": {
|
||||
"version": "13.6.1",
|
||||
"resolved": "https://registry.npmjs.org/@octokit/types/-/types-13.6.1.tgz",
|
||||
"integrity": "sha512-PHZE9Z+kWXb23Ndik8MKPirBPziOc0D2/3KH1P+6jK5nGWe96kadZuE4jev2/Jq7FvIfTlT2Ltg8Fv2x1v0a5g==",
|
||||
"version": "13.10.0",
|
||||
"resolved": "https://registry.npmjs.org/@octokit/types/-/types-13.10.0.tgz",
|
||||
"integrity": "sha512-ifLaO34EbbPj0Xgro4G5lP5asESjwHracYJvVaPIyXMuiuXLlhic3S47cBdTb+jfODkTE5YtGCLt3Ay3+J97sA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@octokit/openapi-types": "^22.2.0"
|
||||
"@octokit/openapi-types": "^24.2.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@octokit/request/node_modules/@octokit/endpoint": {
|
||||
"version": "9.0.6",
|
||||
"resolved": "https://registry.npmjs.org/@octokit/endpoint/-/endpoint-9.0.6.tgz",
|
||||
"integrity": "sha512-H1fNTMA57HbkFESSt3Y9+FBICv+0jFceJFPWDePYlR/iMGrwM5ph+Dd4XRQs+8X+PUFURLQgX9ChPfhJ/1uNQw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@octokit/types": "^13.1.0",
|
||||
"universal-user-agent": "^6.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 18"
|
||||
}
|
||||
},
|
||||
"node_modules/@octokit/request/node_modules/@octokit/openapi-types": {
|
||||
"version": "22.2.0",
|
||||
"resolved": "https://registry.npmjs.org/@octokit/openapi-types/-/openapi-types-22.2.0.tgz",
|
||||
"integrity": "sha512-QBhVjcUa9W7Wwhm6DBFu6ZZ+1/t/oYxqc2tp81Pi41YNuJinbFRx8B133qVOrAaBbF7D/m0Et6f9/pZt9Rc+tg=="
|
||||
"version": "24.2.0",
|
||||
"resolved": "https://registry.npmjs.org/@octokit/openapi-types/-/openapi-types-24.2.0.tgz",
|
||||
"integrity": "sha512-9sIH3nSUttelJSXUrmGzl7QUBFul0/mB8HRYl3fOlgHbIWG+WnYDXU3v/2zMtAvuzZ/ed00Ei6on975FhBfzrg==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@octokit/request/node_modules/@octokit/types": {
|
||||
"version": "13.6.1",
|
||||
"resolved": "https://registry.npmjs.org/@octokit/types/-/types-13.6.1.tgz",
|
||||
"integrity": "sha512-PHZE9Z+kWXb23Ndik8MKPirBPziOc0D2/3KH1P+6jK5nGWe96kadZuE4jev2/Jq7FvIfTlT2Ltg8Fv2x1v0a5g==",
|
||||
"version": "13.10.0",
|
||||
"resolved": "https://registry.npmjs.org/@octokit/types/-/types-13.10.0.tgz",
|
||||
"integrity": "sha512-ifLaO34EbbPj0Xgro4G5lP5asESjwHracYJvVaPIyXMuiuXLlhic3S47cBdTb+jfODkTE5YtGCLt3Ay3+J97sA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@octokit/openapi-types": "^22.2.0"
|
||||
"@octokit/openapi-types": "^24.2.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@octokit/request/node_modules/universal-user-agent": {
|
||||
"version": "6.0.1",
|
||||
"resolved": "https://registry.npmjs.org/universal-user-agent/-/universal-user-agent-6.0.1.tgz",
|
||||
"integrity": "sha512-yCzhz6FN2wU1NiiQRogkTQszlQSlpWaw8SvVegAc+bDxbzHgh1vX8uIe8OYyMH6DwH+sdTJsgMl36+mSMdRJIQ==",
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/@octokit/rest": {
|
||||
"version": "20.0.2",
|
||||
"resolved": "https://registry.npmjs.org/@octokit/rest/-/rest-20.0.2.tgz",
|
||||
@@ -13005,216 +12957,6 @@
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@swc/core": {
|
||||
"version": "1.3.107",
|
||||
"resolved": "https://registry.npmjs.org/@swc/core/-/core-1.3.107.tgz",
|
||||
"integrity": "sha512-zKhqDyFcTsyLIYK1iEmavljZnf4CCor5pF52UzLAz4B6Nu/4GLU+2LQVAf+oRHjusG39PTPjd2AlRT3f3QWfsQ==",
|
||||
"dev": true,
|
||||
"hasInstallScript": true,
|
||||
"optional": true,
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@swc/counter": "^0.1.1",
|
||||
"@swc/types": "^0.1.5"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/swc"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"@swc/core-darwin-arm64": "1.3.107",
|
||||
"@swc/core-darwin-x64": "1.3.107",
|
||||
"@swc/core-linux-arm-gnueabihf": "1.3.107",
|
||||
"@swc/core-linux-arm64-gnu": "1.3.107",
|
||||
"@swc/core-linux-arm64-musl": "1.3.107",
|
||||
"@swc/core-linux-x64-gnu": "1.3.107",
|
||||
"@swc/core-linux-x64-musl": "1.3.107",
|
||||
"@swc/core-win32-arm64-msvc": "1.3.107",
|
||||
"@swc/core-win32-ia32-msvc": "1.3.107",
|
||||
"@swc/core-win32-x64-msvc": "1.3.107"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@swc/helpers": "^0.5.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@swc/helpers": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@swc/core-darwin-arm64": {
|
||||
"version": "1.3.107",
|
||||
"resolved": "https://registry.npmjs.org/@swc/core-darwin-arm64/-/core-darwin-arm64-1.3.107.tgz",
|
||||
"integrity": "sha512-47tD/5vSXWxPd0j/ZllyQUg4bqalbQTsmqSw0J4dDdS82MWqCAwUErUrAZPRjBkjNQ6Kmrf5rpCWaGTtPw+ngw==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"os": [
|
||||
"darwin"
|
||||
],
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
}
|
||||
},
|
||||
"node_modules/@swc/core-darwin-x64": {
|
||||
"version": "1.3.107",
|
||||
"resolved": "https://registry.npmjs.org/@swc/core-darwin-x64/-/core-darwin-x64-1.3.107.tgz",
|
||||
"integrity": "sha512-hwiLJ2ulNkBGAh1m1eTfeY1417OAYbRGcb/iGsJ+LuVLvKAhU/itzsl535CvcwAlt2LayeCFfcI8gdeOLeZa9A==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"os": [
|
||||
"darwin"
|
||||
],
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
}
|
||||
},
|
||||
"node_modules/@swc/core-linux-arm-gnueabihf": {
|
||||
"version": "1.3.107",
|
||||
"resolved": "https://registry.npmjs.org/@swc/core-linux-arm-gnueabihf/-/core-linux-arm-gnueabihf-1.3.107.tgz",
|
||||
"integrity": "sha512-I2wzcC0KXqh0OwymCmYwNRgZ9nxX7DWnOOStJXV3pS0uB83TXAkmqd7wvMBuIl9qu4Hfomi9aDM7IlEEn9tumQ==",
|
||||
"cpu": [
|
||||
"arm"
|
||||
],
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
}
|
||||
},
|
||||
"node_modules/@swc/core-linux-arm64-gnu": {
|
||||
"version": "1.3.107",
|
||||
"resolved": "https://registry.npmjs.org/@swc/core-linux-arm64-gnu/-/core-linux-arm64-gnu-1.3.107.tgz",
|
||||
"integrity": "sha512-HWgnn7JORYlOYnGsdunpSF8A+BCZKPLzLtEUA27/M/ZuANcMZabKL9Zurt7XQXq888uJFAt98Gy+59PU90aHKg==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
}
|
||||
},
|
||||
"node_modules/@swc/core-linux-arm64-musl": {
|
||||
"version": "1.3.107",
|
||||
"resolved": "https://registry.npmjs.org/@swc/core-linux-arm64-musl/-/core-linux-arm64-musl-1.3.107.tgz",
|
||||
"integrity": "sha512-vfPF74cWfAm8hyhS8yvYI94ucMHIo8xIYU+oFOW9uvDlGQRgnUf/6DEVbLyt/3yfX5723Ln57U8uiMALbX5Pyw==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
}
|
||||
},
|
||||
"node_modules/@swc/core-linux-x64-gnu": {
|
||||
"version": "1.3.107",
|
||||
"resolved": "https://registry.npmjs.org/@swc/core-linux-x64-gnu/-/core-linux-x64-gnu-1.3.107.tgz",
|
||||
"integrity": "sha512-uBVNhIg0ip8rH9OnOsCARUFZ3Mq3tbPHxtmWk9uAa5u8jQwGWeBx5+nTHpDOVd3YxKb6+5xDEI/edeeLpha/9g==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
}
|
||||
},
|
||||
"node_modules/@swc/core-linux-x64-musl": {
|
||||
"version": "1.3.107",
|
||||
"resolved": "https://registry.npmjs.org/@swc/core-linux-x64-musl/-/core-linux-x64-musl-1.3.107.tgz",
|
||||
"integrity": "sha512-mvACkUvzSIB12q1H5JtabWATbk3AG+pQgXEN95AmEX2ZA5gbP9+B+mijsg7Sd/3tboHr7ZHLz/q3SHTvdFJrEw==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
}
|
||||
},
|
||||
"node_modules/@swc/core-win32-arm64-msvc": {
|
||||
"version": "1.3.107",
|
||||
"resolved": "https://registry.npmjs.org/@swc/core-win32-arm64-msvc/-/core-win32-arm64-msvc-1.3.107.tgz",
|
||||
"integrity": "sha512-J3P14Ngy/1qtapzbguEH41kY109t6DFxfbK4Ntz9dOWNuVY3o9/RTB841ctnJk0ZHEG+BjfCJjsD2n8H5HcaOA==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"os": [
|
||||
"win32"
|
||||
],
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
}
|
||||
},
|
||||
"node_modules/@swc/core-win32-ia32-msvc": {
|
||||
"version": "1.3.107",
|
||||
"resolved": "https://registry.npmjs.org/@swc/core-win32-ia32-msvc/-/core-win32-ia32-msvc-1.3.107.tgz",
|
||||
"integrity": "sha512-ZBUtgyjTHlz8TPJh7kfwwwFma+ktr6OccB1oXC8fMSopD0AxVnQasgun3l3099wIsAB9eEsJDQ/3lDkOLs1gBA==",
|
||||
"cpu": [
|
||||
"ia32"
|
||||
],
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"os": [
|
||||
"win32"
|
||||
],
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
}
|
||||
},
|
||||
"node_modules/@swc/core-win32-x64-msvc": {
|
||||
"version": "1.3.107",
|
||||
"resolved": "https://registry.npmjs.org/@swc/core-win32-x64-msvc/-/core-win32-x64-msvc-1.3.107.tgz",
|
||||
"integrity": "sha512-Eyzo2XRqWOxqhE1gk9h7LWmUf4Bp4Xn2Ttb0ayAXFp6YSTxQIThXcT9kipXZqcpxcmDwoq8iWbbf2P8XL743EA==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"os": [
|
||||
"win32"
|
||||
],
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
}
|
||||
},
|
||||
"node_modules/@swc/counter": {
|
||||
"version": "0.1.3",
|
||||
"resolved": "https://registry.npmjs.org/@swc/counter/-/counter-0.1.3.tgz",
|
||||
@@ -13232,14 +12974,6 @@
|
||||
"tslib": "^2.8.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@swc/types": {
|
||||
"version": "0.1.5",
|
||||
"resolved": "https://registry.npmjs.org/@swc/types/-/types-0.1.5.tgz",
|
||||
"integrity": "sha512-myfUej5naTBWnqOCc/MdVOLVjXUXtIA+NpDrDBKJtLLg2shUjBu3cZmB/85RyitKc55+lUUyl7oRfLOvkr2hsw==",
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"peer": true
|
||||
},
|
||||
"node_modules/@techteamer/ocsp": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/@techteamer/ocsp/-/ocsp-1.0.1.tgz",
|
||||
@@ -15244,6 +14978,18 @@
|
||||
"proxy-from-env": "^1.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/axios-ntlm": {
|
||||
"version": "1.4.4",
|
||||
"resolved": "https://registry.npmjs.org/axios-ntlm/-/axios-ntlm-1.4.4.tgz",
|
||||
"integrity": "sha512-kpCRdzMfL8gi0Z0o96P3QPAK4XuC8iciGgxGXe+PeQ4oyjI2LZN8WSOKbu0Y9Jo3T/A7pB81n6jYVPIpglEuRA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"axios": "^1.8.4",
|
||||
"des.js": "^1.1.0",
|
||||
"dev-null": "^0.1.1",
|
||||
"js-md4": "^0.3.2"
|
||||
}
|
||||
},
|
||||
"node_modules/axios-retry": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/axios-retry/-/axios-retry-4.0.0.tgz",
|
||||
@@ -17003,6 +16749,16 @@
|
||||
"resolved": "https://registry.npmjs.org/deprecation/-/deprecation-2.3.1.tgz",
|
||||
"integrity": "sha512-xmHIy4F3scKVwMsQ4WnVaS8bHOx0DmVwRywosKhaILI0ywMDWPtBSku2HNxRvF7jtwDRsoEwYQSfbxj8b7RlJQ=="
|
||||
},
|
||||
"node_modules/des.js": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/des.js/-/des.js-1.1.0.tgz",
|
||||
"integrity": "sha512-r17GxjhUCjSRy8aiJpr8/UadFIzMzJGexI3Nmz4ADi9LYSFx4gTBp80+NaX/YsXWWLhpZ7v/v/ubEc/bCNfKwg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"inherits": "^2.0.1",
|
||||
"minimalistic-assert": "^1.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/destroy": {
|
||||
"version": "1.2.0",
|
||||
"resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz",
|
||||
@@ -17030,6 +16786,12 @@
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/dev-null": {
|
||||
"version": "0.1.1",
|
||||
"resolved": "https://registry.npmjs.org/dev-null/-/dev-null-0.1.1.tgz",
|
||||
"integrity": "sha512-nMNZG0zfMgmdv8S5O0TM5cpwNbGKRGPCxVsr0SmA3NZZy9CYBbuNLL0PD3Acx9e5LIUgwONXtM9kM6RlawPxEQ==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/diff": {
|
||||
"version": "4.0.2",
|
||||
"resolved": "https://registry.npmjs.org/diff/-/diff-4.0.2.tgz",
|
||||
@@ -18288,7 +18050,8 @@
|
||||
"node_modules/fast-content-type-parse": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/fast-content-type-parse/-/fast-content-type-parse-1.1.0.tgz",
|
||||
"integrity": "sha512-fBHHqSTFLVnR61C+gltJuE5GkVQMV0S2nqUO8TJ+5Z3qAKG8vAx4FKai1s5jq/inV1+sREynIWSuQ6HgoSXpDQ=="
|
||||
"integrity": "sha512-fBHHqSTFLVnR61C+gltJuE5GkVQMV0S2nqUO8TJ+5Z3qAKG8vAx4FKai1s5jq/inV1+sREynIWSuQ6HgoSXpDQ==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/fast-copy": {
|
||||
"version": "3.0.1",
|
||||
@@ -19077,49 +18840,6 @@
|
||||
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz",
|
||||
"integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w=="
|
||||
},
|
||||
"node_modules/gcp-metadata": {
|
||||
"version": "5.3.0",
|
||||
"resolved": "https://registry.npmjs.org/gcp-metadata/-/gcp-metadata-5.3.0.tgz",
|
||||
"integrity": "sha512-FNTkdNEnBdlqF2oatizolQqNANMrcqJt6AAYt99B3y1aLLC8Hc5IOBb+ZnnzllodEEf6xMBp6wRcBbc16fa65w==",
|
||||
"optional": true,
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"gaxios": "^5.0.0",
|
||||
"json-bigint": "^1.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/gcp-metadata/node_modules/gaxios": {
|
||||
"version": "5.1.3",
|
||||
"resolved": "https://registry.npmjs.org/gaxios/-/gaxios-5.1.3.tgz",
|
||||
"integrity": "sha512-95hVgBRgEIRQQQHIbnxBXeHbW4TqFk4ZDJW7wmVtvYar72FdhRIo1UGOLS2eRAKCPEdPBWu+M7+A33D9CdX9rA==",
|
||||
"optional": true,
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"extend": "^3.0.2",
|
||||
"https-proxy-agent": "^5.0.0",
|
||||
"is-stream": "^2.0.0",
|
||||
"node-fetch": "^2.6.9"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/gcp-metadata/node_modules/is-stream": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz",
|
||||
"integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==",
|
||||
"optional": true,
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/generate-function": {
|
||||
"version": "2.3.1",
|
||||
"resolved": "https://registry.npmjs.org/generate-function/-/generate-function-2.3.1.tgz",
|
||||
@@ -24776,6 +24496,12 @@
|
||||
"jsonwebtoken": "^9.0.2"
|
||||
}
|
||||
},
|
||||
"node_modules/octokit-auth-probot/node_modules/universal-user-agent": {
|
||||
"version": "6.0.1",
|
||||
"resolved": "https://registry.npmjs.org/universal-user-agent/-/universal-user-agent-6.0.1.tgz",
|
||||
"integrity": "sha512-yCzhz6FN2wU1NiiQRogkTQszlQSlpWaw8SvVegAc+bDxbzHgh1vX8uIe8OYyMH6DwH+sdTJsgMl36+mSMdRJIQ==",
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/odbc": {
|
||||
"version": "2.4.9",
|
||||
"resolved": "https://registry.npmjs.org/odbc/-/odbc-2.4.9.tgz",
|
||||
@@ -30705,9 +30431,10 @@
|
||||
"integrity": "sha512-G5o6f95b5BggDGuUfKDApKaCgNYy2x7OdHY0zSMF081O0EJobw+1130VONhrA7ezGSV2FNOGyM+KQpQZAr9bIQ=="
|
||||
},
|
||||
"node_modules/universal-user-agent": {
|
||||
"version": "6.0.1",
|
||||
"resolved": "https://registry.npmjs.org/universal-user-agent/-/universal-user-agent-6.0.1.tgz",
|
||||
"integrity": "sha512-yCzhz6FN2wU1NiiQRogkTQszlQSlpWaw8SvVegAc+bDxbzHgh1vX8uIe8OYyMH6DwH+sdTJsgMl36+mSMdRJIQ=="
|
||||
"version": "7.0.3",
|
||||
"resolved": "https://registry.npmjs.org/universal-user-agent/-/universal-user-agent-7.0.3.tgz",
|
||||
"integrity": "sha512-TmnEAEAsBJVZM/AADELsK76llnwcf9vMKuPz8JflO1frO8Lchitr0fNaN9d+Ap0BjKtqWqd/J17qeDnXh8CL2A==",
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/universalify": {
|
||||
"version": "2.0.1",
|
||||
|
@@ -37,7 +37,7 @@
|
||||
"build": "tsup --sourcemap",
|
||||
"build:frontend": "npm run build --prefix ../frontend",
|
||||
"start": "node --enable-source-maps dist/main.mjs",
|
||||
"type:check": "tsc --noEmit",
|
||||
"type:check": "node --max-old-space-size=8192 ./node_modules/.bin/tsc --noEmit",
|
||||
"lint:fix": "node --max-old-space-size=8192 ./node_modules/.bin/eslint --fix --ext js,ts ./src",
|
||||
"lint": "node --max-old-space-size=8192 ./node_modules/.bin/eslint 'src/**/*.ts'",
|
||||
"test:unit": "vitest run -c vitest.unit.config.ts",
|
||||
@@ -158,6 +158,7 @@
|
||||
"@octokit/core": "^5.2.1",
|
||||
"@octokit/plugin-paginate-graphql": "^4.0.1",
|
||||
"@octokit/plugin-retry": "^5.0.5",
|
||||
"@octokit/request": "8.4.1",
|
||||
"@octokit/rest": "^20.0.2",
|
||||
"@octokit/webhooks-types": "^7.3.1",
|
||||
"@octopusdeploy/api-client": "^3.4.1",
|
||||
@@ -182,6 +183,7 @@
|
||||
"argon2": "^0.31.2",
|
||||
"aws-sdk": "^2.1553.0",
|
||||
"axios": "^1.11.0",
|
||||
"axios-ntlm": "^1.4.4",
|
||||
"axios-retry": "^4.0.0",
|
||||
"bcrypt": "^5.1.1",
|
||||
"botbuilder": "^4.23.2",
|
||||
|
@@ -99,6 +99,7 @@ const main = async () => {
|
||||
(el) =>
|
||||
!el.tableName.includes("_migrations") &&
|
||||
!el.tableName.includes("audit_logs_") &&
|
||||
!el.tableName.includes("active_locks") &&
|
||||
el.tableName !== "intermediate_audit_logs"
|
||||
);
|
||||
|
||||
|
3
backend/src/@types/fastify.d.ts
vendored
3
backend/src/@types/fastify.d.ts
vendored
@@ -18,6 +18,7 @@ import { TExternalKmsServiceFactory } from "@app/ee/services/external-kms/extern
|
||||
import { TGatewayServiceFactory } from "@app/ee/services/gateway/gateway-service";
|
||||
import { TGithubOrgSyncServiceFactory } from "@app/ee/services/github-org-sync/github-org-sync-service";
|
||||
import { TGroupServiceFactory } from "@app/ee/services/group/group-service";
|
||||
import { TIdentityAuthTemplateServiceFactory } from "@app/ee/services/identity-auth-template";
|
||||
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 { TKmipClientDALFactory } from "@app/ee/services/kmip/kmip-client-dal";
|
||||
@@ -147,6 +148,7 @@ declare module "fastify" {
|
||||
interface Session {
|
||||
callbackPort: string;
|
||||
isAdminLogin: boolean;
|
||||
orgSlug?: string;
|
||||
}
|
||||
|
||||
interface FastifyRequest {
|
||||
@@ -300,6 +302,7 @@ declare module "fastify" {
|
||||
reminder: TReminderServiceFactory;
|
||||
bus: TEventBusService;
|
||||
sse: TServerSentEventsService;
|
||||
identityAuthTemplate: TIdentityAuthTemplateServiceFactory;
|
||||
};
|
||||
// this is exclusive use for middlewares in which we need to inject data
|
||||
// everywhere else access using service layer
|
||||
|
10
backend/src/@types/knex.d.ts
vendored
10
backend/src/@types/knex.d.ts
vendored
@@ -494,6 +494,11 @@ import {
|
||||
TAccessApprovalPoliciesEnvironmentsInsert,
|
||||
TAccessApprovalPoliciesEnvironmentsUpdate
|
||||
} from "@app/db/schemas/access-approval-policies-environments";
|
||||
import {
|
||||
TIdentityAuthTemplates,
|
||||
TIdentityAuthTemplatesInsert,
|
||||
TIdentityAuthTemplatesUpdate
|
||||
} from "@app/db/schemas/identity-auth-templates";
|
||||
import {
|
||||
TIdentityLdapAuths,
|
||||
TIdentityLdapAuthsInsert,
|
||||
@@ -878,6 +883,11 @@ declare module "knex/types/tables" {
|
||||
TIdentityProjectAdditionalPrivilegeInsert,
|
||||
TIdentityProjectAdditionalPrivilegeUpdate
|
||||
>;
|
||||
[TableName.IdentityAuthTemplate]: KnexOriginal.CompositeTableType<
|
||||
TIdentityAuthTemplates,
|
||||
TIdentityAuthTemplatesInsert,
|
||||
TIdentityAuthTemplatesUpdate
|
||||
>;
|
||||
|
||||
[TableName.AccessApprovalPolicy]: KnexOriginal.CompositeTableType<
|
||||
TAccessApprovalPolicies,
|
||||
|
@@ -84,6 +84,9 @@ const up = async (knex: Knex): Promise<void> => {
|
||||
t.index("expiresAt");
|
||||
t.index("orgId");
|
||||
t.index("projectId");
|
||||
t.index("eventType");
|
||||
t.index("userAgentType");
|
||||
t.index("actor");
|
||||
});
|
||||
|
||||
console.log("Adding GIN indices...");
|
||||
@@ -119,8 +122,8 @@ const up = async (knex: Knex): Promise<void> => {
|
||||
console.log("Creating audit log partitions ahead of time... next date:", nextDateStr);
|
||||
await createAuditLogPartition(knex, nextDate, new Date(nextDate.getFullYear(), nextDate.getMonth() + 1));
|
||||
|
||||
// create partitions 4 years ahead
|
||||
const partitionMonths = 4 * 12;
|
||||
// create partitions 20 years ahead
|
||||
const partitionMonths = 20 * 12;
|
||||
const partitionPromises: Promise<void>[] = [];
|
||||
for (let x = 1; x <= partitionMonths; x += 1) {
|
||||
partitionPromises.push(
|
||||
|
18
backend/src/db/migrations/20250723220500_remove-srp.ts
Normal file
18
backend/src/db/migrations/20250723220500_remove-srp.ts
Normal file
@@ -0,0 +1,18 @@
|
||||
import { Knex } from "knex";
|
||||
|
||||
import { TableName } from "../schemas";
|
||||
|
||||
export async function up(knex: Knex): Promise<void> {
|
||||
await knex.schema.alterTable(TableName.UserEncryptionKey, (table) => {
|
||||
table.text("encryptedPrivateKey").nullable().alter();
|
||||
table.text("publicKey").nullable().alter();
|
||||
table.text("iv").nullable().alter();
|
||||
table.text("tag").nullable().alter();
|
||||
table.text("salt").nullable().alter();
|
||||
table.text("verifier").nullable().alter();
|
||||
});
|
||||
}
|
||||
|
||||
export async function down(): Promise<void> {
|
||||
// do nothing for now to avoid breaking down migrations
|
||||
}
|
@@ -2,7 +2,7 @@
|
||||
import { Knex } from "knex";
|
||||
|
||||
import { chunkArray } from "@app/lib/fn";
|
||||
import { logger } from "@app/lib/logger";
|
||||
import { initLogger, logger } from "@app/lib/logger";
|
||||
|
||||
import { TableName } from "../schemas";
|
||||
import { TReminders, TRemindersInsert } from "../schemas/reminders";
|
||||
@@ -107,5 +107,6 @@ export async function up(knex: Knex): Promise<void> {
|
||||
}
|
||||
|
||||
export async function down(): Promise<void> {
|
||||
initLogger();
|
||||
logger.info("Rollback not implemented for secret reminders fix migration");
|
||||
}
|
||||
|
@@ -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.Reminder, "fromDate"))) {
|
||||
await knex.schema.alterTable(TableName.Reminder, (t) => {
|
||||
t.timestamp("fromDate", { useTz: true }).nullable();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export async function down(knex: Knex): Promise<void> {
|
||||
if (await knex.schema.hasColumn(TableName.Reminder, "fromDate")) {
|
||||
await knex.schema.alterTable(TableName.Reminder, (t) => {
|
||||
t.dropColumn("fromDate");
|
||||
});
|
||||
}
|
||||
}
|
@@ -0,0 +1,36 @@
|
||||
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.IdentityAuthTemplate))) {
|
||||
await knex.schema.createTable(TableName.IdentityAuthTemplate, (t) => {
|
||||
t.uuid("id", { primaryKey: true }).defaultTo(knex.fn.uuid());
|
||||
t.binary("templateFields").notNullable();
|
||||
t.uuid("orgId").notNullable();
|
||||
t.foreign("orgId").references("id").inTable(TableName.Organization).onDelete("CASCADE");
|
||||
t.string("name", 64).notNullable();
|
||||
t.string("authMethod").notNullable();
|
||||
t.timestamps(true, true, true);
|
||||
});
|
||||
await createOnUpdateTrigger(knex, TableName.IdentityAuthTemplate);
|
||||
}
|
||||
if (!(await knex.schema.hasColumn(TableName.IdentityLdapAuth, "templateId"))) {
|
||||
await knex.schema.alterTable(TableName.IdentityLdapAuth, (t) => {
|
||||
t.uuid("templateId").nullable();
|
||||
t.foreign("templateId").references("id").inTable(TableName.IdentityAuthTemplate).onDelete("SET NULL");
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export async function down(knex: Knex): Promise<void> {
|
||||
if (await knex.schema.hasColumn(TableName.IdentityLdapAuth, "templateId")) {
|
||||
await knex.schema.alterTable(TableName.IdentityLdapAuth, (t) => {
|
||||
t.dropForeign(["templateId"]);
|
||||
t.dropColumn("templateId");
|
||||
});
|
||||
}
|
||||
await knex.schema.dropTableIfExists(TableName.IdentityAuthTemplate);
|
||||
await dropOnUpdateTrigger(knex, TableName.IdentityAuthTemplate);
|
||||
}
|
@@ -0,0 +1,65 @@
|
||||
import { Knex } from "knex";
|
||||
|
||||
import { TableName } from "../schemas";
|
||||
|
||||
export async function up(knex: Knex): Promise<void> {
|
||||
const lastUserLoggedInAuthMethod = await knex.schema.hasColumn(TableName.OrgMembership, "lastLoginAuthMethod");
|
||||
const lastIdentityLoggedInAuthMethod = await knex.schema.hasColumn(
|
||||
TableName.IdentityOrgMembership,
|
||||
"lastLoginAuthMethod"
|
||||
);
|
||||
const lastUserLoggedInTime = await knex.schema.hasColumn(TableName.OrgMembership, "lastLoginTime");
|
||||
const lastIdentityLoggedInTime = await knex.schema.hasColumn(TableName.IdentityOrgMembership, "lastLoginTime");
|
||||
if (!lastUserLoggedInAuthMethod || !lastUserLoggedInTime) {
|
||||
await knex.schema.alterTable(TableName.OrgMembership, (t) => {
|
||||
if (!lastUserLoggedInAuthMethod) {
|
||||
t.string("lastLoginAuthMethod").nullable();
|
||||
}
|
||||
if (!lastUserLoggedInTime) {
|
||||
t.datetime("lastLoginTime").nullable();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if (!lastIdentityLoggedInAuthMethod || !lastIdentityLoggedInTime) {
|
||||
await knex.schema.alterTable(TableName.IdentityOrgMembership, (t) => {
|
||||
if (!lastIdentityLoggedInAuthMethod) {
|
||||
t.string("lastLoginAuthMethod").nullable();
|
||||
}
|
||||
if (!lastIdentityLoggedInTime) {
|
||||
t.datetime("lastLoginTime").nullable();
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export async function down(knex: Knex): Promise<void> {
|
||||
const lastUserLoggedInAuthMethod = await knex.schema.hasColumn(TableName.OrgMembership, "lastLoginAuthMethod");
|
||||
const lastIdentityLoggedInAuthMethod = await knex.schema.hasColumn(
|
||||
TableName.IdentityOrgMembership,
|
||||
"lastLoginAuthMethod"
|
||||
);
|
||||
const lastUserLoggedInTime = await knex.schema.hasColumn(TableName.OrgMembership, "lastLoginTime");
|
||||
const lastIdentityLoggedInTime = await knex.schema.hasColumn(TableName.IdentityOrgMembership, "lastLoginTime");
|
||||
if (lastUserLoggedInAuthMethod || lastUserLoggedInTime) {
|
||||
await knex.schema.alterTable(TableName.OrgMembership, (t) => {
|
||||
if (lastUserLoggedInAuthMethod) {
|
||||
t.dropColumn("lastLoginAuthMethod");
|
||||
}
|
||||
if (lastUserLoggedInTime) {
|
||||
t.dropColumn("lastLoginTime");
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if (lastIdentityLoggedInAuthMethod || lastIdentityLoggedInTime) {
|
||||
await knex.schema.alterTable(TableName.IdentityOrgMembership, (t) => {
|
||||
if (lastIdentityLoggedInAuthMethod) {
|
||||
t.dropColumn("lastLoginAuthMethod");
|
||||
}
|
||||
if (lastIdentityLoggedInTime) {
|
||||
t.dropColumn("lastLoginTime");
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
@@ -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.AccessApprovalPolicy, "maxTimePeriod"))) {
|
||||
await knex.schema.alterTable(TableName.AccessApprovalPolicy, (t) => {
|
||||
t.string("maxTimePeriod").nullable(); // Ex: 1h - Null is permanent
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export async function down(knex: Knex): Promise<void> {
|
||||
if (await knex.schema.hasColumn(TableName.AccessApprovalPolicy, "maxTimePeriod")) {
|
||||
await knex.schema.alterTable(TableName.AccessApprovalPolicy, (t) => {
|
||||
t.dropColumn("maxTimePeriod");
|
||||
});
|
||||
}
|
||||
}
|
@@ -0,0 +1,49 @@
|
||||
import { Knex } from "knex";
|
||||
|
||||
import { TableName } from "../schemas";
|
||||
|
||||
const BATCH_SIZE = 1000;
|
||||
|
||||
export async function up(knex: Knex): Promise<void> {
|
||||
if (!(await knex.schema.hasColumn(TableName.UserAliases, "isEmailVerified"))) {
|
||||
// Add the column
|
||||
await knex.schema.alterTable(TableName.UserAliases, (t) => {
|
||||
t.boolean("isEmailVerified").defaultTo(false);
|
||||
});
|
||||
|
||||
const aliasesToUpdate: { aliasId: string; isEmailVerified: boolean }[] = await knex(TableName.UserAliases)
|
||||
.join(TableName.Users, `${TableName.UserAliases}.userId`, `${TableName.Users}.id`)
|
||||
.select([`${TableName.UserAliases}.id as aliasId`, `${TableName.Users}.isEmailVerified`]);
|
||||
|
||||
for (let i = 0; i < aliasesToUpdate.length; i += BATCH_SIZE) {
|
||||
const batch = aliasesToUpdate.slice(i, i + BATCH_SIZE);
|
||||
|
||||
const trueIds = batch.filter((row) => row.isEmailVerified).map((row) => row.aliasId);
|
||||
|
||||
if (trueIds.length > 0) {
|
||||
// eslint-disable-next-line no-await-in-loop
|
||||
await knex(TableName.UserAliases).whereIn("id", trueIds).update({ isEmailVerified: true });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!(await knex.schema.hasColumn(TableName.AuthTokens, "aliasId"))) {
|
||||
await knex.schema.alterTable(TableName.AuthTokens, (t) => {
|
||||
t.string("aliasId").nullable();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export async function down(knex: Knex): Promise<void> {
|
||||
if (await knex.schema.hasColumn(TableName.UserAliases, "isEmailVerified")) {
|
||||
await knex.schema.alterTable(TableName.UserAliases, (t) => {
|
||||
t.dropColumn("isEmailVerified");
|
||||
});
|
||||
}
|
||||
|
||||
if (await knex.schema.hasColumn(TableName.AuthTokens, "aliasId")) {
|
||||
await knex.schema.alterTable(TableName.AuthTokens, (t) => {
|
||||
t.dropColumn("aliasId");
|
||||
});
|
||||
}
|
||||
}
|
@@ -0,0 +1,38 @@
|
||||
import { Knex } from "knex";
|
||||
|
||||
import { TableName } from "@app/db/schemas";
|
||||
|
||||
export async function up(knex: Knex): Promise<void> {
|
||||
const hasEditNoteCol = await knex.schema.hasColumn(TableName.AccessApprovalRequest, "editNote");
|
||||
const hasEditedByUserId = await knex.schema.hasColumn(TableName.AccessApprovalRequest, "editedByUserId");
|
||||
|
||||
if (!hasEditNoteCol || !hasEditedByUserId) {
|
||||
await knex.schema.alterTable(TableName.AccessApprovalRequest, (t) => {
|
||||
if (!hasEditedByUserId) {
|
||||
t.uuid("editedByUserId").nullable();
|
||||
t.foreign("editedByUserId").references("id").inTable(TableName.Users).onDelete("SET NULL");
|
||||
}
|
||||
|
||||
if (!hasEditNoteCol) {
|
||||
t.string("editNote").nullable();
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export async function down(knex: Knex): Promise<void> {
|
||||
const hasEditNoteCol = await knex.schema.hasColumn(TableName.AccessApprovalRequest, "editNote");
|
||||
const hasEditedByUserId = await knex.schema.hasColumn(TableName.AccessApprovalRequest, "editedByUserId");
|
||||
|
||||
if (hasEditNoteCol || hasEditedByUserId) {
|
||||
await knex.schema.alterTable(TableName.AccessApprovalRequest, (t) => {
|
||||
if (hasEditedByUserId) {
|
||||
t.dropColumn("editedByUserId");
|
||||
}
|
||||
|
||||
if (hasEditNoteCol) {
|
||||
t.dropColumn("editNote");
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
@@ -0,0 +1,39 @@
|
||||
import { Knex } from "knex";
|
||||
|
||||
import { TableName } from "../schemas";
|
||||
|
||||
const GOOGLE_SSO_AUTH_ENFORCED_COLUMN_NAME = "googleSsoAuthEnforced";
|
||||
const GOOGLE_SSO_AUTH_LAST_USED_COLUMN_NAME = "googleSsoAuthLastUsed";
|
||||
export async function up(knex: Knex): Promise<void> {
|
||||
const hasGoogleSsoAuthEnforcedColumn = await knex.schema.hasColumn(
|
||||
TableName.Organization,
|
||||
GOOGLE_SSO_AUTH_ENFORCED_COLUMN_NAME
|
||||
);
|
||||
const hasGoogleSsoAuthLastUsedColumn = await knex.schema.hasColumn(
|
||||
TableName.Organization,
|
||||
GOOGLE_SSO_AUTH_LAST_USED_COLUMN_NAME
|
||||
);
|
||||
|
||||
await knex.schema.alterTable(TableName.Organization, (table) => {
|
||||
if (!hasGoogleSsoAuthEnforcedColumn)
|
||||
table.boolean(GOOGLE_SSO_AUTH_ENFORCED_COLUMN_NAME).defaultTo(false).notNullable();
|
||||
if (!hasGoogleSsoAuthLastUsedColumn) table.timestamp(GOOGLE_SSO_AUTH_LAST_USED_COLUMN_NAME).nullable();
|
||||
});
|
||||
}
|
||||
|
||||
export async function down(knex: Knex): Promise<void> {
|
||||
const hasGoogleSsoAuthEnforcedColumn = await knex.schema.hasColumn(
|
||||
TableName.Organization,
|
||||
GOOGLE_SSO_AUTH_ENFORCED_COLUMN_NAME
|
||||
);
|
||||
|
||||
const hasGoogleSsoAuthLastUsedColumn = await knex.schema.hasColumn(
|
||||
TableName.Organization,
|
||||
GOOGLE_SSO_AUTH_LAST_USED_COLUMN_NAME
|
||||
);
|
||||
|
||||
await knex.schema.alterTable(TableName.Organization, (table) => {
|
||||
if (hasGoogleSsoAuthEnforcedColumn) table.dropColumn(GOOGLE_SSO_AUTH_ENFORCED_COLUMN_NAME);
|
||||
if (hasGoogleSsoAuthLastUsedColumn) table.dropColumn(GOOGLE_SSO_AUTH_LAST_USED_COLUMN_NAME);
|
||||
});
|
||||
}
|
@@ -0,0 +1,57 @@
|
||||
import { Knex } from "knex";
|
||||
|
||||
import { TableName } from "../schemas";
|
||||
|
||||
export async function up(knex: Knex): Promise<void> {
|
||||
if (await knex.schema.hasTable(TableName.IdentityUniversalAuth)) {
|
||||
const hasLockoutEnabled = await knex.schema.hasColumn(TableName.IdentityUniversalAuth, "lockoutEnabled");
|
||||
const hasLockoutThreshold = await knex.schema.hasColumn(TableName.IdentityUniversalAuth, "lockoutThreshold");
|
||||
const hasLockoutDuration = await knex.schema.hasColumn(TableName.IdentityUniversalAuth, "lockoutDurationSeconds");
|
||||
const hasLockoutCounterReset = await knex.schema.hasColumn(
|
||||
TableName.IdentityUniversalAuth,
|
||||
"lockoutCounterResetSeconds"
|
||||
);
|
||||
|
||||
await knex.schema.alterTable(TableName.IdentityUniversalAuth, (t) => {
|
||||
if (!hasLockoutEnabled) {
|
||||
t.boolean("lockoutEnabled").notNullable().defaultTo(true);
|
||||
}
|
||||
if (!hasLockoutThreshold) {
|
||||
t.integer("lockoutThreshold").notNullable().defaultTo(3);
|
||||
}
|
||||
if (!hasLockoutDuration) {
|
||||
t.integer("lockoutDurationSeconds").notNullable().defaultTo(300); // 5 minutes
|
||||
}
|
||||
if (!hasLockoutCounterReset) {
|
||||
t.integer("lockoutCounterResetSeconds").notNullable().defaultTo(30); // 30 seconds
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export async function down(knex: Knex): Promise<void> {
|
||||
if (await knex.schema.hasTable(TableName.IdentityUniversalAuth)) {
|
||||
const hasLockoutEnabled = await knex.schema.hasColumn(TableName.IdentityUniversalAuth, "lockoutEnabled");
|
||||
const hasLockoutThreshold = await knex.schema.hasColumn(TableName.IdentityUniversalAuth, "lockoutThreshold");
|
||||
const hasLockoutDuration = await knex.schema.hasColumn(TableName.IdentityUniversalAuth, "lockoutDurationSeconds");
|
||||
const hasLockoutCounterReset = await knex.schema.hasColumn(
|
||||
TableName.IdentityUniversalAuth,
|
||||
"lockoutCounterResetSeconds"
|
||||
);
|
||||
|
||||
await knex.schema.alterTable(TableName.IdentityUniversalAuth, (t) => {
|
||||
if (hasLockoutEnabled) {
|
||||
t.dropColumn("lockoutEnabled");
|
||||
}
|
||||
if (hasLockoutThreshold) {
|
||||
t.dropColumn("lockoutThreshold");
|
||||
}
|
||||
if (hasLockoutDuration) {
|
||||
t.dropColumn("lockoutDurationSeconds");
|
||||
}
|
||||
if (hasLockoutCounterReset) {
|
||||
t.dropColumn("lockoutCounterResetSeconds");
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
@@ -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.SecretApprovalPolicy, "shouldCheckSecretPermission"))) {
|
||||
await knex.schema.alterTable(TableName.SecretApprovalPolicy, (t) => {
|
||||
t.boolean("shouldCheckSecretPermission").nullable();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export async function down(knex: Knex): Promise<void> {
|
||||
if (await knex.schema.hasColumn(TableName.SecretApprovalPolicy, "shouldCheckSecretPermission")) {
|
||||
await knex.schema.alterTable(TableName.SecretApprovalPolicy, (t) => {
|
||||
t.dropColumn("shouldCheckSecretPermission");
|
||||
});
|
||||
}
|
||||
}
|
@@ -0,0 +1,29 @@
|
||||
import { Knex } from "knex";
|
||||
|
||||
import { selectAllTableCols } from "@app/lib/knex";
|
||||
|
||||
import { TableName } from "../schemas";
|
||||
|
||||
const BATCH_SIZE = 100;
|
||||
|
||||
export async function up(knex: Knex): Promise<void> {
|
||||
if (await knex.schema.hasColumn(TableName.SecretApprovalPolicy, "shouldCheckSecretPermission")) {
|
||||
// find all existing SecretApprovalPolicy rows to backfill shouldCheckSecretPermission flag
|
||||
const rows = await knex(TableName.SecretApprovalPolicy).select(selectAllTableCols(TableName.SecretApprovalPolicy));
|
||||
|
||||
if (rows.length > 0) {
|
||||
for (let i = 0; i < rows.length; i += BATCH_SIZE) {
|
||||
const batch = rows.slice(i, i + BATCH_SIZE);
|
||||
// eslint-disable-next-line no-await-in-loop
|
||||
await knex(TableName.SecretApprovalPolicy)
|
||||
.whereIn(
|
||||
"id",
|
||||
batch.map((row) => row.id)
|
||||
)
|
||||
.update({ shouldCheckSecretPermission: true });
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export async function down(): Promise<void> {}
|
@@ -0,0 +1,23 @@
|
||||
import { Knex } from "knex";
|
||||
|
||||
import { TableName } from "../schemas";
|
||||
|
||||
export async function up(knex: Knex): Promise<void> {
|
||||
const hasPropertiesCol = await knex.schema.hasColumn(TableName.PkiSubscriber, "properties");
|
||||
|
||||
if (!hasPropertiesCol) {
|
||||
await knex.schema.alterTable(TableName.PkiSubscriber, (t) => {
|
||||
t.jsonb("properties").nullable();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export async function down(knex: Knex): Promise<void> {
|
||||
const hasPropertiesCol = await knex.schema.hasColumn(TableName.PkiSubscriber, "properties");
|
||||
|
||||
if (hasPropertiesCol) {
|
||||
await knex.schema.alterTable(TableName.PkiSubscriber, (t) => {
|
||||
t.dropColumn("properties");
|
||||
});
|
||||
}
|
||||
}
|
@@ -17,7 +17,8 @@ export const AccessApprovalPoliciesSchema = z.object({
|
||||
updatedAt: z.date(),
|
||||
enforcementLevel: z.string().default("hard"),
|
||||
deletedAt: z.date().nullable().optional(),
|
||||
allowedSelfApprovals: z.boolean().default(true)
|
||||
allowedSelfApprovals: z.boolean().default(true),
|
||||
maxTimePeriod: z.string().nullable().optional()
|
||||
});
|
||||
|
||||
export type TAccessApprovalPolicies = z.infer<typeof AccessApprovalPoliciesSchema>;
|
||||
|
@@ -20,7 +20,9 @@ export const AccessApprovalRequestsSchema = z.object({
|
||||
requestedByUserId: z.string().uuid(),
|
||||
note: z.string().nullable().optional(),
|
||||
privilegeDeletedAt: z.date().nullable().optional(),
|
||||
status: z.string().default("pending")
|
||||
status: z.string().default("pending"),
|
||||
editedByUserId: z.string().uuid().nullable().optional(),
|
||||
editNote: z.string().nullable().optional()
|
||||
});
|
||||
|
||||
export type TAccessApprovalRequests = z.infer<typeof AccessApprovalRequestsSchema>;
|
||||
|
@@ -17,7 +17,8 @@ export const AuthTokensSchema = z.object({
|
||||
createdAt: z.date(),
|
||||
updatedAt: z.date(),
|
||||
userId: z.string().uuid().nullable().optional(),
|
||||
orgId: z.string().uuid().nullable().optional()
|
||||
orgId: z.string().uuid().nullable().optional(),
|
||||
aliasId: z.string().nullable().optional()
|
||||
});
|
||||
|
||||
export type TAuthTokens = z.infer<typeof AuthTokensSchema>;
|
||||
|
24
backend/src/db/schemas/identity-auth-templates.ts
Normal file
24
backend/src/db/schemas/identity-auth-templates.ts
Normal file
@@ -0,0 +1,24 @@
|
||||
// Code generated by automation script, DO NOT EDIT.
|
||||
// Automated by pulling database and generating zod schema
|
||||
// To update. Just run npm run generate:schema
|
||||
// Written by akhilmhdh.
|
||||
|
||||
import { z } from "zod";
|
||||
|
||||
import { zodBuffer } from "@app/lib/zod";
|
||||
|
||||
import { TImmutableDBKeys } from "./models";
|
||||
|
||||
export const IdentityAuthTemplatesSchema = z.object({
|
||||
id: z.string().uuid(),
|
||||
templateFields: zodBuffer,
|
||||
orgId: z.string().uuid(),
|
||||
name: z.string(),
|
||||
authMethod: z.string(),
|
||||
createdAt: z.date(),
|
||||
updatedAt: z.date()
|
||||
});
|
||||
|
||||
export type TIdentityAuthTemplates = z.infer<typeof IdentityAuthTemplatesSchema>;
|
||||
export type TIdentityAuthTemplatesInsert = Omit<z.input<typeof IdentityAuthTemplatesSchema>, TImmutableDBKeys>;
|
||||
export type TIdentityAuthTemplatesUpdate = Partial<Omit<z.input<typeof IdentityAuthTemplatesSchema>, TImmutableDBKeys>>;
|
@@ -25,7 +25,8 @@ export const IdentityLdapAuthsSchema = z.object({
|
||||
allowedFields: z.unknown().nullable().optional(),
|
||||
createdAt: z.date(),
|
||||
updatedAt: z.date(),
|
||||
accessTokenPeriod: z.coerce.number().default(0)
|
||||
accessTokenPeriod: z.coerce.number().default(0),
|
||||
templateId: z.string().uuid().nullable().optional()
|
||||
});
|
||||
|
||||
export type TIdentityLdapAuths = z.infer<typeof IdentityLdapAuthsSchema>;
|
||||
|
@@ -14,7 +14,9 @@ export const IdentityOrgMembershipsSchema = z.object({
|
||||
orgId: z.string().uuid(),
|
||||
createdAt: z.date(),
|
||||
updatedAt: z.date(),
|
||||
identityId: z.string().uuid()
|
||||
identityId: z.string().uuid(),
|
||||
lastLoginAuthMethod: z.string().nullable().optional(),
|
||||
lastLoginTime: z.date().nullable().optional()
|
||||
});
|
||||
|
||||
export type TIdentityOrgMemberships = z.infer<typeof IdentityOrgMembershipsSchema>;
|
||||
|
@@ -18,7 +18,11 @@ export const IdentityUniversalAuthsSchema = z.object({
|
||||
createdAt: z.date(),
|
||||
updatedAt: z.date(),
|
||||
identityId: z.string().uuid(),
|
||||
accessTokenPeriod: z.coerce.number().default(0)
|
||||
accessTokenPeriod: z.coerce.number().default(0),
|
||||
lockoutEnabled: z.boolean().default(true),
|
||||
lockoutThreshold: z.number().default(3),
|
||||
lockoutDurationSeconds: z.number().default(300),
|
||||
lockoutCounterResetSeconds: z.number().default(30)
|
||||
});
|
||||
|
||||
export type TIdentityUniversalAuths = z.infer<typeof IdentityUniversalAuthsSchema>;
|
||||
|
@@ -91,6 +91,7 @@ export enum TableName {
|
||||
IdentityProjectMembership = "identity_project_memberships",
|
||||
IdentityProjectMembershipRole = "identity_project_membership_role",
|
||||
IdentityProjectAdditionalPrivilege = "identity_project_additional_privilege",
|
||||
IdentityAuthTemplate = "identity_auth_templates",
|
||||
// used by both identity and users
|
||||
IdentityMetadata = "identity_metadata",
|
||||
ResourceMetadata = "resource_metadata",
|
||||
|
@@ -19,7 +19,9 @@ export const OrgMembershipsSchema = z.object({
|
||||
roleId: z.string().uuid().nullable().optional(),
|
||||
projectFavorites: z.string().array().nullable().optional(),
|
||||
isActive: z.boolean().default(true),
|
||||
lastInvitedAt: z.date().nullable().optional()
|
||||
lastInvitedAt: z.date().nullable().optional(),
|
||||
lastLoginAuthMethod: z.string().nullable().optional(),
|
||||
lastLoginTime: z.date().nullable().optional()
|
||||
});
|
||||
|
||||
export type TOrgMemberships = z.infer<typeof OrgMembershipsSchema>;
|
||||
|
@@ -36,7 +36,9 @@ export const OrganizationsSchema = z.object({
|
||||
scannerProductEnabled: z.boolean().default(true).nullable().optional(),
|
||||
shareSecretsProductEnabled: z.boolean().default(true).nullable().optional(),
|
||||
maxSharedSecretLifetime: z.number().default(2592000).nullable().optional(),
|
||||
maxSharedSecretViewLimit: z.number().nullable().optional()
|
||||
maxSharedSecretViewLimit: z.number().nullable().optional(),
|
||||
googleSsoAuthEnforced: z.boolean().default(false),
|
||||
googleSsoAuthLastUsed: z.date().nullable().optional()
|
||||
});
|
||||
|
||||
export type TOrganizations = z.infer<typeof OrganizationsSchema>;
|
||||
|
@@ -25,7 +25,8 @@ export const PkiSubscribersSchema = z.object({
|
||||
lastAutoRenewAt: z.date().nullable().optional(),
|
||||
lastOperationStatus: z.string().nullable().optional(),
|
||||
lastOperationMessage: z.string().nullable().optional(),
|
||||
lastOperationAt: z.date().nullable().optional()
|
||||
lastOperationAt: z.date().nullable().optional(),
|
||||
properties: z.unknown().nullable().optional()
|
||||
});
|
||||
|
||||
export type TPkiSubscribers = z.infer<typeof PkiSubscribersSchema>;
|
||||
|
@@ -14,7 +14,8 @@ export const RemindersSchema = z.object({
|
||||
repeatDays: z.number().nullable().optional(),
|
||||
nextReminderDate: z.date(),
|
||||
createdAt: z.date(),
|
||||
updatedAt: z.date()
|
||||
updatedAt: z.date(),
|
||||
fromDate: z.date().nullable().optional()
|
||||
});
|
||||
|
||||
export type TReminders = z.infer<typeof RemindersSchema>;
|
||||
|
@@ -17,7 +17,8 @@ export const SecretApprovalPoliciesSchema = z.object({
|
||||
updatedAt: z.date(),
|
||||
enforcementLevel: z.string().default("hard"),
|
||||
deletedAt: z.date().nullable().optional(),
|
||||
allowedSelfApprovals: z.boolean().default(true)
|
||||
allowedSelfApprovals: z.boolean().default(true),
|
||||
shouldCheckSecretPermission: z.boolean().nullable().optional()
|
||||
});
|
||||
|
||||
export type TSecretApprovalPolicies = z.infer<typeof SecretApprovalPoliciesSchema>;
|
||||
|
@@ -16,7 +16,8 @@ export const UserAliasesSchema = z.object({
|
||||
emails: z.string().array().nullable().optional(),
|
||||
orgId: z.string().uuid().nullable().optional(),
|
||||
createdAt: z.date(),
|
||||
updatedAt: z.date()
|
||||
updatedAt: z.date(),
|
||||
isEmailVerified: z.boolean().default(false).nullable().optional()
|
||||
});
|
||||
|
||||
export type TUserAliases = z.infer<typeof UserAliasesSchema>;
|
||||
|
@@ -15,12 +15,12 @@ export const UserEncryptionKeysSchema = z.object({
|
||||
protectedKey: z.string().nullable().optional(),
|
||||
protectedKeyIV: z.string().nullable().optional(),
|
||||
protectedKeyTag: z.string().nullable().optional(),
|
||||
publicKey: z.string(),
|
||||
encryptedPrivateKey: z.string(),
|
||||
iv: z.string(),
|
||||
tag: z.string(),
|
||||
salt: z.string(),
|
||||
verifier: z.string(),
|
||||
publicKey: z.string().nullable().optional(),
|
||||
encryptedPrivateKey: z.string().nullable().optional(),
|
||||
iv: z.string().nullable().optional(),
|
||||
tag: z.string().nullable().optional(),
|
||||
salt: z.string().nullable().optional(),
|
||||
verifier: z.string().nullable().optional(),
|
||||
userId: z.string().uuid(),
|
||||
hashedPassword: z.string().nullable().optional(),
|
||||
serverEncryptedPrivateKey: z.string().nullable().optional(),
|
||||
|
@@ -115,6 +115,10 @@ export const generateUserSrpKeys = async (password: string) => {
|
||||
};
|
||||
|
||||
export const getUserPrivateKey = async (password: string, user: TUserEncryptionKeys) => {
|
||||
if (!user.encryptedPrivateKey || !user.iv || !user.tag || !user.salt) {
|
||||
throw new Error("User encrypted private key not found");
|
||||
}
|
||||
|
||||
const derivedKey = await argon2.hash(password, {
|
||||
salt: Buffer.from(user.salt),
|
||||
memoryCost: 65536,
|
||||
|
@@ -1,7 +1,7 @@
|
||||
import { Knex } from "knex";
|
||||
|
||||
import { crypto } from "@app/lib/crypto";
|
||||
import { initLogger } from "@app/lib/logger";
|
||||
import { initEnvConfig } from "@app/lib/config/env";
|
||||
import { initLogger, logger } from "@app/lib/logger";
|
||||
import { superAdminDALFactory } from "@app/services/super-admin/super-admin-dal";
|
||||
|
||||
import { AuthMethod } from "../../services/auth/auth-type";
|
||||
@@ -17,7 +17,7 @@ export async function seed(knex: Knex): Promise<void> {
|
||||
initLogger();
|
||||
|
||||
const superAdminDAL = superAdminDALFactory(knex);
|
||||
await crypto.initialize(superAdminDAL);
|
||||
await initEnvConfig(superAdminDAL, logger);
|
||||
|
||||
await knex(TableName.SuperAdmin).insert([
|
||||
// eslint-disable-next-line
|
||||
@@ -25,6 +25,7 @@ export async function seed(knex: Knex): Promise<void> {
|
||||
{ id: "00000000-0000-0000-0000-000000000000", initialized: true, allowSignUp: true }
|
||||
]);
|
||||
// Inserts seed entries
|
||||
|
||||
const [user] = await knex(TableName.Users)
|
||||
.insert([
|
||||
{
|
||||
|
@@ -1,9 +1,28 @@
|
||||
import { Knex } from "knex";
|
||||
|
||||
import { initEnvConfig } from "@app/lib/config/env";
|
||||
import { crypto, SymmetricKeySize } from "@app/lib/crypto/cryptography";
|
||||
import { generateUserSrpKeys } from "@app/lib/crypto/srp";
|
||||
import { initLogger, logger } from "@app/lib/logger";
|
||||
import { alphaNumericNanoId } from "@app/lib/nanoid";
|
||||
import { AuthMethod } from "@app/services/auth/auth-type";
|
||||
import { assignWorkspaceKeysToMembers, createProjectKey } from "@app/services/project/project-fns";
|
||||
import { projectKeyDALFactory } from "@app/services/project-key/project-key-dal";
|
||||
import { projectMembershipDALFactory } from "@app/services/project-membership/project-membership-dal";
|
||||
import { projectUserMembershipRoleDALFactory } from "@app/services/project-membership/project-user-membership-role-dal";
|
||||
import { superAdminDALFactory } from "@app/services/super-admin/super-admin-dal";
|
||||
import { userDALFactory } from "@app/services/user/user-dal";
|
||||
|
||||
import { ProjectMembershipRole, ProjectType, SecretEncryptionAlgo, SecretKeyEncoding, TableName } from "../schemas";
|
||||
import { buildUserProjectKey, getUserPrivateKey, seedData1 } from "../seed-data";
|
||||
import {
|
||||
OrgMembershipRole,
|
||||
OrgMembershipStatus,
|
||||
ProjectMembershipRole,
|
||||
ProjectType,
|
||||
SecretEncryptionAlgo,
|
||||
SecretKeyEncoding,
|
||||
TableName
|
||||
} from "../schemas";
|
||||
import { seedData1 } from "../seed-data";
|
||||
|
||||
export const DEFAULT_PROJECT_ENVS = [
|
||||
{ name: "Development", slug: "dev" },
|
||||
@@ -11,12 +30,159 @@ export const DEFAULT_PROJECT_ENVS = [
|
||||
{ name: "Production", slug: "prod" }
|
||||
];
|
||||
|
||||
const createUserWithGhostUser = async (
|
||||
orgId: string,
|
||||
projectId: string,
|
||||
userId: string,
|
||||
userOrgMembershipId: string,
|
||||
knex: Knex
|
||||
) => {
|
||||
const projectKeyDAL = projectKeyDALFactory(knex);
|
||||
const userDAL = userDALFactory(knex);
|
||||
const projectMembershipDAL = projectMembershipDALFactory(knex);
|
||||
const projectUserMembershipRoleDAL = projectUserMembershipRoleDALFactory(knex);
|
||||
|
||||
const email = `sudo-${alphaNumericNanoId(16)}-${orgId}@infisical.com`; // We add a nanoid because the email is unique. And we have to create a new ghost user each time, so we can have access to the private key.
|
||||
|
||||
const password = crypto.randomBytes(128).toString("hex");
|
||||
|
||||
const [ghostUser] = await knex(TableName.Users)
|
||||
.insert({
|
||||
isGhost: true,
|
||||
authMethods: [AuthMethod.EMAIL],
|
||||
username: email,
|
||||
email,
|
||||
isAccepted: true
|
||||
})
|
||||
.returning("*");
|
||||
|
||||
const encKeys = await generateUserSrpKeys(email, password);
|
||||
|
||||
await knex(TableName.UserEncryptionKey)
|
||||
.insert({ userId: ghostUser.id, encryptionVersion: 2, publicKey: encKeys.publicKey })
|
||||
.onConflict("userId")
|
||||
.merge();
|
||||
|
||||
await knex(TableName.OrgMembership)
|
||||
.insert({
|
||||
orgId,
|
||||
userId: ghostUser.id,
|
||||
role: OrgMembershipRole.Admin,
|
||||
status: OrgMembershipStatus.Accepted,
|
||||
isActive: true
|
||||
})
|
||||
.returning("*");
|
||||
|
||||
const [projectMembership] = await knex(TableName.ProjectMembership)
|
||||
.insert({
|
||||
userId: ghostUser.id,
|
||||
projectId
|
||||
})
|
||||
.returning("*");
|
||||
|
||||
await knex(TableName.ProjectUserMembershipRole).insert({
|
||||
projectMembershipId: projectMembership.id,
|
||||
role: ProjectMembershipRole.Admin
|
||||
});
|
||||
|
||||
const { key: encryptedProjectKey, iv: encryptedProjectKeyIv } = createProjectKey({
|
||||
publicKey: encKeys.publicKey,
|
||||
privateKey: encKeys.plainPrivateKey
|
||||
});
|
||||
|
||||
await knex(TableName.ProjectKeys).insert({
|
||||
projectId,
|
||||
receiverId: ghostUser.id,
|
||||
encryptedKey: encryptedProjectKey,
|
||||
nonce: encryptedProjectKeyIv,
|
||||
senderId: ghostUser.id
|
||||
});
|
||||
|
||||
const { iv, tag, ciphertext, encoding, algorithm } = crypto
|
||||
.encryption()
|
||||
.symmetric()
|
||||
.encryptWithRootEncryptionKey(encKeys.plainPrivateKey);
|
||||
|
||||
await knex(TableName.ProjectBot).insert({
|
||||
name: "Infisical Bot (Ghost)",
|
||||
projectId,
|
||||
tag,
|
||||
iv,
|
||||
encryptedProjectKey,
|
||||
encryptedProjectKeyNonce: encryptedProjectKeyIv,
|
||||
encryptedPrivateKey: ciphertext,
|
||||
isActive: true,
|
||||
publicKey: encKeys.publicKey,
|
||||
senderId: ghostUser.id,
|
||||
algorithm,
|
||||
keyEncoding: encoding
|
||||
});
|
||||
|
||||
const latestKey = await projectKeyDAL.findLatestProjectKey(ghostUser.id, projectId, knex);
|
||||
|
||||
if (!latestKey) {
|
||||
throw new Error("Latest key not found for user");
|
||||
}
|
||||
|
||||
const user = await userDAL.findUserEncKeyByUserId(userId, knex);
|
||||
|
||||
if (!user || !user.publicKey) {
|
||||
throw new Error("User not found");
|
||||
}
|
||||
|
||||
const [projectAdmin] = assignWorkspaceKeysToMembers({
|
||||
decryptKey: latestKey,
|
||||
userPrivateKey: encKeys.plainPrivateKey,
|
||||
members: [
|
||||
{
|
||||
userPublicKey: user.publicKey,
|
||||
orgMembershipId: userOrgMembershipId
|
||||
}
|
||||
]
|
||||
});
|
||||
|
||||
// Create a membership for the user
|
||||
const userProjectMembership = await projectMembershipDAL.create(
|
||||
{
|
||||
projectId,
|
||||
userId: user.id
|
||||
},
|
||||
knex
|
||||
);
|
||||
await projectUserMembershipRoleDAL.create(
|
||||
{ projectMembershipId: userProjectMembership.id, role: ProjectMembershipRole.Admin },
|
||||
knex
|
||||
);
|
||||
|
||||
// Create a project key for the user
|
||||
await projectKeyDAL.create(
|
||||
{
|
||||
encryptedKey: projectAdmin.workspaceEncryptedKey,
|
||||
nonce: projectAdmin.workspaceEncryptedNonce,
|
||||
senderId: ghostUser.id,
|
||||
receiverId: user.id,
|
||||
projectId
|
||||
},
|
||||
knex
|
||||
);
|
||||
|
||||
return {
|
||||
user: ghostUser,
|
||||
keys: encKeys
|
||||
};
|
||||
};
|
||||
|
||||
export async function seed(knex: Knex): Promise<void> {
|
||||
// Deletes ALL existing entries
|
||||
await knex(TableName.Project).del();
|
||||
await knex(TableName.Environment).del();
|
||||
await knex(TableName.SecretFolder).del();
|
||||
|
||||
initLogger();
|
||||
|
||||
const superAdminDAL = superAdminDALFactory(knex);
|
||||
await initEnvConfig(superAdminDAL, logger);
|
||||
|
||||
const [project] = await knex(TableName.Project)
|
||||
.insert({
|
||||
name: seedData1.project.name,
|
||||
@@ -29,29 +195,24 @@ export async function seed(knex: Knex): Promise<void> {
|
||||
})
|
||||
.returning("*");
|
||||
|
||||
const projectMembership = await knex(TableName.ProjectMembership)
|
||||
.insert({
|
||||
projectId: project.id,
|
||||
const userOrgMembership = await knex(TableName.OrgMembership)
|
||||
.where({
|
||||
orgId: seedData1.organization.id,
|
||||
userId: seedData1.id
|
||||
})
|
||||
.returning("*");
|
||||
await knex(TableName.ProjectUserMembershipRole).insert({
|
||||
role: ProjectMembershipRole.Admin,
|
||||
projectMembershipId: projectMembership[0].id
|
||||
});
|
||||
.first();
|
||||
|
||||
if (!userOrgMembership) {
|
||||
throw new Error("User org membership not found");
|
||||
}
|
||||
const user = await knex(TableName.UserEncryptionKey).where({ userId: seedData1.id }).first();
|
||||
if (!user) throw new Error("User not found");
|
||||
|
||||
const userPrivateKey = await getUserPrivateKey(seedData1.password, user);
|
||||
const projectKey = buildUserProjectKey(userPrivateKey, user.publicKey);
|
||||
await knex(TableName.ProjectKeys).insert({
|
||||
projectId: project.id,
|
||||
nonce: projectKey.nonce,
|
||||
encryptedKey: projectKey.ciphertext,
|
||||
receiverId: seedData1.id,
|
||||
senderId: seedData1.id
|
||||
});
|
||||
if (!user.publicKey) {
|
||||
throw new Error("User public key not found");
|
||||
}
|
||||
|
||||
await createUserWithGhostUser(seedData1.organization.id, project.id, seedData1.id, userOrgMembership.id, knex);
|
||||
|
||||
// create default environments and default folders
|
||||
const envs = await knex(TableName.Environment)
|
||||
|
@@ -1,6 +1,9 @@
|
||||
import { Knex } from "knex";
|
||||
|
||||
import { initEnvConfig } from "@app/lib/config/env";
|
||||
import { crypto } from "@app/lib/crypto/cryptography";
|
||||
import { initLogger, logger } from "@app/lib/logger";
|
||||
import { superAdminDALFactory } from "@app/services/super-admin/super-admin-dal";
|
||||
|
||||
import { IdentityAuthMethod, OrgMembershipRole, ProjectMembershipRole, TableName } from "../schemas";
|
||||
import { seedData1 } from "../seed-data";
|
||||
@@ -10,6 +13,11 @@ export async function seed(knex: Knex): Promise<void> {
|
||||
await knex(TableName.Identity).del();
|
||||
await knex(TableName.IdentityOrgMembership).del();
|
||||
|
||||
initLogger();
|
||||
|
||||
const superAdminDAL = superAdminDALFactory(knex);
|
||||
await initEnvConfig(superAdminDAL, logger);
|
||||
|
||||
// Inserts seed entries
|
||||
await knex(TableName.Identity).insert([
|
||||
{
|
||||
|
@@ -3,12 +3,32 @@ import { z } from "zod";
|
||||
|
||||
import { ApproverType, BypasserType } from "@app/ee/services/access-approval-policy/access-approval-policy-types";
|
||||
import { removeTrailingSlash } from "@app/lib/fn";
|
||||
import { ms } from "@app/lib/ms";
|
||||
import { EnforcementLevel } from "@app/lib/types";
|
||||
import { readLimit, writeLimit } from "@app/server/config/rateLimiter";
|
||||
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
|
||||
import { sapPubSchema } from "@app/server/routes/sanitizedSchemas";
|
||||
import { AuthMode } from "@app/services/auth/auth-type";
|
||||
|
||||
const maxTimePeriodSchema = z
|
||||
.string()
|
||||
.trim()
|
||||
.nullish()
|
||||
.transform((val, ctx) => {
|
||||
if (val === undefined) return undefined;
|
||||
if (!val || val === "permanent") return null;
|
||||
const parsedMs = ms(val);
|
||||
|
||||
if (typeof parsedMs !== "number" || parsedMs <= 0) {
|
||||
ctx.addIssue({
|
||||
code: z.ZodIssueCode.custom,
|
||||
message: "Invalid time period format or value. Must be a positive duration (e.g., '1h', '30m', '2d')."
|
||||
});
|
||||
return z.NEVER;
|
||||
}
|
||||
return val;
|
||||
});
|
||||
|
||||
export const registerAccessApprovalPolicyRouter = async (server: FastifyZodProvider) => {
|
||||
server.route({
|
||||
url: "/",
|
||||
@@ -71,7 +91,8 @@ export const registerAccessApprovalPolicyRouter = async (server: FastifyZodProvi
|
||||
.optional(),
|
||||
approvals: z.number().min(1).default(1),
|
||||
enforcementLevel: z.nativeEnum(EnforcementLevel).default(EnforcementLevel.Hard),
|
||||
allowedSelfApprovals: z.boolean().default(true)
|
||||
allowedSelfApprovals: z.boolean().default(true),
|
||||
maxTimePeriod: maxTimePeriodSchema
|
||||
})
|
||||
.refine(
|
||||
(val) => Boolean(val.environment) || Boolean(val.environments),
|
||||
@@ -124,7 +145,8 @@ export const registerAccessApprovalPolicyRouter = async (server: FastifyZodProvi
|
||||
.array()
|
||||
.nullable()
|
||||
.optional(),
|
||||
bypassers: z.object({ type: z.nativeEnum(BypasserType), id: z.string().nullable().optional() }).array()
|
||||
bypassers: z.object({ type: z.nativeEnum(BypasserType), id: z.string().nullable().optional() }).array(),
|
||||
maxTimePeriod: z.string().nullable().optional()
|
||||
})
|
||||
.array()
|
||||
.nullable()
|
||||
@@ -233,7 +255,8 @@ export const registerAccessApprovalPolicyRouter = async (server: FastifyZodProvi
|
||||
stepNumber: z.number().int()
|
||||
})
|
||||
.array()
|
||||
.optional()
|
||||
.optional(),
|
||||
maxTimePeriod: maxTimePeriodSchema
|
||||
}),
|
||||
response: {
|
||||
200: z.object({
|
||||
@@ -314,7 +337,8 @@ export const registerAccessApprovalPolicyRouter = async (server: FastifyZodProvi
|
||||
})
|
||||
.array()
|
||||
.nullable()
|
||||
.optional()
|
||||
.optional(),
|
||||
maxTimePeriod: z.string().nullable().optional()
|
||||
})
|
||||
})
|
||||
}
|
||||
|
@@ -2,6 +2,7 @@ import { z } from "zod";
|
||||
|
||||
import { AccessApprovalRequestsReviewersSchema, AccessApprovalRequestsSchema, UsersSchema } from "@app/db/schemas";
|
||||
import { ApprovalStatus } from "@app/ee/services/access-approval-request/access-approval-request-types";
|
||||
import { ms } from "@app/lib/ms";
|
||||
import { writeLimit } from "@app/server/config/rateLimiter";
|
||||
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
|
||||
import { AuthMode } from "@app/services/auth/auth-type";
|
||||
@@ -26,7 +27,23 @@ export const registerAccessApprovalRequestRouter = async (server: FastifyZodProv
|
||||
body: z.object({
|
||||
permissions: z.any().array(),
|
||||
isTemporary: z.boolean(),
|
||||
temporaryRange: z.string().optional(),
|
||||
temporaryRange: z
|
||||
.string()
|
||||
.optional()
|
||||
.transform((val, ctx) => {
|
||||
if (!val || val === "permanent") return undefined;
|
||||
|
||||
const parsedMs = ms(val);
|
||||
|
||||
if (typeof parsedMs !== "number" || parsedMs <= 0) {
|
||||
ctx.addIssue({
|
||||
code: z.ZodIssueCode.custom,
|
||||
message: "Invalid time period format or value. Must be a positive duration (e.g., '1h', '30m', '2d')."
|
||||
});
|
||||
return z.NEVER;
|
||||
}
|
||||
return val;
|
||||
}),
|
||||
note: z.string().max(255).optional()
|
||||
}),
|
||||
querystring: z.object({
|
||||
@@ -116,6 +133,7 @@ export const registerAccessApprovalRequestRouter = async (server: FastifyZodProv
|
||||
approvals: z.number(),
|
||||
approvers: z
|
||||
.object({
|
||||
isOrgMembershipActive: z.boolean().nullable().optional(),
|
||||
userId: z.string().nullable().optional(),
|
||||
sequence: z.number().nullable().optional(),
|
||||
approvalsRequired: z.number().nullable().optional(),
|
||||
@@ -128,10 +146,12 @@ export const registerAccessApprovalRequestRouter = async (server: FastifyZodProv
|
||||
envId: z.string(),
|
||||
enforcementLevel: z.string(),
|
||||
deletedAt: z.date().nullish(),
|
||||
allowedSelfApprovals: z.boolean()
|
||||
allowedSelfApprovals: z.boolean(),
|
||||
maxTimePeriod: z.string().nullable().optional()
|
||||
}),
|
||||
reviewers: z
|
||||
.object({
|
||||
isOrgMembershipActive: z.boolean().nullable().optional(),
|
||||
userId: z.string(),
|
||||
status: z.string()
|
||||
})
|
||||
@@ -189,4 +209,47 @@ export const registerAccessApprovalRequestRouter = async (server: FastifyZodProv
|
||||
return { review };
|
||||
}
|
||||
});
|
||||
|
||||
server.route({
|
||||
url: "/:requestId",
|
||||
method: "PATCH",
|
||||
schema: {
|
||||
params: z.object({
|
||||
requestId: z.string().trim()
|
||||
}),
|
||||
body: z.object({
|
||||
temporaryRange: z.string().transform((val, ctx) => {
|
||||
const parsedMs = ms(val);
|
||||
|
||||
if (typeof parsedMs !== "number" || parsedMs <= 0) {
|
||||
ctx.addIssue({
|
||||
code: z.ZodIssueCode.custom,
|
||||
message: "Invalid time period format or value. Must be a positive duration (e.g., '1h', '30m', '2d')."
|
||||
});
|
||||
return z.NEVER;
|
||||
}
|
||||
return val;
|
||||
}),
|
||||
editNote: z.string().max(255)
|
||||
}),
|
||||
response: {
|
||||
200: z.object({
|
||||
approval: AccessApprovalRequestsSchema
|
||||
})
|
||||
}
|
||||
},
|
||||
onRequest: verifyAuth([AuthMode.JWT]),
|
||||
handler: async (req) => {
|
||||
const { request } = await server.services.accessApprovalRequest.updateAccessApprovalRequest({
|
||||
actor: req.permission.type,
|
||||
actorId: req.permission.id,
|
||||
actorAuthMethod: req.permission.authMethod,
|
||||
actorOrgId: req.permission.orgId,
|
||||
temporaryRange: req.body.temporaryRange,
|
||||
editNote: req.body.editNote,
|
||||
requestId: req.params.requestId
|
||||
});
|
||||
return { approval: request };
|
||||
}
|
||||
});
|
||||
};
|
||||
|
@@ -126,4 +126,39 @@ export const registerGithubOrgSyncRouter = async (server: FastifyZodProvider) =>
|
||||
return { githubOrgSyncConfig };
|
||||
}
|
||||
});
|
||||
|
||||
server.route({
|
||||
url: "/sync-all-teams",
|
||||
method: "POST",
|
||||
config: {
|
||||
rateLimit: writeLimit
|
||||
},
|
||||
onRequest: verifyAuth([AuthMode.JWT]),
|
||||
schema: {
|
||||
response: {
|
||||
200: z.object({
|
||||
totalUsers: z.number(),
|
||||
errors: z.array(z.string()),
|
||||
createdTeams: z.array(z.string()),
|
||||
updatedTeams: z.array(z.string()),
|
||||
removedMemberships: z.number(),
|
||||
syncDuration: z.number()
|
||||
})
|
||||
}
|
||||
},
|
||||
handler: async (req) => {
|
||||
const result = await server.services.githubOrgSync.syncAllTeams({
|
||||
orgPermission: req.permission
|
||||
});
|
||||
|
||||
return {
|
||||
totalUsers: result.totalUsers,
|
||||
errors: result.errors,
|
||||
createdTeams: result.createdTeams,
|
||||
updatedTeams: result.updatedTeams,
|
||||
removedMemberships: result.removedMemberships,
|
||||
syncDuration: result.syncDuration
|
||||
};
|
||||
}
|
||||
});
|
||||
};
|
||||
|
391
backend/src/ee/routes/v1/identity-template-router.ts
Normal file
391
backend/src/ee/routes/v1/identity-template-router.ts
Normal file
@@ -0,0 +1,391 @@
|
||||
import { z } from "zod";
|
||||
|
||||
import { IdentityAuthTemplatesSchema } from "@app/db/schemas/identity-auth-templates";
|
||||
import { EventType } from "@app/ee/services/audit-log/audit-log-types";
|
||||
import {
|
||||
IdentityAuthTemplateMethod,
|
||||
TEMPLATE_SUCCESS_MESSAGES,
|
||||
TEMPLATE_VALIDATION_MESSAGES
|
||||
} from "@app/ee/services/identity-auth-template/identity-auth-template-enums";
|
||||
import { readLimit, writeLimit } from "@app/server/config/rateLimiter";
|
||||
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
|
||||
import { AuthMode } from "@app/services/auth/auth-type";
|
||||
|
||||
const ldapTemplateFieldsSchema = z.object({
|
||||
url: z.string().min(1, TEMPLATE_VALIDATION_MESSAGES.LDAP.URL_REQUIRED),
|
||||
bindDN: z.string().min(1, TEMPLATE_VALIDATION_MESSAGES.LDAP.BIND_DN_REQUIRED),
|
||||
bindPass: z.string().min(1, TEMPLATE_VALIDATION_MESSAGES.LDAP.BIND_PASSWORD_REQUIRED),
|
||||
searchBase: z.string().min(1, TEMPLATE_VALIDATION_MESSAGES.LDAP.SEARCH_BASE_REQUIRED),
|
||||
ldapCaCertificate: z.string().trim().optional()
|
||||
});
|
||||
|
||||
export const registerIdentityTemplateRouter = async (server: FastifyZodProvider) => {
|
||||
server.route({
|
||||
method: "POST",
|
||||
url: "/",
|
||||
config: {
|
||||
rateLimit: writeLimit
|
||||
},
|
||||
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
||||
schema: {
|
||||
hide: false,
|
||||
description: "Create identity auth template",
|
||||
security: [
|
||||
{
|
||||
bearerAuth: []
|
||||
}
|
||||
],
|
||||
body: z.object({
|
||||
name: z
|
||||
.string()
|
||||
.trim()
|
||||
.min(1, TEMPLATE_VALIDATION_MESSAGES.TEMPLATE_NAME_REQUIRED)
|
||||
.max(64, TEMPLATE_VALIDATION_MESSAGES.TEMPLATE_NAME_MAX_LENGTH),
|
||||
authMethod: z.nativeEnum(IdentityAuthTemplateMethod),
|
||||
templateFields: ldapTemplateFieldsSchema
|
||||
}),
|
||||
response: {
|
||||
200: IdentityAuthTemplatesSchema.extend({
|
||||
templateFields: z.record(z.string(), z.unknown())
|
||||
})
|
||||
}
|
||||
},
|
||||
handler: async (req) => {
|
||||
const template = await server.services.identityAuthTemplate.createTemplate({
|
||||
actor: req.permission.type,
|
||||
actorId: req.permission.id,
|
||||
actorAuthMethod: req.permission.authMethod,
|
||||
actorOrgId: req.permission.orgId,
|
||||
name: req.body.name,
|
||||
authMethod: req.body.authMethod,
|
||||
templateFields: req.body.templateFields
|
||||
});
|
||||
|
||||
await server.services.auditLog.createAuditLog({
|
||||
...req.auditLogInfo,
|
||||
orgId: req.permission.orgId,
|
||||
event: {
|
||||
type: EventType.MACHINE_IDENTITY_AUTH_TEMPLATE_CREATE,
|
||||
metadata: {
|
||||
templateId: template.id,
|
||||
name: template.name
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return template;
|
||||
}
|
||||
});
|
||||
|
||||
server.route({
|
||||
method: "PATCH",
|
||||
url: "/:templateId",
|
||||
config: {
|
||||
rateLimit: writeLimit
|
||||
},
|
||||
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
||||
schema: {
|
||||
hide: false,
|
||||
description: "Update identity auth template",
|
||||
security: [
|
||||
{
|
||||
bearerAuth: []
|
||||
}
|
||||
],
|
||||
params: z.object({
|
||||
templateId: z.string().min(1, TEMPLATE_VALIDATION_MESSAGES.TEMPLATE_ID_REQUIRED)
|
||||
}),
|
||||
body: z.object({
|
||||
name: z
|
||||
.string()
|
||||
.trim()
|
||||
.min(1, TEMPLATE_VALIDATION_MESSAGES.TEMPLATE_NAME_REQUIRED)
|
||||
.max(64, TEMPLATE_VALIDATION_MESSAGES.TEMPLATE_NAME_MAX_LENGTH)
|
||||
.optional(),
|
||||
templateFields: ldapTemplateFieldsSchema.partial().optional()
|
||||
}),
|
||||
response: {
|
||||
200: IdentityAuthTemplatesSchema.extend({
|
||||
templateFields: z.record(z.string(), z.unknown())
|
||||
})
|
||||
}
|
||||
},
|
||||
handler: async (req) => {
|
||||
const template = await server.services.identityAuthTemplate.updateTemplate({
|
||||
templateId: req.params.templateId,
|
||||
name: req.body.name,
|
||||
templateFields: req.body.templateFields,
|
||||
actor: req.permission.type,
|
||||
actorId: req.permission.id,
|
||||
actorAuthMethod: req.permission.authMethod,
|
||||
actorOrgId: req.permission.orgId
|
||||
});
|
||||
|
||||
await server.services.auditLog.createAuditLog({
|
||||
...req.auditLogInfo,
|
||||
orgId: req.permission.orgId,
|
||||
event: {
|
||||
type: EventType.MACHINE_IDENTITY_AUTH_TEMPLATE_UPDATE,
|
||||
metadata: {
|
||||
templateId: template.id,
|
||||
name: template.name
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return template;
|
||||
}
|
||||
});
|
||||
|
||||
server.route({
|
||||
method: "DELETE",
|
||||
url: "/:templateId",
|
||||
config: {
|
||||
rateLimit: writeLimit
|
||||
},
|
||||
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
||||
schema: {
|
||||
hide: false,
|
||||
description: "Delete identity auth template",
|
||||
security: [
|
||||
{
|
||||
bearerAuth: []
|
||||
}
|
||||
],
|
||||
params: z.object({
|
||||
templateId: z.string().min(1, TEMPLATE_VALIDATION_MESSAGES.TEMPLATE_ID_REQUIRED)
|
||||
}),
|
||||
response: {
|
||||
200: z.object({
|
||||
message: z.string()
|
||||
})
|
||||
}
|
||||
},
|
||||
handler: async (req) => {
|
||||
const template = await server.services.identityAuthTemplate.deleteTemplate({
|
||||
templateId: req.params.templateId,
|
||||
actor: req.permission.type,
|
||||
actorId: req.permission.id,
|
||||
actorAuthMethod: req.permission.authMethod,
|
||||
actorOrgId: req.permission.orgId
|
||||
});
|
||||
|
||||
await server.services.auditLog.createAuditLog({
|
||||
...req.auditLogInfo,
|
||||
orgId: req.permission.orgId,
|
||||
event: {
|
||||
type: EventType.MACHINE_IDENTITY_AUTH_TEMPLATE_DELETE,
|
||||
metadata: {
|
||||
templateId: template.id,
|
||||
name: template.name
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return { message: TEMPLATE_SUCCESS_MESSAGES.DELETED };
|
||||
}
|
||||
});
|
||||
|
||||
server.route({
|
||||
method: "GET",
|
||||
url: "/:templateId",
|
||||
config: {
|
||||
rateLimit: readLimit
|
||||
},
|
||||
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
||||
schema: {
|
||||
hide: false,
|
||||
description: "Get identity auth template by ID",
|
||||
security: [
|
||||
{
|
||||
bearerAuth: []
|
||||
}
|
||||
],
|
||||
params: z.object({
|
||||
templateId: z.string().min(1, TEMPLATE_VALIDATION_MESSAGES.TEMPLATE_ID_REQUIRED)
|
||||
}),
|
||||
response: {
|
||||
200: IdentityAuthTemplatesSchema.extend({
|
||||
templateFields: ldapTemplateFieldsSchema
|
||||
})
|
||||
}
|
||||
},
|
||||
handler: async (req) => {
|
||||
const template = await server.services.identityAuthTemplate.getTemplate({
|
||||
templateId: req.params.templateId,
|
||||
actor: req.permission.type,
|
||||
actorId: req.permission.id,
|
||||
actorAuthMethod: req.permission.authMethod,
|
||||
actorOrgId: req.permission.orgId
|
||||
});
|
||||
|
||||
return template;
|
||||
}
|
||||
});
|
||||
|
||||
server.route({
|
||||
method: "GET",
|
||||
url: "/search",
|
||||
config: {
|
||||
rateLimit: readLimit
|
||||
},
|
||||
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
||||
schema: {
|
||||
hide: false,
|
||||
description: "List identity auth templates",
|
||||
security: [
|
||||
{
|
||||
bearerAuth: []
|
||||
}
|
||||
],
|
||||
querystring: z.object({
|
||||
limit: z.coerce.number().positive().max(100).default(5).optional(),
|
||||
offset: z.coerce.number().min(0).default(0).optional(),
|
||||
search: z.string().optional()
|
||||
}),
|
||||
response: {
|
||||
200: z.object({
|
||||
templates: IdentityAuthTemplatesSchema.extend({
|
||||
templateFields: ldapTemplateFieldsSchema
|
||||
}).array(),
|
||||
totalCount: z.number()
|
||||
})
|
||||
}
|
||||
},
|
||||
handler: async (req) => {
|
||||
const { templates, totalCount } = await server.services.identityAuthTemplate.listTemplates({
|
||||
limit: req.query.limit,
|
||||
offset: req.query.offset,
|
||||
search: req.query.search,
|
||||
actor: req.permission.type,
|
||||
actorId: req.permission.id,
|
||||
actorAuthMethod: req.permission.authMethod,
|
||||
actorOrgId: req.permission.orgId
|
||||
});
|
||||
|
||||
return { templates, totalCount };
|
||||
}
|
||||
});
|
||||
|
||||
server.route({
|
||||
method: "GET",
|
||||
url: "/",
|
||||
config: {
|
||||
rateLimit: readLimit
|
||||
},
|
||||
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
||||
schema: {
|
||||
hide: false,
|
||||
description: "Get identity auth templates by authentication method",
|
||||
security: [
|
||||
{
|
||||
bearerAuth: []
|
||||
}
|
||||
],
|
||||
querystring: z.object({
|
||||
authMethod: z.nativeEnum(IdentityAuthTemplateMethod)
|
||||
}),
|
||||
response: {
|
||||
200: IdentityAuthTemplatesSchema.extend({
|
||||
templateFields: ldapTemplateFieldsSchema
|
||||
}).array()
|
||||
}
|
||||
},
|
||||
handler: async (req) => {
|
||||
const templates = await server.services.identityAuthTemplate.getTemplatesByAuthMethod({
|
||||
authMethod: req.query.authMethod,
|
||||
actor: req.permission.type,
|
||||
actorId: req.permission.id,
|
||||
actorAuthMethod: req.permission.authMethod,
|
||||
actorOrgId: req.permission.orgId
|
||||
});
|
||||
|
||||
return templates;
|
||||
}
|
||||
});
|
||||
|
||||
server.route({
|
||||
method: "GET",
|
||||
url: "/:templateId/usage",
|
||||
config: {
|
||||
rateLimit: readLimit
|
||||
},
|
||||
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
||||
schema: {
|
||||
hide: false,
|
||||
description: "Get template usage by template ID",
|
||||
security: [
|
||||
{
|
||||
bearerAuth: []
|
||||
}
|
||||
],
|
||||
params: z.object({
|
||||
templateId: z.string()
|
||||
}),
|
||||
response: {
|
||||
200: z
|
||||
.object({
|
||||
identityId: z.string(),
|
||||
identityName: z.string()
|
||||
})
|
||||
.array()
|
||||
}
|
||||
},
|
||||
handler: async (req) => {
|
||||
const templates = await server.services.identityAuthTemplate.findTemplateUsages({
|
||||
templateId: req.params.templateId,
|
||||
actor: req.permission.type,
|
||||
actorId: req.permission.id,
|
||||
actorAuthMethod: req.permission.authMethod,
|
||||
actorOrgId: req.permission.orgId
|
||||
});
|
||||
|
||||
return templates;
|
||||
}
|
||||
});
|
||||
|
||||
server.route({
|
||||
method: "POST",
|
||||
url: "/:templateId/delete-usage",
|
||||
config: {
|
||||
rateLimit: writeLimit
|
||||
},
|
||||
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
||||
schema: {
|
||||
hide: false,
|
||||
description: "Unlink identity auth template usage",
|
||||
security: [
|
||||
{
|
||||
bearerAuth: []
|
||||
}
|
||||
],
|
||||
params: z.object({
|
||||
templateId: z.string()
|
||||
}),
|
||||
body: z.object({
|
||||
identityIds: z.string().array()
|
||||
}),
|
||||
response: {
|
||||
200: z
|
||||
.object({
|
||||
authId: z.string(),
|
||||
identityId: z.string(),
|
||||
identityName: z.string()
|
||||
})
|
||||
.array()
|
||||
}
|
||||
},
|
||||
handler: async (req) => {
|
||||
const templates = await server.services.identityAuthTemplate.unlinkTemplateUsage({
|
||||
templateId: req.params.templateId,
|
||||
identityIds: req.body.identityIds,
|
||||
actor: req.permission.type,
|
||||
actorId: req.permission.id,
|
||||
actorAuthMethod: req.permission.authMethod,
|
||||
actorOrgId: req.permission.orgId
|
||||
});
|
||||
|
||||
return templates;
|
||||
}
|
||||
});
|
||||
};
|
@@ -13,6 +13,7 @@ import { registerGatewayRouter } from "./gateway-router";
|
||||
import { registerGithubOrgSyncRouter } from "./github-org-sync-router";
|
||||
import { registerGroupRouter } from "./group-router";
|
||||
import { registerIdentityProjectAdditionalPrivilegeRouter } from "./identity-project-additional-privilege-router";
|
||||
import { registerIdentityTemplateRouter } from "./identity-template-router";
|
||||
import { registerKmipRouter } from "./kmip-router";
|
||||
import { registerKmipSpecRouter } from "./kmip-spec-router";
|
||||
import { registerLdapRouter } from "./ldap-router";
|
||||
@@ -125,6 +126,7 @@ export const registerV1EERoutes = async (server: FastifyZodProvider) => {
|
||||
await server.register(registerExternalKmsRouter, {
|
||||
prefix: "/external-kms"
|
||||
});
|
||||
await server.register(registerIdentityTemplateRouter, { prefix: "/identity-templates" });
|
||||
|
||||
await server.register(registerProjectTemplateRouter, { prefix: "/project-templates" });
|
||||
|
||||
|
@@ -379,14 +379,17 @@ export const registerLdapRouter = async (server: FastifyZodProvider) => {
|
||||
|
||||
server.route({
|
||||
method: "POST",
|
||||
url: "/config/:configId/test-connection",
|
||||
url: "/config/test-connection",
|
||||
config: {
|
||||
rateLimit: readLimit
|
||||
},
|
||||
onRequest: verifyAuth([AuthMode.JWT]),
|
||||
schema: {
|
||||
params: z.object({
|
||||
configId: z.string().trim()
|
||||
body: z.object({
|
||||
url: z.string().trim(),
|
||||
bindDN: z.string().trim(),
|
||||
bindPass: z.string().trim(),
|
||||
caCert: z.string().trim()
|
||||
}),
|
||||
response: {
|
||||
200: z.boolean()
|
||||
@@ -399,8 +402,9 @@ export const registerLdapRouter = async (server: FastifyZodProvider) => {
|
||||
orgId: req.permission.orgId,
|
||||
actorAuthMethod: req.permission.authMethod,
|
||||
actorOrgId: req.permission.orgId,
|
||||
ldapConfigId: req.params.configId
|
||||
...req.body
|
||||
});
|
||||
|
||||
return result;
|
||||
}
|
||||
});
|
||||
|
@@ -294,22 +294,30 @@ export const registerSecretApprovalRequestRouter = async (server: FastifyZodProv
|
||||
200: z.object({
|
||||
approval: SecretApprovalRequestsSchema.merge(
|
||||
z.object({
|
||||
// secretPath: z.string(),
|
||||
policy: z.object({
|
||||
id: z.string(),
|
||||
name: z.string(),
|
||||
approvals: z.number(),
|
||||
approvers: approvalRequestUser.array(),
|
||||
approvers: approvalRequestUser
|
||||
.extend({ isOrgMembershipActive: z.boolean().nullable().optional() })
|
||||
.array(),
|
||||
bypassers: approvalRequestUser.array(),
|
||||
secretPath: z.string().optional().nullable(),
|
||||
enforcementLevel: z.string(),
|
||||
deletedAt: z.date().nullish(),
|
||||
allowedSelfApprovals: z.boolean()
|
||||
allowedSelfApprovals: z.boolean(),
|
||||
shouldCheckSecretPermission: z.boolean().nullable().optional()
|
||||
}),
|
||||
environment: z.string(),
|
||||
statusChangedByUser: approvalRequestUser.optional(),
|
||||
committerUser: approvalRequestUser.nullish(),
|
||||
reviewers: approvalRequestUser.extend({ status: z.string(), comment: z.string().optional() }).array(),
|
||||
reviewers: approvalRequestUser
|
||||
.extend({
|
||||
status: z.string(),
|
||||
comment: z.string().optional(),
|
||||
isOrgMembershipActive: z.boolean().nullable().optional()
|
||||
})
|
||||
.array(),
|
||||
secretPath: z.string(),
|
||||
commits: secretRawSchema
|
||||
.omit({ _id: true, environment: true, workspace: true, type: true, version: true, secretValue: true })
|
||||
|
@@ -56,6 +56,7 @@ export interface TAccessApprovalPolicyDALFactory
|
||||
allowedSelfApprovals: boolean;
|
||||
secretPath: string;
|
||||
deletedAt?: Date | null | undefined;
|
||||
maxTimePeriod?: string | null;
|
||||
projectId: string;
|
||||
bypassers: (
|
||||
| {
|
||||
@@ -96,6 +97,7 @@ export interface TAccessApprovalPolicyDALFactory
|
||||
allowedSelfApprovals: boolean;
|
||||
secretPath: string;
|
||||
deletedAt?: Date | null | undefined;
|
||||
maxTimePeriod?: string | null;
|
||||
environments: {
|
||||
id: string;
|
||||
name: string;
|
||||
@@ -141,6 +143,7 @@ export interface TAccessApprovalPolicyDALFactory
|
||||
allowedSelfApprovals: boolean;
|
||||
secretPath: string;
|
||||
deletedAt?: Date | null | undefined;
|
||||
maxTimePeriod?: string | null;
|
||||
}
|
||||
| undefined
|
||||
>;
|
||||
|
@@ -100,7 +100,8 @@ export const accessApprovalPolicyServiceFactory = ({
|
||||
environments,
|
||||
enforcementLevel,
|
||||
allowedSelfApprovals,
|
||||
approvalsRequired
|
||||
approvalsRequired,
|
||||
maxTimePeriod
|
||||
}) => {
|
||||
const project = await projectDAL.findProjectBySlug(projectSlug, actorOrgId);
|
||||
if (!project) throw new NotFoundError({ message: `Project with slug '${projectSlug}' not found` });
|
||||
@@ -219,7 +220,8 @@ export const accessApprovalPolicyServiceFactory = ({
|
||||
secretPath,
|
||||
name,
|
||||
enforcementLevel,
|
||||
allowedSelfApprovals
|
||||
allowedSelfApprovals,
|
||||
maxTimePeriod
|
||||
},
|
||||
tx
|
||||
);
|
||||
@@ -318,7 +320,8 @@ export const accessApprovalPolicyServiceFactory = ({
|
||||
enforcementLevel,
|
||||
allowedSelfApprovals,
|
||||
approvalsRequired,
|
||||
environments
|
||||
environments,
|
||||
maxTimePeriod
|
||||
}: TUpdateAccessApprovalPolicy) => {
|
||||
const groupApprovers = approvers.filter((approver) => approver.type === ApproverType.Group);
|
||||
|
||||
@@ -461,7 +464,8 @@ export const accessApprovalPolicyServiceFactory = ({
|
||||
secretPath,
|
||||
name,
|
||||
enforcementLevel,
|
||||
allowedSelfApprovals
|
||||
allowedSelfApprovals,
|
||||
maxTimePeriod
|
||||
},
|
||||
tx
|
||||
);
|
||||
|
@@ -41,6 +41,7 @@ export type TCreateAccessApprovalPolicy = {
|
||||
enforcementLevel: EnforcementLevel;
|
||||
allowedSelfApprovals: boolean;
|
||||
approvalsRequired?: { numberOfApprovals: number; stepNumber: number }[];
|
||||
maxTimePeriod?: string | null;
|
||||
} & Omit<TProjectPermission, "projectId">;
|
||||
|
||||
export type TUpdateAccessApprovalPolicy = {
|
||||
@@ -60,6 +61,7 @@ export type TUpdateAccessApprovalPolicy = {
|
||||
allowedSelfApprovals: boolean;
|
||||
approvalsRequired?: { numberOfApprovals: number; stepNumber: number }[];
|
||||
environments?: string[];
|
||||
maxTimePeriod?: string | null;
|
||||
} & Omit<TProjectPermission, "projectId">;
|
||||
|
||||
export type TDeleteAccessApprovalPolicy = {
|
||||
@@ -104,7 +106,8 @@ export interface TAccessApprovalPolicyServiceFactory {
|
||||
environment,
|
||||
enforcementLevel,
|
||||
allowedSelfApprovals,
|
||||
approvalsRequired
|
||||
approvalsRequired,
|
||||
maxTimePeriod
|
||||
}: TCreateAccessApprovalPolicy) => Promise<{
|
||||
environment: {
|
||||
name: string;
|
||||
@@ -135,6 +138,7 @@ export interface TAccessApprovalPolicyServiceFactory {
|
||||
allowedSelfApprovals: boolean;
|
||||
secretPath: string;
|
||||
deletedAt?: Date | null | undefined;
|
||||
maxTimePeriod?: string | null;
|
||||
}>;
|
||||
deleteAccessApprovalPolicy: ({
|
||||
policyId,
|
||||
@@ -159,6 +163,7 @@ export interface TAccessApprovalPolicyServiceFactory {
|
||||
allowedSelfApprovals: boolean;
|
||||
secretPath: string;
|
||||
deletedAt?: Date | null | undefined;
|
||||
maxTimePeriod?: string | null;
|
||||
environment: {
|
||||
id: string;
|
||||
name: string;
|
||||
@@ -185,7 +190,8 @@ export interface TAccessApprovalPolicyServiceFactory {
|
||||
enforcementLevel,
|
||||
allowedSelfApprovals,
|
||||
approvalsRequired,
|
||||
environments
|
||||
environments,
|
||||
maxTimePeriod
|
||||
}: TUpdateAccessApprovalPolicy) => Promise<{
|
||||
environment: {
|
||||
id: string;
|
||||
@@ -208,6 +214,7 @@ export interface TAccessApprovalPolicyServiceFactory {
|
||||
allowedSelfApprovals: boolean;
|
||||
secretPath?: string | null | undefined;
|
||||
deletedAt?: Date | null | undefined;
|
||||
maxTimePeriod?: string | null;
|
||||
}>;
|
||||
getAccessApprovalPolicyByProjectSlug: ({
|
||||
actorId,
|
||||
@@ -242,6 +249,7 @@ export interface TAccessApprovalPolicyServiceFactory {
|
||||
allowedSelfApprovals: boolean;
|
||||
secretPath: string;
|
||||
deletedAt?: Date | null | undefined;
|
||||
maxTimePeriod?: string | null;
|
||||
environment: {
|
||||
id: string;
|
||||
name: string;
|
||||
@@ -298,6 +306,7 @@ export interface TAccessApprovalPolicyServiceFactory {
|
||||
allowedSelfApprovals: boolean;
|
||||
secretPath: string;
|
||||
deletedAt?: Date | null | undefined;
|
||||
maxTimePeriod?: string | null;
|
||||
environment: {
|
||||
id: string;
|
||||
name: string;
|
||||
|
@@ -5,6 +5,7 @@ import {
|
||||
AccessApprovalRequestsSchema,
|
||||
TableName,
|
||||
TAccessApprovalRequests,
|
||||
TOrgMemberships,
|
||||
TUserGroupMembership,
|
||||
TUsers
|
||||
} from "@app/db/schemas";
|
||||
@@ -63,6 +64,7 @@ export interface TAccessApprovalRequestDALFactory extends Omit<TOrmify<TableName
|
||||
enforcementLevel: string;
|
||||
allowedSelfApprovals: boolean;
|
||||
deletedAt: Date | null | undefined;
|
||||
maxTimePeriod?: string | null;
|
||||
};
|
||||
projectId: string;
|
||||
environments: string[];
|
||||
@@ -143,6 +145,7 @@ export interface TAccessApprovalRequestDALFactory extends Omit<TOrmify<TableName
|
||||
approvalsRequired: number | null | undefined;
|
||||
email: string | null | undefined;
|
||||
username: string;
|
||||
isOrgMembershipActive: boolean;
|
||||
}
|
||||
| {
|
||||
userId: string;
|
||||
@@ -150,6 +153,7 @@ export interface TAccessApprovalRequestDALFactory extends Omit<TOrmify<TableName
|
||||
approvalsRequired: number | null | undefined;
|
||||
email: string | null | undefined;
|
||||
username: string;
|
||||
isOrgMembershipActive: boolean;
|
||||
}
|
||||
)[];
|
||||
bypassers: string[];
|
||||
@@ -161,6 +165,7 @@ export interface TAccessApprovalRequestDALFactory extends Omit<TOrmify<TableName
|
||||
allowedSelfApprovals: boolean;
|
||||
envId: string;
|
||||
deletedAt: Date | null | undefined;
|
||||
maxTimePeriod?: string | null;
|
||||
};
|
||||
projectId: string;
|
||||
environment: string;
|
||||
@@ -200,6 +205,7 @@ export interface TAccessApprovalRequestDALFactory extends Omit<TOrmify<TableName
|
||||
reviewers: {
|
||||
userId: string;
|
||||
status: string;
|
||||
isOrgMembershipActive: boolean;
|
||||
}[];
|
||||
approvers: (
|
||||
| {
|
||||
@@ -208,6 +214,7 @@ export interface TAccessApprovalRequestDALFactory extends Omit<TOrmify<TableName
|
||||
approvalsRequired: number | null | undefined;
|
||||
email: string | null | undefined;
|
||||
username: string;
|
||||
isOrgMembershipActive: boolean;
|
||||
}
|
||||
| {
|
||||
userId: string;
|
||||
@@ -215,6 +222,7 @@ export interface TAccessApprovalRequestDALFactory extends Omit<TOrmify<TableName
|
||||
approvalsRequired: number | null | undefined;
|
||||
email: string | null | undefined;
|
||||
username: string;
|
||||
isOrgMembershipActive: boolean;
|
||||
}
|
||||
)[];
|
||||
bypassers: string[];
|
||||
@@ -286,6 +294,24 @@ export const accessApprovalRequestDALFactory = (db: TDbClient): TAccessApprovalR
|
||||
`requestedByUser.id`
|
||||
)
|
||||
|
||||
.leftJoin<TOrgMemberships>(
|
||||
db(TableName.OrgMembership).as("approverOrgMembership"),
|
||||
`${TableName.AccessApprovalPolicyApprover}.approverUserId`,
|
||||
`approverOrgMembership.userId`
|
||||
)
|
||||
|
||||
.leftJoin<TOrgMemberships>(
|
||||
db(TableName.OrgMembership).as("approverGroupOrgMembership"),
|
||||
`${TableName.Users}.id`,
|
||||
`approverGroupOrgMembership.userId`
|
||||
)
|
||||
|
||||
.leftJoin<TOrgMemberships>(
|
||||
db(TableName.OrgMembership).as("reviewerOrgMembership"),
|
||||
`${TableName.AccessApprovalRequestReviewer}.reviewerUserId`,
|
||||
`reviewerOrgMembership.userId`
|
||||
)
|
||||
|
||||
.leftJoin(TableName.Environment, `${TableName.AccessApprovalPolicy}.envId`, `${TableName.Environment}.id`)
|
||||
|
||||
.select(selectAllTableCols(TableName.AccessApprovalRequest))
|
||||
@@ -297,7 +323,12 @@ export const accessApprovalRequestDALFactory = (db: TDbClient): TAccessApprovalR
|
||||
db.ref("enforcementLevel").withSchema(TableName.AccessApprovalPolicy).as("policyEnforcementLevel"),
|
||||
db.ref("allowedSelfApprovals").withSchema(TableName.AccessApprovalPolicy).as("policyAllowedSelfApprovals"),
|
||||
db.ref("envId").withSchema(TableName.AccessApprovalPolicy).as("policyEnvId"),
|
||||
db.ref("deletedAt").withSchema(TableName.AccessApprovalPolicy).as("policyDeletedAt")
|
||||
db.ref("deletedAt").withSchema(TableName.AccessApprovalPolicy).as("policyDeletedAt"),
|
||||
|
||||
db.ref("isActive").withSchema("approverOrgMembership").as("approverIsOrgMembershipActive"),
|
||||
db.ref("isActive").withSchema("approverGroupOrgMembership").as("approverGroupIsOrgMembershipActive"),
|
||||
db.ref("isActive").withSchema("reviewerOrgMembership").as("reviewerIsOrgMembershipActive"),
|
||||
db.ref("maxTimePeriod").withSchema(TableName.AccessApprovalPolicy).as("policyMaxTimePeriod")
|
||||
)
|
||||
.select(db.ref("approverUserId").withSchema(TableName.AccessApprovalPolicyApprover))
|
||||
.select(db.ref("sequence").withSchema(TableName.AccessApprovalPolicyApprover).as("approverSequence"))
|
||||
@@ -364,7 +395,8 @@ export const accessApprovalRequestDALFactory = (db: TDbClient): TAccessApprovalR
|
||||
enforcementLevel: doc.policyEnforcementLevel,
|
||||
allowedSelfApprovals: doc.policyAllowedSelfApprovals,
|
||||
envId: doc.policyEnvId,
|
||||
deletedAt: doc.policyDeletedAt
|
||||
deletedAt: doc.policyDeletedAt,
|
||||
maxTimePeriod: doc.policyMaxTimePeriod
|
||||
},
|
||||
requestedByUser: {
|
||||
userId: doc.requestedByUserId,
|
||||
@@ -392,17 +424,26 @@ export const accessApprovalRequestDALFactory = (db: TDbClient): TAccessApprovalR
|
||||
{
|
||||
key: "reviewerUserId",
|
||||
label: "reviewers" as const,
|
||||
mapper: ({ reviewerUserId: userId, reviewerStatus: status }) => (userId ? { userId, status } : undefined)
|
||||
mapper: ({ reviewerUserId: userId, reviewerStatus: status, reviewerIsOrgMembershipActive }) =>
|
||||
userId ? { userId, status, isOrgMembershipActive: reviewerIsOrgMembershipActive } : undefined
|
||||
},
|
||||
{
|
||||
key: "approverUserId",
|
||||
label: "approvers" as const,
|
||||
mapper: ({ approverUserId, approverSequence, approvalsRequired, approverUsername, approverEmail }) => ({
|
||||
mapper: ({
|
||||
approverUserId,
|
||||
approverSequence,
|
||||
approvalsRequired,
|
||||
approverUsername,
|
||||
approverEmail,
|
||||
approverIsOrgMembershipActive
|
||||
}) => ({
|
||||
userId: approverUserId,
|
||||
sequence: approverSequence,
|
||||
approvalsRequired,
|
||||
email: approverEmail,
|
||||
username: approverUsername
|
||||
username: approverUsername,
|
||||
isOrgMembershipActive: approverIsOrgMembershipActive
|
||||
})
|
||||
},
|
||||
{
|
||||
@@ -413,13 +454,15 @@ export const accessApprovalRequestDALFactory = (db: TDbClient): TAccessApprovalR
|
||||
approverSequence,
|
||||
approvalsRequired,
|
||||
approverGroupEmail,
|
||||
approverGroupUsername
|
||||
approverGroupUsername,
|
||||
approverGroupIsOrgMembershipActive
|
||||
}) => ({
|
||||
userId: approverGroupUserId,
|
||||
sequence: approverSequence,
|
||||
approvalsRequired,
|
||||
email: approverGroupEmail,
|
||||
username: approverGroupUsername
|
||||
username: approverGroupUsername,
|
||||
isOrgMembershipActive: approverGroupIsOrgMembershipActive
|
||||
})
|
||||
},
|
||||
{ key: "bypasserUserId", label: "bypassers" as const, mapper: ({ bypasserUserId }) => bypasserUserId },
|
||||
@@ -574,7 +617,8 @@ export const accessApprovalRequestDALFactory = (db: TDbClient): TAccessApprovalR
|
||||
tx.ref("enforcementLevel").withSchema(TableName.AccessApprovalPolicy).as("policyEnforcementLevel"),
|
||||
tx.ref("allowedSelfApprovals").withSchema(TableName.AccessApprovalPolicy).as("policyAllowedSelfApprovals"),
|
||||
tx.ref("approvals").withSchema(TableName.AccessApprovalPolicy).as("policyApprovals"),
|
||||
tx.ref("deletedAt").withSchema(TableName.AccessApprovalPolicy).as("policyDeletedAt")
|
||||
tx.ref("deletedAt").withSchema(TableName.AccessApprovalPolicy).as("policyDeletedAt"),
|
||||
tx.ref("maxTimePeriod").withSchema(TableName.AccessApprovalPolicy).as("policyMaxTimePeriod")
|
||||
);
|
||||
|
||||
const findById: TAccessApprovalRequestDALFactory["findById"] = async (id, tx) => {
|
||||
@@ -595,7 +639,8 @@ export const accessApprovalRequestDALFactory = (db: TDbClient): TAccessApprovalR
|
||||
secretPath: el.policySecretPath,
|
||||
enforcementLevel: el.policyEnforcementLevel,
|
||||
allowedSelfApprovals: el.policyAllowedSelfApprovals,
|
||||
deletedAt: el.policyDeletedAt
|
||||
deletedAt: el.policyDeletedAt,
|
||||
maxTimePeriod: el.policyMaxTimePeriod
|
||||
},
|
||||
requestedByUser: {
|
||||
userId: el.requestedByUserId,
|
||||
|
@@ -54,7 +54,7 @@ type TSecretApprovalRequestServiceFactoryDep = {
|
||||
accessApprovalPolicyDAL: Pick<TAccessApprovalPolicyDALFactory, "findOne" | "find" | "findLastValidPolicy">;
|
||||
accessApprovalRequestReviewerDAL: Pick<
|
||||
TAccessApprovalRequestReviewerDALFactory,
|
||||
"create" | "find" | "findOne" | "transaction"
|
||||
"create" | "find" | "findOne" | "transaction" | "delete"
|
||||
>;
|
||||
groupDAL: Pick<TGroupDALFactory, "findAllGroupPossibleMembers">;
|
||||
projectMembershipDAL: Pick<TProjectMembershipDALFactory, "findById">;
|
||||
@@ -156,6 +156,15 @@ export const accessApprovalRequestServiceFactory = ({
|
||||
throw new BadRequestError({ message: "The policy linked to this request has been deleted" });
|
||||
}
|
||||
|
||||
// Check if the requested time falls under policy.maxTimePeriod
|
||||
if (policy.maxTimePeriod) {
|
||||
if (!temporaryRange || ms(temporaryRange) > ms(policy.maxTimePeriod)) {
|
||||
throw new BadRequestError({
|
||||
message: `Requested access time range is limited to ${policy.maxTimePeriod} by policy`
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
const approverIds: string[] = [];
|
||||
const approverGroupIds: string[] = [];
|
||||
|
||||
@@ -292,6 +301,155 @@ export const accessApprovalRequestServiceFactory = ({
|
||||
return { request: approval };
|
||||
};
|
||||
|
||||
const updateAccessApprovalRequest: TAccessApprovalRequestServiceFactory["updateAccessApprovalRequest"] = async ({
|
||||
temporaryRange,
|
||||
actorId,
|
||||
actor,
|
||||
actorOrgId,
|
||||
actorAuthMethod,
|
||||
editNote,
|
||||
requestId
|
||||
}) => {
|
||||
const cfg = getConfig();
|
||||
|
||||
const accessApprovalRequest = await accessApprovalRequestDAL.findById(requestId);
|
||||
if (!accessApprovalRequest) {
|
||||
throw new NotFoundError({ message: `Access request with ID '${requestId}' not found` });
|
||||
}
|
||||
|
||||
const { policy, requestedByUser } = accessApprovalRequest;
|
||||
if (policy.deletedAt) {
|
||||
throw new BadRequestError({
|
||||
message: "The policy associated with this access request has been deleted."
|
||||
});
|
||||
}
|
||||
|
||||
const { membership, hasRole } = await permissionService.getProjectPermission({
|
||||
actor,
|
||||
actorId,
|
||||
projectId: accessApprovalRequest.projectId,
|
||||
actorAuthMethod,
|
||||
actorOrgId,
|
||||
actionProjectType: ActionProjectType.SecretManager
|
||||
});
|
||||
|
||||
if (!membership) {
|
||||
throw new ForbiddenRequestError({ message: "You are not a member of this project" });
|
||||
}
|
||||
|
||||
const isApprover = policy.approvers.find((approver) => approver.userId === actorId);
|
||||
|
||||
if (!hasRole(ProjectMembershipRole.Admin) && !isApprover) {
|
||||
throw new ForbiddenRequestError({ message: "You are not authorized to modify this request" });
|
||||
}
|
||||
|
||||
const project = await projectDAL.findById(accessApprovalRequest.projectId);
|
||||
|
||||
if (!project) {
|
||||
throw new NotFoundError({
|
||||
message: `The project associated with this access request was not found. [projectId=${accessApprovalRequest.projectId}]`
|
||||
});
|
||||
}
|
||||
|
||||
if (accessApprovalRequest.status !== ApprovalStatus.PENDING) {
|
||||
throw new BadRequestError({ message: "The request has been closed" });
|
||||
}
|
||||
|
||||
const editedByUser = await userDAL.findById(actorId);
|
||||
|
||||
if (!editedByUser) throw new NotFoundError({ message: "Editing user not found" });
|
||||
|
||||
if (accessApprovalRequest.isTemporary && accessApprovalRequest.temporaryRange) {
|
||||
if (ms(temporaryRange) > ms(accessApprovalRequest.temporaryRange)) {
|
||||
throw new BadRequestError({ message: "Updated access duration must be less than current access duration" });
|
||||
}
|
||||
}
|
||||
|
||||
const { envSlug, secretPath, accessTypes } = verifyRequestedPermissions({
|
||||
permissions: accessApprovalRequest.permissions
|
||||
});
|
||||
|
||||
const approval = await accessApprovalRequestDAL.transaction(async (tx) => {
|
||||
const approvalRequest = await accessApprovalRequestDAL.updateById(
|
||||
requestId,
|
||||
{
|
||||
temporaryRange,
|
||||
isTemporary: true,
|
||||
editNote,
|
||||
editedByUserId: actorId
|
||||
},
|
||||
tx
|
||||
);
|
||||
|
||||
// reset review progress
|
||||
await accessApprovalRequestReviewerDAL.delete(
|
||||
{
|
||||
requestId
|
||||
},
|
||||
tx
|
||||
);
|
||||
|
||||
const requesterFullName = `${requestedByUser.firstName} ${requestedByUser.lastName}`;
|
||||
const editorFullName = `${editedByUser.firstName} ${editedByUser.lastName}`;
|
||||
const approvalUrl = `${cfg.SITE_URL}/projects/secret-management/${project.id}/approval`;
|
||||
|
||||
await triggerWorkflowIntegrationNotification({
|
||||
input: {
|
||||
notification: {
|
||||
type: TriggerFeature.ACCESS_REQUEST_UPDATED,
|
||||
payload: {
|
||||
projectName: project.name,
|
||||
requesterFullName,
|
||||
isTemporary: true,
|
||||
requesterEmail: requestedByUser.email as string,
|
||||
secretPath,
|
||||
environment: envSlug,
|
||||
permissions: accessTypes,
|
||||
approvalUrl,
|
||||
editNote,
|
||||
editorEmail: editedByUser.email as string,
|
||||
editorFullName
|
||||
}
|
||||
},
|
||||
projectId: project.id
|
||||
},
|
||||
dependencies: {
|
||||
projectDAL,
|
||||
projectSlackConfigDAL,
|
||||
kmsService,
|
||||
microsoftTeamsService,
|
||||
projectMicrosoftTeamsConfigDAL
|
||||
}
|
||||
});
|
||||
|
||||
await smtpService.sendMail({
|
||||
recipients: policy.approvers
|
||||
.filter((approver) => Boolean(approver.email) && approver.userId !== editedByUser.id)
|
||||
.map((approver) => approver.email!),
|
||||
subjectLine: "Access Approval Request Updated",
|
||||
substitutions: {
|
||||
projectName: project.name,
|
||||
requesterFullName,
|
||||
requesterEmail: requestedByUser.email,
|
||||
isTemporary: true,
|
||||
expiresIn: msFn(ms(temporaryRange || ""), { long: true }),
|
||||
secretPath,
|
||||
environment: envSlug,
|
||||
permissions: accessTypes,
|
||||
approvalUrl,
|
||||
editNote,
|
||||
editorFullName,
|
||||
editorEmail: editedByUser.email
|
||||
},
|
||||
template: SmtpTemplates.AccessApprovalRequestUpdated
|
||||
});
|
||||
|
||||
return approvalRequest;
|
||||
});
|
||||
|
||||
return { request: approval };
|
||||
};
|
||||
|
||||
const listApprovalRequests: TAccessApprovalRequestServiceFactory["listApprovalRequests"] = async ({
|
||||
projectSlug,
|
||||
authorUserId,
|
||||
@@ -641,6 +799,7 @@ export const accessApprovalRequestServiceFactory = ({
|
||||
|
||||
return {
|
||||
createAccessApprovalRequest,
|
||||
updateAccessApprovalRequest,
|
||||
listApprovalRequests,
|
||||
reviewAccessRequest,
|
||||
getCount
|
||||
|
@@ -30,6 +30,12 @@ export type TCreateAccessApprovalRequestDTO = {
|
||||
note?: string;
|
||||
} & Omit<TProjectPermission, "projectId">;
|
||||
|
||||
export type TUpdateAccessApprovalRequestDTO = {
|
||||
requestId: string;
|
||||
temporaryRange: string;
|
||||
editNote: string;
|
||||
} & Omit<TProjectPermission, "projectId">;
|
||||
|
||||
export type TListApprovalRequestsDTO = {
|
||||
projectSlug: string;
|
||||
authorUserId?: string;
|
||||
@@ -54,6 +60,23 @@ export interface TAccessApprovalRequestServiceFactory {
|
||||
privilegeDeletedAt?: Date | null | undefined;
|
||||
};
|
||||
}>;
|
||||
updateAccessApprovalRequest: (arg: TUpdateAccessApprovalRequestDTO) => Promise<{
|
||||
request: {
|
||||
status: string;
|
||||
id: string;
|
||||
createdAt: Date;
|
||||
updatedAt: Date;
|
||||
policyId: string;
|
||||
isTemporary: boolean;
|
||||
requestedByUserId: string;
|
||||
privilegeId?: string | null | undefined;
|
||||
requestedBy?: string | null | undefined;
|
||||
temporaryRange?: string | null | undefined;
|
||||
permissions?: unknown;
|
||||
note?: string | null | undefined;
|
||||
privilegeDeletedAt?: Date | null | undefined;
|
||||
};
|
||||
}>;
|
||||
listApprovalRequests: (arg: TListApprovalRequestsDTO) => Promise<{
|
||||
requests: {
|
||||
policy: {
|
||||
@@ -64,6 +87,7 @@ export interface TAccessApprovalRequestServiceFactory {
|
||||
approvalsRequired: number | null | undefined;
|
||||
email: string | null | undefined;
|
||||
username: string;
|
||||
isOrgMembershipActive: boolean;
|
||||
}
|
||||
| {
|
||||
userId: string;
|
||||
@@ -71,6 +95,7 @@ export interface TAccessApprovalRequestServiceFactory {
|
||||
approvalsRequired: number | null | undefined;
|
||||
email: string | null | undefined;
|
||||
username: string;
|
||||
isOrgMembershipActive: boolean;
|
||||
}
|
||||
)[];
|
||||
bypassers: string[];
|
||||
@@ -82,6 +107,7 @@ export interface TAccessApprovalRequestServiceFactory {
|
||||
allowedSelfApprovals: boolean;
|
||||
envId: string;
|
||||
deletedAt: Date | null | undefined;
|
||||
maxTimePeriod?: string | null;
|
||||
};
|
||||
projectId: string;
|
||||
environment: string;
|
||||
@@ -121,6 +147,7 @@ export interface TAccessApprovalRequestServiceFactory {
|
||||
reviewers: {
|
||||
userId: string;
|
||||
status: string;
|
||||
isOrgMembershipActive: boolean;
|
||||
}[];
|
||||
approvers: (
|
||||
| {
|
||||
@@ -129,6 +156,7 @@ export interface TAccessApprovalRequestServiceFactory {
|
||||
approvalsRequired: number | null | undefined;
|
||||
email: string | null | undefined;
|
||||
username: string;
|
||||
isOrgMembershipActive: boolean;
|
||||
}
|
||||
| {
|
||||
userId: string;
|
||||
@@ -136,6 +164,7 @@ export interface TAccessApprovalRequestServiceFactory {
|
||||
approvalsRequired: number | null | undefined;
|
||||
email: string | null | undefined;
|
||||
username: string;
|
||||
isOrgMembershipActive: boolean;
|
||||
}
|
||||
)[];
|
||||
bypassers: string[];
|
||||
|
@@ -1,8 +1,10 @@
|
||||
// weird commonjs-related error in the CI requires us to do the import like this
|
||||
import knex from "knex";
|
||||
import { v4 as uuidv4 } from "uuid";
|
||||
|
||||
import { TDbClient } from "@app/db";
|
||||
import { TableName, TAuditLogs } from "@app/db/schemas";
|
||||
import { getConfig } from "@app/lib/config/env";
|
||||
import { DatabaseError, GatewayTimeoutError } from "@app/lib/errors";
|
||||
import { ormify, selectAllTableCols, TOrmify } from "@app/lib/knex";
|
||||
import { logger } from "@app/lib/logger";
|
||||
@@ -12,7 +14,7 @@ import { ActorType } from "@app/services/auth/auth-type";
|
||||
import { EventType, filterableSecretEvents } from "./audit-log-types";
|
||||
|
||||
export interface TAuditLogDALFactory extends Omit<TOrmify<TableName.AuditLog>, "find"> {
|
||||
pruneAuditLog: (tx?: knex.Knex) => Promise<void>;
|
||||
pruneAuditLog: () => Promise<void>;
|
||||
find: (
|
||||
arg: Omit<TFindQuery, "actor" | "eventType"> & {
|
||||
actorId?: string | undefined;
|
||||
@@ -39,6 +41,10 @@ type TFindQuery = {
|
||||
offset?: number;
|
||||
};
|
||||
|
||||
const QUERY_TIMEOUT_MS = 10 * 60 * 1000; // 10 minutes
|
||||
const AUDIT_LOG_PRUNE_BATCH_SIZE = 10000;
|
||||
const MAX_RETRY_ON_FAILURE = 3;
|
||||
|
||||
export const auditLogDALFactory = (db: TDbClient) => {
|
||||
const auditLogOrm = ormify(db, TableName.AuditLog);
|
||||
|
||||
@@ -149,10 +155,7 @@ export const auditLogDALFactory = (db: TDbClient) => {
|
||||
};
|
||||
|
||||
// delete all audit log that have expired
|
||||
const pruneAuditLog: TAuditLogDALFactory["pruneAuditLog"] = async (tx) => {
|
||||
const AUDIT_LOG_PRUNE_BATCH_SIZE = 10000;
|
||||
const MAX_RETRY_ON_FAILURE = 3;
|
||||
|
||||
const pruneAuditLog: TAuditLogDALFactory["pruneAuditLog"] = async () => {
|
||||
const today = new Date();
|
||||
let deletedAuditLogIds: { id: string }[] = [];
|
||||
let numberOfRetryOnFailure = 0;
|
||||
@@ -161,21 +164,27 @@ export const auditLogDALFactory = (db: TDbClient) => {
|
||||
logger.info(`${QueueName.DailyResourceCleanUp}: audit log started`);
|
||||
do {
|
||||
try {
|
||||
const findExpiredLogSubQuery = (tx || db)(TableName.AuditLog)
|
||||
.where("expiresAt", "<", today)
|
||||
.where("createdAt", "<", today) // to use audit log partition
|
||||
.orderBy(`${TableName.AuditLog}.createdAt`, "desc")
|
||||
.select("id")
|
||||
.limit(AUDIT_LOG_PRUNE_BATCH_SIZE);
|
||||
|
||||
// eslint-disable-next-line no-await-in-loop
|
||||
deletedAuditLogIds = await (tx || db)(TableName.AuditLog)
|
||||
.whereIn("id", findExpiredLogSubQuery)
|
||||
.del()
|
||||
.returning("id");
|
||||
deletedAuditLogIds = await db.transaction(async (trx) => {
|
||||
await trx.raw(`SET statement_timeout = ${QUERY_TIMEOUT_MS}`);
|
||||
|
||||
const findExpiredLogSubQuery = trx(TableName.AuditLog)
|
||||
.where("expiresAt", "<", today)
|
||||
.where("createdAt", "<", today) // to use audit log partition
|
||||
.orderBy(`${TableName.AuditLog}.createdAt`, "desc")
|
||||
.select("id")
|
||||
.limit(AUDIT_LOG_PRUNE_BATCH_SIZE);
|
||||
|
||||
// eslint-disable-next-line no-await-in-loop
|
||||
const results = await trx(TableName.AuditLog).whereIn("id", findExpiredLogSubQuery).del().returning("id");
|
||||
|
||||
return results;
|
||||
});
|
||||
|
||||
numberOfRetryOnFailure = 0; // reset
|
||||
} catch (error) {
|
||||
numberOfRetryOnFailure += 1;
|
||||
deletedAuditLogIds = [];
|
||||
logger.error(error, "Failed to delete audit log on pruning");
|
||||
} finally {
|
||||
// eslint-disable-next-line no-await-in-loop
|
||||
@@ -188,5 +197,20 @@ export const auditLogDALFactory = (db: TDbClient) => {
|
||||
logger.info(`${QueueName.DailyResourceCleanUp}: audit log completed`);
|
||||
};
|
||||
|
||||
return { ...auditLogOrm, pruneAuditLog, find };
|
||||
const create: TAuditLogDALFactory["create"] = async (tx) => {
|
||||
const config = getConfig();
|
||||
|
||||
if (config.DISABLE_AUDIT_LOG_STORAGE) {
|
||||
return {
|
||||
...tx,
|
||||
id: uuidv4(),
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date()
|
||||
};
|
||||
}
|
||||
|
||||
return auditLogOrm.create(tx);
|
||||
};
|
||||
|
||||
return { ...auditLogOrm, create, pruneAuditLog, find };
|
||||
};
|
||||
|
@@ -1,8 +1,6 @@
|
||||
import { AxiosError, RawAxiosRequestHeaders } from "axios";
|
||||
|
||||
import { ProjectType, SecretKeyEncoding } from "@app/db/schemas";
|
||||
import { TEventBusService } from "@app/ee/services/event/event-bus-service";
|
||||
import { TopicName, toPublishableEvent } from "@app/ee/services/event/types";
|
||||
import { SecretKeyEncoding } from "@app/db/schemas";
|
||||
import { request } from "@app/lib/config/request";
|
||||
import { crypto } from "@app/lib/crypto/cryptography";
|
||||
import { logger } from "@app/lib/logger";
|
||||
@@ -22,7 +20,6 @@ type TAuditLogQueueServiceFactoryDep = {
|
||||
queueService: TQueueServiceFactory;
|
||||
projectDAL: Pick<TProjectDALFactory, "findById">;
|
||||
licenseService: Pick<TLicenseServiceFactory, "getPlan">;
|
||||
eventBusService: TEventBusService;
|
||||
};
|
||||
|
||||
export type TAuditLogQueueServiceFactory = {
|
||||
@@ -38,8 +35,7 @@ export const auditLogQueueServiceFactory = async ({
|
||||
queueService,
|
||||
projectDAL,
|
||||
licenseService,
|
||||
auditLogStreamDAL,
|
||||
eventBusService
|
||||
auditLogStreamDAL
|
||||
}: TAuditLogQueueServiceFactoryDep): Promise<TAuditLogQueueServiceFactory> => {
|
||||
const pushToLog = async (data: TCreateAuditLogDTO) => {
|
||||
await queueService.queue<QueueName.AuditLog>(QueueName.AuditLog, QueueJobs.AuditLog, data, {
|
||||
@@ -145,16 +141,6 @@ export const auditLogQueueServiceFactory = async ({
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
const publishable = toPublishableEvent(event);
|
||||
|
||||
if (publishable) {
|
||||
await eventBusService.publish(TopicName.CoreServers, {
|
||||
type: ProjectType.SecretManager,
|
||||
source: "infiscal",
|
||||
data: publishable.data
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
return {
|
||||
|
@@ -6,9 +6,9 @@ import { getConfig } from "@app/lib/config/env";
|
||||
import { BadRequestError } from "@app/lib/errors";
|
||||
import { ActorType } from "@app/services/auth/auth-type";
|
||||
|
||||
import { OrgPermissionActions, OrgPermissionSubjects } from "../permission/org-permission";
|
||||
import { OrgPermissionAuditLogsActions, OrgPermissionSubjects } from "../permission/org-permission";
|
||||
import { TPermissionServiceFactory } from "../permission/permission-service-types";
|
||||
import { ProjectPermissionActions, ProjectPermissionSub } from "../permission/project-permission";
|
||||
import { ProjectPermissionAuditLogsActions, ProjectPermissionSub } from "../permission/project-permission";
|
||||
import { TAuditLogDALFactory } from "./audit-log-dal";
|
||||
import { TAuditLogQueueServiceFactory } from "./audit-log-queue";
|
||||
import { EventType, TAuditLogServiceFactory } from "./audit-log-types";
|
||||
@@ -41,7 +41,10 @@ export const auditLogServiceFactory = ({
|
||||
actorOrgId,
|
||||
actionProjectType: ActionProjectType.Any
|
||||
});
|
||||
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.AuditLogs);
|
||||
ForbiddenError.from(permission).throwUnlessCan(
|
||||
ProjectPermissionAuditLogsActions.Read,
|
||||
ProjectPermissionSub.AuditLogs
|
||||
);
|
||||
} else {
|
||||
// Organization-wide logs
|
||||
const { permission } = await permissionService.getOrgPermission(
|
||||
@@ -52,7 +55,10 @@ export const auditLogServiceFactory = ({
|
||||
actorOrgId
|
||||
);
|
||||
|
||||
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Read, OrgPermissionSubjects.AuditLogs);
|
||||
ForbiddenError.from(permission).throwUnlessCan(
|
||||
OrgPermissionAuditLogsActions.Read,
|
||||
OrgPermissionSubjects.AuditLogs
|
||||
);
|
||||
}
|
||||
|
||||
// If project ID is not provided, then we need to return all the audit logs for the organization itself.
|
||||
|
@@ -161,6 +161,9 @@ export enum EventType {
|
||||
CREATE_IDENTITY = "create-identity",
|
||||
UPDATE_IDENTITY = "update-identity",
|
||||
DELETE_IDENTITY = "delete-identity",
|
||||
MACHINE_IDENTITY_AUTH_TEMPLATE_CREATE = "machine-identity-auth-template-create",
|
||||
MACHINE_IDENTITY_AUTH_TEMPLATE_UPDATE = "machine-identity-auth-template-update",
|
||||
MACHINE_IDENTITY_AUTH_TEMPLATE_DELETE = "machine-identity-auth-template-delete",
|
||||
LOGIN_IDENTITY_UNIVERSAL_AUTH = "login-identity-universal-auth",
|
||||
ADD_IDENTITY_UNIVERSAL_AUTH = "add-identity-universal-auth",
|
||||
UPDATE_IDENTITY_UNIVERSAL_AUTH = "update-identity-universal-auth",
|
||||
@@ -195,6 +198,7 @@ export enum EventType {
|
||||
|
||||
CREATE_IDENTITY_UNIVERSAL_AUTH_CLIENT_SECRET = "create-identity-universal-auth-client-secret",
|
||||
REVOKE_IDENTITY_UNIVERSAL_AUTH_CLIENT_SECRET = "revoke-identity-universal-auth-client-secret",
|
||||
CLEAR_IDENTITY_UNIVERSAL_AUTH_LOCKOUTS = "clear-identity-universal-auth-lockouts",
|
||||
|
||||
GET_IDENTITY_UNIVERSAL_AUTH_CLIENT_SECRETS = "get-identity-universal-auth-client-secret",
|
||||
GET_IDENTITY_UNIVERSAL_AUTH_CLIENT_SECRET_BY_ID = "get-identity-universal-auth-client-secret-by-id",
|
||||
@@ -278,6 +282,7 @@ export enum EventType {
|
||||
UPDATE_SSH_CERTIFICATE_TEMPLATE = "update-ssh-certificate-template",
|
||||
DELETE_SSH_CERTIFICATE_TEMPLATE = "delete-ssh-certificate-template",
|
||||
GET_SSH_CERTIFICATE_TEMPLATE = "get-ssh-certificate-template",
|
||||
GET_AZURE_AD_TEMPLATES = "get-azure-ad-templates",
|
||||
GET_SSH_HOST = "get-ssh-host",
|
||||
CREATE_SSH_HOST = "create-ssh-host",
|
||||
UPDATE_SSH_HOST = "update-ssh-host",
|
||||
@@ -830,6 +835,30 @@ interface LoginIdentityUniversalAuthEvent {
|
||||
};
|
||||
}
|
||||
|
||||
interface MachineIdentityAuthTemplateCreateEvent {
|
||||
type: EventType.MACHINE_IDENTITY_AUTH_TEMPLATE_CREATE;
|
||||
metadata: {
|
||||
templateId: string;
|
||||
name: string;
|
||||
};
|
||||
}
|
||||
|
||||
interface MachineIdentityAuthTemplateUpdateEvent {
|
||||
type: EventType.MACHINE_IDENTITY_AUTH_TEMPLATE_UPDATE;
|
||||
metadata: {
|
||||
templateId: string;
|
||||
name: string;
|
||||
};
|
||||
}
|
||||
|
||||
interface MachineIdentityAuthTemplateDeleteEvent {
|
||||
type: EventType.MACHINE_IDENTITY_AUTH_TEMPLATE_DELETE;
|
||||
metadata: {
|
||||
templateId: string;
|
||||
name: string;
|
||||
};
|
||||
}
|
||||
|
||||
interface AddIdentityUniversalAuthEvent {
|
||||
type: EventType.ADD_IDENTITY_UNIVERSAL_AUTH;
|
||||
metadata: {
|
||||
@@ -839,6 +868,10 @@ interface AddIdentityUniversalAuthEvent {
|
||||
accessTokenMaxTTL: number;
|
||||
accessTokenNumUsesLimit: number;
|
||||
accessTokenTrustedIps: Array<TIdentityTrustedIp>;
|
||||
lockoutEnabled: boolean;
|
||||
lockoutThreshold: number;
|
||||
lockoutDurationSeconds: number;
|
||||
lockoutCounterResetSeconds: number;
|
||||
};
|
||||
}
|
||||
|
||||
@@ -851,6 +884,10 @@ interface UpdateIdentityUniversalAuthEvent {
|
||||
accessTokenMaxTTL?: number;
|
||||
accessTokenNumUsesLimit?: number;
|
||||
accessTokenTrustedIps?: Array<TIdentityTrustedIp>;
|
||||
lockoutEnabled?: boolean;
|
||||
lockoutThreshold?: number;
|
||||
lockoutDurationSeconds?: number;
|
||||
lockoutCounterResetSeconds?: number;
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1010,6 +1047,13 @@ interface RevokeIdentityUniversalAuthClientSecretEvent {
|
||||
};
|
||||
}
|
||||
|
||||
interface ClearIdentityUniversalAuthLockoutsEvent {
|
||||
type: EventType.CLEAR_IDENTITY_UNIVERSAL_AUTH_LOCKOUTS;
|
||||
metadata: {
|
||||
identityId: string;
|
||||
};
|
||||
}
|
||||
|
||||
interface LoginIdentityGcpAuthEvent {
|
||||
type: EventType.LOGIN_IDENTITY_GCP_AUTH;
|
||||
metadata: {
|
||||
@@ -1325,6 +1369,7 @@ interface AddIdentityLdapAuthEvent {
|
||||
accessTokenTrustedIps?: Array<TIdentityTrustedIp>;
|
||||
allowedFields?: TAllowedFields[];
|
||||
url: string;
|
||||
templateId?: string | null;
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1338,6 +1383,7 @@ interface UpdateIdentityLdapAuthEvent {
|
||||
accessTokenTrustedIps?: Array<TIdentityTrustedIp>;
|
||||
allowedFields?: TAllowedFields[];
|
||||
url?: string;
|
||||
templateId?: string | null;
|
||||
};
|
||||
}
|
||||
|
||||
@@ -2468,6 +2514,14 @@ interface CreateCertificateTemplateEstConfig {
|
||||
};
|
||||
}
|
||||
|
||||
interface GetAzureAdCsTemplatesEvent {
|
||||
type: EventType.GET_AZURE_AD_TEMPLATES;
|
||||
metadata: {
|
||||
caId: string;
|
||||
amount: number;
|
||||
};
|
||||
}
|
||||
|
||||
interface UpdateCertificateTemplateEstConfig {
|
||||
type: EventType.UPDATE_CERTIFICATE_TEMPLATE_EST_CONFIG;
|
||||
metadata: {
|
||||
@@ -3439,6 +3493,9 @@ export type Event =
|
||||
| UpdateIdentityEvent
|
||||
| DeleteIdentityEvent
|
||||
| LoginIdentityUniversalAuthEvent
|
||||
| MachineIdentityAuthTemplateCreateEvent
|
||||
| MachineIdentityAuthTemplateUpdateEvent
|
||||
| MachineIdentityAuthTemplateDeleteEvent
|
||||
| AddIdentityUniversalAuthEvent
|
||||
| UpdateIdentityUniversalAuthEvent
|
||||
| DeleteIdentityUniversalAuthEvent
|
||||
@@ -3459,6 +3516,7 @@ export type Event =
|
||||
| GetIdentityUniversalAuthClientSecretsEvent
|
||||
| GetIdentityUniversalAuthClientSecretByIdEvent
|
||||
| RevokeIdentityUniversalAuthClientSecretEvent
|
||||
| ClearIdentityUniversalAuthLockoutsEvent
|
||||
| LoginIdentityGcpAuthEvent
|
||||
| AddIdentityGcpAuthEvent
|
||||
| DeleteIdentityGcpAuthEvent
|
||||
@@ -3604,6 +3662,7 @@ export type Event =
|
||||
| CreateCertificateTemplateEstConfig
|
||||
| UpdateCertificateTemplateEstConfig
|
||||
| GetCertificateTemplateEstConfig
|
||||
| GetAzureAdCsTemplatesEvent
|
||||
| AttemptCreateSlackIntegration
|
||||
| AttemptReinstallSlackIntegration
|
||||
| UpdateSlackIntegration
|
||||
|
@@ -9,7 +9,7 @@ import { getDbConnectionHost } from "@app/lib/knex";
|
||||
export const verifyHostInputValidity = async (host: string, isGateway = false) => {
|
||||
const appCfg = getConfig();
|
||||
|
||||
if (appCfg.isDevelopmentMode) return [host];
|
||||
if (appCfg.isDevelopmentMode || appCfg.isTestMode) return [host];
|
||||
|
||||
if (isGateway) return [host];
|
||||
|
||||
|
@@ -15,6 +15,7 @@ import { z } from "zod";
|
||||
import { CustomAWSHasher } from "@app/lib/aws/hashing";
|
||||
import { crypto } from "@app/lib/crypto";
|
||||
import { BadRequestError } from "@app/lib/errors";
|
||||
import { sanitizeString } from "@app/lib/fn";
|
||||
import { validateHandlebarTemplate } from "@app/lib/template/validate-handlebars";
|
||||
|
||||
import { DynamicSecretAwsElastiCacheSchema, TDynamicProviderFns } from "./models";
|
||||
@@ -170,14 +171,29 @@ export const AwsElastiCacheDatabaseProvider = (): TDynamicProviderFns => {
|
||||
};
|
||||
const validateConnection = async (inputs: unknown) => {
|
||||
const providerInputs = await validateProviderInputs(inputs);
|
||||
await ElastiCacheUserManager(
|
||||
{
|
||||
accessKeyId: providerInputs.accessKeyId,
|
||||
secretAccessKey: providerInputs.secretAccessKey
|
||||
},
|
||||
providerInputs.region
|
||||
).verifyCredentials(providerInputs.clusterName);
|
||||
return true;
|
||||
try {
|
||||
await ElastiCacheUserManager(
|
||||
{
|
||||
accessKeyId: providerInputs.accessKeyId,
|
||||
secretAccessKey: providerInputs.secretAccessKey
|
||||
},
|
||||
providerInputs.region
|
||||
).verifyCredentials(providerInputs.clusterName);
|
||||
return true;
|
||||
} catch (err) {
|
||||
const sanitizedErrorMessage = sanitizeString({
|
||||
unsanitizedString: (err as Error)?.message,
|
||||
tokens: [
|
||||
providerInputs.accessKeyId,
|
||||
providerInputs.secretAccessKey,
|
||||
providerInputs.clusterName,
|
||||
providerInputs.region
|
||||
]
|
||||
});
|
||||
throw new BadRequestError({
|
||||
message: `Failed to connect with provider: ${sanitizedErrorMessage}`
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const create = async (data: {
|
||||
@@ -206,21 +222,37 @@ export const AwsElastiCacheDatabaseProvider = (): TDynamicProviderFns => {
|
||||
|
||||
const parsedStatement = CreateElastiCacheUserSchema.parse(JSON.parse(creationStatement));
|
||||
|
||||
await ElastiCacheUserManager(
|
||||
{
|
||||
accessKeyId: providerInputs.accessKeyId,
|
||||
secretAccessKey: providerInputs.secretAccessKey
|
||||
},
|
||||
providerInputs.region
|
||||
).createUser(parsedStatement, providerInputs.clusterName);
|
||||
try {
|
||||
await ElastiCacheUserManager(
|
||||
{
|
||||
accessKeyId: providerInputs.accessKeyId,
|
||||
secretAccessKey: providerInputs.secretAccessKey
|
||||
},
|
||||
providerInputs.region
|
||||
).createUser(parsedStatement, providerInputs.clusterName);
|
||||
|
||||
return {
|
||||
entityId: leaseUsername,
|
||||
data: {
|
||||
DB_USERNAME: leaseUsername,
|
||||
DB_PASSWORD: leasePassword
|
||||
}
|
||||
};
|
||||
return {
|
||||
entityId: leaseUsername,
|
||||
data: {
|
||||
DB_USERNAME: leaseUsername,
|
||||
DB_PASSWORD: leasePassword
|
||||
}
|
||||
};
|
||||
} catch (err) {
|
||||
const sanitizedErrorMessage = sanitizeString({
|
||||
unsanitizedString: (err as Error)?.message,
|
||||
tokens: [
|
||||
leaseUsername,
|
||||
leasePassword,
|
||||
providerInputs.accessKeyId,
|
||||
providerInputs.secretAccessKey,
|
||||
providerInputs.clusterName
|
||||
]
|
||||
});
|
||||
throw new BadRequestError({
|
||||
message: `Failed to create lease from provider: ${sanitizedErrorMessage}`
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const revoke = async (inputs: unknown, entityId: string) => {
|
||||
@@ -229,15 +261,25 @@ export const AwsElastiCacheDatabaseProvider = (): TDynamicProviderFns => {
|
||||
const revokeStatement = handlebars.compile(providerInputs.revocationStatement)({ username: entityId });
|
||||
const parsedStatement = DeleteElasticCacheUserSchema.parse(JSON.parse(revokeStatement));
|
||||
|
||||
await ElastiCacheUserManager(
|
||||
{
|
||||
accessKeyId: providerInputs.accessKeyId,
|
||||
secretAccessKey: providerInputs.secretAccessKey
|
||||
},
|
||||
providerInputs.region
|
||||
).deleteUser(parsedStatement);
|
||||
try {
|
||||
await ElastiCacheUserManager(
|
||||
{
|
||||
accessKeyId: providerInputs.accessKeyId,
|
||||
secretAccessKey: providerInputs.secretAccessKey
|
||||
},
|
||||
providerInputs.region
|
||||
).deleteUser(parsedStatement);
|
||||
|
||||
return { entityId };
|
||||
return { entityId };
|
||||
} catch (err) {
|
||||
const sanitizedErrorMessage = sanitizeString({
|
||||
unsanitizedString: (err as Error)?.message,
|
||||
tokens: [entityId, providerInputs.accessKeyId, providerInputs.secretAccessKey, providerInputs.clusterName]
|
||||
});
|
||||
throw new BadRequestError({
|
||||
message: `Failed to revoke lease from provider: ${sanitizedErrorMessage}`
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const renew = async (_inputs: unknown, entityId: string) => {
|
||||
|
@@ -23,6 +23,7 @@ import { CustomAWSHasher } from "@app/lib/aws/hashing";
|
||||
import { getConfig } from "@app/lib/config/env";
|
||||
import { crypto } from "@app/lib/crypto/cryptography";
|
||||
import { BadRequestError, UnauthorizedError } from "@app/lib/errors";
|
||||
import { sanitizeString } from "@app/lib/fn";
|
||||
import { alphaNumericNanoId } from "@app/lib/nanoid";
|
||||
|
||||
import { AwsIamAuthType, DynamicSecretAwsIamSchema, TDynamicProviderFns } from "./models";
|
||||
@@ -118,22 +119,39 @@ export const AwsIamProvider = (): TDynamicProviderFns => {
|
||||
|
||||
const validateConnection = async (inputs: unknown, { projectId }: { projectId: string }) => {
|
||||
const providerInputs = await validateProviderInputs(inputs);
|
||||
const client = await $getClient(providerInputs, projectId);
|
||||
const isConnected = await client
|
||||
.send(new GetUserCommand({}))
|
||||
.then(() => true)
|
||||
.catch((err) => {
|
||||
const message = (err as Error)?.message;
|
||||
if (
|
||||
(providerInputs.method === AwsIamAuthType.AssumeRole || providerInputs.method === AwsIamAuthType.IRSA) &&
|
||||
// assume role will throw an error asking to provider username, but if so this has access in aws correctly
|
||||
message.includes("Must specify userName when calling with non-User credentials")
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
throw err;
|
||||
try {
|
||||
const client = await $getClient(providerInputs, projectId);
|
||||
const isConnected = await client
|
||||
.send(new GetUserCommand({}))
|
||||
.then(() => true)
|
||||
.catch((err) => {
|
||||
const message = (err as Error)?.message;
|
||||
if (
|
||||
(providerInputs.method === AwsIamAuthType.AssumeRole || providerInputs.method === AwsIamAuthType.IRSA) &&
|
||||
// assume role will throw an error asking to provider username, but if so this has access in aws correctly
|
||||
message.includes("Must specify userName when calling with non-User credentials")
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
throw err;
|
||||
});
|
||||
return isConnected;
|
||||
} catch (err) {
|
||||
const sensitiveTokens = [];
|
||||
if (providerInputs.method === AwsIamAuthType.AccessKey) {
|
||||
sensitiveTokens.push(providerInputs.accessKey, providerInputs.secretAccessKey);
|
||||
}
|
||||
if (providerInputs.method === AwsIamAuthType.AssumeRole) {
|
||||
sensitiveTokens.push(providerInputs.roleArn);
|
||||
}
|
||||
const sanitizedErrorMessage = sanitizeString({
|
||||
unsanitizedString: (err as Error)?.message,
|
||||
tokens: sensitiveTokens
|
||||
});
|
||||
return isConnected;
|
||||
throw new BadRequestError({
|
||||
message: `Failed to connect with provider: ${sanitizedErrorMessage}`
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const create = async (data: {
|
||||
@@ -162,62 +180,81 @@ export const AwsIamProvider = (): TDynamicProviderFns => {
|
||||
awsTags.push(...additionalTags);
|
||||
}
|
||||
|
||||
const createUserRes = await client.send(
|
||||
new CreateUserCommand({
|
||||
Path: awsPath,
|
||||
PermissionsBoundary: permissionBoundaryPolicyArn || undefined,
|
||||
Tags: awsTags,
|
||||
UserName: username
|
||||
})
|
||||
);
|
||||
|
||||
if (!createUserRes.User) throw new BadRequestError({ message: "Failed to create AWS IAM User" });
|
||||
if (userGroups) {
|
||||
await Promise.all(
|
||||
userGroups
|
||||
.split(",")
|
||||
.filter(Boolean)
|
||||
.map((group) =>
|
||||
client.send(new AddUserToGroupCommand({ UserName: createUserRes?.User?.UserName, GroupName: group }))
|
||||
)
|
||||
);
|
||||
}
|
||||
if (policyArns) {
|
||||
await Promise.all(
|
||||
policyArns
|
||||
.split(",")
|
||||
.filter(Boolean)
|
||||
.map((policyArn) =>
|
||||
client.send(new AttachUserPolicyCommand({ UserName: createUserRes?.User?.UserName, PolicyArn: policyArn }))
|
||||
)
|
||||
);
|
||||
}
|
||||
if (policyDocument) {
|
||||
await client.send(
|
||||
new PutUserPolicyCommand({
|
||||
UserName: createUserRes.User.UserName,
|
||||
PolicyName: `infisical-dynamic-policy-${alphaNumericNanoId(4)}`,
|
||||
PolicyDocument: policyDocument
|
||||
try {
|
||||
const createUserRes = await client.send(
|
||||
new CreateUserCommand({
|
||||
Path: awsPath,
|
||||
PermissionsBoundary: permissionBoundaryPolicyArn || undefined,
|
||||
Tags: awsTags,
|
||||
UserName: username
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
const createAccessKeyRes = await client.send(
|
||||
new CreateAccessKeyCommand({
|
||||
UserName: createUserRes.User.UserName
|
||||
})
|
||||
);
|
||||
if (!createAccessKeyRes.AccessKey)
|
||||
throw new BadRequestError({ message: "Failed to create AWS IAM User access key" });
|
||||
|
||||
return {
|
||||
entityId: username,
|
||||
data: {
|
||||
ACCESS_KEY: createAccessKeyRes.AccessKey.AccessKeyId,
|
||||
SECRET_ACCESS_KEY: createAccessKeyRes.AccessKey.SecretAccessKey,
|
||||
USERNAME: username
|
||||
if (!createUserRes.User) throw new BadRequestError({ message: "Failed to create AWS IAM User" });
|
||||
if (userGroups) {
|
||||
await Promise.all(
|
||||
userGroups
|
||||
.split(",")
|
||||
.filter(Boolean)
|
||||
.map((group) =>
|
||||
client.send(new AddUserToGroupCommand({ UserName: createUserRes?.User?.UserName, GroupName: group }))
|
||||
)
|
||||
);
|
||||
}
|
||||
};
|
||||
if (policyArns) {
|
||||
await Promise.all(
|
||||
policyArns
|
||||
.split(",")
|
||||
.filter(Boolean)
|
||||
.map((policyArn) =>
|
||||
client.send(
|
||||
new AttachUserPolicyCommand({ UserName: createUserRes?.User?.UserName, PolicyArn: policyArn })
|
||||
)
|
||||
)
|
||||
);
|
||||
}
|
||||
if (policyDocument) {
|
||||
await client.send(
|
||||
new PutUserPolicyCommand({
|
||||
UserName: createUserRes.User.UserName,
|
||||
PolicyName: `infisical-dynamic-policy-${alphaNumericNanoId(4)}`,
|
||||
PolicyDocument: policyDocument
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
const createAccessKeyRes = await client.send(
|
||||
new CreateAccessKeyCommand({
|
||||
UserName: createUserRes.User.UserName
|
||||
})
|
||||
);
|
||||
if (!createAccessKeyRes.AccessKey)
|
||||
throw new BadRequestError({ message: "Failed to create AWS IAM User access key" });
|
||||
|
||||
return {
|
||||
entityId: username,
|
||||
data: {
|
||||
ACCESS_KEY: createAccessKeyRes.AccessKey.AccessKeyId,
|
||||
SECRET_ACCESS_KEY: createAccessKeyRes.AccessKey.SecretAccessKey,
|
||||
USERNAME: username
|
||||
}
|
||||
};
|
||||
} catch (err) {
|
||||
const sensitiveTokens = [username];
|
||||
if (providerInputs.method === AwsIamAuthType.AccessKey) {
|
||||
sensitiveTokens.push(providerInputs.accessKey, providerInputs.secretAccessKey);
|
||||
}
|
||||
if (providerInputs.method === AwsIamAuthType.AssumeRole) {
|
||||
sensitiveTokens.push(providerInputs.roleArn);
|
||||
}
|
||||
const sanitizedErrorMessage = sanitizeString({
|
||||
unsanitizedString: (err as Error)?.message,
|
||||
tokens: sensitiveTokens
|
||||
});
|
||||
throw new BadRequestError({
|
||||
message: `Failed to create lease from provider: ${sanitizedErrorMessage}`
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const revoke = async (inputs: unknown, entityId: string, metadata: { projectId: string }) => {
|
||||
@@ -278,8 +315,25 @@ export const AwsIamProvider = (): TDynamicProviderFns => {
|
||||
)
|
||||
);
|
||||
|
||||
await client.send(new DeleteUserCommand({ UserName: username }));
|
||||
return { entityId: username };
|
||||
try {
|
||||
await client.send(new DeleteUserCommand({ UserName: username }));
|
||||
return { entityId: username };
|
||||
} catch (err) {
|
||||
const sensitiveTokens = [username];
|
||||
if (providerInputs.method === AwsIamAuthType.AccessKey) {
|
||||
sensitiveTokens.push(providerInputs.accessKey, providerInputs.secretAccessKey);
|
||||
}
|
||||
if (providerInputs.method === AwsIamAuthType.AssumeRole) {
|
||||
sensitiveTokens.push(providerInputs.roleArn);
|
||||
}
|
||||
const sanitizedErrorMessage = sanitizeString({
|
||||
unsanitizedString: (err as Error)?.message,
|
||||
tokens: sensitiveTokens
|
||||
});
|
||||
throw new BadRequestError({
|
||||
message: `Failed to revoke lease from provider: ${sanitizedErrorMessage}`
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const renew = async (_inputs: unknown, entityId: string) => {
|
||||
|
@@ -2,6 +2,7 @@ import axios from "axios";
|
||||
import { customAlphabet } from "nanoid";
|
||||
|
||||
import { BadRequestError } from "@app/lib/errors";
|
||||
import { sanitizeString } from "@app/lib/fn";
|
||||
|
||||
import { AzureEntraIDSchema, TDynamicProviderFns } from "./models";
|
||||
|
||||
@@ -51,45 +52,82 @@ export const AzureEntraIDProvider = (): TDynamicProviderFns & {
|
||||
|
||||
const validateConnection = async (inputs: unknown) => {
|
||||
const providerInputs = await validateProviderInputs(inputs);
|
||||
const data = await $getToken(providerInputs.tenantId, providerInputs.applicationId, providerInputs.clientSecret);
|
||||
return data.success;
|
||||
try {
|
||||
const data = await $getToken(providerInputs.tenantId, providerInputs.applicationId, providerInputs.clientSecret);
|
||||
return data.success;
|
||||
} catch (err) {
|
||||
const sanitizedErrorMessage = sanitizeString({
|
||||
unsanitizedString: (err as Error)?.message,
|
||||
tokens: [providerInputs.clientSecret, providerInputs.applicationId, providerInputs.tenantId]
|
||||
});
|
||||
throw new BadRequestError({
|
||||
message: `Failed to connect with provider: ${sanitizedErrorMessage}`
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const create = async ({ inputs }: { inputs: unknown }) => {
|
||||
const providerInputs = await validateProviderInputs(inputs);
|
||||
const data = await $getToken(providerInputs.tenantId, providerInputs.applicationId, providerInputs.clientSecret);
|
||||
if (!data.success) {
|
||||
throw new BadRequestError({ message: "Failed to authorize to Microsoft Entra ID" });
|
||||
}
|
||||
|
||||
const password = generatePassword();
|
||||
|
||||
const response = await axios.patch(
|
||||
`${MSFT_GRAPH_API_URL}/users/${providerInputs.userId}`,
|
||||
{
|
||||
passwordProfile: {
|
||||
forceChangePasswordNextSignIn: false,
|
||||
password
|
||||
}
|
||||
},
|
||||
{
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
Authorization: `Bearer ${data.token}`
|
||||
}
|
||||
try {
|
||||
const data = await $getToken(providerInputs.tenantId, providerInputs.applicationId, providerInputs.clientSecret);
|
||||
if (!data.success) {
|
||||
throw new BadRequestError({ message: "Failed to authorize to Microsoft Entra ID" });
|
||||
}
|
||||
);
|
||||
if (response.status !== 204) {
|
||||
throw new BadRequestError({ message: "Failed to update password" });
|
||||
}
|
||||
|
||||
return { entityId: providerInputs.userId, data: { email: providerInputs.email, password } };
|
||||
const response = await axios.patch(
|
||||
`${MSFT_GRAPH_API_URL}/users/${providerInputs.userId}`,
|
||||
{
|
||||
passwordProfile: {
|
||||
forceChangePasswordNextSignIn: false,
|
||||
password
|
||||
}
|
||||
},
|
||||
{
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
Authorization: `Bearer ${data.token}`
|
||||
}
|
||||
}
|
||||
);
|
||||
if (response.status !== 204) {
|
||||
throw new BadRequestError({ message: "Failed to update password" });
|
||||
}
|
||||
|
||||
return { entityId: providerInputs.userId, data: { email: providerInputs.email, password } };
|
||||
} catch (err) {
|
||||
const sanitizedErrorMessage = sanitizeString({
|
||||
unsanitizedString: (err as Error)?.message,
|
||||
tokens: [
|
||||
providerInputs.clientSecret,
|
||||
providerInputs.applicationId,
|
||||
providerInputs.userId,
|
||||
providerInputs.email,
|
||||
password
|
||||
]
|
||||
});
|
||||
throw new BadRequestError({
|
||||
message: `Failed to create lease from provider: ${sanitizedErrorMessage}`
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const revoke = async (inputs: unknown, entityId: string) => {
|
||||
// Creates a new password
|
||||
await create({ inputs });
|
||||
return { entityId };
|
||||
const providerInputs = await validateProviderInputs(inputs);
|
||||
try {
|
||||
// Creates a new password
|
||||
await create({ inputs });
|
||||
return { entityId };
|
||||
} catch (err) {
|
||||
const sanitizedErrorMessage = sanitizeString({
|
||||
unsanitizedString: (err as Error)?.message,
|
||||
tokens: [providerInputs.clientSecret, providerInputs.applicationId, entityId]
|
||||
});
|
||||
throw new BadRequestError({
|
||||
message: `Failed to revoke lease from provider: ${sanitizedErrorMessage}`
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const fetchAzureEntraIdUsers = async (tenantId: string, applicationId: string, clientSecret: string) => {
|
||||
|
@@ -3,6 +3,8 @@ import handlebars from "handlebars";
|
||||
import { customAlphabet } from "nanoid";
|
||||
import { z } from "zod";
|
||||
|
||||
import { BadRequestError } from "@app/lib/errors";
|
||||
import { sanitizeString } from "@app/lib/fn";
|
||||
import { alphaNumericNanoId } from "@app/lib/nanoid";
|
||||
import { validateHandlebarTemplate } from "@app/lib/template/validate-handlebars";
|
||||
|
||||
@@ -71,9 +73,24 @@ export const CassandraProvider = (): TDynamicProviderFns => {
|
||||
const providerInputs = await validateProviderInputs(inputs);
|
||||
const client = await $getClient(providerInputs);
|
||||
|
||||
const isConnected = await client.execute("SELECT * FROM system_schema.keyspaces").then(() => true);
|
||||
await client.shutdown();
|
||||
return isConnected;
|
||||
try {
|
||||
const isConnected = await client.execute("SELECT * FROM system_schema.keyspaces").then(() => true);
|
||||
await client.shutdown();
|
||||
return isConnected;
|
||||
} catch (err) {
|
||||
const tokens = [providerInputs.password, providerInputs.username];
|
||||
if (providerInputs.keyspace) {
|
||||
tokens.push(providerInputs.keyspace);
|
||||
}
|
||||
const sanitizedErrorMessage = sanitizeString({
|
||||
unsanitizedString: (err as Error)?.message,
|
||||
tokens
|
||||
});
|
||||
await client.shutdown();
|
||||
throw new BadRequestError({
|
||||
message: `Failed to connect with provider: ${sanitizedErrorMessage}`
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const create = async (data: {
|
||||
@@ -89,23 +106,39 @@ export const CassandraProvider = (): TDynamicProviderFns => {
|
||||
const username = generateUsername(usernameTemplate, identity);
|
||||
const password = generatePassword();
|
||||
const { keyspace } = providerInputs;
|
||||
const expiration = new Date(expireAt).toISOString();
|
||||
|
||||
const creationStatement = handlebars.compile(providerInputs.creationStatement, { noEscape: true })({
|
||||
username,
|
||||
password,
|
||||
expiration,
|
||||
keyspace
|
||||
});
|
||||
try {
|
||||
const expiration = new Date(expireAt).toISOString();
|
||||
|
||||
const queries = creationStatement.toString().split(";").filter(Boolean);
|
||||
for (const query of queries) {
|
||||
// eslint-disable-next-line
|
||||
await client.execute(query);
|
||||
const creationStatement = handlebars.compile(providerInputs.creationStatement, { noEscape: true })({
|
||||
username,
|
||||
password,
|
||||
expiration,
|
||||
keyspace
|
||||
});
|
||||
|
||||
const queries = creationStatement.toString().split(";").filter(Boolean);
|
||||
for (const query of queries) {
|
||||
// eslint-disable-next-line
|
||||
await client.execute(query);
|
||||
}
|
||||
await client.shutdown();
|
||||
|
||||
return { entityId: username, data: { DB_USERNAME: username, DB_PASSWORD: password } };
|
||||
} catch (err) {
|
||||
const tokens = [username, password];
|
||||
if (keyspace) {
|
||||
tokens.push(keyspace);
|
||||
}
|
||||
const sanitizedErrorMessage = sanitizeString({
|
||||
unsanitizedString: (err as Error)?.message,
|
||||
tokens
|
||||
});
|
||||
await client.shutdown();
|
||||
throw new BadRequestError({
|
||||
message: `Failed to create lease from provider: ${sanitizedErrorMessage}`
|
||||
});
|
||||
}
|
||||
await client.shutdown();
|
||||
|
||||
return { entityId: username, data: { DB_USERNAME: username, DB_PASSWORD: password } };
|
||||
};
|
||||
|
||||
const revoke = async (inputs: unknown, entityId: string) => {
|
||||
@@ -115,14 +148,29 @@ export const CassandraProvider = (): TDynamicProviderFns => {
|
||||
const username = entityId;
|
||||
const { keyspace } = providerInputs;
|
||||
|
||||
const revokeStatement = handlebars.compile(providerInputs.revocationStatement)({ username, keyspace });
|
||||
const queries = revokeStatement.toString().split(";").filter(Boolean);
|
||||
for (const query of queries) {
|
||||
// eslint-disable-next-line
|
||||
await client.execute(query);
|
||||
try {
|
||||
const revokeStatement = handlebars.compile(providerInputs.revocationStatement)({ username, keyspace });
|
||||
const queries = revokeStatement.toString().split(";").filter(Boolean);
|
||||
for (const query of queries) {
|
||||
// eslint-disable-next-line
|
||||
await client.execute(query);
|
||||
}
|
||||
await client.shutdown();
|
||||
return { entityId: username };
|
||||
} catch (err) {
|
||||
const tokens = [username];
|
||||
if (keyspace) {
|
||||
tokens.push(keyspace);
|
||||
}
|
||||
const sanitizedErrorMessage = sanitizeString({
|
||||
unsanitizedString: (err as Error)?.message,
|
||||
tokens
|
||||
});
|
||||
await client.shutdown();
|
||||
throw new BadRequestError({
|
||||
message: `Failed to revoke lease from provider: ${sanitizedErrorMessage}`
|
||||
});
|
||||
}
|
||||
await client.shutdown();
|
||||
return { entityId: username };
|
||||
};
|
||||
|
||||
const renew = async (inputs: unknown, entityId: string, expireAt: number) => {
|
||||
@@ -130,21 +178,36 @@ export const CassandraProvider = (): TDynamicProviderFns => {
|
||||
if (!providerInputs.renewStatement) return { entityId };
|
||||
|
||||
const client = await $getClient(providerInputs);
|
||||
|
||||
const expiration = new Date(expireAt).toISOString();
|
||||
const { keyspace } = providerInputs;
|
||||
|
||||
const renewStatement = handlebars.compile(providerInputs.renewStatement)({
|
||||
username: entityId,
|
||||
keyspace,
|
||||
expiration
|
||||
});
|
||||
const queries = renewStatement.toString().split(";").filter(Boolean);
|
||||
for await (const query of queries) {
|
||||
await client.execute(query);
|
||||
try {
|
||||
const expiration = new Date(expireAt).toISOString();
|
||||
|
||||
const renewStatement = handlebars.compile(providerInputs.renewStatement)({
|
||||
username: entityId,
|
||||
keyspace,
|
||||
expiration
|
||||
});
|
||||
const queries = renewStatement.toString().split(";").filter(Boolean);
|
||||
for await (const query of queries) {
|
||||
await client.execute(query);
|
||||
}
|
||||
await client.shutdown();
|
||||
return { entityId };
|
||||
} catch (err) {
|
||||
const tokens = [entityId];
|
||||
if (keyspace) {
|
||||
tokens.push(keyspace);
|
||||
}
|
||||
const sanitizedErrorMessage = sanitizeString({
|
||||
unsanitizedString: (err as Error)?.message,
|
||||
tokens
|
||||
});
|
||||
await client.shutdown();
|
||||
throw new BadRequestError({
|
||||
message: `Failed to renew lease from provider: ${sanitizedErrorMessage}`
|
||||
});
|
||||
}
|
||||
await client.shutdown();
|
||||
return { entityId };
|
||||
};
|
||||
|
||||
return {
|
||||
|
289
backend/src/ee/services/dynamic-secret/providers/couchbase.ts
Normal file
289
backend/src/ee/services/dynamic-secret/providers/couchbase.ts
Normal file
@@ -0,0 +1,289 @@
|
||||
import crypto from "node:crypto";
|
||||
|
||||
import axios from "axios";
|
||||
import RE2 from "re2";
|
||||
|
||||
import { BadRequestError } from "@app/lib/errors";
|
||||
import { sanitizeString } from "@app/lib/fn";
|
||||
import { alphaNumericNanoId } from "@app/lib/nanoid";
|
||||
import { blockLocalAndPrivateIpAddresses } from "@app/lib/validator/validate-url";
|
||||
|
||||
import { DynamicSecretCouchbaseSchema, PasswordRequirements, TDynamicProviderFns } from "./models";
|
||||
import { compileUsernameTemplate } from "./templateUtils";
|
||||
|
||||
type TCreateCouchbaseUser = {
|
||||
name: string;
|
||||
password: string;
|
||||
access: {
|
||||
privileges: string[];
|
||||
resources: {
|
||||
buckets: {
|
||||
name: string;
|
||||
scopes?: {
|
||||
name: string;
|
||||
collections?: string[];
|
||||
}[];
|
||||
}[];
|
||||
};
|
||||
}[];
|
||||
};
|
||||
|
||||
type CouchbaseUserResponse = {
|
||||
id: string;
|
||||
uuid?: string;
|
||||
};
|
||||
|
||||
const sanitizeCouchbaseUsername = (username: string): string => {
|
||||
// Couchbase username restrictions:
|
||||
// - Cannot contain: ) ( > < , ; : " \ / ] [ ? = } {
|
||||
// - Cannot begin with @ character
|
||||
|
||||
const forbiddenCharsPattern = new RE2('[\\)\\(><,;:"\\\\\\[\\]\\?=\\}\\{]', "g");
|
||||
let sanitized = forbiddenCharsPattern.replace(username, "-");
|
||||
|
||||
const leadingAtPattern = new RE2("^@+");
|
||||
sanitized = leadingAtPattern.replace(sanitized, "");
|
||||
|
||||
if (!sanitized || sanitized.length === 0) {
|
||||
return alphaNumericNanoId(12);
|
||||
}
|
||||
|
||||
return sanitized;
|
||||
};
|
||||
|
||||
/**
|
||||
* Normalizes bucket configuration to handle wildcard (*) access consistently.
|
||||
*
|
||||
* Key behaviors:
|
||||
* - If "*" appears anywhere (string or array), grants access to ALL buckets, scopes, and collections
|
||||
*
|
||||
* @param buckets - Either a string or array of bucket configurations
|
||||
* @returns Normalized bucket resources for Couchbase API
|
||||
*/
|
||||
const normalizeBucketConfiguration = (
|
||||
buckets:
|
||||
| string
|
||||
| Array<{
|
||||
name: string;
|
||||
scopes?: Array<{
|
||||
name: string;
|
||||
collections?: string[];
|
||||
}>;
|
||||
}>
|
||||
) => {
|
||||
if (typeof buckets === "string") {
|
||||
// Simple string format - either "*" or comma-separated bucket names
|
||||
const bucketNames = buckets
|
||||
.split(",")
|
||||
.map((bucket) => bucket.trim())
|
||||
.filter((bucket) => bucket.length > 0);
|
||||
|
||||
// If "*" is present anywhere, grant access to all buckets, scopes, and collections
|
||||
if (bucketNames.includes("*") || buckets === "*") {
|
||||
return [{ name: "*" }];
|
||||
}
|
||||
return bucketNames.map((bucketName) => ({ name: bucketName }));
|
||||
}
|
||||
|
||||
// Array of bucket objects with scopes and collections
|
||||
// Check if any bucket is "*" - if so, grant access to all buckets, scopes, and collections
|
||||
const hasWildcardBucket = buckets.some((bucket) => bucket.name === "*");
|
||||
|
||||
if (hasWildcardBucket) {
|
||||
return [{ name: "*" }];
|
||||
}
|
||||
|
||||
return buckets.map((bucket) => ({
|
||||
name: bucket.name,
|
||||
scopes: bucket.scopes?.map((scope) => ({
|
||||
name: scope.name,
|
||||
collections: scope.collections || []
|
||||
}))
|
||||
}));
|
||||
};
|
||||
|
||||
const generateUsername = (usernameTemplate?: string | null, identity?: { name: string }) => {
|
||||
const randomUsername = alphaNumericNanoId(12);
|
||||
if (!usernameTemplate) return sanitizeCouchbaseUsername(randomUsername);
|
||||
|
||||
const compiledUsername = compileUsernameTemplate({
|
||||
usernameTemplate,
|
||||
randomUsername,
|
||||
identity
|
||||
});
|
||||
|
||||
return sanitizeCouchbaseUsername(compiledUsername);
|
||||
};
|
||||
|
||||
const generatePassword = (requirements?: PasswordRequirements): string => {
|
||||
const {
|
||||
length = 12,
|
||||
required = { lowercase: 1, uppercase: 1, digits: 1, symbols: 1 },
|
||||
allowedSymbols = "!@#$%^()_+-=[]{}:,?/~`"
|
||||
} = requirements || {};
|
||||
|
||||
const lowercase = "abcdefghijklmnopqrstuvwxyz";
|
||||
const uppercase = "ABCDEFGHIJKLMNOPQRSTUVWXYZ";
|
||||
const digits = "0123456789";
|
||||
const symbols = allowedSymbols;
|
||||
|
||||
let password = "";
|
||||
let remaining = length;
|
||||
|
||||
// Add required characters
|
||||
for (let i = 0; i < required.lowercase; i += 1) {
|
||||
password += lowercase[crypto.randomInt(lowercase.length)];
|
||||
remaining -= 1;
|
||||
}
|
||||
for (let i = 0; i < required.uppercase; i += 1) {
|
||||
password += uppercase[crypto.randomInt(uppercase.length)];
|
||||
remaining -= 1;
|
||||
}
|
||||
for (let i = 0; i < required.digits; i += 1) {
|
||||
password += digits[crypto.randomInt(digits.length)];
|
||||
remaining -= 1;
|
||||
}
|
||||
for (let i = 0; i < required.symbols; i += 1) {
|
||||
password += symbols[crypto.randomInt(symbols.length)];
|
||||
remaining -= 1;
|
||||
}
|
||||
|
||||
// Fill remaining with random characters from all sets
|
||||
const allChars = lowercase + uppercase + digits + symbols;
|
||||
for (let i = 0; i < remaining; i += 1) {
|
||||
password += allChars[crypto.randomInt(allChars.length)];
|
||||
}
|
||||
|
||||
// Shuffle the password
|
||||
return password
|
||||
.split("")
|
||||
.sort(() => crypto.randomInt(3) - 1)
|
||||
.join("");
|
||||
};
|
||||
|
||||
const couchbaseApiRequest = async (
|
||||
method: string,
|
||||
url: string,
|
||||
apiKey: string,
|
||||
data?: unknown
|
||||
): Promise<CouchbaseUserResponse> => {
|
||||
await blockLocalAndPrivateIpAddresses(url);
|
||||
|
||||
try {
|
||||
const response = await axios({
|
||||
method: method.toLowerCase() as "get" | "post" | "put" | "delete",
|
||||
url,
|
||||
headers: {
|
||||
Authorization: `Bearer ${apiKey}`,
|
||||
"Content-Type": "application/json"
|
||||
},
|
||||
data: data || undefined,
|
||||
timeout: 30000
|
||||
});
|
||||
|
||||
return response.data as CouchbaseUserResponse;
|
||||
} catch (err) {
|
||||
const sanitizedErrorMessage = sanitizeString({
|
||||
unsanitizedString: (err as Error)?.message,
|
||||
tokens: [apiKey]
|
||||
});
|
||||
throw new BadRequestError({
|
||||
message: `Failed to connect with provider: ${sanitizedErrorMessage}`
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
export const CouchbaseProvider = (): TDynamicProviderFns => {
|
||||
const validateProviderInputs = async (inputs: object) => {
|
||||
const providerInputs = DynamicSecretCouchbaseSchema.parse(inputs);
|
||||
|
||||
await blockLocalAndPrivateIpAddresses(providerInputs.url);
|
||||
|
||||
return providerInputs;
|
||||
};
|
||||
|
||||
const validateConnection = async (inputs: unknown): Promise<boolean> => {
|
||||
try {
|
||||
const providerInputs = await validateProviderInputs(inputs as object);
|
||||
|
||||
// Test connection by trying to get organization info
|
||||
const url = `${providerInputs.url}/v4/organizations/${providerInputs.orgId}`;
|
||||
await couchbaseApiRequest("GET", url, providerInputs.auth.apiKey);
|
||||
|
||||
return true;
|
||||
} catch (error) {
|
||||
throw new BadRequestError({
|
||||
message: `Failed to connect to Couchbase: ${error instanceof Error ? error.message : "Unknown error"}`
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const create = async ({
|
||||
inputs,
|
||||
usernameTemplate,
|
||||
identity
|
||||
}: {
|
||||
inputs: unknown;
|
||||
usernameTemplate?: string | null;
|
||||
identity?: { name: string };
|
||||
}) => {
|
||||
const providerInputs = await validateProviderInputs(inputs as object);
|
||||
|
||||
const username = generateUsername(usernameTemplate, identity);
|
||||
|
||||
const password = generatePassword(providerInputs.passwordRequirements);
|
||||
|
||||
const createUserUrl = `${providerInputs.url}/v4/organizations/${providerInputs.orgId}/projects/${providerInputs.projectId}/clusters/${providerInputs.clusterId}/users`;
|
||||
|
||||
const bucketResources = normalizeBucketConfiguration(providerInputs.buckets);
|
||||
|
||||
const userData: TCreateCouchbaseUser = {
|
||||
name: username,
|
||||
password,
|
||||
access: [
|
||||
{
|
||||
privileges: providerInputs.roles,
|
||||
resources: {
|
||||
buckets: bucketResources
|
||||
}
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
const response = await couchbaseApiRequest("POST", createUserUrl, providerInputs.auth.apiKey, userData);
|
||||
|
||||
const userUuid = response?.id || response?.uuid || username;
|
||||
|
||||
return {
|
||||
entityId: userUuid,
|
||||
data: {
|
||||
username,
|
||||
password
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
const revoke = async (inputs: unknown, entityId: string) => {
|
||||
const providerInputs = await validateProviderInputs(inputs as object);
|
||||
|
||||
const deleteUserUrl = `${providerInputs.url}/v4/organizations/${providerInputs.orgId}/projects/${providerInputs.projectId}/clusters/${providerInputs.clusterId}/users/${encodeURIComponent(entityId)}`;
|
||||
|
||||
await couchbaseApiRequest("DELETE", deleteUserUrl, providerInputs.auth.apiKey);
|
||||
|
||||
return { entityId };
|
||||
};
|
||||
|
||||
const renew = async (_inputs: unknown, entityId: string) => {
|
||||
// Couchbase Cloud API doesn't support renewing user credentials
|
||||
// The user remains valid until explicitly deleted
|
||||
return { entityId };
|
||||
};
|
||||
|
||||
return {
|
||||
validateProviderInputs,
|
||||
validateConnection,
|
||||
create,
|
||||
revoke,
|
||||
renew
|
||||
};
|
||||
};
|
@@ -2,6 +2,8 @@ import { Client as ElasticSearchClient } from "@elastic/elasticsearch";
|
||||
import { customAlphabet } from "nanoid";
|
||||
import { z } from "zod";
|
||||
|
||||
import { BadRequestError } from "@app/lib/errors";
|
||||
import { sanitizeString } from "@app/lib/fn";
|
||||
import { alphaNumericNanoId } from "@app/lib/nanoid";
|
||||
|
||||
import { verifyHostInputValidity } from "../dynamic-secret-fns";
|
||||
@@ -63,12 +65,24 @@ export const ElasticSearchProvider = (): TDynamicProviderFns => {
|
||||
const providerInputs = await validateProviderInputs(inputs);
|
||||
const connection = await $getClient(providerInputs);
|
||||
|
||||
const infoResponse = await connection
|
||||
.info()
|
||||
.then(() => true)
|
||||
.catch(() => false);
|
||||
|
||||
return infoResponse;
|
||||
try {
|
||||
const infoResponse = await connection.info().then(() => true);
|
||||
return infoResponse;
|
||||
} catch (err) {
|
||||
const tokens = [];
|
||||
if (providerInputs.auth.type === ElasticSearchAuthTypes.ApiKey) {
|
||||
tokens.push(providerInputs.auth.apiKey, providerInputs.auth.apiKeyId);
|
||||
} else {
|
||||
tokens.push(providerInputs.auth.username, providerInputs.auth.password);
|
||||
}
|
||||
const sanitizedErrorMessage = sanitizeString({
|
||||
unsanitizedString: (err as Error)?.message,
|
||||
tokens
|
||||
});
|
||||
throw new BadRequestError({
|
||||
message: `Failed to connect with provider: ${sanitizedErrorMessage}`
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const create = async (data: { inputs: unknown; usernameTemplate?: string | null; identity?: { name: string } }) => {
|
||||
@@ -79,27 +93,49 @@ export const ElasticSearchProvider = (): TDynamicProviderFns => {
|
||||
const username = generateUsername(usernameTemplate, identity);
|
||||
const password = generatePassword();
|
||||
|
||||
await connection.security.putUser({
|
||||
username,
|
||||
password,
|
||||
full_name: "Managed by Infisical.com",
|
||||
roles: providerInputs.roles
|
||||
});
|
||||
try {
|
||||
await connection.security.putUser({
|
||||
username,
|
||||
password,
|
||||
full_name: "Managed by Infisical.com",
|
||||
roles: providerInputs.roles
|
||||
});
|
||||
|
||||
await connection.close();
|
||||
return { entityId: username, data: { DB_USERNAME: username, DB_PASSWORD: password } };
|
||||
await connection.close();
|
||||
return { entityId: username, data: { DB_USERNAME: username, DB_PASSWORD: password } };
|
||||
} catch (err) {
|
||||
const sanitizedErrorMessage = sanitizeString({
|
||||
unsanitizedString: (err as Error)?.message,
|
||||
tokens: [username, password]
|
||||
});
|
||||
await connection.close();
|
||||
throw new BadRequestError({
|
||||
message: `Failed to create lease from provider: ${sanitizedErrorMessage}`
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const revoke = async (inputs: unknown, entityId: string) => {
|
||||
const providerInputs = await validateProviderInputs(inputs);
|
||||
const connection = await $getClient(providerInputs);
|
||||
|
||||
await connection.security.deleteUser({
|
||||
username: entityId
|
||||
});
|
||||
try {
|
||||
await connection.security.deleteUser({
|
||||
username: entityId
|
||||
});
|
||||
|
||||
await connection.close();
|
||||
return { entityId };
|
||||
await connection.close();
|
||||
return { entityId };
|
||||
} catch (err) {
|
||||
const sanitizedErrorMessage = sanitizeString({
|
||||
unsanitizedString: (err as Error)?.message,
|
||||
tokens: [entityId]
|
||||
});
|
||||
await connection.close();
|
||||
throw new BadRequestError({
|
||||
message: `Failed to revoke lease from provider: ${sanitizedErrorMessage}`
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const renew = async (_inputs: unknown, entityId: string) => {
|
||||
|
@@ -3,6 +3,7 @@ import { GetAccessTokenResponse } from "google-auth-library/build/src/auth/oauth
|
||||
|
||||
import { getConfig } from "@app/lib/config/env";
|
||||
import { BadRequestError, InternalServerError } from "@app/lib/errors";
|
||||
import { sanitizeString } from "@app/lib/fn";
|
||||
import { alphaNumericNanoId } from "@app/lib/nanoid";
|
||||
|
||||
import { DynamicSecretGcpIamSchema, TDynamicProviderFns } from "./models";
|
||||
@@ -65,8 +66,18 @@ export const GcpIamProvider = (): TDynamicProviderFns => {
|
||||
|
||||
const validateConnection = async (inputs: unknown) => {
|
||||
const providerInputs = await validateProviderInputs(inputs);
|
||||
await $getToken(providerInputs.serviceAccountEmail, 10);
|
||||
return true;
|
||||
try {
|
||||
await $getToken(providerInputs.serviceAccountEmail, 10);
|
||||
return true;
|
||||
} catch (err) {
|
||||
const sanitizedErrorMessage = sanitizeString({
|
||||
unsanitizedString: (err as Error)?.message,
|
||||
tokens: [providerInputs.serviceAccountEmail]
|
||||
});
|
||||
throw new BadRequestError({
|
||||
message: `Failed to connect with provider: ${sanitizedErrorMessage}`
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const create = async (data: { inputs: unknown; expireAt: number }) => {
|
||||
@@ -74,13 +85,23 @@ export const GcpIamProvider = (): TDynamicProviderFns => {
|
||||
|
||||
const providerInputs = await validateProviderInputs(inputs);
|
||||
|
||||
const now = Math.floor(Date.now() / 1000);
|
||||
const ttl = Math.max(Math.floor(expireAt / 1000) - now, 0);
|
||||
try {
|
||||
const now = Math.floor(Date.now() / 1000);
|
||||
const ttl = Math.max(Math.floor(expireAt / 1000) - now, 0);
|
||||
|
||||
const token = await $getToken(providerInputs.serviceAccountEmail, ttl);
|
||||
const entityId = alphaNumericNanoId(32);
|
||||
const token = await $getToken(providerInputs.serviceAccountEmail, ttl);
|
||||
const entityId = alphaNumericNanoId(32);
|
||||
|
||||
return { entityId, data: { SERVICE_ACCOUNT_EMAIL: providerInputs.serviceAccountEmail, TOKEN: token } };
|
||||
return { entityId, data: { SERVICE_ACCOUNT_EMAIL: providerInputs.serviceAccountEmail, TOKEN: token } };
|
||||
} catch (err) {
|
||||
const sanitizedErrorMessage = sanitizeString({
|
||||
unsanitizedString: (err as Error)?.message,
|
||||
tokens: [providerInputs.serviceAccountEmail]
|
||||
});
|
||||
throw new BadRequestError({
|
||||
message: `Failed to create lease from provider: ${sanitizedErrorMessage}`
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const revoke = async (_inputs: unknown, entityId: string) => {
|
||||
@@ -89,10 +110,21 @@ export const GcpIamProvider = (): TDynamicProviderFns => {
|
||||
};
|
||||
|
||||
const renew = async (inputs: unknown, entityId: string, expireAt: number) => {
|
||||
// To renew a token it must be re-created
|
||||
const data = await create({ inputs, expireAt });
|
||||
try {
|
||||
// To renew a token it must be re-created
|
||||
const data = await create({ inputs, expireAt });
|
||||
|
||||
return { ...data, entityId };
|
||||
return { ...data, entityId };
|
||||
} catch (err) {
|
||||
const providerInputs = await validateProviderInputs(inputs);
|
||||
const sanitizedErrorMessage = sanitizeString({
|
||||
unsanitizedString: (err as Error)?.message,
|
||||
tokens: [providerInputs.serviceAccountEmail]
|
||||
});
|
||||
throw new BadRequestError({
|
||||
message: `Failed to renew lease from provider: ${sanitizedErrorMessage}`
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
return {
|
||||
|
@@ -3,6 +3,7 @@ import jwt from "jsonwebtoken";
|
||||
|
||||
import { crypto } from "@app/lib/crypto";
|
||||
import { BadRequestError, InternalServerError } from "@app/lib/errors";
|
||||
import { sanitizeString } from "@app/lib/fn";
|
||||
import { alphaNumericNanoId } from "@app/lib/nanoid";
|
||||
import { IntegrationUrls } from "@app/services/integration-auth/integration-list";
|
||||
|
||||
@@ -89,26 +90,46 @@ export const GithubProvider = (): TDynamicProviderFns => {
|
||||
|
||||
const validateConnection = async (inputs: unknown) => {
|
||||
const providerInputs = await validateProviderInputs(inputs);
|
||||
await $generateGitHubInstallationAccessToken(providerInputs);
|
||||
return true;
|
||||
try {
|
||||
await $generateGitHubInstallationAccessToken(providerInputs);
|
||||
return true;
|
||||
} catch (err) {
|
||||
const sanitizedErrorMessage = sanitizeString({
|
||||
unsanitizedString: (err as Error)?.message,
|
||||
tokens: [providerInputs.privateKey, String(providerInputs.appId), String(providerInputs.installationId)]
|
||||
});
|
||||
throw new BadRequestError({
|
||||
message: `Failed to connect with provider: ${sanitizedErrorMessage}`
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const create = async (data: { inputs: unknown }) => {
|
||||
const { inputs } = data;
|
||||
const providerInputs = await validateProviderInputs(inputs);
|
||||
|
||||
const ghTokenData = await $generateGitHubInstallationAccessToken(providerInputs);
|
||||
const entityId = alphaNumericNanoId(32);
|
||||
try {
|
||||
const ghTokenData = await $generateGitHubInstallationAccessToken(providerInputs);
|
||||
const entityId = alphaNumericNanoId(32);
|
||||
|
||||
return {
|
||||
entityId,
|
||||
data: {
|
||||
TOKEN: ghTokenData.token,
|
||||
EXPIRES_AT: ghTokenData.expires_at,
|
||||
PERMISSIONS: ghTokenData.permissions,
|
||||
REPOSITORY_SELECTION: ghTokenData.repository_selection
|
||||
}
|
||||
};
|
||||
return {
|
||||
entityId,
|
||||
data: {
|
||||
TOKEN: ghTokenData.token,
|
||||
EXPIRES_AT: ghTokenData.expires_at,
|
||||
PERMISSIONS: ghTokenData.permissions,
|
||||
REPOSITORY_SELECTION: ghTokenData.repository_selection
|
||||
}
|
||||
};
|
||||
} catch (err) {
|
||||
const sanitizedErrorMessage = sanitizeString({
|
||||
unsanitizedString: (err as Error)?.message,
|
||||
tokens: [providerInputs.privateKey, String(providerInputs.appId), String(providerInputs.installationId)]
|
||||
});
|
||||
throw new BadRequestError({
|
||||
message: `Failed to create lease from provider: ${sanitizedErrorMessage}`
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const revoke = async () => {
|
||||
|
@@ -5,6 +5,7 @@ import { AwsElastiCacheDatabaseProvider } from "./aws-elasticache";
|
||||
import { AwsIamProvider } from "./aws-iam";
|
||||
import { AzureEntraIDProvider } from "./azure-entra-id";
|
||||
import { CassandraProvider } from "./cassandra";
|
||||
import { CouchbaseProvider } from "./couchbase";
|
||||
import { ElasticSearchProvider } from "./elastic-search";
|
||||
import { GcpIamProvider } from "./gcp-iam";
|
||||
import { GithubProvider } from "./github";
|
||||
@@ -46,5 +47,6 @@ export const buildDynamicSecretProviders = ({
|
||||
[DynamicSecretProviders.Kubernetes]: KubernetesProvider({ gatewayService }),
|
||||
[DynamicSecretProviders.Vertica]: VerticaProvider({ gatewayService }),
|
||||
[DynamicSecretProviders.GcpIam]: GcpIamProvider(),
|
||||
[DynamicSecretProviders.Github]: GithubProvider()
|
||||
[DynamicSecretProviders.Github]: GithubProvider(),
|
||||
[DynamicSecretProviders.Couchbase]: CouchbaseProvider()
|
||||
});
|
||||
|
@@ -2,7 +2,8 @@ import axios, { AxiosError } from "axios";
|
||||
import handlebars from "handlebars";
|
||||
import https from "https";
|
||||
|
||||
import { BadRequestError, InternalServerError } from "@app/lib/errors";
|
||||
import { BadRequestError } from "@app/lib/errors";
|
||||
import { sanitizeString } from "@app/lib/fn";
|
||||
import { GatewayHttpProxyActions, GatewayProxyProtocol, withGatewayProxy } from "@app/lib/gateway";
|
||||
import { alphaNumericNanoId } from "@app/lib/nanoid";
|
||||
import { blockLocalAndPrivateIpAddresses } from "@app/lib/validator";
|
||||
@@ -356,8 +357,12 @@ export const KubernetesProvider = ({ gatewayService }: TKubernetesProviderDTO):
|
||||
errorMessage = (error.response?.data as { message: string }).message;
|
||||
}
|
||||
|
||||
throw new InternalServerError({
|
||||
message: `Failed to validate connection: ${errorMessage}`
|
||||
const sanitizedErrorMessage = sanitizeString({
|
||||
unsanitizedString: errorMessage,
|
||||
tokens: [providerInputs.clusterToken || ""]
|
||||
});
|
||||
throw new BadRequestError({
|
||||
message: `Failed to connect with provider: ${sanitizedErrorMessage}`
|
||||
});
|
||||
}
|
||||
};
|
||||
@@ -602,8 +607,12 @@ export const KubernetesProvider = ({ gatewayService }: TKubernetesProviderDTO):
|
||||
errorMessage = (error.response?.data as { message: string }).message;
|
||||
}
|
||||
|
||||
throw new InternalServerError({
|
||||
message: `Failed to create dynamic secret: ${errorMessage}`
|
||||
const sanitizedErrorMessage = sanitizeString({
|
||||
unsanitizedString: errorMessage,
|
||||
tokens: [providerInputs.clusterToken || ""]
|
||||
});
|
||||
throw new BadRequestError({
|
||||
message: `Failed to create lease from provider: ${sanitizedErrorMessage}`
|
||||
});
|
||||
}
|
||||
};
|
||||
@@ -683,50 +692,65 @@ export const KubernetesProvider = ({ gatewayService }: TKubernetesProviderDTO):
|
||||
};
|
||||
|
||||
if (providerInputs.credentialType === KubernetesCredentialType.Dynamic) {
|
||||
const rawUrl =
|
||||
providerInputs.authMethod === KubernetesAuthMethod.Gateway
|
||||
? GATEWAY_AUTH_DEFAULT_URL
|
||||
: providerInputs.url || "";
|
||||
try {
|
||||
const rawUrl =
|
||||
providerInputs.authMethod === KubernetesAuthMethod.Gateway
|
||||
? GATEWAY_AUTH_DEFAULT_URL
|
||||
: providerInputs.url || "";
|
||||
|
||||
const url = new URL(rawUrl);
|
||||
const k8sGatewayHost = url.hostname;
|
||||
const k8sPort = url.port ? Number(url.port) : 443;
|
||||
const k8sHost = `${url.protocol}//${url.hostname}`;
|
||||
const url = new URL(rawUrl);
|
||||
const k8sGatewayHost = url.hostname;
|
||||
const k8sPort = url.port ? Number(url.port) : 443;
|
||||
const k8sHost = `${url.protocol}//${url.hostname}`;
|
||||
|
||||
const httpsAgent =
|
||||
providerInputs.ca && providerInputs.sslEnabled
|
||||
? new https.Agent({
|
||||
ca: providerInputs.ca,
|
||||
rejectUnauthorized: true
|
||||
})
|
||||
: undefined;
|
||||
const httpsAgent =
|
||||
providerInputs.ca && providerInputs.sslEnabled
|
||||
? new https.Agent({
|
||||
ca: providerInputs.ca,
|
||||
rejectUnauthorized: true
|
||||
})
|
||||
: undefined;
|
||||
|
||||
if (providerInputs.gatewayId) {
|
||||
if (providerInputs.authMethod === KubernetesAuthMethod.Gateway) {
|
||||
await $gatewayProxyWrapper(
|
||||
{
|
||||
gatewayId: providerInputs.gatewayId,
|
||||
targetHost: k8sHost,
|
||||
targetPort: k8sPort,
|
||||
httpsAgent,
|
||||
reviewTokenThroughGateway: true
|
||||
},
|
||||
serviceAccountDynamicCallback
|
||||
);
|
||||
if (providerInputs.gatewayId) {
|
||||
if (providerInputs.authMethod === KubernetesAuthMethod.Gateway) {
|
||||
await $gatewayProxyWrapper(
|
||||
{
|
||||
gatewayId: providerInputs.gatewayId,
|
||||
targetHost: k8sHost,
|
||||
targetPort: k8sPort,
|
||||
httpsAgent,
|
||||
reviewTokenThroughGateway: true
|
||||
},
|
||||
serviceAccountDynamicCallback
|
||||
);
|
||||
} else {
|
||||
await $gatewayProxyWrapper(
|
||||
{
|
||||
gatewayId: providerInputs.gatewayId,
|
||||
targetHost: k8sGatewayHost,
|
||||
targetPort: k8sPort,
|
||||
httpsAgent,
|
||||
reviewTokenThroughGateway: false
|
||||
},
|
||||
serviceAccountDynamicCallback
|
||||
);
|
||||
}
|
||||
} else {
|
||||
await $gatewayProxyWrapper(
|
||||
{
|
||||
gatewayId: providerInputs.gatewayId,
|
||||
targetHost: k8sGatewayHost,
|
||||
targetPort: k8sPort,
|
||||
httpsAgent,
|
||||
reviewTokenThroughGateway: false
|
||||
},
|
||||
serviceAccountDynamicCallback
|
||||
);
|
||||
await serviceAccountDynamicCallback(k8sHost, k8sPort, httpsAgent);
|
||||
}
|
||||
} else {
|
||||
await serviceAccountDynamicCallback(k8sHost, k8sPort, httpsAgent);
|
||||
} catch (error) {
|
||||
let errorMessage = error instanceof Error ? error.message : "Unknown error";
|
||||
if (axios.isAxiosError(error) && (error.response?.data as { message: string })?.message) {
|
||||
errorMessage = (error.response?.data as { message: string }).message;
|
||||
}
|
||||
|
||||
const sanitizedErrorMessage = sanitizeString({
|
||||
unsanitizedString: errorMessage,
|
||||
tokens: [entityId, providerInputs.clusterToken || ""]
|
||||
});
|
||||
throw new BadRequestError({
|
||||
message: `Failed to revoke lease from provider: ${sanitizedErrorMessage}`
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
@@ -6,6 +6,7 @@ import RE2 from "re2";
|
||||
import { z } from "zod";
|
||||
|
||||
import { BadRequestError } from "@app/lib/errors";
|
||||
import { sanitizeString } from "@app/lib/fn";
|
||||
import { alphaNumericNanoId } from "@app/lib/nanoid";
|
||||
|
||||
import { LdapCredentialType, LdapSchema, TDynamicProviderFns } from "./models";
|
||||
@@ -91,8 +92,18 @@ export const LdapProvider = (): TDynamicProviderFns => {
|
||||
|
||||
const validateConnection = async (inputs: unknown) => {
|
||||
const providerInputs = await validateProviderInputs(inputs);
|
||||
const client = await $getClient(providerInputs);
|
||||
return client.connected;
|
||||
try {
|
||||
const client = await $getClient(providerInputs);
|
||||
return client.connected;
|
||||
} catch (err) {
|
||||
const sanitizedErrorMessage = sanitizeString({
|
||||
unsanitizedString: (err as Error)?.message,
|
||||
tokens: [providerInputs.bindpass, providerInputs.binddn]
|
||||
});
|
||||
throw new BadRequestError({
|
||||
message: `Failed to connect with provider: ${sanitizedErrorMessage}`
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const executeLdif = async (client: ldapjs.Client, ldif_file: string) => {
|
||||
@@ -205,11 +216,11 @@ export const LdapProvider = (): TDynamicProviderFns => {
|
||||
if (providerInputs.credentialType === LdapCredentialType.Static) {
|
||||
const dnRegex = new RE2("^dn:\\s*(.+)", "m");
|
||||
const dnMatch = dnRegex.exec(providerInputs.rotationLdif);
|
||||
const username = dnMatch?.[1];
|
||||
if (!username) throw new BadRequestError({ message: "Username not found from Ldif" });
|
||||
const password = generatePassword();
|
||||
|
||||
if (dnMatch) {
|
||||
const username = dnMatch[1];
|
||||
const password = generatePassword();
|
||||
|
||||
const generatedLdif = generateLDIF({ username, password, ldifTemplate: providerInputs.rotationLdif });
|
||||
|
||||
try {
|
||||
@@ -217,7 +228,11 @@ export const LdapProvider = (): TDynamicProviderFns => {
|
||||
|
||||
return { entityId: username, data: { DN_ARRAY: dnArray, USERNAME: username, PASSWORD: password } };
|
||||
} catch (err) {
|
||||
throw new BadRequestError({ message: (err as Error).message });
|
||||
const sanitizedErrorMessage = sanitizeString({
|
||||
unsanitizedString: (err as Error)?.message,
|
||||
tokens: [username, password, providerInputs.binddn, providerInputs.bindpass]
|
||||
});
|
||||
throw new BadRequestError({ message: sanitizedErrorMessage });
|
||||
}
|
||||
} else {
|
||||
throw new BadRequestError({
|
||||
@@ -238,7 +253,11 @@ export const LdapProvider = (): TDynamicProviderFns => {
|
||||
const rollbackLdif = generateLDIF({ username, password, ldifTemplate: providerInputs.rollbackLdif });
|
||||
await executeLdif(client, rollbackLdif);
|
||||
}
|
||||
throw new BadRequestError({ message: (err as Error).message });
|
||||
const sanitizedErrorMessage = sanitizeString({
|
||||
unsanitizedString: (err as Error)?.message,
|
||||
tokens: [username, password, providerInputs.binddn, providerInputs.bindpass]
|
||||
});
|
||||
throw new BadRequestError({ message: sanitizedErrorMessage });
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -262,7 +281,11 @@ export const LdapProvider = (): TDynamicProviderFns => {
|
||||
|
||||
return { entityId: username, data: { DN_ARRAY: dnArray, USERNAME: username, PASSWORD: password } };
|
||||
} catch (err) {
|
||||
throw new BadRequestError({ message: (err as Error).message });
|
||||
const sanitizedErrorMessage = sanitizeString({
|
||||
unsanitizedString: (err as Error)?.message,
|
||||
tokens: [username, password, providerInputs.binddn, providerInputs.bindpass]
|
||||
});
|
||||
throw new BadRequestError({ message: sanitizedErrorMessage });
|
||||
}
|
||||
} else {
|
||||
throw new BadRequestError({
|
||||
@@ -278,7 +301,7 @@ export const LdapProvider = (): TDynamicProviderFns => {
|
||||
return { entityId };
|
||||
};
|
||||
|
||||
const renew = async (inputs: unknown, entityId: string) => {
|
||||
const renew = async (_inputs: unknown, entityId: string) => {
|
||||
// No renewal necessary
|
||||
return { entityId };
|
||||
};
|
||||
|
@@ -505,6 +505,91 @@ export const DynamicSecretGithubSchema = z.object({
|
||||
.describe("The private key generated for your GitHub App.")
|
||||
});
|
||||
|
||||
export const DynamicSecretCouchbaseSchema = z.object({
|
||||
url: z.string().url().trim().min(1).describe("Couchbase Cloud API URL"),
|
||||
orgId: z.string().trim().min(1).describe("Organization ID"),
|
||||
projectId: z.string().trim().min(1).describe("Project ID"),
|
||||
clusterId: z.string().trim().min(1).describe("Cluster ID"),
|
||||
roles: z.array(z.string().trim().min(1)).min(1).describe("Roles to assign to the user"),
|
||||
buckets: z
|
||||
.union([
|
||||
z
|
||||
.string()
|
||||
.trim()
|
||||
.min(1)
|
||||
.default("*")
|
||||
.refine((val) => {
|
||||
if (val.includes(",")) {
|
||||
const buckets = val
|
||||
.split(",")
|
||||
.map((b) => b.trim())
|
||||
.filter((b) => b.length > 0);
|
||||
if (buckets.includes("*") && buckets.length > 1) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}, "Cannot combine '*' with other bucket names"),
|
||||
z
|
||||
.array(
|
||||
z.object({
|
||||
name: z.string().trim().min(1).describe("Bucket name"),
|
||||
scopes: z
|
||||
.array(
|
||||
z.object({
|
||||
name: z.string().trim().min(1).describe("Scope name"),
|
||||
collections: z.array(z.string().trim().min(1)).optional().describe("Collection names")
|
||||
})
|
||||
)
|
||||
.optional()
|
||||
.describe("Scopes within the bucket")
|
||||
})
|
||||
)
|
||||
.refine((buckets) => {
|
||||
const hasWildcard = buckets.some((bucket) => bucket.name === "*");
|
||||
if (hasWildcard && buckets.length > 1) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}, "Cannot combine '*' bucket with other buckets")
|
||||
])
|
||||
.default("*")
|
||||
.describe(
|
||||
"Bucket configuration: '*' for all buckets, scopes, and collections or array of bucket objects with specific scopes and collections"
|
||||
),
|
||||
passwordRequirements: z
|
||||
.object({
|
||||
length: z.number().min(8, "Password must be at least 8 characters").max(128),
|
||||
required: z
|
||||
.object({
|
||||
lowercase: z.number().min(1, "At least 1 lowercase character required"),
|
||||
uppercase: z.number().min(1, "At least 1 uppercase character required"),
|
||||
digits: z.number().min(1, "At least 1 digit required"),
|
||||
symbols: z.number().min(1, "At least 1 special character required")
|
||||
})
|
||||
.refine((data) => {
|
||||
const total = Object.values(data).reduce((sum, count) => sum + count, 0);
|
||||
return total <= 128;
|
||||
}, "Sum of required characters cannot exceed 128"),
|
||||
allowedSymbols: z
|
||||
.string()
|
||||
.refine((symbols) => {
|
||||
const forbiddenChars = ["<", ">", ";", ".", "*", "&", "|", "£"];
|
||||
return !forbiddenChars.some((char) => symbols?.includes(char));
|
||||
}, "Cannot contain: < > ; . * & | £")
|
||||
.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 for Couchbase"),
|
||||
auth: z.object({
|
||||
apiKey: z.string().trim().min(1).describe("Couchbase Cloud API Key")
|
||||
})
|
||||
});
|
||||
|
||||
export enum DynamicSecretProviders {
|
||||
SqlDatabase = "sql-database",
|
||||
Cassandra = "cassandra",
|
||||
@@ -524,7 +609,8 @@ export enum DynamicSecretProviders {
|
||||
Kubernetes = "kubernetes",
|
||||
Vertica = "vertica",
|
||||
GcpIam = "gcp-iam",
|
||||
Github = "github"
|
||||
Github = "github",
|
||||
Couchbase = "couchbase"
|
||||
}
|
||||
|
||||
export const DynamicSecretProviderSchema = z.discriminatedUnion("type", [
|
||||
@@ -546,7 +632,8 @@ export const DynamicSecretProviderSchema = z.discriminatedUnion("type", [
|
||||
z.object({ type: z.literal(DynamicSecretProviders.Kubernetes), inputs: DynamicSecretKubernetesSchema }),
|
||||
z.object({ type: z.literal(DynamicSecretProviders.Vertica), inputs: DynamicSecretVerticaSchema }),
|
||||
z.object({ type: z.literal(DynamicSecretProviders.GcpIam), inputs: DynamicSecretGcpIamSchema }),
|
||||
z.object({ type: z.literal(DynamicSecretProviders.Github), inputs: DynamicSecretGithubSchema })
|
||||
z.object({ type: z.literal(DynamicSecretProviders.Github), inputs: DynamicSecretGithubSchema }),
|
||||
z.object({ type: z.literal(DynamicSecretProviders.Couchbase), inputs: DynamicSecretCouchbaseSchema })
|
||||
]);
|
||||
|
||||
export type TDynamicProviderFns = {
|
||||
|
@@ -3,6 +3,8 @@ import { customAlphabet } from "nanoid";
|
||||
import { z } from "zod";
|
||||
|
||||
import { createDigestAuthRequestInterceptor } from "@app/lib/axios/digest-auth";
|
||||
import { BadRequestError } from "@app/lib/errors";
|
||||
import { sanitizeString } from "@app/lib/fn";
|
||||
import { alphaNumericNanoId } from "@app/lib/nanoid";
|
||||
|
||||
import { DynamicSecretMongoAtlasSchema, TDynamicProviderFns } from "./models";
|
||||
@@ -49,19 +51,25 @@ export const MongoAtlasProvider = (): TDynamicProviderFns => {
|
||||
const providerInputs = await validateProviderInputs(inputs);
|
||||
const client = await $getClient(providerInputs);
|
||||
|
||||
const isConnected = await client({
|
||||
method: "GET",
|
||||
url: `v2/groups/${providerInputs.groupId}/databaseUsers`,
|
||||
params: { itemsPerPage: 1 }
|
||||
})
|
||||
.then(() => true)
|
||||
.catch((error) => {
|
||||
if ((error as AxiosError).response) {
|
||||
throw new Error(JSON.stringify((error as AxiosError).response?.data));
|
||||
}
|
||||
throw error;
|
||||
try {
|
||||
const isConnected = await client({
|
||||
method: "GET",
|
||||
url: `v2/groups/${providerInputs.groupId}/databaseUsers`,
|
||||
params: { itemsPerPage: 1 }
|
||||
}).then(() => true);
|
||||
return isConnected;
|
||||
} catch (error) {
|
||||
const errorMessage = (error as AxiosError).response
|
||||
? JSON.stringify((error as AxiosError).response?.data)
|
||||
: (error as Error)?.message;
|
||||
const sanitizedErrorMessage = sanitizeString({
|
||||
unsanitizedString: errorMessage,
|
||||
tokens: [providerInputs.adminPublicKey, providerInputs.adminPrivateKey, providerInputs.groupId]
|
||||
});
|
||||
return isConnected;
|
||||
throw new BadRequestError({
|
||||
message: `Failed to connect with provider: ${sanitizedErrorMessage}`
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const create = async (data: {
|
||||
@@ -77,25 +85,39 @@ export const MongoAtlasProvider = (): TDynamicProviderFns => {
|
||||
const username = generateUsername(usernameTemplate, identity);
|
||||
const password = generatePassword();
|
||||
const expiration = new Date(expireAt).toISOString();
|
||||
await client({
|
||||
method: "POST",
|
||||
url: `/v2/groups/${providerInputs.groupId}/databaseUsers`,
|
||||
data: {
|
||||
roles: providerInputs.roles,
|
||||
scopes: providerInputs.scopes,
|
||||
deleteAfterDate: expiration,
|
||||
username,
|
||||
password,
|
||||
databaseName: "admin",
|
||||
groupId: providerInputs.groupId
|
||||
}
|
||||
}).catch((error) => {
|
||||
if ((error as AxiosError).response) {
|
||||
throw new Error(JSON.stringify((error as AxiosError).response?.data));
|
||||
}
|
||||
throw error;
|
||||
});
|
||||
return { entityId: username, data: { DB_USERNAME: username, DB_PASSWORD: password } };
|
||||
try {
|
||||
await client({
|
||||
method: "POST",
|
||||
url: `/v2/groups/${providerInputs.groupId}/databaseUsers`,
|
||||
data: {
|
||||
roles: providerInputs.roles,
|
||||
scopes: providerInputs.scopes,
|
||||
deleteAfterDate: expiration,
|
||||
username,
|
||||
password,
|
||||
databaseName: "admin",
|
||||
groupId: providerInputs.groupId
|
||||
}
|
||||
});
|
||||
return { entityId: username, data: { DB_USERNAME: username, DB_PASSWORD: password } };
|
||||
} catch (error) {
|
||||
const errorMessage = (error as AxiosError).response
|
||||
? JSON.stringify((error as AxiosError).response?.data)
|
||||
: (error as Error)?.message;
|
||||
const sanitizedErrorMessage = sanitizeString({
|
||||
unsanitizedString: errorMessage,
|
||||
tokens: [
|
||||
username,
|
||||
password,
|
||||
providerInputs.adminPublicKey,
|
||||
providerInputs.adminPrivateKey,
|
||||
providerInputs.groupId
|
||||
]
|
||||
});
|
||||
throw new BadRequestError({
|
||||
message: `Failed to create lease from provider: ${sanitizedErrorMessage}`
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const revoke = async (inputs: unknown, entityId: string) => {
|
||||
@@ -111,15 +133,23 @@ export const MongoAtlasProvider = (): TDynamicProviderFns => {
|
||||
throw err;
|
||||
});
|
||||
if (isExisting) {
|
||||
await client({
|
||||
method: "DELETE",
|
||||
url: `/v2/groups/${providerInputs.groupId}/databaseUsers/admin/${username}`
|
||||
}).catch((error) => {
|
||||
if ((error as AxiosError).response) {
|
||||
throw new Error(JSON.stringify((error as AxiosError).response?.data));
|
||||
}
|
||||
throw error;
|
||||
});
|
||||
try {
|
||||
await client({
|
||||
method: "DELETE",
|
||||
url: `/v2/groups/${providerInputs.groupId}/databaseUsers/admin/${username}`
|
||||
});
|
||||
} catch (error) {
|
||||
const errorMessage = (error as AxiosError).response
|
||||
? JSON.stringify((error as AxiosError).response?.data)
|
||||
: (error as Error)?.message;
|
||||
const sanitizedErrorMessage = sanitizeString({
|
||||
unsanitizedString: errorMessage,
|
||||
tokens: [username, providerInputs.adminPublicKey, providerInputs.adminPrivateKey, providerInputs.groupId]
|
||||
});
|
||||
throw new BadRequestError({
|
||||
message: `Failed to revoke lease from provider: ${sanitizedErrorMessage}`
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return { entityId: username };
|
||||
@@ -132,21 +162,29 @@ export const MongoAtlasProvider = (): TDynamicProviderFns => {
|
||||
const username = entityId;
|
||||
const expiration = new Date(expireAt).toISOString();
|
||||
|
||||
await client({
|
||||
method: "PATCH",
|
||||
url: `/v2/groups/${providerInputs.groupId}/databaseUsers/admin/${username}`,
|
||||
data: {
|
||||
deleteAfterDate: expiration,
|
||||
databaseName: "admin",
|
||||
groupId: providerInputs.groupId
|
||||
}
|
||||
}).catch((error) => {
|
||||
if ((error as AxiosError).response) {
|
||||
throw new Error(JSON.stringify((error as AxiosError).response?.data));
|
||||
}
|
||||
throw error;
|
||||
});
|
||||
return { entityId: username };
|
||||
try {
|
||||
await client({
|
||||
method: "PATCH",
|
||||
url: `/v2/groups/${providerInputs.groupId}/databaseUsers/admin/${username}`,
|
||||
data: {
|
||||
deleteAfterDate: expiration,
|
||||
databaseName: "admin",
|
||||
groupId: providerInputs.groupId
|
||||
}
|
||||
});
|
||||
return { entityId: username };
|
||||
} catch (error) {
|
||||
const errorMessage = (error as AxiosError).response
|
||||
? JSON.stringify((error as AxiosError).response?.data)
|
||||
: (error as Error)?.message;
|
||||
const sanitizedErrorMessage = sanitizeString({
|
||||
unsanitizedString: errorMessage,
|
||||
tokens: [username, providerInputs.adminPublicKey, providerInputs.adminPrivateKey, providerInputs.groupId]
|
||||
});
|
||||
throw new BadRequestError({
|
||||
message: `Failed to renew lease from provider: ${sanitizedErrorMessage}`
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
return {
|
||||
|
@@ -2,6 +2,8 @@ import { MongoClient } from "mongodb";
|
||||
import { customAlphabet } from "nanoid";
|
||||
import { z } from "zod";
|
||||
|
||||
import { BadRequestError } from "@app/lib/errors";
|
||||
import { sanitizeString } from "@app/lib/fn";
|
||||
import { alphaNumericNanoId } from "@app/lib/nanoid";
|
||||
|
||||
import { verifyHostInputValidity } from "../dynamic-secret-fns";
|
||||
@@ -51,13 +53,24 @@ export const MongoDBProvider = (): TDynamicProviderFns => {
|
||||
const providerInputs = await validateProviderInputs(inputs);
|
||||
const client = await $getClient(providerInputs);
|
||||
|
||||
const isConnected = await client
|
||||
.db(providerInputs.database)
|
||||
.command({ ping: 1 })
|
||||
.then(() => true);
|
||||
try {
|
||||
const isConnected = await client
|
||||
.db(providerInputs.database)
|
||||
.command({ ping: 1 })
|
||||
.then(() => true);
|
||||
|
||||
await client.close();
|
||||
return isConnected;
|
||||
await client.close();
|
||||
return isConnected;
|
||||
} catch (err) {
|
||||
await client.close();
|
||||
const sanitizedErrorMessage = sanitizeString({
|
||||
unsanitizedString: (err as Error)?.message,
|
||||
tokens: [providerInputs.password, providerInputs.username, providerInputs.database, providerInputs.host]
|
||||
});
|
||||
throw new BadRequestError({
|
||||
message: `Failed to connect with provider: ${sanitizedErrorMessage}`
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const create = async (data: { inputs: unknown; usernameTemplate?: string | null; identity?: { name: string } }) => {
|
||||
@@ -68,16 +81,27 @@ export const MongoDBProvider = (): TDynamicProviderFns => {
|
||||
const username = generateUsername(usernameTemplate, identity);
|
||||
const password = generatePassword();
|
||||
|
||||
const db = client.db(providerInputs.database);
|
||||
try {
|
||||
const db = client.db(providerInputs.database);
|
||||
|
||||
await db.command({
|
||||
createUser: username,
|
||||
pwd: password,
|
||||
roles: providerInputs.roles
|
||||
});
|
||||
await client.close();
|
||||
await db.command({
|
||||
createUser: username,
|
||||
pwd: password,
|
||||
roles: providerInputs.roles
|
||||
});
|
||||
await client.close();
|
||||
|
||||
return { entityId: username, data: { DB_USERNAME: username, DB_PASSWORD: password } };
|
||||
return { entityId: username, data: { DB_USERNAME: username, DB_PASSWORD: password } };
|
||||
} catch (err) {
|
||||
await client.close();
|
||||
const sanitizedErrorMessage = sanitizeString({
|
||||
unsanitizedString: (err as Error)?.message,
|
||||
tokens: [username, password, providerInputs.password, providerInputs.username, providerInputs.database]
|
||||
});
|
||||
throw new BadRequestError({
|
||||
message: `Failed to create lease from provider: ${sanitizedErrorMessage}`
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const revoke = async (inputs: unknown, entityId: string) => {
|
||||
@@ -86,13 +110,24 @@ export const MongoDBProvider = (): TDynamicProviderFns => {
|
||||
|
||||
const username = entityId;
|
||||
|
||||
const db = client.db(providerInputs.database);
|
||||
await db.command({
|
||||
dropUser: username
|
||||
});
|
||||
await client.close();
|
||||
try {
|
||||
const db = client.db(providerInputs.database);
|
||||
await db.command({
|
||||
dropUser: username
|
||||
});
|
||||
await client.close();
|
||||
|
||||
return { entityId: username };
|
||||
return { entityId: username };
|
||||
} catch (err) {
|
||||
await client.close();
|
||||
const sanitizedErrorMessage = sanitizeString({
|
||||
unsanitizedString: (err as Error)?.message,
|
||||
tokens: [username, providerInputs.password, providerInputs.username, providerInputs.database]
|
||||
});
|
||||
throw new BadRequestError({
|
||||
message: `Failed to revoke lease from provider: ${sanitizedErrorMessage}`
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const renew = async (_inputs: unknown, entityId: string) => {
|
||||
|
@@ -3,6 +3,8 @@ import https from "https";
|
||||
import { customAlphabet } from "nanoid";
|
||||
import { z } from "zod";
|
||||
|
||||
import { BadRequestError } from "@app/lib/errors";
|
||||
import { sanitizeString } from "@app/lib/fn";
|
||||
import { logger } from "@app/lib/logger";
|
||||
import { alphaNumericNanoId } from "@app/lib/nanoid";
|
||||
|
||||
@@ -110,11 +112,19 @@ export const RabbitMqProvider = (): TDynamicProviderFns => {
|
||||
|
||||
const validateConnection = async (inputs: unknown) => {
|
||||
const providerInputs = await validateProviderInputs(inputs);
|
||||
const connection = await $getClient(providerInputs);
|
||||
|
||||
const infoResponse = await connection.get("/whoami").then(() => true);
|
||||
|
||||
return infoResponse;
|
||||
try {
|
||||
const connection = await $getClient(providerInputs);
|
||||
const infoResponse = await connection.get("/whoami").then(() => true);
|
||||
return infoResponse;
|
||||
} catch (err) {
|
||||
const sanitizedErrorMessage = sanitizeString({
|
||||
unsanitizedString: (err as Error)?.message,
|
||||
tokens: [providerInputs.password, providerInputs.username, providerInputs.host]
|
||||
});
|
||||
throw new BadRequestError({
|
||||
message: `Failed to connect with provider: ${sanitizedErrorMessage}`
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const create = async (data: { inputs: unknown; usernameTemplate?: string | null; identity?: { name: string } }) => {
|
||||
@@ -125,26 +135,44 @@ export const RabbitMqProvider = (): TDynamicProviderFns => {
|
||||
const username = generateUsername(usernameTemplate, identity);
|
||||
const password = generatePassword();
|
||||
|
||||
await createRabbitMqUser({
|
||||
axiosInstance: connection,
|
||||
virtualHost: providerInputs.virtualHost,
|
||||
createUser: {
|
||||
password,
|
||||
username,
|
||||
tags: [...(providerInputs.tags ?? []), "infisical-user"]
|
||||
}
|
||||
});
|
||||
|
||||
return { entityId: username, data: { DB_USERNAME: username, DB_PASSWORD: password } };
|
||||
try {
|
||||
await createRabbitMqUser({
|
||||
axiosInstance: connection,
|
||||
virtualHost: providerInputs.virtualHost,
|
||||
createUser: {
|
||||
password,
|
||||
username,
|
||||
tags: [...(providerInputs.tags ?? []), "infisical-user"]
|
||||
}
|
||||
});
|
||||
return { entityId: username, data: { DB_USERNAME: username, DB_PASSWORD: password } };
|
||||
} catch (err) {
|
||||
const sanitizedErrorMessage = sanitizeString({
|
||||
unsanitizedString: (err as Error)?.message,
|
||||
tokens: [username, password, providerInputs.password, providerInputs.username]
|
||||
});
|
||||
throw new BadRequestError({
|
||||
message: `Failed to create lease from provider: ${sanitizedErrorMessage}`
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const revoke = async (inputs: unknown, entityId: string) => {
|
||||
const providerInputs = await validateProviderInputs(inputs);
|
||||
const connection = await $getClient(providerInputs);
|
||||
|
||||
await deleteRabbitMqUser({ axiosInstance: connection, usernameToDelete: entityId });
|
||||
|
||||
return { entityId };
|
||||
try {
|
||||
await deleteRabbitMqUser({ axiosInstance: connection, usernameToDelete: entityId });
|
||||
return { entityId };
|
||||
} catch (err) {
|
||||
const sanitizedErrorMessage = sanitizeString({
|
||||
unsanitizedString: (err as Error)?.message,
|
||||
tokens: [entityId, providerInputs.password, providerInputs.username]
|
||||
});
|
||||
throw new BadRequestError({
|
||||
message: `Failed to revoke lease from provider: ${sanitizedErrorMessage}`
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const renew = async (_inputs: unknown, entityId: string) => {
|
||||
|
@@ -4,6 +4,7 @@ import { customAlphabet } from "nanoid";
|
||||
import { z } from "zod";
|
||||
|
||||
import { BadRequestError } from "@app/lib/errors";
|
||||
import { sanitizeString } from "@app/lib/fn";
|
||||
import { alphaNumericNanoId } from "@app/lib/nanoid";
|
||||
import { validateHandlebarTemplate } from "@app/lib/template/validate-handlebars";
|
||||
|
||||
@@ -112,14 +113,27 @@ export const RedisDatabaseProvider = (): TDynamicProviderFns => {
|
||||
|
||||
const validateConnection = async (inputs: unknown) => {
|
||||
const providerInputs = await validateProviderInputs(inputs);
|
||||
const connection = await $getClient(providerInputs);
|
||||
|
||||
const pingResponse = await connection
|
||||
.ping()
|
||||
.then(() => true)
|
||||
.catch(() => false);
|
||||
|
||||
return pingResponse;
|
||||
let connection;
|
||||
try {
|
||||
connection = await $getClient(providerInputs);
|
||||
const pingResponse = await connection.ping().then(() => true);
|
||||
await connection.quit();
|
||||
return pingResponse;
|
||||
} catch (err) {
|
||||
if (connection) await connection.quit();
|
||||
const sanitizedErrorMessage = sanitizeString({
|
||||
unsanitizedString: (err as Error)?.message,
|
||||
tokens: [
|
||||
providerInputs.password || "",
|
||||
providerInputs.username,
|
||||
providerInputs.host,
|
||||
String(providerInputs.port)
|
||||
]
|
||||
});
|
||||
throw new BadRequestError({
|
||||
message: `Failed to connect with provider: ${sanitizedErrorMessage}`
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const create = async (data: {
|
||||
@@ -144,10 +158,20 @@ export const RedisDatabaseProvider = (): TDynamicProviderFns => {
|
||||
|
||||
const queries = creationStatement.toString().split(";").filter(Boolean);
|
||||
|
||||
await executeTransactions(connection, queries);
|
||||
|
||||
await connection.quit();
|
||||
return { entityId: username, data: { DB_USERNAME: username, DB_PASSWORD: password } };
|
||||
try {
|
||||
await executeTransactions(connection, queries);
|
||||
await connection.quit();
|
||||
return { entityId: username, data: { DB_USERNAME: username, DB_PASSWORD: password } };
|
||||
} catch (err) {
|
||||
await connection.quit();
|
||||
const sanitizedErrorMessage = sanitizeString({
|
||||
unsanitizedString: (err as Error)?.message,
|
||||
tokens: [username, password, providerInputs.password || "", providerInputs.username]
|
||||
});
|
||||
throw new BadRequestError({
|
||||
message: `Failed to create lease from provider: ${sanitizedErrorMessage}`
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const revoke = async (inputs: unknown, entityId: string) => {
|
||||
@@ -159,10 +183,20 @@ export const RedisDatabaseProvider = (): TDynamicProviderFns => {
|
||||
const revokeStatement = handlebars.compile(providerInputs.revocationStatement)({ username });
|
||||
const queries = revokeStatement.toString().split(";").filter(Boolean);
|
||||
|
||||
await executeTransactions(connection, queries);
|
||||
|
||||
await connection.quit();
|
||||
return { entityId: username };
|
||||
try {
|
||||
await executeTransactions(connection, queries);
|
||||
await connection.quit();
|
||||
return { entityId: username };
|
||||
} catch (err) {
|
||||
await connection.quit();
|
||||
const sanitizedErrorMessage = sanitizeString({
|
||||
unsanitizedString: (err as Error)?.message,
|
||||
tokens: [username, providerInputs.password || "", providerInputs.username]
|
||||
});
|
||||
throw new BadRequestError({
|
||||
message: `Failed to revoke lease from provider: ${sanitizedErrorMessage}`
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const renew = async (inputs: unknown, entityId: string, expireAt: number) => {
|
||||
@@ -176,13 +210,23 @@ export const RedisDatabaseProvider = (): TDynamicProviderFns => {
|
||||
|
||||
const renewStatement = handlebars.compile(providerInputs.renewStatement)({ username, expiration });
|
||||
|
||||
if (renewStatement) {
|
||||
const queries = renewStatement.toString().split(";").filter(Boolean);
|
||||
await executeTransactions(connection, queries);
|
||||
try {
|
||||
if (renewStatement) {
|
||||
const queries = renewStatement.toString().split(";").filter(Boolean);
|
||||
await executeTransactions(connection, queries);
|
||||
}
|
||||
await connection.quit();
|
||||
return { entityId: username };
|
||||
} catch (err) {
|
||||
await connection.quit();
|
||||
const sanitizedErrorMessage = sanitizeString({
|
||||
unsanitizedString: (err as Error)?.message,
|
||||
tokens: [username, providerInputs.password || "", providerInputs.username]
|
||||
});
|
||||
throw new BadRequestError({
|
||||
message: `Failed to renew lease from provider: ${sanitizedErrorMessage}`
|
||||
});
|
||||
}
|
||||
|
||||
await connection.quit();
|
||||
return { entityId: username };
|
||||
};
|
||||
|
||||
return {
|
||||
|
@@ -4,6 +4,7 @@ import odbc from "odbc";
|
||||
import { z } from "zod";
|
||||
|
||||
import { BadRequestError } from "@app/lib/errors";
|
||||
import { sanitizeString } from "@app/lib/fn";
|
||||
import { alphaNumericNanoId } from "@app/lib/nanoid";
|
||||
import { validateHandlebarTemplate } from "@app/lib/template/validate-handlebars";
|
||||
|
||||
@@ -67,25 +68,41 @@ export const SapAseProvider = (): TDynamicProviderFns => {
|
||||
|
||||
const validateConnection = async (inputs: unknown) => {
|
||||
const providerInputs = await validateProviderInputs(inputs);
|
||||
const masterClient = await $getClient(providerInputs, true);
|
||||
const client = await $getClient(providerInputs);
|
||||
let masterClient;
|
||||
let client;
|
||||
try {
|
||||
masterClient = await $getClient(providerInputs, true);
|
||||
client = await $getClient(providerInputs);
|
||||
|
||||
const [resultFromMasterDatabase] = await masterClient.query<{ version: string }>("SELECT @@VERSION AS version");
|
||||
const [resultFromSelectedDatabase] = await client.query<{ version: string }>("SELECT @@VERSION AS version");
|
||||
const [resultFromMasterDatabase] = await masterClient.query<{ version: string }>("SELECT @@VERSION AS version");
|
||||
const [resultFromSelectedDatabase] = await client.query<{ version: string }>("SELECT @@VERSION AS version");
|
||||
|
||||
if (!resultFromSelectedDatabase.version) {
|
||||
if (!resultFromSelectedDatabase.version) {
|
||||
throw new BadRequestError({
|
||||
message: "Failed to validate SAP ASE connection, version query failed"
|
||||
});
|
||||
}
|
||||
|
||||
if (resultFromMasterDatabase.version !== resultFromSelectedDatabase.version) {
|
||||
throw new BadRequestError({
|
||||
message: "Failed to validate SAP ASE connection (master), version mismatch"
|
||||
});
|
||||
}
|
||||
|
||||
await masterClient.close();
|
||||
await client.close();
|
||||
return true;
|
||||
} catch (err) {
|
||||
if (masterClient) await masterClient.close();
|
||||
if (client) await client.close();
|
||||
const sanitizedErrorMessage = sanitizeString({
|
||||
unsanitizedString: (err as Error)?.message,
|
||||
tokens: [providerInputs.password, providerInputs.username, providerInputs.host, providerInputs.database]
|
||||
});
|
||||
throw new BadRequestError({
|
||||
message: "Failed to validate SAP ASE connection, version query failed"
|
||||
message: `Failed to connect with provider: ${sanitizedErrorMessage}`
|
||||
});
|
||||
}
|
||||
|
||||
if (resultFromMasterDatabase.version !== resultFromSelectedDatabase.version) {
|
||||
throw new BadRequestError({
|
||||
message: "Failed to validate SAP ASE connection (master), version mismatch"
|
||||
});
|
||||
}
|
||||
|
||||
return true;
|
||||
};
|
||||
|
||||
const create = async (data: { inputs: unknown; usernameTemplate?: string | null; identity?: { name: string } }) => {
|
||||
@@ -105,16 +122,26 @@ export const SapAseProvider = (): TDynamicProviderFns => {
|
||||
|
||||
const queries = creationStatement.trim().replaceAll("\n", "").split(";").filter(Boolean);
|
||||
|
||||
for await (const query of queries) {
|
||||
// If it's an adduser query, we need to first call sp_addlogin on the MASTER database.
|
||||
// If not done, then the newly created user won't be able to authenticate.
|
||||
await (query.startsWith(SapCommands.CreateLogin) ? masterClient : client).query(query);
|
||||
try {
|
||||
for await (const query of queries) {
|
||||
// If it's an adduser query, we need to first call sp_addlogin on the MASTER database.
|
||||
// If not done, then the newly created user won't be able to authenticate.
|
||||
await (query.startsWith(SapCommands.CreateLogin) ? masterClient : client).query(query);
|
||||
}
|
||||
await masterClient.close();
|
||||
await client.close();
|
||||
return { entityId: username, data: { DB_USERNAME: username, DB_PASSWORD: password } };
|
||||
} catch (err) {
|
||||
await masterClient.close();
|
||||
await client.close();
|
||||
const sanitizedErrorMessage = sanitizeString({
|
||||
unsanitizedString: (err as Error)?.message,
|
||||
tokens: [username, password, providerInputs.password, providerInputs.username, providerInputs.database]
|
||||
});
|
||||
throw new BadRequestError({
|
||||
message: `Failed to create lease from provider: ${sanitizedErrorMessage}`
|
||||
});
|
||||
}
|
||||
|
||||
await masterClient.close();
|
||||
await client.close();
|
||||
|
||||
return { entityId: username, data: { DB_USERNAME: username, DB_PASSWORD: password } };
|
||||
};
|
||||
|
||||
const revoke = async (inputs: unknown, username: string) => {
|
||||
@@ -140,14 +167,24 @@ export const SapAseProvider = (): TDynamicProviderFns => {
|
||||
}
|
||||
}
|
||||
|
||||
for await (const query of queries) {
|
||||
await (query.startsWith(SapCommands.DropLogin) ? masterClient : client).query(query);
|
||||
try {
|
||||
for await (const query of queries) {
|
||||
await (query.startsWith(SapCommands.DropLogin) ? masterClient : client).query(query);
|
||||
}
|
||||
await masterClient.close();
|
||||
await client.close();
|
||||
return { entityId: username };
|
||||
} catch (err) {
|
||||
await masterClient.close();
|
||||
await client.close();
|
||||
const sanitizedErrorMessage = sanitizeString({
|
||||
unsanitizedString: (err as Error)?.message,
|
||||
tokens: [username, providerInputs.password, providerInputs.username, providerInputs.database]
|
||||
});
|
||||
throw new BadRequestError({
|
||||
message: `Failed to revoke lease from provider: ${sanitizedErrorMessage}`
|
||||
});
|
||||
}
|
||||
|
||||
await masterClient.close();
|
||||
await client.close();
|
||||
|
||||
return { entityId: username };
|
||||
};
|
||||
|
||||
const renew = async (_: unknown, username: string) => {
|
||||
|
@@ -10,6 +10,7 @@ import { customAlphabet } from "nanoid";
|
||||
import { z } from "zod";
|
||||
|
||||
import { BadRequestError } from "@app/lib/errors";
|
||||
import { sanitizeString } from "@app/lib/fn";
|
||||
import { alphaNumericNanoId } from "@app/lib/nanoid";
|
||||
import { validateHandlebarTemplate } from "@app/lib/template/validate-handlebars";
|
||||
|
||||
@@ -83,19 +84,26 @@ export const SapHanaProvider = (): TDynamicProviderFns => {
|
||||
|
||||
const validateConnection = async (inputs: unknown) => {
|
||||
const providerInputs = await validateProviderInputs(inputs);
|
||||
const client = await $getClient(providerInputs);
|
||||
|
||||
const testResult = await new Promise<boolean>((resolve, reject) => {
|
||||
client.exec("SELECT 1 FROM DUMMY;", (err: any) => {
|
||||
if (err) {
|
||||
reject();
|
||||
}
|
||||
|
||||
resolve(true);
|
||||
try {
|
||||
const client = await $getClient(providerInputs);
|
||||
const testResult = await new Promise<boolean>((resolve, reject) => {
|
||||
client.exec("SELECT 1 FROM DUMMY;", (err: any) => {
|
||||
if (err) {
|
||||
return reject(err);
|
||||
}
|
||||
resolve(true);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
return testResult;
|
||||
return testResult;
|
||||
} catch (err) {
|
||||
const sanitizedErrorMessage = sanitizeString({
|
||||
unsanitizedString: (err as Error)?.message,
|
||||
tokens: [providerInputs.password, providerInputs.username, providerInputs.host]
|
||||
});
|
||||
throw new BadRequestError({
|
||||
message: `Failed to connect with provider: ${sanitizedErrorMessage}`
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const create = async (data: {
|
||||
@@ -119,18 +127,22 @@ export const SapHanaProvider = (): TDynamicProviderFns => {
|
||||
});
|
||||
|
||||
const queries = creationStatement.toString().split(";").filter(Boolean);
|
||||
for await (const query of queries) {
|
||||
await new Promise((resolve, reject) => {
|
||||
client.exec(query, (err: any) => {
|
||||
if (err) {
|
||||
reject(
|
||||
new BadRequestError({
|
||||
message: err.message
|
||||
})
|
||||
);
|
||||
}
|
||||
resolve(true);
|
||||
try {
|
||||
for await (const query of queries) {
|
||||
await new Promise((resolve, reject) => {
|
||||
client.exec(query, (err: any) => {
|
||||
if (err) return reject(err);
|
||||
resolve(true);
|
||||
});
|
||||
});
|
||||
}
|
||||
} catch (err) {
|
||||
const sanitizedErrorMessage = sanitizeString({
|
||||
unsanitizedString: (err as Error)?.message,
|
||||
tokens: [username, password, providerInputs.password, providerInputs.username]
|
||||
});
|
||||
throw new BadRequestError({
|
||||
message: `Failed to create lease from provider: ${sanitizedErrorMessage}`
|
||||
});
|
||||
}
|
||||
|
||||
@@ -142,18 +154,24 @@ export const SapHanaProvider = (): TDynamicProviderFns => {
|
||||
const client = await $getClient(providerInputs);
|
||||
const revokeStatement = handlebars.compile(providerInputs.revocationStatement)({ username });
|
||||
const queries = revokeStatement.toString().split(";").filter(Boolean);
|
||||
for await (const query of queries) {
|
||||
await new Promise((resolve, reject) => {
|
||||
client.exec(query, (err: any) => {
|
||||
if (err) {
|
||||
reject(
|
||||
new BadRequestError({
|
||||
message: err.message
|
||||
})
|
||||
);
|
||||
}
|
||||
resolve(true);
|
||||
try {
|
||||
for await (const query of queries) {
|
||||
await new Promise((resolve, reject) => {
|
||||
client.exec(query, (err: any) => {
|
||||
if (err) {
|
||||
reject(err);
|
||||
}
|
||||
resolve(true);
|
||||
});
|
||||
});
|
||||
}
|
||||
} catch (err) {
|
||||
const sanitizedErrorMessage = sanitizeString({
|
||||
unsanitizedString: (err as Error)?.message,
|
||||
tokens: [username, providerInputs.password, providerInputs.username]
|
||||
});
|
||||
throw new BadRequestError({
|
||||
message: `Failed to revoke lease from provider: ${sanitizedErrorMessage}`
|
||||
});
|
||||
}
|
||||
|
||||
@@ -174,16 +192,20 @@ export const SapHanaProvider = (): TDynamicProviderFns => {
|
||||
await new Promise((resolve, reject) => {
|
||||
client.exec(query, (err: any) => {
|
||||
if (err) {
|
||||
reject(
|
||||
new BadRequestError({
|
||||
message: err.message
|
||||
})
|
||||
);
|
||||
reject(err);
|
||||
}
|
||||
resolve(true);
|
||||
});
|
||||
});
|
||||
}
|
||||
} catch (err) {
|
||||
const sanitizedErrorMessage = sanitizeString({
|
||||
unsanitizedString: (err as Error)?.message,
|
||||
tokens: [entityId, providerInputs.password, providerInputs.username]
|
||||
});
|
||||
throw new BadRequestError({
|
||||
message: `Failed to renew lease from provider: ${sanitizedErrorMessage}`
|
||||
});
|
||||
} finally {
|
||||
client.disconnect();
|
||||
}
|
||||
|
@@ -4,6 +4,7 @@ import snowflake from "snowflake-sdk";
|
||||
import { z } from "zod";
|
||||
|
||||
import { BadRequestError } from "@app/lib/errors";
|
||||
import { sanitizeString } from "@app/lib/fn";
|
||||
import { alphaNumericNanoId } from "@app/lib/nanoid";
|
||||
import { validateHandlebarTemplate } from "@app/lib/template/validate-handlebars";
|
||||
|
||||
@@ -69,12 +70,10 @@ export const SnowflakeProvider = (): TDynamicProviderFns => {
|
||||
|
||||
const validateConnection = async (inputs: unknown) => {
|
||||
const providerInputs = await validateProviderInputs(inputs);
|
||||
const client = await $getClient(providerInputs);
|
||||
|
||||
let isValidConnection: boolean;
|
||||
|
||||
let client;
|
||||
try {
|
||||
isValidConnection = await Promise.race([
|
||||
client = await $getClient(providerInputs);
|
||||
const isValidConnection = await Promise.race([
|
||||
client.isValidAsync(),
|
||||
new Promise((resolve) => {
|
||||
setTimeout(resolve, 10000);
|
||||
@@ -82,11 +81,18 @@ export const SnowflakeProvider = (): TDynamicProviderFns => {
|
||||
throw new BadRequestError({ message: "Unable to establish connection - verify credentials" });
|
||||
})
|
||||
]);
|
||||
return isValidConnection;
|
||||
} catch (err) {
|
||||
const sanitizedErrorMessage = sanitizeString({
|
||||
unsanitizedString: (err as Error)?.message,
|
||||
tokens: [providerInputs.password, providerInputs.username, providerInputs.accountId, providerInputs.orgId]
|
||||
});
|
||||
throw new BadRequestError({
|
||||
message: `Failed to connect with provider: ${sanitizedErrorMessage}`
|
||||
});
|
||||
} finally {
|
||||
client.destroy(noop);
|
||||
if (client) client.destroy(noop);
|
||||
}
|
||||
|
||||
return isValidConnection;
|
||||
};
|
||||
|
||||
const create = async (data: {
|
||||
@@ -116,13 +122,19 @@ export const SnowflakeProvider = (): TDynamicProviderFns => {
|
||||
sqlText: creationStatement,
|
||||
complete(err) {
|
||||
if (err) {
|
||||
return reject(new BadRequestError({ name: "CreateLease", message: err.message }));
|
||||
return reject(err);
|
||||
}
|
||||
|
||||
return resolve(true);
|
||||
}
|
||||
});
|
||||
});
|
||||
} catch (err) {
|
||||
const sanitizedErrorMessage = sanitizeString({
|
||||
unsanitizedString: (err as Error).message,
|
||||
tokens: [username, password, providerInputs.password, providerInputs.username]
|
||||
});
|
||||
throw new BadRequestError({ message: `Failed to create lease from provider: ${sanitizedErrorMessage}` });
|
||||
} finally {
|
||||
client.destroy(noop);
|
||||
}
|
||||
@@ -143,13 +155,19 @@ export const SnowflakeProvider = (): TDynamicProviderFns => {
|
||||
sqlText: revokeStatement,
|
||||
complete(err) {
|
||||
if (err) {
|
||||
return reject(new BadRequestError({ name: "RevokeLease", message: err.message }));
|
||||
return reject(err);
|
||||
}
|
||||
|
||||
return resolve(true);
|
||||
}
|
||||
});
|
||||
});
|
||||
} catch (err) {
|
||||
const sanitizedErrorMessage = sanitizeString({
|
||||
unsanitizedString: (err as Error).message,
|
||||
tokens: [username, providerInputs.password, providerInputs.username]
|
||||
});
|
||||
throw new BadRequestError({ message: `Failed to revoke lease from provider: ${sanitizedErrorMessage}` });
|
||||
} finally {
|
||||
client.destroy(noop);
|
||||
}
|
||||
@@ -175,13 +193,19 @@ export const SnowflakeProvider = (): TDynamicProviderFns => {
|
||||
sqlText: renewStatement,
|
||||
complete(err) {
|
||||
if (err) {
|
||||
return reject(new BadRequestError({ name: "RenewLease", message: err.message }));
|
||||
return reject(err);
|
||||
}
|
||||
|
||||
return resolve(true);
|
||||
}
|
||||
});
|
||||
});
|
||||
} catch (err) {
|
||||
const sanitizedErrorMessage = sanitizeString({
|
||||
unsanitizedString: (err as Error).message,
|
||||
tokens: [entityId, providerInputs.password, providerInputs.username]
|
||||
});
|
||||
throw new BadRequestError({ message: `Failed to renew lease from provider: ${sanitizedErrorMessage}` });
|
||||
} finally {
|
||||
client.destroy(noop);
|
||||
}
|
||||
|
@@ -3,6 +3,8 @@ import knex from "knex";
|
||||
import { z } from "zod";
|
||||
|
||||
import { crypto } from "@app/lib/crypto/cryptography";
|
||||
import { BadRequestError } from "@app/lib/errors";
|
||||
import { sanitizeString } from "@app/lib/fn";
|
||||
import { GatewayProxyProtocol, withGatewayProxy } from "@app/lib/gateway";
|
||||
import { alphaNumericNanoId } from "@app/lib/nanoid";
|
||||
import { validateHandlebarTemplate } from "@app/lib/template/validate-handlebars";
|
||||
@@ -212,8 +214,19 @@ export const SqlDatabaseProvider = ({ gatewayService }: TSqlDatabaseProviderDTO)
|
||||
// oracle needs from keyword
|
||||
const testStatement = providerInputs.client === SqlProviders.Oracle ? "SELECT 1 FROM DUAL" : "SELECT 1";
|
||||
|
||||
isConnected = await db.raw(testStatement).then(() => true);
|
||||
await db.destroy();
|
||||
try {
|
||||
isConnected = await db.raw(testStatement).then(() => true);
|
||||
} catch (err) {
|
||||
const sanitizedErrorMessage = sanitizeString({
|
||||
unsanitizedString: (err as Error)?.message,
|
||||
tokens: [providerInputs.username]
|
||||
});
|
||||
throw new BadRequestError({
|
||||
message: `Failed to connect with provider: ${sanitizedErrorMessage}`
|
||||
});
|
||||
} finally {
|
||||
await db.destroy();
|
||||
}
|
||||
};
|
||||
|
||||
if (providerInputs.gatewayId) {
|
||||
@@ -233,13 +246,13 @@ export const SqlDatabaseProvider = ({ gatewayService }: TSqlDatabaseProviderDTO)
|
||||
const { inputs, expireAt, usernameTemplate, identity } = data;
|
||||
|
||||
const providerInputs = await validateProviderInputs(inputs);
|
||||
const { database } = providerInputs;
|
||||
const username = generateUsername(providerInputs.client, usernameTemplate, identity);
|
||||
|
||||
const password = generatePassword(providerInputs.client, providerInputs.passwordRequirements);
|
||||
const gatewayCallback = async (host = providerInputs.host, port = providerInputs.port) => {
|
||||
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 })({
|
||||
@@ -256,6 +269,14 @@ export const SqlDatabaseProvider = ({ gatewayService }: TSqlDatabaseProviderDTO)
|
||||
await tx.raw(query);
|
||||
}
|
||||
});
|
||||
} catch (err) {
|
||||
const sanitizedErrorMessage = sanitizeString({
|
||||
unsanitizedString: (err as Error)?.message,
|
||||
tokens: [username, password, database]
|
||||
});
|
||||
throw new BadRequestError({
|
||||
message: `Failed to create lease from provider: ${sanitizedErrorMessage}`
|
||||
});
|
||||
} finally {
|
||||
await db.destroy();
|
||||
}
|
||||
@@ -283,6 +304,14 @@ export const SqlDatabaseProvider = ({ gatewayService }: TSqlDatabaseProviderDTO)
|
||||
await tx.raw(query);
|
||||
}
|
||||
});
|
||||
} catch (err) {
|
||||
const sanitizedErrorMessage = sanitizeString({
|
||||
unsanitizedString: (err as Error)?.message,
|
||||
tokens: [username, database]
|
||||
});
|
||||
throw new BadRequestError({
|
||||
message: `Failed to revoke lease from provider: ${sanitizedErrorMessage}`
|
||||
});
|
||||
} finally {
|
||||
await db.destroy();
|
||||
}
|
||||
@@ -319,6 +348,14 @@ export const SqlDatabaseProvider = ({ gatewayService }: TSqlDatabaseProviderDTO)
|
||||
}
|
||||
});
|
||||
}
|
||||
} catch (err) {
|
||||
const sanitizedErrorMessage = sanitizeString({
|
||||
unsanitizedString: (err as Error)?.message,
|
||||
tokens: [database]
|
||||
});
|
||||
throw new BadRequestError({
|
||||
message: `Failed to renew lease from provider: ${sanitizedErrorMessage}`
|
||||
});
|
||||
} finally {
|
||||
await db.destroy();
|
||||
}
|
||||
|
@@ -1,6 +1,8 @@
|
||||
import { authenticator } from "otplib";
|
||||
import { HashAlgorithms } from "otplib/core";
|
||||
|
||||
import { BadRequestError } from "@app/lib/errors";
|
||||
import { sanitizeString } from "@app/lib/fn";
|
||||
import { alphaNumericNanoId } from "@app/lib/nanoid";
|
||||
|
||||
import { DynamicSecretTotpSchema, TDynamicProviderFns, TotpConfigType } from "./models";
|
||||
@@ -12,62 +14,84 @@ export const TotpProvider = (): TDynamicProviderFns => {
|
||||
return providerInputs;
|
||||
};
|
||||
|
||||
const validateConnection = async () => {
|
||||
return true;
|
||||
const validateConnection = async (inputs: unknown) => {
|
||||
try {
|
||||
await validateProviderInputs(inputs);
|
||||
return true;
|
||||
} catch (err) {
|
||||
const sanitizedErrorMessage = sanitizeString({
|
||||
unsanitizedString: (err as Error)?.message,
|
||||
tokens: []
|
||||
});
|
||||
throw new BadRequestError({
|
||||
message: `Failed to connect with provider: ${sanitizedErrorMessage}`
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const create = async (inputs: unknown) => {
|
||||
const providerInputs = await validateProviderInputs(inputs);
|
||||
const create = async (data: { inputs: unknown }) => {
|
||||
const { inputs } = data;
|
||||
try {
|
||||
const providerInputs = await validateProviderInputs(inputs);
|
||||
|
||||
const entityId = alphaNumericNanoId(32);
|
||||
const authenticatorInstance = authenticator.clone();
|
||||
const entityId = alphaNumericNanoId(32);
|
||||
const authenticatorInstance = authenticator.clone();
|
||||
|
||||
let secret: string;
|
||||
let period: number | null | undefined;
|
||||
let digits: number | null | undefined;
|
||||
let algorithm: HashAlgorithms | null | undefined;
|
||||
let secret: string;
|
||||
let period: number | null | undefined;
|
||||
let digits: number | null | undefined;
|
||||
let algorithm: HashAlgorithms | null | undefined;
|
||||
|
||||
if (providerInputs.configType === TotpConfigType.URL) {
|
||||
const urlObj = new URL(providerInputs.url);
|
||||
secret = urlObj.searchParams.get("secret") as string;
|
||||
const periodFromUrl = urlObj.searchParams.get("period");
|
||||
const digitsFromUrl = urlObj.searchParams.get("digits");
|
||||
const algorithmFromUrl = urlObj.searchParams.get("algorithm");
|
||||
if (providerInputs.configType === TotpConfigType.URL) {
|
||||
const urlObj = new URL(providerInputs.url);
|
||||
secret = urlObj.searchParams.get("secret") as string;
|
||||
const periodFromUrl = urlObj.searchParams.get("period");
|
||||
const digitsFromUrl = urlObj.searchParams.get("digits");
|
||||
const algorithmFromUrl = urlObj.searchParams.get("algorithm");
|
||||
|
||||
if (periodFromUrl) {
|
||||
period = +periodFromUrl;
|
||||
if (periodFromUrl) {
|
||||
period = +periodFromUrl;
|
||||
}
|
||||
|
||||
if (digitsFromUrl) {
|
||||
digits = +digitsFromUrl;
|
||||
}
|
||||
|
||||
if (algorithmFromUrl) {
|
||||
algorithm = algorithmFromUrl.toLowerCase() as HashAlgorithms;
|
||||
}
|
||||
} else {
|
||||
secret = providerInputs.secret;
|
||||
period = providerInputs.period;
|
||||
digits = providerInputs.digits;
|
||||
algorithm = providerInputs.algorithm as unknown as HashAlgorithms;
|
||||
}
|
||||
|
||||
if (digitsFromUrl) {
|
||||
digits = +digitsFromUrl;
|
||||
if (digits) {
|
||||
authenticatorInstance.options = { digits };
|
||||
}
|
||||
|
||||
if (algorithmFromUrl) {
|
||||
algorithm = algorithmFromUrl.toLowerCase() as HashAlgorithms;
|
||||
if (algorithm) {
|
||||
authenticatorInstance.options = { algorithm };
|
||||
}
|
||||
} else {
|
||||
secret = providerInputs.secret;
|
||||
period = providerInputs.period;
|
||||
digits = providerInputs.digits;
|
||||
algorithm = providerInputs.algorithm as unknown as HashAlgorithms;
|
||||
}
|
||||
|
||||
if (digits) {
|
||||
authenticatorInstance.options = { digits };
|
||||
}
|
||||
if (period) {
|
||||
authenticatorInstance.options = { step: period };
|
||||
}
|
||||
|
||||
if (algorithm) {
|
||||
authenticatorInstance.options = { algorithm };
|
||||
return {
|
||||
entityId,
|
||||
data: { TOTP: authenticatorInstance.generate(secret), TIME_REMAINING: authenticatorInstance.timeRemaining() }
|
||||
};
|
||||
} catch (err) {
|
||||
const sanitizedErrorMessage = sanitizeString({
|
||||
unsanitizedString: (err as Error)?.message,
|
||||
tokens: []
|
||||
});
|
||||
throw new BadRequestError({
|
||||
message: `Failed to create lease from provider: ${sanitizedErrorMessage}`
|
||||
});
|
||||
}
|
||||
|
||||
if (period) {
|
||||
authenticatorInstance.options = { step: period };
|
||||
}
|
||||
|
||||
return {
|
||||
entityId,
|
||||
data: { TOTP: authenticatorInstance.generate(secret), TIME_REMAINING: authenticatorInstance.timeRemaining() }
|
||||
};
|
||||
};
|
||||
|
||||
const revoke = async (_inputs: unknown, entityId: string) => {
|
||||
|
@@ -4,6 +4,7 @@ import { z } from "zod";
|
||||
|
||||
import { crypto } from "@app/lib/crypto/cryptography";
|
||||
import { BadRequestError } from "@app/lib/errors";
|
||||
import { sanitizeString } from "@app/lib/fn";
|
||||
import { GatewayProxyProtocol, withGatewayProxy } from "@app/lib/gateway";
|
||||
import { logger } from "@app/lib/logger";
|
||||
import { alphaNumericNanoId } from "@app/lib/nanoid";
|
||||
@@ -275,6 +276,14 @@ export const VerticaProvider = ({ gatewayService }: TVerticaProviderDTO): TDynam
|
||||
await client.raw(trimmedQuery);
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
const sanitizedErrorMessage = sanitizeString({
|
||||
unsanitizedString: (err as Error)?.message,
|
||||
tokens: [username, password, providerInputs.username, providerInputs.password]
|
||||
});
|
||||
throw new BadRequestError({
|
||||
message: `Failed to create lease from provider: ${sanitizedErrorMessage}`
|
||||
});
|
||||
} finally {
|
||||
if (client) await client.destroy();
|
||||
}
|
||||
@@ -339,6 +348,14 @@ export const VerticaProvider = ({ gatewayService }: TVerticaProviderDTO): TDynam
|
||||
await client.raw(trimmedQuery);
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
const sanitizedErrorMessage = sanitizeString({
|
||||
unsanitizedString: (err as Error)?.message,
|
||||
tokens: [username, providerInputs.username, providerInputs.password]
|
||||
});
|
||||
throw new BadRequestError({
|
||||
message: `Failed to revoke lease from provider: ${sanitizedErrorMessage}`
|
||||
});
|
||||
} finally {
|
||||
if (client) await client.destroy();
|
||||
}
|
||||
|
@@ -3,7 +3,7 @@ import { z } from "zod";
|
||||
|
||||
import { logger } from "@app/lib/logger";
|
||||
|
||||
import { EventSchema, TopicName } from "./types";
|
||||
import { BusEventSchema, TopicName } from "./types";
|
||||
|
||||
export const eventBusFactory = (redis: Redis) => {
|
||||
const publisher = redis.duplicate();
|
||||
@@ -28,7 +28,7 @@ export const eventBusFactory = (redis: Redis) => {
|
||||
* @param topic - The topic to publish the event to.
|
||||
* @param event - The event data to publish.
|
||||
*/
|
||||
const publish = async <T extends z.input<typeof EventSchema>>(topic: TopicName, event: T) => {
|
||||
const publish = async <T extends z.input<typeof BusEventSchema>>(topic: TopicName, event: T) => {
|
||||
const json = JSON.stringify(event);
|
||||
|
||||
return publisher.publish(topic, json, (err) => {
|
||||
@@ -44,7 +44,7 @@ export const eventBusFactory = (redis: Redis) => {
|
||||
* @template T - The type of the event data, which should match the schema defined in EventSchema.
|
||||
* @returns A function that can be called to unsubscribe from the event bus.
|
||||
*/
|
||||
const subscribe = <T extends z.infer<typeof EventSchema>>(fn: (data: T) => Promise<void> | void) => {
|
||||
const subscribe = <T extends z.infer<typeof BusEventSchema>>(fn: (data: T) => Promise<void> | void) => {
|
||||
// Not using async await cause redis client's `on` method does not expect async listeners.
|
||||
const listener = (channel: string, message: string) => {
|
||||
try {
|
||||
|
@@ -7,17 +7,15 @@ import { logger } from "@app/lib/logger";
|
||||
|
||||
import { TEventBusService } from "./event-bus-service";
|
||||
import { createEventStreamClient, EventStreamClient, IEventStreamClientOpts } from "./event-sse-stream";
|
||||
import { EventData, RegisteredEvent, toBusEventName } from "./types";
|
||||
import { BusEvent, RegisteredEvent } from "./types";
|
||||
|
||||
const AUTH_REFRESH_INTERVAL = 60 * 1000;
|
||||
const HEART_BEAT_INTERVAL = 15 * 1000;
|
||||
|
||||
export const sseServiceFactory = (bus: TEventBusService, redis: Redis) => {
|
||||
let heartbeatInterval: NodeJS.Timeout | null = null;
|
||||
|
||||
const clients = new Set<EventStreamClient>();
|
||||
|
||||
heartbeatInterval = setInterval(() => {
|
||||
const heartbeatInterval = setInterval(() => {
|
||||
for (const client of clients) {
|
||||
if (client.stream.closed) continue;
|
||||
void client.ping();
|
||||
@@ -71,8 +69,8 @@ export const sseServiceFactory = (bus: TEventBusService, redis: Redis) => {
|
||||
}
|
||||
};
|
||||
|
||||
function filterEventsForClient(client: EventStreamClient, event: EventData, registered: RegisteredEvent[]) {
|
||||
const eventType = toBusEventName(event.data.eventType);
|
||||
function filterEventsForClient(client: EventStreamClient, event: BusEvent, registered: RegisteredEvent[]) {
|
||||
const eventType = event.data.event;
|
||||
const match = registered.find((r) => r.event === eventType);
|
||||
if (!match) return;
|
||||
|
||||
|
@@ -12,7 +12,7 @@ import { KeyStorePrefixes } from "@app/keystore/keystore";
|
||||
import { conditionsMatcher } from "@app/lib/casl";
|
||||
import { logger } from "@app/lib/logger";
|
||||
|
||||
import { EventData, RegisteredEvent } from "./types";
|
||||
import { BusEvent, RegisteredEvent } from "./types";
|
||||
|
||||
export const getServerSentEventsHeaders = () =>
|
||||
({
|
||||
@@ -55,7 +55,7 @@ export type EventStreamClient = {
|
||||
id: string;
|
||||
stream: Readable;
|
||||
open: () => Promise<void>;
|
||||
send: (data: EventMessage | EventData) => void;
|
||||
send: (data: EventMessage | BusEvent) => void;
|
||||
ping: () => Promise<void>;
|
||||
refresh: () => Promise<void>;
|
||||
close: () => void;
|
||||
@@ -66,15 +66,21 @@ export type EventStreamClient = {
|
||||
};
|
||||
|
||||
export function createEventStreamClient(redis: Redis, options: IEventStreamClientOpts): EventStreamClient {
|
||||
const rules = options.registered.map((r) => ({
|
||||
subject: options.type,
|
||||
action: "subscribe",
|
||||
conditions: {
|
||||
eventType: r.event,
|
||||
secretPath: r.conditions?.secretPath ?? "/",
|
||||
environment: r.conditions?.environmentSlug
|
||||
}
|
||||
}));
|
||||
const rules = options.registered.map((r) => {
|
||||
const secretPath = r.conditions?.secretPath;
|
||||
const hasConditions = r.conditions?.environmentSlug || r.conditions?.secretPath;
|
||||
|
||||
return {
|
||||
subject: options.type,
|
||||
action: "subscribe",
|
||||
conditions: hasConditions
|
||||
? {
|
||||
environment: r.conditions?.environmentSlug ?? "",
|
||||
secretPath: { $glob: secretPath }
|
||||
}
|
||||
: undefined
|
||||
};
|
||||
});
|
||||
|
||||
const id = `sse-${nanoid()}`;
|
||||
const control = new AbortController();
|
||||
@@ -89,7 +95,7 @@ export function createEventStreamClient(redis: Redis, options: IEventStreamClien
|
||||
// We will manually push data to the stream
|
||||
stream._read = () => {};
|
||||
|
||||
const send = (data: EventMessage | EventData) => {
|
||||
const send = (data: EventMessage | BusEvent) => {
|
||||
const chunk = serializeSseEvent(data);
|
||||
if (!stream.push(chunk)) {
|
||||
logger.debug("Backpressure detected: dropped manual event");
|
||||
@@ -117,7 +123,7 @@ export function createEventStreamClient(redis: Redis, options: IEventStreamClien
|
||||
|
||||
await redis.set(key, "1", "EX", 60);
|
||||
|
||||
stream.push("1");
|
||||
send({ type: "ping" });
|
||||
};
|
||||
|
||||
const close = () => {
|
||||
|
@@ -1,7 +1,8 @@
|
||||
import { z } from "zod";
|
||||
|
||||
import { ProjectType } from "@app/db/schemas";
|
||||
import { Event, EventType } from "@app/ee/services/audit-log/audit-log-types";
|
||||
|
||||
import { ProjectPermissionSecretEventActions } from "../permission/project-permission";
|
||||
|
||||
export enum TopicName {
|
||||
CoreServers = "infisical::core-servers"
|
||||
@@ -10,84 +11,44 @@ export enum TopicName {
|
||||
export enum BusEventName {
|
||||
CreateSecret = "secret:create",
|
||||
UpdateSecret = "secret:update",
|
||||
DeleteSecret = "secret:delete"
|
||||
DeleteSecret = "secret:delete",
|
||||
ImportMutation = "secret:import-mutation"
|
||||
}
|
||||
|
||||
type PublisableEventTypes =
|
||||
| EventType.CREATE_SECRET
|
||||
| EventType.CREATE_SECRETS
|
||||
| EventType.DELETE_SECRET
|
||||
| EventType.DELETE_SECRETS
|
||||
| EventType.UPDATE_SECRETS
|
||||
| EventType.UPDATE_SECRET;
|
||||
|
||||
export function toBusEventName(input: EventType) {
|
||||
switch (input) {
|
||||
case EventType.CREATE_SECRET:
|
||||
case EventType.CREATE_SECRETS:
|
||||
return BusEventName.CreateSecret;
|
||||
case EventType.UPDATE_SECRET:
|
||||
case EventType.UPDATE_SECRETS:
|
||||
return BusEventName.UpdateSecret;
|
||||
case EventType.DELETE_SECRET:
|
||||
case EventType.DELETE_SECRETS:
|
||||
return BusEventName.DeleteSecret;
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
const isBulkEvent = (event: Event): event is Extract<Event, { metadata: { secrets: Array<unknown> } }> => {
|
||||
return event.type.endsWith("-secrets"); // Feels so wrong
|
||||
};
|
||||
|
||||
export const toPublishableEvent = (event: Event) => {
|
||||
const name = toBusEventName(event.type);
|
||||
|
||||
if (!name) return null;
|
||||
|
||||
const e = event as Extract<Event, { type: PublisableEventTypes }>;
|
||||
|
||||
if (isBulkEvent(e)) {
|
||||
return {
|
||||
name,
|
||||
isBulk: true,
|
||||
data: {
|
||||
eventType: e.type,
|
||||
payload: e.metadata.secrets.map((s) => ({
|
||||
environment: e.metadata.environment,
|
||||
secretPath: e.metadata.secretPath,
|
||||
...s
|
||||
}))
|
||||
}
|
||||
} as const;
|
||||
}
|
||||
|
||||
return {
|
||||
name,
|
||||
isBulk: false,
|
||||
data: {
|
||||
eventType: e.type,
|
||||
payload: {
|
||||
...e.metadata,
|
||||
environment: e.metadata.environment
|
||||
}
|
||||
export const Mappings = {
|
||||
BusEventToAction(input: BusEventName) {
|
||||
switch (input) {
|
||||
case BusEventName.CreateSecret:
|
||||
return ProjectPermissionSecretEventActions.SubscribeCreated;
|
||||
case BusEventName.DeleteSecret:
|
||||
return ProjectPermissionSecretEventActions.SubscribeDeleted;
|
||||
case BusEventName.ImportMutation:
|
||||
return ProjectPermissionSecretEventActions.SubscribeImportMutations;
|
||||
case BusEventName.UpdateSecret:
|
||||
return ProjectPermissionSecretEventActions.SubscribeUpdated;
|
||||
default:
|
||||
throw new Error("Unknown bus event name");
|
||||
}
|
||||
} as const;
|
||||
}
|
||||
};
|
||||
|
||||
export const EventName = z.nativeEnum(BusEventName);
|
||||
|
||||
const EventSecretPayload = z.object({
|
||||
secretPath: z.string().optional(),
|
||||
secretId: z.string(),
|
||||
secretPath: z.string().optional(),
|
||||
secretKey: z.string(),
|
||||
environment: z.string()
|
||||
});
|
||||
|
||||
const EventImportMutationPayload = z.object({
|
||||
secretPath: z.string(),
|
||||
environment: z.string()
|
||||
});
|
||||
|
||||
export type EventSecret = z.infer<typeof EventSecretPayload>;
|
||||
|
||||
export const EventSchema = z.object({
|
||||
export const BusEventSchema = z.object({
|
||||
datacontenttype: z.literal("application/json").optional().default("application/json"),
|
||||
type: z.nativeEnum(ProjectType),
|
||||
source: z.string(),
|
||||
@@ -95,25 +56,38 @@ export const EventSchema = z.object({
|
||||
.string()
|
||||
.optional()
|
||||
.default(() => new Date().toISOString()),
|
||||
data: z.discriminatedUnion("eventType", [
|
||||
data: z.discriminatedUnion("event", [
|
||||
z.object({
|
||||
specversion: z.number().optional().default(1),
|
||||
eventType: z.enum([EventType.CREATE_SECRET, EventType.UPDATE_SECRET, EventType.DELETE_SECRET]),
|
||||
payload: EventSecretPayload
|
||||
event: z.enum([BusEventName.CreateSecret, BusEventName.DeleteSecret, BusEventName.UpdateSecret]),
|
||||
payload: z.union([EventSecretPayload, EventSecretPayload.array()])
|
||||
}),
|
||||
z.object({
|
||||
specversion: z.number().optional().default(1),
|
||||
eventType: z.enum([EventType.CREATE_SECRETS, EventType.UPDATE_SECRETS, EventType.DELETE_SECRETS]),
|
||||
payload: EventSecretPayload.array()
|
||||
event: z.enum([BusEventName.ImportMutation]),
|
||||
payload: z.union([EventImportMutationPayload, EventImportMutationPayload.array()])
|
||||
})
|
||||
// Add more event types as needed
|
||||
])
|
||||
});
|
||||
|
||||
export type EventData = z.infer<typeof EventSchema>;
|
||||
export type BusEvent = z.infer<typeof BusEventSchema>;
|
||||
|
||||
type PublishableEventPayload = z.input<typeof BusEventSchema>["data"];
|
||||
type PublishableSecretEvent = Extract<
|
||||
PublishableEventPayload,
|
||||
{ event: Exclude<BusEventName, BusEventName.ImportMutation> }
|
||||
>["payload"];
|
||||
|
||||
export type PublishableEvent = {
|
||||
created?: PublishableSecretEvent;
|
||||
updated?: PublishableSecretEvent;
|
||||
deleted?: PublishableSecretEvent;
|
||||
importMutation?: Extract<PublishableEventPayload, { event: BusEventName.ImportMutation }>["payload"];
|
||||
};
|
||||
|
||||
export const EventRegisterSchema = z.object({
|
||||
event: EventName,
|
||||
event: z.nativeEnum(BusEventName),
|
||||
conditions: z
|
||||
.object({
|
||||
secretPath: z.string().optional().default("/"),
|
||||
|
@@ -1,14 +1,19 @@
|
||||
/* eslint-disable @typescript-eslint/return-await */
|
||||
/* eslint-disable no-await-in-loop */
|
||||
import { ForbiddenError } from "@casl/ability";
|
||||
import { Octokit } from "@octokit/core";
|
||||
import { paginateGraphql } from "@octokit/plugin-paginate-graphql";
|
||||
import { Octokit as OctokitRest } from "@octokit/rest";
|
||||
import RE2 from "re2";
|
||||
|
||||
import { OrgMembershipRole } from "@app/db/schemas";
|
||||
import { BadRequestError, NotFoundError } from "@app/lib/errors";
|
||||
import { groupBy } from "@app/lib/fn";
|
||||
import { logger } from "@app/lib/logger";
|
||||
import { retryWithBackoff } from "@app/lib/retry";
|
||||
import { TKmsServiceFactory } from "@app/services/kms/kms-service";
|
||||
import { KmsDataKey } from "@app/services/kms/kms-types";
|
||||
import { TOrgMembershipDALFactory } from "@app/services/org-membership/org-membership-dal";
|
||||
|
||||
import { TGroupDALFactory } from "../group/group-dal";
|
||||
import { TUserGroupMembershipDALFactory } from "../group/user-group-membership-dal";
|
||||
@@ -16,20 +21,67 @@ import { TLicenseServiceFactory } from "../license/license-service";
|
||||
import { OrgPermissionActions, OrgPermissionSubjects } from "../permission/org-permission";
|
||||
import { TPermissionServiceFactory } from "../permission/permission-service-types";
|
||||
import { TGithubOrgSyncDALFactory } from "./github-org-sync-dal";
|
||||
import { TCreateGithubOrgSyncDTO, TDeleteGithubOrgSyncDTO, TUpdateGithubOrgSyncDTO } from "./github-org-sync-types";
|
||||
import {
|
||||
TCreateGithubOrgSyncDTO,
|
||||
TDeleteGithubOrgSyncDTO,
|
||||
TSyncAllTeamsDTO,
|
||||
TSyncResult,
|
||||
TUpdateGithubOrgSyncDTO,
|
||||
TValidateGithubTokenDTO
|
||||
} from "./github-org-sync-types";
|
||||
|
||||
const OctokitWithPlugin = Octokit.plugin(paginateGraphql);
|
||||
|
||||
// Type definitions for GitHub API errors
|
||||
interface GitHubApiError extends Error {
|
||||
status?: number;
|
||||
response?: {
|
||||
status?: number;
|
||||
headers?: {
|
||||
"x-ratelimit-reset"?: string;
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
interface OrgMembershipWithUser {
|
||||
id: string;
|
||||
orgId: string;
|
||||
role: string;
|
||||
status: string;
|
||||
isActive: boolean;
|
||||
inviteEmail: string | null;
|
||||
user: {
|
||||
id: string;
|
||||
email: string;
|
||||
username: string | null;
|
||||
firstName: string | null;
|
||||
lastName: string | null;
|
||||
} | null;
|
||||
}
|
||||
|
||||
interface GroupMembership {
|
||||
id: string;
|
||||
groupId: string;
|
||||
groupName: string;
|
||||
orgMembershipId: string;
|
||||
firstName: string | null;
|
||||
lastName: string | null;
|
||||
}
|
||||
|
||||
type TGithubOrgSyncServiceFactoryDep = {
|
||||
githubOrgSyncDAL: TGithubOrgSyncDALFactory;
|
||||
permissionService: Pick<TPermissionServiceFactory, "getOrgPermission">;
|
||||
kmsService: Pick<TKmsServiceFactory, "createCipherPairWithDataKey">;
|
||||
userGroupMembershipDAL: Pick<
|
||||
TUserGroupMembershipDALFactory,
|
||||
"findGroupMembershipsByUserIdInOrg" | "insertMany" | "delete"
|
||||
"findGroupMembershipsByUserIdInOrg" | "findGroupMembershipsByGroupIdInOrg" | "insertMany" | "delete"
|
||||
>;
|
||||
groupDAL: Pick<TGroupDALFactory, "insertMany" | "transaction" | "find">;
|
||||
licenseService: Pick<TLicenseServiceFactory, "getPlan">;
|
||||
orgMembershipDAL: Pick<
|
||||
TOrgMembershipDALFactory,
|
||||
"find" | "findOrgMembershipById" | "findOrgMembershipsWithUsersByOrgId"
|
||||
>;
|
||||
};
|
||||
|
||||
export type TGithubOrgSyncServiceFactory = ReturnType<typeof githubOrgSyncServiceFactory>;
|
||||
@@ -40,7 +92,8 @@ export const githubOrgSyncServiceFactory = ({
|
||||
kmsService,
|
||||
userGroupMembershipDAL,
|
||||
groupDAL,
|
||||
licenseService
|
||||
licenseService,
|
||||
orgMembershipDAL
|
||||
}: TGithubOrgSyncServiceFactoryDep) => {
|
||||
const createGithubOrgSync = async ({
|
||||
githubOrgName,
|
||||
@@ -304,8 +357,8 @@ export const githubOrgSyncServiceFactory = ({
|
||||
const removeFromTeams = infisicalUserGroups.filter((el) => !githubUserTeamSet.has(el.groupName));
|
||||
|
||||
if (newTeams.length || updateTeams.length || removeFromTeams.length) {
|
||||
await groupDAL.transaction(async (tx) => {
|
||||
if (newTeams.length) {
|
||||
if (newTeams.length) {
|
||||
await groupDAL.transaction(async (tx) => {
|
||||
const newGroups = await groupDAL.insertMany(
|
||||
newTeams.map((newGroupName) => ({
|
||||
name: newGroupName,
|
||||
@@ -322,9 +375,11 @@ export const githubOrgSyncServiceFactory = ({
|
||||
})),
|
||||
tx
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if (updateTeams.length) {
|
||||
if (updateTeams.length) {
|
||||
await groupDAL.transaction(async (tx) => {
|
||||
await userGroupMembershipDAL.insertMany(
|
||||
updateTeams.map((el) => ({
|
||||
groupId: githubUserTeamOnInfisicalGroupByName[el][0].id,
|
||||
@@ -332,16 +387,433 @@ export const githubOrgSyncServiceFactory = ({
|
||||
})),
|
||||
tx
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if (removeFromTeams.length) {
|
||||
if (removeFromTeams.length) {
|
||||
await groupDAL.transaction(async (tx) => {
|
||||
await userGroupMembershipDAL.delete(
|
||||
{ userId, $in: { groupId: removeFromTeams.map((el) => el.groupId) } },
|
||||
tx
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const validateGithubToken = async ({ orgPermission, githubOrgAccessToken }: TValidateGithubTokenDTO) => {
|
||||
const { permission } = await permissionService.getOrgPermission(
|
||||
orgPermission.type,
|
||||
orgPermission.id,
|
||||
orgPermission.orgId,
|
||||
orgPermission.authMethod,
|
||||
orgPermission.orgId
|
||||
);
|
||||
|
||||
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Read, OrgPermissionSubjects.GithubOrgSync);
|
||||
|
||||
const plan = await licenseService.getPlan(orgPermission.orgId);
|
||||
if (!plan.githubOrgSync) {
|
||||
throw new BadRequestError({
|
||||
message:
|
||||
"Failed to validate GitHub token due to plan restriction. Upgrade plan to use GitHub organization sync."
|
||||
});
|
||||
}
|
||||
|
||||
const config = await githubOrgSyncDAL.findOne({ orgId: orgPermission.orgId });
|
||||
if (!config) {
|
||||
throw new BadRequestError({ message: "GitHub organization sync is not configured" });
|
||||
}
|
||||
|
||||
try {
|
||||
const testOctokit = new OctokitRest({
|
||||
auth: githubOrgAccessToken,
|
||||
request: {
|
||||
signal: AbortSignal.timeout(10000)
|
||||
}
|
||||
});
|
||||
|
||||
const { data: org } = await testOctokit.rest.orgs.get({
|
||||
org: config.githubOrgName
|
||||
});
|
||||
|
||||
const octokitGraphQL = new OctokitWithPlugin({
|
||||
auth: githubOrgAccessToken,
|
||||
request: {
|
||||
signal: AbortSignal.timeout(10000)
|
||||
}
|
||||
});
|
||||
|
||||
await octokitGraphQL.graphql(`query($org: String!) { organization(login: $org) { id name } }`, {
|
||||
org: config.githubOrgName
|
||||
});
|
||||
|
||||
return {
|
||||
valid: true,
|
||||
organizationInfo: {
|
||||
id: org.id,
|
||||
login: org.login,
|
||||
name: org.name || org.login,
|
||||
publicRepos: org.public_repos,
|
||||
privateRepos: org.owned_private_repos || 0
|
||||
}
|
||||
};
|
||||
} catch (error) {
|
||||
logger.error(error, `GitHub token validation failed for org ${config.githubOrgName}`);
|
||||
|
||||
const gitHubError = error as GitHubApiError;
|
||||
const statusCode = gitHubError.status || gitHubError.response?.status;
|
||||
if (statusCode) {
|
||||
if (statusCode === 401) {
|
||||
throw new BadRequestError({
|
||||
message: "GitHub access token is invalid or expired."
|
||||
});
|
||||
}
|
||||
if (statusCode === 403) {
|
||||
throw new BadRequestError({
|
||||
message:
|
||||
"GitHub access token lacks required permissions. Required: 1) 'read:org' scope for organization teams, 2) Token owner must be an organization member with team visibility access, 3) Organization settings must allow team visibility. Check GitHub token scopes and organization member permissions."
|
||||
});
|
||||
}
|
||||
if (statusCode === 404) {
|
||||
throw new BadRequestError({
|
||||
message: `Organization '${config.githubOrgName}' not found or access token does not have access to it.`
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
throw new BadRequestError({
|
||||
message: `GitHub token validation failed: ${(error as Error).message}`
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const syncAllTeams = async ({ orgPermission }: TSyncAllTeamsDTO): Promise<TSyncResult> => {
|
||||
const { permission } = await permissionService.getOrgPermission(
|
||||
orgPermission.type,
|
||||
orgPermission.id,
|
||||
orgPermission.orgId,
|
||||
orgPermission.authMethod,
|
||||
orgPermission.orgId
|
||||
);
|
||||
|
||||
ForbiddenError.from(permission).throwUnlessCan(
|
||||
OrgPermissionActions.Edit,
|
||||
OrgPermissionSubjects.GithubOrgSyncManual
|
||||
);
|
||||
|
||||
const plan = await licenseService.getPlan(orgPermission.orgId);
|
||||
if (!plan.githubOrgSync) {
|
||||
throw new BadRequestError({
|
||||
message:
|
||||
"Failed to sync all GitHub teams due to plan restriction. Upgrade plan to use GitHub organization sync."
|
||||
});
|
||||
}
|
||||
|
||||
const config = await githubOrgSyncDAL.findOne({ orgId: orgPermission.orgId });
|
||||
if (!config || !config?.isActive) {
|
||||
throw new BadRequestError({ message: "GitHub organization sync is not configured or not active" });
|
||||
}
|
||||
|
||||
const { decryptor } = await kmsService.createCipherPairWithDataKey({
|
||||
type: KmsDataKey.Organization,
|
||||
orgId: orgPermission.orgId
|
||||
});
|
||||
|
||||
if (!config.encryptedGithubOrgAccessToken) {
|
||||
throw new BadRequestError({
|
||||
message: "GitHub organization access token is required. Please set a token first."
|
||||
});
|
||||
}
|
||||
|
||||
const orgAccessToken = decryptor({ cipherTextBlob: config.encryptedGithubOrgAccessToken }).toString();
|
||||
|
||||
try {
|
||||
const testOctokit = new OctokitRest({
|
||||
auth: orgAccessToken,
|
||||
request: {
|
||||
signal: AbortSignal.timeout(10000)
|
||||
}
|
||||
});
|
||||
|
||||
await testOctokit.rest.orgs.get({
|
||||
org: config.githubOrgName
|
||||
});
|
||||
|
||||
await testOctokit.rest.users.getAuthenticated();
|
||||
} catch (error) {
|
||||
throw new BadRequestError({
|
||||
message: "Stored GitHub access token is invalid or expired. Please set a new token."
|
||||
});
|
||||
}
|
||||
|
||||
const allMembers = await orgMembershipDAL.findOrgMembershipsWithUsersByOrgId(orgPermission.orgId);
|
||||
const activeMembers = allMembers.filter(
|
||||
(member) => member.status === "accepted" && member.isActive
|
||||
) as OrgMembershipWithUser[];
|
||||
|
||||
const startTime = Date.now();
|
||||
const syncErrors: string[] = [];
|
||||
|
||||
const octokit = new OctokitWithPlugin({
|
||||
auth: orgAccessToken,
|
||||
request: {
|
||||
signal: AbortSignal.timeout(30000)
|
||||
}
|
||||
});
|
||||
|
||||
const data = await retryWithBackoff(async () => {
|
||||
return octokit.graphql
|
||||
.paginate<{
|
||||
organization: {
|
||||
teams: {
|
||||
totalCount: number;
|
||||
edges: {
|
||||
node: {
|
||||
name: string;
|
||||
description: string;
|
||||
members: {
|
||||
edges: {
|
||||
node: {
|
||||
login: string;
|
||||
};
|
||||
}[];
|
||||
};
|
||||
};
|
||||
}[];
|
||||
};
|
||||
};
|
||||
}>(
|
||||
`
|
||||
query orgTeams($cursor: String, $org: String!) {
|
||||
organization(login: $org) {
|
||||
teams(first: 100, after: $cursor) {
|
||||
totalCount
|
||||
edges {
|
||||
node {
|
||||
name
|
||||
description
|
||||
members(first: 100) {
|
||||
edges {
|
||||
node {
|
||||
login
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
pageInfo {
|
||||
hasNextPage
|
||||
endCursor
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
`,
|
||||
{
|
||||
org: config.githubOrgName
|
||||
}
|
||||
)
|
||||
.catch((err) => {
|
||||
logger.error(err, "GitHub GraphQL error for batched team sync");
|
||||
|
||||
const gitHubError = err as GitHubApiError;
|
||||
const statusCode = gitHubError.status || gitHubError.response?.status;
|
||||
if (statusCode) {
|
||||
if (statusCode === 401) {
|
||||
throw new BadRequestError({
|
||||
message: "GitHub access token is invalid or expired. Please provide a new token."
|
||||
});
|
||||
}
|
||||
if (statusCode === 403) {
|
||||
throw new BadRequestError({
|
||||
message:
|
||||
"GitHub access token lacks required permissions for organization team sync. Required: 1) 'admin:org' scope, 2) Token owner must be organization owner or have team read permissions, 3) Organization settings must allow team visibility. Check token scopes and user role."
|
||||
});
|
||||
}
|
||||
if (statusCode === 404) {
|
||||
throw new BadRequestError({
|
||||
message: `Organization ${config.githubOrgName} not found or access token does not have sufficient permissions to read it.`
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if ((err as Error)?.message?.includes("Although you appear to have the correct authorization credential")) {
|
||||
throw new BadRequestError({
|
||||
message:
|
||||
"Organization has restricted OAuth app access. Please check that: 1) Your organization has approved the Infisical OAuth application, 2) The token owner has sufficient organization permissions."
|
||||
});
|
||||
}
|
||||
throw new BadRequestError({ message: `GitHub GraphQL query failed: ${(err as Error)?.message}` });
|
||||
});
|
||||
});
|
||||
|
||||
const {
|
||||
organization: { teams }
|
||||
} = data;
|
||||
|
||||
const userTeamMap = new Map<string, string[]>();
|
||||
const allGithubUsernamesInTeams = new Set<string>();
|
||||
|
||||
teams?.edges?.forEach((teamEdge) => {
|
||||
const teamName = teamEdge.node.name.toLowerCase();
|
||||
|
||||
teamEdge.node.members.edges.forEach((memberEdge) => {
|
||||
const username = memberEdge.node.login.toLowerCase();
|
||||
allGithubUsernamesInTeams.add(username);
|
||||
|
||||
if (!userTeamMap.has(username)) {
|
||||
userTeamMap.set(username, []);
|
||||
}
|
||||
userTeamMap.get(username)!.push(teamName);
|
||||
});
|
||||
});
|
||||
|
||||
const allGithubTeamNames = Array.from(new Set(teams?.edges?.map((edge) => edge.node.name.toLowerCase()) || []));
|
||||
|
||||
const existingTeamsOnInfisical = await groupDAL.find({
|
||||
orgId: orgPermission.orgId,
|
||||
$in: { name: allGithubTeamNames }
|
||||
});
|
||||
const existingTeamsMap = groupBy(existingTeamsOnInfisical, (i) => i.name);
|
||||
|
||||
const teamsToCreate = allGithubTeamNames.filter((teamName) => !(teamName in existingTeamsMap));
|
||||
const createdTeams = new Set<string>();
|
||||
const updatedTeams = new Set<string>();
|
||||
const totalRemovedMemberships = 0;
|
||||
|
||||
await groupDAL.transaction(async (tx) => {
|
||||
if (teamsToCreate.length > 0) {
|
||||
const newGroups = await groupDAL.insertMany(
|
||||
teamsToCreate.map((teamName) => ({
|
||||
name: teamName,
|
||||
role: OrgMembershipRole.Member,
|
||||
slug: teamName,
|
||||
orgId: orgPermission.orgId
|
||||
})),
|
||||
tx
|
||||
);
|
||||
|
||||
newGroups.forEach((group) => {
|
||||
if (!existingTeamsMap[group.name]) {
|
||||
existingTeamsMap[group.name] = [];
|
||||
}
|
||||
existingTeamsMap[group.name].push(group);
|
||||
createdTeams.add(group.name);
|
||||
});
|
||||
}
|
||||
|
||||
const allTeams = [...Object.values(existingTeamsMap).flat()];
|
||||
|
||||
for (const team of allTeams) {
|
||||
const teamName = team.name.toLowerCase();
|
||||
|
||||
const currentMemberships = (await userGroupMembershipDAL.findGroupMembershipsByGroupIdInOrg(
|
||||
team.id,
|
||||
orgPermission.orgId
|
||||
)) as GroupMembership[];
|
||||
|
||||
const expectedUserIds = new Set<string>();
|
||||
teams?.edges?.forEach((teamEdge) => {
|
||||
if (teamEdge.node.name.toLowerCase() === teamName) {
|
||||
teamEdge.node.members.edges.forEach((memberEdge) => {
|
||||
const githubUsername = memberEdge.node.login.toLowerCase();
|
||||
|
||||
const matchingMember = activeMembers.find((member) => {
|
||||
const email = member.user?.email || member.inviteEmail;
|
||||
if (!email) return false;
|
||||
|
||||
const emailPrefix = email.split("@")[0].toLowerCase();
|
||||
const emailDomain = email.split("@")[1].toLowerCase();
|
||||
|
||||
if (emailPrefix === githubUsername) {
|
||||
return true;
|
||||
}
|
||||
const domainName = emailDomain.split(".")[0];
|
||||
if (githubUsername.endsWith(domainName) && githubUsername.length > domainName.length) {
|
||||
const baseUsername = githubUsername.slice(0, -domainName.length);
|
||||
if (emailPrefix === baseUsername) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
const emailSplitRegex = new RE2(/[._-]/);
|
||||
const emailParts = emailPrefix.split(emailSplitRegex);
|
||||
const longestEmailPart = emailParts.reduce((a, b) => (a.length > b.length ? a : b), "");
|
||||
if (longestEmailPart.length >= 4 && githubUsername.includes(longestEmailPart)) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
});
|
||||
|
||||
if (matchingMember?.user?.id) {
|
||||
expectedUserIds.add(matchingMember.user.id);
|
||||
logger.info(
|
||||
`Matched GitHub user ${githubUsername} to email ${matchingMember.user?.email || matchingMember.inviteEmail}`
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
const currentUserIds = new Set<string>();
|
||||
currentMemberships.forEach((membership) => {
|
||||
const activeMember = activeMembers.find((am) => am.id === membership.orgMembershipId);
|
||||
if (activeMember?.user?.id) {
|
||||
currentUserIds.add(activeMember.user.id);
|
||||
}
|
||||
});
|
||||
|
||||
const usersToAdd = Array.from(expectedUserIds).filter((userId) => !currentUserIds.has(userId));
|
||||
|
||||
const membershipsToRemove = currentMemberships.filter((membership) => {
|
||||
const activeMember = activeMembers.find((am) => am.id === membership.orgMembershipId);
|
||||
return activeMember?.user?.id && !expectedUserIds.has(activeMember.user.id);
|
||||
});
|
||||
|
||||
if (usersToAdd.length > 0) {
|
||||
await userGroupMembershipDAL.insertMany(
|
||||
usersToAdd.map((userId) => ({
|
||||
userId,
|
||||
groupId: team.id
|
||||
})),
|
||||
tx
|
||||
);
|
||||
updatedTeams.add(teamName);
|
||||
}
|
||||
|
||||
if (membershipsToRemove.length > 0) {
|
||||
await userGroupMembershipDAL.delete(
|
||||
{
|
||||
$in: {
|
||||
id: membershipsToRemove.map((m) => m.id)
|
||||
}
|
||||
},
|
||||
tx
|
||||
);
|
||||
updatedTeams.add(teamName);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
const syncDuration = Date.now() - startTime;
|
||||
|
||||
logger.info(
|
||||
{
|
||||
orgId: orgPermission.orgId,
|
||||
createdTeams: createdTeams.size,
|
||||
syncDuration
|
||||
},
|
||||
"GitHub team sync completed"
|
||||
);
|
||||
|
||||
return {
|
||||
totalUsers: activeMembers.length,
|
||||
errors: syncErrors,
|
||||
createdTeams: Array.from(createdTeams),
|
||||
updatedTeams: Array.from(updatedTeams),
|
||||
removedMemberships: totalRemovedMemberships,
|
||||
syncDuration
|
||||
};
|
||||
};
|
||||
|
||||
return {
|
||||
@@ -349,6 +821,8 @@ export const githubOrgSyncServiceFactory = ({
|
||||
updateGithubOrgSync,
|
||||
deleteGithubOrgSync,
|
||||
getGithubOrgSync,
|
||||
syncUserGroups
|
||||
syncUserGroups,
|
||||
syncAllTeams,
|
||||
validateGithubToken
|
||||
};
|
||||
};
|
||||
|
@@ -21,3 +21,21 @@ export interface TDeleteGithubOrgSyncDTO {
|
||||
export interface TGetGithubOrgSyncDTO {
|
||||
orgPermission: OrgServiceActor;
|
||||
}
|
||||
|
||||
export interface TSyncAllTeamsDTO {
|
||||
orgPermission: OrgServiceActor;
|
||||
}
|
||||
|
||||
export interface TSyncResult {
|
||||
totalUsers: number;
|
||||
errors: string[];
|
||||
createdTeams: string[];
|
||||
updatedTeams: string[];
|
||||
removedMemberships: number;
|
||||
syncDuration: number;
|
||||
}
|
||||
|
||||
export interface TValidateGithubTokenDTO {
|
||||
orgPermission: OrgServiceActor;
|
||||
githubOrgAccessToken: string;
|
||||
}
|
||||
|
@@ -1,6 +1,6 @@
|
||||
import { Knex } from "knex";
|
||||
|
||||
import { SecretKeyEncoding, TableName, TUsers } from "@app/db/schemas";
|
||||
import { ProjectVersion, SecretKeyEncoding, TableName, TUsers } from "@app/db/schemas";
|
||||
import { crypto } from "@app/lib/crypto/cryptography";
|
||||
import { BadRequestError, ForbiddenRequestError, NotFoundError, ScimRequestError } from "@app/lib/errors";
|
||||
|
||||
@@ -65,6 +65,18 @@ const addAcceptedUsersToGroup = async ({
|
||||
const userKeysSet = new Set(keys.map((k) => `${k.projectId}-${k.receiverId}`));
|
||||
|
||||
for await (const projectId of projectIds) {
|
||||
const project = await projectDAL.findById(projectId, tx);
|
||||
if (!project) {
|
||||
throw new NotFoundError({
|
||||
message: `Failed to find project with ID '${projectId}'`
|
||||
});
|
||||
}
|
||||
|
||||
if (project.version !== ProjectVersion.V1 && project.version !== ProjectVersion.V2) {
|
||||
// eslint-disable-next-line no-continue
|
||||
continue;
|
||||
}
|
||||
|
||||
const usersToAddProjectKeyFor = users.filter((u) => !userKeysSet.has(`${projectId}-${u.userId}`));
|
||||
|
||||
if (usersToAddProjectKeyFor.length) {
|
||||
@@ -86,6 +98,12 @@ const addAcceptedUsersToGroup = async ({
|
||||
});
|
||||
}
|
||||
|
||||
if (!ghostUserLatestKey.sender.publicKey) {
|
||||
throw new NotFoundError({
|
||||
message: `Failed to find project owner's public key in project with ID '${projectId}'`
|
||||
});
|
||||
}
|
||||
|
||||
const bot = await projectBotDAL.findOne({ projectId }, tx);
|
||||
|
||||
if (!bot) {
|
||||
@@ -112,6 +130,12 @@ const addAcceptedUsersToGroup = async ({
|
||||
});
|
||||
|
||||
const projectKeysToAdd = usersToAddProjectKeyFor.map((user) => {
|
||||
if (!user.publicKey) {
|
||||
throw new NotFoundError({
|
||||
message: `Failed to find user's public key in project with ID '${projectId}'`
|
||||
});
|
||||
}
|
||||
|
||||
const { ciphertext: encryptedKey, nonce } = crypto
|
||||
.encryption()
|
||||
.asymmetric()
|
||||
|
@@ -41,7 +41,7 @@ type TGroupServiceFactoryDep = {
|
||||
TUserGroupMembershipDALFactory,
|
||||
"findOne" | "delete" | "filterProjectsByUserMembership" | "transaction" | "insertMany" | "find"
|
||||
>;
|
||||
projectDAL: Pick<TProjectDALFactory, "findProjectGhostUser">;
|
||||
projectDAL: Pick<TProjectDALFactory, "findProjectGhostUser" | "findById">;
|
||||
projectBotDAL: Pick<TProjectBotDALFactory, "findOne">;
|
||||
projectKeyDAL: Pick<TProjectKeyDALFactory, "find" | "delete" | "findLatestProjectKey" | "insertMany">;
|
||||
permissionService: Pick<TPermissionServiceFactory, "getOrgPermission" | "getOrgPermissionByRole">;
|
||||
|
@@ -65,7 +65,7 @@ export type TAddUsersToGroup = {
|
||||
userGroupMembershipDAL: Pick<TUserGroupMembershipDALFactory, "find" | "transaction" | "insertMany">;
|
||||
groupProjectDAL: Pick<TGroupProjectDALFactory, "find">;
|
||||
projectKeyDAL: Pick<TProjectKeyDALFactory, "find" | "findLatestProjectKey" | "insertMany">;
|
||||
projectDAL: Pick<TProjectDALFactory, "findProjectGhostUser">;
|
||||
projectDAL: Pick<TProjectDALFactory, "findProjectGhostUser" | "findById">;
|
||||
projectBotDAL: Pick<TProjectBotDALFactory, "findOne">;
|
||||
tx: Knex;
|
||||
};
|
||||
@@ -78,7 +78,7 @@ export type TAddUsersToGroupByUserIds = {
|
||||
orgDAL: Pick<TOrgDALFactory, "findMembership">;
|
||||
groupProjectDAL: Pick<TGroupProjectDALFactory, "find">;
|
||||
projectKeyDAL: Pick<TProjectKeyDALFactory, "find" | "findLatestProjectKey" | "insertMany">;
|
||||
projectDAL: Pick<TProjectDALFactory, "findProjectGhostUser">;
|
||||
projectDAL: Pick<TProjectDALFactory, "findProjectGhostUser" | "findById">;
|
||||
projectBotDAL: Pick<TProjectBotDALFactory, "findOne">;
|
||||
tx?: Knex;
|
||||
};
|
||||
@@ -102,7 +102,7 @@ export type TConvertPendingGroupAdditionsToGroupMemberships = {
|
||||
>;
|
||||
groupProjectDAL: Pick<TGroupProjectDALFactory, "find">;
|
||||
projectKeyDAL: Pick<TProjectKeyDALFactory, "find" | "findLatestProjectKey" | "insertMany">;
|
||||
projectDAL: Pick<TProjectDALFactory, "findProjectGhostUser">;
|
||||
projectDAL: Pick<TProjectDALFactory, "findProjectGhostUser" | "findById">;
|
||||
projectBotDAL: Pick<TProjectBotDALFactory, "findOne">;
|
||||
tx?: Knex;
|
||||
};
|
||||
|
@@ -0,0 +1,83 @@
|
||||
/* eslint-disable no-case-declarations */
|
||||
import { Knex } from "knex";
|
||||
|
||||
import { TDbClient } from "@app/db";
|
||||
import { TableName } from "@app/db/schemas";
|
||||
import { buildFindFilter, ormify } from "@app/lib/knex";
|
||||
|
||||
import { IdentityAuthTemplateMethod } from "./identity-auth-template-enums";
|
||||
|
||||
export type TIdentityAuthTemplateDALFactory = ReturnType<typeof identityAuthTemplateDALFactory>;
|
||||
|
||||
export const identityAuthTemplateDALFactory = (db: TDbClient) => {
|
||||
const identityAuthTemplateOrm = ormify(db, TableName.IdentityAuthTemplate);
|
||||
|
||||
const findByOrgId = async (
|
||||
orgId: string,
|
||||
{ limit, offset, search, tx }: { limit?: number; offset?: number; search?: string; tx?: Knex } = {}
|
||||
) => {
|
||||
let query = (tx || db.replicaNode())(TableName.IdentityAuthTemplate).where({ orgId });
|
||||
let countQuery = (tx || db.replicaNode())(TableName.IdentityAuthTemplate).where({ orgId });
|
||||
|
||||
if (search) {
|
||||
const searchFilter = `%${search.toLowerCase()}%`;
|
||||
query = query.whereRaw("LOWER(name) LIKE ?", [searchFilter]);
|
||||
countQuery = countQuery.whereRaw("LOWER(name) LIKE ?", [searchFilter]);
|
||||
}
|
||||
|
||||
query = query.orderBy("createdAt", "desc");
|
||||
|
||||
if (limit !== undefined) {
|
||||
query = query.limit(limit);
|
||||
}
|
||||
if (offset !== undefined) {
|
||||
query = query.offset(offset);
|
||||
}
|
||||
|
||||
const docs = await query;
|
||||
|
||||
const [{ count }] = (await countQuery.count("* as count")) as [{ count: string | number }];
|
||||
|
||||
return { docs, totalCount: Number(count) };
|
||||
};
|
||||
|
||||
const findByAuthMethod = async (authMethod: string, orgId: string, tx?: Knex) => {
|
||||
const query = (tx || db.replicaNode())(TableName.IdentityAuthTemplate)
|
||||
.where({ authMethod, orgId })
|
||||
.orderBy("createdAt", "desc");
|
||||
const docs = await query;
|
||||
return docs;
|
||||
};
|
||||
|
||||
const findTemplateUsages = async (templateId: string, authMethod: string, tx?: Knex) => {
|
||||
switch (authMethod) {
|
||||
case IdentityAuthTemplateMethod.LDAP:
|
||||
const query = (tx || db.replicaNode())(TableName.IdentityLdapAuth)
|
||||
.join(TableName.Identity, `${TableName.IdentityLdapAuth}.identityId`, `${TableName.Identity}.id`)
|
||||
// eslint-disable-next-line @typescript-eslint/no-misused-promises
|
||||
.where(buildFindFilter({ templateId }, TableName.IdentityLdapAuth))
|
||||
.select(
|
||||
db.ref("identityId").withSchema(TableName.IdentityLdapAuth),
|
||||
db.ref("name").withSchema(TableName.Identity).as("identityName")
|
||||
);
|
||||
const docs = await query;
|
||||
return docs;
|
||||
default:
|
||||
return [];
|
||||
}
|
||||
};
|
||||
|
||||
const findByIdAndOrgId = async (id: string, orgId: string, tx?: Knex) => {
|
||||
const query = (tx || db.replicaNode())(TableName.IdentityAuthTemplate).where({ id, orgId });
|
||||
const doc = await query;
|
||||
return doc?.[0];
|
||||
};
|
||||
|
||||
return {
|
||||
...identityAuthTemplateOrm,
|
||||
findByOrgId,
|
||||
findByAuthMethod,
|
||||
findTemplateUsages,
|
||||
findByIdAndOrgId
|
||||
};
|
||||
};
|
@@ -0,0 +1,22 @@
|
||||
export enum IdentityAuthTemplateMethod {
|
||||
LDAP = "ldap"
|
||||
}
|
||||
|
||||
export const TEMPLATE_VALIDATION_MESSAGES = {
|
||||
TEMPLATE_NAME_REQUIRED: "Template name is required",
|
||||
TEMPLATE_NAME_MAX_LENGTH: "Template name must be at most 64 characters long",
|
||||
AUTH_METHOD_REQUIRED: "Auth method is required",
|
||||
TEMPLATE_ID_REQUIRED: "Template ID is required",
|
||||
LDAP: {
|
||||
URL_REQUIRED: "LDAP URL is required",
|
||||
BIND_DN_REQUIRED: "Bind DN is required",
|
||||
BIND_PASSWORD_REQUIRED: "Bind password is required",
|
||||
SEARCH_BASE_REQUIRED: "Search base is required"
|
||||
}
|
||||
} as const;
|
||||
|
||||
export const TEMPLATE_SUCCESS_MESSAGES = {
|
||||
CREATED: "Template created successfully",
|
||||
UPDATED: "Template updated successfully",
|
||||
DELETED: "Template deleted successfully"
|
||||
} as const;
|
@@ -0,0 +1,454 @@
|
||||
import { ForbiddenError } from "@casl/ability";
|
||||
|
||||
import { EventType, TAuditLogServiceFactory } from "@app/ee/services/audit-log/audit-log-types";
|
||||
import { TLicenseServiceFactory } from "@app/ee/services/license/license-service";
|
||||
import {
|
||||
OrgPermissionMachineIdentityAuthTemplateActions,
|
||||
OrgPermissionSubjects
|
||||
} from "@app/ee/services/permission/org-permission";
|
||||
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service-types";
|
||||
import { BadRequestError, NotFoundError } from "@app/lib/errors";
|
||||
import { TOrgPermission } from "@app/lib/types";
|
||||
import { ActorType } from "@app/services/auth/auth-type";
|
||||
import { TIdentityLdapAuthDALFactory } from "@app/services/identity-ldap-auth/identity-ldap-auth-dal";
|
||||
import { TKmsServiceFactory } from "@app/services/kms/kms-service";
|
||||
import { KmsDataKey } from "@app/services/kms/kms-types";
|
||||
|
||||
import { TIdentityAuthTemplateDALFactory } from "./identity-auth-template-dal";
|
||||
import { IdentityAuthTemplateMethod } from "./identity-auth-template-enums";
|
||||
import {
|
||||
TDeleteIdentityAuthTemplateDTO,
|
||||
TFindTemplateUsagesDTO,
|
||||
TGetIdentityAuthTemplateDTO,
|
||||
TGetTemplatesByAuthMethodDTO,
|
||||
TLdapTemplateFields,
|
||||
TListIdentityAuthTemplatesDTO,
|
||||
TUnlinkTemplateUsageDTO
|
||||
} from "./identity-auth-template-types";
|
||||
|
||||
type TIdentityAuthTemplateServiceFactoryDep = {
|
||||
identityAuthTemplateDAL: TIdentityAuthTemplateDALFactory;
|
||||
identityLdapAuthDAL: TIdentityLdapAuthDALFactory;
|
||||
permissionService: Pick<TPermissionServiceFactory, "getOrgPermission">;
|
||||
kmsService: Pick<TKmsServiceFactory, "createCipherPairWithDataKey" | "encryptWithInputKey" | "decryptWithInputKey">;
|
||||
licenseService: Pick<TLicenseServiceFactory, "getPlan">;
|
||||
auditLogService: Pick<TAuditLogServiceFactory, "createAuditLog">;
|
||||
};
|
||||
|
||||
export type TIdentityAuthTemplateServiceFactory = ReturnType<typeof identityAuthTemplateServiceFactory>;
|
||||
|
||||
export const identityAuthTemplateServiceFactory = ({
|
||||
identityAuthTemplateDAL,
|
||||
identityLdapAuthDAL,
|
||||
permissionService,
|
||||
kmsService,
|
||||
licenseService,
|
||||
auditLogService
|
||||
}: TIdentityAuthTemplateServiceFactoryDep) => {
|
||||
// Plan check
|
||||
const $checkPlan = async (orgId: string) => {
|
||||
const plan = await licenseService.getPlan(orgId);
|
||||
if (!plan.machineIdentityAuthTemplates)
|
||||
throw new BadRequestError({
|
||||
message:
|
||||
"Failed to use identity auth template due to plan restriction. Upgrade plan to access machine identity auth templates."
|
||||
});
|
||||
};
|
||||
const createTemplate = async ({
|
||||
name,
|
||||
authMethod,
|
||||
templateFields,
|
||||
actorId,
|
||||
actorAuthMethod,
|
||||
actor,
|
||||
actorOrgId
|
||||
}: {
|
||||
name: string;
|
||||
authMethod: string;
|
||||
templateFields: Record<string, unknown>;
|
||||
} & Omit<TOrgPermission, "orgId">) => {
|
||||
await $checkPlan(actorOrgId);
|
||||
const { permission } = await permissionService.getOrgPermission(
|
||||
actor,
|
||||
actorId,
|
||||
actorOrgId,
|
||||
actorAuthMethod,
|
||||
actorOrgId
|
||||
);
|
||||
ForbiddenError.from(permission).throwUnlessCan(
|
||||
OrgPermissionMachineIdentityAuthTemplateActions.CreateTemplates,
|
||||
OrgPermissionSubjects.MachineIdentityAuthTemplate
|
||||
);
|
||||
|
||||
const { encryptor } = await kmsService.createCipherPairWithDataKey({
|
||||
type: KmsDataKey.Organization,
|
||||
orgId: actorOrgId
|
||||
});
|
||||
const template = await identityAuthTemplateDAL.create({
|
||||
name,
|
||||
authMethod,
|
||||
templateFields: encryptor({ plainText: Buffer.from(JSON.stringify(templateFields)) }).cipherTextBlob,
|
||||
orgId: actorOrgId
|
||||
});
|
||||
|
||||
return { ...template, templateFields };
|
||||
};
|
||||
|
||||
const updateTemplate = async ({
|
||||
templateId,
|
||||
name,
|
||||
templateFields,
|
||||
actorId,
|
||||
actorAuthMethod,
|
||||
actor,
|
||||
actorOrgId
|
||||
}: {
|
||||
templateId: string;
|
||||
name?: string;
|
||||
templateFields?: Record<string, unknown>;
|
||||
} & Omit<TOrgPermission, "orgId">) => {
|
||||
await $checkPlan(actorOrgId);
|
||||
const template = await identityAuthTemplateDAL.findByIdAndOrgId(templateId, actorOrgId);
|
||||
if (!template) {
|
||||
throw new NotFoundError({ message: "Template not found" });
|
||||
}
|
||||
|
||||
const { permission } = await permissionService.getOrgPermission(
|
||||
actor,
|
||||
actorId,
|
||||
template.orgId,
|
||||
actorAuthMethod,
|
||||
actorOrgId
|
||||
);
|
||||
ForbiddenError.from(permission).throwUnlessCan(
|
||||
OrgPermissionMachineIdentityAuthTemplateActions.EditTemplates,
|
||||
OrgPermissionSubjects.MachineIdentityAuthTemplate
|
||||
);
|
||||
|
||||
const { encryptor } = await kmsService.createCipherPairWithDataKey({
|
||||
type: KmsDataKey.Organization,
|
||||
orgId: template.orgId
|
||||
});
|
||||
|
||||
let finalTemplateFields: Record<string, unknown> = {};
|
||||
|
||||
const updatedTemplate = await identityAuthTemplateDAL.transaction(async (tx) => {
|
||||
const authTemplate = await identityAuthTemplateDAL.updateById(
|
||||
templateId,
|
||||
{
|
||||
name,
|
||||
...(templateFields && {
|
||||
templateFields: encryptor({ plainText: Buffer.from(JSON.stringify(templateFields)) }).cipherTextBlob
|
||||
})
|
||||
},
|
||||
tx
|
||||
);
|
||||
|
||||
if (templateFields && template.authMethod === IdentityAuthTemplateMethod.LDAP) {
|
||||
const { decryptor } = await kmsService.createCipherPairWithDataKey({
|
||||
type: KmsDataKey.Organization,
|
||||
orgId: template.orgId
|
||||
});
|
||||
|
||||
const currentTemplateFields = JSON.parse(
|
||||
decryptor({ cipherTextBlob: template.templateFields }).toString()
|
||||
) as TLdapTemplateFields;
|
||||
|
||||
const mergedTemplateFields: TLdapTemplateFields = { ...currentTemplateFields, ...templateFields };
|
||||
finalTemplateFields = mergedTemplateFields;
|
||||
const ldapUpdateData: {
|
||||
url?: string;
|
||||
searchBase?: string;
|
||||
encryptedBindDN?: Buffer;
|
||||
encryptedBindPass?: Buffer;
|
||||
encryptedLdapCaCertificate?: Buffer;
|
||||
} = {};
|
||||
|
||||
if ("url" in templateFields) {
|
||||
ldapUpdateData.url = mergedTemplateFields.url;
|
||||
}
|
||||
if ("searchBase" in templateFields) {
|
||||
ldapUpdateData.searchBase = mergedTemplateFields.searchBase;
|
||||
}
|
||||
if ("bindDN" in templateFields) {
|
||||
ldapUpdateData.encryptedBindDN = encryptor({
|
||||
plainText: Buffer.from(mergedTemplateFields.bindDN)
|
||||
}).cipherTextBlob;
|
||||
}
|
||||
if ("bindPass" in templateFields) {
|
||||
ldapUpdateData.encryptedBindPass = encryptor({
|
||||
plainText: Buffer.from(mergedTemplateFields.bindPass)
|
||||
}).cipherTextBlob;
|
||||
}
|
||||
if ("ldapCaCertificate" in templateFields) {
|
||||
ldapUpdateData.encryptedLdapCaCertificate = encryptor({
|
||||
plainText: Buffer.from(mergedTemplateFields.ldapCaCertificate || "")
|
||||
}).cipherTextBlob;
|
||||
}
|
||||
|
||||
if (Object.keys(ldapUpdateData).length > 0) {
|
||||
const updatedLdapAuths = await identityLdapAuthDAL.update({ templateId }, ldapUpdateData, tx);
|
||||
await Promise.all(
|
||||
updatedLdapAuths.map(async (updatedLdapAuth) => {
|
||||
await auditLogService.createAuditLog({
|
||||
actor: {
|
||||
type: ActorType.PLATFORM,
|
||||
metadata: {}
|
||||
},
|
||||
orgId: actorOrgId,
|
||||
event: {
|
||||
type: EventType.UPDATE_IDENTITY_LDAP_AUTH,
|
||||
metadata: {
|
||||
identityId: updatedLdapAuth.identityId,
|
||||
templateId: template.id
|
||||
}
|
||||
}
|
||||
});
|
||||
})
|
||||
);
|
||||
}
|
||||
}
|
||||
return authTemplate;
|
||||
});
|
||||
|
||||
return { ...updatedTemplate, templateFields: finalTemplateFields };
|
||||
};
|
||||
|
||||
const deleteTemplate = async ({
|
||||
templateId,
|
||||
actorId,
|
||||
actorAuthMethod,
|
||||
actor,
|
||||
actorOrgId
|
||||
}: TDeleteIdentityAuthTemplateDTO) => {
|
||||
await $checkPlan(actorOrgId);
|
||||
const template = await identityAuthTemplateDAL.findByIdAndOrgId(templateId, actorOrgId);
|
||||
if (!template) {
|
||||
throw new NotFoundError({ message: "Template not found" });
|
||||
}
|
||||
|
||||
const { permission } = await permissionService.getOrgPermission(
|
||||
actor,
|
||||
actorId,
|
||||
template.orgId,
|
||||
actorAuthMethod,
|
||||
actorOrgId
|
||||
);
|
||||
ForbiddenError.from(permission).throwUnlessCan(
|
||||
OrgPermissionMachineIdentityAuthTemplateActions.DeleteTemplates,
|
||||
OrgPermissionSubjects.MachineIdentityAuthTemplate
|
||||
);
|
||||
|
||||
const deletedTemplate = await identityAuthTemplateDAL.transaction(async (tx) => {
|
||||
// Remove template reference from identityLdapAuth records
|
||||
const updatedLdapAuths = await identityLdapAuthDAL.update({ templateId }, { templateId: null }, tx);
|
||||
await Promise.all(
|
||||
updatedLdapAuths.map(async (updatedLdapAuth) => {
|
||||
await auditLogService.createAuditLog({
|
||||
actor: {
|
||||
type: ActorType.PLATFORM,
|
||||
metadata: {}
|
||||
},
|
||||
orgId: actorOrgId,
|
||||
event: {
|
||||
type: EventType.UPDATE_IDENTITY_LDAP_AUTH,
|
||||
metadata: {
|
||||
identityId: updatedLdapAuth.identityId,
|
||||
templateId: template.id
|
||||
}
|
||||
}
|
||||
});
|
||||
})
|
||||
);
|
||||
|
||||
// Delete the template
|
||||
const [deletedTpl] = await identityAuthTemplateDAL.delete({ id: templateId }, tx);
|
||||
return deletedTpl;
|
||||
});
|
||||
|
||||
return deletedTemplate;
|
||||
};
|
||||
|
||||
const getTemplate = async ({
|
||||
templateId,
|
||||
actorId,
|
||||
actorAuthMethod,
|
||||
actor,
|
||||
actorOrgId
|
||||
}: TGetIdentityAuthTemplateDTO) => {
|
||||
await $checkPlan(actorOrgId);
|
||||
const template = await identityAuthTemplateDAL.findByIdAndOrgId(templateId, actorOrgId);
|
||||
if (!template) {
|
||||
throw new NotFoundError({ message: "Template not found" });
|
||||
}
|
||||
|
||||
const { permission } = await permissionService.getOrgPermission(
|
||||
actor,
|
||||
actorId,
|
||||
template.orgId,
|
||||
actorAuthMethod,
|
||||
actorOrgId
|
||||
);
|
||||
ForbiddenError.from(permission).throwUnlessCan(
|
||||
OrgPermissionMachineIdentityAuthTemplateActions.ListTemplates,
|
||||
OrgPermissionSubjects.MachineIdentityAuthTemplate
|
||||
);
|
||||
|
||||
const { decryptor } = await kmsService.createCipherPairWithDataKey({
|
||||
type: KmsDataKey.Organization,
|
||||
orgId: template.orgId
|
||||
});
|
||||
const decryptedTemplateFields = decryptor({ cipherTextBlob: template.templateFields }).toString();
|
||||
return {
|
||||
...template,
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
|
||||
templateFields: JSON.parse(decryptedTemplateFields)
|
||||
};
|
||||
};
|
||||
|
||||
const listTemplates = async ({
|
||||
limit,
|
||||
offset,
|
||||
search,
|
||||
actorId,
|
||||
actorAuthMethod,
|
||||
actor,
|
||||
actorOrgId
|
||||
}: TListIdentityAuthTemplatesDTO) => {
|
||||
await $checkPlan(actorOrgId);
|
||||
const { permission } = await permissionService.getOrgPermission(
|
||||
actor,
|
||||
actorId,
|
||||
actorOrgId,
|
||||
actorAuthMethod,
|
||||
actorOrgId
|
||||
);
|
||||
ForbiddenError.from(permission).throwUnlessCan(
|
||||
OrgPermissionMachineIdentityAuthTemplateActions.ListTemplates,
|
||||
OrgPermissionSubjects.MachineIdentityAuthTemplate
|
||||
);
|
||||
|
||||
const { docs, totalCount } = await identityAuthTemplateDAL.findByOrgId(actorOrgId, { limit, offset, search });
|
||||
|
||||
const { decryptor } = await kmsService.createCipherPairWithDataKey({
|
||||
type: KmsDataKey.Organization,
|
||||
orgId: actorOrgId
|
||||
});
|
||||
return {
|
||||
totalCount,
|
||||
templates: docs.map((doc) => ({
|
||||
...doc,
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
|
||||
templateFields: JSON.parse(decryptor({ cipherTextBlob: doc.templateFields }).toString())
|
||||
}))
|
||||
};
|
||||
};
|
||||
|
||||
const getTemplatesByAuthMethod = async ({
|
||||
authMethod,
|
||||
actorId,
|
||||
actorAuthMethod,
|
||||
actor,
|
||||
actorOrgId
|
||||
}: TGetTemplatesByAuthMethodDTO) => {
|
||||
await $checkPlan(actorOrgId);
|
||||
const { permission } = await permissionService.getOrgPermission(
|
||||
actor,
|
||||
actorId,
|
||||
actorOrgId,
|
||||
actorAuthMethod,
|
||||
actorOrgId
|
||||
);
|
||||
ForbiddenError.from(permission).throwUnlessCan(
|
||||
OrgPermissionMachineIdentityAuthTemplateActions.AttachTemplates,
|
||||
OrgPermissionSubjects.MachineIdentityAuthTemplate
|
||||
);
|
||||
|
||||
const docs = await identityAuthTemplateDAL.findByAuthMethod(authMethod, actorOrgId);
|
||||
|
||||
const { decryptor } = await kmsService.createCipherPairWithDataKey({
|
||||
type: KmsDataKey.Organization,
|
||||
orgId: actorOrgId
|
||||
});
|
||||
return docs.map((doc) => ({
|
||||
...doc,
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
|
||||
templateFields: JSON.parse(decryptor({ cipherTextBlob: doc.templateFields }).toString())
|
||||
}));
|
||||
};
|
||||
|
||||
const findTemplateUsages = async ({
|
||||
templateId,
|
||||
actorId,
|
||||
actorAuthMethod,
|
||||
actor,
|
||||
actorOrgId
|
||||
}: TFindTemplateUsagesDTO) => {
|
||||
await $checkPlan(actorOrgId);
|
||||
const { permission } = await permissionService.getOrgPermission(
|
||||
actor,
|
||||
actorId,
|
||||
actorOrgId,
|
||||
actorAuthMethod,
|
||||
actorOrgId
|
||||
);
|
||||
ForbiddenError.from(permission).throwUnlessCan(
|
||||
OrgPermissionMachineIdentityAuthTemplateActions.ListTemplates,
|
||||
OrgPermissionSubjects.MachineIdentityAuthTemplate
|
||||
);
|
||||
|
||||
const template = await identityAuthTemplateDAL.findByIdAndOrgId(templateId, actorOrgId);
|
||||
if (!template) {
|
||||
throw new NotFoundError({ message: "Template not found" });
|
||||
}
|
||||
|
||||
const docs = await identityAuthTemplateDAL.findTemplateUsages(templateId, template.authMethod);
|
||||
return docs;
|
||||
};
|
||||
|
||||
const unlinkTemplateUsage = async ({
|
||||
templateId,
|
||||
identityIds,
|
||||
actorId,
|
||||
actorAuthMethod,
|
||||
actor,
|
||||
actorOrgId
|
||||
}: TUnlinkTemplateUsageDTO) => {
|
||||
await $checkPlan(actorOrgId);
|
||||
const { permission } = await permissionService.getOrgPermission(
|
||||
actor,
|
||||
actorId,
|
||||
actorOrgId,
|
||||
actorAuthMethod,
|
||||
actorOrgId
|
||||
);
|
||||
ForbiddenError.from(permission).throwUnlessCan(
|
||||
OrgPermissionMachineIdentityAuthTemplateActions.UnlinkTemplates,
|
||||
OrgPermissionSubjects.MachineIdentityAuthTemplate
|
||||
);
|
||||
|
||||
const template = await identityAuthTemplateDAL.findByIdAndOrgId(templateId, actorOrgId);
|
||||
if (!template) {
|
||||
throw new NotFoundError({ message: "Template not found" });
|
||||
}
|
||||
|
||||
switch (template.authMethod) {
|
||||
case IdentityAuthTemplateMethod.LDAP:
|
||||
await identityLdapAuthDAL.update({ $in: { identityId: identityIds }, templateId }, { templateId: null });
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
};
|
||||
|
||||
return {
|
||||
createTemplate,
|
||||
updateTemplate,
|
||||
deleteTemplate,
|
||||
getTemplate,
|
||||
listTemplates,
|
||||
getTemplatesByAuthMethod,
|
||||
findTemplateUsages,
|
||||
unlinkTemplateUsage
|
||||
};
|
||||
};
|
@@ -0,0 +1,61 @@
|
||||
import { TProjectPermission } from "@app/lib/types";
|
||||
|
||||
import { IdentityAuthTemplateMethod } from "./identity-auth-template-enums";
|
||||
|
||||
// Method-specific template field types
|
||||
export type TLdapTemplateFields = {
|
||||
url: string;
|
||||
bindDN: string;
|
||||
bindPass: string;
|
||||
searchBase: string;
|
||||
ldapCaCertificate?: string;
|
||||
};
|
||||
|
||||
// Union type for all template field types
|
||||
export type TTemplateFieldsByMethod = {
|
||||
[IdentityAuthTemplateMethod.LDAP]: TLdapTemplateFields;
|
||||
};
|
||||
|
||||
// Generic base types that use conditional types for type safety
|
||||
export type TCreateIdentityAuthTemplateDTO = {
|
||||
name: string;
|
||||
authMethod: IdentityAuthTemplateMethod;
|
||||
templateFields: TTemplateFieldsByMethod[IdentityAuthTemplateMethod];
|
||||
} & Omit<TProjectPermission, "projectId">;
|
||||
|
||||
export type TUpdateIdentityAuthTemplateDTO = {
|
||||
templateId: string;
|
||||
name?: string;
|
||||
templateFields?: Partial<TTemplateFieldsByMethod[IdentityAuthTemplateMethod]>;
|
||||
} & Omit<TProjectPermission, "projectId">;
|
||||
|
||||
export type TDeleteIdentityAuthTemplateDTO = {
|
||||
templateId: string;
|
||||
} & Omit<TProjectPermission, "projectId">;
|
||||
|
||||
export type TGetIdentityAuthTemplateDTO = {
|
||||
templateId: string;
|
||||
} & Omit<TProjectPermission, "projectId">;
|
||||
|
||||
export type TListIdentityAuthTemplatesDTO = {
|
||||
limit?: number;
|
||||
offset?: number;
|
||||
search?: string;
|
||||
} & Omit<TProjectPermission, "projectId">;
|
||||
|
||||
export type TGetTemplatesByAuthMethodDTO = {
|
||||
authMethod: string;
|
||||
} & Omit<TProjectPermission, "projectId">;
|
||||
|
||||
export type TFindTemplateUsagesDTO = {
|
||||
templateId: string;
|
||||
} & Omit<TProjectPermission, "projectId">;
|
||||
|
||||
export type TUnlinkTemplateUsageDTO = {
|
||||
templateId: string;
|
||||
identityIds: string[];
|
||||
} & Omit<TProjectPermission, "projectId">;
|
||||
|
||||
// Specific LDAP types for convenience
|
||||
export type TCreateLdapTemplateDTO = TCreateIdentityAuthTemplateDTO;
|
||||
export type TUpdateLdapTemplateDTO = TUpdateIdentityAuthTemplateDTO;
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user