mirror of
https://github.com/Infisical/infisical.git
synced 2025-03-22 10:16:46 +00:00
Compare commits
610 Commits
stylistic-
...
daniel/cli
Author | SHA1 | Date | |
---|---|---|---|
de92ba157a | |||
dadea751e3 | |||
0ff0357a7c | |||
85f257b4db | |||
18d7a14e3f | |||
ff4d932631 | |||
519f0025c0 | |||
d8d6d7dc1b | |||
a975fbd8a4 | |||
3a6ec3717b | |||
a4a961996b | |||
5b4777c1a5 | |||
2f526850d6 | |||
4f5d31d06f | |||
a8264b17e4 | |||
cb66733e6d | |||
40a0691ccb | |||
6410d51033 | |||
bc30ba9ad1 | |||
a0259712df | |||
1132d07dea | |||
1f0b1964b9 | |||
690e72b44c | |||
fbc7b34786 | |||
9e6641c058 | |||
d035403af1 | |||
1af0d958dd | |||
66a51658d7 | |||
28dc3a4b1c | |||
b27cadb651 | |||
3dca82ad2f | |||
1c90df9dd4 | |||
e15c9e72c6 | |||
71575b1d2e | |||
51f164c399 | |||
702cd0d403 | |||
75267987fc | |||
d734a3f6f4 | |||
cbb749e34a | |||
4535c1069a | |||
747acfe070 | |||
fa1b236f26 | |||
c98ef0eca8 | |||
9f23106c6c | |||
1e7744b498 | |||
44c736facd | |||
51928ddb47 | |||
c7cded4af6 | |||
8b56e20b42 | |||
39c2c37cc0 | |||
3131ae7dae | |||
5315a67d74 | |||
79de7f9f5b | |||
71ffed026d | |||
ee98b15e2b | |||
945d81ad4b | |||
ff8354605c | |||
09b63eee90 | |||
d175256bb4 | |||
ee0c79d018 | |||
d5d7564550 | |||
0db682c5f0 | |||
a01a995585 | |||
2ac785493a | |||
85489a81ff | |||
7116c85f2c | |||
31e4da0dd3 | |||
f255d891ae | |||
4774469244 | |||
e143a31e79 | |||
0baea4c5fd | |||
f6cc20b08b | |||
90e125454e | |||
fbdf3dc9ce | |||
f333c905d9 | |||
71e60df39a | |||
8b4d050d05 | |||
3b4bb591a3 | |||
54f1a4416b | |||
47e3f1b510 | |||
5810b76027 | |||
246e6c64d1 | |||
4e836c5dca | |||
63a289c3be | |||
0a52bbd55d | |||
593bdf74b8 | |||
1f3742e619 | |||
d6e5ac2133 | |||
fea48518a3 | |||
dde24d4c71 | |||
94d509eb01 | |||
8f1e662688 | |||
dcbbb67f03 | |||
055fd34c33 | |||
dc0d3b860e | |||
c0fb3c905e | |||
18b0766d96 | |||
b423696630 | |||
bf60489fde | |||
85ea6d2585 | |||
a97737ab90 | |||
3793858f0a | |||
66c48fbff8 | |||
b6b040375b | |||
9ad5e082e2 | |||
f1805811aa | |||
b135258cce | |||
a651de53d1 | |||
7d0a535f46 | |||
c4e3dd84e3 | |||
9193f13970 | |||
016f22c295 | |||
4d7182c9b1 | |||
6ea7b04efa | |||
3981d61853 | |||
3d391b4e2d | |||
4123177133 | |||
4d61188d0f | |||
fa33f35fcd | |||
13629223fb | |||
74fefa9879 | |||
ff2c8d017f | |||
ba1f8f4564 | |||
e26df005c2 | |||
aca9b47f82 | |||
a16ce8899b | |||
b61511d100 | |||
f8ea421a0e | |||
a945bdfc4c | |||
f7b8345da4 | |||
f6d7ec52c2 | |||
3f6999b2e3 | |||
9128461409 | |||
893235c40f | |||
d3cdaa8449 | |||
e0f655ae30 | |||
93aeca3a38 | |||
1edebdf8a5 | |||
1017707642 | |||
5639306303 | |||
b3a9661755 | |||
72f50ec399 | |||
effc7a3627 | |||
175ce865aa | |||
51f220ba2c | |||
51819e57d1 | |||
510c91cef1 | |||
9be5d89fcf | |||
94f4497903 | |||
e1d9f779b2 | |||
b5af5646ee | |||
1554618167 | |||
5fbfcdda30 | |||
cdbb3b9c47 | |||
0042a95b21 | |||
53233e05d4 | |||
4f15f9c8d3 | |||
97223fabe6 | |||
04b312cbe4 | |||
40bb9668fe | |||
97e5069cf5 | |||
93146fcd96 | |||
87d98de4c1 | |||
26f647b948 | |||
80b3cdd128 | |||
8dd85a0d65 | |||
17995d301a | |||
094b48a2b1 | |||
abd62867eb | |||
179573a269 | |||
457edef5fe | |||
f0b84d5bc9 | |||
7b8bfe38f0 | |||
9903f7c4a0 | |||
42cd98d4d9 | |||
4b203e9ad3 | |||
36bf1b2abc | |||
42fb732955 | |||
da2dcb347a | |||
b9482966cf | |||
1e4b4591ed | |||
9fddcea3db | |||
4a325d6d96 | |||
5e20573110 | |||
f623c8159d | |||
4323407da7 | |||
4c496d5e3d | |||
0c2e566184 | |||
d68dc4c3e0 | |||
e64c579dfd | |||
d0c0d5835c | |||
af2dcdd0c7 | |||
6c628a7265 | |||
00f2d40803 | |||
0a66cbe729 | |||
7fec7c9bf5 | |||
38adc83f2b | |||
f2e5f1bb10 | |||
d1afec4f9a | |||
9460eafd91 | |||
31ad6b0c86 | |||
8afecac7d8 | |||
bf13b81c0f | |||
c753a91958 | |||
695a4a34b5 | |||
372f71f2b0 | |||
e46256f45b | |||
64e868a151 | |||
c8cbcaf10c | |||
51716336c2 | |||
6b51c7269a | |||
f551a4158d | |||
e850b82fb3 | |||
8f85f292db | |||
5f84de039f | |||
8529fac098 | |||
81cf19cb4a | |||
edbe1c8eae | |||
a5039494cd | |||
a908471e66 | |||
84204c3c37 | |||
4931e8579c | |||
20dc243fd9 | |||
785a1389d9 | |||
5a3fc3568a | |||
497601e398 | |||
0da6262ead | |||
8db019d2fe | |||
07d1d91110 | |||
bb506fff9f | |||
7a561bcbdf | |||
8784f80fc1 | |||
0793e70c26 | |||
99f8799ff4 | |||
3f05c8b7ae | |||
6bd624a0f6 | |||
4a11096ea8 | |||
1589eb8e03 | |||
b370d6e415 | |||
65937d6a17 | |||
d20bc1b38a | |||
882ad8729c | |||
0fdf5032f9 | |||
75d9463ceb | |||
e258b84796 | |||
1ab6b21b25 | |||
775037539e | |||
4f05e4ce93 | |||
2e8680c5d4 | |||
e5136c9ef5 | |||
812fe5cf31 | |||
50082e192c | |||
1e1b5d655e | |||
3befd90723 | |||
88549f4030 | |||
46a638cc63 | |||
566f7e4c61 | |||
9ff3210ed6 | |||
f91a6683c2 | |||
c29cb667d7 | |||
7c623562e1 | |||
aef8d79101 | |||
d735ec71b8 | |||
84651d473b | |||
9501386882 | |||
d11f958443 | |||
087a4bb7d2 | |||
750210e6c3 | |||
90cf4e9137 | |||
17bb2e8a7d | |||
b912cd585c | |||
282434de8e | |||
1f939a5e58 | |||
ac0f5369de | |||
6eba64c975 | |||
12515c1866 | |||
c882da2e1a | |||
8a7774f9ac | |||
a7d2ec80c6 | |||
494543ec53 | |||
b7b875b6a7 | |||
3ddd06a3d1 | |||
a1a8364cd1 | |||
3e51fcb546 | |||
c52a16cc47 | |||
f91c77baa3 | |||
e7c2f6f88c | |||
f7c2d38aef | |||
cfb497dd58 | |||
f7122c21fd | |||
b23deca8e4 | |||
b606990dfb | |||
2240277243 | |||
c8c5caba62 | |||
f408a6f60c | |||
391ed0ed74 | |||
aef40212d2 | |||
5aa7cd46c1 | |||
6c0b916ad8 | |||
d7bc80308d | |||
b7c7b242e8 | |||
b592f4cb6d | |||
cd0e1a87cf | |||
b5d7699b8d | |||
69297bc16e | |||
37827367ed | |||
403b1ce993 | |||
c3c0006a25 | |||
2241908d0a | |||
59b822510c | |||
d1408aff35 | |||
c67084f08d | |||
a280e002ed | |||
76c4a8660f | |||
8c54dd611e | |||
98ea2c1828 | |||
5c75f526e7 | |||
113e777b25 | |||
2a93449ffe | |||
1ef1c042da | |||
b64672a921 | |||
227e013502 | |||
88f7e4255e | |||
44ca8c315e | |||
7766a7f4dd | |||
3cb150a749 | |||
9e9ce261c8 | |||
fab7167850 | |||
c7de9aab4e | |||
3560346f85 | |||
f0bf2f8dd0 | |||
2a6216b8fc | |||
a07d055347 | |||
c05230f667 | |||
d68055a264 | |||
e3e62430ba | |||
dc6056b564 | |||
94f0811661 | |||
7b84ae6173 | |||
5710a304f8 | |||
91e3bbba34 | |||
02112ede07 | |||
08cfbf64e4 | |||
18da522b45 | |||
8cf68fbd9c | |||
d6b82dfaa4 | |||
7bd4eed328 | |||
0341c32da0 | |||
caea055281 | |||
c08c78de8d | |||
3765a14246 | |||
c5a11e839b | |||
93bd3d8270 | |||
b9601dd418 | |||
ae3bc04b07 | |||
11edefa66f | |||
f71459ede0 | |||
33324a5a3c | |||
5c6781a705 | |||
71e31518d7 | |||
f6f6db2898 | |||
55780b65d3 | |||
83bbf9599d | |||
f8f2b2574d | |||
318d12addd | |||
872a28d02a | |||
6f53a5631c | |||
ff2098408d | |||
9e85d9bbf0 | |||
0f3a48bb32 | |||
f869def8ea | |||
378bc57a88 | |||
242179598b | |||
70fe80414d | |||
e201e80a06 | |||
177cd385cc | |||
ab48c3b4fe | |||
69f36d1df6 | |||
11c7b5c674 | |||
ee29577e6d | |||
e3e049b66c | |||
878e4a79e7 | |||
609ce8e5cc | |||
04c1ea9b11 | |||
8ffbaa2f6c | |||
796d5e3540 | |||
3baca73e53 | |||
36adf6863b | |||
6363e7d30a | |||
f9621fad8e | |||
90be28b87a | |||
671adee4d7 | |||
c9cb90c98e | |||
9f691df395 | |||
d702a61586 | |||
1c16f406a7 | |||
90f739caa6 | |||
ede8b6f286 | |||
232c547d75 | |||
fe08bbb691 | |||
2bd06ecde4 | |||
08b79d65ea | |||
4e1733ba6c | |||
a4e495ea1c | |||
a750d68363 | |||
d7161a353d | |||
12c414817f | |||
e5e494d0ee | |||
5a21b85e9e | |||
348fdf6429 | |||
88e609cb66 | |||
78058d691a | |||
1d465a50c3 | |||
ffc7249c7c | |||
90bcf23097 | |||
686b88fc97 | |||
5fa4d9029d | |||
7160cf58ee | |||
6b2d757e39 | |||
c075fcceca | |||
e25f5dd65f | |||
3eef023c30 | |||
2a134b9dc2 | |||
d8d63ecaec | |||
e63deb0860 | |||
efc186ae6c | |||
02b2851990 | |||
cb828200e1 | |||
77d068ae2c | |||
8702af671d | |||
31c0fd96ea | |||
2c539697df | |||
ae97b74933 | |||
3e6af2dae5 | |||
3c91e1127f | |||
0e31a9146a | |||
d2a93eb1d2 | |||
fa1b28b33f | |||
415cf31b2d | |||
9002e6cb33 | |||
1ede551c3e | |||
b7b43858f6 | |||
c91789e6d0 | |||
db0ba4be10 | |||
f73c807aa0 | |||
d1dacd81aa | |||
e8b635ce37 | |||
1d3e03e308 | |||
88e2eff7eb | |||
cd192ee228 | |||
1e657968f6 | |||
b8ba51512c | |||
1ac8ddbd92 | |||
a257743fa5 | |||
b5a7240375 | |||
5c2a108c52 | |||
b78d8d28ed | |||
9c9ade52db | |||
4d229ec745 | |||
605dad29ca | |||
bebdad8159 | |||
b547309ae4 | |||
d1ebdbcc03 | |||
c94caa6fb5 | |||
f53fa46c51 | |||
c42d407cda | |||
80b4bc18ec | |||
1dbf80d4e6 | |||
700a072ec5 | |||
8f42914df5 | |||
831da10073 | |||
6904cd3bda | |||
52fd09b87b | |||
0081bbdf9e | |||
c9e5f2bb75 | |||
73cc97cf17 | |||
0c1d37cc75 | |||
60fbd8ac44 | |||
36efa6ba63 | |||
961a73f712 | |||
6e2f3800d4 | |||
258c9e45d4 | |||
8573263379 | |||
9a724db6ab | |||
60a37e784b | |||
14c60bd075 | |||
de715c03ad | |||
ddb1d5a1ab | |||
41323f205d | |||
771498b817 | |||
22b2fb4c98 | |||
9bbba92768 | |||
46eea972f7 | |||
9eb2a74bdf | |||
b80579fdef | |||
214894c88b | |||
8ff37e3ec9 | |||
926f719967 | |||
c3a56f469a | |||
2bd9914373 | |||
354bac486a | |||
ba22a7fca6 | |||
4aef8ab8ee | |||
e89503f00f | |||
f5f20fbdca | |||
4d4887059a | |||
c11c5ec85e | |||
f0e3c9a4b2 | |||
eace4f1bdc | |||
0bd3f32c6e | |||
ad0504e957 | |||
1e20d780ec | |||
7e2685d604 | |||
92fd2d080d | |||
6d60413593 | |||
f59a75d790 | |||
835c36d161 | |||
e4dba6d5c8 | |||
b9986be387 | |||
5f5d62a285 | |||
667fa7a9e3 | |||
27dcb06083 | |||
9b1a15331a | |||
65776b7ab9 | |||
a9c1f278a1 | |||
900facdb36 | |||
fe638ce2c1 | |||
750a43c978 | |||
08b5975f26 | |||
885d1fbd7f | |||
bb2413d659 | |||
dac5529b6c | |||
bd92e35729 | |||
5b7562a76d | |||
edbf459d04 | |||
560274bde8 | |||
7df614a018 | |||
47287be5bf | |||
6e96f2338c | |||
7fd6b63b5d | |||
995777d76f | |||
2a6032a8cf | |||
ec4d1dd1b2 | |||
143de12d67 | |||
52cf937826 | |||
dbd7561037 | |||
d287c3e152 | |||
8fc081973d | |||
c42bbbea8b | |||
29b2b12ec7 | |||
4f80234afa | |||
a1fa0c652d | |||
8327f41b8e | |||
c2bfeb89e8 | |||
4a0668e92e | |||
716e705c2a | |||
f860fd3abe | |||
30e7fe8a45 | |||
307b89e799 | |||
dbf498b44a | |||
5eb3258311 | |||
bd3cbb3c7b | |||
96abbd9f80 | |||
92441e018f | |||
a9bba02f44 | |||
aaca3ac229 | |||
f0383dd55c | |||
a766329de5 | |||
c0b0c0754b | |||
34618041ca | |||
f36a056c62 | |||
e7b11eac2b | |||
0f14fab915 | |||
12a6fba645 | |||
ce057f44ac | |||
2032063c24 | |||
bbceb37d06 | |||
e917b744f4 | |||
7438c114dd | |||
8e3fc044ca | |||
9473de2212 | |||
744c510a51 | |||
f845749a4d | |||
203e00216f | |||
56fc5a2a8c | |||
7edebbabaa | |||
0e698e9355 | |||
ee215bccfa | |||
00b99e7255 | |||
2b7784718d | |||
5f99e58674 | |||
f77942c702 | |||
7a3a6663f1 | |||
8c491668dc | |||
c873e2cba8 | |||
1bc045a7fa | |||
533de93199 | |||
115b4664bf | |||
6b92a5f4db | |||
81e961e8bc | |||
6a7a6ce942 | |||
1695412278 | |||
b4fa07334d | |||
29c244c635 | |||
b80a5989a8 | |||
dc696f8932 | |||
c8f0796952 | |||
9282dd08d9 | |||
df459d456a | |||
c8cfb43316 |
.env.exampleorganization-members.png
.github/workflows
.gitignore.infisicalignoreDockerfile.standalone-infisicalREADME.mdbackend
e2e-test
package.jsonscripts
src
@types
db
migrations
20240318164718_dynamic-secret.ts20240326172010_project-user-additional-privilege.ts20240326172011_machine-identity-additional-privilege.ts20240405000045_org-memberships-unique-constraint.ts20240412174842_group.ts
schemas
ee
routes/v1
dynamic-secret-lease-router.tsdynamic-secret-router.tsgroup-router.tsidentity-project-additional-privilege-router.tsindex.tsldap-router.tslicense-router.tsorg-role-router.tsproject-role-router.tsproject-router.tssaml-router.tsscim-router.tssecret-approval-policy-router.tssecret-approval-request-router.tssecret-rotation-provider-router.tssecret-rotation-router.tssecret-scanning-router.tssecret-version-router.tssnapshot-router.tstrusted-ip-router.tsuser-additional-privilege-router.ts
services
audit-log
dynamic-secret-lease
dynamic-secret-lease-dal.tsdynamic-secret-lease-queue.tsdynamic-secret-lease-service.tsdynamic-secret-lease-types.ts
dynamic-secret
group
identity-project-additional-privilege
identity-project-additional-privilege-dal.tsidentity-project-additional-privilege-service.tsidentity-project-additional-privilege-types.ts
ldap-config
license
permission
project-user-additional-privilege
project-user-additional-privilege-dal.tsproject-user-additional-privilege-service.tsproject-user-additional-privilege-types.ts
saml-config
scim
secret-approval-policy
secret-approval-request
secret-rotation
secret-scanning
secret-snapshot
trusted-ip
lib
api-docs
config
errors
fn
knex
types
queue
server
app.ts
config
plugins
routes
index.tssanitizedSchemas.ts
v1
admin-router.tsauth-router.tsbot-router.tsidentity-access-token-router.tsidentity-router.tsidentity-ua.tsintegration-auth-router.tsintegration-router.tsinvite-org-router.tsorganization-router.tspassword-router.tsproject-env-router.tsproject-key-router.tsproject-membership-router.tsproject-router.tssecret-folder-router.tssecret-import-router.tssecret-tag-router.tsuser-action-router.tsuser-router.tswebhook-router.ts
v2
group-project-router.tsidentity-org-router.tsidentity-project-router.tsindex.tsmfa-router.tsorganization-router.tsproject-membership-router.tsproject-router.tsservice-token-router.tsuser-router.ts
v3
services
auth
auth-fns.tsauth-login-service.tsauth-login-type.tsauth-password-service.tsauth-signup-service.tsauth-type.ts
group-project
group-project-dal.tsgroup-project-membership-role-dal.tsgroup-project-service.tsgroup-project-types.ts
identity-access-token
identity-project
identity-ua
identity
integration-auth
integration
org
project-bot
project-env
project-key
project-membership
project-role
project
secret-blind-index
secret-folder
secret-import
secret-tag
secret
service-token
super-admin
webhook
cli
.gitignore
packages
docs
api-reference
endpoints
identity-specific-privilege
integrations
create-auth.mdxcreate.mdxdelete-auth-by-id.mdxdelete-auth.mdxdelete.mdxfind-auth.mdxlist-auth.mdxlist-project-integrations.mdxupdate.mdx
secret-tags
secrets
overview
cli/commands
documentation
getting-started
guides
platform
access-controls
access-requests.mdxadditional-privileges.mdxoverview.mdxrole-based-access-controls.mdxtemporary-access.mdx
audit-logs.mdxauth-methods
dynamic-secrets
folder.mdxgroups.mdxidentities
ip-allowlisting.mdxldap.mdxldap
mfa.mdxorganization.mdxpit-recovery.mdxpr-workflows.mdxproject.mdxrole-based-access-controls.mdxscim
secret-reference.mdxsecret-rotation
secret-versioning.mdxsso
azure.mdxgithub.mdxgitlab.mdxgoogle-saml.mdxgoogle.mdxjumpcloud.mdxkeycloak-saml.mdxokta.mdxoverview.mdx
token.mdxwebhooks.mdximages
agent
auth-methods
integrations
aws
integrations-amplify-app-id.pngintegrations-amplify-env-console.pngintegrations-aws-secret-manager-auth.pngintegrations-aws-secret-manager-create.png
github
platform
access-controls
access-request-policies.pngaccess-requests-completed.pngaccess-requests-pending.pngadd-additional-privileges.pngadditional-privileges.pngconfigure-temporary-access.pngconfirm-additional-privileges.pngcreate-access-request-policy.pngedit-role.pngrbac.pngrequest-access.pngreview-access-request.pngtemporary-access.png
dynamic-secrets
add-dynamic-secret-button.pngdynamic-secret-modal.pngdynamic-secret-setup-modal.pngdynamic-secret.pnglease-data.pnglease-values.pngmodify-sql-statements.pngprovision-lease.png
groups
groups-org-create.pnggroups-org-users-assign.pnggroups-org-users.pnggroups-org.pnggroups-project-create.pnggroups-project.png
organization
organization-machine-identities.pngorganization-members-roles.pngorganization-members.pngorganization-settings-auth.png
project
scim/okta
secret-versioning.pngsecret-rotation
self-hosting
configuration/email
reference-architectures
sso/keycloak
client-mappers-by-configuration.pngclient-mappers-completed.pngclient-mappers-email.pngclient-mappers-empty.pngclient-mappers-id.pngclient-mappers-predefined.pngclient-mappers-user-property.pngclient-mappers-username.pngclient-saml-capabilities.pngclient-scopes-list.pngclient-signature-encryption.pngclients-list.pngcreate-client-general-settings.pngcreate-client-login-settings.pngenable-saml.pngidp-values.pnginit-config.pngorg-security-section.pngrealm-saml-metadata.pngrealm-settings-keys.png
integrations
cicd
cloud
frameworks
platforms
internals
mint.jsonsdks
self-hosting
style.cssfrontend
.eslintrc.jsindex.tsuseTimedReset.tsxi18n.ts
.storybook
Dockerfilepackage-lock.jsonpackage.jsonpublic/locales
scripts
src
components
analytics
basic
Error.tsxInputField.tsxListbox.tsx
buttons
dialog
AddProjectMemberDialog.tsxAddUpdateEnvironmentDialog.tsxAddUserDialog.tsxAddWorkspaceDialog.tsxDeleteActionModal.tsxDeleteEnvVar.tsxDeleteUserDialog.tsx
popups
table
context/Notifications
dashboard
ConfirmEnvOverwriteModal.tsxDashboardInputField.tsxDeleteActionButton.tsxDownloadSecretsMenu.tsxDropZone.tsx
features
integrations
navigation
notifications
permissions
signup
tags/CreateTagModal
utilities
SecurityClient.tsattemptChangePassword.tsattemptCliLogin.tsattemptCliLoginMfa.tsattemptLoginMfa.ts
checks/password
cryptography
intercom
isValidHexColor.tssaveTokenToLocalStorage.tstelemetry
v2
Accordion
Button
Card
Checkbox
DeleteActionModal
Drawer
Dropdown
EmailServiceSetupModal
FormControl
HoverCard
HoverCardv2
Menu
Modal
Pagination
Popover
Popoverv2
RadioGroup
SecretInput
Select
Spinner
Stepper
Table
Tabs
Tag
Tooltip
UpgradeProjectAlert
config
context
ee
helpers
hoc/withPermission
hooks
api
apiKeys
auditLogs
auth
bots
dynamicSecret
dynamicSecretLease
groups
identities
identityProjectAdditionalPrivilege
index.tsxintegrationAuth
integrations
ldapConfig
organization
projectUserAdditionalPrivilege
scim
secretImports
serverDetails
serviceTokens
ssoConfig
subscriptions
trustedIps
types.tsusers
workspace
layouts
lib/fn
pages
404.tsx_app.tsx
reactQuery.tsxadmin
api
cli-redirect.tsxemail-not-verified.tsxindex.tsxintegrations
aws-parameter-store
aws-secret-manager
checkly
cloudflare-pages
cloudflare-workers
flyio
github
heroku
qovery
railway
render
terraform-cloud
login
org
password-reset.tsxpersonal-settings.tsxproject/[id]
requestnewinvite.tsxsecret-scanning.tsxsignup/sso
signupinvite.tsxverify-email.tsxservices
styles
views
IntegrationsPage
Login
Login.tsxLogin.utils.tsxLoginSSO.tsx
components
Org
MembersPage
MembersPage.tsxindex.tsx
components
OrgGroupsTab
OrgIdentityTab
OrgIdentityTab.tsxindex.tsx
components
IdentitySection
IdentityAuthMethodModal.tsxIdentityModal.tsxIdentitySection.tsxIdentityTable.tsxIdentityUniversalAuthClientSecretModal.tsxIdentityUniversalAuthForm.tsxindex.tsx
index.tsxOrgMembersTab
OrgRoleTabSection
OrgRoleModifySection
OrgRoleModifySection.tsxOrgRoleModifySection.utils.tsSimpleLevelPermissionOptions.tsxWorkspacePermission.tsx
OrgRoleTable.tsxNonePage
components
Project
AuditLogsPage
IPAllowListPage
MembersPage
MembersPage.tsx
components
GroupsTab
IdentityTab
MemberListTab
ProjectRoleListTab/components
ProjectRoleList
ProjectRoleModifySection
ServiceTokenTab
index.tsxSecretApprovalPage/components
SecretApprovalPolicyList
SecretApprovalRequest/components
SecretMainPage
SecretMainPage.store.tsxSecretMainPage.tsx
components
ActionBar
CreateSecretForm
DynamicSecretListView
CreateDynamicSecretLease.tsxDynamicSecretLease.tsxDynamicSecretListView.tsx
EditDynamicSecretForm
RenewDynamicSecretLease.tsxindex.tsxFolderListView
SecretDropzone
SecretImportListView
SecretListView
SnapshotView
SecretOverviewPage
SecretOverviewPage.tsx
components
CreateSecretForm
ProjectIndexSecretsSection
SecretOverviewDynamicSecretRow
SecretOverviewFolderRow
SecretOverviewTableRow
SecretRotationPage
SecretScanning/components
Settings
BillingSettingsPage
BillingSettingsPage.tsxindex.tsx
components
BillingCloudTab
BillingDetailsTab
BillingDetailsTab.tsxCompanyNameSection.tsxInvoiceEmailSection.tsxTaxIDModal.tsxTaxIDSection.tsxindex.tsx
BillingReceiptsTab
BillingSelfHostedTab
BillingTabGroup
index.tsxOrgSettingsPage
OrgSettingsPage.tsx
components
OrgAuthTab
LDAPModal.tsxOrgGeneralAuthSection.tsxOrgLDAPSection.tsxOrgSCIMSection.tsxOrgSSOSection.tsxSSOModal.tsxScimTokenModal.tsxindex.tsx
OrgDeleteSection
OrgGeneralTab
OrgIncidentContactsSection
OrgNameChangeSection
OrgTabGroup
index.tsxPersonalSettingsPage
APIKeySection
AuthMethodSection
ChangeLanguageSection
ChangePasswordSection
DeleteAccountSection
EmergencyKitSection
PersonalAPIKeyTab
PersonalAuthTab
PersonalGeneralTab
PersonalSettingsPage.tsxPersonalTabGroup
SecuritySection
SessionsSection
UserNameSection
index.tsxProjectSettingsPage
ProjectSettingsPage.tsx
components
AutoCapitalizationSection
DeleteProjectSection
E2EESection
EnvironmentSection
ProjectGeneralTab
ProjectNameChangeSection
ProjectTabGroup
SecretTagsSection
WebhooksTab
index.tsxSignup
admin
DashboardPage
SignUpPage
helm-charts
infisical-standalone-postgres
secrets-operator
k8-operator
@ -3,9 +3,6 @@
|
||||
# THIS IS A SAMPLE ENCRYPTION KEY AND SHOULD NEVER BE USED FOR PRODUCTION
|
||||
ENCRYPTION_KEY=6c1fe4e407b8911c104518103505b218
|
||||
|
||||
# Required
|
||||
DB_CONNECTION_URI=postgres://${POSTGRES_USER}:${POSTGRES_PASSWORD}@db:5432/${POSTGRES_DB}
|
||||
|
||||
# JWT
|
||||
# Required secrets to sign JWT tokens
|
||||
# THIS IS A SAMPLE AUTH_SECRET KEY AND SHOULD NEVER BE USED FOR PRODUCTION
|
||||
@ -16,6 +13,9 @@ POSTGRES_PASSWORD=infisical
|
||||
POSTGRES_USER=infisical
|
||||
POSTGRES_DB=infisical
|
||||
|
||||
# Required
|
||||
DB_CONNECTION_URI=postgres://${POSTGRES_USER}:${POSTGRES_PASSWORD}@db:5432/${POSTGRES_DB}
|
||||
|
||||
# Redis
|
||||
REDIS_URL=redis://redis:6379
|
||||
|
||||
|
@ -41,6 +41,7 @@ jobs:
|
||||
load: true
|
||||
context: backend
|
||||
tags: infisical/infisical:test
|
||||
platforms: linux/amd64,linux/arm64
|
||||
- name: ⏻ Spawn backend container and dependencies
|
||||
run: |
|
||||
docker compose -f .github/resources/docker-compose.be-test.yml up --wait --quiet-pull
|
||||
@ -92,6 +93,7 @@ jobs:
|
||||
project: 64mmf0n610
|
||||
context: frontend
|
||||
tags: infisical/frontend:test
|
||||
platforms: linux/amd64,linux/arm64
|
||||
build-args: |
|
||||
POSTHOG_API_KEY=${{ secrets.PUBLIC_POSTHOG_API_KEY }}
|
||||
NEXT_INFISICAL_PLATFORM_VERSION=${{ steps.extract_version.outputs.version }}
|
||||
|
140
.github/workflows/build-staging-and-deploy-aws.yml
vendored
Normal file
140
.github/workflows/build-staging-and-deploy-aws.yml
vendored
Normal file
@ -0,0 +1,140 @@
|
||||
name: Deployment pipeline
|
||||
on: [workflow_dispatch]
|
||||
|
||||
permissions:
|
||||
id-token: write
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
infisical-image:
|
||||
name: Build backend image
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: ☁️ Checkout source
|
||||
uses: actions/checkout@v3
|
||||
- name: 📦 Install dependencies to test all dependencies
|
||||
run: npm ci --only-production
|
||||
working-directory: backend
|
||||
- name: Save commit hashes for tag
|
||||
id: commit
|
||||
uses: pr-mpt/actions-commit-hash@v2
|
||||
- name: 🔧 Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v2
|
||||
- name: 🐋 Login to Docker Hub
|
||||
uses: docker/login-action@v2
|
||||
with:
|
||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
- name: Set up Depot CLI
|
||||
uses: depot/setup-action@v1
|
||||
- name: 🏗️ Build backend and push to docker hub
|
||||
uses: depot/build-push-action@v1
|
||||
with:
|
||||
project: 64mmf0n610
|
||||
token: ${{ secrets.DEPOT_PROJECT_TOKEN }}
|
||||
push: true
|
||||
context: .
|
||||
file: Dockerfile.standalone-infisical
|
||||
tags: |
|
||||
infisical/staging_infisical:${{ steps.commit.outputs.short }}
|
||||
infisical/staging_infisical:latest
|
||||
platforms: linux/amd64,linux/arm64
|
||||
build-args: |
|
||||
POSTHOG_API_KEY=${{ secrets.PUBLIC_POSTHOG_API_KEY }}
|
||||
INFISICAL_PLATFORM_VERSION=${{ steps.commit.outputs.short }}
|
||||
|
||||
gamma-deployment:
|
||||
name: Deploy to gamma
|
||||
runs-on: ubuntu-latest
|
||||
needs: [infisical-image]
|
||||
environment:
|
||||
name: Gamma
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v2
|
||||
- name: Setup Node.js environment
|
||||
uses: actions/setup-node@v2
|
||||
with:
|
||||
node-version: "20"
|
||||
- name: Change directory to backend and install dependencies
|
||||
env:
|
||||
DB_CONNECTION_URI: ${{ secrets.DB_CONNECTION_URI }}
|
||||
run: |
|
||||
cd backend
|
||||
npm install
|
||||
npm run migration:latest
|
||||
- name: Configure AWS Credentials
|
||||
uses: aws-actions/configure-aws-credentials@v4
|
||||
with:
|
||||
audience: sts.amazonaws.com
|
||||
aws-region: us-east-1
|
||||
role-to-assume: arn:aws:iam::905418227878:role/deploy-new-ecs-img
|
||||
- name: Save commit hashes for tag
|
||||
id: commit
|
||||
uses: pr-mpt/actions-commit-hash@v2
|
||||
- name: Download task definition
|
||||
run: |
|
||||
aws ecs describe-task-definition --task-definition infisical-prod-platform --query taskDefinition > task-definition.json
|
||||
- name: Render Amazon ECS task definition
|
||||
id: render-web-container
|
||||
uses: aws-actions/amazon-ecs-render-task-definition@v1
|
||||
with:
|
||||
task-definition: task-definition.json
|
||||
container-name: infisical-prod-platform
|
||||
image: infisical/staging_infisical:${{ steps.commit.outputs.short }}
|
||||
environment-variables: "LOG_LEVEL=info"
|
||||
- name: Deploy to Amazon ECS service
|
||||
uses: aws-actions/amazon-ecs-deploy-task-definition@v1
|
||||
with:
|
||||
task-definition: ${{ steps.render-web-container.outputs.task-definition }}
|
||||
service: infisical-prod-platform
|
||||
cluster: infisical-prod-platform
|
||||
wait-for-service-stability: true
|
||||
|
||||
production-postgres-deployment:
|
||||
name: Deploy to production
|
||||
runs-on: ubuntu-latest
|
||||
needs: [gamma-deployment]
|
||||
environment:
|
||||
name: Production
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v2
|
||||
- name: Setup Node.js environment
|
||||
uses: actions/setup-node@v2
|
||||
with:
|
||||
node-version: "20"
|
||||
- name: Change directory to backend and install dependencies
|
||||
env:
|
||||
DB_CONNECTION_URI: ${{ secrets.DB_CONNECTION_URI }}
|
||||
run: |
|
||||
cd backend
|
||||
npm install
|
||||
npm run migration:latest
|
||||
- name: Configure AWS Credentials
|
||||
uses: aws-actions/configure-aws-credentials@v4
|
||||
with:
|
||||
audience: sts.amazonaws.com
|
||||
aws-region: us-east-1
|
||||
role-to-assume: arn:aws:iam::381492033652:role/gha-make-prod-deployment
|
||||
- name: Save commit hashes for tag
|
||||
id: commit
|
||||
uses: pr-mpt/actions-commit-hash@v2
|
||||
- name: Download task definition
|
||||
run: |
|
||||
aws ecs describe-task-definition --task-definition infisical-prod-platform --query taskDefinition > task-definition.json
|
||||
- name: Render Amazon ECS task definition
|
||||
id: render-web-container
|
||||
uses: aws-actions/amazon-ecs-render-task-definition@v1
|
||||
with:
|
||||
task-definition: task-definition.json
|
||||
container-name: infisical-prod-platform
|
||||
image: infisical/staging_infisical:${{ steps.commit.outputs.short }}
|
||||
environment-variables: "LOG_LEVEL=info"
|
||||
- name: Deploy to Amazon ECS service
|
||||
uses: aws-actions/amazon-ecs-deploy-task-definition@v1
|
||||
with:
|
||||
task-definition: ${{ steps.render-web-container.outputs.task-definition }}
|
||||
service: infisical-prod-platform
|
||||
cluster: infisical-prod-platform
|
||||
wait-for-service-stability: true
|
120
.github/workflows/build-staging-and-deploy.yml
vendored
120
.github/workflows/build-staging-and-deploy.yml
vendored
@ -1,120 +0,0 @@
|
||||
name: Build, Publish and Deploy to Gamma
|
||||
on: [workflow_dispatch]
|
||||
|
||||
jobs:
|
||||
infisical-image:
|
||||
name: Build backend image
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: ☁️ Checkout source
|
||||
uses: actions/checkout@v3
|
||||
- name: 📦 Install dependencies to test all dependencies
|
||||
run: npm ci --only-production
|
||||
working-directory: backend
|
||||
# - name: 🧪 Run tests
|
||||
# run: npm run test:ci
|
||||
# working-directory: backend
|
||||
- name: Save commit hashes for tag
|
||||
id: commit
|
||||
uses: pr-mpt/actions-commit-hash@v2
|
||||
- name: 🔧 Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v2
|
||||
- name: 🐋 Login to Docker Hub
|
||||
uses: docker/login-action@v2
|
||||
with:
|
||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
- name: Set up Depot CLI
|
||||
uses: depot/setup-action@v1
|
||||
- name: 📦 Build backend and export to Docker
|
||||
uses: depot/build-push-action@v1
|
||||
with:
|
||||
project: 64mmf0n610
|
||||
token: ${{ secrets.DEPOT_PROJECT_TOKEN }}
|
||||
load: true
|
||||
context: .
|
||||
file: Dockerfile.standalone-infisical
|
||||
tags: infisical/infisical:test
|
||||
# - name: ⏻ Spawn backend container and dependencies
|
||||
# run: |
|
||||
# docker compose -f .github/resources/docker-compose.be-test.yml up --wait --quiet-pull
|
||||
# - name: 🧪 Test backend image
|
||||
# run: |
|
||||
# ./.github/resources/healthcheck.sh infisical-backend-test
|
||||
# - name: ⏻ Shut down backend container and dependencies
|
||||
# run: |
|
||||
# docker compose -f .github/resources/docker-compose.be-test.yml down
|
||||
- name: 🏗️ Build backend and push
|
||||
uses: depot/build-push-action@v1
|
||||
with:
|
||||
project: 64mmf0n610
|
||||
token: ${{ secrets.DEPOT_PROJECT_TOKEN }}
|
||||
push: true
|
||||
context: .
|
||||
file: Dockerfile.standalone-infisical
|
||||
tags: |
|
||||
infisical/staging_infisical:${{ steps.commit.outputs.short }}
|
||||
infisical/staging_infisical:latest
|
||||
platforms: linux/amd64,linux/arm64
|
||||
build-args: |
|
||||
POSTHOG_API_KEY=${{ secrets.PUBLIC_POSTHOG_API_KEY }}
|
||||
INFISICAL_PLATFORM_VERSION=${{ steps.extract_version.outputs.version }}
|
||||
postgres-migration:
|
||||
name: Run latest migration files
|
||||
runs-on: ubuntu-latest
|
||||
needs: [infisical-image]
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v2
|
||||
- name: Setup Node.js environment
|
||||
uses: actions/setup-node@v2
|
||||
with:
|
||||
node-version: "20"
|
||||
- name: Change directory to backend and install dependencies
|
||||
env:
|
||||
DB_CONNECTION_URI: ${{ secrets.DB_CONNECTION_URI }}
|
||||
run: |
|
||||
cd backend
|
||||
npm install
|
||||
npm run migration:latest
|
||||
# - name: Run postgres DB migration files
|
||||
# env:
|
||||
# DB_CONNECTION_URI: ${{ secrets.DB_CONNECTION_URI }}
|
||||
# run: npm run migration:latest
|
||||
gamma-deployment:
|
||||
name: Deploy to gamma
|
||||
runs-on: ubuntu-latest
|
||||
needs: [postgres-migration]
|
||||
steps:
|
||||
- name: ☁️ Checkout source
|
||||
uses: actions/checkout@v3
|
||||
- name: Install Helm
|
||||
uses: azure/setup-helm@v3
|
||||
with:
|
||||
version: v3.10.0
|
||||
- name: Install infisical helm chart
|
||||
run: |
|
||||
helm repo add infisical-helm-charts 'https://dl.cloudsmith.io/public/infisical/helm-charts/helm/charts/'
|
||||
helm repo update
|
||||
- name: Install kubectl
|
||||
uses: azure/setup-kubectl@v3
|
||||
- name: Install doctl
|
||||
uses: digitalocean/action-doctl@v2
|
||||
with:
|
||||
token: ${{ secrets.DIGITALOCEAN_ACCESS_TOKEN }}
|
||||
- name: Save DigitalOcean kubeconfig with short-lived credentials
|
||||
run: doctl kubernetes cluster kubeconfig save --expiry-seconds 600 infisical-gamma-postgres
|
||||
- name: switch to gamma namespace
|
||||
run: kubectl config set-context --current --namespace=gamma
|
||||
- name: test kubectl
|
||||
run: kubectl get ingress
|
||||
- name: Download helm values to file and upgrade gamma deploy
|
||||
run: |
|
||||
wget https://raw.githubusercontent.com/Infisical/infisical/main/.github/values.yaml
|
||||
helm upgrade infisical infisical-helm-charts/infisical-standalone --values values.yaml --wait --install
|
||||
if [[ $(helm status infisical) == *"FAILED"* ]]; then
|
||||
echo "Helm upgrade failed"
|
||||
exit 1
|
||||
else
|
||||
echo "Helm upgrade was successful"
|
||||
fi
|
2
.gitignore
vendored
2
.gitignore
vendored
@ -59,6 +59,8 @@ yarn-error.log*
|
||||
# Infisical init
|
||||
.infisical.json
|
||||
|
||||
.infisicalignore
|
||||
|
||||
# Editor specific
|
||||
.vscode/*
|
||||
|
||||
|
@ -1 +1,5 @@
|
||||
.github/resources/docker-compose.be-test.yml:generic-api-key:16
|
||||
frontend/src/views/Project/MembersPage/components/IdentityTab/components/IdentityRoleForm/IdentityRbacSection.tsx:generic-api-key:206
|
||||
frontend/src/views/Project/MembersPage/components/IdentityTab/components/IdentityRoleForm/SpecificPrivilegeSection.tsx:generic-api-key:304
|
||||
frontend/src/views/Project/MembersPage/components/MemberListTab/MemberRoleForm/MemberRbacSection.tsx:generic-api-key:206
|
||||
frontend/src/views/Project/MembersPage/components/MemberListTab/MemberRoleForm/SpecificPrivilegeSection.tsx:generic-api-key:292
|
@ -1,6 +1,7 @@
|
||||
ARG POSTHOG_HOST=https://app.posthog.com
|
||||
ARG POSTHOG_API_KEY=posthog-api-key
|
||||
ARG INTERCOM_ID=intercom-id
|
||||
ARG SAML_ORG_SLUG=saml-org-slug-default
|
||||
|
||||
FROM node:20-alpine AS base
|
||||
|
||||
@ -35,6 +36,8 @@ ARG INTERCOM_ID
|
||||
ENV NEXT_PUBLIC_INTERCOM_ID $INTERCOM_ID
|
||||
ARG INFISICAL_PLATFORM_VERSION
|
||||
ENV NEXT_PUBLIC_INFISICAL_PLATFORM_VERSION $INFISICAL_PLATFORM_VERSION
|
||||
ARG SAML_ORG_SLUG
|
||||
ENV NEXT_PUBLIC_SAML_ORG_SLUG=$SAML_ORG_SLUG
|
||||
|
||||
# Build
|
||||
RUN npm run build
|
||||
@ -100,6 +103,9 @@ ENV NEXT_PUBLIC_POSTHOG_API_KEY=$POSTHOG_API_KEY \
|
||||
ARG INTERCOM_ID=intercom-id
|
||||
ENV NEXT_PUBLIC_INTERCOM_ID=$INTERCOM_ID \
|
||||
BAKED_NEXT_PUBLIC_INTERCOM_ID=$INTERCOM_ID
|
||||
ARG SAML_ORG_SLUG
|
||||
ENV NEXT_PUBLIC_SAML_ORG_SLUG=$SAML_ORG_SLUG \
|
||||
BAKED_NEXT_PUBLIC_SAML_ORG_SLUG=$SAML_ORG_SLUG
|
||||
|
||||
WORKDIR /
|
||||
|
||||
@ -118,9 +124,6 @@ WORKDIR /backend
|
||||
|
||||
ENV TELEMETRY_ENABLED true
|
||||
|
||||
HEALTHCHECK --interval=10s --timeout=3s --start-period=10s \
|
||||
CMD node healthcheck.js
|
||||
|
||||
EXPOSE 8080
|
||||
EXPOSE 443
|
||||
|
||||
|
@ -10,7 +10,8 @@
|
||||
<a href="https://infisical.com/">Infisical Cloud</a> |
|
||||
<a href="https://infisical.com/docs/self-hosting/overview">Self-Hosting</a> |
|
||||
<a href="https://infisical.com/docs/documentation/getting-started/introduction">Docs</a> |
|
||||
<a href="https://www.infisical.com">Website</a>
|
||||
<a href="https://www.infisical.com">Website</a> |
|
||||
<a href="https://infisical.com/careers">Hiring (Remote/SF)</a>
|
||||
</h4>
|
||||
|
||||
<p align="center">
|
||||
|
@ -46,7 +46,7 @@ const deleteSecretImport = async (id: string) => {
|
||||
|
||||
describe("Secret Import Router", async () => {
|
||||
test.each([
|
||||
{ importEnv: "dev", importPath: "/" }, // one in root
|
||||
{ importEnv: "prod", importPath: "/" }, // one in root
|
||||
{ importEnv: "staging", importPath: "/" } // then create a deep one creating intermediate ones
|
||||
])("Create secret import $importEnv with path $importPath", async ({ importPath, importEnv }) => {
|
||||
// check for default environments
|
||||
@ -66,7 +66,7 @@ describe("Secret Import Router", async () => {
|
||||
});
|
||||
|
||||
test("Get secret imports", async () => {
|
||||
const createdImport1 = await createSecretImport("/", "dev");
|
||||
const createdImport1 = await createSecretImport("/", "prod");
|
||||
const createdImport2 = await createSecretImport("/", "staging");
|
||||
const res = await testServer.inject({
|
||||
method: "GET",
|
||||
@ -103,10 +103,10 @@ describe("Secret Import Router", async () => {
|
||||
});
|
||||
|
||||
test("Update secret import position", async () => {
|
||||
const devImportDetails = { path: "/", envSlug: "dev" };
|
||||
const prodImportDetails = { path: "/", envSlug: "prod" };
|
||||
const stagingImportDetails = { path: "/", envSlug: "staging" };
|
||||
|
||||
const createdImport1 = await createSecretImport(devImportDetails.path, devImportDetails.envSlug);
|
||||
const createdImport1 = await createSecretImport(prodImportDetails.path, prodImportDetails.envSlug);
|
||||
const createdImport2 = await createSecretImport(stagingImportDetails.path, stagingImportDetails.envSlug);
|
||||
|
||||
const updateImportRes = await testServer.inject({
|
||||
@ -136,7 +136,7 @@ describe("Secret Import Router", async () => {
|
||||
position: 2,
|
||||
importEnv: expect.objectContaining({
|
||||
name: expect.any(String),
|
||||
slug: expect.stringMatching(devImportDetails.envSlug),
|
||||
slug: expect.stringMatching(prodImportDetails.envSlug),
|
||||
id: expect.any(String)
|
||||
})
|
||||
})
|
||||
@ -166,7 +166,7 @@ describe("Secret Import Router", async () => {
|
||||
});
|
||||
|
||||
test("Delete secret import position", async () => {
|
||||
const createdImport1 = await createSecretImport("/", "dev");
|
||||
const createdImport1 = await createSecretImport("/", "prod");
|
||||
const createdImport2 = await createSecretImport("/", "staging");
|
||||
const deletedImport = await deleteSecretImport(createdImport1.id);
|
||||
// check for default environments
|
||||
|
@ -10,7 +10,7 @@ import { seedData1 } from "@app/db/seed-data";
|
||||
import { initEnvConfig } from "@app/lib/config/env";
|
||||
import { initLogger } from "@app/lib/logger";
|
||||
import { main } from "@app/server/app";
|
||||
import { AuthTokenType } from "@app/services/auth/auth-type";
|
||||
import { AuthMethod, AuthTokenType } from "@app/services/auth/auth-type";
|
||||
|
||||
import { mockQueue } from "./mocks/queue";
|
||||
import { mockSmtpServer } from "./mocks/smtp";
|
||||
@ -52,6 +52,8 @@ export default {
|
||||
authTokenType: AuthTokenType.ACCESS_TOKEN,
|
||||
userId: seedData1.id,
|
||||
tokenVersionId: seedData1.token.id,
|
||||
authMethod: AuthMethod.EMAIL,
|
||||
organizationId: seedData1.organization.id,
|
||||
accessVersion: 1
|
||||
},
|
||||
cfg.AUTH_SECRET,
|
||||
|
@ -108,7 +108,7 @@
|
||||
"libsodium-wrappers": "^0.7.13",
|
||||
"lodash.isequal": "^4.5.0",
|
||||
"ms": "^2.1.3",
|
||||
"mysql2": "^3.9.1",
|
||||
"mysql2": "^3.9.4",
|
||||
"nanoid": "^5.0.4",
|
||||
"nodemailer": "^6.9.9",
|
||||
"ora": "^7.0.1",
|
||||
|
@ -103,11 +103,15 @@ export const ${dalName} = (db: TDbClient) => {
|
||||
`import { z } from "zod";
|
||||
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
|
||||
import { AuthMode } from "@app/services/auth/auth-type";
|
||||
import { readLimit } from "@app/server/config/rateLimiter";
|
||||
|
||||
export const register${pascalCase}Router = async (server: FastifyZodProvider) => {
|
||||
server.route({
|
||||
url: "/",
|
||||
method: "GET",
|
||||
url: "/",
|
||||
config: {
|
||||
rateLimit: readLimit
|
||||
},
|
||||
schema: {
|
||||
params: z.object({}),
|
||||
response: {
|
||||
|
@ -7,10 +7,10 @@ const prompt = promptSync({ sigint: true });
|
||||
|
||||
const migrationName = prompt("Enter name for migration: ");
|
||||
|
||||
// Remove spaces from migration name and replace with hyphens
|
||||
const formattedMigrationName = migrationName.replace(/\s+/g, "-");
|
||||
|
||||
execSync(
|
||||
`npx knex migrate:make --knexfile ${path.join(
|
||||
__dirname,
|
||||
"../src/db/knexfile.ts"
|
||||
)} -x ts ${migrationName}`,
|
||||
`npx knex migrate:make --knexfile ${path.join(__dirname, "../src/db/knexfile.ts")} -x ts ${formattedMigrationName}`,
|
||||
{ stdio: "inherit" }
|
||||
);
|
||||
|
17
backend/src/@types/fastify.d.ts
vendored
17
backend/src/@types/fastify.d.ts
vendored
@ -3,9 +3,14 @@ import "fastify";
|
||||
import { TUsers } from "@app/db/schemas";
|
||||
import { TAuditLogServiceFactory } from "@app/ee/services/audit-log/audit-log-service";
|
||||
import { TCreateAuditLogDTO } from "@app/ee/services/audit-log/audit-log-types";
|
||||
import { TDynamicSecretServiceFactory } from "@app/ee/services/dynamic-secret/dynamic-secret-service";
|
||||
import { TDynamicSecretLeaseServiceFactory } from "@app/ee/services/dynamic-secret-lease/dynamic-secret-lease-service";
|
||||
import { TGroupServiceFactory } from "@app/ee/services/group/group-service";
|
||||
import { TIdentityProjectAdditionalPrivilegeServiceFactory } from "@app/ee/services/identity-project-additional-privilege/identity-project-additional-privilege-service";
|
||||
import { TLdapConfigServiceFactory } from "@app/ee/services/ldap-config/ldap-config-service";
|
||||
import { TLicenseServiceFactory } from "@app/ee/services/license/license-service";
|
||||
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service";
|
||||
import { TProjectUserAdditionalPrivilegeServiceFactory } from "@app/ee/services/project-user-additional-privilege/project-user-additional-privilege-service";
|
||||
import { TSamlConfigServiceFactory } from "@app/ee/services/saml-config/saml-config-service";
|
||||
import { TScimServiceFactory } from "@app/ee/services/scim/scim-service";
|
||||
import { TSecretApprovalPolicyServiceFactory } from "@app/ee/services/secret-approval-policy/secret-approval-policy-service";
|
||||
@ -19,8 +24,9 @@ import { TApiKeyServiceFactory } from "@app/services/api-key/api-key-service";
|
||||
import { TAuthLoginFactory } from "@app/services/auth/auth-login-service";
|
||||
import { TAuthPasswordFactory } from "@app/services/auth/auth-password-service";
|
||||
import { TAuthSignupFactory } from "@app/services/auth/auth-signup-service";
|
||||
import { ActorType } from "@app/services/auth/auth-type";
|
||||
import { ActorAuthMethod, ActorType } from "@app/services/auth/auth-type";
|
||||
import { TAuthTokenServiceFactory } from "@app/services/auth-token/auth-token-service";
|
||||
import { TGroupProjectServiceFactory } from "@app/services/group-project/group-project-service";
|
||||
import { TIdentityServiceFactory } from "@app/services/identity/identity-service";
|
||||
import { TIdentityAccessTokenServiceFactory } from "@app/services/identity-access-token/identity-access-token-service";
|
||||
import { TIdentityProjectServiceFactory } from "@app/services/identity-project/identity-project-service";
|
||||
@ -59,9 +65,10 @@ declare module "fastify" {
|
||||
// identity injection. depending on which kinda of token the information is filled in auth
|
||||
auth: TAuthMode;
|
||||
permission: {
|
||||
authMethod: ActorAuthMethod;
|
||||
type: ActorType;
|
||||
id: string;
|
||||
orgId?: string;
|
||||
orgId: string;
|
||||
};
|
||||
// passport data
|
||||
passportUser: {
|
||||
@ -84,6 +91,8 @@ declare module "fastify" {
|
||||
orgRole: TOrgRoleServiceFactory;
|
||||
superAdmin: TSuperAdminServiceFactory;
|
||||
user: TUserServiceFactory;
|
||||
group: TGroupServiceFactory;
|
||||
groupProject: TGroupProjectServiceFactory;
|
||||
apiKey: TApiKeyServiceFactory;
|
||||
project: TProjectServiceFactory;
|
||||
projectMembership: TProjectMembershipServiceFactory;
|
||||
@ -116,6 +125,10 @@ declare module "fastify" {
|
||||
trustedIp: TTrustedIpServiceFactory;
|
||||
secretBlindIndex: TSecretBlindIndexServiceFactory;
|
||||
telemetry: TTelemetryServiceFactory;
|
||||
dynamicSecret: TDynamicSecretServiceFactory;
|
||||
dynamicSecretLease: TDynamicSecretLeaseServiceFactory;
|
||||
projectUserAdditionalPrivilege: TProjectUserAdditionalPrivilegeServiceFactory;
|
||||
identityProjectAdditionalPrivilege: TIdentityProjectAdditionalPrivilegeServiceFactory;
|
||||
};
|
||||
// this is exclusive use for middlewares in which we need to inject data
|
||||
// everywhere else access using service layer
|
||||
|
56
backend/src/@types/knex.d.ts
vendored
56
backend/src/@types/knex.d.ts
vendored
@ -17,12 +17,27 @@ import {
|
||||
TBackupPrivateKey,
|
||||
TBackupPrivateKeyInsert,
|
||||
TBackupPrivateKeyUpdate,
|
||||
TDynamicSecretLeases,
|
||||
TDynamicSecretLeasesInsert,
|
||||
TDynamicSecretLeasesUpdate,
|
||||
TDynamicSecrets,
|
||||
TDynamicSecretsInsert,
|
||||
TDynamicSecretsUpdate,
|
||||
TGitAppInstallSessions,
|
||||
TGitAppInstallSessionsInsert,
|
||||
TGitAppInstallSessionsUpdate,
|
||||
TGitAppOrg,
|
||||
TGitAppOrgInsert,
|
||||
TGitAppOrgUpdate,
|
||||
TGroupProjectMembershipRoles,
|
||||
TGroupProjectMembershipRolesInsert,
|
||||
TGroupProjectMembershipRolesUpdate,
|
||||
TGroupProjectMemberships,
|
||||
TGroupProjectMembershipsInsert,
|
||||
TGroupProjectMembershipsUpdate,
|
||||
TGroups,
|
||||
TGroupsInsert,
|
||||
TGroupsUpdate,
|
||||
TIdentities,
|
||||
TIdentitiesInsert,
|
||||
TIdentitiesUpdate,
|
||||
@ -32,6 +47,9 @@ import {
|
||||
TIdentityOrgMemberships,
|
||||
TIdentityOrgMembershipsInsert,
|
||||
TIdentityOrgMembershipsUpdate,
|
||||
TIdentityProjectAdditionalPrivilege,
|
||||
TIdentityProjectAdditionalPrivilegeInsert,
|
||||
TIdentityProjectAdditionalPrivilegeUpdate,
|
||||
TIdentityProjectMembershipRole,
|
||||
TIdentityProjectMembershipRoleInsert,
|
||||
TIdentityProjectMembershipRoleUpdate,
|
||||
@ -86,6 +104,9 @@ import {
|
||||
TProjects,
|
||||
TProjectsInsert,
|
||||
TProjectsUpdate,
|
||||
TProjectUserAdditionalPrivilege,
|
||||
TProjectUserAdditionalPrivilegeInsert,
|
||||
TProjectUserAdditionalPrivilegeUpdate,
|
||||
TProjectUserMembershipRoles,
|
||||
TProjectUserMembershipRolesInsert,
|
||||
TProjectUserMembershipRolesUpdate,
|
||||
@ -176,6 +197,9 @@ import {
|
||||
TUserEncryptionKeys,
|
||||
TUserEncryptionKeysInsert,
|
||||
TUserEncryptionKeysUpdate,
|
||||
TUserGroupMembership,
|
||||
TUserGroupMembershipInsert,
|
||||
TUserGroupMembershipUpdate,
|
||||
TUsers,
|
||||
TUsersInsert,
|
||||
TUsersUpdate,
|
||||
@ -187,6 +211,22 @@ import {
|
||||
declare module "knex/types/tables" {
|
||||
interface Tables {
|
||||
[TableName.Users]: Knex.CompositeTableType<TUsers, TUsersInsert, TUsersUpdate>;
|
||||
[TableName.Groups]: Knex.CompositeTableType<TGroups, TGroupsInsert, TGroupsUpdate>;
|
||||
[TableName.UserGroupMembership]: Knex.CompositeTableType<
|
||||
TUserGroupMembership,
|
||||
TUserGroupMembershipInsert,
|
||||
TUserGroupMembershipUpdate
|
||||
>;
|
||||
[TableName.GroupProjectMembership]: Knex.CompositeTableType<
|
||||
TGroupProjectMemberships,
|
||||
TGroupProjectMembershipsInsert,
|
||||
TGroupProjectMembershipsUpdate
|
||||
>;
|
||||
[TableName.GroupProjectMembershipRole]: Knex.CompositeTableType<
|
||||
TGroupProjectMembershipRoles,
|
||||
TGroupProjectMembershipRolesInsert,
|
||||
TGroupProjectMembershipRolesUpdate
|
||||
>;
|
||||
[TableName.UserAliases]: Knex.CompositeTableType<TUserAliases, TUserAliasesInsert, TUserAliasesUpdate>;
|
||||
[TableName.UserEncryptionKey]: Knex.CompositeTableType<
|
||||
TUserEncryptionKeys,
|
||||
@ -233,6 +273,11 @@ declare module "knex/types/tables" {
|
||||
TProjectUserMembershipRolesUpdate
|
||||
>;
|
||||
[TableName.ProjectRoles]: Knex.CompositeTableType<TProjectRoles, TProjectRolesInsert, TProjectRolesUpdate>;
|
||||
[TableName.ProjectUserAdditionalPrivilege]: Knex.CompositeTableType<
|
||||
TProjectUserAdditionalPrivilege,
|
||||
TProjectUserAdditionalPrivilegeInsert,
|
||||
TProjectUserAdditionalPrivilegeUpdate
|
||||
>;
|
||||
[TableName.ProjectKeys]: Knex.CompositeTableType<TProjectKeys, TProjectKeysInsert, TProjectKeysUpdate>;
|
||||
[TableName.Secret]: Knex.CompositeTableType<TSecrets, TSecretsInsert, TSecretsUpdate>;
|
||||
[TableName.SecretBlindIndex]: Knex.CompositeTableType<
|
||||
@ -288,6 +333,11 @@ declare module "knex/types/tables" {
|
||||
TIdentityProjectMembershipRoleInsert,
|
||||
TIdentityProjectMembershipRoleUpdate
|
||||
>;
|
||||
[TableName.IdentityProjectAdditionalPrivilege]: Knex.CompositeTableType<
|
||||
TIdentityProjectAdditionalPrivilege,
|
||||
TIdentityProjectAdditionalPrivilegeInsert,
|
||||
TIdentityProjectAdditionalPrivilegeUpdate
|
||||
>;
|
||||
[TableName.ScimToken]: Knex.CompositeTableType<TScimTokens, TScimTokensInsert, TScimTokensUpdate>;
|
||||
[TableName.SecretApprovalPolicy]: Knex.CompositeTableType<
|
||||
TSecretApprovalPolicies,
|
||||
@ -340,6 +390,12 @@ declare module "knex/types/tables" {
|
||||
TSecretSnapshotFoldersInsert,
|
||||
TSecretSnapshotFoldersUpdate
|
||||
>;
|
||||
[TableName.DynamicSecret]: Knex.CompositeTableType<TDynamicSecrets, TDynamicSecretsInsert, TDynamicSecretsUpdate>;
|
||||
[TableName.DynamicSecretLease]: Knex.CompositeTableType<
|
||||
TDynamicSecretLeases,
|
||||
TDynamicSecretLeasesInsert,
|
||||
TDynamicSecretLeasesUpdate
|
||||
>;
|
||||
[TableName.SamlConfig]: Knex.CompositeTableType<TSamlConfigs, TSamlConfigsInsert, TSamlConfigsUpdate>;
|
||||
[TableName.LdapConfig]: Knex.CompositeTableType<TLdapConfigs, TLdapConfigsInsert, TLdapConfigsUpdate>;
|
||||
[TableName.OrgBot]: Knex.CompositeTableType<TOrgBots, TOrgBotsInsert, TOrgBotsUpdate>;
|
||||
|
58
backend/src/db/migrations/20240318164718_dynamic-secret.ts
Normal file
58
backend/src/db/migrations/20240318164718_dynamic-secret.ts
Normal file
@ -0,0 +1,58 @@
|
||||
import { Knex } from "knex";
|
||||
|
||||
import { SecretEncryptionAlgo, SecretKeyEncoding, TableName } from "../schemas";
|
||||
import { createOnUpdateTrigger, dropOnUpdateTrigger } from "../utils";
|
||||
|
||||
export async function up(knex: Knex): Promise<void> {
|
||||
const doesTableExist = await knex.schema.hasTable(TableName.DynamicSecret);
|
||||
if (!doesTableExist) {
|
||||
await knex.schema.createTable(TableName.DynamicSecret, (t) => {
|
||||
t.uuid("id", { primaryKey: true }).defaultTo(knex.fn.uuid());
|
||||
t.string("name").notNullable();
|
||||
t.integer("version").notNullable();
|
||||
t.string("type").notNullable();
|
||||
t.string("defaultTTL").notNullable();
|
||||
t.string("maxTTL");
|
||||
t.string("inputIV").notNullable();
|
||||
t.text("inputCiphertext").notNullable();
|
||||
t.string("inputTag").notNullable();
|
||||
t.string("algorithm").notNullable().defaultTo(SecretEncryptionAlgo.AES_256_GCM);
|
||||
t.string("keyEncoding").notNullable().defaultTo(SecretKeyEncoding.UTF8);
|
||||
t.uuid("folderId").notNullable();
|
||||
// for background process communication
|
||||
t.string("status");
|
||||
t.string("statusDetails");
|
||||
t.foreign("folderId").references("id").inTable(TableName.SecretFolder).onDelete("CASCADE");
|
||||
t.unique(["name", "folderId"]);
|
||||
t.timestamps(true, true, true);
|
||||
});
|
||||
}
|
||||
|
||||
await createOnUpdateTrigger(knex, TableName.DynamicSecret);
|
||||
|
||||
const doesTableDynamicSecretLease = await knex.schema.hasTable(TableName.DynamicSecretLease);
|
||||
if (!doesTableDynamicSecretLease) {
|
||||
await knex.schema.createTable(TableName.DynamicSecretLease, (t) => {
|
||||
t.uuid("id", { primaryKey: true }).defaultTo(knex.fn.uuid());
|
||||
t.integer("version").notNullable();
|
||||
t.string("externalEntityId").notNullable();
|
||||
t.datetime("expireAt").notNullable();
|
||||
// for background process communication
|
||||
t.string("status");
|
||||
t.string("statusDetails");
|
||||
t.uuid("dynamicSecretId").notNullable();
|
||||
t.foreign("dynamicSecretId").references("id").inTable(TableName.DynamicSecret).onDelete("CASCADE");
|
||||
t.timestamps(true, true, true);
|
||||
});
|
||||
}
|
||||
|
||||
await createOnUpdateTrigger(knex, TableName.DynamicSecretLease);
|
||||
}
|
||||
|
||||
export async function down(knex: Knex): Promise<void> {
|
||||
await dropOnUpdateTrigger(knex, TableName.DynamicSecretLease);
|
||||
await knex.schema.dropTableIfExists(TableName.DynamicSecretLease);
|
||||
|
||||
await dropOnUpdateTrigger(knex, TableName.DynamicSecret);
|
||||
await knex.schema.dropTableIfExists(TableName.DynamicSecret);
|
||||
}
|
@ -0,0 +1,29 @@
|
||||
import { Knex } from "knex";
|
||||
|
||||
import { TableName } from "../schemas";
|
||||
import { createOnUpdateTrigger, dropOnUpdateTrigger } from "../utils";
|
||||
|
||||
export async function up(knex: Knex): Promise<void> {
|
||||
if (!(await knex.schema.hasTable(TableName.ProjectUserAdditionalPrivilege))) {
|
||||
await knex.schema.createTable(TableName.ProjectUserAdditionalPrivilege, (t) => {
|
||||
t.uuid("id", { primaryKey: true }).defaultTo(knex.fn.uuid());
|
||||
t.string("slug", 60).notNullable();
|
||||
t.uuid("projectMembershipId").notNullable();
|
||||
t.foreign("projectMembershipId").references("id").inTable(TableName.ProjectMembership).onDelete("CASCADE");
|
||||
t.boolean("isTemporary").notNullable().defaultTo(false);
|
||||
t.string("temporaryMode");
|
||||
t.string("temporaryRange"); // could be cron or relative time like 1H or 1minute etc
|
||||
t.datetime("temporaryAccessStartTime");
|
||||
t.datetime("temporaryAccessEndTime");
|
||||
t.jsonb("permissions").notNullable();
|
||||
t.timestamps(true, true, true);
|
||||
});
|
||||
}
|
||||
|
||||
await createOnUpdateTrigger(knex, TableName.ProjectUserAdditionalPrivilege);
|
||||
}
|
||||
|
||||
export async function down(knex: Knex): Promise<void> {
|
||||
await dropOnUpdateTrigger(knex, TableName.ProjectUserAdditionalPrivilege);
|
||||
await knex.schema.dropTableIfExists(TableName.ProjectUserAdditionalPrivilege);
|
||||
}
|
@ -0,0 +1,32 @@
|
||||
import { Knex } from "knex";
|
||||
|
||||
import { TableName } from "../schemas";
|
||||
import { createOnUpdateTrigger, dropOnUpdateTrigger } from "../utils";
|
||||
|
||||
export async function up(knex: Knex): Promise<void> {
|
||||
if (!(await knex.schema.hasTable(TableName.IdentityProjectAdditionalPrivilege))) {
|
||||
await knex.schema.createTable(TableName.IdentityProjectAdditionalPrivilege, (t) => {
|
||||
t.uuid("id", { primaryKey: true }).defaultTo(knex.fn.uuid());
|
||||
t.string("slug", 60).notNullable();
|
||||
t.uuid("projectMembershipId").notNullable();
|
||||
t.foreign("projectMembershipId")
|
||||
.references("id")
|
||||
.inTable(TableName.IdentityProjectMembership)
|
||||
.onDelete("CASCADE");
|
||||
t.boolean("isTemporary").notNullable().defaultTo(false);
|
||||
t.string("temporaryMode");
|
||||
t.string("temporaryRange"); // could be cron or relative time like 1H or 1minute etc
|
||||
t.datetime("temporaryAccessStartTime");
|
||||
t.datetime("temporaryAccessEndTime");
|
||||
t.jsonb("permissions").notNullable();
|
||||
t.timestamps(true, true, true);
|
||||
});
|
||||
}
|
||||
|
||||
await createOnUpdateTrigger(knex, TableName.IdentityProjectAdditionalPrivilege);
|
||||
}
|
||||
|
||||
export async function down(knex: Knex): Promise<void> {
|
||||
await dropOnUpdateTrigger(knex, TableName.IdentityProjectAdditionalPrivilege);
|
||||
await knex.schema.dropTableIfExists(TableName.IdentityProjectAdditionalPrivilege);
|
||||
}
|
@ -0,0 +1,111 @@
|
||||
import { Knex } from "knex";
|
||||
import { z } from "zod";
|
||||
|
||||
import { TableName, TOrgMemberships } from "../schemas";
|
||||
|
||||
const validateOrgMembership = (membershipToValidate: TOrgMemberships, firstMembership: TOrgMemberships) => {
|
||||
const firstOrgId = firstMembership.orgId;
|
||||
const firstUserId = firstMembership.userId;
|
||||
|
||||
if (membershipToValidate.id === firstMembership.id) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (membershipToValidate.inviteEmail !== firstMembership.inviteEmail) {
|
||||
throw new Error(`Invite emails are different for the same userId and orgId: ${firstUserId}, ${firstOrgId}`);
|
||||
}
|
||||
if (membershipToValidate.orgId !== firstMembership.orgId) {
|
||||
throw new Error(`OrgIds are different for the same userId and orgId: ${firstUserId}, ${firstOrgId}`);
|
||||
}
|
||||
if (membershipToValidate.role !== firstMembership.role) {
|
||||
throw new Error(`Roles are different for the same userId and orgId: ${firstUserId}, ${firstOrgId}`);
|
||||
}
|
||||
if (membershipToValidate.roleId !== firstMembership.roleId) {
|
||||
throw new Error(`RoleIds are different for the same userId and orgId: ${firstUserId}, ${firstOrgId}`);
|
||||
}
|
||||
if (membershipToValidate.status !== firstMembership.status) {
|
||||
throw new Error(`Statuses are different for the same userId and orgId: ${firstUserId}, ${firstOrgId}`);
|
||||
}
|
||||
if (membershipToValidate.userId !== firstMembership.userId) {
|
||||
throw new Error(`UserIds are different for the same userId and orgId: ${firstUserId}, ${firstOrgId}`);
|
||||
}
|
||||
};
|
||||
|
||||
export async function up(knex: Knex): Promise<void> {
|
||||
const RowSchema = z.object({
|
||||
userId: z.string(),
|
||||
orgId: z.string(),
|
||||
cnt: z.string()
|
||||
});
|
||||
|
||||
// Transactional find and delete duplicate rows
|
||||
await knex.transaction(async (tx) => {
|
||||
const duplicateRows = await tx(TableName.OrgMembership)
|
||||
.select("userId", "orgId") // Select the userId and orgId so we can group by them
|
||||
.count("* as cnt") // Count the number of rows for each userId and orgId, so we can make sure there are more than 1 row (a duplicate)
|
||||
.groupBy("userId", "orgId")
|
||||
.havingRaw("count(*) > ?", [1]); // Using havingRaw for direct SQL expressions
|
||||
|
||||
// Parse the rows to ensure they are in the correct format, and for type safety
|
||||
const parsedRows = RowSchema.array().parse(duplicateRows);
|
||||
|
||||
// For each of the duplicate rows, loop through and find the actual memberships to delete
|
||||
for (const row of parsedRows) {
|
||||
const count = Number(row.cnt);
|
||||
|
||||
// An extra check to ensure that the count is actually a number, and the number is greater than 2
|
||||
if (typeof count !== "number" || count < 2) {
|
||||
// eslint-disable-next-line no-continue
|
||||
continue;
|
||||
}
|
||||
|
||||
// Find all the organization memberships that have the same userId and orgId
|
||||
// eslint-disable-next-line no-await-in-loop
|
||||
const rowsToDelete = await tx(TableName.OrgMembership).where({
|
||||
userId: row.userId,
|
||||
orgId: row.orgId
|
||||
});
|
||||
|
||||
// Ensure that all the rows have exactly the same value, except id, createdAt, updatedAt
|
||||
for (const rowToDelete of rowsToDelete) {
|
||||
validateOrgMembership(rowToDelete, rowsToDelete[0]);
|
||||
}
|
||||
|
||||
// Find the row with the latest createdAt, which we will keep
|
||||
|
||||
let lowestCreatedAt: number | null = null;
|
||||
let latestCreatedRow: TOrgMemberships | null = null;
|
||||
|
||||
for (const rowToDelete of rowsToDelete) {
|
||||
if (lowestCreatedAt === null || rowToDelete.createdAt.getTime() < lowestCreatedAt) {
|
||||
lowestCreatedAt = rowToDelete.createdAt.getTime();
|
||||
latestCreatedRow = rowToDelete;
|
||||
}
|
||||
}
|
||||
if (!latestCreatedRow) {
|
||||
throw new Error("Failed to find last created membership");
|
||||
}
|
||||
|
||||
// Filter out the latest row from the rows to delete
|
||||
const membershipIdsToDelete = rowsToDelete.map((r) => r.id).filter((id) => id !== latestCreatedRow!.id);
|
||||
|
||||
// eslint-disable-next-line no-await-in-loop
|
||||
const numberOfRowsDeleted = await tx(TableName.OrgMembership).whereIn("id", membershipIdsToDelete).delete();
|
||||
|
||||
// eslint-disable-next-line no-console
|
||||
console.log(
|
||||
`Deleted ${numberOfRowsDeleted} duplicate organization memberships for ${row.userId} and ${row.orgId}`
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
await knex.schema.alterTable(TableName.OrgMembership, (table) => {
|
||||
table.unique(["userId", "orgId"]);
|
||||
});
|
||||
}
|
||||
|
||||
export async function down(knex: Knex): Promise<void> {
|
||||
await knex.schema.alterTable(TableName.OrgMembership, (table) => {
|
||||
table.dropUnique(["userId", "orgId"]);
|
||||
});
|
||||
}
|
82
backend/src/db/migrations/20240412174842_group.ts
Normal file
82
backend/src/db/migrations/20240412174842_group.ts
Normal file
@ -0,0 +1,82 @@
|
||||
import { Knex } from "knex";
|
||||
|
||||
import { TableName } from "../schemas";
|
||||
import { createOnUpdateTrigger, dropOnUpdateTrigger } from "../utils";
|
||||
|
||||
export async function up(knex: Knex): Promise<void> {
|
||||
if (!(await knex.schema.hasTable(TableName.Groups))) {
|
||||
await knex.schema.createTable(TableName.Groups, (t) => {
|
||||
t.uuid("id", { primaryKey: true }).defaultTo(knex.fn.uuid());
|
||||
t.uuid("orgId").notNullable();
|
||||
t.foreign("orgId").references("id").inTable(TableName.Organization).onDelete("CASCADE");
|
||||
t.string("name").notNullable();
|
||||
t.string("slug").notNullable();
|
||||
t.unique(["orgId", "slug"]);
|
||||
t.string("role").notNullable();
|
||||
t.uuid("roleId");
|
||||
t.foreign("roleId").references("id").inTable(TableName.OrgRoles);
|
||||
t.timestamps(true, true, true);
|
||||
});
|
||||
}
|
||||
|
||||
await createOnUpdateTrigger(knex, TableName.Groups);
|
||||
|
||||
if (!(await knex.schema.hasTable(TableName.UserGroupMembership))) {
|
||||
await knex.schema.createTable(TableName.UserGroupMembership, (t) => {
|
||||
t.uuid("id", { primaryKey: true }).defaultTo(knex.fn.uuid()); // link to user and link to groups cascade on groups
|
||||
t.uuid("userId").notNullable();
|
||||
t.foreign("userId").references("id").inTable(TableName.Users).onDelete("CASCADE");
|
||||
t.uuid("groupId").notNullable();
|
||||
t.foreign("groupId").references("id").inTable(TableName.Groups).onDelete("CASCADE");
|
||||
t.timestamps(true, true, true);
|
||||
});
|
||||
}
|
||||
|
||||
await createOnUpdateTrigger(knex, TableName.UserGroupMembership);
|
||||
|
||||
if (!(await knex.schema.hasTable(TableName.GroupProjectMembership))) {
|
||||
await knex.schema.createTable(TableName.GroupProjectMembership, (t) => {
|
||||
t.uuid("id", { primaryKey: true }).defaultTo(knex.fn.uuid());
|
||||
t.string("projectId").notNullable();
|
||||
t.foreign("projectId").references("id").inTable(TableName.Project).onDelete("CASCADE");
|
||||
t.uuid("groupId").notNullable();
|
||||
t.foreign("groupId").references("id").inTable(TableName.Groups).onDelete("CASCADE");
|
||||
t.timestamps(true, true, true);
|
||||
});
|
||||
}
|
||||
await createOnUpdateTrigger(knex, TableName.GroupProjectMembership);
|
||||
|
||||
if (!(await knex.schema.hasTable(TableName.GroupProjectMembershipRole))) {
|
||||
await knex.schema.createTable(TableName.GroupProjectMembershipRole, (t) => {
|
||||
t.uuid("id", { primaryKey: true }).defaultTo(knex.fn.uuid());
|
||||
t.string("role").notNullable();
|
||||
t.uuid("projectMembershipId").notNullable();
|
||||
t.foreign("projectMembershipId").references("id").inTable(TableName.GroupProjectMembership).onDelete("CASCADE");
|
||||
// until role is changed/removed the role should not deleted
|
||||
t.uuid("customRoleId");
|
||||
t.foreign("customRoleId").references("id").inTable(TableName.ProjectRoles);
|
||||
t.boolean("isTemporary").notNullable().defaultTo(false);
|
||||
t.string("temporaryMode");
|
||||
t.string("temporaryRange"); // could be cron or relative time like 1H or 1minute etc
|
||||
t.datetime("temporaryAccessStartTime");
|
||||
t.datetime("temporaryAccessEndTime");
|
||||
t.timestamps(true, true, true);
|
||||
});
|
||||
}
|
||||
|
||||
await createOnUpdateTrigger(knex, TableName.GroupProjectMembershipRole);
|
||||
}
|
||||
|
||||
export async function down(knex: Knex): Promise<void> {
|
||||
await knex.schema.dropTableIfExists(TableName.GroupProjectMembershipRole);
|
||||
await dropOnUpdateTrigger(knex, TableName.GroupProjectMembershipRole);
|
||||
|
||||
await knex.schema.dropTableIfExists(TableName.UserGroupMembership);
|
||||
await dropOnUpdateTrigger(knex, TableName.UserGroupMembership);
|
||||
|
||||
await knex.schema.dropTableIfExists(TableName.GroupProjectMembership);
|
||||
await dropOnUpdateTrigger(knex, TableName.GroupProjectMembership);
|
||||
|
||||
await knex.schema.dropTableIfExists(TableName.Groups);
|
||||
await dropOnUpdateTrigger(knex, TableName.Groups);
|
||||
}
|
24
backend/src/db/schemas/dynamic-secret-leases.ts
Normal file
24
backend/src/db/schemas/dynamic-secret-leases.ts
Normal file
@ -0,0 +1,24 @@
|
||||
// Code generated by automation script, DO NOT EDIT.
|
||||
// Automated by pulling database and generating zod schema
|
||||
// To update. Just run npm run generate:schema
|
||||
// Written by akhilmhdh.
|
||||
|
||||
import { z } from "zod";
|
||||
|
||||
import { TImmutableDBKeys } from "./models";
|
||||
|
||||
export const DynamicSecretLeasesSchema = z.object({
|
||||
id: z.string().uuid(),
|
||||
version: z.number(),
|
||||
externalEntityId: z.string(),
|
||||
expireAt: z.date(),
|
||||
status: z.string().nullable().optional(),
|
||||
statusDetails: z.string().nullable().optional(),
|
||||
dynamicSecretId: z.string().uuid(),
|
||||
createdAt: z.date(),
|
||||
updatedAt: z.date()
|
||||
});
|
||||
|
||||
export type TDynamicSecretLeases = z.infer<typeof DynamicSecretLeasesSchema>;
|
||||
export type TDynamicSecretLeasesInsert = Omit<z.input<typeof DynamicSecretLeasesSchema>, TImmutableDBKeys>;
|
||||
export type TDynamicSecretLeasesUpdate = Partial<Omit<z.input<typeof DynamicSecretLeasesSchema>, TImmutableDBKeys>>;
|
31
backend/src/db/schemas/dynamic-secrets.ts
Normal file
31
backend/src/db/schemas/dynamic-secrets.ts
Normal file
@ -0,0 +1,31 @@
|
||||
// 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 DynamicSecretsSchema = z.object({
|
||||
id: z.string().uuid(),
|
||||
name: z.string(),
|
||||
version: z.number(),
|
||||
type: z.string(),
|
||||
defaultTTL: z.string(),
|
||||
maxTTL: z.string().nullable().optional(),
|
||||
inputIV: z.string(),
|
||||
inputCiphertext: z.string(),
|
||||
inputTag: z.string(),
|
||||
algorithm: z.string().default("aes-256-gcm"),
|
||||
keyEncoding: z.string().default("utf8"),
|
||||
folderId: z.string().uuid(),
|
||||
status: z.string().nullable().optional(),
|
||||
statusDetails: z.string().nullable().optional(),
|
||||
createdAt: z.date(),
|
||||
updatedAt: z.date()
|
||||
});
|
||||
|
||||
export type TDynamicSecrets = z.infer<typeof DynamicSecretsSchema>;
|
||||
export type TDynamicSecretsInsert = Omit<z.input<typeof DynamicSecretsSchema>, TImmutableDBKeys>;
|
||||
export type TDynamicSecretsUpdate = Partial<Omit<z.input<typeof DynamicSecretsSchema>, TImmutableDBKeys>>;
|
31
backend/src/db/schemas/group-project-membership-roles.ts
Normal file
31
backend/src/db/schemas/group-project-membership-roles.ts
Normal file
@ -0,0 +1,31 @@
|
||||
// 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 GroupProjectMembershipRolesSchema = z.object({
|
||||
id: z.string().uuid(),
|
||||
role: z.string(),
|
||||
projectMembershipId: z.string().uuid(),
|
||||
customRoleId: z.string().uuid().nullable().optional(),
|
||||
isTemporary: z.boolean().default(false),
|
||||
temporaryMode: z.string().nullable().optional(),
|
||||
temporaryRange: z.string().nullable().optional(),
|
||||
temporaryAccessStartTime: z.date().nullable().optional(),
|
||||
temporaryAccessEndTime: z.date().nullable().optional(),
|
||||
createdAt: z.date(),
|
||||
updatedAt: z.date()
|
||||
});
|
||||
|
||||
export type TGroupProjectMembershipRoles = z.infer<typeof GroupProjectMembershipRolesSchema>;
|
||||
export type TGroupProjectMembershipRolesInsert = Omit<
|
||||
z.input<typeof GroupProjectMembershipRolesSchema>,
|
||||
TImmutableDBKeys
|
||||
>;
|
||||
export type TGroupProjectMembershipRolesUpdate = Partial<
|
||||
Omit<z.input<typeof GroupProjectMembershipRolesSchema>, TImmutableDBKeys>
|
||||
>;
|
22
backend/src/db/schemas/group-project-memberships.ts
Normal file
22
backend/src/db/schemas/group-project-memberships.ts
Normal file
@ -0,0 +1,22 @@
|
||||
// Code generated by automation script, DO NOT EDIT.
|
||||
// Automated by pulling database and generating zod schema
|
||||
// To update. Just run npm run generate:schema
|
||||
// Written by akhilmhdh.
|
||||
|
||||
import { z } from "zod";
|
||||
|
||||
import { TImmutableDBKeys } from "./models";
|
||||
|
||||
export const GroupProjectMembershipsSchema = z.object({
|
||||
id: z.string().uuid(),
|
||||
projectId: z.string(),
|
||||
groupId: z.string().uuid(),
|
||||
createdAt: z.date(),
|
||||
updatedAt: z.date()
|
||||
});
|
||||
|
||||
export type TGroupProjectMemberships = z.infer<typeof GroupProjectMembershipsSchema>;
|
||||
export type TGroupProjectMembershipsInsert = Omit<z.input<typeof GroupProjectMembershipsSchema>, TImmutableDBKeys>;
|
||||
export type TGroupProjectMembershipsUpdate = Partial<
|
||||
Omit<z.input<typeof GroupProjectMembershipsSchema>, TImmutableDBKeys>
|
||||
>;
|
23
backend/src/db/schemas/groups.ts
Normal file
23
backend/src/db/schemas/groups.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 GroupsSchema = z.object({
|
||||
id: z.string().uuid(),
|
||||
orgId: z.string().uuid(),
|
||||
name: z.string(),
|
||||
slug: z.string(),
|
||||
role: z.string(),
|
||||
roleId: z.string().uuid().nullable().optional(),
|
||||
createdAt: z.date(),
|
||||
updatedAt: z.date()
|
||||
});
|
||||
|
||||
export type TGroups = z.infer<typeof GroupsSchema>;
|
||||
export type TGroupsInsert = Omit<z.input<typeof GroupsSchema>, TImmutableDBKeys>;
|
||||
export type TGroupsUpdate = Partial<Omit<z.input<typeof GroupsSchema>, TImmutableDBKeys>>;
|
@ -0,0 +1,31 @@
|
||||
// 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 IdentityProjectAdditionalPrivilegeSchema = z.object({
|
||||
id: z.string().uuid(),
|
||||
slug: z.string(),
|
||||
projectMembershipId: z.string().uuid(),
|
||||
isTemporary: z.boolean().default(false),
|
||||
temporaryMode: z.string().nullable().optional(),
|
||||
temporaryRange: z.string().nullable().optional(),
|
||||
temporaryAccessStartTime: z.date().nullable().optional(),
|
||||
temporaryAccessEndTime: z.date().nullable().optional(),
|
||||
permissions: z.unknown(),
|
||||
createdAt: z.date(),
|
||||
updatedAt: z.date()
|
||||
});
|
||||
|
||||
export type TIdentityProjectAdditionalPrivilege = z.infer<typeof IdentityProjectAdditionalPrivilegeSchema>;
|
||||
export type TIdentityProjectAdditionalPrivilegeInsert = Omit<
|
||||
z.input<typeof IdentityProjectAdditionalPrivilegeSchema>,
|
||||
TImmutableDBKeys
|
||||
>;
|
||||
export type TIdentityProjectAdditionalPrivilegeUpdate = Partial<
|
||||
Omit<z.input<typeof IdentityProjectAdditionalPrivilegeSchema>, TImmutableDBKeys>
|
||||
>;
|
@ -3,11 +3,17 @@ export * from "./audit-logs";
|
||||
export * from "./auth-token-sessions";
|
||||
export * from "./auth-tokens";
|
||||
export * from "./backup-private-key";
|
||||
export * from "./dynamic-secret-leases";
|
||||
export * from "./dynamic-secrets";
|
||||
export * from "./git-app-install-sessions";
|
||||
export * from "./git-app-org";
|
||||
export * from "./group-project-membership-roles";
|
||||
export * from "./group-project-memberships";
|
||||
export * from "./groups";
|
||||
export * from "./identities";
|
||||
export * from "./identity-access-tokens";
|
||||
export * from "./identity-org-memberships";
|
||||
export * from "./identity-project-additional-privilege";
|
||||
export * from "./identity-project-membership-role";
|
||||
export * from "./identity-project-memberships";
|
||||
export * from "./identity-ua-client-secrets";
|
||||
@ -26,6 +32,7 @@ export * from "./project-environments";
|
||||
export * from "./project-keys";
|
||||
export * from "./project-memberships";
|
||||
export * from "./project-roles";
|
||||
export * from "./project-user-additional-privilege";
|
||||
export * from "./project-user-membership-roles";
|
||||
export * from "./projects";
|
||||
export * from "./saml-configs";
|
||||
@ -57,5 +64,6 @@ export * from "./trusted-ips";
|
||||
export * from "./user-actions";
|
||||
export * from "./user-aliases";
|
||||
export * from "./user-encryption-keys";
|
||||
export * from "./user-group-membership";
|
||||
export * from "./users";
|
||||
export * from "./webhooks";
|
||||
|
@ -2,6 +2,10 @@ import { z } from "zod";
|
||||
|
||||
export enum TableName {
|
||||
Users = "users",
|
||||
Groups = "groups",
|
||||
GroupProjectMembership = "group_project_memberships",
|
||||
GroupProjectMembershipRole = "group_project_membership_roles",
|
||||
UserGroupMembership = "user_group_membership",
|
||||
UserAliases = "user_aliases",
|
||||
UserEncryptionKey = "user_encryption_keys",
|
||||
AuthTokens = "auth_tokens",
|
||||
@ -20,6 +24,7 @@ export enum TableName {
|
||||
Environment = "project_environments",
|
||||
ProjectMembership = "project_memberships",
|
||||
ProjectRoles = "project_roles",
|
||||
ProjectUserAdditionalPrivilege = "project_user_additional_privilege",
|
||||
ProjectUserMembershipRole = "project_user_membership_roles",
|
||||
ProjectKeys = "project_keys",
|
||||
Secret = "secrets",
|
||||
@ -43,6 +48,7 @@ export enum TableName {
|
||||
IdentityOrgMembership = "identity_org_memberships",
|
||||
IdentityProjectMembership = "identity_project_memberships",
|
||||
IdentityProjectMembershipRole = "identity_project_membership_role",
|
||||
IdentityProjectAdditionalPrivilege = "identity_project_additional_privilege",
|
||||
ScimToken = "scim_tokens",
|
||||
SecretApprovalPolicy = "secret_approval_policies",
|
||||
SecretApprovalPolicyApprover = "secret_approval_policies_approvers",
|
||||
@ -59,6 +65,8 @@ export enum TableName {
|
||||
GitAppOrg = "git_app_org",
|
||||
SecretScanningGitRisk = "secret_scanning_git_risks",
|
||||
TrustedIps = "trusted_ips",
|
||||
DynamicSecret = "dynamic_secrets",
|
||||
DynamicSecretLease = "dynamic_secret_leases",
|
||||
// junction tables with tags
|
||||
JnSecretTag = "secret_tag_junction",
|
||||
SecretVersionTag = "secret_version_tag_junction"
|
||||
|
31
backend/src/db/schemas/project-user-additional-privilege.ts
Normal file
31
backend/src/db/schemas/project-user-additional-privilege.ts
Normal file
@ -0,0 +1,31 @@
|
||||
// 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 ProjectUserAdditionalPrivilegeSchema = z.object({
|
||||
id: z.string().uuid(),
|
||||
slug: z.string(),
|
||||
projectMembershipId: z.string().uuid(),
|
||||
isTemporary: z.boolean().default(false),
|
||||
temporaryMode: z.string().nullable().optional(),
|
||||
temporaryRange: z.string().nullable().optional(),
|
||||
temporaryAccessStartTime: z.date().nullable().optional(),
|
||||
temporaryAccessEndTime: z.date().nullable().optional(),
|
||||
permissions: z.unknown(),
|
||||
createdAt: z.date(),
|
||||
updatedAt: z.date()
|
||||
});
|
||||
|
||||
export type TProjectUserAdditionalPrivilege = z.infer<typeof ProjectUserAdditionalPrivilegeSchema>;
|
||||
export type TProjectUserAdditionalPrivilegeInsert = Omit<
|
||||
z.input<typeof ProjectUserAdditionalPrivilegeSchema>,
|
||||
TImmutableDBKeys
|
||||
>;
|
||||
export type TProjectUserAdditionalPrivilegeUpdate = Partial<
|
||||
Omit<z.input<typeof ProjectUserAdditionalPrivilegeSchema>, TImmutableDBKeys>
|
||||
>;
|
20
backend/src/db/schemas/user-group-membership.ts
Normal file
20
backend/src/db/schemas/user-group-membership.ts
Normal file
@ -0,0 +1,20 @@
|
||||
// Code generated by automation script, DO NOT EDIT.
|
||||
// Automated by pulling database and generating zod schema
|
||||
// To update. Just run npm run generate:schema
|
||||
// Written by akhilmhdh.
|
||||
|
||||
import { z } from "zod";
|
||||
|
||||
import { TImmutableDBKeys } from "./models";
|
||||
|
||||
export const UserGroupMembershipSchema = z.object({
|
||||
id: z.string().uuid(),
|
||||
userId: z.string().uuid(),
|
||||
groupId: z.string().uuid(),
|
||||
createdAt: z.date(),
|
||||
updatedAt: z.date()
|
||||
});
|
||||
|
||||
export type TUserGroupMembership = z.infer<typeof UserGroupMembershipSchema>;
|
||||
export type TUserGroupMembershipInsert = Omit<z.input<typeof UserGroupMembershipSchema>, TImmutableDBKeys>;
|
||||
export type TUserGroupMembershipUpdate = Partial<Omit<z.input<typeof UserGroupMembershipSchema>, TImmutableDBKeys>>;
|
197
backend/src/ee/routes/v1/dynamic-secret-lease-router.ts
Normal file
197
backend/src/ee/routes/v1/dynamic-secret-lease-router.ts
Normal file
@ -0,0 +1,197 @@
|
||||
import ms from "ms";
|
||||
import { z } from "zod";
|
||||
|
||||
import { DynamicSecretLeasesSchema } from "@app/db/schemas";
|
||||
import { DYNAMIC_SECRET_LEASES } from "@app/lib/api-docs";
|
||||
import { daysToMillisecond } from "@app/lib/dates";
|
||||
import { removeTrailingSlash } from "@app/lib/fn";
|
||||
import { readLimit, writeLimit } from "@app/server/config/rateLimiter";
|
||||
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
|
||||
import { SanitizedDynamicSecretSchema } from "@app/server/routes/sanitizedSchemas";
|
||||
import { AuthMode } from "@app/services/auth/auth-type";
|
||||
|
||||
export const registerDynamicSecretLeaseRouter = async (server: FastifyZodProvider) => {
|
||||
server.route({
|
||||
method: "POST",
|
||||
url: "/",
|
||||
config: {
|
||||
rateLimit: writeLimit
|
||||
},
|
||||
schema: {
|
||||
body: z.object({
|
||||
dynamicSecretName: z.string().min(1).describe(DYNAMIC_SECRET_LEASES.CREATE.dynamicSecretName).toLowerCase(),
|
||||
projectSlug: z.string().min(1).describe(DYNAMIC_SECRET_LEASES.CREATE.projectSlug),
|
||||
ttl: z
|
||||
.string()
|
||||
.optional()
|
||||
.describe(DYNAMIC_SECRET_LEASES.CREATE.ttl)
|
||||
.superRefine((val, ctx) => {
|
||||
if (!val) return;
|
||||
const valMs = ms(val);
|
||||
if (valMs < 60 * 1000)
|
||||
ctx.addIssue({ code: z.ZodIssueCode.custom, message: "TTL must be a greater than 1min" });
|
||||
if (valMs > daysToMillisecond(1))
|
||||
ctx.addIssue({ code: z.ZodIssueCode.custom, message: "TTL must be less than a day" });
|
||||
}),
|
||||
path: z.string().trim().default("/").transform(removeTrailingSlash).describe(DYNAMIC_SECRET_LEASES.CREATE.path),
|
||||
environmentSlug: z.string().min(1).describe(DYNAMIC_SECRET_LEASES.CREATE.path)
|
||||
}),
|
||||
response: {
|
||||
200: z.object({
|
||||
lease: DynamicSecretLeasesSchema,
|
||||
dynamicSecret: SanitizedDynamicSecretSchema,
|
||||
data: z.unknown()
|
||||
})
|
||||
}
|
||||
},
|
||||
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
||||
handler: async (req) => {
|
||||
const { data, lease, dynamicSecret } = await server.services.dynamicSecretLease.create({
|
||||
actor: req.permission.type,
|
||||
actorId: req.permission.id,
|
||||
actorAuthMethod: req.permission.authMethod,
|
||||
actorOrgId: req.permission.orgId,
|
||||
name: req.body.dynamicSecretName,
|
||||
...req.body
|
||||
});
|
||||
return { lease, data, dynamicSecret };
|
||||
}
|
||||
});
|
||||
|
||||
server.route({
|
||||
method: "DELETE",
|
||||
url: "/:leaseId",
|
||||
config: {
|
||||
rateLimit: writeLimit
|
||||
},
|
||||
schema: {
|
||||
params: z.object({
|
||||
leaseId: z.string().min(1).describe(DYNAMIC_SECRET_LEASES.DELETE.leaseId)
|
||||
}),
|
||||
body: z.object({
|
||||
projectSlug: z.string().min(1).describe(DYNAMIC_SECRET_LEASES.DELETE.projectSlug),
|
||||
path: z
|
||||
.string()
|
||||
.min(1)
|
||||
.trim()
|
||||
.default("/")
|
||||
.transform(removeTrailingSlash)
|
||||
.describe(DYNAMIC_SECRET_LEASES.DELETE.path),
|
||||
environmentSlug: z.string().min(1).describe(DYNAMIC_SECRET_LEASES.DELETE.environmentSlug),
|
||||
isForced: z.boolean().default(false).describe(DYNAMIC_SECRET_LEASES.DELETE.isForced)
|
||||
}),
|
||||
response: {
|
||||
200: z.object({
|
||||
lease: DynamicSecretLeasesSchema
|
||||
})
|
||||
}
|
||||
},
|
||||
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
||||
handler: async (req) => {
|
||||
const lease = await server.services.dynamicSecretLease.revokeLease({
|
||||
actor: req.permission.type,
|
||||
actorId: req.permission.id,
|
||||
actorAuthMethod: req.permission.authMethod,
|
||||
actorOrgId: req.permission.orgId,
|
||||
leaseId: req.params.leaseId,
|
||||
...req.body
|
||||
});
|
||||
return { lease };
|
||||
}
|
||||
});
|
||||
|
||||
server.route({
|
||||
method: "POST",
|
||||
url: "/:leaseId/renew",
|
||||
config: {
|
||||
rateLimit: writeLimit
|
||||
},
|
||||
schema: {
|
||||
params: z.object({
|
||||
leaseId: z.string().min(1).describe(DYNAMIC_SECRET_LEASES.RENEW.leaseId)
|
||||
}),
|
||||
body: z.object({
|
||||
ttl: z
|
||||
.string()
|
||||
.describe(DYNAMIC_SECRET_LEASES.RENEW.ttl)
|
||||
.optional()
|
||||
.superRefine((val, ctx) => {
|
||||
if (!val) return;
|
||||
const valMs = ms(val);
|
||||
if (valMs < 60 * 1000)
|
||||
ctx.addIssue({ code: z.ZodIssueCode.custom, message: "TTL must be a greater than 1min" });
|
||||
if (valMs > daysToMillisecond(1))
|
||||
ctx.addIssue({ code: z.ZodIssueCode.custom, message: "TTL must be less than a day" });
|
||||
}),
|
||||
projectSlug: z.string().min(1).describe(DYNAMIC_SECRET_LEASES.RENEW.projectSlug),
|
||||
path: z
|
||||
.string()
|
||||
.min(1)
|
||||
.trim()
|
||||
.default("/")
|
||||
.transform(removeTrailingSlash)
|
||||
.describe(DYNAMIC_SECRET_LEASES.RENEW.path),
|
||||
environmentSlug: z.string().min(1).describe(DYNAMIC_SECRET_LEASES.RENEW.ttl)
|
||||
}),
|
||||
response: {
|
||||
200: z.object({
|
||||
lease: DynamicSecretLeasesSchema
|
||||
})
|
||||
}
|
||||
},
|
||||
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
||||
handler: async (req) => {
|
||||
const lease = await server.services.dynamicSecretLease.renewLease({
|
||||
actor: req.permission.type,
|
||||
actorId: req.permission.id,
|
||||
actorAuthMethod: req.permission.authMethod,
|
||||
actorOrgId: req.permission.orgId,
|
||||
leaseId: req.params.leaseId,
|
||||
...req.body
|
||||
});
|
||||
return { lease };
|
||||
}
|
||||
});
|
||||
|
||||
server.route({
|
||||
url: "/:leaseId",
|
||||
method: "GET",
|
||||
config: {
|
||||
rateLimit: readLimit
|
||||
},
|
||||
schema: {
|
||||
params: z.object({
|
||||
leaseId: z.string().min(1).describe(DYNAMIC_SECRET_LEASES.GET_BY_LEASEID.leaseId)
|
||||
}),
|
||||
querystring: z.object({
|
||||
projectSlug: z.string().min(1).describe(DYNAMIC_SECRET_LEASES.GET_BY_LEASEID.projectSlug),
|
||||
path: z
|
||||
.string()
|
||||
.trim()
|
||||
.default("/")
|
||||
.transform(removeTrailingSlash)
|
||||
.describe(DYNAMIC_SECRET_LEASES.GET_BY_LEASEID.path),
|
||||
environmentSlug: z.string().min(1).describe(DYNAMIC_SECRET_LEASES.GET_BY_LEASEID.environmentSlug)
|
||||
}),
|
||||
response: {
|
||||
200: z.object({
|
||||
lease: DynamicSecretLeasesSchema.extend({
|
||||
dynamicSecret: SanitizedDynamicSecretSchema
|
||||
})
|
||||
})
|
||||
}
|
||||
},
|
||||
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
||||
handler: async (req) => {
|
||||
const lease = await server.services.dynamicSecretLease.getLeaseDetails({
|
||||
actor: req.permission.type,
|
||||
actorId: req.permission.id,
|
||||
actorAuthMethod: req.permission.authMethod,
|
||||
actorOrgId: req.permission.orgId,
|
||||
leaseId: req.params.leaseId,
|
||||
...req.query
|
||||
});
|
||||
return { lease };
|
||||
}
|
||||
});
|
||||
};
|
290
backend/src/ee/routes/v1/dynamic-secret-router.ts
Normal file
290
backend/src/ee/routes/v1/dynamic-secret-router.ts
Normal file
@ -0,0 +1,290 @@
|
||||
import slugify from "@sindresorhus/slugify";
|
||||
import ms from "ms";
|
||||
import { z } from "zod";
|
||||
|
||||
import { DynamicSecretLeasesSchema } from "@app/db/schemas";
|
||||
import { DynamicSecretProviderSchema } from "@app/ee/services/dynamic-secret/providers/models";
|
||||
import { DYNAMIC_SECRETS } from "@app/lib/api-docs";
|
||||
import { daysToMillisecond } from "@app/lib/dates";
|
||||
import { removeTrailingSlash } from "@app/lib/fn";
|
||||
import { readLimit, writeLimit } from "@app/server/config/rateLimiter";
|
||||
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
|
||||
import { SanitizedDynamicSecretSchema } from "@app/server/routes/sanitizedSchemas";
|
||||
import { AuthMode } from "@app/services/auth/auth-type";
|
||||
|
||||
export const registerDynamicSecretRouter = async (server: FastifyZodProvider) => {
|
||||
server.route({
|
||||
method: "POST",
|
||||
url: "/",
|
||||
config: {
|
||||
rateLimit: writeLimit
|
||||
},
|
||||
schema: {
|
||||
body: z.object({
|
||||
projectSlug: z.string().min(1).describe(DYNAMIC_SECRETS.CREATE.projectSlug),
|
||||
provider: DynamicSecretProviderSchema.describe(DYNAMIC_SECRETS.CREATE.provider),
|
||||
defaultTTL: z
|
||||
.string()
|
||||
.describe(DYNAMIC_SECRETS.CREATE.defaultTTL)
|
||||
.superRefine((val, ctx) => {
|
||||
const valMs = ms(val);
|
||||
if (valMs < 60 * 1000)
|
||||
ctx.addIssue({ code: z.ZodIssueCode.custom, message: "TTL must be a greater than 1min" });
|
||||
if (valMs > daysToMillisecond(1))
|
||||
ctx.addIssue({ code: z.ZodIssueCode.custom, message: "TTL must be less than a day" });
|
||||
}),
|
||||
maxTTL: z
|
||||
.string()
|
||||
.describe(DYNAMIC_SECRETS.CREATE.maxTTL)
|
||||
.optional()
|
||||
.superRefine((val, ctx) => {
|
||||
if (!val) return;
|
||||
const valMs = ms(val);
|
||||
if (valMs < 60 * 1000)
|
||||
ctx.addIssue({ code: z.ZodIssueCode.custom, message: "TTL must be a greater than 1min" });
|
||||
if (valMs > daysToMillisecond(1))
|
||||
ctx.addIssue({ code: z.ZodIssueCode.custom, message: "TTL must be less than a day" });
|
||||
})
|
||||
.nullable(),
|
||||
path: z.string().describe(DYNAMIC_SECRETS.CREATE.path).trim().default("/").transform(removeTrailingSlash),
|
||||
environmentSlug: z.string().describe(DYNAMIC_SECRETS.CREATE.environmentSlug).min(1),
|
||||
name: z
|
||||
.string()
|
||||
.describe(DYNAMIC_SECRETS.CREATE.name)
|
||||
.min(1)
|
||||
.toLowerCase()
|
||||
.max(64)
|
||||
.refine((v) => slugify(v) === v, {
|
||||
message: "Slug must be a valid"
|
||||
})
|
||||
}),
|
||||
response: {
|
||||
200: z.object({
|
||||
dynamicSecret: SanitizedDynamicSecretSchema
|
||||
})
|
||||
}
|
||||
},
|
||||
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
||||
handler: async (req) => {
|
||||
const dynamicSecretCfg = await server.services.dynamicSecret.create({
|
||||
actor: req.permission.type,
|
||||
actorId: req.permission.id,
|
||||
actorAuthMethod: req.permission.authMethod,
|
||||
actorOrgId: req.permission.orgId,
|
||||
...req.body
|
||||
});
|
||||
return { dynamicSecret: dynamicSecretCfg };
|
||||
}
|
||||
});
|
||||
|
||||
server.route({
|
||||
method: "PATCH",
|
||||
url: "/:name",
|
||||
config: {
|
||||
rateLimit: writeLimit
|
||||
},
|
||||
schema: {
|
||||
params: z.object({
|
||||
name: z.string().toLowerCase().describe(DYNAMIC_SECRETS.UPDATE.name)
|
||||
}),
|
||||
body: z.object({
|
||||
projectSlug: z.string().min(1).describe(DYNAMIC_SECRETS.UPDATE.projectSlug),
|
||||
path: z.string().trim().default("/").transform(removeTrailingSlash).describe(DYNAMIC_SECRETS.UPDATE.path),
|
||||
environmentSlug: z.string().min(1).describe(DYNAMIC_SECRETS.UPDATE.environmentSlug),
|
||||
data: z.object({
|
||||
inputs: z.any().optional().describe(DYNAMIC_SECRETS.UPDATE.inputs),
|
||||
defaultTTL: z
|
||||
.string()
|
||||
.describe(DYNAMIC_SECRETS.UPDATE.defaultTTL)
|
||||
.optional()
|
||||
.superRefine((val, ctx) => {
|
||||
if (!val) return;
|
||||
const valMs = ms(val);
|
||||
if (valMs < 60 * 1000)
|
||||
ctx.addIssue({ code: z.ZodIssueCode.custom, message: "TTL must be a greater than 1min" });
|
||||
if (valMs > daysToMillisecond(1))
|
||||
ctx.addIssue({ code: z.ZodIssueCode.custom, message: "TTL must be less than a day" });
|
||||
}),
|
||||
maxTTL: z
|
||||
.string()
|
||||
.describe(DYNAMIC_SECRETS.UPDATE.maxTTL)
|
||||
.optional()
|
||||
.superRefine((val, ctx) => {
|
||||
if (!val) return;
|
||||
const valMs = ms(val);
|
||||
if (valMs < 60 * 1000)
|
||||
ctx.addIssue({ code: z.ZodIssueCode.custom, message: "TTL must be a greater than 1min" });
|
||||
if (valMs > daysToMillisecond(1))
|
||||
ctx.addIssue({ code: z.ZodIssueCode.custom, message: "TTL must be less than a day" });
|
||||
})
|
||||
.nullable(),
|
||||
newName: z.string().describe(DYNAMIC_SECRETS.UPDATE.newName).optional()
|
||||
})
|
||||
}),
|
||||
response: {
|
||||
200: z.object({
|
||||
dynamicSecret: SanitizedDynamicSecretSchema
|
||||
})
|
||||
}
|
||||
},
|
||||
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
||||
handler: async (req) => {
|
||||
const dynamicSecretCfg = await server.services.dynamicSecret.updateByName({
|
||||
actor: req.permission.type,
|
||||
actorId: req.permission.id,
|
||||
actorAuthMethod: req.permission.authMethod,
|
||||
actorOrgId: req.permission.orgId,
|
||||
name: req.params.name,
|
||||
path: req.body.path,
|
||||
projectSlug: req.body.projectSlug,
|
||||
environmentSlug: req.body.environmentSlug,
|
||||
...req.body.data
|
||||
});
|
||||
return { dynamicSecret: dynamicSecretCfg };
|
||||
}
|
||||
});
|
||||
|
||||
server.route({
|
||||
method: "DELETE",
|
||||
url: "/:name",
|
||||
config: {
|
||||
rateLimit: writeLimit
|
||||
},
|
||||
schema: {
|
||||
params: z.object({
|
||||
name: z.string().toLowerCase().describe(DYNAMIC_SECRETS.DELETE.name)
|
||||
}),
|
||||
body: z.object({
|
||||
projectSlug: z.string().min(1).describe(DYNAMIC_SECRETS.DELETE.projectSlug),
|
||||
path: z.string().trim().default("/").transform(removeTrailingSlash).describe(DYNAMIC_SECRETS.DELETE.path),
|
||||
environmentSlug: z.string().min(1).describe(DYNAMIC_SECRETS.DELETE.environmentSlug),
|
||||
isForced: z.boolean().default(false).describe(DYNAMIC_SECRETS.DELETE.isForced)
|
||||
}),
|
||||
response: {
|
||||
200: z.object({
|
||||
dynamicSecret: SanitizedDynamicSecretSchema
|
||||
})
|
||||
}
|
||||
},
|
||||
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
||||
handler: async (req) => {
|
||||
const dynamicSecretCfg = await server.services.dynamicSecret.deleteByName({
|
||||
actor: req.permission.type,
|
||||
actorId: req.permission.id,
|
||||
actorAuthMethod: req.permission.authMethod,
|
||||
actorOrgId: req.permission.orgId,
|
||||
name: req.params.name,
|
||||
...req.body
|
||||
});
|
||||
return { dynamicSecret: dynamicSecretCfg };
|
||||
}
|
||||
});
|
||||
|
||||
server.route({
|
||||
url: "/:name",
|
||||
method: "GET",
|
||||
config: {
|
||||
rateLimit: readLimit
|
||||
},
|
||||
schema: {
|
||||
params: z.object({
|
||||
name: z.string().min(1).describe(DYNAMIC_SECRETS.GET_BY_NAME.name)
|
||||
}),
|
||||
querystring: z.object({
|
||||
projectSlug: z.string().min(1).describe(DYNAMIC_SECRETS.GET_BY_NAME.projectSlug),
|
||||
path: z.string().trim().default("/").transform(removeTrailingSlash).describe(DYNAMIC_SECRETS.GET_BY_NAME.path),
|
||||
environmentSlug: z.string().min(1).describe(DYNAMIC_SECRETS.GET_BY_NAME.environmentSlug)
|
||||
}),
|
||||
response: {
|
||||
200: z.object({
|
||||
dynamicSecret: SanitizedDynamicSecretSchema.extend({
|
||||
inputs: z.unknown()
|
||||
})
|
||||
})
|
||||
}
|
||||
},
|
||||
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
||||
handler: async (req) => {
|
||||
const dynamicSecretCfg = await server.services.dynamicSecret.getDetails({
|
||||
actor: req.permission.type,
|
||||
actorId: req.permission.id,
|
||||
actorAuthMethod: req.permission.authMethod,
|
||||
actorOrgId: req.permission.orgId,
|
||||
name: req.params.name,
|
||||
...req.query
|
||||
});
|
||||
return { dynamicSecret: dynamicSecretCfg };
|
||||
}
|
||||
});
|
||||
|
||||
server.route({
|
||||
url: "/",
|
||||
method: "GET",
|
||||
config: {
|
||||
rateLimit: readLimit
|
||||
},
|
||||
schema: {
|
||||
querystring: z.object({
|
||||
projectSlug: z.string().min(1).describe(DYNAMIC_SECRETS.LIST.projectSlug),
|
||||
path: z.string().trim().default("/").transform(removeTrailingSlash).describe(DYNAMIC_SECRETS.LIST.path),
|
||||
environmentSlug: z.string().min(1).describe(DYNAMIC_SECRETS.LIST.environmentSlug)
|
||||
}),
|
||||
response: {
|
||||
200: z.object({
|
||||
dynamicSecrets: SanitizedDynamicSecretSchema.array()
|
||||
})
|
||||
}
|
||||
},
|
||||
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
||||
handler: async (req) => {
|
||||
const dynamicSecretCfgs = await server.services.dynamicSecret.list({
|
||||
actor: req.permission.type,
|
||||
actorId: req.permission.id,
|
||||
actorAuthMethod: req.permission.authMethod,
|
||||
actorOrgId: req.permission.orgId,
|
||||
...req.query
|
||||
});
|
||||
return { dynamicSecrets: dynamicSecretCfgs };
|
||||
}
|
||||
});
|
||||
|
||||
server.route({
|
||||
url: "/:name/leases",
|
||||
method: "GET",
|
||||
config: {
|
||||
rateLimit: readLimit
|
||||
},
|
||||
schema: {
|
||||
params: z.object({
|
||||
name: z.string().min(1).describe(DYNAMIC_SECRETS.LIST_LEAES_BY_NAME.name)
|
||||
}),
|
||||
querystring: z.object({
|
||||
projectSlug: z.string().min(1).describe(DYNAMIC_SECRETS.LIST_LEAES_BY_NAME.projectSlug),
|
||||
path: z
|
||||
.string()
|
||||
.trim()
|
||||
.default("/")
|
||||
.transform(removeTrailingSlash)
|
||||
.describe(DYNAMIC_SECRETS.LIST_LEAES_BY_NAME.path),
|
||||
environmentSlug: z.string().min(1).describe(DYNAMIC_SECRETS.LIST_LEAES_BY_NAME.environmentSlug)
|
||||
}),
|
||||
response: {
|
||||
200: z.object({
|
||||
leases: DynamicSecretLeasesSchema.array()
|
||||
})
|
||||
}
|
||||
},
|
||||
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
||||
handler: async (req) => {
|
||||
const leases = await server.services.dynamicSecretLease.listLeases({
|
||||
actor: req.permission.type,
|
||||
actorId: req.permission.id,
|
||||
actorAuthMethod: req.permission.authMethod,
|
||||
actorOrgId: req.permission.orgId,
|
||||
name: req.params.name,
|
||||
...req.query
|
||||
});
|
||||
return { leases };
|
||||
}
|
||||
});
|
||||
};
|
220
backend/src/ee/routes/v1/group-router.ts
Normal file
220
backend/src/ee/routes/v1/group-router.ts
Normal file
@ -0,0 +1,220 @@
|
||||
import slugify from "@sindresorhus/slugify";
|
||||
import { z } from "zod";
|
||||
|
||||
import { GroupsSchema, OrgMembershipRole, UsersSchema } from "@app/db/schemas";
|
||||
import { GROUPS } from "@app/lib/api-docs";
|
||||
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
|
||||
import { AuthMode } from "@app/services/auth/auth-type";
|
||||
|
||||
export const registerGroupRouter = async (server: FastifyZodProvider) => {
|
||||
server.route({
|
||||
url: "/",
|
||||
method: "POST",
|
||||
onRequest: verifyAuth([AuthMode.JWT]),
|
||||
schema: {
|
||||
body: z.object({
|
||||
name: z.string().trim().min(1).max(50).describe(GROUPS.CREATE.name),
|
||||
slug: z
|
||||
.string()
|
||||
.min(5)
|
||||
.max(36)
|
||||
.refine((v) => slugify(v) === v, {
|
||||
message: "Slug must be a valid slug"
|
||||
})
|
||||
.optional()
|
||||
.describe(GROUPS.CREATE.slug),
|
||||
role: z.string().trim().min(1).default(OrgMembershipRole.NoAccess).describe(GROUPS.CREATE.role)
|
||||
}),
|
||||
response: {
|
||||
200: GroupsSchema
|
||||
}
|
||||
},
|
||||
handler: async (req) => {
|
||||
const group = await server.services.group.createGroup({
|
||||
actor: req.permission.type,
|
||||
actorId: req.permission.id,
|
||||
actorAuthMethod: req.permission.authMethod,
|
||||
actorOrgId: req.permission.orgId,
|
||||
...req.body
|
||||
});
|
||||
|
||||
return group;
|
||||
}
|
||||
});
|
||||
|
||||
server.route({
|
||||
url: "/:currentSlug",
|
||||
method: "PATCH",
|
||||
onRequest: verifyAuth([AuthMode.JWT]),
|
||||
schema: {
|
||||
params: z.object({
|
||||
currentSlug: z.string().trim().describe(GROUPS.UPDATE.currentSlug)
|
||||
}),
|
||||
body: z
|
||||
.object({
|
||||
name: z.string().trim().min(1).describe(GROUPS.UPDATE.name),
|
||||
slug: z
|
||||
.string()
|
||||
.min(5)
|
||||
.max(36)
|
||||
.refine((v) => slugify(v) === v, {
|
||||
message: "Slug must be a valid slug"
|
||||
})
|
||||
.describe(GROUPS.UPDATE.slug),
|
||||
role: z.string().trim().min(1).describe(GROUPS.UPDATE.role)
|
||||
})
|
||||
.partial(),
|
||||
response: {
|
||||
200: GroupsSchema
|
||||
}
|
||||
},
|
||||
handler: async (req) => {
|
||||
const group = await server.services.group.updateGroup({
|
||||
currentSlug: req.params.currentSlug,
|
||||
actor: req.permission.type,
|
||||
actorId: req.permission.id,
|
||||
actorAuthMethod: req.permission.authMethod,
|
||||
actorOrgId: req.permission.orgId,
|
||||
...req.body
|
||||
});
|
||||
|
||||
return group;
|
||||
}
|
||||
});
|
||||
|
||||
server.route({
|
||||
url: "/:slug",
|
||||
method: "DELETE",
|
||||
onRequest: verifyAuth([AuthMode.JWT]),
|
||||
schema: {
|
||||
params: z.object({
|
||||
slug: z.string().trim().describe(GROUPS.DELETE.slug)
|
||||
}),
|
||||
response: {
|
||||
200: GroupsSchema
|
||||
}
|
||||
},
|
||||
handler: async (req) => {
|
||||
const group = await server.services.group.deleteGroup({
|
||||
groupSlug: req.params.slug,
|
||||
actor: req.permission.type,
|
||||
actorId: req.permission.id,
|
||||
actorAuthMethod: req.permission.authMethod,
|
||||
actorOrgId: req.permission.orgId
|
||||
});
|
||||
|
||||
return group;
|
||||
}
|
||||
});
|
||||
|
||||
server.route({
|
||||
method: "GET",
|
||||
url: "/:slug/users",
|
||||
onRequest: verifyAuth([AuthMode.JWT]),
|
||||
schema: {
|
||||
params: z.object({
|
||||
slug: z.string().trim().describe(GROUPS.LIST_USERS.slug)
|
||||
}),
|
||||
querystring: z.object({
|
||||
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),
|
||||
username: z.string().optional().describe(GROUPS.LIST_USERS.username)
|
||||
}),
|
||||
response: {
|
||||
200: z.object({
|
||||
users: UsersSchema.pick({
|
||||
email: true,
|
||||
username: true,
|
||||
firstName: true,
|
||||
lastName: true,
|
||||
id: true
|
||||
})
|
||||
.merge(
|
||||
z.object({
|
||||
isPartOfGroup: z.boolean()
|
||||
})
|
||||
)
|
||||
.array(),
|
||||
totalCount: z.number()
|
||||
})
|
||||
}
|
||||
},
|
||||
handler: async (req) => {
|
||||
const { users, totalCount } = await server.services.group.listGroupUsers({
|
||||
groupSlug: req.params.slug,
|
||||
actor: req.permission.type,
|
||||
actorId: req.permission.id,
|
||||
actorAuthMethod: req.permission.authMethod,
|
||||
actorOrgId: req.permission.orgId,
|
||||
...req.query
|
||||
});
|
||||
return { users, totalCount };
|
||||
}
|
||||
});
|
||||
|
||||
server.route({
|
||||
method: "POST",
|
||||
url: "/:slug/users/:username",
|
||||
onRequest: verifyAuth([AuthMode.JWT]),
|
||||
schema: {
|
||||
params: z.object({
|
||||
slug: z.string().trim().describe(GROUPS.ADD_USER.slug),
|
||||
username: z.string().trim().describe(GROUPS.ADD_USER.username)
|
||||
}),
|
||||
response: {
|
||||
200: UsersSchema.pick({
|
||||
email: true,
|
||||
username: true,
|
||||
firstName: true,
|
||||
lastName: true,
|
||||
id: true
|
||||
})
|
||||
}
|
||||
},
|
||||
handler: async (req) => {
|
||||
const user = await server.services.group.addUserToGroup({
|
||||
groupSlug: req.params.slug,
|
||||
username: req.params.username,
|
||||
actor: req.permission.type,
|
||||
actorId: req.permission.id,
|
||||
actorAuthMethod: req.permission.authMethod,
|
||||
actorOrgId: req.permission.orgId
|
||||
});
|
||||
|
||||
return user;
|
||||
}
|
||||
});
|
||||
|
||||
server.route({
|
||||
method: "DELETE",
|
||||
url: "/:slug/users/:username",
|
||||
onRequest: verifyAuth([AuthMode.JWT]),
|
||||
schema: {
|
||||
params: z.object({
|
||||
slug: z.string().trim().describe(GROUPS.DELETE_USER.slug),
|
||||
username: z.string().trim().describe(GROUPS.DELETE_USER.username)
|
||||
}),
|
||||
response: {
|
||||
200: UsersSchema.pick({
|
||||
email: true,
|
||||
username: true,
|
||||
firstName: true,
|
||||
lastName: true,
|
||||
id: true
|
||||
})
|
||||
}
|
||||
},
|
||||
handler: async (req) => {
|
||||
const user = await server.services.group.removeUserFromGroup({
|
||||
groupSlug: req.params.slug,
|
||||
username: req.params.username,
|
||||
actor: req.permission.type,
|
||||
actorId: req.permission.id,
|
||||
actorAuthMethod: req.permission.authMethod,
|
||||
actorOrgId: req.permission.orgId
|
||||
});
|
||||
|
||||
return user;
|
||||
}
|
||||
});
|
||||
};
|
@ -0,0 +1,329 @@
|
||||
import { MongoAbility, RawRuleOf } from "@casl/ability";
|
||||
import { PackRule, packRules, unpackRules } from "@casl/ability/extra";
|
||||
import slugify from "@sindresorhus/slugify";
|
||||
import ms from "ms";
|
||||
import { z } from "zod";
|
||||
|
||||
import { IdentityProjectAdditionalPrivilegeSchema } from "@app/db/schemas";
|
||||
import { IdentityProjectAdditionalPrivilegeTemporaryMode } from "@app/ee/services/identity-project-additional-privilege/identity-project-additional-privilege-types";
|
||||
import { ProjectPermissionSet } from "@app/ee/services/permission/project-permission";
|
||||
import { IDENTITY_ADDITIONAL_PRIVILEGE } from "@app/lib/api-docs";
|
||||
import { alphaNumericNanoId } from "@app/lib/nanoid";
|
||||
import { readLimit, writeLimit } from "@app/server/config/rateLimiter";
|
||||
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
|
||||
import { AuthMode } from "@app/services/auth/auth-type";
|
||||
|
||||
export const registerIdentityProjectAdditionalPrivilegeRouter = async (server: FastifyZodProvider) => {
|
||||
server.route({
|
||||
method: "POST",
|
||||
url: "/permanent",
|
||||
config: {
|
||||
rateLimit: writeLimit
|
||||
},
|
||||
schema: {
|
||||
description: "Create a permanent or a non expiry specific privilege for identity.",
|
||||
security: [
|
||||
{
|
||||
bearerAuth: []
|
||||
}
|
||||
],
|
||||
body: z.object({
|
||||
identityId: z.string().min(1).describe(IDENTITY_ADDITIONAL_PRIVILEGE.CREATE.identityId),
|
||||
projectSlug: z.string().min(1).describe(IDENTITY_ADDITIONAL_PRIVILEGE.CREATE.projectSlug),
|
||||
slug: z
|
||||
.string()
|
||||
.min(1)
|
||||
.max(60)
|
||||
.trim()
|
||||
.refine((val) => val.toLowerCase() === val, "Must be lowercase")
|
||||
.refine((v) => slugify(v) === v, {
|
||||
message: "Slug must be a valid slug"
|
||||
})
|
||||
.optional()
|
||||
.describe(IDENTITY_ADDITIONAL_PRIVILEGE.CREATE.slug),
|
||||
permissions: z.any().array().describe(IDENTITY_ADDITIONAL_PRIVILEGE.CREATE.permissions)
|
||||
}),
|
||||
response: {
|
||||
200: z.object({
|
||||
privilege: IdentityProjectAdditionalPrivilegeSchema
|
||||
})
|
||||
}
|
||||
},
|
||||
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
||||
handler: async (req) => {
|
||||
const privilege = await server.services.identityProjectAdditionalPrivilege.create({
|
||||
actorId: req.permission.id,
|
||||
actor: req.permission.type,
|
||||
actorOrgId: req.permission.orgId,
|
||||
actorAuthMethod: req.permission.authMethod,
|
||||
...req.body,
|
||||
slug: req.body.slug ? slugify(req.body.slug) : slugify(alphaNumericNanoId(12)),
|
||||
isTemporary: false,
|
||||
permissions: JSON.stringify(packRules(req.body.permissions))
|
||||
});
|
||||
return { privilege };
|
||||
}
|
||||
});
|
||||
|
||||
server.route({
|
||||
method: "POST",
|
||||
url: "/temporary",
|
||||
config: {
|
||||
rateLimit: writeLimit
|
||||
},
|
||||
schema: {
|
||||
description: "Create a temporary or a expiring specific privilege for identity.",
|
||||
security: [
|
||||
{
|
||||
bearerAuth: []
|
||||
}
|
||||
],
|
||||
body: z.object({
|
||||
identityId: z.string().min(1).describe(IDENTITY_ADDITIONAL_PRIVILEGE.CREATE.identityId),
|
||||
projectSlug: z.string().min(1).describe(IDENTITY_ADDITIONAL_PRIVILEGE.CREATE.projectSlug),
|
||||
slug: z
|
||||
.string()
|
||||
.min(1)
|
||||
.max(60)
|
||||
.trim()
|
||||
.refine((val) => val.toLowerCase() === val, "Must be lowercase")
|
||||
.refine((v) => slugify(v) === v, {
|
||||
message: "Slug must be a valid slug"
|
||||
})
|
||||
.optional()
|
||||
.describe(IDENTITY_ADDITIONAL_PRIVILEGE.CREATE.slug),
|
||||
permissions: z.any().array().describe(IDENTITY_ADDITIONAL_PRIVILEGE.CREATE.permissions),
|
||||
temporaryMode: z
|
||||
.nativeEnum(IdentityProjectAdditionalPrivilegeTemporaryMode)
|
||||
.describe(IDENTITY_ADDITIONAL_PRIVILEGE.CREATE.temporaryMode),
|
||||
temporaryRange: z
|
||||
.string()
|
||||
.refine((val) => ms(val) > 0, "Temporary range must be a positive number")
|
||||
.describe(IDENTITY_ADDITIONAL_PRIVILEGE.CREATE.temporaryRange),
|
||||
temporaryAccessStartTime: z
|
||||
.string()
|
||||
.datetime()
|
||||
.describe(IDENTITY_ADDITIONAL_PRIVILEGE.CREATE.temporaryAccessStartTime)
|
||||
}),
|
||||
response: {
|
||||
200: z.object({
|
||||
privilege: IdentityProjectAdditionalPrivilegeSchema
|
||||
})
|
||||
}
|
||||
},
|
||||
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
||||
handler: async (req) => {
|
||||
const privilege = await server.services.identityProjectAdditionalPrivilege.create({
|
||||
actorId: req.permission.id,
|
||||
actor: req.permission.type,
|
||||
actorOrgId: req.permission.orgId,
|
||||
actorAuthMethod: req.permission.authMethod,
|
||||
...req.body,
|
||||
slug: req.body.slug ? slugify(req.body.slug) : slugify(alphaNumericNanoId(12)),
|
||||
isTemporary: true,
|
||||
permissions: JSON.stringify(packRules(req.body.permissions))
|
||||
});
|
||||
return { privilege };
|
||||
}
|
||||
});
|
||||
|
||||
server.route({
|
||||
method: "PATCH",
|
||||
url: "/",
|
||||
config: {
|
||||
rateLimit: writeLimit
|
||||
},
|
||||
schema: {
|
||||
description: "Update a specific privilege of an identity.",
|
||||
security: [
|
||||
{
|
||||
bearerAuth: []
|
||||
}
|
||||
],
|
||||
body: z.object({
|
||||
// disallow empty string
|
||||
privilegeSlug: z.string().min(1).describe(IDENTITY_ADDITIONAL_PRIVILEGE.UPDATE.slug),
|
||||
identityId: z.string().min(1).describe(IDENTITY_ADDITIONAL_PRIVILEGE.UPDATE.identityId),
|
||||
projectSlug: z.string().min(1).describe(IDENTITY_ADDITIONAL_PRIVILEGE.UPDATE.projectSlug),
|
||||
privilegeDetails: z
|
||||
.object({
|
||||
slug: z
|
||||
.string()
|
||||
.min(1)
|
||||
.max(60)
|
||||
.trim()
|
||||
.refine((val) => val.toLowerCase() === val, "Must be lowercase")
|
||||
.refine((v) => slugify(v) === v, {
|
||||
message: "Slug must be a valid slug"
|
||||
})
|
||||
.describe(IDENTITY_ADDITIONAL_PRIVILEGE.UPDATE.newSlug),
|
||||
permissions: z.any().array().describe(IDENTITY_ADDITIONAL_PRIVILEGE.UPDATE.permissions),
|
||||
isTemporary: z.boolean().describe(IDENTITY_ADDITIONAL_PRIVILEGE.UPDATE.isTemporary),
|
||||
temporaryMode: z
|
||||
.nativeEnum(IdentityProjectAdditionalPrivilegeTemporaryMode)
|
||||
.describe(IDENTITY_ADDITIONAL_PRIVILEGE.UPDATE.temporaryMode),
|
||||
temporaryRange: z
|
||||
.string()
|
||||
.refine((val) => ms(val) > 0, "Temporary range must be a positive number")
|
||||
.describe(IDENTITY_ADDITIONAL_PRIVILEGE.UPDATE.temporaryRange),
|
||||
temporaryAccessStartTime: z
|
||||
.string()
|
||||
.datetime()
|
||||
.describe(IDENTITY_ADDITIONAL_PRIVILEGE.UPDATE.temporaryAccessStartTime)
|
||||
})
|
||||
.partial()
|
||||
}),
|
||||
response: {
|
||||
200: z.object({
|
||||
privilege: IdentityProjectAdditionalPrivilegeSchema
|
||||
})
|
||||
}
|
||||
},
|
||||
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
||||
handler: async (req) => {
|
||||
const updatedInfo = req.body.privilegeDetails;
|
||||
const privilege = await server.services.identityProjectAdditionalPrivilege.updateBySlug({
|
||||
actorId: req.permission.id,
|
||||
actor: req.permission.type,
|
||||
actorOrgId: req.permission.orgId,
|
||||
actorAuthMethod: req.permission.authMethod,
|
||||
slug: req.body.privilegeSlug,
|
||||
identityId: req.body.identityId,
|
||||
projectSlug: req.body.projectSlug,
|
||||
data: {
|
||||
...updatedInfo,
|
||||
permissions: updatedInfo?.permissions ? JSON.stringify(packRules(updatedInfo.permissions)) : undefined
|
||||
}
|
||||
});
|
||||
return { privilege };
|
||||
}
|
||||
});
|
||||
|
||||
server.route({
|
||||
method: "DELETE",
|
||||
url: "/",
|
||||
config: {
|
||||
rateLimit: writeLimit
|
||||
},
|
||||
schema: {
|
||||
description: "Delete a specific privilege of an identity.",
|
||||
security: [
|
||||
{
|
||||
bearerAuth: []
|
||||
}
|
||||
],
|
||||
body: z.object({
|
||||
privilegeSlug: z.string().min(1).describe(IDENTITY_ADDITIONAL_PRIVILEGE.DELETE.slug),
|
||||
identityId: z.string().min(1).describe(IDENTITY_ADDITIONAL_PRIVILEGE.DELETE.identityId),
|
||||
projectSlug: z.string().min(1).describe(IDENTITY_ADDITIONAL_PRIVILEGE.DELETE.projectSlug)
|
||||
}),
|
||||
response: {
|
||||
200: z.object({
|
||||
privilege: IdentityProjectAdditionalPrivilegeSchema
|
||||
})
|
||||
}
|
||||
},
|
||||
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
||||
handler: async (req) => {
|
||||
const privilege = await server.services.identityProjectAdditionalPrivilege.deleteBySlug({
|
||||
actorId: req.permission.id,
|
||||
actor: req.permission.type,
|
||||
actorAuthMethod: req.permission.authMethod,
|
||||
actorOrgId: req.permission.orgId,
|
||||
slug: req.body.privilegeSlug,
|
||||
identityId: req.body.identityId,
|
||||
projectSlug: req.body.projectSlug
|
||||
});
|
||||
return { privilege };
|
||||
}
|
||||
});
|
||||
|
||||
server.route({
|
||||
method: "GET",
|
||||
url: "/:privilegeSlug",
|
||||
config: {
|
||||
rateLimit: readLimit
|
||||
},
|
||||
schema: {
|
||||
description: "Retrieve details of a specific privilege by privilege slug.",
|
||||
security: [
|
||||
{
|
||||
bearerAuth: []
|
||||
}
|
||||
],
|
||||
params: z.object({
|
||||
privilegeSlug: z.string().min(1).describe(IDENTITY_ADDITIONAL_PRIVILEGE.GET_BY_SLUG.slug)
|
||||
}),
|
||||
querystring: z.object({
|
||||
identityId: z.string().min(1).describe(IDENTITY_ADDITIONAL_PRIVILEGE.GET_BY_SLUG.identityId),
|
||||
projectSlug: z.string().min(1).describe(IDENTITY_ADDITIONAL_PRIVILEGE.GET_BY_SLUG.projectSlug)
|
||||
}),
|
||||
response: {
|
||||
200: z.object({
|
||||
privilege: IdentityProjectAdditionalPrivilegeSchema
|
||||
})
|
||||
}
|
||||
},
|
||||
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
||||
handler: async (req) => {
|
||||
const privilege = await server.services.identityProjectAdditionalPrivilege.getPrivilegeDetailsBySlug({
|
||||
actorId: req.permission.id,
|
||||
actorAuthMethod: req.permission.authMethod,
|
||||
actor: req.permission.type,
|
||||
actorOrgId: req.permission.orgId,
|
||||
slug: req.params.privilegeSlug,
|
||||
...req.query
|
||||
});
|
||||
return { privilege };
|
||||
}
|
||||
});
|
||||
|
||||
server.route({
|
||||
method: "GET",
|
||||
url: "/",
|
||||
config: {
|
||||
rateLimit: readLimit
|
||||
},
|
||||
schema: {
|
||||
description: "List of a specific privilege of an identity in a project.",
|
||||
security: [
|
||||
{
|
||||
bearerAuth: []
|
||||
}
|
||||
],
|
||||
querystring: z.object({
|
||||
identityId: z.string().min(1).describe(IDENTITY_ADDITIONAL_PRIVILEGE.LIST.identityId),
|
||||
projectSlug: z.string().min(1).describe(IDENTITY_ADDITIONAL_PRIVILEGE.LIST.projectSlug),
|
||||
unpacked: z
|
||||
.enum(["false", "true"])
|
||||
.transform((el) => el === "true")
|
||||
.default("true")
|
||||
.describe(IDENTITY_ADDITIONAL_PRIVILEGE.LIST.unpacked)
|
||||
}),
|
||||
response: {
|
||||
200: z.object({
|
||||
privileges: IdentityProjectAdditionalPrivilegeSchema.array()
|
||||
})
|
||||
}
|
||||
},
|
||||
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
||||
handler: async (req) => {
|
||||
const privileges = await server.services.identityProjectAdditionalPrivilege.listIdentityProjectPrivileges({
|
||||
actorId: req.permission.id,
|
||||
actor: req.permission.type,
|
||||
actorAuthMethod: req.permission.authMethod,
|
||||
actorOrgId: req.permission.orgId,
|
||||
...req.query
|
||||
});
|
||||
if (req.query.unpacked) {
|
||||
return {
|
||||
privileges: privileges.map(({ permissions, ...el }) => ({
|
||||
...el,
|
||||
permissions: unpackRules(permissions as PackRule<RawRuleOf<MongoAbility<ProjectPermissionSet>>>[])
|
||||
}))
|
||||
};
|
||||
}
|
||||
return { privileges };
|
||||
}
|
||||
});
|
||||
};
|
@ -1,3 +1,7 @@
|
||||
import { registerDynamicSecretLeaseRouter } from "./dynamic-secret-lease-router";
|
||||
import { registerDynamicSecretRouter } from "./dynamic-secret-router";
|
||||
import { registerGroupRouter } from "./group-router";
|
||||
import { registerIdentityProjectAdditionalPrivilegeRouter } from "./identity-project-additional-privilege-router";
|
||||
import { registerLdapRouter } from "./ldap-router";
|
||||
import { registerLicenseRouter } from "./license-router";
|
||||
import { registerOrgRoleRouter } from "./org-role-router";
|
||||
@ -13,6 +17,7 @@ import { registerSecretScanningRouter } from "./secret-scanning-router";
|
||||
import { registerSecretVersionRouter } from "./secret-version-router";
|
||||
import { registerSnapshotRouter } from "./snapshot-router";
|
||||
import { registerTrustedIpRouter } from "./trusted-ip-router";
|
||||
import { registerUserAdditionalPrivilegeRouter } from "./user-additional-privilege-router";
|
||||
|
||||
export const registerV1EERoutes = async (server: FastifyZodProvider) => {
|
||||
// org role starts with organization
|
||||
@ -34,10 +39,27 @@ export const registerV1EERoutes = async (server: FastifyZodProvider) => {
|
||||
await server.register(registerSecretRotationProviderRouter, {
|
||||
prefix: "/secret-rotation-providers"
|
||||
});
|
||||
|
||||
await server.register(
|
||||
async (dynamicSecretRouter) => {
|
||||
await dynamicSecretRouter.register(registerDynamicSecretRouter);
|
||||
await dynamicSecretRouter.register(registerDynamicSecretLeaseRouter, { prefix: "/leases" });
|
||||
},
|
||||
{ prefix: "/dynamic-secrets" }
|
||||
);
|
||||
|
||||
await server.register(registerSamlRouter, { prefix: "/sso" });
|
||||
await server.register(registerScimRouter, { prefix: "/scim" });
|
||||
await server.register(registerLdapRouter, { prefix: "/ldap" });
|
||||
await server.register(registerSecretScanningRouter, { prefix: "/secret-scanning" });
|
||||
await server.register(registerSecretRotationRouter, { prefix: "/secret-rotations" });
|
||||
await server.register(registerSecretVersionRouter, { prefix: "/secret" });
|
||||
await server.register(registerGroupRouter, { prefix: "/groups" });
|
||||
await server.register(
|
||||
async (privilegeRouter) => {
|
||||
await privilegeRouter.register(registerUserAdditionalPrivilegeRouter, { prefix: "/users" });
|
||||
await privilegeRouter.register(registerIdentityProjectAdditionalPrivilegeRouter, { prefix: "/identity" });
|
||||
},
|
||||
{ prefix: "/additional-privilege" }
|
||||
);
|
||||
};
|
||||
|
@ -17,6 +17,7 @@ import { z } from "zod";
|
||||
import { LdapConfigsSchema } from "@app/db/schemas";
|
||||
import { getConfig } from "@app/lib/config/env";
|
||||
import { logger } from "@app/lib/logger";
|
||||
import { readLimit, writeLimit } from "@app/server/config/rateLimiter";
|
||||
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
|
||||
import { AuthMode } from "@app/services/auth/auth-type";
|
||||
|
||||
@ -97,8 +98,11 @@ export const registerLdapRouter = async (server: FastifyZodProvider) => {
|
||||
});
|
||||
|
||||
server.route({
|
||||
url: "/config",
|
||||
method: "GET",
|
||||
url: "/config",
|
||||
config: {
|
||||
rateLimit: readLimit
|
||||
},
|
||||
onRequest: verifyAuth([AuthMode.JWT]),
|
||||
schema: {
|
||||
querystring: z.object({
|
||||
@ -122,6 +126,7 @@ export const registerLdapRouter = async (server: FastifyZodProvider) => {
|
||||
actor: req.permission.type,
|
||||
actorId: req.permission.id,
|
||||
orgId: req.query.organizationId,
|
||||
actorAuthMethod: req.permission.authMethod,
|
||||
actorOrgId: req.permission.orgId
|
||||
});
|
||||
return ldap;
|
||||
@ -129,8 +134,11 @@ export const registerLdapRouter = async (server: FastifyZodProvider) => {
|
||||
});
|
||||
|
||||
server.route({
|
||||
url: "/config",
|
||||
method: "POST",
|
||||
url: "/config",
|
||||
config: {
|
||||
rateLimit: writeLimit
|
||||
},
|
||||
onRequest: verifyAuth([AuthMode.JWT]),
|
||||
schema: {
|
||||
body: z.object({
|
||||
@ -151,6 +159,7 @@ export const registerLdapRouter = async (server: FastifyZodProvider) => {
|
||||
actor: req.permission.type,
|
||||
actorId: req.permission.id,
|
||||
orgId: req.body.organizationId,
|
||||
actorAuthMethod: req.permission.authMethod,
|
||||
actorOrgId: req.permission.orgId,
|
||||
...req.body
|
||||
});
|
||||
@ -162,6 +171,9 @@ export const registerLdapRouter = async (server: FastifyZodProvider) => {
|
||||
server.route({
|
||||
url: "/config",
|
||||
method: "PATCH",
|
||||
config: {
|
||||
rateLimit: writeLimit
|
||||
},
|
||||
onRequest: verifyAuth([AuthMode.JWT]),
|
||||
schema: {
|
||||
body: z
|
||||
@ -184,6 +196,7 @@ export const registerLdapRouter = async (server: FastifyZodProvider) => {
|
||||
actor: req.permission.type,
|
||||
actorId: req.permission.id,
|
||||
orgId: req.body.organizationId,
|
||||
actorAuthMethod: req.permission.authMethod,
|
||||
actorOrgId: req.permission.orgId,
|
||||
...req.body
|
||||
});
|
||||
|
@ -3,13 +3,17 @@
|
||||
// TODO(akhilmhdh): Fix this when licence service gets it type
|
||||
import { z } from "zod";
|
||||
|
||||
import { readLimit, writeLimit } from "@app/server/config/rateLimiter";
|
||||
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
|
||||
import { AuthMode } from "@app/services/auth/auth-type";
|
||||
|
||||
export const registerLicenseRouter = async (server: FastifyZodProvider) => {
|
||||
server.route({
|
||||
url: "/:organizationId/plans/table",
|
||||
method: "GET",
|
||||
url: "/:organizationId/plans/table",
|
||||
config: {
|
||||
rateLimit: readLimit
|
||||
},
|
||||
schema: {
|
||||
querystring: z.object({ billingCycle: z.enum(["monthly", "yearly"]) }),
|
||||
params: z.object({ organizationId: z.string().trim() }),
|
||||
@ -24,6 +28,7 @@ export const registerLicenseRouter = async (server: FastifyZodProvider) => {
|
||||
actor: req.permission.type,
|
||||
actorOrgId: req.permission.orgId,
|
||||
orgId: req.params.organizationId,
|
||||
actorAuthMethod: req.permission.authMethod,
|
||||
billingCycle: req.query.billingCycle
|
||||
});
|
||||
return data;
|
||||
@ -31,8 +36,11 @@ export const registerLicenseRouter = async (server: FastifyZodProvider) => {
|
||||
});
|
||||
|
||||
server.route({
|
||||
url: "/:organizationId/plan",
|
||||
method: "GET",
|
||||
url: "/:organizationId/plan",
|
||||
config: {
|
||||
rateLimit: readLimit
|
||||
},
|
||||
schema: {
|
||||
params: z.object({ organizationId: z.string().trim() }),
|
||||
response: {
|
||||
@ -45,6 +53,7 @@ export const registerLicenseRouter = async (server: FastifyZodProvider) => {
|
||||
actorId: req.permission.id,
|
||||
actor: req.permission.type,
|
||||
actorOrgId: req.permission.orgId,
|
||||
actorAuthMethod: req.permission.authMethod,
|
||||
orgId: req.params.organizationId
|
||||
});
|
||||
return { plan };
|
||||
@ -52,8 +61,11 @@ export const registerLicenseRouter = async (server: FastifyZodProvider) => {
|
||||
});
|
||||
|
||||
server.route({
|
||||
url: "/:organizationId/plans",
|
||||
method: "GET",
|
||||
url: "/:organizationId/plans",
|
||||
config: {
|
||||
rateLimit: readLimit
|
||||
},
|
||||
schema: {
|
||||
params: z.object({ organizationId: z.string().trim() }),
|
||||
querystring: z.object({ workspaceId: z.string().trim().optional() }),
|
||||
@ -66,6 +78,8 @@ export const registerLicenseRouter = async (server: FastifyZodProvider) => {
|
||||
const data = await server.services.license.getOrgPlan({
|
||||
actorId: req.permission.id,
|
||||
actor: req.permission.type,
|
||||
actorOrgId: req.permission.orgId,
|
||||
actorAuthMethod: req.permission.authMethod,
|
||||
orgId: req.params.organizationId
|
||||
});
|
||||
return data;
|
||||
@ -73,8 +87,11 @@ export const registerLicenseRouter = async (server: FastifyZodProvider) => {
|
||||
});
|
||||
|
||||
server.route({
|
||||
url: "/:organizationId/session/trial",
|
||||
method: "POST",
|
||||
url: "/:organizationId/session/trial",
|
||||
config: {
|
||||
rateLimit: writeLimit
|
||||
},
|
||||
schema: {
|
||||
params: z.object({ organizationId: z.string().trim() }),
|
||||
body: z.object({ success_url: z.string().trim() }),
|
||||
@ -89,6 +106,7 @@ export const registerLicenseRouter = async (server: FastifyZodProvider) => {
|
||||
actor: req.permission.type,
|
||||
actorOrgId: req.permission.orgId,
|
||||
orgId: req.params.organizationId,
|
||||
actorAuthMethod: req.permission.authMethod,
|
||||
success_url: req.body.success_url
|
||||
});
|
||||
return data;
|
||||
@ -98,6 +116,9 @@ export const registerLicenseRouter = async (server: FastifyZodProvider) => {
|
||||
server.route({
|
||||
url: "/:organizationId/customer-portal-session",
|
||||
method: "POST",
|
||||
config: {
|
||||
rateLimit: writeLimit
|
||||
},
|
||||
schema: {
|
||||
params: z.object({ organizationId: z.string().trim() }),
|
||||
response: {
|
||||
@ -110,6 +131,7 @@ export const registerLicenseRouter = async (server: FastifyZodProvider) => {
|
||||
actorId: req.permission.id,
|
||||
actor: req.permission.type,
|
||||
actorOrgId: req.permission.orgId,
|
||||
actorAuthMethod: req.permission.authMethod,
|
||||
orgId: req.params.organizationId
|
||||
});
|
||||
return data;
|
||||
@ -117,8 +139,11 @@ export const registerLicenseRouter = async (server: FastifyZodProvider) => {
|
||||
});
|
||||
|
||||
server.route({
|
||||
url: "/:organizationId/plan/billing",
|
||||
method: "GET",
|
||||
url: "/:organizationId/plan/billing",
|
||||
config: {
|
||||
rateLimit: readLimit
|
||||
},
|
||||
schema: {
|
||||
params: z.object({ organizationId: z.string().trim() }),
|
||||
response: {
|
||||
@ -131,6 +156,7 @@ export const registerLicenseRouter = async (server: FastifyZodProvider) => {
|
||||
actorId: req.permission.id,
|
||||
actor: req.permission.type,
|
||||
actorOrgId: req.permission.orgId,
|
||||
actorAuthMethod: req.permission.authMethod,
|
||||
orgId: req.params.organizationId
|
||||
});
|
||||
return data;
|
||||
@ -138,8 +164,11 @@ export const registerLicenseRouter = async (server: FastifyZodProvider) => {
|
||||
});
|
||||
|
||||
server.route({
|
||||
url: "/:organizationId/plan/table",
|
||||
method: "GET",
|
||||
url: "/:organizationId/plan/table",
|
||||
config: {
|
||||
rateLimit: readLimit
|
||||
},
|
||||
schema: {
|
||||
params: z.object({ organizationId: z.string().trim() }),
|
||||
response: {
|
||||
@ -152,6 +181,7 @@ export const registerLicenseRouter = async (server: FastifyZodProvider) => {
|
||||
actorId: req.permission.id,
|
||||
actor: req.permission.type,
|
||||
actorOrgId: req.permission.orgId,
|
||||
actorAuthMethod: req.permission.authMethod,
|
||||
orgId: req.params.organizationId
|
||||
});
|
||||
return data;
|
||||
@ -159,8 +189,11 @@ export const registerLicenseRouter = async (server: FastifyZodProvider) => {
|
||||
});
|
||||
|
||||
server.route({
|
||||
url: "/:organizationId/billing-details",
|
||||
method: "GET",
|
||||
url: "/:organizationId/billing-details",
|
||||
config: {
|
||||
rateLimit: readLimit
|
||||
},
|
||||
schema: {
|
||||
params: z.object({ organizationId: z.string().trim() }),
|
||||
response: {
|
||||
@ -173,6 +206,7 @@ export const registerLicenseRouter = async (server: FastifyZodProvider) => {
|
||||
actorId: req.permission.id,
|
||||
actor: req.permission.type,
|
||||
actorOrgId: req.permission.orgId,
|
||||
actorAuthMethod: req.permission.authMethod,
|
||||
orgId: req.params.organizationId
|
||||
});
|
||||
return data;
|
||||
@ -180,8 +214,11 @@ export const registerLicenseRouter = async (server: FastifyZodProvider) => {
|
||||
});
|
||||
|
||||
server.route({
|
||||
url: "/:organizationId/billing-details",
|
||||
method: "PATCH",
|
||||
url: "/:organizationId/billing-details",
|
||||
config: {
|
||||
rateLimit: writeLimit
|
||||
},
|
||||
schema: {
|
||||
params: z.object({ organizationId: z.string().trim() }),
|
||||
body: z.object({
|
||||
@ -198,6 +235,7 @@ export const registerLicenseRouter = async (server: FastifyZodProvider) => {
|
||||
actorId: req.permission.id,
|
||||
actor: req.permission.type,
|
||||
actorOrgId: req.permission.orgId,
|
||||
actorAuthMethod: req.permission.authMethod,
|
||||
orgId: req.params.organizationId,
|
||||
name: req.body.name,
|
||||
email: req.body.email
|
||||
@ -207,8 +245,11 @@ export const registerLicenseRouter = async (server: FastifyZodProvider) => {
|
||||
});
|
||||
|
||||
server.route({
|
||||
url: "/:organizationId/billing-details/payment-methods",
|
||||
method: "GET",
|
||||
url: "/:organizationId/billing-details/payment-methods",
|
||||
config: {
|
||||
rateLimit: readLimit
|
||||
},
|
||||
schema: {
|
||||
params: z.object({ organizationId: z.string().trim() }),
|
||||
response: {
|
||||
@ -221,6 +262,7 @@ export const registerLicenseRouter = async (server: FastifyZodProvider) => {
|
||||
actorId: req.permission.id,
|
||||
actor: req.permission.type,
|
||||
actorOrgId: req.permission.orgId,
|
||||
actorAuthMethod: req.permission.authMethod,
|
||||
orgId: req.params.organizationId
|
||||
});
|
||||
return data;
|
||||
@ -228,8 +270,11 @@ export const registerLicenseRouter = async (server: FastifyZodProvider) => {
|
||||
});
|
||||
|
||||
server.route({
|
||||
url: "/:organizationId/billing-details/payment-methods",
|
||||
method: "POST",
|
||||
url: "/:organizationId/billing-details/payment-methods",
|
||||
config: {
|
||||
rateLimit: writeLimit
|
||||
},
|
||||
schema: {
|
||||
params: z.object({ organizationId: z.string().trim() }),
|
||||
body: z.object({
|
||||
@ -246,6 +291,7 @@ export const registerLicenseRouter = async (server: FastifyZodProvider) => {
|
||||
actorId: req.permission.id,
|
||||
actor: req.permission.type,
|
||||
actorOrgId: req.permission.orgId,
|
||||
actorAuthMethod: req.permission.authMethod,
|
||||
orgId: req.params.organizationId,
|
||||
success_url: req.body.success_url,
|
||||
cancel_url: req.body.cancel_url
|
||||
@ -255,8 +301,11 @@ export const registerLicenseRouter = async (server: FastifyZodProvider) => {
|
||||
});
|
||||
|
||||
server.route({
|
||||
url: "/:organizationId/billing-details/payment-methods/:pmtMethodId",
|
||||
method: "DELETE",
|
||||
url: "/:organizationId/billing-details/payment-methods/:pmtMethodId",
|
||||
config: {
|
||||
rateLimit: writeLimit
|
||||
},
|
||||
schema: {
|
||||
params: z.object({
|
||||
organizationId: z.string().trim(),
|
||||
@ -271,6 +320,7 @@ export const registerLicenseRouter = async (server: FastifyZodProvider) => {
|
||||
const data = await server.services.license.delOrgPmtMethods({
|
||||
actorId: req.permission.id,
|
||||
actor: req.permission.type,
|
||||
actorAuthMethod: req.permission.authMethod,
|
||||
actorOrgId: req.permission.orgId,
|
||||
orgId: req.params.organizationId,
|
||||
pmtMethodId: req.params.pmtMethodId
|
||||
@ -280,8 +330,11 @@ export const registerLicenseRouter = async (server: FastifyZodProvider) => {
|
||||
});
|
||||
|
||||
server.route({
|
||||
url: "/:organizationId/billing-details/tax-ids",
|
||||
method: "GET",
|
||||
url: "/:organizationId/billing-details/tax-ids",
|
||||
config: {
|
||||
rateLimit: readLimit
|
||||
},
|
||||
schema: {
|
||||
params: z.object({
|
||||
organizationId: z.string().trim()
|
||||
@ -295,6 +348,7 @@ export const registerLicenseRouter = async (server: FastifyZodProvider) => {
|
||||
const data = await server.services.license.getOrgTaxIds({
|
||||
actorId: req.permission.id,
|
||||
actor: req.permission.type,
|
||||
actorAuthMethod: req.permission.authMethod,
|
||||
actorOrgId: req.permission.orgId,
|
||||
orgId: req.params.organizationId
|
||||
});
|
||||
@ -303,8 +357,11 @@ export const registerLicenseRouter = async (server: FastifyZodProvider) => {
|
||||
});
|
||||
|
||||
server.route({
|
||||
url: "/:organizationId/billing-details/tax-ids",
|
||||
method: "POST",
|
||||
url: "/:organizationId/billing-details/tax-ids",
|
||||
config: {
|
||||
rateLimit: writeLimit
|
||||
},
|
||||
schema: {
|
||||
params: z.object({
|
||||
organizationId: z.string().trim()
|
||||
@ -322,6 +379,7 @@ export const registerLicenseRouter = async (server: FastifyZodProvider) => {
|
||||
const data = await server.services.license.addOrgTaxId({
|
||||
actorId: req.permission.id,
|
||||
actor: req.permission.type,
|
||||
actorAuthMethod: req.permission.authMethod,
|
||||
actorOrgId: req.permission.orgId,
|
||||
orgId: req.params.organizationId,
|
||||
type: req.body.type,
|
||||
@ -332,8 +390,11 @@ export const registerLicenseRouter = async (server: FastifyZodProvider) => {
|
||||
});
|
||||
|
||||
server.route({
|
||||
url: "/:organizationId/billing-details/tax-ids/:taxId",
|
||||
method: "DELETE",
|
||||
url: "/:organizationId/billing-details/tax-ids/:taxId",
|
||||
config: {
|
||||
rateLimit: writeLimit
|
||||
},
|
||||
schema: {
|
||||
params: z.object({
|
||||
organizationId: z.string().trim(),
|
||||
@ -348,6 +409,7 @@ export const registerLicenseRouter = async (server: FastifyZodProvider) => {
|
||||
const data = await server.services.license.delOrgTaxId({
|
||||
actorId: req.permission.id,
|
||||
actor: req.permission.type,
|
||||
actorAuthMethod: req.permission.authMethod,
|
||||
actorOrgId: req.permission.orgId,
|
||||
orgId: req.params.organizationId,
|
||||
taxId: req.params.taxId
|
||||
@ -357,8 +419,11 @@ export const registerLicenseRouter = async (server: FastifyZodProvider) => {
|
||||
});
|
||||
|
||||
server.route({
|
||||
url: "/:organizationId/invoices",
|
||||
method: "GET",
|
||||
url: "/:organizationId/invoices",
|
||||
config: {
|
||||
rateLimit: readLimit
|
||||
},
|
||||
schema: {
|
||||
params: z.object({
|
||||
organizationId: z.string().trim()
|
||||
@ -373,15 +438,19 @@ export const registerLicenseRouter = async (server: FastifyZodProvider) => {
|
||||
actorId: req.permission.id,
|
||||
actor: req.permission.type,
|
||||
actorOrgId: req.permission.orgId,
|
||||
orgId: req.params.organizationId
|
||||
orgId: req.params.organizationId,
|
||||
actorAuthMethod: req.permission.authMethod
|
||||
});
|
||||
return data;
|
||||
}
|
||||
});
|
||||
|
||||
server.route({
|
||||
url: "/:organizationId/licenses",
|
||||
method: "GET",
|
||||
url: "/:organizationId/licenses",
|
||||
config: {
|
||||
rateLimit: readLimit
|
||||
},
|
||||
schema: {
|
||||
params: z.object({
|
||||
organizationId: z.string().trim()
|
||||
@ -396,6 +465,7 @@ export const registerLicenseRouter = async (server: FastifyZodProvider) => {
|
||||
actorId: req.permission.id,
|
||||
actor: req.permission.type,
|
||||
actorOrgId: req.permission.orgId,
|
||||
actorAuthMethod: req.permission.authMethod,
|
||||
orgId: req.params.organizationId
|
||||
});
|
||||
return data;
|
||||
|
@ -2,6 +2,7 @@ import slugify from "@sindresorhus/slugify";
|
||||
import { z } from "zod";
|
||||
|
||||
import { OrgMembershipRole, OrgMembershipsSchema, OrgRolesSchema } from "@app/db/schemas";
|
||||
import { readLimit, writeLimit } from "@app/server/config/rateLimiter";
|
||||
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
|
||||
import { AuthMode } from "@app/services/auth/auth-type";
|
||||
|
||||
@ -9,6 +10,9 @@ export const registerOrgRoleRouter = async (server: FastifyZodProvider) => {
|
||||
server.route({
|
||||
method: "POST",
|
||||
url: "/:organizationId/roles",
|
||||
config: {
|
||||
rateLimit: writeLimit
|
||||
},
|
||||
schema: {
|
||||
params: z.object({
|
||||
organizationId: z.string().trim()
|
||||
@ -19,7 +23,7 @@ export const registerOrgRoleRouter = async (server: FastifyZodProvider) => {
|
||||
.min(1)
|
||||
.trim()
|
||||
.refine(
|
||||
(val) => Object.keys(OrgMembershipRole).includes(val),
|
||||
(val) => !Object.keys(OrgMembershipRole).includes(val),
|
||||
"Please choose a different slug, the slug you have entered is reserved"
|
||||
)
|
||||
.refine((v) => slugify(v) === v, {
|
||||
@ -41,6 +45,7 @@ export const registerOrgRoleRouter = async (server: FastifyZodProvider) => {
|
||||
req.permission.id,
|
||||
req.params.organizationId,
|
||||
req.body,
|
||||
req.permission.authMethod,
|
||||
req.permission.orgId
|
||||
);
|
||||
return { role };
|
||||
@ -50,6 +55,9 @@ export const registerOrgRoleRouter = async (server: FastifyZodProvider) => {
|
||||
server.route({
|
||||
method: "PATCH",
|
||||
url: "/:organizationId/roles/:roleId",
|
||||
config: {
|
||||
rateLimit: writeLimit
|
||||
},
|
||||
schema: {
|
||||
params: z.object({
|
||||
organizationId: z.string().trim(),
|
||||
@ -84,6 +92,7 @@ export const registerOrgRoleRouter = async (server: FastifyZodProvider) => {
|
||||
req.params.organizationId,
|
||||
req.params.roleId,
|
||||
req.body,
|
||||
req.permission.authMethod,
|
||||
req.permission.orgId
|
||||
);
|
||||
return { role };
|
||||
@ -93,6 +102,9 @@ export const registerOrgRoleRouter = async (server: FastifyZodProvider) => {
|
||||
server.route({
|
||||
method: "DELETE",
|
||||
url: "/:organizationId/roles/:roleId",
|
||||
config: {
|
||||
rateLimit: writeLimit
|
||||
},
|
||||
schema: {
|
||||
params: z.object({
|
||||
organizationId: z.string().trim(),
|
||||
@ -110,6 +122,7 @@ export const registerOrgRoleRouter = async (server: FastifyZodProvider) => {
|
||||
req.permission.id,
|
||||
req.params.organizationId,
|
||||
req.params.roleId,
|
||||
req.permission.authMethod,
|
||||
req.permission.orgId
|
||||
);
|
||||
return { role };
|
||||
@ -119,6 +132,9 @@ export const registerOrgRoleRouter = async (server: FastifyZodProvider) => {
|
||||
server.route({
|
||||
method: "GET",
|
||||
url: "/:organizationId/roles",
|
||||
config: {
|
||||
rateLimit: readLimit
|
||||
},
|
||||
schema: {
|
||||
params: z.object({
|
||||
organizationId: z.string().trim()
|
||||
@ -138,6 +154,7 @@ export const registerOrgRoleRouter = async (server: FastifyZodProvider) => {
|
||||
const roles = await server.services.orgRole.listRoles(
|
||||
req.permission.id,
|
||||
req.params.organizationId,
|
||||
req.permission.authMethod,
|
||||
req.permission.orgId
|
||||
);
|
||||
return { data: { roles } };
|
||||
@ -147,6 +164,9 @@ export const registerOrgRoleRouter = async (server: FastifyZodProvider) => {
|
||||
server.route({
|
||||
method: "GET",
|
||||
url: "/:organizationId/permissions",
|
||||
config: {
|
||||
rateLimit: readLimit
|
||||
},
|
||||
schema: {
|
||||
params: z.object({
|
||||
organizationId: z.string().trim()
|
||||
@ -163,6 +183,7 @@ export const registerOrgRoleRouter = async (server: FastifyZodProvider) => {
|
||||
const { permissions, membership } = await server.services.orgRole.getUserPermission(
|
||||
req.permission.id,
|
||||
req.params.organizationId,
|
||||
req.permission.authMethod,
|
||||
req.permission.orgId
|
||||
);
|
||||
return { permissions, membership };
|
||||
|
@ -1,6 +1,7 @@
|
||||
import { z } from "zod";
|
||||
|
||||
import { ProjectMembershipsSchema, ProjectRolesSchema } from "@app/db/schemas";
|
||||
import { readLimit, writeLimit } from "@app/server/config/rateLimiter";
|
||||
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
|
||||
import { AuthMode } from "@app/services/auth/auth-type";
|
||||
|
||||
@ -8,6 +9,9 @@ export const registerProjectRoleRouter = async (server: FastifyZodProvider) => {
|
||||
server.route({
|
||||
method: "POST",
|
||||
url: "/:projectId/roles",
|
||||
config: {
|
||||
rateLimit: writeLimit
|
||||
},
|
||||
schema: {
|
||||
params: z.object({
|
||||
projectId: z.string().trim()
|
||||
@ -31,6 +35,7 @@ export const registerProjectRoleRouter = async (server: FastifyZodProvider) => {
|
||||
req.permission.id,
|
||||
req.params.projectId,
|
||||
req.body,
|
||||
req.permission.authMethod,
|
||||
req.permission.orgId
|
||||
);
|
||||
return { role };
|
||||
@ -40,6 +45,9 @@ export const registerProjectRoleRouter = async (server: FastifyZodProvider) => {
|
||||
server.route({
|
||||
method: "PATCH",
|
||||
url: "/:projectId/roles/:roleId",
|
||||
config: {
|
||||
rateLimit: writeLimit
|
||||
},
|
||||
schema: {
|
||||
params: z.object({
|
||||
projectId: z.string().trim(),
|
||||
@ -65,6 +73,7 @@ export const registerProjectRoleRouter = async (server: FastifyZodProvider) => {
|
||||
req.params.projectId,
|
||||
req.params.roleId,
|
||||
req.body,
|
||||
req.permission.authMethod,
|
||||
req.permission.orgId
|
||||
);
|
||||
return { role };
|
||||
@ -74,6 +83,9 @@ export const registerProjectRoleRouter = async (server: FastifyZodProvider) => {
|
||||
server.route({
|
||||
method: "DELETE",
|
||||
url: "/:projectId/roles/:roleId",
|
||||
config: {
|
||||
rateLimit: writeLimit
|
||||
},
|
||||
schema: {
|
||||
params: z.object({
|
||||
projectId: z.string().trim(),
|
||||
@ -92,6 +104,7 @@ export const registerProjectRoleRouter = async (server: FastifyZodProvider) => {
|
||||
req.permission.id,
|
||||
req.params.projectId,
|
||||
req.params.roleId,
|
||||
req.permission.authMethod,
|
||||
req.permission.orgId
|
||||
);
|
||||
return { role };
|
||||
@ -101,6 +114,9 @@ export const registerProjectRoleRouter = async (server: FastifyZodProvider) => {
|
||||
server.route({
|
||||
method: "GET",
|
||||
url: "/:projectId/roles",
|
||||
config: {
|
||||
rateLimit: readLimit
|
||||
},
|
||||
schema: {
|
||||
params: z.object({
|
||||
projectId: z.string().trim()
|
||||
@ -121,6 +137,7 @@ export const registerProjectRoleRouter = async (server: FastifyZodProvider) => {
|
||||
req.permission.type,
|
||||
req.permission.id,
|
||||
req.params.projectId,
|
||||
req.permission.authMethod,
|
||||
req.permission.orgId
|
||||
);
|
||||
return { data: { roles } };
|
||||
@ -130,6 +147,9 @@ export const registerProjectRoleRouter = async (server: FastifyZodProvider) => {
|
||||
server.route({
|
||||
method: "GET",
|
||||
url: "/:projectId/permissions",
|
||||
config: {
|
||||
rateLimit: readLimit
|
||||
},
|
||||
schema: {
|
||||
params: z.object({
|
||||
projectId: z.string().trim()
|
||||
@ -148,8 +168,10 @@ export const registerProjectRoleRouter = async (server: FastifyZodProvider) => {
|
||||
const { permissions, membership } = await server.services.projectRole.getUserPermission(
|
||||
req.permission.id,
|
||||
req.params.projectId,
|
||||
req.permission.authMethod,
|
||||
req.permission.orgId
|
||||
);
|
||||
|
||||
return { data: { permissions, membership } };
|
||||
}
|
||||
});
|
||||
|
@ -3,7 +3,8 @@ import { z } from "zod";
|
||||
import { AuditLogsSchema, SecretSnapshotsSchema } from "@app/db/schemas";
|
||||
import { EventType, UserAgentType } from "@app/ee/services/audit-log/audit-log-types";
|
||||
import { AUDIT_LOGS, PROJECTS } from "@app/lib/api-docs";
|
||||
import { removeTrailingSlash } from "@app/lib/fn";
|
||||
import { getLastMidnightDateISO, removeTrailingSlash } from "@app/lib/fn";
|
||||
import { readLimit } from "@app/server/config/rateLimiter";
|
||||
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
|
||||
import { AuthMode } from "@app/services/auth/auth-type";
|
||||
|
||||
@ -11,6 +12,9 @@ export const registerProjectRouter = async (server: FastifyZodProvider) => {
|
||||
server.route({
|
||||
method: "GET",
|
||||
url: "/:workspaceId/secret-snapshots",
|
||||
config: {
|
||||
rateLimit: readLimit
|
||||
},
|
||||
schema: {
|
||||
description: "Return project secret snapshots ids",
|
||||
security: [
|
||||
@ -38,6 +42,7 @@ export const registerProjectRouter = async (server: FastifyZodProvider) => {
|
||||
handler: async (req) => {
|
||||
const secretSnapshots = await server.services.snapshot.listSnapshots({
|
||||
actor: req.permission.type,
|
||||
actorAuthMethod: req.permission.authMethod,
|
||||
actorId: req.permission.id,
|
||||
actorOrgId: req.permission.orgId,
|
||||
projectId: req.params.workspaceId,
|
||||
@ -50,6 +55,9 @@ export const registerProjectRouter = async (server: FastifyZodProvider) => {
|
||||
server.route({
|
||||
method: "GET",
|
||||
url: "/:workspaceId/secret-snapshots/count",
|
||||
config: {
|
||||
rateLimit: readLimit
|
||||
},
|
||||
schema: {
|
||||
params: z.object({
|
||||
workspaceId: z.string().trim()
|
||||
@ -69,6 +77,7 @@ export const registerProjectRouter = async (server: FastifyZodProvider) => {
|
||||
const count = await server.services.snapshot.projectSecretSnapshotCount({
|
||||
actor: req.permission.type,
|
||||
actorId: req.permission.id,
|
||||
actorAuthMethod: req.permission.authMethod,
|
||||
actorOrgId: req.permission.orgId,
|
||||
projectId: req.params.workspaceId,
|
||||
environment: req.query.environment,
|
||||
@ -81,6 +90,9 @@ export const registerProjectRouter = async (server: FastifyZodProvider) => {
|
||||
server.route({
|
||||
method: "GET",
|
||||
url: "/:workspaceId/audit-logs",
|
||||
config: {
|
||||
rateLimit: readLimit
|
||||
},
|
||||
schema: {
|
||||
description: "Return audit logs",
|
||||
security: [
|
||||
@ -130,8 +142,10 @@ export const registerProjectRouter = async (server: FastifyZodProvider) => {
|
||||
const auditLogs = await server.services.auditLog.listProjectAuditLogs({
|
||||
actorId: req.permission.id,
|
||||
actorOrgId: req.permission.orgId,
|
||||
actorAuthMethod: req.permission.authMethod,
|
||||
projectId: req.params.workspaceId,
|
||||
...req.query,
|
||||
startDate: req.query.endDate || getLastMidnightDateISO(),
|
||||
auditLogActor: req.query.actor,
|
||||
actor: req.permission.type
|
||||
});
|
||||
@ -142,6 +156,9 @@ export const registerProjectRouter = async (server: FastifyZodProvider) => {
|
||||
server.route({
|
||||
method: "GET",
|
||||
url: "/:workspaceId/audit-logs/filters/actors",
|
||||
config: {
|
||||
rateLimit: readLimit
|
||||
},
|
||||
schema: {
|
||||
params: z.object({
|
||||
workspaceId: z.string().trim()
|
||||
|
@ -17,6 +17,7 @@ import { SamlProviders, TGetSamlCfgDTO } from "@app/ee/services/saml-config/saml
|
||||
import { getConfig } from "@app/lib/config/env";
|
||||
import { BadRequestError } from "@app/lib/errors";
|
||||
import { logger } from "@app/lib/logger";
|
||||
import { readLimit, writeLimit } from "@app/server/config/rateLimiter";
|
||||
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
|
||||
import { AuthMode } from "@app/services/auth/auth-type";
|
||||
|
||||
@ -203,8 +204,11 @@ export const registerSamlRouter = async (server: FastifyZodProvider) => {
|
||||
});
|
||||
|
||||
server.route({
|
||||
url: "/config",
|
||||
method: "GET",
|
||||
url: "/config",
|
||||
config: {
|
||||
rateLimit: readLimit
|
||||
},
|
||||
onRequest: verifyAuth([AuthMode.JWT]),
|
||||
schema: {
|
||||
querystring: z.object({
|
||||
@ -231,6 +235,7 @@ export const registerSamlRouter = async (server: FastifyZodProvider) => {
|
||||
actor: req.permission.type,
|
||||
actorId: req.permission.id,
|
||||
actorOrgId: req.permission.orgId,
|
||||
actorAuthMethod: req.permission.authMethod,
|
||||
orgId: req.query.organizationId,
|
||||
type: "org"
|
||||
});
|
||||
@ -239,8 +244,11 @@ export const registerSamlRouter = async (server: FastifyZodProvider) => {
|
||||
});
|
||||
|
||||
server.route({
|
||||
url: "/config",
|
||||
method: "POST",
|
||||
url: "/config",
|
||||
config: {
|
||||
rateLimit: writeLimit
|
||||
},
|
||||
onRequest: verifyAuth([AuthMode.JWT]),
|
||||
schema: {
|
||||
body: z.object({
|
||||
@ -259,6 +267,7 @@ export const registerSamlRouter = async (server: FastifyZodProvider) => {
|
||||
const saml = await server.services.saml.createSamlCfg({
|
||||
actor: req.permission.type,
|
||||
actorId: req.permission.id,
|
||||
actorAuthMethod: req.permission.authMethod,
|
||||
actorOrgId: req.permission.orgId,
|
||||
orgId: req.body.organizationId,
|
||||
...req.body
|
||||
@ -268,8 +277,11 @@ export const registerSamlRouter = async (server: FastifyZodProvider) => {
|
||||
});
|
||||
|
||||
server.route({
|
||||
url: "/config",
|
||||
method: "PATCH",
|
||||
url: "/config",
|
||||
config: {
|
||||
rateLimit: writeLimit
|
||||
},
|
||||
onRequest: verifyAuth([AuthMode.JWT]),
|
||||
schema: {
|
||||
body: z
|
||||
@ -290,6 +302,7 @@ export const registerSamlRouter = async (server: FastifyZodProvider) => {
|
||||
const saml = await server.services.saml.updateSamlCfg({
|
||||
actor: req.permission.type,
|
||||
actorId: req.permission.id,
|
||||
actorAuthMethod: req.permission.authMethod,
|
||||
actorOrgId: req.permission.orgId,
|
||||
orgId: req.body.organizationId,
|
||||
...req.body
|
||||
|
@ -1,6 +1,7 @@
|
||||
import { z } from "zod";
|
||||
|
||||
import { ScimTokensSchema } from "@app/db/schemas";
|
||||
import { readLimit, writeLimit } from "@app/server/config/rateLimiter";
|
||||
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
|
||||
import { AuthMode } from "@app/services/auth/auth-type";
|
||||
|
||||
@ -20,6 +21,9 @@ export const registerScimRouter = async (server: FastifyZodProvider) => {
|
||||
server.route({
|
||||
url: "/scim-tokens",
|
||||
method: "POST",
|
||||
config: {
|
||||
rateLimit: writeLimit
|
||||
},
|
||||
onRequest: verifyAuth([AuthMode.JWT]),
|
||||
schema: {
|
||||
body: z.object({
|
||||
@ -39,6 +43,7 @@ export const registerScimRouter = async (server: FastifyZodProvider) => {
|
||||
actorId: req.permission.id,
|
||||
actorOrgId: req.permission.orgId,
|
||||
orgId: req.body.organizationId,
|
||||
actorAuthMethod: req.permission.authMethod,
|
||||
description: req.body.description,
|
||||
ttlDays: req.body.ttlDays
|
||||
});
|
||||
@ -50,6 +55,9 @@ export const registerScimRouter = async (server: FastifyZodProvider) => {
|
||||
server.route({
|
||||
url: "/scim-tokens",
|
||||
method: "GET",
|
||||
config: {
|
||||
rateLimit: readLimit
|
||||
},
|
||||
onRequest: verifyAuth([AuthMode.JWT]),
|
||||
schema: {
|
||||
querystring: z.object({
|
||||
@ -65,6 +73,7 @@ export const registerScimRouter = async (server: FastifyZodProvider) => {
|
||||
const scimTokens = await server.services.scim.listScimTokens({
|
||||
actor: req.permission.type,
|
||||
actorId: req.permission.id,
|
||||
actorAuthMethod: req.permission.authMethod,
|
||||
actorOrgId: req.permission.orgId,
|
||||
orgId: req.query.organizationId
|
||||
});
|
||||
@ -76,6 +85,9 @@ export const registerScimRouter = async (server: FastifyZodProvider) => {
|
||||
server.route({
|
||||
url: "/scim-tokens/:scimTokenId",
|
||||
method: "DELETE",
|
||||
config: {
|
||||
rateLimit: writeLimit
|
||||
},
|
||||
onRequest: verifyAuth([AuthMode.JWT]),
|
||||
schema: {
|
||||
params: z.object({
|
||||
@ -92,6 +104,7 @@ export const registerScimRouter = async (server: FastifyZodProvider) => {
|
||||
scimTokenId: req.params.scimTokenId,
|
||||
actor: req.permission.type,
|
||||
actorId: req.permission.id,
|
||||
actorAuthMethod: req.permission.authMethod,
|
||||
actorOrgId: req.permission.orgId
|
||||
});
|
||||
|
||||
@ -143,7 +156,7 @@ export const registerScimRouter = async (server: FastifyZodProvider) => {
|
||||
offset: req.query.startIndex,
|
||||
limit: req.query.count,
|
||||
filter: req.query.filter,
|
||||
orgId: req.permission.orgId as string
|
||||
orgId: req.permission.orgId
|
||||
});
|
||||
return users;
|
||||
}
|
||||
@ -181,7 +194,7 @@ export const registerScimRouter = async (server: FastifyZodProvider) => {
|
||||
handler: async (req) => {
|
||||
const user = await req.server.services.scim.getScimUser({
|
||||
userId: req.params.userId,
|
||||
orgId: req.permission.orgId as string
|
||||
orgId: req.permission.orgId
|
||||
});
|
||||
return user;
|
||||
}
|
||||
@ -193,7 +206,7 @@ export const registerScimRouter = async (server: FastifyZodProvider) => {
|
||||
schema: {
|
||||
body: z.object({
|
||||
schemas: z.array(z.string()),
|
||||
userName: z.string().trim().email(),
|
||||
userName: z.string().trim(),
|
||||
name: z.object({
|
||||
familyName: z.string().trim(),
|
||||
givenName: z.string().trim()
|
||||
@ -214,7 +227,7 @@ export const registerScimRouter = async (server: FastifyZodProvider) => {
|
||||
200: z.object({
|
||||
schemas: z.array(z.string()),
|
||||
id: z.string().trim(),
|
||||
userName: z.string().trim().email(),
|
||||
userName: z.string().trim(),
|
||||
name: z.object({
|
||||
familyName: z.string().trim(),
|
||||
givenName: z.string().trim()
|
||||
@ -240,7 +253,7 @@ export const registerScimRouter = async (server: FastifyZodProvider) => {
|
||||
email: primaryEmail,
|
||||
firstName: req.body.name.givenName,
|
||||
lastName: req.body.name.familyName,
|
||||
orgId: req.permission.orgId as string
|
||||
orgId: req.permission.orgId
|
||||
});
|
||||
|
||||
return user;
|
||||
@ -249,38 +262,257 @@ export const registerScimRouter = async (server: FastifyZodProvider) => {
|
||||
|
||||
server.route({
|
||||
url: "/Users/:userId",
|
||||
method: "PATCH",
|
||||
method: "DELETE",
|
||||
schema: {
|
||||
params: z.object({
|
||||
userId: z.string().trim()
|
||||
}),
|
||||
body: z.object({
|
||||
schemas: z.array(z.string()),
|
||||
Operations: z.array(
|
||||
z.object({
|
||||
op: z.string().trim(),
|
||||
path: z.string().trim().optional(),
|
||||
value: z.union([
|
||||
z.object({
|
||||
active: z.boolean()
|
||||
}),
|
||||
z.string().trim()
|
||||
])
|
||||
})
|
||||
)
|
||||
}),
|
||||
response: {
|
||||
200: z.object({})
|
||||
}
|
||||
},
|
||||
onRequest: verifyAuth([AuthMode.SCIM_TOKEN]),
|
||||
handler: async (req) => {
|
||||
const user = await req.server.services.scim.updateScimUser({
|
||||
const user = await req.server.services.scim.deleteScimUser({
|
||||
userId: req.params.userId,
|
||||
orgId: req.permission.orgId as string,
|
||||
orgId: req.permission.orgId
|
||||
});
|
||||
|
||||
return user;
|
||||
}
|
||||
});
|
||||
|
||||
server.route({
|
||||
url: "/Groups",
|
||||
method: "POST",
|
||||
schema: {
|
||||
body: z.object({
|
||||
schemas: z.array(z.string()),
|
||||
displayName: z.string().trim(),
|
||||
members: z.array(z.any()).length(0).optional() // okta-specific
|
||||
}),
|
||||
response: {
|
||||
200: z.object({
|
||||
schemas: z.array(z.string()),
|
||||
id: z.string().trim(),
|
||||
displayName: z.string().trim(),
|
||||
members: z.array(z.any()).length(0),
|
||||
meta: z.object({
|
||||
resourceType: z.string().trim()
|
||||
})
|
||||
})
|
||||
}
|
||||
},
|
||||
onRequest: verifyAuth([AuthMode.SCIM_TOKEN]),
|
||||
handler: async (req) => {
|
||||
const group = await req.server.services.scim.createScimGroup({
|
||||
displayName: req.body.displayName,
|
||||
orgId: req.permission.orgId
|
||||
});
|
||||
|
||||
return group;
|
||||
}
|
||||
});
|
||||
|
||||
server.route({
|
||||
url: "/Groups",
|
||||
method: "GET",
|
||||
schema: {
|
||||
querystring: z.object({
|
||||
startIndex: z.coerce.number().default(1),
|
||||
count: z.coerce.number().default(20),
|
||||
filter: z.string().trim().optional()
|
||||
}),
|
||||
response: {
|
||||
200: z.object({
|
||||
Resources: z.array(
|
||||
z.object({
|
||||
schemas: z.array(z.string()),
|
||||
id: z.string().trim(),
|
||||
displayName: z.string().trim(),
|
||||
members: z.array(z.any()).length(0),
|
||||
meta: z.object({
|
||||
resourceType: z.string().trim()
|
||||
})
|
||||
})
|
||||
),
|
||||
itemsPerPage: z.number(),
|
||||
schemas: z.array(z.string()),
|
||||
startIndex: z.number(),
|
||||
totalResults: z.number()
|
||||
})
|
||||
}
|
||||
},
|
||||
onRequest: verifyAuth([AuthMode.SCIM_TOKEN]),
|
||||
handler: async (req) => {
|
||||
const groups = await req.server.services.scim.listScimGroups({
|
||||
orgId: req.permission.orgId,
|
||||
offset: req.query.startIndex,
|
||||
limit: req.query.count
|
||||
});
|
||||
|
||||
return groups;
|
||||
}
|
||||
});
|
||||
|
||||
server.route({
|
||||
url: "/Groups/:groupId",
|
||||
method: "GET",
|
||||
schema: {
|
||||
params: z.object({
|
||||
groupId: z.string().trim()
|
||||
}),
|
||||
response: {
|
||||
200: z.object({
|
||||
schemas: z.array(z.string()),
|
||||
id: z.string().trim(),
|
||||
displayName: z.string().trim(),
|
||||
members: z.array(
|
||||
z.object({
|
||||
value: z.string(),
|
||||
display: z.string()
|
||||
})
|
||||
),
|
||||
meta: z.object({
|
||||
resourceType: z.string().trim()
|
||||
})
|
||||
})
|
||||
}
|
||||
},
|
||||
onRequest: verifyAuth([AuthMode.SCIM_TOKEN]),
|
||||
handler: async (req) => {
|
||||
const group = await req.server.services.scim.getScimGroup({
|
||||
groupId: req.params.groupId,
|
||||
orgId: req.permission.orgId
|
||||
});
|
||||
return group;
|
||||
}
|
||||
});
|
||||
|
||||
server.route({
|
||||
url: "/Groups/:groupId",
|
||||
method: "PUT",
|
||||
schema: {
|
||||
params: z.object({
|
||||
groupId: z.string().trim()
|
||||
}),
|
||||
body: z.object({
|
||||
schemas: z.array(z.string()),
|
||||
id: z.string().trim(),
|
||||
displayName: z.string().trim(),
|
||||
members: z.array(z.any()).length(0)
|
||||
}),
|
||||
response: {
|
||||
200: z.object({
|
||||
schemas: z.array(z.string()),
|
||||
id: z.string().trim(),
|
||||
displayName: z.string().trim(),
|
||||
members: z.array(
|
||||
z.object({
|
||||
value: z.string(),
|
||||
display: z.string()
|
||||
})
|
||||
),
|
||||
meta: z.object({
|
||||
resourceType: z.string().trim()
|
||||
})
|
||||
})
|
||||
}
|
||||
},
|
||||
onRequest: verifyAuth([AuthMode.SCIM_TOKEN]),
|
||||
handler: async (req) => {
|
||||
const group = await req.server.services.scim.updateScimGroupNamePut({
|
||||
groupId: req.params.groupId,
|
||||
orgId: req.permission.orgId,
|
||||
displayName: req.body.displayName
|
||||
});
|
||||
|
||||
return group;
|
||||
}
|
||||
});
|
||||
|
||||
server.route({
|
||||
url: "/Groups/:groupId",
|
||||
method: "PATCH",
|
||||
schema: {
|
||||
params: z.object({
|
||||
groupId: z.string().trim()
|
||||
}),
|
||||
body: z.object({
|
||||
schemas: z.array(z.string()),
|
||||
Operations: z.array(
|
||||
z.union([
|
||||
z.object({
|
||||
op: z.literal("replace"),
|
||||
value: z.object({
|
||||
id: z.string().trim(),
|
||||
displayName: z.string().trim()
|
||||
})
|
||||
}),
|
||||
z.object({
|
||||
op: z.literal("remove"),
|
||||
path: z.string().trim()
|
||||
}),
|
||||
z.object({
|
||||
op: z.literal("add"),
|
||||
value: z.object({
|
||||
value: z.string().trim(),
|
||||
display: z.string().trim().optional()
|
||||
})
|
||||
})
|
||||
])
|
||||
)
|
||||
}),
|
||||
response: {
|
||||
200: z.object({
|
||||
schemas: z.array(z.string()),
|
||||
id: z.string().trim(),
|
||||
displayName: z.string().trim(),
|
||||
members: z.array(
|
||||
z.object({
|
||||
value: z.string(),
|
||||
display: z.string()
|
||||
})
|
||||
),
|
||||
meta: z.object({
|
||||
resourceType: z.string().trim()
|
||||
})
|
||||
})
|
||||
}
|
||||
},
|
||||
onRequest: verifyAuth([AuthMode.SCIM_TOKEN]),
|
||||
handler: async (req) => {
|
||||
// console.log("PATCH /Groups/:groupId req.body: ", req.body);
|
||||
// console.log("PATCH /Groups/:groupId req.body: ", req.body.Operations[0]);
|
||||
const group = await req.server.services.scim.updateScimGroupNamePatch({
|
||||
groupId: req.params.groupId,
|
||||
orgId: req.permission.orgId,
|
||||
operations: req.body.Operations
|
||||
});
|
||||
return user;
|
||||
|
||||
return group;
|
||||
}
|
||||
});
|
||||
|
||||
server.route({
|
||||
url: "/Groups/:groupId",
|
||||
method: "DELETE",
|
||||
schema: {
|
||||
params: z.object({
|
||||
groupId: z.string().trim()
|
||||
}),
|
||||
response: {
|
||||
200: z.object({})
|
||||
}
|
||||
},
|
||||
onRequest: verifyAuth([AuthMode.SCIM_TOKEN]),
|
||||
handler: async (req) => {
|
||||
const group = await req.server.services.scim.deleteScimGroup({
|
||||
groupId: req.params.groupId,
|
||||
orgId: req.permission.orgId
|
||||
});
|
||||
|
||||
return group;
|
||||
}
|
||||
});
|
||||
|
||||
@ -327,7 +559,7 @@ export const registerScimRouter = async (server: FastifyZodProvider) => {
|
||||
handler: async (req) => {
|
||||
const user = await req.server.services.scim.replaceScimUser({
|
||||
userId: req.params.userId,
|
||||
orgId: req.permission.orgId as string,
|
||||
orgId: req.permission.orgId,
|
||||
active: req.body.active
|
||||
});
|
||||
return user;
|
||||
|
@ -1,6 +1,7 @@
|
||||
import { nanoid } from "nanoid";
|
||||
import { z } from "zod";
|
||||
|
||||
import { readLimit, writeLimit } from "@app/server/config/rateLimiter";
|
||||
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
|
||||
import { sapPubSchema } from "@app/server/routes/sanitizedSchemas";
|
||||
import { AuthMode } from "@app/services/auth/auth-type";
|
||||
@ -9,6 +10,9 @@ export const registerSecretApprovalPolicyRouter = async (server: FastifyZodProvi
|
||||
server.route({
|
||||
url: "/",
|
||||
method: "POST",
|
||||
config: {
|
||||
rateLimit: writeLimit
|
||||
},
|
||||
schema: {
|
||||
body: z
|
||||
.object({
|
||||
@ -34,6 +38,7 @@ export const registerSecretApprovalPolicyRouter = async (server: FastifyZodProvi
|
||||
const approval = await server.services.secretApprovalPolicy.createSecretApprovalPolicy({
|
||||
actor: req.permission.type,
|
||||
actorId: req.permission.id,
|
||||
actorAuthMethod: req.permission.authMethod,
|
||||
actorOrgId: req.permission.orgId,
|
||||
projectId: req.body.workspaceId,
|
||||
...req.body,
|
||||
@ -46,6 +51,9 @@ export const registerSecretApprovalPolicyRouter = async (server: FastifyZodProvi
|
||||
server.route({
|
||||
url: "/:sapId",
|
||||
method: "PATCH",
|
||||
config: {
|
||||
rateLimit: writeLimit
|
||||
},
|
||||
schema: {
|
||||
params: z.object({
|
||||
sapId: z.string()
|
||||
@ -72,6 +80,7 @@ export const registerSecretApprovalPolicyRouter = async (server: FastifyZodProvi
|
||||
const approval = await server.services.secretApprovalPolicy.updateSecretApprovalPolicy({
|
||||
actor: req.permission.type,
|
||||
actorId: req.permission.id,
|
||||
actorAuthMethod: req.permission.authMethod,
|
||||
actorOrgId: req.permission.orgId,
|
||||
...req.body,
|
||||
secretPolicyId: req.params.sapId
|
||||
@ -83,6 +92,9 @@ export const registerSecretApprovalPolicyRouter = async (server: FastifyZodProvi
|
||||
server.route({
|
||||
url: "/:sapId",
|
||||
method: "DELETE",
|
||||
config: {
|
||||
rateLimit: writeLimit
|
||||
},
|
||||
schema: {
|
||||
params: z.object({
|
||||
sapId: z.string()
|
||||
@ -98,6 +110,7 @@ export const registerSecretApprovalPolicyRouter = async (server: FastifyZodProvi
|
||||
const approval = await server.services.secretApprovalPolicy.deleteSecretApprovalPolicy({
|
||||
actor: req.permission.type,
|
||||
actorId: req.permission.id,
|
||||
actorAuthMethod: req.permission.authMethod,
|
||||
actorOrgId: req.permission.orgId,
|
||||
secretPolicyId: req.params.sapId
|
||||
});
|
||||
@ -108,6 +121,9 @@ export const registerSecretApprovalPolicyRouter = async (server: FastifyZodProvi
|
||||
server.route({
|
||||
url: "/",
|
||||
method: "GET",
|
||||
config: {
|
||||
rateLimit: readLimit
|
||||
},
|
||||
schema: {
|
||||
querystring: z.object({
|
||||
workspaceId: z.string().trim()
|
||||
@ -123,6 +139,7 @@ export const registerSecretApprovalPolicyRouter = async (server: FastifyZodProvi
|
||||
const approvals = await server.services.secretApprovalPolicy.getSecretApprovalPolicyByProjectId({
|
||||
actor: req.permission.type,
|
||||
actorId: req.permission.id,
|
||||
actorAuthMethod: req.permission.authMethod,
|
||||
actorOrgId: req.permission.orgId,
|
||||
projectId: req.query.workspaceId
|
||||
});
|
||||
@ -133,6 +150,9 @@ export const registerSecretApprovalPolicyRouter = async (server: FastifyZodProvi
|
||||
server.route({
|
||||
url: "/board",
|
||||
method: "GET",
|
||||
config: {
|
||||
rateLimit: readLimit
|
||||
},
|
||||
schema: {
|
||||
querystring: z.object({
|
||||
workspaceId: z.string().trim(),
|
||||
@ -150,6 +170,7 @@ export const registerSecretApprovalPolicyRouter = async (server: FastifyZodProvi
|
||||
const policy = await server.services.secretApprovalPolicy.getSecretApprovalPolicyOfFolder({
|
||||
actor: req.permission.type,
|
||||
actorId: req.permission.id,
|
||||
actorAuthMethod: req.permission.authMethod,
|
||||
actorOrgId: req.permission.orgId,
|
||||
projectId: req.query.workspaceId,
|
||||
...req.query
|
||||
|
@ -10,13 +10,17 @@ import {
|
||||
} from "@app/db/schemas";
|
||||
import { EventType } from "@app/ee/services/audit-log/audit-log-types";
|
||||
import { ApprovalStatus, RequestState } from "@app/ee/services/secret-approval-request/secret-approval-request-types";
|
||||
import { readLimit, writeLimit } from "@app/server/config/rateLimiter";
|
||||
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
|
||||
import { AuthMode } from "@app/services/auth/auth-type";
|
||||
|
||||
export const registerSecretApprovalRequestRouter = async (server: FastifyZodProvider) => {
|
||||
server.route({
|
||||
url: "/",
|
||||
method: "GET",
|
||||
url: "/",
|
||||
config: {
|
||||
rateLimit: readLimit
|
||||
},
|
||||
schema: {
|
||||
querystring: z.object({
|
||||
workspaceId: z.string().trim(),
|
||||
@ -52,6 +56,7 @@ export const registerSecretApprovalRequestRouter = async (server: FastifyZodProv
|
||||
const approvals = await server.services.secretApprovalRequest.getSecretApprovals({
|
||||
actor: req.permission.type,
|
||||
actorId: req.permission.id,
|
||||
actorAuthMethod: req.permission.authMethod,
|
||||
actorOrgId: req.permission.orgId,
|
||||
...req.query,
|
||||
projectId: req.query.workspaceId
|
||||
@ -61,8 +66,11 @@ export const registerSecretApprovalRequestRouter = async (server: FastifyZodProv
|
||||
});
|
||||
|
||||
server.route({
|
||||
url: "/count",
|
||||
method: "GET",
|
||||
url: "/count",
|
||||
config: {
|
||||
rateLimit: readLimit
|
||||
},
|
||||
schema: {
|
||||
querystring: z.object({
|
||||
workspaceId: z.string().trim()
|
||||
@ -81,6 +89,7 @@ export const registerSecretApprovalRequestRouter = async (server: FastifyZodProv
|
||||
const approvals = await server.services.secretApprovalRequest.requestCount({
|
||||
actor: req.permission.type,
|
||||
actorId: req.permission.id,
|
||||
actorAuthMethod: req.permission.authMethod,
|
||||
actorOrgId: req.permission.orgId,
|
||||
projectId: req.query.workspaceId
|
||||
});
|
||||
@ -91,6 +100,9 @@ export const registerSecretApprovalRequestRouter = async (server: FastifyZodProv
|
||||
server.route({
|
||||
url: "/:id/merge",
|
||||
method: "POST",
|
||||
config: {
|
||||
rateLimit: writeLimit
|
||||
},
|
||||
schema: {
|
||||
params: z.object({
|
||||
id: z.string()
|
||||
@ -106,6 +118,7 @@ export const registerSecretApprovalRequestRouter = async (server: FastifyZodProv
|
||||
const { approval } = await server.services.secretApprovalRequest.mergeSecretApprovalRequest({
|
||||
actorId: req.permission.id,
|
||||
actor: req.permission.type,
|
||||
actorAuthMethod: req.permission.authMethod,
|
||||
actorOrgId: req.permission.orgId,
|
||||
approvalId: req.params.id
|
||||
});
|
||||
@ -114,8 +127,11 @@ export const registerSecretApprovalRequestRouter = async (server: FastifyZodProv
|
||||
});
|
||||
|
||||
server.route({
|
||||
url: "/:id/review",
|
||||
method: "POST",
|
||||
url: "/:id/review",
|
||||
config: {
|
||||
rateLimit: writeLimit
|
||||
},
|
||||
schema: {
|
||||
params: z.object({
|
||||
id: z.string()
|
||||
@ -134,6 +150,7 @@ export const registerSecretApprovalRequestRouter = async (server: FastifyZodProv
|
||||
const review = await server.services.secretApprovalRequest.reviewApproval({
|
||||
actorId: req.permission.id,
|
||||
actor: req.permission.type,
|
||||
actorAuthMethod: req.permission.authMethod,
|
||||
actorOrgId: req.permission.orgId,
|
||||
approvalId: req.params.id,
|
||||
status: req.body.status
|
||||
@ -143,8 +160,11 @@ export const registerSecretApprovalRequestRouter = async (server: FastifyZodProv
|
||||
});
|
||||
|
||||
server.route({
|
||||
url: "/:id/status",
|
||||
method: "POST",
|
||||
url: "/:id/status",
|
||||
config: {
|
||||
rateLimit: writeLimit
|
||||
},
|
||||
schema: {
|
||||
params: z.object({
|
||||
id: z.string()
|
||||
@ -163,6 +183,7 @@ export const registerSecretApprovalRequestRouter = async (server: FastifyZodProv
|
||||
const approval = await server.services.secretApprovalRequest.updateApprovalStatus({
|
||||
actorId: req.permission.id,
|
||||
actor: req.permission.type,
|
||||
actorAuthMethod: req.permission.authMethod,
|
||||
actorOrgId: req.permission.orgId,
|
||||
approvalId: req.params.id,
|
||||
status: req.body.status
|
||||
@ -198,8 +219,11 @@ export const registerSecretApprovalRequestRouter = async (server: FastifyZodProv
|
||||
.array()
|
||||
.optional();
|
||||
server.route({
|
||||
url: "/:id",
|
||||
method: "GET",
|
||||
url: "/:id",
|
||||
config: {
|
||||
rateLimit: readLimit
|
||||
},
|
||||
schema: {
|
||||
params: z.object({
|
||||
id: z.string()
|
||||
@ -271,6 +295,7 @@ export const registerSecretApprovalRequestRouter = async (server: FastifyZodProv
|
||||
const approval = await server.services.secretApprovalRequest.getSecretApprovalDetails({
|
||||
actor: req.permission.type,
|
||||
actorId: req.permission.id,
|
||||
actorAuthMethod: req.permission.authMethod,
|
||||
actorOrgId: req.permission.orgId,
|
||||
id: req.params.id
|
||||
});
|
||||
|
@ -1,12 +1,16 @@
|
||||
import { z } from "zod";
|
||||
|
||||
import { readLimit } from "@app/server/config/rateLimiter";
|
||||
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
|
||||
import { AuthMode } from "@app/services/auth/auth-type";
|
||||
|
||||
export const registerSecretRotationProviderRouter = async (server: FastifyZodProvider) => {
|
||||
server.route({
|
||||
url: "/:workspaceId",
|
||||
method: "GET",
|
||||
url: "/:workspaceId",
|
||||
config: {
|
||||
rateLimit: readLimit
|
||||
},
|
||||
schema: {
|
||||
params: z.object({
|
||||
workspaceId: z.string().trim()
|
||||
@ -30,6 +34,7 @@ export const registerSecretRotationProviderRouter = async (server: FastifyZodPro
|
||||
const providers = await server.services.secretRotation.getProviderTemplates({
|
||||
actor: req.permission.type,
|
||||
actorId: req.permission.id,
|
||||
actorAuthMethod: req.permission.authMethod,
|
||||
actorOrgId: req.permission.orgId,
|
||||
projectId: req.params.workspaceId
|
||||
});
|
||||
|
@ -2,13 +2,17 @@ import { z } from "zod";
|
||||
|
||||
import { SecretRotationOutputsSchema, SecretRotationsSchema, SecretsSchema } from "@app/db/schemas";
|
||||
import { removeTrailingSlash } from "@app/lib/fn";
|
||||
import { readLimit, writeLimit } from "@app/server/config/rateLimiter";
|
||||
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
|
||||
import { AuthMode } from "@app/services/auth/auth-type";
|
||||
|
||||
export const registerSecretRotationRouter = async (server: FastifyZodProvider) => {
|
||||
server.route({
|
||||
url: "/",
|
||||
method: "POST",
|
||||
url: "/",
|
||||
config: {
|
||||
rateLimit: writeLimit
|
||||
},
|
||||
schema: {
|
||||
body: z.object({
|
||||
workspaceId: z.string().trim(),
|
||||
@ -39,6 +43,7 @@ export const registerSecretRotationRouter = async (server: FastifyZodProvider) =
|
||||
handler: async (req) => {
|
||||
const secretRotation = await server.services.secretRotation.createRotation({
|
||||
actor: req.permission.type,
|
||||
actorAuthMethod: req.permission.authMethod,
|
||||
actorId: req.permission.id,
|
||||
actorOrgId: req.permission.orgId,
|
||||
...req.body,
|
||||
@ -51,6 +56,9 @@ export const registerSecretRotationRouter = async (server: FastifyZodProvider) =
|
||||
server.route({
|
||||
url: "/restart",
|
||||
method: "POST",
|
||||
config: {
|
||||
rateLimit: writeLimit
|
||||
},
|
||||
schema: {
|
||||
body: z.object({
|
||||
id: z.string().trim()
|
||||
@ -74,6 +82,7 @@ export const registerSecretRotationRouter = async (server: FastifyZodProvider) =
|
||||
const secretRotation = await server.services.secretRotation.restartById({
|
||||
actor: req.permission.type,
|
||||
actorId: req.permission.id,
|
||||
actorAuthMethod: req.permission.authMethod,
|
||||
actorOrgId: req.permission.orgId,
|
||||
rotationId: req.body.id
|
||||
});
|
||||
@ -84,6 +93,9 @@ export const registerSecretRotationRouter = async (server: FastifyZodProvider) =
|
||||
server.route({
|
||||
url: "/",
|
||||
method: "GET",
|
||||
config: {
|
||||
rateLimit: readLimit
|
||||
},
|
||||
schema: {
|
||||
querystring: z.object({
|
||||
workspaceId: z.string().trim()
|
||||
@ -125,6 +137,7 @@ export const registerSecretRotationRouter = async (server: FastifyZodProvider) =
|
||||
const secretRotations = await server.services.secretRotation.getByProjectId({
|
||||
actor: req.permission.type,
|
||||
actorId: req.permission.id,
|
||||
actorAuthMethod: req.permission.authMethod,
|
||||
actorOrgId: req.permission.orgId,
|
||||
projectId: req.query.workspaceId
|
||||
});
|
||||
@ -133,8 +146,11 @@ export const registerSecretRotationRouter = async (server: FastifyZodProvider) =
|
||||
});
|
||||
|
||||
server.route({
|
||||
url: "/:id",
|
||||
method: "DELETE",
|
||||
url: "/:id",
|
||||
config: {
|
||||
rateLimit: writeLimit
|
||||
},
|
||||
schema: {
|
||||
params: z.object({
|
||||
id: z.string().trim()
|
||||
@ -158,6 +174,7 @@ export const registerSecretRotationRouter = async (server: FastifyZodProvider) =
|
||||
const secretRotation = await server.services.secretRotation.deleteById({
|
||||
actor: req.permission.type,
|
||||
actorId: req.permission.id,
|
||||
actorAuthMethod: req.permission.authMethod,
|
||||
actorOrgId: req.permission.orgId,
|
||||
rotationId: req.params.id
|
||||
});
|
||||
|
@ -2,13 +2,17 @@ import { z } from "zod";
|
||||
|
||||
import { GitAppOrgSchema, SecretScanningGitRisksSchema } from "@app/db/schemas";
|
||||
import { SecretScanningRiskStatus } from "@app/ee/services/secret-scanning/secret-scanning-types";
|
||||
import { readLimit, writeLimit } from "@app/server/config/rateLimiter";
|
||||
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
|
||||
import { AuthMode } from "@app/services/auth/auth-type";
|
||||
|
||||
export const registerSecretScanningRouter = async (server: FastifyZodProvider) => {
|
||||
server.route({
|
||||
url: "/create-installation-session/organization",
|
||||
method: "POST",
|
||||
url: "/create-installation-session/organization",
|
||||
config: {
|
||||
rateLimit: writeLimit
|
||||
},
|
||||
schema: {
|
||||
body: z.object({ organizationId: z.string().trim() }),
|
||||
response: {
|
||||
@ -22,6 +26,7 @@ export const registerSecretScanningRouter = async (server: FastifyZodProvider) =
|
||||
const session = await server.services.secretScanning.createInstallationSession({
|
||||
actor: req.permission.type,
|
||||
actorId: req.permission.id,
|
||||
actorAuthMethod: req.permission.authMethod,
|
||||
actorOrgId: req.permission.orgId,
|
||||
orgId: req.body.organizationId
|
||||
});
|
||||
@ -30,8 +35,11 @@ export const registerSecretScanningRouter = async (server: FastifyZodProvider) =
|
||||
});
|
||||
|
||||
server.route({
|
||||
url: "/link-installation",
|
||||
method: "POST",
|
||||
url: "/link-installation",
|
||||
config: {
|
||||
rateLimit: writeLimit
|
||||
},
|
||||
schema: {
|
||||
body: z.object({
|
||||
installationId: z.string(),
|
||||
@ -46,6 +54,7 @@ export const registerSecretScanningRouter = async (server: FastifyZodProvider) =
|
||||
const { installatedApp } = await server.services.secretScanning.linkInstallationToOrg({
|
||||
actor: req.permission.type,
|
||||
actorId: req.permission.id,
|
||||
actorAuthMethod: req.permission.authMethod,
|
||||
actorOrgId: req.permission.orgId,
|
||||
...req.body
|
||||
});
|
||||
@ -54,8 +63,11 @@ export const registerSecretScanningRouter = async (server: FastifyZodProvider) =
|
||||
});
|
||||
|
||||
server.route({
|
||||
url: "/installation-status/organization/:organizationId",
|
||||
method: "GET",
|
||||
url: "/installation-status/organization/:organizationId",
|
||||
config: {
|
||||
rateLimit: readLimit
|
||||
},
|
||||
schema: {
|
||||
params: z.object({ organizationId: z.string().trim() }),
|
||||
response: {
|
||||
@ -67,6 +79,7 @@ export const registerSecretScanningRouter = async (server: FastifyZodProvider) =
|
||||
const appInstallationCompleted = await server.services.secretScanning.getOrgInstallationStatus({
|
||||
actor: req.permission.type,
|
||||
actorId: req.permission.id,
|
||||
actorAuthMethod: req.permission.authMethod,
|
||||
actorOrgId: req.permission.orgId,
|
||||
orgId: req.params.organizationId
|
||||
});
|
||||
@ -77,6 +90,9 @@ export const registerSecretScanningRouter = async (server: FastifyZodProvider) =
|
||||
server.route({
|
||||
url: "/organization/:organizationId/risks",
|
||||
method: "GET",
|
||||
config: {
|
||||
rateLimit: readLimit
|
||||
},
|
||||
schema: {
|
||||
params: z.object({ organizationId: z.string().trim() }),
|
||||
response: {
|
||||
@ -88,6 +104,7 @@ export const registerSecretScanningRouter = async (server: FastifyZodProvider) =
|
||||
const { risks } = await server.services.secretScanning.getRisksByOrg({
|
||||
actor: req.permission.type,
|
||||
actorId: req.permission.id,
|
||||
actorAuthMethod: req.permission.authMethod,
|
||||
actorOrgId: req.permission.orgId,
|
||||
orgId: req.params.organizationId
|
||||
});
|
||||
@ -96,8 +113,11 @@ export const registerSecretScanningRouter = async (server: FastifyZodProvider) =
|
||||
});
|
||||
|
||||
server.route({
|
||||
url: "/organization/:organizationId/risks/:riskId/status",
|
||||
method: "POST",
|
||||
url: "/organization/:organizationId/risks/:riskId/status",
|
||||
config: {
|
||||
rateLimit: writeLimit
|
||||
},
|
||||
schema: {
|
||||
params: z.object({ organizationId: z.string().trim(), riskId: z.string().trim() }),
|
||||
body: z.object({ status: z.nativeEnum(SecretScanningRiskStatus) }),
|
||||
@ -110,6 +130,7 @@ export const registerSecretScanningRouter = async (server: FastifyZodProvider) =
|
||||
const { risk } = await server.services.secretScanning.updateRiskStatus({
|
||||
actor: req.permission.type,
|
||||
actorId: req.permission.id,
|
||||
actorAuthMethod: req.permission.authMethod,
|
||||
actorOrgId: req.permission.orgId,
|
||||
orgId: req.params.organizationId,
|
||||
riskId: req.params.riskId,
|
||||
|
@ -1,13 +1,17 @@
|
||||
import { z } from "zod";
|
||||
|
||||
import { SecretVersionsSchema } from "@app/db/schemas";
|
||||
import { readLimit } from "@app/server/config/rateLimiter";
|
||||
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
|
||||
import { AuthMode } from "@app/services/auth/auth-type";
|
||||
|
||||
export const registerSecretVersionRouter = async (server: FastifyZodProvider) => {
|
||||
server.route({
|
||||
url: "/:secretId/secret-versions",
|
||||
method: "GET",
|
||||
url: "/:secretId/secret-versions",
|
||||
config: {
|
||||
rateLimit: readLimit
|
||||
},
|
||||
schema: {
|
||||
params: z.object({
|
||||
secretId: z.string()
|
||||
@ -27,6 +31,7 @@ export const registerSecretVersionRouter = async (server: FastifyZodProvider) =>
|
||||
const secretVersions = await server.services.secret.getSecretVersions({
|
||||
actor: req.permission.type,
|
||||
actorId: req.permission.id,
|
||||
actorAuthMethod: req.permission.authMethod,
|
||||
actorOrgId: req.permission.orgId,
|
||||
limit: req.query.limit,
|
||||
offset: req.query.offset,
|
||||
|
@ -2,6 +2,7 @@ import { z } from "zod";
|
||||
|
||||
import { SecretSnapshotsSchema, SecretTagsSchema, SecretVersionsSchema } from "@app/db/schemas";
|
||||
import { PROJECTS } from "@app/lib/api-docs";
|
||||
import { readLimit, writeLimit } from "@app/server/config/rateLimiter";
|
||||
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
|
||||
import { AuthMode } from "@app/services/auth/auth-type";
|
||||
|
||||
@ -9,6 +10,9 @@ export const registerSnapshotRouter = async (server: FastifyZodProvider) => {
|
||||
server.route({
|
||||
method: "GET",
|
||||
url: "/:secretSnapshotId",
|
||||
config: {
|
||||
rateLimit: readLimit
|
||||
},
|
||||
schema: {
|
||||
params: z.object({
|
||||
secretSnapshotId: z.string().trim()
|
||||
@ -47,6 +51,7 @@ export const registerSnapshotRouter = async (server: FastifyZodProvider) => {
|
||||
const secretSnapshot = await server.services.snapshot.getSnapshotData({
|
||||
actor: req.permission.type,
|
||||
actorId: req.permission.id,
|
||||
actorAuthMethod: req.permission.authMethod,
|
||||
actorOrgId: req.permission.orgId,
|
||||
id: req.params.secretSnapshotId
|
||||
});
|
||||
@ -57,6 +62,9 @@ export const registerSnapshotRouter = async (server: FastifyZodProvider) => {
|
||||
server.route({
|
||||
method: "POST",
|
||||
url: "/:secretSnapshotId/rollback",
|
||||
config: {
|
||||
rateLimit: writeLimit
|
||||
},
|
||||
schema: {
|
||||
description: "Roll back project secrets to those captured in a secret snapshot version.",
|
||||
security: [
|
||||
@ -79,6 +87,7 @@ export const registerSnapshotRouter = async (server: FastifyZodProvider) => {
|
||||
const secretSnapshot = await server.services.snapshot.rollbackSnapshot({
|
||||
actor: req.permission.type,
|
||||
actorId: req.permission.id,
|
||||
actorAuthMethod: req.permission.authMethod,
|
||||
actorOrgId: req.permission.orgId,
|
||||
id: req.params.secretSnapshotId
|
||||
});
|
||||
|
@ -2,13 +2,17 @@ import { z } from "zod";
|
||||
|
||||
import { TrustedIpsSchema } from "@app/db/schemas";
|
||||
import { EventType } from "@app/ee/services/audit-log/audit-log-types";
|
||||
import { readLimit, writeLimit } from "@app/server/config/rateLimiter";
|
||||
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
|
||||
import { AuthMode } from "@app/services/auth/auth-type";
|
||||
|
||||
export const registerTrustedIpRouter = async (server: FastifyZodProvider) => {
|
||||
server.route({
|
||||
url: "/:workspaceId/trusted-ips",
|
||||
method: "GET",
|
||||
url: "/:workspaceId/trusted-ips",
|
||||
config: {
|
||||
rateLimit: readLimit
|
||||
},
|
||||
schema: {
|
||||
params: z.object({
|
||||
workspaceId: z.string().trim()
|
||||
@ -22,6 +26,7 @@ export const registerTrustedIpRouter = async (server: FastifyZodProvider) => {
|
||||
onRequest: verifyAuth([AuthMode.JWT]),
|
||||
handler: async (req) => {
|
||||
const trustedIps = await server.services.trustedIp.listIpsByProjectId({
|
||||
actorAuthMethod: req.permission.authMethod,
|
||||
projectId: req.params.workspaceId,
|
||||
actor: req.permission.type,
|
||||
actorId: req.permission.id,
|
||||
@ -32,8 +37,11 @@ export const registerTrustedIpRouter = async (server: FastifyZodProvider) => {
|
||||
});
|
||||
|
||||
server.route({
|
||||
url: "/:workspaceId/trusted-ips",
|
||||
method: "POST",
|
||||
url: "/:workspaceId/trusted-ips",
|
||||
config: {
|
||||
rateLimit: writeLimit
|
||||
},
|
||||
schema: {
|
||||
params: z.object({
|
||||
workspaceId: z.string().trim()
|
||||
@ -52,6 +60,7 @@ export const registerTrustedIpRouter = async (server: FastifyZodProvider) => {
|
||||
onRequest: verifyAuth([AuthMode.JWT]),
|
||||
handler: async (req) => {
|
||||
const { trustedIp, project } = await server.services.trustedIp.addProjectIp({
|
||||
actorAuthMethod: req.permission.authMethod,
|
||||
projectId: req.params.workspaceId,
|
||||
actor: req.permission.type,
|
||||
actorId: req.permission.id,
|
||||
@ -76,8 +85,11 @@ export const registerTrustedIpRouter = async (server: FastifyZodProvider) => {
|
||||
});
|
||||
|
||||
server.route({
|
||||
url: "/:workspaceId/trusted-ips/:trustedIpId",
|
||||
method: "PATCH",
|
||||
url: "/:workspaceId/trusted-ips/:trustedIpId",
|
||||
config: {
|
||||
rateLimit: writeLimit
|
||||
},
|
||||
schema: {
|
||||
params: z.object({
|
||||
workspaceId: z.string().trim(),
|
||||
@ -99,6 +111,7 @@ export const registerTrustedIpRouter = async (server: FastifyZodProvider) => {
|
||||
projectId: req.params.workspaceId,
|
||||
actor: req.permission.type,
|
||||
actorId: req.permission.id,
|
||||
actorAuthMethod: req.permission.authMethod,
|
||||
actorOrgId: req.permission.orgId,
|
||||
trustedIpId: req.params.trustedIpId,
|
||||
...req.body
|
||||
@ -121,8 +134,11 @@ export const registerTrustedIpRouter = async (server: FastifyZodProvider) => {
|
||||
});
|
||||
|
||||
server.route({
|
||||
url: "/:workspaceId/trusted-ips/:trustedIpId",
|
||||
method: "DELETE",
|
||||
url: "/:workspaceId/trusted-ips/:trustedIpId",
|
||||
config: {
|
||||
rateLimit: writeLimit
|
||||
},
|
||||
schema: {
|
||||
params: z.object({
|
||||
workspaceId: z.string().trim(),
|
||||
@ -140,6 +156,7 @@ export const registerTrustedIpRouter = async (server: FastifyZodProvider) => {
|
||||
projectId: req.params.workspaceId,
|
||||
actor: req.permission.type,
|
||||
actorId: req.permission.id,
|
||||
actorAuthMethod: req.permission.authMethod,
|
||||
actorOrgId: req.permission.orgId,
|
||||
trustedIpId: req.params.trustedIpId
|
||||
});
|
||||
|
256
backend/src/ee/routes/v1/user-additional-privilege-router.ts
Normal file
256
backend/src/ee/routes/v1/user-additional-privilege-router.ts
Normal file
@ -0,0 +1,256 @@
|
||||
import slugify from "@sindresorhus/slugify";
|
||||
import ms from "ms";
|
||||
import { z } from "zod";
|
||||
|
||||
import { ProjectUserAdditionalPrivilegeSchema } from "@app/db/schemas";
|
||||
import { ProjectUserAdditionalPrivilegeTemporaryMode } from "@app/ee/services/project-user-additional-privilege/project-user-additional-privilege-types";
|
||||
import { PROJECT_USER_ADDITIONAL_PRIVILEGE } from "@app/lib/api-docs";
|
||||
import { alphaNumericNanoId } from "@app/lib/nanoid";
|
||||
import { readLimit, writeLimit } from "@app/server/config/rateLimiter";
|
||||
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
|
||||
import { AuthMode } from "@app/services/auth/auth-type";
|
||||
|
||||
export const registerUserAdditionalPrivilegeRouter = async (server: FastifyZodProvider) => {
|
||||
server.route({
|
||||
url: "/permanent",
|
||||
method: "POST",
|
||||
config: {
|
||||
rateLimit: writeLimit
|
||||
},
|
||||
schema: {
|
||||
body: z.object({
|
||||
projectMembershipId: z.string().min(1).describe(PROJECT_USER_ADDITIONAL_PRIVILEGE.CREATE.projectMembershipId),
|
||||
slug: z
|
||||
.string()
|
||||
.min(1)
|
||||
.max(60)
|
||||
.trim()
|
||||
.refine((v) => v.toLowerCase() === v, "Slug must be lowercase")
|
||||
.refine((v) => slugify(v) === v, {
|
||||
message: "Slug must be a valid slug"
|
||||
})
|
||||
.optional()
|
||||
.describe(PROJECT_USER_ADDITIONAL_PRIVILEGE.CREATE.slug),
|
||||
permissions: z.any().array().describe(PROJECT_USER_ADDITIONAL_PRIVILEGE.CREATE.permissions)
|
||||
}),
|
||||
response: {
|
||||
200: z.object({
|
||||
privilege: ProjectUserAdditionalPrivilegeSchema
|
||||
})
|
||||
}
|
||||
},
|
||||
onRequest: verifyAuth([AuthMode.JWT]),
|
||||
handler: async (req) => {
|
||||
const privilege = await server.services.projectUserAdditionalPrivilege.create({
|
||||
actorId: req.permission.id,
|
||||
actor: req.permission.type,
|
||||
actorOrgId: req.permission.orgId,
|
||||
actorAuthMethod: req.permission.authMethod,
|
||||
...req.body,
|
||||
slug: req.body.slug ? slugify(req.body.slug) : slugify(alphaNumericNanoId(12)),
|
||||
isTemporary: false,
|
||||
permissions: JSON.stringify(req.body.permissions)
|
||||
});
|
||||
return { privilege };
|
||||
}
|
||||
});
|
||||
|
||||
server.route({
|
||||
method: "POST",
|
||||
url: "/temporary",
|
||||
config: {
|
||||
rateLimit: writeLimit
|
||||
},
|
||||
schema: {
|
||||
body: z.object({
|
||||
projectMembershipId: z.string().min(1).describe(PROJECT_USER_ADDITIONAL_PRIVILEGE.CREATE.projectMembershipId),
|
||||
slug: z
|
||||
.string()
|
||||
.min(1)
|
||||
.max(60)
|
||||
.trim()
|
||||
.refine((v) => v.toLowerCase() === v, "Slug must be lowercase")
|
||||
.refine((v) => slugify(v) === v, {
|
||||
message: "Slug must be a valid slug"
|
||||
})
|
||||
.optional()
|
||||
.describe(PROJECT_USER_ADDITIONAL_PRIVILEGE.CREATE.slug),
|
||||
permissions: z.any().array().describe(PROJECT_USER_ADDITIONAL_PRIVILEGE.CREATE.permissions),
|
||||
temporaryMode: z
|
||||
.nativeEnum(ProjectUserAdditionalPrivilegeTemporaryMode)
|
||||
.describe(PROJECT_USER_ADDITIONAL_PRIVILEGE.CREATE.temporaryMode),
|
||||
temporaryRange: z
|
||||
.string()
|
||||
.refine((val) => ms(val) > 0, "Temporary range must be a positive number")
|
||||
.describe(PROJECT_USER_ADDITIONAL_PRIVILEGE.CREATE.temporaryRange),
|
||||
temporaryAccessStartTime: z
|
||||
.string()
|
||||
.datetime()
|
||||
.describe(PROJECT_USER_ADDITIONAL_PRIVILEGE.CREATE.temporaryAccessStartTime)
|
||||
}),
|
||||
response: {
|
||||
200: z.object({
|
||||
privilege: ProjectUserAdditionalPrivilegeSchema
|
||||
})
|
||||
}
|
||||
},
|
||||
onRequest: verifyAuth([AuthMode.JWT]),
|
||||
handler: async (req) => {
|
||||
const privilege = await server.services.projectUserAdditionalPrivilege.create({
|
||||
actorId: req.permission.id,
|
||||
actor: req.permission.type,
|
||||
actorOrgId: req.permission.orgId,
|
||||
actorAuthMethod: req.permission.authMethod,
|
||||
...req.body,
|
||||
slug: req.body.slug ? slugify(req.body.slug) : `privilege-${slugify(alphaNumericNanoId(12))}`,
|
||||
isTemporary: true,
|
||||
permissions: JSON.stringify(req.body.permissions)
|
||||
});
|
||||
return { privilege };
|
||||
}
|
||||
});
|
||||
|
||||
server.route({
|
||||
method: "PATCH",
|
||||
url: "/:privilegeId",
|
||||
config: {
|
||||
rateLimit: writeLimit
|
||||
},
|
||||
schema: {
|
||||
params: z.object({
|
||||
privilegeId: z.string().min(1).describe(PROJECT_USER_ADDITIONAL_PRIVILEGE.UPDATE.privilegeId)
|
||||
}),
|
||||
body: z
|
||||
.object({
|
||||
slug: z
|
||||
.string()
|
||||
.max(60)
|
||||
.trim()
|
||||
.refine((v) => v.toLowerCase() === v, "Slug must be lowercase")
|
||||
.refine((v) => slugify(v) === v, {
|
||||
message: "Slug must be a valid slug"
|
||||
})
|
||||
.describe(PROJECT_USER_ADDITIONAL_PRIVILEGE.UPDATE.slug),
|
||||
permissions: z.any().array().describe(PROJECT_USER_ADDITIONAL_PRIVILEGE.UPDATE.permissions),
|
||||
isTemporary: z.boolean().describe(PROJECT_USER_ADDITIONAL_PRIVILEGE.UPDATE.isTemporary),
|
||||
temporaryMode: z
|
||||
.nativeEnum(ProjectUserAdditionalPrivilegeTemporaryMode)
|
||||
.describe(PROJECT_USER_ADDITIONAL_PRIVILEGE.UPDATE.temporaryMode),
|
||||
temporaryRange: z
|
||||
.string()
|
||||
.refine((val) => ms(val) > 0, "Temporary range must be a positive number")
|
||||
.describe(PROJECT_USER_ADDITIONAL_PRIVILEGE.UPDATE.temporaryRange),
|
||||
temporaryAccessStartTime: z
|
||||
.string()
|
||||
.datetime()
|
||||
.describe(PROJECT_USER_ADDITIONAL_PRIVILEGE.UPDATE.temporaryAccessStartTime)
|
||||
})
|
||||
.partial(),
|
||||
response: {
|
||||
200: z.object({
|
||||
privilege: ProjectUserAdditionalPrivilegeSchema
|
||||
})
|
||||
}
|
||||
},
|
||||
onRequest: verifyAuth([AuthMode.JWT]),
|
||||
handler: async (req) => {
|
||||
const privilege = await server.services.projectUserAdditionalPrivilege.updateById({
|
||||
actorId: req.permission.id,
|
||||
actor: req.permission.type,
|
||||
actorOrgId: req.permission.orgId,
|
||||
actorAuthMethod: req.permission.authMethod,
|
||||
...req.body,
|
||||
permissions: req.body.permissions ? JSON.stringify(req.body.permissions) : undefined,
|
||||
privilegeId: req.params.privilegeId
|
||||
});
|
||||
return { privilege };
|
||||
}
|
||||
});
|
||||
|
||||
server.route({
|
||||
method: "DELETE",
|
||||
url: "/:privilegeId",
|
||||
config: {
|
||||
rateLimit: writeLimit
|
||||
},
|
||||
schema: {
|
||||
params: z.object({
|
||||
privilegeId: z.string().describe(PROJECT_USER_ADDITIONAL_PRIVILEGE.DELETE.privilegeId)
|
||||
}),
|
||||
response: {
|
||||
200: z.object({
|
||||
privilege: ProjectUserAdditionalPrivilegeSchema
|
||||
})
|
||||
}
|
||||
},
|
||||
onRequest: verifyAuth([AuthMode.JWT]),
|
||||
handler: async (req) => {
|
||||
const privilege = await server.services.projectUserAdditionalPrivilege.deleteById({
|
||||
actorId: req.permission.id,
|
||||
actor: req.permission.type,
|
||||
actorOrgId: req.permission.orgId,
|
||||
actorAuthMethod: req.permission.authMethod,
|
||||
privilegeId: req.params.privilegeId
|
||||
});
|
||||
return { privilege };
|
||||
}
|
||||
});
|
||||
|
||||
server.route({
|
||||
method: "GET",
|
||||
url: "/",
|
||||
config: {
|
||||
rateLimit: readLimit
|
||||
},
|
||||
schema: {
|
||||
querystring: z.object({
|
||||
projectMembershipId: z.string().describe(PROJECT_USER_ADDITIONAL_PRIVILEGE.LIST.projectMembershipId)
|
||||
}),
|
||||
response: {
|
||||
200: z.object({
|
||||
privileges: ProjectUserAdditionalPrivilegeSchema.array()
|
||||
})
|
||||
}
|
||||
},
|
||||
onRequest: verifyAuth([AuthMode.JWT]),
|
||||
handler: async (req) => {
|
||||
const privileges = await server.services.projectUserAdditionalPrivilege.listPrivileges({
|
||||
actorId: req.permission.id,
|
||||
actor: req.permission.type,
|
||||
actorOrgId: req.permission.orgId,
|
||||
actorAuthMethod: req.permission.authMethod,
|
||||
projectMembershipId: req.query.projectMembershipId
|
||||
});
|
||||
return { privileges };
|
||||
}
|
||||
});
|
||||
|
||||
server.route({
|
||||
method: "GET",
|
||||
url: "/:privilegeId",
|
||||
config: {
|
||||
rateLimit: readLimit
|
||||
},
|
||||
schema: {
|
||||
params: z.object({
|
||||
privilegeId: z.string().describe(PROJECT_USER_ADDITIONAL_PRIVILEGE.GET_BY_PRIVILEGEID.privilegeId)
|
||||
}),
|
||||
response: {
|
||||
200: z.object({
|
||||
privilege: ProjectUserAdditionalPrivilegeSchema
|
||||
})
|
||||
}
|
||||
},
|
||||
onRequest: verifyAuth([AuthMode.JWT]),
|
||||
handler: async (req) => {
|
||||
const privilege = await server.services.projectUserAdditionalPrivilege.getPrivilegeDetailsById({
|
||||
actorId: req.permission.id,
|
||||
actor: req.permission.type,
|
||||
actorOrgId: req.permission.orgId,
|
||||
actorAuthMethod: req.permission.authMethod,
|
||||
privilegeId: req.params.privilegeId
|
||||
});
|
||||
return { privilege };
|
||||
}
|
||||
});
|
||||
};
|
@ -31,10 +31,17 @@ export const auditLogServiceFactory = ({
|
||||
actor,
|
||||
actorId,
|
||||
actorOrgId,
|
||||
actorAuthMethod,
|
||||
projectId,
|
||||
auditLogActor
|
||||
}: TListProjectAuditLogDTO) => {
|
||||
const { permission } = await permissionService.getProjectPermission(actor, actorId, projectId, actorOrgId);
|
||||
const { permission } = await permissionService.getProjectPermission(
|
||||
actor,
|
||||
actorId,
|
||||
projectId,
|
||||
actorAuthMethod,
|
||||
actorOrgId
|
||||
);
|
||||
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.AuditLogs);
|
||||
const auditLogs = await auditLogDAL.find({
|
||||
startDate,
|
||||
|
@ -0,0 +1,80 @@
|
||||
import { Knex } from "knex";
|
||||
|
||||
import { TDbClient } from "@app/db";
|
||||
import { DynamicSecretLeasesSchema, TableName } from "@app/db/schemas";
|
||||
import { DatabaseError } from "@app/lib/errors";
|
||||
import { ormify, selectAllTableCols } from "@app/lib/knex";
|
||||
|
||||
export type TDynamicSecretLeaseDALFactory = ReturnType<typeof dynamicSecretLeaseDALFactory>;
|
||||
|
||||
export const dynamicSecretLeaseDALFactory = (db: TDbClient) => {
|
||||
const orm = ormify(db, TableName.DynamicSecretLease);
|
||||
|
||||
const countLeasesForDynamicSecret = async (dynamicSecretId: string, tx?: Knex) => {
|
||||
try {
|
||||
const doc = await (tx || db)(TableName.DynamicSecretLease).count("*").where({ dynamicSecretId }).first();
|
||||
return parseInt(doc || "0", 10);
|
||||
} catch (error) {
|
||||
throw new DatabaseError({ error, name: "DynamicSecretCountLeases" });
|
||||
}
|
||||
};
|
||||
|
||||
const findById = async (id: string, tx?: Knex) => {
|
||||
try {
|
||||
const doc = await (tx || db)(TableName.DynamicSecretLease)
|
||||
.where({ [`${TableName.DynamicSecretLease}.id` as "id"]: id })
|
||||
.first()
|
||||
.join(
|
||||
TableName.DynamicSecret,
|
||||
`${TableName.DynamicSecretLease}.dynamicSecretId`,
|
||||
`${TableName.DynamicSecret}.id`
|
||||
)
|
||||
.select(selectAllTableCols(TableName.DynamicSecretLease))
|
||||
.select(
|
||||
db.ref("id").withSchema(TableName.DynamicSecret).as("dynId"),
|
||||
db.ref("name").withSchema(TableName.DynamicSecret).as("dynName"),
|
||||
db.ref("version").withSchema(TableName.DynamicSecret).as("dynVersion"),
|
||||
db.ref("type").withSchema(TableName.DynamicSecret).as("dynType"),
|
||||
db.ref("defaultTTL").withSchema(TableName.DynamicSecret).as("dynDefaultTTL"),
|
||||
db.ref("maxTTL").withSchema(TableName.DynamicSecret).as("dynMaxTTL"),
|
||||
db.ref("inputIV").withSchema(TableName.DynamicSecret).as("dynInputIV"),
|
||||
db.ref("inputTag").withSchema(TableName.DynamicSecret).as("dynInputTag"),
|
||||
db.ref("inputCiphertext").withSchema(TableName.DynamicSecret).as("dynInputCiphertext"),
|
||||
db.ref("algorithm").withSchema(TableName.DynamicSecret).as("dynAlgorithm"),
|
||||
db.ref("keyEncoding").withSchema(TableName.DynamicSecret).as("dynKeyEncoding"),
|
||||
db.ref("folderId").withSchema(TableName.DynamicSecret).as("dynFolderId"),
|
||||
db.ref("status").withSchema(TableName.DynamicSecret).as("dynStatus"),
|
||||
db.ref("statusDetails").withSchema(TableName.DynamicSecret).as("dynStatusDetails"),
|
||||
db.ref("createdAt").withSchema(TableName.DynamicSecret).as("dynCreatedAt"),
|
||||
db.ref("updatedAt").withSchema(TableName.DynamicSecret).as("dynUpdatedAt")
|
||||
);
|
||||
if (!doc) return;
|
||||
|
||||
return {
|
||||
...DynamicSecretLeasesSchema.parse(doc),
|
||||
dynamicSecret: {
|
||||
id: doc.dynId,
|
||||
name: doc.dynName,
|
||||
version: doc.dynVersion,
|
||||
type: doc.dynType,
|
||||
defaultTTL: doc.dynDefaultTTL,
|
||||
maxTTL: doc.dynMaxTTL,
|
||||
inputIV: doc.dynInputIV,
|
||||
inputTag: doc.dynInputTag,
|
||||
inputCiphertext: doc.dynInputCiphertext,
|
||||
algorithm: doc.dynAlgorithm,
|
||||
keyEncoding: doc.dynKeyEncoding,
|
||||
folderId: doc.dynFolderId,
|
||||
status: doc.dynStatus,
|
||||
statusDetails: doc.dynStatusDetails,
|
||||
createdAt: doc.dynCreatedAt,
|
||||
updatedAt: doc.dynUpdatedAt
|
||||
}
|
||||
};
|
||||
} catch (error) {
|
||||
throw new DatabaseError({ error, name: "DynamicSecretLeaseFindById" });
|
||||
}
|
||||
};
|
||||
|
||||
return { ...orm, findById, countLeasesForDynamicSecret };
|
||||
};
|
@ -0,0 +1,159 @@
|
||||
import { SecretKeyEncoding } from "@app/db/schemas";
|
||||
import { DisableRotationErrors } from "@app/ee/services/secret-rotation/secret-rotation-queue";
|
||||
import { infisicalSymmetricDecrypt } from "@app/lib/crypto/encryption";
|
||||
import { logger } from "@app/lib/logger";
|
||||
import { QueueJobs, QueueName, TQueueServiceFactory } from "@app/queue";
|
||||
|
||||
import { TDynamicSecretDALFactory } from "../dynamic-secret/dynamic-secret-dal";
|
||||
import { DynamicSecretStatus } from "../dynamic-secret/dynamic-secret-types";
|
||||
import { DynamicSecretProviders, TDynamicProviderFns } from "../dynamic-secret/providers/models";
|
||||
import { TDynamicSecretLeaseDALFactory } from "./dynamic-secret-lease-dal";
|
||||
|
||||
type TDynamicSecretLeaseQueueServiceFactoryDep = {
|
||||
queueService: TQueueServiceFactory;
|
||||
dynamicSecretLeaseDAL: Pick<TDynamicSecretLeaseDALFactory, "findById" | "deleteById" | "find" | "updateById">;
|
||||
dynamicSecretDAL: Pick<TDynamicSecretDALFactory, "findById" | "deleteById" | "updateById">;
|
||||
dynamicSecretProviders: Record<DynamicSecretProviders, TDynamicProviderFns>;
|
||||
};
|
||||
|
||||
export type TDynamicSecretLeaseQueueServiceFactory = ReturnType<typeof dynamicSecretLeaseQueueServiceFactory>;
|
||||
|
||||
export const dynamicSecretLeaseQueueServiceFactory = ({
|
||||
queueService,
|
||||
dynamicSecretDAL,
|
||||
dynamicSecretProviders,
|
||||
dynamicSecretLeaseDAL
|
||||
}: TDynamicSecretLeaseQueueServiceFactoryDep) => {
|
||||
const pruneDynamicSecret = async (dynamicSecretCfgId: string) => {
|
||||
await queueService.queue(
|
||||
QueueName.DynamicSecretRevocation,
|
||||
QueueJobs.DynamicSecretPruning,
|
||||
{ dynamicSecretCfgId },
|
||||
{
|
||||
jobId: dynamicSecretCfgId,
|
||||
backoff: {
|
||||
type: "exponential",
|
||||
delay: 3000
|
||||
},
|
||||
removeOnFail: {
|
||||
count: 3
|
||||
},
|
||||
removeOnComplete: true
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
const setLeaseRevocation = async (leaseId: string, expiry: number) => {
|
||||
await queueService.queue(
|
||||
QueueName.DynamicSecretRevocation,
|
||||
QueueJobs.DynamicSecretRevocation,
|
||||
{ leaseId },
|
||||
{
|
||||
jobId: leaseId,
|
||||
backoff: {
|
||||
type: "exponential",
|
||||
delay: 3000
|
||||
},
|
||||
delay: expiry,
|
||||
removeOnFail: {
|
||||
count: 3
|
||||
},
|
||||
removeOnComplete: true
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
const unsetLeaseRevocation = async (leaseId: string) => {
|
||||
await queueService.stopJobById(QueueName.DynamicSecretRevocation, leaseId);
|
||||
};
|
||||
|
||||
queueService.start(QueueName.DynamicSecretRevocation, async (job) => {
|
||||
try {
|
||||
if (job.name === QueueJobs.DynamicSecretRevocation) {
|
||||
const { leaseId } = job.data as { leaseId: string };
|
||||
logger.info("Dynamic secret lease revocation started: ", leaseId, job.id);
|
||||
|
||||
const dynamicSecretLease = await dynamicSecretLeaseDAL.findById(leaseId);
|
||||
if (!dynamicSecretLease) throw new DisableRotationErrors({ message: "Dynamic secret lease not found" });
|
||||
|
||||
const dynamicSecretCfg = dynamicSecretLease.dynamicSecret;
|
||||
const selectedProvider = dynamicSecretProviders[dynamicSecretCfg.type as DynamicSecretProviders];
|
||||
const decryptedStoredInput = JSON.parse(
|
||||
infisicalSymmetricDecrypt({
|
||||
keyEncoding: dynamicSecretCfg.keyEncoding as SecretKeyEncoding,
|
||||
ciphertext: dynamicSecretCfg.inputCiphertext,
|
||||
tag: dynamicSecretCfg.inputTag,
|
||||
iv: dynamicSecretCfg.inputIV
|
||||
})
|
||||
) as object;
|
||||
|
||||
await selectedProvider.revoke(decryptedStoredInput, dynamicSecretLease.externalEntityId);
|
||||
await dynamicSecretLeaseDAL.deleteById(dynamicSecretLease.id);
|
||||
return;
|
||||
}
|
||||
|
||||
if (job.name === QueueJobs.DynamicSecretPruning) {
|
||||
const { dynamicSecretCfgId } = job.data as { dynamicSecretCfgId: string };
|
||||
logger.info("Dynamic secret pruning started: ", dynamicSecretCfgId, job.id);
|
||||
const dynamicSecretCfg = await dynamicSecretDAL.findById(dynamicSecretCfgId);
|
||||
if (!dynamicSecretCfg) throw new DisableRotationErrors({ message: "Dynamic secret not found" });
|
||||
if ((dynamicSecretCfg.status as DynamicSecretStatus) !== DynamicSecretStatus.Deleting)
|
||||
throw new DisableRotationErrors({ message: "Document not deleted" });
|
||||
|
||||
const dynamicSecretLeases = await dynamicSecretLeaseDAL.find({ dynamicSecretId: dynamicSecretCfgId });
|
||||
if (dynamicSecretLeases.length) {
|
||||
const selectedProvider = dynamicSecretProviders[dynamicSecretCfg.type as DynamicSecretProviders];
|
||||
const decryptedStoredInput = JSON.parse(
|
||||
infisicalSymmetricDecrypt({
|
||||
keyEncoding: dynamicSecretCfg.keyEncoding as SecretKeyEncoding,
|
||||
ciphertext: dynamicSecretCfg.inputCiphertext,
|
||||
tag: dynamicSecretCfg.inputTag,
|
||||
iv: dynamicSecretCfg.inputIV
|
||||
})
|
||||
) as object;
|
||||
|
||||
await Promise.all(dynamicSecretLeases.map(({ id }) => unsetLeaseRevocation(id)));
|
||||
await Promise.all(
|
||||
dynamicSecretLeases.map(({ externalEntityId }) =>
|
||||
selectedProvider.revoke(decryptedStoredInput, externalEntityId)
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
await dynamicSecretDAL.deleteById(dynamicSecretCfgId);
|
||||
}
|
||||
logger.info("Finished dynamic secret job", job.id);
|
||||
} catch (error) {
|
||||
logger.error(error);
|
||||
|
||||
if (job?.name === QueueJobs.DynamicSecretPruning) {
|
||||
const { dynamicSecretCfgId } = job.data as { dynamicSecretCfgId: string };
|
||||
await dynamicSecretDAL.updateById(dynamicSecretCfgId, {
|
||||
status: DynamicSecretStatus.FailedDeletion,
|
||||
statusDetails: (error as Error)?.message?.slice(0, 255)
|
||||
});
|
||||
}
|
||||
|
||||
if (job?.name === QueueJobs.DynamicSecretRevocation) {
|
||||
const { leaseId } = job.data as { leaseId: string };
|
||||
await dynamicSecretLeaseDAL.updateById(leaseId, {
|
||||
status: DynamicSecretStatus.FailedDeletion,
|
||||
statusDetails: (error as Error)?.message?.slice(0, 255)
|
||||
});
|
||||
}
|
||||
if (error instanceof DisableRotationErrors) {
|
||||
if (job.id) {
|
||||
await queueService.stopRepeatableJobByJobId(QueueName.DynamicSecretRevocation, job.id);
|
||||
}
|
||||
}
|
||||
// propogate to next part
|
||||
throw error;
|
||||
}
|
||||
});
|
||||
|
||||
return {
|
||||
pruneDynamicSecret,
|
||||
setLeaseRevocation,
|
||||
unsetLeaseRevocation
|
||||
};
|
||||
};
|
@ -0,0 +1,343 @@
|
||||
import { ForbiddenError, subject } from "@casl/ability";
|
||||
import ms from "ms";
|
||||
|
||||
import { SecretKeyEncoding } from "@app/db/schemas";
|
||||
import { TLicenseServiceFactory } from "@app/ee/services/license/license-service";
|
||||
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service";
|
||||
import { ProjectPermissionActions, ProjectPermissionSub } from "@app/ee/services/permission/project-permission";
|
||||
import { getConfig } from "@app/lib/config/env";
|
||||
import { infisicalSymmetricDecrypt } from "@app/lib/crypto/encryption";
|
||||
import { BadRequestError } from "@app/lib/errors";
|
||||
import { logger } from "@app/lib/logger";
|
||||
import { TProjectDALFactory } from "@app/services/project/project-dal";
|
||||
import { TSecretFolderDALFactory } from "@app/services/secret-folder/secret-folder-dal";
|
||||
|
||||
import { TDynamicSecretDALFactory } from "../dynamic-secret/dynamic-secret-dal";
|
||||
import { DynamicSecretProviders, TDynamicProviderFns } from "../dynamic-secret/providers/models";
|
||||
import { TDynamicSecretLeaseDALFactory } from "./dynamic-secret-lease-dal";
|
||||
import { TDynamicSecretLeaseQueueServiceFactory } from "./dynamic-secret-lease-queue";
|
||||
import {
|
||||
DynamicSecretLeaseStatus,
|
||||
TCreateDynamicSecretLeaseDTO,
|
||||
TDeleteDynamicSecretLeaseDTO,
|
||||
TDetailsDynamicSecretLeaseDTO,
|
||||
TListDynamicSecretLeasesDTO,
|
||||
TRenewDynamicSecretLeaseDTO
|
||||
} from "./dynamic-secret-lease-types";
|
||||
|
||||
type TDynamicSecretLeaseServiceFactoryDep = {
|
||||
dynamicSecretLeaseDAL: TDynamicSecretLeaseDALFactory;
|
||||
dynamicSecretDAL: Pick<TDynamicSecretDALFactory, "findOne">;
|
||||
dynamicSecretProviders: Record<DynamicSecretProviders, TDynamicProviderFns>;
|
||||
dynamicSecretQueueService: TDynamicSecretLeaseQueueServiceFactory;
|
||||
licenseService: Pick<TLicenseServiceFactory, "getPlan">;
|
||||
folderDAL: Pick<TSecretFolderDALFactory, "findBySecretPath">;
|
||||
permissionService: Pick<TPermissionServiceFactory, "getProjectPermission">;
|
||||
projectDAL: Pick<TProjectDALFactory, "findProjectBySlug">;
|
||||
};
|
||||
|
||||
export type TDynamicSecretLeaseServiceFactory = ReturnType<typeof dynamicSecretLeaseServiceFactory>;
|
||||
|
||||
export const dynamicSecretLeaseServiceFactory = ({
|
||||
dynamicSecretLeaseDAL,
|
||||
dynamicSecretProviders,
|
||||
dynamicSecretDAL,
|
||||
folderDAL,
|
||||
permissionService,
|
||||
dynamicSecretQueueService,
|
||||
projectDAL,
|
||||
licenseService
|
||||
}: TDynamicSecretLeaseServiceFactoryDep) => {
|
||||
const create = async ({
|
||||
environmentSlug,
|
||||
path,
|
||||
name,
|
||||
projectSlug,
|
||||
actor,
|
||||
actorId,
|
||||
actorOrgId,
|
||||
actorAuthMethod,
|
||||
ttl
|
||||
}: TCreateDynamicSecretLeaseDTO) => {
|
||||
const appCfg = getConfig();
|
||||
const project = await projectDAL.findProjectBySlug(projectSlug, actorOrgId);
|
||||
if (!project) throw new BadRequestError({ message: "Project not found" });
|
||||
|
||||
const 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 plan = await licenseService.getPlan(actorOrgId);
|
||||
if (!plan?.dynamicSecret) {
|
||||
throw new BadRequestError({
|
||||
message: "Failed to create lease due to plan restriction. Upgrade plan to create dynamic secret."
|
||||
});
|
||||
}
|
||||
|
||||
const folder = await folderDAL.findBySecretPath(projectId, environmentSlug, path);
|
||||
if (!folder) throw new BadRequestError({ message: "Folder not found" });
|
||||
|
||||
const dynamicSecretCfg = await dynamicSecretDAL.findOne({ name, folderId: folder.id });
|
||||
if (!dynamicSecretCfg) throw new BadRequestError({ message: "Dynamic secret not found" });
|
||||
|
||||
const totalLeasesTaken = await dynamicSecretLeaseDAL.countLeasesForDynamicSecret(dynamicSecretCfg.id);
|
||||
if (totalLeasesTaken >= appCfg.MAX_LEASE_LIMIT)
|
||||
throw new BadRequestError({ message: `Max lease limit reached. Limit: ${appCfg.MAX_LEASE_LIMIT}` });
|
||||
|
||||
const selectedProvider = dynamicSecretProviders[dynamicSecretCfg.type as DynamicSecretProviders];
|
||||
const decryptedStoredInput = JSON.parse(
|
||||
infisicalSymmetricDecrypt({
|
||||
keyEncoding: dynamicSecretCfg.keyEncoding as SecretKeyEncoding,
|
||||
ciphertext: dynamicSecretCfg.inputCiphertext,
|
||||
tag: dynamicSecretCfg.inputTag,
|
||||
iv: dynamicSecretCfg.inputIV
|
||||
})
|
||||
) as object;
|
||||
|
||||
const selectedTTL = ttl ?? dynamicSecretCfg.defaultTTL;
|
||||
const { maxTTL } = dynamicSecretCfg;
|
||||
const expireAt = new Date(new Date().getTime() + ms(selectedTTL));
|
||||
if (maxTTL) {
|
||||
const maxExpiryDate = new Date(new Date().getTime() + ms(maxTTL));
|
||||
if (expireAt > maxExpiryDate) throw new BadRequestError({ message: "TTL cannot be larger than max TTL" });
|
||||
}
|
||||
|
||||
const { entityId, data } = await selectedProvider.create(decryptedStoredInput, expireAt.getTime());
|
||||
const dynamicSecretLease = await dynamicSecretLeaseDAL.create({
|
||||
expireAt,
|
||||
version: 1,
|
||||
dynamicSecretId: dynamicSecretCfg.id,
|
||||
externalEntityId: entityId
|
||||
});
|
||||
await dynamicSecretQueueService.setLeaseRevocation(dynamicSecretLease.id, Number(expireAt) - Number(new Date()));
|
||||
return { lease: dynamicSecretLease, dynamicSecret: dynamicSecretCfg, data };
|
||||
};
|
||||
|
||||
const renewLease = async ({
|
||||
ttl,
|
||||
actorAuthMethod,
|
||||
actorOrgId,
|
||||
actorId,
|
||||
actor,
|
||||
projectSlug,
|
||||
path,
|
||||
environmentSlug,
|
||||
leaseId
|
||||
}: TRenewDynamicSecretLeaseDTO) => {
|
||||
const project = await projectDAL.findProjectBySlug(projectSlug, actorOrgId);
|
||||
if (!project) throw new BadRequestError({ message: "Project not found" });
|
||||
|
||||
const projectId = project.id;
|
||||
const { permission } = await permissionService.getProjectPermission(
|
||||
actor,
|
||||
actorId,
|
||||
projectId,
|
||||
actorAuthMethod,
|
||||
actorOrgId
|
||||
);
|
||||
ForbiddenError.from(permission).throwUnlessCan(
|
||||
ProjectPermissionActions.Edit,
|
||||
subject(ProjectPermissionSub.Secrets, { environment: environmentSlug, secretPath: path })
|
||||
);
|
||||
|
||||
const plan = await licenseService.getPlan(actorOrgId);
|
||||
if (!plan?.dynamicSecret) {
|
||||
throw new BadRequestError({
|
||||
message: "Failed to renew lease due to plan restriction. Upgrade plan to create dynamic secret."
|
||||
});
|
||||
}
|
||||
|
||||
const folder = await folderDAL.findBySecretPath(projectId, environmentSlug, path);
|
||||
if (!folder) throw new BadRequestError({ message: "Folder not found" });
|
||||
|
||||
const dynamicSecretLease = await dynamicSecretLeaseDAL.findById(leaseId);
|
||||
if (!dynamicSecretLease) throw new BadRequestError({ message: "Dynamic secret lease not found" });
|
||||
|
||||
const dynamicSecretCfg = dynamicSecretLease.dynamicSecret;
|
||||
const selectedProvider = dynamicSecretProviders[dynamicSecretCfg.type as DynamicSecretProviders];
|
||||
const decryptedStoredInput = JSON.parse(
|
||||
infisicalSymmetricDecrypt({
|
||||
keyEncoding: dynamicSecretCfg.keyEncoding as SecretKeyEncoding,
|
||||
ciphertext: dynamicSecretCfg.inputCiphertext,
|
||||
tag: dynamicSecretCfg.inputTag,
|
||||
iv: dynamicSecretCfg.inputIV
|
||||
})
|
||||
) as object;
|
||||
|
||||
const selectedTTL = ttl ?? dynamicSecretCfg.defaultTTL;
|
||||
const { maxTTL } = dynamicSecretCfg;
|
||||
const expireAt = new Date(dynamicSecretLease.expireAt.getTime() + ms(selectedTTL));
|
||||
if (maxTTL) {
|
||||
const maxExpiryDate = new Date(dynamicSecretLease.createdAt.getTime() + ms(maxTTL));
|
||||
if (expireAt > maxExpiryDate) throw new BadRequestError({ message: "TTL cannot be larger than max ttl" });
|
||||
}
|
||||
|
||||
const { entityId } = await selectedProvider.renew(
|
||||
decryptedStoredInput,
|
||||
dynamicSecretLease.externalEntityId,
|
||||
expireAt.getTime()
|
||||
);
|
||||
|
||||
await dynamicSecretQueueService.unsetLeaseRevocation(dynamicSecretLease.id);
|
||||
await dynamicSecretQueueService.setLeaseRevocation(dynamicSecretLease.id, Number(expireAt) - Number(new Date()));
|
||||
const updatedDynamicSecretLease = await dynamicSecretLeaseDAL.updateById(dynamicSecretLease.id, {
|
||||
expireAt,
|
||||
externalEntityId: entityId
|
||||
});
|
||||
return updatedDynamicSecretLease;
|
||||
};
|
||||
|
||||
const revokeLease = async ({
|
||||
leaseId,
|
||||
environmentSlug,
|
||||
path,
|
||||
projectSlug,
|
||||
actor,
|
||||
actorId,
|
||||
actorOrgId,
|
||||
actorAuthMethod,
|
||||
isForced
|
||||
}: TDeleteDynamicSecretLeaseDTO) => {
|
||||
const project = await projectDAL.findProjectBySlug(projectSlug, actorOrgId);
|
||||
if (!project) throw new BadRequestError({ message: "Project not found" });
|
||||
|
||||
const projectId = project.id;
|
||||
const { permission } = await permissionService.getProjectPermission(
|
||||
actor,
|
||||
actorId,
|
||||
projectId,
|
||||
actorAuthMethod,
|
||||
actorOrgId
|
||||
);
|
||||
ForbiddenError.from(permission).throwUnlessCan(
|
||||
ProjectPermissionActions.Delete,
|
||||
subject(ProjectPermissionSub.Secrets, { environment: environmentSlug, secretPath: path })
|
||||
);
|
||||
|
||||
const folder = await folderDAL.findBySecretPath(projectId, environmentSlug, path);
|
||||
if (!folder) throw new BadRequestError({ message: "Folder not found" });
|
||||
|
||||
const dynamicSecretLease = await dynamicSecretLeaseDAL.findById(leaseId);
|
||||
if (!dynamicSecretLease) throw new BadRequestError({ message: "Dynamic secret lease not found" });
|
||||
|
||||
const dynamicSecretCfg = dynamicSecretLease.dynamicSecret;
|
||||
const selectedProvider = dynamicSecretProviders[dynamicSecretCfg.type as DynamicSecretProviders];
|
||||
const decryptedStoredInput = JSON.parse(
|
||||
infisicalSymmetricDecrypt({
|
||||
keyEncoding: dynamicSecretCfg.keyEncoding as SecretKeyEncoding,
|
||||
ciphertext: dynamicSecretCfg.inputCiphertext,
|
||||
tag: dynamicSecretCfg.inputTag,
|
||||
iv: dynamicSecretCfg.inputIV
|
||||
})
|
||||
) as object;
|
||||
|
||||
const revokeResponse = await selectedProvider
|
||||
.revoke(decryptedStoredInput, dynamicSecretLease.externalEntityId)
|
||||
.catch(async (err) => {
|
||||
// only propogate this error if forced is false
|
||||
if (!isForced) return { error: err as Error };
|
||||
});
|
||||
|
||||
if ((revokeResponse as { error?: Error })?.error) {
|
||||
const { error } = revokeResponse as { error?: Error };
|
||||
logger.error(error?.message, "Failed to revoke lease");
|
||||
const deletedDynamicSecretLease = await dynamicSecretLeaseDAL.updateById(dynamicSecretLease.id, {
|
||||
status: DynamicSecretLeaseStatus.FailedDeletion,
|
||||
statusDetails: error?.message?.slice(0, 255)
|
||||
});
|
||||
return deletedDynamicSecretLease;
|
||||
}
|
||||
|
||||
await dynamicSecretQueueService.unsetLeaseRevocation(dynamicSecretLease.id);
|
||||
const deletedDynamicSecretLease = await dynamicSecretLeaseDAL.deleteById(dynamicSecretLease.id);
|
||||
return deletedDynamicSecretLease;
|
||||
};
|
||||
|
||||
const listLeases = async ({
|
||||
path,
|
||||
name,
|
||||
actor,
|
||||
actorId,
|
||||
projectSlug,
|
||||
actorOrgId,
|
||||
environmentSlug,
|
||||
actorAuthMethod
|
||||
}: TListDynamicSecretLeasesDTO) => {
|
||||
const project = await projectDAL.findProjectBySlug(projectSlug, actorOrgId);
|
||||
if (!project) throw new BadRequestError({ message: "Project not found" });
|
||||
|
||||
const 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 BadRequestError({ message: "Folder not found" });
|
||||
|
||||
const dynamicSecretCfg = await dynamicSecretDAL.findOne({ name, folderId: folder.id });
|
||||
if (!dynamicSecretCfg) throw new BadRequestError({ message: "Dynamic secret not found" });
|
||||
|
||||
const dynamicSecretLeases = await dynamicSecretLeaseDAL.find({ dynamicSecretId: dynamicSecretCfg.id });
|
||||
return dynamicSecretLeases;
|
||||
};
|
||||
|
||||
const getLeaseDetails = async ({
|
||||
projectSlug,
|
||||
actorOrgId,
|
||||
path,
|
||||
environmentSlug,
|
||||
actor,
|
||||
actorId,
|
||||
leaseId,
|
||||
actorAuthMethod
|
||||
}: TDetailsDynamicSecretLeaseDTO) => {
|
||||
const project = await projectDAL.findProjectBySlug(projectSlug, actorOrgId);
|
||||
if (!project) throw new BadRequestError({ message: "Project not found" });
|
||||
|
||||
const 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 BadRequestError({ message: "Folder not found" });
|
||||
|
||||
const dynamicSecretLease = await dynamicSecretLeaseDAL.findById(leaseId);
|
||||
if (!dynamicSecretLease) throw new BadRequestError({ message: "Dynamic secret lease not found" });
|
||||
|
||||
return dynamicSecretLease;
|
||||
};
|
||||
|
||||
return {
|
||||
create,
|
||||
listLeases,
|
||||
revokeLease,
|
||||
renewLease,
|
||||
getLeaseDetails
|
||||
};
|
||||
};
|
@ -0,0 +1,43 @@
|
||||
import { TProjectPermission } from "@app/lib/types";
|
||||
|
||||
export enum DynamicSecretLeaseStatus {
|
||||
FailedDeletion = "Failed to delete"
|
||||
}
|
||||
|
||||
export type TCreateDynamicSecretLeaseDTO = {
|
||||
name: string;
|
||||
path: string;
|
||||
environmentSlug: string;
|
||||
ttl?: string;
|
||||
projectSlug: string;
|
||||
} & Omit<TProjectPermission, "projectId">;
|
||||
|
||||
export type TDetailsDynamicSecretLeaseDTO = {
|
||||
leaseId: string;
|
||||
path: string;
|
||||
environmentSlug: string;
|
||||
projectSlug: string;
|
||||
} & Omit<TProjectPermission, "projectId">;
|
||||
|
||||
export type TListDynamicSecretLeasesDTO = {
|
||||
name: string;
|
||||
path: string;
|
||||
environmentSlug: string;
|
||||
projectSlug: string;
|
||||
} & Omit<TProjectPermission, "projectId">;
|
||||
|
||||
export type TDeleteDynamicSecretLeaseDTO = {
|
||||
leaseId: string;
|
||||
path: string;
|
||||
environmentSlug: string;
|
||||
projectSlug: string;
|
||||
isForced?: boolean;
|
||||
} & Omit<TProjectPermission, "projectId">;
|
||||
|
||||
export type TRenewDynamicSecretLeaseDTO = {
|
||||
leaseId: string;
|
||||
path: string;
|
||||
environmentSlug: string;
|
||||
ttl?: string;
|
||||
projectSlug: string;
|
||||
} & Omit<TProjectPermission, "projectId">;
|
10
backend/src/ee/services/dynamic-secret/dynamic-secret-dal.ts
Normal file
10
backend/src/ee/services/dynamic-secret/dynamic-secret-dal.ts
Normal file
@ -0,0 +1,10 @@
|
||||
import { TDbClient } from "@app/db";
|
||||
import { TableName } from "@app/db/schemas";
|
||||
import { ormify } from "@app/lib/knex";
|
||||
|
||||
export type TDynamicSecretDALFactory = ReturnType<typeof dynamicSecretDALFactory>;
|
||||
|
||||
export const dynamicSecretDALFactory = (db: TDbClient) => {
|
||||
const orm = ormify(db, TableName.DynamicSecret);
|
||||
return orm;
|
||||
};
|
341
backend/src/ee/services/dynamic-secret/dynamic-secret-service.ts
Normal file
341
backend/src/ee/services/dynamic-secret/dynamic-secret-service.ts
Normal file
@ -0,0 +1,341 @@
|
||||
import { ForbiddenError, subject } from "@casl/ability";
|
||||
|
||||
import { SecretKeyEncoding } from "@app/db/schemas";
|
||||
import { TLicenseServiceFactory } from "@app/ee/services/license/license-service";
|
||||
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service";
|
||||
import { ProjectPermissionActions, ProjectPermissionSub } from "@app/ee/services/permission/project-permission";
|
||||
import { infisicalSymmetricDecrypt, infisicalSymmetricEncypt } from "@app/lib/crypto/encryption";
|
||||
import { BadRequestError } from "@app/lib/errors";
|
||||
import { TProjectDALFactory } from "@app/services/project/project-dal";
|
||||
import { TSecretFolderDALFactory } from "@app/services/secret-folder/secret-folder-dal";
|
||||
|
||||
import { TDynamicSecretLeaseDALFactory } from "../dynamic-secret-lease/dynamic-secret-lease-dal";
|
||||
import { TDynamicSecretLeaseQueueServiceFactory } from "../dynamic-secret-lease/dynamic-secret-lease-queue";
|
||||
import { TDynamicSecretDALFactory } from "./dynamic-secret-dal";
|
||||
import {
|
||||
DynamicSecretStatus,
|
||||
TCreateDynamicSecretDTO,
|
||||
TDeleteDynamicSecretDTO,
|
||||
TDetailsDynamicSecretDTO,
|
||||
TListDynamicSecretsDTO,
|
||||
TUpdateDynamicSecretDTO
|
||||
} from "./dynamic-secret-types";
|
||||
import { DynamicSecretProviders, TDynamicProviderFns } from "./providers/models";
|
||||
|
||||
type TDynamicSecretServiceFactoryDep = {
|
||||
dynamicSecretDAL: TDynamicSecretDALFactory;
|
||||
dynamicSecretLeaseDAL: Pick<TDynamicSecretLeaseDALFactory, "find">;
|
||||
dynamicSecretProviders: Record<DynamicSecretProviders, TDynamicProviderFns>;
|
||||
dynamicSecretQueueService: Pick<
|
||||
TDynamicSecretLeaseQueueServiceFactory,
|
||||
"pruneDynamicSecret" | "unsetLeaseRevocation"
|
||||
>;
|
||||
licenseService: Pick<TLicenseServiceFactory, "getPlan">;
|
||||
folderDAL: Pick<TSecretFolderDALFactory, "findBySecretPath">;
|
||||
projectDAL: Pick<TProjectDALFactory, "findProjectBySlug">;
|
||||
permissionService: Pick<TPermissionServiceFactory, "getProjectPermission">;
|
||||
};
|
||||
|
||||
export type TDynamicSecretServiceFactory = ReturnType<typeof dynamicSecretServiceFactory>;
|
||||
|
||||
export const dynamicSecretServiceFactory = ({
|
||||
dynamicSecretDAL,
|
||||
dynamicSecretLeaseDAL,
|
||||
licenseService,
|
||||
folderDAL,
|
||||
dynamicSecretProviders,
|
||||
permissionService,
|
||||
dynamicSecretQueueService,
|
||||
projectDAL
|
||||
}: TDynamicSecretServiceFactoryDep) => {
|
||||
const create = async ({
|
||||
path,
|
||||
actor,
|
||||
name,
|
||||
actorId,
|
||||
maxTTL,
|
||||
provider,
|
||||
environmentSlug,
|
||||
projectSlug,
|
||||
actorOrgId,
|
||||
defaultTTL,
|
||||
actorAuthMethod
|
||||
}: TCreateDynamicSecretDTO) => {
|
||||
const project = await projectDAL.findProjectBySlug(projectSlug, actorOrgId);
|
||||
if (!project) throw new BadRequestError({ message: "Project not found" });
|
||||
|
||||
const projectId = project.id;
|
||||
const { permission } = await permissionService.getProjectPermission(
|
||||
actor,
|
||||
actorId,
|
||||
projectId,
|
||||
actorAuthMethod,
|
||||
actorOrgId
|
||||
);
|
||||
ForbiddenError.from(permission).throwUnlessCan(
|
||||
ProjectPermissionActions.Create,
|
||||
subject(ProjectPermissionSub.Secrets, { environment: environmentSlug, secretPath: path })
|
||||
);
|
||||
|
||||
const plan = await licenseService.getPlan(actorOrgId);
|
||||
if (!plan?.dynamicSecret) {
|
||||
throw new BadRequestError({
|
||||
message: "Failed to create dynamic secret due to plan restriction. Upgrade plan to create dynamic secret."
|
||||
});
|
||||
}
|
||||
|
||||
const folder = await folderDAL.findBySecretPath(projectId, environmentSlug, path);
|
||||
if (!folder) throw new BadRequestError({ message: "Folder not found" });
|
||||
|
||||
const existingDynamicSecret = await dynamicSecretDAL.findOne({ name, folderId: folder.id });
|
||||
if (existingDynamicSecret)
|
||||
throw new BadRequestError({ message: "Provided dynamic secret already exist under the folder" });
|
||||
|
||||
const selectedProvider = dynamicSecretProviders[provider.type];
|
||||
const inputs = await selectedProvider.validateProviderInputs(provider.inputs);
|
||||
|
||||
const isConnected = await selectedProvider.validateConnection(provider.inputs);
|
||||
if (!isConnected) throw new BadRequestError({ message: "Provider connection failed" });
|
||||
|
||||
const encryptedInput = infisicalSymmetricEncypt(JSON.stringify(inputs));
|
||||
const dynamicSecretCfg = await dynamicSecretDAL.create({
|
||||
type: provider.type,
|
||||
version: 1,
|
||||
inputIV: encryptedInput.iv,
|
||||
inputTag: encryptedInput.tag,
|
||||
inputCiphertext: encryptedInput.ciphertext,
|
||||
algorithm: encryptedInput.algorithm,
|
||||
keyEncoding: encryptedInput.encoding,
|
||||
maxTTL,
|
||||
defaultTTL,
|
||||
folderId: folder.id,
|
||||
name
|
||||
});
|
||||
return dynamicSecretCfg;
|
||||
};
|
||||
|
||||
const updateByName = async ({
|
||||
name,
|
||||
maxTTL,
|
||||
defaultTTL,
|
||||
inputs,
|
||||
environmentSlug,
|
||||
projectSlug,
|
||||
path,
|
||||
actor,
|
||||
actorId,
|
||||
newName,
|
||||
actorOrgId,
|
||||
actorAuthMethod
|
||||
}: TUpdateDynamicSecretDTO) => {
|
||||
const project = await projectDAL.findProjectBySlug(projectSlug, actorOrgId);
|
||||
if (!project) throw new BadRequestError({ message: "Project not found" });
|
||||
|
||||
const projectId = project.id;
|
||||
|
||||
const { permission } = await permissionService.getProjectPermission(
|
||||
actor,
|
||||
actorId,
|
||||
projectId,
|
||||
actorAuthMethod,
|
||||
actorOrgId
|
||||
);
|
||||
ForbiddenError.from(permission).throwUnlessCan(
|
||||
ProjectPermissionActions.Edit,
|
||||
subject(ProjectPermissionSub.Secrets, { environment: environmentSlug, secretPath: path })
|
||||
);
|
||||
|
||||
const plan = await licenseService.getPlan(actorOrgId);
|
||||
if (!plan?.dynamicSecret) {
|
||||
throw new BadRequestError({
|
||||
message: "Failed to update dynamic secret due to plan restriction. Upgrade plan to create dynamic secret."
|
||||
});
|
||||
}
|
||||
|
||||
const folder = await folderDAL.findBySecretPath(projectId, environmentSlug, path);
|
||||
if (!folder) throw new BadRequestError({ message: "Folder not found" });
|
||||
|
||||
const dynamicSecretCfg = await dynamicSecretDAL.findOne({ name, folderId: folder.id });
|
||||
if (!dynamicSecretCfg) throw new BadRequestError({ message: "Dynamic secret not found" });
|
||||
|
||||
if (newName) {
|
||||
const existingDynamicSecret = await dynamicSecretDAL.findOne({ name: newName, folderId: folder.id });
|
||||
if (existingDynamicSecret)
|
||||
throw new BadRequestError({ message: "Provided dynamic secret already exist under the folder" });
|
||||
}
|
||||
|
||||
const selectedProvider = dynamicSecretProviders[dynamicSecretCfg.type as DynamicSecretProviders];
|
||||
const decryptedStoredInput = JSON.parse(
|
||||
infisicalSymmetricDecrypt({
|
||||
keyEncoding: dynamicSecretCfg.keyEncoding as SecretKeyEncoding,
|
||||
ciphertext: dynamicSecretCfg.inputCiphertext,
|
||||
tag: dynamicSecretCfg.inputTag,
|
||||
iv: dynamicSecretCfg.inputIV
|
||||
})
|
||||
) as object;
|
||||
const newInput = { ...decryptedStoredInput, ...(inputs || {}) };
|
||||
const updatedInput = await selectedProvider.validateProviderInputs(newInput);
|
||||
|
||||
const isConnected = await selectedProvider.validateConnection(newInput);
|
||||
if (!isConnected) throw new BadRequestError({ message: "Provider connection failed" });
|
||||
|
||||
const encryptedInput = infisicalSymmetricEncypt(JSON.stringify(updatedInput));
|
||||
const updatedDynamicCfg = await dynamicSecretDAL.updateById(dynamicSecretCfg.id, {
|
||||
inputIV: encryptedInput.iv,
|
||||
inputTag: encryptedInput.tag,
|
||||
inputCiphertext: encryptedInput.ciphertext,
|
||||
algorithm: encryptedInput.algorithm,
|
||||
keyEncoding: encryptedInput.encoding,
|
||||
maxTTL,
|
||||
defaultTTL,
|
||||
name: newName ?? name,
|
||||
status: null,
|
||||
statusDetails: null
|
||||
});
|
||||
|
||||
return updatedDynamicCfg;
|
||||
};
|
||||
|
||||
const deleteByName = async ({
|
||||
actorAuthMethod,
|
||||
actorOrgId,
|
||||
actorId,
|
||||
actor,
|
||||
projectSlug,
|
||||
name,
|
||||
path,
|
||||
environmentSlug,
|
||||
isForced
|
||||
}: TDeleteDynamicSecretDTO) => {
|
||||
const project = await projectDAL.findProjectBySlug(projectSlug, actorOrgId);
|
||||
if (!project) throw new BadRequestError({ message: "Project not found" });
|
||||
|
||||
const projectId = project.id;
|
||||
|
||||
const { permission } = await permissionService.getProjectPermission(
|
||||
actor,
|
||||
actorId,
|
||||
projectId,
|
||||
actorAuthMethod,
|
||||
actorOrgId
|
||||
);
|
||||
ForbiddenError.from(permission).throwUnlessCan(
|
||||
ProjectPermissionActions.Edit,
|
||||
subject(ProjectPermissionSub.Secrets, { environment: environmentSlug, secretPath: path })
|
||||
);
|
||||
|
||||
const folder = await folderDAL.findBySecretPath(projectId, environmentSlug, path);
|
||||
if (!folder) throw new BadRequestError({ message: "Folder not found" });
|
||||
|
||||
const dynamicSecretCfg = await dynamicSecretDAL.findOne({ name, folderId: folder.id });
|
||||
if (!dynamicSecretCfg) throw new BadRequestError({ message: "Dynamic secret not found" });
|
||||
|
||||
const leases = await dynamicSecretLeaseDAL.find({ dynamicSecretId: dynamicSecretCfg.id });
|
||||
// when not forced we check with the external system to first remove the things
|
||||
// we introduce a forced concept because consider the external lease got deleted by some other external like a human or another system
|
||||
// this allows user to clean up it from infisical
|
||||
if (isForced) {
|
||||
// clear all queues for lease revocations
|
||||
await Promise.all(leases.map(({ id: leaseId }) => dynamicSecretQueueService.unsetLeaseRevocation(leaseId)));
|
||||
|
||||
const deletedDynamicSecretCfg = await dynamicSecretDAL.deleteById(dynamicSecretCfg.id);
|
||||
return deletedDynamicSecretCfg;
|
||||
}
|
||||
// if leases exist we should flag it as deleting and then remove leases in background
|
||||
// then delete the main one
|
||||
if (leases.length) {
|
||||
const updatedDynamicSecretCfg = await dynamicSecretDAL.updateById(dynamicSecretCfg.id, {
|
||||
status: DynamicSecretStatus.Deleting
|
||||
});
|
||||
await dynamicSecretQueueService.pruneDynamicSecret(updatedDynamicSecretCfg.id);
|
||||
return updatedDynamicSecretCfg;
|
||||
}
|
||||
// if no leases just delete the config
|
||||
const deletedDynamicSecretCfg = await dynamicSecretDAL.deleteById(dynamicSecretCfg.id);
|
||||
return deletedDynamicSecretCfg;
|
||||
};
|
||||
|
||||
const getDetails = async ({
|
||||
name,
|
||||
projectSlug,
|
||||
path,
|
||||
environmentSlug,
|
||||
actorAuthMethod,
|
||||
actorOrgId,
|
||||
actorId,
|
||||
actor
|
||||
}: TDetailsDynamicSecretDTO) => {
|
||||
const project = await projectDAL.findProjectBySlug(projectSlug, actorOrgId);
|
||||
if (!project) throw new BadRequestError({ message: "Project not found" });
|
||||
|
||||
const projectId = project.id;
|
||||
const { permission } = await permissionService.getProjectPermission(
|
||||
actor,
|
||||
actorId,
|
||||
projectId,
|
||||
actorAuthMethod,
|
||||
actorOrgId
|
||||
);
|
||||
ForbiddenError.from(permission).throwUnlessCan(
|
||||
ProjectPermissionActions.Edit,
|
||||
subject(ProjectPermissionSub.Secrets, { environment: environmentSlug, secretPath: path })
|
||||
);
|
||||
|
||||
const folder = await folderDAL.findBySecretPath(projectId, environmentSlug, path);
|
||||
if (!folder) throw new BadRequestError({ message: "Folder not found" });
|
||||
|
||||
const dynamicSecretCfg = await dynamicSecretDAL.findOne({ name, folderId: folder.id });
|
||||
if (!dynamicSecretCfg) throw new BadRequestError({ message: "Dynamic secret not found" });
|
||||
const decryptedStoredInput = JSON.parse(
|
||||
infisicalSymmetricDecrypt({
|
||||
keyEncoding: dynamicSecretCfg.keyEncoding as SecretKeyEncoding,
|
||||
ciphertext: dynamicSecretCfg.inputCiphertext,
|
||||
tag: dynamicSecretCfg.inputTag,
|
||||
iv: dynamicSecretCfg.inputIV
|
||||
})
|
||||
) as object;
|
||||
const selectedProvider = dynamicSecretProviders[dynamicSecretCfg.type as DynamicSecretProviders];
|
||||
const providerInputs = (await selectedProvider.validateProviderInputs(decryptedStoredInput)) as object;
|
||||
return { ...dynamicSecretCfg, inputs: providerInputs };
|
||||
};
|
||||
|
||||
const list = async ({
|
||||
actorAuthMethod,
|
||||
actorOrgId,
|
||||
actorId,
|
||||
actor,
|
||||
projectSlug,
|
||||
path,
|
||||
environmentSlug
|
||||
}: TListDynamicSecretsDTO) => {
|
||||
const project = await projectDAL.findProjectBySlug(projectSlug, actorOrgId);
|
||||
if (!project) throw new BadRequestError({ message: "Project not found" });
|
||||
|
||||
const 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 BadRequestError({ message: "Folder not found" });
|
||||
|
||||
const dynamicSecretCfg = await dynamicSecretDAL.find({ folderId: folder.id });
|
||||
return dynamicSecretCfg;
|
||||
};
|
||||
|
||||
return {
|
||||
create,
|
||||
updateByName,
|
||||
deleteByName,
|
||||
getDetails,
|
||||
list
|
||||
};
|
||||
};
|
@ -0,0 +1,54 @@
|
||||
import { z } from "zod";
|
||||
|
||||
import { TProjectPermission } from "@app/lib/types";
|
||||
|
||||
import { DynamicSecretProviderSchema } from "./providers/models";
|
||||
|
||||
// various status for dynamic secret that happens in background
|
||||
export enum DynamicSecretStatus {
|
||||
Deleting = "Revocation in process",
|
||||
FailedDeletion = "Failed to delete"
|
||||
}
|
||||
|
||||
type TProvider = z.infer<typeof DynamicSecretProviderSchema>;
|
||||
export type TCreateDynamicSecretDTO = {
|
||||
provider: TProvider;
|
||||
defaultTTL: string;
|
||||
maxTTL?: string | null;
|
||||
path: string;
|
||||
environmentSlug: string;
|
||||
name: string;
|
||||
projectSlug: string;
|
||||
} & Omit<TProjectPermission, "projectId">;
|
||||
|
||||
export type TUpdateDynamicSecretDTO = {
|
||||
name: string;
|
||||
newName?: string;
|
||||
defaultTTL?: string;
|
||||
maxTTL?: string | null;
|
||||
path: string;
|
||||
environmentSlug: string;
|
||||
inputs?: TProvider["inputs"];
|
||||
projectSlug: string;
|
||||
} & Omit<TProjectPermission, "projectId">;
|
||||
|
||||
export type TDeleteDynamicSecretDTO = {
|
||||
name: string;
|
||||
path: string;
|
||||
environmentSlug: string;
|
||||
projectSlug: string;
|
||||
isForced?: boolean;
|
||||
} & Omit<TProjectPermission, "projectId">;
|
||||
|
||||
export type TDetailsDynamicSecretDTO = {
|
||||
name: string;
|
||||
path: string;
|
||||
environmentSlug: string;
|
||||
projectSlug: string;
|
||||
} & Omit<TProjectPermission, "projectId">;
|
||||
|
||||
export type TListDynamicSecretsDTO = {
|
||||
path: string;
|
||||
environmentSlug: string;
|
||||
projectSlug: string;
|
||||
} & Omit<TProjectPermission, "projectId">;
|
@ -0,0 +1,6 @@
|
||||
import { DynamicSecretProviders } from "./models";
|
||||
import { SqlDatabaseProvider } from "./sql-database";
|
||||
|
||||
export const buildDynamicSecretProviders = () => ({
|
||||
[DynamicSecretProviders.SqlDatabase]: SqlDatabaseProvider()
|
||||
});
|
35
backend/src/ee/services/dynamic-secret/providers/models.ts
Normal file
35
backend/src/ee/services/dynamic-secret/providers/models.ts
Normal file
@ -0,0 +1,35 @@
|
||||
import { z } from "zod";
|
||||
|
||||
export enum SqlProviders {
|
||||
Postgres = "postgres",
|
||||
MySQL = "mysql2"
|
||||
}
|
||||
|
||||
export const DynamicSecretSqlDBSchema = z.object({
|
||||
client: z.nativeEnum(SqlProviders),
|
||||
host: z.string().toLowerCase(),
|
||||
port: z.number(),
|
||||
database: z.string(),
|
||||
username: z.string(),
|
||||
password: z.string(),
|
||||
creationStatement: z.string(),
|
||||
revocationStatement: z.string(),
|
||||
renewStatement: z.string().optional(),
|
||||
ca: z.string().optional()
|
||||
});
|
||||
|
||||
export enum DynamicSecretProviders {
|
||||
SqlDatabase = "sql-database"
|
||||
}
|
||||
|
||||
export const DynamicSecretProviderSchema = z.discriminatedUnion("type", [
|
||||
z.object({ type: z.literal(DynamicSecretProviders.SqlDatabase), inputs: DynamicSecretSqlDBSchema })
|
||||
]);
|
||||
|
||||
export type TDynamicProviderFns = {
|
||||
create: (inputs: unknown, expireAt: number) => Promise<{ entityId: string; data: unknown }>;
|
||||
validateConnection: (inputs: unknown) => Promise<boolean>;
|
||||
validateProviderInputs: (inputs: object) => Promise<unknown>;
|
||||
revoke: (inputs: unknown, entityId: string) => Promise<{ entityId: string }>;
|
||||
renew: (inputs: unknown, entityId: string, expireAt: number) => Promise<{ entityId: string }>;
|
||||
};
|
152
backend/src/ee/services/dynamic-secret/providers/sql-database.ts
Normal file
152
backend/src/ee/services/dynamic-secret/providers/sql-database.ts
Normal file
@ -0,0 +1,152 @@
|
||||
import handlebars from "handlebars";
|
||||
import knex from "knex";
|
||||
import { customAlphabet } from "nanoid";
|
||||
import { z } from "zod";
|
||||
|
||||
import { getConfig } from "@app/lib/config/env";
|
||||
import { BadRequestError } from "@app/lib/errors";
|
||||
import { getDbConnectionHost } from "@app/lib/knex";
|
||||
import { alphaNumericNanoId } from "@app/lib/nanoid";
|
||||
|
||||
import { DynamicSecretSqlDBSchema, TDynamicProviderFns } from "./models";
|
||||
|
||||
const EXTERNAL_REQUEST_TIMEOUT = 10 * 1000;
|
||||
|
||||
const generatePassword = (size?: number) => {
|
||||
const charset = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-_.~!*$#";
|
||||
return customAlphabet(charset, 48)(size);
|
||||
};
|
||||
|
||||
export const SqlDatabaseProvider = (): TDynamicProviderFns => {
|
||||
const validateProviderInputs = async (inputs: unknown) => {
|
||||
const appCfg = getConfig();
|
||||
const dbHost = appCfg.DB_HOST || getDbConnectionHost(appCfg.DB_CONNECTION_URI);
|
||||
|
||||
const providerInputs = await DynamicSecretSqlDBSchema.parseAsync(inputs);
|
||||
if (
|
||||
// localhost
|
||||
providerInputs.host === "localhost" ||
|
||||
providerInputs.host === "127.0.0.1" ||
|
||||
// database infisical uses
|
||||
dbHost === providerInputs.host ||
|
||||
// internal ips
|
||||
providerInputs.host === "host.docker.internal" ||
|
||||
providerInputs.host.match(/^10\.\d+\.\d+\.\d+/) ||
|
||||
providerInputs.host.match(/^192\.168\.\d+\.\d+/)
|
||||
)
|
||||
throw new BadRequestError({ message: "Invalid db host" });
|
||||
return providerInputs;
|
||||
};
|
||||
|
||||
const getClient = async (providerInputs: z.infer<typeof DynamicSecretSqlDBSchema>) => {
|
||||
const ssl = providerInputs.ca ? { rejectUnauthorized: false, ca: providerInputs.ca } : undefined;
|
||||
const db = knex({
|
||||
client: providerInputs.client,
|
||||
connection: {
|
||||
database: providerInputs.database,
|
||||
port: providerInputs.port,
|
||||
host: providerInputs.host,
|
||||
user: providerInputs.username,
|
||||
password: providerInputs.password,
|
||||
ssl,
|
||||
pool: { min: 0, max: 1 }
|
||||
},
|
||||
acquireConnectionTimeout: EXTERNAL_REQUEST_TIMEOUT
|
||||
});
|
||||
return db;
|
||||
};
|
||||
|
||||
const validateConnection = async (inputs: unknown) => {
|
||||
const providerInputs = await validateProviderInputs(inputs);
|
||||
const db = await getClient(providerInputs);
|
||||
const isConnected = await db
|
||||
.raw("SELECT NOW()")
|
||||
.then(() => true)
|
||||
.catch(() => false);
|
||||
await db.destroy();
|
||||
return isConnected;
|
||||
};
|
||||
|
||||
const create = async (inputs: unknown, expireAt: number) => {
|
||||
const providerInputs = await validateProviderInputs(inputs);
|
||||
const db = await getClient(providerInputs);
|
||||
|
||||
const username = alphaNumericNanoId(32);
|
||||
const password = generatePassword();
|
||||
const { database } = providerInputs;
|
||||
const expiration = new Date(expireAt).toISOString();
|
||||
|
||||
const creationStatement = handlebars.compile(providerInputs.creationStatement, { noEscape: true })({
|
||||
username,
|
||||
password,
|
||||
expiration,
|
||||
database
|
||||
});
|
||||
|
||||
await db.transaction(async (tx) =>
|
||||
Promise.all(
|
||||
creationStatement
|
||||
.toString()
|
||||
.split(";")
|
||||
.filter(Boolean)
|
||||
.map((query) => tx.raw(query))
|
||||
)
|
||||
);
|
||||
await db.destroy();
|
||||
return { entityId: username, data: { DB_USERNAME: username, DB_PASSWORD: password } };
|
||||
};
|
||||
|
||||
const revoke = async (inputs: unknown, entityId: string) => {
|
||||
const providerInputs = await validateProviderInputs(inputs);
|
||||
const db = await getClient(providerInputs);
|
||||
|
||||
const username = entityId;
|
||||
const { database } = providerInputs;
|
||||
|
||||
const revokeStatement = handlebars.compile(providerInputs.revocationStatement)({ username, database });
|
||||
await db.transaction(async (tx) =>
|
||||
Promise.all(
|
||||
revokeStatement
|
||||
.toString()
|
||||
.split(";")
|
||||
.filter(Boolean)
|
||||
.map((query) => tx.raw(query))
|
||||
)
|
||||
);
|
||||
|
||||
await db.destroy();
|
||||
return { entityId: username };
|
||||
};
|
||||
|
||||
const renew = async (inputs: unknown, entityId: string, expireAt: number) => {
|
||||
const providerInputs = await validateProviderInputs(inputs);
|
||||
const db = await getClient(providerInputs);
|
||||
|
||||
const username = entityId;
|
||||
const expiration = new Date(expireAt).toISOString();
|
||||
const { database } = providerInputs;
|
||||
|
||||
const renewStatement = handlebars.compile(providerInputs.renewStatement)({ username, expiration, database });
|
||||
if (renewStatement)
|
||||
await db.transaction(async (tx) =>
|
||||
Promise.all(
|
||||
renewStatement
|
||||
.toString()
|
||||
.split(";")
|
||||
.filter(Boolean)
|
||||
.map((query) => tx.raw(query))
|
||||
)
|
||||
);
|
||||
|
||||
await db.destroy();
|
||||
return { entityId: username };
|
||||
};
|
||||
|
||||
return {
|
||||
validateProviderInputs,
|
||||
validateConnection,
|
||||
create,
|
||||
revoke,
|
||||
renew
|
||||
};
|
||||
};
|
157
backend/src/ee/services/group/group-dal.ts
Normal file
157
backend/src/ee/services/group/group-dal.ts
Normal file
@ -0,0 +1,157 @@
|
||||
import { Knex } from "knex";
|
||||
|
||||
import { TDbClient } from "@app/db";
|
||||
import { TableName, TGroups } from "@app/db/schemas";
|
||||
import { DatabaseError } from "@app/lib/errors";
|
||||
import { buildFindFilter, ormify, selectAllTableCols, TFindFilter, TFindOpt } from "@app/lib/knex";
|
||||
|
||||
export type TGroupDALFactory = ReturnType<typeof groupDALFactory>;
|
||||
|
||||
export const groupDALFactory = (db: TDbClient) => {
|
||||
const groupOrm = ormify(db, TableName.Groups);
|
||||
|
||||
const findGroups = async (filter: TFindFilter<TGroups>, { offset, limit, sort, tx }: TFindOpt<TGroups> = {}) => {
|
||||
try {
|
||||
const query = (tx || db)(TableName.Groups)
|
||||
// eslint-disable-next-line
|
||||
.where(buildFindFilter(filter))
|
||||
.select(selectAllTableCols(TableName.Groups));
|
||||
|
||||
if (limit) void query.limit(limit);
|
||||
if (offset) void query.limit(offset);
|
||||
if (sort) {
|
||||
void query.orderBy(sort.map(([column, order, nulls]) => ({ column: column as string, order, nulls })));
|
||||
}
|
||||
|
||||
const res = await query;
|
||||
return res;
|
||||
} catch (err) {
|
||||
throw new DatabaseError({ error: err, name: "Find groups" });
|
||||
}
|
||||
};
|
||||
|
||||
const findByOrgId = async (orgId: string, tx?: Knex) => {
|
||||
try {
|
||||
const docs = await (tx || db)(TableName.Groups)
|
||||
.where(`${TableName.Groups}.orgId`, orgId)
|
||||
.leftJoin(TableName.OrgRoles, `${TableName.Groups}.roleId`, `${TableName.OrgRoles}.id`)
|
||||
.select(selectAllTableCols(TableName.Groups))
|
||||
// cr stands for custom role
|
||||
.select(db.ref("id").as("crId").withSchema(TableName.OrgRoles))
|
||||
.select(db.ref("name").as("crName").withSchema(TableName.OrgRoles))
|
||||
.select(db.ref("slug").as("crSlug").withSchema(TableName.OrgRoles))
|
||||
.select(db.ref("description").as("crDescription").withSchema(TableName.OrgRoles))
|
||||
.select(db.ref("permissions").as("crPermission").withSchema(TableName.OrgRoles));
|
||||
return docs.map(({ crId, crDescription, crSlug, crPermission, crName, ...el }) => ({
|
||||
...el,
|
||||
customRole: el.roleId
|
||||
? {
|
||||
id: crId,
|
||||
name: crName,
|
||||
slug: crSlug,
|
||||
permissions: crPermission,
|
||||
description: crDescription
|
||||
}
|
||||
: undefined
|
||||
}));
|
||||
} catch (error) {
|
||||
throw new DatabaseError({ error, name: "FindByOrgId" });
|
||||
}
|
||||
};
|
||||
|
||||
const countAllGroupMembers = async ({ orgId, groupId }: { orgId: string; groupId: string }) => {
|
||||
try {
|
||||
interface CountResult {
|
||||
count: string;
|
||||
}
|
||||
|
||||
const doc = await db<CountResult>(TableName.OrgMembership)
|
||||
.where(`${TableName.OrgMembership}.orgId`, orgId)
|
||||
.join(TableName.Users, `${TableName.OrgMembership}.userId`, `${TableName.Users}.id`)
|
||||
.leftJoin(TableName.UserGroupMembership, function () {
|
||||
this.on(`${TableName.UserGroupMembership}.userId`, "=", `${TableName.Users}.id`).andOn(
|
||||
`${TableName.UserGroupMembership}.groupId`,
|
||||
"=",
|
||||
db.raw("?", [groupId])
|
||||
);
|
||||
})
|
||||
.where({ isGhost: false })
|
||||
.count(`${TableName.Users}.id`)
|
||||
.first();
|
||||
|
||||
return parseInt((doc?.count as string) || "0", 10);
|
||||
} catch (err) {
|
||||
throw new DatabaseError({ error: err, name: "Count all group members" });
|
||||
}
|
||||
};
|
||||
|
||||
// special query
|
||||
const findAllGroupMembers = async ({
|
||||
orgId,
|
||||
groupId,
|
||||
offset = 0,
|
||||
limit,
|
||||
username
|
||||
}: {
|
||||
orgId: string;
|
||||
groupId: string;
|
||||
offset?: number;
|
||||
limit?: number;
|
||||
username?: string;
|
||||
}) => {
|
||||
try {
|
||||
let query = db(TableName.OrgMembership)
|
||||
.where(`${TableName.OrgMembership}.orgId`, orgId)
|
||||
.join(TableName.Users, `${TableName.OrgMembership}.userId`, `${TableName.Users}.id`)
|
||||
.leftJoin(TableName.UserGroupMembership, function () {
|
||||
this.on(`${TableName.UserGroupMembership}.userId`, "=", `${TableName.Users}.id`).andOn(
|
||||
`${TableName.UserGroupMembership}.groupId`,
|
||||
"=",
|
||||
db.raw("?", [groupId])
|
||||
);
|
||||
})
|
||||
.select(
|
||||
db.ref("id").withSchema(TableName.OrgMembership),
|
||||
db.ref("groupId").withSchema(TableName.UserGroupMembership),
|
||||
db.ref("email").withSchema(TableName.Users),
|
||||
db.ref("username").withSchema(TableName.Users),
|
||||
db.ref("firstName").withSchema(TableName.Users),
|
||||
db.ref("lastName").withSchema(TableName.Users),
|
||||
db.ref("id").withSchema(TableName.Users).as("userId")
|
||||
)
|
||||
.where({ isGhost: false })
|
||||
.offset(offset);
|
||||
|
||||
if (limit) {
|
||||
query = query.limit(limit);
|
||||
}
|
||||
|
||||
if (username) {
|
||||
query = query.andWhere(`${TableName.Users}.username`, "ilike", `%${username}%`);
|
||||
}
|
||||
|
||||
const members = await query;
|
||||
|
||||
return members.map(
|
||||
({ email, username: memberUsername, firstName, lastName, userId, groupId: memberGroupId }) => ({
|
||||
id: userId,
|
||||
email,
|
||||
username: memberUsername,
|
||||
firstName,
|
||||
lastName,
|
||||
isPartOfGroup: !!memberGroupId
|
||||
})
|
||||
);
|
||||
} catch (error) {
|
||||
throw new DatabaseError({ error, name: "Find all org members" });
|
||||
}
|
||||
};
|
||||
|
||||
return {
|
||||
findGroups,
|
||||
findByOrgId,
|
||||
countAllGroupMembers,
|
||||
findAllGroupMembers,
|
||||
...groupOrm
|
||||
};
|
||||
};
|
474
backend/src/ee/services/group/group-service.ts
Normal file
474
backend/src/ee/services/group/group-service.ts
Normal file
@ -0,0 +1,474 @@
|
||||
import { ForbiddenError } from "@casl/ability";
|
||||
import slugify from "@sindresorhus/slugify";
|
||||
|
||||
import { OrgMembershipRole, SecretKeyEncoding, TOrgRoles } from "@app/db/schemas";
|
||||
import { isAtLeastAsPrivileged } from "@app/lib/casl";
|
||||
import { decryptAsymmetric, encryptAsymmetric, infisicalSymmetricDecrypt } from "@app/lib/crypto/encryption";
|
||||
import { BadRequestError, ForbiddenRequestError } from "@app/lib/errors";
|
||||
import { alphaNumericNanoId } from "@app/lib/nanoid";
|
||||
|
||||
import { TGroupProjectDALFactory } from "../../../services/group-project/group-project-dal";
|
||||
import { TOrgDALFactory } from "../../../services/org/org-dal";
|
||||
import { TProjectDALFactory } from "../../../services/project/project-dal";
|
||||
import { TProjectBotDALFactory } from "../../../services/project-bot/project-bot-dal";
|
||||
import { TProjectKeyDALFactory } from "../../../services/project-key/project-key-dal";
|
||||
import { TUserDALFactory } from "../../../services/user/user-dal";
|
||||
import { TLicenseServiceFactory } from "../license/license-service";
|
||||
import { OrgPermissionActions, OrgPermissionSubjects } from "../permission/org-permission";
|
||||
import { TPermissionServiceFactory } from "../permission/permission-service";
|
||||
import { TGroupDALFactory } from "./group-dal";
|
||||
import {
|
||||
TAddUserToGroupDTO,
|
||||
TCreateGroupDTO,
|
||||
TDeleteGroupDTO,
|
||||
TListGroupUsersDTO,
|
||||
TRemoveUserFromGroupDTO,
|
||||
TUpdateGroupDTO
|
||||
} from "./group-types";
|
||||
import { TUserGroupMembershipDALFactory } from "./user-group-membership-dal";
|
||||
|
||||
type TGroupServiceFactoryDep = {
|
||||
userDAL: Pick<TUserDALFactory, "findOne" | "findUserEncKeyByUsername">;
|
||||
groupDAL: Pick<
|
||||
TGroupDALFactory,
|
||||
"create" | "findOne" | "update" | "delete" | "findAllGroupMembers" | "countAllGroupMembers"
|
||||
>;
|
||||
groupProjectDAL: Pick<TGroupProjectDALFactory, "find">;
|
||||
orgDAL: Pick<TOrgDALFactory, "findMembership">;
|
||||
userGroupMembershipDAL: Pick<
|
||||
TUserGroupMembershipDALFactory,
|
||||
"findOne" | "create" | "delete" | "filterProjectsByUserMembership"
|
||||
>;
|
||||
projectDAL: Pick<TProjectDALFactory, "findProjectGhostUser">;
|
||||
projectBotDAL: Pick<TProjectBotDALFactory, "findOne">;
|
||||
projectKeyDAL: Pick<TProjectKeyDALFactory, "find" | "create" | "delete" | "findLatestProjectKey">;
|
||||
permissionService: Pick<TPermissionServiceFactory, "getOrgPermission" | "getOrgPermissionByRole">;
|
||||
licenseService: Pick<TLicenseServiceFactory, "getPlan">;
|
||||
};
|
||||
|
||||
export type TGroupServiceFactory = ReturnType<typeof groupServiceFactory>;
|
||||
|
||||
export const groupServiceFactory = ({
|
||||
userDAL,
|
||||
groupDAL,
|
||||
groupProjectDAL,
|
||||
orgDAL,
|
||||
userGroupMembershipDAL,
|
||||
projectDAL,
|
||||
projectBotDAL,
|
||||
projectKeyDAL,
|
||||
permissionService,
|
||||
licenseService
|
||||
}: TGroupServiceFactoryDep) => {
|
||||
const createGroup = async ({ name, slug, role, actor, actorId, actorAuthMethod, actorOrgId }: TCreateGroupDTO) => {
|
||||
if (!actorOrgId) throw new BadRequestError({ message: "Failed to create group without organization" });
|
||||
|
||||
const { permission } = await permissionService.getOrgPermission(
|
||||
actor,
|
||||
actorId,
|
||||
actorOrgId,
|
||||
actorAuthMethod,
|
||||
actorOrgId
|
||||
);
|
||||
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Create, OrgPermissionSubjects.Groups);
|
||||
|
||||
const plan = await licenseService.getPlan(actorOrgId);
|
||||
if (!plan.groups)
|
||||
throw new BadRequestError({
|
||||
message: "Failed to create group due to plan restriction. Upgrade plan to create group."
|
||||
});
|
||||
|
||||
const { permission: rolePermission, role: customRole } = await permissionService.getOrgPermissionByRole(
|
||||
role,
|
||||
actorOrgId
|
||||
);
|
||||
const isCustomRole = Boolean(customRole);
|
||||
const hasRequiredPriviledges = isAtLeastAsPrivileged(permission, rolePermission);
|
||||
if (!hasRequiredPriviledges) throw new BadRequestError({ message: "Failed to create a more privileged group" });
|
||||
|
||||
const group = await groupDAL.create({
|
||||
name,
|
||||
slug: slug || slugify(`${name}-${alphaNumericNanoId(4)}`),
|
||||
orgId: actorOrgId,
|
||||
role: isCustomRole ? OrgMembershipRole.Custom : role,
|
||||
roleId: customRole?.id
|
||||
});
|
||||
|
||||
return group;
|
||||
};
|
||||
|
||||
const updateGroup = async ({
|
||||
currentSlug,
|
||||
name,
|
||||
slug,
|
||||
role,
|
||||
actor,
|
||||
actorId,
|
||||
actorAuthMethod,
|
||||
actorOrgId
|
||||
}: TUpdateGroupDTO) => {
|
||||
if (!actorOrgId) throw new BadRequestError({ message: "Failed to create group without organization" });
|
||||
|
||||
const { permission } = await permissionService.getOrgPermission(
|
||||
actor,
|
||||
actorId,
|
||||
actorOrgId,
|
||||
actorAuthMethod,
|
||||
actorOrgId
|
||||
);
|
||||
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Edit, OrgPermissionSubjects.Groups);
|
||||
|
||||
const plan = await licenseService.getPlan(actorOrgId);
|
||||
if (!plan.groups)
|
||||
throw new BadRequestError({
|
||||
message: "Failed to update group due to plan restrictio Upgrade plan to update group."
|
||||
});
|
||||
|
||||
const group = await groupDAL.findOne({ orgId: actorOrgId, slug: currentSlug });
|
||||
if (!group) throw new BadRequestError({ message: `Failed to find group with slug ${currentSlug}` });
|
||||
|
||||
let customRole: TOrgRoles | undefined;
|
||||
if (role) {
|
||||
const { permission: rolePermission, role: customOrgRole } = await permissionService.getOrgPermissionByRole(
|
||||
role,
|
||||
group.orgId
|
||||
);
|
||||
|
||||
const isCustomRole = Boolean(customOrgRole);
|
||||
const hasRequiredNewRolePermission = isAtLeastAsPrivileged(permission, rolePermission);
|
||||
if (!hasRequiredNewRolePermission)
|
||||
throw new BadRequestError({ message: "Failed to create a more privileged group" });
|
||||
if (isCustomRole) customRole = customOrgRole;
|
||||
}
|
||||
|
||||
const [updatedGroup] = await groupDAL.update(
|
||||
{
|
||||
orgId: actorOrgId,
|
||||
slug: currentSlug
|
||||
},
|
||||
{
|
||||
name,
|
||||
slug: slug ? slugify(slug) : undefined,
|
||||
...(role
|
||||
? {
|
||||
role: customRole ? OrgMembershipRole.Custom : role,
|
||||
roleId: customRole?.id ?? null
|
||||
}
|
||||
: {})
|
||||
}
|
||||
);
|
||||
|
||||
return updatedGroup;
|
||||
};
|
||||
|
||||
const deleteGroup = async ({ groupSlug, actor, actorId, actorAuthMethod, actorOrgId }: TDeleteGroupDTO) => {
|
||||
if (!actorOrgId) throw new BadRequestError({ message: "Failed to create group without organization" });
|
||||
|
||||
const { permission } = await permissionService.getOrgPermission(
|
||||
actor,
|
||||
actorId,
|
||||
actorOrgId,
|
||||
actorAuthMethod,
|
||||
actorOrgId
|
||||
);
|
||||
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Delete, OrgPermissionSubjects.Groups);
|
||||
|
||||
const plan = await licenseService.getPlan(actorOrgId);
|
||||
|
||||
if (!plan.groups)
|
||||
throw new BadRequestError({
|
||||
message: "Failed to delete group due to plan restriction. Upgrade plan to delete group."
|
||||
});
|
||||
|
||||
const [group] = await groupDAL.delete({
|
||||
orgId: actorOrgId,
|
||||
slug: groupSlug
|
||||
});
|
||||
|
||||
return group;
|
||||
};
|
||||
|
||||
const listGroupUsers = async ({
|
||||
groupSlug,
|
||||
offset,
|
||||
limit,
|
||||
username,
|
||||
actor,
|
||||
actorId,
|
||||
actorAuthMethod,
|
||||
actorOrgId
|
||||
}: TListGroupUsersDTO) => {
|
||||
if (!actorOrgId) throw new BadRequestError({ message: "Failed to create group without organization" });
|
||||
|
||||
const { permission } = await permissionService.getOrgPermission(
|
||||
actor,
|
||||
actorId,
|
||||
actorOrgId,
|
||||
actorAuthMethod,
|
||||
actorOrgId
|
||||
);
|
||||
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Read, OrgPermissionSubjects.Groups);
|
||||
|
||||
const group = await groupDAL.findOne({
|
||||
orgId: actorOrgId,
|
||||
slug: groupSlug
|
||||
});
|
||||
|
||||
if (!group)
|
||||
throw new BadRequestError({
|
||||
message: `Failed to find group with slug ${groupSlug}`
|
||||
});
|
||||
|
||||
const users = await groupDAL.findAllGroupMembers({
|
||||
orgId: group.orgId,
|
||||
groupId: group.id,
|
||||
offset,
|
||||
limit,
|
||||
username
|
||||
});
|
||||
|
||||
const totalCount = await groupDAL.countAllGroupMembers({
|
||||
orgId: group.orgId,
|
||||
groupId: group.id
|
||||
});
|
||||
|
||||
return { users, totalCount };
|
||||
};
|
||||
|
||||
const addUserToGroup = async ({
|
||||
groupSlug,
|
||||
username,
|
||||
actor,
|
||||
actorId,
|
||||
actorAuthMethod,
|
||||
actorOrgId
|
||||
}: TAddUserToGroupDTO) => {
|
||||
if (!actorOrgId) throw new BadRequestError({ message: "Failed to create group without organization" });
|
||||
|
||||
const { permission } = await permissionService.getOrgPermission(
|
||||
actor,
|
||||
actorId,
|
||||
actorOrgId,
|
||||
actorAuthMethod,
|
||||
actorOrgId
|
||||
);
|
||||
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Edit, OrgPermissionSubjects.Groups);
|
||||
|
||||
// check if group with slug exists
|
||||
const group = await groupDAL.findOne({
|
||||
orgId: actorOrgId,
|
||||
slug: groupSlug
|
||||
});
|
||||
|
||||
if (!group)
|
||||
throw new BadRequestError({
|
||||
message: `Failed to find group with slug ${groupSlug}`
|
||||
});
|
||||
|
||||
const { permission: groupRolePermission } = await permissionService.getOrgPermissionByRole(group.role, actorOrgId);
|
||||
|
||||
// check if user has broader or equal to privileges than group
|
||||
const hasRequiredPriviledges = isAtLeastAsPrivileged(permission, groupRolePermission);
|
||||
if (!hasRequiredPriviledges)
|
||||
throw new ForbiddenRequestError({ message: "Failed to add user to more privileged group" });
|
||||
|
||||
// get user with username
|
||||
const user = await userDAL.findUserEncKeyByUsername({
|
||||
username
|
||||
});
|
||||
|
||||
if (!user)
|
||||
throw new BadRequestError({
|
||||
message: `Failed to find user with username ${username}`
|
||||
});
|
||||
|
||||
// check if user group membership already exists
|
||||
const existingUserGroupMembership = await userGroupMembershipDAL.findOne({
|
||||
groupId: group.id,
|
||||
userId: user.userId
|
||||
});
|
||||
|
||||
if (existingUserGroupMembership)
|
||||
throw new BadRequestError({
|
||||
message: `User ${username} is already part of the group ${groupSlug}`
|
||||
});
|
||||
|
||||
// check if user is even part of the organization
|
||||
const existingUserOrgMembership = await orgDAL.findMembership({
|
||||
userId: user.userId,
|
||||
orgId: actorOrgId
|
||||
});
|
||||
|
||||
if (!existingUserOrgMembership)
|
||||
throw new BadRequestError({
|
||||
message: `User ${username} is not part of the organization`
|
||||
});
|
||||
|
||||
await userGroupMembershipDAL.create({
|
||||
userId: user.userId,
|
||||
groupId: group.id
|
||||
});
|
||||
|
||||
// check which projects the group is part of
|
||||
const projectIds = (
|
||||
await groupProjectDAL.find({
|
||||
groupId: group.id
|
||||
})
|
||||
).map((gp) => gp.projectId);
|
||||
|
||||
const keys = await projectKeyDAL.find({
|
||||
receiverId: user.userId,
|
||||
$in: {
|
||||
projectId: projectIds
|
||||
}
|
||||
});
|
||||
|
||||
const keysSet = new Set(keys.map((k) => k.projectId));
|
||||
const projectsToAddKeyFor = projectIds.filter((p) => !keysSet.has(p));
|
||||
|
||||
for await (const projectId of projectsToAddKeyFor) {
|
||||
const ghostUser = await projectDAL.findProjectGhostUser(projectId);
|
||||
|
||||
if (!ghostUser) {
|
||||
throw new BadRequestError({
|
||||
message: "Failed to find sudo user"
|
||||
});
|
||||
}
|
||||
|
||||
const ghostUserLatestKey = await projectKeyDAL.findLatestProjectKey(ghostUser.id, projectId);
|
||||
|
||||
if (!ghostUserLatestKey) {
|
||||
throw new BadRequestError({
|
||||
message: "Failed to find sudo user latest key"
|
||||
});
|
||||
}
|
||||
|
||||
const bot = await projectBotDAL.findOne({ projectId });
|
||||
|
||||
if (!bot) {
|
||||
throw new BadRequestError({
|
||||
message: "Failed to find bot"
|
||||
});
|
||||
}
|
||||
|
||||
const botPrivateKey = infisicalSymmetricDecrypt({
|
||||
keyEncoding: bot.keyEncoding as SecretKeyEncoding,
|
||||
iv: bot.iv,
|
||||
tag: bot.tag,
|
||||
ciphertext: bot.encryptedPrivateKey
|
||||
});
|
||||
|
||||
const plaintextProjectKey = decryptAsymmetric({
|
||||
ciphertext: ghostUserLatestKey.encryptedKey,
|
||||
nonce: ghostUserLatestKey.nonce,
|
||||
publicKey: ghostUserLatestKey.sender.publicKey,
|
||||
privateKey: botPrivateKey
|
||||
});
|
||||
|
||||
const { ciphertext: encryptedKey, nonce } = encryptAsymmetric(plaintextProjectKey, user.publicKey, botPrivateKey);
|
||||
|
||||
await projectKeyDAL.create({
|
||||
encryptedKey,
|
||||
nonce,
|
||||
senderId: ghostUser.id,
|
||||
receiverId: user.userId,
|
||||
projectId
|
||||
});
|
||||
}
|
||||
|
||||
return user;
|
||||
};
|
||||
|
||||
const removeUserFromGroup = async ({
|
||||
groupSlug,
|
||||
username,
|
||||
actor,
|
||||
actorId,
|
||||
actorAuthMethod,
|
||||
actorOrgId
|
||||
}: TRemoveUserFromGroupDTO) => {
|
||||
if (!actorOrgId) throw new BadRequestError({ message: "Failed to create group without organization" });
|
||||
|
||||
const { permission } = await permissionService.getOrgPermission(
|
||||
actor,
|
||||
actorId,
|
||||
actorOrgId,
|
||||
actorAuthMethod,
|
||||
actorOrgId
|
||||
);
|
||||
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Edit, OrgPermissionSubjects.Groups);
|
||||
|
||||
// check if group with slug exists
|
||||
const group = await groupDAL.findOne({
|
||||
orgId: actorOrgId,
|
||||
slug: groupSlug
|
||||
});
|
||||
|
||||
if (!group)
|
||||
throw new BadRequestError({
|
||||
message: `Failed to find group with slug ${groupSlug}`
|
||||
});
|
||||
|
||||
const { permission: groupRolePermission } = await permissionService.getOrgPermissionByRole(group.role, actorOrgId);
|
||||
|
||||
// check if user has broader or equal to privileges than group
|
||||
const hasRequiredPriviledges = isAtLeastAsPrivileged(permission, groupRolePermission);
|
||||
if (!hasRequiredPriviledges)
|
||||
throw new ForbiddenRequestError({ message: "Failed to delete user from more privileged group" });
|
||||
|
||||
const user = await userDAL.findOne({
|
||||
username
|
||||
});
|
||||
|
||||
if (!user)
|
||||
throw new BadRequestError({
|
||||
message: `Failed to find user with username ${username}`
|
||||
});
|
||||
|
||||
// check if user group membership already exists
|
||||
const existingUserGroupMembership = await userGroupMembershipDAL.findOne({
|
||||
groupId: group.id,
|
||||
userId: user.id
|
||||
});
|
||||
|
||||
if (!existingUserGroupMembership)
|
||||
throw new BadRequestError({
|
||||
message: `User ${username} is not part of the group ${groupSlug}`
|
||||
});
|
||||
|
||||
const projectIds = (
|
||||
await groupProjectDAL.find({
|
||||
groupId: group.id
|
||||
})
|
||||
).map((gp) => gp.projectId);
|
||||
|
||||
const t = await userGroupMembershipDAL.filterProjectsByUserMembership(user.id, group.id, projectIds);
|
||||
|
||||
const projectsToDeleteKeyFor = projectIds.filter((p) => !t.has(p));
|
||||
|
||||
if (projectsToDeleteKeyFor.length) {
|
||||
await projectKeyDAL.delete({
|
||||
receiverId: user.id,
|
||||
$in: {
|
||||
projectId: projectsToDeleteKeyFor
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
await userGroupMembershipDAL.delete({
|
||||
groupId: group.id,
|
||||
userId: user.id
|
||||
});
|
||||
|
||||
return user;
|
||||
};
|
||||
|
||||
return {
|
||||
createGroup,
|
||||
updateGroup,
|
||||
deleteGroup,
|
||||
listGroupUsers,
|
||||
addUserToGroup,
|
||||
removeUserFromGroup
|
||||
};
|
||||
};
|
37
backend/src/ee/services/group/group-types.ts
Normal file
37
backend/src/ee/services/group/group-types.ts
Normal file
@ -0,0 +1,37 @@
|
||||
import { TGenericPermission } from "@app/lib/types";
|
||||
|
||||
export type TCreateGroupDTO = {
|
||||
name: string;
|
||||
slug?: string;
|
||||
role: string;
|
||||
} & TGenericPermission;
|
||||
|
||||
export type TUpdateGroupDTO = {
|
||||
currentSlug: string;
|
||||
} & Partial<{
|
||||
name: string;
|
||||
slug: string;
|
||||
role: string;
|
||||
}> &
|
||||
TGenericPermission;
|
||||
|
||||
export type TDeleteGroupDTO = {
|
||||
groupSlug: string;
|
||||
} & TGenericPermission;
|
||||
|
||||
export type TListGroupUsersDTO = {
|
||||
groupSlug: string;
|
||||
offset: number;
|
||||
limit: number;
|
||||
username?: string;
|
||||
} & TGenericPermission;
|
||||
|
||||
export type TAddUserToGroupDTO = {
|
||||
groupSlug: string;
|
||||
username: string;
|
||||
} & TGenericPermission;
|
||||
|
||||
export type TRemoveUserFromGroupDTO = {
|
||||
groupSlug: string;
|
||||
username: string;
|
||||
} & TGenericPermission;
|
125
backend/src/ee/services/group/user-group-membership-dal.ts
Normal file
125
backend/src/ee/services/group/user-group-membership-dal.ts
Normal file
@ -0,0 +1,125 @@
|
||||
import { TDbClient } from "@app/db";
|
||||
import { TableName, TUserEncryptionKeys } from "@app/db/schemas";
|
||||
import { DatabaseError } from "@app/lib/errors";
|
||||
import { ormify } from "@app/lib/knex";
|
||||
|
||||
export type TUserGroupMembershipDALFactory = ReturnType<typeof userGroupMembershipDALFactory>;
|
||||
|
||||
export const userGroupMembershipDALFactory = (db: TDbClient) => {
|
||||
const userGroupMembershipOrm = ormify(db, TableName.UserGroupMembership);
|
||||
|
||||
/**
|
||||
* Returns a sub-set of projectIds fed into this function corresponding to projects where either:
|
||||
* - The user is a direct member of the project.
|
||||
* - The user is a member of a group that is a member of the project, excluding projects that they are part of
|
||||
* through the group with id [groupId].
|
||||
*/
|
||||
const filterProjectsByUserMembership = async (userId: string, groupId: string, projectIds: string[]) => {
|
||||
const userProjectMemberships: string[] = await db(TableName.ProjectMembership)
|
||||
.where(`${TableName.ProjectMembership}.userId`, userId)
|
||||
.whereIn(`${TableName.ProjectMembership}.projectId`, projectIds)
|
||||
.pluck(`${TableName.ProjectMembership}.projectId`);
|
||||
|
||||
const userGroupMemberships: string[] = await db(TableName.UserGroupMembership)
|
||||
.where(`${TableName.UserGroupMembership}.userId`, userId)
|
||||
.whereNot(`${TableName.UserGroupMembership}.groupId`, groupId)
|
||||
.join(
|
||||
TableName.GroupProjectMembership,
|
||||
`${TableName.UserGroupMembership}.groupId`,
|
||||
`${TableName.GroupProjectMembership}.groupId`
|
||||
)
|
||||
.whereIn(`${TableName.GroupProjectMembership}.projectId`, projectIds)
|
||||
.pluck(`${TableName.GroupProjectMembership}.projectId`);
|
||||
|
||||
return new Set(userProjectMemberships.concat(userGroupMemberships));
|
||||
};
|
||||
|
||||
// special query
|
||||
const findUserGroupMembershipsInProject = async (usernames: string[], projectId: string) => {
|
||||
try {
|
||||
const usernameDocs: string[] = await db(TableName.UserGroupMembership)
|
||||
.join(
|
||||
TableName.GroupProjectMembership,
|
||||
`${TableName.UserGroupMembership}.groupId`,
|
||||
`${TableName.GroupProjectMembership}.groupId`
|
||||
)
|
||||
.join(TableName.Users, `${TableName.UserGroupMembership}.userId`, `${TableName.Users}.id`)
|
||||
.where(`${TableName.GroupProjectMembership}.projectId`, projectId)
|
||||
.whereIn(`${TableName.Users}.username`, usernames) // TODO: pluck usernames
|
||||
.pluck(`${TableName.Users}.id`);
|
||||
|
||||
return usernameDocs;
|
||||
} catch (error) {
|
||||
throw new DatabaseError({ error, name: "Find user group members in project" });
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Return list of users that are part of the group with id [groupId]
|
||||
* that have not yet been added individually to project with id [projectId].
|
||||
*
|
||||
* Note: Filters out users that are part of other groups in the project.
|
||||
* @param groupId
|
||||
* @param projectId
|
||||
* @returns
|
||||
*/
|
||||
const findGroupMembersNotInProject = async (groupId: string, projectId: string) => {
|
||||
try {
|
||||
// get list of groups in the project with id [projectId]
|
||||
// that that are not the group with id [groupId]
|
||||
const groups: string[] = await db(TableName.GroupProjectMembership)
|
||||
.where(`${TableName.GroupProjectMembership}.projectId`, projectId)
|
||||
.whereNot(`${TableName.GroupProjectMembership}.groupId`, groupId)
|
||||
.pluck(`${TableName.GroupProjectMembership}.groupId`);
|
||||
|
||||
// main query
|
||||
const members = await db(TableName.UserGroupMembership)
|
||||
.where(`${TableName.UserGroupMembership}.groupId`, groupId)
|
||||
.join(TableName.Users, `${TableName.UserGroupMembership}.userId`, `${TableName.Users}.id`)
|
||||
.leftJoin(TableName.ProjectMembership, function () {
|
||||
this.on(`${TableName.Users}.id`, "=", `${TableName.ProjectMembership}.userId`).andOn(
|
||||
`${TableName.ProjectMembership}.projectId`,
|
||||
"=",
|
||||
db.raw("?", [projectId])
|
||||
);
|
||||
})
|
||||
.whereNull(`${TableName.ProjectMembership}.userId`)
|
||||
.leftJoin<TUserEncryptionKeys>(
|
||||
TableName.UserEncryptionKey,
|
||||
`${TableName.UserEncryptionKey}.userId`,
|
||||
`${TableName.Users}.id`
|
||||
)
|
||||
.select(
|
||||
db.ref("id").withSchema(TableName.UserGroupMembership),
|
||||
db.ref("groupId").withSchema(TableName.UserGroupMembership),
|
||||
db.ref("email").withSchema(TableName.Users),
|
||||
db.ref("username").withSchema(TableName.Users),
|
||||
db.ref("firstName").withSchema(TableName.Users),
|
||||
db.ref("lastName").withSchema(TableName.Users),
|
||||
db.ref("id").withSchema(TableName.Users).as("userId"),
|
||||
db.ref("publicKey").withSchema(TableName.UserEncryptionKey)
|
||||
)
|
||||
.where({ isGhost: false }) // MAKE SURE USER IS NOT A GHOST USER
|
||||
.whereNotIn(`${TableName.UserGroupMembership}.userId`, function () {
|
||||
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
||||
this.select(`${TableName.UserGroupMembership}.userId`)
|
||||
.from(TableName.UserGroupMembership)
|
||||
.whereIn(`${TableName.UserGroupMembership}.groupId`, groups);
|
||||
});
|
||||
|
||||
return members.map(({ email, username, firstName, lastName, userId, publicKey, ...data }) => ({
|
||||
...data,
|
||||
user: { email, username, firstName, lastName, id: userId, publicKey }
|
||||
}));
|
||||
} catch (error) {
|
||||
throw new DatabaseError({ error, name: "Find group members not in project" });
|
||||
}
|
||||
};
|
||||
|
||||
return {
|
||||
...userGroupMembershipOrm,
|
||||
filterProjectsByUserMembership,
|
||||
findUserGroupMembershipsInProject,
|
||||
findGroupMembersNotInProject
|
||||
};
|
||||
};
|
12
backend/src/ee/services/identity-project-additional-privilege/identity-project-additional-privilege-dal.ts
Normal file
12
backend/src/ee/services/identity-project-additional-privilege/identity-project-additional-privilege-dal.ts
Normal file
@ -0,0 +1,12 @@
|
||||
import { TDbClient } from "@app/db";
|
||||
import { TableName } from "@app/db/schemas";
|
||||
import { ormify } from "@app/lib/knex";
|
||||
|
||||
export type TIdentityProjectAdditionalPrivilegeDALFactory = ReturnType<
|
||||
typeof identityProjectAdditionalPrivilegeDALFactory
|
||||
>;
|
||||
|
||||
export const identityProjectAdditionalPrivilegeDALFactory = (db: TDbClient) => {
|
||||
const orm = ormify(db, TableName.IdentityProjectAdditionalPrivilege);
|
||||
return orm;
|
||||
};
|
297
backend/src/ee/services/identity-project-additional-privilege/identity-project-additional-privilege-service.ts
Normal file
297
backend/src/ee/services/identity-project-additional-privilege/identity-project-additional-privilege-service.ts
Normal file
@ -0,0 +1,297 @@
|
||||
import { ForbiddenError } from "@casl/ability";
|
||||
import ms from "ms";
|
||||
|
||||
import { isAtLeastAsPrivileged } from "@app/lib/casl";
|
||||
import { BadRequestError, ForbiddenRequestError } from "@app/lib/errors";
|
||||
import { ActorType } from "@app/services/auth/auth-type";
|
||||
import { TIdentityProjectDALFactory } from "@app/services/identity-project/identity-project-dal";
|
||||
import { TProjectDALFactory } from "@app/services/project/project-dal";
|
||||
|
||||
import { TPermissionServiceFactory } from "../permission/permission-service";
|
||||
import { ProjectPermissionActions, ProjectPermissionSub } from "../permission/project-permission";
|
||||
import { TIdentityProjectAdditionalPrivilegeDALFactory } from "./identity-project-additional-privilege-dal";
|
||||
import {
|
||||
IdentityProjectAdditionalPrivilegeTemporaryMode,
|
||||
TCreateIdentityPrivilegeDTO,
|
||||
TDeleteIdentityPrivilegeDTO,
|
||||
TGetIdentityPrivilegeDetailsDTO,
|
||||
TListIdentityPrivilegesDTO,
|
||||
TUpdateIdentityPrivilegeDTO
|
||||
} from "./identity-project-additional-privilege-types";
|
||||
|
||||
type TIdentityProjectAdditionalPrivilegeServiceFactoryDep = {
|
||||
identityProjectAdditionalPrivilegeDAL: TIdentityProjectAdditionalPrivilegeDALFactory;
|
||||
identityProjectDAL: Pick<TIdentityProjectDALFactory, "findOne" | "findById">;
|
||||
projectDAL: Pick<TProjectDALFactory, "findProjectBySlug">;
|
||||
permissionService: Pick<TPermissionServiceFactory, "getProjectPermission">;
|
||||
};
|
||||
|
||||
export type TIdentityProjectAdditionalPrivilegeServiceFactory = ReturnType<
|
||||
typeof identityProjectAdditionalPrivilegeServiceFactory
|
||||
>;
|
||||
|
||||
export const identityProjectAdditionalPrivilegeServiceFactory = ({
|
||||
identityProjectAdditionalPrivilegeDAL,
|
||||
identityProjectDAL,
|
||||
permissionService,
|
||||
projectDAL
|
||||
}: TIdentityProjectAdditionalPrivilegeServiceFactoryDep) => {
|
||||
const create = async ({
|
||||
slug,
|
||||
actor,
|
||||
actorId,
|
||||
identityId,
|
||||
projectSlug,
|
||||
permissions: customPermission,
|
||||
actorOrgId,
|
||||
actorAuthMethod,
|
||||
...dto
|
||||
}: TCreateIdentityPrivilegeDTO) => {
|
||||
const project = await projectDAL.findProjectBySlug(projectSlug, actorOrgId);
|
||||
if (!project) throw new BadRequestError({ message: "Project not found" });
|
||||
const projectId = project.id;
|
||||
|
||||
const identityProjectMembership = await identityProjectDAL.findOne({ identityId, projectId });
|
||||
if (!identityProjectMembership)
|
||||
throw new BadRequestError({ message: `Failed to find identity with id ${identityId}` });
|
||||
|
||||
const { permission } = await permissionService.getProjectPermission(
|
||||
actor,
|
||||
actorId,
|
||||
identityProjectMembership.projectId,
|
||||
actorAuthMethod,
|
||||
actorOrgId
|
||||
);
|
||||
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Edit, ProjectPermissionSub.Identity);
|
||||
const { permission: identityRolePermission } = await permissionService.getProjectPermission(
|
||||
ActorType.IDENTITY,
|
||||
identityId,
|
||||
identityProjectMembership.projectId,
|
||||
actorAuthMethod,
|
||||
actorOrgId
|
||||
);
|
||||
const hasRequiredPriviledges = isAtLeastAsPrivileged(permission, identityRolePermission);
|
||||
if (!hasRequiredPriviledges)
|
||||
throw new ForbiddenRequestError({ message: "Failed to update more privileged identity" });
|
||||
|
||||
const existingSlug = await identityProjectAdditionalPrivilegeDAL.findOne({
|
||||
slug,
|
||||
projectMembershipId: identityProjectMembership.id
|
||||
});
|
||||
if (existingSlug) throw new BadRequestError({ message: "Additional privilege of provided slug exist" });
|
||||
|
||||
if (!dto.isTemporary) {
|
||||
const additionalPrivilege = await identityProjectAdditionalPrivilegeDAL.create({
|
||||
projectMembershipId: identityProjectMembership.id,
|
||||
slug,
|
||||
permissions: customPermission
|
||||
});
|
||||
return additionalPrivilege;
|
||||
}
|
||||
|
||||
const relativeTempAllocatedTimeInMs = ms(dto.temporaryRange);
|
||||
const additionalPrivilege = await identityProjectAdditionalPrivilegeDAL.create({
|
||||
projectMembershipId: identityProjectMembership.id,
|
||||
slug,
|
||||
permissions: customPermission,
|
||||
isTemporary: true,
|
||||
temporaryMode: IdentityProjectAdditionalPrivilegeTemporaryMode.Relative,
|
||||
temporaryRange: dto.temporaryRange,
|
||||
temporaryAccessStartTime: new Date(dto.temporaryAccessStartTime),
|
||||
temporaryAccessEndTime: new Date(new Date(dto.temporaryAccessStartTime).getTime() + relativeTempAllocatedTimeInMs)
|
||||
});
|
||||
return additionalPrivilege;
|
||||
};
|
||||
|
||||
const updateBySlug = async ({
|
||||
projectSlug,
|
||||
slug,
|
||||
identityId,
|
||||
data,
|
||||
actorOrgId,
|
||||
actor,
|
||||
actorId,
|
||||
actorAuthMethod
|
||||
}: TUpdateIdentityPrivilegeDTO) => {
|
||||
const project = await projectDAL.findProjectBySlug(projectSlug, actorOrgId);
|
||||
if (!project) throw new BadRequestError({ message: "Project not found" });
|
||||
const projectId = project.id;
|
||||
|
||||
const identityProjectMembership = await identityProjectDAL.findOne({ identityId, projectId });
|
||||
if (!identityProjectMembership)
|
||||
throw new BadRequestError({ message: `Failed to find identity with id ${identityId}` });
|
||||
|
||||
const { permission } = await permissionService.getProjectPermission(
|
||||
actor,
|
||||
actorId,
|
||||
identityProjectMembership.projectId,
|
||||
actorAuthMethod,
|
||||
actorOrgId
|
||||
);
|
||||
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Edit, ProjectPermissionSub.Identity);
|
||||
const { permission: identityRolePermission } = await permissionService.getProjectPermission(
|
||||
ActorType.IDENTITY,
|
||||
identityProjectMembership.identityId,
|
||||
identityProjectMembership.projectId,
|
||||
actorAuthMethod,
|
||||
actorOrgId
|
||||
);
|
||||
const hasRequiredPriviledges = isAtLeastAsPrivileged(permission, identityRolePermission);
|
||||
if (!hasRequiredPriviledges)
|
||||
throw new ForbiddenRequestError({ message: "Failed to update more privileged identity" });
|
||||
|
||||
const identityPrivilege = await identityProjectAdditionalPrivilegeDAL.findOne({
|
||||
slug,
|
||||
projectMembershipId: identityProjectMembership.id
|
||||
});
|
||||
if (!identityPrivilege) throw new BadRequestError({ message: "Identity additional privilege not found" });
|
||||
if (data?.slug) {
|
||||
const existingSlug = await identityProjectAdditionalPrivilegeDAL.findOne({
|
||||
slug: data.slug,
|
||||
projectMembershipId: identityProjectMembership.id
|
||||
});
|
||||
if (existingSlug && existingSlug.id !== identityPrivilege.id)
|
||||
throw new BadRequestError({ message: "Additional privilege of provided slug exist" });
|
||||
}
|
||||
|
||||
const isTemporary = typeof data?.isTemporary !== "undefined" ? data.isTemporary : identityPrivilege.isTemporary;
|
||||
if (isTemporary) {
|
||||
const temporaryAccessStartTime = data?.temporaryAccessStartTime || identityPrivilege?.temporaryAccessStartTime;
|
||||
const temporaryRange = data?.temporaryRange || identityPrivilege?.temporaryRange;
|
||||
const additionalPrivilege = await identityProjectAdditionalPrivilegeDAL.updateById(identityPrivilege.id, {
|
||||
...data,
|
||||
temporaryAccessStartTime: new Date(temporaryAccessStartTime || ""),
|
||||
temporaryAccessEndTime: new Date(new Date(temporaryAccessStartTime || "").getTime() + ms(temporaryRange || ""))
|
||||
});
|
||||
return additionalPrivilege;
|
||||
}
|
||||
|
||||
const additionalPrivilege = await identityProjectAdditionalPrivilegeDAL.updateById(identityPrivilege.id, {
|
||||
...data,
|
||||
isTemporary: false,
|
||||
temporaryAccessStartTime: null,
|
||||
temporaryAccessEndTime: null,
|
||||
temporaryRange: null,
|
||||
temporaryMode: null
|
||||
});
|
||||
return additionalPrivilege;
|
||||
};
|
||||
|
||||
const deleteBySlug = async ({
|
||||
actorId,
|
||||
slug,
|
||||
identityId,
|
||||
projectSlug,
|
||||
actor,
|
||||
actorOrgId,
|
||||
actorAuthMethod
|
||||
}: TDeleteIdentityPrivilegeDTO) => {
|
||||
const project = await projectDAL.findProjectBySlug(projectSlug, actorOrgId);
|
||||
if (!project) throw new BadRequestError({ message: "Project not found" });
|
||||
const projectId = project.id;
|
||||
|
||||
const identityProjectMembership = await identityProjectDAL.findOne({ identityId, projectId });
|
||||
if (!identityProjectMembership)
|
||||
throw new BadRequestError({ message: `Failed to find identity with id ${identityId}` });
|
||||
|
||||
const { permission } = await permissionService.getProjectPermission(
|
||||
actor,
|
||||
actorId,
|
||||
identityProjectMembership.projectId,
|
||||
actorAuthMethod,
|
||||
actorOrgId
|
||||
);
|
||||
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Edit, ProjectPermissionSub.Identity);
|
||||
const { permission: identityRolePermission } = await permissionService.getProjectPermission(
|
||||
ActorType.IDENTITY,
|
||||
identityProjectMembership.identityId,
|
||||
identityProjectMembership.projectId,
|
||||
actorAuthMethod,
|
||||
actorOrgId
|
||||
);
|
||||
const hasRequiredPriviledges = isAtLeastAsPrivileged(permission, identityRolePermission);
|
||||
if (!hasRequiredPriviledges)
|
||||
throw new ForbiddenRequestError({ message: "Failed to edit more privileged identity" });
|
||||
|
||||
const identityPrivilege = await identityProjectAdditionalPrivilegeDAL.findOne({
|
||||
slug,
|
||||
projectMembershipId: identityProjectMembership.id
|
||||
});
|
||||
if (!identityPrivilege) throw new BadRequestError({ message: "Identity additional privilege not found" });
|
||||
|
||||
const deletedPrivilege = await identityProjectAdditionalPrivilegeDAL.deleteById(identityPrivilege.id);
|
||||
return deletedPrivilege;
|
||||
};
|
||||
|
||||
const getPrivilegeDetailsBySlug = async ({
|
||||
projectSlug,
|
||||
identityId,
|
||||
slug,
|
||||
actorOrgId,
|
||||
actor,
|
||||
actorId,
|
||||
actorAuthMethod
|
||||
}: TGetIdentityPrivilegeDetailsDTO) => {
|
||||
const project = await projectDAL.findProjectBySlug(projectSlug, actorOrgId);
|
||||
if (!project) throw new BadRequestError({ message: "Project not found" });
|
||||
const projectId = project.id;
|
||||
|
||||
const identityProjectMembership = await identityProjectDAL.findOne({ identityId, projectId });
|
||||
if (!identityProjectMembership)
|
||||
throw new BadRequestError({ message: `Failed to find identity with id ${identityId}` });
|
||||
const { permission } = await permissionService.getProjectPermission(
|
||||
actor,
|
||||
actorId,
|
||||
identityProjectMembership.projectId,
|
||||
actorAuthMethod,
|
||||
actorOrgId
|
||||
);
|
||||
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Edit, ProjectPermissionSub.Identity);
|
||||
|
||||
const identityPrivilege = await identityProjectAdditionalPrivilegeDAL.findOne({
|
||||
slug,
|
||||
projectMembershipId: identityProjectMembership.id
|
||||
});
|
||||
if (!identityPrivilege) throw new BadRequestError({ message: "Identity additional privilege not found" });
|
||||
|
||||
return identityPrivilege;
|
||||
};
|
||||
|
||||
const listIdentityProjectPrivileges = async ({
|
||||
identityId,
|
||||
actorOrgId,
|
||||
actor,
|
||||
actorId,
|
||||
actorAuthMethod,
|
||||
projectSlug
|
||||
}: TListIdentityPrivilegesDTO) => {
|
||||
const project = await projectDAL.findProjectBySlug(projectSlug, actorOrgId);
|
||||
if (!project) throw new BadRequestError({ message: "Project not found" });
|
||||
const projectId = project.id;
|
||||
|
||||
const identityProjectMembership = await identityProjectDAL.findOne({ identityId, projectId });
|
||||
if (!identityProjectMembership)
|
||||
throw new BadRequestError({ message: `Failed to find identity with id ${identityId}` });
|
||||
const { permission } = await permissionService.getProjectPermission(
|
||||
actor,
|
||||
actorId,
|
||||
identityProjectMembership.projectId,
|
||||
actorAuthMethod,
|
||||
actorOrgId
|
||||
);
|
||||
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Edit, ProjectPermissionSub.Identity);
|
||||
|
||||
const identityPrivileges = await identityProjectAdditionalPrivilegeDAL.find({
|
||||
projectMembershipId: identityProjectMembership.id
|
||||
});
|
||||
return identityPrivileges;
|
||||
};
|
||||
|
||||
return {
|
||||
create,
|
||||
updateBySlug,
|
||||
deleteBySlug,
|
||||
getPrivilegeDetailsBySlug,
|
||||
listIdentityProjectPrivileges
|
||||
};
|
||||
};
|
54
backend/src/ee/services/identity-project-additional-privilege/identity-project-additional-privilege-types.ts
Normal file
54
backend/src/ee/services/identity-project-additional-privilege/identity-project-additional-privilege-types.ts
Normal file
@ -0,0 +1,54 @@
|
||||
import { TProjectPermission } from "@app/lib/types";
|
||||
|
||||
export enum IdentityProjectAdditionalPrivilegeTemporaryMode {
|
||||
Relative = "relative"
|
||||
}
|
||||
|
||||
export type TCreateIdentityPrivilegeDTO = {
|
||||
permissions: unknown;
|
||||
identityId: string;
|
||||
projectSlug: string;
|
||||
slug: string;
|
||||
} & (
|
||||
| {
|
||||
isTemporary: false;
|
||||
}
|
||||
| {
|
||||
isTemporary: true;
|
||||
temporaryMode: IdentityProjectAdditionalPrivilegeTemporaryMode.Relative;
|
||||
temporaryRange: string;
|
||||
temporaryAccessStartTime: string;
|
||||
}
|
||||
) &
|
||||
Omit<TProjectPermission, "projectId">;
|
||||
|
||||
export type TUpdateIdentityPrivilegeDTO = { slug: string; identityId: string; projectSlug: string } & Omit<
|
||||
TProjectPermission,
|
||||
"projectId"
|
||||
> & {
|
||||
data: Partial<{
|
||||
permissions: unknown;
|
||||
slug: string;
|
||||
isTemporary: boolean;
|
||||
temporaryMode: IdentityProjectAdditionalPrivilegeTemporaryMode.Relative;
|
||||
temporaryRange: string;
|
||||
temporaryAccessStartTime: string;
|
||||
}>;
|
||||
};
|
||||
|
||||
export type TDeleteIdentityPrivilegeDTO = Omit<TProjectPermission, "projectId"> & {
|
||||
slug: string;
|
||||
identityId: string;
|
||||
projectSlug: string;
|
||||
};
|
||||
|
||||
export type TGetIdentityPrivilegeDetailsDTO = Omit<TProjectPermission, "projectId"> & {
|
||||
slug: string;
|
||||
identityId: string;
|
||||
projectSlug: string;
|
||||
};
|
||||
|
||||
export type TListIdentityPrivilegesDTO = Omit<TProjectPermission, "projectId"> & {
|
||||
identityId: string;
|
||||
projectSlug: string;
|
||||
};
|
@ -12,7 +12,6 @@ import {
|
||||
infisicalSymmetricEncypt
|
||||
} from "@app/lib/crypto/encryption";
|
||||
import { BadRequestError } from "@app/lib/errors";
|
||||
import { TOrgPermission } from "@app/lib/types";
|
||||
import { AuthMethod, AuthTokenType } from "@app/services/auth/auth-type";
|
||||
import { TOrgBotDALFactory } from "@app/services/org/org-bot-dal";
|
||||
import { TOrgDALFactory } from "@app/services/org/org-dal";
|
||||
@ -24,7 +23,7 @@ import { TLicenseServiceFactory } from "../license/license-service";
|
||||
import { OrgPermissionActions, OrgPermissionSubjects } from "../permission/org-permission";
|
||||
import { TPermissionServiceFactory } from "../permission/permission-service";
|
||||
import { TLdapConfigDALFactory } from "./ldap-config-dal";
|
||||
import { TCreateLdapCfgDTO, TLdapLoginDTO, TUpdateLdapCfgDTO } from "./ldap-config-types";
|
||||
import { TCreateLdapCfgDTO, TGetLdapCfgDTO, TLdapLoginDTO, TUpdateLdapCfgDTO } from "./ldap-config-types";
|
||||
|
||||
type TLdapConfigServiceFactoryDep = {
|
||||
ldapConfigDAL: TLdapConfigDALFactory;
|
||||
@ -55,6 +54,7 @@ export const ldapConfigServiceFactory = ({
|
||||
actorId,
|
||||
orgId,
|
||||
actorOrgId,
|
||||
actorAuthMethod,
|
||||
isActive,
|
||||
url,
|
||||
bindDN,
|
||||
@ -62,7 +62,7 @@ export const ldapConfigServiceFactory = ({
|
||||
searchBase,
|
||||
caCert
|
||||
}: TCreateLdapCfgDTO) => {
|
||||
const { permission } = await permissionService.getOrgPermission(actor, actorId, orgId, actorOrgId);
|
||||
const { permission } = await permissionService.getOrgPermission(actor, actorId, orgId, actorAuthMethod, actorOrgId);
|
||||
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Create, OrgPermissionSubjects.Ldap);
|
||||
|
||||
const plan = await licenseService.getPlan(orgId);
|
||||
@ -149,13 +149,14 @@ export const ldapConfigServiceFactory = ({
|
||||
orgId,
|
||||
actorOrgId,
|
||||
isActive,
|
||||
actorAuthMethod,
|
||||
url,
|
||||
bindDN,
|
||||
bindPass,
|
||||
searchBase,
|
||||
caCert
|
||||
}: TUpdateLdapCfgDTO) => {
|
||||
const { permission } = await permissionService.getOrgPermission(actor, actorId, orgId, actorOrgId);
|
||||
const { permission } = await permissionService.getOrgPermission(actor, actorId, orgId, actorAuthMethod, actorOrgId);
|
||||
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Edit, OrgPermissionSubjects.Ldap);
|
||||
|
||||
const plan = await licenseService.getPlan(orgId);
|
||||
@ -274,8 +275,14 @@ export const ldapConfigServiceFactory = ({
|
||||
};
|
||||
};
|
||||
|
||||
const getLdapCfgWithPermissionCheck = async ({ actor, actorId, orgId, actorOrgId }: TOrgPermission) => {
|
||||
const { permission } = await permissionService.getOrgPermission(actor, actorId, orgId, actorOrgId);
|
||||
const getLdapCfgWithPermissionCheck = async ({
|
||||
actor,
|
||||
actorId,
|
||||
orgId,
|
||||
actorAuthMethod,
|
||||
actorOrgId
|
||||
}: TGetLdapCfgDTO) => {
|
||||
const { permission } = await permissionService.getOrgPermission(actor, actorId, orgId, actorAuthMethod, actorOrgId);
|
||||
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Read, OrgPermissionSubjects.Ldap);
|
||||
return getLdapCfg({
|
||||
orgId
|
||||
|
@ -1,6 +1,7 @@
|
||||
import { TOrgPermission } from "@app/lib/types";
|
||||
|
||||
export type TCreateLdapCfgDTO = {
|
||||
orgId: string;
|
||||
isActive: boolean;
|
||||
url: string;
|
||||
bindDN: string;
|
||||
@ -9,7 +10,9 @@ export type TCreateLdapCfgDTO = {
|
||||
caCert: string;
|
||||
} & TOrgPermission;
|
||||
|
||||
export type TUpdateLdapCfgDTO = Partial<{
|
||||
export type TUpdateLdapCfgDTO = {
|
||||
orgId: string;
|
||||
} & Partial<{
|
||||
isActive: boolean;
|
||||
url: string;
|
||||
bindDN: string;
|
||||
@ -19,6 +22,10 @@ export type TUpdateLdapCfgDTO = Partial<{
|
||||
}> &
|
||||
TOrgPermission;
|
||||
|
||||
export type TGetLdapCfgDTO = {
|
||||
orgId: string;
|
||||
} & TOrgPermission;
|
||||
|
||||
export type TLdapLoginDTO = {
|
||||
externalId: string;
|
||||
username: string;
|
||||
|
@ -17,9 +17,10 @@ export const getDefaultOnPremFeatures = () => {
|
||||
customAlerts: false,
|
||||
auditLogs: false,
|
||||
auditLogsRetentionDays: 0,
|
||||
samlSSO: true,
|
||||
samlSSO: false,
|
||||
scim: false,
|
||||
ldap: false,
|
||||
groups: false,
|
||||
status: null,
|
||||
trial_end: null,
|
||||
has_used_trial: true,
|
||||
|
@ -15,6 +15,7 @@ export const getDefaultOnPremFeatures = (): TFeatureSet => ({
|
||||
membersUsed: 0,
|
||||
environmentLimit: null,
|
||||
environmentsUsed: 0,
|
||||
dynamicSecret: false,
|
||||
secretVersioning: true,
|
||||
pitRecovery: false,
|
||||
ipAllowlisting: false,
|
||||
@ -23,9 +24,10 @@ export const getDefaultOnPremFeatures = (): TFeatureSet => ({
|
||||
customAlerts: false,
|
||||
auditLogs: false,
|
||||
auditLogsRetentionDays: 0,
|
||||
samlSSO: true,
|
||||
samlSSO: false,
|
||||
scim: false,
|
||||
ldap: false,
|
||||
groups: false,
|
||||
status: null,
|
||||
trial_end: null,
|
||||
has_used_trial: true,
|
||||
|
@ -224,9 +224,10 @@ export const licenseServiceFactory = ({
|
||||
actor,
|
||||
actorId,
|
||||
actorOrgId,
|
||||
actorAuthMethod,
|
||||
billingCycle
|
||||
}: TOrgPlansTableDTO) => {
|
||||
const { permission } = await permissionService.getOrgPermission(actor, actorId, orgId, actorOrgId);
|
||||
const { permission } = await permissionService.getOrgPermission(actor, actorId, orgId, actorAuthMethod, actorOrgId);
|
||||
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Read, OrgPermissionSubjects.Billing);
|
||||
const { data } = await licenseServerCloudApi.request.get(
|
||||
`/api/license-server/v1/cloud-products?billing-cycle=${billingCycle}`
|
||||
@ -234,15 +235,22 @@ export const licenseServiceFactory = ({
|
||||
return data;
|
||||
};
|
||||
|
||||
const getOrgPlan = async ({ orgId, actor, actorId, actorOrgId, projectId }: TOrgPlanDTO) => {
|
||||
const { permission } = await permissionService.getOrgPermission(actor, actorId, orgId, actorOrgId);
|
||||
const getOrgPlan = async ({ orgId, actor, actorId, actorOrgId, actorAuthMethod, projectId }: TOrgPlanDTO) => {
|
||||
const { permission } = await permissionService.getOrgPermission(actor, actorId, orgId, actorAuthMethod, actorOrgId);
|
||||
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Read, OrgPermissionSubjects.Billing);
|
||||
const plan = await getPlan(orgId, projectId);
|
||||
return plan;
|
||||
};
|
||||
|
||||
const startOrgTrial = async ({ orgId, actorId, actor, actorOrgId, success_url }: TStartOrgTrialDTO) => {
|
||||
const { permission } = await permissionService.getOrgPermission(actor, actorId, orgId, actorOrgId);
|
||||
const startOrgTrial = async ({
|
||||
orgId,
|
||||
actorId,
|
||||
actor,
|
||||
actorOrgId,
|
||||
actorAuthMethod,
|
||||
success_url
|
||||
}: TStartOrgTrialDTO) => {
|
||||
const { permission } = await permissionService.getOrgPermission(actor, actorId, orgId, actorAuthMethod, actorOrgId);
|
||||
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Create, OrgPermissionSubjects.Billing);
|
||||
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Edit, OrgPermissionSubjects.Billing);
|
||||
|
||||
@ -263,8 +271,14 @@ export const licenseServiceFactory = ({
|
||||
return { url };
|
||||
};
|
||||
|
||||
const createOrganizationPortalSession = async ({ orgId, actorId, actor, actorOrgId }: TCreateOrgPortalSession) => {
|
||||
const { permission } = await permissionService.getOrgPermission(actor, actorId, orgId, actorOrgId);
|
||||
const createOrganizationPortalSession = async ({
|
||||
orgId,
|
||||
actorId,
|
||||
actor,
|
||||
actorAuthMethod,
|
||||
actorOrgId
|
||||
}: TCreateOrgPortalSession) => {
|
||||
const { permission } = await permissionService.getOrgPermission(actor, actorId, orgId, actorAuthMethod, actorOrgId);
|
||||
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Create, OrgPermissionSubjects.Billing);
|
||||
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Edit, OrgPermissionSubjects.Billing);
|
||||
|
||||
@ -310,8 +324,8 @@ export const licenseServiceFactory = ({
|
||||
return { url };
|
||||
};
|
||||
|
||||
const getOrgBillingInfo = async ({ orgId, actor, actorId, actorOrgId }: TGetOrgBillInfoDTO) => {
|
||||
const { permission } = await permissionService.getOrgPermission(actor, actorId, orgId, actorOrgId);
|
||||
const getOrgBillingInfo = async ({ orgId, actor, actorId, actorAuthMethod, actorOrgId }: TGetOrgBillInfoDTO) => {
|
||||
const { permission } = await permissionService.getOrgPermission(actor, actorId, orgId, actorAuthMethod, actorOrgId);
|
||||
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Read, OrgPermissionSubjects.Billing);
|
||||
|
||||
const organization = await orgDAL.findOrgById(orgId);
|
||||
@ -327,8 +341,8 @@ export const licenseServiceFactory = ({
|
||||
};
|
||||
|
||||
// returns org current plan feature table
|
||||
const getOrgPlanTable = async ({ orgId, actor, actorId, actorOrgId }: TGetOrgBillInfoDTO) => {
|
||||
const { permission } = await permissionService.getOrgPermission(actor, actorId, orgId, actorOrgId);
|
||||
const getOrgPlanTable = async ({ orgId, actor, actorId, actorAuthMethod, actorOrgId }: TGetOrgBillInfoDTO) => {
|
||||
const { permission } = await permissionService.getOrgPermission(actor, actorId, orgId, actorAuthMethod, actorOrgId);
|
||||
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Read, OrgPermissionSubjects.Billing);
|
||||
|
||||
const organization = await orgDAL.findOrgById(orgId);
|
||||
@ -343,8 +357,8 @@ export const licenseServiceFactory = ({
|
||||
return data;
|
||||
};
|
||||
|
||||
const getOrgBillingDetails = async ({ orgId, actor, actorId, actorOrgId }: TGetOrgBillInfoDTO) => {
|
||||
const { permission } = await permissionService.getOrgPermission(actor, actorId, orgId, actorOrgId);
|
||||
const getOrgBillingDetails = async ({ orgId, actor, actorId, actorAuthMethod, actorOrgId }: TGetOrgBillInfoDTO) => {
|
||||
const { permission } = await permissionService.getOrgPermission(actor, actorId, orgId, actorAuthMethod, actorOrgId);
|
||||
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Read, OrgPermissionSubjects.Billing);
|
||||
|
||||
const organization = await orgDAL.findOrgById(orgId);
|
||||
@ -364,11 +378,12 @@ export const licenseServiceFactory = ({
|
||||
actorId,
|
||||
actor,
|
||||
actorOrgId,
|
||||
actorAuthMethod,
|
||||
orgId,
|
||||
name,
|
||||
email
|
||||
}: TUpdateOrgBillingDetailsDTO) => {
|
||||
const { permission } = await permissionService.getOrgPermission(actor, actorId, orgId, actorOrgId);
|
||||
const { permission } = await permissionService.getOrgPermission(actor, actorId, orgId, actorAuthMethod, actorOrgId);
|
||||
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Read, OrgPermissionSubjects.Billing);
|
||||
|
||||
const organization = await orgDAL.findOrgById(orgId);
|
||||
@ -387,8 +402,8 @@ export const licenseServiceFactory = ({
|
||||
return data;
|
||||
};
|
||||
|
||||
const getOrgPmtMethods = async ({ orgId, actor, actorId, actorOrgId }: TOrgPmtMethodsDTO) => {
|
||||
const { permission } = await permissionService.getOrgPermission(actor, actorId, orgId, actorOrgId);
|
||||
const getOrgPmtMethods = async ({ orgId, actor, actorId, actorAuthMethod, actorOrgId }: TOrgPmtMethodsDTO) => {
|
||||
const { permission } = await permissionService.getOrgPermission(actor, actorId, orgId, actorAuthMethod, actorOrgId);
|
||||
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Read, OrgPermissionSubjects.Billing);
|
||||
|
||||
const organization = await orgDAL.findOrgById(orgId);
|
||||
@ -410,11 +425,12 @@ export const licenseServiceFactory = ({
|
||||
orgId,
|
||||
actor,
|
||||
actorId,
|
||||
actorAuthMethod,
|
||||
actorOrgId,
|
||||
success_url,
|
||||
cancel_url
|
||||
}: TAddOrgPmtMethodDTO) => {
|
||||
const { permission } = await permissionService.getOrgPermission(actor, actorId, orgId, actorOrgId);
|
||||
const { permission } = await permissionService.getOrgPermission(actor, actorId, orgId, actorAuthMethod, actorOrgId);
|
||||
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Read, OrgPermissionSubjects.Billing);
|
||||
|
||||
const organization = await orgDAL.findOrgById(orgId);
|
||||
@ -435,8 +451,15 @@ export const licenseServiceFactory = ({
|
||||
return { url };
|
||||
};
|
||||
|
||||
const delOrgPmtMethods = async ({ actorId, actor, actorOrgId, orgId, pmtMethodId }: TDelOrgPmtMethodDTO) => {
|
||||
const { permission } = await permissionService.getOrgPermission(actor, actorId, orgId, actorOrgId);
|
||||
const delOrgPmtMethods = async ({
|
||||
actorId,
|
||||
actor,
|
||||
actorAuthMethod,
|
||||
actorOrgId,
|
||||
orgId,
|
||||
pmtMethodId
|
||||
}: TDelOrgPmtMethodDTO) => {
|
||||
const { permission } = await permissionService.getOrgPermission(actor, actorId, orgId, actorAuthMethod, actorOrgId);
|
||||
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Read, OrgPermissionSubjects.Billing);
|
||||
|
||||
const organization = await orgDAL.findOrgById(orgId);
|
||||
@ -452,8 +475,8 @@ export const licenseServiceFactory = ({
|
||||
return data;
|
||||
};
|
||||
|
||||
const getOrgTaxIds = async ({ orgId, actor, actorId, actorOrgId }: TGetOrgTaxIdDTO) => {
|
||||
const { permission } = await permissionService.getOrgPermission(actor, actorId, orgId, actorOrgId);
|
||||
const getOrgTaxIds = async ({ orgId, actor, actorId, actorAuthMethod, actorOrgId }: TGetOrgTaxIdDTO) => {
|
||||
const { permission } = await permissionService.getOrgPermission(actor, actorId, orgId, actorAuthMethod, actorOrgId);
|
||||
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Read, OrgPermissionSubjects.Billing);
|
||||
|
||||
const organization = await orgDAL.findOrgById(orgId);
|
||||
@ -470,8 +493,8 @@ export const licenseServiceFactory = ({
|
||||
return taxIds;
|
||||
};
|
||||
|
||||
const addOrgTaxId = async ({ actorId, actor, actorOrgId, orgId, type, value }: TAddOrgTaxIdDTO) => {
|
||||
const { permission } = await permissionService.getOrgPermission(actor, actorId, orgId, actorOrgId);
|
||||
const addOrgTaxId = async ({ actorId, actor, actorAuthMethod, actorOrgId, orgId, type, value }: TAddOrgTaxIdDTO) => {
|
||||
const { permission } = await permissionService.getOrgPermission(actor, actorId, orgId, actorAuthMethod, actorOrgId);
|
||||
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Read, OrgPermissionSubjects.Billing);
|
||||
|
||||
const organization = await orgDAL.findOrgById(orgId);
|
||||
@ -491,8 +514,8 @@ export const licenseServiceFactory = ({
|
||||
return data;
|
||||
};
|
||||
|
||||
const delOrgTaxId = async ({ orgId, actor, actorId, actorOrgId, taxId }: TDelOrgTaxIdDTO) => {
|
||||
const { permission } = await permissionService.getOrgPermission(actor, actorId, orgId, actorOrgId);
|
||||
const delOrgTaxId = async ({ orgId, actor, actorId, actorAuthMethod, actorOrgId, taxId }: TDelOrgTaxIdDTO) => {
|
||||
const { permission } = await permissionService.getOrgPermission(actor, actorId, orgId, actorAuthMethod, actorOrgId);
|
||||
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Read, OrgPermissionSubjects.Billing);
|
||||
|
||||
const organization = await orgDAL.findOrgById(orgId);
|
||||
@ -508,8 +531,8 @@ export const licenseServiceFactory = ({
|
||||
return data;
|
||||
};
|
||||
|
||||
const getOrgTaxInvoices = async ({ actorId, actor, actorOrgId, orgId }: TOrgInvoiceDTO) => {
|
||||
const { permission } = await permissionService.getOrgPermission(actor, actorId, orgId, actorOrgId);
|
||||
const getOrgTaxInvoices = async ({ actorId, actor, actorOrgId, actorAuthMethod, orgId }: TOrgInvoiceDTO) => {
|
||||
const { permission } = await permissionService.getOrgPermission(actor, actorId, orgId, actorAuthMethod, actorOrgId);
|
||||
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Read, OrgPermissionSubjects.Billing);
|
||||
|
||||
const organization = await orgDAL.findOrgById(orgId);
|
||||
@ -525,8 +548,8 @@ export const licenseServiceFactory = ({
|
||||
return invoices;
|
||||
};
|
||||
|
||||
const getOrgLicenses = async ({ orgId, actor, actorId, actorOrgId }: TOrgLicensesDTO) => {
|
||||
const { permission } = await permissionService.getOrgPermission(actor, actorId, orgId, actorOrgId);
|
||||
const getOrgLicenses = async ({ orgId, actor, actorId, actorAuthMethod, actorOrgId }: TOrgLicensesDTO) => {
|
||||
const { permission } = await permissionService.getOrgPermission(actor, actorId, orgId, actorAuthMethod, actorOrgId);
|
||||
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Read, OrgPermissionSubjects.Billing);
|
||||
|
||||
const organization = await orgDAL.findOrgById(orgId);
|
||||
|
@ -27,6 +27,7 @@ export type TFeatureSet = {
|
||||
tier: -1;
|
||||
workspaceLimit: null;
|
||||
workspacesUsed: 0;
|
||||
dynamicSecret: false;
|
||||
memberLimit: null;
|
||||
membersUsed: 0;
|
||||
environmentLimit: null;
|
||||
@ -39,9 +40,10 @@ export type TFeatureSet = {
|
||||
customAlerts: false;
|
||||
auditLogs: false;
|
||||
auditLogsRetentionDays: 0;
|
||||
samlSSO: true;
|
||||
samlSSO: false;
|
||||
scim: false;
|
||||
ldap: false;
|
||||
groups: false;
|
||||
status: null;
|
||||
trial_end: null;
|
||||
has_used_trial: true;
|
||||
|
@ -18,6 +18,7 @@ export enum OrgPermissionSubjects {
|
||||
Sso = "sso",
|
||||
Scim = "scim",
|
||||
Ldap = "ldap",
|
||||
Groups = "groups",
|
||||
Billing = "billing",
|
||||
SecretScanning = "secret-scanning",
|
||||
Identity = "identity"
|
||||
@ -33,6 +34,7 @@ export type OrgPermissionSet =
|
||||
| [OrgPermissionActions, OrgPermissionSubjects.Sso]
|
||||
| [OrgPermissionActions, OrgPermissionSubjects.Scim]
|
||||
| [OrgPermissionActions, OrgPermissionSubjects.Ldap]
|
||||
| [OrgPermissionActions, OrgPermissionSubjects.Groups]
|
||||
| [OrgPermissionActions, OrgPermissionSubjects.SecretScanning]
|
||||
| [OrgPermissionActions, OrgPermissionSubjects.Billing]
|
||||
| [OrgPermissionActions, OrgPermissionSubjects.Identity];
|
||||
@ -83,6 +85,11 @@ const buildAdminPermission = () => {
|
||||
can(OrgPermissionActions.Edit, OrgPermissionSubjects.Ldap);
|
||||
can(OrgPermissionActions.Delete, OrgPermissionSubjects.Ldap);
|
||||
|
||||
can(OrgPermissionActions.Read, OrgPermissionSubjects.Groups);
|
||||
can(OrgPermissionActions.Create, OrgPermissionSubjects.Groups);
|
||||
can(OrgPermissionActions.Edit, OrgPermissionSubjects.Groups);
|
||||
can(OrgPermissionActions.Delete, OrgPermissionSubjects.Groups);
|
||||
|
||||
can(OrgPermissionActions.Read, OrgPermissionSubjects.Billing);
|
||||
can(OrgPermissionActions.Create, OrgPermissionSubjects.Billing);
|
||||
can(OrgPermissionActions.Edit, OrgPermissionSubjects.Billing);
|
||||
@ -105,6 +112,7 @@ const buildMemberPermission = () => {
|
||||
can(OrgPermissionActions.Create, OrgPermissionSubjects.Workspace);
|
||||
can(OrgPermissionActions.Read, OrgPermissionSubjects.Member);
|
||||
can(OrgPermissionActions.Create, OrgPermissionSubjects.Member);
|
||||
can(OrgPermissionActions.Read, OrgPermissionSubjects.Groups);
|
||||
can(OrgPermissionActions.Read, OrgPermissionSubjects.Role);
|
||||
can(OrgPermissionActions.Read, OrgPermissionSubjects.Settings);
|
||||
can(OrgPermissionActions.Read, OrgPermissionSubjects.Billing);
|
||||
|
@ -45,6 +45,43 @@ export const permissionDALFactory = (db: TDbClient) => {
|
||||
|
||||
const getProjectPermission = async (userId: string, projectId: string) => {
|
||||
try {
|
||||
const groups: string[] = await db(TableName.GroupProjectMembership)
|
||||
.where(`${TableName.GroupProjectMembership}.projectId`, projectId)
|
||||
.pluck(`${TableName.GroupProjectMembership}.groupId`);
|
||||
|
||||
const groupDocs = await db(TableName.UserGroupMembership)
|
||||
.where(`${TableName.UserGroupMembership}.userId`, userId)
|
||||
.whereIn(`${TableName.UserGroupMembership}.groupId`, groups)
|
||||
.join(
|
||||
TableName.GroupProjectMembership,
|
||||
`${TableName.GroupProjectMembership}.groupId`,
|
||||
`${TableName.UserGroupMembership}.groupId`
|
||||
)
|
||||
.join(
|
||||
TableName.GroupProjectMembershipRole,
|
||||
`${TableName.GroupProjectMembershipRole}.projectMembershipId`,
|
||||
`${TableName.GroupProjectMembership}.id`
|
||||
)
|
||||
.leftJoin(
|
||||
TableName.ProjectRoles,
|
||||
`${TableName.GroupProjectMembershipRole}.customRoleId`,
|
||||
`${TableName.ProjectRoles}.id`
|
||||
)
|
||||
.join(TableName.Project, `${TableName.GroupProjectMembership}.projectId`, `${TableName.Project}.id`)
|
||||
.join(TableName.Organization, `${TableName.Project}.orgId`, `${TableName.Organization}.id`)
|
||||
.select(selectAllTableCols(TableName.GroupProjectMembershipRole))
|
||||
.select(
|
||||
db.ref("id").withSchema(TableName.GroupProjectMembership).as("membershipId"),
|
||||
// TODO(roll-forward-migration): remove this field when we drop this in next migration after a week
|
||||
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")
|
||||
)
|
||||
.select("permissions");
|
||||
|
||||
const docs = await db(TableName.ProjectMembership)
|
||||
.join(
|
||||
TableName.ProjectUserMembershipRole,
|
||||
@ -56,6 +93,11 @@ export const permissionDALFactory = (db: TDbClient) => {
|
||||
`${TableName.ProjectUserMembershipRole}.customRoleId`,
|
||||
`${TableName.ProjectRoles}.id`
|
||||
)
|
||||
.leftJoin(
|
||||
TableName.ProjectUserAdditionalPrivilege,
|
||||
`${TableName.ProjectUserAdditionalPrivilege}.projectMembershipId`,
|
||||
`${TableName.ProjectMembership}.id`
|
||||
)
|
||||
.join(TableName.Project, `${TableName.ProjectMembership}.projectId`, `${TableName.Project}.id`)
|
||||
.join(TableName.Organization, `${TableName.Project}.orgId`, `${TableName.Organization}.id`)
|
||||
.where("userId", userId)
|
||||
@ -64,30 +106,36 @@ export const permissionDALFactory = (db: TDbClient) => {
|
||||
.select(
|
||||
db.ref("id").withSchema(TableName.ProjectMembership).as("membershipId"),
|
||||
// TODO(roll-forward-migration): remove this field when we drop this in next migration after a week
|
||||
db.ref("role").withSchema(TableName.ProjectMembership).as("oldRoleField"),
|
||||
db.ref("createdAt").withSchema(TableName.ProjectMembership).as("membershipCreatedAt"),
|
||||
db.ref("updatedAt").withSchema(TableName.ProjectMembership).as("membershipUpdatedAt"),
|
||||
db.ref("projectId").withSchema(TableName.ProjectMembership),
|
||||
db.ref("authEnforced").withSchema(TableName.Organization).as("orgAuthEnforced"),
|
||||
db.ref("orgId").withSchema(TableName.Project),
|
||||
db.ref("slug").withSchema(TableName.ProjectRoles).as("customRoleSlug")
|
||||
)
|
||||
.select("permissions");
|
||||
db.ref("slug").withSchema(TableName.ProjectRoles).as("customRoleSlug"),
|
||||
db.ref("permissions").withSchema(TableName.ProjectRoles),
|
||||
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("temporaryAccessStartTime")
|
||||
.withSchema(TableName.ProjectUserAdditionalPrivilege)
|
||||
.as("userApTemporaryAccessStartTime"),
|
||||
db
|
||||
.ref("temporaryAccessEndTime")
|
||||
.withSchema(TableName.ProjectUserAdditionalPrivilege)
|
||||
.as("userApTemporaryAccessEndTime")
|
||||
);
|
||||
|
||||
const permission = sqlNestRelationships({
|
||||
data: docs,
|
||||
key: "membershipId",
|
||||
parentMapper: ({
|
||||
orgId,
|
||||
orgAuthEnforced,
|
||||
membershipId,
|
||||
membershipCreatedAt,
|
||||
membershipUpdatedAt,
|
||||
oldRoleField
|
||||
}) => ({
|
||||
key: "projectId",
|
||||
parentMapper: ({ orgId, orgAuthEnforced, membershipId, membershipCreatedAt, membershipUpdatedAt, role }) => ({
|
||||
orgId,
|
||||
orgAuthEnforced,
|
||||
userId,
|
||||
role: oldRoleField,
|
||||
role,
|
||||
id: membershipId,
|
||||
projectId,
|
||||
createdAt: membershipCreatedAt,
|
||||
@ -102,15 +150,91 @@ export const permissionDALFactory = (db: TDbClient) => {
|
||||
permissions: z.unknown(),
|
||||
customRoleSlug: z.string().optional().nullable()
|
||||
}).parse(data)
|
||||
},
|
||||
{
|
||||
key: "userApId",
|
||||
label: "additionalPrivileges" as const,
|
||||
mapper: ({
|
||||
userApId,
|
||||
userApPermissions,
|
||||
userApIsTemporary,
|
||||
userApTemporaryMode,
|
||||
userApTemporaryRange,
|
||||
userApTemporaryAccessEndTime,
|
||||
userApTemporaryAccessStartTime
|
||||
}) => ({
|
||||
id: userApId,
|
||||
permissions: userApPermissions,
|
||||
temporaryRange: userApTemporaryRange,
|
||||
temporaryMode: userApTemporaryMode,
|
||||
temporaryAccessEndTime: userApTemporaryAccessEndTime,
|
||||
temporaryAccessStartTime: userApTemporaryAccessStartTime,
|
||||
isTemporary: userApIsTemporary
|
||||
})
|
||||
}
|
||||
]
|
||||
});
|
||||
|
||||
const groupPermission = groupDocs.length
|
||||
? sqlNestRelationships({
|
||||
data: groupDocs,
|
||||
key: "projectId",
|
||||
parentMapper: ({
|
||||
orgId,
|
||||
orgAuthEnforced,
|
||||
membershipId,
|
||||
membershipCreatedAt,
|
||||
membershipUpdatedAt,
|
||||
role
|
||||
}) => ({
|
||||
orgId,
|
||||
orgAuthEnforced,
|
||||
userId,
|
||||
role,
|
||||
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)
|
||||
}
|
||||
]
|
||||
})
|
||||
: [];
|
||||
|
||||
if (!permission?.[0] && !groupPermission[0]) return undefined;
|
||||
|
||||
// when introducting cron mode change it here
|
||||
const activeRoles = permission?.[0]?.roles.filter(
|
||||
const activeRoles =
|
||||
permission?.[0]?.roles?.filter(
|
||||
({ isTemporary, temporaryAccessEndTime }) =>
|
||||
!isTemporary || (isTemporary && temporaryAccessEndTime && new Date() < temporaryAccessEndTime)
|
||||
) ?? [];
|
||||
|
||||
const activeGroupRoles =
|
||||
groupPermission?.[0]?.roles?.filter(
|
||||
({ isTemporary, temporaryAccessEndTime }) =>
|
||||
!isTemporary || (isTemporary && temporaryAccessEndTime && new Date() < temporaryAccessEndTime)
|
||||
) ?? [];
|
||||
|
||||
const activeAdditionalPrivileges = permission?.[0]?.additionalPrivileges?.filter(
|
||||
({ isTemporary, temporaryAccessEndTime }) =>
|
||||
!isTemporary || (isTemporary && temporaryAccessEndTime && new Date() < temporaryAccessEndTime)
|
||||
);
|
||||
return permission?.[0] ? { ...permission[0], roles: activeRoles } : undefined;
|
||||
|
||||
return {
|
||||
...(permission[0] || groupPermission[0]),
|
||||
roles: [...activeRoles, ...activeGroupRoles],
|
||||
additionalPrivileges: activeAdditionalPrivileges
|
||||
};
|
||||
} catch (error) {
|
||||
throw new DatabaseError({ error, name: "GetProjectPermission" });
|
||||
}
|
||||
@ -129,31 +253,62 @@ export const permissionDALFactory = (db: TDbClient) => {
|
||||
`${TableName.IdentityProjectMembershipRole}.customRoleId`,
|
||||
`${TableName.ProjectRoles}.id`
|
||||
)
|
||||
.leftJoin(
|
||||
TableName.IdentityProjectAdditionalPrivilege,
|
||||
`${TableName.IdentityProjectAdditionalPrivilege}.projectMembershipId`,
|
||||
`${TableName.IdentityProjectMembership}.id`
|
||||
)
|
||||
.join(
|
||||
// Join the Project table to later select orgId
|
||||
TableName.Project,
|
||||
`${TableName.IdentityProjectMembership}.projectId`,
|
||||
`${TableName.Project}.id`
|
||||
)
|
||||
.where("identityId", identityId)
|
||||
.where(`${TableName.IdentityProjectMembership}.projectId`, projectId)
|
||||
.select(selectAllTableCols(TableName.IdentityProjectMembershipRole))
|
||||
.select(
|
||||
db.ref("id").withSchema(TableName.IdentityProjectMembership).as("membershipId"),
|
||||
db.ref("orgId").withSchema(TableName.Project).as("orgId"), // Now you can select orgId from Project
|
||||
db.ref("role").withSchema(TableName.IdentityProjectMembership).as("oldRoleField"),
|
||||
db.ref("createdAt").withSchema(TableName.IdentityProjectMembership).as("membershipCreatedAt"),
|
||||
db.ref("updatedAt").withSchema(TableName.IdentityProjectMembership).as("membershipUpdatedAt"),
|
||||
db.ref("slug").withSchema(TableName.ProjectRoles).as("customRoleSlug")
|
||||
)
|
||||
.select("permissions");
|
||||
db.ref("slug").withSchema(TableName.ProjectRoles).as("customRoleSlug"),
|
||||
db.ref("permissions").withSchema(TableName.ProjectRoles),
|
||||
db.ref("id").withSchema(TableName.IdentityProjectAdditionalPrivilege).as("identityApId"),
|
||||
db.ref("permissions").withSchema(TableName.IdentityProjectAdditionalPrivilege).as("identityApPermissions"),
|
||||
db
|
||||
.ref("temporaryMode")
|
||||
.withSchema(TableName.IdentityProjectAdditionalPrivilege)
|
||||
.as("identityApTemporaryMode"),
|
||||
db.ref("isTemporary").withSchema(TableName.IdentityProjectAdditionalPrivilege).as("identityApIsTemporary"),
|
||||
db
|
||||
.ref("temporaryRange")
|
||||
.withSchema(TableName.IdentityProjectAdditionalPrivilege)
|
||||
.as("identityApTemporaryRange"),
|
||||
db
|
||||
.ref("temporaryAccessStartTime")
|
||||
.withSchema(TableName.IdentityProjectAdditionalPrivilege)
|
||||
.as("identityApTemporaryAccessStartTime"),
|
||||
db
|
||||
.ref("temporaryAccessEndTime")
|
||||
.withSchema(TableName.IdentityProjectAdditionalPrivilege)
|
||||
.as("identityApTemporaryAccessEndTime")
|
||||
);
|
||||
|
||||
const permission = sqlNestRelationships({
|
||||
data: docs,
|
||||
key: "membershipId",
|
||||
parentMapper: ({ membershipId, membershipCreatedAt, membershipUpdatedAt, oldRoleField }) => ({
|
||||
parentMapper: ({ membershipId, membershipCreatedAt, membershipUpdatedAt, oldRoleField, orgId }) => ({
|
||||
id: membershipId,
|
||||
identityId,
|
||||
projectId,
|
||||
role: oldRoleField,
|
||||
createdAt: membershipCreatedAt,
|
||||
updatedAt: membershipUpdatedAt,
|
||||
orgId,
|
||||
// just a prefilled value
|
||||
orgAuthEnforced: false,
|
||||
orgId: ""
|
||||
orgAuthEnforced: false
|
||||
}),
|
||||
childrenMapper: [
|
||||
{
|
||||
@ -164,16 +319,44 @@ export const permissionDALFactory = (db: TDbClient) => {
|
||||
permissions: z.unknown(),
|
||||
customRoleSlug: z.string().optional().nullable()
|
||||
}).parse(data)
|
||||
},
|
||||
{
|
||||
key: "identityApId",
|
||||
label: "additionalPrivileges" as const,
|
||||
mapper: ({
|
||||
identityApId,
|
||||
identityApPermissions,
|
||||
identityApIsTemporary,
|
||||
identityApTemporaryMode,
|
||||
identityApTemporaryRange,
|
||||
identityApTemporaryAccessEndTime,
|
||||
identityApTemporaryAccessStartTime
|
||||
}) => ({
|
||||
id: identityApId,
|
||||
permissions: identityApPermissions,
|
||||
temporaryRange: identityApTemporaryRange,
|
||||
temporaryMode: identityApTemporaryMode,
|
||||
temporaryAccessEndTime: identityApTemporaryAccessEndTime,
|
||||
temporaryAccessStartTime: identityApTemporaryAccessStartTime,
|
||||
isTemporary: identityApIsTemporary
|
||||
})
|
||||
}
|
||||
]
|
||||
});
|
||||
|
||||
if (!permission?.[0]) return undefined;
|
||||
|
||||
// when introducting cron mode change it here
|
||||
const activeRoles = permission?.[0]?.roles.filter(
|
||||
({ isTemporary, temporaryAccessEndTime }) =>
|
||||
!isTemporary || (isTemporary && temporaryAccessEndTime && new Date() < temporaryAccessEndTime)
|
||||
);
|
||||
return permission?.[0] ? { ...permission[0], roles: activeRoles } : undefined;
|
||||
const activeAdditionalPrivileges = permission?.[0]?.additionalPrivileges?.filter(
|
||||
({ isTemporary, temporaryAccessEndTime }) =>
|
||||
!isTemporary || (isTemporary && temporaryAccessEndTime && new Date() < temporaryAccessEndTime)
|
||||
);
|
||||
|
||||
return { ...permission[0], roles: activeRoles, additionalPrivileges: activeAdditionalPrivileges };
|
||||
} catch (error) {
|
||||
throw new DatabaseError({ error, name: "GetProjectIdentityPermission" });
|
||||
}
|
||||
|
27
backend/src/ee/services/permission/permission-fns.ts
Normal file
27
backend/src/ee/services/permission/permission-fns.ts
Normal file
@ -0,0 +1,27 @@
|
||||
import { TOrganizations } from "@app/db/schemas";
|
||||
import { UnauthorizedError } from "@app/lib/errors";
|
||||
import { ActorAuthMethod, AuthMethod } from "@app/services/auth/auth-type";
|
||||
|
||||
function isAuthMethodSaml(actorAuthMethod: ActorAuthMethod) {
|
||||
if (!actorAuthMethod) return false;
|
||||
|
||||
return [
|
||||
AuthMethod.AZURE_SAML,
|
||||
AuthMethod.OKTA_SAML,
|
||||
AuthMethod.JUMPCLOUD_SAML,
|
||||
AuthMethod.GOOGLE_SAML,
|
||||
AuthMethod.KEYCLOAK_SAML
|
||||
].includes(actorAuthMethod);
|
||||
}
|
||||
|
||||
function validateOrgSAML(actorAuthMethod: ActorAuthMethod, isSamlEnforced: TOrganizations["authEnforced"]) {
|
||||
if (actorAuthMethod === undefined) {
|
||||
throw new UnauthorizedError({ name: "No auth method defined" });
|
||||
}
|
||||
|
||||
if (isSamlEnforced && actorAuthMethod !== null && !isAuthMethodSaml(actorAuthMethod)) {
|
||||
throw new UnauthorizedError({ name: "Cannot access org-scoped resource" });
|
||||
}
|
||||
}
|
||||
|
||||
export { isAuthMethodSaml, validateOrgSAML };
|
@ -11,13 +11,15 @@ import {
|
||||
} from "@app/db/schemas";
|
||||
import { conditionsMatcher } from "@app/lib/casl";
|
||||
import { BadRequestError, UnauthorizedError } from "@app/lib/errors";
|
||||
import { ActorType } from "@app/services/auth/auth-type";
|
||||
import { ActorAuthMethod, ActorType } from "@app/services/auth/auth-type";
|
||||
import { TOrgRoleDALFactory } from "@app/services/org/org-role-dal";
|
||||
import { TProjectDALFactory } from "@app/services/project/project-dal";
|
||||
import { TProjectRoleDALFactory } from "@app/services/project-role/project-role-dal";
|
||||
import { TServiceTokenDALFactory } from "@app/services/service-token/service-token-dal";
|
||||
|
||||
import { orgAdminPermissions, orgMemberPermissions, orgNoAccessPermissions, OrgPermissionSet } from "./org-permission";
|
||||
import { TPermissionDALFactory } from "./permission-dal";
|
||||
import { validateOrgSAML } from "./permission-fns";
|
||||
import { TBuildProjectPermissionDTO } from "./permission-types";
|
||||
import {
|
||||
buildServiceTokenProjectPermission,
|
||||
@ -32,6 +34,7 @@ type TPermissionServiceFactoryDep = {
|
||||
orgRoleDAL: Pick<TOrgRoleDALFactory, "findOne">;
|
||||
projectRoleDAL: Pick<TProjectRoleDALFactory, "findOne">;
|
||||
serviceTokenDAL: Pick<TServiceTokenDALFactory, "findById">;
|
||||
projectDAL: Pick<TProjectDALFactory, "findById">;
|
||||
permissionDAL: TPermissionDALFactory;
|
||||
};
|
||||
|
||||
@ -41,7 +44,8 @@ export const permissionServiceFactory = ({
|
||||
permissionDAL,
|
||||
orgRoleDAL,
|
||||
projectRoleDAL,
|
||||
serviceTokenDAL
|
||||
serviceTokenDAL,
|
||||
projectDAL
|
||||
}: TPermissionServiceFactoryDep) => {
|
||||
const buildOrgPermission = (role: string, permission?: unknown) => {
|
||||
switch (role) {
|
||||
@ -98,16 +102,30 @@ export const permissionServiceFactory = ({
|
||||
|
||||
/*
|
||||
* Get user permission in an organization
|
||||
* */
|
||||
const getUserOrgPermission = async (userId: string, orgId: string, userOrgId?: string) => {
|
||||
*/
|
||||
const getUserOrgPermission = async (
|
||||
userId: string,
|
||||
orgId: string,
|
||||
authMethod: ActorAuthMethod,
|
||||
userOrgId?: string
|
||||
) => {
|
||||
const membership = await permissionDAL.getOrgPermission(userId, orgId);
|
||||
if (!membership) throw new UnauthorizedError({ name: "User not in org" });
|
||||
if (membership.role === OrgMembershipRole.Custom && !membership.permissions) {
|
||||
throw new BadRequestError({ name: "Custom permission not found" });
|
||||
}
|
||||
if (membership.orgAuthEnforced && membership.orgId !== userOrgId) {
|
||||
throw new BadRequestError({ name: "Cannot access org-scoped resource" });
|
||||
|
||||
// If the org ID is API_KEY, the request is being made with an API Key.
|
||||
// Since we can't scope API keys to an organization, we'll need to do an arbitrary check to see if the user is a member of the organization.
|
||||
|
||||
// Extra: This means that when users are using API keys to make requests, they can't use slug-based routes.
|
||||
// Slug-based routes depend on the organization ID being present on the request, since project slugs aren't globally unique, and we need a way to filter by organization.
|
||||
if (userOrgId !== "API_KEY" && membership.orgId !== userOrgId) {
|
||||
throw new UnauthorizedError({ name: "You are not logged into this organization" });
|
||||
}
|
||||
|
||||
validateOrgSAML(authMethod, membership.orgAuthEnforced);
|
||||
|
||||
return { permission: buildOrgPermission(membership.role, membership.permissions), membership };
|
||||
};
|
||||
|
||||
@ -120,10 +138,16 @@ export const permissionServiceFactory = ({
|
||||
return { permission: buildOrgPermission(membership.role, membership.permissions), membership };
|
||||
};
|
||||
|
||||
const getOrgPermission = async (type: ActorType, id: string, orgId: string, actorOrgId?: string) => {
|
||||
const getOrgPermission = async (
|
||||
type: ActorType,
|
||||
id: string,
|
||||
orgId: string,
|
||||
authMethod: ActorAuthMethod,
|
||||
actorOrgId: string | undefined
|
||||
) => {
|
||||
switch (type) {
|
||||
case ActorType.USER:
|
||||
return getUserOrgPermission(id, orgId, actorOrgId);
|
||||
return getUserOrgPermission(id, orgId, authMethod, actorOrgId);
|
||||
case ActorType.IDENTITY:
|
||||
return getIdentityOrgPermission(id, orgId);
|
||||
default:
|
||||
@ -153,6 +177,7 @@ export const permissionServiceFactory = ({
|
||||
const getUserProjectPermission = async (
|
||||
userId: string,
|
||||
projectId: string,
|
||||
authMethod: ActorAuthMethod,
|
||||
userOrgId?: string
|
||||
): Promise<TProjectPermissionRT<ActorType.USER>> => {
|
||||
const userProjectPermission = await permissionDAL.getProjectPermission(userId, projectId);
|
||||
@ -164,12 +189,27 @@ export const permissionServiceFactory = ({
|
||||
throw new BadRequestError({ name: "Custom permission not found" });
|
||||
}
|
||||
|
||||
if (userProjectPermission.orgAuthEnforced && userProjectPermission.orgId !== userOrgId) {
|
||||
throw new BadRequestError({ name: "Cannot access org-scoped resource" });
|
||||
// If the org ID is API_KEY, the request is being made with an API Key.
|
||||
// Since we can't scope API keys to an organization, we'll need to do an arbitrary check to see if the user is a member of the organization.
|
||||
|
||||
// Extra: This means that when users are using API keys to make requests, they can't use slug-based routes.
|
||||
// Slug-based routes depend on the organization ID being present on the request, since project slugs aren't globally unique, and we need a way to filter by organization.
|
||||
if (userOrgId !== "API_KEY" && userProjectPermission.orgId !== userOrgId) {
|
||||
throw new UnauthorizedError({ name: "You are not logged into this organization" });
|
||||
}
|
||||
|
||||
validateOrgSAML(authMethod, userProjectPermission.orgAuthEnforced);
|
||||
|
||||
// join two permissions and pass to build the final permission set
|
||||
const rolePermissions = userProjectPermission.roles?.map(({ role, permissions }) => ({ role, permissions })) || [];
|
||||
const additionalPrivileges =
|
||||
userProjectPermission.additionalPrivileges?.map(({ permissions }) => ({
|
||||
role: ProjectMembershipRole.Custom,
|
||||
permissions
|
||||
})) || [];
|
||||
|
||||
return {
|
||||
permission: buildProjectPermission(userProjectPermission.roles),
|
||||
permission: buildProjectPermission(rolePermissions.concat(additionalPrivileges)),
|
||||
membership: userProjectPermission,
|
||||
hasRole: (role: string) =>
|
||||
userProjectPermission.roles.findIndex(
|
||||
@ -180,7 +220,8 @@ export const permissionServiceFactory = ({
|
||||
|
||||
const getIdentityProjectPermission = async (
|
||||
identityId: string,
|
||||
projectId: string
|
||||
projectId: string,
|
||||
identityOrgId: string | undefined
|
||||
): Promise<TProjectPermissionRT<ActorType.IDENTITY>> => {
|
||||
const identityProjectPermission = await permissionDAL.getProjectIdentityPermission(identityId, projectId);
|
||||
if (!identityProjectPermission) throw new UnauthorizedError({ name: "Identity not in project" });
|
||||
@ -193,8 +234,20 @@ export const permissionServiceFactory = ({
|
||||
throw new BadRequestError({ name: "Custom permission not found" });
|
||||
}
|
||||
|
||||
if (identityProjectPermission.orgId !== identityOrgId) {
|
||||
throw new UnauthorizedError({ name: "You are not a member of this organization" });
|
||||
}
|
||||
|
||||
const rolePermissions =
|
||||
identityProjectPermission.roles?.map(({ role, permissions }) => ({ role, permissions })) || [];
|
||||
const additionalPrivileges =
|
||||
identityProjectPermission.additionalPrivileges?.map(({ permissions }) => ({
|
||||
role: ProjectMembershipRole.Custom,
|
||||
permissions
|
||||
})) || [];
|
||||
|
||||
return {
|
||||
permission: buildProjectPermission(identityProjectPermission.roles),
|
||||
permission: buildProjectPermission(rolePermissions.concat(additionalPrivileges)),
|
||||
membership: identityProjectPermission,
|
||||
hasRole: (role: string) =>
|
||||
identityProjectPermission.roles.findIndex(
|
||||
@ -203,14 +256,32 @@ export const permissionServiceFactory = ({
|
||||
};
|
||||
};
|
||||
|
||||
const getServiceTokenProjectPermission = async (serviceTokenId: string, projectId: string) => {
|
||||
const getServiceTokenProjectPermission = async (
|
||||
serviceTokenId: string,
|
||||
projectId: string,
|
||||
actorOrgId: string | undefined
|
||||
) => {
|
||||
const serviceToken = await serviceTokenDAL.findById(serviceTokenId);
|
||||
if (!serviceToken) throw new BadRequestError({ message: "Service token not found" });
|
||||
|
||||
const serviceTokenProject = await projectDAL.findById(serviceToken.projectId);
|
||||
|
||||
if (!serviceTokenProject) throw new BadRequestError({ message: "Service token not linked to a project" });
|
||||
|
||||
if (serviceTokenProject.orgId !== actorOrgId) {
|
||||
throw new UnauthorizedError({ message: "Service token not a part of this organization" });
|
||||
}
|
||||
|
||||
if (serviceToken.projectId !== projectId)
|
||||
throw new UnauthorizedError({
|
||||
message: "Failed to find service authorization for given project"
|
||||
});
|
||||
|
||||
if (serviceTokenProject.orgId !== actorOrgId)
|
||||
throw new UnauthorizedError({
|
||||
message: "Failed to find service authorization for given project"
|
||||
});
|
||||
|
||||
const scopes = ServiceTokenScopes.parse(serviceToken.scopes || []);
|
||||
return {
|
||||
permission: buildServiceTokenProjectPermission(scopes, serviceToken.permissions),
|
||||
@ -238,15 +309,16 @@ export const permissionServiceFactory = ({
|
||||
type: T,
|
||||
id: string,
|
||||
projectId: string,
|
||||
actorOrgId?: string
|
||||
actorAuthMethod: ActorAuthMethod,
|
||||
actorOrgId: string | undefined
|
||||
): Promise<TProjectPermissionRT<T>> => {
|
||||
switch (type) {
|
||||
case ActorType.USER:
|
||||
return getUserProjectPermission(id, projectId, actorOrgId) as Promise<TProjectPermissionRT<T>>;
|
||||
return getUserProjectPermission(id, projectId, actorAuthMethod, actorOrgId) as Promise<TProjectPermissionRT<T>>;
|
||||
case ActorType.SERVICE:
|
||||
return getServiceTokenProjectPermission(id, projectId) as Promise<TProjectPermissionRT<T>>;
|
||||
return getServiceTokenProjectPermission(id, projectId, actorOrgId) as Promise<TProjectPermissionRT<T>>;
|
||||
case ActorType.IDENTITY:
|
||||
return getIdentityProjectPermission(id, projectId) as Promise<TProjectPermissionRT<T>>;
|
||||
return getIdentityProjectPermission(id, projectId, actorOrgId) as Promise<TProjectPermissionRT<T>>;
|
||||
default:
|
||||
throw new UnauthorizedError({
|
||||
message: "Permission not defined",
|
||||
|
@ -12,6 +12,7 @@ export enum ProjectPermissionActions {
|
||||
export enum ProjectPermissionSub {
|
||||
Role = "role",
|
||||
Member = "member",
|
||||
Groups = "groups",
|
||||
Settings = "settings",
|
||||
Integrations = "integrations",
|
||||
Webhooks = "webhooks",
|
||||
@ -41,6 +42,7 @@ export type ProjectPermissionSet =
|
||||
| [ProjectPermissionActions, ProjectPermissionSub.Role]
|
||||
| [ProjectPermissionActions, ProjectPermissionSub.Tags]
|
||||
| [ProjectPermissionActions, ProjectPermissionSub.Member]
|
||||
| [ProjectPermissionActions, ProjectPermissionSub.Groups]
|
||||
| [ProjectPermissionActions, ProjectPermissionSub.Integrations]
|
||||
| [ProjectPermissionActions, ProjectPermissionSub.Webhooks]
|
||||
| [ProjectPermissionActions, ProjectPermissionSub.AuditLogs]
|
||||
@ -82,6 +84,11 @@ const buildAdminPermissionRules = () => {
|
||||
can(ProjectPermissionActions.Edit, ProjectPermissionSub.Member);
|
||||
can(ProjectPermissionActions.Delete, ProjectPermissionSub.Member);
|
||||
|
||||
can(ProjectPermissionActions.Read, ProjectPermissionSub.Groups);
|
||||
can(ProjectPermissionActions.Create, ProjectPermissionSub.Groups);
|
||||
can(ProjectPermissionActions.Edit, ProjectPermissionSub.Groups);
|
||||
can(ProjectPermissionActions.Delete, ProjectPermissionSub.Groups);
|
||||
|
||||
can(ProjectPermissionActions.Read, ProjectPermissionSub.Role);
|
||||
can(ProjectPermissionActions.Create, ProjectPermissionSub.Role);
|
||||
can(ProjectPermissionActions.Edit, ProjectPermissionSub.Role);
|
||||
@ -157,6 +164,8 @@ const buildMemberPermissionRules = () => {
|
||||
can(ProjectPermissionActions.Read, ProjectPermissionSub.Member);
|
||||
can(ProjectPermissionActions.Create, ProjectPermissionSub.Member);
|
||||
|
||||
can(ProjectPermissionActions.Read, ProjectPermissionSub.Groups);
|
||||
|
||||
can(ProjectPermissionActions.Read, ProjectPermissionSub.Integrations);
|
||||
can(ProjectPermissionActions.Create, ProjectPermissionSub.Integrations);
|
||||
can(ProjectPermissionActions.Edit, ProjectPermissionSub.Integrations);
|
||||
@ -209,6 +218,7 @@ const buildViewerPermissionRules = () => {
|
||||
can(ProjectPermissionActions.Read, ProjectPermissionSub.SecretRollback);
|
||||
can(ProjectPermissionActions.Read, ProjectPermissionSub.SecretRotation);
|
||||
can(ProjectPermissionActions.Read, ProjectPermissionSub.Member);
|
||||
can(ProjectPermissionActions.Read, ProjectPermissionSub.Groups);
|
||||
can(ProjectPermissionActions.Read, ProjectPermissionSub.Role);
|
||||
can(ProjectPermissionActions.Read, ProjectPermissionSub.Integrations);
|
||||
can(ProjectPermissionActions.Read, ProjectPermissionSub.Webhooks);
|
||||
|
10
backend/src/ee/services/project-user-additional-privilege/project-user-additional-privilege-dal.ts
Normal file
10
backend/src/ee/services/project-user-additional-privilege/project-user-additional-privilege-dal.ts
Normal file
@ -0,0 +1,10 @@
|
||||
import { TDbClient } from "@app/db";
|
||||
import { TableName } from "@app/db/schemas";
|
||||
import { ormify } from "@app/lib/knex";
|
||||
|
||||
export type TProjectUserAdditionalPrivilegeDALFactory = ReturnType<typeof projectUserAdditionalPrivilegeDALFactory>;
|
||||
|
||||
export const projectUserAdditionalPrivilegeDALFactory = (db: TDbClient) => {
|
||||
const orm = ormify(db, TableName.ProjectUserAdditionalPrivilege);
|
||||
return orm;
|
||||
};
|
212
backend/src/ee/services/project-user-additional-privilege/project-user-additional-privilege-service.ts
Normal file
212
backend/src/ee/services/project-user-additional-privilege/project-user-additional-privilege-service.ts
Normal file
@ -0,0 +1,212 @@
|
||||
import { ForbiddenError } from "@casl/ability";
|
||||
import ms from "ms";
|
||||
|
||||
import { BadRequestError } from "@app/lib/errors";
|
||||
import { TProjectMembershipDALFactory } from "@app/services/project-membership/project-membership-dal";
|
||||
|
||||
import { TPermissionServiceFactory } from "../permission/permission-service";
|
||||
import { ProjectPermissionActions, ProjectPermissionSub } from "../permission/project-permission";
|
||||
import { TProjectUserAdditionalPrivilegeDALFactory } from "./project-user-additional-privilege-dal";
|
||||
import {
|
||||
ProjectUserAdditionalPrivilegeTemporaryMode,
|
||||
TCreateUserPrivilegeDTO,
|
||||
TDeleteUserPrivilegeDTO,
|
||||
TGetUserPrivilegeDetailsDTO,
|
||||
TListUserPrivilegesDTO,
|
||||
TUpdateUserPrivilegeDTO
|
||||
} from "./project-user-additional-privilege-types";
|
||||
|
||||
type TProjectUserAdditionalPrivilegeServiceFactoryDep = {
|
||||
projectUserAdditionalPrivilegeDAL: TProjectUserAdditionalPrivilegeDALFactory;
|
||||
projectMembershipDAL: Pick<TProjectMembershipDALFactory, "findById">;
|
||||
permissionService: Pick<TPermissionServiceFactory, "getProjectPermission">;
|
||||
};
|
||||
|
||||
export type TProjectUserAdditionalPrivilegeServiceFactory = ReturnType<
|
||||
typeof projectUserAdditionalPrivilegeServiceFactory
|
||||
>;
|
||||
|
||||
export const projectUserAdditionalPrivilegeServiceFactory = ({
|
||||
projectUserAdditionalPrivilegeDAL,
|
||||
projectMembershipDAL,
|
||||
permissionService
|
||||
}: TProjectUserAdditionalPrivilegeServiceFactoryDep) => {
|
||||
const create = async ({
|
||||
slug,
|
||||
actor,
|
||||
actorId,
|
||||
permissions: customPermission,
|
||||
actorOrgId,
|
||||
actorAuthMethod,
|
||||
projectMembershipId,
|
||||
...dto
|
||||
}: TCreateUserPrivilegeDTO) => {
|
||||
const projectMembership = await projectMembershipDAL.findById(projectMembershipId);
|
||||
if (!projectMembership) throw new BadRequestError({ message: "Project membership not found" });
|
||||
|
||||
const { permission } = await permissionService.getProjectPermission(
|
||||
actor,
|
||||
actorId,
|
||||
projectMembership.projectId,
|
||||
actorAuthMethod,
|
||||
actorOrgId
|
||||
);
|
||||
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Edit, ProjectPermissionSub.Member);
|
||||
|
||||
const existingSlug = await projectUserAdditionalPrivilegeDAL.findOne({ slug, projectMembershipId });
|
||||
if (existingSlug) throw new BadRequestError({ message: "Additional privilege of provided slug exist" });
|
||||
|
||||
if (!dto.isTemporary) {
|
||||
const additionalPrivilege = await projectUserAdditionalPrivilegeDAL.create({
|
||||
projectMembershipId,
|
||||
slug,
|
||||
permissions: customPermission
|
||||
});
|
||||
return additionalPrivilege;
|
||||
}
|
||||
|
||||
const relativeTempAllocatedTimeInMs = ms(dto.temporaryRange);
|
||||
const additionalPrivilege = await projectUserAdditionalPrivilegeDAL.create({
|
||||
projectMembershipId,
|
||||
slug,
|
||||
permissions: customPermission,
|
||||
isTemporary: true,
|
||||
temporaryMode: ProjectUserAdditionalPrivilegeTemporaryMode.Relative,
|
||||
temporaryRange: dto.temporaryRange,
|
||||
temporaryAccessStartTime: new Date(dto.temporaryAccessStartTime),
|
||||
temporaryAccessEndTime: new Date(new Date(dto.temporaryAccessStartTime).getTime() + relativeTempAllocatedTimeInMs)
|
||||
});
|
||||
return additionalPrivilege;
|
||||
};
|
||||
|
||||
const updateById = async ({
|
||||
privilegeId,
|
||||
actorOrgId,
|
||||
actor,
|
||||
actorId,
|
||||
actorAuthMethod,
|
||||
...dto
|
||||
}: TUpdateUserPrivilegeDTO) => {
|
||||
const userPrivilege = await projectUserAdditionalPrivilegeDAL.findById(privilegeId);
|
||||
if (!userPrivilege) throw new BadRequestError({ message: "User additional privilege not found" });
|
||||
|
||||
const projectMembership = await projectMembershipDAL.findById(userPrivilege.projectMembershipId);
|
||||
if (!projectMembership) throw new BadRequestError({ message: "Project membership not found" });
|
||||
|
||||
const { permission } = await permissionService.getProjectPermission(
|
||||
actor,
|
||||
actorId,
|
||||
projectMembership.projectId,
|
||||
actorAuthMethod,
|
||||
actorOrgId
|
||||
);
|
||||
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Edit, ProjectPermissionSub.Member);
|
||||
|
||||
if (dto?.slug) {
|
||||
const existingSlug = await projectUserAdditionalPrivilegeDAL.findOne({
|
||||
slug: dto.slug,
|
||||
projectMembershipId: projectMembership.id
|
||||
});
|
||||
if (existingSlug && existingSlug.id !== userPrivilege.id)
|
||||
throw new BadRequestError({ message: "Additional privilege of provided slug exist" });
|
||||
}
|
||||
|
||||
const isTemporary = typeof dto?.isTemporary !== "undefined" ? dto.isTemporary : userPrivilege.isTemporary;
|
||||
if (isTemporary) {
|
||||
const temporaryAccessStartTime = dto?.temporaryAccessStartTime || userPrivilege?.temporaryAccessStartTime;
|
||||
const temporaryRange = dto?.temporaryRange || userPrivilege?.temporaryRange;
|
||||
const additionalPrivilege = await projectUserAdditionalPrivilegeDAL.updateById(userPrivilege.id, {
|
||||
...dto,
|
||||
temporaryAccessStartTime: new Date(temporaryAccessStartTime || ""),
|
||||
temporaryAccessEndTime: new Date(new Date(temporaryAccessStartTime || "").getTime() + ms(temporaryRange || ""))
|
||||
});
|
||||
return additionalPrivilege;
|
||||
}
|
||||
|
||||
const additionalPrivilege = await projectUserAdditionalPrivilegeDAL.updateById(userPrivilege.id, {
|
||||
...dto,
|
||||
isTemporary: false,
|
||||
temporaryAccessStartTime: null,
|
||||
temporaryAccessEndTime: null,
|
||||
temporaryRange: null,
|
||||
temporaryMode: null
|
||||
});
|
||||
return additionalPrivilege;
|
||||
};
|
||||
|
||||
const deleteById = async ({ actorId, actor, actorOrgId, actorAuthMethod, privilegeId }: TDeleteUserPrivilegeDTO) => {
|
||||
const userPrivilege = await projectUserAdditionalPrivilegeDAL.findById(privilegeId);
|
||||
if (!userPrivilege) throw new BadRequestError({ message: "User additional privilege not found" });
|
||||
|
||||
const projectMembership = await projectMembershipDAL.findById(userPrivilege.projectMembershipId);
|
||||
if (!projectMembership) throw new BadRequestError({ message: "Project membership not found" });
|
||||
|
||||
const { permission } = await permissionService.getProjectPermission(
|
||||
actor,
|
||||
actorId,
|
||||
projectMembership.projectId,
|
||||
actorAuthMethod,
|
||||
actorOrgId
|
||||
);
|
||||
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Edit, ProjectPermissionSub.Member);
|
||||
|
||||
const deletedPrivilege = await projectUserAdditionalPrivilegeDAL.deleteById(userPrivilege.id);
|
||||
return deletedPrivilege;
|
||||
};
|
||||
|
||||
const getPrivilegeDetailsById = async ({
|
||||
privilegeId,
|
||||
actorOrgId,
|
||||
actor,
|
||||
actorId,
|
||||
actorAuthMethod
|
||||
}: TGetUserPrivilegeDetailsDTO) => {
|
||||
const userPrivilege = await projectUserAdditionalPrivilegeDAL.findById(privilegeId);
|
||||
if (!userPrivilege) throw new BadRequestError({ message: "User additional privilege not found" });
|
||||
|
||||
const projectMembership = await projectMembershipDAL.findById(userPrivilege.projectMembershipId);
|
||||
if (!projectMembership) throw new BadRequestError({ message: "Project membership not found" });
|
||||
|
||||
const { permission } = await permissionService.getProjectPermission(
|
||||
actor,
|
||||
actorId,
|
||||
projectMembership.projectId,
|
||||
actorAuthMethod,
|
||||
actorOrgId
|
||||
);
|
||||
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.Member);
|
||||
|
||||
return userPrivilege;
|
||||
};
|
||||
|
||||
const listPrivileges = async ({
|
||||
projectMembershipId,
|
||||
actorOrgId,
|
||||
actor,
|
||||
actorId,
|
||||
actorAuthMethod
|
||||
}: TListUserPrivilegesDTO) => {
|
||||
const projectMembership = await projectMembershipDAL.findById(projectMembershipId);
|
||||
if (!projectMembership) throw new BadRequestError({ message: "Project membership not found" });
|
||||
|
||||
const { permission } = await permissionService.getProjectPermission(
|
||||
actor,
|
||||
actorId,
|
||||
projectMembership.projectId,
|
||||
actorAuthMethod,
|
||||
actorOrgId
|
||||
);
|
||||
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.Member);
|
||||
|
||||
const userPrivileges = await projectUserAdditionalPrivilegeDAL.find({ projectMembershipId });
|
||||
return userPrivileges;
|
||||
};
|
||||
|
||||
return {
|
||||
create,
|
||||
updateById,
|
||||
deleteById,
|
||||
getPrivilegeDetailsById,
|
||||
listPrivileges
|
||||
};
|
||||
};
|
40
backend/src/ee/services/project-user-additional-privilege/project-user-additional-privilege-types.ts
Normal file
40
backend/src/ee/services/project-user-additional-privilege/project-user-additional-privilege-types.ts
Normal file
@ -0,0 +1,40 @@
|
||||
import { TProjectPermission } from "@app/lib/types";
|
||||
|
||||
export enum ProjectUserAdditionalPrivilegeTemporaryMode {
|
||||
Relative = "relative"
|
||||
}
|
||||
|
||||
export type TCreateUserPrivilegeDTO = (
|
||||
| {
|
||||
permissions: unknown;
|
||||
projectMembershipId: string;
|
||||
slug: string;
|
||||
isTemporary: false;
|
||||
}
|
||||
| {
|
||||
permissions: unknown;
|
||||
projectMembershipId: string;
|
||||
slug: string;
|
||||
isTemporary: true;
|
||||
temporaryMode: ProjectUserAdditionalPrivilegeTemporaryMode.Relative;
|
||||
temporaryRange: string;
|
||||
temporaryAccessStartTime: string;
|
||||
}
|
||||
) &
|
||||
Omit<TProjectPermission, "projectId">;
|
||||
|
||||
export type TUpdateUserPrivilegeDTO = { privilegeId: string } & Omit<TProjectPermission, "projectId"> &
|
||||
Partial<{
|
||||
permissions: unknown;
|
||||
slug: string;
|
||||
isTemporary: boolean;
|
||||
temporaryMode: ProjectUserAdditionalPrivilegeTemporaryMode.Relative;
|
||||
temporaryRange: string;
|
||||
temporaryAccessStartTime: string;
|
||||
}>;
|
||||
|
||||
export type TDeleteUserPrivilegeDTO = Omit<TProjectPermission, "projectId"> & { privilegeId: string };
|
||||
|
||||
export type TGetUserPrivilegeDetailsDTO = Omit<TProjectPermission, "projectId"> & { privilegeId: string };
|
||||
|
||||
export type TListUserPrivilegesDTO = Omit<TProjectPermission, "projectId"> & { projectMembershipId: string };
|
@ -55,6 +55,7 @@ export const samlConfigServiceFactory = ({
|
||||
const createSamlCfg = async ({
|
||||
cert,
|
||||
actor,
|
||||
actorAuthMethod,
|
||||
actorOrgId,
|
||||
orgId,
|
||||
issuer,
|
||||
@ -63,7 +64,7 @@ export const samlConfigServiceFactory = ({
|
||||
entryPoint,
|
||||
authProvider
|
||||
}: TCreateSamlCfgDTO) => {
|
||||
const { permission } = await permissionService.getOrgPermission(actor, actorId, orgId, actorOrgId);
|
||||
const { permission } = await permissionService.getOrgPermission(actor, actorId, orgId, actorAuthMethod, actorOrgId);
|
||||
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Create, OrgPermissionSubjects.Sso);
|
||||
|
||||
const plan = await licenseService.getPlan(orgId);
|
||||
@ -146,6 +147,7 @@ export const samlConfigServiceFactory = ({
|
||||
orgId,
|
||||
actor,
|
||||
actorOrgId,
|
||||
actorAuthMethod,
|
||||
cert,
|
||||
actorId,
|
||||
issuer,
|
||||
@ -153,7 +155,7 @@ export const samlConfigServiceFactory = ({
|
||||
entryPoint,
|
||||
authProvider
|
||||
}: TUpdateSamlCfgDTO) => {
|
||||
const { permission } = await permissionService.getOrgPermission(actor, actorId, orgId, actorOrgId);
|
||||
const { permission } = await permissionService.getOrgPermission(actor, actorId, orgId, actorAuthMethod, actorOrgId);
|
||||
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Edit, OrgPermissionSubjects.Sso);
|
||||
const plan = await licenseService.getPlan(orgId);
|
||||
if (!plan.samlSSO)
|
||||
@ -238,6 +240,7 @@ export const samlConfigServiceFactory = ({
|
||||
dto.actor,
|
||||
dto.actorId,
|
||||
ssoConfig.orgId,
|
||||
dto.actorAuthMethod,
|
||||
dto.actorOrgId
|
||||
);
|
||||
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Read, OrgPermissionSubjects.Sso);
|
||||
@ -316,6 +319,11 @@ export const samlConfigServiceFactory = ({
|
||||
const organization = await orgDAL.findOrgById(orgId);
|
||||
if (!organization) throw new BadRequestError({ message: "Org not found" });
|
||||
|
||||
// TODO(dangtony98): remove this after aliases update
|
||||
if (authProvider === AuthMethod.KEYCLOAK_SAML && appCfg.LICENSE_SERVER_KEY) {
|
||||
throw new BadRequestError({ message: "Keycloak SAML is not yet available on Infisical Cloud" });
|
||||
}
|
||||
|
||||
if (user) {
|
||||
await userDAL.transaction(async (tx) => {
|
||||
const [orgMembership] = await orgDAL.findMembership(
|
||||
|
@ -1,11 +1,12 @@
|
||||
import { TOrgPermission } from "@app/lib/types";
|
||||
import { ActorType } from "@app/services/auth/auth-type";
|
||||
import { ActorAuthMethod, ActorType } from "@app/services/auth/auth-type";
|
||||
|
||||
export enum SamlProviders {
|
||||
OKTA_SAML = "okta-saml",
|
||||
AZURE_SAML = "azure-saml",
|
||||
JUMPCLOUD_SAML = "jumpcloud-saml",
|
||||
GOOGLE_SAML = "google-saml"
|
||||
GOOGLE_SAML = "google-saml",
|
||||
KEYCLOAK_SAML = "keycloak-saml"
|
||||
}
|
||||
|
||||
export type TCreateSamlCfgDTO = {
|
||||
@ -26,7 +27,14 @@ export type TUpdateSamlCfgDTO = Partial<{
|
||||
TOrgPermission;
|
||||
|
||||
export type TGetSamlCfgDTO =
|
||||
| { type: "org"; orgId: string; actor: ActorType; actorId: string; actorOrgId?: string }
|
||||
| {
|
||||
type: "org";
|
||||
orgId: string;
|
||||
actor: ActorType;
|
||||
actorId: string;
|
||||
actorAuthMethod: ActorAuthMethod;
|
||||
actorOrgId: string | undefined;
|
||||
}
|
||||
| {
|
||||
type: "orgSlug";
|
||||
orgSlug: string;
|
||||
|
@ -1,4 +1,4 @@
|
||||
import { TListScimUsers, TScimUser } from "./scim-types";
|
||||
import { TListScimGroups, TListScimUsers, TScimGroup, TScimUser } from "./scim-types";
|
||||
|
||||
export const buildScimUserList = ({
|
||||
scimUsers,
|
||||
@ -62,3 +62,47 @@ export const buildScimUser = ({
|
||||
|
||||
return scimUser;
|
||||
};
|
||||
|
||||
export const buildScimGroupList = ({
|
||||
scimGroups,
|
||||
offset,
|
||||
limit
|
||||
}: {
|
||||
scimGroups: TScimGroup[];
|
||||
offset: number;
|
||||
limit: number;
|
||||
}): TListScimGroups => {
|
||||
return {
|
||||
Resources: scimGroups,
|
||||
itemsPerPage: limit,
|
||||
schemas: ["urn:ietf:params:scim:api:messages:2.0:ListResponse"],
|
||||
startIndex: offset,
|
||||
totalResults: scimGroups.length
|
||||
};
|
||||
};
|
||||
|
||||
export const buildScimGroup = ({
|
||||
groupId,
|
||||
name,
|
||||
members
|
||||
}: {
|
||||
groupId: string;
|
||||
name: string;
|
||||
members: {
|
||||
value: string;
|
||||
display: string;
|
||||
}[];
|
||||
}): TScimGroup => {
|
||||
const scimGroup = {
|
||||
schemas: ["urn:ietf:params:scim:schemas:core:2.0:Group"],
|
||||
id: groupId,
|
||||
displayName: name,
|
||||
members,
|
||||
meta: {
|
||||
resourceType: "Group",
|
||||
location: null
|
||||
}
|
||||
};
|
||||
|
||||
return scimGroup;
|
||||
};
|
||||
|
@ -1,10 +1,13 @@
|
||||
import { ForbiddenError } from "@casl/ability";
|
||||
import slugify from "@sindresorhus/slugify";
|
||||
import jwt from "jsonwebtoken";
|
||||
|
||||
import { OrgMembershipRole, OrgMembershipStatus, TableName } from "@app/db/schemas";
|
||||
import { OrgMembershipRole, OrgMembershipStatus, TableName, TGroups } from "@app/db/schemas";
|
||||
import { TGroupDALFactory } from "@app/ee/services/group/group-dal";
|
||||
import { TScimDALFactory } from "@app/ee/services/scim/scim-dal";
|
||||
import { getConfig } from "@app/lib/config/env";
|
||||
import { BadRequestError, ScimRequestError, UnauthorizedError } from "@app/lib/errors";
|
||||
import { alphaNumericNanoId } from "@app/lib/nanoid";
|
||||
import { TOrgPermission } from "@app/lib/types";
|
||||
import { AuthMethod, AuthTokenType } from "@app/services/auth/auth-type";
|
||||
import { TOrgDALFactory } from "@app/services/org/org-dal";
|
||||
@ -17,16 +20,23 @@ import { TUserDALFactory } from "@app/services/user/user-dal";
|
||||
import { TLicenseServiceFactory } from "../license/license-service";
|
||||
import { OrgPermissionActions, OrgPermissionSubjects } from "../permission/org-permission";
|
||||
import { TPermissionServiceFactory } from "../permission/permission-service";
|
||||
import { buildScimUser, buildScimUserList } from "./scim-fns";
|
||||
import { buildScimGroup, buildScimGroupList, buildScimUser, buildScimUserList } from "./scim-fns";
|
||||
import {
|
||||
TCreateScimGroupDTO,
|
||||
TCreateScimTokenDTO,
|
||||
TCreateScimUserDTO,
|
||||
TDeleteScimGroupDTO,
|
||||
TDeleteScimTokenDTO,
|
||||
TDeleteScimUserDTO,
|
||||
TGetScimGroupDTO,
|
||||
TGetScimUserDTO,
|
||||
TListScimGroupsDTO,
|
||||
TListScimUsers,
|
||||
TListScimUsersDTO,
|
||||
TReplaceScimUserDTO,
|
||||
TScimTokenJwtPayload,
|
||||
TUpdateScimGroupNamePatchDTO,
|
||||
TUpdateScimGroupNamePutDTO,
|
||||
TUpdateScimUserDTO
|
||||
} from "./scim-types";
|
||||
|
||||
@ -39,6 +49,7 @@ type TScimServiceFactoryDep = {
|
||||
>;
|
||||
projectDAL: Pick<TProjectDALFactory, "find">;
|
||||
projectMembershipDAL: Pick<TProjectMembershipDALFactory, "find" | "delete">;
|
||||
groupDAL: Pick<TGroupDALFactory, "create" | "findOne" | "findAllGroupMembers" | "update" | "delete" | "findGroups">;
|
||||
licenseService: Pick<TLicenseServiceFactory, "getPlan">;
|
||||
permissionService: Pick<TPermissionServiceFactory, "getOrgPermission">;
|
||||
smtpService: TSmtpService;
|
||||
@ -53,11 +64,20 @@ export const scimServiceFactory = ({
|
||||
orgDAL,
|
||||
projectDAL,
|
||||
projectMembershipDAL,
|
||||
groupDAL,
|
||||
permissionService,
|
||||
smtpService
|
||||
}: TScimServiceFactoryDep) => {
|
||||
const createScimToken = async ({ actor, actorId, actorOrgId, orgId, description, ttlDays }: TCreateScimTokenDTO) => {
|
||||
const { permission } = await permissionService.getOrgPermission(actor, actorId, orgId, actorOrgId);
|
||||
const createScimToken = async ({
|
||||
actor,
|
||||
actorId,
|
||||
actorOrgId,
|
||||
actorAuthMethod,
|
||||
orgId,
|
||||
description,
|
||||
ttlDays
|
||||
}: TCreateScimTokenDTO) => {
|
||||
const { permission } = await permissionService.getOrgPermission(actor, actorId, orgId, actorAuthMethod, actorOrgId);
|
||||
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Create, OrgPermissionSubjects.Scim);
|
||||
|
||||
const plan = await licenseService.getPlan(orgId);
|
||||
@ -85,8 +105,8 @@ export const scimServiceFactory = ({
|
||||
return { scimToken };
|
||||
};
|
||||
|
||||
const listScimTokens = async ({ actor, actorId, actorOrgId, orgId }: TOrgPermission) => {
|
||||
const { permission } = await permissionService.getOrgPermission(actor, actorId, orgId, actorOrgId);
|
||||
const listScimTokens = async ({ actor, actorId, actorOrgId, actorAuthMethod, orgId }: TOrgPermission) => {
|
||||
const { permission } = await permissionService.getOrgPermission(actor, actorId, orgId, actorAuthMethod, actorOrgId);
|
||||
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Read, OrgPermissionSubjects.Scim);
|
||||
|
||||
const plan = await licenseService.getPlan(orgId);
|
||||
@ -99,11 +119,17 @@ export const scimServiceFactory = ({
|
||||
return scimTokens;
|
||||
};
|
||||
|
||||
const deleteScimToken = async ({ scimTokenId, actor, actorId, actorOrgId }: TDeleteScimTokenDTO) => {
|
||||
const deleteScimToken = async ({ scimTokenId, actor, actorId, actorAuthMethod, actorOrgId }: TDeleteScimTokenDTO) => {
|
||||
let scimToken = await scimDAL.findById(scimTokenId);
|
||||
if (!scimToken) throw new BadRequestError({ message: "Failed to find SCIM token to delete" });
|
||||
|
||||
const { permission } = await permissionService.getOrgPermission(actor, actorId, scimToken.orgId, actorOrgId);
|
||||
const { permission } = await permissionService.getOrgPermission(
|
||||
actor,
|
||||
actorId,
|
||||
scimToken.orgId,
|
||||
actorAuthMethod,
|
||||
actorOrgId
|
||||
);
|
||||
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Delete, OrgPermissionSubjects.Scim);
|
||||
|
||||
const plan = await licenseService.getPlan(scimToken.orgId);
|
||||
@ -409,6 +435,221 @@ export const scimServiceFactory = ({
|
||||
});
|
||||
};
|
||||
|
||||
const deleteScimUser = async ({ userId, orgId }: TDeleteScimUserDTO) => {
|
||||
const [membership] = await orgDAL
|
||||
.findMembership({
|
||||
userId,
|
||||
[`${TableName.OrgMembership}.orgId` as "id"]: orgId
|
||||
})
|
||||
.catch(() => {
|
||||
throw new ScimRequestError({
|
||||
detail: "User not found",
|
||||
status: 404
|
||||
});
|
||||
});
|
||||
|
||||
if (!membership)
|
||||
throw new ScimRequestError({
|
||||
detail: "User not found",
|
||||
status: 404
|
||||
});
|
||||
|
||||
if (!membership.scimEnabled) {
|
||||
throw new ScimRequestError({
|
||||
detail: "SCIM is disabled for the organization",
|
||||
status: 403
|
||||
});
|
||||
}
|
||||
|
||||
await deleteOrgMembership({
|
||||
orgMembershipId: membership.id,
|
||||
orgId: membership.orgId,
|
||||
orgDAL,
|
||||
projectDAL,
|
||||
projectMembershipDAL
|
||||
});
|
||||
|
||||
return {}; // intentionally return empty object upon success
|
||||
};
|
||||
|
||||
const listScimGroups = async ({ orgId, offset, limit }: TListScimGroupsDTO) => {
|
||||
const org = await orgDAL.findById(orgId);
|
||||
|
||||
if (!org.scimEnabled)
|
||||
throw new ScimRequestError({
|
||||
detail: "SCIM is disabled for the organization",
|
||||
status: 403
|
||||
});
|
||||
|
||||
const groups = await groupDAL.findGroups({
|
||||
orgId
|
||||
});
|
||||
|
||||
const scimGroups = groups.map((group) =>
|
||||
buildScimGroup({
|
||||
groupId: group.id,
|
||||
name: group.name,
|
||||
members: []
|
||||
})
|
||||
);
|
||||
|
||||
return buildScimGroupList({
|
||||
scimGroups,
|
||||
offset,
|
||||
limit
|
||||
});
|
||||
};
|
||||
|
||||
const createScimGroup = async ({ displayName, orgId }: TCreateScimGroupDTO) => {
|
||||
const org = await orgDAL.findById(orgId);
|
||||
|
||||
if (!org.scimEnabled)
|
||||
throw new ScimRequestError({
|
||||
detail: "SCIM is disabled for the organization",
|
||||
status: 403
|
||||
});
|
||||
|
||||
const group = await groupDAL.create({
|
||||
name: displayName,
|
||||
slug: slugify(`${displayName}-${alphaNumericNanoId(4)}`),
|
||||
orgId,
|
||||
role: OrgMembershipRole.NoAccess
|
||||
});
|
||||
|
||||
return buildScimGroup({
|
||||
groupId: group.id,
|
||||
name: group.name,
|
||||
members: []
|
||||
});
|
||||
};
|
||||
|
||||
const getScimGroup = async ({ groupId, orgId }: TGetScimGroupDTO) => {
|
||||
const group = await groupDAL.findOne({
|
||||
id: groupId,
|
||||
orgId
|
||||
});
|
||||
|
||||
if (!group) {
|
||||
throw new ScimRequestError({
|
||||
detail: "Group Not Found",
|
||||
status: 404
|
||||
});
|
||||
}
|
||||
|
||||
const users = await groupDAL.findAllGroupMembers({
|
||||
orgId: group.orgId,
|
||||
groupId: group.id
|
||||
});
|
||||
|
||||
return buildScimGroup({
|
||||
groupId: group.id,
|
||||
name: group.name,
|
||||
members: users
|
||||
.filter((user) => user.isPartOfGroup)
|
||||
.map((user) => ({
|
||||
value: user.id,
|
||||
display: `${user.firstName} ${user.lastName}`
|
||||
}))
|
||||
});
|
||||
};
|
||||
|
||||
const updateScimGroupNamePut = async ({ groupId, orgId, displayName }: TUpdateScimGroupNamePutDTO) => {
|
||||
const [group] = await groupDAL.update(
|
||||
{
|
||||
id: groupId,
|
||||
orgId
|
||||
},
|
||||
{
|
||||
name: displayName
|
||||
}
|
||||
);
|
||||
|
||||
if (!group) {
|
||||
throw new ScimRequestError({
|
||||
detail: "Group Not Found",
|
||||
status: 404
|
||||
});
|
||||
}
|
||||
|
||||
return buildScimGroup({
|
||||
groupId: group.id,
|
||||
name: group.name,
|
||||
members: []
|
||||
});
|
||||
};
|
||||
|
||||
// TODO: add support for add/remove op
|
||||
const updateScimGroupNamePatch = async ({ groupId, orgId, operations }: TUpdateScimGroupNamePatchDTO) => {
|
||||
const org = await orgDAL.findById(orgId);
|
||||
|
||||
if (!org.scimEnabled)
|
||||
throw new ScimRequestError({
|
||||
detail: "SCIM is disabled for the organization",
|
||||
status: 403
|
||||
});
|
||||
|
||||
let group: TGroups | undefined;
|
||||
for await (const operation of operations) {
|
||||
switch (operation.op) {
|
||||
case "replace": {
|
||||
await groupDAL.update(
|
||||
{
|
||||
id: groupId,
|
||||
orgId
|
||||
},
|
||||
{
|
||||
name: operation.value.displayName
|
||||
}
|
||||
);
|
||||
break;
|
||||
}
|
||||
case "add": {
|
||||
// TODO
|
||||
break;
|
||||
}
|
||||
case "remove": {
|
||||
// TODO
|
||||
break;
|
||||
}
|
||||
default: {
|
||||
throw new ScimRequestError({
|
||||
detail: "Invalid Operation",
|
||||
status: 400
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!group) {
|
||||
throw new ScimRequestError({
|
||||
detail: "Group Not Found",
|
||||
status: 404
|
||||
});
|
||||
}
|
||||
|
||||
return buildScimGroup({
|
||||
groupId: group.id,
|
||||
name: group.name,
|
||||
members: []
|
||||
});
|
||||
};
|
||||
|
||||
const deleteScimGroup = async ({ groupId, orgId }: TDeleteScimGroupDTO) => {
|
||||
const [group] = await groupDAL.delete({
|
||||
id: groupId,
|
||||
orgId
|
||||
});
|
||||
|
||||
if (!group) {
|
||||
throw new ScimRequestError({
|
||||
detail: "Group Not Found",
|
||||
status: 404
|
||||
});
|
||||
}
|
||||
|
||||
return {}; // intentionally return empty object upon success
|
||||
};
|
||||
|
||||
const fnValidateScimToken = async (token: TScimTokenJwtPayload) => {
|
||||
const scimToken = await scimDAL.findById(token.scimTokenId);
|
||||
if (!scimToken) throw new UnauthorizedError();
|
||||
@ -441,6 +682,13 @@ export const scimServiceFactory = ({
|
||||
createScimUser,
|
||||
updateScimUser,
|
||||
replaceScimUser,
|
||||
deleteScimUser,
|
||||
listScimGroups,
|
||||
createScimGroup,
|
||||
getScimGroup,
|
||||
deleteScimGroup,
|
||||
updateScimGroupNamePut,
|
||||
updateScimGroupNamePatch,
|
||||
fnValidateScimToken
|
||||
};
|
||||
};
|
||||
|
@ -59,6 +59,73 @@ export type TReplaceScimUserDTO = {
|
||||
orgId: string;
|
||||
};
|
||||
|
||||
export type TDeleteScimUserDTO = {
|
||||
userId: string;
|
||||
orgId: string;
|
||||
};
|
||||
|
||||
export type TListScimGroupsDTO = {
|
||||
offset: number;
|
||||
limit: number;
|
||||
orgId: string;
|
||||
};
|
||||
|
||||
export type TListScimGroups = {
|
||||
schemas: ["urn:ietf:params:scim:api:messages:2.0:ListResponse"];
|
||||
totalResults: number;
|
||||
Resources: TScimGroup[];
|
||||
itemsPerPage: number;
|
||||
startIndex: number;
|
||||
};
|
||||
|
||||
export type TCreateScimGroupDTO = {
|
||||
displayName: string;
|
||||
orgId: string;
|
||||
};
|
||||
|
||||
export type TGetScimGroupDTO = {
|
||||
groupId: string;
|
||||
orgId: string;
|
||||
};
|
||||
|
||||
export type TUpdateScimGroupNamePutDTO = {
|
||||
groupId: string;
|
||||
orgId: string;
|
||||
displayName: string;
|
||||
};
|
||||
|
||||
export type TUpdateScimGroupNamePatchDTO = {
|
||||
groupId: string;
|
||||
orgId: string;
|
||||
operations: (TRemoveOp | TReplaceOp | TAddOp)[];
|
||||
};
|
||||
|
||||
type TReplaceOp = {
|
||||
op: "replace";
|
||||
value: {
|
||||
id: string;
|
||||
displayName: string;
|
||||
};
|
||||
};
|
||||
|
||||
type TRemoveOp = {
|
||||
op: "remove";
|
||||
path: string;
|
||||
};
|
||||
|
||||
type TAddOp = {
|
||||
op: "add";
|
||||
value: {
|
||||
value: string;
|
||||
display?: string;
|
||||
};
|
||||
};
|
||||
|
||||
export type TDeleteScimGroupDTO = {
|
||||
groupId: string;
|
||||
orgId: string;
|
||||
};
|
||||
|
||||
export type TScimTokenJwtPayload = {
|
||||
scimTokenId: string;
|
||||
authTokenType: string;
|
||||
@ -86,3 +153,17 @@ export type TScimUser = {
|
||||
location: null;
|
||||
};
|
||||
};
|
||||
|
||||
export type TScimGroup = {
|
||||
schemas: string[];
|
||||
id: string;
|
||||
displayName: string;
|
||||
members: {
|
||||
value: string;
|
||||
display: string;
|
||||
}[];
|
||||
meta: {
|
||||
resourceType: string;
|
||||
location: null;
|
||||
};
|
||||
};
|
||||
|
@ -45,6 +45,7 @@ export const secretApprovalPolicyServiceFactory = ({
|
||||
actor,
|
||||
actorId,
|
||||
actorOrgId,
|
||||
actorAuthMethod,
|
||||
approvals,
|
||||
approvers,
|
||||
projectId,
|
||||
@ -54,7 +55,13 @@ export const secretApprovalPolicyServiceFactory = ({
|
||||
if (approvals > approvers.length)
|
||||
throw new BadRequestError({ message: "Approvals cannot be greater than approvers" });
|
||||
|
||||
const { permission } = await permissionService.getProjectPermission(actor, actorId, projectId, actorOrgId);
|
||||
const { permission } = await permissionService.getProjectPermission(
|
||||
actor,
|
||||
actorId,
|
||||
projectId,
|
||||
actorAuthMethod,
|
||||
actorOrgId
|
||||
);
|
||||
ForbiddenError.from(permission).throwUnlessCan(
|
||||
ProjectPermissionActions.Create,
|
||||
ProjectPermissionSub.SecretApproval
|
||||
@ -98,6 +105,7 @@ export const secretApprovalPolicyServiceFactory = ({
|
||||
actorId,
|
||||
actor,
|
||||
actorOrgId,
|
||||
actorAuthMethod,
|
||||
approvals,
|
||||
secretPolicyId
|
||||
}: TUpdateSapDTO) => {
|
||||
@ -108,6 +116,7 @@ export const secretApprovalPolicyServiceFactory = ({
|
||||
actor,
|
||||
actorId,
|
||||
secretApprovalPolicy.projectId,
|
||||
actorAuthMethod,
|
||||
actorOrgId
|
||||
);
|
||||
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Edit, ProjectPermissionSub.SecretApproval);
|
||||
@ -152,7 +161,13 @@ export const secretApprovalPolicyServiceFactory = ({
|
||||
};
|
||||
};
|
||||
|
||||
const deleteSecretApprovalPolicy = async ({ secretPolicyId, actor, actorId, actorOrgId }: TDeleteSapDTO) => {
|
||||
const deleteSecretApprovalPolicy = async ({
|
||||
secretPolicyId,
|
||||
actor,
|
||||
actorId,
|
||||
actorAuthMethod,
|
||||
actorOrgId
|
||||
}: TDeleteSapDTO) => {
|
||||
const sapPolicy = await secretApprovalPolicyDAL.findById(secretPolicyId);
|
||||
if (!sapPolicy) throw new BadRequestError({ message: "Secret approval policy not found" });
|
||||
|
||||
@ -160,6 +175,7 @@ export const secretApprovalPolicyServiceFactory = ({
|
||||
actor,
|
||||
actorId,
|
||||
sapPolicy.projectId,
|
||||
actorAuthMethod,
|
||||
actorOrgId
|
||||
);
|
||||
ForbiddenError.from(permission).throwUnlessCan(
|
||||
@ -171,8 +187,20 @@ export const secretApprovalPolicyServiceFactory = ({
|
||||
return sapPolicy;
|
||||
};
|
||||
|
||||
const getSecretApprovalPolicyByProjectId = async ({ actorId, actor, actorOrgId, projectId }: TListSapDTO) => {
|
||||
const { permission } = await permissionService.getProjectPermission(actor, actorId, projectId, actorOrgId);
|
||||
const getSecretApprovalPolicyByProjectId = async ({
|
||||
actorId,
|
||||
actor,
|
||||
actorOrgId,
|
||||
actorAuthMethod,
|
||||
projectId
|
||||
}: TListSapDTO) => {
|
||||
const { permission } = await permissionService.getProjectPermission(
|
||||
actor,
|
||||
actorId,
|
||||
projectId,
|
||||
actorAuthMethod,
|
||||
actorOrgId
|
||||
);
|
||||
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.SecretApproval);
|
||||
|
||||
const sapPolicies = await secretApprovalPolicyDAL.find({ projectId });
|
||||
@ -201,10 +229,17 @@ export const secretApprovalPolicyServiceFactory = ({
|
||||
actor,
|
||||
actorId,
|
||||
actorOrgId,
|
||||
actorAuthMethod,
|
||||
environment,
|
||||
secretPath
|
||||
}: TGetBoardSapDTO) => {
|
||||
const { permission } = await permissionService.getProjectPermission(actor, actorId, projectId, actorOrgId);
|
||||
const { permission } = await permissionService.getProjectPermission(
|
||||
actor,
|
||||
actorId,
|
||||
projectId,
|
||||
actorAuthMethod,
|
||||
actorOrgId
|
||||
);
|
||||
ForbiddenError.from(permission).throwUnlessCan(
|
||||
ProjectPermissionActions.Read,
|
||||
subject(ProjectPermissionSub.Secrets, { secretPath, environment })
|
||||
|
@ -82,13 +82,14 @@ export const secretApprovalRequestServiceFactory = ({
|
||||
secretVersionDAL,
|
||||
secretQueueService
|
||||
}: TSecretApprovalRequestServiceFactoryDep) => {
|
||||
const requestCount = async ({ projectId, actor, actorId, actorOrgId }: TApprovalRequestCountDTO) => {
|
||||
const requestCount = async ({ projectId, actor, actorId, actorOrgId, actorAuthMethod }: TApprovalRequestCountDTO) => {
|
||||
if (actor === ActorType.SERVICE) throw new BadRequestError({ message: "Cannot use service token" });
|
||||
|
||||
const { membership } = await permissionService.getProjectPermission(
|
||||
actor as ActorType.USER,
|
||||
actorId,
|
||||
projectId,
|
||||
actorAuthMethod,
|
||||
actorOrgId
|
||||
);
|
||||
|
||||
@ -100,6 +101,7 @@ export const secretApprovalRequestServiceFactory = ({
|
||||
projectId,
|
||||
actorId,
|
||||
actor,
|
||||
actorAuthMethod,
|
||||
actorOrgId,
|
||||
status,
|
||||
environment,
|
||||
@ -109,7 +111,13 @@ export const secretApprovalRequestServiceFactory = ({
|
||||
}: TListApprovalsDTO) => {
|
||||
if (actor === ActorType.SERVICE) throw new BadRequestError({ message: "Cannot use service token" });
|
||||
|
||||
const { membership } = await permissionService.getProjectPermission(actor, actorId, projectId, actorOrgId);
|
||||
const { membership } = await permissionService.getProjectPermission(
|
||||
actor,
|
||||
actorId,
|
||||
projectId,
|
||||
actorAuthMethod,
|
||||
actorOrgId
|
||||
);
|
||||
const approvals = await secretApprovalRequestDAL.findByProjectId({
|
||||
projectId,
|
||||
committer,
|
||||
@ -122,7 +130,13 @@ export const secretApprovalRequestServiceFactory = ({
|
||||
return approvals;
|
||||
};
|
||||
|
||||
const getSecretApprovalDetails = async ({ actor, actorId, actorOrgId, id }: TSecretApprovalDetailsDTO) => {
|
||||
const getSecretApprovalDetails = async ({
|
||||
actor,
|
||||
actorId,
|
||||
actorOrgId,
|
||||
actorAuthMethod,
|
||||
id
|
||||
}: TSecretApprovalDetailsDTO) => {
|
||||
if (actor === ActorType.SERVICE) throw new BadRequestError({ message: "Cannot use service token" });
|
||||
|
||||
const secretApprovalRequest = await secretApprovalRequestDAL.findById(id);
|
||||
@ -133,6 +147,7 @@ export const secretApprovalRequestServiceFactory = ({
|
||||
actor,
|
||||
actorId,
|
||||
secretApprovalRequest.projectId,
|
||||
actorAuthMethod,
|
||||
actorOrgId
|
||||
);
|
||||
if (
|
||||
@ -150,7 +165,14 @@ export const secretApprovalRequestServiceFactory = ({
|
||||
return { ...secretApprovalRequest, secretPath: secretPath?.[0]?.path || "/", commits: secrets };
|
||||
};
|
||||
|
||||
const reviewApproval = async ({ approvalId, actor, status, actorId, actorOrgId }: TReviewRequestDTO) => {
|
||||
const reviewApproval = async ({
|
||||
approvalId,
|
||||
actor,
|
||||
status,
|
||||
actorId,
|
||||
actorAuthMethod,
|
||||
actorOrgId
|
||||
}: TReviewRequestDTO) => {
|
||||
const secretApprovalRequest = await secretApprovalRequestDAL.findById(approvalId);
|
||||
if (!secretApprovalRequest) throw new BadRequestError({ message: "Secret approval request not found" });
|
||||
if (actor !== ActorType.USER) throw new BadRequestError({ message: "Must be a user" });
|
||||
@ -160,6 +182,7 @@ export const secretApprovalRequestServiceFactory = ({
|
||||
ActorType.USER,
|
||||
actorId,
|
||||
secretApprovalRequest.projectId,
|
||||
actorAuthMethod,
|
||||
actorOrgId
|
||||
);
|
||||
if (
|
||||
@ -192,7 +215,14 @@ export const secretApprovalRequestServiceFactory = ({
|
||||
return reviewStatus;
|
||||
};
|
||||
|
||||
const updateApprovalStatus = async ({ actorId, status, approvalId, actor, actorOrgId }: TStatusChangeDTO) => {
|
||||
const updateApprovalStatus = async ({
|
||||
actorId,
|
||||
status,
|
||||
approvalId,
|
||||
actor,
|
||||
actorOrgId,
|
||||
actorAuthMethod
|
||||
}: TStatusChangeDTO) => {
|
||||
const secretApprovalRequest = await secretApprovalRequestDAL.findById(approvalId);
|
||||
if (!secretApprovalRequest) throw new BadRequestError({ message: "Secret approval request not found" });
|
||||
if (actor !== ActorType.USER) throw new BadRequestError({ message: "Must be a user" });
|
||||
@ -202,6 +232,7 @@ export const secretApprovalRequestServiceFactory = ({
|
||||
ActorType.USER,
|
||||
actorId,
|
||||
secretApprovalRequest.projectId,
|
||||
actorAuthMethod,
|
||||
actorOrgId
|
||||
);
|
||||
if (
|
||||
@ -229,7 +260,8 @@ export const secretApprovalRequestServiceFactory = ({
|
||||
approvalId,
|
||||
actor,
|
||||
actorId,
|
||||
actorOrgId
|
||||
actorOrgId,
|
||||
actorAuthMethod
|
||||
}: TMergeSecretApprovalRequestDTO) => {
|
||||
const secretApprovalRequest = await secretApprovalRequestDAL.findById(approvalId);
|
||||
if (!secretApprovalRequest) throw new BadRequestError({ message: "Secret approval request not found" });
|
||||
@ -240,8 +272,10 @@ export const secretApprovalRequestServiceFactory = ({
|
||||
ActorType.USER,
|
||||
actorId,
|
||||
projectId,
|
||||
actorAuthMethod,
|
||||
actorOrgId
|
||||
);
|
||||
|
||||
if (
|
||||
!hasRole(ProjectMembershipRole.Admin) &&
|
||||
secretApprovalRequest.committerId !== membership.id &&
|
||||
@ -438,6 +472,7 @@ export const secretApprovalRequestServiceFactory = ({
|
||||
actorId,
|
||||
actor,
|
||||
actorOrgId,
|
||||
actorAuthMethod,
|
||||
policy,
|
||||
projectId,
|
||||
secretPath,
|
||||
@ -449,6 +484,7 @@ export const secretApprovalRequestServiceFactory = ({
|
||||
actor,
|
||||
actorId,
|
||||
projectId,
|
||||
actorAuthMethod,
|
||||
actorOrgId
|
||||
);
|
||||
ForbiddenError.from(permission).throwUnlessCan(
|
||||
|
@ -9,6 +9,7 @@ import jmespath from "jmespath";
|
||||
import knex from "knex";
|
||||
|
||||
import { getConfig } from "@app/lib/config/env";
|
||||
import { getDbConnectionHost } from "@app/lib/knex";
|
||||
import { alphaNumericNanoId } from "@app/lib/nanoid";
|
||||
|
||||
import { TAssignOp, TDbProviderClients, TDirectAssignOp, THttpProviderFunction } from "../templates/types";
|
||||
@ -89,7 +90,17 @@ export const secretRotationDbFn = async ({
|
||||
const appCfg = getConfig();
|
||||
|
||||
const ssl = ca ? { rejectUnauthorized: false, ca } : undefined;
|
||||
if (host === "localhost" || host === "127.0.0.1" || appCfg.DB_CONNECTION_URI.includes(host))
|
||||
const dbHost = appCfg.DB_HOST || getDbConnectionHost(appCfg.DB_CONNECTION_URI);
|
||||
if (
|
||||
host === "localhost" ||
|
||||
host === "127.0.0.1" ||
|
||||
// database infisical uses
|
||||
dbHost === host ||
|
||||
// internal ips
|
||||
host === "host.docker.internal" ||
|
||||
host.match(/^10\.\d+\.\d+\.\d+/) ||
|
||||
host.match(/^192\.168\.\d+\.\d+/)
|
||||
)
|
||||
throw new Error("Invalid db host");
|
||||
|
||||
const db = knex({
|
||||
|
@ -39,8 +39,20 @@ export const secretRotationServiceFactory = ({
|
||||
folderDAL,
|
||||
secretDAL
|
||||
}: TSecretRotationServiceFactoryDep) => {
|
||||
const getProviderTemplates = async ({ actor, actorId, actorOrgId, projectId }: TProjectPermission) => {
|
||||
const { permission } = await permissionService.getProjectPermission(actor, actorId, projectId, actorOrgId);
|
||||
const getProviderTemplates = async ({
|
||||
actor,
|
||||
actorId,
|
||||
actorOrgId,
|
||||
actorAuthMethod,
|
||||
projectId
|
||||
}: TProjectPermission) => {
|
||||
const { permission } = await permissionService.getProjectPermission(
|
||||
actor,
|
||||
actorId,
|
||||
projectId,
|
||||
actorAuthMethod,
|
||||
actorOrgId
|
||||
);
|
||||
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.SecretRotation);
|
||||
|
||||
return {
|
||||
@ -54,6 +66,7 @@ export const secretRotationServiceFactory = ({
|
||||
actorId,
|
||||
actor,
|
||||
actorOrgId,
|
||||
actorAuthMethod,
|
||||
inputs,
|
||||
outputs,
|
||||
interval,
|
||||
@ -61,7 +74,13 @@ export const secretRotationServiceFactory = ({
|
||||
secretPath,
|
||||
environment
|
||||
}: TCreateSecretRotationDTO) => {
|
||||
const { permission } = await permissionService.getProjectPermission(actor, actorId, projectId, actorOrgId);
|
||||
const { permission } = await permissionService.getProjectPermission(
|
||||
actor,
|
||||
actorId,
|
||||
projectId,
|
||||
actorAuthMethod,
|
||||
actorOrgId
|
||||
);
|
||||
ForbiddenError.from(permission).throwUnlessCan(
|
||||
ProjectPermissionActions.Create,
|
||||
ProjectPermissionSub.SecretRotation
|
||||
@ -139,14 +158,20 @@ export const secretRotationServiceFactory = ({
|
||||
return secretRotation;
|
||||
};
|
||||
|
||||
const getByProjectId = async ({ actorId, projectId, actor, actorOrgId }: TListByProjectIdDTO) => {
|
||||
const { permission } = await permissionService.getProjectPermission(actor, actorId, projectId, actorOrgId);
|
||||
const getByProjectId = async ({ actorId, projectId, actor, actorOrgId, actorAuthMethod }: TListByProjectIdDTO) => {
|
||||
const { permission } = await permissionService.getProjectPermission(
|
||||
actor,
|
||||
actorId,
|
||||
projectId,
|
||||
actorAuthMethod,
|
||||
actorOrgId
|
||||
);
|
||||
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.SecretRotation);
|
||||
const doc = await secretRotationDAL.find({ projectId });
|
||||
return doc;
|
||||
};
|
||||
|
||||
const restartById = async ({ actor, actorId, actorOrgId, rotationId }: TRestartDTO) => {
|
||||
const restartById = async ({ actor, actorId, actorOrgId, actorAuthMethod, rotationId }: TRestartDTO) => {
|
||||
const doc = await secretRotationDAL.findById(rotationId);
|
||||
if (!doc) throw new BadRequestError({ message: "Rotation not found" });
|
||||
|
||||
@ -157,18 +182,30 @@ export const secretRotationServiceFactory = ({
|
||||
message: "Failed to add secret rotation due to plan restriction. Upgrade plan to add secret rotation."
|
||||
});
|
||||
|
||||
const { permission } = await permissionService.getProjectPermission(actor, actorId, doc.projectId, actorOrgId);
|
||||
const { permission } = await permissionService.getProjectPermission(
|
||||
actor,
|
||||
actorId,
|
||||
doc.projectId,
|
||||
actorAuthMethod,
|
||||
actorOrgId
|
||||
);
|
||||
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Edit, ProjectPermissionSub.SecretRotation);
|
||||
await secretRotationQueue.removeFromQueue(doc.id, doc.interval);
|
||||
await secretRotationQueue.addToQueue(doc.id, doc.interval);
|
||||
return doc;
|
||||
};
|
||||
|
||||
const deleteById = async ({ actor, actorId, actorOrgId, rotationId }: TDeleteDTO) => {
|
||||
const deleteById = async ({ actor, actorId, actorOrgId, actorAuthMethod, rotationId }: TDeleteDTO) => {
|
||||
const doc = await secretRotationDAL.findById(rotationId);
|
||||
if (!doc) throw new BadRequestError({ message: "Rotation not found" });
|
||||
|
||||
const { permission } = await permissionService.getProjectPermission(actor, actorId, doc.projectId, actorOrgId);
|
||||
const { permission } = await permissionService.getProjectPermission(
|
||||
actor,
|
||||
actorId,
|
||||
doc.projectId,
|
||||
actorAuthMethod,
|
||||
actorOrgId
|
||||
);
|
||||
ForbiddenError.from(permission).throwUnlessCan(
|
||||
ProjectPermissionActions.Delete,
|
||||
ProjectPermissionSub.SecretRotation
|
||||
|
@ -39,8 +39,14 @@ export const secretScanningServiceFactory = ({
|
||||
permissionService,
|
||||
secretScanningQueue
|
||||
}: TSecretScanningServiceFactoryDep) => {
|
||||
const createInstallationSession = async ({ actor, orgId, actorId, actorOrgId }: TInstallAppSessionDTO) => {
|
||||
const { permission } = await permissionService.getOrgPermission(actor, actorId, orgId, actorOrgId);
|
||||
const createInstallationSession = async ({
|
||||
actor,
|
||||
orgId,
|
||||
actorId,
|
||||
actorAuthMethod,
|
||||
actorOrgId
|
||||
}: TInstallAppSessionDTO) => {
|
||||
const { permission } = await permissionService.getOrgPermission(actor, actorId, orgId, actorAuthMethod, actorOrgId);
|
||||
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Create, OrgPermissionSubjects.SecretScanning);
|
||||
|
||||
const sessionId = crypto.randomBytes(16).toString("hex");
|
||||
@ -53,12 +59,19 @@ export const secretScanningServiceFactory = ({
|
||||
actorId,
|
||||
installationId,
|
||||
actor,
|
||||
actorAuthMethod,
|
||||
actorOrgId
|
||||
}: TLinkInstallSessionDTO) => {
|
||||
const session = await gitAppInstallSessionDAL.findOne({ sessionId });
|
||||
if (!session) throw new UnauthorizedError({ message: "Session not found" });
|
||||
|
||||
const { permission } = await permissionService.getOrgPermission(actor, actorId, session.orgId, actorOrgId);
|
||||
const { permission } = await permissionService.getOrgPermission(
|
||||
actor,
|
||||
actorId,
|
||||
session.orgId,
|
||||
actorAuthMethod,
|
||||
actorOrgId
|
||||
);
|
||||
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Create, OrgPermissionSubjects.SecretScanning);
|
||||
const installatedApp = await gitAppOrgDAL.transaction(async (tx) => {
|
||||
await gitAppInstallSessionDAL.deleteById(session.id, tx);
|
||||
@ -89,23 +102,37 @@ export const secretScanningServiceFactory = ({
|
||||
return { installatedApp };
|
||||
};
|
||||
|
||||
const getOrgInstallationStatus = async ({ actorId, orgId, actor, actorOrgId }: TGetOrgInstallStatusDTO) => {
|
||||
const { permission } = await permissionService.getOrgPermission(actor, actorId, orgId, actorOrgId);
|
||||
const getOrgInstallationStatus = async ({
|
||||
actorId,
|
||||
orgId,
|
||||
actor,
|
||||
actorAuthMethod,
|
||||
actorOrgId
|
||||
}: TGetOrgInstallStatusDTO) => {
|
||||
const { permission } = await permissionService.getOrgPermission(actor, actorId, orgId, actorAuthMethod, actorOrgId);
|
||||
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Read, OrgPermissionSubjects.SecretScanning);
|
||||
|
||||
const appInstallation = await gitAppOrgDAL.findOne({ orgId });
|
||||
return Boolean(appInstallation);
|
||||
};
|
||||
|
||||
const getRisksByOrg = async ({ actor, orgId, actorId, actorOrgId }: TGetOrgRisksDTO) => {
|
||||
const { permission } = await permissionService.getOrgPermission(actor, actorId, orgId, actorOrgId);
|
||||
const getRisksByOrg = async ({ actor, orgId, actorId, actorAuthMethod, actorOrgId }: TGetOrgRisksDTO) => {
|
||||
const { permission } = await permissionService.getOrgPermission(actor, actorId, orgId, actorAuthMethod, actorOrgId);
|
||||
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Read, OrgPermissionSubjects.SecretScanning);
|
||||
const risks = await secretScanningDAL.find({ orgId }, { sort: [["createdAt", "desc"]] });
|
||||
return { risks };
|
||||
};
|
||||
|
||||
const updateRiskStatus = async ({ actorId, orgId, actor, actorOrgId, riskId, status }: TUpdateRiskStatusDTO) => {
|
||||
const { permission } = await permissionService.getOrgPermission(actor, actorId, orgId, actorOrgId);
|
||||
const updateRiskStatus = async ({
|
||||
actorId,
|
||||
orgId,
|
||||
actor,
|
||||
actorOrgId,
|
||||
actorAuthMethod,
|
||||
riskId,
|
||||
status
|
||||
}: TUpdateRiskStatusDTO) => {
|
||||
const { permission } = await permissionService.getOrgPermission(actor, actorId, orgId, actorAuthMethod, actorOrgId);
|
||||
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Edit, OrgPermissionSubjects.SecretScanning);
|
||||
|
||||
const isRiskResolved = Boolean(
|
||||
|
@ -59,9 +59,16 @@ export const secretSnapshotServiceFactory = ({
|
||||
actorId,
|
||||
actor,
|
||||
actorOrgId,
|
||||
actorAuthMethod,
|
||||
path
|
||||
}: TProjectSnapshotCountDTO) => {
|
||||
const { permission } = await permissionService.getProjectPermission(actor, actorId, projectId, actorOrgId);
|
||||
const { permission } = await permissionService.getProjectPermission(
|
||||
actor,
|
||||
actorId,
|
||||
projectId,
|
||||
actorAuthMethod,
|
||||
actorOrgId
|
||||
);
|
||||
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.SecretRollback);
|
||||
|
||||
const folder = await folderDAL.findBySecretPath(projectId, environment, path);
|
||||
@ -77,11 +84,18 @@ export const secretSnapshotServiceFactory = ({
|
||||
actorId,
|
||||
actor,
|
||||
actorOrgId,
|
||||
actorAuthMethod,
|
||||
path,
|
||||
limit = 20,
|
||||
offset = 0
|
||||
}: TProjectSnapshotListDTO) => {
|
||||
const { permission } = await permissionService.getProjectPermission(actor, actorId, projectId, actorOrgId);
|
||||
const { permission } = await permissionService.getProjectPermission(
|
||||
actor,
|
||||
actorId,
|
||||
projectId,
|
||||
actorAuthMethod,
|
||||
actorOrgId
|
||||
);
|
||||
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.SecretRollback);
|
||||
|
||||
const folder = await folderDAL.findBySecretPath(projectId, environment, path);
|
||||
@ -91,10 +105,16 @@ export const secretSnapshotServiceFactory = ({
|
||||
return snapshots;
|
||||
};
|
||||
|
||||
const getSnapshotData = async ({ actorId, actor, actorOrgId, id }: TGetSnapshotDataDTO) => {
|
||||
const getSnapshotData = async ({ actorId, actor, actorOrgId, actorAuthMethod, id }: TGetSnapshotDataDTO) => {
|
||||
const snapshot = await snapshotDAL.findSecretSnapshotDataById(id);
|
||||
if (!snapshot) throw new BadRequestError({ message: "Snapshot not found" });
|
||||
const { permission } = await permissionService.getProjectPermission(actor, actorId, snapshot.projectId, actorOrgId);
|
||||
const { permission } = await permissionService.getProjectPermission(
|
||||
actor,
|
||||
actorId,
|
||||
snapshot.projectId,
|
||||
actorAuthMethod,
|
||||
actorOrgId
|
||||
);
|
||||
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.SecretRollback);
|
||||
return snapshot;
|
||||
};
|
||||
@ -145,11 +165,23 @@ export const secretSnapshotServiceFactory = ({
|
||||
}
|
||||
};
|
||||
|
||||
const rollbackSnapshot = async ({ id: snapshotId, actor, actorId, actorOrgId }: TRollbackSnapshotDTO) => {
|
||||
const rollbackSnapshot = async ({
|
||||
id: snapshotId,
|
||||
actor,
|
||||
actorId,
|
||||
actorAuthMethod,
|
||||
actorOrgId
|
||||
}: TRollbackSnapshotDTO) => {
|
||||
const snapshot = await snapshotDAL.findById(snapshotId);
|
||||
if (!snapshot) throw new BadRequestError({ message: "Snapshot not found" });
|
||||
|
||||
const { permission } = await permissionService.getProjectPermission(actor, actorId, snapshot.projectId, actorOrgId);
|
||||
const { permission } = await permissionService.getProjectPermission(
|
||||
actor,
|
||||
actorId,
|
||||
snapshot.projectId,
|
||||
actorAuthMethod,
|
||||
actorOrgId
|
||||
);
|
||||
ForbiddenError.from(permission).throwUnlessCan(
|
||||
ProjectPermissionActions.Create,
|
||||
ProjectPermissionSub.SecretRollback
|
||||
|
@ -26,8 +26,14 @@ export const trustedIpServiceFactory = ({
|
||||
licenseService,
|
||||
projectDAL
|
||||
}: TTrustedIpServiceFactoryDep) => {
|
||||
const listIpsByProjectId = async ({ projectId, actor, actorId, actorOrgId }: TProjectPermission) => {
|
||||
const { permission } = await permissionService.getProjectPermission(actor, actorId, projectId, actorOrgId);
|
||||
const listIpsByProjectId = async ({ projectId, actor, actorId, actorAuthMethod, actorOrgId }: TProjectPermission) => {
|
||||
const { permission } = await permissionService.getProjectPermission(
|
||||
actor,
|
||||
actorId,
|
||||
projectId,
|
||||
actorAuthMethod,
|
||||
actorOrgId
|
||||
);
|
||||
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.IpAllowList);
|
||||
const trustedIps = await trustedIpDAL.find({
|
||||
projectId
|
||||
@ -38,13 +44,20 @@ export const trustedIpServiceFactory = ({
|
||||
const addProjectIp = async ({
|
||||
projectId,
|
||||
actorId,
|
||||
actorAuthMethod,
|
||||
actor,
|
||||
actorOrgId,
|
||||
ipAddress: ip,
|
||||
comment,
|
||||
isActive
|
||||
}: TCreateIpDTO) => {
|
||||
const { permission } = await permissionService.getProjectPermission(actor, actorId, projectId, actorOrgId);
|
||||
const { permission } = await permissionService.getProjectPermission(
|
||||
actor,
|
||||
actorId,
|
||||
projectId,
|
||||
actorAuthMethod,
|
||||
actorOrgId
|
||||
);
|
||||
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Create, ProjectPermissionSub.IpAllowList);
|
||||
|
||||
const project = await projectDAL.findById(projectId);
|
||||
@ -78,11 +91,18 @@ export const trustedIpServiceFactory = ({
|
||||
actorId,
|
||||
actor,
|
||||
actorOrgId,
|
||||
actorAuthMethod,
|
||||
ipAddress: ip,
|
||||
comment,
|
||||
trustedIpId
|
||||
}: TUpdateIpDTO) => {
|
||||
const { permission } = await permissionService.getProjectPermission(actor, actorId, projectId, actorOrgId);
|
||||
const { permission } = await permissionService.getProjectPermission(
|
||||
actor,
|
||||
actorId,
|
||||
projectId,
|
||||
actorAuthMethod,
|
||||
actorOrgId
|
||||
);
|
||||
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Create, ProjectPermissionSub.IpAllowList);
|
||||
|
||||
const project = await projectDAL.findById(projectId);
|
||||
@ -113,8 +133,21 @@ export const trustedIpServiceFactory = ({
|
||||
return { trustedIp, project }; // for audit log
|
||||
};
|
||||
|
||||
const deleteProjectIp = async ({ projectId, actorId, actor, actorOrgId, trustedIpId }: TDeleteIpDTO) => {
|
||||
const { permission } = await permissionService.getProjectPermission(actor, actorId, projectId, actorOrgId);
|
||||
const deleteProjectIp = async ({
|
||||
projectId,
|
||||
actorId,
|
||||
actor,
|
||||
actorOrgId,
|
||||
actorAuthMethod,
|
||||
trustedIpId
|
||||
}: TDeleteIpDTO) => {
|
||||
const { permission } = await permissionService.getProjectPermission(
|
||||
actor,
|
||||
actorId,
|
||||
projectId,
|
||||
actorAuthMethod,
|
||||
actorOrgId
|
||||
);
|
||||
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Create, ProjectPermissionSub.IpAllowList);
|
||||
|
||||
const project = await projectDAL.findById(projectId);
|
||||
|
@ -1,3 +1,34 @@
|
||||
export const GROUPS = {
|
||||
CREATE: {
|
||||
name: "The name of the group to create.",
|
||||
slug: "The slug of the group to create.",
|
||||
role: "The role of the group to create."
|
||||
},
|
||||
UPDATE: {
|
||||
currentSlug: "The current slug of the group to update.",
|
||||
name: "The new name of the group to update to.",
|
||||
slug: "The new slug of the group to update to.",
|
||||
role: "The new role of the group to update to."
|
||||
},
|
||||
DELETE: {
|
||||
slug: "The slug of the group to delete"
|
||||
},
|
||||
LIST_USERS: {
|
||||
slug: "The slug of the group to list users for",
|
||||
offset: "The offset to start from. If you enter 10, it will start from the 10th user.",
|
||||
limit: "The number of users to return.",
|
||||
username: "The username to search for."
|
||||
},
|
||||
ADD_USER: {
|
||||
slug: "The slug of the group to add the user to.",
|
||||
username: "The username of the user to add to the group."
|
||||
},
|
||||
DELETE_USER: {
|
||||
slug: "The slug of the group to remove the user from.",
|
||||
username: "The username of the user to remove from the group."
|
||||
}
|
||||
} as const;
|
||||
|
||||
export const IDENTITIES = {
|
||||
CREATE: {
|
||||
name: "The name of the identity to create.",
|
||||
@ -79,12 +110,15 @@ export const ORGANIZATIONS = {
|
||||
},
|
||||
GET_PROJECTS: {
|
||||
organizationId: "The ID of the organization to get projects from."
|
||||
},
|
||||
LIST_GROUPS: {
|
||||
organizationId: "The ID of the organization to list groups for."
|
||||
}
|
||||
} as const;
|
||||
|
||||
export const PROJECTS = {
|
||||
CREATE: {
|
||||
organizationId: "The ID of the organization to create the project in.",
|
||||
organizationSlug: "The slug of the organization to create the project in.",
|
||||
projectName: "The name of the project to create.",
|
||||
slug: "An optional slug for the project."
|
||||
},
|
||||
@ -141,6 +175,29 @@ export const PROJECTS = {
|
||||
},
|
||||
ROLLBACK_TO_SNAPSHOT: {
|
||||
secretSnapshotId: "The ID of the snapshot to rollback to."
|
||||
},
|
||||
ADD_GROUP_TO_PROJECT: {
|
||||
projectSlug: "The slug of the project to add the group to.",
|
||||
groupSlug: "The slug of the group to add to the project.",
|
||||
role: "The role for the group to assume in the project."
|
||||
},
|
||||
UPDATE_GROUP_IN_PROJECT: {
|
||||
projectSlug: "The slug of the project to update the group in.",
|
||||
groupSlug: "The slug of the group to update in the project.",
|
||||
roles: "A list of roles to update the group to."
|
||||
},
|
||||
REMOVE_GROUP_FROM_PROJECT: {
|
||||
projectSlug: "The slug of the project to delete the group from.",
|
||||
groupSlug: "The slug of the group to delete from the project."
|
||||
},
|
||||
LIST_GROUPS_IN_PROJECT: {
|
||||
projectSlug: "The slug of the project to list groups for."
|
||||
},
|
||||
LIST_INTEGRATION: {
|
||||
workspaceId: "The ID of the project to list integrations for."
|
||||
},
|
||||
LIST_INTEGRATION_AUTHORIZATION: {
|
||||
workspaceId: "The ID of the project to list integration auths for."
|
||||
}
|
||||
} as const;
|
||||
|
||||
@ -194,9 +251,31 @@ export const FOLDERS = {
|
||||
}
|
||||
} as const;
|
||||
|
||||
export const SECRETS = {
|
||||
ATTACH_TAGS: {
|
||||
secretName: "The name of the secret to attach tags to.",
|
||||
secretPath: "The path of the secret to attach tags to.",
|
||||
type: "The type of the secret to attach tags to. (shared/personal)",
|
||||
environment: "The slug of the environment where the secret is located",
|
||||
projectSlug: "The slug of the project where the secret is located",
|
||||
tagSlugs: "An array of existing tag slugs to attach to the secret."
|
||||
},
|
||||
DETACH_TAGS: {
|
||||
secretName: "The name of the secret to detach tags from.",
|
||||
secretPath: "The path of the secret to detach tags from.",
|
||||
type: "The type of the secret to attach tags to. (shared/personal)",
|
||||
environment: "The slug of the environment where the secret is located",
|
||||
projectSlug: "The slug of the project where the secret is located",
|
||||
tagSlugs: "An array of existing tag slugs to detach from the secret."
|
||||
}
|
||||
} as const;
|
||||
|
||||
export const RAW_SECRETS = {
|
||||
LIST: {
|
||||
recursive:
|
||||
"Whether or not to fetch all secrets from the specified base path, and all of its subdirectories. Note, the max depth is 20 deep.",
|
||||
workspaceId: "The ID of the project to list secrets from.",
|
||||
workspaceSlug: "The slug of the project to list secrets from. This parameter is only usable by machine identities.",
|
||||
environment: "The slug of the environment to list secrets from.",
|
||||
secretPath: "The secret path to list secrets from.",
|
||||
includeImports: "Weather to include imported secrets or not."
|
||||
@ -284,3 +363,248 @@ export const AUDIT_LOGS = {
|
||||
actor: "The actor to filter the audit logs by."
|
||||
}
|
||||
} as const;
|
||||
|
||||
export const DYNAMIC_SECRETS = {
|
||||
LIST: {
|
||||
projectSlug: "The slug of the project to create dynamic secret in.",
|
||||
environmentSlug: "The slug of the environment to list folders from.",
|
||||
path: "The path to list folders from."
|
||||
},
|
||||
LIST_LEAES_BY_NAME: {
|
||||
projectSlug: "The slug of the project to create dynamic secret in.",
|
||||
environmentSlug: "The slug of the environment to list folders from.",
|
||||
path: "The path to list folders from.",
|
||||
name: "The name of the dynamic secret."
|
||||
},
|
||||
GET_BY_NAME: {
|
||||
projectSlug: "The slug of the project to create dynamic secret in.",
|
||||
environmentSlug: "The slug of the environment to list folders from.",
|
||||
path: "The path to list folders from.",
|
||||
name: "The name of the dynamic secret."
|
||||
},
|
||||
CREATE: {
|
||||
projectSlug: "The slug of the project to create dynamic secret in.",
|
||||
environmentSlug: "The slug of the environment to create the dynamic secret in.",
|
||||
path: "The path to create the dynamic secret in.",
|
||||
name: "The name of the dynamic secret.",
|
||||
provider: "The type of dynamic secret.",
|
||||
defaultTTL: "The default TTL that will be applied for all the leases.",
|
||||
maxTTL: "The maximum limit a TTL can be leases or renewed."
|
||||
},
|
||||
UPDATE: {
|
||||
projectSlug: "The slug of the project to update dynamic secret in.",
|
||||
environmentSlug: "The slug of the environment to update the dynamic secret in.",
|
||||
path: "The path to update the dynamic secret in.",
|
||||
name: "The name of the dynamic secret.",
|
||||
inputs: "The new partial values for the configurated provider of the dynamic secret",
|
||||
defaultTTL: "The default TTL that will be applied for all the leases.",
|
||||
maxTTL: "The maximum limit a TTL can be leases or renewed.",
|
||||
newName: "The new name for the dynamic secret."
|
||||
},
|
||||
DELETE: {
|
||||
projectSlug: "The slug of the project to delete dynamic secret in.",
|
||||
environmentSlug: "The slug of the environment to delete the dynamic secret in.",
|
||||
path: "The path to delete the dynamic secret in.",
|
||||
name: "The name of the dynamic secret.",
|
||||
isForced:
|
||||
"A boolean flag to delete the the dynamic secret from infisical without trying to remove it from external provider. Used when the dynamic secret got modified externally."
|
||||
}
|
||||
} as const;
|
||||
|
||||
export const DYNAMIC_SECRET_LEASES = {
|
||||
GET_BY_LEASEID: {
|
||||
projectSlug: "The slug of the project to create dynamic secret in.",
|
||||
environmentSlug: "The slug of the environment to list folders from.",
|
||||
path: "The path to list folders from.",
|
||||
leaseId: "The ID of the dynamic secret lease."
|
||||
},
|
||||
CREATE: {
|
||||
projectSlug: "The slug of the project of the dynamic secret in.",
|
||||
environmentSlug: "The slug of the environment of the dynamic secret in.",
|
||||
path: "The path of the dynamic secret in.",
|
||||
dynamicSecretName: "The name of the dynamic secret.",
|
||||
ttl: "The lease lifetime ttl. If not provided the default TTL of dynamic secret will be used."
|
||||
},
|
||||
RENEW: {
|
||||
projectSlug: "The slug of the project of the dynamic secret in.",
|
||||
environmentSlug: "The slug of the environment of the dynamic secret in.",
|
||||
path: "The path of the dynamic secret in.",
|
||||
leaseId: "The ID of the dynamic secret lease.",
|
||||
ttl: "The renew TTL that gets added with current expiry (ensure it's below max TTL) for a total less than creation time + max TTL."
|
||||
},
|
||||
DELETE: {
|
||||
projectSlug: "The slug of the project of the dynamic secret in.",
|
||||
environmentSlug: "The slug of the environment of the dynamic secret in.",
|
||||
path: "The path of the dynamic secret in.",
|
||||
leaseId: "The ID of the dynamic secret lease.",
|
||||
isForced:
|
||||
"A boolean flag to delete the the dynamic secret from infisical without trying to remove it from external provider. Used when the dynamic secret got modified externally."
|
||||
}
|
||||
} as const;
|
||||
export const SECRET_TAGS = {
|
||||
LIST: {
|
||||
projectId: "The ID of the project to list tags from."
|
||||
},
|
||||
CREATE: {
|
||||
projectId: "The ID of the project to create the tag in.",
|
||||
name: "The name of the tag to create.",
|
||||
slug: "The slug of the tag to create.",
|
||||
color: "The color of the tag to create."
|
||||
},
|
||||
DELETE: {
|
||||
tagId: "The ID of the tag to delete.",
|
||||
projectId: "The ID of the project to delete the tag from."
|
||||
}
|
||||
} as const;
|
||||
|
||||
export const IDENTITY_ADDITIONAL_PRIVILEGE = {
|
||||
CREATE: {
|
||||
projectSlug: "The slug of the project of the identity in.",
|
||||
identityId: "The ID of the identity to delete.",
|
||||
slug: "The slug of the privilege to create.",
|
||||
permissions: `The permission object for the privilege.
|
||||
1. [["read", "secrets", {environment: "dev", secretPath: {$glob: "/"}}]]
|
||||
2. [["read", "secrets", {environment: "dev"}], ["create", "secrets", {environment: "dev"}]]
|
||||
2. [["read", "secrets", {environment: "dev"}]]
|
||||
`,
|
||||
isPackPermission: "Whether the server should pack(compact) the permission object.",
|
||||
isTemporary: "Whether the privilege is temporary.",
|
||||
temporaryMode: "Type of temporary access given. Types: relative",
|
||||
temporaryRange: "TTL for the temporay time. Eg: 1m, 1h, 1d",
|
||||
temporaryAccessStartTime: "ISO time for which temporary access should begin."
|
||||
},
|
||||
UPDATE: {
|
||||
projectSlug: "The slug of the project of the identity in.",
|
||||
identityId: "The ID of the identity to update.",
|
||||
slug: "The slug of the privilege to update.",
|
||||
newSlug: "The new slug of the privilege to update.",
|
||||
permissions: `The permission object for the privilege.
|
||||
1. [["read", "secrets", {environment: "dev", secretPath: {$glob: "/"}}]]
|
||||
2. [["read", "secrets", {environment: "dev"}], ["create", "secrets", {environment: "dev"}]]
|
||||
2. [["read", "secrets", {environment: "dev"}]]
|
||||
`,
|
||||
isPackPermission: "Whether the server should pack(compact) the permission object.",
|
||||
isTemporary: "Whether the privilege is temporary.",
|
||||
temporaryMode: "Type of temporary access given. Types: relative",
|
||||
temporaryRange: "TTL for the temporay time. Eg: 1m, 1h, 1d",
|
||||
temporaryAccessStartTime: "ISO time for which temporary access should begin."
|
||||
},
|
||||
DELETE: {
|
||||
projectSlug: "The slug of the project of the identity in.",
|
||||
identityId: "The ID of the identity to delete.",
|
||||
slug: "The slug of the privilege to delete."
|
||||
},
|
||||
GET_BY_SLUG: {
|
||||
projectSlug: "The slug of the project of the identity in.",
|
||||
identityId: "The ID of the identity to list.",
|
||||
slug: "The slug of the privilege."
|
||||
},
|
||||
LIST: {
|
||||
projectSlug: "The slug of the project of the identity in.",
|
||||
identityId: "The ID of the identity to list.",
|
||||
unpacked: "Whether the system should send the permissions as unpacked"
|
||||
}
|
||||
};
|
||||
|
||||
export const PROJECT_USER_ADDITIONAL_PRIVILEGE = {
|
||||
CREATE: {
|
||||
projectMembershipId: "Project membership id of user",
|
||||
slug: "The slug of the privilege to create.",
|
||||
permissions:
|
||||
"The permission object for the privilege. Refer https://casl.js.org/v6/en/guide/define-rules#the-shape-of-raw-rule to understand the shape",
|
||||
isPackPermission: "Whether the server should pack(compact) the permission object.",
|
||||
isTemporary: "Whether the privilege is temporary.",
|
||||
temporaryMode: "Type of temporary access given. Types: relative",
|
||||
temporaryRange: "TTL for the temporay time. Eg: 1m, 1h, 1d",
|
||||
temporaryAccessStartTime: "ISO time for which temporary access should begin."
|
||||
},
|
||||
UPDATE: {
|
||||
privilegeId: "The id of privilege object",
|
||||
slug: "The slug of the privilege to create.",
|
||||
newSlug: "The new slug of the privilege to create.",
|
||||
permissions:
|
||||
"The permission object for the privilege. Refer https://casl.js.org/v6/en/guide/define-rules#the-shape-of-raw-rule to understand the shape",
|
||||
isPackPermission: "Whether the server should pack(compact) the permission object.",
|
||||
isTemporary: "Whether the privilege is temporary.",
|
||||
temporaryMode: "Type of temporary access given. Types: relative",
|
||||
temporaryRange: "TTL for the temporay time. Eg: 1m, 1h, 1d",
|
||||
temporaryAccessStartTime: "ISO time for which temporary access should begin."
|
||||
},
|
||||
DELETE: {
|
||||
privilegeId: "The id of privilege object"
|
||||
},
|
||||
GET_BY_PRIVILEGEID: {
|
||||
privilegeId: "The id of privilege object"
|
||||
},
|
||||
LIST: {
|
||||
projectMembershipId: "Project membership id of user"
|
||||
}
|
||||
};
|
||||
|
||||
export const INTEGRATION_AUTH = {
|
||||
GET: {
|
||||
integrationAuthId: "The id of integration authentication object."
|
||||
},
|
||||
DELETE: {
|
||||
integration: "The slug of the integration to be unauthorized.",
|
||||
projectId: "The ID of the project to delete the integration auth from."
|
||||
},
|
||||
DELETE_BY_ID: {
|
||||
integrationAuthId: "The id of integration authentication object to delete."
|
||||
},
|
||||
CREATE_ACCESS_TOKEN: {
|
||||
workspaceId: "The ID of the project to create the integration auth for.",
|
||||
integration: "The slug of integration for the auth object.",
|
||||
accessId: "The unique authorized access id of the external integration provider.",
|
||||
accessToken: "The unique authorized access token of the external integration provider.",
|
||||
url: "",
|
||||
namespace: "",
|
||||
refreshToken: "The refresh token for integration authorization."
|
||||
}
|
||||
} as const;
|
||||
|
||||
export const INTEGRATION = {
|
||||
CREATE: {
|
||||
integrationAuthId: "The ID of the integration auth object to link with integration.",
|
||||
app: "The name of the external integration providers app entity that you want to sync secrets with. Used in Netlify, GitHub, Vercel integrations.",
|
||||
isActive: "Whether the integration should be active or disabled.",
|
||||
appId:
|
||||
"The ID of the external integration providers app entity that you want to sync secrets with. Used in Netlify, GitHub, Vercel integrations.",
|
||||
secretPath: "The path of the secrets to sync secrets from.",
|
||||
sourceEnvironment: "The environment to sync secret from.",
|
||||
targetEnvironment:
|
||||
"The target environment of the integration provider. Used in cloudflare pages, TeamCity, Gitlab integrations.",
|
||||
targetEnvironmentId:
|
||||
"The target environment id of the integration provider. Used in cloudflare pages, teamcity, gitlab integrations.",
|
||||
targetService:
|
||||
"The service based grouping identifier of the external provider. Used in Terraform cloud, Checkly, Railway and NorthFlank",
|
||||
targetServiceId:
|
||||
"The service based grouping identifier ID of the external provider. Used in Terraform cloud, Checkly, Railway and NorthFlank",
|
||||
owner: "External integration providers service entity owner. Used in Github.",
|
||||
path: "Path to save the synced secrets. Used by Gitlab, AWS Parameter Store, Vault",
|
||||
region: "AWS region to sync secrets to.",
|
||||
scope: "Scope of the provider. Used by Github, Qovery",
|
||||
metadata: {
|
||||
secretPrefix: "The prefix for the saved secret. Used by GCP",
|
||||
secretSuffix: "The suffix for the saved secret. Used by GCP",
|
||||
initialSyncBehavoir: "Type of syncing behavoir with the integration",
|
||||
shouldAutoRedeploy: "Used by Render to trigger auto deploy",
|
||||
secretGCPLabel: "The label for the GCP secrets"
|
||||
}
|
||||
},
|
||||
UPDATE: {
|
||||
integrationId: "The ID of the integration object.",
|
||||
app: "The name of the external integration providers app entity that you want to sync secrets with. Used in Netlify, GitHub, Vercel integrations.",
|
||||
appId:
|
||||
"The ID of the external integration providers app entity that you want to sync secrets with. Used in Netlify, GitHub, Vercel integrations.",
|
||||
isActive: "Whether the integration should be active or disabled.",
|
||||
secretPath: "The path of the secrets to sync secrets from.",
|
||||
owner: "External integration providers service entity owner. Used in Github.",
|
||||
targetEnvironment:
|
||||
"The target environment of the integration provider. Used in cloudflare pages, TeamCity, Gitlab integrations.",
|
||||
environment: "The environment to sync secrets from."
|
||||
},
|
||||
DELETE: {
|
||||
integrationId: "The ID of the integration object."
|
||||
}
|
||||
};
|
||||
|
@ -18,6 +18,7 @@ const envSchema = z
|
||||
DB_CONNECTION_URI: zpStr(z.string().describe("Postgres database connection string")).default(
|
||||
`postgresql://${process.env.DB_USER}:${process.env.DB_PASSWORD}@${process.env.DB_HOST}:${process.env.DB_PORT}/${process.env.DB_NAME}`
|
||||
),
|
||||
MAX_LEASE_LIMIT: z.coerce.number().default(10000),
|
||||
DB_ROOT_CERT: zpStr(z.string().describe("Postgres database base64-encoded CA cert").optional()),
|
||||
DB_HOST: zpStr(z.string().describe("Postgres database host").optional()),
|
||||
DB_PORT: zpStr(z.string().describe("Postgres database port").optional()).default("5432"),
|
||||
@ -113,7 +114,8 @@ const envSchema = z
|
||||
.enum(["true", "false"])
|
||||
.transform((val) => val === "true")
|
||||
.optional(),
|
||||
INFISICAL_CLOUD: zodStrBool.default("false")
|
||||
INFISICAL_CLOUD: zodStrBool.default("false"),
|
||||
MAINTENANCE_MODE: zodStrBool.default("false")
|
||||
})
|
||||
.transform((data) => ({
|
||||
...data,
|
||||
|
@ -59,6 +59,18 @@ export class BadRequestError extends Error {
|
||||
}
|
||||
}
|
||||
|
||||
export class DisableRotationErrors extends Error {
|
||||
name: string;
|
||||
|
||||
error: unknown;
|
||||
|
||||
constructor({ name, error, message }: { message: string; name?: string; error?: unknown }) {
|
||||
super(message);
|
||||
this.name = name || "DisableRotationErrors";
|
||||
this.error = error;
|
||||
}
|
||||
}
|
||||
|
||||
export class ScimRequestError extends Error {
|
||||
name: string;
|
||||
|
||||
|
@ -0,0 +1,2 @@
|
||||
export const getLastMidnightDateISO = (last = 1) =>
|
||||
`${new Date(new Date().setDate(new Date().getDate() - last)).toISOString().slice(0, 10)}T00:00:00Z`;
|
||||
|
@ -2,5 +2,6 @@
|
||||
// Full credits goes to https://github.com/rayapps to those functions
|
||||
// Code taken to keep in in house and to adjust somethings for our needs
|
||||
export * from "./array";
|
||||
export * from "./dates";
|
||||
export * from "./object";
|
||||
export * from "./string";
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user