Compare commits

..

197 Commits

Author SHA1 Message Date
af4428acec Add external parent ca support to docs 2024-08-20 22:43:29 -07:00
61370cc6b2 Finish allow installing intermediate CA with external parent CA 2024-08-20 21:44:41 -07:00
7b4b802a9b Merge pull request #2308 from Infisical/daniel/sdk-docs-updates
Fix: Include imports SDK docs
2024-08-20 01:21:28 +04:00
95cf3cf6cc Docs: Add expand secret references to single secret sdk docs 2024-08-20 01:12:42 +04:00
d021b414cf Fix: Include imports SDK docs 2024-08-20 01:09:53 +04:00
bed75c36dd Merge pull request #2291 from Infisical/feature/certificate-template
feat: certificate templates
2024-08-19 11:48:03 -07:00
04cb499f0f doc: finalized sample request response values 2024-08-19 16:53:17 +08:00
189a610f52 doc: add cert template api usage 2024-08-19 16:40:18 +08:00
00039ba0e4 misc: addressed PR feedback regarding audit logs and endpoint structure 2024-08-19 16:15:43 +08:00
abdcb95a8f Merge remote-tracking branch 'origin/main' into feature/certificate-template 2024-08-19 14:51:42 +08:00
47ea4ae9a6 Fix merge conflicts 2024-08-18 12:05:15 -07:00
903b2c3dc6 Merge pull request #2303 from Infisical/handbook-update
added talking-to-customers.mdx to handbook
2024-08-18 12:55:20 -04:00
c795b3b3a0 added talking-to-customers.mdx to handbook 2024-08-17 22:49:53 -07:00
0d8ff1828e Merge pull request #2266 from Infisical/certificate-alerting
Alerting System for expiring CA + Certificates
2024-08-17 22:45:24 -07:00
30d6af7760 Make PR review adjustments 2024-08-17 21:23:25 -07:00
44b42359da Merge remote-tracking branch 'origin' into certificate-alerting 2024-08-17 19:47:37 -07:00
38373722e3 Merge remote-tracking branch 'origin' into certificate-alerting 2024-08-17 19:46:23 -07:00
7ec68ca9a1 Update expiry badge display for certs 2024-08-17 19:44:47 -07:00
a49d5b121b Merge pull request #2299 from akhilmhdh/fix/#2288
fix: resolved add all members to project failing when there is pending users in organization
2024-08-16 20:52:43 -04:00
901ff7a605 Merge pull request #2300 from akhilmhdh/feat/license-check-approval-api
feat: license check in secret approval api level
2024-08-16 14:07:03 -04:00
ba4aa15c92 doc: added platform docs for certificate template 2024-08-17 01:04:29 +08:00
=
a00103aa1e feat: license check in secret approval api level 2024-08-16 22:34:10 +05:30
0c17cc3577 doc: API references 2024-08-17 00:15:11 +08:00
51d84a47b9 misc: added certificate templates to permissions 2024-08-16 23:30:29 +08:00
d529670a52 misc: addressed failing github actions 2024-08-16 23:14:45 +08:00
ed0463e3e4 misc: added audit logs for certificate template 2024-08-16 23:08:01 +08:00
20db0a255c Merge pull request #2239 from Infisical/ca-renewal
CA Renewal (Same Key Pair)
2024-08-16 10:37:44 -04:00
6fe1d77375 misc: added descriptive tooltips 2024-08-16 21:09:35 +08:00
f90855e7a5 misc: add tracking of certificate template ID 2024-08-16 20:17:52 +08:00
97f5c33aea feat: added collection selection for cert template 2024-08-16 19:40:23 +08:00
=
34c2200269 fix: resolved add all members to project failing when there is pending users 2024-08-16 16:20:38 +05:30
69925721cc Merge branch 'certificate-alerting' into feature/certificate-template 2024-08-16 18:42:12 +08:00
0961d2f1c6 feat: subject alternative name policy enforcement 2024-08-16 17:24:33 +08:00
b9bd518aa6 feat: initial enforcement of template policy 2024-08-16 16:32:47 +08:00
692c9b5d9c Merge pull request #2297 from dthree/patch-1
fix: added missing word
2024-08-16 00:24:05 -04:00
DC
32046ca880 fix: added missing word 2024-08-15 20:17:40 -07:00
590dbbcb04 Merge pull request #2296 from Infisical/maidul-iqdgqwuygd
Add DISABLE_AUDIT_LOG_GENERATION
2024-08-15 22:44:08 -04:00
27d2af4979 Add DISABLE_AUDIT_LOG_GENERATION
Added `DISABLE_AUDIT_LOG_GENERATION` which when set to true will prevent the creation of audit logs in Infisical.

This will be used for load testing purposes and help verify if audit logs are a bottle neck for performance
2024-08-15 22:39:49 -04:00
a1e6c6f7d5 Merge pull request #2295 from akhilmhdh/feat/replication-test
fix: switched sync integration to have redis lock
2024-08-15 15:18:42 -04:00
=
cc94a3366a feat: made requested changes for integration sync 2024-08-15 23:38:16 +05:30
6a6c084b8a Add description to PKI collection 2024-08-15 11:03:58 -07:00
7baa3b4cbe Add PKI collection to issue cert modal 2024-08-15 10:41:37 -07:00
=
6cab7504fc fix: switched sync integration to have redis lock 2024-08-15 22:04:32 +05:30
ca3d8c5594 Bring PR up to speed with ca renewal changes 2024-08-15 09:31:21 -07:00
28a2a6c41a feat: initial integration of cert template management 2024-08-15 21:06:37 +08:00
05efd95472 feat: completed certificate template schema endpoints 2024-08-15 15:46:35 +08:00
fa31f87479 Merge pull request #2292 from Infisical/doc/add-dynamic-secrets-to-api-reference
doc: add dynamic secrets to api references
2024-08-15 15:37:42 +08:00
b176f13392 doc: add dynamic secrets to api references 2024-08-15 15:21:49 +08:00
f4384bb01e feat: initial structure 2024-08-15 14:49:30 +08:00
856c2423be Update cert structure to ref correct ca cert in cert chain retrieval 2024-08-14 15:43:49 -07:00
4570de09ae Merge pull request #2290 from akhilmhdh/feat/replication-test
feat: resolved getSecretByName empty value from imported in kms arch
2024-08-14 16:05:01 -04:00
=
4feff5b4ca feat: resolved getSecretByName empty value from imported in kms arch 2024-08-15 01:24:54 +05:30
6081e2927e Merge pull request #2280 from rhythmbhiwani/fix-pagination-disappear
Fixed Pagination Disappearing on Secret Sharing Page
2024-08-14 14:54:37 -04:00
0b42f29916 Merge pull request #2289 from akhilmhdh/feat/replication-test
feat: added log point for aws tag and check for delete secret in bridge
2024-08-14 12:25:28 -04:00
=
b60d0992f4 feat: added log point for aws tag and check for delete secret in bridge 2024-08-14 21:42:07 +05:30
a8a68f600c Merge pull request #2287 from akhilmhdh/feat/replication-test
feat(ui): resolved a race condition in ui
2024-08-13 14:48:08 -04:00
=
742f5f6621 feat(ui): resolved a race condition in ui 2024-08-14 00:13:55 +05:30
f993e4aa5c Clear type check issue 2024-08-13 11:31:57 -07:00
bb6416acb7 Update CA certificate tracking impl to use foreign ref instead of number 2024-08-13 11:22:45 -07:00
f3cd7efe0e Merge pull request #2285 from akhilmhdh/feat/replication-test
feat: added more endpoints for delete
2024-08-13 12:41:54 -04:00
2b16c19b70 improve logs for aws ssm debug 2024-08-13 12:40:02 -04:00
=
943b540383 feat: added more endpoints for delete 2024-08-13 21:48:03 +05:30
e180021aa6 Merge pull request #2283 from akhilmhdh/feat/replication-test
feat: added debug points to test ssm integration in replication
2024-08-13 11:23:25 -04:00
=
8e08c443ad feat: added log to print operation based keys 2024-08-13 20:50:19 +05:30
=
dae26daeeb feat: added debug points to test ssm integration in replication 2024-08-13 20:40:53 +05:30
170f8d9add Merge pull request #2248 from Infisical/misc/addressed-reported-cli-behaviors
misc: addressed reported flaws with CLI usage
2024-08-13 12:49:20 +08:00
8d41ef198a Merge pull request #2282 from akhilmhdh/feat/client-secret-cleanup
fix: resolved secret approval broken due to tag name removal
2024-08-12 16:51:55 -04:00
=
69d60a227a fix: resolved secret approval broken due to tag name removal 2024-08-13 02:16:57 +05:30
c8eefcfbf9 Merge pull request #2281 from akhilmhdh/feat/client-secret-cleanup
feat: switched to ssm update as overwrite with tag as seperate operation
2024-08-12 16:38:57 -04:00
=
53cec754cc feat: switched to ssm update as overwrite with tag as seperate operation 2024-08-13 02:04:55 +05:30
5db3e177eb Fixed Pagination Disappearing on Secret Sharing Page 2024-08-13 02:01:25 +05:30
3fcc3ccff4 fix spending money tpyo 2024-08-12 12:41:15 -04:00
df07d7b6d7 update spending docs 2024-08-12 11:34:32 -04:00
28a655bef1 Merge pull request #2276 from akhilmhdh/feat/client-secret-cleanup
Client secret cleanup on resource cleanup queue
2024-08-12 11:01:46 -04:00
=
5f2cd04f46 feat: removed not needed condition 2024-08-12 20:29:05 +05:30
=
897ce1f267 chore: new reviewable command in root make file to check all the entities lint and type error 2024-08-12 13:19:55 +05:30
=
6afc17b84b feat: implemented universal auth client secret cleanup in resource cleanup queue 2024-08-12 13:19:25 +05:30
9017a5e838 Update spending-money.mdx 2024-08-12 01:29:45 -04:00
cb8e4d884e add equipment details to handbook 2024-08-11 23:17:58 -04:00
16807c3dd6 update k8s helm chart image tag 2024-08-11 13:09:22 -04:00
61791e385c update chart version of k8 2024-08-11 10:34:23 -04:00
bbd7bfb0f5 Merge pull request #2274 from MohamadTahir/fix-operator-bugs
Bug Fixes
2024-08-11 10:33:26 -04:00
4de8c48b2c Merge pull request #2270 from Ayush-Dutt-Sharma/ayush/minor-bug-#2269
replaced "creditnals" to "credentials"
2024-08-11 19:18:43 +05:30
a4bbe2c612 fix the client site url & the creation of new variable instead of updating the previous initiated variable 2024-08-11 16:46:48 +03:00
541a2e7d05 replaced "creditnals" to "credentials" 2024-08-11 14:10:49 +05:30
ea4e51d826 Merge pull request #2268 from Ayush-Dutt-Sharma/ayush/bug-2267-backend
better logging and while loop for ask propmt again
2024-08-10 19:48:05 +05:30
3bc920c593 better logging and while loop for ask propmt again 2024-08-10 15:36:43 +05:30
f4244c6d4d Finish docs for pki alerting + expose endpoints 2024-08-09 19:22:47 -07:00
e1b9965f01 Add frontend audit log ui for pki alerting / collection 2024-08-09 09:28:15 -07:00
705b4f7513 Add audit logging for pki alerts / collections 2024-08-09 09:07:02 -07:00
df38c761ad Merge pull request #2265 from akhilmhdh/fix/migration-switch-batch-insert
Secret migration switched to chunking based batch insert
2024-08-09 11:46:19 -04:00
=
32a84471f2 feat: added a new batch insert operation to convert inserts into chunks and updated secret migration 2024-08-09 21:02:26 +05:30
fc4a20caf2 Rename pki alerting structures 2024-08-09 08:27:53 -07:00
ea14df2cbd Merge pull request #2242 from akhilmhdh/fix/tag-filter-secret-api
Tag based filtering for secret endpoint
2024-08-09 20:33:43 +05:30
6bd6cac366 Merge pull request #2264 from Infisical/misc/addressed-misleading-google-saml-setup
misc: addressed misleading docs and placeholder values for Google SAML
2024-08-09 07:43:56 -07:00
45294253aa Merge pull request #2194 from GLEF1X/bugfix/yaml-exporting
fix(cli): make yaml exporting reliable and standardized
2024-08-09 10:01:26 -04:00
635fbdc80b misc: addressedm misleading docs and placeholder values for google saml 2024-08-09 21:29:33 +08:00
d20c48b7cf Merge pull request #2263 from Ayush-Dutt-Sharma/ayush/document-fixes
kubernetes operators integration doc fix
2024-08-09 14:59:50 +05:30
=
1fc18fe23b feat: added name in attach tag 2024-08-09 14:38:15 +05:30
99403e122b kubernetes operators integration doc fix 2024-08-09 14:33:29 +05:30
5176e70437 rephrase error messages 2024-08-08 18:15:13 -04:00
82b2b0af97 Merge pull request #2255 from akhilmhdh/feat/secret-get-personal
fix: resolved cli failign to get overriden secret in get command
2024-08-08 15:08:39 -04:00
e313c866a2 remove backup test for temp 2024-08-08 14:25:12 -04:00
2d81606049 update test with typo fix 2024-08-08 14:03:52 -04:00
718f4ef129 Merge pull request #2256 from Infisical/maidu-2321e
remove INFISICAL_VAULT_FILE_PASSPHRASE because it is being auto generated now
2024-08-08 13:52:05 -04:00
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
=
809e4eeba1 fix: resolved cli failign to get overriden secret in get command 2024-08-08 21:23:04 +05:30
=
254446c895 fix: resolved keyring dataset too big by keeping only the encryption key 2024-08-08 13:04:33 +05:30
bb52e2beb4 Update secret-tag-router.ts 2024-08-08 00:31:41 -04:00
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
556e4d62c4 Added pki collection table, pki alert modal 2024-08-07 17:09:07 -07: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
1690a9429c Begin cert alerting 2024-08-07 07:08:47 -07:00
=
c109fbab3e feat: removed tag name used in queries 2024-08-07 13:24:22 +05:30
=
15fb01089b feat: name removal in tag respective changes in frontend 2024-08-07 13:15:53 +05:30
=
6f4be3e25a feat: removed name from tag and stricter slugification for tag endpoint 2024-08-07 13:14:39 +05:30
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
14c1b4f07b misc: hide not found text when flag plain is enabled 2024-08-06 21:21:45 +08:00
3028bdd424 misc: made local workspace file not required if using auth token 2024-08-06 21:06:14 +08: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
=
8626bce632 feat: added tag support for secret operation in cli 2024-08-06 15:36:03 +05:30
=
c5a2b0321f feat: completed secret v3 raw to support tag based filtering 2024-08-06 15:35:00 +05:30
5af53d3398 Update/remove comments 2024-08-05 20:19:14 -07:00
8da8c6a66c Block expired CAs from issuing certs 2024-08-05 20:06:17 -07:00
88a4390ea0 Add docs for CA renewal 2024-08-05 19:49:35 -07:00
c70d0a577c Finish preliminary CA renewal with same key pair 2024-08-05 15:43:40 -07:00
1070954bdd misc: used destructuring 2024-08-06 02:05:13 +08:00
587a4a1120 Make progress on ca cert versioning 2024-08-05 11:00:22 -07: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
0f23b7e1d3 misc: added check for undefined orgId 2024-08-03 02:10:47 +08:00
33193a47ae misc: updated default onprem rate limits 2024-08-03 01:52:04 +08:00
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
55a6740714 misc: moved to plan-based rate limit 2024-08-02 21:37:48 +08:00
891cb06de0 Update keyringwrapper.go 2024-07-31 16:55:53 +02:00
02e8f20cbf remove extra : 2024-07-31 03:14:06 +00:00
dbe771dba0 refactor: remove unnecessary comment 2024-07-30 05:30:13 -04:00
273fd6c98f refactor: remove deprecated errors package
- Replace errors.Wrap with fmt.Errorf and %w verb
2024-07-30 05:23:43 -04:00
d5f4ce4376 Update vault.go 2024-07-30 10:22:15 +02:00
18aac6508b fix(cli): make yaml exporting reliable and standardized 2024-07-29 22:38:10 -04:00
85653a90d5 update phrasing 2024-07-29 22:06:03 -04: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
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
ff0e7feeee feat(cli): CLI Keyring improvements 2024-07-26 19:14:21 +02:00
320 changed files with 12601 additions and 1939 deletions

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

@ -15,3 +15,16 @@ up-prod:
down:
docker compose -f docker-compose.dev.yml down
reviewable-ui:
cd frontend && \
npm run lint:fix && \
npm run type:check
reviewable-api:
cd backend && \
npm run lint:fix && \
npm run type:check
reviewable: reviewable-ui reviewable-api

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",
@ -75,6 +76,7 @@
"pino": "^8.16.2",
"posthog-node": "^3.6.2",
"probot": "^13.0.0",
"safe-regex": "^2.1.1",
"smee-client": "^2.0.0",
"tedious": "^18.2.1",
"tweetnacl": "^1.0.3",
@ -106,6 +108,7 @@
"@types/picomatch": "^2.3.3",
"@types/prompt-sync": "^4.2.3",
"@types/resolve": "^1.20.6",
"@types/safe-regex": "^1.1.6",
"@types/uuid": "^9.0.7",
"@typescript-eslint/eslint-plugin": "^6.20.0",
"@typescript-eslint/parser": "^6.20.0",
@ -7812,19 +7815,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": {
@ -9774,6 +9803,12 @@
"integrity": "sha512-A4STmOXPhMUtHH+S6ymgE2GiBSMqf4oTvcQZMcHzokuTLVYzXTB8ttjcgxOVaAp2lGwEdzZ0J+cRbbeevQj1UQ==",
"dev": true
},
"node_modules/@types/safe-regex": {
"version": "1.1.6",
"resolved": "https://registry.npmjs.org/@types/safe-regex/-/safe-regex-1.1.6.tgz",
"integrity": "sha512-CQ/uPB9fLOPKwDsrTeVbNIkwfUthTWOx0l6uIGwVFjZxv7e68pCW5gtTYFzdJi3EBJp8h8zYhJbTasAbX7gEMQ==",
"dev": true
},
"node_modules/@types/semver": {
"version": "7.5.6",
"resolved": "https://registry.npmjs.org/@types/semver/-/semver-7.5.6.tgz",
@ -17396,6 +17431,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",
@ -17778,6 +17829,14 @@
"@babel/runtime": "^7.8.4"
}
},
"node_modules/regexp-tree": {
"version": "0.1.27",
"resolved": "https://registry.npmjs.org/regexp-tree/-/regexp-tree-0.1.27.tgz",
"integrity": "sha512-iETxpjK6YoRWJG5o6hXLwvjYAoW+FEZn9os0PD/b6AP6xQwsa/Y7lCVgIixBbUPMfhu+i2LtdeAqVTgGlQarfA==",
"bin": {
"regexp-tree": "bin/regexp-tree"
}
},
"node_modules/regexp.prototype.flags": {
"version": "1.5.1",
"resolved": "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.5.1.tgz",
@ -18094,6 +18153,14 @@
}
]
},
"node_modules/safe-regex": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/safe-regex/-/safe-regex-2.1.1.tgz",
"integrity": "sha512-rx+x8AMzKb5Q5lQ95Zoi6ZbJqwCLkqi3XuJXp5P3rT8OEc6sZCJG5AE5dU3lsgRr/F4Bs31jSlVN+j5KrsGu9A==",
"dependencies": {
"regexp-tree": "~0.1.1"
}
},
"node_modules/safe-regex-test": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/safe-regex-test/-/safe-regex-test-1.0.0.tgz",

View File

@ -78,6 +78,7 @@
"@types/picomatch": "^2.3.3",
"@types/prompt-sync": "^4.2.3",
"@types/resolve": "^1.20.6",
"@types/safe-regex": "^1.1.6",
"@types/uuid": "^9.0.7",
"@typescript-eslint/eslint-plugin": "^6.20.0",
"@typescript-eslint/parser": "^6.20.0",
@ -121,6 +122,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",
@ -171,6 +173,7 @@
"pino": "^8.16.2",
"posthog-node": "^3.6.2",
"probot": "^13.0.0",
"safe-regex": "^2.1.1",
"smee-client": "^2.0.0",
"tedious": "^18.2.1",
"tweetnacl": "^1.0.3",

View File

@ -7,14 +7,33 @@ const prompt = promptSync({
sigint: true
});
type ComponentType = 1 | 2 | 3;
console.log(`
Component List
--------------
0. Exit
1. Service component
2. DAL component
3. Router component
`);
const componentType = parseInt(prompt("Select a component: "), 10);
function getComponentType(): ComponentType {
while (true) {
const input = prompt("Select a component (0-3): ");
const componentType = parseInt(input, 10);
if (componentType === 0) {
console.log("Exiting the program. Goodbye!");
process.exit(0);
} else if (componentType === 1 || componentType === 2 || componentType === 3) {
return componentType;
} else {
console.log("Invalid input. Please enter 0, 1, 2, or 3.");
}
}
}
const componentType = getComponentType();
if (componentType === 1) {
const componentName = prompt("Enter service name: ");

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";
@ -35,6 +36,7 @@ import { ActorAuthMethod, ActorType } from "@app/services/auth/auth-type";
import { TAuthTokenServiceFactory } from "@app/services/auth-token/auth-token-service";
import { TCertificateServiceFactory } from "@app/services/certificate/certificate-service";
import { TCertificateAuthorityServiceFactory } from "@app/services/certificate-authority/certificate-authority-service";
import { TCertificateTemplateServiceFactory } from "@app/services/certificate-template/certificate-template-service";
import { TGroupProjectServiceFactory } from "@app/services/group-project/group-project-service";
import { TIdentityServiceFactory } from "@app/services/identity/identity-service";
import { TIdentityAccessTokenServiceFactory } from "@app/services/identity-access-token/identity-access-token-service";
@ -50,6 +52,9 @@ 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 { TPkiAlertServiceFactory } from "@app/services/pki-alert/pki-alert-service";
import { TPkiCollectionServiceFactory } from "@app/services/pki-collection/pki-collection-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 +93,7 @@ declare module "fastify" {
id: string;
orgId: string;
};
rateLimits: RateLimitConfiguration;
// passport data
passportUser: {
isUserCompleted: string;
@ -113,6 +119,7 @@ declare module "fastify" {
group: TGroupServiceFactory;
groupProject: TGroupProjectServiceFactory;
apiKey: TApiKeyServiceFactory;
pkiAlert: TPkiAlertServiceFactory;
project: TProjectServiceFactory;
projectMembership: TProjectMembershipServiceFactory;
projectEnv: TProjectEnvServiceFactory;
@ -150,8 +157,10 @@ declare module "fastify" {
auditLog: TAuditLogServiceFactory;
auditLogStream: TAuditLogStreamServiceFactory;
certificate: TCertificateServiceFactory;
certificateTemplate: TCertificateTemplateServiceFactory;
certificateAuthority: TCertificateAuthorityServiceFactory;
certificateAuthorityCrl: TCertificateAuthorityCrlServiceFactory;
pkiCollection: TPkiCollectionServiceFactory;
secretScanning: TSecretScanningServiceFactory;
license: TLicenseServiceFactory;
trustedIp: TTrustedIpServiceFactory;
@ -165,6 +174,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

@ -53,6 +53,9 @@ import {
TCertificateSecretsUpdate,
TCertificatesInsert,
TCertificatesUpdate,
TCertificateTemplates,
TCertificateTemplatesInsert,
TCertificateTemplatesUpdate,
TDynamicSecretLeases,
TDynamicSecretLeasesInsert,
TDynamicSecretLeasesUpdate,
@ -161,6 +164,15 @@ import {
TOrgRoles,
TOrgRolesInsert,
TOrgRolesUpdate,
TPkiAlerts,
TPkiAlertsInsert,
TPkiAlertsUpdate,
TPkiCollectionItems,
TPkiCollectionItemsInsert,
TPkiCollectionItemsUpdate,
TPkiCollections,
TPkiCollectionsInsert,
TPkiCollectionsUpdate,
TProjectBots,
TProjectBotsInsert,
TProjectBotsUpdate,
@ -355,6 +367,11 @@ declare module "knex/types/tables" {
TCertificateAuthorityCrlUpdate
>;
[TableName.Certificate]: KnexOriginal.CompositeTableType<TCertificates, TCertificatesInsert, TCertificatesUpdate>;
[TableName.CertificateTemplate]: KnexOriginal.CompositeTableType<
TCertificateTemplates,
TCertificateTemplatesInsert,
TCertificateTemplatesUpdate
>;
[TableName.CertificateBody]: KnexOriginal.CompositeTableType<
TCertificateBodies,
TCertificateBodiesInsert,
@ -365,6 +382,17 @@ declare module "knex/types/tables" {
TCertificateSecretsInsert,
TCertificateSecretsUpdate
>;
[TableName.PkiAlert]: KnexOriginal.CompositeTableType<TPkiAlerts, TPkiAlertsInsert, TPkiAlertsUpdate>;
[TableName.PkiCollection]: KnexOriginal.CompositeTableType<
TPkiCollections,
TPkiCollectionsInsert,
TPkiCollectionsUpdate
>;
[TableName.PkiCollectionItem]: KnexOriginal.CompositeTableType<
TPkiCollectionItems,
TPkiCollectionItemsInsert,
TPkiCollectionItemsUpdate
>;
[TableName.UserGroupMembership]: KnexOriginal.CompositeTableType<
TUserGroupMembership,
TUserGroupMembershipInsert,

View File

@ -0,0 +1,117 @@
import { Knex } from "knex";
import { TableName } from "../schemas";
export async function up(knex: Knex): Promise<void> {
if (await knex.schema.hasTable(TableName.CertificateAuthority)) {
const hasActiveCaCertIdColumn = await knex.schema.hasColumn(TableName.CertificateAuthority, "activeCaCertId");
if (!hasActiveCaCertIdColumn) {
await knex.schema.alterTable(TableName.CertificateAuthority, (t) => {
t.uuid("activeCaCertId").nullable();
t.foreign("activeCaCertId").references("id").inTable(TableName.CertificateAuthorityCert);
});
await knex.raw(`
UPDATE "${TableName.CertificateAuthority}" ca
SET "activeCaCertId" = cac.id
FROM "${TableName.CertificateAuthorityCert}" cac
WHERE ca.id = cac."caId"
`);
}
}
if (await knex.schema.hasTable(TableName.CertificateAuthorityCert)) {
const hasVersionColumn = await knex.schema.hasColumn(TableName.CertificateAuthorityCert, "version");
if (!hasVersionColumn) {
await knex.schema.alterTable(TableName.CertificateAuthorityCert, (t) => {
t.integer("version").nullable();
t.dropUnique(["caId"]);
});
await knex(TableName.CertificateAuthorityCert).update({ version: 1 }).whereNull("version");
await knex.schema.alterTable(TableName.CertificateAuthorityCert, (t) => {
t.integer("version").notNullable().alter();
});
}
const hasCaSecretIdColumn = await knex.schema.hasColumn(TableName.CertificateAuthorityCert, "caSecretId");
if (!hasCaSecretIdColumn) {
await knex.schema.alterTable(TableName.CertificateAuthorityCert, (t) => {
t.uuid("caSecretId").nullable();
t.foreign("caSecretId").references("id").inTable(TableName.CertificateAuthoritySecret).onDelete("CASCADE");
});
await knex.raw(`
UPDATE "${TableName.CertificateAuthorityCert}" cert
SET "caSecretId" = (
SELECT sec.id
FROM "${TableName.CertificateAuthoritySecret}" sec
WHERE sec."caId" = cert."caId"
)
`);
await knex.schema.alterTable(TableName.CertificateAuthorityCert, (t) => {
t.uuid("caSecretId").notNullable().alter();
});
}
}
if (await knex.schema.hasTable(TableName.CertificateAuthoritySecret)) {
await knex.schema.alterTable(TableName.CertificateAuthoritySecret, (t) => {
t.dropUnique(["caId"]);
});
}
if (await knex.schema.hasTable(TableName.Certificate)) {
await knex.schema.alterTable(TableName.Certificate, (t) => {
t.uuid("caCertId").nullable();
t.foreign("caCertId").references("id").inTable(TableName.CertificateAuthorityCert);
});
await knex.raw(`
UPDATE "${TableName.Certificate}" cert
SET "caCertId" = (
SELECT caCert.id
FROM "${TableName.CertificateAuthorityCert}" caCert
WHERE caCert."caId" = cert."caId"
)
`);
await knex.schema.alterTable(TableName.Certificate, (t) => {
t.uuid("caCertId").notNullable().alter();
});
}
}
export async function down(knex: Knex): Promise<void> {
if (await knex.schema.hasTable(TableName.CertificateAuthority)) {
if (await knex.schema.hasColumn(TableName.CertificateAuthority, "activeCaCertId")) {
await knex.schema.alterTable(TableName.CertificateAuthority, (t) => {
t.dropColumn("activeCaCertId");
});
}
}
if (await knex.schema.hasTable(TableName.CertificateAuthorityCert)) {
if (await knex.schema.hasColumn(TableName.CertificateAuthorityCert, "version")) {
await knex.schema.alterTable(TableName.CertificateAuthorityCert, (t) => {
t.dropColumn("version");
});
}
if (await knex.schema.hasColumn(TableName.CertificateAuthorityCert, "caSecretId")) {
await knex.schema.alterTable(TableName.CertificateAuthorityCert, (t) => {
t.dropColumn("caSecretId");
});
}
}
if (await knex.schema.hasTable(TableName.Certificate)) {
if (await knex.schema.hasColumn(TableName.Certificate, "caCertId")) {
await knex.schema.alterTable(TableName.Certificate, (t) => {
t.dropColumn("caCertId");
});
}
}
}

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,21 @@
import { Knex } from "knex";
import { TableName } from "../schemas";
export async function up(knex: Knex): Promise<void> {
const hasNameField = await knex.schema.hasColumn(TableName.SecretTag, "name");
if (hasNameField) {
await knex.schema.alterTable(TableName.SecretTag, (t) => {
t.dropColumn("name");
});
}
}
export async function down(knex: Knex): Promise<void> {
const hasNameField = await knex.schema.hasColumn(TableName.SecretTag, "name");
if (!hasNameField) {
await knex.schema.alterTable(TableName.SecretTag, (t) => {
t.string("name");
});
}
}

View File

@ -0,0 +1,62 @@
import { Knex } from "knex";
import { TableName } from "../schemas";
import { createOnUpdateTrigger, dropOnUpdateTrigger } from "../utils";
export async function up(knex: Knex): Promise<void> {
if (!(await knex.schema.hasTable(TableName.PkiCollection))) {
await knex.schema.createTable(TableName.PkiCollection, (t) => {
t.uuid("id", { primaryKey: true }).defaultTo(knex.fn.uuid());
t.timestamps(true, true, true);
t.string("projectId").notNullable();
t.foreign("projectId").references("id").inTable(TableName.Project).onDelete("CASCADE");
t.string("name").notNullable();
t.string("description").notNullable();
});
}
await createOnUpdateTrigger(knex, TableName.PkiCollection);
if (!(await knex.schema.hasTable(TableName.PkiCollectionItem))) {
await knex.schema.createTable(TableName.PkiCollectionItem, (t) => {
t.uuid("id", { primaryKey: true }).defaultTo(knex.fn.uuid());
t.timestamps(true, true, true);
t.uuid("pkiCollectionId").notNullable();
t.foreign("pkiCollectionId").references("id").inTable(TableName.PkiCollection).onDelete("CASCADE");
t.uuid("caId").nullable();
t.foreign("caId").references("id").inTable(TableName.CertificateAuthority).onDelete("CASCADE");
t.uuid("certId").nullable();
t.foreign("certId").references("id").inTable(TableName.Certificate).onDelete("CASCADE");
});
}
await createOnUpdateTrigger(knex, TableName.PkiCollectionItem);
if (!(await knex.schema.hasTable(TableName.PkiAlert))) {
await knex.schema.createTable(TableName.PkiAlert, (t) => {
t.uuid("id", { primaryKey: true }).defaultTo(knex.fn.uuid());
t.timestamps(true, true, true);
t.string("projectId").notNullable();
t.foreign("projectId").references("id").inTable(TableName.Project).onDelete("CASCADE");
t.uuid("pkiCollectionId").notNullable();
t.foreign("pkiCollectionId").references("id").inTable(TableName.PkiCollection).onDelete("CASCADE");
t.string("name").notNullable();
t.integer("alertBeforeDays").notNullable();
t.string("recipientEmails").notNullable();
t.unique(["name", "projectId"]);
});
}
await createOnUpdateTrigger(knex, TableName.PkiAlert);
}
export async function down(knex: Knex): Promise<void> {
await knex.schema.dropTableIfExists(TableName.PkiAlert);
await dropOnUpdateTrigger(knex, TableName.PkiAlert);
await knex.schema.dropTableIfExists(TableName.PkiCollectionItem);
await dropOnUpdateTrigger(knex, TableName.PkiCollectionItem);
await knex.schema.dropTableIfExists(TableName.PkiCollection);
await dropOnUpdateTrigger(knex, TableName.PkiCollection);
}

View File

@ -0,0 +1,55 @@
import { Knex } from "knex";
import { TableName } from "../schemas";
import { createOnUpdateTrigger, dropOnUpdateTrigger } from "../utils";
export async function up(knex: Knex): Promise<void> {
const hasCertificateTemplateTable = await knex.schema.hasTable(TableName.CertificateTemplate);
if (!hasCertificateTemplateTable) {
await knex.schema.createTable(TableName.CertificateTemplate, (tb) => {
tb.uuid("id", { primaryKey: true }).defaultTo(knex.fn.uuid());
tb.uuid("caId").notNullable();
tb.foreign("caId").references("id").inTable(TableName.CertificateAuthority).onDelete("CASCADE");
tb.uuid("pkiCollectionId");
tb.foreign("pkiCollectionId").references("id").inTable(TableName.PkiCollection).onDelete("SET NULL");
tb.string("name").notNullable();
tb.string("commonName").notNullable();
tb.string("subjectAlternativeName").notNullable();
tb.string("ttl").notNullable();
tb.timestamps(true, true, true);
});
await createOnUpdateTrigger(knex, TableName.CertificateTemplate);
}
const doesCertificateTableHaveTemplateId = await knex.schema.hasColumn(
TableName.Certificate,
"certificateTemplateId"
);
if (!doesCertificateTableHaveTemplateId) {
await knex.schema.alterTable(TableName.Certificate, (tb) => {
tb.uuid("certificateTemplateId");
tb.foreign("certificateTemplateId").references("id").inTable(TableName.CertificateTemplate).onDelete("SET NULL");
});
}
}
export async function down(knex: Knex): Promise<void> {
const doesCertificateTableHaveTemplateId = await knex.schema.hasColumn(
TableName.Certificate,
"certificateTemplateId"
);
if (doesCertificateTableHaveTemplateId) {
await knex.schema.alterTable(TableName.Certificate, (t) => {
t.dropColumn("certificateTemplateId");
});
}
const hasCertificateTemplateTable = await knex.schema.hasTable(TableName.CertificateTemplate);
if (hasCertificateTemplateTable) {
await knex.schema.dropTable(TableName.CertificateTemplate);
await dropOnUpdateTrigger(knex, TableName.CertificateTemplate);
}
}

View File

@ -27,7 +27,8 @@ export const CertificateAuthoritiesSchema = z.object({
maxPathLength: z.number().nullable().optional(),
keyAlgorithm: z.string(),
notBefore: z.date().nullable().optional(),
notAfter: z.date().nullable().optional()
notAfter: z.date().nullable().optional(),
activeCaCertId: z.string().uuid().nullable().optional()
});
export type TCertificateAuthorities = z.infer<typeof CertificateAuthoritiesSchema>;

View File

@ -15,7 +15,9 @@ export const CertificateAuthorityCertsSchema = z.object({
updatedAt: z.date(),
caId: z.string().uuid(),
encryptedCertificate: zodBuffer,
encryptedCertificateChain: zodBuffer
encryptedCertificateChain: zodBuffer,
version: z.number(),
caSecretId: z.string().uuid()
});
export type TCertificateAuthorityCerts = z.infer<typeof CertificateAuthorityCertsSchema>;

View File

@ -0,0 +1,24 @@
// Code generated by automation script, DO NOT EDIT.
// Automated by pulling database and generating zod schema
// To update. Just run npm run generate:schema
// Written by akhilmhdh.
import { z } from "zod";
import { TImmutableDBKeys } from "./models";
export const CertificateTemplatesSchema = z.object({
id: z.string().uuid(),
caId: z.string().uuid(),
pkiCollectionId: z.string().uuid().nullable().optional(),
name: z.string(),
commonName: z.string(),
subjectAlternativeName: z.string(),
ttl: z.string(),
createdAt: z.date(),
updatedAt: z.date()
});
export type TCertificateTemplates = z.infer<typeof CertificateTemplatesSchema>;
export type TCertificateTemplatesInsert = Omit<z.input<typeof CertificateTemplatesSchema>, TImmutableDBKeys>;
export type TCertificateTemplatesUpdate = Partial<Omit<z.input<typeof CertificateTemplatesSchema>, TImmutableDBKeys>>;

View File

@ -20,7 +20,9 @@ export const CertificatesSchema = z.object({
notAfter: z.date(),
revokedAt: z.date().nullable().optional(),
revocationReason: z.number().nullable().optional(),
altNames: z.string().default("").nullable().optional()
altNames: z.string().default("").nullable().optional(),
caCertId: z.string().uuid(),
certificateTemplateId: z.string().uuid().nullable().optional()
});
export type TCertificates = z.infer<typeof CertificatesSchema>;

View File

@ -14,6 +14,7 @@ export * from "./certificate-authority-crl";
export * from "./certificate-authority-secret";
export * from "./certificate-bodies";
export * from "./certificate-secrets";
export * from "./certificate-templates";
export * from "./certificates";
export * from "./dynamic-secret-leases";
export * from "./dynamic-secrets";
@ -52,6 +53,9 @@ export * from "./org-bots";
export * from "./org-memberships";
export * from "./org-roles";
export * from "./organizations";
export * from "./pki-alerts";
export * from "./pki-collection-items";
export * from "./pki-collections";
export * from "./project-bots";
export * from "./project-environments";
export * from "./project-keys";

View File

@ -9,6 +9,10 @@ export enum TableName {
Certificate = "certificates",
CertificateBody = "certificate_bodies",
CertificateSecret = "certificate_secrets",
CertificateTemplate = "certificate_templates",
PkiAlert = "pki_alerts",
PkiCollection = "pki_collections",
PkiCollectionItem = "pki_collection_items",
Groups = "groups",
GroupProjectMembership = "group_project_memberships",
GroupProjectMembershipRole = "group_project_membership_roles",

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 PkiAlertsSchema = z.object({
id: z.string().uuid(),
createdAt: z.date(),
updatedAt: z.date(),
projectId: z.string(),
pkiCollectionId: z.string().uuid(),
name: z.string(),
alertBeforeDays: z.number(),
recipientEmails: z.string()
});
export type TPkiAlerts = z.infer<typeof PkiAlertsSchema>;
export type TPkiAlertsInsert = Omit<z.input<typeof PkiAlertsSchema>, TImmutableDBKeys>;
export type TPkiAlertsUpdate = Partial<Omit<z.input<typeof PkiAlertsSchema>, 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 PkiCollectionItemsSchema = z.object({
id: z.string().uuid(),
createdAt: z.date(),
updatedAt: z.date(),
pkiCollectionId: z.string().uuid(),
caId: z.string().uuid().nullable().optional(),
certId: z.string().uuid().nullable().optional()
});
export type TPkiCollectionItems = z.infer<typeof PkiCollectionItemsSchema>;
export type TPkiCollectionItemsInsert = Omit<z.input<typeof PkiCollectionItemsSchema>, TImmutableDBKeys>;
export type TPkiCollectionItemsUpdate = Partial<Omit<z.input<typeof PkiCollectionItemsSchema>, 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 PkiCollectionsSchema = z.object({
id: z.string().uuid(),
createdAt: z.date(),
updatedAt: z.date(),
projectId: z.string(),
name: z.string(),
description: z.string()
});
export type TPkiCollections = z.infer<typeof PkiCollectionsSchema>;
export type TPkiCollectionsInsert = Omit<z.input<typeof PkiCollectionsSchema>, TImmutableDBKeys>;
export type TPkiCollectionsUpdate = Partial<Omit<z.input<typeof PkiCollectionsSchema>, TImmutableDBKeys>>;

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

@ -9,7 +9,6 @@ import { TImmutableDBKeys } from "./models";
export const SecretTagsSchema = z.object({
id: z.string().uuid(),
name: z.string(),
slug: z.string(),
color: z.string().nullable().optional(),
createdAt: z.date(),

View File

@ -131,7 +131,7 @@ export const registerDynamicSecretLeaseRouter = async (server: FastifyZodProvide
.default("/")
.transform(removeTrailingSlash)
.describe(DYNAMIC_SECRET_LEASES.RENEW.path),
environmentSlug: z.string().min(1).describe(DYNAMIC_SECRET_LEASES.RENEW.ttl)
environmentSlug: z.string().min(1).describe(DYNAMIC_SECRET_LEASES.RENEW.environmentSlug)
}),
response: {
200: z.object({

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

@ -75,15 +75,16 @@ export const auditLogDALFactory = (db: TDbClient) => {
.del()
.returning("id");
numberOfRetryOnFailure = 0; // reset
// eslint-disable-next-line no-await-in-loop
await new Promise((resolve) => {
setTimeout(resolve, 100); // time to breathe for db
});
} catch (error) {
numberOfRetryOnFailure += 1;
logger.error(error, "Failed to delete audit log on pruning");
} finally {
// eslint-disable-next-line no-await-in-loop
await new Promise((resolve) => {
setTimeout(resolve, 10); // time to breathe for db
});
}
} while (deletedAuditLogIds.length > 0 && numberOfRetryOnFailure < MAX_RETRY_ON_FAILURE);
} while (deletedAuditLogIds.length > 0 || numberOfRetryOnFailure < MAX_RETRY_ON_FAILURE);
};
return { ...auditLogOrm, pruneAuditLog, find };

View File

@ -1,5 +1,6 @@
import { ForbiddenError } from "@casl/ability";
import { getConfig } from "@app/lib/config/env";
import { BadRequestError } from "@app/lib/errors";
import { TPermissionServiceFactory } from "../permission/permission-service";
@ -61,6 +62,10 @@ export const auditLogServiceFactory = ({
};
const createAuditLog = async (data: TCreateAuditLogDTO) => {
const appCfg = getConfig();
if (appCfg.DISABLE_AUDIT_LOG_GENERATION) {
return;
}
// add all cases in which project id or org id cannot be added
if (data.event.type !== EventType.LOGIN_IDENTITY_UNIVERSAL_AUTH) {
if (!data.projectId && !data.orgId) throw new BadRequestError({ message: "Must either project id or org id" });

View File

@ -2,6 +2,7 @@ import { TProjectPermission } from "@app/lib/types";
import { ActorType } from "@app/services/auth/auth-type";
import { CaStatus } from "@app/services/certificate-authority/certificate-authority-types";
import { TIdentityTrustedIp } from "@app/services/identity/identity-types";
import { PkiItemType } from "@app/services/pki-collection/pki-collection-types";
export type TListProjectAuditLogDTO = {
auditLogActor?: string;
@ -130,7 +131,9 @@ export enum EventType {
GET_CA = "get-certificate-authority",
UPDATE_CA = "update-certificate-authority",
DELETE_CA = "delete-certificate-authority",
RENEW_CA = "renew-certificate-authority",
GET_CA_CSR = "get-certificate-authority-csr",
GET_CA_CERTS = "get-certificate-authority-certs",
GET_CA_CERT = "get-certificate-authority-cert",
SIGN_INTERMEDIATE = "sign-intermediate",
IMPORT_CA_CERT = "import-certificate-authority-cert",
@ -141,13 +144,29 @@ export enum EventType {
DELETE_CERT = "delete-cert",
REVOKE_CERT = "revoke-cert",
GET_CERT_BODY = "get-cert-body",
CREATE_PKI_ALERT = "create-pki-alert",
GET_PKI_ALERT = "get-pki-alert",
UPDATE_PKI_ALERT = "update-pki-alert",
DELETE_PKI_ALERT = "delete-pki-alert",
CREATE_PKI_COLLECTION = "create-pki-collection",
GET_PKI_COLLECTION = "get-pki-collection",
UPDATE_PKI_COLLECTION = "update-pki-collection",
DELETE_PKI_COLLECTION = "delete-pki-collection",
GET_PKI_COLLECTION_ITEMS = "get-pki-collection-items",
ADD_PKI_COLLECTION_ITEM = "add-pki-collection-item",
DELETE_PKI_COLLECTION_ITEM = "delete-pki-collection-item",
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"
LOAD_PROJECT_KMS_BACKUP = "load-project-kms-backup",
ORG_ADMIN_ACCESS_PROJECT = "org-admin-accessed-project",
CREATE_CERTIFICATE_TEMPLATE = "create-certificate-template",
UPDATE_CERTIFICATE_TEMPLATE = "update-certificate-template",
DELETE_CERTIFICATE_TEMPLATE = "delete-certificate-template",
GET_CERTIFICATE_TEMPLATE = "get-certificate-template"
}
interface UserActorMetadata {
@ -337,6 +356,7 @@ interface DeleteIntegrationEvent {
targetServiceId?: string;
path?: string;
region?: string;
shouldDeleteIntegrationSecrets?: boolean;
};
}
@ -1094,6 +1114,14 @@ interface DeleteCa {
};
}
interface RenewCa {
type: EventType.RENEW_CA;
metadata: {
caId: string;
dn: string;
};
}
interface GetCaCsr {
type: EventType.GET_CA_CSR;
metadata: {
@ -1102,6 +1130,14 @@ interface GetCaCsr {
};
}
interface GetCaCerts {
type: EventType.GET_CA_CERTS;
metadata: {
caId: string;
dn: string;
};
}
interface GetCaCert {
type: EventType.GET_CA_CERT;
metadata: {
@ -1189,6 +1225,95 @@ interface GetCertBody {
};
}
interface CreatePkiAlert {
type: EventType.CREATE_PKI_ALERT;
metadata: {
pkiAlertId: string;
pkiCollectionId: string;
name: string;
alertBeforeDays: number;
recipientEmails: string;
};
}
interface GetPkiAlert {
type: EventType.GET_PKI_ALERT;
metadata: {
pkiAlertId: string;
};
}
interface UpdatePkiAlert {
type: EventType.UPDATE_PKI_ALERT;
metadata: {
pkiAlertId: string;
pkiCollectionId?: string;
name?: string;
alertBeforeDays?: number;
recipientEmails?: string;
};
}
interface DeletePkiAlert {
type: EventType.DELETE_PKI_ALERT;
metadata: {
pkiAlertId: string;
};
}
interface CreatePkiCollection {
type: EventType.CREATE_PKI_COLLECTION;
metadata: {
pkiCollectionId: string;
name: string;
};
}
interface GetPkiCollection {
type: EventType.GET_PKI_COLLECTION;
metadata: {
pkiCollectionId: string;
};
}
interface UpdatePkiCollection {
type: EventType.UPDATE_PKI_COLLECTION;
metadata: {
pkiCollectionId: string;
name?: string;
};
}
interface DeletePkiCollection {
type: EventType.DELETE_PKI_COLLECTION;
metadata: {
pkiCollectionId: string;
};
}
interface GetPkiCollectionItems {
type: EventType.GET_PKI_COLLECTION_ITEMS;
metadata: {
pkiCollectionId: string;
};
}
interface AddPkiCollectionItem {
type: EventType.ADD_PKI_COLLECTION_ITEM;
metadata: {
pkiCollectionItemId: string;
pkiCollectionId: string;
type: PkiItemType;
itemId: string;
};
}
interface DeletePkiCollectionItem {
type: EventType.DELETE_PKI_COLLECTION_ITEM;
metadata: {
pkiCollectionItemId: string;
pkiCollectionId: string;
};
}
interface CreateKmsEvent {
type: EventType.CREATE_KMS;
metadata: {
@ -1245,6 +1370,56 @@ interface LoadProjectKmsBackupEvent {
metadata: Record<string, string>; // no metadata yet
}
interface CreateCertificateTemplate {
type: EventType.CREATE_CERTIFICATE_TEMPLATE;
metadata: {
certificateTemplateId: string;
caId: string;
pkiCollectionId?: string;
name: string;
commonName: string;
subjectAlternativeName: string;
ttl: string;
};
}
interface GetCertificateTemplate {
type: EventType.GET_CERTIFICATE_TEMPLATE;
metadata: {
certificateTemplateId: string;
};
}
interface UpdateCertificateTemplate {
type: EventType.UPDATE_CERTIFICATE_TEMPLATE;
metadata: {
certificateTemplateId: string;
caId: string;
pkiCollectionId?: string;
name: string;
commonName: string;
subjectAlternativeName: string;
ttl: string;
};
}
interface DeleteCertificateTemplate {
type: EventType.DELETE_CERTIFICATE_TEMPLATE;
metadata: {
certificateTemplateId: string;
};
}
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
@ -1337,7 +1512,9 @@ export type Event =
| GetCa
| UpdateCa
| DeleteCa
| RenewCa
| GetCaCsr
| GetCaCerts
| GetCaCert
| SignIntermediate
| ImportCaCert
@ -1348,10 +1525,26 @@ export type Event =
| DeleteCert
| RevokeCert
| GetCertBody
| CreatePkiAlert
| GetPkiAlert
| UpdatePkiAlert
| DeletePkiAlert
| CreatePkiCollection
| GetPkiCollection
| UpdatePkiCollection
| DeletePkiCollection
| GetPkiCollectionItems
| AddPkiCollectionItem
| DeletePkiCollectionItem
| CreateKmsEvent
| UpdateKmsEvent
| DeleteKmsEvent
| GetKmsEvent
| UpdateProjectKmsEvent
| GetProjectKmsBackupEvent
| LoadProjectKmsBackupEvent;
| LoadProjectKmsBackupEvent
| OrgAdminAccessProjectEvent
| CreateCertificateTemplate
| UpdateCertificateTemplate
| GetCertificateTemplate
| DeleteCertificateTemplate;

View File

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

View File

@ -58,6 +58,11 @@ export type TFeatureSet = {
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",
@ -22,7 +26,8 @@ export enum OrgPermissionSubjects {
Billing = "billing",
SecretScanning = "secret-scanning",
Identity = "identity",
Kms = "kms"
Kms = "kms",
AdminConsole = "organization-admin-console"
}
export type OrgPermissionSet =
@ -39,7 +44,8 @@ export type OrgPermissionSet =
| [OrgPermissionActions, OrgPermissionSubjects.SecretScanning]
| [OrgPermissionActions, OrgPermissionSubjects.Billing]
| [OrgPermissionActions, OrgPermissionSubjects.Identity]
| [OrgPermissionActions, OrgPermissionSubjects.Kms];
| [OrgPermissionActions, OrgPermissionSubjects.Kms]
| [OrgPermissionAdminConsoleAction, OrgPermissionSubjects.AdminConsole];
const buildAdminPermission = () => {
const { can, build } = new AbilityBuilder<MongoAbility<OrgPermissionSet>>(createMongoAbility);
@ -107,6 +113,8 @@ const buildAdminPermission = () => {
can(OrgPermissionActions.Edit, OrgPermissionSubjects.Kms);
can(OrgPermissionActions.Delete, OrgPermissionSubjects.Kms);
can(OrgPermissionAdminConsoleAction.AccessAllProjects, OrgPermissionSubjects.AdminConsole);
return build({ conditionsMatcher });
};

View File

@ -30,6 +30,9 @@ export enum ProjectPermissionSub {
Identity = "identity",
CertificateAuthorities = "certificate-authorities",
Certificates = "certificates",
CertificateTemplates = "certificate-templates",
PkiAlerts = "pki-alerts",
PkiCollections = "pki-collections",
Kms = "kms"
}
@ -63,6 +66,9 @@ export type ProjectPermissionSet =
| [ProjectPermissionActions, ProjectPermissionSub.Identity]
| [ProjectPermissionActions, ProjectPermissionSub.CertificateAuthorities]
| [ProjectPermissionActions, ProjectPermissionSub.Certificates]
| [ProjectPermissionActions, ProjectPermissionSub.CertificateTemplates]
| [ProjectPermissionActions, ProjectPermissionSub.PkiAlerts]
| [ProjectPermissionActions, ProjectPermissionSub.PkiCollections]
| [ProjectPermissionActions.Delete, ProjectPermissionSub.Project]
| [ProjectPermissionActions.Edit, ProjectPermissionSub.Project]
| [ProjectPermissionActions.Read, ProjectPermissionSub.SecretRollback]
@ -161,6 +167,21 @@ const buildAdminPermissionRules = () => {
can(ProjectPermissionActions.Edit, ProjectPermissionSub.Certificates);
can(ProjectPermissionActions.Delete, ProjectPermissionSub.Certificates);
can(ProjectPermissionActions.Read, ProjectPermissionSub.CertificateTemplates);
can(ProjectPermissionActions.Create, ProjectPermissionSub.CertificateTemplates);
can(ProjectPermissionActions.Edit, ProjectPermissionSub.CertificateTemplates);
can(ProjectPermissionActions.Delete, ProjectPermissionSub.CertificateTemplates);
can(ProjectPermissionActions.Read, ProjectPermissionSub.PkiAlerts);
can(ProjectPermissionActions.Create, ProjectPermissionSub.PkiAlerts);
can(ProjectPermissionActions.Edit, ProjectPermissionSub.PkiAlerts);
can(ProjectPermissionActions.Delete, ProjectPermissionSub.PkiAlerts);
can(ProjectPermissionActions.Read, ProjectPermissionSub.PkiCollections);
can(ProjectPermissionActions.Create, ProjectPermissionSub.PkiCollections);
can(ProjectPermissionActions.Edit, ProjectPermissionSub.PkiCollections);
can(ProjectPermissionActions.Delete, ProjectPermissionSub.PkiCollections);
can(ProjectPermissionActions.Edit, ProjectPermissionSub.Project);
can(ProjectPermissionActions.Delete, ProjectPermissionSub.Project);
@ -237,6 +258,11 @@ const buildMemberPermissionRules = () => {
can(ProjectPermissionActions.Edit, ProjectPermissionSub.Certificates);
can(ProjectPermissionActions.Delete, ProjectPermissionSub.Certificates);
can(ProjectPermissionActions.Read, ProjectPermissionSub.CertificateTemplates);
can(ProjectPermissionActions.Read, ProjectPermissionSub.PkiAlerts);
can(ProjectPermissionActions.Read, ProjectPermissionSub.PkiCollections);
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

@ -8,6 +8,7 @@ import { removeTrailingSlash } from "@app/lib/fn";
import { containsGlobPatterns } from "@app/lib/picomatch";
import { TProjectEnvDALFactory } from "@app/services/project-env/project-env-dal";
import { TLicenseServiceFactory } from "../license/license-service";
import { TSecretApprovalPolicyApproverDALFactory } from "./secret-approval-policy-approver-dal";
import { TSecretApprovalPolicyDALFactory } from "./secret-approval-policy-dal";
import {
@ -28,6 +29,7 @@ type TSecretApprovalPolicyServiceFactoryDep = {
secretApprovalPolicyDAL: TSecretApprovalPolicyDALFactory;
projectEnvDAL: Pick<TProjectEnvDALFactory, "findOne">;
secretApprovalPolicyApproverDAL: TSecretApprovalPolicyApproverDALFactory;
licenseService: Pick<TLicenseServiceFactory, "getPlan">;
};
export type TSecretApprovalPolicyServiceFactory = ReturnType<typeof secretApprovalPolicyServiceFactory>;
@ -36,7 +38,8 @@ export const secretApprovalPolicyServiceFactory = ({
secretApprovalPolicyDAL,
permissionService,
secretApprovalPolicyApproverDAL,
projectEnvDAL
projectEnvDAL,
licenseService
}: TSecretApprovalPolicyServiceFactoryDep) => {
const createSecretApprovalPolicy = async ({
name,
@ -65,6 +68,15 @@ export const secretApprovalPolicyServiceFactory = ({
ProjectPermissionActions.Create,
ProjectPermissionSub.SecretApproval
);
const plan = await licenseService.getPlan(actorOrgId);
if (!plan.secretApproval) {
throw new BadRequestError({
message:
"Failed to create secret approval policy due to plan restriction. Upgrade plan to create secret approval policy."
});
}
const env = await projectEnvDAL.findOne({ slug: environment, projectId });
if (!env) throw new BadRequestError({ message: "Environment not found" });
@ -115,6 +127,14 @@ export const secretApprovalPolicyServiceFactory = ({
);
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Edit, ProjectPermissionSub.SecretApproval);
const plan = await licenseService.getPlan(actorOrgId);
if (!plan.secretApproval) {
throw new BadRequestError({
message:
"Failed to update secret approval policy due to plan restriction. Upgrade plan to update secret approval policy."
});
}
const updatedSap = await secretApprovalPolicyDAL.transaction(async (tx) => {
const doc = await secretApprovalPolicyDAL.updateById(
secretApprovalPolicy.id,
@ -167,6 +187,14 @@ export const secretApprovalPolicyServiceFactory = ({
ProjectPermissionSub.SecretApproval
);
const plan = await licenseService.getPlan(actorOrgId);
if (!plan.secretApproval) {
throw new BadRequestError({
message:
"Failed to update secret approval policy due to plan restriction. Upgrade plan to update secret approval policy."
});
}
await secretApprovalPolicyDAL.deleteById(secretPolicyId);
return sapPolicy;
};

View File

@ -81,15 +81,13 @@ export const secretApprovalRequestSecretDALFactory = (db: TDbClient) => {
.select({
secVerTagId: "secVerTag.id",
secVerTagColor: "secVerTag.color",
secVerTagSlug: "secVerTag.slug",
secVerTagName: "secVerTag.name"
secVerTagSlug: "secVerTag.slug"
})
.select(
db.ref("id").withSchema(TableName.SecretTag).as("tagId"),
db.ref("id").withSchema(TableName.SecretApprovalRequestSecretTag).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")
db.ref("slug").withSchema(TableName.SecretTag).as("tagSlug")
)
.select(
db.ref("secretBlindIndex").withSchema(TableName.Secret).as("orgSecBlindIndex"),
@ -124,9 +122,9 @@ export const secretApprovalRequestSecretDALFactory = (db: TDbClient) => {
{
key: "tagJnId",
label: "tags" as const,
mapper: ({ tagId: id, tagName: name, tagSlug: slug, tagColor: color }) => ({
mapper: ({ tagId: id, tagSlug: slug, tagColor: color }) => ({
id,
name,
name: slug,
slug,
color
})
@ -200,11 +198,11 @@ export const secretApprovalRequestSecretDALFactory = (db: TDbClient) => {
{
key: "secVerTagId",
label: "tags" as const,
mapper: ({ secVerTagId: id, secVerTagName: name, secVerTagSlug: slug, secVerTagColor: color }) => ({
mapper: ({ secVerTagId: id, secVerTagSlug: slug, secVerTagColor: color }) => ({
// eslint-disable-next-line
id,
// eslint-disable-next-line
name,
name: slug,
// eslint-disable-next-line
slug,
// eslint-disable-next-line
@ -262,15 +260,13 @@ export const secretApprovalRequestSecretDALFactory = (db: TDbClient) => {
.select({
secVerTagId: "secVerTag.id",
secVerTagColor: "secVerTag.color",
secVerTagSlug: "secVerTag.slug",
secVerTagName: "secVerTag.name"
secVerTagSlug: "secVerTag.slug"
})
.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")
db.ref("slug").withSchema(TableName.SecretTag).as("tagSlug")
)
.select(
db.ref("version").withSchema(TableName.SecretV2).as("orgSecVersion"),
@ -292,9 +288,9 @@ export const secretApprovalRequestSecretDALFactory = (db: TDbClient) => {
{
key: "tagJnId",
label: "tags" as const,
mapper: ({ tagId: id, tagName: name, tagSlug: slug, tagColor: color }) => ({
mapper: ({ tagId: id, tagSlug: slug, tagColor: color }) => ({
id,
name,
name: slug,
slug,
color
})
@ -330,11 +326,11 @@ export const secretApprovalRequestSecretDALFactory = (db: TDbClient) => {
{
key: "secVerTagId",
label: "tags" as const,
mapper: ({ secVerTagId: id, secVerTagName: name, secVerTagSlug: slug, secVerTagColor: color }) => ({
mapper: ({ secVerTagId: id, secVerTagSlug: slug, secVerTagColor: color }) => ({
// eslint-disable-next-line
id,
// eslint-disable-next-line
name,
name: slug,
// eslint-disable-next-line
slug,
// eslint-disable-next-line

View File

@ -50,6 +50,7 @@ import { TSecretVersionV2TagDALFactory } from "@app/services/secret-v2-bridge/se
import { SmtpTemplates, TSmtpService } from "@app/services/smtp/smtp-service";
import { TUserDALFactory } from "@app/services/user/user-dal";
import { TLicenseServiceFactory } from "../license/license-service";
import { TPermissionServiceFactory } from "../permission/permission-service";
import { ProjectPermissionActions, ProjectPermissionSub } from "../permission/project-permission";
import { TSecretSnapshotServiceFactory } from "../secret-snapshot/secret-snapshot-service";
@ -97,6 +98,7 @@ type TSecretApprovalRequestServiceFactoryDep = {
>;
secretVersionV2BridgeDAL: Pick<TSecretVersionV2DALFactory, "insertMany" | "findLatestVersionMany">;
secretVersionTagV2BridgeDAL: Pick<TSecretVersionV2TagDALFactory, "insertMany">;
licenseService: Pick<TLicenseServiceFactory, "getPlan">;
};
export type TSecretApprovalRequestServiceFactory = ReturnType<typeof secretApprovalRequestServiceFactory>;
@ -122,7 +124,8 @@ export const secretApprovalRequestServiceFactory = ({
kmsService,
secretV2BridgeDAL,
secretVersionV2BridgeDAL,
secretVersionTagV2BridgeDAL
secretVersionTagV2BridgeDAL,
licenseService
}: TSecretApprovalRequestServiceFactoryDep) => {
const requestCount = async ({ projectId, actor, actorId, actorOrgId, actorAuthMethod }: TApprovalRequestCountDTO) => {
if (actor === ActorType.SERVICE) throw new BadRequestError({ message: "Cannot use service token" });
@ -224,12 +227,10 @@ export const secretApprovalRequestServiceFactory = ({
secretKey: el.key,
id: el.id,
version: el.version,
secretValue: el.encryptedValue
? secretManagerDecryptor({ cipherTextBlob: el.encryptedValue }).toString()
: undefined,
secretValue: el.encryptedValue ? secretManagerDecryptor({ cipherTextBlob: el.encryptedValue }).toString() : "",
secretComment: el.encryptedComment
? secretManagerDecryptor({ cipherTextBlob: el.encryptedComment }).toString()
: undefined,
: "",
secret: el.secret
? {
secretKey: el.secret.key,
@ -237,10 +238,10 @@ export const secretApprovalRequestServiceFactory = ({
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
@ -250,10 +251,10 @@ export const secretApprovalRequestServiceFactory = ({
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
}));
@ -297,6 +298,14 @@ export const secretApprovalRequestServiceFactory = ({
if (!secretApprovalRequest) throw new BadRequestError({ message: "Secret approval request not found" });
if (actor !== ActorType.USER) throw new BadRequestError({ message: "Must be a user" });
const plan = await licenseService.getPlan(actorOrgId);
if (!plan.secretApproval) {
throw new BadRequestError({
message:
"Failed to review secret approval request due to plan restriction. Upgrade plan to review secret approval request."
});
}
const { policy } = secretApprovalRequest;
const { hasRole } = await permissionService.getProjectPermission(
ActorType.USER,
@ -347,6 +356,14 @@ export const secretApprovalRequestServiceFactory = ({
if (!secretApprovalRequest) throw new BadRequestError({ message: "Secret approval request not found" });
if (actor !== ActorType.USER) throw new BadRequestError({ message: "Must be a user" });
const plan = await licenseService.getPlan(actorOrgId);
if (!plan.secretApproval) {
throw new BadRequestError({
message:
"Failed to update secret approval request due to plan restriction. Upgrade plan to update secret approval request."
});
}
const { policy } = secretApprovalRequest;
const { hasRole } = await permissionService.getProjectPermission(
ActorType.USER,
@ -388,6 +405,14 @@ export const secretApprovalRequestServiceFactory = ({
if (!secretApprovalRequest) throw new BadRequestError({ message: "Secret approval request not found" });
if (actor !== ActorType.USER) throw new BadRequestError({ message: "Must be a user" });
const plan = await licenseService.getPlan(actorOrgId);
if (!plan.secretApproval) {
throw new BadRequestError({
message:
"Failed to merge secret approval request due to plan restriction. Upgrade plan to merge secret approval request."
});
}
const { policy, folderId, projectId } = secretApprovalRequest;
const { hasRole } = await permissionService.getProjectPermission(
ActorType.USER,

View File

@ -257,7 +257,7 @@ export const secretReplicationServiceFactory = ({
secretDAL: secretV2BridgeDAL,
folderDAL,
secretImportDAL,
decryptor: (value) => (value ? secretManagerDecryptor({ cipherTextBlob: value }).toString() : undefined)
decryptor: (value) => (value ? secretManagerDecryptor({ cipherTextBlob: value }).toString() : "")
});
// secrets that gets replicated across imports
const sourceDecryptedLocalSecrets = sourceLocalSecrets.map((el) => ({
@ -449,7 +449,7 @@ export const secretReplicationServiceFactory = ({
});
}
if (locallyDeletedSecrets.length) {
await secretDAL.delete(
await secretV2BridgeDAL.delete(
{
$in: {
id: locallyDeletedSecrets.map(({ id }) => id)

View File

@ -164,10 +164,10 @@ export const secretSnapshotServiceFactory = ({
secretKey: el.key,
secretValue: el.encryptedValue
? secretManagerDecryptor({ cipherTextBlob: el.encryptedValue }).toString()
: undefined,
: "",
secretComment: el.encryptedComment
? secretManagerDecryptor({ cipherTextBlob: el.encryptedComment }).toString()
: undefined
: ""
}))
};
} else {

View File

@ -100,8 +100,7 @@ export const snapshotDALFactory = (db: TDbClient) => {
db.ref("id").withSchema(TableName.SecretTag).as("tagId"),
db.ref("id").withSchema(TableName.SecretVersionTag).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")
db.ref("slug").withSchema(TableName.SecretTag).as("tagSlug")
);
return sqlNestRelationships({
data,
@ -132,9 +131,9 @@ export const snapshotDALFactory = (db: TDbClient) => {
{
key: "tagVersionId",
label: "tags" as const,
mapper: ({ tagId: id, tagName: name, tagSlug: slug, tagColor: color, tagVersionId: vId }) => ({
mapper: ({ tagId: id, tagSlug: slug, tagColor: color, tagVersionId: vId }) => ({
id,
name,
name: slug,
slug,
color,
vId
@ -195,8 +194,7 @@ export const snapshotDALFactory = (db: TDbClient) => {
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")
db.ref("slug").withSchema(TableName.SecretTag).as("tagSlug")
);
return sqlNestRelationships({
data,
@ -227,9 +225,9 @@ export const snapshotDALFactory = (db: TDbClient) => {
{
key: "tagVersionId",
label: "tags" as const,
mapper: ({ tagId: id, tagName: name, tagSlug: slug, tagColor: color, tagVersionId: vId }) => ({
mapper: ({ tagId: id, tagSlug: slug, tagColor: color, tagVersionId: vId }) => ({
id,
name,
name: slug,
slug,
color,
vId
@ -353,8 +351,7 @@ export const snapshotDALFactory = (db: TDbClient) => {
db.ref("id").withSchema(TableName.SecretTag).as("tagId"),
db.ref("id").withSchema(TableName.SecretVersionTag).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")
db.ref("slug").withSchema(TableName.SecretTag).as("tagSlug")
);
const formated = sqlNestRelationships({
@ -377,9 +374,9 @@ export const snapshotDALFactory = (db: TDbClient) => {
{
key: "tagVersionId",
label: "tags" as const,
mapper: ({ tagId: id, tagName: name, tagSlug: slug, tagColor: color, tagVersionId: vId }) => ({
mapper: ({ tagId: id, tagSlug: slug, tagColor: color, tagVersionId: vId }) => ({
id,
name,
name: slug,
slug,
color,
vId
@ -508,8 +505,7 @@ export const snapshotDALFactory = (db: TDbClient) => {
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")
db.ref("slug").withSchema(TableName.SecretTag).as("tagSlug")
);
const formated = sqlNestRelationships({
@ -532,9 +528,9 @@ export const snapshotDALFactory = (db: TDbClient) => {
{
key: "tagVersionId",
label: "tags" as const,
mapper: ({ tagId: id, tagName: name, tagSlug: slug, tagColor: color, tagVersionId: vId }) => ({
mapper: ({ tagId: id, tagSlug: slug, tagColor: color, tagVersionId: vId }) => ({
id,
name,
name: slug,
slug,
color,
vId

View File

@ -5,17 +5,26 @@ import { Redlock, Settings } from "@app/lib/red-lock";
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",
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-"
}
export const KeyStorePrefixes = {
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-",
SyncSecretIntegrationLock: (projectId: string, environmentSlug: string, secretPath: string) =>
`sync-integration-mutex-${projectId}-${environmentSlug}-${secretPath}` as const,
SyncSecretIntegrationLastRunTimestamp: (projectId: string, environmentSlug: string, secretPath: string) =>
`sync-integration-last-run-${projectId}-${environmentSlug}-${secretPath}` as const
};
export const KeyStoreTtls = {
SetSyncSecretIntegrationLastRunTimestampInSeconds: 10
};
type TWaitTillReady = {
key: string;
@ -37,10 +46,10 @@ export const keyStoreFactory = (redisUrl: string) => {
const setItemWithExpiry = async (
key: string,
exp: number | string,
expiryInSeconds: number | string,
value: string | number | Buffer,
prefix?: string
) => redis.set(prefix ? `${prefix}:${key}` : key, value, "EX", exp);
) => redis.set(prefix ? `${prefix}:${key}` : key, value, "EX", expiryInSeconds);
const deleteItem = async (key: string) => redis.del(key);

View File

@ -596,7 +596,8 @@ export const RAW_SECRETS = {
"The slug of the project to list secrets from. This parameter is only applicable by machine identities.",
environment: "The slug of the environment to list secrets from.",
secretPath: "The secret path to list secrets from.",
includeImports: "Weather to include imported secrets or not."
includeImports: "Weather to include imported secrets or not.",
tagSlugs: "The comma separated tag slugs to filter secrets"
},
CREATE: {
secretName: "The name of the secret to create.",
@ -1048,12 +1049,27 @@ export const CERTIFICATE_AUTHORITIES = {
caId: "The ID of the CA to generate CSR from",
csr: "The generated CSR from the CA"
},
RENEW_CA_CERT: {
caId: "The ID of the CA to renew the CA certificate for",
type: "The type of behavior to use for the renewal operation. Currently Infisical is only able to renew a CA certificate with the same key pair.",
notAfter: "The expiry date and time for the renewed CA certificate in YYYY-MM-DDTHH:mm:ss.sssZ format",
certificate: "The renewed CA certificate body",
certificateChain: "The certificate chain of the CA",
serialNumber: "The serial number of the renewed CA certificate"
},
GET_CERT: {
caId: "The ID of the CA to get the certificate body and certificate chain from",
certificate: "The certificate body of the CA",
certificateChain: "The certificate chain of the CA",
serialNumber: "The serial number of the CA certificate"
},
GET_CA_CERTS: {
caId: "The ID of the CA to get the CA certificates for",
certificate: "The certificate body of the CA certificate",
certificateChain: "The certificate chain of the CA certificate",
serialNumber: "The serial number of the CA certificate",
version: "The version of the CA certificate. The version is incremented for each CA renewal operation."
},
SIGN_INTERMEDIATE: {
caId: "The ID of the CA to sign the intermediate certificate with",
csr: "The pem-encoded CSR to sign with the CA",
@ -1073,6 +1089,8 @@ export const CERTIFICATE_AUTHORITIES = {
},
ISSUE_CERT: {
caId: "The ID of the CA to issue the certificate from",
certificateTemplateId: "The ID of the certificate template to issue the certificate from",
pkiCollectionId: "The ID of the PKI collection to add the certificate to",
friendlyName: "A friendly name for the certificate",
commonName: "The common name (CN) for the certificate",
altNames:
@ -1088,6 +1106,7 @@ export const CERTIFICATE_AUTHORITIES = {
},
SIGN_CERT: {
caId: "The ID of the CA to issue the certificate from",
pkiCollectionId: "The ID of the PKI collection to add the certificate to",
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",
@ -1129,6 +1148,91 @@ export const CERTIFICATES = {
}
};
export const CERTIFICATE_TEMPLATES = {
CREATE: {
caId: "The ID of the certificate authority to associate the template with",
pkiCollectionId: "The ID of the PKI collection to bind to the template",
name: "The name of the template",
commonName: "The regular expression string to use for validating common names",
subjectAlternativeName: "The regular expression string to use for validating subject alternative names",
ttl: "The max TTL for the template"
},
GET: {
certificateTemplateId: "The ID of the certificate template to get"
},
UPDATE: {
certificateTemplateId: "The ID of the certificate template to update",
caId: "The ID of the certificate authority to update the association with the template",
pkiCollectionId: "The ID of the PKI collection to update the binding to the template",
name: "The updated name of the template",
commonName: "The updated regular expression string for validating common names",
subjectAlternativeName: "The updated regular expression string for validating subject alternative names",
ttl: "The updated max TTL for the template"
},
DELETE: {
certificateTemplateId: "The ID of the certificate template to delete"
}
};
export const ALERTS = {
CREATE: {
projectId: "The ID of the project to create the alert in",
pkiCollectionId: "The ID of the PKI collection to bind to the alert",
name: "The name of the alert",
alertBeforeDays: "The number of days before the certificate expires to trigger the alert",
emails: "The email addresses to send the alert email to"
},
GET: {
alertId: "The ID of the alert to get"
},
UPDATE: {
alertId: "The ID of the alert to update",
name: "The name of the alert to update to",
alertBeforeDays: "The number of days before the certificate expires to trigger the alert to update to",
pkiCollectionId: "The ID of the PKI collection to bind to the alert to update to",
emails: "The email addresses to send the alert email to update to"
},
DELETE: {
alertId: "The ID of the alert to delete"
}
};
export const PKI_COLLECTIONS = {
CREATE: {
projectId: "The ID of the project to create the PKI collection in",
name: "The name of the PKI collection",
description: "A description for the PKI collection"
},
GET: {
collectionId: "The ID of the PKI collection to get"
},
UPDATE: {
collectionId: "The ID of the PKI collection to update",
name: "The name of the PKI collection to update to",
description: "The description for the PKI collection to update to"
},
DELETE: {
collectionId: "The ID of the PKI collection to delete"
},
LIST_ITEMS: {
collectionId: "The ID of the PKI collection to list items from",
type: "The type of the PKI collection item to list",
offset: "The offset to start from",
limit: "The number of items to return"
},
ADD_ITEM: {
collectionId: "The ID of the PKI collection to add the item to",
type: "The type of the PKI collection item to add",
itemId: "The resource ID of the PKI collection item to add"
},
DELETE_ITEM: {
collectionId: "The ID of the PKI collection to delete the item from",
collectionItemId: "The ID of the PKI collection item to delete",
type: "The type of the deleted PKI collection item",
itemId: "The resource ID of the deleted PKI collection item"
}
};
export const PROJECT_ROLE = {
CREATE: {
projectSlug: "Slug of the project to create the role for.",

View File

@ -140,7 +140,8 @@ const envSchema = z
MAINTENANCE_MODE: zodStrBool.default("false"),
CAPTCHA_SECRET: zpStr(z.string().optional()),
PLAIN_API_KEY: zpStr(z.string().optional()),
PLAIN_WISH_LABEL_IDS: zpStr(z.string().optional())
PLAIN_WISH_LABEL_IDS: zpStr(z.string().optional()),
DISABLE_AUDIT_LOG_GENERATION: zodStrBool.default("false")
})
.transform((data) => ({
...data,

View File

@ -1,2 +1,8 @@
export const getLastMidnightDateISO = (last = 1) =>
`${new Date(new Date().setDate(new Date().getDate() - last)).toISOString().slice(0, 10)}T00:00:00Z`;
export const getTimeDifferenceInSeconds = (lhsTimestamp: string, rhsTimestamp: string) => {
const lhs = new Date(lhsTimestamp);
const rhs = new Date(rhsTimestamp);
return Math.floor((Number(lhs) - Number(rhs)) / 1000);
};

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,16 @@ export const ormify = <DbOps extends object, Tname extends keyof Tables>(db: Kne
throw new DatabaseError({ error, name: "Create" });
}
},
// This spilit the insert into multiple chunk
batchInsert: async (data: readonly Tables[Tname]["insert"][], tx?: Knex) => {
try {
if (!data.length) return [];
const res = await (tx || db).batchInsert(tableName, data as never).returning("*");
return res as Tables[Tname]["base"][];
} catch (error) {
throw new DatabaseError({ error, name: "batchInsert" });
}
},
upsert: async (data: readonly Tables[Tname]["insert"][], onConflictField: keyof Tables[Tname]["base"], tx?: Knex) => {
try {
if (!data.length) return [];

View File

@ -16,6 +16,7 @@ export enum QueueName {
// TODO(akhilmhdh): This will get removed later. For now this is kept to stop the repeatable queue
AuditLogPrune = "audit-log-prune",
DailyResourceCleanUp = "daily-resource-cleanup",
DailyExpiringPkiItemAlert = "daily-expiring-pki-item-alert",
TelemetryInstanceStats = "telemtry-self-hosted-stats",
IntegrationSync = "sync-integrations",
SecretWebhook = "secret-webhook",
@ -36,6 +37,7 @@ export enum QueueJobs {
// TODO(akhilmhdh): This will get removed later. For now this is kept to stop the repeatable queue
AuditLogPrune = "audit-log-prune-job",
DailyResourceCleanUp = "daily-resource-cleanup-job",
DailyExpiringPkiItemAlert = "daily-expiring-pki-item-alert",
SecWebhook = "secret-webhook-trigger",
TelemetryInstanceStats = "telemetry-self-hosted-stats",
IntegrationSync = "secret-integration-pull",
@ -71,6 +73,10 @@ export type TQueueJobTypes = {
name: QueueJobs.DailyResourceCleanUp;
payload: undefined;
};
[QueueName.DailyExpiringPkiItemAlert]: {
name: QueueJobs.DailyExpiringPkiItemAlert;
payload: undefined;
};
[QueueName.AuditLogPrune]: {
name: QueueJobs.AuditLogPrune;
payload: undefined;

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

@ -89,6 +89,8 @@ import { certificateAuthorityDALFactory } from "@app/services/certificate-author
import { certificateAuthorityQueueFactory } from "@app/services/certificate-authority/certificate-authority-queue";
import { certificateAuthoritySecretDALFactory } from "@app/services/certificate-authority/certificate-authority-secret-dal";
import { certificateAuthorityServiceFactory } from "@app/services/certificate-authority/certificate-authority-service";
import { certificateTemplateDALFactory } from "@app/services/certificate-template/certificate-template-dal";
import { certificateTemplateServiceFactory } from "@app/services/certificate-template/certificate-template-service";
import { groupProjectDALFactory } from "@app/services/group-project/group-project-dal";
import { groupProjectMembershipRoleDALFactory } from "@app/services/group-project/group-project-membership-role-dal";
import { groupProjectServiceFactory } from "@app/services/group-project/group-project-service";
@ -129,7 +131,14 @@ 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 { dailyExpiringPkiItemAlertQueueServiceFactory } from "@app/services/pki-alert/expiring-pki-item-alert-queue";
import { pkiAlertDALFactory } from "@app/services/pki-alert/pki-alert-dal";
import { pkiAlertServiceFactory } from "@app/services/pki-alert/pki-alert-service";
import { pkiCollectionDALFactory } from "@app/services/pki-collection/pki-collection-dal";
import { pkiCollectionItemDALFactory } from "@app/services/pki-collection/pki-collection-item-dal";
import { pkiCollectionServiceFactory } from "@app/services/pki-collection/pki-collection-service";
import { projectDALFactory } from "@app/services/project/project-dal";
import { projectQueueFactory } from "@app/services/project/project-queue";
import { projectServiceFactory } from "@app/services/project/project-service";
@ -183,6 +192,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";
@ -354,7 +364,8 @@ export const registerRoutes = async (
projectEnvDAL,
secretApprovalPolicyApproverDAL: sapApproverDAL,
permissionService,
secretApprovalPolicyDAL
secretApprovalPolicyDAL,
licenseService
});
const tokenService = tokenServiceFactory({ tokenDAL: authTokenDAL, userDAL, orgMembershipDAL });
@ -498,6 +509,16 @@ export const registerRoutes = async (
keyStore,
licenseService
});
const orgAdminService = orgAdminServiceFactory({
projectDAL,
permissionService,
projectUserMembershipRoleDAL,
userDAL,
projectBotDAL,
projectKeyDAL,
projectMembershipDAL
});
const rateLimitService = rateLimitServiceFactory({
rateLimitDAL,
licenseService
@ -568,10 +589,15 @@ export const registerRoutes = async (
const certificateAuthorityCertDAL = certificateAuthorityCertDALFactory(db);
const certificateAuthoritySecretDAL = certificateAuthoritySecretDALFactory(db);
const certificateAuthorityCrlDAL = certificateAuthorityCrlDALFactory(db);
const certificateTemplateDAL = certificateTemplateDALFactory(db);
const certificateDAL = certificateDALFactory(db);
const certificateBodyDAL = certificateBodyDALFactory(db);
const pkiAlertDAL = pkiAlertDALFactory(db);
const pkiCollectionDAL = pkiCollectionDALFactory(db);
const pkiCollectionItemDAL = pkiCollectionItemDALFactory(db);
const certificateService = certificateServiceFactory({
certificateDAL,
certificateBodyDAL,
@ -599,9 +625,12 @@ export const registerRoutes = async (
certificateAuthorityCertDAL,
certificateAuthoritySecretDAL,
certificateAuthorityCrlDAL,
certificateTemplateDAL,
certificateAuthorityQueue,
certificateDAL,
certificateBodyDAL,
pkiCollectionDAL,
pkiCollectionItemDAL,
projectDAL,
kmsService,
permissionService
@ -616,6 +645,27 @@ export const registerRoutes = async (
licenseService
});
const certificateTemplateService = certificateTemplateServiceFactory({
certificateTemplateDAL,
certificateAuthorityDAL,
permissionService
});
const pkiAlertService = pkiAlertServiceFactory({
pkiAlertDAL,
pkiCollectionDAL,
permissionService,
smtpService
});
const pkiCollectionService = pkiCollectionServiceFactory({
pkiCollectionDAL,
pkiCollectionItemDAL,
certificateAuthorityDAL,
certificateDAL,
permissionService
});
const projectService = projectServiceFactory({
permissionService,
projectDAL,
@ -632,11 +682,14 @@ export const registerRoutes = async (
licenseService,
certificateAuthorityDAL,
certificateDAL,
pkiAlertDAL,
pkiCollectionDAL,
projectUserMembershipRoleDAL,
identityProjectMembershipRoleDAL,
keyStore,
kmsService,
projectBotDAL
projectBotDAL,
certificateTemplateDAL
});
const projectEnvService = projectEnvServiceFactory({
@ -699,6 +752,7 @@ export const registerRoutes = async (
kmsService
});
const secretQueueService = secretQueueFactory({
keyStore,
queueService,
secretDAL,
folderDAL,
@ -784,7 +838,8 @@ export const registerRoutes = async (
secretVersionTagV2BridgeDAL,
smtpService,
projectEnvDAL,
userDAL
userDAL,
licenseService
});
const secretService = secretServiceFactory({
@ -885,8 +940,15 @@ export const registerRoutes = async (
folderDAL,
integrationDAL,
integrationAuthDAL,
secretQueueService
secretQueueService,
integrationAuthService,
projectBotService,
secretV2BridgeDAL,
secretImportDAL,
secretDAL,
kmsService
});
const serviceTokenService = serviceTokenServiceFactory({
projectEnvDAL,
serviceTokenDAL,
@ -1018,7 +1080,13 @@ export const registerRoutes = async (
snapshotDAL,
identityAccessTokenDAL,
secretSharingDAL,
secretVersionV2DAL: secretVersionV2BridgeDAL
secretVersionV2DAL: secretVersionV2BridgeDAL,
identityUniversalAuthClientSecretDAL: identityUaClientSecretDAL
});
const dailyExpiringPkiItemAlert = dailyExpiringPkiItemAlertQueueServiceFactory({
queueService,
pkiAlertService
});
const oidcService = oidcConfigServiceFactory({
@ -1045,6 +1113,7 @@ export const registerRoutes = async (
await telemetryQueue.startTelemetryCheck();
await dailyResourceCleanUp.startCleanUp();
await dailyExpiringPkiItemAlert.startSendingAlerts();
await kmsService.startService();
// inject all services
@ -1102,7 +1171,10 @@ export const registerRoutes = async (
auditLogStream: auditLogStreamService,
certificate: certificateService,
certificateAuthority: certificateAuthorityService,
certificateTemplate: certificateTemplateService,
certificateAuthorityCrl: certificateAuthorityCrlService,
pkiAlert: pkiAlertService,
pkiCollection: pkiCollectionService,
secretScanning: secretScanningService,
license: licenseService,
trustedIp: trustedIpService,
@ -1113,7 +1185,8 @@ export const registerRoutes = async (
identityProjectAdditionalPrivilege: identityProjectAdditionalPrivilegeService,
secretSharing: secretSharingService,
userEngagement: userEngagementService,
externalKms: externalKmsService
externalKms: externalKmsService,
orgAdmin: orgAdminService
});
const cronJobs: CronJob[] = [];
@ -1130,6 +1203,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

@ -63,8 +63,8 @@ export const secretRawSchema = z.object({
version: z.number(),
type: z.string(),
secretKey: z.string(),
secretValue: z.string().optional(),
secretComment: z.string().optional(),
secretValue: z.string(),
secretComment: z.string(),
secretReminderNote: z.string().nullable().optional(),
secretReminderRepeatDays: z.number().nullable().optional(),
skipMultilineEncoding: z.boolean().default(false).nullable().optional(),

View File

@ -8,7 +8,7 @@ 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 { CertKeyAlgorithm } from "@app/services/certificate/certificate-types";
import { CaStatus, CaType } from "@app/services/certificate-authority/certificate-authority-types";
import { CaRenewalType, CaStatus, CaType } from "@app/services/certificate-authority/certificate-authority-types";
import {
validateAltNamesField,
validateCaDateField
@ -275,15 +275,118 @@ export const registerCaRouter = async (server: FastifyZodProvider) => {
}
});
server.route({
method: "POST",
url: "/:caId/renew",
config: {
rateLimit: writeLimit
},
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
schema: {
description: "Perform CA certificate renewal",
params: z.object({
caId: z.string().trim().describe(CERTIFICATE_AUTHORITIES.RENEW_CA_CERT.caId)
}),
body: z.object({
type: z.nativeEnum(CaRenewalType).describe(CERTIFICATE_AUTHORITIES.RENEW_CA_CERT.type),
notAfter: validateCaDateField.describe(CERTIFICATE_AUTHORITIES.RENEW_CA_CERT.notAfter)
}),
response: {
200: z.object({
certificate: z.string().trim().describe(CERTIFICATE_AUTHORITIES.RENEW_CA_CERT.certificate),
certificateChain: z.string().trim().describe(CERTIFICATE_AUTHORITIES.RENEW_CA_CERT.certificateChain),
serialNumber: z.string().trim().describe(CERTIFICATE_AUTHORITIES.RENEW_CA_CERT.serialNumber)
})
}
},
handler: async (req) => {
const { certificate, certificateChain, serialNumber, ca } =
await server.services.certificateAuthority.renewCaCert({
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.RENEW_CA,
metadata: {
caId: ca.id,
dn: ca.dn
}
}
});
return {
certificate,
certificateChain,
serialNumber
};
}
});
server.route({
method: "GET",
url: "/:caId/certificate",
url: "/:caId/ca-certificates",
config: {
rateLimit: readLimit
},
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
schema: {
description: "Get cert and cert chain of a CA",
description: "Get list of past and current CA certificates for a CA",
params: z.object({
caId: z.string().trim().describe(CERTIFICATE_AUTHORITIES.GET_CA_CERTS.caId)
}),
response: {
200: z.array(
z.object({
certificate: z.string().describe(CERTIFICATE_AUTHORITIES.GET_CA_CERTS.certificate),
certificateChain: z.string().describe(CERTIFICATE_AUTHORITIES.GET_CA_CERTS.certificateChain),
serialNumber: z.string().describe(CERTIFICATE_AUTHORITIES.GET_CA_CERTS.serialNumber),
version: z.number().describe(CERTIFICATE_AUTHORITIES.GET_CA_CERTS.version)
})
)
}
},
handler: async (req) => {
const { caCerts, ca } = await server.services.certificateAuthority.getCaCerts({
caId: req.params.caId,
actor: req.permission.type,
actorId: req.permission.id,
actorAuthMethod: req.permission.authMethod,
actorOrgId: req.permission.orgId
});
await server.services.auditLog.createAuditLog({
...req.auditLogInfo,
projectId: ca.projectId,
event: {
type: EventType.GET_CA_CERTS,
metadata: {
caId: ca.id,
dn: ca.dn
}
}
});
return caCerts;
}
});
server.route({
method: "GET",
url: "/:caId/certificate", // TODO: consider updating endpoint structure considering CA certificates
config: {
rateLimit: readLimit
},
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
schema: {
description: "Get current CA cert and cert chain of a CA",
params: z.object({
caId: z.string().trim().describe(CERTIFICATE_AUTHORITIES.GET_CERT.caId)
}),
@ -453,6 +556,7 @@ export const registerCaRouter = async (server: FastifyZodProvider) => {
}),
body: z
.object({
pkiCollectionId: z.string().trim().optional().describe(CERTIFICATE_AUTHORITIES.ISSUE_CERT.pkiCollectionId),
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),
@ -532,6 +636,7 @@ export const registerCaRouter = async (server: FastifyZodProvider) => {
body: z
.object({
csr: z.string().trim().min(1).describe(CERTIFICATE_AUTHORITIES.SIGN_CERT.csr),
pkiCollectionId: z.string().trim().optional().describe(CERTIFICATE_AUTHORITIES.SIGN_CERT.pkiCollectionId),
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),

View File

@ -1,12 +1,17 @@
import ms from "ms";
import { z } from "zod";
import { CertificatesSchema } from "@app/db/schemas";
import { EventType } from "@app/ee/services/audit-log/audit-log-types";
import { CERTIFICATES } from "@app/lib/api-docs";
import { CERTIFICATE_AUTHORITIES, CERTIFICATES } 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 { CrlReason } from "@app/services/certificate/certificate-types";
import {
validateAltNamesField,
validateCaDateField
} from "@app/services/certificate-authority/certificate-authority-validators";
export const registerCertRouter = async (server: FastifyZodProvider) => {
server.route({
@ -55,6 +60,185 @@ export const registerCertRouter = async (server: FastifyZodProvider) => {
}
});
server.route({
method: "POST",
url: "/issue-certificate",
config: {
rateLimit: writeLimit
},
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
schema: {
description: "Issue certificate",
body: z
.object({
caId: z.string().trim().optional().describe(CERTIFICATE_AUTHORITIES.ISSUE_CERT.caId),
certificateTemplateId: z
.string()
.trim()
.optional()
.describe(CERTIFICATE_AUTHORITIES.ISSUE_CERT.certificateTemplateId),
pkiCollectionId: z.string().trim().optional().describe(CERTIFICATE_AUTHORITIES.SIGN_CERT.pkiCollectionId),
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
.string()
.refine((val) => ms(val) > 0, "TTL must be a positive number")
.describe(CERTIFICATE_AUTHORITIES.ISSUE_CERT.ttl),
notBefore: validateCaDateField.optional().describe(CERTIFICATE_AUTHORITIES.ISSUE_CERT.notBefore),
notAfter: validateCaDateField.optional().describe(CERTIFICATE_AUTHORITIES.ISSUE_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"]
}
)
.refine(
(data) =>
(data.caId !== undefined && data.certificateTemplateId === undefined) ||
(data.caId === undefined && data.certificateTemplateId !== undefined),
{
message: "Either CA ID or Certificate Template ID must be present, but not both",
path: ["caId", "certificateTemplateId"]
}
),
response: {
200: z.object({
certificate: z.string().trim().describe(CERTIFICATE_AUTHORITIES.ISSUE_CERT.certificate),
issuingCaCertificate: z.string().trim().describe(CERTIFICATE_AUTHORITIES.ISSUE_CERT.issuingCaCertificate),
certificateChain: z.string().trim().describe(CERTIFICATE_AUTHORITIES.ISSUE_CERT.certificateChain),
privateKey: z.string().trim().describe(CERTIFICATE_AUTHORITIES.ISSUE_CERT.privateKey),
serialNumber: z.string().trim().describe(CERTIFICATE_AUTHORITIES.ISSUE_CERT.serialNumber)
})
}
},
handler: async (req) => {
const { certificate, certificateChain, issuingCaCertificate, privateKey, serialNumber, ca } =
await server.services.certificateAuthority.issueCertFromCa({
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.ISSUE_CERT,
metadata: {
caId: ca.id,
dn: ca.dn,
serialNumber
}
}
});
return {
certificate,
certificateChain,
issuingCaCertificate,
privateKey,
serialNumber
};
}
});
server.route({
method: "POST",
url: "/sign-certificate",
config: {
rateLimit: writeLimit
},
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
schema: {
description: "Sign certificate",
body: z
.object({
caId: z.string().trim().optional().describe(CERTIFICATE_AUTHORITIES.SIGN_CERT.caId),
certificateTemplateId: z
.string()
.trim()
.optional()
.describe(CERTIFICATE_AUTHORITIES.ISSUE_CERT.certificateTemplateId),
pkiCollectionId: z.string().trim().optional().describe(CERTIFICATE_AUTHORITIES.SIGN_CERT.pkiCollectionId),
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"]
}
)
.refine(
(data) =>
(data.caId !== undefined && data.certificateTemplateId === undefined) ||
(data.caId === undefined && data.certificateTemplateId !== undefined),
{
message: "Either CA ID or Certificate Template ID must be present, but not both",
path: ["caId", "certificateTemplateId"]
}
),
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({
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
};
}
});
server.route({
method: "POST",
url: "/:serialNumber/revoke",

View File

@ -0,0 +1,205 @@
import ms from "ms";
import { z } from "zod";
import { EventType } from "@app/ee/services/audit-log/audit-log-types";
import { CERTIFICATE_TEMPLATES } 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 { sanitizedCertificateTemplate } from "@app/services/certificate-template/certificate-template-schema";
import { validateTemplateRegexField } from "@app/services/certificate-template/certificate-template-validators";
export const registerCertificateTemplateRouter = async (server: FastifyZodProvider) => {
server.route({
method: "GET",
url: "/:certificateTemplateId",
config: {
rateLimit: readLimit
},
schema: {
params: z.object({
certificateTemplateId: z.string().describe(CERTIFICATE_TEMPLATES.GET.certificateTemplateId)
}),
response: {
200: sanitizedCertificateTemplate
}
},
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
handler: async (req) => {
const certificateTemplate = await server.services.certificateTemplate.getCertTemplate({
id: req.params.certificateTemplateId,
actor: req.permission.type,
actorId: req.permission.id,
actorAuthMethod: req.permission.authMethod,
actorOrgId: req.permission.orgId
});
await server.services.auditLog.createAuditLog({
...req.auditLogInfo,
projectId: certificateTemplate.projectId,
event: {
type: EventType.GET_CERTIFICATE_TEMPLATE,
metadata: {
certificateTemplateId: certificateTemplate.id
}
}
});
return certificateTemplate;
}
});
server.route({
method: "POST",
url: "/",
config: {
rateLimit: writeLimit
},
schema: {
body: z.object({
caId: z.string().describe(CERTIFICATE_TEMPLATES.CREATE.caId),
pkiCollectionId: z.string().optional().describe(CERTIFICATE_TEMPLATES.CREATE.pkiCollectionId),
name: z.string().min(1).describe(CERTIFICATE_TEMPLATES.CREATE.name),
commonName: validateTemplateRegexField.describe(CERTIFICATE_TEMPLATES.CREATE.commonName),
subjectAlternativeName: validateTemplateRegexField.describe(
CERTIFICATE_TEMPLATES.CREATE.subjectAlternativeName
),
ttl: z
.string()
.refine((val) => ms(val) > 0, "TTL must be a positive number")
.describe(CERTIFICATE_TEMPLATES.CREATE.ttl)
}),
response: {
200: sanitizedCertificateTemplate
}
},
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
handler: async (req) => {
const certificateTemplate = await server.services.certificateTemplate.createCertTemplate({
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: certificateTemplate.projectId,
event: {
type: EventType.CREATE_CERTIFICATE_TEMPLATE,
metadata: {
certificateTemplateId: certificateTemplate.id,
caId: certificateTemplate.caId,
pkiCollectionId: certificateTemplate.pkiCollectionId as string,
name: certificateTemplate.name,
commonName: certificateTemplate.commonName,
subjectAlternativeName: certificateTemplate.subjectAlternativeName,
ttl: certificateTemplate.ttl
}
}
});
return certificateTemplate;
}
});
server.route({
method: "PATCH",
url: "/:certificateTemplateId",
config: {
rateLimit: writeLimit
},
schema: {
body: z.object({
caId: z.string().optional().describe(CERTIFICATE_TEMPLATES.UPDATE.caId),
pkiCollectionId: z.string().optional().describe(CERTIFICATE_TEMPLATES.UPDATE.pkiCollectionId),
name: z.string().min(1).optional().describe(CERTIFICATE_TEMPLATES.UPDATE.name),
commonName: validateTemplateRegexField.optional().describe(CERTIFICATE_TEMPLATES.UPDATE.commonName),
subjectAlternativeName: validateTemplateRegexField
.optional()
.describe(CERTIFICATE_TEMPLATES.UPDATE.subjectAlternativeName),
ttl: z
.string()
.refine((val) => ms(val) > 0, "TTL must be a positive number")
.optional()
.describe(CERTIFICATE_TEMPLATES.UPDATE.ttl)
}),
params: z.object({
certificateTemplateId: z.string().describe(CERTIFICATE_TEMPLATES.UPDATE.certificateTemplateId)
}),
response: {
200: sanitizedCertificateTemplate
}
},
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
handler: async (req) => {
const certificateTemplate = await server.services.certificateTemplate.updateCertTemplate({
...req.body,
id: req.params.certificateTemplateId,
actor: req.permission.type,
actorId: req.permission.id,
actorAuthMethod: req.permission.authMethod,
actorOrgId: req.permission.orgId
});
await server.services.auditLog.createAuditLog({
...req.auditLogInfo,
projectId: certificateTemplate.projectId,
event: {
type: EventType.UPDATE_CERTIFICATE_TEMPLATE,
metadata: {
certificateTemplateId: certificateTemplate.id,
caId: certificateTemplate.caId,
pkiCollectionId: certificateTemplate.pkiCollectionId as string,
name: certificateTemplate.name,
commonName: certificateTemplate.commonName,
subjectAlternativeName: certificateTemplate.subjectAlternativeName,
ttl: certificateTemplate.ttl
}
}
});
return certificateTemplate;
}
});
server.route({
method: "DELETE",
url: "/:certificateTemplateId",
config: {
rateLimit: writeLimit
},
schema: {
params: z.object({
certificateTemplateId: z.string().describe(CERTIFICATE_TEMPLATES.DELETE.certificateTemplateId)
}),
response: {
200: sanitizedCertificateTemplate
}
},
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
handler: async (req) => {
const certificateTemplate = await server.services.certificateTemplate.deleteCertTemplate({
id: req.params.certificateTemplateId,
actor: req.permission.type,
actorId: req.permission.id,
actorAuthMethod: req.permission.authMethod,
actorOrgId: req.permission.orgId
});
await server.services.auditLog.createAuditLog({
...req.auditLogInfo,
projectId: certificateTemplate.projectId,
event: {
type: EventType.DELETE_CERTIFICATE_TEMPLATE,
metadata: {
certificateTemplateId: certificateTemplate.id
}
}
});
return certificateTemplate;
}
});
};

View File

@ -3,7 +3,7 @@ import { z } from "zod";
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";
@ -16,7 +16,7 @@ export const registerIdentityRouter = async (server: FastifyZodProvider) => {
method: "POST",
url: "/",
config: {
rateLimit: creationLimit
rateLimit: writeLimit
},
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
schema: {

View File

@ -3,6 +3,7 @@ import { registerAuthRoutes } from "./auth-router";
import { registerProjectBotRouter } from "./bot-router";
import { registerCaRouter } from "./certificate-authority-router";
import { registerCertRouter } from "./certificate-router";
import { registerCertificateTemplateRouter } from "./certificate-template-router";
import { registerIdentityAccessTokenRouter } from "./identity-access-token-router";
import { registerIdentityAwsAuthRouter } from "./identity-aws-iam-auth-router";
import { registerIdentityAzureAuthRouter } from "./identity-azure-auth-router";
@ -15,8 +16,11 @@ 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 { registerPkiAlertRouter } from "./pki-alert-router";
import { registerPkiCollectionRouter } from "./pki-collection-router";
import { registerProjectEnvRouter } from "./project-env-router";
import { registerProjectKeyRouter } from "./project-key-router";
import { registerProjectMembershipRouter } from "./project-membership-router";
@ -50,6 +54,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" });
@ -72,6 +77,9 @@ export const registerV1Routes = async (server: FastifyZodProvider) => {
async (pkiRouter) => {
await pkiRouter.register(registerCaRouter, { prefix: "/ca" });
await pkiRouter.register(registerCertRouter, { prefix: "/certificates" });
await pkiRouter.register(registerCertificateTemplateRouter, { prefix: "/certificate-templates" });
await pkiRouter.register(registerPkiAlertRouter, { prefix: "/alerts" });
await pkiRouter.register(registerPkiCollectionRouter, { prefix: "/collections" });
},
{ prefix: "/pki" }
);

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

@ -0,0 +1,198 @@
import { z } from "zod";
import { PkiAlertsSchema } from "@app/db/schemas";
import { EventType } from "@app/ee/services/audit-log/audit-log-types";
import { ALERTS } 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";
export const registerPkiAlertRouter = async (server: FastifyZodProvider) => {
server.route({
method: "POST",
url: "/",
config: {
rateLimit: writeLimit
},
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
schema: {
description: "Create PKI alert",
body: z.object({
projectId: z.string().trim().describe(ALERTS.CREATE.projectId),
pkiCollectionId: z.string().trim().describe(ALERTS.CREATE.pkiCollectionId),
name: z.string().trim().describe(ALERTS.CREATE.name),
alertBeforeDays: z.number().describe(ALERTS.CREATE.alertBeforeDays),
emails: z
.array(z.string().trim().email({ message: "Invalid email address" }))
.min(1, { message: "You must specify at least 1 email" })
.max(5, { message: "You can specify a maximum of 5 emails" })
.describe(ALERTS.CREATE.emails)
}),
response: {
200: PkiAlertsSchema
}
},
handler: async (req) => {
const alert = await server.services.pkiAlert.createPkiAlert({
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: alert.projectId,
event: {
type: EventType.CREATE_PKI_ALERT,
metadata: {
pkiAlertId: alert.id,
pkiCollectionId: alert.pkiCollectionId,
name: alert.name,
alertBeforeDays: alert.alertBeforeDays,
recipientEmails: alert.recipientEmails
}
}
});
return alert;
}
});
server.route({
method: "GET",
url: "/:alertId",
config: {
rateLimit: readLimit
},
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
schema: {
description: "Get PKI alert",
params: z.object({
alertId: z.string().trim().describe(ALERTS.GET.alertId)
}),
response: {
200: PkiAlertsSchema
}
},
handler: async (req) => {
const alert = await server.services.pkiAlert.getPkiAlertById({
alertId: req.params.alertId,
actor: req.permission.type,
actorId: req.permission.id,
actorAuthMethod: req.permission.authMethod,
actorOrgId: req.permission.orgId
});
await server.services.auditLog.createAuditLog({
...req.auditLogInfo,
projectId: alert.projectId,
event: {
type: EventType.GET_PKI_ALERT,
metadata: {
pkiAlertId: alert.id
}
}
});
return alert;
}
});
server.route({
method: "PATCH",
url: "/:alertId",
config: {
rateLimit: readLimit
},
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
schema: {
description: "Update PKI alert",
params: z.object({
alertId: z.string().trim().describe(ALERTS.UPDATE.alertId)
}),
body: z.object({
name: z.string().trim().optional().describe(ALERTS.UPDATE.name),
alertBeforeDays: z.number().optional().describe(ALERTS.UPDATE.alertBeforeDays),
pkiCollectionId: z.string().trim().optional().describe(ALERTS.UPDATE.pkiCollectionId),
emails: z
.array(z.string().trim().email({ message: "Invalid email address" }))
.min(1, { message: "You must specify at least 1 email" })
.max(5, { message: "You can specify a maximum of 5 emails" })
.optional()
.describe(ALERTS.UPDATE.emails)
}),
response: {
200: PkiAlertsSchema
}
},
handler: async (req) => {
const alert = await server.services.pkiAlert.updatePkiAlert({
alertId: req.params.alertId,
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: alert.projectId,
event: {
type: EventType.UPDATE_PKI_ALERT,
metadata: {
pkiAlertId: alert.id,
pkiCollectionId: alert.pkiCollectionId,
name: alert.name,
alertBeforeDays: alert.alertBeforeDays,
recipientEmails: alert.recipientEmails
}
}
});
return alert;
}
});
server.route({
method: "DELETE",
url: "/:alertId",
config: {
rateLimit: writeLimit
},
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
schema: {
description: "Delete PKI alert",
params: z.object({
alertId: z.string().trim().describe(ALERTS.DELETE.alertId)
}),
response: {
200: PkiAlertsSchema
}
},
handler: async (req) => {
const alert = await server.services.pkiAlert.deletePkiAlert({
alertId: req.params.alertId,
actor: req.permission.type,
actorId: req.permission.id,
actorAuthMethod: req.permission.authMethod,
actorOrgId: req.permission.orgId
});
await server.services.auditLog.createAuditLog({
...req.auditLogInfo,
projectId: alert.projectId,
event: {
type: EventType.DELETE_PKI_ALERT,
metadata: {
pkiAlertId: alert.id
}
}
});
return alert;
}
});
};

View File

@ -0,0 +1,338 @@
import { z } from "zod";
import { PkiCollectionItemsSchema, PkiCollectionsSchema } from "@app/db/schemas";
import { EventType } from "@app/ee/services/audit-log/audit-log-types";
import { PKI_COLLECTIONS } 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 { PkiItemType } from "@app/services/pki-collection/pki-collection-types";
export const registerPkiCollectionRouter = async (server: FastifyZodProvider) => {
server.route({
method: "POST",
url: "/",
config: {
rateLimit: writeLimit
},
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
schema: {
description: "Create PKI collection",
body: z.object({
projectId: z.string().trim().describe(PKI_COLLECTIONS.CREATE.projectId),
name: z.string().trim().describe(PKI_COLLECTIONS.CREATE.name),
description: z.string().trim().default("").describe(PKI_COLLECTIONS.CREATE.description)
}),
response: {
200: PkiCollectionsSchema
}
},
handler: async (req) => {
const pkiCollection = await server.services.pkiCollection.createPkiCollection({
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: pkiCollection.projectId,
event: {
type: EventType.CREATE_PKI_COLLECTION,
metadata: {
pkiCollectionId: pkiCollection.id,
name: pkiCollection.name
}
}
});
return pkiCollection;
}
});
server.route({
method: "GET",
url: "/:collectionId",
config: {
rateLimit: readLimit
},
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
schema: {
description: "Get PKI collection",
params: z.object({
collectionId: z.string().trim().describe(PKI_COLLECTIONS.GET.collectionId)
}),
response: {
200: PkiCollectionsSchema
}
},
handler: async (req) => {
const pkiCollection = await server.services.pkiCollection.getPkiCollectionById({
collectionId: req.params.collectionId,
actor: req.permission.type,
actorId: req.permission.id,
actorAuthMethod: req.permission.authMethod,
actorOrgId: req.permission.orgId
});
await server.services.auditLog.createAuditLog({
...req.auditLogInfo,
projectId: pkiCollection.projectId,
event: {
type: EventType.GET_PKI_COLLECTION,
metadata: {
pkiCollectionId: pkiCollection.id
}
}
});
return pkiCollection;
}
});
server.route({
method: "PATCH",
url: "/:collectionId",
config: {
rateLimit: writeLimit
},
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
schema: {
description: "Update PKI collection",
params: z.object({
collectionId: z.string().trim().describe(PKI_COLLECTIONS.UPDATE.collectionId)
}),
body: z.object({
name: z.string().trim().optional().describe(PKI_COLLECTIONS.UPDATE.name),
description: z.string().trim().optional().describe(PKI_COLLECTIONS.UPDATE.description)
}),
response: {
200: PkiCollectionsSchema
}
},
handler: async (req) => {
const pkiCollection = await server.services.pkiCollection.updatePkiCollection({
collectionId: req.params.collectionId,
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: pkiCollection.projectId,
event: {
type: EventType.UPDATE_PKI_COLLECTION,
metadata: {
pkiCollectionId: pkiCollection.id,
name: pkiCollection.name
}
}
});
return pkiCollection;
}
});
server.route({
method: "DELETE",
url: "/:collectionId",
config: {
rateLimit: writeLimit
},
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
schema: {
description: "Delete PKI collection",
params: z.object({
collectionId: z.string().trim().describe(PKI_COLLECTIONS.DELETE.collectionId)
}),
response: {
200: PkiCollectionsSchema
}
},
handler: async (req) => {
const pkiCollection = await server.services.pkiCollection.deletePkiCollection({
collectionId: req.params.collectionId,
actor: req.permission.type,
actorId: req.permission.id,
actorAuthMethod: req.permission.authMethod,
actorOrgId: req.permission.orgId
});
await server.services.auditLog.createAuditLog({
...req.auditLogInfo,
projectId: pkiCollection.projectId,
event: {
type: EventType.DELETE_PKI_COLLECTION,
metadata: {
pkiCollectionId: pkiCollection.id
}
}
});
return pkiCollection;
}
});
server.route({
method: "GET",
url: "/:collectionId/items",
config: {
rateLimit: writeLimit
},
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
schema: {
description: "Get items in PKI collection",
params: z.object({
collectionId: z.string().trim().describe(PKI_COLLECTIONS.LIST_ITEMS.collectionId)
}),
querystring: z.object({
type: z.nativeEnum(PkiItemType).optional().describe(PKI_COLLECTIONS.LIST_ITEMS.type),
offset: z.coerce.number().min(0).max(100).default(0).describe(PKI_COLLECTIONS.LIST_ITEMS.offset),
limit: z.coerce.number().min(1).max(100).default(25).describe(PKI_COLLECTIONS.LIST_ITEMS.limit)
}),
response: {
200: z.object({
collectionItems: z.array(
PkiCollectionItemsSchema.omit({ caId: true, certId: true }).extend({
type: z.nativeEnum(PkiItemType),
itemId: z.string().trim(),
notBefore: z.date(),
notAfter: z.date(),
friendlyName: z.string().trim()
})
),
totalCount: z.number()
})
}
},
handler: async (req) => {
const { pkiCollection, pkiCollectionItems, totalCount } =
await server.services.pkiCollection.getPkiCollectionItems({
collectionId: req.params.collectionId,
actor: req.permission.type,
actorId: req.permission.id,
actorAuthMethod: req.permission.authMethod,
actorOrgId: req.permission.orgId,
...req.query
});
await server.services.auditLog.createAuditLog({
...req.auditLogInfo,
projectId: pkiCollection.projectId,
event: {
type: EventType.GET_PKI_COLLECTION_ITEMS,
metadata: {
pkiCollectionId: pkiCollection.id
}
}
});
return {
collectionItems: pkiCollectionItems,
totalCount
};
}
});
server.route({
method: "POST",
url: "/:collectionId/items",
config: {
rateLimit: writeLimit
},
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
schema: {
description: "Add item to PKI collection",
params: z.object({
collectionId: z.string().trim().describe(PKI_COLLECTIONS.ADD_ITEM.collectionId)
}),
body: z.object({
type: z.nativeEnum(PkiItemType).describe(PKI_COLLECTIONS.ADD_ITEM.type),
itemId: z.string().trim().describe(PKI_COLLECTIONS.ADD_ITEM.itemId)
}),
response: {
200: PkiCollectionItemsSchema.omit({ caId: true, certId: true }).extend({
type: z.nativeEnum(PkiItemType).describe(PKI_COLLECTIONS.ADD_ITEM.type),
itemId: z.string().trim().describe(PKI_COLLECTIONS.ADD_ITEM.itemId)
})
}
},
handler: async (req) => {
const { pkiCollection, pkiCollectionItem } = await server.services.pkiCollection.addItemToPkiCollection({
collectionId: req.params.collectionId,
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: pkiCollection.projectId,
event: {
type: EventType.ADD_PKI_COLLECTION_ITEM,
metadata: {
pkiCollectionId: pkiCollection.id,
pkiCollectionItemId: pkiCollectionItem.id,
type: pkiCollectionItem.type,
itemId: pkiCollectionItem.itemId
}
}
});
return pkiCollectionItem;
}
});
server.route({
method: "DELETE",
url: "/:collectionId/items/:collectionItemId",
config: {
rateLimit: writeLimit
},
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
schema: {
description: "Remove item from PKI collection",
params: z.object({
collectionId: z.string().trim().describe(PKI_COLLECTIONS.DELETE_ITEM.collectionId),
collectionItemId: z.string().trim().describe(PKI_COLLECTIONS.DELETE_ITEM.collectionItemId)
}),
response: {
200: PkiCollectionItemsSchema.omit({ caId: true, certId: true }).extend({
type: z.nativeEnum(PkiItemType).describe(PKI_COLLECTIONS.DELETE_ITEM.type),
itemId: z.string().trim().describe(PKI_COLLECTIONS.DELETE_ITEM.itemId)
})
}
},
handler: async (req) => {
const { pkiCollection, pkiCollectionItem } = await server.services.pkiCollection.removeItemFromPkiCollection({
collectionId: req.params.collectionId,
itemId: req.params.collectionItemId,
actor: req.permission.type,
actorId: req.permission.id,
actorAuthMethod: req.permission.authMethod,
actorOrgId: req.permission.orgId
});
await server.services.auditLog.createAuditLog({
...req.auditLogInfo,
projectId: pkiCollection.projectId,
event: {
type: EventType.DELETE_PKI_COLLECTION_ITEM,
metadata: {
pkiCollectionId: pkiCollection.id,
pkiCollectionItemId: pkiCollectionItem.id
}
}
});
return pkiCollectionItem;
}
});
};

View File

@ -1,3 +1,4 @@
import slugify from "@sindresorhus/slugify";
import { z } from "zod";
import { SecretTagsSchema } from "@app/db/schemas";
@ -49,7 +50,8 @@ export const registerSecretTagRouter = async (server: FastifyZodProvider) => {
}),
response: {
200: z.object({
workspaceTag: SecretTagsSchema
// akhilmhdh: for terraform backward compatiability
workspaceTag: SecretTagsSchema.extend({ name: z.string() })
})
}
},
@ -79,7 +81,8 @@ export const registerSecretTagRouter = async (server: FastifyZodProvider) => {
}),
response: {
200: z.object({
workspaceTag: SecretTagsSchema
// akhilmhdh: for terraform backward compatiability
workspaceTag: SecretTagsSchema.extend({ name: z.string() })
})
}
},
@ -108,8 +111,14 @@ export const registerSecretTagRouter = async (server: FastifyZodProvider) => {
projectId: z.string().trim().describe(SECRET_TAGS.CREATE.projectId)
}),
body: z.object({
name: z.string().trim().describe(SECRET_TAGS.CREATE.name),
slug: z.string().trim().describe(SECRET_TAGS.CREATE.slug),
slug: z
.string()
.toLowerCase()
.trim()
.describe(SECRET_TAGS.CREATE.slug)
.refine((v) => slugify(v) === v, {
message: "Invalid slug. Slug can only contain alphanumeric characters and hyphens."
}),
color: z.string().trim().describe(SECRET_TAGS.CREATE.color)
}),
response: {
@ -144,8 +153,14 @@ export const registerSecretTagRouter = async (server: FastifyZodProvider) => {
tagId: z.string().trim().describe(SECRET_TAGS.UPDATE.tagId)
}),
body: z.object({
name: z.string().trim().describe(SECRET_TAGS.UPDATE.name),
slug: z.string().trim().describe(SECRET_TAGS.UPDATE.slug),
slug: z
.string()
.toLowerCase()
.trim()
.describe(SECRET_TAGS.UPDATE.slug)
.refine((v) => slugify(v) === v, {
message: "Invalid slug. Slug can only contain alphanumeric characters and hyphens."
}),
color: z.string().trim().describe(SECRET_TAGS.UPDATE.color)
}),
response: {

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({

View File

@ -1,14 +1,21 @@
import slugify from "@sindresorhus/slugify";
import { z } from "zod";
import { CertificateAuthoritiesSchema, CertificatesSchema, ProjectKeysSchema } from "@app/db/schemas";
import {
CertificateAuthoritiesSchema,
CertificatesSchema,
PkiAlertsSchema,
PkiCollectionsSchema,
ProjectKeysSchema
} from "@app/db/schemas";
import { EventType } from "@app/ee/services/audit-log/audit-log-types";
import { PROJECTS } 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 { CaStatus } from "@app/services/certificate-authority/certificate-authority-types";
import { sanitizedCertificateTemplate } from "@app/services/certificate-template/certificate-template-schema";
import { ProjectFilterType } from "@app/services/project/project-types";
import { PostHogEventTypes } from "@app/services/telemetry/telemetry-types";
@ -142,7 +149,7 @@ export const registerProjectRouter = async (server: FastifyZodProvider) => {
method: "POST",
url: "/",
config: {
rateLimit: creationLimit
rateLimit: writeLimit
},
schema: {
description: "Create a new project",
@ -392,4 +399,94 @@ export const registerProjectRouter = async (server: FastifyZodProvider) => {
return { certificates, totalCount };
}
});
server.route({
method: "GET",
url: "/:projectId/pki-alerts",
config: {
rateLimit: readLimit
},
schema: {
params: z.object({
projectId: z.string().trim()
}),
response: {
200: z.object({
alerts: z.array(PkiAlertsSchema)
})
}
},
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
handler: async (req) => {
const { alerts } = await server.services.project.listProjectAlerts({
projectId: req.params.projectId,
actorId: req.permission.id,
actorOrgId: req.permission.orgId,
actorAuthMethod: req.permission.authMethod,
actor: req.permission.type
});
return { alerts };
}
});
server.route({
method: "GET",
url: "/:projectId/pki-collections",
config: {
rateLimit: readLimit
},
schema: {
params: z.object({
projectId: z.string().trim()
}),
response: {
200: z.object({
collections: z.array(PkiCollectionsSchema)
})
}
},
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
handler: async (req) => {
const { pkiCollections } = await server.services.project.listProjectPkiCollections({
projectId: req.params.projectId,
actorId: req.permission.id,
actorOrgId: req.permission.orgId,
actorAuthMethod: req.permission.authMethod,
actor: req.permission.type
});
return { collections: pkiCollections };
}
});
server.route({
method: "GET",
url: "/:projectId/certificate-templates",
config: {
rateLimit: readLimit
},
schema: {
params: z.object({
projectId: z.string().trim()
}),
response: {
200: z.object({
certificateTemplates: sanitizedCertificateTemplate.array()
})
}
},
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
handler: async (req) => {
const { certificateTemplates } = await server.services.project.listProjectCertificateTemplates({
projectId: req.params.projectId,
actorId: req.permission.id,
actorOrgId: req.permission.orgId,
actorAuthMethod: req.permission.authMethod,
actor: req.permission.type
});
return { certificateTemplates };
}
});
};

View File

@ -59,9 +59,10 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => {
tags: SecretTagsSchema.pick({
id: true,
slug: true,
name: true,
color: true
}).array()
})
.extend({ name: z.string() })
.array()
})
)
})
@ -116,16 +117,15 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => {
}),
response: {
200: z.object({
secret: SecretsSchema.omit({ secretBlindIndex: true }).merge(
z.object({
tags: SecretTagsSchema.pick({
id: true,
slug: true,
name: true,
color: true
}).array()
secret: SecretsSchema.omit({ secretBlindIndex: true }).extend({
tags: SecretTagsSchema.pick({
id: true,
slug: true,
color: true
})
)
.extend({ name: z.string() })
.array()
})
})
}
},
@ -180,7 +180,13 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => {
.enum(["true", "false"])
.default("false")
.transform((value) => value === "true")
.describe(RAW_SECRETS.LIST.includeImports)
.describe(RAW_SECRETS.LIST.includeImports),
tagSlugs: z
.string()
.describe(RAW_SECRETS.LIST.tagSlugs)
.optional()
// split by comma and trim the strings
.transform((el) => (el ? el.split(",").map((i) => i.trim()) : []))
}),
response: {
200: z.object({
@ -190,9 +196,9 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => {
tags: SecretTagsSchema.pick({
id: true,
slug: true,
name: true,
color: true
})
.extend({ name: z.string() })
.array()
.optional()
})
@ -251,7 +257,8 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => {
projectId: workspaceId,
path: secretPath,
includeImports: req.query.include_imports,
recursive: req.query.recursive
recursive: req.query.recursive,
tagSlugs: req.query.tagSlugs
});
await server.services.auditLog.createAuditLog({
@ -325,9 +332,9 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => {
tags: SecretTagsSchema.pick({
id: true,
slug: true,
name: true,
color: true
})
.extend({ name: z.string() })
.array()
.optional()
})
@ -731,9 +738,10 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => {
tags: SecretTagsSchema.pick({
id: true,
slug: true,
name: true,
color: true
}).array()
})
.extend({ name: z.string() })
.array()
})
.array(),
imports: z

View File

@ -1,11 +1,17 @@
import * as x509 from "@peculiar/x509";
import crypto from "crypto";
import { BadRequestError } from "@app/lib/errors";
import { NotFoundError } from "@app/lib/errors";
import { getProjectKmsCertificateKeyId } from "@app/services/project/project-fns";
import { CertKeyAlgorithm, CertStatus } from "../certificate/certificate-types";
import { TDNParts, TGetCaCertChainDTO, TGetCaCredentialsDTO, TRebuildCaCrlDTO } from "./certificate-authority-types";
import {
TDNParts,
TGetCaCertChainDTO,
TGetCaCertChainsDTO,
TGetCaCredentialsDTO,
TRebuildCaCrlDTO
} from "./certificate-authority-types";
export const createDistinguishedName = (parts: TDNParts) => {
const dnParts = [];
@ -89,6 +95,8 @@ export const keyAlgorithmToAlgCfg = (keyAlgorithm: CertKeyAlgorithm) => {
* Return the public and private key of CA with id [caId]
* Note: credentials are returned as crypto.webcrypto.CryptoKey
* suitable for use with @peculiar/x509 module
*
* TODO: Update to get latest CA Secret once support for CA renewal with new key pair is added
*/
export const getCaCredentials = async ({
caId,
@ -98,10 +106,10 @@ export const getCaCredentials = async ({
kmsService
}: TGetCaCredentialsDTO) => {
const ca = await certificateAuthorityDAL.findById(caId);
if (!ca) throw new BadRequestError({ message: "CA not found" });
if (!ca) throw new NotFoundError({ message: "CA not found" });
const caSecret = await certificateAuthoritySecretDAL.findOne({ caId });
if (!caSecret) throw new BadRequestError({ message: "CA secret not found" });
if (!caSecret) throw new NotFoundError({ message: "CA secret not found" });
const keyId = await getProjectKmsCertificateKeyId({
projectId: ca.projectId,
@ -132,26 +140,73 @@ export const getCaCredentials = async ({
]);
return {
caSecret,
caPrivateKey,
caPublicKey
};
};
/**
* Return the decrypted pem-encoded certificate and certificate chain
* Return the list of decrypted pem-encoded certificates and certificate chains
* for CA with id [caId].
*/
export const getCaCertChain = async ({
export const getCaCertChains = async ({
caId,
certificateAuthorityDAL,
certificateAuthorityCertDAL,
projectDAL,
kmsService
}: TGetCaCertChainDTO) => {
}: TGetCaCertChainsDTO) => {
const ca = await certificateAuthorityDAL.findById(caId);
if (!ca) throw new BadRequestError({ message: "CA not found" });
if (!ca) throw new NotFoundError({ message: "CA not found" });
const caCert = await certificateAuthorityCertDAL.findOne({ caId: ca.id });
const keyId = await getProjectKmsCertificateKeyId({
projectId: ca.projectId,
projectDAL,
kmsService
});
const kmsDecryptor = await kmsService.decryptWithKmsKey({
kmsId: keyId
});
const caCerts = await certificateAuthorityCertDAL.find({ caId: ca.id }, { sort: [["version", "asc"]] });
const decryptedChains = await Promise.all(
caCerts.map(async (caCert) => {
const decryptedCaCert = await kmsDecryptor({
cipherTextBlob: caCert.encryptedCertificate
});
const caCertObj = new x509.X509Certificate(decryptedCaCert);
const decryptedChain = await kmsDecryptor({
cipherTextBlob: caCert.encryptedCertificateChain
});
return {
certificate: caCertObj.toString("pem"),
certificateChain: decryptedChain.toString("utf-8"),
serialNumber: caCertObj.serialNumber,
version: caCert.version
};
})
);
return decryptedChains;
};
/**
* Return the decrypted pem-encoded certificate and certificate chain
* corresponding to CA certificate with id [caCertId].
*/
export const getCaCertChain = async ({
caCertId,
certificateAuthorityDAL,
certificateAuthorityCertDAL,
projectDAL,
kmsService
}: TGetCaCertChainDTO) => {
const caCert = await certificateAuthorityCertDAL.findById(caCertId);
if (!caCert) throw new NotFoundError({ message: "CA certificate not found" });
const ca = await certificateAuthorityDAL.findById(caCert.caId);
const keyId = await getProjectKmsCertificateKeyId({
projectId: ca.projectId,
@ -194,7 +249,7 @@ export const rebuildCaCrl = async ({
kmsService
}: TRebuildCaCrlDTO) => {
const ca = await certificateAuthorityDAL.findById(caId);
if (!ca) throw new BadRequestError({ message: "CA not found" });
if (!ca) throw new NotFoundError({ message: "CA not found" });
const caSecret = await certificateAuthoritySecretDAL.findOne({ caId: ca.id });

View File

@ -5,22 +5,28 @@ import crypto, { KeyObject } from "crypto";
import ms from "ms";
import { z } from "zod";
import { TCertificateAuthorities, TCertificateTemplates } from "@app/db/schemas";
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service";
import { ProjectPermissionActions, ProjectPermissionSub } from "@app/ee/services/permission/project-permission";
import { BadRequestError } from "@app/lib/errors";
import { BadRequestError, NotFoundError } from "@app/lib/errors";
import { TCertificateBodyDALFactory } from "@app/services/certificate/certificate-body-dal";
import { TCertificateDALFactory } from "@app/services/certificate/certificate-dal";
import { TKmsServiceFactory } from "@app/services/kms/kms-service";
import { TPkiCollectionDALFactory } from "@app/services/pki-collection/pki-collection-dal";
import { TPkiCollectionItemDALFactory } from "@app/services/pki-collection/pki-collection-item-dal";
import { TProjectDALFactory } from "@app/services/project/project-dal";
import { getProjectKmsCertificateKeyId } from "@app/services/project/project-fns";
import { TCertificateAuthorityCrlDALFactory } from "../../ee/services/certificate-authority-crl/certificate-authority-crl-dal";
import { CertKeyAlgorithm, CertStatus } from "../certificate/certificate-types";
import { TCertificateTemplateDALFactory } from "../certificate-template/certificate-template-dal";
import { validateCertificateDetailsAgainstTemplate } from "../certificate-template/certificate-template-fns";
import { TCertificateAuthorityCertDALFactory } from "./certificate-authority-cert-dal";
import { TCertificateAuthorityDALFactory } from "./certificate-authority-dal";
import {
createDistinguishedName,
getCaCertChain,
getCaCertChain, // TODO: consider rename
getCaCertChains,
getCaCredentials,
keyAlgorithmToAlgCfg,
parseDistinguishedName
@ -33,10 +39,12 @@ import {
TCreateCaDTO,
TDeleteCaDTO,
TGetCaCertDTO,
TGetCaCertsDTO,
TGetCaCsrDTO,
TGetCaDTO,
TImportCertToCaDTO,
TIssueCertFromCaDTO,
TRenewCaCertDTO,
TSignCertFromCaDTO,
TSignIntermediateDTO,
TUpdateCaDTO
@ -48,12 +56,18 @@ type TCertificateAuthorityServiceFactoryDep = {
TCertificateAuthorityDALFactory,
"transaction" | "create" | "findById" | "updateById" | "deleteById" | "findOne"
>;
certificateAuthorityCertDAL: Pick<TCertificateAuthorityCertDALFactory, "create" | "findOne" | "transaction">;
certificateAuthorityCertDAL: Pick<
TCertificateAuthorityCertDALFactory,
"create" | "findOne" | "transaction" | "find" | "findById"
>;
certificateAuthoritySecretDAL: Pick<TCertificateAuthoritySecretDALFactory, "create" | "findOne">;
certificateAuthorityCrlDAL: Pick<TCertificateAuthorityCrlDALFactory, "create" | "findOne" | "update">;
certificateTemplateDAL: Pick<TCertificateTemplateDALFactory, "getById">;
certificateAuthorityQueue: TCertificateAuthorityQueueFactory; // TODO: Pick
certificateDAL: Pick<TCertificateDALFactory, "transaction" | "create" | "find">;
certificateBodyDAL: Pick<TCertificateBodyDALFactory, "create">;
pkiCollectionDAL: Pick<TPkiCollectionDALFactory, "findById">;
pkiCollectionItemDAL: Pick<TPkiCollectionItemDALFactory, "create">;
projectDAL: Pick<TProjectDALFactory, "findProjectBySlug" | "findOne" | "updateById" | "findById" | "transaction">;
kmsService: Pick<TKmsServiceFactory, "generateKmsKey" | "encryptWithKmsKey" | "decryptWithKmsKey">;
permissionService: Pick<TPermissionServiceFactory, "getProjectPermission">;
@ -66,8 +80,11 @@ export const certificateAuthorityServiceFactory = ({
certificateAuthorityCertDAL,
certificateAuthoritySecretDAL,
certificateAuthorityCrlDAL,
certificateTemplateDAL,
certificateDAL,
certificateBodyDAL,
pkiCollectionDAL,
pkiCollectionItemDAL,
projectDAL,
kmsService,
permissionService
@ -165,6 +182,24 @@ export const certificateAuthorityServiceFactory = ({
kmsId: certificateManagerKmsId
});
// https://nodejs.org/api/crypto.html#static-method-keyobjectfromkey
const skObj = KeyObject.from(keys.privateKey);
const { cipherTextBlob: encryptedPrivateKey } = await kmsEncryptor({
plainText: skObj.export({
type: "pkcs8",
format: "der"
})
});
const caSecret = await certificateAuthoritySecretDAL.create(
{
caId: ca.id,
encryptedPrivateKey
},
tx
);
if (type === CaType.ROOT) {
// note: create self-signed cert only applicable for root CA
const cert = await x509.X509CertificateGenerator.createSelfSigned({
@ -191,11 +226,21 @@ export const certificateAuthorityServiceFactory = ({
plainText: Buffer.alloc(0)
});
await certificateAuthorityCertDAL.create(
const caCert = await certificateAuthorityCertDAL.create(
{
caId: ca.id,
encryptedCertificate,
encryptedCertificateChain
encryptedCertificateChain,
version: 1,
caSecretId: caSecret.id
},
tx
);
await certificateAuthorityDAL.updateById(
ca.id,
{
activeCaCertId: caCert.id
},
tx
);
@ -223,24 +268,6 @@ export const certificateAuthorityServiceFactory = ({
tx
);
// https://nodejs.org/api/crypto.html#static-method-keyobjectfromkey
const skObj = KeyObject.from(keys.privateKey);
const { cipherTextBlob: encryptedPrivateKey } = await kmsEncryptor({
plainText: skObj.export({
type: "pkcs8",
format: "der"
})
});
await certificateAuthoritySecretDAL.create(
{
caId: ca.id,
encryptedPrivateKey
},
tx
);
return ca;
});
@ -342,9 +369,6 @@ export const certificateAuthorityServiceFactory = ({
if (ca.type === CaType.ROOT) throw new BadRequestError({ message: "Root CA cannot generate CSR" });
const caCert = await certificateAuthorityCertDAL.findOne({ caId: ca.id });
if (caCert) throw new BadRequestError({ message: "CA already has a certificate installed" });
const { caPrivateKey, caPublicKey } = await getCaCredentials({
caId,
certificateAuthorityDAL,
@ -381,9 +405,284 @@ export const certificateAuthorityServiceFactory = ({
};
/**
* Return certificate and certificate chain for CA
* Renew certificate for CA with id [caId]
* Note 1: This CA renewal method is only applicable to CAs with internal parent CAs
* Note 2: Currently implements CA renewal with same key-pair only
*/
const getCaCert = async ({ caId, actorId, actorAuthMethod, actor, actorOrgId }: TGetCaCertDTO) => {
const renewCaCert = async ({ caId, notAfter, actorId, actorAuthMethod, actor, actorOrgId }: TRenewCaCertDTO) => {
const ca = await certificateAuthorityDAL.findById(caId);
if (!ca) throw new BadRequestError({ message: "CA not found" });
if (!ca.activeCaCertId) throw new BadRequestError({ message: "CA does not have a certificate installed" });
const { permission } = await permissionService.getProjectPermission(
actor,
actorId,
ca.projectId,
actorAuthMethod,
actorOrgId
);
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Create,
ProjectPermissionSub.CertificateAuthorities
);
if (ca.status === CaStatus.DISABLED) throw new BadRequestError({ message: "CA is disabled" });
// get latest CA certificate
const caCert = await certificateAuthorityCertDAL.findById(ca.activeCaCertId);
const serialNumber = crypto.randomBytes(32).toString("hex");
const certificateManagerKmsId = await getProjectKmsCertificateKeyId({
projectId: ca.projectId,
projectDAL,
kmsService
});
const kmsEncryptor = await kmsService.encryptWithKmsKey({
kmsId: certificateManagerKmsId
});
const { caPrivateKey, caPublicKey, caSecret } = await getCaCredentials({
caId: ca.id,
certificateAuthorityDAL,
certificateAuthoritySecretDAL,
projectDAL,
kmsService
});
const alg = keyAlgorithmToAlgCfg(ca.keyAlgorithm as CertKeyAlgorithm);
const kmsDecryptor = await kmsService.decryptWithKmsKey({
kmsId: certificateManagerKmsId
});
const decryptedCaCert = await kmsDecryptor({
cipherTextBlob: caCert.encryptedCertificate
});
const caCertObj = new x509.X509Certificate(decryptedCaCert);
let certificate = "";
let certificateChain = "";
switch (ca.type) {
case CaType.ROOT: {
if (new Date(notAfter) <= new Date(caCertObj.notAfter)) {
throw new BadRequestError({
message:
"New Root CA certificate must have notAfter date that is greater than the current certificate notAfter date"
});
}
const notBeforeDate = new Date();
const cert = await x509.X509CertificateGenerator.createSelfSigned({
name: ca.dn,
serialNumber,
notBefore: notBeforeDate,
notAfter: new Date(notAfter),
signingAlgorithm: alg,
keys: {
privateKey: caPrivateKey,
publicKey: caPublicKey
},
extensions: [
new x509.BasicConstraintsExtension(
true,
ca.maxPathLength === -1 || !ca.maxPathLength ? undefined : ca.maxPathLength,
true
),
new x509.ExtendedKeyUsageExtension(["1.2.3.4.5.6.7", "2.3.4.5.6.7.8"], true),
// eslint-disable-next-line no-bitwise
new x509.KeyUsagesExtension(x509.KeyUsageFlags.keyCertSign | x509.KeyUsageFlags.cRLSign, true),
await x509.SubjectKeyIdentifierExtension.create(caPublicKey)
]
});
const { cipherTextBlob: encryptedCertificate } = await kmsEncryptor({
plainText: Buffer.from(new Uint8Array(cert.rawData))
});
const { cipherTextBlob: encryptedCertificateChain } = await kmsEncryptor({
plainText: Buffer.alloc(0)
});
await certificateAuthorityDAL.transaction(async (tx) => {
const newCaCert = await certificateAuthorityCertDAL.create(
{
caId: ca.id,
encryptedCertificate,
encryptedCertificateChain,
version: caCert.version + 1,
caSecretId: caSecret.id
},
tx
);
await certificateAuthorityDAL.updateById(
ca.id,
{
activeCaCertId: newCaCert.id,
notBefore: notBeforeDate,
notAfter: new Date(notAfter)
},
tx
);
});
certificate = cert.toString("pem");
break;
}
case CaType.INTERMEDIATE: {
if (!ca.parentCaId) {
// TODO: look into optimal way to support renewal of intermediate CA with external parent CA
throw new BadRequestError({
message: "Failed to renew intermediate CA certificate with external parent CA"
});
}
const parentCa = await certificateAuthorityDAL.findById(ca.parentCaId);
const { caPrivateKey: parentCaPrivateKey } = await getCaCredentials({
caId: parentCa.id,
certificateAuthorityDAL,
certificateAuthoritySecretDAL,
projectDAL,
kmsService
});
// get latest parent CA certificate
if (!parentCa.activeCaCertId)
throw new BadRequestError({ message: "Parent CA does not have a certificate installed" });
const parentCaCert = await certificateAuthorityCertDAL.findById(parentCa.activeCaCertId);
const decryptedParentCaCert = await kmsDecryptor({
cipherTextBlob: parentCaCert.encryptedCertificate
});
const parentCaCertObj = new x509.X509Certificate(decryptedParentCaCert);
if (new Date(notAfter) <= new Date(caCertObj.notAfter)) {
throw new BadRequestError({
message:
"New Intermediate CA certificate must have notAfter date that is greater than the current certificate notAfter date"
});
}
if (new Date(notAfter) > new Date(parentCaCertObj.notAfter)) {
throw new BadRequestError({
message:
"New Intermediate CA certificate must have notAfter date that is equal to or smaller than the notAfter date of the parent CA certificate current certificate notAfter date"
});
}
const csrObj = await x509.Pkcs10CertificateRequestGenerator.create({
name: ca.dn,
keys: {
privateKey: caPrivateKey,
publicKey: caPublicKey
},
signingAlgorithm: alg,
extensions: [
// eslint-disable-next-line no-bitwise
new x509.KeyUsagesExtension(
x509.KeyUsageFlags.keyCertSign |
x509.KeyUsageFlags.cRLSign |
x509.KeyUsageFlags.digitalSignature |
x509.KeyUsageFlags.keyEncipherment
)
],
attributes: [new x509.ChallengePasswordAttribute("password")]
});
const notBeforeDate = new Date();
const intermediateCert = await x509.X509CertificateGenerator.create({
serialNumber,
subject: csrObj.subject,
issuer: parentCaCertObj.subject,
notBefore: notBeforeDate,
notAfter: new Date(notAfter),
signingKey: parentCaPrivateKey,
publicKey: csrObj.publicKey,
signingAlgorithm: alg,
extensions: [
new x509.KeyUsagesExtension(
x509.KeyUsageFlags.keyCertSign |
x509.KeyUsageFlags.cRLSign |
x509.KeyUsageFlags.digitalSignature |
x509.KeyUsageFlags.keyEncipherment,
true
),
new x509.BasicConstraintsExtension(
true,
ca.maxPathLength === -1 || !ca.maxPathLength ? undefined : ca.maxPathLength,
true
),
await x509.AuthorityKeyIdentifierExtension.create(parentCaCertObj, false),
await x509.SubjectKeyIdentifierExtension.create(csrObj.publicKey)
]
});
const { cipherTextBlob: encryptedCertificate } = await kmsEncryptor({
plainText: Buffer.from(new Uint8Array(intermediateCert.rawData))
});
const { caCert: parentCaCertificate, caCertChain: parentCaCertChain } = await getCaCertChain({
caCertId: parentCa.activeCaCertId,
certificateAuthorityDAL,
certificateAuthorityCertDAL,
projectDAL,
kmsService
});
certificateChain = `${parentCaCertificate}\n${parentCaCertChain}`.trim();
const { cipherTextBlob: encryptedCertificateChain } = await kmsEncryptor({
plainText: Buffer.from(certificateChain)
});
await certificateAuthorityDAL.transaction(async (tx) => {
const newCaCert = await certificateAuthorityCertDAL.create(
{
caId: ca.id,
encryptedCertificate,
encryptedCertificateChain,
version: caCert.version + 1,
caSecretId: caSecret.id
},
tx
);
await certificateAuthorityDAL.updateById(
ca.id,
{
activeCaCertId: newCaCert.id,
notBefore: notBeforeDate,
notAfter: new Date(notAfter)
},
tx
);
});
certificate = intermediateCert.toString("pem");
break;
}
default: {
throw new BadRequestError({
message: "Unrecognized CA type"
});
}
}
return {
certificate,
certificateChain,
serialNumber,
ca
};
};
const getCaCerts = async ({ caId, actorId, actorAuthMethod, actor, actorOrgId }: TGetCaCertsDTO) => {
const ca = await certificateAuthorityDAL.findById(caId);
if (!ca) throw new BadRequestError({ message: "CA not found" });
@ -400,7 +699,7 @@ export const certificateAuthorityServiceFactory = ({
ProjectPermissionSub.CertificateAuthorities
);
const { caCert, caCertChain, serialNumber } = await getCaCertChain({
const caCertChains = await getCaCertChains({
caId,
certificateAuthorityDAL,
certificateAuthorityCertDAL,
@ -408,6 +707,41 @@ export const certificateAuthorityServiceFactory = ({
kmsService
});
return {
ca,
caCerts: caCertChains
};
};
/**
* Return current certificate and certificate chain for CA
*/
const getCaCert = async ({ caId, actorId, actorAuthMethod, actor, actorOrgId }: TGetCaCertDTO) => {
const ca = await certificateAuthorityDAL.findById(caId);
if (!ca) throw new BadRequestError({ message: "CA not found" });
if (!ca.activeCaCertId) throw new BadRequestError({ message: "CA does not have a certificate installed" });
const { permission } = await permissionService.getProjectPermission(
actor,
actorId,
ca.projectId,
actorAuthMethod,
actorOrgId
);
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Read,
ProjectPermissionSub.CertificateAuthorities
);
const { caCert, caCertChain, serialNumber } = await getCaCertChain({
caCertId: ca.activeCaCertId,
certificateAuthorityDAL,
certificateAuthorityCertDAL,
projectDAL,
kmsService
});
return {
certificate: caCert,
certificateChain: caCertChain,
@ -447,6 +781,13 @@ export const certificateAuthorityServiceFactory = ({
);
if (ca.status === CaStatus.DISABLED) throw new BadRequestError({ message: "CA is disabled" });
if (!ca.activeCaCertId) throw new BadRequestError({ message: "CA does not have a certificate installed" });
const caCert = await certificateAuthorityCertDAL.findById(ca.activeCaCertId);
if (ca.notAfter && new Date() > new Date(ca.notAfter)) {
throw new BadRequestError({ message: "CA is expired" });
}
const alg = keyAlgorithmToAlgCfg(ca.keyAlgorithm as CertKeyAlgorithm);
@ -459,7 +800,6 @@ export const certificateAuthorityServiceFactory = ({
kmsId: certificateManagerKmsId
});
const caCert = await certificateAuthorityCertDAL.findOne({ caId: ca.id });
const decryptedCaCert = await kmsDecryptor({
cipherTextBlob: caCert.encryptedCertificate
});
@ -531,7 +871,7 @@ export const certificateAuthorityServiceFactory = ({
});
const { caCert: issuingCaCertificate, caCertChain } = await getCaCertChain({
caId,
caCertId: ca.activeCaCertId,
certificateAuthorityDAL,
certificateAuthorityCertDAL,
projectDAL,
@ -548,9 +888,9 @@ export const certificateAuthorityServiceFactory = ({
};
/**
* Import certificate for (un-installed) CA with id [caId].
* Import certificate for CA with id [caId].
* Note: Can be used to import an external certificate and certificate chain
* to be installed into the CA.
* to be into an installed or uninstalled CA.
*/
const importCertToCa = async ({
caId,
@ -577,8 +917,18 @@ export const certificateAuthorityServiceFactory = ({
ProjectPermissionSub.CertificateAuthorities
);
const caCert = await certificateAuthorityCertDAL.findOne({ caId: ca.id });
if (caCert) throw new BadRequestError({ message: "CA has already imported a certificate" });
if (ca.parentCaId) {
/**
* re-evaluate in the future if we should allow users to import a new CA certificate for an intermediate
* CA chained to an internal parent CA. Doing so would allow users to re-chain the CA to a different
* internal CA.
*/
throw new BadRequestError({
message: "Cannot import certificate to intermediate CA chained to internal parent CA"
});
}
const caCert = ca.activeCaCertId ? await certificateAuthorityCertDAL.findById(ca.activeCaCertId) : undefined;
const certObj = new x509.X509Certificate(certificate);
const maxPathLength = certObj.getExtension(x509.BasicConstraintsExtension)?.pathLength;
@ -625,12 +975,32 @@ export const certificateAuthorityServiceFactory = ({
plainText: Buffer.from(certificateChain)
});
// TODO: validate that latest key-pair of CA is used to sign the certificate
// once renewal with new key pair is supported
const { caSecret, caPublicKey } = await getCaCredentials({
caId: ca.id,
certificateAuthorityDAL,
certificateAuthoritySecretDAL,
projectDAL,
kmsService
});
const isCaAndCertPublicKeySame = Buffer.from(await crypto.subtle.exportKey("spki", caPublicKey)).equals(
Buffer.from(certObj.publicKey.rawData)
);
if (!isCaAndCertPublicKeySame) {
throw new BadRequestError({ message: "CA and certificate public key do not match" });
}
await certificateAuthorityCertDAL.transaction(async (tx) => {
await certificateAuthorityCertDAL.create(
const newCaCert = await certificateAuthorityCertDAL.create(
{
caId: ca.id,
encryptedCertificate,
encryptedCertificateChain
encryptedCertificateChain,
version: caCert ? caCert.version + 1 : 1,
caSecretId: caSecret.id
},
tx
);
@ -643,7 +1013,8 @@ export const certificateAuthorityServiceFactory = ({
notBefore: new Date(certObj.notBefore),
notAfter: new Date(certObj.notAfter),
serialNumber: certObj.serialNumber,
parentCaId: parentCa?.id
parentCaId: parentCa?.id,
activeCaCertId: newCaCert.id
},
tx
);
@ -658,6 +1029,8 @@ export const certificateAuthorityServiceFactory = ({
*/
const issueCertFromCa = async ({
caId,
certificateTemplateId,
pkiCollectionId,
friendlyName,
commonName,
altNames,
@ -669,8 +1042,27 @@ export const certificateAuthorityServiceFactory = ({
actor,
actorOrgId
}: TIssueCertFromCaDTO) => {
const ca = await certificateAuthorityDAL.findById(caId);
if (!ca) throw new BadRequestError({ message: "CA not found" });
let ca: TCertificateAuthorities | undefined;
let certificateTemplate: TCertificateTemplates | undefined;
let collectionId = pkiCollectionId;
if (caId) {
ca = await certificateAuthorityDAL.findById(caId);
} else if (certificateTemplateId) {
certificateTemplate = await certificateTemplateDAL.getById(certificateTemplateId);
if (!certificateTemplate) {
throw new NotFoundError({
message: "Certificate template not found"
});
}
collectionId = certificateTemplate.pkiCollectionId as string;
ca = await certificateAuthorityDAL.findById(certificateTemplate.caId);
}
if (!ca) {
throw new BadRequestError({ message: "CA not found" });
}
const { permission } = await permissionService.getProjectPermission(
actor,
@ -683,9 +1075,19 @@ export const certificateAuthorityServiceFactory = ({
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Create, ProjectPermissionSub.Certificates);
if (ca.status === CaStatus.DISABLED) throw new BadRequestError({ message: "CA is disabled" });
if (!ca.activeCaCertId) throw new BadRequestError({ message: "CA does not have a certificate installed" });
const caCert = await certificateAuthorityCertDAL.findById(ca.activeCaCertId);
const caCert = await certificateAuthorityCertDAL.findOne({ caId: ca.id });
if (!caCert) throw new BadRequestError({ message: "CA does not have a certificate installed" });
if (ca.notAfter && new Date() > new Date(ca.notAfter)) {
throw new BadRequestError({ message: "CA is expired" });
}
// check PKI collection
if (collectionId) {
const pkiCollection = await pkiCollectionDAL.findById(collectionId);
if (!pkiCollection) throw new NotFoundError({ message: "PKI collection not found" });
if (pkiCollection.projectId !== ca.projectId) throw new BadRequestError({ message: "Invalid PKI collection" });
}
const certificateManagerKmsId = await getProjectKmsCertificateKeyId({
projectId: ca.projectId,
@ -755,11 +1157,13 @@ export const certificateAuthorityServiceFactory = ({
await x509.SubjectKeyIdentifierExtension.create(csrObj.publicKey)
];
let altNamesArray: {
type: "email" | "dns";
value: string;
}[] = [];
if (altNames) {
const altNamesArray: {
type: "email" | "dns";
value: string;
}[] = altNames
altNamesArray = altNames
.split(",")
.map((name) => name.trim())
.map((altName) => {
@ -787,6 +1191,18 @@ export const certificateAuthorityServiceFactory = ({
extensions.push(altNamesExtension);
}
if (certificateTemplate) {
validateCertificateDetailsAgainstTemplate(
{
commonName,
notBeforeDate,
notAfterDate,
altNames: altNamesArray.map((entry) => entry.value)
},
certificateTemplate
);
}
const serialNumber = crypto.randomBytes(32).toString("hex");
const leafCert = await x509.X509CertificateGenerator.create({
serialNumber,
@ -813,7 +1229,9 @@ export const certificateAuthorityServiceFactory = ({
await certificateDAL.transaction(async (tx) => {
const cert = await certificateDAL.create(
{
caId: ca.id,
caId: (ca as TCertificateAuthorities).id,
caCertId: caCert.id,
certificateTemplateId: certificateTemplate?.id,
status: CertStatus.ACTIVE,
friendlyName: friendlyName || commonName,
commonName,
@ -833,11 +1251,21 @@ export const certificateAuthorityServiceFactory = ({
tx
);
if (collectionId) {
await pkiCollectionItemDAL.create(
{
pkiCollectionId: collectionId,
certId: cert.id
},
tx
);
}
return cert;
});
const { caCert: issuingCaCertificate, caCertChain } = await getCaCertChain({
caId: ca.id,
caCertId: caCert.id,
certificateAuthorityDAL,
certificateAuthorityCertDAL,
projectDAL,
@ -860,7 +1288,9 @@ export const certificateAuthorityServiceFactory = ({
*/
const signCertFromCa = async ({
caId,
certificateTemplateId,
csr,
pkiCollectionId,
friendlyName,
commonName,
altNames,
@ -872,8 +1302,27 @@ export const certificateAuthorityServiceFactory = ({
actor,
actorOrgId
}: TSignCertFromCaDTO) => {
const ca = await certificateAuthorityDAL.findById(caId);
if (!ca) throw new BadRequestError({ message: "CA not found" });
let ca: TCertificateAuthorities | undefined;
let certificateTemplate: TCertificateTemplates | undefined;
let collectionId = pkiCollectionId;
if (caId) {
ca = await certificateAuthorityDAL.findById(caId);
} else if (certificateTemplateId) {
certificateTemplate = await certificateTemplateDAL.getById(certificateTemplateId);
if (!certificateTemplate) {
throw new NotFoundError({
message: "Certificate template not found"
});
}
collectionId = certificateTemplate.pkiCollectionId as string;
ca = await certificateAuthorityDAL.findById(certificateTemplate.caId);
}
if (!ca) {
throw new BadRequestError({ message: "CA not found" });
}
const { permission } = await permissionService.getProjectPermission(
actor,
@ -886,9 +1335,20 @@ export const certificateAuthorityServiceFactory = ({
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Create, ProjectPermissionSub.Certificates);
if (ca.status === CaStatus.DISABLED) throw new BadRequestError({ message: "CA is disabled" });
if (!ca.activeCaCertId) throw new BadRequestError({ message: "CA does not have a certificate installed" });
const caCert = await certificateAuthorityCertDAL.findOne({ caId: ca.id });
if (!caCert) throw new BadRequestError({ message: "CA does not have a certificate installed" });
const caCert = await certificateAuthorityCertDAL.findById(ca.activeCaCertId);
if (ca.notAfter && new Date() > new Date(ca.notAfter)) {
throw new BadRequestError({ message: "CA is expired" });
}
// check PKI collection
if (pkiCollectionId) {
const pkiCollection = await pkiCollectionDAL.findById(pkiCollectionId);
if (!pkiCollection) throw new NotFoundError({ message: "PKI collection not found" });
if (pkiCollection.projectId !== ca.projectId) throw new BadRequestError({ message: "Invalid PKI collection" });
}
const certificateManagerKmsId = await getProjectKmsCertificateKeyId({
projectId: ca.projectId,
@ -957,11 +1417,12 @@ export const certificateAuthorityServiceFactory = ({
await x509.SubjectKeyIdentifierExtension.create(csrObj.publicKey)
];
let altNamesArray: {
type: "email" | "dns";
value: string;
}[] = [];
if (altNames) {
const altNamesArray: {
type: "email" | "dns";
value: string;
}[] = altNames
altNamesArray = altNames
.split(",")
.map((name) => name.trim())
.map((altName) => {
@ -989,6 +1450,18 @@ export const certificateAuthorityServiceFactory = ({
extensions.push(altNamesExtension);
}
if (certificateTemplate) {
validateCertificateDetailsAgainstTemplate(
{
commonName: cn,
notBeforeDate,
notAfterDate,
altNames: altNamesArray.map((entry) => entry.value)
},
certificateTemplate
);
}
const serialNumber = crypto.randomBytes(32).toString("hex");
const leafCert = await x509.X509CertificateGenerator.create({
serialNumber,
@ -1012,7 +1485,9 @@ export const certificateAuthorityServiceFactory = ({
await certificateDAL.transaction(async (tx) => {
const cert = await certificateDAL.create(
{
caId: ca.id,
caId: (ca as TCertificateAuthorities).id,
caCertId: caCert.id,
certificateTemplateId: certificateTemplate?.id,
status: CertStatus.ACTIVE,
friendlyName: friendlyName || csrObj.subject,
commonName: cn,
@ -1032,11 +1507,21 @@ export const certificateAuthorityServiceFactory = ({
tx
);
if (collectionId) {
await pkiCollectionItemDAL.create(
{
pkiCollectionId: collectionId,
certId: cert.id
},
tx
);
}
return cert;
});
const { caCert: issuingCaCertificate, caCertChain } = await getCaCertChain({
caId: ca.id,
caCertId: ca.activeCaCertId,
certificateAuthorityDAL,
certificateAuthorityCertDAL,
projectDAL,
@ -1058,6 +1543,8 @@ export const certificateAuthorityServiceFactory = ({
updateCaById,
deleteCaById,
getCaCsr,
renewCaCert,
getCaCerts,
getCaCert,
signIntermediate,
importCertToCa,

View File

@ -20,6 +20,10 @@ export enum CaStatus {
PENDING_CERTIFICATE = "pending-certificate"
}
export enum CaRenewalType {
EXISTING = "existing"
}
export type TCreateCaDTO = {
projectSlug: string;
type: CaType;
@ -53,6 +57,16 @@ export type TGetCaCsrDTO = {
caId: string;
} & Omit<TProjectPermission, "projectId">;
export type TRenewCaCertDTO = {
caId: string;
notAfter: string;
type: CaRenewalType;
} & Omit<TProjectPermission, "projectId">;
export type TGetCaCertsDTO = {
caId: string;
} & Omit<TProjectPermission, "projectId">;
export type TGetCaCertDTO = {
caId: string;
} & Omit<TProjectPermission, "projectId">;
@ -72,7 +86,9 @@ export type TImportCertToCaDTO = {
} & Omit<TProjectPermission, "projectId">;
export type TIssueCertFromCaDTO = {
caId: string;
caId?: string;
certificateTemplateId?: string;
pkiCollectionId?: string;
friendlyName?: string;
commonName: string;
altNames: string;
@ -82,8 +98,10 @@ export type TIssueCertFromCaDTO = {
} & Omit<TProjectPermission, "projectId">;
export type TSignCertFromCaDTO = {
caId: string;
caId?: string;
csr: string;
certificateTemplateId?: string;
pkiCollectionId?: string;
friendlyName?: string;
commonName?: string;
altNames: string;
@ -109,10 +127,18 @@ export type TGetCaCredentialsDTO = {
kmsService: Pick<TKmsServiceFactory, "decryptWithKmsKey" | "generateKmsKey">;
};
export type TGetCaCertChainDTO = {
export type TGetCaCertChainsDTO = {
caId: string;
certificateAuthorityDAL: Pick<TCertificateAuthorityDALFactory, "findById">;
certificateAuthorityCertDAL: Pick<TCertificateAuthorityCertDALFactory, "findOne">;
certificateAuthorityCertDAL: Pick<TCertificateAuthorityCertDALFactory, "find">;
projectDAL: Pick<TProjectDALFactory, "findOne" | "updateById" | "transaction">;
kmsService: Pick<TKmsServiceFactory, "decryptWithKmsKey" | "generateKmsKey">;
};
export type TGetCaCertChainDTO = {
caCertId: string;
certificateAuthorityDAL: Pick<TCertificateAuthorityDALFactory, "findById">;
certificateAuthorityCertDAL: Pick<TCertificateAuthorityCertDALFactory, "findById">;
projectDAL: Pick<TProjectDALFactory, "findOne" | "updateById" | "transaction">;
kmsService: Pick<TKmsServiceFactory, "decryptWithKmsKey" | "generateKmsKey">;
};

View File

@ -0,0 +1,57 @@
import { TDbClient } from "@app/db";
import { TableName } from "@app/db/schemas";
import { DatabaseError } from "@app/lib/errors";
import { ormify, selectAllTableCols } from "@app/lib/knex";
export type TCertificateTemplateDALFactory = ReturnType<typeof certificateTemplateDALFactory>;
export const certificateTemplateDALFactory = (db: TDbClient) => {
const certificateTemplateOrm = ormify(db, TableName.CertificateTemplate);
const getCertTemplatesByProjectId = async (projectId: string) => {
try {
const certTemplates = await db
.replicaNode()(TableName.CertificateTemplate)
.join(
TableName.CertificateAuthority,
`${TableName.CertificateAuthority}.id`,
`${TableName.CertificateTemplate}.caId`
)
.where(`${TableName.CertificateAuthority}.projectId`, "=", projectId)
.select(selectAllTableCols(TableName.CertificateTemplate))
.select(
db.ref("friendlyName").as("caName").withSchema(TableName.CertificateAuthority),
db.ref("projectId").withSchema(TableName.CertificateAuthority)
);
return certTemplates;
} catch (error) {
throw new DatabaseError({ error, name: "Get certificate templates by project ID" });
}
};
const getById = async (id: string) => {
try {
const certTemplate = await db
.replicaNode()(TableName.CertificateTemplate)
.join(
TableName.CertificateAuthority,
`${TableName.CertificateAuthority}.id`,
`${TableName.CertificateTemplate}.caId`
)
.where(`${TableName.CertificateTemplate}.id`, "=", id)
.select(selectAllTableCols(TableName.CertificateTemplate))
.select(
db.ref("projectId").withSchema(TableName.CertificateAuthority),
db.ref("friendlyName").as("caName").withSchema(TableName.CertificateAuthority)
)
.first();
return certTemplate;
} catch (error) {
throw new DatabaseError({ error, name: "Get certificate template by ID" });
}
};
return { ...certificateTemplateOrm, getCertTemplatesByProjectId, getById };
};

View File

@ -0,0 +1,36 @@
import ms from "ms";
import { TCertificateTemplates } from "@app/db/schemas";
import { BadRequestError } from "@app/lib/errors";
export const validateCertificateDetailsAgainstTemplate = (
cert: {
commonName: string;
notBeforeDate: Date;
notAfterDate: Date;
altNames: string[];
},
template: TCertificateTemplates
) => {
const commonNameRegex = new RegExp(template.commonName);
if (!commonNameRegex.test(cert.commonName)) {
throw new BadRequestError({
message: "Invalid common name based on template policy"
});
}
if (cert.notAfterDate.getTime() - cert.notBeforeDate.getTime() > ms(template.ttl)) {
throw new BadRequestError({
message: "Invalid validity date based on template policy"
});
}
const subjectAlternativeNameRegex = new RegExp(template.subjectAlternativeName);
cert.altNames.forEach((altName) => {
if (!subjectAlternativeNameRegex.test(altName)) {
throw new BadRequestError({
message: "Invalid subject alternative name based on template policy"
});
}
});
};

View File

@ -0,0 +1,18 @@
import z from "zod";
import { CertificateTemplatesSchema } from "@app/db/schemas";
export const sanitizedCertificateTemplate = CertificateTemplatesSchema.pick({
id: true,
caId: true,
name: true,
commonName: true,
subjectAlternativeName: true,
pkiCollectionId: true,
ttl: true
}).merge(
z.object({
projectId: z.string(),
caName: z.string()
})
);

View File

@ -0,0 +1,196 @@
import { ForbiddenError } from "@casl/ability";
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service";
import { ProjectPermissionActions, ProjectPermissionSub } from "@app/ee/services/permission/project-permission";
import { BadRequestError, NotFoundError } from "@app/lib/errors";
import { TCertificateAuthorityDALFactory } from "../certificate-authority/certificate-authority-dal";
import { TCertificateTemplateDALFactory } from "./certificate-template-dal";
import {
TCreateCertTemplateDTO,
TDeleteCertTemplateDTO,
TGetCertTemplateDTO,
TUpdateCertTemplateDTO
} from "./certificate-template-types";
type TCertificateTemplateServiceFactoryDep = {
certificateTemplateDAL: TCertificateTemplateDALFactory;
certificateAuthorityDAL: Pick<TCertificateAuthorityDALFactory, "findById">;
permissionService: Pick<TPermissionServiceFactory, "getProjectPermission">;
};
export type TCertificateTemplateServiceFactory = ReturnType<typeof certificateTemplateServiceFactory>;
export const certificateTemplateServiceFactory = ({
certificateTemplateDAL,
certificateAuthorityDAL,
permissionService
}: TCertificateTemplateServiceFactoryDep) => {
const createCertTemplate = async ({
caId,
pkiCollectionId,
name,
commonName,
subjectAlternativeName,
ttl,
actorId,
actorAuthMethod,
actor,
actorOrgId
}: TCreateCertTemplateDTO) => {
const ca = await certificateAuthorityDAL.findById(caId);
if (!ca) {
throw new NotFoundError({
message: "CA not found"
});
}
const { permission } = await permissionService.getProjectPermission(
actor,
actorId,
ca.projectId,
actorAuthMethod,
actorOrgId
);
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Create,
ProjectPermissionSub.CertificateTemplates
);
const { id } = await certificateTemplateDAL.create({
caId,
pkiCollectionId,
name,
commonName,
subjectAlternativeName,
ttl
});
const certificateTemplate = await certificateTemplateDAL.getById(id);
if (!certificateTemplate) {
throw new NotFoundError({
message: "Certificate template not found"
});
}
return certificateTemplate;
};
const updateCertTemplate = async ({
id,
caId,
pkiCollectionId,
name,
commonName,
subjectAlternativeName,
ttl,
actorId,
actorAuthMethod,
actor,
actorOrgId
}: TUpdateCertTemplateDTO) => {
const certTemplate = await certificateTemplateDAL.getById(id);
if (!certTemplate) {
throw new NotFoundError({
message: "Certificate template not found."
});
}
const { permission } = await permissionService.getProjectPermission(
actor,
actorId,
certTemplate.projectId,
actorAuthMethod,
actorOrgId
);
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Edit,
ProjectPermissionSub.CertificateTemplates
);
if (caId) {
const ca = await certificateAuthorityDAL.findById(caId);
if (!ca || ca.projectId !== certTemplate.projectId) {
throw new BadRequestError({
message: "Invalid CA"
});
}
}
await certificateTemplateDAL.updateById(certTemplate.id, {
caId,
pkiCollectionId,
commonName,
subjectAlternativeName,
name,
ttl
});
const updatedTemplate = await certificateTemplateDAL.getById(id);
if (!updatedTemplate) {
throw new NotFoundError({
message: "Certificate template not found"
});
}
return updatedTemplate;
};
const deleteCertTemplate = async ({ id, actorId, actorAuthMethod, actor, actorOrgId }: TDeleteCertTemplateDTO) => {
const certTemplate = await certificateTemplateDAL.getById(id);
if (!certTemplate) {
throw new NotFoundError({
message: "Certificate template not found."
});
}
const { permission } = await permissionService.getProjectPermission(
actor,
actorId,
certTemplate.projectId,
actorAuthMethod,
actorOrgId
);
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Delete,
ProjectPermissionSub.CertificateTemplates
);
await certificateTemplateDAL.deleteById(certTemplate.id);
return certTemplate;
};
const getCertTemplate = async ({ id, actorId, actorAuthMethod, actor, actorOrgId }: TGetCertTemplateDTO) => {
const certTemplate = await certificateTemplateDAL.getById(id);
if (!certTemplate) {
throw new NotFoundError({
message: "Certificate template not found."
});
}
const { permission } = await permissionService.getProjectPermission(
actor,
actorId,
certTemplate.projectId,
actorAuthMethod,
actorOrgId
);
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Read,
ProjectPermissionSub.CertificateTemplates
);
return certTemplate;
};
return {
createCertTemplate,
getCertTemplate,
deleteCertTemplate,
updateCertTemplate
};
};

View File

@ -0,0 +1,28 @@
import { TProjectPermission } from "@app/lib/types";
export type TCreateCertTemplateDTO = {
caId: string;
pkiCollectionId?: string;
name: string;
commonName: string;
subjectAlternativeName: string;
ttl: string;
} & Omit<TProjectPermission, "projectId">;
export type TUpdateCertTemplateDTO = {
id: string;
caId?: string;
pkiCollectionId?: string;
name?: string;
commonName?: string;
subjectAlternativeName?: string;
ttl?: string;
} & Omit<TProjectPermission, "projectId">;
export type TGetCertTemplateDTO = {
id: string;
} & Omit<TProjectPermission, "projectId">;
export type TDeleteCertTemplateDTO = {
id: string;
} & Omit<TProjectPermission, "projectId">;

View File

@ -0,0 +1,14 @@
import safe from "safe-regex";
import z from "zod";
export const validateTemplateRegexField = z
.string()
.min(1)
.max(100)
.regex(/^[a-zA-Z0-9 *@\-\\.\\]+$/, {
message: "Invalid pattern: only alphanumeric characters, spaces, *, ., @, -, and \\ are allowed."
})
// we ensure that the inputted pattern is computationally safe by limiting star height to 1
.refine((v) => safe(v), {
message: "Unsafe REGEX pattern"
});

View File

@ -21,7 +21,7 @@ type TCertificateServiceFactoryDep = {
certificateDAL: Pick<TCertificateDALFactory, "findOne" | "deleteById" | "update" | "find">;
certificateBodyDAL: Pick<TCertificateBodyDALFactory, "findOne">;
certificateAuthorityDAL: Pick<TCertificateAuthorityDALFactory, "findById">;
certificateAuthorityCertDAL: Pick<TCertificateAuthorityCertDALFactory, "findOne">;
certificateAuthorityCertDAL: Pick<TCertificateAuthorityCertDALFactory, "findById">;
certificateAuthorityCrlDAL: Pick<TCertificateAuthorityCrlDALFactory, "update">;
certificateAuthoritySecretDAL: Pick<TCertificateAuthoritySecretDALFactory, "findOne">;
projectDAL: Pick<TProjectDALFactory, "findOne" | "updateById" | "findById" | "transaction">;
@ -180,7 +180,7 @@ export const certificateServiceFactory = ({
const certObj = new x509.X509Certificate(decryptedCert);
const { caCert, caCertChain } = await getCaCertChain({
caId: ca.id,
caCertId: cert.caCertId,
certificateAuthorityDAL,
certificateAuthorityCertDAL,
projectDAL,

View File

@ -4,6 +4,7 @@ import { TDbClient } from "@app/db";
import { TableName } from "@app/db/schemas";
import { DatabaseError } from "@app/lib/errors";
import { ormify } from "@app/lib/knex";
import { logger } from "@app/lib/logger";
export type TIdentityUaClientSecretDALFactory = ReturnType<typeof identityUaClientSecretDALFactory>;
@ -23,5 +24,55 @@ export const identityUaClientSecretDALFactory = (db: TDbClient) => {
}
};
return { ...uaClientSecretOrm, incrementUsage };
const removeExpiredClientSecrets = async (tx?: Knex) => {
const BATCH_SIZE = 10000;
const MAX_RETRY_ON_FAILURE = 3;
let deletedClientSecret: { id: string }[] = [];
let numberOfRetryOnFailure = 0;
do {
try {
const findExpiredClientSecretQuery = (tx || db)(TableName.IdentityUaClientSecret)
.where({
isClientSecretRevoked: true
})
.orWhere((qb) => {
void qb
.where("clientSecretNumUses", ">", 0)
.andWhere(
"clientSecretNumUses",
">=",
db.ref("clientSecretNumUsesLimit").withSchema(TableName.IdentityUaClientSecret)
);
})
.orWhere((qb) => {
void qb
.where("clientSecretTTL", ">", 0)
.andWhereRaw(
`"${TableName.IdentityUaClientSecret}"."createdAt" + make_interval(secs => "${TableName.IdentityUaClientSecret}"."clientSecretTTL") < NOW()`
);
})
.select("id")
.limit(BATCH_SIZE);
// eslint-disable-next-line no-await-in-loop
deletedClientSecret = await (tx || db)(TableName.IdentityUaClientSecret)
.whereIn("id", findExpiredClientSecretQuery)
.del()
.returning("id");
numberOfRetryOnFailure = 0; // reset
} catch (error) {
numberOfRetryOnFailure += 1;
logger.error(error, "Failed to delete client secret on pruning");
} finally {
// eslint-disable-next-line no-await-in-loop
await new Promise((resolve) => {
setTimeout(resolve, 10); // time to breathe for db
});
}
} while (deletedClientSecret.length > 0 || numberOfRetryOnFailure < MAX_RETRY_ON_FAILURE);
};
return { ...uaClientSecretOrm, incrementUsage, removeExpiredClientSecrets };
};

View File

@ -0,0 +1,357 @@
import { retry } from "@octokit/plugin-retry";
import { Octokit } from "@octokit/rest";
import { TIntegrationAuths, TIntegrations } from "@app/db/schemas";
import { decryptSymmetric128BitHexKeyUTF8 } from "@app/lib/crypto";
import { BadRequestError, NotFoundError } from "@app/lib/errors";
import { logger } from "@app/lib/logger";
import { IntegrationMetadataSchema } from "../integration/integration-schema";
import { TKmsServiceFactory } from "../kms/kms-service";
import { KmsDataKey } from "../kms/kms-types";
import { TProjectBotServiceFactory } from "../project-bot/project-bot-service";
import { TSecretDALFactory } from "../secret/secret-dal";
import { TSecretFolderDALFactory } from "../secret-folder/secret-folder-dal";
import { TSecretImportDALFactory } from "../secret-import/secret-import-dal";
import { fnSecretsV2FromImports } from "../secret-import/secret-import-fns";
import { TSecretV2BridgeDALFactory } from "../secret-v2-bridge/secret-v2-bridge-dal";
import { TIntegrationAuthServiceFactory } from "./integration-auth-service";
import { Integrations } from "./integration-list";
const MAX_SYNC_SECRET_DEPTH = 5;
/**
* Return the secrets in a given [folderId] including secrets from
* nested imported folders recursively.
*/
const getIntegrationSecretsV2 = async (
dto: {
projectId: string;
environment: string;
folderId: string;
depth: number;
decryptor: (value: Buffer | null | undefined) => string;
},
secretV2BridgeDAL: Pick<TSecretV2BridgeDALFactory, "find" | "findByFolderId">,
folderDAL: Pick<TSecretFolderDALFactory, "findByManySecretPath">,
secretImportDAL: Pick<TSecretImportDALFactory, "find" | "findByFolderIds">
) => {
const content: Record<string, boolean> = {};
if (dto.depth > MAX_SYNC_SECRET_DEPTH) {
logger.info(
`getIntegrationSecrets: secret depth exceeded for [projectId=${dto.projectId}] [folderId=${dto.folderId}] [depth=${dto.depth}]`
);
return content;
}
// process secrets in current folder
const secrets = await secretV2BridgeDAL.findByFolderId(dto.folderId);
secrets.forEach((secret) => {
const secretKey = secret.key;
content[secretKey] = true;
});
// check if current folder has any imports from other folders
const secretImports = await secretImportDAL.find({ folderId: dto.folderId, isReplication: false });
// if no imports then return secrets in the current folder
if (!secretImports.length) return content;
const importedSecrets = await fnSecretsV2FromImports({
decryptor: dto.decryptor,
folderDAL,
secretDAL: secretV2BridgeDAL,
secretImportDAL,
allowedImports: secretImports
});
for (let i = importedSecrets.length - 1; i >= 0; i -= 1) {
for (let j = 0; j < importedSecrets[i].secrets.length; j += 1) {
const importedSecret = importedSecrets[i].secrets[j];
if (!content[importedSecret.key]) {
content[importedSecret.key] = true;
}
}
}
return content;
};
/**
* Return the secrets in a given [folderId] including secrets from
* nested imported folders recursively.
*/
const getIntegrationSecretsV1 = async (
dto: {
projectId: string;
environment: string;
folderId: string;
key: string;
depth: number;
},
secretDAL: Pick<TSecretDALFactory, "findByFolderId">,
folderDAL: Pick<TSecretFolderDALFactory, "findByManySecretPath">,
secretImportDAL: Pick<TSecretImportDALFactory, "find" | "findByFolderIds">
) => {
let content: Record<string, boolean> = {};
if (dto.depth > MAX_SYNC_SECRET_DEPTH) {
logger.info(
`getIntegrationSecrets: secret depth exceeded for [projectId=${dto.projectId}] [folderId=${dto.folderId}] [depth=${dto.depth}]`
);
return content;
}
// process secrets in current folder
const secrets = await secretDAL.findByFolderId(dto.folderId);
secrets.forEach((secret) => {
const secretKey = decryptSymmetric128BitHexKeyUTF8({
ciphertext: secret.secretKeyCiphertext,
iv: secret.secretKeyIV,
tag: secret.secretKeyTag,
key: dto.key
});
content[secretKey] = true;
});
// check if current folder has any imports from other folders
const secretImport = await secretImportDAL.find({ folderId: dto.folderId, isReplication: false });
// if no imports then return secrets in the current folder
if (!secretImport) return content;
const importedFolders = await folderDAL.findByManySecretPath(
secretImport.map(({ importEnv, importPath }) => ({
envId: importEnv.id,
secretPath: importPath
}))
);
for await (const folder of importedFolders) {
if (folder) {
// get secrets contained in each imported folder by recursively calling
// this function against the imported folder
const importedSecrets = await getIntegrationSecretsV1(
{
environment: dto.environment,
projectId: dto.projectId,
folderId: folder.id,
key: dto.key,
depth: dto.depth + 1
},
secretDAL,
folderDAL,
secretImportDAL
);
// add the imported secrets to the current folder secrets
content = { ...importedSecrets, ...content };
}
}
return content;
};
export const deleteGithubSecrets = async ({
integration,
secrets,
accessToken
}: {
integration: Omit<TIntegrations, "envId">;
secrets: Record<string, boolean>;
accessToken: string;
}) => {
interface GitHubSecret {
name: string;
created_at: string;
updated_at: string;
visibility?: "all" | "private" | "selected";
selected_repositories_url?: string | undefined;
}
const OctokitWithRetry = Octokit.plugin(retry);
const octokit = new OctokitWithRetry({
auth: accessToken
});
enum GithubScope {
Repo = "github-repo",
Org = "github-org",
Env = "github-env"
}
let encryptedGithubSecrets: GitHubSecret[];
switch (integration.scope) {
case GithubScope.Org: {
encryptedGithubSecrets = (
await octokit.request("GET /orgs/{org}/actions/secrets", {
org: integration.owner as string
})
).data.secrets;
break;
}
case GithubScope.Env: {
encryptedGithubSecrets = (
await octokit.request("GET /repositories/{repository_id}/environments/{environment_name}/secrets", {
repository_id: Number(integration.appId),
environment_name: integration.targetEnvironmentId as string
})
).data.secrets;
break;
}
default: {
encryptedGithubSecrets = (
await octokit.request("GET /repos/{owner}/{repo}/actions/secrets", {
owner: integration.owner as string,
repo: integration.app as string
})
).data.secrets;
break;
}
}
for await (const encryptedSecret of encryptedGithubSecrets) {
if (encryptedSecret.name in secrets) {
switch (integration.scope) {
case GithubScope.Org: {
await octokit.request("DELETE /orgs/{org}/actions/secrets/{secret_name}", {
org: integration.owner as string,
secret_name: encryptedSecret.name
});
break;
}
case GithubScope.Env: {
await octokit.request(
"DELETE /repositories/{repository_id}/environments/{environment_name}/secrets/{secret_name}",
{
repository_id: Number(integration.appId),
environment_name: integration.targetEnvironmentId as string,
secret_name: encryptedSecret.name
}
);
break;
}
default: {
await octokit.request("DELETE /repos/{owner}/{repo}/actions/secrets/{secret_name}", {
owner: integration.owner as string,
repo: integration.app as string,
secret_name: encryptedSecret.name
});
break;
}
}
// small delay to prevent hitting API rate limits
await new Promise((resolve) => {
setTimeout(resolve, 50);
});
}
}
};
export const deleteIntegrationSecrets = async ({
integration,
integrationAuth,
integrationAuthService,
projectBotService,
secretV2BridgeDAL,
folderDAL,
secretDAL,
secretImportDAL,
kmsService
}: {
integration: Omit<TIntegrations, "envId"> & {
projectId: string;
environment: {
id: string;
name: string;
slug: string;
};
secretPath: string;
};
integrationAuth: TIntegrationAuths;
integrationAuthService: Pick<TIntegrationAuthServiceFactory, "getIntegrationAccessToken" | "getIntegrationAuth">;
projectBotService: Pick<TProjectBotServiceFactory, "getBotKey">;
secretV2BridgeDAL: Pick<TSecretV2BridgeDALFactory, "find" | "findByFolderId">;
folderDAL: Pick<TSecretFolderDALFactory, "findByManySecretPath" | "findBySecretPath">;
secretImportDAL: Pick<TSecretImportDALFactory, "find" | "findByFolderIds">;
secretDAL: Pick<TSecretDALFactory, "findByFolderId">;
kmsService: Pick<TKmsServiceFactory, "createCipherPairWithDataKey">;
}) => {
const { shouldUseSecretV2Bridge, botKey } = await projectBotService.getBotKey(integration.projectId);
const { decryptor: secretManagerDecryptor } = await kmsService.createCipherPairWithDataKey({
type: KmsDataKey.SecretManager,
projectId: integration.projectId
});
const folder = await folderDAL.findBySecretPath(
integration.projectId,
integration.environment.slug,
integration.secretPath
);
if (!folder) {
throw new NotFoundError({
message: "Folder not found."
});
}
const { accessToken } = await integrationAuthService.getIntegrationAccessToken(
integrationAuth,
shouldUseSecretV2Bridge,
botKey
);
const secrets = shouldUseSecretV2Bridge
? await getIntegrationSecretsV2(
{
environment: integration.environment.id,
projectId: integration.projectId,
folderId: folder.id,
depth: 1,
decryptor: (value) => (value ? secretManagerDecryptor({ cipherTextBlob: value }).toString() : "")
},
secretV2BridgeDAL,
folderDAL,
secretImportDAL
)
: await getIntegrationSecretsV1(
{
environment: integration.environment.id,
projectId: integration.projectId,
folderId: folder.id,
key: botKey as string,
depth: 1
},
secretDAL,
folderDAL,
secretImportDAL
);
const suffixedSecrets: typeof secrets = {};
const metadata = IntegrationMetadataSchema.parse(integration.metadata);
if (metadata) {
Object.keys(secrets).forEach((key) => {
const prefix = metadata?.secretPrefix || "";
const suffix = metadata?.secretSuffix || "";
const newKey = prefix + key + suffix;
suffixedSecrets[newKey] = secrets[key];
});
}
switch (integration.integration) {
case Integrations.GITHUB: {
await deleteGithubSecrets({
integration,
accessToken,
secrets: Object.keys(suffixedSecrets).length !== 0 ? suffixedSecrets : secrets
});
break;
}
default:
throw new BadRequestError({
message: "Invalid integration"
});
}
};

View File

@ -538,19 +538,20 @@ const syncSecretsAWSParameterStore = async ({
integration,
secrets,
accessId,
accessToken
accessToken,
projectId
}: {
integration: TIntegrations;
integration: TIntegrations & { secretPath: string; environment: { slug: string } };
secrets: Record<string, { value: string; comment?: string }>;
accessId: string | null;
accessToken: string;
projectId?: string;
}) => {
let response: { isSynced: boolean; syncMessage: string } | null = null;
if (!accessId) {
throw new Error("AWS access ID is required");
}
const config = new AWS.Config({
region: integration.region as string,
credentials: {
@ -567,7 +568,9 @@ const syncSecretsAWSParameterStore = async ({
const metadata = z.record(z.any()).parse(integration.metadata || {});
const awsParameterStoreSecretsObj: Record<string, AWS.SSM.Parameter> = {};
logger.info(
`getIntegrationSecrets: integration sync triggered for ssm with [projectId=${projectId}] [environment=${integration.environment.slug}] [secretPath=${integration.secretPath}] [shouldDisableDelete=${metadata.shouldDisableDelete}]`
);
// now fetch all aws parameter store secrets
let hasNext = true;
let nextToken: string | undefined;
@ -594,6 +597,18 @@ const syncSecretsAWSParameterStore = async ({
nextToken = parameters.NextToken;
}
logger.info(
`getIntegrationSecrets: all fetched keys from AWS SSM [projectId=${projectId}] [environment=${
integration.environment.slug
}] [secretPath=${integration.secretPath}] [awsParameterStoreSecretsObj=${Object.keys(
awsParameterStoreSecretsObj
).join(",")}]`
);
logger.info(
`getIntegrationSecrets: all secrets from Infisical to send to AWS SSM [projectId=${projectId}] [environment=${
integration.environment.slug
}] [secretPath=${integration.secretPath}] [secrets=${Object.keys(secrets).join(",")}]`
);
// Identify secrets to create
// don't use Promise.all() and promise map here
// it will cause rate limit
@ -603,24 +618,56 @@ const syncSecretsAWSParameterStore = async ({
// case: secret does not exist in AWS parameter store
// -> create secret
if (secrets[key].value) {
logger.info(
`getIntegrationSecrets: create secret in AWS SSM for [projectId=${projectId}] [environment=${integration.environment.slug}] [secretPath=${integration.secretPath}] [key=${key}]`
);
await ssm
.putParameter({
Name: `${integration.path}${key}`,
Type: "SecureString",
Value: secrets[key].value,
...(metadata.kmsKeyId && { KeyId: metadata.kmsKeyId }),
// Overwrite: true,
Tags: metadata.secretAWSTag
? metadata.secretAWSTag.map((tag: { key: string; value: string }) => ({
Key: tag.key,
Value: tag.value
}))
: []
Overwrite: true
})
.promise();
if (metadata.secretAWSTag?.length) {
try {
await ssm
.addTagsToResource({
ResourceType: "Parameter",
ResourceId: `${integration.path}${key}`,
Tags: metadata.secretAWSTag
? metadata.secretAWSTag.map((tag: { key: string; value: string }) => ({
Key: tag.key,
Value: tag.value
}))
: []
})
.promise();
} catch (err) {
logger.error(
err,
`getIntegrationSecrets: create secret in AWS SSM for failed [projectId=${projectId}] [environment=${integration.environment.slug}] [secretPath=${integration.secretPath}] [key=${key}]`
);
// eslint-disable-next-line @typescript-eslint/no-explicit-any
if ((err as any).code === "AccessDeniedException") {
logger.error(
`AWS Parameter Store Error [integration=${integration.id}]: double check AWS account permissions (refer to the Infisical docs)`
);
}
response = {
isSynced: false,
syncMessage: (err as AWSError)?.message || "Error syncing with AWS Parameter Store"
};
}
}
}
// case: secret exists in AWS parameter store
} else {
logger.info(
`getIntegrationSecrets: update secret in AWS SSM for [projectId=${projectId}] [environment=${integration.environment.slug}] [secretPath=${integration.secretPath}] [key=${key}]`
);
// -> update secret
if (awsParameterStoreSecretsObj[key].Value !== secrets[key].value) {
await ssm
@ -648,6 +695,10 @@ const syncSecretsAWSParameterStore = async ({
})
.promise();
} catch (err) {
logger.error(
err,
`getIntegrationSecrets: update secret in AWS SSM for failed [projectId=${projectId}] [environment=${integration.environment.slug}] [secretPath=${integration.secretPath}] [key=${key}]`
);
// eslint-disable-next-line @typescript-eslint/no-explicit-any
if ((err as any).code === "AccessDeniedException") {
logger.error(
@ -670,9 +721,18 @@ const syncSecretsAWSParameterStore = async ({
}
if (!metadata.shouldDisableDelete) {
logger.info(
`getIntegrationSecrets: inside of shouldDisableDelete AWS SSM [projectId=${projectId}] [environment=${integration.environment.slug}] [secretPath=${integration.secretPath}] [step=1]`
);
for (const key in awsParameterStoreSecretsObj) {
if (Object.hasOwn(awsParameterStoreSecretsObj, key)) {
logger.info(
`getIntegrationSecrets: inside of shouldDisableDelete AWS SSM [projectId=${projectId}] [environment=${integration.environment.slug}] [secretPath=${integration.secretPath}] [key=${key}] [step=2]`
);
if (!(key in secrets)) {
logger.info(
`getIntegrationSecrets: inside of shouldDisableDelete AWS SSM [projectId=${projectId}] [environment=${integration.environment.slug}] [secretPath=${integration.secretPath}] [key=${key}] [step=3]`
);
// case:
// -> delete secret
await ssm
@ -680,6 +740,9 @@ const syncSecretsAWSParameterStore = async ({
Name: awsParameterStoreSecretsObj[key].Name as string
})
.promise();
logger.info(
`getIntegrationSecrets: inside of shouldDisableDelete AWS SSM [projectId=${projectId}] [environment=${integration.environment.slug}] [secretPath=${integration.secretPath}] [key=${key}] [step=4]`
);
}
await new Promise((resolve) => {
setTimeout(resolve, 50);
@ -3656,7 +3719,8 @@ export const syncIntegrationSecrets = async ({
integration,
secrets,
accessId,
accessToken
accessToken,
projectId
});
break;
case Integrations.AWS_SECRET_MANAGER:

View File

@ -6,8 +6,15 @@ import { BadRequestError } from "@app/lib/errors";
import { TProjectPermission } from "@app/lib/types";
import { TIntegrationAuthDALFactory } from "../integration-auth/integration-auth-dal";
import { TIntegrationAuthServiceFactory } from "../integration-auth/integration-auth-service";
import { deleteIntegrationSecrets } from "../integration-auth/integration-delete-secret";
import { TKmsServiceFactory } from "../kms/kms-service";
import { TProjectBotServiceFactory } from "../project-bot/project-bot-service";
import { TSecretDALFactory } from "../secret/secret-dal";
import { TSecretQueueFactory } from "../secret/secret-queue";
import { TSecretFolderDALFactory } from "../secret-folder/secret-folder-dal";
import { TSecretImportDALFactory } from "../secret-import/secret-import-dal";
import { TSecretV2BridgeDALFactory } from "../secret-v2-bridge/secret-v2-bridge-dal";
import { TIntegrationDALFactory } from "./integration-dal";
import {
TCreateIntegrationDTO,
@ -19,9 +26,15 @@ import {
type TIntegrationServiceFactoryDep = {
integrationDAL: TIntegrationDALFactory;
integrationAuthDAL: TIntegrationAuthDALFactory;
folderDAL: Pick<TSecretFolderDALFactory, "findBySecretPath">;
integrationAuthService: TIntegrationAuthServiceFactory;
folderDAL: Pick<TSecretFolderDALFactory, "findBySecretPath" | "findByManySecretPath">;
permissionService: Pick<TPermissionServiceFactory, "getProjectPermission">;
projectBotService: TProjectBotServiceFactory;
secretQueueService: Pick<TSecretQueueFactory, "syncIntegrations">;
secretV2BridgeDAL: Pick<TSecretV2BridgeDALFactory, "find" | "findByFolderId">;
secretImportDAL: Pick<TSecretImportDALFactory, "find" | "findByFolderIds">;
kmsService: Pick<TKmsServiceFactory, "createCipherPairWithDataKey">;
secretDAL: Pick<TSecretDALFactory, "findByFolderId">;
};
export type TIntegrationServiceFactory = ReturnType<typeof integrationServiceFactory>;
@ -31,7 +44,13 @@ export const integrationServiceFactory = ({
integrationAuthDAL,
folderDAL,
permissionService,
secretQueueService
secretQueueService,
integrationAuthService,
projectBotService,
secretV2BridgeDAL,
secretImportDAL,
kmsService,
secretDAL
}: TIntegrationServiceFactoryDep) => {
const createIntegration = async ({
app,
@ -161,7 +180,14 @@ export const integrationServiceFactory = ({
return updatedIntegration;
};
const deleteIntegration = async ({ actorId, id, actor, actorAuthMethod, actorOrgId }: TDeleteIntegrationDTO) => {
const deleteIntegration = async ({
actorId,
id,
actor,
actorAuthMethod,
actorOrgId,
shouldDeleteIntegrationSecrets
}: TDeleteIntegrationDTO) => {
const integration = await integrationDAL.findById(id);
if (!integration) throw new BadRequestError({ message: "Integration auth not found" });
@ -174,6 +200,22 @@ export const integrationServiceFactory = ({
);
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Delete, ProjectPermissionSub.Integrations);
const integrationAuth = await integrationAuthDAL.findById(integration.integrationAuthId);
if (shouldDeleteIntegrationSecrets) {
await deleteIntegrationSecrets({
integration,
integrationAuth,
projectBotService,
integrationAuthService,
secretV2BridgeDAL,
folderDAL,
secretImportDAL,
secretDAL,
kmsService
});
}
const deletedIntegration = await integrationDAL.transaction(async (tx) => {
// delete integration
const deletedIntegrationResult = await integrationDAL.deleteById(id, tx);

View File

@ -63,6 +63,7 @@ export type TUpdateIntegrationDTO = {
export type TDeleteIntegrationDTO = {
id: string;
shouldDeleteIntegrationSecrets?: boolean;
} & Omit<TProjectPermission, "projectId">;
export type TSyncIntegrationDTO = {

View File

@ -0,0 +1,5 @@
export type TOrgAdminDALFactory = ReturnType<typeof orgAdminDALFactory>;
export const orgAdminDALFactory = () => {
return {};
};

View File

@ -0,0 +1,191 @@
import { ForbiddenError } from "@casl/ability";
import { ProjectMembershipRole, ProjectVersion, SecretKeyEncoding } from "@app/db/schemas";
import { OrgPermissionAdminConsoleAction, OrgPermissionSubjects } from "@app/ee/services/permission/org-permission";
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service";
import { infisicalSymmetricDecrypt } from "@app/lib/crypto/encryption";
import { BadRequestError } from "@app/lib/errors";
import { TProjectDALFactory } from "../project/project-dal";
import { assignWorkspaceKeysToMembers } from "../project/project-fns";
import { TProjectBotDALFactory } from "../project-bot/project-bot-dal";
import { TProjectKeyDALFactory } from "../project-key/project-key-dal";
import { TProjectMembershipDALFactory } from "../project-membership/project-membership-dal";
import { TProjectUserMembershipRoleDALFactory } from "../project-membership/project-user-membership-role-dal";
import { TUserDALFactory } from "../user/user-dal";
import { TAccessProjectDTO, TListOrgProjectsDTO } from "./org-admin-types";
type TOrgAdminServiceFactoryDep = {
permissionService: Pick<TPermissionServiceFactory, "getOrgPermission">;
projectDAL: Pick<TProjectDALFactory, "find" | "findById" | "findProjectGhostUser">;
projectMembershipDAL: Pick<TProjectMembershipDALFactory, "findOne" | "create" | "transaction" | "delete">;
projectKeyDAL: Pick<TProjectKeyDALFactory, "findLatestProjectKey" | "create">;
projectBotDAL: Pick<TProjectBotDALFactory, "findOne">;
userDAL: Pick<TUserDALFactory, "findUserEncKeyByUserId">;
projectUserMembershipRoleDAL: Pick<TProjectUserMembershipRoleDALFactory, "create" | "delete">;
};
export type TOrgAdminServiceFactory = ReturnType<typeof orgAdminServiceFactory>;
export const orgAdminServiceFactory = ({
permissionService,
projectDAL,
projectMembershipDAL,
projectKeyDAL,
projectBotDAL,
userDAL,
projectUserMembershipRoleDAL
}: TOrgAdminServiceFactoryDep) => {
const listOrgProjects = async ({
actor,
limit,
actorId,
offset,
search,
actorOrgId,
actorAuthMethod
}: TListOrgProjectsDTO) => {
const { permission } = await permissionService.getOrgPermission(
actor,
actorId,
actorOrgId,
actorAuthMethod,
actorOrgId
);
ForbiddenError.from(permission).throwUnlessCan(
OrgPermissionAdminConsoleAction.AccessAllProjects,
OrgPermissionSubjects.AdminConsole
);
const projects = await projectDAL.find(
{
orgId: actorOrgId,
$search: {
name: search ? `%${search}%` : undefined
}
},
{ offset, limit, sort: [["name", "asc"]], count: true }
);
const count = projects?.[0]?.count ? parseInt(projects?.[0]?.count, 10) : 0;
return { projects, count };
};
const grantProjectAdminAccess = async ({
actor,
actorId,
actorOrgId,
actorAuthMethod,
projectId
}: TAccessProjectDTO) => {
const { permission, membership } = await permissionService.getOrgPermission(
actor,
actorId,
actorOrgId,
actorAuthMethod,
actorOrgId
);
ForbiddenError.from(permission).throwUnlessCan(
OrgPermissionAdminConsoleAction.AccessAllProjects,
OrgPermissionSubjects.AdminConsole
);
const project = await projectDAL.findById(projectId);
if (!project) throw new BadRequestError({ message: "Project not found" });
if (project.version === ProjectVersion.V1) {
throw new BadRequestError({ message: "Please upgrade your project on your dashboard" });
}
// check already there exist a membership if there return it
const projectMembership = await projectMembershipDAL.findOne({
projectId,
userId: actorId
});
if (projectMembership) {
// reset and make the user admin
await projectMembershipDAL.transaction(async (tx) => {
await projectUserMembershipRoleDAL.delete({ projectMembershipId: projectMembership.id }, tx);
await projectUserMembershipRoleDAL.create(
{
projectMembershipId: projectMembership.id,
role: ProjectMembershipRole.Admin
},
tx
);
});
return { isExistingMember: true, membership: projectMembership };
}
// missing membership thus add admin back as admin to project
const ghostUser = await projectDAL.findProjectGhostUser(projectId);
if (!ghostUser) {
throw new BadRequestError({
message: "Failed to find sudo user"
});
}
const ghostUserLatestKey = await projectKeyDAL.findLatestProjectKey(ghostUser.id, projectId);
if (!ghostUserLatestKey) {
throw new BadRequestError({
message: "Failed to find sudo user latest key"
});
}
const bot = await projectBotDAL.findOne({ projectId });
if (!bot) {
throw new BadRequestError({
message: "Failed to find bot"
});
}
const botPrivateKey = infisicalSymmetricDecrypt({
keyEncoding: bot.keyEncoding as SecretKeyEncoding,
iv: bot.iv,
tag: bot.tag,
ciphertext: bot.encryptedPrivateKey
});
const userEncryptionKey = await userDAL.findUserEncKeyByUserId(actorId);
if (!userEncryptionKey) throw new BadRequestError({ message: "user encryption key not found" });
const [newWsMember] = assignWorkspaceKeysToMembers({
decryptKey: ghostUserLatestKey,
userPrivateKey: botPrivateKey,
members: [
{
orgMembershipId: membership.id,
projectMembershipRole: ProjectMembershipRole.Admin,
userPublicKey: userEncryptionKey.publicKey
}
]
});
const updatedMembership = await projectMembershipDAL.transaction(async (tx) => {
const newProjectMembership = await projectMembershipDAL.create(
{
projectId,
userId: actorId
},
tx
);
await projectUserMembershipRoleDAL.create(
{ projectMembershipId: newProjectMembership.id, role: ProjectMembershipRole.Admin },
tx
);
await projectKeyDAL.create(
{
encryptedKey: newWsMember.workspaceEncryptedKey,
nonce: newWsMember.workspaceEncryptedNonce,
senderId: ghostUser.id,
receiverId: actorId,
projectId
},
tx
);
return newProjectMembership;
});
return { isExistingMember: false, membership: updatedMembership };
};
return { listOrgProjects, grantProjectAdminAccess };
};

View File

@ -0,0 +1,11 @@
import { TOrgPermission } from "@app/lib/types";
export type TListOrgProjectsDTO = {
limit?: number;
offset?: number;
search?: string;
} & Omit<TOrgPermission, "orgId">;
export type TAccessProjectDTO = {
projectId: string;
} & Omit<TOrgPermission, "orgId">;

View File

@ -0,0 +1,48 @@
import { logger } from "@app/lib/logger";
import { QueueJobs, QueueName, TQueueServiceFactory } from "@app/queue";
import { TPkiAlertServiceFactory } from "@app/services/pki-alert/pki-alert-service";
type TDailyExpiringPkiItemAlertQueueServiceFactoryDep = {
queueService: TQueueServiceFactory;
pkiAlertService: Pick<TPkiAlertServiceFactory, "sendPkiItemExpiryNotices">;
};
export type TDailyExpiringPkiItemAlertQueueServiceFactory = ReturnType<
typeof dailyExpiringPkiItemAlertQueueServiceFactory
>;
export const dailyExpiringPkiItemAlertQueueServiceFactory = ({
queueService,
pkiAlertService
}: TDailyExpiringPkiItemAlertQueueServiceFactoryDep) => {
queueService.start(QueueName.DailyExpiringPkiItemAlert, async () => {
logger.info(`${QueueName.DailyExpiringPkiItemAlert}: queue task started`);
await pkiAlertService.sendPkiItemExpiryNotices();
logger.info(`${QueueName.DailyExpiringPkiItemAlert}: queue task completed`);
});
// we do a repeat cron job in utc timezone at 12 Midnight each day
const startSendingAlerts = async () => {
// clear previous job
await queueService.stopRepeatableJob(
QueueName.DailyExpiringPkiItemAlert,
QueueJobs.DailyExpiringPkiItemAlert,
{ pattern: "0 0 * * *", utc: true },
QueueName.DailyExpiringPkiItemAlert // just a job id
);
await queueService.queue(QueueName.DailyExpiringPkiItemAlert, QueueJobs.DailyExpiringPkiItemAlert, undefined, {
delay: 5000,
jobId: QueueName.DailyExpiringPkiItemAlert,
repeat: { pattern: "0 0 * * *", utc: true }
});
};
queueService.listen(QueueName.DailyExpiringPkiItemAlert, "failed", (_, err) => {
logger.error(err, `${QueueName.DailyExpiringPkiItemAlert}: Expiring PKI item alert failed`);
});
return {
startSendingAlerts
};
};

View File

@ -0,0 +1,84 @@
import { TDbClient } from "@app/db";
import { TableName } from "@app/db/schemas";
import { DatabaseError } from "@app/lib/errors";
import { ormify } from "@app/lib/knex";
import { PkiItemType } from "../pki-collection/pki-collection-types";
export type TPkiAlertDALFactory = ReturnType<typeof pkiAlertDALFactory>;
export const pkiAlertDALFactory = (db: TDbClient) => {
const pkiAlertOrm = ormify(db, TableName.PkiAlert);
const getExpiringPkiCollectionItemsForAlerting = async () => {
try {
type AlertItem = {
type: PkiItemType;
id: string; // id of the CA or certificate
expiryDate: Date;
serialNumber: string;
friendlyName: string;
pkiCollectionId: string;
alertId: string;
alertName: string;
alertBeforeDays: number;
recipientEmails: string;
};
// gets CAs and certificates as part of PKI collection items
const combinedQuery = db
.replicaNode()
.select(
db.raw("? as type", [PkiItemType.CA]),
`${PkiItemType.CA}.id`,
`${PkiItemType.CA}.notAfter as expiryDate`,
`${PkiItemType.CA}.serialNumber`,
`${PkiItemType.CA}.friendlyName`,
"pci.pkiCollectionId"
)
.from(`${TableName.CertificateAuthority} as ${PkiItemType.CA}`)
.join(`${TableName.PkiCollectionItem} as pci`, `${PkiItemType.CA}.id`, "pci.caId")
.unionAll((qb) => {
void qb
.select(
db.raw("? as type", [PkiItemType.CERTIFICATE]),
`${PkiItemType.CERTIFICATE}.id`,
`${PkiItemType.CERTIFICATE}.notAfter as expiryDate`,
`${PkiItemType.CERTIFICATE}.serialNumber`,
`${PkiItemType.CERTIFICATE}.friendlyName`,
"pci.pkiCollectionId"
)
.from(`${TableName.Certificate} as ${PkiItemType.CERTIFICATE}`)
.join(`${TableName.PkiCollectionItem} as pci`, `${PkiItemType.CERTIFICATE}.id`, "pci.certId");
});
/**
* Gets alerts to send based on alertBeforeDays on PKI alerts connected to PKI collection items
* Note: Results are clamped to 1-day window to avoid sending multiple alerts for the same item
*/
const alertQuery = db
.replicaNode()
.select("combined.*", "pa.id as alertId", "pa.name as alertName", "pa.alertBeforeDays", "pa.recipientEmails")
.from(db.raw("(?) as combined", [combinedQuery]))
.join(`${TableName.PkiAlert} as pa`, "combined.pkiCollectionId", "pa.pkiCollectionId")
.whereRaw(
`
combined."expiryDate" <= CURRENT_TIMESTAMP + (pa."alertBeforeDays" * INTERVAL '1 day')
AND combined."expiryDate" > CURRENT_TIMESTAMP + ((pa."alertBeforeDays" - 1) * INTERVAL '1 day')
`
)
.orderBy("combined.expiryDate");
const results = (await alertQuery) as AlertItem[];
return results;
} catch (error) {
throw new DatabaseError({ error, name: "Get expiring PKI collection items for alerting" });
}
};
return {
getExpiringPkiCollectionItemsForAlerting,
...pkiAlertOrm
};
};

View File

@ -0,0 +1,182 @@
import { ForbiddenError } from "@casl/ability";
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service";
import { ProjectPermissionActions, ProjectPermissionSub } from "@app/ee/services/permission/project-permission";
import { NotFoundError, UnauthorizedError } from "@app/lib/errors";
import { groupBy } from "@app/lib/fn";
import { TPkiCollectionDALFactory } from "@app/services/pki-collection/pki-collection-dal";
import { pkiItemTypeToNameMap } from "@app/services/pki-collection/pki-collection-types";
import { SmtpTemplates, TSmtpService } from "@app/services/smtp/smtp-service";
import { TPkiAlertDALFactory } from "./pki-alert-dal";
import { TCreateAlertDTO, TDeleteAlertDTO, TGetAlertByIdDTO, TUpdateAlertDTO } from "./pki-alert-types";
type TPkiAlertServiceFactoryDep = {
pkiAlertDAL: Pick<
TPkiAlertDALFactory,
"create" | "findById" | "updateById" | "deleteById" | "getExpiringPkiCollectionItemsForAlerting"
>;
pkiCollectionDAL: Pick<TPkiCollectionDALFactory, "findById">;
permissionService: Pick<TPermissionServiceFactory, "getProjectPermission">;
smtpService: Pick<TSmtpService, "sendMail">;
};
export type TPkiAlertServiceFactory = ReturnType<typeof pkiAlertServiceFactory>;
export const pkiAlertServiceFactory = ({
pkiAlertDAL,
pkiCollectionDAL,
permissionService,
smtpService
}: TPkiAlertServiceFactoryDep) => {
const sendPkiItemExpiryNotices = async () => {
const allAlertItems = await pkiAlertDAL.getExpiringPkiCollectionItemsForAlerting();
const flattenedResults = allAlertItems.flatMap(({ recipientEmails, ...item }) =>
recipientEmails.split(",").map((email) => ({
...item,
recipientEmail: email.trim()
}))
);
const groupedByEmail = groupBy(flattenedResults, (item) => item.recipientEmail);
for await (const [email, items] of Object.entries(groupedByEmail)) {
const groupedByAlert = groupBy(items, (item) => item.alertId);
for await (const [, alertItems] of Object.entries(groupedByAlert)) {
await smtpService.sendMail({
recipients: [email],
subjectLine: `Infisical CA/Certificate expiration notice: ${alertItems[0].alertName}`,
substitutions: {
alertName: alertItems[0].alertName,
alertBeforeDays: items[0].alertBeforeDays,
items: alertItems.map((alertItem) => ({
...alertItem,
type: pkiItemTypeToNameMap[alertItem.type],
expiryDate: new Date(alertItem.expiryDate).toString()
}))
},
template: SmtpTemplates.PkiExpirationAlert
});
}
}
};
const createPkiAlert = async ({
projectId,
name,
pkiCollectionId,
alertBeforeDays,
emails,
actorId,
actorAuthMethod,
actor,
actorOrgId
}: TCreateAlertDTO) => {
const { permission } = await permissionService.getProjectPermission(
actor,
actorId,
projectId,
actorAuthMethod,
actorOrgId
);
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Create, ProjectPermissionSub.PkiAlerts);
const pkiCollection = await pkiCollectionDAL.findById(pkiCollectionId);
if (!pkiCollection) throw new NotFoundError({ message: "PKI collection not found" });
if (pkiCollection.projectId !== projectId)
throw new UnauthorizedError({ message: "PKI collection not found in project" });
const alert = await pkiAlertDAL.create({
projectId,
pkiCollectionId,
name,
alertBeforeDays,
recipientEmails: emails.join(",")
});
return alert;
};
const getPkiAlertById = async ({ alertId, actorId, actorAuthMethod, actor, actorOrgId }: TGetAlertByIdDTO) => {
const alert = await pkiAlertDAL.findById(alertId);
if (!alert) throw new NotFoundError({ message: "Alert not found" });
const { permission } = await permissionService.getProjectPermission(
actor,
actorId,
alert.projectId,
actorAuthMethod,
actorOrgId
);
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.PkiAlerts);
return alert;
};
const updatePkiAlert = async ({
alertId,
name,
pkiCollectionId,
alertBeforeDays,
emails,
actorId,
actorAuthMethod,
actor,
actorOrgId
}: TUpdateAlertDTO) => {
let alert = await pkiAlertDAL.findById(alertId);
if (!alert) throw new NotFoundError({ message: "Alert not found" });
const { permission } = await permissionService.getProjectPermission(
actor,
actorId,
alert.projectId,
actorAuthMethod,
actorOrgId
);
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Edit, ProjectPermissionSub.PkiAlerts);
if (pkiCollectionId) {
const pkiCollection = await pkiCollectionDAL.findById(pkiCollectionId);
if (!pkiCollection) throw new NotFoundError({ message: "PKI collection not found" });
if (pkiCollection.projectId !== alert.projectId)
throw new UnauthorizedError({ message: "PKI collection not found in project" });
}
alert = await pkiAlertDAL.updateById(alertId, {
name,
alertBeforeDays,
...(pkiCollectionId && { pkiCollectionId }),
...(emails && { recipientEmails: emails.join(",") })
});
return alert;
};
const deletePkiAlert = async ({ alertId, actorId, actorAuthMethod, actor, actorOrgId }: TDeleteAlertDTO) => {
let alert = await pkiAlertDAL.findById(alertId);
if (!alert) throw new NotFoundError({ message: "Alert not found" });
const { permission } = await permissionService.getProjectPermission(
actor,
actorId,
alert.projectId,
actorAuthMethod,
actorOrgId
);
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Delete, ProjectPermissionSub.PkiAlerts);
alert = await pkiAlertDAL.deleteById(alertId);
return alert;
};
return {
sendPkiItemExpiryNotices,
createPkiAlert,
getPkiAlertById,
updatePkiAlert,
deletePkiAlert
};
};

View File

@ -0,0 +1,24 @@
import { TProjectPermission } from "@app/lib/types";
export type TCreateAlertDTO = {
name: string;
pkiCollectionId: string;
alertBeforeDays: number;
emails: string[];
} & TProjectPermission;
export type TGetAlertByIdDTO = {
alertId: string;
} & Omit<TProjectPermission, "projectId">;
export type TUpdateAlertDTO = {
alertId: string;
name?: string;
pkiCollectionId?: string;
alertBeforeDays?: number;
emails?: string[];
} & Omit<TProjectPermission, "projectId">;
export type TDeleteAlertDTO = {
alertId: string;
} & Omit<TProjectPermission, "projectId">;

View File

@ -0,0 +1,13 @@
import { TDbClient } from "@app/db";
import { TableName } from "@app/db/schemas";
import { ormify } from "@app/lib/knex";
export type TPkiCollectionDALFactory = ReturnType<typeof pkiCollectionDALFactory>;
export const pkiCollectionDALFactory = (db: TDbClient) => {
const pkiCollectionOrm = ormify(db, TableName.PkiCollection);
return {
...pkiCollectionOrm
};
};

View File

@ -0,0 +1,30 @@
import { TPkiCollectionItems } from "@app/db/schemas";
import { PkiItemType } from "./pki-collection-types";
/**
* Transforms a PKI Collection Item from the database to the expected API response format
*/
export const transformPkiCollectionItem = (pkiCollectionItem: TPkiCollectionItems) => {
let type: PkiItemType;
let itemId: string;
if (pkiCollectionItem.caId) {
type = PkiItemType.CA;
itemId = pkiCollectionItem.caId;
} else if (pkiCollectionItem.certId) {
type = PkiItemType.CERTIFICATE;
itemId = pkiCollectionItem.certId;
} else {
throw new Error("Invalid PKI Collection Item: must have either caId or certId");
}
return {
id: pkiCollectionItem.id,
pkiCollectionId: pkiCollectionItem.pkiCollectionId,
type,
itemId,
createdAt: pkiCollectionItem.createdAt,
updatedAt: pkiCollectionItem.updatedAt
};
};

View File

@ -0,0 +1,93 @@
import { TDbClient } from "@app/db";
import { TableName, TPkiCollectionItems } from "@app/db/schemas";
import { DatabaseError } from "@app/lib/errors";
import { ormify } from "@app/lib/knex";
import { PkiItemType } from "./pki-collection-types";
export type TPkiCollectionItemDALFactory = ReturnType<typeof pkiCollectionItemDALFactory>;
export const pkiCollectionItemDALFactory = (db: TDbClient) => {
const pkiCollectionItemOrm = ormify(db, TableName.PkiCollectionItem);
const findPkiCollectionItems = async ({
collectionId,
type,
offset,
limit
}: {
collectionId: string;
type?: PkiItemType;
offset?: number;
limit?: number;
}) => {
try {
const query = db
.replicaNode()(TableName.PkiCollectionItem)
.select(
"pki_collection_items.*",
db.raw(
`COALESCE("${TableName.CertificateAuthority}"."notBefore", "${TableName.Certificate}"."notBefore") as "notBefore"`
),
db.raw(
`COALESCE("${TableName.CertificateAuthority}"."notAfter", "${TableName.Certificate}"."notAfter") as "notAfter"`
),
db.raw(
`COALESCE("${TableName.CertificateAuthority}"."friendlyName", "${TableName.Certificate}"."friendlyName") as "friendlyName"`
)
)
.leftJoin(
TableName.CertificateAuthority,
`${TableName.PkiCollectionItem}.caId`,
`${TableName.CertificateAuthority}.id`
)
.leftJoin(TableName.Certificate, `${TableName.PkiCollectionItem}.certId`, `${TableName.Certificate}.id`)
.where((builder) => {
void builder.where(`${TableName.PkiCollectionItem}.pkiCollectionId`, collectionId);
if (type === PkiItemType.CA) {
void builder.whereNull(`${TableName.PkiCollectionItem}.certId`);
} else if (type === PkiItemType.CERTIFICATE) {
void builder.whereNull(`${TableName.PkiCollectionItem}.caId`);
}
});
if (offset) {
void query.offset(offset);
}
if (limit) {
void query.limit(limit);
}
void query.orderBy(`${TableName.PkiCollectionItem}.createdAt`, "desc");
const result = await query;
return result as (TPkiCollectionItems & { notAfter: Date; notBefore: Date; friendlyName: string })[];
} catch (error) {
throw new DatabaseError({ error, name: "Find all PKI collection items" });
}
};
const countItemsInPkiCollection = async (collectionId: string) => {
try {
interface CountResult {
count: string;
}
const query = db
.replicaNode()(TableName.PkiCollectionItem)
.where(`${TableName.PkiCollectionItem}.pkiCollectionId`, collectionId);
const count = await query.count("*").first();
return parseInt((count as unknown as CountResult).count || "0", 10);
} catch (error) {
throw new DatabaseError({ error, name: "Count all PKI collection items" });
}
};
return {
...pkiCollectionItemOrm,
findPkiCollectionItems,
countItemsInPkiCollection
};
};

View File

@ -0,0 +1,331 @@
import { ForbiddenError } from "@casl/ability";
import { TPkiCollectionItems } from "@app/db/schemas";
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service";
import { ProjectPermissionActions, ProjectPermissionSub } from "@app/ee/services/permission/project-permission";
import { BadRequestError, NotFoundError } from "@app/lib/errors";
import { TCertificateDALFactory } from "@app/services/certificate/certificate-dal";
import { TCertificateAuthorityDALFactory } from "@app/services/certificate-authority/certificate-authority-dal";
import { TPkiCollectionDALFactory } from "./pki-collection-dal";
import { transformPkiCollectionItem } from "./pki-collection-fns";
import { TPkiCollectionItemDALFactory } from "./pki-collection-item-dal";
import {
PkiItemType,
TAddItemToPkiCollectionDTO,
TCreatePkiCollectionDTO,
TDeletePkiCollectionDTO,
TGetPkiCollectionByIdDTO,
TGetPkiCollectionItems,
TRemoveItemFromPkiCollectionDTO,
TUpdatePkiCollectionDTO
} from "./pki-collection-types";
type TPkiCollectionServiceFactoryDep = {
pkiCollectionDAL: Pick<TPkiCollectionDALFactory, "create" | "findById" | "updateById" | "deleteById">;
pkiCollectionItemDAL: Pick<
TPkiCollectionItemDALFactory,
"findOne" | "create" | "deleteById" | "findPkiCollectionItems" | "countItemsInPkiCollection"
>;
certificateAuthorityDAL: Pick<TCertificateAuthorityDALFactory, "find" | "findOne">;
certificateDAL: Pick<TCertificateDALFactory, "find">;
permissionService: Pick<TPermissionServiceFactory, "getProjectPermission">;
};
export type TPkiCollectionServiceFactory = ReturnType<typeof pkiCollectionServiceFactory>;
export const pkiCollectionServiceFactory = ({
pkiCollectionDAL,
pkiCollectionItemDAL,
certificateAuthorityDAL,
certificateDAL,
permissionService
}: TPkiCollectionServiceFactoryDep) => {
const createPkiCollection = async ({
name,
description,
projectId,
actorId,
actorAuthMethod,
actor,
actorOrgId
}: TCreatePkiCollectionDTO) => {
const { permission } = await permissionService.getProjectPermission(
actor,
actorId,
projectId,
actorAuthMethod,
actorOrgId
);
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Create,
ProjectPermissionSub.PkiCollections
);
const pkiCollection = await pkiCollectionDAL.create({
projectId,
name,
description
});
return pkiCollection;
};
const getPkiCollectionById = async ({
collectionId,
actorId,
actorAuthMethod,
actor,
actorOrgId
}: TGetPkiCollectionByIdDTO) => {
const pkiCollection = await pkiCollectionDAL.findById(collectionId);
if (!pkiCollection) throw new NotFoundError({ message: "PKI collection not found" });
const { permission } = await permissionService.getProjectPermission(
actor,
actorId,
pkiCollection.projectId,
actorAuthMethod,
actorOrgId
);
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.PkiCollections);
return pkiCollection;
};
const updatePkiCollection = async ({
collectionId,
name,
description,
actorId,
actorAuthMethod,
actor,
actorOrgId
}: TUpdatePkiCollectionDTO) => {
let pkiCollection = await pkiCollectionDAL.findById(collectionId);
if (!pkiCollection) throw new NotFoundError({ message: "PKI collection not found" });
const { permission } = await permissionService.getProjectPermission(
actor,
actorId,
pkiCollection.projectId,
actorAuthMethod,
actorOrgId
);
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Edit, ProjectPermissionSub.PkiCollections);
pkiCollection = await pkiCollectionDAL.updateById(collectionId, {
name,
description
});
return pkiCollection;
};
const deletePkiCollection = async ({
collectionId,
actorId,
actorAuthMethod,
actor,
actorOrgId
}: TDeletePkiCollectionDTO) => {
let pkiCollection = await pkiCollectionDAL.findById(collectionId);
if (!pkiCollection) throw new NotFoundError({ message: "PKI collection not found" });
const { permission } = await permissionService.getProjectPermission(
actor,
actorId,
pkiCollection.projectId,
actorAuthMethod,
actorOrgId
);
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Delete,
ProjectPermissionSub.PkiCollections
);
pkiCollection = await pkiCollectionDAL.deleteById(collectionId);
return pkiCollection;
};
const getPkiCollectionItems = async ({
collectionId,
type,
offset = 0,
limit = 25,
actorId,
actorAuthMethod,
actor,
actorOrgId
}: TGetPkiCollectionItems) => {
const pkiCollection = await pkiCollectionDAL.findById(collectionId);
if (!pkiCollection) throw new NotFoundError({ message: "PKI collection not found" });
const { permission } = await permissionService.getProjectPermission(
actor,
actorId,
pkiCollection.projectId,
actorAuthMethod,
actorOrgId
);
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.PkiCollections);
const pkiCollectionItems = await pkiCollectionItemDAL.findPkiCollectionItems({
collectionId,
type,
offset,
limit
});
const count = await pkiCollectionItemDAL.countItemsInPkiCollection(collectionId);
return {
pkiCollection,
pkiCollectionItems: pkiCollectionItems.map((p) => ({
...transformPkiCollectionItem(p),
notBefore: p.notBefore,
notAfter: p.notAfter,
friendlyName: p.friendlyName
})),
totalCount: count
};
};
const addItemToPkiCollection = async ({
collectionId,
actorId,
actorAuthMethod,
actor,
actorOrgId,
type,
itemId
}: TAddItemToPkiCollectionDTO) => {
const pkiCollection = await pkiCollectionDAL.findById(collectionId);
if (!pkiCollection) throw new NotFoundError({ message: "PKI collection not found" });
const { permission } = await permissionService.getProjectPermission(
actor,
actorId,
pkiCollection.projectId,
actorAuthMethod,
actorOrgId
);
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Create,
ProjectPermissionSub.PkiCollections
);
let pkiCollectionItem: TPkiCollectionItems;
switch (type) {
case PkiItemType.CA: {
// validate that CA has not already been added to PKI collection
const isCaAdded = await pkiCollectionItemDAL.findOne({
pkiCollectionId: collectionId,
caId: itemId
});
if (isCaAdded) throw new BadRequestError({ message: "CA is already part of the PKI collection" });
// validate that there exists a CA in same project as PKI collection
const ca = await certificateAuthorityDAL.findOne({
id: itemId,
projectId: pkiCollection.projectId
});
if (!ca) throw new NotFoundError({ message: "CA not found" });
pkiCollectionItem = await pkiCollectionItemDAL.create({
pkiCollectionId: collectionId,
caId: itemId
});
break;
}
case PkiItemType.CERTIFICATE: {
// validate that certificate has not already been added to PKI collection
const isCertAdded = await pkiCollectionItemDAL.findOne({
pkiCollectionId: collectionId,
certId: itemId
});
if (isCertAdded) throw new BadRequestError({ message: "Certificate already part of the PKI collection" });
// validate that there exists a certificate in same project as PKI collection
const cas = await certificateAuthorityDAL.find({ projectId: pkiCollection.projectId });
// TODO: consider making this more efficient
const [certificate] = await certificateDAL.find({
$in: {
caId: cas.map((ca) => ca.id)
},
id: itemId
});
if (!certificate) throw new NotFoundError({ message: "Certificate not found" });
pkiCollectionItem = await pkiCollectionItemDAL.create({
pkiCollectionId: collectionId,
certId: itemId
});
break;
}
default: {
throw new BadRequestError({ message: "Invalid PKI item type" });
}
}
return {
pkiCollection,
pkiCollectionItem: transformPkiCollectionItem(pkiCollectionItem)
};
};
const removeItemFromPkiCollection = async ({
collectionId,
actorId,
actorAuthMethod,
actor,
actorOrgId,
itemId
}: TRemoveItemFromPkiCollectionDTO) => {
const pkiCollection = await pkiCollectionDAL.findById(collectionId);
if (!pkiCollection) throw new NotFoundError({ message: "PKI collection not found" });
let pkiCollectionItem = await pkiCollectionItemDAL.findOne({
pkiCollectionId: collectionId,
id: itemId
});
if (!pkiCollectionItem) throw new NotFoundError({ message: "PKI collection item not found" });
const { permission } = await permissionService.getProjectPermission(
actor,
actorId,
pkiCollection.projectId,
actorAuthMethod,
actorOrgId
);
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Delete,
ProjectPermissionSub.PkiCollections
);
pkiCollectionItem = await pkiCollectionItemDAL.deleteById(itemId);
return {
pkiCollection,
pkiCollectionItem: transformPkiCollectionItem(pkiCollectionItem)
};
};
return {
createPkiCollection,
getPkiCollectionById,
updatePkiCollection,
deletePkiCollection,
getPkiCollectionItems,
addItemToPkiCollection,
removeItemFromPkiCollection
};
};

View File

@ -0,0 +1,48 @@
import { TProjectPermission } from "@app/lib/types";
export type TCreatePkiCollectionDTO = {
name: string;
description: string;
} & TProjectPermission;
export type TGetPkiCollectionByIdDTO = {
collectionId: string;
} & Omit<TProjectPermission, "projectId">;
export type TUpdatePkiCollectionDTO = {
collectionId: string;
name?: string;
description?: string;
} & Omit<TProjectPermission, "projectId">;
export type TDeletePkiCollectionDTO = {
collectionId: string;
} & Omit<TProjectPermission, "projectId">;
export enum PkiItemType {
CERTIFICATE = "certificate",
CA = "ca"
}
export const pkiItemTypeToNameMap: { [K in PkiItemType]: string } = {
[PkiItemType.CA]: "CA",
[PkiItemType.CERTIFICATE]: "Certificate"
};
export type TGetPkiCollectionItems = {
collectionId: string;
type?: PkiItemType;
offset: number;
limit: number;
} & Omit<TProjectPermission, "projectId">;
export type TAddItemToPkiCollectionDTO = {
collectionId: string;
type: PkiItemType;
itemId: string;
} & Omit<TProjectPermission, "projectId">;
export type TRemoveItemFromPkiCollectionDTO = {
collectionId: string;
itemId: string;
} & Omit<TProjectPermission, "projectId">;

View File

@ -46,6 +46,7 @@ export const projectBotDALFactory = (db: TDbClient) => {
const doc = await db
.replicaNode()(TableName.ProjectMembership)
.where(`${TableName.ProjectMembership}.projectId` as "projectId", projectId)
.where(`${TableName.ProjectKeys}.projectId` as "projectId", projectId)
.where(`${TableName.Users}.isGhost` as "isGhost", false)
.join(TableName.Users, `${TableName.ProjectMembership}.userId`, `${TableName.Users}.id`)
.join(TableName.ProjectKeys, `${TableName.ProjectMembership}.userId`, `${TableName.ProjectKeys}.receiverId`)

View File

@ -66,10 +66,10 @@ export const getBotKeyFnFactory = (
await projectBotDAL.create({
name: "Infisical Bot (Ghost)",
projectId,
isActive: true,
tag,
iv,
encryptedPrivateKey: ciphertext,
isActive: true,
publicKey: botKey.publicKey,
algorithm,
keyEncoding: encoding,
@ -80,6 +80,12 @@ export const getBotKeyFnFactory = (
} else {
await projectBotDAL.updateById(bot.id, {
isActive: true,
tag,
iv,
encryptedPrivateKey: ciphertext,
publicKey: botKey.publicKey,
algorithm,
keyEncoding: encoding,
encryptedProjectKey: encryptedWorkspaceKey.ciphertext,
encryptedProjectKeyNonce: encryptedWorkspaceKey.nonce,
senderId: projectV1Keys.userId
@ -89,7 +95,6 @@ export const getBotKeyFnFactory = (
}
const botPrivateKey = getBotPrivateKey({ bot });
const botKey = decryptAsymmetric({
ciphertext: bot.encryptedProjectKey,
privateKey: botPrivateKey,

View File

@ -256,7 +256,6 @@ export const projectMembershipServiceFactory = ({
}
const bot = await projectBotDAL.findOne({ projectId });
if (!bot) {
throw new BadRequestError({
message: "Failed to find bot"

View File

@ -16,12 +16,15 @@ import { TProjectPermission } from "@app/lib/types";
import { ActorType } from "../auth/auth-type";
import { TCertificateDALFactory } from "../certificate/certificate-dal";
import { TCertificateAuthorityDALFactory } from "../certificate-authority/certificate-authority-dal";
import { TCertificateTemplateDALFactory } from "../certificate-template/certificate-template-dal";
import { TIdentityOrgDALFactory } from "../identity/identity-org-dal";
import { TIdentityProjectDALFactory } from "../identity-project/identity-project-dal";
import { TIdentityProjectMembershipRoleDALFactory } from "../identity-project/identity-project-membership-role-dal";
import { TKmsServiceFactory } from "../kms/kms-service";
import { TOrgDALFactory } from "../org/org-dal";
import { TOrgServiceFactory } from "../org/org-service";
import { TPkiAlertDALFactory } from "../pki-alert/pki-alert-dal";
import { TPkiCollectionDALFactory } from "../pki-collection/pki-collection-dal";
import { TProjectBotDALFactory } from "../project-bot/project-bot-dal";
import { TProjectEnvDALFactory } from "../project-env/project-env-dal";
import { TProjectKeyDALFactory } from "../project-key/project-key-dal";
@ -37,7 +40,9 @@ import {
TDeleteProjectDTO,
TGetProjectDTO,
TGetProjectKmsKey,
TListProjectAlertsDTO,
TListProjectCasDTO,
TListProjectCertificateTemplatesDTO,
TListProjectCertsDTO,
TLoadProjectKmsBackupDTO,
TToggleProjectAutoCapitalizationDTO,
@ -70,6 +75,9 @@ type TProjectServiceFactoryDep = {
projectUserMembershipRoleDAL: Pick<TProjectUserMembershipRoleDALFactory, "create">;
certificateAuthorityDAL: Pick<TCertificateAuthorityDALFactory, "find">;
certificateDAL: Pick<TCertificateDALFactory, "find" | "countCertificatesInProject">;
certificateTemplateDAL: Pick<TCertificateTemplateDALFactory, "getCertTemplatesByProjectId">;
pkiAlertDAL: Pick<TPkiAlertDALFactory, "find">;
pkiCollectionDAL: Pick<TPkiCollectionDALFactory, "find">;
permissionService: TPermissionServiceFactory;
orgService: Pick<TOrgServiceFactory, "addGhostUser">;
licenseService: Pick<TLicenseServiceFactory, "getPlan">;
@ -107,6 +115,9 @@ export const projectServiceFactory = ({
identityProjectMembershipRoleDAL,
certificateAuthorityDAL,
certificateDAL,
certificateTemplateDAL,
pkiCollectionDAL,
pkiAlertDAL,
keyStore,
kmsService,
projectBotDAL
@ -676,6 +687,90 @@ export const projectServiceFactory = ({
};
};
/**
* Return list of (PKI) alerts configured for project
*/
const listProjectAlerts = async ({
projectId,
actor,
actorId,
actorAuthMethod,
actorOrgId
}: TListProjectAlertsDTO) => {
const { permission } = await permissionService.getProjectPermission(
actor,
actorId,
projectId,
actorAuthMethod,
actorOrgId
);
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.PkiAlerts);
const alerts = await pkiAlertDAL.find({ projectId });
return {
alerts
};
};
/**
* Return list of PKI collections for project
*/
const listProjectPkiCollections = async ({
projectId,
actor,
actorId,
actorAuthMethod,
actorOrgId
}: TListProjectAlertsDTO) => {
const { permission } = await permissionService.getProjectPermission(
actor,
actorId,
projectId,
actorAuthMethod,
actorOrgId
);
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.PkiCollections);
const pkiCollections = await pkiCollectionDAL.find({ projectId });
return {
pkiCollections
};
};
/**
* Return list of certificate templates for project
*/
const listProjectCertificateTemplates = async ({
projectId,
actorId,
actorOrgId,
actorAuthMethod,
actor
}: TListProjectCertificateTemplatesDTO) => {
const { permission } = await permissionService.getProjectPermission(
actor,
actorId,
projectId,
actorAuthMethod,
actorOrgId
);
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Read,
ProjectPermissionSub.CertificateTemplates
);
const certificateTemplates = await certificateTemplateDAL.getCertTemplatesByProjectId(projectId);
return {
certificateTemplates
};
};
const updateProjectKmsKey = async ({
projectId,
kms,
@ -794,6 +889,9 @@ export const projectServiceFactory = ({
upgradeProject,
listProjectCas,
listProjectCertificates,
listProjectAlerts,
listProjectPkiCollections,
listProjectCertificateTemplates,
updateVersionLimit,
updateAuditLogsRetention,
updateProjectKmsKey,

View File

@ -106,6 +106,8 @@ export type TListProjectCertsDTO = {
commonName?: string;
} & Omit<TProjectPermission, "projectId">;
export type TListProjectAlertsDTO = TProjectPermission;
export type TUpdateProjectKmsDTO = {
kms: { type: KmsType.Internal } | { type: KmsType.External; kmsId: string };
} & TProjectPermission;
@ -115,3 +117,5 @@ export type TLoadProjectKmsBackupDTO = {
} & TProjectPermission;
export type TGetProjectKmsKey = TProjectPermission;
export type TListProjectCertificateTemplatesDTO = TProjectPermission;

View File

@ -4,6 +4,7 @@ import { logger } from "@app/lib/logger";
import { QueueJobs, QueueName, TQueueServiceFactory } from "@app/queue";
import { TIdentityAccessTokenDALFactory } from "../identity-access-token/identity-access-token-dal";
import { TIdentityUaClientSecretDALFactory } from "../identity-ua/identity-ua-client-secret-dal";
import { TSecretVersionDALFactory } from "../secret/secret-version-dal";
import { TSecretFolderVersionDALFactory } from "../secret-folder/secret-folder-version-dal";
import { TSecretSharingDALFactory } from "../secret-sharing/secret-sharing-dal";
@ -12,6 +13,7 @@ import { TSecretVersionV2DALFactory } from "../secret-v2-bridge/secret-version-d
type TDailyResourceCleanUpQueueServiceFactoryDep = {
auditLogDAL: Pick<TAuditLogDALFactory, "pruneAuditLog">;
identityAccessTokenDAL: Pick<TIdentityAccessTokenDALFactory, "removeExpiredTokens">;
identityUniversalAuthClientSecretDAL: Pick<TIdentityUaClientSecretDALFactory, "removeExpiredClientSecrets">;
secretVersionDAL: Pick<TSecretVersionDALFactory, "pruneExcessVersions">;
secretVersionV2DAL: Pick<TSecretVersionV2DALFactory, "pruneExcessVersions">;
secretFolderVersionDAL: Pick<TSecretFolderVersionDALFactory, "pruneExcessVersions">;
@ -30,12 +32,14 @@ export const dailyResourceCleanUpQueueServiceFactory = ({
secretFolderVersionDAL,
identityAccessTokenDAL,
secretSharingDAL,
secretVersionV2DAL
secretVersionV2DAL,
identityUniversalAuthClientSecretDAL
}: TDailyResourceCleanUpQueueServiceFactoryDep) => {
queueService.start(QueueName.DailyResourceCleanUp, async () => {
logger.info(`${QueueName.DailyResourceCleanUp}: queue task started`);
await auditLogDAL.pruneAuditLog();
await identityAccessTokenDAL.removeExpiredTokens();
await identityUniversalAuthClientSecretDAL.removeExpiredClientSecrets();
await secretSharingDAL.pruneExpiredSharedSecrets();
await snapshotDAL.pruneExcessSnapshots();
await secretVersionDAL.pruneExcessVersions();

View File

@ -36,8 +36,8 @@ type TSecretImportSecretsV2 = {
secretKey: string;
// akhilmhdh: yes i know you can put ?.
// But for somereason ts consider ? and undefined explicit as different just ts things
secretValue: string | undefined;
secretComment: string | undefined;
secretValue: string;
secretComment: string;
})[];
};
@ -157,7 +157,7 @@ export const fnSecretsV2FromImports = async ({
secretImportDAL: Pick<TSecretImportDALFactory, "findByFolderIds">;
depth?: number;
cyclicDetector?: Set<string>;
decryptor: (value?: Buffer | null) => string | undefined;
decryptor: (value?: Buffer | null) => string;
expandSecretReferences?: (
secrets: Record<string, { value?: string; comment?: string; skipMultilineEncoding?: boolean | null }>
) => Promise<Record<string, { value?: string; comment?: string; skipMultilineEncoding?: boolean | null }>>;
@ -231,6 +231,7 @@ export const fnSecretsV2FromImports = async ({
_id: item.id // The old Python SDK depends on the _id field being returned. We return this to keep the older Python SDK versions backwards compatible with the new Postgres backend.
}))
.concat(folderDeeperImportSecrets);
return {
secretPath: importPath,
environment: importEnv.slug,
@ -254,7 +255,7 @@ export const fnSecretsV2FromImports = async ({
};
return acc;
},
{} as Record<string, { value?: string; comment?: string; skipMultilineEncoding?: boolean | null }>
{} as Record<string, { value: string; comment?: string; skipMultilineEncoding?: boolean | null }>
);
// eslint-disable-next-line
await expandSecretReferences(secretsGroupByKey);

View File

@ -507,7 +507,7 @@ export const secretImportServiceFactory = ({
folderDAL,
secretDAL: secretV2BridgeDAL,
secretImportDAL,
decryptor: (value) => (value ? secretManagerDecryptor({ cipherTextBlob: value }).toString() : undefined)
decryptor: (value) => (value ? secretManagerDecryptor({ cipherTextBlob: value }).toString() : "")
});
return importedSecrets;
}

View File

@ -51,7 +51,7 @@ export const secretTagDALFactory = (db: TDbClient) => {
...secretTagOrm,
saveTagsToSecret: secretJnTagOrm.insertMany,
deleteTagsToSecret: secretJnTagOrm.delete,
saveTagsToSecretV2: secretV2JnTagOrm.insertMany,
saveTagsToSecretV2: secretV2JnTagOrm.batchInsert,
deleteTagsToSecretV2: secretV2JnTagOrm.delete,
findSecretTagsByProjectId,
deleteTagsManySecret,

View File

@ -22,16 +22,7 @@ type TSecretTagServiceFactoryDep = {
export type TSecretTagServiceFactory = ReturnType<typeof secretTagServiceFactory>;
export const secretTagServiceFactory = ({ secretTagDAL, permissionService }: TSecretTagServiceFactoryDep) => {
const createTag = async ({
name,
slug,
actor,
color,
actorId,
actorOrgId,
actorAuthMethod,
projectId
}: TCreateTagDTO) => {
const createTag = async ({ slug, actor, color, actorId, actorOrgId, actorAuthMethod, projectId }: TCreateTagDTO) => {
const { permission } = await permissionService.getProjectPermission(
actor,
actorId,
@ -46,7 +37,6 @@ export const secretTagServiceFactory = ({ secretTagDAL, permissionService }: TSe
const newTag = await secretTagDAL.create({
projectId,
name,
slug,
color,
createdBy: actorId,
@ -55,7 +45,7 @@ export const secretTagServiceFactory = ({ secretTagDAL, permissionService }: TSe
return newTag;
};
const updateTag = async ({ actorId, actor, actorOrgId, actorAuthMethod, id, name, color, slug }: TUpdateTagDTO) => {
const updateTag = async ({ actorId, actor, actorOrgId, actorAuthMethod, id, color, slug }: TUpdateTagDTO) => {
const tag = await secretTagDAL.findById(id);
if (!tag) throw new BadRequestError({ message: "Tag doesn't exist" });
@ -73,7 +63,7 @@ export const secretTagServiceFactory = ({ secretTagDAL, permissionService }: TSe
);
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Edit, ProjectPermissionSub.Tags);
const updatedTag = await secretTagDAL.updateById(tag.id, { name, color, slug });
const updatedTag = await secretTagDAL.updateById(tag.id, { color, slug });
return updatedTag;
};
@ -107,7 +97,7 @@ export const secretTagServiceFactory = ({ secretTagDAL, permissionService }: TSe
);
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.Tags);
return tag;
return { ...tag, name: tag.slug };
};
const getTagBySlug = async ({ actorId, actor, actorOrgId, actorAuthMethod, slug, projectId }: TGetTagBySlugDTO) => {
@ -123,7 +113,7 @@ export const secretTagServiceFactory = ({ secretTagDAL, permissionService }: TSe
);
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.Tags);
return tag;
return { ...tag, name: tag.slug };
};
const getProjectTags = async ({ actor, actorId, actorOrgId, actorAuthMethod, projectId }: TListProjectTagsDTO) => {

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