Compare commits

..

344 Commits

Author SHA1 Message Date
a42f3b3763 remove INFISICAL_VAULT_FILE_PASSPHRASE because it is being auto generated now 2024-08-08 13:50:34 -04:00
f7d882a6fc Merge pull request #2254 from akhilmhdh/fix/backup
Resolved keyring dataset too big by keeping only the encryption key
2024-08-08 13:19:50 -04:00
385afdfcf8 generate random string fn 2024-08-08 13:03:45 -04:00
281d703cc3 removeed vault use command and auto generated passphrase 2024-08-08 13:02:08 -04:00
6f56ed5474 add missing error logs on secrets backup 2024-08-08 13:01:14 -04:00
=
254446c895 fix: resolved keyring dataset too big by keeping only the encryption key 2024-08-08 13:04:33 +05:30
2739b08e59 revert bb934ef7b1c47195b2ff65a335712add791cb59c 2024-08-07 22:15:06 -04:00
ba5e877a3b Revert "add base64 package"
This reverts commit 4892eea009ee1ed73c27d783d2dc4e7adc735d11.
2024-08-07 22:14:08 -04:00
d2752216f6 Merge pull request #2253 from Infisical/revert-2252-maidul-dhusduqwdhj
Revert "Patch CLI auto select file vault "
2024-08-07 22:13:00 -04:00
d91fb0db02 Revert "Patch CLI auto select file vault " 2024-08-07 22:12:50 -04:00
4892eea009 add base64 package 2024-08-07 19:06:25 -04:00
09c6fcb73b Merge pull request #2252 from Infisical/maidul-dhusduqwdhj
Patch CLI auto select file vault
2024-08-07 19:03:38 -04:00
79181a1e3d remove os 2024-08-07 23:03:14 +00:00
bb934ef7b1 set vault type when auto selection enabled 2024-08-07 23:02:35 +00:00
cd9316537d prevent auto saving passphrase to disk 2024-08-07 18:56:15 -04:00
942e5f2f65 update phrase 2024-08-07 18:35:57 -04:00
353d231a4e Patch CLI auto select file vault
# Description 📣

When we auto select file vault, we also need to set it's type. When we set the type, we don't need to fall back to file vault in the `GetValueInKeyring` and `DeleteValueInKeyring` because `currentVaultBackend` will be `file`.

Also rephrased the text asking the user to eneter a passphrase.
2024-08-07 18:35:07 -04:00
68e05b7198 add debug log to print keyring error 2024-08-07 14:51:55 -04:00
4f998e3940 Merge pull request #2251 from akhilmhdh/fix/replication
fix: resolved replication secret not getting deleted
2024-08-07 11:57:14 -04:00
=
1248840dc8 fix: resolved replication secret not getting deleted 2024-08-07 21:23:22 +05:30
64c8125e4b add external secrets operator mention in k8s docs 2024-08-07 11:13:02 -04:00
8d33647739 Merge pull request #2249 from Infisical/maidul-sqhdqwdgvqwjf
patch findProjectUserWorkspaceKey
2024-08-06 22:12:03 +05:30
d1c142e5b1 patch findProjectUserWorkspaceKey 2024-08-06 12:39:06 -04:00
bb1cad0c5b Merge pull request #2223 from Infisical/misc/add-org-level-rate-limit
misc: moved to license-plan-based rate limits
2024-08-06 10:42:57 -04:00
2a1cfe15b4 update text when secrets deleted after integ delete 2024-08-06 10:07:41 -04:00
881d70bc64 Merge pull request #2238 from Infisical/feat/enabled-secrets-deletion-on-integ-removal
feat: added secrets deletion feature on integration removal
2024-08-06 09:54:15 -04:00
902a0b0ed4 Merge pull request #2243 from akhilmhdh/fix/missing-coment-field 2024-08-06 08:18:18 -04:00
ba92192537 misc: removed creation limits completely 2024-08-06 19:41:09 +08:00
26ed8df73c misc: finalized list of license rate limits 2024-08-06 19:14:49 +08:00
c1decab912 misc: addressed comments 2024-08-06 18:58:07 +08:00
=
216c073290 fix: missing comment key in updated project 2024-08-06 16:14:25 +05:30
1070954bdd misc: used destructuring 2024-08-06 02:05:13 +08:00
cc689d3178 feat: added secrets deletion feature on integration removal 2024-08-06 01:52:58 +08:00
e6848828f2 Merge pull request #2184 from Infisical/daniel/keyring-cli-improvements
feat(cli): persistant `file` vault passphrase
2024-08-05 13:13:29 -04:00
c8b93e4467 Update doc to show correct command 2024-08-05 13:11:40 -04:00
0bca24bb00 Merge pull request #2235 from Infisical/handbook-update
add meetings article to handbook
2024-08-05 12:42:07 -04:00
c563ada50f Merge pull request #2237 from akhilmhdh/fix/bot-creation-failing
fix: resolved auto bot create failing on update
2024-08-05 11:15:25 -04:00
=
26d1616e22 fix: resolved auto bot create failing on update 2024-08-05 20:41:19 +05:30
5fd071d1de Merge pull request #2225 from akhilmhdh/feat/org-project-management
Feat/org project management
2024-08-05 10:21:09 -04:00
a6ac78356b rename org admin console subject name 2024-08-05 10:03:33 -04:00
e4a2137991 update permission action name for org admin console 2024-08-05 10:01:15 -04:00
9721d7a15e add meetings article to handbook 2024-08-04 14:04:09 -07:00
93db5c4555 Merge pull request #2234 from Infisical/maidul-mdjhquwqjhd
update broken image in ksm docs
2024-08-04 11:48:16 -04:00
ad4393fdef update broken image in ksm docs 2024-08-04 11:46:58 -04:00
cd06e4e7f3 hot patch 2024-08-03 19:05:34 -04:00
711a4179ce rename admin panel 2024-08-03 07:52:35 -04:00
=
b4a2a477d3 feat: brought back workspace permission and made requested changes 2024-08-03 14:55:30 +05:30
8e53a1b171 Merge pull request #2232 from Infisical/daniel/fix-lint
Fix: Linting
2024-08-02 22:00:28 -04:00
71af463ad8 fix format 2024-08-03 03:49:47 +02:00
7abd18b11c Merge pull request #2219 from LemmyMwaura/parse-secret-on-paste
feat: parse secrets (key,value) on paste
2024-08-03 03:33:17 +02:00
1aee50a751 Fix: Parser improvements and lint fixes 2024-08-03 03:29:45 +02:00
0f23b7e1d3 misc: added check for undefined orgId 2024-08-03 02:10:47 +08:00
e9b37a1f98 Merge pull request #2227 from Vishvsalvi/deleteActionModal-Placeholder
Placeholder value is same as it's label
2024-08-02 14:04:40 -04:00
33193a47ae misc: updated default onprem rate limits 2024-08-03 01:52:04 +08:00
43fded2350 refactor: take into account other delimiters 2024-08-02 20:41:47 +03:00
7b6f4d810d Placeholder value is same as it's label 2024-08-02 20:51:08 +05:30
1ad286ca87 misc: name updates and more comments 2024-08-02 22:58:53 +08:00
be7c11a3f5 Merge remote-tracking branch 'origin/main' into misc/add-org-level-rate-limit 2024-08-02 22:42:23 +08:00
=
b97bbe5beb feat: text change in sidebar 2024-08-02 19:54:43 +05:30
=
cf5260b383 feat: minor bug fix on access operation 2024-08-02 19:54:42 +05:30
=
13e0dd8e0f feat: completed org admin based project access feature 2024-08-02 19:54:42 +05:30
7f9150e60e Merge pull request #2226 from Infisical/maidul-wdqwdwf
Update docker-compose to docker compose in GHA
2024-08-02 19:54:17 +05:30
995f0360fb update docker-compsoe to docker compose 2024-08-02 10:22:21 -04:00
ecab69a7ab Merge pull request #2213 from Infisical/issue-cert-csr
Add Sign Certificate Endpoint for Certificate Issuance
2024-08-02 07:16:17 -07:00
cca36ab106 Merge remote-tracking branch 'origin' into issue-cert-csr 2024-08-02 07:06:58 -07:00
76311a1b5f Update DN parsing fn 2024-08-02 07:00:36 -07:00
55a6740714 misc: moved to plan-based rate limit 2024-08-02 21:37:48 +08:00
a0490d0fde Merge pull request #2220 from Infisical/feat/added-secret-folder-rbac
feat: added secret folder permissions
2024-08-02 19:05:12 +08:00
78e41a51c0 update workspace to project 2024-08-01 17:29:33 -04:00
8414f04e94 Merge pull request #2221 from akhilmhdh/feat/remove-migration-webhooks
feat: resolved invite failing and removed all unused things from frontend for previous upgrade
2024-08-01 11:18:50 -04:00
=
79e414ea9f feat: resolved invite failing and removed all unused things from frontend on previous upgrade 2024-08-01 20:12:23 +05:30
83772c1770 Merge pull request #2218 from GLEF1X/refactor/required-key-secret-input
refactor(secret-key-input): pass `isRequired` prop to secret key input
2024-08-01 10:35:23 -04:00
09928efba3 feat: added secret folder rbac' 2024-08-01 22:24:35 +08:00
48eb4e772f Merge pull request #2217 from akhilmhdh/feat/remove-migration-webhooks
feat: removed all the migration done for webhook and dynamic secret to KMS
2024-08-01 09:26:49 -04:00
7467a05fc4 fix(lint): fix triple equal strict check 2024-08-01 14:42:15 +03:00
afba636850 feat: parse full env secrets (key,value) when pasted from clipboard 2024-08-01 14:22:22 +03:00
96cc315762 refactor(secret-key-input): pass isRequired prop to secret key input 2024-08-01 06:22:49 -04:00
=
e95d7e55c1 feat: removed all the migration done for webhook and dynamic secret towards kms encryption 2024-08-01 13:39:41 +05:30
520c068ac4 Merge pull request #2209 from Infisical/doc/add-documentation-for-kms-with-aws-hsm
doc: added documentation for using AWS HSM
2024-07-31 21:37:23 -04:00
cf330777ed kms and hsm doc updates 2024-07-31 21:36:52 -04:00
c1eae42b26 update aws kms docs 2024-07-31 20:40:54 -04:00
9f0d7c6d11 Correct sign-certificate endpoint ref in docs 2024-07-31 14:04:52 -07:00
683e3dd7be Add sign certificate endpoint 2024-07-31 13:57:47 -07:00
46ca3856b3 change upgrade btn based on admin 2024-07-31 10:59:36 -04:00
891cb06de0 Update keyringwrapper.go 2024-07-31 16:55:53 +02:00
aff7481fbc doc: added documentation for using AWS HSM 2024-07-31 20:30:40 +08:00
e7c1a4d4a0 Merge pull request #2207 from Infisical/misc/added-error-prompt-for-fetch-secrets-kms
misc: added error prompt for fetch secrets issue with kms
2024-07-31 14:34:35 +05:30
27f9628dc5 misc: updated refetch interval 2024-07-31 17:02:28 +08:00
1866ce4240 misc: moved get project secrets error handling to hook 2024-07-31 16:48:48 +08:00
e6b6de5e8e misc: added error prompt for fetch secrets issue with kms 2024-07-31 16:31:37 +08:00
02e8f20cbf remove extra : 2024-07-31 03:14:06 +00:00
9184ec0765 Merge pull request #2206 from GLEF1X/refactor/hashicorp-vault-integration
refactor(hashicorp-integration): make hashicorp vault integration easier to use
2024-07-30 23:05:28 -04:00
1d55c7bcb0 refactor(integration): add aria-required to Input component 2024-07-30 22:07:50 -04:00
96cffd6196 refactor(integration): make hashicorp vault integration easier to use
* Makes `namespace` optional allowing to use self-hosted OSS hashicorp vault
2024-07-30 22:06:54 -04:00
5bb2866b28 Merge pull request #2199 from Infisical/secret-engine-v2-bridge
Secret engine v2 bridge
2024-07-30 21:41:21 -04:00
7a7841e487 Merge pull request #2202 from Infisical/daniel/tls-docs
docs(sdks): Custom TLS certificate support
2024-07-30 20:52:52 -04:00
b0819ee592 update agent functions docs 2024-07-30 20:50:35 -04:00
b4689bed17 fix docs typo 2024-07-30 20:11:30 -04:00
bfd24ea938 Merge pull request #2204 from Infisical/maidul-dig2urdy3
add single secret fetch for agent
2024-07-30 20:09:32 -04:00
cea1a5e7ea add docs for single and list secrets functions for agent 2024-07-30 20:01:52 -04:00
8d32ca2fb6 Merge pull request #2205 from Infisical/vmatsiiako-docs-patch-1
Update migrating-from-envkey.mdx
2024-07-30 16:25:56 -07:00
d468067d43 Update migrating-from-envkey.mdx 2024-07-30 16:24:47 -07:00
3a640d6cf8 add single secret fetch for agent 2024-07-30 19:23:24 -04:00
8fc85105a9 Merge pull request #2203 from Infisical/secret-sharing-fix-padding
Add More Padding to Secret Sharing Banner
2024-07-30 13:49:29 -07:00
48bd354bae Add more padding for secret sharing promo banner 2024-07-30 13:46:40 -07:00
6e1dc7375c Update csharp.mdx 2024-07-30 22:24:43 +02:00
164627139e TLS docs 2024-07-30 22:24:23 +02:00
=
f7c962425c feat: renamed migrations to be latest 2024-07-30 23:54:07 +05:30
=
d92979d50e feat: resolved rebase ts errors 2024-07-30 23:50:04 +05:30
021dbf3558 Merge pull request #2200 from Infisical/secret-sharing-fix
Minor UI Improvements
2024-07-30 11:17:53 -07:00
=
29060ffc9e feat: added a success message on upgrade success 2024-07-30 23:19:33 +05:30
=
d9c7724857 feat: removed replica node from delete db query 2024-07-30 23:19:33 +05:30
=
9063787772 feat: changed webhook and dynamic secret change to migration mode, resolved snapshot deletion issue in update 2024-07-30 23:19:33 +05:30
c821bc0e14 misc: address project set kms issue 2024-07-30 23:19:33 +05:30
83eed831da text rephrase 2024-07-30 23:19:32 +05:30
=
5c8d6157d7 feat: added logic for webhook and dynamic secret to use the kms encryption 2024-07-30 23:19:32 +05:30
=
5d78b6941d feat: made encryption tab hidden for project v2 and v1 2024-07-30 23:19:32 +05:30
=
1d09d4cdfd feat: resolved a edge case on snapshot based secret version insertion due to missing snapshots in some parts 2024-07-30 23:19:32 +05:30
=
9877444117 feat: updated operator version title for migration 2024-07-30 23:19:32 +05:30
=
6f2ae344a7 feat: correction in test command 2024-07-30 23:19:32 +05:30
=
549d388f59 feat: improved migration wizard to info user prerequisite check list 2024-07-30 23:19:32 +05:30
=
e2caa98c74 feat: finished migrator logic 2024-07-30 23:19:32 +05:30
=
6bb41913bf feat: completed migration backend logic 2024-07-30 23:19:31 +05:30
=
844a4ebc02 feat: sanitized project schema on routes to avoid exposing encrypted keys 2024-07-30 23:19:31 +05:30
=
b37f780c4c feat: added auto bot creator when bot is missing by taking the user old server encrypted private key 2024-07-30 23:19:31 +05:30
=
6e7997b1bd feat: added kms deletion on project deletion and removed e2ee blind index upgrade banner 2024-07-30 23:19:08 +05:30
=
e210a6a24f feat: added missing index in secret v2 2024-07-30 23:19:08 +05:30
=
b950bf0cf7 feat: added back secret referencing expansion support 2024-07-30 23:19:07 +05:30
=
a53d0b2334 feat: added first version of migrator to secret v2 2024-07-30 23:19:07 +05:30
=
ab88e6c414 checkpoint 2024-07-30 23:19:07 +05:30
49eb6d6474 misc: removed minimum requirements for kms description 2024-07-30 23:19:07 +05:30
05d7e26f8b misc: addressed minor kms issues 2024-07-30 23:19:07 +05:30
=
6a156371c0 feat: resolved bug reported on search and integration failing due to typo in integration field 2024-07-30 23:19:07 +05:30
=
8435b20178 feat: added test case for secret v2 with raw endpoints 2024-07-30 23:19:07 +05:30
=
7d7fcd0db6 feat: resolved failing testcases 2024-07-30 23:19:06 +05:30
=
b5182550da feat: lint fix 2024-07-30 23:19:06 +05:30
=
3e0ae5765f feat: updated kms service to return only kms details and some more minor changes 2024-07-30 23:19:06 +05:30
=
f7ef86eb11 feat: fixed secret approval for architecture v2 2024-07-30 23:19:06 +05:30
=
acf9a488ac feat: secret v2 architecture for secret rotation 2024-07-30 23:19:06 +05:30
=
4a06e3e712 feat: testing v2 architecture changes and corrections as needed 2024-07-30 23:19:06 +05:30
=
b7b0e60b1d feat: ui removed all private key except secret rotation to raw endpoints version 2024-07-30 23:19:05 +05:30
=
d4747abba8 feat: resolved concurrent bug with kms management 2024-07-30 23:19:05 +05:30
=
641860cdb8 feat: resolved all ts issues on router schema and other functions 2024-07-30 23:19:05 +05:30
=
36ac1f47ca feat: all the services are now working with secrets v2 architecture 2024-07-30 23:17:41 +05:30
=
643d13b0ec checkpoint 2024-07-30 23:11:04 +05:30
=
ef2816b2ee feat: added bridge logic in secret replication, snapshot and approval for raw endpoint 2024-07-30 23:11:04 +05:30
=
9e314d7a09 feat: migration updated for secret v2 snapshot and secret approval 2024-07-30 23:03:56 +05:30
=
8eab27d752 feat: added kms encryption and decryption secret bridge 2024-07-30 23:03:56 +05:30
=
b563c4030b feat: created base for secret v2 bridge and plugged it to secret-router 2024-07-30 23:03:56 +05:30
=
761a0f121c feat: added new secret v2 data structures 2024-07-30 23:03:56 +05:30
70400ef369 misc: made kms description optional 2024-07-30 23:03:56 +05:30
9aecfe77ad doc: added aws permission setup doc for kms 2024-07-30 23:03:56 +05:30
cedeb1ce27 doc: initial docs for kms 2024-07-30 23:03:55 +05:30
0e75a8f6d7 misc: made kms hook generic 2024-07-30 23:03:55 +05:30
a5b030c4a7 misc: renamed project method 2024-07-30 23:03:55 +05:30
4009580cf2 misc: removed kms from service 2024-07-30 23:03:55 +05:30
64869ea8e0 misc: created abstraction for get kms by id 2024-07-30 23:03:55 +05:30
ffc1b1ec1c misc: modified design of advanced settings 2024-07-30 23:03:55 +05:30
880a689376 misc: finalized project backup prompts 2024-07-30 23:03:54 +05:30
3709f31b5a misc: added empty metadata 2024-07-30 23:03:54 +05:30
6b6fd9735c misc: added ability for users to select KMS during project creation 2024-07-30 23:03:54 +05:30
a57d1f1c9a misc: modified modal text 2024-07-30 23:03:54 +05:30
6c06de6da4 misc: addressed type issue with audit log 2024-07-30 23:03:54 +05:30
0c9e979fb8 feat: load project kms backup 2024-07-30 23:03:54 +05:30
32fc254ae1 misc: added UI for load backup 2024-07-30 23:03:53 +05:30
69d813887b misc: added audit logs for kms backup and other minor edits 2024-07-30 23:03:53 +05:30
80be054425 misc: developed create kms backup feature 2024-07-30 23:03:53 +05:30
4d032cfbfa misc: made project key and data key creation concurrency safe 2024-07-30 23:03:53 +05:30
d41011e056 misc: made org key and data key concurrency safe 2024-07-30 23:03:53 +05:30
d918f3ecdf misc: finalized switching of project KMS 2024-07-30 23:03:53 +05:30
7e5c3e8163 misc: partial project kms switch 2024-07-30 23:03:52 +05:30
cb347aa16a misc: changed order of aws validate connection and creation 2024-07-30 23:03:14 +05:30
88a7cc3068 misc: added audit logs for external kms 2024-07-30 23:03:14 +05:30
4ddfb05134 misc: added license checks for external kms management 2024-07-30 23:03:14 +05:30
7bb0ec0111 misc: migrated to dedicated org permissions for kms management 2024-07-30 23:03:13 +05:30
31af4a4602 misc: minor UI updates 2024-07-30 23:03:13 +05:30
dd46a21035 feat: finalized kms settings in org-level 2024-07-30 23:03:13 +05:30
26a5d74b14 misc: modified encryption/decryption of external kms config 2024-07-30 23:03:13 +05:30
7e9389cb26 Made with love 2024-07-30 10:32:58 -07:00
eda57881ec Minor UI adjustments 2024-07-30 10:31:30 -07:00
5eafdba6c8 Merge remote-tracking branch 'akhilmhdh/feat/aws-kms-sm' into feat/integrate-external-kms 2024-07-30 23:01:13 +05:30
9c4bb79472 misc: connected aws add kms 2024-07-30 23:01:13 +05:30
937b0c0a7c feat: added initial aws form 2024-07-30 23:01:12 +05:30
=
cb132f4c65 fix: resolving undefined secret key 2024-07-30 23:01:12 +05:30
=
4caa77e28a refactor(ui): migrated secret endpoints of e2ee to raw 2024-07-30 23:01:12 +05:30
=
547be80dcf feat: made raw secret endpoints and normal e2ee ones to be same functionality 2024-07-30 23:01:12 +05:30
2cbae96c9a feat: added project data key 2024-07-30 23:01:12 +05:30
553d51e5b3 Merge pull request #2198 from Infisical/maidul-dwdqwdfwef
Lint fixes to unblock prod pipeline
2024-07-30 11:06:01 -04:00
16e0a441ae unblock prod pipeline 2024-07-30 11:00:27 -04:00
d6c0941fa9 Merge pull request #2190 from Infisical/secret-sharing-update
Secret Sharing Update
2024-07-30 07:27:56 -07:00
7cbd254f06 Add back hashed hex for secret sharing 2024-07-30 07:16:03 -07:00
4b83b92725 Merge pull request #2196 from Infisical/handbook-update
add envkey migration page
2024-07-30 08:54:40 -04:00
fe72f034c1 Update migrating-from-envkey.mdx 2024-07-30 08:54:22 -04:00
d5f4ce4376 Update vault.go 2024-07-30 10:22:15 +02:00
6803553b21 add envkey migration page 2024-07-29 23:23:05 -07:00
1c8299054a Merge pull request #2192 from GLEF1X/perf/optimize-group-delete
perf(group-fns): optimize sequential delete to be concurrent
2024-07-29 22:13:00 -04:00
85653a90d5 update phrasing 2024-07-29 22:06:03 -04:00
98b6373d6a perf(group-fns): optimize sequential delete to be concurrent 2024-07-29 21:40:48 -04:00
1d97921c7c Merge pull request #2182 from LemmyMwaura/delete-secret-modal
feat: add confirm step (modal) before deleting a secret
2024-07-29 19:52:51 -04:00
0d4164ea81 Merge remote-tracking branch 'origin' into secret-sharing-update 2024-07-29 15:22:13 -07:00
79bd8613d3 Fix padding 2024-07-29 15:16:11 -07:00
8deea21a83 Bring back logo, promo text in secret sharing 2024-07-29 15:05:38 -07:00
3b3c2be933 Merge pull request #2186 from LemmyMwaura/persist-tab-state
feat: persist tab state on route change.
2024-07-29 17:35:07 -04:00
c041e44399 Continue secret sharing 2024-07-29 14:32:11 -07:00
c1aeb04174 Merge pull request #2188 from Infisical/vmatsiiako-changelog-patch-1
Update changelog
2024-07-29 17:26:28 -04:00
3f3c0aab0f refactor: revert the org level enum to only types that existed before 2024-07-29 20:04:58 +03:00
b740e8c900 Rename types to Types with correct case 2024-07-29 20:02:42 +03:00
4416b11094 refactor: change folder name to uppercase for consistency 2024-07-29 19:48:49 +03:00
d8169a866d refactor: update types import path 2024-07-29 19:41:02 +03:00
7239158e7f refactor: localize tabs at both the org and project level 2024-07-29 19:37:19 +03:00
879ef2c178 Update keyringwrapper.go 2024-07-29 12:37:58 +02:00
8777cfe680 Update keyringwrapper.go 2024-07-29 12:34:35 +02:00
2b630f75aa Update keyringwrapper.go 2024-07-29 12:31:02 +02:00
91cee20cc8 Minor improvemnets 2024-07-29 12:21:38 +02:00
4249ec6030 Update login.go 2024-07-29 12:21:31 +02:00
e7a95e6af2 Update login.go 2024-07-29 12:15:53 +02:00
a9f04a3c1f Update keyringwrapper.go 2024-07-29 12:13:40 +02:00
3d380710ee Update keyringwrapper.go 2024-07-29 12:10:42 +02:00
2177ec6bcc Update vault.go 2024-07-29 12:04:34 +02:00
fefe2d1de1 Update changelog 2024-07-28 10:53:44 -07:00
3f3e41282d fix: remove unnecessary selectedTab div 2024-07-28 20:33:17 +03:00
c14f94177a Merge pull request #2187 from Infisical/vmatsiiako-changelog-update-july2024
Update changelog
2024-07-28 10:14:59 -07:00
ceb741955d Update changelog 2024-07-28 10:08:58 -07:00
f5bc4e1b5f refactor: return value as Tabsection from isTabSection fn (avoids assertion at setState level) 2024-07-28 07:50:27 +03:00
06900b9c99 refactor: create helper fn to check if string is in TabSections 2024-07-28 07:14:57 +03:00
d71cb96adf fix(lint): resolve type error 2024-07-27 23:33:09 +03:00
61ebec25b3 refactor: update envs to environments 2024-07-27 23:24:10 +03:00
57320c51fb fix: add selectedtab when moving back from roles page 2024-07-27 23:10:12 +03:00
4aa9cd0f72 feat: also persist the state on delete 2024-07-27 22:58:36 +03:00
ea39ef9269 feat: persist state at the org level when tab switching 2024-07-27 22:45:53 +03:00
15749a1f52 feat: update url onvalue change 2024-07-27 22:18:56 +03:00
9e9aff129e feat: use shared enum for consistent values 2024-07-27 22:12:19 +03:00
4ac487c974 feat: selectTab state from url 2024-07-27 22:04:43 +03:00
2e50072caa feat: move shared enum to separate file 2024-07-27 22:04:11 +03:00
2bd170df7d feat: add queryparam when switching tabs 2024-07-27 22:03:44 +03:00
938a7b7e72 Merge pull request #2185 from Infisical/secret-sharing
Secret Sharing UI/UX Adjustment
2024-07-27 10:09:03 -07:00
af864b456b Adjust secret sharing screen form padding 2024-07-27 07:32:56 -07:00
a30e3874cd Adjustments to secret sharing styling 2024-07-27 07:31:30 -07:00
de886f8dd0 feat: make title dynamic when deleting folders and secrets 2024-07-27 12:27:06 +03:00
b3db29ac37 refactor: update modal message to match other delete modals in the dashboard 2024-07-27 11:42:30 +03:00
070eb2aacd Update keyringwrapper.go 2024-07-26 22:47:46 +02:00
e619cfa313 feat(cli): set persistent file vault password 2024-07-26 22:47:37 +02:00
c3038e3ca1 docs: passphrase command 2024-07-26 22:47:07 +02:00
ce1db38afd refactor: re-use existing modal for deletion 2024-07-26 22:05:44 +03:00
0fa6b7a08a Merge pull request #2183 from Infisical/project-role-concept
Project Role Page
2024-07-26 11:27:25 -07:00
29c5bf5491 Remove top margin from RolePermissionSecretsRow 2024-07-26 11:22:15 -07:00
4d711ae149 Finish project role page 2024-07-26 11:00:47 -07:00
ff0e7feeee feat(cli): CLI Keyring improvements 2024-07-26 19:14:21 +02:00
9dd675ff98 refactor: move delete statement into body tag 2024-07-26 19:56:31 +03:00
8fd3e50d04 feat: implement delete secret via modal logic 2024-07-26 19:48:30 +03:00
391ed0723e feat: add delete secret modal 2024-07-26 19:47:35 +03:00
84af8e708e Merge remote-tracking branch 'origin' into project-role-concept 2024-07-26 07:28:17 -07:00
b39b5bd1a1 Merge pull request #2181 from Infisical/patch-org-role-update
Fix updating org role details should not send empty array of permissions
2024-07-26 07:27:51 -07:00
b3d9d91b52 Fix updating org role details should not send empty array of permissions 2024-07-26 06:52:21 -07:00
5ad4061881 Continue project role page 2024-07-26 06:43:09 -07:00
f29862eaf2 Merge pull request #2180 from Infisical/list-ca-endpoint-descriptions
Add descriptions for parameters for LIST (GET) CAs / certificates endpoints
2024-07-25 17:59:57 -04:00
7cb174b644 Add descriptions for list cas/certs endpoints 2024-07-25 14:53:41 -07:00
bf00d16c80 Continue progress on project role page 2024-07-25 14:45:02 -07:00
e30a0fe8be Merge pull request #2178 from Infisical/cert-search-filtering
Add List CAs / Certificates to Documentation + Filter Options
2024-07-25 09:40:44 -07:00
6e6f0252ae Adjust default offsets for cas/certs query 2024-07-25 08:09:21 -07:00
2348df7a4d Add list cert, ca + logical filters to docs 2024-07-25 08:06:18 -07:00
962cf67dfb Merge pull request #2173 from felixtrav/patch-1
Update envars.mdx - Added PORT
2024-07-25 10:21:06 -04:00
32627c20c4 Merge pull request #2176 from Infisical/org-role-cleanup
Cleanup frontend unused org role logic (moved)
2024-07-25 07:17:56 -07:00
c50f8fd78c Merge pull request #2175 from akhilmhdh/feat/cli-login-fallback-missing
Missing paste token option in CLI brower login flow
2024-07-25 10:08:57 -04:00
1cb4dc9e84 Start project role concept 2024-07-25 06:47:18 -07:00
977ce09245 Cleanup frontend unused org role logic (moved) 2024-07-25 05:43:57 -07:00
=
08d7dead8c fix(cli): resolved not printing the url on api override 2024-07-25 15:28:54 +05:30
=
a30e06e392 feat: added back missing token paste option in cli login from browser 2024-07-25 15:28:29 +05:30
23f3f09cb6 temporarily remove linux deployment 2024-07-24 23:42:36 -04:00
5cd0f665fa Update envars.mdx - Added PORT
Added the PORT configuration option to the documentation which controls the port the application listens on.
2024-07-24 19:17:33 -04:00
443e76c1df Merge pull request #2171 from Infisical/daniel/aarch64-binary-fix
fix(binary): aarch64 binary native bindings fix
2024-07-24 16:33:15 +02:00
4ea22b6761 Updated ubuntu version 2024-07-24 14:17:19 +00:00
ae7e0d0963 Merge pull request #2168 from Infisical/misc/added-email-self-host-conditionals
misc: added checks for formatting email templates for self-hosted or cloud
2024-07-24 09:22:49 -04:00
ed6c6d54c0 Update build-binaries.yml 2024-07-24 11:16:58 +02:00
428ff5186f Removed compression for testing 2024-07-24 10:47:20 +02:00
d07b0d20d6 Update build-binaries.yml 2024-07-24 10:46:55 +02:00
8e373fe9bf misc: added email formatting for remaining templates 2024-07-24 16:33:41 +08:00
28087cdcc4 misc: added email self-host conditionals 2024-07-24 00:55:02 +08:00
dcef49950d Merge pull request #2167 from Infisical/daniel/ruby-docs
feat(docs): Ruby sdk
2024-07-23 08:36:32 -07:00
1e5d567ef7 Update ruby.mdx 2024-07-23 15:30:13 +02:00
d09c320150 fix: bad documentation link 2024-07-23 15:27:23 +02:00
229599b8de docs: ruby sdk documentation 2024-07-23 15:27:11 +02:00
02eea4d886 Merge pull request #2166 from Infisical/misc/updated-cf-worker-integration-doc
misc: updated cf worker integration doc
2024-07-23 21:16:56 +08:00
d12144a7e7 misc: added highligting 2024-07-23 21:03:46 +08:00
5fa69235d1 misc: updated cf worker integration doc 2024-07-23 20:40:07 +08:00
7dd9337b1c Merge pull request #2165 from Infisical/daniel/deployment-doc-fix
chore(docs): typo in url
2024-07-23 10:19:59 +02:00
f9eaee4dbc Merge pull request #2164 from Infisical/daniel/deployment-doc-fix
chore(docs): remove redundant doc
2024-07-23 09:55:10 +02:00
cd3a64f3e7 Update standalone-binary.mdx 2024-07-23 09:52:42 +02:00
121254f98d Update standalone-binary.mdx 2024-07-23 09:52:14 +02:00
1591c1dbac Merge pull request #2163 from Infisical/misc/add-endpoint-for-terraform-environment
misc: add endpoint for environment terraform resource
2024-07-23 15:39:20 +08:00
3c59d288c4 misc: readded auth 2024-07-23 15:33:09 +08:00
632b775d7f misc: removed api key auth from get env by id 2024-07-23 15:28:58 +08:00
d66da3d770 misc: removed deprecated auth method 2024-07-23 15:23:50 +08:00
da43f405c4 Merge pull request #2162 from Infisical/role-concept
Organization Role Page
2024-07-23 12:26:43 +07:00
7a642e7634 Merge pull request #2161 from Infisical/daniel/cli-security-warning
fix(cli): dependency security warning
2024-07-22 21:36:48 +02:00
de686acc23 misc: add endpoint for environment terraform resource 2024-07-23 02:35:37 +08:00
4abb3ef348 Fix 2024-07-22 13:42:59 +02:00
73e764474d Update go.sum 2024-07-22 13:42:27 +02:00
7eb5689b4c Update go.mod 2024-07-22 13:42:24 +02:00
5d945f432d Merge pull request #2160 from Infisical/daniel/minor-ui-change
chore(ui): Grammar fix
2024-07-22 13:39:07 +02:00
1066710c4f Fix: Rename tips to tip 2024-07-22 13:14:25 +02:00
37137b8c68 Merge pull request #2156 from akhilmhdh/feat/login-cli-token-paste
Feat/login cli token paste
2024-07-21 23:37:41 -04:00
8b10cf863d add verify jwt, update text phrasing and fix double render input field 2024-07-21 23:35:09 -04:00
=
eb45bed7d9 feat: updated text over cli 2024-07-21 22:34:23 +05:30
=
1ee65205a0 feat: ui option to paste token on sending token to cli fails 2024-07-21 19:25:41 +05:30
=
f41272d4df feat: added option for manually adding token in browser login failure 2024-07-21 19:24:54 +05:30
8bf4df9f27 Merge pull request #2123 from akhilmhdh/cli-operation-to-raw
Switched CLI and K8s operator to secret raw endpoint
2024-07-20 13:19:13 -04:00
037a8f2ebb Merge branch 'main' into cli-operation-to-raw 2024-07-20 13:15:56 -04:00
14bc436283 Merge pull request #2155 from Infisical/fix/import-failing 2024-07-20 09:19:10 -04:00
a108c7dde1 fix: resolved get secret failing when import is invalid 2024-07-20 14:42:26 +05:30
54ccd73d2a Merge pull request #2153 from aheruz/feat/secret-request-bypass-reason
feat: add bypass reason on bypassed secret requests
2024-07-19 17:22:18 -04:00
729ca7b6d6 small nits for bypass pr 2024-07-19 17:19:06 -04:00
754db67f11 update chart version 2024-07-19 16:47:21 -04:00
f97756a07b doc: approval workflows revamp 2024-07-19 22:14:40 +02:00
22df51ab8e feat(frontend): send bypass reason on bypassed merges 2024-07-19 21:33:41 +02:00
bff8f55ea2 feat(backend): save bypass reason on secret_approval_requests 2024-07-19 21:31:48 +02:00
2f17f5e7df update bot not found message 2024-07-19 14:50:16 -04:00
72d2247bf2 add support for --projectId when user is logged in on secrets set 2024-07-19 12:55:52 -04:00
4ecd4c0337 Merge pull request #2152 from aheruz/feat/ENG-985-secret-share-organization
feat(ENG-985): secret share within organization
2024-07-19 10:30:32 -04:00
=
538613dd40 feat: added bot error message for old instance 2024-07-19 19:59:16 +05:30
4c5c24f689 feat: remove accessType hardcap in migration + style 2024-07-19 16:19:45 +02:00
dead16a98a Merge pull request #2147 from Infisical/daniel/deployment-documentation
docs(deployment): Standalone and high availability deployment
2024-07-19 08:28:26 -04:00
224368b172 fix: general access accessible only from private creation 2024-07-19 12:20:46 +02:00
2f2c9d4508 style: message structure on SecretTable 2024-07-19 03:36:24 +02:00
774017adbe style: remove logs 2024-07-19 03:32:46 +02:00
f9d1d9c89f doc: Adding accessType on secret sharing 2024-07-19 03:26:26 +02:00
eb82fc0d9a feat(frontend): Adding accessType on secret sharing 2024-07-19 03:17:47 +02:00
e45585a909 feat(backend): Adding accessType on secret sharing 2024-07-19 03:15:38 +02:00
6f0484f074 update bot key message 2024-07-18 18:29:39 -04:00
4ba529f22d print error message when projectId flag is not passed for set secret 2024-07-18 18:05:01 -04:00
5360fb033a fix set secret 2024-07-18 17:53:25 -04:00
7622cac07e Update high-availability.mdx 2024-07-18 09:58:48 +02:00
a101602e0a Update overview.mdx 2024-07-18 08:25:37 +02:00
ca63a7baa7 Standalone binary docs 2024-07-18 08:25:31 +02:00
ff4f15c437 HA deployment docs 2024-07-18 08:25:26 +02:00
d6c2715852 Update mint.json 2024-07-18 08:25:14 +02:00
fc386c0cbc Create haproxy-stats.png 2024-07-18 08:25:11 +02:00
263a88379f Create ha-stack.png 2024-07-18 08:25:07 +02:00
=
1f6c33bdb8 feat: updated bot not found error message 2024-07-18 01:46:28 +05:30
=
1d6d424c91 fix: removed print not used 2024-07-16 14:08:09 +05:30
=
c39ea130b1 feat: changed backup secret to keyring and resolved backup not working in previous versions 2024-07-16 14:05:38 +05:30
=
5514508482 refactor(cli): removed unused secret logic for e2ee used 2024-07-15 01:23:47 +05:30
=
5921dcaa51 feat: switched operator to raw endpoints for secret management 2024-07-15 01:23:03 +05:30
=
b2c62c4193 feat(cli): changed all secret endpoint to raw endpoint 2024-07-14 15:05:53 +05:30
426 changed files with 20662 additions and 7582 deletions

View File

@ -14,7 +14,6 @@ defaults:
jobs:
build-and-deploy:
runs-on: ubuntu-20.04
strategy:
matrix:
arch: [x64, arm64]
@ -24,6 +23,7 @@ jobs:
target: node20-linux
- os: win
target: node20-win
runs-on: ${{ (matrix.arch == 'arm64' && matrix.os == 'linux') && 'ubuntu24-arm64' || 'ubuntu-latest' }}
steps:
- name: Checkout code
@ -49,9 +49,9 @@ jobs:
- name: Package into node binary
run: |
if [ "${{ matrix.os }}" != "linux" ]; then
pkg --no-bytecode --public-packages "*" --public --compress Brotli --target ${{ matrix.target }}-${{ matrix.arch }} --output ./binary/infisical-core-${{ matrix.os }}-${{ matrix.arch }} .
pkg --no-bytecode --public-packages "*" --public --target ${{ matrix.target }}-${{ matrix.arch }} --output ./binary/infisical-core-${{ matrix.os }}-${{ matrix.arch }} .
else
pkg --no-bytecode --public-packages "*" --public --compress Brotli --target ${{ matrix.target }}-${{ matrix.arch }} --output ./binary/infisical-core .
pkg --no-bytecode --public-packages "*" --public --target ${{ matrix.target }}-${{ matrix.arch }} --output ./binary/infisical-core .
fi
# Set up .deb package structure (Debian/Ubuntu only)
@ -84,7 +84,12 @@ jobs:
mv infisical-core.deb ./binary/infisical-core-${{matrix.arch}}.deb
- uses: actions/setup-python@v4
- run: pip install --upgrade cloudsmith-cli
with:
python-version: "3.x" # Specify the Python version you need
- name: Install Python dependencies
run: |
python -m pip install --upgrade pip
pip install --upgrade cloudsmith-cli
# Publish .deb file to Cloudsmith (Debian/Ubuntu only)
- name: Publish to Cloudsmith (Debian/Ubuntu)

View File

@ -22,14 +22,14 @@ jobs:
# uncomment this when testing locally using nektos/act
- uses: KengoTODA/actions-setup-docker-compose@v1
if: ${{ env.ACT }}
name: Install `docker-compose` for local simulations
name: Install `docker compose` for local simulations
with:
version: "2.14.2"
- name: 📦Build the latest image
run: docker build --tag infisical-api .
working-directory: backend
- name: Start postgres and redis
run: touch .env && docker-compose -f docker-compose.dev.yml up -d db redis
run: touch .env && docker compose -f docker-compose.dev.yml up -d db redis
- name: Start the server
run: |
echo "SECRET_SCANNING_GIT_APP_ID=793712" >> .env
@ -72,6 +72,6 @@ jobs:
run: oasdiff breaking https://app.infisical.com/api/docs/json http://localhost:4000/api/docs/json --fail-on ERR
- name: cleanup
run: |
docker-compose -f "docker-compose.dev.yml" down
docker compose -f "docker-compose.dev.yml" down
docker stop infisical-api
docker remove infisical-api

View File

@ -20,7 +20,7 @@ jobs:
uses: actions/checkout@v3
- uses: KengoTODA/actions-setup-docker-compose@v1
if: ${{ env.ACT }}
name: Install `docker-compose` for local simulations
name: Install `docker compose` for local simulations
with:
version: "2.14.2"
- name: 🔧 Setup Node 20
@ -33,7 +33,7 @@ jobs:
run: npm install
working-directory: backend
- name: Start postgres and redis
run: touch .env && docker-compose -f docker-compose.dev.yml up -d db redis
run: touch .env && docker compose -f docker-compose.dev.yml up -d db redis
- name: Start integration test
run: npm run test:e2e
working-directory: backend
@ -44,4 +44,4 @@ jobs:
ENCRYPTION_KEY: 4bnfe4e407b8921c104518903515b218
- name: cleanup
run: |
docker-compose -f "docker-compose.dev.yml" down
docker compose -f "docker-compose.dev.yml" down

View File

@ -50,6 +50,6 @@ jobs:
CLI_TESTS_ENV_SLUG: ${{ secrets.CLI_TESTS_ENV_SLUG }}
CLI_TESTS_USER_EMAIL: ${{ secrets.CLI_TESTS_USER_EMAIL }}
CLI_TESTS_USER_PASSWORD: ${{ secrets.CLI_TESTS_USER_PASSWORD }}
INFISICAL_VAULT_FILE_PASSPHRASE: ${{ secrets.CLI_TESTS_INFISICAL_VAULT_FILE_PASSPHRASE }}
# INFISICAL_VAULT_FILE_PASSPHRASE: ${{ secrets.CLI_TESTS_INFISICAL_VAULT_FILE_PASSPHRASE }}
run: go test -v -count=1 ./test

View File

@ -0,0 +1,576 @@
import { SecretType } from "@app/db/schemas";
import { seedData1 } from "@app/db/seed-data";
import { AuthMode } from "@app/services/auth/auth-type";
type TRawSecret = {
secretKey: string;
secretValue: string;
secretComment?: string;
version: number;
};
const createSecret = async (dto: { path: string; key: string; value: string; comment: string; type?: SecretType }) => {
const createSecretReqBody = {
workspaceId: seedData1.projectV3.id,
environment: seedData1.environment.slug,
type: dto.type || SecretType.Shared,
secretPath: dto.path,
secretKey: dto.key,
secretValue: dto.value,
secretComment: dto.comment
};
const createSecRes = await testServer.inject({
method: "POST",
url: `/api/v3/secrets/raw/${dto.key}`,
headers: {
authorization: `Bearer ${jwtAuthToken}`
},
body: createSecretReqBody
});
expect(createSecRes.statusCode).toBe(200);
const createdSecretPayload = JSON.parse(createSecRes.payload);
expect(createdSecretPayload).toHaveProperty("secret");
return createdSecretPayload.secret as TRawSecret;
};
const deleteSecret = async (dto: { path: string; key: string }) => {
const deleteSecRes = await testServer.inject({
method: "DELETE",
url: `/api/v3/secrets/raw/${dto.key}`,
headers: {
authorization: `Bearer ${jwtAuthToken}`
},
body: {
workspaceId: seedData1.projectV3.id,
environment: seedData1.environment.slug,
secretPath: dto.path
}
});
expect(deleteSecRes.statusCode).toBe(200);
const updatedSecretPayload = JSON.parse(deleteSecRes.payload);
expect(updatedSecretPayload).toHaveProperty("secret");
return updatedSecretPayload.secret as TRawSecret;
};
describe.each([{ auth: AuthMode.JWT }, { auth: AuthMode.IDENTITY_ACCESS_TOKEN }])(
"Secret V2 Architecture - $auth mode",
async ({ auth }) => {
let folderId = "";
let authToken = "";
const secretTestCases = [
{
path: "/",
secret: {
key: "SEC1",
value: "something-secret",
comment: "some comment"
}
},
{
path: "/nested1/nested2/folder",
secret: {
key: "NESTED-SEC1",
value: "something-secret",
comment: "some comment"
}
},
{
path: "/",
secret: {
key: "secret-key-2",
value: `-----BEGIN PRIVATE KEY-----
MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQCa6eeFk+cMVqFn
hoVQDYgn2Ptp5Azysr2UPq6P73pCL9BzUtOXKZROqDyGehzzfg3wE2KdYU1Jk5Uq
fP0ZOWDIlM2SaVCSI3FW32o5+ZiggjpqcVdLFc/PS0S/ZdSmpPd8h11iO2brtIAI
ugTW8fcKlGSNUwx9aFmE7A6JnTRliTxB1l6QaC+YAwTK39VgeVH2gDSWC407aS15
QobAkaBKKmFkzB5D7i2ZJwt+uXJV/rbLmyDmtnw0lubciGn7NX9wbYef180fisqT
aPNAz0nPKk0fFH2Wd5MZixNGbrrpDA+FCYvI5doThZyT2hpj08qWP07oXXCAqw46
IEupNSILAgMBAAECggEBAIJb5KzeaiZS3B3O8G4OBQ5rJB3WfyLYUHnoSWLsBbie
nc392/ovThLmtZAAQE6SO85Tsb93+t64Z2TKqv1H8G658UeMgfWIB78v4CcLJ2mi
TN/3opqXrzjkQOTDHzBgT7al/mpETHZ6fOdbCemK0fVALGFUioUZg4M8VXtuI4Jw
q28jAyoRKrCrzda4BeQ553NZ4G5RvwhX3O2I8B8upTbt5hLcisBKy8MPLYY5LUFj
YKAP+raf6QLliP6KYHuVxUlgzxjLTxVG41etcyqqZF+foyiKBO3PU3n8oh++tgQP
ExOxiR0JSkBG5b+oOBD0zxcvo3/SjBHn0dJOZCSU2SkCgYEAyCe676XnNyBZMRD7
6trsaoiCWBpA6M8H44+x3w4cQFtqV38RyLy60D+iMKjIaLqeBbnay61VMzo24Bz3
EuF2n4+9k/MetLJ0NCw8HmN5k0WSMD2BFsJWG8glVbzaqzehP4tIclwDTYc1jQVt
IoV2/iL7HGT+x2daUwbU5kN5hK0CgYEAxiLB+fmjxJW7VY4SHDLqPdpIW0q/kv4K
d/yZBrCX799vjmFb9vLh7PkQUfJhMJ/ttJOd7EtT3xh4mfkBeLfHwVU0d/ahbmSH
UJu/E9ZGxAW3PP0kxHZtPrLKQwBnfq8AxBauIhR3rPSorQTIOKtwz1jMlHFSUpuL
3KeK2YfDYJcCgYEAkQnJOlNcAuRb/WQzSHIvktssqK8NjiZHryy3Vc0hx7j2jES2
HGI2dSVHYD9OSiXA0KFm3OTTsnViwm/60iGzFdjRJV6tR39xGUVcoyCuPnvRfUd0
PYvBXgxgkYpyYlPDcwp5CvWGJy3tLi1acgOIwIuUr3S38sL//t4adGk8q1kCgYB8
Jbs1Tl53BvrimKpwUNbE+sjrquJu0A7vL68SqgQJoQ7dP9PH4Ff/i+/V6PFM7mib
BQOm02wyFbs7fvKVGVJoqWK+6CIucX732x7W5yRgHtS5ukQXdbzt1Ek3wkEW98Cb
HTruz7RNAt/NyXlLSODeit1lBbx3Vk9EaxZtRsv88QKBgGn7JwXgez9NOyobsNIo
QVO80rpUeenSjuFi+R0VmbLKe/wgAQbYJ0xTAsQ0btqViMzB27D6mJyC+KUIwWNX
MN8a+m46v4kqvZkKL2c4gmDibyURNe/vCtCHFuanJS/1mo2tr4XDyEeiuK52eTd9
omQDpP86RX/hIIQ+JyLSaWYa
-----END PRIVATE KEY-----`,
comment:
"Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation"
}
},
{
path: "/nested1/nested2/folder",
secret: {
key: "secret-key-3",
value: `-----BEGIN PRIVATE KEY-----
MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQCa6eeFk+cMVqFn
hoVQDYgn2Ptp5Azysr2UPq6P73pCL9BzUtOXKZROqDyGehzzfg3wE2KdYU1Jk5Uq
fP0ZOWDIlM2SaVCSI3FW32o5+ZiggjpqcVdLFc/PS0S/ZdSmpPd8h11iO2brtIAI
ugTW8fcKlGSNUwx9aFmE7A6JnTRliTxB1l6QaC+YAwTK39VgeVH2gDSWC407aS15
QobAkaBKKmFkzB5D7i2ZJwt+uXJV/rbLmyDmtnw0lubciGn7NX9wbYef180fisqT
aPNAz0nPKk0fFH2Wd5MZixNGbrrpDA+FCYvI5doThZyT2hpj08qWP07oXXCAqw46
IEupNSILAgMBAAECggEBAIJb5KzeaiZS3B3O8G4OBQ5rJB3WfyLYUHnoSWLsBbie
nc392/ovThLmtZAAQE6SO85Tsb93+t64Z2TKqv1H8G658UeMgfWIB78v4CcLJ2mi
TN/3opqXrzjkQOTDHzBgT7al/mpETHZ6fOdbCemK0fVALGFUioUZg4M8VXtuI4Jw
q28jAyoRKrCrzda4BeQ553NZ4G5RvwhX3O2I8B8upTbt5hLcisBKy8MPLYY5LUFj
YKAP+raf6QLliP6KYHuVxUlgzxjLTxVG41etcyqqZF+foyiKBO3PU3n8oh++tgQP
ExOxiR0JSkBG5b+oOBD0zxcvo3/SjBHn0dJOZCSU2SkCgYEAyCe676XnNyBZMRD7
6trsaoiCWBpA6M8H44+x3w4cQFtqV38RyLy60D+iMKjIaLqeBbnay61VMzo24Bz3
EuF2n4+9k/MetLJ0NCw8HmN5k0WSMD2BFsJWG8glVbzaqzehP4tIclwDTYc1jQVt
IoV2/iL7HGT+x2daUwbU5kN5hK0CgYEAxiLB+fmjxJW7VY4SHDLqPdpIW0q/kv4K
d/yZBrCX799vjmFb9vLh7PkQUfJhMJ/ttJOd7EtT3xh4mfkBeLfHwVU0d/ahbmSH
UJu/E9ZGxAW3PP0kxHZtPrLKQwBnfq8AxBauIhR3rPSorQTIOKtwz1jMlHFSUpuL
3KeK2YfDYJcCgYEAkQnJOlNcAuRb/WQzSHIvktssqK8NjiZHryy3Vc0hx7j2jES2
HGI2dSVHYD9OSiXA0KFm3OTTsnViwm/60iGzFdjRJV6tR39xGUVcoyCuPnvRfUd0
PYvBXgxgkYpyYlPDcwp5CvWGJy3tLi1acgOIwIuUr3S38sL//t4adGk8q1kCgYB8
Jbs1Tl53BvrimKpwUNbE+sjrquJu0A7vL68SqgQJoQ7dP9PH4Ff/i+/V6PFM7mib
BQOm02wyFbs7fvKVGVJoqWK+6CIucX732x7W5yRgHtS5ukQXdbzt1Ek3wkEW98Cb
HTruz7RNAt/NyXlLSODeit1lBbx3Vk9EaxZtRsv88QKBgGn7JwXgez9NOyobsNIo
QVO80rpUeenSjuFi+R0VmbLKe/wgAQbYJ0xTAsQ0btqViMzB27D6mJyC+KUIwWNX
MN8a+m46v4kqvZkKL2c4gmDibyURNe/vCtCHFuanJS/1mo2tr4XDyEeiuK52eTd9
omQDpP86RX/hIIQ+JyLSaWYa
-----END PRIVATE KEY-----`,
comment:
"Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation"
}
},
{
path: "/nested1/nested2/folder",
secret: {
key: "secret-key-3",
value:
"TG9yZW0gaXBzdW0gZG9sb3Igc2l0IGFtZXQsIGNvbnNlY3RldHVyIGFkaXBpc2NpbmcgZWxpdC4gU2VkIGRvIGVpdXNtb2QgdGVtcG9yIGluY2lkaWR1bnQgdXQgbGFib3JlIGV0IGRvbG9yZSBtYWduYSBhbGlxdWEuIFV0IGVuaW0gYWQgbWluaW0gdmVuaWFtLCBxdWlzIG5vc3RydWQgZXhlcmNpdGF0aW9uCg==",
comment: ""
}
}
];
beforeAll(async () => {
if (auth === AuthMode.JWT) {
authToken = jwtAuthToken;
} else if (auth === AuthMode.IDENTITY_ACCESS_TOKEN) {
const identityLogin = await testServer.inject({
method: "POST",
url: "/api/v1/auth/universal-auth/login",
body: {
clientSecret: seedData1.machineIdentity.clientCredentials.secret,
clientId: seedData1.machineIdentity.clientCredentials.id
}
});
expect(identityLogin.statusCode).toBe(200);
authToken = identityLogin.json().accessToken;
}
// create a deep folder
const folderCreate = await testServer.inject({
method: "POST",
url: `/api/v1/folders`,
headers: {
authorization: `Bearer ${jwtAuthToken}`
},
body: {
workspaceId: seedData1.projectV3.id,
environment: seedData1.environment.slug,
name: "folder",
path: "/nested1/nested2"
}
});
expect(folderCreate.statusCode).toBe(200);
folderId = folderCreate.json().folder.id;
});
afterAll(async () => {
const deleteFolder = await testServer.inject({
method: "DELETE",
url: `/api/v1/folders/${folderId}`,
headers: {
authorization: `Bearer ${authToken}`
},
body: {
workspaceId: seedData1.projectV3.id,
environment: seedData1.environment.slug,
path: "/nested1/nested2"
}
});
expect(deleteFolder.statusCode).toBe(200);
});
const getSecrets = async (environment: string, secretPath = "/") => {
const res = await testServer.inject({
method: "GET",
url: `/api/v3/secrets/raw`,
headers: {
authorization: `Bearer ${authToken}`
},
query: {
secretPath,
environment,
workspaceId: seedData1.projectV3.id
}
});
const secrets: TRawSecret[] = JSON.parse(res.payload).secrets || [];
return secrets;
};
test.each(secretTestCases)("Create secret in path $path", async ({ secret, path }) => {
const createdSecret = await createSecret({ path, ...secret });
expect(createdSecret.secretKey).toEqual(secret.key);
expect(createdSecret.secretValue).toEqual(secret.value);
expect(createdSecret.secretComment || "").toEqual(secret.comment);
expect(createdSecret.version).toEqual(1);
const secrets = await getSecrets(seedData1.environment.slug, path);
expect(secrets).toEqual(
expect.arrayContaining([
expect.objectContaining({
secretKey: secret.key,
secretValue: secret.value,
type: SecretType.Shared
})
])
);
await deleteSecret({ path, key: secret.key });
});
test.each(secretTestCases)("Get secret by name in path $path", async ({ secret, path }) => {
await createSecret({ path, ...secret });
const getSecByNameRes = await testServer.inject({
method: "GET",
url: `/api/v3/secrets/raw/${secret.key}`,
headers: {
authorization: `Bearer ${authToken}`
},
query: {
secretPath: path,
workspaceId: seedData1.projectV3.id,
environment: seedData1.environment.slug
}
});
expect(getSecByNameRes.statusCode).toBe(200);
const getSecretByNamePayload = JSON.parse(getSecByNameRes.payload);
expect(getSecretByNamePayload).toHaveProperty("secret");
const decryptedSecret = getSecretByNamePayload.secret as TRawSecret;
expect(decryptedSecret.secretKey).toEqual(secret.key);
expect(decryptedSecret.secretValue).toEqual(secret.value);
expect(decryptedSecret.secretComment || "").toEqual(secret.comment);
await deleteSecret({ path, key: secret.key });
});
if (auth === AuthMode.JWT) {
test.each(secretTestCases)(
"Creating personal secret without shared throw error in path $path",
async ({ secret }) => {
const createSecretReqBody = {
workspaceId: seedData1.projectV3.id,
environment: seedData1.environment.slug,
type: SecretType.Personal,
secretKey: secret.key,
secretValue: secret.value,
secretComment: secret.comment
};
const createSecRes = await testServer.inject({
method: "POST",
url: `/api/v3/secrets/raw/SEC2`,
headers: {
authorization: `Bearer ${authToken}`
},
body: createSecretReqBody
});
const payload = JSON.parse(createSecRes.payload);
expect(createSecRes.statusCode).toBe(400);
expect(payload.error).toEqual("BadRequest");
}
);
test.each(secretTestCases)("Creating personal secret in path $path", async ({ secret, path }) => {
await createSecret({ path, ...secret });
const createSecretReqBody = {
workspaceId: seedData1.projectV3.id,
environment: seedData1.environment.slug,
type: SecretType.Personal,
secretPath: path,
secretKey: secret.key,
secretValue: "personal-value",
secretComment: secret.comment
};
const createSecRes = await testServer.inject({
method: "POST",
url: `/api/v3/secrets/raw/${secret.key}`,
headers: {
authorization: `Bearer ${authToken}`
},
body: createSecretReqBody
});
expect(createSecRes.statusCode).toBe(200);
// list secrets should contain personal one and shared one
const secrets = await getSecrets(seedData1.environment.slug, path);
expect(secrets).toEqual(
expect.arrayContaining([
expect.objectContaining({
secretKey: secret.key,
secretValue: secret.value,
type: SecretType.Shared
}),
expect.objectContaining({
secretKey: secret.key,
secretValue: "personal-value",
type: SecretType.Personal
})
])
);
await deleteSecret({ path, key: secret.key });
});
test.each(secretTestCases)(
"Deleting personal one should not delete shared secret in path $path",
async ({ secret, path }) => {
await createSecret({ path, ...secret }); // shared one
await createSecret({ path, ...secret, type: SecretType.Personal });
// shared secret deletion should delete personal ones also
const secrets = await getSecrets(seedData1.environment.slug, path);
expect(secrets).toEqual(
expect.arrayContaining([
expect.objectContaining({
secretKey: secret.key,
type: SecretType.Shared
}),
expect.not.objectContaining({
secretKey: secret.key,
type: SecretType.Personal
})
])
);
await deleteSecret({ path, key: secret.key });
}
);
}
test.each(secretTestCases)("Update secret in path $path", async ({ path, secret }) => {
await createSecret({ path, ...secret });
const updateSecretReqBody = {
workspaceId: seedData1.projectV3.id,
environment: seedData1.environment.slug,
type: SecretType.Shared,
secretPath: path,
secretKey: secret.key,
secretValue: "new-value",
secretComment: secret.comment
};
const updateSecRes = await testServer.inject({
method: "PATCH",
url: `/api/v3/secrets/raw/${secret.key}`,
headers: {
authorization: `Bearer ${authToken}`
},
body: updateSecretReqBody
});
expect(updateSecRes.statusCode).toBe(200);
const updatedSecretPayload = JSON.parse(updateSecRes.payload);
expect(updatedSecretPayload).toHaveProperty("secret");
const decryptedSecret = updatedSecretPayload.secret;
expect(decryptedSecret.secretKey).toEqual(secret.key);
expect(decryptedSecret.secretValue).toEqual("new-value");
expect(decryptedSecret.secretComment || "").toEqual(secret.comment);
// list secret should have updated value
const secrets = await getSecrets(seedData1.environment.slug, path);
expect(secrets).toEqual(
expect.arrayContaining([
expect.objectContaining({
secretKey: secret.key,
secretValue: "new-value",
type: SecretType.Shared
})
])
);
await deleteSecret({ path, key: secret.key });
});
test.each(secretTestCases)("Delete secret in path $path", async ({ secret, path }) => {
await createSecret({ path, ...secret });
const deletedSecret = await deleteSecret({ path, key: secret.key });
expect(deletedSecret.secretKey).toEqual(secret.key);
// shared secret deletion should delete personal ones also
const secrets = await getSecrets(seedData1.environment.slug, path);
expect(secrets).toEqual(
expect.not.arrayContaining([
expect.objectContaining({
secretKey: secret.key,
type: SecretType.Shared
}),
expect.objectContaining({
secretKey: secret.key,
type: SecretType.Personal
})
])
);
});
test.each(secretTestCases)("Bulk create secrets in path $path", async ({ secret, path }) => {
const createSharedSecRes = await testServer.inject({
method: "POST",
url: `/api/v3/secrets/batch/raw`,
headers: {
authorization: `Bearer ${authToken}`
},
body: {
workspaceId: seedData1.projectV3.id,
environment: seedData1.environment.slug,
secretPath: path,
secrets: Array.from(Array(5)).map((_e, i) => ({
secretKey: `BULK-${secret.key}-${i + 1}`,
secretValue: secret.value,
secretComment: secret.comment
}))
}
});
expect(createSharedSecRes.statusCode).toBe(200);
const createSharedSecPayload = JSON.parse(createSharedSecRes.payload);
expect(createSharedSecPayload).toHaveProperty("secrets");
// bulk ones should exist
const secrets = await getSecrets(seedData1.environment.slug, path);
expect(secrets).toEqual(
expect.arrayContaining(
Array.from(Array(5)).map((_e, i) =>
expect.objectContaining({
secretKey: `BULK-${secret.key}-${i + 1}`,
secretValue: secret.value,
type: SecretType.Shared
})
)
)
);
await Promise.all(
Array.from(Array(5)).map((_e, i) => deleteSecret({ path, key: `BULK-${secret.key}-${i + 1}` }))
);
});
test.each(secretTestCases)("Bulk create fail on existing secret in path $path", async ({ secret, path }) => {
await createSecret({ ...secret, key: `BULK-${secret.key}-1`, path });
const createSharedSecRes = await testServer.inject({
method: "POST",
url: `/api/v3/secrets/batch/raw`,
headers: {
authorization: `Bearer ${authToken}`
},
body: {
workspaceId: seedData1.projectV3.id,
environment: seedData1.environment.slug,
secretPath: path,
secrets: Array.from(Array(5)).map((_e, i) => ({
secretKey: `BULK-${secret.key}-${i + 1}`,
secretValue: secret.value,
secretComment: secret.comment
}))
}
});
expect(createSharedSecRes.statusCode).toBe(400);
await deleteSecret({ path, key: `BULK-${secret.key}-1` });
});
test.each(secretTestCases)("Bulk update secrets in path $path", async ({ secret, path }) => {
await Promise.all(
Array.from(Array(5)).map((_e, i) => createSecret({ ...secret, key: `BULK-${secret.key}-${i + 1}`, path }))
);
const updateSharedSecRes = await testServer.inject({
method: "PATCH",
url: `/api/v3/secrets/batch/raw`,
headers: {
authorization: `Bearer ${authToken}`
},
body: {
workspaceId: seedData1.projectV3.id,
environment: seedData1.environment.slug,
secretPath: path,
secrets: Array.from(Array(5)).map((_e, i) => ({
secretKey: `BULK-${secret.key}-${i + 1}`,
secretValue: "update-value",
secretComment: secret.comment
}))
}
});
expect(updateSharedSecRes.statusCode).toBe(200);
const updateSharedSecPayload = JSON.parse(updateSharedSecRes.payload);
expect(updateSharedSecPayload).toHaveProperty("secrets");
// bulk ones should exist
const secrets = await getSecrets(seedData1.environment.slug, path);
expect(secrets).toEqual(
expect.arrayContaining(
Array.from(Array(5)).map((_e, i) =>
expect.objectContaining({
secretKey: `BULK-${secret.key}-${i + 1}`,
secretValue: "update-value",
type: SecretType.Shared
})
)
)
);
await Promise.all(
Array.from(Array(5)).map((_e, i) => deleteSecret({ path, key: `BULK-${secret.key}-${i + 1}` }))
);
});
test.each(secretTestCases)("Bulk delete secrets in path $path", async ({ secret, path }) => {
await Promise.all(
Array.from(Array(5)).map((_e, i) => createSecret({ ...secret, key: `BULK-${secret.key}-${i + 1}`, path }))
);
const deletedSharedSecRes = await testServer.inject({
method: "DELETE",
url: `/api/v3/secrets/batch/raw`,
headers: {
authorization: `Bearer ${authToken}`
},
body: {
workspaceId: seedData1.projectV3.id,
environment: seedData1.environment.slug,
secretPath: path,
secrets: Array.from(Array(5)).map((_e, i) => ({
secretKey: `BULK-${secret.key}-${i + 1}`
}))
}
});
expect(deletedSharedSecRes.statusCode).toBe(200);
const deletedSecretPayload = JSON.parse(deletedSharedSecRes.payload);
expect(deletedSecretPayload).toHaveProperty("secrets");
// bulk ones should exist
const secrets = await getSecrets(seedData1.environment.slug, path);
expect(secrets).toEqual(
expect.not.arrayContaining(
Array.from(Array(5)).map((_e, i) =>
expect.objectContaining({
secretKey: `BULK-${secret.value}-${i + 1}`,
type: SecretType.Shared
})
)
)
);
});
}
);

View File

@ -25,6 +25,7 @@
"@fastify/swagger": "^8.14.0",
"@fastify/swagger-ui": "^2.1.0",
"@node-saml/passport-saml": "^4.0.4",
"@octokit/plugin-retry": "^5.0.5",
"@octokit/rest": "^20.0.2",
"@octokit/webhooks-types": "^7.3.1",
"@peculiar/asn1-schema": "^2.3.8",
@ -7812,19 +7813,45 @@
}
},
"node_modules/@octokit/plugin-retry": {
"version": "6.0.1",
"resolved": "https://registry.npmjs.org/@octokit/plugin-retry/-/plugin-retry-6.0.1.tgz",
"integrity": "sha512-SKs+Tz9oj0g4p28qkZwl/topGcb0k0qPNX/i7vBKmDsjoeqnVfFUquqrE/O9oJY7+oLzdCtkiWSXLpLjvl6uog==",
"version": "5.0.5",
"resolved": "https://registry.npmjs.org/@octokit/plugin-retry/-/plugin-retry-5.0.5.tgz",
"integrity": "sha512-sB1RWMhSrre02Atv95K6bhESlJ/sPdZkK/wE/w1IdSCe0yM6FxSjksLa6T7aAvxvxlLKzQEC4KIiqpqyov1Tbg==",
"dependencies": {
"@octokit/request-error": "^5.0.0",
"@octokit/types": "^12.0.0",
"@octokit/request-error": "^4.0.1",
"@octokit/types": "^10.0.0",
"bottleneck": "^2.15.3"
},
"engines": {
"node": ">= 18"
},
"peerDependencies": {
"@octokit/core": ">=5"
"@octokit/core": ">=3"
}
},
"node_modules/@octokit/plugin-retry/node_modules/@octokit/openapi-types": {
"version": "18.1.1",
"resolved": "https://registry.npmjs.org/@octokit/openapi-types/-/openapi-types-18.1.1.tgz",
"integrity": "sha512-VRaeH8nCDtF5aXWnjPuEMIYf1itK/s3JYyJcWFJT8X9pSNnBtriDf7wlEWsGuhPLl4QIH4xM8fqTXDwJ3Mu6sw=="
},
"node_modules/@octokit/plugin-retry/node_modules/@octokit/request-error": {
"version": "4.0.2",
"resolved": "https://registry.npmjs.org/@octokit/request-error/-/request-error-4.0.2.tgz",
"integrity": "sha512-uqwUEmZw3x4I9DGYq9fODVAAvcLsPQv97NRycP6syEFu5916M189VnNBW2zANNwqg3OiligNcAey7P0SET843w==",
"dependencies": {
"@octokit/types": "^10.0.0",
"deprecation": "^2.0.0",
"once": "^1.4.0"
},
"engines": {
"node": ">= 18"
}
},
"node_modules/@octokit/plugin-retry/node_modules/@octokit/types": {
"version": "10.0.0",
"resolved": "https://registry.npmjs.org/@octokit/types/-/types-10.0.0.tgz",
"integrity": "sha512-Vm8IddVmhCgU1fxC1eyinpwqzXPEYu0NrYzD3YZjlGjyftdLBTeqNblRC0jmJmgxbJIsQlyogVeGnrNaaMVzIg==",
"dependencies": {
"@octokit/openapi-types": "^18.0.0"
}
},
"node_modules/@octokit/plugin-throttling": {
@ -17396,6 +17423,22 @@
"node": ">=18"
}
},
"node_modules/probot/node_modules/@octokit/plugin-retry": {
"version": "6.0.1",
"resolved": "https://registry.npmjs.org/@octokit/plugin-retry/-/plugin-retry-6.0.1.tgz",
"integrity": "sha512-SKs+Tz9oj0g4p28qkZwl/topGcb0k0qPNX/i7vBKmDsjoeqnVfFUquqrE/O9oJY7+oLzdCtkiWSXLpLjvl6uog==",
"dependencies": {
"@octokit/request-error": "^5.0.0",
"@octokit/types": "^12.0.0",
"bottleneck": "^2.15.3"
},
"engines": {
"node": ">= 18"
},
"peerDependencies": {
"@octokit/core": ">=5"
}
},
"node_modules/probot/node_modules/commander": {
"version": "11.1.0",
"resolved": "https://registry.npmjs.org/commander/-/commander-11.1.0.tgz",

View File

@ -40,8 +40,8 @@
"type:check": "tsc --noEmit",
"lint:fix": "eslint --fix --ext js,ts ./src",
"lint": "eslint 'src/**/*.ts'",
"test:e2e": "vitest run -c vitest.e2e.config.ts",
"test:e2e-watch": "vitest -c vitest.e2e.config.ts",
"test:e2e": "vitest run -c vitest.e2e.config.ts --bail=1",
"test:e2e-watch": "vitest -c vitest.e2e.config.ts --bail=1",
"test:e2e-coverage": "vitest run --coverage -c vitest.e2e.config.ts",
"generate:component": "tsx ./scripts/create-backend-file.ts",
"generate:schema": "tsx ./scripts/generate-schema-types.ts",
@ -121,6 +121,7 @@
"@fastify/swagger": "^8.14.0",
"@fastify/swagger-ui": "^2.1.0",
"@node-saml/passport-saml": "^4.0.4",
"@octokit/plugin-retry": "^5.0.5",
"@octokit/rest": "^20.0.2",
"@octokit/webhooks-types": "^7.3.1",
"@peculiar/asn1-schema": "^2.3.8",

View File

@ -18,6 +18,7 @@ import { TOidcConfigServiceFactory } from "@app/ee/services/oidc/oidc-config-ser
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service";
import { TProjectUserAdditionalPrivilegeServiceFactory } from "@app/ee/services/project-user-additional-privilege/project-user-additional-privilege-service";
import { TRateLimitServiceFactory } from "@app/ee/services/rate-limit/rate-limit-service";
import { RateLimitConfiguration } from "@app/ee/services/rate-limit/rate-limit-types";
import { TSamlConfigServiceFactory } from "@app/ee/services/saml-config/saml-config-service";
import { TScimServiceFactory } from "@app/ee/services/scim/scim-service";
import { TSecretApprovalPolicyServiceFactory } from "@app/ee/services/secret-approval-policy/secret-approval-policy-service";
@ -50,6 +51,7 @@ import { TIntegrationServiceFactory } from "@app/services/integration/integratio
import { TIntegrationAuthServiceFactory } from "@app/services/integration-auth/integration-auth-service";
import { TOrgRoleServiceFactory } from "@app/services/org/org-role-service";
import { TOrgServiceFactory } from "@app/services/org/org-service";
import { TOrgAdminServiceFactory } from "@app/services/org-admin/org-admin-service";
import { TProjectServiceFactory } from "@app/services/project/project-service";
import { TProjectBotServiceFactory } from "@app/services/project-bot/project-bot-service";
import { TProjectEnvServiceFactory } from "@app/services/project-env/project-env-service";
@ -88,6 +90,7 @@ declare module "fastify" {
id: string;
orgId: string;
};
rateLimits: RateLimitConfiguration;
// passport data
passportUser: {
isUserCompleted: string;
@ -165,6 +168,7 @@ declare module "fastify" {
rateLimit: TRateLimitServiceFactory;
userEngagement: TUserEngagementServiceFactory;
externalKms: TExternalKmsServiceFactory;
orgAdmin: TOrgAdminServiceFactory;
};
// this is exclusive use for middlewares in which we need to inject data
// everywhere else access using service layer

View File

@ -204,6 +204,9 @@ import {
TSecretApprovalRequestSecretTags,
TSecretApprovalRequestSecretTagsInsert,
TSecretApprovalRequestSecretTagsUpdate,
TSecretApprovalRequestSecretTagsV2,
TSecretApprovalRequestSecretTagsV2Insert,
TSecretApprovalRequestSecretTagsV2Update,
TSecretApprovalRequestsInsert,
TSecretApprovalRequestsReviewers,
TSecretApprovalRequestsReviewersInsert,
@ -211,6 +214,9 @@ import {
TSecretApprovalRequestsSecrets,
TSecretApprovalRequestsSecretsInsert,
TSecretApprovalRequestsSecretsUpdate,
TSecretApprovalRequestsSecretsV2,
TSecretApprovalRequestsSecretsV2Insert,
TSecretApprovalRequestsSecretsV2Update,
TSecretApprovalRequestsUpdate,
TSecretBlindIndexes,
TSecretBlindIndexesInsert,
@ -227,9 +233,15 @@ import {
TSecretReferences,
TSecretReferencesInsert,
TSecretReferencesUpdate,
TSecretReferencesV2,
TSecretReferencesV2Insert,
TSecretReferencesV2Update,
TSecretRotationOutputs,
TSecretRotationOutputsInsert,
TSecretRotationOutputsUpdate,
TSecretRotationOutputV2,
TSecretRotationOutputV2Insert,
TSecretRotationOutputV2Update,
TSecretRotations,
TSecretRotationsInsert,
TSecretRotationsUpdate,
@ -248,6 +260,9 @@ import {
TSecretSnapshotSecrets,
TSecretSnapshotSecretsInsert,
TSecretSnapshotSecretsUpdate,
TSecretSnapshotSecretsV2,
TSecretSnapshotSecretsV2Insert,
TSecretSnapshotSecretsV2Update,
TSecretSnapshotsInsert,
TSecretSnapshotsUpdate,
TSecretsUpdate,
@ -263,6 +278,9 @@ import {
TSecretVersionTagJunction,
TSecretVersionTagJunctionInsert,
TSecretVersionTagJunctionUpdate,
TSecretVersionV2TagJunction,
TSecretVersionV2TagJunctionInsert,
TSecretVersionV2TagJunctionUpdate,
TServiceTokens,
TServiceTokensInsert,
TServiceTokensUpdate,
@ -291,6 +309,17 @@ import {
TWebhooksInsert,
TWebhooksUpdate
} from "@app/db/schemas";
import {
TSecretV2TagJunction,
TSecretV2TagJunctionInsert,
TSecretV2TagJunctionUpdate
} from "@app/db/schemas/secret-v2-tag-junction";
import {
TSecretVersionsV2,
TSecretVersionsV2Insert,
TSecretVersionsV2Update
} from "@app/db/schemas/secret-versions-v2";
import { TSecretsV2, TSecretsV2Insert, TSecretsV2Update } from "@app/db/schemas/secrets-v2";
declare module "knex" {
namespace Knex {
@ -645,7 +674,23 @@ declare module "knex/types/tables" {
TSecretScanningGitRisksUpdate
>;
[TableName.TrustedIps]: KnexOriginal.CompositeTableType<TTrustedIps, TTrustedIpsInsert, TTrustedIpsUpdate>;
[TableName.SecretV2]: KnexOriginal.CompositeTableType<TSecretsV2, TSecretsV2Insert, TSecretsV2Update>;
[TableName.SecretVersionV2]: KnexOriginal.CompositeTableType<
TSecretVersionsV2,
TSecretVersionsV2Insert,
TSecretVersionsV2Update
>;
[TableName.SecretReferenceV2]: KnexOriginal.CompositeTableType<
TSecretReferencesV2,
TSecretReferencesV2Insert,
TSecretReferencesV2Update
>;
// Junction tables
[TableName.SecretV2JnTag]: KnexOriginal.CompositeTableType<
TSecretV2TagJunction,
TSecretV2TagJunctionInsert,
TSecretV2TagJunctionUpdate
>;
[TableName.JnSecretTag]: KnexOriginal.CompositeTableType<
TSecretTagJunction,
TSecretTagJunctionInsert,
@ -656,6 +701,31 @@ declare module "knex/types/tables" {
TSecretVersionTagJunctionInsert,
TSecretVersionTagJunctionUpdate
>;
[TableName.SecretVersionV2Tag]: KnexOriginal.CompositeTableType<
TSecretVersionV2TagJunction,
TSecretVersionV2TagJunctionInsert,
TSecretVersionV2TagJunctionUpdate
>;
[TableName.SnapshotSecretV2]: KnexOriginal.CompositeTableType<
TSecretSnapshotSecretsV2,
TSecretSnapshotSecretsV2Insert,
TSecretSnapshotSecretsV2Update
>;
[TableName.SecretApprovalRequestSecretV2]: KnexOriginal.CompositeTableType<
TSecretApprovalRequestsSecretsV2,
TSecretApprovalRequestsSecretsV2Insert,
TSecretApprovalRequestsSecretsV2Update
>;
[TableName.SecretApprovalRequestSecretTagV2]: KnexOriginal.CompositeTableType<
TSecretApprovalRequestSecretTagsV2,
TSecretApprovalRequestSecretTagsV2Insert,
TSecretApprovalRequestSecretTagsV2Update
>;
[TableName.SecretRotationOutputV2]: KnexOriginal.CompositeTableType<
TSecretRotationOutputV2,
TSecretRotationOutputV2Insert,
TSecretRotationOutputV2Update
>;
// KMS service
[TableName.KmsServerRootConfig]: KnexOriginal.CompositeTableType<
TKmsRootConfig,

View File

@ -0,0 +1,23 @@
import { Knex } from "knex";
import { SecretSharingAccessType } from "@app/lib/types";
import { TableName } from "../schemas";
export async function up(knex: Knex): Promise<void> {
const hasColumn = await knex.schema.hasColumn(TableName.SecretSharing, "accessType");
if (!hasColumn) {
await knex.schema.table(TableName.SecretSharing, (table) => {
table.string("accessType").notNullable().defaultTo(SecretSharingAccessType.Anyone);
});
}
}
export async function down(knex: Knex): Promise<void> {
const hasColumn = await knex.schema.hasColumn(TableName.SecretSharing, "accessType");
if (hasColumn) {
await knex.schema.table(TableName.SecretSharing, (table) => {
table.dropColumn("accessType");
});
}
}

View File

@ -0,0 +1,21 @@
import { Knex } from "knex";
import { TableName } from "../schemas";
export async function up(knex: Knex): Promise<void> {
const hasColumn = await knex.schema.hasColumn(TableName.SecretApprovalRequest, "bypassReason");
if (!hasColumn) {
await knex.schema.table(TableName.SecretApprovalRequest, (table) => {
table.string("bypassReason").nullable();
});
}
}
export async function down(knex: Knex): Promise<void> {
const hasColumn = await knex.schema.hasColumn(TableName.SecretApprovalRequest, "bypassReason");
if (hasColumn) {
await knex.schema.table(TableName.SecretApprovalRequest, (table) => {
table.dropColumn("bypassReason");
});
}
}

View File

@ -0,0 +1,39 @@
import { Knex } from "knex";
import { TableName } from "../schemas";
export async function up(knex: Knex): Promise<void> {
if (await knex.schema.hasTable(TableName.SecretSharing)) {
const doesNameExist = await knex.schema.hasColumn(TableName.SecretSharing, "name");
if (!doesNameExist) {
await knex.schema.alterTable(TableName.SecretSharing, (t) => {
t.string("name").nullable();
});
}
const doesLastViewedAtExist = await knex.schema.hasColumn(TableName.SecretSharing, "lastViewedAt");
if (!doesLastViewedAtExist) {
await knex.schema.alterTable(TableName.SecretSharing, (t) => {
t.timestamp("lastViewedAt").nullable();
});
}
}
}
export async function down(knex: Knex): Promise<void> {
if (await knex.schema.hasTable(TableName.SecretSharing)) {
const doesNameExist = await knex.schema.hasColumn(TableName.SecretSharing, "name");
if (doesNameExist) {
await knex.schema.alterTable(TableName.SecretSharing, (t) => {
t.dropColumn("name");
});
}
const doesLastViewedAtExist = await knex.schema.hasColumn(TableName.SecretSharing, "lastViewedAt");
if (doesLastViewedAtExist) {
await knex.schema.alterTable(TableName.SecretSharing, (t) => {
t.dropColumn("lastViewedAt");
});
}
}
}

View File

@ -0,0 +1,21 @@
import { Knex } from "knex";
import { TableName } from "../schemas";
export async function up(knex: Knex): Promise<void> {
const hasKmsDataKeyCol = await knex.schema.hasColumn(TableName.Organization, "kmsEncryptedDataKey");
await knex.schema.alterTable(TableName.Organization, (tb) => {
if (!hasKmsDataKeyCol) {
tb.binary("kmsEncryptedDataKey");
}
});
}
export async function down(knex: Knex): Promise<void> {
const hasKmsDataKeyCol = await knex.schema.hasColumn(TableName.Organization, "kmsEncryptedDataKey");
await knex.schema.alterTable(TableName.Organization, (t) => {
if (hasKmsDataKeyCol) {
t.dropColumn("kmsEncryptedDataKey");
}
});
}

View File

@ -0,0 +1,29 @@
import { Knex } from "knex";
import { TableName } from "../schemas";
export async function up(knex: Knex): Promise<void> {
const hasKmsSecretManagerEncryptedDataKey = await knex.schema.hasColumn(
TableName.Project,
"kmsSecretManagerEncryptedDataKey"
);
await knex.schema.alterTable(TableName.Project, (tb) => {
if (!hasKmsSecretManagerEncryptedDataKey) {
tb.binary("kmsSecretManagerEncryptedDataKey");
}
});
}
export async function down(knex: Knex): Promise<void> {
const hasKmsSecretManagerEncryptedDataKey = await knex.schema.hasColumn(
TableName.Project,
"kmsSecretManagerEncryptedDataKey"
);
await knex.schema.alterTable(TableName.Project, (t) => {
if (hasKmsSecretManagerEncryptedDataKey) {
t.dropColumn("kmsSecretManagerEncryptedDataKey");
}
});
}

View File

@ -0,0 +1,181 @@
/* eslint-disable @typescript-eslint/ban-ts-comment */
import { Knex } from "knex";
import { SecretType, TableName } from "../schemas";
import { createJunctionTable, createOnUpdateTrigger, dropOnUpdateTrigger } from "../utils";
export async function up(knex: Knex): Promise<void> {
const doesSecretV2TableExist = await knex.schema.hasTable(TableName.SecretV2);
if (!doesSecretV2TableExist) {
await knex.schema.createTable(TableName.SecretV2, (t) => {
t.uuid("id", { primaryKey: true }).defaultTo(knex.fn.uuid());
t.integer("version").defaultTo(1).notNullable();
t.string("type").notNullable().defaultTo(SecretType.Shared);
t.string("key", 500).notNullable();
t.binary("encryptedValue");
t.binary("encryptedComment");
t.string("reminderNote");
t.integer("reminderRepeatDays");
t.boolean("skipMultilineEncoding").defaultTo(false);
t.jsonb("metadata");
t.uuid("userId");
t.foreign("userId").references("id").inTable(TableName.Users).onDelete("CASCADE");
t.uuid("folderId").notNullable();
t.foreign("folderId").references("id").inTable(TableName.SecretFolder).onDelete("CASCADE");
t.timestamps(true, true, true);
t.index(["folderId", "userId"]);
});
}
await createOnUpdateTrigger(knex, TableName.SecretV2);
// many to many relation between tags
await createJunctionTable(knex, TableName.SecretV2JnTag, TableName.SecretV2, TableName.SecretTag);
const doesSecretV2VersionTableExist = await knex.schema.hasTable(TableName.SecretVersionV2);
if (!doesSecretV2VersionTableExist) {
await knex.schema.createTable(TableName.SecretVersionV2, (t) => {
t.uuid("id", { primaryKey: true }).defaultTo(knex.fn.uuid());
t.integer("version").defaultTo(1).notNullable();
t.string("type").notNullable().defaultTo(SecretType.Shared);
t.string("key", 500).notNullable();
t.binary("encryptedValue");
t.binary("encryptedComment");
t.string("reminderNote");
t.integer("reminderRepeatDays");
t.boolean("skipMultilineEncoding").defaultTo(false);
t.jsonb("metadata");
// to avoid orphan rows
t.uuid("envId");
t.foreign("envId").references("id").inTable(TableName.Environment).onDelete("CASCADE");
t.uuid("secretId").notNullable();
t.uuid("folderId").notNullable();
t.uuid("userId");
t.foreign("userId").references("id").inTable(TableName.Users).onDelete("CASCADE");
t.timestamps(true, true, true);
});
}
await createOnUpdateTrigger(knex, TableName.SecretVersionV2);
if (!(await knex.schema.hasTable(TableName.SecretReferenceV2))) {
await knex.schema.createTable(TableName.SecretReferenceV2, (t) => {
t.uuid("id", { primaryKey: true }).defaultTo(knex.fn.uuid());
t.string("environment").notNullable();
t.string("secretPath").notNullable();
t.string("secretKey", 500).notNullable();
t.uuid("secretId").notNullable();
t.foreign("secretId").references("id").inTable(TableName.SecretV2).onDelete("CASCADE");
});
}
await createJunctionTable(knex, TableName.SecretVersionV2Tag, TableName.SecretVersionV2, TableName.SecretTag);
if (!(await knex.schema.hasTable(TableName.SecretApprovalRequestSecretV2))) {
await knex.schema.createTable(TableName.SecretApprovalRequestSecretV2, (t) => {
// everything related to secret
t.uuid("id", { primaryKey: true }).defaultTo(knex.fn.uuid());
t.integer("version").defaultTo(1);
t.string("key", 500).notNullable();
t.binary("encryptedValue");
t.binary("encryptedComment");
t.string("reminderNote");
t.integer("reminderRepeatDays");
t.boolean("skipMultilineEncoding").defaultTo(false);
t.jsonb("metadata");
t.timestamps(true, true, true);
// commit details
t.uuid("requestId").notNullable();
t.foreign("requestId").references("id").inTable(TableName.SecretApprovalRequest).onDelete("CASCADE");
t.string("op").notNullable();
t.uuid("secretId");
t.foreign("secretId").references("id").inTable(TableName.SecretV2).onDelete("SET NULL");
t.uuid("secretVersion");
t.foreign("secretVersion").references("id").inTable(TableName.SecretVersionV2).onDelete("SET NULL");
});
}
if (!(await knex.schema.hasTable(TableName.SecretApprovalRequestSecretTagV2))) {
await knex.schema.createTable(TableName.SecretApprovalRequestSecretTagV2, (t) => {
t.uuid("id", { primaryKey: true }).defaultTo(knex.fn.uuid());
t.uuid("secretId").notNullable();
t.foreign("secretId").references("id").inTable(TableName.SecretApprovalRequestSecretV2).onDelete("CASCADE");
t.uuid("tagId").notNullable();
t.foreign("tagId").references("id").inTable(TableName.SecretTag).onDelete("CASCADE");
t.timestamps(true, true, true);
});
}
if (!(await knex.schema.hasTable(TableName.SnapshotSecretV2))) {
await knex.schema.createTable(TableName.SnapshotSecretV2, (t) => {
t.uuid("id", { primaryKey: true }).defaultTo(knex.fn.uuid());
t.uuid("envId").index().notNullable();
t.foreign("envId").references("id").inTable(TableName.Environment).onDelete("CASCADE");
// not a relation kept like that to keep it when rolled back
t.uuid("secretVersionId").index().notNullable();
t.foreign("secretVersionId").references("id").inTable(TableName.SecretVersionV2).onDelete("CASCADE");
t.uuid("snapshotId").index().notNullable();
t.foreign("snapshotId").references("id").inTable(TableName.Snapshot).onDelete("CASCADE");
t.timestamps(true, true, true);
});
}
if (await knex.schema.hasTable(TableName.IntegrationAuth)) {
const hasEncryptedAccess = await knex.schema.hasColumn(TableName.IntegrationAuth, "encryptedAccess");
const hasEncryptedAccessId = await knex.schema.hasColumn(TableName.IntegrationAuth, "encryptedAccessId");
const hasEncryptedRefresh = await knex.schema.hasColumn(TableName.IntegrationAuth, "encryptedRefresh");
const hasEncryptedAwsIamAssumRole = await knex.schema.hasColumn(
TableName.IntegrationAuth,
"encryptedAwsAssumeIamRoleArn"
);
await knex.schema.alterTable(TableName.IntegrationAuth, (t) => {
if (!hasEncryptedAccess) t.binary("encryptedAccess");
if (!hasEncryptedAccessId) t.binary("encryptedAccessId");
if (!hasEncryptedRefresh) t.binary("encryptedRefresh");
if (!hasEncryptedAwsIamAssumRole) t.binary("encryptedAwsAssumeIamRoleArn");
});
}
if (!(await knex.schema.hasTable(TableName.SecretRotationOutputV2))) {
await knex.schema.createTable(TableName.SecretRotationOutputV2, (t) => {
t.uuid("id", { primaryKey: true }).defaultTo(knex.fn.uuid());
t.string("key").notNullable();
t.uuid("secretId").notNullable();
t.foreign("secretId").references("id").inTable(TableName.SecretV2).onDelete("CASCADE");
t.uuid("rotationId").notNullable();
t.foreign("rotationId").references("id").inTable(TableName.SecretRotation).onDelete("CASCADE");
});
}
}
export async function down(knex: Knex): Promise<void> {
await knex.schema.dropTableIfExists(TableName.SnapshotSecretV2);
await knex.schema.dropTableIfExists(TableName.SecretApprovalRequestSecretTagV2);
await knex.schema.dropTableIfExists(TableName.SecretApprovalRequestSecretV2);
await knex.schema.dropTableIfExists(TableName.SecretV2JnTag);
await knex.schema.dropTableIfExists(TableName.SecretReferenceV2);
await knex.schema.dropTableIfExists(TableName.SecretRotationOutputV2);
await dropOnUpdateTrigger(knex, TableName.SecretVersionV2);
await knex.schema.dropTableIfExists(TableName.SecretVersionV2Tag);
await knex.schema.dropTableIfExists(TableName.SecretVersionV2);
await dropOnUpdateTrigger(knex, TableName.SecretV2);
await knex.schema.dropTableIfExists(TableName.SecretV2);
if (await knex.schema.hasTable(TableName.IntegrationAuth)) {
const hasEncryptedAccess = await knex.schema.hasColumn(TableName.IntegrationAuth, "encryptedAccess");
const hasEncryptedAccessId = await knex.schema.hasColumn(TableName.IntegrationAuth, "encryptedAccessId");
const hasEncryptedRefresh = await knex.schema.hasColumn(TableName.IntegrationAuth, "encryptedRefresh");
const hasEncryptedAwsIamAssumRole = await knex.schema.hasColumn(
TableName.IntegrationAuth,
"encryptedAwsAssumeIamRoleArn"
);
await knex.schema.alterTable(TableName.IntegrationAuth, (t) => {
if (hasEncryptedAccess) t.dropColumn("encryptedAccess");
if (hasEncryptedAccessId) t.dropColumn("encryptedAccessId");
if (hasEncryptedRefresh) t.dropColumn("encryptedRefresh");
if (hasEncryptedAwsIamAssumRole) t.dropColumn("encryptedAwsAssumeIamRoleArn");
});
}
}

View File

@ -0,0 +1,21 @@
import { Knex } from "knex";
import { TableName } from "../schemas";
export async function up(knex: Knex): Promise<void> {
const hasCreationLimitCol = await knex.schema.hasColumn(TableName.RateLimit, "creationLimit");
await knex.schema.alterTable(TableName.RateLimit, (t) => {
if (hasCreationLimitCol) {
t.dropColumn("creationLimit");
}
});
}
export async function down(knex: Knex): Promise<void> {
const hasCreationLimitCol = await knex.schema.hasColumn(TableName.RateLimit, "creationLimit");
await knex.schema.alterTable(TableName.RateLimit, (t) => {
if (!hasCreationLimitCol) {
t.integer("creationLimit").defaultTo(30).notNullable();
}
});
}

View File

@ -0,0 +1,105 @@
import slugify from "@sindresorhus/slugify";
import { Knex } from "knex";
import { TableName } from "@app/db/schemas";
import { randomSecureBytes } from "@app/lib/crypto";
import { symmetricCipherService, SymmetricEncryption } from "@app/lib/crypto/cipher";
import { alphaNumericNanoId } from "@app/lib/nanoid";
const getInstanceRootKey = async (knex: Knex) => {
const encryptionKey = process.env.ENCRYPTION_KEY || process.env.ROOT_ENCRYPTION_KEY;
// if root key its base64 encoded
const isBase64 = !process.env.ENCRYPTION_KEY;
if (!encryptionKey) throw new Error("ENCRYPTION_KEY variable needed for migration");
const encryptionKeyBuffer = Buffer.from(encryptionKey, isBase64 ? "base64" : "utf8");
const KMS_ROOT_CONFIG_UUID = "00000000-0000-0000-0000-000000000000";
const kmsRootConfig = await knex(TableName.KmsServerRootConfig).where({ id: KMS_ROOT_CONFIG_UUID }).first();
const cipher = symmetricCipherService(SymmetricEncryption.AES_GCM_256);
if (kmsRootConfig) {
const decryptedRootKey = cipher.decrypt(kmsRootConfig.encryptedRootKey, encryptionKeyBuffer);
// set the flag so that other instancen nodes can start
return decryptedRootKey;
}
const newRootKey = randomSecureBytes(32);
const encryptedRootKey = cipher.encrypt(newRootKey, encryptionKeyBuffer);
await knex(TableName.KmsServerRootConfig).insert({
encryptedRootKey,
// eslint-disable-next-line
// @ts-ignore id is kept as fixed for idempotence and to avoid race condition
id: KMS_ROOT_CONFIG_UUID
});
return encryptedRootKey;
};
export const getSecretManagerDataKey = async (knex: Knex, projectId: string) => {
const KMS_VERSION = "v01";
const KMS_VERSION_BLOB_LENGTH = 3;
const cipher = symmetricCipherService(SymmetricEncryption.AES_GCM_256);
const project = await knex(TableName.Project).where({ id: projectId }).first();
if (!project) throw new Error("Missing project id");
const ROOT_ENCRYPTION_KEY = await getInstanceRootKey(knex);
let secretManagerKmsKey;
const projectSecretManagerKmsId = project?.kmsSecretManagerKeyId;
if (projectSecretManagerKmsId) {
const kmsDoc = await knex(TableName.KmsKey)
.leftJoin(TableName.InternalKms, `${TableName.KmsKey}.id`, `${TableName.InternalKms}.kmsKeyId`)
.where({ [`${TableName.KmsKey}.id` as "id"]: projectSecretManagerKmsId })
.first();
if (!kmsDoc) throw new Error("missing kms");
secretManagerKmsKey = cipher.decrypt(kmsDoc.encryptedKey, ROOT_ENCRYPTION_KEY);
} else {
const [kmsDoc] = await knex(TableName.KmsKey)
.insert({
slug: slugify(alphaNumericNanoId(8).toLowerCase()),
orgId: project.orgId,
isReserved: false
})
.returning("*");
secretManagerKmsKey = randomSecureBytes(32);
const encryptedKeyMaterial = cipher.encrypt(secretManagerKmsKey, ROOT_ENCRYPTION_KEY);
await knex(TableName.InternalKms).insert({
version: 1,
encryptedKey: encryptedKeyMaterial,
encryptionAlgorithm: SymmetricEncryption.AES_GCM_256,
kmsKeyId: kmsDoc.id
});
}
const encryptedSecretManagerDataKey = project?.kmsSecretManagerEncryptedDataKey;
let dataKey: Buffer;
if (!encryptedSecretManagerDataKey) {
dataKey = randomSecureBytes();
// the below versioning we do it automatically in kms service
const unversionedDataKey = cipher.encrypt(dataKey, secretManagerKmsKey);
const versionBlob = Buffer.from(KMS_VERSION, "utf8"); // length is 3
await knex(TableName.Project)
.where({ id: projectId })
.update({
kmsSecretManagerEncryptedDataKey: Buffer.concat([unversionedDataKey, versionBlob])
});
} else {
const cipherTextBlob = encryptedSecretManagerDataKey.subarray(0, -KMS_VERSION_BLOB_LENGTH);
dataKey = cipher.decrypt(cipherTextBlob, secretManagerKmsKey);
}
return {
encryptor: ({ plainText }: { plainText: Buffer }) => {
const encryptedPlainTextBlob = cipher.encrypt(plainText, dataKey);
// Buffer#1 encrypted text + Buffer#2 version number
const versionBlob = Buffer.from(KMS_VERSION, "utf8"); // length is 3
const cipherTextBlob = Buffer.concat([encryptedPlainTextBlob, versionBlob]);
return { cipherTextBlob };
},
decryptor: ({ cipherTextBlob: versionedCipherTextBlob }: { cipherTextBlob: Buffer }) => {
const cipherTextBlob = versionedCipherTextBlob.subarray(0, -KMS_VERSION_BLOB_LENGTH);
const decryptedBlob = cipher.decrypt(cipherTextBlob, dataKey);
return decryptedBlob;
}
};
};

View File

@ -5,8 +5,6 @@
import { z } from "zod";
import { EnforcementLevel } from "@app/lib/types";
import { TImmutableDBKeys } from "./models";
export const AccessApprovalPoliciesSchema = z.object({
@ -17,7 +15,7 @@ export const AccessApprovalPoliciesSchema = z.object({
envId: z.string().uuid(),
createdAt: z.date(),
updatedAt: z.date(),
enforcementLevel: z.nativeEnum(EnforcementLevel).default(EnforcementLevel.Hard)
enforcementLevel: z.string().default("hard")
});
export type TAccessApprovalPolicies = z.infer<typeof AccessApprovalPoliciesSchema>;

View File

@ -66,26 +66,35 @@ export * from "./scim-tokens";
export * from "./secret-approval-policies";
export * from "./secret-approval-policies-approvers";
export * from "./secret-approval-request-secret-tags";
export * from "./secret-approval-request-secret-tags-v2";
export * from "./secret-approval-requests";
export * from "./secret-approval-requests-reviewers";
export * from "./secret-approval-requests-secrets";
export * from "./secret-approval-requests-secrets-v2";
export * from "./secret-blind-indexes";
export * from "./secret-folder-versions";
export * from "./secret-folders";
export * from "./secret-imports";
export * from "./secret-references";
export * from "./secret-references-v2";
export * from "./secret-rotation-output-v2";
export * from "./secret-rotation-outputs";
export * from "./secret-rotations";
export * from "./secret-scanning-git-risks";
export * from "./secret-sharing";
export * from "./secret-snapshot-folders";
export * from "./secret-snapshot-secrets";
export * from "./secret-snapshot-secrets-v2";
export * from "./secret-snapshots";
export * from "./secret-tag-junction";
export * from "./secret-tags";
export * from "./secret-v2-tag-junction";
export * from "./secret-version-tag-junction";
export * from "./secret-version-v2-tag-junction";
export * from "./secret-versions";
export * from "./secret-versions-v2";
export * from "./secrets";
export * from "./secrets-v2";
export * from "./service-tokens";
export * from "./super-admin";
export * from "./trusted-ips";

View File

@ -5,6 +5,8 @@
import { z } from "zod";
import { zodBuffer } from "@app/lib/zod";
import { TImmutableDBKeys } from "./models";
export const IntegrationAuthsSchema = z.object({
@ -32,7 +34,11 @@ export const IntegrationAuthsSchema = z.object({
updatedAt: z.date(),
awsAssumeIamRoleArnCipherText: z.string().nullable().optional(),
awsAssumeIamRoleArnIV: z.string().nullable().optional(),
awsAssumeIamRoleArnTag: z.string().nullable().optional()
awsAssumeIamRoleArnTag: z.string().nullable().optional(),
encryptedAccess: zodBuffer.nullable().optional(),
encryptedAccessId: zodBuffer.nullable().optional(),
encryptedRefresh: zodBuffer.nullable().optional(),
encryptedAwsAssumeIamRoleArn: zodBuffer.nullable().optional()
});
export type TIntegrationAuths = z.infer<typeof IntegrationAuthsSchema>;

View File

@ -13,9 +13,9 @@ export const KmsKeysSchema = z.object({
isDisabled: z.boolean().default(false).nullable().optional(),
isReserved: z.boolean().default(true).nullable().optional(),
orgId: z.string().uuid(),
slug: z.string(),
createdAt: z.date(),
updatedAt: z.date(),
slug: z.string()
updatedAt: z.date()
});
export type TKmsKeys = z.infer<typeof KmsKeysSchema>;

View File

@ -90,9 +90,18 @@ export enum TableName {
TrustedIps = "trusted_ips",
DynamicSecret = "dynamic_secrets",
DynamicSecretLease = "dynamic_secret_leases",
SecretV2 = "secrets_v2",
SecretReferenceV2 = "secret_references_v2",
SecretVersionV2 = "secret_versions_v2",
SecretApprovalRequestSecretV2 = "secret_approval_requests_secrets_v2",
SecretApprovalRequestSecretTagV2 = "secret_approval_request_secret_tags_v2",
SnapshotSecretV2 = "secret_snapshot_secrets_v2",
// junction tables with tags
SecretV2JnTag = "secret_v2_tag_junction",
JnSecretTag = "secret_tag_junction",
SecretVersionTag = "secret_version_tag_junction",
SecretVersionV2Tag = "secret_version_v2_tag_junction",
SecretRotationOutputV2 = "secret_rotation_output_v2",
// KMS Service
KmsServerRootConfig = "kms_root_config",
KmsKey = "kms_keys",
@ -157,7 +166,8 @@ export enum SecretType {
export enum ProjectVersion {
V1 = 1,
V2 = 2
V2 = 2,
V3 = 3
}
export enum ProjectUpgradeStatus {

View File

@ -18,7 +18,7 @@ export const OrgMembershipsSchema = z.object({
orgId: z.string().uuid(),
roleId: z.string().uuid().nullable().optional(),
projectFavorites: z.string().array().nullable().optional(),
isActive: z.boolean()
isActive: z.boolean().default(true)
});
export type TOrgMemberships = z.infer<typeof OrgMembershipsSchema>;

View File

@ -5,6 +5,8 @@
import { z } from "zod";
import { zodBuffer } from "@app/lib/zod";
import { TImmutableDBKeys } from "./models";
export const OrganizationsSchema = z.object({
@ -16,7 +18,8 @@ export const OrganizationsSchema = z.object({
updatedAt: z.date(),
authEnforced: z.boolean().default(false).nullable().optional(),
scimEnabled: z.boolean().default(false).nullable().optional(),
kmsDefaultKeyId: z.string().uuid().nullable().optional()
kmsDefaultKeyId: z.string().uuid().nullable().optional(),
kmsEncryptedDataKey: zodBuffer.nullable().optional()
});
export type TOrganizations = z.infer<typeof OrganizationsSchema>;

View File

@ -5,6 +5,8 @@
import { z } from "zod";
import { zodBuffer } from "@app/lib/zod";
import { TImmutableDBKeys } from "./models";
export const ProjectsSchema = z.object({
@ -20,7 +22,8 @@ export const ProjectsSchema = z.object({
pitVersionLimit: z.number().default(10),
kmsCertificateKeyId: z.string().uuid().nullable().optional(),
auditLogsRetentionDays: z.number().nullable().optional(),
kmsSecretManagerKeyId: z.string().uuid().nullable().optional()
kmsSecretManagerKeyId: z.string().uuid().nullable().optional(),
kmsSecretManagerEncryptedDataKey: zodBuffer.nullable().optional()
});
export type TProjects = z.infer<typeof ProjectsSchema>;

View File

@ -15,7 +15,6 @@ export const RateLimitSchema = z.object({
authRateLimit: z.number().default(60),
inviteUserRateLimit: z.number().default(30),
mfaRateLimit: z.number().default(20),
creationLimit: z.number().default(30),
publicEndpointLimit: z.number().default(30),
createdAt: z.date(),
updatedAt: z.date()

View 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 SecretApprovalRequestSecretTagsV2Schema = z.object({
id: z.string().uuid(),
secretId: z.string().uuid(),
tagId: z.string().uuid(),
createdAt: z.date(),
updatedAt: z.date()
});
export type TSecretApprovalRequestSecretTagsV2 = z.infer<typeof SecretApprovalRequestSecretTagsV2Schema>;
export type TSecretApprovalRequestSecretTagsV2Insert = Omit<
z.input<typeof SecretApprovalRequestSecretTagsV2Schema>,
TImmutableDBKeys
>;
export type TSecretApprovalRequestSecretTagsV2Update = Partial<
Omit<z.input<typeof SecretApprovalRequestSecretTagsV2Schema>, TImmutableDBKeys>
>;

View File

@ -0,0 +1,37 @@
// Code generated by automation script, DO NOT EDIT.
// Automated by pulling database and generating zod schema
// To update. Just run npm run generate:schema
// Written by akhilmhdh.
import { z } from "zod";
import { zodBuffer } from "@app/lib/zod";
import { TImmutableDBKeys } from "./models";
export const SecretApprovalRequestsSecretsV2Schema = z.object({
id: z.string().uuid(),
version: z.number().default(1).nullable().optional(),
key: z.string(),
encryptedValue: zodBuffer.nullable().optional(),
encryptedComment: zodBuffer.nullable().optional(),
reminderNote: z.string().nullable().optional(),
reminderRepeatDays: z.number().nullable().optional(),
skipMultilineEncoding: z.boolean().default(false).nullable().optional(),
metadata: z.unknown().nullable().optional(),
createdAt: z.date(),
updatedAt: z.date(),
requestId: z.string().uuid(),
op: z.string(),
secretId: z.string().uuid().nullable().optional(),
secretVersion: z.string().uuid().nullable().optional()
});
export type TSecretApprovalRequestsSecretsV2 = z.infer<typeof SecretApprovalRequestsSecretsV2Schema>;
export type TSecretApprovalRequestsSecretsV2Insert = Omit<
z.input<typeof SecretApprovalRequestsSecretsV2Schema>,
TImmutableDBKeys
>;
export type TSecretApprovalRequestsSecretsV2Update = Partial<
Omit<z.input<typeof SecretApprovalRequestsSecretsV2Schema>, TImmutableDBKeys>
>;

View File

@ -19,7 +19,8 @@ export const SecretApprovalRequestsSchema = z.object({
updatedAt: z.date(),
isReplicated: z.boolean().nullable().optional(),
committerUserId: z.string().uuid(),
statusChangedByUserId: z.string().uuid().nullable().optional()
statusChangedByUserId: z.string().uuid().nullable().optional(),
bypassReason: z.string().nullable().optional()
});
export type TSecretApprovalRequests = z.infer<typeof SecretApprovalRequestsSchema>;

View File

@ -0,0 +1,20 @@
// Code generated by automation script, DO NOT EDIT.
// Automated by pulling database and generating zod schema
// To update. Just run npm run generate:schema
// Written by akhilmhdh.
import { z } from "zod";
import { TImmutableDBKeys } from "./models";
export const SecretReferencesV2Schema = z.object({
id: z.string().uuid(),
environment: z.string(),
secretPath: z.string(),
secretKey: z.string(),
secretId: z.string().uuid()
});
export type TSecretReferencesV2 = z.infer<typeof SecretReferencesV2Schema>;
export type TSecretReferencesV2Insert = Omit<z.input<typeof SecretReferencesV2Schema>, TImmutableDBKeys>;
export type TSecretReferencesV2Update = Partial<Omit<z.input<typeof SecretReferencesV2Schema>, TImmutableDBKeys>>;

View File

@ -0,0 +1,21 @@
// 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 SecretRotationOutputV2Schema = z.object({
id: z.string().uuid(),
key: z.string(),
secretId: z.string().uuid(),
rotationId: z.string().uuid()
});
export type TSecretRotationOutputV2 = z.infer<typeof SecretRotationOutputV2Schema>;
export type TSecretRotationOutputV2Insert = Omit<z.input<typeof SecretRotationOutputV2Schema>, TImmutableDBKeys>;
export type TSecretRotationOutputV2Update = Partial<
Omit<z.input<typeof SecretRotationOutputV2Schema>, TImmutableDBKeys>
>;

View File

@ -18,7 +18,10 @@ export const SecretSharingSchema = z.object({
orgId: z.string().uuid().nullable().optional(),
createdAt: z.date(),
updatedAt: z.date(),
expiresAfterViews: z.number().nullable().optional()
expiresAfterViews: z.number().nullable().optional(),
accessType: z.string().default("anyone"),
name: z.string().nullable().optional(),
lastViewedAt: z.date().nullable().optional()
});
export type TSecretSharing = z.infer<typeof SecretSharingSchema>;

View File

@ -0,0 +1,23 @@
// Code generated by automation script, DO NOT EDIT.
// Automated by pulling database and generating zod schema
// To update. Just run npm run generate:schema
// Written by akhilmhdh.
import { z } from "zod";
import { TImmutableDBKeys } from "./models";
export const SecretSnapshotSecretsV2Schema = z.object({
id: z.string().uuid(),
envId: z.string().uuid(),
secretVersionId: z.string().uuid(),
snapshotId: z.string().uuid(),
createdAt: z.date(),
updatedAt: z.date()
});
export type TSecretSnapshotSecretsV2 = z.infer<typeof SecretSnapshotSecretsV2Schema>;
export type TSecretSnapshotSecretsV2Insert = Omit<z.input<typeof SecretSnapshotSecretsV2Schema>, TImmutableDBKeys>;
export type TSecretSnapshotSecretsV2Update = Partial<
Omit<z.input<typeof SecretSnapshotSecretsV2Schema>, TImmutableDBKeys>
>;

View File

@ -0,0 +1,18 @@
// 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 SecretV2TagJunctionSchema = z.object({
id: z.string().uuid(),
secrets_v2Id: z.string().uuid(),
secret_tagsId: z.string().uuid()
});
export type TSecretV2TagJunction = z.infer<typeof SecretV2TagJunctionSchema>;
export type TSecretV2TagJunctionInsert = Omit<z.input<typeof SecretV2TagJunctionSchema>, TImmutableDBKeys>;
export type TSecretV2TagJunctionUpdate = Partial<Omit<z.input<typeof SecretV2TagJunctionSchema>, TImmutableDBKeys>>;

View File

@ -0,0 +1,23 @@
// Code generated by automation script, DO NOT EDIT.
// Automated by pulling database and generating zod schema
// To update. Just run npm run generate:schema
// Written by akhilmhdh.
import { z } from "zod";
import { TImmutableDBKeys } from "./models";
export const SecretVersionV2TagJunctionSchema = z.object({
id: z.string().uuid(),
secret_versions_v2Id: z.string().uuid(),
secret_tagsId: z.string().uuid()
});
export type TSecretVersionV2TagJunction = z.infer<typeof SecretVersionV2TagJunctionSchema>;
export type TSecretVersionV2TagJunctionInsert = Omit<
z.input<typeof SecretVersionV2TagJunctionSchema>,
TImmutableDBKeys
>;
export type TSecretVersionV2TagJunctionUpdate = Partial<
Omit<z.input<typeof SecretVersionV2TagJunctionSchema>, TImmutableDBKeys>
>;

View File

@ -0,0 +1,33 @@
// Code generated by automation script, DO NOT EDIT.
// Automated by pulling database and generating zod schema
// To update. Just run npm run generate:schema
// Written by akhilmhdh.
import { z } from "zod";
import { zodBuffer } from "@app/lib/zod";
import { TImmutableDBKeys } from "./models";
export const SecretVersionsV2Schema = z.object({
id: z.string().uuid(),
version: z.number().default(1),
type: z.string().default("shared"),
key: z.string(),
encryptedValue: zodBuffer.nullable().optional(),
encryptedComment: zodBuffer.nullable().optional(),
reminderNote: z.string().nullable().optional(),
reminderRepeatDays: z.number().nullable().optional(),
skipMultilineEncoding: z.boolean().default(false).nullable().optional(),
metadata: z.unknown().nullable().optional(),
envId: z.string().uuid().nullable().optional(),
secretId: z.string().uuid(),
folderId: z.string().uuid(),
userId: z.string().uuid().nullable().optional(),
createdAt: z.date(),
updatedAt: z.date()
});
export type TSecretVersionsV2 = z.infer<typeof SecretVersionsV2Schema>;
export type TSecretVersionsV2Insert = Omit<z.input<typeof SecretVersionsV2Schema>, TImmutableDBKeys>;
export type TSecretVersionsV2Update = Partial<Omit<z.input<typeof SecretVersionsV2Schema>, TImmutableDBKeys>>;

View File

@ -0,0 +1,31 @@
// Code generated by automation script, DO NOT EDIT.
// Automated by pulling database and generating zod schema
// To update. Just run npm run generate:schema
// Written by akhilmhdh.
import { z } from "zod";
import { zodBuffer } from "@app/lib/zod";
import { TImmutableDBKeys } from "./models";
export const SecretsV2Schema = z.object({
id: z.string().uuid(),
version: z.number().default(1),
type: z.string().default("shared"),
key: z.string(),
encryptedValue: zodBuffer.nullable().optional(),
encryptedComment: zodBuffer.nullable().optional(),
reminderNote: z.string().nullable().optional(),
reminderRepeatDays: z.number().nullable().optional(),
skipMultilineEncoding: z.boolean().default(false).nullable().optional(),
metadata: z.unknown().nullable().optional(),
userId: z.string().uuid().nullable().optional(),
folderId: z.string().uuid(),
createdAt: z.date(),
updatedAt: z.date()
});
export type TSecretsV2 = z.infer<typeof SecretsV2Schema>;
export type TSecretsV2Insert = Omit<z.input<typeof SecretsV2Schema>, TImmutableDBKeys>;
export type TSecretsV2Update = Partial<Omit<z.input<typeof SecretsV2Schema>, TImmutableDBKeys>>;

View File

@ -33,6 +33,11 @@ export const seedData1 = {
name: "first project",
slug: "first-project"
},
projectV3: {
id: "77fa7aed-9288-401e-a4c9-3a9430be62a4",
name: "first project v2",
slug: "first-project-v2"
},
environment: {
name: "Development",
slug: "dev"

View File

@ -0,0 +1,50 @@
import { Knex } from "knex";
import { ProjectMembershipRole, ProjectVersion, TableName } from "../schemas";
import { seedData1 } from "../seed-data";
export const DEFAULT_PROJECT_ENVS = [
{ name: "Development", slug: "dev" },
{ name: "Staging", slug: "staging" },
{ name: "Production", slug: "prod" }
];
export async function seed(knex: Knex): Promise<void> {
const [projectV2] = await knex(TableName.Project)
.insert({
name: seedData1.projectV3.name,
orgId: seedData1.organization.id,
slug: seedData1.projectV3.slug,
version: ProjectVersion.V3,
// eslint-disable-next-line
// @ts-ignore
id: seedData1.projectV3.id
})
.returning("*");
const projectMembershipV3 = await knex(TableName.ProjectMembership)
.insert({
projectId: projectV2.id,
userId: seedData1.id
})
.returning("*");
await knex(TableName.ProjectUserMembershipRole).insert({
role: ProjectMembershipRole.Admin,
projectMembershipId: projectMembershipV3[0].id
});
// create default environments and default folders
const projectV3Envs = await knex(TableName.Environment)
.insert(
DEFAULT_PROJECT_ENVS.map(({ name, slug }, index) => ({
name,
slug,
projectId: seedData1.projectV3.id,
position: index + 1
}))
)
.returning("*");
await knex(TableName.SecretFolder).insert(
projectV3Envs.map(({ id }) => ({ name: "root", envId: id, parentId: null }))
);
}

View File

@ -86,4 +86,15 @@ export async function seed(knex: Knex): Promise<void> {
role: ProjectMembershipRole.Admin,
projectMembershipId: identityProjectMembership[0].id
});
const identityProjectMembershipV3 = await knex(TableName.IdentityProjectMembership)
.insert({
identityId: seedData1.machineIdentity.id,
projectId: seedData1.projectV3.id
})
.returning("*");
await knex(TableName.IdentityProjectMembershipRole).insert({
role: ProjectMembershipRole.Admin,
projectMembershipId: identityProjectMembershipV3[0].id
});
}

View File

@ -1,6 +1,7 @@
import { z } from "zod";
import { ExternalKmsSchema, KmsKeysSchema } from "@app/db/schemas";
import { EventType } from "@app/ee/services/audit-log/audit-log-types";
import {
ExternalKmsAwsSchema,
ExternalKmsInputSchema,
@ -19,6 +20,23 @@ const sanitizedExternalSchema = KmsKeysSchema.extend({
})
});
const sanitizedExternalSchemaForGetAll = KmsKeysSchema.pick({
id: true,
description: true,
isDisabled: true,
createdAt: true,
updatedAt: true,
slug: true
})
.extend({
externalKms: ExternalKmsSchema.pick({
provider: true,
status: true,
statusDetails: true
})
})
.array();
const sanitizedExternalSchemaForGetById = KmsKeysSchema.extend({
external: ExternalKmsSchema.pick({
id: true,
@ -39,8 +57,8 @@ export const registerExternalKmsRouter = async (server: FastifyZodProvider) => {
},
schema: {
body: z.object({
slug: z.string().min(1).trim().toLowerCase().optional(),
description: z.string().min(1).trim().optional(),
slug: z.string().min(1).trim().toLowerCase(),
description: z.string().trim().optional(),
provider: ExternalKmsInputSchema
}),
response: {
@ -60,6 +78,21 @@ export const registerExternalKmsRouter = async (server: FastifyZodProvider) => {
provider: req.body.provider,
description: req.body.description
});
await server.services.auditLog.createAuditLog({
...req.auditLogInfo,
orgId: req.permission.orgId,
event: {
type: EventType.CREATE_KMS,
metadata: {
kmsId: externalKms.id,
provider: req.body.provider.type,
slug: req.body.slug,
description: req.body.description
}
}
});
return { externalKms };
}
});
@ -76,7 +109,7 @@ export const registerExternalKmsRouter = async (server: FastifyZodProvider) => {
}),
body: z.object({
slug: z.string().min(1).trim().toLowerCase().optional(),
description: z.string().min(1).trim().optional(),
description: z.string().trim().optional(),
provider: ExternalKmsInputUpdateSchema
}),
response: {
@ -97,6 +130,21 @@ export const registerExternalKmsRouter = async (server: FastifyZodProvider) => {
description: req.body.description,
id: req.params.id
});
await server.services.auditLog.createAuditLog({
...req.auditLogInfo,
orgId: req.permission.orgId,
event: {
type: EventType.UPDATE_KMS,
metadata: {
kmsId: externalKms.id,
provider: req.body.provider.type,
slug: req.body.slug,
description: req.body.description
}
}
});
return { externalKms };
}
});
@ -126,6 +174,19 @@ export const registerExternalKmsRouter = async (server: FastifyZodProvider) => {
actorOrgId: req.permission.orgId,
id: req.params.id
});
await server.services.auditLog.createAuditLog({
...req.auditLogInfo,
orgId: req.permission.orgId,
event: {
type: EventType.DELETE_KMS,
metadata: {
kmsId: externalKms.id,
slug: externalKms.slug
}
}
});
return { externalKms };
}
});
@ -155,10 +216,48 @@ export const registerExternalKmsRouter = async (server: FastifyZodProvider) => {
actorOrgId: req.permission.orgId,
id: req.params.id
});
await server.services.auditLog.createAuditLog({
...req.auditLogInfo,
orgId: req.permission.orgId,
event: {
type: EventType.GET_KMS,
metadata: {
kmsId: externalKms.id,
slug: externalKms.slug
}
}
});
return { externalKms };
}
});
server.route({
method: "GET",
url: "/",
config: {
rateLimit: readLimit
},
schema: {
response: {
200: z.object({
externalKmsList: sanitizedExternalSchemaForGetAll
})
}
},
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
handler: async (req) => {
const externalKmsList = await server.services.externalKms.list({
actor: req.permission.type,
actorId: req.permission.id,
actorAuthMethod: req.permission.authMethod,
actorOrgId: req.permission.orgId
});
return { externalKmsList };
}
});
server.route({
method: "GET",
url: "/slug/:slug",

View File

@ -4,6 +4,7 @@ import { registerAuditLogStreamRouter } from "./audit-log-stream-router";
import { registerCaCrlRouter } from "./certificate-authority-crl-router";
import { registerDynamicSecretLeaseRouter } from "./dynamic-secret-lease-router";
import { registerDynamicSecretRouter } from "./dynamic-secret-router";
import { registerExternalKmsRouter } from "./external-kms-router";
import { registerGroupRouter } from "./group-router";
import { registerIdentityProjectAdditionalPrivilegeRouter } from "./identity-project-additional-privilege-router";
import { registerLdapRouter } from "./ldap-router";
@ -87,4 +88,8 @@ export const registerV1EERoutes = async (server: FastifyZodProvider) => {
},
{ prefix: "/additional-privilege" }
);
await server.register(registerExternalKmsRouter, {
prefix: "/external-kms"
});
};

View File

@ -107,7 +107,7 @@ export const registerOrgRoleRouter = async (server: FastifyZodProvider) => {
}),
name: z.string().trim().optional(),
description: z.string().trim().optional(),
permissions: z.any().array()
permissions: z.any().array().optional()
}),
response: {
200: z.object({

View File

@ -101,7 +101,7 @@ export const registerProjectRoleRouter = async (server: FastifyZodProvider) => {
message: "Slug must be a valid"
}),
name: z.string().trim().optional().describe(PROJECT_ROLE.UPDATE.name),
permissions: ProjectPermissionSchema.array().describe(PROJECT_ROLE.UPDATE.permissions)
permissions: ProjectPermissionSchema.array().describe(PROJECT_ROLE.UPDATE.permissions).optional()
}),
response: {
200: z.object({
@ -120,7 +120,7 @@ export const registerProjectRoleRouter = async (server: FastifyZodProvider) => {
roleId: req.params.roleId,
data: {
...req.body,
permissions: JSON.stringify(packRules(req.body.permissions))
permissions: req.body.permissions ? JSON.stringify(packRules(req.body.permissions)) : undefined
}
});
return { role };

View File

@ -4,9 +4,10 @@ import { AuditLogsSchema, SecretSnapshotsSchema } from "@app/db/schemas";
import { EventType, UserAgentType } from "@app/ee/services/audit-log/audit-log-types";
import { AUDIT_LOGS, PROJECTS } from "@app/lib/api-docs";
import { getLastMidnightDateISO, removeTrailingSlash } from "@app/lib/fn";
import { readLimit } from "@app/server/config/rateLimiter";
import { readLimit, writeLimit } from "@app/server/config/rateLimiter";
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
import { AuthMode } from "@app/services/auth/auth-type";
import { KmsType } from "@app/services/kms/kms-types";
export const registerProjectRouter = async (server: FastifyZodProvider) => {
server.route({
@ -171,4 +172,212 @@ export const registerProjectRouter = async (server: FastifyZodProvider) => {
onRequest: verifyAuth([AuthMode.JWT]),
handler: async () => ({ actors: [] })
});
server.route({
method: "GET",
url: "/:workspaceId/kms",
config: {
rateLimit: readLimit
},
schema: {
params: z.object({
workspaceId: z.string().trim()
}),
response: {
200: z.object({
secretManagerKmsKey: z.object({
id: z.string(),
slug: z.string(),
isExternal: z.boolean()
})
})
}
},
onRequest: verifyAuth([AuthMode.JWT]),
handler: async (req) => {
const kmsKey = await server.services.project.getProjectKmsKeys({
actor: req.permission.type,
actorId: req.permission.id,
actorAuthMethod: req.permission.authMethod,
actorOrgId: req.permission.orgId,
projectId: req.params.workspaceId
});
return kmsKey;
}
});
server.route({
method: "PATCH",
url: "/:workspaceId/kms",
config: {
rateLimit: writeLimit
},
schema: {
params: z.object({
workspaceId: z.string().trim()
}),
body: z.object({
kms: z.discriminatedUnion("type", [
z.object({ type: z.literal(KmsType.Internal) }),
z.object({ type: z.literal(KmsType.External), kmsId: z.string() })
])
}),
response: {
200: z.object({
secretManagerKmsKey: z.object({
id: z.string(),
slug: z.string(),
isExternal: z.boolean()
})
})
}
},
onRequest: verifyAuth([AuthMode.JWT]),
handler: async (req) => {
const { secretManagerKmsKey } = await server.services.project.updateProjectKmsKey({
actor: req.permission.type,
actorId: req.permission.id,
actorAuthMethod: req.permission.authMethod,
actorOrgId: req.permission.orgId,
projectId: req.params.workspaceId,
...req.body
});
await server.services.auditLog.createAuditLog({
...req.auditLogInfo,
projectId: req.params.workspaceId,
event: {
type: EventType.UPDATE_PROJECT_KMS,
metadata: {
secretManagerKmsKey: {
id: secretManagerKmsKey.id,
slug: secretManagerKmsKey.slug
}
}
}
});
return {
secretManagerKmsKey
};
}
});
server.route({
method: "GET",
url: "/:workspaceId/kms/backup",
config: {
rateLimit: readLimit
},
schema: {
params: z.object({
workspaceId: z.string().trim()
}),
response: {
200: z.object({
secretManager: z.string()
})
}
},
onRequest: verifyAuth([AuthMode.JWT]),
handler: async (req) => {
const backup = await server.services.project.getProjectKmsBackup({
actor: req.permission.type,
actorId: req.permission.id,
actorAuthMethod: req.permission.authMethod,
actorOrgId: req.permission.orgId,
projectId: req.params.workspaceId
});
await server.services.auditLog.createAuditLog({
...req.auditLogInfo,
projectId: req.params.workspaceId,
event: {
type: EventType.GET_PROJECT_KMS_BACKUP,
metadata: {}
}
});
return backup;
}
});
server.route({
method: "POST",
url: "/:workspaceId/kms/backup",
config: {
rateLimit: writeLimit
},
schema: {
params: z.object({
workspaceId: z.string().trim()
}),
body: z.object({
backup: z.string().min(1)
}),
response: {
200: z.object({
secretManagerKmsKey: z.object({
id: z.string(),
slug: z.string(),
isExternal: z.boolean()
})
})
}
},
onRequest: verifyAuth([AuthMode.JWT]),
handler: async (req) => {
const backup = await server.services.project.loadProjectKmsBackup({
actor: req.permission.type,
actorId: req.permission.id,
actorAuthMethod: req.permission.authMethod,
actorOrgId: req.permission.orgId,
projectId: req.params.workspaceId,
backup: req.body.backup
});
await server.services.auditLog.createAuditLog({
...req.auditLogInfo,
projectId: req.params.workspaceId,
event: {
type: EventType.LOAD_PROJECT_KMS_BACKUP,
metadata: {}
}
});
return backup;
}
});
server.route({
method: "POST",
url: "/:workspaceId/migrate-v3",
config: {
rateLimit: writeLimit
},
schema: {
params: z.object({
workspaceId: z.string().trim()
}),
response: {
200: z.object({
message: z.string()
})
}
},
onRequest: verifyAuth([AuthMode.JWT]),
handler: async (req) => {
const migration = await server.services.secret.startSecretV2Migration({
actor: req.permission.type,
actorId: req.permission.id,
actorAuthMethod: req.permission.authMethod,
actorOrgId: req.permission.orgId,
projectId: req.params.workspaceId
});
return migration;
}
});
};

View File

@ -58,7 +58,6 @@ export const registerRateLimitRouter = async (server: FastifyZodProvider) => {
authRateLimit: z.number(),
inviteUserRateLimit: z.number(),
mfaRateLimit: z.number(),
creationLimit: z.number(),
publicEndpointLimit: z.number()
}),
response: {

View File

@ -3,16 +3,14 @@ import { z } from "zod";
import {
SecretApprovalRequestsReviewersSchema,
SecretApprovalRequestsSchema,
SecretApprovalRequestsSecretsSchema,
SecretsSchema,
SecretTagsSchema,
SecretVersionsSchema,
UsersSchema
} from "@app/db/schemas";
import { EventType } from "@app/ee/services/audit-log/audit-log-types";
import { ApprovalStatus, RequestState } from "@app/ee/services/secret-approval-request/secret-approval-request-types";
import { readLimit, writeLimit } from "@app/server/config/rateLimiter";
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
import { secretRawSchema } from "@app/server/routes/sanitizedSchemas";
import { AuthMode } from "@app/services/auth/auth-type";
const approvalRequestUser = z.object({ userId: z.string() }).merge(
@ -117,6 +115,9 @@ export const registerSecretApprovalRequestRouter = async (server: FastifyZodProv
params: z.object({
id: z.string()
}),
body: z.object({
bypassReason: z.string().optional()
}),
response: {
200: z.object({
approval: SecretApprovalRequestsSchema
@ -130,7 +131,8 @@ export const registerSecretApprovalRequestRouter = async (server: FastifyZodProv
actor: req.permission.type,
actorAuthMethod: req.permission.authMethod,
actorOrgId: req.permission.orgId,
approvalId: req.params.id
approvalId: req.params.id,
bypassReason: req.body.bypassReason
});
return { approval };
}
@ -257,46 +259,32 @@ export const registerSecretApprovalRequestRouter = async (server: FastifyZodProv
committerUser: approvalRequestUser,
reviewers: approvalRequestUser.extend({ status: z.string() }).array(),
secretPath: z.string(),
commits: SecretApprovalRequestsSecretsSchema.omit({ secretBlindIndex: true })
.merge(
z.object({
tags: tagSchema,
secret: SecretsSchema.pick({
id: true,
version: true,
secretKeyIV: true,
secretKeyTag: true,
secretKeyCiphertext: true,
secretValueIV: true,
secretValueTag: true,
secretValueCiphertext: true,
secretCommentIV: true,
secretCommentTag: true,
secretCommentCiphertext: true
commits: secretRawSchema
.omit({ _id: true, environment: true, workspace: true, type: true, version: true })
.extend({
op: z.string(),
tags: tagSchema,
secret: z
.object({
id: z.string(),
version: z.number(),
secretKey: z.string(),
secretValue: z.string().optional(),
secretComment: z.string().optional()
})
.optional()
.nullable(),
secretVersion: SecretVersionsSchema.pick({
id: true,
version: true,
secretKeyIV: true,
secretKeyTag: true,
secretKeyCiphertext: true,
secretValueIV: true,
secretValueTag: true,
secretValueCiphertext: true,
secretCommentIV: true,
secretCommentTag: true,
secretCommentCiphertext: true
.optional()
.nullable(),
secretVersion: z
.object({
id: z.string(),
version: z.number(),
secretKey: z.string(),
secretValue: z.string().optional(),
secretComment: z.string().optional(),
tags: tagSchema
})
.merge(
z.object({
tags: tagSchema
})
)
.optional()
})
)
.optional()
})
.array()
})
)

View File

@ -1,6 +1,6 @@
import { z } from "zod";
import { SecretRotationOutputsSchema, SecretRotationsSchema, SecretsSchema } from "@app/db/schemas";
import { SecretRotationOutputsSchema, SecretRotationsSchema } from "@app/db/schemas";
import { removeTrailingSlash } from "@app/lib/fn";
import { readLimit, writeLimit } from "@app/server/config/rateLimiter";
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
@ -112,18 +112,10 @@ export const registerSecretRotationRouter = async (server: FastifyZodProvider) =
outputs: z
.object({
key: z.string(),
secret: SecretsSchema.pick({
id: true,
version: true,
secretKeyIV: true,
secretKeyTag: true,
secretKeyCiphertext: true,
secretValueIV: true,
secretValueTag: true,
secretValueCiphertext: true,
secretCommentIV: true,
secretCommentTag: true,
secretCommentCiphertext: true
secret: z.object({
secretKey: z.string(),
id: z.string(),
version: z.number()
})
})
.array()

View File

@ -1,8 +1,8 @@
import { z } from "zod";
import { SecretVersionsSchema } from "@app/db/schemas";
import { readLimit } from "@app/server/config/rateLimiter";
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
import { secretRawSchema } from "@app/server/routes/sanitizedSchemas";
import { AuthMode } from "@app/services/auth/auth-type";
export const registerSecretVersionRouter = async (server: FastifyZodProvider) => {
@ -22,7 +22,7 @@ export const registerSecretVersionRouter = async (server: FastifyZodProvider) =>
}),
response: {
200: z.object({
secretVersions: SecretVersionsSchema.omit({ secretBlindIndex: true }).array()
secretVersions: secretRawSchema.array()
})
}
},

View File

@ -1,9 +1,10 @@
import { z } from "zod";
import { SecretSnapshotsSchema, SecretTagsSchema, SecretVersionsSchema } from "@app/db/schemas";
import { SecretSnapshotsSchema, SecretTagsSchema } from "@app/db/schemas";
import { PROJECTS } from "@app/lib/api-docs";
import { readLimit, writeLimit } from "@app/server/config/rateLimiter";
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
import { secretRawSchema } from "@app/server/routes/sanitizedSchemas";
import { AuthMode } from "@app/services/auth/auth-type";
export const registerSnapshotRouter = async (server: FastifyZodProvider) => {
@ -27,17 +28,17 @@ export const registerSnapshotRouter = async (server: FastifyZodProvider) => {
slug: z.string(),
name: z.string()
}),
secretVersions: SecretVersionsSchema.omit({ secretBlindIndex: true })
.merge(
z.object({
tags: SecretTagsSchema.pick({
id: true,
slug: true,
name: true,
color: true
}).array()
})
)
secretVersions: secretRawSchema
.omit({ _id: true, environment: true, workspace: true, type: true })
.extend({
secretId: z.string(),
tags: SecretTagsSchema.pick({
id: true,
slug: true,
name: true,
color: true
}).array()
})
.array(),
folderVersion: z.object({ id: z.string(), name: z.string() }).array(),
createdAt: z.date(),

View File

@ -106,6 +106,7 @@ export enum EventType {
CREATE_ENVIRONMENT = "create-environment",
UPDATE_ENVIRONMENT = "update-environment",
DELETE_ENVIRONMENT = "delete-environment",
GET_ENVIRONMENT = "get-environment",
ADD_WORKSPACE_MEMBER = "add-workspace-member",
ADD_BATCH_WORKSPACE_MEMBER = "add-workspace-members",
REMOVE_WORKSPACE_MEMBER = "remove-workspace-member",
@ -135,10 +136,19 @@ export enum EventType {
IMPORT_CA_CERT = "import-certificate-authority-cert",
GET_CA_CRL = "get-certificate-authority-crl",
ISSUE_CERT = "issue-cert",
SIGN_CERT = "sign-cert",
GET_CERT = "get-cert",
DELETE_CERT = "delete-cert",
REVOKE_CERT = "revoke-cert",
GET_CERT_BODY = "get-cert-body"
GET_CERT_BODY = "get-cert-body",
CREATE_KMS = "create-kms",
UPDATE_KMS = "update-kms",
DELETE_KMS = "delete-kms",
GET_KMS = "get-kms",
UPDATE_PROJECT_KMS = "update-project-kms",
GET_PROJECT_KMS_BACKUP = "get-project-kms-backup",
LOAD_PROJECT_KMS_BACKUP = "load-project-kms-backup",
ORG_ADMIN_ACCESS_PROJECT = "org-admin-accessed-project"
}
interface UserActorMetadata {
@ -328,6 +338,7 @@ interface DeleteIntegrationEvent {
targetServiceId?: string;
path?: string;
region?: string;
shouldDeleteIntegrationSecrets?: boolean;
};
}
@ -831,6 +842,13 @@ interface CreateEnvironmentEvent {
};
}
interface GetEnvironmentEvent {
type: EventType.GET_ENVIRONMENT;
metadata: {
id: string;
};
}
interface UpdateEnvironmentEvent {
type: EventType.UPDATE_ENVIRONMENT;
metadata: {
@ -1128,6 +1146,15 @@ interface IssueCert {
};
}
interface SignCert {
type: EventType.SIGN_CERT;
metadata: {
caId: string;
dn: string;
serialNumber: string;
};
}
interface GetCert {
type: EventType.GET_CERT;
metadata: {
@ -1164,6 +1191,72 @@ interface GetCertBody {
};
}
interface CreateKmsEvent {
type: EventType.CREATE_KMS;
metadata: {
kmsId: string;
provider: string;
slug: string;
description?: string;
};
}
interface DeleteKmsEvent {
type: EventType.DELETE_KMS;
metadata: {
kmsId: string;
slug: string;
};
}
interface UpdateKmsEvent {
type: EventType.UPDATE_KMS;
metadata: {
kmsId: string;
provider: string;
slug?: string;
description?: string;
};
}
interface GetKmsEvent {
type: EventType.GET_KMS;
metadata: {
kmsId: string;
slug: string;
};
}
interface UpdateProjectKmsEvent {
type: EventType.UPDATE_PROJECT_KMS;
metadata: {
secretManagerKmsKey: {
id: string;
slug: string;
};
};
}
interface GetProjectKmsBackupEvent {
type: EventType.GET_PROJECT_KMS_BACKUP;
metadata: Record<string, string>; // no metadata yet
}
interface LoadProjectKmsBackupEvent {
type: EventType.LOAD_PROJECT_KMS_BACKUP;
metadata: Record<string, string>; // no metadata yet
}
interface OrgAdminAccessProjectEvent {
type: EventType.ORG_ADMIN_ACCESS_PROJECT;
metadata: {
userId: string;
username: string;
email: string;
projectId: string;
}; // no metadata yet
}
export type Event =
| GetSecretsEvent
| GetSecretEvent
@ -1230,6 +1323,7 @@ export type Event =
| UpdateIdentityOidcAuthEvent
| GetIdentityOidcAuthEvent
| CreateEnvironmentEvent
| GetEnvironmentEvent
| UpdateEnvironmentEvent
| DeleteEnvironmentEvent
| AddWorkspaceMemberEvent
@ -1261,7 +1355,16 @@ export type Event =
| ImportCaCert
| GetCaCrl
| IssueCert
| SignCert
| GetCert
| DeleteCert
| RevokeCert
| GetCertBody;
| GetCertBody
| CreateKmsEvent
| UpdateKmsEvent
| DeleteKmsEvent
| GetKmsEvent
| UpdateProjectKmsEvent
| GetProjectKmsBackupEvent
| LoadProjectKmsBackupEvent
| OrgAdminAccessProjectEvent;

View File

@ -72,7 +72,7 @@ export const certificateAuthorityCrlServiceFactory = ({
kmsId: keyId
});
const decryptedCrl = kmsDecryptor({ cipherTextBlob: caCrl.encryptedCrl });
const decryptedCrl = await kmsDecryptor({ cipherTextBlob: caCrl.encryptedCrl });
const crl = new x509.X509Crl(decryptedCrl);
const base64crl = crl.toString("base64");

View File

@ -12,10 +12,7 @@ export const dynamicSecretLeaseDALFactory = (db: TDbClient) => {
const countLeasesForDynamicSecret = async (dynamicSecretId: string, tx?: Knex) => {
try {
const doc = await (tx || db.replicaNode())(TableName.DynamicSecretLease)
.count("*")
.where({ dynamicSecretId })
.first();
const doc = await (tx || db)(TableName.DynamicSecretLease).count("*").where({ dynamicSecretId }).first();
return parseInt(doc || "0", 10);
} catch (error) {
throw new DatabaseError({ error, name: "DynamicSecretCountLeases" });
@ -24,7 +21,7 @@ export const dynamicSecretLeaseDALFactory = (db: TDbClient) => {
const findById = async (id: string, tx?: Knex) => {
try {
const doc = await (tx || db.replicaNode())(TableName.DynamicSecretLease)
const doc = await (tx || db)(TableName.DynamicSecretLease)
.where({ [`${TableName.DynamicSecretLease}.id` as "id"]: id })
.first()
.join(

View File

@ -31,6 +31,8 @@ export const externalKmsDALFactory = (db: TDbClient) => {
isReserved: el.isReserved,
orgId: el.orgId,
slug: el.slug,
createdAt: el.createdAt,
updatedAt: el.updatedAt,
externalKms: {
id: el.externalKmsId,
provider: el.externalKmsProvider,

View File

@ -5,7 +5,9 @@ import { BadRequestError } from "@app/lib/errors";
import { alphaNumericNanoId } from "@app/lib/nanoid";
import { TKmsKeyDALFactory } from "@app/services/kms/kms-key-dal";
import { TKmsServiceFactory } from "@app/services/kms/kms-service";
import { KmsDataKey } from "@app/services/kms/kms-types";
import { TLicenseServiceFactory } from "../license/license-service";
import { OrgPermissionActions, OrgPermissionSubjects } from "../permission/org-permission";
import { TPermissionServiceFactory } from "../permission/permission-service";
import { TExternalKmsDALFactory } from "./external-kms-dal";
@ -22,9 +24,10 @@ import { ExternalKmsAwsSchema, KmsProviders } from "./providers/model";
type TExternalKmsServiceFactoryDep = {
externalKmsDAL: TExternalKmsDALFactory;
kmsService: Pick<TKmsServiceFactory, "getOrgKmsKeyId" | "encryptWithKmsKey" | "decryptWithKmsKey">;
kmsService: Pick<TKmsServiceFactory, "getOrgKmsKeyId" | "createCipherPairWithDataKey">;
kmsDAL: Pick<TKmsKeyDALFactory, "create" | "updateById" | "findById" | "deleteById" | "findOne">;
permissionService: Pick<TPermissionServiceFactory, "getOrgPermission">;
licenseService: Pick<TLicenseServiceFactory, "getPlan">;
};
export type TExternalKmsServiceFactory = ReturnType<typeof externalKmsServiceFactory>;
@ -32,6 +35,7 @@ export type TExternalKmsServiceFactory = ReturnType<typeof externalKmsServiceFac
export const externalKmsServiceFactory = ({
externalKmsDAL,
permissionService,
licenseService,
kmsService,
kmsDAL
}: TExternalKmsServiceFactoryDep) => {
@ -51,7 +55,15 @@ export const externalKmsServiceFactory = ({
actorAuthMethod,
actorOrgId
);
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Edit, OrgPermissionSubjects.Settings);
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Create, OrgPermissionSubjects.Kms);
const plan = await licenseService.getPlan(actorOrgId);
if (!plan.externalKms) {
throw new BadRequestError({
message: "Failed to create external KMS due to plan restriction. Upgrade to the Enterprise plan."
});
}
const kmsSlug = slug ? slugify(slug) : slugify(alphaNumericNanoId(8).toLowerCase());
let sanitizedProviderInput = "";
@ -59,21 +71,23 @@ export const externalKmsServiceFactory = ({
case KmsProviders.Aws:
{
const externalKms = await AwsKmsProviderFactory({ inputs: provider.inputs });
await externalKms.validateConnection();
// if missing kms key this generate a new kms key id and returns new provider input
const newProviderInput = await externalKms.generateInputKmsKey();
sanitizedProviderInput = JSON.stringify(newProviderInput);
await externalKms.validateConnection();
}
break;
default:
throw new BadRequestError({ message: "external kms provided is invalid" });
}
const orgKmsKeyId = await kmsService.getOrgKmsKeyId(actorOrgId);
const kmsEncryptor = await kmsService.encryptWithKmsKey({
kmsId: orgKmsKeyId
const { encryptor: orgDataKeyEncryptor } = await kmsService.createCipherPairWithDataKey({
type: KmsDataKey.Organization,
orgId: actorOrgId
});
const { cipherTextBlob: encryptedProviderInputs } = kmsEncryptor({
const { cipherTextBlob: encryptedProviderInputs } = orgDataKeyEncryptor({
plainText: Buffer.from(sanitizedProviderInput, "utf8")
});
@ -119,19 +133,28 @@ export const externalKmsServiceFactory = ({
actorAuthMethod,
actorOrgId
);
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Edit, OrgPermissionSubjects.Settings);
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Edit, OrgPermissionSubjects.Kms);
const plan = await licenseService.getPlan(kmsDoc.orgId);
if (!plan.externalKms) {
throw new BadRequestError({
message: "Failed to update external KMS due to plan restriction. Upgrade to the Enterprise plan."
});
}
const kmsSlug = slug ? slugify(slug) : undefined;
const externalKmsDoc = await externalKmsDAL.findOne({ kmsKeyId: kmsDoc.id });
if (!externalKmsDoc) throw new BadRequestError({ message: "External kms not found" });
const orgDefaultKmsId = await kmsService.getOrgKmsKeyId(kmsDoc.orgId);
let sanitizedProviderInput = "";
if (provider) {
const kmsDecryptor = await kmsService.decryptWithKmsKey({
kmsId: orgDefaultKmsId
const { encryptor: orgDataKeyEncryptor, decryptor: orgDataKeyDecryptor } =
await kmsService.createCipherPairWithDataKey({
type: KmsDataKey.Organization,
orgId: actorOrgId
});
const decryptedProviderInputBlob = kmsDecryptor({
if (provider) {
const decryptedProviderInputBlob = orgDataKeyDecryptor({
cipherTextBlob: externalKmsDoc.encryptedProviderInputs
});
@ -154,10 +177,7 @@ export const externalKmsServiceFactory = ({
let encryptedProviderInputs: Buffer | undefined;
if (sanitizedProviderInput) {
const kmsEncryptor = await kmsService.encryptWithKmsKey({
kmsId: orgDefaultKmsId
});
const { cipherTextBlob } = kmsEncryptor({
const { cipherTextBlob } = orgDataKeyEncryptor({
plainText: Buffer.from(sanitizedProviderInput, "utf8")
});
encryptedProviderInputs = cipherTextBlob;
@ -197,7 +217,7 @@ export const externalKmsServiceFactory = ({
actorAuthMethod,
actorOrgId
);
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Edit, OrgPermissionSubjects.Settings);
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Delete, OrgPermissionSubjects.Kms);
const externalKmsDoc = await externalKmsDAL.findOne({ kmsKeyId: kmsDoc.id });
if (!externalKmsDoc) throw new BadRequestError({ message: "External kms not found" });
@ -218,7 +238,7 @@ export const externalKmsServiceFactory = ({
actorAuthMethod,
actorOrgId
);
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Edit, OrgPermissionSubjects.Settings);
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Read, OrgPermissionSubjects.Kms);
const externalKmsDocs = await externalKmsDAL.find({ orgId: actorOrgId });
@ -234,16 +254,18 @@ export const externalKmsServiceFactory = ({
actorAuthMethod,
actorOrgId
);
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Edit, OrgPermissionSubjects.Settings);
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Read, OrgPermissionSubjects.Kms);
const externalKmsDoc = await externalKmsDAL.findOne({ kmsKeyId: kmsDoc.id });
if (!externalKmsDoc) throw new BadRequestError({ message: "External kms not found" });
const orgDefaultKmsId = await kmsService.getOrgKmsKeyId(kmsDoc.orgId);
const kmsDecryptor = await kmsService.decryptWithKmsKey({
kmsId: orgDefaultKmsId
const { decryptor: orgDataKeyDecryptor } = await kmsService.createCipherPairWithDataKey({
type: KmsDataKey.Organization,
orgId: actorOrgId
});
const decryptedProviderInputBlob = kmsDecryptor({
const decryptedProviderInputBlob = orgDataKeyDecryptor({
cipherTextBlob: externalKmsDoc.encryptedProviderInputs
});
switch (externalKmsDoc.provider) {
@ -273,16 +295,17 @@ export const externalKmsServiceFactory = ({
actorAuthMethod,
actorOrgId
);
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Edit, OrgPermissionSubjects.Settings);
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Read, OrgPermissionSubjects.Kms);
const externalKmsDoc = await externalKmsDAL.findOne({ kmsKeyId: kmsDoc.id });
if (!externalKmsDoc) throw new BadRequestError({ message: "External kms not found" });
const orgDefaultKmsId = await kmsService.getOrgKmsKeyId(kmsDoc.orgId);
const kmsDecryptor = await kmsService.decryptWithKmsKey({
kmsId: orgDefaultKmsId
const { decryptor: orgDataKeyDecryptor } = await kmsService.createCipherPairWithDataKey({
type: KmsDataKey.Organization,
orgId: actorOrgId
});
const decryptedProviderInputBlob = kmsDecryptor({
const decryptedProviderInputBlob = orgDataKeyDecryptor({
cipherTextBlob: externalKmsDoc.encryptedProviderInputs
});

View File

@ -50,17 +50,26 @@ type TAwsKmsProviderFactoryReturn = TExternalKmsProviderFns & {
};
export const AwsKmsProviderFactory = async ({ inputs }: AwsKmsProviderArgs): Promise<TAwsKmsProviderFactoryReturn> => {
const providerInputs = await ExternalKmsAwsSchema.parseAsync(inputs);
const awsClient = await getAwsKmsClient(providerInputs);
let providerInputs = await ExternalKmsAwsSchema.parseAsync(inputs);
let awsClient = await getAwsKmsClient(providerInputs);
const generateInputKmsKey = async () => {
if (providerInputs.kmsKeyId) return providerInputs;
const command = new CreateKeyCommand({ Tags: [{ TagKey: "author", TagValue: "infisical" }] });
const kmsKey = await awsClient.send(command);
if (!kmsKey.KeyMetadata?.KeyId) throw new Error("Failed to generate kms key");
return { ...providerInputs, kmsKeyId: kmsKey.KeyMetadata?.KeyId };
const updatedProviderInputs = await ExternalKmsAwsSchema.parseAsync({
...providerInputs,
kmsKeyId: kmsKey.KeyMetadata?.KeyId
});
providerInputs = updatedProviderInputs;
awsClient = await getAwsKmsClient(providerInputs);
return updatedProviderInputs;
};
const validateConnection = async () => {

View File

@ -336,31 +336,36 @@ export const removeUsersFromGroupByUserIds = async ({
)
);
// 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));
const promises: Array<Promise<void>> = [];
for (const userId of userIds) {
promises.push(
(async () => {
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
);
}
if (projectsToDeleteKeyFor.length) {
await projectKeyDAL.delete(
{
receiverId: userId,
$in: {
projectId: projectsToDeleteKeyFor
}
},
tx
);
}
await userGroupMembershipDAL.delete(
{
groupId: group.id,
userId
},
tx
await userGroupMembershipDAL.delete(
{
groupId: group.id,
userId
},
tx
);
})()
);
}
await Promise.all(promises);
}
if (membersToRemoveFromGroupPending.length) {

View File

@ -39,7 +39,13 @@ export const getDefaultOnPremFeatures = (): TFeatureSet => ({
secretApproval: false,
secretRotation: true,
caCrl: false,
instanceUserManagement: false
instanceUserManagement: false,
externalKms: false,
rateLimits: {
readLimit: 60,
writeLimit: 200,
secretsLimit: 40
}
});
export const setupLicenceRequestWithStore = (baseURL: string, refreshUrl: string, licenseKey: string) => {

View File

@ -57,6 +57,12 @@ export type TFeatureSet = {
secretRotation: true;
caCrl: false;
instanceUserManagement: false;
externalKms: false;
rateLimits: {
readLimit: number;
writeLimit: number;
secretsLimit: number;
};
};
export type TOrgPlansTableDTO = {

View File

@ -9,6 +9,10 @@ export enum OrgPermissionActions {
Delete = "delete"
}
export enum OrgPermissionAdminConsoleAction {
AccessAllProjects = "access-all-projects"
}
export enum OrgPermissionSubjects {
Workspace = "workspace",
Role = "role",
@ -21,7 +25,9 @@ export enum OrgPermissionSubjects {
Groups = "groups",
Billing = "billing",
SecretScanning = "secret-scanning",
Identity = "identity"
Identity = "identity",
Kms = "kms",
AdminConsole = "organization-admin-console"
}
export type OrgPermissionSet =
@ -37,7 +43,9 @@ export type OrgPermissionSet =
| [OrgPermissionActions, OrgPermissionSubjects.Groups]
| [OrgPermissionActions, OrgPermissionSubjects.SecretScanning]
| [OrgPermissionActions, OrgPermissionSubjects.Billing]
| [OrgPermissionActions, OrgPermissionSubjects.Identity];
| [OrgPermissionActions, OrgPermissionSubjects.Identity]
| [OrgPermissionActions, OrgPermissionSubjects.Kms]
| [OrgPermissionAdminConsoleAction, OrgPermissionSubjects.AdminConsole];
const buildAdminPermission = () => {
const { can, build } = new AbilityBuilder<MongoAbility<OrgPermissionSet>>(createMongoAbility);
@ -100,6 +108,13 @@ const buildAdminPermission = () => {
can(OrgPermissionActions.Edit, OrgPermissionSubjects.Identity);
can(OrgPermissionActions.Delete, OrgPermissionSubjects.Identity);
can(OrgPermissionActions.Read, OrgPermissionSubjects.Kms);
can(OrgPermissionActions.Create, OrgPermissionSubjects.Kms);
can(OrgPermissionActions.Edit, OrgPermissionSubjects.Kms);
can(OrgPermissionActions.Delete, OrgPermissionSubjects.Kms);
can(OrgPermissionAdminConsoleAction.AccessAllProjects, OrgPermissionSubjects.AdminConsole);
return build({ conditionsMatcher });
};

View File

@ -23,12 +23,14 @@ export enum ProjectPermissionSub {
IpAllowList = "ip-allowlist",
Project = "workspace",
Secrets = "secrets",
SecretFolders = "secret-folders",
SecretRollback = "secret-rollback",
SecretApproval = "secret-approval",
SecretRotation = "secret-rotation",
Identity = "identity",
CertificateAuthorities = "certificate-authorities",
Certificates = "certificates"
Certificates = "certificates",
Kms = "kms"
}
type SubjectFields = {
@ -41,6 +43,10 @@ export type ProjectPermissionSet =
ProjectPermissionActions,
ProjectPermissionSub.Secrets | (ForcedSubject<ProjectPermissionSub.Secrets> & SubjectFields)
]
| [
ProjectPermissionActions,
ProjectPermissionSub.SecretFolders | (ForcedSubject<ProjectPermissionSub.SecretFolders> & SubjectFields)
]
| [ProjectPermissionActions, ProjectPermissionSub.Role]
| [ProjectPermissionActions, ProjectPermissionSub.Tags]
| [ProjectPermissionActions, ProjectPermissionSub.Member]
@ -60,7 +66,8 @@ export type ProjectPermissionSet =
| [ProjectPermissionActions.Delete, ProjectPermissionSub.Project]
| [ProjectPermissionActions.Edit, ProjectPermissionSub.Project]
| [ProjectPermissionActions.Read, ProjectPermissionSub.SecretRollback]
| [ProjectPermissionActions.Create, ProjectPermissionSub.SecretRollback];
| [ProjectPermissionActions.Create, ProjectPermissionSub.SecretRollback]
| [ProjectPermissionActions.Edit, ProjectPermissionSub.Kms];
const buildAdminPermissionRules = () => {
const { can, rules } = new AbilityBuilder<MongoAbility<ProjectPermissionSet>>(createMongoAbility);
@ -157,6 +164,8 @@ const buildAdminPermissionRules = () => {
can(ProjectPermissionActions.Edit, ProjectPermissionSub.Project);
can(ProjectPermissionActions.Delete, ProjectPermissionSub.Project);
can(ProjectPermissionActions.Edit, ProjectPermissionSub.Kms);
return rules;
};

View File

@ -4,17 +4,16 @@ import { logger } from "@app/lib/logger";
import { TLicenseServiceFactory } from "../license/license-service";
import { TRateLimitDALFactory } from "./rate-limit-dal";
import { TRateLimit, TRateLimitUpdateDTO } from "./rate-limit-types";
import { RateLimitConfiguration, TRateLimit, TRateLimitUpdateDTO } from "./rate-limit-types";
let rateLimitMaxConfiguration = {
let rateLimitMaxConfiguration: RateLimitConfiguration = {
readLimit: 60,
publicEndpointLimit: 30,
writeLimit: 200,
secretsLimit: 60,
authRateLimit: 60,
inviteUserRateLimit: 30,
mfaRateLimit: 20,
creationLimit: 30
mfaRateLimit: 20
};
Object.freeze(rateLimitMaxConfiguration);
@ -67,8 +66,7 @@ export const rateLimitServiceFactory = ({ rateLimitDAL, licenseService }: TRateL
secretsLimit: rateLimit.secretsRateLimit,
authRateLimit: rateLimit.authRateLimit,
inviteUserRateLimit: rateLimit.inviteUserRateLimit,
mfaRateLimit: rateLimit.mfaRateLimit,
creationLimit: rateLimit.creationLimit
mfaRateLimit: rateLimit.mfaRateLimit
};
logger.info(`syncRateLimitConfiguration: rate limit configuration: %o`, newRateLimitMaxConfiguration);

View File

@ -5,7 +5,6 @@ export type TRateLimitUpdateDTO = {
authRateLimit: number;
inviteUserRateLimit: number;
mfaRateLimit: number;
creationLimit: number;
publicEndpointLimit: number;
};
@ -14,3 +13,13 @@ export type TRateLimit = {
createdAt: Date;
updatedAt: Date;
} & TRateLimitUpdateDTO;
export type RateLimitConfiguration = {
readLimit: number;
publicEndpointLimit: number;
writeLimit: number;
secretsLimit: number;
authRateLimit: number;
inviteUserRateLimit: number;
mfaRateLimit: number;
};

View File

@ -94,6 +94,7 @@ export const secretApprovalRequestDALFactory = (db: TDbClient) => {
tx.ref("projectId").withSchema(TableName.Environment),
tx.ref("slug").withSchema(TableName.Environment).as("environment"),
tx.ref("secretPath").withSchema(TableName.SecretApprovalPolicy).as("policySecretPath"),
tx.ref("envId").withSchema(TableName.SecretApprovalPolicy).as("policyEnvId"),
tx.ref("enforcementLevel").withSchema(TableName.SecretApprovalPolicy).as("policyEnforcementLevel"),
tx.ref("approvals").withSchema(TableName.SecretApprovalPolicy).as("policyApprovals")
);
@ -130,7 +131,8 @@ export const secretApprovalRequestDALFactory = (db: TDbClient) => {
name: el.policyName,
approvals: el.policyApprovals,
secretPath: el.policySecretPath,
enforcementLevel: el.policyEnforcementLevel
enforcementLevel: el.policyEnforcementLevel,
envId: el.policyEnvId
}
}),
childrenMapper: [
@ -354,5 +356,161 @@ export const secretApprovalRequestDALFactory = (db: TDbClient) => {
}
};
return { ...secretApprovalRequestOrm, findById, findProjectRequestCount, findByProjectId };
const findByProjectIdBridgeSecretV2 = async (
{ status, limit = 20, offset = 0, projectId, committer, environment, userId }: TFindQueryFilter,
tx?: Knex
) => {
try {
// akhilmhdh: If ever u wanted a 1 to so many relationship connected with pagination
// this is the place u wanna look at.
const query = (tx || db.replicaNode())(TableName.SecretApprovalRequest)
.join(TableName.SecretFolder, `${TableName.SecretApprovalRequest}.folderId`, `${TableName.SecretFolder}.id`)
.join(TableName.Environment, `${TableName.SecretFolder}.envId`, `${TableName.Environment}.id`)
.join(
TableName.SecretApprovalPolicy,
`${TableName.SecretApprovalRequest}.policyId`,
`${TableName.SecretApprovalPolicy}.id`
)
.join(
TableName.SecretApprovalPolicyApprover,
`${TableName.SecretApprovalPolicy}.id`,
`${TableName.SecretApprovalPolicyApprover}.policyId`
)
.join<TUsers>(
db(TableName.Users).as("committerUser"),
`${TableName.SecretApprovalRequest}.committerUserId`,
`committerUser.id`
)
.leftJoin(
TableName.SecretApprovalRequestReviewer,
`${TableName.SecretApprovalRequest}.id`,
`${TableName.SecretApprovalRequestReviewer}.requestId`
)
.leftJoin<TSecretApprovalRequestsSecrets>(
TableName.SecretApprovalRequestSecretV2,
`${TableName.SecretApprovalRequestSecretV2}.requestId`,
`${TableName.SecretApprovalRequest}.id`
)
.where(
stripUndefinedInWhere({
projectId,
[`${TableName.Environment}.slug` as "slug"]: environment,
[`${TableName.SecretApprovalRequest}.status`]: status,
committerUserId: committer
})
)
.andWhere(
(bd) =>
void bd
.where(`${TableName.SecretApprovalPolicyApprover}.approverUserId`, userId)
.orWhere(`${TableName.SecretApprovalRequest}.committerUserId`, userId)
)
.select(selectAllTableCols(TableName.SecretApprovalRequest))
.select(
db.ref("projectId").withSchema(TableName.Environment),
db.ref("slug").withSchema(TableName.Environment).as("environment"),
db.ref("id").withSchema(TableName.SecretApprovalRequestReviewer).as("reviewerId"),
db.ref("reviewerUserId").withSchema(TableName.SecretApprovalRequestReviewer),
db.ref("status").withSchema(TableName.SecretApprovalRequestReviewer).as("reviewerStatus"),
db.ref("id").withSchema(TableName.SecretApprovalPolicy).as("policyId"),
db.ref("name").withSchema(TableName.SecretApprovalPolicy).as("policyName"),
db.ref("op").withSchema(TableName.SecretApprovalRequestSecretV2).as("commitOp"),
db.ref("secretId").withSchema(TableName.SecretApprovalRequestSecretV2).as("commitSecretId"),
db.ref("id").withSchema(TableName.SecretApprovalRequestSecretV2).as("commitId"),
db.raw(
`DENSE_RANK() OVER (partition by ${TableName.Environment}."projectId" ORDER BY ${TableName.SecretApprovalRequest}."id" DESC) as rank`
),
db.ref("secretPath").withSchema(TableName.SecretApprovalPolicy).as("policySecretPath"),
db.ref("approvals").withSchema(TableName.SecretApprovalPolicy).as("policyApprovals"),
db.ref("enforcementLevel").withSchema(TableName.SecretApprovalPolicy).as("policyEnforcementLevel"),
db.ref("approverUserId").withSchema(TableName.SecretApprovalPolicyApprover),
db.ref("email").withSchema("committerUser").as("committerUserEmail"),
db.ref("username").withSchema("committerUser").as("committerUserUsername"),
db.ref("firstName").withSchema("committerUser").as("committerUserFirstName"),
db.ref("lastName").withSchema("committerUser").as("committerUserLastName")
)
.orderBy("createdAt", "desc");
const docs = await (tx || db)
.with("w", query)
.select("*")
.from<Awaited<typeof query>[number]>("w")
.where("w.rank", ">=", offset)
.andWhere("w.rank", "<", offset + limit);
const formatedDoc = sqlNestRelationships({
data: docs,
key: "id",
parentMapper: (el) => ({
...SecretApprovalRequestsSchema.parse(el),
environment: el.environment,
projectId: el.projectId,
policy: {
id: el.policyId,
name: el.policyName,
approvals: el.policyApprovals,
secretPath: el.policySecretPath,
enforcementLevel: el.policyEnforcementLevel
},
committerUser: {
userId: el.committerUserId,
email: el.committerUserEmail,
firstName: el.committerUserFirstName,
lastName: el.committerUserLastName,
username: el.committerUserUsername
}
}),
childrenMapper: [
{
key: "reviewerId",
label: "reviewers" as const,
mapper: ({ reviewerUserId, reviewerStatus: s }) =>
reviewerUserId ? { userId: reviewerUserId, status: s } : undefined
},
{
key: "approverUserId",
label: "approvers" as const,
mapper: ({ approverUserId }) => approverUserId
},
{
key: "commitId",
label: "commits" as const,
mapper: ({ commitSecretId: secretId, commitId: id, commitOp: op }) => ({
op,
id,
secretId
})
}
]
});
return formatedDoc.map((el) => ({
...el,
policy: { ...el.policy, approvers: el.approvers }
}));
} catch (error) {
throw new DatabaseError({ error, name: "FindSAR" });
}
};
const deleteByProjectId = async (projectId: string, tx?: Knex) => {
try {
const query = await (tx || db)(TableName.SecretApprovalRequest)
.join(TableName.SecretFolder, `${TableName.SecretApprovalRequest}.folderId`, `${TableName.SecretFolder}.id`)
.join(TableName.Environment, `${TableName.SecretFolder}.envId`, `${TableName.Environment}.id`)
.where({ projectId })
.delete();
return query;
} catch (error) {
throw new DatabaseError({ error, name: "DeleteByProjectId" });
}
};
return {
...secretApprovalRequestOrm,
findById,
findProjectRequestCount,
findByProjectId,
findByProjectIdBridgeSecretV2,
deleteByProjectId
};
};

View File

@ -3,6 +3,7 @@ import { Knex } from "knex";
import { TDbClient } from "@app/db";
import {
SecretApprovalRequestsSecretsSchema,
SecretApprovalRequestsSecretsV2Schema,
TableName,
TSecretApprovalRequestsSecrets,
TSecretTags
@ -15,6 +16,8 @@ export type TSecretApprovalRequestSecretDALFactory = ReturnType<typeof secretApp
export const secretApprovalRequestSecretDALFactory = (db: TDbClient) => {
const secretApprovalRequestSecretOrm = ormify(db, TableName.SecretApprovalRequestSecret);
const secretApprovalRequestSecretTagOrm = ormify(db, TableName.SecretApprovalRequestSecretTag);
const secretApprovalRequestSecretV2TagOrm = ormify(db, TableName.SecretApprovalRequestSecretTagV2);
const secretApprovalRequestSecretV2Orm = ormify(db, TableName.SecretApprovalRequestSecretV2);
const bulkUpdateNoVersionIncrement = async (data: TSecretApprovalRequestsSecrets[], tx?: Knex) => {
try {
@ -221,10 +224,197 @@ export const secretApprovalRequestSecretDALFactory = (db: TDbClient) => {
throw new DatabaseError({ error, name: "FindByRequestId" });
}
};
const findByRequestIdBridgeSecretV2 = async (requestId: string, tx?: Knex) => {
try {
const doc = await (tx || db.replicaNode())({
secVerTag: TableName.SecretTag
})
.from(TableName.SecretApprovalRequestSecretV2)
.where({ requestId })
.leftJoin(
TableName.SecretApprovalRequestSecretTagV2,
`${TableName.SecretApprovalRequestSecretV2}.id`,
`${TableName.SecretApprovalRequestSecretTagV2}.secretId`
)
.leftJoin(
TableName.SecretTag,
`${TableName.SecretApprovalRequestSecretTagV2}.tagId`,
`${TableName.SecretTag}.id`
)
.leftJoin(TableName.SecretV2, `${TableName.SecretApprovalRequestSecretV2}.secretId`, `${TableName.SecretV2}.id`)
.leftJoin(
TableName.SecretVersionV2,
`${TableName.SecretVersionV2}.id`,
`${TableName.SecretApprovalRequestSecretV2}.secretVersion`
)
.leftJoin(
TableName.SecretVersionV2Tag,
`${TableName.SecretVersionV2Tag}.${TableName.SecretVersionV2}Id`,
`${TableName.SecretVersionV2}.id`
)
.leftJoin<TSecretTags>(
db.ref(TableName.SecretTag).as("secVerTag"),
`${TableName.SecretVersionV2Tag}.${TableName.SecretTag}Id`,
db.ref("id").withSchema("secVerTag")
)
.select(selectAllTableCols(TableName.SecretApprovalRequestSecretV2))
.select({
secVerTagId: "secVerTag.id",
secVerTagColor: "secVerTag.color",
secVerTagSlug: "secVerTag.slug",
secVerTagName: "secVerTag.name"
})
.select(
db.ref("id").withSchema(TableName.SecretTag).as("tagId"),
db.ref("id").withSchema(TableName.SecretApprovalRequestSecretTagV2).as("tagJnId"),
db.ref("color").withSchema(TableName.SecretTag).as("tagColor"),
db.ref("slug").withSchema(TableName.SecretTag).as("tagSlug"),
db.ref("name").withSchema(TableName.SecretTag).as("tagName")
)
.select(
db.ref("version").withSchema(TableName.SecretV2).as("orgSecVersion"),
db.ref("key").withSchema(TableName.SecretV2).as("orgSecKey"),
db.ref("encryptedValue").withSchema(TableName.SecretV2).as("orgSecValue"),
db.ref("encryptedComment").withSchema(TableName.SecretV2).as("orgSecComment")
)
.select(
db.ref("version").withSchema(TableName.SecretVersionV2).as("secVerVersion"),
db.ref("key").withSchema(TableName.SecretVersionV2).as("secVerKey"),
db.ref("encryptedValue").withSchema(TableName.SecretVersionV2).as("secVerValue"),
db.ref("encryptedComment").withSchema(TableName.SecretVersionV2).as("secVerComment")
);
const formatedDoc = sqlNestRelationships({
data: doc,
key: "id",
parentMapper: (data) => SecretApprovalRequestsSecretsV2Schema.omit({ secretVersion: true }).parse(data),
childrenMapper: [
{
key: "tagJnId",
label: "tags" as const,
mapper: ({ tagId: id, tagName: name, tagSlug: slug, tagColor: color }) => ({
id,
name,
slug,
color
})
},
{
key: "secretId",
label: "secret" as const,
mapper: ({ orgSecVersion, orgSecKey, orgSecValue, orgSecComment, secretId }) =>
secretId
? {
id: secretId,
version: orgSecVersion,
key: orgSecKey,
encryptedValue: orgSecValue,
encryptedComment: orgSecComment
}
: undefined
},
{
key: "secretVersion",
label: "secretVersion" as const,
mapper: ({ secretVersion, secVerVersion, secVerKey, secVerValue, secVerComment }) =>
secretVersion
? {
version: secVerVersion,
id: secretVersion,
key: secVerKey,
encryptedValue: secVerValue,
encryptedComment: secVerComment
}
: undefined,
childrenMapper: [
{
key: "secVerTagId",
label: "tags" as const,
mapper: ({ secVerTagId: id, secVerTagName: name, secVerTagSlug: slug, secVerTagColor: color }) => ({
// eslint-disable-next-line
id,
// eslint-disable-next-line
name,
// eslint-disable-next-line
slug,
// eslint-disable-next-line
color
})
}
]
}
]
});
return formatedDoc?.map(({ secret, secretVersion, ...el }) => ({
...el,
secret: secret?.[0],
secretVersion: secretVersion?.[0]
}));
} catch (error) {
throw new DatabaseError({ error, name: "FindByRequestId" });
}
};
// special query for migration to v2 secret
const findByProjectId = async (projectId: string, tx?: Knex) => {
try {
const docs = await (tx || db)(TableName.SecretApprovalRequestSecret)
.join(
TableName.SecretApprovalRequest,
`${TableName.SecretApprovalRequest}.id`,
`${TableName.SecretApprovalRequestSecret}.requestId`
)
.join(TableName.SecretFolder, `${TableName.SecretApprovalRequest}.folderId`, `${TableName.SecretFolder}.id`)
.join(TableName.Environment, `${TableName.SecretFolder}.envId`, `${TableName.Environment}.id`)
.leftJoin(
TableName.SecretApprovalRequestSecretTag,
`${TableName.SecretApprovalRequestSecret}.id`,
`${TableName.SecretApprovalRequestSecretTag}.secretId`
)
.where({ projectId })
.select(selectAllTableCols(TableName.SecretApprovalRequestSecret))
.select(
db.ref("id").withSchema(TableName.SecretApprovalRequestSecretTag).as("secretApprovalTagId"),
db.ref("secretId").withSchema(TableName.SecretApprovalRequestSecretTag).as("secretApprovalTagSecretId"),
db.ref("tagId").withSchema(TableName.SecretApprovalRequestSecretTag).as("secretApprovalTagSecretTagId"),
db.ref("createdAt").withSchema(TableName.SecretApprovalRequestSecretTag).as("secretApprovalTagCreatedAt"),
db.ref("updatedAt").withSchema(TableName.SecretApprovalRequestSecretTag).as("secretApprovalTagUpdatedAt")
);
const formatedDoc = sqlNestRelationships({
data: docs,
key: "id",
parentMapper: (data) => SecretApprovalRequestsSecretsSchema.parse(data),
childrenMapper: [
{
key: "secretApprovalTagId",
label: "tags" as const,
mapper: ({
secretApprovalTagSecretId,
secretApprovalTagId,
secretApprovalTagUpdatedAt,
secretApprovalTagCreatedAt
}) => ({
secretApprovalTagSecretId,
secretApprovalTagId,
secretApprovalTagUpdatedAt,
secretApprovalTagCreatedAt
})
}
]
});
return formatedDoc;
} catch (error) {
throw new DatabaseError({ error, name: "FindByRequestId" });
}
};
return {
...secretApprovalRequestSecretOrm,
insertV2Bridge: secretApprovalRequestSecretV2Orm.insertMany,
findByRequestId,
findByRequestIdBridgeSecretV2,
bulkUpdateNoVersionIncrement,
insertApprovalSecretTags: secretApprovalRequestSecretTagOrm.insertMany
findByProjectId,
insertApprovalSecretTags: secretApprovalRequestSecretTagOrm.insertMany,
insertApprovalSecretV2Tags: secretApprovalRequestSecretV2TagOrm.insertMany
};
};

View File

@ -5,18 +5,25 @@ import {
SecretEncryptionAlgo,
SecretKeyEncoding,
SecretType,
TSecretApprovalRequestsSecretsInsert
TSecretApprovalRequestsSecretsInsert,
TSecretApprovalRequestsSecretsV2Insert
} from "@app/db/schemas";
import { getConfig } from "@app/lib/config/env";
import { decryptSymmetric128BitHexKeyUTF8 } from "@app/lib/crypto";
import { BadRequestError, UnauthorizedError } from "@app/lib/errors";
import { groupBy, pick, unique } from "@app/lib/fn";
import { setKnexStringValue } from "@app/lib/knex";
import { alphaNumericNanoId } from "@app/lib/nanoid";
import { EnforcementLevel } from "@app/lib/types";
import { ActorType } from "@app/services/auth/auth-type";
import { TKmsServiceFactory } from "@app/services/kms/kms-service";
import { KmsDataKey } from "@app/services/kms/kms-types";
import { TProjectDALFactory } from "@app/services/project/project-dal";
import { TProjectBotServiceFactory } from "@app/services/project-bot/project-bot-service";
import { TProjectEnvDALFactory } from "@app/services/project-env/project-env-dal";
import { TSecretDALFactory } from "@app/services/secret/secret-dal";
import {
decryptSecretWithBot,
fnSecretBlindIndexCheck,
fnSecretBlindIndexCheckV2,
fnSecretBulkDelete,
@ -31,6 +38,17 @@ import { TSecretVersionTagDALFactory } from "@app/services/secret/secret-version
import { TSecretBlindIndexDALFactory } from "@app/services/secret-blind-index/secret-blind-index-dal";
import { TSecretFolderDALFactory } from "@app/services/secret-folder/secret-folder-dal";
import { TSecretTagDALFactory } from "@app/services/secret-tag/secret-tag-dal";
import { TSecretV2BridgeDALFactory } from "@app/services/secret-v2-bridge/secret-v2-bridge-dal";
import {
fnSecretBulkDelete as fnSecretV2BridgeBulkDelete,
fnSecretBulkInsert as fnSecretV2BridgeBulkInsert,
fnSecretBulkUpdate as fnSecretV2BridgeBulkUpdate,
getAllNestedSecretReferences as getAllNestedSecretReferencesV2Bridge
} from "@app/services/secret-v2-bridge/secret-v2-bridge-fns";
import { TSecretVersionV2DALFactory } from "@app/services/secret-v2-bridge/secret-version-dal";
import { TSecretVersionV2TagDALFactory } from "@app/services/secret-v2-bridge/secret-version-tag-dal";
import { SmtpTemplates, TSmtpService } from "@app/services/smtp/smtp-service";
import { TUserDALFactory } from "@app/services/user/user-dal";
import { TPermissionServiceFactory } from "../permission/permission-service";
import { ProjectPermissionActions, ProjectPermissionSub } from "../permission/project-permission";
@ -43,6 +61,7 @@ import {
RequestState,
TApprovalRequestCountDTO,
TGenerateSecretApprovalRequestDTO,
TGenerateSecretApprovalRequestV2BridgeDTO,
TListApprovalsDTO,
TMergeSecretApprovalRequestDTO,
TReviewRequestDTO,
@ -58,13 +77,26 @@ type TSecretApprovalRequestServiceFactoryDep = {
secretApprovalRequestReviewerDAL: TSecretApprovalRequestReviewerDALFactory;
folderDAL: Pick<TSecretFolderDALFactory, "findBySecretPath" | "findSecretPathByFolderIds">;
secretDAL: TSecretDALFactory;
secretTagDAL: Pick<TSecretTagDALFactory, "findManyTagsById" | "saveTagsToSecret" | "deleteTagsManySecret">;
secretTagDAL: Pick<
TSecretTagDALFactory,
"findManyTagsById" | "saveTagsToSecret" | "deleteTagsManySecret" | "saveTagsToSecretV2" | "deleteTagsToSecretV2"
>;
secretBlindIndexDAL: Pick<TSecretBlindIndexDALFactory, "findOne">;
snapshotService: Pick<TSecretSnapshotServiceFactory, "performSnapshot">;
secretVersionDAL: Pick<TSecretVersionDALFactory, "findLatestVersionMany" | "insertMany">;
secretVersionTagDAL: Pick<TSecretVersionTagDALFactory, "insertMany">;
projectDAL: Pick<TProjectDALFactory, "checkProjectUpgradeStatus">;
smtpService: Pick<TSmtpService, "sendMail">;
userDAL: Pick<TUserDALFactory, "find" | "findOne">;
projectEnvDAL: Pick<TProjectEnvDALFactory, "findOne">;
projectDAL: Pick<TProjectDALFactory, "checkProjectUpgradeStatus" | "findById" | "findProjectById">;
secretQueueService: Pick<TSecretQueueFactory, "syncSecrets" | "removeSecretReminder">;
kmsService: Pick<TKmsServiceFactory, "createCipherPairWithDataKey" | "encryptWithInputKey" | "decryptWithInputKey">;
secretV2BridgeDAL: Pick<
TSecretV2BridgeDALFactory,
"insertMany" | "upsertSecretReferences" | "findBySecretKeys" | "bulkUpdate" | "deleteMany"
>;
secretVersionV2BridgeDAL: Pick<TSecretVersionV2DALFactory, "insertMany" | "findLatestVersionMany">;
secretVersionTagV2BridgeDAL: Pick<TSecretVersionV2TagDALFactory, "insertMany">;
};
export type TSecretApprovalRequestServiceFactory = ReturnType<typeof secretApprovalRequestServiceFactory>;
@ -83,7 +115,14 @@ export const secretApprovalRequestServiceFactory = ({
snapshotService,
secretVersionDAL,
secretQueueService,
projectBotService
projectBotService,
smtpService,
userDAL,
projectEnvDAL,
kmsService,
secretV2BridgeDAL,
secretVersionV2BridgeDAL,
secretVersionTagV2BridgeDAL
}: TSecretApprovalRequestServiceFactoryDep) => {
const requestCount = async ({ projectId, actor, actorId, actorOrgId, actorAuthMethod }: TApprovalRequestCountDTO) => {
if (actor === ActorType.SERVICE) throw new BadRequestError({ message: "Cannot use service token" });
@ -115,6 +154,19 @@ export const secretApprovalRequestServiceFactory = ({
if (actor === ActorType.SERVICE) throw new BadRequestError({ message: "Cannot use service token" });
await permissionService.getProjectPermission(actor, actorId, projectId, actorAuthMethod, actorOrgId);
const { shouldUseSecretV2Bridge } = await projectBotService.getBotKey(projectId);
if (shouldUseSecretV2Bridge) {
return secretApprovalRequestDAL.findByProjectIdBridgeSecretV2({
projectId,
committer,
environment,
status,
userId: actorId,
limit,
offset
});
}
const approvals = await secretApprovalRequestDAL.findByProjectId({
projectId,
committer,
@ -139,11 +191,14 @@ export const secretApprovalRequestServiceFactory = ({
const secretApprovalRequest = await secretApprovalRequestDAL.findById(id);
if (!secretApprovalRequest) throw new BadRequestError({ message: "Secret approval request not found" });
const { projectId } = secretApprovalRequest;
const { botKey, shouldUseSecretV2Bridge } = await projectBotService.getBotKey(projectId);
const { policy } = secretApprovalRequest;
const { hasRole } = await permissionService.getProjectPermission(
actor,
actorId,
secretApprovalRequest.projectId,
projectId,
actorAuthMethod,
actorOrgId
);
@ -155,7 +210,75 @@ export const secretApprovalRequestServiceFactory = ({
throw new UnauthorizedError({ message: "User has no access" });
}
const secrets = await secretApprovalRequestSecretDAL.findByRequestId(secretApprovalRequest.id);
let secrets;
if (shouldUseSecretV2Bridge) {
const { decryptor: secretManagerDecryptor } = await kmsService.createCipherPairWithDataKey({
type: KmsDataKey.SecretManager,
projectId
});
const encrypedSecrets = await secretApprovalRequestSecretDAL.findByRequestIdBridgeSecretV2(
secretApprovalRequest.id
);
secrets = encrypedSecrets.map((el) => ({
...el,
secretKey: el.key,
id: el.id,
version: el.version,
secretValue: el.encryptedValue
? secretManagerDecryptor({ cipherTextBlob: el.encryptedValue }).toString()
: undefined,
secretComment: el.encryptedComment
? secretManagerDecryptor({ cipherTextBlob: el.encryptedComment }).toString()
: undefined,
secret: el.secret
? {
secretKey: el.secret.key,
id: el.secret.id,
version: el.secret.version,
secretValue: el.secret.encryptedValue
? secretManagerDecryptor({ cipherTextBlob: el.secret.encryptedValue }).toString()
: undefined,
secretComment: el.secret.encryptedComment
? secretManagerDecryptor({ cipherTextBlob: el.secret.encryptedComment }).toString()
: undefined
}
: undefined,
secretVersion: el.secretVersion
? {
secretKey: el.secretVersion.key,
id: el.secretVersion.id,
version: el.secretVersion.version,
secretValue: el.secretVersion.encryptedValue
? secretManagerDecryptor({ cipherTextBlob: el.secretVersion.encryptedValue }).toString()
: undefined,
secretComment: el.secretVersion.encryptedComment
? secretManagerDecryptor({ cipherTextBlob: el.secretVersion.encryptedComment }).toString()
: undefined
}
: undefined
}));
} else {
if (!botKey) throw new BadRequestError({ message: "Bot key not found" });
const encrypedSecrets = await secretApprovalRequestSecretDAL.findByRequestId(secretApprovalRequest.id);
secrets = encrypedSecrets.map((el) => ({
...el,
...decryptSecretWithBot(el, botKey),
secret: el.secret
? {
id: el.secret.id,
version: el.secret.version,
...decryptSecretWithBot(el.secret, botKey)
}
: undefined,
secretVersion: el.secretVersion
? {
id: el.secretVersion.id,
version: el.secretVersion.version,
...decryptSecretWithBot(el.secretVersion, botKey)
}
: undefined
}));
}
const secretPath = await folderDAL.findSecretPathByFolderIds(secretApprovalRequest.projectId, [
secretApprovalRequest.folderId
]);
@ -258,7 +381,8 @@ export const secretApprovalRequestServiceFactory = ({
actor,
actorId,
actorOrgId,
actorAuthMethod
actorAuthMethod,
bypassReason
}: TMergeSecretApprovalRequestDTO) => {
const secretApprovalRequest = await secretApprovalRequestDAL.findById(approvalId);
if (!secretApprovalRequest) throw new BadRequestError({ message: "Secret approval request not found" });
@ -289,48 +413,167 @@ export const secretApprovalRequestServiceFactory = ({
secretApprovalRequest.policy.approvers.filter(
({ userId: approverId }) => reviewers[approverId.toString()] === ApprovalStatus.APPROVED
).length;
const isSoftEnforcement = secretApprovalRequest.policy.enforcementLevel === EnforcementLevel.Soft;
if (!hasMinApproval && !isSoftEnforcement)
throw new BadRequestError({ message: "Doesn't have minimum approvals needed" });
const secretApprovalSecrets = await secretApprovalRequestSecretDAL.findByRequestId(secretApprovalRequest.id);
if (!secretApprovalSecrets) throw new BadRequestError({ message: "No secrets found" });
const conflicts: Array<{ secretId: string; op: SecretOperations }> = [];
let secretCreationCommits = secretApprovalSecrets.filter(({ op }) => op === SecretOperations.Create);
if (secretCreationCommits.length) {
const { secsGroupedByBlindIndex: conflictGroupByBlindIndex } = await fnSecretBlindIndexCheckV2({
folderId,
secretDAL,
inputSecrets: secretCreationCommits.map(({ secretBlindIndex }) => {
if (!secretBlindIndex) {
throw new BadRequestError({
message: "Missing secret blind index"
});
}
return { secretBlindIndex };
})
});
secretCreationCommits
.filter(({ secretBlindIndex }) => conflictGroupByBlindIndex[secretBlindIndex || ""])
.forEach((el) => {
conflicts.push({ op: SecretOperations.Create, secretId: el.id });
});
secretCreationCommits = secretCreationCommits.filter(
({ secretBlindIndex }) => !conflictGroupByBlindIndex[secretBlindIndex || ""]
const { botKey, shouldUseSecretV2Bridge } = await projectBotService.getBotKey(projectId);
let mergeStatus;
if (shouldUseSecretV2Bridge) {
// this cycle if for bridged secrets
const secretApprovalSecrets = await secretApprovalRequestSecretDAL.findByRequestIdBridgeSecretV2(
secretApprovalRequest.id
);
}
if (!secretApprovalSecrets) throw new BadRequestError({ message: "No secrets found" });
let secretUpdationCommits = secretApprovalSecrets.filter(({ op }) => op === SecretOperations.Update);
if (secretUpdationCommits.length) {
const { secsGroupedByBlindIndex: conflictGroupByBlindIndex } = await fnSecretBlindIndexCheckV2({
folderId,
secretDAL,
userId: "",
inputSecrets: secretUpdationCommits
.filter(({ secretBlindIndex, secret }) => secret && secret.secretBlindIndex !== secretBlindIndex)
.map(({ secretBlindIndex }) => {
const { decryptor: secretManagerDecryptor } = await kmsService.createCipherPairWithDataKey({
type: KmsDataKey.SecretManager,
projectId
});
const conflicts: Array<{ secretId: string; op: SecretOperations }> = [];
let secretCreationCommits = secretApprovalSecrets.filter(({ op }) => op === SecretOperations.Create);
if (secretCreationCommits.length) {
const secrets = await secretV2BridgeDAL.findBySecretKeys(
folderId,
secretCreationCommits.map((el) => ({
key: el.key,
type: SecretType.Shared
}))
);
const creationConflictSecretsGroupByKey = groupBy(secrets, (i) => i.key);
secretCreationCommits
.filter(({ key }) => creationConflictSecretsGroupByKey[key])
.forEach((el) => {
conflicts.push({ op: SecretOperations.Create, secretId: el.id });
});
secretCreationCommits = secretCreationCommits.filter(({ key }) => !creationConflictSecretsGroupByKey[key]);
}
let secretUpdationCommits = secretApprovalSecrets.filter(({ op }) => op === SecretOperations.Update);
if (secretUpdationCommits.length) {
const secrets = await secretV2BridgeDAL.findBySecretKeys(
folderId,
secretCreationCommits.map((el) => ({
key: el.key,
type: SecretType.Shared
}))
);
const updationConflictSecretsGroupByKey = groupBy(secrets, (i) => i.key);
secretUpdationCommits
.filter(({ key, secretId }) => updationConflictSecretsGroupByKey[key] || !secretId)
.forEach((el) => {
conflicts.push({ op: SecretOperations.Update, secretId: el.id });
});
secretUpdationCommits = secretUpdationCommits.filter(
({ key, secretId }) => Boolean(secretId) && !updationConflictSecretsGroupByKey[key]
);
}
const secretDeletionCommits = secretApprovalSecrets.filter(({ op }) => op === SecretOperations.Delete);
mergeStatus = await secretApprovalRequestDAL.transaction(async (tx) => {
const newSecrets = secretCreationCommits.length
? await fnSecretV2BridgeBulkInsert({
tx,
folderId,
inputSecrets: secretCreationCommits.map((el) => ({
tagIds: el?.tags.map(({ id }) => id),
version: 1,
encryptedComment: el.encryptedComment,
encryptedValue: el.encryptedValue,
skipMultilineEncoding: el.skipMultilineEncoding,
key: el.key,
references: el.encryptedValue
? getAllNestedSecretReferencesV2Bridge(
secretManagerDecryptor({
cipherTextBlob: el.encryptedValue
}).toString()
)
: [],
type: SecretType.Shared
})),
secretDAL: secretV2BridgeDAL,
secretVersionDAL: secretVersionV2BridgeDAL,
secretTagDAL,
secretVersionTagDAL: secretVersionTagV2BridgeDAL
})
: [];
const updatedSecrets = secretUpdationCommits.length
? await fnSecretV2BridgeBulkUpdate({
folderId,
tx,
inputSecrets: secretUpdationCommits.map((el) => {
const encryptedValue =
typeof el.encryptedValue !== "undefined"
? {
encryptedValue: el.encryptedValue as Buffer,
references: el.encryptedValue
? getAllNestedSecretReferencesV2Bridge(
secretManagerDecryptor({
cipherTextBlob: el.encryptedValue
}).toString()
)
: []
}
: {};
return {
filter: { id: el.secretId as string, type: SecretType.Shared },
data: {
reminderRepeatDays: el.reminderRepeatDays,
encryptedComment: el.encryptedComment,
reminderNote: el.reminderNote,
skipMultilineEncoding: el.skipMultilineEncoding,
key: el.key,
tagIds: el?.tags.map(({ id }) => id),
...encryptedValue
}
};
}),
secretDAL: secretV2BridgeDAL,
secretVersionDAL: secretVersionV2BridgeDAL,
secretTagDAL,
secretVersionTagDAL: secretVersionTagV2BridgeDAL
})
: [];
const deletedSecret = secretDeletionCommits.length
? await fnSecretV2BridgeBulkDelete({
projectId,
folderId,
tx,
actorId: "",
secretDAL: secretV2BridgeDAL,
secretQueueService,
inputSecrets: secretDeletionCommits.map(({ key }) => ({ secretKey: key, type: SecretType.Shared }))
})
: [];
const updatedSecretApproval = await secretApprovalRequestDAL.updateById(
secretApprovalRequest.id,
{
conflicts: JSON.stringify(conflicts),
hasMerged: true,
status: RequestState.Closed,
statusChangedByUserId: actorId
},
tx
);
return {
secrets: { created: newSecrets, updated: updatedSecrets, deleted: deletedSecret },
approval: updatedSecretApproval
};
});
} else {
const secretApprovalSecrets = await secretApprovalRequestSecretDAL.findByRequestId(secretApprovalRequest.id);
if (!secretApprovalSecrets) throw new BadRequestError({ message: "No secrets found" });
const conflicts: Array<{ secretId: string; op: SecretOperations }> = [];
let secretCreationCommits = secretApprovalSecrets.filter(({ op }) => op === SecretOperations.Create);
if (secretCreationCommits.length) {
const { secsGroupedByBlindIndex: conflictGroupByBlindIndex } = await fnSecretBlindIndexCheckV2({
folderId,
secretDAL,
inputSecrets: secretCreationCommits.map(({ secretBlindIndex }) => {
if (!secretBlindIndex) {
throw new BadRequestError({
message: "Missing secret blind index"
@ -338,80 +581,56 @@ export const secretApprovalRequestServiceFactory = ({
}
return { secretBlindIndex };
})
});
secretUpdationCommits
.filter(
({ secretBlindIndex, secretId }) =>
(secretBlindIndex && conflictGroupByBlindIndex[secretBlindIndex]) || !secretId
)
.forEach((el) => {
conflicts.push({ op: SecretOperations.Update, secretId: el.id });
});
secretCreationCommits
.filter(({ secretBlindIndex }) => conflictGroupByBlindIndex[secretBlindIndex || ""])
.forEach((el) => {
conflicts.push({ op: SecretOperations.Create, secretId: el.id });
});
secretCreationCommits = secretCreationCommits.filter(
({ secretBlindIndex }) => !conflictGroupByBlindIndex[secretBlindIndex || ""]
);
}
secretUpdationCommits = secretUpdationCommits.filter(
({ secretBlindIndex, secretId }) =>
Boolean(secretId) && (secretBlindIndex ? !conflictGroupByBlindIndex[secretBlindIndex] : true)
);
}
let secretUpdationCommits = secretApprovalSecrets.filter(({ op }) => op === SecretOperations.Update);
if (secretUpdationCommits.length) {
const { secsGroupedByBlindIndex: conflictGroupByBlindIndex } = await fnSecretBlindIndexCheckV2({
folderId,
secretDAL,
userId: "",
inputSecrets: secretUpdationCommits
.filter(({ secretBlindIndex, secret }) => secret && secret.secretBlindIndex !== secretBlindIndex)
.map(({ secretBlindIndex }) => {
if (!secretBlindIndex) {
throw new BadRequestError({
message: "Missing secret blind index"
});
}
return { secretBlindIndex };
})
});
secretUpdationCommits
.filter(
({ secretBlindIndex, secretId }) =>
(secretBlindIndex && conflictGroupByBlindIndex[secretBlindIndex]) || !secretId
)
.forEach((el) => {
conflicts.push({ op: SecretOperations.Update, secretId: el.id });
});
const secretDeletionCommits = secretApprovalSecrets.filter(({ op }) => op === SecretOperations.Delete);
const botKey = await projectBotService.getBotKey(projectId).catch(() => null);
const mergeStatus = await secretApprovalRequestDAL.transaction(async (tx) => {
const newSecrets = secretCreationCommits.length
? await fnSecretBulkInsert({
tx,
folderId,
inputSecrets: secretCreationCommits.map((el) => ({
...pick(el, [
"secretCommentCiphertext",
"secretCommentTag",
"secretCommentIV",
"secretValueIV",
"secretValueTag",
"secretValueCiphertext",
"secretKeyCiphertext",
"secretKeyTag",
"secretKeyIV",
"metadata",
"skipMultilineEncoding",
"secretReminderNote",
"secretReminderRepeatDays",
"algorithm",
"keyEncoding",
"secretBlindIndex"
]),
tags: el?.tags.map(({ id }) => id),
version: 1,
type: SecretType.Shared,
references: botKey
? getAllNestedSecretReferences(
decryptSymmetric128BitHexKeyUTF8({
ciphertext: el.secretValueCiphertext,
iv: el.secretValueIV,
tag: el.secretValueTag,
key: botKey
})
)
: undefined
})),
secretDAL,
secretVersionDAL,
secretTagDAL,
secretVersionTagDAL
})
: [];
const updatedSecrets = secretUpdationCommits.length
? await fnSecretBulkUpdate({
folderId,
projectId,
tx,
inputSecrets: secretUpdationCommits.map((el) => ({
filter: {
id: el.secretId as string, // this null check is already checked at top on conflict strategy
type: SecretType.Shared
},
data: {
tags: el?.tags.map(({ id }) => id),
secretUpdationCommits = secretUpdationCommits.filter(
({ secretBlindIndex, secretId }) =>
Boolean(secretId) && (secretBlindIndex ? !conflictGroupByBlindIndex[secretBlindIndex] : true)
);
}
const secretDeletionCommits = secretApprovalSecrets.filter(({ op }) => op === SecretOperations.Delete);
mergeStatus = await secretApprovalRequestDAL.transaction(async (tx) => {
const newSecrets = secretCreationCommits.length
? await fnSecretBulkInsert({
tx,
folderId,
inputSecrets: secretCreationCommits.map((el) => ({
...pick(el, [
"secretCommentCiphertext",
"secretCommentTag",
@ -426,8 +645,13 @@ export const secretApprovalRequestServiceFactory = ({
"skipMultilineEncoding",
"secretReminderNote",
"secretReminderRepeatDays",
"algorithm",
"keyEncoding",
"secretBlindIndex"
]),
tags: el?.tags.map(({ id }) => id),
version: 1,
type: SecretType.Shared,
references: botKey
? getAllNestedSecretReferences(
decryptSymmetric128BitHexKeyUTF8({
@ -438,47 +662,94 @@ export const secretApprovalRequestServiceFactory = ({
})
)
: undefined
}
})),
secretDAL,
secretVersionDAL,
secretTagDAL,
secretVersionTagDAL
})
: [];
const deletedSecret = secretDeletionCommits.length
? await fnSecretBulkDelete({
projectId,
folderId,
tx,
actorId: "",
secretDAL,
secretQueueService,
inputSecrets: secretDeletionCommits.map(({ secretBlindIndex }) => {
if (!secretBlindIndex) {
throw new BadRequestError({
message: "Missing secret blind index"
});
}
return { secretBlindIndex, type: SecretType.Shared };
})),
secretDAL,
secretVersionDAL,
secretTagDAL,
secretVersionTagDAL
})
})
: [];
const updatedSecretApproval = await secretApprovalRequestDAL.updateById(
secretApprovalRequest.id,
{
conflicts: JSON.stringify(conflicts),
hasMerged: true,
status: RequestState.Closed,
statusChangedByUserId: actorId
},
tx
);
return {
secrets: { created: newSecrets, updated: updatedSecrets, deleted: deletedSecret },
approval: updatedSecretApproval
};
});
: [];
const updatedSecrets = secretUpdationCommits.length
? await fnSecretBulkUpdate({
folderId,
projectId,
tx,
inputSecrets: secretUpdationCommits.map((el) => ({
filter: {
id: el.secretId as string, // this null check is already checked at top on conflict strategy
type: SecretType.Shared
},
data: {
tags: el?.tags.map(({ id }) => id),
...pick(el, [
"secretCommentCiphertext",
"secretCommentTag",
"secretCommentIV",
"secretValueIV",
"secretValueTag",
"secretValueCiphertext",
"secretKeyCiphertext",
"secretKeyTag",
"secretKeyIV",
"metadata",
"skipMultilineEncoding",
"secretReminderNote",
"secretReminderRepeatDays",
"secretBlindIndex"
]),
references: botKey
? getAllNestedSecretReferences(
decryptSymmetric128BitHexKeyUTF8({
ciphertext: el.secretValueCiphertext,
iv: el.secretValueIV,
tag: el.secretValueTag,
key: botKey
})
)
: undefined
}
})),
secretDAL,
secretVersionDAL,
secretTagDAL,
secretVersionTagDAL
})
: [];
const deletedSecret = secretDeletionCommits.length
? await fnSecretBulkDelete({
projectId,
folderId,
tx,
actorId: "",
secretDAL,
secretQueueService,
inputSecrets: secretDeletionCommits.map(({ secretBlindIndex }) => {
if (!secretBlindIndex) {
throw new BadRequestError({
message: "Missing secret blind index"
});
}
return { secretBlindIndex, type: SecretType.Shared };
})
})
: [];
const updatedSecretApproval = await secretApprovalRequestDAL.updateById(
secretApprovalRequest.id,
{
conflicts: JSON.stringify(conflicts),
hasMerged: true,
status: RequestState.Closed,
statusChangedByUserId: actorId
},
tx
);
return {
secrets: { created: newSecrets, updated: updatedSecrets, deleted: deletedSecret },
approval: updatedSecretApproval
};
});
}
await snapshotService.performSnapshot(folderId);
const [folder] = await folderDAL.findSecretPathByFolderIds(projectId, [folderId]);
if (!folder) throw new BadRequestError({ message: "Folder not found" });
@ -489,6 +760,35 @@ export const secretApprovalRequestServiceFactory = ({
actorId,
actor
});
if (isSoftEnforcement) {
const cfg = getConfig();
const project = await projectDAL.findProjectById(projectId);
const env = await projectEnvDAL.findOne({ id: policy.envId });
const requestedByUser = await userDAL.findOne({ id: actorId });
const approverUsers = await userDAL.find({
$in: {
id: policy.approvers.map((approver: { userId: string }) => approver.userId)
}
});
await smtpService.sendMail({
recipients: approverUsers.filter((approver) => approver.email).map((approver) => approver.email!),
subjectLine: "Infisical Secret Change Policy Bypassed",
substitutions: {
projectName: project.name,
requesterFullName: `${requestedByUser.firstName} ${requestedByUser.lastName}`,
requesterEmail: requestedByUser.email,
bypassReason,
secretPath: policy.secretPath,
environment: env.name,
approvalUrl: `${cfg.SITE_URL}/project/${project.id}/approval`
},
template: SmtpTemplates.AccessSecretRequestBypassed
});
}
return mergeStatus;
};
@ -738,8 +1038,262 @@ export const secretApprovalRequestServiceFactory = ({
});
return secretApprovalRequest;
};
const generateSecretApprovalRequestV2Bridge = async ({
data,
actorId,
actor,
actorOrgId,
actorAuthMethod,
policy,
projectId,
secretPath,
environment
}: TGenerateSecretApprovalRequestV2BridgeDTO) => {
if (actor === ActorType.SERVICE || actor === ActorType.Machine)
throw new BadRequestError({ message: "Cannot use service token or machine token over protected branches" });
const { permission } = await permissionService.getProjectPermission(
actor,
actorId,
projectId,
actorAuthMethod,
actorOrgId
);
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Read,
subject(ProjectPermissionSub.Secrets, { environment, secretPath })
);
const folder = await folderDAL.findBySecretPath(projectId, environment, secretPath);
if (!folder)
throw new BadRequestError({
message: "Folder not found for the given environment slug & secret path",
name: "GenSecretApproval"
});
const folderId = folder.id;
const commits: Omit<TSecretApprovalRequestsSecretsV2Insert, "requestId">[] = [];
const commitTagIds: Record<string, string[]> = {};
const { encryptor: secretManagerEncryptor } = await kmsService.createCipherPairWithDataKey({
type: KmsDataKey.SecretManager,
projectId
});
// for created secret approval change
const createdSecrets = data[SecretOperations.Create];
if (createdSecrets && createdSecrets?.length) {
const secrets = await secretV2BridgeDAL.findBySecretKeys(
folderId,
createdSecrets.map((el) => ({
key: el.secretKey,
type: SecretType.Shared
}))
);
if (secrets.length)
throw new BadRequestError({ message: `Secret already exist: ${secrets.map((el) => el.key).join(",")}` });
commits.push(
...createdSecrets.map((createdSecret) => ({
op: SecretOperations.Create,
version: 1,
encryptedComment: setKnexStringValue(
createdSecret.secretComment,
(value) => secretManagerEncryptor({ plainText: Buffer.from(value) }).cipherTextBlob
),
encryptedValue: setKnexStringValue(
createdSecret.secretValue,
(value) => secretManagerEncryptor({ plainText: Buffer.from(value) }).cipherTextBlob
),
skipMultilineEncoding: createdSecret.skipMultilineEncoding,
key: createdSecret.secretKey,
type: SecretType.Shared
}))
);
createdSecrets.forEach(({ tagIds, secretKey }) => {
if (tagIds?.length) commitTagIds[secretKey] = tagIds;
});
}
// not secret approval for update operations
const secretsToUpdate = data[SecretOperations.Update];
if (secretsToUpdate && secretsToUpdate?.length) {
const secretsToUpdateStoredInDB = await secretV2BridgeDAL.findBySecretKeys(
folderId,
secretsToUpdate.map((el) => ({
key: el.secretKey,
type: SecretType.Shared
}))
);
if (secretsToUpdateStoredInDB.length !== secretsToUpdate.length)
throw new BadRequestError({
message: `Secret not exist: ${secretsToUpdateStoredInDB.map((el) => el.key).join(",")}`
});
// now find any secret that needs to update its name
// same process as above
const secretsWithNewName = secretsToUpdate.filter(({ newSecretName }) => Boolean(newSecretName));
if (secretsWithNewName.length) {
const secrets = await secretV2BridgeDAL.findBySecretKeys(
folderId,
secretsWithNewName.map((el) => ({
key: el.secretKey,
type: SecretType.Shared
}))
);
if (secrets.length)
throw new BadRequestError({
message: `Secret not exist: ${secretsToUpdateStoredInDB.map((el) => el.key).join(",")}`
});
}
const updatingSecretsGroupByKey = groupBy(secretsToUpdateStoredInDB, (el) => el.key);
const latestSecretVersions = await secretVersionV2BridgeDAL.findLatestVersionMany(
folderId,
secretsToUpdateStoredInDB.map(({ id }) => id)
);
commits.push(
...secretsToUpdate.map(
({
newSecretName,
secretKey,
tagIds,
secretValue,
reminderRepeatDays,
reminderNote,
secretComment,
metadata,
skipMultilineEncoding
}) => {
const secretId = updatingSecretsGroupByKey[secretKey][0].id;
if (tagIds?.length) commitTagIds[secretKey] = tagIds;
return {
...latestSecretVersions[secretId],
key: newSecretName || secretKey,
encryptedComment: setKnexStringValue(
secretComment,
(value) => secretManagerEncryptor({ plainText: Buffer.from(value) }).cipherTextBlob
),
encryptedValue: setKnexStringValue(
secretValue,
(value) => secretManagerEncryptor({ plainText: Buffer.from(value) }).cipherTextBlob
),
reminderRepeatDays,
reminderNote,
metadata,
skipMultilineEncoding,
op: SecretOperations.Update as const,
secret: secretId,
secretVersion: latestSecretVersions[secretId].id,
version: updatingSecretsGroupByKey[secretKey][0].version || 1
};
}
)
);
}
// deleted secrets
const deletedSecrets = data[SecretOperations.Delete];
if (deletedSecrets && deletedSecrets.length) {
const secretsToDeleteInDB = await secretV2BridgeDAL.findBySecretKeys(
folderId,
deletedSecrets.map((el) => ({
key: el.secretKey,
type: SecretType.Shared
}))
);
if (secretsToDeleteInDB.length !== deletedSecrets.length)
throw new BadRequestError({
message: `Secret not exist: ${secretsToDeleteInDB.map((el) => el.key).join(",")}`
});
const secretsGroupedByKey = groupBy(secretsToDeleteInDB, (i) => i.key);
const deletedSecretIds = deletedSecrets.map((el) => secretsGroupedByKey[el.secretKey][0].id);
const latestSecretVersions = await secretVersionV2BridgeDAL.findLatestVersionMany(folderId, deletedSecretIds);
commits.push(
...deletedSecrets.map(({ secretKey }) => {
const secretId = secretsGroupedByKey[secretKey][0].id;
return {
op: SecretOperations.Delete as const,
...latestSecretVersions[secretId],
key: secretKey,
secret: secretId,
secretVersion: latestSecretVersions[secretId].id
};
})
);
}
if (!commits.length) throw new BadRequestError({ message: "Empty commits" });
const tagIds = unique(Object.values(commitTagIds).flat());
const tags = tagIds.length ? await secretTagDAL.findManyTagsById(projectId, tagIds) : [];
if (tagIds.length !== tags.length) throw new BadRequestError({ message: "Tag not found" });
const secretApprovalRequest = await secretApprovalRequestDAL.transaction(async (tx) => {
const doc = await secretApprovalRequestDAL.create(
{
folderId,
slug: alphaNumericNanoId(),
policyId: policy.id,
status: "open",
hasMerged: false,
committerUserId: actorId
},
tx
);
const approvalCommits = await secretApprovalRequestSecretDAL.insertV2Bridge(
commits.map(
({
version,
op,
key,
encryptedComment,
skipMultilineEncoding,
metadata,
reminderNote,
reminderRepeatDays,
encryptedValue,
secretId,
secretVersion
}) => ({
version,
requestId: doc.id,
op,
secretId,
metadata,
secretVersion,
skipMultilineEncoding,
encryptedValue,
reminderRepeatDays,
reminderNote,
encryptedComment,
key
})
),
tx
);
const commitsGroupByKey = groupBy(approvalCommits, (i) => i.key);
if (tagIds.length) {
await secretApprovalRequestSecretDAL.insertApprovalSecretV2Tags(
Object.keys(commitTagIds).flatMap((blindIndex) =>
commitTagIds[blindIndex]
? commitTagIds[blindIndex].map((tagId) => ({
secretId: commitsGroupByKey[blindIndex][0].id,
tagId
}))
: []
),
tx
);
}
return { ...doc, commits: approvalCommits };
});
return secretApprovalRequest;
};
return {
generateSecretApprovalRequest,
generateSecretApprovalRequestV2Bridge,
mergeSecretApprovalRequest,
reviewApproval,
updateApprovalStatus,

View File

@ -26,6 +26,23 @@ export type TApprovalUpdateSecret = Partial<TApprovalCreateSecret> & {
tagIds?: string[];
};
export type TApprovalCreateSecretV2Bridge = {
secretKey: string;
secretValue?: string;
secretComment?: string;
reminderNote?: string | null;
reminderRepeatDays?: number | null;
skipMultilineEncoding?: boolean;
metadata?: Record<string, string>;
tagIds?: string[];
};
export type TApprovalUpdateSecretV2Bridge = Partial<TApprovalCreateSecretV2Bridge> & {
secretKey: string;
newSecretName?: string;
tagIds?: string[];
};
export type TGenerateSecretApprovalRequestDTO = {
environment: string;
secretPath: string;
@ -37,8 +54,20 @@ export type TGenerateSecretApprovalRequestDTO = {
};
} & TProjectPermission;
export type TGenerateSecretApprovalRequestV2BridgeDTO = {
environment: string;
secretPath: string;
policy: TSecretApprovalPolicies;
data: {
[SecretOperations.Create]?: TApprovalCreateSecretV2Bridge[];
[SecretOperations.Update]?: TApprovalUpdateSecretV2Bridge[];
[SecretOperations.Delete]?: { secretKey: string }[];
};
} & TProjectPermission;
export type TMergeSecretApprovalRequestDTO = {
approvalId: string;
bypassReason?: string;
} & Omit<TProjectPermission, "projectId">;
export type TStatusChangeDTO = {

View File

@ -1,4 +1,4 @@
import { SecretType, TSecrets } from "@app/db/schemas";
import { SecretType, TSecrets, TSecretsV2 } from "@app/db/schemas";
import { TSecretApprovalPolicyServiceFactory } from "@app/ee/services/secret-approval-policy/secret-approval-policy-service";
import { TSecretApprovalRequestDALFactory } from "@app/ee/services/secret-approval-request/secret-approval-request-dal";
import { TSecretApprovalRequestSecretDALFactory } from "@app/ee/services/secret-approval-request/secret-approval-request-secret-dal";
@ -10,6 +10,8 @@ import { logger } from "@app/lib/logger";
import { alphaNumericNanoId } from "@app/lib/nanoid";
import { QueueName, TQueueServiceFactory } from "@app/queue";
import { ActorType } from "@app/services/auth/auth-type";
import { TKmsServiceFactory } from "@app/services/kms/kms-service";
import { KmsDataKey } from "@app/services/kms/kms-types";
import { TProjectBotServiceFactory } from "@app/services/project-bot/project-bot-service";
import { TSecretDALFactory } from "@app/services/secret/secret-dal";
import { fnSecretBulkInsert, fnSecretBulkUpdate } from "@app/services/secret/secret-fns";
@ -17,12 +19,20 @@ import { TSecretQueueFactory, uniqueSecretQueueKey } from "@app/services/secret/
import { SecretOperations } from "@app/services/secret/secret-types";
import { TSecretVersionDALFactory } from "@app/services/secret/secret-version-dal";
import { TSecretVersionTagDALFactory } from "@app/services/secret/secret-version-tag-dal";
import { TSecretBlindIndexDALFactory } from "@app/services/secret-blind-index/secret-blind-index-dal";
import { TSecretFolderDALFactory } from "@app/services/secret-folder/secret-folder-dal";
import { ReservedFolders } from "@app/services/secret-folder/secret-folder-types";
import { TSecretImportDALFactory } from "@app/services/secret-import/secret-import-dal";
import { fnSecretsFromImports } from "@app/services/secret-import/secret-import-fns";
import { fnSecretsFromImports, fnSecretsV2FromImports } from "@app/services/secret-import/secret-import-fns";
import { TSecretTagDALFactory } from "@app/services/secret-tag/secret-tag-dal";
import { TSecretV2BridgeDALFactory } from "@app/services/secret-v2-bridge/secret-v2-bridge-dal";
import {
fnSecretBulkInsert as fnSecretV2BridgeBulkInsert,
fnSecretBulkUpdate as fnSecretV2BridgeBulkUpdate,
getAllNestedSecretReferences,
getAllNestedSecretReferences as getAllNestedSecretReferencesV2Bridge
} from "@app/services/secret-v2-bridge/secret-v2-bridge-fns";
import { TSecretVersionV2DALFactory } from "@app/services/secret-v2-bridge/secret-version-dal";
import { TSecretVersionV2TagDALFactory } from "@app/services/secret-v2-bridge/secret-version-tag-dal";
import { MAX_REPLICATION_DEPTH } from "./secret-replication-constants";
@ -32,24 +42,42 @@ type TSecretReplicationServiceFactoryDep = {
"find" | "findByBlindIndexes" | "insertMany" | "bulkUpdate" | "delete" | "upsertSecretReferences" | "transaction"
>;
secretVersionDAL: Pick<TSecretVersionDALFactory, "find" | "insertMany" | "update" | "findLatestVersionMany">;
secretV2BridgeDAL: Pick<
TSecretV2BridgeDALFactory,
"find" | "findBySecretKeys" | "insertMany" | "bulkUpdate" | "delete" | "upsertSecretReferences" | "transaction"
>;
secretVersionV2BridgeDAL: Pick<
TSecretVersionV2DALFactory,
"find" | "insertMany" | "update" | "findLatestVersionMany"
>;
secretImportDAL: Pick<TSecretImportDALFactory, "find" | "updateById" | "findByFolderIds">;
folderDAL: Pick<
TSecretFolderDALFactory,
"findSecretPathByFolderIds" | "findBySecretPath" | "create" | "findOne" | "findByManySecretPath"
>;
secretVersionTagDAL: Pick<TSecretVersionTagDALFactory, "find" | "insertMany">;
secretVersionV2TagBridgeDAL: Pick<TSecretVersionV2TagDALFactory, "find" | "insertMany">;
secretQueueService: Pick<TSecretQueueFactory, "syncSecrets" | "replicateSecrets">;
queueService: Pick<TQueueServiceFactory, "start" | "listen" | "queue" | "stopJobById">;
secretApprovalPolicyService: Pick<TSecretApprovalPolicyServiceFactory, "getSecretApprovalPolicy">;
keyStore: Pick<TKeyStoreFactory, "acquireLock" | "setItemWithExpiry" | "getItem">;
secretBlindIndexDAL: Pick<TSecretBlindIndexDALFactory, "findOne">;
secretTagDAL: Pick<TSecretTagDALFactory, "findManyTagsById" | "saveTagsToSecret" | "deleteTagsManySecret" | "find">;
secretTagDAL: Pick<
TSecretTagDALFactory,
| "findManyTagsById"
| "saveTagsToSecret"
| "deleteTagsManySecret"
| "find"
| "saveTagsToSecretV2"
| "deleteTagsToSecretV2"
>;
secretApprovalRequestDAL: Pick<TSecretApprovalRequestDALFactory, "create" | "transaction">;
secretApprovalRequestSecretDAL: Pick<
TSecretApprovalRequestSecretDALFactory,
"insertMany" | "insertApprovalSecretTags"
"insertMany" | "insertApprovalSecretTags" | "insertV2Bridge"
>;
projectBotService: Pick<TProjectBotServiceFactory, "getBotKey">;
kmsService: Pick<TKmsServiceFactory, "createCipherPairWithDataKey">;
};
export type TSecretReplicationServiceFactory = ReturnType<typeof secretReplicationServiceFactory>;
@ -90,9 +118,13 @@ export const secretReplicationServiceFactory = ({
secretApprovalRequestSecretDAL,
secretApprovalRequestDAL,
secretQueueService,
projectBotService
projectBotService,
secretVersionV2TagBridgeDAL,
secretVersionV2BridgeDAL,
secretV2BridgeDAL,
kmsService
}: TSecretReplicationServiceFactoryDep) => {
const getReplicatedSecrets = (
const $getReplicatedSecrets = (
botKey: string,
localSecrets: TSecrets[],
importedSecrets: { secrets: TSecrets[] }[]
@ -119,6 +151,25 @@ export const secretReplicationServiceFactory = ({
return secrets;
};
const $getReplicatedSecretsV2 = (
localSecrets: (TSecretsV2 & { secretKey: string; secretValue?: string })[],
importedSecrets: { secrets: (TSecretsV2 & { secretKey: string; secretValue?: string })[] }[]
) => {
const deDupe = new Set<string>();
const secrets = [...localSecrets];
for (let i = importedSecrets.length - 1; i >= 0; i = -1) {
importedSecrets[i].secrets.forEach((el) => {
if (deDupe.has(el.key)) {
return;
}
deDupe.add(el.key);
secrets.push(el);
});
}
return secrets;
};
// IMPORTANT NOTE BEFORE READING THE FUNCTION
// SOURCE - Where secrets are copied from
// DESTINATION - Where the replicated imports that points to SOURCE from Destination
@ -139,6 +190,7 @@ export const secretReplicationServiceFactory = ({
const folder = await folderDAL.findBySecretPath(projectId, environmentSlug, secretPath);
if (!folder) return;
const { botKey, shouldUseSecretV2Bridge } = await projectBotService.getBotKey(projectId);
// the the replicated imports made to the source. These are the destinations
const destinationSecretImports = await secretImportDAL.find({
@ -191,8 +243,270 @@ export const secretReplicationServiceFactory = ({
: destinationReplicatedSecretImports;
if (!destinationReplicatedSecretImports.length) return;
const botKey = await projectBotService.getBotKey(projectId);
if (shouldUseSecretV2Bridge) {
const { decryptor: secretManagerDecryptor } = await kmsService.createCipherPairWithDataKey({
type: KmsDataKey.SecretManager,
projectId
});
// these are the secrets to be added in replicated folders
const sourceLocalSecrets = await secretV2BridgeDAL.find({ folderId: folder.id, type: SecretType.Shared });
const sourceSecretImports = await secretImportDAL.find({ folderId: folder.id });
const sourceImportedSecrets = await fnSecretsV2FromImports({
allowedImports: sourceSecretImports,
secretDAL: secretV2BridgeDAL,
folderDAL,
secretImportDAL,
decryptor: (value) => (value ? secretManagerDecryptor({ cipherTextBlob: value }).toString() : undefined)
});
// secrets that gets replicated across imports
const sourceDecryptedLocalSecrets = sourceLocalSecrets.map((el) => ({
...el,
secretKey: el.key,
secretValue: el.encryptedValue
? secretManagerDecryptor({ cipherTextBlob: el.encryptedValue }).toString()
: undefined
}));
const sourceSecrets = $getReplicatedSecretsV2(sourceDecryptedLocalSecrets, sourceImportedSecrets);
const sourceSecretsGroupByKey = groupBy(sourceSecrets, (i) => i.key);
const lock = await keyStore.acquireLock(
[getReplicationKeyLockPrefix(projectId, environmentSlug, secretPath)],
5000
);
try {
/* eslint-disable no-await-in-loop */
for (const destinationSecretImport of destinationReplicatedSecretImports) {
try {
const hasJobCompleted = await keyStore.getItem(
keystoreReplicationSuccessKey(job.id as string, destinationSecretImport.id),
KeyStorePrefixes.SecretReplication
);
if (hasJobCompleted) {
logger.info(
{ jobId: job.id, importId: destinationSecretImport.id },
"Skipping this job as this has been successfully replicated."
);
// eslint-disable-next-line
continue;
}
const [destinationFolder] = await folderDAL.findSecretPathByFolderIds(projectId, [
destinationSecretImport.folderId
]);
if (!destinationFolder) throw new BadRequestError({ message: "Imported folder not found" });
let destinationReplicationFolder = await folderDAL.findOne({
parentId: destinationFolder.id,
name: getReplicationFolderName(destinationSecretImport.id),
isReserved: true
});
if (!destinationReplicationFolder) {
destinationReplicationFolder = await folderDAL.create({
parentId: destinationFolder.id,
name: getReplicationFolderName(destinationSecretImport.id),
envId: destinationFolder.envId,
isReserved: true
});
}
const destinationReplicationFolderId = destinationReplicationFolder.id;
const destinationLocalSecretsFromDB = await secretV2BridgeDAL.find({
folderId: destinationReplicationFolderId
});
const destinationLocalSecrets = destinationLocalSecretsFromDB.map((el) => ({
...el,
secretKey: el.key,
secretValue: el.encryptedValue
? secretManagerDecryptor({ cipherTextBlob: el.encryptedValue }).toString()
: undefined
}));
const destinationLocalSecretsGroupedByKey = groupBy(destinationLocalSecrets, (i) => i.key);
const locallyCreatedSecrets = sourceSecrets
.filter(({ key }) => !destinationLocalSecretsGroupedByKey[key]?.[0])
.map((el) => ({ ...el, operation: SecretOperations.Create })); // rewrite update ops to create
const locallyUpdatedSecrets = sourceSecrets
.filter(
({ key, secretKey, secretValue }) =>
destinationLocalSecretsGroupedByKey[key]?.[0] &&
// if key or value changed
(destinationLocalSecretsGroupedByKey[key]?.[0]?.secretKey !== secretKey ||
destinationLocalSecretsGroupedByKey[key]?.[0]?.secretValue !== secretValue)
)
.map((el) => ({ ...el, operation: SecretOperations.Update })); // rewrite update ops to create
const locallyDeletedSecrets = destinationLocalSecrets
.filter(({ key }) => !sourceSecretsGroupByKey[key]?.[0])
.map((el) => ({ ...el, operation: SecretOperations.Delete }));
const isEmtpy =
locallyCreatedSecrets.length + locallyUpdatedSecrets.length + locallyDeletedSecrets.length === 0;
// eslint-disable-next-line
if (isEmtpy) continue;
const policy = await secretApprovalPolicyService.getSecretApprovalPolicy(
projectId,
destinationFolder.environmentSlug,
destinationFolder.path
);
// this means it should be a approval request rather than direct replication
if (policy && actor === ActorType.USER) {
const localSecretsLatestVersions = destinationLocalSecrets.map(({ id }) => id);
const latestSecretVersions = await secretVersionV2BridgeDAL.findLatestVersionMany(
destinationReplicationFolderId,
localSecretsLatestVersions
);
await secretApprovalRequestDAL.transaction(async (tx) => {
const approvalRequestDoc = await secretApprovalRequestDAL.create(
{
folderId: destinationReplicationFolderId,
slug: alphaNumericNanoId(),
policyId: policy.id,
status: "open",
hasMerged: false,
committerUserId: actorId,
isReplicated: true
},
tx
);
const commits = locallyCreatedSecrets
.concat(locallyUpdatedSecrets)
.concat(locallyDeletedSecrets)
.map((doc) => {
const { operation } = doc;
const localSecret = destinationLocalSecretsGroupedByKey[doc.key]?.[0];
return {
op: operation,
requestId: approvalRequestDoc.id,
metadata: doc.metadata,
key: doc.key,
encryptedValue: doc.encryptedValue,
encryptedComment: doc.encryptedComment,
skipMultilineEncoding: doc.skipMultilineEncoding,
// except create operation other two needs the secret id and version id
...(operation !== SecretOperations.Create
? { secretId: localSecret.id, secretVersion: latestSecretVersions[localSecret.id].id }
: {})
};
});
const approvalCommits = await secretApprovalRequestSecretDAL.insertV2Bridge(commits, tx);
return { ...approvalRequestDoc, commits: approvalCommits };
});
} else {
await secretDAL.transaction(async (tx) => {
if (locallyCreatedSecrets.length) {
await fnSecretV2BridgeBulkInsert({
folderId: destinationReplicationFolderId,
secretVersionDAL: secretVersionV2BridgeDAL,
secretDAL: secretV2BridgeDAL,
tx,
secretTagDAL,
secretVersionTagDAL: secretVersionV2TagBridgeDAL,
inputSecrets: locallyCreatedSecrets.map((doc) => {
return {
type: doc.type,
metadata: doc.metadata,
key: doc.key,
encryptedValue: doc.encryptedValue,
encryptedComment: doc.encryptedComment,
skipMultilineEncoding: doc.skipMultilineEncoding,
references: doc.secretValue ? getAllNestedSecretReferencesV2Bridge(doc.secretValue) : []
};
})
});
}
if (locallyUpdatedSecrets.length) {
await fnSecretV2BridgeBulkUpdate({
folderId: destinationReplicationFolderId,
secretVersionDAL: secretVersionV2BridgeDAL,
secretDAL: secretV2BridgeDAL,
tx,
secretTagDAL,
secretVersionTagDAL: secretVersionV2TagBridgeDAL,
inputSecrets: locallyUpdatedSecrets.map((doc) => {
return {
filter: {
folderId: destinationReplicationFolderId,
id: destinationLocalSecretsGroupedByKey[doc.key][0].id
},
data: {
type: doc.type,
metadata: doc.metadata,
key: doc.key,
encryptedValue: doc.encryptedValue as Buffer,
encryptedComment: doc.encryptedComment,
skipMultilineEncoding: doc.skipMultilineEncoding,
references: doc.secretValue ? getAllNestedSecretReferencesV2Bridge(doc.secretValue) : []
}
};
})
});
}
if (locallyDeletedSecrets.length) {
await secretV2BridgeDAL.delete(
{
$in: {
id: locallyDeletedSecrets.map(({ id }) => id)
},
folderId: destinationReplicationFolderId
},
tx
);
}
});
await secretQueueService.syncSecrets({
projectId,
secretPath: destinationFolder.path,
environmentSlug: destinationFolder.environmentSlug,
actorId,
actor,
_depth: depth + 1,
_deDupeReplicationQueue: deDupeReplicationQueue,
_deDupeQueue: deDupeQueue
});
}
// this is used to avoid multiple times generating secret approval by failed one
await keyStore.setItemWithExpiry(
keystoreReplicationSuccessKey(job.id as string, destinationSecretImport.id),
SECRET_IMPORT_SUCCESS_LOCK,
1,
KeyStorePrefixes.SecretReplication
);
await secretImportDAL.updateById(destinationSecretImport.id, {
lastReplicated: new Date(),
replicationStatus: null,
isReplicationSuccess: true
});
} catch (err) {
logger.error(
err,
`Failed to replicate secret with import id=[${destinationSecretImport.id}] env=[${destinationSecretImport.importEnv.slug}] path=[${destinationSecretImport.importPath}]`
);
await secretImportDAL.updateById(destinationSecretImport.id, {
lastReplicated: new Date(),
replicationStatus: (err as Error)?.message.slice(0, 500),
isReplicationSuccess: false
});
}
}
/* eslint-enable no-await-in-loop */
} finally {
await lock.release();
logger.info(job.data, "Replication finished");
}
return;
}
if (!botKey) throw new BadRequestError({ message: "Bot not found" });
// these are the secrets to be added in replicated folders
const sourceLocalSecrets = await secretDAL.find({ folderId: folder.id, type: SecretType.Shared });
const sourceSecretImports = await secretImportDAL.find({ folderId: folder.id });
@ -203,7 +517,7 @@ export const secretReplicationServiceFactory = ({
secretImportDAL
});
// secrets that gets replicated across imports
const sourceSecrets = getReplicatedSecrets(botKey, sourceLocalSecrets, sourceImportedSecrets);
const sourceSecrets = $getReplicatedSecrets(botKey, sourceLocalSecrets, sourceImportedSecrets);
const sourceSecretsGroupByBlindIndex = groupBy(sourceSecrets, (i) => i.secretBlindIndex as string);
const lock = await keyStore.acquireLock(
@ -372,7 +686,8 @@ export const secretReplicationServiceFactory = ({
secretCommentIV: doc.secretCommentIV,
secretCommentTag: doc.secretCommentTag,
secretCommentCiphertext: doc.secretCommentCiphertext,
skipMultilineEncoding: doc.skipMultilineEncoding
skipMultilineEncoding: doc.skipMultilineEncoding,
references: getAllNestedSecretReferences(doc.secretValue)
};
})
});
@ -407,7 +722,8 @@ export const secretReplicationServiceFactory = ({
secretCommentIV: doc.secretCommentIV,
secretCommentTag: doc.secretCommentTag,
secretCommentCiphertext: doc.secretCommentCiphertext,
skipMultilineEncoding: doc.skipMultilineEncoding
skipMultilineEncoding: doc.skipMultilineEncoding,
references: getAllNestedSecretReferences(doc.secretValue)
}
};
})

View File

@ -10,6 +10,7 @@ export type TSecretRotationDALFactory = ReturnType<typeof secretRotationDALFacto
export const secretRotationDALFactory = (db: TDbClient) => {
const secretRotationOrm = ormify(db, TableName.SecretRotation);
const secretRotationOutputOrm = ormify(db, TableName.SecretRotationOutput);
const secretRotationOutputV2Orm = ormify(db, TableName.SecretRotationOutputV2);
const findQuery = (filter: TFindFilter<TSecretRotations & { projectId: string }>, tx: Knex) =>
tx(TableName.SecretRotation)
@ -31,13 +32,7 @@ export const secretRotationDALFactory = (db: TDbClient) => {
.select(tx.ref("version").withSchema(TableName.Secret).as("secVersion"))
.select(tx.ref("secretKeyIV").withSchema(TableName.Secret))
.select(tx.ref("secretKeyTag").withSchema(TableName.Secret))
.select(tx.ref("secretKeyCiphertext").withSchema(TableName.Secret))
.select(tx.ref("secretValueIV").withSchema(TableName.Secret))
.select(tx.ref("secretValueTag").withSchema(TableName.Secret))
.select(tx.ref("secretValueCiphertext").withSchema(TableName.Secret))
.select(tx.ref("secretCommentIV").withSchema(TableName.Secret))
.select(tx.ref("secretCommentTag").withSchema(TableName.Secret))
.select(tx.ref("secretCommentCiphertext").withSchema(TableName.Secret));
.select(tx.ref("secretKeyCiphertext").withSchema(TableName.Secret));
const find = async (filter: TFindFilter<TSecretRotations & { projectId: string }>, tx?: Knex) => {
try {
@ -54,33 +49,65 @@ export const secretRotationDALFactory = (db: TDbClient) => {
{
key: "secId",
label: "outputs" as const,
mapper: ({
secId,
outputKey,
secVersion,
secretKeyIV,
secretKeyTag,
secretKeyCiphertext,
secretValueTag,
secretValueIV,
secretValueCiphertext,
secretCommentIV,
secretCommentTag,
secretCommentCiphertext
}) => ({
mapper: ({ secId, outputKey, secVersion, secretKeyIV, secretKeyTag, secretKeyCiphertext }) => ({
key: outputKey,
secret: {
id: secId,
version: secVersion,
secretKeyIV,
secretKeyTag,
secretKeyCiphertext,
secretValueTag,
secretValueIV,
secretValueCiphertext,
secretCommentIV,
secretCommentTag,
secretCommentCiphertext
secretKeyCiphertext
}
})
}
]
});
} catch (error) {
throw new DatabaseError({ error, name: "SecretRotationFind" });
}
};
const findQuerySecretV2 = (filter: TFindFilter<TSecretRotations & { projectId: string }>, tx: Knex) =>
tx(TableName.SecretRotation)
.where(filter)
.join(TableName.Environment, `${TableName.SecretRotation}.envId`, `${TableName.Environment}.id`)
.leftJoin(
TableName.SecretRotationOutputV2,
`${TableName.SecretRotation}.id`,
`${TableName.SecretRotationOutputV2}.rotationId`
)
.join(TableName.SecretV2, `${TableName.SecretRotationOutputV2}.secretId`, `${TableName.SecretV2}.id`)
.select(selectAllTableCols(TableName.SecretRotation))
.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(tx.ref("key").withSchema(TableName.SecretRotationOutputV2).as("outputKey"))
.select(tx.ref("id").withSchema(TableName.SecretV2).as("secId"))
.select(tx.ref("version").withSchema(TableName.SecretV2).as("secVersion"))
.select(tx.ref("key").withSchema(TableName.SecretV2).as("secretKey"));
const findSecretV2 = async (filter: TFindFilter<TSecretRotations & { projectId: string }>, tx?: Knex) => {
try {
const data = await findQuerySecretV2(filter, tx || db.replicaNode());
return sqlNestRelationships({
data,
key: "id",
parentMapper: (el) => ({
...SecretRotationsSchema.parse(el),
projectId: el.projectId,
environment: { id: el.envId, name: el.envName, slug: el.envSlug }
}),
childrenMapper: [
{
key: "secId",
label: "outputs" as const,
mapper: ({ secId, outputKey, secVersion, secretKey }) => ({
key: outputKey,
secret: {
id: secId,
version: secVersion,
secretKey
}
})
}
@ -114,12 +141,19 @@ export const secretRotationDALFactory = (db: TDbClient) => {
};
const findRotationOutputsByRotationId = async (rotationId: string) => secretRotationOutputOrm.find({ rotationId });
const findRotationOutputsV2ByRotationId = async (rotationId: string) =>
secretRotationOutputV2Orm.find({ rotationId });
// special query
return {
...secretRotationOrm,
find,
findSecretV2,
findById,
secretOutputInsertMany: secretRotationOutputOrm.insertMany,
findRotationOutputsByRotationId
secretOutputV2InsertMany: secretRotationOutputV2Orm.insertMany,
findRotationOutputsByRotationId,
findRotationOutputsV2ByRotationId
};
};

View File

@ -17,9 +17,13 @@ import { BadRequestError } from "@app/lib/errors";
import { logger } from "@app/lib/logger";
import { alphaNumericNanoId } from "@app/lib/nanoid";
import { QueueJobs, QueueName, TQueueServiceFactory } from "@app/queue";
import { TKmsServiceFactory } from "@app/services/kms/kms-service";
import { KmsDataKey } from "@app/services/kms/kms-types";
import { TProjectBotServiceFactory } from "@app/services/project-bot/project-bot-service";
import { TSecretDALFactory } from "@app/services/secret/secret-dal";
import { TSecretVersionDALFactory } from "@app/services/secret/secret-version-dal";
import { TSecretV2BridgeDALFactory } from "@app/services/secret-v2-bridge/secret-v2-bridge-dal";
import { TSecretVersionV2DALFactory } from "@app/services/secret-v2-bridge/secret-version-dal";
import { TTelemetryServiceFactory } from "@app/services/telemetry/telemetry-service";
import { PostHogEventTypes } from "@app/services/telemetry/telemetry-types";
@ -47,8 +51,11 @@ type TSecretRotationQueueFactoryDep = {
secretRotationDAL: TSecretRotationDALFactory;
projectBotService: Pick<TProjectBotServiceFactory, "getBotKey">;
secretDAL: Pick<TSecretDALFactory, "bulkUpdate" | "find">;
secretV2BridgeDAL: Pick<TSecretV2BridgeDALFactory, "bulkUpdate" | "find">;
secretVersionDAL: Pick<TSecretVersionDALFactory, "insertMany" | "findLatestVersionMany">;
secretVersionV2BridgeDAL: Pick<TSecretVersionV2DALFactory, "insertMany" | "findLatestVersionMany">;
telemetryService: Pick<TTelemetryServiceFactory, "sendPostHogEvents">;
kmsService: Pick<TKmsServiceFactory, "createCipherPairWithDataKey">;
};
// These error should stop the repeatable job and ask user to reconfigure rotation
@ -70,7 +77,10 @@ export const secretRotationQueueFactory = ({
projectBotService,
secretDAL,
secretVersionDAL,
telemetryService
telemetryService,
secretV2BridgeDAL,
secretVersionV2BridgeDAL,
kmsService
}: TSecretRotationQueueFactoryDep) => {
const addToQueue = async (rotationId: string, interval: number) => {
const appCfg = getConfig();
@ -111,7 +121,13 @@ export const secretRotationQueueFactory = ({
try {
if (!rotationProvider || !secretRotation) throw new DisableRotationErrors({ message: "Provider not found" });
const rotationOutputs = await secretRotationDAL.findRotationOutputsByRotationId(rotationId);
const { botKey, shouldUseSecretV2Bridge } = await projectBotService.getBotKey(secretRotation.projectId);
let rotationOutputs;
if (shouldUseSecretV2Bridge) {
rotationOutputs = await secretRotationDAL.findRotationOutputsV2ByRotationId(rotationId);
} else {
rotationOutputs = await secretRotationDAL.findRotationOutputsByRotationId(rotationId);
}
if (!rotationOutputs.length) throw new DisableRotationErrors({ message: "Secrets not found" });
// deep copy
@ -267,62 +283,112 @@ export const secretRotationQueueFactory = ({
internal: newCredential.internal
});
const encVarData = infisicalSymmetricEncypt(JSON.stringify(variables));
const key = await projectBotService.getBotKey(secretRotation.projectId);
const encryptedSecrets = rotationOutputs.map(({ key: outputKey, secretId }) => ({
secretId,
value: encryptSymmetric128BitHexKeyUTF8(
typeof newCredential.outputs[outputKey] === "object"
? JSON.stringify(newCredential.outputs[outputKey])
: String(newCredential.outputs[outputKey]),
key
)
}));
// map the final values to output keys in the board
await secretRotationDAL.transaction(async (tx) => {
await secretRotationDAL.updateById(
rotationId,
{
encryptedData: encVarData.ciphertext,
encryptedDataIV: encVarData.iv,
encryptedDataTag: encVarData.tag,
keyEncoding: encVarData.encoding,
algorithm: encVarData.algorithm,
lastRotatedAt: new Date(),
statusMessage: "Rotated successfull",
status: "success"
},
tx
);
const updatedSecrets = await secretDAL.bulkUpdate(
encryptedSecrets.map(({ secretId, value }) => ({
// this secret id is validated when user is inserted
filter: { id: secretId, type: SecretType.Shared },
data: {
secretValueCiphertext: value.ciphertext,
secretValueIV: value.iv,
secretValueTag: value.tag
}
})),
tx
);
await secretVersionDAL.insertMany(
updatedSecrets.map(({ id, updatedAt, createdAt, ...el }) => {
if (!el.secretBlindIndex) throw new BadRequestError({ message: "Missing blind index" });
return {
...el,
secretId: id,
secretBlindIndex: el.secretBlindIndex
};
}),
tx
);
const { encryptor: secretManagerEncryptor } = await kmsService.createCipherPairWithDataKey({
type: KmsDataKey.SecretManager,
projectId: secretRotation.projectId
});
const numberOfSecretsRotated = rotationOutputs.length;
if (shouldUseSecretV2Bridge) {
const encryptedSecrets = rotationOutputs.map(({ key: outputKey, secretId }) => ({
secretId,
value:
typeof newCredential.outputs[outputKey] === "object"
? JSON.stringify(newCredential.outputs[outputKey])
: String(newCredential.outputs[outputKey])
}));
// map the final values to output keys in the board
await secretRotationDAL.transaction(async (tx) => {
await secretRotationDAL.updateById(
rotationId,
{
encryptedData: encVarData.ciphertext,
encryptedDataIV: encVarData.iv,
encryptedDataTag: encVarData.tag,
keyEncoding: encVarData.encoding,
algorithm: encVarData.algorithm,
lastRotatedAt: new Date(),
statusMessage: "Rotated successfull",
status: "success"
},
tx
);
const updatedSecrets = await secretV2BridgeDAL.bulkUpdate(
encryptedSecrets.map(({ secretId, value }) => ({
// this secret id is validated when user is inserted
filter: { id: secretId, type: SecretType.Shared },
data: {
encryptedValue: secretManagerEncryptor({ plainText: Buffer.from(value) }).cipherTextBlob
}
})),
tx
);
await secretVersionV2BridgeDAL.insertMany(
updatedSecrets.map(({ id, updatedAt, createdAt, ...el }) => ({
...el,
secretId: id
})),
tx
);
});
} else {
if (!botKey) throw new BadRequestError({ message: "Bot not found" });
const encryptedSecrets = rotationOutputs.map(({ key: outputKey, secretId }) => ({
secretId,
value: encryptSymmetric128BitHexKeyUTF8(
typeof newCredential.outputs[outputKey] === "object"
? JSON.stringify(newCredential.outputs[outputKey])
: String(newCredential.outputs[outputKey]),
botKey
)
}));
// map the final values to output keys in the board
await secretRotationDAL.transaction(async (tx) => {
await secretRotationDAL.updateById(
rotationId,
{
encryptedData: encVarData.ciphertext,
encryptedDataIV: encVarData.iv,
encryptedDataTag: encVarData.tag,
keyEncoding: encVarData.encoding,
algorithm: encVarData.algorithm,
lastRotatedAt: new Date(),
statusMessage: "Rotated successfull",
status: "success"
},
tx
);
const updatedSecrets = await secretDAL.bulkUpdate(
encryptedSecrets.map(({ secretId, value }) => ({
// this secret id is validated when user is inserted
filter: { id: secretId, type: SecretType.Shared },
data: {
secretValueCiphertext: value.ciphertext,
secretValueIV: value.iv,
secretValueTag: value.tag
}
})),
tx
);
await secretVersionDAL.insertMany(
updatedSecrets.map(({ id, updatedAt, createdAt, ...el }) => {
if (!el.secretBlindIndex) throw new BadRequestError({ message: "Missing blind index" });
return {
...el,
secretId: id,
secretBlindIndex: el.secretBlindIndex
};
}),
tx
);
});
}
await telemetryService.sendPostHogEvents({
event: PostHogEventTypes.SecretRotated,
distinctId: "",
properties: {
numberOfSecrets: encryptedSecrets.length,
numberOfSecrets: numberOfSecretsRotated,
environment: secretRotation.environment.slug,
secretPath: secretRotation.secretPath,
workspaceId: secretRotation.projectId

View File

@ -1,12 +1,15 @@
import { ForbiddenError, subject } from "@casl/ability";
import Ajv from "ajv";
import { infisicalSymmetricEncypt } from "@app/lib/crypto/encryption";
import { ProjectVersion } from "@app/db/schemas";
import { decryptSymmetric128BitHexKeyUTF8, infisicalSymmetricEncypt } from "@app/lib/crypto/encryption";
import { BadRequestError } from "@app/lib/errors";
import { TProjectPermission } from "@app/lib/types";
import { TProjectDALFactory } from "@app/services/project/project-dal";
import { TProjectBotServiceFactory } from "@app/services/project-bot/project-bot-service";
import { TSecretDALFactory } from "@app/services/secret/secret-dal";
import { TSecretFolderDALFactory } from "@app/services/secret-folder/secret-folder-dal";
import { TSecretV2BridgeDALFactory } from "@app/services/secret-v2-bridge/secret-v2-bridge-dal";
import { TLicenseServiceFactory } from "../license/license-service";
import { TPermissionServiceFactory } from "../permission/permission-service";
@ -22,9 +25,11 @@ type TSecretRotationServiceFactoryDep = {
projectDAL: Pick<TProjectDALFactory, "findById">;
folderDAL: Pick<TSecretFolderDALFactory, "findBySecretPath">;
secretDAL: Pick<TSecretDALFactory, "find">;
secretV2BridgeDAL: Pick<TSecretV2BridgeDALFactory, "find">;
licenseService: Pick<TLicenseServiceFactory, "getPlan">;
permissionService: Pick<TPermissionServiceFactory, "getProjectPermission">;
secretRotationQueue: TSecretRotationQueueFactory;
projectBotService: Pick<TProjectBotServiceFactory, "getBotKey">;
};
export type TSecretRotationServiceFactory = ReturnType<typeof secretRotationServiceFactory>;
@ -37,7 +42,9 @@ export const secretRotationServiceFactory = ({
licenseService,
projectDAL,
folderDAL,
secretDAL
secretDAL,
projectBotService,
secretV2BridgeDAL
}: TSecretRotationServiceFactoryDep) => {
const getProviderTemplates = async ({
actor,
@ -92,15 +99,25 @@ export const secretRotationServiceFactory = ({
ProjectPermissionActions.Edit,
subject(ProjectPermissionSub.Secrets, { environment, secretPath })
);
const selectedSecrets = await secretDAL.find({
folderId: folder.id,
$in: { id: Object.values(outputs) }
});
if (selectedSecrets.length !== Object.values(outputs).length)
throw new BadRequestError({ message: "Secrets not found" });
const project = await projectDAL.findById(projectId);
const shouldUseBridge = project.version === ProjectVersion.V3;
if (shouldUseBridge) {
const selectedSecrets = await secretV2BridgeDAL.find({
folderId: folder.id,
$in: { id: Object.values(outputs) }
});
if (selectedSecrets.length !== Object.values(outputs).length)
throw new BadRequestError({ message: "Secrets not found" });
} else {
const selectedSecrets = await secretDAL.find({
folderId: folder.id,
$in: { id: Object.values(outputs) }
});
if (selectedSecrets.length !== Object.values(outputs).length)
throw new BadRequestError({ message: "Secrets not found" });
}
const plan = await licenseService.getPlan(project.orgId);
if (!plan.secretRotation)
throw new BadRequestError({
@ -148,10 +165,18 @@ export const secretRotationServiceFactory = ({
},
tx
);
const outputSecretMapping = await secretRotationDAL.secretOutputInsertMany(
Object.entries(outputs).map(([key, secretId]) => ({ key, secretId, rotationId: doc.id })),
tx
);
let outputSecretMapping;
if (shouldUseBridge) {
outputSecretMapping = await secretRotationDAL.secretOutputV2InsertMany(
Object.entries(outputs).map(([key, secretId]) => ({ key, secretId, rotationId: doc.id })),
tx
);
} else {
outputSecretMapping = await secretRotationDAL.secretOutputInsertMany(
Object.entries(outputs).map(([key, secretId]) => ({ key, secretId, rotationId: doc.id })),
tx
);
}
return { ...doc, outputs: outputSecretMapping, environment: folder.environment };
});
await secretRotationQueue.addToQueue(secretRotation.id, secretRotation.interval);
@ -167,8 +192,30 @@ export const secretRotationServiceFactory = ({
actorOrgId
);
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.SecretRotation);
const doc = await secretRotationDAL.find({ projectId });
return doc;
const { botKey, shouldUseSecretV2Bridge } = await projectBotService.getBotKey(projectId);
if (shouldUseSecretV2Bridge) {
const docs = await secretRotationDAL.findSecretV2({ projectId });
return docs;
}
if (!botKey) throw new BadRequestError({ message: "bot not found" });
const docs = await secretRotationDAL.find({ projectId });
return docs.map((el) => ({
...el,
outputs: el.outputs.map((output) => ({
...output,
secret: {
id: output.secret.id,
version: output.secret.version,
secretKey: decryptSymmetric128BitHexKeyUTF8({
ciphertext: output.secret.secretKeyCiphertext,
iv: output.secret.secretKeyIV,
tag: output.secret.secretKeyTag,
key: botKey
})
}
}))
}));
};
const restartById = async ({ actor, actorId, actorOrgId, actorAuthMethod, rotationId }: TRestartDTO) => {

View File

@ -1,15 +1,22 @@
import { ForbiddenError, subject } from "@casl/ability";
import { TableName, TSecretTagJunctionInsert } from "@app/db/schemas";
import { TableName, TSecretTagJunctionInsert, TSecretV2TagJunctionInsert } from "@app/db/schemas";
import { decryptSymmetric128BitHexKeyUTF8 } from "@app/lib/crypto";
import { BadRequestError, InternalServerError } from "@app/lib/errors";
import { groupBy } from "@app/lib/fn";
import { logger } from "@app/lib/logger";
import { TKmsServiceFactory } from "@app/services/kms/kms-service";
import { KmsDataKey } from "@app/services/kms/kms-types";
import { TProjectBotServiceFactory } from "@app/services/project-bot/project-bot-service";
import { TSecretDALFactory } from "@app/services/secret/secret-dal";
import { TSecretVersionDALFactory } from "@app/services/secret/secret-version-dal";
import { TSecretVersionTagDALFactory } from "@app/services/secret/secret-version-tag-dal";
import { TSecretFolderDALFactory } from "@app/services/secret-folder/secret-folder-dal";
import { TSecretFolderVersionDALFactory } from "@app/services/secret-folder/secret-folder-version-dal";
import { TSecretTagDALFactory } from "@app/services/secret-tag/secret-tag-dal";
import { TSecretV2BridgeDALFactory } from "@app/services/secret-v2-bridge/secret-v2-bridge-dal";
import { TSecretVersionV2DALFactory } from "@app/services/secret-v2-bridge/secret-version-dal";
import { TSecretVersionV2TagDALFactory } from "@app/services/secret-v2-bridge/secret-version-tag-dal";
import { TLicenseServiceFactory } from "../license/license-service";
import { TPermissionServiceFactory } from "../permission/permission-service";
@ -23,20 +30,27 @@ import {
import { TSnapshotDALFactory } from "./snapshot-dal";
import { TSnapshotFolderDALFactory } from "./snapshot-folder-dal";
import { TSnapshotSecretDALFactory } from "./snapshot-secret-dal";
import { TSnapshotSecretV2DALFactory } from "./snapshot-secret-v2-dal";
import { getFullFolderPath } from "./snapshot-service-fns";
type TSecretSnapshotServiceFactoryDep = {
snapshotDAL: TSnapshotDALFactory;
snapshotSecretDAL: TSnapshotSecretDALFactory;
snapshotSecretV2BridgeDAL: TSnapshotSecretV2DALFactory;
snapshotFolderDAL: TSnapshotFolderDALFactory;
secretVersionDAL: Pick<TSecretVersionDALFactory, "insertMany" | "findLatestVersionByFolderId">;
secretVersionV2BridgeDAL: Pick<TSecretVersionV2DALFactory, "insertMany" | "findLatestVersionByFolderId">;
folderVersionDAL: Pick<TSecretFolderVersionDALFactory, "findLatestVersionByFolderId" | "insertMany">;
secretDAL: Pick<TSecretDALFactory, "delete" | "insertMany">;
secretTagDAL: Pick<TSecretTagDALFactory, "saveTagsToSecret">;
secretV2BridgeDAL: Pick<TSecretV2BridgeDALFactory, "delete" | "insertMany">;
secretTagDAL: Pick<TSecretTagDALFactory, "saveTagsToSecret" | "saveTagsToSecretV2">;
secretVersionTagDAL: Pick<TSecretVersionTagDALFactory, "insertMany">;
secretVersionV2TagBridgeDAL: Pick<TSecretVersionV2TagDALFactory, "insertMany">;
folderDAL: Pick<TSecretFolderDALFactory, "findById" | "findBySecretPath" | "delete" | "insertMany" | "find">;
permissionService: Pick<TPermissionServiceFactory, "getProjectPermission">;
licenseService: Pick<TLicenseServiceFactory, "isValidLicense">;
kmsService: Pick<TKmsServiceFactory, "createCipherPairWithDataKey">;
projectBotService: Pick<TProjectBotServiceFactory, "getBotKey">;
};
export type TSecretSnapshotServiceFactory = ReturnType<typeof secretSnapshotServiceFactory>;
@ -52,7 +66,13 @@ export const secretSnapshotServiceFactory = ({
permissionService,
licenseService,
secretTagDAL,
secretVersionTagDAL
secretVersionTagDAL,
secretVersionV2BridgeDAL,
secretV2BridgeDAL,
snapshotSecretV2BridgeDAL,
secretVersionV2TagBridgeDAL,
kmsService,
projectBotService
}: TSecretSnapshotServiceFactoryDep) => {
const projectSecretSnapshotCount = async ({
environment,
@ -118,7 +138,7 @@ export const secretSnapshotServiceFactory = ({
};
const getSnapshotData = async ({ actorId, actor, actorOrgId, actorAuthMethod, id }: TGetSnapshotDataDTO) => {
const snapshot = await snapshotDAL.findSecretSnapshotDataById(id);
const snapshot = await snapshotDAL.findById(id);
if (!snapshot) throw new BadRequestError({ message: "Snapshot not found" });
const { permission } = await permissionService.getProjectPermission(
actor,
@ -127,31 +147,122 @@ export const secretSnapshotServiceFactory = ({
actorAuthMethod,
actorOrgId
);
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.SecretRollback);
const shouldUseBridge = snapshot.projectVersion === 3;
let snapshotDetails;
if (shouldUseBridge) {
const { decryptor: secretManagerDecryptor } = await kmsService.createCipherPairWithDataKey({
type: KmsDataKey.SecretManager,
projectId: snapshot.projectId
});
const encryptedSnapshotDetails = await snapshotDAL.findSecretSnapshotV2DataById(id);
snapshotDetails = {
...encryptedSnapshotDetails,
secretVersions: encryptedSnapshotDetails.secretVersions.map((el) => ({
...el,
secretKey: el.key,
secretValue: el.encryptedValue
? secretManagerDecryptor({ cipherTextBlob: el.encryptedValue }).toString()
: undefined,
secretComment: el.encryptedComment
? secretManagerDecryptor({ cipherTextBlob: el.encryptedComment }).toString()
: undefined
}))
};
} else {
const encryptedSnapshotDetails = await snapshotDAL.findSecretSnapshotDataById(id);
const { botKey } = await projectBotService.getBotKey(snapshot.projectId);
if (!botKey) throw new BadRequestError({ message: "bot not found" });
snapshotDetails = {
...encryptedSnapshotDetails,
secretVersions: encryptedSnapshotDetails.secretVersions.map((el) => ({
...el,
secretKey: decryptSymmetric128BitHexKeyUTF8({
ciphertext: el.secretKeyCiphertext,
iv: el.secretKeyIV,
tag: el.secretKeyTag,
key: botKey
}),
secretValue: decryptSymmetric128BitHexKeyUTF8({
ciphertext: el.secretValueCiphertext,
iv: el.secretValueIV,
tag: el.secretValueTag,
key: botKey
}),
secretComment:
el.secretCommentTag && el.secretCommentIV && el.secretCommentCiphertext
? decryptSymmetric128BitHexKeyUTF8({
ciphertext: el.secretCommentCiphertext,
iv: el.secretCommentIV,
tag: el.secretCommentTag,
key: botKey
})
: ""
}))
};
}
const fullFolderPath = await getFullFolderPath({
folderDAL,
folderId: snapshot.folderId,
envId: snapshot.environment.id
folderId: snapshotDetails.folderId,
envId: snapshotDetails.environment.id
});
// We need to check if the user has access to the secrets in the folder. If we don't do this, a user could theoretically access snapshot secret values even if they don't have read access to the secrets in the folder.
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Read,
subject(ProjectPermissionSub.Secrets, { environment: snapshot.environment.slug, secretPath: fullFolderPath })
subject(ProjectPermissionSub.Secrets, {
environment: snapshotDetails.environment.slug,
secretPath: fullFolderPath
})
);
return snapshot;
return snapshotDetails;
};
const performSnapshot = async (folderId: string) => {
try {
if (!licenseService.isValidLicense) throw new InternalServerError({ message: "Invalid license" });
const folder = await folderDAL.findById(folderId);
if (!folder) throw new BadRequestError({ message: "Folder not found" });
const shouldUseSecretV2Bridge = folder.projectVersion === 3;
if (shouldUseSecretV2Bridge) {
const snapshot = await snapshotDAL.transaction(async (tx) => {
const secretVersions = await secretVersionV2BridgeDAL.findLatestVersionByFolderId(folderId, tx);
const folderVersions = await folderVersionDAL.findLatestVersionByFolderId(folderId, tx);
const newSnapshot = await snapshotDAL.create(
{
folderId,
envId: folder.environment.envId,
parentFolderId: folder.parentId
},
tx
);
const snapshotSecrets = await snapshotSecretV2BridgeDAL.insertMany(
secretVersions.map(({ id }) => ({
secretVersionId: id,
envId: folder.environment.envId,
snapshotId: newSnapshot.id
})),
tx
);
const snapshotFolders = await snapshotFolderDAL.insertMany(
folderVersions.map(({ id }) => ({
folderVersionId: id,
envId: folder.environment.envId,
snapshotId: newSnapshot.id
})),
tx
);
return { ...newSnapshot, secrets: snapshotSecrets, folder: snapshotFolders };
});
return snapshot;
}
const snapshot = await snapshotDAL.transaction(async (tx) => {
const folder = await folderDAL.findById(folderId, tx);
if (!folder) throw new BadRequestError({ message: "Folder not found" });
const secretVersions = await secretVersionDAL.findLatestVersionByFolderId(folderId, tx);
const folderVersions = await folderVersionDAL.findLatestVersionByFolderId(folderId, tx);
const newSnapshot = await snapshotDAL.create(
@ -199,6 +310,7 @@ export const secretSnapshotServiceFactory = ({
}: TRollbackSnapshotDTO) => {
const snapshot = await snapshotDAL.findById(snapshotId);
if (!snapshot) throw new BadRequestError({ message: "Snapshot not found" });
const shouldUseBridge = snapshot.projectVersion === 3;
const { permission } = await permissionService.getProjectPermission(
actor,
@ -212,6 +324,117 @@ export const secretSnapshotServiceFactory = ({
ProjectPermissionSub.SecretRollback
);
if (shouldUseBridge) {
const rollback = await snapshotDAL.transaction(async (tx) => {
const rollbackSnaps = await snapshotDAL.findRecursivelySnapshotsV2Bridge(snapshot.id, tx);
// this will remove all secrets in current folder
const deletedTopLevelSecs = await secretV2BridgeDAL.delete({ folderId: snapshot.folderId }, tx);
const deletedTopLevelSecsGroupById = groupBy(deletedTopLevelSecs, (item) => item.id);
// this will remove all secrets and folders on child
// due to sql foreign key and link list connection removing the folders removes everything below too
const deletedFolders = await folderDAL.delete({ parentId: snapshot.folderId, isReserved: false }, tx);
const deletedTopLevelFolders = groupBy(
deletedFolders.filter(({ parentId }) => parentId === snapshot.folderId),
(item) => item.id
);
const folders = await folderDAL.insertMany(
rollbackSnaps.flatMap(({ folderVersion, folderId }) =>
folderVersion.map(({ name, id, latestFolderVersion }) => ({
envId: snapshot.envId,
id,
// this means don't bump up the version if not root folder
// because below ones can be same version as nothing changed
version: deletedTopLevelFolders[folderId] ? latestFolderVersion + 1 : latestFolderVersion,
name,
parentId: folderId
}))
),
tx
);
const secrets = await secretV2BridgeDAL.insertMany(
rollbackSnaps.flatMap(({ secretVersions, folderId }) =>
secretVersions.map(
({ latestSecretVersion, version, updatedAt, createdAt, secretId, envId, id, tags, ...el }) => ({
...el,
id: secretId,
version: deletedTopLevelSecsGroupById[secretId] ? latestSecretVersion + 1 : latestSecretVersion,
folderId
})
)
),
tx
);
const secretTagsToBeInsert: TSecretV2TagJunctionInsert[] = [];
const secretVerTagToBeInsert: Record<string, string[]> = {};
rollbackSnaps.forEach(({ secretVersions }) => {
secretVersions.forEach((secVer) => {
secVer.tags.forEach((tag) => {
secretTagsToBeInsert.push({ secrets_v2Id: secVer.secretId, secret_tagsId: tag.id });
if (!secretVerTagToBeInsert?.[secVer.secretId]) secretVerTagToBeInsert[secVer.secretId] = [];
secretVerTagToBeInsert[secVer.secretId].push(tag.id);
});
});
});
await secretTagDAL.saveTagsToSecretV2(secretTagsToBeInsert, tx);
const folderVersions = await folderVersionDAL.insertMany(
folders.map(({ version, name, id, envId }) => ({
name,
version,
folderId: id,
envId
})),
tx
);
const secretVersions = await secretVersionV2BridgeDAL.insertMany(
secrets.map(({ id, updatedAt, createdAt, ...el }) => ({ ...el, secretId: id })),
tx
);
await secretVersionV2TagBridgeDAL.insertMany(
secretVersions.flatMap(({ secretId, id }) =>
secretVerTagToBeInsert?.[secretId]?.length
? secretVerTagToBeInsert[secretId].map((tagId) => ({
[`${TableName.SecretTag}Id` as const]: tagId,
[`${TableName.SecretVersionV2}Id` as const]: id
}))
: []
),
tx
);
const newSnapshot = await snapshotDAL.create(
{
folderId: snapshot.folderId,
envId: snapshot.envId,
parentFolderId: snapshot.parentFolderId
},
tx
);
const snapshotSecrets = await snapshotSecretV2BridgeDAL.insertMany(
secretVersions
.filter(({ secretId }) => Boolean(deletedTopLevelSecsGroupById?.[secretId]))
.map(({ id }) => ({
secretVersionId: id,
envId: newSnapshot.envId,
snapshotId: newSnapshot.id
})),
tx
);
const snapshotFolders = await snapshotFolderDAL.insertMany(
folderVersions
.filter(({ folderId }) => Boolean(deletedTopLevelFolders?.[folderId]))
.map(({ id }) => ({
folderVersionId: id,
envId: newSnapshot.envId,
snapshotId: newSnapshot.id
})),
tx
);
return { ...newSnapshot, snapshotSecrets, snapshotFolders };
});
return rollback;
}
const rollback = await snapshotDAL.transaction(async (tx) => {
const rollbackSnaps = await snapshotDAL.findRecursivelySnapshots(snapshot.id, tx);
// this will remove all secrets in current folder

View File

@ -1,14 +1,17 @@
/* eslint-disable no-await-in-loop */
import { Knex } from "knex";
import { z } from "zod";
import { TDbClient } from "@app/db";
import {
SecretVersionsSchema,
SecretVersionsV2Schema,
TableName,
TSecretFolderVersions,
TSecretSnapshotFolders,
TSecretSnapshots,
TSecretVersions
TSecretVersions,
TSecretVersionsV2
} from "@app/db/schemas";
import { DatabaseError } from "@app/lib/errors";
import { ormify, selectAllTableCols, sqlNestRelationships } from "@app/lib/knex";
@ -24,12 +27,14 @@ export const snapshotDALFactory = (db: TDbClient) => {
const data = await (tx || db.replicaNode())(TableName.Snapshot)
.where(`${TableName.Snapshot}.id`, id)
.join(TableName.Environment, `${TableName.Snapshot}.envId`, `${TableName.Environment}.id`)
.join(TableName.Project, `${TableName.Environment}.projectId`, `${TableName.Project}.id`)
.select(selectAllTableCols(TableName.Snapshot))
.select(
db.ref("id").withSchema(TableName.Environment).as("envId"),
db.ref("projectId").withSchema(TableName.Environment),
db.ref("name").withSchema(TableName.Environment).as("envName"),
db.ref("slug").withSchema(TableName.Environment).as("envSlug")
db.ref("slug").withSchema(TableName.Environment).as("envSlug"),
db.ref("version").withSchema(TableName.Project).as("projectVersion")
)
.first();
if (data) {
@ -149,6 +154,101 @@ export const snapshotDALFactory = (db: TDbClient) => {
}
};
const findSecretSnapshotV2DataById = async (snapshotId: string, tx?: Knex) => {
try {
const data = await (tx || db.replicaNode())(TableName.Snapshot)
.where(`${TableName.Snapshot}.id`, snapshotId)
.join(TableName.Environment, `${TableName.Snapshot}.envId`, `${TableName.Environment}.id`)
.leftJoin(TableName.SnapshotSecretV2, `${TableName.Snapshot}.id`, `${TableName.SnapshotSecretV2}.snapshotId`)
.leftJoin(
TableName.SecretVersionV2,
`${TableName.SnapshotSecretV2}.secretVersionId`,
`${TableName.SecretVersionV2}.id`
)
.leftJoin(
TableName.SecretVersionV2Tag,
`${TableName.SecretVersionV2Tag}.${TableName.SecretVersionV2}Id`,
`${TableName.SecretVersionV2}.id`
)
.leftJoin(
TableName.SecretTag,
`${TableName.SecretVersionV2Tag}.${TableName.SecretTag}Id`,
`${TableName.SecretTag}.id`
)
.leftJoin(TableName.SnapshotFolder, `${TableName.SnapshotFolder}.snapshotId`, `${TableName.Snapshot}.id`)
.leftJoin<TSecretFolderVersions>(
TableName.SecretFolderVersion,
`${TableName.SnapshotFolder}.folderVersionId`,
`${TableName.SecretFolderVersion}.id`
)
.select(selectAllTableCols(TableName.SecretVersionV2))
.select(
db.ref("id").withSchema(TableName.Snapshot).as("snapshotId"),
db.ref("createdAt").withSchema(TableName.Snapshot).as("snapshotCreatedAt"),
db.ref("updatedAt").withSchema(TableName.Snapshot).as("snapshotUpdatedAt"),
db.ref("id").withSchema(TableName.Environment).as("envId"),
db.ref("name").withSchema(TableName.Environment).as("envName"),
db.ref("slug").withSchema(TableName.Environment).as("envSlug"),
db.ref("projectId").withSchema(TableName.Environment),
db.ref("name").withSchema(TableName.SecretFolderVersion).as("folderVerName"),
db.ref("folderId").withSchema(TableName.SecretFolderVersion).as("folderVerId"),
db.ref("id").withSchema(TableName.SecretTag).as("tagId"),
db.ref("id").withSchema(TableName.SecretVersionV2Tag).as("tagVersionId"),
db.ref("color").withSchema(TableName.SecretTag).as("tagColor"),
db.ref("slug").withSchema(TableName.SecretTag).as("tagSlug"),
db.ref("name").withSchema(TableName.SecretTag).as("tagName")
);
return sqlNestRelationships({
data,
key: "snapshotId",
parentMapper: ({
snapshotId: id,
folderId,
projectId,
envId,
envSlug,
envName,
snapshotCreatedAt: createdAt,
snapshotUpdatedAt: updatedAt
}) => ({
id,
folderId,
projectId,
createdAt,
updatedAt,
environment: { id: envId, slug: envSlug, name: envName }
}),
childrenMapper: [
{
key: "id",
label: "secretVersions" as const,
mapper: (el) => SecretVersionsV2Schema.parse(el),
childrenMapper: [
{
key: "tagVersionId",
label: "tags" as const,
mapper: ({ tagId: id, tagName: name, tagSlug: slug, tagColor: color, tagVersionId: vId }) => ({
id,
name,
slug,
color,
vId
})
}
]
},
{
key: "folderVerId",
label: "folderVersion" as const,
mapper: ({ folderVerId: id, folderVerName: name }) => ({ id, name })
}
]
})?.[0];
} catch (error) {
throw new DatabaseError({ error, name: "FindSecretSnapshotDataById" });
}
};
// this is used for rollback
// from a starting snapshot it will collect all the secrets and folder of that
// then it will start go through recursively the below folders latest snapshots then their child folder snapshot until leaf node
@ -304,6 +404,161 @@ export const snapshotDALFactory = (db: TDbClient) => {
}
};
// this is used for rollback
// from a starting snapshot it will collect all the secrets and folder of that
// then it will start go through recursively the below folders latest snapshots then their child folder snapshot until leaf node
// the recursive part find all snapshot id
// then joins with respective secrets and folder
const findRecursivelySnapshotsV2Bridge = async (snapshotId: string, tx?: Knex) => {
try {
const data = await (tx || db)
.withRecursive("parent", (qb) => {
void qb
.from(TableName.Snapshot)
.leftJoin<TSecretSnapshotFolders>(
TableName.SnapshotFolder,
`${TableName.SnapshotFolder}.snapshotId`,
`${TableName.Snapshot}.id`
)
.leftJoin<TSecretFolderVersions>(
TableName.SecretFolderVersion,
`${TableName.SnapshotFolder}.folderVersionId`,
`${TableName.SecretFolderVersion}.id`
)
.select(selectAllTableCols(TableName.Snapshot))
.select({ depth: 1 })
.select(
db.ref("name").withSchema(TableName.SecretFolderVersion).as("folderVerName"),
db.ref("folderId").withSchema(TableName.SecretFolderVersion).as("folderVerId")
)
.where(`${TableName.Snapshot}.id`, snapshotId)
.union(
(cb) =>
void cb
.select(selectAllTableCols(TableName.Snapshot))
.select({ depth: db.raw("parent.depth + 1") })
.select(
db.ref("name").withSchema(TableName.SecretFolderVersion).as("folderVerName"),
db.ref("folderId").withSchema(TableName.SecretFolderVersion).as("folderVerId")
)
.from(TableName.Snapshot)
.join<TSecretSnapshots, TSecretSnapshots & { secretId: string; max: number }>(
db(TableName.Snapshot).groupBy("folderId").max("createdAt").select("folderId").as("latestVersion"),
`${TableName.Snapshot}.createdAt`,
"latestVersion.max"
)
.leftJoin<TSecretSnapshotFolders>(
TableName.SnapshotFolder,
`${TableName.SnapshotFolder}.snapshotId`,
`${TableName.Snapshot}.id`
)
.leftJoin<TSecretFolderVersions>(
TableName.SecretFolderVersion,
`${TableName.SnapshotFolder}.folderVersionId`,
`${TableName.SecretFolderVersion}.id`
)
.join("parent", "parent.folderVerId", `${TableName.Snapshot}.folderId`)
);
})
.orderBy("depth", "asc")
.from<TSecretSnapshots & { folderVerId: string; folderVerName: string }>("parent")
.leftJoin<TSecretSnapshots>(TableName.SnapshotSecretV2, `parent.id`, `${TableName.SnapshotSecretV2}.snapshotId`)
.leftJoin<TSecretVersionsV2>(
TableName.SecretVersionV2,
`${TableName.SnapshotSecretV2}.secretVersionId`,
`${TableName.SecretVersionV2}.id`
)
.leftJoin(
TableName.SecretVersionV2Tag,
`${TableName.SecretVersionV2Tag}.${TableName.SecretVersionV2}Id`,
`${TableName.SecretVersionV2}.id`
)
.leftJoin(
TableName.SecretTag,
`${TableName.SecretVersionV2Tag}.${TableName.SecretTag}Id`,
`${TableName.SecretTag}.id`
)
.leftJoin<{ latestSecretVersion: number }>(
(tx || db)(TableName.SecretVersionV2)
.groupBy("secretId")
.select("secretId")
.max("version")
.as("secGroupByMaxVersion"),
`${TableName.SecretVersionV2}.secretId`,
"secGroupByMaxVersion.secretId"
)
.leftJoin<{ latestFolderVersion: number }>(
(tx || db)(TableName.SecretFolderVersion)
.groupBy("folderId")
.select("folderId")
.max("version")
.as("folderGroupByMaxVersion"),
`parent.folderId`,
"folderGroupByMaxVersion.folderId"
)
.select(selectAllTableCols(TableName.SecretVersionV2))
.select(
db.ref("id").withSchema("parent").as("snapshotId"),
db.ref("folderId").withSchema("parent").as("snapshotFolderId"),
db.ref("parentFolderId").withSchema("parent").as("snapshotParentFolderId"),
db.ref("folderVerName").withSchema("parent"),
db.ref("folderVerId").withSchema("parent"),
db.ref("max").withSchema("secGroupByMaxVersion").as("latestSecretVersion"),
db.ref("max").withSchema("folderGroupByMaxVersion").as("latestFolderVersion"),
db.ref("id").withSchema(TableName.SecretTag).as("tagId"),
db.ref("id").withSchema(TableName.SecretVersionV2Tag).as("tagVersionId"),
db.ref("color").withSchema(TableName.SecretTag).as("tagColor"),
db.ref("slug").withSchema(TableName.SecretTag).as("tagSlug"),
db.ref("name").withSchema(TableName.SecretTag).as("tagName")
);
const formated = sqlNestRelationships({
data,
key: "snapshotId",
parentMapper: ({ snapshotId: id, snapshotFolderId: folderId, snapshotParentFolderId: parentFolderId }) => ({
id,
folderId,
parentFolderId
}),
childrenMapper: [
{
key: "id",
label: "secretVersions" as const,
mapper: (el) => ({
...SecretVersionsV2Schema.parse(el),
latestSecretVersion: el.latestSecretVersion as number
}),
childrenMapper: [
{
key: "tagVersionId",
label: "tags" as const,
mapper: ({ tagId: id, tagName: name, tagSlug: slug, tagColor: color, tagVersionId: vId }) => ({
id,
name,
slug,
color,
vId
})
}
]
},
{
key: "folderVerId",
label: "folderVersion" as const,
mapper: ({ folderVerId: id, folderVerName: name, latestFolderVersion }) => ({
id,
name,
latestFolderVersion: latestFolderVersion as number
})
}
]
});
return formated;
} catch (error) {
throw new DatabaseError({ error, name: "FindRecursivelySnapshots" });
}
};
// instead of copying all child folders
// we will take the latest snapshot of those folders
// when we need to rollback we will pull from these snapshots
@ -465,13 +720,108 @@ export const snapshotDALFactory = (db: TDbClient) => {
}
};
// special query for migration for secret v2
const findNSecretV1SnapshotByFolderId = async (folderId: string, n = 15, tx?: Knex) => {
try {
const query = (tx || db.replicaNode())(TableName.Snapshot)
.leftJoin(TableName.SnapshotSecret, `${TableName.Snapshot}.id`, `${TableName.SnapshotSecret}.snapshotId`)
.leftJoin(
TableName.SecretVersion,
`${TableName.SnapshotSecret}.secretVersionId`,
`${TableName.SecretVersion}.id`
)
.leftJoin(
TableName.SecretVersionTag,
`${TableName.SecretVersionTag}.${TableName.SecretVersion}Id`,
`${TableName.SecretVersion}.id`
)
.select(selectAllTableCols(TableName.SecretVersion))
.select(
db.ref("id").withSchema(TableName.Snapshot).as("snapshotId"),
db.ref("createdAt").withSchema(TableName.Snapshot).as("snapshotCreatedAt"),
db.ref("updatedAt").withSchema(TableName.Snapshot).as("snapshotUpdatedAt"),
db.ref("envId").withSchema(TableName.SnapshotSecret).as("snapshotEnvId"),
db.ref("id").withSchema(TableName.SecretVersionTag).as("secretVersionTagId"),
db.ref("secret_versionsId").withSchema(TableName.SecretVersionTag).as("secretVersionTagSecretId"),
db.ref("secret_tagsId").withSchema(TableName.SecretVersionTag).as("secretVersionTagSecretTagId"),
db.raw(
`DENSE_RANK() OVER (partition by ${TableName.Snapshot}."id" ORDER BY ${TableName.SecretVersion}."createdAt") as rank`
)
)
.orderBy(`${TableName.Snapshot}.createdAt`, "desc")
.where(`${TableName.Snapshot}.folderId`, folderId);
const data = await (tx || db)
.with("w", query)
.select("*")
.from<Awaited<typeof query>[number]>("w")
.andWhere("w.rank", "<", n);
return sqlNestRelationships({
data,
key: "snapshotId",
parentMapper: ({ snapshotId: id, snapshotCreatedAt: createdAt, snapshotUpdatedAt: updatedAt }) => ({
id,
folderId,
createdAt,
updatedAt
}),
childrenMapper: [
{
key: "id",
label: "secretVersions" as const,
mapper: (el) => SecretVersionsSchema.extend({ snapshotEnvId: z.string() }).parse(el),
childrenMapper: [
{
key: "secretVersionTagId",
label: "tags" as const,
mapper: ({ secretVersionTagId, secretVersionTagSecretId, secretVersionTagSecretTagId }) => ({
id: secretVersionTagId,
secretVersionId: secretVersionTagSecretId,
secretTagId: secretVersionTagSecretTagId
})
}
]
}
]
});
} catch (error) {
throw new DatabaseError({ error, name: "FindSecretSnapshotDataById" });
}
};
const deleteSnapshotsAboveLimit = async (folderId: string, n = 15, tx?: Knex) => {
try {
const query = await (tx || db)
.with("to_delete", (qb) => {
void qb
.select("id")
.from(TableName.Snapshot)
.where("folderId", folderId)
.orderBy("createdAt", "desc")
.offset(n);
})
.from(TableName.Snapshot)
.whereIn("id", (qb) => {
void qb.select("id").from("to_delete");
})
.delete();
return query;
} catch (error) {
throw new DatabaseError({ error, name: "DeleteSnapshotsAboveLimit" });
}
};
return {
...secretSnapshotOrm,
findById,
findLatestSnapshotByFolderId,
findRecursivelySnapshots,
findRecursivelySnapshotsV2Bridge,
countOfSnapshotsByFolderId,
findSecretSnapshotDataById,
pruneExcessSnapshots
findSecretSnapshotV2DataById,
pruneExcessSnapshots,
findNSecretV1SnapshotByFolderId,
deleteSnapshotsAboveLimit
};
};

View File

@ -0,0 +1,10 @@
import { TDbClient } from "@app/db";
import { TableName } from "@app/db/schemas";
import { ormify } from "@app/lib/knex";
export type TSnapshotSecretV2DALFactory = ReturnType<typeof snapshotSecretV2DALFactory>;
export const snapshotSecretV2DALFactory = (db: TDbClient) => {
const snapshotSecretOrm = ormify(db, TableName.SnapshotSecretV2);
return snapshotSecretOrm;
};

View File

@ -6,7 +6,15 @@ export type TKeyStoreFactory = ReturnType<typeof keyStoreFactory>;
// all the key prefixes used must be set here to avoid conflict
export enum KeyStorePrefixes {
SecretReplication = "secret-replication-import-lock"
SecretReplication = "secret-replication-import-lock",
KmsProjectDataKeyCreation = "kms-project-data-key-creation-lock",
KmsProjectKeyCreation = "kms-project-key-creation-lock",
WaitUntilReadyKmsProjectDataKeyCreation = "wait-until-ready-kms-project-data-key-creation-",
WaitUntilReadyKmsProjectKeyCreation = "wait-until-ready-kms-project-key-creation-",
KmsOrgKeyCreation = "kms-org-key-creation-lock",
KmsOrgDataKeyCreation = "kms-org-data-key-creation-lock",
WaitUntilReadyKmsOrgKeyCreation = "wait-until-ready-kms-org-key-creation-",
WaitUntilReadyKmsOrgDataKeyCreation = "wait-until-ready-kms-org-data-key-creation-"
}
type TWaitTillReady = {
@ -32,7 +40,7 @@ export const keyStoreFactory = (redisUrl: string) => {
exp: number | string,
value: string | number | Buffer,
prefix?: string
) => redis.setex(prefix ? `${prefix}:${key}` : key, exp, value);
) => redis.set(prefix ? `${prefix}:${key}` : key, value, "EX", exp);
const deleteItem = async (key: string) => redis.del(key);
@ -57,7 +65,7 @@ export const keyStoreFactory = (redisUrl: string) => {
});
attempts += 1;
// eslint-disable-next-line
isReady = keyCheckCb(await getItem(key, "wait_till_ready"));
isReady = keyCheckCb(await getItem(key));
}
};

View File

@ -425,6 +425,21 @@ export const PROJECTS = {
},
LIST_INTEGRATION_AUTHORIZATION: {
workspaceId: "The ID of the project to list integration auths for."
},
LIST_CAS: {
slug: "The slug of the project to list CAs for.",
status: "The status of the CA to filter by.",
friendlyName: "The friendly name of the CA to filter by.",
commonName: "The common name of the CA to filter by.",
offset: "The offset to start from. If you enter 10, it will start from the 10th CA.",
limit: "The number of CAs to return."
},
LIST_CERTIFICATES: {
slug: "The slug of the project to list certificates for.",
friendlyName: "The friendly name of the certificate to filter by.",
commonName: "The common name of the certificate to filter by.",
offset: "The offset to start from. If you enter 10, it will start from the 10th certificate.",
limit: "The number of certificates to return."
}
} as const;
@ -510,6 +525,10 @@ export const ENVIRONMENTS = {
DELETE: {
workspaceId: "The ID of the project to delete the environment from.",
id: "The ID of the environment to delete."
},
GET: {
workspaceId: "The ID of the project the environment belongs to.",
id: "The ID of the environment to fetch."
}
} as const;
@ -589,7 +608,9 @@ export const RAW_SECRETS = {
skipMultilineEncoding: "Skip multiline encoding for the secret value.",
type: "The type of the secret to create.",
workspaceId: "The ID of the project to create the secret in.",
tagIds: "The ID of the tags to be attached to the created secret."
tagIds: "The ID of the tags to be attached to the created secret.",
secretReminderRepeatDays: "Interval for secret rotation notifications, measured in days",
secretReminderNote: "Note to be attached in notification email"
},
GET: {
expand: "Whether or not to expand secret references",
@ -612,7 +633,10 @@ export const RAW_SECRETS = {
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.",
tagIds: "The ID of the tags to be attached to the updated secret."
tagIds: "The ID of the tags to be attached to the updated secret.",
secretReminderRepeatDays: "Interval for secret rotation notifications, measured in days",
secretReminderNote: "Note to be attached in notification email",
newSecretName: "The new name for the secret"
},
DELETE: {
secretName: "The name of the secret to delete.",
@ -1032,7 +1056,7 @@ export const CERTIFICATE_AUTHORITIES = {
},
SIGN_INTERMEDIATE: {
caId: "The ID of the CA to sign the intermediate certificate with",
csr: "The CSR to sign with the CA",
csr: "The pem-encoded CSR to sign with the CA",
notBefore: "The date and time when the intermediate CA becomes valid in YYYY-MM-DDTHH:mm:ss.sssZ format",
notAfter: "The date and time when the intermediate CA expires in YYYY-MM-DDTHH:mm:ss.sssZ format",
maxPathLength:
@ -1062,6 +1086,21 @@ export const CERTIFICATE_AUTHORITIES = {
privateKey: "The private key of the issued certificate",
serialNumber: "The serial number of the issued certificate"
},
SIGN_CERT: {
caId: "The ID of the CA to issue the certificate from",
csr: "The pem-encoded CSR to sign with the CA to be used for certificate issuance",
friendlyName: "A friendly name for the certificate",
commonName: "The common name (CN) for the certificate",
altNames:
"A comma-delimited list of Subject Alternative Names (SANs) for the certificate; these can be host names or email addresses.",
ttl: "The time to live for the certificate such as 1m, 1h, 1d, 1y, ...",
notBefore: "The date and time when the certificate becomes valid in YYYY-MM-DDTHH:mm:ss.sssZ format",
notAfter: "The date and time when the certificate expires in YYYY-MM-DDTHH:mm:ss.sssZ format",
certificate: "The issued certificate",
issuingCaCertificate: "The certificate of the issuing CA",
certificateChain: "The certificate chain of the issued certificate",
serialNumber: "The serial number of the issued certificate"
},
GET_CRL: {
caId: "The ID of the CA to get the certificate revocation list (CRL) for",
crl: "The certificate revocation list (CRL) of the CA"

View File

@ -116,6 +116,8 @@ export const decryptAsymmetric = ({ ciphertext, nonce, publicKey, privateKey }:
export const generateSymmetricKey = (size = 32) => crypto.randomBytes(size).toString("base64");
export const generateHash = (value: string) => crypto.createHash("sha256").update(value).digest("hex");
export const generateAsymmetricKeyPair = () => {
const pair = nacl.box.keyPair();
@ -224,8 +226,9 @@ export const infisicalSymmetricDecrypt = <T = string>({
keyEncoding: SecretKeyEncoding;
}) => {
const appCfg = getConfig();
const rootEncryptionKey = appCfg.ROOT_ENCRYPTION_KEY;
const encryptionKey = appCfg.ENCRYPTION_KEY;
// the or gate is used used in migration
const rootEncryptionKey = appCfg?.ROOT_ENCRYPTION_KEY || process.env.ROOT_ENCRYPTION_KEY;
const encryptionKey = appCfg?.ENCRYPTION_KEY || process.env.ENCRYPTION_KEY;
if (rootEncryptionKey && keyEncoding === SecretKeyEncoding.BASE64) {
const data = decryptSymmetric({ key: rootEncryptionKey, iv, tag, ciphertext });
return data as T;

View File

@ -17,6 +17,23 @@ export const groupBy = <T, Key extends string | number | symbol>(
{} as Record<Key, T[]>
);
/**
* Sorts an array of items into groups. The return value is a map where the keys are
* the group ids the given getGroupId function produced and the value will be the last found one for the group key
*/
export const groupByUnique = <T, Key extends string | number | symbol>(
array: readonly T[],
getGroupId: (item: T) => Key
): Record<Key, T> =>
array.reduce(
(acc, item) => {
const groupId = getGroupId(item);
acc[groupId] = item;
return acc;
},
{} as Record<Key, T>
);
/**
* Given a list of items returns a new list with only
* unique items. Accepts an optional identity function

View File

@ -6,3 +6,4 @@ export * from "./array";
export * from "./dates";
export * from "./object";
export * from "./string";
export * from "./undefined";

View File

@ -0,0 +1,3 @@
export const executeIfDefined = <T, R>(func: (input: T) => R, input: T | undefined): R | undefined => {
return input === undefined ? undefined : func(input);
};

View File

@ -19,23 +19,43 @@ export const withTransaction = <K extends object>(db: Knex, dal: K) => ({
export type TFindFilter<R extends object = object> = Partial<R> & {
$in?: Partial<{ [k in keyof R]: R[k][] }>;
$search?: Partial<{ [k in keyof R]: R[k] }>;
};
export const buildFindFilter =
<R extends object = object>({ $in, ...filter }: TFindFilter<R>) =>
<R extends object = object>({ $in, $search, ...filter }: TFindFilter<R>) =>
(bd: Knex.QueryBuilder<R, R>) => {
void bd.where(filter);
if ($in) {
Object.entries($in).forEach(([key, val]) => {
void bd.whereIn(key as never, val as never);
if (val) {
void bd.whereIn(key as never, val as never);
}
});
}
if ($search) {
Object.entries($search).forEach(([key, val]) => {
if (val) {
void bd.whereILike(key as never, val as never);
}
});
}
return bd;
};
export type TFindOpt<R extends object = object> = {
export type TFindReturn<TQuery extends Knex.QueryBuilder, TCount extends boolean = false> = Array<
Awaited<TQuery>[0] &
(TCount extends true
? {
count: string;
}
: unknown)
>;
export type TFindOpt<R extends object = object, TCount extends boolean = boolean> = {
limit?: number;
offset?: number;
sort?: Array<[keyof R, "asc" | "desc"] | [keyof R, "asc" | "desc", "first" | "last"]>;
count?: TCount;
tx?: Knex;
};
@ -66,18 +86,22 @@ export const ormify = <DbOps extends object, Tname extends keyof Tables>(db: Kne
throw new DatabaseError({ error, name: "Find one" });
}
},
find: async (
find: async <TCount extends boolean = false>(
filter: TFindFilter<Tables[Tname]["base"]>,
{ offset, limit, sort, tx }: TFindOpt<Tables[Tname]["base"]> = {}
{ offset, limit, sort, count, tx }: TFindOpt<Tables[Tname]["base"], TCount> = {}
) => {
try {
const query = (tx || db.replicaNode())(tableName).where(buildFindFilter(filter));
if (count) {
void query.select(db.raw("COUNT(*) OVER() AS count"));
void query.select("*");
}
if (limit) void query.limit(limit);
if (offset) void query.offset(offset);
if (sort) {
void query.orderBy(sort.map(([column, order, nulls]) => ({ column: column as string, order, nulls })));
}
const res = await query;
const res = (await query) as TFindReturn<typeof query, TCount>;
return res;
} catch (error) {
throw new DatabaseError({ error, name: "Find one" });
@ -104,6 +128,19 @@ export const ormify = <DbOps extends object, Tname extends keyof Tables>(db: Kne
throw new DatabaseError({ error, name: "Create" });
}
},
upsert: async (data: readonly Tables[Tname]["insert"][], onConflictField: keyof Tables[Tname]["base"], tx?: Knex) => {
try {
if (!data.length) return [];
const res = await (tx || db)(tableName)
.insert(data as never)
.onConflict(onConflictField as never)
.merge()
.returning("*");
return res;
} catch (error) {
throw new DatabaseError({ error, name: "Create" });
}
},
updateById: async (
id: string,
{

View File

@ -12,3 +12,12 @@ export const stripUndefinedInWhere = <T extends object>(val: T): Exclude<T, unde
});
return copy as Exclude<T, undefined>;
};
// if its undefined its skipped in knex
// if its empty string its set as null
// else pass to the required one
export const setKnexStringValue = <T>(value: string | null | undefined, cb: (arg: string) => T) => {
if (typeof value === "undefined") return;
if (value === "" || value === null) return null;
return cb(value);
};

View File

@ -47,3 +47,8 @@ export enum EnforcementLevel {
Hard = "hard",
Soft = "soft"
}
export enum SecretSharingAccessType {
Anyone = "anyone",
Organization = "organization"
}

View File

@ -25,7 +25,8 @@ export enum QueueName {
DynamicSecretRevocation = "dynamic-secret-revocation",
CaCrlRotation = "ca-crl-rotation",
SecretReplication = "secret-replication",
SecretSync = "secret-sync" // parent queue to push integration sync, webhook, and secret replication
SecretSync = "secret-sync", // parent queue to push integration sync, webhook, and secret replication
ProjectV3Migration = "project-v3-migration"
}
export enum QueueJobs {
@ -44,7 +45,8 @@ export enum QueueJobs {
DynamicSecretPruning = "dynamic-secret-pruning",
CaCrlRotation = "ca-crl-rotation-job",
SecretReplication = "secret-replication",
SecretSync = "secret-sync" // parent queue to push integration sync, webhook, and secret replication
SecretSync = "secret-sync", // parent queue to push integration sync, webhook, and secret replication
ProjectV3Migration = "project-v3-migration"
}
export type TQueueJobTypes = {
@ -136,6 +138,10 @@ export type TQueueJobTypes = {
name: QueueJobs.SecretSync;
payload: TSyncSecretsDTO;
};
[QueueName.ProjectV3Migration]: {
name: QueueJobs.ProjectV3Migration;
payload: { projectId: string };
};
};
export type TQueueServiceFactory = ReturnType<typeof queueServiceFactory>;
@ -210,6 +216,7 @@ export const queueServiceFactory = (redisUrl: string) => {
const job = await q.getJob(jobId);
if (!job) return true;
if (!job.repeatJobKey) return true;
await job.remove();
return q.removeRepeatableByKey(job.repeatJobKey);
};

View File

@ -1,7 +1,6 @@
import type { RateLimitOptions, RateLimitPluginOptions } from "@fastify/rate-limit";
import { Redis } from "ioredis";
import { getRateLimiterConfig } from "@app/ee/services/rate-limit/rate-limit-service";
import { getConfig } from "@app/lib/config/env";
export const globalRateLimiterCfg = (): RateLimitPluginOptions => {
@ -22,14 +21,16 @@ export const globalRateLimiterCfg = (): RateLimitPluginOptions => {
// GET endpoints
export const readLimit: RateLimitOptions = {
timeWindow: 60 * 1000,
max: () => getRateLimiterConfig().readLimit,
hook: "preValidation",
max: (req) => req.rateLimits.readLimit,
keyGenerator: (req) => req.realIp
};
// POST, PATCH, PUT, DELETE endpoints
export const writeLimit: RateLimitOptions = {
timeWindow: 60 * 1000,
max: () => getRateLimiterConfig().writeLimit,
hook: "preValidation",
max: (req) => req.rateLimits.writeLimit,
keyGenerator: (req) => req.realIp
};
@ -37,42 +38,40 @@ export const writeLimit: RateLimitOptions = {
export const secretsLimit: RateLimitOptions = {
// secrets, folders, secret imports
timeWindow: 60 * 1000,
max: () => getRateLimiterConfig().secretsLimit,
hook: "preValidation",
max: (req) => req.rateLimits.secretsLimit,
keyGenerator: (req) => req.realIp
};
export const authRateLimit: RateLimitOptions = {
timeWindow: 60 * 1000,
max: () => getRateLimiterConfig().authRateLimit,
hook: "preValidation",
max: (req) => req.rateLimits.authRateLimit,
keyGenerator: (req) => req.realIp
};
export const inviteUserRateLimit: RateLimitOptions = {
timeWindow: 60 * 1000,
max: () => getRateLimiterConfig().inviteUserRateLimit,
hook: "preValidation",
max: (req) => req.rateLimits.inviteUserRateLimit,
keyGenerator: (req) => req.realIp
};
export const mfaRateLimit: RateLimitOptions = {
timeWindow: 60 * 1000,
max: () => getRateLimiterConfig().mfaRateLimit,
hook: "preValidation",
max: (req) => req.rateLimits.mfaRateLimit,
keyGenerator: (req) => {
return req.headers.authorization?.split(" ")[1] || req.realIp;
}
};
export const creationLimit: RateLimitOptions = {
// identity, project, org
timeWindow: 60 * 1000,
max: () => getRateLimiterConfig().creationLimit,
keyGenerator: (req) => req.realIp
};
// Public endpoints to avoid brute force attacks
export const publicEndpointLimit: RateLimitOptions = {
// Read Shared Secrets
timeWindow: 60 * 1000,
max: () => getRateLimiterConfig().publicEndpointLimit,
hook: "preValidation",
max: (req) => req.rateLimits.publicEndpointLimit,
keyGenerator: (req) => req.realIp
};

View File

@ -0,0 +1,38 @@
import fp from "fastify-plugin";
import { getRateLimiterConfig } from "@app/ee/services/rate-limit/rate-limit-service";
import { getConfig } from "@app/lib/config/env";
export const injectRateLimits = fp(async (server) => {
server.decorateRequest("rateLimits", null);
server.addHook("onRequest", async (req) => {
const appCfg = getConfig();
const instanceRateLimiterConfig = getRateLimiterConfig();
if (!req.auth?.orgId) {
// for public endpoints, we always use the instance-wide default rate limits
req.rateLimits = instanceRateLimiterConfig;
return;
}
const { rateLimits, customRateLimits } = await server.services.license.getPlan(req.auth.orgId);
if (customRateLimits && !appCfg.isCloud) {
// we do this because for self-hosted/dedicated instances, we want custom rate limits to be based on admin configuration
// note that the syncing of custom rate limit happens on the instanceRateLimiterConfig object
req.rateLimits = instanceRateLimiterConfig;
return;
}
// we're using the null coalescing operator in order to handle outdated licenses
req.rateLimits = {
readLimit: rateLimits?.readLimit ?? instanceRateLimiterConfig.readLimit,
writeLimit: rateLimits?.writeLimit ?? instanceRateLimiterConfig.writeLimit,
secretsLimit: rateLimits?.secretsLimit ?? instanceRateLimiterConfig.secretsLimit,
publicEndpointLimit: instanceRateLimiterConfig.publicEndpointLimit,
authRateLimit: instanceRateLimiterConfig.authRateLimit,
inviteUserRateLimit: instanceRateLimiterConfig.inviteUserRateLimit,
mfaRateLimit: instanceRateLimiterConfig.mfaRateLimit
};
});
});

View File

@ -66,6 +66,7 @@ import { secretSnapshotServiceFactory } from "@app/ee/services/secret-snapshot/s
import { snapshotDALFactory } from "@app/ee/services/secret-snapshot/snapshot-dal";
import { snapshotFolderDALFactory } from "@app/ee/services/secret-snapshot/snapshot-folder-dal";
import { snapshotSecretDALFactory } from "@app/ee/services/secret-snapshot/snapshot-secret-dal";
import { snapshotSecretV2DALFactory } from "@app/ee/services/secret-snapshot/snapshot-secret-v2-dal";
import { trustedIpDALFactory } from "@app/ee/services/trusted-ip/trusted-ip-dal";
import { trustedIpServiceFactory } from "@app/ee/services/trusted-ip/trusted-ip-service";
import { TKeyStoreFactory } from "@app/keystore/keystore";
@ -128,6 +129,7 @@ import { orgDALFactory } from "@app/services/org/org-dal";
import { orgRoleDALFactory } from "@app/services/org/org-role-dal";
import { orgRoleServiceFactory } from "@app/services/org/org-role-service";
import { orgServiceFactory } from "@app/services/org/org-service";
import { orgAdminServiceFactory } from "@app/services/org-admin/org-admin-service";
import { orgMembershipDALFactory } from "@app/services/org-membership/org-membership-dal";
import { projectDALFactory } from "@app/services/project/project-dal";
import { projectQueueFactory } from "@app/services/project/project-queue";
@ -160,6 +162,10 @@ import { secretSharingDALFactory } from "@app/services/secret-sharing/secret-sha
import { secretSharingServiceFactory } from "@app/services/secret-sharing/secret-sharing-service";
import { secretTagDALFactory } from "@app/services/secret-tag/secret-tag-dal";
import { secretTagServiceFactory } from "@app/services/secret-tag/secret-tag-service";
import { secretV2BridgeDALFactory } from "@app/services/secret-v2-bridge/secret-v2-bridge-dal";
import { secretV2BridgeServiceFactory } from "@app/services/secret-v2-bridge/secret-v2-bridge-service";
import { secretVersionV2BridgeDALFactory } from "@app/services/secret-v2-bridge/secret-version-dal";
import { secretVersionV2TagBridgeDALFactory } from "@app/services/secret-v2-bridge/secret-version-tag-dal";
import { serviceTokenDALFactory } from "@app/services/service-token/service-token-dal";
import { serviceTokenServiceFactory } from "@app/services/service-token/service-token-service";
import { TSmtpService } from "@app/services/smtp/smtp-service";
@ -178,6 +184,7 @@ import { webhookServiceFactory } from "@app/services/webhook/webhook-service";
import { injectAuditLogInfo } from "../plugins/audit-log";
import { injectIdentity } from "../plugins/auth/inject-identity";
import { injectPermission } from "../plugins/auth/inject-permission";
import { injectRateLimits } from "../plugins/inject-rate-limits";
import { registerSecretScannerGhApp } from "../plugins/secret-scanner";
import { registerV1Routes } from "./v1";
import { registerV2Routes } from "./v2";
@ -229,6 +236,10 @@ export const registerRoutes = async (
const secretVersionTagDAL = secretVersionTagDALFactory(db);
const secretBlindIndexDAL = secretBlindIndexDALFactory(db);
const secretV2BridgeDAL = secretV2BridgeDALFactory(db);
const secretVersionV2BridgeDAL = secretVersionV2BridgeDALFactory(db);
const secretVersionTagV2BridgeDAL = secretVersionV2TagBridgeDALFactory(db);
const integrationDAL = integrationDALFactory(db);
const integrationAuthDAL = integrationAuthDALFactory(db);
const webhookDAL = webhookDALFactory(db);
@ -277,6 +288,7 @@ export const registerRoutes = async (
const secretRotationDAL = secretRotationDALFactory(db);
const snapshotDAL = snapshotDALFactory(db);
const snapshotSecretDAL = snapshotSecretDALFactory(db);
const snapshotSecretV2BridgeDAL = snapshotSecretV2DALFactory(db);
const snapshotFolderDAL = snapshotFolderDALFactory(db);
const gitAppInstallSessionDAL = gitAppInstallSessionDALFactory(db);
@ -316,7 +328,8 @@ export const registerRoutes = async (
kmsDAL,
kmsService,
permissionService,
externalKmsDAL
externalKmsDAL,
licenseService
});
const trustedIpService = trustedIpServiceFactory({
@ -487,6 +500,16 @@ export const registerRoutes = async (
keyStore,
licenseService
});
const orgAdminService = orgAdminServiceFactory({
projectDAL,
permissionService,
projectUserMembershipRoleDAL,
userDAL,
projectBotDAL,
projectKeyDAL,
projectMembershipDAL
});
const rateLimitService = rateLimitServiceFactory({
rateLimitDAL,
licenseService
@ -609,10 +632,8 @@ export const registerRoutes = async (
permissionService,
projectDAL,
projectQueue: projectQueueService,
secretBlindIndexDAL,
identityProjectDAL,
identityOrgMembershipDAL,
projectBotDAL,
projectKeyDAL,
userDAL,
projectEnvDAL,
@ -625,7 +646,9 @@ export const registerRoutes = async (
certificateDAL,
projectUserMembershipRoleDAL,
identityProjectMembershipRoleDAL,
keyStore
keyStore,
kmsService,
projectBotDAL
});
const projectEnvService = projectEnvServiceFactory({
@ -655,7 +678,13 @@ export const registerRoutes = async (
secretVersionDAL,
folderVersionDAL,
secretTagDAL,
secretVersionTagDAL
secretVersionTagDAL,
projectBotService,
kmsService,
secretV2BridgeDAL,
secretVersionV2BridgeDAL,
snapshotSecretV2BridgeDAL,
secretVersionV2TagBridgeDAL: secretVersionTagV2BridgeDAL
});
const webhookService = webhookServiceFactory({
permissionService,
@ -678,8 +707,8 @@ export const registerRoutes = async (
integrationAuthDAL,
integrationDAL,
permissionService,
projectBotDAL,
projectBotService
projectBotService,
kmsService
});
const secretQueueService = secretQueueFactory({
queueService,
@ -699,45 +728,51 @@ export const registerRoutes = async (
secretVersionDAL,
secretBlindIndexDAL,
secretTagDAL,
secretVersionTagDAL
secretVersionTagDAL,
kmsService,
secretVersionV2BridgeDAL,
secretV2BridgeDAL,
secretVersionTagV2BridgeDAL,
secretRotationDAL,
integrationAuthDAL,
snapshotDAL,
snapshotSecretV2BridgeDAL,
secretApprovalRequestDAL
});
const secretImportService = secretImportServiceFactory({
licenseService,
projectBotService,
projectEnvDAL,
folderDAL,
permissionService,
secretImportDAL,
projectDAL,
secretDAL,
secretQueueService
secretQueueService,
secretV2BridgeDAL,
kmsService
});
const secretBlindIndexService = secretBlindIndexServiceFactory({
permissionService,
secretDAL,
secretBlindIndexDAL
});
const secretService = secretServiceFactory({
folderDAL,
secretVersionDAL,
secretVersionTagDAL,
secretBlindIndexDAL,
permissionService,
projectDAL,
secretDAL,
secretTagDAL,
snapshotService,
secretQueueService,
secretImportDAL,
projectEnvDAL,
projectBotService,
secretApprovalPolicyService,
secretApprovalRequestDAL,
secretApprovalRequestSecretDAL
});
const secretSharingService = secretSharingServiceFactory({
const secretV2BridgeService = secretV2BridgeServiceFactory({
folderDAL,
secretVersionDAL: secretVersionV2BridgeDAL,
secretQueueService,
secretDAL: secretV2BridgeDAL,
permissionService,
secretSharingDAL
secretVersionTagDAL: secretVersionTagV2BridgeDAL,
secretTagDAL,
projectEnvDAL,
secretImportDAL,
secretApprovalRequestDAL,
secretApprovalPolicyService,
secretApprovalRequestSecretDAL,
kmsService,
snapshotService
});
const secretApprovalRequestService = secretApprovalRequestServiceFactory({
@ -754,7 +789,41 @@ export const registerRoutes = async (
secretApprovalRequestDAL,
snapshotService,
secretVersionTagDAL,
secretQueueService
secretQueueService,
kmsService,
secretV2BridgeDAL,
secretVersionV2BridgeDAL,
secretVersionTagV2BridgeDAL,
smtpService,
projectEnvDAL,
userDAL
});
const secretService = secretServiceFactory({
folderDAL,
secretVersionDAL,
secretVersionTagDAL,
secretBlindIndexDAL,
permissionService,
projectDAL,
secretDAL,
secretTagDAL,
snapshotService,
secretQueueService,
secretImportDAL,
projectEnvDAL,
projectBotService,
secretApprovalPolicyService,
secretApprovalRequestDAL,
secretApprovalRequestSecretDAL,
secretV2BridgeService,
secretApprovalRequestService
});
const secretSharingService = secretSharingServiceFactory({
permissionService,
secretSharingDAL,
orgDAL
});
const accessApprovalPolicyService = accessApprovalPolicyServiceFactory({
@ -790,11 +859,14 @@ export const registerRoutes = async (
queueService,
folderDAL,
secretApprovalPolicyService,
secretBlindIndexDAL,
secretApprovalRequestDAL,
secretApprovalRequestSecretDAL,
secretQueueService,
projectBotService
projectBotService,
kmsService,
secretV2BridgeDAL,
secretVersionV2TagBridgeDAL: secretVersionTagV2BridgeDAL,
secretVersionV2BridgeDAL
});
const secretRotationQueue = secretRotationQueueFactory({
telemetryService,
@ -802,7 +874,10 @@ export const registerRoutes = async (
queue: queueService,
secretDAL,
secretVersionDAL,
projectBotService
projectBotService,
secretVersionV2BridgeDAL,
secretV2BridgeDAL,
kmsService
});
const secretRotationService = secretRotationServiceFactory({
@ -812,7 +887,9 @@ export const registerRoutes = async (
projectDAL,
licenseService,
secretDAL,
folderDAL
folderDAL,
projectBotService,
secretV2BridgeDAL
});
const integrationService = integrationServiceFactory({
@ -820,8 +897,15 @@ export const registerRoutes = async (
folderDAL,
integrationDAL,
integrationAuthDAL,
secretQueueService
secretQueueService,
integrationAuthService,
projectBotService,
secretV2BridgeDAL,
secretImportDAL,
secretDAL,
kmsService
});
const serviceTokenService = serviceTokenServiceFactory({
projectEnvDAL,
serviceTokenDAL,
@ -952,7 +1036,8 @@ export const registerRoutes = async (
secretFolderVersionDAL: folderVersionDAL,
snapshotDAL,
identityAccessTokenDAL,
secretSharingDAL
secretSharingDAL,
secretVersionV2DAL: secretVersionV2BridgeDAL
});
const oidcService = oidcConfigServiceFactory({
@ -1047,7 +1132,8 @@ export const registerRoutes = async (
identityProjectAdditionalPrivilege: identityProjectAdditionalPrivilegeService,
secretSharing: secretSharingService,
userEngagement: userEngagementService,
externalKms: externalKmsService
externalKms: externalKmsService,
orgAdmin: orgAdminService
});
const cronJobs: CronJob[] = [];
@ -1064,6 +1150,7 @@ export const registerRoutes = async (
await server.register(injectIdentity, { userDAL, serviceTokenDAL });
await server.register(injectPermission);
await server.register(injectRateLimits);
await server.register(injectAuditLogInfo);
server.route({

View File

@ -5,6 +5,7 @@ import {
IdentityProjectAdditionalPrivilegeSchema,
IntegrationAuthsSchema,
ProjectRolesSchema,
ProjectsSchema,
SecretApprovalPoliciesSchema,
UsersSchema
} from "@app/db/schemas";
@ -62,8 +63,14 @@ export const secretRawSchema = z.object({
version: z.number(),
type: z.string(),
secretKey: z.string(),
secretValue: z.string(),
secretComment: z.string().optional()
secretValue: z.string().optional(),
secretComment: z.string().optional(),
secretReminderNote: z.string().nullable().optional(),
secretReminderRepeatDays: z.number().nullable().optional(),
skipMultilineEncoding: z.boolean().default(false).nullable().optional(),
metadata: z.unknown().nullable().optional(),
createdAt: z.date(),
updatedAt: z.date()
});
export const ProjectPermissionSchema = z.object({
@ -135,3 +142,18 @@ export const SanitizedAuditLogStreamSchema = z.object({
createdAt: z.date(),
updatedAt: z.date()
});
export const SanitizedProjectSchema = ProjectsSchema.pick({
id: true,
name: true,
slug: true,
autoCapitalization: true,
orgId: true,
createdAt: true,
updatedAt: true,
version: true,
upgradeStatus: true,
pitVersionLimit: true,
kmsCertificateKeyId: true,
auditLogsRetentionDays: true
});

View File

@ -337,7 +337,7 @@ export const registerCaRouter = async (server: FastifyZodProvider) => {
caId: z.string().trim().describe(CERTIFICATE_AUTHORITIES.SIGN_INTERMEDIATE.caId)
}),
body: z.object({
csr: z.string().trim().describe(CERTIFICATE_AUTHORITIES.SIGN_INTERMEDIATE.csr),
csr: z.string().trim().min(1).describe(CERTIFICATE_AUTHORITIES.SIGN_INTERMEDIATE.csr),
notBefore: validateCaDateField.optional().describe(CERTIFICATE_AUTHORITIES.SIGN_INTERMEDIATE.notBefore),
notAfter: validateCaDateField.describe(CERTIFICATE_AUTHORITIES.SIGN_INTERMEDIATE.notAfter),
maxPathLength: z.number().min(-1).default(-1).describe(CERTIFICATE_AUTHORITIES.SIGN_INTERMEDIATE.maxPathLength)
@ -453,7 +453,7 @@ export const registerCaRouter = async (server: FastifyZodProvider) => {
}),
body: z
.object({
friendlyName: z.string().optional().describe(CERTIFICATE_AUTHORITIES.ISSUE_CERT.friendlyName),
friendlyName: z.string().trim().optional().describe(CERTIFICATE_AUTHORITIES.ISSUE_CERT.friendlyName),
commonName: z.string().trim().min(1).describe(CERTIFICATE_AUTHORITIES.ISSUE_CERT.commonName),
altNames: validateAltNamesField.describe(CERTIFICATE_AUTHORITIES.ISSUE_CERT.altNames),
ttl: z
@ -516,4 +516,81 @@ export const registerCaRouter = async (server: FastifyZodProvider) => {
};
}
});
server.route({
method: "POST",
url: "/:caId/sign-certificate",
config: {
rateLimit: writeLimit
},
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
schema: {
description: "Sign certificate from CA",
params: z.object({
caId: z.string().trim().describe(CERTIFICATE_AUTHORITIES.SIGN_CERT.caId)
}),
body: z
.object({
csr: z.string().trim().min(1).describe(CERTIFICATE_AUTHORITIES.SIGN_CERT.csr),
friendlyName: z.string().trim().optional().describe(CERTIFICATE_AUTHORITIES.SIGN_CERT.friendlyName),
commonName: z.string().trim().min(1).optional().describe(CERTIFICATE_AUTHORITIES.SIGN_CERT.commonName),
altNames: validateAltNamesField.describe(CERTIFICATE_AUTHORITIES.SIGN_CERT.altNames),
ttl: z
.string()
.refine((val) => ms(val) > 0, "TTL must be a positive number")
.describe(CERTIFICATE_AUTHORITIES.SIGN_CERT.ttl),
notBefore: validateCaDateField.optional().describe(CERTIFICATE_AUTHORITIES.SIGN_CERT.notBefore),
notAfter: validateCaDateField.optional().describe(CERTIFICATE_AUTHORITIES.SIGN_CERT.notAfter)
})
.refine(
(data) => {
const { ttl, notAfter } = data;
return (ttl !== undefined && notAfter === undefined) || (ttl === undefined && notAfter !== undefined);
},
{
message: "Either ttl or notAfter must be present, but not both",
path: ["ttl", "notAfter"]
}
),
response: {
200: z.object({
certificate: z.string().trim().describe(CERTIFICATE_AUTHORITIES.SIGN_CERT.certificate),
issuingCaCertificate: z.string().trim().describe(CERTIFICATE_AUTHORITIES.ISSUE_CERT.issuingCaCertificate),
certificateChain: z.string().trim().describe(CERTIFICATE_AUTHORITIES.ISSUE_CERT.certificateChain),
serialNumber: z.string().trim().describe(CERTIFICATE_AUTHORITIES.ISSUE_CERT.serialNumber)
})
}
},
handler: async (req) => {
const { certificate, certificateChain, issuingCaCertificate, serialNumber, ca } =
await server.services.certificateAuthority.signCertFromCa({
caId: req.params.caId,
actor: req.permission.type,
actorId: req.permission.id,
actorAuthMethod: req.permission.authMethod,
actorOrgId: req.permission.orgId,
...req.body
});
await server.services.auditLog.createAuditLog({
...req.auditLogInfo,
projectId: ca.projectId,
event: {
type: EventType.SIGN_CERT,
metadata: {
caId: ca.id,
dn: ca.dn,
serialNumber
}
}
});
return {
certificate,
certificateChain,
issuingCaCertificate,
serialNumber
};
}
});
};

View File

@ -1,26 +1,22 @@
import { z } from "zod";
import {
IdentitiesSchema,
IdentityOrgMembershipsSchema,
OrgMembershipRole,
OrgRolesSchema,
ProjectsSchema
} from "@app/db/schemas";
import { IdentitiesSchema, IdentityOrgMembershipsSchema, OrgMembershipRole, OrgRolesSchema } from "@app/db/schemas";
import { EventType } from "@app/ee/services/audit-log/audit-log-types";
import { IDENTITIES } from "@app/lib/api-docs";
import { creationLimit, readLimit, writeLimit } from "@app/server/config/rateLimiter";
import { readLimit, writeLimit } from "@app/server/config/rateLimiter";
import { getTelemetryDistinctId } from "@app/server/lib/telemetry";
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
import { AuthMode } from "@app/services/auth/auth-type";
import { PostHogEventTypes } from "@app/services/telemetry/telemetry-types";
import { SanitizedProjectSchema } from "../sanitizedSchemas";
export const registerIdentityRouter = async (server: FastifyZodProvider) => {
server.route({
method: "POST",
url: "/",
config: {
rateLimit: creationLimit
rateLimit: writeLimit
},
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
schema: {
@ -307,7 +303,7 @@ export const registerIdentityRouter = async (server: FastifyZodProvider) => {
})
),
identity: IdentitiesSchema.pick({ name: true, id: true, authMethod: true }),
project: ProjectsSchema.pick({ name: true, id: true })
project: SanitizedProjectSchema.pick({ name: true, id: true })
})
)
})

View File

@ -15,6 +15,7 @@ import { registerIdentityUaRouter } from "./identity-universal-auth-router";
import { registerIntegrationAuthRouter } from "./integration-auth-router";
import { registerIntegrationRouter } from "./integration-router";
import { registerInviteOrgRouter } from "./invite-org-router";
import { registerOrgAdminRouter } from "./org-admin-router";
import { registerOrgRouter } from "./organization-router";
import { registerPasswordRouter } from "./password-router";
import { registerProjectEnvRouter } from "./project-env-router";
@ -50,6 +51,7 @@ export const registerV1Routes = async (server: FastifyZodProvider) => {
await server.register(registerPasswordRouter, { prefix: "/password" });
await server.register(registerOrgRouter, { prefix: "/organization" });
await server.register(registerAdminRouter, { prefix: "/admin" });
await server.register(registerOrgAdminRouter, { prefix: "/organization-admin" });
await server.register(registerUserRouter, { prefix: "/user" });
await server.register(registerInviteOrgRouter, { prefix: "/invite-org" });
await server.register(registerUserActionRouter, { prefix: "/user-action" });

View File

@ -170,6 +170,12 @@ export const registerIntegrationRouter = async (server: FastifyZodProvider) => {
params: z.object({
integrationId: z.string().trim().describe(INTEGRATION.DELETE.integrationId)
}),
querystring: z.object({
shouldDeleteIntegrationSecrets: z
.enum(["true", "false"])
.optional()
.transform((val) => val === "true")
}),
response: {
200: z.object({
integration: IntegrationsSchema
@ -183,7 +189,8 @@ export const registerIntegrationRouter = async (server: FastifyZodProvider) => {
actorAuthMethod: req.permission.authMethod,
actor: req.permission.type,
actorOrgId: req.permission.orgId,
id: req.params.integrationId
id: req.params.integrationId,
shouldDeleteIntegrationSecrets: req.query.shouldDeleteIntegrationSecrets
});
await server.services.auditLog.createAuditLog({
@ -205,7 +212,8 @@ export const registerIntegrationRouter = async (server: FastifyZodProvider) => {
targetService: integration.targetService,
targetServiceId: integration.targetServiceId,
path: integration.path,
region: integration.region
region: integration.region,
shouldDeleteIntegrationSecrets: req.query.shouldDeleteIntegrationSecrets
// eslint-disable-next-line
}) as any
}

View File

@ -0,0 +1,90 @@
import { z } from "zod";
import { ProjectMembershipsSchema } from "@app/db/schemas";
import { EventType } from "@app/ee/services/audit-log/audit-log-types";
import { readLimit } from "@app/server/config/rateLimiter";
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
import { AuthMode } from "@app/services/auth/auth-type";
import { SanitizedProjectSchema } from "../sanitizedSchemas";
export const registerOrgAdminRouter = async (server: FastifyZodProvider) => {
server.route({
method: "GET",
url: "/projects",
config: {
rateLimit: readLimit
},
schema: {
querystring: z.object({
search: z.string().optional(),
offset: z.coerce.number().default(0),
limit: z.coerce.number().max(100).default(50)
}),
response: {
200: z.object({
projects: SanitizedProjectSchema.array(),
count: z.coerce.number()
})
}
},
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
handler: async (req) => {
const { projects, count } = await server.services.orgAdmin.listOrgProjects({
limit: req.query.limit,
offset: req.query.offset,
search: req.query.search,
actorOrgId: req.permission.orgId,
actorAuthMethod: req.permission.authMethod,
actorId: req.permission.id,
actor: req.permission.type
});
return { projects, count };
}
});
server.route({
method: "POST",
url: "/projects/:projectId/grant-admin-access",
config: {
rateLimit: readLimit
},
schema: {
params: z.object({
projectId: z.string()
}),
response: {
200: z.object({
membership: ProjectMembershipsSchema
})
}
},
onRequest: verifyAuth([AuthMode.JWT]),
handler: async (req) => {
const { membership } = await server.services.orgAdmin.grantProjectAdminAccess({
actorOrgId: req.permission.orgId,
actorAuthMethod: req.permission.authMethod,
actorId: req.permission.id,
actor: req.permission.type,
projectId: req.params.projectId
});
if (req.auth.authMode === AuthMode.JWT) {
await server.services.auditLog.createAuditLog({
...req.auditLogInfo,
projectId: req.params.projectId,
event: {
type: EventType.ORG_ADMIN_ACCESS_PROJECT,
metadata: {
projectId: req.params.projectId,
username: req.auth.user.username,
email: req.auth.user.email || "",
userId: req.auth.userId
}
}
});
}
return { membership };
}
});
};

View File

@ -9,6 +9,55 @@ import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
import { AuthMode } from "@app/services/auth/auth-type";
export const registerProjectEnvRouter = async (server: FastifyZodProvider) => {
server.route({
method: "GET",
url: "/:workspaceId/environments/:envId",
config: {
rateLimit: writeLimit
},
schema: {
description: "Get Environment",
security: [
{
bearerAuth: []
}
],
params: z.object({
workspaceId: z.string().trim().describe(ENVIRONMENTS.GET.workspaceId),
envId: z.string().trim().describe(ENVIRONMENTS.GET.id)
}),
response: {
200: z.object({
environment: ProjectEnvironmentsSchema
})
}
},
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
handler: async (req) => {
const environment = await server.services.projectEnv.getEnvironmentById({
actorId: req.permission.id,
actor: req.permission.type,
actorOrgId: req.permission.orgId,
actorAuthMethod: req.permission.authMethod,
projectId: req.params.workspaceId,
id: req.params.envId
});
await server.services.auditLog.createAuditLog({
...req.auditLogInfo,
projectId: environment.projectId,
event: {
type: EventType.GET_ENVIRONMENT,
metadata: {
id: environment.id
}
}
});
return { environment };
}
});
server.route({
method: "POST",
url: "/:workspaceId/environments",

View File

@ -1,22 +1,16 @@
import { z } from "zod";
import {
IntegrationsSchema,
ProjectMembershipsSchema,
ProjectsSchema,
UserEncryptionKeysSchema,
UsersSchema
} from "@app/db/schemas";
import { IntegrationsSchema, ProjectMembershipsSchema, UserEncryptionKeysSchema, UsersSchema } from "@app/db/schemas";
import { PROJECTS } from "@app/lib/api-docs";
import { readLimit, writeLimit } from "@app/server/config/rateLimiter";
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
import { AuthMode } from "@app/services/auth/auth-type";
import { ProjectFilterType } from "@app/services/project/project-types";
import { integrationAuthPubSchema } from "../sanitizedSchemas";
import { integrationAuthPubSchema, SanitizedProjectSchema } from "../sanitizedSchemas";
import { sanitizedServiceTokenSchema } from "../v2/service-token-router";
const projectWithEnv = ProjectsSchema.merge(
const projectWithEnv = SanitizedProjectSchema.merge(
z.object({
_id: z.string(),
environments: z.object({ name: z.string(), slug: z.string(), id: z.string() }).array()
@ -78,7 +72,7 @@ export const registerProjectRouter = async (server: FastifyZodProvider) => {
lastName: true,
id: true
}).merge(UserEncryptionKeysSchema.pick({ publicKey: true })),
project: ProjectsSchema.pick({ name: true, id: true }),
project: SanitizedProjectSchema.pick({ name: true, id: true }),
roles: z.array(
z.object({
id: z.string(),
@ -188,7 +182,7 @@ export const registerProjectRouter = async (server: FastifyZodProvider) => {
}),
response: {
200: z.object({
workspace: ProjectsSchema.optional()
workspace: SanitizedProjectSchema.optional()
})
}
},
@ -224,7 +218,7 @@ export const registerProjectRouter = async (server: FastifyZodProvider) => {
response: {
200: z.object({
message: z.string(),
workspace: ProjectsSchema
workspace: SanitizedProjectSchema
})
}
},
@ -272,7 +266,7 @@ export const registerProjectRouter = async (server: FastifyZodProvider) => {
}),
response: {
200: z.object({
workspace: ProjectsSchema
workspace: SanitizedProjectSchema
})
}
},
@ -314,7 +308,7 @@ export const registerProjectRouter = async (server: FastifyZodProvider) => {
response: {
200: z.object({
message: z.string(),
workspace: ProjectsSchema
workspace: SanitizedProjectSchema
})
}
},
@ -351,7 +345,7 @@ export const registerProjectRouter = async (server: FastifyZodProvider) => {
response: {
200: z.object({
message: z.string(),
workspace: ProjectsSchema
workspace: SanitizedProjectSchema
})
}
},
@ -389,7 +383,7 @@ export const registerProjectRouter = async (server: FastifyZodProvider) => {
response: {
200: z.object({
message: z.string(),
workspace: ProjectsSchema
workspace: SanitizedProjectSchema
})
}
},

View File

@ -8,6 +8,8 @@ import { readLimit, secretsLimit } from "@app/server/config/rateLimiter";
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
import { AuthMode } from "@app/services/auth/auth-type";
import { secretRawSchema } from "../sanitizedSchemas";
export const registerSecretImportRouter = async (server: FastifyZodProvider) => {
server.route({
method: "POST",
@ -353,4 +355,48 @@ export const registerSecretImportRouter = async (server: FastifyZodProvider) =>
return { secrets: importedSecrets };
}
});
server.route({
url: "/secrets/raw",
method: "GET",
config: {
rateLimit: secretsLimit
},
schema: {
querystring: z.object({
workspaceId: z.string().trim(),
environment: z.string().trim(),
path: z.string().trim().default("/").transform(removeTrailingSlash)
}),
response: {
200: z.object({
secrets: z
.object({
secretPath: z.string(),
environment: z.string(),
environmentInfo: z.object({
id: z.string(),
name: z.string(),
slug: z.string()
}),
folderId: z.string().optional(),
secrets: secretRawSchema.array()
})
.array()
})
}
},
onRequest: verifyAuth([AuthMode.JWT, AuthMode.API_KEY, AuthMode.SERVICE_TOKEN, AuthMode.IDENTITY_ACCESS_TOKEN]),
handler: async (req) => {
const importedSecrets = await server.services.secretImport.getRawSecretsFromImports({
actorId: req.permission.id,
actor: req.permission.type,
actorAuthMethod: req.permission.authMethod,
actorOrgId: req.permission.orgId,
...req.query,
projectId: req.query.workspaceId
});
return { secrets: importedSecrets };
}
});
};

View File

@ -1,6 +1,7 @@
import { z } from "zod";
import { SecretSharingSchema } from "@app/db/schemas";
import { SecretSharingAccessType } from "@app/lib/types";
import {
publicEndpointLimit,
publicSecretShareCreationLimit,
@ -18,21 +19,31 @@ export const registerSecretSharingRouter = async (server: FastifyZodProvider) =>
rateLimit: readLimit
},
schema: {
querystring: z.object({
offset: z.coerce.number().min(0).max(100).default(0),
limit: z.coerce.number().min(1).max(100).default(25)
}),
response: {
200: z.array(SecretSharingSchema)
200: z.object({
secrets: z.array(SecretSharingSchema),
totalCount: z.number()
})
}
},
onRequest: verifyAuth([AuthMode.JWT]),
handler: async (req) => {
const sharedSecrets = await req.server.services.secretSharing.getSharedSecrets({
const { secrets, totalCount } = await req.server.services.secretSharing.getSharedSecrets({
actor: req.permission.type,
actorId: req.permission.id,
orgId: req.permission.orgId,
actorAuthMethod: req.permission.authMethod,
actorOrgId: req.permission.orgId
actorOrgId: req.permission.orgId,
...req.query
});
return sharedSecrets;
return {
secrets,
totalCount
};
}
});
@ -47,7 +58,7 @@ export const registerSecretSharingRouter = async (server: FastifyZodProvider) =>
id: z.string().uuid()
}),
querystring: z.object({
hashedHex: z.string()
hashedHex: z.string().min(1)
}),
response: {
200: SecretSharingSchema.pick({
@ -55,22 +66,28 @@ export const registerSecretSharingRouter = async (server: FastifyZodProvider) =>
iv: true,
tag: true,
expiresAt: true,
expiresAfterViews: true
expiresAfterViews: true,
accessType: true
}).extend({
orgName: z.string().optional()
})
}
},
handler: async (req) => {
const sharedSecret = await req.server.services.secretSharing.getActiveSharedSecretByIdAndHashedHex(
req.params.id,
req.query.hashedHex
);
const sharedSecret = await req.server.services.secretSharing.getActiveSharedSecretById({
sharedSecretId: req.params.id,
hashedHex: req.query.hashedHex,
orgId: req.permission?.orgId
});
if (!sharedSecret) return undefined;
return {
encryptedValue: sharedSecret.encryptedValue,
iv: sharedSecret.iv,
tag: sharedSecret.tag,
expiresAt: sharedSecret.expiresAt,
expiresAfterViews: sharedSecret.expiresAfterViews
expiresAfterViews: sharedSecret.expiresAfterViews,
accessType: sharedSecret.accessType,
orgName: sharedSecret.orgName
};
}
});
@ -84,11 +101,11 @@ export const registerSecretSharingRouter = async (server: FastifyZodProvider) =>
schema: {
body: z.object({
encryptedValue: z.string(),
hashedHex: z.string(),
iv: z.string(),
tag: z.string(),
hashedHex: z.string(),
expiresAt: z.string(),
expiresAfterViews: z.number()
expiresAfterViews: z.number().min(1).optional()
}),
response: {
200: z.object({
@ -97,14 +114,9 @@ export const registerSecretSharingRouter = async (server: FastifyZodProvider) =>
}
},
handler: async (req) => {
const { encryptedValue, iv, tag, hashedHex, expiresAt, expiresAfterViews } = req.body;
const sharedSecret = await req.server.services.secretSharing.createPublicSharedSecret({
encryptedValue,
iv,
tag,
hashedHex,
expiresAt: new Date(expiresAt),
expiresAfterViews
...req.body,
accessType: SecretSharingAccessType.Anyone
});
return { id: sharedSecret.id };
}
@ -118,12 +130,14 @@ export const registerSecretSharingRouter = async (server: FastifyZodProvider) =>
},
schema: {
body: z.object({
name: z.string().max(50).optional(),
encryptedValue: z.string(),
hashedHex: z.string(),
iv: z.string(),
tag: z.string(),
hashedHex: z.string(),
expiresAt: z.string(),
expiresAfterViews: z.number()
expiresAfterViews: z.number().min(1).optional(),
accessType: z.nativeEnum(SecretSharingAccessType).default(SecretSharingAccessType.Organization)
}),
response: {
200: z.object({
@ -133,19 +147,13 @@ export const registerSecretSharingRouter = async (server: FastifyZodProvider) =>
},
onRequest: verifyAuth([AuthMode.JWT]),
handler: async (req) => {
const { encryptedValue, iv, tag, hashedHex, expiresAt, expiresAfterViews } = req.body;
const sharedSecret = await req.server.services.secretSharing.createSharedSecret({
actor: req.permission.type,
actorId: req.permission.id,
orgId: req.permission.orgId,
actorAuthMethod: req.permission.authMethod,
actorOrgId: req.permission.orgId,
encryptedValue,
iv,
tag,
hashedHex,
expiresAt: new Date(expiresAt),
expiresAfterViews
...req.body
});
return { id: sharedSecret.id };
}

View File

@ -8,25 +8,24 @@ import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
import { AuthMode } from "@app/services/auth/auth-type";
import { WebhookType } from "@app/services/webhook/webhook-types";
export const sanitizedWebhookSchema = WebhooksSchema.omit({
encryptedSecretKey: true,
iv: true,
tag: true,
algorithm: true,
keyEncoding: true,
urlCipherText: true,
urlIV: true,
urlTag: true
}).merge(
z.object({
projectId: z.string(),
environment: z.object({
id: z.string(),
name: z.string(),
slug: z.string()
})
export const sanitizedWebhookSchema = WebhooksSchema.pick({
id: true,
secretPath: true,
lastStatus: true,
lastRunErrorMessage: true,
isDisabled: true,
createdAt: true,
updatedAt: true,
envId: true,
type: true
}).extend({
projectId: z.string(),
environment: z.object({
id: z.string(),
name: z.string(),
slug: z.string()
})
);
});
export const registerWebhookRouter = async (server: FastifyZodProvider) => {
server.route({
@ -228,7 +227,7 @@ export const registerWebhookRouter = async (server: FastifyZodProvider) => {
response: {
200: z.object({
message: z.string(),
webhooks: sanitizedWebhookSchema.array()
webhooks: sanitizedWebhookSchema.extend({ url: z.string() }).array()
})
}
},

View File

@ -5,7 +5,6 @@ import {
IdentitiesSchema,
IdentityProjectMembershipsSchema,
ProjectMembershipRole,
ProjectsSchema,
ProjectUserMembershipRolesSchema
} from "@app/db/schemas";
import { PROJECT_IDENTITIES } from "@app/lib/api-docs";
@ -15,6 +14,8 @@ import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
import { AuthMode } from "@app/services/auth/auth-type";
import { ProjectUserMembershipTemporaryMode } from "@app/services/project-membership/project-membership-types";
import { SanitizedProjectSchema } from "../sanitizedSchemas";
export const registerIdentityProjectRouter = async (server: FastifyZodProvider) => {
server.route({
method: "POST",
@ -236,7 +237,7 @@ export const registerIdentityProjectRouter = async (server: FastifyZodProvider)
})
),
identity: IdentitiesSchema.pick({ name: true, id: true, authMethod: true }),
project: ProjectsSchema.pick({ name: true, id: true })
project: SanitizedProjectSchema.pick({ name: true, id: true })
})
.array()
})
@ -294,7 +295,7 @@ export const registerIdentityProjectRouter = async (server: FastifyZodProvider)
})
),
identity: IdentitiesSchema.pick({ name: true, id: true, authMethod: true }),
project: ProjectsSchema.pick({ name: true, id: true })
project: SanitizedProjectSchema.pick({ name: true, id: true })
})
})
}

View File

@ -9,7 +9,7 @@ import {
UsersSchema
} from "@app/db/schemas";
import { ORGANIZATIONS } from "@app/lib/api-docs";
import { creationLimit, readLimit, writeLimit } from "@app/server/config/rateLimiter";
import { readLimit, writeLimit } from "@app/server/config/rateLimiter";
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
import { ActorType, AuthMode } from "@app/services/auth/auth-type";
@ -307,7 +307,7 @@ export const registerOrgRouter = async (server: FastifyZodProvider) => {
method: "POST",
url: "/",
config: {
rateLimit: creationLimit
rateLimit: writeLimit
},
schema: {
body: z.object({

Some files were not shown because too many files have changed in this diff Show More