Compare commits

..

203 Commits

Author SHA1 Message Date
e8d19eb823 improvement: disable tooltip hover content for env name tooltip 2025-06-26 09:12:11 -07:00
5d30215ea7 improvement: increase env tooltip max width and adjust alignment 2025-06-26 07:56:47 -07:00
86c145301e improvement: add collapsed environment view to secret overview page and minor ui adjustments 2025-06-25 16:49:34 -07:00
b59fa14bb6 Merge pull request #3818 from Infisical/feat/cli-bootstrap-create-k8-secret
feat: added auto-bootstrap support to helm
2025-06-24 17:03:13 -04:00
0eb36d7e35 misc: final doc changes 2025-06-24 20:56:06 +00:00
ae2da0066a misc: add helm chart auto bootstrap to methods 2025-06-25 04:40:07 +08:00
1d7da56b40 misc: used kubernetes client 2025-06-25 02:38:51 +08:00
3d2465ae41 Merge pull request #3825 from Infisical/feat/add-cloudflare-app-connection-and-sync
feat: added cloudflare app connection and secret sync
2025-06-25 00:44:58 +08:00
f4f34802bc Merge pull request #3816 from Infisical/fix/addProjectSlugToSecretsV3
Add projectSlug parameter on secrets v3 endpoints
2025-06-24 13:28:23 -03:00
59cc857aef fix: further improve inconsistencies 2025-06-24 19:37:32 +04:00
a6713b2f76 Merge pull request #3846 from Infisical/daniel/multiple-folders
fix(folders): duplicate folders
2025-06-24 19:04:26 +04:00
3c9a7c77ff chore: re-add comment 2025-06-24 18:58:03 +04:00
f1bfea61d0 fix: replace keystore lock with postgres lock 2025-06-24 18:54:18 +04:00
144ad2f25f misc: added image for generated token 2025-06-24 14:51:11 +00:00
02a2309953 misc: added note for bootstrap output flag 2025-06-24 18:26:17 +08:00
f1587d8375 misc: addressed comments 2025-06-24 18:18:07 +08:00
42aaddccd5 Lint fix 2025-06-23 23:13:29 -03:00
39abeaaab5 Small fix on workspaceId variable definition on secret-router 2025-06-23 23:05:12 -03:00
b336c0c3d6 Update secret-folder-service.ts 2025-06-24 03:33:45 +04:00
305f2d79de remove unused path 2025-06-24 03:32:18 +04:00
d4a6faa92c fix(folders): multiple folders being created 2025-06-24 03:24:47 +04:00
4800e9c36e Address PR comments 2025-06-23 17:45:21 -03:00
842a2e9a06 Merge pull request #3834 from Infisical/misc/add-self-serve-for-github-app-connection-setup
misc: add self-serve for github app connection setup
2025-06-24 02:45:51 +08:00
de81d2d380 Merge pull request #3833 from akhilmhdh/feat/pg-queue
feat: migrated dynamic secret to pg queue and corrected service layer
2025-06-23 23:51:06 +05:30
=
f5d769fa05 feat: addressed review comments 2025-06-23 23:38:07 +05:30
b3ace353ce Merge pull request #3843 from Infisical/email-verify-more-aggressive-rate-limit
improvement(verify-endpoints): add more aggressive rate limiting to verify endpoints
2025-06-23 10:43:25 -07:00
48353ab201 Merge pull request #3842 from Infisical/sort-tax-id-dropdown
sort tax ID dropdown
2025-06-23 13:40:01 -04:00
2137d13157 improve key check operator 2025-06-23 10:36:09 -07:00
647e13d654 improvement: add more aggressive rate limiting to verify endpoints 2025-06-23 10:27:36 -07:00
bb2a933a39 sort tax ID dropdown 2025-06-23 13:26:54 -04:00
6f75debb9c Merge pull request #3841 from Infisical/daniel/fix-k8s-dynamic-secret-without-gateway
fix(dynamic-secrets/k8s): fix for SSL when not using gateway
2025-06-23 21:26:20 +04:00
90588bc3c9 fix(dynamic-secrets/k8s): fix for SSL when not using gateway 2025-06-23 21:18:15 +04:00
4a09fc5e63 Merge pull request #3840 from Infisical/doc/added-architecture-doc-for-cloud
doc: architecture for US and EU cloud
2025-06-24 00:53:54 +08:00
f0ec8c883f misc: addressed comments 2025-06-24 00:52:18 +08:00
8024d7448f misc: updated docs json 2025-06-23 22:18:50 +08:00
c65b79e00d Merge remote-tracking branch 'origin/main' into feat/add-cloudflare-app-connection-and-sync 2025-06-23 22:16:09 +08:00
f5238598aa misc: updated admin integration picture 2025-06-23 14:12:54 +00:00
982aa80092 misc: added tabs for admin integrations 2025-06-23 22:05:08 +08:00
b30706607f misc: changed from for to of 2025-06-23 21:13:59 +08:00
2a3d19dcb2 misc: finalized title 2025-06-23 19:31:19 +08:00
b4ff620b44 doc: removed specifics 2025-06-23 19:28:05 +08:00
23f1888123 misc: added mention of separated AWS accounts 2025-06-23 19:16:08 +08:00
7764f63299 misc: made terms consistent 2025-06-23 19:12:09 +08:00
cb3365afd4 misc: removed troubleshooting section 2025-06-23 19:08:36 +08:00
58705ffc3f doc: removed duplicate permission block 2025-06-23 19:03:50 +08:00
67e57d8993 doc: added mention of NAT 2025-06-23 19:00:45 +08:00
90ff13a6b5 doc: architecture for US and EU cloud 2025-06-23 18:49:26 +08:00
36145a15c1 Merge pull request #3838 from Infisical/docs-update
upgrade mintlify docs
2025-06-23 03:38:53 -04:00
4f64ed6b42 upgrade mintlify docs 2025-06-22 17:25:17 -07:00
d47959ca83 Merge pull request #3822 from Infisical/approval-ui-revisions
improvements(approval-workflows): Improve Approval Workflow Tables and Add Additional Functionality
2025-06-20 15:25:19 -07:00
3b2953ca58 chore: revert license 2025-06-20 12:37:24 -07:00
1daa503e0e improvement: add space to users/groups list label 2025-06-20 12:34:20 -07:00
d69e8d2a8d deconflict merge 2025-06-20 12:33:37 -07:00
7c7af347fc improvements: address feedback and fix bugs 2025-06-20 12:25:28 -07:00
f85efdc6f8 misc: add auto-sync after config update 2025-06-21 02:57:34 +08:00
8680c52412 Merge branch 'misc/add-self-serve-for-github-app-connection-setup' of https://github.com/Infisical/infisical into misc/add-self-serve-for-github-app-connection-setup 2025-06-21 02:41:39 +08:00
0ad3c67f82 misc: minor renames 2025-06-21 02:41:15 +08:00
f75fff0565 doc: add image 2025-06-20 18:31:36 +00:00
1fa1d0a15a misc: add self-serve for github connection setup 2025-06-21 02:23:20 +08:00
e5a967b918 Update license-fns.ts 2025-06-20 23:50:03 +05:30
=
3cfe2223b6 feat: migrated dynamic secret to pg queue and corrected service layer types to non infer version 2025-06-20 23:32:40 +05:30
a43d4fd430 addressed greptie 2025-06-20 21:02:09 +08:00
80b6fb677c misc: addressed url issue 2025-06-20 20:52:00 +08:00
5bc8acd0a7 doc: added api references 2025-06-20 20:46:31 +08:00
2575845df7 misc: added images to secret sync doc 2025-06-20 12:36:39 +00:00
641d58c157 misc: addressed sync overflow issue 2025-06-20 20:23:03 +08:00
430f5d516c misc: text updates to secret sync 2025-06-20 20:20:10 +08:00
5cec194e74 misc: initial cloudflare pages sync doc 2025-06-20 20:17:02 +08:00
5ede4f6f4b misc: added placeholder for account ID 2025-06-20 20:08:07 +08:00
4d3581f835 doc: added assets for app connection 2025-06-20 12:07:21 +00:00
665f7fa5c3 misc: updated account ID 2025-06-20 19:50:03 +08:00
9f4b1d2565 image path updates 2025-06-20 19:42:22 +08:00
59e2a20180 misc: addressed minor issues 2025-06-20 19:39:33 +08:00
4fee5a5839 doc: added initial app connection doc 2025-06-20 19:36:27 +08:00
61e245ea58 Merge remote-tracking branch 'origin/main' into feat/add-cloudflare-app-connection-and-sync 2025-06-20 19:24:45 +08:00
8d6712aa58 Merge pull request #3824 from Infisical/doc/add-helm-install-for-pki-issuer
doc: add mention of helm install for pki issuer
2025-06-20 19:20:19 +08:00
a767870ad6 Merge pull request #3813 from akhilmhdh/patch/min-knex
feat: added min 0 for knexjs pool
2025-06-19 21:16:08 -04:00
a0c432628a Merge pull request #3831 from Infisical/docs/fix-broken-link
Docs links fix
2025-06-19 21:15:22 -04:00
08a74a63b5 Docs links fix 2025-06-19 21:10:58 -04:00
8329240822 Merge pull request #3821 from Infisical/ENG-2832
feat(dynamic-secret): Github App Tokens
2025-06-19 21:03:46 -04:00
ec3cbb9460 Merge pull request #3830 from Infisical/revert-cli-refresh
Revert CLI refresh PR
2025-06-19 20:58:11 -04:00
f167ba0fb8 Revert "Merge pull request #3797 from Infisical/ENG-2690"
This reverts commit 7d90d183fb, reversing
changes made to f385386a4b.
2025-06-19 20:46:55 -04:00
f291aa1c01 Merge pull request #3829 from Infisical/fix/cli-jwt-issue
Revert back to `RefreshToken` from `refreshToken` to support older CLI versions
2025-06-19 19:41:31 -04:00
72131373ec Merge branch 'main' into fix/cli-jwt-issue 2025-06-19 19:19:12 -04:00
16c48de031 refreshToken -> RefreshToken 2025-06-19 19:18:02 -04:00
436a5afab5 Merge pull request #3828 from Infisical/fix/cli-jwt-issue 2025-06-19 19:01:17 -04:00
9445f717f4 Revert back to JTWToken 2025-06-19 18:55:41 -04:00
251e83a3fb Merge pull request #3827 from Infisical/fix/cli-jwt-issue
Fix CLI issue
2025-06-19 17:33:37 -04:00
66df285245 Improvements 2025-06-19 17:26:58 -04:00
73fe2659b5 Fix CLI issue 2025-06-19 17:17:10 -04:00
091f02d1cd Merge pull request #3826 from akhilmhdh/feat/aws-auth-increase-limit
feat: patched up approval sequence ui bugs
2025-06-19 14:15:54 -07:00
57e97a146b feat: cloudflare pages secret sync 2025-06-20 03:43:36 +08:00
66140dc151 Merge pull request #3809 from Infisical/feat/dynamicSecretAwsIamCustomTags
feat(dynamic-secret): Add custom tags to AWS IAM dynamic secret
2025-06-19 16:42:53 -03:00
a8c54d27ef remove debug console logs 2025-06-19 16:19:02 -03:00
9ac4453523 Review fixes 2025-06-19 15:12:41 -04:00
=
a6a9c2404d feat: patched up approval sequence ui bugs 2025-06-20 00:12:49 +05:30
e5352e7aa8 Merge pull request #3806 from Infisical/feat/addHerokuSecretSync
feat(secret-sync): Add Heroku Secret Sync
2025-06-19 15:28:56 -03:00
c52180c890 feat(secret-sync): minor fix on heroku docs 2025-06-19 15:17:36 -03:00
20f0eeed35 Moved tags to aws iam provider inputs 2025-06-19 15:01:35 -03:00
d2c7ed62d0 feat: added cloudflare app connection 2025-06-20 01:16:56 +08:00
7e9743b4c2 improvement: standardize and update server side pagination for change requests 2025-06-19 09:39:42 -07:00
34cf544b3a fix: correct empty state/search logic 2025-06-19 09:39:42 -07:00
12fd063cd5 improvements: minor ui adjustments/additions and pagination for access request table 2025-06-19 09:39:42 -07:00
8fb6063686 improvement: better badge color 2025-06-19 09:39:42 -07:00
459b262865 improvements: improve approval tables UI and add additional functionality 2025-06-19 09:39:42 -07:00
7581300a67 feat(secret-sync): minor fix on heroku sync 2025-06-19 13:38:20 -03:00
7d90d183fb Merge pull request #3797 from Infisical/ENG-2690
feat: Lower token lifetime to 1 day (refresh 14 days) and fix CLI refresh token functionality
2025-06-19 12:05:24 -04:00
f27d4ee973 doc: add mention of helm install for pki issuer 2025-06-19 22:41:39 +08:00
470d7cca6a misc: updated chart version 2025-06-19 20:57:42 +08:00
7473e3e21e Add Heroku PR suggestions 2025-06-19 09:28:43 -03:00
8e3918ada3 misc: addressed tag issue for CLI 2025-06-19 20:20:53 +08:00
6720217cee Merge remote-tracking branch 'origin/main' into feat/addHerokuSecretSync 2025-06-19 08:47:03 -03:00
f385386a4b Merge pull request #3823 from akhilmhdh/feat/aws-auth-increase-limit
feat: resolved okta oidc failing
2025-06-19 07:06:21 -04:00
=
62a0d6e614 feat: corrected the error message 2025-06-19 16:10:15 +05:30
=
8c64c731f9 feat: added additional validation for name 2025-06-19 16:09:22 +05:30
=
d51f6ca4fd feat: resolved okta oidc failing 2025-06-19 16:04:55 +05:30
5abcbe36ca Update oncall-summery-template.mdx 2025-06-18 18:51:48 -04:00
7a13c27055 Greptile review comments and lint 2025-06-18 18:41:58 -04:00
e7ac783b10 feat(dynamic-secret): Github App Tokens 2025-06-18 18:33:11 -04:00
0a509e5033 Merge pull request #3791 from Infisical/feat/add-render-app-connection-and-secret-sync
feat: render app connection and secret sync
2025-06-19 04:49:01 +08:00
bd54054bc3 misc: enabled auto bootstrap for check 2025-06-19 03:53:57 +08:00
cfe51d4a52 misc: improved template dcs 2025-06-19 03:50:56 +08:00
d0c01755fe misc: addressed type issue 2025-06-19 03:29:42 +08:00
41e65775ab misc: addressed comments 2025-06-19 03:24:32 +08:00
e3f4a2e604 Merge pull request #3819 from akhilmhdh/feat/aws-auth-increase-limit
fix: resolved failing duplication of predefined roles
2025-06-19 00:49:18 +05:30
f6e6bdb691 Merge remote-tracking branch 'origin/main' into feat/add-render-app-connection-and-secret-sync 2025-06-19 03:14:23 +08:00
=
819a021e9c feat: corrected enum usage 2025-06-19 00:05:40 +05:30
=
80113c2cea fix: resolved failing duplication of predefined roles 2025-06-19 00:02:17 +05:30
9cdd7380df misc: greptie 2025-06-19 02:30:26 +08:00
07d491acd1 misc: corrected template doc 2025-06-19 02:26:13 +08:00
3276853427 misc: added helm support for auto bootstrap 2025-06-19 02:12:08 +08:00
1f1fb3f3d1 Merge pull request #3817 from akhilmhdh/feat/aws-auth-increase-limit
fix: updated aws principal arn field size to 2048
2025-06-18 23:21:59 +05:30
a8eb72a8c5 Fix type issue 2025-06-18 14:48:29 -03:00
2b8220a71b feat: added support for outputting bootstrap credentials to k8 secret 2025-06-19 01:43:47 +08:00
f76d3e2a14 Add projectSlug parameter on secrets v3 endpoints 2025-06-18 14:35:49 -03:00
=
d35331b0a8 fix: updated aws principal arn field size to 2048 2025-06-18 23:00:52 +05:30
ff6d94cbd0 Merge pull request #3815 from Infisical/daniel/update-dotnet-docs
docs: update .net sdk
2025-06-18 18:55:09 +04:00
=
01ef498397 feat: added min 0 for knexjs pool 2025-06-18 15:16:07 +05:30
59ac14380a Merge pull request #3810 from Infisical/daniel/secret-syncs-permissions
feat(secret-syncs): better permissioning
2025-06-17 21:44:47 -04:00
7b5c86f4ef revert previous commit 2025-06-17 17:34:00 -07:00
a745be2546 improvements: remove secret permission checks from secret syncs 2025-06-17 17:28:21 -07:00
02f311515c feat(secret-sync): Add PR suggestions for Heroku Integration 2025-06-17 21:19:21 -03:00
e8cb3f8b4a improvements: fix secret sync policy parsing, add read checks/filters and disable ui based of conditions 2025-06-17 16:18:41 -07:00
4c8063c532 docs: update .net sdk 2025-06-18 01:51:33 +04:00
6a9b2d3d48 Merge pull request #3804 from Infisical/service-tokens-table-improvements
improvement(service-tokens): Improve Service Tokens Table
2025-06-17 14:15:07 -07:00
0a39e138a1 fix: move service token form to separate component to prevent reset issue 2025-06-17 14:10:48 -07:00
0dce2045ec Merge pull request #3802 from Infisical/ENG-2929
feat(secret-sync, app-connection): Fly.io Secret Sync + App Connection
2025-06-17 16:57:58 -04:00
b4c118d246 requested changes 2025-06-18 00:26:26 +04:00
90e675de1e docs(secret-syncs): add conditions support 2025-06-18 00:22:25 +04:00
741e0ec78f Fixed credential validation 2025-06-17 16:18:35 -04:00
3f654e115d feat(secret-syncs): better permissioning 2025-06-18 00:17:39 +04:00
1921346b4f Review fixes 2025-06-17 16:05:09 -04:00
76c95ace63 Merge branch 'main' into ENG-2929 2025-06-17 15:57:31 -04:00
f4ae40cb86 Merge pull request #3805 from Infisical/access-control-tab-consistency
improvement(project-access-control): minor UI adjustments for consistency
2025-06-17 12:47:11 -07:00
b790dbb36f feat(dynamic-secret): Add tags to AWS IAM docs and add aws key-value limits to the schema 2025-06-17 16:21:29 -03:00
14449b8b41 improvements: address feedback 2025-06-17 12:17:53 -07:00
489bd124d2 feat(dynamic-secret): Add custom tags to AWS IAM dynamic secret 2025-06-17 16:06:35 -03:00
bcdcaa33a4 Merge pull request #3807 from Infisical/conditional-dynamic-secret-access-display
improvement(access-tree): dynamic secret conditional display
2025-06-17 11:49:45 -07:00
e8a8542757 Merge pull request #3803 from Infisical/project-roles-table-improvements
improvement(project-roles): Add pagination, search and column sorting to Project Roles table
2025-06-17 11:49:31 -07:00
e61d35d824 Merge pull request #3808 from Infisical/daniel/fix-editing-value-hidden-secrets
fix: editing secrets with value hidden
2025-06-17 22:08:50 +04:00
714d6831bd Update SecretListView.tsx 2025-06-17 21:23:30 +04:00
956f75eb43 fix: editing secrets with value hidden 2025-06-17 21:02:47 +04:00
bfee34f38d Merge pull request #3801 from Infisical/doc/production-hardening
doc: production hardening
2025-06-17 22:10:22 +08:00
840b64a049 fix mint.json openapi url used for local test 2025-06-17 10:54:52 -03:00
c2612f242c feat(secret-sync): Add Heroku Secret Sync 2025-06-17 10:52:55 -03:00
092b89c59e misc: corrected kms section 2025-06-17 20:28:28 +08:00
3d76ae3399 misc: some more updates in examples 2025-06-17 20:25:38 +08:00
23aa97feff misc: addressed comments 2025-06-17 20:17:17 +08:00
0c5155f8e6 improvement: minor UI adjustments to make project access control tabs more uniform 2025-06-16 17:17:55 -07:00
796d6bfc85 improvement: add scope handling to service token search 2025-06-16 16:42:11 -07:00
4afe2f2377 improvements: use stored preferred page size for project roles table and add reset helper 2025-06-16 16:36:03 -07:00
6eaa16bd07 improvement: add pagination, search and column sort to service token table and improve table rows 2025-06-16 16:13:09 -07:00
1e07c2fe23 improvements: add sorting, search, and pagination to project roles table and improve dropdown menu 2025-06-16 15:00:40 -07:00
149f98a1b7 Update docs/integrations/secret-syncs/flyio.mdx
Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com>
2025-06-16 16:55:34 -04:00
14745b560c Update docs/integrations/app-connections/flyio.mdx
Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com>
2025-06-16 16:55:22 -04:00
dcfa0a2386 Update docs/integrations/secret-syncs/flyio.mdx
Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com>
2025-06-16 16:55:07 -04:00
199339ac32 Minor schema improvements 2025-06-16 16:28:09 -04:00
2aeb02b74a Fly.io secret sync & app connection docs 2025-06-16 16:26:54 -04:00
fe75627ab7 Fly.io secret sync 2025-06-16 15:49:42 -04:00
191486519f Merge branch 'doc/production-hardening' of https://github.com/Infisical/infisical into doc/production-hardening 2025-06-17 03:45:54 +08:00
cab8fb0d8e misc: reorganized 2025-06-17 03:45:35 +08:00
8bfd728ce4 misc: added mint json 2025-06-16 19:22:35 +00:00
c9eab0af18 misc: updated section on db 2025-06-17 03:21:26 +08:00
d7dfc531fc doc: added guide for production hardening 2025-06-17 03:20:11 +08:00
a89bd08c08 Merge pull request #3795 from Infisical/ENG-2928
feat(machine-identities): Delete protection
2025-06-16 14:57:45 -04:00
4bfb9e8e74 Merge pull request #3789 from Infisical/misc/add-custom-role-slug-in-fetch-group
misc: add custom role slug in fetch group
2025-06-16 22:40:37 +04:00
da5f054a65 Fly.io app connection 2025-06-16 14:08:42 -04:00
77fe2ffb3b Add error handling 2025-06-14 01:43:32 -04:00
edf4e75e55 Spelling fix "JTW" -> "JWT" 2025-06-14 01:38:29 -04:00
de917a5d74 Fix CLI refresh token functionality + reduce token lifetime to 1d & 14d
for refresh
2025-06-14 01:31:44 -04:00
c12bfa766c Review fixes 2025-06-13 14:51:39 -04:00
3432a16d4f Update frontend/src/pages/organization/AccessManagementPage/components/OrgIdentityTab/components/IdentitySection/IdentityModal.tsx
Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com>
2025-06-13 14:45:38 -04:00
19a403f467 feat(machine-identities): Delete protection 2025-06-13 14:37:15 -04:00
ed1100bc90 misc: api references 2025-06-13 23:58:57 +08:00
dabe7e42ec misc: add deprecation for native render integration 2025-06-13 23:52:18 +08:00
c8ca6710ba misc: add secret sync docs 2025-06-13 15:48:59 +00:00
4ecb2eb383 doc: added docs for render app connection 2025-06-13 15:24:45 +00:00
e51278c276 misc: added max length to apiKey 2025-06-13 23:03:04 +08:00
c014c12ecb misc: addressed frontend lint 2025-06-13 23:01:09 +08:00
097b04afee misc: addressed type 2025-06-13 22:59:08 +08:00
63ccfc40ac feat: added render secret sync 2025-06-13 22:53:35 +08:00
f9c012387c feat: added render app connection 2025-06-13 20:14:24 +08:00
06a7e804eb misc: add custom role slug in fetch group 2025-06-13 17:26:36 +08:00
419 changed files with 14854 additions and 4378 deletions

View File

@ -51,11 +51,18 @@ jobs:
--from-literal=ENCRYPTION_KEY=6c1fe4e407b8911c104518103505b218 \ --from-literal=ENCRYPTION_KEY=6c1fe4e407b8911c104518103505b218 \
--from-literal=SITE_URL=http://localhost:8080 --from-literal=SITE_URL=http://localhost:8080
- name: Create bootstrap secret
run: |
kubectl create secret generic infisical-bootstrap-credentials \
--namespace infisical-standalone-postgres \
--from-literal=INFISICAL_ADMIN_EMAIL=admin@example.com \
--from-literal=INFISICAL_ADMIN_PASSWORD=admin-password
- name: Run chart-testing (install) - name: Run chart-testing (install)
run: | run: |
ct install \ ct install \
--config ct.yaml \ --config ct.yaml \
--charts helm-charts/infisical-standalone-postgres \ --charts helm-charts/infisical-standalone-postgres \
--helm-extra-args="--timeout=300s" \ --helm-extra-args="--timeout=300s" \
--helm-extra-set-args="--set ingress.nginx.enabled=false --set infisical.autoDatabaseSchemaMigration=false --set infisical.replicaCount=1 --set infisical.image.tag=v0.132.2-postgres" \ --helm-extra-set-args="--set ingress.nginx.enabled=false --set infisical.autoDatabaseSchemaMigration=false --set infisical.replicaCount=1 --set infisical.image.tag=v0.132.2-postgres --set infisical.autoBootstrap.enabled=true" \
--namespace infisical-standalone-postgres --namespace infisical-standalone-postgres

View File

@ -45,3 +45,4 @@ cli/detect/config/gitleaks.toml:gcp-api-key:582
.github/workflows/helm-release-infisical-core.yml:generic-api-key:48 .github/workflows/helm-release-infisical-core.yml:generic-api-key:48
.github/workflows/helm-release-infisical-core.yml:generic-api-key:47 .github/workflows/helm-release-infisical-core.yml:generic-api-key:47
backend/src/services/smtp/smtp-service.ts:generic-api-key:79 backend/src/services/smtp/smtp-service.ts:generic-api-key:79
frontend/src/components/secret-syncs/forms/SecretSyncDestinationFields/CloudflarePagesSyncFields.tsx:cloudflare-api-key:7

View File

@ -26,6 +26,7 @@ export const mockQueue = (): TQueueServiceFactory => {
getRepeatableJobs: async () => [], getRepeatableJobs: async () => [],
clearQueue: async () => {}, clearQueue: async () => {},
stopJobById: async () => {}, stopJobById: async () => {},
stopJobByIdPg: async () => {},
stopRepeatableJobByJobId: async () => true, stopRepeatableJobByJobId: async () => true,
stopRepeatableJobByKey: async () => true stopRepeatableJobByKey: async () => true
}; };

View File

@ -10,8 +10,8 @@ import { TAuditLogServiceFactory, TCreateAuditLogDTO } from "@app/ee/services/au
import { TAuditLogStreamServiceFactory } from "@app/ee/services/audit-log-stream/audit-log-stream-types"; import { TAuditLogStreamServiceFactory } from "@app/ee/services/audit-log-stream/audit-log-stream-types";
import { TCertificateAuthorityCrlServiceFactory } from "@app/ee/services/certificate-authority-crl/certificate-authority-crl-types"; import { TCertificateAuthorityCrlServiceFactory } from "@app/ee/services/certificate-authority-crl/certificate-authority-crl-types";
import { TCertificateEstServiceFactory } from "@app/ee/services/certificate-est/certificate-est-service"; import { TCertificateEstServiceFactory } from "@app/ee/services/certificate-est/certificate-est-service";
import { TDynamicSecretServiceFactory } from "@app/ee/services/dynamic-secret/dynamic-secret-service"; import { TDynamicSecretServiceFactory } from "@app/ee/services/dynamic-secret/dynamic-secret-types";
import { TDynamicSecretLeaseServiceFactory } from "@app/ee/services/dynamic-secret-lease/dynamic-secret-lease-service"; import { TDynamicSecretLeaseServiceFactory } from "@app/ee/services/dynamic-secret-lease/dynamic-secret-lease-types";
import { TExternalKmsServiceFactory } from "@app/ee/services/external-kms/external-kms-service"; import { TExternalKmsServiceFactory } from "@app/ee/services/external-kms/external-kms-service";
import { TGatewayServiceFactory } from "@app/ee/services/gateway/gateway-service"; import { TGatewayServiceFactory } from "@app/ee/services/gateway/gateway-service";
import { TGithubOrgSyncServiceFactory } from "@app/ee/services/github-org-sync/github-org-sync-service"; import { TGithubOrgSyncServiceFactory } from "@app/ee/services/github-org-sync/github-org-sync-service";

View File

@ -50,6 +50,8 @@ export const initDbConnection = ({
} }
: false : false
}, },
// https://knexjs.org/guide/#pool
pool: { min: 0, max: 10 },
migrations: { migrations: {
tableName: "infisical_migrations" tableName: "infisical_migrations"
} }
@ -70,7 +72,8 @@ export const initDbConnection = ({
}, },
migrations: { migrations: {
tableName: "infisical_migrations" tableName: "infisical_migrations"
} },
pool: { min: 0, max: 10 }
}); });
}); });

View File

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

View File

@ -0,0 +1,21 @@
import { Knex } from "knex";
import { TableName } from "../schemas";
export async function up(knex: Knex): Promise<void> {
const hasColumn = await knex.schema.hasColumn(TableName.IdentityAwsAuth, "allowedPrincipalArns");
if (hasColumn) {
await knex.schema.alterTable(TableName.IdentityAwsAuth, (t) => {
t.string("allowedPrincipalArns", 2048).notNullable().alter();
});
}
}
export async function down(knex: Knex): Promise<void> {
const hasColumn = await knex.schema.hasColumn(TableName.IdentityAwsAuth, "allowedPrincipalArns");
if (hasColumn) {
await knex.schema.alterTable(TableName.IdentityAwsAuth, (t) => {
t.string("allowedPrincipalArns", 255).notNullable().alter();
});
}
}

View File

@ -0,0 +1,91 @@
import { Knex } from "knex";
import { TableName } from "../schemas";
export async function up(knex: Knex): Promise<void> {
const hasEncryptedGithubAppConnectionClientIdColumn = await knex.schema.hasColumn(
TableName.SuperAdmin,
"encryptedGitHubAppConnectionClientId"
);
const hasEncryptedGithubAppConnectionClientSecretColumn = await knex.schema.hasColumn(
TableName.SuperAdmin,
"encryptedGitHubAppConnectionClientSecret"
);
const hasEncryptedGithubAppConnectionSlugColumn = await knex.schema.hasColumn(
TableName.SuperAdmin,
"encryptedGitHubAppConnectionSlug"
);
const hasEncryptedGithubAppConnectionAppIdColumn = await knex.schema.hasColumn(
TableName.SuperAdmin,
"encryptedGitHubAppConnectionId"
);
const hasEncryptedGithubAppConnectionAppPrivateKeyColumn = await knex.schema.hasColumn(
TableName.SuperAdmin,
"encryptedGitHubAppConnectionPrivateKey"
);
await knex.schema.alterTable(TableName.SuperAdmin, (t) => {
if (!hasEncryptedGithubAppConnectionClientIdColumn) {
t.binary("encryptedGitHubAppConnectionClientId").nullable();
}
if (!hasEncryptedGithubAppConnectionClientSecretColumn) {
t.binary("encryptedGitHubAppConnectionClientSecret").nullable();
}
if (!hasEncryptedGithubAppConnectionSlugColumn) {
t.binary("encryptedGitHubAppConnectionSlug").nullable();
}
if (!hasEncryptedGithubAppConnectionAppIdColumn) {
t.binary("encryptedGitHubAppConnectionId").nullable();
}
if (!hasEncryptedGithubAppConnectionAppPrivateKeyColumn) {
t.binary("encryptedGitHubAppConnectionPrivateKey").nullable();
}
});
}
export async function down(knex: Knex): Promise<void> {
const hasEncryptedGithubAppConnectionClientIdColumn = await knex.schema.hasColumn(
TableName.SuperAdmin,
"encryptedGitHubAppConnectionClientId"
);
const hasEncryptedGithubAppConnectionClientSecretColumn = await knex.schema.hasColumn(
TableName.SuperAdmin,
"encryptedGitHubAppConnectionClientSecret"
);
const hasEncryptedGithubAppConnectionSlugColumn = await knex.schema.hasColumn(
TableName.SuperAdmin,
"encryptedGitHubAppConnectionSlug"
);
const hasEncryptedGithubAppConnectionAppIdColumn = await knex.schema.hasColumn(
TableName.SuperAdmin,
"encryptedGitHubAppConnectionId"
);
const hasEncryptedGithubAppConnectionAppPrivateKeyColumn = await knex.schema.hasColumn(
TableName.SuperAdmin,
"encryptedGitHubAppConnectionPrivateKey"
);
await knex.schema.alterTable(TableName.SuperAdmin, (t) => {
if (hasEncryptedGithubAppConnectionClientIdColumn) {
t.dropColumn("encryptedGitHubAppConnectionClientId");
}
if (hasEncryptedGithubAppConnectionClientSecretColumn) {
t.dropColumn("encryptedGitHubAppConnectionClientSecret");
}
if (hasEncryptedGithubAppConnectionSlugColumn) {
t.dropColumn("encryptedGitHubAppConnectionSlug");
}
if (hasEncryptedGithubAppConnectionAppIdColumn) {
t.dropColumn("encryptedGitHubAppConnectionId");
}
if (hasEncryptedGithubAppConnectionAppPrivateKeyColumn) {
t.dropColumn("encryptedGitHubAppConnectionPrivateKey");
}
});
}

View File

@ -12,7 +12,8 @@ export const IdentitiesSchema = z.object({
name: z.string(), name: z.string(),
authMethod: z.string().nullable().optional(), authMethod: z.string().nullable().optional(),
createdAt: z.date(), createdAt: z.date(),
updatedAt: z.date() updatedAt: z.date(),
hasDeleteProtection: z.boolean().default(false)
}); });
export type TIdentities = z.infer<typeof IdentitiesSchema>; export type TIdentities = z.infer<typeof IdentitiesSchema>;

View File

@ -29,7 +29,12 @@ export const SuperAdminSchema = z.object({
adminIdentityIds: z.string().array().nullable().optional(), adminIdentityIds: z.string().array().nullable().optional(),
encryptedMicrosoftTeamsAppId: zodBuffer.nullable().optional(), encryptedMicrosoftTeamsAppId: zodBuffer.nullable().optional(),
encryptedMicrosoftTeamsClientSecret: zodBuffer.nullable().optional(), encryptedMicrosoftTeamsClientSecret: zodBuffer.nullable().optional(),
encryptedMicrosoftTeamsBotId: zodBuffer.nullable().optional() encryptedMicrosoftTeamsBotId: zodBuffer.nullable().optional(),
encryptedGitHubAppConnectionClientId: zodBuffer.nullable().optional(),
encryptedGitHubAppConnectionClientSecret: zodBuffer.nullable().optional(),
encryptedGitHubAppConnectionSlug: zodBuffer.nullable().optional(),
encryptedGitHubAppConnectionId: zodBuffer.nullable().optional(),
encryptedGitHubAppConnectionPrivateKey: zodBuffer.nullable().optional()
}); });
export type TSuperAdmin = z.infer<typeof SuperAdminSchema>; export type TSuperAdmin = z.infer<typeof SuperAdminSchema>;

View File

@ -89,7 +89,7 @@ export const registerAccessApprovalRequestRouter = async (server: FastifyZodProv
schema: { schema: {
querystring: z.object({ querystring: z.object({
projectSlug: z.string().trim(), projectSlug: z.string().trim(),
authorProjectMembershipId: z.string().trim().optional(), authorUserId: z.string().trim().optional(),
envSlug: z.string().trim().optional() envSlug: z.string().trim().optional()
}), }),
response: { response: {
@ -143,7 +143,7 @@ export const registerAccessApprovalRequestRouter = async (server: FastifyZodProv
handler: async (req) => { handler: async (req) => {
const { requests } = await server.services.accessApprovalRequest.listApprovalRequests({ const { requests } = await server.services.accessApprovalRequest.listApprovalRequests({
projectSlug: req.query.projectSlug, projectSlug: req.query.projectSlug,
authorProjectMembershipId: req.query.authorProjectMembershipId, authorUserId: req.query.authorUserId,
envSlug: req.query.envSlug, envSlug: req.query.envSlug,
actor: req.permission.type, actor: req.permission.type,
actorId: req.permission.id, actorId: req.permission.id,

View File

@ -48,7 +48,9 @@ export const registerGroupRouter = async (server: FastifyZodProvider) => {
id: z.string().trim().describe(GROUPS.GET_BY_ID.id) id: z.string().trim().describe(GROUPS.GET_BY_ID.id)
}), }),
response: { response: {
200: GroupsSchema 200: GroupsSchema.extend({
customRoleSlug: z.string().nullable()
})
} }
}, },
handler: async (req) => { handler: async (req) => {

View File

@ -30,6 +30,7 @@ export const registerSecretApprovalRequestRouter = async (server: FastifyZodProv
workspaceId: z.string().trim(), workspaceId: z.string().trim(),
environment: z.string().trim().optional(), environment: z.string().trim().optional(),
committer: z.string().trim().optional(), committer: z.string().trim().optional(),
search: z.string().trim().optional(),
status: z.nativeEnum(RequestState).optional(), status: z.nativeEnum(RequestState).optional(),
limit: z.coerce.number().default(20), limit: z.coerce.number().default(20),
offset: z.coerce.number().default(0) offset: z.coerce.number().default(0)
@ -66,13 +67,14 @@ export const registerSecretApprovalRequestRouter = async (server: FastifyZodProv
userId: z.string().nullable().optional() userId: z.string().nullable().optional()
}) })
.array() .array()
}).array() }).array(),
totalCount: z.number()
}) })
} }
}, },
onRequest: verifyAuth([AuthMode.JWT]), onRequest: verifyAuth([AuthMode.JWT]),
handler: async (req) => { handler: async (req) => {
const approvals = await server.services.secretApprovalRequest.getSecretApprovals({ const { approvals, totalCount } = await server.services.secretApprovalRequest.getSecretApprovals({
actor: req.permission.type, actor: req.permission.type,
actorId: req.permission.id, actorId: req.permission.id,
actorAuthMethod: req.permission.authMethod, actorAuthMethod: req.permission.authMethod,
@ -80,7 +82,7 @@ export const registerSecretApprovalRequestRouter = async (server: FastifyZodProv
...req.query, ...req.query,
projectId: req.query.workspaceId projectId: req.query.workspaceId
}); });
return { approvals }; return { approvals, totalCount };
} }
}); });

View File

@ -725,16 +725,17 @@ export const accessApprovalRequestDALFactory = (db: TDbClient): TAccessApprovalR
) )
.where(`${TableName.Environment}.projectId`, projectId) .where(`${TableName.Environment}.projectId`, projectId)
.where(`${TableName.AccessApprovalPolicy}.deletedAt`, null)
.select(selectAllTableCols(TableName.AccessApprovalRequest)) .select(selectAllTableCols(TableName.AccessApprovalRequest))
.select(db.ref("status").withSchema(TableName.AccessApprovalRequestReviewer).as("reviewerStatus")) .select(db.ref("status").withSchema(TableName.AccessApprovalRequestReviewer).as("reviewerStatus"))
.select(db.ref("reviewerUserId").withSchema(TableName.AccessApprovalRequestReviewer).as("reviewerUserId")); .select(db.ref("reviewerUserId").withSchema(TableName.AccessApprovalRequestReviewer).as("reviewerUserId"))
.select(db.ref("deletedAt").withSchema(TableName.AccessApprovalPolicy).as("policyDeletedAt"));
const formattedRequests = sqlNestRelationships({ const formattedRequests = sqlNestRelationships({
data: accessRequests, data: accessRequests,
key: "id", key: "id",
parentMapper: (doc) => ({ parentMapper: (doc) => ({
...AccessApprovalRequestsSchema.parse(doc) ...AccessApprovalRequestsSchema.parse(doc),
isPolicyDeleted: Boolean(doc.policyDeletedAt)
}), }),
childrenMapper: [ childrenMapper: [
{ {
@ -751,7 +752,8 @@ export const accessApprovalRequestDALFactory = (db: TDbClient): TAccessApprovalR
(req) => (req) =>
!req.privilegeId && !req.privilegeId &&
!req.reviewers.some((r) => r.status === ApprovalStatus.REJECTED) && !req.reviewers.some((r) => r.status === ApprovalStatus.REJECTED) &&
req.status === ApprovalStatus.PENDING req.status === ApprovalStatus.PENDING &&
!req.isPolicyDeleted
); );
// an approval is finalized if there are any rejections, a privilege ID is set or the number of approvals is equal to the number of approvals required. // an approval is finalized if there are any rejections, a privilege ID is set or the number of approvals is equal to the number of approvals required.
@ -759,7 +761,8 @@ export const accessApprovalRequestDALFactory = (db: TDbClient): TAccessApprovalR
(req) => (req) =>
req.privilegeId || req.privilegeId ||
req.reviewers.some((r) => r.status === ApprovalStatus.REJECTED) || req.reviewers.some((r) => r.status === ApprovalStatus.REJECTED) ||
req.status !== ApprovalStatus.PENDING req.status !== ApprovalStatus.PENDING ||
req.isPolicyDeleted
); );
return { pendingCount: pendingApprovals.length, finalizedCount: finalizedApprovals.length }; return { pendingCount: pendingApprovals.length, finalizedCount: finalizedApprovals.length };

View File

@ -275,7 +275,7 @@ export const accessApprovalRequestServiceFactory = ({
const listApprovalRequests: TAccessApprovalRequestServiceFactory["listApprovalRequests"] = async ({ const listApprovalRequests: TAccessApprovalRequestServiceFactory["listApprovalRequests"] = async ({
projectSlug, projectSlug,
authorProjectMembershipId, authorUserId,
envSlug, envSlug,
actor, actor,
actorOrgId, actorOrgId,
@ -300,8 +300,8 @@ export const accessApprovalRequestServiceFactory = ({
const policies = await accessApprovalPolicyDAL.find({ projectId: project.id }); const policies = await accessApprovalPolicyDAL.find({ projectId: project.id });
let requests = await accessApprovalRequestDAL.findRequestsWithPrivilegeByPolicyIds(policies.map((p) => p.id)); let requests = await accessApprovalRequestDAL.findRequestsWithPrivilegeByPolicyIds(policies.map((p) => p.id));
if (authorProjectMembershipId) { if (authorUserId) {
requests = requests.filter((request) => request.requestedByUserId === actorId); requests = requests.filter((request) => request.requestedByUserId === authorUserId);
} }
if (envSlug) { if (envSlug) {

View File

@ -31,7 +31,7 @@ export type TCreateAccessApprovalRequestDTO = {
export type TListApprovalRequestsDTO = { export type TListApprovalRequestsDTO = {
projectSlug: string; projectSlug: string;
authorProjectMembershipId?: string; authorUserId?: string;
envSlug?: string; envSlug?: string;
} & Omit<TProjectPermission, "projectId">; } & Omit<TProjectPermission, "projectId">;

View File

@ -780,6 +780,7 @@ interface CreateIdentityEvent {
metadata: { metadata: {
identityId: string; identityId: string;
name: string; name: string;
hasDeleteProtection: boolean;
}; };
} }
@ -788,6 +789,7 @@ interface UpdateIdentityEvent {
metadata: { metadata: {
identityId: string; identityId: string;
name?: string; name?: string;
hasDeleteProtection?: boolean;
}; };
} }

View File

@ -3,9 +3,43 @@ import { Knex } from "knex";
import { TDbClient } from "@app/db"; import { TDbClient } from "@app/db";
import { DynamicSecretLeasesSchema, TableName } from "@app/db/schemas"; import { DynamicSecretLeasesSchema, TableName } from "@app/db/schemas";
import { DatabaseError } from "@app/lib/errors"; import { DatabaseError } from "@app/lib/errors";
import { ormify, selectAllTableCols } from "@app/lib/knex"; import { ormify, selectAllTableCols, TOrmify } from "@app/lib/knex";
export type TDynamicSecretLeaseDALFactory = ReturnType<typeof dynamicSecretLeaseDALFactory>; export interface TDynamicSecretLeaseDALFactory extends Omit<TOrmify<TableName.DynamicSecretLease>, "findById"> {
countLeasesForDynamicSecret: (dynamicSecretId: string, tx?: Knex) => Promise<number>;
findById: (
id: string,
tx?: Knex
) => Promise<
| {
dynamicSecret: {
id: string;
name: string;
version: number;
type: string;
defaultTTL: string;
maxTTL: string | null | undefined;
encryptedInput: Buffer;
folderId: string;
status: string | null | undefined;
statusDetails: string | null | undefined;
createdAt: Date;
updatedAt: Date;
};
version: number;
id: string;
createdAt: Date;
updatedAt: Date;
externalEntityId: string;
expireAt: Date;
dynamicSecretId: string;
status?: string | null | undefined;
config?: unknown;
statusDetails?: string | null | undefined;
}
| undefined
>;
}
export const dynamicSecretLeaseDALFactory = (db: TDbClient) => { export const dynamicSecretLeaseDALFactory = (db: TDbClient) => {
const orm = ormify(db, TableName.DynamicSecretLease); const orm = ormify(db, TableName.DynamicSecretLease);

View File

@ -21,7 +21,12 @@ type TDynamicSecretLeaseQueueServiceFactoryDep = {
folderDAL: Pick<TSecretFolderDALFactory, "findById">; folderDAL: Pick<TSecretFolderDALFactory, "findById">;
}; };
export type TDynamicSecretLeaseQueueServiceFactory = ReturnType<typeof dynamicSecretLeaseQueueServiceFactory>; export type TDynamicSecretLeaseQueueServiceFactory = {
pruneDynamicSecret: (dynamicSecretCfgId: string) => Promise<void>;
setLeaseRevocation: (leaseId: string, expiryAt: Date) => Promise<void>;
unsetLeaseRevocation: (leaseId: string) => Promise<void>;
init: () => Promise<void>;
};
export const dynamicSecretLeaseQueueServiceFactory = ({ export const dynamicSecretLeaseQueueServiceFactory = ({
queueService, queueService,
@ -30,55 +35,48 @@ export const dynamicSecretLeaseQueueServiceFactory = ({
dynamicSecretLeaseDAL, dynamicSecretLeaseDAL,
kmsService, kmsService,
folderDAL folderDAL
}: TDynamicSecretLeaseQueueServiceFactoryDep) => { }: TDynamicSecretLeaseQueueServiceFactoryDep): TDynamicSecretLeaseQueueServiceFactory => {
const pruneDynamicSecret = async (dynamicSecretCfgId: string) => { const pruneDynamicSecret = async (dynamicSecretCfgId: string) => {
await queueService.queue( await queueService.queuePg<QueueName.DynamicSecretRevocation>(
QueueName.DynamicSecretRevocation,
QueueJobs.DynamicSecretPruning, QueueJobs.DynamicSecretPruning,
{ dynamicSecretCfgId }, { dynamicSecretCfgId },
{ {
jobId: dynamicSecretCfgId, singletonKey: dynamicSecretCfgId,
backoff: { retryLimit: 3,
type: "exponential", retryBackoff: true
delay: 3000
},
removeOnFail: {
count: 3
},
removeOnComplete: true
} }
); );
}; };
const setLeaseRevocation = async (leaseId: string, expiry: number) => { const setLeaseRevocation = async (leaseId: string, expiryAt: Date) => {
await queueService.queue( await queueService.queuePg<QueueName.DynamicSecretRevocation>(
QueueName.DynamicSecretRevocation,
QueueJobs.DynamicSecretRevocation, QueueJobs.DynamicSecretRevocation,
{ leaseId }, { leaseId },
{ {
jobId: leaseId, id: leaseId,
backoff: { singletonKey: leaseId,
type: "exponential", startAfter: expiryAt,
delay: 3000 retryLimit: 3,
}, retryBackoff: true,
delay: expiry, retentionDays: 2
removeOnFail: {
count: 3
},
removeOnComplete: true
} }
); );
}; };
const unsetLeaseRevocation = async (leaseId: string) => { const unsetLeaseRevocation = async (leaseId: string) => {
await queueService.stopJobById(QueueName.DynamicSecretRevocation, leaseId); await queueService.stopJobById(QueueName.DynamicSecretRevocation, leaseId);
await queueService.stopJobByIdPg(QueueName.DynamicSecretRevocation, leaseId);
}; };
queueService.start(QueueName.DynamicSecretRevocation, async (job) => { const $dynamicSecretQueueJob = async (
jobName: string,
jobId: string,
data: { leaseId: string } | { dynamicSecretCfgId: string }
): Promise<void> => {
try { try {
if (job.name === QueueJobs.DynamicSecretRevocation) { if (jobName === QueueJobs.DynamicSecretRevocation) {
const { leaseId } = job.data as { leaseId: string }; const { leaseId } = data as { leaseId: string };
logger.info("Dynamic secret lease revocation started: ", leaseId, job.id); logger.info("Dynamic secret lease revocation started: ", leaseId, jobId);
const dynamicSecretLease = await dynamicSecretLeaseDAL.findById(leaseId); const dynamicSecretLease = await dynamicSecretLeaseDAL.findById(leaseId);
if (!dynamicSecretLease) throw new DisableRotationErrors({ message: "Dynamic secret lease not found" }); if (!dynamicSecretLease) throw new DisableRotationErrors({ message: "Dynamic secret lease not found" });
@ -107,9 +105,9 @@ export const dynamicSecretLeaseQueueServiceFactory = ({
return; return;
} }
if (job.name === QueueJobs.DynamicSecretPruning) { if (jobName === QueueJobs.DynamicSecretPruning) {
const { dynamicSecretCfgId } = job.data as { dynamicSecretCfgId: string }; const { dynamicSecretCfgId } = data as { dynamicSecretCfgId: string };
logger.info("Dynamic secret pruning started: ", dynamicSecretCfgId, job.id); logger.info("Dynamic secret pruning started: ", dynamicSecretCfgId, jobId);
const dynamicSecretCfg = await dynamicSecretDAL.findById(dynamicSecretCfgId); const dynamicSecretCfg = await dynamicSecretDAL.findById(dynamicSecretCfgId);
if (!dynamicSecretCfg) throw new DisableRotationErrors({ message: "Dynamic secret not found" }); if (!dynamicSecretCfg) throw new DisableRotationErrors({ message: "Dynamic secret not found" });
if ((dynamicSecretCfg.status as DynamicSecretStatus) !== DynamicSecretStatus.Deleting) if ((dynamicSecretCfg.status as DynamicSecretStatus) !== DynamicSecretStatus.Deleting)
@ -150,38 +148,68 @@ export const dynamicSecretLeaseQueueServiceFactory = ({
await dynamicSecretDAL.deleteById(dynamicSecretCfgId); await dynamicSecretDAL.deleteById(dynamicSecretCfgId);
} }
logger.info("Finished dynamic secret job", job.id); logger.info("Finished dynamic secret job", jobId);
} catch (error) { } catch (error) {
logger.error(error); logger.error(error);
if (job?.name === QueueJobs.DynamicSecretPruning) { if (jobName === QueueJobs.DynamicSecretPruning) {
const { dynamicSecretCfgId } = job.data as { dynamicSecretCfgId: string }; const { dynamicSecretCfgId } = data as { dynamicSecretCfgId: string };
await dynamicSecretDAL.updateById(dynamicSecretCfgId, { await dynamicSecretDAL.updateById(dynamicSecretCfgId, {
status: DynamicSecretStatus.FailedDeletion, status: DynamicSecretStatus.FailedDeletion,
statusDetails: (error as Error)?.message?.slice(0, 255) statusDetails: (error as Error)?.message?.slice(0, 255)
}); });
} }
if (job?.name === QueueJobs.DynamicSecretRevocation) { if (jobName === QueueJobs.DynamicSecretRevocation) {
const { leaseId } = job.data as { leaseId: string }; const { leaseId } = data as { leaseId: string };
await dynamicSecretLeaseDAL.updateById(leaseId, { await dynamicSecretLeaseDAL.updateById(leaseId, {
status: DynamicSecretStatus.FailedDeletion, status: DynamicSecretStatus.FailedDeletion,
statusDetails: (error as Error)?.message?.slice(0, 255) statusDetails: (error as Error)?.message?.slice(0, 255)
}); });
} }
if (error instanceof DisableRotationErrors) { if (error instanceof DisableRotationErrors) {
if (job.id) { if (jobId) {
await queueService.stopRepeatableJobByJobId(QueueName.DynamicSecretRevocation, job.id); await queueService.stopRepeatableJobByJobId(QueueName.DynamicSecretRevocation, jobId);
await queueService.stopJobByIdPg(QueueName.DynamicSecretRevocation, jobId);
} }
} }
// propogate to next part // propogate to next part
throw error; throw error;
} }
};
queueService.start(QueueName.DynamicSecretRevocation, async (job) => {
await $dynamicSecretQueueJob(job.name, job.id as string, job.data);
}); });
const init = async () => {
await queueService.startPg<QueueName.DynamicSecretRevocation>(
QueueJobs.DynamicSecretRevocation,
async ([job]) => {
await $dynamicSecretQueueJob(job.name, job.id, job.data);
},
{
workerCount: 5,
pollingIntervalSeconds: 1
}
);
await queueService.startPg<QueueName.DynamicSecretRevocation>(
QueueJobs.DynamicSecretPruning,
async ([job]) => {
await $dynamicSecretQueueJob(job.name, job.id, job.data);
},
{
workerCount: 1,
pollingIntervalSeconds: 1
}
);
};
return { return {
pruneDynamicSecret, pruneDynamicSecret,
setLeaseRevocation, setLeaseRevocation,
unsetLeaseRevocation unsetLeaseRevocation,
init
}; };
}; };

View File

@ -26,12 +26,8 @@ import { TDynamicSecretLeaseDALFactory } from "./dynamic-secret-lease-dal";
import { TDynamicSecretLeaseQueueServiceFactory } from "./dynamic-secret-lease-queue"; import { TDynamicSecretLeaseQueueServiceFactory } from "./dynamic-secret-lease-queue";
import { import {
DynamicSecretLeaseStatus, DynamicSecretLeaseStatus,
TCreateDynamicSecretLeaseDTO,
TDeleteDynamicSecretLeaseDTO,
TDetailsDynamicSecretLeaseDTO,
TDynamicSecretLeaseConfig, TDynamicSecretLeaseConfig,
TListDynamicSecretLeasesDTO, TDynamicSecretLeaseServiceFactory
TRenewDynamicSecretLeaseDTO
} from "./dynamic-secret-lease-types"; } from "./dynamic-secret-lease-types";
type TDynamicSecretLeaseServiceFactoryDep = { type TDynamicSecretLeaseServiceFactoryDep = {
@ -48,8 +44,6 @@ type TDynamicSecretLeaseServiceFactoryDep = {
identityDAL: TIdentityDALFactory; identityDAL: TIdentityDALFactory;
}; };
export type TDynamicSecretLeaseServiceFactory = ReturnType<typeof dynamicSecretLeaseServiceFactory>;
export const dynamicSecretLeaseServiceFactory = ({ export const dynamicSecretLeaseServiceFactory = ({
dynamicSecretLeaseDAL, dynamicSecretLeaseDAL,
dynamicSecretProviders, dynamicSecretProviders,
@ -62,14 +56,14 @@ export const dynamicSecretLeaseServiceFactory = ({
kmsService, kmsService,
userDAL, userDAL,
identityDAL identityDAL
}: TDynamicSecretLeaseServiceFactoryDep) => { }: TDynamicSecretLeaseServiceFactoryDep): TDynamicSecretLeaseServiceFactory => {
const extractEmailUsername = (email: string) => { const extractEmailUsername = (email: string) => {
const regex = new RE2(/^([^@]+)/); const regex = new RE2(/^([^@]+)/);
const match = email.match(regex); const match = email.match(regex);
return match ? match[1] : email; return match ? match[1] : email;
}; };
const create = async ({ const create: TDynamicSecretLeaseServiceFactory["create"] = async ({
environmentSlug, environmentSlug,
path, path,
name, name,
@ -80,7 +74,7 @@ export const dynamicSecretLeaseServiceFactory = ({
actorAuthMethod, actorAuthMethod,
ttl, ttl,
config config
}: TCreateDynamicSecretLeaseDTO) => { }) => {
const appCfg = getConfig(); const appCfg = getConfig();
const project = await projectDAL.findProjectBySlug(projectSlug, actorOrgId); const project = await projectDAL.findProjectBySlug(projectSlug, actorOrgId);
if (!project) throw new NotFoundError({ message: `Project with slug '${projectSlug}' not found` }); if (!project) throw new NotFoundError({ message: `Project with slug '${projectSlug}' not found` });
@ -184,11 +178,11 @@ export const dynamicSecretLeaseServiceFactory = ({
config config
}); });
await dynamicSecretQueueService.setLeaseRevocation(dynamicSecretLease.id, Number(expireAt) - Number(new Date())); await dynamicSecretQueueService.setLeaseRevocation(dynamicSecretLease.id, expireAt);
return { lease: dynamicSecretLease, dynamicSecret: dynamicSecretCfg, data }; return { lease: dynamicSecretLease, dynamicSecret: dynamicSecretCfg, data };
}; };
const renewLease = async ({ const renewLease: TDynamicSecretLeaseServiceFactory["renewLease"] = async ({
ttl, ttl,
actorAuthMethod, actorAuthMethod,
actorOrgId, actorOrgId,
@ -198,7 +192,7 @@ export const dynamicSecretLeaseServiceFactory = ({
path, path,
environmentSlug, environmentSlug,
leaseId leaseId
}: TRenewDynamicSecretLeaseDTO) => { }) => {
const project = await projectDAL.findProjectBySlug(projectSlug, actorOrgId); const project = await projectDAL.findProjectBySlug(projectSlug, actorOrgId);
if (!project) throw new NotFoundError({ message: `Project with slug '${projectSlug}' not found` }); if (!project) throw new NotFoundError({ message: `Project with slug '${projectSlug}' not found` });
@ -278,7 +272,7 @@ export const dynamicSecretLeaseServiceFactory = ({
); );
await dynamicSecretQueueService.unsetLeaseRevocation(dynamicSecretLease.id); await dynamicSecretQueueService.unsetLeaseRevocation(dynamicSecretLease.id);
await dynamicSecretQueueService.setLeaseRevocation(dynamicSecretLease.id, Number(expireAt) - Number(new Date())); await dynamicSecretQueueService.setLeaseRevocation(dynamicSecretLease.id, expireAt);
const updatedDynamicSecretLease = await dynamicSecretLeaseDAL.updateById(dynamicSecretLease.id, { const updatedDynamicSecretLease = await dynamicSecretLeaseDAL.updateById(dynamicSecretLease.id, {
expireAt, expireAt,
externalEntityId: entityId externalEntityId: entityId
@ -286,7 +280,7 @@ export const dynamicSecretLeaseServiceFactory = ({
return updatedDynamicSecretLease; return updatedDynamicSecretLease;
}; };
const revokeLease = async ({ const revokeLease: TDynamicSecretLeaseServiceFactory["revokeLease"] = async ({
leaseId, leaseId,
environmentSlug, environmentSlug,
path, path,
@ -296,7 +290,7 @@ export const dynamicSecretLeaseServiceFactory = ({
actorOrgId, actorOrgId,
actorAuthMethod, actorAuthMethod,
isForced isForced
}: TDeleteDynamicSecretLeaseDTO) => { }) => {
const project = await projectDAL.findProjectBySlug(projectSlug, actorOrgId); const project = await projectDAL.findProjectBySlug(projectSlug, actorOrgId);
if (!project) throw new NotFoundError({ message: `Project with slug '${projectSlug}' not found` }); if (!project) throw new NotFoundError({ message: `Project with slug '${projectSlug}' not found` });
@ -376,7 +370,7 @@ export const dynamicSecretLeaseServiceFactory = ({
return deletedDynamicSecretLease; return deletedDynamicSecretLease;
}; };
const listLeases = async ({ const listLeases: TDynamicSecretLeaseServiceFactory["listLeases"] = async ({
path, path,
name, name,
actor, actor,
@ -385,7 +379,7 @@ export const dynamicSecretLeaseServiceFactory = ({
actorOrgId, actorOrgId,
environmentSlug, environmentSlug,
actorAuthMethod actorAuthMethod
}: TListDynamicSecretLeasesDTO) => { }) => {
const project = await projectDAL.findProjectBySlug(projectSlug, actorOrgId); const project = await projectDAL.findProjectBySlug(projectSlug, actorOrgId);
if (!project) throw new NotFoundError({ message: `Project with slug '${projectSlug}' not found` }); if (!project) throw new NotFoundError({ message: `Project with slug '${projectSlug}' not found` });
@ -424,7 +418,7 @@ export const dynamicSecretLeaseServiceFactory = ({
return dynamicSecretLeases; return dynamicSecretLeases;
}; };
const getLeaseDetails = async ({ const getLeaseDetails: TDynamicSecretLeaseServiceFactory["getLeaseDetails"] = async ({
projectSlug, projectSlug,
actorOrgId, actorOrgId,
path, path,
@ -433,7 +427,7 @@ export const dynamicSecretLeaseServiceFactory = ({
actorId, actorId,
leaseId, leaseId,
actorAuthMethod actorAuthMethod
}: TDetailsDynamicSecretLeaseDTO) => { }) => {
const project = await projectDAL.findProjectBySlug(projectSlug, actorOrgId); const project = await projectDAL.findProjectBySlug(projectSlug, actorOrgId);
if (!project) throw new NotFoundError({ message: `Project with slug '${projectSlug}' not found` }); if (!project) throw new NotFoundError({ message: `Project with slug '${projectSlug}' not found` });

View File

@ -1,4 +1,5 @@
import { TProjectPermission } from "@app/lib/types"; import { TDynamicSecretLeases } from "@app/db/schemas";
import { TDynamicSecretWithMetadata, TProjectPermission } from "@app/lib/types";
export enum DynamicSecretLeaseStatus { export enum DynamicSecretLeaseStatus {
FailedDeletion = "Failed to delete" FailedDeletion = "Failed to delete"
@ -48,3 +49,40 @@ export type TDynamicSecretKubernetesLeaseConfig = {
}; };
export type TDynamicSecretLeaseConfig = TDynamicSecretKubernetesLeaseConfig; export type TDynamicSecretLeaseConfig = TDynamicSecretKubernetesLeaseConfig;
export type TDynamicSecretLeaseServiceFactory = {
create: (arg: TCreateDynamicSecretLeaseDTO) => Promise<{
lease: TDynamicSecretLeases;
dynamicSecret: TDynamicSecretWithMetadata;
data: unknown;
}>;
listLeases: (arg: TListDynamicSecretLeasesDTO) => Promise<TDynamicSecretLeases[]>;
revokeLease: (arg: TDeleteDynamicSecretLeaseDTO) => Promise<TDynamicSecretLeases>;
renewLease: (arg: TRenewDynamicSecretLeaseDTO) => Promise<TDynamicSecretLeases>;
getLeaseDetails: (arg: TDetailsDynamicSecretLeaseDTO) => Promise<{
dynamicSecret: {
id: string;
name: string;
version: number;
type: string;
defaultTTL: string;
maxTTL: string | null | undefined;
encryptedInput: Buffer;
folderId: string;
status: string | null | undefined;
statusDetails: string | null | undefined;
createdAt: Date;
updatedAt: Date;
};
version: number;
id: string;
createdAt: Date;
updatedAt: Date;
externalEntityId: string;
expireAt: Date;
dynamicSecretId: string;
status?: string | null | undefined;
config?: unknown;
statusDetails?: string | null | undefined;
}>;
};

View File

@ -10,17 +10,35 @@ import {
selectAllTableCols, selectAllTableCols,
sqlNestRelationships, sqlNestRelationships,
TFindFilter, TFindFilter,
TFindOpt TFindOpt,
TOrmify
} from "@app/lib/knex"; } from "@app/lib/knex";
import { OrderByDirection } from "@app/lib/types"; import { OrderByDirection, TDynamicSecretWithMetadata } from "@app/lib/types";
import { SecretsOrderBy } from "@app/services/secret/secret-types"; import { SecretsOrderBy } from "@app/services/secret/secret-types";
export type TDynamicSecretDALFactory = ReturnType<typeof dynamicSecretDALFactory>; export interface TDynamicSecretDALFactory extends Omit<TOrmify<TableName.DynamicSecret>, "findOne"> {
findOne: (filter: TFindFilter<TDynamicSecrets>, tx?: Knex) => Promise<TDynamicSecretWithMetadata>;
listDynamicSecretsByFolderIds: (
arg: {
folderIds: string[];
search?: string | undefined;
limit?: number | undefined;
offset?: number | undefined;
orderBy?: SecretsOrderBy | undefined;
orderDirection?: OrderByDirection | undefined;
},
tx?: Knex
) => Promise<Array<TDynamicSecretWithMetadata & { environment: string }>>;
findWithMetadata: (
filter: TFindFilter<TDynamicSecrets>,
arg?: TFindOpt<TDynamicSecrets>
) => Promise<TDynamicSecretWithMetadata[]>;
}
export const dynamicSecretDALFactory = (db: TDbClient) => { export const dynamicSecretDALFactory = (db: TDbClient): TDynamicSecretDALFactory => {
const orm = ormify(db, TableName.DynamicSecret); const orm = ormify(db, TableName.DynamicSecret);
const findOne = async (filter: TFindFilter<TDynamicSecrets>, tx?: Knex) => { const findOne: TDynamicSecretDALFactory["findOne"] = async (filter, tx) => {
const query = (tx || db.replicaNode())(TableName.DynamicSecret) const query = (tx || db.replicaNode())(TableName.DynamicSecret)
.leftJoin( .leftJoin(
TableName.ResourceMetadata, TableName.ResourceMetadata,
@ -55,9 +73,9 @@ export const dynamicSecretDALFactory = (db: TDbClient) => {
return docs[0]; return docs[0];
}; };
const findWithMetadata = async ( const findWithMetadata: TDynamicSecretDALFactory["findWithMetadata"] = async (
filter: TFindFilter<TDynamicSecrets>, filter,
{ offset, limit, sort, tx }: TFindOpt<TDynamicSecrets> = {} { offset, limit, sort, tx } = {}
) => { ) => {
const query = (tx || db.replicaNode())(TableName.DynamicSecret) const query = (tx || db.replicaNode())(TableName.DynamicSecret)
.leftJoin( .leftJoin(
@ -101,23 +119,9 @@ export const dynamicSecretDALFactory = (db: TDbClient) => {
}; };
// find dynamic secrets for multiple environments (folder IDs are cross env, thus need to rank for pagination) // find dynamic secrets for multiple environments (folder IDs are cross env, thus need to rank for pagination)
const listDynamicSecretsByFolderIds = async ( const listDynamicSecretsByFolderIds: TDynamicSecretDALFactory["listDynamicSecretsByFolderIds"] = async (
{ { folderIds, search, limit, offset = 0, orderBy = SecretsOrderBy.Name, orderDirection = OrderByDirection.ASC },
folderIds, tx
search,
limit,
offset = 0,
orderBy = SecretsOrderBy.Name,
orderDirection = OrderByDirection.ASC
}: {
folderIds: string[];
search?: string;
limit?: number;
offset?: number;
orderBy?: SecretsOrderBy;
orderDirection?: OrderByDirection;
},
tx?: Knex
) => { ) => {
try { try {
const query = (tx || db.replicaNode())(TableName.DynamicSecret) const query = (tx || db.replicaNode())(TableName.DynamicSecret)

View File

@ -8,7 +8,7 @@ import {
ProjectPermissionSub ProjectPermissionSub
} from "@app/ee/services/permission/project-permission"; } from "@app/ee/services/permission/project-permission";
import { BadRequestError, NotFoundError } from "@app/lib/errors"; import { BadRequestError, NotFoundError } from "@app/lib/errors";
import { OrderByDirection, OrgServiceActor } from "@app/lib/types"; import { OrderByDirection } from "@app/lib/types";
import { TKmsServiceFactory } from "@app/services/kms/kms-service"; import { TKmsServiceFactory } from "@app/services/kms/kms-service";
import { KmsDataKey } from "@app/services/kms/kms-types"; import { KmsDataKey } from "@app/services/kms/kms-types";
import { TProjectDALFactory } from "@app/services/project/project-dal"; import { TProjectDALFactory } from "@app/services/project/project-dal";
@ -20,17 +20,7 @@ import { TDynamicSecretLeaseQueueServiceFactory } from "../dynamic-secret-lease/
import { TGatewayDALFactory } from "../gateway/gateway-dal"; import { TGatewayDALFactory } from "../gateway/gateway-dal";
import { OrgPermissionGatewayActions, OrgPermissionSubjects } from "../permission/org-permission"; import { OrgPermissionGatewayActions, OrgPermissionSubjects } from "../permission/org-permission";
import { TDynamicSecretDALFactory } from "./dynamic-secret-dal"; import { TDynamicSecretDALFactory } from "./dynamic-secret-dal";
import { import { DynamicSecretStatus, TDynamicSecretServiceFactory } from "./dynamic-secret-types";
DynamicSecretStatus,
TCreateDynamicSecretDTO,
TDeleteDynamicSecretDTO,
TDetailsDynamicSecretDTO,
TGetDynamicSecretsCountDTO,
TListDynamicSecretsByFolderMappingsDTO,
TListDynamicSecretsDTO,
TListDynamicSecretsMultiEnvDTO,
TUpdateDynamicSecretDTO
} from "./dynamic-secret-types";
import { AzureEntraIDProvider } from "./providers/azure-entra-id"; import { AzureEntraIDProvider } from "./providers/azure-entra-id";
import { DynamicSecretProviders, TDynamicProviderFns } from "./providers/models"; import { DynamicSecretProviders, TDynamicProviderFns } from "./providers/models";
@ -51,8 +41,6 @@ type TDynamicSecretServiceFactoryDep = {
resourceMetadataDAL: Pick<TResourceMetadataDALFactory, "insertMany" | "delete">; resourceMetadataDAL: Pick<TResourceMetadataDALFactory, "insertMany" | "delete">;
}; };
export type TDynamicSecretServiceFactory = ReturnType<typeof dynamicSecretServiceFactory>;
export const dynamicSecretServiceFactory = ({ export const dynamicSecretServiceFactory = ({
dynamicSecretDAL, dynamicSecretDAL,
dynamicSecretLeaseDAL, dynamicSecretLeaseDAL,
@ -65,8 +53,8 @@ export const dynamicSecretServiceFactory = ({
kmsService, kmsService,
gatewayDAL, gatewayDAL,
resourceMetadataDAL resourceMetadataDAL
}: TDynamicSecretServiceFactoryDep) => { }: TDynamicSecretServiceFactoryDep): TDynamicSecretServiceFactory => {
const create = async ({ const create: TDynamicSecretServiceFactory["create"] = async ({
path, path,
actor, actor,
name, name,
@ -80,7 +68,7 @@ export const dynamicSecretServiceFactory = ({
actorAuthMethod, actorAuthMethod,
metadata, metadata,
usernameTemplate usernameTemplate
}: TCreateDynamicSecretDTO) => { }) => {
const project = await projectDAL.findProjectBySlug(projectSlug, actorOrgId); const project = await projectDAL.findProjectBySlug(projectSlug, actorOrgId);
if (!project) throw new NotFoundError({ message: `Project with slug '${projectSlug}' not found` }); if (!project) throw new NotFoundError({ message: `Project with slug '${projectSlug}' not found` });
@ -188,7 +176,7 @@ export const dynamicSecretServiceFactory = ({
return dynamicSecretCfg; return dynamicSecretCfg;
}; };
const updateByName = async ({ const updateByName: TDynamicSecretServiceFactory["updateByName"] = async ({
name, name,
maxTTL, maxTTL,
defaultTTL, defaultTTL,
@ -203,7 +191,7 @@ export const dynamicSecretServiceFactory = ({
actorAuthMethod, actorAuthMethod,
metadata, metadata,
usernameTemplate usernameTemplate
}: TUpdateDynamicSecretDTO) => { }) => {
const project = await projectDAL.findProjectBySlug(projectSlug, actorOrgId); const project = await projectDAL.findProjectBySlug(projectSlug, actorOrgId);
if (!project) throw new NotFoundError({ message: `Project with slug '${projectSlug}' not found` }); if (!project) throw new NotFoundError({ message: `Project with slug '${projectSlug}' not found` });
@ -345,7 +333,7 @@ export const dynamicSecretServiceFactory = ({
return updatedDynamicCfg; return updatedDynamicCfg;
}; };
const deleteByName = async ({ const deleteByName: TDynamicSecretServiceFactory["deleteByName"] = async ({
actorAuthMethod, actorAuthMethod,
actorOrgId, actorOrgId,
actorId, actorId,
@ -355,7 +343,7 @@ export const dynamicSecretServiceFactory = ({
path, path,
environmentSlug, environmentSlug,
isForced isForced
}: TDeleteDynamicSecretDTO) => { }) => {
const project = await projectDAL.findProjectBySlug(projectSlug, actorOrgId); const project = await projectDAL.findProjectBySlug(projectSlug, actorOrgId);
if (!project) throw new NotFoundError({ message: `Project with slug '${projectSlug}' not found` }); if (!project) throw new NotFoundError({ message: `Project with slug '${projectSlug}' not found` });
@ -413,7 +401,7 @@ export const dynamicSecretServiceFactory = ({
return deletedDynamicSecretCfg; return deletedDynamicSecretCfg;
}; };
const getDetails = async ({ const getDetails: TDynamicSecretServiceFactory["getDetails"] = async ({
name, name,
projectSlug, projectSlug,
path, path,
@ -422,7 +410,7 @@ export const dynamicSecretServiceFactory = ({
actorOrgId, actorOrgId,
actorId, actorId,
actor actor
}: TDetailsDynamicSecretDTO) => { }) => {
const project = await projectDAL.findProjectBySlug(projectSlug, actorOrgId); const project = await projectDAL.findProjectBySlug(projectSlug, actorOrgId);
if (!project) throw new NotFoundError({ message: `Project with slug '${projectSlug}' not found` }); if (!project) throw new NotFoundError({ message: `Project with slug '${projectSlug}' not found` });
@ -480,7 +468,7 @@ export const dynamicSecretServiceFactory = ({
}; };
// get unique dynamic secret count across multiple envs // get unique dynamic secret count across multiple envs
const getCountMultiEnv = async ({ const getCountMultiEnv: TDynamicSecretServiceFactory["getCountMultiEnv"] = async ({
actorAuthMethod, actorAuthMethod,
actorOrgId, actorOrgId,
actorId, actorId,
@ -490,7 +478,7 @@ export const dynamicSecretServiceFactory = ({
environmentSlugs, environmentSlugs,
search, search,
isInternal isInternal
}: TListDynamicSecretsMultiEnvDTO) => { }) => {
if (!isInternal) { if (!isInternal) {
const { permission } = await permissionService.getProjectPermission({ const { permission } = await permissionService.getProjectPermission({
actor, actor,
@ -526,7 +514,7 @@ export const dynamicSecretServiceFactory = ({
}; };
// get dynamic secret count for a single env // get dynamic secret count for a single env
const getDynamicSecretCount = async ({ const getDynamicSecretCount: TDynamicSecretServiceFactory["getDynamicSecretCount"] = async ({
actorAuthMethod, actorAuthMethod,
actorOrgId, actorOrgId,
actorId, actorId,
@ -535,7 +523,7 @@ export const dynamicSecretServiceFactory = ({
environmentSlug, environmentSlug,
search, search,
projectId projectId
}: TGetDynamicSecretsCountDTO) => { }) => {
const { permission } = await permissionService.getProjectPermission({ const { permission } = await permissionService.getProjectPermission({
actor, actor,
actorId, actorId,
@ -561,7 +549,7 @@ export const dynamicSecretServiceFactory = ({
return Number(dynamicSecretCfg[0]?.count ?? 0); return Number(dynamicSecretCfg[0]?.count ?? 0);
}; };
const listDynamicSecretsByEnv = async ({ const listDynamicSecretsByEnv: TDynamicSecretServiceFactory["listDynamicSecretsByEnv"] = async ({
actorAuthMethod, actorAuthMethod,
actorOrgId, actorOrgId,
actorId, actorId,
@ -575,7 +563,7 @@ export const dynamicSecretServiceFactory = ({
orderDirection = OrderByDirection.ASC, orderDirection = OrderByDirection.ASC,
search, search,
...params ...params
}: TListDynamicSecretsDTO) => { }) => {
let { projectId } = params; let { projectId } = params;
if (!projectId) { if (!projectId) {
@ -619,9 +607,9 @@ export const dynamicSecretServiceFactory = ({
}); });
}; };
const listDynamicSecretsByFolderIds = async ( const listDynamicSecretsByFolderIds: TDynamicSecretServiceFactory["listDynamicSecretsByFolderIds"] = async (
{ folderMappings, filters, projectId }: TListDynamicSecretsByFolderMappingsDTO, { folderMappings, filters, projectId },
actor: OrgServiceActor actor
) => { ) => {
const { permission } = await permissionService.getProjectPermission({ const { permission } = await permissionService.getProjectPermission({
actor: actor.type, actor: actor.type,
@ -657,7 +645,7 @@ export const dynamicSecretServiceFactory = ({
}; };
// get dynamic secrets for multiple envs // get dynamic secrets for multiple envs
const listDynamicSecretsByEnvs = async ({ const listDynamicSecretsByEnvs: TDynamicSecretServiceFactory["listDynamicSecretsByEnvs"] = async ({
actorAuthMethod, actorAuthMethod,
actorOrgId, actorOrgId,
actorId, actorId,
@ -667,7 +655,7 @@ export const dynamicSecretServiceFactory = ({
projectId, projectId,
isInternal, isInternal,
...params ...params
}: TListDynamicSecretsMultiEnvDTO) => { }) => {
const { permission } = await permissionService.getProjectPermission({ const { permission } = await permissionService.getProjectPermission({
actor, actor,
actorId, actorId,
@ -700,14 +688,10 @@ export const dynamicSecretServiceFactory = ({
}); });
}; };
const fetchAzureEntraIdUsers = async ({ const fetchAzureEntraIdUsers: TDynamicSecretServiceFactory["fetchAzureEntraIdUsers"] = async ({
tenantId, tenantId,
applicationId, applicationId,
clientSecret clientSecret
}: {
tenantId: string;
applicationId: string;
clientSecret: string;
}) => { }) => {
const azureEntraIdUsers = await AzureEntraIDProvider().fetchAzureEntraIdUsers( const azureEntraIdUsers = await AzureEntraIDProvider().fetchAzureEntraIdUsers(
tenantId, tenantId,

View File

@ -1,6 +1,7 @@
import { z } from "zod"; import { z } from "zod";
import { OrderByDirection, TProjectPermission } from "@app/lib/types"; import { TDynamicSecrets } from "@app/db/schemas";
import { OrderByDirection, OrgServiceActor, TDynamicSecretWithMetadata, TProjectPermission } from "@app/lib/types";
import { ResourceMetadataDTO } from "@app/services/resource-metadata/resource-metadata-schema"; import { ResourceMetadataDTO } from "@app/services/resource-metadata/resource-metadata-schema";
import { SecretsOrderBy } from "@app/services/secret/secret-types"; import { SecretsOrderBy } from "@app/services/secret/secret-types";
@ -83,3 +84,27 @@ export type TListDynamicSecretsMultiEnvDTO = Omit<
export type TGetDynamicSecretsCountDTO = Omit<TListDynamicSecretsDTO, "projectSlug" | "projectId"> & { export type TGetDynamicSecretsCountDTO = Omit<TListDynamicSecretsDTO, "projectSlug" | "projectId"> & {
projectId: string; projectId: string;
}; };
export type TDynamicSecretServiceFactory = {
create: (arg: TCreateDynamicSecretDTO) => Promise<TDynamicSecrets>;
updateByName: (arg: TUpdateDynamicSecretDTO) => Promise<TDynamicSecrets>;
deleteByName: (arg: TDeleteDynamicSecretDTO) => Promise<TDynamicSecrets>;
getDetails: (arg: TDetailsDynamicSecretDTO) => Promise<TDynamicSecretWithMetadata>;
listDynamicSecretsByEnv: (arg: TListDynamicSecretsDTO) => Promise<TDynamicSecretWithMetadata[]>;
listDynamicSecretsByEnvs: (
arg: TListDynamicSecretsMultiEnvDTO
) => Promise<Array<TDynamicSecretWithMetadata & { environment: string }>>;
getDynamicSecretCount: (arg: TGetDynamicSecretsCountDTO) => Promise<number>;
getCountMultiEnv: (arg: TListDynamicSecretsMultiEnvDTO) => Promise<number>;
fetchAzureEntraIdUsers: (arg: { tenantId: string; applicationId: string; clientSecret: string }) => Promise<
{
name: string;
id: string;
email: string;
}[]
>;
listDynamicSecretsByFolderIds: (
arg: TListDynamicSecretsByFolderMappingsDTO,
actor: OrgServiceActor
) => Promise<Array<TDynamicSecretWithMetadata & { environment: string; path: string }>>;
};

View File

@ -128,11 +128,21 @@ export const AwsIamProvider = (): TDynamicProviderFns => {
const username = generateUsername(usernameTemplate, identity); const username = generateUsername(usernameTemplate, identity);
const { policyArns, userGroups, policyDocument, awsPath, permissionBoundaryPolicyArn } = providerInputs; const { policyArns, userGroups, policyDocument, awsPath, permissionBoundaryPolicyArn } = providerInputs;
const awsTags = [{ Key: "createdBy", Value: "infisical-dynamic-secret" }];
if (providerInputs.tags && Array.isArray(providerInputs.tags)) {
const additionalTags = providerInputs.tags.map((tag) => ({
Key: tag.key,
Value: tag.value
}));
awsTags.push(...additionalTags);
}
const createUserRes = await client.send( const createUserRes = await client.send(
new CreateUserCommand({ new CreateUserCommand({
Path: awsPath, Path: awsPath,
PermissionsBoundary: permissionBoundaryPolicyArn || undefined, PermissionsBoundary: permissionBoundaryPolicyArn || undefined,
Tags: [{ Key: "createdBy", Value: "infisical-dynamic-secret" }], Tags: awsTags,
UserName: username UserName: username
}) })
); );

View File

@ -0,0 +1,133 @@
import axios from "axios";
import * as jwt from "jsonwebtoken";
import { BadRequestError, InternalServerError } from "@app/lib/errors";
import { alphaNumericNanoId } from "@app/lib/nanoid";
import { IntegrationUrls } from "@app/services/integration-auth/integration-list";
import { DynamicSecretGithubSchema, TDynamicProviderFns } from "./models";
interface GitHubInstallationTokenResponse {
token: string;
expires_at: string; // ISO 8601 timestamp e.g., "2024-01-15T12:00:00Z"
permissions?: Record<string, string>;
repository_selection?: string;
}
interface TGithubProviderInputs {
appId: number;
installationId: number;
privateKey: string;
}
export const GithubProvider = (): TDynamicProviderFns => {
const validateProviderInputs = async (inputs: unknown) => {
const providerInputs = await DynamicSecretGithubSchema.parseAsync(inputs);
return providerInputs;
};
const $generateGitHubInstallationAccessToken = async (
credentials: TGithubProviderInputs
): Promise<GitHubInstallationTokenResponse> => {
const { appId, installationId, privateKey } = credentials;
const nowInSeconds = Math.floor(Date.now() / 1000);
const jwtPayload = {
iat: nowInSeconds - 5,
exp: nowInSeconds + 60,
iss: String(appId)
};
let appJwt: string;
try {
appJwt = jwt.sign(jwtPayload, privateKey, { algorithm: "RS256" });
} catch (error) {
let message = "Failed to sign JWT.";
if (error instanceof jwt.JsonWebTokenError) {
message += ` JsonWebTokenError: ${error.message}`;
}
throw new InternalServerError({
message
});
}
const tokenUrl = `${IntegrationUrls.GITHUB_API_URL}/app/installations/${String(installationId)}/access_tokens`;
try {
const response = await axios.post<GitHubInstallationTokenResponse>(tokenUrl, undefined, {
headers: {
Authorization: `Bearer ${appJwt}`,
Accept: "application/vnd.github.v3+json",
"X-GitHub-Api-Version": "2022-11-28"
}
});
if (response.status === 201 && response.data.token) {
return response.data; // Includes token, expires_at, permissions, repository_selection
}
throw new InternalServerError({
message: `GitHub API responded with unexpected status ${response.status}: ${JSON.stringify(response.data)}`
});
} catch (error) {
let message = "Failed to fetch GitHub installation access token.";
if (axios.isAxiosError(error) && error.response) {
const githubErrorMsg =
(error.response.data as { message?: string })?.message || JSON.stringify(error.response.data);
message += ` GitHub API Error: ${error.response.status} - ${githubErrorMsg}`;
// Classify as BadRequestError for auth-related issues (401, 403, 404) which might be due to user input
if ([401, 403, 404].includes(error.response.status)) {
throw new BadRequestError({ message });
}
}
throw new InternalServerError({ message });
}
};
const validateConnection = async (inputs: unknown) => {
const providerInputs = await validateProviderInputs(inputs);
await $generateGitHubInstallationAccessToken(providerInputs);
return true;
};
const create = async (data: { inputs: unknown }) => {
const { inputs } = data;
const providerInputs = await validateProviderInputs(inputs);
const ghTokenData = await $generateGitHubInstallationAccessToken(providerInputs);
const entityId = alphaNumericNanoId(32);
return {
entityId,
data: {
TOKEN: ghTokenData.token,
EXPIRES_AT: ghTokenData.expires_at,
PERMISSIONS: ghTokenData.permissions,
REPOSITORY_SELECTION: ghTokenData.repository_selection
}
};
};
const revoke = async () => {
// GitHub installation tokens cannot be revoked.
throw new BadRequestError({
message:
"Github dynamic secret does not support revocation because GitHub itself cannot revoke installation tokens"
});
};
const renew = async () => {
// No renewal
throw new BadRequestError({ message: "Github dynamic secret does not support renewal" });
};
return {
validateProviderInputs,
validateConnection,
create,
revoke,
renew
};
};

View File

@ -7,6 +7,7 @@ import { AzureEntraIDProvider } from "./azure-entra-id";
import { CassandraProvider } from "./cassandra"; import { CassandraProvider } from "./cassandra";
import { ElasticSearchProvider } from "./elastic-search"; import { ElasticSearchProvider } from "./elastic-search";
import { GcpIamProvider } from "./gcp-iam"; import { GcpIamProvider } from "./gcp-iam";
import { GithubProvider } from "./github";
import { KubernetesProvider } from "./kubernetes"; import { KubernetesProvider } from "./kubernetes";
import { LdapProvider } from "./ldap"; import { LdapProvider } from "./ldap";
import { DynamicSecretProviders, TDynamicProviderFns } from "./models"; import { DynamicSecretProviders, TDynamicProviderFns } from "./models";
@ -44,5 +45,6 @@ export const buildDynamicSecretProviders = ({
[DynamicSecretProviders.SapAse]: SapAseProvider(), [DynamicSecretProviders.SapAse]: SapAseProvider(),
[DynamicSecretProviders.Kubernetes]: KubernetesProvider({ gatewayService }), [DynamicSecretProviders.Kubernetes]: KubernetesProvider({ gatewayService }),
[DynamicSecretProviders.Vertica]: VerticaProvider({ gatewayService }), [DynamicSecretProviders.Vertica]: VerticaProvider({ gatewayService }),
[DynamicSecretProviders.GcpIam]: GcpIamProvider() [DynamicSecretProviders.GcpIam]: GcpIamProvider(),
[DynamicSecretProviders.Github]: GithubProvider()
}); });

View File

@ -52,9 +52,8 @@ export const KubernetesProvider = ({ gatewayService }: TKubernetesProviderDTO):
gatewayId: string; gatewayId: string;
targetHost: string; targetHost: string;
targetPort: number; targetPort: number;
caCert?: string; httpsAgent?: https.Agent;
reviewTokenThroughGateway: boolean; reviewTokenThroughGateway: boolean;
enableSsl: boolean;
}, },
gatewayCallback: (host: string, port: number, httpsAgent?: https.Agent) => Promise<T> gatewayCallback: (host: string, port: number, httpsAgent?: https.Agent) => Promise<T>
): Promise<T> => { ): Promise<T> => {
@ -85,10 +84,7 @@ export const KubernetesProvider = ({ gatewayService }: TKubernetesProviderDTO):
key: relayDetails.privateKey.toString() key: relayDetails.privateKey.toString()
}, },
// we always pass this, because its needed for both tcp and http protocol // we always pass this, because its needed for both tcp and http protocol
httpsAgent: new https.Agent({ httpsAgent: inputs.httpsAgent
ca: inputs.caCert,
rejectUnauthorized: inputs.enableSsl
})
} }
); );
@ -311,6 +307,14 @@ export const KubernetesProvider = ({ gatewayService }: TKubernetesProviderDTO):
const k8sHost = `${url.protocol}//${url.hostname}`; const k8sHost = `${url.protocol}//${url.hostname}`;
try { try {
const httpsAgent =
providerInputs.ca && providerInputs.sslEnabled
? new https.Agent({
ca: providerInputs.ca,
rejectUnauthorized: true
})
: undefined;
if (providerInputs.gatewayId) { if (providerInputs.gatewayId) {
if (providerInputs.authMethod === KubernetesAuthMethod.Gateway) { if (providerInputs.authMethod === KubernetesAuthMethod.Gateway) {
await $gatewayProxyWrapper( await $gatewayProxyWrapper(
@ -318,8 +322,7 @@ export const KubernetesProvider = ({ gatewayService }: TKubernetesProviderDTO):
gatewayId: providerInputs.gatewayId, gatewayId: providerInputs.gatewayId,
targetHost: k8sHost, targetHost: k8sHost,
targetPort: k8sPort, targetPort: k8sPort,
enableSsl: providerInputs.sslEnabled, httpsAgent,
caCert: providerInputs.ca,
reviewTokenThroughGateway: true reviewTokenThroughGateway: true
}, },
providerInputs.credentialType === KubernetesCredentialType.Static providerInputs.credentialType === KubernetesCredentialType.Static
@ -332,8 +335,7 @@ export const KubernetesProvider = ({ gatewayService }: TKubernetesProviderDTO):
gatewayId: providerInputs.gatewayId, gatewayId: providerInputs.gatewayId,
targetHost: k8sGatewayHost, targetHost: k8sGatewayHost,
targetPort: k8sPort, targetPort: k8sPort,
enableSsl: providerInputs.sslEnabled, httpsAgent,
caCert: providerInputs.ca,
reviewTokenThroughGateway: false reviewTokenThroughGateway: false
}, },
providerInputs.credentialType === KubernetesCredentialType.Static providerInputs.credentialType === KubernetesCredentialType.Static
@ -342,9 +344,9 @@ export const KubernetesProvider = ({ gatewayService }: TKubernetesProviderDTO):
); );
} }
} else if (providerInputs.credentialType === KubernetesCredentialType.Static) { } else if (providerInputs.credentialType === KubernetesCredentialType.Static) {
await serviceAccountStaticCallback(k8sHost, k8sPort); await serviceAccountStaticCallback(k8sHost, k8sPort, httpsAgent);
} else { } else {
await serviceAccountDynamicCallback(k8sHost, k8sPort); await serviceAccountDynamicCallback(k8sHost, k8sPort, httpsAgent);
} }
return true; return true;
@ -546,6 +548,15 @@ export const KubernetesProvider = ({ gatewayService }: TKubernetesProviderDTO):
try { try {
let tokenData; let tokenData;
const httpsAgent =
providerInputs.ca && providerInputs.sslEnabled
? new https.Agent({
ca: providerInputs.ca,
rejectUnauthorized: true
})
: undefined;
if (providerInputs.gatewayId) { if (providerInputs.gatewayId) {
if (providerInputs.authMethod === KubernetesAuthMethod.Gateway) { if (providerInputs.authMethod === KubernetesAuthMethod.Gateway) {
tokenData = await $gatewayProxyWrapper( tokenData = await $gatewayProxyWrapper(
@ -553,8 +564,7 @@ export const KubernetesProvider = ({ gatewayService }: TKubernetesProviderDTO):
gatewayId: providerInputs.gatewayId, gatewayId: providerInputs.gatewayId,
targetHost: k8sHost, targetHost: k8sHost,
targetPort: k8sPort, targetPort: k8sPort,
enableSsl: providerInputs.sslEnabled, httpsAgent,
caCert: providerInputs.ca,
reviewTokenThroughGateway: true reviewTokenThroughGateway: true
}, },
providerInputs.credentialType === KubernetesCredentialType.Static providerInputs.credentialType === KubernetesCredentialType.Static
@ -567,8 +577,7 @@ export const KubernetesProvider = ({ gatewayService }: TKubernetesProviderDTO):
gatewayId: providerInputs.gatewayId, gatewayId: providerInputs.gatewayId,
targetHost: k8sGatewayHost, targetHost: k8sGatewayHost,
targetPort: k8sPort, targetPort: k8sPort,
enableSsl: providerInputs.sslEnabled, httpsAgent,
caCert: providerInputs.ca,
reviewTokenThroughGateway: false reviewTokenThroughGateway: false
}, },
providerInputs.credentialType === KubernetesCredentialType.Static providerInputs.credentialType === KubernetesCredentialType.Static
@ -579,8 +588,8 @@ export const KubernetesProvider = ({ gatewayService }: TKubernetesProviderDTO):
} else { } else {
tokenData = tokenData =
providerInputs.credentialType === KubernetesCredentialType.Static providerInputs.credentialType === KubernetesCredentialType.Static
? await tokenRequestStaticCallback(k8sHost, k8sPort) ? await tokenRequestStaticCallback(k8sHost, k8sPort, httpsAgent)
: await serviceAccountDynamicCallback(k8sHost, k8sPort); : await serviceAccountDynamicCallback(k8sHost, k8sPort, httpsAgent);
} }
return { return {
@ -684,6 +693,14 @@ export const KubernetesProvider = ({ gatewayService }: TKubernetesProviderDTO):
const k8sPort = url.port ? Number(url.port) : 443; const k8sPort = url.port ? Number(url.port) : 443;
const k8sHost = `${url.protocol}//${url.hostname}`; const k8sHost = `${url.protocol}//${url.hostname}`;
const httpsAgent =
providerInputs.ca && providerInputs.sslEnabled
? new https.Agent({
ca: providerInputs.ca,
rejectUnauthorized: true
})
: undefined;
if (providerInputs.gatewayId) { if (providerInputs.gatewayId) {
if (providerInputs.authMethod === KubernetesAuthMethod.Gateway) { if (providerInputs.authMethod === KubernetesAuthMethod.Gateway) {
await $gatewayProxyWrapper( await $gatewayProxyWrapper(
@ -691,8 +708,7 @@ export const KubernetesProvider = ({ gatewayService }: TKubernetesProviderDTO):
gatewayId: providerInputs.gatewayId, gatewayId: providerInputs.gatewayId,
targetHost: k8sHost, targetHost: k8sHost,
targetPort: k8sPort, targetPort: k8sPort,
enableSsl: providerInputs.sslEnabled, httpsAgent,
caCert: providerInputs.ca,
reviewTokenThroughGateway: true reviewTokenThroughGateway: true
}, },
serviceAccountDynamicCallback serviceAccountDynamicCallback
@ -703,15 +719,14 @@ export const KubernetesProvider = ({ gatewayService }: TKubernetesProviderDTO):
gatewayId: providerInputs.gatewayId, gatewayId: providerInputs.gatewayId,
targetHost: k8sGatewayHost, targetHost: k8sGatewayHost,
targetPort: k8sPort, targetPort: k8sPort,
enableSsl: providerInputs.sslEnabled, httpsAgent,
caCert: providerInputs.ca,
reviewTokenThroughGateway: false reviewTokenThroughGateway: false
}, },
serviceAccountDynamicCallback serviceAccountDynamicCallback
); );
} }
} else { } else {
await serviceAccountDynamicCallback(k8sHost, k8sPort); await serviceAccountDynamicCallback(k8sHost, k8sPort, httpsAgent);
} }
} }

View File

@ -2,6 +2,7 @@ import RE2 from "re2";
import { z } from "zod"; import { z } from "zod";
import { CharacterType, characterValidator } from "@app/lib/validator/validate-string"; import { CharacterType, characterValidator } from "@app/lib/validator/validate-string";
import { ResourceMetadataSchema } from "@app/services/resource-metadata/resource-metadata-schema";
import { TDynamicSecretLeaseConfig } from "../../dynamic-secret-lease/dynamic-secret-lease-types"; import { TDynamicSecretLeaseConfig } from "../../dynamic-secret-lease/dynamic-secret-lease-types";
@ -207,7 +208,8 @@ export const DynamicSecretAwsIamSchema = z.preprocess(
permissionBoundaryPolicyArn: z.string().trim().optional(), permissionBoundaryPolicyArn: z.string().trim().optional(),
policyDocument: z.string().trim().optional(), policyDocument: z.string().trim().optional(),
userGroups: z.string().trim().optional(), userGroups: z.string().trim().optional(),
policyArns: z.string().trim().optional() policyArns: z.string().trim().optional(),
tags: ResourceMetadataSchema.optional()
}), }),
z.object({ z.object({
method: z.literal(AwsIamAuthType.AssumeRole), method: z.literal(AwsIamAuthType.AssumeRole),
@ -217,7 +219,8 @@ export const DynamicSecretAwsIamSchema = z.preprocess(
permissionBoundaryPolicyArn: z.string().trim().optional(), permissionBoundaryPolicyArn: z.string().trim().optional(),
policyDocument: z.string().trim().optional(), policyDocument: z.string().trim().optional(),
userGroups: z.string().trim().optional(), userGroups: z.string().trim().optional(),
policyArns: z.string().trim().optional() policyArns: z.string().trim().optional(),
tags: ResourceMetadataSchema.optional()
}) })
]) ])
); );
@ -474,6 +477,23 @@ export const DynamicSecretGcpIamSchema = z.object({
serviceAccountEmail: z.string().email().trim().min(1, "Service account email required").max(128) serviceAccountEmail: z.string().email().trim().min(1, "Service account email required").max(128)
}); });
export const DynamicSecretGithubSchema = z.object({
appId: z.number().min(1).describe("The ID of your GitHub App."),
installationId: z.number().min(1).describe("The ID of the GitHub App installation."),
privateKey: z
.string()
.trim()
.min(1)
.refine(
(val) =>
new RE2(
/^-----BEGIN(?:(?: RSA| PGP| ENCRYPTED)? PRIVATE KEY)-----\s*[\s\S]*?-----END(?:(?: RSA| PGP| ENCRYPTED)? PRIVATE KEY)-----$/
).test(val),
"Invalid PEM format for private key"
)
.describe("The private key generated for your GitHub App.")
});
export enum DynamicSecretProviders { export enum DynamicSecretProviders {
SqlDatabase = "sql-database", SqlDatabase = "sql-database",
Cassandra = "cassandra", Cassandra = "cassandra",
@ -492,7 +512,8 @@ export enum DynamicSecretProviders {
SapAse = "sap-ase", SapAse = "sap-ase",
Kubernetes = "kubernetes", Kubernetes = "kubernetes",
Vertica = "vertica", Vertica = "vertica",
GcpIam = "gcp-iam" GcpIam = "gcp-iam",
Github = "github"
} }
export const DynamicSecretProviderSchema = z.discriminatedUnion("type", [ export const DynamicSecretProviderSchema = z.discriminatedUnion("type", [
@ -513,7 +534,8 @@ export const DynamicSecretProviderSchema = z.discriminatedUnion("type", [
z.object({ type: z.literal(DynamicSecretProviders.Totp), inputs: DynamicSecretTotpSchema }), z.object({ type: z.literal(DynamicSecretProviders.Totp), inputs: DynamicSecretTotpSchema }),
z.object({ type: z.literal(DynamicSecretProviders.Kubernetes), inputs: DynamicSecretKubernetesSchema }), z.object({ type: z.literal(DynamicSecretProviders.Kubernetes), inputs: DynamicSecretKubernetesSchema }),
z.object({ type: z.literal(DynamicSecretProviders.Vertica), inputs: DynamicSecretVerticaSchema }), z.object({ type: z.literal(DynamicSecretProviders.Vertica), inputs: DynamicSecretVerticaSchema }),
z.object({ type: z.literal(DynamicSecretProviders.GcpIam), inputs: DynamicSecretGcpIamSchema }) z.object({ type: z.literal(DynamicSecretProviders.GcpIam), inputs: DynamicSecretGcpIamSchema }),
z.object({ type: z.literal(DynamicSecretProviders.Github), inputs: DynamicSecretGithubSchema })
]); ]);
export type TDynamicProviderFns = { export type TDynamicProviderFns = {

View File

@ -169,11 +169,29 @@ export const groupDALFactory = (db: TDbClient) => {
} }
}; };
const findById = async (id: string, tx?: Knex) => {
try {
const doc = await (tx || db.replicaNode())(TableName.Groups)
.leftJoin(TableName.OrgRoles, `${TableName.Groups}.roleId`, `${TableName.OrgRoles}.id`)
.where(`${TableName.Groups}.id`, id)
.select(
selectAllTableCols(TableName.Groups),
db.ref("slug").as("customRoleSlug").withSchema(TableName.OrgRoles)
)
.first();
return doc;
} catch (error) {
throw new DatabaseError({ error, name: "Find by id" });
}
};
return { return {
...groupOrm,
findGroups, findGroups,
findByOrgId, findByOrgId,
findAllGroupPossibleMembers, findAllGroupPossibleMembers,
findGroupsByProjectId, findGroupsByProjectId,
...groupOrm findById
}; };
}; };

View File

@ -698,9 +698,9 @@ export const oidcConfigServiceFactory = ({
// eslint-disable-next-line @typescript-eslint/no-explicit-any // eslint-disable-next-line @typescript-eslint/no-explicit-any
(_req: any, tokenSet: TokenSet, cb: any) => { (_req: any, tokenSet: TokenSet, cb: any) => {
const claims = tokenSet.claims(); const claims = tokenSet.claims();
if (!claims.email || !claims.given_name) { if (!claims.email) {
throw new BadRequestError({ throw new BadRequestError({
message: "Invalid request. Missing email or first name" message: "Invalid request. Missing email claim."
}); });
} }
@ -713,12 +713,19 @@ export const oidcConfigServiceFactory = ({
} }
} }
const name = claims?.given_name || claims?.name;
if (!name) {
throw new BadRequestError({
message: "Invalid request. Missing name claim."
});
}
const groups = typeof claims.groups === "string" ? [claims.groups] : (claims.groups as string[] | undefined); const groups = typeof claims.groups === "string" ? [claims.groups] : (claims.groups as string[] | undefined);
oidcLogin({ oidcLogin({
email: claims.email.toLowerCase(), email: claims.email.toLowerCase(),
externalId: claims.sub, externalId: claims.sub,
firstName: claims.given_name ?? "", firstName: name,
lastName: claims.family_name ?? "", lastName: claims.family_name ?? "",
orgId: org.id, orgId: org.id,
groups, groups,

View File

@ -211,6 +211,11 @@ export type SecretFolderSubjectFields = {
secretPath: string; secretPath: string;
}; };
export type SecretSyncSubjectFields = {
environment: string;
secretPath: string;
};
export type DynamicSecretSubjectFields = { export type DynamicSecretSubjectFields = {
environment: string; environment: string;
secretPath: string; secretPath: string;
@ -267,6 +272,10 @@ export type ProjectPermissionSet =
| (ForcedSubject<ProjectPermissionSub.DynamicSecrets> & DynamicSecretSubjectFields) | (ForcedSubject<ProjectPermissionSub.DynamicSecrets> & DynamicSecretSubjectFields)
) )
] ]
| [
ProjectPermissionSecretSyncActions,
ProjectPermissionSub.SecretSyncs | (ForcedSubject<ProjectPermissionSub.SecretSyncs> & SecretSyncSubjectFields)
]
| [ | [
ProjectPermissionActions, ProjectPermissionActions,
( (
@ -323,7 +332,6 @@ export type ProjectPermissionSet =
| [ProjectPermissionActions, ProjectPermissionSub.SshHostGroups] | [ProjectPermissionActions, ProjectPermissionSub.SshHostGroups]
| [ProjectPermissionActions, ProjectPermissionSub.PkiAlerts] | [ProjectPermissionActions, ProjectPermissionSub.PkiAlerts]
| [ProjectPermissionActions, ProjectPermissionSub.PkiCollections] | [ProjectPermissionActions, ProjectPermissionSub.PkiCollections]
| [ProjectPermissionSecretSyncActions, ProjectPermissionSub.SecretSyncs]
| [ProjectPermissionKmipActions, ProjectPermissionSub.Kmip] | [ProjectPermissionKmipActions, ProjectPermissionSub.Kmip]
| [ProjectPermissionCmekActions, ProjectPermissionSub.Cmek] | [ProjectPermissionCmekActions, ProjectPermissionSub.Cmek]
| [ProjectPermissionActions.Delete, ProjectPermissionSub.Project] | [ProjectPermissionActions.Delete, ProjectPermissionSub.Project]
@ -412,6 +420,23 @@ const DynamicSecretConditionV2Schema = z
}) })
.partial(); .partial();
const SecretSyncConditionV2Schema = z
.object({
environment: z.union([
z.string(),
z
.object({
[PermissionConditionOperators.$EQ]: PermissionConditionSchema[PermissionConditionOperators.$EQ],
[PermissionConditionOperators.$NEQ]: PermissionConditionSchema[PermissionConditionOperators.$NEQ],
[PermissionConditionOperators.$IN]: PermissionConditionSchema[PermissionConditionOperators.$IN],
[PermissionConditionOperators.$GLOB]: PermissionConditionSchema[PermissionConditionOperators.$GLOB]
})
.partial()
]),
secretPath: SECRET_PATH_PERMISSION_OPERATOR_SCHEMA
})
.partial();
const SecretImportConditionSchema = z const SecretImportConditionSchema = z
.object({ .object({
environment: z.union([ environment: z.union([
@ -671,12 +696,6 @@ const GeneralPermissionSchema = [
"Describe what action an entity can take." "Describe what action an entity can take."
) )
}), }),
z.object({
subject: z.literal(ProjectPermissionSub.SecretSyncs).describe("The entity this permission pertains to."),
action: CASL_ACTION_SCHEMA_NATIVE_ENUM(ProjectPermissionSecretSyncActions).describe(
"Describe what action an entity can take."
)
}),
z.object({ z.object({
subject: z.literal(ProjectPermissionSub.Kmip).describe("The entity this permission pertains to."), subject: z.literal(ProjectPermissionSub.Kmip).describe("The entity this permission pertains to."),
action: CASL_ACTION_SCHEMA_NATIVE_ENUM(ProjectPermissionKmipActions).describe( action: CASL_ACTION_SCHEMA_NATIVE_ENUM(ProjectPermissionKmipActions).describe(
@ -836,6 +855,16 @@ export const ProjectPermissionV2Schema = z.discriminatedUnion("subject", [
"When specified, only matching conditions will be allowed to access given resource." "When specified, only matching conditions will be allowed to access given resource."
).optional() ).optional()
}), }),
z.object({
subject: z.literal(ProjectPermissionSub.SecretSyncs).describe("The entity this permission pertains to."),
inverted: z.boolean().optional().describe("Whether rule allows or forbids."),
action: CASL_ACTION_SCHEMA_NATIVE_ENUM(ProjectPermissionSecretSyncActions).describe(
"Describe what action an entity can take."
),
conditions: SecretSyncConditionV2Schema.describe(
"When specified, only matching conditions will be allowed to access given resource."
).optional()
}),
...GeneralPermissionSchema ...GeneralPermissionSchema
]); ]);

View File

@ -24,6 +24,7 @@ type TFindQueryFilter = {
committer?: string; committer?: string;
limit?: number; limit?: number;
offset?: number; offset?: number;
search?: string;
}; };
export const secretApprovalRequestDALFactory = (db: TDbClient) => { export const secretApprovalRequestDALFactory = (db: TDbClient) => {
@ -314,7 +315,6 @@ export const secretApprovalRequestDALFactory = (db: TDbClient) => {
.where(`${TableName.SecretApprovalPolicyApprover}.approverUserId`, userId) .where(`${TableName.SecretApprovalPolicyApprover}.approverUserId`, userId)
.orWhere(`${TableName.SecretApprovalRequest}.committerUserId`, userId) .orWhere(`${TableName.SecretApprovalRequest}.committerUserId`, userId)
) )
.andWhere((bd) => void bd.where(`${TableName.SecretApprovalPolicy}.deletedAt`, null))
.select("status", `${TableName.SecretApprovalRequest}.id`) .select("status", `${TableName.SecretApprovalRequest}.id`)
.groupBy(`${TableName.SecretApprovalRequest}.id`, "status") .groupBy(`${TableName.SecretApprovalRequest}.id`, "status")
.count("status") .count("status")
@ -340,13 +340,13 @@ export const secretApprovalRequestDALFactory = (db: TDbClient) => {
}; };
const findByProjectId = async ( const findByProjectId = async (
{ status, limit = 20, offset = 0, projectId, committer, environment, userId }: TFindQueryFilter, { status, limit = 20, offset = 0, projectId, committer, environment, userId, search }: TFindQueryFilter,
tx?: Knex tx?: Knex
) => { ) => {
try { try {
// akhilmhdh: If ever u wanted a 1 to so many relationship connected with pagination // akhilmhdh: If ever u wanted a 1 to so many relationship connected with pagination
// this is the place u wanna look at. // this is the place u wanna look at.
const query = (tx || db.replicaNode())(TableName.SecretApprovalRequest) const innerQuery = (tx || db.replicaNode())(TableName.SecretApprovalRequest)
.join(TableName.SecretFolder, `${TableName.SecretApprovalRequest}.folderId`, `${TableName.SecretFolder}.id`) .join(TableName.SecretFolder, `${TableName.SecretApprovalRequest}.folderId`, `${TableName.SecretFolder}.id`)
.join(TableName.Environment, `${TableName.SecretFolder}.envId`, `${TableName.Environment}.id`) .join(TableName.Environment, `${TableName.SecretFolder}.envId`, `${TableName.Environment}.id`)
.join( .join(
@ -435,7 +435,30 @@ export const secretApprovalRequestDALFactory = (db: TDbClient) => {
db.ref("firstName").withSchema("committerUser").as("committerUserFirstName"), db.ref("firstName").withSchema("committerUser").as("committerUserFirstName"),
db.ref("lastName").withSchema("committerUser").as("committerUserLastName") db.ref("lastName").withSchema("committerUser").as("committerUserLastName")
) )
.orderBy("createdAt", "desc"); .distinctOn(`${TableName.SecretApprovalRequest}.id`)
.as("inner");
const query = (tx || db)
.select("*")
.select(db.raw("count(*) OVER() as total_count"))
.from(innerQuery)
.orderBy("createdAt", "desc") as typeof innerQuery;
if (search) {
void query.where((qb) => {
void qb
.whereRaw(`CONCAT_WS(' ', ??, ??) ilike ?`, [
db.ref("firstName").withSchema("committerUser"),
db.ref("lastName").withSchema("committerUser"),
`%${search}%`
])
.orWhereRaw(`?? ilike ?`, [db.ref("username").withSchema("committerUser"), `%${search}%`])
.orWhereRaw(`?? ilike ?`, [db.ref("email").withSchema("committerUser"), `%${search}%`])
.orWhereILike(`${TableName.Environment}.name`, `%${search}%`)
.orWhereILike(`${TableName.Environment}.slug`, `%${search}%`)
.orWhereILike(`${TableName.SecretApprovalPolicy}.secretPath`, `%${search}%`);
});
}
const docs = await (tx || db) const docs = await (tx || db)
.with("w", query) .with("w", query)
@ -443,6 +466,10 @@ export const secretApprovalRequestDALFactory = (db: TDbClient) => {
.from<Awaited<typeof query>[number]>("w") .from<Awaited<typeof query>[number]>("w")
.where("w.rank", ">=", offset) .where("w.rank", ">=", offset)
.andWhere("w.rank", "<", offset + limit); .andWhere("w.rank", "<", offset + limit);
// @ts-expect-error knex does not infer
const totalCount = Number(docs[0]?.total_count || 0);
const formattedDoc = sqlNestRelationships({ const formattedDoc = sqlNestRelationships({
data: docs, data: docs,
key: "id", key: "id",
@ -504,23 +531,26 @@ export const secretApprovalRequestDALFactory = (db: TDbClient) => {
} }
] ]
}); });
return formattedDoc.map((el) => ({ return {
...el, approvals: formattedDoc.map((el) => ({
policy: { ...el.policy, approvers: el.approvers, bypassers: el.bypassers } ...el,
})); policy: { ...el.policy, approvers: el.approvers, bypassers: el.bypassers }
})),
totalCount
};
} catch (error) { } catch (error) {
throw new DatabaseError({ error, name: "FindSAR" }); throw new DatabaseError({ error, name: "FindSAR" });
} }
}; };
const findByProjectIdBridgeSecretV2 = async ( const findByProjectIdBridgeSecretV2 = async (
{ status, limit = 20, offset = 0, projectId, committer, environment, userId }: TFindQueryFilter, { status, limit = 20, offset = 0, projectId, committer, environment, userId, search }: TFindQueryFilter,
tx?: Knex tx?: Knex
) => { ) => {
try { try {
// akhilmhdh: If ever u wanted a 1 to so many relationship connected with pagination // akhilmhdh: If ever u wanted a 1 to so many relationship connected with pagination
// this is the place u wanna look at. // this is the place u wanna look at.
const query = (tx || db.replicaNode())(TableName.SecretApprovalRequest) const innerQuery = (tx || db.replicaNode())(TableName.SecretApprovalRequest)
.join(TableName.SecretFolder, `${TableName.SecretApprovalRequest}.folderId`, `${TableName.SecretFolder}.id`) .join(TableName.SecretFolder, `${TableName.SecretApprovalRequest}.folderId`, `${TableName.SecretFolder}.id`)
.join(TableName.Environment, `${TableName.SecretFolder}.envId`, `${TableName.Environment}.id`) .join(TableName.Environment, `${TableName.SecretFolder}.envId`, `${TableName.Environment}.id`)
.join( .join(
@ -609,14 +639,42 @@ export const secretApprovalRequestDALFactory = (db: TDbClient) => {
db.ref("firstName").withSchema("committerUser").as("committerUserFirstName"), db.ref("firstName").withSchema("committerUser").as("committerUserFirstName"),
db.ref("lastName").withSchema("committerUser").as("committerUserLastName") db.ref("lastName").withSchema("committerUser").as("committerUserLastName")
) )
.orderBy("createdAt", "desc"); .distinctOn(`${TableName.SecretApprovalRequest}.id`)
.as("inner");
const query = (tx || db)
.select("*")
.select(db.raw("count(*) OVER() as total_count"))
.from(innerQuery)
.orderBy("createdAt", "desc") as typeof innerQuery;
if (search) {
void query.where((qb) => {
void qb
.whereRaw(`CONCAT_WS(' ', ??, ??) ilike ?`, [
db.ref("firstName").withSchema("committerUser"),
db.ref("lastName").withSchema("committerUser"),
`%${search}%`
])
.orWhereRaw(`?? ilike ?`, [db.ref("username").withSchema("committerUser"), `%${search}%`])
.orWhereRaw(`?? ilike ?`, [db.ref("email").withSchema("committerUser"), `%${search}%`])
.orWhereILike(`${TableName.Environment}.name`, `%${search}%`)
.orWhereILike(`${TableName.Environment}.slug`, `%${search}%`)
.orWhereILike(`${TableName.SecretApprovalPolicy}.secretPath`, `%${search}%`);
});
}
const rankOffset = offset + 1;
const docs = await (tx || db) const docs = await (tx || db)
.with("w", query) .with("w", query)
.select("*") .select("*")
.from<Awaited<typeof query>[number]>("w") .from<Awaited<typeof query>[number]>("w")
.where("w.rank", ">=", offset) .where("w.rank", ">=", rankOffset)
.andWhere("w.rank", "<", offset + limit); .andWhere("w.rank", "<", rankOffset + limit);
// @ts-expect-error knex does not infer
const totalCount = Number(docs[0]?.total_count || 0);
const formattedDoc = sqlNestRelationships({ const formattedDoc = sqlNestRelationships({
data: docs, data: docs,
key: "id", key: "id",
@ -682,10 +740,13 @@ export const secretApprovalRequestDALFactory = (db: TDbClient) => {
} }
] ]
}); });
return formattedDoc.map((el) => ({ return {
...el, approvals: formattedDoc.map((el) => ({
policy: { ...el.policy, approvers: el.approvers, bypassers: el.bypassers } ...el,
})); policy: { ...el.policy, approvers: el.approvers, bypassers: el.bypassers }
})),
totalCount
};
} catch (error) { } catch (error) {
throw new DatabaseError({ error, name: "FindSAR" }); throw new DatabaseError({ error, name: "FindSAR" });
} }

View File

@ -194,7 +194,8 @@ export const secretApprovalRequestServiceFactory = ({
environment, environment,
committer, committer,
limit, limit,
offset offset,
search
}: TListApprovalsDTO) => { }: TListApprovalsDTO) => {
if (actor === ActorType.SERVICE) throw new BadRequestError({ message: "Cannot use service token" }); if (actor === ActorType.SERVICE) throw new BadRequestError({ message: "Cannot use service token" });
@ -208,6 +209,7 @@ export const secretApprovalRequestServiceFactory = ({
}); });
const { shouldUseSecretV2Bridge } = await projectBotService.getBotKey(projectId); const { shouldUseSecretV2Bridge } = await projectBotService.getBotKey(projectId);
if (shouldUseSecretV2Bridge) { if (shouldUseSecretV2Bridge) {
return secretApprovalRequestDAL.findByProjectIdBridgeSecretV2({ return secretApprovalRequestDAL.findByProjectIdBridgeSecretV2({
projectId, projectId,
@ -216,19 +218,21 @@ export const secretApprovalRequestServiceFactory = ({
status, status,
userId: actorId, userId: actorId,
limit, limit,
offset offset,
search
}); });
} }
const approvals = await secretApprovalRequestDAL.findByProjectId({
return secretApprovalRequestDAL.findByProjectId({
projectId, projectId,
committer, committer,
environment, environment,
status, status,
userId: actorId, userId: actorId,
limit, limit,
offset offset,
search
}); });
return approvals;
}; };
const getSecretApprovalDetails = async ({ const getSecretApprovalDetails = async ({

View File

@ -93,6 +93,7 @@ export type TListApprovalsDTO = {
committer?: string; committer?: string;
limit?: number; limit?: number;
offset?: number; offset?: number;
search?: string;
} & TProjectPermission; } & TProjectPermission;
export type TSecretApprovalDetailsDTO = { export type TSecretApprovalDetailsDTO = {

View File

@ -11,7 +11,8 @@ export const PgSqlLock = {
OrgGatewayRootCaInit: (orgId: string) => pgAdvisoryLockHashText(`org-gateway-root-ca:${orgId}`), OrgGatewayRootCaInit: (orgId: string) => pgAdvisoryLockHashText(`org-gateway-root-ca:${orgId}`),
OrgGatewayCertExchange: (orgId: string) => pgAdvisoryLockHashText(`org-gateway-cert-exchange:${orgId}`), OrgGatewayCertExchange: (orgId: string) => pgAdvisoryLockHashText(`org-gateway-cert-exchange:${orgId}`),
SecretRotationV2Creation: (folderId: string) => pgAdvisoryLockHashText(`secret-rotation-v2-creation:${folderId}`), SecretRotationV2Creation: (folderId: string) => pgAdvisoryLockHashText(`secret-rotation-v2-creation:${folderId}`),
CreateProject: (orgId: string) => pgAdvisoryLockHashText(`create-project:${orgId}`) CreateProject: (orgId: string) => pgAdvisoryLockHashText(`create-project:${orgId}`),
CreateFolder: (envId: string, projectId: string) => pgAdvisoryLockHashText(`create-folder:${envId}-${projectId}`)
} as const; } as const;
// all the key prefixes used must be set here to avoid conflict // all the key prefixes used must be set here to avoid conflict

View File

@ -111,12 +111,14 @@ export const IDENTITIES = {
CREATE: { CREATE: {
name: "The name of the identity to create.", name: "The name of the identity to create.",
organizationId: "The organization ID to which the identity belongs.", organizationId: "The organization ID to which the identity belongs.",
role: "The role of the identity. Possible values are 'no-access', 'member', and 'admin'." role: "The role of the identity. Possible values are 'no-access', 'member', and 'admin'.",
hasDeleteProtection: "Prevents deletion of the identity when enabled."
}, },
UPDATE: { UPDATE: {
identityId: "The ID of the identity to update.", identityId: "The ID of the identity to update.",
name: "The new name of the identity.", name: "The new name of the identity.",
role: "The new role of the identity." role: "The new role of the identity.",
hasDeleteProtection: "Prevents deletion of the identity when enabled."
}, },
DELETE: { DELETE: {
identityId: "The ID of the identity to delete." identityId: "The ID of the identity to delete."
@ -2223,6 +2225,9 @@ export const AppConnections = {
ONEPASS: { ONEPASS: {
instanceUrl: "The URL of the 1Password Connect Server instance to authenticate with.", instanceUrl: "The URL of the 1Password Connect Server instance to authenticate with.",
apiToken: "The API token used to access the 1Password Connect Server." apiToken: "The API token used to access the 1Password Connect Server."
},
FLYIO: {
accessToken: "The Access Token used to access fly.io."
} }
} }
}; };
@ -2384,6 +2389,22 @@ export const SecretSyncs = {
}, },
ONEPASS: { ONEPASS: {
vaultId: "The ID of the 1Password vault to sync secrets to." vaultId: "The ID of the 1Password vault to sync secrets to."
},
HEROKU: {
app: "The ID of the Heroku app to sync secrets to.",
appName: "The name of the Heroku app to sync secrets to."
},
RENDER: {
serviceId: "The ID of the Render service to sync secrets to.",
scope: "The Render scope that secrets should be synced to.",
type: "The Render resource type to sync secrets to."
},
FLYIO: {
appId: "The ID of the Fly.io app to sync secrets to."
},
CLOUDFLARE_PAGES: {
projectName: "The name of the Cloudflare Pages project to sync secrets to.",
environment: "The environment of the Cloudflare Pages project to sync secrets to."
} }
} }
}; };

View File

@ -19,3 +19,5 @@ export const getMinExpiresIn = (exp1: string | number, exp2: string | number): s
return ms1 <= ms2 ? exp1 : exp2; return ms1 <= ms2 ? exp1 : exp2;
}; };
export const convertMsToSecond = (time: number) => time / 1000;

View File

@ -1,3 +1,4 @@
import { TDynamicSecrets } from "@app/db/schemas";
import { ActorAuthMethod, ActorType } from "@app/services/auth/auth-type"; import { ActorAuthMethod, ActorType } from "@app/services/auth/auth-type";
export type TGenericPermission = { export type TGenericPermission = {
@ -84,3 +85,7 @@ export enum QueueWorkerProfile {
Standard = "standard", Standard = "standard",
SecretScanning = "secret-scanning" SecretScanning = "secret-scanning"
} }
export interface TDynamicSecretWithMetadata extends TDynamicSecrets {
metadata: { id: string; key: string; value: string }[];
}

View File

@ -377,6 +377,7 @@ export type TQueueServiceFactory = {
stopRepeatableJobByKey: <T extends QueueName>(name: T, repeatJobKey: string) => Promise<boolean>; stopRepeatableJobByKey: <T extends QueueName>(name: T, repeatJobKey: string) => Promise<boolean>;
clearQueue: (name: QueueName) => Promise<void>; clearQueue: (name: QueueName) => Promise<void>;
stopJobById: <T extends QueueName>(name: T, jobId: string) => Promise<void | undefined>; stopJobById: <T extends QueueName>(name: T, jobId: string) => Promise<void | undefined>;
stopJobByIdPg: <T extends QueueName>(name: T, jobId: string) => Promise<void | undefined>;
getRepeatableJobs: ( getRepeatableJobs: (
name: QueueName, name: QueueName,
startOffset?: number, startOffset?: number,
@ -542,6 +543,10 @@ export const queueServiceFactory = (
return q.removeRepeatableByKey(repeatJobKey); return q.removeRepeatableByKey(repeatJobKey);
}; };
const stopJobByIdPg: TQueueServiceFactory["stopJobByIdPg"] = async (name, jobId) => {
await pgBoss.deleteJob(name, jobId);
};
const stopJobById: TQueueServiceFactory["stopJobById"] = async (name, jobId) => { const stopJobById: TQueueServiceFactory["stopJobById"] = async (name, jobId) => {
const q = queueContainer[name]; const q = queueContainer[name];
const job = await q.getJob(jobId); const job = await q.getJob(jobId);
@ -568,6 +573,7 @@ export const queueServiceFactory = (
stopRepeatableJobByKey, stopRepeatableJobByKey,
clearQueue, clearQueue,
stopJobById, stopJobById,
stopJobByIdPg,
getRepeatableJobs, getRepeatableJobs,
startPg, startPg,
queuePg, queuePg,

View File

@ -1903,6 +1903,7 @@ export const registerRoutes = async (
await pkiSubscriberQueue.startDailyAutoRenewalJob(); await pkiSubscriberQueue.startDailyAutoRenewalJob();
await kmsService.startService(); await kmsService.startService();
await microsoftTeamsService.start(); await microsoftTeamsService.start();
await dynamicSecretQueueService.init();
// inject all services // inject all services
server.decorate<FastifyZodProvider["services"]>("services", { server.decorate<FastifyZodProvider["services"]>("services", {
@ -2020,10 +2021,16 @@ export const registerRoutes = async (
if (licenseSyncJob) { if (licenseSyncJob) {
cronJobs.push(licenseSyncJob); cronJobs.push(licenseSyncJob);
} }
const microsoftTeamsSyncJob = await microsoftTeamsService.initializeBackgroundSync(); const microsoftTeamsSyncJob = await microsoftTeamsService.initializeBackgroundSync();
if (microsoftTeamsSyncJob) { if (microsoftTeamsSyncJob) {
cronJobs.push(microsoftTeamsSyncJob); cronJobs.push(microsoftTeamsSyncJob);
} }
const adminIntegrationsSyncJob = await superAdminService.initializeAdminIntegrationConfigSync();
if (adminIntegrationsSyncJob) {
cronJobs.push(adminIntegrationsSyncJob);
}
} }
server.decorate<FastifyZodProvider["store"]>("store", { server.decorate<FastifyZodProvider["store"]>("store", {

View File

@ -37,7 +37,12 @@ export const registerAdminRouter = async (server: FastifyZodProvider) => {
encryptedSlackClientSecret: true, encryptedSlackClientSecret: true,
encryptedMicrosoftTeamsAppId: true, encryptedMicrosoftTeamsAppId: true,
encryptedMicrosoftTeamsClientSecret: true, encryptedMicrosoftTeamsClientSecret: true,
encryptedMicrosoftTeamsBotId: true encryptedMicrosoftTeamsBotId: true,
encryptedGitHubAppConnectionClientId: true,
encryptedGitHubAppConnectionClientSecret: true,
encryptedGitHubAppConnectionSlug: true,
encryptedGitHubAppConnectionId: true,
encryptedGitHubAppConnectionPrivateKey: true
}).extend({ }).extend({
isMigrationModeOn: z.boolean(), isMigrationModeOn: z.boolean(),
defaultAuthOrgSlug: z.string().nullable(), defaultAuthOrgSlug: z.string().nullable(),
@ -87,6 +92,11 @@ export const registerAdminRouter = async (server: FastifyZodProvider) => {
microsoftTeamsAppId: z.string().optional(), microsoftTeamsAppId: z.string().optional(),
microsoftTeamsClientSecret: z.string().optional(), microsoftTeamsClientSecret: z.string().optional(),
microsoftTeamsBotId: z.string().optional(), microsoftTeamsBotId: z.string().optional(),
gitHubAppConnectionClientId: z.string().optional(),
gitHubAppConnectionClientSecret: z.string().optional(),
gitHubAppConnectionSlug: z.string().optional(),
gitHubAppConnectionId: z.string().optional(),
gitHubAppConnectionPrivateKey: z.string().optional(),
authConsentContent: z authConsentContent: z
.string() .string()
.trim() .trim()
@ -348,6 +358,13 @@ export const registerAdminRouter = async (server: FastifyZodProvider) => {
appId: z.string(), appId: z.string(),
clientSecret: z.string(), clientSecret: z.string(),
botId: z.string() botId: z.string()
}),
gitHubAppConnection: z.object({
clientId: z.string(),
clientSecret: z.string(),
appSlug: z.string(),
appId: z.string(),
privateKey: z.string()
}) })
}) })
} }

View File

@ -39,6 +39,7 @@ import {
DatabricksConnectionListItemSchema, DatabricksConnectionListItemSchema,
SanitizedDatabricksConnectionSchema SanitizedDatabricksConnectionSchema
} from "@app/services/app-connection/databricks"; } from "@app/services/app-connection/databricks";
import { FlyioConnectionListItemSchema, SanitizedFlyioConnectionSchema } from "@app/services/app-connection/flyio";
import { GcpConnectionListItemSchema, SanitizedGcpConnectionSchema } from "@app/services/app-connection/gcp"; import { GcpConnectionListItemSchema, SanitizedGcpConnectionSchema } from "@app/services/app-connection/gcp";
import { GitHubConnectionListItemSchema, SanitizedGitHubConnectionSchema } from "@app/services/app-connection/github"; import { GitHubConnectionListItemSchema, SanitizedGitHubConnectionSchema } from "@app/services/app-connection/github";
import { import {
@ -49,6 +50,7 @@ import {
HCVaultConnectionListItemSchema, HCVaultConnectionListItemSchema,
SanitizedHCVaultConnectionSchema SanitizedHCVaultConnectionSchema
} from "@app/services/app-connection/hc-vault"; } from "@app/services/app-connection/hc-vault";
import { HerokuConnectionListItemSchema, SanitizedHerokuConnectionSchema } from "@app/services/app-connection/heroku";
import { import {
HumanitecConnectionListItemSchema, HumanitecConnectionListItemSchema,
SanitizedHumanitecConnectionSchema SanitizedHumanitecConnectionSchema
@ -60,6 +62,10 @@ import {
PostgresConnectionListItemSchema, PostgresConnectionListItemSchema,
SanitizedPostgresConnectionSchema SanitizedPostgresConnectionSchema
} from "@app/services/app-connection/postgres"; } from "@app/services/app-connection/postgres";
import {
RenderConnectionListItemSchema,
SanitizedRenderConnectionSchema
} from "@app/services/app-connection/render/render-connection-schema";
import { import {
SanitizedTeamCityConnectionSchema, SanitizedTeamCityConnectionSchema,
TeamCityConnectionListItemSchema TeamCityConnectionListItemSchema
@ -74,6 +80,10 @@ import {
WindmillConnectionListItemSchema WindmillConnectionListItemSchema
} from "@app/services/app-connection/windmill"; } from "@app/services/app-connection/windmill";
import { AuthMode } from "@app/services/auth/auth-type"; import { AuthMode } from "@app/services/auth/auth-type";
import {
CloudflareConnectionListItemSchema,
SanitizedCloudflareConnectionSchema
} from "@app/services/app-connection/cloudflare/cloudflare-connection-schema";
// can't use discriminated due to multiple schemas for certain apps // can't use discriminated due to multiple schemas for certain apps
const SanitizedAppConnectionSchema = z.union([ const SanitizedAppConnectionSchema = z.union([
@ -100,7 +110,11 @@ const SanitizedAppConnectionSchema = z.union([
...SanitizedTeamCityConnectionSchema.options, ...SanitizedTeamCityConnectionSchema.options,
...SanitizedOCIConnectionSchema.options, ...SanitizedOCIConnectionSchema.options,
...SanitizedOracleDBConnectionSchema.options, ...SanitizedOracleDBConnectionSchema.options,
...SanitizedOnePassConnectionSchema.options ...SanitizedOnePassConnectionSchema.options,
...SanitizedHerokuConnectionSchema.options,
...SanitizedRenderConnectionSchema.options,
...SanitizedFlyioConnectionSchema.options,
...SanitizedCloudflareConnectionSchema.options
]); ]);
const AppConnectionOptionsSchema = z.discriminatedUnion("app", [ const AppConnectionOptionsSchema = z.discriminatedUnion("app", [
@ -127,7 +141,11 @@ const AppConnectionOptionsSchema = z.discriminatedUnion("app", [
TeamCityConnectionListItemSchema, TeamCityConnectionListItemSchema,
OCIConnectionListItemSchema, OCIConnectionListItemSchema,
OracleDBConnectionListItemSchema, OracleDBConnectionListItemSchema,
OnePassConnectionListItemSchema OnePassConnectionListItemSchema,
HerokuConnectionListItemSchema,
RenderConnectionListItemSchema,
FlyioConnectionListItemSchema,
CloudflareConnectionListItemSchema
]); ]);
export const registerAppConnectionRouter = async (server: FastifyZodProvider) => { export const registerAppConnectionRouter = async (server: FastifyZodProvider) => {

View File

@ -0,0 +1,53 @@
import z from "zod";
import { readLimit } from "@app/server/config/rateLimiter";
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
import { AppConnection } from "@app/services/app-connection/app-connection-enums";
import {
CreateCloudflareConnectionSchema,
SanitizedCloudflareConnectionSchema,
UpdateCloudflareConnectionSchema
} from "@app/services/app-connection/cloudflare/cloudflare-connection-schema";
import { AuthMode } from "@app/services/auth/auth-type";
import { registerAppConnectionEndpoints } from "./app-connection-endpoints";
export const registerCloudflareConnectionRouter = async (server: FastifyZodProvider) => {
registerAppConnectionEndpoints({
app: AppConnection.Cloudflare,
server,
sanitizedResponseSchema: SanitizedCloudflareConnectionSchema,
createSchema: CreateCloudflareConnectionSchema,
updateSchema: UpdateCloudflareConnectionSchema
});
// The below endpoints are not exposed and for Infisical App use
server.route({
method: "GET",
url: `/:connectionId/cloudflare-pages-projects`,
config: {
rateLimit: readLimit
},
schema: {
params: z.object({
connectionId: z.string().uuid()
}),
response: {
200: z
.object({
id: z.string(),
name: z.string()
})
.array()
}
},
onRequest: verifyAuth([AuthMode.JWT]),
handler: async (req) => {
const { connectionId } = req.params;
const projects = await server.services.appConnection.cloudflare.listPagesProjects(connectionId, req.permission);
return projects;
}
});
};

View File

@ -0,0 +1,51 @@
import z from "zod";
import { readLimit } from "@app/server/config/rateLimiter";
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
import { AppConnection } from "@app/services/app-connection/app-connection-enums";
import {
CreateFlyioConnectionSchema,
SanitizedFlyioConnectionSchema,
UpdateFlyioConnectionSchema
} from "@app/services/app-connection/flyio";
import { AuthMode } from "@app/services/auth/auth-type";
import { registerAppConnectionEndpoints } from "./app-connection-endpoints";
export const registerFlyioConnectionRouter = async (server: FastifyZodProvider) => {
registerAppConnectionEndpoints({
app: AppConnection.Flyio,
server,
sanitizedResponseSchema: SanitizedFlyioConnectionSchema,
createSchema: CreateFlyioConnectionSchema,
updateSchema: UpdateFlyioConnectionSchema
});
// The following endpoints are for internal Infisical App use only and not part of the public API
server.route({
method: "GET",
url: `/:connectionId/apps`,
config: {
rateLimit: readLimit
},
schema: {
params: z.object({
connectionId: z.string().uuid()
}),
response: {
200: z
.object({
id: z.string(),
name: z.string()
})
.array()
}
},
onRequest: verifyAuth([AuthMode.JWT]),
handler: async (req) => {
const { connectionId } = req.params;
const apps = await server.services.appConnection.flyio.listApps(connectionId, req.permission);
return apps;
}
});
};

View File

@ -0,0 +1,54 @@
import z from "zod";
import { readLimit } from "@app/server/config/rateLimiter";
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
import { AppConnection } from "@app/services/app-connection/app-connection-enums";
import {
CreateHerokuConnectionSchema,
SanitizedHerokuConnectionSchema,
THerokuApp,
UpdateHerokuConnectionSchema
} from "@app/services/app-connection/heroku";
import { AuthMode } from "@app/services/auth/auth-type";
import { registerAppConnectionEndpoints } from "./app-connection-endpoints";
export const registerHerokuConnectionRouter = async (server: FastifyZodProvider) => {
registerAppConnectionEndpoints({
app: AppConnection.Heroku,
server,
sanitizedResponseSchema: SanitizedHerokuConnectionSchema,
createSchema: CreateHerokuConnectionSchema,
updateSchema: UpdateHerokuConnectionSchema
});
// The below endpoints are not exposed and for Infisical App use
server.route({
method: "GET",
url: `/:connectionId/apps`,
config: {
rateLimit: readLimit
},
schema: {
params: z.object({
connectionId: z.string().uuid()
}),
response: {
200: z
.object({
id: z.string(),
name: z.string()
})
.array()
}
},
onRequest: verifyAuth([AuthMode.JWT]),
handler: async (req) => {
const { connectionId } = req.params;
const apps: THerokuApp[] = await server.services.appConnection.heroku.listApps(connectionId, req.permission);
return apps;
}
});
};

View File

@ -11,19 +11,23 @@ import { registerAzureDevOpsConnectionRouter } from "./azure-devops-connection-r
import { registerAzureKeyVaultConnectionRouter } from "./azure-key-vault-connection-router"; import { registerAzureKeyVaultConnectionRouter } from "./azure-key-vault-connection-router";
import { registerCamundaConnectionRouter } from "./camunda-connection-router"; import { registerCamundaConnectionRouter } from "./camunda-connection-router";
import { registerDatabricksConnectionRouter } from "./databricks-connection-router"; import { registerDatabricksConnectionRouter } from "./databricks-connection-router";
import { registerFlyioConnectionRouter } from "./flyio-connection-router";
import { registerGcpConnectionRouter } from "./gcp-connection-router"; import { registerGcpConnectionRouter } from "./gcp-connection-router";
import { registerGitHubConnectionRouter } from "./github-connection-router"; import { registerGitHubConnectionRouter } from "./github-connection-router";
import { registerGitHubRadarConnectionRouter } from "./github-radar-connection-router"; import { registerGitHubRadarConnectionRouter } from "./github-radar-connection-router";
import { registerHCVaultConnectionRouter } from "./hc-vault-connection-router"; import { registerHCVaultConnectionRouter } from "./hc-vault-connection-router";
import { registerHerokuConnectionRouter } from "./heroku-connection-router";
import { registerHumanitecConnectionRouter } from "./humanitec-connection-router"; import { registerHumanitecConnectionRouter } from "./humanitec-connection-router";
import { registerLdapConnectionRouter } from "./ldap-connection-router"; import { registerLdapConnectionRouter } from "./ldap-connection-router";
import { registerMsSqlConnectionRouter } from "./mssql-connection-router"; import { registerMsSqlConnectionRouter } from "./mssql-connection-router";
import { registerMySqlConnectionRouter } from "./mysql-connection-router"; import { registerMySqlConnectionRouter } from "./mysql-connection-router";
import { registerPostgresConnectionRouter } from "./postgres-connection-router"; import { registerPostgresConnectionRouter } from "./postgres-connection-router";
import { registerRenderConnectionRouter } from "./render-connection-router";
import { registerTeamCityConnectionRouter } from "./teamcity-connection-router"; import { registerTeamCityConnectionRouter } from "./teamcity-connection-router";
import { registerTerraformCloudConnectionRouter } from "./terraform-cloud-router"; import { registerTerraformCloudConnectionRouter } from "./terraform-cloud-router";
import { registerVercelConnectionRouter } from "./vercel-connection-router"; import { registerVercelConnectionRouter } from "./vercel-connection-router";
import { registerWindmillConnectionRouter } from "./windmill-connection-router"; import { registerWindmillConnectionRouter } from "./windmill-connection-router";
import { registerCloudflareConnectionRouter } from "./cloudflare-connection-router";
export * from "./app-connection-router"; export * from "./app-connection-router";
@ -52,5 +56,9 @@ export const APP_CONNECTION_REGISTER_ROUTER_MAP: Record<AppConnection, (server:
[AppConnection.TeamCity]: registerTeamCityConnectionRouter, [AppConnection.TeamCity]: registerTeamCityConnectionRouter,
[AppConnection.OCI]: registerOCIConnectionRouter, [AppConnection.OCI]: registerOCIConnectionRouter,
[AppConnection.OracleDB]: registerOracleDBConnectionRouter, [AppConnection.OracleDB]: registerOracleDBConnectionRouter,
[AppConnection.OnePass]: registerOnePassConnectionRouter [AppConnection.OnePass]: registerOnePassConnectionRouter,
[AppConnection.Heroku]: registerHerokuConnectionRouter,
[AppConnection.Render]: registerRenderConnectionRouter,
[AppConnection.Flyio]: registerFlyioConnectionRouter,
[AppConnection.Cloudflare]: registerCloudflareConnectionRouter
}; };

View File

@ -0,0 +1,52 @@
import z from "zod";
import { readLimit } from "@app/server/config/rateLimiter";
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
import { AppConnection } from "@app/services/app-connection/app-connection-enums";
import {
CreateRenderConnectionSchema,
SanitizedRenderConnectionSchema,
UpdateRenderConnectionSchema
} from "@app/services/app-connection/render/render-connection-schema";
import { AuthMode } from "@app/services/auth/auth-type";
import { registerAppConnectionEndpoints } from "./app-connection-endpoints";
export const registerRenderConnectionRouter = async (server: FastifyZodProvider) => {
registerAppConnectionEndpoints({
app: AppConnection.Render,
server,
sanitizedResponseSchema: SanitizedRenderConnectionSchema,
createSchema: CreateRenderConnectionSchema,
updateSchema: UpdateRenderConnectionSchema
});
// The below endpoints are not exposed and for Infisical App use
server.route({
method: "GET",
url: `/:connectionId/services`,
config: {
rateLimit: readLimit
},
schema: {
params: z.object({
connectionId: z.string().uuid()
}),
response: {
200: z
.object({
id: z.string(),
name: z.string()
})
.array()
}
},
onRequest: verifyAuth([AuthMode.JWT]),
handler: async (req) => {
const { connectionId } = req.params;
const services = await server.services.appConnection.render.listServices(connectionId, req.permission);
return services;
}
});
};

View File

@ -44,6 +44,7 @@ export const registerIdentityRouter = async (server: FastifyZodProvider) => {
name: z.string().trim().describe(IDENTITIES.CREATE.name), name: z.string().trim().describe(IDENTITIES.CREATE.name),
organizationId: z.string().trim().describe(IDENTITIES.CREATE.organizationId), organizationId: z.string().trim().describe(IDENTITIES.CREATE.organizationId),
role: z.string().trim().min(1).default(OrgMembershipRole.NoAccess).describe(IDENTITIES.CREATE.role), role: z.string().trim().min(1).default(OrgMembershipRole.NoAccess).describe(IDENTITIES.CREATE.role),
hasDeleteProtection: z.boolean().default(false).describe(IDENTITIES.CREATE.hasDeleteProtection),
metadata: z metadata: z
.object({ key: z.string().trim().min(1), value: z.string().trim().min(1) }) .object({ key: z.string().trim().min(1), value: z.string().trim().min(1) })
.array() .array()
@ -75,6 +76,7 @@ export const registerIdentityRouter = async (server: FastifyZodProvider) => {
type: EventType.CREATE_IDENTITY, type: EventType.CREATE_IDENTITY,
metadata: { metadata: {
name: identity.name, name: identity.name,
hasDeleteProtection: identity.hasDeleteProtection,
identityId: identity.id identityId: identity.id
} }
} }
@ -86,6 +88,7 @@ export const registerIdentityRouter = async (server: FastifyZodProvider) => {
properties: { properties: {
orgId: req.body.organizationId, orgId: req.body.organizationId,
name: identity.name, name: identity.name,
hasDeleteProtection: identity.hasDeleteProtection,
identityId: identity.id, identityId: identity.id,
...req.auditLogInfo ...req.auditLogInfo
} }
@ -117,6 +120,7 @@ export const registerIdentityRouter = async (server: FastifyZodProvider) => {
body: z.object({ body: z.object({
name: z.string().trim().optional().describe(IDENTITIES.UPDATE.name), name: z.string().trim().optional().describe(IDENTITIES.UPDATE.name),
role: z.string().trim().min(1).optional().describe(IDENTITIES.UPDATE.role), role: z.string().trim().min(1).optional().describe(IDENTITIES.UPDATE.role),
hasDeleteProtection: z.boolean().optional().describe(IDENTITIES.UPDATE.hasDeleteProtection),
metadata: z metadata: z
.object({ key: z.string().trim().min(1), value: z.string().trim().min(1) }) .object({ key: z.string().trim().min(1), value: z.string().trim().min(1) })
.array() .array()
@ -148,6 +152,7 @@ export const registerIdentityRouter = async (server: FastifyZodProvider) => {
type: EventType.UPDATE_IDENTITY, type: EventType.UPDATE_IDENTITY,
metadata: { metadata: {
name: identity.name, name: identity.name,
hasDeleteProtection: identity.hasDeleteProtection,
identityId: identity.id identityId: identity.id
} }
} }
@ -243,7 +248,7 @@ export const registerIdentityRouter = async (server: FastifyZodProvider) => {
permissions: true, permissions: true,
description: true description: true
}).optional(), }).optional(),
identity: IdentitiesSchema.pick({ name: true, id: true }).extend({ identity: IdentitiesSchema.pick({ name: true, id: true, hasDeleteProtection: true }).extend({
authMethods: z.array(z.string()) authMethods: z.array(z.string())
}) })
}) })
@ -292,7 +297,7 @@ export const registerIdentityRouter = async (server: FastifyZodProvider) => {
permissions: true, permissions: true,
description: true description: true
}).optional(), }).optional(),
identity: IdentitiesSchema.pick({ name: true, id: true }).extend({ identity: IdentitiesSchema.pick({ name: true, id: true, hasDeleteProtection: true }).extend({
authMethods: z.array(z.string()) authMethods: z.array(z.string())
}) })
}).array(), }).array(),
@ -386,7 +391,7 @@ export const registerIdentityRouter = async (server: FastifyZodProvider) => {
permissions: true, permissions: true,
description: true description: true
}).optional(), }).optional(),
identity: IdentitiesSchema.pick({ name: true, id: true }).extend({ identity: IdentitiesSchema.pick({ name: true, id: true, hasDeleteProtection: true }).extend({
authMethods: z.array(z.string()) authMethods: z.array(z.string())
}) })
}).array(), }).array(),
@ -451,7 +456,7 @@ export const registerIdentityRouter = async (server: FastifyZodProvider) => {
temporaryAccessEndTime: z.date().nullable().optional() temporaryAccessEndTime: z.date().nullable().optional()
}) })
), ),
identity: IdentitiesSchema.pick({ name: true, id: true }).extend({ identity: IdentitiesSchema.pick({ name: true, id: true, hasDeleteProtection: true }).extend({
authMethods: z.array(z.string()) authMethods: z.array(z.string())
}), }),
project: SanitizedProjectSchema.pick({ name: true, id: true, type: true }) project: SanitizedProjectSchema.pick({ name: true, id: true, type: true })

View File

@ -83,7 +83,7 @@ export const registerInviteOrgRouter = async (server: FastifyZodProvider) => {
config: { config: {
rateLimit: smtpRateLimit({ rateLimit: smtpRateLimit({
keyGenerator: (req) => keyGenerator: (req) =>
(req.body as { membershipId?: string })?.membershipId?.trim().substring(0, 100) ?? req.realIp (req.body as { membershipId?: string })?.membershipId?.trim().substring(0, 100) || req.realIp
}) })
}, },
method: "POST", method: "POST",

View File

@ -81,7 +81,7 @@ export const registerPasswordRouter = async (server: FastifyZodProvider) => {
url: "/email/password-reset", url: "/email/password-reset",
config: { config: {
rateLimit: smtpRateLimit({ rateLimit: smtpRateLimit({
keyGenerator: (req) => (req.body as { email?: string })?.email?.trim().substring(0, 100) ?? req.realIp keyGenerator: (req) => (req.body as { email?: string })?.email?.trim().substring(0, 100) || req.realIp
}) })
}, },
schema: { schema: {
@ -107,7 +107,9 @@ export const registerPasswordRouter = async (server: FastifyZodProvider) => {
method: "POST", method: "POST",
url: "/email/password-reset-verify", url: "/email/password-reset-verify",
config: { config: {
rateLimit: authRateLimit rateLimit: smtpRateLimit({
keyGenerator: (req) => (req.body as { email?: string })?.email?.trim().substring(0, 100) || req.realIp
})
}, },
schema: { schema: {
body: z.object({ body: z.object({

View File

@ -0,0 +1,16 @@
import { SecretSync } from "@app/services/secret-sync/secret-sync-enums";
import { registerSyncSecretsEndpoints } from "./secret-sync-endpoints";
import {
CloudflarePagesSyncSchema,
CreateCloudflarePagesSyncSchema,
UpdateCloudflarePagesSyncSchema
} from "@app/services/secret-sync/cloudflare-pages/cloudflare-pages-schema";
export const registerCloudflarePagesSyncRouter = async (server: FastifyZodProvider) =>
registerSyncSecretsEndpoints({
destination: SecretSync.CloudflarePages,
server,
responseSchema: CloudflarePagesSyncSchema,
createSchema: CreateCloudflarePagesSyncSchema,
updateSchema: UpdateCloudflarePagesSyncSchema
});

View File

@ -0,0 +1,13 @@
import { CreateFlyioSyncSchema, FlyioSyncSchema, UpdateFlyioSyncSchema } from "@app/services/secret-sync/flyio";
import { SecretSync } from "@app/services/secret-sync/secret-sync-enums";
import { registerSyncSecretsEndpoints } from "./secret-sync-endpoints";
export const registerFlyioSyncRouter = async (server: FastifyZodProvider) =>
registerSyncSecretsEndpoints({
destination: SecretSync.Flyio,
server,
responseSchema: FlyioSyncSchema,
createSchema: CreateFlyioSyncSchema,
updateSchema: UpdateFlyioSyncSchema
});

View File

@ -0,0 +1,13 @@
import { CreateHerokuSyncSchema, HerokuSyncSchema, UpdateHerokuSyncSchema } from "@app/services/secret-sync/heroku";
import { SecretSync } from "@app/services/secret-sync/secret-sync-enums";
import { registerSyncSecretsEndpoints } from "./secret-sync-endpoints";
export const registerHerokuSyncRouter = async (server: FastifyZodProvider) =>
registerSyncSecretsEndpoints({
destination: SecretSync.Heroku,
server,
responseSchema: HerokuSyncSchema,
createSchema: CreateHerokuSyncSchema,
updateSchema: UpdateHerokuSyncSchema
});

View File

@ -8,11 +8,15 @@ import { registerAzureAppConfigurationSyncRouter } from "./azure-app-configurati
import { registerAzureDevOpsSyncRouter } from "./azure-devops-sync-router"; import { registerAzureDevOpsSyncRouter } from "./azure-devops-sync-router";
import { registerAzureKeyVaultSyncRouter } from "./azure-key-vault-sync-router"; import { registerAzureKeyVaultSyncRouter } from "./azure-key-vault-sync-router";
import { registerCamundaSyncRouter } from "./camunda-sync-router"; import { registerCamundaSyncRouter } from "./camunda-sync-router";
import { registerCloudflarePagesSyncRouter } from "./cloudflare-pages-sync-router";
import { registerDatabricksSyncRouter } from "./databricks-sync-router"; import { registerDatabricksSyncRouter } from "./databricks-sync-router";
import { registerFlyioSyncRouter } from "./flyio-sync-router";
import { registerGcpSyncRouter } from "./gcp-sync-router"; import { registerGcpSyncRouter } from "./gcp-sync-router";
import { registerGitHubSyncRouter } from "./github-sync-router"; import { registerGitHubSyncRouter } from "./github-sync-router";
import { registerHCVaultSyncRouter } from "./hc-vault-sync-router"; import { registerHCVaultSyncRouter } from "./hc-vault-sync-router";
import { registerHerokuSyncRouter } from "./heroku-sync-router";
import { registerHumanitecSyncRouter } from "./humanitec-sync-router"; import { registerHumanitecSyncRouter } from "./humanitec-sync-router";
import { registerRenderSyncRouter } from "./render-sync-router";
import { registerTeamCitySyncRouter } from "./teamcity-sync-router"; import { registerTeamCitySyncRouter } from "./teamcity-sync-router";
import { registerTerraformCloudSyncRouter } from "./terraform-cloud-sync-router"; import { registerTerraformCloudSyncRouter } from "./terraform-cloud-sync-router";
import { registerVercelSyncRouter } from "./vercel-sync-router"; import { registerVercelSyncRouter } from "./vercel-sync-router";
@ -37,5 +41,9 @@ export const SECRET_SYNC_REGISTER_ROUTER_MAP: Record<SecretSync, (server: Fastif
[SecretSync.HCVault]: registerHCVaultSyncRouter, [SecretSync.HCVault]: registerHCVaultSyncRouter,
[SecretSync.TeamCity]: registerTeamCitySyncRouter, [SecretSync.TeamCity]: registerTeamCitySyncRouter,
[SecretSync.OCIVault]: registerOCIVaultSyncRouter, [SecretSync.OCIVault]: registerOCIVaultSyncRouter,
[SecretSync.OnePass]: registerOnePassSyncRouter [SecretSync.OnePass]: registerOnePassSyncRouter,
[SecretSync.Heroku]: registerHerokuSyncRouter,
[SecretSync.Render]: registerRenderSyncRouter,
[SecretSync.Flyio]: registerFlyioSyncRouter,
[SecretSync.CloudflarePages]: registerCloudflarePagesSyncRouter
}; };

View File

@ -0,0 +1,17 @@
import {
CreateRenderSyncSchema,
RenderSyncSchema,
UpdateRenderSyncSchema
} from "@app/services/secret-sync/render/render-sync-schemas";
import { SecretSync } from "@app/services/secret-sync/secret-sync-enums";
import { registerSyncSecretsEndpoints } from "./secret-sync-endpoints";
export const registerRenderSyncRouter = async (server: FastifyZodProvider) =>
registerSyncSecretsEndpoints({
destination: SecretSync.Render,
server,
responseSchema: RenderSyncSchema,
createSchema: CreateRenderSyncSchema,
updateSchema: UpdateRenderSyncSchema
});

View File

@ -23,14 +23,21 @@ import { AzureDevOpsSyncListItemSchema, AzureDevOpsSyncSchema } from "@app/servi
import { AzureKeyVaultSyncListItemSchema, AzureKeyVaultSyncSchema } from "@app/services/secret-sync/azure-key-vault"; import { AzureKeyVaultSyncListItemSchema, AzureKeyVaultSyncSchema } from "@app/services/secret-sync/azure-key-vault";
import { CamundaSyncListItemSchema, CamundaSyncSchema } from "@app/services/secret-sync/camunda"; import { CamundaSyncListItemSchema, CamundaSyncSchema } from "@app/services/secret-sync/camunda";
import { DatabricksSyncListItemSchema, DatabricksSyncSchema } from "@app/services/secret-sync/databricks"; import { DatabricksSyncListItemSchema, DatabricksSyncSchema } from "@app/services/secret-sync/databricks";
import { FlyioSyncListItemSchema, FlyioSyncSchema } from "@app/services/secret-sync/flyio";
import { GcpSyncListItemSchema, GcpSyncSchema } from "@app/services/secret-sync/gcp"; import { GcpSyncListItemSchema, GcpSyncSchema } from "@app/services/secret-sync/gcp";
import { GitHubSyncListItemSchema, GitHubSyncSchema } from "@app/services/secret-sync/github"; import { GitHubSyncListItemSchema, GitHubSyncSchema } from "@app/services/secret-sync/github";
import { HCVaultSyncListItemSchema, HCVaultSyncSchema } from "@app/services/secret-sync/hc-vault"; import { HCVaultSyncListItemSchema, HCVaultSyncSchema } from "@app/services/secret-sync/hc-vault";
import { HerokuSyncListItemSchema, HerokuSyncSchema } from "@app/services/secret-sync/heroku";
import { HumanitecSyncListItemSchema, HumanitecSyncSchema } from "@app/services/secret-sync/humanitec"; import { HumanitecSyncListItemSchema, HumanitecSyncSchema } from "@app/services/secret-sync/humanitec";
import { RenderSyncListItemSchema, RenderSyncSchema } from "@app/services/secret-sync/render/render-sync-schemas";
import { TeamCitySyncListItemSchema, TeamCitySyncSchema } from "@app/services/secret-sync/teamcity"; import { TeamCitySyncListItemSchema, TeamCitySyncSchema } from "@app/services/secret-sync/teamcity";
import { TerraformCloudSyncListItemSchema, TerraformCloudSyncSchema } from "@app/services/secret-sync/terraform-cloud"; import { TerraformCloudSyncListItemSchema, TerraformCloudSyncSchema } from "@app/services/secret-sync/terraform-cloud";
import { VercelSyncListItemSchema, VercelSyncSchema } from "@app/services/secret-sync/vercel"; import { VercelSyncListItemSchema, VercelSyncSchema } from "@app/services/secret-sync/vercel";
import { WindmillSyncListItemSchema, WindmillSyncSchema } from "@app/services/secret-sync/windmill"; import { WindmillSyncListItemSchema, WindmillSyncSchema } from "@app/services/secret-sync/windmill";
import {
CloudflarePagesSyncListItemSchema,
CloudflarePagesSyncSchema
} from "@app/services/secret-sync/cloudflare-pages/cloudflare-pages-schema";
const SecretSyncSchema = z.discriminatedUnion("destination", [ const SecretSyncSchema = z.discriminatedUnion("destination", [
AwsParameterStoreSyncSchema, AwsParameterStoreSyncSchema,
@ -49,7 +56,11 @@ const SecretSyncSchema = z.discriminatedUnion("destination", [
HCVaultSyncSchema, HCVaultSyncSchema,
TeamCitySyncSchema, TeamCitySyncSchema,
OCIVaultSyncSchema, OCIVaultSyncSchema,
OnePassSyncSchema OnePassSyncSchema,
HerokuSyncSchema,
RenderSyncSchema,
FlyioSyncSchema,
CloudflarePagesSyncSchema
]); ]);
const SecretSyncOptionsSchema = z.discriminatedUnion("destination", [ const SecretSyncOptionsSchema = z.discriminatedUnion("destination", [
@ -69,7 +80,11 @@ const SecretSyncOptionsSchema = z.discriminatedUnion("destination", [
HCVaultSyncListItemSchema, HCVaultSyncListItemSchema,
TeamCitySyncListItemSchema, TeamCitySyncListItemSchema,
OCIVaultSyncListItemSchema, OCIVaultSyncListItemSchema,
OnePassSyncListItemSchema OnePassSyncListItemSchema,
HerokuSyncListItemSchema,
RenderSyncListItemSchema,
FlyioSyncListItemSchema,
CloudflarePagesSyncListItemSchema
]); ]);
export const registerSecretSyncRouter = async (server: FastifyZodProvider) => { export const registerSecretSyncRouter = async (server: FastifyZodProvider) => {

View File

@ -2,7 +2,7 @@ import { z } from "zod";
import { AuthTokenSessionsSchema, UserEncryptionKeysSchema, UsersSchema } from "@app/db/schemas"; import { AuthTokenSessionsSchema, UserEncryptionKeysSchema, UsersSchema } from "@app/db/schemas";
import { ApiKeysSchema } from "@app/db/schemas/api-keys"; import { ApiKeysSchema } from "@app/db/schemas/api-keys";
import { authRateLimit, readLimit, smtpRateLimit, writeLimit } from "@app/server/config/rateLimiter"; import { readLimit, smtpRateLimit, writeLimit } from "@app/server/config/rateLimiter";
import { verifyAuth } from "@app/server/plugins/auth/verify-auth"; import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
import { AuthMethod, AuthMode, MfaMethod } from "@app/services/auth/auth-type"; import { AuthMethod, AuthMode, MfaMethod } from "@app/services/auth/auth-type";
import { sanitizedOrganizationSchema } from "@app/services/org/org-schema"; import { sanitizedOrganizationSchema } from "@app/services/org/org-schema";
@ -13,7 +13,7 @@ export const registerUserRouter = async (server: FastifyZodProvider) => {
url: "/me/emails/code", url: "/me/emails/code",
config: { config: {
rateLimit: smtpRateLimit({ rateLimit: smtpRateLimit({
keyGenerator: (req) => (req.body as { username?: string })?.username?.trim().substring(0, 100) ?? req.realIp keyGenerator: (req) => (req.body as { username?: string })?.username?.trim().substring(0, 100) || req.realIp
}) })
}, },
schema: { schema: {
@ -34,7 +34,9 @@ export const registerUserRouter = async (server: FastifyZodProvider) => {
method: "POST", method: "POST",
url: "/me/emails/verify", url: "/me/emails/verify",
config: { config: {
rateLimit: authRateLimit rateLimit: smtpRateLimit({
keyGenerator: (req) => (req.body as { username?: string })?.username?.trim().substring(0, 100) || req.realIp
})
}, },
schema: { schema: {
body: z.object({ body: z.object({

View File

@ -4,7 +4,7 @@ import { z } from "zod";
import { SecretApprovalRequestsSchema, SecretsSchema, SecretType, ServiceTokenScopes } from "@app/db/schemas"; import { SecretApprovalRequestsSchema, SecretsSchema, SecretType, ServiceTokenScopes } from "@app/db/schemas";
import { EventType, UserAgentType } from "@app/ee/services/audit-log/audit-log-types"; import { EventType, UserAgentType } from "@app/ee/services/audit-log/audit-log-types";
import { ApiDocsTags, RAW_SECRETS, SECRETS } from "@app/lib/api-docs"; import { ApiDocsTags, RAW_SECRETS, SECRETS } from "@app/lib/api-docs";
import { BadRequestError, NotFoundError } from "@app/lib/errors"; import { BadRequestError } from "@app/lib/errors";
import { removeTrailingSlash } from "@app/lib/fn"; import { removeTrailingSlash } from "@app/lib/fn";
import { secretsLimit, writeLimit } from "@app/server/config/rateLimiter"; import { secretsLimit, writeLimit } from "@app/server/config/rateLimiter";
import { BaseSecretNameSchema, SecretNameSchema } from "@app/server/lib/schemas"; import { BaseSecretNameSchema, SecretNameSchema } from "@app/server/lib/schemas";
@ -12,7 +12,6 @@ import { getTelemetryDistinctId } from "@app/server/lib/telemetry";
import { getUserAgentType } from "@app/server/plugins/audit-log"; import { getUserAgentType } from "@app/server/plugins/audit-log";
import { verifyAuth } from "@app/server/plugins/auth/verify-auth"; import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
import { ActorType, AuthMode } from "@app/services/auth/auth-type"; import { ActorType, AuthMode } from "@app/services/auth/auth-type";
import { ProjectFilterType } from "@app/services/project/project-types";
import { ResourceMetadataSchema } from "@app/services/resource-metadata/resource-metadata-schema"; import { ResourceMetadataSchema } from "@app/services/resource-metadata/resource-metadata-schema";
import { SecretOperations, SecretProtectionType } from "@app/services/secret/secret-types"; import { SecretOperations, SecretProtectionType } from "@app/services/secret/secret-types";
import { SecretUpdateMode } from "@app/services/secret-v2-bridge/secret-v2-bridge-types"; import { SecretUpdateMode } from "@app/services/secret-v2-bridge/secret-v2-bridge-types";
@ -286,22 +285,17 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => {
environment = scope[0].environment; environment = scope[0].environment;
workspaceId = req.auth.serviceToken.projectId; workspaceId = req.auth.serviceToken.projectId;
} }
} else if (req.permission.type === ActorType.IDENTITY && req.query.workspaceSlug && !workspaceId) { } else {
const workspace = await server.services.project.getAProject({ const projectId = await server.services.project.extractProjectIdFromSlug({
filter: { projectSlug: req.query.workspaceSlug,
type: ProjectFilterType.SLUG, projectId: workspaceId,
orgId: req.permission.orgId,
slug: req.query.workspaceSlug
},
actorId: req.permission.id, actorId: req.permission.id,
actorAuthMethod: req.permission.authMethod, actorAuthMethod: req.permission.authMethod,
actor: req.permission.type, actor: req.permission.type,
actorOrgId: req.permission.orgId actorOrgId: req.permission.orgId
}); });
if (!workspace) throw new NotFoundError({ message: `No project found with slug ${req.query.workspaceSlug}` }); workspaceId = projectId;
workspaceId = workspace.id;
} }
if (!workspaceId || !environment) throw new BadRequestError({ message: "Missing workspace id or environment" }); if (!workspaceId || !environment) throw new BadRequestError({ message: "Missing workspace id or environment" });
@ -442,11 +436,23 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => {
environment = scope[0].environment; environment = scope[0].environment;
workspaceId = req.auth.serviceToken.projectId; workspaceId = req.auth.serviceToken.projectId;
} }
} else {
const projectId = await server.services.project.extractProjectIdFromSlug({
projectSlug: workspaceSlug,
projectId: workspaceId,
actorId: req.permission.id,
actorAuthMethod: req.permission.authMethod,
actor: req.permission.type,
actorOrgId: req.permission.orgId
});
workspaceId = projectId;
} }
if (!environment) throw new BadRequestError({ message: "Missing environment" }); if (!environment) throw new BadRequestError({ message: "Missing environment" });
if (!workspaceId && !workspaceSlug) if (!workspaceId) {
throw new BadRequestError({ message: "You must provide workspaceSlug or workspaceId" }); throw new BadRequestError({ message: "You must provide workspaceSlug or workspaceId" });
}
const secret = await server.services.secret.getSecretByNameRaw({ const secret = await server.services.secret.getSecretByNameRaw({
actorId: req.permission.id, actorId: req.permission.id,
@ -457,7 +463,6 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => {
environment, environment,
projectId: workspaceId, projectId: workspaceId,
viewSecretValue: req.query.viewSecretValue, viewSecretValue: req.query.viewSecretValue,
projectSlug: workspaceSlug,
path: secretPath, path: secretPath,
secretName: req.params.secretName, secretName: req.params.secretName,
type: req.query.type, type: req.query.type,
@ -518,7 +523,8 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => {
secretName: SecretNameSchema.describe(RAW_SECRETS.CREATE.secretName) secretName: SecretNameSchema.describe(RAW_SECRETS.CREATE.secretName)
}), }),
body: z.object({ body: z.object({
workspaceId: z.string().trim().describe(RAW_SECRETS.CREATE.workspaceId), workspaceId: z.string().trim().optional().describe(RAW_SECRETS.CREATE.workspaceId),
projectSlug: z.string().trim().optional().describe(RAW_SECRETS.CREATE.projectSlug),
environment: z.string().trim().describe(RAW_SECRETS.CREATE.environment), environment: z.string().trim().describe(RAW_SECRETS.CREATE.environment),
secretPath: z secretPath: z
.string() .string()
@ -558,13 +564,22 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => {
}, },
onRequest: verifyAuth([AuthMode.JWT, AuthMode.API_KEY, AuthMode.SERVICE_TOKEN, AuthMode.IDENTITY_ACCESS_TOKEN]), onRequest: verifyAuth([AuthMode.JWT, AuthMode.API_KEY, AuthMode.SERVICE_TOKEN, AuthMode.IDENTITY_ACCESS_TOKEN]),
handler: async (req) => { handler: async (req) => {
const projectId = await server.services.project.extractProjectIdFromSlug({
projectSlug: req.body.projectSlug,
projectId: req.body.workspaceId,
actorId: req.permission.id,
actorAuthMethod: req.permission.authMethod,
actor: req.permission.type,
actorOrgId: req.permission.orgId
});
const secretOperation = await server.services.secret.createSecretRaw({ const secretOperation = await server.services.secret.createSecretRaw({
actorId: req.permission.id, actorId: req.permission.id,
actor: req.permission.type, actor: req.permission.type,
actorOrgId: req.permission.orgId, actorOrgId: req.permission.orgId,
environment: req.body.environment, environment: req.body.environment,
actorAuthMethod: req.permission.authMethod, actorAuthMethod: req.permission.authMethod,
projectId: req.body.workspaceId, projectId,
secretPath: req.body.secretPath, secretPath: req.body.secretPath,
secretName: req.params.secretName, secretName: req.params.secretName,
type: req.body.type, type: req.body.type,
@ -582,7 +597,7 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => {
const { secret } = secretOperation; const { secret } = secretOperation;
await server.services.auditLog.createAuditLog({ await server.services.auditLog.createAuditLog({
projectId: req.body.workspaceId, projectId,
...req.auditLogInfo, ...req.auditLogInfo,
event: { event: {
type: EventType.CREATE_SECRET, type: EventType.CREATE_SECRET,
@ -602,7 +617,7 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => {
distinctId: getTelemetryDistinctId(req), distinctId: getTelemetryDistinctId(req),
properties: { properties: {
numberOfSecrets: 1, numberOfSecrets: 1,
workspaceId: req.body.workspaceId, workspaceId: projectId,
environment: req.body.environment, environment: req.body.environment,
secretPath: req.body.secretPath, secretPath: req.body.secretPath,
channel: getUserAgentType(req.headers["user-agent"]), channel: getUserAgentType(req.headers["user-agent"]),
@ -633,7 +648,8 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => {
secretName: BaseSecretNameSchema.describe(RAW_SECRETS.UPDATE.secretName) secretName: BaseSecretNameSchema.describe(RAW_SECRETS.UPDATE.secretName)
}), }),
body: z.object({ body: z.object({
workspaceId: z.string().trim().describe(RAW_SECRETS.UPDATE.workspaceId), workspaceId: z.string().trim().optional().describe(RAW_SECRETS.UPDATE.workspaceId),
projectSlug: z.string().trim().optional().describe(RAW_SECRETS.UPDATE.projectSlug),
environment: z.string().trim().describe(RAW_SECRETS.UPDATE.environment), environment: z.string().trim().describe(RAW_SECRETS.UPDATE.environment),
secretValue: z secretValue: z
.string() .string()
@ -679,13 +695,22 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => {
}, },
onRequest: verifyAuth([AuthMode.JWT, AuthMode.API_KEY, AuthMode.SERVICE_TOKEN, AuthMode.IDENTITY_ACCESS_TOKEN]), onRequest: verifyAuth([AuthMode.JWT, AuthMode.API_KEY, AuthMode.SERVICE_TOKEN, AuthMode.IDENTITY_ACCESS_TOKEN]),
handler: async (req) => { handler: async (req) => {
const projectId = await server.services.project.extractProjectIdFromSlug({
projectSlug: req.body.projectSlug,
projectId: req.body.workspaceId,
actorId: req.permission.id,
actorAuthMethod: req.permission.authMethod,
actor: req.permission.type,
actorOrgId: req.permission.orgId
});
const secretOperation = await server.services.secret.updateSecretRaw({ const secretOperation = await server.services.secret.updateSecretRaw({
actorId: req.permission.id, actorId: req.permission.id,
actor: req.permission.type, actor: req.permission.type,
actorOrgId: req.permission.orgId, actorOrgId: req.permission.orgId,
actorAuthMethod: req.permission.authMethod, actorAuthMethod: req.permission.authMethod,
environment: req.body.environment, environment: req.body.environment,
projectId: req.body.workspaceId, projectId,
secretPath: req.body.secretPath, secretPath: req.body.secretPath,
secretName: req.params.secretName, secretName: req.params.secretName,
type: req.body.type, type: req.body.type,
@ -707,7 +732,7 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => {
const { secret } = secretOperation; const { secret } = secretOperation;
await server.services.auditLog.createAuditLog({ await server.services.auditLog.createAuditLog({
projectId: req.body.workspaceId, projectId,
...req.auditLogInfo, ...req.auditLogInfo,
event: { event: {
type: EventType.UPDATE_SECRET, type: EventType.UPDATE_SECRET,
@ -727,7 +752,7 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => {
distinctId: getTelemetryDistinctId(req), distinctId: getTelemetryDistinctId(req),
properties: { properties: {
numberOfSecrets: 1, numberOfSecrets: 1,
workspaceId: req.body.workspaceId, workspaceId: projectId,
environment: req.body.environment, environment: req.body.environment,
secretPath: req.body.secretPath, secretPath: req.body.secretPath,
channel: getUserAgentType(req.headers["user-agent"]), channel: getUserAgentType(req.headers["user-agent"]),
@ -757,7 +782,8 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => {
secretName: z.string().min(1).describe(RAW_SECRETS.DELETE.secretName) secretName: z.string().min(1).describe(RAW_SECRETS.DELETE.secretName)
}), }),
body: z.object({ body: z.object({
workspaceId: z.string().trim().describe(RAW_SECRETS.DELETE.workspaceId), workspaceId: z.string().trim().optional().describe(RAW_SECRETS.DELETE.workspaceId),
projectSlug: z.string().trim().optional().describe(RAW_SECRETS.DELETE.projectSlug),
environment: z.string().trim().describe(RAW_SECRETS.DELETE.environment), environment: z.string().trim().describe(RAW_SECRETS.DELETE.environment),
secretPath: z secretPath: z
.string() .string()
@ -780,13 +806,22 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => {
}, },
onRequest: verifyAuth([AuthMode.JWT, AuthMode.API_KEY, AuthMode.SERVICE_TOKEN, AuthMode.IDENTITY_ACCESS_TOKEN]), onRequest: verifyAuth([AuthMode.JWT, AuthMode.API_KEY, AuthMode.SERVICE_TOKEN, AuthMode.IDENTITY_ACCESS_TOKEN]),
handler: async (req) => { handler: async (req) => {
const projectId = await server.services.project.extractProjectIdFromSlug({
projectSlug: req.body.projectSlug,
projectId: req.body.workspaceId,
actorId: req.permission.id,
actorAuthMethod: req.permission.authMethod,
actor: req.permission.type,
actorOrgId: req.permission.orgId
});
const secretOperation = await server.services.secret.deleteSecretRaw({ const secretOperation = await server.services.secret.deleteSecretRaw({
actorId: req.permission.id, actorId: req.permission.id,
actor: req.permission.type, actor: req.permission.type,
actorAuthMethod: req.permission.authMethod, actorAuthMethod: req.permission.authMethod,
actorOrgId: req.permission.orgId, actorOrgId: req.permission.orgId,
environment: req.body.environment, environment: req.body.environment,
projectId: req.body.workspaceId, projectId,
secretPath: req.body.secretPath, secretPath: req.body.secretPath,
secretName: req.params.secretName, secretName: req.params.secretName,
type: req.body.type type: req.body.type
@ -798,7 +833,7 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => {
const { secret } = secretOperation; const { secret } = secretOperation;
await server.services.auditLog.createAuditLog({ await server.services.auditLog.createAuditLog({
projectId: req.body.workspaceId, projectId,
...req.auditLogInfo, ...req.auditLogInfo,
event: { event: {
type: EventType.DELETE_SECRET, type: EventType.DELETE_SECRET,
@ -817,7 +852,7 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => {
distinctId: getTelemetryDistinctId(req), distinctId: getTelemetryDistinctId(req),
properties: { properties: {
numberOfSecrets: 1, numberOfSecrets: 1,
workspaceId: req.body.workspaceId, workspaceId: projectId,
environment: req.body.environment, environment: req.body.environment,
secretPath: req.body.secretPath, secretPath: req.body.secretPath,
channel: getUserAgentType(req.headers["user-agent"]), channel: getUserAgentType(req.headers["user-agent"]),

View File

@ -14,7 +14,7 @@ export const registerSignupRouter = async (server: FastifyZodProvider) => {
method: "POST", method: "POST",
config: { config: {
rateLimit: smtpRateLimit({ rateLimit: smtpRateLimit({
keyGenerator: (req) => (req.body as { email?: string })?.email?.trim().substring(0, 100) ?? req.realIp keyGenerator: (req) => (req.body as { email?: string })?.email?.trim().substring(0, 100) || req.realIp
}) })
}, },
schema: { schema: {
@ -55,7 +55,9 @@ export const registerSignupRouter = async (server: FastifyZodProvider) => {
url: "/email/verify", url: "/email/verify",
method: "POST", method: "POST",
config: { config: {
rateLimit: authRateLimit rateLimit: smtpRateLimit({
keyGenerator: (req) => (req.body as { email?: string })?.email?.trim().substring(0, 100) || req.realIp
})
}, },
schema: { schema: {
body: z.object({ body: z.object({

View File

@ -22,7 +22,11 @@ export enum AppConnection {
TeamCity = "teamcity", TeamCity = "teamcity",
OCI = "oci", OCI = "oci",
OracleDB = "oracledb", OracleDB = "oracledb",
OnePass = "1password" OnePass = "1password",
Heroku = "heroku",
Render = "render",
Flyio = "flyio",
Cloudflare = "cloudflare"
} }
export enum AWSRegion { export enum AWSRegion {

View File

@ -56,6 +56,7 @@ import {
getDatabricksConnectionListItem, getDatabricksConnectionListItem,
validateDatabricksConnectionCredentials validateDatabricksConnectionCredentials
} from "./databricks"; } from "./databricks";
import { FlyioConnectionMethod, getFlyioConnectionListItem, validateFlyioConnectionCredentials } from "./flyio";
import { GcpConnectionMethod, getGcpConnectionListItem, validateGcpConnectionCredentials } from "./gcp"; import { GcpConnectionMethod, getGcpConnectionListItem, validateGcpConnectionCredentials } from "./gcp";
import { getGitHubConnectionListItem, GitHubConnectionMethod, validateGitHubConnectionCredentials } from "./github"; import { getGitHubConnectionListItem, GitHubConnectionMethod, validateGitHubConnectionCredentials } from "./github";
import { import {
@ -68,6 +69,7 @@ import {
HCVaultConnectionMethod, HCVaultConnectionMethod,
validateHCVaultConnectionCredentials validateHCVaultConnectionCredentials
} from "./hc-vault"; } from "./hc-vault";
import { getHerokuConnectionListItem, HerokuConnectionMethod, validateHerokuConnectionCredentials } from "./heroku";
import { import {
getHumanitecConnectionListItem, getHumanitecConnectionListItem,
HumanitecConnectionMethod, HumanitecConnectionMethod,
@ -78,6 +80,8 @@ import { getMsSqlConnectionListItem, MsSqlConnectionMethod } from "./mssql";
import { MySqlConnectionMethod } from "./mysql/mysql-connection-enums"; import { MySqlConnectionMethod } from "./mysql/mysql-connection-enums";
import { getMySqlConnectionListItem } from "./mysql/mysql-connection-fns"; import { getMySqlConnectionListItem } from "./mysql/mysql-connection-fns";
import { getPostgresConnectionListItem, PostgresConnectionMethod } from "./postgres"; import { getPostgresConnectionListItem, PostgresConnectionMethod } from "./postgres";
import { RenderConnectionMethod } from "./render/render-connection-enums";
import { getRenderConnectionListItem, validateRenderConnectionCredentials } from "./render/render-connection-fns";
import { import {
getTeamCityConnectionListItem, getTeamCityConnectionListItem,
TeamCityConnectionMethod, TeamCityConnectionMethod,
@ -95,6 +99,11 @@ import {
validateWindmillConnectionCredentials, validateWindmillConnectionCredentials,
WindmillConnectionMethod WindmillConnectionMethod
} from "./windmill"; } from "./windmill";
import {
getCloudflareConnectionListItem,
validateCloudflareConnectionCredentials
} from "./cloudflare/cloudflare-connection-fns";
import { CloudflareConnectionMethod } from "./cloudflare/cloudflare-connection-enum";
export const listAppConnectionOptions = () => { export const listAppConnectionOptions = () => {
return [ return [
@ -121,7 +130,11 @@ export const listAppConnectionOptions = () => {
getTeamCityConnectionListItem(), getTeamCityConnectionListItem(),
getOCIConnectionListItem(), getOCIConnectionListItem(),
getOracleDBConnectionListItem(), getOracleDBConnectionListItem(),
getOnePassConnectionListItem() getOnePassConnectionListItem(),
getHerokuConnectionListItem(),
getRenderConnectionListItem(),
getFlyioConnectionListItem(),
getCloudflareConnectionListItem()
].sort((a, b) => a.name.localeCompare(b.name)); ].sort((a, b) => a.name.localeCompare(b.name));
}; };
@ -196,7 +209,11 @@ export const validateAppConnectionCredentials = async (
[AppConnection.TeamCity]: validateTeamCityConnectionCredentials as TAppConnectionCredentialsValidator, [AppConnection.TeamCity]: validateTeamCityConnectionCredentials as TAppConnectionCredentialsValidator,
[AppConnection.OCI]: validateOCIConnectionCredentials as TAppConnectionCredentialsValidator, [AppConnection.OCI]: validateOCIConnectionCredentials as TAppConnectionCredentialsValidator,
[AppConnection.OracleDB]: validateSqlConnectionCredentials as TAppConnectionCredentialsValidator, [AppConnection.OracleDB]: validateSqlConnectionCredentials as TAppConnectionCredentialsValidator,
[AppConnection.OnePass]: validateOnePassConnectionCredentials as TAppConnectionCredentialsValidator [AppConnection.OnePass]: validateOnePassConnectionCredentials as TAppConnectionCredentialsValidator,
[AppConnection.Heroku]: validateHerokuConnectionCredentials as TAppConnectionCredentialsValidator,
[AppConnection.Render]: validateRenderConnectionCredentials as TAppConnectionCredentialsValidator,
[AppConnection.Flyio]: validateFlyioConnectionCredentials as TAppConnectionCredentialsValidator,
[AppConnection.Cloudflare]: validateCloudflareConnectionCredentials as TAppConnectionCredentialsValidator
}; };
return VALIDATE_APP_CONNECTION_CREDENTIALS_MAP[appConnection.app](appConnection); return VALIDATE_APP_CONNECTION_CREDENTIALS_MAP[appConnection.app](appConnection);
@ -212,7 +229,10 @@ export const getAppConnectionMethodName = (method: TAppConnection["method"]) =>
case AzureClientSecretsConnectionMethod.OAuth: case AzureClientSecretsConnectionMethod.OAuth:
case GitHubConnectionMethod.OAuth: case GitHubConnectionMethod.OAuth:
case AzureDevOpsConnectionMethod.OAuth: case AzureDevOpsConnectionMethod.OAuth:
case HerokuConnectionMethod.OAuth:
return "OAuth"; return "OAuth";
case HerokuConnectionMethod.AuthToken:
return "Auth Token";
case AwsConnectionMethod.AccessKey: case AwsConnectionMethod.AccessKey:
case OCIConnectionMethod.AccessKey: case OCIConnectionMethod.AccessKey:
return "Access Key"; return "Access Key";
@ -228,6 +248,7 @@ export const getAppConnectionMethodName = (method: TAppConnection["method"]) =>
case TerraformCloudConnectionMethod.ApiToken: case TerraformCloudConnectionMethod.ApiToken:
case VercelConnectionMethod.ApiToken: case VercelConnectionMethod.ApiToken:
case OnePassConnectionMethod.ApiToken: case OnePassConnectionMethod.ApiToken:
case CloudflareConnectionMethod.APIToken:
return "API Token"; return "API Token";
case PostgresConnectionMethod.UsernameAndPassword: case PostgresConnectionMethod.UsernameAndPassword:
case MsSqlConnectionMethod.UsernameAndPassword: case MsSqlConnectionMethod.UsernameAndPassword:
@ -238,6 +259,7 @@ export const getAppConnectionMethodName = (method: TAppConnection["method"]) =>
case HCVaultConnectionMethod.AccessToken: case HCVaultConnectionMethod.AccessToken:
case TeamCityConnectionMethod.AccessToken: case TeamCityConnectionMethod.AccessToken:
case AzureDevOpsConnectionMethod.AccessToken: case AzureDevOpsConnectionMethod.AccessToken:
case FlyioConnectionMethod.AccessToken:
return "Access Token"; return "Access Token";
case Auth0ConnectionMethod.ClientCredentials: case Auth0ConnectionMethod.ClientCredentials:
return "Client Credentials"; return "Client Credentials";
@ -245,6 +267,8 @@ export const getAppConnectionMethodName = (method: TAppConnection["method"]) =>
return "App Role"; return "App Role";
case LdapConnectionMethod.SimpleBind: case LdapConnectionMethod.SimpleBind:
return "Simple Bind"; return "Simple Bind";
case RenderConnectionMethod.ApiKey:
return "API Key";
default: default:
// eslint-disable-next-line @typescript-eslint/restrict-template-expressions // eslint-disable-next-line @typescript-eslint/restrict-template-expressions
throw new Error(`Unhandled App Connection Method: ${method}`); throw new Error(`Unhandled App Connection Method: ${method}`);
@ -299,7 +323,11 @@ export const TRANSITION_CONNECTION_CREDENTIALS_TO_PLATFORM: Record<
[AppConnection.TeamCity]: platformManagedCredentialsNotSupported, [AppConnection.TeamCity]: platformManagedCredentialsNotSupported,
[AppConnection.OCI]: platformManagedCredentialsNotSupported, [AppConnection.OCI]: platformManagedCredentialsNotSupported,
[AppConnection.OracleDB]: transferSqlConnectionCredentialsToPlatform as TAppConnectionTransitionCredentialsToPlatform, [AppConnection.OracleDB]: transferSqlConnectionCredentialsToPlatform as TAppConnectionTransitionCredentialsToPlatform,
[AppConnection.OnePass]: platformManagedCredentialsNotSupported [AppConnection.OnePass]: platformManagedCredentialsNotSupported,
[AppConnection.Heroku]: platformManagedCredentialsNotSupported,
[AppConnection.Render]: platformManagedCredentialsNotSupported,
[AppConnection.Flyio]: platformManagedCredentialsNotSupported,
[AppConnection.Cloudflare]: platformManagedCredentialsNotSupported
}; };
export const enterpriseAppCheck = async ( export const enterpriseAppCheck = async (

View File

@ -24,7 +24,11 @@ export const APP_CONNECTION_NAME_MAP: Record<AppConnection, string> = {
[AppConnection.TeamCity]: "TeamCity", [AppConnection.TeamCity]: "TeamCity",
[AppConnection.OCI]: "OCI", [AppConnection.OCI]: "OCI",
[AppConnection.OracleDB]: "OracleDB", [AppConnection.OracleDB]: "OracleDB",
[AppConnection.OnePass]: "1Password" [AppConnection.OnePass]: "1Password",
[AppConnection.Heroku]: "Heroku",
[AppConnection.Render]: "Render",
[AppConnection.Flyio]: "Fly.io",
[AppConnection.Cloudflare]: "Cloudflare"
}; };
export const APP_CONNECTION_PLAN_MAP: Record<AppConnection, AppConnectionPlanType> = { export const APP_CONNECTION_PLAN_MAP: Record<AppConnection, AppConnectionPlanType> = {
@ -51,5 +55,9 @@ export const APP_CONNECTION_PLAN_MAP: Record<AppConnection, AppConnectionPlanTyp
[AppConnection.OCI]: AppConnectionPlanType.Enterprise, [AppConnection.OCI]: AppConnectionPlanType.Enterprise,
[AppConnection.OracleDB]: AppConnectionPlanType.Enterprise, [AppConnection.OracleDB]: AppConnectionPlanType.Enterprise,
[AppConnection.OnePass]: AppConnectionPlanType.Regular, [AppConnection.OnePass]: AppConnectionPlanType.Regular,
[AppConnection.MySql]: AppConnectionPlanType.Regular [AppConnection.MySql]: AppConnectionPlanType.Regular,
[AppConnection.Heroku]: AppConnectionPlanType.Regular,
[AppConnection.Render]: AppConnectionPlanType.Regular,
[AppConnection.Flyio]: AppConnectionPlanType.Regular,
[AppConnection.Cloudflare]: AppConnectionPlanType.Regular
}; };

View File

@ -47,8 +47,12 @@ import { azureDevOpsConnectionService } from "./azure-devops/azure-devops-servic
import { ValidateAzureKeyVaultConnectionCredentialsSchema } from "./azure-key-vault"; import { ValidateAzureKeyVaultConnectionCredentialsSchema } from "./azure-key-vault";
import { ValidateCamundaConnectionCredentialsSchema } from "./camunda"; import { ValidateCamundaConnectionCredentialsSchema } from "./camunda";
import { camundaConnectionService } from "./camunda/camunda-connection-service"; import { camundaConnectionService } from "./camunda/camunda-connection-service";
import { ValidateCloudflareConnectionCredentialsSchema } from "./cloudflare/cloudflare-connection-schema";
import { cloudflareConnectionService } from "./cloudflare/cloudflare-connection-service";
import { ValidateDatabricksConnectionCredentialsSchema } from "./databricks"; import { ValidateDatabricksConnectionCredentialsSchema } from "./databricks";
import { databricksConnectionService } from "./databricks/databricks-connection-service"; import { databricksConnectionService } from "./databricks/databricks-connection-service";
import { ValidateFlyioConnectionCredentialsSchema } from "./flyio";
import { flyioConnectionService } from "./flyio/flyio-connection-service";
import { ValidateGcpConnectionCredentialsSchema } from "./gcp"; import { ValidateGcpConnectionCredentialsSchema } from "./gcp";
import { gcpConnectionService } from "./gcp/gcp-connection-service"; import { gcpConnectionService } from "./gcp/gcp-connection-service";
import { ValidateGitHubConnectionCredentialsSchema } from "./github"; import { ValidateGitHubConnectionCredentialsSchema } from "./github";
@ -56,12 +60,16 @@ import { githubConnectionService } from "./github/github-connection-service";
import { ValidateGitHubRadarConnectionCredentialsSchema } from "./github-radar"; import { ValidateGitHubRadarConnectionCredentialsSchema } from "./github-radar";
import { ValidateHCVaultConnectionCredentialsSchema } from "./hc-vault"; import { ValidateHCVaultConnectionCredentialsSchema } from "./hc-vault";
import { hcVaultConnectionService } from "./hc-vault/hc-vault-connection-service"; import { hcVaultConnectionService } from "./hc-vault/hc-vault-connection-service";
import { ValidateHerokuConnectionCredentialsSchema } from "./heroku";
import { herokuConnectionService } from "./heroku/heroku-connection-service";
import { ValidateHumanitecConnectionCredentialsSchema } from "./humanitec"; import { ValidateHumanitecConnectionCredentialsSchema } from "./humanitec";
import { humanitecConnectionService } from "./humanitec/humanitec-connection-service"; import { humanitecConnectionService } from "./humanitec/humanitec-connection-service";
import { ValidateLdapConnectionCredentialsSchema } from "./ldap"; import { ValidateLdapConnectionCredentialsSchema } from "./ldap";
import { ValidateMsSqlConnectionCredentialsSchema } from "./mssql"; import { ValidateMsSqlConnectionCredentialsSchema } from "./mssql";
import { ValidateMySqlConnectionCredentialsSchema } from "./mysql"; import { ValidateMySqlConnectionCredentialsSchema } from "./mysql";
import { ValidatePostgresConnectionCredentialsSchema } from "./postgres"; import { ValidatePostgresConnectionCredentialsSchema } from "./postgres";
import { ValidateRenderConnectionCredentialsSchema } from "./render/render-connection-schema";
import { renderConnectionService } from "./render/render-connection-service";
import { ValidateTeamCityConnectionCredentialsSchema } from "./teamcity"; import { ValidateTeamCityConnectionCredentialsSchema } from "./teamcity";
import { teamcityConnectionService } from "./teamcity/teamcity-connection-service"; import { teamcityConnectionService } from "./teamcity/teamcity-connection-service";
import { ValidateTerraformCloudConnectionCredentialsSchema } from "./terraform-cloud"; import { ValidateTerraformCloudConnectionCredentialsSchema } from "./terraform-cloud";
@ -104,7 +112,11 @@ const VALIDATE_APP_CONNECTION_CREDENTIALS_MAP: Record<AppConnection, TValidateAp
[AppConnection.TeamCity]: ValidateTeamCityConnectionCredentialsSchema, [AppConnection.TeamCity]: ValidateTeamCityConnectionCredentialsSchema,
[AppConnection.OCI]: ValidateOCIConnectionCredentialsSchema, [AppConnection.OCI]: ValidateOCIConnectionCredentialsSchema,
[AppConnection.OracleDB]: ValidateOracleDBConnectionCredentialsSchema, [AppConnection.OracleDB]: ValidateOracleDBConnectionCredentialsSchema,
[AppConnection.OnePass]: ValidateOnePassConnectionCredentialsSchema [AppConnection.OnePass]: ValidateOnePassConnectionCredentialsSchema,
[AppConnection.Heroku]: ValidateHerokuConnectionCredentialsSchema,
[AppConnection.Render]: ValidateRenderConnectionCredentialsSchema,
[AppConnection.Flyio]: ValidateFlyioConnectionCredentialsSchema,
[AppConnection.Cloudflare]: ValidateCloudflareConnectionCredentialsSchema
}; };
export const appConnectionServiceFactory = ({ export const appConnectionServiceFactory = ({
@ -509,6 +521,10 @@ export const appConnectionServiceFactory = ({
windmill: windmillConnectionService(connectAppConnectionById), windmill: windmillConnectionService(connectAppConnectionById),
teamcity: teamcityConnectionService(connectAppConnectionById), teamcity: teamcityConnectionService(connectAppConnectionById),
oci: ociConnectionService(connectAppConnectionById, licenseService), oci: ociConnectionService(connectAppConnectionById, licenseService),
onepass: onePassConnectionService(connectAppConnectionById) onepass: onePassConnectionService(connectAppConnectionById),
heroku: herokuConnectionService(connectAppConnectionById, appConnectionDAL, kmsService),
render: renderConnectionService(connectAppConnectionById),
cloudflare: cloudflareConnectionService(connectAppConnectionById),
flyio: flyioConnectionService(connectAppConnectionById)
}; };
}; };

View File

@ -68,6 +68,12 @@ import {
TDatabricksConnectionInput, TDatabricksConnectionInput,
TValidateDatabricksConnectionCredentialsSchema TValidateDatabricksConnectionCredentialsSchema
} from "./databricks"; } from "./databricks";
import {
TFlyioConnection,
TFlyioConnectionConfig,
TFlyioConnectionInput,
TValidateFlyioConnectionCredentialsSchema
} from "./flyio";
import { import {
TGcpConnection, TGcpConnection,
TGcpConnectionConfig, TGcpConnectionConfig,
@ -92,6 +98,12 @@ import {
THCVaultConnectionInput, THCVaultConnectionInput,
TValidateHCVaultConnectionCredentialsSchema TValidateHCVaultConnectionCredentialsSchema
} from "./hc-vault"; } from "./hc-vault";
import {
THerokuConnection,
THerokuConnectionConfig,
THerokuConnectionInput,
TValidateHerokuConnectionCredentialsSchema
} from "./heroku";
import { import {
THumanitecConnection, THumanitecConnection,
THumanitecConnectionConfig, THumanitecConnectionConfig,
@ -111,6 +123,12 @@ import {
TPostgresConnectionInput, TPostgresConnectionInput,
TValidatePostgresConnectionCredentialsSchema TValidatePostgresConnectionCredentialsSchema
} from "./postgres"; } from "./postgres";
import {
TRenderConnection,
TRenderConnectionConfig,
TRenderConnectionInput,
TValidateRenderConnectionCredentialsSchema
} from "./render/render-connection-types";
import { import {
TTeamCityConnection, TTeamCityConnection,
TTeamCityConnectionConfig, TTeamCityConnectionConfig,
@ -135,6 +153,12 @@ import {
TWindmillConnectionConfig, TWindmillConnectionConfig,
TWindmillConnectionInput TWindmillConnectionInput
} from "./windmill"; } from "./windmill";
import {
TCloudflareConnection,
TCloudflareConnectionConfig,
TCloudflareConnectionInput,
TValidateCloudflareConnectionCredentialsSchema
} from "./cloudflare/cloudflare-connection-types";
export type TAppConnection = { id: string } & ( export type TAppConnection = { id: string } & (
| TAwsConnection | TAwsConnection
@ -161,6 +185,10 @@ export type TAppConnection = { id: string } & (
| TOCIConnection | TOCIConnection
| TOracleDBConnection | TOracleDBConnection
| TOnePassConnection | TOnePassConnection
| THerokuConnection
| TRenderConnection
| TFlyioConnection
| TCloudflareConnection
); );
export type TAppConnectionRaw = NonNullable<Awaited<ReturnType<TAppConnectionDALFactory["findById"]>>>; export type TAppConnectionRaw = NonNullable<Awaited<ReturnType<TAppConnectionDALFactory["findById"]>>>;
@ -192,6 +220,10 @@ export type TAppConnectionInput = { id: string } & (
| TOCIConnectionInput | TOCIConnectionInput
| TOracleDBConnectionInput | TOracleDBConnectionInput
| TOnePassConnectionInput | TOnePassConnectionInput
| THerokuConnectionInput
| TRenderConnectionInput
| TFlyioConnectionInput
| TCloudflareConnectionInput
); );
export type TSqlConnectionInput = export type TSqlConnectionInput =
@ -230,7 +262,11 @@ export type TAppConnectionConfig =
| TLdapConnectionConfig | TLdapConnectionConfig
| TTeamCityConnectionConfig | TTeamCityConnectionConfig
| TOCIConnectionConfig | TOCIConnectionConfig
| TOnePassConnectionConfig; | TOnePassConnectionConfig
| THerokuConnectionConfig
| TRenderConnectionConfig
| TFlyioConnectionConfig
| TCloudflareConnectionConfig;
export type TValidateAppConnectionCredentialsSchema = export type TValidateAppConnectionCredentialsSchema =
| TValidateAwsConnectionCredentialsSchema | TValidateAwsConnectionCredentialsSchema
@ -256,7 +292,11 @@ export type TValidateAppConnectionCredentialsSchema =
| TValidateTeamCityConnectionCredentialsSchema | TValidateTeamCityConnectionCredentialsSchema
| TValidateOCIConnectionCredentialsSchema | TValidateOCIConnectionCredentialsSchema
| TValidateOracleDBConnectionCredentialsSchema | TValidateOracleDBConnectionCredentialsSchema
| TValidateOnePassConnectionCredentialsSchema; | TValidateOnePassConnectionCredentialsSchema
| TValidateHerokuConnectionCredentialsSchema
| TValidateRenderConnectionCredentialsSchema
| TValidateFlyioConnectionCredentialsSchema
| TValidateCloudflareConnectionCredentialsSchema;
export type TListAwsConnectionKmsKeys = { export type TListAwsConnectionKmsKeys = {
connectionId: string; connectionId: string;

View File

@ -0,0 +1,3 @@
export enum CloudflareConnectionMethod {
APIToken = "api-token"
}

View File

@ -0,0 +1,75 @@
import { AxiosError } from "axios";
import { request } from "@app/lib/config/request";
import { BadRequestError } from "@app/lib/errors";
import { AppConnection } from "@app/services/app-connection/app-connection-enums";
import { IntegrationUrls } from "@app/services/integration-auth/integration-list";
import { CloudflareConnectionMethod } from "./cloudflare-connection-enum";
import {
TCloudflareConnection,
TCloudflareConnectionConfig,
TCloudflarePagesProject
} from "./cloudflare-connection-types";
export const getCloudflareConnectionListItem = () => {
return {
name: "Cloudflare" as const,
app: AppConnection.Cloudflare as const,
methods: Object.values(CloudflareConnectionMethod) as [CloudflareConnectionMethod.APIToken]
};
};
export const listCloudflarePagesProjects = async (
appConnection: TCloudflareConnection
): Promise<TCloudflarePagesProject[]> => {
const {
credentials: { apiToken, accountId }
} = appConnection;
const { data } = await request.get<{ result: { name: string; id: string }[] }>(
`${IntegrationUrls.CLOUDFLARE_API_URL}/client/v4/accounts/${accountId}/pages/projects`,
{
headers: {
Authorization: `Bearer ${apiToken}`,
Accept: "application/json"
}
}
);
return data.result.map((a) => ({
name: a.name,
id: a.id
}));
};
export const validateCloudflareConnectionCredentials = async (config: TCloudflareConnectionConfig) => {
const { apiToken, accountId } = config.credentials;
try {
const resp = await request.get(`${IntegrationUrls.CLOUDFLARE_API_URL}/client/v4/accounts/${accountId}`, {
headers: {
Authorization: `Bearer ${apiToken}`,
Accept: "application/json"
}
});
if (resp.data === null) {
throw new BadRequestError({
message: "Unable to validate connection: Invalid API token provided."
});
}
} catch (error: unknown) {
if (error instanceof AxiosError) {
throw new BadRequestError({
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
message: `Failed to validate credentials: ${error.response?.data?.errors?.[0]?.message || error.message || "Unknown error"}`
});
}
throw new BadRequestError({
message: "Unable to validate connection: verify credentials"
});
}
return config.credentials;
};

View File

@ -0,0 +1,74 @@
import z from "zod";
import { AppConnections } from "@app/lib/api-docs";
import { AppConnection } from "@app/services/app-connection/app-connection-enums";
import {
BaseAppConnectionSchema,
GenericCreateAppConnectionFieldsSchema,
GenericUpdateAppConnectionFieldsSchema
} from "@app/services/app-connection/app-connection-schemas";
import { CloudflareConnectionMethod } from "./cloudflare-connection-enum";
import { CharacterType, characterValidator } from "@app/lib/validator/validate-string";
const accountIdCharacterValidator = characterValidator([
CharacterType.AlphaNumeric,
CharacterType.Underscore,
CharacterType.Hyphen
]);
export const CloudflareConnectionApiTokenCredentialsSchema = z.object({
accountId: z
.string()
.trim()
.min(1, "Account ID required")
.max(256, "Account ID cannot exceed 256 characters")
.refine(
(val) => accountIdCharacterValidator(val),
"Account ID can only contain alphanumeric characters, underscores, and hyphens"
),
apiToken: z.string().trim().min(1, "API token required").max(256, "API token cannot exceed 256 characters")
});
const BaseCloudflareConnectionSchema = BaseAppConnectionSchema.extend({ app: z.literal(AppConnection.Cloudflare) });
export const CloudflareConnectionSchema = BaseCloudflareConnectionSchema.extend({
method: z.literal(CloudflareConnectionMethod.APIToken),
credentials: CloudflareConnectionApiTokenCredentialsSchema
});
export const SanitizedCloudflareConnectionSchema = z.discriminatedUnion("method", [
BaseCloudflareConnectionSchema.extend({
method: z.literal(CloudflareConnectionMethod.APIToken),
credentials: CloudflareConnectionApiTokenCredentialsSchema.pick({ accountId: true })
})
]);
export const ValidateCloudflareConnectionCredentialsSchema = z.discriminatedUnion("method", [
z.object({
method: z
.literal(CloudflareConnectionMethod.APIToken)
.describe(AppConnections.CREATE(AppConnection.Cloudflare).method),
credentials: CloudflareConnectionApiTokenCredentialsSchema.describe(
AppConnections.CREATE(AppConnection.Cloudflare).credentials
)
})
]);
export const CreateCloudflareConnectionSchema = ValidateCloudflareConnectionCredentialsSchema.and(
GenericCreateAppConnectionFieldsSchema(AppConnection.Cloudflare)
);
export const UpdateCloudflareConnectionSchema = z
.object({
credentials: CloudflareConnectionApiTokenCredentialsSchema.optional().describe(
AppConnections.UPDATE(AppConnection.Cloudflare).credentials
)
})
.and(GenericUpdateAppConnectionFieldsSchema(AppConnection.Cloudflare));
export const CloudflareConnectionListItemSchema = z.object({
name: z.literal("Cloudflare"),
app: z.literal(AppConnection.Cloudflare),
methods: z.nativeEnum(CloudflareConnectionMethod).array()
});

View File

@ -0,0 +1,30 @@
import { logger } from "@app/lib/logger";
import { OrgServiceActor } from "@app/lib/types";
import { AppConnection } from "../app-connection-enums";
import { listCloudflarePagesProjects } from "./cloudflare-connection-fns";
import { TCloudflareConnection } from "./cloudflare-connection-types";
type TGetAppConnectionFunc = (
app: AppConnection,
connectionId: string,
actor: OrgServiceActor
) => Promise<TCloudflareConnection>;
export const cloudflareConnectionService = (getAppConnection: TGetAppConnectionFunc) => {
const listPagesProjects = async (connectionId: string, actor: OrgServiceActor) => {
const appConnection = await getAppConnection(AppConnection.Cloudflare, connectionId, actor);
try {
const projects = await listCloudflarePagesProjects(appConnection);
return projects;
} catch (error) {
logger.error(error, "Failed to list Cloudflare Pages projects for Cloudflare connection");
return [];
}
};
return {
listPagesProjects
};
};

View File

@ -0,0 +1,30 @@
import z from "zod";
import { DiscriminativePick } from "@app/lib/types";
import { AppConnection } from "../app-connection-enums";
import {
CloudflareConnectionSchema,
CreateCloudflareConnectionSchema,
ValidateCloudflareConnectionCredentialsSchema
} from "./cloudflare-connection-schema";
export type TCloudflareConnection = z.infer<typeof CloudflareConnectionSchema>;
export type TCloudflareConnectionInput = z.infer<typeof CreateCloudflareConnectionSchema> & {
app: AppConnection.Cloudflare;
};
export type TValidateCloudflareConnectionCredentialsSchema = typeof ValidateCloudflareConnectionCredentialsSchema;
export type TCloudflareConnectionConfig = DiscriminativePick<
TCloudflareConnectionInput,
"method" | "app" | "credentials"
> & {
orgId: string;
};
export type TCloudflarePagesProject = {
id: string;
name: string;
};

View File

@ -0,0 +1,3 @@
export enum FlyioConnectionMethod {
AccessToken = "access-token"
}

View File

@ -0,0 +1,72 @@
import { AxiosError } from "axios";
import { request } from "@app/lib/config/request";
import { BadRequestError } from "@app/lib/errors";
import { AppConnection } from "@app/services/app-connection/app-connection-enums";
import { IntegrationUrls } from "@app/services/integration-auth/integration-list";
import { FlyioConnectionMethod } from "./flyio-connection-enums";
import { TFlyioApp, TFlyioConnection, TFlyioConnectionConfig } from "./flyio-connection-types";
export const getFlyioConnectionListItem = () => {
return {
name: "Fly.io" as const,
app: AppConnection.Flyio as const,
methods: Object.values(FlyioConnectionMethod) as [FlyioConnectionMethod.AccessToken]
};
};
export const validateFlyioConnectionCredentials = async (config: TFlyioConnectionConfig) => {
const { accessToken } = config.credentials;
try {
const resp = await request.post<{ data: { viewer: { id: string | null; email: string } } | null }>(
IntegrationUrls.FLYIO_API_URL,
{ query: "query { viewer { id email } }" },
{
headers: {
Authorization: `Bearer ${accessToken}`,
"Content-Type": "application/json"
}
}
);
if (resp.data.data === null) {
throw new BadRequestError({
message: "Unable to validate connection: Invalid access token provided."
});
}
} catch (error: unknown) {
if (error instanceof AxiosError) {
throw new BadRequestError({
message: `Failed to validate credentials: ${error.message || "Unknown error"}`
});
}
throw new BadRequestError({
message: "Unable to validate connection: verify credentials"
});
}
return config.credentials;
};
export const listFlyioApps = async (appConnection: TFlyioConnection) => {
const { accessToken } = appConnection.credentials;
const resp = await request.post<{ data: { apps: { nodes: TFlyioApp[] } } }>(
IntegrationUrls.FLYIO_API_URL,
{
query:
"query GetApps { apps { nodes { id name hostname status organization { id slug } currentRelease { version status createdAt } } } }"
},
{
headers: {
Authorization: `Bearer ${accessToken}`,
"Content-Type": "application/json",
Accept: "application/json"
}
}
);
return resp.data.data.apps.nodes;
};

View File

@ -0,0 +1,62 @@
import z from "zod";
import { AppConnections } from "@app/lib/api-docs";
import { AppConnection } from "@app/services/app-connection/app-connection-enums";
import {
BaseAppConnectionSchema,
GenericCreateAppConnectionFieldsSchema,
GenericUpdateAppConnectionFieldsSchema
} from "@app/services/app-connection/app-connection-schemas";
import { FlyioConnectionMethod } from "./flyio-connection-enums";
export const FlyioConnectionAccessTokenCredentialsSchema = z.object({
accessToken: z
.string()
.trim()
.min(1, "Access Token required")
.max(1000)
.startsWith("FlyV1", "Token must start with 'FlyV1'")
.describe(AppConnections.CREDENTIALS.FLYIO.accessToken)
});
const BaseFlyioConnectionSchema = BaseAppConnectionSchema.extend({ app: z.literal(AppConnection.Flyio) });
export const FlyioConnectionSchema = BaseFlyioConnectionSchema.extend({
method: z.literal(FlyioConnectionMethod.AccessToken),
credentials: FlyioConnectionAccessTokenCredentialsSchema
});
export const SanitizedFlyioConnectionSchema = z.discriminatedUnion("method", [
BaseFlyioConnectionSchema.extend({
method: z.literal(FlyioConnectionMethod.AccessToken),
credentials: FlyioConnectionAccessTokenCredentialsSchema.pick({})
})
]);
export const ValidateFlyioConnectionCredentialsSchema = z.discriminatedUnion("method", [
z.object({
method: z.literal(FlyioConnectionMethod.AccessToken).describe(AppConnections.CREATE(AppConnection.Flyio).method),
credentials: FlyioConnectionAccessTokenCredentialsSchema.describe(
AppConnections.CREATE(AppConnection.Flyio).credentials
)
})
]);
export const CreateFlyioConnectionSchema = ValidateFlyioConnectionCredentialsSchema.and(
GenericCreateAppConnectionFieldsSchema(AppConnection.Flyio)
);
export const UpdateFlyioConnectionSchema = z
.object({
credentials: FlyioConnectionAccessTokenCredentialsSchema.optional().describe(
AppConnections.UPDATE(AppConnection.Flyio).credentials
)
})
.and(GenericUpdateAppConnectionFieldsSchema(AppConnection.Flyio));
export const FlyioConnectionListItemSchema = z.object({
name: z.literal("Fly.io"),
app: z.literal(AppConnection.Flyio),
methods: z.nativeEnum(FlyioConnectionMethod).array()
});

View File

@ -0,0 +1,30 @@
import { logger } from "@app/lib/logger";
import { OrgServiceActor } from "@app/lib/types";
import { AppConnection } from "../app-connection-enums";
import { listFlyioApps } from "./flyio-connection-fns";
import { TFlyioConnection } from "./flyio-connection-types";
type TGetAppConnectionFunc = (
app: AppConnection,
connectionId: string,
actor: OrgServiceActor
) => Promise<TFlyioConnection>;
export const flyioConnectionService = (getAppConnection: TGetAppConnectionFunc) => {
const listApps = async (connectionId: string, actor: OrgServiceActor) => {
const appConnection = await getAppConnection(AppConnection.Flyio, connectionId, actor);
try {
const apps = await listFlyioApps(appConnection);
return apps;
} catch (error) {
logger.error(error, "Failed to establish connection with fly.io");
return [];
}
};
return {
listApps
};
};

View File

@ -0,0 +1,27 @@
import z from "zod";
import { DiscriminativePick } from "@app/lib/types";
import { AppConnection } from "../app-connection-enums";
import {
CreateFlyioConnectionSchema,
FlyioConnectionSchema,
ValidateFlyioConnectionCredentialsSchema
} from "./flyio-connection-schemas";
export type TFlyioConnection = z.infer<typeof FlyioConnectionSchema>;
export type TFlyioConnectionInput = z.infer<typeof CreateFlyioConnectionSchema> & {
app: AppConnection.Flyio;
};
export type TValidateFlyioConnectionCredentialsSchema = typeof ValidateFlyioConnectionCredentialsSchema;
export type TFlyioConnectionConfig = DiscriminativePick<TFlyioConnectionInput, "method" | "app" | "credentials"> & {
orgId: string;
};
export type TFlyioApp = {
id: string;
name: string;
};

View File

@ -0,0 +1,4 @@
export * from "./flyio-connection-enums";
export * from "./flyio-connection-fns";
export * from "./flyio-connection-schemas";
export * from "./flyio-connection-types";

View File

@ -7,6 +7,7 @@ import { request } from "@app/lib/config/request";
import { BadRequestError, ForbiddenRequestError, InternalServerError } from "@app/lib/errors"; import { BadRequestError, ForbiddenRequestError, InternalServerError } from "@app/lib/errors";
import { getAppConnectionMethodName } from "@app/services/app-connection/app-connection-fns"; import { getAppConnectionMethodName } from "@app/services/app-connection/app-connection-fns";
import { IntegrationUrls } from "@app/services/integration-auth/integration-list"; import { IntegrationUrls } from "@app/services/integration-auth/integration-list";
import { getInstanceIntegrationsConfig } from "@app/services/super-admin/super-admin-service";
import { AppConnection } from "../app-connection-enums"; import { AppConnection } from "../app-connection-enums";
import { GitHubConnectionMethod } from "./github-connection-enums"; import { GitHubConnectionMethod } from "./github-connection-enums";
@ -14,13 +15,14 @@ import { TGitHubConnection, TGitHubConnectionConfig } from "./github-connection-
export const getGitHubConnectionListItem = () => { export const getGitHubConnectionListItem = () => {
const { INF_APP_CONNECTION_GITHUB_OAUTH_CLIENT_ID, INF_APP_CONNECTION_GITHUB_APP_SLUG } = getConfig(); const { INF_APP_CONNECTION_GITHUB_OAUTH_CLIENT_ID, INF_APP_CONNECTION_GITHUB_APP_SLUG } = getConfig();
const { gitHubAppConnection } = getInstanceIntegrationsConfig();
return { return {
name: "GitHub" as const, name: "GitHub" as const,
app: AppConnection.GitHub as const, app: AppConnection.GitHub as const,
methods: Object.values(GitHubConnectionMethod) as [GitHubConnectionMethod.App, GitHubConnectionMethod.OAuth], methods: Object.values(GitHubConnectionMethod) as [GitHubConnectionMethod.App, GitHubConnectionMethod.OAuth],
oauthClientId: INF_APP_CONNECTION_GITHUB_OAUTH_CLIENT_ID, oauthClientId: INF_APP_CONNECTION_GITHUB_OAUTH_CLIENT_ID,
appClientSlug: INF_APP_CONNECTION_GITHUB_APP_SLUG appClientSlug: gitHubAppConnection.appSlug || INF_APP_CONNECTION_GITHUB_APP_SLUG
}; };
}; };
@ -30,23 +32,24 @@ export const getGitHubClient = (appConnection: TGitHubConnection) => {
const { method, credentials } = appConnection; const { method, credentials } = appConnection;
let client: Octokit; let client: Octokit;
const { gitHubAppConnection } = getInstanceIntegrationsConfig();
const appId = gitHubAppConnection.appId || appCfg.INF_APP_CONNECTION_GITHUB_APP_ID;
const appPrivateKey = gitHubAppConnection.privateKey || appCfg.INF_APP_CONNECTION_GITHUB_APP_PRIVATE_KEY;
switch (method) { switch (method) {
case GitHubConnectionMethod.App: case GitHubConnectionMethod.App:
if (!appCfg.INF_APP_CONNECTION_GITHUB_APP_ID || !appCfg.INF_APP_CONNECTION_GITHUB_APP_PRIVATE_KEY) { if (!appId || !appPrivateKey) {
throw new InternalServerError({ throw new InternalServerError({
message: `GitHub ${getAppConnectionMethodName(method).replace( message: `GitHub ${getAppConnectionMethodName(method).replace("GitHub", "")} has not been configured`
"GitHub",
""
)} environment variables have not been configured`
}); });
} }
client = new Octokit({ client = new Octokit({
authStrategy: createAppAuth, authStrategy: createAppAuth,
auth: { auth: {
appId: appCfg.INF_APP_CONNECTION_GITHUB_APP_ID, appId,
privateKey: appCfg.INF_APP_CONNECTION_GITHUB_APP_PRIVATE_KEY, privateKey: appPrivateKey,
installationId: credentials.installationId installationId: credentials.installationId
} }
}); });
@ -154,6 +157,8 @@ type TokenRespData = {
export const validateGitHubConnectionCredentials = async (config: TGitHubConnectionConfig) => { export const validateGitHubConnectionCredentials = async (config: TGitHubConnectionConfig) => {
const { credentials, method } = config; const { credentials, method } = config;
const { gitHubAppConnection } = getInstanceIntegrationsConfig();
const { const {
INF_APP_CONNECTION_GITHUB_OAUTH_CLIENT_ID, INF_APP_CONNECTION_GITHUB_OAUTH_CLIENT_ID,
INF_APP_CONNECTION_GITHUB_OAUTH_CLIENT_SECRET, INF_APP_CONNECTION_GITHUB_OAUTH_CLIENT_SECRET,
@ -165,8 +170,8 @@ export const validateGitHubConnectionCredentials = async (config: TGitHubConnect
const { clientId, clientSecret } = const { clientId, clientSecret } =
method === GitHubConnectionMethod.App method === GitHubConnectionMethod.App
? { ? {
clientId: INF_APP_CONNECTION_GITHUB_APP_CLIENT_ID, clientId: gitHubAppConnection.clientId || INF_APP_CONNECTION_GITHUB_APP_CLIENT_ID,
clientSecret: INF_APP_CONNECTION_GITHUB_APP_CLIENT_SECRET clientSecret: gitHubAppConnection.clientSecret || INF_APP_CONNECTION_GITHUB_APP_CLIENT_SECRET
} }
: // oauth : // oauth
{ {

View File

@ -0,0 +1,4 @@
export enum HerokuConnectionMethod {
AuthToken = "auth-token",
OAuth = "oauth"
}

View File

@ -0,0 +1,208 @@
import { AxiosError, AxiosResponse } from "axios";
import { getConfig } from "@app/lib/config/env";
import { request } from "@app/lib/config/request";
import { BadRequestError, InternalServerError } from "@app/lib/errors";
import { AppConnection } from "@app/services/app-connection/app-connection-enums";
import { encryptAppConnectionCredentials } from "@app/services/app-connection/app-connection-fns";
import { IntegrationUrls } from "@app/services/integration-auth/integration-list";
import { TKmsServiceFactory } from "@app/services/kms/kms-service";
import { TAppConnectionDALFactory } from "../app-connection-dal";
import { HerokuConnectionMethod } from "./heroku-connection-enums";
import { THerokuApp, THerokuConnection, THerokuConnectionConfig } from "./heroku-connection-types";
interface HerokuOAuthTokenResponse {
access_token: string;
expires_in: number;
refresh_token: string;
token_type: string;
user_id: string;
session_nonce: string;
}
export const getHerokuConnectionListItem = () => {
const { CLIENT_ID_HEROKU } = getConfig();
return {
name: "Heroku" as const,
app: AppConnection.Heroku as const,
methods: Object.values(HerokuConnectionMethod) as [HerokuConnectionMethod.AuthToken, HerokuConnectionMethod.OAuth],
oauthClientId: CLIENT_ID_HEROKU
};
};
export const refreshHerokuToken = async (
refreshToken: string,
appId: string,
orgId: string,
appConnectionDAL: Pick<TAppConnectionDALFactory, "updateById">,
kmsService: Pick<TKmsServiceFactory, "createCipherPairWithDataKey">
): Promise<string> => {
const { CLIENT_SECRET_HEROKU } = getConfig();
const payload = {
grant_type: "refresh_token",
refresh_token: refreshToken,
client_secret: CLIENT_SECRET_HEROKU
};
const { data } = await request.post<{ access_token: string; expires_in: number }>(
IntegrationUrls.HEROKU_TOKEN_URL,
payload,
{
headers: {
"Content-Type": "application/x-www-form-urlencoded"
}
}
);
const encryptedCredentials = await encryptAppConnectionCredentials({
credentials: {
refreshToken,
authToken: data.access_token,
expiresAt: new Date(Date.now() + data.expires_in * 1000 - 60000)
},
orgId,
kmsService
});
await appConnectionDAL.updateById(appId, { encryptedCredentials });
return data.access_token;
};
export const exchangeHerokuOAuthCode = async (code: string): Promise<HerokuOAuthTokenResponse> => {
const { CLIENT_SECRET_HEROKU } = getConfig();
try {
const response = await request.post<HerokuOAuthTokenResponse>(
IntegrationUrls.HEROKU_TOKEN_URL,
{
grant_type: "authorization_code",
code,
client_secret: CLIENT_SECRET_HEROKU
},
{
headers: {
"Content-Type": "application/x-www-form-urlencoded"
}
}
);
if (!response.data) {
throw new InternalServerError({
message: "Failed to exchange OAuth code: Empty response"
});
}
return response.data;
} catch (error: unknown) {
if (error instanceof AxiosError) {
throw new BadRequestError({
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
message: `Failed to exchange OAuth code: ${error.response?.data?.message || error.message || "Unknown error"}`
});
}
throw new BadRequestError({
message: "Unable to exchange OAuth code"
});
}
};
export const validateHerokuConnectionCredentials = async (config: THerokuConnectionConfig) => {
const { credentials: inputCredentials, method } = config;
let authToken: string;
let oauthData: HerokuOAuthTokenResponse | null = null;
if (method === HerokuConnectionMethod.OAuth && "code" in inputCredentials) {
oauthData = await exchangeHerokuOAuthCode(inputCredentials.code);
authToken = oauthData.access_token;
} else if (method === HerokuConnectionMethod.AuthToken && "authToken" in inputCredentials) {
authToken = inputCredentials.authToken;
} else {
throw new BadRequestError({
message: "Invalid credentials for the selected connection method"
});
}
let response: AxiosResponse<THerokuApp[]> | null = null;
try {
response = await request.get<THerokuApp[]>(`${IntegrationUrls.HEROKU_API_URL}/apps`, {
headers: {
Authorization: `Bearer ${authToken}`,
Accept: "application/vnd.heroku+json; version=3"
}
});
} catch (error: unknown) {
if (error instanceof AxiosError) {
throw new BadRequestError({
message: `Failed to validate credentials: ${error.message || "Unknown error"}`
});
}
throw new BadRequestError({
message: "Unable to validate connection: verify credentials"
});
}
if (!response?.data) {
throw new InternalServerError({
message: "Failed to get apps: Response was empty"
});
}
if (method === HerokuConnectionMethod.OAuth && oauthData) {
return {
authToken,
refreshToken: oauthData.refresh_token,
expiresIn: oauthData.expires_in,
tokenType: oauthData.token_type,
userId: oauthData.user_id,
sessionNonce: oauthData.session_nonce
};
}
return inputCredentials;
};
export const listHerokuApps = async ({
appConnection,
appConnectionDAL,
kmsService
}: {
appConnection: THerokuConnection;
appConnectionDAL: Pick<TAppConnectionDALFactory, "updateById">;
kmsService: Pick<TKmsServiceFactory, "createCipherPairWithDataKey">;
}): Promise<THerokuApp[]> => {
let authCredential = appConnection.credentials.authToken;
if (
appConnection.method === HerokuConnectionMethod.OAuth &&
appConnection.credentials.refreshToken &&
appConnection.credentials.expiresAt < new Date()
) {
authCredential = await refreshHerokuToken(
appConnection.credentials.refreshToken,
appConnection.id,
appConnection.orgId,
appConnectionDAL,
kmsService
);
}
const { data } = await request.get<THerokuApp[]>(`${IntegrationUrls.HEROKU_API_URL}/apps`, {
headers: {
Authorization: `Bearer ${authCredential}`,
Accept: "application/vnd.heroku+json; version=3"
}
});
if (!data) {
throw new InternalServerError({
message: "Failed to get apps: Response was empty"
});
}
return data.map((res) => ({ name: res.name, id: res.id }));
};

View File

@ -0,0 +1,103 @@
import z from "zod";
import { AppConnections } from "@app/lib/api-docs";
import { AppConnection } from "@app/services/app-connection/app-connection-enums";
import {
BaseAppConnectionSchema,
GenericCreateAppConnectionFieldsSchema,
GenericUpdateAppConnectionFieldsSchema
} from "@app/services/app-connection/app-connection-schemas";
import { HerokuConnectionMethod } from "./heroku-connection-enums";
export const HerokuConnectionAuthTokenCredentialsSchema = z.object({
authToken: z.string().trim().min(1, "Auth Token required").startsWith("HRKU-", "Token must start with 'HRKU-")
});
export const HerokuConnectionOAuthCredentialsSchema = z.object({
code: z.string().trim().min(1, "OAuth code required")
});
export const HerokuConnectionOAuthOutputCredentialsSchema = z.object({
authToken: z.string().trim(),
refreshToken: z.string().trim(),
expiresAt: z.date()
});
// Schema for refresh token input during initial setup
export const HerokuConnectionRefreshTokenCredentialsSchema = z.object({
refreshToken: z.string().trim().min(1, "Refresh token required")
});
const BaseHerokuConnectionSchema = BaseAppConnectionSchema.extend({
app: z.literal(AppConnection.Heroku)
});
export const HerokuConnectionSchema = z.intersection(
BaseHerokuConnectionSchema,
z.discriminatedUnion("method", [
z.object({
method: z.literal(HerokuConnectionMethod.AuthToken),
credentials: HerokuConnectionAuthTokenCredentialsSchema
}),
z.object({
method: z.literal(HerokuConnectionMethod.OAuth),
credentials: HerokuConnectionOAuthOutputCredentialsSchema
})
])
);
export const SanitizedHerokuConnectionSchema = z.discriminatedUnion("method", [
BaseHerokuConnectionSchema.extend({
method: z.literal(HerokuConnectionMethod.AuthToken),
credentials: HerokuConnectionAuthTokenCredentialsSchema.pick({})
}),
BaseHerokuConnectionSchema.extend({
method: z.literal(HerokuConnectionMethod.OAuth),
credentials: HerokuConnectionOAuthOutputCredentialsSchema.pick({})
})
]);
export const ValidateHerokuConnectionCredentialsSchema = z.discriminatedUnion("method", [
z.object({
method: z.literal(HerokuConnectionMethod.AuthToken).describe(AppConnections.CREATE(AppConnection.Heroku).method),
credentials: HerokuConnectionAuthTokenCredentialsSchema.describe(
AppConnections.CREATE(AppConnection.Heroku).credentials
)
}),
z.object({
method: z.literal(HerokuConnectionMethod.OAuth).describe(AppConnections.CREATE(AppConnection.Heroku).method),
credentials: z
.union([
HerokuConnectionOAuthCredentialsSchema,
HerokuConnectionRefreshTokenCredentialsSchema,
HerokuConnectionOAuthOutputCredentialsSchema
])
.describe(AppConnections.CREATE(AppConnection.Heroku).credentials)
})
]);
export const CreateHerokuConnectionSchema = ValidateHerokuConnectionCredentialsSchema.and(
GenericCreateAppConnectionFieldsSchema(AppConnection.Heroku)
);
export const UpdateHerokuConnectionSchema = z
.object({
credentials: z
.union([
HerokuConnectionAuthTokenCredentialsSchema,
HerokuConnectionOAuthOutputCredentialsSchema,
HerokuConnectionRefreshTokenCredentialsSchema,
HerokuConnectionOAuthCredentialsSchema
])
.optional()
.describe(AppConnections.UPDATE(AppConnection.Heroku).credentials)
})
.and(GenericUpdateAppConnectionFieldsSchema(AppConnection.Heroku));
export const HerokuConnectionListItemSchema = z.object({
name: z.literal("Heroku"),
app: z.literal(AppConnection.Heroku),
methods: z.nativeEnum(HerokuConnectionMethod).array(),
oauthClientId: z.string().optional()
});

View File

@ -0,0 +1,35 @@
import { logger } from "@app/lib/logger";
import { OrgServiceActor } from "@app/lib/types";
import { TKmsServiceFactory } from "@app/services/kms/kms-service";
import { TAppConnectionDALFactory } from "../app-connection-dal";
import { AppConnection } from "../app-connection-enums";
import { listHerokuApps as getHerokuApps } from "./heroku-connection-fns";
import { THerokuConnection } from "./heroku-connection-types";
type TGetAppConnectionFunc = (
app: AppConnection,
connectionId: string,
actor: OrgServiceActor
) => Promise<THerokuConnection>;
export const herokuConnectionService = (
getAppConnection: TGetAppConnectionFunc,
appConnectionDAL: Pick<TAppConnectionDALFactory, "updateById">,
kmsService: Pick<TKmsServiceFactory, "createCipherPairWithDataKey">
) => {
const listApps = async (connectionId: string, actor: OrgServiceActor) => {
const appConnection = await getAppConnection(AppConnection.Heroku, connectionId, actor);
try {
const apps = await getHerokuApps({ appConnection, appConnectionDAL, kmsService });
return apps;
} catch (error) {
logger.error(error, `Failed to establish connection with Heroku for app ${connectionId}`);
return [];
}
};
return {
listApps
};
};

View File

@ -0,0 +1,27 @@
import z from "zod";
import { DiscriminativePick } from "@app/lib/types";
import { AppConnection } from "../app-connection-enums";
import {
CreateHerokuConnectionSchema,
HerokuConnectionSchema,
ValidateHerokuConnectionCredentialsSchema
} from "./heroku-connection-schemas";
export type THerokuConnection = z.infer<typeof HerokuConnectionSchema>;
export type THerokuConnectionInput = z.infer<typeof CreateHerokuConnectionSchema> & {
app: AppConnection.Heroku;
};
export type TValidateHerokuConnectionCredentialsSchema = typeof ValidateHerokuConnectionCredentialsSchema;
export type THerokuConnectionConfig = DiscriminativePick<THerokuConnectionInput, "method" | "app" | "credentials"> & {
orgId: string;
};
export type THerokuApp = {
name: string;
id: string;
};

View File

@ -0,0 +1,4 @@
export * from "./heroku-connection-enums";
export * from "./heroku-connection-fns";
export * from "./heroku-connection-schemas";
export * from "./heroku-connection-types";

View File

@ -0,0 +1,3 @@
export enum RenderConnectionMethod {
ApiKey = "api-key"
}

View File

@ -0,0 +1,88 @@
/* eslint-disable no-await-in-loop */
import { AxiosError } from "axios";
import { request } from "@app/lib/config/request";
import { BadRequestError } from "@app/lib/errors";
import { IntegrationUrls } from "@app/services/integration-auth/integration-list";
import { AppConnection } from "../app-connection-enums";
import { RenderConnectionMethod } from "./render-connection-enums";
import {
TRawRenderService,
TRenderConnection,
TRenderConnectionConfig,
TRenderService
} from "./render-connection-types";
export const getRenderConnectionListItem = () => {
return {
name: "Render" as const,
app: AppConnection.Render as const,
methods: Object.values(RenderConnectionMethod) as [RenderConnectionMethod.ApiKey]
};
};
export const listRenderServices = async (appConnection: TRenderConnection): Promise<TRenderService[]> => {
const {
credentials: { apiKey }
} = appConnection;
const services: TRenderService[] = [];
let hasMorePages = true;
const perPage = 100;
let cursor;
while (hasMorePages) {
const res: TRawRenderService[] = (
await request.get<TRawRenderService[]>(`${IntegrationUrls.RENDER_API_URL}/v1/services`, {
params: new URLSearchParams({
...(cursor ? { cursor: String(cursor) } : {}),
limit: String(perPage)
}),
headers: {
Authorization: `Bearer ${apiKey}`,
Accept: "application/json",
"Accept-Encoding": "application/json"
}
})
).data;
res.forEach((item) => {
services.push({
name: item.service.name,
id: item.service.id
});
});
if (res.length < perPage) {
hasMorePages = false;
} else {
cursor = res[res.length - 1].cursor;
}
}
return services;
};
export const validateRenderConnectionCredentials = async (config: TRenderConnectionConfig) => {
const { credentials: inputCredentials } = config;
try {
await request.get(`${IntegrationUrls.RENDER_API_URL}/v1/users`, {
headers: {
Authorization: `Bearer ${inputCredentials.apiKey}`
}
});
} catch (error: unknown) {
if (error instanceof AxiosError) {
throw new BadRequestError({
message: `Failed to validate credentials: ${error.message || "Unknown error"}`
});
}
throw new BadRequestError({
message: "Unable to validate connection: verify credentials"
});
}
return inputCredentials;
};

View File

@ -0,0 +1,56 @@
import z from "zod";
import { AppConnections } from "@app/lib/api-docs";
import { AppConnection } from "@app/services/app-connection/app-connection-enums";
import {
BaseAppConnectionSchema,
GenericCreateAppConnectionFieldsSchema,
GenericUpdateAppConnectionFieldsSchema
} from "@app/services/app-connection/app-connection-schemas";
import { RenderConnectionMethod } from "./render-connection-enums";
export const RenderConnectionApiKeyCredentialsSchema = z.object({
apiKey: z.string().trim().min(1, "API key required").max(256, "API key cannot exceed 256 characters")
});
const BaseRenderConnectionSchema = BaseAppConnectionSchema.extend({ app: z.literal(AppConnection.Render) });
export const RenderConnectionSchema = BaseRenderConnectionSchema.extend({
method: z.literal(RenderConnectionMethod.ApiKey),
credentials: RenderConnectionApiKeyCredentialsSchema
});
export const SanitizedRenderConnectionSchema = z.discriminatedUnion("method", [
BaseRenderConnectionSchema.extend({
method: z.literal(RenderConnectionMethod.ApiKey),
credentials: RenderConnectionApiKeyCredentialsSchema.pick({})
})
]);
export const ValidateRenderConnectionCredentialsSchema = z.discriminatedUnion("method", [
z.object({
method: z.literal(RenderConnectionMethod.ApiKey).describe(AppConnections.CREATE(AppConnection.Render).method),
credentials: RenderConnectionApiKeyCredentialsSchema.describe(
AppConnections.CREATE(AppConnection.Render).credentials
)
})
]);
export const CreateRenderConnectionSchema = ValidateRenderConnectionCredentialsSchema.and(
GenericCreateAppConnectionFieldsSchema(AppConnection.Render)
);
export const UpdateRenderConnectionSchema = z
.object({
credentials: RenderConnectionApiKeyCredentialsSchema.optional().describe(
AppConnections.UPDATE(AppConnection.Render).credentials
)
})
.and(GenericUpdateAppConnectionFieldsSchema(AppConnection.Render));
export const RenderConnectionListItemSchema = z.object({
name: z.literal("Render"),
app: z.literal(AppConnection.Render),
methods: z.nativeEnum(RenderConnectionMethod).array()
});

View File

@ -0,0 +1,30 @@
import { logger } from "@app/lib/logger";
import { OrgServiceActor } from "@app/lib/types";
import { AppConnection } from "../app-connection-enums";
import { listRenderServices } from "./render-connection-fns";
import { TRenderConnection } from "./render-connection-types";
type TGetAppConnectionFunc = (
app: AppConnection,
connectionId: string,
actor: OrgServiceActor
) => Promise<TRenderConnection>;
export const renderConnectionService = (getAppConnection: TGetAppConnectionFunc) => {
const listServices = async (connectionId: string, actor: OrgServiceActor) => {
const appConnection = await getAppConnection(AppConnection.Render, connectionId, actor);
try {
const services = await listRenderServices(appConnection);
return services;
} catch (error) {
logger.error(error, "Failed to list services for Render connection");
return [];
}
};
return {
listServices
};
};

View File

@ -0,0 +1,35 @@
import z from "zod";
import { DiscriminativePick } from "@app/lib/types";
import { AppConnection } from "../app-connection-enums";
import {
CreateRenderConnectionSchema,
RenderConnectionSchema,
ValidateRenderConnectionCredentialsSchema
} from "./render-connection-schema";
export type TRenderConnection = z.infer<typeof RenderConnectionSchema>;
export type TRenderConnectionInput = z.infer<typeof CreateRenderConnectionSchema> & {
app: AppConnection.Render;
};
export type TValidateRenderConnectionCredentialsSchema = typeof ValidateRenderConnectionCredentialsSchema;
export type TRenderConnectionConfig = DiscriminativePick<TRenderConnectionInput, "method" | "app" | "credentials"> & {
orgId: string;
};
export type TRenderService = {
name: string;
id: string;
};
export type TRawRenderService = {
cursor: string;
service: {
id: string;
name: string;
};
};

View File

@ -9,6 +9,7 @@ const arnRegex = new RE2(/^arn:aws:iam::\d{12}:(user\/[a-zA-Z0-9_.@+*/-]+|role\/
export const validateAccountIds = z export const validateAccountIds = z
.string() .string()
.trim() .trim()
.max(2048)
.default("") .default("")
// Custom validation to ensure each part is a 12-digit number // Custom validation to ensure each part is a 12-digit number
.refine( .refine(
@ -36,6 +37,7 @@ export const validateAccountIds = z
export const validatePrincipalArns = z export const validatePrincipalArns = z
.string() .string()
.trim() .trim()
.max(2048)
.default("") .default("")
// Custom validation for ARN format // Custom validation for ARN format
.refine( .refine(

View File

@ -101,6 +101,7 @@ export const identityProjectDALFactory = (db: TDbClient) => {
db.ref("id").as("identityId").withSchema(TableName.Identity), db.ref("id").as("identityId").withSchema(TableName.Identity),
db.ref("name").as("identityName").withSchema(TableName.Identity), db.ref("name").as("identityName").withSchema(TableName.Identity),
db.ref("hasDeleteProtection").withSchema(TableName.Identity),
db.ref("id").withSchema(TableName.IdentityProjectMembership), db.ref("id").withSchema(TableName.IdentityProjectMembership),
db.ref("role").withSchema(TableName.IdentityProjectMembershipRole), db.ref("role").withSchema(TableName.IdentityProjectMembershipRole),
db.ref("id").withSchema(TableName.IdentityProjectMembershipRole).as("membershipRoleId"), db.ref("id").withSchema(TableName.IdentityProjectMembershipRole).as("membershipRoleId"),
@ -130,6 +131,7 @@ export const identityProjectDALFactory = (db: TDbClient) => {
data: docs, data: docs,
parentMapper: ({ parentMapper: ({
identityName, identityName,
hasDeleteProtection,
uaId, uaId,
awsId, awsId,
gcpId, gcpId,
@ -151,6 +153,7 @@ export const identityProjectDALFactory = (db: TDbClient) => {
identity: { identity: {
id: identityId, id: identityId,
name: identityName, name: identityName,
hasDeleteProtection,
authMethods: buildAuthMethods({ authMethods: buildAuthMethods({
uaId, uaId,
awsId, awsId,

View File

@ -114,16 +114,18 @@ export const identityOrgDALFactory = (db: TDbClient) => {
db.ref("id").as("tokenId").withSchema(TableName.IdentityTokenAuth), db.ref("id").as("tokenId").withSchema(TableName.IdentityTokenAuth),
db.ref("id").as("jwtId").withSchema(TableName.IdentityJwtAuth), db.ref("id").as("jwtId").withSchema(TableName.IdentityJwtAuth),
db.ref("id").as("ldapId").withSchema(TableName.IdentityLdapAuth), db.ref("id").as("ldapId").withSchema(TableName.IdentityLdapAuth),
db.ref("name").withSchema(TableName.Identity) db.ref("name").withSchema(TableName.Identity),
db.ref("hasDeleteProtection").withSchema(TableName.Identity)
); );
if (data) { if (data) {
const { name } = data; const { name, hasDeleteProtection } = data;
return { return {
...data, ...data,
identity: { identity: {
id: data.identityId, id: data.identityId,
name, name,
hasDeleteProtection,
authMethods: buildAuthMethods(data) authMethods: buildAuthMethods(data)
} }
}; };
@ -155,7 +157,8 @@ export const identityOrgDALFactory = (db: TDbClient) => {
.orderBy(`${TableName.Identity}.${orderBy}`, orderDirection) .orderBy(`${TableName.Identity}.${orderBy}`, orderDirection)
.select( .select(
selectAllTableCols(TableName.IdentityOrgMembership), selectAllTableCols(TableName.IdentityOrgMembership),
db.ref("name").withSchema(TableName.Identity).as("identityName") db.ref("name").withSchema(TableName.Identity).as("identityName"),
db.ref("hasDeleteProtection").withSchema(TableName.Identity)
) )
.where(filter) .where(filter)
.as("paginatedIdentity"); .as("paginatedIdentity");
@ -245,6 +248,7 @@ export const identityOrgDALFactory = (db: TDbClient) => {
db.ref("updatedAt").withSchema("paginatedIdentity"), db.ref("updatedAt").withSchema("paginatedIdentity"),
db.ref("identityId").withSchema("paginatedIdentity").as("identityId"), db.ref("identityId").withSchema("paginatedIdentity").as("identityId"),
db.ref("identityName").withSchema("paginatedIdentity"), db.ref("identityName").withSchema("paginatedIdentity"),
db.ref("hasDeleteProtection").withSchema("paginatedIdentity"),
db.ref("id").as("uaId").withSchema(TableName.IdentityUniversalAuth), db.ref("id").as("uaId").withSchema(TableName.IdentityUniversalAuth),
db.ref("id").as("gcpId").withSchema(TableName.IdentityGcpAuth), db.ref("id").as("gcpId").withSchema(TableName.IdentityGcpAuth),
@ -286,6 +290,7 @@ export const identityOrgDALFactory = (db: TDbClient) => {
crName, crName,
identityId, identityId,
identityName, identityName,
hasDeleteProtection,
role, role,
roleId, roleId,
id, id,
@ -324,6 +329,7 @@ export const identityOrgDALFactory = (db: TDbClient) => {
identity: { identity: {
id: identityId, id: identityId,
name: identityName, name: identityName,
hasDeleteProtection,
authMethods: buildAuthMethods({ authMethods: buildAuthMethods({
uaId, uaId,
alicloudId, alicloudId,
@ -476,6 +482,7 @@ export const identityOrgDALFactory = (db: TDbClient) => {
db.ref("updatedAt").withSchema(TableName.IdentityOrgMembership), db.ref("updatedAt").withSchema(TableName.IdentityOrgMembership),
db.ref("identityId").withSchema(TableName.IdentityOrgMembership).as("identityId"), db.ref("identityId").withSchema(TableName.IdentityOrgMembership).as("identityId"),
db.ref("name").withSchema(TableName.Identity).as("identityName"), db.ref("name").withSchema(TableName.Identity).as("identityName"),
db.ref("hasDeleteProtection").withSchema(TableName.Identity),
db.ref("id").as("uaId").withSchema(TableName.IdentityUniversalAuth), db.ref("id").as("uaId").withSchema(TableName.IdentityUniversalAuth),
db.ref("id").as("gcpId").withSchema(TableName.IdentityGcpAuth), db.ref("id").as("gcpId").withSchema(TableName.IdentityGcpAuth),
@ -518,6 +525,7 @@ export const identityOrgDALFactory = (db: TDbClient) => {
crName, crName,
identityId, identityId,
identityName, identityName,
hasDeleteProtection,
role, role,
roleId, roleId,
total_count, total_count,
@ -556,6 +564,7 @@ export const identityOrgDALFactory = (db: TDbClient) => {
identity: { identity: {
id: identityId, id: identityId,
name: identityName, name: identityName,
hasDeleteProtection,
authMethods: buildAuthMethods({ authMethods: buildAuthMethods({
uaId, uaId,
alicloudId, alicloudId,

View File

@ -47,6 +47,7 @@ export const identityServiceFactory = ({
const createIdentity = async ({ const createIdentity = async ({
name, name,
role, role,
hasDeleteProtection,
actor, actor,
orgId, orgId,
actorId, actorId,
@ -96,7 +97,7 @@ export const identityServiceFactory = ({
} }
const identity = await identityDAL.transaction(async (tx) => { const identity = await identityDAL.transaction(async (tx) => {
const newIdentity = await identityDAL.create({ name }, tx); const newIdentity = await identityDAL.create({ name, hasDeleteProtection }, tx);
await identityOrgMembershipDAL.create( await identityOrgMembershipDAL.create(
{ {
identityId: newIdentity.id, identityId: newIdentity.id,
@ -138,6 +139,7 @@ export const identityServiceFactory = ({
const updateIdentity = async ({ const updateIdentity = async ({
id, id,
role, role,
hasDeleteProtection,
name, name,
actor, actor,
actorId, actorId,
@ -189,7 +191,11 @@ export const identityServiceFactory = ({
} }
const identity = await identityDAL.transaction(async (tx) => { const identity = await identityDAL.transaction(async (tx) => {
const newIdentity = name ? await identityDAL.updateById(id, { name }, tx) : await identityDAL.findById(id, tx); const newIdentity =
name || hasDeleteProtection
? await identityDAL.updateById(id, { name, hasDeleteProtection }, tx)
: await identityDAL.findById(id, tx);
if (role) { if (role) {
await identityOrgMembershipDAL.updateById( await identityOrgMembershipDAL.updateById(
identityOrgMembership.id, identityOrgMembership.id,
@ -272,6 +278,9 @@ export const identityServiceFactory = ({
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionIdentityActions.Delete, OrgPermissionSubjects.Identity); ForbiddenError.from(permission).throwUnlessCan(OrgPermissionIdentityActions.Delete, OrgPermissionSubjects.Identity);
if (identityOrgMembership.identity.hasDeleteProtection)
throw new BadRequestError({ message: "Identity has delete protection" });
const deletedIdentity = await identityDAL.deleteById(id); const deletedIdentity = await identityDAL.deleteById(id);
await licenseService.updateSubscriptionOrgMemberCount(identityOrgMembership.orgId); await licenseService.updateSubscriptionOrgMemberCount(identityOrgMembership.orgId);

View File

@ -5,12 +5,14 @@ import { OrderByDirection, TOrgPermission } from "@app/lib/types";
export type TCreateIdentityDTO = { export type TCreateIdentityDTO = {
role: string; role: string;
name: string; name: string;
hasDeleteProtection: boolean;
metadata?: { key: string; value: string }[]; metadata?: { key: string; value: string }[];
} & TOrgPermission; } & TOrgPermission;
export type TUpdateIdentityDTO = { export type TUpdateIdentityDTO = {
id: string; id: string;
role?: string; role?: string;
hasDeleteProtection?: boolean;
name?: string; name?: string;
metadata?: { key: string; value: string }[]; metadata?: { key: string; value: string }[];
isActorSuperAdmin?: boolean; isActorSuperAdmin?: boolean;

View File

@ -84,6 +84,8 @@ export enum IntegrationUrls {
QOVERY_API_URL = "https://api.qovery.com", QOVERY_API_URL = "https://api.qovery.com",
TERRAFORM_CLOUD_API_URL = "https://app.terraform.io", TERRAFORM_CLOUD_API_URL = "https://app.terraform.io",
CLOUDFLARE_PAGES_API_URL = "https://api.cloudflare.com", CLOUDFLARE_PAGES_API_URL = "https://api.cloudflare.com",
// eslint-disable-next-line @typescript-eslint/no-duplicate-enum-values
CLOUDFLARE_API_URL = "https://api.cloudflare.com",
// eslint-disable-next-line // eslint-disable-next-line
CLOUDFLARE_WORKERS_API_URL = "https://api.cloudflare.com", CLOUDFLARE_WORKERS_API_URL = "https://api.cloudflare.com",
BITBUCKET_API_URL = "https://api.bitbucket.org", BITBUCKET_API_URL = "https://api.bitbucket.org",

View File

@ -42,7 +42,7 @@ import { TProjectPermission } from "@app/lib/types";
import { TQueueServiceFactory } from "@app/queue"; import { TQueueServiceFactory } from "@app/queue";
import { TPkiSubscriberDALFactory } from "@app/services/pki-subscriber/pki-subscriber-dal"; import { TPkiSubscriberDALFactory } from "@app/services/pki-subscriber/pki-subscriber-dal";
import { ActorType } from "../auth/auth-type"; import { ActorAuthMethod, ActorType } from "../auth/auth-type";
import { TCertificateDALFactory } from "../certificate/certificate-dal"; import { TCertificateDALFactory } from "../certificate/certificate-dal";
import { TCertificateAuthorityDALFactory } from "../certificate-authority/certificate-authority-dal"; import { TCertificateAuthorityDALFactory } from "../certificate-authority/certificate-authority-dal";
import { expandInternalCa } from "../certificate-authority/certificate-authority-fns"; import { expandInternalCa } from "../certificate-authority/certificate-authority-fns";
@ -82,6 +82,7 @@ import { assignWorkspaceKeysToMembers, bootstrapSshProject, createProjectKey } f
import { TProjectQueueFactory } from "./project-queue"; import { TProjectQueueFactory } from "./project-queue";
import { TProjectSshConfigDALFactory } from "./project-ssh-config-dal"; import { TProjectSshConfigDALFactory } from "./project-ssh-config-dal";
import { import {
ProjectFilterType,
TCreateProjectDTO, TCreateProjectDTO,
TDeleteProjectDTO, TDeleteProjectDTO,
TDeleteProjectWorkflowIntegration, TDeleteProjectWorkflowIntegration,
@ -866,6 +867,39 @@ export const projectServiceFactory = ({
}); });
}; };
const extractProjectIdFromSlug = async ({
projectSlug,
projectId,
actorId,
actorAuthMethod,
actor,
actorOrgId
}: {
projectSlug?: string;
projectId?: string;
actorId: string;
actorAuthMethod: ActorAuthMethod;
actor: ActorType;
actorOrgId: string;
}) => {
if (projectId) return projectId;
if (!projectSlug) throw new BadRequestError({ message: "You must provide projectSlug or workspaceId" });
const project = await getAProject({
filter: {
type: ProjectFilterType.SLUG,
orgId: actorOrgId,
slug: projectSlug
},
actorId,
actorAuthMethod,
actor,
actorOrgId
});
if (!project) throw new NotFoundError({ message: `No project found with slug ${projectSlug}` });
return project.id;
};
const getProjectUpgradeStatus = async ({ const getProjectUpgradeStatus = async ({
projectId, projectId,
actor, actor,
@ -2006,6 +2040,7 @@ export const projectServiceFactory = ({
getProjectSshConfig, getProjectSshConfig,
updateProjectSshConfig, updateProjectSshConfig,
requestProjectAccess, requestProjectAccess,
searchProjects searchProjects,
extractProjectIdFromSlug
}; };
}; };

View File

@ -6,6 +6,7 @@ import { ActionProjectType, TSecretFoldersInsert } from "@app/db/schemas";
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service-types"; import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service-types";
import { ProjectPermissionActions, ProjectPermissionSub } from "@app/ee/services/permission/project-permission"; import { ProjectPermissionActions, ProjectPermissionSub } from "@app/ee/services/permission/project-permission";
import { TSecretSnapshotServiceFactory } from "@app/ee/services/secret-snapshot/secret-snapshot-service"; import { TSecretSnapshotServiceFactory } from "@app/ee/services/secret-snapshot/secret-snapshot-service";
import { PgSqlLock } from "@app/keystore/keystore";
import { BadRequestError, NotFoundError } from "@app/lib/errors"; import { BadRequestError, NotFoundError } from "@app/lib/errors";
import { OrderByDirection, OrgServiceActor } from "@app/lib/types"; import { OrderByDirection, OrgServiceActor } from "@app/lib/types";
import { buildFolderPath } from "@app/services/secret-folder/secret-folder-fns"; import { buildFolderPath } from "@app/services/secret-folder/secret-folder-fns";
@ -83,36 +84,75 @@ export const secretFolderServiceFactory = ({
// that is this request must be idempotent // that is this request must be idempotent
// so we do a tricky move. we try to find the to be created folder path if that is exactly match return that // so we do a tricky move. we try to find the to be created folder path if that is exactly match return that
// else we get some path before that then we will start creating remaining folder // else we get some path before that then we will start creating remaining folder
await tx.raw("SELECT pg_advisory_xact_lock(?)", [PgSqlLock.CreateFolder(env.id, env.projectId)]);
const pathWithFolder = path.join(secretPath, name); const pathWithFolder = path.join(secretPath, name);
const parentFolder = await folderDAL.findClosestFolder(projectId, environment, pathWithFolder, tx); const parentFolder = await folderDAL.findClosestFolder(projectId, environment, pathWithFolder, tx);
// no folder found is not possible root should be their
if (!parentFolder) { if (!parentFolder) {
throw new NotFoundError({ throw new NotFoundError({
message: `Folder with path '${pathWithFolder}' in environment with slug '${environment}' not found` message: `Parent folder for path '${pathWithFolder}' not found`
}); });
} }
// exact folder
if (parentFolder.path === pathWithFolder) return parentFolder;
let parentFolderId = parentFolder.id; // check if the exact folder already exists
const existingFolder = await folderDAL.findOne(
{
envId: env.id,
parentId: parentFolder.id,
name,
isReserved: false
},
tx
);
if (existingFolder) {
return existingFolder;
}
// exact folder case
if (parentFolder.path === pathWithFolder) {
return parentFolder;
}
let currentParentId = parentFolder.id;
// build the full path we need by processing each segment
if (parentFolder.path !== secretPath) { if (parentFolder.path !== secretPath) {
// this is upsert folder in a path const missingSegments = secretPath.substring(parentFolder.path.length).split("/").filter(Boolean);
// we are not taking snapshots of this because
// snapshot will be removed from automatic for all commits to user click or cron based const newFolders: TSecretFoldersInsert[] = [];
const missingSegment = secretPath.substring(parentFolder.path.length).split("/").filter(Boolean);
if (missingSegment.length) { // process each segment sequentially
const newFolders: Array<TSecretFoldersInsert & { id: string }> = missingSegment.map((segment) => { for await (const segment of missingSegments) {
const existingSegment = await folderDAL.findOne(
{
name: segment,
parentId: currentParentId,
envId: env.id,
isReserved: false
},
tx
);
if (existingSegment) {
// use existing folder and update the path / parent
currentParentId = existingSegment.id;
} else {
const newFolder = { const newFolder = {
name: segment, name: segment,
parentId: parentFolderId, parentId: currentParentId,
id: uuidv4(), id: uuidv4(),
envId: env.id, envId: env.id,
version: 1 version: 1
}; };
parentFolderId = newFolder.id;
return newFolder; currentParentId = newFolder.id;
}); newFolders.push(newFolder);
parentFolderId = newFolders.at(-1)?.id as string; }
}
if (newFolders.length) {
const docs = await folderDAL.insertMany(newFolders, tx); const docs = await folderDAL.insertMany(newFolders, tx);
const folderVersions = await folderVersionDAL.insertMany( const folderVersions = await folderVersionDAL.insertMany(
docs.map((doc) => ({ docs.map((doc) => ({
@ -133,7 +173,7 @@ export const secretFolderServiceFactory = ({
} }
}, },
message: "Folder created", message: "Folder created",
folderId: parentFolderId, folderId: currentParentId,
changes: folderVersions.map((fv) => ({ changes: folderVersions.map((fv) => ({
type: CommitType.ADD, type: CommitType.ADD,
folderVersionId: fv.id folderVersionId: fv.id
@ -145,9 +185,10 @@ export const secretFolderServiceFactory = ({
} }
const doc = await folderDAL.create( const doc = await folderDAL.create(
{ name, envId: env.id, version: 1, parentId: parentFolderId, description }, { name, envId: env.id, version: 1, parentId: currentParentId, description },
tx tx
); );
const folderVersion = await folderVersionDAL.create( const folderVersion = await folderVersionDAL.create(
{ {
name: doc.name, name: doc.name,
@ -158,6 +199,7 @@ export const secretFolderServiceFactory = ({
}, },
tx tx
); );
await folderCommitService.createCommit( await folderCommitService.createCommit(
{ {
actor: { actor: {
@ -167,7 +209,7 @@ export const secretFolderServiceFactory = ({
} }
}, },
message: "Folder created", message: "Folder created",
folderId: parentFolderId, folderId: doc.id,
changes: [ changes: [
{ {
type: CommitType.ADD, type: CommitType.ADD,
@ -177,6 +219,7 @@ export const secretFolderServiceFactory = ({
}, },
tx tx
); );
return doc; return doc;
}); });

View File

@ -0,0 +1,10 @@
import { AppConnection } from "@app/services/app-connection/app-connection-enums";
import { SecretSync } from "@app/services/secret-sync/secret-sync-enums";
import { TSecretSyncListItem } from "@app/services/secret-sync/secret-sync-types";
export const CLOUDFLARE_PAGES_SYNC_LIST_OPTION: TSecretSyncListItem = {
name: "Cloudflare Pages",
destination: SecretSync.CloudflarePages,
connection: AppConnection.Cloudflare,
canImportSecrets: false
};

View File

@ -0,0 +1,138 @@
import { request } from "@app/lib/config/request";
import { IntegrationUrls } from "@app/services/integration-auth/integration-list";
import { matchesSchema } from "@app/services/secret-sync/secret-sync-fns";
import { TSecretMap } from "@app/services/secret-sync/secret-sync-types";
import { SECRET_SYNC_NAME_MAP } from "../secret-sync-maps";
import { TCloudflarePagesSyncWithCredentials } from "./cloudflare-pages-types";
const getProjectEnvironmentSecrets = async (secretSync: TCloudflarePagesSyncWithCredentials) => {
const {
destinationConfig,
connection: {
credentials: { apiToken, accountId }
}
} = secretSync;
const secrets = (
await request.get<{
result: {
deployment_configs: Record<
string,
{
env_vars: Record<string, { type: "plain_text" | "secret_text"; value: string }>;
}
>;
};
}>(
`${IntegrationUrls.CLOUDFLARE_PAGES_API_URL}/client/v4/accounts/${accountId}/pages/projects/${destinationConfig.projectName}`,
{
headers: {
Authorization: `Bearer ${apiToken}`,
Accept: "application/json"
}
}
)
).data.result.deployment_configs[destinationConfig.environment].env_vars;
return Object.entries(secrets ?? {}).map(([key, envVar]) => ({
key,
value: envVar.value
}));
};
export const CloudflarePagesSyncFns = {
syncSecrets: async (secretSync: TCloudflarePagesSyncWithCredentials, secretMap: TSecretMap) => {
const {
destinationConfig,
connection: {
credentials: { apiToken, accountId }
}
} = secretSync;
// Create/update secret entries
let secretEntries: [string, object | null][] = Object.entries(secretMap).map(([key, val]) => [
key,
{ type: "secret_text", value: val.value }
]);
// Handle deletions if not disabled
if (!secretSync.syncOptions.disableSecretDeletion) {
const existingSecrets = await getProjectEnvironmentSecrets(secretSync);
const toDeleteKeys = existingSecrets
.filter(
(secret) =>
matchesSchema(secret.key, secretSync.environment?.slug || "", secretSync.syncOptions.keySchema) &&
!secretMap[secret.key]
)
.map((secret) => secret.key);
const toDeleteEntries: [string, null][] = toDeleteKeys.map((key) => [key, null]);
secretEntries = [...secretEntries, ...toDeleteEntries];
}
const data = {
deployment_configs: {
[destinationConfig.environment]: {
env_vars: Object.fromEntries(secretEntries)
}
}
};
await request.patch(
`${IntegrationUrls.CLOUDFLARE_PAGES_API_URL}/client/v4/accounts/${accountId}/pages/projects/${destinationConfig.projectName}`,
data,
{
headers: {
Authorization: `Bearer ${apiToken}`,
Accept: "application/json"
}
}
);
},
getSecrets: async (secretSync: TCloudflarePagesSyncWithCredentials): Promise<TSecretMap> => {
throw new Error(`${SECRET_SYNC_NAME_MAP[secretSync.destination]} does not support importing secrets.`);
},
removeSecrets: async (secretSync: TCloudflarePagesSyncWithCredentials, secretMap: TSecretMap) => {
const {
destinationConfig,
connection: {
credentials: { apiToken, accountId }
}
} = secretSync;
const secrets = await getProjectEnvironmentSecrets(secretSync);
const toDeleteKeys = secrets
.filter(
(secret) =>
matchesSchema(secret.key, secretSync.environment?.slug || "", secretSync.syncOptions.keySchema) &&
secret.key in secretMap
)
.map((secret) => secret.key);
if (toDeleteKeys.length === 0) return;
const secretEntries: [string, null][] = toDeleteKeys.map((key) => [key, null]);
const data = {
deployment_configs: {
[destinationConfig.environment]: {
env_vars: Object.fromEntries(secretEntries)
}
}
};
await request.patch(
`${IntegrationUrls.CLOUDFLARE_PAGES_API_URL}/client/v4/accounts/${accountId}/pages/projects/${destinationConfig.projectName}`,
data,
{
headers: {
Authorization: `Bearer ${apiToken}`,
Accept: "application/json"
}
}
);
}
};

View File

@ -0,0 +1,53 @@
import { z } from "zod";
import { SecretSyncs } from "@app/lib/api-docs";
import { AppConnection } from "@app/services/app-connection/app-connection-enums";
import { SecretSync } from "@app/services/secret-sync/secret-sync-enums";
import {
BaseSecretSyncSchema,
GenericCreateSecretSyncFieldsSchema,
GenericUpdateSecretSyncFieldsSchema
} from "@app/services/secret-sync/secret-sync-schemas";
import { TSyncOptionsConfig } from "@app/services/secret-sync/secret-sync-types";
const CloudflarePagesSyncDestinationConfigSchema = z.object({
projectName: z
.string()
.min(1, "Project name is required")
.describe(SecretSyncs.DESTINATION_CONFIG.CLOUDFLARE_PAGES.projectName),
environment: z
.string()
.min(1, "Environment is required")
.describe(SecretSyncs.DESTINATION_CONFIG.CLOUDFLARE_PAGES.environment)
});
const CloudflarePagesSyncOptionsConfig: TSyncOptionsConfig = { canImportSecrets: false };
export const CloudflarePagesSyncSchema = BaseSecretSyncSchema(
SecretSync.CloudflarePages,
CloudflarePagesSyncOptionsConfig
).extend({
destination: z.literal(SecretSync.CloudflarePages),
destinationConfig: CloudflarePagesSyncDestinationConfigSchema
});
export const CreateCloudflarePagesSyncSchema = GenericCreateSecretSyncFieldsSchema(
SecretSync.CloudflarePages,
CloudflarePagesSyncOptionsConfig
).extend({
destinationConfig: CloudflarePagesSyncDestinationConfigSchema
});
export const UpdateCloudflarePagesSyncSchema = GenericUpdateSecretSyncFieldsSchema(
SecretSync.CloudflarePages,
CloudflarePagesSyncOptionsConfig
).extend({
destinationConfig: CloudflarePagesSyncDestinationConfigSchema.optional()
});
export const CloudflarePagesSyncListItemSchema = z.object({
name: z.literal("Cloudflare Pages"),
connection: z.literal(AppConnection.Cloudflare),
destination: z.literal(SecretSync.CloudflarePages),
canImportSecrets: z.literal(false)
});

View File

@ -0,0 +1,19 @@
import z from "zod";
import { TCloudflareConnection } from "@app/services/app-connection/cloudflare/cloudflare-connection-types";
import {
CloudflarePagesSyncListItemSchema,
CloudflarePagesSyncSchema,
CreateCloudflarePagesSyncSchema
} from "./cloudflare-pages-schema";
export type TCloudflarePagesSyncListItem = z.infer<typeof CloudflarePagesSyncListItemSchema>;
export type TCloudflarePagesSync = z.infer<typeof CloudflarePagesSyncSchema>;
export type TCloudflarePagesSyncInput = z.infer<typeof CreateCloudflarePagesSyncSchema>;
export type TCloudflarePagesSyncWithCredentials = TCloudflarePagesSync & {
connection: TCloudflareConnection;
};

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