1
0
mirror of https://github.com/Infisical/infisical.git synced 2025-03-18 08:25:31 +00:00

Compare commits

..

237 Commits

Author SHA1 Message Date
a536b614f9 add direct link td provider 2024-11-22 17:01:24 +01:00
7ed9257b5e misc: made identity metadata value not nullable 2024-11-22 17:01:23 +01:00
8a86ce13a5 replace loader with spinner 2024-11-22 17:01:23 +01:00
b45babf50a minor text revisions/additions and add colors/icons to totp token expiry countdown 2024-11-22 17:01:23 +01:00
b836d2aa10 misc: add handling for lease regen 2024-11-22 17:01:23 +01:00
8192903911 misc: added timer 2024-11-22 17:01:23 +01:00
4ce370614f misc: addressed review comments 2024-11-22 17:01:23 +01:00
f8c881f753 feat: added support for configuring totp with secret key 2024-11-22 17:01:23 +01:00
32dff81055 docs: dynamic secret doc typos addressed 2024-11-22 17:01:22 +01:00
6e644baa8e misc: addressed comments 2024-11-22 17:01:22 +01:00
51e2ef6ca7 misc: added URL string validation 2024-11-22 17:01:22 +01:00
05cc15d792 doc: added docs for totp dynamic secret 2024-11-22 17:01:21 +01:00
b125401850 misc: finalized option setting logic 2024-11-22 17:01:21 +01:00
f713c20595 feat: totp dynamic secret 2024-11-22 17:01:21 +01:00
d674903bb1 fixing typo in docs/integrations/platforms/kubernetes 2024-11-22 17:01:21 +01:00
6f26295bb9 reverse license 2024-11-22 17:01:21 +01:00
e16df4d559 improvements: minor UI/labeling adjustments, only show tags loading if can read, and remove rounded bottom on overview table 2024-11-22 17:01:21 +01:00
=
a7bc12d790 feat: resolve dependency cycle error 2024-11-22 17:01:20 +01:00
=
340ab76aea feat: added action button for notification toast and one action each for forbidden error and validation error details 2024-11-22 17:01:20 +01:00
=
90b2a20669 feat: modified error handler to return possible rules for a validation failed rules 2024-11-22 17:01:20 +01:00
1d54288193 Update README.md 2024-11-22 17:01:20 +01:00
b3bbe74414 requested changes 2024-11-22 17:01:20 +01:00
aa1bdb924e Update README.md 2024-11-22 17:01:19 +01:00
0de6add3f7 set all new and existing secrets to be sensitive: true 2024-11-20 17:28:09 +01:00
35fbd5d49d Merge pull request from Infisical/daniel/pre-commit-cli-check
chore: check for CLI installation before pre-commit
2024-11-20 19:01:54 +04:00
d03b453e3d Merge pull request from Infisical/daniel/actor-id-mismatch
fix(audit-logs): actor / actor ID mismatch
2024-11-20 18:58:33 +04:00
96e331b678 fix(audit-logs): actor / actor ID mismatch 2024-11-20 18:50:29 +04:00
d4d468660d chore: check for CLI installation before pre-commit 2024-11-20 17:29:36 +04:00
9a62efea4f Merge pull request from Infisical/docs-update-note
update docs note
2024-11-19 23:51:44 +04:00
506c30bcdb update docs note 2024-11-19 14:47:39 -05:00
735ad4ff65 Merge pull request from Infisical/misc/metrics-observability
feat: added setup for production observability (metrics via OTEL)
2024-11-19 13:56:29 -05:00
41e36dfcef misc: updated service name 2024-11-20 02:34:46 +08:00
421d8578b7 Merge pull request from Infisical/daniel/access-token-cleanup
fix(identity): remove access tokens when auth method is removed
2024-11-19 22:31:51 +04:00
6685f8aa0a fix(identity): remove access tokens when auth method is removed 2024-11-19 22:24:17 +04:00
d6c37c1065 misc: added metrics setup to self-host docs 2024-11-20 01:01:41 +08:00
54f3f94185 Merge pull request from phamleduy04/sort-repo-github-intergration-app
Add sort to Github integration dropdown box
2024-11-19 11:46:43 -05:00
907537f7c0 Merge pull request from Infisical/empty-secret-value-fixes
Fix: Handle Empty Secret Values in Update, Bulk Create and Bulk Update Secret(s)
2024-11-19 08:45:38 -08:00
61263b9384 fix: unhandle empty value in bulk create/insert secrets 2024-11-19 08:30:58 -08:00
d71c85e052 misc: finalized config files 2024-11-20 00:28:27 +08:00
b6d8be2105 fix: handle empty string to allow clearing secret on update 2024-11-19 08:16:30 -08:00
0693f81d0a misc: finalized instrumentation setup 2024-11-19 23:25:20 +08:00
61d516ef35 Merge pull request from Infisical/daniel/azure-auth-better-error 2024-11-19 09:00:23 -05:00
31fc64fb4c Update identity-azure-auth-service.ts 2024-11-19 17:54:31 +04:00
8bf7e4c4d1 Merge pull request from akhilmhdh/fix/auth-method-migration
fix: migration in loop due to cornercase
2024-11-18 16:01:04 -05:00
=
2027d4b44e feat: moved auth method deletion to top 2024-11-19 02:17:25 +05:30
d401c9074e Merge pull request from Infisical/misc/finalize-org-migration-script
misc: finalize org migration script
2024-11-18 14:15:20 -05:00
afe35dbbb5 Merge pull request from Infisical/misc/finalized-design-of-totp-registration
misc: finalized design of totp registration
2024-11-19 02:13:54 +08:00
6ff1602fd5 Merge pull request from Infisical/misc/oidc-setup-extra-handling
misc: added OIDC error and edge-case handling
2024-11-18 10:56:09 -05:00
6603364749 Merge pull request from Infisical/daniel/migrate-unlock-command
fix: add migration unlock command
2024-11-18 10:28:43 -05:00
53bea22b85 fix: added unlock command 2024-11-18 19:22:43 +04:00
7c84adc1c2 misc: added new package to lock 2024-11-18 23:04:01 +08:00
fa8d6735a1 misc: reverted package lock 2024-11-18 23:00:55 +08:00
a6137f267d Merge remote-tracking branch 'origin/main' into misc/metrics-observability 2024-11-18 22:54:14 +08:00
d521ee7b7e Merge pull request from Infisical/misc/address-role-slugs-issue-invite-user-endpoint
misc: address role slug issue in invite user endpoint
2024-11-18 21:58:31 +08:00
827931e416 misc: addressed comment 2024-11-18 21:52:36 +08:00
faa83344a7 misc: address role slug issue in invite user endpoint 2024-11-18 21:43:06 +08:00
089a7e880b misc: added message for bypass 2024-11-18 17:29:01 +08:00
64ec741f1a misc: updated documentation totp ui 2024-11-18 17:24:03 +08:00
c98233ddaf misc: finalized design of totp registration 2024-11-18 17:14:21 +08:00
ae17981c41 Merge pull request from Infisical/vmatsiiako-changelog-patch-1
added handbook updates
2024-11-17 23:44:49 -05:00
6c49c7da3c added handbook updates 2024-11-17 23:43:57 -05:00
2de04b6fe5 Merge pull request from Infisical/vmatsiiako-docs-patch-1-1
Fix typo in docs
2024-11-17 23:01:15 -05:00
5c9ec1e4be Fix typo in docs 2024-11-17 09:55:32 -05:00
ba89491d4c Merge pull request from Infisical/feat/totp-authenticator
feat: TOTP authenticator
2024-11-16 11:58:39 +08:00
483e596a7a Merge pull request from Infisical/daniel/npm-cli-windows-fix
fix: NPM-based CLI windows symlink
2024-11-15 15:37:32 -07:00
65f122bd41 Update index.cjs 2024-11-16 01:37:43 +04:00
682b552fdc misc: addressed remaining comments 2024-11-16 03:15:39 +08:00
=
d4cfd0b6ed fix: migration in loop due to cornercase 2024-11-16 00:37:57 +05:30
e8f09d2c7b fix(ui): add sort to github integration dropdown box 2024-11-15 10:26:38 -06:00
774371a218 misc: added mention of authenticator in the docs 2024-11-16 00:10:56 +08:00
c4b54de303 misc: migrated to switch component 2024-11-15 23:49:20 +08:00
433971a72d misc: addressed comments 1 2024-11-15 23:25:32 +08:00
4acf9413f0 Merge pull request from Infisical/backfill-identity-metadata
Fix: Handle Missing User/Identity Metadata Keys in Permissions Check
2024-11-15 01:34:45 -07:00
f0549cab98 Merge pull request from Infisical/fix-ca-alert-migrations
only create triggers when create new table
2024-11-15 00:56:39 -07:00
d75e49dce5 update trigegr to only create if it doesn't exit 2024-11-15 00:52:08 -07:00
8819abd710 only create triggers when create new table 2024-11-15 00:42:30 -07:00
796f76da46 Merge pull request from Infisical/fix-cert-migration
Fix ca version migration
2024-11-14 23:20:09 -07:00
d6e1ed4d1e revert docker compose changes 2024-11-14 23:10:54 -07:00
1295b68d80 Fix ca version migration
We didn't do a check to see if the column already exists. Because of this, we get this error during migrations:

```
| migration file "20240802181855_ca-cert-version.ts" failed
infisical-db-migration  | migration failed with error: alter table "certificates" add column "caCertId" uuid null - column "caCertId" of relation "certificates" already exists
```
2024-11-14 23:07:30 -07:00
c79f84c064 fix: use proxy on metadata permissions check to handle missing keys 2024-11-14 11:36:07 -08:00
d0c50960ef Merge pull request from Infisical/doc/add-gitlab-oidc-auth-documentation
doc: add docs for gitlab oidc auth
2024-11-14 10:44:01 -07:00
85089a08e1 Merge pull request from Infisical/misc/update-login-self-hosting-label
misc: updated login self-hosting label to include dedicated
2024-11-15 01:41:45 +08:00
bf97294dad misc: added idp label 2024-11-15 01:41:20 +08:00
4053078d95 misc: updated login self-hosting label for dedicated 2024-11-15 01:36:33 +08:00
4ba3899861 doc: add docs for gitlab oidc auth 2024-11-15 01:07:36 +08:00
6bae3628c0 misc: readded saml email error 2024-11-14 19:37:13 +08:00
4cb935dae7 misc: addressed signupinvite issue 2024-11-14 19:10:21 +08:00
ccad684ab2 Merge pull request from Infisical/docs-for-linux-ha
linux HA reference architecture
2024-11-14 02:04:13 -07:00
fd77708cad add docs for linux ha 2024-11-14 02:02:23 -07:00
9aebd712d1 Merge pull request from Infisical/daniel/npm-cli-fixes
fix: cli npm release windows and symlink bugs
2024-11-13 20:58:22 -07:00
05f07b25ac fix: cli npm release windows and symlink bugs 2024-11-14 06:13:14 +04:00
5b0dbf04b2 misc: minor ui 2024-11-14 03:22:02 +08:00
b050db84ab feat: added totp support for cli 2024-11-14 02:27:33 +08:00
8fef6911f1 misc: addressed lint 2024-11-14 01:25:23 +08:00
44ba31a743 misc: added org mfa settings update and other fixes 2024-11-14 01:16:15 +08:00
6bdbac4750 feat: initial implementation for totp authenticator 2024-11-14 00:07:35 +08:00
60fb195706 Merge pull request from Infisical/scott/paste-secrets
Feat: Paste Secrets for Upload
2024-11-12 17:57:13 -08:00
c8109b4e84 improvement: add example paste value formats 2024-11-12 16:46:35 -08:00
1f2b0443cc improvement: address requested changes 2024-11-12 16:11:47 -08:00
dd1cabf9f6 Merge pull request from Infisical/daniel/fix-npm-cli-symlink
fix: npm cli symlink
2024-11-12 22:47:01 +04:00
8b781b925a fix: npm cli symlink 2024-11-12 22:45:37 +04:00
ddcf5b576b improvement: improve field error message 2024-11-12 10:25:23 -08:00
7138b392f2 Feature: add ability to paste .env, .yml or .json secrets for upload and also fix upload when keys conflict but are not on current page 2024-11-12 10:21:07 -08:00
bfce1021fb Merge pull request from G3root/infisical-npm
feat: infisical cli for npm
2024-11-12 21:48:47 +04:00
93c0313b28 docs: added NPM install option 2024-11-12 21:48:04 +04:00
8cfc217519 Update README.md 2024-11-12 21:38:34 +04:00
d272c6217a Merge pull request from Infisical/scott/secret-refrence-fixes
Fix: Secret Reference Multiple References and Special Character Stripping
2024-11-12 22:49:18 +05:30
2fe2ddd9fc Update package.json 2024-11-12 21:17:53 +04:00
e330ddd5ee fix: remove dry run 2024-11-12 20:56:18 +04:00
7aba9c1a50 Update index.cjs 2024-11-12 20:54:55 +04:00
4cd8e0fa67 fix: workflow fixes 2024-11-12 20:47:10 +04:00
ea3d164ead Update release_build_infisical_cli.yml 2024-11-12 20:40:45 +04:00
df468e4865 Update release_build_infisical_cli.yml 2024-11-12 20:39:16 +04:00
66e96018c4 Update release_build_infisical_cli.yml 2024-11-12 20:37:28 +04:00
3b02eedca6 feat: npm CLI 2024-11-12 20:36:09 +04:00
a55fe2b788 chore: add git ignore 2024-11-12 17:40:46 +04:00
5d7a267f1d chore: add package.json 2024-11-12 17:40:37 +04:00
b16ab6f763 feat: add script 2024-11-12 17:40:37 +04:00
2d2ad0724f Merge pull request 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 from Infisical/daniel/hsm-docs
docs: hardware security module
2024-11-11 16:21:56 -07:00
17d5e4bdab Merge pull request 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
334a728259 chore: remove console log 2024-11-11 14:06:12 -08:00
4a3143e689 fix: correct unique secret check to account for env and path 2024-11-11 14:04:36 -08:00
14810de054 fix: correct secret reference value replacement to support special characters 2024-11-11 13:46:39 -08:00
8cfcbaa12c fix: correct secret reference validation check to permit referencing the same secret multiple times and improve error message 2024-11-11 13:17:25 -08:00
0e946f73bd Merge pull request 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 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 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 from scott-ray-wilson/project-templates-feature
Feature: Project Templates
2024-11-10 21:03:11 -07:00
f9f6ec0a8d Merge pull request 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 from Infisical/feat/ldap-static-dynamic-secret
feat: static ldap credentials
2024-11-10 13:01:56 +08:00
ada63b9e7d misc: finalize org migration script 2024-11-10 11:49:25 +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 from felixhummel/main
docs: fix link to cli
2024-11-09 09:01:08 -05:00
8bc538af93 Merge pull request 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
3f6a0c77f1 misc: finalized user messages 2024-11-09 01:51:11 +08:00
f91f9c9487 Merge pull request 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
9e4b66e215 misc: made users automatically verified 2024-11-09 00:38:45 +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
8a14914bc3 misc: added more error handling 2024-11-08 21:43:25 +08:00
bf09fa33fa Merge pull request 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 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 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 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 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 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
c6fa647825 Merge pull request 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 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 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 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
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
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
fc3a409164 misc: added support for more config options 2024-06-12 01:39:06 +08:00
ffc58b0313 Merge remote-tracking branch 'origin/main' into misc/metrics-observability 2024-06-11 23:50:08 +08:00
9a7e05369c misc: added env-based flag for enabling telemetry 2024-06-06 00:56:11 +08:00
33b49f4466 misc: finalized config files 2024-06-06 00:42:24 +08:00
60895537a7 misc: initial working setup for metrics observabilit 2024-06-05 21:46:10 +08:00
337 changed files with 13934 additions and 3043 deletions
.env.example
.github/workflows
.gitignore
.husky
.infisicalignoreDockerfile.fips.standalone-infisicalDockerfile.standalone-infisicalMakefile
backend
DockerfileDockerfile.dev
e2e-test
package-lock.jsonpackage.json
scripts
src
@types
db
ee
keystore
lib
main.ts
server
services
cli/packages
company
docker-compose.dev.ymldocker-compose.prod.yml
docs
frontend
package-lock.jsonpackage.json
public
data
images/secretRotation
src
components
context
OrgPermissionContext
ProjectPermissionContext
helpers
hooks/api
layouts/AppLayout
lib/schemas
pages
integrations
aws-secret-manager
bitbucket
login
org/[id]/overview
signupinvite.tsx
reactQuery.tsx
styles
views
IntegrationsPage
IntegrationDetailsPage/components
components/IntegrationsSection
Login
Mfa.tsx
components/PasswordStep
Org
Project
CertificatesPage/components/CertificatesTab/components
MembersPage/components/MembersTab/components
RolePage/components/RolePermissionsSection/components
SecretApprovalPage/components/SecretApprovalRequest/components
SecretMainPage
SecretOverviewPage
SecretOverviewPage.tsx
components
CreateSecretForm
SecretSearchInput/components
Settings
Signup/components/UserInfoSSOStep
admin/DashboardPage
helm-charts/infisical-standalone-postgres
npm
otel-collector-config.yamlpackage-lock.jsonpackage.jsonprometheus.dev.yml

@ -74,7 +74,17 @@ CAPTCHA_SECRET=
NEXT_PUBLIC_CAPTCHA_SITE_KEY=
OTEL_TELEMETRY_COLLECTION_ENABLED=
OTEL_EXPORT_TYPE=
OTEL_EXPORT_OTLP_ENDPOINT=
OTEL_OTLP_PUSH_INTERVAL=
OTEL_COLLECTOR_BASIC_AUTH_USERNAME=
OTEL_COLLECTOR_BASIC_AUTH_PASSWORD=
PLAIN_API_KEY=
PLAIN_WISH_LABEL_IDS=
SSL_CLIENT_CERTIFICATE_HEADER_KEY=
ENABLE_MSSQL_SECRET_ROTATION_ENCRYPT=true

@ -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 }}

@ -10,8 +10,7 @@ on:
permissions:
contents: write
# packages: write
# issues: write
jobs:
cli-integration-tests:
name: Run tests before deployment
@ -26,6 +25,63 @@ jobs:
CLI_TESTS_USER_PASSWORD: ${{ secrets.CLI_TESTS_USER_PASSWORD }}
CLI_TESTS_INFISICAL_VAULT_FILE_PASSPHRASE: ${{ secrets.CLI_TESTS_INFISICAL_VAULT_FILE_PASSPHRASE }}
npm-release:
runs-on: ubuntu-20.04
env:
working-directory: ./npm
needs:
- cli-integration-tests
- goreleaser
steps:
- uses: actions/checkout@v3
with:
fetch-depth: 0
- name: Extract version
run: |
VERSION=$(echo ${{ github.ref_name }} | sed 's/infisical-cli\/v//')
echo "Version extracted: $VERSION"
echo "CLI_VERSION=$VERSION" >> $GITHUB_ENV
- name: Print version
run: echo ${{ env.CLI_VERSION }}
- name: Setup Node
uses: actions/setup-node@8f152de45cc393bb48ce5d89d36b731f54556e65 # v4.0.0
with:
node-version: 20
cache: "npm"
cache-dependency-path: ./npm/package-lock.json
- name: Install dependencies
working-directory: ${{ env.working-directory }}
run: npm install --ignore-scripts
- name: Set NPM version
working-directory: ${{ env.working-directory }}
run: npm version ${{ env.CLI_VERSION }} --allow-same-version --no-git-tag-version
- name: Setup NPM
working-directory: ${{ env.working-directory }}
run: |
echo 'registry="https://registry.npmjs.org/"' > ./.npmrc
echo "//registry.npmjs.org/:_authToken=$NPM_TOKEN" >> ./.npmrc
echo 'registry="https://registry.npmjs.org/"' > ~/.npmrc
echo "//registry.npmjs.org/:_authToken=$NPM_TOKEN" >> ~/.npmrc
env:
NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
- name: Pack NPM
working-directory: ${{ env.working-directory }}
run: npm pack
- name: Publish NPM
working-directory: ${{ env.working-directory }}
run: npm publish --tarball=./infisical-sdk-${{github.ref_name}} --access public --registry=https://registry.npmjs.org/
env:
NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
goreleaser:
runs-on: ubuntu-20.04
needs: [cli-integration-tests]

2
.gitignore vendored

@ -71,3 +71,5 @@ frontend-build
cli/infisical-merge
cli/test/infisical-merge
/backend/binary
/npm/bin

@ -1,6 +1,12 @@
#!/usr/bin/env sh
. "$(dirname -- "$0")/_/husky.sh"
# Check if infisical is installed
if ! command -v infisical >/dev/null 2>&1; then
echo "\nError: Infisical CLI is not installed. Please install the Infisical CLI before comitting.\n You can refer to the documentation at https://infisical.com/docs/cli/overview\n\n"
exit 1
fi
npx lint-staged
infisical scan git-changes --staged -v

@ -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

@ -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"]

@ -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

@ -10,6 +10,9 @@ up-dev:
up-dev-ldap:
docker compose -f docker-compose.dev.yml --profile ldap up --build
up-dev-metrics:
docker compose -f docker-compose.dev.yml --profile metrics up --build
up-prod:
docker-compose -f docker-compose.prod.yml up --build
@ -27,4 +30,3 @@ reviewable-api:
npm run type:check
reviewable: reviewable-ui reviewable-api

@ -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 .

@ -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

@ -5,6 +5,9 @@ export const mockSmtpServer = (): TSmtpService => {
return {
sendMail: async (data) => {
storage.push(data);
},
verify: async () => {
return true;
}
};
};

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

@ -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

1701
backend/package-lock.json generated

File diff suppressed because it is too large Load Diff

@ -50,6 +50,7 @@
"auditlog-migration:down": "knex --knexfile ./src/db/auditlog-knexfile.ts --client pg migrate:down",
"auditlog-migration:list": "knex --knexfile ./src/db/auditlog-knexfile.ts --client pg migrate:list",
"auditlog-migration:status": "knex --knexfile ./src/db/auditlog-knexfile.ts --client pg migrate:status",
"auditlog-migration:unlock": "knex --knexfile ./src/db/auditlog-knexfile.ts migrate:unlock",
"auditlog-migration:rollback": "knex --knexfile ./src/db/auditlog-knexfile.ts migrate:rollback",
"migration:new": "tsx ./scripts/create-migration.ts",
"migration:up": "npm run auditlog-migration:up && knex --knexfile ./src/db/knexfile.ts --client pg migrate:up",
@ -58,6 +59,7 @@
"migration:latest": "npm run auditlog-migration:latest && knex --knexfile ./src/db/knexfile.ts --client pg migrate:latest",
"migration:status": "npm run auditlog-migration:status && knex --knexfile ./src/db/knexfile.ts --client pg migrate:status",
"migration:rollback": "npm run auditlog-migration:rollback && knex --knexfile ./src/db/knexfile.ts migrate:rollback",
"migration:unlock": "npm run auditlog-migration:unlock && knex --knexfile ./src/db/knexfile.ts migrate:unlock",
"migrate:org": "tsx ./scripts/migrate-organization.ts",
"seed:new": "tsx ./scripts/create-seed-file.ts",
"seed": "knex --knexfile ./src/db/knexfile.ts --client pg seed:run",
@ -84,6 +86,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",
@ -137,6 +140,14 @@
"@octokit/plugin-retry": "^5.0.5",
"@octokit/rest": "^20.0.2",
"@octokit/webhooks-types": "^7.3.1",
"@opentelemetry/api": "^1.9.0",
"@opentelemetry/auto-instrumentations-node": "^0.53.0",
"@opentelemetry/exporter-metrics-otlp-proto": "^0.55.0",
"@opentelemetry/exporter-prometheus": "^0.55.0",
"@opentelemetry/instrumentation": "^0.55.0",
"@opentelemetry/resources": "^1.28.0",
"@opentelemetry/sdk-metrics": "^1.28.0",
"@opentelemetry/semantic-conventions": "^1.27.0",
"@peculiar/asn1-schema": "^2.3.8",
"@peculiar/x509": "^1.12.1",
"@serdnam/pino-cloudwatch-transport": "^1.0.4",
@ -180,6 +191,7 @@
"openid-client": "^5.6.5",
"ora": "^7.0.1",
"oracledb": "^6.4.0",
"otplib": "^12.0.1",
"passport-github": "^1.1.0",
"passport-gitlab2": "^5.0.0",
"passport-google-oauth20": "^2.0.0",
@ -188,6 +200,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",

@ -8,61 +8,80 @@ const prompt = promptSync({
sigint: true
});
const sanitizeInputParam = (value: string) => {
// Escape double quotes and wrap the entire value in double quotes
if (value) {
return `"${value.replace(/"/g, '\\"')}"`;
}
return '""';
};
const exportDb = () => {
const exportHost = prompt("Enter your Postgres Host to migrate from: ");
const exportPort = prompt("Enter your Postgres Port to migrate from [Default = 5432]: ") ?? "5432";
const exportUser = prompt("Enter your Postgres User to migrate from: [Default = infisical]: ") ?? "infisical";
const exportPassword = prompt("Enter your Postgres Password to migrate from: ");
const exportDatabase = prompt("Enter your Postgres Database to migrate from [Default = infisical]: ") ?? "infisical";
const exportHost = sanitizeInputParam(prompt("Enter your Postgres Host to migrate from: "));
const exportPort = sanitizeInputParam(
prompt("Enter your Postgres Port to migrate from [Default = 5432]: ") ?? "5432"
);
const exportUser = sanitizeInputParam(
prompt("Enter your Postgres User to migrate from: [Default = infisical]: ") ?? "infisical"
);
const exportPassword = sanitizeInputParam(prompt("Enter your Postgres Password to migrate from: "));
const exportDatabase = sanitizeInputParam(
prompt("Enter your Postgres Database to migrate from [Default = infisical]: ") ?? "infisical"
);
// we do not include the audit_log and secret_sharing entries
execSync(
`PGDATABASE="${exportDatabase}" PGPASSWORD="${exportPassword}" PGHOST="${exportHost}" PGPORT=${exportPort} PGUSER=${exportUser} pg_dump infisical --exclude-table-data="secret_sharing" --exclude-table-data="audit_log*" > ${path.join(
`PGDATABASE=${exportDatabase} PGPASSWORD=${exportPassword} PGHOST=${exportHost} PGPORT=${exportPort} PGUSER=${exportUser} pg_dump -Fc infisical --exclude-table-data="secret_sharing" --exclude-table-data="audit_log*" > ${path.join(
__dirname,
"../src/db/dump.sql"
"../src/db/backup.dump"
)}`,
{ stdio: "inherit" }
);
};
const importDbForOrg = () => {
const importHost = prompt("Enter your Postgres Host to migrate to: ");
const importPort = prompt("Enter your Postgres Port to migrate to [Default = 5432]: ") ?? "5432";
const importUser = prompt("Enter your Postgres User to migrate to: [Default = infisical]: ") ?? "infisical";
const importPassword = prompt("Enter your Postgres Password to migrate to: ");
const importDatabase = prompt("Enter your Postgres Database to migrate to [Default = infisical]: ") ?? "infisical";
const orgId = prompt("Enter the organization ID to migrate: ");
const importHost = sanitizeInputParam(prompt("Enter your Postgres Host to migrate to: "));
const importPort = sanitizeInputParam(prompt("Enter your Postgres Port to migrate to [Default = 5432]: ") ?? "5432");
const importUser = sanitizeInputParam(
prompt("Enter your Postgres User to migrate to: [Default = infisical]: ") ?? "infisical"
);
const importPassword = sanitizeInputParam(prompt("Enter your Postgres Password to migrate to: "));
const importDatabase = sanitizeInputParam(
prompt("Enter your Postgres Database to migrate to [Default = infisical]: ") ?? "infisical"
);
const orgId = sanitizeInputParam(prompt("Enter the organization ID to migrate: "));
if (!existsSync(path.join(__dirname, "../src/db/dump.sql"))) {
if (!existsSync(path.join(__dirname, "../src/db/backup.dump"))) {
console.log("File not found, please export the database first.");
return;
}
execSync(
`PGDATABASE="${importDatabase}" PGPASSWORD="${importPassword}" PGHOST="${importHost}" PGPORT=${importPort} PGUSER=${importUser} psql -f ${path.join(
`PGDATABASE=${importDatabase} PGPASSWORD=${importPassword} PGHOST=${importHost} PGPORT=${importPort} PGUSER=${importUser} pg_restore -d ${importDatabase} --verbose ${path.join(
__dirname,
"../src/db/dump.sql"
)}`
"../src/db/backup.dump"
)}`,
{ maxBuffer: 1024 * 1024 * 4096 }
);
execSync(
`PGDATABASE="${importDatabase}" PGPASSWORD="${importPassword}" PGHOST="${importHost}" PGPORT=${importPort} PGUSER=${importUser} psql -c "DELETE FROM public.organizations WHERE id != '${orgId}'"`
`PGDATABASE=${importDatabase} PGPASSWORD=${importPassword} PGHOST=${importHost} PGPORT=${importPort} PGUSER=${importUser} psql -c "DELETE FROM public.organizations WHERE id != '${orgId}'"`
);
// delete global/instance-level resources not relevant to the organization to migrate
// users
execSync(
`PGDATABASE="${importDatabase}" PGPASSWORD="${importPassword}" PGHOST="${importHost}" PGPORT=${importPort} PGUSER=${importUser} psql -c 'DELETE FROM users WHERE users.id NOT IN (SELECT org_memberships."userId" FROM org_memberships)'`
`PGDATABASE=${importDatabase} PGPASSWORD=${importPassword} PGHOST=${importHost} PGPORT=${importPort} PGUSER=${importUser} psql -c 'DELETE FROM users WHERE users.id NOT IN (SELECT org_memberships."userId" FROM org_memberships)'`
);
// identities
execSync(
`PGDATABASE="${importDatabase}" PGPASSWORD="${importPassword}" PGHOST="${importHost}" PGPORT=${importPort} PGUSER=${importUser} psql -c 'DELETE FROM identities WHERE id NOT IN (SELECT "identityId" FROM identity_org_memberships)'`
`PGDATABASE=${importDatabase} PGPASSWORD=${importPassword} PGHOST=${importHost} PGPORT=${importPort} PGUSER=${importUser} psql -c 'DELETE FROM identities WHERE id NOT IN (SELECT "identityId" FROM identity_org_memberships)'`
);
// reset slack configuration in superAdmin
execSync(
`PGDATABASE="${importDatabase}" PGPASSWORD="${importPassword}" PGHOST="${importHost}" PGPORT=${importPort} PGUSER=${importUser} psql -c 'UPDATE super_admin SET "encryptedSlackClientId" = null, "encryptedSlackClientSecret" = null'`
`PGDATABASE=${importDatabase} PGPASSWORD=${importPassword} PGHOST=${importHost} PGPORT=${importPort} PGUSER=${importUser} psql -c 'UPDATE super_admin SET "encryptedSlackClientId" = null, "encryptedSlackClientSecret" = null'`
);
console.log("Organization migrated successfully.");

@ -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";
@ -77,6 +79,7 @@ import { TServiceTokenServiceFactory } from "@app/services/service-token/service
import { TSlackServiceFactory } from "@app/services/slack/slack-service";
import { TSuperAdminServiceFactory } from "@app/services/super-admin/super-admin-service";
import { TTelemetryServiceFactory } from "@app/services/telemetry/telemetry-service";
import { TTotpServiceFactory } from "@app/services/totp/totp-service";
import { TUserDALFactory } from "@app/services/user/user-dal";
import { TUserServiceFactory } from "@app/services/user/user-service";
import { TUserEngagementServiceFactory } from "@app/services/user-engagement/user-engagement-service";
@ -183,12 +186,15 @@ 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;
totp: TTotpServiceFactory;
};
// this is exclusive use for middlewares in which we need to inject data
// everywhere else access using service layer

@ -200,6 +200,9 @@ import {
TProjectSlackConfigsInsert,
TProjectSlackConfigsUpdate,
TProjectsUpdate,
TProjectTemplates,
TProjectTemplatesInsert,
TProjectTemplatesUpdate,
TProjectUserAdditionalPrivilege,
TProjectUserAdditionalPrivilegeInsert,
TProjectUserAdditionalPrivilegeUpdate,
@ -311,6 +314,9 @@ import {
TSuperAdmin,
TSuperAdminInsert,
TSuperAdminUpdate,
TTotpConfigs,
TTotpConfigsInsert,
TTotpConfigsUpdate,
TTrustedIps,
TTrustedIpsInsert,
TTrustedIpsUpdate,
@ -818,5 +824,11 @@ declare module "knex/types/tables" {
TExternalGroupOrgRoleMappingsInsert,
TExternalGroupOrgRoleMappingsUpdate
>;
[TableName.ProjectTemplates]: KnexOriginal.CompositeTableType<
TProjectTemplates,
TProjectTemplatesInsert,
TProjectTemplatesUpdate
>;
[TableName.TotpConfig]: KnexOriginal.CompositeTableType<TTotpConfigs, TTotpConfigsInsert, TTotpConfigsUpdate>;
}
}

@ -64,23 +64,25 @@ export async function up(knex: Knex): Promise<void> {
}
if (await knex.schema.hasTable(TableName.Certificate)) {
await knex.schema.alterTable(TableName.Certificate, (t) => {
t.uuid("caCertId").nullable();
t.foreign("caCertId").references("id").inTable(TableName.CertificateAuthorityCert);
});
const hasCaCertIdColumn = await knex.schema.hasColumn(TableName.Certificate, "caCertId");
if (!hasCaCertIdColumn) {
await knex.schema.alterTable(TableName.Certificate, (t) => {
t.uuid("caCertId").nullable();
t.foreign("caCertId").references("id").inTable(TableName.CertificateAuthorityCert);
});
await knex.raw(`
await knex.raw(`
UPDATE "${TableName.Certificate}" cert
SET "caCertId" = (
SELECT caCert.id
FROM "${TableName.CertificateAuthorityCert}" caCert
WHERE caCert."caId" = cert."caId"
)
`);
)`);
await knex.schema.alterTable(TableName.Certificate, (t) => {
t.uuid("caCertId").notNullable().alter();
});
await knex.schema.alterTable(TableName.Certificate, (t) => {
t.uuid("caCertId").notNullable().alter();
});
}
}
}

@ -2,7 +2,7 @@ import { Knex } from "knex";
import { TableName } from "../schemas";
const BATCH_SIZE = 30_000;
const BATCH_SIZE = 10_000;
export async function up(knex: Knex): Promise<void> {
const hasAuthMethodColumnAccessToken = await knex.schema.hasColumn(TableName.IdentityAccessToken, "authMethod");
@ -12,7 +12,18 @@ export async function up(knex: Knex): Promise<void> {
t.string("authMethod").nullable();
});
let nullableAccessTokens = await knex(TableName.IdentityAccessToken).whereNull("authMethod").limit(BATCH_SIZE);
// first we remove identities without auth method that is unused
// ! We delete all access tokens where the identity has no auth method set!
// ! Which means un-configured identities that for some reason have access tokens, will have their access tokens deleted.
await knex(TableName.IdentityAccessToken)
.leftJoin(TableName.Identity, `${TableName.Identity}.id`, `${TableName.IdentityAccessToken}.identityId`)
.whereNull(`${TableName.Identity}.authMethod`)
.delete();
let nullableAccessTokens = await knex(TableName.IdentityAccessToken)
.whereNull("authMethod")
.limit(BATCH_SIZE)
.select("id");
let totalUpdated = 0;
do {
@ -33,24 +44,15 @@ export async function up(knex: Knex): Promise<void> {
});
// eslint-disable-next-line no-await-in-loop
nullableAccessTokens = await knex(TableName.IdentityAccessToken).whereNull("authMethod").limit(BATCH_SIZE);
nullableAccessTokens = await knex(TableName.IdentityAccessToken)
.whereNull("authMethod")
.limit(BATCH_SIZE)
.select("id");
totalUpdated += batchIds.length;
console.log(`Updated ${batchIds.length} access tokens in batch <> Total updated: ${totalUpdated}`);
} while (nullableAccessTokens.length > 0);
// ! We delete all access tokens where the identity has no auth method set!
// ! Which means un-configured identities that for some reason have access tokens, will have their access tokens deleted.
await knex(TableName.IdentityAccessToken)
.whereNotExists((queryBuilder) => {
void queryBuilder
.select("id")
.from(TableName.Identity)
.whereRaw(`${TableName.IdentityAccessToken}."identityId" = ${TableName.Identity}.id`)
.whereNotNull("authMethod");
})
.delete();
// Finally we set the authMethod to notNullable after populating the column.
// This will fail if the data is not populated correctly, so it's safe.
await knex.schema.alterTable(TableName.IdentityAccessToken, (t) => {
@ -72,5 +74,5 @@ export async function down(knex: Knex): Promise<void> {
}
}
const config = {transaction: false};
const config = { transaction: false };
export { config };

@ -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);
}
}

@ -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");
}
});
}

@ -0,0 +1,21 @@
import { Knex } from "knex";
import { TableName } from "../schemas";
export async function up(knex: Knex): Promise<void> {
if (await knex.schema.hasColumn(TableName.OidcConfig, "orgId")) {
await knex.schema.alterTable(TableName.OidcConfig, (t) => {
t.dropForeign("orgId");
t.foreign("orgId").references("id").inTable(TableName.Organization).onDelete("CASCADE");
});
}
}
export async function down(knex: Knex): Promise<void> {
if (await knex.schema.hasColumn(TableName.OidcConfig, "orgId")) {
await knex.schema.alterTable(TableName.OidcConfig, (t) => {
t.dropForeign("orgId");
t.foreign("orgId").references("id").inTable(TableName.Organization);
});
}
}

@ -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);
});
}

@ -0,0 +1,54 @@
import { Knex } from "knex";
import { TableName } from "../schemas";
import { createOnUpdateTrigger, dropOnUpdateTrigger } from "../utils";
export async function up(knex: Knex): Promise<void> {
if (!(await knex.schema.hasTable(TableName.TotpConfig))) {
await knex.schema.createTable(TableName.TotpConfig, (t) => {
t.uuid("id", { primaryKey: true }).defaultTo(knex.fn.uuid());
t.uuid("userId").notNullable();
t.foreign("userId").references("id").inTable(TableName.Users).onDelete("CASCADE");
t.boolean("isVerified").defaultTo(false).notNullable();
t.binary("encryptedRecoveryCodes").notNullable();
t.binary("encryptedSecret").notNullable();
t.timestamps(true, true, true);
t.unique("userId");
});
await createOnUpdateTrigger(knex, TableName.TotpConfig);
}
const doesOrgMfaMethodColExist = await knex.schema.hasColumn(TableName.Organization, "selectedMfaMethod");
await knex.schema.alterTable(TableName.Organization, (t) => {
if (!doesOrgMfaMethodColExist) {
t.string("selectedMfaMethod");
}
});
const doesUserSelectedMfaMethodColExist = await knex.schema.hasColumn(TableName.Users, "selectedMfaMethod");
await knex.schema.alterTable(TableName.Users, (t) => {
if (!doesUserSelectedMfaMethodColExist) {
t.string("selectedMfaMethod");
}
});
}
export async function down(knex: Knex): Promise<void> {
await dropOnUpdateTrigger(knex, TableName.TotpConfig);
await knex.schema.dropTableIfExists(TableName.TotpConfig);
const doesOrgMfaMethodColExist = await knex.schema.hasColumn(TableName.Organization, "selectedMfaMethod");
await knex.schema.alterTable(TableName.Organization, (t) => {
if (doesOrgMfaMethodColExist) {
t.dropColumn("selectedMfaMethod");
}
});
const doesUserSelectedMfaMethodColExist = await knex.schema.hasColumn(TableName.Users, "selectedMfaMethod");
await knex.schema.alterTable(TableName.Users, (t) => {
if (doesUserSelectedMfaMethodColExist) {
t.dropColumn("selectedMfaMethod");
}
});
}

@ -0,0 +1,20 @@
import { Knex } from "knex";
import { TableName } from "../schemas";
export async function up(knex: Knex): Promise<void> {
if (await knex.schema.hasColumn(TableName.IdentityMetadata, "value")) {
await knex(TableName.IdentityMetadata).whereNull("value").delete();
await knex.schema.alterTable(TableName.IdentityMetadata, (t) => {
t.string("value", 1020).notNullable().alter();
});
}
}
export async function down(knex: Knex): Promise<void> {
if (await knex.schema.hasColumn(TableName.IdentityMetadata, "value")) {
await knex.schema.alterTable(TableName.IdentityMetadata, (t) => {
t.string("value", 1020).alter();
});
}
}

@ -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>;

@ -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";
@ -105,6 +106,7 @@ export * from "./secrets-v2";
export * from "./service-tokens";
export * from "./slack-integrations";
export * from "./super-admin";
export * from "./totp-configs";
export * from "./trusted-ips";
export * from "./user-actions";
export * from "./user-aliases";

@ -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>;

@ -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",
@ -116,6 +117,7 @@ export enum TableName {
ExternalKms = "external_kms",
InternalKms = "internal_kms",
InternalKmsKeyVersion = "internal_kms_key_version",
TotpConfig = "totp_configs",
// @depreciated
KmsKeyVersion = "kms_key_versions",
WorkflowIntegrations = "workflow_integrations",

@ -21,7 +21,8 @@ export const OrganizationsSchema = z.object({
kmsDefaultKeyId: z.string().uuid().nullable().optional(),
kmsEncryptedDataKey: zodBuffer.nullable().optional(),
defaultMembershipRole: z.string().default("member"),
enforceMfa: z.boolean().default(false)
enforceMfa: z.boolean().default(false),
selectedMfaMethod: z.string().nullable().optional()
});
export type TOrganizations = z.infer<typeof OrganizationsSchema>;

@ -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>;

@ -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>>;

@ -0,0 +1,24 @@
// Code generated by automation script, DO NOT EDIT.
// Automated by pulling database and generating zod schema
// To update. Just run npm run generate:schema
// Written by akhilmhdh.
import { z } from "zod";
import { zodBuffer } from "@app/lib/zod";
import { TImmutableDBKeys } from "./models";
export const TotpConfigsSchema = z.object({
id: z.string().uuid(),
userId: z.string().uuid(),
isVerified: z.boolean().default(false),
encryptedRecoveryCodes: zodBuffer,
encryptedSecret: zodBuffer,
createdAt: z.date(),
updatedAt: z.date()
});
export type TTotpConfigs = z.infer<typeof TotpConfigsSchema>;
export type TTotpConfigsInsert = Omit<z.input<typeof TotpConfigsSchema>, TImmutableDBKeys>;
export type TTotpConfigsUpdate = Partial<Omit<z.input<typeof TotpConfigsSchema>, TImmutableDBKeys>>;

@ -26,7 +26,8 @@ export const UsersSchema = z.object({
consecutiveFailedMfaAttempts: z.number().default(0).nullable().optional(),
isLocked: z.boolean().default(false).nullable().optional(),
temporaryLockDateEnd: z.date().nullable().optional(),
consecutiveFailedPasswordAttempts: z.number().default(0).nullable().optional()
consecutiveFailedPasswordAttempts: z.number().default(0).nullable().optional(),
selectedMfaMethod: z.string().nullable().optional()
});
export type TUsers = z.infer<typeof UsersSchema>;

@ -2,6 +2,9 @@ import { Knex } from "knex";
import { TableName } from "./schemas";
interface PgTriggerResult {
rows: Array<{ exists: boolean }>;
}
export const createJunctionTable = (knex: Knex, tableName: TableName, table1Name: TableName, table2Name: TableName) =>
knex.schema.createTable(tableName, (table) => {
table.uuid("id", { primaryKey: true }).defaultTo(knex.fn.uuid());
@ -28,13 +31,26 @@ DROP FUNCTION IF EXISTS on_update_timestamp() CASCADE;
// we would be using this to apply updatedAt where ever we wanta
// remember to set `timestamps(true,true,true)` before this on schema
export const createOnUpdateTrigger = (knex: Knex, tableName: string) =>
knex.raw(`
CREATE TRIGGER "${tableName}_updatedAt"
BEFORE UPDATE ON ${tableName}
FOR EACH ROW
EXECUTE PROCEDURE on_update_timestamp();
`);
export const createOnUpdateTrigger = async (knex: Knex, tableName: string) => {
const triggerExists = await knex.raw<PgTriggerResult>(`
SELECT EXISTS (
SELECT 1
FROM pg_trigger
WHERE tgname = '${tableName}_updatedAt'
);
`);
if (!triggerExists?.rows?.[0]?.exists) {
return knex.raw(`
CREATE TRIGGER "${tableName}_updatedAt"
BEFORE UPDATE ON ${tableName}
FOR EACH ROW
EXECUTE PROCEDURE on_update_timestamp();
`);
}
return null;
};
export const dropOnUpdateTrigger = (knex: Knex, tableName: string) =>
knex.raw(`DROP TRIGGER IF EXISTS "${tableName}_updatedAt" ON ${tableName}`);

@ -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" });
};

@ -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 })
})
}
},

@ -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 };
}
});
};

@ -122,6 +122,8 @@ export const registerSamlRouter = async (server: FastifyZodProvider) => {
},
`email: ${email} firstName: ${profile.firstName as string}`
);
throw new Error("Invalid saml request. Missing email or first name");
}
const userMetadata = Object.keys(profile.attributes || {})

@ -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 })
})
}
},

@ -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;

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

@ -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, ProjectServiceActor } 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";
@ -457,7 +457,7 @@ export const dynamicSecretServiceFactory = ({
const listDynamicSecretsByFolderIds = async (
{ folderMappings, filters, projectId }: TListDynamicSecretsByFolderMappingsDTO,
actor: ProjectServiceActor
actor: OrgServiceActor
) => {
const { permission } = await permissionService.getProjectPermission(
actor.type,

@ -13,6 +13,7 @@ import { RabbitMqProvider } from "./rabbit-mq";
import { RedisDatabaseProvider } from "./redis";
import { SapHanaProvider } from "./sap-hana";
import { SqlDatabaseProvider } from "./sql-database";
import { TotpProvider } from "./totp";
export const buildDynamicSecretProviders = () => ({
[DynamicSecretProviders.SqlDatabase]: SqlDatabaseProvider(),
@ -27,5 +28,6 @@ export const buildDynamicSecretProviders = () => ({
[DynamicSecretProviders.AzureEntraID]: AzureEntraIDProvider(),
[DynamicSecretProviders.Ldap]: LdapProvider(),
[DynamicSecretProviders.SapHana]: SapHanaProvider(),
[DynamicSecretProviders.Snowflake]: SnowflakeProvider()
[DynamicSecretProviders.Snowflake]: SnowflakeProvider(),
[DynamicSecretProviders.Totp]: TotpProvider()
});

@ -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 };
};

@ -12,6 +12,22 @@ export enum ElasticSearchAuthTypes {
ApiKey = "api-key"
}
export enum LdapCredentialType {
Dynamic = "dynamic",
Static = "static"
}
export enum TotpConfigType {
URL = "url",
MANUAL = "manual"
}
export enum TotpAlgorithm {
SHA1 = "sha1",
SHA256 = "sha256",
SHA512 = "sha512"
}
export const DynamicSecretRedisDBSchema = z.object({
host: z.string().trim().toLowerCase(),
port: z.number(),
@ -195,16 +211,54 @@ 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(),
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)
})
]);
creationLdif: z.string().min(1),
revocationLdif: z.string().min(1),
rollbackLdif: z.string().optional()
});
export const DynamicSecretTotpSchema = z.discriminatedUnion("configType", [
z.object({
configType: z.literal(TotpConfigType.URL),
url: z
.string()
.url()
.trim()
.min(1)
.refine((val) => {
const urlObj = new URL(val);
const secret = urlObj.searchParams.get("secret");
return Boolean(secret);
}, "OTP URL must contain secret field")
}),
z.object({
configType: z.literal(TotpConfigType.MANUAL),
secret: z
.string()
.trim()
.min(1)
.transform((val) => val.replace(/\s+/g, "")),
period: z.number().optional(),
algorithm: z.nativeEnum(TotpAlgorithm).optional(),
digits: z.number().optional()
})
]);
export enum DynamicSecretProviders {
SqlDatabase = "sql-database",
@ -219,7 +273,8 @@ export enum DynamicSecretProviders {
AzureEntraID = "azure-entra-id",
Ldap = "ldap",
SapHana = "sap-hana",
Snowflake = "snowflake"
Snowflake = "snowflake",
Totp = "totp"
}
export const DynamicSecretProviderSchema = z.discriminatedUnion("type", [
@ -235,7 +290,8 @@ export const DynamicSecretProviderSchema = z.discriminatedUnion("type", [
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.Snowflake), inputs: DynamicSecretSnowflakeSchema })
z.object({ type: z.literal(DynamicSecretProviders.Snowflake), inputs: DynamicSecretSnowflakeSchema }),
z.object({ type: z.literal(DynamicSecretProviders.Totp), inputs: DynamicSecretTotpSchema })
]);
export type TDynamicProviderFns = {

@ -0,0 +1,92 @@
import { authenticator } from "otplib";
import { HashAlgorithms } from "otplib/core";
import { BadRequestError } from "@app/lib/errors";
import { alphaNumericNanoId } from "@app/lib/nanoid";
import { DynamicSecretTotpSchema, TDynamicProviderFns, TotpConfigType } from "./models";
export const TotpProvider = (): TDynamicProviderFns => {
const validateProviderInputs = async (inputs: unknown) => {
const providerInputs = await DynamicSecretTotpSchema.parseAsync(inputs);
return providerInputs;
};
const validateConnection = async () => {
return true;
};
const create = async (inputs: unknown) => {
const providerInputs = await validateProviderInputs(inputs);
const entityId = alphaNumericNanoId(32);
const authenticatorInstance = authenticator.clone();
let secret: string;
let period: number | null | undefined;
let digits: number | null | undefined;
let algorithm: HashAlgorithms | null | undefined;
if (providerInputs.configType === TotpConfigType.URL) {
const urlObj = new URL(providerInputs.url);
secret = urlObj.searchParams.get("secret") as string;
const periodFromUrl = urlObj.searchParams.get("period");
const digitsFromUrl = urlObj.searchParams.get("digits");
const algorithmFromUrl = urlObj.searchParams.get("algorithm");
if (periodFromUrl) {
period = +periodFromUrl;
}
if (digitsFromUrl) {
digits = +digitsFromUrl;
}
if (algorithmFromUrl) {
algorithm = algorithmFromUrl.toLowerCase() as HashAlgorithms;
}
} else {
secret = providerInputs.secret;
period = providerInputs.period;
digits = providerInputs.digits;
algorithm = providerInputs.algorithm as unknown as HashAlgorithms;
}
if (digits) {
authenticatorInstance.options = { digits };
}
if (algorithm) {
authenticatorInstance.options = { algorithm };
}
if (period) {
authenticatorInstance.options = { step: period };
}
return {
entityId,
data: { TOTP: authenticatorInstance.generate(secret), TIME_REMAINING: authenticatorInstance.timeRemaining() }
};
};
const revoke = async (_inputs: unknown, entityId: string) => {
return { entityId };
};
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const renew = async (_inputs: unknown, _entityId: string) => {
throw new BadRequestError({
message: "Lease renewal is not supported for TOTPs"
});
};
return {
validateProviderInputs,
validateConnection,
create,
revoke,
renew
};
};

@ -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 });

@ -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
};
};

@ -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
};
};

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

@ -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) => {

@ -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 = {

@ -17,7 +17,7 @@ import {
infisicalSymmetricDecrypt,
infisicalSymmetricEncypt
} from "@app/lib/crypto/encryption";
import { BadRequestError, ForbiddenRequestError, NotFoundError } from "@app/lib/errors";
import { BadRequestError, ForbiddenRequestError, NotFoundError, OidcAuthError } from "@app/lib/errors";
import { AuthMethod, AuthTokenType } from "@app/services/auth/auth-type";
import { TAuthTokenServiceFactory } from "@app/services/auth-token/auth-token-service";
import { TokenType } from "@app/services/auth-token/auth-token-types";
@ -56,7 +56,7 @@ type TOidcConfigServiceFactoryDep = {
orgBotDAL: Pick<TOrgBotDALFactory, "findOne" | "create" | "transaction">;
licenseService: Pick<TLicenseServiceFactory, "getPlan" | "updateSubscriptionOrgMemberCount">;
tokenService: Pick<TAuthTokenServiceFactory, "createTokenForUser">;
smtpService: Pick<TSmtpService, "sendMail">;
smtpService: Pick<TSmtpService, "sendMail" | "verify">;
permissionService: Pick<TPermissionServiceFactory, "getOrgPermission">;
oidcConfigDAL: Pick<TOidcConfigDALFactory, "findOne" | "update" | "create">;
};
@ -223,6 +223,7 @@ export const oidcConfigServiceFactory = ({
let newUser: TUsers | undefined;
if (serverCfg.trustOidcEmails) {
// we prioritize getting the most complete user to create the new alias under
newUser = await userDAL.findOne(
{
email,
@ -230,6 +231,23 @@ export const oidcConfigServiceFactory = ({
},
tx
);
if (!newUser) {
// this fetches user entries created via invites
newUser = await userDAL.findOne(
{
username: email
},
tx
);
if (newUser && !newUser.isEmailVerified) {
// we automatically mark it as email-verified because we've configured trust for OIDC emails
newUser = await userDAL.updateById(newUser.id, {
isEmailVerified: true
});
}
}
}
if (!newUser) {
@ -332,14 +350,20 @@ export const oidcConfigServiceFactory = ({
userId: user.id
});
await smtpService.sendMail({
template: SmtpTemplates.EmailVerification,
subjectLine: "Infisical confirmation code",
recipients: [user.email],
substitutions: {
code: token
}
});
await smtpService
.sendMail({
template: SmtpTemplates.EmailVerification,
subjectLine: "Infisical confirmation code",
recipients: [user.email],
substitutions: {
code: token
}
})
.catch((err: Error) => {
throw new OidcAuthError({
message: `Error sending email confirmation code for user registration - contact the Infisical instance admin. ${err.message}`
});
});
}
return { isUserCompleted, providerAuthToken };
@ -395,6 +419,18 @@ export const oidcConfigServiceFactory = ({
message: `Organization bot for organization with ID '${org.id}' not found`,
name: "OrgBotNotFound"
});
const serverCfg = await getServerCfg();
if (isActive && !serverCfg.trustOidcEmails) {
const isSmtpConnected = await smtpService.verify();
if (!isSmtpConnected) {
throw new BadRequestError({
message:
"Cannot enable OIDC when there are issues with the instance's SMTP configuration. Bypass this by turning on trust for OIDC emails in the server admin console."
});
}
}
const key = infisicalSymmetricDecrypt({
ciphertext: orgBot.encryptedSymmetricKey,
iv: orgBot.symmetricKeyIV,

@ -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;

@ -29,4 +29,18 @@ function validateOrgSSO(actorAuthMethod: ActorAuthMethod, isOrgSsoEnforced: TOrg
}
}
export { isAuthMethodSaml, validateOrgSSO };
const escapeHandlebarsMissingMetadata = (obj: Record<string, string>) => {
const handler = {
get(target: Record<string, string>, prop: string) {
if (!(prop in target)) {
// eslint-disable-next-line no-param-reassign
target[prop] = `{{identity.metadata.${prop}}}`; // Add missing key as an "own" property
}
return target[prop];
}
};
return new Proxy(obj, handler);
};
export { escapeHandlebarsMissingMetadata, isAuthMethodSaml, validateOrgSSO };

@ -21,7 +21,7 @@ import { TServiceTokenDALFactory } from "@app/services/service-token/service-tok
import { orgAdminPermissions, orgMemberPermissions, orgNoAccessPermissions, OrgPermissionSet } from "./org-permission";
import { TPermissionDALFactory } from "./permission-dal";
import { validateOrgSSO } from "./permission-fns";
import { escapeHandlebarsMissingMetadata, validateOrgSSO } from "./permission-fns";
import { TBuildOrgPermissionDTO, TBuildProjectPermissionDTO } from "./permission-service-types";
import {
buildServiceTokenProjectPermission,
@ -227,11 +227,13 @@ export const permissionServiceFactory = ({
})) || [];
const rules = buildProjectPermissionRules(rolePermissions.concat(additionalPrivileges));
const templatedRules = handlebars.compile(JSON.stringify(rules), { data: false, strict: true });
const metadataKeyValuePair = objectify(
userProjectPermission.metadata,
(i) => i.key,
(i) => i.value
const templatedRules = handlebars.compile(JSON.stringify(rules), { data: false });
const metadataKeyValuePair = escapeHandlebarsMissingMetadata(
objectify(
userProjectPermission.metadata,
(i) => i.key,
(i) => i.value
)
);
const interpolateRules = templatedRules(
{
@ -292,12 +294,15 @@ export const permissionServiceFactory = ({
})) || [];
const rules = buildProjectPermissionRules(rolePermissions.concat(additionalPrivileges));
const templatedRules = handlebars.compile(JSON.stringify(rules), { data: false, strict: true });
const metadataKeyValuePair = objectify(
identityProjectPermission.metadata,
(i) => i.key,
(i) => i.value
const templatedRules = handlebars.compile(JSON.stringify(rules), { data: false });
const metadataKeyValuePair = escapeHandlebarsMissingMetadata(
objectify(
identityProjectPermission.metadata,
(i) => i.key,
(i) => i.value
)
);
const interpolateRules = templatedRules(
{
identity: {

@ -1,14 +1,7 @@
import picomatch from "picomatch";
import { z } from "zod";
export enum PermissionConditionOperators {
$IN = "$in",
$ALL = "$all",
$REGEX = "$regex",
$EQ = "$eq",
$NEQ = "$ne",
$GLOB = "$glob"
}
import { PermissionConditionOperators } from "@app/lib/casl";
export const PermissionConditionSchema = {
[PermissionConditionOperators.$IN]: z.string().trim().min(1).array(),

@ -1,10 +1,10 @@
import { AbilityBuilder, createMongoAbility, ForcedSubject, MongoAbility } from "@casl/ability";
import { z } from "zod";
import { conditionsMatcher } from "@app/lib/casl";
import { conditionsMatcher, PermissionConditionOperators } from "@app/lib/casl";
import { UnpackedPermissionSchema } from "@app/server/routes/santizedSchemas/permission";
import { PermissionConditionOperators, PermissionConditionSchema } from "./permission-types";
import { PermissionConditionSchema } from "./permission-types";
export enum ProjectPermissionActions {
Read = "read",

@ -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 }
];

@ -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);

@ -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);

@ -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
};
};

@ -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"
}

@ -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
}
};

@ -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}'`,

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

@ -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

@ -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",

@ -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" }
}
};

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

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

@ -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."
@ -1438,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."
}
};

@ -54,3 +54,12 @@ export const isAtLeastAsPrivileged = (permissions1: MongoAbility, permissions2:
return set1.size >= set2.size;
};
export enum PermissionConditionOperators {
$IN = "$in",
$ALL = "$all",
$REGEX = "$regex",
$EQ = "$eq",
$NEQ = "$ne",
$GLOB = "$glob"
}

@ -157,15 +157,37 @@ const envSchema = z
INFISICAL_CLOUD: zodStrBool.default("false"),
MAINTENANCE_MODE: zodStrBool.default("false"),
CAPTCHA_SECRET: zpStr(z.string().optional()),
// TELEMETRY
OTEL_TELEMETRY_COLLECTION_ENABLED: zodStrBool.default("false"),
OTEL_EXPORT_OTLP_ENDPOINT: zpStr(z.string().optional()),
OTEL_OTLP_PUSH_INTERVAL: z.coerce.number().default(30000),
OTEL_COLLECTOR_BASIC_AUTH_USERNAME: zpStr(z.string().optional()),
OTEL_COLLECTOR_BASIC_AUTH_PASSWORD: zpStr(z.string().optional()),
OTEL_EXPORT_TYPE: z.enum(["prometheus", "otlp"]).optional(),
PLAIN_API_KEY: zpStr(z.string().optional()),
PLAIN_WISH_LABEL_IDS: zpStr(z.string().optional()),
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 +196,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(",")
}));
@ -186,11 +212,11 @@ let envCfg: Readonly<z.infer<typeof envSchema>>;
export const getConfig = () => envCfg;
// cannot import singleton logger directly as it needs config to load various transport
export const initEnvConfig = (logger: Logger) => {
export const initEnvConfig = (logger?: Logger) => {
const parsedEnv = envSchema.safeParse(process.env);
if (!parsedEnv.success) {
logger.error("Invalid environment variables. Check the error below");
logger.error(parsedEnv.error.issues);
(logger ?? console).error("Invalid environment variables. Check the error below");
(logger ?? console).error(parsedEnv.error.issues);
process.exit(-1);
}

@ -133,3 +133,15 @@ export class ScimRequestError extends Error {
this.status = status;
}
}
export class OidcAuthError extends Error {
name: string;
error: unknown;
constructor({ name, error, message }: { message?: string; name?: string; error?: unknown }) {
super(message || "Something went wrong");
this.name = name || "OidcAuthError";
this.error = error;
}
}

@ -0,0 +1,91 @@
import opentelemetry, { diag, DiagConsoleLogger, DiagLogLevel } from "@opentelemetry/api";
import { getNodeAutoInstrumentations } from "@opentelemetry/auto-instrumentations-node";
import { OTLPMetricExporter } from "@opentelemetry/exporter-metrics-otlp-proto";
import { PrometheusExporter } from "@opentelemetry/exporter-prometheus";
import { registerInstrumentations } from "@opentelemetry/instrumentation";
import { Resource } from "@opentelemetry/resources";
import { AggregationTemporality, MeterProvider, PeriodicExportingMetricReader } from "@opentelemetry/sdk-metrics";
import { ATTR_SERVICE_NAME, ATTR_SERVICE_VERSION } from "@opentelemetry/semantic-conventions";
import dotenv from "dotenv";
import { initEnvConfig } from "../config/env";
dotenv.config();
const initTelemetryInstrumentation = ({
exportType,
otlpURL,
otlpUser,
otlpPassword,
otlpPushInterval
}: {
exportType?: string;
otlpURL?: string;
otlpUser?: string;
otlpPassword?: string;
otlpPushInterval?: number;
}) => {
diag.setLogger(new DiagConsoleLogger(), DiagLogLevel.DEBUG);
const resource = Resource.default().merge(
new Resource({
[ATTR_SERVICE_NAME]: "infisical-core",
[ATTR_SERVICE_VERSION]: "0.1.0"
})
);
const metricReaders = [];
switch (exportType) {
case "prometheus": {
const promExporter = new PrometheusExporter();
metricReaders.push(promExporter);
break;
}
case "otlp": {
const otlpExporter = new OTLPMetricExporter({
url: `${otlpURL}/v1/metrics`,
headers: {
Authorization: `Basic ${btoa(`${otlpUser}:${otlpPassword}`)}`
},
temporalityPreference: AggregationTemporality.DELTA
});
metricReaders.push(
new PeriodicExportingMetricReader({
exporter: otlpExporter,
exportIntervalMillis: otlpPushInterval
})
);
break;
}
default:
throw new Error("Invalid OTEL export type");
}
const meterProvider = new MeterProvider({
resource,
readers: metricReaders
});
opentelemetry.metrics.setGlobalMeterProvider(meterProvider);
registerInstrumentations({
instrumentations: [getNodeAutoInstrumentations()]
});
};
const setupTelemetry = () => {
const appCfg = initEnvConfig();
if (appCfg.OTEL_TELEMETRY_COLLECTION_ENABLED) {
console.log("Initializing telemetry instrumentation");
initTelemetryInstrumentation({
otlpURL: appCfg.OTEL_EXPORT_OTLP_ENDPOINT,
otlpUser: appCfg.OTEL_COLLECTOR_BASIC_AUTH_USERNAME,
otlpPassword: appCfg.OTEL_COLLECTOR_BASIC_AUTH_PASSWORD,
otlpPushInterval: appCfg.OTEL_OTLP_PUSH_INTERVAL,
exportType: appCfg.OTEL_EXPORT_TYPE
});
}
};
void setupTelemetry();

@ -58,7 +58,7 @@ export enum OrderByDirection {
DESC = "desc"
}
export type ProjectServiceActor = {
export type OrgServiceActor = {
type: ActorType;
id: string;
authMethod: ActorAuthMethod;

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

@ -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));

@ -1,6 +1,10 @@
import "./lib/telemetry/instrumentation";
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";
@ -16,6 +20,7 @@ dotenv.config();
const run = async () => {
const logger = await initLogger();
const appCfg = initEnvConfig(logger);
const db = initDbConnection({
dbConnectionUri: appCfg.DB_CONNECTION_URI,
dbRootCert: appCfg.DB_ROOT_CERT,
@ -53,13 +58,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 +76,7 @@ const run = async () => {
process.on("SIGTERM", async () => {
await server.close();
await db.destroy();
hsmModule.finalize();
process.exit(0);
});

@ -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";
@ -21,6 +22,7 @@ import { TSmtpService } from "@app/services/smtp/smtp-service";
import { globalRateLimiterCfg } from "./config/rateLimiter";
import { addErrorsToResponseSchemas } from "./plugins/add-errors-to-response-schemas";
import { apiMetrics } from "./plugins/api-metrics";
import { fastifyErrHandler } from "./plugins/error-handler";
import { registerExternalNextjs } from "./plugins/external-nextjs";
import { serializerCompiler, validatorCompiler, ZodTypeProvider } from "./plugins/fastify-zod";
@ -36,16 +38,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);
@ -82,6 +87,10 @@ export const main = async ({ db, auditLogDb, smtp, logger, queue, keyStore }: TM
// pull ip based on various proxy headers
await server.register(fastifyIp);
if (appCfg.OTEL_TELEMETRY_COLLECTION_ENABLED) {
await server.register(apiMetrics);
}
await server.register(fastifySwagger);
await server.register(fastifyFormBody);
await server.register(fastifyErrHandler);
@ -95,7 +104,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, {

@ -46,10 +46,10 @@ export const bootstrapCheck = async ({ db }: BootstrapOpt) => {
await createTransport(smtpCfg)
.verify()
.then(async () => {
console.info("SMTP successfully connected");
console.info(`SMTP - Verified connection to ${appCfg.SMTP_HOST}:${appCfg.SMTP_PORT}`);
})
.catch((err) => {
console.error(`SMTP - Failed to connect to ${appCfg.SMTP_HOST}:${appCfg.SMTP_PORT}`);
.catch((err: Error) => {
console.error(`SMTP - Failed to connect to ${appCfg.SMTP_HOST}:${appCfg.SMTP_PORT} - ${err.message}`);
logger.error(err);
});

@ -0,0 +1,21 @@
import opentelemetry from "@opentelemetry/api";
import fp from "fastify-plugin";
export const apiMetrics = fp(async (fastify) => {
const apiMeter = opentelemetry.metrics.getMeter("API");
const latencyHistogram = apiMeter.createHistogram("API_latency", {
unit: "ms"
});
fastify.addHook("onResponse", async (request, reply) => {
const { method } = request;
const route = request.routerPath;
const { statusCode } = reply;
latencyHistogram.record(reply.elapsedTime, {
route,
method,
statusCode
});
});
});

@ -1,4 +1,4 @@
import { ForbiddenError } from "@casl/ability";
import { ForbiddenError, PureAbility } from "@casl/ability";
import fastifyPlugin from "fastify-plugin";
import jwt from "jsonwebtoken";
import { ZodError } from "zod";
@ -10,6 +10,7 @@ import {
GatewayTimeoutError,
InternalServerError,
NotFoundError,
OidcAuthError,
RateLimitError,
ScimRequestError,
UnauthorizedError
@ -63,7 +64,13 @@ export const fastifyErrHandler = fastifyPlugin(async (server: FastifyZodProvider
void res.status(HttpStatusCodes.Forbidden).send({
statusCode: HttpStatusCodes.Forbidden,
error: "PermissionDenied",
message: `You are not allowed to ${error.action} on ${error.subjectType} - ${JSON.stringify(error.subject)}`
message: `You are not allowed to ${error.action} on ${error.subjectType}`,
details: (error.ability as PureAbility).rulesFor(error.action as string, error.subjectType).map((el) => ({
action: el.action,
inverted: el.inverted,
subject: el.subject,
conditions: el.conditions
}))
});
} else if (error instanceof ForbiddenRequestError) {
void res.status(HttpStatusCodes.Forbidden).send({
@ -83,7 +90,10 @@ export const fastifyErrHandler = fastifyPlugin(async (server: FastifyZodProvider
status: error.status,
detail: error.detail
});
// Handle JWT errors and make them more human-readable for the end-user.
} else if (error instanceof OidcAuthError) {
void res
.status(HttpStatusCodes.InternalServerError)
.send({ statusCode: HttpStatusCodes.InternalServerError, message: error.message, error: error.name });
} else if (error instanceof jwt.JsonWebTokenError) {
const message = (() => {
if (error.message === JWTErrors.JwtExpired) {

@ -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";
@ -198,6 +201,8 @@ import { getServerCfg, superAdminServiceFactory } from "@app/services/super-admi
import { telemetryDALFactory } from "@app/services/telemetry/telemetry-dal";
import { telemetryQueueServiceFactory } from "@app/services/telemetry/telemetry-queue";
import { telemetryServiceFactory } from "@app/services/telemetry/telemetry-service";
import { totpConfigDALFactory } from "@app/services/totp/totp-config-dal";
import { totpServiceFactory } from "@app/services/totp/totp-service";
import { userDALFactory } from "@app/services/user/user-dal";
import { userServiceFactory } from "@app/services/user/user-service";
import { userAliasDALFactory } from "@app/services/user-alias/user-alias-dal";
@ -221,10 +226,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" });
@ -337,9 +350,12 @@ export const registerRoutes = async (
const slackIntegrationDAL = slackIntegrationDALFactory(db);
const projectSlackConfigDAL = projectSlackConfigDALFactory(db);
const workflowIntegrationDAL = workflowIntegrationDALFactory(db);
const totpConfigDAL = totpConfigDALFactory(db);
const externalGroupOrgRoleMappingDAL = externalGroupOrgRoleMappingDALFactory(db);
const projectTemplateDAL = projectTemplateDALFactory(db);
const permissionService = permissionServiceFactory({
permissionDAL,
orgRoleDAL,
@ -348,14 +364,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,
@ -491,12 +514,19 @@ export const registerRoutes = async (
projectMembershipDAL
});
const loginService = authLoginServiceFactory({ userDAL, smtpService, tokenService, orgDAL });
const totpService = totpServiceFactory({
totpConfigDAL,
userDAL,
kmsService
});
const loginService = authLoginServiceFactory({ userDAL, smtpService, tokenService, orgDAL, totpService });
const passwordService = authPaswordServiceFactory({
tokenService,
smtpService,
authDAL,
userDAL
userDAL,
totpConfigDAL
});
const projectBotService = projectBotServiceFactory({ permissionService, projectBotDAL, projectDAL });
@ -552,6 +582,7 @@ export const registerRoutes = async (
userDAL,
authService: loginService,
serverCfgDAL: superAdminDAL,
kmsRootConfigDAL,
orgService,
keyStore,
licenseService,
@ -732,6 +763,12 @@ export const registerRoutes = async (
permissionService
});
const projectTemplateService = projectTemplateServiceFactory({
licenseService,
permissionService,
projectTemplateDAL
});
const projectService = projectServiceFactory({
permissionService,
projectDAL,
@ -758,7 +795,8 @@ export const registerRoutes = async (
projectBotDAL,
certificateTemplateDAL,
projectSlackConfigDAL,
slackIntegrationDAL
slackIntegrationDAL,
projectTemplateService
});
const projectEnvService = projectEnvServiceFactory({
@ -1250,10 +1288,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 +1372,15 @@ 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,
totp: totpService
});
const cronJobs: CronJob[] = [];

@ -47,6 +47,7 @@ export const DefaultResponseErrorsSchema = {
403: z.object({
statusCode: z.literal(403),
message: z.string(),
details: z.any().optional(),
error: z.string()
}),
500: z.object({

@ -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";
@ -195,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",

@ -108,7 +108,8 @@ export const registerAuthRoutes = async (server: FastifyZodProvider) => {
tokenVersionId: tokenVersion.id,
accessVersion: tokenVersion.accessVersion,
organizationId: decodedToken.organizationId,
isMfaVerified: decodedToken.isMfaVerified
isMfaVerified: decodedToken.isMfaVerified,
mfaMethod: decodedToken.mfaMethod
},
appCfg.AUTH_SECRET,
{ expiresIn: appCfg.JWT_AUTH_LIFETIME }

@ -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: {

@ -840,4 +840,91 @@ export const registerDashboardRouter = async (server: FastifyZodProvider) => {
};
}
});
server.route({
method: "GET",
url: "/secrets-by-keys",
config: {
rateLimit: secretsLimit
},
schema: {
security: [
{
bearerAuth: []
}
],
querystring: z.object({
projectId: z.string().trim(),
environment: z.string().trim(),
secretPath: z.string().trim().default("/").transform(removeTrailingSlash),
keys: z.string().trim().transform(decodeURIComponent)
}),
response: {
200: z.object({
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, environment } = req.query;
const keys = req.query.keys?.split(",").filter((key) => Boolean(key.trim())) ?? [];
if (!keys.length) throw new BadRequestError({ message: "One or more keys required" });
const { secrets } = await server.services.secret.getSecretsRaw({
actorId: req.permission.id,
actor: req.permission.type,
actorOrgId: req.permission.orgId,
environment,
actorAuthMethod: req.permission.authMethod,
projectId,
path: secretPath,
keys
});
await server.services.auditLog.createAuditLog({
projectId,
...req.auditLogInfo,
event: {
type: EventType.GET_SECRETS,
metadata: {
environment,
secretPath,
numberOfSecrets: secrets.length
}
}
});
if (getUserAgentType(req.headers["user-agent"]) !== UserAgentType.K8_OPERATOR) {
await server.services.telemetry.sendPostHogEvents({
event: PostHogEventTypes.SecretPulled,
distinctId: getTelemetryDistinctId(req),
properties: {
numberOfSecrets: secrets.length,
workspaceId: projectId,
environment,
secretPath,
channel: getUserAgentType(req.headers["user-agent"]),
...req.auditLogInfo
}
});
}
return { secrets };
}
});
};

@ -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",

@ -15,7 +15,7 @@ import { AUDIT_LOGS, ORGANIZATIONS } from "@app/lib/api-docs";
import { getLastMidnightDateISO } from "@app/lib/fn";
import { readLimit, writeLimit } from "@app/server/config/rateLimiter";
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
import { ActorType, AuthMode } from "@app/services/auth/auth-type";
import { ActorType, AuthMode, MfaMethod } from "@app/services/auth/auth-type";
import { integrationAuthPubSchema } from "../sanitizedSchemas";
@ -259,7 +259,8 @@ export const registerOrgRouter = async (server: FastifyZodProvider) => {
message: "Membership role must be a valid slug"
})
.optional(),
enforceMfa: z.boolean().optional()
enforceMfa: z.boolean().optional(),
selectedMfaMethod: z.nativeEnum(MfaMethod).optional()
}),
response: {
200: z.object({

@ -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()

@ -169,4 +169,103 @@ export const registerUserRouter = async (server: FastifyZodProvider) => {
return groupMemberships;
}
});
server.route({
method: "GET",
url: "/me/totp",
config: {
rateLimit: readLimit
},
schema: {
response: {
200: z.object({
isVerified: z.boolean(),
recoveryCodes: z.string().array()
})
}
},
onRequest: verifyAuth([AuthMode.JWT]),
handler: async (req) => {
return server.services.totp.getUserTotpConfig({
userId: req.permission.id
});
}
});
server.route({
method: "DELETE",
url: "/me/totp",
config: {
rateLimit: writeLimit
},
onRequest: verifyAuth([AuthMode.JWT]),
handler: async (req) => {
return server.services.totp.deleteUserTotpConfig({
userId: req.permission.id
});
}
});
server.route({
method: "POST",
url: "/me/totp/register",
config: {
rateLimit: writeLimit
},
schema: {
response: {
200: z.object({
otpUrl: z.string(),
recoveryCodes: z.string().array()
})
}
},
onRequest: verifyAuth([AuthMode.JWT], {
requireOrg: false
}),
handler: async (req) => {
return server.services.totp.registerUserTotp({
userId: req.permission.id
});
}
});
server.route({
method: "POST",
url: "/me/totp/verify",
config: {
rateLimit: writeLimit
},
schema: {
body: z.object({
totp: z.string()
}),
response: {
200: z.object({})
}
},
onRequest: verifyAuth([AuthMode.JWT], {
requireOrg: false
}),
handler: async (req) => {
return server.services.totp.verifyUserTotpConfig({
userId: req.permission.id,
totp: req.body.totp
});
}
});
server.route({
method: "POST",
url: "/me/totp/recovery-codes",
config: {
rateLimit: writeLimit
},
onRequest: verifyAuth([AuthMode.JWT]),
handler: async (req) => {
return server.services.totp.createUserTotpRecoveryCodes({
userId: req.permission.id
});
}
});
};

@ -2,8 +2,9 @@ import jwt from "jsonwebtoken";
import { z } from "zod";
import { getConfig } from "@app/lib/config/env";
import { BadRequestError, NotFoundError } from "@app/lib/errors";
import { mfaRateLimit } from "@app/server/config/rateLimiter";
import { AuthModeMfaJwtTokenPayload, AuthTokenType } from "@app/services/auth/auth-type";
import { AuthModeMfaJwtTokenPayload, AuthTokenType, MfaMethod } from "@app/services/auth/auth-type";
export const registerMfaRouter = async (server: FastifyZodProvider) => {
const cfg = getConfig();
@ -49,6 +50,38 @@ export const registerMfaRouter = async (server: FastifyZodProvider) => {
}
});
server.route({
method: "GET",
url: "/mfa/check/totp",
config: {
rateLimit: mfaRateLimit
},
schema: {
response: {
200: z.object({
isVerified: z.boolean()
})
}
},
handler: async (req) => {
try {
const totpConfig = await server.services.totp.getUserTotpConfig({
userId: req.mfa.userId
});
return {
isVerified: Boolean(totpConfig)
};
} catch (error) {
if (error instanceof NotFoundError || error instanceof BadRequestError) {
return { isVerified: false };
}
throw error;
}
}
});
server.route({
url: "/mfa/verify",
method: "POST",
@ -57,7 +90,8 @@ export const registerMfaRouter = async (server: FastifyZodProvider) => {
},
schema: {
body: z.object({
mfaToken: z.string().trim()
mfaToken: z.string().trim(),
mfaMethod: z.nativeEnum(MfaMethod).optional().default(MfaMethod.EMAIL)
}),
response: {
200: z.object({
@ -86,7 +120,8 @@ export const registerMfaRouter = async (server: FastifyZodProvider) => {
ip: req.realIp,
userId: req.mfa.userId,
orgId: req.mfa.orgId,
mfaToken: req.body.mfaToken
mfaToken: req.body.mfaToken,
mfaMethod: req.body.mfaMethod
});
void res.setCookie("jid", token.refresh, {

@ -27,7 +27,7 @@ export const registerProjectMembershipRouter = async (server: FastifyZodProvider
body: z.object({
emails: z.string().email().array().default([]).describe(PROJECT_USERS.INVITE_MEMBER.emails),
usernames: z.string().array().default([]).describe(PROJECT_USERS.INVITE_MEMBER.usernames),
roleSlugs: z.string().array().optional().describe(PROJECT_USERS.INVITE_MEMBER.roleSlugs)
roleSlugs: z.string().array().min(1).optional().describe(PROJECT_USERS.INVITE_MEMBER.roleSlugs)
}),
response: {
200: z.object({
@ -49,7 +49,7 @@ export const registerProjectMembershipRouter = async (server: FastifyZodProvider
projects: [
{
id: req.params.projectId,
projectRoleSlug: [ProjectMembershipRole.Member]
projectRoleSlug: req.body.roleSlugs || [ProjectMembershipRole.Member]
}
]
});

@ -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 };
}
});

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