mirror of
https://github.com/Infisical/infisical.git
synced 2025-07-20 01:48:03 +00:00
Compare commits
596 Commits
v0.85.0-po
...
doc/update
Author | SHA1 | Date | |
---|---|---|---|
5f2144eca5 | |||
45b9de63f0 | |||
114966ded4 | |||
71081d8e9a | |||
dad3d50f3e | |||
e5ca5d3da2 | |||
301cd54dc3 | |||
593bda8bc6 | |||
4db79edf19 | |||
e3a356cda9 | |||
521b24debf | |||
ca3b64bf6c | |||
b7e48fd556 | |||
c01ea048ce | |||
7e7d9a2bd5 | |||
782e3a8985 | |||
8497ac831f | |||
e5821122d5 | |||
340693cfcd | |||
014b9585e0 | |||
67373b0883 | |||
2101040a77 | |||
2e2fea304b | |||
571709370d | |||
e1dbe769a8 | |||
e7e0d84c8e | |||
4c2ed1cc8b | |||
067b0f4232 | |||
6ed786e6d0 | |||
d187cc3d4d | |||
764446a2d9 | |||
614e4934a2 | |||
14e92f895a | |||
0a38374a73 | |||
ec3b94a335 | |||
ca0241bb51 | |||
7403385e7c | |||
2cd1141a65 | |||
256627b2cc | |||
fd7e196f8b | |||
212748f140 | |||
b61582a60e | |||
9ca8da152b | |||
c5aa1b8664 | |||
90dbb417ac | |||
7fb3076238 | |||
946651496f | |||
5a8ac850b5 | |||
77a88f1575 | |||
c6f66226c8 | |||
be00d13a46 | |||
84814a0012 | |||
a0865cda2e | |||
de03692469 | |||
fb2d3e4eb7 | |||
29150e809d | |||
e18a606b23 | |||
67708411cd | |||
1e7b1ccf22 | |||
3e4bd28916 | |||
a2e16370fa | |||
d677654311 | |||
903fac1005 | |||
ff045214d6 | |||
57dcf5ab28 | |||
959a5ec55b | |||
b22a93a175 | |||
5debeb421d | |||
25b30e441a | |||
d7d88f3356 | |||
dbaef9d227 | |||
38d8b14b03 | |||
8b9244b079 | |||
3d938ea62f | |||
78f668bd7f | |||
13c0b315a4 | |||
99e65f7b59 | |||
96bad7bf90 | |||
5e5f20cab2 | |||
8eb668cd72 | |||
2383c93139 | |||
154ea9e55d | |||
d36a9e2000 | |||
6f334e4cab | |||
700c5409bf | |||
6158b8a91d | |||
0c3024819c | |||
c8410ac6f3 | |||
41e4af4e65 | |||
bac9936c2a | |||
936a48f458 | |||
43cfd63660 | |||
0f10874f80 | |||
a9e6c229d0 | |||
7cd83ad945 | |||
2f691db0a2 | |||
eb6d5d2fb9 | |||
fc5487396b | |||
6db8c100ba | |||
acfb4693ee | |||
aeaabe2c27 | |||
c60d957269 | |||
b6dc6ffc01 | |||
181821f8f5 | |||
6ac44a79b2 | |||
77740d2c86 | |||
17567ebd0f | |||
7ed0818279 | |||
bb079b3e46 | |||
d94b4b2a3c | |||
9d90c35629 | |||
7a77dc7343 | |||
2cff772caa | |||
849cad054e | |||
518ca5fe58 | |||
65e42f980c | |||
f95957d534 | |||
bd1ed2614e | |||
01920d7a50 | |||
83ac8abf81 | |||
44544e0491 | |||
c47e0d661b | |||
9192c5caa2 | |||
b0fc5c7e27 | |||
bf5d7b2ba1 | |||
8da2213bf1 | |||
5b4c4f4543 | |||
080cf67b8c | |||
36bb954373 | |||
93afa91239 | |||
73fbf66d4c | |||
8ae0d97973 | |||
ca5ec94082 | |||
5d5da97b45 | |||
d61f36bca8 | |||
96f5dc7300 | |||
8e5debca90 | |||
08ed544e52 | |||
8c4a26b0e2 | |||
bda0681dee | |||
cf092d8b4f | |||
a11bcab0db | |||
986bcaf0df | |||
192d1b0be3 | |||
82c8ca9c3d | |||
4a1adb76ab | |||
94b799e80b | |||
bdae136bed | |||
73e73c5489 | |||
f3bcdf74df | |||
87cd3ea727 | |||
114f42fc14 | |||
6daa1aa221 | |||
52f85753c5 | |||
0a5634aa05 | |||
3e8b9aa296 | |||
67058d8b55 | |||
d112ec2f0a | |||
73382c5363 | |||
96c0e718d0 | |||
522e1dfd0e | |||
08145f9b96 | |||
faf2c6df90 | |||
b8f3814df0 | |||
1f4db2bd80 | |||
d8d784a0bc | |||
2dc1416f30 | |||
7fdcb29bab | |||
6a89e3527c | |||
d1d0667cd5 | |||
c176a20010 | |||
865db5a9b3 | |||
ad2f19658b | |||
bed8efb24c | |||
aa9af7b41c | |||
02fd484632 | |||
96eab464c7 | |||
162005d72f | |||
09d28156f8 | |||
fc67c496c5 | |||
540a1a29b1 | |||
3163adf486 | |||
e042f9b5e2 | |||
05a1b5397b | |||
19776df46c | |||
64fd65aa52 | |||
3d58eba78c | |||
565884d089 | |||
2a83da1cb6 | |||
f186ce9649 | |||
6ecfee5faf | |||
662f1a31f6 | |||
06f9a1484b | |||
c90e8ca715 | |||
6ddc4ce4b1 | |||
4fffac07fd | |||
059c552307 | |||
75d71d4208 | |||
e38628509d | |||
0b247176bb | |||
faad09961d | |||
98d4f808e5 | |||
2ae91db65d | |||
529328f0ae | |||
e59d9ff3c6 | |||
4aad36601c | |||
4aaba3ef9f | |||
b482a9cda7 | |||
595eb739af | |||
b46bbea0c5 | |||
6dad24ffde | |||
f8759b9801 | |||
049c77c902 | |||
1478833c9c | |||
c8d40c6905 | |||
ff815b5f42 | |||
e5138d0e99 | |||
f43725a16e | |||
f6c65584bf | |||
246020729e | |||
63cc4e347d | |||
ecaca82d9a | |||
d6ef0d1c83 | |||
f2a7f164e1 | |||
dfbdc46971 | |||
3049f9e719 | |||
391c9abbb0 | |||
e191a72ca0 | |||
68c38f228d | |||
a823347c99 | |||
22b417b50b | |||
98ed063ce6 | |||
c0fb493f57 | |||
eae5e57346 | |||
f6fcef24c6 | |||
5bf6f69fca | |||
acf054d992 | |||
56798f09bf | |||
4c1253dc87 | |||
09793979c7 | |||
fa360b8208 | |||
9f6d837a9b | |||
f94e100c30 | |||
33b54e78f9 | |||
98cca7039c | |||
f50b0876e4 | |||
c30763c98f | |||
6fc95c3ff8 | |||
eef1f2b6ef | |||
128b1cf856 | |||
6b9944001e | |||
1cc22a6195 | |||
af643468fd | |||
f8358a0807 | |||
3eefb98f30 | |||
8f39f953f8 | |||
5e4af7e568 | |||
24bd13403a | |||
4149cbdf07 | |||
ced3ab97e8 | |||
3f7f0a7b0a | |||
20bcf8aab8 | |||
0814245ce6 | |||
1687d66a0e | |||
cf446a38b3 | |||
36ef87909e | |||
6bfeac5e98 | |||
d669320385 | |||
8dbdb79833 | |||
2d2f27ea46 | |||
4aeb2bf65e | |||
24da76db19 | |||
3c49936eee | |||
b416e79d63 | |||
92c529587b | |||
3b74c232dc | |||
6164dc32d7 | |||
37e7040eea | |||
a7ebb4b241 | |||
2fc562ff2d | |||
b5c83fea4d | |||
b586f98926 | |||
e6205c086f | |||
2ca34099ed | |||
5da6c12941 | |||
e2612b75fc | |||
ca5edb95f1 | |||
724e2b3692 | |||
2c93561a3b | |||
0b24cc8631 | |||
6c6e932899 | |||
c66a711890 | |||
787f8318fe | |||
9a27873af5 | |||
0abab57d83 | |||
d5662dfef4 | |||
ee2ee48b47 | |||
896d977b95 | |||
d1966b60a8 | |||
e05f05f9ed | |||
81846d9c67 | |||
723f0e862d | |||
2d0433b96c | |||
e3cbcf5853 | |||
bdf1f7c601 | |||
24b23d4f90 | |||
09c1a5f778 | |||
73a9cf01f3 | |||
97e860cf21 | |||
25b55087cf | |||
25f694bbdb | |||
7cd85cf84a | |||
cf5c886b6f | |||
e667c7c988 | |||
fd254fbeec | |||
859c556425 | |||
9b1615f2fb | |||
a3cad030e5 | |||
342e9f99d3 | |||
8ed04d0b75 | |||
5b5a8ff03f | |||
e0199084ad | |||
dc8c3a30bd | |||
67a6deed72 | |||
86cb51364a | |||
355113e15d | |||
40c589eced | |||
ec4f175f73 | |||
2273c21eb2 | |||
97c2b15e29 | |||
2f90ee067b | |||
7b64288019 | |||
e6e1ed7ca9 | |||
73838190fd | |||
d32fad87d1 | |||
67db9679fa | |||
3edd48a8b3 | |||
a4091bfcdd | |||
24483631a0 | |||
0f74a1a011 | |||
62d6e3763b | |||
39ea7a032f | |||
3ac125f9c7 | |||
7667a7e665 | |||
d7499fc5c5 | |||
f6885b239b | |||
4928322cdb | |||
77e191d63e | |||
15c98a1d2e | |||
ed757bdeff | |||
65241ad8bf | |||
6a7760f33f | |||
fdc62e21ef | |||
32f866f834 | |||
fbf52850e8 | |||
ab9b207f96 | |||
5532b9cfea | |||
449d3f0304 | |||
f0210c2607 | |||
ad88aaf17f | |||
0485b56e8d | |||
b65842f5c1 | |||
22b6e0afcd | |||
b0e536e576 | |||
54e4314e88 | |||
d00b1847cc | |||
be02617855 | |||
b5065f13c9 | |||
659b6d5d19 | |||
9c33251c44 | |||
1a0896475c | |||
7e820745a4 | |||
fa63c150dd | |||
1a2495a95c | |||
d79099946a | |||
27afad583b | |||
acde0867a0 | |||
d44f99bac2 | |||
2b35e20b1d | |||
da15957c3f | |||
208fc3452d | |||
ba1db870a4 | |||
7885a3b0ff | |||
66485f0464 | |||
0741058c1d | |||
3a6e79c575 | |||
70aa73482e | |||
2fa30bdd0e | |||
b28fe30bba | |||
9ba39e99c6 | |||
0e6aed7497 | |||
7e11fbe7a3 | |||
23abab987f | |||
5856a42807 | |||
a44b3efeb7 | |||
1992a09ac2 | |||
efa54e0c46 | |||
bde2d5e0a6 | |||
4090c894fc | |||
221bde01f8 | |||
b191a3c2f4 | |||
032197ee9f | |||
d5a4eb609a | |||
e7f1980b80 | |||
d430293c66 | |||
180d2692cd | |||
433e58655a | |||
5ffb6b7232 | |||
55ca9149d5 | |||
4ea57ca9a0 | |||
7ac4b0b79f | |||
2d51ed317f | |||
02c51b05b6 | |||
cd09f03f0b | |||
bc475e0f08 | |||
441b008709 | |||
4d81a0251e | |||
59da513481 | |||
c17047a193 | |||
f50a881273 | |||
afd6dd5257 | |||
3a43d7c5d5 | |||
65375886bd | |||
8495107849 | |||
c011d99b8b | |||
adc3542750 | |||
82e3241f1b | |||
2bca46886a | |||
971987c786 | |||
cd71a13bb7 | |||
98290fe31b | |||
9f15fb1474 | |||
301a867f8b | |||
658a044e85 | |||
2c1e29445d | |||
3f4c4f7418 | |||
592cc13b1f | |||
e70c2f3d10 | |||
bac865eab1 | |||
3d8fbc0a58 | |||
1fcfab7efa | |||
499334eef1 | |||
9fd76b8729 | |||
80d450e980 | |||
a1f2629366 | |||
bf8e1f2bfd | |||
f7d10ceeda | |||
095883a94e | |||
51638b7c71 | |||
adaddad370 | |||
cf6ff58f16 | |||
3e3f42a8f7 | |||
974e21d856 | |||
da86338bfe | |||
3a9a6767a0 | |||
fe8a1e6ce6 | |||
55aa3f7b58 | |||
59f3581370 | |||
ccae63936c | |||
6733349af0 | |||
f63c6b725b | |||
50b51f1810 | |||
fc39b3b0dd | |||
5964976e47 | |||
677a87150b | |||
2469c8d0c6 | |||
dafb89d1dd | |||
8da01445e5 | |||
6b2273d314 | |||
b886e66ee9 | |||
3afcb19727 | |||
06d2480f30 | |||
fd7d8ddf2d | |||
1dc0f4e5b8 | |||
fa64a88c24 | |||
385ec05e57 | |||
3a38e1e413 | |||
7f04e9e97d | |||
839f0c7e1c | |||
2352e29902 | |||
fcbc7fcece | |||
c2252c65a4 | |||
e150673de4 | |||
4f5c49a529 | |||
7107089ad3 | |||
967818f57d | |||
14c89c9be5 | |||
02111c2dc2 | |||
ebea74b607 | |||
5bbe5421bf | |||
279289989f | |||
bb4a16cf7c | |||
309db49f1b | |||
62a582ef17 | |||
d6b389760d | |||
bd4deb02b0 | |||
449e7672f9 | |||
31ff6d3c17 | |||
cfcc32271f | |||
e2ea84f28a | |||
6885ef2e54 | |||
8fa9f476e3 | |||
1cf8d1e3fa | |||
9f61177b62 | |||
59b8e83476 | |||
eee4d00a08 | |||
51c0598b50 | |||
69311f058b | |||
0f70c3ea9a | |||
b5660c87a0 | |||
2a686e65cd | |||
2bb0386220 | |||
526605a0bb | |||
5b9903a226 | |||
3fc60bf596 | |||
7815d6538f | |||
4c4d525655 | |||
e44213a8a9 | |||
e87656631c | |||
e102ccf9f0 | |||
63af75a330 | |||
8a10af9b62 | |||
18308950d1 | |||
86a9676a9c | |||
aa12a71ff3 | |||
aee46d1902 | |||
279a1791f6 | |||
8d71b295ea | |||
f72cedae10 | |||
864cf23416 | |||
10574bfe26 | |||
02085ce902 | |||
4eeea0b27c | |||
93b7f56337 | |||
12ecefa832 | |||
dd9a00679d | |||
081502848d | |||
0fa9fa20bc | |||
0a1f25a659 | |||
bc74c44f97 | |||
c50e325f53 | |||
0225e6fabb | |||
3caa46ade8 | |||
998bbe92f7 | |||
009be0ded8 | |||
c9f6207e32 | |||
36adc5e00e | |||
cb24b2aac8 | |||
1e0eb26dce | |||
f8161c8c72 | |||
862e2e9d65 | |||
0e734bd638 | |||
a35054f6ba | |||
e0ace85d6e | |||
7867587884 | |||
0564d06923 | |||
8ace72d134 | |||
491331e9e3 | |||
4a324eafd8 | |||
173cf0238d | |||
fd792e7e1d | |||
d0656358a2 | |||
040fa511f6 | |||
75099f159f | |||
e4a83ad2e2 | |||
760f9d487c | |||
a02e73e2a4 | |||
d4c95ab1a7 | |||
fbebeaf38f | |||
97245c740e | |||
03c4c2056a | |||
cee982754b | |||
a6497b844a | |||
788dcf2c73 | |||
6d9f80805e | |||
7f055450df | |||
9234213c62 | |||
5a40b5a1cf | |||
19e4a6de4d | |||
0daca059c7 | |||
e7278c4cd9 | |||
3e79dbb3f5 | |||
0fd193f8e0 | |||
342c713805 | |||
9b2565e387 | |||
1c5a8cabe9 | |||
0df80c5b2d | |||
613b97c93d | |||
c577f51c19 | |||
335f3f7d37 | |||
b3f0d36ddc | |||
24d121ab59 | |||
ccbf09398e | |||
afbca118b7 | |||
bd29d6feb9 | |||
dbb8617180 |
@ -36,16 +36,22 @@ CLIENT_ID_HEROKU=
|
|||||||
CLIENT_ID_VERCEL=
|
CLIENT_ID_VERCEL=
|
||||||
CLIENT_ID_NETLIFY=
|
CLIENT_ID_NETLIFY=
|
||||||
CLIENT_ID_GITHUB=
|
CLIENT_ID_GITHUB=
|
||||||
|
CLIENT_ID_GITHUB_APP=
|
||||||
|
CLIENT_SLUG_GITHUB_APP=
|
||||||
CLIENT_ID_GITLAB=
|
CLIENT_ID_GITLAB=
|
||||||
CLIENT_ID_BITBUCKET=
|
CLIENT_ID_BITBUCKET=
|
||||||
CLIENT_SECRET_HEROKU=
|
CLIENT_SECRET_HEROKU=
|
||||||
CLIENT_SECRET_VERCEL=
|
CLIENT_SECRET_VERCEL=
|
||||||
CLIENT_SECRET_NETLIFY=
|
CLIENT_SECRET_NETLIFY=
|
||||||
CLIENT_SECRET_GITHUB=
|
CLIENT_SECRET_GITHUB=
|
||||||
|
CLIENT_SECRET_GITHUB_APP=
|
||||||
CLIENT_SECRET_GITLAB=
|
CLIENT_SECRET_GITLAB=
|
||||||
CLIENT_SECRET_BITBUCKET=
|
CLIENT_SECRET_BITBUCKET=
|
||||||
CLIENT_SLUG_VERCEL=
|
CLIENT_SLUG_VERCEL=
|
||||||
|
|
||||||
|
CLIENT_PRIVATE_KEY_GITHUB_APP=
|
||||||
|
CLIENT_APP_ID_GITHUB_APP=
|
||||||
|
|
||||||
# Sentry (optional) for monitoring errors
|
# Sentry (optional) for monitoring errors
|
||||||
SENTRY_DSN=
|
SENTRY_DSN=
|
||||||
|
|
||||||
|
@ -1 +1,2 @@
|
|||||||
DB_CONNECTION_URI=
|
DB_CONNECTION_URI=
|
||||||
|
AUDIT_LOGS_DB_CONNECTION_URI=
|
||||||
|
1
.github/pull_request_template.md
vendored
1
.github/pull_request_template.md
vendored
@ -6,6 +6,7 @@
|
|||||||
|
|
||||||
- [ ] Bug fix
|
- [ ] Bug fix
|
||||||
- [ ] New feature
|
- [ ] New feature
|
||||||
|
- [ ] Improvement
|
||||||
- [ ] Breaking change
|
- [ ] Breaking change
|
||||||
- [ ] Documentation
|
- [ ] Documentation
|
||||||
|
|
||||||
|
91
.github/workflows/build-binaries.yml
vendored
91
.github/workflows/build-binaries.yml
vendored
@ -7,7 +7,6 @@ on:
|
|||||||
description: "Version number"
|
description: "Version number"
|
||||||
required: true
|
required: true
|
||||||
type: string
|
type: string
|
||||||
|
|
||||||
defaults:
|
defaults:
|
||||||
run:
|
run:
|
||||||
working-directory: ./backend
|
working-directory: ./backend
|
||||||
@ -49,9 +48,9 @@ jobs:
|
|||||||
- name: Package into node binary
|
- name: Package into node binary
|
||||||
run: |
|
run: |
|
||||||
if [ "${{ matrix.os }}" != "linux" ]; then
|
if [ "${{ matrix.os }}" != "linux" ]; then
|
||||||
pkg --no-bytecode --public-packages "*" --public --target ${{ matrix.target }}-${{ matrix.arch }} --output ./binary/infisical-core-${{ matrix.os }}-${{ matrix.arch }} .
|
pkg --no-bytecode --public-packages "*" --public --compress GZip --target ${{ matrix.target }}-${{ matrix.arch }} --output ./binary/infisical-core-${{ matrix.os }}-${{ matrix.arch }} .
|
||||||
else
|
else
|
||||||
pkg --no-bytecode --public-packages "*" --public --target ${{ matrix.target }}-${{ matrix.arch }} --output ./binary/infisical-core .
|
pkg --no-bytecode --public-packages "*" --public --compress GZip --target ${{ matrix.target }}-${{ matrix.arch }} --output ./binary/infisical-core .
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Set up .deb package structure (Debian/Ubuntu only)
|
# Set up .deb package structure (Debian/Ubuntu only)
|
||||||
@ -83,6 +82,86 @@ jobs:
|
|||||||
dpkg-deb --build infisical-core
|
dpkg-deb --build infisical-core
|
||||||
mv infisical-core.deb ./binary/infisical-core-${{matrix.arch}}.deb
|
mv infisical-core.deb ./binary/infisical-core-${{matrix.arch}}.deb
|
||||||
|
|
||||||
|
### RPM
|
||||||
|
|
||||||
|
# Set up .rpm package structure
|
||||||
|
- name: Set up .rpm package structure
|
||||||
|
if: matrix.os == 'linux'
|
||||||
|
run: |
|
||||||
|
mkdir -p infisical-core-rpm/usr/local/bin
|
||||||
|
cp ./binary/infisical-core infisical-core-rpm/usr/local/bin/
|
||||||
|
chmod +x infisical-core-rpm/usr/local/bin/infisical-core
|
||||||
|
|
||||||
|
# Install RPM build tools
|
||||||
|
- name: Install RPM build tools
|
||||||
|
if: matrix.os == 'linux'
|
||||||
|
run: sudo apt-get update && sudo apt-get install -y rpm
|
||||||
|
|
||||||
|
# Create .spec file for RPM
|
||||||
|
- name: Create .spec file for RPM
|
||||||
|
if: matrix.os == 'linux'
|
||||||
|
run: |
|
||||||
|
cat <<EOF > infisical-core.spec
|
||||||
|
|
||||||
|
%global _enable_debug_package 0
|
||||||
|
%global debug_package %{nil}
|
||||||
|
%global __os_install_post /usr/lib/rpm/brp-compress %{nil}
|
||||||
|
|
||||||
|
Name: infisical-core
|
||||||
|
Version: ${{ github.event.inputs.version }}
|
||||||
|
Release: 1%{?dist}
|
||||||
|
Summary: Infisical Core standalone executable
|
||||||
|
License: Proprietary
|
||||||
|
URL: https://app.infisical.com
|
||||||
|
|
||||||
|
%description
|
||||||
|
Infisical Core standalone executable (app.infisical.com)
|
||||||
|
|
||||||
|
%install
|
||||||
|
mkdir -p %{buildroot}/usr/local/bin
|
||||||
|
cp %{_sourcedir}/infisical-core %{buildroot}/usr/local/bin/
|
||||||
|
|
||||||
|
%files
|
||||||
|
/usr/local/bin/infisical-core
|
||||||
|
|
||||||
|
%pre
|
||||||
|
|
||||||
|
%post
|
||||||
|
|
||||||
|
%preun
|
||||||
|
|
||||||
|
%postun
|
||||||
|
EOF
|
||||||
|
|
||||||
|
# Build .rpm file
|
||||||
|
- name: Build .rpm package
|
||||||
|
if: matrix.os == 'linux'
|
||||||
|
run: |
|
||||||
|
# Create necessary directories
|
||||||
|
mkdir -p rpmbuild/{BUILD,RPMS,SOURCES,SPECS,SRPMS}
|
||||||
|
|
||||||
|
# Copy the binary directly to SOURCES
|
||||||
|
cp ./binary/infisical-core rpmbuild/SOURCES/
|
||||||
|
|
||||||
|
# Run rpmbuild with verbose output
|
||||||
|
rpmbuild -vv -bb \
|
||||||
|
--define "_topdir $(pwd)/rpmbuild" \
|
||||||
|
--define "_sourcedir $(pwd)/rpmbuild/SOURCES" \
|
||||||
|
--define "_rpmdir $(pwd)/rpmbuild/RPMS" \
|
||||||
|
--target ${{ matrix.arch == 'x64' && 'x86_64' || 'aarch64' }} \
|
||||||
|
infisical-core.spec
|
||||||
|
|
||||||
|
# Try to find the RPM file
|
||||||
|
find rpmbuild -name "*.rpm"
|
||||||
|
|
||||||
|
# Move the RPM file if found
|
||||||
|
if [ -n "$(find rpmbuild -name '*.rpm')" ]; then
|
||||||
|
mv $(find rpmbuild -name '*.rpm') ./binary/infisical-core-${{matrix.arch}}.rpm
|
||||||
|
else
|
||||||
|
echo "RPM file not found!"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
- uses: actions/setup-python@v4
|
- uses: actions/setup-python@v4
|
||||||
with:
|
with:
|
||||||
python-version: "3.x" # Specify the Python version you need
|
python-version: "3.x" # Specify the Python version you need
|
||||||
@ -97,6 +176,12 @@ jobs:
|
|||||||
working-directory: ./backend
|
working-directory: ./backend
|
||||||
run: cloudsmith push deb --republish --no-wait-for-sync --api-key=${{ secrets.CLOUDSMITH_API_KEY }} infisical/infisical-core/any-distro/any-version ./binary/infisical-core-${{ matrix.arch }}.deb
|
run: cloudsmith push deb --republish --no-wait-for-sync --api-key=${{ secrets.CLOUDSMITH_API_KEY }} infisical/infisical-core/any-distro/any-version ./binary/infisical-core-${{ matrix.arch }}.deb
|
||||||
|
|
||||||
|
# Publish .rpm file to Cloudsmith (Red Hat-based systems only)
|
||||||
|
- name: Publish .rpm to Cloudsmith
|
||||||
|
if: matrix.os == 'linux'
|
||||||
|
working-directory: ./backend
|
||||||
|
run: cloudsmith push rpm --republish --no-wait-for-sync --api-key=${{ secrets.CLOUDSMITH_API_KEY }} infisical/infisical-core/any-distro/any-version ./binary/infisical-core-${{ matrix.arch }}.rpm
|
||||||
|
|
||||||
# Publish .exe file to Cloudsmith (Windows only)
|
# Publish .exe file to Cloudsmith (Windows only)
|
||||||
- name: Publish to Cloudsmith (Windows)
|
- name: Publish to Cloudsmith (Windows)
|
||||||
if: matrix.os == 'win'
|
if: matrix.os == 'win'
|
||||||
|
@ -127,6 +127,7 @@ jobs:
|
|||||||
- name: Change directory to backend and install dependencies
|
- name: Change directory to backend and install dependencies
|
||||||
env:
|
env:
|
||||||
DB_CONNECTION_URI: ${{ secrets.DB_CONNECTION_URI }}
|
DB_CONNECTION_URI: ${{ secrets.DB_CONNECTION_URI }}
|
||||||
|
AUDIT_LOGS_DB_CONNECTION_URI: ${{ secrets.AUDIT_LOGS_DB_CONNECTION_URI }}
|
||||||
run: |
|
run: |
|
||||||
cd backend
|
cd backend
|
||||||
npm install
|
npm install
|
||||||
|
@ -95,6 +95,10 @@ RUN mkdir frontend-build
|
|||||||
# Production stage
|
# Production stage
|
||||||
FROM base AS production
|
FROM base AS production
|
||||||
RUN apk add --upgrade --no-cache ca-certificates
|
RUN apk add --upgrade --no-cache ca-certificates
|
||||||
|
RUN apk add --no-cache bash curl && curl -1sLf \
|
||||||
|
'https://dl.cloudsmith.io/public/infisical/infisical-cli/setup.alpine.sh' | bash \
|
||||||
|
&& apk add infisical=0.31.1 && apk add --no-cache git
|
||||||
|
|
||||||
RUN addgroup --system --gid 1001 nodejs \
|
RUN addgroup --system --gid 1001 nodejs \
|
||||||
&& adduser --system --uid 1001 non-root-user
|
&& adduser --system --uid 1001 non-root-user
|
||||||
|
|
||||||
|
10
README.md
10
README.md
@ -73,6 +73,11 @@ We're on a mission to make security tooling more accessible to everyone, not jus
|
|||||||
- **[Infisical PKI Issuer for Kubernetes](https://infisical.com/docs/documentation/platform/pki/pki-issuer)**: Deliver TLS certificates to your Kubernetes workloads with automatic renewal.
|
- **[Infisical PKI Issuer for Kubernetes](https://infisical.com/docs/documentation/platform/pki/pki-issuer)**: Deliver TLS certificates to your Kubernetes workloads with automatic renewal.
|
||||||
- **[Enrollment over Secure Transport](https://infisical.com/docs/documentation/platform/pki/est)**: Enroll and manage certificates via EST protocol.
|
- **[Enrollment over Secure Transport](https://infisical.com/docs/documentation/platform/pki/est)**: Enroll and manage certificates via EST protocol.
|
||||||
|
|
||||||
|
### Key Management (KMS):
|
||||||
|
|
||||||
|
- **[Cryptograhic Keys](https://infisical.com/docs/documentation/platform/kms)**: Centrally manage keys across projects through a user-friendly interface or via the API.
|
||||||
|
- **[Encrypt and Decrypt Data](https://infisical.com/docs/documentation/platform/kms#guide-to-encrypting-data)**: Use symmetric keys to encrypt and decrypt data.
|
||||||
|
|
||||||
### General Platform:
|
### General Platform:
|
||||||
- **Authentication Methods**: Authenticate machine identities with Infisical using a cloud-native or platform agnostic authentication method ([Kubernetes Auth](https://infisical.com/docs/documentation/platform/identities/kubernetes-auth), [GCP Auth](https://infisical.com/docs/documentation/platform/identities/gcp-auth), [Azure Auth](https://infisical.com/docs/documentation/platform/identities/azure-auth), [AWS Auth](https://infisical.com/docs/documentation/platform/identities/aws-auth), [OIDC Auth](https://infisical.com/docs/documentation/platform/identities/oidc-auth/general), [Universal Auth](https://infisical.com/docs/documentation/platform/identities/universal-auth)).
|
- **Authentication Methods**: Authenticate machine identities with Infisical using a cloud-native or platform agnostic authentication method ([Kubernetes Auth](https://infisical.com/docs/documentation/platform/identities/kubernetes-auth), [GCP Auth](https://infisical.com/docs/documentation/platform/identities/gcp-auth), [Azure Auth](https://infisical.com/docs/documentation/platform/identities/azure-auth), [AWS Auth](https://infisical.com/docs/documentation/platform/identities/aws-auth), [OIDC Auth](https://infisical.com/docs/documentation/platform/identities/oidc-auth/general), [Universal Auth](https://infisical.com/docs/documentation/platform/identities/universal-auth)).
|
||||||
- **[Access Controls](https://infisical.com/docs/documentation/platform/access-controls/overview)**: Define advanced authorization controls for users and machine identities with [RBAC](https://infisical.com/docs/documentation/platform/access-controls/role-based-access-controls), [additional privileges](https://infisical.com/docs/documentation/platform/access-controls/additional-privileges), [temporary access](https://infisical.com/docs/documentation/platform/access-controls/temporary-access), [access requests](https://infisical.com/docs/documentation/platform/access-controls/access-requests), [approval workflows](https://infisical.com/docs/documentation/platform/pr-workflows), and more.
|
- **[Access Controls](https://infisical.com/docs/documentation/platform/access-controls/overview)**: Define advanced authorization controls for users and machine identities with [RBAC](https://infisical.com/docs/documentation/platform/access-controls/role-based-access-controls), [additional privileges](https://infisical.com/docs/documentation/platform/access-controls/additional-privileges), [temporary access](https://infisical.com/docs/documentation/platform/access-controls/temporary-access), [access requests](https://infisical.com/docs/documentation/platform/access-controls/access-requests), [approval workflows](https://infisical.com/docs/documentation/platform/pr-workflows), and more.
|
||||||
@ -130,9 +135,7 @@ Lean about Infisical's code scanning feature [here](https://infisical.com/docs/c
|
|||||||
|
|
||||||
This repo available under the [MIT expat license](https://github.com/Infisical/infisical/blob/main/LICENSE), with the exception of the `ee` directory which will contain premium enterprise features requiring a Infisical license.
|
This repo available under the [MIT expat license](https://github.com/Infisical/infisical/blob/main/LICENSE), with the exception of the `ee` directory which will contain premium enterprise features requiring a Infisical license.
|
||||||
|
|
||||||
If you are interested in managed Infisical Cloud of self-hosted Enterprise Offering, take a look at [our website](https://infisical.com/) or [book a meeting with us](https://infisical.cal.com/vlad/infisical-demo):
|
If you are interested in managed Infisical Cloud of self-hosted Enterprise Offering, take a look at [our website](https://infisical.com/) or [book a meeting with us](https://infisical.cal.com/vlad/infisical-demo).
|
||||||
|
|
||||||
<a href="[https://infisical.cal.com/vlad/infisical-demo](https://infisical.cal.com/vlad/infisical-demo)"><img alt="Schedule a meeting" src="https://cal.com/book-with-cal-dark.svg" /></a>
|
|
||||||
|
|
||||||
## Security
|
## Security
|
||||||
|
|
||||||
@ -158,4 +161,3 @@ Not sure where to get started? You can:
|
|||||||
- [Twitter](https://twitter.com/infisical) for fast news
|
- [Twitter](https://twitter.com/infisical) for fast news
|
||||||
- [YouTube](https://www.youtube.com/@infisical_os) for videos on secret management
|
- [YouTube](https://www.youtube.com/@infisical_os) for videos on secret management
|
||||||
- [Blog](https://infisical.com/blog) for secret management insights, articles, tutorials, and updates
|
- [Blog](https://infisical.com/blog) for secret management insights, articles, tutorials, and updates
|
||||||
- [Roadmap](https://www.notion.so/infisical/be2d2585a6694e40889b03aef96ea36b?v=5b19a8127d1a4060b54769567a8785fa) for planned features
|
|
@ -39,8 +39,6 @@ describe("Login V1 Router", async () => {
|
|||||||
});
|
});
|
||||||
expect(res.statusCode).toBe(200);
|
expect(res.statusCode).toBe(200);
|
||||||
const payload = JSON.parse(res.payload);
|
const payload = JSON.parse(res.payload);
|
||||||
expect(payload).toHaveProperty("mfaEnabled");
|
|
||||||
expect(payload).toHaveProperty("token");
|
expect(payload).toHaveProperty("token");
|
||||||
expect(payload.mfaEnabled).toBeFalsy();
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@ -123,7 +123,7 @@ describe("Project Environment Router", async () => {
|
|||||||
id: deletedProjectEnvironment.id,
|
id: deletedProjectEnvironment.id,
|
||||||
name: mockProjectEnv.name,
|
name: mockProjectEnv.name,
|
||||||
slug: mockProjectEnv.slug,
|
slug: mockProjectEnv.slug,
|
||||||
position: 4,
|
position: 5,
|
||||||
createdAt: expect.any(String),
|
createdAt: expect.any(String),
|
||||||
updatedAt: expect.any(String)
|
updatedAt: expect.any(String)
|
||||||
})
|
})
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
import { seedData1 } from "@app/db/seed-data";
|
import { seedData1 } from "@app/db/seed-data";
|
||||||
|
import { ApproverType } from "@app/ee/services/access-approval-policy/access-approval-policy-types";
|
||||||
|
|
||||||
const createPolicy = async (dto: { name: string; secretPath: string; approvers: string[]; approvals: number }) => {
|
const createPolicy = async (dto: { name: string; secretPath: string; approvers: {type: ApproverType.User, id: string}[]; approvals: number }) => {
|
||||||
const res = await testServer.inject({
|
const res = await testServer.inject({
|
||||||
method: "POST",
|
method: "POST",
|
||||||
url: `/api/v1/secret-approvals`,
|
url: `/api/v1/secret-approvals`,
|
||||||
@ -26,7 +27,7 @@ describe("Secret approval policy router", async () => {
|
|||||||
const policy = await createPolicy({
|
const policy = await createPolicy({
|
||||||
secretPath: "/",
|
secretPath: "/",
|
||||||
approvals: 1,
|
approvals: 1,
|
||||||
approvers: [seedData1.id],
|
approvers: [{id:seedData1.id, type: ApproverType.User}],
|
||||||
name: "test-policy"
|
name: "test-policy"
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -510,7 +510,7 @@ describe("Service token fail cases", async () => {
|
|||||||
authorization: `Bearer ${serviceToken}`
|
authorization: `Bearer ${serviceToken}`
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
expect(fetchSecrets.statusCode).toBe(401);
|
expect(fetchSecrets.statusCode).toBe(403);
|
||||||
expect(fetchSecrets.json().error).toBe("PermissionDenied");
|
expect(fetchSecrets.json().error).toBe("PermissionDenied");
|
||||||
await deleteServiceToken();
|
await deleteServiceToken();
|
||||||
});
|
});
|
||||||
@ -532,7 +532,7 @@ describe("Service token fail cases", async () => {
|
|||||||
authorization: `Bearer ${serviceToken}`
|
authorization: `Bearer ${serviceToken}`
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
expect(fetchSecrets.statusCode).toBe(401);
|
expect(fetchSecrets.statusCode).toBe(403);
|
||||||
expect(fetchSecrets.json().error).toBe("PermissionDenied");
|
expect(fetchSecrets.json().error).toBe("PermissionDenied");
|
||||||
await deleteServiceToken();
|
await deleteServiceToken();
|
||||||
});
|
});
|
||||||
@ -557,7 +557,7 @@ describe("Service token fail cases", async () => {
|
|||||||
authorization: `Bearer ${serviceToken}`
|
authorization: `Bearer ${serviceToken}`
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
expect(writeSecrets.statusCode).toBe(401);
|
expect(writeSecrets.statusCode).toBe(403);
|
||||||
expect(writeSecrets.json().error).toBe("PermissionDenied");
|
expect(writeSecrets.json().error).toBe("PermissionDenied");
|
||||||
|
|
||||||
// but read access should still work fine
|
// but read access should still work fine
|
||||||
|
@ -1075,7 +1075,7 @@ describe("Secret V3 Raw Router Without E2EE enabled", async () => {
|
|||||||
},
|
},
|
||||||
body: createSecretReqBody
|
body: createSecretReqBody
|
||||||
});
|
});
|
||||||
expect(createSecRes.statusCode).toBe(400);
|
expect(createSecRes.statusCode).toBe(404);
|
||||||
});
|
});
|
||||||
|
|
||||||
test("Update secret raw", async () => {
|
test("Update secret raw", async () => {
|
||||||
@ -1093,7 +1093,7 @@ describe("Secret V3 Raw Router Without E2EE enabled", async () => {
|
|||||||
},
|
},
|
||||||
body: updateSecretReqBody
|
body: updateSecretReqBody
|
||||||
});
|
});
|
||||||
expect(updateSecRes.statusCode).toBe(400);
|
expect(updateSecRes.statusCode).toBe(404);
|
||||||
});
|
});
|
||||||
|
|
||||||
test("Delete secret raw", async () => {
|
test("Delete secret raw", async () => {
|
||||||
@ -1110,6 +1110,6 @@ describe("Secret V3 Raw Router Without E2EE enabled", async () => {
|
|||||||
},
|
},
|
||||||
body: deletedSecretReqBody
|
body: deletedSecretReqBody
|
||||||
});
|
});
|
||||||
expect(deletedSecRes.statusCode).toBe(400);
|
expect(deletedSecRes.statusCode).toBe(404);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
1054
backend/package-lock.json
generated
1054
backend/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@ -45,13 +45,20 @@
|
|||||||
"test:e2e-coverage": "vitest run --coverage -c vitest.e2e.config.ts",
|
"test:e2e-coverage": "vitest run --coverage -c vitest.e2e.config.ts",
|
||||||
"generate:component": "tsx ./scripts/create-backend-file.ts",
|
"generate:component": "tsx ./scripts/create-backend-file.ts",
|
||||||
"generate:schema": "tsx ./scripts/generate-schema-types.ts",
|
"generate:schema": "tsx ./scripts/generate-schema-types.ts",
|
||||||
|
"auditlog-migration:latest": "knex --knexfile ./src/db/auditlog-knexfile.ts --client pg migrate:latest",
|
||||||
|
"auditlog-migration:up": "knex --knexfile ./src/db/auditlog-knexfile.ts --client pg migrate:up",
|
||||||
|
"auditlog-migration:down": "knex --knexfile ./src/db/auditlog-knexfile.ts --client pg migrate:down",
|
||||||
|
"auditlog-migration:list": "knex --knexfile ./src/db/auditlog-knexfile.ts --client pg migrate:list",
|
||||||
|
"auditlog-migration:status": "knex --knexfile ./src/db/auditlog-knexfile.ts --client pg migrate:status",
|
||||||
|
"auditlog-migration:rollback": "knex --knexfile ./src/db/auditlog-knexfile.ts migrate:rollback",
|
||||||
"migration:new": "tsx ./scripts/create-migration.ts",
|
"migration:new": "tsx ./scripts/create-migration.ts",
|
||||||
"migration:up": "knex --knexfile ./src/db/knexfile.ts --client pg migrate:up",
|
"migration:up": "npm run auditlog-migration:up && knex --knexfile ./src/db/knexfile.ts --client pg migrate:up",
|
||||||
"migration:down": "knex --knexfile ./src/db/knexfile.ts --client pg migrate:down",
|
"migration:down": "npm run auditlog-migration:down && knex --knexfile ./src/db/knexfile.ts --client pg migrate:down",
|
||||||
"migration:list": "knex --knexfile ./src/db/knexfile.ts --client pg migrate:list",
|
"migration:list": "npm run auditlog-migration:list && knex --knexfile ./src/db/knexfile.ts --client pg migrate:list",
|
||||||
"migration:latest": "knex --knexfile ./src/db/knexfile.ts --client pg migrate:latest",
|
"migration:latest": "npm run auditlog-migration:latest && knex --knexfile ./src/db/knexfile.ts --client pg migrate:latest",
|
||||||
"migration:status": "knex --knexfile ./src/db/knexfile.ts --client pg migrate:status",
|
"migration:status": "npm run auditlog-migration:status && knex --knexfile ./src/db/knexfile.ts --client pg migrate:status",
|
||||||
"migration:rollback": "knex --knexfile ./src/db/knexfile.ts migrate:rollback",
|
"migration:rollback": "npm run auditlog-migration:rollback && knex --knexfile ./src/db/knexfile.ts migrate:rollback",
|
||||||
|
"migrate:org": "tsx ./scripts/migrate-organization.ts",
|
||||||
"seed:new": "tsx ./scripts/create-seed-file.ts",
|
"seed:new": "tsx ./scripts/create-seed-file.ts",
|
||||||
"seed": "knex --knexfile ./src/db/knexfile.ts --client pg seed:run",
|
"seed": "knex --knexfile ./src/db/knexfile.ts --client pg seed:run",
|
||||||
"db:reset": "npm run migration:rollback -- --all && npm run migration:latest"
|
"db:reset": "npm run migration:rollback -- --all && npm run migration:latest"
|
||||||
@ -80,6 +87,7 @@
|
|||||||
"@types/prompt-sync": "^4.2.3",
|
"@types/prompt-sync": "^4.2.3",
|
||||||
"@types/resolve": "^1.20.6",
|
"@types/resolve": "^1.20.6",
|
||||||
"@types/safe-regex": "^1.1.6",
|
"@types/safe-regex": "^1.1.6",
|
||||||
|
"@types/sjcl": "^1.0.34",
|
||||||
"@types/uuid": "^9.0.7",
|
"@types/uuid": "^9.0.7",
|
||||||
"@typescript-eslint/eslint-plugin": "^6.20.0",
|
"@typescript-eslint/eslint-plugin": "^6.20.0",
|
||||||
"@typescript-eslint/parser": "^6.20.0",
|
"@typescript-eslint/parser": "^6.20.0",
|
||||||
@ -118,12 +126,14 @@
|
|||||||
"@fastify/etag": "^5.1.0",
|
"@fastify/etag": "^5.1.0",
|
||||||
"@fastify/formbody": "^7.4.0",
|
"@fastify/formbody": "^7.4.0",
|
||||||
"@fastify/helmet": "^11.1.1",
|
"@fastify/helmet": "^11.1.1",
|
||||||
|
"@fastify/multipart": "8.3.0",
|
||||||
"@fastify/passport": "^2.4.0",
|
"@fastify/passport": "^2.4.0",
|
||||||
"@fastify/rate-limit": "^9.0.0",
|
"@fastify/rate-limit": "^9.0.0",
|
||||||
"@fastify/session": "^10.7.0",
|
"@fastify/session": "^10.7.0",
|
||||||
"@fastify/swagger": "^8.14.0",
|
"@fastify/swagger": "^8.14.0",
|
||||||
"@fastify/swagger-ui": "^2.1.0",
|
"@fastify/swagger-ui": "^2.1.0",
|
||||||
"@node-saml/passport-saml": "^4.0.4",
|
"@node-saml/passport-saml": "^4.0.4",
|
||||||
|
"@octokit/auth-app": "^7.1.1",
|
||||||
"@octokit/plugin-retry": "^5.0.5",
|
"@octokit/plugin-retry": "^5.0.5",
|
||||||
"@octokit/rest": "^20.0.2",
|
"@octokit/rest": "^20.0.2",
|
||||||
"@octokit/webhooks-types": "^7.3.1",
|
"@octokit/webhooks-types": "^7.3.1",
|
||||||
@ -158,6 +168,7 @@
|
|||||||
"jwks-rsa": "^3.1.0",
|
"jwks-rsa": "^3.1.0",
|
||||||
"knex": "^3.0.1",
|
"knex": "^3.0.1",
|
||||||
"ldapjs": "^3.0.7",
|
"ldapjs": "^3.0.7",
|
||||||
|
"ldif": "0.5.1",
|
||||||
"libsodium-wrappers": "^0.7.13",
|
"libsodium-wrappers": "^0.7.13",
|
||||||
"lodash.isequal": "^4.5.0",
|
"lodash.isequal": "^4.5.0",
|
||||||
"mongodb": "^6.8.1",
|
"mongodb": "^6.8.1",
|
||||||
@ -178,10 +189,11 @@
|
|||||||
"pino": "^8.16.2",
|
"pino": "^8.16.2",
|
||||||
"pkijs": "^3.2.4",
|
"pkijs": "^3.2.4",
|
||||||
"posthog-node": "^3.6.2",
|
"posthog-node": "^3.6.2",
|
||||||
"probot": "^13.0.0",
|
"probot": "^13.3.8",
|
||||||
"safe-regex": "^2.1.1",
|
"safe-regex": "^2.1.1",
|
||||||
"scim-patch": "^0.8.3",
|
"scim-patch": "^0.8.3",
|
||||||
"scim2-parse-filter": "^0.2.10",
|
"scim2-parse-filter": "^0.2.10",
|
||||||
|
"sjcl": "^1.0.8",
|
||||||
"smee-client": "^2.0.0",
|
"smee-client": "^2.0.0",
|
||||||
"tedious": "^18.2.1",
|
"tedious": "^18.2.1",
|
||||||
"tweetnacl": "^1.0.3",
|
"tweetnacl": "^1.0.3",
|
||||||
|
@ -90,7 +90,12 @@ const main = async () => {
|
|||||||
.whereRaw("table_schema = current_schema()")
|
.whereRaw("table_schema = current_schema()")
|
||||||
.select<{ tableName: string }[]>("table_name as tableName")
|
.select<{ tableName: string }[]>("table_name as tableName")
|
||||||
.orderBy("table_name")
|
.orderBy("table_name")
|
||||||
).filter((el) => !el.tableName.includes("_migrations"));
|
).filter(
|
||||||
|
(el) =>
|
||||||
|
!el.tableName.includes("_migrations") &&
|
||||||
|
!el.tableName.includes("audit_logs_") &&
|
||||||
|
el.tableName !== "intermediate_audit_logs"
|
||||||
|
);
|
||||||
|
|
||||||
for (let i = 0; i < tables.length; i += 1) {
|
for (let i = 0; i < tables.length; i += 1) {
|
||||||
const { tableName } = tables[i];
|
const { tableName } = tables[i];
|
||||||
|
84
backend/scripts/migrate-organization.ts
Normal file
84
backend/scripts/migrate-organization.ts
Normal file
@ -0,0 +1,84 @@
|
|||||||
|
/* eslint-disable */
|
||||||
|
import promptSync from "prompt-sync";
|
||||||
|
import { execSync } from "child_process";
|
||||||
|
import path from "path";
|
||||||
|
import { existsSync } from "fs";
|
||||||
|
|
||||||
|
const prompt = promptSync({
|
||||||
|
sigint: true
|
||||||
|
});
|
||||||
|
|
||||||
|
const exportDb = () => {
|
||||||
|
const exportHost = prompt("Enter your Postgres Host to migrate from: ");
|
||||||
|
const exportPort = prompt("Enter your Postgres Port to migrate from [Default = 5432]: ") ?? "5432";
|
||||||
|
const exportUser = prompt("Enter your Postgres User to migrate from: [Default = infisical]: ") ?? "infisical";
|
||||||
|
const exportPassword = prompt("Enter your Postgres Password to migrate from: ");
|
||||||
|
const exportDatabase = prompt("Enter your Postgres Database to migrate from [Default = infisical]: ") ?? "infisical";
|
||||||
|
|
||||||
|
// we do not include the audit_log and secret_sharing entries
|
||||||
|
execSync(
|
||||||
|
`PGDATABASE="${exportDatabase}" PGPASSWORD="${exportPassword}" PGHOST="${exportHost}" PGPORT=${exportPort} PGUSER=${exportUser} pg_dump infisical --exclude-table-data="secret_sharing" --exclude-table-data="audit_log*" > ${path.join(
|
||||||
|
__dirname,
|
||||||
|
"../src/db/dump.sql"
|
||||||
|
)}`,
|
||||||
|
{ stdio: "inherit" }
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const importDbForOrg = () => {
|
||||||
|
const importHost = prompt("Enter your Postgres Host to migrate to: ");
|
||||||
|
const importPort = prompt("Enter your Postgres Port to migrate to [Default = 5432]: ") ?? "5432";
|
||||||
|
const importUser = prompt("Enter your Postgres User to migrate to: [Default = infisical]: ") ?? "infisical";
|
||||||
|
const importPassword = prompt("Enter your Postgres Password to migrate to: ");
|
||||||
|
const importDatabase = prompt("Enter your Postgres Database to migrate to [Default = infisical]: ") ?? "infisical";
|
||||||
|
const orgId = prompt("Enter the organization ID to migrate: ");
|
||||||
|
|
||||||
|
if (!existsSync(path.join(__dirname, "../src/db/dump.sql"))) {
|
||||||
|
console.log("File not found, please export the database first.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
execSync(
|
||||||
|
`PGDATABASE="${importDatabase}" PGPASSWORD="${importPassword}" PGHOST="${importHost}" PGPORT=${importPort} PGUSER=${importUser} psql -f ${path.join(
|
||||||
|
__dirname,
|
||||||
|
"../src/db/dump.sql"
|
||||||
|
)}`
|
||||||
|
);
|
||||||
|
|
||||||
|
execSync(
|
||||||
|
`PGDATABASE="${importDatabase}" PGPASSWORD="${importPassword}" PGHOST="${importHost}" PGPORT=${importPort} PGUSER=${importUser} psql -c "DELETE FROM public.organizations WHERE id != '${orgId}'"`
|
||||||
|
);
|
||||||
|
|
||||||
|
// delete global/instance-level resources not relevant to the organization to migrate
|
||||||
|
// users
|
||||||
|
execSync(
|
||||||
|
`PGDATABASE="${importDatabase}" PGPASSWORD="${importPassword}" PGHOST="${importHost}" PGPORT=${importPort} PGUSER=${importUser} psql -c 'DELETE FROM users WHERE users.id NOT IN (SELECT org_memberships."userId" FROM org_memberships)'`
|
||||||
|
);
|
||||||
|
|
||||||
|
// identities
|
||||||
|
execSync(
|
||||||
|
`PGDATABASE="${importDatabase}" PGPASSWORD="${importPassword}" PGHOST="${importHost}" PGPORT=${importPort} PGUSER=${importUser} psql -c 'DELETE FROM identities WHERE id NOT IN (SELECT "identityId" FROM identity_org_memberships)'`
|
||||||
|
);
|
||||||
|
|
||||||
|
// reset slack configuration in superAdmin
|
||||||
|
execSync(
|
||||||
|
`PGDATABASE="${importDatabase}" PGPASSWORD="${importPassword}" PGHOST="${importHost}" PGPORT=${importPort} PGUSER=${importUser} psql -c 'UPDATE super_admin SET "encryptedSlackClientId" = null, "encryptedSlackClientSecret" = null'`
|
||||||
|
);
|
||||||
|
|
||||||
|
console.log("Organization migrated successfully.");
|
||||||
|
};
|
||||||
|
|
||||||
|
const main = () => {
|
||||||
|
const action = prompt(
|
||||||
|
"Enter the action to perform\n 1. Export from existing instance.\n 2. Import org to instance.\n \n Action: "
|
||||||
|
);
|
||||||
|
if (action === "1") {
|
||||||
|
exportDb();
|
||||||
|
} else if (action === "2") {
|
||||||
|
importDbForOrg();
|
||||||
|
} else {
|
||||||
|
console.log("Invalid action");
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
main();
|
6
backend/src/@types/fastify.d.ts
vendored
6
backend/src/@types/fastify.d.ts
vendored
@ -38,6 +38,9 @@ import { TAuthTokenServiceFactory } from "@app/services/auth-token/auth-token-se
|
|||||||
import { TCertificateServiceFactory } from "@app/services/certificate/certificate-service";
|
import { TCertificateServiceFactory } from "@app/services/certificate/certificate-service";
|
||||||
import { TCertificateAuthorityServiceFactory } from "@app/services/certificate-authority/certificate-authority-service";
|
import { TCertificateAuthorityServiceFactory } from "@app/services/certificate-authority/certificate-authority-service";
|
||||||
import { TCertificateTemplateServiceFactory } from "@app/services/certificate-template/certificate-template-service";
|
import { TCertificateTemplateServiceFactory } from "@app/services/certificate-template/certificate-template-service";
|
||||||
|
import { TCmekServiceFactory } from "@app/services/cmek/cmek-service";
|
||||||
|
import { TExternalGroupOrgRoleMappingServiceFactory } from "@app/services/external-group-org-role-mapping/external-group-org-role-mapping-service";
|
||||||
|
import { TExternalMigrationServiceFactory } from "@app/services/external-migration/external-migration-service";
|
||||||
import { TGroupProjectServiceFactory } from "@app/services/group-project/group-project-service";
|
import { TGroupProjectServiceFactory } from "@app/services/group-project/group-project-service";
|
||||||
import { TIdentityServiceFactory } from "@app/services/identity/identity-service";
|
import { TIdentityServiceFactory } from "@app/services/identity/identity-service";
|
||||||
import { TIdentityAccessTokenServiceFactory } from "@app/services/identity-access-token/identity-access-token-service";
|
import { TIdentityAccessTokenServiceFactory } from "@app/services/identity-access-token/identity-access-token-service";
|
||||||
@ -181,6 +184,9 @@ declare module "fastify" {
|
|||||||
orgAdmin: TOrgAdminServiceFactory;
|
orgAdmin: TOrgAdminServiceFactory;
|
||||||
slack: TSlackServiceFactory;
|
slack: TSlackServiceFactory;
|
||||||
workflowIntegration: TWorkflowIntegrationServiceFactory;
|
workflowIntegration: TWorkflowIntegrationServiceFactory;
|
||||||
|
cmek: TCmekServiceFactory;
|
||||||
|
migration: TExternalMigrationServiceFactory;
|
||||||
|
externalGroupOrgRoleMapping: TExternalGroupOrgRoleMappingServiceFactory;
|
||||||
};
|
};
|
||||||
// this is exclusive use for middlewares in which we need to inject data
|
// this is exclusive use for middlewares in which we need to inject data
|
||||||
// everywhere else access using service layer
|
// everywhere else access using service layer
|
||||||
|
18
backend/src/@types/knex.d.ts
vendored
18
backend/src/@types/knex.d.ts
vendored
@ -101,6 +101,9 @@ import {
|
|||||||
TIdentityKubernetesAuths,
|
TIdentityKubernetesAuths,
|
||||||
TIdentityKubernetesAuthsInsert,
|
TIdentityKubernetesAuthsInsert,
|
||||||
TIdentityKubernetesAuthsUpdate,
|
TIdentityKubernetesAuthsUpdate,
|
||||||
|
TIdentityMetadata,
|
||||||
|
TIdentityMetadataInsert,
|
||||||
|
TIdentityMetadataUpdate,
|
||||||
TIdentityOidcAuths,
|
TIdentityOidcAuths,
|
||||||
TIdentityOidcAuthsInsert,
|
TIdentityOidcAuthsInsert,
|
||||||
TIdentityOidcAuthsUpdate,
|
TIdentityOidcAuthsUpdate,
|
||||||
@ -333,6 +336,11 @@ import {
|
|||||||
TWorkflowIntegrationsInsert,
|
TWorkflowIntegrationsInsert,
|
||||||
TWorkflowIntegrationsUpdate
|
TWorkflowIntegrationsUpdate
|
||||||
} from "@app/db/schemas";
|
} from "@app/db/schemas";
|
||||||
|
import {
|
||||||
|
TExternalGroupOrgRoleMappings,
|
||||||
|
TExternalGroupOrgRoleMappingsInsert,
|
||||||
|
TExternalGroupOrgRoleMappingsUpdate
|
||||||
|
} from "@app/db/schemas/external-group-org-role-mappings";
|
||||||
import {
|
import {
|
||||||
TSecretV2TagJunction,
|
TSecretV2TagJunction,
|
||||||
TSecretV2TagJunctionInsert,
|
TSecretV2TagJunctionInsert,
|
||||||
@ -546,6 +554,11 @@ declare module "knex/types/tables" {
|
|||||||
TIdentityUniversalAuthsInsert,
|
TIdentityUniversalAuthsInsert,
|
||||||
TIdentityUniversalAuthsUpdate
|
TIdentityUniversalAuthsUpdate
|
||||||
>;
|
>;
|
||||||
|
[TableName.IdentityMetadata]: KnexOriginal.CompositeTableType<
|
||||||
|
TIdentityMetadata,
|
||||||
|
TIdentityMetadataInsert,
|
||||||
|
TIdentityMetadataUpdate
|
||||||
|
>;
|
||||||
[TableName.IdentityKubernetesAuth]: KnexOriginal.CompositeTableType<
|
[TableName.IdentityKubernetesAuth]: KnexOriginal.CompositeTableType<
|
||||||
TIdentityKubernetesAuths,
|
TIdentityKubernetesAuths,
|
||||||
TIdentityKubernetesAuthsInsert,
|
TIdentityKubernetesAuthsInsert,
|
||||||
@ -800,5 +813,10 @@ declare module "knex/types/tables" {
|
|||||||
TWorkflowIntegrationsInsert,
|
TWorkflowIntegrationsInsert,
|
||||||
TWorkflowIntegrationsUpdate
|
TWorkflowIntegrationsUpdate
|
||||||
>;
|
>;
|
||||||
|
[TableName.ExternalGroupOrgRoleMapping]: KnexOriginal.CompositeTableType<
|
||||||
|
TExternalGroupOrgRoleMappings,
|
||||||
|
TExternalGroupOrgRoleMappingsInsert,
|
||||||
|
TExternalGroupOrgRoleMappingsUpdate
|
||||||
|
>;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
4
backend/src/@types/ldif.d.ts
vendored
Normal file
4
backend/src/@types/ldif.d.ts
vendored
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
declare module "ldif" {
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Untyped, the function returns `any`.
|
||||||
|
function parse(input: string, ...args: any[]): any;
|
||||||
|
}
|
75
backend/src/db/auditlog-knexfile.ts
Normal file
75
backend/src/db/auditlog-knexfile.ts
Normal file
@ -0,0 +1,75 @@
|
|||||||
|
// eslint-disable-next-line
|
||||||
|
import "ts-node/register";
|
||||||
|
|
||||||
|
import dotenv from "dotenv";
|
||||||
|
import type { Knex } from "knex";
|
||||||
|
import path from "path";
|
||||||
|
|
||||||
|
// Update with your config settings. .
|
||||||
|
dotenv.config({
|
||||||
|
path: path.join(__dirname, "../../../.env.migration")
|
||||||
|
});
|
||||||
|
dotenv.config({
|
||||||
|
path: path.join(__dirname, "../../../.env")
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!process.env.AUDIT_LOGS_DB_CONNECTION_URI && !process.env.AUDIT_LOGS_DB_HOST) {
|
||||||
|
console.info("Dedicated audit log database not found. No further migrations necessary");
|
||||||
|
process.exit(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
console.info("Executing migration on audit log database...");
|
||||||
|
|
||||||
|
export default {
|
||||||
|
development: {
|
||||||
|
client: "postgres",
|
||||||
|
connection: {
|
||||||
|
connectionString: process.env.AUDIT_LOGS_DB_CONNECTION_URI,
|
||||||
|
host: process.env.AUDIT_LOGS_DB_HOST,
|
||||||
|
port: process.env.AUDIT_LOGS_DB_PORT,
|
||||||
|
user: process.env.AUDIT_LOGS_DB_USER,
|
||||||
|
database: process.env.AUDIT_LOGS_DB_NAME,
|
||||||
|
password: process.env.AUDIT_LOGS_DB_PASSWORD,
|
||||||
|
ssl: process.env.AUDIT_LOGS_DB_ROOT_CERT
|
||||||
|
? {
|
||||||
|
rejectUnauthorized: true,
|
||||||
|
ca: Buffer.from(process.env.AUDIT_LOGS_DB_ROOT_CERT, "base64").toString("ascii")
|
||||||
|
}
|
||||||
|
: false
|
||||||
|
},
|
||||||
|
pool: {
|
||||||
|
min: 2,
|
||||||
|
max: 10
|
||||||
|
},
|
||||||
|
seeds: {
|
||||||
|
directory: "./seeds"
|
||||||
|
},
|
||||||
|
migrations: {
|
||||||
|
tableName: "infisical_migrations"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
production: {
|
||||||
|
client: "postgres",
|
||||||
|
connection: {
|
||||||
|
connectionString: process.env.AUDIT_LOGS_DB_CONNECTION_URI,
|
||||||
|
host: process.env.AUDIT_LOGS_DB_HOST,
|
||||||
|
port: process.env.AUDIT_LOGS_DB_PORT,
|
||||||
|
user: process.env.AUDIT_LOGS_DB_USER,
|
||||||
|
database: process.env.AUDIT_LOGS_DB_NAME,
|
||||||
|
password: process.env.AUDIT_LOGS_DB_PASSWORD,
|
||||||
|
ssl: process.env.AUDIT_LOGS_DB_ROOT_CERT
|
||||||
|
? {
|
||||||
|
rejectUnauthorized: true,
|
||||||
|
ca: Buffer.from(process.env.AUDIT_LOGS_DB_ROOT_CERT, "base64").toString("ascii")
|
||||||
|
}
|
||||||
|
: false
|
||||||
|
},
|
||||||
|
pool: {
|
||||||
|
min: 2,
|
||||||
|
max: 10
|
||||||
|
},
|
||||||
|
migrations: {
|
||||||
|
tableName: "infisical_migrations"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} as Knex.Config;
|
@ -1,2 +1,2 @@
|
|||||||
export type { TDbClient } from "./instance";
|
export type { TDbClient } from "./instance";
|
||||||
export { initDbConnection } from "./instance";
|
export { initAuditLogDbConnection, initDbConnection } from "./instance";
|
||||||
|
@ -70,3 +70,45 @@ export const initDbConnection = ({
|
|||||||
|
|
||||||
return db;
|
return db;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const initAuditLogDbConnection = ({
|
||||||
|
dbConnectionUri,
|
||||||
|
dbRootCert
|
||||||
|
}: {
|
||||||
|
dbConnectionUri: string;
|
||||||
|
dbRootCert?: string;
|
||||||
|
}) => {
|
||||||
|
// akhilmhdh: the default Knex is knex.Knex<any, any[]>. but when assigned with knex({<config>}) the value is knex.Knex<any, unknown[]>
|
||||||
|
// this was causing issue with files like `snapshot-dal` `findRecursivelySnapshots` this i am explicitly putting the any and unknown[]
|
||||||
|
// eslint-disable-next-line
|
||||||
|
const db: Knex<any, unknown[]> = knex({
|
||||||
|
client: "pg",
|
||||||
|
connection: {
|
||||||
|
connectionString: dbConnectionUri,
|
||||||
|
host: process.env.AUDIT_LOGS_DB_HOST,
|
||||||
|
// @ts-expect-error I have no clue why only for the port there is a type error
|
||||||
|
// eslint-disable-next-line
|
||||||
|
port: process.env.AUDIT_LOGS_DB_PORT,
|
||||||
|
user: process.env.AUDIT_LOGS_DB_USER,
|
||||||
|
database: process.env.AUDIT_LOGS_DB_NAME,
|
||||||
|
password: process.env.AUDIT_LOGS_DB_PASSWORD,
|
||||||
|
ssl: dbRootCert
|
||||||
|
? {
|
||||||
|
rejectUnauthorized: true,
|
||||||
|
ca: Buffer.from(dbRootCert, "base64").toString("ascii")
|
||||||
|
}
|
||||||
|
: false
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// we add these overrides so that auditLogDb and the primary DB are interchangeable
|
||||||
|
db.primaryNode = () => {
|
||||||
|
return db;
|
||||||
|
};
|
||||||
|
|
||||||
|
db.replicaNode = () => {
|
||||||
|
return db;
|
||||||
|
};
|
||||||
|
|
||||||
|
return db;
|
||||||
|
};
|
||||||
|
161
backend/src/db/manual-migrations/partition-audit-logs.ts
Normal file
161
backend/src/db/manual-migrations/partition-audit-logs.ts
Normal file
@ -0,0 +1,161 @@
|
|||||||
|
import kx, { Knex } from "knex";
|
||||||
|
|
||||||
|
import { TableName } from "../schemas";
|
||||||
|
|
||||||
|
const INTERMEDIATE_AUDIT_LOG_TABLE = "intermediate_audit_logs";
|
||||||
|
|
||||||
|
const formatPartitionDate = (date: Date) => {
|
||||||
|
const year = date.getFullYear();
|
||||||
|
const month = String(date.getMonth() + 1).padStart(2, "0");
|
||||||
|
const day = String(date.getDate()).padStart(2, "0");
|
||||||
|
|
||||||
|
return `${year}-${month}-${day}`;
|
||||||
|
};
|
||||||
|
|
||||||
|
const createAuditLogPartition = async (knex: Knex, startDate: Date, endDate: Date) => {
|
||||||
|
const startDateStr = formatPartitionDate(startDate);
|
||||||
|
const endDateStr = formatPartitionDate(endDate);
|
||||||
|
|
||||||
|
const partitionName = `${TableName.AuditLog}_${startDateStr.replace(/-/g, "")}_${endDateStr.replace(/-/g, "")}`;
|
||||||
|
|
||||||
|
await knex.schema.raw(
|
||||||
|
`CREATE TABLE ${partitionName} PARTITION OF ${TableName.AuditLog} FOR VALUES FROM ('${startDateStr}') TO ('${endDateStr}')`
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const up = async (knex: Knex): Promise<void> => {
|
||||||
|
console.info("Dropping primary key of audit log table...");
|
||||||
|
await knex.schema.alterTable(TableName.AuditLog, (t) => {
|
||||||
|
// remove existing keys
|
||||||
|
t.dropPrimary();
|
||||||
|
});
|
||||||
|
|
||||||
|
// Get all indices of the audit log table and drop them
|
||||||
|
const indexNames: { rows: { indexname: string }[] } = await knex.raw(
|
||||||
|
`
|
||||||
|
SELECT indexname
|
||||||
|
FROM pg_indexes
|
||||||
|
WHERE tablename = '${TableName.AuditLog}'
|
||||||
|
`
|
||||||
|
);
|
||||||
|
|
||||||
|
console.log(
|
||||||
|
"Deleting existing audit log indices:",
|
||||||
|
indexNames.rows.map((e) => e.indexname)
|
||||||
|
);
|
||||||
|
|
||||||
|
for await (const row of indexNames.rows) {
|
||||||
|
await knex.raw(`DROP INDEX IF EXISTS ${row.indexname}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// renaming audit log to intermediate table
|
||||||
|
console.log("Renaming audit log table to the intermediate name");
|
||||||
|
await knex.schema.renameTable(TableName.AuditLog, INTERMEDIATE_AUDIT_LOG_TABLE);
|
||||||
|
|
||||||
|
if (!(await knex.schema.hasTable(TableName.AuditLog))) {
|
||||||
|
const createTableSql = knex.schema
|
||||||
|
.createTable(TableName.AuditLog, (t) => {
|
||||||
|
t.uuid("id").defaultTo(knex.fn.uuid());
|
||||||
|
t.string("actor").notNullable();
|
||||||
|
t.jsonb("actorMetadata").notNullable();
|
||||||
|
t.string("ipAddress");
|
||||||
|
t.string("eventType").notNullable();
|
||||||
|
t.jsonb("eventMetadata");
|
||||||
|
t.string("userAgent");
|
||||||
|
t.string("userAgentType");
|
||||||
|
t.datetime("expiresAt");
|
||||||
|
t.timestamps(true, true, true);
|
||||||
|
t.uuid("orgId");
|
||||||
|
t.string("projectId");
|
||||||
|
t.string("projectName");
|
||||||
|
t.primary(["id", "createdAt"]);
|
||||||
|
})
|
||||||
|
.toString();
|
||||||
|
|
||||||
|
console.info("Creating partition table...");
|
||||||
|
await knex.schema.raw(`
|
||||||
|
${createTableSql} PARTITION BY RANGE ("createdAt");
|
||||||
|
`);
|
||||||
|
|
||||||
|
console.log("Adding indices...");
|
||||||
|
await knex.schema.alterTable(TableName.AuditLog, (t) => {
|
||||||
|
t.index(["projectId", "createdAt"]);
|
||||||
|
t.index(["orgId", "createdAt"]);
|
||||||
|
t.index("expiresAt");
|
||||||
|
t.index("orgId");
|
||||||
|
t.index("projectId");
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log("Adding GIN indices...");
|
||||||
|
|
||||||
|
await knex.raw(
|
||||||
|
`CREATE INDEX IF NOT EXISTS "audit_logs_actorMetadata_idx" ON ${TableName.AuditLog} USING gin("actorMetadata" jsonb_path_ops)`
|
||||||
|
);
|
||||||
|
console.log("GIN index for actorMetadata done");
|
||||||
|
|
||||||
|
await knex.raw(
|
||||||
|
`CREATE INDEX IF NOT EXISTS "audit_logs_eventMetadata_idx" ON ${TableName.AuditLog} USING gin("eventMetadata" jsonb_path_ops)`
|
||||||
|
);
|
||||||
|
console.log("GIN index for eventMetadata done");
|
||||||
|
|
||||||
|
// create default partition
|
||||||
|
console.log("Creating default partition...");
|
||||||
|
await knex.schema.raw(`CREATE TABLE ${TableName.AuditLog}_default PARTITION OF ${TableName.AuditLog} DEFAULT`);
|
||||||
|
|
||||||
|
const nextDate = new Date();
|
||||||
|
nextDate.setDate(nextDate.getDate() + 1);
|
||||||
|
const nextDateStr = formatPartitionDate(nextDate);
|
||||||
|
|
||||||
|
console.log("Attaching existing audit log table as a partition...");
|
||||||
|
await knex.schema.raw(`
|
||||||
|
ALTER TABLE ${INTERMEDIATE_AUDIT_LOG_TABLE} ADD CONSTRAINT audit_log_old
|
||||||
|
CHECK ( "createdAt" < DATE '${nextDateStr}' );
|
||||||
|
|
||||||
|
ALTER TABLE ${TableName.AuditLog} ATTACH PARTITION ${INTERMEDIATE_AUDIT_LOG_TABLE}
|
||||||
|
FOR VALUES FROM (MINVALUE) TO ('${nextDateStr}' );
|
||||||
|
`);
|
||||||
|
|
||||||
|
// create partition from now until end of month
|
||||||
|
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;
|
||||||
|
const partitionPromises: Promise<void>[] = [];
|
||||||
|
for (let x = 1; x <= partitionMonths; x += 1) {
|
||||||
|
partitionPromises.push(
|
||||||
|
createAuditLogPartition(
|
||||||
|
knex,
|
||||||
|
new Date(nextDate.getFullYear(), nextDate.getMonth() + x, 1),
|
||||||
|
new Date(nextDate.getFullYear(), nextDate.getMonth() + (x + 1), 1)
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
await Promise.all(partitionPromises);
|
||||||
|
console.log("Partition migration complete");
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export const executeMigration = async (url: string) => {
|
||||||
|
console.log("Executing migration...");
|
||||||
|
const knex = kx({
|
||||||
|
client: "pg",
|
||||||
|
connection: url
|
||||||
|
});
|
||||||
|
|
||||||
|
await knex.transaction(async (tx) => {
|
||||||
|
await up(tx);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const dbUrl = process.env.AUDIT_LOGS_DB_CONNECTION_URI;
|
||||||
|
if (!dbUrl) {
|
||||||
|
console.error("Please provide a DB connection URL to the AUDIT_LOGS_DB_CONNECTION_URI env");
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
void executeMigration(dbUrl).then(() => {
|
||||||
|
console.log("Migration: partition-audit-logs DONE");
|
||||||
|
process.exit(0);
|
||||||
|
});
|
@ -0,0 +1,76 @@
|
|||||||
|
import { Knex } from "knex";
|
||||||
|
|
||||||
|
import { TableName } from "../schemas";
|
||||||
|
|
||||||
|
export async function up(knex: Knex): Promise<void> {
|
||||||
|
const hasAccessApproverGroupId = await knex.schema.hasColumn(
|
||||||
|
TableName.AccessApprovalPolicyApprover,
|
||||||
|
"approverGroupId"
|
||||||
|
);
|
||||||
|
const hasAccessApproverUserId = await knex.schema.hasColumn(TableName.AccessApprovalPolicyApprover, "approverUserId");
|
||||||
|
const hasSecretApproverGroupId = await knex.schema.hasColumn(
|
||||||
|
TableName.SecretApprovalPolicyApprover,
|
||||||
|
"approverGroupId"
|
||||||
|
);
|
||||||
|
const hasSecretApproverUserId = await knex.schema.hasColumn(TableName.SecretApprovalPolicyApprover, "approverUserId");
|
||||||
|
if (await knex.schema.hasTable(TableName.AccessApprovalPolicyApprover)) {
|
||||||
|
await knex.schema.alterTable(TableName.AccessApprovalPolicyApprover, (table) => {
|
||||||
|
// add column approverGroupId to AccessApprovalPolicyApprover
|
||||||
|
if (!hasAccessApproverGroupId) {
|
||||||
|
table.uuid("approverGroupId").nullable().references("id").inTable(TableName.Groups).onDelete("CASCADE");
|
||||||
|
}
|
||||||
|
|
||||||
|
// make approverUserId nullable
|
||||||
|
if (hasAccessApproverUserId) {
|
||||||
|
table.uuid("approverUserId").nullable().alter();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
await knex.schema.alterTable(TableName.SecretApprovalPolicyApprover, (table) => {
|
||||||
|
// add column approverGroupId to SecretApprovalPolicyApprover
|
||||||
|
if (!hasSecretApproverGroupId) {
|
||||||
|
table.uuid("approverGroupId").nullable().references("id").inTable(TableName.Groups).onDelete("CASCADE");
|
||||||
|
}
|
||||||
|
|
||||||
|
// make approverUserId nullable
|
||||||
|
if (hasSecretApproverUserId) {
|
||||||
|
table.uuid("approverUserId").nullable().alter();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function down(knex: Knex): Promise<void> {
|
||||||
|
const hasAccessApproverGroupId = await knex.schema.hasColumn(
|
||||||
|
TableName.AccessApprovalPolicyApprover,
|
||||||
|
"approverGroupId"
|
||||||
|
);
|
||||||
|
const hasAccessApproverUserId = await knex.schema.hasColumn(TableName.AccessApprovalPolicyApprover, "approverUserId");
|
||||||
|
const hasSecretApproverGroupId = await knex.schema.hasColumn(
|
||||||
|
TableName.SecretApprovalPolicyApprover,
|
||||||
|
"approverGroupId"
|
||||||
|
);
|
||||||
|
const hasSecretApproverUserId = await knex.schema.hasColumn(TableName.SecretApprovalPolicyApprover, "approverUserId");
|
||||||
|
|
||||||
|
if (await knex.schema.hasTable(TableName.AccessApprovalPolicyApprover)) {
|
||||||
|
await knex.schema.alterTable(TableName.AccessApprovalPolicyApprover, (table) => {
|
||||||
|
if (hasAccessApproverGroupId) {
|
||||||
|
table.dropColumn("approverGroupId");
|
||||||
|
}
|
||||||
|
// make approverUserId not nullable
|
||||||
|
if (hasAccessApproverUserId) {
|
||||||
|
table.uuid("approverUserId").notNullable().alter();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// remove
|
||||||
|
await knex.schema.alterTable(TableName.SecretApprovalPolicyApprover, (table) => {
|
||||||
|
if (hasSecretApproverGroupId) {
|
||||||
|
table.dropColumn("approverGroupId");
|
||||||
|
}
|
||||||
|
// make approverUserId not nullable
|
||||||
|
if (hasSecretApproverUserId) {
|
||||||
|
table.uuid("approverUserId").notNullable().alter();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,24 @@
|
|||||||
|
import { Knex } from "knex";
|
||||||
|
|
||||||
|
import { TableName } from "../schemas";
|
||||||
|
|
||||||
|
export async function up(knex: Knex): Promise<void> {
|
||||||
|
if (!(await knex.schema.hasTable(TableName.IdentityMetadata))) {
|
||||||
|
await knex.schema.createTable(TableName.IdentityMetadata, (tb) => {
|
||||||
|
tb.uuid("id", { primaryKey: true }).defaultTo(knex.fn.uuid());
|
||||||
|
tb.string("key").notNullable();
|
||||||
|
tb.string("value").notNullable();
|
||||||
|
tb.uuid("orgId").notNullable();
|
||||||
|
tb.foreign("orgId").references("id").inTable(TableName.Organization).onDelete("CASCADE");
|
||||||
|
tb.uuid("userId");
|
||||||
|
tb.foreign("userId").references("id").inTable(TableName.Users).onDelete("CASCADE");
|
||||||
|
tb.uuid("identityId");
|
||||||
|
tb.foreign("identityId").references("id").inTable(TableName.Identity).onDelete("CASCADE");
|
||||||
|
tb.timestamps(true, true, true);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function down(knex: Knex): Promise<void> {
|
||||||
|
await knex.schema.dropTableIfExists(TableName.IdentityMetadata);
|
||||||
|
}
|
@ -0,0 +1,30 @@
|
|||||||
|
import { Knex } from "knex";
|
||||||
|
|
||||||
|
import { TableName } from "../schemas";
|
||||||
|
|
||||||
|
export async function up(knex: Knex): Promise<void> {
|
||||||
|
if (await knex.schema.hasTable(TableName.SecretSharing)) {
|
||||||
|
await knex.schema.alterTable(TableName.SecretSharing, (t) => {
|
||||||
|
t.string("iv").nullable().alter();
|
||||||
|
t.string("tag").nullable().alter();
|
||||||
|
t.string("encryptedValue").nullable().alter();
|
||||||
|
|
||||||
|
t.binary("encryptedSecret").nullable();
|
||||||
|
t.string("hashedHex").nullable().alter();
|
||||||
|
|
||||||
|
t.string("identifier", 64).nullable();
|
||||||
|
t.unique("identifier");
|
||||||
|
t.index("identifier");
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function down(knex: Knex): Promise<void> {
|
||||||
|
if (await knex.schema.hasTable(TableName.SecretSharing)) {
|
||||||
|
await knex.schema.alterTable(TableName.SecretSharing, (t) => {
|
||||||
|
t.dropColumn("encryptedSecret");
|
||||||
|
|
||||||
|
t.dropColumn("identifier");
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
@ -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.OidcConfig, "lastUsed"))) {
|
||||||
|
await knex.schema.alterTable(TableName.OidcConfig, (tb) => {
|
||||||
|
tb.datetime("lastUsed");
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function down(knex: Knex): Promise<void> {
|
||||||
|
if (await knex.schema.hasColumn(TableName.OidcConfig, "lastUsed")) {
|
||||||
|
await knex.schema.alterTable(TableName.OidcConfig, (tb) => {
|
||||||
|
tb.dropColumn("lastUsed");
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,46 @@
|
|||||||
|
import { Knex } from "knex";
|
||||||
|
|
||||||
|
import { dropConstraintIfExists } from "@app/db/migrations/utils/dropConstraintIfExists";
|
||||||
|
import { TableName } from "@app/db/schemas";
|
||||||
|
|
||||||
|
export async function up(knex: Knex): Promise<void> {
|
||||||
|
if (await knex.schema.hasTable(TableName.KmsKey)) {
|
||||||
|
const hasOrgId = await knex.schema.hasColumn(TableName.KmsKey, "orgId");
|
||||||
|
const hasSlug = await knex.schema.hasColumn(TableName.KmsKey, "slug");
|
||||||
|
|
||||||
|
// drop constraint if exists (won't exist if rolled back, see below)
|
||||||
|
await dropConstraintIfExists(TableName.KmsKey, "kms_keys_orgid_slug_unique", knex);
|
||||||
|
|
||||||
|
// projectId for CMEK functionality
|
||||||
|
await knex.schema.alterTable(TableName.KmsKey, (table) => {
|
||||||
|
table.string("projectId").nullable().references("id").inTable(TableName.Project).onDelete("CASCADE");
|
||||||
|
|
||||||
|
if (hasOrgId) {
|
||||||
|
table.unique(["orgId", "projectId", "slug"]);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (hasSlug) {
|
||||||
|
table.renameColumn("slug", "name");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function down(knex: Knex): Promise<void> {
|
||||||
|
if (await knex.schema.hasTable(TableName.KmsKey)) {
|
||||||
|
const hasOrgId = await knex.schema.hasColumn(TableName.KmsKey, "orgId");
|
||||||
|
const hasName = await knex.schema.hasColumn(TableName.KmsKey, "name");
|
||||||
|
|
||||||
|
// remove projectId for CMEK functionality
|
||||||
|
await knex.schema.alterTable(TableName.KmsKey, (table) => {
|
||||||
|
if (hasName) {
|
||||||
|
table.renameColumn("name", "slug");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (hasOrgId) {
|
||||||
|
table.dropUnique(["orgId", "projectId", "slug"]);
|
||||||
|
}
|
||||||
|
table.dropColumn("projectId");
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,30 @@
|
|||||||
|
import { Knex } from "knex";
|
||||||
|
|
||||||
|
import { TableName } from "@app/db/schemas";
|
||||||
|
|
||||||
|
export async function up(knex: Knex): Promise<void> {
|
||||||
|
if (await knex.schema.hasTable(TableName.KmsKey)) {
|
||||||
|
const hasSlug = await knex.schema.hasColumn(TableName.KmsKey, "slug");
|
||||||
|
|
||||||
|
if (!hasSlug) {
|
||||||
|
// add slug back temporarily and set value equal to name
|
||||||
|
await knex.schema
|
||||||
|
.alterTable(TableName.KmsKey, (table) => {
|
||||||
|
table.string("slug", 32);
|
||||||
|
})
|
||||||
|
.then(() => knex(TableName.KmsKey).update("slug", knex.ref("name")));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function down(knex: Knex): Promise<void> {
|
||||||
|
if (await knex.schema.hasTable(TableName.KmsKey)) {
|
||||||
|
const hasSlug = await knex.schema.hasColumn(TableName.KmsKey, "slug");
|
||||||
|
|
||||||
|
if (hasSlug) {
|
||||||
|
await knex.schema.alterTable(TableName.KmsKey, (table) => {
|
||||||
|
table.dropColumn("slug");
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,48 @@
|
|||||||
|
import { Knex } from "knex";
|
||||||
|
|
||||||
|
import { TableName } from "../schemas";
|
||||||
|
|
||||||
|
export async function up(knex: Knex): Promise<void> {
|
||||||
|
if (await knex.schema.hasTable(TableName.AuditLog)) {
|
||||||
|
const doesProjectIdExist = await knex.schema.hasColumn(TableName.AuditLog, "projectId");
|
||||||
|
const doesOrgIdExist = await knex.schema.hasColumn(TableName.AuditLog, "orgId");
|
||||||
|
const doesProjectNameExist = await knex.schema.hasColumn(TableName.AuditLog, "projectName");
|
||||||
|
|
||||||
|
await knex.schema.alterTable(TableName.AuditLog, (t) => {
|
||||||
|
if (doesOrgIdExist) {
|
||||||
|
t.dropForeign("orgId");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (doesProjectIdExist) {
|
||||||
|
t.dropForeign("projectId");
|
||||||
|
}
|
||||||
|
|
||||||
|
// add normalized field
|
||||||
|
if (!doesProjectNameExist) {
|
||||||
|
t.string("projectName");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function down(knex: Knex): Promise<void> {
|
||||||
|
const doesProjectIdExist = await knex.schema.hasColumn(TableName.AuditLog, "projectId");
|
||||||
|
const doesOrgIdExist = await knex.schema.hasColumn(TableName.AuditLog, "orgId");
|
||||||
|
const doesProjectNameExist = await knex.schema.hasColumn(TableName.AuditLog, "projectName");
|
||||||
|
|
||||||
|
if (await knex.schema.hasTable(TableName.AuditLog)) {
|
||||||
|
await knex.schema.alterTable(TableName.AuditLog, (t) => {
|
||||||
|
if (doesOrgIdExist) {
|
||||||
|
t.foreign("orgId").references("id").inTable(TableName.Organization).onDelete("CASCADE");
|
||||||
|
}
|
||||||
|
if (doesProjectIdExist) {
|
||||||
|
t.foreign("projectId").references("id").inTable(TableName.Project).onDelete("CASCADE");
|
||||||
|
}
|
||||||
|
|
||||||
|
// remove normalized field
|
||||||
|
if (doesProjectNameExist) {
|
||||||
|
t.dropColumn("projectName");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,29 @@
|
|||||||
|
import { Knex } from "knex";
|
||||||
|
|
||||||
|
import { TableName } from "@app/db/schemas";
|
||||||
|
|
||||||
|
export async function up(knex: Knex): Promise<void> {
|
||||||
|
// org default role
|
||||||
|
if (await knex.schema.hasTable(TableName.Organization)) {
|
||||||
|
const hasDefaultRoleCol = await knex.schema.hasColumn(TableName.Organization, "defaultMembershipRole");
|
||||||
|
|
||||||
|
if (!hasDefaultRoleCol) {
|
||||||
|
await knex.schema.alterTable(TableName.Organization, (tb) => {
|
||||||
|
tb.string("defaultMembershipRole").notNullable().defaultTo("member");
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function down(knex: Knex): Promise<void> {
|
||||||
|
// org default role
|
||||||
|
if (await knex.schema.hasTable(TableName.Organization)) {
|
||||||
|
const hasDefaultRoleCol = await knex.schema.hasColumn(TableName.Organization, "defaultMembershipRole");
|
||||||
|
|
||||||
|
if (hasDefaultRoleCol) {
|
||||||
|
await knex.schema.alterTable(TableName.Organization, (tb) => {
|
||||||
|
tb.dropColumn("defaultMembershipRole");
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -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.IdentityMetadata, "value")) {
|
||||||
|
await knex.schema.alterTable(TableName.IdentityMetadata, (t) => {
|
||||||
|
t.string("value", 1020).alter();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function down(knex: Knex): Promise<void> {
|
||||||
|
if (await knex.schema.hasColumn(TableName.IdentityMetadata, "value")) {
|
||||||
|
await knex.schema.alterTable(TableName.IdentityMetadata, (t) => {
|
||||||
|
t.string("value", 255).alter();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,32 @@
|
|||||||
|
import { Knex } from "knex";
|
||||||
|
|
||||||
|
import { TableName } from "@app/db/schemas";
|
||||||
|
import { createOnUpdateTrigger, dropOnUpdateTrigger } from "@app/db/utils";
|
||||||
|
|
||||||
|
export async function up(knex: Knex): Promise<void> {
|
||||||
|
// add external group to org role mapping table
|
||||||
|
if (!(await knex.schema.hasTable(TableName.ExternalGroupOrgRoleMapping))) {
|
||||||
|
await knex.schema.createTable(TableName.ExternalGroupOrgRoleMapping, (t) => {
|
||||||
|
t.uuid("id", { primaryKey: true }).defaultTo(knex.fn.uuid());
|
||||||
|
t.string("groupName").notNullable();
|
||||||
|
t.index("groupName");
|
||||||
|
t.string("role").notNullable();
|
||||||
|
t.uuid("roleId");
|
||||||
|
t.foreign("roleId").references("id").inTable(TableName.OrgRoles);
|
||||||
|
t.uuid("orgId").notNullable();
|
||||||
|
t.foreign("orgId").references("id").inTable(TableName.Organization).onDelete("CASCADE");
|
||||||
|
t.timestamps(true, true, true);
|
||||||
|
t.unique(["orgId", "groupName"]);
|
||||||
|
});
|
||||||
|
|
||||||
|
await createOnUpdateTrigger(knex, TableName.ExternalGroupOrgRoleMapping);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function down(knex: Knex): Promise<void> {
|
||||||
|
if (await knex.schema.hasTable(TableName.ExternalGroupOrgRoleMapping)) {
|
||||||
|
await dropOnUpdateTrigger(knex, TableName.ExternalGroupOrgRoleMapping);
|
||||||
|
|
||||||
|
await knex.schema.dropTable(TableName.ExternalGroupOrgRoleMapping);
|
||||||
|
}
|
||||||
|
}
|
@ -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.Organization, "enforceMfa"))) {
|
||||||
|
await knex.schema.alterTable(TableName.Organization, (tb) => {
|
||||||
|
tb.boolean("enforceMfa").defaultTo(false).notNullable();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function down(knex: Knex): Promise<void> {
|
||||||
|
if (await knex.schema.hasColumn(TableName.Organization, "enforceMfa")) {
|
||||||
|
await knex.schema.alterTable(TableName.Organization, (t) => {
|
||||||
|
t.dropColumn("enforceMfa");
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,21 @@
|
|||||||
|
import { Knex } from "knex";
|
||||||
|
|
||||||
|
import { TableName } from "../schemas";
|
||||||
|
|
||||||
|
export async function up(knex: Knex): Promise<void> {
|
||||||
|
if (await knex.schema.hasColumn(TableName.SamlConfig, "orgId")) {
|
||||||
|
await knex.schema.alterTable(TableName.SamlConfig, (t) => {
|
||||||
|
t.dropForeign("orgId");
|
||||||
|
t.foreign("orgId").references("id").inTable(TableName.Organization).onDelete("CASCADE");
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function down(knex: Knex): Promise<void> {
|
||||||
|
if (await knex.schema.hasColumn(TableName.SamlConfig, "orgId")) {
|
||||||
|
await knex.schema.alterTable(TableName.SamlConfig, (t) => {
|
||||||
|
t.dropForeign("orgId");
|
||||||
|
t.foreign("orgId").references("id").inTable(TableName.Organization);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,6 @@
|
|||||||
|
import { Knex } from "knex";
|
||||||
|
|
||||||
|
import { TableName } from "@app/db/schemas";
|
||||||
|
|
||||||
|
export const dropConstraintIfExists = (tableName: TableName, constraintName: string, knex: Knex) =>
|
||||||
|
knex.raw(`ALTER TABLE ${tableName} DROP CONSTRAINT IF EXISTS ${constraintName};`);
|
@ -54,7 +54,7 @@ export const getSecretManagerDataKey = async (knex: Knex, projectId: string) =>
|
|||||||
} else {
|
} else {
|
||||||
const [kmsDoc] = await knex(TableName.KmsKey)
|
const [kmsDoc] = await knex(TableName.KmsKey)
|
||||||
.insert({
|
.insert({
|
||||||
slug: slugify(alphaNumericNanoId(8).toLowerCase()),
|
name: slugify(alphaNumericNanoId(8).toLowerCase()),
|
||||||
orgId: project.orgId,
|
orgId: project.orgId,
|
||||||
isReserved: false
|
isReserved: false
|
||||||
})
|
})
|
||||||
|
@ -12,7 +12,8 @@ export const AccessApprovalPoliciesApproversSchema = z.object({
|
|||||||
policyId: z.string().uuid(),
|
policyId: z.string().uuid(),
|
||||||
createdAt: z.date(),
|
createdAt: z.date(),
|
||||||
updatedAt: z.date(),
|
updatedAt: z.date(),
|
||||||
approverUserId: z.string().uuid()
|
approverUserId: z.string().uuid().nullable().optional(),
|
||||||
|
approverGroupId: z.string().uuid().nullable().optional()
|
||||||
});
|
});
|
||||||
|
|
||||||
export type TAccessApprovalPoliciesApprovers = z.infer<typeof AccessApprovalPoliciesApproversSchema>;
|
export type TAccessApprovalPoliciesApprovers = z.infer<typeof AccessApprovalPoliciesApproversSchema>;
|
||||||
|
@ -20,7 +20,8 @@ export const AuditLogsSchema = z.object({
|
|||||||
createdAt: z.date(),
|
createdAt: z.date(),
|
||||||
updatedAt: z.date(),
|
updatedAt: z.date(),
|
||||||
orgId: z.string().uuid().nullable().optional(),
|
orgId: z.string().uuid().nullable().optional(),
|
||||||
projectId: z.string().nullable().optional()
|
projectId: z.string().nullable().optional(),
|
||||||
|
projectName: z.string().nullable().optional()
|
||||||
});
|
});
|
||||||
|
|
||||||
export type TAuditLogs = z.infer<typeof AuditLogsSchema>;
|
export type TAuditLogs = z.infer<typeof AuditLogsSchema>;
|
||||||
|
27
backend/src/db/schemas/external-group-org-role-mappings.ts
Normal file
27
backend/src/db/schemas/external-group-org-role-mappings.ts
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
// Code generated by automation script, DO NOT EDIT.
|
||||||
|
// Automated by pulling database and generating zod schema
|
||||||
|
// To update. Just run npm run generate:schema
|
||||||
|
// Written by akhilmhdh.
|
||||||
|
|
||||||
|
import { z } from "zod";
|
||||||
|
|
||||||
|
import { TImmutableDBKeys } from "./models";
|
||||||
|
|
||||||
|
export const ExternalGroupOrgRoleMappingsSchema = z.object({
|
||||||
|
id: z.string().uuid(),
|
||||||
|
groupName: z.string(),
|
||||||
|
role: z.string(),
|
||||||
|
roleId: z.string().uuid().nullable().optional(),
|
||||||
|
orgId: z.string().uuid(),
|
||||||
|
createdAt: z.date(),
|
||||||
|
updatedAt: z.date()
|
||||||
|
});
|
||||||
|
|
||||||
|
export type TExternalGroupOrgRoleMappings = z.infer<typeof ExternalGroupOrgRoleMappingsSchema>;
|
||||||
|
export type TExternalGroupOrgRoleMappingsInsert = Omit<
|
||||||
|
z.input<typeof ExternalGroupOrgRoleMappingsSchema>,
|
||||||
|
TImmutableDBKeys
|
||||||
|
>;
|
||||||
|
export type TExternalGroupOrgRoleMappingsUpdate = Partial<
|
||||||
|
Omit<z.input<typeof ExternalGroupOrgRoleMappingsSchema>, TImmutableDBKeys>
|
||||||
|
>;
|
23
backend/src/db/schemas/identity-metadata.ts
Normal file
23
backend/src/db/schemas/identity-metadata.ts
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
// Code generated by automation script, DO NOT EDIT.
|
||||||
|
// Automated by pulling database and generating zod schema
|
||||||
|
// To update. Just run npm run generate:schema
|
||||||
|
// Written by akhilmhdh.
|
||||||
|
|
||||||
|
import { z } from "zod";
|
||||||
|
|
||||||
|
import { TImmutableDBKeys } from "./models";
|
||||||
|
|
||||||
|
export const IdentityMetadataSchema = z.object({
|
||||||
|
id: z.string().uuid(),
|
||||||
|
key: z.string(),
|
||||||
|
value: z.string(),
|
||||||
|
orgId: z.string().uuid(),
|
||||||
|
userId: z.string().uuid().nullable().optional(),
|
||||||
|
identityId: z.string().uuid().nullable().optional(),
|
||||||
|
createdAt: z.date(),
|
||||||
|
updatedAt: z.date()
|
||||||
|
});
|
||||||
|
|
||||||
|
export type TIdentityMetadata = z.infer<typeof IdentityMetadataSchema>;
|
||||||
|
export type TIdentityMetadataInsert = Omit<z.input<typeof IdentityMetadataSchema>, TImmutableDBKeys>;
|
||||||
|
export type TIdentityMetadataUpdate = Partial<Omit<z.input<typeof IdentityMetadataSchema>, TImmutableDBKeys>>;
|
@ -31,6 +31,7 @@ export * from "./identity-aws-auths";
|
|||||||
export * from "./identity-azure-auths";
|
export * from "./identity-azure-auths";
|
||||||
export * from "./identity-gcp-auths";
|
export * from "./identity-gcp-auths";
|
||||||
export * from "./identity-kubernetes-auths";
|
export * from "./identity-kubernetes-auths";
|
||||||
|
export * from "./identity-metadata";
|
||||||
export * from "./identity-oidc-auths";
|
export * from "./identity-oidc-auths";
|
||||||
export * from "./identity-org-memberships";
|
export * from "./identity-org-memberships";
|
||||||
export * from "./identity-project-additional-privilege";
|
export * from "./identity-project-additional-privilege";
|
||||||
|
@ -13,9 +13,11 @@ export const KmsKeysSchema = z.object({
|
|||||||
isDisabled: z.boolean().default(false).nullable().optional(),
|
isDisabled: z.boolean().default(false).nullable().optional(),
|
||||||
isReserved: z.boolean().default(true).nullable().optional(),
|
isReserved: z.boolean().default(true).nullable().optional(),
|
||||||
orgId: z.string().uuid(),
|
orgId: z.string().uuid(),
|
||||||
slug: z.string(),
|
name: z.string(),
|
||||||
createdAt: z.date(),
|
createdAt: z.date(),
|
||||||
updatedAt: z.date()
|
updatedAt: z.date(),
|
||||||
|
projectId: z.string().nullable().optional(),
|
||||||
|
slug: z.string().nullable().optional()
|
||||||
});
|
});
|
||||||
|
|
||||||
export type TKmsKeys = z.infer<typeof KmsKeysSchema>;
|
export type TKmsKeys = z.infer<typeof KmsKeysSchema>;
|
||||||
|
@ -17,6 +17,7 @@ export enum TableName {
|
|||||||
Groups = "groups",
|
Groups = "groups",
|
||||||
GroupProjectMembership = "group_project_memberships",
|
GroupProjectMembership = "group_project_memberships",
|
||||||
GroupProjectMembershipRole = "group_project_membership_roles",
|
GroupProjectMembershipRole = "group_project_membership_roles",
|
||||||
|
ExternalGroupOrgRoleMapping = "external_group_org_role_mappings",
|
||||||
UserGroupMembership = "user_group_membership",
|
UserGroupMembership = "user_group_membership",
|
||||||
UserAliases = "user_aliases",
|
UserAliases = "user_aliases",
|
||||||
UserEncryptionKey = "user_encryption_keys",
|
UserEncryptionKey = "user_encryption_keys",
|
||||||
@ -70,6 +71,8 @@ export enum TableName {
|
|||||||
IdentityProjectMembership = "identity_project_memberships",
|
IdentityProjectMembership = "identity_project_memberships",
|
||||||
IdentityProjectMembershipRole = "identity_project_membership_role",
|
IdentityProjectMembershipRole = "identity_project_membership_role",
|
||||||
IdentityProjectAdditionalPrivilege = "identity_project_additional_privilege",
|
IdentityProjectAdditionalPrivilege = "identity_project_additional_privilege",
|
||||||
|
// used by both identity and users
|
||||||
|
IdentityMetadata = "identity_metadata",
|
||||||
ScimToken = "scim_tokens",
|
ScimToken = "scim_tokens",
|
||||||
AccessApprovalPolicy = "access_approval_policies",
|
AccessApprovalPolicy = "access_approval_policies",
|
||||||
AccessApprovalPolicyApprover = "access_approval_policies_approvers",
|
AccessApprovalPolicyApprover = "access_approval_policies_approvers",
|
||||||
|
@ -26,7 +26,8 @@ export const OidcConfigsSchema = z.object({
|
|||||||
isActive: z.boolean(),
|
isActive: z.boolean(),
|
||||||
createdAt: z.date(),
|
createdAt: z.date(),
|
||||||
updatedAt: z.date(),
|
updatedAt: z.date(),
|
||||||
orgId: z.string().uuid()
|
orgId: z.string().uuid(),
|
||||||
|
lastUsed: z.date().nullable().optional()
|
||||||
});
|
});
|
||||||
|
|
||||||
export type TOidcConfigs = z.infer<typeof OidcConfigsSchema>;
|
export type TOidcConfigs = z.infer<typeof OidcConfigsSchema>;
|
||||||
|
@ -19,7 +19,9 @@ export const OrganizationsSchema = z.object({
|
|||||||
authEnforced: z.boolean().default(false).nullable().optional(),
|
authEnforced: z.boolean().default(false).nullable().optional(),
|
||||||
scimEnabled: z.boolean().default(false).nullable().optional(),
|
scimEnabled: z.boolean().default(false).nullable().optional(),
|
||||||
kmsDefaultKeyId: z.string().uuid().nullable().optional(),
|
kmsDefaultKeyId: z.string().uuid().nullable().optional(),
|
||||||
kmsEncryptedDataKey: zodBuffer.nullable().optional()
|
kmsEncryptedDataKey: zodBuffer.nullable().optional(),
|
||||||
|
defaultMembershipRole: z.string().default("member"),
|
||||||
|
enforceMfa: z.boolean().default(false)
|
||||||
});
|
});
|
||||||
|
|
||||||
export type TOrganizations = z.infer<typeof OrganizationsSchema>;
|
export type TOrganizations = z.infer<typeof OrganizationsSchema>;
|
||||||
|
@ -12,7 +12,8 @@ export const SecretApprovalPoliciesApproversSchema = z.object({
|
|||||||
policyId: z.string().uuid(),
|
policyId: z.string().uuid(),
|
||||||
createdAt: z.date(),
|
createdAt: z.date(),
|
||||||
updatedAt: z.date(),
|
updatedAt: z.date(),
|
||||||
approverUserId: z.string().uuid()
|
approverUserId: z.string().uuid().nullable().optional(),
|
||||||
|
approverGroupId: z.string().uuid().nullable().optional()
|
||||||
});
|
});
|
||||||
|
|
||||||
export type TSecretApprovalPoliciesApprovers = z.infer<typeof SecretApprovalPoliciesApproversSchema>;
|
export type TSecretApprovalPoliciesApprovers = z.infer<typeof SecretApprovalPoliciesApproversSchema>;
|
||||||
|
@ -5,14 +5,16 @@
|
|||||||
|
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
|
|
||||||
|
import { zodBuffer } from "@app/lib/zod";
|
||||||
|
|
||||||
import { TImmutableDBKeys } from "./models";
|
import { TImmutableDBKeys } from "./models";
|
||||||
|
|
||||||
export const SecretSharingSchema = z.object({
|
export const SecretSharingSchema = z.object({
|
||||||
id: z.string().uuid(),
|
id: z.string().uuid(),
|
||||||
encryptedValue: z.string(),
|
encryptedValue: z.string().nullable().optional(),
|
||||||
iv: z.string(),
|
iv: z.string().nullable().optional(),
|
||||||
tag: z.string(),
|
tag: z.string().nullable().optional(),
|
||||||
hashedHex: z.string(),
|
hashedHex: z.string().nullable().optional(),
|
||||||
expiresAt: z.date(),
|
expiresAt: z.date(),
|
||||||
userId: z.string().uuid().nullable().optional(),
|
userId: z.string().uuid().nullable().optional(),
|
||||||
orgId: z.string().uuid().nullable().optional(),
|
orgId: z.string().uuid().nullable().optional(),
|
||||||
@ -22,7 +24,9 @@ export const SecretSharingSchema = z.object({
|
|||||||
accessType: z.string().default("anyone"),
|
accessType: z.string().default("anyone"),
|
||||||
name: z.string().nullable().optional(),
|
name: z.string().nullable().optional(),
|
||||||
lastViewedAt: z.date().nullable().optional(),
|
lastViewedAt: z.date().nullable().optional(),
|
||||||
password: z.string().nullable().optional()
|
password: z.string().nullable().optional(),
|
||||||
|
encryptedSecret: zodBuffer.nullable().optional(),
|
||||||
|
identifier: z.string().nullable().optional()
|
||||||
});
|
});
|
||||||
|
|
||||||
export type TSecretSharing = z.infer<typeof SecretSharingSchema>;
|
export type TSecretSharing = z.infer<typeof SecretSharingSchema>;
|
||||||
|
@ -1,7 +1,9 @@
|
|||||||
import { nanoid } from "nanoid";
|
import { nanoid } from "nanoid";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
|
|
||||||
|
import { ApproverType } from "@app/ee/services/access-approval-policy/access-approval-policy-types";
|
||||||
import { EnforcementLevel } from "@app/lib/types";
|
import { EnforcementLevel } from "@app/lib/types";
|
||||||
|
import { readLimit, writeLimit } from "@app/server/config/rateLimiter";
|
||||||
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
|
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
|
||||||
import { sapPubSchema } from "@app/server/routes/sanitizedSchemas";
|
import { sapPubSchema } from "@app/server/routes/sanitizedSchemas";
|
||||||
import { AuthMode } from "@app/services/auth/auth-type";
|
import { AuthMode } from "@app/services/auth/auth-type";
|
||||||
@ -10,28 +12,32 @@ export const registerAccessApprovalPolicyRouter = async (server: FastifyZodProvi
|
|||||||
server.route({
|
server.route({
|
||||||
url: "/",
|
url: "/",
|
||||||
method: "POST",
|
method: "POST",
|
||||||
|
config: {
|
||||||
|
rateLimit: writeLimit
|
||||||
|
},
|
||||||
schema: {
|
schema: {
|
||||||
body: z
|
body: z.object({
|
||||||
.object({
|
projectSlug: z.string().trim(),
|
||||||
projectSlug: z.string().trim(),
|
name: z.string().optional(),
|
||||||
name: z.string().optional(),
|
secretPath: z.string().trim().default("/"),
|
||||||
secretPath: z.string().trim().default("/"),
|
environment: z.string(),
|
||||||
environment: z.string(),
|
approvers: z
|
||||||
approvers: z.string().array().min(1),
|
.discriminatedUnion("type", [
|
||||||
approvals: z.number().min(1).default(1),
|
z.object({ type: z.literal(ApproverType.Group), id: z.string() }),
|
||||||
enforcementLevel: z.nativeEnum(EnforcementLevel).default(EnforcementLevel.Hard)
|
z.object({ type: z.literal(ApproverType.User), id: z.string().optional(), name: z.string().optional() })
|
||||||
})
|
])
|
||||||
.refine((data) => data.approvals <= data.approvers.length, {
|
.array()
|
||||||
path: ["approvals"],
|
.min(1, { message: "At least one approver should be provided" }),
|
||||||
message: "The number of approvals should be lower than the number of approvers."
|
approvals: z.number().min(1).default(1),
|
||||||
}),
|
enforcementLevel: z.nativeEnum(EnforcementLevel).default(EnforcementLevel.Hard)
|
||||||
|
}),
|
||||||
response: {
|
response: {
|
||||||
200: z.object({
|
200: z.object({
|
||||||
approval: sapPubSchema
|
approval: sapPubSchema
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
onRequest: verifyAuth([AuthMode.JWT]),
|
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
||||||
handler: async (req) => {
|
handler: async (req) => {
|
||||||
const approval = await server.services.accessApprovalPolicy.createAccessApprovalPolicy({
|
const approval = await server.services.accessApprovalPolicy.createAccessApprovalPolicy({
|
||||||
actor: req.permission.type,
|
actor: req.permission.type,
|
||||||
@ -50,6 +56,9 @@ export const registerAccessApprovalPolicyRouter = async (server: FastifyZodProvi
|
|||||||
server.route({
|
server.route({
|
||||||
url: "/",
|
url: "/",
|
||||||
method: "GET",
|
method: "GET",
|
||||||
|
config: {
|
||||||
|
rateLimit: readLimit
|
||||||
|
},
|
||||||
schema: {
|
schema: {
|
||||||
querystring: z.object({
|
querystring: z.object({
|
||||||
projectSlug: z.string().trim()
|
projectSlug: z.string().trim()
|
||||||
@ -58,14 +67,15 @@ export const registerAccessApprovalPolicyRouter = async (server: FastifyZodProvi
|
|||||||
200: z.object({
|
200: z.object({
|
||||||
approvals: sapPubSchema
|
approvals: sapPubSchema
|
||||||
.extend({
|
.extend({
|
||||||
userApprovers: z
|
approvers: z
|
||||||
.object({
|
.object({ type: z.nativeEnum(ApproverType), id: z.string().nullable().optional() })
|
||||||
userId: z.string()
|
.array()
|
||||||
})
|
.nullable()
|
||||||
.array(),
|
.optional()
|
||||||
secretPath: z.string().optional().nullable()
|
|
||||||
})
|
})
|
||||||
.array()
|
.array()
|
||||||
|
.nullable()
|
||||||
|
.optional()
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@ -115,33 +125,37 @@ export const registerAccessApprovalPolicyRouter = async (server: FastifyZodProvi
|
|||||||
server.route({
|
server.route({
|
||||||
url: "/:policyId",
|
url: "/:policyId",
|
||||||
method: "PATCH",
|
method: "PATCH",
|
||||||
|
config: {
|
||||||
|
rateLimit: writeLimit
|
||||||
|
},
|
||||||
schema: {
|
schema: {
|
||||||
params: z.object({
|
params: z.object({
|
||||||
policyId: z.string()
|
policyId: z.string()
|
||||||
}),
|
}),
|
||||||
body: z
|
body: z.object({
|
||||||
.object({
|
name: z.string().optional(),
|
||||||
name: z.string().optional(),
|
secretPath: z
|
||||||
secretPath: z
|
.string()
|
||||||
.string()
|
.trim()
|
||||||
.trim()
|
.optional()
|
||||||
.optional()
|
.transform((val) => (val === "" ? "/" : val)),
|
||||||
.transform((val) => (val === "" ? "/" : val)),
|
approvers: z
|
||||||
approvers: z.string().array().min(1),
|
.discriminatedUnion("type", [
|
||||||
approvals: z.number().min(1).default(1),
|
z.object({ type: z.literal(ApproverType.Group), id: z.string() }),
|
||||||
enforcementLevel: z.nativeEnum(EnforcementLevel).default(EnforcementLevel.Hard)
|
z.object({ type: z.literal(ApproverType.User), id: z.string().optional(), name: z.string().optional() })
|
||||||
})
|
])
|
||||||
.refine((data) => data.approvals <= data.approvers.length, {
|
.array()
|
||||||
path: ["approvals"],
|
.min(1, { message: "At least one approver should be provided" }),
|
||||||
message: "The number of approvals should be lower than the number of approvers."
|
approvals: z.number().min(1).optional(),
|
||||||
}),
|
enforcementLevel: z.nativeEnum(EnforcementLevel).default(EnforcementLevel.Hard)
|
||||||
|
}),
|
||||||
response: {
|
response: {
|
||||||
200: z.object({
|
200: z.object({
|
||||||
approval: sapPubSchema
|
approval: sapPubSchema
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
onRequest: verifyAuth([AuthMode.JWT]),
|
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
||||||
handler: async (req) => {
|
handler: async (req) => {
|
||||||
await server.services.accessApprovalPolicy.updateAccessApprovalPolicy({
|
await server.services.accessApprovalPolicy.updateAccessApprovalPolicy({
|
||||||
policyId: req.params.policyId,
|
policyId: req.params.policyId,
|
||||||
@ -157,6 +171,9 @@ export const registerAccessApprovalPolicyRouter = async (server: FastifyZodProvi
|
|||||||
server.route({
|
server.route({
|
||||||
url: "/:policyId",
|
url: "/:policyId",
|
||||||
method: "DELETE",
|
method: "DELETE",
|
||||||
|
config: {
|
||||||
|
rateLimit: writeLimit
|
||||||
|
},
|
||||||
schema: {
|
schema: {
|
||||||
params: z.object({
|
params: z.object({
|
||||||
policyId: z.string()
|
policyId: z.string()
|
||||||
@ -167,7 +184,7 @@ export const registerAccessApprovalPolicyRouter = async (server: FastifyZodProvi
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
onRequest: verifyAuth([AuthMode.JWT]),
|
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
||||||
handler: async (req) => {
|
handler: async (req) => {
|
||||||
const approval = await server.services.accessApprovalPolicy.deleteAccessApprovalPolicy({
|
const approval = await server.services.accessApprovalPolicy.deleteAccessApprovalPolicy({
|
||||||
actor: req.permission.type,
|
actor: req.permission.type,
|
||||||
@ -179,4 +196,44 @@ export const registerAccessApprovalPolicyRouter = async (server: FastifyZodProvi
|
|||||||
return { approval };
|
return { approval };
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
server.route({
|
||||||
|
url: "/:policyId",
|
||||||
|
method: "GET",
|
||||||
|
config: {
|
||||||
|
rateLimit: readLimit
|
||||||
|
},
|
||||||
|
schema: {
|
||||||
|
params: z.object({
|
||||||
|
policyId: z.string()
|
||||||
|
}),
|
||||||
|
response: {
|
||||||
|
200: z.object({
|
||||||
|
approval: sapPubSchema.extend({
|
||||||
|
approvers: z
|
||||||
|
.object({
|
||||||
|
type: z.nativeEnum(ApproverType),
|
||||||
|
id: z.string().nullable().optional(),
|
||||||
|
name: z.string().nullable().optional()
|
||||||
|
})
|
||||||
|
.array()
|
||||||
|
.nullable()
|
||||||
|
.optional()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
||||||
|
handler: async (req) => {
|
||||||
|
const approval = await server.services.accessApprovalPolicy.getAccessApprovalPolicyById({
|
||||||
|
actor: req.permission.type,
|
||||||
|
actorId: req.permission.id,
|
||||||
|
actorAuthMethod: req.permission.authMethod,
|
||||||
|
actorOrgId: req.permission.orgId,
|
||||||
|
...req.params
|
||||||
|
});
|
||||||
|
|
||||||
|
return { approval };
|
||||||
|
}
|
||||||
|
});
|
||||||
};
|
};
|
||||||
|
@ -77,6 +77,39 @@ export const registerDynamicSecretRouter = async (server: FastifyZodProvider) =>
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
server.route({
|
||||||
|
method: "POST",
|
||||||
|
url: "/entra-id/users",
|
||||||
|
config: {
|
||||||
|
rateLimit: readLimit
|
||||||
|
},
|
||||||
|
schema: {
|
||||||
|
body: z.object({
|
||||||
|
tenantId: z.string().min(1).describe("The tenant ID of the Azure Entra ID"),
|
||||||
|
applicationId: z.string().min(1).describe("The application ID of the Azure Entra ID App Registration"),
|
||||||
|
clientSecret: z.string().min(1).describe("The client secret of the Azure Entra ID App Registration")
|
||||||
|
}),
|
||||||
|
response: {
|
||||||
|
200: z
|
||||||
|
.object({
|
||||||
|
name: z.string().min(1).describe("The name of the user"),
|
||||||
|
id: z.string().min(1).describe("The ID of the user"),
|
||||||
|
email: z.string().min(1).describe("The email of the user")
|
||||||
|
})
|
||||||
|
.array()
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
||||||
|
handler: async (req) => {
|
||||||
|
const data = await server.services.dynamicSecret.fetchAzureEntraIdUsers({
|
||||||
|
tenantId: req.body.tenantId,
|
||||||
|
applicationId: req.body.applicationId,
|
||||||
|
clientSecret: req.body.clientSecret
|
||||||
|
});
|
||||||
|
return data;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
server.route({
|
server.route({
|
||||||
method: "PATCH",
|
method: "PATCH",
|
||||||
url: "/:name",
|
url: "/:name",
|
||||||
@ -237,7 +270,7 @@ export const registerDynamicSecretRouter = async (server: FastifyZodProvider) =>
|
|||||||
},
|
},
|
||||||
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
||||||
handler: async (req) => {
|
handler: async (req) => {
|
||||||
const dynamicSecretCfgs = await server.services.dynamicSecret.list({
|
const dynamicSecretCfgs = await server.services.dynamicSecret.listDynamicSecretsByEnv({
|
||||||
actor: req.permission.type,
|
actor: req.permission.type,
|
||||||
actorId: req.permission.id,
|
actorId: req.permission.id,
|
||||||
actorAuthMethod: req.permission.authMethod,
|
actorAuthMethod: req.permission.authMethod,
|
||||||
|
@ -26,7 +26,7 @@ const sanitizedExternalSchemaForGetAll = KmsKeysSchema.pick({
|
|||||||
isDisabled: true,
|
isDisabled: true,
|
||||||
createdAt: true,
|
createdAt: true,
|
||||||
updatedAt: true,
|
updatedAt: true,
|
||||||
slug: true
|
name: true
|
||||||
})
|
})
|
||||||
.extend({
|
.extend({
|
||||||
externalKms: ExternalKmsSchema.pick({
|
externalKms: ExternalKmsSchema.pick({
|
||||||
@ -57,7 +57,7 @@ export const registerExternalKmsRouter = async (server: FastifyZodProvider) => {
|
|||||||
},
|
},
|
||||||
schema: {
|
schema: {
|
||||||
body: z.object({
|
body: z.object({
|
||||||
slug: z.string().min(1).trim().toLowerCase(),
|
name: z.string().min(1).trim().toLowerCase(),
|
||||||
description: z.string().trim().optional(),
|
description: z.string().trim().optional(),
|
||||||
provider: ExternalKmsInputSchema
|
provider: ExternalKmsInputSchema
|
||||||
}),
|
}),
|
||||||
@ -74,7 +74,7 @@ export const registerExternalKmsRouter = async (server: FastifyZodProvider) => {
|
|||||||
actorId: req.permission.id,
|
actorId: req.permission.id,
|
||||||
actorAuthMethod: req.permission.authMethod,
|
actorAuthMethod: req.permission.authMethod,
|
||||||
actorOrgId: req.permission.orgId,
|
actorOrgId: req.permission.orgId,
|
||||||
slug: req.body.slug,
|
name: req.body.name,
|
||||||
provider: req.body.provider,
|
provider: req.body.provider,
|
||||||
description: req.body.description
|
description: req.body.description
|
||||||
});
|
});
|
||||||
@ -87,7 +87,7 @@ export const registerExternalKmsRouter = async (server: FastifyZodProvider) => {
|
|||||||
metadata: {
|
metadata: {
|
||||||
kmsId: externalKms.id,
|
kmsId: externalKms.id,
|
||||||
provider: req.body.provider.type,
|
provider: req.body.provider.type,
|
||||||
slug: req.body.slug,
|
name: req.body.name,
|
||||||
description: req.body.description
|
description: req.body.description
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -108,7 +108,7 @@ export const registerExternalKmsRouter = async (server: FastifyZodProvider) => {
|
|||||||
id: z.string().trim().min(1)
|
id: z.string().trim().min(1)
|
||||||
}),
|
}),
|
||||||
body: z.object({
|
body: z.object({
|
||||||
slug: z.string().min(1).trim().toLowerCase().optional(),
|
name: z.string().min(1).trim().toLowerCase().optional(),
|
||||||
description: z.string().trim().optional(),
|
description: z.string().trim().optional(),
|
||||||
provider: ExternalKmsInputUpdateSchema
|
provider: ExternalKmsInputUpdateSchema
|
||||||
}),
|
}),
|
||||||
@ -125,7 +125,7 @@ export const registerExternalKmsRouter = async (server: FastifyZodProvider) => {
|
|||||||
actorId: req.permission.id,
|
actorId: req.permission.id,
|
||||||
actorAuthMethod: req.permission.authMethod,
|
actorAuthMethod: req.permission.authMethod,
|
||||||
actorOrgId: req.permission.orgId,
|
actorOrgId: req.permission.orgId,
|
||||||
slug: req.body.slug,
|
name: req.body.name,
|
||||||
provider: req.body.provider,
|
provider: req.body.provider,
|
||||||
description: req.body.description,
|
description: req.body.description,
|
||||||
id: req.params.id
|
id: req.params.id
|
||||||
@ -139,7 +139,7 @@ export const registerExternalKmsRouter = async (server: FastifyZodProvider) => {
|
|||||||
metadata: {
|
metadata: {
|
||||||
kmsId: externalKms.id,
|
kmsId: externalKms.id,
|
||||||
provider: req.body.provider.type,
|
provider: req.body.provider.type,
|
||||||
slug: req.body.slug,
|
name: req.body.name,
|
||||||
description: req.body.description
|
description: req.body.description
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -182,7 +182,7 @@ export const registerExternalKmsRouter = async (server: FastifyZodProvider) => {
|
|||||||
type: EventType.DELETE_KMS,
|
type: EventType.DELETE_KMS,
|
||||||
metadata: {
|
metadata: {
|
||||||
kmsId: externalKms.id,
|
kmsId: externalKms.id,
|
||||||
slug: externalKms.slug
|
name: externalKms.name
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@ -224,7 +224,7 @@ export const registerExternalKmsRouter = async (server: FastifyZodProvider) => {
|
|||||||
type: EventType.GET_KMS,
|
type: EventType.GET_KMS,
|
||||||
metadata: {
|
metadata: {
|
||||||
kmsId: externalKms.id,
|
kmsId: externalKms.id,
|
||||||
slug: externalKms.slug
|
name: externalKms.name
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@ -260,13 +260,13 @@ export const registerExternalKmsRouter = async (server: FastifyZodProvider) => {
|
|||||||
|
|
||||||
server.route({
|
server.route({
|
||||||
method: "GET",
|
method: "GET",
|
||||||
url: "/slug/:slug",
|
url: "/name/:name",
|
||||||
config: {
|
config: {
|
||||||
rateLimit: readLimit
|
rateLimit: readLimit
|
||||||
},
|
},
|
||||||
schema: {
|
schema: {
|
||||||
params: z.object({
|
params: z.object({
|
||||||
slug: z.string().trim().min(1)
|
name: z.string().trim().min(1)
|
||||||
}),
|
}),
|
||||||
response: {
|
response: {
|
||||||
200: z.object({
|
200: z.object({
|
||||||
@ -276,12 +276,12 @@ export const registerExternalKmsRouter = async (server: FastifyZodProvider) => {
|
|||||||
},
|
},
|
||||||
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
||||||
handler: async (req) => {
|
handler: async (req) => {
|
||||||
const externalKms = await server.services.externalKms.findBySlug({
|
const externalKms = await server.services.externalKms.findByName({
|
||||||
actor: req.permission.type,
|
actor: req.permission.type,
|
||||||
actorId: req.permission.id,
|
actorId: req.permission.id,
|
||||||
actorAuthMethod: req.permission.authMethod,
|
actorAuthMethod: req.permission.authMethod,
|
||||||
actorOrgId: req.permission.orgId,
|
actorOrgId: req.permission.orgId,
|
||||||
slug: req.params.slug
|
name: req.params.name
|
||||||
});
|
});
|
||||||
return { externalKms };
|
return { externalKms };
|
||||||
}
|
}
|
||||||
|
@ -10,7 +10,7 @@ export const registerGroupRouter = async (server: FastifyZodProvider) => {
|
|||||||
server.route({
|
server.route({
|
||||||
url: "/",
|
url: "/",
|
||||||
method: "POST",
|
method: "POST",
|
||||||
onRequest: verifyAuth([AuthMode.JWT]),
|
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
||||||
schema: {
|
schema: {
|
||||||
body: z.object({
|
body: z.object({
|
||||||
name: z.string().trim().min(1).max(50).describe(GROUPS.CREATE.name),
|
name: z.string().trim().min(1).max(50).describe(GROUPS.CREATE.name),
|
||||||
@ -43,12 +43,59 @@ export const registerGroupRouter = async (server: FastifyZodProvider) => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
server.route({
|
server.route({
|
||||||
url: "/:currentSlug",
|
url: "/:id",
|
||||||
method: "PATCH",
|
method: "GET",
|
||||||
onRequest: verifyAuth([AuthMode.JWT]),
|
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
||||||
schema: {
|
schema: {
|
||||||
params: z.object({
|
params: z.object({
|
||||||
currentSlug: z.string().trim().describe(GROUPS.UPDATE.currentSlug)
|
id: z.string().trim().describe(GROUPS.GET_BY_ID.id)
|
||||||
|
}),
|
||||||
|
response: {
|
||||||
|
200: GroupsSchema
|
||||||
|
}
|
||||||
|
},
|
||||||
|
handler: async (req) => {
|
||||||
|
const group = await server.services.group.getGroupById({
|
||||||
|
actor: req.permission.type,
|
||||||
|
actorId: req.permission.id,
|
||||||
|
actorAuthMethod: req.permission.authMethod,
|
||||||
|
actorOrgId: req.permission.orgId,
|
||||||
|
id: req.params.id
|
||||||
|
});
|
||||||
|
|
||||||
|
return group;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
server.route({
|
||||||
|
url: "/",
|
||||||
|
method: "GET",
|
||||||
|
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
||||||
|
schema: {
|
||||||
|
response: {
|
||||||
|
200: GroupsSchema.array()
|
||||||
|
}
|
||||||
|
},
|
||||||
|
handler: async (req) => {
|
||||||
|
const groups = await server.services.org.getOrgGroups({
|
||||||
|
actor: req.permission.type,
|
||||||
|
actorId: req.permission.id,
|
||||||
|
orgId: req.permission.orgId,
|
||||||
|
actorAuthMethod: req.permission.authMethod,
|
||||||
|
actorOrgId: req.permission.orgId
|
||||||
|
});
|
||||||
|
|
||||||
|
return groups;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
server.route({
|
||||||
|
url: "/:id",
|
||||||
|
method: "PATCH",
|
||||||
|
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
||||||
|
schema: {
|
||||||
|
params: z.object({
|
||||||
|
id: z.string().trim().describe(GROUPS.UPDATE.id)
|
||||||
}),
|
}),
|
||||||
body: z
|
body: z
|
||||||
.object({
|
.object({
|
||||||
@ -70,7 +117,7 @@ export const registerGroupRouter = async (server: FastifyZodProvider) => {
|
|||||||
},
|
},
|
||||||
handler: async (req) => {
|
handler: async (req) => {
|
||||||
const group = await server.services.group.updateGroup({
|
const group = await server.services.group.updateGroup({
|
||||||
currentSlug: req.params.currentSlug,
|
id: req.params.id,
|
||||||
actor: req.permission.type,
|
actor: req.permission.type,
|
||||||
actorId: req.permission.id,
|
actorId: req.permission.id,
|
||||||
actorAuthMethod: req.permission.authMethod,
|
actorAuthMethod: req.permission.authMethod,
|
||||||
@ -83,12 +130,12 @@ export const registerGroupRouter = async (server: FastifyZodProvider) => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
server.route({
|
server.route({
|
||||||
url: "/:slug",
|
url: "/:id",
|
||||||
method: "DELETE",
|
method: "DELETE",
|
||||||
onRequest: verifyAuth([AuthMode.JWT]),
|
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
||||||
schema: {
|
schema: {
|
||||||
params: z.object({
|
params: z.object({
|
||||||
slug: z.string().trim().describe(GROUPS.DELETE.slug)
|
id: z.string().trim().describe(GROUPS.DELETE.id)
|
||||||
}),
|
}),
|
||||||
response: {
|
response: {
|
||||||
200: GroupsSchema
|
200: GroupsSchema
|
||||||
@ -96,7 +143,7 @@ export const registerGroupRouter = async (server: FastifyZodProvider) => {
|
|||||||
},
|
},
|
||||||
handler: async (req) => {
|
handler: async (req) => {
|
||||||
const group = await server.services.group.deleteGroup({
|
const group = await server.services.group.deleteGroup({
|
||||||
groupSlug: req.params.slug,
|
id: req.params.id,
|
||||||
actor: req.permission.type,
|
actor: req.permission.type,
|
||||||
actorId: req.permission.id,
|
actorId: req.permission.id,
|
||||||
actorAuthMethod: req.permission.authMethod,
|
actorAuthMethod: req.permission.authMethod,
|
||||||
@ -109,16 +156,17 @@ export const registerGroupRouter = async (server: FastifyZodProvider) => {
|
|||||||
|
|
||||||
server.route({
|
server.route({
|
||||||
method: "GET",
|
method: "GET",
|
||||||
url: "/:slug/users",
|
url: "/:id/users",
|
||||||
onRequest: verifyAuth([AuthMode.JWT]),
|
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
||||||
schema: {
|
schema: {
|
||||||
params: z.object({
|
params: z.object({
|
||||||
slug: z.string().trim().describe(GROUPS.LIST_USERS.slug)
|
id: z.string().trim().describe(GROUPS.LIST_USERS.id)
|
||||||
}),
|
}),
|
||||||
querystring: z.object({
|
querystring: z.object({
|
||||||
offset: z.coerce.number().min(0).max(100).default(0).describe(GROUPS.LIST_USERS.offset),
|
offset: z.coerce.number().min(0).max(100).default(0).describe(GROUPS.LIST_USERS.offset),
|
||||||
limit: z.coerce.number().min(1).max(100).default(10).describe(GROUPS.LIST_USERS.limit),
|
limit: z.coerce.number().min(1).max(100).default(10).describe(GROUPS.LIST_USERS.limit),
|
||||||
username: z.string().optional().describe(GROUPS.LIST_USERS.username)
|
username: z.string().trim().optional().describe(GROUPS.LIST_USERS.username),
|
||||||
|
search: z.string().trim().optional().describe(GROUPS.LIST_USERS.search)
|
||||||
}),
|
}),
|
||||||
response: {
|
response: {
|
||||||
200: z.object({
|
200: z.object({
|
||||||
@ -141,24 +189,25 @@ export const registerGroupRouter = async (server: FastifyZodProvider) => {
|
|||||||
},
|
},
|
||||||
handler: async (req) => {
|
handler: async (req) => {
|
||||||
const { users, totalCount } = await server.services.group.listGroupUsers({
|
const { users, totalCount } = await server.services.group.listGroupUsers({
|
||||||
groupSlug: req.params.slug,
|
id: req.params.id,
|
||||||
actor: req.permission.type,
|
actor: req.permission.type,
|
||||||
actorId: req.permission.id,
|
actorId: req.permission.id,
|
||||||
actorAuthMethod: req.permission.authMethod,
|
actorAuthMethod: req.permission.authMethod,
|
||||||
actorOrgId: req.permission.orgId,
|
actorOrgId: req.permission.orgId,
|
||||||
...req.query
|
...req.query
|
||||||
});
|
});
|
||||||
|
|
||||||
return { users, totalCount };
|
return { users, totalCount };
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
server.route({
|
server.route({
|
||||||
method: "POST",
|
method: "POST",
|
||||||
url: "/:slug/users/:username",
|
url: "/:id/users/:username",
|
||||||
onRequest: verifyAuth([AuthMode.JWT]),
|
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
||||||
schema: {
|
schema: {
|
||||||
params: z.object({
|
params: z.object({
|
||||||
slug: z.string().trim().describe(GROUPS.ADD_USER.slug),
|
id: z.string().trim().describe(GROUPS.ADD_USER.id),
|
||||||
username: z.string().trim().describe(GROUPS.ADD_USER.username)
|
username: z.string().trim().describe(GROUPS.ADD_USER.username)
|
||||||
}),
|
}),
|
||||||
response: {
|
response: {
|
||||||
@ -173,7 +222,7 @@ export const registerGroupRouter = async (server: FastifyZodProvider) => {
|
|||||||
},
|
},
|
||||||
handler: async (req) => {
|
handler: async (req) => {
|
||||||
const user = await server.services.group.addUserToGroup({
|
const user = await server.services.group.addUserToGroup({
|
||||||
groupSlug: req.params.slug,
|
id: req.params.id,
|
||||||
username: req.params.username,
|
username: req.params.username,
|
||||||
actor: req.permission.type,
|
actor: req.permission.type,
|
||||||
actorId: req.permission.id,
|
actorId: req.permission.id,
|
||||||
@ -187,11 +236,11 @@ export const registerGroupRouter = async (server: FastifyZodProvider) => {
|
|||||||
|
|
||||||
server.route({
|
server.route({
|
||||||
method: "DELETE",
|
method: "DELETE",
|
||||||
url: "/:slug/users/:username",
|
url: "/:id/users/:username",
|
||||||
onRequest: verifyAuth([AuthMode.JWT]),
|
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
||||||
schema: {
|
schema: {
|
||||||
params: z.object({
|
params: z.object({
|
||||||
slug: z.string().trim().describe(GROUPS.DELETE_USER.slug),
|
id: z.string().trim().describe(GROUPS.DELETE_USER.id),
|
||||||
username: z.string().trim().describe(GROUPS.DELETE_USER.username)
|
username: z.string().trim().describe(GROUPS.DELETE_USER.username)
|
||||||
}),
|
}),
|
||||||
response: {
|
response: {
|
||||||
@ -206,7 +255,7 @@ export const registerGroupRouter = async (server: FastifyZodProvider) => {
|
|||||||
},
|
},
|
||||||
handler: async (req) => {
|
handler: async (req) => {
|
||||||
const user = await server.services.group.removeUserFromGroup({
|
const user = await server.services.group.removeUserFromGroup({
|
||||||
groupSlug: req.params.slug,
|
id: req.params.id,
|
||||||
username: req.params.username,
|
username: req.params.username,
|
||||||
actor: req.permission.type,
|
actor: req.permission.type,
|
||||||
actorId: req.permission.id,
|
actorId: req.permission.id,
|
||||||
|
@ -5,7 +5,7 @@ import { z } from "zod";
|
|||||||
|
|
||||||
import { IdentityProjectAdditionalPrivilegeTemporaryMode } from "@app/ee/services/identity-project-additional-privilege/identity-project-additional-privilege-types";
|
import { IdentityProjectAdditionalPrivilegeTemporaryMode } from "@app/ee/services/identity-project-additional-privilege/identity-project-additional-privilege-types";
|
||||||
import { IDENTITY_ADDITIONAL_PRIVILEGE } from "@app/lib/api-docs";
|
import { IDENTITY_ADDITIONAL_PRIVILEGE } from "@app/lib/api-docs";
|
||||||
import { BadRequestError } from "@app/lib/errors";
|
import { UnauthorizedError } from "@app/lib/errors";
|
||||||
import { alphaNumericNanoId } from "@app/lib/nanoid";
|
import { alphaNumericNanoId } from "@app/lib/nanoid";
|
||||||
import { readLimit, writeLimit } from "@app/server/config/rateLimiter";
|
import { readLimit, writeLimit } from "@app/server/config/rateLimiter";
|
||||||
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
|
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
|
||||||
@ -61,7 +61,7 @@ export const registerIdentityProjectAdditionalPrivilegeRouter = async (server: F
|
|||||||
handler: async (req) => {
|
handler: async (req) => {
|
||||||
const { permissions, privilegePermission } = req.body;
|
const { permissions, privilegePermission } = req.body;
|
||||||
if (!permissions && !privilegePermission) {
|
if (!permissions && !privilegePermission) {
|
||||||
throw new BadRequestError({ message: "Permission or privilegePermission must be provided" });
|
throw new UnauthorizedError({ message: "Permission or privilegePermission must be provided" });
|
||||||
}
|
}
|
||||||
|
|
||||||
const permission = privilegePermission
|
const permission = privilegePermission
|
||||||
@ -140,7 +140,7 @@ export const registerIdentityProjectAdditionalPrivilegeRouter = async (server: F
|
|||||||
handler: async (req) => {
|
handler: async (req) => {
|
||||||
const { permissions, privilegePermission } = req.body;
|
const { permissions, privilegePermission } = req.body;
|
||||||
if (!permissions && !privilegePermission) {
|
if (!permissions && !privilegePermission) {
|
||||||
throw new BadRequestError({ message: "Permission or privilegePermission must be provided" });
|
throw new UnauthorizedError({ message: "Permission or privilegePermission must be provided" });
|
||||||
}
|
}
|
||||||
|
|
||||||
const permission = privilegePermission
|
const permission = privilegePermission
|
||||||
@ -224,7 +224,7 @@ export const registerIdentityProjectAdditionalPrivilegeRouter = async (server: F
|
|||||||
handler: async (req) => {
|
handler: async (req) => {
|
||||||
const { permissions, privilegePermission, ...updatedInfo } = req.body.privilegeDetails;
|
const { permissions, privilegePermission, ...updatedInfo } = req.body.privilegeDetails;
|
||||||
if (!permissions && !privilegePermission) {
|
if (!permissions && !privilegePermission) {
|
||||||
throw new BadRequestError({ message: "Permission or privilegePermission must be provided" });
|
throw new UnauthorizedError({ message: "Permission or privilegePermission must be provided" });
|
||||||
}
|
}
|
||||||
|
|
||||||
const permission = privilegePermission
|
const permission = privilegePermission
|
||||||
|
@ -3,10 +3,11 @@ import slugify from "@sindresorhus/slugify";
|
|||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
|
|
||||||
import { ProjectMembershipRole, ProjectMembershipsSchema, ProjectRolesSchema } from "@app/db/schemas";
|
import { ProjectMembershipRole, ProjectMembershipsSchema, ProjectRolesSchema } from "@app/db/schemas";
|
||||||
|
import { ProjectPermissionSchema } from "@app/ee/services/permission/project-permission";
|
||||||
import { PROJECT_ROLE } from "@app/lib/api-docs";
|
import { PROJECT_ROLE } from "@app/lib/api-docs";
|
||||||
import { readLimit, writeLimit } from "@app/server/config/rateLimiter";
|
import { readLimit, writeLimit } from "@app/server/config/rateLimiter";
|
||||||
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
|
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
|
||||||
import { ProjectPermissionSchema, SanitizedRoleSchema } from "@app/server/routes/sanitizedSchemas";
|
import { SanitizedRoleSchema } from "@app/server/routes/sanitizedSchemas";
|
||||||
import { AuthMode } from "@app/services/auth/auth-type";
|
import { AuthMode } from "@app/services/auth/auth-type";
|
||||||
|
|
||||||
export const registerProjectRoleRouter = async (server: FastifyZodProvider) => {
|
export const registerProjectRoleRouter = async (server: FastifyZodProvider) => {
|
||||||
|
@ -87,6 +87,12 @@ export const registerProjectRouter = async (server: FastifyZodProvider) => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Daniel: This endpoint is no longer is use.
|
||||||
|
* We are keeping it for now because it has been exposed in our public api docs for a while, so by removing it we are likely to break users workflows.
|
||||||
|
*
|
||||||
|
* Please refer to the new endpoint, GET /api/v1/organization/audit-logs, for the same (and more) functionality.
|
||||||
|
*/
|
||||||
server.route({
|
server.route({
|
||||||
method: "GET",
|
method: "GET",
|
||||||
url: "/:workspaceId/audit-logs",
|
url: "/:workspaceId/audit-logs",
|
||||||
@ -101,7 +107,7 @@ export const registerProjectRouter = async (server: FastifyZodProvider) => {
|
|||||||
}
|
}
|
||||||
],
|
],
|
||||||
params: z.object({
|
params: z.object({
|
||||||
workspaceId: z.string().trim().describe(AUDIT_LOGS.EXPORT.workspaceId)
|
workspaceId: z.string().trim().describe(AUDIT_LOGS.EXPORT.projectId)
|
||||||
}),
|
}),
|
||||||
querystring: z.object({
|
querystring: z.object({
|
||||||
eventType: z.nativeEnum(EventType).optional().describe(AUDIT_LOGS.EXPORT.eventType),
|
eventType: z.nativeEnum(EventType).optional().describe(AUDIT_LOGS.EXPORT.eventType),
|
||||||
@ -122,10 +128,12 @@ export const registerProjectRouter = async (server: FastifyZodProvider) => {
|
|||||||
})
|
})
|
||||||
.merge(
|
.merge(
|
||||||
z.object({
|
z.object({
|
||||||
project: z.object({
|
project: z
|
||||||
name: z.string(),
|
.object({
|
||||||
slug: z.string()
|
name: z.string(),
|
||||||
}),
|
slug: z.string()
|
||||||
|
})
|
||||||
|
.optional(),
|
||||||
event: z.object({
|
event: z.object({
|
||||||
type: z.string(),
|
type: z.string(),
|
||||||
metadata: z.any()
|
metadata: z.any()
|
||||||
@ -195,7 +203,7 @@ export const registerProjectRouter = async (server: FastifyZodProvider) => {
|
|||||||
200: z.object({
|
200: z.object({
|
||||||
secretManagerKmsKey: z.object({
|
secretManagerKmsKey: z.object({
|
||||||
id: z.string(),
|
id: z.string(),
|
||||||
slug: z.string(),
|
name: z.string(),
|
||||||
isExternal: z.boolean()
|
isExternal: z.boolean()
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
@ -235,7 +243,7 @@ export const registerProjectRouter = async (server: FastifyZodProvider) => {
|
|||||||
200: z.object({
|
200: z.object({
|
||||||
secretManagerKmsKey: z.object({
|
secretManagerKmsKey: z.object({
|
||||||
id: z.string(),
|
id: z.string(),
|
||||||
slug: z.string(),
|
name: z.string(),
|
||||||
isExternal: z.boolean()
|
isExternal: z.boolean()
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
@ -260,7 +268,7 @@ export const registerProjectRouter = async (server: FastifyZodProvider) => {
|
|||||||
metadata: {
|
metadata: {
|
||||||
secretManagerKmsKey: {
|
secretManagerKmsKey: {
|
||||||
id: secretManagerKmsKey.id,
|
id: secretManagerKmsKey.id,
|
||||||
slug: secretManagerKmsKey.slug
|
name: secretManagerKmsKey.name
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -328,7 +336,7 @@ export const registerProjectRouter = async (server: FastifyZodProvider) => {
|
|||||||
200: z.object({
|
200: z.object({
|
||||||
secretManagerKmsKey: z.object({
|
secretManagerKmsKey: z.object({
|
||||||
id: z.string(),
|
id: z.string(),
|
||||||
slug: z.string(),
|
name: z.string(),
|
||||||
isExternal: z.boolean()
|
isExternal: z.boolean()
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
|
|
||||||
import { RateLimitSchema } from "@app/db/schemas";
|
import { RateLimitSchema } from "@app/db/schemas";
|
||||||
import { BadRequestError } from "@app/lib/errors";
|
import { NotFoundError } from "@app/lib/errors";
|
||||||
import { readLimit } from "@app/server/config/rateLimiter";
|
import { readLimit } from "@app/server/config/rateLimiter";
|
||||||
import { verifySuperAdmin } from "@app/server/plugins/auth/superAdmin";
|
import { verifySuperAdmin } from "@app/server/plugins/auth/superAdmin";
|
||||||
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
|
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
|
||||||
@ -29,7 +29,7 @@ export const registerRateLimitRouter = async (server: FastifyZodProvider) => {
|
|||||||
handler: async () => {
|
handler: async () => {
|
||||||
const rateLimit = await server.services.rateLimit.getRateLimits();
|
const rateLimit = await server.services.rateLimit.getRateLimits();
|
||||||
if (!rateLimit) {
|
if (!rateLimit) {
|
||||||
throw new BadRequestError({
|
throw new NotFoundError({
|
||||||
name: "Get Rate Limit Error",
|
name: "Get Rate Limit Error",
|
||||||
message: "Rate limit configuration does not exist."
|
message: "Rate limit configuration does not exist."
|
||||||
});
|
});
|
||||||
|
@ -61,7 +61,7 @@ export const registerSamlRouter = async (server: FastifyZodProvider) => {
|
|||||||
id: samlConfigId
|
id: samlConfigId
|
||||||
};
|
};
|
||||||
} else {
|
} else {
|
||||||
throw new BadRequestError({ message: "Missing sso identitier or org slug" });
|
throw new BadRequestError({ message: "Missing sso identifier or org slug" });
|
||||||
}
|
}
|
||||||
|
|
||||||
const ssoConfig = await server.services.saml.getSaml(ssoLookupDetails);
|
const ssoConfig = await server.services.saml.getSaml(ssoLookupDetails);
|
||||||
@ -100,6 +100,7 @@ export const registerSamlRouter = async (server: FastifyZodProvider) => {
|
|||||||
async (req, profile, cb) => {
|
async (req, profile, cb) => {
|
||||||
try {
|
try {
|
||||||
if (!profile) throw new BadRequestError({ message: "Missing profile" });
|
if (!profile) throw new BadRequestError({ message: "Missing profile" });
|
||||||
|
|
||||||
const email =
|
const email =
|
||||||
profile?.email ??
|
profile?.email ??
|
||||||
// entra sends data in this format
|
// entra sends data in this format
|
||||||
@ -123,6 +124,17 @@ export const registerSamlRouter = async (server: FastifyZodProvider) => {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const userMetadata = Object.keys(profile.attributes || {})
|
||||||
|
.map((key) => {
|
||||||
|
// for the ones like in format: http://schemas.xmlsoap.org/ws/2005/05/identity/claims/email
|
||||||
|
const formatedKey = key.startsWith("http") ? key.split("/").at(-1) || "" : key;
|
||||||
|
return {
|
||||||
|
key: formatedKey,
|
||||||
|
value: String((profile.attributes as Record<string, string>)[key]).substring(0, 1020)
|
||||||
|
};
|
||||||
|
})
|
||||||
|
.filter((el) => el.key && !["email", "firstName", "lastName"].includes(el.key));
|
||||||
|
|
||||||
const { isUserCompleted, providerAuthToken } = await server.services.saml.samlLogin({
|
const { isUserCompleted, providerAuthToken } = await server.services.saml.samlLogin({
|
||||||
externalId: profile.nameID,
|
externalId: profile.nameID,
|
||||||
email,
|
email,
|
||||||
@ -130,7 +142,8 @@ export const registerSamlRouter = async (server: FastifyZodProvider) => {
|
|||||||
lastName: lastName as string,
|
lastName: lastName as string,
|
||||||
relayState: (req.body as { RelayState?: string }).RelayState,
|
relayState: (req.body as { RelayState?: string }).RelayState,
|
||||||
authProvider: (req as unknown as FastifyRequest).ssoConfig?.authProvider as string,
|
authProvider: (req as unknown as FastifyRequest).ssoConfig?.authProvider as string,
|
||||||
orgId: (req as unknown as FastifyRequest).ssoConfig?.orgId as string
|
orgId: (req as unknown as FastifyRequest).ssoConfig?.orgId as string,
|
||||||
|
metadata: userMetadata
|
||||||
});
|
});
|
||||||
cb(null, { isUserCompleted, providerAuthToken });
|
cb(null, { isUserCompleted, providerAuthToken });
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
@ -20,7 +20,7 @@ const ScimUserSchema = z.object({
|
|||||||
z.object({
|
z.object({
|
||||||
primary: z.boolean(),
|
primary: z.boolean(),
|
||||||
value: z.string().email(),
|
value: z.string().email(),
|
||||||
type: z.string().trim()
|
type: z.string().trim().default("work")
|
||||||
})
|
})
|
||||||
)
|
)
|
||||||
.optional(),
|
.optional(),
|
||||||
@ -210,8 +210,7 @@ export const registerScimRouter = async (server: FastifyZodProvider) => {
|
|||||||
.array(
|
.array(
|
||||||
z.object({
|
z.object({
|
||||||
primary: z.boolean(),
|
primary: z.boolean(),
|
||||||
value: z.string().email(),
|
value: z.string().email()
|
||||||
type: z.string().trim()
|
|
||||||
})
|
})
|
||||||
)
|
)
|
||||||
.optional(),
|
.optional(),
|
||||||
@ -281,8 +280,7 @@ export const registerScimRouter = async (server: FastifyZodProvider) => {
|
|||||||
.array(
|
.array(
|
||||||
z.object({
|
z.object({
|
||||||
primary: z.boolean(),
|
primary: z.boolean(),
|
||||||
value: z.string().email(),
|
value: z.string().email()
|
||||||
type: z.string().trim()
|
|
||||||
})
|
})
|
||||||
)
|
)
|
||||||
.optional(),
|
.optional(),
|
||||||
@ -301,7 +299,7 @@ export const registerScimRouter = async (server: FastifyZodProvider) => {
|
|||||||
z.object({
|
z.object({
|
||||||
primary: z.boolean(),
|
primary: z.boolean(),
|
||||||
value: z.string().email(),
|
value: z.string().email(),
|
||||||
type: z.string().trim()
|
type: z.string().trim().default("work")
|
||||||
})
|
})
|
||||||
),
|
),
|
||||||
displayName: z.string().trim(),
|
displayName: z.string().trim(),
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
import { nanoid } from "nanoid";
|
import { nanoid } from "nanoid";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
|
|
||||||
|
import { ApproverType } from "@app/ee/services/access-approval-policy/access-approval-policy-types";
|
||||||
import { removeTrailingSlash } from "@app/lib/fn";
|
import { removeTrailingSlash } from "@app/lib/fn";
|
||||||
import { EnforcementLevel } from "@app/lib/types";
|
import { EnforcementLevel } from "@app/lib/types";
|
||||||
import { readLimit, writeLimit } from "@app/server/config/rateLimiter";
|
import { readLimit, writeLimit } from "@app/server/config/rateLimiter";
|
||||||
@ -16,32 +17,33 @@ export const registerSecretApprovalPolicyRouter = async (server: FastifyZodProvi
|
|||||||
rateLimit: writeLimit
|
rateLimit: writeLimit
|
||||||
},
|
},
|
||||||
schema: {
|
schema: {
|
||||||
body: z
|
body: z.object({
|
||||||
.object({
|
workspaceId: z.string(),
|
||||||
workspaceId: z.string(),
|
name: z.string().optional(),
|
||||||
name: z.string().optional(),
|
environment: z.string(),
|
||||||
environment: z.string(),
|
secretPath: z
|
||||||
secretPath: z
|
.string()
|
||||||
.string()
|
.optional()
|
||||||
.optional()
|
.nullable()
|
||||||
.nullable()
|
.default("/")
|
||||||
.default("/")
|
.transform((val) => (val ? removeTrailingSlash(val) : val)),
|
||||||
.transform((val) => (val ? removeTrailingSlash(val) : val)),
|
approvers: z
|
||||||
approvers: z.string().array().min(1),
|
.discriminatedUnion("type", [
|
||||||
approvals: z.number().min(1).default(1),
|
z.object({ type: z.literal(ApproverType.Group), id: z.string() }),
|
||||||
enforcementLevel: z.nativeEnum(EnforcementLevel).default(EnforcementLevel.Hard)
|
z.object({ type: z.literal(ApproverType.User), id: z.string().optional(), name: z.string().optional() })
|
||||||
})
|
])
|
||||||
.refine((data) => data.approvals <= data.approvers.length, {
|
.array()
|
||||||
path: ["approvals"],
|
.min(1, { message: "At least one approver should be provided" }),
|
||||||
message: "The number of approvals should be lower than the number of approvers."
|
approvals: z.number().min(1).default(1),
|
||||||
}),
|
enforcementLevel: z.nativeEnum(EnforcementLevel).default(EnforcementLevel.Hard)
|
||||||
|
}),
|
||||||
response: {
|
response: {
|
||||||
200: z.object({
|
200: z.object({
|
||||||
approval: sapPubSchema
|
approval: sapPubSchema
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
onRequest: verifyAuth([AuthMode.JWT]),
|
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
||||||
handler: async (req) => {
|
handler: async (req) => {
|
||||||
const approval = await server.services.secretApprovalPolicy.createSecretApprovalPolicy({
|
const approval = await server.services.secretApprovalPolicy.createSecretApprovalPolicy({
|
||||||
actor: req.permission.type,
|
actor: req.permission.type,
|
||||||
@ -67,30 +69,31 @@ export const registerSecretApprovalPolicyRouter = async (server: FastifyZodProvi
|
|||||||
params: z.object({
|
params: z.object({
|
||||||
sapId: z.string()
|
sapId: z.string()
|
||||||
}),
|
}),
|
||||||
body: z
|
body: z.object({
|
||||||
.object({
|
name: z.string().optional(),
|
||||||
name: z.string().optional(),
|
approvers: z
|
||||||
approvers: z.string().array().min(1),
|
.discriminatedUnion("type", [
|
||||||
approvals: z.number().min(1).default(1),
|
z.object({ type: z.literal(ApproverType.Group), id: z.string() }),
|
||||||
secretPath: z
|
z.object({ type: z.literal(ApproverType.User), id: z.string().optional(), name: z.string().optional() })
|
||||||
.string()
|
])
|
||||||
.optional()
|
.array()
|
||||||
.nullable()
|
.min(1, { message: "At least one approver should be provided" }),
|
||||||
.transform((val) => (val ? removeTrailingSlash(val) : val))
|
approvals: z.number().min(1).default(1),
|
||||||
.transform((val) => (val === "" ? "/" : val)),
|
secretPath: z
|
||||||
enforcementLevel: z.nativeEnum(EnforcementLevel).optional()
|
.string()
|
||||||
})
|
.optional()
|
||||||
.refine((data) => data.approvals <= data.approvers.length, {
|
.nullable()
|
||||||
path: ["approvals"],
|
.transform((val) => (val ? removeTrailingSlash(val) : val))
|
||||||
message: "The number of approvals should be lower than the number of approvers."
|
.transform((val) => (val === "" ? "/" : val)),
|
||||||
}),
|
enforcementLevel: z.nativeEnum(EnforcementLevel).optional()
|
||||||
|
}),
|
||||||
response: {
|
response: {
|
||||||
200: z.object({
|
200: z.object({
|
||||||
approval: sapPubSchema
|
approval: sapPubSchema
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
onRequest: verifyAuth([AuthMode.JWT]),
|
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
||||||
handler: async (req) => {
|
handler: async (req) => {
|
||||||
const approval = await server.services.secretApprovalPolicy.updateSecretApprovalPolicy({
|
const approval = await server.services.secretApprovalPolicy.updateSecretApprovalPolicy({
|
||||||
actor: req.permission.type,
|
actor: req.permission.type,
|
||||||
@ -120,7 +123,7 @@ export const registerSecretApprovalPolicyRouter = async (server: FastifyZodProvi
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
onRequest: verifyAuth([AuthMode.JWT]),
|
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
||||||
handler: async (req) => {
|
handler: async (req) => {
|
||||||
const approval = await server.services.secretApprovalPolicy.deleteSecretApprovalPolicy({
|
const approval = await server.services.secretApprovalPolicy.deleteSecretApprovalPolicy({
|
||||||
actor: req.permission.type,
|
actor: req.permission.type,
|
||||||
@ -147,9 +150,10 @@ export const registerSecretApprovalPolicyRouter = async (server: FastifyZodProvi
|
|||||||
200: z.object({
|
200: z.object({
|
||||||
approvals: sapPubSchema
|
approvals: sapPubSchema
|
||||||
.extend({
|
.extend({
|
||||||
userApprovers: z
|
approvers: z
|
||||||
.object({
|
.object({
|
||||||
userId: z.string()
|
id: z.string().nullable().optional(),
|
||||||
|
type: z.nativeEnum(ApproverType)
|
||||||
})
|
})
|
||||||
.array()
|
.array()
|
||||||
})
|
})
|
||||||
@ -170,6 +174,44 @@ export const registerSecretApprovalPolicyRouter = async (server: FastifyZodProvi
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
server.route({
|
||||||
|
url: "/:sapId",
|
||||||
|
method: "GET",
|
||||||
|
config: {
|
||||||
|
rateLimit: readLimit
|
||||||
|
},
|
||||||
|
schema: {
|
||||||
|
params: z.object({
|
||||||
|
sapId: z.string()
|
||||||
|
}),
|
||||||
|
response: {
|
||||||
|
200: z.object({
|
||||||
|
approval: sapPubSchema.extend({
|
||||||
|
approvers: z
|
||||||
|
.object({
|
||||||
|
id: z.string().nullable().optional(),
|
||||||
|
type: z.nativeEnum(ApproverType),
|
||||||
|
name: z.string().nullable().optional()
|
||||||
|
})
|
||||||
|
.array()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
||||||
|
handler: async (req) => {
|
||||||
|
const approval = await server.services.secretApprovalPolicy.getSecretApprovalPolicyById({
|
||||||
|
actor: req.permission.type,
|
||||||
|
actorId: req.permission.id,
|
||||||
|
actorAuthMethod: req.permission.authMethod,
|
||||||
|
actorOrgId: req.permission.orgId,
|
||||||
|
...req.params
|
||||||
|
});
|
||||||
|
|
||||||
|
return { approval };
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
server.route({
|
server.route({
|
||||||
url: "/board",
|
url: "/board",
|
||||||
method: "GET",
|
method: "GET",
|
||||||
@ -186,7 +228,7 @@ export const registerSecretApprovalPolicyRouter = async (server: FastifyZodProvi
|
|||||||
200: z.object({
|
200: z.object({
|
||||||
policy: sapPubSchema
|
policy: sapPubSchema
|
||||||
.extend({
|
.extend({
|
||||||
userApprovers: z.object({ userId: z.string() }).array()
|
userApprovers: z.object({ userId: z.string().nullable().optional() }).array()
|
||||||
})
|
})
|
||||||
.optional()
|
.optional()
|
||||||
})
|
})
|
||||||
|
@ -13,7 +13,7 @@ import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
|
|||||||
import { secretRawSchema } from "@app/server/routes/sanitizedSchemas";
|
import { secretRawSchema } from "@app/server/routes/sanitizedSchemas";
|
||||||
import { AuthMode } from "@app/services/auth/auth-type";
|
import { AuthMode } from "@app/services/auth/auth-type";
|
||||||
|
|
||||||
const approvalRequestUser = z.object({ userId: z.string() }).merge(
|
const approvalRequestUser = z.object({ userId: z.string().nullable().optional() }).merge(
|
||||||
UsersSchema.pick({
|
UsersSchema.pick({
|
||||||
email: true,
|
email: true,
|
||||||
firstName: true,
|
firstName: true,
|
||||||
@ -46,7 +46,11 @@ export const registerSecretApprovalRequestRouter = async (server: FastifyZodProv
|
|||||||
id: z.string(),
|
id: z.string(),
|
||||||
name: z.string(),
|
name: z.string(),
|
||||||
approvals: z.number(),
|
approvals: z.number(),
|
||||||
approvers: z.string().array(),
|
approvers: z
|
||||||
|
.object({
|
||||||
|
userId: z.string().nullable().optional()
|
||||||
|
})
|
||||||
|
.array(),
|
||||||
secretPath: z.string().optional().nullable(),
|
secretPath: z.string().optional().nullable(),
|
||||||
enforcementLevel: z.string()
|
enforcementLevel: z.string()
|
||||||
}),
|
}),
|
||||||
@ -54,7 +58,11 @@ export const registerSecretApprovalRequestRouter = async (server: FastifyZodProv
|
|||||||
commits: z.object({ op: z.string(), secretId: z.string().nullable().optional() }).array(),
|
commits: z.object({ op: z.string(), secretId: z.string().nullable().optional() }).array(),
|
||||||
environment: z.string(),
|
environment: z.string(),
|
||||||
reviewers: z.object({ userId: z.string(), status: z.string() }).array(),
|
reviewers: z.object({ userId: z.string(), status: z.string() }).array(),
|
||||||
approvers: z.string().array()
|
approvers: z
|
||||||
|
.object({
|
||||||
|
userId: z.string().nullable().optional()
|
||||||
|
})
|
||||||
|
.array()
|
||||||
}).array()
|
}).array()
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
@ -2,6 +2,8 @@ import { z } from "zod";
|
|||||||
|
|
||||||
import { GitAppOrgSchema, SecretScanningGitRisksSchema } from "@app/db/schemas";
|
import { GitAppOrgSchema, SecretScanningGitRisksSchema } from "@app/db/schemas";
|
||||||
import { SecretScanningRiskStatus } from "@app/ee/services/secret-scanning/secret-scanning-types";
|
import { SecretScanningRiskStatus } from "@app/ee/services/secret-scanning/secret-scanning-types";
|
||||||
|
import { getConfig } from "@app/lib/config/env";
|
||||||
|
import { BadRequestError } from "@app/lib/errors";
|
||||||
import { readLimit, writeLimit } from "@app/server/config/rateLimiter";
|
import { readLimit, writeLimit } from "@app/server/config/rateLimiter";
|
||||||
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
|
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
|
||||||
import { AuthMode } from "@app/services/auth/auth-type";
|
import { AuthMode } from "@app/services/auth/auth-type";
|
||||||
@ -23,6 +25,13 @@ export const registerSecretScanningRouter = async (server: FastifyZodProvider) =
|
|||||||
},
|
},
|
||||||
onRequest: verifyAuth([AuthMode.JWT]),
|
onRequest: verifyAuth([AuthMode.JWT]),
|
||||||
handler: async (req) => {
|
handler: async (req) => {
|
||||||
|
const appCfg = getConfig();
|
||||||
|
if (!appCfg.SECRET_SCANNING_ORG_WHITELIST?.includes(req.auth.orgId)) {
|
||||||
|
throw new BadRequestError({
|
||||||
|
message: "Secret scanning is temporarily unavailable."
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
const session = await server.services.secretScanning.createInstallationSession({
|
const session = await server.services.secretScanning.createInstallationSession({
|
||||||
actor: req.permission.type,
|
actor: req.permission.type,
|
||||||
actorId: req.permission.id,
|
actorId: req.permission.id,
|
||||||
@ -30,6 +39,7 @@ export const registerSecretScanningRouter = async (server: FastifyZodProvider) =
|
|||||||
actorOrgId: req.permission.orgId,
|
actorOrgId: req.permission.orgId,
|
||||||
orgId: req.body.organizationId
|
orgId: req.body.organizationId
|
||||||
});
|
});
|
||||||
|
|
||||||
return session;
|
return session;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
@ -5,22 +5,38 @@ import { AccessApprovalPoliciesSchema, TableName, TAccessApprovalPolicies } from
|
|||||||
import { DatabaseError } from "@app/lib/errors";
|
import { DatabaseError } from "@app/lib/errors";
|
||||||
import { buildFindFilter, ormify, selectAllTableCols, sqlNestRelationships, TFindFilter } from "@app/lib/knex";
|
import { buildFindFilter, ormify, selectAllTableCols, sqlNestRelationships, TFindFilter } from "@app/lib/knex";
|
||||||
|
|
||||||
|
import { ApproverType } from "./access-approval-policy-types";
|
||||||
|
|
||||||
export type TAccessApprovalPolicyDALFactory = ReturnType<typeof accessApprovalPolicyDALFactory>;
|
export type TAccessApprovalPolicyDALFactory = ReturnType<typeof accessApprovalPolicyDALFactory>;
|
||||||
|
|
||||||
export const accessApprovalPolicyDALFactory = (db: TDbClient) => {
|
export const accessApprovalPolicyDALFactory = (db: TDbClient) => {
|
||||||
const accessApprovalPolicyOrm = ormify(db, TableName.AccessApprovalPolicy);
|
const accessApprovalPolicyOrm = ormify(db, TableName.AccessApprovalPolicy);
|
||||||
|
|
||||||
const accessApprovalPolicyFindQuery = async (tx: Knex, filter: TFindFilter<TAccessApprovalPolicies>) => {
|
const accessApprovalPolicyFindQuery = async (
|
||||||
|
tx: Knex,
|
||||||
|
filter: TFindFilter<TAccessApprovalPolicies>,
|
||||||
|
customFilter?: {
|
||||||
|
policyId?: string;
|
||||||
|
}
|
||||||
|
) => {
|
||||||
const result = await tx(TableName.AccessApprovalPolicy)
|
const result = await tx(TableName.AccessApprovalPolicy)
|
||||||
// eslint-disable-next-line
|
// eslint-disable-next-line
|
||||||
.where(buildFindFilter(filter))
|
.where(buildFindFilter(filter))
|
||||||
|
.where((qb) => {
|
||||||
|
if (customFilter?.policyId) {
|
||||||
|
void qb.where(`${TableName.AccessApprovalPolicy}.id`, "=", customFilter.policyId);
|
||||||
|
}
|
||||||
|
})
|
||||||
.join(TableName.Environment, `${TableName.AccessApprovalPolicy}.envId`, `${TableName.Environment}.id`)
|
.join(TableName.Environment, `${TableName.AccessApprovalPolicy}.envId`, `${TableName.Environment}.id`)
|
||||||
.leftJoin(
|
.leftJoin(
|
||||||
TableName.AccessApprovalPolicyApprover,
|
TableName.AccessApprovalPolicyApprover,
|
||||||
`${TableName.AccessApprovalPolicy}.id`,
|
`${TableName.AccessApprovalPolicy}.id`,
|
||||||
`${TableName.AccessApprovalPolicyApprover}.policyId`
|
`${TableName.AccessApprovalPolicyApprover}.policyId`
|
||||||
)
|
)
|
||||||
|
.leftJoin(TableName.Users, `${TableName.AccessApprovalPolicyApprover}.approverUserId`, `${TableName.Users}.id`)
|
||||||
|
.select(tx.ref("username").withSchema(TableName.Users).as("approverUsername"))
|
||||||
.select(tx.ref("approverUserId").withSchema(TableName.AccessApprovalPolicyApprover))
|
.select(tx.ref("approverUserId").withSchema(TableName.AccessApprovalPolicyApprover))
|
||||||
|
.select(tx.ref("approverGroupId").withSchema(TableName.AccessApprovalPolicyApprover))
|
||||||
.select(tx.ref("name").withSchema(TableName.Environment).as("envName"))
|
.select(tx.ref("name").withSchema(TableName.Environment).as("envName"))
|
||||||
.select(tx.ref("slug").withSchema(TableName.Environment).as("envSlug"))
|
.select(tx.ref("slug").withSchema(TableName.Environment).as("envSlug"))
|
||||||
.select(tx.ref("id").withSchema(TableName.Environment).as("envId"))
|
.select(tx.ref("id").withSchema(TableName.Environment).as("envId"))
|
||||||
@ -30,10 +46,10 @@ export const accessApprovalPolicyDALFactory = (db: TDbClient) => {
|
|||||||
return result;
|
return result;
|
||||||
};
|
};
|
||||||
|
|
||||||
const findById = async (id: string, tx?: Knex) => {
|
const findById = async (policyId: string, tx?: Knex) => {
|
||||||
try {
|
try {
|
||||||
const doc = await accessApprovalPolicyFindQuery(tx || db.replicaNode(), {
|
const doc = await accessApprovalPolicyFindQuery(tx || db.replicaNode(), {
|
||||||
[`${TableName.AccessApprovalPolicy}.id` as "id"]: id
|
[`${TableName.AccessApprovalPolicy}.id` as "id"]: policyId
|
||||||
});
|
});
|
||||||
const formattedDoc = sqlNestRelationships({
|
const formattedDoc = sqlNestRelationships({
|
||||||
data: doc,
|
data: doc,
|
||||||
@ -50,9 +66,18 @@ export const accessApprovalPolicyDALFactory = (db: TDbClient) => {
|
|||||||
childrenMapper: [
|
childrenMapper: [
|
||||||
{
|
{
|
||||||
key: "approverUserId",
|
key: "approverUserId",
|
||||||
label: "userApprovers" as const,
|
label: "approvers" as const,
|
||||||
mapper: ({ approverUserId }) => ({
|
mapper: ({ approverUserId: id }) => ({
|
||||||
userId: approverUserId
|
id,
|
||||||
|
type: "user"
|
||||||
|
})
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "approverGroupId",
|
||||||
|
label: "approvers" as const,
|
||||||
|
mapper: ({ approverGroupId: id }) => ({
|
||||||
|
id,
|
||||||
|
type: "group"
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
@ -64,9 +89,15 @@ export const accessApprovalPolicyDALFactory = (db: TDbClient) => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const find = async (filter: TFindFilter<TAccessApprovalPolicies & { projectId: string }>, tx?: Knex) => {
|
const find = async (
|
||||||
|
filter: TFindFilter<TAccessApprovalPolicies & { projectId: string }>,
|
||||||
|
customFilter?: {
|
||||||
|
policyId?: string;
|
||||||
|
},
|
||||||
|
tx?: Knex
|
||||||
|
) => {
|
||||||
try {
|
try {
|
||||||
const docs = await accessApprovalPolicyFindQuery(tx || db.replicaNode(), filter);
|
const docs = await accessApprovalPolicyFindQuery(tx || db.replicaNode(), filter, customFilter);
|
||||||
|
|
||||||
const formattedDocs = sqlNestRelationships({
|
const formattedDocs = sqlNestRelationships({
|
||||||
data: docs,
|
data: docs,
|
||||||
@ -84,9 +115,19 @@ export const accessApprovalPolicyDALFactory = (db: TDbClient) => {
|
|||||||
childrenMapper: [
|
childrenMapper: [
|
||||||
{
|
{
|
||||||
key: "approverUserId",
|
key: "approverUserId",
|
||||||
label: "userApprovers" as const,
|
label: "approvers" as const,
|
||||||
mapper: ({ approverUserId }) => ({
|
mapper: ({ approverUserId: id, approverUsername }) => ({
|
||||||
userId: approverUserId
|
id,
|
||||||
|
type: ApproverType.User,
|
||||||
|
name: approverUsername
|
||||||
|
})
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "approverGroupId",
|
||||||
|
label: "approvers" as const,
|
||||||
|
mapper: ({ approverGroupId: id }) => ({
|
||||||
|
id,
|
||||||
|
type: ApproverType.Group
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
@ -1,12 +1,11 @@
|
|||||||
import { ForbiddenError, subject } from "@casl/ability";
|
import { ForbiddenError, subject } from "@casl/ability";
|
||||||
|
|
||||||
import { BadRequestError } from "@app/lib/errors";
|
|
||||||
import { ActorType } from "@app/services/auth/auth-type";
|
import { ActorType } from "@app/services/auth/auth-type";
|
||||||
|
|
||||||
import { ProjectPermissionActions, ProjectPermissionSub } from "../permission/project-permission";
|
import { ProjectPermissionActions, ProjectPermissionSub } from "../permission/project-permission";
|
||||||
import { TVerifyApprovers } from "./access-approval-policy-types";
|
import { TIsApproversValid } from "./access-approval-policy-types";
|
||||||
|
|
||||||
export const verifyApprovers = async ({
|
export const isApproversValid = async ({
|
||||||
userIds,
|
userIds,
|
||||||
projectId,
|
projectId,
|
||||||
orgId,
|
orgId,
|
||||||
@ -14,9 +13,9 @@ export const verifyApprovers = async ({
|
|||||||
actorAuthMethod,
|
actorAuthMethod,
|
||||||
secretPath,
|
secretPath,
|
||||||
permissionService
|
permissionService
|
||||||
}: TVerifyApprovers) => {
|
}: TIsApproversValid) => {
|
||||||
for await (const userId of userIds) {
|
try {
|
||||||
try {
|
for await (const userId of userIds) {
|
||||||
const { permission: approverPermission } = await permissionService.getProjectPermission(
|
const { permission: approverPermission } = await permissionService.getProjectPermission(
|
||||||
ActorType.USER,
|
ActorType.USER,
|
||||||
userId,
|
userId,
|
||||||
@ -29,8 +28,9 @@ export const verifyApprovers = async ({
|
|||||||
ProjectPermissionActions.Create,
|
ProjectPermissionActions.Create,
|
||||||
subject(ProjectPermissionSub.Secrets, { environment: envSlug, secretPath })
|
subject(ProjectPermissionSub.Secrets, { environment: envSlug, secretPath })
|
||||||
);
|
);
|
||||||
} catch (err) {
|
|
||||||
throw new BadRequestError({ message: "One or more approvers doesn't have access to be specified secret path" });
|
|
||||||
}
|
}
|
||||||
|
} catch {
|
||||||
|
return false;
|
||||||
}
|
}
|
||||||
|
return true;
|
||||||
};
|
};
|
||||||
|
@ -2,17 +2,21 @@ import { ForbiddenError } from "@casl/ability";
|
|||||||
|
|
||||||
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service";
|
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service";
|
||||||
import { ProjectPermissionActions, ProjectPermissionSub } from "@app/ee/services/permission/project-permission";
|
import { ProjectPermissionActions, ProjectPermissionSub } from "@app/ee/services/permission/project-permission";
|
||||||
import { BadRequestError } from "@app/lib/errors";
|
import { BadRequestError, ForbiddenRequestError, NotFoundError } from "@app/lib/errors";
|
||||||
import { TProjectDALFactory } from "@app/services/project/project-dal";
|
import { TProjectDALFactory } from "@app/services/project/project-dal";
|
||||||
import { TProjectEnvDALFactory } from "@app/services/project-env/project-env-dal";
|
import { TProjectEnvDALFactory } from "@app/services/project-env/project-env-dal";
|
||||||
import { TProjectMembershipDALFactory } from "@app/services/project-membership/project-membership-dal";
|
import { TProjectMembershipDALFactory } from "@app/services/project-membership/project-membership-dal";
|
||||||
|
import { TUserDALFactory } from "@app/services/user/user-dal";
|
||||||
|
|
||||||
|
import { TGroupDALFactory } from "../group/group-dal";
|
||||||
import { TAccessApprovalPolicyApproverDALFactory } from "./access-approval-policy-approver-dal";
|
import { TAccessApprovalPolicyApproverDALFactory } from "./access-approval-policy-approver-dal";
|
||||||
import { TAccessApprovalPolicyDALFactory } from "./access-approval-policy-dal";
|
import { TAccessApprovalPolicyDALFactory } from "./access-approval-policy-dal";
|
||||||
import { verifyApprovers } from "./access-approval-policy-fns";
|
import { isApproversValid } from "./access-approval-policy-fns";
|
||||||
import {
|
import {
|
||||||
|
ApproverType,
|
||||||
TCreateAccessApprovalPolicy,
|
TCreateAccessApprovalPolicy,
|
||||||
TDeleteAccessApprovalPolicy,
|
TDeleteAccessApprovalPolicy,
|
||||||
|
TGetAccessApprovalPolicyByIdDTO,
|
||||||
TGetAccessPolicyCountByEnvironmentDTO,
|
TGetAccessPolicyCountByEnvironmentDTO,
|
||||||
TListAccessApprovalPoliciesDTO,
|
TListAccessApprovalPoliciesDTO,
|
||||||
TUpdateAccessApprovalPolicy
|
TUpdateAccessApprovalPolicy
|
||||||
@ -25,6 +29,8 @@ type TSecretApprovalPolicyServiceFactoryDep = {
|
|||||||
projectEnvDAL: Pick<TProjectEnvDALFactory, "find" | "findOne">;
|
projectEnvDAL: Pick<TProjectEnvDALFactory, "find" | "findOne">;
|
||||||
accessApprovalPolicyApproverDAL: TAccessApprovalPolicyApproverDALFactory;
|
accessApprovalPolicyApproverDAL: TAccessApprovalPolicyApproverDALFactory;
|
||||||
projectMembershipDAL: Pick<TProjectMembershipDALFactory, "find">;
|
projectMembershipDAL: Pick<TProjectMembershipDALFactory, "find">;
|
||||||
|
groupDAL: TGroupDALFactory;
|
||||||
|
userDAL: Pick<TUserDALFactory, "find">;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type TAccessApprovalPolicyServiceFactory = ReturnType<typeof accessApprovalPolicyServiceFactory>;
|
export type TAccessApprovalPolicyServiceFactory = ReturnType<typeof accessApprovalPolicyServiceFactory>;
|
||||||
@ -32,9 +38,11 @@ export type TAccessApprovalPolicyServiceFactory = ReturnType<typeof accessApprov
|
|||||||
export const accessApprovalPolicyServiceFactory = ({
|
export const accessApprovalPolicyServiceFactory = ({
|
||||||
accessApprovalPolicyDAL,
|
accessApprovalPolicyDAL,
|
||||||
accessApprovalPolicyApproverDAL,
|
accessApprovalPolicyApproverDAL,
|
||||||
|
groupDAL,
|
||||||
permissionService,
|
permissionService,
|
||||||
projectEnvDAL,
|
projectEnvDAL,
|
||||||
projectDAL
|
projectDAL,
|
||||||
|
userDAL
|
||||||
}: TSecretApprovalPolicyServiceFactoryDep) => {
|
}: TSecretApprovalPolicyServiceFactoryDep) => {
|
||||||
const createAccessApprovalPolicy = async ({
|
const createAccessApprovalPolicy = async ({
|
||||||
name,
|
name,
|
||||||
@ -50,9 +58,23 @@ export const accessApprovalPolicyServiceFactory = ({
|
|||||||
enforcementLevel
|
enforcementLevel
|
||||||
}: TCreateAccessApprovalPolicy) => {
|
}: TCreateAccessApprovalPolicy) => {
|
||||||
const project = await projectDAL.findProjectBySlug(projectSlug, actorOrgId);
|
const project = await projectDAL.findProjectBySlug(projectSlug, actorOrgId);
|
||||||
if (!project) throw new BadRequestError({ message: "Project not found" });
|
if (!project) throw new NotFoundError({ message: `Project with slug '${projectSlug}' not found` });
|
||||||
|
|
||||||
if (approvals > approvers.length)
|
// If there is a group approver people might be added to the group later to meet the approvers quota
|
||||||
|
const groupApprovers = approvers
|
||||||
|
.filter((approver) => approver.type === ApproverType.Group)
|
||||||
|
.map((approver) => approver.id) as string[];
|
||||||
|
|
||||||
|
const userApprovers = approvers
|
||||||
|
.filter((approver) => approver.type === ApproverType.User)
|
||||||
|
.map((approver) => approver.id)
|
||||||
|
.filter(Boolean) as string[];
|
||||||
|
|
||||||
|
const userApproverNames = approvers
|
||||||
|
.map((approver) => (approver.type === ApproverType.User ? approver.name : undefined))
|
||||||
|
.filter(Boolean) as string[];
|
||||||
|
|
||||||
|
if (!groupApprovers && approvals > userApprovers.length + userApproverNames.length)
|
||||||
throw new BadRequestError({ message: "Approvals cannot be greater than approvers" });
|
throw new BadRequestError({ message: "Approvals cannot be greater than approvers" });
|
||||||
|
|
||||||
const { permission } = await permissionService.getProjectPermission(
|
const { permission } = await permissionService.getProjectPermission(
|
||||||
@ -67,18 +89,67 @@ export const accessApprovalPolicyServiceFactory = ({
|
|||||||
ProjectPermissionSub.SecretApproval
|
ProjectPermissionSub.SecretApproval
|
||||||
);
|
);
|
||||||
const env = await projectEnvDAL.findOne({ slug: environment, projectId: project.id });
|
const env = await projectEnvDAL.findOne({ slug: environment, projectId: project.id });
|
||||||
if (!env) throw new BadRequestError({ message: "Environment not found" });
|
if (!env) throw new NotFoundError({ message: `Environment with slug '${environment}' not found` });
|
||||||
|
|
||||||
await verifyApprovers({
|
let approverUserIds = userApprovers;
|
||||||
|
if (userApproverNames.length) {
|
||||||
|
const approverUsers = await userDAL.find({
|
||||||
|
$in: {
|
||||||
|
username: userApproverNames
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const approverNamesFromDb = approverUsers.map((user) => user.username);
|
||||||
|
const invalidUsernames = userApproverNames.filter((username) => !approverNamesFromDb.includes(username));
|
||||||
|
|
||||||
|
if (invalidUsernames.length) {
|
||||||
|
throw new BadRequestError({
|
||||||
|
message: `Invalid approver user: ${invalidUsernames.join(", ")}`
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
approverUserIds = approverUserIds.concat(approverUsers.map((user) => user.id));
|
||||||
|
}
|
||||||
|
|
||||||
|
const usersPromises: Promise<
|
||||||
|
{
|
||||||
|
id: string;
|
||||||
|
email: string | null | undefined;
|
||||||
|
username: string;
|
||||||
|
firstName: string | null | undefined;
|
||||||
|
lastName: string | null | undefined;
|
||||||
|
isPartOfGroup: boolean;
|
||||||
|
}[]
|
||||||
|
>[] = [];
|
||||||
|
const verifyAllApprovers = [...approverUserIds];
|
||||||
|
|
||||||
|
for (const groupId of groupApprovers) {
|
||||||
|
usersPromises.push(
|
||||||
|
groupDAL.findAllGroupPossibleMembers({ orgId: actorOrgId, groupId, offset: 0 }).then((group) => group.members)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
const verifyGroupApprovers = (await Promise.all(usersPromises))
|
||||||
|
.flat()
|
||||||
|
.filter((user) => user.isPartOfGroup)
|
||||||
|
.map((user) => user.id);
|
||||||
|
verifyAllApprovers.push(...verifyGroupApprovers);
|
||||||
|
|
||||||
|
const approversValid = await isApproversValid({
|
||||||
projectId: project.id,
|
projectId: project.id,
|
||||||
orgId: actorOrgId,
|
orgId: actorOrgId,
|
||||||
envSlug: environment,
|
envSlug: environment,
|
||||||
secretPath,
|
secretPath,
|
||||||
actorAuthMethod,
|
actorAuthMethod,
|
||||||
permissionService,
|
permissionService,
|
||||||
userIds: approvers
|
userIds: verifyAllApprovers
|
||||||
});
|
});
|
||||||
|
|
||||||
|
if (!approversValid) {
|
||||||
|
throw new BadRequestError({
|
||||||
|
message: "One or more approvers doesn't have access to be specified secret path"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
const accessApproval = await accessApprovalPolicyDAL.transaction(async (tx) => {
|
const accessApproval = await accessApprovalPolicyDAL.transaction(async (tx) => {
|
||||||
const doc = await accessApprovalPolicyDAL.create(
|
const doc = await accessApprovalPolicyDAL.create(
|
||||||
{
|
{
|
||||||
@ -90,13 +161,26 @@ export const accessApprovalPolicyServiceFactory = ({
|
|||||||
},
|
},
|
||||||
tx
|
tx
|
||||||
);
|
);
|
||||||
await accessApprovalPolicyApproverDAL.insertMany(
|
if (approverUserIds.length) {
|
||||||
approvers.map((userId) => ({
|
await accessApprovalPolicyApproverDAL.insertMany(
|
||||||
approverUserId: userId,
|
approverUserIds.map((userId) => ({
|
||||||
policyId: doc.id
|
approverUserId: userId,
|
||||||
})),
|
policyId: doc.id
|
||||||
tx
|
})),
|
||||||
);
|
tx
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (groupApprovers) {
|
||||||
|
await accessApprovalPolicyApproverDAL.insertMany(
|
||||||
|
groupApprovers.map((groupId) => ({
|
||||||
|
approverGroupId: groupId,
|
||||||
|
policyId: doc.id
|
||||||
|
})),
|
||||||
|
tx
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
return doc;
|
return doc;
|
||||||
});
|
});
|
||||||
return { ...accessApproval, environment: env, projectId: project.id };
|
return { ...accessApproval, environment: env, projectId: project.id };
|
||||||
@ -110,7 +194,7 @@ export const accessApprovalPolicyServiceFactory = ({
|
|||||||
projectSlug
|
projectSlug
|
||||||
}: TListAccessApprovalPoliciesDTO) => {
|
}: TListAccessApprovalPoliciesDTO) => {
|
||||||
const project = await projectDAL.findProjectBySlug(projectSlug, actorOrgId);
|
const project = await projectDAL.findProjectBySlug(projectSlug, actorOrgId);
|
||||||
if (!project) throw new BadRequestError({ message: "Project not found" });
|
if (!project) throw new NotFoundError({ message: `Project with slug '${projectSlug}' not found` });
|
||||||
|
|
||||||
// Anyone in the project should be able to get the policies.
|
// Anyone in the project should be able to get the policies.
|
||||||
/* const { permission } = */ await permissionService.getProjectPermission(
|
/* const { permission } = */ await permissionService.getProjectPermission(
|
||||||
@ -138,8 +222,32 @@ export const accessApprovalPolicyServiceFactory = ({
|
|||||||
approvals,
|
approvals,
|
||||||
enforcementLevel
|
enforcementLevel
|
||||||
}: TUpdateAccessApprovalPolicy) => {
|
}: TUpdateAccessApprovalPolicy) => {
|
||||||
|
const groupApprovers = approvers
|
||||||
|
.filter((approver) => approver.type === ApproverType.Group)
|
||||||
|
.map((approver) => approver.id) as string[];
|
||||||
|
|
||||||
|
const userApprovers = approvers
|
||||||
|
.filter((approver) => approver.type === ApproverType.User)
|
||||||
|
.map((approver) => approver.id)
|
||||||
|
.filter(Boolean) as string[];
|
||||||
|
|
||||||
|
const userApproverNames = approvers
|
||||||
|
.map((approver) => (approver.type === ApproverType.User ? approver.name : undefined))
|
||||||
|
.filter(Boolean) as string[];
|
||||||
|
|
||||||
const accessApprovalPolicy = await accessApprovalPolicyDAL.findById(policyId);
|
const accessApprovalPolicy = await accessApprovalPolicyDAL.findById(policyId);
|
||||||
if (!accessApprovalPolicy) throw new BadRequestError({ message: "Secret approval policy not found" });
|
const currentAppovals = approvals || accessApprovalPolicy.approvals;
|
||||||
|
if (
|
||||||
|
groupApprovers?.length === 0 &&
|
||||||
|
userApprovers &&
|
||||||
|
currentAppovals > userApprovers.length + userApproverNames.length
|
||||||
|
) {
|
||||||
|
throw new BadRequestError({ message: "Approvals cannot be greater than approvers" });
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!accessApprovalPolicy) {
|
||||||
|
throw new NotFoundError({ message: `Secret approval policy with ID '${policyId}' not found` });
|
||||||
|
}
|
||||||
const { permission } = await permissionService.getProjectPermission(
|
const { permission } = await permissionService.getProjectPermission(
|
||||||
actor,
|
actor,
|
||||||
actorId,
|
actorId,
|
||||||
@ -161,26 +269,104 @@ export const accessApprovalPolicyServiceFactory = ({
|
|||||||
},
|
},
|
||||||
tx
|
tx
|
||||||
);
|
);
|
||||||
if (approvers) {
|
|
||||||
await verifyApprovers({
|
await accessApprovalPolicyApproverDAL.delete({ policyId: doc.id }, tx);
|
||||||
|
|
||||||
|
if (userApprovers.length || userApproverNames.length) {
|
||||||
|
let userApproverIds = userApprovers;
|
||||||
|
if (userApproverNames.length) {
|
||||||
|
const approverUsers = await userDAL.find({
|
||||||
|
$in: {
|
||||||
|
username: userApproverNames
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const approverNamesFromDb = approverUsers.map((user) => user.username);
|
||||||
|
const invalidUsernames = userApproverNames.filter((username) => !approverNamesFromDb.includes(username));
|
||||||
|
|
||||||
|
if (invalidUsernames.length) {
|
||||||
|
throw new BadRequestError({
|
||||||
|
message: `Invalid approver user: ${invalidUsernames.join(", ")}`
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
userApproverIds = userApproverIds.concat(approverUsers.map((user) => user.id));
|
||||||
|
}
|
||||||
|
|
||||||
|
const approversValid = await isApproversValid({
|
||||||
projectId: accessApprovalPolicy.projectId,
|
projectId: accessApprovalPolicy.projectId,
|
||||||
orgId: actorOrgId,
|
orgId: actorOrgId,
|
||||||
envSlug: accessApprovalPolicy.environment.slug,
|
envSlug: accessApprovalPolicy.environment.slug,
|
||||||
secretPath: doc.secretPath!,
|
secretPath: doc.secretPath!,
|
||||||
actorAuthMethod,
|
actorAuthMethod,
|
||||||
permissionService,
|
permissionService,
|
||||||
userIds: approvers
|
userIds: userApproverIds
|
||||||
});
|
});
|
||||||
|
|
||||||
await accessApprovalPolicyApproverDAL.delete({ policyId: doc.id }, tx);
|
if (!approversValid) {
|
||||||
|
throw new BadRequestError({
|
||||||
|
message: "One or more approvers doesn't have access to be specified secret path"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
await accessApprovalPolicyApproverDAL.insertMany(
|
await accessApprovalPolicyApproverDAL.insertMany(
|
||||||
approvers.map((userId) => ({
|
userApproverIds.map((userId) => ({
|
||||||
approverUserId: userId,
|
approverUserId: userId,
|
||||||
policyId: doc.id
|
policyId: doc.id
|
||||||
})),
|
})),
|
||||||
tx
|
tx
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (groupApprovers) {
|
||||||
|
const usersPromises: Promise<
|
||||||
|
{
|
||||||
|
id: string;
|
||||||
|
email: string | null | undefined;
|
||||||
|
username: string;
|
||||||
|
firstName: string | null | undefined;
|
||||||
|
lastName: string | null | undefined;
|
||||||
|
isPartOfGroup: boolean;
|
||||||
|
}[]
|
||||||
|
>[] = [];
|
||||||
|
|
||||||
|
for (const groupId of groupApprovers) {
|
||||||
|
usersPromises.push(
|
||||||
|
groupDAL
|
||||||
|
.findAllGroupPossibleMembers({ orgId: actorOrgId, groupId, offset: 0 })
|
||||||
|
.then((group) => group.members)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
const verifyGroupApprovers = (await Promise.all(usersPromises))
|
||||||
|
.flat()
|
||||||
|
.filter((user) => user.isPartOfGroup)
|
||||||
|
.map((user) => user.id);
|
||||||
|
|
||||||
|
const approversValid = await isApproversValid({
|
||||||
|
projectId: accessApprovalPolicy.projectId,
|
||||||
|
orgId: actorOrgId,
|
||||||
|
envSlug: accessApprovalPolicy.environment.slug,
|
||||||
|
secretPath: doc.secretPath!,
|
||||||
|
actorAuthMethod,
|
||||||
|
permissionService,
|
||||||
|
userIds: verifyGroupApprovers
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!approversValid) {
|
||||||
|
throw new BadRequestError({
|
||||||
|
message: "One or more approvers doesn't have access to be specified secret path"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
await accessApprovalPolicyApproverDAL.insertMany(
|
||||||
|
groupApprovers.map((groupId) => ({
|
||||||
|
approverGroupId: groupId,
|
||||||
|
policyId: doc.id
|
||||||
|
})),
|
||||||
|
tx
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
return doc;
|
return doc;
|
||||||
});
|
});
|
||||||
return {
|
return {
|
||||||
@ -198,7 +384,7 @@ export const accessApprovalPolicyServiceFactory = ({
|
|||||||
actorOrgId
|
actorOrgId
|
||||||
}: TDeleteAccessApprovalPolicy) => {
|
}: TDeleteAccessApprovalPolicy) => {
|
||||||
const policy = await accessApprovalPolicyDAL.findById(policyId);
|
const policy = await accessApprovalPolicyDAL.findById(policyId);
|
||||||
if (!policy) throw new BadRequestError({ message: "Secret approval policy not found" });
|
if (!policy) throw new NotFoundError({ message: `Secret approval policy with ID '${policyId}' not found` });
|
||||||
|
|
||||||
const { permission } = await permissionService.getProjectPermission(
|
const { permission } = await permissionService.getProjectPermission(
|
||||||
actor,
|
actor,
|
||||||
@ -226,7 +412,7 @@ export const accessApprovalPolicyServiceFactory = ({
|
|||||||
}: TGetAccessPolicyCountByEnvironmentDTO) => {
|
}: TGetAccessPolicyCountByEnvironmentDTO) => {
|
||||||
const project = await projectDAL.findProjectBySlug(projectSlug, actorOrgId);
|
const project = await projectDAL.findProjectBySlug(projectSlug, actorOrgId);
|
||||||
|
|
||||||
if (!project) throw new BadRequestError({ message: "Project not found" });
|
if (!project) throw new NotFoundError({ message: `Project with slug '${projectSlug}' not found` });
|
||||||
|
|
||||||
const { membership } = await permissionService.getProjectPermission(
|
const { membership } = await permissionService.getProjectPermission(
|
||||||
actor,
|
actor,
|
||||||
@ -235,22 +421,53 @@ export const accessApprovalPolicyServiceFactory = ({
|
|||||||
actorAuthMethod,
|
actorAuthMethod,
|
||||||
actorOrgId
|
actorOrgId
|
||||||
);
|
);
|
||||||
if (!membership) throw new BadRequestError({ message: "User not found in project" });
|
if (!membership) {
|
||||||
|
throw new ForbiddenRequestError({ message: "You are not a member of this project" });
|
||||||
|
}
|
||||||
|
|
||||||
const environment = await projectEnvDAL.findOne({ projectId: project.id, slug: envSlug });
|
const environment = await projectEnvDAL.findOne({ projectId: project.id, slug: envSlug });
|
||||||
if (!environment) throw new BadRequestError({ message: "Environment not found" });
|
if (!environment) throw new NotFoundError({ message: `Environment with slug '${envSlug}' not found` });
|
||||||
|
|
||||||
const policies = await accessApprovalPolicyDAL.find({ envId: environment.id, projectId: project.id });
|
const policies = await accessApprovalPolicyDAL.find({ envId: environment.id, projectId: project.id });
|
||||||
if (!policies) throw new BadRequestError({ message: "No policies found" });
|
if (!policies) throw new NotFoundError({ message: `No policies found in environment with slug '${envSlug}'` });
|
||||||
|
|
||||||
return { count: policies.length };
|
return { count: policies.length };
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const getAccessApprovalPolicyById = async ({
|
||||||
|
actorId,
|
||||||
|
actor,
|
||||||
|
actorOrgId,
|
||||||
|
actorAuthMethod,
|
||||||
|
policyId
|
||||||
|
}: TGetAccessApprovalPolicyByIdDTO) => {
|
||||||
|
const [policy] = await accessApprovalPolicyDAL.find({}, { policyId });
|
||||||
|
|
||||||
|
if (!policy) {
|
||||||
|
throw new NotFoundError({
|
||||||
|
message: `Cannot find access approval policy with ID ${policyId}`
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const { permission } = await permissionService.getProjectPermission(
|
||||||
|
actor,
|
||||||
|
actorId,
|
||||||
|
policy.projectId,
|
||||||
|
actorAuthMethod,
|
||||||
|
actorOrgId
|
||||||
|
);
|
||||||
|
|
||||||
|
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.SecretApproval);
|
||||||
|
|
||||||
|
return policy;
|
||||||
|
};
|
||||||
|
|
||||||
return {
|
return {
|
||||||
getAccessPolicyCountByEnvSlug,
|
getAccessPolicyCountByEnvSlug,
|
||||||
createAccessApprovalPolicy,
|
createAccessApprovalPolicy,
|
||||||
deleteAccessApprovalPolicy,
|
deleteAccessApprovalPolicy,
|
||||||
updateAccessApprovalPolicy,
|
updateAccessApprovalPolicy,
|
||||||
getAccessApprovalPolicyByProjectSlug
|
getAccessApprovalPolicyByProjectSlug,
|
||||||
|
getAccessApprovalPolicyById
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
@ -3,7 +3,7 @@ import { ActorAuthMethod } from "@app/services/auth/auth-type";
|
|||||||
|
|
||||||
import { TPermissionServiceFactory } from "../permission/permission-service";
|
import { TPermissionServiceFactory } from "../permission/permission-service";
|
||||||
|
|
||||||
export type TVerifyApprovers = {
|
export type TIsApproversValid = {
|
||||||
userIds: string[];
|
userIds: string[];
|
||||||
permissionService: Pick<TPermissionServiceFactory, "getProjectPermission">;
|
permissionService: Pick<TPermissionServiceFactory, "getProjectPermission">;
|
||||||
envSlug: string;
|
envSlug: string;
|
||||||
@ -13,11 +13,16 @@ export type TVerifyApprovers = {
|
|||||||
orgId: string;
|
orgId: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export enum ApproverType {
|
||||||
|
Group = "group",
|
||||||
|
User = "user"
|
||||||
|
}
|
||||||
|
|
||||||
export type TCreateAccessApprovalPolicy = {
|
export type TCreateAccessApprovalPolicy = {
|
||||||
approvals: number;
|
approvals: number;
|
||||||
secretPath: string;
|
secretPath: string;
|
||||||
environment: string;
|
environment: string;
|
||||||
approvers: string[];
|
approvers: ({ type: ApproverType.Group; id: string } | { type: ApproverType.User; id?: string; name?: string })[];
|
||||||
projectSlug: string;
|
projectSlug: string;
|
||||||
name: string;
|
name: string;
|
||||||
enforcementLevel: EnforcementLevel;
|
enforcementLevel: EnforcementLevel;
|
||||||
@ -26,7 +31,7 @@ export type TCreateAccessApprovalPolicy = {
|
|||||||
export type TUpdateAccessApprovalPolicy = {
|
export type TUpdateAccessApprovalPolicy = {
|
||||||
policyId: string;
|
policyId: string;
|
||||||
approvals?: number;
|
approvals?: number;
|
||||||
approvers?: string[];
|
approvers: ({ type: ApproverType.Group; id: string } | { type: ApproverType.User; id?: string; name?: string })[];
|
||||||
secretPath?: string;
|
secretPath?: string;
|
||||||
name?: string;
|
name?: string;
|
||||||
enforcementLevel?: EnforcementLevel;
|
enforcementLevel?: EnforcementLevel;
|
||||||
@ -41,6 +46,10 @@ export type TGetAccessPolicyCountByEnvironmentDTO = {
|
|||||||
projectSlug: string;
|
projectSlug: string;
|
||||||
} & Omit<TProjectPermission, "projectId">;
|
} & Omit<TProjectPermission, "projectId">;
|
||||||
|
|
||||||
|
export type TGetAccessApprovalPolicyByIdDTO = {
|
||||||
|
policyId: string;
|
||||||
|
} & Omit<TProjectPermission, "projectId">;
|
||||||
|
|
||||||
export type TListAccessApprovalPoliciesDTO = {
|
export type TListAccessApprovalPoliciesDTO = {
|
||||||
projectSlug: string;
|
projectSlug: string;
|
||||||
} & Omit<TProjectPermission, "projectId">;
|
} & Omit<TProjectPermission, "projectId">;
|
||||||
|
@ -39,6 +39,12 @@ export const accessApprovalRequestDALFactory = (db: TDbClient) => {
|
|||||||
`${TableName.AccessApprovalPolicy}.id`,
|
`${TableName.AccessApprovalPolicy}.id`,
|
||||||
`${TableName.AccessApprovalPolicyApprover}.policyId`
|
`${TableName.AccessApprovalPolicyApprover}.policyId`
|
||||||
)
|
)
|
||||||
|
.leftJoin(
|
||||||
|
TableName.UserGroupMembership,
|
||||||
|
`${TableName.AccessApprovalPolicyApprover}.approverGroupId`,
|
||||||
|
`${TableName.UserGroupMembership}.groupId`
|
||||||
|
)
|
||||||
|
.leftJoin(TableName.Users, `${TableName.UserGroupMembership}.userId`, `${TableName.Users}.id`)
|
||||||
|
|
||||||
.join<TUsers>(
|
.join<TUsers>(
|
||||||
db(TableName.Users).as("requestedByUser"),
|
db(TableName.Users).as("requestedByUser"),
|
||||||
@ -59,6 +65,7 @@ export const accessApprovalRequestDALFactory = (db: TDbClient) => {
|
|||||||
)
|
)
|
||||||
|
|
||||||
.select(db.ref("approverUserId").withSchema(TableName.AccessApprovalPolicyApprover))
|
.select(db.ref("approverUserId").withSchema(TableName.AccessApprovalPolicyApprover))
|
||||||
|
.select(db.ref("userId").withSchema(TableName.UserGroupMembership).as("approverGroupUserId"))
|
||||||
|
|
||||||
.select(
|
.select(
|
||||||
db.ref("projectId").withSchema(TableName.Environment),
|
db.ref("projectId").withSchema(TableName.Environment),
|
||||||
@ -142,7 +149,12 @@ export const accessApprovalRequestDALFactory = (db: TDbClient) => {
|
|||||||
label: "reviewers" as const,
|
label: "reviewers" as const,
|
||||||
mapper: ({ reviewerUserId: userId, reviewerStatus: status }) => (userId ? { userId, status } : undefined)
|
mapper: ({ reviewerUserId: userId, reviewerStatus: status }) => (userId ? { userId, status } : undefined)
|
||||||
},
|
},
|
||||||
{ key: "approverUserId", label: "approvers" as const, mapper: ({ approverUserId }) => approverUserId }
|
{ key: "approverUserId", label: "approvers" as const, mapper: ({ approverUserId }) => approverUserId },
|
||||||
|
{
|
||||||
|
key: "approverGroupUserId",
|
||||||
|
label: "approvers" as const,
|
||||||
|
mapper: ({ approverGroupUserId }) => approverGroupUserId
|
||||||
|
}
|
||||||
]
|
]
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -172,17 +184,28 @@ export const accessApprovalRequestDALFactory = (db: TDbClient) => {
|
|||||||
`requestedByUser.id`
|
`requestedByUser.id`
|
||||||
)
|
)
|
||||||
|
|
||||||
.join(
|
.leftJoin(
|
||||||
TableName.AccessApprovalPolicyApprover,
|
TableName.AccessApprovalPolicyApprover,
|
||||||
`${TableName.AccessApprovalPolicy}.id`,
|
`${TableName.AccessApprovalPolicy}.id`,
|
||||||
`${TableName.AccessApprovalPolicyApprover}.policyId`
|
`${TableName.AccessApprovalPolicyApprover}.policyId`
|
||||||
)
|
)
|
||||||
|
|
||||||
.join<TUsers>(
|
.leftJoin<TUsers>(
|
||||||
db(TableName.Users).as("accessApprovalPolicyApproverUser"),
|
db(TableName.Users).as("accessApprovalPolicyApproverUser"),
|
||||||
`${TableName.AccessApprovalPolicyApprover}.approverUserId`,
|
`${TableName.AccessApprovalPolicyApprover}.approverUserId`,
|
||||||
"accessApprovalPolicyApproverUser.id"
|
"accessApprovalPolicyApproverUser.id"
|
||||||
)
|
)
|
||||||
|
.leftJoin(
|
||||||
|
TableName.UserGroupMembership,
|
||||||
|
`${TableName.AccessApprovalPolicyApprover}.approverGroupId`,
|
||||||
|
`${TableName.UserGroupMembership}.groupId`
|
||||||
|
)
|
||||||
|
|
||||||
|
.leftJoin<TUsers>(
|
||||||
|
db(TableName.Users).as("accessApprovalPolicyGroupApproverUser"),
|
||||||
|
`${TableName.UserGroupMembership}.userId`,
|
||||||
|
"accessApprovalPolicyGroupApproverUser.id"
|
||||||
|
)
|
||||||
|
|
||||||
.leftJoin(
|
.leftJoin(
|
||||||
TableName.AccessApprovalRequestReviewer,
|
TableName.AccessApprovalRequestReviewer,
|
||||||
@ -200,10 +223,15 @@ export const accessApprovalRequestDALFactory = (db: TDbClient) => {
|
|||||||
.select(selectAllTableCols(TableName.AccessApprovalRequest))
|
.select(selectAllTableCols(TableName.AccessApprovalRequest))
|
||||||
.select(
|
.select(
|
||||||
tx.ref("approverUserId").withSchema(TableName.AccessApprovalPolicyApprover),
|
tx.ref("approverUserId").withSchema(TableName.AccessApprovalPolicyApprover),
|
||||||
|
tx.ref("userId").withSchema(TableName.UserGroupMembership),
|
||||||
tx.ref("email").withSchema("accessApprovalPolicyApproverUser").as("approverEmail"),
|
tx.ref("email").withSchema("accessApprovalPolicyApproverUser").as("approverEmail"),
|
||||||
|
tx.ref("email").withSchema("accessApprovalPolicyGroupApproverUser").as("approverGroupEmail"),
|
||||||
tx.ref("username").withSchema("accessApprovalPolicyApproverUser").as("approverUsername"),
|
tx.ref("username").withSchema("accessApprovalPolicyApproverUser").as("approverUsername"),
|
||||||
|
tx.ref("username").withSchema("accessApprovalPolicyGroupApproverUser").as("approverGroupUsername"),
|
||||||
tx.ref("firstName").withSchema("accessApprovalPolicyApproverUser").as("approverFirstName"),
|
tx.ref("firstName").withSchema("accessApprovalPolicyApproverUser").as("approverFirstName"),
|
||||||
|
tx.ref("firstName").withSchema("accessApprovalPolicyGroupApproverUser").as("approverGroupFirstName"),
|
||||||
tx.ref("lastName").withSchema("accessApprovalPolicyApproverUser").as("approverLastName"),
|
tx.ref("lastName").withSchema("accessApprovalPolicyApproverUser").as("approverLastName"),
|
||||||
|
tx.ref("lastName").withSchema("accessApprovalPolicyGroupApproverUser").as("approverGroupLastName"),
|
||||||
tx.ref("email").withSchema("requestedByUser").as("requestedByUserEmail"),
|
tx.ref("email").withSchema("requestedByUser").as("requestedByUserEmail"),
|
||||||
tx.ref("username").withSchema("requestedByUser").as("requestedByUserUsername"),
|
tx.ref("username").withSchema("requestedByUser").as("requestedByUserUsername"),
|
||||||
tx.ref("firstName").withSchema("requestedByUser").as("requestedByUserFirstName"),
|
tx.ref("firstName").withSchema("requestedByUser").as("requestedByUserFirstName"),
|
||||||
@ -282,6 +310,23 @@ export const accessApprovalRequestDALFactory = (db: TDbClient) => {
|
|||||||
lastName,
|
lastName,
|
||||||
username
|
username
|
||||||
})
|
})
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "userId",
|
||||||
|
label: "approvers" as const,
|
||||||
|
mapper: ({
|
||||||
|
userId,
|
||||||
|
approverGroupEmail: email,
|
||||||
|
approverGroupUsername: username,
|
||||||
|
approverGroupLastName: lastName,
|
||||||
|
approverFirstName: firstName
|
||||||
|
}) => ({
|
||||||
|
userId,
|
||||||
|
email,
|
||||||
|
firstName,
|
||||||
|
lastName,
|
||||||
|
username
|
||||||
|
})
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
});
|
});
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
import { PackRule, unpackRules } from "@casl/ability/extra";
|
import { PackRule, unpackRules } from "@casl/ability/extra";
|
||||||
|
|
||||||
import { UnauthorizedError } from "@app/lib/errors";
|
import { BadRequestError } from "@app/lib/errors";
|
||||||
|
|
||||||
import { TVerifyPermission } from "./access-approval-request-types";
|
import { TVerifyPermission } from "./access-approval-request-types";
|
||||||
|
|
||||||
@ -19,7 +19,7 @@ export const verifyRequestedPermissions = ({ permissions }: TVerifyPermission) =
|
|||||||
);
|
);
|
||||||
|
|
||||||
if (!permission || !permission.length) {
|
if (!permission || !permission.length) {
|
||||||
throw new UnauthorizedError({ message: "No permission provided" });
|
throw new BadRequestError({ message: "No permission provided" });
|
||||||
}
|
}
|
||||||
|
|
||||||
const requestedPermissions: string[] = [];
|
const requestedPermissions: string[] = [];
|
||||||
@ -39,10 +39,10 @@ export const verifyRequestedPermissions = ({ permissions }: TVerifyPermission) =
|
|||||||
const permissionEnv = firstPermission.conditions?.environment;
|
const permissionEnv = firstPermission.conditions?.environment;
|
||||||
|
|
||||||
if (!permissionEnv || typeof permissionEnv !== "string") {
|
if (!permissionEnv || typeof permissionEnv !== "string") {
|
||||||
throw new UnauthorizedError({ message: "Permission environment is not a string" });
|
throw new BadRequestError({ message: "Permission environment is not a string" });
|
||||||
}
|
}
|
||||||
if (!permissionSecretPath || typeof permissionSecretPath !== "string") {
|
if (!permissionSecretPath || typeof permissionSecretPath !== "string") {
|
||||||
throw new UnauthorizedError({ message: "Permission path is not a string" });
|
throw new BadRequestError({ message: "Permission path is not a string" });
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
@ -3,7 +3,7 @@ import ms from "ms";
|
|||||||
|
|
||||||
import { ProjectMembershipRole } from "@app/db/schemas";
|
import { ProjectMembershipRole } from "@app/db/schemas";
|
||||||
import { getConfig } from "@app/lib/config/env";
|
import { getConfig } from "@app/lib/config/env";
|
||||||
import { BadRequestError, UnauthorizedError } from "@app/lib/errors";
|
import { BadRequestError, ForbiddenRequestError, NotFoundError } from "@app/lib/errors";
|
||||||
import { alphaNumericNanoId } from "@app/lib/nanoid";
|
import { alphaNumericNanoId } from "@app/lib/nanoid";
|
||||||
import { TKmsServiceFactory } from "@app/services/kms/kms-service";
|
import { TKmsServiceFactory } from "@app/services/kms/kms-service";
|
||||||
import { TProjectDALFactory } from "@app/services/project/project-dal";
|
import { TProjectDALFactory } from "@app/services/project/project-dal";
|
||||||
@ -17,7 +17,8 @@ import { TUserDALFactory } from "@app/services/user/user-dal";
|
|||||||
|
|
||||||
import { TAccessApprovalPolicyApproverDALFactory } from "../access-approval-policy/access-approval-policy-approver-dal";
|
import { TAccessApprovalPolicyApproverDALFactory } from "../access-approval-policy/access-approval-policy-approver-dal";
|
||||||
import { TAccessApprovalPolicyDALFactory } from "../access-approval-policy/access-approval-policy-dal";
|
import { TAccessApprovalPolicyDALFactory } from "../access-approval-policy/access-approval-policy-dal";
|
||||||
import { verifyApprovers } from "../access-approval-policy/access-approval-policy-fns";
|
import { isApproversValid } from "../access-approval-policy/access-approval-policy-fns";
|
||||||
|
import { TGroupDALFactory } from "../group/group-dal";
|
||||||
import { TPermissionServiceFactory } from "../permission/permission-service";
|
import { TPermissionServiceFactory } from "../permission/permission-service";
|
||||||
import { TProjectUserAdditionalPrivilegeDALFactory } from "../project-user-additional-privilege/project-user-additional-privilege-dal";
|
import { TProjectUserAdditionalPrivilegeDALFactory } from "../project-user-additional-privilege/project-user-additional-privilege-dal";
|
||||||
import { ProjectUserAdditionalPrivilegeTemporaryMode } from "../project-user-additional-privilege/project-user-additional-privilege-types";
|
import { ProjectUserAdditionalPrivilegeTemporaryMode } from "../project-user-additional-privilege/project-user-additional-privilege-types";
|
||||||
@ -57,6 +58,7 @@ type TSecretApprovalRequestServiceFactoryDep = {
|
|||||||
TAccessApprovalRequestReviewerDALFactory,
|
TAccessApprovalRequestReviewerDALFactory,
|
||||||
"create" | "find" | "findOne" | "transaction"
|
"create" | "find" | "findOne" | "transaction"
|
||||||
>;
|
>;
|
||||||
|
groupDAL: Pick<TGroupDALFactory, "findAllGroupPossibleMembers">;
|
||||||
projectMembershipDAL: Pick<TProjectMembershipDALFactory, "findById">;
|
projectMembershipDAL: Pick<TProjectMembershipDALFactory, "findById">;
|
||||||
smtpService: Pick<TSmtpService, "sendMail">;
|
smtpService: Pick<TSmtpService, "sendMail">;
|
||||||
userDAL: Pick<
|
userDAL: Pick<
|
||||||
@ -70,6 +72,7 @@ type TSecretApprovalRequestServiceFactoryDep = {
|
|||||||
export type TAccessApprovalRequestServiceFactory = ReturnType<typeof accessApprovalRequestServiceFactory>;
|
export type TAccessApprovalRequestServiceFactory = ReturnType<typeof accessApprovalRequestServiceFactory>;
|
||||||
|
|
||||||
export const accessApprovalRequestServiceFactory = ({
|
export const accessApprovalRequestServiceFactory = ({
|
||||||
|
groupDAL,
|
||||||
projectDAL,
|
projectDAL,
|
||||||
projectEnvDAL,
|
projectEnvDAL,
|
||||||
permissionService,
|
permissionService,
|
||||||
@ -96,7 +99,7 @@ export const accessApprovalRequestServiceFactory = ({
|
|||||||
}: TCreateAccessApprovalRequestDTO) => {
|
}: TCreateAccessApprovalRequestDTO) => {
|
||||||
const cfg = getConfig();
|
const cfg = getConfig();
|
||||||
const project = await projectDAL.findProjectBySlug(projectSlug, actorOrgId);
|
const project = await projectDAL.findProjectBySlug(projectSlug, actorOrgId);
|
||||||
if (!project) throw new UnauthorizedError({ message: "Project not found" });
|
if (!project) throw new NotFoundError({ message: `Project with slug '${projectSlug}' not found` });
|
||||||
|
|
||||||
// Anyone can create an access approval request.
|
// Anyone can create an access approval request.
|
||||||
const { membership } = await permissionService.getProjectPermission(
|
const { membership } = await permissionService.getProjectPermission(
|
||||||
@ -106,31 +109,62 @@ export const accessApprovalRequestServiceFactory = ({
|
|||||||
actorAuthMethod,
|
actorAuthMethod,
|
||||||
actorOrgId
|
actorOrgId
|
||||||
);
|
);
|
||||||
if (!membership) throw new UnauthorizedError({ message: "You are not a member of this project" });
|
if (!membership) {
|
||||||
|
throw new ForbiddenRequestError({ message: "You are not a member of this project" });
|
||||||
|
}
|
||||||
|
|
||||||
const requestedByUser = await userDAL.findById(actorId);
|
const requestedByUser = await userDAL.findById(actorId);
|
||||||
if (!requestedByUser) throw new UnauthorizedError({ message: "User not found" });
|
if (!requestedByUser) throw new ForbiddenRequestError({ message: "User not found" });
|
||||||
|
|
||||||
await projectDAL.checkProjectUpgradeStatus(project.id);
|
await projectDAL.checkProjectUpgradeStatus(project.id);
|
||||||
|
|
||||||
const { envSlug, secretPath, accessTypes } = verifyRequestedPermissions({ permissions: requestedPermissions });
|
const { envSlug, secretPath, accessTypes } = verifyRequestedPermissions({ permissions: requestedPermissions });
|
||||||
const environment = await projectEnvDAL.findOne({ projectId: project.id, slug: envSlug });
|
const environment = await projectEnvDAL.findOne({ projectId: project.id, slug: envSlug });
|
||||||
|
|
||||||
if (!environment) throw new UnauthorizedError({ message: "Environment not found" });
|
if (!environment) throw new NotFoundError({ message: `Environment with slug '${envSlug}' not found` });
|
||||||
|
|
||||||
const policy = await accessApprovalPolicyDAL.findOne({
|
const policy = await accessApprovalPolicyDAL.findOne({
|
||||||
envId: environment.id,
|
envId: environment.id,
|
||||||
secretPath
|
secretPath
|
||||||
});
|
});
|
||||||
if (!policy) throw new UnauthorizedError({ message: "No policy matching criteria was found." });
|
if (!policy) {
|
||||||
|
throw new NotFoundError({
|
||||||
|
message: `No policy in environment with slug '${environment.slug}' and with secret path '${secretPath}' was found.`
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const approverIds: string[] = [];
|
||||||
|
const approverGroupIds: string[] = [];
|
||||||
|
|
||||||
const approvers = await accessApprovalPolicyApproverDAL.find({
|
const approvers = await accessApprovalPolicyApproverDAL.find({
|
||||||
policyId: policy.id
|
policyId: policy.id
|
||||||
});
|
});
|
||||||
|
|
||||||
|
approvers.forEach((approver) => {
|
||||||
|
if (approver.approverUserId) {
|
||||||
|
approverIds.push(approver.approverUserId);
|
||||||
|
} else if (approver.approverGroupId) {
|
||||||
|
approverGroupIds.push(approver.approverGroupId);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const groupUsers = (
|
||||||
|
await Promise.all(
|
||||||
|
approverGroupIds.map((groupApproverId) =>
|
||||||
|
groupDAL
|
||||||
|
.findAllGroupPossibleMembers({
|
||||||
|
orgId: actorOrgId,
|
||||||
|
groupId: groupApproverId
|
||||||
|
})
|
||||||
|
.then((group) => group.members)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
).flat();
|
||||||
|
approverIds.push(...groupUsers.filter((user) => user.isPartOfGroup).map((user) => user.id));
|
||||||
|
|
||||||
const approverUsers = await userDAL.find({
|
const approverUsers = await userDAL.find({
|
||||||
$in: {
|
$in: {
|
||||||
id: approvers.map((approver) => approver.approverUserId)
|
id: [...new Set(approverIds)]
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -236,7 +270,7 @@ export const accessApprovalRequestServiceFactory = ({
|
|||||||
actorAuthMethod
|
actorAuthMethod
|
||||||
}: TListApprovalRequestsDTO) => {
|
}: TListApprovalRequestsDTO) => {
|
||||||
const project = await projectDAL.findProjectBySlug(projectSlug, actorOrgId);
|
const project = await projectDAL.findProjectBySlug(projectSlug, actorOrgId);
|
||||||
if (!project) throw new UnauthorizedError({ message: "Project not found" });
|
if (!project) throw new NotFoundError({ message: `Project with slug '${projectSlug}' not found` });
|
||||||
|
|
||||||
const { membership } = await permissionService.getProjectPermission(
|
const { membership } = await permissionService.getProjectPermission(
|
||||||
actor,
|
actor,
|
||||||
@ -245,7 +279,9 @@ export const accessApprovalRequestServiceFactory = ({
|
|||||||
actorAuthMethod,
|
actorAuthMethod,
|
||||||
actorOrgId
|
actorOrgId
|
||||||
);
|
);
|
||||||
if (!membership) throw new UnauthorizedError({ message: "You are not a member of this project" });
|
if (!membership) {
|
||||||
|
throw new ForbiddenRequestError({ message: "You are not a member of this project" });
|
||||||
|
}
|
||||||
|
|
||||||
const policies = await accessApprovalPolicyDAL.find({ projectId: project.id });
|
const policies = await accessApprovalPolicyDAL.find({ projectId: project.id });
|
||||||
let requests = await accessApprovalRequestDAL.findRequestsWithPrivilegeByPolicyIds(policies.map((p) => p.id));
|
let requests = await accessApprovalRequestDAL.findRequestsWithPrivilegeByPolicyIds(policies.map((p) => p.id));
|
||||||
@ -270,7 +306,9 @@ export const accessApprovalRequestServiceFactory = ({
|
|||||||
actorOrgId
|
actorOrgId
|
||||||
}: TReviewAccessRequestDTO) => {
|
}: TReviewAccessRequestDTO) => {
|
||||||
const accessApprovalRequest = await accessApprovalRequestDAL.findById(requestId);
|
const accessApprovalRequest = await accessApprovalRequestDAL.findById(requestId);
|
||||||
if (!accessApprovalRequest) throw new BadRequestError({ message: "Secret approval request not found" });
|
if (!accessApprovalRequest) {
|
||||||
|
throw new NotFoundError({ message: `Secret approval request with ID '${requestId}' not found` });
|
||||||
|
}
|
||||||
|
|
||||||
const { policy } = accessApprovalRequest;
|
const { policy } = accessApprovalRequest;
|
||||||
const { membership, hasRole } = await permissionService.getProjectPermission(
|
const { membership, hasRole } = await permissionService.getProjectPermission(
|
||||||
@ -281,19 +319,21 @@ export const accessApprovalRequestServiceFactory = ({
|
|||||||
actorOrgId
|
actorOrgId
|
||||||
);
|
);
|
||||||
|
|
||||||
if (!membership) throw new UnauthorizedError({ message: "You are not a member of this project" });
|
if (!membership) {
|
||||||
|
throw new ForbiddenRequestError({ message: "You are not a member of this project" });
|
||||||
|
}
|
||||||
|
|
||||||
if (
|
if (
|
||||||
!hasRole(ProjectMembershipRole.Admin) &&
|
!hasRole(ProjectMembershipRole.Admin) &&
|
||||||
accessApprovalRequest.requestedByUserId !== actorId && // The request wasn't made by the current user
|
accessApprovalRequest.requestedByUserId !== actorId && // The request wasn't made by the current user
|
||||||
!policy.approvers.find((approver) => approver.userId === actorId) // The request isn't performed by an assigned approver
|
!policy.approvers.find((approver) => approver.userId === actorId) // The request isn't performed by an assigned approver
|
||||||
) {
|
) {
|
||||||
throw new UnauthorizedError({ message: "You are not authorized to approve this request" });
|
throw new ForbiddenRequestError({ message: "You are not authorized to approve this request" });
|
||||||
}
|
}
|
||||||
|
|
||||||
const reviewerProjectMembership = await projectMembershipDAL.findById(membership.id);
|
const reviewerProjectMembership = await projectMembershipDAL.findById(membership.id);
|
||||||
|
|
||||||
await verifyApprovers({
|
const approversValid = await isApproversValid({
|
||||||
projectId: accessApprovalRequest.projectId,
|
projectId: accessApprovalRequest.projectId,
|
||||||
orgId: actorOrgId,
|
orgId: actorOrgId,
|
||||||
envSlug: accessApprovalRequest.environment,
|
envSlug: accessApprovalRequest.environment,
|
||||||
@ -303,6 +343,10 @@ export const accessApprovalRequestServiceFactory = ({
|
|||||||
userIds: [reviewerProjectMembership.userId]
|
userIds: [reviewerProjectMembership.userId]
|
||||||
});
|
});
|
||||||
|
|
||||||
|
if (!approversValid) {
|
||||||
|
throw new ForbiddenRequestError({ message: "You don't have access to approve this request" });
|
||||||
|
}
|
||||||
|
|
||||||
const existingReviews = await accessApprovalRequestReviewerDAL.find({ requestId: accessApprovalRequest.id });
|
const existingReviews = await accessApprovalRequestReviewerDAL.find({ requestId: accessApprovalRequest.id });
|
||||||
if (existingReviews.some((review) => review.status === ApprovalStatus.REJECTED)) {
|
if (existingReviews.some((review) => review.status === ApprovalStatus.REJECTED)) {
|
||||||
throw new BadRequestError({ message: "The request has already been rejected by another reviewer" });
|
throw new BadRequestError({ message: "The request has already been rejected by another reviewer" });
|
||||||
@ -385,7 +429,7 @@ export const accessApprovalRequestServiceFactory = ({
|
|||||||
|
|
||||||
const getCount = async ({ projectSlug, actor, actorAuthMethod, actorId, actorOrgId }: TGetAccessRequestCountDTO) => {
|
const getCount = async ({ projectSlug, actor, actorAuthMethod, actorId, actorOrgId }: TGetAccessRequestCountDTO) => {
|
||||||
const project = await projectDAL.findProjectBySlug(projectSlug, actorOrgId);
|
const project = await projectDAL.findProjectBySlug(projectSlug, actorOrgId);
|
||||||
if (!project) throw new UnauthorizedError({ message: "Project not found" });
|
if (!project) throw new NotFoundError({ message: `Project with slug '${projectSlug}' not found` });
|
||||||
|
|
||||||
const { membership } = await permissionService.getProjectPermission(
|
const { membership } = await permissionService.getProjectPermission(
|
||||||
actor,
|
actor,
|
||||||
@ -394,7 +438,9 @@ export const accessApprovalRequestServiceFactory = ({
|
|||||||
actorAuthMethod,
|
actorAuthMethod,
|
||||||
actorOrgId
|
actorOrgId
|
||||||
);
|
);
|
||||||
if (!membership) throw new BadRequestError({ message: "User not found in project" });
|
if (!membership) {
|
||||||
|
throw new ForbiddenRequestError({ message: "You are not a member of this project" });
|
||||||
|
}
|
||||||
|
|
||||||
const count = await accessApprovalRequestDAL.getCount({ projectId: project.id });
|
const count = await accessApprovalRequestDAL.getCount({ projectId: project.id });
|
||||||
|
|
||||||
|
@ -5,7 +5,7 @@ import { SecretKeyEncoding } from "@app/db/schemas";
|
|||||||
import { getConfig } from "@app/lib/config/env";
|
import { getConfig } from "@app/lib/config/env";
|
||||||
import { request } from "@app/lib/config/request";
|
import { request } from "@app/lib/config/request";
|
||||||
import { infisicalSymmetricDecrypt, infisicalSymmetricEncypt } from "@app/lib/crypto/encryption";
|
import { infisicalSymmetricDecrypt, infisicalSymmetricEncypt } from "@app/lib/crypto/encryption";
|
||||||
import { BadRequestError } from "@app/lib/errors";
|
import { BadRequestError, NotFoundError, UnauthorizedError } from "@app/lib/errors";
|
||||||
import { blockLocalAndPrivateIpAddresses } from "@app/lib/validator";
|
import { blockLocalAndPrivateIpAddresses } from "@app/lib/validator";
|
||||||
|
|
||||||
import { AUDIT_LOG_STREAM_TIMEOUT } from "../audit-log/audit-log-queue";
|
import { AUDIT_LOG_STREAM_TIMEOUT } from "../audit-log/audit-log-queue";
|
||||||
@ -43,14 +43,15 @@ export const auditLogStreamServiceFactory = ({
|
|||||||
actorOrgId,
|
actorOrgId,
|
||||||
actorAuthMethod
|
actorAuthMethod
|
||||||
}: TCreateAuditLogStreamDTO) => {
|
}: TCreateAuditLogStreamDTO) => {
|
||||||
if (!actorOrgId) throw new BadRequestError({ message: "Missing org id from token" });
|
if (!actorOrgId) throw new UnauthorizedError({ message: "No organization ID attached to authentication token" });
|
||||||
|
|
||||||
const appCfg = getConfig();
|
const appCfg = getConfig();
|
||||||
const plan = await licenseService.getPlan(actorOrgId);
|
const plan = await licenseService.getPlan(actorOrgId);
|
||||||
if (!plan.auditLogStreams)
|
if (!plan.auditLogStreams) {
|
||||||
throw new BadRequestError({
|
throw new BadRequestError({
|
||||||
message: "Failed to create audit log streams due to plan restriction. Upgrade plan to create group."
|
message: "Failed to create audit log streams due to plan restriction. Upgrade plan to create group."
|
||||||
});
|
});
|
||||||
|
}
|
||||||
|
|
||||||
const { permission } = await permissionService.getOrgPermission(
|
const { permission } = await permissionService.getOrgPermission(
|
||||||
actor,
|
actor,
|
||||||
@ -120,7 +121,7 @@ export const auditLogStreamServiceFactory = ({
|
|||||||
actorOrgId,
|
actorOrgId,
|
||||||
actorAuthMethod
|
actorAuthMethod
|
||||||
}: TUpdateAuditLogStreamDTO) => {
|
}: TUpdateAuditLogStreamDTO) => {
|
||||||
if (!actorOrgId) throw new BadRequestError({ message: "Missing org id from token" });
|
if (!actorOrgId) throw new UnauthorizedError({ message: "No organization ID attached to authentication token" });
|
||||||
|
|
||||||
const plan = await licenseService.getPlan(actorOrgId);
|
const plan = await licenseService.getPlan(actorOrgId);
|
||||||
if (!plan.auditLogStreams)
|
if (!plan.auditLogStreams)
|
||||||
@ -129,7 +130,7 @@ export const auditLogStreamServiceFactory = ({
|
|||||||
});
|
});
|
||||||
|
|
||||||
const logStream = await auditLogStreamDAL.findById(id);
|
const logStream = await auditLogStreamDAL.findById(id);
|
||||||
if (!logStream) throw new BadRequestError({ message: "Audit log stream not found" });
|
if (!logStream) throw new NotFoundError({ message: `Audit log stream with ID '${id}' not found` });
|
||||||
|
|
||||||
const { orgId } = logStream;
|
const { orgId } = logStream;
|
||||||
const { permission } = await permissionService.getOrgPermission(actor, actorId, orgId, actorAuthMethod, actorOrgId);
|
const { permission } = await permissionService.getOrgPermission(actor, actorId, orgId, actorAuthMethod, actorOrgId);
|
||||||
@ -178,10 +179,10 @@ export const auditLogStreamServiceFactory = ({
|
|||||||
};
|
};
|
||||||
|
|
||||||
const deleteById = async ({ id, actor, actorId, actorOrgId, actorAuthMethod }: TDeleteAuditLogStreamDTO) => {
|
const deleteById = async ({ id, actor, actorId, actorOrgId, actorAuthMethod }: TDeleteAuditLogStreamDTO) => {
|
||||||
if (!actorOrgId) throw new BadRequestError({ message: "Missing org id from token" });
|
if (!actorOrgId) throw new UnauthorizedError({ message: "No organization ID attached to authentication token" });
|
||||||
|
|
||||||
const logStream = await auditLogStreamDAL.findById(id);
|
const logStream = await auditLogStreamDAL.findById(id);
|
||||||
if (!logStream) throw new BadRequestError({ message: "Audit log stream not found" });
|
if (!logStream) throw new NotFoundError({ message: `Audit log stream with ID '${id}' not found` });
|
||||||
|
|
||||||
const { orgId } = logStream;
|
const { orgId } = logStream;
|
||||||
const { permission } = await permissionService.getOrgPermission(actor, actorId, orgId, actorAuthMethod, actorOrgId);
|
const { permission } = await permissionService.getOrgPermission(actor, actorId, orgId, actorAuthMethod, actorOrgId);
|
||||||
@ -193,7 +194,7 @@ export const auditLogStreamServiceFactory = ({
|
|||||||
|
|
||||||
const getById = async ({ id, actor, actorId, actorOrgId, actorAuthMethod }: TGetDetailsAuditLogStreamDTO) => {
|
const getById = async ({ id, actor, actorId, actorOrgId, actorAuthMethod }: TGetDetailsAuditLogStreamDTO) => {
|
||||||
const logStream = await auditLogStreamDAL.findById(id);
|
const logStream = await auditLogStreamDAL.findById(id);
|
||||||
if (!logStream) throw new BadRequestError({ message: "Audit log stream not found" });
|
if (!logStream) throw new NotFoundError({ message: `Audit log stream with ID '${id}' not found` });
|
||||||
|
|
||||||
const { orgId } = logStream;
|
const { orgId } = logStream;
|
||||||
const { permission } = await permissionService.getOrgPermission(actor, actorId, orgId, actorAuthMethod, actorOrgId);
|
const { permission } = await permissionService.getOrgPermission(actor, actorId, orgId, actorAuthMethod, actorOrgId);
|
||||||
|
@ -1,9 +1,10 @@
|
|||||||
import { Knex } from "knex";
|
// weird commonjs-related error in the CI requires us to do the import like this
|
||||||
|
import knex from "knex";
|
||||||
|
|
||||||
import { TDbClient } from "@app/db";
|
import { TDbClient } from "@app/db";
|
||||||
import { AuditLogsSchema, TableName } from "@app/db/schemas";
|
import { TableName } from "@app/db/schemas";
|
||||||
import { DatabaseError } from "@app/lib/errors";
|
import { DatabaseError, GatewayTimeoutError } from "@app/lib/errors";
|
||||||
import { ormify, selectAllTableCols, stripUndefinedInWhere } from "@app/lib/knex";
|
import { ormify, selectAllTableCols } from "@app/lib/knex";
|
||||||
import { logger } from "@app/lib/logger";
|
import { logger } from "@app/lib/logger";
|
||||||
import { QueueName } from "@app/queue";
|
import { QueueName } from "@app/queue";
|
||||||
import { ActorType } from "@app/services/auth/auth-type";
|
import { ActorType } from "@app/services/auth/auth-type";
|
||||||
@ -46,71 +47,83 @@ export const auditLogDALFactory = (db: TDbClient) => {
|
|||||||
eventType?: EventType[];
|
eventType?: EventType[];
|
||||||
eventMetadata?: Record<string, string>;
|
eventMetadata?: Record<string, string>;
|
||||||
},
|
},
|
||||||
tx?: Knex
|
tx?: knex.Knex
|
||||||
) => {
|
) => {
|
||||||
|
if (!orgId && !projectId) {
|
||||||
|
throw new Error("Either orgId or projectId must be provided");
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
// Find statements
|
||||||
const sqlQuery = (tx || db.replicaNode())(TableName.AuditLog)
|
const sqlQuery = (tx || db.replicaNode())(TableName.AuditLog)
|
||||||
.where(
|
// eslint-disable-next-line func-names
|
||||||
stripUndefinedInWhere({
|
.where(function () {
|
||||||
projectId,
|
if (orgId) {
|
||||||
[`${TableName.AuditLog}.orgId`]: orgId,
|
void this.where(`${TableName.AuditLog}.orgId`, orgId);
|
||||||
userAgentType
|
} else if (projectId) {
|
||||||
})
|
void this.where(`${TableName.AuditLog}.projectId`, projectId);
|
||||||
)
|
}
|
||||||
|
});
|
||||||
|
|
||||||
.leftJoin(TableName.Project, `${TableName.AuditLog}.projectId`, `${TableName.Project}.id`)
|
if (userAgentType) {
|
||||||
|
void sqlQuery.where("userAgentType", userAgentType);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Select statements
|
||||||
|
void sqlQuery
|
||||||
.select(selectAllTableCols(TableName.AuditLog))
|
.select(selectAllTableCols(TableName.AuditLog))
|
||||||
|
|
||||||
.select(
|
|
||||||
db.ref("name").withSchema(TableName.Project).as("projectName"),
|
|
||||||
db.ref("slug").withSchema(TableName.Project).as("projectSlug")
|
|
||||||
)
|
|
||||||
|
|
||||||
.limit(limit)
|
.limit(limit)
|
||||||
.offset(offset)
|
.offset(offset)
|
||||||
.orderBy(`${TableName.AuditLog}.createdAt`, "desc");
|
.orderBy(`${TableName.AuditLog}.createdAt`, "desc");
|
||||||
|
|
||||||
|
// Special case: Filter by actor ID
|
||||||
if (actorId) {
|
if (actorId) {
|
||||||
void sqlQuery.whereRaw(`"actorMetadata"->>'userId' = ?`, [actorId]);
|
void sqlQuery.whereRaw(`"actorMetadata" @> jsonb_build_object('userId', ?::text)`, [actorId]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Special case: Filter by key/value pairs in eventMetadata field
|
||||||
if (eventMetadata && Object.keys(eventMetadata).length) {
|
if (eventMetadata && Object.keys(eventMetadata).length) {
|
||||||
Object.entries(eventMetadata).forEach(([key, value]) => {
|
Object.entries(eventMetadata).forEach(([key, value]) => {
|
||||||
void sqlQuery.whereRaw(`"eventMetadata"->>'${key}' = ?`, [value]);
|
void sqlQuery.whereRaw(`"eventMetadata" @> jsonb_build_object(?::text, ?::text)`, [key, value]);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Filter by actor type
|
||||||
if (actorType) {
|
if (actorType) {
|
||||||
void sqlQuery.where("actor", actorType);
|
void sqlQuery.where("actor", actorType);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Filter by event types
|
||||||
if (eventType?.length) {
|
if (eventType?.length) {
|
||||||
void sqlQuery.whereIn("eventType", eventType);
|
void sqlQuery.whereIn("eventType", eventType);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Filter by date range
|
||||||
if (startDate) {
|
if (startDate) {
|
||||||
void sqlQuery.where(`${TableName.AuditLog}.createdAt`, ">=", startDate);
|
void sqlQuery.where(`${TableName.AuditLog}.createdAt`, ">=", startDate);
|
||||||
}
|
}
|
||||||
if (endDate) {
|
if (endDate) {
|
||||||
void sqlQuery.where(`${TableName.AuditLog}.createdAt`, "<=", endDate);
|
void sqlQuery.where(`${TableName.AuditLog}.createdAt`, "<=", endDate);
|
||||||
}
|
}
|
||||||
const docs = await sqlQuery;
|
|
||||||
|
|
||||||
return docs.map((doc) => ({
|
// we timeout long running queries to prevent DB resource issues (2 minutes)
|
||||||
...AuditLogsSchema.parse(doc),
|
const docs = await sqlQuery.timeout(1000 * 120);
|
||||||
project: {
|
|
||||||
name: doc.projectName,
|
return docs;
|
||||||
slug: doc.projectSlug
|
|
||||||
}
|
|
||||||
}));
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
if (error instanceof knex.KnexTimeoutError) {
|
||||||
|
throw new GatewayTimeoutError({
|
||||||
|
error,
|
||||||
|
message: "Failed to fetch audit logs due to timeout. Add more search filters."
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
throw new DatabaseError({ error });
|
throw new DatabaseError({ error });
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// delete all audit log that have expired
|
// delete all audit log that have expired
|
||||||
const pruneAuditLog = async (tx?: Knex) => {
|
const pruneAuditLog = async (tx?: knex.Knex) => {
|
||||||
const AUDIT_LOG_PRUNE_BATCH_SIZE = 10000;
|
const AUDIT_LOG_PRUNE_BATCH_SIZE = 10000;
|
||||||
const MAX_RETRY_ON_FAILURE = 3;
|
const MAX_RETRY_ON_FAILURE = 3;
|
||||||
|
|
||||||
@ -126,6 +139,7 @@ export const auditLogDALFactory = (db: TDbClient) => {
|
|||||||
.where("expiresAt", "<", today)
|
.where("expiresAt", "<", today)
|
||||||
.select("id")
|
.select("id")
|
||||||
.limit(AUDIT_LOG_PRUNE_BATCH_SIZE);
|
.limit(AUDIT_LOG_PRUNE_BATCH_SIZE);
|
||||||
|
|
||||||
// eslint-disable-next-line no-await-in-loop
|
// eslint-disable-next-line no-await-in-loop
|
||||||
deletedAuditLogIds = await (tx || db)(TableName.AuditLog)
|
deletedAuditLogIds = await (tx || db)(TableName.AuditLog)
|
||||||
.whereIn("id", findExpiredLogSubQuery)
|
.whereIn("id", findExpiredLogSubQuery)
|
||||||
|
@ -74,6 +74,7 @@ export const auditLogQueueServiceFactory = ({
|
|||||||
actorMetadata: actor.metadata,
|
actorMetadata: actor.metadata,
|
||||||
userAgent,
|
userAgent,
|
||||||
projectId,
|
projectId,
|
||||||
|
projectName: project?.name,
|
||||||
ipAddress,
|
ipAddress,
|
||||||
orgId,
|
orgId,
|
||||||
eventType: event.type,
|
eventType: event.type,
|
||||||
|
@ -24,6 +24,7 @@ export const auditLogServiceFactory = ({
|
|||||||
permissionService
|
permissionService
|
||||||
}: TAuditLogServiceFactoryDep) => {
|
}: TAuditLogServiceFactoryDep) => {
|
||||||
const listAuditLogs = async ({ actorAuthMethod, actorId, actorOrgId, actor, filter }: TListProjectAuditLogDTO) => {
|
const listAuditLogs = async ({ actorAuthMethod, actorId, actorOrgId, actor, filter }: TListProjectAuditLogDTO) => {
|
||||||
|
// Filter logs for specific project
|
||||||
if (filter.projectId) {
|
if (filter.projectId) {
|
||||||
const { permission } = await permissionService.getProjectPermission(
|
const { permission } = await permissionService.getProjectPermission(
|
||||||
actor,
|
actor,
|
||||||
@ -34,6 +35,7 @@ export const auditLogServiceFactory = ({
|
|||||||
);
|
);
|
||||||
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.AuditLogs);
|
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.AuditLogs);
|
||||||
} else {
|
} else {
|
||||||
|
// Organization-wide logs
|
||||||
const { permission } = await permissionService.getOrgPermission(
|
const { permission } = await permissionService.getOrgPermission(
|
||||||
actor,
|
actor,
|
||||||
actorId,
|
actorId,
|
||||||
@ -44,13 +46,12 @@ export const auditLogServiceFactory = ({
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* NOTE (dangtony98): Update this to organization-level audit log permission check once audit logs are moved
|
* NOTE (dangtony98): Update this to organization-level audit log permission check once audit logs are moved
|
||||||
* to the organization level
|
* to the organization level ✅
|
||||||
*/
|
*/
|
||||||
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Read, OrgPermissionSubjects.Member);
|
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Read, OrgPermissionSubjects.AuditLogs);
|
||||||
}
|
}
|
||||||
|
|
||||||
// If project ID is not provided, then we need to return all the audit logs for the organization itself.
|
// If project ID is not provided, then we need to return all the audit logs for the organization itself.
|
||||||
|
|
||||||
const auditLogs = await auditLogDAL.find({
|
const auditLogs = await auditLogDAL.find({
|
||||||
startDate: filter.startDate,
|
startDate: filter.startDate,
|
||||||
endDate: filter.endDate,
|
endDate: filter.endDate,
|
||||||
|
@ -1,3 +1,4 @@
|
|||||||
|
import { SymmetricEncryption } from "@app/lib/crypto/cipher";
|
||||||
import { TProjectPermission } from "@app/lib/types";
|
import { TProjectPermission } from "@app/lib/types";
|
||||||
import { ActorType } from "@app/services/auth/auth-type";
|
import { ActorType } from "@app/services/auth/auth-type";
|
||||||
import { CaStatus } from "@app/services/certificate-authority/certificate-authority-types";
|
import { CaStatus } from "@app/services/certificate-authority/certificate-authority-types";
|
||||||
@ -122,6 +123,7 @@ export enum EventType {
|
|||||||
UPDATE_WEBHOOK_STATUS = "update-webhook-status",
|
UPDATE_WEBHOOK_STATUS = "update-webhook-status",
|
||||||
DELETE_WEBHOOK = "delete-webhook",
|
DELETE_WEBHOOK = "delete-webhook",
|
||||||
GET_SECRET_IMPORTS = "get-secret-imports",
|
GET_SECRET_IMPORTS = "get-secret-imports",
|
||||||
|
GET_SECRET_IMPORT = "get-secret-import",
|
||||||
CREATE_SECRET_IMPORT = "create-secret-import",
|
CREATE_SECRET_IMPORT = "create-secret-import",
|
||||||
UPDATE_SECRET_IMPORT = "update-secret-import",
|
UPDATE_SECRET_IMPORT = "update-secret-import",
|
||||||
DELETE_SECRET_IMPORT = "delete-secret-import",
|
DELETE_SECRET_IMPORT = "delete-secret-import",
|
||||||
@ -182,7 +184,15 @@ export enum EventType {
|
|||||||
DELETE_SLACK_INTEGRATION = "delete-slack-integration",
|
DELETE_SLACK_INTEGRATION = "delete-slack-integration",
|
||||||
GET_PROJECT_SLACK_CONFIG = "get-project-slack-config",
|
GET_PROJECT_SLACK_CONFIG = "get-project-slack-config",
|
||||||
UPDATE_PROJECT_SLACK_CONFIG = "update-project-slack-config",
|
UPDATE_PROJECT_SLACK_CONFIG = "update-project-slack-config",
|
||||||
INTEGRATION_SYNCED = "integration-synced"
|
INTEGRATION_SYNCED = "integration-synced",
|
||||||
|
CREATE_CMEK = "create-cmek",
|
||||||
|
UPDATE_CMEK = "update-cmek",
|
||||||
|
DELETE_CMEK = "delete-cmek",
|
||||||
|
GET_CMEKS = "get-cmeks",
|
||||||
|
CMEK_ENCRYPT = "cmek-encrypt",
|
||||||
|
CMEK_DECRYPT = "cmek-decrypt",
|
||||||
|
UPDATE_EXTERNAL_GROUP_ORG_ROLE_MAPPINGS = "update-external-group-org-role-mapping",
|
||||||
|
GET_EXTERNAL_GROUP_ORG_ROLE_MAPPINGS = "get-external-group-org-role-mapping"
|
||||||
}
|
}
|
||||||
|
|
||||||
interface UserActorMetadata {
|
interface UserActorMetadata {
|
||||||
@ -1004,6 +1014,14 @@ interface GetSecretImportsEvent {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface GetSecretImportEvent {
|
||||||
|
type: EventType.GET_SECRET_IMPORT;
|
||||||
|
metadata: {
|
||||||
|
secretImportId: string;
|
||||||
|
folderId: string;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
interface CreateSecretImportEvent {
|
interface CreateSecretImportEvent {
|
||||||
type: EventType.CREATE_SECRET_IMPORT;
|
type: EventType.CREATE_SECRET_IMPORT;
|
||||||
metadata: {
|
metadata: {
|
||||||
@ -1350,7 +1368,7 @@ interface CreateKmsEvent {
|
|||||||
metadata: {
|
metadata: {
|
||||||
kmsId: string;
|
kmsId: string;
|
||||||
provider: string;
|
provider: string;
|
||||||
slug: string;
|
name: string;
|
||||||
description?: string;
|
description?: string;
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@ -1359,7 +1377,7 @@ interface DeleteKmsEvent {
|
|||||||
type: EventType.DELETE_KMS;
|
type: EventType.DELETE_KMS;
|
||||||
metadata: {
|
metadata: {
|
||||||
kmsId: string;
|
kmsId: string;
|
||||||
slug: string;
|
name: string;
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1368,7 +1386,7 @@ interface UpdateKmsEvent {
|
|||||||
metadata: {
|
metadata: {
|
||||||
kmsId: string;
|
kmsId: string;
|
||||||
provider: string;
|
provider: string;
|
||||||
slug?: string;
|
name?: string;
|
||||||
description?: string;
|
description?: string;
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@ -1377,7 +1395,7 @@ interface GetKmsEvent {
|
|||||||
type: EventType.GET_KMS;
|
type: EventType.GET_KMS;
|
||||||
metadata: {
|
metadata: {
|
||||||
kmsId: string;
|
kmsId: string;
|
||||||
slug: string;
|
name: string;
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1386,7 +1404,7 @@ interface UpdateProjectKmsEvent {
|
|||||||
metadata: {
|
metadata: {
|
||||||
secretManagerKmsKey: {
|
secretManagerKmsKey: {
|
||||||
id: string;
|
id: string;
|
||||||
slug: string;
|
name: string;
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@ -1541,6 +1559,65 @@ interface IntegrationSyncedEvent {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface CreateCmekEvent {
|
||||||
|
type: EventType.CREATE_CMEK;
|
||||||
|
metadata: {
|
||||||
|
keyId: string;
|
||||||
|
name: string;
|
||||||
|
description?: string;
|
||||||
|
encryptionAlgorithm: SymmetricEncryption;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
interface DeleteCmekEvent {
|
||||||
|
type: EventType.DELETE_CMEK;
|
||||||
|
metadata: {
|
||||||
|
keyId: string;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
interface UpdateCmekEvent {
|
||||||
|
type: EventType.UPDATE_CMEK;
|
||||||
|
metadata: {
|
||||||
|
keyId: string;
|
||||||
|
name?: string;
|
||||||
|
description?: string;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
interface GetCmeksEvent {
|
||||||
|
type: EventType.GET_CMEKS;
|
||||||
|
metadata: {
|
||||||
|
keyIds: string[];
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
interface CmekEncryptEvent {
|
||||||
|
type: EventType.CMEK_ENCRYPT;
|
||||||
|
metadata: {
|
||||||
|
keyId: string;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
interface CmekDecryptEvent {
|
||||||
|
type: EventType.CMEK_DECRYPT;
|
||||||
|
metadata: {
|
||||||
|
keyId: string;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
interface GetExternalGroupOrgRoleMappingsEvent {
|
||||||
|
type: EventType.GET_EXTERNAL_GROUP_ORG_ROLE_MAPPINGS;
|
||||||
|
metadata?: Record<string, never>; // not needed, based off orgId
|
||||||
|
}
|
||||||
|
|
||||||
|
interface UpdateExternalGroupOrgRoleMappingsEvent {
|
||||||
|
type: EventType.UPDATE_EXTERNAL_GROUP_ORG_ROLE_MAPPINGS;
|
||||||
|
metadata: {
|
||||||
|
mappings: { groupName: string; roleSlug: string }[];
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
export type Event =
|
export type Event =
|
||||||
| GetSecretsEvent
|
| GetSecretsEvent
|
||||||
| GetSecretEvent
|
| GetSecretEvent
|
||||||
@ -1620,6 +1697,7 @@ export type Event =
|
|||||||
| UpdateWebhookStatusEvent
|
| UpdateWebhookStatusEvent
|
||||||
| DeleteWebhookEvent
|
| DeleteWebhookEvent
|
||||||
| GetSecretImportsEvent
|
| GetSecretImportsEvent
|
||||||
|
| GetSecretImportEvent
|
||||||
| CreateSecretImportEvent
|
| CreateSecretImportEvent
|
||||||
| UpdateSecretImportEvent
|
| UpdateSecretImportEvent
|
||||||
| DeleteSecretImportEvent
|
| DeleteSecretImportEvent
|
||||||
@ -1680,4 +1758,12 @@ export type Event =
|
|||||||
| GetSlackIntegration
|
| GetSlackIntegration
|
||||||
| UpdateProjectSlackConfig
|
| UpdateProjectSlackConfig
|
||||||
| GetProjectSlackConfig
|
| GetProjectSlackConfig
|
||||||
| IntegrationSyncedEvent;
|
| IntegrationSyncedEvent
|
||||||
|
| CreateCmekEvent
|
||||||
|
| UpdateCmekEvent
|
||||||
|
| DeleteCmekEvent
|
||||||
|
| GetCmeksEvent
|
||||||
|
| CmekEncryptEvent
|
||||||
|
| CmekDecryptEvent
|
||||||
|
| GetExternalGroupOrgRoleMappingsEvent
|
||||||
|
| UpdateExternalGroupOrgRoleMappingsEvent;
|
||||||
|
@ -2,10 +2,9 @@ import { ForbiddenError } from "@casl/ability";
|
|||||||
import * as x509 from "@peculiar/x509";
|
import * as x509 from "@peculiar/x509";
|
||||||
|
|
||||||
import { TCertificateAuthorityCrlDALFactory } from "@app/ee/services/certificate-authority-crl/certificate-authority-crl-dal";
|
import { TCertificateAuthorityCrlDALFactory } from "@app/ee/services/certificate-authority-crl/certificate-authority-crl-dal";
|
||||||
// import { TLicenseServiceFactory } from "@app/ee/services/license/license-service";
|
|
||||||
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service";
|
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service";
|
||||||
import { ProjectPermissionActions, ProjectPermissionSub } from "@app/ee/services/permission/project-permission";
|
import { ProjectPermissionActions, ProjectPermissionSub } from "@app/ee/services/permission/project-permission";
|
||||||
import { BadRequestError, NotFoundError } from "@app/lib/errors";
|
import { NotFoundError } from "@app/lib/errors";
|
||||||
import { TCertificateAuthorityDALFactory } from "@app/services/certificate-authority/certificate-authority-dal";
|
import { TCertificateAuthorityDALFactory } from "@app/services/certificate-authority/certificate-authority-dal";
|
||||||
import { TKmsServiceFactory } from "@app/services/kms/kms-service";
|
import { TKmsServiceFactory } from "@app/services/kms/kms-service";
|
||||||
import { TProjectDALFactory } from "@app/services/project/project-dal";
|
import { TProjectDALFactory } from "@app/services/project/project-dal";
|
||||||
@ -19,7 +18,6 @@ type TCertificateAuthorityCrlServiceFactoryDep = {
|
|||||||
projectDAL: Pick<TProjectDALFactory, "findOne" | "updateById" | "transaction">;
|
projectDAL: Pick<TProjectDALFactory, "findOne" | "updateById" | "transaction">;
|
||||||
kmsService: Pick<TKmsServiceFactory, "decryptWithKmsKey" | "generateKmsKey">;
|
kmsService: Pick<TKmsServiceFactory, "decryptWithKmsKey" | "generateKmsKey">;
|
||||||
permissionService: Pick<TPermissionServiceFactory, "getProjectPermission">;
|
permissionService: Pick<TPermissionServiceFactory, "getProjectPermission">;
|
||||||
// licenseService: Pick<TLicenseServiceFactory, "getPlan">;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export type TCertificateAuthorityCrlServiceFactory = ReturnType<typeof certificateAuthorityCrlServiceFactory>;
|
export type TCertificateAuthorityCrlServiceFactory = ReturnType<typeof certificateAuthorityCrlServiceFactory>;
|
||||||
@ -36,7 +34,7 @@ export const certificateAuthorityCrlServiceFactory = ({
|
|||||||
*/
|
*/
|
||||||
const getCrlById = async (crlId: TGetCrlById) => {
|
const getCrlById = async (crlId: TGetCrlById) => {
|
||||||
const caCrl = await certificateAuthorityCrlDAL.findById(crlId);
|
const caCrl = await certificateAuthorityCrlDAL.findById(crlId);
|
||||||
if (!caCrl) throw new NotFoundError({ message: "CRL not found" });
|
if (!caCrl) throw new NotFoundError({ message: `CRL with ID '${crlId}' not found` });
|
||||||
|
|
||||||
const ca = await certificateAuthorityDAL.findById(caCrl.caId);
|
const ca = await certificateAuthorityDAL.findById(caCrl.caId);
|
||||||
|
|
||||||
@ -66,7 +64,7 @@ export const certificateAuthorityCrlServiceFactory = ({
|
|||||||
*/
|
*/
|
||||||
const getCaCrls = async ({ caId, actorId, actorAuthMethod, actor, actorOrgId }: TGetCaCrlsDTO) => {
|
const getCaCrls = async ({ caId, actorId, actorAuthMethod, actor, actorOrgId }: TGetCaCrlsDTO) => {
|
||||||
const ca = await certificateAuthorityDAL.findById(caId);
|
const ca = await certificateAuthorityDAL.findById(caId);
|
||||||
if (!ca) throw new BadRequestError({ message: "CA not found" });
|
if (!ca) throw new NotFoundError({ message: `CA with ID '${caId}' not found` });
|
||||||
|
|
||||||
const { permission } = await permissionService.getProjectPermission(
|
const { permission } = await permissionService.getProjectPermission(
|
||||||
actor,
|
actor,
|
||||||
@ -81,13 +79,6 @@ export const certificateAuthorityCrlServiceFactory = ({
|
|||||||
ProjectPermissionSub.CertificateAuthorities
|
ProjectPermissionSub.CertificateAuthorities
|
||||||
);
|
);
|
||||||
|
|
||||||
// const plan = await licenseService.getPlan(actorOrgId);
|
|
||||||
// if (!plan.caCrl)
|
|
||||||
// throw new BadRequestError({
|
|
||||||
// message:
|
|
||||||
// "Failed to get CA certificate revocation lists (CRLs) due to plan restriction. Upgrade plan to get the CA CRL."
|
|
||||||
// });
|
|
||||||
|
|
||||||
const caCrls = await certificateAuthorityCrlDAL.find({ caId: ca.id }, { sort: [["createdAt", "desc"]] });
|
const caCrls = await certificateAuthorityCrlDAL.find({ caId: ca.id }, { sort: [["createdAt", "desc"]] });
|
||||||
|
|
||||||
const keyId = await getProjectKmsCertificateKeyId({
|
const keyId = await getProjectKmsCertificateKeyId({
|
||||||
|
@ -211,7 +211,7 @@ export const certificateEstServiceFactory = ({
|
|||||||
const certTemplate = await certificateTemplateDAL.findById(certificateTemplateId);
|
const certTemplate = await certificateTemplateDAL.findById(certificateTemplateId);
|
||||||
if (!certTemplate) {
|
if (!certTemplate) {
|
||||||
throw new NotFoundError({
|
throw new NotFoundError({
|
||||||
message: "Certificate template not found"
|
message: `Certificate template with ID '${certificateTemplateId}' not found`
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -236,7 +236,7 @@ export const certificateEstServiceFactory = ({
|
|||||||
const ca = await certificateAuthorityDAL.findById(certTemplate.caId);
|
const ca = await certificateAuthorityDAL.findById(certTemplate.caId);
|
||||||
if (!ca) {
|
if (!ca) {
|
||||||
throw new NotFoundError({
|
throw new NotFoundError({
|
||||||
message: "Certificate Authority not found"
|
message: `Certificate Authority with ID '${certTemplate.caId}' not found`
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -7,7 +7,7 @@ import { TPermissionServiceFactory } from "@app/ee/services/permission/permissio
|
|||||||
import { ProjectPermissionActions, ProjectPermissionSub } from "@app/ee/services/permission/project-permission";
|
import { ProjectPermissionActions, ProjectPermissionSub } from "@app/ee/services/permission/project-permission";
|
||||||
import { getConfig } from "@app/lib/config/env";
|
import { getConfig } from "@app/lib/config/env";
|
||||||
import { infisicalSymmetricDecrypt } from "@app/lib/crypto/encryption";
|
import { infisicalSymmetricDecrypt } from "@app/lib/crypto/encryption";
|
||||||
import { BadRequestError } from "@app/lib/errors";
|
import { BadRequestError, NotFoundError } from "@app/lib/errors";
|
||||||
import { logger } from "@app/lib/logger";
|
import { logger } from "@app/lib/logger";
|
||||||
import { TProjectDALFactory } from "@app/services/project/project-dal";
|
import { TProjectDALFactory } from "@app/services/project/project-dal";
|
||||||
import { TSecretFolderDALFactory } from "@app/services/secret-folder/secret-folder-dal";
|
import { TSecretFolderDALFactory } from "@app/services/secret-folder/secret-folder-dal";
|
||||||
@ -61,7 +61,7 @@ export const dynamicSecretLeaseServiceFactory = ({
|
|||||||
}: TCreateDynamicSecretLeaseDTO) => {
|
}: TCreateDynamicSecretLeaseDTO) => {
|
||||||
const appCfg = getConfig();
|
const appCfg = getConfig();
|
||||||
const project = await projectDAL.findProjectBySlug(projectSlug, actorOrgId);
|
const project = await projectDAL.findProjectBySlug(projectSlug, actorOrgId);
|
||||||
if (!project) throw new BadRequestError({ message: "Project not found" });
|
if (!project) throw new NotFoundError({ message: `Project with slug '${projectSlug}' not found` });
|
||||||
|
|
||||||
const projectId = project.id;
|
const projectId = project.id;
|
||||||
const { permission } = await permissionService.getProjectPermission(
|
const { permission } = await permissionService.getProjectPermission(
|
||||||
@ -84,10 +84,16 @@ export const dynamicSecretLeaseServiceFactory = ({
|
|||||||
}
|
}
|
||||||
|
|
||||||
const folder = await folderDAL.findBySecretPath(projectId, environmentSlug, path);
|
const folder = await folderDAL.findBySecretPath(projectId, environmentSlug, path);
|
||||||
if (!folder) throw new BadRequestError({ message: "Folder not found" });
|
if (!folder)
|
||||||
|
throw new NotFoundError({
|
||||||
|
message: `Folder with path '${path}' in environment with slug '${environmentSlug}' not found`
|
||||||
|
});
|
||||||
|
|
||||||
const dynamicSecretCfg = await dynamicSecretDAL.findOne({ name, folderId: folder.id });
|
const dynamicSecretCfg = await dynamicSecretDAL.findOne({ name, folderId: folder.id });
|
||||||
if (!dynamicSecretCfg) throw new BadRequestError({ message: "Dynamic secret not found" });
|
if (!dynamicSecretCfg)
|
||||||
|
throw new NotFoundError({
|
||||||
|
message: `Dynamic secret with name '${name}' in folder with path '${path}' not found`
|
||||||
|
});
|
||||||
|
|
||||||
const totalLeasesTaken = await dynamicSecretLeaseDAL.countLeasesForDynamicSecret(dynamicSecretCfg.id);
|
const totalLeasesTaken = await dynamicSecretLeaseDAL.countLeasesForDynamicSecret(dynamicSecretCfg.id);
|
||||||
if (totalLeasesTaken >= appCfg.MAX_LEASE_LIMIT)
|
if (totalLeasesTaken >= appCfg.MAX_LEASE_LIMIT)
|
||||||
@ -134,7 +140,7 @@ export const dynamicSecretLeaseServiceFactory = ({
|
|||||||
leaseId
|
leaseId
|
||||||
}: TRenewDynamicSecretLeaseDTO) => {
|
}: TRenewDynamicSecretLeaseDTO) => {
|
||||||
const project = await projectDAL.findProjectBySlug(projectSlug, actorOrgId);
|
const project = await projectDAL.findProjectBySlug(projectSlug, actorOrgId);
|
||||||
if (!project) throw new BadRequestError({ message: "Project not found" });
|
if (!project) throw new NotFoundError({ message: `Project with slug '${projectSlug}' not found` });
|
||||||
|
|
||||||
const projectId = project.id;
|
const projectId = project.id;
|
||||||
const { permission } = await permissionService.getProjectPermission(
|
const { permission } = await permissionService.getProjectPermission(
|
||||||
@ -157,10 +163,15 @@ export const dynamicSecretLeaseServiceFactory = ({
|
|||||||
}
|
}
|
||||||
|
|
||||||
const folder = await folderDAL.findBySecretPath(projectId, environmentSlug, path);
|
const folder = await folderDAL.findBySecretPath(projectId, environmentSlug, path);
|
||||||
if (!folder) throw new BadRequestError({ message: "Folder not found" });
|
if (!folder)
|
||||||
|
throw new NotFoundError({
|
||||||
|
message: `Folder with path '${path}' in environment with slug '${environmentSlug}' not found`
|
||||||
|
});
|
||||||
|
|
||||||
const dynamicSecretLease = await dynamicSecretLeaseDAL.findById(leaseId);
|
const dynamicSecretLease = await dynamicSecretLeaseDAL.findById(leaseId);
|
||||||
if (!dynamicSecretLease) throw new BadRequestError({ message: "Dynamic secret lease not found" });
|
if (!dynamicSecretLease) {
|
||||||
|
throw new NotFoundError({ message: `Dynamic secret lease with ID '${leaseId}' not found` });
|
||||||
|
}
|
||||||
|
|
||||||
const dynamicSecretCfg = dynamicSecretLease.dynamicSecret;
|
const dynamicSecretCfg = dynamicSecretLease.dynamicSecret;
|
||||||
const selectedProvider = dynamicSecretProviders[dynamicSecretCfg.type as DynamicSecretProviders];
|
const selectedProvider = dynamicSecretProviders[dynamicSecretCfg.type as DynamicSecretProviders];
|
||||||
@ -208,7 +219,7 @@ export const dynamicSecretLeaseServiceFactory = ({
|
|||||||
isForced
|
isForced
|
||||||
}: TDeleteDynamicSecretLeaseDTO) => {
|
}: TDeleteDynamicSecretLeaseDTO) => {
|
||||||
const project = await projectDAL.findProjectBySlug(projectSlug, actorOrgId);
|
const project = await projectDAL.findProjectBySlug(projectSlug, actorOrgId);
|
||||||
if (!project) throw new BadRequestError({ message: "Project not found" });
|
if (!project) throw new NotFoundError({ message: `Project with slug '${projectSlug}' not found` });
|
||||||
|
|
||||||
const projectId = project.id;
|
const projectId = project.id;
|
||||||
const { permission } = await permissionService.getProjectPermission(
|
const { permission } = await permissionService.getProjectPermission(
|
||||||
@ -224,10 +235,14 @@ export const dynamicSecretLeaseServiceFactory = ({
|
|||||||
);
|
);
|
||||||
|
|
||||||
const folder = await folderDAL.findBySecretPath(projectId, environmentSlug, path);
|
const folder = await folderDAL.findBySecretPath(projectId, environmentSlug, path);
|
||||||
if (!folder) throw new BadRequestError({ message: "Folder not found" });
|
if (!folder)
|
||||||
|
throw new NotFoundError({
|
||||||
|
message: `Folder with path '${path}' in environment with slug '${environmentSlug}' not found`
|
||||||
|
});
|
||||||
|
|
||||||
const dynamicSecretLease = await dynamicSecretLeaseDAL.findById(leaseId);
|
const dynamicSecretLease = await dynamicSecretLeaseDAL.findById(leaseId);
|
||||||
if (!dynamicSecretLease) throw new BadRequestError({ message: "Dynamic secret lease not found" });
|
if (!dynamicSecretLease)
|
||||||
|
throw new NotFoundError({ message: `Dynamic secret lease with ID '${leaseId}' not found` });
|
||||||
|
|
||||||
const dynamicSecretCfg = dynamicSecretLease.dynamicSecret;
|
const dynamicSecretCfg = dynamicSecretLease.dynamicSecret;
|
||||||
const selectedProvider = dynamicSecretProviders[dynamicSecretCfg.type as DynamicSecretProviders];
|
const selectedProvider = dynamicSecretProviders[dynamicSecretCfg.type as DynamicSecretProviders];
|
||||||
@ -273,7 +288,7 @@ export const dynamicSecretLeaseServiceFactory = ({
|
|||||||
actorAuthMethod
|
actorAuthMethod
|
||||||
}: TListDynamicSecretLeasesDTO) => {
|
}: TListDynamicSecretLeasesDTO) => {
|
||||||
const project = await projectDAL.findProjectBySlug(projectSlug, actorOrgId);
|
const project = await projectDAL.findProjectBySlug(projectSlug, actorOrgId);
|
||||||
if (!project) throw new BadRequestError({ message: "Project not found" });
|
if (!project) throw new NotFoundError({ message: `Project with slug '${projectSlug}' not found` });
|
||||||
|
|
||||||
const projectId = project.id;
|
const projectId = project.id;
|
||||||
const { permission } = await permissionService.getProjectPermission(
|
const { permission } = await permissionService.getProjectPermission(
|
||||||
@ -289,10 +304,16 @@ export const dynamicSecretLeaseServiceFactory = ({
|
|||||||
);
|
);
|
||||||
|
|
||||||
const folder = await folderDAL.findBySecretPath(projectId, environmentSlug, path);
|
const folder = await folderDAL.findBySecretPath(projectId, environmentSlug, path);
|
||||||
if (!folder) throw new BadRequestError({ message: "Folder not found" });
|
if (!folder)
|
||||||
|
throw new NotFoundError({
|
||||||
|
message: `Folder with path '${path}' in environment with slug '${environmentSlug}' not found`
|
||||||
|
});
|
||||||
|
|
||||||
const dynamicSecretCfg = await dynamicSecretDAL.findOne({ name, folderId: folder.id });
|
const dynamicSecretCfg = await dynamicSecretDAL.findOne({ name, folderId: folder.id });
|
||||||
if (!dynamicSecretCfg) throw new BadRequestError({ message: "Dynamic secret not found" });
|
if (!dynamicSecretCfg)
|
||||||
|
throw new NotFoundError({
|
||||||
|
message: `Dynamic secret with name '${name}' in folder with path '${path}' not found`
|
||||||
|
});
|
||||||
|
|
||||||
const dynamicSecretLeases = await dynamicSecretLeaseDAL.find({ dynamicSecretId: dynamicSecretCfg.id });
|
const dynamicSecretLeases = await dynamicSecretLeaseDAL.find({ dynamicSecretId: dynamicSecretCfg.id });
|
||||||
return dynamicSecretLeases;
|
return dynamicSecretLeases;
|
||||||
@ -309,7 +330,7 @@ export const dynamicSecretLeaseServiceFactory = ({
|
|||||||
actorAuthMethod
|
actorAuthMethod
|
||||||
}: TDetailsDynamicSecretLeaseDTO) => {
|
}: TDetailsDynamicSecretLeaseDTO) => {
|
||||||
const project = await projectDAL.findProjectBySlug(projectSlug, actorOrgId);
|
const project = await projectDAL.findProjectBySlug(projectSlug, actorOrgId);
|
||||||
if (!project) throw new BadRequestError({ message: "Project not found" });
|
if (!project) throw new NotFoundError({ message: `Project with slug '${projectSlug}' not found` });
|
||||||
|
|
||||||
const projectId = project.id;
|
const projectId = project.id;
|
||||||
const { permission } = await permissionService.getProjectPermission(
|
const { permission } = await permissionService.getProjectPermission(
|
||||||
@ -325,10 +346,11 @@ export const dynamicSecretLeaseServiceFactory = ({
|
|||||||
);
|
);
|
||||||
|
|
||||||
const folder = await folderDAL.findBySecretPath(projectId, environmentSlug, path);
|
const folder = await folderDAL.findBySecretPath(projectId, environmentSlug, path);
|
||||||
if (!folder) throw new BadRequestError({ message: "Folder not found" });
|
if (!folder) throw new NotFoundError({ message: `Folder with path '${path}' not found` });
|
||||||
|
|
||||||
const dynamicSecretLease = await dynamicSecretLeaseDAL.findById(leaseId);
|
const dynamicSecretLease = await dynamicSecretLeaseDAL.findById(leaseId);
|
||||||
if (!dynamicSecretLease) throw new BadRequestError({ message: "Dynamic secret lease not found" });
|
if (!dynamicSecretLease)
|
||||||
|
throw new NotFoundError({ message: `Dynamic secret lease with ID '${leaseId}' not found` });
|
||||||
|
|
||||||
return dynamicSecretLease;
|
return dynamicSecretLease;
|
||||||
};
|
};
|
||||||
|
@ -1,10 +1,70 @@
|
|||||||
|
import { Knex } from "knex";
|
||||||
|
|
||||||
import { TDbClient } from "@app/db";
|
import { TDbClient } from "@app/db";
|
||||||
import { TableName } from "@app/db/schemas";
|
import { TableName } from "@app/db/schemas";
|
||||||
import { ormify } from "@app/lib/knex";
|
import { DatabaseError } from "@app/lib/errors";
|
||||||
|
import { ormify, selectAllTableCols } from "@app/lib/knex";
|
||||||
|
import { OrderByDirection } from "@app/lib/types";
|
||||||
|
import { SecretsOrderBy } from "@app/services/secret/secret-types";
|
||||||
|
|
||||||
export type TDynamicSecretDALFactory = ReturnType<typeof dynamicSecretDALFactory>;
|
export type TDynamicSecretDALFactory = ReturnType<typeof dynamicSecretDALFactory>;
|
||||||
|
|
||||||
export const dynamicSecretDALFactory = (db: TDbClient) => {
|
export const dynamicSecretDALFactory = (db: TDbClient) => {
|
||||||
const orm = ormify(db, TableName.DynamicSecret);
|
const orm = ormify(db, TableName.DynamicSecret);
|
||||||
return orm;
|
|
||||||
|
// find dynamic secrets for multiple environments (folder IDs are cross env, thus need to rank for pagination)
|
||||||
|
const listDynamicSecretsByFolderIds = async (
|
||||||
|
{
|
||||||
|
folderIds,
|
||||||
|
search,
|
||||||
|
limit,
|
||||||
|
offset = 0,
|
||||||
|
orderBy = SecretsOrderBy.Name,
|
||||||
|
orderDirection = OrderByDirection.ASC
|
||||||
|
}: {
|
||||||
|
folderIds: string[];
|
||||||
|
search?: string;
|
||||||
|
limit?: number;
|
||||||
|
offset?: number;
|
||||||
|
orderBy?: SecretsOrderBy;
|
||||||
|
orderDirection?: OrderByDirection;
|
||||||
|
},
|
||||||
|
tx?: Knex
|
||||||
|
) => {
|
||||||
|
try {
|
||||||
|
const query = (tx || db.replicaNode())(TableName.DynamicSecret)
|
||||||
|
.whereIn("folderId", folderIds)
|
||||||
|
.where((bd) => {
|
||||||
|
if (search) {
|
||||||
|
void bd.whereILike(`${TableName.DynamicSecret}.name`, `%${search}%`);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.leftJoin(TableName.SecretFolder, `${TableName.SecretFolder}.id`, `${TableName.DynamicSecret}.folderId`)
|
||||||
|
.leftJoin(TableName.Environment, `${TableName.SecretFolder}.envId`, `${TableName.Environment}.id`)
|
||||||
|
.select(
|
||||||
|
selectAllTableCols(TableName.DynamicSecret),
|
||||||
|
db.ref("slug").withSchema(TableName.Environment).as("environment"),
|
||||||
|
db.raw(`DENSE_RANK() OVER (ORDER BY ${TableName.DynamicSecret}."name" ${orderDirection}) as rank`)
|
||||||
|
)
|
||||||
|
.orderBy(`${TableName.DynamicSecret}.${orderBy}`, orderDirection);
|
||||||
|
|
||||||
|
if (limit) {
|
||||||
|
const rankOffset = offset + 1;
|
||||||
|
return await (tx || db)
|
||||||
|
.with("w", query)
|
||||||
|
.select("*")
|
||||||
|
.from<Awaited<typeof query>[number]>("w")
|
||||||
|
.where("w.rank", ">=", rankOffset)
|
||||||
|
.andWhere("w.rank", "<", rankOffset + limit);
|
||||||
|
}
|
||||||
|
|
||||||
|
const dynamicSecrets = await query;
|
||||||
|
|
||||||
|
return dynamicSecrets;
|
||||||
|
} catch (error) {
|
||||||
|
throw new DatabaseError({ error, name: "List dynamic secret multi env" });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return { ...orm, listDynamicSecretsByFolderIds };
|
||||||
};
|
};
|
||||||
|
@ -5,7 +5,8 @@ import { TLicenseServiceFactory } from "@app/ee/services/license/license-service
|
|||||||
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service";
|
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service";
|
||||||
import { ProjectPermissionActions, ProjectPermissionSub } from "@app/ee/services/permission/project-permission";
|
import { ProjectPermissionActions, ProjectPermissionSub } from "@app/ee/services/permission/project-permission";
|
||||||
import { infisicalSymmetricDecrypt, infisicalSymmetricEncypt } from "@app/lib/crypto/encryption";
|
import { infisicalSymmetricDecrypt, infisicalSymmetricEncypt } from "@app/lib/crypto/encryption";
|
||||||
import { BadRequestError } from "@app/lib/errors";
|
import { BadRequestError, NotFoundError } from "@app/lib/errors";
|
||||||
|
import { OrderByDirection } from "@app/lib/types";
|
||||||
import { TProjectDALFactory } from "@app/services/project/project-dal";
|
import { TProjectDALFactory } from "@app/services/project/project-dal";
|
||||||
import { TSecretFolderDALFactory } from "@app/services/secret-folder/secret-folder-dal";
|
import { TSecretFolderDALFactory } from "@app/services/secret-folder/secret-folder-dal";
|
||||||
|
|
||||||
@ -17,9 +18,12 @@ import {
|
|||||||
TCreateDynamicSecretDTO,
|
TCreateDynamicSecretDTO,
|
||||||
TDeleteDynamicSecretDTO,
|
TDeleteDynamicSecretDTO,
|
||||||
TDetailsDynamicSecretDTO,
|
TDetailsDynamicSecretDTO,
|
||||||
|
TGetDynamicSecretsCountDTO,
|
||||||
TListDynamicSecretsDTO,
|
TListDynamicSecretsDTO,
|
||||||
|
TListDynamicSecretsMultiEnvDTO,
|
||||||
TUpdateDynamicSecretDTO
|
TUpdateDynamicSecretDTO
|
||||||
} from "./dynamic-secret-types";
|
} from "./dynamic-secret-types";
|
||||||
|
import { AzureEntraIDProvider } from "./providers/azure-entra-id";
|
||||||
import { DynamicSecretProviders, TDynamicProviderFns } from "./providers/models";
|
import { DynamicSecretProviders, TDynamicProviderFns } from "./providers/models";
|
||||||
|
|
||||||
type TDynamicSecretServiceFactoryDep = {
|
type TDynamicSecretServiceFactoryDep = {
|
||||||
@ -31,7 +35,7 @@ type TDynamicSecretServiceFactoryDep = {
|
|||||||
"pruneDynamicSecret" | "unsetLeaseRevocation"
|
"pruneDynamicSecret" | "unsetLeaseRevocation"
|
||||||
>;
|
>;
|
||||||
licenseService: Pick<TLicenseServiceFactory, "getPlan">;
|
licenseService: Pick<TLicenseServiceFactory, "getPlan">;
|
||||||
folderDAL: Pick<TSecretFolderDALFactory, "findBySecretPath">;
|
folderDAL: Pick<TSecretFolderDALFactory, "findBySecretPath" | "findBySecretPathMultiEnv">;
|
||||||
projectDAL: Pick<TProjectDALFactory, "findProjectBySlug">;
|
projectDAL: Pick<TProjectDALFactory, "findProjectBySlug">;
|
||||||
permissionService: Pick<TPermissionServiceFactory, "getProjectPermission">;
|
permissionService: Pick<TPermissionServiceFactory, "getProjectPermission">;
|
||||||
};
|
};
|
||||||
@ -62,7 +66,7 @@ export const dynamicSecretServiceFactory = ({
|
|||||||
actorAuthMethod
|
actorAuthMethod
|
||||||
}: TCreateDynamicSecretDTO) => {
|
}: TCreateDynamicSecretDTO) => {
|
||||||
const project = await projectDAL.findProjectBySlug(projectSlug, actorOrgId);
|
const project = await projectDAL.findProjectBySlug(projectSlug, actorOrgId);
|
||||||
if (!project) throw new BadRequestError({ message: "Project not found" });
|
if (!project) throw new NotFoundError({ message: `Project with slug '${projectSlug}' not found` });
|
||||||
|
|
||||||
const projectId = project.id;
|
const projectId = project.id;
|
||||||
const { permission } = await permissionService.getProjectPermission(
|
const { permission } = await permissionService.getProjectPermission(
|
||||||
@ -85,7 +89,9 @@ export const dynamicSecretServiceFactory = ({
|
|||||||
}
|
}
|
||||||
|
|
||||||
const folder = await folderDAL.findBySecretPath(projectId, environmentSlug, path);
|
const folder = await folderDAL.findBySecretPath(projectId, environmentSlug, path);
|
||||||
if (!folder) throw new BadRequestError({ message: "Folder not found" });
|
if (!folder) {
|
||||||
|
throw new NotFoundError({ message: `Folder with path '${path}' in environment '${environmentSlug}' not found` });
|
||||||
|
}
|
||||||
|
|
||||||
const existingDynamicSecret = await dynamicSecretDAL.findOne({ name, folderId: folder.id });
|
const existingDynamicSecret = await dynamicSecretDAL.findOne({ name, folderId: folder.id });
|
||||||
if (existingDynamicSecret)
|
if (existingDynamicSecret)
|
||||||
@ -130,7 +136,7 @@ export const dynamicSecretServiceFactory = ({
|
|||||||
actorAuthMethod
|
actorAuthMethod
|
||||||
}: TUpdateDynamicSecretDTO) => {
|
}: TUpdateDynamicSecretDTO) => {
|
||||||
const project = await projectDAL.findProjectBySlug(projectSlug, actorOrgId);
|
const project = await projectDAL.findProjectBySlug(projectSlug, actorOrgId);
|
||||||
if (!project) throw new BadRequestError({ message: "Project not found" });
|
if (!project) throw new NotFoundError({ message: `Project with slug '${projectSlug}' not found` });
|
||||||
|
|
||||||
const projectId = project.id;
|
const projectId = project.id;
|
||||||
|
|
||||||
@ -154,11 +160,15 @@ export const dynamicSecretServiceFactory = ({
|
|||||||
}
|
}
|
||||||
|
|
||||||
const folder = await folderDAL.findBySecretPath(projectId, environmentSlug, path);
|
const folder = await folderDAL.findBySecretPath(projectId, environmentSlug, path);
|
||||||
if (!folder) throw new BadRequestError({ message: "Folder not found" });
|
if (!folder)
|
||||||
|
throw new NotFoundError({ message: `Folder with path '${path}' in environment '${environmentSlug}' not found` });
|
||||||
|
|
||||||
const dynamicSecretCfg = await dynamicSecretDAL.findOne({ name, folderId: folder.id });
|
const dynamicSecretCfg = await dynamicSecretDAL.findOne({ name, folderId: folder.id });
|
||||||
if (!dynamicSecretCfg) throw new BadRequestError({ message: "Dynamic secret not found" });
|
if (!dynamicSecretCfg) {
|
||||||
|
throw new NotFoundError({
|
||||||
|
message: `Dynamic secret with name '${name}' in folder '${folder.path}' not found`
|
||||||
|
});
|
||||||
|
}
|
||||||
if (newName) {
|
if (newName) {
|
||||||
const existingDynamicSecret = await dynamicSecretDAL.findOne({ name: newName, folderId: folder.id });
|
const existingDynamicSecret = await dynamicSecretDAL.findOne({ name: newName, folderId: folder.id });
|
||||||
if (existingDynamicSecret)
|
if (existingDynamicSecret)
|
||||||
@ -209,7 +219,7 @@ export const dynamicSecretServiceFactory = ({
|
|||||||
isForced
|
isForced
|
||||||
}: TDeleteDynamicSecretDTO) => {
|
}: TDeleteDynamicSecretDTO) => {
|
||||||
const project = await projectDAL.findProjectBySlug(projectSlug, actorOrgId);
|
const project = await projectDAL.findProjectBySlug(projectSlug, actorOrgId);
|
||||||
if (!project) throw new BadRequestError({ message: "Project not found" });
|
if (!project) throw new NotFoundError({ message: `Project with slug '${projectSlug}' not found` });
|
||||||
|
|
||||||
const projectId = project.id;
|
const projectId = project.id;
|
||||||
|
|
||||||
@ -226,10 +236,13 @@ export const dynamicSecretServiceFactory = ({
|
|||||||
);
|
);
|
||||||
|
|
||||||
const folder = await folderDAL.findBySecretPath(projectId, environmentSlug, path);
|
const folder = await folderDAL.findBySecretPath(projectId, environmentSlug, path);
|
||||||
if (!folder) throw new BadRequestError({ message: "Folder not found" });
|
if (!folder)
|
||||||
|
throw new NotFoundError({ message: `Folder with path '${path}' in environment '${environmentSlug}' not found` });
|
||||||
|
|
||||||
const dynamicSecretCfg = await dynamicSecretDAL.findOne({ name, folderId: folder.id });
|
const dynamicSecretCfg = await dynamicSecretDAL.findOne({ name, folderId: folder.id });
|
||||||
if (!dynamicSecretCfg) throw new BadRequestError({ message: "Dynamic secret not found" });
|
if (!dynamicSecretCfg) {
|
||||||
|
throw new NotFoundError({ message: `Dynamic secret with name '${name}' in folder '${folder.path}' not found` });
|
||||||
|
}
|
||||||
|
|
||||||
const leases = await dynamicSecretLeaseDAL.find({ dynamicSecretId: dynamicSecretCfg.id });
|
const leases = await dynamicSecretLeaseDAL.find({ dynamicSecretId: dynamicSecretCfg.id });
|
||||||
// when not forced we check with the external system to first remove the things
|
// when not forced we check with the external system to first remove the things
|
||||||
@ -267,7 +280,7 @@ export const dynamicSecretServiceFactory = ({
|
|||||||
actor
|
actor
|
||||||
}: TDetailsDynamicSecretDTO) => {
|
}: TDetailsDynamicSecretDTO) => {
|
||||||
const project = await projectDAL.findProjectBySlug(projectSlug, actorOrgId);
|
const project = await projectDAL.findProjectBySlug(projectSlug, actorOrgId);
|
||||||
if (!project) throw new BadRequestError({ message: "Project not found" });
|
if (!project) throw new NotFoundError({ message: `Project with slug '${projectSlug}' not found` });
|
||||||
|
|
||||||
const projectId = project.id;
|
const projectId = project.id;
|
||||||
const { permission } = await permissionService.getProjectPermission(
|
const { permission } = await permissionService.getProjectPermission(
|
||||||
@ -283,10 +296,13 @@ export const dynamicSecretServiceFactory = ({
|
|||||||
);
|
);
|
||||||
|
|
||||||
const folder = await folderDAL.findBySecretPath(projectId, environmentSlug, path);
|
const folder = await folderDAL.findBySecretPath(projectId, environmentSlug, path);
|
||||||
if (!folder) throw new BadRequestError({ message: "Folder not found" });
|
if (!folder)
|
||||||
|
throw new NotFoundError({ message: `Folder with path '${path}' in environment '${environmentSlug}' not found` });
|
||||||
|
|
||||||
const dynamicSecretCfg = await dynamicSecretDAL.findOne({ name, folderId: folder.id });
|
const dynamicSecretCfg = await dynamicSecretDAL.findOne({ name, folderId: folder.id });
|
||||||
if (!dynamicSecretCfg) throw new BadRequestError({ message: "Dynamic secret not found" });
|
if (!dynamicSecretCfg) {
|
||||||
|
throw new NotFoundError({ message: `Dynamic secret with name '${name} in folder '${path}' not found` });
|
||||||
|
}
|
||||||
const decryptedStoredInput = JSON.parse(
|
const decryptedStoredInput = JSON.parse(
|
||||||
infisicalSymmetricDecrypt({
|
infisicalSymmetricDecrypt({
|
||||||
keyEncoding: dynamicSecretCfg.keyEncoding as SecretKeyEncoding,
|
keyEncoding: dynamicSecretCfg.keyEncoding as SecretKeyEncoding,
|
||||||
@ -300,19 +316,62 @@ export const dynamicSecretServiceFactory = ({
|
|||||||
return { ...dynamicSecretCfg, inputs: providerInputs };
|
return { ...dynamicSecretCfg, inputs: providerInputs };
|
||||||
};
|
};
|
||||||
|
|
||||||
const list = async ({
|
// get unique dynamic secret count across multiple envs
|
||||||
|
const getCountMultiEnv = async ({
|
||||||
actorAuthMethod,
|
actorAuthMethod,
|
||||||
actorOrgId,
|
actorOrgId,
|
||||||
actorId,
|
actorId,
|
||||||
actor,
|
actor,
|
||||||
projectSlug,
|
projectId,
|
||||||
path,
|
path,
|
||||||
environmentSlug
|
environmentSlugs,
|
||||||
}: TListDynamicSecretsDTO) => {
|
search,
|
||||||
const project = await projectDAL.findProjectBySlug(projectSlug, actorOrgId);
|
isInternal
|
||||||
if (!project) throw new BadRequestError({ message: "Project not found" });
|
}: TListDynamicSecretsMultiEnvDTO) => {
|
||||||
|
if (!isInternal) {
|
||||||
|
const { permission } = await permissionService.getProjectPermission(
|
||||||
|
actor,
|
||||||
|
actorId,
|
||||||
|
projectId,
|
||||||
|
actorAuthMethod,
|
||||||
|
actorOrgId
|
||||||
|
);
|
||||||
|
|
||||||
const projectId = project.id;
|
// verify user has access to each env in request
|
||||||
|
environmentSlugs.forEach((environmentSlug) =>
|
||||||
|
ForbiddenError.from(permission).throwUnlessCan(
|
||||||
|
ProjectPermissionActions.Read,
|
||||||
|
subject(ProjectPermissionSub.Secrets, { environment: environmentSlug, secretPath: path })
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const folders = await folderDAL.findBySecretPathMultiEnv(projectId, environmentSlugs, path);
|
||||||
|
if (!folders.length) {
|
||||||
|
throw new NotFoundError({
|
||||||
|
message: `Folders with path '${path}' in environments with slugs '${environmentSlugs.join(", ")}' not found`
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const dynamicSecretCfg = await dynamicSecretDAL.find(
|
||||||
|
{ $in: { folderId: folders.map((folder) => folder.id) }, $search: search ? { name: `%${search}%` } : undefined },
|
||||||
|
{ countDistinct: "name" }
|
||||||
|
);
|
||||||
|
|
||||||
|
return Number(dynamicSecretCfg[0]?.count ?? 0);
|
||||||
|
};
|
||||||
|
|
||||||
|
// get dynamic secret count for a single env
|
||||||
|
const getDynamicSecretCount = async ({
|
||||||
|
actorAuthMethod,
|
||||||
|
actorOrgId,
|
||||||
|
actorId,
|
||||||
|
actor,
|
||||||
|
path,
|
||||||
|
environmentSlug,
|
||||||
|
search,
|
||||||
|
projectId
|
||||||
|
}: TGetDynamicSecretsCountDTO) => {
|
||||||
const { permission } = await permissionService.getProjectPermission(
|
const { permission } = await permissionService.getProjectPermission(
|
||||||
actor,
|
actor,
|
||||||
actorId,
|
actorId,
|
||||||
@ -326,17 +385,138 @@ export const dynamicSecretServiceFactory = ({
|
|||||||
);
|
);
|
||||||
|
|
||||||
const folder = await folderDAL.findBySecretPath(projectId, environmentSlug, path);
|
const folder = await folderDAL.findBySecretPath(projectId, environmentSlug, path);
|
||||||
if (!folder) throw new BadRequestError({ message: "Folder not found" });
|
if (!folder) {
|
||||||
|
throw new NotFoundError({ message: `Folder with path '${path}' in environment '${environmentSlug}' not found` });
|
||||||
|
}
|
||||||
|
|
||||||
const dynamicSecretCfg = await dynamicSecretDAL.find({ folderId: folder.id });
|
const dynamicSecretCfg = await dynamicSecretDAL.find(
|
||||||
|
{ folderId: folder.id, $search: search ? { name: `%${search}%` } : undefined },
|
||||||
|
{ count: true }
|
||||||
|
);
|
||||||
|
return Number(dynamicSecretCfg[0]?.count ?? 0);
|
||||||
|
};
|
||||||
|
|
||||||
|
const listDynamicSecretsByEnv = async ({
|
||||||
|
actorAuthMethod,
|
||||||
|
actorOrgId,
|
||||||
|
actorId,
|
||||||
|
actor,
|
||||||
|
projectSlug,
|
||||||
|
path,
|
||||||
|
environmentSlug,
|
||||||
|
limit,
|
||||||
|
offset,
|
||||||
|
orderBy,
|
||||||
|
orderDirection = OrderByDirection.ASC,
|
||||||
|
search,
|
||||||
|
...params
|
||||||
|
}: TListDynamicSecretsDTO) => {
|
||||||
|
let { projectId } = params;
|
||||||
|
|
||||||
|
if (!projectId) {
|
||||||
|
if (!projectSlug) throw new BadRequestError({ message: "Project ID or slug required" });
|
||||||
|
const project = await projectDAL.findProjectBySlug(projectSlug, actorOrgId);
|
||||||
|
if (!project) throw new NotFoundError({ message: `Project with slug '${projectSlug}' not found` });
|
||||||
|
projectId = project.id;
|
||||||
|
}
|
||||||
|
|
||||||
|
const { permission } = await permissionService.getProjectPermission(
|
||||||
|
actor,
|
||||||
|
actorId,
|
||||||
|
projectId,
|
||||||
|
actorAuthMethod,
|
||||||
|
actorOrgId
|
||||||
|
);
|
||||||
|
ForbiddenError.from(permission).throwUnlessCan(
|
||||||
|
ProjectPermissionActions.Read,
|
||||||
|
subject(ProjectPermissionSub.Secrets, { environment: environmentSlug, secretPath: path })
|
||||||
|
);
|
||||||
|
|
||||||
|
const folder = await folderDAL.findBySecretPath(projectId, environmentSlug, path);
|
||||||
|
if (!folder)
|
||||||
|
throw new NotFoundError({ message: `Folder with path '${path}' in environment '${environmentSlug}' not found` });
|
||||||
|
|
||||||
|
const dynamicSecretCfg = await dynamicSecretDAL.find(
|
||||||
|
{ folderId: folder.id, $search: search ? { name: `%${search}%` } : undefined },
|
||||||
|
{
|
||||||
|
limit,
|
||||||
|
offset,
|
||||||
|
sort: orderBy ? [[orderBy, orderDirection]] : undefined
|
||||||
|
}
|
||||||
|
);
|
||||||
return dynamicSecretCfg;
|
return dynamicSecretCfg;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// get dynamic secrets for multiple envs
|
||||||
|
const listDynamicSecretsByFolderIds = async ({
|
||||||
|
actorAuthMethod,
|
||||||
|
actorOrgId,
|
||||||
|
actorId,
|
||||||
|
actor,
|
||||||
|
path,
|
||||||
|
environmentSlugs,
|
||||||
|
projectId,
|
||||||
|
isInternal,
|
||||||
|
...params
|
||||||
|
}: TListDynamicSecretsMultiEnvDTO) => {
|
||||||
|
if (!isInternal) {
|
||||||
|
const { permission } = await permissionService.getProjectPermission(
|
||||||
|
actor,
|
||||||
|
actorId,
|
||||||
|
projectId,
|
||||||
|
actorAuthMethod,
|
||||||
|
actorOrgId
|
||||||
|
);
|
||||||
|
|
||||||
|
// verify user has access to each env in request
|
||||||
|
environmentSlugs.forEach((environmentSlug) =>
|
||||||
|
ForbiddenError.from(permission).throwUnlessCan(
|
||||||
|
ProjectPermissionActions.Read,
|
||||||
|
subject(ProjectPermissionSub.Secrets, { environment: environmentSlug, secretPath: path })
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const folders = await folderDAL.findBySecretPathMultiEnv(projectId, environmentSlugs, path);
|
||||||
|
if (!folders.length)
|
||||||
|
throw new NotFoundError({
|
||||||
|
message: `Folders with path '${path} in environments with slugs '${environmentSlugs.join(", ")}' not found`
|
||||||
|
});
|
||||||
|
|
||||||
|
const dynamicSecretCfg = await dynamicSecretDAL.listDynamicSecretsByFolderIds({
|
||||||
|
folderIds: folders.map((folder) => folder.id),
|
||||||
|
...params
|
||||||
|
});
|
||||||
|
|
||||||
|
return dynamicSecretCfg;
|
||||||
|
};
|
||||||
|
|
||||||
|
const fetchAzureEntraIdUsers = async ({
|
||||||
|
tenantId,
|
||||||
|
applicationId,
|
||||||
|
clientSecret
|
||||||
|
}: {
|
||||||
|
tenantId: string;
|
||||||
|
applicationId: string;
|
||||||
|
clientSecret: string;
|
||||||
|
}) => {
|
||||||
|
const azureEntraIdUsers = await AzureEntraIDProvider().fetchAzureEntraIdUsers(
|
||||||
|
tenantId,
|
||||||
|
applicationId,
|
||||||
|
clientSecret
|
||||||
|
);
|
||||||
|
return azureEntraIdUsers;
|
||||||
|
};
|
||||||
|
|
||||||
return {
|
return {
|
||||||
create,
|
create,
|
||||||
updateByName,
|
updateByName,
|
||||||
deleteByName,
|
deleteByName,
|
||||||
getDetails,
|
getDetails,
|
||||||
list
|
listDynamicSecretsByEnv,
|
||||||
|
listDynamicSecretsByFolderIds,
|
||||||
|
getDynamicSecretCount,
|
||||||
|
getCountMultiEnv,
|
||||||
|
fetchAzureEntraIdUsers
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
|
|
||||||
import { TProjectPermission } from "@app/lib/types";
|
import { OrderByDirection, TProjectPermission } from "@app/lib/types";
|
||||||
|
import { SecretsOrderBy } from "@app/services/secret/secret-types";
|
||||||
|
|
||||||
import { DynamicSecretProviderSchema } from "./providers/models";
|
import { DynamicSecretProviderSchema } from "./providers/models";
|
||||||
|
|
||||||
@ -50,5 +51,20 @@ export type TDetailsDynamicSecretDTO = {
|
|||||||
export type TListDynamicSecretsDTO = {
|
export type TListDynamicSecretsDTO = {
|
||||||
path: string;
|
path: string;
|
||||||
environmentSlug: string;
|
environmentSlug: string;
|
||||||
projectSlug: string;
|
projectSlug?: string;
|
||||||
|
projectId?: string;
|
||||||
|
offset?: number;
|
||||||
|
limit?: number;
|
||||||
|
orderBy?: SecretsOrderBy;
|
||||||
|
orderDirection?: OrderByDirection;
|
||||||
|
search?: string;
|
||||||
} & Omit<TProjectPermission, "projectId">;
|
} & Omit<TProjectPermission, "projectId">;
|
||||||
|
|
||||||
|
export type TListDynamicSecretsMultiEnvDTO = Omit<
|
||||||
|
TListDynamicSecretsDTO,
|
||||||
|
"projectId" | "environmentSlug" | "projectSlug"
|
||||||
|
> & { projectId: string; environmentSlugs: string[]; isInternal?: boolean };
|
||||||
|
|
||||||
|
export type TGetDynamicSecretsCountDTO = Omit<TListDynamicSecretsDTO, "projectSlug" | "projectId"> & {
|
||||||
|
projectId: string;
|
||||||
|
};
|
||||||
|
@ -0,0 +1,138 @@
|
|||||||
|
import axios from "axios";
|
||||||
|
import { customAlphabet } from "nanoid";
|
||||||
|
|
||||||
|
import { BadRequestError } from "@app/lib/errors";
|
||||||
|
|
||||||
|
import { AzureEntraIDSchema, TDynamicProviderFns } from "./models";
|
||||||
|
|
||||||
|
const MSFT_GRAPH_API_URL = "https://graph.microsoft.com/v1.0/";
|
||||||
|
const MSFT_LOGIN_URL = "https://login.microsoftonline.com";
|
||||||
|
|
||||||
|
const generatePassword = () => {
|
||||||
|
const charset = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-_.~!*$#";
|
||||||
|
return customAlphabet(charset, 64)();
|
||||||
|
};
|
||||||
|
|
||||||
|
type User = { name: string; id: string; email: string };
|
||||||
|
|
||||||
|
export const AzureEntraIDProvider = (): TDynamicProviderFns & {
|
||||||
|
fetchAzureEntraIdUsers: (tenantId: string, applicationId: string, clientSecret: string) => Promise<User[]>;
|
||||||
|
} => {
|
||||||
|
const validateProviderInputs = async (inputs: unknown) => {
|
||||||
|
const providerInputs = await AzureEntraIDSchema.parseAsync(inputs);
|
||||||
|
return providerInputs;
|
||||||
|
};
|
||||||
|
|
||||||
|
const getToken = async (
|
||||||
|
tenantId: string,
|
||||||
|
applicationId: string,
|
||||||
|
clientSecret: string
|
||||||
|
): Promise<{ token?: string; success: boolean }> => {
|
||||||
|
const response = await axios.post<{ access_token: string }>(
|
||||||
|
`${MSFT_LOGIN_URL}/${tenantId}/oauth2/v2.0/token`,
|
||||||
|
{
|
||||||
|
grant_type: "client_credentials",
|
||||||
|
client_id: applicationId,
|
||||||
|
client_secret: clientSecret,
|
||||||
|
scope: "https://graph.microsoft.com/.default"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/x-www-form-urlencoded"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
if (response.status === 200) {
|
||||||
|
return { token: response.data.access_token, success: true };
|
||||||
|
}
|
||||||
|
return { success: false };
|
||||||
|
};
|
||||||
|
|
||||||
|
const validateConnection = async (inputs: unknown) => {
|
||||||
|
const providerInputs = await validateProviderInputs(inputs);
|
||||||
|
const data = await getToken(providerInputs.tenantId, providerInputs.applicationId, providerInputs.clientSecret);
|
||||||
|
return data.success;
|
||||||
|
};
|
||||||
|
|
||||||
|
const renew = async (inputs: unknown, entityId: string) => {
|
||||||
|
// Do nothing
|
||||||
|
return { entityId };
|
||||||
|
};
|
||||||
|
|
||||||
|
const create = async (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}`
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
if (response.status !== 204) {
|
||||||
|
throw new BadRequestError({ message: "Failed to update password" });
|
||||||
|
}
|
||||||
|
|
||||||
|
return { entityId: providerInputs.userId, data: { email: providerInputs.email, password } };
|
||||||
|
};
|
||||||
|
|
||||||
|
const revoke = async (inputs: unknown, entityId: string) => {
|
||||||
|
// Creates a new password
|
||||||
|
await create(inputs);
|
||||||
|
return { entityId };
|
||||||
|
};
|
||||||
|
|
||||||
|
const fetchAzureEntraIdUsers = async (tenantId: string, applicationId: string, clientSecret: string) => {
|
||||||
|
const data = await getToken(tenantId, applicationId, clientSecret);
|
||||||
|
if (!data.success) {
|
||||||
|
throw new BadRequestError({ message: "Failed to authorize to Microsoft Entra ID" });
|
||||||
|
}
|
||||||
|
|
||||||
|
const response = await axios.get<{ value: [{ id: string; displayName: string; userPrincipalName: string }] }>(
|
||||||
|
`${MSFT_GRAPH_API_URL}/users`,
|
||||||
|
{
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/x-www-form-urlencoded",
|
||||||
|
Authorization: `Bearer ${data.token}`
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
if (response.status !== 200) {
|
||||||
|
throw new BadRequestError({ message: "Failed to fetch users" });
|
||||||
|
}
|
||||||
|
|
||||||
|
const users = response.data.value.map((user) => {
|
||||||
|
return {
|
||||||
|
name: user.displayName,
|
||||||
|
id: user.id,
|
||||||
|
email: user.userPrincipalName
|
||||||
|
};
|
||||||
|
});
|
||||||
|
return users;
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
validateProviderInputs,
|
||||||
|
validateConnection,
|
||||||
|
create,
|
||||||
|
revoke,
|
||||||
|
renew,
|
||||||
|
fetchAzureEntraIdUsers
|
||||||
|
};
|
||||||
|
};
|
@ -1,7 +1,9 @@
|
|||||||
import { AwsElastiCacheDatabaseProvider } from "./aws-elasticache";
|
import { AwsElastiCacheDatabaseProvider } from "./aws-elasticache";
|
||||||
import { AwsIamProvider } from "./aws-iam";
|
import { AwsIamProvider } from "./aws-iam";
|
||||||
|
import { AzureEntraIDProvider } from "./azure-entra-id";
|
||||||
import { CassandraProvider } from "./cassandra";
|
import { CassandraProvider } from "./cassandra";
|
||||||
import { ElasticSearchProvider } from "./elastic-search";
|
import { ElasticSearchProvider } from "./elastic-search";
|
||||||
|
import { LdapProvider } from "./ldap";
|
||||||
import { DynamicSecretProviders } from "./models";
|
import { DynamicSecretProviders } from "./models";
|
||||||
import { MongoAtlasProvider } from "./mongo-atlas";
|
import { MongoAtlasProvider } from "./mongo-atlas";
|
||||||
import { MongoDBProvider } from "./mongo-db";
|
import { MongoDBProvider } from "./mongo-db";
|
||||||
@ -18,5 +20,7 @@ export const buildDynamicSecretProviders = () => ({
|
|||||||
[DynamicSecretProviders.MongoAtlas]: MongoAtlasProvider(),
|
[DynamicSecretProviders.MongoAtlas]: MongoAtlasProvider(),
|
||||||
[DynamicSecretProviders.MongoDB]: MongoDBProvider(),
|
[DynamicSecretProviders.MongoDB]: MongoDBProvider(),
|
||||||
[DynamicSecretProviders.ElasticSearch]: ElasticSearchProvider(),
|
[DynamicSecretProviders.ElasticSearch]: ElasticSearchProvider(),
|
||||||
[DynamicSecretProviders.RabbitMq]: RabbitMqProvider()
|
[DynamicSecretProviders.RabbitMq]: RabbitMqProvider(),
|
||||||
|
[DynamicSecretProviders.AzureEntraID]: AzureEntraIDProvider(),
|
||||||
|
[DynamicSecretProviders.Ldap]: LdapProvider()
|
||||||
});
|
});
|
||||||
|
235
backend/src/ee/services/dynamic-secret/providers/ldap.ts
Normal file
235
backend/src/ee/services/dynamic-secret/providers/ldap.ts
Normal file
@ -0,0 +1,235 @@
|
|||||||
|
import handlebars from "handlebars";
|
||||||
|
import ldapjs from "ldapjs";
|
||||||
|
import ldif from "ldif";
|
||||||
|
import { customAlphabet } from "nanoid";
|
||||||
|
import { z } from "zod";
|
||||||
|
|
||||||
|
import { BadRequestError } from "@app/lib/errors";
|
||||||
|
import { alphaNumericNanoId } from "@app/lib/nanoid";
|
||||||
|
|
||||||
|
import { LdapSchema, TDynamicProviderFns } from "./models";
|
||||||
|
|
||||||
|
const generatePassword = () => {
|
||||||
|
const charset = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-_.~!*$#";
|
||||||
|
return customAlphabet(charset, 64)();
|
||||||
|
};
|
||||||
|
|
||||||
|
const encodePassword = (password?: string) => {
|
||||||
|
const quotedPassword = `"${password}"`;
|
||||||
|
const utf16lePassword = Buffer.from(quotedPassword, "utf16le");
|
||||||
|
const base64Password = utf16lePassword.toString("base64");
|
||||||
|
return base64Password;
|
||||||
|
};
|
||||||
|
|
||||||
|
const generateUsername = () => {
|
||||||
|
return alphaNumericNanoId(20);
|
||||||
|
};
|
||||||
|
|
||||||
|
const generateLDIF = ({
|
||||||
|
username,
|
||||||
|
password,
|
||||||
|
ldifTemplate
|
||||||
|
}: {
|
||||||
|
username: string;
|
||||||
|
password?: string;
|
||||||
|
ldifTemplate: string;
|
||||||
|
}): string => {
|
||||||
|
const data = {
|
||||||
|
Username: username,
|
||||||
|
Password: password,
|
||||||
|
EncodedPassword: encodePassword(password)
|
||||||
|
};
|
||||||
|
|
||||||
|
const renderTemplate = handlebars.compile(ldifTemplate);
|
||||||
|
const renderedLdif = renderTemplate(data);
|
||||||
|
|
||||||
|
return renderedLdif;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const LdapProvider = (): TDynamicProviderFns => {
|
||||||
|
const validateProviderInputs = async (inputs: unknown) => {
|
||||||
|
const providerInputs = await LdapSchema.parseAsync(inputs);
|
||||||
|
return providerInputs;
|
||||||
|
};
|
||||||
|
|
||||||
|
const getClient = async (providerInputs: z.infer<typeof LdapSchema>): Promise<ldapjs.Client> => {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
const client = ldapjs.createClient({
|
||||||
|
url: providerInputs.url,
|
||||||
|
tlsOptions: {
|
||||||
|
ca: providerInputs.ca ? providerInputs.ca : null,
|
||||||
|
rejectUnauthorized: !!providerInputs.ca
|
||||||
|
},
|
||||||
|
reconnect: true,
|
||||||
|
bindDN: providerInputs.binddn,
|
||||||
|
bindCredentials: providerInputs.bindpass
|
||||||
|
});
|
||||||
|
|
||||||
|
client.on("error", (err: Error) => {
|
||||||
|
client.unbind();
|
||||||
|
reject(new BadRequestError({ message: err.message }));
|
||||||
|
});
|
||||||
|
|
||||||
|
client.bind(providerInputs.binddn, providerInputs.bindpass, (err) => {
|
||||||
|
if (err) {
|
||||||
|
client.unbind();
|
||||||
|
reject(new BadRequestError({ message: err.message }));
|
||||||
|
} else {
|
||||||
|
resolve(client);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const validateConnection = async (inputs: unknown) => {
|
||||||
|
const providerInputs = await validateProviderInputs(inputs);
|
||||||
|
const client = await getClient(providerInputs);
|
||||||
|
return client.connected;
|
||||||
|
};
|
||||||
|
|
||||||
|
const executeLdif = async (client: ldapjs.Client, ldif_file: string) => {
|
||||||
|
type TEntry = {
|
||||||
|
dn: string;
|
||||||
|
type: string;
|
||||||
|
|
||||||
|
changes: {
|
||||||
|
operation?: string;
|
||||||
|
attribute: {
|
||||||
|
attribute: string;
|
||||||
|
};
|
||||||
|
value: {
|
||||||
|
value: string;
|
||||||
|
};
|
||||||
|
values: {
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Untyped, can be any for ldapjs.Change.modification.values
|
||||||
|
value: any;
|
||||||
|
}[];
|
||||||
|
}[];
|
||||||
|
};
|
||||||
|
|
||||||
|
let parsedEntries: TEntry[];
|
||||||
|
|
||||||
|
try {
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
|
||||||
|
parsedEntries = ldif.parse(ldif_file).entries as TEntry[];
|
||||||
|
} catch (err) {
|
||||||
|
throw new BadRequestError({
|
||||||
|
message: "Invalid LDIF format, refer to the documentation at Dynamic secrets > LDAP > LDIF Entries."
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const dnArray: string[] = [];
|
||||||
|
|
||||||
|
for await (const entry of parsedEntries) {
|
||||||
|
const { dn } = entry;
|
||||||
|
let responseDn: string;
|
||||||
|
|
||||||
|
if (entry.type === "add") {
|
||||||
|
const attributes: Record<string, string | string[]> = {};
|
||||||
|
|
||||||
|
entry.changes.forEach((change) => {
|
||||||
|
const attrName = change.attribute.attribute;
|
||||||
|
const attrValue = change.value.value;
|
||||||
|
|
||||||
|
attributes[attrName] = Array.isArray(attrValue) ? attrValue : [attrValue];
|
||||||
|
});
|
||||||
|
|
||||||
|
responseDn = await new Promise((resolve, reject) => {
|
||||||
|
client.add(dn, attributes, (err) => {
|
||||||
|
if (err) {
|
||||||
|
reject(new BadRequestError({ message: err.message }));
|
||||||
|
} else {
|
||||||
|
resolve(dn);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
} else if (entry.type === "modify") {
|
||||||
|
const changes: ldapjs.Change[] = [];
|
||||||
|
|
||||||
|
entry.changes.forEach((change) => {
|
||||||
|
changes.push(
|
||||||
|
new ldapjs.Change({
|
||||||
|
operation: change.operation || "replace",
|
||||||
|
modification: {
|
||||||
|
type: change.attribute.attribute,
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-unsafe-return
|
||||||
|
values: change.values.map((value) => value.value)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
responseDn = await new Promise((resolve, reject) => {
|
||||||
|
client.modify(dn, changes, (err) => {
|
||||||
|
if (err) {
|
||||||
|
reject(new BadRequestError({ message: err.message }));
|
||||||
|
} else {
|
||||||
|
resolve(dn);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
} else if (entry.type === "delete") {
|
||||||
|
responseDn = await new Promise((resolve, reject) => {
|
||||||
|
client.del(dn, (err) => {
|
||||||
|
if (err) {
|
||||||
|
reject(new BadRequestError({ message: err.message }));
|
||||||
|
} else {
|
||||||
|
resolve(dn);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
client.unbind();
|
||||||
|
throw new BadRequestError({ message: `Unsupported operation type ${entry.type}` });
|
||||||
|
}
|
||||||
|
|
||||||
|
dnArray.push(responseDn);
|
||||||
|
}
|
||||||
|
client.unbind();
|
||||||
|
return dnArray;
|
||||||
|
};
|
||||||
|
|
||||||
|
const create = async (inputs: unknown) => {
|
||||||
|
const providerInputs = await validateProviderInputs(inputs);
|
||||||
|
const client = await getClient(providerInputs);
|
||||||
|
|
||||||
|
const username = generateUsername();
|
||||||
|
const password = generatePassword();
|
||||||
|
const generatedLdif = generateLDIF({ username, password, ldifTemplate: providerInputs.creationLdif });
|
||||||
|
|
||||||
|
try {
|
||||||
|
const dnArray = await executeLdif(client, generatedLdif);
|
||||||
|
|
||||||
|
return { entityId: username, data: { DN_ARRAY: dnArray, USERNAME: username, PASSWORD: password } };
|
||||||
|
} catch (err) {
|
||||||
|
if (providerInputs.rollbackLdif) {
|
||||||
|
const rollbackLdif = generateLDIF({ username, password, ldifTemplate: providerInputs.rollbackLdif });
|
||||||
|
await executeLdif(client, rollbackLdif);
|
||||||
|
}
|
||||||
|
throw new BadRequestError({ message: (err as Error).message });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const revoke = async (inputs: unknown, entityId: string) => {
|
||||||
|
const providerInputs = await validateProviderInputs(inputs);
|
||||||
|
const connection = await getClient(providerInputs);
|
||||||
|
const revocationLdif = generateLDIF({ username: entityId, ldifTemplate: providerInputs.revocationLdif });
|
||||||
|
|
||||||
|
await executeLdif(connection, revocationLdif);
|
||||||
|
|
||||||
|
return { entityId };
|
||||||
|
};
|
||||||
|
|
||||||
|
const renew = async (inputs: unknown, entityId: string) => {
|
||||||
|
// Do nothing
|
||||||
|
return { entityId };
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
validateProviderInputs,
|
||||||
|
validateConnection,
|
||||||
|
create,
|
||||||
|
revoke,
|
||||||
|
renew
|
||||||
|
};
|
||||||
|
};
|
@ -166,6 +166,25 @@ export const DynamicSecretMongoDBSchema = z.object({
|
|||||||
)
|
)
|
||||||
});
|
});
|
||||||
|
|
||||||
|
export const AzureEntraIDSchema = z.object({
|
||||||
|
tenantId: z.string().trim().min(1),
|
||||||
|
userId: z.string().trim().min(1),
|
||||||
|
email: z.string().trim().min(1),
|
||||||
|
applicationId: z.string().trim().min(1),
|
||||||
|
clientSecret: z.string().trim().min(1)
|
||||||
|
});
|
||||||
|
|
||||||
|
export const LdapSchema = z.object({
|
||||||
|
url: z.string().trim().min(1),
|
||||||
|
binddn: z.string().trim().min(1),
|
||||||
|
bindpass: z.string().trim().min(1),
|
||||||
|
ca: z.string().optional(),
|
||||||
|
|
||||||
|
creationLdif: z.string().min(1),
|
||||||
|
revocationLdif: z.string().min(1),
|
||||||
|
rollbackLdif: z.string().optional()
|
||||||
|
});
|
||||||
|
|
||||||
export enum DynamicSecretProviders {
|
export enum DynamicSecretProviders {
|
||||||
SqlDatabase = "sql-database",
|
SqlDatabase = "sql-database",
|
||||||
Cassandra = "cassandra",
|
Cassandra = "cassandra",
|
||||||
@ -175,7 +194,9 @@ export enum DynamicSecretProviders {
|
|||||||
MongoAtlas = "mongo-db-atlas",
|
MongoAtlas = "mongo-db-atlas",
|
||||||
ElasticSearch = "elastic-search",
|
ElasticSearch = "elastic-search",
|
||||||
MongoDB = "mongo-db",
|
MongoDB = "mongo-db",
|
||||||
RabbitMq = "rabbit-mq"
|
RabbitMq = "rabbit-mq",
|
||||||
|
AzureEntraID = "azure-entra-id",
|
||||||
|
Ldap = "ldap"
|
||||||
}
|
}
|
||||||
|
|
||||||
export const DynamicSecretProviderSchema = z.discriminatedUnion("type", [
|
export const DynamicSecretProviderSchema = z.discriminatedUnion("type", [
|
||||||
@ -187,7 +208,9 @@ export const DynamicSecretProviderSchema = z.discriminatedUnion("type", [
|
|||||||
z.object({ type: z.literal(DynamicSecretProviders.MongoAtlas), inputs: DynamicSecretMongoAtlasSchema }),
|
z.object({ type: z.literal(DynamicSecretProviders.MongoAtlas), inputs: DynamicSecretMongoAtlasSchema }),
|
||||||
z.object({ type: z.literal(DynamicSecretProviders.ElasticSearch), inputs: DynamicSecretElasticSearchSchema }),
|
z.object({ type: z.literal(DynamicSecretProviders.ElasticSearch), inputs: DynamicSecretElasticSearchSchema }),
|
||||||
z.object({ type: z.literal(DynamicSecretProviders.MongoDB), inputs: DynamicSecretMongoDBSchema }),
|
z.object({ type: z.literal(DynamicSecretProviders.MongoDB), inputs: DynamicSecretMongoDBSchema }),
|
||||||
z.object({ type: z.literal(DynamicSecretProviders.RabbitMq), inputs: DynamicSecretRabbitMqSchema })
|
z.object({ type: z.literal(DynamicSecretProviders.RabbitMq), inputs: DynamicSecretRabbitMqSchema }),
|
||||||
|
z.object({ type: z.literal(DynamicSecretProviders.AzureEntraID), inputs: AzureEntraIDSchema }),
|
||||||
|
z.object({ type: z.literal(DynamicSecretProviders.Ldap), inputs: LdapSchema })
|
||||||
]);
|
]);
|
||||||
|
|
||||||
export type TDynamicProviderFns = {
|
export type TDynamicProviderFns = {
|
||||||
|
@ -30,7 +30,7 @@ export const externalKmsDALFactory = (db: TDbClient) => {
|
|||||||
isDisabled: el.isDisabled,
|
isDisabled: el.isDisabled,
|
||||||
isReserved: el.isReserved,
|
isReserved: el.isReserved,
|
||||||
orgId: el.orgId,
|
orgId: el.orgId,
|
||||||
slug: el.slug,
|
name: el.name,
|
||||||
createdAt: el.createdAt,
|
createdAt: el.createdAt,
|
||||||
updatedAt: el.updatedAt,
|
updatedAt: el.updatedAt,
|
||||||
externalKms: {
|
externalKms: {
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
import { ForbiddenError } from "@casl/ability";
|
import { ForbiddenError } from "@casl/ability";
|
||||||
import slugify from "@sindresorhus/slugify";
|
import slugify from "@sindresorhus/slugify";
|
||||||
|
|
||||||
import { BadRequestError } from "@app/lib/errors";
|
import { BadRequestError, NotFoundError } from "@app/lib/errors";
|
||||||
import { alphaNumericNanoId } from "@app/lib/nanoid";
|
import { alphaNumericNanoId } from "@app/lib/nanoid";
|
||||||
import { TKmsKeyDALFactory } from "@app/services/kms/kms-key-dal";
|
import { TKmsKeyDALFactory } from "@app/services/kms/kms-key-dal";
|
||||||
import { TKmsServiceFactory } from "@app/services/kms/kms-service";
|
import { TKmsServiceFactory } from "@app/services/kms/kms-service";
|
||||||
@ -43,7 +43,7 @@ export const externalKmsServiceFactory = ({
|
|||||||
provider,
|
provider,
|
||||||
description,
|
description,
|
||||||
actor,
|
actor,
|
||||||
slug,
|
name,
|
||||||
actorId,
|
actorId,
|
||||||
actorOrgId,
|
actorOrgId,
|
||||||
actorAuthMethod
|
actorAuthMethod
|
||||||
@ -64,7 +64,7 @@ export const externalKmsServiceFactory = ({
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
const kmsSlug = slug ? slugify(slug) : slugify(alphaNumericNanoId(8).toLowerCase());
|
const kmsName = name ? slugify(name) : slugify(alphaNumericNanoId(8).toLowerCase());
|
||||||
|
|
||||||
let sanitizedProviderInput = "";
|
let sanitizedProviderInput = "";
|
||||||
switch (provider.type) {
|
switch (provider.type) {
|
||||||
@ -96,7 +96,7 @@ export const externalKmsServiceFactory = ({
|
|||||||
{
|
{
|
||||||
isReserved: false,
|
isReserved: false,
|
||||||
description,
|
description,
|
||||||
slug: kmsSlug,
|
name: kmsName,
|
||||||
orgId: actorOrgId
|
orgId: actorOrgId
|
||||||
},
|
},
|
||||||
tx
|
tx
|
||||||
@ -120,7 +120,7 @@ export const externalKmsServiceFactory = ({
|
|||||||
description,
|
description,
|
||||||
actor,
|
actor,
|
||||||
id: kmsId,
|
id: kmsId,
|
||||||
slug,
|
name,
|
||||||
actorId,
|
actorId,
|
||||||
actorOrgId,
|
actorOrgId,
|
||||||
actorAuthMethod
|
actorAuthMethod
|
||||||
@ -142,10 +142,10 @@ export const externalKmsServiceFactory = ({
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
const kmsSlug = slug ? slugify(slug) : undefined;
|
const kmsName = name ? slugify(name) : undefined;
|
||||||
|
|
||||||
const externalKmsDoc = await externalKmsDAL.findOne({ kmsKeyId: kmsDoc.id });
|
const externalKmsDoc = await externalKmsDAL.findOne({ kmsKeyId: kmsDoc.id });
|
||||||
if (!externalKmsDoc) throw new BadRequestError({ message: "External kms not found" });
|
if (!externalKmsDoc) throw new NotFoundError({ message: `External KMS with ID '${kmsId}' not found` });
|
||||||
|
|
||||||
let sanitizedProviderInput = "";
|
let sanitizedProviderInput = "";
|
||||||
const { encryptor: orgDataKeyEncryptor, decryptor: orgDataKeyDecryptor } =
|
const { encryptor: orgDataKeyEncryptor, decryptor: orgDataKeyDecryptor } =
|
||||||
@ -188,7 +188,7 @@ export const externalKmsServiceFactory = ({
|
|||||||
kmsDoc.id,
|
kmsDoc.id,
|
||||||
{
|
{
|
||||||
description,
|
description,
|
||||||
slug: kmsSlug
|
name: kmsName
|
||||||
},
|
},
|
||||||
tx
|
tx
|
||||||
);
|
);
|
||||||
@ -220,7 +220,7 @@ export const externalKmsServiceFactory = ({
|
|||||||
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Delete, OrgPermissionSubjects.Kms);
|
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Delete, OrgPermissionSubjects.Kms);
|
||||||
|
|
||||||
const externalKmsDoc = await externalKmsDAL.findOne({ kmsKeyId: kmsDoc.id });
|
const externalKmsDoc = await externalKmsDAL.findOne({ kmsKeyId: kmsDoc.id });
|
||||||
if (!externalKmsDoc) throw new BadRequestError({ message: "External kms not found" });
|
if (!externalKmsDoc) throw new NotFoundError({ message: `External KMS with ID '${kmsId}' not found` });
|
||||||
|
|
||||||
const externalKms = await externalKmsDAL.transaction(async (tx) => {
|
const externalKms = await externalKmsDAL.transaction(async (tx) => {
|
||||||
const kms = await kmsDAL.deleteById(kmsDoc.id, tx);
|
const kms = await kmsDAL.deleteById(kmsDoc.id, tx);
|
||||||
@ -258,7 +258,7 @@ export const externalKmsServiceFactory = ({
|
|||||||
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Read, OrgPermissionSubjects.Kms);
|
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Read, OrgPermissionSubjects.Kms);
|
||||||
|
|
||||||
const externalKmsDoc = await externalKmsDAL.findOne({ kmsKeyId: kmsDoc.id });
|
const externalKmsDoc = await externalKmsDAL.findOne({ kmsKeyId: kmsDoc.id });
|
||||||
if (!externalKmsDoc) throw new BadRequestError({ message: "External kms not found" });
|
if (!externalKmsDoc) throw new NotFoundError({ message: `External KMS with ID '${kmsId}' not found` });
|
||||||
|
|
||||||
const { decryptor: orgDataKeyDecryptor } = await kmsService.createCipherPairWithDataKey({
|
const { decryptor: orgDataKeyDecryptor } = await kmsService.createCipherPairWithDataKey({
|
||||||
type: KmsDataKey.Organization,
|
type: KmsDataKey.Organization,
|
||||||
@ -280,14 +280,14 @@ export const externalKmsServiceFactory = ({
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const findBySlug = async ({
|
const findByName = async ({
|
||||||
actor,
|
actor,
|
||||||
actorId,
|
actorId,
|
||||||
actorOrgId,
|
actorOrgId,
|
||||||
actorAuthMethod,
|
actorAuthMethod,
|
||||||
slug: kmsSlug
|
name: kmsName
|
||||||
}: TGetExternalKmsBySlugDTO) => {
|
}: TGetExternalKmsBySlugDTO) => {
|
||||||
const kmsDoc = await kmsDAL.findOne({ slug: kmsSlug, orgId: actorOrgId });
|
const kmsDoc = await kmsDAL.findOne({ name: kmsName, orgId: actorOrgId });
|
||||||
const { permission } = await permissionService.getOrgPermission(
|
const { permission } = await permissionService.getOrgPermission(
|
||||||
actor,
|
actor,
|
||||||
actorId,
|
actorId,
|
||||||
@ -298,7 +298,7 @@ export const externalKmsServiceFactory = ({
|
|||||||
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Read, OrgPermissionSubjects.Kms);
|
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Read, OrgPermissionSubjects.Kms);
|
||||||
|
|
||||||
const externalKmsDoc = await externalKmsDAL.findOne({ kmsKeyId: kmsDoc.id });
|
const externalKmsDoc = await externalKmsDAL.findOne({ kmsKeyId: kmsDoc.id });
|
||||||
if (!externalKmsDoc) throw new BadRequestError({ message: "External kms not found" });
|
if (!externalKmsDoc) throw new NotFoundError({ message: `External KMS with ID '${kmsDoc.id}' not found` });
|
||||||
|
|
||||||
const { decryptor: orgDataKeyDecryptor } = await kmsService.createCipherPairWithDataKey({
|
const { decryptor: orgDataKeyDecryptor } = await kmsService.createCipherPairWithDataKey({
|
||||||
type: KmsDataKey.Organization,
|
type: KmsDataKey.Organization,
|
||||||
@ -327,6 +327,6 @@ export const externalKmsServiceFactory = ({
|
|||||||
deleteById,
|
deleteById,
|
||||||
list,
|
list,
|
||||||
findById,
|
findById,
|
||||||
findBySlug
|
findByName
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
@ -3,14 +3,14 @@ import { TOrgPermission } from "@app/lib/types";
|
|||||||
import { TExternalKmsInputSchema, TExternalKmsInputUpdateSchema } from "./providers/model";
|
import { TExternalKmsInputSchema, TExternalKmsInputUpdateSchema } from "./providers/model";
|
||||||
|
|
||||||
export type TCreateExternalKmsDTO = {
|
export type TCreateExternalKmsDTO = {
|
||||||
slug?: string;
|
name?: string;
|
||||||
description?: string;
|
description?: string;
|
||||||
provider: TExternalKmsInputSchema;
|
provider: TExternalKmsInputSchema;
|
||||||
} & Omit<TOrgPermission, "orgId">;
|
} & Omit<TOrgPermission, "orgId">;
|
||||||
|
|
||||||
export type TUpdateExternalKmsDTO = {
|
export type TUpdateExternalKmsDTO = {
|
||||||
id: string;
|
id: string;
|
||||||
slug?: string;
|
name?: string;
|
||||||
description?: string;
|
description?: string;
|
||||||
provider?: TExternalKmsInputUpdateSchema;
|
provider?: TExternalKmsInputUpdateSchema;
|
||||||
} & Omit<TOrgPermission, "orgId">;
|
} & Omit<TOrgPermission, "orgId">;
|
||||||
@ -26,5 +26,5 @@ export type TGetExternalKmsByIdDTO = {
|
|||||||
} & Omit<TOrgPermission, "orgId">;
|
} & Omit<TOrgPermission, "orgId">;
|
||||||
|
|
||||||
export type TGetExternalKmsBySlugDTO = {
|
export type TGetExternalKmsBySlugDTO = {
|
||||||
slug: string;
|
name: string;
|
||||||
} & Omit<TOrgPermission, "orgId">;
|
} & Omit<TOrgPermission, "orgId">;
|
||||||
|
@ -60,21 +60,23 @@ export const groupDALFactory = (db: TDbClient) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
// special query
|
// special query
|
||||||
const findAllGroupMembers = async ({
|
const findAllGroupPossibleMembers = async ({
|
||||||
orgId,
|
orgId,
|
||||||
groupId,
|
groupId,
|
||||||
offset = 0,
|
offset = 0,
|
||||||
limit,
|
limit,
|
||||||
username
|
username, // depreciated in favor of search
|
||||||
|
search
|
||||||
}: {
|
}: {
|
||||||
orgId: string;
|
orgId: string;
|
||||||
groupId: string;
|
groupId: string;
|
||||||
offset?: number;
|
offset?: number;
|
||||||
limit?: number;
|
limit?: number;
|
||||||
username?: string;
|
username?: string;
|
||||||
|
search?: string;
|
||||||
}) => {
|
}) => {
|
||||||
try {
|
try {
|
||||||
let query = db
|
const query = db
|
||||||
.replicaNode()(TableName.OrgMembership)
|
.replicaNode()(TableName.OrgMembership)
|
||||||
.where(`${TableName.OrgMembership}.orgId`, orgId)
|
.where(`${TableName.OrgMembership}.orgId`, orgId)
|
||||||
.join(TableName.Users, `${TableName.OrgMembership}.userId`, `${TableName.Users}.id`)
|
.join(TableName.Users, `${TableName.OrgMembership}.userId`, `${TableName.Users}.id`)
|
||||||
@ -92,31 +94,39 @@ export const groupDALFactory = (db: TDbClient) => {
|
|||||||
db.ref("username").withSchema(TableName.Users),
|
db.ref("username").withSchema(TableName.Users),
|
||||||
db.ref("firstName").withSchema(TableName.Users),
|
db.ref("firstName").withSchema(TableName.Users),
|
||||||
db.ref("lastName").withSchema(TableName.Users),
|
db.ref("lastName").withSchema(TableName.Users),
|
||||||
db.ref("id").withSchema(TableName.Users).as("userId")
|
db.ref("id").withSchema(TableName.Users).as("userId"),
|
||||||
|
db.raw(`count(*) OVER() as total_count`)
|
||||||
)
|
)
|
||||||
.where({ isGhost: false })
|
.where({ isGhost: false })
|
||||||
.offset(offset);
|
.offset(offset)
|
||||||
|
.orderBy("firstName", "asc");
|
||||||
|
|
||||||
if (limit) {
|
if (limit) {
|
||||||
query = query.limit(limit);
|
void query.limit(limit);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (username) {
|
if (search) {
|
||||||
query = query.andWhere(`${TableName.Users}.username`, "ilike", `%${username}%`);
|
void query.andWhereRaw(`CONCAT_WS(' ', "firstName", "lastName", "username") ilike '%${search}%'`);
|
||||||
|
} else if (username) {
|
||||||
|
void query.andWhere(`${TableName.Users}.username`, "ilike", `%${username}%`);
|
||||||
}
|
}
|
||||||
|
|
||||||
const members = await query;
|
const members = await query;
|
||||||
|
|
||||||
return members.map(
|
return {
|
||||||
({ email, username: memberUsername, firstName, lastName, userId, groupId: memberGroupId }) => ({
|
members: members.map(
|
||||||
id: userId,
|
({ email, username: memberUsername, firstName, lastName, userId, groupId: memberGroupId }) => ({
|
||||||
email,
|
id: userId,
|
||||||
username: memberUsername,
|
email,
|
||||||
firstName,
|
username: memberUsername,
|
||||||
lastName,
|
firstName,
|
||||||
isPartOfGroup: !!memberGroupId
|
lastName,
|
||||||
})
|
isPartOfGroup: !!memberGroupId
|
||||||
);
|
})
|
||||||
|
),
|
||||||
|
// @ts-expect-error col select is raw and not strongly typed
|
||||||
|
totalCount: Number(members?.[0]?.total_count ?? 0)
|
||||||
|
};
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
throw new DatabaseError({ error, name: "Find all org members" });
|
throw new DatabaseError({ error, name: "Find all org members" });
|
||||||
}
|
}
|
||||||
@ -125,7 +135,7 @@ export const groupDALFactory = (db: TDbClient) => {
|
|||||||
return {
|
return {
|
||||||
findGroups,
|
findGroups,
|
||||||
findByOrgId,
|
findByOrgId,
|
||||||
findAllGroupMembers,
|
findAllGroupPossibleMembers,
|
||||||
...groupOrm
|
...groupOrm
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
@ -2,7 +2,7 @@ import { Knex } from "knex";
|
|||||||
|
|
||||||
import { SecretKeyEncoding, TableName, TUsers } from "@app/db/schemas";
|
import { SecretKeyEncoding, TableName, TUsers } from "@app/db/schemas";
|
||||||
import { decryptAsymmetric, encryptAsymmetric, infisicalSymmetricDecrypt } from "@app/lib/crypto/encryption";
|
import { decryptAsymmetric, encryptAsymmetric, infisicalSymmetricDecrypt } from "@app/lib/crypto/encryption";
|
||||||
import { BadRequestError, ScimRequestError } from "@app/lib/errors";
|
import { BadRequestError, ForbiddenRequestError, NotFoundError, ScimRequestError } from "@app/lib/errors";
|
||||||
|
|
||||||
import {
|
import {
|
||||||
TAddUsersToGroup,
|
TAddUsersToGroup,
|
||||||
@ -73,24 +73,24 @@ const addAcceptedUsersToGroup = async ({
|
|||||||
const ghostUser = await projectDAL.findProjectGhostUser(projectId, tx);
|
const ghostUser = await projectDAL.findProjectGhostUser(projectId, tx);
|
||||||
|
|
||||||
if (!ghostUser) {
|
if (!ghostUser) {
|
||||||
throw new BadRequestError({
|
throw new NotFoundError({
|
||||||
message: "Failed to find sudo user"
|
message: `Failed to find project owner of project with ID '${projectId}'`
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
const ghostUserLatestKey = await projectKeyDAL.findLatestProjectKey(ghostUser.id, projectId, tx);
|
const ghostUserLatestKey = await projectKeyDAL.findLatestProjectKey(ghostUser.id, projectId, tx);
|
||||||
|
|
||||||
if (!ghostUserLatestKey) {
|
if (!ghostUserLatestKey) {
|
||||||
throw new BadRequestError({
|
throw new NotFoundError({
|
||||||
message: "Failed to find sudo user latest key"
|
message: `Failed to find project owner's latest key in project with ID '${projectId}'`
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
const bot = await projectBotDAL.findOne({ projectId }, tx);
|
const bot = await projectBotDAL.findOne({ projectId }, tx);
|
||||||
|
|
||||||
if (!bot) {
|
if (!bot) {
|
||||||
throw new BadRequestError({
|
throw new NotFoundError({
|
||||||
message: "Failed to find bot"
|
message: `Failed to find project bot in project with ID '${projectId}'`
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -200,7 +200,7 @@ export const addUsersToGroupByUserIds = async ({
|
|||||||
|
|
||||||
userIds.forEach((userId) => {
|
userIds.forEach((userId) => {
|
||||||
if (!existingUserOrgMembershipsUserIdsSet.has(userId))
|
if (!existingUserOrgMembershipsUserIdsSet.has(userId))
|
||||||
throw new BadRequestError({
|
throw new ForbiddenRequestError({
|
||||||
message: `User with id ${userId} is not part of the organization`
|
message: `User with id ${userId} is not part of the organization`
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@ -303,7 +303,7 @@ export const removeUsersFromGroupByUserIds = async ({
|
|||||||
|
|
||||||
userIds.forEach((userId) => {
|
userIds.forEach((userId) => {
|
||||||
if (!existingUserGroupMembershipsUserIdsSet.has(userId))
|
if (!existingUserGroupMembershipsUserIdsSet.has(userId))
|
||||||
throw new BadRequestError({
|
throw new ForbiddenRequestError({
|
||||||
message: `User(s) are not part of the group ${group.slug}`
|
message: `User(s) are not part of the group ${group.slug}`
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@ -415,7 +415,7 @@ export const convertPendingGroupAdditionsToGroupMemberships = async ({
|
|||||||
const usersUserIdsSet = new Set(users.map((u) => u.id));
|
const usersUserIdsSet = new Set(users.map((u) => u.id));
|
||||||
userIds.forEach((userId) => {
|
userIds.forEach((userId) => {
|
||||||
if (!usersUserIdsSet.has(userId)) {
|
if (!usersUserIdsSet.has(userId)) {
|
||||||
throw new BadRequestError({
|
throw new NotFoundError({
|
||||||
message: `Failed to find user with id ${userId}`
|
message: `Failed to find user with id ${userId}`
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -3,7 +3,7 @@ import slugify from "@sindresorhus/slugify";
|
|||||||
|
|
||||||
import { OrgMembershipRole, TOrgRoles } from "@app/db/schemas";
|
import { OrgMembershipRole, TOrgRoles } from "@app/db/schemas";
|
||||||
import { isAtLeastAsPrivileged } from "@app/lib/casl";
|
import { isAtLeastAsPrivileged } from "@app/lib/casl";
|
||||||
import { BadRequestError, ForbiddenRequestError } from "@app/lib/errors";
|
import { BadRequestError, ForbiddenRequestError, NotFoundError, UnauthorizedError } from "@app/lib/errors";
|
||||||
import { alphaNumericNanoId } from "@app/lib/nanoid";
|
import { alphaNumericNanoId } from "@app/lib/nanoid";
|
||||||
import { TGroupProjectDALFactory } from "@app/services/group-project/group-project-dal";
|
import { TGroupProjectDALFactory } from "@app/services/group-project/group-project-dal";
|
||||||
import { TOrgDALFactory } from "@app/services/org/org-dal";
|
import { TOrgDALFactory } from "@app/services/org/org-dal";
|
||||||
@ -21,6 +21,7 @@ import {
|
|||||||
TAddUserToGroupDTO,
|
TAddUserToGroupDTO,
|
||||||
TCreateGroupDTO,
|
TCreateGroupDTO,
|
||||||
TDeleteGroupDTO,
|
TDeleteGroupDTO,
|
||||||
|
TGetGroupByIdDTO,
|
||||||
TListGroupUsersDTO,
|
TListGroupUsersDTO,
|
||||||
TRemoveUserFromGroupDTO,
|
TRemoveUserFromGroupDTO,
|
||||||
TUpdateGroupDTO
|
TUpdateGroupDTO
|
||||||
@ -29,7 +30,10 @@ import { TUserGroupMembershipDALFactory } from "./user-group-membership-dal";
|
|||||||
|
|
||||||
type TGroupServiceFactoryDep = {
|
type TGroupServiceFactoryDep = {
|
||||||
userDAL: Pick<TUserDALFactory, "find" | "findUserEncKeyByUserIdsBatch" | "transaction" | "findOne">;
|
userDAL: Pick<TUserDALFactory, "find" | "findUserEncKeyByUserIdsBatch" | "transaction" | "findOne">;
|
||||||
groupDAL: Pick<TGroupDALFactory, "create" | "findOne" | "update" | "delete" | "findAllGroupMembers">;
|
groupDAL: Pick<
|
||||||
|
TGroupDALFactory,
|
||||||
|
"create" | "findOne" | "update" | "delete" | "findAllGroupPossibleMembers" | "findById"
|
||||||
|
>;
|
||||||
groupProjectDAL: Pick<TGroupProjectDALFactory, "find">;
|
groupProjectDAL: Pick<TGroupProjectDALFactory, "find">;
|
||||||
orgDAL: Pick<TOrgDALFactory, "findMembership" | "countAllOrgMembers">;
|
orgDAL: Pick<TOrgDALFactory, "findMembership" | "countAllOrgMembers">;
|
||||||
userGroupMembershipDAL: Pick<
|
userGroupMembershipDAL: Pick<
|
||||||
@ -58,7 +62,7 @@ export const groupServiceFactory = ({
|
|||||||
licenseService
|
licenseService
|
||||||
}: TGroupServiceFactoryDep) => {
|
}: TGroupServiceFactoryDep) => {
|
||||||
const createGroup = async ({ name, slug, role, actor, actorId, actorAuthMethod, actorOrgId }: TCreateGroupDTO) => {
|
const createGroup = async ({ name, slug, role, actor, actorId, actorAuthMethod, actorOrgId }: TCreateGroupDTO) => {
|
||||||
if (!actorOrgId) throw new BadRequestError({ message: "Failed to create group without organization" });
|
if (!actorOrgId) throw new UnauthorizedError({ message: "No organization ID provided in request" });
|
||||||
|
|
||||||
const { permission } = await permissionService.getOrgPermission(
|
const { permission } = await permissionService.getOrgPermission(
|
||||||
actor,
|
actor,
|
||||||
@ -81,7 +85,8 @@ export const groupServiceFactory = ({
|
|||||||
);
|
);
|
||||||
const isCustomRole = Boolean(customRole);
|
const isCustomRole = Boolean(customRole);
|
||||||
const hasRequiredPriviledges = isAtLeastAsPrivileged(permission, rolePermission);
|
const hasRequiredPriviledges = isAtLeastAsPrivileged(permission, rolePermission);
|
||||||
if (!hasRequiredPriviledges) throw new BadRequestError({ message: "Failed to create a more privileged group" });
|
if (!hasRequiredPriviledges)
|
||||||
|
throw new ForbiddenRequestError({ message: "Failed to create a more privileged group" });
|
||||||
|
|
||||||
const group = await groupDAL.create({
|
const group = await groupDAL.create({
|
||||||
name,
|
name,
|
||||||
@ -95,7 +100,7 @@ export const groupServiceFactory = ({
|
|||||||
};
|
};
|
||||||
|
|
||||||
const updateGroup = async ({
|
const updateGroup = async ({
|
||||||
currentSlug,
|
id,
|
||||||
name,
|
name,
|
||||||
slug,
|
slug,
|
||||||
role,
|
role,
|
||||||
@ -104,7 +109,7 @@ export const groupServiceFactory = ({
|
|||||||
actorAuthMethod,
|
actorAuthMethod,
|
||||||
actorOrgId
|
actorOrgId
|
||||||
}: TUpdateGroupDTO) => {
|
}: TUpdateGroupDTO) => {
|
||||||
if (!actorOrgId) throw new BadRequestError({ message: "Failed to create group without organization" });
|
if (!actorOrgId) throw new UnauthorizedError({ message: "No organization ID provided in request" });
|
||||||
|
|
||||||
const { permission } = await permissionService.getOrgPermission(
|
const { permission } = await permissionService.getOrgPermission(
|
||||||
actor,
|
actor,
|
||||||
@ -121,8 +126,10 @@ export const groupServiceFactory = ({
|
|||||||
message: "Failed to update group due to plan restrictio Upgrade plan to update group."
|
message: "Failed to update group due to plan restrictio Upgrade plan to update group."
|
||||||
});
|
});
|
||||||
|
|
||||||
const group = await groupDAL.findOne({ orgId: actorOrgId, slug: currentSlug });
|
const group = await groupDAL.findOne({ orgId: actorOrgId, id });
|
||||||
if (!group) throw new BadRequestError({ message: `Failed to find group with slug ${currentSlug}` });
|
if (!group) {
|
||||||
|
throw new NotFoundError({ message: `Failed to find group with ID ${id}` });
|
||||||
|
}
|
||||||
|
|
||||||
let customRole: TOrgRoles | undefined;
|
let customRole: TOrgRoles | undefined;
|
||||||
if (role) {
|
if (role) {
|
||||||
@ -134,14 +141,13 @@ export const groupServiceFactory = ({
|
|||||||
const isCustomRole = Boolean(customOrgRole);
|
const isCustomRole = Boolean(customOrgRole);
|
||||||
const hasRequiredNewRolePermission = isAtLeastAsPrivileged(permission, rolePermission);
|
const hasRequiredNewRolePermission = isAtLeastAsPrivileged(permission, rolePermission);
|
||||||
if (!hasRequiredNewRolePermission)
|
if (!hasRequiredNewRolePermission)
|
||||||
throw new BadRequestError({ message: "Failed to create a more privileged group" });
|
throw new ForbiddenRequestError({ message: "Failed to create a more privileged group" });
|
||||||
if (isCustomRole) customRole = customOrgRole;
|
if (isCustomRole) customRole = customOrgRole;
|
||||||
}
|
}
|
||||||
|
|
||||||
const [updatedGroup] = await groupDAL.update(
|
const [updatedGroup] = await groupDAL.update(
|
||||||
{
|
{
|
||||||
orgId: actorOrgId,
|
id: group.id
|
||||||
slug: currentSlug
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name,
|
name,
|
||||||
@ -158,8 +164,8 @@ export const groupServiceFactory = ({
|
|||||||
return updatedGroup;
|
return updatedGroup;
|
||||||
};
|
};
|
||||||
|
|
||||||
const deleteGroup = async ({ groupSlug, actor, actorId, actorAuthMethod, actorOrgId }: TDeleteGroupDTO) => {
|
const deleteGroup = async ({ id, actor, actorId, actorAuthMethod, actorOrgId }: TDeleteGroupDTO) => {
|
||||||
if (!actorOrgId) throw new BadRequestError({ message: "Failed to create group without organization" });
|
if (!actorOrgId) throw new UnauthorizedError({ message: "No organization ID provided in request" });
|
||||||
|
|
||||||
const { permission } = await permissionService.getOrgPermission(
|
const { permission } = await permissionService.getOrgPermission(
|
||||||
actor,
|
actor,
|
||||||
@ -178,24 +184,47 @@ export const groupServiceFactory = ({
|
|||||||
});
|
});
|
||||||
|
|
||||||
const [group] = await groupDAL.delete({
|
const [group] = await groupDAL.delete({
|
||||||
orgId: actorOrgId,
|
id,
|
||||||
slug: groupSlug
|
orgId: actorOrgId
|
||||||
});
|
});
|
||||||
|
|
||||||
return group;
|
return group;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const getGroupById = async ({ id, actor, actorId, actorAuthMethod, actorOrgId }: TGetGroupByIdDTO) => {
|
||||||
|
if (!actorOrgId) throw new UnauthorizedError({ message: "No organization ID provided in request" });
|
||||||
|
|
||||||
|
const { permission } = await permissionService.getOrgPermission(
|
||||||
|
actor,
|
||||||
|
actorId,
|
||||||
|
actorOrgId,
|
||||||
|
actorAuthMethod,
|
||||||
|
actorOrgId
|
||||||
|
);
|
||||||
|
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Read, OrgPermissionSubjects.Groups);
|
||||||
|
|
||||||
|
const group = await groupDAL.findById(id);
|
||||||
|
if (!group) {
|
||||||
|
throw new NotFoundError({
|
||||||
|
message: `Cannot find group with ID ${id}`
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return group;
|
||||||
|
};
|
||||||
|
|
||||||
const listGroupUsers = async ({
|
const listGroupUsers = async ({
|
||||||
groupSlug,
|
id,
|
||||||
offset,
|
offset,
|
||||||
limit,
|
limit,
|
||||||
username,
|
username,
|
||||||
actor,
|
actor,
|
||||||
actorId,
|
actorId,
|
||||||
actorAuthMethod,
|
actorAuthMethod,
|
||||||
actorOrgId
|
actorOrgId,
|
||||||
|
search
|
||||||
}: TListGroupUsersDTO) => {
|
}: TListGroupUsersDTO) => {
|
||||||
if (!actorOrgId) throw new BadRequestError({ message: "Failed to create group without organization" });
|
if (!actorOrgId) throw new UnauthorizedError({ message: "No organization ID provided in request" });
|
||||||
|
|
||||||
const { permission } = await permissionService.getOrgPermission(
|
const { permission } = await permissionService.getOrgPermission(
|
||||||
actor,
|
actor,
|
||||||
@ -208,36 +237,28 @@ export const groupServiceFactory = ({
|
|||||||
|
|
||||||
const group = await groupDAL.findOne({
|
const group = await groupDAL.findOne({
|
||||||
orgId: actorOrgId,
|
orgId: actorOrgId,
|
||||||
slug: groupSlug
|
id
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!group)
|
if (!group)
|
||||||
throw new BadRequestError({
|
throw new NotFoundError({
|
||||||
message: `Failed to find group with slug ${groupSlug}`
|
message: `Failed to find group with ID ${id}`
|
||||||
});
|
});
|
||||||
|
|
||||||
const users = await groupDAL.findAllGroupMembers({
|
const { members, totalCount } = await groupDAL.findAllGroupPossibleMembers({
|
||||||
orgId: group.orgId,
|
orgId: group.orgId,
|
||||||
groupId: group.id,
|
groupId: group.id,
|
||||||
offset,
|
offset,
|
||||||
limit,
|
limit,
|
||||||
username
|
username,
|
||||||
|
search
|
||||||
});
|
});
|
||||||
|
|
||||||
const count = await orgDAL.countAllOrgMembers(group.orgId);
|
return { users: members, totalCount };
|
||||||
|
|
||||||
return { users, totalCount: count };
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const addUserToGroup = async ({
|
const addUserToGroup = async ({ id, username, actor, actorId, actorAuthMethod, actorOrgId }: TAddUserToGroupDTO) => {
|
||||||
groupSlug,
|
if (!actorOrgId) throw new UnauthorizedError({ message: "No organization ID provided in request" });
|
||||||
username,
|
|
||||||
actor,
|
|
||||||
actorId,
|
|
||||||
actorAuthMethod,
|
|
||||||
actorOrgId
|
|
||||||
}: TAddUserToGroupDTO) => {
|
|
||||||
if (!actorOrgId) throw new BadRequestError({ message: "Failed to create group without organization" });
|
|
||||||
|
|
||||||
const { permission } = await permissionService.getOrgPermission(
|
const { permission } = await permissionService.getOrgPermission(
|
||||||
actor,
|
actor,
|
||||||
@ -251,12 +272,12 @@ export const groupServiceFactory = ({
|
|||||||
// check if group with slug exists
|
// check if group with slug exists
|
||||||
const group = await groupDAL.findOne({
|
const group = await groupDAL.findOne({
|
||||||
orgId: actorOrgId,
|
orgId: actorOrgId,
|
||||||
slug: groupSlug
|
id
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!group)
|
if (!group)
|
||||||
throw new BadRequestError({
|
throw new NotFoundError({
|
||||||
message: `Failed to find group with slug ${groupSlug}`
|
message: `Failed to find group with ID ${id}`
|
||||||
});
|
});
|
||||||
|
|
||||||
const { permission: groupRolePermission } = await permissionService.getOrgPermissionByRole(group.role, actorOrgId);
|
const { permission: groupRolePermission } = await permissionService.getOrgPermissionByRole(group.role, actorOrgId);
|
||||||
@ -267,7 +288,7 @@ export const groupServiceFactory = ({
|
|||||||
throw new ForbiddenRequestError({ message: "Failed to add user to more privileged group" });
|
throw new ForbiddenRequestError({ message: "Failed to add user to more privileged group" });
|
||||||
|
|
||||||
const user = await userDAL.findOne({ username });
|
const user = await userDAL.findOne({ username });
|
||||||
if (!user) throw new BadRequestError({ message: `Failed to find user with username ${username}` });
|
if (!user) throw new NotFoundError({ message: `Failed to find user with username ${username}` });
|
||||||
|
|
||||||
const users = await addUsersToGroupByUserIds({
|
const users = await addUsersToGroupByUserIds({
|
||||||
group,
|
group,
|
||||||
@ -285,14 +306,14 @@ export const groupServiceFactory = ({
|
|||||||
};
|
};
|
||||||
|
|
||||||
const removeUserFromGroup = async ({
|
const removeUserFromGroup = async ({
|
||||||
groupSlug,
|
id,
|
||||||
username,
|
username,
|
||||||
actor,
|
actor,
|
||||||
actorId,
|
actorId,
|
||||||
actorAuthMethod,
|
actorAuthMethod,
|
||||||
actorOrgId
|
actorOrgId
|
||||||
}: TRemoveUserFromGroupDTO) => {
|
}: TRemoveUserFromGroupDTO) => {
|
||||||
if (!actorOrgId) throw new BadRequestError({ message: "Failed to create group without organization" });
|
if (!actorOrgId) throw new UnauthorizedError({ message: "No organization ID provided in request" });
|
||||||
|
|
||||||
const { permission } = await permissionService.getOrgPermission(
|
const { permission } = await permissionService.getOrgPermission(
|
||||||
actor,
|
actor,
|
||||||
@ -306,12 +327,12 @@ export const groupServiceFactory = ({
|
|||||||
// check if group with slug exists
|
// check if group with slug exists
|
||||||
const group = await groupDAL.findOne({
|
const group = await groupDAL.findOne({
|
||||||
orgId: actorOrgId,
|
orgId: actorOrgId,
|
||||||
slug: groupSlug
|
id
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!group)
|
if (!group)
|
||||||
throw new BadRequestError({
|
throw new NotFoundError({
|
||||||
message: `Failed to find group with slug ${groupSlug}`
|
message: `Failed to find group with ID ${id}`
|
||||||
});
|
});
|
||||||
|
|
||||||
const { permission: groupRolePermission } = await permissionService.getOrgPermissionByRole(group.role, actorOrgId);
|
const { permission: groupRolePermission } = await permissionService.getOrgPermissionByRole(group.role, actorOrgId);
|
||||||
@ -322,7 +343,7 @@ export const groupServiceFactory = ({
|
|||||||
throw new ForbiddenRequestError({ message: "Failed to delete user from more privileged group" });
|
throw new ForbiddenRequestError({ message: "Failed to delete user from more privileged group" });
|
||||||
|
|
||||||
const user = await userDAL.findOne({ username });
|
const user = await userDAL.findOne({ username });
|
||||||
if (!user) throw new BadRequestError({ message: `Failed to find user with username ${username}` });
|
if (!user) throw new NotFoundError({ message: `Failed to find user with username ${username}` });
|
||||||
|
|
||||||
const users = await removeUsersFromGroupByUserIds({
|
const users = await removeUsersFromGroupByUserIds({
|
||||||
group,
|
group,
|
||||||
@ -342,6 +363,7 @@ export const groupServiceFactory = ({
|
|||||||
deleteGroup,
|
deleteGroup,
|
||||||
listGroupUsers,
|
listGroupUsers,
|
||||||
addUserToGroup,
|
addUserToGroup,
|
||||||
removeUserFromGroup
|
removeUserFromGroup,
|
||||||
|
getGroupById
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
@ -17,7 +17,7 @@ export type TCreateGroupDTO = {
|
|||||||
} & TGenericPermission;
|
} & TGenericPermission;
|
||||||
|
|
||||||
export type TUpdateGroupDTO = {
|
export type TUpdateGroupDTO = {
|
||||||
currentSlug: string;
|
id: string;
|
||||||
} & Partial<{
|
} & Partial<{
|
||||||
name: string;
|
name: string;
|
||||||
slug: string;
|
slug: string;
|
||||||
@ -26,23 +26,28 @@ export type TUpdateGroupDTO = {
|
|||||||
TGenericPermission;
|
TGenericPermission;
|
||||||
|
|
||||||
export type TDeleteGroupDTO = {
|
export type TDeleteGroupDTO = {
|
||||||
groupSlug: string;
|
id: string;
|
||||||
|
} & TGenericPermission;
|
||||||
|
|
||||||
|
export type TGetGroupByIdDTO = {
|
||||||
|
id: string;
|
||||||
} & TGenericPermission;
|
} & TGenericPermission;
|
||||||
|
|
||||||
export type TListGroupUsersDTO = {
|
export type TListGroupUsersDTO = {
|
||||||
groupSlug: string;
|
id: string;
|
||||||
offset: number;
|
offset: number;
|
||||||
limit: number;
|
limit: number;
|
||||||
username?: string;
|
username?: string;
|
||||||
|
search?: string;
|
||||||
} & TGenericPermission;
|
} & TGenericPermission;
|
||||||
|
|
||||||
export type TAddUserToGroupDTO = {
|
export type TAddUserToGroupDTO = {
|
||||||
groupSlug: string;
|
id: string;
|
||||||
username: string;
|
username: string;
|
||||||
} & TGenericPermission;
|
} & TGenericPermission;
|
||||||
|
|
||||||
export type TRemoveUserFromGroupDTO = {
|
export type TRemoveUserFromGroupDTO = {
|
||||||
groupSlug: string;
|
id: string;
|
||||||
username: string;
|
username: string;
|
||||||
} & TGenericPermission;
|
} & TGenericPermission;
|
||||||
|
|
||||||
|
@ -4,7 +4,7 @@ import ms from "ms";
|
|||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
|
|
||||||
import { isAtLeastAsPrivileged } from "@app/lib/casl";
|
import { isAtLeastAsPrivileged } from "@app/lib/casl";
|
||||||
import { BadRequestError, ForbiddenRequestError } from "@app/lib/errors";
|
import { BadRequestError, ForbiddenRequestError, NotFoundError } from "@app/lib/errors";
|
||||||
import { ActorType } from "@app/services/auth/auth-type";
|
import { ActorType } from "@app/services/auth/auth-type";
|
||||||
import { TIdentityProjectDALFactory } from "@app/services/identity-project/identity-project-dal";
|
import { TIdentityProjectDALFactory } from "@app/services/identity-project/identity-project-dal";
|
||||||
import { TProjectDALFactory } from "@app/services/project/project-dal";
|
import { TProjectDALFactory } from "@app/services/project/project-dal";
|
||||||
@ -34,18 +34,12 @@ export type TIdentityProjectAdditionalPrivilegeServiceFactory = ReturnType<
|
|||||||
|
|
||||||
// TODO(akhilmhdh): move this to more centralized
|
// TODO(akhilmhdh): move this to more centralized
|
||||||
export const UnpackedPermissionSchema = z.object({
|
export const UnpackedPermissionSchema = z.object({
|
||||||
subject: z.union([z.string().min(1), z.string().array()]).optional(),
|
subject: z
|
||||||
action: z.union([z.string().min(1), z.string().array()]),
|
.union([z.string().min(1), z.string().array()])
|
||||||
conditions: z
|
.transform((el) => (typeof el !== "string" ? el[0] : el))
|
||||||
.object({
|
.optional(),
|
||||||
environment: z.string().optional(),
|
action: z.union([z.string().min(1), z.string().array()]).transform((el) => (typeof el === "string" ? [el] : el)),
|
||||||
secretPath: z
|
conditions: z.unknown().optional()
|
||||||
.object({
|
|
||||||
$glob: z.string().min(1)
|
|
||||||
})
|
|
||||||
.optional()
|
|
||||||
})
|
|
||||||
.optional()
|
|
||||||
});
|
});
|
||||||
|
|
||||||
const unpackPermissions = (permissions: unknown) =>
|
const unpackPermissions = (permissions: unknown) =>
|
||||||
@ -71,12 +65,12 @@ export const identityProjectAdditionalPrivilegeServiceFactory = ({
|
|||||||
...dto
|
...dto
|
||||||
}: TCreateIdentityPrivilegeDTO) => {
|
}: TCreateIdentityPrivilegeDTO) => {
|
||||||
const project = await projectDAL.findProjectBySlug(projectSlug, actorOrgId);
|
const project = await projectDAL.findProjectBySlug(projectSlug, actorOrgId);
|
||||||
if (!project) throw new BadRequestError({ message: "Project not found" });
|
if (!project) throw new NotFoundError({ message: `Project with slug '${projectSlug}' not found` });
|
||||||
const projectId = project.id;
|
const projectId = project.id;
|
||||||
|
|
||||||
const identityProjectMembership = await identityProjectDAL.findOne({ identityId, projectId });
|
const identityProjectMembership = await identityProjectDAL.findOne({ identityId, projectId });
|
||||||
if (!identityProjectMembership)
|
if (!identityProjectMembership)
|
||||||
throw new BadRequestError({ message: `Failed to find identity with id ${identityId}` });
|
throw new NotFoundError({ message: `Failed to find identity with id ${identityId}` });
|
||||||
|
|
||||||
const { permission } = await permissionService.getProjectPermission(
|
const { permission } = await permissionService.getProjectPermission(
|
||||||
actor,
|
actor,
|
||||||
@ -143,12 +137,12 @@ export const identityProjectAdditionalPrivilegeServiceFactory = ({
|
|||||||
actorAuthMethod
|
actorAuthMethod
|
||||||
}: TUpdateIdentityPrivilegeDTO) => {
|
}: TUpdateIdentityPrivilegeDTO) => {
|
||||||
const project = await projectDAL.findProjectBySlug(projectSlug, actorOrgId);
|
const project = await projectDAL.findProjectBySlug(projectSlug, actorOrgId);
|
||||||
if (!project) throw new BadRequestError({ message: "Project not found" });
|
if (!project) throw new NotFoundError({ message: `Project with slug '${projectSlug}' not found` });
|
||||||
const projectId = project.id;
|
const projectId = project.id;
|
||||||
|
|
||||||
const identityProjectMembership = await identityProjectDAL.findOne({ identityId, projectId });
|
const identityProjectMembership = await identityProjectDAL.findOne({ identityId, projectId });
|
||||||
if (!identityProjectMembership)
|
if (!identityProjectMembership)
|
||||||
throw new BadRequestError({ message: `Failed to find identity with id ${identityId}` });
|
throw new NotFoundError({ message: `Failed to find identity with id ${identityId}` });
|
||||||
|
|
||||||
const { permission } = await permissionService.getProjectPermission(
|
const { permission } = await permissionService.getProjectPermission(
|
||||||
actor,
|
actor,
|
||||||
@ -173,7 +167,11 @@ export const identityProjectAdditionalPrivilegeServiceFactory = ({
|
|||||||
slug,
|
slug,
|
||||||
projectMembershipId: identityProjectMembership.id
|
projectMembershipId: identityProjectMembership.id
|
||||||
});
|
});
|
||||||
if (!identityPrivilege) throw new BadRequestError({ message: "Identity additional privilege not found" });
|
if (!identityPrivilege) {
|
||||||
|
throw new NotFoundError({
|
||||||
|
message: `Identity additional privilege with slug '${slug}' not found for the specified identity with ID '${identityProjectMembership.identityId}'`
|
||||||
|
});
|
||||||
|
}
|
||||||
if (data?.slug) {
|
if (data?.slug) {
|
||||||
const existingSlug = await identityProjectAdditionalPrivilegeDAL.findOne({
|
const existingSlug = await identityProjectAdditionalPrivilegeDAL.findOne({
|
||||||
slug: data.slug,
|
slug: data.slug,
|
||||||
@ -224,12 +222,12 @@ export const identityProjectAdditionalPrivilegeServiceFactory = ({
|
|||||||
actorAuthMethod
|
actorAuthMethod
|
||||||
}: TDeleteIdentityPrivilegeDTO) => {
|
}: TDeleteIdentityPrivilegeDTO) => {
|
||||||
const project = await projectDAL.findProjectBySlug(projectSlug, actorOrgId);
|
const project = await projectDAL.findProjectBySlug(projectSlug, actorOrgId);
|
||||||
if (!project) throw new BadRequestError({ message: "Project not found" });
|
if (!project) throw new NotFoundError({ message: `Project with slug '${projectSlug}' not found` });
|
||||||
const projectId = project.id;
|
const projectId = project.id;
|
||||||
|
|
||||||
const identityProjectMembership = await identityProjectDAL.findOne({ identityId, projectId });
|
const identityProjectMembership = await identityProjectDAL.findOne({ identityId, projectId });
|
||||||
if (!identityProjectMembership)
|
if (!identityProjectMembership)
|
||||||
throw new BadRequestError({ message: `Failed to find identity with id ${identityId}` });
|
throw new NotFoundError({ message: `Failed to find identity with id ${identityId}` });
|
||||||
|
|
||||||
const { permission } = await permissionService.getProjectPermission(
|
const { permission } = await permissionService.getProjectPermission(
|
||||||
actor,
|
actor,
|
||||||
@ -254,7 +252,11 @@ export const identityProjectAdditionalPrivilegeServiceFactory = ({
|
|||||||
slug,
|
slug,
|
||||||
projectMembershipId: identityProjectMembership.id
|
projectMembershipId: identityProjectMembership.id
|
||||||
});
|
});
|
||||||
if (!identityPrivilege) throw new BadRequestError({ message: "Identity additional privilege not found" });
|
if (!identityPrivilege) {
|
||||||
|
throw new NotFoundError({
|
||||||
|
message: `Identity additional privilege with slug '${slug}' not found for the specified identity with ID '${identityProjectMembership.identityId}'`
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
const deletedPrivilege = await identityProjectAdditionalPrivilegeDAL.deleteById(identityPrivilege.id);
|
const deletedPrivilege = await identityProjectAdditionalPrivilegeDAL.deleteById(identityPrivilege.id);
|
||||||
return {
|
return {
|
||||||
@ -274,12 +276,12 @@ export const identityProjectAdditionalPrivilegeServiceFactory = ({
|
|||||||
actorAuthMethod
|
actorAuthMethod
|
||||||
}: TGetIdentityPrivilegeDetailsDTO) => {
|
}: TGetIdentityPrivilegeDetailsDTO) => {
|
||||||
const project = await projectDAL.findProjectBySlug(projectSlug, actorOrgId);
|
const project = await projectDAL.findProjectBySlug(projectSlug, actorOrgId);
|
||||||
if (!project) throw new BadRequestError({ message: "Project not found" });
|
if (!project) throw new NotFoundError({ message: `Project with slug '${projectSlug}' not found` });
|
||||||
const projectId = project.id;
|
const projectId = project.id;
|
||||||
|
|
||||||
const identityProjectMembership = await identityProjectDAL.findOne({ identityId, projectId });
|
const identityProjectMembership = await identityProjectDAL.findOne({ identityId, projectId });
|
||||||
if (!identityProjectMembership)
|
if (!identityProjectMembership)
|
||||||
throw new BadRequestError({ message: `Failed to find identity with id ${identityId}` });
|
throw new NotFoundError({ message: `Failed to find identity with id ${identityId}` });
|
||||||
const { permission } = await permissionService.getProjectPermission(
|
const { permission } = await permissionService.getProjectPermission(
|
||||||
actor,
|
actor,
|
||||||
actorId,
|
actorId,
|
||||||
@ -293,8 +295,11 @@ export const identityProjectAdditionalPrivilegeServiceFactory = ({
|
|||||||
slug,
|
slug,
|
||||||
projectMembershipId: identityProjectMembership.id
|
projectMembershipId: identityProjectMembership.id
|
||||||
});
|
});
|
||||||
if (!identityPrivilege) throw new BadRequestError({ message: "Identity additional privilege not found" });
|
if (!identityPrivilege) {
|
||||||
|
throw new NotFoundError({
|
||||||
|
message: `Identity additional privilege with slug '${slug}' not found for the specified identity with ID '${identityProjectMembership.identityId}'`
|
||||||
|
});
|
||||||
|
}
|
||||||
return {
|
return {
|
||||||
...identityPrivilege,
|
...identityPrivilege,
|
||||||
permissions: unpackPermissions(identityPrivilege.permissions)
|
permissions: unpackPermissions(identityPrivilege.permissions)
|
||||||
@ -310,12 +315,12 @@ export const identityProjectAdditionalPrivilegeServiceFactory = ({
|
|||||||
projectSlug
|
projectSlug
|
||||||
}: TListIdentityPrivilegesDTO) => {
|
}: TListIdentityPrivilegesDTO) => {
|
||||||
const project = await projectDAL.findProjectBySlug(projectSlug, actorOrgId);
|
const project = await projectDAL.findProjectBySlug(projectSlug, actorOrgId);
|
||||||
if (!project) throw new BadRequestError({ message: "Project not found" });
|
if (!project) throw new NotFoundError({ message: `Project with slug '${projectSlug}' not found` });
|
||||||
const projectId = project.id;
|
const projectId = project.id;
|
||||||
|
|
||||||
const identityProjectMembership = await identityProjectDAL.findOne({ identityId, projectId });
|
const identityProjectMembership = await identityProjectDAL.findOne({ identityId, projectId });
|
||||||
if (!identityProjectMembership)
|
if (!identityProjectMembership)
|
||||||
throw new BadRequestError({ message: `Failed to find identity with id ${identityId}` });
|
throw new NotFoundError({ message: `Failed to find identity with id ${identityId}` });
|
||||||
const { permission } = await permissionService.getProjectPermission(
|
const { permission } = await permissionService.getProjectPermission(
|
||||||
actor,
|
actor,
|
||||||
actorId,
|
actorId,
|
||||||
|
@ -1,14 +1,7 @@
|
|||||||
import { ForbiddenError } from "@casl/ability";
|
import { ForbiddenError } from "@casl/ability";
|
||||||
import jwt from "jsonwebtoken";
|
import jwt from "jsonwebtoken";
|
||||||
|
|
||||||
import {
|
import { OrgMembershipStatus, SecretKeyEncoding, TableName, TLdapConfigsUpdate, TUsers } from "@app/db/schemas";
|
||||||
OrgMembershipRole,
|
|
||||||
OrgMembershipStatus,
|
|
||||||
SecretKeyEncoding,
|
|
||||||
TableName,
|
|
||||||
TLdapConfigsUpdate,
|
|
||||||
TUsers
|
|
||||||
} from "@app/db/schemas";
|
|
||||||
import { TGroupDALFactory } from "@app/ee/services/group/group-dal";
|
import { TGroupDALFactory } from "@app/ee/services/group/group-dal";
|
||||||
import { addUsersToGroupByUserIds, removeUsersFromGroupByUserIds } from "@app/ee/services/group/group-fns";
|
import { addUsersToGroupByUserIds, removeUsersFromGroupByUserIds } from "@app/ee/services/group/group-fns";
|
||||||
import { TUserGroupMembershipDALFactory } from "@app/ee/services/group/user-group-membership-dal";
|
import { TUserGroupMembershipDALFactory } from "@app/ee/services/group/user-group-membership-dal";
|
||||||
@ -21,13 +14,14 @@ import {
|
|||||||
infisicalSymmetricDecrypt,
|
infisicalSymmetricDecrypt,
|
||||||
infisicalSymmetricEncypt
|
infisicalSymmetricEncypt
|
||||||
} from "@app/lib/crypto/encryption";
|
} from "@app/lib/crypto/encryption";
|
||||||
import { BadRequestError } from "@app/lib/errors";
|
import { BadRequestError, ForbiddenRequestError, NotFoundError } from "@app/lib/errors";
|
||||||
import { AuthMethod, AuthTokenType } from "@app/services/auth/auth-type";
|
import { AuthMethod, AuthTokenType } from "@app/services/auth/auth-type";
|
||||||
import { TAuthTokenServiceFactory } from "@app/services/auth-token/auth-token-service";
|
import { TAuthTokenServiceFactory } from "@app/services/auth-token/auth-token-service";
|
||||||
import { TokenType } from "@app/services/auth-token/auth-token-types";
|
import { TokenType } from "@app/services/auth-token/auth-token-types";
|
||||||
import { TGroupProjectDALFactory } from "@app/services/group-project/group-project-dal";
|
import { TGroupProjectDALFactory } from "@app/services/group-project/group-project-dal";
|
||||||
import { TOrgBotDALFactory } from "@app/services/org/org-bot-dal";
|
import { TOrgBotDALFactory } from "@app/services/org/org-bot-dal";
|
||||||
import { TOrgDALFactory } from "@app/services/org/org-dal";
|
import { TOrgDALFactory } from "@app/services/org/org-dal";
|
||||||
|
import { getDefaultOrgMembershipRole } from "@app/services/org/org-role-fns";
|
||||||
import { TOrgMembershipDALFactory } from "@app/services/org-membership/org-membership-dal";
|
import { TOrgMembershipDALFactory } from "@app/services/org-membership/org-membership-dal";
|
||||||
import { TProjectDALFactory } from "@app/services/project/project-dal";
|
import { TProjectDALFactory } from "@app/services/project/project-dal";
|
||||||
import { TProjectBotDALFactory } from "@app/services/project-bot/project-bot-dal";
|
import { TProjectBotDALFactory } from "@app/services/project-bot/project-bot-dal";
|
||||||
@ -253,7 +247,11 @@ export const ldapConfigServiceFactory = ({
|
|||||||
};
|
};
|
||||||
|
|
||||||
const orgBot = await orgBotDAL.findOne({ orgId });
|
const orgBot = await orgBotDAL.findOne({ orgId });
|
||||||
if (!orgBot) throw new BadRequestError({ message: "Org bot not found", name: "OrgBotNotFound" });
|
if (!orgBot)
|
||||||
|
throw new NotFoundError({
|
||||||
|
message: `Organization bot in organization with ID '${orgId}' not found`,
|
||||||
|
name: "OrgBotNotFound"
|
||||||
|
});
|
||||||
const key = infisicalSymmetricDecrypt({
|
const key = infisicalSymmetricDecrypt({
|
||||||
ciphertext: orgBot.encryptedSymmetricKey,
|
ciphertext: orgBot.encryptedSymmetricKey,
|
||||||
iv: orgBot.symmetricKeyIV,
|
iv: orgBot.symmetricKeyIV,
|
||||||
@ -289,10 +287,19 @@ export const ldapConfigServiceFactory = ({
|
|||||||
|
|
||||||
const getLdapCfg = async (filter: { orgId: string; isActive?: boolean; id?: string }) => {
|
const getLdapCfg = async (filter: { orgId: string; isActive?: boolean; id?: string }) => {
|
||||||
const ldapConfig = await ldapConfigDAL.findOne(filter);
|
const ldapConfig = await ldapConfigDAL.findOne(filter);
|
||||||
if (!ldapConfig) throw new BadRequestError({ message: "Failed to find organization LDAP data" });
|
if (!ldapConfig) {
|
||||||
|
throw new NotFoundError({
|
||||||
|
message: `Failed to find organization LDAP data in organization with ID '${filter.orgId}'`
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
const orgBot = await orgBotDAL.findOne({ orgId: ldapConfig.orgId });
|
const orgBot = await orgBotDAL.findOne({ orgId: ldapConfig.orgId });
|
||||||
if (!orgBot) throw new BadRequestError({ message: "Org bot not found", name: "OrgBotNotFound" });
|
if (!orgBot) {
|
||||||
|
throw new NotFoundError({
|
||||||
|
message: `Organization bot not found in organization with ID ${ldapConfig.orgId}`,
|
||||||
|
name: "OrgBotNotFound"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
const key = infisicalSymmetricDecrypt({
|
const key = infisicalSymmetricDecrypt({
|
||||||
ciphertext: orgBot.encryptedSymmetricKey,
|
ciphertext: orgBot.encryptedSymmetricKey,
|
||||||
@ -375,7 +382,7 @@ export const ldapConfigServiceFactory = ({
|
|||||||
|
|
||||||
const bootLdap = async (organizationSlug: string) => {
|
const bootLdap = async (organizationSlug: string) => {
|
||||||
const organization = await orgDAL.findOne({ slug: organizationSlug });
|
const organization = await orgDAL.findOne({ slug: organizationSlug });
|
||||||
if (!organization) throw new BadRequestError({ message: "Org not found" });
|
if (!organization) throw new NotFoundError({ message: `Organization with slug '${organizationSlug}' not found` });
|
||||||
|
|
||||||
const ldapConfig = await getLdapCfg({
|
const ldapConfig = await getLdapCfg({
|
||||||
orgId: organization.id,
|
orgId: organization.id,
|
||||||
@ -420,7 +427,7 @@ export const ldapConfigServiceFactory = ({
|
|||||||
const serverCfg = await getServerCfg();
|
const serverCfg = await getServerCfg();
|
||||||
|
|
||||||
if (serverCfg.enabledLoginMethods && !serverCfg.enabledLoginMethods.includes(LoginMethod.LDAP)) {
|
if (serverCfg.enabledLoginMethods && !serverCfg.enabledLoginMethods.includes(LoginMethod.LDAP)) {
|
||||||
throw new BadRequestError({
|
throw new ForbiddenRequestError({
|
||||||
message: "Login with LDAP is disabled by administrator."
|
message: "Login with LDAP is disabled by administrator."
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@ -432,7 +439,7 @@ export const ldapConfigServiceFactory = ({
|
|||||||
});
|
});
|
||||||
|
|
||||||
const organization = await orgDAL.findOrgById(orgId);
|
const organization = await orgDAL.findOrgById(orgId);
|
||||||
if (!organization) throw new BadRequestError({ message: "Org not found" });
|
if (!organization) throw new NotFoundError({ message: `Organization with ID '${orgId}' not found` });
|
||||||
|
|
||||||
if (userAlias) {
|
if (userAlias) {
|
||||||
await userDAL.transaction(async (tx) => {
|
await userDAL.transaction(async (tx) => {
|
||||||
@ -444,11 +451,14 @@ export const ldapConfigServiceFactory = ({
|
|||||||
{ tx }
|
{ tx }
|
||||||
);
|
);
|
||||||
if (!orgMembership) {
|
if (!orgMembership) {
|
||||||
|
const { role, roleId } = await getDefaultOrgMembershipRole(organization.defaultMembershipRole);
|
||||||
|
|
||||||
await orgDAL.createMembership(
|
await orgDAL.createMembership(
|
||||||
{
|
{
|
||||||
userId: userAlias.userId,
|
userId: userAlias.userId,
|
||||||
orgId,
|
orgId,
|
||||||
role: OrgMembershipRole.Member,
|
role,
|
||||||
|
roleId,
|
||||||
status: OrgMembershipStatus.Accepted,
|
status: OrgMembershipStatus.Accepted,
|
||||||
isActive: true
|
isActive: true
|
||||||
},
|
},
|
||||||
@ -529,12 +539,15 @@ export const ldapConfigServiceFactory = ({
|
|||||||
);
|
);
|
||||||
|
|
||||||
if (!orgMembership) {
|
if (!orgMembership) {
|
||||||
|
const { role, roleId } = await getDefaultOrgMembershipRole(organization.defaultMembershipRole);
|
||||||
|
|
||||||
await orgMembershipDAL.create(
|
await orgMembershipDAL.create(
|
||||||
{
|
{
|
||||||
userId: newUser.id,
|
userId: newUser.id,
|
||||||
inviteEmail: email,
|
inviteEmail: email,
|
||||||
orgId,
|
orgId,
|
||||||
role: OrgMembershipRole.Member,
|
role,
|
||||||
|
roleId,
|
||||||
status: newUser.isAccepted ? OrgMembershipStatus.Accepted : OrgMembershipStatus.Invited, // if user is fully completed, then set status to accepted, otherwise set it to invited so we can update it later
|
status: newUser.isAccepted ? OrgMembershipStatus.Accepted : OrgMembershipStatus.Invited, // if user is fully completed, then set status to accepted, otherwise set it to invited so we can update it later
|
||||||
isActive: true
|
isActive: true
|
||||||
},
|
},
|
||||||
@ -700,7 +713,11 @@ export const ldapConfigServiceFactory = ({
|
|||||||
orgId
|
orgId
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!ldapConfig) throw new BadRequestError({ message: "Failed to find organization LDAP data" });
|
if (!ldapConfig) {
|
||||||
|
throw new NotFoundError({
|
||||||
|
message: `Failed to find organization LDAP data with ID '${ldapConfigId}' in organization with ID ${orgId}`
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
const groupMaps = await ldapGroupMapDAL.findLdapGroupMapsByLdapConfigId(ldapConfigId);
|
const groupMaps = await ldapGroupMapDAL.findLdapGroupMapsByLdapConfigId(ldapConfigId);
|
||||||
|
|
||||||
@ -741,13 +758,17 @@ export const ldapConfigServiceFactory = ({
|
|||||||
const groups = await searchGroups(ldapConfig, groupSearchFilter, ldapConfig.groupSearchBase);
|
const groups = await searchGroups(ldapConfig, groupSearchFilter, ldapConfig.groupSearchBase);
|
||||||
|
|
||||||
if (!groups.some((g) => g.cn === ldapGroupCN)) {
|
if (!groups.some((g) => g.cn === ldapGroupCN)) {
|
||||||
throw new BadRequestError({
|
throw new NotFoundError({
|
||||||
message: "Failed to find LDAP Group CN"
|
message: "Failed to find LDAP Group CN"
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
const group = await groupDAL.findOne({ slug: groupSlug, orgId });
|
const group = await groupDAL.findOne({ slug: groupSlug, orgId });
|
||||||
if (!group) throw new BadRequestError({ message: "Failed to find group" });
|
if (!group) {
|
||||||
|
throw new NotFoundError({
|
||||||
|
message: `Failed to find group with slug '${groupSlug}' in organization with ID '${orgId}'`
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
const groupMap = await ldapGroupMapDAL.create({
|
const groupMap = await ldapGroupMapDAL.create({
|
||||||
ldapConfigId,
|
ldapConfigId,
|
||||||
@ -781,7 +802,11 @@ export const ldapConfigServiceFactory = ({
|
|||||||
orgId
|
orgId
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!ldapConfig) throw new BadRequestError({ message: "Failed to find organization LDAP data" });
|
if (!ldapConfig) {
|
||||||
|
throw new NotFoundError({
|
||||||
|
message: `Failed to find organization LDAP data with ID '${ldapConfigId}' in organization with ID ${orgId}`
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
const [deletedGroupMap] = await ldapGroupMapDAL.delete({
|
const [deletedGroupMap] = await ldapGroupMapDAL.delete({
|
||||||
ldapConfigId: ldapConfig.id,
|
ldapConfigId: ldapConfig.id,
|
||||||
|
@ -46,7 +46,8 @@ export const getDefaultOnPremFeatures = (): TFeatureSet => ({
|
|||||||
writeLimit: 200,
|
writeLimit: 200,
|
||||||
secretsLimit: 40
|
secretsLimit: 40
|
||||||
},
|
},
|
||||||
pkiEst: false
|
pkiEst: false,
|
||||||
|
enforceMfa: false
|
||||||
});
|
});
|
||||||
|
|
||||||
export const setupLicenseRequestWithStore = (baseURL: string, refreshUrl: string, licenseKey: string) => {
|
export const setupLicenseRequestWithStore = (baseURL: string, refreshUrl: string, licenseKey: string) => {
|
||||||
|
@ -10,7 +10,7 @@ import { Knex } from "knex";
|
|||||||
import { TKeyStoreFactory } from "@app/keystore/keystore";
|
import { TKeyStoreFactory } from "@app/keystore/keystore";
|
||||||
import { getConfig } from "@app/lib/config/env";
|
import { getConfig } from "@app/lib/config/env";
|
||||||
import { verifyOfflineLicense } from "@app/lib/crypto";
|
import { verifyOfflineLicense } from "@app/lib/crypto";
|
||||||
import { BadRequestError } from "@app/lib/errors";
|
import { NotFoundError } from "@app/lib/errors";
|
||||||
import { logger } from "@app/lib/logger";
|
import { logger } from "@app/lib/logger";
|
||||||
import { TOrgDALFactory } from "@app/services/org/org-dal";
|
import { TOrgDALFactory } from "@app/services/org/org-dal";
|
||||||
|
|
||||||
@ -145,7 +145,7 @@ export const licenseServiceFactory = ({
|
|||||||
if (cachedPlan) return JSON.parse(cachedPlan) as TFeatureSet;
|
if (cachedPlan) return JSON.parse(cachedPlan) as TFeatureSet;
|
||||||
|
|
||||||
const org = await orgDAL.findOrgById(orgId);
|
const org = await orgDAL.findOrgById(orgId);
|
||||||
if (!org) throw new BadRequestError({ message: "Org not found" });
|
if (!org) throw new NotFoundError({ message: `Organization with ID '${orgId}' not found` });
|
||||||
const {
|
const {
|
||||||
data: { currentPlan }
|
data: { currentPlan }
|
||||||
} = await licenseServerCloudApi.request.get<{ currentPlan: TFeatureSet }>(
|
} = await licenseServerCloudApi.request.get<{ currentPlan: TFeatureSet }>(
|
||||||
@ -204,7 +204,7 @@ export const licenseServiceFactory = ({
|
|||||||
const updateSubscriptionOrgMemberCount = async (orgId: string, tx?: Knex) => {
|
const updateSubscriptionOrgMemberCount = async (orgId: string, tx?: Knex) => {
|
||||||
if (instanceType === InstanceType.Cloud) {
|
if (instanceType === InstanceType.Cloud) {
|
||||||
const org = await orgDAL.findOrgById(orgId);
|
const org = await orgDAL.findOrgById(orgId);
|
||||||
if (!org) throw new BadRequestError({ message: "Org not found" });
|
if (!org) throw new NotFoundError({ message: `Organization with ID '${orgId}' not found` });
|
||||||
|
|
||||||
const quantity = await licenseDAL.countOfOrgMembers(orgId, tx);
|
const quantity = await licenseDAL.countOfOrgMembers(orgId, tx);
|
||||||
const quantityIdentities = await licenseDAL.countOrgUsersAndIdentities(orgId, tx);
|
const quantityIdentities = await licenseDAL.countOrgUsersAndIdentities(orgId, tx);
|
||||||
@ -266,8 +266,8 @@ export const licenseServiceFactory = ({
|
|||||||
|
|
||||||
const organization = await orgDAL.findOrgById(orgId);
|
const organization = await orgDAL.findOrgById(orgId);
|
||||||
if (!organization) {
|
if (!organization) {
|
||||||
throw new BadRequestError({
|
throw new NotFoundError({
|
||||||
message: "Failed to find organization"
|
message: `Organization with ID '${orgId}' not found`
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -294,8 +294,8 @@ export const licenseServiceFactory = ({
|
|||||||
|
|
||||||
const organization = await orgDAL.findOrgById(orgId);
|
const organization = await orgDAL.findOrgById(orgId);
|
||||||
if (!organization) {
|
if (!organization) {
|
||||||
throw new BadRequestError({
|
throw new NotFoundError({
|
||||||
message: "Failed to find organization"
|
message: "Organization not found"
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -340,8 +340,8 @@ export const licenseServiceFactory = ({
|
|||||||
|
|
||||||
const organization = await orgDAL.findOrgById(orgId);
|
const organization = await orgDAL.findOrgById(orgId);
|
||||||
if (!organization) {
|
if (!organization) {
|
||||||
throw new BadRequestError({
|
throw new NotFoundError({
|
||||||
message: "Failed to find organization"
|
message: `Organization with ID '${orgId}' not found`
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
const { data } = await licenseServerCloudApi.request.get(
|
const { data } = await licenseServerCloudApi.request.get(
|
||||||
@ -357,8 +357,8 @@ export const licenseServiceFactory = ({
|
|||||||
|
|
||||||
const organization = await orgDAL.findOrgById(orgId);
|
const organization = await orgDAL.findOrgById(orgId);
|
||||||
if (!organization) {
|
if (!organization) {
|
||||||
throw new BadRequestError({
|
throw new NotFoundError({
|
||||||
message: "Failed to find organization"
|
message: `Organization with ID '${orgId}' not found`
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
const { data } = await licenseServerCloudApi.request.get(
|
const { data } = await licenseServerCloudApi.request.get(
|
||||||
@ -373,8 +373,8 @@ export const licenseServiceFactory = ({
|
|||||||
|
|
||||||
const organization = await orgDAL.findOrgById(orgId);
|
const organization = await orgDAL.findOrgById(orgId);
|
||||||
if (!organization) {
|
if (!organization) {
|
||||||
throw new BadRequestError({
|
throw new NotFoundError({
|
||||||
message: "Failed to find organization"
|
message: `Organization with ID '${orgId}' not found`
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -398,8 +398,8 @@ export const licenseServiceFactory = ({
|
|||||||
|
|
||||||
const organization = await orgDAL.findOrgById(orgId);
|
const organization = await orgDAL.findOrgById(orgId);
|
||||||
if (!organization) {
|
if (!organization) {
|
||||||
throw new BadRequestError({
|
throw new NotFoundError({
|
||||||
message: "Failed to find organization"
|
message: `Organization with ID '${orgId}' not found`
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
const { data } = await licenseServerCloudApi.request.patch(
|
const { data } = await licenseServerCloudApi.request.patch(
|
||||||
@ -418,8 +418,8 @@ export const licenseServiceFactory = ({
|
|||||||
|
|
||||||
const organization = await orgDAL.findOrgById(orgId);
|
const organization = await orgDAL.findOrgById(orgId);
|
||||||
if (!organization) {
|
if (!organization) {
|
||||||
throw new BadRequestError({
|
throw new NotFoundError({
|
||||||
message: "Failed to find organization"
|
message: `Organization with ID '${orgId}' not found`
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -445,8 +445,8 @@ export const licenseServiceFactory = ({
|
|||||||
|
|
||||||
const organization = await orgDAL.findOrgById(orgId);
|
const organization = await orgDAL.findOrgById(orgId);
|
||||||
if (!organization) {
|
if (!organization) {
|
||||||
throw new BadRequestError({
|
throw new NotFoundError({
|
||||||
message: "Failed to find organization"
|
message: `Organization with ID '${orgId}' not found`
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
const {
|
const {
|
||||||
@ -474,8 +474,8 @@ export const licenseServiceFactory = ({
|
|||||||
|
|
||||||
const organization = await orgDAL.findOrgById(orgId);
|
const organization = await orgDAL.findOrgById(orgId);
|
||||||
if (!organization) {
|
if (!organization) {
|
||||||
throw new BadRequestError({
|
throw new NotFoundError({
|
||||||
message: "Failed to find organization"
|
message: `Organization with ID '${orgId}' not found`
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -491,8 +491,8 @@ export const licenseServiceFactory = ({
|
|||||||
|
|
||||||
const organization = await orgDAL.findOrgById(orgId);
|
const organization = await orgDAL.findOrgById(orgId);
|
||||||
if (!organization) {
|
if (!organization) {
|
||||||
throw new BadRequestError({
|
throw new NotFoundError({
|
||||||
message: "Failed to find organization"
|
message: `Organization with ID '${orgId}' not found`
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
const {
|
const {
|
||||||
@ -509,8 +509,8 @@ export const licenseServiceFactory = ({
|
|||||||
|
|
||||||
const organization = await orgDAL.findOrgById(orgId);
|
const organization = await orgDAL.findOrgById(orgId);
|
||||||
if (!organization) {
|
if (!organization) {
|
||||||
throw new BadRequestError({
|
throw new NotFoundError({
|
||||||
message: "Failed to find organization"
|
message: `Organization with ID '${orgId}' not found`
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -530,8 +530,8 @@ export const licenseServiceFactory = ({
|
|||||||
|
|
||||||
const organization = await orgDAL.findOrgById(orgId);
|
const organization = await orgDAL.findOrgById(orgId);
|
||||||
if (!organization) {
|
if (!organization) {
|
||||||
throw new BadRequestError({
|
throw new NotFoundError({
|
||||||
message: "Failed to find organization"
|
message: `Organization with ID '${orgId}' not found`
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -547,8 +547,8 @@ export const licenseServiceFactory = ({
|
|||||||
|
|
||||||
const organization = await orgDAL.findOrgById(orgId);
|
const organization = await orgDAL.findOrgById(orgId);
|
||||||
if (!organization) {
|
if (!organization) {
|
||||||
throw new BadRequestError({
|
throw new NotFoundError({
|
||||||
message: "Failed to find organization"
|
message: `Organization with ID '${orgId}' not found`
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -564,8 +564,8 @@ export const licenseServiceFactory = ({
|
|||||||
|
|
||||||
const organization = await orgDAL.findOrgById(orgId);
|
const organization = await orgDAL.findOrgById(orgId);
|
||||||
if (!organization) {
|
if (!organization) {
|
||||||
throw new BadRequestError({
|
throw new NotFoundError({
|
||||||
message: "Failed to find organization"
|
message: `Organization with ID '${orgId}' not found`
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -64,6 +64,7 @@ export type TFeatureSet = {
|
|||||||
secretsLimit: number;
|
secretsLimit: number;
|
||||||
};
|
};
|
||||||
pkiEst: boolean;
|
pkiEst: boolean;
|
||||||
|
enforceMfa: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type TOrgPlansTableDTO = {
|
export type TOrgPlansTableDTO = {
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
import { TDbClient } from "@app/db";
|
import { TDbClient } from "@app/db";
|
||||||
import { TableName } from "@app/db/schemas";
|
import { TableName } from "@app/db/schemas";
|
||||||
|
import { DatabaseError } from "@app/lib/errors";
|
||||||
import { ormify } from "@app/lib/knex";
|
import { ormify } from "@app/lib/knex";
|
||||||
|
|
||||||
export type TOidcConfigDALFactory = ReturnType<typeof oidcConfigDALFactory>;
|
export type TOidcConfigDALFactory = ReturnType<typeof oidcConfigDALFactory>;
|
||||||
@ -7,5 +8,22 @@ export type TOidcConfigDALFactory = ReturnType<typeof oidcConfigDALFactory>;
|
|||||||
export const oidcConfigDALFactory = (db: TDbClient) => {
|
export const oidcConfigDALFactory = (db: TDbClient) => {
|
||||||
const oidcCfgOrm = ormify(db, TableName.OidcConfig);
|
const oidcCfgOrm = ormify(db, TableName.OidcConfig);
|
||||||
|
|
||||||
return { ...oidcCfgOrm };
|
const findEnforceableOidcCfg = async (orgId: string) => {
|
||||||
|
try {
|
||||||
|
const oidcCfg = await db
|
||||||
|
.replicaNode()(TableName.OidcConfig)
|
||||||
|
.where({
|
||||||
|
orgId,
|
||||||
|
isActive: true
|
||||||
|
})
|
||||||
|
.whereNotNull("lastUsed")
|
||||||
|
.first();
|
||||||
|
|
||||||
|
return oidcCfg;
|
||||||
|
} catch (error) {
|
||||||
|
throw new DatabaseError({ error, name: "Find org by id" });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return { ...oidcCfgOrm, findEnforceableOidcCfg };
|
||||||
};
|
};
|
||||||
|
@ -3,7 +3,7 @@ import { ForbiddenError } from "@casl/ability";
|
|||||||
import jwt from "jsonwebtoken";
|
import jwt from "jsonwebtoken";
|
||||||
import { Issuer, Issuer as OpenIdIssuer, Strategy as OpenIdStrategy, TokenSet } from "openid-client";
|
import { Issuer, Issuer as OpenIdIssuer, Strategy as OpenIdStrategy, TokenSet } from "openid-client";
|
||||||
|
|
||||||
import { OrgMembershipRole, OrgMembershipStatus, SecretKeyEncoding, TableName, TUsers } from "@app/db/schemas";
|
import { OrgMembershipStatus, SecretKeyEncoding, TableName, TUsers } from "@app/db/schemas";
|
||||||
import { TOidcConfigsUpdate } from "@app/db/schemas/oidc-configs";
|
import { TOidcConfigsUpdate } from "@app/db/schemas/oidc-configs";
|
||||||
import { TLicenseServiceFactory } from "@app/ee/services/license/license-service";
|
import { TLicenseServiceFactory } from "@app/ee/services/license/license-service";
|
||||||
import { OrgPermissionActions, OrgPermissionSubjects } from "@app/ee/services/permission/org-permission";
|
import { OrgPermissionActions, OrgPermissionSubjects } from "@app/ee/services/permission/org-permission";
|
||||||
@ -17,12 +17,13 @@ import {
|
|||||||
infisicalSymmetricDecrypt,
|
infisicalSymmetricDecrypt,
|
||||||
infisicalSymmetricEncypt
|
infisicalSymmetricEncypt
|
||||||
} from "@app/lib/crypto/encryption";
|
} from "@app/lib/crypto/encryption";
|
||||||
import { BadRequestError } from "@app/lib/errors";
|
import { BadRequestError, ForbiddenRequestError, NotFoundError } from "@app/lib/errors";
|
||||||
import { AuthMethod, AuthTokenType } from "@app/services/auth/auth-type";
|
import { AuthMethod, AuthTokenType } from "@app/services/auth/auth-type";
|
||||||
import { TAuthTokenServiceFactory } from "@app/services/auth-token/auth-token-service";
|
import { TAuthTokenServiceFactory } from "@app/services/auth-token/auth-token-service";
|
||||||
import { TokenType } from "@app/services/auth-token/auth-token-types";
|
import { TokenType } from "@app/services/auth-token/auth-token-types";
|
||||||
import { TOrgBotDALFactory } from "@app/services/org/org-bot-dal";
|
import { TOrgBotDALFactory } from "@app/services/org/org-bot-dal";
|
||||||
import { TOrgDALFactory } from "@app/services/org/org-dal";
|
import { TOrgDALFactory } from "@app/services/org/org-dal";
|
||||||
|
import { getDefaultOrgMembershipRole } from "@app/services/org/org-role-fns";
|
||||||
import { TOrgMembershipDALFactory } from "@app/services/org-membership/org-membership-dal";
|
import { TOrgMembershipDALFactory } from "@app/services/org-membership/org-membership-dal";
|
||||||
import { SmtpTemplates, TSmtpService } from "@app/services/smtp/smtp-service";
|
import { SmtpTemplates, TSmtpService } from "@app/services/smtp/smtp-service";
|
||||||
import { getServerCfg } from "@app/services/super-admin/super-admin-service";
|
import { getServerCfg } from "@app/services/super-admin/super-admin-service";
|
||||||
@ -77,8 +78,8 @@ export const oidcConfigServiceFactory = ({
|
|||||||
const getOidc = async (dto: TGetOidcCfgDTO) => {
|
const getOidc = async (dto: TGetOidcCfgDTO) => {
|
||||||
const org = await orgDAL.findOne({ slug: dto.orgSlug });
|
const org = await orgDAL.findOne({ slug: dto.orgSlug });
|
||||||
if (!org) {
|
if (!org) {
|
||||||
throw new BadRequestError({
|
throw new NotFoundError({
|
||||||
message: "Organization not found",
|
message: `Organization with slug '${dto.orgSlug}' not found`,
|
||||||
name: "OrgNotFound"
|
name: "OrgNotFound"
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@ -98,15 +99,18 @@ export const oidcConfigServiceFactory = ({
|
|||||||
});
|
});
|
||||||
|
|
||||||
if (!oidcCfg) {
|
if (!oidcCfg) {
|
||||||
throw new BadRequestError({
|
throw new NotFoundError({
|
||||||
message: "Failed to find organization OIDC configuration"
|
message: `OIDC configuration for organization with slug '${dto.orgSlug}' not found`
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// decrypt and return cfg
|
// decrypt and return cfg
|
||||||
const orgBot = await orgBotDAL.findOne({ orgId: oidcCfg.orgId });
|
const orgBot = await orgBotDAL.findOne({ orgId: oidcCfg.orgId });
|
||||||
if (!orgBot) {
|
if (!orgBot) {
|
||||||
throw new BadRequestError({ message: "Org bot not found", name: "OrgBotNotFound" });
|
throw new NotFoundError({
|
||||||
|
message: `Organization bot for organization with ID '${oidcCfg.orgId}' not found`,
|
||||||
|
name: "OrgBotNotFound"
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
const key = infisicalSymmetricDecrypt({
|
const key = infisicalSymmetricDecrypt({
|
||||||
@ -160,7 +164,7 @@ export const oidcConfigServiceFactory = ({
|
|||||||
const serverCfg = await getServerCfg();
|
const serverCfg = await getServerCfg();
|
||||||
|
|
||||||
if (serverCfg.enabledLoginMethods && !serverCfg.enabledLoginMethods.includes(LoginMethod.OIDC)) {
|
if (serverCfg.enabledLoginMethods && !serverCfg.enabledLoginMethods.includes(LoginMethod.OIDC)) {
|
||||||
throw new BadRequestError({
|
throw new ForbiddenRequestError({
|
||||||
message: "Login with OIDC is disabled by administrator."
|
message: "Login with OIDC is disabled by administrator."
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@ -173,7 +177,7 @@ export const oidcConfigServiceFactory = ({
|
|||||||
});
|
});
|
||||||
|
|
||||||
const organization = await orgDAL.findOrgById(orgId);
|
const organization = await orgDAL.findOrgById(orgId);
|
||||||
if (!organization) throw new BadRequestError({ message: "Org not found" });
|
if (!organization) throw new NotFoundError({ message: `Organization with ID '${orgId}' not found` });
|
||||||
|
|
||||||
let user: TUsers;
|
let user: TUsers;
|
||||||
if (userAlias) {
|
if (userAlias) {
|
||||||
@ -187,12 +191,15 @@ export const oidcConfigServiceFactory = ({
|
|||||||
{ tx }
|
{ tx }
|
||||||
);
|
);
|
||||||
if (!orgMembership) {
|
if (!orgMembership) {
|
||||||
|
const { role, roleId } = await getDefaultOrgMembershipRole(organization.defaultMembershipRole);
|
||||||
|
|
||||||
await orgMembershipDAL.create(
|
await orgMembershipDAL.create(
|
||||||
{
|
{
|
||||||
userId: userAlias.userId,
|
userId: userAlias.userId,
|
||||||
inviteEmail: email,
|
inviteEmail: email,
|
||||||
orgId,
|
orgId,
|
||||||
role: OrgMembershipRole.Member,
|
role,
|
||||||
|
roleId,
|
||||||
status: foundUser.isAccepted ? OrgMembershipStatus.Accepted : OrgMembershipStatus.Invited, // if user is fully completed, then set status to accepted, otherwise set it to invited so we can update it later
|
status: foundUser.isAccepted ? OrgMembershipStatus.Accepted : OrgMembershipStatus.Invited, // if user is fully completed, then set status to accepted, otherwise set it to invited so we can update it later
|
||||||
isActive: true
|
isActive: true
|
||||||
},
|
},
|
||||||
@ -261,12 +268,15 @@ export const oidcConfigServiceFactory = ({
|
|||||||
);
|
);
|
||||||
|
|
||||||
if (!orgMembership) {
|
if (!orgMembership) {
|
||||||
|
const { role, roleId } = await getDefaultOrgMembershipRole(organization.defaultMembershipRole);
|
||||||
|
|
||||||
await orgMembershipDAL.create(
|
await orgMembershipDAL.create(
|
||||||
{
|
{
|
||||||
userId: newUser.id,
|
userId: newUser.id,
|
||||||
inviteEmail: email,
|
inviteEmail: email,
|
||||||
orgId,
|
orgId,
|
||||||
role: OrgMembershipRole.Member,
|
role,
|
||||||
|
roleId,
|
||||||
status: newUser.isAccepted ? OrgMembershipStatus.Accepted : OrgMembershipStatus.Invited, // if user is fully completed, then set status to accepted, otherwise set it to invited so we can update it later
|
status: newUser.isAccepted ? OrgMembershipStatus.Accepted : OrgMembershipStatus.Invited, // if user is fully completed, then set status to accepted, otherwise set it to invited so we can update it later
|
||||||
isActive: true
|
isActive: true
|
||||||
},
|
},
|
||||||
@ -314,6 +324,8 @@ export const oidcConfigServiceFactory = ({
|
|||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
|
await oidcConfigDAL.update({ orgId }, { lastUsed: new Date() });
|
||||||
|
|
||||||
if (user.email && !user.isEmailVerified) {
|
if (user.email && !user.isEmailVerified) {
|
||||||
const token = await tokenService.createTokenForUser({
|
const token = await tokenService.createTokenForUser({
|
||||||
type: TokenType.TOKEN_EMAIL_VERIFICATION,
|
type: TokenType.TOKEN_EMAIL_VERIFICATION,
|
||||||
@ -356,8 +368,8 @@ export const oidcConfigServiceFactory = ({
|
|||||||
});
|
});
|
||||||
|
|
||||||
if (!org) {
|
if (!org) {
|
||||||
throw new BadRequestError({
|
throw new NotFoundError({
|
||||||
message: "Organization not found"
|
message: `Organization with slug '${orgSlug}' not found`
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -378,7 +390,11 @@ export const oidcConfigServiceFactory = ({
|
|||||||
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Edit, OrgPermissionSubjects.Sso);
|
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Edit, OrgPermissionSubjects.Sso);
|
||||||
|
|
||||||
const orgBot = await orgBotDAL.findOne({ orgId: org.id });
|
const orgBot = await orgBotDAL.findOne({ orgId: org.id });
|
||||||
if (!orgBot) throw new BadRequestError({ message: "Org bot not found", name: "OrgBotNotFound" });
|
if (!orgBot)
|
||||||
|
throw new NotFoundError({
|
||||||
|
message: `Organization bot for organization with ID '${org.id}' not found`,
|
||||||
|
name: "OrgBotNotFound"
|
||||||
|
});
|
||||||
const key = infisicalSymmetricDecrypt({
|
const key = infisicalSymmetricDecrypt({
|
||||||
ciphertext: orgBot.encryptedSymmetricKey,
|
ciphertext: orgBot.encryptedSymmetricKey,
|
||||||
iv: orgBot.symmetricKeyIV,
|
iv: orgBot.symmetricKeyIV,
|
||||||
@ -395,7 +411,8 @@ export const oidcConfigServiceFactory = ({
|
|||||||
tokenEndpoint,
|
tokenEndpoint,
|
||||||
userinfoEndpoint,
|
userinfoEndpoint,
|
||||||
jwksUri,
|
jwksUri,
|
||||||
isActive
|
isActive,
|
||||||
|
lastUsed: null
|
||||||
};
|
};
|
||||||
|
|
||||||
if (clientId !== undefined) {
|
if (clientId !== undefined) {
|
||||||
@ -418,6 +435,7 @@ export const oidcConfigServiceFactory = ({
|
|||||||
}
|
}
|
||||||
|
|
||||||
const [ssoConfig] = await oidcConfigDAL.update({ orgId: org.id }, updateQuery);
|
const [ssoConfig] = await oidcConfigDAL.update({ orgId: org.id }, updateQuery);
|
||||||
|
await orgDAL.updateById(org.id, { authEnforced: false, scimEnabled: false });
|
||||||
return ssoConfig;
|
return ssoConfig;
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -443,8 +461,8 @@ export const oidcConfigServiceFactory = ({
|
|||||||
slug: orgSlug
|
slug: orgSlug
|
||||||
});
|
});
|
||||||
if (!org) {
|
if (!org) {
|
||||||
throw new BadRequestError({
|
throw new NotFoundError({
|
||||||
message: "Organization not found"
|
message: `Organization with slug '${orgSlug}' not found`
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -549,8 +567,8 @@ export const oidcConfigServiceFactory = ({
|
|||||||
});
|
});
|
||||||
|
|
||||||
if (!org) {
|
if (!org) {
|
||||||
throw new BadRequestError({
|
throw new NotFoundError({
|
||||||
message: "Organization not found."
|
message: `Organization with slug '${orgSlug}' not found`
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -560,7 +578,7 @@ export const oidcConfigServiceFactory = ({
|
|||||||
});
|
});
|
||||||
|
|
||||||
if (!oidcCfg || !oidcCfg.isActive) {
|
if (!oidcCfg || !oidcCfg.isActive) {
|
||||||
throw new BadRequestError({
|
throw new ForbiddenRequestError({
|
||||||
message: "Failed to authenticate with OIDC SSO"
|
message: "Failed to authenticate with OIDC SSO"
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@ -617,7 +635,7 @@ export const oidcConfigServiceFactory = ({
|
|||||||
if (oidcCfg.allowedEmailDomains) {
|
if (oidcCfg.allowedEmailDomains) {
|
||||||
const allowedDomains = oidcCfg.allowedEmailDomains.split(", ");
|
const allowedDomains = oidcCfg.allowedEmailDomains.split(", ");
|
||||||
if (!allowedDomains.includes(claims.email.split("@")[1])) {
|
if (!allowedDomains.includes(claims.email.split("@")[1])) {
|
||||||
throw new BadRequestError({
|
throw new ForbiddenRequestError({
|
||||||
message: "Email not allowed."
|
message: "Email not allowed."
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -1,7 +1,5 @@
|
|||||||
import { AbilityBuilder, createMongoAbility, MongoAbility } from "@casl/ability";
|
import { AbilityBuilder, createMongoAbility, MongoAbility } from "@casl/ability";
|
||||||
|
|
||||||
import { conditionsMatcher } from "@app/lib/casl";
|
|
||||||
|
|
||||||
export enum OrgPermissionActions {
|
export enum OrgPermissionActions {
|
||||||
Read = "read",
|
Read = "read",
|
||||||
Create = "create",
|
Create = "create",
|
||||||
@ -27,7 +25,8 @@ export enum OrgPermissionSubjects {
|
|||||||
SecretScanning = "secret-scanning",
|
SecretScanning = "secret-scanning",
|
||||||
Identity = "identity",
|
Identity = "identity",
|
||||||
Kms = "kms",
|
Kms = "kms",
|
||||||
AdminConsole = "organization-admin-console"
|
AdminConsole = "organization-admin-console",
|
||||||
|
AuditLogs = "audit-logs"
|
||||||
}
|
}
|
||||||
|
|
||||||
export type OrgPermissionSet =
|
export type OrgPermissionSet =
|
||||||
@ -45,10 +44,11 @@ export type OrgPermissionSet =
|
|||||||
| [OrgPermissionActions, OrgPermissionSubjects.Billing]
|
| [OrgPermissionActions, OrgPermissionSubjects.Billing]
|
||||||
| [OrgPermissionActions, OrgPermissionSubjects.Identity]
|
| [OrgPermissionActions, OrgPermissionSubjects.Identity]
|
||||||
| [OrgPermissionActions, OrgPermissionSubjects.Kms]
|
| [OrgPermissionActions, OrgPermissionSubjects.Kms]
|
||||||
|
| [OrgPermissionActions, OrgPermissionSubjects.AuditLogs]
|
||||||
| [OrgPermissionAdminConsoleAction, OrgPermissionSubjects.AdminConsole];
|
| [OrgPermissionAdminConsoleAction, OrgPermissionSubjects.AdminConsole];
|
||||||
|
|
||||||
const buildAdminPermission = () => {
|
const buildAdminPermission = () => {
|
||||||
const { can, build } = new AbilityBuilder<MongoAbility<OrgPermissionSet>>(createMongoAbility);
|
const { can, rules } = new AbilityBuilder<MongoAbility<OrgPermissionSet>>(createMongoAbility);
|
||||||
// ws permissions
|
// ws permissions
|
||||||
can(OrgPermissionActions.Read, OrgPermissionSubjects.Workspace);
|
can(OrgPermissionActions.Read, OrgPermissionSubjects.Workspace);
|
||||||
can(OrgPermissionActions.Create, OrgPermissionSubjects.Workspace);
|
can(OrgPermissionActions.Create, OrgPermissionSubjects.Workspace);
|
||||||
@ -113,15 +113,20 @@ const buildAdminPermission = () => {
|
|||||||
can(OrgPermissionActions.Edit, OrgPermissionSubjects.Kms);
|
can(OrgPermissionActions.Edit, OrgPermissionSubjects.Kms);
|
||||||
can(OrgPermissionActions.Delete, OrgPermissionSubjects.Kms);
|
can(OrgPermissionActions.Delete, OrgPermissionSubjects.Kms);
|
||||||
|
|
||||||
|
can(OrgPermissionActions.Read, OrgPermissionSubjects.AuditLogs);
|
||||||
|
can(OrgPermissionActions.Create, OrgPermissionSubjects.AuditLogs);
|
||||||
|
can(OrgPermissionActions.Edit, OrgPermissionSubjects.AuditLogs);
|
||||||
|
can(OrgPermissionActions.Delete, OrgPermissionSubjects.AuditLogs);
|
||||||
|
|
||||||
can(OrgPermissionAdminConsoleAction.AccessAllProjects, OrgPermissionSubjects.AdminConsole);
|
can(OrgPermissionAdminConsoleAction.AccessAllProjects, OrgPermissionSubjects.AdminConsole);
|
||||||
|
|
||||||
return build({ conditionsMatcher });
|
return rules;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const orgAdminPermissions = buildAdminPermission();
|
export const orgAdminPermissions = buildAdminPermission();
|
||||||
|
|
||||||
const buildMemberPermission = () => {
|
const buildMemberPermission = () => {
|
||||||
const { can, build } = new AbilityBuilder<MongoAbility<OrgPermissionSet>>(createMongoAbility);
|
const { can, rules } = new AbilityBuilder<MongoAbility<OrgPermissionSet>>(createMongoAbility);
|
||||||
|
|
||||||
can(OrgPermissionActions.Read, OrgPermissionSubjects.Workspace);
|
can(OrgPermissionActions.Read, OrgPermissionSubjects.Workspace);
|
||||||
can(OrgPermissionActions.Create, OrgPermissionSubjects.Workspace);
|
can(OrgPermissionActions.Create, OrgPermissionSubjects.Workspace);
|
||||||
@ -142,14 +147,16 @@ const buildMemberPermission = () => {
|
|||||||
can(OrgPermissionActions.Edit, OrgPermissionSubjects.Identity);
|
can(OrgPermissionActions.Edit, OrgPermissionSubjects.Identity);
|
||||||
can(OrgPermissionActions.Delete, OrgPermissionSubjects.Identity);
|
can(OrgPermissionActions.Delete, OrgPermissionSubjects.Identity);
|
||||||
|
|
||||||
return build({ conditionsMatcher });
|
can(OrgPermissionActions.Read, OrgPermissionSubjects.AuditLogs);
|
||||||
|
|
||||||
|
return rules;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const orgMemberPermissions = buildMemberPermission();
|
export const orgMemberPermissions = buildMemberPermission();
|
||||||
|
|
||||||
const buildNoAccessPermission = () => {
|
const buildNoAccessPermission = () => {
|
||||||
const { build } = new AbilityBuilder<MongoAbility<OrgPermissionSet>>(createMongoAbility);
|
const { rules } = new AbilityBuilder<MongoAbility<OrgPermissionSet>>(createMongoAbility);
|
||||||
return build({ conditionsMatcher });
|
return rules;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const orgNoAccessPermissions = buildNoAccessPermission();
|
export const orgNoAccessPermissions = buildNoAccessPermission();
|
||||||
|
@ -1,7 +1,13 @@
|
|||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
|
|
||||||
import { TDbClient } from "@app/db";
|
import { TDbClient } from "@app/db";
|
||||||
import { IdentityProjectMembershipRoleSchema, ProjectUserMembershipRolesSchema, TableName } from "@app/db/schemas";
|
import {
|
||||||
|
IdentityProjectMembershipRoleSchema,
|
||||||
|
OrgMembershipsSchema,
|
||||||
|
TableName,
|
||||||
|
TProjectRoles,
|
||||||
|
TProjects
|
||||||
|
} from "@app/db/schemas";
|
||||||
import { DatabaseError } from "@app/lib/errors";
|
import { DatabaseError } from "@app/lib/errors";
|
||||||
import { selectAllTableCols, sqlNestRelationships } from "@app/lib/knex";
|
import { selectAllTableCols, sqlNestRelationships } from "@app/lib/knex";
|
||||||
|
|
||||||
@ -10,18 +16,92 @@ export type TPermissionDALFactory = ReturnType<typeof permissionDALFactory>;
|
|||||||
export const permissionDALFactory = (db: TDbClient) => {
|
export const permissionDALFactory = (db: TDbClient) => {
|
||||||
const getOrgPermission = async (userId: string, orgId: string) => {
|
const getOrgPermission = async (userId: string, orgId: string) => {
|
||||||
try {
|
try {
|
||||||
|
const groupSubQuery = db(TableName.Groups)
|
||||||
|
.where(`${TableName.Groups}.orgId`, orgId)
|
||||||
|
.join(TableName.UserGroupMembership, (queryBuilder) => {
|
||||||
|
queryBuilder
|
||||||
|
.on(`${TableName.UserGroupMembership}.groupId`, `${TableName.Groups}.id`)
|
||||||
|
.andOn(`${TableName.UserGroupMembership}.userId`, db.raw("?", [userId]));
|
||||||
|
})
|
||||||
|
.leftJoin(TableName.OrgRoles, `${TableName.Groups}.roleId`, `${TableName.OrgRoles}.id`)
|
||||||
|
.select(
|
||||||
|
db.ref("id").withSchema(TableName.Groups).as("groupId"),
|
||||||
|
db.ref("orgId").withSchema(TableName.Groups).as("groupOrgId"),
|
||||||
|
db.ref("name").withSchema(TableName.Groups).as("groupName"),
|
||||||
|
db.ref("slug").withSchema(TableName.Groups).as("groupSlug"),
|
||||||
|
db.ref("role").withSchema(TableName.Groups).as("groupRole"),
|
||||||
|
db.ref("roleId").withSchema(TableName.Groups).as("groupRoleId"),
|
||||||
|
db.ref("createdAt").withSchema(TableName.Groups).as("groupCreatedAt"),
|
||||||
|
db.ref("updatedAt").withSchema(TableName.Groups).as("groupUpdatedAt"),
|
||||||
|
db.ref("permissions").withSchema(TableName.OrgRoles).as("groupCustomRolePermission")
|
||||||
|
);
|
||||||
|
|
||||||
const membership = await db
|
const membership = await db
|
||||||
.replicaNode()(TableName.OrgMembership)
|
.replicaNode()(TableName.OrgMembership)
|
||||||
.leftJoin(TableName.OrgRoles, `${TableName.OrgMembership}.roleId`, `${TableName.OrgRoles}.id`)
|
|
||||||
.join(TableName.Organization, `${TableName.OrgMembership}.orgId`, `${TableName.Organization}.id`)
|
|
||||||
.where("userId", userId)
|
|
||||||
.where(`${TableName.OrgMembership}.orgId`, orgId)
|
.where(`${TableName.OrgMembership}.orgId`, orgId)
|
||||||
.select(db.ref("authEnforced").withSchema(TableName.Organization).as("orgAuthEnforced"))
|
.where(`${TableName.OrgMembership}.userId`, userId)
|
||||||
.select("permissions")
|
.leftJoin(TableName.OrgRoles, `${TableName.OrgRoles}.id`, `${TableName.OrgMembership}.roleId`)
|
||||||
.select(selectAllTableCols(TableName.OrgMembership))
|
.leftJoin<Awaited<typeof groupSubQuery>[0]>(
|
||||||
.first();
|
groupSubQuery.as("userGroups"),
|
||||||
|
"userGroups.groupOrgId",
|
||||||
|
db.raw("?", [orgId])
|
||||||
|
)
|
||||||
|
.join(TableName.Organization, `${TableName.Organization}.id`, `${TableName.OrgMembership}.orgId`)
|
||||||
|
.select(
|
||||||
|
selectAllTableCols(TableName.OrgMembership),
|
||||||
|
db.ref("slug").withSchema(TableName.OrgRoles).withSchema(TableName.OrgRoles).as("customRoleSlug"),
|
||||||
|
db.ref("permissions").withSchema(TableName.OrgRoles),
|
||||||
|
db.ref("authEnforced").withSchema(TableName.Organization).as("orgAuthEnforced"),
|
||||||
|
db.ref("groupId").withSchema("userGroups"),
|
||||||
|
db.ref("groupOrgId").withSchema("userGroups"),
|
||||||
|
db.ref("groupName").withSchema("userGroups"),
|
||||||
|
db.ref("groupSlug").withSchema("userGroups"),
|
||||||
|
db.ref("groupRole").withSchema("userGroups"),
|
||||||
|
db.ref("groupRoleId").withSchema("userGroups"),
|
||||||
|
db.ref("groupCreatedAt").withSchema("userGroups"),
|
||||||
|
db.ref("groupUpdatedAt").withSchema("userGroups"),
|
||||||
|
db.ref("groupCustomRolePermission").withSchema("userGroups")
|
||||||
|
);
|
||||||
|
|
||||||
return membership;
|
const [formatedDoc] = sqlNestRelationships({
|
||||||
|
data: membership,
|
||||||
|
key: "id",
|
||||||
|
parentMapper: (el) =>
|
||||||
|
OrgMembershipsSchema.extend({
|
||||||
|
permissions: z.unknown(),
|
||||||
|
orgAuthEnforced: z.boolean().optional().nullable(),
|
||||||
|
customRoleSlug: z.string().optional().nullable()
|
||||||
|
}).parse(el),
|
||||||
|
childrenMapper: [
|
||||||
|
{
|
||||||
|
key: "groupId",
|
||||||
|
label: "groups" as const,
|
||||||
|
mapper: ({
|
||||||
|
groupId,
|
||||||
|
groupUpdatedAt,
|
||||||
|
groupCreatedAt,
|
||||||
|
groupRole,
|
||||||
|
groupRoleId,
|
||||||
|
groupCustomRolePermission,
|
||||||
|
groupName,
|
||||||
|
groupSlug,
|
||||||
|
groupOrgId
|
||||||
|
}) => ({
|
||||||
|
id: groupId,
|
||||||
|
updatedAt: groupUpdatedAt,
|
||||||
|
createdAt: groupCreatedAt,
|
||||||
|
role: groupRole,
|
||||||
|
roleId: groupRoleId,
|
||||||
|
customRolePermission: groupCustomRolePermission,
|
||||||
|
name: groupName,
|
||||||
|
slug: groupSlug,
|
||||||
|
orgId: groupOrgId
|
||||||
|
})
|
||||||
|
}
|
||||||
|
]
|
||||||
|
});
|
||||||
|
|
||||||
|
return formatedDoc;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
throw new DatabaseError({ error, name: "GetOrgPermission" });
|
throw new DatabaseError({ error, name: "GetOrgPermission" });
|
||||||
}
|
}
|
||||||
@ -47,74 +127,31 @@ export const permissionDALFactory = (db: TDbClient) => {
|
|||||||
|
|
||||||
const getProjectPermission = async (userId: string, projectId: string) => {
|
const getProjectPermission = async (userId: string, projectId: string) => {
|
||||||
try {
|
try {
|
||||||
const groups: string[] = await db
|
const docs = await db
|
||||||
.replicaNode()(TableName.GroupProjectMembership)
|
.replicaNode()(TableName.Users)
|
||||||
.where(`${TableName.GroupProjectMembership}.projectId`, projectId)
|
.where(`${TableName.Users}.id`, userId)
|
||||||
.pluck(`${TableName.GroupProjectMembership}.groupId`);
|
.leftJoin(TableName.UserGroupMembership, `${TableName.UserGroupMembership}.userId`, `${TableName.Users}.id`)
|
||||||
|
.leftJoin(TableName.GroupProjectMembership, (queryBuilder) => {
|
||||||
const groupDocs = await db
|
void queryBuilder
|
||||||
.replicaNode()(TableName.UserGroupMembership)
|
.on(`${TableName.GroupProjectMembership}.projectId`, db.raw("?", [projectId]))
|
||||||
.where(`${TableName.UserGroupMembership}.userId`, userId)
|
.andOn(`${TableName.GroupProjectMembership}.groupId`, `${TableName.UserGroupMembership}.groupId`);
|
||||||
.whereIn(`${TableName.UserGroupMembership}.groupId`, groups)
|
})
|
||||||
.join(
|
.leftJoin(
|
||||||
TableName.GroupProjectMembership,
|
|
||||||
`${TableName.GroupProjectMembership}.groupId`,
|
|
||||||
`${TableName.UserGroupMembership}.groupId`
|
|
||||||
)
|
|
||||||
.join(
|
|
||||||
TableName.GroupProjectMembershipRole,
|
TableName.GroupProjectMembershipRole,
|
||||||
`${TableName.GroupProjectMembershipRole}.projectMembershipId`,
|
`${TableName.GroupProjectMembershipRole}.projectMembershipId`,
|
||||||
`${TableName.GroupProjectMembership}.id`
|
`${TableName.GroupProjectMembership}.id`
|
||||||
)
|
)
|
||||||
|
.leftJoin<TProjectRoles>(
|
||||||
.leftJoin(
|
{ groupCustomRoles: TableName.ProjectRoles },
|
||||||
TableName.ProjectRoles,
|
|
||||||
`${TableName.GroupProjectMembershipRole}.customRoleId`,
|
`${TableName.GroupProjectMembershipRole}.customRoleId`,
|
||||||
`${TableName.ProjectRoles}.id`
|
`groupCustomRoles.id`
|
||||||
)
|
)
|
||||||
.join(TableName.Project, `${TableName.GroupProjectMembership}.projectId`, `${TableName.Project}.id`)
|
.leftJoin(TableName.ProjectMembership, (queryBuilder) => {
|
||||||
.join(TableName.Organization, `${TableName.Project}.orgId`, `${TableName.Organization}.id`)
|
void queryBuilder
|
||||||
|
.on(`${TableName.ProjectMembership}.projectId`, db.raw("?", [projectId]))
|
||||||
|
.andOn(`${TableName.ProjectMembership}.userId`, `${TableName.Users}.id`);
|
||||||
|
})
|
||||||
.leftJoin(
|
.leftJoin(
|
||||||
TableName.ProjectUserAdditionalPrivilege,
|
|
||||||
`${TableName.GroupProjectMembership}.projectId`,
|
|
||||||
`${TableName.Project}.id`
|
|
||||||
)
|
|
||||||
.select(selectAllTableCols(TableName.GroupProjectMembershipRole))
|
|
||||||
.select(
|
|
||||||
db.ref("id").withSchema(TableName.GroupProjectMembership).as("membershipId"),
|
|
||||||
db.ref("createdAt").withSchema(TableName.GroupProjectMembership).as("membershipCreatedAt"),
|
|
||||||
db.ref("updatedAt").withSchema(TableName.GroupProjectMembership).as("membershipUpdatedAt"),
|
|
||||||
db.ref("projectId").withSchema(TableName.GroupProjectMembership),
|
|
||||||
db.ref("authEnforced").withSchema(TableName.Organization).as("orgAuthEnforced"),
|
|
||||||
db.ref("orgId").withSchema(TableName.Project),
|
|
||||||
db.ref("slug").withSchema(TableName.ProjectRoles).as("customRoleSlug"),
|
|
||||||
|
|
||||||
db.ref("permissions").withSchema(TableName.ProjectRoles).as("permissions"),
|
|
||||||
// db.ref("permissions").withSchema(TableName.ProjectUserAdditionalPrivilege).as("apPermissions")
|
|
||||||
// Additional Privileges
|
|
||||||
db.ref("id").withSchema(TableName.ProjectUserAdditionalPrivilege).as("userApId"),
|
|
||||||
db.ref("permissions").withSchema(TableName.ProjectUserAdditionalPrivilege).as("userApPermissions"),
|
|
||||||
db.ref("temporaryMode").withSchema(TableName.ProjectUserAdditionalPrivilege).as("userApTemporaryMode"),
|
|
||||||
db.ref("isTemporary").withSchema(TableName.ProjectUserAdditionalPrivilege).as("userApIsTemporary"),
|
|
||||||
db.ref("temporaryRange").withSchema(TableName.ProjectUserAdditionalPrivilege).as("userApTemporaryRange"),
|
|
||||||
|
|
||||||
db.ref("projectId").withSchema(TableName.ProjectUserAdditionalPrivilege).as("userApProjectId"),
|
|
||||||
db.ref("userId").withSchema(TableName.ProjectUserAdditionalPrivilege).as("userApUserId"),
|
|
||||||
|
|
||||||
db
|
|
||||||
.ref("temporaryAccessStartTime")
|
|
||||||
.withSchema(TableName.ProjectUserAdditionalPrivilege)
|
|
||||||
.as("userApTemporaryAccessStartTime"),
|
|
||||||
db
|
|
||||||
.ref("temporaryAccessEndTime")
|
|
||||||
.withSchema(TableName.ProjectUserAdditionalPrivilege)
|
|
||||||
.as("userApTemporaryAccessEndTime")
|
|
||||||
);
|
|
||||||
// .select(`${TableName.ProjectRoles}.permissions`);
|
|
||||||
|
|
||||||
const docs = await db(TableName.ProjectMembership)
|
|
||||||
.join(
|
|
||||||
TableName.ProjectUserMembershipRole,
|
TableName.ProjectUserMembershipRole,
|
||||||
`${TableName.ProjectUserMembershipRole}.projectMembershipId`,
|
`${TableName.ProjectUserMembershipRole}.projectMembershipId`,
|
||||||
`${TableName.ProjectMembership}.id`
|
`${TableName.ProjectMembership}.id`
|
||||||
@ -124,176 +161,249 @@ export const permissionDALFactory = (db: TDbClient) => {
|
|||||||
`${TableName.ProjectUserMembershipRole}.customRoleId`,
|
`${TableName.ProjectUserMembershipRole}.customRoleId`,
|
||||||
`${TableName.ProjectRoles}.id`
|
`${TableName.ProjectRoles}.id`
|
||||||
)
|
)
|
||||||
.leftJoin(
|
.leftJoin(TableName.ProjectUserAdditionalPrivilege, (queryBuilder) => {
|
||||||
TableName.ProjectUserAdditionalPrivilege,
|
void queryBuilder
|
||||||
`${TableName.ProjectUserAdditionalPrivilege}.projectId`,
|
.on(`${TableName.ProjectUserAdditionalPrivilege}.projectId`, db.raw("?", [projectId]))
|
||||||
`${TableName.ProjectMembership}.projectId`
|
.andOn(`${TableName.ProjectUserAdditionalPrivilege}.userId`, `${TableName.Users}.id`);
|
||||||
)
|
})
|
||||||
|
.join<TProjects>(TableName.Project, `${TableName.Project}.id`, db.raw("?", [projectId]))
|
||||||
.join(TableName.Project, `${TableName.ProjectMembership}.projectId`, `${TableName.Project}.id`)
|
|
||||||
.join(TableName.Organization, `${TableName.Project}.orgId`, `${TableName.Organization}.id`)
|
.join(TableName.Organization, `${TableName.Project}.orgId`, `${TableName.Organization}.id`)
|
||||||
.where(`${TableName.ProjectMembership}.userId`, userId)
|
.leftJoin(TableName.IdentityMetadata, (queryBuilder) => {
|
||||||
.where(`${TableName.ProjectMembership}.projectId`, projectId)
|
void queryBuilder
|
||||||
.select(selectAllTableCols(TableName.ProjectUserMembershipRole))
|
.on(`${TableName.Users}.id`, `${TableName.IdentityMetadata}.userId`)
|
||||||
|
.andOn(`${TableName.Organization}.id`, `${TableName.IdentityMetadata}.orgId`);
|
||||||
|
})
|
||||||
.select(
|
.select(
|
||||||
|
db.ref("id").withSchema(TableName.Users).as("userId"),
|
||||||
|
db.ref("username").withSchema(TableName.Users).as("username"),
|
||||||
|
// groups specific
|
||||||
|
db.ref("id").withSchema(TableName.GroupProjectMembership).as("groupMembershipId"),
|
||||||
|
db.ref("createdAt").withSchema(TableName.GroupProjectMembership).as("groupMembershipCreatedAt"),
|
||||||
|
db.ref("updatedAt").withSchema(TableName.GroupProjectMembership).as("groupMembershipUpdatedAt"),
|
||||||
|
db.ref("slug").withSchema("groupCustomRoles").as("userGroupProjectMembershipRoleCustomRoleSlug"),
|
||||||
|
db.ref("permissions").withSchema("groupCustomRoles").as("userGroupProjectMembershipRolePermission"),
|
||||||
|
db.ref("id").withSchema(TableName.GroupProjectMembershipRole).as("userGroupProjectMembershipRoleId"),
|
||||||
|
db.ref("role").withSchema(TableName.GroupProjectMembershipRole).as("userGroupProjectMembershipRole"),
|
||||||
|
db
|
||||||
|
.ref("customRoleId")
|
||||||
|
.withSchema(TableName.GroupProjectMembershipRole)
|
||||||
|
.as("userGroupProjectMembershipRoleCustomRoleId"),
|
||||||
|
db
|
||||||
|
.ref("isTemporary")
|
||||||
|
.withSchema(TableName.GroupProjectMembershipRole)
|
||||||
|
.as("userGroupProjectMembershipRoleIsTemporary"),
|
||||||
|
db
|
||||||
|
.ref("temporaryMode")
|
||||||
|
.withSchema(TableName.GroupProjectMembershipRole)
|
||||||
|
.as("userGroupProjectMembershipRoleTemporaryMode"),
|
||||||
|
db
|
||||||
|
.ref("temporaryRange")
|
||||||
|
.withSchema(TableName.GroupProjectMembershipRole)
|
||||||
|
.as("userGroupProjectMembershipRoleTemporaryRange"),
|
||||||
|
db
|
||||||
|
.ref("temporaryAccessStartTime")
|
||||||
|
.withSchema(TableName.GroupProjectMembershipRole)
|
||||||
|
.as("userGroupProjectMembershipRoleTemporaryAccessStartTime"),
|
||||||
|
db
|
||||||
|
.ref("temporaryAccessEndTime")
|
||||||
|
.withSchema(TableName.GroupProjectMembershipRole)
|
||||||
|
.as("userGroupProjectMembershipRoleTemporaryAccessEndTime"),
|
||||||
|
// user specific
|
||||||
db.ref("id").withSchema(TableName.ProjectMembership).as("membershipId"),
|
db.ref("id").withSchema(TableName.ProjectMembership).as("membershipId"),
|
||||||
db.ref("createdAt").withSchema(TableName.ProjectMembership).as("membershipCreatedAt"),
|
db.ref("createdAt").withSchema(TableName.ProjectMembership).as("membershipCreatedAt"),
|
||||||
db.ref("updatedAt").withSchema(TableName.ProjectMembership).as("membershipUpdatedAt"),
|
db.ref("updatedAt").withSchema(TableName.ProjectMembership).as("membershipUpdatedAt"),
|
||||||
db.ref("projectId").withSchema(TableName.ProjectMembership),
|
db.ref("slug").withSchema(TableName.ProjectRoles).as("userProjectMembershipRoleCustomRoleSlug"),
|
||||||
db.ref("authEnforced").withSchema(TableName.Organization).as("orgAuthEnforced"),
|
db.ref("permissions").withSchema(TableName.ProjectRoles).as("userProjectCustomRolePermission"),
|
||||||
db.ref("orgId").withSchema(TableName.Project),
|
db.ref("id").withSchema(TableName.ProjectUserMembershipRole).as("userProjectMembershipRoleId"),
|
||||||
db.ref("slug").withSchema(TableName.ProjectRoles).as("customRoleSlug"),
|
db.ref("role").withSchema(TableName.ProjectUserMembershipRole).as("userProjectMembershipRole"),
|
||||||
db.ref("permissions").withSchema(TableName.ProjectRoles),
|
db
|
||||||
db.ref("id").withSchema(TableName.ProjectUserAdditionalPrivilege).as("userApId"),
|
.ref("temporaryMode")
|
||||||
db.ref("permissions").withSchema(TableName.ProjectUserAdditionalPrivilege).as("userApPermissions"),
|
.withSchema(TableName.ProjectUserMembershipRole)
|
||||||
db.ref("temporaryMode").withSchema(TableName.ProjectUserAdditionalPrivilege).as("userApTemporaryMode"),
|
.as("userProjectMembershipRoleTemporaryMode"),
|
||||||
db.ref("isTemporary").withSchema(TableName.ProjectUserAdditionalPrivilege).as("userApIsTemporary"),
|
db
|
||||||
db.ref("temporaryRange").withSchema(TableName.ProjectUserAdditionalPrivilege).as("userApTemporaryRange"),
|
.ref("isTemporary")
|
||||||
|
.withSchema(TableName.ProjectUserMembershipRole)
|
||||||
db.ref("projectId").withSchema(TableName.ProjectUserAdditionalPrivilege).as("userApProjectId"),
|
.as("userProjectMembershipRoleIsTemporary"),
|
||||||
db.ref("userId").withSchema(TableName.ProjectUserAdditionalPrivilege).as("userApUserId"),
|
db
|
||||||
|
.ref("temporaryRange")
|
||||||
|
.withSchema(TableName.ProjectUserMembershipRole)
|
||||||
|
.as("userProjectMembershipRoleTemporaryRange"),
|
||||||
|
db
|
||||||
|
.ref("temporaryAccessStartTime")
|
||||||
|
.withSchema(TableName.ProjectUserMembershipRole)
|
||||||
|
.as("userProjectMembershipRoleTemporaryAccessStartTime"),
|
||||||
|
db
|
||||||
|
.ref("temporaryAccessEndTime")
|
||||||
|
.withSchema(TableName.ProjectUserMembershipRole)
|
||||||
|
.as("userProjectMembershipRoleTemporaryAccessEndTime"),
|
||||||
|
db.ref("id").withSchema(TableName.ProjectUserAdditionalPrivilege).as("userAdditionalPrivilegesId"),
|
||||||
|
db
|
||||||
|
.ref("permissions")
|
||||||
|
.withSchema(TableName.ProjectUserAdditionalPrivilege)
|
||||||
|
.as("userAdditionalPrivilegesPermissions"),
|
||||||
|
db
|
||||||
|
.ref("temporaryMode")
|
||||||
|
.withSchema(TableName.ProjectUserAdditionalPrivilege)
|
||||||
|
.as("userAdditionalPrivilegesTemporaryMode"),
|
||||||
|
db
|
||||||
|
.ref("isTemporary")
|
||||||
|
.withSchema(TableName.ProjectUserAdditionalPrivilege)
|
||||||
|
.as("userAdditionalPrivilegesIsTemporary"),
|
||||||
|
db
|
||||||
|
.ref("temporaryRange")
|
||||||
|
.withSchema(TableName.ProjectUserAdditionalPrivilege)
|
||||||
|
.as("userAdditionalPrivilegesTemporaryRange"),
|
||||||
|
db.ref("userId").withSchema(TableName.ProjectUserAdditionalPrivilege).as("userAdditionalPrivilegesUserId"),
|
||||||
db
|
db
|
||||||
.ref("temporaryAccessStartTime")
|
.ref("temporaryAccessStartTime")
|
||||||
.withSchema(TableName.ProjectUserAdditionalPrivilege)
|
.withSchema(TableName.ProjectUserAdditionalPrivilege)
|
||||||
.as("userApTemporaryAccessStartTime"),
|
.as("userAdditionalPrivilegesTemporaryAccessStartTime"),
|
||||||
db
|
db
|
||||||
.ref("temporaryAccessEndTime")
|
.ref("temporaryAccessEndTime")
|
||||||
.withSchema(TableName.ProjectUserAdditionalPrivilege)
|
.withSchema(TableName.ProjectUserAdditionalPrivilege)
|
||||||
.as("userApTemporaryAccessEndTime")
|
.as("userAdditionalPrivilegesTemporaryAccessEndTime"),
|
||||||
|
// general
|
||||||
|
db.ref("id").withSchema(TableName.IdentityMetadata).as("metadataId"),
|
||||||
|
db.ref("key").withSchema(TableName.IdentityMetadata).as("metadataKey"),
|
||||||
|
db.ref("value").withSchema(TableName.IdentityMetadata).as("metadataValue"),
|
||||||
|
db.ref("authEnforced").withSchema(TableName.Organization).as("orgAuthEnforced"),
|
||||||
|
db.ref("orgId").withSchema(TableName.Project),
|
||||||
|
db.ref("id").withSchema(TableName.Project).as("projectId")
|
||||||
);
|
);
|
||||||
|
|
||||||
const permission = sqlNestRelationships({
|
const [userPermission] = sqlNestRelationships({
|
||||||
data: docs,
|
data: docs,
|
||||||
key: "projectId",
|
key: "projectId",
|
||||||
parentMapper: ({ orgId, orgAuthEnforced, membershipId, membershipCreatedAt, membershipUpdatedAt }) => ({
|
parentMapper: ({
|
||||||
|
orgId,
|
||||||
|
username,
|
||||||
|
orgAuthEnforced,
|
||||||
|
membershipId,
|
||||||
|
groupMembershipId,
|
||||||
|
membershipCreatedAt,
|
||||||
|
groupMembershipCreatedAt,
|
||||||
|
groupMembershipUpdatedAt,
|
||||||
|
membershipUpdatedAt
|
||||||
|
}) => ({
|
||||||
orgId,
|
orgId,
|
||||||
orgAuthEnforced,
|
orgAuthEnforced,
|
||||||
userId,
|
userId,
|
||||||
id: membershipId,
|
|
||||||
projectId,
|
projectId,
|
||||||
createdAt: membershipCreatedAt,
|
username,
|
||||||
updatedAt: membershipUpdatedAt
|
id: membershipId || groupMembershipId,
|
||||||
|
createdAt: membershipCreatedAt || groupMembershipCreatedAt,
|
||||||
|
updatedAt: membershipUpdatedAt || groupMembershipUpdatedAt
|
||||||
}),
|
}),
|
||||||
childrenMapper: [
|
childrenMapper: [
|
||||||
{
|
{
|
||||||
key: "id",
|
key: "userGroupProjectMembershipRoleId",
|
||||||
label: "roles" as const,
|
label: "userGroupRoles" as const,
|
||||||
mapper: (data) =>
|
mapper: ({
|
||||||
ProjectUserMembershipRolesSchema.extend({
|
userGroupProjectMembershipRoleId,
|
||||||
permissions: z.unknown(),
|
userGroupProjectMembershipRole,
|
||||||
customRoleSlug: z.string().optional().nullable()
|
userGroupProjectMembershipRolePermission,
|
||||||
}).parse(data)
|
userGroupProjectMembershipRoleCustomRoleSlug,
|
||||||
|
userGroupProjectMembershipRoleIsTemporary,
|
||||||
|
userGroupProjectMembershipRoleTemporaryMode,
|
||||||
|
userGroupProjectMembershipRoleTemporaryAccessEndTime,
|
||||||
|
userGroupProjectMembershipRoleTemporaryAccessStartTime,
|
||||||
|
userGroupProjectMembershipRoleTemporaryRange
|
||||||
|
}) => ({
|
||||||
|
id: userGroupProjectMembershipRoleId,
|
||||||
|
role: userGroupProjectMembershipRole,
|
||||||
|
customRoleSlug: userGroupProjectMembershipRoleCustomRoleSlug,
|
||||||
|
permissions: userGroupProjectMembershipRolePermission,
|
||||||
|
temporaryRange: userGroupProjectMembershipRoleTemporaryRange,
|
||||||
|
temporaryMode: userGroupProjectMembershipRoleTemporaryMode,
|
||||||
|
temporaryAccessStartTime: userGroupProjectMembershipRoleTemporaryAccessStartTime,
|
||||||
|
temporaryAccessEndTime: userGroupProjectMembershipRoleTemporaryAccessEndTime,
|
||||||
|
isTemporary: userGroupProjectMembershipRoleIsTemporary
|
||||||
|
})
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: "userApId",
|
key: "userProjectMembershipRoleId",
|
||||||
|
label: "projecMembershiptRoles" as const,
|
||||||
|
mapper: ({
|
||||||
|
userProjectMembershipRoleId,
|
||||||
|
userProjectMembershipRole,
|
||||||
|
userProjectCustomRolePermission,
|
||||||
|
userProjectMembershipRoleIsTemporary,
|
||||||
|
userProjectMembershipRoleTemporaryMode,
|
||||||
|
userProjectMembershipRoleTemporaryRange,
|
||||||
|
userProjectMembershipRoleTemporaryAccessEndTime,
|
||||||
|
userProjectMembershipRoleTemporaryAccessStartTime,
|
||||||
|
userProjectMembershipRoleCustomRoleSlug
|
||||||
|
}) => ({
|
||||||
|
id: userProjectMembershipRoleId,
|
||||||
|
role: userProjectMembershipRole,
|
||||||
|
customRoleSlug: userProjectMembershipRoleCustomRoleSlug,
|
||||||
|
permissions: userProjectCustomRolePermission,
|
||||||
|
temporaryRange: userProjectMembershipRoleTemporaryRange,
|
||||||
|
temporaryMode: userProjectMembershipRoleTemporaryMode,
|
||||||
|
temporaryAccessStartTime: userProjectMembershipRoleTemporaryAccessStartTime,
|
||||||
|
temporaryAccessEndTime: userProjectMembershipRoleTemporaryAccessEndTime,
|
||||||
|
isTemporary: userProjectMembershipRoleIsTemporary
|
||||||
|
})
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "userAdditionalPrivilegesId",
|
||||||
label: "additionalPrivileges" as const,
|
label: "additionalPrivileges" as const,
|
||||||
mapper: ({
|
mapper: ({
|
||||||
userApId,
|
userAdditionalPrivilegesId,
|
||||||
userApPermissions,
|
userAdditionalPrivilegesPermissions,
|
||||||
userApIsTemporary,
|
userAdditionalPrivilegesIsTemporary,
|
||||||
userApTemporaryMode,
|
userAdditionalPrivilegesTemporaryMode,
|
||||||
userApTemporaryRange,
|
userAdditionalPrivilegesTemporaryRange,
|
||||||
userApTemporaryAccessEndTime,
|
userAdditionalPrivilegesTemporaryAccessEndTime,
|
||||||
userApTemporaryAccessStartTime
|
userAdditionalPrivilegesTemporaryAccessStartTime
|
||||||
}) => ({
|
}) => ({
|
||||||
id: userApId,
|
id: userAdditionalPrivilegesId,
|
||||||
permissions: userApPermissions,
|
permissions: userAdditionalPrivilegesPermissions,
|
||||||
temporaryRange: userApTemporaryRange,
|
temporaryRange: userAdditionalPrivilegesTemporaryRange,
|
||||||
temporaryMode: userApTemporaryMode,
|
temporaryMode: userAdditionalPrivilegesTemporaryMode,
|
||||||
temporaryAccessEndTime: userApTemporaryAccessEndTime,
|
temporaryAccessStartTime: userAdditionalPrivilegesTemporaryAccessStartTime,
|
||||||
temporaryAccessStartTime: userApTemporaryAccessStartTime,
|
temporaryAccessEndTime: userAdditionalPrivilegesTemporaryAccessEndTime,
|
||||||
isTemporary: userApIsTemporary
|
isTemporary: userAdditionalPrivilegesIsTemporary
|
||||||
|
})
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "metadataId",
|
||||||
|
label: "metadata" as const,
|
||||||
|
mapper: ({ metadataKey, metadataValue, metadataId }) => ({
|
||||||
|
id: metadataId,
|
||||||
|
key: metadataKey,
|
||||||
|
value: metadataValue
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
});
|
});
|
||||||
|
|
||||||
const groupPermission = groupDocs.length
|
if (!userPermission) return undefined;
|
||||||
? sqlNestRelationships({
|
if (!userPermission?.userGroupRoles?.[0] && !userPermission?.projecMembershiptRoles?.[0]) return undefined;
|
||||||
data: groupDocs,
|
|
||||||
key: "projectId",
|
|
||||||
parentMapper: ({ orgId, orgAuthEnforced, membershipId, membershipCreatedAt, membershipUpdatedAt }) => ({
|
|
||||||
orgId,
|
|
||||||
orgAuthEnforced,
|
|
||||||
userId,
|
|
||||||
id: membershipId,
|
|
||||||
projectId,
|
|
||||||
createdAt: membershipCreatedAt,
|
|
||||||
updatedAt: membershipUpdatedAt
|
|
||||||
}),
|
|
||||||
childrenMapper: [
|
|
||||||
{
|
|
||||||
key: "id",
|
|
||||||
label: "roles" as const,
|
|
||||||
mapper: (data) =>
|
|
||||||
ProjectUserMembershipRolesSchema.extend({
|
|
||||||
permissions: z.unknown(),
|
|
||||||
customRoleSlug: z.string().optional().nullable()
|
|
||||||
}).parse(data)
|
|
||||||
},
|
|
||||||
{
|
|
||||||
key: "userApId",
|
|
||||||
label: "additionalPrivileges" as const,
|
|
||||||
mapper: ({
|
|
||||||
userApId,
|
|
||||||
userApProjectId,
|
|
||||||
userApUserId,
|
|
||||||
userApPermissions,
|
|
||||||
userApIsTemporary,
|
|
||||||
userApTemporaryMode,
|
|
||||||
userApTemporaryRange,
|
|
||||||
userApTemporaryAccessEndTime,
|
|
||||||
userApTemporaryAccessStartTime
|
|
||||||
}) => ({
|
|
||||||
id: userApId,
|
|
||||||
userId: userApUserId,
|
|
||||||
projectId: userApProjectId,
|
|
||||||
permissions: userApPermissions,
|
|
||||||
temporaryRange: userApTemporaryRange,
|
|
||||||
temporaryMode: userApTemporaryMode,
|
|
||||||
temporaryAccessEndTime: userApTemporaryAccessEndTime,
|
|
||||||
temporaryAccessStartTime: userApTemporaryAccessStartTime,
|
|
||||||
isTemporary: userApIsTemporary
|
|
||||||
})
|
|
||||||
}
|
|
||||||
]
|
|
||||||
})
|
|
||||||
: [];
|
|
||||||
|
|
||||||
if (!permission?.[0] && !groupPermission[0]) return undefined;
|
|
||||||
|
|
||||||
// when introducting cron mode change it here
|
// when introducting cron mode change it here
|
||||||
const activeRoles =
|
const activeRoles =
|
||||||
permission?.[0]?.roles?.filter(
|
userPermission?.projecMembershiptRoles?.filter(
|
||||||
({ isTemporary, temporaryAccessEndTime }) =>
|
({ isTemporary, temporaryAccessEndTime }) =>
|
||||||
!isTemporary || (isTemporary && temporaryAccessEndTime && new Date() < temporaryAccessEndTime)
|
!isTemporary || (isTemporary && temporaryAccessEndTime && new Date() < temporaryAccessEndTime)
|
||||||
) ?? [];
|
) ?? [];
|
||||||
|
|
||||||
const activeGroupRoles =
|
const activeGroupRoles =
|
||||||
groupPermission?.[0]?.roles?.filter(
|
userPermission?.userGroupRoles?.filter(
|
||||||
({ isTemporary, temporaryAccessEndTime }) =>
|
({ isTemporary, temporaryAccessEndTime }) =>
|
||||||
!isTemporary || (isTemporary && temporaryAccessEndTime && new Date() < temporaryAccessEndTime)
|
!isTemporary || (isTemporary && temporaryAccessEndTime && new Date() < temporaryAccessEndTime)
|
||||||
) ?? [];
|
) ?? [];
|
||||||
|
|
||||||
const activeAdditionalPrivileges =
|
const activeAdditionalPrivileges =
|
||||||
permission?.[0]?.additionalPrivileges?.filter(
|
userPermission?.additionalPrivileges?.filter(
|
||||||
({ isTemporary, temporaryAccessEndTime }) =>
|
({ isTemporary, temporaryAccessEndTime }) =>
|
||||||
!isTemporary || (isTemporary && temporaryAccessEndTime && new Date() < temporaryAccessEndTime)
|
!isTemporary || (isTemporary && temporaryAccessEndTime && new Date() < temporaryAccessEndTime)
|
||||||
) ?? [];
|
) ?? [];
|
||||||
|
|
||||||
const activeGroupAdditionalPrivileges =
|
|
||||||
groupPermission?.[0]?.additionalPrivileges?.filter(
|
|
||||||
({ isTemporary, temporaryAccessEndTime, userId: apUserId, projectId: apProjectId }) =>
|
|
||||||
apProjectId === projectId &&
|
|
||||||
apUserId === userId &&
|
|
||||||
(!isTemporary || (isTemporary && temporaryAccessEndTime && new Date() < temporaryAccessEndTime))
|
|
||||||
) ?? [];
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
...(permission[0] || groupPermission[0]),
|
...userPermission,
|
||||||
roles: [...activeRoles, ...activeGroupRoles],
|
roles: [...activeRoles, ...activeGroupRoles],
|
||||||
additionalPrivileges: [...activeAdditionalPrivileges, ...activeGroupAdditionalPrivileges]
|
additionalPrivileges: activeAdditionalPrivileges
|
||||||
};
|
};
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
throw new DatabaseError({ error, name: "GetProjectPermission" });
|
throw new DatabaseError({ error, name: "GetProjectPermission" });
|
||||||
@ -309,6 +419,7 @@ export const permissionDALFactory = (db: TDbClient) => {
|
|||||||
`${TableName.IdentityProjectMembershipRole}.projectMembershipId`,
|
`${TableName.IdentityProjectMembershipRole}.projectMembershipId`,
|
||||||
`${TableName.IdentityProjectMembership}.id`
|
`${TableName.IdentityProjectMembership}.id`
|
||||||
)
|
)
|
||||||
|
.join(TableName.Identity, `${TableName.Identity}.id`, `${TableName.IdentityProjectMembership}.identityId`)
|
||||||
.leftJoin(
|
.leftJoin(
|
||||||
TableName.ProjectRoles,
|
TableName.ProjectRoles,
|
||||||
`${TableName.IdentityProjectMembershipRole}.customRoleId`,
|
`${TableName.IdentityProjectMembershipRole}.customRoleId`,
|
||||||
@ -325,11 +436,17 @@ export const permissionDALFactory = (db: TDbClient) => {
|
|||||||
`${TableName.IdentityProjectMembership}.projectId`,
|
`${TableName.IdentityProjectMembership}.projectId`,
|
||||||
`${TableName.Project}.id`
|
`${TableName.Project}.id`
|
||||||
)
|
)
|
||||||
.where("identityId", identityId)
|
.leftJoin(TableName.IdentityMetadata, (queryBuilder) => {
|
||||||
|
void queryBuilder
|
||||||
|
.on(`${TableName.Identity}.id`, `${TableName.IdentityMetadata}.identityId`)
|
||||||
|
.andOn(`${TableName.Project}.orgId`, `${TableName.IdentityMetadata}.orgId`);
|
||||||
|
})
|
||||||
|
.where(`${TableName.IdentityProjectMembership}.identityId`, identityId)
|
||||||
.where(`${TableName.IdentityProjectMembership}.projectId`, projectId)
|
.where(`${TableName.IdentityProjectMembership}.projectId`, projectId)
|
||||||
.select(selectAllTableCols(TableName.IdentityProjectMembershipRole))
|
.select(selectAllTableCols(TableName.IdentityProjectMembershipRole))
|
||||||
.select(
|
.select(
|
||||||
db.ref("id").withSchema(TableName.IdentityProjectMembership).as("membershipId"),
|
db.ref("id").withSchema(TableName.IdentityProjectMembership).as("membershipId"),
|
||||||
|
db.ref("name").withSchema(TableName.Identity).as("identityName"),
|
||||||
db.ref("orgId").withSchema(TableName.Project).as("orgId"), // Now you can select orgId from Project
|
db.ref("orgId").withSchema(TableName.Project).as("orgId"), // Now you can select orgId from Project
|
||||||
db.ref("createdAt").withSchema(TableName.IdentityProjectMembership).as("membershipCreatedAt"),
|
db.ref("createdAt").withSchema(TableName.IdentityProjectMembership).as("membershipCreatedAt"),
|
||||||
db.ref("updatedAt").withSchema(TableName.IdentityProjectMembership).as("membershipUpdatedAt"),
|
db.ref("updatedAt").withSchema(TableName.IdentityProjectMembership).as("membershipUpdatedAt"),
|
||||||
@ -353,15 +470,19 @@ export const permissionDALFactory = (db: TDbClient) => {
|
|||||||
db
|
db
|
||||||
.ref("temporaryAccessEndTime")
|
.ref("temporaryAccessEndTime")
|
||||||
.withSchema(TableName.IdentityProjectAdditionalPrivilege)
|
.withSchema(TableName.IdentityProjectAdditionalPrivilege)
|
||||||
.as("identityApTemporaryAccessEndTime")
|
.as("identityApTemporaryAccessEndTime"),
|
||||||
|
db.ref("id").withSchema(TableName.IdentityMetadata).as("metadataId"),
|
||||||
|
db.ref("key").withSchema(TableName.IdentityMetadata).as("metadataKey"),
|
||||||
|
db.ref("value").withSchema(TableName.IdentityMetadata).as("metadataValue")
|
||||||
);
|
);
|
||||||
|
|
||||||
const permission = sqlNestRelationships({
|
const permission = sqlNestRelationships({
|
||||||
data: docs,
|
data: docs,
|
||||||
key: "membershipId",
|
key: "membershipId",
|
||||||
parentMapper: ({ membershipId, membershipCreatedAt, membershipUpdatedAt, orgId }) => ({
|
parentMapper: ({ membershipId, membershipCreatedAt, membershipUpdatedAt, orgId, identityName }) => ({
|
||||||
id: membershipId,
|
id: membershipId,
|
||||||
identityId,
|
identityId,
|
||||||
|
username: identityName,
|
||||||
projectId,
|
projectId,
|
||||||
createdAt: membershipCreatedAt,
|
createdAt: membershipCreatedAt,
|
||||||
updatedAt: membershipUpdatedAt,
|
updatedAt: membershipUpdatedAt,
|
||||||
@ -399,6 +520,15 @@ export const permissionDALFactory = (db: TDbClient) => {
|
|||||||
temporaryAccessStartTime: identityApTemporaryAccessStartTime,
|
temporaryAccessStartTime: identityApTemporaryAccessStartTime,
|
||||||
isTemporary: identityApIsTemporary
|
isTemporary: identityApIsTemporary
|
||||||
})
|
})
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "metadataId",
|
||||||
|
label: "metadata" as const,
|
||||||
|
mapper: ({ metadataKey, metadataValue, metadataId }) => ({
|
||||||
|
id: metadataId,
|
||||||
|
key: metadataKey,
|
||||||
|
value: metadataValue
|
||||||
|
})
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
});
|
});
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
import { TOrganizations } from "@app/db/schemas";
|
import { TOrganizations } from "@app/db/schemas";
|
||||||
import { UnauthorizedError } from "@app/lib/errors";
|
import { ForbiddenRequestError, UnauthorizedError } from "@app/lib/errors";
|
||||||
import { ActorAuthMethod, AuthMethod } from "@app/services/auth/auth-type";
|
import { ActorAuthMethod, AuthMethod } from "@app/services/auth/auth-type";
|
||||||
|
|
||||||
function isAuthMethodSaml(actorAuthMethod: ActorAuthMethod) {
|
function isAuthMethodSaml(actorAuthMethod: ActorAuthMethod) {
|
||||||
@ -14,14 +14,19 @@ function isAuthMethodSaml(actorAuthMethod: ActorAuthMethod) {
|
|||||||
].includes(actorAuthMethod);
|
].includes(actorAuthMethod);
|
||||||
}
|
}
|
||||||
|
|
||||||
function validateOrgSAML(actorAuthMethod: ActorAuthMethod, isSamlEnforced: TOrganizations["authEnforced"]) {
|
function validateOrgSSO(actorAuthMethod: ActorAuthMethod, isOrgSsoEnforced: TOrganizations["authEnforced"]) {
|
||||||
if (actorAuthMethod === undefined) {
|
if (actorAuthMethod === undefined) {
|
||||||
throw new UnauthorizedError({ name: "No auth method defined" });
|
throw new UnauthorizedError({ name: "No auth method defined" });
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isSamlEnforced && actorAuthMethod !== null && !isAuthMethodSaml(actorAuthMethod)) {
|
if (
|
||||||
throw new UnauthorizedError({ name: "Cannot access org-scoped resource" });
|
isOrgSsoEnforced &&
|
||||||
|
actorAuthMethod !== null &&
|
||||||
|
!isAuthMethodSaml(actorAuthMethod) &&
|
||||||
|
actorAuthMethod !== AuthMethod.OIDC
|
||||||
|
) {
|
||||||
|
throw new ForbiddenRequestError({ name: "Org auth enforced. Cannot access org-scoped resource" });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export { isAuthMethodSaml, validateOrgSAML };
|
export { isAuthMethodSaml, validateOrgSSO };
|
||||||
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user