Compare commits

..

207 Commits

Author SHA1 Message Date
bf97294dad misc: added idp label 2024-11-15 01:41:20 +08:00
4ba3899861 doc: add docs for gitlab oidc auth 2024-11-15 01:07:36 +08:00
2d2ad0724f Merge pull request #2681 from Infisical/dependabot/npm_and_yarn/frontend/multi-6b7e5c81f3
chore(deps): bump body-parser and express in /frontend
2024-11-11 17:36:37 -07:00
e90efb7fc8 Merge pull request #2684 from Infisical/daniel/hsm-docs
docs: hardware security module
2024-11-11 16:21:56 -07:00
17d5e4bdab Merge pull request #2671 from Infisical/daniel/hsm
feat: hardware security module's support
2024-11-11 15:38:02 -07:00
f22a5580a6 requested changes 2024-11-12 02:27:38 +04:00
0e946f73bd Merge pull request #2713 from scott-ray-wilson/bitbucket-integration-additions
Feature: Add Support for Deployment Environment Scope for Bitbucket Integration
2024-11-11 11:27:12 -08:00
7b8551f883 fix: use constant url for bitbucket update/create secret 2024-11-11 10:56:26 -08:00
3b1ce86ee6 Merge pull request #2704 from Infisical/feat/add-support-for-no-bootstrap-cert-est
feat: add support for EST device enrollment without bootstrap certs
2024-11-12 02:40:37 +08:00
c649661133 misc: remove not nullable from alter 2024-11-12 02:35:21 +08:00
70e44d04ef Merge pull request #2720 from akhilmhdh/fix/random-patch
feat: random patches
2024-11-11 11:35:04 -07:00
=
0dddd58be1 feat: random patches 2024-11-11 23:59:26 +05:30
148f522c58 updated migrations 2024-11-11 21:52:35 +04:00
d4c911a28f feature: add support for deployment environment scope for bitbucket and refactor bitbucket create UI 2024-11-11 09:47:23 -08:00
603fcd8ab5 Update hsm-service.ts 2024-11-11 21:47:07 +04:00
a1474145ae Update hsm-service.ts 2024-11-11 21:47:07 +04:00
7c055f71f7 Update hsm-service.ts 2024-11-11 21:47:07 +04:00
14884cd6b0 Update Dockerfile.standalone-infisical 2024-11-11 21:47:07 +04:00
98fd146e85 cleanup 2024-11-11 21:47:07 +04:00
1d3dca11e7 Revert "temp: team debugging"
This reverts commit 6533d731f829d79f41bf2f7209e3a636553792b1.
2024-11-11 21:47:00 +04:00
22f8a3daa7 temp: team debugging 2024-11-11 21:46:53 +04:00
395b3d9e05 requested changes
requested changes

temp: team debugging

Revert "temp: team debugging"

This reverts commit 6533d731f829d79f41bf2f7209e3a636553792b1.

feat: hsm support

Update hsm-service.ts

feat: hsm support
2024-11-11 21:45:06 +04:00
1041e136fb added keystore 2024-11-11 21:45:06 +04:00
21024b0d72 requested changes 2024-11-11 21:45:06 +04:00
00e68dc0bf Update hsm-fns.ts 2024-11-11 21:45:06 +04:00
5e068cd8a0 feat: wait for session wrapper 2024-11-11 21:45:06 +04:00
abdf8f46a3 Update super-admin-service.ts 2024-11-11 21:45:06 +04:00
1cf046f6b3 Update super-admin-service.ts 2024-11-11 21:45:06 +04:00
0fda6d6f4d requested changes 2024-11-11 21:45:06 +04:00
8d4115925c requested changes 2024-11-11 21:45:06 +04:00
d0b3c6b66a Create docker-compose.hsm.prod.yml 2024-11-11 21:45:06 +04:00
a1685af119 feat: hsm cryptographic tests 2024-11-11 21:45:06 +04:00
8d4a06e9e4 modified: src/lib/config/env.ts 2024-11-11 21:45:06 +04:00
6dbe3c8793 fix: removed exported field 2024-11-11 21:45:06 +04:00
a3ec1a27de fix: removed recovery 2024-11-11 21:45:06 +04:00
472f02e8b1 feat: added key wrapping 2024-11-11 21:45:06 +04:00
3989646b80 fix: dockerfile 2024-11-11 21:45:06 +04:00
472f5eb8b4 Update env.ts 2024-11-11 21:45:05 +04:00
f5b039f939 Update vitest-environment-knex.ts 2024-11-11 21:45:05 +04:00
b7b3d07e9f cleanup 2024-11-11 21:45:05 +04:00
891a1ea2b9 feat: HSM support 2024-11-11 21:45:05 +04:00
a807f0cf6c feat: added option for choosing encryption method 2024-11-11 21:45:05 +04:00
cfc0b2fb8d fix: renamed migration 2024-11-11 21:45:05 +04:00
f096a567de feat: Hardware security modules 2024-11-11 21:45:05 +04:00
65d642113d Update mint.json 2024-11-10 21:06:57 -07:00
92e7e90c21 Merge pull request #2701 from scott-ray-wilson/project-templates-feature
Feature: Project Templates
2024-11-10 21:03:11 -07:00
f9f6ec0a8d Merge pull request #2717 from Infisical/misc/true-myssql-rotation-flag-default
misc: made myssql rotation flag true in example
2024-11-10 20:43:11 -07:00
d9621b0b17 misc: made myssql rotation flag true in example 2024-11-11 11:42:14 +08:00
d80a70731d Merge pull request #2641 from Infisical/feat/ldap-static-dynamic-secret
feat: static ldap credentials
2024-11-10 13:01:56 +08:00
bd99b4e356 improvement: reduce json max size limit based of aws policy limit 2024-11-09 16:32:50 -08:00
7db0bd7daa Merge pull request #2702 from felixhummel/main
docs: fix link to cli
2024-11-09 09:01:08 -05:00
8bc538af93 Merge pull request #2711 from Infisical/misc/moved-aws-sm-integration-to-react-hook-form
misc: moved aws secret manager integration to react hook form
2024-11-09 08:59:38 -05:00
8ef078872e Update hsm-integration.mdx 2024-11-09 05:16:52 +04:00
d5f718c6ad improve: improve template form buttons 2024-11-08 14:07:50 -08:00
5f93016d22 Update hsm-integration.mdx 2024-11-09 00:41:39 +04:00
f220246eb4 feat: hsm docs 2024-11-09 00:33:54 +04:00
829b399cda misc: moved to react hook form 2024-11-09 03:02:22 +08:00
f91f9c9487 Merge pull request #2707 from akhilmhdh/feat/create-secret-tag
feat: added tag support on create secret
2024-11-08 12:47:32 -05:00
f0d19e4701 fix: handle tag select overflow for create secret modal. minor text revisions. 2024-11-08 09:29:42 -08:00
=
7eeff6c406 feat: added banner to notify user doesn't have permission to read tags 2024-11-08 21:04:20 +05:30
=
132c3080bb feat: added tag support on create secret 2024-11-08 19:16:19 +05:30
bf09fa33fa Merge pull request #2705 from Infisical/vmatsiiako-changelog-patch-1-2
Update changelog
2024-11-07 18:27:49 -08:00
a87e7b792c fix typo 2024-11-07 18:17:43 -08:00
e8ca020903 Update changelog 2024-11-07 18:10:59 -08:00
a603938488 Fix typo 2024-11-07 18:08:29 -08:00
cff7981fe0 Added Update component to changelog 2024-11-07 18:07:25 -08:00
b39d5c6682 Update changelog 2024-11-07 17:54:28 -08:00
829ae7d3c0 chore: revert license 2024-11-07 12:40:48 -08:00
19c26c680c improvement: address requested feedback 2024-11-07 12:38:33 -08:00
dd1f1d07cc Merge pull request #2689 from Infisical/doc/updated-internal-permission
doc: updated internal permission docs for v2
2024-11-07 13:42:01 -05:00
027b200b1a misc: renamed disable flag + docs 2024-11-08 02:02:12 +08:00
c3f8c55672 Merge pull request #2703 from Infisical/remove-ip
Remove unused ip package from frontend
2024-11-07 09:54:10 -08:00
75aeef3897 Remove ip package from frontend 2024-11-07 09:48:14 -08:00
e761e65322 feat: add support for no bootstrap cert EST 2024-11-08 01:42:47 +08:00
c97fe77aec Merge pull request #2696 from akhilmhdh/fix/debounce-secret-sync
feat: added queue level debounce for secret sync and removed stale check
2024-11-07 12:37:36 -05:00
370ed45abb docs: fix link to cli 2024-11-07 15:27:42 +01:00
3e16d7e160 doc: added migration tips 2024-11-07 18:51:26 +08:00
6bf4b4a380 Merge pull request #2692 from Infisical/daniel/more-envkey-fixes
fix(external-migrations): env-key edge cases
2024-11-07 02:27:46 -05:00
61f786e8d8 chore: add comment explaining ID 2024-11-06 23:25:31 -08:00
26064e3a08 docs: add images 2024-11-06 23:13:13 -08:00
9b246166a1 feature: project templates with docs 2024-11-06 23:12:32 -08:00
9dedaa6779 update infisical helm docs 2024-11-06 16:57:02 -05:00
8eab7d2f01 Merge pull request #2700 from Infisical/infisical-helm-auto-create-sa
Add support for auto creating SA for job and deployment
2024-11-06 16:41:57 -05:00
4e796e7e41 Add support for auto creating SA for job and deployment 2024-11-06 16:37:34 -05:00
c6fa647825 Merge pull request #2699 from Infisical/misc/address-remaining-ui-ux-issues-audit
misc: address other ui/ux issues with audit logs
2024-11-06 14:24:42 -05:00
496cebb08f misc: address other ui/ux issues with audit 2024-11-07 03:07:39 +08:00
33db6df7f2 Merge pull request #2698 from Infisical/misc/made-audit-logs-metadata-json
misc: made audit logs metadata into json
2024-11-06 12:36:28 -05:00
88d25e97e9 misc: added undefined handling for actor 2024-11-07 01:33:38 +08:00
4ad9fa1ad1 misc: made audit logs metadata into json 2024-11-07 01:26:26 +08:00
=
1642fb42d8 feat: resolved test failing due to timeout 2024-11-06 16:54:54 +05:30
=
3983c2bc4a feat: added queue level debounce for secret sync and removed stale check in sync 2024-11-06 16:29:03 +05:30
34d87ca30f Update external-migration-fns.ts 2024-11-06 10:49:45 +04:00
12b6f27151 fix envkey 2024-11-06 10:35:27 +04:00
ea426e8b2d Merge pull request #2685 from akhilmhdh/fix/tag-no-update-in-approval
fix: resolved tag update not happening via approval
2024-11-05 09:54:13 -05:00
=
4d567f0b08 fix: resolved tag update not happening via approval 2024-11-05 20:18:16 +05:30
6548372e3b Merge pull request #2690 from Infisical/feat/add-mssql-secret-rotation-support
feat: add mssql secret rotation template
2024-11-05 22:33:56 +08:00
77af640c4c misc: addressed lint issues 2024-11-05 22:22:41 +08:00
90f85152bc misc: added configurable env for enabling/disabling encrypt 2024-11-05 22:08:16 +08:00
cfa8770bdc misc: addressed issue 2024-11-05 21:57:40 +08:00
be8562824d feat: add mssql secret rotation template 2024-11-05 18:38:09 +08:00
6956d14e2e docs: suggested changes 2024-11-04 19:46:25 -08:00
4f1fe8a9fa doc: updated overview 2024-11-05 01:37:26 +08:00
b0031b71e0 doc: updated internal permission docs 2024-11-05 01:21:35 +08:00
bae7c6c3d7 docs: hardware security module 2024-11-04 03:22:32 +04:00
7503876ca0 Merge pull request #2683 from Infisical/blueprint-org-structure
added blueprint for org structure
2024-11-03 09:48:27 -08:00
36b5a3dc90 fix typo 2024-11-03 09:40:33 -08:00
dfe36f346f Merge pull request #2682 from cyberbohu/patch-1
Update overview.mdx
2024-11-03 09:29:56 -08:00
b1b61842c6 added blueprint for org structure 2024-11-03 09:29:05 -08:00
f9ca9b51b2 Update overview.mdx
spell check
2024-11-03 12:37:30 +01:00
e8b33f27fc chore(deps): bump body-parser and express in /frontend
Bumps [body-parser](https://github.com/expressjs/body-parser) and [express](https://github.com/expressjs/express). These dependencies needed to be updated together.

Updates `body-parser` from 1.20.2 to 1.20.3
- [Release notes](https://github.com/expressjs/body-parser/releases)
- [Changelog](https://github.com/expressjs/body-parser/blob/master/HISTORY.md)
- [Commits](https://github.com/expressjs/body-parser/compare/1.20.2...1.20.3)

Updates `express` from 4.19.2 to 4.21.1
- [Release notes](https://github.com/expressjs/express/releases)
- [Changelog](https://github.com/expressjs/express/blob/4.21.1/History.md)
- [Commits](https://github.com/expressjs/express/compare/4.19.2...4.21.1)

---
updated-dependencies:
- dependency-name: body-parser
  dependency-type: indirect
- dependency-name: express
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-11-02 22:36:32 +00:00
7e7e6ade5c Update deployment-pipeline.yml 2024-11-02 13:19:50 -04:00
4010817916 Increase batch size and remove transation 2024-11-02 12:48:34 -04:00
eea367c3bc Merge pull request #2606 from Infisical/daniel/multiple-auth-methods
feat: multiple auth methods for identities
2024-11-02 12:17:37 -04:00
56567ee7c9 Update deployment-pipeline.yml 2024-11-02 11:31:37 -04:00
6b7bc2a3c4 Merge pull request #2677 from un/main
fix: minor typo
2024-11-01 13:12:07 -07:00
cb52568ebd fix: minor typo 2024-11-01 19:59:08 +01:00
9d30fb3870 Merge pull request #2676 from scott-ray-wilson/oidc-default-org-docs
Docs: OIDC Default Org Support and OIDC/SAML Tip/Info Improvements
2024-11-01 10:46:15 -07:00
161ac5e097 docs: oidc added to default org description and improve oidc/smal info/tips 2024-11-01 10:38:57 -07:00
bb5b585cf6 Merge pull request #2674 from scott-ray-wilson/docs-update-api-base-url
Docs: Update OpenAPI Spec Servers
2024-11-01 00:54:51 -04:00
fa94191c40 Merge pull request #2673 from areifert/misc/make-azure-devops-variables-secret
Make synced Azure DevOps variables secret
2024-10-31 20:39:59 -07:00
6a5eabc411 docs: update urls for openapi docs 2024-10-31 19:51:26 -07:00
c956a0f91f Merge pull request #2667 from scott-ray-wilson/oidc-default-org-slug
Feature: OIDC Default Org
2024-10-31 21:56:53 -04:00
df7b55606e feature: oidc support for oidc and only display saml/oidc login if enforced 2024-10-31 15:13:13 -07:00
5f14b27f41 Make Azure DevOps variables secret 2024-10-31 13:29:43 -06:00
02b2395276 Merge pull request #2666 from scott-ray-wilson/snowflake-dynamic-secrets
Feature: Snowflake Dynamic Secrets
2024-10-31 11:58:00 -07:00
402fa2b0e0 fix: correct typo 2024-10-31 11:53:02 -07:00
3725241f52 improvement: improve error for leases 2024-10-31 10:39:43 -07:00
10b457a695 fix: correct early return for renew 2024-10-31 10:31:50 -07:00
3912e2082d fix: check that renew statement actually exists 2024-10-31 10:27:55 -07:00
7dd6eac20a improvement: address feedback 2024-10-31 10:24:30 -07:00
5664e1ff26 Merge pull request #2670 from Infisical/feat/added-key-id-column
feat: added key id column
2024-11-01 01:14:46 +08:00
a27a428329 misc: added mint json changes 2024-11-01 00:47:06 +08:00
b196251c19 doc: add kubernetes encryption 2024-11-01 00:42:23 +08:00
b18d8d542f misc: add copy to clipboard 2024-11-01 00:22:21 +08:00
3c287600ab Merge pull request #2645 from scott-ray-wilson/secrets-quick-search
Feature: Secrets Dashboard Quick/Deep Search
2024-10-31 08:41:54 -07:00
759d11ff21 feat: added key id column 2024-10-31 19:05:53 +08:00
2bd817765c Merge pull request #2668 from Infisical/vmatsiiako-link-patch-1
Update audit-log-streams.mdx
2024-10-30 23:08:12 -04:00
7aa9c5dd00 Update audit-log-streams.mdx 2024-10-30 19:55:32 -07:00
b693c035ce chore: remove dev value 2024-10-30 15:02:25 -07:00
c65a991943 fix: add missing type properties on client side 2024-10-30 15:01:21 -07:00
3a3811cb3c feature: snowflake dynamic secrets 2024-10-30 14:57:15 -07:00
332ca61f5d Merge pull request #2665 from akhilmhdh/fix/resolve-service-token
feat: fixed missing secret folder and import permission in service token
2024-10-30 15:50:59 -04:00
=
64f43e59d0 feat: fixed missing secret folder and import permission in service token 2024-10-31 01:17:41 +05:30
ccaf4c00af Merge pull request #2663 from Infisical/vmatsiiako-docs-link-patch-1
fix link in docs
2024-10-30 15:47:24 -04:00
e3ba1c59bf improvement: add search filter tooltip to quick search 2024-10-30 10:38:25 -07:00
ce0bc191d8 Merge pull request #2655 from Daemoen/daemoen/minor-grammar-correction
Daemoen/minor grammar correction
2024-10-30 09:07:51 -07:00
489ccb8e15 fix link in docs 2024-10-29 21:57:50 -07:00
ae8f695b6f Update attribute-based-access-controls.mdx 2024-10-29 23:10:53 -04:00
19357d4bd7 Merge pull request #2662 from Infisical/vmatsiiako-docs-audit-patch-1
Update audit-log-streams.mdx
2024-10-29 23:07:16 -04:00
776d0a0fe1 Update audit-log-streams.mdx 2024-10-29 20:06:27 -07:00
85dec28667 Merge pull request #2661 from Infisical/maidul-ABAC
abac docs
2024-10-29 18:40:25 -04:00
21ea7dd317 feature: deep search for secrets dashboard 2024-10-29 15:08:19 -07:00
57e214ef50 improvement: add back comma 2024-10-29 14:46:53 -07:00
1986fe9617 improvement: minor doc adjustment and add new page to sidebar 2024-10-29 14:45:38 -07:00
1309f30af9 Merge pull request #2660 from scott-ray-wilson/invite-link-expanded
Improvement: Display Full Invite Link in Insecure Context and Increase Default Pagination Size to 100
2024-10-29 13:32:09 -07:00
89a4fc91ca abac docs 2024-10-29 15:42:38 -04:00
af0ec2400d Reverting as not to affect logging consistency 2024-10-29 12:08:09 -07:00
770e73e40b improvement: adjust default pagination size and do not truncate invite links in insecure context 2024-10-29 11:10:51 -07:00
39fdeabdea Merge pull request #2646 from Infisical/feat/sap-hana-dynamic-secrets
feat: SAP HANA dynamic secrets
2024-10-29 22:45:46 +08:00
25c26f2cde Merge pull request #2658 from Infisical/misc/add-missing-helm-updates-operator
misc: added helm related configs for operator
2024-10-29 09:53:17 -04:00
1ca8b9ba08 misc: install secret operator updates 2024-10-29 21:50:23 +08:00
14d9fe01e0 misc: updated chart 2024-10-29 21:46:17 +08:00
216810f289 misc: added helm related configs 2024-10-29 13:37:03 +08:00
f530b78eb8 Merge pull request #2652 from Infisical/feat/add-support-for-custom-ca
feat: add support for custom ca in k8 operator
2024-10-29 01:14:30 -04:00
c3809ed22b Merge branch 'feat/add-support-for-custom-ca' of https://github.com/Infisical/infisical into feat/add-support-for-custom-ca 2024-10-29 12:00:09 +08:00
9f85d8bba1 feat: added handling of empty ca 2024-10-29 11:59:41 +08:00
1056645ee3 fix small nit 2024-10-28 22:25:21 -04:00
5e9914b738 Merge pull request #2657 from Infisical/vmatsiiako--docs-patch-1
Update docker-swarm.mdx
2024-10-28 22:17:19 -04:00
1ea52e6a80 update chart version 2024-10-28 21:03:27 -04:00
20da697de8 rename change log file 2024-10-28 21:01:20 -04:00
16abf48081 add change log 2024-10-28 20:56:42 -04:00
e73ae485bc patch service account namespace 2024-10-28 20:32:38 -04:00
621f73e223 add support for variable init container img 2024-10-28 20:32:38 -04:00
93e69bd34e Merge pull request #2656 from scott-ray-wilson/insecure-context-banner
Feature: Display Warning Banner for Insecure Connection
2024-10-28 16:47:18 -07:00
e382135384 improvements: make banner full width and adjust icon/margins 2024-10-28 16:43:15 -07:00
f2a554b5fd Update docker-swarm.mdx 2024-10-28 16:16:36 -07:00
df5bdf3773 feature: display warning banner for insecure context 2024-10-28 16:00:40 -07:00
8401048daf Final fix 2024-10-28 13:28:47 -07:00
335a87d856 Think I got them all 2024-10-28 13:20:36 -07:00
1add9dd965 Minor adjustments to grammar and consistency 2024-10-28 13:00:29 -07:00
df46daf93d Merge pull request #2654 from scott-ray-wilson/kms-doc-fix
Docs: Correct KMS API Docs for Decrypt Endpoint
2024-10-28 11:18:43 -07:00
f82f7ae8d0 fix: correct api constant reference for kms docs 2024-10-28 11:10:04 -07:00
8536a1c987 Merge pull request #2653 from scott-ray-wilson/fix-copy-shared-secret-link
Fix: Copy Shared Secret Link to Clipboard on Generate
2024-10-28 10:56:19 -07:00
b3cf43b46d fix: copy shared secret link to clipboard on generate 2024-10-28 10:25:09 -07:00
9d4dbb63ae misc: updated go-sdk version 2024-10-28 21:49:34 +08:00
9c6f23fba6 misc: documentation and samples 2024-10-28 17:45:49 +08:00
babe483ca9 feat: add support for custom ca in k8 operator 2024-10-28 17:03:56 +08:00
38ede687cd Merge pull request #2650 from Infisical/revert-2649-revert-2603-feat/secret-reference-path-way
"feat: secret reference graph for understanding how its pulled""
2024-10-27 22:04:52 -04:00
5f465c4832 update env of prod eu ci 2024-10-26 21:19:53 -04:00
a0618086b0 update sts endpoint for eu ci 2024-10-26 20:38:37 -04:00
9a9bb4ca43 update eu deployment job 2024-10-26 18:45:12 -04:00
b68ddfae1b wait for gamma to be fully deployed 2024-10-26 17:57:28 -04:00
7646670378 update ci job names 2024-10-26 17:46:27 -04:00
d18be0f74c fix deployment ci 2024-10-26 17:44:05 -04:00
ec96db3503 Add EU support in deployment 2024-10-26 17:41:08 -04:00
bed620aad0 fix: downgrade collapsible version 2024-10-26 11:12:38 -07:00
02d9dbb987 Revert "Revert "feat: secret reference graph for understanding how its pulled"" 2024-10-26 13:51:07 -04:00
3841394eb7 misc: migrated existing to new helper 2024-10-26 03:18:57 +08:00
3552119c7d misc: moved host validity check to helper 2024-10-25 22:49:57 +08:00
7a46725523 doc: added note for transaction 2024-10-25 22:45:06 +08:00
3bc39c6cec feat: add usage of ca 2024-10-25 21:29:47 +08:00
b5b1e57fe7 doc: sap hana 2024-10-25 21:19:22 +08:00
1a5f66fe46 feat: added support for sap hana dynamic secrets 2024-10-25 20:33:23 +08:00
ac0cb6d96f misc: updated docs 2024-10-24 20:37:39 +08:00
f71f894de8 feat: added rotation handling for static ldap 2024-10-24 02:45:09 +08:00
66d2cc8947 misc: updated ldap edit 2024-10-24 02:10:55 +08:00
e034aa381a feat: initial schema setup 2024-10-24 00:29:41 +08:00
319 changed files with 16129 additions and 3584 deletions

View File

@ -78,3 +78,5 @@ PLAIN_API_KEY=
PLAIN_WISH_LABEL_IDS=
SSL_CLIENT_CERTIFICATE_HEADER_KEY=
ENABLE_MSSQL_SECRET_ROTATION_ENCRYPT=true

View File

@ -7,12 +7,12 @@ permissions:
jobs:
infisical-tests:
name: Run tests before deployment
name: Integration tests
# https://docs.github.com/en/actions/using-workflows/reusing-workflows#overview
uses: ./.github/workflows/run-backend-tests.yml
infisical-image:
name: Build backend image
name: Build
runs-on: ubuntu-latest
needs: [infisical-tests]
steps:
@ -102,10 +102,10 @@ jobs:
task-definition: ${{ steps.render-web-container.outputs.task-definition }}
service: infisical-core-gamma-stage
cluster: infisical-gamma-stage
wait-for-service-stability: false
wait-for-service-stability: true
production-postgres-deployment:
name: Deploy to production
production-us:
name: US production deploy
runs-on: ubuntu-latest
needs: [gamma-deployment]
environment:
@ -159,3 +159,54 @@ jobs:
service: infisical-core-platform
cluster: infisical-core-platform
wait-for-service-stability: true
production-eu:
name: EU production deploy
runs-on: ubuntu-latest
needs: [production-us]
environment:
name: production-eu
steps:
- uses: twingate/github-action@v1
with:
service-key: ${{ secrets.TWINGATE_SERVICE_KEY }}
- name: Configure AWS Credentials
uses: aws-actions/configure-aws-credentials@v4
with:
audience: sts.amazonaws.com
aws-region: eu-central-1
role-to-assume: arn:aws:iam::345594589636:role/gha-make-prod-deployment
- name: Checkout code
uses: actions/checkout@v2
- name: Setup Node.js environment
uses: actions/setup-node@v2
with:
node-version: "20"
- name: Change directory to backend and install dependencies
env:
DB_CONNECTION_URI: ${{ secrets.DB_CONNECTION_URI }}
run: |
cd backend
npm install
npm run migration:latest
- name: Save commit hashes for tag
id: commit
uses: pr-mpt/actions-commit-hash@v2
- name: Download task definition
run: |
aws ecs describe-task-definition --task-definition infisical-core-platform --query taskDefinition > task-definition.json
- name: Render Amazon ECS task definition
id: render-web-container
uses: aws-actions/amazon-ecs-render-task-definition@v1
with:
task-definition: task-definition.json
container-name: infisical-core-platform
image: infisical/staging_infisical:${{ steps.commit.outputs.short }}
environment-variables: "LOG_LEVEL=info"
- name: Deploy to Amazon ECS service
uses: aws-actions/amazon-ecs-deploy-task-definition@v1
with:
task-definition: ${{ steps.render-web-container.outputs.task-definition }}
service: infisical-core-platform
cluster: infisical-core-platform
wait-for-service-stability: true

View File

@ -1,62 +1,115 @@
name: Release standalone docker image
on:
push:
tags:
- "infisical/v*.*.*-postgres"
push:
tags:
- "infisical/v*.*.*-postgres"
jobs:
infisical-tests:
name: Run tests before deployment
# https://docs.github.com/en/actions/using-workflows/reusing-workflows#overview
uses: ./.github/workflows/run-backend-tests.yml
infisical-standalone:
name: Build infisical standalone image postgres
runs-on: ubuntu-latest
needs: [infisical-tests]
steps:
- name: Extract version from tag
id: extract_version
run: echo "::set-output name=version::${GITHUB_REF_NAME#infisical/}"
- name: ☁️ Checkout source
uses: actions/checkout@v3
with:
fetch-depth: 0
- name: 📦 Install dependencies to test all dependencies
run: npm ci --only-production
working-directory: backend
- name: version output
run: |
echo "Output Value: ${{ steps.version.outputs.major }}"
echo "Output Value: ${{ steps.version.outputs.minor }}"
echo "Output Value: ${{ steps.version.outputs.patch }}"
echo "Output Value: ${{ steps.version.outputs.version }}"
echo "Output Value: ${{ steps.version.outputs.version_type }}"
echo "Output Value: ${{ steps.version.outputs.increment }}"
- name: Save commit hashes for tag
id: commit
uses: pr-mpt/actions-commit-hash@v2
- name: 🔧 Set up Docker Buildx
uses: docker/setup-buildx-action@v2
- name: 🐋 Login to Docker Hub
uses: docker/login-action@v2
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Set up Depot CLI
uses: depot/setup-action@v1
- name: 📦 Build backend and export to Docker
uses: depot/build-push-action@v1
with:
project: 64mmf0n610
token: ${{ secrets.DEPOT_PROJECT_TOKEN }}
push: true
context: .
tags: |
infisical/infisical:latest-postgres
infisical/infisical:${{ steps.commit.outputs.short }}
infisical/infisical:${{ steps.extract_version.outputs.version }}
platforms: linux/amd64,linux/arm64
file: Dockerfile.standalone-infisical
build-args: |
POSTHOG_API_KEY=${{ secrets.PUBLIC_POSTHOG_API_KEY }}
INFISICAL_PLATFORM_VERSION=${{ steps.extract_version.outputs.version }}
infisical-tests:
name: Run tests before deployment
# https://docs.github.com/en/actions/using-workflows/reusing-workflows#overview
uses: ./.github/workflows/run-backend-tests.yml
infisical-standalone:
name: Build infisical standalone image postgres
runs-on: ubuntu-latest
needs: [infisical-tests]
steps:
- name: Extract version from tag
id: extract_version
run: echo "::set-output name=version::${GITHUB_REF_NAME#infisical/}"
- name: ☁️ Checkout source
uses: actions/checkout@v3
with:
fetch-depth: 0
- name: 📦 Install dependencies to test all dependencies
run: npm ci --only-production
working-directory: backend
- name: version output
run: |
echo "Output Value: ${{ steps.version.outputs.major }}"
echo "Output Value: ${{ steps.version.outputs.minor }}"
echo "Output Value: ${{ steps.version.outputs.patch }}"
echo "Output Value: ${{ steps.version.outputs.version }}"
echo "Output Value: ${{ steps.version.outputs.version_type }}"
echo "Output Value: ${{ steps.version.outputs.increment }}"
- name: Save commit hashes for tag
id: commit
uses: pr-mpt/actions-commit-hash@v2
- name: 🔧 Set up Docker Buildx
uses: docker/setup-buildx-action@v2
- name: 🐋 Login to Docker Hub
uses: docker/login-action@v2
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Set up Depot CLI
uses: depot/setup-action@v1
- name: 📦 Build backend and export to Docker
uses: depot/build-push-action@v1
with:
project: 64mmf0n610
token: ${{ secrets.DEPOT_PROJECT_TOKEN }}
push: true
context: .
tags: |
infisical/infisical:latest-postgres
infisical/infisical:${{ steps.commit.outputs.short }}
infisical/infisical:${{ steps.extract_version.outputs.version }}
platforms: linux/amd64,linux/arm64
file: Dockerfile.standalone-infisical
build-args: |
POSTHOG_API_KEY=${{ secrets.PUBLIC_POSTHOG_API_KEY }}
INFISICAL_PLATFORM_VERSION=${{ steps.extract_version.outputs.version }}
infisical-fips-standalone:
name: Build infisical standalone image postgres
runs-on: ubuntu-latest
needs: [infisical-tests]
steps:
- name: Extract version from tag
id: extract_version
run: echo "::set-output name=version::${GITHUB_REF_NAME#infisical/}"
- name: ☁️ Checkout source
uses: actions/checkout@v3
with:
fetch-depth: 0
- name: 📦 Install dependencies to test all dependencies
run: npm ci --only-production
working-directory: backend
- name: version output
run: |
echo "Output Value: ${{ steps.version.outputs.major }}"
echo "Output Value: ${{ steps.version.outputs.minor }}"
echo "Output Value: ${{ steps.version.outputs.patch }}"
echo "Output Value: ${{ steps.version.outputs.version }}"
echo "Output Value: ${{ steps.version.outputs.version_type }}"
echo "Output Value: ${{ steps.version.outputs.increment }}"
- name: Save commit hashes for tag
id: commit
uses: pr-mpt/actions-commit-hash@v2
- name: 🔧 Set up Docker Buildx
uses: docker/setup-buildx-action@v2
- name: 🐋 Login to Docker Hub
uses: docker/login-action@v2
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Set up Depot CLI
uses: depot/setup-action@v1
- name: 📦 Build backend and export to Docker
uses: depot/build-push-action@v1
with:
project: 64mmf0n610
token: ${{ secrets.DEPOT_PROJECT_TOKEN }}
push: true
context: .
tags: |
infisical/infisical-fips:latest-postgres
infisical/infisical-fips:${{ steps.commit.outputs.short }}
infisical/infisical-fips:${{ steps.extract_version.outputs.version }}
platforms: linux/amd64,linux/arm64
file: Dockerfile.fips.standalone-infisical
build-args: |
POSTHOG_API_KEY=${{ secrets.PUBLIC_POSTHOG_API_KEY }}
INFISICAL_PLATFORM_VERSION=${{ steps.extract_version.outputs.version }}

View File

@ -6,3 +6,4 @@ frontend/src/views/Project/MembersPage/components/MemberListTab/MemberRoleForm/S
docs/self-hosting/configuration/envars.mdx:generic-api-key:106
frontend/src/views/Project/MembersPage/components/MemberListTab/MemberRoleForm/SpecificPrivilegeSection.tsx:generic-api-key:451
docs/mint.json:generic-api-key:651
backend/src/ee/services/hsm/hsm-service.ts:generic-api-key:134

View File

@ -0,0 +1,167 @@
ARG POSTHOG_HOST=https://app.posthog.com
ARG POSTHOG_API_KEY=posthog-api-key
ARG INTERCOM_ID=intercom-id
ARG CAPTCHA_SITE_KEY=captcha-site-key
FROM node:20-slim AS base
FROM base AS frontend-dependencies
WORKDIR /app
COPY frontend/package.json frontend/package-lock.json frontend/next.config.js ./
# Install dependencies
RUN npm ci --only-production --ignore-scripts
# Rebuild the source code only when needed
FROM base AS frontend-builder
WORKDIR /app
# Copy dependencies
COPY --from=frontend-dependencies /app/node_modules ./node_modules
# Copy all files
COPY /frontend .
ENV NODE_ENV production
ENV NEXT_PUBLIC_ENV production
ARG POSTHOG_HOST
ENV NEXT_PUBLIC_POSTHOG_HOST $POSTHOG_HOST
ARG POSTHOG_API_KEY
ENV NEXT_PUBLIC_POSTHOG_API_KEY $POSTHOG_API_KEY
ARG INTERCOM_ID
ENV NEXT_PUBLIC_INTERCOM_ID $INTERCOM_ID
ARG INFISICAL_PLATFORM_VERSION
ENV NEXT_PUBLIC_INFISICAL_PLATFORM_VERSION $INFISICAL_PLATFORM_VERSION
ARG CAPTCHA_SITE_KEY
ENV NEXT_PUBLIC_CAPTCHA_SITE_KEY $CAPTCHA_SITE_KEY
# Build
RUN npm run build
# Production image
FROM base AS frontend-runner
WORKDIR /app
RUN groupadd -r -g 1001 nodejs && useradd -r -u 1001 -g nodejs non-root-user
RUN mkdir -p /app/.next/cache/images && chown non-root-user:nodejs /app/.next/cache/images
VOLUME /app/.next/cache/images
COPY --chown=non-root-user:nodejs --chmod=555 frontend/scripts ./scripts
COPY --from=frontend-builder /app/public ./public
RUN chown non-root-user:nodejs ./public/data
COPY --from=frontend-builder --chown=non-root-user:nodejs /app/.next/standalone ./
COPY --from=frontend-builder --chown=non-root-user:nodejs /app/.next/static ./.next/static
USER non-root-user
ENV NEXT_TELEMETRY_DISABLED 1
##
## BACKEND
##
FROM base AS backend-build
ENV ChrystokiConfigurationPath=/usr/safenet/lunaclient/
RUN groupadd -r -g 1001 nodejs && useradd -r -u 1001 -g nodejs non-root-user
WORKDIR /app
# Required for pkcs11js
RUN apt-get update && apt-get install -y \
python3 \
make \
g++ \
&& rm -rf /var/lib/apt/lists/*
COPY backend/package*.json ./
RUN npm ci --only-production
COPY /backend .
COPY --chown=non-root-user:nodejs standalone-entrypoint.sh standalone-entrypoint.sh
RUN npm i -D tsconfig-paths
RUN npm run build
# Production stage
FROM base AS backend-runner
ENV ChrystokiConfigurationPath=/usr/safenet/lunaclient/
WORKDIR /app
# Required for pkcs11js
RUN apt-get update && apt-get install -y \
python3 \
make \
g++ \
&& rm -rf /var/lib/apt/lists/*
COPY backend/package*.json ./
RUN npm ci --only-production
COPY --from=backend-build /app .
RUN mkdir frontend-build
# Production stage
FROM base AS production
# Install necessary packages
RUN apt-get update && apt-get install -y \
ca-certificates \
curl \
git \
&& rm -rf /var/lib/apt/lists/*
# Install Infisical CLI
RUN curl -1sLf 'https://dl.cloudsmith.io/public/infisical/infisical-cli/setup.deb.sh' | bash \
&& apt-get update && apt-get install -y infisical=0.31.1 \
&& rm -rf /var/lib/apt/lists/*
RUN groupadd -r -g 1001 nodejs && useradd -r -u 1001 -g nodejs non-root-user
# Give non-root-user permission to update SSL certs
RUN chown -R non-root-user /etc/ssl/certs
RUN chown non-root-user /etc/ssl/certs/ca-certificates.crt
RUN chmod -R u+rwx /etc/ssl/certs
RUN chmod u+rw /etc/ssl/certs/ca-certificates.crt
RUN chown non-root-user /usr/sbin/update-ca-certificates
RUN chmod u+rx /usr/sbin/update-ca-certificates
## set pre baked keys
ARG POSTHOG_API_KEY
ENV NEXT_PUBLIC_POSTHOG_API_KEY=$POSTHOG_API_KEY \
BAKED_NEXT_PUBLIC_POSTHOG_API_KEY=$POSTHOG_API_KEY
ARG INTERCOM_ID=intercom-id
ENV NEXT_PUBLIC_INTERCOM_ID=$INTERCOM_ID \
BAKED_NEXT_PUBLIC_INTERCOM_ID=$INTERCOM_ID
ARG CAPTCHA_SITE_KEY
ENV NEXT_PUBLIC_CAPTCHA_SITE_KEY=$CAPTCHA_SITE_KEY \
BAKED_NEXT_PUBLIC_CAPTCHA_SITE_KEY=$CAPTCHA_SITE_KEY
WORKDIR /
COPY --from=backend-runner /app /backend
COPY --from=frontend-runner /app ./backend/frontend-build
ENV PORT 8080
ENV HOST=0.0.0.0
ENV HTTPS_ENABLED false
ENV NODE_ENV production
ENV STANDALONE_BUILD true
ENV STANDALONE_MODE true
ENV ChrystokiConfigurationPath=/usr/safenet/lunaclient/
WORKDIR /backend
ENV TELEMETRY_ENABLED true
EXPOSE 8080
EXPOSE 443
USER non-root-user
CMD ["./standalone-entrypoint.sh"]

View File

@ -72,6 +72,9 @@ RUN addgroup --system --gid 1001 nodejs \
WORKDIR /app
# Required for pkcs11js
RUN apk add --no-cache python3 make g++
COPY backend/package*.json ./
RUN npm ci --only-production
@ -85,6 +88,9 @@ FROM base AS backend-runner
WORKDIR /app
# Required for pkcs11js
RUN apk add --no-cache python3 make g++
COPY backend/package*.json ./
RUN npm ci --only-production

View File

@ -3,6 +3,12 @@ FROM node:20-alpine AS build
WORKDIR /app
# Required for pkcs11js
RUN apk --update add \
python3 \
make \
g++
COPY package*.json ./
RUN npm ci --only-production
@ -11,12 +17,17 @@ RUN npm run build
# Production stage
FROM node:20-alpine
WORKDIR /app
ENV npm_config_cache /home/node/.npm
COPY package*.json ./
RUN apk --update add \
python3 \
make \
g++
RUN npm ci --only-production && npm cache clean --force
COPY --from=build /app .

View File

@ -1,5 +1,44 @@
FROM node:20-alpine
# ? Setup a test SoftHSM module. In production a real HSM is used.
ARG SOFTHSM2_VERSION=2.5.0
ENV SOFTHSM2_VERSION=${SOFTHSM2_VERSION} \
SOFTHSM2_SOURCES=/tmp/softhsm2
# install build dependencies including python3
RUN apk --update add \
alpine-sdk \
autoconf \
automake \
git \
libtool \
openssl-dev \
python3 \
make \
g++
# build and install SoftHSM2
RUN git clone https://github.com/opendnssec/SoftHSMv2.git ${SOFTHSM2_SOURCES}
WORKDIR ${SOFTHSM2_SOURCES}
RUN git checkout ${SOFTHSM2_VERSION} -b ${SOFTHSM2_VERSION} \
&& sh autogen.sh \
&& ./configure --prefix=/usr/local --disable-gost \
&& make \
&& make install
WORKDIR /root
RUN rm -fr ${SOFTHSM2_SOURCES}
# install pkcs11-tool
RUN apk --update add opensc
RUN softhsm2-util --init-token --slot 0 --label "auth-app" --pin 1234 --so-pin 0000
# ? App setup
RUN apk add --no-cache bash curl && curl -1sLf \
'https://dl.cloudsmith.io/public/infisical/infisical-cli/setup.alpine.sh' | bash \
&& apk add infisical=0.8.1 && apk add --no-cache git

View File

@ -118,9 +118,9 @@ describe.each([{ secretPath: "/" }, { secretPath: "/deep" }])(
value: "stage-value"
});
// wait for 5 second for replication to finish
// wait for 10 second for replication to finish
await new Promise((resolve) => {
setTimeout(resolve, 5000); // time to breathe for db
setTimeout(resolve, 10000); // time to breathe for db
});
const secret = await getSecretByNameV2({
@ -173,9 +173,9 @@ describe.each([{ secretPath: "/" }, { secretPath: "/deep" }])(
value: "prod-value"
});
// wait for 5 second for replication to finish
// wait for 10 second for replication to finish
await new Promise((resolve) => {
setTimeout(resolve, 5000); // time to breathe for db
setTimeout(resolve, 10000); // time to breathe for db
});
const secret = await getSecretByNameV2({
@ -343,9 +343,9 @@ describe.each([{ path: "/" }, { path: "/deep" }])(
value: "prod-value"
});
// wait for 5 second for replication to finish
// wait for 10 second for replication to finish
await new Promise((resolve) => {
setTimeout(resolve, 5000); // time to breathe for db
setTimeout(resolve, 10000); // time to breathe for db
});
const secret = await getSecretByNameV2({

View File

@ -16,6 +16,7 @@ import { initDbConnection } from "@app/db";
import { queueServiceFactory } from "@app/queue";
import { keyStoreFactory } from "@app/keystore/keystore";
import { Redis } from "ioredis";
import { initializeHsmModule } from "@app/ee/services/hsm/hsm-fns";
dotenv.config({ path: path.join(__dirname, "../../.env.test"), debug: true });
export default {
@ -54,7 +55,12 @@ export default {
const smtp = mockSmtpServer();
const queue = queueServiceFactory(cfg.REDIS_URL);
const keyStore = keyStoreFactory(cfg.REDIS_URL);
const server = await main({ db, smtp, logger, queue, keyStore });
const hsmModule = initializeHsmModule();
hsmModule.initialize();
const server = await main({ db, smtp, logger, queue, keyStore, hsmModule: hsmModule.getModule() });
// @ts-expect-error type
globalThis.testServer = server;
// @ts-expect-error type

2783
backend/package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -84,6 +84,7 @@
"@types/passport-google-oauth20": "^2.0.14",
"@types/pg": "^8.10.9",
"@types/picomatch": "^2.3.3",
"@types/pkcs11js": "^1.0.4",
"@types/prompt-sync": "^4.2.3",
"@types/resolve": "^1.20.6",
"@types/safe-regex": "^1.1.6",
@ -161,6 +162,7 @@
"google-auth-library": "^9.9.0",
"googleapis": "^137.1.0",
"handlebars": "^4.7.8",
"hdb": "^0.19.10",
"ioredis": "^5.3.2",
"jmespath": "^0.16.0",
"jsonwebtoken": "^9.0.2",
@ -187,6 +189,7 @@
"pg-query-stream": "^4.5.3",
"picomatch": "^3.0.1",
"pino": "^8.16.2",
"pkcs11js": "^2.1.6",
"pkijs": "^3.2.4",
"posthog-node": "^3.6.2",
"probot": "^13.3.8",
@ -195,6 +198,7 @@
"scim2-parse-filter": "^0.2.10",
"sjcl": "^1.0.8",
"smee-client": "^2.0.0",
"snowflake-sdk": "^1.14.0",
"tedious": "^18.2.1",
"tweetnacl": "^1.0.3",
"tweetnacl-util": "^0.15.1",

View File

@ -18,6 +18,7 @@ import { TLdapConfigServiceFactory } from "@app/ee/services/ldap-config/ldap-con
import { TLicenseServiceFactory } from "@app/ee/services/license/license-service";
import { TOidcConfigServiceFactory } from "@app/ee/services/oidc/oidc-config-service";
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service";
import { TProjectTemplateServiceFactory } from "@app/ee/services/project-template/project-template-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";
@ -43,6 +44,7 @@ import { TCmekServiceFactory } from "@app/services/cmek/cmek-service";
import { TExternalGroupOrgRoleMappingServiceFactory } from "@app/services/external-group-org-role-mapping/external-group-org-role-mapping-service";
import { TExternalMigrationServiceFactory } from "@app/services/external-migration/external-migration-service";
import { TGroupProjectServiceFactory } from "@app/services/group-project/group-project-service";
import { THsmServiceFactory } from "@app/services/hsm/hsm-service";
import { TIdentityServiceFactory } from "@app/services/identity/identity-service";
import { TIdentityAccessTokenServiceFactory } from "@app/services/identity-access-token/identity-access-token-service";
import { TIdentityAwsAuthServiceFactory } from "@app/services/identity-aws-auth/identity-aws-auth-service";
@ -183,12 +185,14 @@ declare module "fastify" {
rateLimit: TRateLimitServiceFactory;
userEngagement: TUserEngagementServiceFactory;
externalKms: TExternalKmsServiceFactory;
hsm: THsmServiceFactory;
orgAdmin: TOrgAdminServiceFactory;
slack: TSlackServiceFactory;
workflowIntegration: TWorkflowIntegrationServiceFactory;
cmek: TCmekServiceFactory;
migration: TExternalMigrationServiceFactory;
externalGroupOrgRoleMapping: TExternalGroupOrgRoleMappingServiceFactory;
projectTemplate: TProjectTemplateServiceFactory;
};
// this is exclusive use for middlewares in which we need to inject data
// everywhere else access using service layer

4
backend/src/@types/hdb.d.ts vendored Normal file
View File

@ -0,0 +1,4 @@
declare module "hdb" {
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Untyped, the function returns `any`.
function createClient(options): any;
}

View File

@ -200,6 +200,9 @@ import {
TProjectSlackConfigsInsert,
TProjectSlackConfigsUpdate,
TProjectsUpdate,
TProjectTemplates,
TProjectTemplatesInsert,
TProjectTemplatesUpdate,
TProjectUserAdditionalPrivilege,
TProjectUserAdditionalPrivilegeInsert,
TProjectUserAdditionalPrivilegeUpdate,
@ -818,5 +821,10 @@ declare module "knex/types/tables" {
TExternalGroupOrgRoleMappingsInsert,
TExternalGroupOrgRoleMappingsUpdate
>;
[TableName.ProjectTemplates]: KnexOriginal.CompositeTableType<
TProjectTemplates,
TProjectTemplatesInsert,
TProjectTemplatesUpdate
>;
}
}

View File

@ -9,7 +9,7 @@ export async function up(knex: Knex): Promise<void> {
t.uuid("id", { primaryKey: true }).defaultTo(knex.fn.uuid());
t.string("integration").notNullable();
t.string("teamId"); // vercel-specific
t.string("url"); // for self hosted
t.string("url"); // for self-hosted
t.string("namespace"); // hashicorp specific
t.string("accountId"); // netlify
t.text("refreshCiphertext");
@ -36,7 +36,7 @@ export async function up(knex: Knex): Promise<void> {
await knex.schema.createTable(TableName.Integration, (t) => {
t.uuid("id", { primaryKey: true }).defaultTo(knex.fn.uuid());
t.boolean("isActive").notNullable();
t.string("url"); // self hosted
t.string("url"); // self-hosted
t.string("app"); // name of app in provider
t.string("appId");
t.string("targetEnvironment");

View File

@ -2,7 +2,7 @@ import { Knex } from "knex";
import { TableName } from "../schemas";
const BATCH_SIZE = 10_000;
const BATCH_SIZE = 30_000;
export async function up(knex: Knex): Promise<void> {
const hasAuthMethodColumnAccessToken = await knex.schema.hasColumn(TableName.IdentityAccessToken, "authMethod");
@ -71,3 +71,6 @@ export async function down(knex: Knex): Promise<void> {
});
}
}
const config = { transaction: false };
export { config };

View File

@ -0,0 +1,28 @@
import { Knex } from "knex";
import { TableName } from "@app/db/schemas";
import { createOnUpdateTrigger, dropOnUpdateTrigger } from "@app/db/utils";
export async function up(knex: Knex): Promise<void> {
if (!(await knex.schema.hasTable(TableName.ProjectTemplates))) {
await knex.schema.createTable(TableName.ProjectTemplates, (t) => {
t.uuid("id", { primaryKey: true }).defaultTo(knex.fn.uuid());
t.string("name", 32).notNullable();
t.string("description").nullable();
t.jsonb("roles").notNullable();
t.jsonb("environments").notNullable();
t.uuid("orgId").notNullable().references("id").inTable(TableName.Organization).onDelete("CASCADE");
t.timestamps(true, true, true);
});
await createOnUpdateTrigger(knex, TableName.ProjectTemplates);
}
}
export async function down(knex: Knex): Promise<void> {
if (await knex.schema.hasTable(TableName.ProjectTemplates)) {
await dropOnUpdateTrigger(knex, TableName.ProjectTemplates);
await knex.schema.dropTable(TableName.ProjectTemplates);
}
}

View File

@ -0,0 +1,35 @@
import { Knex } from "knex";
import { TableName } from "../schemas";
export async function up(knex: Knex): Promise<void> {
const hasDisableBootstrapCertValidationCol = await knex.schema.hasColumn(
TableName.CertificateTemplateEstConfig,
"disableBootstrapCertValidation"
);
const hasCaChainCol = await knex.schema.hasColumn(TableName.CertificateTemplateEstConfig, "encryptedCaChain");
await knex.schema.alterTable(TableName.CertificateTemplateEstConfig, (t) => {
if (!hasDisableBootstrapCertValidationCol) {
t.boolean("disableBootstrapCertValidation").defaultTo(false).notNullable();
}
if (hasCaChainCol) {
t.binary("encryptedCaChain").nullable().alter();
}
});
}
export async function down(knex: Knex): Promise<void> {
const hasDisableBootstrapCertValidationCol = await knex.schema.hasColumn(
TableName.CertificateTemplateEstConfig,
"disableBootstrapCertValidation"
);
await knex.schema.alterTable(TableName.CertificateTemplateEstConfig, (t) => {
if (hasDisableBootstrapCertValidationCol) {
t.dropColumn("disableBootstrapCertValidation");
}
});
}

View File

@ -0,0 +1,23 @@
import { Knex } from "knex";
import { TableName } from "../schemas";
export async function up(knex: Knex): Promise<void> {
const hasEncryptionStrategy = await knex.schema.hasColumn(TableName.KmsServerRootConfig, "encryptionStrategy");
const hasTimestampsCol = await knex.schema.hasColumn(TableName.KmsServerRootConfig, "createdAt");
await knex.schema.alterTable(TableName.KmsServerRootConfig, (t) => {
if (!hasEncryptionStrategy) t.string("encryptionStrategy").defaultTo("SOFTWARE");
if (!hasTimestampsCol) t.timestamps(true, true, true);
});
}
export async function down(knex: Knex): Promise<void> {
const hasEncryptionStrategy = await knex.schema.hasColumn(TableName.KmsServerRootConfig, "encryptionStrategy");
const hasTimestampsCol = await knex.schema.hasColumn(TableName.KmsServerRootConfig, "createdAt");
await knex.schema.alterTable(TableName.KmsServerRootConfig, (t) => {
if (hasEncryptionStrategy) t.dropColumn("encryptionStrategy");
if (hasTimestampsCol) t.dropTimestamps(true);
});
}

View File

@ -12,11 +12,12 @@ import { TImmutableDBKeys } from "./models";
export const CertificateTemplateEstConfigsSchema = z.object({
id: z.string().uuid(),
certificateTemplateId: z.string().uuid(),
encryptedCaChain: zodBuffer,
encryptedCaChain: zodBuffer.nullable().optional(),
hashedPassphrase: z.string(),
isEnabled: z.boolean(),
createdAt: z.date(),
updatedAt: z.date()
updatedAt: z.date(),
disableBootstrapCertValidation: z.boolean().default(false)
});
export type TCertificateTemplateEstConfigs = z.infer<typeof CertificateTemplateEstConfigsSchema>;

View File

@ -64,6 +64,7 @@ export * from "./project-keys";
export * from "./project-memberships";
export * from "./project-roles";
export * from "./project-slack-configs";
export * from "./project-templates";
export * from "./project-user-additional-privilege";
export * from "./project-user-membership-roles";
export * from "./projects";

View File

@ -11,7 +11,10 @@ import { TImmutableDBKeys } from "./models";
export const KmsRootConfigSchema = z.object({
id: z.string().uuid(),
encryptedRootKey: zodBuffer
encryptedRootKey: zodBuffer,
encryptionStrategy: z.string(),
createdAt: z.date(),
updatedAt: z.date()
});
export type TKmsRootConfig = z.infer<typeof KmsRootConfigSchema>;

View File

@ -41,6 +41,7 @@ export enum TableName {
ProjectUserAdditionalPrivilege = "project_user_additional_privilege",
ProjectUserMembershipRole = "project_user_membership_roles",
ProjectKeys = "project_keys",
ProjectTemplates = "project_templates",
Secret = "secrets",
SecretReference = "secret_references",
SecretSharing = "secret_sharing",

View File

@ -15,7 +15,8 @@ export const ProjectRolesSchema = z.object({
permissions: z.unknown(),
createdAt: z.date(),
updatedAt: z.date(),
projectId: z.string()
projectId: z.string(),
version: z.number().default(1)
});
export type TProjectRoles = z.infer<typeof ProjectRolesSchema>;

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 ProjectTemplatesSchema = z.object({
id: z.string().uuid(),
name: z.string(),
description: z.string().nullable().optional(),
roles: z.unknown(),
environments: z.unknown(),
orgId: z.string().uuid(),
createdAt: z.date(),
updatedAt: z.date()
});
export type TProjectTemplates = z.infer<typeof ProjectTemplatesSchema>;
export type TProjectTemplatesInsert = Omit<z.input<typeof ProjectTemplatesSchema>, TImmutableDBKeys>;
export type TProjectTemplatesUpdate = Partial<Omit<z.input<typeof ProjectTemplatesSchema>, TImmutableDBKeys>>;

View File

@ -1,3 +1,5 @@
import { registerProjectTemplateRouter } from "@app/ee/routes/v1/project-template-router";
import { registerAccessApprovalPolicyRouter } from "./access-approval-policy-router";
import { registerAccessApprovalRequestRouter } from "./access-approval-request-router";
import { registerAuditLogStreamRouter } from "./audit-log-stream-router";
@ -92,4 +94,6 @@ export const registerV1EERoutes = async (server: FastifyZodProvider) => {
await server.register(registerExternalKmsRouter, {
prefix: "/external-kms"
});
await server.register(registerProjectTemplateRouter, { prefix: "/project-templates" });
};

View File

@ -192,7 +192,7 @@ export const registerProjectRoleRouter = async (server: FastifyZodProvider) => {
}),
response: {
200: z.object({
roles: ProjectRolesSchema.omit({ permissions: true }).array()
roles: ProjectRolesSchema.omit({ permissions: true, version: true }).array()
})
}
},
@ -225,7 +225,7 @@ export const registerProjectRoleRouter = async (server: FastifyZodProvider) => {
}),
response: {
200: z.object({
role: SanitizedRoleSchemaV1
role: SanitizedRoleSchemaV1.omit({ version: true })
})
}
},

View File

@ -0,0 +1,309 @@
import slugify from "@sindresorhus/slugify";
import { z } from "zod";
import { ProjectMembershipRole, ProjectTemplatesSchema } from "@app/db/schemas";
import { EventType } from "@app/ee/services/audit-log/audit-log-types";
import { ProjectPermissionV2Schema } from "@app/ee/services/permission/project-permission";
import { ProjectTemplateDefaultEnvironments } from "@app/ee/services/project-template/project-template-constants";
import { isInfisicalProjectTemplate } from "@app/ee/services/project-template/project-template-fns";
import { ProjectTemplates } from "@app/lib/api-docs";
import { readLimit, writeLimit } from "@app/server/config/rateLimiter";
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
import { UnpackedPermissionSchema } from "@app/server/routes/santizedSchemas/permission";
import { AuthMode } from "@app/services/auth/auth-type";
const MAX_JSON_SIZE_LIMIT_IN_BYTES = 32_768;
const SlugSchema = z
.string()
.trim()
.min(1)
.max(32)
.refine((val) => val.toLowerCase() === val, "Must be lowercase")
.refine((v) => slugify(v) === v, {
message: "Must be valid slug format"
});
const isReservedRoleSlug = (slug: string) =>
Object.values(ProjectMembershipRole).includes(slug as ProjectMembershipRole);
const isReservedRoleName = (name: string) =>
["custom", "admin", "viewer", "developer", "no access"].includes(name.toLowerCase());
const SanitizedProjectTemplateSchema = ProjectTemplatesSchema.extend({
roles: z
.object({
name: z.string().trim().min(1),
slug: SlugSchema,
permissions: UnpackedPermissionSchema.array()
})
.array(),
environments: z
.object({
name: z.string().trim().min(1),
slug: SlugSchema,
position: z.number().min(1)
})
.array()
});
const ProjectTemplateRolesSchema = z
.object({
name: z.string().trim().min(1),
slug: SlugSchema,
permissions: ProjectPermissionV2Schema.array()
})
.array()
.superRefine((roles, ctx) => {
if (!roles.length) return;
if (Buffer.byteLength(JSON.stringify(roles)) > MAX_JSON_SIZE_LIMIT_IN_BYTES)
ctx.addIssue({ code: z.ZodIssueCode.custom, message: "Size limit exceeded" });
if (new Set(roles.map((v) => v.slug)).size !== roles.length)
ctx.addIssue({ code: z.ZodIssueCode.custom, message: "Role slugs must be unique" });
if (new Set(roles.map((v) => v.name)).size !== roles.length)
ctx.addIssue({ code: z.ZodIssueCode.custom, message: "Role names must be unique" });
roles.forEach((role) => {
if (isReservedRoleSlug(role.slug))
ctx.addIssue({ code: z.ZodIssueCode.custom, message: `Role slug "${role.slug}" is reserved` });
if (isReservedRoleName(role.name))
ctx.addIssue({ code: z.ZodIssueCode.custom, message: `Role name "${role.name}" is reserved` });
});
});
const ProjectTemplateEnvironmentsSchema = z
.object({
name: z.string().trim().min(1),
slug: SlugSchema,
position: z.number().min(1)
})
.array()
.min(1)
.superRefine((environments, ctx) => {
if (Buffer.byteLength(JSON.stringify(environments)) > MAX_JSON_SIZE_LIMIT_IN_BYTES)
ctx.addIssue({ code: z.ZodIssueCode.custom, message: "Size limit exceeded" });
if (new Set(environments.map((v) => v.name)).size !== environments.length)
ctx.addIssue({ code: z.ZodIssueCode.custom, message: "Environment names must be unique" });
if (new Set(environments.map((v) => v.slug)).size !== environments.length)
ctx.addIssue({ code: z.ZodIssueCode.custom, message: "Environment slugs must be unique" });
if (
environments.some((env) => env.position < 1 || env.position > environments.length) ||
new Set(environments.map((env) => env.position)).size !== environments.length
)
ctx.addIssue({
code: z.ZodIssueCode.custom,
message: "One or more of the positions specified is invalid. Positions must be sequential starting from 1."
});
});
export const registerProjectTemplateRouter = async (server: FastifyZodProvider) => {
server.route({
method: "GET",
url: "/",
config: {
rateLimit: readLimit
},
schema: {
description: "List project templates for the current organization.",
response: {
200: z.object({
projectTemplates: SanitizedProjectTemplateSchema.array()
})
}
},
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
handler: async (req) => {
const projectTemplates = await server.services.projectTemplate.listProjectTemplatesByOrg(req.permission);
const auditTemplates = projectTemplates.filter((template) => !isInfisicalProjectTemplate(template.name));
await server.services.auditLog.createAuditLog({
...req.auditLogInfo,
orgId: req.permission.orgId,
event: {
type: EventType.GET_PROJECT_TEMPLATES,
metadata: {
count: auditTemplates.length,
templateIds: auditTemplates.map((template) => template.id)
}
}
});
return { projectTemplates };
}
});
server.route({
method: "GET",
url: "/:templateId",
config: {
rateLimit: readLimit
},
schema: {
description: "Get a project template by ID.",
params: z.object({
templateId: z.string().uuid()
}),
response: {
200: z.object({
projectTemplate: SanitizedProjectTemplateSchema
})
}
},
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
handler: async (req) => {
const projectTemplate = await server.services.projectTemplate.findProjectTemplateById(
req.params.templateId,
req.permission
);
await server.services.auditLog.createAuditLog({
...req.auditLogInfo,
orgId: req.permission.orgId,
event: {
type: EventType.GET_PROJECT_TEMPLATE,
metadata: {
templateId: req.params.templateId
}
}
});
return { projectTemplate };
}
});
server.route({
method: "POST",
url: "/",
config: {
rateLimit: writeLimit
},
schema: {
description: "Create a project template.",
body: z.object({
name: SlugSchema.refine((val) => !isInfisicalProjectTemplate(val), {
message: `The requested project template name is reserved.`
}).describe(ProjectTemplates.CREATE.name),
description: z.string().max(256).trim().optional().describe(ProjectTemplates.CREATE.description),
roles: ProjectTemplateRolesSchema.default([]).describe(ProjectTemplates.CREATE.roles),
environments: ProjectTemplateEnvironmentsSchema.default(ProjectTemplateDefaultEnvironments).describe(
ProjectTemplates.CREATE.environments
)
}),
response: {
200: z.object({
projectTemplate: SanitizedProjectTemplateSchema
})
}
},
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
handler: async (req) => {
const projectTemplate = await server.services.projectTemplate.createProjectTemplate(req.body, req.permission);
await server.services.auditLog.createAuditLog({
...req.auditLogInfo,
orgId: req.permission.orgId,
event: {
type: EventType.CREATE_PROJECT_TEMPLATE,
metadata: req.body
}
});
return { projectTemplate };
}
});
server.route({
method: "PATCH",
url: "/:templateId",
config: {
rateLimit: writeLimit
},
schema: {
description: "Update a project template.",
params: z.object({ templateId: z.string().uuid().describe(ProjectTemplates.UPDATE.templateId) }),
body: z.object({
name: SlugSchema.refine((val) => !isInfisicalProjectTemplate(val), {
message: `The requested project template name is reserved.`
})
.optional()
.describe(ProjectTemplates.UPDATE.name),
description: z.string().max(256).trim().optional().describe(ProjectTemplates.UPDATE.description),
roles: ProjectTemplateRolesSchema.optional().describe(ProjectTemplates.UPDATE.roles),
environments: ProjectTemplateEnvironmentsSchema.optional().describe(ProjectTemplates.UPDATE.environments)
}),
response: {
200: z.object({
projectTemplate: SanitizedProjectTemplateSchema
})
}
},
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
handler: async (req) => {
const projectTemplate = await server.services.projectTemplate.updateProjectTemplateById(
req.params.templateId,
req.body,
req.permission
);
await server.services.auditLog.createAuditLog({
...req.auditLogInfo,
orgId: req.permission.orgId,
event: {
type: EventType.UPDATE_PROJECT_TEMPLATE,
metadata: {
templateId: req.params.templateId,
...req.body
}
}
});
return { projectTemplate };
}
});
server.route({
method: "DELETE",
url: "/:templateId",
config: {
rateLimit: writeLimit
},
schema: {
description: "Delete a project template.",
params: z.object({ templateId: z.string().uuid().describe(ProjectTemplates.DELETE.templateId) }),
response: {
200: z.object({
projectTemplate: SanitizedProjectTemplateSchema
})
}
},
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
handler: async (req) => {
const projectTemplate = await server.services.projectTemplate.deleteProjectTemplateById(
req.params.templateId,
req.permission
);
await server.services.auditLog.createAuditLog({
...req.auditLogInfo,
orgId: req.permission.orgId,
event: {
type: EventType.DELETE_PROJECT_TEMPLATE,
metadata: {
templateId: req.params.templateId
}
}
});
return { projectTemplate };
}
});
};

View File

@ -186,7 +186,7 @@ export const registerProjectRoleRouter = async (server: FastifyZodProvider) => {
}),
response: {
200: z.object({
roles: ProjectRolesSchema.omit({ permissions: true }).array()
roles: ProjectRolesSchema.omit({ permissions: true, version: true }).array()
})
}
},
@ -219,7 +219,7 @@ export const registerProjectRoleRouter = async (server: FastifyZodProvider) => {
}),
response: {
200: z.object({
role: SanitizedRoleSchema
role: SanitizedRoleSchema.omit({ version: true })
})
}
},

View File

@ -1,3 +1,7 @@
import {
TCreateProjectTemplateDTO,
TUpdateProjectTemplateDTO
} from "@app/ee/services/project-template/project-template-types";
import { SymmetricEncryption } from "@app/lib/crypto/cipher";
import { TProjectPermission } from "@app/lib/types";
import { ActorType } from "@app/services/auth/auth-type";
@ -192,7 +196,13 @@ export enum EventType {
CMEK_ENCRYPT = "cmek-encrypt",
CMEK_DECRYPT = "cmek-decrypt",
UPDATE_EXTERNAL_GROUP_ORG_ROLE_MAPPINGS = "update-external-group-org-role-mapping",
GET_EXTERNAL_GROUP_ORG_ROLE_MAPPINGS = "get-external-group-org-role-mapping"
GET_EXTERNAL_GROUP_ORG_ROLE_MAPPINGS = "get-external-group-org-role-mapping",
GET_PROJECT_TEMPLATES = "get-project-templates",
GET_PROJECT_TEMPLATE = "get-project-template",
CREATE_PROJECT_TEMPLATE = "create-project-template",
UPDATE_PROJECT_TEMPLATE = "update-project-template",
DELETE_PROJECT_TEMPLATE = "delete-project-template",
APPLY_PROJECT_TEMPLATE = "apply-project-template"
}
interface UserActorMetadata {
@ -1618,6 +1628,46 @@ interface UpdateExternalGroupOrgRoleMappingsEvent {
};
}
interface GetProjectTemplatesEvent {
type: EventType.GET_PROJECT_TEMPLATES;
metadata: {
count: number;
templateIds: string[];
};
}
interface GetProjectTemplateEvent {
type: EventType.GET_PROJECT_TEMPLATE;
metadata: {
templateId: string;
};
}
interface CreateProjectTemplateEvent {
type: EventType.CREATE_PROJECT_TEMPLATE;
metadata: TCreateProjectTemplateDTO;
}
interface UpdateProjectTemplateEvent {
type: EventType.UPDATE_PROJECT_TEMPLATE;
metadata: TUpdateProjectTemplateDTO & { templateId: string };
}
interface DeleteProjectTemplateEvent {
type: EventType.DELETE_PROJECT_TEMPLATE;
metadata: {
templateId: string;
};
}
interface ApplyProjectTemplateEvent {
type: EventType.APPLY_PROJECT_TEMPLATE;
metadata: {
template: string;
projectId: string;
};
}
export type Event =
| GetSecretsEvent
| GetSecretEvent
@ -1766,4 +1816,10 @@ export type Event =
| CmekEncryptEvent
| CmekDecryptEvent
| GetExternalGroupOrgRoleMappingsEvent
| UpdateExternalGroupOrgRoleMappingsEvent;
| UpdateExternalGroupOrgRoleMappingsEvent
| GetProjectTemplatesEvent
| GetProjectTemplateEvent
| CreateProjectTemplateEvent
| UpdateProjectTemplateEvent
| DeleteProjectTemplateEvent
| ApplyProjectTemplateEvent;

View File

@ -171,27 +171,29 @@ export const certificateEstServiceFactory = ({
});
}
const caCerts = estConfig.caChain
.match(/-----BEGIN CERTIFICATE-----[\s\S]+?-----END CERTIFICATE-----/g)
?.map((cert) => {
return new x509.X509Certificate(cert);
});
if (!estConfig.disableBootstrapCertValidation) {
const caCerts = estConfig.caChain
.match(/-----BEGIN CERTIFICATE-----[\s\S]+?-----END CERTIFICATE-----/g)
?.map((cert) => {
return new x509.X509Certificate(cert);
});
if (!caCerts) {
throw new BadRequestError({ message: "Failed to parse certificate chain" });
}
if (!caCerts) {
throw new BadRequestError({ message: "Failed to parse certificate chain" });
}
const leafCertificate = decodeURIComponent(sslClientCert).match(
/-----BEGIN CERTIFICATE-----[\s\S]+?-----END CERTIFICATE-----/g
)?.[0];
const leafCertificate = decodeURIComponent(sslClientCert).match(
/-----BEGIN CERTIFICATE-----[\s\S]+?-----END CERTIFICATE-----/g
)?.[0];
if (!leafCertificate) {
throw new BadRequestError({ message: "Missing client certificate" });
}
if (!leafCertificate) {
throw new BadRequestError({ message: "Missing client certificate" });
}
const certObj = new x509.X509Certificate(leafCertificate);
if (!(await isCertChainValid([certObj, ...caCerts]))) {
throw new BadRequestError({ message: "Invalid certificate chain" });
const certObj = new x509.X509Certificate(leafCertificate);
if (!(await isCertChainValid([certObj, ...caCerts]))) {
throw new BadRequestError({ message: "Invalid certificate chain" });
}
}
const { certificate } = await certificateAuthorityService.signCertFromCa({

View File

@ -0,0 +1,20 @@
import { getConfig } from "@app/lib/config/env";
import { BadRequestError } from "@app/lib/errors";
import { getDbConnectionHost } from "@app/lib/knex";
export const verifyHostInputValidity = (host: string) => {
const appCfg = getConfig();
const dbHost = appCfg.DB_HOST || getDbConnectionHost(appCfg.DB_CONNECTION_URI);
if (
appCfg.isCloud &&
// localhost
// internal ips
(host === "host.docker.internal" || host.match(/^10\.\d+\.\d+\.\d+/) || host.match(/^192\.168\.\d+\.\d+/))
)
throw new BadRequestError({ message: "Invalid db host" });
if (host === "localhost" || host === "127.0.0.1" || dbHost === host) {
throw new BadRequestError({ message: "Invalid db host" });
}
};

View File

@ -9,7 +9,7 @@ import {
} from "@app/ee/services/permission/project-permission";
import { infisicalSymmetricDecrypt, infisicalSymmetricEncypt } from "@app/lib/crypto/encryption";
import { BadRequestError, NotFoundError } from "@app/lib/errors";
import { OrderByDirection } from "@app/lib/types";
import { OrderByDirection, OrgServiceActor } from "@app/lib/types";
import { TProjectDALFactory } from "@app/services/project/project-dal";
import { TSecretFolderDALFactory } from "@app/services/secret-folder/secret-folder-dal";
@ -22,6 +22,7 @@ import {
TDeleteDynamicSecretDTO,
TDetailsDynamicSecretDTO,
TGetDynamicSecretsCountDTO,
TListDynamicSecretsByFolderMappingsDTO,
TListDynamicSecretsDTO,
TListDynamicSecretsMultiEnvDTO,
TUpdateDynamicSecretDTO
@ -454,8 +455,44 @@ export const dynamicSecretServiceFactory = ({
return dynamicSecretCfg;
};
const listDynamicSecretsByFolderIds = async (
{ folderMappings, filters, projectId }: TListDynamicSecretsByFolderMappingsDTO,
actor: OrgServiceActor
) => {
const { permission } = await permissionService.getProjectPermission(
actor.type,
actor.id,
projectId,
actor.authMethod,
actor.orgId
);
const userAccessibleFolderMappings = folderMappings.filter(({ path, environment }) =>
permission.can(
ProjectPermissionDynamicSecretActions.ReadRootCredential,
subject(ProjectPermissionSub.DynamicSecrets, { environment, secretPath: path })
)
);
const groupedFolderMappings = new Map(userAccessibleFolderMappings.map((path) => [path.folderId, path]));
const dynamicSecrets = await dynamicSecretDAL.listDynamicSecretsByFolderIds({
folderIds: userAccessibleFolderMappings.map(({ folderId }) => folderId),
...filters
});
return dynamicSecrets.map((dynamicSecret) => {
const { environment, path } = groupedFolderMappings.get(dynamicSecret.folderId)!;
return {
...dynamicSecret,
environment,
path
};
});
};
// get dynamic secrets for multiple envs
const listDynamicSecretsByFolderIds = async ({
const listDynamicSecretsByEnvs = async ({
actorAuthMethod,
actorOrgId,
actorId,
@ -521,9 +558,10 @@ export const dynamicSecretServiceFactory = ({
deleteByName,
getDetails,
listDynamicSecretsByEnv,
listDynamicSecretsByFolderIds,
listDynamicSecretsByEnvs,
getDynamicSecretCount,
getCountMultiEnv,
fetchAzureEntraIdUsers
fetchAzureEntraIdUsers,
listDynamicSecretsByFolderIds
};
};

View File

@ -48,17 +48,27 @@ export type TDetailsDynamicSecretDTO = {
projectSlug: string;
} & Omit<TProjectPermission, "projectId">;
export type TListDynamicSecretsDTO = {
path: string;
environmentSlug: string;
projectSlug?: string;
projectId?: string;
export type ListDynamicSecretsFilters = {
offset?: number;
limit?: number;
orderBy?: SecretsOrderBy;
orderDirection?: OrderByDirection;
search?: string;
} & Omit<TProjectPermission, "projectId">;
};
export type TListDynamicSecretsDTO = {
path: string;
environmentSlug: string;
projectSlug?: string;
projectId?: string;
} & ListDynamicSecretsFilters &
Omit<TProjectPermission, "projectId">;
export type TListDynamicSecretsByFolderMappingsDTO = {
projectId: string;
folderMappings: { folderId: string; path: string; environment: string }[];
filters: ListDynamicSecretsFilters;
};
export type TListDynamicSecretsMultiEnvDTO = Omit<
TListDynamicSecretsDTO,

View File

@ -2,10 +2,9 @@ import { Client as ElasticSearchClient } from "@elastic/elasticsearch";
import { customAlphabet } from "nanoid";
import { z } from "zod";
import { getConfig } from "@app/lib/config/env";
import { BadRequestError } from "@app/lib/errors";
import { alphaNumericNanoId } from "@app/lib/nanoid";
import { verifyHostInputValidity } from "../dynamic-secret-fns";
import { DynamicSecretElasticSearchSchema, ElasticSearchAuthTypes, TDynamicProviderFns } from "./models";
const generatePassword = () => {
@ -19,23 +18,8 @@ const generateUsername = () => {
export const ElasticSearchProvider = (): TDynamicProviderFns => {
const validateProviderInputs = async (inputs: unknown) => {
const appCfg = getConfig();
const isCloud = Boolean(appCfg.LICENSE_SERVER_KEY); // quick and dirty way to check if its cloud or not
const providerInputs = await DynamicSecretElasticSearchSchema.parseAsync(inputs);
if (
isCloud &&
// localhost
// internal ips
(providerInputs.host === "host.docker.internal" ||
providerInputs.host.match(/^10\.\d+\.\d+\.\d+/) ||
providerInputs.host.match(/^192\.168\.\d+\.\d+/))
) {
throw new BadRequestError({ message: "Invalid db host" });
}
if (providerInputs.host === "localhost" || providerInputs.host === "127.0.0.1") {
throw new BadRequestError({ message: "Invalid db host" });
}
verifyHostInputValidity(providerInputs.host);
return providerInputs;
};

View File

@ -1,3 +1,5 @@
import { SnowflakeProvider } from "@app/ee/services/dynamic-secret/providers/snowflake";
import { AwsElastiCacheDatabaseProvider } from "./aws-elasticache";
import { AwsIamProvider } from "./aws-iam";
import { AzureEntraIDProvider } from "./azure-entra-id";
@ -9,6 +11,7 @@ import { MongoAtlasProvider } from "./mongo-atlas";
import { MongoDBProvider } from "./mongo-db";
import { RabbitMqProvider } from "./rabbit-mq";
import { RedisDatabaseProvider } from "./redis";
import { SapHanaProvider } from "./sap-hana";
import { SqlDatabaseProvider } from "./sql-database";
export const buildDynamicSecretProviders = () => ({
@ -22,5 +25,7 @@ export const buildDynamicSecretProviders = () => ({
[DynamicSecretProviders.ElasticSearch]: ElasticSearchProvider(),
[DynamicSecretProviders.RabbitMq]: RabbitMqProvider(),
[DynamicSecretProviders.AzureEntraID]: AzureEntraIDProvider(),
[DynamicSecretProviders.Ldap]: LdapProvider()
[DynamicSecretProviders.Ldap]: LdapProvider(),
[DynamicSecretProviders.SapHana]: SapHanaProvider(),
[DynamicSecretProviders.Snowflake]: SnowflakeProvider()
});

View File

@ -7,7 +7,7 @@ import { z } from "zod";
import { BadRequestError } from "@app/lib/errors";
import { alphaNumericNanoId } from "@app/lib/nanoid";
import { LdapSchema, TDynamicProviderFns } from "./models";
import { LdapCredentialType, LdapSchema, TDynamicProviderFns } from "./models";
const generatePassword = () => {
const charset = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-_.~!*$#";
@ -193,29 +193,76 @@ export const LdapProvider = (): TDynamicProviderFns => {
const providerInputs = await validateProviderInputs(inputs);
const client = await getClient(providerInputs);
const username = generateUsername();
const password = generatePassword();
const generatedLdif = generateLDIF({ username, password, ldifTemplate: providerInputs.creationLdif });
if (providerInputs.credentialType === LdapCredentialType.Static) {
const dnMatch = providerInputs.rotationLdif.match(/^dn:\s*(.+)/m);
try {
const dnArray = await executeLdif(client, generatedLdif);
if (dnMatch) {
const username = dnMatch[1];
const password = generatePassword();
return { entityId: username, data: { DN_ARRAY: dnArray, USERNAME: username, PASSWORD: password } };
} catch (err) {
if (providerInputs.rollbackLdif) {
const rollbackLdif = generateLDIF({ username, password, ldifTemplate: providerInputs.rollbackLdif });
await executeLdif(client, rollbackLdif);
const generatedLdif = generateLDIF({ username, password, ldifTemplate: providerInputs.rotationLdif });
try {
const dnArray = await executeLdif(client, generatedLdif);
return { entityId: username, data: { DN_ARRAY: dnArray, USERNAME: username, PASSWORD: password } };
} catch (err) {
throw new BadRequestError({ message: (err as Error).message });
}
} else {
throw new BadRequestError({
message: "Invalid rotation LDIF, missing DN."
});
}
} else {
const username = generateUsername();
const password = generatePassword();
const generatedLdif = generateLDIF({ username, password, ldifTemplate: providerInputs.creationLdif });
try {
const dnArray = await executeLdif(client, generatedLdif);
return { entityId: username, data: { DN_ARRAY: dnArray, USERNAME: username, PASSWORD: password } };
} catch (err) {
if (providerInputs.rollbackLdif) {
const rollbackLdif = generateLDIF({ username, password, ldifTemplate: providerInputs.rollbackLdif });
await executeLdif(client, rollbackLdif);
}
throw new BadRequestError({ message: (err as Error).message });
}
throw new BadRequestError({ message: (err as Error).message });
}
};
const revoke = async (inputs: unknown, entityId: string) => {
const providerInputs = await validateProviderInputs(inputs);
const connection = await getClient(providerInputs);
const client = await getClient(providerInputs);
if (providerInputs.credentialType === LdapCredentialType.Static) {
const dnMatch = providerInputs.rotationLdif.match(/^dn:\s*(.+)/m);
if (dnMatch) {
const username = dnMatch[1];
const password = generatePassword();
const generatedLdif = generateLDIF({ username, password, ldifTemplate: providerInputs.rotationLdif });
try {
const dnArray = await executeLdif(client, generatedLdif);
return { entityId: username, data: { DN_ARRAY: dnArray, USERNAME: username, PASSWORD: password } };
} catch (err) {
throw new BadRequestError({ message: (err as Error).message });
}
} else {
throw new BadRequestError({
message: "Invalid rotation LDIF, missing DN."
});
}
}
const revocationLdif = generateLDIF({ username: entityId, ldifTemplate: providerInputs.revocationLdif });
await executeLdif(connection, revocationLdif);
await executeLdif(client, revocationLdif);
return { entityId };
};

View File

@ -12,6 +12,11 @@ export enum ElasticSearchAuthTypes {
ApiKey = "api-key"
}
export enum LdapCredentialType {
Dynamic = "dynamic",
Static = "static"
}
export const DynamicSecretRedisDBSchema = z.object({
host: z.string().trim().toLowerCase(),
port: z.number(),
@ -166,6 +171,27 @@ export const DynamicSecretMongoDBSchema = z.object({
)
});
export const DynamicSecretSapHanaSchema = z.object({
host: z.string().trim().toLowerCase(),
port: z.number(),
username: z.string().trim(),
password: z.string().trim(),
creationStatement: z.string().trim(),
revocationStatement: z.string().trim(),
renewStatement: z.string().trim().optional(),
ca: z.string().optional()
});
export const DynamicSecretSnowflakeSchema = z.object({
accountId: z.string().trim().min(1),
orgId: z.string().trim().min(1),
username: z.string().trim().min(1),
password: z.string().trim().min(1),
creationStatement: z.string().trim().min(1),
revocationStatement: z.string().trim().min(1),
renewStatement: z.string().trim().optional()
});
export const AzureEntraIDSchema = z.object({
tenantId: z.string().trim().min(1),
userId: z.string().trim().min(1),
@ -174,16 +200,26 @@ export const AzureEntraIDSchema = z.object({
clientSecret: z.string().trim().min(1)
});
export const LdapSchema = z.object({
url: z.string().trim().min(1),
binddn: z.string().trim().min(1),
bindpass: z.string().trim().min(1),
ca: z.string().optional(),
creationLdif: z.string().min(1),
revocationLdif: z.string().min(1),
rollbackLdif: z.string().optional()
});
export const LdapSchema = z.union([
z.object({
url: z.string().trim().min(1),
binddn: z.string().trim().min(1),
bindpass: z.string().trim().min(1),
ca: z.string().optional(),
credentialType: z.literal(LdapCredentialType.Dynamic).optional().default(LdapCredentialType.Dynamic),
creationLdif: z.string().min(1),
revocationLdif: z.string().min(1),
rollbackLdif: z.string().optional()
}),
z.object({
url: z.string().trim().min(1),
binddn: z.string().trim().min(1),
bindpass: z.string().trim().min(1),
ca: z.string().optional(),
credentialType: z.literal(LdapCredentialType.Static),
rotationLdif: z.string().min(1)
})
]);
export enum DynamicSecretProviders {
SqlDatabase = "sql-database",
@ -196,7 +232,9 @@ export enum DynamicSecretProviders {
MongoDB = "mongo-db",
RabbitMq = "rabbit-mq",
AzureEntraID = "azure-entra-id",
Ldap = "ldap"
Ldap = "ldap",
SapHana = "sap-hana",
Snowflake = "snowflake"
}
export const DynamicSecretProviderSchema = z.discriminatedUnion("type", [
@ -204,13 +242,15 @@ export const DynamicSecretProviderSchema = z.discriminatedUnion("type", [
z.object({ type: z.literal(DynamicSecretProviders.Cassandra), inputs: DynamicSecretCassandraSchema }),
z.object({ type: z.literal(DynamicSecretProviders.AwsIam), inputs: DynamicSecretAwsIamSchema }),
z.object({ type: z.literal(DynamicSecretProviders.Redis), inputs: DynamicSecretRedisDBSchema }),
z.object({ type: z.literal(DynamicSecretProviders.SapHana), inputs: DynamicSecretSapHanaSchema }),
z.object({ type: z.literal(DynamicSecretProviders.AwsElastiCache), inputs: DynamicSecretAwsElastiCacheSchema }),
z.object({ type: z.literal(DynamicSecretProviders.MongoAtlas), inputs: DynamicSecretMongoAtlasSchema }),
z.object({ type: z.literal(DynamicSecretProviders.ElasticSearch), inputs: DynamicSecretElasticSearchSchema }),
z.object({ type: z.literal(DynamicSecretProviders.MongoDB), inputs: DynamicSecretMongoDBSchema }),
z.object({ type: z.literal(DynamicSecretProviders.RabbitMq), inputs: DynamicSecretRabbitMqSchema }),
z.object({ type: z.literal(DynamicSecretProviders.AzureEntraID), inputs: AzureEntraIDSchema }),
z.object({ type: z.literal(DynamicSecretProviders.Ldap), inputs: LdapSchema })
z.object({ type: z.literal(DynamicSecretProviders.Ldap), inputs: LdapSchema }),
z.object({ type: z.literal(DynamicSecretProviders.Snowflake), inputs: DynamicSecretSnowflakeSchema })
]);
export type TDynamicProviderFns = {

View File

@ -2,10 +2,9 @@ import { MongoClient } from "mongodb";
import { customAlphabet } from "nanoid";
import { z } from "zod";
import { getConfig } from "@app/lib/config/env";
import { BadRequestError } from "@app/lib/errors";
import { alphaNumericNanoId } from "@app/lib/nanoid";
import { verifyHostInputValidity } from "../dynamic-secret-fns";
import { DynamicSecretMongoDBSchema, TDynamicProviderFns } from "./models";
const generatePassword = (size = 48) => {
@ -19,22 +18,8 @@ const generateUsername = () => {
export const MongoDBProvider = (): TDynamicProviderFns => {
const validateProviderInputs = async (inputs: unknown) => {
const appCfg = getConfig();
const providerInputs = await DynamicSecretMongoDBSchema.parseAsync(inputs);
if (
appCfg.isCloud &&
// localhost
// internal ips
(providerInputs.host === "host.docker.internal" ||
providerInputs.host.match(/^10\.\d+\.\d+\.\d+/) ||
providerInputs.host.match(/^192\.168\.\d+\.\d+/))
)
throw new BadRequestError({ message: "Invalid db host" });
if (providerInputs.host === "localhost" || providerInputs.host === "127.0.0.1") {
throw new BadRequestError({ message: "Invalid db host" });
}
verifyHostInputValidity(providerInputs.host);
return providerInputs;
};

View File

@ -3,12 +3,11 @@ import https from "https";
import { customAlphabet } from "nanoid";
import { z } from "zod";
import { getConfig } from "@app/lib/config/env";
import { BadRequestError } from "@app/lib/errors";
import { removeTrailingSlash } from "@app/lib/fn";
import { logger } from "@app/lib/logger";
import { alphaNumericNanoId } from "@app/lib/nanoid";
import { verifyHostInputValidity } from "../dynamic-secret-fns";
import { DynamicSecretRabbitMqSchema, TDynamicProviderFns } from "./models";
const generatePassword = () => {
@ -79,23 +78,8 @@ async function deleteRabbitMqUser({ axiosInstance, usernameToDelete }: TDeleteRa
export const RabbitMqProvider = (): TDynamicProviderFns => {
const validateProviderInputs = async (inputs: unknown) => {
const appCfg = getConfig();
const isCloud = Boolean(appCfg.LICENSE_SERVER_KEY); // quick and dirty way to check if its cloud or not
const providerInputs = await DynamicSecretRabbitMqSchema.parseAsync(inputs);
if (
isCloud &&
// localhost
// internal ips
(providerInputs.host === "host.docker.internal" ||
providerInputs.host.match(/^10\.\d+\.\d+\.\d+/) ||
providerInputs.host.match(/^192\.168\.\d+\.\d+/))
) {
throw new BadRequestError({ message: "Invalid db host" });
}
if (providerInputs.host === "localhost" || providerInputs.host === "127.0.0.1") {
throw new BadRequestError({ message: "Invalid db host" });
}
verifyHostInputValidity(providerInputs.host);
return providerInputs;
};

View File

@ -3,11 +3,10 @@ import { Redis } from "ioredis";
import { customAlphabet } from "nanoid";
import { z } from "zod";
import { getConfig } from "@app/lib/config/env";
import { BadRequestError } from "@app/lib/errors";
import { getDbConnectionHost } from "@app/lib/knex";
import { alphaNumericNanoId } from "@app/lib/nanoid";
import { verifyHostInputValidity } from "../dynamic-secret-fns";
import { DynamicSecretRedisDBSchema, TDynamicProviderFns } from "./models";
const generatePassword = () => {
@ -51,22 +50,8 @@ const executeTransactions = async (connection: Redis, commands: string[]): Promi
export const RedisDatabaseProvider = (): TDynamicProviderFns => {
const validateProviderInputs = async (inputs: unknown) => {
const appCfg = getConfig();
const isCloud = Boolean(appCfg.LICENSE_SERVER_KEY); // quick and dirty way to check if its cloud or not
const dbHost = appCfg.DB_HOST || getDbConnectionHost(appCfg.DB_CONNECTION_URI);
const providerInputs = await DynamicSecretRedisDBSchema.parseAsync(inputs);
if (
isCloud &&
// localhost
// internal ips
(providerInputs.host === "host.docker.internal" ||
providerInputs.host.match(/^10\.\d+\.\d+\.\d+/) ||
providerInputs.host.match(/^192\.168\.\d+\.\d+/))
)
throw new BadRequestError({ message: "Invalid db host" });
if (providerInputs.host === "localhost" || providerInputs.host === "127.0.0.1" || dbHost === providerInputs.host)
throw new BadRequestError({ message: "Invalid db host" });
verifyHostInputValidity(providerInputs.host);
return providerInputs;
};

View File

@ -0,0 +1,174 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
/* eslint-disable @typescript-eslint/no-unsafe-member-access */
/* eslint-disable @typescript-eslint/no-unsafe-call */
/* eslint-disable @typescript-eslint/no-unsafe-return */
/* eslint-disable @typescript-eslint/no-unsafe-assignment */
import handlebars from "handlebars";
import hdb from "hdb";
import { customAlphabet } from "nanoid";
import { z } from "zod";
import { BadRequestError } from "@app/lib/errors";
import { alphaNumericNanoId } from "@app/lib/nanoid";
import { verifyHostInputValidity } from "../dynamic-secret-fns";
import { DynamicSecretSapHanaSchema, TDynamicProviderFns } from "./models";
const generatePassword = (size = 48) => {
const charset = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789";
return customAlphabet(charset, 48)(size);
};
const generateUsername = () => {
return alphaNumericNanoId(32);
};
export const SapHanaProvider = (): TDynamicProviderFns => {
const validateProviderInputs = async (inputs: unknown) => {
const providerInputs = await DynamicSecretSapHanaSchema.parseAsync(inputs);
verifyHostInputValidity(providerInputs.host);
return providerInputs;
};
const getClient = async (providerInputs: z.infer<typeof DynamicSecretSapHanaSchema>) => {
const client = hdb.createClient({
host: providerInputs.host,
port: providerInputs.port,
user: providerInputs.username,
password: providerInputs.password,
...(providerInputs.ca
? {
ca: providerInputs.ca
}
: {})
});
await new Promise((resolve, reject) => {
client.connect((err: any) => {
if (err) {
return reject(err);
}
if (client.readyState) {
return resolve(true);
}
reject(new Error("SAP HANA client not ready"));
});
});
return client;
};
const validateConnection = async (inputs: unknown) => {
const providerInputs = await validateProviderInputs(inputs);
const client = await getClient(providerInputs);
const testResult: boolean = await new Promise((resolve, reject) => {
client.exec("SELECT 1 FROM DUMMY;", (err: any) => {
if (err) {
reject();
}
resolve(true);
});
});
return testResult;
};
const create = async (inputs: unknown, expireAt: number) => {
const providerInputs = await validateProviderInputs(inputs);
const username = generateUsername();
const password = generatePassword();
const expiration = new Date(expireAt).toISOString();
const client = await getClient(providerInputs);
const creationStatement = handlebars.compile(providerInputs.creationStatement, { noEscape: true })({
username,
password,
expiration
});
const queries = creationStatement.toString().split(";").filter(Boolean);
for await (const query of queries) {
await new Promise((resolve, reject) => {
client.exec(query, (err: any) => {
if (err) {
reject(
new BadRequestError({
message: err.message
})
);
}
resolve(true);
});
});
}
return { entityId: username, data: { DB_USERNAME: username, DB_PASSWORD: password } };
};
const revoke = async (inputs: unknown, username: string) => {
const providerInputs = await validateProviderInputs(inputs);
const client = await getClient(providerInputs);
const revokeStatement = handlebars.compile(providerInputs.revocationStatement)({ username });
const queries = revokeStatement.toString().split(";").filter(Boolean);
for await (const query of queries) {
await new Promise((resolve, reject) => {
client.exec(query, (err: any) => {
if (err) {
reject(
new BadRequestError({
message: err.message
})
);
}
resolve(true);
});
});
}
return { entityId: username };
};
const renew = async (inputs: unknown, username: string, expireAt: number) => {
const providerInputs = await validateProviderInputs(inputs);
const client = await getClient(providerInputs);
try {
const expiration = new Date(expireAt).toISOString();
const renewStatement = handlebars.compile(providerInputs.renewStatement)({ username, expiration });
const queries = renewStatement.toString().split(";").filter(Boolean);
for await (const query of queries) {
await new Promise((resolve, reject) => {
client.exec(query, (err: any) => {
if (err) {
reject(
new BadRequestError({
message: err.message
})
);
}
resolve(true);
});
});
}
} finally {
client.disconnect();
}
return { entityId: username };
};
return {
validateProviderInputs,
validateConnection,
create,
revoke,
renew
};
};

View File

@ -0,0 +1,174 @@
import handlebars from "handlebars";
import { customAlphabet } from "nanoid";
import snowflake from "snowflake-sdk";
import { z } from "zod";
import { BadRequestError } from "@app/lib/errors";
import { alphaNumericNanoId } from "@app/lib/nanoid";
import { DynamicSecretSnowflakeSchema, TDynamicProviderFns } from "./models";
// destroy client requires callback...
const noop = () => {};
const generatePassword = (size = 48) => {
const charset = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-_.~!*$#";
return customAlphabet(charset, 48)(size);
};
const generateUsername = () => {
return `infisical_${alphaNumericNanoId(32)}`; // username must start with alpha character, hence prefix
};
const getDaysToExpiry = (expiryDate: Date) => {
const start = new Date().getTime();
const end = new Date(expiryDate).getTime();
const diffTime = Math.abs(end - start);
return Math.ceil(diffTime / (1000 * 60 * 60 * 24));
};
export const SnowflakeProvider = (): TDynamicProviderFns => {
const validateProviderInputs = async (inputs: unknown) => {
const providerInputs = await DynamicSecretSnowflakeSchema.parseAsync(inputs);
return providerInputs;
};
const getClient = async (providerInputs: z.infer<typeof DynamicSecretSnowflakeSchema>) => {
const client = snowflake.createConnection({
account: `${providerInputs.orgId}-${providerInputs.accountId}`,
username: providerInputs.username,
password: providerInputs.password,
application: "Infisical"
});
await client.connectAsync(noop);
return client;
};
const validateConnection = async (inputs: unknown) => {
const providerInputs = await validateProviderInputs(inputs);
const client = await getClient(providerInputs);
let isValidConnection: boolean;
try {
isValidConnection = await Promise.race([
client.isValidAsync(),
new Promise((resolve) => {
setTimeout(resolve, 10000);
}).then(() => {
throw new BadRequestError({ message: "Unable to establish connection - verify credentials" });
})
]);
} finally {
client.destroy(noop);
}
return isValidConnection;
};
const create = async (inputs: unknown, expireAt: number) => {
const providerInputs = await validateProviderInputs(inputs);
const client = await getClient(providerInputs);
const username = generateUsername();
const password = generatePassword();
try {
const expiration = getDaysToExpiry(new Date(expireAt));
const creationStatement = handlebars.compile(providerInputs.creationStatement, { noEscape: true })({
username,
password,
expiration
});
await new Promise((resolve, reject) => {
client.execute({
sqlText: creationStatement,
complete(err) {
if (err) {
return reject(new BadRequestError({ name: "CreateLease", message: err.message }));
}
return resolve(true);
}
});
});
} finally {
client.destroy(noop);
}
return { entityId: username, data: { DB_USERNAME: username, DB_PASSWORD: password } };
};
const revoke = async (inputs: unknown, username: string) => {
const providerInputs = await validateProviderInputs(inputs);
const client = await getClient(providerInputs);
try {
const revokeStatement = handlebars.compile(providerInputs.revocationStatement)({ username });
await new Promise((resolve, reject) => {
client.execute({
sqlText: revokeStatement,
complete(err) {
if (err) {
return reject(new BadRequestError({ name: "RevokeLease", message: err.message }));
}
return resolve(true);
}
});
});
} finally {
client.destroy(noop);
}
return { entityId: username };
};
const renew = async (inputs: unknown, username: string, expireAt: number) => {
const providerInputs = await validateProviderInputs(inputs);
if (!providerInputs.renewStatement) return { entityId: username };
const client = await getClient(providerInputs);
try {
const expiration = getDaysToExpiry(new Date(expireAt));
const renewStatement = handlebars.compile(providerInputs.renewStatement)({
username,
expiration
});
await new Promise((resolve, reject) => {
client.execute({
sqlText: renewStatement,
complete(err) {
if (err) {
return reject(new BadRequestError({ name: "RenewLease", message: err.message }));
}
return resolve(true);
}
});
});
} finally {
client.destroy(noop);
}
return { entityId: username };
};
return {
validateProviderInputs,
validateConnection,
create,
revoke,
renew
};
};

View File

@ -3,11 +3,9 @@ import knex from "knex";
import { customAlphabet } from "nanoid";
import { z } from "zod";
import { getConfig } from "@app/lib/config/env";
import { BadRequestError } from "@app/lib/errors";
import { getDbConnectionHost } from "@app/lib/knex";
import { alphaNumericNanoId } from "@app/lib/nanoid";
import { verifyHostInputValidity } from "../dynamic-secret-fns";
import { DynamicSecretSqlDBSchema, SqlProviders, TDynamicProviderFns } from "./models";
const EXTERNAL_REQUEST_TIMEOUT = 10 * 1000;
@ -29,27 +27,8 @@ const generateUsername = (provider: SqlProviders) => {
export const SqlDatabaseProvider = (): TDynamicProviderFns => {
const validateProviderInputs = async (inputs: unknown) => {
const appCfg = getConfig();
const isCloud = Boolean(appCfg.LICENSE_SERVER_KEY); // quick and dirty way to check if its cloud or not
const dbHost = appCfg.DB_HOST || getDbConnectionHost(appCfg.DB_CONNECTION_URI);
const providerInputs = await DynamicSecretSqlDBSchema.parseAsync(inputs);
if (
isCloud &&
// localhost
// internal ips
(providerInputs.host === "host.docker.internal" ||
providerInputs.host.match(/^10\.\d+\.\d+\.\d+/) ||
providerInputs.host.match(/^192\.168\.\d+\.\d+/))
)
throw new BadRequestError({ message: "Invalid db host" });
if (
providerInputs.host === "localhost" ||
providerInputs.host === "127.0.0.1" ||
// database infisical uses
dbHost === providerInputs.host
)
throw new BadRequestError({ message: "Invalid db host" });
verifyHostInputValidity(providerInputs.host);
return providerInputs;
};

View File

@ -123,7 +123,7 @@ export const groupServiceFactory = ({
const plan = await licenseService.getPlan(actorOrgId);
if (!plan.groups)
throw new BadRequestError({
message: "Failed to update group due to plan restrictio Upgrade plan to update group."
message: "Failed to update group due to plan restriction Upgrade plan to update group."
});
const group = await groupDAL.findOne({ orgId: actorOrgId, id });

View File

@ -0,0 +1,58 @@
import * as pkcs11js from "pkcs11js";
import { getConfig } from "@app/lib/config/env";
import { logger } from "@app/lib/logger";
import { HsmModule } from "./hsm-types";
export const initializeHsmModule = () => {
const appCfg = getConfig();
// Create a new instance of PKCS11 module
const pkcs11 = new pkcs11js.PKCS11();
let isInitialized = false;
const initialize = () => {
if (!appCfg.isHsmConfigured) {
return;
}
try {
// Load the PKCS#11 module
pkcs11.load(appCfg.HSM_LIB_PATH!);
// Initialize the module
pkcs11.C_Initialize();
isInitialized = true;
logger.info("PKCS#11 module initialized");
} catch (err) {
logger.error("Failed to initialize PKCS#11 module:", err);
throw err;
}
};
const finalize = () => {
if (isInitialized) {
try {
pkcs11.C_Finalize();
isInitialized = false;
logger.info("PKCS#11 module finalized");
} catch (err) {
logger.error("Failed to finalize PKCS#11 module:", err);
throw err;
}
}
};
const getModule = (): HsmModule => ({
pkcs11,
isInitialized
});
return {
initialize,
finalize,
getModule
};
};

View File

@ -0,0 +1,470 @@
import pkcs11js from "pkcs11js";
import { getConfig } from "@app/lib/config/env";
import { logger } from "@app/lib/logger";
import { HsmKeyType, HsmModule } from "./hsm-types";
type THsmServiceFactoryDep = {
hsmModule: HsmModule;
};
export type THsmServiceFactory = ReturnType<typeof hsmServiceFactory>;
type SyncOrAsync<T> = T | Promise<T>;
type SessionCallback<T> = (session: pkcs11js.Handle) => SyncOrAsync<T>;
// eslint-disable-next-line no-empty-pattern
export const hsmServiceFactory = ({ hsmModule: { isInitialized, pkcs11 } }: THsmServiceFactoryDep) => {
const appCfg = getConfig();
// Constants for buffer structures
const IV_LENGTH = 16; // Luna HSM typically expects 16-byte IV for cbc
const BLOCK_SIZE = 16;
const HMAC_SIZE = 32;
const AES_KEY_SIZE = 256;
const HMAC_KEY_SIZE = 256;
const $withSession = async <T>(callbackWithSession: SessionCallback<T>): Promise<T> => {
const RETRY_INTERVAL = 200; // 200ms between attempts
const MAX_TIMEOUT = 90_000; // 90 seconds maximum total time
let sessionHandle: pkcs11js.Handle | null = null;
const removeSession = () => {
if (sessionHandle !== null) {
try {
pkcs11.C_Logout(sessionHandle);
pkcs11.C_CloseSession(sessionHandle);
logger.info("HSM: Terminated session successfully");
} catch (error) {
logger.error(error, "HSM: Failed to terminate session");
} finally {
sessionHandle = null;
}
}
};
try {
if (!pkcs11 || !isInitialized) {
throw new Error("PKCS#11 module is not initialized");
}
// Get slot list
let slots: pkcs11js.Handle[];
try {
slots = pkcs11.C_GetSlotList(false); // false to get all slots
} catch (error) {
throw new Error(`Failed to get slot list: ${(error as Error)?.message}`);
}
if (slots.length === 0) {
throw new Error("No slots available");
}
if (appCfg.HSM_SLOT >= slots.length) {
throw new Error(`HSM slot ${appCfg.HSM_SLOT} not found or not initialized`);
}
const slotId = slots[appCfg.HSM_SLOT];
const startTime = Date.now();
while (Date.now() - startTime < MAX_TIMEOUT) {
try {
// Open session
// eslint-disable-next-line no-bitwise
sessionHandle = pkcs11.C_OpenSession(slotId, pkcs11js.CKF_SERIAL_SESSION | pkcs11js.CKF_RW_SESSION);
// Login
try {
pkcs11.C_Login(sessionHandle, pkcs11js.CKU_USER, appCfg.HSM_PIN);
logger.info("HSM: Successfully authenticated");
break;
} catch (error) {
// Handle specific error cases
if (error instanceof pkcs11js.Pkcs11Error) {
if (error.code === pkcs11js.CKR_PIN_INCORRECT) {
// We throw instantly here to prevent further attempts, because if too many attempts are made, the HSM will potentially wipe all key material
logger.error(error, `HSM: Incorrect PIN detected for HSM slot ${appCfg.HSM_SLOT}`);
throw new Error("HSM: Incorrect HSM Pin detected. Please check the HSM configuration.");
}
if (error.code === pkcs11js.CKR_USER_ALREADY_LOGGED_IN) {
logger.warn("HSM: Session already logged in");
}
}
throw error; // Re-throw other errors
}
} catch (error) {
logger.warn(`HSM: Session creation failed. Retrying... Error: ${(error as Error)?.message}`);
if (sessionHandle !== null) {
try {
pkcs11.C_CloseSession(sessionHandle);
} catch (closeError) {
logger.error(closeError, "HSM: Failed to close session");
}
sessionHandle = null;
}
// Wait before retrying
// eslint-disable-next-line no-await-in-loop
await new Promise((resolve) => {
setTimeout(resolve, RETRY_INTERVAL);
});
}
}
if (sessionHandle === null) {
throw new Error("HSM: Failed to open session after maximum retries");
}
// Execute callback with session handle
const result = await callbackWithSession(sessionHandle);
removeSession();
return result;
} catch (error) {
logger.error(error, "HSM: Failed to open session");
throw error;
} finally {
// Ensure cleanup
removeSession();
}
};
const $findKey = (sessionHandle: pkcs11js.Handle, type: HsmKeyType) => {
const label = type === HsmKeyType.HMAC ? `${appCfg.HSM_KEY_LABEL}_HMAC` : appCfg.HSM_KEY_LABEL;
const keyType = type === HsmKeyType.HMAC ? pkcs11js.CKK_GENERIC_SECRET : pkcs11js.CKK_AES;
const template = [
{ type: pkcs11js.CKA_CLASS, value: pkcs11js.CKO_SECRET_KEY },
{ type: pkcs11js.CKA_KEY_TYPE, value: keyType },
{ type: pkcs11js.CKA_LABEL, value: label }
];
try {
// Initialize search
pkcs11.C_FindObjectsInit(sessionHandle, template);
try {
// Find first matching object
const handles = pkcs11.C_FindObjects(sessionHandle, 1);
if (handles.length === 0) {
throw new Error("Failed to find master key");
}
return handles[0]; // Return the key handle
} finally {
// Always finalize the search operation
pkcs11.C_FindObjectsFinal(sessionHandle);
}
} catch (error) {
return null;
}
};
const $keyExists = (session: pkcs11js.Handle, type: HsmKeyType): boolean => {
try {
const key = $findKey(session, type);
// items(0) will throw an error if no items are found
// Return true only if we got a valid object with handle
return !!key && key.length > 0;
} catch (error) {
// If items(0) throws, it means no key was found
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access, @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-call
logger.error(error, "HSM: Failed while checking for HSM key presence");
if (error instanceof pkcs11js.Pkcs11Error) {
if (error.code === pkcs11js.CKR_OBJECT_HANDLE_INVALID) {
return false;
}
}
return false;
}
};
const encrypt: {
(data: Buffer, providedSession: pkcs11js.Handle): Promise<Buffer>;
(data: Buffer): Promise<Buffer>;
} = async (data: Buffer, providedSession?: pkcs11js.Handle) => {
if (!pkcs11 || !isInitialized) {
throw new Error("PKCS#11 module is not initialized");
}
const $performEncryption = (sessionHandle: pkcs11js.Handle) => {
try {
const aesKey = $findKey(sessionHandle, HsmKeyType.AES);
if (!aesKey) {
throw new Error("HSM: Encryption failed, AES key not found");
}
const hmacKey = $findKey(sessionHandle, HsmKeyType.HMAC);
if (!hmacKey) {
throw new Error("HSM: Encryption failed, HMAC key not found");
}
const iv = Buffer.alloc(IV_LENGTH);
pkcs11.C_GenerateRandom(sessionHandle, iv);
const encryptMechanism = {
mechanism: pkcs11js.CKM_AES_CBC_PAD,
parameter: iv
};
pkcs11.C_EncryptInit(sessionHandle, encryptMechanism, aesKey);
// Calculate max buffer size (input length + potential full block of padding)
const maxEncryptedLength = Math.ceil(data.length / BLOCK_SIZE) * BLOCK_SIZE + BLOCK_SIZE;
// Encrypt the data - this returns the encrypted data directly
const encryptedData = pkcs11.C_Encrypt(sessionHandle, data, Buffer.alloc(maxEncryptedLength));
// Initialize HMAC
const hmacMechanism = {
mechanism: pkcs11js.CKM_SHA256_HMAC
};
pkcs11.C_SignInit(sessionHandle, hmacMechanism, hmacKey);
// Sign the IV and encrypted data
pkcs11.C_SignUpdate(sessionHandle, iv);
pkcs11.C_SignUpdate(sessionHandle, encryptedData);
// Get the HMAC
const hmac = Buffer.alloc(HMAC_SIZE);
pkcs11.C_SignFinal(sessionHandle, hmac);
// Combine encrypted data and HMAC [Encrypted Data | HMAC]
const finalBuffer = Buffer.alloc(encryptedData.length + hmac.length);
encryptedData.copy(finalBuffer);
hmac.copy(finalBuffer, encryptedData.length);
return Buffer.concat([iv, finalBuffer]);
} catch (error) {
logger.error(error, "HSM: Failed to perform encryption");
throw new Error(`HSM: Encryption failed: ${(error as Error)?.message}`);
}
};
if (providedSession) {
return $performEncryption(providedSession);
}
const result = await $withSession($performEncryption);
return result;
};
const decrypt: {
(encryptedBlob: Buffer, providedSession: pkcs11js.Handle): Promise<Buffer>;
(encryptedBlob: Buffer): Promise<Buffer>;
} = async (encryptedBlob: Buffer, providedSession?: pkcs11js.Handle) => {
if (!pkcs11 || !isInitialized) {
throw new Error("PKCS#11 module is not initialized");
}
const $performDecryption = (sessionHandle: pkcs11js.Handle) => {
try {
// structure is: [IV (16 bytes) | Encrypted Data (N bytes) | HMAC (32 bytes)]
const iv = encryptedBlob.subarray(0, IV_LENGTH);
const encryptedDataWithHmac = encryptedBlob.subarray(IV_LENGTH);
// Split encrypted data and HMAC
const hmac = encryptedDataWithHmac.subarray(-HMAC_SIZE); // Last 32 bytes are HMAC
const encryptedData = encryptedDataWithHmac.subarray(0, -HMAC_SIZE); // Everything except last 32 bytes
// Find the keys
const aesKey = $findKey(sessionHandle, HsmKeyType.AES);
if (!aesKey) {
throw new Error("HSM: Decryption failed, AES key not found");
}
const hmacKey = $findKey(sessionHandle, HsmKeyType.HMAC);
if (!hmacKey) {
throw new Error("HSM: Decryption failed, HMAC key not found");
}
// Verify HMAC first
const hmacMechanism = {
mechanism: pkcs11js.CKM_SHA256_HMAC
};
pkcs11.C_VerifyInit(sessionHandle, hmacMechanism, hmacKey);
pkcs11.C_VerifyUpdate(sessionHandle, iv);
pkcs11.C_VerifyUpdate(sessionHandle, encryptedData);
try {
pkcs11.C_VerifyFinal(sessionHandle, hmac);
} catch (error) {
logger.error(error, "HSM: HMAC verification failed");
throw new Error("HSM: Decryption failed"); // Generic error for failed verification
}
// Only decrypt if verification passed
const decryptMechanism = {
mechanism: pkcs11js.CKM_AES_CBC_PAD,
parameter: iv
};
pkcs11.C_DecryptInit(sessionHandle, decryptMechanism, aesKey);
const tempBuffer = Buffer.alloc(encryptedData.length);
const decryptedData = pkcs11.C_Decrypt(sessionHandle, encryptedData, tempBuffer);
// Create a new buffer from the decrypted data
return Buffer.from(decryptedData);
} catch (error) {
logger.error(error, "HSM: Failed to perform decryption");
throw new Error("HSM: Decryption failed"); // Generic error for failed decryption, to avoid leaking details about why it failed (such as padding related errors)
}
};
if (providedSession) {
return $performDecryption(providedSession);
}
const result = await $withSession($performDecryption);
return result;
};
// We test the core functionality of the PKCS#11 module that we are using throughout Infisical. This is to ensure that the user doesn't configure a faulty or unsupported HSM device.
const $testPkcs11Module = async (session: pkcs11js.Handle) => {
try {
if (!pkcs11 || !isInitialized) {
throw new Error("PKCS#11 module is not initialized");
}
if (!session) {
throw new Error("HSM: Attempted to run test without a valid session");
}
const randomData = pkcs11.C_GenerateRandom(session, Buffer.alloc(500));
const encryptedData = await encrypt(randomData, session);
const decryptedData = await decrypt(encryptedData, session);
const randomDataHex = randomData.toString("hex");
const decryptedDataHex = decryptedData.toString("hex");
if (randomDataHex !== decryptedDataHex && Buffer.compare(randomData, decryptedData)) {
throw new Error("HSM: Startup test failed. Decrypted data does not match original data");
}
return true;
} catch (error) {
logger.error(error, "HSM: Error testing PKCS#11 module");
return false;
}
};
const isActive = async () => {
if (!isInitialized || !appCfg.isHsmConfigured) {
return false;
}
let pkcs11TestPassed = false;
try {
pkcs11TestPassed = await $withSession($testPkcs11Module);
} catch (err) {
logger.error(err, "HSM: Error testing PKCS#11 module");
}
return appCfg.isHsmConfigured && isInitialized && pkcs11TestPassed;
};
const startService = async () => {
if (!appCfg.isHsmConfigured || !pkcs11 || !isInitialized) return;
try {
await $withSession(async (sessionHandle) => {
// Check if master key exists, create if not
const genericAttributes = [
{ type: pkcs11js.CKA_TOKEN, value: true }, // Persistent storage
{ type: pkcs11js.CKA_EXTRACTABLE, value: false }, // Cannot be extracted
{ type: pkcs11js.CKA_SENSITIVE, value: true }, // Sensitive value
{ type: pkcs11js.CKA_PRIVATE, value: true } // Requires authentication
];
if (!$keyExists(sessionHandle, HsmKeyType.AES)) {
// Template for generating 256-bit AES master key
const keyTemplate = [
{ type: pkcs11js.CKA_CLASS, value: pkcs11js.CKO_SECRET_KEY },
{ type: pkcs11js.CKA_KEY_TYPE, value: pkcs11js.CKK_AES },
{ type: pkcs11js.CKA_VALUE_LEN, value: AES_KEY_SIZE / 8 },
{ type: pkcs11js.CKA_LABEL, value: appCfg.HSM_KEY_LABEL! },
{ type: pkcs11js.CKA_ENCRYPT, value: true }, // Allow encryption
{ type: pkcs11js.CKA_DECRYPT, value: true }, // Allow decryption
...genericAttributes
];
// Generate the key
pkcs11.C_GenerateKey(
sessionHandle,
{
mechanism: pkcs11js.CKM_AES_KEY_GEN
},
keyTemplate
);
logger.info(`HSM: Master key created successfully with label: ${appCfg.HSM_KEY_LABEL}`);
}
// Check if HMAC key exists, create if not
if (!$keyExists(sessionHandle, HsmKeyType.HMAC)) {
const hmacKeyTemplate = [
{ type: pkcs11js.CKA_CLASS, value: pkcs11js.CKO_SECRET_KEY },
{ type: pkcs11js.CKA_KEY_TYPE, value: pkcs11js.CKK_GENERIC_SECRET },
{ type: pkcs11js.CKA_VALUE_LEN, value: HMAC_KEY_SIZE / 8 }, // 256-bit key
{ type: pkcs11js.CKA_LABEL, value: `${appCfg.HSM_KEY_LABEL!}_HMAC` },
{ type: pkcs11js.CKA_SIGN, value: true }, // Allow signing
{ type: pkcs11js.CKA_VERIFY, value: true }, // Allow verification
...genericAttributes
];
// Generate the HMAC key
pkcs11.C_GenerateKey(
sessionHandle,
{
mechanism: pkcs11js.CKM_GENERIC_SECRET_KEY_GEN
},
hmacKeyTemplate
);
logger.info(`HSM: HMAC key created successfully with label: ${appCfg.HSM_KEY_LABEL}_HMAC`);
}
// Get slot info to check supported mechanisms
const slotId = pkcs11.C_GetSessionInfo(sessionHandle).slotID;
const mechanisms = pkcs11.C_GetMechanismList(slotId);
// Check for AES CBC PAD support
const hasAesCbc = mechanisms.includes(pkcs11js.CKM_AES_CBC_PAD);
if (!hasAesCbc) {
throw new Error(`Required mechanism CKM_AEC_CBC_PAD not supported by HSM`);
}
// Run test encryption/decryption
const testPassed = await $testPkcs11Module(sessionHandle);
if (!testPassed) {
throw new Error("PKCS#11 module test failed. Please ensure that the HSM is correctly configured.");
}
});
} catch (error) {
logger.error(error, "HSM: Error initializing HSM service:");
throw error;
}
};
return {
encrypt,
startService,
isActive,
decrypt
};
};

View File

@ -0,0 +1,11 @@
import pkcs11js from "pkcs11js";
export type HsmModule = {
pkcs11: pkcs11js.PKCS11;
isInitialized: boolean;
};
export enum HsmKeyType {
AES = "AES",
HMAC = "hmac"
}

View File

@ -29,6 +29,7 @@ export const getDefaultOnPremFeatures = (): TFeatureSet => ({
auditLogStreams: false,
auditLogStreamLimit: 3,
samlSSO: false,
hsm: false,
oidcSSO: false,
scim: false,
ldap: false,
@ -47,7 +48,8 @@ export const getDefaultOnPremFeatures = (): TFeatureSet => ({
secretsLimit: 40
},
pkiEst: false,
enforceMfa: false
enforceMfa: false,
projectTemplates: false
});
export const setupLicenseRequestWithStore = (baseURL: string, refreshUrl: string, licenseKey: string) => {

View File

@ -129,7 +129,7 @@ export const licenseServiceFactory = ({
}
}
// this means this is self hosted oss version
// this means this is the self-hosted oss version
// else it would reach catch statement
isValidLicense = true;
} catch (error) {

View File

@ -46,6 +46,7 @@ export type TFeatureSet = {
auditLogStreams: false;
auditLogStreamLimit: 3;
samlSSO: false;
hsm: false;
oidcSSO: false;
scim: false;
ldap: false;
@ -65,6 +66,7 @@ export type TFeatureSet = {
};
pkiEst: boolean;
enforceMfa: boolean;
projectTemplates: false;
};
export type TOrgPlansTableDTO = {

View File

@ -26,7 +26,8 @@ export enum OrgPermissionSubjects {
Identity = "identity",
Kms = "kms",
AdminConsole = "organization-admin-console",
AuditLogs = "audit-logs"
AuditLogs = "audit-logs",
ProjectTemplates = "project-templates"
}
export type OrgPermissionSet =
@ -45,6 +46,7 @@ export type OrgPermissionSet =
| [OrgPermissionActions, OrgPermissionSubjects.Identity]
| [OrgPermissionActions, OrgPermissionSubjects.Kms]
| [OrgPermissionActions, OrgPermissionSubjects.AuditLogs]
| [OrgPermissionActions, OrgPermissionSubjects.ProjectTemplates]
| [OrgPermissionAdminConsoleAction, OrgPermissionSubjects.AdminConsole];
const buildAdminPermission = () => {
@ -118,6 +120,11 @@ const buildAdminPermission = () => {
can(OrgPermissionActions.Edit, OrgPermissionSubjects.AuditLogs);
can(OrgPermissionActions.Delete, OrgPermissionSubjects.AuditLogs);
can(OrgPermissionActions.Read, OrgPermissionSubjects.ProjectTemplates);
can(OrgPermissionActions.Create, OrgPermissionSubjects.ProjectTemplates);
can(OrgPermissionActions.Edit, OrgPermissionSubjects.ProjectTemplates);
can(OrgPermissionActions.Delete, OrgPermissionSubjects.ProjectTemplates);
can(OrgPermissionAdminConsoleAction.AccessAllProjects, OrgPermissionSubjects.AdminConsole);
return rules;

View File

@ -694,31 +694,35 @@ export const buildServiceTokenProjectPermission = (
const canRead = permission.includes("read");
const { can, build } = new AbilityBuilder<MongoAbility<ProjectPermissionSet>>(createMongoAbility);
scopes.forEach(({ secretPath, environment }) => {
if (canWrite) {
// TODO: @Akhi
// @ts-expect-error type
can(ProjectPermissionActions.Edit, ProjectPermissionSub.Secrets, {
secretPath: { $glob: secretPath },
environment
});
// @ts-expect-error type
can(ProjectPermissionActions.Create, ProjectPermissionSub.Secrets, {
secretPath: { $glob: secretPath },
environment
});
// @ts-expect-error type
can(ProjectPermissionActions.Delete, ProjectPermissionSub.Secrets, {
secretPath: { $glob: secretPath },
environment
});
}
if (canRead) {
// @ts-expect-error type
can(ProjectPermissionActions.Read, ProjectPermissionSub.Secrets, {
secretPath: { $glob: secretPath },
environment
});
}
[ProjectPermissionSub.Secrets, ProjectPermissionSub.SecretImports, ProjectPermissionSub.SecretFolders].forEach(
(subject) => {
if (canWrite) {
// TODO: @Akhi
// @ts-expect-error type
can(ProjectPermissionActions.Edit, subject, {
secretPath: { $glob: secretPath },
environment
});
// @ts-expect-error type
can(ProjectPermissionActions.Create, subject, {
secretPath: { $glob: secretPath },
environment
});
// @ts-expect-error type
can(ProjectPermissionActions.Delete, subject, {
secretPath: { $glob: secretPath },
environment
});
}
if (canRead) {
// @ts-expect-error type
can(ProjectPermissionActions.Read, subject, {
secretPath: { $glob: secretPath },
environment
});
}
}
);
});
return build({ conditionsMatcher });

View File

@ -0,0 +1,5 @@
export const ProjectTemplateDefaultEnvironments = [
{ name: "Development", slug: "dev", position: 1 },
{ name: "Staging", slug: "staging", position: 2 },
{ name: "Production", slug: "prod", position: 3 }
];

View File

@ -0,0 +1,7 @@
import { TDbClient } from "@app/db";
import { TableName } from "@app/db/schemas";
import { ormify } from "@app/lib/knex";
export type TProjectTemplateDALFactory = ReturnType<typeof projectTemplateDALFactory>;
export const projectTemplateDALFactory = (db: TDbClient) => ormify(db, TableName.ProjectTemplates);

View File

@ -0,0 +1,24 @@
import { ProjectTemplateDefaultEnvironments } from "@app/ee/services/project-template/project-template-constants";
import {
InfisicalProjectTemplate,
TUnpackedPermission
} from "@app/ee/services/project-template/project-template-types";
import { getPredefinedRoles } from "@app/services/project-role/project-role-fns";
export const getDefaultProjectTemplate = (orgId: string) => ({
id: "b11b49a9-09a9-4443-916a-4246f9ff2c69", // random ID to appease zod
name: InfisicalProjectTemplate.Default,
createdAt: new Date(),
updatedAt: new Date(),
description: "Infisical's default project template",
environments: ProjectTemplateDefaultEnvironments,
roles: [...getPredefinedRoles("project-template")].map(({ name, slug, permissions }) => ({
name,
slug,
permissions: permissions as TUnpackedPermission[]
})),
orgId
});
export const isInfisicalProjectTemplate = (template: string) =>
Object.values(InfisicalProjectTemplate).includes(template as InfisicalProjectTemplate);

View File

@ -0,0 +1,265 @@
import { ForbiddenError } from "@casl/ability";
import { packRules } from "@casl/ability/extra";
import { TProjectTemplates } from "@app/db/schemas";
import { TLicenseServiceFactory } from "@app/ee/services/license/license-service";
import { OrgPermissionActions, OrgPermissionSubjects } from "@app/ee/services/permission/org-permission";
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service";
import { getDefaultProjectTemplate } from "@app/ee/services/project-template/project-template-fns";
import {
TCreateProjectTemplateDTO,
TProjectTemplateEnvironment,
TProjectTemplateRole,
TUnpackedPermission,
TUpdateProjectTemplateDTO
} from "@app/ee/services/project-template/project-template-types";
import { BadRequestError, NotFoundError } from "@app/lib/errors";
import { OrgServiceActor } from "@app/lib/types";
import { unpackPermissions } from "@app/server/routes/santizedSchemas/permission";
import { getPredefinedRoles } from "@app/services/project-role/project-role-fns";
import { TProjectTemplateDALFactory } from "./project-template-dal";
type TProjectTemplatesServiceFactoryDep = {
licenseService: TLicenseServiceFactory;
permissionService: TPermissionServiceFactory;
projectTemplateDAL: TProjectTemplateDALFactory;
};
export type TProjectTemplateServiceFactory = ReturnType<typeof projectTemplateServiceFactory>;
const $unpackProjectTemplate = ({ roles, environments, ...rest }: TProjectTemplates) => ({
...rest,
environments: environments as TProjectTemplateEnvironment[],
roles: [
...getPredefinedRoles("project-template").map(({ name, slug, permissions }) => ({
name,
slug,
permissions: permissions as TUnpackedPermission[]
})),
...(roles as TProjectTemplateRole[]).map((role) => ({
...role,
permissions: unpackPermissions(role.permissions)
}))
]
});
export const projectTemplateServiceFactory = ({
licenseService,
permissionService,
projectTemplateDAL
}: TProjectTemplatesServiceFactoryDep) => {
const listProjectTemplatesByOrg = async (actor: OrgServiceActor) => {
const plan = await licenseService.getPlan(actor.orgId);
if (!plan.projectTemplates)
throw new BadRequestError({
message: "Failed to access project templates due to plan restriction. Upgrade plan to access project templates."
});
const { permission } = await permissionService.getOrgPermission(
actor.type,
actor.id,
actor.orgId,
actor.authMethod,
actor.orgId
);
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Read, OrgPermissionSubjects.ProjectTemplates);
const projectTemplates = await projectTemplateDAL.find({
orgId: actor.orgId
});
return [
getDefaultProjectTemplate(actor.orgId),
...projectTemplates.map((template) => $unpackProjectTemplate(template))
];
};
const findProjectTemplateByName = async (name: string, actor: OrgServiceActor) => {
const plan = await licenseService.getPlan(actor.orgId);
if (!plan.projectTemplates)
throw new BadRequestError({
message: "Failed to access project template due to plan restriction. Upgrade plan to access project templates."
});
const projectTemplate = await projectTemplateDAL.findOne({ name, orgId: actor.orgId });
if (!projectTemplate) throw new NotFoundError({ message: `Could not find project template with Name "${name}"` });
const { permission } = await permissionService.getOrgPermission(
actor.type,
actor.id,
projectTemplate.orgId,
actor.authMethod,
actor.orgId
);
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Read, OrgPermissionSubjects.ProjectTemplates);
return {
...$unpackProjectTemplate(projectTemplate),
packedRoles: projectTemplate.roles as TProjectTemplateRole[] // preserve packed for when applying template
};
};
const findProjectTemplateById = async (id: string, actor: OrgServiceActor) => {
const plan = await licenseService.getPlan(actor.orgId);
if (!plan.projectTemplates)
throw new BadRequestError({
message: "Failed to access project template due to plan restriction. Upgrade plan to access project templates."
});
const projectTemplate = await projectTemplateDAL.findById(id);
if (!projectTemplate) throw new NotFoundError({ message: `Could not find project template with ID ${id}` });
const { permission } = await permissionService.getOrgPermission(
actor.type,
actor.id,
projectTemplate.orgId,
actor.authMethod,
actor.orgId
);
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Read, OrgPermissionSubjects.ProjectTemplates);
return {
...$unpackProjectTemplate(projectTemplate),
packedRoles: projectTemplate.roles as TProjectTemplateRole[] // preserve packed for when applying template
};
};
const createProjectTemplate = async (
{ roles, environments, ...params }: TCreateProjectTemplateDTO,
actor: OrgServiceActor
) => {
const plan = await licenseService.getPlan(actor.orgId);
if (!plan.projectTemplates)
throw new BadRequestError({
message: "Failed to create project template due to plan restriction. Upgrade plan to access project templates."
});
const { permission } = await permissionService.getOrgPermission(
actor.type,
actor.id,
actor.orgId,
actor.authMethod,
actor.orgId
);
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Create, OrgPermissionSubjects.ProjectTemplates);
const isConflictingName = Boolean(
await projectTemplateDAL.findOne({
name: params.name,
orgId: actor.orgId
})
);
if (isConflictingName)
throw new BadRequestError({
message: `A project template with the name "${params.name}" already exists.`
});
const projectTemplate = await projectTemplateDAL.create({
...params,
roles: JSON.stringify(roles.map((role) => ({ ...role, permissions: packRules(role.permissions) }))),
environments: JSON.stringify(environments),
orgId: actor.orgId
});
return $unpackProjectTemplate(projectTemplate);
};
const updateProjectTemplateById = async (
id: string,
{ roles, environments, ...params }: TUpdateProjectTemplateDTO,
actor: OrgServiceActor
) => {
const plan = await licenseService.getPlan(actor.orgId);
if (!plan.projectTemplates)
throw new BadRequestError({
message: "Failed to update project template due to plan restriction. Upgrade plan to access project templates."
});
const projectTemplate = await projectTemplateDAL.findById(id);
if (!projectTemplate) throw new NotFoundError({ message: `Could not find project template with ID ${id}` });
const { permission } = await permissionService.getOrgPermission(
actor.type,
actor.id,
projectTemplate.orgId,
actor.authMethod,
actor.orgId
);
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Edit, OrgPermissionSubjects.ProjectTemplates);
if (params.name && projectTemplate.name !== params.name) {
const isConflictingName = Boolean(
await projectTemplateDAL.findOne({
name: params.name,
orgId: projectTemplate.orgId
})
);
if (isConflictingName)
throw new BadRequestError({
message: `A project template with the name "${params.name}" already exists.`
});
}
const updatedProjectTemplate = await projectTemplateDAL.updateById(id, {
...params,
roles: roles
? JSON.stringify(roles.map((role) => ({ ...role, permissions: packRules(role.permissions) })))
: undefined,
environments: environments ? JSON.stringify(environments) : undefined
});
return $unpackProjectTemplate(updatedProjectTemplate);
};
const deleteProjectTemplateById = async (id: string, actor: OrgServiceActor) => {
const plan = await licenseService.getPlan(actor.orgId);
if (!plan.projectTemplates)
throw new BadRequestError({
message: "Failed to delete project template due to plan restriction. Upgrade plan to access project templates."
});
const projectTemplate = await projectTemplateDAL.findById(id);
if (!projectTemplate) throw new NotFoundError({ message: `Could not find project template with ID ${id}` });
const { permission } = await permissionService.getOrgPermission(
actor.type,
actor.id,
projectTemplate.orgId,
actor.authMethod,
actor.orgId
);
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Delete, OrgPermissionSubjects.ProjectTemplates);
const deletedProjectTemplate = await projectTemplateDAL.deleteById(id);
return $unpackProjectTemplate(deletedProjectTemplate);
};
return {
listProjectTemplatesByOrg,
createProjectTemplate,
updateProjectTemplateById,
deleteProjectTemplateById,
findProjectTemplateById,
findProjectTemplateByName
};
};

View File

@ -0,0 +1,28 @@
import { z } from "zod";
import { TProjectEnvironments } from "@app/db/schemas";
import { TProjectPermissionV2Schema } from "@app/ee/services/permission/project-permission";
import { UnpackedPermissionSchema } from "@app/server/routes/santizedSchemas/permission";
export type TProjectTemplateEnvironment = Pick<TProjectEnvironments, "name" | "slug" | "position">;
export type TProjectTemplateRole = {
slug: string;
name: string;
permissions: TProjectPermissionV2Schema[];
};
export type TCreateProjectTemplateDTO = {
name: string;
description?: string;
roles: TProjectTemplateRole[];
environments: TProjectTemplateEnvironment[];
};
export type TUpdateProjectTemplateDTO = Partial<TCreateProjectTemplateDTO>;
export type TUnpackedPermission = z.infer<typeof UnpackedPermissionSchema>;
export enum InfisicalProjectTemplate {
Default = "default"
}

View File

@ -267,7 +267,8 @@ export const secretApprovalRequestServiceFactory = ({
: "",
secretComment: el.secretVersion.encryptedComment
? secretManagerDecryptor({ cipherTextBlob: el.secretVersion.encryptedComment }).toString()
: ""
: "",
tags: el.secretVersion.tags
}
: undefined
}));
@ -571,7 +572,7 @@ export const secretApprovalRequestServiceFactory = ({
reminderNote: el.reminderNote,
skipMultilineEncoding: el.skipMultilineEncoding,
key: el.key,
tagIds: el?.tags.map(({ id }) => id),
tags: el?.tags.map(({ id }) => id),
...encryptedValue
}
};

View File

@ -85,7 +85,8 @@ export const secretRotationDbFn = async ({
password,
username,
client,
variables
variables,
options
}: TSecretRotationDbFn) => {
const appCfg = getConfig();
@ -117,7 +118,8 @@ export const secretRotationDbFn = async ({
password,
connectionTimeoutMillis: EXTERNAL_REQUEST_TIMEOUT,
ssl,
pool: { min: 0, max: 1 }
pool: { min: 0, max: 1 },
options
}
});
const data = await db.raw(query, variables);
@ -153,6 +155,14 @@ export const getDbSetQuery = (db: TDbProviderClients, variables: { username: str
variables: [variables.username]
};
}
if (db === TDbProviderClients.MsSqlServer) {
return {
query: `ALTER LOGIN ?? WITH PASSWORD = '${variables.password}'`,
variables: [variables.username]
};
}
// add more based on client
return {
query: `ALTER USER ?? IDENTIFIED BY '${variables.password}'`,

View File

@ -24,4 +24,5 @@ export type TSecretRotationDbFn = {
query: string;
variables: unknown[];
ca?: string;
options?: Record<string, unknown>;
};

View File

@ -94,7 +94,9 @@ export const secretRotationQueueFactory = ({
// on prod it this will be in days, in development this will be second
every: appCfg.NODE_ENV === "development" ? secondsToMillis(interval) : daysToMillisecond(interval),
immediately: true
}
},
removeOnComplete: true,
removeOnFail: true
}
);
};
@ -114,6 +116,7 @@ export const secretRotationQueueFactory = ({
queue.start(QueueName.SecretRotation, async (job) => {
const { rotationId } = job.data;
const appCfg = getConfig();
logger.info(`secretRotationQueue.process: [rotationDocument=${rotationId}]`);
const secretRotation = await secretRotationDAL.findById(rotationId);
const rotationProvider = rotationTemplates.find(({ name }) => name === secretRotation?.provider);
@ -172,6 +175,15 @@ export const secretRotationQueueFactory = ({
// set a random value for new password
newCredential.internal.rotated_password = alphaNumericNanoId(32);
const { admin_username: username, admin_password: password, host, database, port, ca } = newCredential.inputs;
const options =
provider.template.client === TDbProviderClients.MsSqlServer
? ({
encrypt: appCfg.ENABLE_MSSQL_SECRET_ROTATION_ENCRYPT,
cryptoCredentialsDetails: ca ? { ca } : {}
} as Record<string, unknown>)
: undefined;
const dbFunctionArg = {
username,
password,
@ -179,8 +191,10 @@ export const secretRotationQueueFactory = ({
database,
port,
ca: ca as string,
client: provider.template.client === TDbProviderClients.MySql ? "mysql2" : provider.template.client
client: provider.template.client === TDbProviderClients.MySql ? "mysql2" : provider.template.client,
options
} as TSecretRotationDbFn;
// set function
await secretRotationDbFn({
...dbFunctionArg,
@ -189,12 +203,17 @@ export const secretRotationQueueFactory = ({
username: newCredential.internal.username as string
})
});
// test function
const testQuery =
provider.template.client === TDbProviderClients.MsSqlServer ? "SELECT GETDATE()" : "SELECT NOW()";
await secretRotationDbFn({
...dbFunctionArg,
query: "SELECT NOW()",
query: testQuery,
variables: []
});
newCredential.outputs.db_username = newCredential.internal.username;
newCredential.outputs.db_password = newCredential.internal.rotated_password;
// clean up

View File

@ -1,4 +1,5 @@
import { AWS_IAM_TEMPLATE } from "./aws-iam";
import { MSSQL_TEMPLATE } from "./mssql";
import { MYSQL_TEMPLATE } from "./mysql";
import { POSTGRES_TEMPLATE } from "./postgres";
import { SENDGRID_TEMPLATE } from "./sendgrid";
@ -26,6 +27,13 @@ export const rotationTemplates: TSecretRotationProviderTemplate[] = [
description: "Rotate MySQL@7/MariaDB user credentials",
template: MYSQL_TEMPLATE
},
{
name: "mssql",
title: "Microsoft SQL Server",
image: "mssqlserver.png",
description: "Rotate Microsoft SQL server user credentials",
template: MSSQL_TEMPLATE
},
{
name: "aws-iam",
title: "AWS IAM",

View File

@ -0,0 +1,33 @@
import { TDbProviderClients, TProviderFunctionTypes } from "./types";
export const MSSQL_TEMPLATE = {
type: TProviderFunctionTypes.DB as const,
client: TDbProviderClients.MsSqlServer,
inputs: {
type: "object" as const,
properties: {
admin_username: { type: "string" as const },
admin_password: { type: "string" as const },
host: { type: "string" as const },
database: { type: "string" as const, default: "master" },
port: { type: "integer" as const, default: "1433" },
username1: {
type: "string",
default: "infisical-sql-user1",
desc: "SQL Server login name that must be created at server level with a matching database user"
},
username2: {
type: "string",
default: "infisical-sql-user2",
desc: "SQL Server login name that must be created at server level with a matching database user"
},
ca: { type: "string", desc: "SSL certificate for db auth(string)" }
},
required: ["admin_username", "admin_password", "host", "database", "username1", "username2", "port"],
additionalProperties: false
},
outputs: {
db_username: { type: "string" },
db_password: { type: "string" }
}
};

View File

@ -8,7 +8,9 @@ export enum TDbProviderClients {
// postgres, cockroack db, amazon red shift
Pg = "pg",
// mysql and maria db
MySql = "mysql"
MySql = "mysql",
MsSqlServer = "mssql"
}
export enum TAwsProviderSystems {

View File

@ -29,7 +29,7 @@ export const KeyStorePrefixes = {
};
export const KeyStoreTtls = {
SetSyncSecretIntegrationLastRunTimestampInSeconds: 10,
SetSyncSecretIntegrationLastRunTimestampInSeconds: 60,
AccessTokenStatusUpdateInSeconds: 120
};

View File

@ -391,7 +391,8 @@ export const PROJECTS = {
CREATE: {
organizationSlug: "The slug of the organization to create the project in.",
projectName: "The name of the project to create.",
slug: "An optional slug for the project."
slug: "An optional slug for the project.",
template: "The name of the project template, if specified, to apply to this project."
},
DELETE: {
workspaceId: "The ID of the project to delete."
@ -669,6 +670,12 @@ export const RAW_SECRETS = {
type: "The type of the secret to delete.",
projectSlug: "The slug of the project to delete the secret in.",
workspaceId: "The ID of the project where the secret is located."
},
GET_REFERENCE_TREE: {
secretName: "The name of the secret to get the reference tree for.",
workspaceId: "The ID of the project where the secret is located.",
environment: "The slug of the environment where the the secret is located.",
secretPath: "The folder path where the secret is located."
}
} as const;
@ -1432,3 +1439,22 @@ export const KMS = {
ciphertext: "The ciphertext to be decrypted (base64 encoded)."
}
};
export const ProjectTemplates = {
CREATE: {
name: "The name of the project template to be created. Must be slug-friendly.",
description: "An optional description of the project template.",
roles: "The roles to be created when the template is applied to a project.",
environments: "The environments to be created when the template is applied to a project."
},
UPDATE: {
templateId: "The ID of the project template to be updated.",
name: "The updated name of the project template. Must be slug-friendly.",
description: "The updated description of the project template.",
roles: "The updated roles to be created when the template is applied to a project.",
environments: "The updated environments to be created when the template is applied to a project."
},
DELETE: {
templateId: "The ID of the project template to be deleted."
}
};

View File

@ -162,10 +162,23 @@ const envSchema = z
DISABLE_AUDIT_LOG_GENERATION: zodStrBool.default("false"),
SSL_CLIENT_CERTIFICATE_HEADER_KEY: zpStr(z.string().optional()).default("x-ssl-client-cert"),
WORKFLOW_SLACK_CLIENT_ID: zpStr(z.string().optional()),
WORKFLOW_SLACK_CLIENT_SECRET: zpStr(z.string().optional())
WORKFLOW_SLACK_CLIENT_SECRET: zpStr(z.string().optional()),
ENABLE_MSSQL_SECRET_ROTATION_ENCRYPT: zodStrBool.default("true"),
// HSM
HSM_LIB_PATH: zpStr(z.string().optional()),
HSM_PIN: zpStr(z.string().optional()),
HSM_KEY_LABEL: zpStr(z.string().optional()),
HSM_SLOT: z.coerce.number().optional().default(0)
})
// To ensure that basic encryption is always possible.
.refine(
(data) => Boolean(data.ENCRYPTION_KEY) || Boolean(data.ROOT_ENCRYPTION_KEY),
"Either ENCRYPTION_KEY or ROOT_ENCRYPTION_KEY must be defined."
)
.transform((data) => ({
...data,
DB_READ_REPLICAS: data.DB_READ_REPLICAS
? databaseReadReplicaSchema.parse(JSON.parse(data.DB_READ_REPLICAS))
: undefined,
@ -174,10 +187,14 @@ const envSchema = z
isRedisConfigured: Boolean(data.REDIS_URL),
isDevelopmentMode: data.NODE_ENV === "development",
isProductionMode: data.NODE_ENV === "production" || IS_PACKAGED,
isSecretScanningConfigured:
Boolean(data.SECRET_SCANNING_GIT_APP_ID) &&
Boolean(data.SECRET_SCANNING_PRIVATE_KEY) &&
Boolean(data.SECRET_SCANNING_WEBHOOK_SECRET),
isHsmConfigured:
Boolean(data.HSM_LIB_PATH) && Boolean(data.HSM_PIN) && Boolean(data.HSM_KEY_LABEL) && data.HSM_SLOT !== undefined,
samlDefaultOrgSlug: data.DEFAULT_SAML_ORG_SLUG,
SECRET_SCANNING_ORG_WHITELIST: data.SECRET_SCANNING_ORG_WHITELIST?.split(",")
}));

View File

@ -57,3 +57,10 @@ export enum OrderByDirection {
ASC = "asc",
DESC = "desc"
}
export type OrgServiceActor = {
type: ActorType;
id: string;
authMethod: ActorAuthMethod;
orgId: string;
};

View File

@ -1,2 +1,3 @@
export { isDisposableEmail } from "./validate-email";
export { isValidFolderName, isValidSecretPath } from "./validate-folder-name";
export { blockLocalAndPrivateIpAddresses } from "./validate-url";

View File

@ -0,0 +1,8 @@
// regex to allow only alphanumeric, dash, underscore
export const isValidFolderName = (name: string) => /^[a-zA-Z0-9-_]+$/.test(name);
export const isValidSecretPath = (path: string) =>
path
.split("/")
.filter((el) => el.length)
.every((name) => isValidFolderName(name));

View File

@ -1,6 +1,8 @@
import dotenv from "dotenv";
import path from "path";
import { initializeHsmModule } from "@app/ee/services/hsm/hsm-fns";
import { initAuditLogDbConnection, initDbConnection } from "./db";
import { keyStoreFactory } from "./keystore/keystore";
import { formatSmtpConfig, initEnvConfig, IS_PACKAGED } from "./lib/config/env";
@ -53,13 +55,17 @@ const run = async () => {
const queue = queueServiceFactory(appCfg.REDIS_URL);
const keyStore = keyStoreFactory(appCfg.REDIS_URL);
const server = await main({ db, auditLogDb, smtp, logger, queue, keyStore });
const hsmModule = initializeHsmModule();
hsmModule.initialize();
const server = await main({ db, auditLogDb, hsmModule: hsmModule.getModule(), smtp, logger, queue, keyStore });
const bootstrap = await bootstrapCheck({ db });
// eslint-disable-next-line
process.on("SIGINT", async () => {
await server.close();
await db.destroy();
hsmModule.finalize();
process.exit(0);
});
@ -67,6 +73,7 @@ const run = async () => {
process.on("SIGTERM", async () => {
await server.close();
await db.destroy();
hsmModule.finalize();
process.exit(0);
});

View File

@ -14,6 +14,7 @@ import fastify from "fastify";
import { Knex } from "knex";
import { Logger } from "pino";
import { HsmModule } from "@app/ee/services/hsm/hsm-types";
import { TKeyStoreFactory } from "@app/keystore/keystore";
import { getConfig, IS_PACKAGED } from "@app/lib/config/env";
import { TQueueServiceFactory } from "@app/queue";
@ -36,16 +37,19 @@ type TMain = {
logger?: Logger;
queue: TQueueServiceFactory;
keyStore: TKeyStoreFactory;
hsmModule: HsmModule;
};
// Run the server!
export const main = async ({ db, auditLogDb, smtp, logger, queue, keyStore }: TMain) => {
export const main = async ({ db, hsmModule, auditLogDb, smtp, logger, queue, keyStore }: TMain) => {
const appCfg = getConfig();
const server = fastify({
logger: appCfg.NODE_ENV === "test" ? false : logger,
trustProxy: true,
connectionTimeout: 30 * 1000,
ignoreTrailingSlash: true
connectionTimeout: appCfg.isHsmConfigured ? 90_000 : 30_000,
ignoreTrailingSlash: true,
pluginTimeout: 40_000
}).withTypeProvider<ZodTypeProvider>();
server.setValidatorCompiler(validatorCompiler);
@ -95,7 +99,7 @@ export const main = async ({ db, auditLogDb, smtp, logger, queue, keyStore }: TM
await server.register(maintenanceMode);
await server.register(registerRoutes, { smtp, queue, db, auditLogDb, keyStore });
await server.register(registerRoutes, { smtp, queue, db, auditLogDb, keyStore, hsmModule });
if (appCfg.isProductionMode) {
await server.register(registerExternalNextjs, {

View File

@ -15,8 +15,12 @@ export const fastifySwagger = fp(async (fastify) => {
},
servers: [
{
url: "https://app.infisical.com",
description: "Production server"
url: "https://us.infisical.com",
description: "Production server (US)"
},
{
url: "https://eu.infisical.com",
description: "Production server (EU)"
},
{
url: "http://localhost:8080",

View File

@ -1,5 +1,4 @@
import { CronJob } from "cron";
// import { Redis } from "ioredis";
import { Knex } from "knex";
import { z } from "zod";
@ -31,6 +30,8 @@ import { externalKmsServiceFactory } from "@app/ee/services/external-kms/externa
import { groupDALFactory } from "@app/ee/services/group/group-dal";
import { groupServiceFactory } from "@app/ee/services/group/group-service";
import { userGroupMembershipDALFactory } from "@app/ee/services/group/user-group-membership-dal";
import { hsmServiceFactory } from "@app/ee/services/hsm/hsm-service";
import { HsmModule } from "@app/ee/services/hsm/hsm-types";
import { identityProjectAdditionalPrivilegeDALFactory } from "@app/ee/services/identity-project-additional-privilege/identity-project-additional-privilege-dal";
import { identityProjectAdditionalPrivilegeServiceFactory } from "@app/ee/services/identity-project-additional-privilege/identity-project-additional-privilege-service";
import { identityProjectAdditionalPrivilegeV2ServiceFactory } from "@app/ee/services/identity-project-additional-privilege-v2/identity-project-additional-privilege-v2-service";
@ -43,6 +44,8 @@ import { oidcConfigDALFactory } from "@app/ee/services/oidc/oidc-config-dal";
import { oidcConfigServiceFactory } from "@app/ee/services/oidc/oidc-config-service";
import { permissionDALFactory } from "@app/ee/services/permission/permission-dal";
import { permissionServiceFactory } from "@app/ee/services/permission/permission-service";
import { projectTemplateDALFactory } from "@app/ee/services/project-template/project-template-dal";
import { projectTemplateServiceFactory } from "@app/ee/services/project-template/project-template-service";
import { projectUserAdditionalPrivilegeDALFactory } from "@app/ee/services/project-user-additional-privilege/project-user-additional-privilege-dal";
import { projectUserAdditionalPrivilegeServiceFactory } from "@app/ee/services/project-user-additional-privilege/project-user-additional-privilege-service";
import { rateLimitDALFactory } from "@app/ee/services/rate-limit/rate-limit-dal";
@ -221,10 +224,18 @@ export const registerRoutes = async (
{
auditLogDb,
db,
hsmModule,
smtp: smtpService,
queue: queueService,
keyStore
}: { auditLogDb?: Knex; db: Knex; smtp: TSmtpService; queue: TQueueServiceFactory; keyStore: TKeyStoreFactory }
}: {
auditLogDb?: Knex;
db: Knex;
hsmModule: HsmModule;
smtp: TSmtpService;
queue: TQueueServiceFactory;
keyStore: TKeyStoreFactory;
}
) => {
const appCfg = getConfig();
await server.register(registerSecretScannerGhApp, { prefix: "/ss-webhook" });
@ -340,6 +351,8 @@ export const registerRoutes = async (
const externalGroupOrgRoleMappingDAL = externalGroupOrgRoleMappingDALFactory(db);
const projectTemplateDAL = projectTemplateDALFactory(db);
const permissionService = permissionServiceFactory({
permissionDAL,
orgRoleDAL,
@ -348,14 +361,21 @@ export const registerRoutes = async (
projectDAL
});
const licenseService = licenseServiceFactory({ permissionService, orgDAL, licenseDAL, keyStore });
const hsmService = hsmServiceFactory({
hsmModule
});
const kmsService = kmsServiceFactory({
kmsRootConfigDAL,
keyStore,
kmsDAL,
internalKmsDAL,
orgDAL,
projectDAL
projectDAL,
hsmService
});
const externalKmsService = externalKmsServiceFactory({
kmsDAL,
kmsService,
@ -552,6 +572,7 @@ export const registerRoutes = async (
userDAL,
authService: loginService,
serverCfgDAL: superAdminDAL,
kmsRootConfigDAL,
orgService,
keyStore,
licenseService,
@ -732,6 +753,12 @@ export const registerRoutes = async (
permissionService
});
const projectTemplateService = projectTemplateServiceFactory({
licenseService,
permissionService,
projectTemplateDAL
});
const projectService = projectServiceFactory({
permissionService,
projectDAL,
@ -758,7 +785,8 @@ export const registerRoutes = async (
projectBotDAL,
certificateTemplateDAL,
projectSlackConfigDAL,
slackIntegrationDAL
slackIntegrationDAL,
projectTemplateService
});
const projectEnvService = projectEnvServiceFactory({
@ -1250,10 +1278,13 @@ export const registerRoutes = async (
});
await superAdminService.initServerCfg();
//
// setup the communication with license key server
await licenseService.init();
// Start HSM service if it's configured/enabled.
await hsmService.startService();
await telemetryQueue.startTelemetryCheck();
await dailyResourceCleanUp.startCleanUp();
await dailyExpiringPkiItemAlert.startSendingAlerts();
@ -1331,12 +1362,14 @@ export const registerRoutes = async (
secretSharing: secretSharingService,
userEngagement: userEngagementService,
externalKms: externalKmsService,
hsm: hsmService,
cmek: cmekService,
orgAdmin: orgAdminService,
slack: slackService,
workflowIntegration: workflowIntegrationService,
migration: migrationService,
externalGroupOrgRoleMapping: externalGroupOrgRoleMappingService
externalGroupOrgRoleMapping: externalGroupOrgRoleMappingService,
projectTemplate: projectTemplateService
});
const cronJobs: CronJob[] = [];

View File

@ -7,6 +7,7 @@ import { readLimit, writeLimit } from "@app/server/config/rateLimiter";
import { verifySuperAdmin } from "@app/server/plugins/auth/superAdmin";
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
import { AuthMode } from "@app/services/auth/auth-type";
import { RootKeyEncryptionStrategy } from "@app/services/kms/kms-types";
import { getServerCfg } from "@app/services/super-admin/super-admin-service";
import { LoginMethod } from "@app/services/super-admin/super-admin-types";
import { PostHogEventTypes } from "@app/services/telemetry/telemetry-types";
@ -29,6 +30,8 @@ export const registerAdminRouter = async (server: FastifyZodProvider) => {
}).extend({
isMigrationModeOn: z.boolean(),
defaultAuthOrgSlug: z.string().nullable(),
defaultAuthOrgAuthEnforced: z.boolean().nullish(),
defaultAuthOrgAuthMethod: z.string().nullish(),
isSecretScanningDisabled: z.boolean()
})
})
@ -193,6 +196,57 @@ export const registerAdminRouter = async (server: FastifyZodProvider) => {
}
});
server.route({
method: "GET",
url: "/encryption-strategies",
config: {
rateLimit: readLimit
},
schema: {
response: {
200: z.object({
strategies: z
.object({
strategy: z.nativeEnum(RootKeyEncryptionStrategy),
enabled: z.boolean()
})
.array()
})
}
},
onRequest: (req, res, done) => {
verifyAuth([AuthMode.JWT])(req, res, () => {
verifySuperAdmin(req, res, done);
});
},
handler: async () => {
const encryptionDetails = await server.services.superAdmin.getConfiguredEncryptionStrategies();
return encryptionDetails;
}
});
server.route({
method: "PATCH",
url: "/encryption-strategies",
config: {
rateLimit: writeLimit
},
schema: {
body: z.object({
strategy: z.nativeEnum(RootKeyEncryptionStrategy)
})
},
onRequest: (req, res, done) => {
verifyAuth([AuthMode.JWT])(req, res, () => {
verifySuperAdmin(req, res, done);
});
},
handler: async (req) => {
await server.services.superAdmin.updateRootEncryptionStrategy(req.body.strategy);
}
});
server.route({
method: "POST",
url: "/signup",

View File

@ -14,7 +14,8 @@ import { validateTemplateRegexField } from "@app/services/certificate-template/c
const sanitizedEstConfig = CertificateTemplateEstConfigsSchema.pick({
id: true,
certificateTemplateId: true,
isEnabled: true
isEnabled: true,
disableBootstrapCertValidation: true
});
export const registerCertificateTemplateRouter = async (server: FastifyZodProvider) => {
@ -241,11 +242,18 @@ export const registerCertificateTemplateRouter = async (server: FastifyZodProvid
params: z.object({
certificateTemplateId: z.string().trim()
}),
body: z.object({
caChain: z.string().trim().min(1),
passphrase: z.string().min(1),
isEnabled: z.boolean().default(true)
}),
body: z
.object({
caChain: z.string().trim().optional(),
passphrase: z.string().min(1),
isEnabled: z.boolean().default(true),
disableBootstrapCertValidation: z.boolean().default(false)
})
.refine(
({ caChain, disableBootstrapCertValidation }) =>
disableBootstrapCertValidation || (!disableBootstrapCertValidation && caChain),
"CA chain is required"
),
response: {
200: sanitizedEstConfig
}
@ -289,8 +297,9 @@ export const registerCertificateTemplateRouter = async (server: FastifyZodProvid
certificateTemplateId: z.string().trim()
}),
body: z.object({
caChain: z.string().trim().min(1).optional(),
caChain: z.string().trim().optional(),
passphrase: z.string().min(1).optional(),
disableBootstrapCertValidation: z.boolean().optional(),
isEnabled: z.boolean().optional()
}),
response: {

View File

@ -293,10 +293,10 @@ export const registerCmekRouter = async (server: FastifyZodProvider) => {
schema: {
description: "Decrypt data with KMS key",
params: z.object({
keyId: z.string().uuid().describe(KMS.ENCRYPT.keyId)
keyId: z.string().uuid().describe(KMS.DECRYPT.keyId)
}),
body: z.object({
ciphertext: base64Schema.describe(KMS.ENCRYPT.plaintext)
ciphertext: base64Schema.describe(KMS.DECRYPT.ciphertext)
}),
response: {
200: z.object({

View File

@ -20,6 +20,8 @@ import { AuthMode } from "@app/services/auth/auth-type";
import { SecretsOrderBy } from "@app/services/secret/secret-types";
import { PostHogEventTypes } from "@app/services/telemetry/telemetry-types";
const MAX_DEEP_SEARCH_LIMIT = 500; // arbitrary limit to prevent excessive results
// handle querystring boolean values
const booleanSchema = z
.union([z.boolean(), z.string().trim()])
@ -34,6 +36,35 @@ const booleanSchema = z
.optional()
.default(true);
const parseSecretPathSearch = (search?: string) => {
if (!search)
return {
searchName: "",
searchPath: ""
};
if (!search.includes("/"))
return {
searchName: search,
searchPath: ""
};
if (search === "/")
return {
searchName: "",
searchPath: "/"
};
const [searchName, ...searchPathSegments] = search.split("/").reverse();
let searchPath = removeTrailingSlash(searchPathSegments.reverse().join("/").toLowerCase());
if (!searchPath.startsWith("/")) searchPath = `/${searchPath}`;
return {
searchName,
searchPath
};
};
export const registerDashboardRouter = async (server: FastifyZodProvider) => {
server.route({
method: "GET",
@ -134,7 +165,7 @@ export const registerDashboardRouter = async (server: FastifyZodProvider) => {
let folders: Awaited<ReturnType<typeof server.services.folder.getFoldersMultiEnv>> | undefined;
let secrets: Awaited<ReturnType<typeof server.services.secret.getSecretsRawMultiEnv>> | undefined;
let dynamicSecrets:
| Awaited<ReturnType<typeof server.services.dynamicSecret.listDynamicSecretsByFolderIds>>
| Awaited<ReturnType<typeof server.services.dynamicSecret.listDynamicSecretsByEnvs>>
| undefined;
let totalFolderCount: number | undefined;
@ -218,7 +249,7 @@ export const registerDashboardRouter = async (server: FastifyZodProvider) => {
});
if (remainingLimit > 0 && totalDynamicSecretCount > adjustedOffset) {
dynamicSecrets = await server.services.dynamicSecret.listDynamicSecretsByFolderIds({
dynamicSecrets = await server.services.dynamicSecret.listDynamicSecretsByEnvs({
actor: req.permission.type,
actorId: req.permission.id,
actorAuthMethod: req.permission.authMethod,
@ -633,4 +664,180 @@ export const registerDashboardRouter = async (server: FastifyZodProvider) => {
};
}
});
server.route({
method: "GET",
url: "/secrets-deep-search",
config: {
rateLimit: secretsLimit
},
schema: {
security: [
{
bearerAuth: []
}
],
querystring: z.object({
projectId: z.string().trim(),
environments: z.string().trim().transform(decodeURIComponent),
secretPath: z.string().trim().default("/").transform(removeTrailingSlash),
search: z.string().trim().optional(),
tags: z.string().trim().transform(decodeURIComponent).optional()
}),
response: {
200: z.object({
folders: SecretFoldersSchema.extend({ path: z.string() }).array().optional(),
dynamicSecrets: SanitizedDynamicSecretSchema.extend({ path: z.string(), environment: z.string() })
.array()
.optional(),
secrets: secretRawSchema
.extend({
secretPath: z.string().optional(),
tags: SecretTagsSchema.pick({
id: true,
slug: true,
color: true
})
.extend({ name: z.string() })
.array()
.optional()
})
.array()
.optional()
})
}
},
onRequest: verifyAuth([AuthMode.JWT]),
handler: async (req) => {
const { secretPath, projectId, search } = req.query;
const environments = req.query.environments.split(",").filter((env) => Boolean(env.trim()));
if (!environments.length) throw new BadRequestError({ message: "One or more environments required" });
const tags = req.query.tags?.split(",").filter((tag) => Boolean(tag.trim())) ?? [];
if (!search && !tags.length) throw new BadRequestError({ message: "Search or tags required" });
const searchHasTags = Boolean(tags.length);
const allFolders = await server.services.folder.getFoldersDeepByEnvs(
{
projectId,
environments,
secretPath
},
req.permission
);
const { searchName, searchPath } = parseSecretPathSearch(search);
const folderMappings = allFolders.map((folder) => ({
folderId: folder.id,
path: folder.path,
environment: folder.environment
}));
const sharedFilters = {
search: searchName,
limit: MAX_DEEP_SEARCH_LIMIT,
orderBy: SecretsOrderBy.Name
};
const secrets = await server.services.secret.getSecretsRawByFolderMappings(
{
projectId,
folderMappings,
filters: {
...sharedFilters,
tagSlugs: tags,
includeTagsInSearch: true
}
},
req.permission
);
const dynamicSecrets = searchHasTags
? []
: await server.services.dynamicSecret.listDynamicSecretsByFolderIds(
{
projectId,
folderMappings,
filters: sharedFilters
},
req.permission
);
for await (const environment of environments) {
const secretCountForEnv = secrets.filter((secret) => secret.environment === environment).length;
if (secretCountForEnv) {
await server.services.auditLog.createAuditLog({
projectId,
...req.auditLogInfo,
event: {
type: EventType.GET_SECRETS,
metadata: {
environment,
secretPath,
numberOfSecrets: secretCountForEnv
}
}
});
if (getUserAgentType(req.headers["user-agent"]) !== UserAgentType.K8_OPERATOR) {
await server.services.telemetry.sendPostHogEvents({
event: PostHogEventTypes.SecretPulled,
distinctId: getTelemetryDistinctId(req),
properties: {
numberOfSecrets: secretCountForEnv,
workspaceId: projectId,
environment,
secretPath,
channel: getUserAgentType(req.headers["user-agent"]),
...req.auditLogInfo
}
});
}
}
}
const sliceQuickSearch = <T>(array: T[]) => array.slice(0, 25);
return {
secrets: sliceQuickSearch(
searchPath ? secrets.filter((secret) => secret.secretPath.endsWith(searchPath)) : secrets
),
dynamicSecrets: sliceQuickSearch(
searchPath
? dynamicSecrets.filter((dynamicSecret) => dynamicSecret.path.endsWith(searchPath))
: dynamicSecrets
),
folders: searchHasTags
? []
: sliceQuickSearch(
allFolders.filter((folder) => {
const [folderName, ...folderPathSegments] = folder.path.split("/").reverse();
const folderPath = folderPathSegments.reverse().join("/").toLowerCase() || "/";
if (searchPath) {
if (searchPath === "/") {
// only show root folders if no folder name search
if (!searchName) return folderPath === searchPath;
// start partial match on root folders
return folderName.toLowerCase().startsWith(searchName.toLowerCase());
}
// support ending partial path match
return (
folderPath.endsWith(searchPath) && folderName.toLowerCase().startsWith(searchName.toLowerCase())
);
}
// no search path, "fuzzy" match all folders
return folderName.toLowerCase().includes(searchName.toLowerCase());
})
)
};
}
});
};

View File

@ -891,6 +891,48 @@ export const registerIntegrationAuthRouter = async (server: FastifyZodProvider)
}
});
server.route({
method: "GET",
url: "/:integrationAuthId/bitbucket/environments",
config: {
rateLimit: readLimit
},
onRequest: verifyAuth([AuthMode.JWT]),
schema: {
params: z.object({
integrationAuthId: z.string().trim()
}),
querystring: z.object({
workspaceSlug: z.string().trim().min(1, { message: "Workspace slug required" }),
repoSlug: z.string().trim().min(1, { message: "Repo slug required" })
}),
response: {
200: z.object({
environments: z
.object({
name: z.string(),
slug: z.string(),
uuid: z.string(),
type: z.string()
})
.array()
})
}
},
handler: async (req) => {
const environments = await server.services.integrationAuth.getBitbucketEnvironments({
actorId: req.permission.id,
actor: req.permission.type,
actorAuthMethod: req.permission.authMethod,
actorOrgId: req.permission.orgId,
id: req.params.integrationAuthId,
workspaceSlug: req.query.workspaceSlug,
repoSlug: req.query.repoSlug
});
return { environments };
}
});
server.route({
method: "GET",
url: "/:integrationAuthId/northflank/secret-groups",

View File

@ -4,6 +4,7 @@ import { SecretFoldersSchema } from "@app/db/schemas";
import { EventType } from "@app/ee/services/audit-log/audit-log-types";
import { FOLDERS } from "@app/lib/api-docs";
import { prefixWithSlash, removeTrailingSlash } from "@app/lib/fn";
import { isValidFolderName } from "@app/lib/validator";
import { readLimit, secretsLimit } from "@app/server/config/rateLimiter";
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
import { AuthMode } from "@app/services/auth/auth-type";
@ -25,7 +26,13 @@ export const registerSecretFolderRouter = async (server: FastifyZodProvider) =>
body: z.object({
workspaceId: z.string().trim().describe(FOLDERS.CREATE.workspaceId),
environment: z.string().trim().describe(FOLDERS.CREATE.environment),
name: z.string().trim().describe(FOLDERS.CREATE.name),
name: z
.string()
.trim()
.describe(FOLDERS.CREATE.name)
.refine((name) => isValidFolderName(name), {
message: "Invalid folder name. Only alphanumeric characters, dashes, and underscores are allowed."
}),
path: z
.string()
.trim()
@ -97,7 +104,13 @@ export const registerSecretFolderRouter = async (server: FastifyZodProvider) =>
body: z.object({
workspaceId: z.string().trim().describe(FOLDERS.UPDATE.workspaceId),
environment: z.string().trim().describe(FOLDERS.UPDATE.environment),
name: z.string().trim().describe(FOLDERS.UPDATE.name),
name: z
.string()
.trim()
.describe(FOLDERS.UPDATE.name)
.refine((name) => isValidFolderName(name), {
message: "Invalid folder name. Only alphanumeric characters, dashes, and underscores are allowed."
}),
path: z
.string()
.trim()
@ -170,7 +183,13 @@ export const registerSecretFolderRouter = async (server: FastifyZodProvider) =>
.object({
id: z.string().describe(FOLDERS.UPDATE.folderId),
environment: z.string().trim().describe(FOLDERS.UPDATE.environment),
name: z.string().trim().describe(FOLDERS.UPDATE.name),
name: z
.string()
.trim()
.describe(FOLDERS.UPDATE.name)
.refine((name) => isValidFolderName(name), {
message: "Invalid folder name. Only alphanumeric characters, dashes, and underscores are allowed."
}),
path: z
.string()
.trim()

View File

@ -9,6 +9,7 @@ import {
ProjectKeysSchema
} from "@app/db/schemas";
import { EventType } from "@app/ee/services/audit-log/audit-log-types";
import { InfisicalProjectTemplate } from "@app/ee/services/project-template/project-template-types";
import { PROJECTS } from "@app/lib/api-docs";
import { readLimit, writeLimit } from "@app/server/config/rateLimiter";
import { getTelemetryDistinctId } from "@app/server/lib/telemetry";
@ -169,7 +170,15 @@ export const registerProjectRouter = async (server: FastifyZodProvider) => {
})
.optional()
.describe(PROJECTS.CREATE.slug),
kmsKeyId: z.string().optional()
kmsKeyId: z.string().optional(),
template: z
.string()
.refine((v) => slugify(v) === v, {
message: "Template name must be in slug format"
})
.optional()
.default(InfisicalProjectTemplate.Default)
.describe(PROJECTS.CREATE.template)
}),
response: {
200: z.object({
@ -186,7 +195,8 @@ export const registerProjectRouter = async (server: FastifyZodProvider) => {
actorAuthMethod: req.permission.authMethod,
workspaceName: req.body.projectName,
slug: req.body.slug,
kmsKeyId: req.body.kmsKeyId
kmsKeyId: req.body.kmsKeyId,
template: req.body.template
});
await server.services.telemetry.sendPostHogEvents({
@ -199,6 +209,20 @@ export const registerProjectRouter = async (server: FastifyZodProvider) => {
}
});
if (req.body.template) {
await server.services.auditLog.createAuditLog({
...req.auditLogInfo,
orgId: req.permission.orgId,
event: {
type: EventType.APPLY_PROJECT_TEMPLATE,
metadata: {
template: req.body.template,
projectId: project.id
}
}
});
}
return { project };
}
});

View File

@ -23,6 +23,18 @@ import { PostHogEventTypes } from "@app/services/telemetry/telemetry-types";
import { secretRawSchema } from "../sanitizedSchemas";
const SecretReferenceNode = z.object({
key: z.string(),
value: z.string().optional(),
environment: z.string(),
secretPath: z.string()
});
type TSecretReferenceNode = z.infer<typeof SecretReferenceNode> & { children: TSecretReferenceNode[] };
const SecretReferenceNodeTree: z.ZodType<TSecretReferenceNode> = SecretReferenceNode.extend({
children: z.lazy(() => SecretReferenceNodeTree.array())
});
export const registerSecretRouter = async (server: FastifyZodProvider) => {
server.route({
method: "POST",
@ -2102,6 +2114,58 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => {
}
});
server.route({
method: "GET",
url: "/raw/:secretName/secret-reference-tree",
config: {
rateLimit: secretsLimit
},
schema: {
description: "Get secret reference tree",
security: [
{
bearerAuth: []
}
],
params: z.object({
secretName: z.string().trim().describe(RAW_SECRETS.GET_REFERENCE_TREE.secretName)
}),
querystring: z.object({
workspaceId: z.string().trim().describe(RAW_SECRETS.GET_REFERENCE_TREE.workspaceId),
environment: z.string().trim().describe(RAW_SECRETS.GET_REFERENCE_TREE.environment),
secretPath: z
.string()
.trim()
.default("/")
.transform(removeTrailingSlash)
.describe(RAW_SECRETS.GET_REFERENCE_TREE.secretPath)
}),
response: {
200: z.object({
tree: SecretReferenceNodeTree,
value: z.string().optional()
})
}
},
onRequest: verifyAuth([AuthMode.JWT]),
handler: async (req) => {
const { secretName } = req.params;
const { secretPath, environment, workspaceId } = req.query;
const { tree, value } = await server.services.secret.getSecretReferenceTree({
actorId: req.permission.id,
actor: req.permission.type,
actorAuthMethod: req.permission.authMethod,
actorOrgId: req.permission.orgId,
projectId: workspaceId,
secretName,
secretPath,
environment
});
return { tree, value };
}
});
server.route({
method: "POST",
url: "/backfill-secret-references",

View File

@ -235,7 +235,8 @@ export const certificateTemplateServiceFactory = ({
actorId,
actorAuthMethod,
actor,
actorOrgId
actorOrgId,
disableBootstrapCertValidation
}: TCreateEstConfigurationDTO) => {
const plan = await licenseService.getPlan(actorOrgId);
if (!plan.pkiEst) {
@ -266,39 +267,45 @@ export const certificateTemplateServiceFactory = ({
const appCfg = getConfig();
const certificateManagerKmsId = await getProjectKmsCertificateKeyId({
projectId: certTemplate.projectId,
projectDAL,
kmsService
});
let encryptedCaChain: Buffer | undefined;
if (caChain) {
const certificateManagerKmsId = await getProjectKmsCertificateKeyId({
projectId: certTemplate.projectId,
projectDAL,
kmsService
});
// validate CA chain
const certificates = caChain
.match(/-----BEGIN CERTIFICATE-----[\s\S]+?-----END CERTIFICATE-----/g)
?.map((cert) => new x509.X509Certificate(cert));
// validate CA chain
const certificates = caChain
.match(/-----BEGIN CERTIFICATE-----[\s\S]+?-----END CERTIFICATE-----/g)
?.map((cert) => new x509.X509Certificate(cert));
if (!certificates) {
throw new BadRequestError({ message: "Failed to parse certificate chain" });
if (!certificates) {
throw new BadRequestError({ message: "Failed to parse certificate chain" });
}
if (!(await isCertChainValid(certificates))) {
throw new BadRequestError({ message: "Invalid certificate chain" });
}
const kmsEncryptor = await kmsService.encryptWithKmsKey({
kmsId: certificateManagerKmsId
});
const { cipherTextBlob } = await kmsEncryptor({
plainText: Buffer.from(caChain)
});
encryptedCaChain = cipherTextBlob;
}
if (!(await isCertChainValid(certificates))) {
throw new BadRequestError({ message: "Invalid certificate chain" });
}
const kmsEncryptor = await kmsService.encryptWithKmsKey({
kmsId: certificateManagerKmsId
});
const { cipherTextBlob: encryptedCaChain } = await kmsEncryptor({
plainText: Buffer.from(caChain)
});
const hashedPassphrase = await bcrypt.hash(passphrase, appCfg.SALT_ROUNDS);
const estConfig = await certificateTemplateEstConfigDAL.create({
certificateTemplateId,
hashedPassphrase,
encryptedCaChain,
isEnabled
isEnabled,
disableBootstrapCertValidation
});
return { ...estConfig, projectId: certTemplate.projectId };
@ -312,7 +319,8 @@ export const certificateTemplateServiceFactory = ({
actorId,
actorAuthMethod,
actor,
actorOrgId
actorOrgId,
disableBootstrapCertValidation
}: TUpdateEstConfigurationDTO) => {
const plan = await licenseService.getPlan(actorOrgId);
if (!plan.pkiEst) {
@ -360,7 +368,8 @@ export const certificateTemplateServiceFactory = ({
});
const updatedData: TCertificateTemplateEstConfigsUpdate = {
isEnabled
isEnabled,
disableBootstrapCertValidation
};
if (caChain) {
@ -442,18 +451,24 @@ export const certificateTemplateServiceFactory = ({
kmsId: certificateManagerKmsId
});
const decryptedCaChain = await kmsDecryptor({
cipherTextBlob: estConfig.encryptedCaChain
});
let decryptedCaChain = "";
if (estConfig.encryptedCaChain) {
decryptedCaChain = (
await kmsDecryptor({
cipherTextBlob: estConfig.encryptedCaChain
})
).toString();
}
return {
certificateTemplateId,
id: estConfig.id,
isEnabled: estConfig.isEnabled,
caChain: decryptedCaChain.toString(),
caChain: decryptedCaChain,
hashedPassphrase: estConfig.hashedPassphrase,
projectId: certTemplate.projectId,
orgId: certTemplate.orgId
orgId: certTemplate.orgId,
disableBootstrapCertValidation: estConfig.disableBootstrapCertValidation
};
};

View File

@ -34,9 +34,10 @@ export type TDeleteCertTemplateDTO = {
export type TCreateEstConfigurationDTO = {
certificateTemplateId: string;
caChain: string;
caChain?: string;
passphrase: string;
isEnabled: boolean;
disableBootstrapCertValidation: boolean;
} & Omit<TProjectPermission, "projectId">;
export type TUpdateEstConfigurationDTO = {
@ -44,6 +45,7 @@ export type TUpdateEstConfigurationDTO = {
caChain?: string;
passphrase?: string;
isEnabled?: boolean;
disableBootstrapCertValidation?: boolean;
} & Omit<TProjectPermission, "projectId">;
export type TGetEstConfigurationDTO =

View File

@ -1,9 +1,9 @@
import { ForbiddenError } from "@casl/ability";
import { FastifyRequest } from "fastify";
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service";
import { ProjectPermissionCmekActions, ProjectPermissionSub } from "@app/ee/services/permission/project-permission";
import { BadRequestError, NotFoundError } from "@app/lib/errors";
import { OrgServiceActor } from "@app/lib/types";
import {
TCmekDecryptDTO,
TCmekEncryptDTO,
@ -23,7 +23,7 @@ type TCmekServiceFactoryDep = {
export type TCmekServiceFactory = ReturnType<typeof cmekServiceFactory>;
export const cmekServiceFactory = ({ kmsService, kmsDAL, permissionService }: TCmekServiceFactoryDep) => {
const createCmek = async ({ projectId, ...dto }: TCreateCmekDTO, actor: FastifyRequest["permission"]) => {
const createCmek = async ({ projectId, ...dto }: TCreateCmekDTO, actor: OrgServiceActor) => {
const { permission } = await permissionService.getProjectPermission(
actor.type,
actor.id,
@ -43,7 +43,7 @@ export const cmekServiceFactory = ({ kmsService, kmsDAL, permissionService }: TC
return cmek;
};
const updateCmekById = async ({ keyId, ...data }: TUpdabteCmekByIdDTO, actor: FastifyRequest["permission"]) => {
const updateCmekById = async ({ keyId, ...data }: TUpdabteCmekByIdDTO, actor: OrgServiceActor) => {
const key = await kmsDAL.findById(keyId);
if (!key) throw new NotFoundError({ message: `Key with ID ${keyId} not found` });
@ -65,7 +65,7 @@ export const cmekServiceFactory = ({ kmsService, kmsDAL, permissionService }: TC
return cmek;
};
const deleteCmekById = async (keyId: string, actor: FastifyRequest["permission"]) => {
const deleteCmekById = async (keyId: string, actor: OrgServiceActor) => {
const key = await kmsDAL.findById(keyId);
if (!key) throw new NotFoundError({ message: `Key with ID ${keyId} not found` });
@ -87,10 +87,7 @@ export const cmekServiceFactory = ({ kmsService, kmsDAL, permissionService }: TC
return cmek;
};
const listCmeksByProjectId = async (
{ projectId, ...filters }: TListCmeksByProjectIdDTO,
actor: FastifyRequest["permission"]
) => {
const listCmeksByProjectId = async ({ projectId, ...filters }: TListCmeksByProjectIdDTO, actor: OrgServiceActor) => {
const { permission } = await permissionService.getProjectPermission(
actor.type,
actor.id,
@ -106,7 +103,7 @@ export const cmekServiceFactory = ({ kmsService, kmsDAL, permissionService }: TC
return { cmeks, totalCount };
};
const cmekEncrypt = async ({ keyId, plaintext }: TCmekEncryptDTO, actor: FastifyRequest["permission"]) => {
const cmekEncrypt = async ({ keyId, plaintext }: TCmekEncryptDTO, actor: OrgServiceActor) => {
const key = await kmsDAL.findById(keyId);
if (!key) throw new NotFoundError({ message: `Key with ID ${keyId} not found` });
@ -132,7 +129,7 @@ export const cmekServiceFactory = ({ kmsService, kmsDAL, permissionService }: TC
return cipherTextBlob.toString("base64");
};
const cmekDecrypt = async ({ keyId, ciphertext }: TCmekDecryptDTO, actor: FastifyRequest["permission"]) => {
const cmekDecrypt = async ({ keyId, ciphertext }: TCmekDecryptDTO, actor: OrgServiceActor) => {
const key = await kmsDAL.findById(keyId);
if (!key) throw new NotFoundError({ message: `Key with ID ${keyId} not found` });

View File

@ -1,9 +1,9 @@
import { ForbiddenError } from "@casl/ability";
import { FastifyRequest } from "fastify";
import { TLicenseServiceFactory } from "@app/ee/services/license/license-service";
import { OrgPermissionActions, OrgPermissionSubjects } from "@app/ee/services/permission/org-permission";
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service";
import { OrgServiceActor } from "@app/lib/types";
import { constructGroupOrgMembershipRoleMappings } from "@app/services/external-group-org-role-mapping/external-group-org-role-mapping-fns";
import { TSyncExternalGroupOrgMembershipRoleMappingsDTO } from "@app/services/external-group-org-role-mapping/external-group-org-role-mapping-types";
import { TOrgRoleDALFactory } from "@app/services/org/org-role-dal";
@ -25,7 +25,7 @@ export const externalGroupOrgRoleMappingServiceFactory = ({
permissionService,
orgRoleDAL
}: TExternalGroupOrgRoleMappingServiceFactoryDep) => {
const listExternalGroupOrgRoleMappings = async (actor: FastifyRequest["permission"]) => {
const listExternalGroupOrgRoleMappings = async (actor: OrgServiceActor) => {
const { permission } = await permissionService.getOrgPermission(
actor.type,
actor.id,
@ -46,7 +46,7 @@ export const externalGroupOrgRoleMappingServiceFactory = ({
const updateExternalGroupOrgRoleMappings = async (
dto: TSyncExternalGroupOrgMembershipRoleMappingsDTO,
actor: FastifyRequest["permission"]
actor: OrgServiceActor
) => {
const { permission } = await permissionService.getOrgPermission(
actor.type,

View File

@ -126,6 +126,8 @@ export const parseEnvKeyDataFn = async (decryptedJson: string): Promise<Infisica
return findRootInheritedSecret(inheritedEnv.variables[secretName], secretName, envs);
};
const targetIdToFolderIdsMap = new Map<string, string>();
const processBranches = () => {
for (const subEnv of parsedJson.subEnvironments) {
const app = parsedJson.apps.find((a) => a.id === subEnv.envParentId);
@ -135,12 +137,21 @@ export const parseEnvKeyDataFn = async (decryptedJson: string): Promise<Infisica
// Handle regular app branches
const branchEnvironment = infisicalImportData.environments.find((e) => e.id === subEnv.parentEnvironmentId);
infisicalImportData.folders.push({
name: subEnv.subName,
parentFolderId: subEnv.parentEnvironmentId,
environmentId: branchEnvironment!.id,
id: subEnv.id
});
// check if the folder already exists in the same parent environment with the same name
const folderExists = infisicalImportData.folders.some(
(f) => f.name === subEnv.subName && f.parentFolderId === subEnv.parentEnvironmentId
);
// No need to map to target ID's here, because we are not dealing with blocks
if (!folderExists) {
infisicalImportData.folders.push({
name: subEnv.subName,
parentFolderId: subEnv.parentEnvironmentId,
environmentId: branchEnvironment!.id,
id: subEnv.id
});
}
}
if (block) {
@ -162,13 +173,22 @@ export const parseEnvKeyDataFn = async (decryptedJson: string): Promise<Infisica
// eslint-disable-next-line no-continue
if (!matchingAppEnv) continue;
// 3. Create a folder in the matching app environment
infisicalImportData.folders.push({
name: subEnv.subName,
parentFolderId: matchingAppEnv.id,
environmentId: matchingAppEnv.id,
id: `${subEnv.id}-${appId}` // Create unique ID for each app's copy of the branch
});
const folderExists = infisicalImportData.folders.some(
(f) => f.name === subEnv.subName && f.parentFolderId === matchingAppEnv.id
);
if (!folderExists) {
// 3. Create a folder in the matching app environment
infisicalImportData.folders.push({
name: subEnv.subName,
parentFolderId: matchingAppEnv.id,
environmentId: matchingAppEnv.id,
id: `${subEnv.id}-${appId}` // Create unique ID for each app's copy of the branch
});
} else {
// folder already exists, so lets map the old folder id to the new folder id
targetIdToFolderIdsMap.set(subEnv.id, `${subEnv.id}-${appId}`);
}
// 4. Process secrets in the block branch for this app
const branchSecrets = parsedJson.envs[subEnv.id]?.variables || {};
@ -408,17 +428,18 @@ export const parseEnvKeyDataFn = async (decryptedJson: string): Promise<Infisica
// Process each secret in this environment or branch
for (const [secretName, secretData] of Object.entries(envData.variables)) {
const environmentId = subEnv ? subEnv.parentEnvironmentId : env;
const indexOfExistingSecret = infisicalImportData.secrets.findIndex(
(s) => s.name === secretName && s.environmentId === environmentId
(s) =>
s.name === secretName &&
(s.environmentId === subEnv?.parentEnvironmentId || s.environmentId === env) &&
(s.folderId ? s.folderId === subEnv?.id : true) &&
(secretData.val ? s.value === secretData.val : true)
);
if (secretData.inheritsEnvironmentId) {
const resolvedSecret = findRootInheritedSecret(secretData, secretName, parsedJson.envs);
// Check if there's already a secret with this name in the environment, if there is, we should override it. Because if there's already one, we know its coming from a block.
// Variables from the normal environment should take precedence over variables from the block.
if (indexOfExistingSecret !== -1) {
// if a existing secret is found, we should replace it directly
const newSecret: (typeof infisicalImportData.secrets)[number] = {
@ -456,12 +477,14 @@ export const parseEnvKeyDataFn = async (decryptedJson: string): Promise<Infisica
continue;
}
const folderId = targetIdToFolderIdsMap.get(subEnv?.id || "") || subEnv?.id;
infisicalImportData.secrets.push({
id: randomUUID(),
name: secretName,
environmentId: subEnv ? subEnv.parentEnvironmentId : env,
value: secretData.val || "",
...(subEnv && { folderId: subEnv.id }) // Add folderId if this is a branch secret
...(folderId && { folderId })
});
}
}
@ -591,6 +614,7 @@ export const importDataIntoInfisicalFn = async ({
secretKey: string;
secretValue: string;
folderId?: string;
isFromBlock?: boolean;
}[]
>();
@ -599,6 +623,8 @@ export const importDataIntoInfisicalFn = async ({
// Skip if we can't find either an environment or folder mapping for this secret
if (!originalToNewEnvironmentId.get(secret.environmentId) && !originalToNewFolderId.get(targetId)) {
logger.info({ secret }, "[importDataIntoInfisicalFn]: Could not find environment or folder for secret");
// eslint-disable-next-line no-continue
continue;
}
@ -606,10 +632,22 @@ export const importDataIntoInfisicalFn = async ({
if (!mappedToEnvironmentId.has(targetId)) {
mappedToEnvironmentId.set(targetId, []);
}
const alreadyHasSecret = mappedToEnvironmentId
.get(targetId)!
.find((el) => el.secretKey === secret.name && el.folderId === secret.folderId);
if (alreadyHasSecret && alreadyHasSecret.isFromBlock) {
// remove the existing secret if any
mappedToEnvironmentId
.get(targetId)!
.splice(mappedToEnvironmentId.get(targetId)!.indexOf(alreadyHasSecret), 1);
}
mappedToEnvironmentId.get(targetId)!.push({
secretKey: secret.name,
secretValue: secret.value || "",
folderId: secret.folderId
folderId: secret.folderId,
isFromBlock: secret.appBlockOrderIndex !== undefined
});
}

View File

@ -20,6 +20,7 @@ import { getApps } from "./integration-app-list";
import { TIntegrationAuthDALFactory } from "./integration-auth-dal";
import { IntegrationAuthMetadataSchema, TIntegrationAuthMetadata } from "./integration-auth-schema";
import {
TBitbucketEnvironment,
TBitbucketWorkspace,
TChecklyGroups,
TDeleteIntegrationAuthByIdDTO,
@ -30,6 +31,7 @@ import {
THerokuPipelineCoupling,
TIntegrationAuthAppsDTO,
TIntegrationAuthAwsKmsKeyDTO,
TIntegrationAuthBitbucketEnvironmentsDTO,
TIntegrationAuthBitbucketWorkspaceDTO,
TIntegrationAuthChecklyGroupsDTO,
TIntegrationAuthGithubEnvsDTO,
@ -1261,6 +1263,55 @@ export const integrationAuthServiceFactory = ({
return workspaces;
};
const getBitbucketEnvironments = async ({
workspaceSlug,
repoSlug,
actorId,
actor,
actorOrgId,
actorAuthMethod,
id
}: TIntegrationAuthBitbucketEnvironmentsDTO) => {
const integrationAuth = await integrationAuthDAL.findById(id);
if (!integrationAuth) throw new NotFoundError({ message: `Integration auth with ID '${id}' not found` });
const { permission } = await permissionService.getProjectPermission(
actor,
actorId,
integrationAuth.projectId,
actorAuthMethod,
actorOrgId
);
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.Integrations);
const { shouldUseSecretV2Bridge, botKey } = await projectBotService.getBotKey(integrationAuth.projectId);
const { accessToken } = await getIntegrationAccessToken(integrationAuth, shouldUseSecretV2Bridge, botKey);
const environments: TBitbucketEnvironment[] = [];
let hasNextPage = true;
let environmentsUrl = `${IntegrationUrls.BITBUCKET_API_URL}/2.0/repositories/${workspaceSlug}/${repoSlug}/environments`;
while (hasNextPage) {
// eslint-disable-next-line
const { data }: { data: { values: TBitbucketEnvironment[]; next: string } } = await request.get(environmentsUrl, {
headers: {
Authorization: `Bearer ${accessToken}`,
"Accept-Encoding": "application/json"
}
});
if (data?.values.length > 0) {
environments.push(...data.values);
}
if (data.next) {
environmentsUrl = data.next;
} else {
hasNextPage = false;
}
}
return environments;
};
const getNorthFlankSecretGroups = async ({
id,
actor,
@ -1499,6 +1550,7 @@ export const integrationAuthServiceFactory = ({
getNorthFlankSecretGroups,
getTeamcityBuildConfigs,
getBitbucketWorkspaces,
getBitbucketEnvironments,
getIntegrationAccessToken,
duplicateIntegrationAuth
};

View File

@ -99,6 +99,12 @@ export type TIntegrationAuthBitbucketWorkspaceDTO = {
id: string;
} & Omit<TProjectPermission, "projectId">;
export type TIntegrationAuthBitbucketEnvironmentsDTO = {
workspaceSlug: string;
repoSlug: string;
id: string;
} & Omit<TProjectPermission, "projectId">;
export type TIntegrationAuthNorthflankSecretGroupDTO = {
id: string;
appId: string;
@ -148,6 +154,13 @@ export type TBitbucketWorkspace = {
updated_on: string;
};
export type TBitbucketEnvironment = {
type: string;
uuid: string;
name: string;
slug: string;
};
export type TNorthflankSecretGroup = {
id: string;
name: string;

View File

@ -334,7 +334,7 @@ export const getIntegrationOptions = async () => {
docsLink: ""
},
{
name: "BitBucket",
name: "Bitbucket",
slug: "bitbucket",
image: "BitBucket.png",
isAvailable: true,

View File

@ -2540,9 +2540,9 @@ const syncSecretsAzureDevops = async ({
const { groupId, groupName } = await getEnvGroupId(integration.app, integration.appId, integration.environment.name);
const variables: Record<string, { value: string }> = {};
const variables: Record<string, { value: string; isSecret: boolean }> = {};
for (const key of Object.keys(secrets)) {
variables[key] = { value: secrets[key].value };
variables[key] = { value: secrets[key].value, isSecret: true };
}
if (!groupId) {
@ -3631,7 +3631,14 @@ const syncSecretsBitBucket = async ({
const res: { [key: string]: BitbucketVariable } = {};
let hasNextPage = true;
let variablesUrl = `${IntegrationUrls.BITBUCKET_API_URL}/2.0/repositories/${integration.targetEnvironmentId}/${integration.appId}/pipelines_config/variables`;
const rootUrl = integration.targetServiceId
? // scope: deployment environment
`${IntegrationUrls.BITBUCKET_API_URL}/2.0/repositories/${integration.targetEnvironmentId}/${integration.appId}/deployments_config/environments/${integration.targetServiceId}/variables`
: // scope: repository
`${IntegrationUrls.BITBUCKET_API_URL}/2.0/repositories/${integration.targetEnvironmentId}/${integration.appId}/pipelines_config/variables`;
let variablesUrl = rootUrl;
while (hasNextPage) {
const { data }: { data: VariablesResponse } = await request.get(variablesUrl, {
@ -3658,7 +3665,7 @@ const syncSecretsBitBucket = async ({
if (key in res) {
// update existing secret
await request.put(
`${variablesUrl}/${res[key].uuid}`,
`${rootUrl}/${res[key].uuid}`,
{
key,
value: secrets[key].value,
@ -3674,7 +3681,7 @@ const syncSecretsBitBucket = async ({
} else {
// create new secret
await request.post(
variablesUrl,
rootUrl,
{
key,
value: secrets[key].value,

View File

@ -1,5 +1,7 @@
import { SymmetricEncryption } from "@app/lib/crypto/cipher";
export const KMS_ROOT_CONFIG_UUID = "00000000-0000-0000-0000-000000000000";
export const getByteLengthForAlgorithm = (encryptionAlgorithm: SymmetricEncryption) => {
switch (encryptionAlgorithm) {
case SymmetricEncryption.AES_GCM_128:

View File

@ -2,13 +2,14 @@ import slugify from "@sindresorhus/slugify";
import { Knex } from "knex";
import { z } from "zod";
import { KmsKeysSchema } from "@app/db/schemas";
import { KmsKeysSchema, TKmsRootConfig } from "@app/db/schemas";
import { AwsKmsProviderFactory } from "@app/ee/services/external-kms/providers/aws-kms";
import {
ExternalKmsAwsSchema,
KmsProviders,
TExternalKmsProviderFns
} from "@app/ee/services/external-kms/providers/model";
import { THsmServiceFactory } from "@app/ee/services/hsm/hsm-service";
import { KeyStorePrefixes, TKeyStoreFactory } from "@app/keystore/keystore";
import { getConfig } from "@app/lib/config/env";
import { randomSecureBytes } from "@app/lib/crypto";
@ -17,7 +18,7 @@ import { generateHash } from "@app/lib/crypto/encryption";
import { BadRequestError, ForbiddenRequestError, NotFoundError } from "@app/lib/errors";
import { logger } from "@app/lib/logger";
import { alphaNumericNanoId } from "@app/lib/nanoid";
import { getByteLengthForAlgorithm } from "@app/services/kms/kms-fns";
import { getByteLengthForAlgorithm, KMS_ROOT_CONFIG_UUID } from "@app/services/kms/kms-fns";
import { TOrgDALFactory } from "../org/org-dal";
import { TProjectDALFactory } from "../project/project-dal";
@ -27,6 +28,7 @@ import { TKmsRootConfigDALFactory } from "./kms-root-config-dal";
import {
KmsDataKey,
KmsType,
RootKeyEncryptionStrategy,
TDecryptWithKeyDTO,
TDecryptWithKmsDTO,
TEncryptionWithKeyDTO,
@ -40,15 +42,14 @@ type TKmsServiceFactoryDep = {
kmsDAL: TKmsKeyDALFactory;
projectDAL: Pick<TProjectDALFactory, "findById" | "updateById" | "transaction">;
orgDAL: Pick<TOrgDALFactory, "findById" | "updateById" | "transaction">;
kmsRootConfigDAL: Pick<TKmsRootConfigDALFactory, "findById" | "create">;
kmsRootConfigDAL: Pick<TKmsRootConfigDALFactory, "findById" | "create" | "updateById">;
keyStore: Pick<TKeyStoreFactory, "acquireLock" | "waitTillReady" | "setItemWithExpiry">;
internalKmsDAL: Pick<TInternalKmsDALFactory, "create">;
hsmService: THsmServiceFactory;
};
export type TKmsServiceFactory = ReturnType<typeof kmsServiceFactory>;
const KMS_ROOT_CONFIG_UUID = "00000000-0000-0000-0000-000000000000";
const KMS_ROOT_CREATION_WAIT_KEY = "wait_till_ready_kms_root_key";
const KMS_ROOT_CREATION_WAIT_TIME = 10;
@ -63,7 +64,8 @@ export const kmsServiceFactory = ({
keyStore,
internalKmsDAL,
orgDAL,
projectDAL
projectDAL,
hsmService
}: TKmsServiceFactoryDep) => {
let ROOT_ENCRYPTION_KEY = Buffer.alloc(0);
@ -610,6 +612,65 @@ export const kmsServiceFactory = ({
}
};
const $getBasicEncryptionKey = () => {
const appCfg = getConfig();
const encryptionKey = appCfg.ENCRYPTION_KEY || appCfg.ROOT_ENCRYPTION_KEY;
const isBase64 = !appCfg.ENCRYPTION_KEY;
if (!encryptionKey)
throw new Error(
"Root encryption key not found for KMS service. Did you set the ENCRYPTION_KEY or ROOT_ENCRYPTION_KEY environment variables?"
);
const encryptionKeyBuffer = Buffer.from(encryptionKey, isBase64 ? "base64" : "utf8");
return encryptionKeyBuffer;
};
const $decryptRootKey = async (kmsRootConfig: TKmsRootConfig) => {
// case 1: root key is encrypted with HSM
if (kmsRootConfig.encryptionStrategy === RootKeyEncryptionStrategy.HSM) {
const hsmIsActive = await hsmService.isActive();
if (!hsmIsActive) {
throw new Error("Unable to decrypt root KMS key. HSM service is inactive. Did you configure the HSM?");
}
const decryptedKey = await hsmService.decrypt(kmsRootConfig.encryptedRootKey);
return decryptedKey;
}
// case 2: root key is encrypted with software encryption
if (kmsRootConfig.encryptionStrategy === RootKeyEncryptionStrategy.Software) {
const cipher = symmetricCipherService(SymmetricEncryption.AES_GCM_256);
const encryptionKeyBuffer = $getBasicEncryptionKey();
return cipher.decrypt(kmsRootConfig.encryptedRootKey, encryptionKeyBuffer);
}
throw new Error(`Invalid root key encryption strategy: ${kmsRootConfig.encryptionStrategy}`);
};
const $encryptRootKey = async (plainKeyBuffer: Buffer, strategy: RootKeyEncryptionStrategy) => {
if (strategy === RootKeyEncryptionStrategy.HSM) {
const hsmIsActive = await hsmService.isActive();
if (!hsmIsActive) {
throw new Error("Unable to encrypt root KMS key. HSM service is inactive. Did you configure the HSM?");
}
const encrypted = await hsmService.encrypt(plainKeyBuffer);
return encrypted;
}
if (strategy === RootKeyEncryptionStrategy.Software) {
const cipher = symmetricCipherService(SymmetricEncryption.AES_GCM_256);
const encryptionKeyBuffer = $getBasicEncryptionKey();
return cipher.encrypt(plainKeyBuffer, encryptionKeyBuffer);
}
// eslint-disable-next-line @typescript-eslint/restrict-template-expressions
throw new Error(`Invalid root key encryption strategy: ${strategy}`);
};
// by keeping the decrypted data key in inner scope
// none of the entities outside can interact directly or expose the data key
// NOTICE: If changing here update migrations/utils/kms
@ -771,7 +832,6 @@ export const kmsServiceFactory = ({
},
tx
);
return kmsDAL.findByIdWithAssociatedKms(key.id, tx);
});
@ -794,14 +854,6 @@ export const kmsServiceFactory = ({
// akhilmhdh: a copy of this is made in migrations/utils/kms
const startService = async () => {
const appCfg = getConfig();
// This will switch to a seal process and HMS flow in future
const encryptionKey = appCfg.ENCRYPTION_KEY || appCfg.ROOT_ENCRYPTION_KEY;
// if root key its base64 encoded
const isBase64 = !appCfg.ENCRYPTION_KEY;
if (!encryptionKey) throw new Error("Root encryption key not found for KMS service.");
const encryptionKeyBuffer = Buffer.from(encryptionKey, isBase64 ? "base64" : "utf8");
const lock = await keyStore.acquireLock([`KMS_ROOT_CFG_LOCK`], 3000, { retryCount: 3 }).catch(() => null);
if (!lock) {
await keyStore.waitTillReady({
@ -813,31 +865,69 @@ export const kmsServiceFactory = ({
// check if KMS root key was already generated and saved in DB
const kmsRootConfig = await kmsRootConfigDAL.findById(KMS_ROOT_CONFIG_UUID);
const cipher = symmetricCipherService(SymmetricEncryption.AES_GCM_256);
// case 1: a root key already exists in the DB
if (kmsRootConfig) {
if (lock) await lock.release();
logger.info("KMS: Encrypted ROOT Key found from DB. Decrypting.");
const decryptedRootKey = cipher.decrypt(kmsRootConfig.encryptedRootKey, encryptionKeyBuffer);
// set the flag so that other instancen nodes can start
logger.info(`KMS: Encrypted ROOT Key found from DB. Decrypting. [strategy=${kmsRootConfig.encryptionStrategy}]`);
const decryptedRootKey = await $decryptRootKey(kmsRootConfig);
// set the flag so that other instance nodes can start
await keyStore.setItemWithExpiry(KMS_ROOT_CREATION_WAIT_KEY, KMS_ROOT_CREATION_WAIT_TIME, "true");
logger.info("KMS: Loading ROOT Key into Memory.");
ROOT_ENCRYPTION_KEY = decryptedRootKey;
return;
}
logger.info("KMS: Generating ROOT Key");
// case 2: no config is found, so we create a new root key with basic encryption
logger.info("KMS: Generating new ROOT Key");
const newRootKey = randomSecureBytes(32);
const encryptedRootKey = cipher.encrypt(newRootKey, encryptionKeyBuffer);
// @ts-expect-error id is kept as fixed for idempotence and to avoid race condition
await kmsRootConfigDAL.create({ encryptedRootKey, id: KMS_ROOT_CONFIG_UUID });
const encryptedRootKey = await $encryptRootKey(newRootKey, RootKeyEncryptionStrategy.Software).catch((err) => {
logger.error({ hsmEnabled: hsmService.isActive() }, "KMS: Failed to encrypt ROOT Key");
throw err;
});
// set the flag so that other instancen nodes can start
await kmsRootConfigDAL.create({
// @ts-expect-error id is kept as fixed for idempotence and to avoid race condition
id: KMS_ROOT_CONFIG_UUID,
encryptedRootKey,
encryptionStrategy: RootKeyEncryptionStrategy.Software
});
// set the flag so that other instance nodes can start
await keyStore.setItemWithExpiry(KMS_ROOT_CREATION_WAIT_KEY, KMS_ROOT_CREATION_WAIT_TIME, "true");
logger.info("KMS: Saved and loaded ROOT Key into memory");
if (lock) await lock.release();
ROOT_ENCRYPTION_KEY = newRootKey;
};
const updateEncryptionStrategy = async (strategy: RootKeyEncryptionStrategy) => {
const kmsRootConfig = await kmsRootConfigDAL.findById(KMS_ROOT_CONFIG_UUID);
if (!kmsRootConfig) {
throw new NotFoundError({ message: "KMS root config not found" });
}
if (kmsRootConfig.encryptionStrategy === strategy) {
return;
}
const decryptedRootKey = await $decryptRootKey(kmsRootConfig);
const encryptedRootKey = await $encryptRootKey(decryptedRootKey, strategy);
if (!encryptedRootKey) {
logger.error("KMS: Failed to re-encrypt ROOT Key with selected strategy");
throw new Error("Failed to re-encrypt ROOT Key with selected strategy");
}
await kmsRootConfigDAL.updateById(KMS_ROOT_CONFIG_UUID, {
encryptedRootKey,
encryptionStrategy: strategy
});
ROOT_ENCRYPTION_KEY = decryptedRootKey;
};
return {
startService,
generateKmsKey,
@ -849,6 +939,7 @@ export const kmsServiceFactory = ({
encryptWithRootKey,
decryptWithRootKey,
getOrgKmsKeyId,
updateEncryptionStrategy,
getProjectSecretManagerKmsKeyId,
updateProjectSecretManagerKmsKey,
getProjectKeyBackup,

View File

@ -56,3 +56,8 @@ export type TUpdateProjectSecretManagerKmsKeyDTO = {
projectId: string;
kms: { type: KmsType.Internal } | { type: KmsType.External; kmsId: string };
};
export enum RootKeyEncryptionStrategy {
Software = "SOFTWARE",
HSM = "HSM"
}

View File

@ -291,7 +291,7 @@ export const orgServiceFactory = ({
}
if (authEnforced !== undefined) {
if (!plan?.samlSSO || !plan.oidcSSO)
if (!plan?.samlSSO && !plan.oidcSSO)
throw new BadRequestError({
message: "Failed to enforce/un-enforce SSO due to plan restriction. Upgrade plan to enforce/un-enforce SSO."
});

View File

@ -6,6 +6,8 @@ import { TLicenseServiceFactory } from "@app/ee/services/license/license-service
import { OrgPermissionActions, OrgPermissionSubjects } from "@app/ee/services/permission/org-permission";
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service";
import { ProjectPermissionActions, ProjectPermissionSub } from "@app/ee/services/permission/project-permission";
import { TProjectTemplateServiceFactory } from "@app/ee/services/project-template/project-template-service";
import { InfisicalProjectTemplate } from "@app/ee/services/project-template/project-template-types";
import { TKeyStoreFactory } from "@app/keystore/keystore";
import { isAtLeastAsPrivileged } from "@app/lib/casl";
import { infisicalSymmetricEncypt } from "@app/lib/crypto/encryption";
@ -94,7 +96,7 @@ type TProjectServiceFactoryDep = {
orgDAL: Pick<TOrgDALFactory, "findOne">;
keyStore: Pick<TKeyStoreFactory, "deleteItem">;
projectBotDAL: Pick<TProjectBotDALFactory, "create">;
projectRoleDAL: Pick<TProjectRoleDALFactory, "find">;
projectRoleDAL: Pick<TProjectRoleDALFactory, "find" | "insertMany">;
kmsService: Pick<
TKmsServiceFactory,
| "updateProjectSecretManagerKmsKey"
@ -104,6 +106,7 @@ type TProjectServiceFactoryDep = {
| "getProjectSecretManagerKmsKeyId"
| "deleteInternalKms"
>;
projectTemplateService: TProjectTemplateServiceFactory;
};
export type TProjectServiceFactory = ReturnType<typeof projectServiceFactory>;
@ -134,7 +137,8 @@ export const projectServiceFactory = ({
kmsService,
projectBotDAL,
projectSlackConfigDAL,
slackIntegrationDAL
slackIntegrationDAL,
projectTemplateService
}: TProjectServiceFactoryDep) => {
/*
* Create workspace. Make user the admin
@ -148,7 +152,8 @@ export const projectServiceFactory = ({
slug: projectSlug,
kmsKeyId,
tx: trx,
createDefaultEnvs = true
createDefaultEnvs = true,
template = InfisicalProjectTemplate.Default
}: TCreateProjectDTO) => {
const organization = await orgDAL.findOne({ id: actorOrgId });
@ -183,6 +188,21 @@ export const projectServiceFactory = ({
}
}
let projectTemplate: Awaited<ReturnType<typeof projectTemplateService.findProjectTemplateByName>> | null = null;
switch (template) {
case InfisicalProjectTemplate.Default:
projectTemplate = null;
break;
default:
projectTemplate = await projectTemplateService.findProjectTemplateByName(template, {
id: actorId,
orgId: organization.id,
type: actor,
authMethod: actorAuthMethod
});
}
const project = await projectDAL.create(
{
name: workspaceName,
@ -210,7 +230,24 @@ export const projectServiceFactory = ({
// set default environments and root folder for provided environments
let envs: TProjectEnvironments[] = [];
if (createDefaultEnvs) {
if (projectTemplate) {
envs = await projectEnvDAL.insertMany(
projectTemplate.environments.map((env) => ({ ...env, projectId: project.id })),
tx
);
await folderDAL.insertMany(
envs.map(({ id }) => ({ name: ROOT_FOLDER_NAME, envId: id, version: 1 })),
tx
);
await projectRoleDAL.insertMany(
projectTemplate.packedRoles.map((role) => ({
...role,
permissions: JSON.stringify(role.permissions),
projectId: project.id
})),
tx
);
} else if (createDefaultEnvs) {
envs = await projectEnvDAL.insertMany(
DEFAULT_PROJECT_ENVS.map((el, i) => ({ ...el, projectId: project.id, position: i + 1 })),
tx

View File

@ -32,6 +32,7 @@ export type TCreateProjectDTO = {
slug?: string;
kmsKeyId?: string;
createDefaultEnvs?: boolean;
template?: string;
tx?: Knex;
};

View File

@ -6,8 +6,11 @@ import { BadRequestError, DatabaseError } from "@app/lib/errors";
import { groupBy, removeTrailingSlash } from "@app/lib/fn";
import { ormify, selectAllTableCols } from "@app/lib/knex";
import { OrderByDirection } from "@app/lib/types";
import { isValidSecretPath } from "@app/lib/validator";
import { SecretsOrderBy } from "@app/services/secret/secret-types";
import { TFindFoldersDeepByParentIdsDTO } from "./secret-folder-types";
export const validateFolderName = (folderName: string) => {
const validNameRegex = /^[a-zA-Z0-9-_]+$/;
return validNameRegex.test(folderName);
@ -212,6 +215,12 @@ export const secretFolderDALFactory = (db: TDbClient) => {
const secretFolderOrm = ormify(db, TableName.SecretFolder);
const findBySecretPath = async (projectId: string, environment: string, path: string, tx?: Knex) => {
const isValidPath = isValidSecretPath(path);
if (!isValidPath)
throw new BadRequestError({
message: "Invalid secret path. Only alphanumeric characters, dashes, and underscores are allowed."
});
try {
const folder = await sqlFindFolderByPathQuery(
tx || db.replicaNode(),
@ -234,6 +243,12 @@ export const secretFolderDALFactory = (db: TDbClient) => {
// finds folders by path for multiple envs
const findBySecretPathMultiEnv = async (projectId: string, environments: string[], path: string, tx?: Knex) => {
const isValidPath = isValidSecretPath(path);
if (!isValidPath)
throw new BadRequestError({
message: "Invalid secret path. Only alphanumeric characters, dashes, and underscores are allowed."
});
try {
const pathDepth = removeTrailingSlash(path).split("/").filter(Boolean).length + 1;
@ -265,6 +280,12 @@ export const secretFolderDALFactory = (db: TDbClient) => {
// even if its the original given /path1/path2
// it will stop automatically at /path2
const findClosestFolder = async (projectId: string, environment: string, path: string, tx?: Knex) => {
const isValidPath = isValidSecretPath(path);
if (!isValidPath)
throw new BadRequestError({
message: "Invalid secret path. Only alphanumeric characters, dashes, and underscores are allowed."
});
try {
const folder = await sqlFindFolderByPathQuery(
tx || db.replicaNode(),
@ -444,6 +465,48 @@ export const secretFolderDALFactory = (db: TDbClient) => {
}
};
const findByEnvsDeep = async ({ parentIds }: TFindFoldersDeepByParentIdsDTO, tx?: Knex) => {
try {
const folders = await (tx || db.replicaNode())
.withRecursive("parents", (qb) =>
qb
.select(
selectAllTableCols(TableName.SecretFolder),
db.raw("0 as depth"),
db.raw(`'/' as path`),
db.ref(`${TableName.Environment}.slug`).as("environment")
)
.from(TableName.SecretFolder)
.join(TableName.Environment, `${TableName.SecretFolder}.envId`, `${TableName.Environment}.id`)
.whereIn(`${TableName.SecretFolder}.id`, parentIds)
.union((un) => {
void un
.select(
selectAllTableCols(TableName.SecretFolder),
db.raw("parents.depth + 1 as depth"),
db.raw(
`CONCAT(
CASE WHEN parents.path = '/' THEN '' ELSE parents.path END,
CASE WHEN ${TableName.SecretFolder}."parentId" is NULL THEN '' ELSE CONCAT('/', secret_folders.name) END
)`
),
db.ref("parents.environment")
)
.from(TableName.SecretFolder)
.join("parents", `${TableName.SecretFolder}.parentId`, "parents.id");
})
)
.select<(TSecretFolders & { path: string; depth: number; environment: string })[]>("*")
.from("parents")
.orderBy("depth")
.orderBy(`name`);
return folders;
} catch (error) {
throw new DatabaseError({ error, name: "FindByEnvsDeep" });
}
};
return {
...secretFolderOrm,
update,
@ -454,6 +517,7 @@ export const secretFolderDALFactory = (db: TDbClient) => {
findSecretPathByFolderIds,
findClosestFolder,
findByProjectId,
findByMultiEnv
findByMultiEnv,
findByEnvsDeep
};
};

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