mirror of
https://github.com/Infisical/infisical.git
synced 2025-03-22 15:06:12 +00:00
Compare commits
543 Commits
imported-s
...
create-pul
Author | SHA1 | Date | |
---|---|---|---|
306cf8733e | |||
6e829516db | |||
9a585ad930 | |||
95c1fff7d3 | |||
9c2591f3a6 | |||
a579598b6d | |||
af0d31db2c | |||
fb6c4acf31 | |||
551ca0fa8c | |||
4a0ccbe69e | |||
f5a463ddea | |||
ce1ad6f32e | |||
56c8b4f5e5 | |||
29b26e3158 | |||
6e209bf099 | |||
949d210263 | |||
1a2d8e96f3 | |||
9198eb5fba | |||
0580f37c5e | |||
e53d40f0e5 | |||
801c0c5ada | |||
7b8af89bee | |||
ef7f5c9eac | |||
db0b4a5ad1 | |||
cb505d1525 | |||
c66476e2b4 | |||
60a06edd9b | |||
e8e1d46f0e | |||
038fe3508c | |||
7d1dff9e5a | |||
5117f5d3c1 | |||
350dd97b98 | |||
121902e51f | |||
923bf02046 | |||
27447ddc88 | |||
a3b4b650d1 | |||
3f0f45e853 | |||
3bb50b235d | |||
1afd120e8e | |||
ab3593af37 | |||
2c2afbea7a | |||
4eabbb3ac5 | |||
1ccd74e1a5 | |||
812cced9d5 | |||
cd6be68461 | |||
5c69bbf515 | |||
448f89fd1c | |||
3331699f56 | |||
810f670e64 | |||
5894df4370 | |||
2aacd54116 | |||
73d9fcc0de | |||
7ac3bb20df | |||
d659b5a624 | |||
0bbdf2a8f4 | |||
a8eba9cfbf | |||
a3d7c5f599 | |||
c325674da0 | |||
3637152a6b | |||
8ed3c0cd68 | |||
cdd836d58f | |||
3d3b1eb21a | |||
6aab28c4c7 | |||
f038b28c1c | |||
24a286e898 | |||
0c1103e778 | |||
2c1eecaf85 | |||
5884565de7 | |||
dd43268506 | |||
9d362b8597 | |||
972ecc3e92 | |||
dc3014409f | |||
4e449f62c0 | |||
c911a7cd81 | |||
44370d49e3 | |||
c7d2dfd351 | |||
1785548a40 | |||
2baf9e0739 | |||
01e7ed23ba | |||
1f789110e3 | |||
c874c943c1 | |||
dab69dcb51 | |||
8e82bfae86 | |||
bc810ea567 | |||
22470376d9 | |||
bb9503471f | |||
a687b1d0db | |||
0aa77f90c8 | |||
5a04371fb0 | |||
70c06c91c8 | |||
926d324ae3 | |||
e48377dea9 | |||
5e1484bd05 | |||
6d9de752d7 | |||
f9a9b1222e | |||
4326ce970a | |||
7a3a9ca9ea | |||
32a110e0ca | |||
da5278f6bf | |||
7e765681cb | |||
0990ce1f92 | |||
2369ff6813 | |||
478520f090 | |||
54313f9c08 | |||
cb8763bc9c | |||
c5d11eee7f | |||
8e1d19c041 | |||
608c7a4dee | |||
c7b60bcf0e | |||
6ae62675be | |||
fb2ab200b9 | |||
f1428d72c2 | |||
4cb51805f0 | |||
8c40918cef | |||
3a002b921a | |||
299653528c | |||
8c256bd9c8 | |||
f8e0e01bb8 | |||
b59413ded0 | |||
15c747e8e8 | |||
073a9ee6a4 | |||
d371c568f1 | |||
e6c086ab09 | |||
890c8b89be | |||
6f4b62cfbb | |||
076c70f6ff | |||
aedc1f2441 | |||
352d363bd4 | |||
ac92a916b4 | |||
17587ff1b8 | |||
7f1c8d9ff6 | |||
ac24c0f760 | |||
0e95c1bcee | |||
447630135b | |||
ddd6adf804 | |||
a4b6d2650a | |||
2f5d6b11da | |||
d380b7f788 | |||
7aee4fdfcd | |||
83bd3a0bf4 | |||
1f68730aa3 | |||
7fd1d72985 | |||
b298eec9db | |||
696479a2ef | |||
ad6e2aeb9e | |||
ad405109a0 | |||
992a82015a | |||
317956a038 | |||
5255c4075a | |||
eca36f1993 | |||
7e29a6a656 | |||
f458e34c37 | |||
99f5ed1f4b | |||
f981c59b5c | |||
a528d011c0 | |||
d337118803 | |||
68a11db1c6 | |||
91bf6a6dad | |||
12c655a152 | |||
1d2f10178f | |||
c5cd5047d7 | |||
c9f0ba08e1 | |||
308e605b6c | |||
4d8965eb82 | |||
0357e7c80e | |||
ba1b223655 | |||
3b88a2759b | |||
42383d5643 | |||
d198ba1a79 | |||
b3579cb271 | |||
30ccb78c81 | |||
fdd67c89b3 | |||
79e9b1b2ae | |||
86fd4d5fba | |||
4692aa12bd | |||
61a0997adc | |||
b4f1bec1a9 | |||
ab79342743 | |||
1957531ac4 | |||
61ae0e2fc7 | |||
87b571d6ff | |||
1e6af8ad8f | |||
a771ddf859 | |||
c4cd6909bb | |||
49642480d3 | |||
b667dccc0d | |||
fdda247120 | |||
ee8a88d062 | |||
33349839cd | |||
8f3883c7d4 | |||
38cfb7fd41 | |||
a331eb8dc4 | |||
2dcb409d3b | |||
39bcb73f3d | |||
52189111d7 | |||
f369761920 | |||
8eb22630b6 | |||
d650fd68c0 | |||
387c899193 | |||
37882e6344 | |||
68a1aa6f46 | |||
fa18ca41ac | |||
8485fdc1cd | |||
49ae2386c0 | |||
f2b1f3f0e7 | |||
69aa20e35c | |||
524c7ae78f | |||
e13f7a7486 | |||
1867fb2fc4 | |||
5dd144b97b | |||
b1b430e003 | |||
fb09980413 | |||
3b36cb8b3d | |||
be6a98d0bb | |||
f8e1ed09d2 | |||
5c71116be6 | |||
07cc4fd1ab | |||
ea4ef7f7ef | |||
0482424a1c | |||
74bdbc0724 | |||
a0d5c67456 | |||
db4f4d8f28 | |||
d6f6f51d16 | |||
79a0f3d701 | |||
46912c4c3c | |||
6636377cb5 | |||
26320ddce4 | |||
f5964040d7 | |||
dcaa7f1fce | |||
a4119ee1bb | |||
74f866715f | |||
667f696d26 | |||
5f3938c33d | |||
07845ad6af | |||
17fa72be13 | |||
bf3e93460a | |||
306709cde6 | |||
c41518c822 | |||
f0f2905789 | |||
212a7b49f0 | |||
22e3fcb43c | |||
93b65a1534 | |||
039882e78b | |||
f0f51089fe | |||
447141ab1f | |||
d2ba436338 | |||
ad0d281629 | |||
ce2a9c8640 | |||
ac97f273e3 | |||
69c50af14e | |||
c8638479a8 | |||
8aa75484f3 | |||
66d70f5a25 | |||
8e7cf5f9ac | |||
f9f79cb69e | |||
4235be4be9 | |||
5c3f2e66fd | |||
a37b3ccede | |||
d64eb4b901 | |||
519403023a | |||
b2a976f3d4 | |||
6e882aa46e | |||
bf4db0a9ff | |||
3a3e3a7afc | |||
a7af3a48d9 | |||
cdba78b51d | |||
0c324e804c | |||
47aca3f3e2 | |||
80da2a19aa | |||
858a35812a | |||
31ef1a2183 | |||
66a6f9de71 | |||
6333eccc4a | |||
0af2b113df | |||
63a7941047 | |||
edeac08cb5 | |||
019b0ae09a | |||
1d00bb0a64 | |||
d96f1320ed | |||
50dbefeb48 | |||
56ac2c6780 | |||
c2f16da411 | |||
8223aee2ef | |||
5bd2af9621 | |||
b3df6ce6b5 | |||
e12eb5347d | |||
83a4426d31 | |||
3fd1fbc355 | |||
306d2b4bd9 | |||
c2c66af1f9 | |||
7ae65478aa | |||
b1594e65c6 | |||
0bce5b1daa | |||
207db93483 | |||
972f6a4887 | |||
6e1bece9d9 | |||
63e8bc1845 | |||
4f92663b66 | |||
a66a6790c0 | |||
bde853d280 | |||
acda627236 | |||
875afbb4d6 | |||
56f50a18dc | |||
801c438d05 | |||
baba411502 | |||
4c20ac6564 | |||
4e8556dec2 | |||
2d7b9ec1e4 | |||
8bb9ed4394 | |||
e4246ae85f | |||
f24067542f | |||
a7f5a61f37 | |||
b5fd7698d8 | |||
61c3102573 | |||
d6a5bf9d50 | |||
d0cb06d875 | |||
70f63b3190 | |||
d42f620e1b | |||
2b0670a409 | |||
cc25639157 | |||
5ff30aed10 | |||
656ec4bf16 | |||
0bac9a8e02 | |||
5142e6e5f6 | |||
5c0e5a8ae0 | |||
71e309bbcb | |||
8ff407927c | |||
49c735caf9 | |||
b4de2ea85d | |||
8b8baf1ef2 | |||
2a89b872c5 | |||
2d2d9a5987 | |||
a20a60850b | |||
35e38c23dd | |||
b79e61c86b | |||
e555d3129d | |||
a41883137c | |||
c414bf6c39 | |||
9b782a9da6 | |||
497c0cf63d | |||
93761f37ea | |||
68e530e5d2 | |||
d9005e8665 | |||
20b1cdf909 | |||
4bae65cc55 | |||
6da5f12855 | |||
7a242c4976 | |||
b01d381993 | |||
1ac18fcf0c | |||
8d5ef5f4d9 | |||
35b5253853 | |||
99d59a38d5 | |||
9ab1fce0e0 | |||
9992fbf3dd | |||
3ca596d4af | |||
5e0d64525f | |||
8bcf936b91 | |||
1a2508d91a | |||
e81a77652f | |||
1c95b3abe7 | |||
1f3c72b997 | |||
e55b981cea | |||
49d4e67e07 | |||
a54d156bf0 | |||
f3fc898232 | |||
c61602370e | |||
5178663797 | |||
f04f3aee25 | |||
e5333e2718 | |||
f27d9f8cee | |||
cbd568b714 | |||
b330c5570d | |||
d222bbf131 | |||
961c6391a8 | |||
d68d7df0f8 | |||
c44c7810ce | |||
b7893a6a72 | |||
7a3d425b0e | |||
bd570bd02f | |||
b94ffb8a82 | |||
246b8728a4 | |||
00415e1a87 | |||
ad354c106e | |||
26778d92d3 | |||
b135ba263c | |||
9b7ef55ad7 | |||
872f8bdad8 | |||
80b0dc6895 | |||
b067751027 | |||
f2b3b7b726 | |||
2d51445dd9 | |||
20898c00c6 | |||
2200bd646e | |||
fb69236f47 | |||
918734b26b | |||
729c75112b | |||
738e8cfc5c | |||
1ba7a31e0d | |||
233a4f7d77 | |||
44ff1abd74 | |||
08cb105fe4 | |||
62aebe2fd4 | |||
5c0542c5a3 | |||
6874bff302 | |||
e1b8aa8347 | |||
a041fd4762 | |||
1534ba516a | |||
f7183347dc | |||
105b8d6493 | |||
b9d35058bf | |||
22a3c46902 | |||
be8232dc93 | |||
8c566a5ff7 | |||
0a124093d6 | |||
088cb72621 | |||
de21b44486 | |||
6daeed68a0 | |||
31a499c9cd | |||
04491ee1b7 | |||
ad79ee56e4 | |||
519d6f98a2 | |||
973ed37018 | |||
c72280e9ab | |||
032c5b5620 | |||
aa5cd0fd0f | |||
358ca3decd | |||
5bad4adbdf | |||
0899fdb7d5 | |||
e008fb26a2 | |||
34543ef127 | |||
83107f56bb | |||
35071af478 | |||
eb5f71cb05 | |||
9cf1dd38a6 | |||
144a563609 | |||
ca0062f049 | |||
2ed9aa888e | |||
8c7d329f8f | |||
a0aa06e2f5 | |||
1dd0167ac8 | |||
55aea364da | |||
afee47ab45 | |||
9387d9aaac | |||
2b215a510c | |||
89ff6a6c93 | |||
3bcf406688 | |||
580b86cde8 | |||
7a20251261 | |||
ae63898d5e | |||
d4d3c2b10f | |||
0e3cc4fdeb | |||
b893c3e690 | |||
cee13a0e8b | |||
3745b65148 | |||
a0f0593e2d | |||
ea6e739b46 | |||
12f4868957 | |||
4d43a77f6c | |||
3f3c15d715 | |||
ca453df9e9 | |||
c959fa6fdd | |||
d11ded9abc | |||
714a3186a9 | |||
20d1572220 | |||
21290d8e6c | |||
54fcc23a6c | |||
a339c473d5 | |||
718cabe49b | |||
a087deb1eb | |||
7ce283e891 | |||
52cf38449b | |||
8d6f76698a | |||
71cc84c9a5 | |||
5d95d7f31d | |||
2f15e0e767 | |||
6e1b29025b | |||
1dd451f221 | |||
fcc18996d3 | |||
bcaafcb49f | |||
b4558981c1 | |||
64099908eb | |||
98e0c1b4ca | |||
4050e56e60 | |||
4d1a41e24e | |||
43f676b078 | |||
130ec68288 | |||
c4d5c1a454 | |||
e1407cc093 | |||
1b38d969df | |||
6e3d5a8c7c | |||
e2a447dd05 | |||
2522cc1ede | |||
56876a77e4 | |||
0111ee9efb | |||
581ffc613c | |||
03848b30a2 | |||
5537b00a26 | |||
d71d59e399 | |||
8f8553760a | |||
708c2af979 | |||
fa7587900e | |||
e453ddf937 | |||
3f68807179 | |||
ba42aca069 | |||
22c589e2cf | |||
943945f6d7 | |||
b598dd3d47 | |||
ad6d18a905 | |||
46a91515b1 | |||
b79ce8a880 | |||
d31d98b5e0 | |||
afa1e7e139 | |||
2aea73861b | |||
2002db2007 | |||
4b463c6fde | |||
ab83e61068 | |||
cb6cbafcae | |||
bcb3eaab74 | |||
12d5fb1043 | |||
8bf09789d6 | |||
7ab8db0471 | |||
6b473d2b36 | |||
7581b33b3b | |||
be74f4d34c | |||
e973a62753 | |||
08420cc38d | |||
62aa23a059 | |||
e2967f5e61 | |||
97afc4ff51 | |||
c47a91715f | |||
f9957e111c | |||
1193e33890 | |||
ec64753795 | |||
c908310f6e | |||
ee2b8a594a | |||
3ae27e088f | |||
393c0c9e90 | |||
5e453ab8a6 | |||
273c78c0a5 | |||
1bcc742466 | |||
1fc9e60254 | |||
126e385046 | |||
2f932ad103 |
.github
resources
workflows
backend
.eslintrc.js
e2e-test/routes/v3
package-lock.jsonpackage.jsonsrc
@types
db
migrations
20240330075120_org-memberships-unique-constraint.ts20240414192520_drop-role-roleid-project-membership.ts20240417032913_pending-group-addition.ts20240423023203_ldap-config-groups.ts20240424235842_user-search-filter.ts20240429154610_audit-log-index.ts20240503101144_audit-log-stream.ts20240507032811_trusted-saml-ldap-emails.ts20240507162140_access-approval-policy.ts20240507162141_access
schemas
access-approval-policies-approvers.tsaccess-approval-policies.tsaccess-approval-requests-reviewers.tsaccess-approval-requests.tsaudit-log-streams.tsidentity-project-memberships.tsindex.tsldap-configs.tsldap-group-maps.tsmodels.tsproject-memberships.tssuper-admin.tsuser-aliases.tsuser-group-membership.tsusers.ts
seeds
ee
routes/v1
access-approval-policy-router.tsaccess-approval-request-router.tsaudit-log-stream-router.tsidentity-project-additional-privilege-router.tsindex.tsldap-router.tsproject-role-router.tsproject-router.tssaml-router.tsscim-router.tssnapshot-router.ts
services
access-approval-policy
access-approval-policy-approver-dal.tsaccess-approval-policy-dal.tsaccess-approval-policy-fns.tsaccess-approval-policy-service.tsaccess-approval-policy-types.ts
access-approval-request
access-approval-request-dal.tsaccess-approval-request-fns.tsaccess-approval-request-reviewer-dal.tsaccess-approval-request-service.tsaccess-approval-request-types.ts
audit-log-stream
audit-log
dynamic-secret/providers
group
identity-project-additional-privilege
ldap-config
license
permission
saml-config
scim
secret-approval-request
secret-rotation/secret-rotation-queue
secret-snapshot
lib
server
config
plugins
routes
index.tssanitizedSchemas.ts
v1
admin-router.tsintegration-auth-router.tsintegration-router.tsproject-env-router.tsproject-membership-router.tsproject-router.tssecret-folder-router.tssecret-import-router.ts
v2
identity-org-router.tsorganization-router.tsproject-membership-router.tsproject-router.tsuser-router.ts
v3
services
auth-token
auth
group-project
identity-project
integration-auth
integration-app-list.tsintegration-auth-service.tsintegration-auth-types.tsintegration-sync-secret.ts
integration
org-membership
org
project-membership
project
secret-import
secret
smtp
smtp-service.ts
templates
super-admin
user-alias
user
cli
go.modgo.sum
packages
test
.snapshots
test-TestServiceToken_ExportSecretsWithImportstest-TestServiceToken_ExportSecretsWithoutImportstest-TestServiceToken_GetSecretsByNameRecursivetest-TestServiceToken_GetSecretsByNameWithImportstest-TestServiceToken_GetSecretsByNameWithNotFoundSecrettest-TestServiceToken_RunCmdRecursiveAndImportstest-TestServiceToken_RunCmdWithImportstest-TestServiceToken_RunCmdWithoutImportstest-TestServiceToken_SecretsGetWithImportsAndRecursiveCmdtest-TestServiceToken_SecretsGetWithoutImportsAndWithoutRecursiveCmdtest-TestUniversalAuth_ExportSecretsWithImportstest-TestUniversalAuth_ExportSecretsWithoutImportstest-TestUniversalAuth_GetSecretsByNameRecursivetest-TestUniversalAuth_GetSecretsByNameWithImportstest-TestUniversalAuth_GetSecretsByNameWithNotFoundSecrettest-TestUniversalAuth_RunCmdRecursiveAndImportstest-TestUniversalAuth_RunCmdWithImportstest-TestUniversalAuth_RunCmdWithoutImportstest-TestUniversalAuth_SecretsGetWithImportsAndRecursiveCmdtest-TestUniversalAuth_SecretsGetWithoutImportsAndWithoutRecursiveCmdtest-TestUniversalAuth_SecretsGetWrongEnvironment
export_test.gohelper.gologin_test.gorun_test.gosecrets_by_name_test.gosecrets_test.gocompany
docker-compose.dev.ymldocker-swarm
docs
api-reference/endpoints
cli
documentation
getting-started
guides
platform
images
guides/microsoft-power-apps
integrations
aws
integrations-amplify-env-console-identity.pngintegrations-aws-parameter-store-auth.pngintegrations-aws-parameter-store-create.pngintegrations-aws-secret-manager-create.pngintegrations-aws-secret-manager-options.png
jenkins
platform
audit-log-streams
betterstack-create-source.pngbetterstack-source-details.pngdata-create-api-key.pngdata-dog-api-key.pngdatadog-api-sidebar.pngdatadog-logging-endpoint.pngdatadog-source-details.pngstream-create.pngstream-inputs.pngstream-list.png
dynamic-secrets
dynamic-secret-generate.pngdynamic-secret-lease-empty.pngdynamic-secret-lease-renew.pngdynamic-secret-modal-aws-iam.pngdynamic-secret-modal-cassandra.pngdynamic-secret-modal-mysql.pngdynamic-secret-modal-oracle.pngdynamic-secret-setup-modal-aws-iam.pngdynamic-secret-setup-modal-cassandra.pngdynamic-secret.pnglease-values-aws-iam.pngmodify-cql-statements.pngmodify-sql-statement-mysql.pngmodify-sql-statement-oracle.png
ldap
self-hosting/deployment-options/docker-swarm
integrations
cicd
cloud
frameworks
platforms
internals
mint.jsonself-hosting
configuration
deployment-options
reference-architectures
frontend
package-lock.jsonpackage.jsonconst.ts
public
images/integrations
json
locales/en
src
components
permissions
v2
Badge
Button
Divider
InfisicalSecretInput
SecretPathInput
Select
UpgradeProjectAlert
context/ProjectPermissionContext
hooks/api
accessApproval
admin
auditLogStreams
auth
dynamicSecret
identityProjectAdditionalPrivilege
index.tsxintegrationAuth
integrations
ldapConfig
roles
subscriptions
types.tsusers
layouts/AppLayout
lib/fn
pages
integrations
[id].tsx
aws-parameter-store
aws-secret-manager
cloudflare-pages
cloudflare-workers
gcp-secret-manager
gitlab
hasura-cloud
heroku
render
terraform-cloud
login
org/[id]/overview
project/[id]/secrets/v2
signup
views
IntegrationsPage
IntegrationsPage.tsx
components
CloudIntegrationSection
FrameworkIntegrationSection
InfrastructureIntegrationSection
IntegrationsSection
Login
Org/MembersPage/components
OrgGroupsTab/components/OrgGroupsSection
OrgMembersTab/components/OrgMembersSection
Project/MembersPage/components
IdentityTab/components/IdentityRoleForm
MemberListTab/MemberRoleForm
ServiceTokenTab
SecretApprovalPage
SecretApprovalPage.tsx
components
AccessApprovalPolicyList
AccessApprovalRequest
SecretApprovalPolicyList
SecretApprovalRequest
SecretMainPage/components
ActionBar
CreateSecretForm
DynamicSecretListView
CreateDynamicSecretLease.tsx
EditDynamicSecretForm
SecretDropzone
SecretListView
SnapshotView
SecretOverviewPage/components
SecretRotationPage/components/CreateRotationForm/steps
Settings
OrgSettingsPage/components
AuditLogStreamTab
OrgAuthTab
OrgTabGroup
PersonalSettingsPage/PersonalTabGroup
ProjectSettingsPage
ProjectSettingsPage.tsx
components/EnvironmentSection
Signup
SignupSSO.tsx
components
admin/DashboardPage
helm-charts/infisical-standalone-postgres
pg-migrator/src/models/integration
sink
26
.github/resources/rename_migration_files.py
vendored
Normal file
26
.github/resources/rename_migration_files.py
vendored
Normal file
@ -0,0 +1,26 @@
|
||||
import os
|
||||
from datetime import datetime, timedelta
|
||||
|
||||
def rename_migrations():
|
||||
migration_folder = "./backend/src/db/migrations"
|
||||
with open("added_files.txt", "r") as file:
|
||||
changed_files = file.readlines()
|
||||
|
||||
# Find the latest file among the changed files
|
||||
latest_timestamp = datetime.now() # utc time
|
||||
for file_path in changed_files:
|
||||
file_path = file_path.strip()
|
||||
# each new file bump by 1s
|
||||
latest_timestamp = latest_timestamp + timedelta(seconds=1)
|
||||
|
||||
new_filename = os.path.join(migration_folder, latest_timestamp.strftime("%Y%m%d%H%M%S") + f"_{file_path.split('_')[1]}")
|
||||
old_filename = os.path.join(migration_folder, file_path)
|
||||
os.rename(old_filename, new_filename)
|
||||
print(f"Renamed {old_filename} to {new_filename}")
|
||||
|
||||
if len(changed_files) == 0:
|
||||
print("No new files added to migration folder")
|
||||
|
||||
if __name__ == "__main__":
|
||||
rename_migrations()
|
||||
|
38
.github/workflows/build-patroni-docker-img.yml
vendored
Normal file
38
.github/workflows/build-patroni-docker-img.yml
vendored
Normal file
@ -0,0 +1,38 @@
|
||||
name: Build patroni
|
||||
on: [workflow_dispatch]
|
||||
|
||||
jobs:
|
||||
patroni-image:
|
||||
name: Build patroni
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: ☁️ Checkout source
|
||||
uses: actions/checkout@v3
|
||||
with:
|
||||
repository: 'zalando/patroni'
|
||||
- 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
|
||||
tags: |
|
||||
infisical/patroni:${{ steps.commit.outputs.short }}
|
||||
infisical/patroni:latest
|
||||
platforms: linux/amd64,linux/arm64
|
||||
|
||||
|
@ -5,6 +5,7 @@ on:
|
||||
types: [opened, synchronize]
|
||||
paths:
|
||||
- "backend/src/server/routes/**"
|
||||
- "backend/src/ee/routes/**"
|
||||
|
||||
jobs:
|
||||
check-be-api-changes:
|
||||
|
120
.github/workflows/release_build_infisical_cli.yml
vendored
120
.github/workflows/release_build_infisical_cli.yml
vendored
@ -1,60 +1,72 @@
|
||||
name: Build and release CLI
|
||||
|
||||
on:
|
||||
push:
|
||||
# run only against tags
|
||||
tags:
|
||||
- "infisical-cli/v*.*.*"
|
||||
workflow_dispatch:
|
||||
|
||||
push:
|
||||
# run only against tags
|
||||
tags:
|
||||
- "infisical-cli/v*.*.*"
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
# packages: write
|
||||
# issues: write
|
||||
|
||||
contents: write
|
||||
# packages: write
|
||||
# issues: write
|
||||
jobs:
|
||||
goreleaser:
|
||||
runs-on: ubuntu-20.04
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
with:
|
||||
fetch-depth: 0
|
||||
- name: 🐋 Login to Docker Hub
|
||||
uses: docker/login-action@v2
|
||||
with:
|
||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
- name: 🔧 Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v2
|
||||
- run: git fetch --force --tags
|
||||
- run: echo "Ref name ${{github.ref_name}}"
|
||||
- uses: actions/setup-go@v3
|
||||
with:
|
||||
go-version: ">=1.19.3"
|
||||
cache: true
|
||||
cache-dependency-path: cli/go.sum
|
||||
- name: libssl1.1 => libssl1.0-dev for OSXCross
|
||||
run: |
|
||||
echo 'deb http://security.ubuntu.com/ubuntu bionic-security main' | sudo tee -a /etc/apt/sources.list
|
||||
sudo apt update && apt-cache policy libssl1.0-dev
|
||||
sudo apt-get install libssl1.0-dev
|
||||
- name: OSXCross for CGO Support
|
||||
run: |
|
||||
mkdir ../../osxcross
|
||||
git clone https://github.com/plentico/osxcross-target.git ../../osxcross/target
|
||||
- uses: goreleaser/goreleaser-action@v4
|
||||
with:
|
||||
distribution: goreleaser-pro
|
||||
version: latest
|
||||
args: release --clean
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GO_RELEASER_GITHUB_TOKEN }}
|
||||
POSTHOG_API_KEY_FOR_CLI: ${{ secrets.POSTHOG_API_KEY_FOR_CLI }}
|
||||
FURY_TOKEN: ${{ secrets.FURYPUSHTOKEN }}
|
||||
AUR_KEY: ${{ secrets.AUR_KEY }}
|
||||
GORELEASER_KEY: ${{ secrets.GORELEASER_KEY }}
|
||||
- uses: actions/setup-python@v4
|
||||
- run: pip install --upgrade cloudsmith-cli
|
||||
- name: Publish to CloudSmith
|
||||
run: sh cli/upload_to_cloudsmith.sh
|
||||
env:
|
||||
CLOUDSMITH_API_KEY: ${{ secrets.CLOUDSMITH_API_KEY }}
|
||||
cli-integration-tests:
|
||||
name: Run tests before deployment
|
||||
uses: ./.github/workflows/run-cli-tests.yml
|
||||
secrets:
|
||||
CLI_TESTS_UA_CLIENT_ID: ${{ secrets.CLI_TESTS_UA_CLIENT_ID }}
|
||||
CLI_TESTS_UA_CLIENT_SECRET: ${{ secrets.CLI_TESTS_UA_CLIENT_SECRET }}
|
||||
CLI_TESTS_SERVICE_TOKEN: ${{ secrets.CLI_TESTS_SERVICE_TOKEN }}
|
||||
CLI_TESTS_PROJECT_ID: ${{ secrets.CLI_TESTS_PROJECT_ID }}
|
||||
CLI_TESTS_ENV_SLUG: ${{ secrets.CLI_TESTS_ENV_SLUG }}
|
||||
|
||||
goreleaser:
|
||||
runs-on: ubuntu-20.04
|
||||
needs: [cli-integration-tests]
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
with:
|
||||
fetch-depth: 0
|
||||
- name: 🐋 Login to Docker Hub
|
||||
uses: docker/login-action@v2
|
||||
with:
|
||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
- name: 🔧 Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v2
|
||||
- run: git fetch --force --tags
|
||||
- run: echo "Ref name ${{github.ref_name}}"
|
||||
- uses: actions/setup-go@v3
|
||||
with:
|
||||
go-version: ">=1.19.3"
|
||||
cache: true
|
||||
cache-dependency-path: cli/go.sum
|
||||
- name: libssl1.1 => libssl1.0-dev for OSXCross
|
||||
run: |
|
||||
echo 'deb http://security.ubuntu.com/ubuntu bionic-security main' | sudo tee -a /etc/apt/sources.list
|
||||
sudo apt update && apt-cache policy libssl1.0-dev
|
||||
sudo apt-get install libssl1.0-dev
|
||||
- name: OSXCross for CGO Support
|
||||
run: |
|
||||
mkdir ../../osxcross
|
||||
git clone https://github.com/plentico/osxcross-target.git ../../osxcross/target
|
||||
- uses: goreleaser/goreleaser-action@v4
|
||||
with:
|
||||
distribution: goreleaser-pro
|
||||
version: latest
|
||||
args: release --clean
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GO_RELEASER_GITHUB_TOKEN }}
|
||||
POSTHOG_API_KEY_FOR_CLI: ${{ secrets.POSTHOG_API_KEY_FOR_CLI }}
|
||||
FURY_TOKEN: ${{ secrets.FURYPUSHTOKEN }}
|
||||
AUR_KEY: ${{ secrets.AUR_KEY }}
|
||||
GORELEASER_KEY: ${{ secrets.GORELEASER_KEY }}
|
||||
- uses: actions/setup-python@v4
|
||||
- run: pip install --upgrade cloudsmith-cli
|
||||
- name: Publish to CloudSmith
|
||||
run: sh cli/upload_to_cloudsmith.sh
|
||||
env:
|
||||
CLOUDSMITH_API_KEY: ${{ secrets.CLOUDSMITH_API_KEY }}
|
||||
|
47
.github/workflows/run-cli-tests.yml
vendored
Normal file
47
.github/workflows/run-cli-tests.yml
vendored
Normal file
@ -0,0 +1,47 @@
|
||||
name: Go CLI Tests
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
types: [opened, synchronize]
|
||||
paths:
|
||||
- "cli/**"
|
||||
|
||||
workflow_dispatch:
|
||||
|
||||
workflow_call:
|
||||
secrets:
|
||||
CLI_TESTS_UA_CLIENT_ID:
|
||||
required: true
|
||||
CLI_TESTS_UA_CLIENT_SECRET:
|
||||
required: true
|
||||
CLI_TESTS_SERVICE_TOKEN:
|
||||
required: true
|
||||
CLI_TESTS_PROJECT_ID:
|
||||
required: true
|
||||
CLI_TESTS_ENV_SLUG:
|
||||
required: true
|
||||
|
||||
jobs:
|
||||
test:
|
||||
defaults:
|
||||
run:
|
||||
working-directory: ./cli
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: Setup Go
|
||||
uses: actions/setup-go@v4
|
||||
with:
|
||||
go-version: "1.21.x"
|
||||
- name: Install dependencies
|
||||
run: go get .
|
||||
- name: Test with the Go CLI
|
||||
env:
|
||||
CLI_TESTS_UA_CLIENT_ID: ${{ secrets.CLI_TESTS_UA_CLIENT_ID }}
|
||||
CLI_TESTS_UA_CLIENT_SECRET: ${{ secrets.CLI_TESTS_UA_CLIENT_SECRET }}
|
||||
CLI_TESTS_SERVICE_TOKEN: ${{ secrets.CLI_TESTS_SERVICE_TOKEN }}
|
||||
CLI_TESTS_PROJECT_ID: ${{ secrets.CLI_TESTS_PROJECT_ID }}
|
||||
CLI_TESTS_ENV_SLUG: ${{ secrets.CLI_TESTS_ENV_SLUG }}
|
||||
|
||||
run: go test -v -count=1 ./test
|
48
.github/workflows/update-be-new-migration-latest-timestamp.yml
vendored
Normal file
48
.github/workflows/update-be-new-migration-latest-timestamp.yml
vendored
Normal file
@ -0,0 +1,48 @@
|
||||
name: Rename Migrations
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
types: [closed]
|
||||
paths:
|
||||
- 'backend/src/db/migrations/**'
|
||||
|
||||
jobs:
|
||||
rename:
|
||||
runs-on: ubuntu-latest
|
||||
if: github.event.pull_request.merged == true
|
||||
|
||||
steps:
|
||||
- name: Check out repository
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Get list of newly added files in migration folder
|
||||
run: |
|
||||
git diff --name-status HEAD^ HEAD backend/src/db/migrations | grep '^A' | cut -f2 | xargs -n1 basename > added_files.txt
|
||||
if [ ! -s added_files.txt ]; then
|
||||
echo "No new files added. Skipping"
|
||||
echo "SKIP_RENAME=true" >> $GITHUB_ENV
|
||||
fi
|
||||
|
||||
- name: Script to rename migrations
|
||||
if: env.SKIP_RENAME != 'true'
|
||||
run: python .github/resources/rename_migration_files.py
|
||||
|
||||
- name: Commit and push changes
|
||||
if: env.SKIP_RENAME != 'true'
|
||||
run: |
|
||||
git config user.name github-actions
|
||||
git config user.email github-actions@github.com
|
||||
git add ./backend/src/db/migrations
|
||||
rm added_files.txt
|
||||
git commit -m "chore: renamed new migration files to latest timestamp (gh-action)"
|
||||
|
||||
- name: Create Pull Request
|
||||
if: env.SKIP_RENAME != 'true'
|
||||
uses: peter-evans/create-pull-request@v6
|
||||
with:
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
commit-message: 'chore: renamed new migration files to latest UTC (gh-action)'
|
||||
title: 'GH Action: rename new migration file timestamp'
|
||||
branch-suffix: timestamp
|
2
.gitignore
vendored
2
.gitignore
vendored
@ -67,3 +67,5 @@ yarn-error.log*
|
||||
frontend-build
|
||||
|
||||
*.tgz
|
||||
cli/infisical-merge
|
||||
cli/test/infisical-merge
|
||||
|
@ -2,4 +2,6 @@
|
||||
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
|
||||
frontend/src/views/Project/MembersPage/components/MemberListTab/MemberRoleForm/SpecificPrivilegeSection.tsx:generic-api-key:292
|
||||
docs/self-hosting/configuration/envars.mdx:generic-api-key:106
|
||||
frontend/src/views/Project/MembersPage/components/MemberListTab/MemberRoleForm/SpecificPrivilegeSection.tsx:generic-api-key:451
|
||||
|
@ -76,7 +76,7 @@ Check out the [Quickstart Guides](https://infisical.com/docs/getting-started/int
|
||||
|
||||
| Use Infisical Cloud | Deploy Infisical on premise |
|
||||
| ------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| The fastest and most reliable way to <br> get started with Infisical is signing up <br> for free to [Infisical Cloud](https://app.infisical.com/login). | <a href="https://infisical.com/docs/self-hosting/deployment-options/aws-ec2"><img src=".github/images/deploy-to-aws.png" width="150" width="300" /></a> <a href="https://infisical.com/docs/self-hosting/deployment-options/digital-ocean-marketplace" alt="Deploy to DigitalOcean"> <img width="217" alt="Deploy to DO" src="https://www.deploytodo.com/do-btn-blue.svg"/> </a> <br> View all [deployment options](https://infisical.com/docs/self-hosting/overview) |
|
||||
| The fastest and most reliable way to <br> get started with Infisical is signing up <br> for free to [Infisical Cloud](https://app.infisical.com/login). | <br> View all [deployment options](https://infisical.com/docs/self-hosting/overview) |
|
||||
|
||||
### Run Infisical locally
|
||||
|
||||
|
@ -23,16 +23,17 @@ module.exports = {
|
||||
root: true,
|
||||
overrides: [
|
||||
{
|
||||
files: ["./e2e-test/**/*"],
|
||||
files: ["./e2e-test/**/*", "./src/db/migrations/**/*"],
|
||||
rules: {
|
||||
"@typescript-eslint/no-unsafe-member-access": "off",
|
||||
"@typescript-eslint/no-unsafe-assignment": "off",
|
||||
"@typescript-eslint/no-unsafe-argument": "off",
|
||||
"@typescript-eslint/no-unsafe-return": "off",
|
||||
"@typescript-eslint/no-unsafe-call": "off",
|
||||
"@typescript-eslint/no-unsafe-call": "off"
|
||||
}
|
||||
}
|
||||
],
|
||||
|
||||
rules: {
|
||||
"@typescript-eslint/no-empty-function": "off",
|
||||
"@typescript-eslint/no-unsafe-enum-comparison": "off",
|
||||
|
@ -942,6 +942,113 @@ describe.each([{ auth: AuthMode.JWT }, { auth: AuthMode.IDENTITY_ACCESS_TOKEN }]
|
||||
const secrets = await getSecrets(seedData1.environment.slug, path);
|
||||
expect(secrets).toEqual([]);
|
||||
});
|
||||
|
||||
test.each(testRawSecrets)("Bulk create secret raw in path $path", async ({ path, secret }) => {
|
||||
const createSecretReqBody = {
|
||||
projectSlug: seedData1.project.slug,
|
||||
environment: seedData1.environment.slug,
|
||||
secretPath: path,
|
||||
secrets: [
|
||||
{
|
||||
secretKey: secret.key,
|
||||
secretValue: secret.value,
|
||||
secretComment: secret.comment
|
||||
}
|
||||
]
|
||||
};
|
||||
const createSecRes = await testServer.inject({
|
||||
method: "POST",
|
||||
url: `/api/v3/secrets/batch/raw`,
|
||||
headers: {
|
||||
authorization: `Bearer ${authToken}`
|
||||
},
|
||||
body: createSecretReqBody
|
||||
});
|
||||
expect(createSecRes.statusCode).toBe(200);
|
||||
const createdSecretPayload = JSON.parse(createSecRes.payload);
|
||||
expect(createdSecretPayload).toHaveProperty("secrets");
|
||||
|
||||
// fetch secrets
|
||||
const secrets = await getSecrets(seedData1.environment.slug, path);
|
||||
expect(secrets).toEqual(
|
||||
expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
key: secret.key,
|
||||
value: secret.value,
|
||||
type: SecretType.Shared
|
||||
})
|
||||
])
|
||||
);
|
||||
|
||||
await deleteRawSecret({ path, key: secret.key });
|
||||
});
|
||||
|
||||
test.each(testRawSecrets)("Bulk update secret raw in path $path", async ({ secret, path }) => {
|
||||
await createRawSecret({ path, ...secret });
|
||||
const updateSecretReqBody = {
|
||||
projectSlug: seedData1.project.slug,
|
||||
environment: seedData1.environment.slug,
|
||||
secretPath: path,
|
||||
secrets: [
|
||||
{
|
||||
secretValue: "new-value",
|
||||
secretKey: secret.key
|
||||
}
|
||||
]
|
||||
};
|
||||
const updateSecRes = await testServer.inject({
|
||||
method: "PATCH",
|
||||
url: `/api/v3/secrets/batch/raw`,
|
||||
headers: {
|
||||
authorization: `Bearer ${authToken}`
|
||||
},
|
||||
body: updateSecretReqBody
|
||||
});
|
||||
expect(updateSecRes.statusCode).toBe(200);
|
||||
const updatedSecretPayload = JSON.parse(updateSecRes.payload);
|
||||
expect(updatedSecretPayload).toHaveProperty("secrets");
|
||||
|
||||
// fetch secrets
|
||||
const secrets = await getSecrets(seedData1.environment.slug, path);
|
||||
expect(secrets).toEqual(
|
||||
expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
key: secret.key,
|
||||
value: "new-value",
|
||||
version: 2,
|
||||
type: SecretType.Shared
|
||||
})
|
||||
])
|
||||
);
|
||||
|
||||
await deleteRawSecret({ path, key: secret.key });
|
||||
});
|
||||
|
||||
test.each(testRawSecrets)("Bulk delete secret raw in path $path", async ({ path, secret }) => {
|
||||
await createRawSecret({ path, ...secret });
|
||||
|
||||
const deletedSecretReqBody = {
|
||||
projectSlug: seedData1.project.slug,
|
||||
environment: seedData1.environment.slug,
|
||||
secretPath: path,
|
||||
secrets: [{ secretKey: secret.key }]
|
||||
};
|
||||
const deletedSecRes = await testServer.inject({
|
||||
method: "DELETE",
|
||||
url: `/api/v3/secrets/batch/raw`,
|
||||
headers: {
|
||||
authorization: `Bearer ${authToken}`
|
||||
},
|
||||
body: deletedSecretReqBody
|
||||
});
|
||||
expect(deletedSecRes.statusCode).toBe(200);
|
||||
const deletedSecretPayload = JSON.parse(deletedSecRes.payload);
|
||||
expect(deletedSecretPayload).toHaveProperty("secrets");
|
||||
|
||||
// fetch secrets
|
||||
const secrets = await getSecrets(seedData1.environment.slug, path);
|
||||
expect(secrets).toEqual([]);
|
||||
});
|
||||
}
|
||||
);
|
||||
|
||||
|
593
backend/package-lock.json
generated
593
backend/package-lock.json
generated
@ -35,6 +35,7 @@
|
||||
"axios-retry": "^4.0.0",
|
||||
"bcrypt": "^5.1.1",
|
||||
"bullmq": "^5.3.3",
|
||||
"cassandra-driver": "^4.7.2",
|
||||
"dotenv": "^16.4.1",
|
||||
"fastify": "^4.26.0",
|
||||
"fastify-plugin": "^4.5.1",
|
||||
@ -44,13 +45,15 @@
|
||||
"jsonwebtoken": "^9.0.2",
|
||||
"jsrp": "^0.2.4",
|
||||
"knex": "^3.0.1",
|
||||
"ldapjs": "^3.0.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",
|
||||
"oracledb": "^6.4.0",
|
||||
"passport-github": "^1.1.0",
|
||||
"passport-gitlab2": "^5.0.0",
|
||||
"passport-google-oauth20": "^2.0.0",
|
||||
@ -1708,6 +1711,22 @@
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/aix-ppc64": {
|
||||
"version": "0.20.2",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.20.2.tgz",
|
||||
"integrity": "sha512-D+EBOJHXdNZcLJRBkhENNG8Wji2kgc9AZ9KiPr1JuZjsNtyHzrsfLRrY0tk2H2aoFu6RANO1y1iPPUCDYWkb5g==",
|
||||
"cpu": [
|
||||
"ppc64"
|
||||
],
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"os": [
|
||||
"aix"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/android-arm": {
|
||||
"version": "0.18.20",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.18.20.tgz",
|
||||
@ -2492,6 +2511,83 @@
|
||||
"@jridgewell/sourcemap-codec": "^1.4.10"
|
||||
}
|
||||
},
|
||||
"node_modules/@ldapjs/asn1": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/@ldapjs/asn1/-/asn1-2.0.0.tgz",
|
||||
"integrity": "sha512-G9+DkEOirNgdPmD0I8nu57ygQJKOOgFEMKknEuQvIHbGLwP3ny1mY+OTUYLCbCaGJP4sox5eYgBJRuSUpnAddA=="
|
||||
},
|
||||
"node_modules/@ldapjs/attribute": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/@ldapjs/attribute/-/attribute-1.0.0.tgz",
|
||||
"integrity": "sha512-ptMl2d/5xJ0q+RgmnqOi3Zgwk/TMJYG7dYMC0Keko+yZU6n+oFM59MjQOUht5pxJeS4FWrImhu/LebX24vJNRQ==",
|
||||
"dependencies": {
|
||||
"@ldapjs/asn1": "2.0.0",
|
||||
"@ldapjs/protocol": "^1.2.1",
|
||||
"process-warning": "^2.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@ldapjs/change": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/@ldapjs/change/-/change-1.0.0.tgz",
|
||||
"integrity": "sha512-EOQNFH1RIku3M1s0OAJOzGfAohuFYXFY4s73wOhRm4KFGhmQQ7MChOh2YtYu9Kwgvuq1B0xKciXVzHCGkB5V+Q==",
|
||||
"dependencies": {
|
||||
"@ldapjs/asn1": "2.0.0",
|
||||
"@ldapjs/attribute": "1.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@ldapjs/controls": {
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmjs.org/@ldapjs/controls/-/controls-2.1.0.tgz",
|
||||
"integrity": "sha512-2pFdD1yRC9V9hXfAWvCCO2RRWK9OdIEcJIos/9cCVP9O4k72BY1bLDQQ4KpUoJnl4y/JoD4iFgM+YWT3IfITWw==",
|
||||
"dependencies": {
|
||||
"@ldapjs/asn1": "^1.2.0",
|
||||
"@ldapjs/protocol": "^1.2.1"
|
||||
}
|
||||
},
|
||||
"node_modules/@ldapjs/controls/node_modules/@ldapjs/asn1": {
|
||||
"version": "1.2.0",
|
||||
"resolved": "https://registry.npmjs.org/@ldapjs/asn1/-/asn1-1.2.0.tgz",
|
||||
"integrity": "sha512-KX/qQJ2xxzvO2/WOvr1UdQ+8P5dVvuOLk/C9b1bIkXxZss8BaR28njXdPgFCpj5aHaf1t8PmuVnea+N9YG9YMw=="
|
||||
},
|
||||
"node_modules/@ldapjs/dn": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/@ldapjs/dn/-/dn-1.1.0.tgz",
|
||||
"integrity": "sha512-R72zH5ZeBj/Fujf/yBu78YzpJjJXG46YHFo5E4W1EqfNpo1UsVPqdLrRMXeKIsJT3x9dJVIfR6OpzgINlKpi0A==",
|
||||
"dependencies": {
|
||||
"@ldapjs/asn1": "2.0.0",
|
||||
"process-warning": "^2.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@ldapjs/filter": {
|
||||
"version": "2.1.1",
|
||||
"resolved": "https://registry.npmjs.org/@ldapjs/filter/-/filter-2.1.1.tgz",
|
||||
"integrity": "sha512-TwPK5eEgNdUO1ABPBUQabcZ+h9heDORE4V9WNZqCtYLKc06+6+UAJ3IAbr0L0bYTnkkWC/JEQD2F+zAFsuikNw==",
|
||||
"dependencies": {
|
||||
"@ldapjs/asn1": "2.0.0",
|
||||
"@ldapjs/protocol": "^1.2.1",
|
||||
"process-warning": "^2.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@ldapjs/messages": {
|
||||
"version": "1.3.0",
|
||||
"resolved": "https://registry.npmjs.org/@ldapjs/messages/-/messages-1.3.0.tgz",
|
||||
"integrity": "sha512-K7xZpXJ21bj92jS35wtRbdcNrwmxAtPwy4myeh9duy/eR3xQKvikVycbdWVzkYEAVE5Ce520VXNOwCHjomjCZw==",
|
||||
"dependencies": {
|
||||
"@ldapjs/asn1": "^2.0.0",
|
||||
"@ldapjs/attribute": "^1.0.0",
|
||||
"@ldapjs/change": "^1.0.0",
|
||||
"@ldapjs/controls": "^2.1.0",
|
||||
"@ldapjs/dn": "^1.1.0",
|
||||
"@ldapjs/filter": "^2.1.1",
|
||||
"@ldapjs/protocol": "^1.2.1",
|
||||
"process-warning": "^2.2.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@ldapjs/protocol": {
|
||||
"version": "1.2.1",
|
||||
"resolved": "https://registry.npmjs.org/@ldapjs/protocol/-/protocol-1.2.1.tgz",
|
||||
"integrity": "sha512-O89xFDLW2gBoZWNXuXpBSM32/KealKCTb3JGtJdtUQc7RjAk8XzrRgyz02cPAwGKwKPxy0ivuC7UP9bmN87egQ=="
|
||||
},
|
||||
"node_modules/@lukeed/ms": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/@lukeed/ms/-/ms-2.0.1.tgz",
|
||||
@ -3162,9 +3258,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@rollup/rollup-android-arm-eabi": {
|
||||
"version": "4.8.0",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.8.0.tgz",
|
||||
"integrity": "sha512-zdTObFRoNENrdPpnTNnhOljYIcOX7aI7+7wyrSpPFFIOf/nRdedE6IYsjaBE7tjukphh1tMTojgJ7p3lKY8x6Q==",
|
||||
"version": "4.14.3",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.14.3.tgz",
|
||||
"integrity": "sha512-X9alQ3XM6I9IlSlmC8ddAvMSyG1WuHk5oUnXGw+yUBs3BFoTizmG1La/Gr8fVJvDWAq+zlYTZ9DBgrlKRVY06g==",
|
||||
"cpu": [
|
||||
"arm"
|
||||
],
|
||||
@ -3175,9 +3271,9 @@
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-android-arm64": {
|
||||
"version": "4.8.0",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.8.0.tgz",
|
||||
"integrity": "sha512-aiItwP48BiGpMFS9Znjo/xCNQVwTQVcRKkFKsO81m8exrGjHkCBDvm9PHay2kpa8RPnZzzKcD1iQ9KaLY4fPQQ==",
|
||||
"version": "4.14.3",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.14.3.tgz",
|
||||
"integrity": "sha512-eQK5JIi+POhFpzk+LnjKIy4Ks+pwJ+NXmPxOCSvOKSNRPONzKuUvWE+P9JxGZVxrtzm6BAYMaL50FFuPe0oWMQ==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
@ -3188,9 +3284,9 @@
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-darwin-arm64": {
|
||||
"version": "4.8.0",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.8.0.tgz",
|
||||
"integrity": "sha512-zhNIS+L4ZYkYQUjIQUR6Zl0RXhbbA0huvNIWjmPc2SL0cB1h5Djkcy+RZ3/Bwszfb6vgwUvcVJYD6e6Zkpsi8g==",
|
||||
"version": "4.14.3",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.14.3.tgz",
|
||||
"integrity": "sha512-Od4vE6f6CTT53yM1jgcLqNfItTsLt5zE46fdPaEmeFHvPs5SjZYlLpHrSiHEKR1+HdRfxuzXHjDOIxQyC3ptBA==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
@ -3201,9 +3297,9 @@
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-darwin-x64": {
|
||||
"version": "4.8.0",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.8.0.tgz",
|
||||
"integrity": "sha512-A/FAHFRNQYrELrb/JHncRWzTTXB2ticiRFztP4ggIUAfa9Up1qfW8aG2w/mN9jNiZ+HB0t0u0jpJgFXG6BfRTA==",
|
||||
"version": "4.14.3",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.14.3.tgz",
|
||||
"integrity": "sha512-0IMAO21axJeNIrvS9lSe/PGthc8ZUS+zC53O0VhF5gMxfmcKAP4ESkKOCwEi6u2asUrt4mQv2rjY8QseIEb1aw==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
@ -3214,9 +3310,22 @@
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-linux-arm-gnueabihf": {
|
||||
"version": "4.8.0",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.8.0.tgz",
|
||||
"integrity": "sha512-JsidBnh3p2IJJA4/2xOF2puAYqbaczB3elZDT0qHxn362EIoIkq7hrR43Xa8RisgI6/WPfvb2umbGsuvf7E37A==",
|
||||
"version": "4.14.3",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.14.3.tgz",
|
||||
"integrity": "sha512-ge2DC7tHRHa3caVEoSbPRJpq7azhG+xYsd6u2MEnJ6XzPSzQsTKyXvh6iWjXRf7Rt9ykIUWHtl0Uz3T6yXPpKw==",
|
||||
"cpu": [
|
||||
"arm"
|
||||
],
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-linux-arm-musleabihf": {
|
||||
"version": "4.14.3",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.14.3.tgz",
|
||||
"integrity": "sha512-ljcuiDI4V3ySuc7eSk4lQ9wU8J8r8KrOUvB2U+TtK0TiW6OFDmJ+DdIjjwZHIw9CNxzbmXY39wwpzYuFDwNXuw==",
|
||||
"cpu": [
|
||||
"arm"
|
||||
],
|
||||
@ -3227,9 +3336,9 @@
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-linux-arm64-gnu": {
|
||||
"version": "4.8.0",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.8.0.tgz",
|
||||
"integrity": "sha512-hBNCnqw3EVCkaPB0Oqd24bv8SklETptQWcJz06kb9OtiShn9jK1VuTgi7o4zPSt6rNGWQOTDEAccbk0OqJmS+g==",
|
||||
"version": "4.14.3",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.14.3.tgz",
|
||||
"integrity": "sha512-Eci2us9VTHm1eSyn5/eEpaC7eP/mp5n46gTRB3Aar3BgSvDQGJZuicyq6TsH4HngNBgVqC5sDYxOzTExSU+NjA==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
@ -3240,9 +3349,9 @@
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-linux-arm64-musl": {
|
||||
"version": "4.8.0",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.8.0.tgz",
|
||||
"integrity": "sha512-Fw9ChYfJPdltvi9ALJ9wzdCdxGw4wtq4t1qY028b2O7GwB5qLNSGtqMsAel1lfWTZvf4b6/+4HKp0GlSYg0ahA==",
|
||||
"version": "4.14.3",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.14.3.tgz",
|
||||
"integrity": "sha512-UrBoMLCq4E92/LCqlh+blpqMz5h1tJttPIniwUgOFJyjWI1qrtrDhhpHPuFxULlUmjFHfloWdixtDhSxJt5iKw==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
@ -3252,10 +3361,23 @@
|
||||
"linux"
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-linux-powerpc64le-gnu": {
|
||||
"version": "4.14.3",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-powerpc64le-gnu/-/rollup-linux-powerpc64le-gnu-4.14.3.tgz",
|
||||
"integrity": "sha512-5aRjvsS8q1nWN8AoRfrq5+9IflC3P1leMoy4r2WjXyFqf3qcqsxRCfxtZIV58tCxd+Yv7WELPcO9mY9aeQyAmw==",
|
||||
"cpu": [
|
||||
"ppc64"
|
||||
],
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-linux-riscv64-gnu": {
|
||||
"version": "4.8.0",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.8.0.tgz",
|
||||
"integrity": "sha512-BH5xIh7tOzS9yBi8dFrCTG8Z6iNIGWGltd3IpTSKp6+pNWWO6qy8eKoRxOtwFbMrid5NZaidLYN6rHh9aB8bEw==",
|
||||
"version": "4.14.3",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.14.3.tgz",
|
||||
"integrity": "sha512-sk/Qh1j2/RJSX7FhEpJn8n0ndxy/uf0kI/9Zc4b1ELhqULVdTfN6HL31CDaTChiBAOgLcsJ1sgVZjWv8XNEsAQ==",
|
||||
"cpu": [
|
||||
"riscv64"
|
||||
],
|
||||
@ -3265,10 +3387,23 @@
|
||||
"linux"
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-linux-s390x-gnu": {
|
||||
"version": "4.14.3",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.14.3.tgz",
|
||||
"integrity": "sha512-jOO/PEaDitOmY9TgkxF/TQIjXySQe5KVYB57H/8LRP/ux0ZoO8cSHCX17asMSv3ruwslXW/TLBcxyaUzGRHcqg==",
|
||||
"cpu": [
|
||||
"s390x"
|
||||
],
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-linux-x64-gnu": {
|
||||
"version": "4.8.0",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.8.0.tgz",
|
||||
"integrity": "sha512-PmvAj8k6EuWiyLbkNpd6BLv5XeYFpqWuRvRNRl80xVfpGXK/z6KYXmAgbI4ogz7uFiJxCnYcqyvZVD0dgFog7Q==",
|
||||
"version": "4.14.3",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.14.3.tgz",
|
||||
"integrity": "sha512-8ybV4Xjy59xLMyWo3GCfEGqtKV5M5gCSrZlxkPGvEPCGDLNla7v48S662HSGwRd6/2cSneMQWiv+QzcttLrrOA==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
@ -3279,9 +3414,9 @@
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-linux-x64-musl": {
|
||||
"version": "4.8.0",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.8.0.tgz",
|
||||
"integrity": "sha512-mdxnlW2QUzXwY+95TuxZ+CurrhgrPAMveDWI97EQlA9bfhR8tw3Pt7SUlc/eSlCNxlWktpmT//EAA8UfCHOyXg==",
|
||||
"version": "4.14.3",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.14.3.tgz",
|
||||
"integrity": "sha512-s+xf1I46trOY10OqAtZ5Rm6lzHre/UiLA1J2uOhCFXWkbZrJRkYBPO6FhvGfHmdtQ3Bx793MNa7LvoWFAm93bg==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
@ -3292,9 +3427,9 @@
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-win32-arm64-msvc": {
|
||||
"version": "4.8.0",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.8.0.tgz",
|
||||
"integrity": "sha512-ge7saUz38aesM4MA7Cad8CHo0Fyd1+qTaqoIo+Jtk+ipBi4ATSrHWov9/S4u5pbEQmLjgUjB7BJt+MiKG2kzmA==",
|
||||
"version": "4.14.3",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.14.3.tgz",
|
||||
"integrity": "sha512-+4h2WrGOYsOumDQ5S2sYNyhVfrue+9tc9XcLWLh+Kw3UOxAvrfOrSMFon60KspcDdytkNDh7K2Vs6eMaYImAZg==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
@ -3305,9 +3440,9 @@
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-win32-ia32-msvc": {
|
||||
"version": "4.8.0",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.8.0.tgz",
|
||||
"integrity": "sha512-p9E3PZlzurhlsN5h9g7zIP1DnqKXJe8ZUkFwAazqSvHuWfihlIISPxG9hCHCoA+dOOspL/c7ty1eeEVFTE0UTw==",
|
||||
"version": "4.14.3",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.14.3.tgz",
|
||||
"integrity": "sha512-T1l7y/bCeL/kUwh9OD4PQT4aM7Bq43vX05htPJJ46RTI4r5KNt6qJRzAfNfM+OYMNEVBWQzR2Gyk+FXLZfogGw==",
|
||||
"cpu": [
|
||||
"ia32"
|
||||
],
|
||||
@ -3318,9 +3453,9 @@
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-win32-x64-msvc": {
|
||||
"version": "4.8.0",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.8.0.tgz",
|
||||
"integrity": "sha512-kb4/auKXkYKqlUYTE8s40FcJIj5soOyRLHKd4ugR0dCq0G2EfcF54eYcfQiGkHzjidZ40daB4ulsFdtqNKZtBg==",
|
||||
"version": "4.14.3",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.14.3.tgz",
|
||||
"integrity": "sha512-/BypzV0H1y1HzgYpxqRaXGBRqfodgoBBCcsrujT6QRcakDQdfU+Lq9PENPh5jB4I44YWq+0C2eHsHya+nZY1sA==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
@ -4509,6 +4644,15 @@
|
||||
"@types/lodash": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/long": {
|
||||
"version": "5.0.0",
|
||||
"resolved": "https://registry.npmjs.org/@types/long/-/long-5.0.0.tgz",
|
||||
"integrity": "sha512-eQs9RsucA/LNjnMoJvWG/nXa7Pot/RbBzilF/QRIU/xRl+0ApxrSUFsV5lmf01SvSlqMzJ7Zwxe440wmz2SJGA==",
|
||||
"deprecated": "This is a stub types definition. long provides its own type definitions, so you do not need this installed.",
|
||||
"dependencies": {
|
||||
"long": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/mime": {
|
||||
"version": "1.3.5",
|
||||
"resolved": "https://registry.npmjs.org/@types/mime/-/mime-1.3.5.tgz",
|
||||
@ -5257,6 +5401,14 @@
|
||||
"node": ">=0.4.0"
|
||||
}
|
||||
},
|
||||
"node_modules/adm-zip": {
|
||||
"version": "0.5.12",
|
||||
"resolved": "https://registry.npmjs.org/adm-zip/-/adm-zip-0.5.12.tgz",
|
||||
"integrity": "sha512-6TVU49mK6KZb4qG6xWaaM4C7sA/sgUMLy/JYMOzkcp3BvVLpW0fXDFQiIzAuxFCt/2+xD7fNIiPFAoLZPhVNLQ==",
|
||||
"engines": {
|
||||
"node": ">=6.0"
|
||||
}
|
||||
},
|
||||
"node_modules/agent-base": {
|
||||
"version": "6.0.2",
|
||||
"resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz",
|
||||
@ -5916,12 +6068,12 @@
|
||||
}
|
||||
},
|
||||
"node_modules/body-parser": {
|
||||
"version": "1.20.1",
|
||||
"resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.1.tgz",
|
||||
"integrity": "sha512-jWi7abTbYwajOytWCQc37VulmWiRae5RyTpaCyDcS5/lMdtwSz5lOpDE67srw/HYe35f1z3fDQw+3txg7gNtWw==",
|
||||
"version": "1.20.2",
|
||||
"resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.2.tgz",
|
||||
"integrity": "sha512-ml9pReCu3M61kGlqoTm2umSXTlRTuGTx0bfYj+uIUKKYycG5NtSbeetV3faSU6R7ajOPw0g/J1PvK4qNy7s5bA==",
|
||||
"dependencies": {
|
||||
"bytes": "3.1.2",
|
||||
"content-type": "~1.0.4",
|
||||
"content-type": "~1.0.5",
|
||||
"debug": "2.6.9",
|
||||
"depd": "2.0.0",
|
||||
"destroy": "1.2.0",
|
||||
@ -5929,7 +6081,7 @@
|
||||
"iconv-lite": "0.4.24",
|
||||
"on-finished": "2.4.1",
|
||||
"qs": "6.11.0",
|
||||
"raw-body": "2.5.1",
|
||||
"raw-body": "2.5.2",
|
||||
"type-is": "~1.6.18",
|
||||
"unpipe": "1.0.0"
|
||||
},
|
||||
@ -6134,6 +6286,20 @@
|
||||
"node": ">=6"
|
||||
}
|
||||
},
|
||||
"node_modules/cassandra-driver": {
|
||||
"version": "4.7.2",
|
||||
"resolved": "https://registry.npmjs.org/cassandra-driver/-/cassandra-driver-4.7.2.tgz",
|
||||
"integrity": "sha512-gwl1DeYvL8Wy3i1GDMzFtpUg5G473fU7EnHFZj7BUtdLB7loAfgZgB3zBhROc9fbaDSUDs6YwOPPojS5E1kbSA==",
|
||||
"dependencies": {
|
||||
"@types/long": "~5.0.0",
|
||||
"@types/node": ">=8",
|
||||
"adm-zip": "~0.5.10",
|
||||
"long": "~5.2.3"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=16"
|
||||
}
|
||||
},
|
||||
"node_modules/chai": {
|
||||
"version": "4.4.1",
|
||||
"resolved": "https://registry.npmjs.org/chai/-/chai-4.4.1.tgz",
|
||||
@ -7379,16 +7545,16 @@
|
||||
}
|
||||
},
|
||||
"node_modules/express": {
|
||||
"version": "4.18.2",
|
||||
"resolved": "https://registry.npmjs.org/express/-/express-4.18.2.tgz",
|
||||
"integrity": "sha512-5/PsL6iGPdfQ/lKM1UuielYgv3BUoJfz1aUwU9vHZ+J7gyvwdQXFEBIEIaxeGf0GIcreATNyBExtalisDbuMqQ==",
|
||||
"version": "4.19.2",
|
||||
"resolved": "https://registry.npmjs.org/express/-/express-4.19.2.tgz",
|
||||
"integrity": "sha512-5T6nhjsT+EOMzuck8JjBHARTHfMht0POzlA60WV2pMD3gyXw2LZnZ+ueGdNxG+0calOJcWKbpFcuzLZ91YWq9Q==",
|
||||
"dependencies": {
|
||||
"accepts": "~1.3.8",
|
||||
"array-flatten": "1.1.1",
|
||||
"body-parser": "1.20.1",
|
||||
"body-parser": "1.20.2",
|
||||
"content-disposition": "0.5.4",
|
||||
"content-type": "~1.0.4",
|
||||
"cookie": "0.5.0",
|
||||
"cookie": "0.6.0",
|
||||
"cookie-signature": "1.0.6",
|
||||
"debug": "2.6.9",
|
||||
"depd": "2.0.0",
|
||||
@ -7419,6 +7585,14 @@
|
||||
"node": ">= 0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/express/node_modules/cookie": {
|
||||
"version": "0.6.0",
|
||||
"resolved": "https://registry.npmjs.org/cookie/-/cookie-0.6.0.tgz",
|
||||
"integrity": "sha512-U71cyTamuh1CRNCfpGY6to28lxvNwPG4Guz/EVjgf3Jmzv0vlDp1atT9eS5dDjMYHucpHbWns6Lwf3BKz6svdw==",
|
||||
"engines": {
|
||||
"node": ">= 0.6"
|
||||
}
|
||||
},
|
||||
"node_modules/express/node_modules/cookie-signature": {
|
||||
"version": "1.0.6",
|
||||
"resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz",
|
||||
@ -7749,9 +7923,9 @@
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/follow-redirects": {
|
||||
"version": "1.15.4",
|
||||
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.4.tgz",
|
||||
"integrity": "sha512-Cr4D/5wlrb0z9dgERpUL3LrmPKVDsETIJhaCMeDfuFYcqa5bldGV6wBsAN6X/vxlXQtFBMrXdXxdL8CbDTGniw==",
|
||||
"version": "1.15.6",
|
||||
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.6.tgz",
|
||||
"integrity": "sha512-wWN62YITEaOpSK584EZXJafH1AGpO8RVgElfkuXbTOrPX4fIfOyEpW/CsiNd8JdYrAoOvafRTOEnvsO++qCqFA==",
|
||||
"funding": [
|
||||
{
|
||||
"type": "individual",
|
||||
@ -9208,15 +9382,7 @@
|
||||
"node": ">=0.8.0"
|
||||
}
|
||||
},
|
||||
"node_modules/ldapauth-fork/node_modules/lru-cache": {
|
||||
"version": "7.18.3",
|
||||
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-7.18.3.tgz",
|
||||
"integrity": "sha512-jumlc0BIUrS3qJGgIkWZsyfAM7NCWiBcCDhnd+3NNM5KbBmLTgHVfWBcg6W+rLUsIpzpERPsvwUP7CckAQSOoA==",
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/ldapjs": {
|
||||
"node_modules/ldapauth-fork/node_modules/ldapjs": {
|
||||
"version": "2.3.3",
|
||||
"resolved": "https://registry.npmjs.org/ldapjs/-/ldapjs-2.3.3.tgz",
|
||||
"integrity": "sha512-75QiiLJV/PQqtpH+HGls44dXweviFwQ6SiIK27EqzKQ5jU/7UFrl2E5nLdQ3IYRBzJ/AVFJI66u0MZ0uofKYwg==",
|
||||
@ -9234,6 +9400,35 @@
|
||||
"node": ">=10.13.0"
|
||||
}
|
||||
},
|
||||
"node_modules/ldapauth-fork/node_modules/lru-cache": {
|
||||
"version": "7.18.3",
|
||||
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-7.18.3.tgz",
|
||||
"integrity": "sha512-jumlc0BIUrS3qJGgIkWZsyfAM7NCWiBcCDhnd+3NNM5KbBmLTgHVfWBcg6W+rLUsIpzpERPsvwUP7CckAQSOoA==",
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/ldapjs": {
|
||||
"version": "3.0.7",
|
||||
"resolved": "https://registry.npmjs.org/ldapjs/-/ldapjs-3.0.7.tgz",
|
||||
"integrity": "sha512-1ky+WrN+4CFMuoekUOv7Y1037XWdjKpu0xAPwSP+9KdvmV9PG+qOKlssDV6a+U32apwxdD3is/BZcWOYzN30cg==",
|
||||
"dependencies": {
|
||||
"@ldapjs/asn1": "^2.0.0",
|
||||
"@ldapjs/attribute": "^1.0.0",
|
||||
"@ldapjs/change": "^1.0.0",
|
||||
"@ldapjs/controls": "^2.1.0",
|
||||
"@ldapjs/dn": "^1.1.0",
|
||||
"@ldapjs/filter": "^2.1.1",
|
||||
"@ldapjs/messages": "^1.3.0",
|
||||
"@ldapjs/protocol": "^1.2.1",
|
||||
"abstract-logging": "^2.0.1",
|
||||
"assert-plus": "^1.0.0",
|
||||
"backoff": "^2.5.0",
|
||||
"once": "^1.4.0",
|
||||
"vasync": "^2.2.1",
|
||||
"verror": "^1.10.1"
|
||||
}
|
||||
},
|
||||
"node_modules/leven": {
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmjs.org/leven/-/leven-2.1.0.tgz",
|
||||
@ -9759,9 +9954,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/mysql2": {
|
||||
"version": "3.9.1",
|
||||
"resolved": "https://registry.npmjs.org/mysql2/-/mysql2-3.9.1.tgz",
|
||||
"integrity": "sha512-3njoWAAhGBYy0tWBabqUQcLtczZUxrmmtc2vszQUekg3kTJyZ5/IeLC3Fo04u6y6Iy5Sba7pIIa2P/gs8D3ZeQ==",
|
||||
"version": "3.9.4",
|
||||
"resolved": "https://registry.npmjs.org/mysql2/-/mysql2-3.9.4.tgz",
|
||||
"integrity": "sha512-OEESQuwxMza803knC1YSt7NMuc1BrK9j7gZhCSs2WAyxr1vfiI7QLaLOKTh5c9SWGz98qVyQUbK8/WckevNQhg==",
|
||||
"dependencies": {
|
||||
"denque": "^2.1.0",
|
||||
"generate-function": "^2.3.1",
|
||||
@ -10231,6 +10426,15 @@
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/oracledb": {
|
||||
"version": "6.4.0",
|
||||
"resolved": "https://registry.npmjs.org/oracledb/-/oracledb-6.4.0.tgz",
|
||||
"integrity": "sha512-TJI08qzQlf/l7T49VojP9BoQpjEr14NXZmpSzzcLrbNs7qSl0QA/Mc9gGiEdkg5WmwH0wqUjtMC7jlf1WamlYA==",
|
||||
"hasInstallScript": true,
|
||||
"engines": {
|
||||
"node": ">=14.6"
|
||||
}
|
||||
},
|
||||
"node_modules/p-limit": {
|
||||
"version": "3.1.0",
|
||||
"resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz",
|
||||
@ -10818,9 +11022,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/postcss": {
|
||||
"version": "8.4.32",
|
||||
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.32.tgz",
|
||||
"integrity": "sha512-D/kj5JNu6oo2EIy+XL/26JEDTlIbB8hw85G8StOE6L74RQAVVP5rej6wxCNqyMbR4RkPfqvezVbPw81Ngd6Kcw==",
|
||||
"version": "8.4.38",
|
||||
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.38.tgz",
|
||||
"integrity": "sha512-Wglpdk03BSfXkHoQa3b/oulrotAkwrlLDRSOb9D0bN86FdRyE9lppSp33aHNPgBa0JKCoB+drFLZkQoRRYae5A==",
|
||||
"dev": true,
|
||||
"funding": [
|
||||
{
|
||||
@ -10839,7 +11043,7 @@
|
||||
"dependencies": {
|
||||
"nanoid": "^3.3.7",
|
||||
"picocolors": "^1.0.0",
|
||||
"source-map-js": "^1.0.2"
|
||||
"source-map-js": "^1.2.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^10 || ^12 || >=14"
|
||||
@ -11234,9 +11438,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/raw-body": {
|
||||
"version": "2.5.1",
|
||||
"resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.1.tgz",
|
||||
"integrity": "sha512-qqJBtEyVgS0ZmPGdCFPWJ3FreoqvG4MVQln/kCgF7Olq95IbOp0/BWyMwbdtn4VTvkM8Y7khCQ2Xgk/tcrCXig==",
|
||||
"version": "2.5.2",
|
||||
"resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.2.tgz",
|
||||
"integrity": "sha512-8zGqypfENjCIqGhgXToC8aB2r7YrBX+AQAfIPs/Mlk+BtPTztOvTS01NRW/3Eh60J+a48lt8qsCzirQ6loCVfA==",
|
||||
"dependencies": {
|
||||
"bytes": "3.1.2",
|
||||
"http-errors": "2.0.0",
|
||||
@ -11511,10 +11715,13 @@
|
||||
}
|
||||
},
|
||||
"node_modules/rollup": {
|
||||
"version": "4.8.0",
|
||||
"resolved": "https://registry.npmjs.org/rollup/-/rollup-4.8.0.tgz",
|
||||
"integrity": "sha512-NpsklK2fach5CdI+PScmlE5R4Ao/FSWtF7LkoIrHDxPACY/xshNasPsbpG0VVHxUTbf74tJbVT4PrP8JsJ6ZDA==",
|
||||
"version": "4.14.3",
|
||||
"resolved": "https://registry.npmjs.org/rollup/-/rollup-4.14.3.tgz",
|
||||
"integrity": "sha512-ag5tTQKYsj1bhrFC9+OEWqb5O6VYgtQDO9hPDBMmIbePwhfSr+ExlcU741t8Dhw5DkPCQf6noz0jb36D6W9/hw==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"@types/estree": "1.0.5"
|
||||
},
|
||||
"bin": {
|
||||
"rollup": "dist/bin/rollup"
|
||||
},
|
||||
@ -11523,19 +11730,22 @@
|
||||
"npm": ">=8.0.0"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"@rollup/rollup-android-arm-eabi": "4.8.0",
|
||||
"@rollup/rollup-android-arm64": "4.8.0",
|
||||
"@rollup/rollup-darwin-arm64": "4.8.0",
|
||||
"@rollup/rollup-darwin-x64": "4.8.0",
|
||||
"@rollup/rollup-linux-arm-gnueabihf": "4.8.0",
|
||||
"@rollup/rollup-linux-arm64-gnu": "4.8.0",
|
||||
"@rollup/rollup-linux-arm64-musl": "4.8.0",
|
||||
"@rollup/rollup-linux-riscv64-gnu": "4.8.0",
|
||||
"@rollup/rollup-linux-x64-gnu": "4.8.0",
|
||||
"@rollup/rollup-linux-x64-musl": "4.8.0",
|
||||
"@rollup/rollup-win32-arm64-msvc": "4.8.0",
|
||||
"@rollup/rollup-win32-ia32-msvc": "4.8.0",
|
||||
"@rollup/rollup-win32-x64-msvc": "4.8.0",
|
||||
"@rollup/rollup-android-arm-eabi": "4.14.3",
|
||||
"@rollup/rollup-android-arm64": "4.14.3",
|
||||
"@rollup/rollup-darwin-arm64": "4.14.3",
|
||||
"@rollup/rollup-darwin-x64": "4.14.3",
|
||||
"@rollup/rollup-linux-arm-gnueabihf": "4.14.3",
|
||||
"@rollup/rollup-linux-arm-musleabihf": "4.14.3",
|
||||
"@rollup/rollup-linux-arm64-gnu": "4.14.3",
|
||||
"@rollup/rollup-linux-arm64-musl": "4.14.3",
|
||||
"@rollup/rollup-linux-powerpc64le-gnu": "4.14.3",
|
||||
"@rollup/rollup-linux-riscv64-gnu": "4.14.3",
|
||||
"@rollup/rollup-linux-s390x-gnu": "4.14.3",
|
||||
"@rollup/rollup-linux-x64-gnu": "4.14.3",
|
||||
"@rollup/rollup-linux-x64-musl": "4.14.3",
|
||||
"@rollup/rollup-win32-arm64-msvc": "4.14.3",
|
||||
"@rollup/rollup-win32-ia32-msvc": "4.14.3",
|
||||
"@rollup/rollup-win32-x64-msvc": "4.14.3",
|
||||
"fsevents": "~2.3.2"
|
||||
}
|
||||
},
|
||||
@ -11887,9 +12097,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/source-map-js": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.0.2.tgz",
|
||||
"integrity": "sha512-R0XvVJ9WusLiqTCEiGCmICCMplcCkIwwR11mOSD9CR5u+IXYdiseeEuXCVAjS54zqwkLcPNnmU4OeJ6tUrWhDw==",
|
||||
"version": "1.2.0",
|
||||
"resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.0.tgz",
|
||||
"integrity": "sha512-itJW8lvSA0TXEphiRoawsCksnlf8SyvmFzIhltqAHluXd88pkCd+cXJVHTDwdCr0IzwptSm035IHQktUu1QUMg==",
|
||||
"dev": true,
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
@ -12249,9 +12459,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/tar": {
|
||||
"version": "6.2.0",
|
||||
"resolved": "https://registry.npmjs.org/tar/-/tar-6.2.0.tgz",
|
||||
"integrity": "sha512-/Wo7DcT0u5HUV486xg675HtjNd3BXZ6xDbzsCUZPt5iw8bTQ63bP0Raut3mvro9u+CUyq7YQd8Cx55fsZXxqLQ==",
|
||||
"version": "6.2.1",
|
||||
"resolved": "https://registry.npmjs.org/tar/-/tar-6.2.1.tgz",
|
||||
"integrity": "sha512-DZ4yORTwrbTj/7MZYq2w+/ZFdI6OZ/f9SFHR+71gIVUZhOQPHzVCLpvRnPgyaMpfWxxk/4ONva3GQSyNIKRv6A==",
|
||||
"dependencies": {
|
||||
"chownr": "^2.0.0",
|
||||
"fs-minipass": "^2.0.0",
|
||||
@ -13447,14 +13657,14 @@
|
||||
}
|
||||
},
|
||||
"node_modules/vite": {
|
||||
"version": "5.0.12",
|
||||
"resolved": "https://registry.npmjs.org/vite/-/vite-5.0.12.tgz",
|
||||
"integrity": "sha512-4hsnEkG3q0N4Tzf1+t6NdN9dg/L3BM+q8SWgbSPnJvrgH2kgdyzfVJwbR1ic69/4uMJJ/3dqDZZE5/WwqW8U1w==",
|
||||
"version": "5.2.9",
|
||||
"resolved": "https://registry.npmjs.org/vite/-/vite-5.2.9.tgz",
|
||||
"integrity": "sha512-uOQWfuZBlc6Y3W/DTuQ1Sr+oIXWvqljLvS881SVmAj00d5RdgShLcuXWxseWPd4HXwiYBFW/vXHfKFeqj9uQnw==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"esbuild": "^0.19.3",
|
||||
"postcss": "^8.4.32",
|
||||
"rollup": "^4.2.0"
|
||||
"esbuild": "^0.20.1",
|
||||
"postcss": "^8.4.38",
|
||||
"rollup": "^4.13.0"
|
||||
},
|
||||
"bin": {
|
||||
"vite": "bin/vite.js"
|
||||
@ -13589,9 +13799,9 @@
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/vite/node_modules/@esbuild/android-arm": {
|
||||
"version": "0.19.9",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.19.9.tgz",
|
||||
"integrity": "sha512-jkYjjq7SdsWuNI6b5quymW0oC83NN5FdRPuCbs9HZ02mfVdAP8B8eeqLSYU3gb6OJEaY5CQabtTFbqBf26H3GA==",
|
||||
"version": "0.20.2",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.20.2.tgz",
|
||||
"integrity": "sha512-t98Ra6pw2VaDhqNWO2Oph2LXbz/EJcnLmKLGBJwEwXX/JAN83Fym1rU8l0JUWK6HkIbWONCSSatf4sf2NBRx/w==",
|
||||
"cpu": [
|
||||
"arm"
|
||||
],
|
||||
@ -13605,9 +13815,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/vite/node_modules/@esbuild/android-arm64": {
|
||||
"version": "0.19.9",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.19.9.tgz",
|
||||
"integrity": "sha512-q4cR+6ZD0938R19MyEW3jEsMzbb/1rulLXiNAJQADD/XYp7pT+rOS5JGxvpRW8dFDEfjW4wLgC/3FXIw4zYglQ==",
|
||||
"version": "0.20.2",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.20.2.tgz",
|
||||
"integrity": "sha512-mRzjLacRtl/tWU0SvD8lUEwb61yP9cqQo6noDZP/O8VkwafSYwZ4yWy24kan8jE/IMERpYncRt2dw438LP3Xmg==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
@ -13621,9 +13831,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/vite/node_modules/@esbuild/android-x64": {
|
||||
"version": "0.19.9",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.19.9.tgz",
|
||||
"integrity": "sha512-KOqoPntWAH6ZxDwx1D6mRntIgZh9KodzgNOy5Ebt9ghzffOk9X2c1sPwtM9P+0eXbefnDhqYfkh5PLP5ULtWFA==",
|
||||
"version": "0.20.2",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.20.2.tgz",
|
||||
"integrity": "sha512-btzExgV+/lMGDDa194CcUQm53ncxzeBrWJcncOBxuC6ndBkKxnHdFJn86mCIgTELsooUmwUm9FkhSp5HYu00Rg==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
@ -13637,9 +13847,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/vite/node_modules/@esbuild/darwin-arm64": {
|
||||
"version": "0.19.9",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.19.9.tgz",
|
||||
"integrity": "sha512-KBJ9S0AFyLVx2E5D8W0vExqRW01WqRtczUZ8NRu+Pi+87opZn5tL4Y0xT0mA4FtHctd0ZgwNoN639fUUGlNIWw==",
|
||||
"version": "0.20.2",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.20.2.tgz",
|
||||
"integrity": "sha512-4J6IRT+10J3aJH3l1yzEg9y3wkTDgDk7TSDFX+wKFiWjqWp/iCfLIYzGyasx9l0SAFPT1HwSCR+0w/h1ES/MjA==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
@ -13653,9 +13863,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/vite/node_modules/@esbuild/darwin-x64": {
|
||||
"version": "0.19.9",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.19.9.tgz",
|
||||
"integrity": "sha512-vE0VotmNTQaTdX0Q9dOHmMTao6ObjyPm58CHZr1UK7qpNleQyxlFlNCaHsHx6Uqv86VgPmR4o2wdNq3dP1qyDQ==",
|
||||
"version": "0.20.2",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.20.2.tgz",
|
||||
"integrity": "sha512-tBcXp9KNphnNH0dfhv8KYkZhjc+H3XBkF5DKtswJblV7KlT9EI2+jeA8DgBjp908WEuYll6pF+UStUCfEpdysA==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
@ -13669,9 +13879,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/vite/node_modules/@esbuild/freebsd-arm64": {
|
||||
"version": "0.19.9",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.19.9.tgz",
|
||||
"integrity": "sha512-uFQyd/o1IjiEk3rUHSwUKkqZwqdvuD8GevWF065eqgYfexcVkxh+IJgwTaGZVu59XczZGcN/YMh9uF1fWD8j1g==",
|
||||
"version": "0.20.2",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.20.2.tgz",
|
||||
"integrity": "sha512-d3qI41G4SuLiCGCFGUrKsSeTXyWG6yem1KcGZVS+3FYlYhtNoNgYrWcvkOoaqMhwXSMrZRl69ArHsGJ9mYdbbw==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
@ -13685,9 +13895,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/vite/node_modules/@esbuild/freebsd-x64": {
|
||||
"version": "0.19.9",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.19.9.tgz",
|
||||
"integrity": "sha512-WMLgWAtkdTbTu1AWacY7uoj/YtHthgqrqhf1OaEWnZb7PQgpt8eaA/F3LkV0E6K/Lc0cUr/uaVP/49iE4M4asA==",
|
||||
"version": "0.20.2",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.20.2.tgz",
|
||||
"integrity": "sha512-d+DipyvHRuqEeM5zDivKV1KuXn9WeRX6vqSqIDgwIfPQtwMP4jaDsQsDncjTDDsExT4lR/91OLjRo8bmC1e+Cw==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
@ -13701,9 +13911,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/vite/node_modules/@esbuild/linux-arm": {
|
||||
"version": "0.19.9",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.19.9.tgz",
|
||||
"integrity": "sha512-C/ChPohUYoyUaqn1h17m/6yt6OB14hbXvT8EgM1ZWaiiTYz7nWZR0SYmMnB5BzQA4GXl3BgBO1l8MYqL/He3qw==",
|
||||
"version": "0.20.2",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.20.2.tgz",
|
||||
"integrity": "sha512-VhLPeR8HTMPccbuWWcEUD1Az68TqaTYyj6nfE4QByZIQEQVWBB8vup8PpR7y1QHL3CpcF6xd5WVBU/+SBEvGTg==",
|
||||
"cpu": [
|
||||
"arm"
|
||||
],
|
||||
@ -13717,9 +13927,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/vite/node_modules/@esbuild/linux-arm64": {
|
||||
"version": "0.19.9",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.19.9.tgz",
|
||||
"integrity": "sha512-PiPblfe1BjK7WDAKR1Cr9O7VVPqVNpwFcPWgfn4xu0eMemzRp442hXyzF/fSwgrufI66FpHOEJk0yYdPInsmyQ==",
|
||||
"version": "0.20.2",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.20.2.tgz",
|
||||
"integrity": "sha512-9pb6rBjGvTFNira2FLIWqDk/uaf42sSyLE8j1rnUpuzsODBq7FvpwHYZxQ/It/8b+QOS1RYfqgGFNLRI+qlq2A==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
@ -13733,9 +13943,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/vite/node_modules/@esbuild/linux-ia32": {
|
||||
"version": "0.19.9",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.19.9.tgz",
|
||||
"integrity": "sha512-f37i/0zE0MjDxijkPSQw1CO/7C27Eojqb+r3BbHVxMLkj8GCa78TrBZzvPyA/FNLUMzP3eyHCVkAopkKVja+6Q==",
|
||||
"version": "0.20.2",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.20.2.tgz",
|
||||
"integrity": "sha512-o10utieEkNPFDZFQm9CoP7Tvb33UutoJqg3qKf1PWVeeJhJw0Q347PxMvBgVVFgouYLGIhFYG0UGdBumROyiig==",
|
||||
"cpu": [
|
||||
"ia32"
|
||||
],
|
||||
@ -13749,9 +13959,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/vite/node_modules/@esbuild/linux-loong64": {
|
||||
"version": "0.19.9",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.19.9.tgz",
|
||||
"integrity": "sha512-t6mN147pUIf3t6wUt3FeumoOTPfmv9Cc6DQlsVBpB7eCpLOqQDyWBP1ymXn1lDw4fNUSb/gBcKAmvTP49oIkaA==",
|
||||
"version": "0.20.2",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.20.2.tgz",
|
||||
"integrity": "sha512-PR7sp6R/UC4CFVomVINKJ80pMFlfDfMQMYynX7t1tNTeivQ6XdX5r2XovMmha/VjR1YN/HgHWsVcTRIMkymrgQ==",
|
||||
"cpu": [
|
||||
"loong64"
|
||||
],
|
||||
@ -13765,9 +13975,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/vite/node_modules/@esbuild/linux-mips64el": {
|
||||
"version": "0.19.9",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.19.9.tgz",
|
||||
"integrity": "sha512-jg9fujJTNTQBuDXdmAg1eeJUL4Jds7BklOTkkH80ZgQIoCTdQrDaHYgbFZyeTq8zbY+axgptncko3v9p5hLZtw==",
|
||||
"version": "0.20.2",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.20.2.tgz",
|
||||
"integrity": "sha512-4BlTqeutE/KnOiTG5Y6Sb/Hw6hsBOZapOVF6njAESHInhlQAghVVZL1ZpIctBOoTFbQyGW+LsVYZ8lSSB3wkjA==",
|
||||
"cpu": [
|
||||
"mips64el"
|
||||
],
|
||||
@ -13781,9 +13991,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/vite/node_modules/@esbuild/linux-ppc64": {
|
||||
"version": "0.19.9",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.19.9.tgz",
|
||||
"integrity": "sha512-tkV0xUX0pUUgY4ha7z5BbDS85uI7ABw3V1d0RNTii7E9lbmV8Z37Pup2tsLV46SQWzjOeyDi1Q7Wx2+QM8WaCQ==",
|
||||
"version": "0.20.2",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.20.2.tgz",
|
||||
"integrity": "sha512-rD3KsaDprDcfajSKdn25ooz5J5/fWBylaaXkuotBDGnMnDP1Uv5DLAN/45qfnf3JDYyJv/ytGHQaziHUdyzaAg==",
|
||||
"cpu": [
|
||||
"ppc64"
|
||||
],
|
||||
@ -13797,9 +14007,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/vite/node_modules/@esbuild/linux-riscv64": {
|
||||
"version": "0.19.9",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.19.9.tgz",
|
||||
"integrity": "sha512-DfLp8dj91cufgPZDXr9p3FoR++m3ZJ6uIXsXrIvJdOjXVREtXuQCjfMfvmc3LScAVmLjcfloyVtpn43D56JFHg==",
|
||||
"version": "0.20.2",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.20.2.tgz",
|
||||
"integrity": "sha512-snwmBKacKmwTMmhLlz/3aH1Q9T8v45bKYGE3j26TsaOVtjIag4wLfWSiZykXzXuE1kbCE+zJRmwp+ZbIHinnVg==",
|
||||
"cpu": [
|
||||
"riscv64"
|
||||
],
|
||||
@ -13813,9 +14023,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/vite/node_modules/@esbuild/linux-s390x": {
|
||||
"version": "0.19.9",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.19.9.tgz",
|
||||
"integrity": "sha512-zHbglfEdC88KMgCWpOl/zc6dDYJvWGLiUtmPRsr1OgCViu3z5GncvNVdf+6/56O2Ca8jUU+t1BW261V6kp8qdw==",
|
||||
"version": "0.20.2",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.20.2.tgz",
|
||||
"integrity": "sha512-wcWISOobRWNm3cezm5HOZcYz1sKoHLd8VL1dl309DiixxVFoFe/o8HnwuIwn6sXre88Nwj+VwZUvJf4AFxkyrQ==",
|
||||
"cpu": [
|
||||
"s390x"
|
||||
],
|
||||
@ -13829,9 +14039,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/vite/node_modules/@esbuild/linux-x64": {
|
||||
"version": "0.19.9",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.19.9.tgz",
|
||||
"integrity": "sha512-JUjpystGFFmNrEHQnIVG8hKwvA2DN5o7RqiO1CVX8EN/F/gkCjkUMgVn6hzScpwnJtl2mPR6I9XV1oW8k9O+0A==",
|
||||
"version": "0.20.2",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.20.2.tgz",
|
||||
"integrity": "sha512-1MdwI6OOTsfQfek8sLwgyjOXAu+wKhLEoaOLTjbijk6E2WONYpH9ZU2mNtR+lZ2B4uwr+usqGuVfFT9tMtGvGw==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
@ -13845,9 +14055,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/vite/node_modules/@esbuild/netbsd-x64": {
|
||||
"version": "0.19.9",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.19.9.tgz",
|
||||
"integrity": "sha512-GThgZPAwOBOsheA2RUlW5UeroRfESwMq/guy8uEe3wJlAOjpOXuSevLRd70NZ37ZrpO6RHGHgEHvPg1h3S1Jug==",
|
||||
"version": "0.20.2",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.20.2.tgz",
|
||||
"integrity": "sha512-K8/DhBxcVQkzYc43yJXDSyjlFeHQJBiowJ0uVL6Tor3jGQfSGHNNJcWxNbOI8v5k82prYqzPuwkzHt3J1T1iZQ==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
@ -13861,9 +14071,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/vite/node_modules/@esbuild/openbsd-x64": {
|
||||
"version": "0.19.9",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.19.9.tgz",
|
||||
"integrity": "sha512-Ki6PlzppaFVbLnD8PtlVQfsYw4S9n3eQl87cqgeIw+O3sRr9IghpfSKY62mggdt1yCSZ8QWvTZ9jo9fjDSg9uw==",
|
||||
"version": "0.20.2",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.20.2.tgz",
|
||||
"integrity": "sha512-eMpKlV0SThJmmJgiVyN9jTPJ2VBPquf6Kt/nAoo6DgHAoN57K15ZghiHaMvqjCye/uU4X5u3YSMgVBI1h3vKrQ==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
@ -13877,9 +14087,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/vite/node_modules/@esbuild/sunos-x64": {
|
||||
"version": "0.19.9",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.19.9.tgz",
|
||||
"integrity": "sha512-MLHj7k9hWh4y1ddkBpvRj2b9NCBhfgBt3VpWbHQnXRedVun/hC7sIyTGDGTfsGuXo4ebik2+3ShjcPbhtFwWDw==",
|
||||
"version": "0.20.2",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.20.2.tgz",
|
||||
"integrity": "sha512-2UyFtRC6cXLyejf/YEld4Hajo7UHILetzE1vsRcGL3earZEW77JxrFjH4Ez2qaTiEfMgAXxfAZCm1fvM/G/o8w==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
@ -13893,9 +14103,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/vite/node_modules/@esbuild/win32-arm64": {
|
||||
"version": "0.19.9",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.19.9.tgz",
|
||||
"integrity": "sha512-GQoa6OrQ8G08guMFgeXPH7yE/8Dt0IfOGWJSfSH4uafwdC7rWwrfE6P9N8AtPGIjUzdo2+7bN8Xo3qC578olhg==",
|
||||
"version": "0.20.2",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.20.2.tgz",
|
||||
"integrity": "sha512-GRibxoawM9ZCnDxnP3usoUDO9vUkpAxIIZ6GQI+IlVmr5kP3zUq+l17xELTHMWTWzjxa2guPNyrpq1GWmPvcGQ==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
@ -13909,9 +14119,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/vite/node_modules/@esbuild/win32-ia32": {
|
||||
"version": "0.19.9",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.19.9.tgz",
|
||||
"integrity": "sha512-UOozV7Ntykvr5tSOlGCrqU3NBr3d8JqPes0QWN2WOXfvkWVGRajC+Ym0/Wj88fUgecUCLDdJPDF0Nna2UK3Qtg==",
|
||||
"version": "0.20.2",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.20.2.tgz",
|
||||
"integrity": "sha512-HfLOfn9YWmkSKRQqovpnITazdtquEW8/SoHW7pWpuEeguaZI4QnCRW6b+oZTztdBnZOS2hqJ6im/D5cPzBTTlQ==",
|
||||
"cpu": [
|
||||
"ia32"
|
||||
],
|
||||
@ -13925,9 +14135,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/vite/node_modules/@esbuild/win32-x64": {
|
||||
"version": "0.19.9",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.19.9.tgz",
|
||||
"integrity": "sha512-oxoQgglOP7RH6iasDrhY+R/3cHrfwIDvRlT4CGChflq6twk8iENeVvMJjmvBb94Ik1Z+93iGO27err7w6l54GQ==",
|
||||
"version": "0.20.2",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.20.2.tgz",
|
||||
"integrity": "sha512-N49X4lJX27+l9jbLKSqZ6bKNjzQvHaT8IIFUy+YIqmXQdjYCToGWwOItDrfby14c78aDd5NHQl29xingXfCdLQ==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
@ -13941,9 +14151,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/vite/node_modules/esbuild": {
|
||||
"version": "0.19.9",
|
||||
"resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.19.9.tgz",
|
||||
"integrity": "sha512-U9CHtKSy+EpPsEBa+/A2gMs/h3ylBC0H0KSqIg7tpztHerLi6nrrcoUJAkNCEPumx8yJ+Byic4BVwHgRbN0TBg==",
|
||||
"version": "0.20.2",
|
||||
"resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.20.2.tgz",
|
||||
"integrity": "sha512-WdOOppmUNU+IbZ0PaDiTst80zjnrOkyJNHoKupIcVyU8Lvla3Ugx94VzkQ32Ijqd7UhHJy75gNWDMUekcrSJ6g==",
|
||||
"dev": true,
|
||||
"hasInstallScript": true,
|
||||
"bin": {
|
||||
@ -13953,28 +14163,29 @@
|
||||
"node": ">=12"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"@esbuild/android-arm": "0.19.9",
|
||||
"@esbuild/android-arm64": "0.19.9",
|
||||
"@esbuild/android-x64": "0.19.9",
|
||||
"@esbuild/darwin-arm64": "0.19.9",
|
||||
"@esbuild/darwin-x64": "0.19.9",
|
||||
"@esbuild/freebsd-arm64": "0.19.9",
|
||||
"@esbuild/freebsd-x64": "0.19.9",
|
||||
"@esbuild/linux-arm": "0.19.9",
|
||||
"@esbuild/linux-arm64": "0.19.9",
|
||||
"@esbuild/linux-ia32": "0.19.9",
|
||||
"@esbuild/linux-loong64": "0.19.9",
|
||||
"@esbuild/linux-mips64el": "0.19.9",
|
||||
"@esbuild/linux-ppc64": "0.19.9",
|
||||
"@esbuild/linux-riscv64": "0.19.9",
|
||||
"@esbuild/linux-s390x": "0.19.9",
|
||||
"@esbuild/linux-x64": "0.19.9",
|
||||
"@esbuild/netbsd-x64": "0.19.9",
|
||||
"@esbuild/openbsd-x64": "0.19.9",
|
||||
"@esbuild/sunos-x64": "0.19.9",
|
||||
"@esbuild/win32-arm64": "0.19.9",
|
||||
"@esbuild/win32-ia32": "0.19.9",
|
||||
"@esbuild/win32-x64": "0.19.9"
|
||||
"@esbuild/aix-ppc64": "0.20.2",
|
||||
"@esbuild/android-arm": "0.20.2",
|
||||
"@esbuild/android-arm64": "0.20.2",
|
||||
"@esbuild/android-x64": "0.20.2",
|
||||
"@esbuild/darwin-arm64": "0.20.2",
|
||||
"@esbuild/darwin-x64": "0.20.2",
|
||||
"@esbuild/freebsd-arm64": "0.20.2",
|
||||
"@esbuild/freebsd-x64": "0.20.2",
|
||||
"@esbuild/linux-arm": "0.20.2",
|
||||
"@esbuild/linux-arm64": "0.20.2",
|
||||
"@esbuild/linux-ia32": "0.20.2",
|
||||
"@esbuild/linux-loong64": "0.20.2",
|
||||
"@esbuild/linux-mips64el": "0.20.2",
|
||||
"@esbuild/linux-ppc64": "0.20.2",
|
||||
"@esbuild/linux-riscv64": "0.20.2",
|
||||
"@esbuild/linux-s390x": "0.20.2",
|
||||
"@esbuild/linux-x64": "0.20.2",
|
||||
"@esbuild/netbsd-x64": "0.20.2",
|
||||
"@esbuild/openbsd-x64": "0.20.2",
|
||||
"@esbuild/sunos-x64": "0.20.2",
|
||||
"@esbuild/win32-arm64": "0.20.2",
|
||||
"@esbuild/win32-ia32": "0.20.2",
|
||||
"@esbuild/win32-x64": "0.20.2"
|
||||
}
|
||||
},
|
||||
"node_modules/vitest": {
|
||||
|
@ -96,6 +96,7 @@
|
||||
"axios-retry": "^4.0.0",
|
||||
"bcrypt": "^5.1.1",
|
||||
"bullmq": "^5.3.3",
|
||||
"cassandra-driver": "^4.7.2",
|
||||
"dotenv": "^16.4.1",
|
||||
"fastify": "^4.26.0",
|
||||
"fastify-plugin": "^4.5.1",
|
||||
@ -105,6 +106,7 @@
|
||||
"jsonwebtoken": "^9.0.2",
|
||||
"jsrp": "^0.2.4",
|
||||
"knex": "^3.0.1",
|
||||
"ldapjs": "^3.0.7",
|
||||
"libsodium-wrappers": "^0.7.13",
|
||||
"lodash.isequal": "^4.5.0",
|
||||
"ms": "^2.1.3",
|
||||
@ -112,6 +114,7 @@
|
||||
"nanoid": "^5.0.4",
|
||||
"nodemailer": "^6.9.9",
|
||||
"ora": "^7.0.1",
|
||||
"oracledb": "^6.4.0",
|
||||
"passport-github": "^1.1.0",
|
||||
"passport-gitlab2": "^5.0.0",
|
||||
"passport-google-oauth20": "^2.0.0",
|
||||
|
6
backend/src/@types/fastify.d.ts
vendored
6
backend/src/@types/fastify.d.ts
vendored
@ -1,8 +1,11 @@
|
||||
import "fastify";
|
||||
|
||||
import { TUsers } from "@app/db/schemas";
|
||||
import { TAccessApprovalPolicyServiceFactory } from "@app/ee/services/access-approval-policy/access-approval-policy-service";
|
||||
import { TAccessApprovalRequestServiceFactory } from "@app/ee/services/access-approval-request/access-approval-request-service";
|
||||
import { TAuditLogServiceFactory } from "@app/ee/services/audit-log/audit-log-service";
|
||||
import { TCreateAuditLogDTO } from "@app/ee/services/audit-log/audit-log-types";
|
||||
import { TAuditLogStreamServiceFactory } from "@app/ee/services/audit-log-stream/audit-log-stream-service";
|
||||
import { 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";
|
||||
@ -112,6 +115,8 @@ declare module "fastify" {
|
||||
identityAccessToken: TIdentityAccessTokenServiceFactory;
|
||||
identityProject: TIdentityProjectServiceFactory;
|
||||
identityUa: TIdentityUaServiceFactory;
|
||||
accessApprovalPolicy: TAccessApprovalPolicyServiceFactory;
|
||||
accessApprovalRequest: TAccessApprovalRequestServiceFactory;
|
||||
secretApprovalPolicy: TSecretApprovalPolicyServiceFactory;
|
||||
secretApprovalRequest: TSecretApprovalRequestServiceFactory;
|
||||
secretRotation: TSecretRotationServiceFactory;
|
||||
@ -120,6 +125,7 @@ declare module "fastify" {
|
||||
scim: TScimServiceFactory;
|
||||
ldap: TLdapConfigServiceFactory;
|
||||
auditLog: TAuditLogServiceFactory;
|
||||
auditLogStream: TAuditLogStreamServiceFactory;
|
||||
secretScanning: TSecretScanningServiceFactory;
|
||||
license: TLicenseServiceFactory;
|
||||
trustedIp: TTrustedIpServiceFactory;
|
||||
|
49
backend/src/@types/knex.d.ts
vendored
49
backend/src/@types/knex.d.ts
vendored
@ -2,11 +2,26 @@ import { Knex } from "knex";
|
||||
|
||||
import {
|
||||
TableName,
|
||||
TAccessApprovalPolicies,
|
||||
TAccessApprovalPoliciesApprovers,
|
||||
TAccessApprovalPoliciesApproversInsert,
|
||||
TAccessApprovalPoliciesApproversUpdate,
|
||||
TAccessApprovalPoliciesInsert,
|
||||
TAccessApprovalPoliciesUpdate,
|
||||
TAccessApprovalRequests,
|
||||
TAccessApprovalRequestsInsert,
|
||||
TAccessApprovalRequestsReviewers,
|
||||
TAccessApprovalRequestsReviewersInsert,
|
||||
TAccessApprovalRequestsReviewersUpdate,
|
||||
TAccessApprovalRequestsUpdate,
|
||||
TApiKeys,
|
||||
TApiKeysInsert,
|
||||
TApiKeysUpdate,
|
||||
TAuditLogs,
|
||||
TAuditLogsInsert,
|
||||
TAuditLogStreams,
|
||||
TAuditLogStreamsInsert,
|
||||
TAuditLogStreamsUpdate,
|
||||
TAuditLogsUpdate,
|
||||
TAuthTokens,
|
||||
TAuthTokenSessions,
|
||||
@ -74,6 +89,9 @@ import {
|
||||
TLdapConfigs,
|
||||
TLdapConfigsInsert,
|
||||
TLdapConfigsUpdate,
|
||||
TLdapGroupMaps,
|
||||
TLdapGroupMapsInsert,
|
||||
TLdapGroupMapsUpdate,
|
||||
TOrganizations,
|
||||
TOrganizationsInsert,
|
||||
TOrganizationsUpdate,
|
||||
@ -338,6 +356,31 @@ declare module "knex/types/tables" {
|
||||
TIdentityProjectAdditionalPrivilegeInsert,
|
||||
TIdentityProjectAdditionalPrivilegeUpdate
|
||||
>;
|
||||
|
||||
[TableName.AccessApprovalPolicy]: Knex.CompositeTableType<
|
||||
TAccessApprovalPolicies,
|
||||
TAccessApprovalPoliciesInsert,
|
||||
TAccessApprovalPoliciesUpdate
|
||||
>;
|
||||
|
||||
[TableName.AccessApprovalPolicyApprover]: Knex.CompositeTableType<
|
||||
TAccessApprovalPoliciesApprovers,
|
||||
TAccessApprovalPoliciesApproversInsert,
|
||||
TAccessApprovalPoliciesApproversUpdate
|
||||
>;
|
||||
|
||||
[TableName.AccessApprovalRequest]: Knex.CompositeTableType<
|
||||
TAccessApprovalRequests,
|
||||
TAccessApprovalRequestsInsert,
|
||||
TAccessApprovalRequestsUpdate
|
||||
>;
|
||||
|
||||
[TableName.AccessApprovalRequestReviewer]: Knex.CompositeTableType<
|
||||
TAccessApprovalRequestsReviewers,
|
||||
TAccessApprovalRequestsReviewersInsert,
|
||||
TAccessApprovalRequestsReviewersUpdate
|
||||
>;
|
||||
|
||||
[TableName.ScimToken]: Knex.CompositeTableType<TScimTokens, TScimTokensInsert, TScimTokensUpdate>;
|
||||
[TableName.SecretApprovalPolicy]: Knex.CompositeTableType<
|
||||
TSecretApprovalPolicies,
|
||||
@ -398,8 +441,14 @@ declare module "knex/types/tables" {
|
||||
>;
|
||||
[TableName.SamlConfig]: Knex.CompositeTableType<TSamlConfigs, TSamlConfigsInsert, TSamlConfigsUpdate>;
|
||||
[TableName.LdapConfig]: Knex.CompositeTableType<TLdapConfigs, TLdapConfigsInsert, TLdapConfigsUpdate>;
|
||||
[TableName.LdapGroupMap]: Knex.CompositeTableType<TLdapGroupMaps, TLdapGroupMapsInsert, TLdapGroupMapsUpdate>;
|
||||
[TableName.OrgBot]: Knex.CompositeTableType<TOrgBots, TOrgBotsInsert, TOrgBotsUpdate>;
|
||||
[TableName.AuditLog]: Knex.CompositeTableType<TAuditLogs, TAuditLogsInsert, TAuditLogsUpdate>;
|
||||
[TableName.AuditLogStream]: Knex.CompositeTableType<
|
||||
TAuditLogStreams,
|
||||
TAuditLogStreamsInsert,
|
||||
TAuditLogStreamsUpdate
|
||||
>;
|
||||
[TableName.GitAppInstallSession]: Knex.CompositeTableType<
|
||||
TGitAppInstallSessions,
|
||||
TGitAppInstallSessionsInsert,
|
||||
|
@ -42,6 +42,7 @@ export async function up(knex: Knex): Promise<void> {
|
||||
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
|
||||
.whereNotNull("userId") // Ensure that the userId is not null
|
||||
.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
|
@ -0,0 +1,47 @@
|
||||
import { Knex } from "knex";
|
||||
|
||||
import { ProjectMembershipRole, TableName } from "../schemas";
|
||||
|
||||
export async function up(knex: Knex): Promise<void> {
|
||||
const doesProjectRoleFieldExist = await knex.schema.hasColumn(TableName.ProjectMembership, "role");
|
||||
const doesProjectRoleIdFieldExist = await knex.schema.hasColumn(TableName.ProjectMembership, "roleId");
|
||||
await knex.schema.alterTable(TableName.ProjectMembership, (t) => {
|
||||
if (doesProjectRoleFieldExist) t.dropColumn("roleId");
|
||||
if (doesProjectRoleIdFieldExist) t.dropColumn("role");
|
||||
});
|
||||
|
||||
const doesIdentityProjectRoleFieldExist = await knex.schema.hasColumn(TableName.IdentityProjectMembership, "role");
|
||||
const doesIdentityProjectRoleIdFieldExist = await knex.schema.hasColumn(
|
||||
TableName.IdentityProjectMembership,
|
||||
"roleId"
|
||||
);
|
||||
await knex.schema.alterTable(TableName.IdentityProjectMembership, (t) => {
|
||||
if (doesIdentityProjectRoleFieldExist) t.dropColumn("roleId");
|
||||
if (doesIdentityProjectRoleIdFieldExist) t.dropColumn("role");
|
||||
});
|
||||
}
|
||||
|
||||
export async function down(knex: Knex): Promise<void> {
|
||||
const doesProjectRoleFieldExist = await knex.schema.hasColumn(TableName.ProjectMembership, "role");
|
||||
const doesProjectRoleIdFieldExist = await knex.schema.hasColumn(TableName.ProjectMembership, "roleId");
|
||||
await knex.schema.alterTable(TableName.ProjectMembership, (t) => {
|
||||
if (!doesProjectRoleFieldExist) t.string("role").defaultTo(ProjectMembershipRole.Member);
|
||||
if (!doesProjectRoleIdFieldExist) {
|
||||
t.uuid("roleId");
|
||||
t.foreign("roleId").references("id").inTable(TableName.ProjectRoles);
|
||||
}
|
||||
});
|
||||
|
||||
const doesIdentityProjectRoleFieldExist = await knex.schema.hasColumn(TableName.IdentityProjectMembership, "role");
|
||||
const doesIdentityProjectRoleIdFieldExist = await knex.schema.hasColumn(
|
||||
TableName.IdentityProjectMembership,
|
||||
"roleId"
|
||||
);
|
||||
await knex.schema.alterTable(TableName.IdentityProjectMembership, (t) => {
|
||||
if (!doesIdentityProjectRoleFieldExist) t.string("role").defaultTo(ProjectMembershipRole.Member);
|
||||
if (!doesIdentityProjectRoleIdFieldExist) {
|
||||
t.uuid("roleId");
|
||||
t.foreign("roleId").references("id").inTable(TableName.ProjectRoles);
|
||||
}
|
||||
});
|
||||
}
|
@ -0,0 +1,15 @@
|
||||
import { Knex } from "knex";
|
||||
|
||||
import { TableName } from "../schemas";
|
||||
|
||||
export async function up(knex: Knex): Promise<void> {
|
||||
await knex.schema.alterTable(TableName.UserGroupMembership, (t) => {
|
||||
t.boolean("isPending").notNullable().defaultTo(false);
|
||||
});
|
||||
}
|
||||
|
||||
export async function down(knex: Knex): Promise<void> {
|
||||
await knex.schema.alterTable(TableName.UserGroupMembership, (t) => {
|
||||
t.dropColumn("isPending");
|
||||
});
|
||||
}
|
@ -0,0 +1,34 @@
|
||||
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.LdapGroupMap))) {
|
||||
await knex.schema.createTable(TableName.LdapGroupMap, (t) => {
|
||||
t.uuid("id", { primaryKey: true }).defaultTo(knex.fn.uuid());
|
||||
t.uuid("ldapConfigId").notNullable();
|
||||
t.foreign("ldapConfigId").references("id").inTable(TableName.LdapConfig).onDelete("CASCADE");
|
||||
t.string("ldapGroupCN").notNullable();
|
||||
t.uuid("groupId").notNullable();
|
||||
t.foreign("groupId").references("id").inTable(TableName.Groups).onDelete("CASCADE");
|
||||
t.unique(["ldapGroupCN", "groupId", "ldapConfigId"]);
|
||||
});
|
||||
}
|
||||
|
||||
await createOnUpdateTrigger(knex, TableName.LdapGroupMap);
|
||||
|
||||
await knex.schema.alterTable(TableName.LdapConfig, (t) => {
|
||||
t.string("groupSearchBase").notNullable().defaultTo("");
|
||||
t.string("groupSearchFilter").notNullable().defaultTo("");
|
||||
});
|
||||
}
|
||||
|
||||
export async function down(knex: Knex): Promise<void> {
|
||||
await knex.schema.dropTableIfExists(TableName.LdapGroupMap);
|
||||
await dropOnUpdateTrigger(knex, TableName.LdapGroupMap);
|
||||
await knex.schema.alterTable(TableName.LdapConfig, (t) => {
|
||||
t.dropColumn("groupSearchBase");
|
||||
t.dropColumn("groupSearchFilter");
|
||||
});
|
||||
}
|
@ -0,0 +1,15 @@
|
||||
import { Knex } from "knex";
|
||||
|
||||
import { TableName } from "../schemas";
|
||||
|
||||
export async function up(knex: Knex): Promise<void> {
|
||||
await knex.schema.alterTable(TableName.LdapConfig, (t) => {
|
||||
t.string("searchFilter").notNullable().defaultTo("");
|
||||
});
|
||||
}
|
||||
|
||||
export async function down(knex: Knex): Promise<void> {
|
||||
await knex.schema.alterTable(TableName.LdapConfig, (t) => {
|
||||
t.dropColumn("searchFilter");
|
||||
});
|
||||
}
|
28
backend/src/db/migrations/20240429154610_audit-log-index.ts
Normal file
28
backend/src/db/migrations/20240429154610_audit-log-index.ts
Normal file
@ -0,0 +1,28 @@
|
||||
import { Knex } from "knex";
|
||||
|
||||
import { TableName } from "../schemas";
|
||||
|
||||
export async function up(knex: Knex): Promise<void> {
|
||||
const doesOrgIdExist = await knex.schema.hasColumn(TableName.AuditLog, "orgId");
|
||||
const doesProjectIdExist = await knex.schema.hasColumn(TableName.AuditLog, "projectId");
|
||||
const doesCreatedAtExist = await knex.schema.hasColumn(TableName.AuditLog, "createdAt");
|
||||
if (await knex.schema.hasTable(TableName.AuditLog)) {
|
||||
await knex.schema.alterTable(TableName.AuditLog, (t) => {
|
||||
if (doesProjectIdExist && doesCreatedAtExist) t.index(["projectId", "createdAt"]);
|
||||
if (doesOrgIdExist && doesCreatedAtExist) t.index(["orgId", "createdAt"]);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export async function down(knex: Knex): Promise<void> {
|
||||
const doesOrgIdExist = await knex.schema.hasColumn(TableName.AuditLog, "orgId");
|
||||
const doesProjectIdExist = await knex.schema.hasColumn(TableName.AuditLog, "projectId");
|
||||
const doesCreatedAtExist = await knex.schema.hasColumn(TableName.AuditLog, "createdAt");
|
||||
|
||||
if (await knex.schema.hasTable(TableName.AuditLog)) {
|
||||
await knex.schema.alterTable(TableName.AuditLog, (t) => {
|
||||
if (doesProjectIdExist && doesCreatedAtExist) t.dropIndex(["projectId", "createdAt"]);
|
||||
if (doesOrgIdExist && doesCreatedAtExist) t.dropIndex(["orgId", "createdAt"]);
|
||||
});
|
||||
}
|
||||
}
|
28
backend/src/db/migrations/20240503101144_audit-log-stream.ts
Normal file
28
backend/src/db/migrations/20240503101144_audit-log-stream.ts
Normal file
@ -0,0 +1,28 @@
|
||||
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.AuditLogStream))) {
|
||||
await knex.schema.createTable(TableName.AuditLogStream, (t) => {
|
||||
t.uuid("id", { primaryKey: true }).defaultTo(knex.fn.uuid());
|
||||
t.string("url").notNullable();
|
||||
t.text("encryptedHeadersCiphertext");
|
||||
t.text("encryptedHeadersIV");
|
||||
t.text("encryptedHeadersTag");
|
||||
t.string("encryptedHeadersAlgorithm");
|
||||
t.string("encryptedHeadersKeyEncoding");
|
||||
t.uuid("orgId").notNullable();
|
||||
t.foreign("orgId").references("id").inTable(TableName.Organization).onDelete("CASCADE");
|
||||
t.timestamps(true, true, true);
|
||||
});
|
||||
}
|
||||
|
||||
await createOnUpdateTrigger(knex, TableName.AuditLogStream);
|
||||
}
|
||||
|
||||
export async function down(knex: Knex): Promise<void> {
|
||||
await dropOnUpdateTrigger(knex, TableName.AuditLogStream);
|
||||
await knex.schema.dropTableIfExists(TableName.AuditLogStream);
|
||||
}
|
@ -0,0 +1,54 @@
|
||||
import { Knex } from "knex";
|
||||
|
||||
import { TableName } from "../schemas";
|
||||
|
||||
export async function up(knex: Knex): Promise<void> {
|
||||
const isUsersTablePresent = await knex.schema.hasTable(TableName.Users);
|
||||
if (isUsersTablePresent) {
|
||||
const hasIsEmailVerifiedColumn = await knex.schema.hasColumn(TableName.Users, "isEmailVerified");
|
||||
|
||||
if (!hasIsEmailVerifiedColumn) {
|
||||
await knex.schema.alterTable(TableName.Users, (t) => {
|
||||
t.boolean("isEmailVerified").defaultTo(false);
|
||||
});
|
||||
}
|
||||
|
||||
// Backfilling the isEmailVerified to true where isAccepted is true
|
||||
await knex(TableName.Users).update({ isEmailVerified: true }).where("isAccepted", true);
|
||||
}
|
||||
|
||||
const isUserAliasTablePresent = await knex.schema.hasTable(TableName.UserAliases);
|
||||
if (isUserAliasTablePresent) {
|
||||
await knex.schema.alterTable(TableName.UserAliases, (t) => {
|
||||
t.string("username").nullable().alter();
|
||||
});
|
||||
}
|
||||
|
||||
const isSuperAdminTablePresent = await knex.schema.hasTable(TableName.SuperAdmin);
|
||||
if (isSuperAdminTablePresent) {
|
||||
await knex.schema.alterTable(TableName.SuperAdmin, (t) => {
|
||||
t.boolean("trustSamlEmails").defaultTo(false);
|
||||
t.boolean("trustLdapEmails").defaultTo(false);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export async function down(knex: Knex): Promise<void> {
|
||||
if (await knex.schema.hasColumn(TableName.Users, "isEmailVerified")) {
|
||||
await knex.schema.alterTable(TableName.Users, (t) => {
|
||||
t.dropColumn("isEmailVerified");
|
||||
});
|
||||
}
|
||||
|
||||
if (await knex.schema.hasColumn(TableName.SuperAdmin, "trustSamlEmails")) {
|
||||
await knex.schema.alterTable(TableName.SuperAdmin, (t) => {
|
||||
t.dropColumn("trustSamlEmails");
|
||||
});
|
||||
}
|
||||
|
||||
if (await knex.schema.hasColumn(TableName.SuperAdmin, "trustLdapEmails")) {
|
||||
await knex.schema.alterTable(TableName.SuperAdmin, (t) => {
|
||||
t.dropColumn("trustLdapEmails");
|
||||
});
|
||||
}
|
||||
}
|
@ -0,0 +1,41 @@
|
||||
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.AccessApprovalPolicy))) {
|
||||
await knex.schema.createTable(TableName.AccessApprovalPolicy, (t) => {
|
||||
t.uuid("id", { primaryKey: true }).defaultTo(knex.fn.uuid());
|
||||
t.string("name").notNullable();
|
||||
t.integer("approvals").defaultTo(1).notNullable();
|
||||
t.string("secretPath");
|
||||
|
||||
t.uuid("envId").notNullable();
|
||||
t.foreign("envId").references("id").inTable(TableName.Environment).onDelete("CASCADE");
|
||||
t.timestamps(true, true, true);
|
||||
});
|
||||
await createOnUpdateTrigger(knex, TableName.AccessApprovalPolicy);
|
||||
}
|
||||
|
||||
if (!(await knex.schema.hasTable(TableName.AccessApprovalPolicyApprover))) {
|
||||
await knex.schema.createTable(TableName.AccessApprovalPolicyApprover, (t) => {
|
||||
t.uuid("id", { primaryKey: true }).defaultTo(knex.fn.uuid());
|
||||
t.uuid("approverId").notNullable();
|
||||
t.foreign("approverId").references("id").inTable(TableName.ProjectMembership).onDelete("CASCADE");
|
||||
|
||||
t.uuid("policyId").notNullable();
|
||||
t.foreign("policyId").references("id").inTable(TableName.AccessApprovalPolicy).onDelete("CASCADE");
|
||||
t.timestamps(true, true, true);
|
||||
});
|
||||
await createOnUpdateTrigger(knex, TableName.AccessApprovalPolicyApprover);
|
||||
}
|
||||
}
|
||||
|
||||
export async function down(knex: Knex): Promise<void> {
|
||||
await knex.schema.dropTableIfExists(TableName.AccessApprovalPolicyApprover);
|
||||
await knex.schema.dropTableIfExists(TableName.AccessApprovalPolicy);
|
||||
|
||||
await dropOnUpdateTrigger(knex, TableName.AccessApprovalPolicyApprover);
|
||||
await dropOnUpdateTrigger(knex, TableName.AccessApprovalPolicy);
|
||||
}
|
51
backend/src/db/migrations/20240507162141_access
Normal file
51
backend/src/db/migrations/20240507162141_access
Normal file
@ -0,0 +1,51 @@
|
||||
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.AccessApprovalRequest))) {
|
||||
await knex.schema.createTable(TableName.AccessApprovalRequest, (t) => {
|
||||
t.uuid("id", { primaryKey: true }).defaultTo(knex.fn.uuid());
|
||||
|
||||
t.uuid("policyId").notNullable();
|
||||
t.foreign("policyId").references("id").inTable(TableName.AccessApprovalPolicy).onDelete("CASCADE");
|
||||
|
||||
t.uuid("privilegeId").nullable();
|
||||
t.foreign("privilegeId").references("id").inTable(TableName.ProjectUserAdditionalPrivilege).onDelete("CASCADE");
|
||||
|
||||
t.uuid("requestedBy").notNullable();
|
||||
t.foreign("requestedBy").references("id").inTable(TableName.ProjectMembership).onDelete("CASCADE");
|
||||
|
||||
// We use these values to create the actual privilege at a later point in time.
|
||||
t.boolean("isTemporary").notNullable();
|
||||
t.string("temporaryRange").nullable();
|
||||
|
||||
t.jsonb("permissions").notNullable();
|
||||
|
||||
t.timestamps(true, true, true);
|
||||
});
|
||||
}
|
||||
await createOnUpdateTrigger(knex, TableName.AccessApprovalRequest);
|
||||
|
||||
if (!(await knex.schema.hasTable(TableName.AccessApprovalRequestReviewer))) {
|
||||
await knex.schema.createTable(TableName.AccessApprovalRequestReviewer, (t) => {
|
||||
t.uuid("id", { primaryKey: true }).defaultTo(knex.fn.uuid());
|
||||
t.uuid("member").notNullable();
|
||||
t.foreign("member").references("id").inTable(TableName.ProjectMembership).onDelete("CASCADE");
|
||||
t.string("status").notNullable();
|
||||
t.uuid("requestId").notNullable();
|
||||
t.foreign("requestId").references("id").inTable(TableName.AccessApprovalRequest).onDelete("CASCADE");
|
||||
t.timestamps(true, true, true);
|
||||
});
|
||||
}
|
||||
await createOnUpdateTrigger(knex, TableName.AccessApprovalRequestReviewer);
|
||||
}
|
||||
|
||||
export async function down(knex: Knex): Promise<void> {
|
||||
await knex.schema.dropTableIfExists(TableName.AccessApprovalRequestReviewer);
|
||||
await knex.schema.dropTableIfExists(TableName.AccessApprovalRequest);
|
||||
|
||||
await dropOnUpdateTrigger(knex, TableName.AccessApprovalRequestReviewer);
|
||||
await dropOnUpdateTrigger(knex, TableName.AccessApprovalRequest);
|
||||
}
|
25
backend/src/db/schemas/access-approval-policies-approvers.ts
Normal file
25
backend/src/db/schemas/access-approval-policies-approvers.ts
Normal file
@ -0,0 +1,25 @@
|
||||
// Code generated by automation script, DO NOT EDIT.
|
||||
// Automated by pulling database and generating zod schema
|
||||
// To update. Just run npm run generate:schema
|
||||
// Written by akhilmhdh.
|
||||
|
||||
import { z } from "zod";
|
||||
|
||||
import { TImmutableDBKeys } from "./models";
|
||||
|
||||
export const AccessApprovalPoliciesApproversSchema = z.object({
|
||||
id: z.string().uuid(),
|
||||
approverId: z.string().uuid(),
|
||||
policyId: z.string().uuid(),
|
||||
createdAt: z.date(),
|
||||
updatedAt: z.date()
|
||||
});
|
||||
|
||||
export type TAccessApprovalPoliciesApprovers = z.infer<typeof AccessApprovalPoliciesApproversSchema>;
|
||||
export type TAccessApprovalPoliciesApproversInsert = Omit<
|
||||
z.input<typeof AccessApprovalPoliciesApproversSchema>,
|
||||
TImmutableDBKeys
|
||||
>;
|
||||
export type TAccessApprovalPoliciesApproversUpdate = Partial<
|
||||
Omit<z.input<typeof AccessApprovalPoliciesApproversSchema>, TImmutableDBKeys>
|
||||
>;
|
24
backend/src/db/schemas/access-approval-policies.ts
Normal file
24
backend/src/db/schemas/access-approval-policies.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 AccessApprovalPoliciesSchema = z.object({
|
||||
id: z.string().uuid(),
|
||||
name: z.string(),
|
||||
approvals: z.number().default(1),
|
||||
envId: z.string().uuid(),
|
||||
secretPath: z.string().nullable().optional(),
|
||||
createdAt: z.date(),
|
||||
updatedAt: z.date()
|
||||
});
|
||||
|
||||
export type TAccessApprovalPolicies = z.infer<typeof AccessApprovalPoliciesSchema>;
|
||||
export type TAccessApprovalPoliciesInsert = Omit<z.input<typeof AccessApprovalPoliciesSchema>, TImmutableDBKeys>;
|
||||
export type TAccessApprovalPoliciesUpdate = Partial<
|
||||
Omit<z.input<typeof AccessApprovalPoliciesSchema>, TImmutableDBKeys>
|
||||
>;
|
26
backend/src/db/schemas/access-approval-requests-reviewers.ts
Normal file
26
backend/src/db/schemas/access-approval-requests-reviewers.ts
Normal file
@ -0,0 +1,26 @@
|
||||
// Code generated by automation script, DO NOT EDIT.
|
||||
// Automated by pulling database and generating zod schema
|
||||
// To update. Just run npm run generate:schema
|
||||
// Written by akhilmhdh.
|
||||
|
||||
import { z } from "zod";
|
||||
|
||||
import { TImmutableDBKeys } from "./models";
|
||||
|
||||
export const AccessApprovalRequestsReviewersSchema = z.object({
|
||||
id: z.string().uuid(),
|
||||
member: z.string().uuid(),
|
||||
status: z.string(),
|
||||
requestId: z.string().uuid(),
|
||||
createdAt: z.date(),
|
||||
updatedAt: z.date()
|
||||
});
|
||||
|
||||
export type TAccessApprovalRequestsReviewers = z.infer<typeof AccessApprovalRequestsReviewersSchema>;
|
||||
export type TAccessApprovalRequestsReviewersInsert = Omit<
|
||||
z.input<typeof AccessApprovalRequestsReviewersSchema>,
|
||||
TImmutableDBKeys
|
||||
>;
|
||||
export type TAccessApprovalRequestsReviewersUpdate = Partial<
|
||||
Omit<z.input<typeof AccessApprovalRequestsReviewersSchema>, TImmutableDBKeys>
|
||||
>;
|
26
backend/src/db/schemas/access-approval-requests.ts
Normal file
26
backend/src/db/schemas/access-approval-requests.ts
Normal file
@ -0,0 +1,26 @@
|
||||
// Code generated by automation script, DO NOT EDIT.
|
||||
// Automated by pulling database and generating zod schema
|
||||
// To update. Just run npm run generate:schema
|
||||
// Written by akhilmhdh.
|
||||
|
||||
import { z } from "zod";
|
||||
|
||||
import { TImmutableDBKeys } from "./models";
|
||||
|
||||
export const AccessApprovalRequestsSchema = z.object({
|
||||
id: z.string().uuid(),
|
||||
policyId: z.string().uuid(),
|
||||
privilegeId: z.string().uuid().nullable().optional(),
|
||||
requestedBy: z.string().uuid(),
|
||||
isTemporary: z.boolean(),
|
||||
temporaryRange: z.string().nullable().optional(),
|
||||
permissions: z.unknown(),
|
||||
createdAt: z.date(),
|
||||
updatedAt: z.date()
|
||||
});
|
||||
|
||||
export type TAccessApprovalRequests = z.infer<typeof AccessApprovalRequestsSchema>;
|
||||
export type TAccessApprovalRequestsInsert = Omit<z.input<typeof AccessApprovalRequestsSchema>, TImmutableDBKeys>;
|
||||
export type TAccessApprovalRequestsUpdate = Partial<
|
||||
Omit<z.input<typeof AccessApprovalRequestsSchema>, TImmutableDBKeys>
|
||||
>;
|
25
backend/src/db/schemas/audit-log-streams.ts
Normal file
25
backend/src/db/schemas/audit-log-streams.ts
Normal file
@ -0,0 +1,25 @@
|
||||
// Code generated by automation script, DO NOT EDIT.
|
||||
// Automated by pulling database and generating zod schema
|
||||
// To update. Just run npm run generate:schema
|
||||
// Written by akhilmhdh.
|
||||
|
||||
import { z } from "zod";
|
||||
|
||||
import { TImmutableDBKeys } from "./models";
|
||||
|
||||
export const AuditLogStreamsSchema = z.object({
|
||||
id: z.string().uuid(),
|
||||
url: z.string(),
|
||||
encryptedHeadersCiphertext: z.string().nullable().optional(),
|
||||
encryptedHeadersIV: z.string().nullable().optional(),
|
||||
encryptedHeadersTag: z.string().nullable().optional(),
|
||||
encryptedHeadersAlgorithm: z.string().nullable().optional(),
|
||||
encryptedHeadersKeyEncoding: z.string().nullable().optional(),
|
||||
orgId: z.string().uuid(),
|
||||
createdAt: z.date(),
|
||||
updatedAt: z.date()
|
||||
});
|
||||
|
||||
export type TAuditLogStreams = z.infer<typeof AuditLogStreamsSchema>;
|
||||
export type TAuditLogStreamsInsert = Omit<z.input<typeof AuditLogStreamsSchema>, TImmutableDBKeys>;
|
||||
export type TAuditLogStreamsUpdate = Partial<Omit<z.input<typeof AuditLogStreamsSchema>, TImmutableDBKeys>>;
|
@ -9,8 +9,6 @@ import { TImmutableDBKeys } from "./models";
|
||||
|
||||
export const IdentityProjectMembershipsSchema = z.object({
|
||||
id: z.string().uuid(),
|
||||
role: z.string(),
|
||||
roleId: z.string().uuid().nullable().optional(),
|
||||
projectId: z.string(),
|
||||
identityId: z.string().uuid(),
|
||||
createdAt: z.date(),
|
||||
|
@ -1,4 +1,9 @@
|
||||
export * from "./access-approval-policies";
|
||||
export * from "./access-approval-policies-approvers";
|
||||
export * from "./access-approval-requests";
|
||||
export * from "./access-approval-requests-reviewers";
|
||||
export * from "./api-keys";
|
||||
export * from "./audit-log-streams";
|
||||
export * from "./audit-logs";
|
||||
export * from "./auth-token-sessions";
|
||||
export * from "./auth-tokens";
|
||||
@ -22,6 +27,7 @@ export * from "./incident-contacts";
|
||||
export * from "./integration-auths";
|
||||
export * from "./integrations";
|
||||
export * from "./ldap-configs";
|
||||
export * from "./ldap-group-maps";
|
||||
export * from "./models";
|
||||
export * from "./org-bots";
|
||||
export * from "./org-memberships";
|
||||
|
@ -23,7 +23,10 @@ export const LdapConfigsSchema = z.object({
|
||||
caCertIV: z.string(),
|
||||
caCertTag: z.string(),
|
||||
createdAt: z.date(),
|
||||
updatedAt: z.date()
|
||||
updatedAt: z.date(),
|
||||
groupSearchBase: z.string().default(""),
|
||||
groupSearchFilter: z.string().default(""),
|
||||
searchFilter: z.string().default("")
|
||||
});
|
||||
|
||||
export type TLdapConfigs = z.infer<typeof LdapConfigsSchema>;
|
||||
|
19
backend/src/db/schemas/ldap-group-maps.ts
Normal file
19
backend/src/db/schemas/ldap-group-maps.ts
Normal file
@ -0,0 +1,19 @@
|
||||
// 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 LdapGroupMapsSchema = z.object({
|
||||
id: z.string().uuid(),
|
||||
ldapConfigId: z.string().uuid(),
|
||||
ldapGroupCN: z.string(),
|
||||
groupId: z.string().uuid()
|
||||
});
|
||||
|
||||
export type TLdapGroupMaps = z.infer<typeof LdapGroupMapsSchema>;
|
||||
export type TLdapGroupMapsInsert = Omit<z.input<typeof LdapGroupMapsSchema>, TImmutableDBKeys>;
|
||||
export type TLdapGroupMapsUpdate = Partial<Omit<z.input<typeof LdapGroupMapsSchema>, TImmutableDBKeys>>;
|
@ -50,6 +50,10 @@ export enum TableName {
|
||||
IdentityProjectMembershipRole = "identity_project_membership_role",
|
||||
IdentityProjectAdditionalPrivilege = "identity_project_additional_privilege",
|
||||
ScimToken = "scim_tokens",
|
||||
AccessApprovalPolicy = "access_approval_policies",
|
||||
AccessApprovalPolicyApprover = "access_approval_policies_approvers",
|
||||
AccessApprovalRequest = "access_approval_requests",
|
||||
AccessApprovalRequestReviewer = "access_approval_requests_reviewers",
|
||||
SecretApprovalPolicy = "secret_approval_policies",
|
||||
SecretApprovalPolicyApprover = "secret_approval_policies_approvers",
|
||||
SecretApprovalRequest = "secret_approval_requests",
|
||||
@ -60,7 +64,9 @@ export enum TableName {
|
||||
SecretRotationOutput = "secret_rotation_outputs",
|
||||
SamlConfig = "saml_configs",
|
||||
LdapConfig = "ldap_configs",
|
||||
LdapGroupMap = "ldap_group_maps",
|
||||
AuditLog = "audit_logs",
|
||||
AuditLogStream = "audit_log_streams",
|
||||
GitAppInstallSession = "git_app_install_sessions",
|
||||
GitAppOrg = "git_app_org",
|
||||
SecretScanningGitRisk = "secret_scanning_git_risks",
|
||||
|
@ -9,12 +9,10 @@ import { TImmutableDBKeys } from "./models";
|
||||
|
||||
export const ProjectMembershipsSchema = z.object({
|
||||
id: z.string().uuid(),
|
||||
role: z.string(),
|
||||
createdAt: z.date(),
|
||||
updatedAt: z.date(),
|
||||
userId: z.string().uuid(),
|
||||
projectId: z.string(),
|
||||
roleId: z.string().uuid().nullable().optional()
|
||||
projectId: z.string()
|
||||
});
|
||||
|
||||
export type TProjectMemberships = z.infer<typeof ProjectMembershipsSchema>;
|
||||
|
@ -14,7 +14,9 @@ export const SuperAdminSchema = z.object({
|
||||
createdAt: z.date(),
|
||||
updatedAt: z.date(),
|
||||
allowedSignUpDomain: z.string().nullable().optional(),
|
||||
instanceId: z.string().uuid().default("00000000-0000-0000-0000-000000000000")
|
||||
instanceId: z.string().uuid().default("00000000-0000-0000-0000-000000000000"),
|
||||
trustSamlEmails: z.boolean().default(false).nullable().optional(),
|
||||
trustLdapEmails: z.boolean().default(false).nullable().optional()
|
||||
});
|
||||
|
||||
export type TSuperAdmin = z.infer<typeof SuperAdminSchema>;
|
||||
|
@ -10,7 +10,7 @@ import { TImmutableDBKeys } from "./models";
|
||||
export const UserAliasesSchema = z.object({
|
||||
id: z.string().uuid(),
|
||||
userId: z.string().uuid(),
|
||||
username: z.string(),
|
||||
username: z.string().nullable().optional(),
|
||||
aliasType: z.string(),
|
||||
externalId: z.string(),
|
||||
emails: z.string().array().nullable().optional(),
|
||||
|
@ -12,7 +12,8 @@ export const UserGroupMembershipSchema = z.object({
|
||||
userId: z.string().uuid(),
|
||||
groupId: z.string().uuid(),
|
||||
createdAt: z.date(),
|
||||
updatedAt: z.date()
|
||||
updatedAt: z.date(),
|
||||
isPending: z.boolean().default(false)
|
||||
});
|
||||
|
||||
export type TUserGroupMembership = z.infer<typeof UserGroupMembershipSchema>;
|
||||
|
@ -21,7 +21,8 @@ export const UsersSchema = z.object({
|
||||
createdAt: z.date(),
|
||||
updatedAt: z.date(),
|
||||
isGhost: z.boolean().default(false),
|
||||
username: z.string()
|
||||
username: z.string(),
|
||||
isEmailVerified: z.boolean().nullable().optional()
|
||||
});
|
||||
|
||||
export type TUsers = z.infer<typeof UsersSchema>;
|
||||
|
@ -33,8 +33,7 @@ export async function seed(knex: Knex): Promise<void> {
|
||||
const projectMembership = await knex(TableName.ProjectMembership)
|
||||
.insert({
|
||||
projectId: project.id,
|
||||
userId: seedData1.id,
|
||||
role: ProjectMembershipRole.Admin
|
||||
userId: seedData1.id
|
||||
})
|
||||
.returning("*");
|
||||
await knex(TableName.ProjectUserMembershipRole).insert({
|
||||
|
@ -78,8 +78,7 @@ export async function seed(knex: Knex): Promise<void> {
|
||||
const identityProjectMembership = await knex(TableName.IdentityProjectMembership)
|
||||
.insert({
|
||||
identityId: seedData1.machineIdentity.id,
|
||||
projectId: seedData1.project.id,
|
||||
role: ProjectMembershipRole.Admin
|
||||
projectId: seedData1.project.id
|
||||
})
|
||||
.returning("*");
|
||||
|
||||
|
168
backend/src/ee/routes/v1/access-approval-policy-router.ts
Normal file
168
backend/src/ee/routes/v1/access-approval-policy-router.ts
Normal file
@ -0,0 +1,168 @@
|
||||
import { nanoid } from "nanoid";
|
||||
import { z } from "zod";
|
||||
|
||||
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
|
||||
import { sapPubSchema } from "@app/server/routes/sanitizedSchemas";
|
||||
import { AuthMode } from "@app/services/auth/auth-type";
|
||||
|
||||
export const registerAccessApprovalPolicyRouter = async (server: FastifyZodProvider) => {
|
||||
server.route({
|
||||
url: "/",
|
||||
method: "POST",
|
||||
schema: {
|
||||
body: z
|
||||
.object({
|
||||
projectSlug: z.string().trim(),
|
||||
name: z.string().optional(),
|
||||
secretPath: z.string().trim().default("/"),
|
||||
environment: z.string(),
|
||||
approvers: z.string().array().min(1),
|
||||
approvals: z.number().min(1).default(1)
|
||||
})
|
||||
.refine((data) => data.approvals <= data.approvers.length, {
|
||||
path: ["approvals"],
|
||||
message: "The number of approvals should be lower than the number of approvers."
|
||||
}),
|
||||
response: {
|
||||
200: z.object({
|
||||
approval: sapPubSchema
|
||||
})
|
||||
}
|
||||
},
|
||||
onRequest: verifyAuth([AuthMode.JWT]),
|
||||
handler: async (req) => {
|
||||
const approval = await server.services.accessApprovalPolicy.createAccessApprovalPolicy({
|
||||
actor: req.permission.type,
|
||||
actorId: req.permission.id,
|
||||
actorAuthMethod: req.permission.authMethod,
|
||||
actorOrgId: req.permission.orgId,
|
||||
...req.body,
|
||||
projectSlug: req.body.projectSlug,
|
||||
name: req.body.name ?? `${req.body.environment}-${nanoid(3)}`
|
||||
});
|
||||
return { approval };
|
||||
}
|
||||
});
|
||||
|
||||
server.route({
|
||||
url: "/",
|
||||
method: "GET",
|
||||
schema: {
|
||||
querystring: z.object({
|
||||
projectSlug: z.string().trim()
|
||||
}),
|
||||
response: {
|
||||
200: z.object({
|
||||
approvals: sapPubSchema.extend({ approvers: z.string().array(), secretPath: z.string().optional() }).array()
|
||||
})
|
||||
}
|
||||
},
|
||||
onRequest: verifyAuth([AuthMode.JWT]),
|
||||
handler: async (req) => {
|
||||
const approvals = await server.services.accessApprovalPolicy.getAccessApprovalPolicyByProjectSlug({
|
||||
actor: req.permission.type,
|
||||
actorId: req.permission.id,
|
||||
actorAuthMethod: req.permission.authMethod,
|
||||
actorOrgId: req.permission.orgId,
|
||||
projectSlug: req.query.projectSlug
|
||||
});
|
||||
return { approvals };
|
||||
}
|
||||
});
|
||||
|
||||
server.route({
|
||||
url: "/count",
|
||||
method: "GET",
|
||||
schema: {
|
||||
querystring: z.object({
|
||||
projectSlug: z.string(),
|
||||
envSlug: z.string()
|
||||
}),
|
||||
response: {
|
||||
200: z.object({
|
||||
count: z.number()
|
||||
})
|
||||
}
|
||||
},
|
||||
|
||||
onRequest: verifyAuth([AuthMode.JWT]),
|
||||
handler: async (req) => {
|
||||
const { count } = await server.services.accessApprovalPolicy.getAccessPolicyCountByEnvSlug({
|
||||
actor: req.permission.type,
|
||||
actorId: req.permission.id,
|
||||
actorAuthMethod: req.permission.authMethod,
|
||||
projectSlug: req.query.projectSlug,
|
||||
actorOrgId: req.permission.orgId,
|
||||
envSlug: req.query.envSlug
|
||||
});
|
||||
return { count };
|
||||
}
|
||||
});
|
||||
|
||||
server.route({
|
||||
url: "/:policyId",
|
||||
method: "PATCH",
|
||||
schema: {
|
||||
params: z.object({
|
||||
policyId: z.string()
|
||||
}),
|
||||
body: z
|
||||
.object({
|
||||
name: z.string().optional(),
|
||||
secretPath: z
|
||||
.string()
|
||||
.trim()
|
||||
.optional()
|
||||
.transform((val) => (val === "" ? "/" : val)),
|
||||
approvers: z.string().array().min(1),
|
||||
approvals: z.number().min(1).default(1)
|
||||
})
|
||||
.refine((data) => data.approvals <= data.approvers.length, {
|
||||
path: ["approvals"],
|
||||
message: "The number of approvals should be lower than the number of approvers."
|
||||
}),
|
||||
response: {
|
||||
200: z.object({
|
||||
approval: sapPubSchema
|
||||
})
|
||||
}
|
||||
},
|
||||
onRequest: verifyAuth([AuthMode.JWT]),
|
||||
handler: async (req) => {
|
||||
await server.services.accessApprovalPolicy.updateAccessApprovalPolicy({
|
||||
policyId: req.params.policyId,
|
||||
actor: req.permission.type,
|
||||
actorOrgId: req.permission.orgId,
|
||||
actorId: req.permission.id,
|
||||
actorAuthMethod: req.permission.authMethod,
|
||||
...req.body
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
server.route({
|
||||
url: "/:policyId",
|
||||
method: "DELETE",
|
||||
schema: {
|
||||
params: z.object({
|
||||
policyId: z.string()
|
||||
}),
|
||||
response: {
|
||||
200: z.object({
|
||||
approval: sapPubSchema
|
||||
})
|
||||
}
|
||||
},
|
||||
onRequest: verifyAuth([AuthMode.JWT]),
|
||||
handler: async (req) => {
|
||||
const approval = await server.services.accessApprovalPolicy.deleteAccessApprovalPolicy({
|
||||
actor: req.permission.type,
|
||||
actorId: req.permission.id,
|
||||
actorAuthMethod: req.permission.authMethod,
|
||||
actorOrgId: req.permission.orgId,
|
||||
policyId: req.params.policyId
|
||||
});
|
||||
return { approval };
|
||||
}
|
||||
});
|
||||
};
|
160
backend/src/ee/routes/v1/access-approval-request-router.ts
Normal file
160
backend/src/ee/routes/v1/access-approval-request-router.ts
Normal file
@ -0,0 +1,160 @@
|
||||
import { z } from "zod";
|
||||
|
||||
import { AccessApprovalRequestsReviewersSchema, AccessApprovalRequestsSchema } from "@app/db/schemas";
|
||||
import { ApprovalStatus } from "@app/ee/services/access-approval-request/access-approval-request-types";
|
||||
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
|
||||
import { AuthMode } from "@app/services/auth/auth-type";
|
||||
|
||||
export const registerAccessApprovalRequestRouter = async (server: FastifyZodProvider) => {
|
||||
server.route({
|
||||
url: "/",
|
||||
method: "POST",
|
||||
schema: {
|
||||
body: z.object({
|
||||
permissions: z.any().array(),
|
||||
isTemporary: z.boolean(),
|
||||
temporaryRange: z.string().optional()
|
||||
}),
|
||||
querystring: z.object({
|
||||
projectSlug: z.string().trim()
|
||||
}),
|
||||
response: {
|
||||
200: z.object({
|
||||
approval: AccessApprovalRequestsSchema
|
||||
})
|
||||
}
|
||||
},
|
||||
onRequest: verifyAuth([AuthMode.JWT]),
|
||||
handler: async (req) => {
|
||||
const { request } = await server.services.accessApprovalRequest.createAccessApprovalRequest({
|
||||
actor: req.permission.type,
|
||||
actorId: req.permission.id,
|
||||
actorAuthMethod: req.permission.authMethod,
|
||||
permissions: req.body.permissions,
|
||||
actorOrgId: req.permission.orgId,
|
||||
projectSlug: req.query.projectSlug,
|
||||
temporaryRange: req.body.temporaryRange,
|
||||
isTemporary: req.body.isTemporary
|
||||
});
|
||||
return { approval: request };
|
||||
}
|
||||
});
|
||||
|
||||
server.route({
|
||||
url: "/count",
|
||||
method: "GET",
|
||||
schema: {
|
||||
querystring: z.object({
|
||||
projectSlug: z.string().trim()
|
||||
}),
|
||||
response: {
|
||||
200: z.object({
|
||||
pendingCount: z.number(),
|
||||
finalizedCount: z.number()
|
||||
})
|
||||
}
|
||||
},
|
||||
onRequest: verifyAuth([AuthMode.JWT]),
|
||||
handler: async (req) => {
|
||||
const { count } = await server.services.accessApprovalRequest.getCount({
|
||||
projectSlug: req.query.projectSlug,
|
||||
actor: req.permission.type,
|
||||
actorId: req.permission.id,
|
||||
actorOrgId: req.permission.orgId,
|
||||
actorAuthMethod: req.permission.authMethod
|
||||
});
|
||||
|
||||
return { ...count };
|
||||
}
|
||||
});
|
||||
|
||||
server.route({
|
||||
url: "/",
|
||||
method: "GET",
|
||||
schema: {
|
||||
querystring: z.object({
|
||||
projectSlug: z.string().trim(),
|
||||
authorProjectMembershipId: z.string().trim().optional(),
|
||||
envSlug: z.string().trim().optional()
|
||||
}),
|
||||
response: {
|
||||
200: z.object({
|
||||
requests: AccessApprovalRequestsSchema.extend({
|
||||
environmentName: z.string(),
|
||||
isApproved: z.boolean(),
|
||||
privilege: z
|
||||
.object({
|
||||
membershipId: z.string(),
|
||||
isTemporary: z.boolean(),
|
||||
temporaryMode: z.string().nullish(),
|
||||
temporaryRange: z.string().nullish(),
|
||||
temporaryAccessStartTime: z.date().nullish(),
|
||||
temporaryAccessEndTime: z.date().nullish(),
|
||||
permissions: z.unknown()
|
||||
})
|
||||
.nullable(),
|
||||
policy: z.object({
|
||||
id: z.string(),
|
||||
name: z.string(),
|
||||
approvals: z.number(),
|
||||
approvers: z.string().array(),
|
||||
secretPath: z.string().nullish(),
|
||||
envId: z.string()
|
||||
}),
|
||||
reviewers: z
|
||||
.object({
|
||||
member: z.string(),
|
||||
status: z.string()
|
||||
})
|
||||
.array()
|
||||
}).array()
|
||||
})
|
||||
}
|
||||
},
|
||||
onRequest: verifyAuth([AuthMode.JWT]),
|
||||
handler: async (req) => {
|
||||
const { requests } = await server.services.accessApprovalRequest.listApprovalRequests({
|
||||
projectSlug: req.query.projectSlug,
|
||||
authorProjectMembershipId: req.query.authorProjectMembershipId,
|
||||
envSlug: req.query.envSlug,
|
||||
actor: req.permission.type,
|
||||
actorId: req.permission.id,
|
||||
actorOrgId: req.permission.orgId,
|
||||
actorAuthMethod: req.permission.authMethod
|
||||
});
|
||||
|
||||
return { requests };
|
||||
}
|
||||
});
|
||||
|
||||
server.route({
|
||||
url: "/:requestId/review",
|
||||
method: "POST",
|
||||
schema: {
|
||||
params: z.object({
|
||||
requestId: z.string().trim()
|
||||
}),
|
||||
body: z.object({
|
||||
status: z.enum([ApprovalStatus.APPROVED, ApprovalStatus.REJECTED])
|
||||
}),
|
||||
response: {
|
||||
200: z.object({
|
||||
review: AccessApprovalRequestsReviewersSchema
|
||||
})
|
||||
}
|
||||
},
|
||||
onRequest: verifyAuth([AuthMode.JWT]),
|
||||
handler: async (req) => {
|
||||
const review = await server.services.accessApprovalRequest.reviewAccessRequest({
|
||||
actor: req.permission.type,
|
||||
actorId: req.permission.id,
|
||||
actorOrgId: req.permission.orgId,
|
||||
actorAuthMethod: req.permission.authMethod,
|
||||
requestId: req.params.requestId,
|
||||
status: req.body.status
|
||||
});
|
||||
|
||||
return { review };
|
||||
}
|
||||
});
|
||||
};
|
215
backend/src/ee/routes/v1/audit-log-stream-router.ts
Normal file
215
backend/src/ee/routes/v1/audit-log-stream-router.ts
Normal file
@ -0,0 +1,215 @@
|
||||
import { z } from "zod";
|
||||
|
||||
import { AUDIT_LOG_STREAMS } from "@app/lib/api-docs";
|
||||
import { readLimit } from "@app/server/config/rateLimiter";
|
||||
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
|
||||
import { SanitizedAuditLogStreamSchema } from "@app/server/routes/sanitizedSchemas";
|
||||
import { AuthMode } from "@app/services/auth/auth-type";
|
||||
|
||||
export const registerAuditLogStreamRouter = async (server: FastifyZodProvider) => {
|
||||
server.route({
|
||||
method: "POST",
|
||||
url: "/",
|
||||
config: {
|
||||
rateLimit: readLimit
|
||||
},
|
||||
schema: {
|
||||
description: "Create an Audit Log Stream.",
|
||||
security: [
|
||||
{
|
||||
bearerAuth: []
|
||||
}
|
||||
],
|
||||
body: z.object({
|
||||
url: z.string().min(1).describe(AUDIT_LOG_STREAMS.CREATE.url),
|
||||
headers: z
|
||||
.object({
|
||||
key: z.string().min(1).trim().describe(AUDIT_LOG_STREAMS.CREATE.headers.key),
|
||||
value: z.string().min(1).trim().describe(AUDIT_LOG_STREAMS.CREATE.headers.value)
|
||||
})
|
||||
.describe(AUDIT_LOG_STREAMS.CREATE.headers.desc)
|
||||
.array()
|
||||
.optional()
|
||||
}),
|
||||
response: {
|
||||
200: z.object({
|
||||
auditLogStream: SanitizedAuditLogStreamSchema
|
||||
})
|
||||
}
|
||||
},
|
||||
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
||||
handler: async (req) => {
|
||||
const auditLogStream = await server.services.auditLogStream.create({
|
||||
actorId: req.permission.id,
|
||||
actor: req.permission.type,
|
||||
actorOrgId: req.permission.orgId,
|
||||
actorAuthMethod: req.permission.authMethod,
|
||||
url: req.body.url,
|
||||
headers: req.body.headers
|
||||
});
|
||||
|
||||
return { auditLogStream };
|
||||
}
|
||||
});
|
||||
|
||||
server.route({
|
||||
method: "PATCH",
|
||||
url: "/:id",
|
||||
config: {
|
||||
rateLimit: readLimit
|
||||
},
|
||||
schema: {
|
||||
description: "Update an Audit Log Stream by ID.",
|
||||
security: [
|
||||
{
|
||||
bearerAuth: []
|
||||
}
|
||||
],
|
||||
params: z.object({
|
||||
id: z.string().describe(AUDIT_LOG_STREAMS.UPDATE.id)
|
||||
}),
|
||||
body: z.object({
|
||||
url: z.string().optional().describe(AUDIT_LOG_STREAMS.UPDATE.url),
|
||||
headers: z
|
||||
.object({
|
||||
key: z.string().min(1).trim().describe(AUDIT_LOG_STREAMS.UPDATE.headers.key),
|
||||
value: z.string().min(1).trim().describe(AUDIT_LOG_STREAMS.UPDATE.headers.value)
|
||||
})
|
||||
.describe(AUDIT_LOG_STREAMS.UPDATE.headers.desc)
|
||||
.array()
|
||||
.optional()
|
||||
}),
|
||||
response: {
|
||||
200: z.object({
|
||||
auditLogStream: SanitizedAuditLogStreamSchema
|
||||
})
|
||||
}
|
||||
},
|
||||
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
||||
handler: async (req) => {
|
||||
const auditLogStream = await server.services.auditLogStream.updateById({
|
||||
actorId: req.permission.id,
|
||||
actor: req.permission.type,
|
||||
actorOrgId: req.permission.orgId,
|
||||
actorAuthMethod: req.permission.authMethod,
|
||||
id: req.params.id,
|
||||
url: req.body.url,
|
||||
headers: req.body.headers
|
||||
});
|
||||
|
||||
return { auditLogStream };
|
||||
}
|
||||
});
|
||||
|
||||
server.route({
|
||||
method: "DELETE",
|
||||
url: "/:id",
|
||||
config: {
|
||||
rateLimit: readLimit
|
||||
},
|
||||
schema: {
|
||||
description: "Delete an Audit Log Stream by ID.",
|
||||
security: [
|
||||
{
|
||||
bearerAuth: []
|
||||
}
|
||||
],
|
||||
params: z.object({
|
||||
id: z.string().describe(AUDIT_LOG_STREAMS.DELETE.id)
|
||||
}),
|
||||
response: {
|
||||
200: z.object({
|
||||
auditLogStream: SanitizedAuditLogStreamSchema
|
||||
})
|
||||
}
|
||||
},
|
||||
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
||||
handler: async (req) => {
|
||||
const auditLogStream = await server.services.auditLogStream.deleteById({
|
||||
actorId: req.permission.id,
|
||||
actor: req.permission.type,
|
||||
actorOrgId: req.permission.orgId,
|
||||
actorAuthMethod: req.permission.authMethod,
|
||||
id: req.params.id
|
||||
});
|
||||
|
||||
return { auditLogStream };
|
||||
}
|
||||
});
|
||||
|
||||
server.route({
|
||||
method: "GET",
|
||||
url: "/:id",
|
||||
config: {
|
||||
rateLimit: readLimit
|
||||
},
|
||||
schema: {
|
||||
description: "Get an Audit Log Stream by ID.",
|
||||
security: [
|
||||
{
|
||||
bearerAuth: []
|
||||
}
|
||||
],
|
||||
params: z.object({
|
||||
id: z.string().describe(AUDIT_LOG_STREAMS.GET_BY_ID.id)
|
||||
}),
|
||||
response: {
|
||||
200: z.object({
|
||||
auditLogStream: SanitizedAuditLogStreamSchema.extend({
|
||||
headers: z
|
||||
.object({
|
||||
key: z.string(),
|
||||
value: z.string()
|
||||
})
|
||||
.array()
|
||||
.optional()
|
||||
})
|
||||
})
|
||||
}
|
||||
},
|
||||
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
||||
handler: async (req) => {
|
||||
const auditLogStream = await server.services.auditLogStream.getById({
|
||||
actorId: req.permission.id,
|
||||
actor: req.permission.type,
|
||||
actorOrgId: req.permission.orgId,
|
||||
actorAuthMethod: req.permission.authMethod,
|
||||
id: req.params.id
|
||||
});
|
||||
|
||||
return { auditLogStream };
|
||||
}
|
||||
});
|
||||
|
||||
server.route({
|
||||
method: "GET",
|
||||
url: "/",
|
||||
config: {
|
||||
rateLimit: readLimit
|
||||
},
|
||||
schema: {
|
||||
description: "List Audit Log Streams.",
|
||||
security: [
|
||||
{
|
||||
bearerAuth: []
|
||||
}
|
||||
],
|
||||
response: {
|
||||
200: z.object({
|
||||
auditLogStreams: SanitizedAuditLogStreamSchema.array()
|
||||
})
|
||||
}
|
||||
},
|
||||
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
||||
handler: async (req) => {
|
||||
const auditLogStreams = await server.services.auditLogStream.list({
|
||||
actorId: req.permission.id,
|
||||
actor: req.permission.type,
|
||||
actorOrgId: req.permission.orgId,
|
||||
actorAuthMethod: req.permission.authMethod
|
||||
});
|
||||
|
||||
return { auditLogStreams };
|
||||
}
|
||||
});
|
||||
};
|
@ -1,16 +1,14 @@
|
||||
import { MongoAbility, RawRuleOf } from "@casl/ability";
|
||||
import { PackRule, packRules, unpackRules } from "@casl/ability/extra";
|
||||
import { packRules } 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 { PermissionSchema, SanitizedIdentityPrivilegeSchema } from "@app/server/routes/sanitizedSchemas";
|
||||
import { AuthMode } from "@app/services/auth/auth-type";
|
||||
|
||||
export const registerIdentityProjectAdditionalPrivilegeRouter = async (server: FastifyZodProvider) => {
|
||||
@ -41,11 +39,11 @@ export const registerIdentityProjectAdditionalPrivilegeRouter = async (server: F
|
||||
})
|
||||
.optional()
|
||||
.describe(IDENTITY_ADDITIONAL_PRIVILEGE.CREATE.slug),
|
||||
permissions: z.any().array().describe(IDENTITY_ADDITIONAL_PRIVILEGE.CREATE.permissions)
|
||||
permissions: PermissionSchema.array().describe(IDENTITY_ADDITIONAL_PRIVILEGE.CREATE.permissions)
|
||||
}),
|
||||
response: {
|
||||
200: z.object({
|
||||
privilege: IdentityProjectAdditionalPrivilegeSchema
|
||||
privilege: SanitizedIdentityPrivilegeSchema
|
||||
})
|
||||
}
|
||||
},
|
||||
@ -92,7 +90,7 @@ export const registerIdentityProjectAdditionalPrivilegeRouter = async (server: F
|
||||
})
|
||||
.optional()
|
||||
.describe(IDENTITY_ADDITIONAL_PRIVILEGE.CREATE.slug),
|
||||
permissions: z.any().array().describe(IDENTITY_ADDITIONAL_PRIVILEGE.CREATE.permissions),
|
||||
permissions: PermissionSchema.array().describe(IDENTITY_ADDITIONAL_PRIVILEGE.CREATE.permissions),
|
||||
temporaryMode: z
|
||||
.nativeEnum(IdentityProjectAdditionalPrivilegeTemporaryMode)
|
||||
.describe(IDENTITY_ADDITIONAL_PRIVILEGE.CREATE.temporaryMode),
|
||||
@ -107,7 +105,7 @@ export const registerIdentityProjectAdditionalPrivilegeRouter = async (server: F
|
||||
}),
|
||||
response: {
|
||||
200: z.object({
|
||||
privilege: IdentityProjectAdditionalPrivilegeSchema
|
||||
privilege: SanitizedIdentityPrivilegeSchema
|
||||
})
|
||||
}
|
||||
},
|
||||
@ -157,7 +155,7 @@ export const registerIdentityProjectAdditionalPrivilegeRouter = async (server: F
|
||||
message: "Slug must be a valid slug"
|
||||
})
|
||||
.describe(IDENTITY_ADDITIONAL_PRIVILEGE.UPDATE.newSlug),
|
||||
permissions: z.any().array().describe(IDENTITY_ADDITIONAL_PRIVILEGE.UPDATE.permissions),
|
||||
permissions: PermissionSchema.array().describe(IDENTITY_ADDITIONAL_PRIVILEGE.UPDATE.permissions),
|
||||
isTemporary: z.boolean().describe(IDENTITY_ADDITIONAL_PRIVILEGE.UPDATE.isTemporary),
|
||||
temporaryMode: z
|
||||
.nativeEnum(IdentityProjectAdditionalPrivilegeTemporaryMode)
|
||||
@ -175,7 +173,7 @@ export const registerIdentityProjectAdditionalPrivilegeRouter = async (server: F
|
||||
}),
|
||||
response: {
|
||||
200: z.object({
|
||||
privilege: IdentityProjectAdditionalPrivilegeSchema
|
||||
privilege: SanitizedIdentityPrivilegeSchema
|
||||
})
|
||||
}
|
||||
},
|
||||
@ -219,7 +217,7 @@ export const registerIdentityProjectAdditionalPrivilegeRouter = async (server: F
|
||||
}),
|
||||
response: {
|
||||
200: z.object({
|
||||
privilege: IdentityProjectAdditionalPrivilegeSchema
|
||||
privilege: SanitizedIdentityPrivilegeSchema
|
||||
})
|
||||
}
|
||||
},
|
||||
@ -260,7 +258,7 @@ export const registerIdentityProjectAdditionalPrivilegeRouter = async (server: F
|
||||
}),
|
||||
response: {
|
||||
200: z.object({
|
||||
privilege: IdentityProjectAdditionalPrivilegeSchema
|
||||
privilege: SanitizedIdentityPrivilegeSchema
|
||||
})
|
||||
}
|
||||
},
|
||||
@ -293,16 +291,11 @@ export const registerIdentityProjectAdditionalPrivilegeRouter = async (server: F
|
||||
],
|
||||
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)
|
||||
projectSlug: z.string().min(1).describe(IDENTITY_ADDITIONAL_PRIVILEGE.LIST.projectSlug)
|
||||
}),
|
||||
response: {
|
||||
200: z.object({
|
||||
privileges: IdentityProjectAdditionalPrivilegeSchema.array()
|
||||
privileges: SanitizedIdentityPrivilegeSchema.array()
|
||||
})
|
||||
}
|
||||
},
|
||||
@ -315,15 +308,9 @@ export const registerIdentityProjectAdditionalPrivilegeRouter = async (server: F
|
||||
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 };
|
||||
return {
|
||||
privileges
|
||||
};
|
||||
}
|
||||
});
|
||||
};
|
||||
|
@ -1,3 +1,6 @@
|
||||
import { registerAccessApprovalPolicyRouter } from "./access-approval-policy-router";
|
||||
import { registerAccessApprovalRequestRouter } from "./access-approval-request-router";
|
||||
import { registerAuditLogStreamRouter } from "./audit-log-stream-router";
|
||||
import { registerDynamicSecretLeaseRouter } from "./dynamic-secret-lease-router";
|
||||
import { registerDynamicSecretRouter } from "./dynamic-secret-router";
|
||||
import { registerGroupRouter } from "./group-router";
|
||||
@ -40,6 +43,9 @@ export const registerV1EERoutes = async (server: FastifyZodProvider) => {
|
||||
prefix: "/secret-rotation-providers"
|
||||
});
|
||||
|
||||
await server.register(registerAccessApprovalPolicyRouter, { prefix: "/access-approvals/policies" });
|
||||
await server.register(registerAccessApprovalRequestRouter, { prefix: "/access-approvals/requests" });
|
||||
|
||||
await server.register(
|
||||
async (dynamicSecretRouter) => {
|
||||
await dynamicSecretRouter.register(registerDynamicSecretRouter);
|
||||
@ -55,6 +61,7 @@ export const registerV1EERoutes = async (server: FastifyZodProvider) => {
|
||||
await server.register(registerSecretRotationRouter, { prefix: "/secret-rotations" });
|
||||
await server.register(registerSecretVersionRouter, { prefix: "/secret" });
|
||||
await server.register(registerGroupRouter, { prefix: "/groups" });
|
||||
await server.register(registerAuditLogStreamRouter, { prefix: "/audit-log-streams" });
|
||||
await server.register(
|
||||
async (privilegeRouter) => {
|
||||
await privilegeRouter.register(registerUserAdditionalPrivilegeRouter, { prefix: "/users" });
|
||||
|
@ -14,8 +14,11 @@ import { FastifyRequest } from "fastify";
|
||||
import LdapStrategy from "passport-ldapauth";
|
||||
import { z } from "zod";
|
||||
|
||||
import { LdapConfigsSchema } from "@app/db/schemas";
|
||||
import { LdapConfigsSchema, LdapGroupMapsSchema } from "@app/db/schemas";
|
||||
import { TLDAPConfig } from "@app/ee/services/ldap-config/ldap-config-types";
|
||||
import { isValidLdapFilter, searchGroups } from "@app/ee/services/ldap-config/ldap-fns";
|
||||
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";
|
||||
@ -50,20 +53,39 @@ export const registerLdapRouter = async (server: FastifyZodProvider) => {
|
||||
// eslint-disable-next-line
|
||||
async (req: IncomingMessage, user, cb) => {
|
||||
try {
|
||||
if (!user.email) throw new BadRequestError({ message: "Invalid request. Missing email." });
|
||||
const ldapConfig = (req as unknown as FastifyRequest).ldapConfig as TLDAPConfig;
|
||||
|
||||
let groups: { dn: string; cn: string }[] | undefined;
|
||||
if (ldapConfig.groupSearchBase) {
|
||||
const groupFilter = "(|(memberUid={{.Username}})(member={{.UserDN}})(uniqueMember={{.UserDN}}))";
|
||||
const groupSearchFilter = (ldapConfig.groupSearchFilter || groupFilter)
|
||||
.replace(/{{\.Username}}/g, user.uid)
|
||||
.replace(/{{\.UserDN}}/g, user.dn);
|
||||
|
||||
if (!isValidLdapFilter(groupSearchFilter)) {
|
||||
throw new Error("Generated LDAP search filter is invalid.");
|
||||
}
|
||||
|
||||
groups = await searchGroups(ldapConfig, groupSearchFilter, ldapConfig.groupSearchBase);
|
||||
}
|
||||
|
||||
const { isUserCompleted, providerAuthToken } = await server.services.ldap.ldapLogin({
|
||||
ldapConfigId: ldapConfig.id,
|
||||
externalId: user.uidNumber,
|
||||
username: user.uid,
|
||||
firstName: user.givenName,
|
||||
lastName: user.sn,
|
||||
emails: user.mail ? [user.mail] : [],
|
||||
firstName: user.givenName ?? user.cn ?? "",
|
||||
lastName: user.sn ?? "",
|
||||
email: user.mail,
|
||||
groups,
|
||||
relayState: ((req as unknown as FastifyRequest).body as { RelayState?: string }).RelayState,
|
||||
orgId: (req as unknown as FastifyRequest).ldapConfig.organization
|
||||
});
|
||||
|
||||
return cb(null, { isUserCompleted, providerAuthToken });
|
||||
} catch (err) {
|
||||
logger.error(err);
|
||||
return cb(err, false);
|
||||
} catch (error) {
|
||||
logger.error(error);
|
||||
return cb(error, false);
|
||||
}
|
||||
}
|
||||
)
|
||||
@ -117,6 +139,9 @@ export const registerLdapRouter = async (server: FastifyZodProvider) => {
|
||||
bindDN: z.string(),
|
||||
bindPass: z.string(),
|
||||
searchBase: z.string(),
|
||||
searchFilter: z.string(),
|
||||
groupSearchBase: z.string(),
|
||||
groupSearchFilter: z.string(),
|
||||
caCert: z.string()
|
||||
})
|
||||
}
|
||||
@ -148,6 +173,12 @@ export const registerLdapRouter = async (server: FastifyZodProvider) => {
|
||||
bindDN: z.string().trim(),
|
||||
bindPass: z.string().trim(),
|
||||
searchBase: z.string().trim(),
|
||||
searchFilter: z.string().trim().default("(uid={{username}})"),
|
||||
groupSearchBase: z.string().trim(),
|
||||
groupSearchFilter: z
|
||||
.string()
|
||||
.trim()
|
||||
.default("(|(memberUid={{.Username}})(member={{.UserDN}})(uniqueMember={{.UserDN}}))"),
|
||||
caCert: z.string().trim().default("")
|
||||
}),
|
||||
response: {
|
||||
@ -183,6 +214,9 @@ export const registerLdapRouter = async (server: FastifyZodProvider) => {
|
||||
bindDN: z.string().trim(),
|
||||
bindPass: z.string().trim(),
|
||||
searchBase: z.string().trim(),
|
||||
searchFilter: z.string().trim(),
|
||||
groupSearchBase: z.string().trim(),
|
||||
groupSearchFilter: z.string().trim(),
|
||||
caCert: z.string().trim()
|
||||
})
|
||||
.partial()
|
||||
@ -204,4 +238,134 @@ export const registerLdapRouter = async (server: FastifyZodProvider) => {
|
||||
return ldap;
|
||||
}
|
||||
});
|
||||
|
||||
server.route({
|
||||
method: "GET",
|
||||
url: "/config/:configId/group-maps",
|
||||
config: {
|
||||
rateLimit: readLimit
|
||||
},
|
||||
onRequest: verifyAuth([AuthMode.JWT]),
|
||||
schema: {
|
||||
params: z.object({
|
||||
configId: z.string().trim()
|
||||
}),
|
||||
response: {
|
||||
200: z.array(
|
||||
z.object({
|
||||
id: z.string(),
|
||||
ldapConfigId: z.string(),
|
||||
ldapGroupCN: z.string(),
|
||||
group: z.object({
|
||||
id: z.string(),
|
||||
name: z.string(),
|
||||
slug: z.string()
|
||||
})
|
||||
})
|
||||
)
|
||||
}
|
||||
},
|
||||
handler: async (req) => {
|
||||
const ldapGroupMaps = await server.services.ldap.getLdapGroupMaps({
|
||||
actor: req.permission.type,
|
||||
actorId: req.permission.id,
|
||||
orgId: req.permission.orgId,
|
||||
actorAuthMethod: req.permission.authMethod,
|
||||
actorOrgId: req.permission.orgId,
|
||||
ldapConfigId: req.params.configId
|
||||
});
|
||||
return ldapGroupMaps;
|
||||
}
|
||||
});
|
||||
|
||||
server.route({
|
||||
method: "POST",
|
||||
url: "/config/:configId/group-maps",
|
||||
config: {
|
||||
rateLimit: readLimit
|
||||
},
|
||||
onRequest: verifyAuth([AuthMode.JWT]),
|
||||
schema: {
|
||||
params: z.object({
|
||||
configId: z.string().trim()
|
||||
}),
|
||||
body: z.object({
|
||||
ldapGroupCN: z.string().trim(),
|
||||
groupSlug: z.string().trim()
|
||||
}),
|
||||
response: {
|
||||
200: LdapGroupMapsSchema
|
||||
}
|
||||
},
|
||||
handler: async (req) => {
|
||||
const ldapGroupMap = await server.services.ldap.createLdapGroupMap({
|
||||
actor: req.permission.type,
|
||||
actorId: req.permission.id,
|
||||
orgId: req.permission.orgId,
|
||||
actorAuthMethod: req.permission.authMethod,
|
||||
actorOrgId: req.permission.orgId,
|
||||
ldapConfigId: req.params.configId,
|
||||
...req.body
|
||||
});
|
||||
return ldapGroupMap;
|
||||
}
|
||||
});
|
||||
|
||||
server.route({
|
||||
method: "DELETE",
|
||||
url: "/config/:configId/group-maps/:groupMapId",
|
||||
config: {
|
||||
rateLimit: readLimit
|
||||
},
|
||||
onRequest: verifyAuth([AuthMode.JWT]),
|
||||
schema: {
|
||||
params: z.object({
|
||||
configId: z.string().trim(),
|
||||
groupMapId: z.string().trim()
|
||||
}),
|
||||
response: {
|
||||
200: LdapGroupMapsSchema
|
||||
}
|
||||
},
|
||||
handler: async (req) => {
|
||||
const ldapGroupMap = await server.services.ldap.deleteLdapGroupMap({
|
||||
actor: req.permission.type,
|
||||
actorId: req.permission.id,
|
||||
orgId: req.permission.orgId,
|
||||
actorAuthMethod: req.permission.authMethod,
|
||||
actorOrgId: req.permission.orgId,
|
||||
ldapConfigId: req.params.configId,
|
||||
ldapGroupMapId: req.params.groupMapId
|
||||
});
|
||||
return ldapGroupMap;
|
||||
}
|
||||
});
|
||||
|
||||
server.route({
|
||||
method: "POST",
|
||||
url: "/config/:configId/test-connection",
|
||||
config: {
|
||||
rateLimit: readLimit
|
||||
},
|
||||
onRequest: verifyAuth([AuthMode.JWT]),
|
||||
schema: {
|
||||
params: z.object({
|
||||
configId: z.string().trim()
|
||||
}),
|
||||
response: {
|
||||
200: z.boolean()
|
||||
}
|
||||
},
|
||||
handler: async (req) => {
|
||||
const result = await server.services.ldap.testLDAPConnection({
|
||||
actor: req.permission.type,
|
||||
actorId: req.permission.id,
|
||||
orgId: req.permission.orgId,
|
||||
actorAuthMethod: req.permission.authMethod,
|
||||
actorOrgId: req.permission.orgId,
|
||||
ldapConfigId: req.params.configId
|
||||
});
|
||||
return result;
|
||||
}
|
||||
});
|
||||
};
|
||||
|
@ -157,7 +157,13 @@ export const registerProjectRoleRouter = async (server: FastifyZodProvider) => {
|
||||
response: {
|
||||
200: z.object({
|
||||
data: z.object({
|
||||
membership: ProjectMembershipsSchema,
|
||||
membership: ProjectMembershipsSchema.extend({
|
||||
roles: z
|
||||
.object({
|
||||
role: z.string()
|
||||
})
|
||||
.array()
|
||||
}),
|
||||
permissions: z.any().array()
|
||||
})
|
||||
})
|
||||
|
@ -19,7 +19,6 @@ export const registerProjectRouter = async (server: FastifyZodProvider) => {
|
||||
description: "Return project secret snapshots ids",
|
||||
security: [
|
||||
{
|
||||
apiKeyAuth: [],
|
||||
bearerAuth: []
|
||||
}
|
||||
],
|
||||
@ -97,8 +96,7 @@ export const registerProjectRouter = async (server: FastifyZodProvider) => {
|
||||
description: "Return audit logs",
|
||||
security: [
|
||||
{
|
||||
bearerAuth: [],
|
||||
apiKeyAuth: []
|
||||
bearerAuth: []
|
||||
}
|
||||
],
|
||||
params: z.object({
|
||||
|
@ -102,12 +102,12 @@ export const registerSamlRouter = async (server: FastifyZodProvider) => {
|
||||
if (!profile) throw new BadRequestError({ message: "Missing profile" });
|
||||
const email = profile?.email ?? (profile?.emailAddress as string); // emailRippling is added because in Rippling the field `email` reserved
|
||||
|
||||
if (!profile.email || !profile.firstName) {
|
||||
if (!email || !profile.firstName) {
|
||||
throw new BadRequestError({ message: "Invalid request. Missing email or first name" });
|
||||
}
|
||||
|
||||
const { isUserCompleted, providerAuthToken } = await server.services.saml.samlLogin({
|
||||
username: profile.nameID ?? email,
|
||||
externalId: profile.nameID,
|
||||
email,
|
||||
firstName: profile.firstName as string,
|
||||
lastName: profile.lastName as string,
|
||||
|
@ -153,7 +153,7 @@ export const registerScimRouter = async (server: FastifyZodProvider) => {
|
||||
onRequest: verifyAuth([AuthMode.SCIM_TOKEN]),
|
||||
handler: async (req) => {
|
||||
const users = await req.server.services.scim.listScimUsers({
|
||||
offset: req.query.startIndex,
|
||||
startIndex: req.query.startIndex,
|
||||
limit: req.query.count,
|
||||
filter: req.query.filter,
|
||||
orgId: req.permission.orgId
|
||||
@ -163,11 +163,11 @@ export const registerScimRouter = async (server: FastifyZodProvider) => {
|
||||
});
|
||||
|
||||
server.route({
|
||||
url: "/Users/:userId",
|
||||
url: "/Users/:orgMembershipId",
|
||||
method: "GET",
|
||||
schema: {
|
||||
params: z.object({
|
||||
userId: z.string().trim()
|
||||
orgMembershipId: z.string().trim()
|
||||
}),
|
||||
response: {
|
||||
201: z.object({
|
||||
@ -193,7 +193,7 @@ export const registerScimRouter = async (server: FastifyZodProvider) => {
|
||||
onRequest: verifyAuth([AuthMode.SCIM_TOKEN]),
|
||||
handler: async (req) => {
|
||||
const user = await req.server.services.scim.getScimUser({
|
||||
userId: req.params.userId,
|
||||
orgMembershipId: req.params.orgMembershipId,
|
||||
orgId: req.permission.orgId
|
||||
});
|
||||
return user;
|
||||
@ -249,7 +249,7 @@ export const registerScimRouter = async (server: FastifyZodProvider) => {
|
||||
const primaryEmail = req.body.emails?.find((email) => email.primary)?.value;
|
||||
|
||||
const user = await req.server.services.scim.createScimUser({
|
||||
username: req.body.userName,
|
||||
externalId: req.body.userName,
|
||||
email: primaryEmail,
|
||||
firstName: req.body.name.givenName,
|
||||
lastName: req.body.name.familyName,
|
||||
@ -261,11 +261,11 @@ export const registerScimRouter = async (server: FastifyZodProvider) => {
|
||||
});
|
||||
|
||||
server.route({
|
||||
url: "/Users/:userId",
|
||||
url: "/Users/:orgMembershipId",
|
||||
method: "DELETE",
|
||||
schema: {
|
||||
params: z.object({
|
||||
userId: z.string().trim()
|
||||
orgMembershipId: z.string().trim()
|
||||
}),
|
||||
response: {
|
||||
200: z.object({})
|
||||
@ -274,7 +274,7 @@ export const registerScimRouter = async (server: FastifyZodProvider) => {
|
||||
onRequest: verifyAuth([AuthMode.SCIM_TOKEN]),
|
||||
handler: async (req) => {
|
||||
const user = await req.server.services.scim.deleteScimUser({
|
||||
userId: req.params.userId,
|
||||
orgMembershipId: req.params.orgMembershipId,
|
||||
orgId: req.permission.orgId
|
||||
});
|
||||
|
||||
@ -289,14 +289,28 @@ export const registerScimRouter = async (server: FastifyZodProvider) => {
|
||||
body: z.object({
|
||||
schemas: z.array(z.string()),
|
||||
displayName: z.string().trim(),
|
||||
members: z.array(z.any()).length(0).optional() // okta-specific
|
||||
members: z
|
||||
.array(
|
||||
z.object({
|
||||
value: z.string(),
|
||||
display: z.string()
|
||||
})
|
||||
)
|
||||
.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),
|
||||
members: z
|
||||
.array(
|
||||
z.object({
|
||||
value: z.string(),
|
||||
display: z.string()
|
||||
})
|
||||
)
|
||||
.optional(),
|
||||
meta: z.object({
|
||||
resourceType: z.string().trim()
|
||||
})
|
||||
@ -306,8 +320,8 @@ export const registerScimRouter = async (server: FastifyZodProvider) => {
|
||||
onRequest: verifyAuth([AuthMode.SCIM_TOKEN]),
|
||||
handler: async (req) => {
|
||||
const group = await req.server.services.scim.createScimGroup({
|
||||
displayName: req.body.displayName,
|
||||
orgId: req.permission.orgId
|
||||
orgId: req.permission.orgId,
|
||||
...req.body
|
||||
});
|
||||
|
||||
return group;
|
||||
@ -347,7 +361,7 @@ export const registerScimRouter = async (server: FastifyZodProvider) => {
|
||||
handler: async (req) => {
|
||||
const groups = await req.server.services.scim.listScimGroups({
|
||||
orgId: req.permission.orgId,
|
||||
offset: req.query.startIndex,
|
||||
startIndex: req.query.startIndex,
|
||||
limit: req.query.count
|
||||
});
|
||||
|
||||
@ -400,7 +414,12 @@ export const registerScimRouter = async (server: FastifyZodProvider) => {
|
||||
schemas: z.array(z.string()),
|
||||
id: z.string().trim(),
|
||||
displayName: z.string().trim(),
|
||||
members: z.array(z.any()).length(0)
|
||||
members: z.array(
|
||||
z.object({
|
||||
value: z.string(), // infisical orgMembershipId
|
||||
display: z.string()
|
||||
})
|
||||
)
|
||||
}),
|
||||
response: {
|
||||
200: z.object({
|
||||
@ -424,7 +443,7 @@ export const registerScimRouter = async (server: FastifyZodProvider) => {
|
||||
const group = await req.server.services.scim.updateScimGroupNamePut({
|
||||
groupId: req.params.groupId,
|
||||
orgId: req.permission.orgId,
|
||||
displayName: req.body.displayName
|
||||
...req.body
|
||||
});
|
||||
|
||||
return group;
|
||||
@ -482,8 +501,6 @@ export const registerScimRouter = async (server: FastifyZodProvider) => {
|
||||
},
|
||||
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,
|
||||
@ -517,11 +534,11 @@ export const registerScimRouter = async (server: FastifyZodProvider) => {
|
||||
});
|
||||
|
||||
server.route({
|
||||
url: "/Users/:userId",
|
||||
url: "/Users/:orgMembershipId",
|
||||
method: "PUT",
|
||||
schema: {
|
||||
params: z.object({
|
||||
userId: z.string().trim()
|
||||
orgMembershipId: z.string().trim()
|
||||
}),
|
||||
body: z.object({
|
||||
schemas: z.array(z.string()),
|
||||
@ -558,7 +575,7 @@ export const registerScimRouter = async (server: FastifyZodProvider) => {
|
||||
onRequest: verifyAuth([AuthMode.SCIM_TOKEN]),
|
||||
handler: async (req) => {
|
||||
const user = await req.server.services.scim.replaceScimUser({
|
||||
userId: req.params.userId,
|
||||
orgMembershipId: req.params.orgMembershipId,
|
||||
orgId: req.permission.orgId,
|
||||
active: req.body.active
|
||||
});
|
||||
|
@ -69,7 +69,6 @@ export const registerSnapshotRouter = async (server: FastifyZodProvider) => {
|
||||
description: "Roll back project secrets to those captured in a secret snapshot version.",
|
||||
security: [
|
||||
{
|
||||
apiKeyAuth: [],
|
||||
bearerAuth: []
|
||||
}
|
||||
],
|
||||
|
10
backend/src/ee/services/access-approval-policy/access-approval-policy-approver-dal.ts
Normal file
10
backend/src/ee/services/access-approval-policy/access-approval-policy-approver-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 TAccessApprovalPolicyApproverDALFactory = ReturnType<typeof accessApprovalPolicyApproverDALFactory>;
|
||||
|
||||
export const accessApprovalPolicyApproverDALFactory = (db: TDbClient) => {
|
||||
const accessApprovalPolicyApproverOrm = ormify(db, TableName.AccessApprovalPolicyApprover);
|
||||
return { ...accessApprovalPolicyApproverOrm };
|
||||
};
|
@ -0,0 +1,76 @@
|
||||
import { Knex } from "knex";
|
||||
|
||||
import { TDbClient } from "@app/db";
|
||||
import { TableName, TAccessApprovalPolicies } from "@app/db/schemas";
|
||||
import { DatabaseError } from "@app/lib/errors";
|
||||
import { buildFindFilter, mergeOneToManyRelation, ormify, selectAllTableCols, TFindFilter } from "@app/lib/knex";
|
||||
|
||||
export type TAccessApprovalPolicyDALFactory = ReturnType<typeof accessApprovalPolicyDALFactory>;
|
||||
|
||||
export const accessApprovalPolicyDALFactory = (db: TDbClient) => {
|
||||
const accessApprovalPolicyOrm = ormify(db, TableName.AccessApprovalPolicy);
|
||||
|
||||
const accessApprovalPolicyFindQuery = async (tx: Knex, filter: TFindFilter<TAccessApprovalPolicies>) => {
|
||||
const result = await tx(TableName.AccessApprovalPolicy)
|
||||
// eslint-disable-next-line
|
||||
.where(buildFindFilter(filter))
|
||||
.join(TableName.Environment, `${TableName.AccessApprovalPolicy}.envId`, `${TableName.Environment}.id`)
|
||||
.join(
|
||||
TableName.AccessApprovalPolicyApprover,
|
||||
`${TableName.AccessApprovalPolicy}.id`,
|
||||
`${TableName.AccessApprovalPolicyApprover}.policyId`
|
||||
)
|
||||
.select(tx.ref("approverId").withSchema(TableName.AccessApprovalPolicyApprover))
|
||||
.select(tx.ref("name").withSchema(TableName.Environment).as("envName"))
|
||||
.select(tx.ref("slug").withSchema(TableName.Environment).as("envSlug"))
|
||||
.select(tx.ref("id").withSchema(TableName.Environment).as("envId"))
|
||||
.select(tx.ref("projectId").withSchema(TableName.Environment))
|
||||
.select(selectAllTableCols(TableName.AccessApprovalPolicy));
|
||||
|
||||
return result;
|
||||
};
|
||||
|
||||
const findById = async (id: string, tx?: Knex) => {
|
||||
try {
|
||||
const doc = await accessApprovalPolicyFindQuery(tx || db, {
|
||||
[`${TableName.AccessApprovalPolicy}.id` as "id"]: id
|
||||
});
|
||||
const formatedDoc = mergeOneToManyRelation(
|
||||
doc,
|
||||
"id",
|
||||
({ approverId, envId, envName: name, envSlug: slug, ...el }) => ({
|
||||
...el,
|
||||
envId,
|
||||
environment: { id: envId, name, slug }
|
||||
}),
|
||||
({ approverId }) => approverId,
|
||||
"approvers"
|
||||
);
|
||||
return formatedDoc?.[0];
|
||||
} catch (error) {
|
||||
throw new DatabaseError({ error, name: "FindById" });
|
||||
}
|
||||
};
|
||||
|
||||
const find = async (filter: TFindFilter<TAccessApprovalPolicies & { projectId: string }>, tx?: Knex) => {
|
||||
try {
|
||||
const docs = await accessApprovalPolicyFindQuery(tx || db, filter);
|
||||
const formatedDoc = mergeOneToManyRelation(
|
||||
docs,
|
||||
"id",
|
||||
({ approverId, envId, envName: name, envSlug: slug, ...el }) => ({
|
||||
...el,
|
||||
envId,
|
||||
environment: { id: envId, name, slug }
|
||||
}),
|
||||
({ approverId }) => approverId,
|
||||
"approvers"
|
||||
);
|
||||
return formatedDoc.map((policy) => ({ ...policy, secretPath: policy.secretPath || undefined }));
|
||||
} catch (error) {
|
||||
throw new DatabaseError({ error, name: "Find" });
|
||||
}
|
||||
};
|
||||
|
||||
return { ...accessApprovalPolicyOrm, find, findById };
|
||||
};
|
@ -0,0 +1,36 @@
|
||||
import { ForbiddenError, subject } from "@casl/ability";
|
||||
|
||||
import { BadRequestError } from "@app/lib/errors";
|
||||
import { ActorType } from "@app/services/auth/auth-type";
|
||||
|
||||
import { ProjectPermissionActions, ProjectPermissionSub } from "../permission/project-permission";
|
||||
import { TVerifyApprovers } from "./access-approval-policy-types";
|
||||
|
||||
export const verifyApprovers = async ({
|
||||
userIds,
|
||||
projectId,
|
||||
orgId,
|
||||
envSlug,
|
||||
actorAuthMethod,
|
||||
secretPath,
|
||||
permissionService
|
||||
}: TVerifyApprovers) => {
|
||||
for await (const userId of userIds) {
|
||||
try {
|
||||
const { permission: approverPermission } = await permissionService.getProjectPermission(
|
||||
ActorType.USER,
|
||||
userId,
|
||||
projectId,
|
||||
actorAuthMethod,
|
||||
orgId
|
||||
);
|
||||
|
||||
ForbiddenError.from(approverPermission).throwUnlessCan(
|
||||
ProjectPermissionActions.Create,
|
||||
subject(ProjectPermissionSub.Secrets, { environment: envSlug, secretPath })
|
||||
);
|
||||
} catch (err) {
|
||||
throw new BadRequestError({ message: "One or more approvers doesn't have access to be specified secret path" });
|
||||
}
|
||||
}
|
||||
};
|
@ -0,0 +1,273 @@
|
||||
import { ForbiddenError } from "@casl/ability";
|
||||
|
||||
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service";
|
||||
import { ProjectPermissionActions, ProjectPermissionSub } from "@app/ee/services/permission/project-permission";
|
||||
import { BadRequestError } from "@app/lib/errors";
|
||||
import { TProjectDALFactory } from "@app/services/project/project-dal";
|
||||
import { TProjectEnvDALFactory } from "@app/services/project-env/project-env-dal";
|
||||
import { TProjectMembershipDALFactory } from "@app/services/project-membership/project-membership-dal";
|
||||
|
||||
import { TAccessApprovalPolicyApproverDALFactory } from "./access-approval-policy-approver-dal";
|
||||
import { TAccessApprovalPolicyDALFactory } from "./access-approval-policy-dal";
|
||||
import { verifyApprovers } from "./access-approval-policy-fns";
|
||||
import {
|
||||
TCreateAccessApprovalPolicy,
|
||||
TDeleteAccessApprovalPolicy,
|
||||
TGetAccessPolicyCountByEnvironmentDTO,
|
||||
TListAccessApprovalPoliciesDTO,
|
||||
TUpdateAccessApprovalPolicy
|
||||
} from "./access-approval-policy-types";
|
||||
|
||||
type TSecretApprovalPolicyServiceFactoryDep = {
|
||||
projectDAL: TProjectDALFactory;
|
||||
permissionService: Pick<TPermissionServiceFactory, "getProjectPermission">;
|
||||
accessApprovalPolicyDAL: TAccessApprovalPolicyDALFactory;
|
||||
projectEnvDAL: Pick<TProjectEnvDALFactory, "find" | "findOne">;
|
||||
accessApprovalPolicyApproverDAL: TAccessApprovalPolicyApproverDALFactory;
|
||||
projectMembershipDAL: Pick<TProjectMembershipDALFactory, "find">;
|
||||
};
|
||||
|
||||
export type TAccessApprovalPolicyServiceFactory = ReturnType<typeof accessApprovalPolicyServiceFactory>;
|
||||
|
||||
export const accessApprovalPolicyServiceFactory = ({
|
||||
accessApprovalPolicyDAL,
|
||||
accessApprovalPolicyApproverDAL,
|
||||
permissionService,
|
||||
projectEnvDAL,
|
||||
projectDAL,
|
||||
projectMembershipDAL
|
||||
}: TSecretApprovalPolicyServiceFactoryDep) => {
|
||||
const createAccessApprovalPolicy = async ({
|
||||
name,
|
||||
actor,
|
||||
actorId,
|
||||
actorOrgId,
|
||||
secretPath,
|
||||
actorAuthMethod,
|
||||
approvals,
|
||||
approvers,
|
||||
projectSlug,
|
||||
environment
|
||||
}: TCreateAccessApprovalPolicy) => {
|
||||
const project = await projectDAL.findProjectBySlug(projectSlug, actorOrgId);
|
||||
if (!project) throw new BadRequestError({ message: "Project not found" });
|
||||
|
||||
if (approvals > approvers.length)
|
||||
throw new BadRequestError({ message: "Approvals cannot be greater than approvers" });
|
||||
|
||||
const { permission } = await permissionService.getProjectPermission(
|
||||
actor,
|
||||
actorId,
|
||||
project.id,
|
||||
actorAuthMethod,
|
||||
actorOrgId
|
||||
);
|
||||
ForbiddenError.from(permission).throwUnlessCan(
|
||||
ProjectPermissionActions.Create,
|
||||
ProjectPermissionSub.SecretApproval
|
||||
);
|
||||
const env = await projectEnvDAL.findOne({ slug: environment, projectId: project.id });
|
||||
if (!env) throw new BadRequestError({ message: "Environment not found" });
|
||||
|
||||
const secretApprovers = await projectMembershipDAL.find({
|
||||
projectId: project.id,
|
||||
$in: { id: approvers }
|
||||
});
|
||||
|
||||
if (secretApprovers.length !== approvers.length) {
|
||||
throw new BadRequestError({ message: "Approver not found in project" });
|
||||
}
|
||||
|
||||
await verifyApprovers({
|
||||
projectId: project.id,
|
||||
orgId: actorOrgId,
|
||||
envSlug: environment,
|
||||
secretPath,
|
||||
actorAuthMethod,
|
||||
permissionService,
|
||||
userIds: secretApprovers.map((approver) => approver.userId)
|
||||
});
|
||||
|
||||
const accessApproval = await accessApprovalPolicyDAL.transaction(async (tx) => {
|
||||
const doc = await accessApprovalPolicyDAL.create(
|
||||
{
|
||||
envId: env.id,
|
||||
approvals,
|
||||
secretPath,
|
||||
name
|
||||
},
|
||||
tx
|
||||
);
|
||||
await accessApprovalPolicyApproverDAL.insertMany(
|
||||
secretApprovers.map(({ id }) => ({
|
||||
approverId: id,
|
||||
policyId: doc.id
|
||||
})),
|
||||
tx
|
||||
);
|
||||
return doc;
|
||||
});
|
||||
return { ...accessApproval, environment: env, projectId: project.id };
|
||||
};
|
||||
|
||||
const getAccessApprovalPolicyByProjectSlug = async ({
|
||||
actorId,
|
||||
actor,
|
||||
actorOrgId,
|
||||
actorAuthMethod,
|
||||
projectSlug
|
||||
}: TListAccessApprovalPoliciesDTO) => {
|
||||
const project = await projectDAL.findProjectBySlug(projectSlug, actorOrgId);
|
||||
if (!project) throw new BadRequestError({ message: "Project not found" });
|
||||
|
||||
// Anyone in the project should be able to get the policies.
|
||||
/* const { permission } = */ await permissionService.getProjectPermission(
|
||||
actor,
|
||||
actorId,
|
||||
project.id,
|
||||
actorAuthMethod,
|
||||
actorOrgId
|
||||
);
|
||||
// ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.SecretApproval);
|
||||
|
||||
const accessApprovalPolicies = await accessApprovalPolicyDAL.find({ projectId: project.id });
|
||||
return accessApprovalPolicies;
|
||||
};
|
||||
|
||||
const updateAccessApprovalPolicy = async ({
|
||||
policyId,
|
||||
approvers,
|
||||
secretPath,
|
||||
name,
|
||||
actorId,
|
||||
actor,
|
||||
actorOrgId,
|
||||
actorAuthMethod,
|
||||
approvals
|
||||
}: TUpdateAccessApprovalPolicy) => {
|
||||
const accessApprovalPolicy = await accessApprovalPolicyDAL.findById(policyId);
|
||||
if (!accessApprovalPolicy) throw new BadRequestError({ message: "Secret approval policy not found" });
|
||||
const { permission } = await permissionService.getProjectPermission(
|
||||
actor,
|
||||
actorId,
|
||||
accessApprovalPolicy.projectId,
|
||||
actorAuthMethod,
|
||||
actorOrgId
|
||||
);
|
||||
|
||||
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Edit, ProjectPermissionSub.SecretApproval);
|
||||
|
||||
const updatedPolicy = await accessApprovalPolicyDAL.transaction(async (tx) => {
|
||||
const doc = await accessApprovalPolicyDAL.updateById(
|
||||
accessApprovalPolicy.id,
|
||||
{
|
||||
approvals,
|
||||
secretPath,
|
||||
name
|
||||
},
|
||||
tx
|
||||
);
|
||||
if (approvers) {
|
||||
// Find the workspace project memberships of the users passed in the approvers array
|
||||
const secretApprovers = await projectMembershipDAL.find(
|
||||
{
|
||||
projectId: accessApprovalPolicy.projectId,
|
||||
$in: { id: approvers }
|
||||
},
|
||||
{ tx }
|
||||
);
|
||||
|
||||
await verifyApprovers({
|
||||
projectId: accessApprovalPolicy.projectId,
|
||||
orgId: actorOrgId,
|
||||
envSlug: accessApprovalPolicy.environment.slug,
|
||||
secretPath: doc.secretPath!,
|
||||
actorAuthMethod,
|
||||
permissionService,
|
||||
userIds: secretApprovers.map((approver) => approver.userId)
|
||||
});
|
||||
|
||||
if (secretApprovers.length !== approvers.length)
|
||||
throw new BadRequestError({ message: "Approvals cannot be greater than approvers" });
|
||||
await accessApprovalPolicyApproverDAL.delete({ policyId: doc.id }, tx);
|
||||
await accessApprovalPolicyApproverDAL.insertMany(
|
||||
secretApprovers.map(({ id }) => ({
|
||||
approverId: id,
|
||||
policyId: doc.id
|
||||
})),
|
||||
tx
|
||||
);
|
||||
}
|
||||
return doc;
|
||||
});
|
||||
return {
|
||||
...updatedPolicy,
|
||||
environment: accessApprovalPolicy.environment,
|
||||
projectId: accessApprovalPolicy.projectId
|
||||
};
|
||||
};
|
||||
|
||||
const deleteAccessApprovalPolicy = async ({
|
||||
policyId,
|
||||
actor,
|
||||
actorId,
|
||||
actorAuthMethod,
|
||||
actorOrgId
|
||||
}: TDeleteAccessApprovalPolicy) => {
|
||||
const policy = await accessApprovalPolicyDAL.findById(policyId);
|
||||
if (!policy) throw new BadRequestError({ message: "Secret approval policy not found" });
|
||||
|
||||
const { permission } = await permissionService.getProjectPermission(
|
||||
actor,
|
||||
actorId,
|
||||
policy.projectId,
|
||||
actorAuthMethod,
|
||||
actorOrgId
|
||||
);
|
||||
ForbiddenError.from(permission).throwUnlessCan(
|
||||
ProjectPermissionActions.Delete,
|
||||
ProjectPermissionSub.SecretApproval
|
||||
);
|
||||
|
||||
await accessApprovalPolicyDAL.deleteById(policyId);
|
||||
return policy;
|
||||
};
|
||||
|
||||
const getAccessPolicyCountByEnvSlug = async ({
|
||||
actor,
|
||||
actorOrgId,
|
||||
actorAuthMethod,
|
||||
projectSlug,
|
||||
actorId,
|
||||
envSlug
|
||||
}: TGetAccessPolicyCountByEnvironmentDTO) => {
|
||||
const project = await projectDAL.findProjectBySlug(projectSlug, actorOrgId);
|
||||
|
||||
if (!project) throw new BadRequestError({ message: "Project not found" });
|
||||
|
||||
const { membership } = await permissionService.getProjectPermission(
|
||||
actor,
|
||||
actorId,
|
||||
project.id,
|
||||
actorAuthMethod,
|
||||
actorOrgId
|
||||
);
|
||||
if (!membership) throw new BadRequestError({ message: "User not found in project" });
|
||||
|
||||
const environment = await projectEnvDAL.findOne({ projectId: project.id, slug: envSlug });
|
||||
if (!environment) throw new BadRequestError({ message: "Environment not found" });
|
||||
|
||||
const policies = await accessApprovalPolicyDAL.find({ envId: environment.id, projectId: project.id });
|
||||
if (!policies) throw new BadRequestError({ message: "No policies found" });
|
||||
|
||||
return { count: policies.length };
|
||||
};
|
||||
|
||||
return {
|
||||
getAccessPolicyCountByEnvSlug,
|
||||
createAccessApprovalPolicy,
|
||||
deleteAccessApprovalPolicy,
|
||||
updateAccessApprovalPolicy,
|
||||
getAccessApprovalPolicyByProjectSlug
|
||||
};
|
||||
};
|
@ -0,0 +1,44 @@
|
||||
import { TProjectPermission } from "@app/lib/types";
|
||||
import { ActorAuthMethod } from "@app/services/auth/auth-type";
|
||||
|
||||
import { TPermissionServiceFactory } from "../permission/permission-service";
|
||||
|
||||
export type TVerifyApprovers = {
|
||||
userIds: string[];
|
||||
permissionService: Pick<TPermissionServiceFactory, "getProjectPermission">;
|
||||
envSlug: string;
|
||||
actorAuthMethod: ActorAuthMethod;
|
||||
secretPath: string;
|
||||
projectId: string;
|
||||
orgId: string;
|
||||
};
|
||||
|
||||
export type TCreateAccessApprovalPolicy = {
|
||||
approvals: number;
|
||||
secretPath: string;
|
||||
environment: string;
|
||||
approvers: string[];
|
||||
projectSlug: string;
|
||||
name: string;
|
||||
} & Omit<TProjectPermission, "projectId">;
|
||||
|
||||
export type TUpdateAccessApprovalPolicy = {
|
||||
policyId: string;
|
||||
approvals?: number;
|
||||
approvers?: string[];
|
||||
secretPath?: string;
|
||||
name?: string;
|
||||
} & Omit<TProjectPermission, "projectId">;
|
||||
|
||||
export type TDeleteAccessApprovalPolicy = {
|
||||
policyId: string;
|
||||
} & Omit<TProjectPermission, "projectId">;
|
||||
|
||||
export type TGetAccessPolicyCountByEnvironmentDTO = {
|
||||
envSlug: string;
|
||||
projectSlug: string;
|
||||
} & Omit<TProjectPermission, "projectId">;
|
||||
|
||||
export type TListAccessApprovalPoliciesDTO = {
|
||||
projectSlug: string;
|
||||
} & Omit<TProjectPermission, "projectId">;
|
@ -0,0 +1,266 @@
|
||||
import { Knex } from "knex";
|
||||
|
||||
import { TDbClient } from "@app/db";
|
||||
import { AccessApprovalRequestsSchema, TableName, TAccessApprovalRequests } from "@app/db/schemas";
|
||||
import { DatabaseError } from "@app/lib/errors";
|
||||
import { ormify, selectAllTableCols, sqlNestRelationships, TFindFilter } from "@app/lib/knex";
|
||||
|
||||
import { ApprovalStatus } from "./access-approval-request-types";
|
||||
|
||||
export type TAccessApprovalRequestDALFactory = ReturnType<typeof accessApprovalRequestDALFactory>;
|
||||
|
||||
export const accessApprovalRequestDALFactory = (db: TDbClient) => {
|
||||
const accessApprovalRequestOrm = ormify(db, TableName.AccessApprovalRequest);
|
||||
|
||||
const findRequestsWithPrivilegeByPolicyIds = async (policyIds: string[]) => {
|
||||
try {
|
||||
const docs = await db(TableName.AccessApprovalRequest)
|
||||
.whereIn(`${TableName.AccessApprovalRequest}.policyId`, policyIds)
|
||||
|
||||
.leftJoin(
|
||||
TableName.ProjectUserAdditionalPrivilege,
|
||||
`${TableName.AccessApprovalRequest}.privilegeId`,
|
||||
`${TableName.ProjectUserAdditionalPrivilege}.id`
|
||||
)
|
||||
.leftJoin(
|
||||
TableName.AccessApprovalPolicy,
|
||||
`${TableName.AccessApprovalRequest}.policyId`,
|
||||
`${TableName.AccessApprovalPolicy}.id`
|
||||
)
|
||||
|
||||
.leftJoin(
|
||||
TableName.AccessApprovalRequestReviewer,
|
||||
`${TableName.AccessApprovalRequest}.id`,
|
||||
`${TableName.AccessApprovalRequestReviewer}.requestId`
|
||||
)
|
||||
.leftJoin(
|
||||
TableName.AccessApprovalPolicyApprover,
|
||||
`${TableName.AccessApprovalPolicy}.id`,
|
||||
`${TableName.AccessApprovalPolicyApprover}.policyId`
|
||||
)
|
||||
|
||||
.leftJoin(TableName.Environment, `${TableName.AccessApprovalPolicy}.envId`, `${TableName.Environment}.id`)
|
||||
|
||||
.select(selectAllTableCols(TableName.AccessApprovalRequest))
|
||||
.select(
|
||||
db.ref("id").withSchema(TableName.AccessApprovalPolicy).as("policyId"),
|
||||
db.ref("name").withSchema(TableName.AccessApprovalPolicy).as("policyName"),
|
||||
db.ref("approvals").withSchema(TableName.AccessApprovalPolicy).as("policyApprovals"),
|
||||
db.ref("secretPath").withSchema(TableName.AccessApprovalPolicy).as("policySecretPath"),
|
||||
db.ref("envId").withSchema(TableName.AccessApprovalPolicy).as("policyEnvId")
|
||||
)
|
||||
|
||||
.select(db.ref("approverId").withSchema(TableName.AccessApprovalPolicyApprover))
|
||||
|
||||
.select(
|
||||
db.ref("projectId").withSchema(TableName.Environment),
|
||||
db.ref("slug").withSchema(TableName.Environment).as("envSlug"),
|
||||
db.ref("name").withSchema(TableName.Environment).as("envName")
|
||||
)
|
||||
|
||||
.select(
|
||||
db.ref("member").withSchema(TableName.AccessApprovalRequestReviewer).as("reviewerMemberId"),
|
||||
db.ref("status").withSchema(TableName.AccessApprovalRequestReviewer).as("reviewerStatus")
|
||||
)
|
||||
|
||||
.select(
|
||||
db
|
||||
.ref("projectMembershipId")
|
||||
.withSchema(TableName.ProjectUserAdditionalPrivilege)
|
||||
.as("privilegeMembershipId"),
|
||||
db.ref("isTemporary").withSchema(TableName.ProjectUserAdditionalPrivilege).as("privilegeIsTemporary"),
|
||||
db.ref("temporaryMode").withSchema(TableName.ProjectUserAdditionalPrivilege).as("privilegeTemporaryMode"),
|
||||
db.ref("temporaryRange").withSchema(TableName.ProjectUserAdditionalPrivilege).as("privilegeTemporaryRange"),
|
||||
db
|
||||
.ref("temporaryAccessStartTime")
|
||||
.withSchema(TableName.ProjectUserAdditionalPrivilege)
|
||||
.as("privilegeTemporaryAccessStartTime"),
|
||||
db
|
||||
.ref("temporaryAccessEndTime")
|
||||
.withSchema(TableName.ProjectUserAdditionalPrivilege)
|
||||
.as("privilegeTemporaryAccessEndTime"),
|
||||
|
||||
db.ref("permissions").withSchema(TableName.ProjectUserAdditionalPrivilege).as("privilegePermissions")
|
||||
)
|
||||
.orderBy(`${TableName.AccessApprovalRequest}.createdAt`, "desc");
|
||||
|
||||
const formattedDocs = sqlNestRelationships({
|
||||
data: docs,
|
||||
key: "id",
|
||||
parentMapper: (doc) => ({
|
||||
...AccessApprovalRequestsSchema.parse(doc),
|
||||
projectId: doc.projectId,
|
||||
environment: doc.envSlug,
|
||||
environmentName: doc.envName,
|
||||
policy: {
|
||||
id: doc.policyId,
|
||||
name: doc.policyName,
|
||||
approvals: doc.policyApprovals,
|
||||
secretPath: doc.policySecretPath,
|
||||
envId: doc.policyEnvId
|
||||
},
|
||||
privilege: doc.privilegeId
|
||||
? {
|
||||
membershipId: doc.privilegeMembershipId,
|
||||
isTemporary: doc.privilegeIsTemporary,
|
||||
temporaryMode: doc.privilegeTemporaryMode,
|
||||
temporaryRange: doc.privilegeTemporaryRange,
|
||||
temporaryAccessStartTime: doc.privilegeTemporaryAccessStartTime,
|
||||
temporaryAccessEndTime: doc.privilegeTemporaryAccessEndTime,
|
||||
permissions: doc.privilegePermissions
|
||||
}
|
||||
: null,
|
||||
|
||||
isApproved: !!doc.privilegeId
|
||||
}),
|
||||
childrenMapper: [
|
||||
{
|
||||
key: "reviewerMemberId",
|
||||
label: "reviewers" as const,
|
||||
mapper: ({ reviewerMemberId: member, reviewerStatus: status }) => (member ? { member, status } : undefined)
|
||||
},
|
||||
{ key: "approverId", label: "approvers" as const, mapper: ({ approverId }) => approverId }
|
||||
]
|
||||
});
|
||||
|
||||
if (!formattedDocs) return [];
|
||||
|
||||
return formattedDocs.map((doc) => ({
|
||||
...doc,
|
||||
policy: { ...doc.policy, approvers: doc.approvers }
|
||||
}));
|
||||
} catch (error) {
|
||||
throw new DatabaseError({ error, name: "FindRequestsWithPrivilege" });
|
||||
}
|
||||
};
|
||||
|
||||
const findQuery = (filter: TFindFilter<TAccessApprovalRequests>, tx: Knex) =>
|
||||
tx(TableName.AccessApprovalRequest)
|
||||
.where(filter)
|
||||
.join(
|
||||
TableName.AccessApprovalPolicy,
|
||||
`${TableName.AccessApprovalRequest}.policyId`,
|
||||
`${TableName.AccessApprovalPolicy}.id`
|
||||
)
|
||||
|
||||
.join(
|
||||
TableName.AccessApprovalPolicyApprover,
|
||||
`${TableName.AccessApprovalPolicy}.id`,
|
||||
`${TableName.AccessApprovalPolicyApprover}.policyId`
|
||||
)
|
||||
.leftJoin(
|
||||
TableName.AccessApprovalRequestReviewer,
|
||||
`${TableName.AccessApprovalRequest}.id`,
|
||||
`${TableName.AccessApprovalRequestReviewer}.requestId`
|
||||
)
|
||||
|
||||
.leftJoin(TableName.Environment, `${TableName.AccessApprovalPolicy}.envId`, `${TableName.Environment}.id`)
|
||||
.select(selectAllTableCols(TableName.AccessApprovalRequest))
|
||||
.select(
|
||||
tx.ref("member").withSchema(TableName.AccessApprovalRequestReviewer).as("reviewerMemberId"),
|
||||
tx.ref("status").withSchema(TableName.AccessApprovalRequestReviewer).as("reviewerStatus"),
|
||||
tx.ref("id").withSchema(TableName.AccessApprovalPolicy).as("policyId"),
|
||||
tx.ref("name").withSchema(TableName.AccessApprovalPolicy).as("policyName"),
|
||||
tx.ref("projectId").withSchema(TableName.Environment),
|
||||
tx.ref("slug").withSchema(TableName.Environment).as("environment"),
|
||||
tx.ref("secretPath").withSchema(TableName.AccessApprovalPolicy).as("policySecretPath"),
|
||||
tx.ref("approvals").withSchema(TableName.AccessApprovalPolicy).as("policyApprovals"),
|
||||
tx.ref("approverId").withSchema(TableName.AccessApprovalPolicyApprover)
|
||||
);
|
||||
|
||||
const findById = async (id: string, tx?: Knex) => {
|
||||
try {
|
||||
const sql = findQuery({ [`${TableName.AccessApprovalRequest}.id` as "id"]: id }, tx || db);
|
||||
const docs = await sql;
|
||||
const formatedDoc = sqlNestRelationships({
|
||||
data: docs,
|
||||
key: "id",
|
||||
parentMapper: (el) => ({
|
||||
...AccessApprovalRequestsSchema.parse(el),
|
||||
projectId: el.projectId,
|
||||
environment: el.environment,
|
||||
policy: {
|
||||
id: el.policyId,
|
||||
name: el.policyName,
|
||||
approvals: el.policyApprovals,
|
||||
secretPath: el.policySecretPath
|
||||
}
|
||||
}),
|
||||
childrenMapper: [
|
||||
{
|
||||
key: "reviewerMemberId",
|
||||
label: "reviewers" as const,
|
||||
mapper: ({ reviewerMemberId: member, reviewerStatus: status }) => (member ? { member, status } : undefined)
|
||||
},
|
||||
{ key: "approverId", label: "approvers" as const, mapper: ({ approverId }) => approverId }
|
||||
]
|
||||
});
|
||||
if (!formatedDoc?.[0]) return;
|
||||
return {
|
||||
...formatedDoc[0],
|
||||
policy: { ...formatedDoc[0].policy, approvers: formatedDoc[0].approvers }
|
||||
};
|
||||
} catch (error) {
|
||||
throw new DatabaseError({ error, name: "FindByIdAccessApprovalRequest" });
|
||||
}
|
||||
};
|
||||
|
||||
const getCount = async ({ projectId }: { projectId: string }) => {
|
||||
try {
|
||||
const accessRequests = await db(TableName.AccessApprovalRequest)
|
||||
.leftJoin(
|
||||
TableName.AccessApprovalPolicy,
|
||||
`${TableName.AccessApprovalRequest}.policyId`,
|
||||
`${TableName.AccessApprovalPolicy}.id`
|
||||
)
|
||||
.leftJoin(TableName.Environment, `${TableName.AccessApprovalPolicy}.envId`, `${TableName.Environment}.id`)
|
||||
.leftJoin(
|
||||
TableName.ProjectUserAdditionalPrivilege,
|
||||
`${TableName.AccessApprovalRequest}.privilegeId`,
|
||||
`${TableName.ProjectUserAdditionalPrivilege}.id`
|
||||
)
|
||||
|
||||
.leftJoin(
|
||||
TableName.AccessApprovalRequestReviewer,
|
||||
`${TableName.AccessApprovalRequest}.id`,
|
||||
`${TableName.AccessApprovalRequestReviewer}.requestId`
|
||||
)
|
||||
|
||||
.where(`${TableName.Environment}.projectId`, projectId)
|
||||
.select(selectAllTableCols(TableName.AccessApprovalRequest))
|
||||
.select(db.ref("status").withSchema(TableName.AccessApprovalRequestReviewer).as("reviewerStatus"))
|
||||
.select(db.ref("member").withSchema(TableName.AccessApprovalRequestReviewer).as("reviewerMemberId"));
|
||||
|
||||
const formattedRequests = sqlNestRelationships({
|
||||
data: accessRequests,
|
||||
key: "id",
|
||||
parentMapper: (doc) => ({
|
||||
...AccessApprovalRequestsSchema.parse(doc)
|
||||
}),
|
||||
childrenMapper: [
|
||||
{
|
||||
key: "reviewerMemberId",
|
||||
label: "reviewers" as const,
|
||||
mapper: ({ reviewerMemberId: member, reviewerStatus: status }) => (member ? { member, status } : undefined)
|
||||
}
|
||||
]
|
||||
});
|
||||
|
||||
// an approval is pending if there is no reviewer rejections and no privilege ID is set
|
||||
const pendingApprovals = formattedRequests.filter(
|
||||
(req) => !req.privilegeId && !req.reviewers.some((r) => r.status === ApprovalStatus.REJECTED)
|
||||
);
|
||||
|
||||
// an approval is finalized if there are any rejections or a privilege ID is set
|
||||
const finalizedApprovals = formattedRequests.filter(
|
||||
(req) => req.privilegeId || req.reviewers.some((r) => r.status === ApprovalStatus.REJECTED)
|
||||
);
|
||||
|
||||
return { pendingCount: pendingApprovals.length, finalizedCount: finalizedApprovals.length };
|
||||
} catch (error) {
|
||||
throw new DatabaseError({ error, name: "GetCountAccessApprovalRequest" });
|
||||
}
|
||||
};
|
||||
|
||||
return { ...accessApprovalRequestOrm, findById, findRequestsWithPrivilegeByPolicyIds, getCount };
|
||||
};
|
@ -0,0 +1,53 @@
|
||||
import { PackRule, unpackRules } from "@casl/ability/extra";
|
||||
|
||||
import { UnauthorizedError } from "@app/lib/errors";
|
||||
|
||||
import { TVerifyPermission } from "./access-approval-request-types";
|
||||
|
||||
function filterUnique(value: string, index: number, array: string[]) {
|
||||
return array.indexOf(value) === index;
|
||||
}
|
||||
|
||||
export const verifyRequestedPermissions = ({ permissions }: TVerifyPermission) => {
|
||||
const permission = unpackRules(
|
||||
permissions as PackRule<{
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
conditions?: Record<string, any>;
|
||||
action: string;
|
||||
subject: [string];
|
||||
}>[]
|
||||
);
|
||||
|
||||
if (!permission || !permission.length) {
|
||||
throw new UnauthorizedError({ message: "No permission provided" });
|
||||
}
|
||||
|
||||
const requestedPermissions: string[] = [];
|
||||
|
||||
for (const p of permission) {
|
||||
if (p.action[0] === "read") requestedPermissions.push("Read Access");
|
||||
if (p.action[0] === "create") requestedPermissions.push("Create Access");
|
||||
if (p.action[0] === "delete") requestedPermissions.push("Delete Access");
|
||||
if (p.action[0] === "edit") requestedPermissions.push("Edit Access");
|
||||
}
|
||||
|
||||
const firstPermission = permission[0];
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars, @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-member-access
|
||||
const permissionSecretPath = firstPermission.conditions?.secretPath?.$glob;
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars, @typescript-eslint/no-unsafe-assignment
|
||||
const permissionEnv = firstPermission.conditions?.environment;
|
||||
|
||||
if (!permissionEnv || typeof permissionEnv !== "string") {
|
||||
throw new UnauthorizedError({ message: "Permission environment is not a string" });
|
||||
}
|
||||
if (!permissionSecretPath || typeof permissionSecretPath !== "string") {
|
||||
throw new UnauthorizedError({ message: "Permission path is not a string" });
|
||||
}
|
||||
|
||||
return {
|
||||
envSlug: permissionEnv,
|
||||
secretPath: permissionSecretPath,
|
||||
accessTypes: requestedPermissions.filter(filterUnique)
|
||||
};
|
||||
};
|
10
backend/src/ee/services/access-approval-request/access-approval-request-reviewer-dal.ts
Normal file
10
backend/src/ee/services/access-approval-request/access-approval-request-reviewer-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 TAccessApprovalRequestReviewerDALFactory = ReturnType<typeof accessApprovalRequestReviewerDALFactory>;
|
||||
|
||||
export const accessApprovalRequestReviewerDALFactory = (db: TDbClient) => {
|
||||
const secretApprovalRequestReviewerOrm = ormify(db, TableName.AccessApprovalRequestReviewer);
|
||||
return secretApprovalRequestReviewerOrm;
|
||||
};
|
@ -0,0 +1,369 @@
|
||||
import slugify from "@sindresorhus/slugify";
|
||||
import ms from "ms";
|
||||
|
||||
import { ProjectMembershipRole } from "@app/db/schemas";
|
||||
import { getConfig } from "@app/lib/config/env";
|
||||
import { BadRequestError, UnauthorizedError } from "@app/lib/errors";
|
||||
import { alphaNumericNanoId } from "@app/lib/nanoid";
|
||||
import { TProjectDALFactory } from "@app/services/project/project-dal";
|
||||
import { TProjectEnvDALFactory } from "@app/services/project-env/project-env-dal";
|
||||
import { TProjectMembershipDALFactory } from "@app/services/project-membership/project-membership-dal";
|
||||
import { SmtpTemplates, TSmtpService } from "@app/services/smtp/smtp-service";
|
||||
import { TUserDALFactory } from "@app/services/user/user-dal";
|
||||
|
||||
import { TAccessApprovalPolicyApproverDALFactory } from "../access-approval-policy/access-approval-policy-approver-dal";
|
||||
import { TAccessApprovalPolicyDALFactory } from "../access-approval-policy/access-approval-policy-dal";
|
||||
import { verifyApprovers } from "../access-approval-policy/access-approval-policy-fns";
|
||||
import { TPermissionServiceFactory } from "../permission/permission-service";
|
||||
import { TProjectUserAdditionalPrivilegeDALFactory } from "../project-user-additional-privilege/project-user-additional-privilege-dal";
|
||||
import { ProjectUserAdditionalPrivilegeTemporaryMode } from "../project-user-additional-privilege/project-user-additional-privilege-types";
|
||||
import { TAccessApprovalRequestDALFactory } from "./access-approval-request-dal";
|
||||
import { verifyRequestedPermissions } from "./access-approval-request-fns";
|
||||
import { TAccessApprovalRequestReviewerDALFactory } from "./access-approval-request-reviewer-dal";
|
||||
import {
|
||||
ApprovalStatus,
|
||||
TCreateAccessApprovalRequestDTO,
|
||||
TGetAccessRequestCountDTO,
|
||||
TListApprovalRequestsDTO,
|
||||
TReviewAccessRequestDTO
|
||||
} from "./access-approval-request-types";
|
||||
|
||||
type TSecretApprovalRequestServiceFactoryDep = {
|
||||
additionalPrivilegeDAL: Pick<TProjectUserAdditionalPrivilegeDALFactory, "create" | "findById">;
|
||||
permissionService: Pick<TPermissionServiceFactory, "getProjectPermission">;
|
||||
accessApprovalPolicyApproverDAL: Pick<TAccessApprovalPolicyApproverDALFactory, "find">;
|
||||
projectEnvDAL: Pick<TProjectEnvDALFactory, "findOne">;
|
||||
projectDAL: Pick<TProjectDALFactory, "checkProjectUpgradeStatus" | "findProjectBySlug">;
|
||||
accessApprovalRequestDAL: Pick<
|
||||
TAccessApprovalRequestDALFactory,
|
||||
| "create"
|
||||
| "find"
|
||||
| "findRequestsWithPrivilegeByPolicyIds"
|
||||
| "findById"
|
||||
| "transaction"
|
||||
| "updateById"
|
||||
| "findOne"
|
||||
| "getCount"
|
||||
>;
|
||||
accessApprovalPolicyDAL: Pick<TAccessApprovalPolicyDALFactory, "findOne" | "find">;
|
||||
accessApprovalRequestReviewerDAL: Pick<
|
||||
TAccessApprovalRequestReviewerDALFactory,
|
||||
"create" | "find" | "findOne" | "transaction"
|
||||
>;
|
||||
projectMembershipDAL: Pick<TProjectMembershipDALFactory, "findById">;
|
||||
smtpService: Pick<TSmtpService, "sendMail">;
|
||||
userDAL: Pick<TUserDALFactory, "findUserByProjectMembershipId" | "findUsersByProjectMembershipIds">;
|
||||
};
|
||||
|
||||
export type TAccessApprovalRequestServiceFactory = ReturnType<typeof accessApprovalRequestServiceFactory>;
|
||||
|
||||
export const accessApprovalRequestServiceFactory = ({
|
||||
projectDAL,
|
||||
projectEnvDAL,
|
||||
permissionService,
|
||||
accessApprovalRequestDAL,
|
||||
accessApprovalRequestReviewerDAL,
|
||||
projectMembershipDAL,
|
||||
accessApprovalPolicyDAL,
|
||||
accessApprovalPolicyApproverDAL,
|
||||
additionalPrivilegeDAL,
|
||||
smtpService,
|
||||
userDAL
|
||||
}: TSecretApprovalRequestServiceFactoryDep) => {
|
||||
const createAccessApprovalRequest = async ({
|
||||
isTemporary,
|
||||
temporaryRange,
|
||||
actorId,
|
||||
permissions: requestedPermissions,
|
||||
actor,
|
||||
actorOrgId,
|
||||
actorAuthMethod,
|
||||
projectSlug
|
||||
}: TCreateAccessApprovalRequestDTO) => {
|
||||
const cfg = getConfig();
|
||||
const project = await projectDAL.findProjectBySlug(projectSlug, actorOrgId);
|
||||
if (!project) throw new UnauthorizedError({ message: "Project not found" });
|
||||
|
||||
// Anyone can create an access approval request.
|
||||
const { membership } = await permissionService.getProjectPermission(
|
||||
actor,
|
||||
actorId,
|
||||
project.id,
|
||||
actorAuthMethod,
|
||||
actorOrgId
|
||||
);
|
||||
if (!membership) throw new UnauthorizedError({ message: "You are not a member of this project" });
|
||||
|
||||
const requestedByUser = await userDAL.findUserByProjectMembershipId(membership.id);
|
||||
if (!requestedByUser) throw new UnauthorizedError({ message: "User not found" });
|
||||
|
||||
await projectDAL.checkProjectUpgradeStatus(project.id);
|
||||
|
||||
const { envSlug, secretPath, accessTypes } = verifyRequestedPermissions({ permissions: requestedPermissions });
|
||||
const environment = await projectEnvDAL.findOne({ projectId: project.id, slug: envSlug });
|
||||
|
||||
if (!environment) throw new UnauthorizedError({ message: "Environment not found" });
|
||||
|
||||
const policy = await accessApprovalPolicyDAL.findOne({
|
||||
envId: environment.id,
|
||||
secretPath
|
||||
});
|
||||
if (!policy) throw new UnauthorizedError({ message: "No policy matching criteria was found." });
|
||||
|
||||
const approvers = await accessApprovalPolicyApproverDAL.find({
|
||||
policyId: policy.id
|
||||
});
|
||||
|
||||
const approverUsers = await userDAL.findUsersByProjectMembershipIds(
|
||||
approvers.map((approver) => approver.approverId)
|
||||
);
|
||||
|
||||
const duplicateRequests = await accessApprovalRequestDAL.find({
|
||||
policyId: policy.id,
|
||||
requestedBy: membership.id,
|
||||
permissions: JSON.stringify(requestedPermissions),
|
||||
isTemporary
|
||||
});
|
||||
|
||||
if (duplicateRequests?.length > 0) {
|
||||
for await (const duplicateRequest of duplicateRequests) {
|
||||
if (duplicateRequest.privilegeId) {
|
||||
const privilege = await additionalPrivilegeDAL.findById(duplicateRequest.privilegeId);
|
||||
|
||||
const isExpired = new Date() > new Date(privilege.temporaryAccessEndTime || ("" as string));
|
||||
|
||||
if (!isExpired || !privilege.isTemporary) {
|
||||
throw new BadRequestError({ message: "You already have an active privilege with the same criteria" });
|
||||
}
|
||||
} else {
|
||||
const reviewers = await accessApprovalRequestReviewerDAL.find({
|
||||
requestId: duplicateRequest.id
|
||||
});
|
||||
|
||||
const isRejected = reviewers.some((reviewer) => reviewer.status === ApprovalStatus.REJECTED);
|
||||
|
||||
if (!isRejected) {
|
||||
throw new BadRequestError({ message: "You already have a pending access request with the same criteria" });
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const approval = await accessApprovalRequestDAL.transaction(async (tx) => {
|
||||
const approvalRequest = await accessApprovalRequestDAL.create(
|
||||
{
|
||||
policyId: policy.id,
|
||||
requestedBy: membership.id,
|
||||
temporaryRange: temporaryRange || null,
|
||||
permissions: JSON.stringify(requestedPermissions),
|
||||
isTemporary
|
||||
},
|
||||
tx
|
||||
);
|
||||
|
||||
await smtpService.sendMail({
|
||||
recipients: approverUsers.filter((approver) => approver.email).map((approver) => approver.email!),
|
||||
subjectLine: "Access Approval Request",
|
||||
|
||||
substitutions: {
|
||||
projectName: project.name,
|
||||
requesterFullName: `${requestedByUser.firstName} ${requestedByUser.lastName}`,
|
||||
requesterEmail: requestedByUser.email,
|
||||
isTemporary,
|
||||
...(isTemporary && {
|
||||
expiresIn: ms(ms(temporaryRange || ""), { long: true })
|
||||
}),
|
||||
secretPath,
|
||||
environment: envSlug,
|
||||
permissions: accessTypes,
|
||||
approvalUrl: `${cfg.SITE_URL}/project/${project.id}/approval`
|
||||
},
|
||||
template: SmtpTemplates.AccessApprovalRequest
|
||||
});
|
||||
|
||||
return approvalRequest;
|
||||
});
|
||||
|
||||
return { request: approval };
|
||||
};
|
||||
|
||||
const listApprovalRequests = async ({
|
||||
projectSlug,
|
||||
authorProjectMembershipId,
|
||||
envSlug,
|
||||
actor,
|
||||
actorOrgId,
|
||||
actorId,
|
||||
actorAuthMethod
|
||||
}: TListApprovalRequestsDTO) => {
|
||||
const project = await projectDAL.findProjectBySlug(projectSlug, actorOrgId);
|
||||
if (!project) throw new UnauthorizedError({ message: "Project not found" });
|
||||
|
||||
const { membership } = await permissionService.getProjectPermission(
|
||||
actor,
|
||||
actorId,
|
||||
project.id,
|
||||
actorAuthMethod,
|
||||
actorOrgId
|
||||
);
|
||||
if (!membership) throw new UnauthorizedError({ message: "You are not a member of this project" });
|
||||
|
||||
const policies = await accessApprovalPolicyDAL.find({ projectId: project.id });
|
||||
let requests = await accessApprovalRequestDAL.findRequestsWithPrivilegeByPolicyIds(policies.map((p) => p.id));
|
||||
|
||||
if (authorProjectMembershipId) {
|
||||
requests = requests.filter((request) => request.requestedBy === authorProjectMembershipId);
|
||||
}
|
||||
|
||||
if (envSlug) {
|
||||
requests = requests.filter((request) => request.environment === envSlug);
|
||||
}
|
||||
|
||||
return { requests };
|
||||
};
|
||||
|
||||
const reviewAccessRequest = async ({
|
||||
requestId,
|
||||
actor,
|
||||
status,
|
||||
actorId,
|
||||
actorAuthMethod,
|
||||
actorOrgId
|
||||
}: TReviewAccessRequestDTO) => {
|
||||
const accessApprovalRequest = await accessApprovalRequestDAL.findById(requestId);
|
||||
if (!accessApprovalRequest) throw new BadRequestError({ message: "Secret approval request not found" });
|
||||
|
||||
const { policy } = accessApprovalRequest;
|
||||
const { membership, hasRole } = await permissionService.getProjectPermission(
|
||||
actor,
|
||||
actorId,
|
||||
accessApprovalRequest.projectId,
|
||||
actorAuthMethod,
|
||||
actorOrgId
|
||||
);
|
||||
|
||||
if (!membership) throw new UnauthorizedError({ message: "You are not a member of this project" });
|
||||
|
||||
if (
|
||||
!hasRole(ProjectMembershipRole.Admin) &&
|
||||
accessApprovalRequest.requestedBy !== membership.id && // The request wasn't made by the current user
|
||||
!policy.approvers.find((approverId) => approverId === membership.id) // The request isn't performed by an assigned approver
|
||||
) {
|
||||
throw new UnauthorizedError({ message: "You are not authorized to approve this request" });
|
||||
}
|
||||
|
||||
const reviewerProjectMembership = await projectMembershipDAL.findById(membership.id);
|
||||
|
||||
await verifyApprovers({
|
||||
projectId: accessApprovalRequest.projectId,
|
||||
orgId: actorOrgId,
|
||||
envSlug: accessApprovalRequest.environment,
|
||||
secretPath: accessApprovalRequest.policy.secretPath!,
|
||||
actorAuthMethod,
|
||||
permissionService,
|
||||
userIds: [reviewerProjectMembership.userId]
|
||||
});
|
||||
|
||||
const existingReviews = await accessApprovalRequestReviewerDAL.find({ requestId: accessApprovalRequest.id });
|
||||
if (existingReviews.some((review) => review.status === ApprovalStatus.REJECTED)) {
|
||||
throw new BadRequestError({ message: "The request has already been rejected by another reviewer" });
|
||||
}
|
||||
|
||||
const reviewStatus = await accessApprovalRequestReviewerDAL.transaction(async (tx) => {
|
||||
const review = await accessApprovalRequestReviewerDAL.findOne(
|
||||
{
|
||||
requestId: accessApprovalRequest.id,
|
||||
member: membership.id
|
||||
},
|
||||
tx
|
||||
);
|
||||
if (!review) {
|
||||
const newReview = await accessApprovalRequestReviewerDAL.create(
|
||||
{
|
||||
status,
|
||||
requestId: accessApprovalRequest.id,
|
||||
member: membership.id
|
||||
},
|
||||
tx
|
||||
);
|
||||
|
||||
const allReviews = [...existingReviews, newReview];
|
||||
|
||||
const approvedReviews = allReviews.filter((r) => r.status === ApprovalStatus.APPROVED);
|
||||
|
||||
// approvals is the required number of approvals. If the number of approved reviews is equal to the number of required approvals, then the request is approved.
|
||||
if (approvedReviews.length === policy.approvals) {
|
||||
if (accessApprovalRequest.isTemporary && !accessApprovalRequest.temporaryRange) {
|
||||
throw new BadRequestError({ message: "Temporary range is required for temporary access" });
|
||||
}
|
||||
|
||||
let privilegeId: string | null = null;
|
||||
|
||||
if (!accessApprovalRequest.isTemporary && !accessApprovalRequest.temporaryRange) {
|
||||
// Permanent access
|
||||
const privilege = await additionalPrivilegeDAL.create(
|
||||
{
|
||||
projectMembershipId: accessApprovalRequest.requestedBy,
|
||||
slug: `requested-privilege-${slugify(alphaNumericNanoId(12))}`,
|
||||
permissions: JSON.stringify(accessApprovalRequest.permissions)
|
||||
},
|
||||
tx
|
||||
);
|
||||
privilegeId = privilege.id;
|
||||
} else {
|
||||
// Temporary access
|
||||
const relativeTempAllocatedTimeInMs = ms(accessApprovalRequest.temporaryRange!);
|
||||
const startTime = new Date();
|
||||
|
||||
const privilege = await additionalPrivilegeDAL.create(
|
||||
{
|
||||
projectMembershipId: accessApprovalRequest.requestedBy,
|
||||
slug: `requested-privilege-${slugify(alphaNumericNanoId(12))}`,
|
||||
permissions: JSON.stringify(accessApprovalRequest.permissions),
|
||||
isTemporary: true,
|
||||
temporaryMode: ProjectUserAdditionalPrivilegeTemporaryMode.Relative,
|
||||
temporaryRange: accessApprovalRequest.temporaryRange!,
|
||||
temporaryAccessStartTime: startTime,
|
||||
temporaryAccessEndTime: new Date(new Date(startTime).getTime() + relativeTempAllocatedTimeInMs)
|
||||
},
|
||||
tx
|
||||
);
|
||||
privilegeId = privilege.id;
|
||||
}
|
||||
|
||||
await accessApprovalRequestDAL.updateById(accessApprovalRequest.id, { privilegeId }, tx);
|
||||
}
|
||||
|
||||
return newReview;
|
||||
}
|
||||
throw new BadRequestError({ message: "You have already reviewed this request" });
|
||||
});
|
||||
|
||||
return reviewStatus;
|
||||
};
|
||||
|
||||
const getCount = async ({ projectSlug, actor, actorAuthMethod, actorId, actorOrgId }: TGetAccessRequestCountDTO) => {
|
||||
const project = await projectDAL.findProjectBySlug(projectSlug, actorOrgId);
|
||||
if (!project) throw new UnauthorizedError({ message: "Project not found" });
|
||||
|
||||
const { membership } = await permissionService.getProjectPermission(
|
||||
actor,
|
||||
actorId,
|
||||
project.id,
|
||||
actorAuthMethod,
|
||||
actorOrgId
|
||||
);
|
||||
if (!membership) throw new BadRequestError({ message: "User not found in project" });
|
||||
|
||||
const count = await accessApprovalRequestDAL.getCount({ projectId: project.id });
|
||||
|
||||
return { count };
|
||||
};
|
||||
|
||||
return {
|
||||
createAccessApprovalRequest,
|
||||
listApprovalRequests,
|
||||
reviewAccessRequest,
|
||||
getCount
|
||||
};
|
||||
};
|
@ -0,0 +1,33 @@
|
||||
import { TProjectPermission } from "@app/lib/types";
|
||||
|
||||
export enum ApprovalStatus {
|
||||
PENDING = "pending",
|
||||
APPROVED = "approved",
|
||||
REJECTED = "rejected"
|
||||
}
|
||||
|
||||
export type TVerifyPermission = {
|
||||
permissions: unknown;
|
||||
};
|
||||
|
||||
export type TGetAccessRequestCountDTO = {
|
||||
projectSlug: string;
|
||||
} & Omit<TProjectPermission, "projectId">;
|
||||
|
||||
export type TReviewAccessRequestDTO = {
|
||||
requestId: string;
|
||||
status: ApprovalStatus;
|
||||
} & Omit<TProjectPermission, "projectId">;
|
||||
|
||||
export type TCreateAccessApprovalRequestDTO = {
|
||||
projectSlug: string;
|
||||
permissions: unknown;
|
||||
isTemporary: boolean;
|
||||
temporaryRange?: string;
|
||||
} & Omit<TProjectPermission, "projectId">;
|
||||
|
||||
export type TListApprovalRequestsDTO = {
|
||||
projectSlug: string;
|
||||
authorProjectMembershipId?: string;
|
||||
envSlug?: string;
|
||||
} & Omit<TProjectPermission, "projectId">;
|
@ -0,0 +1,11 @@
|
||||
import { TDbClient } from "@app/db";
|
||||
import { TableName } from "@app/db/schemas";
|
||||
import { ormify } from "@app/lib/knex";
|
||||
|
||||
export type TAuditLogStreamDALFactory = ReturnType<typeof auditLogStreamDALFactory>;
|
||||
|
||||
export const auditLogStreamDALFactory = (db: TDbClient) => {
|
||||
const orm = ormify(db, TableName.AuditLogStream);
|
||||
|
||||
return orm;
|
||||
};
|
@ -0,0 +1,233 @@
|
||||
import { ForbiddenError } from "@casl/ability";
|
||||
import { RawAxiosRequestHeaders } from "axios";
|
||||
|
||||
import { SecretKeyEncoding } from "@app/db/schemas";
|
||||
import { request } from "@app/lib/config/request";
|
||||
import { infisicalSymmetricDecrypt, infisicalSymmetricEncypt } from "@app/lib/crypto/encryption";
|
||||
import { BadRequestError } from "@app/lib/errors";
|
||||
import { validateLocalIps } from "@app/lib/validator";
|
||||
|
||||
import { AUDIT_LOG_STREAM_TIMEOUT } from "../audit-log/audit-log-queue";
|
||||
import { TLicenseServiceFactory } from "../license/license-service";
|
||||
import { OrgPermissionActions, OrgPermissionSubjects } from "../permission/org-permission";
|
||||
import { TPermissionServiceFactory } from "../permission/permission-service";
|
||||
import { TAuditLogStreamDALFactory } from "./audit-log-stream-dal";
|
||||
import {
|
||||
LogStreamHeaders,
|
||||
TCreateAuditLogStreamDTO,
|
||||
TDeleteAuditLogStreamDTO,
|
||||
TGetDetailsAuditLogStreamDTO,
|
||||
TListAuditLogStreamDTO,
|
||||
TUpdateAuditLogStreamDTO
|
||||
} from "./audit-log-stream-types";
|
||||
|
||||
type TAuditLogStreamServiceFactoryDep = {
|
||||
auditLogStreamDAL: TAuditLogStreamDALFactory;
|
||||
permissionService: Pick<TPermissionServiceFactory, "getOrgPermission">;
|
||||
licenseService: Pick<TLicenseServiceFactory, "getPlan">;
|
||||
};
|
||||
|
||||
export type TAuditLogStreamServiceFactory = ReturnType<typeof auditLogStreamServiceFactory>;
|
||||
|
||||
export const auditLogStreamServiceFactory = ({
|
||||
auditLogStreamDAL,
|
||||
permissionService,
|
||||
licenseService
|
||||
}: TAuditLogStreamServiceFactoryDep) => {
|
||||
const create = async ({
|
||||
url,
|
||||
actor,
|
||||
headers = [],
|
||||
actorId,
|
||||
actorOrgId,
|
||||
actorAuthMethod
|
||||
}: TCreateAuditLogStreamDTO) => {
|
||||
if (!actorOrgId) throw new BadRequestError({ message: "Missing org id from token" });
|
||||
|
||||
const plan = await licenseService.getPlan(actorOrgId);
|
||||
if (!plan.auditLogStreams)
|
||||
throw new BadRequestError({
|
||||
message: "Failed to create audit log streams due to plan restriction. Upgrade plan to create group."
|
||||
});
|
||||
|
||||
const { permission } = await permissionService.getOrgPermission(
|
||||
actor,
|
||||
actorId,
|
||||
actorOrgId,
|
||||
actorAuthMethod,
|
||||
actorOrgId
|
||||
);
|
||||
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Create, OrgPermissionSubjects.Settings);
|
||||
|
||||
validateLocalIps(url);
|
||||
|
||||
const totalStreams = await auditLogStreamDAL.find({ orgId: actorOrgId });
|
||||
if (totalStreams.length >= plan.auditLogStreamLimit) {
|
||||
throw new BadRequestError({
|
||||
message:
|
||||
"Failed to create audit log streams due to plan limit reached. Kindly contact Infisical to add more streams."
|
||||
});
|
||||
}
|
||||
|
||||
// testing connection first
|
||||
const streamHeaders: RawAxiosRequestHeaders = { "Content-Type": "application/json" };
|
||||
if (headers.length)
|
||||
headers.forEach(({ key, value }) => {
|
||||
streamHeaders[key] = value;
|
||||
});
|
||||
await request
|
||||
.post(
|
||||
url,
|
||||
{ ping: "ok" },
|
||||
{
|
||||
headers: streamHeaders,
|
||||
// request timeout
|
||||
timeout: AUDIT_LOG_STREAM_TIMEOUT,
|
||||
// connection timeout
|
||||
signal: AbortSignal.timeout(AUDIT_LOG_STREAM_TIMEOUT)
|
||||
}
|
||||
)
|
||||
.catch((err) => {
|
||||
throw new Error(`Failed to connect with the source ${(err as Error)?.message}`);
|
||||
});
|
||||
const encryptedHeaders = headers ? infisicalSymmetricEncypt(JSON.stringify(headers)) : undefined;
|
||||
const logStream = await auditLogStreamDAL.create({
|
||||
orgId: actorOrgId,
|
||||
url,
|
||||
...(encryptedHeaders
|
||||
? {
|
||||
encryptedHeadersCiphertext: encryptedHeaders.ciphertext,
|
||||
encryptedHeadersIV: encryptedHeaders.iv,
|
||||
encryptedHeadersTag: encryptedHeaders.tag,
|
||||
encryptedHeadersAlgorithm: encryptedHeaders.algorithm,
|
||||
encryptedHeadersKeyEncoding: encryptedHeaders.encoding
|
||||
}
|
||||
: {})
|
||||
});
|
||||
return logStream;
|
||||
};
|
||||
|
||||
const updateById = async ({
|
||||
id,
|
||||
url,
|
||||
actor,
|
||||
headers = [],
|
||||
actorId,
|
||||
actorOrgId,
|
||||
actorAuthMethod
|
||||
}: TUpdateAuditLogStreamDTO) => {
|
||||
if (!actorOrgId) throw new BadRequestError({ message: "Missing org id from token" });
|
||||
|
||||
const plan = await licenseService.getPlan(actorOrgId);
|
||||
if (!plan.auditLogStreams)
|
||||
throw new BadRequestError({
|
||||
message: "Failed to update audit log streams due to plan restriction. Upgrade plan to create group."
|
||||
});
|
||||
|
||||
const logStream = await auditLogStreamDAL.findById(id);
|
||||
if (!logStream) throw new BadRequestError({ message: "Audit log stream not found" });
|
||||
|
||||
const { orgId } = logStream;
|
||||
const { permission } = await permissionService.getOrgPermission(actor, actorId, orgId, actorAuthMethod, actorOrgId);
|
||||
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Edit, OrgPermissionSubjects.Settings);
|
||||
|
||||
if (url) validateLocalIps(url);
|
||||
|
||||
// testing connection first
|
||||
const streamHeaders: RawAxiosRequestHeaders = { "Content-Type": "application/json" };
|
||||
if (headers.length)
|
||||
headers.forEach(({ key, value }) => {
|
||||
streamHeaders[key] = value;
|
||||
});
|
||||
|
||||
await request
|
||||
.post(
|
||||
url || logStream.url,
|
||||
{ ping: "ok" },
|
||||
{
|
||||
headers: streamHeaders,
|
||||
// request timeout
|
||||
timeout: AUDIT_LOG_STREAM_TIMEOUT,
|
||||
// connection timeout
|
||||
signal: AbortSignal.timeout(AUDIT_LOG_STREAM_TIMEOUT)
|
||||
}
|
||||
)
|
||||
.catch((err) => {
|
||||
throw new Error(`Failed to connect with the source ${(err as Error)?.message}`);
|
||||
});
|
||||
|
||||
const encryptedHeaders = headers ? infisicalSymmetricEncypt(JSON.stringify(headers)) : undefined;
|
||||
const updatedLogStream = await auditLogStreamDAL.updateById(id, {
|
||||
url,
|
||||
...(encryptedHeaders
|
||||
? {
|
||||
encryptedHeadersCiphertext: encryptedHeaders.ciphertext,
|
||||
encryptedHeadersIV: encryptedHeaders.iv,
|
||||
encryptedHeadersTag: encryptedHeaders.tag,
|
||||
encryptedHeadersAlgorithm: encryptedHeaders.algorithm,
|
||||
encryptedHeadersKeyEncoding: encryptedHeaders.encoding
|
||||
}
|
||||
: {})
|
||||
});
|
||||
return updatedLogStream;
|
||||
};
|
||||
|
||||
const deleteById = async ({ id, actor, actorId, actorOrgId, actorAuthMethod }: TDeleteAuditLogStreamDTO) => {
|
||||
if (!actorOrgId) throw new BadRequestError({ message: "Missing org id from token" });
|
||||
|
||||
const logStream = await auditLogStreamDAL.findById(id);
|
||||
if (!logStream) throw new BadRequestError({ message: "Audit log stream not found" });
|
||||
|
||||
const { orgId } = logStream;
|
||||
const { permission } = await permissionService.getOrgPermission(actor, actorId, orgId, actorAuthMethod, actorOrgId);
|
||||
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Delete, OrgPermissionSubjects.Settings);
|
||||
|
||||
const deletedLogStream = await auditLogStreamDAL.deleteById(id);
|
||||
return deletedLogStream;
|
||||
};
|
||||
|
||||
const getById = async ({ id, actor, actorId, actorOrgId, actorAuthMethod }: TGetDetailsAuditLogStreamDTO) => {
|
||||
const logStream = await auditLogStreamDAL.findById(id);
|
||||
if (!logStream) throw new BadRequestError({ message: "Audit log stream not found" });
|
||||
|
||||
const { orgId } = logStream;
|
||||
const { permission } = await permissionService.getOrgPermission(actor, actorId, orgId, actorAuthMethod, actorOrgId);
|
||||
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Read, OrgPermissionSubjects.Settings);
|
||||
|
||||
const headers =
|
||||
logStream?.encryptedHeadersCiphertext && logStream?.encryptedHeadersIV && logStream?.encryptedHeadersTag
|
||||
? (JSON.parse(
|
||||
infisicalSymmetricDecrypt({
|
||||
tag: logStream.encryptedHeadersTag,
|
||||
iv: logStream.encryptedHeadersIV,
|
||||
ciphertext: logStream.encryptedHeadersCiphertext,
|
||||
keyEncoding: logStream.encryptedHeadersKeyEncoding as SecretKeyEncoding
|
||||
})
|
||||
) as LogStreamHeaders[])
|
||||
: undefined;
|
||||
|
||||
return { ...logStream, headers };
|
||||
};
|
||||
|
||||
const list = async ({ actor, actorId, actorOrgId, actorAuthMethod }: TListAuditLogStreamDTO) => {
|
||||
const { permission } = await permissionService.getOrgPermission(
|
||||
actor,
|
||||
actorId,
|
||||
actorOrgId,
|
||||
actorAuthMethod,
|
||||
actorOrgId
|
||||
);
|
||||
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Read, OrgPermissionSubjects.Settings);
|
||||
|
||||
const logStreams = await auditLogStreamDAL.find({ orgId: actorOrgId });
|
||||
return logStreams;
|
||||
};
|
||||
|
||||
return {
|
||||
create,
|
||||
updateById,
|
||||
deleteById,
|
||||
getById,
|
||||
list
|
||||
};
|
||||
};
|
@ -0,0 +1,27 @@
|
||||
import { TOrgPermission } from "@app/lib/types";
|
||||
|
||||
export type LogStreamHeaders = {
|
||||
key: string;
|
||||
value: string;
|
||||
};
|
||||
|
||||
export type TCreateAuditLogStreamDTO = Omit<TOrgPermission, "orgId"> & {
|
||||
url: string;
|
||||
headers?: LogStreamHeaders[];
|
||||
};
|
||||
|
||||
export type TUpdateAuditLogStreamDTO = Omit<TOrgPermission, "orgId"> & {
|
||||
id: string;
|
||||
url?: string;
|
||||
headers?: LogStreamHeaders[];
|
||||
};
|
||||
|
||||
export type TDeleteAuditLogStreamDTO = Omit<TOrgPermission, "orgId"> & {
|
||||
id: string;
|
||||
};
|
||||
|
||||
export type TListAuditLogStreamDTO = Omit<TOrgPermission, "orgId">;
|
||||
|
||||
export type TGetDetailsAuditLogStreamDTO = Omit<TOrgPermission, "orgId"> & {
|
||||
id: string;
|
||||
};
|
@ -1,13 +1,21 @@
|
||||
import { RawAxiosRequestHeaders } from "axios";
|
||||
|
||||
import { SecretKeyEncoding } from "@app/db/schemas";
|
||||
import { request } from "@app/lib/config/request";
|
||||
import { infisicalSymmetricDecrypt } from "@app/lib/crypto/encryption";
|
||||
import { logger } from "@app/lib/logger";
|
||||
import { QueueJobs, QueueName, TQueueServiceFactory } from "@app/queue";
|
||||
import { TProjectDALFactory } from "@app/services/project/project-dal";
|
||||
|
||||
import { TAuditLogStreamDALFactory } from "../audit-log-stream/audit-log-stream-dal";
|
||||
import { LogStreamHeaders } from "../audit-log-stream/audit-log-stream-types";
|
||||
import { TLicenseServiceFactory } from "../license/license-service";
|
||||
import { TAuditLogDALFactory } from "./audit-log-dal";
|
||||
import { TCreateAuditLogDTO } from "./audit-log-types";
|
||||
|
||||
type TAuditLogQueueServiceFactoryDep = {
|
||||
auditLogDAL: TAuditLogDALFactory;
|
||||
auditLogStreamDAL: Pick<TAuditLogStreamDALFactory, "find">;
|
||||
queueService: TQueueServiceFactory;
|
||||
projectDAL: Pick<TProjectDALFactory, "findById">;
|
||||
licenseService: Pick<TLicenseServiceFactory, "getPlan">;
|
||||
@ -15,11 +23,15 @@ type TAuditLogQueueServiceFactoryDep = {
|
||||
|
||||
export type TAuditLogQueueServiceFactory = ReturnType<typeof auditLogQueueServiceFactory>;
|
||||
|
||||
// keep this timeout 5s it must be fast because else the queue will take time to finish
|
||||
// audit log is a crowded queue thus needs to be fast
|
||||
export const AUDIT_LOG_STREAM_TIMEOUT = 5 * 1000;
|
||||
export const auditLogQueueServiceFactory = ({
|
||||
auditLogDAL,
|
||||
queueService,
|
||||
projectDAL,
|
||||
licenseService
|
||||
licenseService,
|
||||
auditLogStreamDAL
|
||||
}: TAuditLogQueueServiceFactoryDep) => {
|
||||
const pushToLog = async (data: TCreateAuditLogDTO) => {
|
||||
await queueService.queue(QueueName.AuditLog, QueueJobs.AuditLog, data, {
|
||||
@ -47,7 +59,7 @@ export const auditLogQueueServiceFactory = ({
|
||||
// skip inserting if audit log retention is 0 meaning its not supported
|
||||
if (ttl === 0) return;
|
||||
|
||||
await auditLogDAL.create({
|
||||
const auditLog = await auditLogDAL.create({
|
||||
actor: actor.type,
|
||||
actorMetadata: actor.metadata,
|
||||
userAgent,
|
||||
@ -59,6 +71,46 @@ export const auditLogQueueServiceFactory = ({
|
||||
eventMetadata: event.metadata,
|
||||
userAgentType
|
||||
});
|
||||
|
||||
const logStreams = orgId ? await auditLogStreamDAL.find({ orgId }) : [];
|
||||
await Promise.allSettled(
|
||||
logStreams.map(
|
||||
async ({
|
||||
url,
|
||||
encryptedHeadersTag,
|
||||
encryptedHeadersIV,
|
||||
encryptedHeadersKeyEncoding,
|
||||
encryptedHeadersCiphertext
|
||||
}) => {
|
||||
const streamHeaders =
|
||||
encryptedHeadersIV && encryptedHeadersCiphertext && encryptedHeadersTag
|
||||
? (JSON.parse(
|
||||
infisicalSymmetricDecrypt({
|
||||
keyEncoding: encryptedHeadersKeyEncoding as SecretKeyEncoding,
|
||||
iv: encryptedHeadersIV,
|
||||
tag: encryptedHeadersTag,
|
||||
ciphertext: encryptedHeadersCiphertext
|
||||
})
|
||||
) as LogStreamHeaders[])
|
||||
: [];
|
||||
|
||||
const headers: RawAxiosRequestHeaders = { "Content-Type": "application/json" };
|
||||
|
||||
if (streamHeaders.length)
|
||||
streamHeaders.forEach(({ key, value }) => {
|
||||
headers[key] = value;
|
||||
});
|
||||
|
||||
return request.post(url, auditLog, {
|
||||
headers,
|
||||
// request timeout
|
||||
timeout: AUDIT_LOG_STREAM_TIMEOUT,
|
||||
// connection timeout
|
||||
signal: AbortSignal.timeout(AUDIT_LOG_STREAM_TIMEOUT)
|
||||
});
|
||||
}
|
||||
)
|
||||
);
|
||||
});
|
||||
|
||||
queueService.start(QueueName.AuditLogPrune, async () => {
|
||||
|
194
backend/src/ee/services/dynamic-secret/providers/aws-iam.ts
Normal file
194
backend/src/ee/services/dynamic-secret/providers/aws-iam.ts
Normal file
@ -0,0 +1,194 @@
|
||||
import {
|
||||
AddUserToGroupCommand,
|
||||
AttachUserPolicyCommand,
|
||||
CreateAccessKeyCommand,
|
||||
CreateUserCommand,
|
||||
DeleteAccessKeyCommand,
|
||||
DeleteUserCommand,
|
||||
DeleteUserPolicyCommand,
|
||||
DetachUserPolicyCommand,
|
||||
GetUserCommand,
|
||||
IAMClient,
|
||||
ListAccessKeysCommand,
|
||||
ListAttachedUserPoliciesCommand,
|
||||
ListGroupsForUserCommand,
|
||||
ListUserPoliciesCommand,
|
||||
PutUserPolicyCommand,
|
||||
RemoveUserFromGroupCommand
|
||||
} from "@aws-sdk/client-iam";
|
||||
import { z } from "zod";
|
||||
|
||||
import { BadRequestError } from "@app/lib/errors";
|
||||
import { alphaNumericNanoId } from "@app/lib/nanoid";
|
||||
|
||||
import { DynamicSecretAwsIamSchema, TDynamicProviderFns } from "./models";
|
||||
|
||||
const generateUsername = () => {
|
||||
return alphaNumericNanoId(32);
|
||||
};
|
||||
|
||||
export const AwsIamProvider = (): TDynamicProviderFns => {
|
||||
const validateProviderInputs = async (inputs: unknown) => {
|
||||
const providerInputs = await DynamicSecretAwsIamSchema.parseAsync(inputs);
|
||||
return providerInputs;
|
||||
};
|
||||
|
||||
const getClient = async (providerInputs: z.infer<typeof DynamicSecretAwsIamSchema>) => {
|
||||
const client = new IAMClient({
|
||||
region: providerInputs.region,
|
||||
credentials: {
|
||||
accessKeyId: providerInputs.accessKey,
|
||||
secretAccessKey: providerInputs.secretAccessKey
|
||||
}
|
||||
});
|
||||
|
||||
return client;
|
||||
};
|
||||
|
||||
const validateConnection = async (inputs: unknown) => {
|
||||
const providerInputs = await validateProviderInputs(inputs);
|
||||
const client = await getClient(providerInputs);
|
||||
|
||||
const isConnected = await client.send(new GetUserCommand({})).then(() => true);
|
||||
return isConnected;
|
||||
};
|
||||
|
||||
const create = async (inputs: unknown) => {
|
||||
const providerInputs = await validateProviderInputs(inputs);
|
||||
const client = await getClient(providerInputs);
|
||||
|
||||
const username = generateUsername();
|
||||
const { policyArns, userGroups, policyDocument, awsPath, permissionBoundaryPolicyArn } = providerInputs;
|
||||
const createUserRes = await client.send(
|
||||
new CreateUserCommand({
|
||||
Path: awsPath,
|
||||
PermissionsBoundary: permissionBoundaryPolicyArn || undefined,
|
||||
Tags: [{ Key: "createdBy", Value: "infisical-dynamic-secret" }],
|
||||
UserName: username
|
||||
})
|
||||
);
|
||||
if (!createUserRes.User) throw new BadRequestError({ message: "Failed to create AWS IAM User" });
|
||||
if (userGroups) {
|
||||
await Promise.all(
|
||||
userGroups
|
||||
.split(",")
|
||||
.filter(Boolean)
|
||||
.map((group) =>
|
||||
client.send(new AddUserToGroupCommand({ UserName: createUserRes?.User?.UserName, GroupName: group }))
|
||||
)
|
||||
);
|
||||
}
|
||||
if (policyArns) {
|
||||
await Promise.all(
|
||||
policyArns
|
||||
.split(",")
|
||||
.filter(Boolean)
|
||||
.map((policyArn) =>
|
||||
client.send(new AttachUserPolicyCommand({ UserName: createUserRes?.User?.UserName, PolicyArn: policyArn }))
|
||||
)
|
||||
);
|
||||
}
|
||||
if (policyDocument) {
|
||||
await client.send(
|
||||
new PutUserPolicyCommand({
|
||||
UserName: createUserRes.User.UserName,
|
||||
PolicyName: `infisical-dynamic-policy-${alphaNumericNanoId(4)}`,
|
||||
PolicyDocument: policyDocument
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
const createAccessKeyRes = await client.send(
|
||||
new CreateAccessKeyCommand({
|
||||
UserName: createUserRes.User.UserName
|
||||
})
|
||||
);
|
||||
if (!createAccessKeyRes.AccessKey)
|
||||
throw new BadRequestError({ message: "Failed to create AWS IAM User access key" });
|
||||
|
||||
return {
|
||||
entityId: username,
|
||||
data: {
|
||||
ACCESS_KEY: createAccessKeyRes.AccessKey.AccessKeyId,
|
||||
SECRET_ACCESS_KEY: createAccessKeyRes.AccessKey.SecretAccessKey,
|
||||
USERNAME: username
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
const revoke = async (inputs: unknown, entityId: string) => {
|
||||
const providerInputs = await validateProviderInputs(inputs);
|
||||
const client = await getClient(providerInputs);
|
||||
|
||||
const username = entityId;
|
||||
|
||||
// remove user from groups
|
||||
const userGroups = await client.send(new ListGroupsForUserCommand({ UserName: username }));
|
||||
await Promise.all(
|
||||
(userGroups.Groups || []).map(({ GroupName }) =>
|
||||
client.send(
|
||||
new RemoveUserFromGroupCommand({
|
||||
GroupName,
|
||||
UserName: username
|
||||
})
|
||||
)
|
||||
)
|
||||
);
|
||||
|
||||
// remove user access keys
|
||||
const userAccessKeys = await client.send(new ListAccessKeysCommand({ UserName: username }));
|
||||
await Promise.all(
|
||||
(userAccessKeys.AccessKeyMetadata || []).map(({ AccessKeyId }) =>
|
||||
client.send(
|
||||
new DeleteAccessKeyCommand({
|
||||
AccessKeyId,
|
||||
UserName: username
|
||||
})
|
||||
)
|
||||
)
|
||||
);
|
||||
|
||||
// remove user inline policies
|
||||
const userInlinePolicies = await client.send(new ListUserPoliciesCommand({ UserName: username }));
|
||||
await Promise.all(
|
||||
(userInlinePolicies.PolicyNames || []).map((policyName) =>
|
||||
client.send(
|
||||
new DeleteUserPolicyCommand({
|
||||
PolicyName: policyName,
|
||||
UserName: username
|
||||
})
|
||||
)
|
||||
)
|
||||
);
|
||||
|
||||
// remove user attached policies
|
||||
const userAttachedPolicies = await client.send(new ListAttachedUserPoliciesCommand({ UserName: username }));
|
||||
await Promise.all(
|
||||
(userAttachedPolicies.AttachedPolicies || []).map((policy) =>
|
||||
client.send(
|
||||
new DetachUserPolicyCommand({
|
||||
PolicyArn: policy.PolicyArn,
|
||||
UserName: username
|
||||
})
|
||||
)
|
||||
)
|
||||
);
|
||||
|
||||
await client.send(new DeleteUserCommand({ UserName: username }));
|
||||
return { entityId: username };
|
||||
};
|
||||
|
||||
const renew = async (_inputs: unknown, entityId: string) => {
|
||||
// do nothing
|
||||
const username = entityId;
|
||||
return { entityId: username };
|
||||
};
|
||||
|
||||
return {
|
||||
validateProviderInputs,
|
||||
validateConnection,
|
||||
create,
|
||||
revoke,
|
||||
renew
|
||||
};
|
||||
};
|
125
backend/src/ee/services/dynamic-secret/providers/cassandra.ts
Normal file
125
backend/src/ee/services/dynamic-secret/providers/cassandra.ts
Normal file
@ -0,0 +1,125 @@
|
||||
import cassandra from "cassandra-driver";
|
||||
import handlebars from "handlebars";
|
||||
import { customAlphabet } from "nanoid";
|
||||
import { z } from "zod";
|
||||
|
||||
import { BadRequestError } from "@app/lib/errors";
|
||||
import { alphaNumericNanoId } from "@app/lib/nanoid";
|
||||
|
||||
import { DynamicSecretCassandraSchema, TDynamicProviderFns } from "./models";
|
||||
|
||||
const generatePassword = (size = 48) => {
|
||||
const charset = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-_.~!*$#";
|
||||
return customAlphabet(charset, 48)(size);
|
||||
};
|
||||
|
||||
const generateUsername = () => {
|
||||
return alphaNumericNanoId(32);
|
||||
};
|
||||
|
||||
export const CassandraProvider = (): TDynamicProviderFns => {
|
||||
const validateProviderInputs = async (inputs: unknown) => {
|
||||
const providerInputs = await DynamicSecretCassandraSchema.parseAsync(inputs);
|
||||
if (providerInputs.host === "localhost" || providerInputs.host === "127.0.0.1") {
|
||||
throw new BadRequestError({ message: "Invalid db host" });
|
||||
}
|
||||
|
||||
return providerInputs;
|
||||
};
|
||||
|
||||
const getClient = async (providerInputs: z.infer<typeof DynamicSecretCassandraSchema>) => {
|
||||
const sslOptions = providerInputs.ca ? { rejectUnauthorized: false, ca: providerInputs.ca } : undefined;
|
||||
const client = new cassandra.Client({
|
||||
sslOptions,
|
||||
protocolOptions: {
|
||||
port: providerInputs.port
|
||||
},
|
||||
credentials: {
|
||||
username: providerInputs.username,
|
||||
password: providerInputs.password
|
||||
},
|
||||
keyspace: providerInputs.keyspace,
|
||||
localDataCenter: providerInputs?.localDataCenter,
|
||||
contactPoints: providerInputs.host.split(",").filter(Boolean)
|
||||
});
|
||||
return client;
|
||||
};
|
||||
|
||||
const validateConnection = async (inputs: unknown) => {
|
||||
const providerInputs = await validateProviderInputs(inputs);
|
||||
const client = await getClient(providerInputs);
|
||||
|
||||
const isConnected = await client.execute("SELECT * FROM system_schema.keyspaces").then(() => true);
|
||||
await client.shutdown();
|
||||
return isConnected;
|
||||
};
|
||||
|
||||
const create = async (inputs: unknown, expireAt: number) => {
|
||||
const providerInputs = await validateProviderInputs(inputs);
|
||||
const client = await getClient(providerInputs);
|
||||
|
||||
const username = generateUsername();
|
||||
const password = generatePassword();
|
||||
const { keyspace } = providerInputs;
|
||||
const expiration = new Date(expireAt).toISOString();
|
||||
|
||||
const creationStatement = handlebars.compile(providerInputs.creationStatement, { noEscape: true })({
|
||||
username,
|
||||
password,
|
||||
expiration,
|
||||
keyspace
|
||||
});
|
||||
|
||||
const queries = creationStatement.toString().split(";").filter(Boolean);
|
||||
for (const query of queries) {
|
||||
// eslint-disable-next-line
|
||||
await client.execute(query);
|
||||
}
|
||||
await client.shutdown();
|
||||
|
||||
return { entityId: username, data: { DB_USERNAME: username, DB_PASSWORD: password } };
|
||||
};
|
||||
|
||||
const revoke = async (inputs: unknown, entityId: string) => {
|
||||
const providerInputs = await validateProviderInputs(inputs);
|
||||
const client = await getClient(providerInputs);
|
||||
|
||||
const username = entityId;
|
||||
const { keyspace } = providerInputs;
|
||||
|
||||
const revokeStatement = handlebars.compile(providerInputs.revocationStatement)({ username, keyspace });
|
||||
const queries = revokeStatement.toString().split(";").filter(Boolean);
|
||||
for (const query of queries) {
|
||||
// eslint-disable-next-line
|
||||
await client.execute(query);
|
||||
}
|
||||
await client.shutdown();
|
||||
return { entityId: username };
|
||||
};
|
||||
|
||||
const renew = async (inputs: unknown, entityId: string, expireAt: number) => {
|
||||
const providerInputs = await validateProviderInputs(inputs);
|
||||
const client = await getClient(providerInputs);
|
||||
|
||||
const username = entityId;
|
||||
const expiration = new Date(expireAt).toISOString();
|
||||
const { keyspace } = providerInputs;
|
||||
|
||||
const renewStatement = handlebars.compile(providerInputs.revocationStatement)({ username, keyspace, expiration });
|
||||
const queries = renewStatement.toString().split(";").filter(Boolean);
|
||||
for (const query of queries) {
|
||||
// eslint-disable-next-line
|
||||
await client.execute(query);
|
||||
}
|
||||
await client.shutdown();
|
||||
return { entityId: username };
|
||||
};
|
||||
|
||||
return {
|
||||
validateProviderInputs,
|
||||
validateConnection,
|
||||
create,
|
||||
revoke,
|
||||
renew
|
||||
};
|
||||
};
|
@ -1,6 +1,10 @@
|
||||
import { AwsIamProvider } from "./aws-iam";
|
||||
import { CassandraProvider } from "./cassandra";
|
||||
import { DynamicSecretProviders } from "./models";
|
||||
import { SqlDatabaseProvider } from "./sql-database";
|
||||
|
||||
export const buildDynamicSecretProviders = () => ({
|
||||
[DynamicSecretProviders.SqlDatabase]: SqlDatabaseProvider()
|
||||
[DynamicSecretProviders.SqlDatabase]: SqlDatabaseProvider(),
|
||||
[DynamicSecretProviders.Cassandra]: CassandraProvider(),
|
||||
[DynamicSecretProviders.AwsIam]: AwsIamProvider()
|
||||
});
|
||||
|
@ -2,28 +2,57 @@ import { z } from "zod";
|
||||
|
||||
export enum SqlProviders {
|
||||
Postgres = "postgres",
|
||||
MySQL = "mysql2"
|
||||
MySQL = "mysql2",
|
||||
Oracle = "oracledb"
|
||||
}
|
||||
|
||||
export const DynamicSecretSqlDBSchema = z.object({
|
||||
client: z.nativeEnum(SqlProviders),
|
||||
host: z.string().toLowerCase(),
|
||||
host: z.string().trim().toLowerCase(),
|
||||
port: z.number(),
|
||||
database: z.string(),
|
||||
username: z.string(),
|
||||
password: z.string(),
|
||||
creationStatement: z.string(),
|
||||
revocationStatement: z.string(),
|
||||
renewStatement: z.string().optional(),
|
||||
database: z.string().trim(),
|
||||
username: z.string().trim(),
|
||||
password: z.string().trim(),
|
||||
creationStatement: z.string().trim(),
|
||||
revocationStatement: z.string().trim(),
|
||||
renewStatement: z.string().trim().optional(),
|
||||
ca: z.string().optional()
|
||||
});
|
||||
|
||||
export const DynamicSecretCassandraSchema = z.object({
|
||||
host: z.string().trim().toLowerCase(),
|
||||
port: z.number(),
|
||||
localDataCenter: z.string().trim().min(1),
|
||||
keyspace: z.string().trim().optional(),
|
||||
username: z.string().trim(),
|
||||
password: z.string().trim(),
|
||||
creationStatement: z.string().trim(),
|
||||
revocationStatement: z.string().trim(),
|
||||
renewStatement: z.string().trim().optional(),
|
||||
ca: z.string().optional()
|
||||
});
|
||||
|
||||
export const DynamicSecretAwsIamSchema = z.object({
|
||||
accessKey: z.string().trim().min(1),
|
||||
secretAccessKey: z.string().trim().min(1),
|
||||
region: z.string().trim().min(1),
|
||||
awsPath: z.string().trim().optional(),
|
||||
permissionBoundaryPolicyArn: z.string().trim().optional(),
|
||||
policyDocument: z.string().trim().optional(),
|
||||
userGroups: z.string().trim().optional(),
|
||||
policyArns: z.string().trim().optional()
|
||||
});
|
||||
|
||||
export enum DynamicSecretProviders {
|
||||
SqlDatabase = "sql-database"
|
||||
SqlDatabase = "sql-database",
|
||||
Cassandra = "cassandra",
|
||||
AwsIam = "aws-iam"
|
||||
}
|
||||
|
||||
export const DynamicSecretProviderSchema = z.discriminatedUnion("type", [
|
||||
z.object({ type: z.literal(DynamicSecretProviders.SqlDatabase), inputs: DynamicSecretSqlDBSchema })
|
||||
z.object({ type: z.literal(DynamicSecretProviders.SqlDatabase), inputs: DynamicSecretSqlDBSchema }),
|
||||
z.object({ type: z.literal(DynamicSecretProviders.Cassandra), inputs: DynamicSecretCassandraSchema }),
|
||||
z.object({ type: z.literal(DynamicSecretProviders.AwsIam), inputs: DynamicSecretAwsIamSchema })
|
||||
]);
|
||||
|
||||
export type TDynamicProviderFns = {
|
||||
|
@ -8,31 +8,46 @@ import { BadRequestError } from "@app/lib/errors";
|
||||
import { getDbConnectionHost } from "@app/lib/knex";
|
||||
import { alphaNumericNanoId } from "@app/lib/nanoid";
|
||||
|
||||
import { DynamicSecretSqlDBSchema, TDynamicProviderFns } from "./models";
|
||||
import { DynamicSecretSqlDBSchema, SqlProviders, TDynamicProviderFns } from "./models";
|
||||
|
||||
const EXTERNAL_REQUEST_TIMEOUT = 10 * 1000;
|
||||
|
||||
const generatePassword = (size?: number) => {
|
||||
const generatePassword = (provider: SqlProviders) => {
|
||||
// oracle has limit of 48 password length
|
||||
const size = provider === SqlProviders.Oracle ? 30 : 48;
|
||||
|
||||
const charset = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-_.~!*$#";
|
||||
return customAlphabet(charset, 48)(size);
|
||||
};
|
||||
|
||||
const generateUsername = (provider: SqlProviders) => {
|
||||
// For oracle, the client assumes everything is upper case when not using quotes around the password
|
||||
if (provider === SqlProviders.Oracle) return alphaNumericNanoId(32).toUpperCase();
|
||||
|
||||
return alphaNumericNanoId(32);
|
||||
};
|
||||
|
||||
export const SqlDatabaseProvider = (): TDynamicProviderFns => {
|
||||
const validateProviderInputs = async (inputs: unknown) => {
|
||||
const appCfg = getConfig();
|
||||
const isCloud = Boolean(appCfg.LICENSE_SERVER_KEY); // quick and dirty way to check if its cloud or not
|
||||
const dbHost = appCfg.DB_HOST || getDbConnectionHost(appCfg.DB_CONNECTION_URI);
|
||||
|
||||
const providerInputs = await DynamicSecretSqlDBSchema.parseAsync(inputs);
|
||||
if (
|
||||
isCloud &&
|
||||
// localhost
|
||||
// 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" });
|
||||
if (
|
||||
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+/)
|
||||
dbHost === providerInputs.host
|
||||
)
|
||||
throw new BadRequestError({ message: "Invalid db host" });
|
||||
return providerInputs;
|
||||
@ -59,10 +74,10 @@ export const SqlDatabaseProvider = (): TDynamicProviderFns => {
|
||||
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);
|
||||
// oracle needs from keyword
|
||||
const testStatement = providerInputs.client === SqlProviders.Oracle ? "SELECT 1 FROM DUAL" : "SELECT 1";
|
||||
|
||||
const isConnected = await db.raw(testStatement).then(() => true);
|
||||
await db.destroy();
|
||||
return isConnected;
|
||||
};
|
||||
@ -71,8 +86,8 @@ export const SqlDatabaseProvider = (): TDynamicProviderFns => {
|
||||
const providerInputs = await validateProviderInputs(inputs);
|
||||
const db = await getClient(providerInputs);
|
||||
|
||||
const username = alphaNumericNanoId(32);
|
||||
const password = generatePassword();
|
||||
const username = generateUsername(providerInputs.client);
|
||||
const password = generatePassword(providerInputs.client);
|
||||
const { database } = providerInputs;
|
||||
const expiration = new Date(expireAt).toISOString();
|
||||
|
||||
@ -83,15 +98,13 @@ export const SqlDatabaseProvider = (): TDynamicProviderFns => {
|
||||
database
|
||||
});
|
||||
|
||||
await db.transaction(async (tx) =>
|
||||
Promise.all(
|
||||
creationStatement
|
||||
.toString()
|
||||
.split(";")
|
||||
.filter(Boolean)
|
||||
.map((query) => tx.raw(query))
|
||||
)
|
||||
);
|
||||
const queries = creationStatement.toString().split(";").filter(Boolean);
|
||||
await db.transaction(async (tx) => {
|
||||
for (const query of queries) {
|
||||
// eslint-disable-next-line
|
||||
await tx.raw(query);
|
||||
}
|
||||
});
|
||||
await db.destroy();
|
||||
return { entityId: username, data: { DB_USERNAME: username, DB_PASSWORD: password } };
|
||||
};
|
||||
@ -104,15 +117,13 @@ export const SqlDatabaseProvider = (): TDynamicProviderFns => {
|
||||
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))
|
||||
)
|
||||
);
|
||||
const queries = revokeStatement.toString().split(";").filter(Boolean);
|
||||
await db.transaction(async (tx) => {
|
||||
for (const query of queries) {
|
||||
// eslint-disable-next-line
|
||||
await tx.raw(query);
|
||||
}
|
||||
});
|
||||
|
||||
await db.destroy();
|
||||
return { entityId: username };
|
||||
@ -127,16 +138,15 @@ export const SqlDatabaseProvider = (): TDynamicProviderFns => {
|
||||
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))
|
||||
)
|
||||
);
|
||||
if (renewStatement) {
|
||||
const queries = renewStatement.toString().split(";").filter(Boolean);
|
||||
await db.transaction(async (tx) => {
|
||||
for (const query of queries) {
|
||||
// eslint-disable-next-line
|
||||
await tx.raw(query);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
await db.destroy();
|
||||
return { entityId: username };
|
||||
|
@ -59,32 +59,6 @@ export const groupDALFactory = (db: TDbClient) => {
|
||||
}
|
||||
};
|
||||
|
||||
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,
|
||||
@ -150,7 +124,6 @@ export const groupDALFactory = (db: TDbClient) => {
|
||||
return {
|
||||
findGroups,
|
||||
findByOrgId,
|
||||
countAllGroupMembers,
|
||||
findAllGroupMembers,
|
||||
...groupOrm
|
||||
};
|
||||
|
450
backend/src/ee/services/group/group-fns.ts
Normal file
450
backend/src/ee/services/group/group-fns.ts
Normal file
@ -0,0 +1,450 @@
|
||||
import { Knex } from "knex";
|
||||
|
||||
import { SecretKeyEncoding, TableName, TUsers } from "@app/db/schemas";
|
||||
import { decryptAsymmetric, encryptAsymmetric, infisicalSymmetricDecrypt } from "@app/lib/crypto/encryption";
|
||||
import { BadRequestError, ScimRequestError } from "@app/lib/errors";
|
||||
|
||||
import {
|
||||
TAddUsersToGroup,
|
||||
TAddUsersToGroupByUserIds,
|
||||
TConvertPendingGroupAdditionsToGroupMemberships,
|
||||
TRemoveUsersFromGroupByUserIds
|
||||
} from "./group-types";
|
||||
|
||||
const addAcceptedUsersToGroup = async ({
|
||||
userIds,
|
||||
group,
|
||||
userGroupMembershipDAL,
|
||||
userDAL,
|
||||
groupProjectDAL,
|
||||
projectKeyDAL,
|
||||
projectDAL,
|
||||
projectBotDAL,
|
||||
tx
|
||||
}: TAddUsersToGroup) => {
|
||||
const users = await userDAL.findUserEncKeyByUserIdsBatch(
|
||||
{
|
||||
userIds
|
||||
},
|
||||
tx
|
||||
);
|
||||
|
||||
await userGroupMembershipDAL.insertMany(
|
||||
users.map((user) => ({
|
||||
userId: user.userId,
|
||||
groupId: group.id,
|
||||
isPending: false
|
||||
})),
|
||||
tx
|
||||
);
|
||||
|
||||
// check which projects the group is part of
|
||||
const projectIds = Array.from(
|
||||
new Set(
|
||||
(
|
||||
await groupProjectDAL.find(
|
||||
{
|
||||
groupId: group.id
|
||||
},
|
||||
{ tx }
|
||||
)
|
||||
).map((gp) => gp.projectId)
|
||||
)
|
||||
);
|
||||
|
||||
const keys = await projectKeyDAL.find(
|
||||
{
|
||||
$in: {
|
||||
projectId: projectIds,
|
||||
receiverId: users.map((u) => u.id)
|
||||
}
|
||||
},
|
||||
{ tx }
|
||||
);
|
||||
|
||||
const userKeysSet = new Set(keys.map((k) => `${k.projectId}-${k.receiverId}`));
|
||||
|
||||
for await (const projectId of projectIds) {
|
||||
const usersToAddProjectKeyFor = users.filter((u) => !userKeysSet.has(`${projectId}-${u.userId}`));
|
||||
|
||||
if (usersToAddProjectKeyFor.length) {
|
||||
// there are users who need to be shared keys
|
||||
// process adding bulk users to projects for each project individually
|
||||
const ghostUser = await projectDAL.findProjectGhostUser(projectId, tx);
|
||||
|
||||
if (!ghostUser) {
|
||||
throw new BadRequestError({
|
||||
message: "Failed to find sudo user"
|
||||
});
|
||||
}
|
||||
|
||||
const ghostUserLatestKey = await projectKeyDAL.findLatestProjectKey(ghostUser.id, projectId, tx);
|
||||
|
||||
if (!ghostUserLatestKey) {
|
||||
throw new BadRequestError({
|
||||
message: "Failed to find sudo user latest key"
|
||||
});
|
||||
}
|
||||
|
||||
const bot = await projectBotDAL.findOne({ projectId }, tx);
|
||||
|
||||
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 projectKeysToAdd = usersToAddProjectKeyFor.map((user) => {
|
||||
const { ciphertext: encryptedKey, nonce } = encryptAsymmetric(
|
||||
plaintextProjectKey,
|
||||
user.publicKey,
|
||||
botPrivateKey
|
||||
);
|
||||
return {
|
||||
encryptedKey,
|
||||
nonce,
|
||||
senderId: ghostUser.id,
|
||||
receiverId: user.userId,
|
||||
projectId
|
||||
};
|
||||
});
|
||||
|
||||
await projectKeyDAL.insertMany(projectKeysToAdd, tx);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Add users with user ids [userIds] to group [group].
|
||||
* - Users may or may not have finished completing their accounts; this function will
|
||||
* handle both adding users to groups directly and via pending group additions.
|
||||
* @param {group} group - group to add user(s) to
|
||||
* @param {string[]} userIds - id(s) of user(s) to add to group
|
||||
*/
|
||||
export const addUsersToGroupByUserIds = async ({
|
||||
group,
|
||||
userIds,
|
||||
userDAL,
|
||||
userGroupMembershipDAL,
|
||||
orgDAL,
|
||||
groupProjectDAL,
|
||||
projectKeyDAL,
|
||||
projectDAL,
|
||||
projectBotDAL,
|
||||
tx: outerTx
|
||||
}: TAddUsersToGroupByUserIds) => {
|
||||
const processAddition = async (tx: Knex) => {
|
||||
const foundMembers = await userDAL.find(
|
||||
{
|
||||
$in: {
|
||||
id: userIds
|
||||
}
|
||||
},
|
||||
{ tx }
|
||||
);
|
||||
|
||||
const foundMembersIdsSet = new Set(foundMembers.map((member) => member.id));
|
||||
|
||||
const isCompleteMatch = userIds.every((userId) => foundMembersIdsSet.has(userId));
|
||||
|
||||
if (!isCompleteMatch) {
|
||||
throw new ScimRequestError({
|
||||
detail: "Members not found",
|
||||
status: 404
|
||||
});
|
||||
}
|
||||
|
||||
// check if user(s) group membership(s) already exists
|
||||
const existingUserGroupMemberships = await userGroupMembershipDAL.find(
|
||||
{
|
||||
groupId: group.id,
|
||||
$in: {
|
||||
userId: userIds
|
||||
}
|
||||
},
|
||||
{ tx }
|
||||
);
|
||||
|
||||
if (existingUserGroupMemberships.length) {
|
||||
throw new BadRequestError({
|
||||
message: `User(s) are already part of the group ${group.slug}`
|
||||
});
|
||||
}
|
||||
|
||||
// check if all user(s) are part of the organization
|
||||
const existingUserOrgMemberships = await orgDAL.findMembership(
|
||||
{
|
||||
[`${TableName.OrgMembership}.orgId` as "orgId"]: group.orgId,
|
||||
$in: {
|
||||
[`${TableName.OrgMembership}.userId` as "userId"]: userIds
|
||||
}
|
||||
},
|
||||
{ tx }
|
||||
);
|
||||
|
||||
const existingUserOrgMembershipsUserIdsSet = new Set(existingUserOrgMemberships.map((u) => u.userId));
|
||||
|
||||
userIds.forEach((userId) => {
|
||||
if (!existingUserOrgMembershipsUserIdsSet.has(userId))
|
||||
throw new BadRequestError({
|
||||
message: `User with id ${userId} is not part of the organization`
|
||||
});
|
||||
});
|
||||
|
||||
const membersToAddToGroupNonPending: TUsers[] = [];
|
||||
const membersToAddToGroupPending: TUsers[] = [];
|
||||
|
||||
foundMembers.forEach((member) => {
|
||||
if (member.isAccepted) {
|
||||
// add accepted member to group
|
||||
membersToAddToGroupNonPending.push(member);
|
||||
} else {
|
||||
// add incomplete member to pending group addition
|
||||
membersToAddToGroupPending.push(member);
|
||||
}
|
||||
});
|
||||
|
||||
if (membersToAddToGroupNonPending.length) {
|
||||
await addAcceptedUsersToGroup({
|
||||
userIds: membersToAddToGroupNonPending.map((member) => member.id),
|
||||
group,
|
||||
userDAL,
|
||||
userGroupMembershipDAL,
|
||||
groupProjectDAL,
|
||||
projectKeyDAL,
|
||||
projectDAL,
|
||||
projectBotDAL,
|
||||
tx
|
||||
});
|
||||
}
|
||||
|
||||
if (membersToAddToGroupPending.length) {
|
||||
await userGroupMembershipDAL.insertMany(
|
||||
membersToAddToGroupPending.map((member) => ({
|
||||
userId: member.id,
|
||||
groupId: group.id,
|
||||
isPending: true
|
||||
})),
|
||||
tx
|
||||
);
|
||||
}
|
||||
|
||||
return membersToAddToGroupNonPending.concat(membersToAddToGroupPending);
|
||||
};
|
||||
|
||||
if (outerTx) {
|
||||
return processAddition(outerTx);
|
||||
}
|
||||
return userDAL.transaction(async (tx) => {
|
||||
return processAddition(tx);
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Remove users with user ids [userIds] from group [group].
|
||||
* - Users may be part of the group (non-pending + pending);
|
||||
* this function will handle both cases.
|
||||
* @param {group} group - group to remove user(s) from
|
||||
* @param {string[]} userIds - id(s) of user(s) to remove from group
|
||||
*/
|
||||
export const removeUsersFromGroupByUserIds = async ({
|
||||
group,
|
||||
userIds,
|
||||
userDAL,
|
||||
userGroupMembershipDAL,
|
||||
groupProjectDAL,
|
||||
projectKeyDAL,
|
||||
tx: outerTx
|
||||
}: TRemoveUsersFromGroupByUserIds) => {
|
||||
const processRemoval = async (tx: Knex) => {
|
||||
const foundMembers = await userDAL.find({
|
||||
$in: {
|
||||
id: userIds
|
||||
}
|
||||
});
|
||||
|
||||
const foundMembersIdsSet = new Set(foundMembers.map((member) => member.id));
|
||||
|
||||
const isCompleteMatch = userIds.every((userId) => foundMembersIdsSet.has(userId));
|
||||
|
||||
if (!isCompleteMatch) {
|
||||
throw new ScimRequestError({
|
||||
detail: "Members not found",
|
||||
status: 404
|
||||
});
|
||||
}
|
||||
|
||||
// check if user group membership already exists
|
||||
const existingUserGroupMemberships = await userGroupMembershipDAL.find(
|
||||
{
|
||||
groupId: group.id,
|
||||
$in: {
|
||||
userId: userIds
|
||||
}
|
||||
},
|
||||
{ tx }
|
||||
);
|
||||
|
||||
const existingUserGroupMembershipsUserIdsSet = new Set(existingUserGroupMemberships.map((u) => u.userId));
|
||||
|
||||
userIds.forEach((userId) => {
|
||||
if (!existingUserGroupMembershipsUserIdsSet.has(userId))
|
||||
throw new BadRequestError({
|
||||
message: `User(s) are not part of the group ${group.slug}`
|
||||
});
|
||||
});
|
||||
|
||||
const membersToRemoveFromGroupNonPending: TUsers[] = [];
|
||||
const membersToRemoveFromGroupPending: TUsers[] = [];
|
||||
|
||||
foundMembers.forEach((member) => {
|
||||
if (member.isAccepted) {
|
||||
// remove accepted member from group
|
||||
membersToRemoveFromGroupNonPending.push(member);
|
||||
} else {
|
||||
// remove incomplete member from pending group addition
|
||||
membersToRemoveFromGroupPending.push(member);
|
||||
}
|
||||
});
|
||||
|
||||
if (membersToRemoveFromGroupNonPending.length) {
|
||||
// check which projects the group is part of
|
||||
const projectIds = Array.from(
|
||||
new Set(
|
||||
(
|
||||
await groupProjectDAL.find(
|
||||
{
|
||||
groupId: group.id
|
||||
},
|
||||
{ tx }
|
||||
)
|
||||
).map((gp) => gp.projectId)
|
||||
)
|
||||
);
|
||||
|
||||
// TODO: this part can be optimized
|
||||
for await (const userId of userIds) {
|
||||
const t = await userGroupMembershipDAL.filterProjectsByUserMembership(userId, group.id, projectIds, tx);
|
||||
const projectsToDeleteKeyFor = projectIds.filter((p) => !t.has(p));
|
||||
|
||||
if (projectsToDeleteKeyFor.length) {
|
||||
await projectKeyDAL.delete(
|
||||
{
|
||||
receiverId: userId,
|
||||
$in: {
|
||||
projectId: projectsToDeleteKeyFor
|
||||
}
|
||||
},
|
||||
tx
|
||||
);
|
||||
}
|
||||
|
||||
await userGroupMembershipDAL.delete(
|
||||
{
|
||||
groupId: group.id,
|
||||
userId
|
||||
},
|
||||
tx
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if (membersToRemoveFromGroupPending.length) {
|
||||
await userGroupMembershipDAL.delete({
|
||||
groupId: group.id,
|
||||
$in: {
|
||||
userId: membersToRemoveFromGroupPending.map((member) => member.id)
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return membersToRemoveFromGroupNonPending.concat(membersToRemoveFromGroupPending);
|
||||
};
|
||||
|
||||
if (outerTx) {
|
||||
return processRemoval(outerTx);
|
||||
}
|
||||
return userDAL.transaction(async (tx) => {
|
||||
return processRemoval(tx);
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Convert pending group additions for users with ids [userIds] to group memberships.
|
||||
* @param {string[]} userIds - id(s) of user(s) to try to convert pending group additions to group memberships
|
||||
*/
|
||||
export const convertPendingGroupAdditionsToGroupMemberships = async ({
|
||||
userIds,
|
||||
userDAL,
|
||||
userGroupMembershipDAL,
|
||||
groupProjectDAL,
|
||||
projectKeyDAL,
|
||||
projectDAL,
|
||||
projectBotDAL,
|
||||
tx: outerTx
|
||||
}: TConvertPendingGroupAdditionsToGroupMemberships) => {
|
||||
const processConversion = async (tx: Knex) => {
|
||||
const users = await userDAL.find(
|
||||
{
|
||||
$in: {
|
||||
id: userIds
|
||||
}
|
||||
},
|
||||
{ tx }
|
||||
);
|
||||
|
||||
const usersUserIdsSet = new Set(users.map((u) => u.id));
|
||||
userIds.forEach((userId) => {
|
||||
if (!usersUserIdsSet.has(userId)) {
|
||||
throw new BadRequestError({
|
||||
message: `Failed to find user with id ${userId}`
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
users.forEach((user) => {
|
||||
if (!user.isAccepted) {
|
||||
throw new BadRequestError({
|
||||
message: `Failed to convert pending group additions to group memberships for user ${user.username} because they have not confirmed their account`
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
const pendingGroupAdditions = await userGroupMembershipDAL.deletePendingUserGroupMembershipsByUserIds(userIds, tx);
|
||||
|
||||
for await (const pendingGroupAddition of pendingGroupAdditions) {
|
||||
await addAcceptedUsersToGroup({
|
||||
userIds: [pendingGroupAddition.user.id],
|
||||
group: pendingGroupAddition.group,
|
||||
userDAL,
|
||||
userGroupMembershipDAL,
|
||||
groupProjectDAL,
|
||||
projectKeyDAL,
|
||||
projectDAL,
|
||||
projectBotDAL,
|
||||
tx
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
if (outerTx) {
|
||||
return processConversion(outerTx);
|
||||
}
|
||||
return userDAL.transaction(async (tx) => {
|
||||
await processConversion(tx);
|
||||
});
|
||||
};
|
@ -1,22 +1,22 @@
|
||||
import { ForbiddenError } from "@casl/ability";
|
||||
import slugify from "@sindresorhus/slugify";
|
||||
|
||||
import { OrgMembershipRole, SecretKeyEncoding, TOrgRoles } from "@app/db/schemas";
|
||||
import { OrgMembershipRole, 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 "@app/services/group-project/group-project-dal";
|
||||
import { TOrgDALFactory } from "@app/services/org/org-dal";
|
||||
import { TProjectDALFactory } from "@app/services/project/project-dal";
|
||||
import { TProjectBotDALFactory } from "@app/services/project-bot/project-bot-dal";
|
||||
import { TProjectKeyDALFactory } from "@app/services/project-key/project-key-dal";
|
||||
import { TUserDALFactory } from "@app/services/user/user-dal";
|
||||
|
||||
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 { addUsersToGroupByUserIds, removeUsersFromGroupByUserIds } from "./group-fns";
|
||||
import {
|
||||
TAddUserToGroupDTO,
|
||||
TCreateGroupDTO,
|
||||
@ -28,20 +28,17 @@ import {
|
||||
import { TUserGroupMembershipDALFactory } from "./user-group-membership-dal";
|
||||
|
||||
type TGroupServiceFactoryDep = {
|
||||
userDAL: Pick<TUserDALFactory, "findOne" | "findUserEncKeyByUsername">;
|
||||
groupDAL: Pick<
|
||||
TGroupDALFactory,
|
||||
"create" | "findOne" | "update" | "delete" | "findAllGroupMembers" | "countAllGroupMembers"
|
||||
>;
|
||||
userDAL: Pick<TUserDALFactory, "find" | "findUserEncKeyByUserIdsBatch" | "transaction" | "findOne">;
|
||||
groupDAL: Pick<TGroupDALFactory, "create" | "findOne" | "update" | "delete" | "findAllGroupMembers">;
|
||||
groupProjectDAL: Pick<TGroupProjectDALFactory, "find">;
|
||||
orgDAL: Pick<TOrgDALFactory, "findMembership">;
|
||||
orgDAL: Pick<TOrgDALFactory, "findMembership" | "countAllOrgMembers">;
|
||||
userGroupMembershipDAL: Pick<
|
||||
TUserGroupMembershipDALFactory,
|
||||
"findOne" | "create" | "delete" | "filterProjectsByUserMembership"
|
||||
"findOne" | "delete" | "filterProjectsByUserMembership" | "transaction" | "insertMany" | "find"
|
||||
>;
|
||||
projectDAL: Pick<TProjectDALFactory, "findProjectGhostUser">;
|
||||
projectBotDAL: Pick<TProjectBotDALFactory, "findOne">;
|
||||
projectKeyDAL: Pick<TProjectKeyDALFactory, "find" | "create" | "delete" | "findLatestProjectKey">;
|
||||
projectKeyDAL: Pick<TProjectKeyDALFactory, "find" | "delete" | "findLatestProjectKey" | "insertMany">;
|
||||
permissionService: Pick<TPermissionServiceFactory, "getOrgPermission" | "getOrgPermissionByRole">;
|
||||
licenseService: Pick<TLicenseServiceFactory, "getPlan">;
|
||||
};
|
||||
@ -227,12 +224,9 @@ export const groupServiceFactory = ({
|
||||
username
|
||||
});
|
||||
|
||||
const totalCount = await groupDAL.countAllGroupMembers({
|
||||
orgId: group.orgId,
|
||||
groupId: group.id
|
||||
});
|
||||
const count = await orgDAL.countAllOrgMembers(group.orgId);
|
||||
|
||||
return { users, totalCount };
|
||||
return { users, totalCount: count };
|
||||
};
|
||||
|
||||
const addUserToGroup = async ({
|
||||
@ -272,111 +266,22 @@ export const groupServiceFactory = ({
|
||||
if (!hasRequiredPriviledges)
|
||||
throw new ForbiddenRequestError({ message: "Failed to add user to more privileged group" });
|
||||
|
||||
// get user with username
|
||||
const user = await userDAL.findUserEncKeyByUsername({
|
||||
username
|
||||
const user = await userDAL.findOne({ username });
|
||||
if (!user) throw new BadRequestError({ message: `Failed to find user with username ${username}` });
|
||||
|
||||
const users = await addUsersToGroupByUserIds({
|
||||
group,
|
||||
userIds: [user.id],
|
||||
userDAL,
|
||||
userGroupMembershipDAL,
|
||||
orgDAL,
|
||||
groupProjectDAL,
|
||||
projectKeyDAL,
|
||||
projectDAL,
|
||||
projectBotDAL
|
||||
});
|
||||
|
||||
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;
|
||||
return users[0];
|
||||
};
|
||||
|
||||
const removeUserFromGroup = async ({
|
||||
@ -416,51 +321,19 @@ export const groupServiceFactory = ({
|
||||
if (!hasRequiredPriviledges)
|
||||
throw new ForbiddenRequestError({ message: "Failed to delete user from more privileged group" });
|
||||
|
||||
const user = await userDAL.findOne({
|
||||
username
|
||||
const user = await userDAL.findOne({ username });
|
||||
if (!user) throw new BadRequestError({ message: `Failed to find user with username ${username}` });
|
||||
|
||||
const users = await removeUsersFromGroupByUserIds({
|
||||
group,
|
||||
userIds: [user.id],
|
||||
userDAL,
|
||||
userGroupMembershipDAL,
|
||||
groupProjectDAL,
|
||||
projectKeyDAL
|
||||
});
|
||||
|
||||
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 users[0];
|
||||
};
|
||||
|
||||
return {
|
||||
|
@ -1,4 +1,14 @@
|
||||
import { Knex } from "knex";
|
||||
|
||||
import { TGroups } from "@app/db/schemas";
|
||||
import { TUserGroupMembershipDALFactory } from "@app/ee/services/group/user-group-membership-dal";
|
||||
import { TGenericPermission } from "@app/lib/types";
|
||||
import { TGroupProjectDALFactory } from "@app/services/group-project/group-project-dal";
|
||||
import { TOrgDALFactory } from "@app/services/org/org-dal";
|
||||
import { TProjectDALFactory } from "@app/services/project/project-dal";
|
||||
import { TProjectBotDALFactory } from "@app/services/project-bot/project-bot-dal";
|
||||
import { TProjectKeyDALFactory } from "@app/services/project-key/project-key-dal";
|
||||
import { TUserDALFactory } from "@app/services/user/user-dal";
|
||||
|
||||
export type TCreateGroupDTO = {
|
||||
name: string;
|
||||
@ -35,3 +45,54 @@ export type TRemoveUserFromGroupDTO = {
|
||||
groupSlug: string;
|
||||
username: string;
|
||||
} & TGenericPermission;
|
||||
|
||||
// group fns types
|
||||
|
||||
export type TAddUsersToGroup = {
|
||||
userIds: string[];
|
||||
group: TGroups;
|
||||
userDAL: Pick<TUserDALFactory, "findUserEncKeyByUserIdsBatch">;
|
||||
userGroupMembershipDAL: Pick<TUserGroupMembershipDALFactory, "find" | "transaction" | "insertMany">;
|
||||
groupProjectDAL: Pick<TGroupProjectDALFactory, "find">;
|
||||
projectKeyDAL: Pick<TProjectKeyDALFactory, "find" | "findLatestProjectKey" | "insertMany">;
|
||||
projectDAL: Pick<TProjectDALFactory, "findProjectGhostUser">;
|
||||
projectBotDAL: Pick<TProjectBotDALFactory, "findOne">;
|
||||
tx: Knex;
|
||||
};
|
||||
|
||||
export type TAddUsersToGroupByUserIds = {
|
||||
group: TGroups;
|
||||
userIds: string[];
|
||||
userDAL: Pick<TUserDALFactory, "find" | "findUserEncKeyByUserIdsBatch" | "transaction">;
|
||||
userGroupMembershipDAL: Pick<TUserGroupMembershipDALFactory, "find" | "transaction" | "insertMany">;
|
||||
orgDAL: Pick<TOrgDALFactory, "findMembership">;
|
||||
groupProjectDAL: Pick<TGroupProjectDALFactory, "find">;
|
||||
projectKeyDAL: Pick<TProjectKeyDALFactory, "find" | "findLatestProjectKey" | "insertMany">;
|
||||
projectDAL: Pick<TProjectDALFactory, "findProjectGhostUser">;
|
||||
projectBotDAL: Pick<TProjectBotDALFactory, "findOne">;
|
||||
tx?: Knex;
|
||||
};
|
||||
|
||||
export type TRemoveUsersFromGroupByUserIds = {
|
||||
group: TGroups;
|
||||
userIds: string[];
|
||||
userDAL: Pick<TUserDALFactory, "find" | "transaction">;
|
||||
userGroupMembershipDAL: Pick<TUserGroupMembershipDALFactory, "find" | "filterProjectsByUserMembership" | "delete">;
|
||||
groupProjectDAL: Pick<TGroupProjectDALFactory, "find">;
|
||||
projectKeyDAL: Pick<TProjectKeyDALFactory, "delete">;
|
||||
tx?: Knex;
|
||||
};
|
||||
|
||||
export type TConvertPendingGroupAdditionsToGroupMemberships = {
|
||||
userIds: string[];
|
||||
userDAL: Pick<TUserDALFactory, "findUserEncKeyByUserIdsBatch" | "transaction" | "find" | "findById">;
|
||||
userGroupMembershipDAL: Pick<
|
||||
TUserGroupMembershipDALFactory,
|
||||
"find" | "transaction" | "insertMany" | "deletePendingUserGroupMembershipsByUserIds"
|
||||
>;
|
||||
groupProjectDAL: Pick<TGroupProjectDALFactory, "find">;
|
||||
projectKeyDAL: Pick<TProjectKeyDALFactory, "find" | "findLatestProjectKey" | "insertMany">;
|
||||
projectDAL: Pick<TProjectDALFactory, "findProjectGhostUser">;
|
||||
projectBotDAL: Pick<TProjectBotDALFactory, "findOne">;
|
||||
tx?: Knex;
|
||||
};
|
||||
|
@ -1,3 +1,5 @@
|
||||
import { Knex } from "knex";
|
||||
|
||||
import { TDbClient } from "@app/db";
|
||||
import { TableName, TUserEncryptionKeys } from "@app/db/schemas";
|
||||
import { DatabaseError } from "@app/lib/errors";
|
||||
@ -14,24 +16,28 @@ export const userGroupMembershipDALFactory = (db: TDbClient) => {
|
||||
* - 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 filterProjectsByUserMembership = async (userId: string, groupId: string, projectIds: string[], tx?: Knex) => {
|
||||
try {
|
||||
const userProjectMemberships: string[] = await (tx || 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`);
|
||||
const userGroupMemberships: string[] = await (tx || 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));
|
||||
return new Set(userProjectMemberships.concat(userGroupMemberships));
|
||||
} catch (error) {
|
||||
throw new DatabaseError({ error, name: "Filter projects by user membership" });
|
||||
}
|
||||
};
|
||||
|
||||
// special query
|
||||
@ -45,7 +51,7 @@ export const userGroupMembershipDALFactory = (db: TDbClient) => {
|
||||
)
|
||||
.join(TableName.Users, `${TableName.UserGroupMembership}.userId`, `${TableName.Users}.id`)
|
||||
.where(`${TableName.GroupProjectMembership}.projectId`, projectId)
|
||||
.whereIn(`${TableName.Users}.username`, usernames) // TODO: pluck usernames
|
||||
.whereIn(`${TableName.Users}.username`, usernames)
|
||||
.pluck(`${TableName.Users}.id`);
|
||||
|
||||
return usernameDocs;
|
||||
@ -55,7 +61,7 @@ export const userGroupMembershipDALFactory = (db: TDbClient) => {
|
||||
};
|
||||
|
||||
/**
|
||||
* Return list of users that are part of the group with id [groupId]
|
||||
* Return list of completed/accepted 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.
|
||||
@ -63,18 +69,19 @@ export const userGroupMembershipDALFactory = (db: TDbClient) => {
|
||||
* @param projectId
|
||||
* @returns
|
||||
*/
|
||||
const findGroupMembersNotInProject = async (groupId: string, projectId: string) => {
|
||||
const findGroupMembersNotInProject = async (groupId: string, projectId: string, tx?: Knex) => {
|
||||
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)
|
||||
const groups: string[] = await (tx || 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)
|
||||
const members = await (tx || db)(TableName.UserGroupMembership)
|
||||
.where(`${TableName.UserGroupMembership}.groupId`, groupId)
|
||||
.where(`${TableName.UserGroupMembership}.isPending`, false)
|
||||
.join(TableName.Users, `${TableName.UserGroupMembership}.userId`, `${TableName.Users}.id`)
|
||||
.leftJoin(TableName.ProjectMembership, function () {
|
||||
this.on(`${TableName.Users}.id`, "=", `${TableName.ProjectMembership}.userId`).andOn(
|
||||
@ -116,10 +123,49 @@ export const userGroupMembershipDALFactory = (db: TDbClient) => {
|
||||
}
|
||||
};
|
||||
|
||||
const deletePendingUserGroupMembershipsByUserIds = async (userIds: string[], tx?: Knex) => {
|
||||
try {
|
||||
const members = await (tx || db)(TableName.UserGroupMembership)
|
||||
.whereIn(`${TableName.UserGroupMembership}.userId`, userIds)
|
||||
.where(`${TableName.UserGroupMembership}.isPending`, true)
|
||||
.join(TableName.Groups, `${TableName.UserGroupMembership}.groupId`, `${TableName.Groups}.id`)
|
||||
.join(TableName.Users, `${TableName.UserGroupMembership}.userId`, `${TableName.Users}.id`);
|
||||
|
||||
await userGroupMembershipOrm.delete(
|
||||
{
|
||||
$in: {
|
||||
userId: userIds
|
||||
}
|
||||
},
|
||||
tx
|
||||
);
|
||||
|
||||
return members.map(({ userId, username, groupId, orgId, name, slug, role, roleId }) => ({
|
||||
user: {
|
||||
id: userId,
|
||||
username
|
||||
},
|
||||
group: {
|
||||
id: groupId,
|
||||
orgId,
|
||||
name,
|
||||
slug,
|
||||
role,
|
||||
roleId,
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date()
|
||||
}
|
||||
}));
|
||||
} catch (error) {
|
||||
throw new DatabaseError({ error, name: "Delete pending user group memberships by user ids" });
|
||||
}
|
||||
};
|
||||
|
||||
return {
|
||||
...userGroupMembershipOrm,
|
||||
filterProjectsByUserMembership,
|
||||
findUserGroupMembershipsInProject,
|
||||
findGroupMembersNotInProject
|
||||
findGroupMembersNotInProject,
|
||||
deletePendingUserGroupMembershipsByUserIds
|
||||
};
|
||||
};
|
||||
|
@ -1,5 +1,7 @@
|
||||
import { ForbiddenError } from "@casl/ability";
|
||||
import { ForbiddenError, MongoAbility, RawRuleOf } from "@casl/ability";
|
||||
import { PackRule, unpackRules } from "@casl/ability/extra";
|
||||
import ms from "ms";
|
||||
import { z } from "zod";
|
||||
|
||||
import { isAtLeastAsPrivileged } from "@app/lib/casl";
|
||||
import { BadRequestError, ForbiddenRequestError } from "@app/lib/errors";
|
||||
@ -8,7 +10,7 @@ import { TIdentityProjectDALFactory } from "@app/services/identity-project/ident
|
||||
import { TProjectDALFactory } from "@app/services/project/project-dal";
|
||||
|
||||
import { TPermissionServiceFactory } from "../permission/permission-service";
|
||||
import { ProjectPermissionActions, ProjectPermissionSub } from "../permission/project-permission";
|
||||
import { ProjectPermissionActions, ProjectPermissionSet, ProjectPermissionSub } from "../permission/project-permission";
|
||||
import { TIdentityProjectAdditionalPrivilegeDALFactory } from "./identity-project-additional-privilege-dal";
|
||||
import {
|
||||
IdentityProjectAdditionalPrivilegeTemporaryMode,
|
||||
@ -30,6 +32,27 @@ export type TIdentityProjectAdditionalPrivilegeServiceFactory = ReturnType<
|
||||
typeof identityProjectAdditionalPrivilegeServiceFactory
|
||||
>;
|
||||
|
||||
// TODO(akhilmhdh): move this to more centralized
|
||||
export const UnpackedPermissionSchema = z.object({
|
||||
subject: z.union([z.string().min(1), z.string().array()]).optional(),
|
||||
action: z.union([z.string().min(1), z.string().array()]),
|
||||
conditions: z
|
||||
.object({
|
||||
environment: z.string().optional(),
|
||||
secretPath: z
|
||||
.object({
|
||||
$glob: z.string().min(1)
|
||||
})
|
||||
.optional()
|
||||
})
|
||||
.optional()
|
||||
});
|
||||
|
||||
const unpackPermissions = (permissions: unknown) =>
|
||||
UnpackedPermissionSchema.array().parse(
|
||||
unpackRules((permissions || []) as PackRule<RawRuleOf<MongoAbility<ProjectPermissionSet>>>[])
|
||||
);
|
||||
|
||||
export const identityProjectAdditionalPrivilegeServiceFactory = ({
|
||||
identityProjectAdditionalPrivilegeDAL,
|
||||
identityProjectDAL,
|
||||
@ -86,7 +109,10 @@ export const identityProjectAdditionalPrivilegeServiceFactory = ({
|
||||
slug,
|
||||
permissions: customPermission
|
||||
});
|
||||
return additionalPrivilege;
|
||||
return {
|
||||
...additionalPrivilege,
|
||||
permissions: unpackPermissions(additionalPrivilege.permissions)
|
||||
};
|
||||
}
|
||||
|
||||
const relativeTempAllocatedTimeInMs = ms(dto.temporaryRange);
|
||||
@ -100,7 +126,10 @@ export const identityProjectAdditionalPrivilegeServiceFactory = ({
|
||||
temporaryAccessStartTime: new Date(dto.temporaryAccessStartTime),
|
||||
temporaryAccessEndTime: new Date(new Date(dto.temporaryAccessStartTime).getTime() + relativeTempAllocatedTimeInMs)
|
||||
});
|
||||
return additionalPrivilege;
|
||||
return {
|
||||
...additionalPrivilege,
|
||||
permissions: unpackPermissions(additionalPrivilege.permissions)
|
||||
};
|
||||
};
|
||||
|
||||
const updateBySlug = async ({
|
||||
@ -163,7 +192,11 @@ export const identityProjectAdditionalPrivilegeServiceFactory = ({
|
||||
temporaryAccessStartTime: new Date(temporaryAccessStartTime || ""),
|
||||
temporaryAccessEndTime: new Date(new Date(temporaryAccessStartTime || "").getTime() + ms(temporaryRange || ""))
|
||||
});
|
||||
return additionalPrivilege;
|
||||
return {
|
||||
...additionalPrivilege,
|
||||
|
||||
permissions: unpackPermissions(additionalPrivilege.permissions)
|
||||
};
|
||||
}
|
||||
|
||||
const additionalPrivilege = await identityProjectAdditionalPrivilegeDAL.updateById(identityPrivilege.id, {
|
||||
@ -174,7 +207,11 @@ export const identityProjectAdditionalPrivilegeServiceFactory = ({
|
||||
temporaryRange: null,
|
||||
temporaryMode: null
|
||||
});
|
||||
return additionalPrivilege;
|
||||
return {
|
||||
...additionalPrivilege,
|
||||
|
||||
permissions: unpackPermissions(additionalPrivilege.permissions)
|
||||
};
|
||||
};
|
||||
|
||||
const deleteBySlug = async ({
|
||||
@ -220,7 +257,11 @@ export const identityProjectAdditionalPrivilegeServiceFactory = ({
|
||||
if (!identityPrivilege) throw new BadRequestError({ message: "Identity additional privilege not found" });
|
||||
|
||||
const deletedPrivilege = await identityProjectAdditionalPrivilegeDAL.deleteById(identityPrivilege.id);
|
||||
return deletedPrivilege;
|
||||
return {
|
||||
...deletedPrivilege,
|
||||
|
||||
permissions: unpackPermissions(deletedPrivilege.permissions)
|
||||
};
|
||||
};
|
||||
|
||||
const getPrivilegeDetailsBySlug = async ({
|
||||
@ -254,7 +295,10 @@ export const identityProjectAdditionalPrivilegeServiceFactory = ({
|
||||
});
|
||||
if (!identityPrivilege) throw new BadRequestError({ message: "Identity additional privilege not found" });
|
||||
|
||||
return identityPrivilege;
|
||||
return {
|
||||
...identityPrivilege,
|
||||
permissions: unpackPermissions(identityPrivilege.permissions)
|
||||
};
|
||||
};
|
||||
|
||||
const listIdentityProjectPrivileges = async ({
|
||||
@ -284,7 +328,11 @@ export const identityProjectAdditionalPrivilegeServiceFactory = ({
|
||||
const identityPrivileges = await identityProjectAdditionalPrivilegeDAL.find({
|
||||
projectMembershipId: identityProjectMembership.id
|
||||
});
|
||||
return identityPrivileges;
|
||||
return identityPrivileges.map((el) => ({
|
||||
...el,
|
||||
|
||||
permissions: unpackPermissions(el.permissions)
|
||||
}));
|
||||
};
|
||||
|
||||
return {
|
||||
|
@ -1,7 +1,17 @@
|
||||
import { ForbiddenError } from "@casl/ability";
|
||||
import jwt from "jsonwebtoken";
|
||||
|
||||
import { OrgMembershipRole, OrgMembershipStatus, SecretKeyEncoding, TLdapConfigsUpdate } from "@app/db/schemas";
|
||||
import {
|
||||
OrgMembershipRole,
|
||||
OrgMembershipStatus,
|
||||
SecretKeyEncoding,
|
||||
TableName,
|
||||
TLdapConfigsUpdate,
|
||||
TUsers
|
||||
} from "@app/db/schemas";
|
||||
import { TGroupDALFactory } from "@app/ee/services/group/group-dal";
|
||||
import { addUsersToGroupByUserIds, removeUsersFromGroupByUserIds } from "@app/ee/services/group/group-fns";
|
||||
import { TUserGroupMembershipDALFactory } from "@app/ee/services/group/user-group-membership-dal";
|
||||
import { getConfig } from "@app/lib/config/env";
|
||||
import {
|
||||
decryptSymmetric,
|
||||
@ -13,26 +23,58 @@ import {
|
||||
} from "@app/lib/crypto/encryption";
|
||||
import { BadRequestError } from "@app/lib/errors";
|
||||
import { AuthMethod, AuthTokenType } from "@app/services/auth/auth-type";
|
||||
import { TGroupProjectDALFactory } from "@app/services/group-project/group-project-dal";
|
||||
import { TOrgBotDALFactory } from "@app/services/org/org-bot-dal";
|
||||
import { TOrgDALFactory } from "@app/services/org/org-dal";
|
||||
import { TOrgMembershipDALFactory } from "@app/services/org-membership/org-membership-dal";
|
||||
import { TProjectDALFactory } from "@app/services/project/project-dal";
|
||||
import { TProjectBotDALFactory } from "@app/services/project-bot/project-bot-dal";
|
||||
import { TProjectKeyDALFactory } from "@app/services/project-key/project-key-dal";
|
||||
import { getServerCfg } from "@app/services/super-admin/super-admin-service";
|
||||
import { TUserDALFactory } from "@app/services/user/user-dal";
|
||||
import { normalizeUsername } from "@app/services/user/user-fns";
|
||||
import { TUserAliasDALFactory } from "@app/services/user-alias/user-alias-dal";
|
||||
import { UserAliasType } from "@app/services/user-alias/user-alias-types";
|
||||
|
||||
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, TGetLdapCfgDTO, TLdapLoginDTO, TUpdateLdapCfgDTO } from "./ldap-config-types";
|
||||
import {
|
||||
TCreateLdapCfgDTO,
|
||||
TCreateLdapGroupMapDTO,
|
||||
TDeleteLdapGroupMapDTO,
|
||||
TGetLdapCfgDTO,
|
||||
TGetLdapGroupMapsDTO,
|
||||
TLdapLoginDTO,
|
||||
TTestLdapConnectionDTO,
|
||||
TUpdateLdapCfgDTO
|
||||
} from "./ldap-config-types";
|
||||
import { testLDAPConfig } from "./ldap-fns";
|
||||
import { TLdapGroupMapDALFactory } from "./ldap-group-map-dal";
|
||||
|
||||
type TLdapConfigServiceFactoryDep = {
|
||||
ldapConfigDAL: TLdapConfigDALFactory;
|
||||
ldapConfigDAL: Pick<TLdapConfigDALFactory, "create" | "update" | "findOne">;
|
||||
ldapGroupMapDAL: Pick<TLdapGroupMapDALFactory, "find" | "create" | "delete" | "findLdapGroupMapsByLdapConfigId">;
|
||||
orgMembershipDAL: Pick<TOrgMembershipDALFactory, "create">;
|
||||
orgDAL: Pick<
|
||||
TOrgDALFactory,
|
||||
"createMembership" | "updateMembershipById" | "findMembership" | "findOrgById" | "findOne" | "updateById"
|
||||
>;
|
||||
orgBotDAL: Pick<TOrgBotDALFactory, "findOne" | "create" | "transaction">;
|
||||
userDAL: Pick<TUserDALFactory, "create" | "findOne" | "transaction" | "updateById">;
|
||||
groupDAL: Pick<TGroupDALFactory, "find" | "findOne">;
|
||||
groupProjectDAL: Pick<TGroupProjectDALFactory, "find">;
|
||||
projectKeyDAL: Pick<TProjectKeyDALFactory, "find" | "findLatestProjectKey" | "insertMany" | "delete">;
|
||||
projectDAL: Pick<TProjectDALFactory, "findProjectGhostUser">;
|
||||
projectBotDAL: Pick<TProjectBotDALFactory, "findOne">;
|
||||
userGroupMembershipDAL: Pick<
|
||||
TUserGroupMembershipDALFactory,
|
||||
"find" | "transaction" | "insertMany" | "filterProjectsByUserMembership" | "delete"
|
||||
>;
|
||||
userDAL: Pick<
|
||||
TUserDALFactory,
|
||||
"create" | "findOne" | "transaction" | "updateById" | "findUserEncKeyByUserIdsBatch" | "find"
|
||||
>;
|
||||
userAliasDAL: Pick<TUserAliasDALFactory, "create" | "findOne">;
|
||||
permissionService: Pick<TPermissionServiceFactory, "getOrgPermission">;
|
||||
licenseService: Pick<TLicenseServiceFactory, "getPlan">;
|
||||
@ -42,8 +84,16 @@ export type TLdapConfigServiceFactory = ReturnType<typeof ldapConfigServiceFacto
|
||||
|
||||
export const ldapConfigServiceFactory = ({
|
||||
ldapConfigDAL,
|
||||
ldapGroupMapDAL,
|
||||
orgDAL,
|
||||
orgMembershipDAL,
|
||||
orgBotDAL,
|
||||
groupDAL,
|
||||
groupProjectDAL,
|
||||
projectKeyDAL,
|
||||
projectDAL,
|
||||
projectBotDAL,
|
||||
userGroupMembershipDAL,
|
||||
userDAL,
|
||||
userAliasDAL,
|
||||
permissionService,
|
||||
@ -60,6 +110,9 @@ export const ldapConfigServiceFactory = ({
|
||||
bindDN,
|
||||
bindPass,
|
||||
searchBase,
|
||||
searchFilter,
|
||||
groupSearchBase,
|
||||
groupSearchFilter,
|
||||
caCert
|
||||
}: TCreateLdapCfgDTO) => {
|
||||
const { permission } = await permissionService.getOrgPermission(actor, actorId, orgId, actorAuthMethod, actorOrgId);
|
||||
@ -135,6 +188,9 @@ export const ldapConfigServiceFactory = ({
|
||||
bindPassIV,
|
||||
bindPassTag,
|
||||
searchBase,
|
||||
searchFilter,
|
||||
groupSearchBase,
|
||||
groupSearchFilter,
|
||||
encryptedCACert,
|
||||
caCertIV,
|
||||
caCertTag
|
||||
@ -154,6 +210,9 @@ export const ldapConfigServiceFactory = ({
|
||||
bindDN,
|
||||
bindPass,
|
||||
searchBase,
|
||||
searchFilter,
|
||||
groupSearchBase,
|
||||
groupSearchFilter,
|
||||
caCert
|
||||
}: TUpdateLdapCfgDTO) => {
|
||||
const { permission } = await permissionService.getOrgPermission(actor, actorId, orgId, actorAuthMethod, actorOrgId);
|
||||
@ -169,7 +228,10 @@ export const ldapConfigServiceFactory = ({
|
||||
const updateQuery: TLdapConfigsUpdate = {
|
||||
isActive,
|
||||
url,
|
||||
searchBase
|
||||
searchBase,
|
||||
searchFilter,
|
||||
groupSearchBase,
|
||||
groupSearchFilter
|
||||
};
|
||||
|
||||
const orgBot = await orgBotDAL.findOne({ orgId });
|
||||
@ -271,6 +333,9 @@ export const ldapConfigServiceFactory = ({
|
||||
bindDN,
|
||||
bindPass,
|
||||
searchBase: ldapConfig.searchBase,
|
||||
searchFilter: ldapConfig.searchFilter,
|
||||
groupSearchBase: ldapConfig.groupSearchBase,
|
||||
groupSearchFilter: ldapConfig.groupSearchFilter,
|
||||
caCert
|
||||
};
|
||||
};
|
||||
@ -304,8 +369,8 @@ export const ldapConfigServiceFactory = ({
|
||||
bindDN: ldapConfig.bindDN,
|
||||
bindCredentials: ldapConfig.bindPass,
|
||||
searchBase: ldapConfig.searchBase,
|
||||
searchFilter: "(uid={{username}})",
|
||||
searchAttributes: ["uid", "uidNumber", "givenName", "sn", "mail"],
|
||||
searchFilter: ldapConfig.searchFilter || "(uid={{username}})",
|
||||
// searchAttributes: ["uid", "uidNumber", "givenName", "sn", "mail"],
|
||||
...(ldapConfig.caCert !== ""
|
||||
? {
|
||||
tlsOptions: {
|
||||
@ -320,12 +385,23 @@ export const ldapConfigServiceFactory = ({
|
||||
return { opts, ldapConfig };
|
||||
};
|
||||
|
||||
const ldapLogin = async ({ externalId, username, firstName, lastName, emails, orgId, relayState }: TLdapLoginDTO) => {
|
||||
const ldapLogin = async ({
|
||||
ldapConfigId,
|
||||
externalId,
|
||||
username,
|
||||
firstName,
|
||||
lastName,
|
||||
email,
|
||||
groups,
|
||||
orgId,
|
||||
relayState
|
||||
}: TLdapLoginDTO) => {
|
||||
const appCfg = getConfig();
|
||||
const serverCfg = await getServerCfg();
|
||||
let userAlias = await userAliasDAL.findOne({
|
||||
externalId,
|
||||
orgId,
|
||||
aliasType: AuthMethod.LDAP
|
||||
aliasType: UserAliasType.LDAP
|
||||
});
|
||||
|
||||
const organization = await orgDAL.findOrgById(orgId);
|
||||
@ -333,7 +409,13 @@ export const ldapConfigServiceFactory = ({
|
||||
|
||||
if (userAlias) {
|
||||
await userDAL.transaction(async (tx) => {
|
||||
const [orgMembership] = await orgDAL.findMembership({ userId: userAlias.userId }, { tx });
|
||||
const [orgMembership] = await orgDAL.findMembership(
|
||||
{
|
||||
[`${TableName.OrgMembership}.userId` as "userId"]: userAlias.userId,
|
||||
[`${TableName.OrgMembership}.orgId` as "id"]: orgId
|
||||
},
|
||||
{ tx }
|
||||
);
|
||||
if (!orgMembership) {
|
||||
await orgDAL.createMembership(
|
||||
{
|
||||
@ -356,45 +438,157 @@ export const ldapConfigServiceFactory = ({
|
||||
});
|
||||
} else {
|
||||
userAlias = await userDAL.transaction(async (tx) => {
|
||||
const uniqueUsername = await normalizeUsername(username, userDAL);
|
||||
const newUser = await userDAL.create(
|
||||
{
|
||||
username: uniqueUsername,
|
||||
email: emails[0],
|
||||
firstName,
|
||||
lastName,
|
||||
authMethods: [AuthMethod.LDAP],
|
||||
isGhost: false
|
||||
},
|
||||
tx
|
||||
);
|
||||
let newUser: TUsers | undefined;
|
||||
if (serverCfg.trustSamlEmails) {
|
||||
newUser = await userDAL.findOne(
|
||||
{
|
||||
email,
|
||||
isEmailVerified: true
|
||||
},
|
||||
tx
|
||||
);
|
||||
}
|
||||
|
||||
if (!newUser) {
|
||||
const uniqueUsername = await normalizeUsername(username, userDAL);
|
||||
newUser = await userDAL.create(
|
||||
{
|
||||
username: serverCfg.trustLdapEmails ? email : uniqueUsername,
|
||||
email,
|
||||
isEmailVerified: serverCfg.trustLdapEmails,
|
||||
firstName,
|
||||
lastName,
|
||||
authMethods: [],
|
||||
isGhost: false
|
||||
},
|
||||
tx
|
||||
);
|
||||
}
|
||||
|
||||
const newUserAlias = await userAliasDAL.create(
|
||||
{
|
||||
userId: newUser.id,
|
||||
username,
|
||||
aliasType: AuthMethod.LDAP,
|
||||
aliasType: UserAliasType.LDAP,
|
||||
externalId,
|
||||
emails,
|
||||
emails: [email],
|
||||
orgId
|
||||
},
|
||||
tx
|
||||
);
|
||||
|
||||
await orgDAL.createMembership(
|
||||
const [orgMembership] = await orgDAL.findMembership(
|
||||
{
|
||||
userId: newUser.id,
|
||||
orgId,
|
||||
role: OrgMembershipRole.Member,
|
||||
status: OrgMembershipStatus.Invited
|
||||
[`${TableName.OrgMembership}.userId` as "userId"]: newUser.id,
|
||||
[`${TableName.OrgMembership}.orgId` as "id"]: orgId
|
||||
},
|
||||
tx
|
||||
{ tx }
|
||||
);
|
||||
|
||||
if (!orgMembership) {
|
||||
await orgMembershipDAL.create(
|
||||
{
|
||||
userId: userAlias.userId,
|
||||
inviteEmail: email,
|
||||
orgId,
|
||||
role: OrgMembershipRole.Member,
|
||||
status: newUser.isAccepted ? OrgMembershipStatus.Accepted : OrgMembershipStatus.Invited // if user is fully completed, then set status to accepted, otherwise set it to invited so we can update it later
|
||||
},
|
||||
tx
|
||||
);
|
||||
// Only update the membership to Accepted if the user account is already completed.
|
||||
} else if (orgMembership.status === OrgMembershipStatus.Invited && newUser.isAccepted) {
|
||||
await orgDAL.updateMembershipById(
|
||||
orgMembership.id,
|
||||
{
|
||||
status: OrgMembershipStatus.Accepted
|
||||
},
|
||||
tx
|
||||
);
|
||||
}
|
||||
|
||||
return newUserAlias;
|
||||
});
|
||||
}
|
||||
|
||||
const user = await userDAL.findOne({ id: userAlias.userId });
|
||||
const user = await userDAL.transaction(async (tx) => {
|
||||
const newUser = await userDAL.findOne({ id: userAlias.userId }, tx);
|
||||
if (groups) {
|
||||
const ldapGroupIdsToBePartOf = (
|
||||
await ldapGroupMapDAL.find({
|
||||
ldapConfigId,
|
||||
$in: {
|
||||
ldapGroupCN: groups.map((group) => group.cn)
|
||||
}
|
||||
})
|
||||
).map((groupMap) => groupMap.groupId);
|
||||
|
||||
const groupsToBePartOf = await groupDAL.find({
|
||||
orgId,
|
||||
$in: {
|
||||
id: ldapGroupIdsToBePartOf
|
||||
}
|
||||
});
|
||||
const toBePartOfGroupIdsSet = new Set(groupsToBePartOf.map((groupToBePartOf) => groupToBePartOf.id));
|
||||
|
||||
const allLdapGroupMaps = await ldapGroupMapDAL.find({
|
||||
ldapConfigId
|
||||
});
|
||||
|
||||
const ldapGroupIdsCurrentlyPartOf = (
|
||||
await userGroupMembershipDAL.find({
|
||||
userId: newUser.id,
|
||||
$in: {
|
||||
groupId: allLdapGroupMaps.map((groupMap) => groupMap.groupId)
|
||||
}
|
||||
})
|
||||
).map((userGroupMembership) => userGroupMembership.groupId);
|
||||
|
||||
const userGroupMembershipGroupIdsSet = new Set(ldapGroupIdsCurrentlyPartOf);
|
||||
|
||||
for await (const group of groupsToBePartOf) {
|
||||
if (!userGroupMembershipGroupIdsSet.has(group.id)) {
|
||||
// add user to group that they should be part of
|
||||
await addUsersToGroupByUserIds({
|
||||
group,
|
||||
userIds: [newUser.id],
|
||||
userDAL,
|
||||
userGroupMembershipDAL,
|
||||
orgDAL,
|
||||
groupProjectDAL,
|
||||
projectKeyDAL,
|
||||
projectDAL,
|
||||
projectBotDAL,
|
||||
tx
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
const groupsCurrentlyPartOf = await groupDAL.find({
|
||||
orgId,
|
||||
$in: {
|
||||
id: ldapGroupIdsCurrentlyPartOf
|
||||
}
|
||||
});
|
||||
|
||||
for await (const group of groupsCurrentlyPartOf) {
|
||||
if (!toBePartOfGroupIdsSet.has(group.id)) {
|
||||
// remove user from group that they should no longer be part of
|
||||
await removeUsersFromGroupByUserIds({
|
||||
group,
|
||||
userIds: [newUser.id],
|
||||
userDAL,
|
||||
userGroupMembershipDAL,
|
||||
groupProjectDAL,
|
||||
projectKeyDAL,
|
||||
tx
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return newUser;
|
||||
});
|
||||
|
||||
const isUserCompleted = Boolean(user.isAccepted);
|
||||
|
||||
@ -403,11 +597,14 @@ export const ldapConfigServiceFactory = ({
|
||||
authTokenType: AuthTokenType.PROVIDER_TOKEN,
|
||||
userId: user.id,
|
||||
username: user.username,
|
||||
...(user.email && { email: user.email, isEmailVerified: user.isEmailVerified }),
|
||||
firstName,
|
||||
lastName,
|
||||
organizationName: organization.name,
|
||||
organizationId: organization.id,
|
||||
organizationSlug: organization.slug,
|
||||
authMethod: AuthMethod.LDAP,
|
||||
authType: UserAliasType.LDAP,
|
||||
isUserCompleted,
|
||||
...(relayState
|
||||
? {
|
||||
@ -424,6 +621,116 @@ export const ldapConfigServiceFactory = ({
|
||||
return { isUserCompleted, providerAuthToken };
|
||||
};
|
||||
|
||||
const getLdapGroupMaps = async ({
|
||||
ldapConfigId,
|
||||
actor,
|
||||
actorId,
|
||||
orgId,
|
||||
actorAuthMethod,
|
||||
actorOrgId
|
||||
}: TGetLdapGroupMapsDTO) => {
|
||||
const { permission } = await permissionService.getOrgPermission(actor, actorId, orgId, actorAuthMethod, actorOrgId);
|
||||
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Read, OrgPermissionSubjects.Ldap);
|
||||
|
||||
const ldapConfig = await ldapConfigDAL.findOne({
|
||||
id: ldapConfigId,
|
||||
orgId
|
||||
});
|
||||
|
||||
if (!ldapConfig) throw new BadRequestError({ message: "Failed to find organization LDAP data" });
|
||||
|
||||
const groupMaps = await ldapGroupMapDAL.findLdapGroupMapsByLdapConfigId(ldapConfigId);
|
||||
|
||||
return groupMaps;
|
||||
};
|
||||
|
||||
const createLdapGroupMap = async ({
|
||||
ldapConfigId,
|
||||
ldapGroupCN,
|
||||
groupSlug,
|
||||
actor,
|
||||
actorId,
|
||||
orgId,
|
||||
actorAuthMethod,
|
||||
actorOrgId
|
||||
}: TCreateLdapGroupMapDTO) => {
|
||||
const { permission } = await permissionService.getOrgPermission(actor, actorId, orgId, actorAuthMethod, actorOrgId);
|
||||
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Create, OrgPermissionSubjects.Ldap);
|
||||
|
||||
const plan = await licenseService.getPlan(orgId);
|
||||
if (!plan.ldap)
|
||||
throw new BadRequestError({
|
||||
message: "Failed to create LDAP group map due to plan restriction. Upgrade plan to create LDAP group map."
|
||||
});
|
||||
|
||||
const ldapConfig = await ldapConfigDAL.findOne({
|
||||
id: ldapConfigId,
|
||||
orgId
|
||||
});
|
||||
if (!ldapConfig) throw new BadRequestError({ message: "Failed to find organization LDAP data" });
|
||||
|
||||
const group = await groupDAL.findOne({ slug: groupSlug, orgId });
|
||||
if (!group) throw new BadRequestError({ message: "Failed to find group" });
|
||||
|
||||
const groupMap = await ldapGroupMapDAL.create({
|
||||
ldapConfigId,
|
||||
ldapGroupCN,
|
||||
groupId: group.id
|
||||
});
|
||||
|
||||
return groupMap;
|
||||
};
|
||||
|
||||
const deleteLdapGroupMap = async ({
|
||||
ldapConfigId,
|
||||
ldapGroupMapId,
|
||||
actor,
|
||||
actorId,
|
||||
orgId,
|
||||
actorAuthMethod,
|
||||
actorOrgId
|
||||
}: TDeleteLdapGroupMapDTO) => {
|
||||
const { permission } = await permissionService.getOrgPermission(actor, actorId, orgId, actorAuthMethod, actorOrgId);
|
||||
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Delete, OrgPermissionSubjects.Ldap);
|
||||
|
||||
const plan = await licenseService.getPlan(orgId);
|
||||
if (!plan.ldap)
|
||||
throw new BadRequestError({
|
||||
message: "Failed to delete LDAP group map due to plan restriction. Upgrade plan to delete LDAP group map."
|
||||
});
|
||||
|
||||
const ldapConfig = await ldapConfigDAL.findOne({
|
||||
id: ldapConfigId,
|
||||
orgId
|
||||
});
|
||||
|
||||
if (!ldapConfig) throw new BadRequestError({ message: "Failed to find organization LDAP data" });
|
||||
|
||||
const [deletedGroupMap] = await ldapGroupMapDAL.delete({
|
||||
ldapConfigId: ldapConfig.id,
|
||||
id: ldapGroupMapId
|
||||
});
|
||||
|
||||
return deletedGroupMap;
|
||||
};
|
||||
|
||||
const testLDAPConnection = async ({ actor, actorId, orgId, actorAuthMethod, actorOrgId }: TTestLdapConnectionDTO) => {
|
||||
const { permission } = await permissionService.getOrgPermission(actor, actorId, orgId, actorAuthMethod, actorOrgId);
|
||||
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Create, OrgPermissionSubjects.Ldap);
|
||||
|
||||
const plan = await licenseService.getPlan(orgId);
|
||||
if (!plan.ldap)
|
||||
throw new BadRequestError({
|
||||
message: "Failed to test LDAP connection due to plan restriction. Upgrade plan to test the LDAP connection."
|
||||
});
|
||||
|
||||
const ldapConfig = await getLdapCfg({
|
||||
orgId
|
||||
});
|
||||
|
||||
return testLDAPConfig(ldapConfig);
|
||||
};
|
||||
|
||||
return {
|
||||
createLdapCfg,
|
||||
updateLdapCfg,
|
||||
@ -431,6 +738,10 @@ export const ldapConfigServiceFactory = ({
|
||||
getLdapCfg,
|
||||
// getLdapPassportOpts,
|
||||
ldapLogin,
|
||||
bootLdap
|
||||
bootLdap,
|
||||
getLdapGroupMaps,
|
||||
createLdapGroupMap,
|
||||
deleteLdapGroupMap,
|
||||
testLDAPConnection
|
||||
};
|
||||
};
|
||||
|
@ -1,5 +1,18 @@
|
||||
import { TOrgPermission } from "@app/lib/types";
|
||||
|
||||
export type TLDAPConfig = {
|
||||
id: string;
|
||||
organization: string;
|
||||
isActive: boolean;
|
||||
url: string;
|
||||
bindDN: string;
|
||||
bindPass: string;
|
||||
searchBase: string;
|
||||
groupSearchBase: string;
|
||||
groupSearchFilter: string;
|
||||
caCert: string;
|
||||
};
|
||||
|
||||
export type TCreateLdapCfgDTO = {
|
||||
orgId: string;
|
||||
isActive: boolean;
|
||||
@ -7,6 +20,9 @@ export type TCreateLdapCfgDTO = {
|
||||
bindDN: string;
|
||||
bindPass: string;
|
||||
searchBase: string;
|
||||
searchFilter: string;
|
||||
groupSearchBase: string;
|
||||
groupSearchFilter: string;
|
||||
caCert: string;
|
||||
} & TOrgPermission;
|
||||
|
||||
@ -18,6 +34,9 @@ export type TUpdateLdapCfgDTO = {
|
||||
bindDN: string;
|
||||
bindPass: string;
|
||||
searchBase: string;
|
||||
searchFilter: string;
|
||||
groupSearchBase: string;
|
||||
groupSearchFilter: string;
|
||||
caCert: string;
|
||||
}> &
|
||||
TOrgPermission;
|
||||
@ -27,11 +46,35 @@ export type TGetLdapCfgDTO = {
|
||||
} & TOrgPermission;
|
||||
|
||||
export type TLdapLoginDTO = {
|
||||
ldapConfigId: string;
|
||||
externalId: string;
|
||||
username: string;
|
||||
firstName: string;
|
||||
lastName: string;
|
||||
emails: string[];
|
||||
email: string;
|
||||
orgId: string;
|
||||
groups?: {
|
||||
dn: string;
|
||||
cn: string;
|
||||
}[];
|
||||
relayState?: string;
|
||||
};
|
||||
|
||||
export type TGetLdapGroupMapsDTO = {
|
||||
ldapConfigId: string;
|
||||
} & TOrgPermission;
|
||||
|
||||
export type TCreateLdapGroupMapDTO = {
|
||||
ldapConfigId: string;
|
||||
ldapGroupCN: string;
|
||||
groupSlug: string;
|
||||
} & TOrgPermission;
|
||||
|
||||
export type TDeleteLdapGroupMapDTO = {
|
||||
ldapConfigId: string;
|
||||
ldapGroupMapId: string;
|
||||
} & TOrgPermission;
|
||||
|
||||
export type TTestLdapConnectionDTO = {
|
||||
ldapConfigId: string;
|
||||
} & TOrgPermission;
|
||||
|
119
backend/src/ee/services/ldap-config/ldap-fns.ts
Normal file
119
backend/src/ee/services/ldap-config/ldap-fns.ts
Normal file
@ -0,0 +1,119 @@
|
||||
import ldapjs from "ldapjs";
|
||||
|
||||
import { logger } from "@app/lib/logger";
|
||||
|
||||
import { TLDAPConfig } from "./ldap-config-types";
|
||||
|
||||
export const isValidLdapFilter = (filter: string) => {
|
||||
try {
|
||||
ldapjs.parseFilter(filter);
|
||||
return true;
|
||||
} catch (error) {
|
||||
logger.error("Invalid LDAP filter");
|
||||
logger.error(error);
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Test the LDAP configuration by attempting to bind to the LDAP server
|
||||
* @param ldapConfig - The LDAP configuration to test
|
||||
* @returns {Boolean} isConnected - Whether or not the connection was successful
|
||||
*/
|
||||
export const testLDAPConfig = async (ldapConfig: TLDAPConfig): Promise<boolean> => {
|
||||
return new Promise((resolve) => {
|
||||
const ldapClient = ldapjs.createClient({
|
||||
url: ldapConfig.url,
|
||||
bindDN: ldapConfig.bindDN,
|
||||
bindCredentials: ldapConfig.bindPass,
|
||||
...(ldapConfig.caCert !== ""
|
||||
? {
|
||||
tlsOptions: {
|
||||
ca: [ldapConfig.caCert]
|
||||
}
|
||||
}
|
||||
: {})
|
||||
});
|
||||
|
||||
ldapClient.on("error", (err) => {
|
||||
logger.error("LDAP client error:", err);
|
||||
logger.error(err);
|
||||
resolve(false);
|
||||
});
|
||||
|
||||
ldapClient.bind(ldapConfig.bindDN, ldapConfig.bindPass, (err) => {
|
||||
if (err) {
|
||||
logger.error("Error binding to LDAP");
|
||||
logger.error(err);
|
||||
ldapClient.unbind();
|
||||
resolve(false);
|
||||
} else {
|
||||
logger.info("Successfully connected and bound to LDAP.");
|
||||
ldapClient.unbind();
|
||||
resolve(true);
|
||||
}
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Search for groups in the LDAP server
|
||||
* @param ldapConfig - The LDAP configuration to use
|
||||
* @param filter - The filter to use when searching for groups
|
||||
* @param base - The base to search from
|
||||
* @returns
|
||||
*/
|
||||
export const searchGroups = async (
|
||||
ldapConfig: TLDAPConfig,
|
||||
filter: string,
|
||||
base: string
|
||||
): Promise<{ dn: string; cn: string }[]> => {
|
||||
return new Promise((resolve, reject) => {
|
||||
const ldapClient = ldapjs.createClient({
|
||||
url: ldapConfig.url,
|
||||
bindDN: ldapConfig.bindDN,
|
||||
bindCredentials: ldapConfig.bindPass,
|
||||
...(ldapConfig.caCert !== ""
|
||||
? {
|
||||
tlsOptions: {
|
||||
ca: [ldapConfig.caCert]
|
||||
}
|
||||
}
|
||||
: {})
|
||||
});
|
||||
|
||||
ldapClient.search(
|
||||
base,
|
||||
{
|
||||
filter,
|
||||
scope: "sub"
|
||||
},
|
||||
(err, res) => {
|
||||
if (err) {
|
||||
ldapClient.unbind();
|
||||
return reject(err);
|
||||
}
|
||||
|
||||
const groups: { dn: string; cn: string }[] = [];
|
||||
|
||||
res.on("searchEntry", (entry) => {
|
||||
const dn = entry.dn.toString();
|
||||
const regex = /cn=([^,]+)/;
|
||||
const match = dn.match(regex);
|
||||
// parse the cn from the dn
|
||||
const cn = (match && match[1]) as string;
|
||||
|
||||
groups.push({ dn, cn });
|
||||
});
|
||||
res.on("error", (error) => {
|
||||
ldapClient.unbind();
|
||||
reject(error);
|
||||
});
|
||||
res.on("end", () => {
|
||||
ldapClient.unbind();
|
||||
resolve(groups);
|
||||
});
|
||||
}
|
||||
);
|
||||
});
|
||||
};
|
41
backend/src/ee/services/ldap-config/ldap-group-map-dal.ts
Normal file
41
backend/src/ee/services/ldap-config/ldap-group-map-dal.ts
Normal file
@ -0,0 +1,41 @@
|
||||
import { TDbClient } from "@app/db";
|
||||
import { TableName } from "@app/db/schemas";
|
||||
import { DatabaseError } from "@app/lib/errors";
|
||||
import { ormify, selectAllTableCols } from "@app/lib/knex";
|
||||
|
||||
export type TLdapGroupMapDALFactory = ReturnType<typeof ldapGroupMapDALFactory>;
|
||||
|
||||
export const ldapGroupMapDALFactory = (db: TDbClient) => {
|
||||
const ldapGroupMapOrm = ormify(db, TableName.LdapGroupMap);
|
||||
|
||||
const findLdapGroupMapsByLdapConfigId = async (ldapConfigId: string) => {
|
||||
try {
|
||||
const docs = await db(TableName.LdapGroupMap)
|
||||
.where(`${TableName.LdapGroupMap}.ldapConfigId`, ldapConfigId)
|
||||
.join(TableName.Groups, `${TableName.LdapGroupMap}.groupId`, `${TableName.Groups}.id`)
|
||||
.select(selectAllTableCols(TableName.LdapGroupMap))
|
||||
.select(
|
||||
db.ref("id").withSchema(TableName.Groups).as("groupId"),
|
||||
db.ref("name").withSchema(TableName.Groups).as("groupName"),
|
||||
db.ref("slug").withSchema(TableName.Groups).as("groupSlug")
|
||||
);
|
||||
|
||||
return docs.map((doc) => {
|
||||
return {
|
||||
id: doc.id,
|
||||
ldapConfigId: doc.ldapConfigId,
|
||||
ldapGroupCN: doc.ldapGroupCN,
|
||||
group: {
|
||||
id: doc.groupId,
|
||||
name: doc.groupName,
|
||||
slug: doc.groupSlug
|
||||
}
|
||||
};
|
||||
});
|
||||
} catch (error) {
|
||||
throw new DatabaseError({ error, name: "findGroupMaps" });
|
||||
}
|
||||
};
|
||||
|
||||
return { ...ldapGroupMapOrm, findLdapGroupMapsByLdapConfigId };
|
||||
};
|
@ -24,6 +24,8 @@ export const getDefaultOnPremFeatures = (): TFeatureSet => ({
|
||||
customAlerts: false,
|
||||
auditLogs: false,
|
||||
auditLogsRetentionDays: 0,
|
||||
auditLogStreams: false,
|
||||
auditLogStreamLimit: 3,
|
||||
samlSSO: false,
|
||||
scim: false,
|
||||
ldap: false,
|
||||
|
@ -121,8 +121,8 @@ export const licenseServiceFactory = ({
|
||||
|
||||
if (isValidOfflineLicense) {
|
||||
onPremFeatures = contents.license.features;
|
||||
instanceType = InstanceType.EnterpriseOnPrem;
|
||||
logger.info(`Instance type: ${InstanceType.EnterpriseOnPrem}`);
|
||||
instanceType = InstanceType.EnterpriseOnPremOffline;
|
||||
logger.info(`Instance type: ${InstanceType.EnterpriseOnPremOffline}`);
|
||||
isValidLicense = true;
|
||||
return;
|
||||
}
|
||||
|
@ -3,6 +3,7 @@ import { TOrgPermission } from "@app/lib/types";
|
||||
export enum InstanceType {
|
||||
OnPrem = "self-hosted",
|
||||
EnterpriseOnPrem = "enterprise-self-hosted",
|
||||
EnterpriseOnPremOffline = "enterprise-self-hosted-offline",
|
||||
Cloud = "cloud"
|
||||
}
|
||||
|
||||
@ -40,6 +41,8 @@ export type TFeatureSet = {
|
||||
customAlerts: false;
|
||||
auditLogs: false;
|
||||
auditLogsRetentionDays: 0;
|
||||
auditLogStreams: false;
|
||||
auditLogStreamLimit: 3;
|
||||
samlSSO: false;
|
||||
scim: false;
|
||||
ldap: false;
|
||||
|
@ -72,7 +72,6 @@ export const permissionDALFactory = (db: TDbClient) => {
|
||||
.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),
|
||||
@ -105,7 +104,6 @@ export const permissionDALFactory = (db: TDbClient) => {
|
||||
.select(selectAllTableCols(TableName.ProjectUserMembershipRole))
|
||||
.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("createdAt").withSchema(TableName.ProjectMembership).as("membershipCreatedAt"),
|
||||
db.ref("updatedAt").withSchema(TableName.ProjectMembership).as("membershipUpdatedAt"),
|
||||
db.ref("projectId").withSchema(TableName.ProjectMembership),
|
||||
@ -131,11 +129,10 @@ export const permissionDALFactory = (db: TDbClient) => {
|
||||
const permission = sqlNestRelationships({
|
||||
data: docs,
|
||||
key: "projectId",
|
||||
parentMapper: ({ orgId, orgAuthEnforced, membershipId, membershipCreatedAt, membershipUpdatedAt, role }) => ({
|
||||
parentMapper: ({ orgId, orgAuthEnforced, membershipId, membershipCreatedAt, membershipUpdatedAt }) => ({
|
||||
orgId,
|
||||
orgAuthEnforced,
|
||||
userId,
|
||||
role,
|
||||
id: membershipId,
|
||||
projectId,
|
||||
createdAt: membershipCreatedAt,
|
||||
@ -179,18 +176,10 @@ export const permissionDALFactory = (db: TDbClient) => {
|
||||
? sqlNestRelationships({
|
||||
data: groupDocs,
|
||||
key: "projectId",
|
||||
parentMapper: ({
|
||||
orgId,
|
||||
orgAuthEnforced,
|
||||
membershipId,
|
||||
membershipCreatedAt,
|
||||
membershipUpdatedAt,
|
||||
role
|
||||
}) => ({
|
||||
parentMapper: ({ orgId, orgAuthEnforced, membershipId, membershipCreatedAt, membershipUpdatedAt }) => ({
|
||||
orgId,
|
||||
orgAuthEnforced,
|
||||
userId,
|
||||
role,
|
||||
id: membershipId,
|
||||
projectId,
|
||||
createdAt: membershipCreatedAt,
|
||||
@ -270,7 +259,6 @@ export const permissionDALFactory = (db: TDbClient) => {
|
||||
.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"),
|
||||
@ -299,11 +287,10 @@ export const permissionDALFactory = (db: TDbClient) => {
|
||||
const permission = sqlNestRelationships({
|
||||
data: docs,
|
||||
key: "membershipId",
|
||||
parentMapper: ({ membershipId, membershipCreatedAt, membershipUpdatedAt, oldRoleField, orgId }) => ({
|
||||
parentMapper: ({ membershipId, membershipCreatedAt, membershipUpdatedAt, orgId }) => ({
|
||||
id: membershipId,
|
||||
identityId,
|
||||
projectId,
|
||||
role: oldRoleField,
|
||||
createdAt: membershipCreatedAt,
|
||||
updatedAt: membershipUpdatedAt,
|
||||
orgId,
|
||||
|
@ -7,7 +7,8 @@ import {
|
||||
SecretKeyEncoding,
|
||||
TableName,
|
||||
TSamlConfigs,
|
||||
TSamlConfigsUpdate
|
||||
TSamlConfigsUpdate,
|
||||
TUsers
|
||||
} from "@app/db/schemas";
|
||||
import { getConfig } from "@app/lib/config/env";
|
||||
import {
|
||||
@ -19,10 +20,18 @@ import {
|
||||
infisicalSymmetricEncypt
|
||||
} from "@app/lib/crypto/encryption";
|
||||
import { BadRequestError } from "@app/lib/errors";
|
||||
import { AuthMethod, AuthTokenType } from "@app/services/auth/auth-type";
|
||||
import { AuthTokenType } from "@app/services/auth/auth-type";
|
||||
import { TAuthTokenServiceFactory } from "@app/services/auth-token/auth-token-service";
|
||||
import { TokenType } from "@app/services/auth-token/auth-token-types";
|
||||
import { TOrgBotDALFactory } from "@app/services/org/org-bot-dal";
|
||||
import { TOrgDALFactory } from "@app/services/org/org-dal";
|
||||
import { TOrgMembershipDALFactory } from "@app/services/org-membership/org-membership-dal";
|
||||
import { SmtpTemplates, TSmtpService } from "@app/services/smtp/smtp-service";
|
||||
import { getServerCfg } from "@app/services/super-admin/super-admin-service";
|
||||
import { TUserDALFactory } from "@app/services/user/user-dal";
|
||||
import { normalizeUsername } from "@app/services/user/user-fns";
|
||||
import { TUserAliasDALFactory } from "@app/services/user-alias/user-alias-dal";
|
||||
import { UserAliasType } from "@app/services/user-alias/user-alias-types";
|
||||
|
||||
import { TLicenseServiceFactory } from "../license/license-service";
|
||||
import { OrgPermissionActions, OrgPermissionSubjects } from "../permission/org-permission";
|
||||
@ -31,15 +40,19 @@ import { TSamlConfigDALFactory } from "./saml-config-dal";
|
||||
import { TCreateSamlCfgDTO, TGetSamlCfgDTO, TSamlLoginDTO, TUpdateSamlCfgDTO } from "./saml-config-types";
|
||||
|
||||
type TSamlConfigServiceFactoryDep = {
|
||||
samlConfigDAL: TSamlConfigDALFactory;
|
||||
userDAL: Pick<TUserDALFactory, "create" | "findOne" | "transaction" | "updateById">;
|
||||
samlConfigDAL: Pick<TSamlConfigDALFactory, "create" | "findOne" | "update" | "findById">;
|
||||
userDAL: Pick<TUserDALFactory, "create" | "findOne" | "transaction" | "updateById" | "findById">;
|
||||
userAliasDAL: Pick<TUserAliasDALFactory, "create" | "findOne">;
|
||||
orgDAL: Pick<
|
||||
TOrgDALFactory,
|
||||
"createMembership" | "updateMembershipById" | "findMembership" | "findOrgById" | "findOne" | "updateById"
|
||||
>;
|
||||
orgMembershipDAL: Pick<TOrgMembershipDALFactory, "create">;
|
||||
orgBotDAL: Pick<TOrgBotDALFactory, "findOne" | "create" | "transaction">;
|
||||
permissionService: Pick<TPermissionServiceFactory, "getOrgPermission">;
|
||||
licenseService: Pick<TLicenseServiceFactory, "getPlan">;
|
||||
tokenService: Pick<TAuthTokenServiceFactory, "createTokenForUser">;
|
||||
smtpService: Pick<TSmtpService, "sendMail">;
|
||||
};
|
||||
|
||||
export type TSamlConfigServiceFactory = ReturnType<typeof samlConfigServiceFactory>;
|
||||
@ -48,9 +61,13 @@ export const samlConfigServiceFactory = ({
|
||||
samlConfigDAL,
|
||||
orgBotDAL,
|
||||
orgDAL,
|
||||
orgMembershipDAL,
|
||||
userDAL,
|
||||
userAliasDAL,
|
||||
permissionService,
|
||||
licenseService
|
||||
licenseService,
|
||||
tokenService,
|
||||
smtpService
|
||||
}: TSamlConfigServiceFactoryDep) => {
|
||||
const createSamlCfg = async ({
|
||||
cert,
|
||||
@ -305,7 +322,7 @@ export const samlConfigServiceFactory = ({
|
||||
};
|
||||
|
||||
const samlLogin = async ({
|
||||
username,
|
||||
externalId,
|
||||
email,
|
||||
firstName,
|
||||
lastName,
|
||||
@ -314,37 +331,40 @@ export const samlConfigServiceFactory = ({
|
||||
relayState
|
||||
}: TSamlLoginDTO) => {
|
||||
const appCfg = getConfig();
|
||||
let user = await userDAL.findOne({ username });
|
||||
const serverCfg = await getServerCfg();
|
||||
const userAlias = await userAliasDAL.findOne({
|
||||
externalId,
|
||||
orgId,
|
||||
aliasType: UserAliasType.SAML
|
||||
});
|
||||
|
||||
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) => {
|
||||
let user: TUsers;
|
||||
if (userAlias) {
|
||||
user = await userDAL.transaction(async (tx) => {
|
||||
const foundUser = await userDAL.findById(userAlias.userId, tx);
|
||||
const [orgMembership] = await orgDAL.findMembership(
|
||||
{
|
||||
userId: user.id,
|
||||
[`${TableName.OrgMembership}.userId` as "userId"]: foundUser.id,
|
||||
[`${TableName.OrgMembership}.orgId` as "id"]: orgId
|
||||
},
|
||||
{ tx }
|
||||
);
|
||||
if (!orgMembership) {
|
||||
await orgDAL.createMembership(
|
||||
await orgMembershipDAL.create(
|
||||
{
|
||||
userId: user.id,
|
||||
orgId,
|
||||
userId: userAlias.userId,
|
||||
inviteEmail: email,
|
||||
orgId,
|
||||
role: OrgMembershipRole.Member,
|
||||
status: OrgMembershipStatus.Accepted
|
||||
status: foundUser.isAccepted ? OrgMembershipStatus.Accepted : OrgMembershipStatus.Invited // if user is fully completed, then set status to accepted, otherwise set it to invited so we can update it later
|
||||
},
|
||||
tx
|
||||
);
|
||||
} else if (orgMembership.status === OrgMembershipStatus.Invited) {
|
||||
// Only update the membership to Accepted if the user account is already completed.
|
||||
} else if (orgMembership.status === OrgMembershipStatus.Invited && foundUser.isAccepted) {
|
||||
await orgDAL.updateMembershipById(
|
||||
orgMembership.id,
|
||||
{
|
||||
@ -353,40 +373,97 @@ export const samlConfigServiceFactory = ({
|
||||
tx
|
||||
);
|
||||
}
|
||||
|
||||
return foundUser;
|
||||
});
|
||||
} else {
|
||||
user = await userDAL.transaction(async (tx) => {
|
||||
const newUser = await userDAL.create(
|
||||
let newUser: TUsers | undefined;
|
||||
if (serverCfg.trustSamlEmails) {
|
||||
newUser = await userDAL.findOne(
|
||||
{
|
||||
email,
|
||||
isEmailVerified: true
|
||||
},
|
||||
tx
|
||||
);
|
||||
}
|
||||
|
||||
if (!newUser) {
|
||||
const uniqueUsername = await normalizeUsername(`${firstName ?? ""}-${lastName ?? ""}`, userDAL);
|
||||
newUser = await userDAL.create(
|
||||
{
|
||||
username: serverCfg.trustSamlEmails ? email : uniqueUsername,
|
||||
email,
|
||||
isEmailVerified: serverCfg.trustSamlEmails,
|
||||
firstName,
|
||||
lastName,
|
||||
authMethods: [],
|
||||
isGhost: false
|
||||
},
|
||||
tx
|
||||
);
|
||||
}
|
||||
|
||||
await userAliasDAL.create(
|
||||
{
|
||||
username,
|
||||
email,
|
||||
firstName,
|
||||
lastName,
|
||||
authMethods: [AuthMethod.EMAIL],
|
||||
isGhost: false
|
||||
userId: newUser.id,
|
||||
aliasType: UserAliasType.SAML,
|
||||
externalId,
|
||||
emails: email ? [email] : [],
|
||||
orgId
|
||||
},
|
||||
tx
|
||||
);
|
||||
await orgDAL.createMembership({
|
||||
inviteEmail: email,
|
||||
orgId,
|
||||
role: OrgMembershipRole.Member,
|
||||
status: OrgMembershipStatus.Invited
|
||||
});
|
||||
|
||||
const [orgMembership] = await orgDAL.findMembership(
|
||||
{
|
||||
[`${TableName.OrgMembership}.userId` as "userId"]: newUser.id,
|
||||
[`${TableName.OrgMembership}.orgId` as "id"]: orgId
|
||||
},
|
||||
{ tx }
|
||||
);
|
||||
|
||||
if (!orgMembership) {
|
||||
await orgMembershipDAL.create(
|
||||
{
|
||||
userId: newUser.id,
|
||||
inviteEmail: email,
|
||||
orgId,
|
||||
role: OrgMembershipRole.Member,
|
||||
status: newUser.isAccepted ? OrgMembershipStatus.Accepted : OrgMembershipStatus.Invited // if user is fully completed, then set status to accepted, otherwise set it to invited so we can update it later
|
||||
},
|
||||
tx
|
||||
);
|
||||
// Only update the membership to Accepted if the user account is already completed.
|
||||
} else if (orgMembership.status === OrgMembershipStatus.Invited && newUser.isAccepted) {
|
||||
await orgDAL.updateMembershipById(
|
||||
orgMembership.id,
|
||||
{
|
||||
status: OrgMembershipStatus.Accepted
|
||||
},
|
||||
tx
|
||||
);
|
||||
}
|
||||
|
||||
return newUser;
|
||||
});
|
||||
}
|
||||
|
||||
const isUserCompleted = Boolean(user.isAccepted);
|
||||
const providerAuthToken = jwt.sign(
|
||||
{
|
||||
authTokenType: AuthTokenType.PROVIDER_TOKEN,
|
||||
userId: user.id,
|
||||
username: user.username,
|
||||
...(user.email && { email: user.email, isEmailVerified: user.isEmailVerified }),
|
||||
firstName,
|
||||
lastName,
|
||||
organizationName: organization.name,
|
||||
organizationId: organization.id,
|
||||
organizationSlug: organization.slug,
|
||||
authMethod: authProvider,
|
||||
authType: UserAliasType.SAML,
|
||||
isUserCompleted,
|
||||
...(relayState
|
||||
? {
|
||||
@ -402,6 +479,22 @@ export const samlConfigServiceFactory = ({
|
||||
|
||||
await samlConfigDAL.update({ orgId }, { lastUsed: new Date() });
|
||||
|
||||
if (user.email && !user.isEmailVerified) {
|
||||
const token = await tokenService.createTokenForUser({
|
||||
type: TokenType.TOKEN_EMAIL_VERIFICATION,
|
||||
userId: user.id
|
||||
});
|
||||
|
||||
await smtpService.sendMail({
|
||||
template: SmtpTemplates.EmailVerification,
|
||||
subjectLine: "Infisical confirmation code",
|
||||
recipients: [user.email],
|
||||
substitutions: {
|
||||
code: token
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return { isUserCompleted, providerAuthToken };
|
||||
};
|
||||
|
||||
|
@ -45,8 +45,8 @@ export type TGetSamlCfgDTO =
|
||||
};
|
||||
|
||||
export type TSamlLoginDTO = {
|
||||
username: string;
|
||||
email?: string;
|
||||
externalId: string;
|
||||
email: string;
|
||||
firstName: string;
|
||||
lastName?: string;
|
||||
authProvider: string;
|
||||
|
@ -2,31 +2,31 @@ import { TListScimGroups, TListScimUsers, TScimGroup, TScimUser } from "./scim-t
|
||||
|
||||
export const buildScimUserList = ({
|
||||
scimUsers,
|
||||
offset,
|
||||
startIndex,
|
||||
limit
|
||||
}: {
|
||||
scimUsers: TScimUser[];
|
||||
offset: number;
|
||||
startIndex: number;
|
||||
limit: number;
|
||||
}): TListScimUsers => {
|
||||
return {
|
||||
Resources: scimUsers,
|
||||
itemsPerPage: limit,
|
||||
schemas: ["urn:ietf:params:scim:api:messages:2.0:ListResponse"],
|
||||
startIndex: offset,
|
||||
startIndex,
|
||||
totalResults: scimUsers.length
|
||||
};
|
||||
};
|
||||
|
||||
export const buildScimUser = ({
|
||||
userId,
|
||||
orgMembershipId,
|
||||
username,
|
||||
email,
|
||||
firstName,
|
||||
lastName,
|
||||
active
|
||||
}: {
|
||||
userId: string;
|
||||
orgMembershipId: string;
|
||||
username: string;
|
||||
email?: string | null;
|
||||
firstName: string;
|
||||
@ -35,7 +35,7 @@ export const buildScimUser = ({
|
||||
}): TScimUser => {
|
||||
const scimUser = {
|
||||
schemas: ["urn:ietf:params:scim:schemas:core:2.0:User"],
|
||||
id: userId,
|
||||
id: orgMembershipId,
|
||||
userName: username,
|
||||
displayName: `${firstName} ${lastName}`,
|
||||
name: {
|
||||
@ -65,18 +65,18 @@ export const buildScimUser = ({
|
||||
|
||||
export const buildScimGroupList = ({
|
||||
scimGroups,
|
||||
offset,
|
||||
startIndex,
|
||||
limit
|
||||
}: {
|
||||
scimGroups: TScimGroup[];
|
||||
offset: number;
|
||||
startIndex: number;
|
||||
limit: number;
|
||||
}): TListScimGroups => {
|
||||
return {
|
||||
Resources: scimGroups,
|
||||
itemsPerPage: limit,
|
||||
schemas: ["urn:ietf:params:scim:api:messages:2.0:ListResponse"],
|
||||
startIndex: offset,
|
||||
startIndex,
|
||||
totalResults: scimGroups.length
|
||||
};
|
||||
};
|
||||
|
@ -2,20 +2,30 @@ import { ForbiddenError } from "@casl/ability";
|
||||
import slugify from "@sindresorhus/slugify";
|
||||
import jwt from "jsonwebtoken";
|
||||
|
||||
import { OrgMembershipRole, OrgMembershipStatus, TableName, TGroups } from "@app/db/schemas";
|
||||
import { OrgMembershipRole, OrgMembershipStatus, TableName, TGroups, TOrgMemberships, TUsers } from "@app/db/schemas";
|
||||
import { TGroupDALFactory } from "@app/ee/services/group/group-dal";
|
||||
import { addUsersToGroupByUserIds, removeUsersFromGroupByUserIds } from "@app/ee/services/group/group-fns";
|
||||
import { TUserGroupMembershipDALFactory } from "@app/ee/services/group/user-group-membership-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 { AuthTokenType } from "@app/services/auth/auth-type";
|
||||
import { TGroupProjectDALFactory } from "@app/services/group-project/group-project-dal";
|
||||
import { TOrgDALFactory } from "@app/services/org/org-dal";
|
||||
import { deleteOrgMembership } from "@app/services/org/org-fns";
|
||||
import { deleteOrgMembershipFn } from "@app/services/org/org-fns";
|
||||
import { TOrgMembershipDALFactory } from "@app/services/org-membership/org-membership-dal";
|
||||
import { TProjectDALFactory } from "@app/services/project/project-dal";
|
||||
import { TProjectBotDALFactory } from "@app/services/project-bot/project-bot-dal";
|
||||
import { TProjectKeyDALFactory } from "@app/services/project-key/project-key-dal";
|
||||
import { TProjectMembershipDALFactory } from "@app/services/project-membership/project-membership-dal";
|
||||
import { SmtpTemplates, TSmtpService } from "@app/services/smtp/smtp-service";
|
||||
import { getServerCfg } from "@app/services/super-admin/super-admin-service";
|
||||
import { TUserDALFactory } from "@app/services/user/user-dal";
|
||||
import { normalizeUsername } from "@app/services/user/user-fns";
|
||||
import { TUserAliasDALFactory } from "@app/services/user-alias/user-alias-dal";
|
||||
import { UserAliasType } from "@app/services/user-alias/user-alias-types";
|
||||
|
||||
import { TLicenseServiceFactory } from "../license/license-service";
|
||||
import { OrgPermissionActions, OrgPermissionSubjects } from "../permission/org-permission";
|
||||
@ -42,17 +52,32 @@ import {
|
||||
|
||||
type TScimServiceFactoryDep = {
|
||||
scimDAL: Pick<TScimDALFactory, "create" | "find" | "findById" | "deleteById">;
|
||||
userDAL: Pick<TUserDALFactory, "findOne" | "create" | "transaction">;
|
||||
userDAL: Pick<
|
||||
TUserDALFactory,
|
||||
"find" | "findOne" | "create" | "transaction" | "findUserEncKeyByUserIdsBatch" | "findById"
|
||||
>;
|
||||
userAliasDAL: Pick<TUserAliasDALFactory, "findOne" | "create" | "delete">;
|
||||
orgDAL: Pick<
|
||||
TOrgDALFactory,
|
||||
"createMembership" | "findById" | "findMembership" | "deleteMembershipById" | "transaction"
|
||||
"createMembership" | "findById" | "findMembership" | "deleteMembershipById" | "transaction" | "updateMembershipById"
|
||||
>;
|
||||
projectDAL: Pick<TProjectDALFactory, "find">;
|
||||
projectMembershipDAL: Pick<TProjectMembershipDALFactory, "find" | "delete">;
|
||||
groupDAL: Pick<TGroupDALFactory, "create" | "findOne" | "findAllGroupMembers" | "update" | "delete" | "findGroups">;
|
||||
licenseService: Pick<TLicenseServiceFactory, "getPlan">;
|
||||
orgMembershipDAL: Pick<TOrgMembershipDALFactory, "find" | "findOne" | "create" | "updateById">;
|
||||
projectDAL: Pick<TProjectDALFactory, "find" | "findProjectGhostUser">;
|
||||
projectMembershipDAL: Pick<TProjectMembershipDALFactory, "find" | "delete" | "findProjectMembershipsByUserId">;
|
||||
groupDAL: Pick<
|
||||
TGroupDALFactory,
|
||||
"create" | "findOne" | "findAllGroupMembers" | "update" | "delete" | "findGroups" | "transaction"
|
||||
>;
|
||||
groupProjectDAL: Pick<TGroupProjectDALFactory, "find">;
|
||||
userGroupMembershipDAL: Pick<
|
||||
TUserGroupMembershipDALFactory,
|
||||
"find" | "transaction" | "insertMany" | "filterProjectsByUserMembership" | "delete"
|
||||
>;
|
||||
projectKeyDAL: Pick<TProjectKeyDALFactory, "find" | "findLatestProjectKey" | "insertMany" | "delete">;
|
||||
projectBotDAL: Pick<TProjectBotDALFactory, "findOne">;
|
||||
licenseService: Pick<TLicenseServiceFactory, "getPlan" | "updateSubscriptionOrgMemberCount">;
|
||||
permissionService: Pick<TPermissionServiceFactory, "getOrgPermission">;
|
||||
smtpService: TSmtpService;
|
||||
smtpService: Pick<TSmtpService, "sendMail">;
|
||||
};
|
||||
|
||||
export type TScimServiceFactory = ReturnType<typeof scimServiceFactory>;
|
||||
@ -61,10 +86,16 @@ export const scimServiceFactory = ({
|
||||
licenseService,
|
||||
scimDAL,
|
||||
userDAL,
|
||||
userAliasDAL,
|
||||
orgDAL,
|
||||
orgMembershipDAL,
|
||||
projectDAL,
|
||||
projectMembershipDAL,
|
||||
groupDAL,
|
||||
groupProjectDAL,
|
||||
userGroupMembershipDAL,
|
||||
projectKeyDAL,
|
||||
projectBotDAL,
|
||||
permissionService,
|
||||
smtpService
|
||||
}: TScimServiceFactoryDep) => {
|
||||
@ -144,7 +175,7 @@ export const scimServiceFactory = ({
|
||||
};
|
||||
|
||||
// SCIM server endpoints
|
||||
const listScimUsers = async ({ offset, limit, filter, orgId }: TListScimUsersDTO): Promise<TListScimUsers> => {
|
||||
const listScimUsers = async ({ startIndex, limit, filter, orgId }: TListScimUsersDTO): Promise<TListScimUsers> => {
|
||||
const org = await orgDAL.findById(orgId);
|
||||
|
||||
if (!org.scimEnabled)
|
||||
@ -162,11 +193,11 @@ export const scimServiceFactory = ({
|
||||
attributeName = "email";
|
||||
}
|
||||
|
||||
return { [attributeName]: parsedValue };
|
||||
return { [attributeName]: parsedValue.replace(/"/g, "") };
|
||||
};
|
||||
|
||||
const findOpts = {
|
||||
...(offset && { offset }),
|
||||
...(startIndex && { offset: startIndex - 1 }),
|
||||
...(limit && { limit })
|
||||
};
|
||||
|
||||
@ -178,10 +209,10 @@ export const scimServiceFactory = ({
|
||||
findOpts
|
||||
);
|
||||
|
||||
const scimUsers = users.map(({ userId, username, firstName, lastName, email }) =>
|
||||
const scimUsers = users.map(({ id, externalId, username, firstName, lastName, email }) =>
|
||||
buildScimUser({
|
||||
userId: userId ?? "",
|
||||
username,
|
||||
orgMembershipId: id ?? "",
|
||||
username: externalId ?? username,
|
||||
firstName: firstName ?? "",
|
||||
lastName: lastName ?? "",
|
||||
email,
|
||||
@ -191,16 +222,16 @@ export const scimServiceFactory = ({
|
||||
|
||||
return buildScimUserList({
|
||||
scimUsers,
|
||||
offset,
|
||||
startIndex,
|
||||
limit
|
||||
});
|
||||
};
|
||||
|
||||
const getScimUser = async ({ userId, orgId }: TGetScimUserDTO) => {
|
||||
const getScimUser = async ({ orgMembershipId, orgId }: TGetScimUserDTO) => {
|
||||
const [membership] = await orgDAL
|
||||
.findMembership({
|
||||
userId,
|
||||
[`${TableName.OrgMembership}.orgId` as "id"]: orgId
|
||||
[`${TableName.OrgMembership}.id` as "id"]: orgMembershipId,
|
||||
[`${TableName.OrgMembership}.orgId` as "orgId"]: orgId
|
||||
})
|
||||
.catch(() => {
|
||||
throw new ScimRequestError({
|
||||
@ -222,8 +253,8 @@ export const scimServiceFactory = ({
|
||||
});
|
||||
|
||||
return buildScimUser({
|
||||
userId: membership.userId as string,
|
||||
username: membership.username,
|
||||
orgMembershipId: membership.id,
|
||||
username: membership.externalId ?? membership.username,
|
||||
email: membership.email ?? "",
|
||||
firstName: membership.firstName as string,
|
||||
lastName: membership.lastName as string,
|
||||
@ -231,7 +262,9 @@ export const scimServiceFactory = ({
|
||||
});
|
||||
};
|
||||
|
||||
const createScimUser = async ({ username, email, firstName, lastName, orgId }: TCreateScimUserDTO) => {
|
||||
const createScimUser = async ({ externalId, email, firstName, lastName, orgId }: TCreateScimUserDTO) => {
|
||||
if (!email) throw new ScimRequestError({ detail: "Invalid request. Missing email.", status: 400 });
|
||||
|
||||
const org = await orgDAL.findById(orgId);
|
||||
|
||||
if (!org)
|
||||
@ -246,67 +279,121 @@ export const scimServiceFactory = ({
|
||||
status: 403
|
||||
});
|
||||
|
||||
let user = await userDAL.findOne({
|
||||
username
|
||||
const appCfg = getConfig();
|
||||
const serverCfg = await getServerCfg();
|
||||
|
||||
const userAlias = await userAliasDAL.findOne({
|
||||
externalId,
|
||||
orgId,
|
||||
aliasType: UserAliasType.SAML
|
||||
});
|
||||
|
||||
if (user) {
|
||||
await userDAL.transaction(async (tx) => {
|
||||
const [orgMembership] = await orgDAL.findMembership(
|
||||
const { user: createdUser, orgMembership: createdOrgMembership } = await userDAL.transaction(async (tx) => {
|
||||
let user: TUsers | undefined;
|
||||
let orgMembership: TOrgMemberships;
|
||||
if (userAlias) {
|
||||
user = await userDAL.findById(userAlias.userId, tx);
|
||||
orgMembership = await orgMembershipDAL.findOne(
|
||||
{
|
||||
userId: user.id,
|
||||
[`${TableName.OrgMembership}.orgId` as "id"]: orgId
|
||||
orgId
|
||||
},
|
||||
{ tx }
|
||||
tx
|
||||
);
|
||||
if (orgMembership)
|
||||
throw new ScimRequestError({
|
||||
detail: "User already exists in the database",
|
||||
status: 409
|
||||
});
|
||||
|
||||
if (!orgMembership) {
|
||||
await orgDAL.createMembership(
|
||||
orgMembership = await orgMembershipDAL.create(
|
||||
{
|
||||
userId: user.id,
|
||||
orgId,
|
||||
userId: userAlias.userId,
|
||||
inviteEmail: email,
|
||||
orgId,
|
||||
role: OrgMembershipRole.Member,
|
||||
status: OrgMembershipStatus.Invited
|
||||
status: user.isAccepted ? OrgMembershipStatus.Accepted : OrgMembershipStatus.Invited // if user is fully completed, then set status to accepted, otherwise set it to invited so we can update it later
|
||||
},
|
||||
tx
|
||||
);
|
||||
} else if (orgMembership.status === OrgMembershipStatus.Invited && user.isAccepted) {
|
||||
orgMembership = await orgMembershipDAL.updateById(
|
||||
orgMembership.id,
|
||||
{
|
||||
status: OrgMembershipStatus.Accepted
|
||||
},
|
||||
tx
|
||||
);
|
||||
}
|
||||
});
|
||||
} else {
|
||||
user = await userDAL.transaction(async (tx) => {
|
||||
const newUser = await userDAL.create(
|
||||
} else {
|
||||
if (serverCfg.trustSamlEmails) {
|
||||
user = await userDAL.findOne(
|
||||
{
|
||||
email,
|
||||
isEmailVerified: true
|
||||
},
|
||||
tx
|
||||
);
|
||||
}
|
||||
|
||||
if (!user) {
|
||||
const uniqueUsername = await normalizeUsername(`${firstName}-${lastName}`, userDAL);
|
||||
user = await userDAL.create(
|
||||
{
|
||||
username: serverCfg.trustSamlEmails ? email : uniqueUsername,
|
||||
email,
|
||||
isEmailVerified: serverCfg.trustSamlEmails,
|
||||
firstName,
|
||||
lastName,
|
||||
authMethods: [],
|
||||
isGhost: false
|
||||
},
|
||||
tx
|
||||
);
|
||||
}
|
||||
|
||||
await userAliasDAL.create(
|
||||
{
|
||||
username,
|
||||
email,
|
||||
firstName,
|
||||
lastName,
|
||||
authMethods: [AuthMethod.EMAIL],
|
||||
isGhost: false
|
||||
userId: user.id,
|
||||
aliasType: UserAliasType.SAML,
|
||||
externalId,
|
||||
emails: email ? [email] : [],
|
||||
orgId
|
||||
},
|
||||
tx
|
||||
);
|
||||
|
||||
await orgDAL.createMembership(
|
||||
const [foundOrgMembership] = await orgDAL.findMembership(
|
||||
{
|
||||
inviteEmail: email,
|
||||
orgId,
|
||||
userId: newUser.id,
|
||||
role: OrgMembershipRole.Member,
|
||||
status: OrgMembershipStatus.Invited
|
||||
[`${TableName.OrgMembership}.userId` as "userId"]: user.id,
|
||||
[`${TableName.OrgMembership}.orgId` as "id"]: orgId
|
||||
},
|
||||
tx
|
||||
{ tx }
|
||||
);
|
||||
return newUser;
|
||||
});
|
||||
}
|
||||
|
||||
const appCfg = getConfig();
|
||||
orgMembership = foundOrgMembership;
|
||||
|
||||
if (!orgMembership) {
|
||||
orgMembership = await orgMembershipDAL.create(
|
||||
{
|
||||
userId: user.id,
|
||||
inviteEmail: email,
|
||||
orgId,
|
||||
role: OrgMembershipRole.Member,
|
||||
status: user.isAccepted ? OrgMembershipStatus.Accepted : OrgMembershipStatus.Invited // if user is fully completed, then set status to accepted, otherwise set it to invited so we can update it later
|
||||
},
|
||||
tx
|
||||
);
|
||||
// Only update the membership to Accepted if the user account is already completed.
|
||||
} else if (orgMembership.status === OrgMembershipStatus.Invited && user.isAccepted) {
|
||||
orgMembership = await orgDAL.updateMembershipById(
|
||||
orgMembership.id,
|
||||
{
|
||||
status: OrgMembershipStatus.Accepted
|
||||
},
|
||||
tx
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return { user, orgMembership };
|
||||
});
|
||||
|
||||
if (email) {
|
||||
await smtpService.sendMail({
|
||||
@ -321,20 +408,20 @@ export const scimServiceFactory = ({
|
||||
}
|
||||
|
||||
return buildScimUser({
|
||||
userId: user.id,
|
||||
username: user.username,
|
||||
firstName: user.firstName as string,
|
||||
lastName: user.lastName as string,
|
||||
email: user.email ?? "",
|
||||
orgMembershipId: createdOrgMembership.id,
|
||||
username: externalId,
|
||||
firstName: createdUser.firstName as string,
|
||||
lastName: createdUser.lastName as string,
|
||||
email: createdUser.email ?? "",
|
||||
active: true
|
||||
});
|
||||
};
|
||||
|
||||
const updateScimUser = async ({ userId, orgId, operations }: TUpdateScimUserDTO) => {
|
||||
const updateScimUser = async ({ orgMembershipId, orgId, operations }: TUpdateScimUserDTO) => {
|
||||
const [membership] = await orgDAL
|
||||
.findMembership({
|
||||
userId,
|
||||
[`${TableName.OrgMembership}.orgId` as "id"]: orgId
|
||||
[`${TableName.OrgMembership}.id` as "id"]: orgMembershipId,
|
||||
[`${TableName.OrgMembership}.orgId` as "orgId"]: orgId
|
||||
})
|
||||
.catch(() => {
|
||||
throw new ScimRequestError({
|
||||
@ -370,18 +457,20 @@ export const scimServiceFactory = ({
|
||||
});
|
||||
|
||||
if (!active) {
|
||||
await deleteOrgMembership({
|
||||
await deleteOrgMembershipFn({
|
||||
orgMembershipId: membership.id,
|
||||
orgId: membership.orgId,
|
||||
orgDAL,
|
||||
projectDAL,
|
||||
projectMembershipDAL
|
||||
projectMembershipDAL,
|
||||
projectKeyDAL,
|
||||
userAliasDAL,
|
||||
licenseService
|
||||
});
|
||||
}
|
||||
|
||||
return buildScimUser({
|
||||
userId: membership.userId as string,
|
||||
username: membership.username,
|
||||
orgMembershipId: membership.id,
|
||||
username: membership.externalId ?? membership.username,
|
||||
email: membership.email,
|
||||
firstName: membership.firstName as string,
|
||||
lastName: membership.lastName as string,
|
||||
@ -389,11 +478,11 @@ export const scimServiceFactory = ({
|
||||
});
|
||||
};
|
||||
|
||||
const replaceScimUser = async ({ userId, active, orgId }: TReplaceScimUserDTO) => {
|
||||
const replaceScimUser = async ({ orgMembershipId, active, orgId }: TReplaceScimUserDTO) => {
|
||||
const [membership] = await orgDAL
|
||||
.findMembership({
|
||||
userId,
|
||||
[`${TableName.OrgMembership}.orgId` as "id"]: orgId
|
||||
[`${TableName.OrgMembership}.id` as "id"]: orgMembershipId,
|
||||
[`${TableName.OrgMembership}.orgId` as "orgId"]: orgId
|
||||
})
|
||||
.catch(() => {
|
||||
throw new ScimRequestError({
|
||||
@ -415,19 +504,20 @@ export const scimServiceFactory = ({
|
||||
});
|
||||
|
||||
if (!active) {
|
||||
// tx
|
||||
await deleteOrgMembership({
|
||||
await deleteOrgMembershipFn({
|
||||
orgMembershipId: membership.id,
|
||||
orgId: membership.orgId,
|
||||
orgDAL,
|
||||
projectDAL,
|
||||
projectMembershipDAL
|
||||
projectMembershipDAL,
|
||||
projectKeyDAL,
|
||||
userAliasDAL,
|
||||
licenseService
|
||||
});
|
||||
}
|
||||
|
||||
return buildScimUser({
|
||||
userId: membership.userId as string,
|
||||
username: membership.username,
|
||||
orgMembershipId: membership.id,
|
||||
username: membership.externalId ?? membership.username,
|
||||
email: membership.email,
|
||||
firstName: membership.firstName as string,
|
||||
lastName: membership.lastName as string,
|
||||
@ -435,18 +525,11 @@ 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
|
||||
});
|
||||
});
|
||||
const deleteScimUser = async ({ orgMembershipId, orgId }: TDeleteScimUserDTO) => {
|
||||
const [membership] = await orgDAL.findMembership({
|
||||
[`${TableName.OrgMembership}.id` as "id"]: orgMembershipId,
|
||||
[`${TableName.OrgMembership}.orgId` as "orgId"]: orgId
|
||||
});
|
||||
|
||||
if (!membership)
|
||||
throw new ScimRequestError({
|
||||
@ -461,19 +544,33 @@ export const scimServiceFactory = ({
|
||||
});
|
||||
}
|
||||
|
||||
await deleteOrgMembership({
|
||||
await deleteOrgMembershipFn({
|
||||
orgMembershipId: membership.id,
|
||||
orgId: membership.orgId,
|
||||
orgDAL,
|
||||
projectDAL,
|
||||
projectMembershipDAL
|
||||
projectMembershipDAL,
|
||||
projectKeyDAL,
|
||||
userAliasDAL,
|
||||
licenseService
|
||||
});
|
||||
|
||||
return {}; // intentionally return empty object upon success
|
||||
};
|
||||
|
||||
const listScimGroups = async ({ orgId, offset, limit }: TListScimGroupsDTO) => {
|
||||
const listScimGroups = async ({ orgId, startIndex, limit }: TListScimGroupsDTO) => {
|
||||
const plan = await licenseService.getPlan(orgId);
|
||||
if (!plan.groups)
|
||||
throw new BadRequestError({
|
||||
message: "Failed to list SCIM groups due to plan restriction. Upgrade plan to list SCIM groups."
|
||||
});
|
||||
|
||||
const org = await orgDAL.findById(orgId);
|
||||
if (!org) {
|
||||
throw new ScimRequestError({
|
||||
detail: "Organization Not Found",
|
||||
status: 404
|
||||
});
|
||||
}
|
||||
|
||||
if (!org.scimEnabled)
|
||||
throw new ScimRequestError({
|
||||
@ -481,49 +578,114 @@ export const scimServiceFactory = ({
|
||||
status: 403
|
||||
});
|
||||
|
||||
const groups = await groupDAL.findGroups({
|
||||
orgId
|
||||
});
|
||||
const groups = await groupDAL.findGroups(
|
||||
{
|
||||
orgId
|
||||
},
|
||||
{
|
||||
offset: startIndex - 1,
|
||||
limit
|
||||
}
|
||||
);
|
||||
|
||||
const scimGroups = groups.map((group) =>
|
||||
buildScimGroup({
|
||||
groupId: group.id,
|
||||
name: group.name,
|
||||
members: []
|
||||
members: [] // does this need to be populated?
|
||||
})
|
||||
);
|
||||
|
||||
return buildScimGroupList({
|
||||
scimGroups,
|
||||
offset,
|
||||
startIndex,
|
||||
limit
|
||||
});
|
||||
};
|
||||
|
||||
const createScimGroup = async ({ displayName, orgId }: TCreateScimGroupDTO) => {
|
||||
const createScimGroup = async ({ displayName, orgId, members }: TCreateScimGroupDTO) => {
|
||||
const plan = await licenseService.getPlan(orgId);
|
||||
if (!plan.groups)
|
||||
throw new BadRequestError({
|
||||
message: "Failed to create a SCIM group due to plan restriction. Upgrade plan to create a SCIM group."
|
||||
});
|
||||
|
||||
const org = await orgDAL.findById(orgId);
|
||||
|
||||
if (!org) {
|
||||
throw new ScimRequestError({
|
||||
detail: "Organization Not Found",
|
||||
status: 404
|
||||
});
|
||||
}
|
||||
|
||||
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
|
||||
const newGroup = await groupDAL.transaction(async (tx) => {
|
||||
const group = await groupDAL.create(
|
||||
{
|
||||
name: displayName,
|
||||
slug: slugify(`${displayName}-${alphaNumericNanoId(4)}`),
|
||||
orgId,
|
||||
role: OrgMembershipRole.NoAccess
|
||||
},
|
||||
tx
|
||||
);
|
||||
|
||||
if (members && members.length) {
|
||||
const orgMemberships = await orgMembershipDAL.find({
|
||||
$in: {
|
||||
id: members.map((member) => member.value)
|
||||
}
|
||||
});
|
||||
|
||||
const newMembers = await addUsersToGroupByUserIds({
|
||||
group,
|
||||
userIds: orgMemberships.map((membership) => membership.userId as string),
|
||||
userDAL,
|
||||
userGroupMembershipDAL,
|
||||
orgDAL,
|
||||
groupProjectDAL,
|
||||
projectKeyDAL,
|
||||
projectDAL,
|
||||
projectBotDAL,
|
||||
tx
|
||||
});
|
||||
|
||||
return { group, newMembers };
|
||||
}
|
||||
|
||||
return { group, newMembers: [] };
|
||||
});
|
||||
|
||||
const orgMemberships = await orgDAL.findMembership({
|
||||
[`${TableName.OrgMembership}.orgId` as "orgId"]: orgId,
|
||||
$in: {
|
||||
[`${TableName.OrgMembership}.userId` as "userId"]: newGroup.newMembers.map((member) => member.id)
|
||||
}
|
||||
});
|
||||
|
||||
return buildScimGroup({
|
||||
groupId: group.id,
|
||||
name: group.name,
|
||||
members: []
|
||||
groupId: newGroup.group.id,
|
||||
name: newGroup.group.name,
|
||||
members: orgMemberships.map(({ id, firstName, lastName }) => ({
|
||||
value: id,
|
||||
display: `${firstName} ${lastName}`
|
||||
}))
|
||||
});
|
||||
};
|
||||
|
||||
const getScimGroup = async ({ groupId, orgId }: TGetScimGroupDTO) => {
|
||||
const plan = await licenseService.getPlan(orgId);
|
||||
if (!plan.groups)
|
||||
throw new BadRequestError({
|
||||
message: "Failed to get SCIM group due to plan restriction. Upgrade plan to get SCIM group."
|
||||
});
|
||||
|
||||
const group = await groupDAL.findOne({
|
||||
id: groupId,
|
||||
orgId
|
||||
@ -541,47 +703,148 @@ export const scimServiceFactory = ({
|
||||
groupId: group.id
|
||||
});
|
||||
|
||||
const orgMemberships = await orgDAL.findMembership({
|
||||
[`${TableName.OrgMembership}.orgId` as "orgId"]: orgId,
|
||||
$in: {
|
||||
[`${TableName.OrgMembership}.userId` as "userId"]: users
|
||||
.filter((user) => user.isPartOfGroup)
|
||||
.map((user) => user.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}`
|
||||
}))
|
||||
members: orgMemberships.map(({ id, firstName, lastName }) => ({
|
||||
value: id,
|
||||
display: `${firstName} ${lastName}`
|
||||
}))
|
||||
});
|
||||
};
|
||||
|
||||
const updateScimGroupNamePut = async ({ groupId, orgId, displayName }: TUpdateScimGroupNamePutDTO) => {
|
||||
const [group] = await groupDAL.update(
|
||||
{
|
||||
id: groupId,
|
||||
orgId
|
||||
},
|
||||
{
|
||||
name: displayName
|
||||
}
|
||||
);
|
||||
const updateScimGroupNamePut = async ({ groupId, orgId, displayName, members }: TUpdateScimGroupNamePutDTO) => {
|
||||
const plan = await licenseService.getPlan(orgId);
|
||||
if (!plan.groups)
|
||||
throw new BadRequestError({
|
||||
message: "Failed to update SCIM group due to plan restriction. Upgrade plan to update SCIM group."
|
||||
});
|
||||
|
||||
if (!group) {
|
||||
const org = await orgDAL.findById(orgId);
|
||||
if (!org) {
|
||||
throw new ScimRequestError({
|
||||
detail: "Group Not Found",
|
||||
detail: "Organization Not Found",
|
||||
status: 404
|
||||
});
|
||||
}
|
||||
|
||||
if (!org.scimEnabled)
|
||||
throw new ScimRequestError({
|
||||
detail: "SCIM is disabled for the organization",
|
||||
status: 403
|
||||
});
|
||||
|
||||
const updatedGroup = await groupDAL.transaction(async (tx) => {
|
||||
const [group] = await groupDAL.update(
|
||||
{
|
||||
id: groupId,
|
||||
orgId
|
||||
},
|
||||
{
|
||||
name: displayName
|
||||
}
|
||||
);
|
||||
|
||||
if (!group) {
|
||||
throw new ScimRequestError({
|
||||
detail: "Group Not Found",
|
||||
status: 404
|
||||
});
|
||||
}
|
||||
|
||||
if (members) {
|
||||
const orgMemberships = await orgMembershipDAL.find({
|
||||
$in: {
|
||||
id: members.map((member) => member.value)
|
||||
}
|
||||
});
|
||||
|
||||
const membersIdsSet = new Set(orgMemberships.map((orgMembership) => orgMembership.userId));
|
||||
|
||||
const directMemberUserIds = (
|
||||
await userGroupMembershipDAL.find({
|
||||
groupId: group.id,
|
||||
isPending: false
|
||||
})
|
||||
).map((membership) => membership.userId);
|
||||
|
||||
const pendingGroupAdditionsUserIds = (
|
||||
await userGroupMembershipDAL.find({
|
||||
groupId: group.id,
|
||||
isPending: true
|
||||
})
|
||||
).map((pendingGroupAddition) => pendingGroupAddition.userId);
|
||||
|
||||
const allMembersUserIds = directMemberUserIds.concat(pendingGroupAdditionsUserIds);
|
||||
const allMembersUserIdsSet = new Set(allMembersUserIds);
|
||||
|
||||
const toAddUserIds = orgMemberships.filter((member) => !allMembersUserIdsSet.has(member.userId as string));
|
||||
const toRemoveUserIds = allMembersUserIds.filter((userId) => !membersIdsSet.has(userId));
|
||||
|
||||
if (toAddUserIds.length) {
|
||||
await addUsersToGroupByUserIds({
|
||||
group,
|
||||
userIds: toAddUserIds.map((member) => member.userId as string),
|
||||
userDAL,
|
||||
userGroupMembershipDAL,
|
||||
orgDAL,
|
||||
groupProjectDAL,
|
||||
projectKeyDAL,
|
||||
projectDAL,
|
||||
projectBotDAL,
|
||||
tx
|
||||
});
|
||||
}
|
||||
|
||||
if (toRemoveUserIds.length) {
|
||||
await removeUsersFromGroupByUserIds({
|
||||
group,
|
||||
userIds: toRemoveUserIds,
|
||||
userDAL,
|
||||
userGroupMembershipDAL,
|
||||
groupProjectDAL,
|
||||
projectKeyDAL,
|
||||
tx
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return group;
|
||||
});
|
||||
|
||||
return buildScimGroup({
|
||||
groupId: group.id,
|
||||
name: group.name,
|
||||
members: []
|
||||
groupId: updatedGroup.id,
|
||||
name: updatedGroup.name,
|
||||
members
|
||||
});
|
||||
};
|
||||
|
||||
// TODO: add support for add/remove op
|
||||
const updateScimGroupNamePatch = async ({ groupId, orgId, operations }: TUpdateScimGroupNamePatchDTO) => {
|
||||
const plan = await licenseService.getPlan(orgId);
|
||||
if (!plan.groups)
|
||||
throw new BadRequestError({
|
||||
message: "Failed to update SCIM group due to plan restriction. Upgrade plan to update SCIM group."
|
||||
});
|
||||
|
||||
const org = await orgDAL.findById(orgId);
|
||||
|
||||
if (!org) {
|
||||
throw new ScimRequestError({
|
||||
detail: "Organization Not Found",
|
||||
status: 404
|
||||
});
|
||||
}
|
||||
|
||||
if (!org.scimEnabled)
|
||||
throw new ScimRequestError({
|
||||
detail: "SCIM is disabled for the organization",
|
||||
@ -635,6 +898,26 @@ export const scimServiceFactory = ({
|
||||
};
|
||||
|
||||
const deleteScimGroup = async ({ groupId, orgId }: TDeleteScimGroupDTO) => {
|
||||
const plan = await licenseService.getPlan(orgId);
|
||||
if (!plan.groups)
|
||||
throw new BadRequestError({
|
||||
message: "Failed to delete SCIM group due to plan restriction. Upgrade plan to delete SCIM group."
|
||||
});
|
||||
|
||||
const org = await orgDAL.findById(orgId);
|
||||
if (!org) {
|
||||
throw new ScimRequestError({
|
||||
detail: "Organization Not Found",
|
||||
status: 404
|
||||
});
|
||||
}
|
||||
|
||||
if (!org.scimEnabled)
|
||||
throw new ScimRequestError({
|
||||
detail: "SCIM is disabled for the organization",
|
||||
status: 403
|
||||
});
|
||||
|
||||
const [group] = await groupDAL.delete({
|
||||
id: groupId,
|
||||
orgId
|
||||
|
@ -12,7 +12,7 @@ export type TDeleteScimTokenDTO = {
|
||||
// SCIM server endpoint types
|
||||
|
||||
export type TListScimUsersDTO = {
|
||||
offset: number;
|
||||
startIndex: number;
|
||||
limit: number;
|
||||
filter?: string;
|
||||
orgId: string;
|
||||
@ -27,12 +27,12 @@ export type TListScimUsers = {
|
||||
};
|
||||
|
||||
export type TGetScimUserDTO = {
|
||||
userId: string;
|
||||
orgMembershipId: string;
|
||||
orgId: string;
|
||||
};
|
||||
|
||||
export type TCreateScimUserDTO = {
|
||||
username: string;
|
||||
externalId: string;
|
||||
email?: string;
|
||||
firstName: string;
|
||||
lastName: string;
|
||||
@ -40,7 +40,7 @@ export type TCreateScimUserDTO = {
|
||||
};
|
||||
|
||||
export type TUpdateScimUserDTO = {
|
||||
userId: string;
|
||||
orgMembershipId: string;
|
||||
orgId: string;
|
||||
operations: {
|
||||
op: string;
|
||||
@ -54,18 +54,18 @@ export type TUpdateScimUserDTO = {
|
||||
};
|
||||
|
||||
export type TReplaceScimUserDTO = {
|
||||
userId: string;
|
||||
orgMembershipId: string;
|
||||
active: boolean;
|
||||
orgId: string;
|
||||
};
|
||||
|
||||
export type TDeleteScimUserDTO = {
|
||||
userId: string;
|
||||
orgMembershipId: string;
|
||||
orgId: string;
|
||||
};
|
||||
|
||||
export type TListScimGroupsDTO = {
|
||||
offset: number;
|
||||
startIndex: number;
|
||||
limit: number;
|
||||
orgId: string;
|
||||
};
|
||||
@ -81,6 +81,11 @@ export type TListScimGroups = {
|
||||
export type TCreateScimGroupDTO = {
|
||||
displayName: string;
|
||||
orgId: string;
|
||||
members?: {
|
||||
// TODO: account for members with value and display (is this optional?)
|
||||
value: string;
|
||||
display: string;
|
||||
}[];
|
||||
};
|
||||
|
||||
export type TGetScimGroupDTO = {
|
||||
@ -92,6 +97,10 @@ export type TUpdateScimGroupNamePutDTO = {
|
||||
groupId: string;
|
||||
orgId: string;
|
||||
displayName: string;
|
||||
members: {
|
||||
value: string;
|
||||
display: string;
|
||||
}[];
|
||||
};
|
||||
|
||||
export type TUpdateScimGroupNamePatchDTO = {
|
||||
|
@ -495,7 +495,11 @@ export const secretApprovalRequestServiceFactory = ({
|
||||
await projectDAL.checkProjectUpgradeStatus(projectId);
|
||||
|
||||
const folder = await folderDAL.findBySecretPath(projectId, environment, secretPath);
|
||||
if (!folder) throw new BadRequestError({ message: "Folder not found", name: "GenSecretApproval" });
|
||||
if (!folder)
|
||||
throw new BadRequestError({
|
||||
message: "Folder not found for the given environment slug & secret path",
|
||||
name: "GenSecretApproval"
|
||||
});
|
||||
const folderId = folder.id;
|
||||
|
||||
const blindIndexCfg = await secretBlindIndexDAL.findOne({ projectId });
|
||||
|
@ -90,16 +90,20 @@ export const secretRotationDbFn = async ({
|
||||
const appCfg = getConfig();
|
||||
|
||||
const ssl = ca ? { rejectUnauthorized: false, ca } : undefined;
|
||||
const isCloud = Boolean(appCfg.LICENSE_SERVER_KEY); // quick and dirty way to check if its cloud or not
|
||||
const dbHost = appCfg.DB_HOST || getDbConnectionHost(appCfg.DB_CONNECTION_URI);
|
||||
|
||||
if (
|
||||
isCloud &&
|
||||
// internal ips
|
||||
(host === "host.docker.internal" || host.match(/^10\.\d+\.\d+\.\d+/) || host.match(/^192\.168\.\d+\.\d+/))
|
||||
)
|
||||
throw new Error("Invalid db host");
|
||||
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+/)
|
||||
dbHost === host
|
||||
)
|
||||
throw new Error("Invalid db host");
|
||||
|
||||
|
@ -1,4 +1,4 @@
|
||||
import { ForbiddenError } from "@casl/ability";
|
||||
import { ForbiddenError, subject } from "@casl/ability";
|
||||
|
||||
import { TableName, TSecretTagJunctionInsert } from "@app/db/schemas";
|
||||
import { BadRequestError, InternalServerError } from "@app/lib/errors";
|
||||
@ -23,6 +23,7 @@ import {
|
||||
import { TSnapshotDALFactory } from "./snapshot-dal";
|
||||
import { TSnapshotFolderDALFactory } from "./snapshot-folder-dal";
|
||||
import { TSnapshotSecretDALFactory } from "./snapshot-secret-dal";
|
||||
import { getFullFolderPath } from "./snapshot-service-fns";
|
||||
|
||||
type TSecretSnapshotServiceFactoryDep = {
|
||||
snapshotDAL: TSnapshotDALFactory;
|
||||
@ -33,7 +34,7 @@ type TSecretSnapshotServiceFactoryDep = {
|
||||
secretDAL: Pick<TSecretDALFactory, "delete" | "insertMany">;
|
||||
secretTagDAL: Pick<TSecretTagDALFactory, "saveTagsToSecret">;
|
||||
secretVersionTagDAL: Pick<TSecretVersionTagDALFactory, "insertMany">;
|
||||
folderDAL: Pick<TSecretFolderDALFactory, "findById" | "findBySecretPath" | "delete" | "insertMany">;
|
||||
folderDAL: Pick<TSecretFolderDALFactory, "findById" | "findBySecretPath" | "delete" | "insertMany" | "find">;
|
||||
permissionService: Pick<TPermissionServiceFactory, "getProjectPermission">;
|
||||
licenseService: Pick<TLicenseServiceFactory, "isValidLicense">;
|
||||
};
|
||||
@ -71,6 +72,12 @@ export const secretSnapshotServiceFactory = ({
|
||||
);
|
||||
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.SecretRollback);
|
||||
|
||||
// We need to check if the user has access to the secrets in the folder. If we don't do this, a user could theoretically access snapshot secret values even if they don't have read access to the secrets in the folder.
|
||||
ForbiddenError.from(permission).throwUnlessCan(
|
||||
ProjectPermissionActions.Read,
|
||||
subject(ProjectPermissionSub.Secrets, { environment, secretPath: path })
|
||||
);
|
||||
|
||||
const folder = await folderDAL.findBySecretPath(projectId, environment, path);
|
||||
if (!folder) throw new BadRequestError({ message: "Folder not found" });
|
||||
|
||||
@ -98,6 +105,12 @@ export const secretSnapshotServiceFactory = ({
|
||||
);
|
||||
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.SecretRollback);
|
||||
|
||||
// We need to check if the user has access to the secrets in the folder. If we don't do this, a user could theoretically access snapshot secret values even if they don't have read access to the secrets in the folder.
|
||||
ForbiddenError.from(permission).throwUnlessCan(
|
||||
ProjectPermissionActions.Read,
|
||||
subject(ProjectPermissionSub.Secrets, { environment, secretPath: path })
|
||||
);
|
||||
|
||||
const folder = await folderDAL.findBySecretPath(projectId, environment, path);
|
||||
if (!folder) throw new BadRequestError({ message: "Folder not found" });
|
||||
|
||||
@ -116,6 +129,19 @@ export const secretSnapshotServiceFactory = ({
|
||||
actorOrgId
|
||||
);
|
||||
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.SecretRollback);
|
||||
|
||||
const fullFolderPath = await getFullFolderPath({
|
||||
folderDAL,
|
||||
folderId: snapshot.folderId,
|
||||
envId: snapshot.environment.id
|
||||
});
|
||||
|
||||
// We need to check if the user has access to the secrets in the folder. If we don't do this, a user could theoretically access snapshot secret values even if they don't have read access to the secrets in the folder.
|
||||
ForbiddenError.from(permission).throwUnlessCan(
|
||||
ProjectPermissionActions.Read,
|
||||
subject(ProjectPermissionSub.Secrets, { environment: snapshot.environment.slug, secretPath: fullFolderPath })
|
||||
);
|
||||
|
||||
return snapshot;
|
||||
};
|
||||
|
||||
|
@ -101,6 +101,7 @@ export const snapshotDALFactory = (db: TDbClient) => {
|
||||
key: "snapshotId",
|
||||
parentMapper: ({
|
||||
snapshotId: id,
|
||||
folderId,
|
||||
projectId,
|
||||
envId,
|
||||
envSlug,
|
||||
@ -109,6 +110,7 @@ export const snapshotDALFactory = (db: TDbClient) => {
|
||||
snapshotUpdatedAt: updatedAt
|
||||
}) => ({
|
||||
id,
|
||||
folderId,
|
||||
projectId,
|
||||
createdAt,
|
||||
updatedAt,
|
||||
|
@ -0,0 +1,28 @@
|
||||
import { TSecretFolderDALFactory } from "@app/services/secret-folder/secret-folder-dal";
|
||||
|
||||
type GetFullFolderPath = {
|
||||
folderDAL: Pick<TSecretFolderDALFactory, "findById" | "find">; // Added findAllInEnv
|
||||
folderId: string;
|
||||
envId: string;
|
||||
};
|
||||
|
||||
export const getFullFolderPath = async ({ folderDAL, folderId, envId }: GetFullFolderPath): Promise<string> => {
|
||||
// Helper function to remove duplicate slashes
|
||||
const removeDuplicateSlashes = (path: string) => path.replace(/\/{2,}/g, "/");
|
||||
|
||||
// Fetch all folders at once based on environment ID to avoid multiple queries
|
||||
const folders = await folderDAL.find({ envId });
|
||||
const folderMap = new Map(folders.map((folder) => [folder.id, folder]));
|
||||
|
||||
const buildPath = (currFolderId: string): string => {
|
||||
const folder = folderMap.get(currFolderId);
|
||||
if (!folder) return "";
|
||||
const folderPathSegment = !folder.parentId && folder.name === "root" ? "/" : `/${folder.name}`;
|
||||
if (folder.parentId) {
|
||||
return removeDuplicateSlashes(`${buildPath(folder.parentId)}${folderPathSegment}`);
|
||||
}
|
||||
return removeDuplicateSlashes(folderPathSegment);
|
||||
};
|
||||
|
||||
return buildPath(folderId);
|
||||
};
|
@ -272,6 +272,7 @@ export const SECRETS = {
|
||||
|
||||
export const RAW_SECRETS = {
|
||||
LIST: {
|
||||
expand: "Whether or not to expand secret references",
|
||||
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.",
|
||||
@ -282,6 +283,7 @@ export const RAW_SECRETS = {
|
||||
},
|
||||
CREATE: {
|
||||
secretName: "The name of the secret to create.",
|
||||
projectSlug: "The slug of the project to create the secret in.",
|
||||
environment: "The slug of the environment to create the secret in.",
|
||||
secretComment: "Attach a comment to the secret.",
|
||||
secretPath: "The path to create the secret in.",
|
||||
@ -301,11 +303,13 @@ export const RAW_SECRETS = {
|
||||
},
|
||||
UPDATE: {
|
||||
secretName: "The name of the secret to update.",
|
||||
secretComment: "Update comment to the secret.",
|
||||
environment: "The slug of the environment where the secret is located.",
|
||||
secretPath: "The path of the secret to update",
|
||||
secretValue: "The new value of the secret.",
|
||||
skipMultilineEncoding: "Skip multiline encoding for the secret value.",
|
||||
type: "The type of the secret to update.",
|
||||
projectSlug: "The slug of the project to update the secret in.",
|
||||
workspaceId: "The ID of the project to update the secret in."
|
||||
},
|
||||
DELETE: {
|
||||
@ -313,6 +317,7 @@ export const RAW_SECRETS = {
|
||||
environment: "The slug of the environment where the secret is located.",
|
||||
secretPath: "The path of the secret.",
|
||||
type: "The type of the secret to delete.",
|
||||
projectSlug: "The slug of the project to delete the secret in.",
|
||||
workspaceId: "The ID of the project where the secret is located."
|
||||
}
|
||||
} as const;
|
||||
@ -463,9 +468,18 @@ export const IDENTITY_ADDITIONAL_PRIVILEGE = {
|
||||
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"}]]
|
||||
- Read secrets
|
||||
\`\`\`
|
||||
{ "permissions": [{"action": "read", "subject": "secrets"]}
|
||||
\`\`\`
|
||||
- Read and Write secrets
|
||||
\`\`\`
|
||||
{ "permissions": [{"action": "read", "subject": "secrets"], {"action": "write", "subject": "secrets"]}
|
||||
\`\`\`
|
||||
- Read secrets scoped to an environment and secret path
|
||||
\`\`\`
|
||||
- { "permissions": [{"action": "read", "subject": "secrets", "conditions": { "environment": "dev", "secretPath": { "$glob": "/" } }}] }
|
||||
\`\`\`
|
||||
`,
|
||||
isPackPermission: "Whether the server should pack(compact) the permission object.",
|
||||
isTemporary: "Whether the privilege is temporary.",
|
||||
@ -479,11 +493,19 @@ export const IDENTITY_ADDITIONAL_PRIVILEGE = {
|
||||
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"}]]
|
||||
- Read secrets
|
||||
\`\`\`
|
||||
{ "permissions": [{"action": "read", "subject": "secrets"]}
|
||||
\`\`\`
|
||||
- Read and Write secrets
|
||||
\`\`\`
|
||||
{ "permissions": [{"action": "read", "subject": "secrets"], {"action": "write", "subject": "secrets"]}
|
||||
\`\`\`
|
||||
- Read secrets scoped to an environment and secret path
|
||||
\`\`\`
|
||||
- { "permissions": [{"action": "read", "subject": "secrets", "conditions": { "environment": "dev", "secretPath": { "$glob": "/" } }}] }
|
||||
\`\`\`
|
||||
`,
|
||||
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",
|
||||
@ -585,12 +607,13 @@ export const INTEGRATION = {
|
||||
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",
|
||||
secretAWSTag: "The tag for the AWS secrets"
|
||||
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 GCP secrets.",
|
||||
secretAWSTag: "The tags for AWS secrets.",
|
||||
kmsKeyId: "The ID of the encryption key from AWS KMS."
|
||||
}
|
||||
},
|
||||
UPDATE: {
|
||||
@ -609,3 +632,29 @@ export const INTEGRATION = {
|
||||
integrationId: "The ID of the integration object."
|
||||
}
|
||||
};
|
||||
|
||||
export const AUDIT_LOG_STREAMS = {
|
||||
CREATE: {
|
||||
url: "The HTTP URL to push logs to.",
|
||||
headers: {
|
||||
desc: "The HTTP headers attached for the external prrovider requests.",
|
||||
key: "The HTTP header key name.",
|
||||
value: "The HTTP header value."
|
||||
}
|
||||
},
|
||||
UPDATE: {
|
||||
id: "The ID of the audit log stream to update.",
|
||||
url: "The HTTP URL to push logs to.",
|
||||
headers: {
|
||||
desc: "The HTTP headers attached for the external prrovider requests.",
|
||||
key: "The HTTP header key name.",
|
||||
value: "The HTTP header value."
|
||||
}
|
||||
},
|
||||
DELETE: {
|
||||
id: "The ID of the audit log stream to delete."
|
||||
},
|
||||
GET_BY_ID: {
|
||||
id: "The ID of the audit log stream to get details."
|
||||
}
|
||||
};
|
||||
|
@ -119,6 +119,7 @@ const envSchema = z
|
||||
})
|
||||
.transform((data) => ({
|
||||
...data,
|
||||
isCloud: Boolean(data.LICENSE_SERVER_KEY),
|
||||
isSmtpConfigured: Boolean(data.SMTP_HOST),
|
||||
isRedisConfigured: Boolean(data.REDIS_URL),
|
||||
isDevelopmentMode: data.NODE_ENV === "development",
|
||||
|
@ -17,7 +17,7 @@ export type TOrgPermission = {
|
||||
actorId: string;
|
||||
orgId: string;
|
||||
actorAuthMethod: ActorAuthMethod;
|
||||
actorOrgId: string | undefined;
|
||||
actorOrgId: string;
|
||||
};
|
||||
|
||||
export type TProjectPermission = {
|
||||
|
@ -1 +1,2 @@
|
||||
export { isDisposableEmail } from "./validate-email";
|
||||
export { validateLocalIps } from "./validate-url";
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user