Compare commits

...

233 Commits

Author SHA1 Message Date
Sheen Capadngan
8e68d21115 misc: support glob patterns for oidc 2024-09-09 17:17:12 +08:00
Maidul Islam
364302a691 Merge pull request #2387 from akhilmhdh/docs/fluent-bit-log-stream
feat: added doc for audit log stream via fluentbit
2024-09-08 15:08:46 -04:00
Maidul Islam
c8dc29d59b revise audit log stream PR 2024-09-08 15:04:30 -04:00
=
3707b75349 feat: added doc for audit log stream via fluentbit 2024-09-08 20:33:47 +05:30
Sheen
f09e18a706 Merge pull request #2383 from Infisical/fix/resolve-cert-invalid-issue
fix: resolve cert invalid issue due to invalid root EKU
2024-09-07 01:09:24 +08:00
Sheen Capadngan
5d9a43a3fd fix: resolve cert invalid issue 2024-09-07 00:42:55 +08:00
Maidul Islam
8d66272ab2 Merge pull request #2366 from ThallesP/patch-1
docs: add mention of SITE_URL as being required
2024-09-05 16:06:49 -04:00
Maidul Islam
0e44e630cb Merge pull request #2377 from Infisical/daniel/refactor-circleci-integration
fix(integrations/circle-ci): Refactored Circle CI integration
2024-09-05 16:04:04 -04:00
Maidul Islam
49c4929c9c Update azure-key-vault.mdx 2024-09-05 15:13:42 -04:00
Daniel Hougaard
da561e37c5 Fix: Backwards compatibility and UI fixes 2024-09-05 21:43:10 +04:00
Maidul Islam
ebc584d36f Merge pull request #2379 from Infisical/fix/client-secret-patch
Update identity-ua-client-secret-dal.ts
2024-09-05 11:02:35 -04:00
Akhil Mohan
656d979d7d Update identity-ua-client-secret-dal.ts 2024-09-05 20:29:18 +05:30
Daniel Hougaard
a29fb613b9 Requested changes 2024-09-05 18:48:20 +04:00
Maidul Islam
5382f3de2d Merge pull request #2378 from Infisical/vmatsiiako-patch-elasticsearch-1
Elasticsearch is one word
2024-09-05 09:11:18 -04:00
Vlad Matsiiako
b2b858f7e8 Elasticsearch is one word 2024-09-05 09:07:23 -04:00
Daniel Hougaard
8f3d328b9a Update integration-sync-secret.ts 2024-09-05 13:38:31 +04:00
Daniel Hougaard
b7d683ee1b fix(integrations/circle-ci): Refactored Circle CI integration
The integration seemingly never worked in the first place due to inpropper project slugs. This PR resolves it.
2024-09-05 13:30:20 +04:00
Thalles Passos
9bd6ec19c4 revert "docs: add mention of SITE_URL as being required" 2024-09-04 18:04:25 -03:00
Thalles Passos
03fd0a1eb9 chore: add site url as required in kubernetes helm deployment 2024-09-04 18:03:18 -03:00
Thalles Passos
97023e7714 chore: add SITE_URL as required in docker installation 2024-09-04 17:58:42 -03:00
Thalles Passos
1d23ed0680 chore: add site url as required in envars docs 2024-09-04 17:56:38 -03:00
Daniel Hougaard
302e068c74 Merge pull request #2376 from Infisical/daniel/info-notif-for-secret-changes
fix(ui): show info notification when secret change is pending review
2024-09-04 20:09:58 +04:00
Daniel Hougaard
95b92caff3 Merge pull request #2375 from Infisical/daniel/fix-access-policy-creation
fix(access-requests): policy creation and edits
2024-09-04 20:00:04 +04:00
Daniel Hougaard
5d894b6d43 fix(ui): info notification when secret change is pending review 2024-09-04 19:57:32 +04:00
Daniel Hougaard
dab3e2efad fix(access-requests): policy creation and edits 2024-09-04 19:46:44 +04:00
Akhil Mohan
04cbbccd25 Merge pull request #2374 from Infisical/revert-2362-bugfix/incorrect-alignment-of-logo-on-login-page
Revert "FIX : padding-and-alignment-login-page"
2024-09-04 19:16:08 +05:30
Akhil Mohan
7f48e9d62e Revert "FIX : padding-and-alignment-login-page" 2024-09-04 19:12:58 +05:30
Daniel Hougaard
8a0018eff2 Merge pull request #2373 from Infisical/daniel/elastisearch-dynamic-secrets
feat(dynamic-secrets): elastic search support
2024-09-04 15:23:23 +04:00
Akhil Mohan
e6a920caa3 Merge pull request #2362 from mukulpadwal/bugfix/incorrect-alignment-of-logo-on-login-page
FIX : padding-and-alignment-login-page
2024-09-04 16:15:36 +05:30
Daniel Hougaard
11411ca4eb Requested changes 2024-09-04 13:47:35 +04:00
Daniel Hougaard
b7c79fa45b Requested changes 2024-09-04 13:47:35 +04:00
Daniel Hougaard
18951b99de Further doc fixes 2024-09-04 13:47:17 +04:00
Daniel Hougaard
bd05c440c3 Update elastic-search.ts 2024-09-04 13:47:17 +04:00
Daniel Hougaard
9ca5013a59 Update mint.json 2024-09-04 13:47:17 +04:00
Daniel Hougaard
b65b8bc362 docs(dynamic-secrets): Elastic Search documentation 2024-09-04 13:47:17 +04:00
Daniel Hougaard
f494c182ff Update aws-elasticache.mdx 2024-09-04 13:47:17 +04:00
Daniel Hougaard
2fae822e1f Fix docs for AWS ElastiCache 2024-09-04 13:47:17 +04:00
Daniel Hougaard
5df140cbd5 feat(dynamic-secrets): ElasticSearch support 2024-09-04 13:47:17 +04:00
Daniel Hougaard
d93cbb023d Update redis.ts 2024-09-04 13:47:17 +04:00
Daniel Hougaard
9056d1be0c feat(dynamic-secrets): ElasticSearch support 2024-09-04 13:47:17 +04:00
Daniel Hougaard
5f503949eb Installed elasticsearch SDK 2024-09-04 13:47:16 +04:00
Daniel Hougaard
9cf917de07 Merge pull request #2360 from Infisical/daniel/redirect-node-docs
feat(integrations): Add visibility support to Github Integration
2024-09-04 10:32:13 +04:00
Maidul Islam
ce7bb82f02 Merge pull request #2313 from akhilmhdh/feat/test-import
Feat/test import
2024-09-03 09:33:26 -04:00
Maidul Islam
7cd092c0cf Merge pull request #2368 from akhilmhdh/fix/audit-log-loop
Audit log queue looping
2024-09-03 08:32:04 -04:00
=
cbfb9af0b9 feat: moved log points inside each function respectively 2024-09-03 17:59:32 +05:30
=
ef236106b4 feat: added log points for resoruce clean up tasks 2024-09-03 17:37:14 +05:30
=
773a338397 fix: resolved looping in audit log resource queue 2024-09-03 17:33:38 +05:30
=
afb5820113 feat: added 1-N sink import pattern testing and fixed padding issue 2024-09-03 15:02:49 +05:30
Maidul Islam
5acc0fc243 Update build-staging-and-deploy-aws.yml 2024-09-02 23:56:24 -04:00
Maidul Islam
c56469ecdb Run integration tests build building gamma 2024-09-02 23:55:05 -04:00
Daniel Hougaard
c59a53180c Update integrations-github-scope-org.png 2024-09-03 04:40:59 +04:00
Daniel Hougaard
f56d265e62 Revert "Docs: Redirect to new SDK"
This reverts commit 56dce67378b3601aec9f45eee0c52e50c1a7e36a.
2024-09-03 04:40:59 +04:00
Daniel Hougaard
cc0ff98d4f chore: cleaned up integrations page 2024-09-03 04:40:59 +04:00
Daniel Hougaard
4a14c3efd2 feat(integrations): visibility support for github integration 2024-09-03 04:40:59 +04:00
Daniel Hougaard
b2d2297914 Fix: Document formatting & changed tooltipText prop to ReactNode type 2024-09-03 04:40:59 +04:00
Daniel Hougaard
836bb6d835 feat(integrations): visibility support for github integration 2024-09-03 04:40:19 +04:00
Daniel Hougaard
177eb2afee docs(github-integration): Updated documentation for github integration 2024-09-03 04:40:19 +04:00
Daniel Hougaard
594df18611 Docs: Redirect to new SDK 2024-09-03 04:40:19 +04:00
Maidul Islam
3bcb8bf6fc Merge pull request #2364 from akhilmhdh/fix/scim-rfc
Resolved scim failing due to missing rfc cases
2024-09-02 18:59:20 -04:00
Thalles Passos
23c362f9cd docs: add mention of SITE_URL as being required 2024-09-02 12:54:00 -03:00
Maidul Islam
a74c37c18b Merge pull request #2350 from akhilmhdh/dynamic-secret/atlas
MongoDB atlas dynamic secret
2024-09-02 10:39:34 -04:00
=
3ece81d663 docs: improved test as commented 2024-09-02 14:43:11 +05:30
=
f6d87ebf32 feat: changed text to advanced as review comment 2024-09-02 14:36:32 +05:30
=
23483ab7e1 feat: removed non rfc related groups in user scim resource 2024-09-02 13:55:56 +05:30
=
fe31d44d22 feat: made scim user default permission as no access in org 2024-09-02 13:50:55 +05:30
=
58bab4d163 feat: resolved some more missing corner case in scim 2024-09-02 13:50:55 +05:30
=
8f48a64fd6 feat: finished fixing scim group 2024-09-02 13:50:55 +05:30
=
929dc059c3 feat: updated scim user endpoint 2024-09-02 13:50:55 +05:30
mukulpadwal
45e471b16a FIX : padding-and-alignment-login-page 2024-08-31 16:25:54 +05:30
Maidul Islam
7c540b6be8 Merge pull request #2244 from LemmyMwaura/password-protect-secret-share
feat: password protect secret share
2024-08-30 13:43:24 -04:00
=
7dbe8dd3c9 feat: patched lock file 2024-08-30 10:56:28 +05:30
=
0dec602729 feat: changed all licence type to license 2024-08-30 10:52:46 +05:30
=
66ded779fc feat: added secret version test with secret import 2024-08-30 10:52:46 +05:30
=
01d24291f2 feat: resolved type error 2024-08-30 10:52:46 +05:30
=
55b36b033e feat: changed expand secret factory to iterative solution 2024-08-30 10:52:46 +05:30
=
8f461bf50c feat: added test for checking secret reference expansion 2024-08-30 10:52:46 +05:30
=
1847491cb3 feat: implemented new secret reference strategy 2024-08-30 10:52:46 +05:30
=
541c7b63cd feat: added test for checkings secrets from import via replication and non replicaiton 2024-08-30 10:52:45 +05:30
=
7e5e177680 feat: vitest mocking by alias for license fns 2024-08-30 10:52:45 +05:30
=
40f552e4f1 feat: fixed typo in license function file name 2024-08-30 10:52:45 +05:30
=
ecb54ee3b3 feat: resolved migration down failing for secret approval policy change 2024-08-30 10:52:45 +05:30
Maidul Islam
b975996158 Merge pull request #2330 from Infisical/doc/made-ecs-with-agent-doc-use-aws-native
doc: updated ecs-with-agent documentation to use AWS native auth
2024-08-29 14:25:45 -04:00
Maidul Islam
122f789cdf Merge pull request #2329 from Infisical/feat/raw-agent-template
feat: added raw template for agent
2024-08-29 14:24:50 -04:00
Maidul Islam
c9911aa841 Merge pull request #2335 from Infisical/vmatsiiako-patch-handbook-1
Update onboarding.mdx
2024-08-29 14:22:30 -04:00
Maidul Islam
32cd0d8af8 Merge pull request #2352 from Infisical/revert-2341-daniel/cli-run-watch-mode
Revert "feat(cli): `run` watch mode"
2024-08-29 12:46:39 -04:00
Maidul Islam
585f0d9f1b Revert "feat(cli): run watch mode" 2024-08-29 12:44:24 -04:00
Daniel Hougaard
d0292aa139 Merge pull request #2341 from Infisical/daniel/cli-run-watch-mode
feat(cli): `run` watch mode
2024-08-29 17:51:41 +04:00
Daniel Hougaard
4e9be8ca3c Changes 2024-08-29 17:38:00 +04:00
=
ad49e9eaf1 docs: updated doc for mongo atlas dynamic secret 2024-08-29 14:52:40 +05:30
=
fed60f7c03 feat: resolved lint fix after rebase 2024-08-29 13:28:45 +05:30
=
1bc0e3087a feat: completed atlas dynamic secret logic for ui 2024-08-29 13:26:15 +05:30
=
80a4f838a1 feat: completed mongo atlas dynamic secret backend logic 2024-08-29 13:22:25 +05:30
Sheen
d31ec44f50 Merge pull request #2345 from Infisical/misc/finalize-certificate-template-and-est
finalized certificate template and EST
2024-08-29 12:47:13 +08:00
Maidul Islam
d0caef37ce Merge pull request #2349 from Infisical/mzidul-wjdhbwhufhjwebf
Add tool tip for k8s auth
2024-08-28 17:11:06 -04:00
Maidul Islam
2d26febe58 a to an 2024-08-28 17:09:47 -04:00
Maidul Islam
c23ad8ebf2 improve tooltip 2024-08-28 17:04:56 -04:00
Maidul Islam
bad068ef19 add tool tip for k8s auth 2024-08-28 16:59:14 -04:00
Daniel Hougaard
53430608a8 Merge pull request #2348 from Infisical/daniel/env-transform-trailing-slashes
Fix: Always remove trailing slashes from SITE_URL
2024-08-28 22:52:03 +04:00
Daniel Hougaard
b9071ab2b3 Fix: Always remove trailing slashes from SITE_URL 2024-08-28 22:45:08 +04:00
Sheen Capadngan
a556c02df6 misc: migrated est to ee and added license checks 2024-08-29 02:20:55 +08:00
Daniel Hougaard
bfab270d68 Merge pull request #2347 from Infisical/daniel/fix-secret-change-emails
Fix: Removed protocol parsing on secret change emails
2024-08-28 22:16:28 +04:00
Daniel Hougaard
8ea6a1f3d5 Fix: Removed protocol parsing 2024-08-28 22:07:31 +04:00
Daniel Hougaard
3c39bf6a0f Add watch interval 2024-08-28 21:11:09 +04:00
Daniel Hougaard
828644799f Merge pull request #2319 from Infisical/daniel/redis-dynamic-secrets
Feat: Redis support for dynamic secrets
2024-08-28 18:06:57 +04:00
Daniel Hougaard
411e67ae41 Finally resolved package-lock 2024-08-28 18:01:31 +04:00
Daniel Hougaard
4914bc4b5a Fix: Package json bugged generation 2024-08-28 18:00:05 +04:00
Daniel Hougaard
d7050a1947 Update package-lock.json 2024-08-28 17:58:32 +04:00
Daniel Hougaard
3c59422511 Fixed package 2024-08-28 17:57:31 +04:00
Daniel Hougaard
c81204e6d5 Test 2024-08-28 17:57:31 +04:00
Daniel Hougaard
880f39519f Update aws-elasticache.mdx 2024-08-28 17:57:31 +04:00
Daniel Hougaard
8646f6c50b Requested changes 2024-08-28 17:57:30 +04:00
Daniel Hougaard
437a9e6ccb AWS elasticache 2024-08-28 17:57:30 +04:00
Daniel Hougaard
b54139bd37 Fix 2024-08-28 17:57:30 +04:00
Daniel Hougaard
8a6a36ac54 Update package-lock.json 2024-08-28 17:57:20 +04:00
Daniel Hougaard
c6eb973da0 Uninstalled unused dependencies 2024-08-28 17:57:19 +04:00
Daniel Hougaard
21750a8c20 Fix: Refactored aws elasticache to separate provider 2024-08-28 17:57:19 +04:00
Daniel Hougaard
a598665b2f Docs: ElastiCache Docs 2024-08-28 17:57:19 +04:00
Daniel Hougaard
56bbf502a2 Update redis.ts 2024-08-28 17:57:19 +04:00
Daniel Hougaard
9975f7d83f Edition fixes 2024-08-28 17:57:19 +04:00
Daniel Hougaard
7ad366b363 Update licence-fns.ts 2024-08-28 17:57:19 +04:00
Daniel Hougaard
cca4d68d94 Fix: AWS ElastiCache support 2024-08-28 17:57:19 +04:00
Daniel Hougaard
b82b94db54 Docs: Redis Dynamic secrets docs 2024-08-28 17:55:04 +04:00
Daniel Hougaard
de9cb265e0 Feat: Redis support for dynamic secrets 2024-08-28 17:55:04 +04:00
Sheen Capadngan
5611b9aba1 misc: added reference to secret template functions 2024-08-28 15:53:36 +08:00
Sheen Capadngan
53075d503a misc: added note to secret template docs 2024-08-28 15:48:25 +08:00
Sheen Capadngan
e47cfa262a misc: added cloud est user guide 2024-08-28 13:59:47 +08:00
Sheen Capadngan
0ab7a4e713 misc: added transaction for cert template create and update 2024-08-28 13:45:25 +08:00
Daniel Hougaard
5138d588db Update cli.go 2024-08-28 05:35:03 +04:00
Daniel Hougaard
7e2d093e29 Docs: watch mode 2024-08-28 05:34:21 +04:00
Daniel Hougaard
2d780e0566 Feat: watch mode for run command 2024-08-28 05:22:27 +04:00
Maidul Islam
7ac4ad3194 Merge pull request #2344 from Infisical/maidul-ddqdqwdqwd3
Update health check
2024-08-27 20:09:51 -04:00
Maidul Islam
3ab6eb62c8 update health check 2024-08-27 20:03:36 -04:00
Daniel Hougaard
8eb234a12f Update run.go 2024-08-27 21:58:53 +04:00
Daniel Hougaard
85590af99e Fix: Removed more duplicate code and started using process groups to fix memory leak 2024-08-27 21:44:32 +04:00
Daniel Hougaard
5c7cec0c81 Update run.go 2024-08-27 20:11:27 +04:00
Daniel Hougaard
68f768749b Update run.go 2024-08-27 20:10:50 +04:00
Daniel Hougaard
2c7e342b18 Update run.go 2024-08-27 20:10:11 +04:00
Daniel Hougaard
632900e516 Update run.go 2024-08-27 20:10:00 +04:00
Daniel Hougaard
5fd975b1d7 Fix: Console error on manual cancel when not using hot reload 2024-08-27 20:09:40 +04:00
Daniel Hougaard
d45ac66064 Fix: Match test cases 2024-08-27 20:03:01 +04:00
Daniel Hougaard
47cba8ec3c Update test-TestUniversalAuth_SecretsGetWrongEnvironment 2024-08-27 19:48:20 +04:00
Daniel Hougaard
d4aab66da2 Update test-TestUniversalAuth_SecretsGetWrongEnvironment 2024-08-27 19:45:00 +04:00
Daniel Hougaard
0dc4c92c89 Feat: --watch flag for watching for secret changes 2024-08-27 19:37:11 +04:00
Daniel Hougaard
f49c963367 Settings for hot reloading 2024-08-27 19:36:38 +04:00
Daniel Hougaard
fe11b8e57e Function for locally generating ETag 2024-08-27 19:36:02 +04:00
Sheen
79680b6a73 Merge pull request #2340 from Infisical/misc/added-timeout-for-hijacked-est-connection
misc: added timeout for est connection
2024-08-27 23:31:07 +08:00
Sheen Capadngan
58838c541f misc: added timeout for est connection 2024-08-27 23:26:56 +08:00
Sheen
03cc71cfed Merge pull request #2284 from Infisical/feature/est-simpleenroll
Certificate EST protocol (simpleenroll, simplereenroll, cacerts)
2024-08-27 13:42:58 +08:00
Maidul Islam
02529106c9 Merge pull request #2336 from akhilmhdh/fix/scim-error
fix: resolved scim group update failing
2024-08-26 16:34:27 -04:00
Vlad Matsiiako
0401f55bc3 Update onboarding.mdx 2024-08-26 13:28:37 -07:00
Vlad Matsiiako
403e0d2d9d Update onboarding.mdx 2024-08-26 13:26:21 -07:00
=
d939ff289d fix: resolved scim group update failing 2024-08-27 01:50:26 +05:30
Daniel Hougaard
d1816c3051 Merge pull request #2334 from Infisical/daniel/azure-devops-docs
Docs: Azure DevOps Integration
2024-08-26 23:49:23 +04:00
Daniel Hougaard
cb350788c0 Update create.tsx 2024-08-26 23:21:56 +04:00
Daniel Hougaard
cd58768d6f Updated images 2024-08-26 23:20:51 +04:00
Daniel Hougaard
dcd6f4d55d Fix: Updated Azure DevOps integration styling 2024-08-26 23:12:00 +04:00
Daniel Hougaard
3c828614b8 Fix: Azure DevOps Label naming typos 2024-08-26 22:44:11 +04:00
Daniel Hougaard
09e7988596 Docs: Azure DevOps Integration 2024-08-26 22:43:49 +04:00
Sheen Capadngan
f40df19334 misc: finalized est config schema 2024-08-27 02:15:01 +08:00
Sheen Capadngan
76c9d3488b Merge remote-tracking branch 'origin/main' into feature/est-simpleenroll 2024-08-27 02:13:59 +08:00
Sheen Capadngan
0809da33e0 misc: improved docs and added support for curl clients 2024-08-27 02:05:35 +08:00
Daniel Hougaard
b528eec4bb Merge pull request #2333 from Infisical/daniel/secret-change-emails
Feat: Email notification on secret change requests
2024-08-26 21:50:30 +04:00
Daniel Hougaard
5179103680 Update SecretApprovalRequest.tsx 2024-08-26 21:46:05 +04:00
Daniel Hougaard
25a9e5f58a Update SecretApprovalRequest.tsx 2024-08-26 21:42:47 +04:00
Daniel Hougaard
8ddfe7b6e9 Update secret-approval-request-fns.ts 2024-08-26 20:53:43 +04:00
Daniel Hougaard
c23f21d57a Update SecretApprovalRequest.tsx 2024-08-26 20:21:05 +04:00
Daniel Hougaard
1242a43d98 Feat: Open approval with ID in URL 2024-08-26 20:04:06 +04:00
Daniel Hougaard
1655ca27d1 Fix: Creation of secret approval policies 2024-08-26 20:02:58 +04:00
Daniel Hougaard
2bcead03b0 Feat: Send secret change request emails to approvers 2024-08-26 19:55:04 +04:00
Daniel Hougaard
41ab1972ce Feat: Find project and include org dal 2024-08-26 19:54:48 +04:00
Daniel Hougaard
b00fff6922 Update index.ts 2024-08-26 19:54:15 +04:00
Daniel Hougaard
97b01ca5f8 Feat: Send secret change request emails to approvers 2024-08-26 19:54:01 +04:00
Daniel Hougaard
c2bd6f5ef3 Feat: Send secret change request emails to approvers 2024-08-26 19:53:49 +04:00
Daniel Hougaard
18efc9a6de Include more user details 2024-08-26 19:53:17 +04:00
Daniel Hougaard
436ccb25fb Merge pull request #2331 from Infisical/daniel/presist-selfhosting-domains
Feat: Persistent self-hosting domains on `infisical login`
2024-08-26 18:04:25 +04:00
Daniel Hougaard
8f08a352dd Merge pull request #2310 from Infisical/daniel/azure-devops-integration
Feat: Azure DevOps Integration
2024-08-26 18:04:04 +04:00
Sheen Capadngan
00f86cfd00 misc: addressed review comments 2024-08-26 21:10:29 +08:00
Daniel Hougaard
3944aafb11 Use slices 2024-08-26 15:18:45 +04:00
Daniel Hougaard
1f24d02c5e Fix: Do not save duplicate domains 2024-08-26 15:08:51 +04:00
Tuan Dang
f560534493 Replace custom pkcs7 fns with module 2024-08-25 20:21:53 -07:00
Daniel Hougaard
7a2f0214f3 Feat: Persist self-hosting domains on infisical login 2024-08-24 13:18:03 +04:00
Maidul Islam
e73d3f87f3 small nit 2024-08-23 14:29:29 -04:00
Sheen Capadngan
b53607f8e4 doc: updated ecs with agent doc to use aws auth 2024-08-24 01:51:39 +08:00
Sheen Capadngan
8f79d3210a feat: added raw template for agent 2024-08-24 01:48:39 +08:00
Tuan Dang
1317266415 Merge remote-tracking branch 'origin' into feature/est-simpleenroll 2024-08-22 14:54:34 -07:00
Sheen Capadngan
288d7e88ae misc: made SSL header key configurable via env 2024-08-23 01:38:12 +08:00
Sheen Capadngan
f88389bf9e misc: added general format 2024-08-22 21:05:34 +08:00
Sheen Capadngan
2e88c5e2c5 misc: improved url examples in est doc 2024-08-22 21:02:38 +08:00
Sheen Capadngan
73f3b8173e doc: added guide for EST usage' 2024-08-22 20:44:21 +08:00
Sheen Capadngan
aa5b88ff04 misc: removed enrollment options from CA page 2024-08-22 15:40:36 +08:00
Sheen Capadngan
b7caff88cf feat: finished up EST cacerts 2024-08-22 15:39:53 +08:00
Sheen Capadngan
760a1e917a feat: added simplereenroll 2024-08-20 23:56:27 +08:00
Sheen Capadngan
2d7ff66246 Merge branch 'feature/est-simpleenroll' of https://github.com/Infisical/infisical into feature/est-simpleenroll 2024-08-20 15:31:58 +08:00
Sheen Capadngan
179497e830 misc: moved est logic to service 2024-08-20 15:31:10 +08:00
Tuan Dang
4c08c80e5b Merge remote-tracking branch 'origin' into feature/est-simpleenroll 2024-08-19 14:53:04 -07:00
Sheen Capadngan
7d6af64904 misc: added proxy header for amazon mtls client cert 2024-08-20 01:53:47 +08:00
Sheen Capadngan
16519f9486 feat: added reading SANs from CSR 2024-08-20 01:39:40 +08:00
Sheen Capadngan
bb27d38a12 misc: ui form adjustments 2024-08-19 21:39:00 +08:00
Sheen Capadngan
5b26928751 misc: added audit logs 2024-08-19 20:25:07 +08:00
Sheen Capadngan
f425e7e48f misc: addressed alignment issue 2024-08-19 19:50:09 +08:00
Sheen Capadngan
4601f46afb misc: finalized variable naming 2024-08-19 19:33:46 +08:00
Sheen Capadngan
692bdc060c misc: updated est configuration to be binded to certificate template 2024-08-19 19:26:20 +08:00
Sheen Capadngan
3a4f8c2e54 Merge branch 'feature/certificate-template' into feature/est-simpleenroll 2024-08-19 17:04:22 +08:00
Sheen Capadngan
146c4284a2 feat: integrated to est routes 2024-08-14 20:52:21 +08:00
Sheen Capadngan
5ae33b9f3b misc: minor UI updates 2024-08-14 01:10:25 +08:00
Sheen Capadngan
1f38b92ec6 feat: finished up integration for est config management 2024-08-14 01:00:31 +08:00
Sheen Capadngan
f2a49a79f0 feat: initial simpleenroll setup (mvp) 2024-08-13 23:22:47 +08:00
=
3ddb4cd27a feat: simplified ui for password based secret sharing 2024-08-10 22:21:17 +05:30
=
a5555c3816 feat: simplified endpoints to support password based secret sharing 2024-08-10 22:19:42 +05:30
lemmyMwaura
8479c406a5 fix: fix type assersion error 2024-08-08 10:06:55 +03:00
lemmyMwaura
8e0b4254b1 refactor: fix lint issues and refactor code 2024-08-08 09:56:18 +03:00
lemmyMwaura
069651bdb4 fix: fix lint errors 2024-08-07 23:26:24 +03:00
lemmyMwaura
9061ec2dff fix(lint): fix type errors 2024-08-07 22:59:50 +03:00
lemmyMwaura
b0a5023723 feat: check if secret is expired before checking if secret has password 2024-08-07 20:55:37 +03:00
lemmyMwaura
69fe5bf71d feat: only update view count when we validate the password if it's set 2024-08-07 16:52:11 +03:00
lemmyMwaura
f12d4d80c6 feat: address changes on the client 2024-08-07 16:13:29 +03:00
lemmyMwaura
56f2a3afa4 feat: only fetch secret if password wasn't set on initial load 2024-08-07 16:06:37 +03:00
lemmyMwaura
406da1b5f0 refactor: convert usequery hook to normal fetch fn (no need for caching) 2024-08-07 08:27:17 +03:00
lemmyMwaura
da45e132a3 Merge branch 'main' of github.com:Infisical/infisical into password-protect-secret-share 2024-08-06 19:49:25 +03:00
lemmyMwaura
fb719a9383 fix(lint): fix some lint issues 2024-08-06 19:25:04 +03:00
lemmyMwaura
3c64359597 feat: handle error logs and validate password 2024-08-06 18:36:21 +03:00
lemmyMwaura
e420973dd2 feat: hashpassword and add validation endpoint 2024-08-06 17:01:13 +03:00
lemmyMwaura
15cc157c5f fix(lint): make password optional 2024-08-06 15:32:48 +03:00
lemmyMwaura
ad89ffe94d feat: show secret if no password was set 2024-08-06 14:42:01 +03:00
lemmyMwaura
4de1713a18 fix: remove error logs 2024-08-06 14:28:02 +03:00
lemmyMwaura
1917e0fdb7 feat: validate via password before showing secret 2024-08-06 14:13:03 +03:00
lemmyMwaura
4b07234997 feat: update frontend queries to retrieve password 2024-08-06 14:08:40 +03:00
lemmyMwaura
6a402950c3 chore: add check migration status cmd scripts 2024-08-06 12:59:46 +03:00
lemmyMwaura
63333159ca feat: fetch password when fetching secrets 2024-08-06 12:58:53 +03:00
lemmyMwaura
ce4ba24ef2 feat: create secret with password 2024-08-06 12:58:27 +03:00
lemmyMwaura
f606e31b98 feat: apply table migrations (add password field) 2024-08-06 12:28:03 +03:00
lemmyMwaura
ecdbb3eb53 feat: update type resolvers to include password 2024-08-06 12:27:16 +03:00
lemmyMwaura
0321ec32fb feat: add password input 2024-08-06 12:26:23 +03:00
213 changed files with 11845 additions and 2188 deletions

View File

@@ -70,3 +70,5 @@ NEXT_PUBLIC_CAPTCHA_SITE_KEY=
PLAIN_API_KEY=
PLAIN_WISH_LABEL_IDS=
SSL_CLIENT_CERTIFICATE_HEADER_KEY=

View File

@@ -6,9 +6,15 @@ permissions:
contents: read
jobs:
infisical-tests:
name: Run tests before deployment
# https://docs.github.com/en/actions/using-workflows/reusing-workflows#overview
uses: ./.github/workflows/run-backend-tests.yml
infisical-image:
name: Build backend image
runs-on: ubuntu-latest
needs: [infisical-tests]
steps:
- name: ☁️ Checkout source
uses: actions/checkout@v3

View File

@@ -0,0 +1,35 @@
import { seedData1 } from "@app/db/seed-data";
const createPolicy = async (dto: { name: string; secretPath: string; approvers: string[]; approvals: number }) => {
const res = await testServer.inject({
method: "POST",
url: `/api/v1/secret-approvals`,
headers: {
authorization: `Bearer ${jwtAuthToken}`
},
body: {
workspaceId: seedData1.project.id,
environment: seedData1.environment.slug,
name: dto.name,
secretPath: dto.secretPath,
approvers: dto.approvers,
approvals: dto.approvals
}
});
expect(res.statusCode).toBe(200);
return res.json().approval;
};
describe("Secret approval policy router", async () => {
test("Create policy", async () => {
const policy = await createPolicy({
secretPath: "/",
approvals: 1,
approvers: [seedData1.id],
name: "test-policy"
});
expect(policy.name).toBe("test-policy");
});
});

View File

@@ -1,73 +1,61 @@
import { createFolder, deleteFolder } from "e2e-test/testUtils/folders";
import { createSecretImport, deleteSecretImport } from "e2e-test/testUtils/secret-imports";
import { createSecretV2, deleteSecretV2, getSecretByNameV2, getSecretsV2 } from "e2e-test/testUtils/secrets";
import { seedData1 } from "@app/db/seed-data";
const createSecretImport = async (importPath: string, importEnv: string) => {
const res = await testServer.inject({
method: "POST",
url: `/api/v1/secret-imports`,
headers: {
authorization: `Bearer ${jwtAuthToken}`
},
body: {
workspaceId: seedData1.project.id,
environment: seedData1.environment.slug,
path: "/",
import: {
environment: importEnv,
path: importPath
}
}
});
expect(res.statusCode).toBe(200);
const payload = JSON.parse(res.payload);
expect(payload).toHaveProperty("secretImport");
return payload.secretImport;
};
const deleteSecretImport = async (id: string) => {
const res = await testServer.inject({
method: "DELETE",
url: `/api/v1/secret-imports/${id}`,
headers: {
authorization: `Bearer ${jwtAuthToken}`
},
body: {
workspaceId: seedData1.project.id,
environment: seedData1.environment.slug,
path: "/"
}
});
expect(res.statusCode).toBe(200);
const payload = JSON.parse(res.payload);
expect(payload).toHaveProperty("secretImport");
return payload.secretImport;
};
describe("Secret Import Router", async () => {
test.each([
{ importEnv: "prod", importPath: "/" }, // one in root
{ importEnv: "staging", importPath: "/" } // then create a deep one creating intermediate ones
])("Create secret import $importEnv with path $importPath", async ({ importPath, importEnv }) => {
// check for default environments
const payload = await createSecretImport(importPath, importEnv);
const payload = await createSecretImport({
authToken: jwtAuthToken,
secretPath: "/",
environmentSlug: seedData1.environment.slug,
workspaceId: seedData1.project.id,
importPath,
importEnv
});
expect(payload).toEqual(
expect.objectContaining({
id: expect.any(String),
importPath: expect.any(String),
importPath,
importEnv: expect.objectContaining({
name: expect.any(String),
slug: expect.any(String),
slug: importEnv,
id: expect.any(String)
})
})
);
await deleteSecretImport(payload.id);
await deleteSecretImport({
id: payload.id,
workspaceId: seedData1.project.id,
environmentSlug: seedData1.environment.slug,
secretPath: "/",
authToken: jwtAuthToken
});
});
test("Get secret imports", async () => {
const createdImport1 = await createSecretImport("/", "prod");
const createdImport2 = await createSecretImport("/", "staging");
const createdImport1 = await createSecretImport({
authToken: jwtAuthToken,
secretPath: "/",
environmentSlug: seedData1.environment.slug,
workspaceId: seedData1.project.id,
importPath: "/",
importEnv: "prod"
});
const createdImport2 = await createSecretImport({
authToken: jwtAuthToken,
secretPath: "/",
environmentSlug: seedData1.environment.slug,
workspaceId: seedData1.project.id,
importPath: "/",
importEnv: "staging"
});
const res = await testServer.inject({
method: "GET",
url: `/api/v1/secret-imports`,
@@ -89,25 +77,60 @@ describe("Secret Import Router", async () => {
expect.arrayContaining([
expect.objectContaining({
id: expect.any(String),
importPath: expect.any(String),
importPath: "/",
importEnv: expect.objectContaining({
name: expect.any(String),
slug: expect.any(String),
slug: "prod",
id: expect.any(String)
})
}),
expect.objectContaining({
id: expect.any(String),
importPath: "/",
importEnv: expect.objectContaining({
name: expect.any(String),
slug: "staging",
id: expect.any(String)
})
})
])
);
await deleteSecretImport(createdImport1.id);
await deleteSecretImport(createdImport2.id);
await deleteSecretImport({
id: createdImport1.id,
workspaceId: seedData1.project.id,
environmentSlug: seedData1.environment.slug,
secretPath: "/",
authToken: jwtAuthToken
});
await deleteSecretImport({
id: createdImport2.id,
workspaceId: seedData1.project.id,
environmentSlug: seedData1.environment.slug,
secretPath: "/",
authToken: jwtAuthToken
});
});
test("Update secret import position", async () => {
const prodImportDetails = { path: "/", envSlug: "prod" };
const stagingImportDetails = { path: "/", envSlug: "staging" };
const createdImport1 = await createSecretImport(prodImportDetails.path, prodImportDetails.envSlug);
const createdImport2 = await createSecretImport(stagingImportDetails.path, stagingImportDetails.envSlug);
const createdImport1 = await createSecretImport({
authToken: jwtAuthToken,
secretPath: "/",
environmentSlug: seedData1.environment.slug,
workspaceId: seedData1.project.id,
importPath: prodImportDetails.path,
importEnv: prodImportDetails.envSlug
});
const createdImport2 = await createSecretImport({
authToken: jwtAuthToken,
secretPath: "/",
environmentSlug: seedData1.environment.slug,
workspaceId: seedData1.project.id,
importPath: stagingImportDetails.path,
importEnv: stagingImportDetails.envSlug
});
const updateImportRes = await testServer.inject({
method: "PATCH",
@@ -161,22 +184,55 @@ describe("Secret Import Router", async () => {
expect(secretImportList.secretImports[1].id).toEqual(createdImport1.id);
expect(secretImportList.secretImports[0].id).toEqual(createdImport2.id);
await deleteSecretImport(createdImport1.id);
await deleteSecretImport(createdImport2.id);
await deleteSecretImport({
id: createdImport1.id,
workspaceId: seedData1.project.id,
environmentSlug: seedData1.environment.slug,
secretPath: "/",
authToken: jwtAuthToken
});
await deleteSecretImport({
id: createdImport2.id,
workspaceId: seedData1.project.id,
environmentSlug: seedData1.environment.slug,
secretPath: "/",
authToken: jwtAuthToken
});
});
test("Delete secret import position", async () => {
const createdImport1 = await createSecretImport("/", "prod");
const createdImport2 = await createSecretImport("/", "staging");
const deletedImport = await deleteSecretImport(createdImport1.id);
const createdImport1 = await createSecretImport({
authToken: jwtAuthToken,
secretPath: "/",
environmentSlug: seedData1.environment.slug,
workspaceId: seedData1.project.id,
importPath: "/",
importEnv: "prod"
});
const createdImport2 = await createSecretImport({
authToken: jwtAuthToken,
secretPath: "/",
environmentSlug: seedData1.environment.slug,
workspaceId: seedData1.project.id,
importPath: "/",
importEnv: "staging"
});
const deletedImport = await deleteSecretImport({
id: createdImport1.id,
workspaceId: seedData1.project.id,
environmentSlug: seedData1.environment.slug,
secretPath: "/",
authToken: jwtAuthToken
});
// check for default environments
expect(deletedImport).toEqual(
expect.objectContaining({
id: expect.any(String),
importPath: expect.any(String),
importPath: "/",
importEnv: expect.objectContaining({
name: expect.any(String),
slug: expect.any(String),
slug: "prod",
id: expect.any(String)
})
})
@@ -201,6 +257,552 @@ describe("Secret Import Router", async () => {
expect(secretImportList.secretImports.length).toEqual(1);
expect(secretImportList.secretImports[0].position).toEqual(1);
await deleteSecretImport(createdImport2.id);
await deleteSecretImport({
id: createdImport2.id,
workspaceId: seedData1.project.id,
environmentSlug: seedData1.environment.slug,
secretPath: "/",
authToken: jwtAuthToken
});
});
});
// dev <- stage <- prod
describe.each([{ path: "/" }, { path: "/deep" }])(
"Secret import waterfall pattern testing - %path",
({ path: testSuitePath }) => {
beforeAll(async () => {
let prodFolder: { id: string };
let stagingFolder: { id: string };
let devFolder: { id: string };
if (testSuitePath !== "/") {
prodFolder = await createFolder({
authToken: jwtAuthToken,
environmentSlug: "prod",
workspaceId: seedData1.projectV3.id,
secretPath: "/",
name: "deep"
});
stagingFolder = await createFolder({
authToken: jwtAuthToken,
environmentSlug: "staging",
workspaceId: seedData1.projectV3.id,
secretPath: "/",
name: "deep"
});
devFolder = await createFolder({
authToken: jwtAuthToken,
environmentSlug: seedData1.environment.slug,
workspaceId: seedData1.projectV3.id,
secretPath: "/",
name: "deep"
});
}
const devImportFromStage = await createSecretImport({
authToken: jwtAuthToken,
secretPath: testSuitePath,
environmentSlug: seedData1.environment.slug,
workspaceId: seedData1.projectV3.id,
importPath: testSuitePath,
importEnv: "staging"
});
const stageImportFromProd = await createSecretImport({
authToken: jwtAuthToken,
secretPath: testSuitePath,
environmentSlug: "staging",
workspaceId: seedData1.projectV3.id,
importPath: testSuitePath,
importEnv: "prod"
});
return async () => {
await deleteSecretImport({
id: stageImportFromProd.id,
workspaceId: seedData1.projectV3.id,
environmentSlug: "staging",
secretPath: testSuitePath,
authToken: jwtAuthToken
});
await deleteSecretImport({
id: devImportFromStage.id,
workspaceId: seedData1.projectV3.id,
environmentSlug: seedData1.environment.slug,
secretPath: testSuitePath,
authToken: jwtAuthToken
});
if (prodFolder) {
await deleteFolder({
authToken: jwtAuthToken,
secretPath: "/",
id: prodFolder.id,
workspaceId: seedData1.projectV3.id,
environmentSlug: "prod"
});
}
if (stagingFolder) {
await deleteFolder({
authToken: jwtAuthToken,
secretPath: "/",
id: stagingFolder.id,
workspaceId: seedData1.projectV3.id,
environmentSlug: "staging"
});
}
if (devFolder) {
await deleteFolder({
authToken: jwtAuthToken,
secretPath: "/",
id: devFolder.id,
workspaceId: seedData1.projectV3.id,
environmentSlug: seedData1.environment.slug
});
}
};
});
test("Check one level imported secret exist", async () => {
await createSecretV2({
environmentSlug: "staging",
workspaceId: seedData1.projectV3.id,
secretPath: testSuitePath,
authToken: jwtAuthToken,
key: "STAGING_KEY",
value: "stage-value"
});
const secret = await getSecretByNameV2({
environmentSlug: seedData1.environment.slug,
workspaceId: seedData1.projectV3.id,
secretPath: testSuitePath,
authToken: jwtAuthToken,
key: "STAGING_KEY"
});
expect(secret.secretKey).toBe("STAGING_KEY");
expect(secret.secretValue).toBe("stage-value");
const listSecrets = await getSecretsV2({
environmentSlug: seedData1.environment.slug,
workspaceId: seedData1.projectV3.id,
secretPath: testSuitePath,
authToken: jwtAuthToken
});
expect(listSecrets.imports).toEqual(
expect.arrayContaining([
expect.objectContaining({
secrets: expect.arrayContaining([
expect.objectContaining({
secretKey: "STAGING_KEY",
secretValue: "stage-value"
})
])
})
])
);
await deleteSecretV2({
environmentSlug: "staging",
workspaceId: seedData1.projectV3.id,
secretPath: testSuitePath,
authToken: jwtAuthToken,
key: "STAGING_KEY"
});
});
test("Check two level imported secret exist", async () => {
await createSecretV2({
environmentSlug: "prod",
workspaceId: seedData1.projectV3.id,
secretPath: testSuitePath,
authToken: jwtAuthToken,
key: "PROD_KEY",
value: "prod-value"
});
const secret = await getSecretByNameV2({
environmentSlug: seedData1.environment.slug,
workspaceId: seedData1.projectV3.id,
secretPath: testSuitePath,
authToken: jwtAuthToken,
key: "PROD_KEY"
});
expect(secret.secretKey).toBe("PROD_KEY");
expect(secret.secretValue).toBe("prod-value");
const listSecrets = await getSecretsV2({
environmentSlug: seedData1.environment.slug,
workspaceId: seedData1.projectV3.id,
secretPath: testSuitePath,
authToken: jwtAuthToken
});
expect(listSecrets.imports).toEqual(
expect.arrayContaining([
expect.objectContaining({
secrets: expect.arrayContaining([
expect.objectContaining({
secretKey: "PROD_KEY",
secretValue: "prod-value"
})
])
})
])
);
await deleteSecretV2({
environmentSlug: "prod",
workspaceId: seedData1.projectV3.id,
secretPath: testSuitePath,
authToken: jwtAuthToken,
key: "PROD_KEY"
});
});
}
);
// dev <- stage, dev <- prod
describe.each([{ path: "/" }, { path: "/deep" }])(
"Secret import multiple destination to one source pattern testing - %path",
({ path: testSuitePath }) => {
beforeAll(async () => {
let prodFolder: { id: string };
let stagingFolder: { id: string };
let devFolder: { id: string };
if (testSuitePath !== "/") {
prodFolder = await createFolder({
authToken: jwtAuthToken,
environmentSlug: "prod",
workspaceId: seedData1.projectV3.id,
secretPath: "/",
name: "deep"
});
stagingFolder = await createFolder({
authToken: jwtAuthToken,
environmentSlug: "staging",
workspaceId: seedData1.projectV3.id,
secretPath: "/",
name: "deep"
});
devFolder = await createFolder({
authToken: jwtAuthToken,
environmentSlug: seedData1.environment.slug,
workspaceId: seedData1.projectV3.id,
secretPath: "/",
name: "deep"
});
}
const devImportFromStage = await createSecretImport({
authToken: jwtAuthToken,
secretPath: testSuitePath,
environmentSlug: seedData1.environment.slug,
workspaceId: seedData1.projectV3.id,
importPath: testSuitePath,
importEnv: "staging"
});
const devImportFromProd = await createSecretImport({
authToken: jwtAuthToken,
secretPath: testSuitePath,
environmentSlug: seedData1.environment.slug,
workspaceId: seedData1.projectV3.id,
importPath: testSuitePath,
importEnv: "prod"
});
return async () => {
await deleteSecretImport({
id: devImportFromProd.id,
workspaceId: seedData1.projectV3.id,
environmentSlug: seedData1.environment.slug,
secretPath: testSuitePath,
authToken: jwtAuthToken
});
await deleteSecretImport({
id: devImportFromStage.id,
workspaceId: seedData1.projectV3.id,
environmentSlug: seedData1.environment.slug,
secretPath: testSuitePath,
authToken: jwtAuthToken
});
if (prodFolder) {
await deleteFolder({
authToken: jwtAuthToken,
secretPath: "/",
id: prodFolder.id,
workspaceId: seedData1.projectV3.id,
environmentSlug: "prod"
});
}
if (stagingFolder) {
await deleteFolder({
authToken: jwtAuthToken,
secretPath: "/",
id: stagingFolder.id,
workspaceId: seedData1.projectV3.id,
environmentSlug: "staging"
});
}
if (devFolder) {
await deleteFolder({
authToken: jwtAuthToken,
secretPath: "/",
id: devFolder.id,
workspaceId: seedData1.projectV3.id,
environmentSlug: seedData1.environment.slug
});
}
};
});
test("Check imported secret exist", async () => {
await createSecretV2({
environmentSlug: "staging",
workspaceId: seedData1.projectV3.id,
secretPath: testSuitePath,
authToken: jwtAuthToken,
key: "STAGING_KEY",
value: "stage-value"
});
await createSecretV2({
environmentSlug: "prod",
workspaceId: seedData1.projectV3.id,
secretPath: testSuitePath,
authToken: jwtAuthToken,
key: "PROD_KEY",
value: "prod-value"
});
const secret = await getSecretByNameV2({
environmentSlug: seedData1.environment.slug,
workspaceId: seedData1.projectV3.id,
secretPath: testSuitePath,
authToken: jwtAuthToken,
key: "STAGING_KEY"
});
expect(secret.secretKey).toBe("STAGING_KEY");
expect(secret.secretValue).toBe("stage-value");
const listSecrets = await getSecretsV2({
environmentSlug: seedData1.environment.slug,
workspaceId: seedData1.projectV3.id,
secretPath: testSuitePath,
authToken: jwtAuthToken
});
expect(listSecrets.imports).toEqual(
expect.arrayContaining([
expect.objectContaining({
secrets: expect.arrayContaining([
expect.objectContaining({
secretKey: "STAGING_KEY",
secretValue: "stage-value"
})
])
}),
expect.objectContaining({
secrets: expect.arrayContaining([
expect.objectContaining({
secretKey: "PROD_KEY",
secretValue: "prod-value"
})
])
})
])
);
await deleteSecretV2({
environmentSlug: "staging",
workspaceId: seedData1.projectV3.id,
secretPath: testSuitePath,
authToken: jwtAuthToken,
key: "STAGING_KEY"
});
await deleteSecretV2({
environmentSlug: "prod",
workspaceId: seedData1.projectV3.id,
secretPath: testSuitePath,
authToken: jwtAuthToken,
key: "PROD_KEY"
});
});
}
);
// dev -> stage, prod
describe.each([{ path: "/" }, { path: "/deep" }])(
"Secret import one source to multiple destination pattern testing - %path",
({ path: testSuitePath }) => {
beforeAll(async () => {
let prodFolder: { id: string };
let stagingFolder: { id: string };
let devFolder: { id: string };
if (testSuitePath !== "/") {
prodFolder = await createFolder({
authToken: jwtAuthToken,
environmentSlug: "prod",
workspaceId: seedData1.projectV3.id,
secretPath: "/",
name: "deep"
});
stagingFolder = await createFolder({
authToken: jwtAuthToken,
environmentSlug: "staging",
workspaceId: seedData1.projectV3.id,
secretPath: "/",
name: "deep"
});
devFolder = await createFolder({
authToken: jwtAuthToken,
environmentSlug: seedData1.environment.slug,
workspaceId: seedData1.projectV3.id,
secretPath: "/",
name: "deep"
});
}
const stageImportFromDev = await createSecretImport({
authToken: jwtAuthToken,
secretPath: testSuitePath,
environmentSlug: "staging",
workspaceId: seedData1.projectV3.id,
importPath: testSuitePath,
importEnv: seedData1.environment.slug
});
const prodImportFromDev = await createSecretImport({
authToken: jwtAuthToken,
secretPath: testSuitePath,
environmentSlug: "prod",
workspaceId: seedData1.projectV3.id,
importPath: testSuitePath,
importEnv: seedData1.environment.slug
});
return async () => {
await deleteSecretImport({
id: prodImportFromDev.id,
workspaceId: seedData1.projectV3.id,
environmentSlug: "prod",
secretPath: testSuitePath,
authToken: jwtAuthToken
});
await deleteSecretImport({
id: stageImportFromDev.id,
workspaceId: seedData1.projectV3.id,
environmentSlug: "staging",
secretPath: testSuitePath,
authToken: jwtAuthToken
});
if (prodFolder) {
await deleteFolder({
authToken: jwtAuthToken,
secretPath: "/",
id: prodFolder.id,
workspaceId: seedData1.projectV3.id,
environmentSlug: "prod"
});
}
if (stagingFolder) {
await deleteFolder({
authToken: jwtAuthToken,
secretPath: "/",
id: stagingFolder.id,
workspaceId: seedData1.projectV3.id,
environmentSlug: "staging"
});
}
if (devFolder) {
await deleteFolder({
authToken: jwtAuthToken,
secretPath: "/",
id: devFolder.id,
workspaceId: seedData1.projectV3.id,
environmentSlug: seedData1.environment.slug
});
}
};
});
test("Check imported secret exist", async () => {
await createSecretV2({
environmentSlug: seedData1.environment.slug,
workspaceId: seedData1.projectV3.id,
secretPath: testSuitePath,
authToken: jwtAuthToken,
key: "STAGING_KEY",
value: "stage-value"
});
await createSecretV2({
environmentSlug: seedData1.environment.slug,
workspaceId: seedData1.projectV3.id,
secretPath: testSuitePath,
authToken: jwtAuthToken,
key: "PROD_KEY",
value: "prod-value"
});
const stagingSecret = await getSecretByNameV2({
environmentSlug: "staging",
workspaceId: seedData1.projectV3.id,
secretPath: testSuitePath,
authToken: jwtAuthToken,
key: "STAGING_KEY"
});
expect(stagingSecret.secretKey).toBe("STAGING_KEY");
expect(stagingSecret.secretValue).toBe("stage-value");
const prodSecret = await getSecretByNameV2({
environmentSlug: "prod",
workspaceId: seedData1.projectV3.id,
secretPath: testSuitePath,
authToken: jwtAuthToken,
key: "PROD_KEY"
});
expect(prodSecret.secretKey).toBe("PROD_KEY");
expect(prodSecret.secretValue).toBe("prod-value");
await deleteSecretV2({
environmentSlug: seedData1.environment.slug,
workspaceId: seedData1.projectV3.id,
secretPath: testSuitePath,
authToken: jwtAuthToken,
key: "STAGING_KEY"
});
await deleteSecretV2({
environmentSlug: seedData1.environment.slug,
workspaceId: seedData1.projectV3.id,
secretPath: testSuitePath,
authToken: jwtAuthToken,
key: "PROD_KEY"
});
});
}
);

View File

@@ -0,0 +1,406 @@
import { createFolder, deleteFolder } from "e2e-test/testUtils/folders";
import { createSecretImport, deleteSecretImport } from "e2e-test/testUtils/secret-imports";
import { createSecretV2, deleteSecretV2, getSecretByNameV2, getSecretsV2 } from "e2e-test/testUtils/secrets";
import { seedData1 } from "@app/db/seed-data";
// dev <- stage <- prod
describe.each([{ secretPath: "/" }, { secretPath: "/deep" }])(
"Secret replication waterfall pattern testing - %secretPath",
({ secretPath: testSuitePath }) => {
beforeAll(async () => {
let prodFolder: { id: string };
let stagingFolder: { id: string };
let devFolder: { id: string };
if (testSuitePath !== "/") {
prodFolder = await createFolder({
authToken: jwtAuthToken,
environmentSlug: "prod",
workspaceId: seedData1.projectV3.id,
secretPath: "/",
name: "deep"
});
stagingFolder = await createFolder({
authToken: jwtAuthToken,
environmentSlug: "staging",
workspaceId: seedData1.projectV3.id,
secretPath: "/",
name: "deep"
});
devFolder = await createFolder({
authToken: jwtAuthToken,
environmentSlug: seedData1.environment.slug,
workspaceId: seedData1.projectV3.id,
secretPath: "/",
name: "deep"
});
}
const devImportFromStage = await createSecretImport({
authToken: jwtAuthToken,
secretPath: testSuitePath,
environmentSlug: seedData1.environment.slug,
workspaceId: seedData1.projectV3.id,
importPath: testSuitePath,
importEnv: "staging",
isReplication: true
});
const stageImportFromProd = await createSecretImport({
authToken: jwtAuthToken,
secretPath: testSuitePath,
environmentSlug: "staging",
workspaceId: seedData1.projectV3.id,
importPath: testSuitePath,
importEnv: "prod",
isReplication: true
});
return async () => {
await deleteSecretImport({
id: stageImportFromProd.id,
workspaceId: seedData1.projectV3.id,
environmentSlug: "staging",
secretPath: testSuitePath,
authToken: jwtAuthToken
});
await deleteSecretImport({
id: devImportFromStage.id,
workspaceId: seedData1.projectV3.id,
environmentSlug: seedData1.environment.slug,
secretPath: testSuitePath,
authToken: jwtAuthToken
});
if (prodFolder) {
await deleteFolder({
authToken: jwtAuthToken,
secretPath: "/",
id: prodFolder.id,
workspaceId: seedData1.projectV3.id,
environmentSlug: "prod"
});
}
if (stagingFolder) {
await deleteFolder({
authToken: jwtAuthToken,
secretPath: "/",
id: stagingFolder.id,
workspaceId: seedData1.projectV3.id,
environmentSlug: "staging"
});
}
if (devFolder) {
await deleteFolder({
authToken: jwtAuthToken,
secretPath: "/",
id: devFolder.id,
workspaceId: seedData1.projectV3.id,
environmentSlug: seedData1.environment.slug
});
}
};
});
test("Check one level imported secret exist", async () => {
await createSecretV2({
environmentSlug: "staging",
workspaceId: seedData1.projectV3.id,
secretPath: testSuitePath,
authToken: jwtAuthToken,
key: "STAGING_KEY",
value: "stage-value"
});
// wait for 5 second for replication to finish
await new Promise((resolve) => {
setTimeout(resolve, 5000); // time to breathe for db
});
const secret = await getSecretByNameV2({
environmentSlug: seedData1.environment.slug,
workspaceId: seedData1.projectV3.id,
secretPath: testSuitePath,
authToken: jwtAuthToken,
key: "STAGING_KEY"
});
expect(secret.secretKey).toBe("STAGING_KEY");
expect(secret.secretValue).toBe("stage-value");
const listSecrets = await getSecretsV2({
environmentSlug: seedData1.environment.slug,
workspaceId: seedData1.projectV3.id,
secretPath: testSuitePath,
authToken: jwtAuthToken
});
expect(listSecrets.imports).toEqual(
expect.arrayContaining([
expect.objectContaining({
secrets: expect.arrayContaining([
expect.objectContaining({
secretKey: "STAGING_KEY",
secretValue: "stage-value"
})
])
})
])
);
await deleteSecretV2({
environmentSlug: "staging",
workspaceId: seedData1.projectV3.id,
secretPath: testSuitePath,
authToken: jwtAuthToken,
key: "STAGING_KEY"
});
});
test("Check two level imported secret exist", async () => {
await createSecretV2({
environmentSlug: "prod",
workspaceId: seedData1.projectV3.id,
secretPath: testSuitePath,
authToken: jwtAuthToken,
key: "PROD_KEY",
value: "prod-value"
});
// wait for 5 second for replication to finish
await new Promise((resolve) => {
setTimeout(resolve, 5000); // time to breathe for db
});
const secret = await getSecretByNameV2({
environmentSlug: seedData1.environment.slug,
workspaceId: seedData1.projectV3.id,
secretPath: testSuitePath,
authToken: jwtAuthToken,
key: "PROD_KEY"
});
expect(secret.secretKey).toBe("PROD_KEY");
expect(secret.secretValue).toBe("prod-value");
const listSecrets = await getSecretsV2({
environmentSlug: seedData1.environment.slug,
workspaceId: seedData1.projectV3.id,
secretPath: testSuitePath,
authToken: jwtAuthToken
});
expect(listSecrets.imports).toEqual(
expect.arrayContaining([
expect.objectContaining({
secrets: expect.arrayContaining([
expect.objectContaining({
secretKey: "PROD_KEY",
secretValue: "prod-value"
})
])
})
])
);
await deleteSecretV2({
environmentSlug: "prod",
workspaceId: seedData1.projectV3.id,
secretPath: testSuitePath,
authToken: jwtAuthToken,
key: "PROD_KEY"
});
});
},
{ timeout: 30000 }
);
// dev <- stage, dev <- prod
describe.each([{ path: "/" }, { path: "/deep" }])(
"Secret replication 1-N pattern testing - %path",
({ path: testSuitePath }) => {
beforeAll(async () => {
let prodFolder: { id: string };
let stagingFolder: { id: string };
let devFolder: { id: string };
if (testSuitePath !== "/") {
prodFolder = await createFolder({
authToken: jwtAuthToken,
environmentSlug: "prod",
workspaceId: seedData1.projectV3.id,
secretPath: "/",
name: "deep"
});
stagingFolder = await createFolder({
authToken: jwtAuthToken,
environmentSlug: "staging",
workspaceId: seedData1.projectV3.id,
secretPath: "/",
name: "deep"
});
devFolder = await createFolder({
authToken: jwtAuthToken,
environmentSlug: seedData1.environment.slug,
workspaceId: seedData1.projectV3.id,
secretPath: "/",
name: "deep"
});
}
const devImportFromStage = await createSecretImport({
authToken: jwtAuthToken,
secretPath: testSuitePath,
environmentSlug: seedData1.environment.slug,
workspaceId: seedData1.projectV3.id,
importPath: testSuitePath,
importEnv: "staging",
isReplication: true
});
const devImportFromProd = await createSecretImport({
authToken: jwtAuthToken,
secretPath: testSuitePath,
environmentSlug: seedData1.environment.slug,
workspaceId: seedData1.projectV3.id,
importPath: testSuitePath,
importEnv: "prod",
isReplication: true
});
return async () => {
await deleteSecretImport({
id: devImportFromProd.id,
workspaceId: seedData1.projectV3.id,
environmentSlug: seedData1.environment.slug,
secretPath: testSuitePath,
authToken: jwtAuthToken
});
await deleteSecretImport({
id: devImportFromStage.id,
workspaceId: seedData1.projectV3.id,
environmentSlug: seedData1.environment.slug,
secretPath: testSuitePath,
authToken: jwtAuthToken
});
if (prodFolder) {
await deleteFolder({
authToken: jwtAuthToken,
secretPath: "/",
id: prodFolder.id,
workspaceId: seedData1.projectV3.id,
environmentSlug: "prod"
});
}
if (stagingFolder) {
await deleteFolder({
authToken: jwtAuthToken,
secretPath: "/",
id: stagingFolder.id,
workspaceId: seedData1.projectV3.id,
environmentSlug: "staging"
});
}
if (devFolder) {
await deleteFolder({
authToken: jwtAuthToken,
secretPath: "/",
id: devFolder.id,
workspaceId: seedData1.projectV3.id,
environmentSlug: seedData1.environment.slug
});
}
};
});
test("Check imported secret exist", async () => {
await createSecretV2({
environmentSlug: "staging",
workspaceId: seedData1.projectV3.id,
secretPath: testSuitePath,
authToken: jwtAuthToken,
key: "STAGING_KEY",
value: "stage-value"
});
await createSecretV2({
environmentSlug: "prod",
workspaceId: seedData1.projectV3.id,
secretPath: testSuitePath,
authToken: jwtAuthToken,
key: "PROD_KEY",
value: "prod-value"
});
// wait for 5 second for replication to finish
await new Promise((resolve) => {
setTimeout(resolve, 5000); // time to breathe for db
});
const secret = await getSecretByNameV2({
environmentSlug: seedData1.environment.slug,
workspaceId: seedData1.projectV3.id,
secretPath: testSuitePath,
authToken: jwtAuthToken,
key: "STAGING_KEY"
});
expect(secret.secretKey).toBe("STAGING_KEY");
expect(secret.secretValue).toBe("stage-value");
const listSecrets = await getSecretsV2({
environmentSlug: seedData1.environment.slug,
workspaceId: seedData1.projectV3.id,
secretPath: testSuitePath,
authToken: jwtAuthToken
});
expect(listSecrets.imports).toEqual(
expect.arrayContaining([
expect.objectContaining({
secrets: expect.arrayContaining([
expect.objectContaining({
secretKey: "STAGING_KEY",
secretValue: "stage-value"
})
])
}),
expect.objectContaining({
secrets: expect.arrayContaining([
expect.objectContaining({
secretKey: "PROD_KEY",
secretValue: "prod-value"
})
])
})
])
);
await deleteSecretV2({
environmentSlug: "staging",
workspaceId: seedData1.projectV3.id,
secretPath: testSuitePath,
authToken: jwtAuthToken,
key: "STAGING_KEY"
});
await deleteSecretV2({
environmentSlug: "prod",
workspaceId: seedData1.projectV3.id,
secretPath: testSuitePath,
authToken: jwtAuthToken,
key: "PROD_KEY"
});
});
},
{ timeout: 30000 }
);

View File

@@ -0,0 +1,330 @@
import { createFolder, deleteFolder } from "e2e-test/testUtils/folders";
import { createSecretImport, deleteSecretImport } from "e2e-test/testUtils/secret-imports";
import { createSecretV2, deleteSecretV2, getSecretByNameV2, getSecretsV2 } from "e2e-test/testUtils/secrets";
import { seedData1 } from "@app/db/seed-data";
describe("Secret expansion", () => {
const projectId = seedData1.projectV3.id;
beforeAll(async () => {
const prodRootFolder = await createFolder({
authToken: jwtAuthToken,
environmentSlug: "prod",
workspaceId: projectId,
secretPath: "/",
name: "deep"
});
await createFolder({
authToken: jwtAuthToken,
environmentSlug: "prod",
workspaceId: projectId,
secretPath: "/deep",
name: "nested"
});
return async () => {
await deleteFolder({
authToken: jwtAuthToken,
secretPath: "/",
id: prodRootFolder.id,
workspaceId: projectId,
environmentSlug: "prod"
});
};
});
test("Local secret reference", async () => {
const secrets = [
{
environmentSlug: seedData1.environment.slug,
workspaceId: projectId,
secretPath: "/",
authToken: jwtAuthToken,
key: "HELLO",
value: "world"
},
{
environmentSlug: seedData1.environment.slug,
workspaceId: projectId,
secretPath: "/",
authToken: jwtAuthToken,
key: "TEST",
// eslint-disable-next-line
value: "hello ${HELLO}"
}
];
await Promise.all(secrets.map((el) => createSecretV2(el)));
const expandedSecret = await getSecretByNameV2({
environmentSlug: seedData1.environment.slug,
workspaceId: projectId,
secretPath: "/",
authToken: jwtAuthToken,
key: "TEST"
});
expect(expandedSecret.secretValue).toBe("hello world");
const listSecrets = await getSecretsV2({
environmentSlug: seedData1.environment.slug,
workspaceId: projectId,
secretPath: "/",
authToken: jwtAuthToken
});
expect(listSecrets.secrets).toEqual(
expect.arrayContaining([
expect.objectContaining({
secretKey: "TEST",
secretValue: "hello world"
})
])
);
await Promise.all(secrets.map((el) => deleteSecretV2(el)));
});
test("Cross environment secret reference", async () => {
const secrets = [
{
environmentSlug: "prod",
workspaceId: projectId,
secretPath: "/deep",
authToken: jwtAuthToken,
key: "DEEP_KEY_1",
value: "testing"
},
{
environmentSlug: "prod",
workspaceId: projectId,
secretPath: "/deep/nested",
authToken: jwtAuthToken,
key: "NESTED_KEY_1",
value: "reference"
},
{
environmentSlug: "prod",
workspaceId: projectId,
secretPath: "/deep/nested",
authToken: jwtAuthToken,
key: "NESTED_KEY_2",
// eslint-disable-next-line
value: "secret ${NESTED_KEY_1}"
},
{
environmentSlug: seedData1.environment.slug,
workspaceId: projectId,
secretPath: "/",
authToken: jwtAuthToken,
key: "KEY",
// eslint-disable-next-line
value: "hello ${prod.deep.DEEP_KEY_1} ${prod.deep.nested.NESTED_KEY_2}"
}
];
await Promise.all(secrets.map((el) => createSecretV2(el)));
const expandedSecret = await getSecretByNameV2({
environmentSlug: seedData1.environment.slug,
workspaceId: projectId,
secretPath: "/",
authToken: jwtAuthToken,
key: "KEY"
});
expect(expandedSecret.secretValue).toBe("hello testing secret reference");
const listSecrets = await getSecretsV2({
environmentSlug: seedData1.environment.slug,
workspaceId: projectId,
secretPath: "/",
authToken: jwtAuthToken
});
expect(listSecrets.secrets).toEqual(
expect.arrayContaining([
expect.objectContaining({
secretKey: "KEY",
secretValue: "hello testing secret reference"
})
])
);
await Promise.all(secrets.map((el) => deleteSecretV2(el)));
});
test("Non replicated secret import secret expansion on local reference and nested reference", async () => {
const secrets = [
{
environmentSlug: "prod",
workspaceId: projectId,
secretPath: "/deep",
authToken: jwtAuthToken,
key: "DEEP_KEY_1",
value: "testing"
},
{
environmentSlug: "prod",
workspaceId: projectId,
secretPath: "/deep/nested",
authToken: jwtAuthToken,
key: "NESTED_KEY_1",
value: "reference"
},
{
environmentSlug: "prod",
workspaceId: projectId,
secretPath: "/deep/nested",
authToken: jwtAuthToken,
key: "NESTED_KEY_2",
// eslint-disable-next-line
value: "secret ${NESTED_KEY_1} ${prod.deep.DEEP_KEY_1}"
},
{
environmentSlug: seedData1.environment.slug,
workspaceId: projectId,
secretPath: "/",
authToken: jwtAuthToken,
key: "KEY",
// eslint-disable-next-line
value: "hello world"
}
];
await Promise.all(secrets.map((el) => createSecretV2(el)));
const secretImportFromProdToDev = await createSecretImport({
environmentSlug: seedData1.environment.slug,
workspaceId: projectId,
secretPath: "/",
authToken: jwtAuthToken,
importEnv: "prod",
importPath: "/deep/nested"
});
const listSecrets = await getSecretsV2({
environmentSlug: seedData1.environment.slug,
workspaceId: projectId,
secretPath: "/",
authToken: jwtAuthToken
});
expect(listSecrets.imports).toEqual(
expect.arrayContaining([
expect.objectContaining({
secretPath: "/deep/nested",
environment: "prod",
secrets: expect.arrayContaining([
expect.objectContaining({
secretKey: "NESTED_KEY_1",
secretValue: "reference"
}),
expect.objectContaining({
secretKey: "NESTED_KEY_2",
secretValue: "secret reference testing"
})
])
})
])
);
await Promise.all(secrets.map((el) => deleteSecretV2(el)));
await deleteSecretImport({
environmentSlug: seedData1.environment.slug,
workspaceId: projectId,
authToken: jwtAuthToken,
id: secretImportFromProdToDev.id,
secretPath: "/"
});
});
test(
"Replicated secret import secret expansion on local reference and nested reference",
async () => {
const secrets = [
{
environmentSlug: "prod",
workspaceId: projectId,
secretPath: "/deep",
authToken: jwtAuthToken,
key: "DEEP_KEY_1",
value: "testing"
},
{
environmentSlug: "prod",
workspaceId: projectId,
secretPath: "/deep/nested",
authToken: jwtAuthToken,
key: "NESTED_KEY_1",
value: "reference"
},
{
environmentSlug: "prod",
workspaceId: projectId,
secretPath: "/deep/nested",
authToken: jwtAuthToken,
key: "NESTED_KEY_2",
// eslint-disable-next-line
value: "secret ${NESTED_KEY_1} ${prod.deep.DEEP_KEY_1}"
},
{
environmentSlug: seedData1.environment.slug,
workspaceId: projectId,
secretPath: "/",
authToken: jwtAuthToken,
key: "KEY",
// eslint-disable-next-line
value: "hello world"
}
];
await Promise.all(secrets.map((el) => createSecretV2(el)));
const secretImportFromProdToDev = await createSecretImport({
environmentSlug: seedData1.environment.slug,
workspaceId: projectId,
secretPath: "/",
authToken: jwtAuthToken,
importEnv: "prod",
importPath: "/deep/nested",
isReplication: true
});
// wait for 5 second for replication to finish
await new Promise((resolve) => {
setTimeout(resolve, 5000); // time to breathe for db
});
const listSecrets = await getSecretsV2({
environmentSlug: seedData1.environment.slug,
workspaceId: projectId,
secretPath: "/",
authToken: jwtAuthToken
});
expect(listSecrets.imports).toEqual(
expect.arrayContaining([
expect.objectContaining({
secretPath: `/__reserve_replication_${secretImportFromProdToDev.id}`,
environment: seedData1.environment.slug,
secrets: expect.arrayContaining([
expect.objectContaining({
secretKey: "NESTED_KEY_1",
secretValue: "reference"
}),
expect.objectContaining({
secretKey: "NESTED_KEY_2",
secretValue: "secret reference testing"
})
])
})
])
);
await Promise.all(secrets.map((el) => deleteSecretV2(el)));
await deleteSecretImport({
environmentSlug: seedData1.environment.slug,
workspaceId: projectId,
authToken: jwtAuthToken,
id: secretImportFromProdToDev.id,
secretPath: "/"
});
},
{ timeout: 10000 }
);
});

View File

@@ -8,6 +8,7 @@ type TRawSecret = {
secretComment?: string;
version: number;
};
const createSecret = async (dto: { path: string; key: string; value: string; comment: string; type?: SecretType }) => {
const createSecretReqBody = {
workspaceId: seedData1.projectV3.id,

View File

@@ -0,0 +1,73 @@
type TFolder = {
id: string;
name: string;
};
export const createFolder = async (dto: {
workspaceId: string;
environmentSlug: string;
secretPath: string;
name: string;
authToken: string;
}) => {
const res = await testServer.inject({
method: "POST",
url: `/api/v1/folders`,
headers: {
authorization: `Bearer ${dto.authToken}`
},
body: {
workspaceId: dto.workspaceId,
environment: dto.environmentSlug,
name: dto.name,
path: dto.secretPath
}
});
expect(res.statusCode).toBe(200);
return res.json().folder as TFolder;
};
export const deleteFolder = async (dto: {
workspaceId: string;
environmentSlug: string;
secretPath: string;
id: string;
authToken: string;
}) => {
const res = await testServer.inject({
method: "DELETE",
url: `/api/v1/folders/${dto.id}`,
headers: {
authorization: `Bearer ${dto.authToken}`
},
body: {
workspaceId: dto.workspaceId,
environment: dto.environmentSlug,
path: dto.secretPath
}
});
expect(res.statusCode).toBe(200);
return res.json().folder as TFolder;
};
export const listFolders = async (dto: {
workspaceId: string;
environmentSlug: string;
secretPath: string;
authToken: string;
}) => {
const res = await testServer.inject({
method: "GET",
url: `/api/v1/folders`,
headers: {
authorization: `Bearer ${dto.authToken}`
},
body: {
workspaceId: dto.workspaceId,
environment: dto.environmentSlug,
path: dto.secretPath
}
});
expect(res.statusCode).toBe(200);
return res.json().folders as TFolder[];
};

View File

@@ -0,0 +1,93 @@
type TSecretImport = {
id: string;
importEnv: {
name: string;
slug: string;
id: string;
};
importPath: string;
};
export const createSecretImport = async (dto: {
workspaceId: string;
environmentSlug: string;
isReplication?: boolean;
secretPath: string;
importPath: string;
importEnv: string;
authToken: string;
}) => {
const res = await testServer.inject({
method: "POST",
url: `/api/v1/secret-imports`,
headers: {
authorization: `Bearer ${dto.authToken}`
},
body: {
workspaceId: dto.workspaceId,
environment: dto.environmentSlug,
isReplication: dto.isReplication,
path: dto.secretPath,
import: {
environment: dto.importEnv,
path: dto.importPath
}
}
});
expect(res.statusCode).toBe(200);
const payload = JSON.parse(res.payload);
expect(payload).toHaveProperty("secretImport");
return payload.secretImport as TSecretImport;
};
export const deleteSecretImport = async (dto: {
workspaceId: string;
environmentSlug: string;
secretPath: string;
authToken: string;
id: string;
}) => {
const res = await testServer.inject({
method: "DELETE",
url: `/api/v1/secret-imports/${dto.id}`,
headers: {
authorization: `Bearer ${dto.authToken}`
},
body: {
workspaceId: dto.workspaceId,
environment: dto.environmentSlug,
path: dto.secretPath
}
});
expect(res.statusCode).toBe(200);
const payload = JSON.parse(res.payload);
expect(payload).toHaveProperty("secretImport");
return payload.secretImport as TSecretImport;
};
export const listSecretImport = async (dto: {
workspaceId: string;
environmentSlug: string;
secretPath: string;
authToken: string;
}) => {
const res = await testServer.inject({
method: "GET",
url: `/api/v1/secret-imports`,
headers: {
authorization: `Bearer ${dto.authToken}`
},
query: {
workspaceId: dto.workspaceId,
environment: dto.environmentSlug,
path: dto.secretPath
}
});
expect(res.statusCode).toBe(200);
const payload = JSON.parse(res.payload);
expect(payload).toHaveProperty("secretImports");
return payload.secretImports as TSecretImport[];
};

View File

@@ -0,0 +1,128 @@
import { SecretType } from "@app/db/schemas";
type TRawSecret = {
secretKey: string;
secretValue: string;
secretComment?: string;
version: number;
};
export const createSecretV2 = async (dto: {
workspaceId: string;
environmentSlug: string;
secretPath: string;
key: string;
value: string;
comment?: string;
authToken: string;
type?: SecretType;
}) => {
const createSecretReqBody = {
workspaceId: dto.workspaceId,
environment: dto.environmentSlug,
type: dto.type || SecretType.Shared,
secretPath: dto.secretPath,
secretKey: dto.key,
secretValue: dto.value,
secretComment: dto.comment
};
const createSecRes = await testServer.inject({
method: "POST",
url: `/api/v3/secrets/raw/${dto.key}`,
headers: {
authorization: `Bearer ${dto.authToken}`
},
body: createSecretReqBody
});
expect(createSecRes.statusCode).toBe(200);
const createdSecretPayload = JSON.parse(createSecRes.payload);
expect(createdSecretPayload).toHaveProperty("secret");
return createdSecretPayload.secret as TRawSecret;
};
export const deleteSecretV2 = async (dto: {
workspaceId: string;
environmentSlug: string;
secretPath: string;
key: string;
authToken: string;
}) => {
const deleteSecRes = await testServer.inject({
method: "DELETE",
url: `/api/v3/secrets/raw/${dto.key}`,
headers: {
authorization: `Bearer ${dto.authToken}`
},
body: {
workspaceId: dto.workspaceId,
environment: dto.environmentSlug,
secretPath: dto.secretPath
}
});
expect(deleteSecRes.statusCode).toBe(200);
const updatedSecretPayload = JSON.parse(deleteSecRes.payload);
expect(updatedSecretPayload).toHaveProperty("secret");
return updatedSecretPayload.secret as TRawSecret;
};
export const getSecretByNameV2 = async (dto: {
workspaceId: string;
environmentSlug: string;
secretPath: string;
key: string;
authToken: string;
}) => {
const response = await testServer.inject({
method: "GET",
url: `/api/v3/secrets/raw/${dto.key}`,
headers: {
authorization: `Bearer ${dto.authToken}`
},
query: {
workspaceId: dto.workspaceId,
environment: dto.environmentSlug,
secretPath: dto.secretPath,
expandSecretReferences: "true",
include_imports: "true"
}
});
expect(response.statusCode).toBe(200);
const payload = JSON.parse(response.payload);
expect(payload).toHaveProperty("secret");
return payload.secret as TRawSecret;
};
export const getSecretsV2 = async (dto: {
workspaceId: string;
environmentSlug: string;
secretPath: string;
authToken: string;
}) => {
const getSecretsResponse = await testServer.inject({
method: "GET",
url: `/api/v3/secrets/raw`,
headers: {
authorization: `Bearer ${dto.authToken}`
},
query: {
workspaceId: dto.workspaceId,
environment: dto.environmentSlug,
secretPath: dto.secretPath,
expandSecretReferences: "true",
include_imports: "true"
}
});
expect(getSecretsResponse.statusCode).toBe(200);
const getSecretsPayload = JSON.parse(getSecretsResponse.payload);
expect(getSecretsPayload).toHaveProperty("secrets");
expect(getSecretsPayload).toHaveProperty("imports");
return getSecretsPayload as {
secrets: TRawSecret[];
imports: {
secretPath: string;
environment: string;
folderId: string;
secrets: TRawSecret[];
}[];
};
};

View File

@@ -11,10 +11,11 @@ import { initLogger } from "@app/lib/logger";
import { main } from "@app/server/app";
import { AuthMethod, AuthTokenType } from "@app/services/auth/auth-type";
import { mockQueue } from "./mocks/queue";
import { mockSmtpServer } from "./mocks/smtp";
import { mockKeyStore } from "./mocks/keystore";
import { initDbConnection } from "@app/db";
import { queueServiceFactory } from "@app/queue";
import { keyStoreFactory } from "@app/keystore/keystore";
import { Redis } from "ioredis";
dotenv.config({ path: path.join(__dirname, "../../.env.test"), debug: true });
export default {
@@ -28,19 +29,31 @@ export default {
dbRootCert: cfg.DB_ROOT_CERT
});
const redis = new Redis(cfg.REDIS_URL);
await redis.flushdb("SYNC");
try {
await db.migrate.rollback(
{
directory: path.join(__dirname, "../src/db/migrations"),
extension: "ts",
tableName: "infisical_migrations"
},
true
);
await db.migrate.latest({
directory: path.join(__dirname, "../src/db/migrations"),
extension: "ts",
tableName: "infisical_migrations"
});
await db.seed.run({
directory: path.join(__dirname, "../src/db/seeds"),
extension: "ts"
});
const smtp = mockSmtpServer();
const queue = mockQueue();
const keyStore = mockKeyStore();
const queue = queueServiceFactory(cfg.REDIS_URL);
const keyStore = keyStoreFactory(cfg.REDIS_URL);
const server = await main({ db, smtp, logger, queue, keyStore });
// @ts-expect-error type
globalThis.testServer = server;
@@ -58,10 +71,12 @@ export default {
{ expiresIn: cfg.JWT_AUTH_LIFETIME }
);
} catch (error) {
// eslint-disable-next-line
console.log("[TEST] Error setting up environment", error);
await db.destroy();
throw error;
}
// custom setup
return {
async teardown() {
@@ -80,6 +95,9 @@ export default {
},
true
);
await redis.flushdb("ASYNC");
redis.disconnect();
await db.destroy();
}
};

File diff suppressed because it is too large Load Diff

View File

@@ -50,6 +50,7 @@
"migration:down": "knex --knexfile ./src/db/knexfile.ts --client pg migrate:down",
"migration:list": "knex --knexfile ./src/db/knexfile.ts --client pg migrate:list",
"migration:latest": "knex --knexfile ./src/db/knexfile.ts --client pg migrate:latest",
"migration:status": "knex --knexfile ./src/db/knexfile.ts --client pg migrate:status",
"migration:rollback": "knex --knexfile ./src/db/knexfile.ts migrate:rollback",
"seed:new": "tsx ./scripts/create-seed-file.ts",
"seed": "knex --knexfile ./src/db/knexfile.ts --client pg seed:run",
@@ -102,15 +103,16 @@
"tsup": "^8.0.1",
"tsx": "^4.4.0",
"typescript": "^5.3.2",
"vite-tsconfig-paths": "^4.2.2",
"vitest": "^1.2.2"
},
"dependencies": {
"@aws-sdk/client-elasticache": "^3.637.0",
"@aws-sdk/client-iam": "^3.525.0",
"@aws-sdk/client-kms": "^3.609.0",
"@aws-sdk/client-secrets-manager": "^3.504.0",
"@aws-sdk/client-sts": "^3.600.0",
"@casl/ability": "^6.5.0",
"@elastic/elasticsearch": "^8.15.0",
"@fastify/cookie": "^9.3.1",
"@fastify/cors": "^8.5.0",
"@fastify/etag": "^5.1.0",
@@ -171,9 +173,12 @@
"pg-query-stream": "^4.5.3",
"picomatch": "^3.0.1",
"pino": "^8.16.2",
"pkijs": "^3.2.4",
"posthog-node": "^3.6.2",
"probot": "^13.0.0",
"safe-regex": "^2.1.1",
"scim-patch": "^0.8.3",
"scim2-parse-filter": "^0.2.10",
"smee-client": "^2.0.0",
"tedious": "^18.2.1",
"tweetnacl": "^1.0.3",

View File

@@ -7,6 +7,7 @@ import { TAuditLogServiceFactory } from "@app/ee/services/audit-log/audit-log-se
import { TCreateAuditLogDTO } from "@app/ee/services/audit-log/audit-log-types";
import { TAuditLogStreamServiceFactory } from "@app/ee/services/audit-log-stream/audit-log-stream-service";
import { TCertificateAuthorityCrlServiceFactory } from "@app/ee/services/certificate-authority-crl/certificate-authority-crl-service";
import { TCertificateEstServiceFactory } from "@app/ee/services/certificate-est/certificate-est-service";
import { TDynamicSecretServiceFactory } from "@app/ee/services/dynamic-secret/dynamic-secret-service";
import { TDynamicSecretLeaseServiceFactory } from "@app/ee/services/dynamic-secret-lease/dynamic-secret-lease-service";
import { TExternalKmsServiceFactory } from "@app/ee/services/external-kms/external-kms-service";
@@ -160,6 +161,7 @@ declare module "fastify" {
certificateTemplate: TCertificateTemplateServiceFactory;
certificateAuthority: TCertificateAuthorityServiceFactory;
certificateAuthorityCrl: TCertificateAuthorityCrlServiceFactory;
certificateEst: TCertificateEstServiceFactory;
pkiCollection: TPkiCollectionServiceFactory;
secretScanning: TSecretScanningServiceFactory;
license: TLicenseServiceFactory;

View File

@@ -53,6 +53,9 @@ import {
TCertificateSecretsUpdate,
TCertificatesInsert,
TCertificatesUpdate,
TCertificateTemplateEstConfigs,
TCertificateTemplateEstConfigsInsert,
TCertificateTemplateEstConfigsUpdate,
TCertificateTemplates,
TCertificateTemplatesInsert,
TCertificateTemplatesUpdate,
@@ -372,6 +375,11 @@ declare module "knex/types/tables" {
TCertificateTemplatesInsert,
TCertificateTemplatesUpdate
>;
[TableName.CertificateTemplateEstConfig]: KnexOriginal.CompositeTableType<
TCertificateTemplateEstConfigs,
TCertificateTemplateEstConfigsInsert,
TCertificateTemplateEstConfigsUpdate
>;
[TableName.CertificateBody]: KnexOriginal.CompositeTableType<
TCertificateBodies,
TCertificateBodiesInsert,

View File

@@ -115,7 +115,14 @@ export async function down(knex: Knex): Promise<void> {
// eslint-disable-next-line
// @ts-ignore because generate schema happens after this
approverId: knex(TableName.ProjectMembership)
.select("id")
.join(
TableName.SecretApprovalPolicy,
`${TableName.SecretApprovalPolicy}.id`,
`${TableName.SecretApprovalPolicyApprover}.policyId`
)
.join(TableName.Environment, `${TableName.Environment}.id`, `${TableName.SecretApprovalPolicy}.envId`)
.select(knex.ref("id").withSchema(TableName.ProjectMembership))
.where(`${TableName.ProjectMembership}.projectId`, knex.raw("??", [`${TableName.Environment}.projectId`]))
.where("userId", knex.raw("??", [`${TableName.SecretApprovalPolicyApprover}.approverUserId`]))
});
await knex.schema.alterTable(TableName.SecretApprovalPolicyApprover, (tb) => {
@@ -147,13 +154,27 @@ export async function down(knex: Knex): Promise<void> {
// eslint-disable-next-line
// @ts-ignore because generate schema happens after this
committerId: knex(TableName.ProjectMembership)
.select("id")
.where("userId", knex.raw("??", [`${TableName.SecretApprovalRequest}.committerUserId`])),
.join(
TableName.SecretApprovalPolicy,
`${TableName.SecretApprovalPolicy}.id`,
`${TableName.SecretApprovalRequest}.policyId`
)
.join(TableName.Environment, `${TableName.Environment}.id`, `${TableName.SecretApprovalPolicy}.envId`)
.where(`${TableName.ProjectMembership}.projectId`, knex.raw("??", [`${TableName.Environment}.projectId`]))
.where("userId", knex.raw("??", [`${TableName.SecretApprovalRequest}.committerUserId`]))
.select(knex.ref("id").withSchema(TableName.ProjectMembership)),
// eslint-disable-next-line
// @ts-ignore because generate schema happens after this
statusChangeBy: knex(TableName.ProjectMembership)
.select("id")
.join(
TableName.SecretApprovalPolicy,
`${TableName.SecretApprovalPolicy}.id`,
`${TableName.SecretApprovalRequest}.policyId`
)
.join(TableName.Environment, `${TableName.Environment}.id`, `${TableName.SecretApprovalPolicy}.envId`)
.where(`${TableName.ProjectMembership}.projectId`, knex.raw("??", [`${TableName.Environment}.projectId`]))
.where("userId", knex.raw("??", [`${TableName.SecretApprovalRequest}.statusChangedByUserId`]))
.select(knex.ref("id").withSchema(TableName.ProjectMembership))
});
await knex.schema.alterTable(TableName.SecretApprovalRequest, (tb) => {
@@ -177,8 +198,20 @@ export async function down(knex: Knex): Promise<void> {
// eslint-disable-next-line
// @ts-ignore because generate schema happens after this
member: knex(TableName.ProjectMembership)
.select("id")
.join(
TableName.SecretApprovalRequest,
`${TableName.SecretApprovalRequest}.id`,
`${TableName.SecretApprovalRequestReviewer}.requestId`
)
.join(
TableName.SecretApprovalPolicy,
`${TableName.SecretApprovalPolicy}.id`,
`${TableName.SecretApprovalRequest}.policyId`
)
.join(TableName.Environment, `${TableName.Environment}.id`, `${TableName.SecretApprovalPolicy}.envId`)
.where(`${TableName.ProjectMembership}.projectId`, knex.raw("??", [`${TableName.Environment}.projectId`]))
.where("userId", knex.raw("??", [`${TableName.SecretApprovalRequestReviewer}.reviewerUserId`]))
.select(knex.ref("id").withSchema(TableName.ProjectMembership))
});
await knex.schema.alterTable(TableName.SecretApprovalRequestReviewer, (tb) => {
tb.uuid("member").notNullable().alter();

View File

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

View File

@@ -0,0 +1,26 @@
import { Knex } from "knex";
import { TableName } from "../schemas";
import { createOnUpdateTrigger, dropOnUpdateTrigger } from "../utils";
export async function up(knex: Knex): Promise<void> {
const hasEstConfigTable = await knex.schema.hasTable(TableName.CertificateTemplateEstConfig);
if (!hasEstConfigTable) {
await knex.schema.createTable(TableName.CertificateTemplateEstConfig, (tb) => {
tb.uuid("id", { primaryKey: true }).defaultTo(knex.fn.uuid());
tb.uuid("certificateTemplateId").notNullable().unique();
tb.foreign("certificateTemplateId").references("id").inTable(TableName.CertificateTemplate).onDelete("CASCADE");
tb.binary("encryptedCaChain").notNullable();
tb.string("hashedPassphrase").notNullable();
tb.boolean("isEnabled").notNullable();
tb.timestamps(true, true, true);
});
await createOnUpdateTrigger(knex, TableName.CertificateTemplateEstConfig);
}
}
export async function down(knex: Knex): Promise<void> {
await knex.schema.dropTableIfExists(TableName.CertificateTemplateEstConfig);
await dropOnUpdateTrigger(knex, TableName.CertificateTemplateEstConfig);
}

View File

@@ -0,0 +1,29 @@
// Code generated by automation script, DO NOT EDIT.
// Automated by pulling database and generating zod schema
// To update. Just run npm run generate:schema
// Written by akhilmhdh.
import { z } from "zod";
import { zodBuffer } from "@app/lib/zod";
import { TImmutableDBKeys } from "./models";
export const CertificateTemplateEstConfigsSchema = z.object({
id: z.string().uuid(),
certificateTemplateId: z.string().uuid(),
encryptedCaChain: zodBuffer,
hashedPassphrase: z.string(),
isEnabled: z.boolean(),
createdAt: z.date(),
updatedAt: z.date()
});
export type TCertificateTemplateEstConfigs = z.infer<typeof CertificateTemplateEstConfigsSchema>;
export type TCertificateTemplateEstConfigsInsert = Omit<
z.input<typeof CertificateTemplateEstConfigsSchema>,
TImmutableDBKeys
>;
export type TCertificateTemplateEstConfigsUpdate = Partial<
Omit<z.input<typeof CertificateTemplateEstConfigsSchema>, TImmutableDBKeys>
>;

View File

@@ -14,6 +14,7 @@ export * from "./certificate-authority-crl";
export * from "./certificate-authority-secret";
export * from "./certificate-bodies";
export * from "./certificate-secrets";
export * from "./certificate-template-est-configs";
export * from "./certificate-templates";
export * from "./certificates";
export * from "./dynamic-secret-leases";

View File

@@ -3,6 +3,7 @@ import { z } from "zod";
export enum TableName {
Users = "users",
CertificateAuthority = "certificate_authorities",
CertificateTemplateEstConfig = "certificate_template_est_configs",
CertificateAuthorityCert = "certificate_authority_certs",
CertificateAuthoritySecret = "certificate_authority_secret",
CertificateAuthorityCrl = "certificate_authority_crl",

View File

@@ -21,6 +21,7 @@ export const SecretSharingSchema = z.object({
expiresAfterViews: z.number().nullable().optional(),
accessType: z.string().default("anyone"),
name: z.string().nullable().optional(),
password: z.string().nullable().optional(),
lastViewedAt: z.date().nullable().optional()
});

View File

@@ -0,0 +1,173 @@
import bcrypt from "bcrypt";
import { z } from "zod";
import { getConfig } from "@app/lib/config/env";
import { BadRequestError, UnauthorizedError } from "@app/lib/errors";
import { readLimit, writeLimit } from "@app/server/config/rateLimiter";
export const registerCertificateEstRouter = async (server: FastifyZodProvider) => {
const appCfg = getConfig();
// add support for CSR bodies
server.addContentTypeParser("application/pkcs10", { parseAs: "string" }, (_, body, done) => {
try {
let csrBody = body as string;
// some EST clients send CSRs in PEM format and some in base64 format
// for CSRs sent in PEM, we leave them as is
// for CSRs sent in base64, we preprocess them to remove new lines and spaces
if (!csrBody.includes("BEGIN CERTIFICATE REQUEST")) {
csrBody = csrBody.replace(/\n/g, "").replace(/ /g, "");
}
done(null, csrBody);
} catch (err) {
const error = err as Error;
done(error, undefined);
}
});
// Authenticate EST client using Passphrase
server.addHook("onRequest", async (req, res) => {
const { authorization } = req.headers;
const urlFragments = req.url.split("/");
// cacerts endpoint should not have any authentication
if (urlFragments[urlFragments.length - 1] === "cacerts") {
return;
}
if (!authorization) {
const wwwAuthenticateHeader = "WWW-Authenticate";
const errAuthRequired = "Authentication required";
await res.hijack();
// definitive connection timeout to clean-up open connections and prevent memory leak
res.raw.setTimeout(10 * 1000, () => {
res.raw.end();
});
res.raw.setHeader(wwwAuthenticateHeader, `Basic realm="infisical"`);
res.raw.setHeader("Content-Length", 0);
res.raw.statusCode = 401;
// Write the error message to the response without ending the connection
res.raw.write(errAuthRequired);
// flush headers
res.raw.flushHeaders();
return;
}
const certificateTemplateId = urlFragments.slice(-2)[0];
const estConfig = await server.services.certificateTemplate.getEstConfiguration({
isInternal: true,
certificateTemplateId
});
if (!estConfig.isEnabled) {
throw new BadRequestError({
message: "EST is disabled"
});
}
const rawCredential = authorization?.split(" ").pop();
if (!rawCredential) {
throw new UnauthorizedError({ message: "Missing HTTP credentials" });
}
// expected format is user:password
const basicCredential = atob(rawCredential);
const password = basicCredential.split(":").pop();
if (!password) {
throw new BadRequestError({
message: "No password provided"
});
}
const isPasswordValid = await bcrypt.compare(password, estConfig.hashedPassphrase);
if (!isPasswordValid) {
throw new UnauthorizedError({
message: "Invalid credentials"
});
}
});
server.route({
method: "POST",
url: "/:certificateTemplateId/simpleenroll",
config: {
rateLimit: writeLimit
},
schema: {
body: z.string().min(1),
params: z.object({
certificateTemplateId: z.string().min(1)
}),
response: {
200: z.string()
}
},
handler: async (req, res) => {
void res.header("Content-Type", "application/pkcs7-mime; smime-type=certs-only");
void res.header("Content-Transfer-Encoding", "base64");
return server.services.certificateEst.simpleEnroll({
csr: req.body,
certificateTemplateId: req.params.certificateTemplateId,
sslClientCert: req.headers[appCfg.SSL_CLIENT_CERTIFICATE_HEADER_KEY] as string
});
}
});
server.route({
method: "POST",
url: "/:certificateTemplateId/simplereenroll",
config: {
rateLimit: writeLimit
},
schema: {
body: z.string().min(1),
params: z.object({
certificateTemplateId: z.string().min(1)
}),
response: {
200: z.string()
}
},
handler: async (req, res) => {
void res.header("Content-Type", "application/pkcs7-mime; smime-type=certs-only");
void res.header("Content-Transfer-Encoding", "base64");
return server.services.certificateEst.simpleReenroll({
csr: req.body,
certificateTemplateId: req.params.certificateTemplateId,
sslClientCert: req.headers[appCfg.SSL_CLIENT_CERTIFICATE_HEADER_KEY] as string
});
}
});
server.route({
method: "GET",
url: "/:certificateTemplateId/cacerts",
config: {
rateLimit: readLimit
},
schema: {
params: z.object({
certificateTemplateId: z.string().min(1)
}),
response: {
200: z.string()
}
},
handler: async (req, res) => {
void res.header("Content-Type", "application/pkcs7-mime; smime-type=certs-only");
void res.header("Content-Transfer-Encoding", "base64");
return server.services.certificateEst.getCaCerts({
certificateTemplateId: req.params.certificateTemplateId
});
}
});
};

View File

@@ -17,11 +17,11 @@ export const registerAccessApprovalPolicyRouter = async (server: FastifyZodProvi
name: z.string().optional(),
secretPath: z.string().trim().default("/"),
environment: z.string(),
approverUserIds: z.string().array().min(1),
approvers: z.string().array().min(1),
approvals: z.number().min(1).default(1),
enforcementLevel: z.nativeEnum(EnforcementLevel).default(EnforcementLevel.Hard)
})
.refine((data) => data.approvals <= data.approverUserIds.length, {
.refine((data) => data.approvals <= data.approvers.length, {
path: ["approvals"],
message: "The number of approvals should be lower than the number of approvers."
}),
@@ -127,11 +127,11 @@ export const registerAccessApprovalPolicyRouter = async (server: FastifyZodProvi
.trim()
.optional()
.transform((val) => (val === "" ? "/" : val)),
approverUserIds: z.string().array().min(1),
approvers: z.string().array().min(1),
approvals: z.number().min(1).default(1),
enforcementLevel: z.nativeEnum(EnforcementLevel).default(EnforcementLevel.Hard)
})
.refine((data) => data.approvals <= data.approverUserIds.length, {
.refine((data) => data.approvals <= data.approvers.length, {
path: ["approvals"],
message: "The number of approvals should be lower than the number of approvers."
}),

View File

@@ -1,6 +1,6 @@
/* eslint-disable @typescript-eslint/no-unsafe-return */
/* eslint-disable @typescript-eslint/no-unsafe-assignment */
// TODO(akhilmhdh): Fix this when licence service gets it type
// TODO(akhilmhdh): Fix this when license service gets it type
import { z } from "zod";
import { readLimit, writeLimit } from "@app/server/config/rateLimiter";

View File

@@ -5,19 +5,47 @@ import { readLimit, writeLimit } from "@app/server/config/rateLimiter";
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
import { AuthMode } from "@app/services/auth/auth-type";
const ScimUserSchema = z.object({
schemas: z.array(z.string()),
id: z.string().trim(),
userName: z.string().trim(),
name: z
.object({
familyName: z.string().trim().optional(),
givenName: z.string().trim().optional()
})
.optional(),
emails: z
.array(
z.object({
primary: z.boolean(),
value: z.string().email(),
type: z.string().trim()
})
)
.optional(),
displayName: z.string().trim(),
active: z.boolean()
});
const ScimGroupSchema = z.object({
schemas: z.array(z.string()),
id: z.string().trim(),
displayName: z.string().trim(),
members: z
.array(
z.object({
value: z.string(),
display: z.string().optional()
})
)
.optional(),
meta: z.object({
resourceType: z.string().trim()
})
});
export const registerScimRouter = async (server: FastifyZodProvider) => {
server.addContentTypeParser("application/scim+json", { parseAs: "string" }, (_, body, done) => {
try {
const strBody = body instanceof Buffer ? body.toString() : body;
const json: unknown = JSON.parse(strBody);
done(null, json);
} catch (err) {
const error = err as Error;
done(error, undefined);
}
});
server.route({
url: "/scim-tokens",
method: "POST",
@@ -124,25 +152,7 @@ export const registerScimRouter = async (server: FastifyZodProvider) => {
}),
response: {
200: z.object({
Resources: z.array(
z.object({
id: z.string().trim(),
userName: z.string().trim(),
name: z.object({
familyName: z.string().trim(),
givenName: z.string().trim()
}),
emails: z.array(
z.object({
primary: z.boolean(),
value: z.string(),
type: z.string().trim()
})
),
displayName: z.string().trim(),
active: z.boolean()
})
),
Resources: z.array(ScimUserSchema),
itemsPerPage: z.number(),
schemas: z.array(z.string()),
startIndex: z.number(),
@@ -170,30 +180,7 @@ export const registerScimRouter = async (server: FastifyZodProvider) => {
orgMembershipId: z.string().trim()
}),
response: {
201: z.object({
schemas: z.array(z.string()),
id: z.string().trim(),
userName: z.string().trim(),
name: z.object({
familyName: z.string().trim(),
givenName: z.string().trim()
}),
emails: z.array(
z.object({
primary: z.boolean(),
value: z.string(),
type: z.string().trim()
})
),
displayName: z.string().trim(),
active: z.boolean(),
groups: z.array(
z.object({
value: z.string().trim(),
display: z.string().trim()
})
)
})
200: ScimUserSchema
}
},
onRequest: verifyAuth([AuthMode.SCIM_TOKEN]),
@@ -213,10 +200,12 @@ export const registerScimRouter = async (server: FastifyZodProvider) => {
body: z.object({
schemas: z.array(z.string()),
userName: z.string().trim(),
name: z.object({
familyName: z.string().trim(),
givenName: z.string().trim()
}),
name: z
.object({
familyName: z.string().trim().optional(),
givenName: z.string().trim().optional()
})
.optional(),
emails: z
.array(
z.object({
@@ -226,28 +215,10 @@ export const registerScimRouter = async (server: FastifyZodProvider) => {
})
)
.optional(),
// displayName: z.string().trim(),
active: z.boolean()
active: z.boolean().default(true)
}),
response: {
200: z.object({
schemas: z.array(z.string()),
id: z.string().trim(),
userName: z.string().trim(),
name: z.object({
familyName: z.string().trim(),
givenName: z.string().trim()
}),
emails: z.array(
z.object({
primary: z.boolean(),
value: z.string().email(),
type: z.string().trim()
})
),
displayName: z.string().trim(),
active: z.boolean()
})
200: ScimUserSchema
}
},
onRequest: verifyAuth([AuthMode.SCIM_TOKEN]),
@@ -257,8 +228,8 @@ export const registerScimRouter = async (server: FastifyZodProvider) => {
const user = await req.server.services.scim.createScimUser({
externalId: req.body.userName,
email: primaryEmail,
firstName: req.body.name.givenName,
lastName: req.body.name.familyName,
firstName: req.body?.name?.givenName,
lastName: req.body?.name?.familyName,
orgId: req.permission.orgId
});
@@ -288,6 +259,116 @@ export const registerScimRouter = async (server: FastifyZodProvider) => {
}
});
server.route({
url: "/Users/:orgMembershipId",
method: "PUT",
schema: {
params: z.object({
orgMembershipId: z.string().trim()
}),
body: z.object({
schemas: z.array(z.string()),
id: z.string().trim(),
userName: z.string().trim(),
name: z
.object({
familyName: z.string().trim().optional(),
givenName: z.string().trim().optional()
})
.optional(),
displayName: z.string().trim(),
emails: z
.array(
z.object({
primary: z.boolean(),
value: z.string().email(),
type: z.string().trim()
})
)
.optional(),
active: z.boolean()
}),
response: {
200: z.object({
schemas: z.array(z.string()),
id: z.string().trim(),
userName: z.string().trim(),
name: z.object({
familyName: z.string().trim(),
givenName: z.string().trim()
}),
emails: z.array(
z.object({
primary: z.boolean(),
value: z.string().email(),
type: z.string().trim()
})
),
displayName: z.string().trim(),
active: z.boolean()
})
}
},
onRequest: verifyAuth([AuthMode.SCIM_TOKEN]),
handler: async (req) => {
const primaryEmail = req.body.emails?.find((email) => email.primary)?.value;
const user = await req.server.services.scim.replaceScimUser({
orgMembershipId: req.params.orgMembershipId,
orgId: req.permission.orgId,
firstName: req.body?.name?.givenName,
lastName: req.body?.name?.familyName,
active: req.body?.active,
email: primaryEmail,
externalId: req.body.userName
});
return user;
}
});
server.route({
url: "/Users/:orgMembershipId",
method: "PATCH",
schema: {
params: z.object({
orgMembershipId: z.string().trim()
}),
body: z.object({
schemas: z.array(z.string()),
Operations: z.array(
z.union([
z.object({
op: z.union([z.literal("remove"), z.literal("Remove")]),
path: z.string().trim(),
value: z
.object({
value: z.string()
})
.array()
.optional()
}),
z.object({
op: z.union([z.literal("add"), z.literal("Add"), z.literal("replace"), z.literal("Replace")]),
path: z.string().trim().optional(),
value: z.any().optional()
})
])
)
}),
response: {
200: ScimUserSchema
}
},
onRequest: verifyAuth([AuthMode.SCIM_TOKEN]),
handler: async (req) => {
const user = await req.server.services.scim.updateScimUser({
orgMembershipId: req.params.orgMembershipId,
orgId: req.permission.orgId,
operations: req.body.Operations
});
return user;
}
});
server.route({
url: "/Groups",
method: "POST",
@@ -302,25 +383,10 @@ export const registerScimRouter = async (server: FastifyZodProvider) => {
display: z.string()
})
)
.optional() // okta-specific
.optional()
}),
response: {
200: z.object({
schemas: z.array(z.string()),
id: z.string().trim(),
displayName: z.string().trim(),
members: z
.array(
z.object({
value: z.string(),
display: z.string()
})
)
.optional(),
meta: z.object({
resourceType: z.string().trim()
})
})
200: ScimGroupSchema
}
},
onRequest: verifyAuth([AuthMode.SCIM_TOKEN]),
@@ -341,26 +407,12 @@ export const registerScimRouter = async (server: FastifyZodProvider) => {
querystring: z.object({
startIndex: z.coerce.number().default(1),
count: z.coerce.number().default(20),
filter: z.string().trim().optional()
filter: z.string().trim().optional(),
excludedAttributes: z.string().trim().optional()
}),
response: {
200: z.object({
Resources: z.array(
z.object({
schemas: z.array(z.string()),
id: z.string().trim(),
displayName: z.string().trim(),
members: z.array(
z.object({
value: z.string(),
display: z.string()
})
),
meta: z.object({
resourceType: z.string().trim()
})
})
),
Resources: z.array(ScimGroupSchema),
itemsPerPage: z.number(),
schemas: z.array(z.string()),
startIndex: z.number(),
@@ -374,7 +426,8 @@ export const registerScimRouter = async (server: FastifyZodProvider) => {
orgId: req.permission.orgId,
startIndex: req.query.startIndex,
filter: req.query.filter,
limit: req.query.count
limit: req.query.count,
isMembersExcluded: req.query.excludedAttributes === "members"
});
return groups;
@@ -389,20 +442,7 @@ export const registerScimRouter = async (server: FastifyZodProvider) => {
groupId: z.string().trim()
}),
response: {
200: z.object({
schemas: z.array(z.string()),
id: z.string().trim(),
displayName: z.string().trim(),
members: z.array(
z.object({
value: z.string(),
display: z.string()
})
),
meta: z.object({
resourceType: z.string().trim()
})
})
200: ScimGroupSchema
}
},
onRequest: verifyAuth([AuthMode.SCIM_TOKEN]),
@@ -411,6 +451,7 @@ export const registerScimRouter = async (server: FastifyZodProvider) => {
groupId: req.params.groupId,
orgId: req.permission.orgId
});
return group;
}
});
@@ -434,25 +475,12 @@ export const registerScimRouter = async (server: FastifyZodProvider) => {
)
}),
response: {
200: z.object({
schemas: z.array(z.string()),
id: z.string().trim(),
displayName: z.string().trim(),
members: z.array(
z.object({
value: z.string(),
display: z.string()
})
),
meta: z.object({
resourceType: z.string().trim()
})
})
200: ScimGroupSchema
}
},
onRequest: verifyAuth([AuthMode.SCIM_TOKEN]),
handler: async (req) => {
const group = await req.server.services.scim.updateScimGroupNamePut({
const group = await req.server.services.scim.replaceScimGroup({
groupId: req.params.groupId,
orgId: req.permission.orgId,
...req.body
@@ -474,54 +502,34 @@ export const registerScimRouter = async (server: FastifyZodProvider) => {
Operations: z.array(
z.union([
z.object({
op: z.literal("replace"),
value: z.object({
id: z.string().trim(),
displayName: z.string().trim()
})
}),
z.object({
op: z.literal("remove"),
path: z.string().trim()
}),
z.object({
op: z.literal("add"),
op: z.union([z.literal("remove"), z.literal("Remove")]),
path: z.string().trim(),
value: z.array(
z.object({
value: z.string().trim(),
display: z.string().trim().optional()
value: z
.object({
value: z.string()
})
)
.array()
.optional()
}),
z.object({
op: z.union([z.literal("add"), z.literal("Add"), z.literal("replace"), z.literal("Replace")]),
path: z.string().trim().optional(),
value: z.any()
})
])
)
}),
response: {
200: z.object({
schemas: z.array(z.string()),
id: z.string().trim(),
displayName: z.string().trim(),
members: z.array(
z.object({
value: z.string(),
display: z.string()
})
),
meta: z.object({
resourceType: z.string().trim()
})
})
200: ScimGroupSchema
}
},
onRequest: verifyAuth([AuthMode.SCIM_TOKEN]),
handler: async (req) => {
const group = await req.server.services.scim.updateScimGroupNamePatch({
const group = await req.server.services.scim.updateScimGroup({
groupId: req.params.groupId,
orgId: req.permission.orgId,
operations: req.body.Operations
});
return group;
}
});
@@ -547,60 +555,4 @@ export const registerScimRouter = async (server: FastifyZodProvider) => {
return group;
}
});
server.route({
url: "/Users/:orgMembershipId",
method: "PUT",
schema: {
params: z.object({
orgMembershipId: z.string().trim()
}),
body: z.object({
schemas: z.array(z.string()),
id: z.string().trim(),
userName: z.string().trim(),
name: z.object({
familyName: z.string().trim(),
givenName: z.string().trim()
}),
displayName: z.string().trim(),
active: z.boolean()
}),
response: {
200: z.object({
schemas: z.array(z.string()),
id: z.string().trim(),
userName: z.string().trim(),
name: z.object({
familyName: z.string().trim(),
givenName: z.string().trim()
}),
emails: z.array(
z.object({
primary: z.boolean(),
value: z.string().email(),
type: z.string().trim()
})
),
displayName: z.string().trim(),
active: z.boolean(),
groups: z.array(
z.object({
value: z.string().trim(),
display: z.string().trim()
})
)
})
}
},
onRequest: verifyAuth([AuthMode.SCIM_TOKEN]),
handler: async (req) => {
const user = await req.server.services.scim.replaceScimUser({
orgMembershipId: req.params.orgMembershipId,
orgId: req.permission.orgId,
active: req.body.active
});
return user;
}
});
};

View File

@@ -44,7 +44,7 @@ export const accessApprovalPolicyServiceFactory = ({
secretPath,
actorAuthMethod,
approvals,
approverUserIds,
approvers,
projectSlug,
environment,
enforcementLevel
@@ -52,7 +52,7 @@ export const accessApprovalPolicyServiceFactory = ({
const project = await projectDAL.findProjectBySlug(projectSlug, actorOrgId);
if (!project) throw new BadRequestError({ message: "Project not found" });
if (approvals > approverUserIds.length)
if (approvals > approvers.length)
throw new BadRequestError({ message: "Approvals cannot be greater than approvers" });
const { permission } = await permissionService.getProjectPermission(
@@ -76,7 +76,7 @@ export const accessApprovalPolicyServiceFactory = ({
secretPath,
actorAuthMethod,
permissionService,
userIds: approverUserIds
userIds: approvers
});
const accessApproval = await accessApprovalPolicyDAL.transaction(async (tx) => {
@@ -91,7 +91,7 @@ export const accessApprovalPolicyServiceFactory = ({
tx
);
await accessApprovalPolicyApproverDAL.insertMany(
approverUserIds.map((userId) => ({
approvers.map((userId) => ({
approverUserId: userId,
policyId: doc.id
})),
@@ -128,7 +128,7 @@ export const accessApprovalPolicyServiceFactory = ({
const updateAccessApprovalPolicy = async ({
policyId,
approverUserIds,
approvers,
secretPath,
name,
actorId,
@@ -161,7 +161,7 @@ export const accessApprovalPolicyServiceFactory = ({
},
tx
);
if (approverUserIds) {
if (approvers) {
await verifyApprovers({
projectId: accessApprovalPolicy.projectId,
orgId: actorOrgId,
@@ -169,12 +169,12 @@ export const accessApprovalPolicyServiceFactory = ({
secretPath: doc.secretPath!,
actorAuthMethod,
permissionService,
userIds: approverUserIds
userIds: approvers
});
await accessApprovalPolicyApproverDAL.delete({ policyId: doc.id }, tx);
await accessApprovalPolicyApproverDAL.insertMany(
approverUserIds.map((userId) => ({
approvers.map((userId) => ({
approverUserId: userId,
policyId: doc.id
})),

View File

@@ -17,7 +17,7 @@ export type TCreateAccessApprovalPolicy = {
approvals: number;
secretPath: string;
environment: string;
approverUserIds: string[];
approvers: string[];
projectSlug: string;
name: string;
enforcementLevel: EnforcementLevel;
@@ -26,7 +26,7 @@ export type TCreateAccessApprovalPolicy = {
export type TUpdateAccessApprovalPolicy = {
policyId: string;
approvals?: number;
approverUserIds?: string[];
approvers?: string[];
secretPath?: string;
name?: string;
enforcementLevel?: EnforcementLevel;

View File

@@ -2,10 +2,11 @@ import { ForbiddenError } from "@casl/ability";
import { RawAxiosRequestHeaders } from "axios";
import { SecretKeyEncoding } from "@app/db/schemas";
import { getConfig } from "@app/lib/config/env";
import { request } from "@app/lib/config/request";
import { infisicalSymmetricDecrypt, infisicalSymmetricEncypt } from "@app/lib/crypto/encryption";
import { BadRequestError } from "@app/lib/errors";
import { validateLocalIps } from "@app/lib/validator";
import { blockLocalAndPrivateIpAddresses } from "@app/lib/validator";
import { AUDIT_LOG_STREAM_TIMEOUT } from "../audit-log/audit-log-queue";
import { TLicenseServiceFactory } from "../license/license-service";
@@ -44,6 +45,7 @@ export const auditLogStreamServiceFactory = ({
}: TCreateAuditLogStreamDTO) => {
if (!actorOrgId) throw new BadRequestError({ message: "Missing org id from token" });
const appCfg = getConfig();
const plan = await licenseService.getPlan(actorOrgId);
if (!plan.auditLogStreams)
throw new BadRequestError({
@@ -59,7 +61,9 @@ export const auditLogStreamServiceFactory = ({
);
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Create, OrgPermissionSubjects.Settings);
validateLocalIps(url);
if (appCfg.isCloud) {
blockLocalAndPrivateIpAddresses(url);
}
const totalStreams = await auditLogStreamDAL.find({ orgId: actorOrgId });
if (totalStreams.length >= plan.auditLogStreamLimit) {
@@ -131,7 +135,8 @@ export const auditLogStreamServiceFactory = ({
const { permission } = await permissionService.getOrgPermission(actor, actorId, orgId, actorAuthMethod, actorOrgId);
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Edit, OrgPermissionSubjects.Settings);
if (url) validateLocalIps(url);
const appCfg = getConfig();
if (url && appCfg.isCloud) blockLocalAndPrivateIpAddresses(url);
// testing connection first
const streamHeaders: RawAxiosRequestHeaders = { "Content-Type": "application/json" };

View File

@@ -5,6 +5,7 @@ import { TableName } from "@app/db/schemas";
import { DatabaseError } from "@app/lib/errors";
import { ormify, stripUndefinedInWhere } from "@app/lib/knex";
import { logger } from "@app/lib/logger";
import { QueueName } from "@app/queue";
export type TAuditLogDALFactory = ReturnType<typeof auditLogDALFactory>;
@@ -62,7 +63,9 @@ export const auditLogDALFactory = (db: TDbClient) => {
const today = new Date();
let deletedAuditLogIds: { id: string }[] = [];
let numberOfRetryOnFailure = 0;
let isRetrying = false;
logger.info(`${QueueName.DailyResourceCleanUp}: audit log started`);
do {
try {
const findExpiredLogSubQuery = (tx || db)(TableName.AuditLog)
@@ -84,7 +87,9 @@ export const auditLogDALFactory = (db: TDbClient) => {
setTimeout(resolve, 10); // time to breathe for db
});
}
} while (deletedAuditLogIds.length > 0 || numberOfRetryOnFailure < MAX_RETRY_ON_FAILURE);
isRetrying = numberOfRetryOnFailure > 0;
} while (deletedAuditLogIds.length > 0 || (isRetrying && numberOfRetryOnFailure < MAX_RETRY_ON_FAILURE));
logger.info(`${QueueName.DailyResourceCleanUp}: audit log completed`);
};
return { ...auditLogOrm, pruneAuditLog, find };

View File

@@ -166,7 +166,10 @@ export enum EventType {
CREATE_CERTIFICATE_TEMPLATE = "create-certificate-template",
UPDATE_CERTIFICATE_TEMPLATE = "update-certificate-template",
DELETE_CERTIFICATE_TEMPLATE = "delete-certificate-template",
GET_CERTIFICATE_TEMPLATE = "get-certificate-template"
GET_CERTIFICATE_TEMPLATE = "get-certificate-template",
CREATE_CERTIFICATE_TEMPLATE_EST_CONFIG = "create-certificate-template-est-config",
UPDATE_CERTIFICATE_TEMPLATE_EST_CONFIG = "update-certificate-template-est-config",
GET_CERTIFICATE_TEMPLATE_EST_CONFIG = "get-certificate-template-est-config"
}
interface UserActorMetadata {
@@ -1420,6 +1423,29 @@ interface OrgAdminAccessProjectEvent {
}; // no metadata yet
}
interface CreateCertificateTemplateEstConfig {
type: EventType.CREATE_CERTIFICATE_TEMPLATE_EST_CONFIG;
metadata: {
certificateTemplateId: string;
isEnabled: boolean;
};
}
interface UpdateCertificateTemplateEstConfig {
type: EventType.UPDATE_CERTIFICATE_TEMPLATE_EST_CONFIG;
metadata: {
certificateTemplateId: string;
isEnabled: boolean;
};
}
interface GetCertificateTemplateEstConfig {
type: EventType.GET_CERTIFICATE_TEMPLATE_EST_CONFIG;
metadata: {
certificateTemplateId: string;
};
}
export type Event =
| GetSecretsEvent
| GetSecretEvent
@@ -1547,4 +1573,7 @@ export type Event =
| CreateCertificateTemplate
| UpdateCertificateTemplate
| GetCertificateTemplate
| DeleteCertificateTemplate;
| DeleteCertificateTemplate
| CreateCertificateTemplateEstConfig
| UpdateCertificateTemplateEstConfig
| GetCertificateTemplateEstConfig;

View File

@@ -0,0 +1,24 @@
import { Certificate, ContentInfo, EncapsulatedContentInfo, SignedData } from "pkijs";
export const convertRawCertsToPkcs7 = (rawCertificate: ArrayBuffer[]) => {
const certs = rawCertificate.map((rawCert) => Certificate.fromBER(rawCert));
const cmsSigned = new SignedData({
encapContentInfo: new EncapsulatedContentInfo({
eContentType: "1.2.840.113549.1.7.1" // not encrypted and not compressed data
}),
certificates: certs
});
const cmsContent = new ContentInfo({
contentType: "1.2.840.113549.1.7.2", // SignedData
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
content: cmsSigned.toSchema()
});
const derBuffer = cmsContent.toSchema().toBER(false);
const base64Pkcs7 = Buffer.from(derBuffer)
.toString("base64")
.replace(/(.{64})/g, "$1\n"); // we add a linebreak for CURL clients
return base64Pkcs7;
};

View File

@@ -0,0 +1,268 @@
import * as x509 from "@peculiar/x509";
import { BadRequestError, NotFoundError, UnauthorizedError } from "@app/lib/errors";
import { isCertChainValid } from "@app/services/certificate/certificate-fns";
import { TCertificateAuthorityCertDALFactory } from "@app/services/certificate-authority/certificate-authority-cert-dal";
import { TCertificateAuthorityDALFactory } from "@app/services/certificate-authority/certificate-authority-dal";
import { getCaCertChain, getCaCertChains } from "@app/services/certificate-authority/certificate-authority-fns";
import { TCertificateAuthorityServiceFactory } from "@app/services/certificate-authority/certificate-authority-service";
import { TCertificateTemplateDALFactory } from "@app/services/certificate-template/certificate-template-dal";
import { TCertificateTemplateServiceFactory } from "@app/services/certificate-template/certificate-template-service";
import { TKmsServiceFactory } from "@app/services/kms/kms-service";
import { TProjectDALFactory } from "@app/services/project/project-dal";
import { TLicenseServiceFactory } from "../license/license-service";
import { convertRawCertsToPkcs7 } from "./certificate-est-fns";
type TCertificateEstServiceFactoryDep = {
certificateAuthorityService: Pick<TCertificateAuthorityServiceFactory, "signCertFromCa">;
certificateTemplateService: Pick<TCertificateTemplateServiceFactory, "getEstConfiguration">;
certificateTemplateDAL: Pick<TCertificateTemplateDALFactory, "findById">;
certificateAuthorityDAL: Pick<TCertificateAuthorityDALFactory, "findById">;
certificateAuthorityCertDAL: Pick<TCertificateAuthorityCertDALFactory, "find" | "findById">;
projectDAL: Pick<TProjectDALFactory, "findOne" | "updateById" | "transaction">;
kmsService: Pick<TKmsServiceFactory, "decryptWithKmsKey" | "generateKmsKey">;
licenseService: Pick<TLicenseServiceFactory, "getPlan">;
};
export type TCertificateEstServiceFactory = ReturnType<typeof certificateEstServiceFactory>;
export const certificateEstServiceFactory = ({
certificateAuthorityService,
certificateTemplateService,
certificateTemplateDAL,
certificateAuthorityCertDAL,
certificateAuthorityDAL,
projectDAL,
kmsService,
licenseService
}: TCertificateEstServiceFactoryDep) => {
const simpleReenroll = async ({
csr,
certificateTemplateId,
sslClientCert
}: {
csr: string;
certificateTemplateId: string;
sslClientCert: string;
}) => {
const estConfig = await certificateTemplateService.getEstConfiguration({
isInternal: true,
certificateTemplateId
});
const plan = await licenseService.getPlan(estConfig.orgId);
if (!plan.pkiEst) {
throw new BadRequestError({
message:
"Failed to perform EST operation - simpleReenroll due to plan restriction. Upgrade to the Enterprise plan."
});
}
if (!estConfig.isEnabled) {
throw new BadRequestError({
message: "EST is disabled"
});
}
const certTemplate = await certificateTemplateDAL.findById(certificateTemplateId);
const leafCertificate = decodeURIComponent(sslClientCert).match(
/-----BEGIN CERTIFICATE-----[\s\S]+?-----END CERTIFICATE-----/g
)?.[0];
if (!leafCertificate) {
throw new UnauthorizedError({ message: "Missing client certificate" });
}
const cert = new x509.X509Certificate(leafCertificate);
// We have to assert that the client certificate provided can be traced back to the Root CA
const caCertChains = await getCaCertChains({
caId: certTemplate.caId,
certificateAuthorityCertDAL,
certificateAuthorityDAL,
projectDAL,
kmsService
});
const verifiedChains = await Promise.all(
caCertChains.map((chain) => {
const caCert = new x509.X509Certificate(chain.certificate);
const caChain =
chain.certificateChain
.match(/-----BEGIN CERTIFICATE-----[\s\S]+?-----END CERTIFICATE-----/g)
?.map((c) => new x509.X509Certificate(c)) || [];
return isCertChainValid([cert, caCert, ...caChain]);
})
);
if (!verifiedChains.some(Boolean)) {
throw new BadRequestError({
message: "Invalid client certificate: unable to build a valid certificate chain"
});
}
// We ensure that the Subject and SubjectAltNames of the CSR and the existing certificate are exactly the same
const csrObj = new x509.Pkcs10CertificateRequest(csr);
if (csrObj.subject !== cert.subject) {
throw new BadRequestError({
message: "Subject mismatch"
});
}
let csrSanSet: Set<string> = new Set();
const csrSanExtension = csrObj.extensions.find((ext) => ext.type === "2.5.29.17");
if (csrSanExtension) {
const sanNames = new x509.GeneralNames(csrSanExtension.value);
csrSanSet = new Set([...sanNames.items.map((name) => `${name.type}-${name.value}`)]);
}
let certSanSet: Set<string> = new Set();
const certSanExtension = cert.extensions.find((ext) => ext.type === "2.5.29.17");
if (certSanExtension) {
const sanNames = new x509.GeneralNames(certSanExtension.value);
certSanSet = new Set([...sanNames.items.map((name) => `${name.type}-${name.value}`)]);
}
if (csrSanSet.size !== certSanSet.size || ![...csrSanSet].every((element) => certSanSet.has(element))) {
throw new BadRequestError({
message: "Subject alternative names mismatch"
});
}
const { certificate } = await certificateAuthorityService.signCertFromCa({
isInternal: true,
certificateTemplateId,
csr
});
return convertRawCertsToPkcs7([certificate.rawData]);
};
const simpleEnroll = async ({
csr,
certificateTemplateId,
sslClientCert
}: {
csr: string;
certificateTemplateId: string;
sslClientCert: string;
}) => {
/* We first have to assert that the client certificate provided can be traced back to the attached
CA chain in the EST configuration
*/
const estConfig = await certificateTemplateService.getEstConfiguration({
isInternal: true,
certificateTemplateId
});
const plan = await licenseService.getPlan(estConfig.orgId);
if (!plan.pkiEst) {
throw new BadRequestError({
message:
"Failed to perform EST operation - simpleEnroll due to plan restriction. Upgrade to the Enterprise plan."
});
}
if (!estConfig.isEnabled) {
throw new BadRequestError({
message: "EST is disabled"
});
}
const caCerts = estConfig.caChain
.match(/-----BEGIN CERTIFICATE-----[\s\S]+?-----END CERTIFICATE-----/g)
?.map((cert) => {
return new x509.X509Certificate(cert);
});
if (!caCerts) {
throw new BadRequestError({ message: "Failed to parse certificate chain" });
}
const leafCertificate = decodeURIComponent(sslClientCert).match(
/-----BEGIN CERTIFICATE-----[\s\S]+?-----END CERTIFICATE-----/g
)?.[0];
if (!leafCertificate) {
throw new BadRequestError({ message: "Missing client certificate" });
}
const certObj = new x509.X509Certificate(leafCertificate);
if (!(await isCertChainValid([certObj, ...caCerts]))) {
throw new BadRequestError({ message: "Invalid certificate chain" });
}
const { certificate } = await certificateAuthorityService.signCertFromCa({
isInternal: true,
certificateTemplateId,
csr
});
return convertRawCertsToPkcs7([certificate.rawData]);
};
/**
* Return the CA certificate and CA certificate chain for the CA bound to
* the certificate template with id [certificateTemplateId] as part of EST protocol
*/
const getCaCerts = async ({ certificateTemplateId }: { certificateTemplateId: string }) => {
const certTemplate = await certificateTemplateDAL.findById(certificateTemplateId);
if (!certTemplate) {
throw new NotFoundError({
message: "Certificate template not found"
});
}
const estConfig = await certificateTemplateService.getEstConfiguration({
isInternal: true,
certificateTemplateId
});
const plan = await licenseService.getPlan(estConfig.orgId);
if (!plan.pkiEst) {
throw new BadRequestError({
message: "Failed to perform EST operation - caCerts due to plan restriction. Upgrade to the Enterprise plan."
});
}
if (!estConfig.isEnabled) {
throw new BadRequestError({
message: "EST is disabled"
});
}
const ca = await certificateAuthorityDAL.findById(certTemplate.caId);
if (!ca) {
throw new NotFoundError({
message: "Certificate Authority not found"
});
}
const { caCert, caCertChain } = await getCaCertChain({
caCertId: ca.activeCaCertId as string,
certificateAuthorityDAL,
certificateAuthorityCertDAL,
projectDAL,
kmsService
});
const certificates = caCertChain
.match(/-----BEGIN CERTIFICATE-----[\s\S]+?-----END CERTIFICATE-----/g)
?.map((cert) => new x509.X509Certificate(cert));
if (!certificates) {
throw new BadRequestError({ message: "Failed to parse certificate chain" });
}
const caCertificate = new x509.X509Certificate(caCert);
return convertRawCertsToPkcs7([caCertificate.rawData, ...certificates.map((cert) => cert.rawData)]);
};
return {
simpleEnroll,
simpleReenroll,
getCaCerts
};
};

View File

@@ -0,0 +1,226 @@
import {
CreateUserCommand,
CreateUserGroupCommand,
DeleteUserCommand,
DescribeReplicationGroupsCommand,
DescribeUserGroupsCommand,
ElastiCache,
ModifyReplicationGroupCommand,
ModifyUserGroupCommand
} from "@aws-sdk/client-elasticache";
import handlebars from "handlebars";
import { customAlphabet } from "nanoid";
import { z } from "zod";
import { BadRequestError } from "@app/lib/errors";
import { DynamicSecretAwsElastiCacheSchema, TDynamicProviderFns } from "./models";
const CreateElastiCacheUserSchema = z.object({
UserId: z.string().trim().min(1),
UserName: z.string().trim().min(1),
Engine: z.string().default("redis"),
Passwords: z.array(z.string().trim().min(1)).min(1).max(1), // Minimum password length is 16 characters, required by AWS.
AccessString: z.string().trim().min(1) // Example: "on ~* +@all"
});
const DeleteElasticCacheUserSchema = z.object({
UserId: z.string().trim().min(1)
});
type TElastiCacheRedisUser = { userId: string; password: string };
type TBasicAWSCredentials = { accessKeyId: string; secretAccessKey: string };
type TCreateElastiCacheUserInput = z.infer<typeof CreateElastiCacheUserSchema>;
type TDeleteElastiCacheUserInput = z.infer<typeof DeleteElasticCacheUserSchema>;
const ElastiCacheUserManager = (credentials: TBasicAWSCredentials, region: string) => {
const elastiCache = new ElastiCache({
region,
credentials
});
const infisicalGroup = "infisical-managed-group-elasticache";
const ensureInfisicalGroupExists = async (clusterName: string) => {
const replicationGroups = await elastiCache.send(new DescribeUserGroupsCommand());
const existingGroup = replicationGroups.UserGroups?.find((group) => group.UserGroupId === infisicalGroup);
let newlyCreatedGroup = false;
if (!existingGroup) {
const createGroupCommand = new CreateUserGroupCommand({
UserGroupId: infisicalGroup,
UserIds: ["default"],
Engine: "redis"
});
await elastiCache.send(createGroupCommand);
newlyCreatedGroup = true;
}
if (existingGroup || newlyCreatedGroup) {
const replicationGroup = (
await elastiCache.send(
new DescribeReplicationGroupsCommand({
ReplicationGroupId: clusterName
})
)
).ReplicationGroups?.[0];
if (!replicationGroup?.UserGroupIds?.includes(infisicalGroup)) {
// If the replication group doesn't have the infisical user group, we need to associate it
const modifyGroupCommand = new ModifyReplicationGroupCommand({
UserGroupIdsToAdd: [infisicalGroup],
UserGroupIdsToRemove: [],
ApplyImmediately: true,
ReplicationGroupId: clusterName
});
await elastiCache.send(modifyGroupCommand);
}
}
};
const addUserToInfisicalGroup = async (userId: string) => {
// figure out if the default user is already in the group, if it is, then we shouldn't add it again
const addUserToGroupCommand = new ModifyUserGroupCommand({
UserGroupId: infisicalGroup,
UserIdsToAdd: [userId],
UserIdsToRemove: []
});
await elastiCache.send(addUserToGroupCommand);
};
const createUser = async (creationInput: TCreateElastiCacheUserInput, clusterName: string) => {
await ensureInfisicalGroupExists(clusterName);
await elastiCache.send(new CreateUserCommand(creationInput)); // First create the user
await addUserToInfisicalGroup(creationInput.UserId); // Then add the user to the group. We know the group is already a part of the cluster because of ensureInfisicalGroupExists()
return {
userId: creationInput.UserId,
password: creationInput.Passwords[0]
};
};
const deleteUser = async (
deletionInput: TDeleteElastiCacheUserInput
): Promise<Pick<TElastiCacheRedisUser, "userId">> => {
await elastiCache.send(new DeleteUserCommand(deletionInput));
return { userId: deletionInput.UserId };
};
const verifyCredentials = async (clusterName: string) => {
await elastiCache.send(
new DescribeReplicationGroupsCommand({
ReplicationGroupId: clusterName
})
);
};
return {
createUser,
deleteUser,
verifyCredentials
};
};
const generatePassword = () => {
const charset = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-_.~!*$#";
return customAlphabet(charset, 64)();
};
const generateUsername = () => {
const charset = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-";
return `inf-${customAlphabet(charset, 32)()}`; // Username must start with an ascii letter, so we prepend the username with "inf-"
};
export const AwsElastiCacheDatabaseProvider = (): TDynamicProviderFns => {
const validateProviderInputs = async (inputs: unknown) => {
const providerInputs = DynamicSecretAwsElastiCacheSchema.parse(inputs);
// We need to ensure the that the creation & revocation statements are valid and can be used to create and revoke users.
// We can't return the parsed statements here because we need to use the handlebars template to generate the username and password, before we can use the parsed statements.
CreateElastiCacheUserSchema.parse(JSON.parse(providerInputs.creationStatement));
DeleteElasticCacheUserSchema.parse(JSON.parse(providerInputs.revocationStatement));
return providerInputs;
};
const validateConnection = async (inputs: unknown) => {
const providerInputs = await validateProviderInputs(inputs);
await ElastiCacheUserManager(
{
accessKeyId: providerInputs.accessKeyId,
secretAccessKey: providerInputs.secretAccessKey
},
providerInputs.region
).verifyCredentials(providerInputs.clusterName);
return true;
};
const create = async (inputs: unknown, expireAt: number) => {
const providerInputs = await validateProviderInputs(inputs);
if (!(await validateConnection(providerInputs))) {
throw new BadRequestError({ message: "Failed to establish connection" });
}
const leaseUsername = generateUsername();
const leasePassword = generatePassword();
const leaseExpiration = new Date(expireAt).toISOString();
const creationStatement = handlebars.compile(providerInputs.creationStatement, { noEscape: true })({
username: leaseUsername,
password: leasePassword,
expiration: leaseExpiration
});
const parsedStatement = CreateElastiCacheUserSchema.parse(JSON.parse(creationStatement));
await ElastiCacheUserManager(
{
accessKeyId: providerInputs.accessKeyId,
secretAccessKey: providerInputs.secretAccessKey
},
providerInputs.region
).createUser(parsedStatement, providerInputs.clusterName);
return {
entityId: leaseUsername,
data: {
DB_USERNAME: leaseUsername,
DB_PASSWORD: leasePassword
}
};
};
const revoke = async (inputs: unknown, entityId: string) => {
const providerInputs = await validateProviderInputs(inputs);
const revokeStatement = handlebars.compile(providerInputs.revocationStatement)({ username: entityId });
const parsedStatement = DeleteElasticCacheUserSchema.parse(JSON.parse(revokeStatement));
await ElastiCacheUserManager(
{
accessKeyId: providerInputs.accessKeyId,
secretAccessKey: providerInputs.secretAccessKey
},
providerInputs.region
).deleteUser(parsedStatement);
return { entityId };
};
const renew = async (inputs: unknown, entityId: string) => {
// Do nothing
return { entityId };
};
return {
validateProviderInputs,
validateConnection,
create,
revoke,
renew
};
};

View File

@@ -0,0 +1,126 @@
import { Client as ElasticSearchClient } from "@elastic/elasticsearch";
import { customAlphabet } from "nanoid";
import { z } from "zod";
import { getConfig } from "@app/lib/config/env";
import { BadRequestError } from "@app/lib/errors";
import { alphaNumericNanoId } from "@app/lib/nanoid";
import { DynamicSecretElasticSearchSchema, ElasticSearchAuthTypes, TDynamicProviderFns } from "./models";
const generatePassword = () => {
const charset = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-_.~!*$#";
return customAlphabet(charset, 64)();
};
const generateUsername = () => {
return alphaNumericNanoId(32);
};
export const ElasticSearchDatabaseProvider = (): TDynamicProviderFns => {
const validateProviderInputs = async (inputs: unknown) => {
const appCfg = getConfig();
const isCloud = Boolean(appCfg.LICENSE_SERVER_KEY); // quick and dirty way to check if its cloud or not
const providerInputs = await DynamicSecretElasticSearchSchema.parseAsync(inputs);
if (
isCloud &&
// localhost
// internal ips
(providerInputs.host === "host.docker.internal" ||
providerInputs.host.match(/^10\.\d+\.\d+\.\d+/) ||
providerInputs.host.match(/^192\.168\.\d+\.\d+/))
) {
throw new BadRequestError({ message: "Invalid db host" });
}
if (providerInputs.host === "localhost" || providerInputs.host === "127.0.0.1") {
throw new BadRequestError({ message: "Invalid db host" });
}
return providerInputs;
};
const getClient = async (providerInputs: z.infer<typeof DynamicSecretElasticSearchSchema>) => {
const connection = new ElasticSearchClient({
node: {
url: new URL(`${providerInputs.host}:${providerInputs.port}`),
...(providerInputs.ca && {
ssl: {
rejectUnauthorized: false,
ca: providerInputs.ca
}
})
},
auth: {
...(providerInputs.auth.type === ElasticSearchAuthTypes.ApiKey
? {
apiKey: {
api_key: providerInputs.auth.apiKey,
id: providerInputs.auth.apiKeyId
}
}
: {
username: providerInputs.auth.username,
password: providerInputs.auth.password
})
}
});
return connection;
};
const validateConnection = async (inputs: unknown) => {
const providerInputs = await validateProviderInputs(inputs);
const connection = await getClient(providerInputs);
const infoResponse = await connection
.info()
.then(() => true)
.catch(() => false);
return infoResponse;
};
const create = async (inputs: unknown) => {
const providerInputs = await validateProviderInputs(inputs);
const connection = await getClient(providerInputs);
const username = generateUsername();
const password = generatePassword();
await connection.security.putUser({
username,
password,
full_name: "Managed by Infisical.com",
roles: providerInputs.roles
});
await connection.close();
return { entityId: username, data: { DB_USERNAME: username, DB_PASSWORD: password } };
};
const revoke = async (inputs: unknown, entityId: string) => {
const providerInputs = await validateProviderInputs(inputs);
const connection = await getClient(providerInputs);
await connection.security.deleteUser({
username: entityId
});
await connection.close();
return { entityId };
};
const renew = async (inputs: unknown, entityId: string) => {
// Do nothing
return { entityId };
};
return {
validateProviderInputs,
validateConnection,
create,
revoke,
renew
};
};

View File

@@ -1,10 +1,18 @@
import { AwsElastiCacheDatabaseProvider } from "./aws-elasticache";
import { AwsIamProvider } from "./aws-iam";
import { CassandraProvider } from "./cassandra";
import { ElasticSearchDatabaseProvider } from "./elastic-search";
import { DynamicSecretProviders } from "./models";
import { MongoAtlasProvider } from "./mongo-atlas";
import { RedisDatabaseProvider } from "./redis";
import { SqlDatabaseProvider } from "./sql-database";
export const buildDynamicSecretProviders = () => ({
[DynamicSecretProviders.SqlDatabase]: SqlDatabaseProvider(),
[DynamicSecretProviders.Cassandra]: CassandraProvider(),
[DynamicSecretProviders.AwsIam]: AwsIamProvider()
[DynamicSecretProviders.AwsIam]: AwsIamProvider(),
[DynamicSecretProviders.Redis]: RedisDatabaseProvider(),
[DynamicSecretProviders.AwsElastiCache]: AwsElastiCacheDatabaseProvider(),
[DynamicSecretProviders.MongoAtlas]: MongoAtlasProvider(),
[DynamicSecretProviders.ElasticSearch]: ElasticSearchDatabaseProvider()
});

View File

@@ -7,6 +7,56 @@ export enum SqlProviders {
MsSQL = "mssql"
}
export enum ElasticSearchAuthTypes {
User = "user",
ApiKey = "api-key"
}
export const DynamicSecretRedisDBSchema = z.object({
host: z.string().trim().toLowerCase(),
port: z.number(),
username: z.string().trim(), // this is often "default".
password: z.string().trim().optional(),
creationStatement: z.string().trim(),
revocationStatement: z.string().trim(),
renewStatement: z.string().trim().optional(),
ca: z.string().optional()
});
export const DynamicSecretAwsElastiCacheSchema = z.object({
clusterName: z.string().trim().min(1),
accessKeyId: z.string().trim().min(1),
secretAccessKey: z.string().trim().min(1),
region: z.string().trim(),
creationStatement: z.string().trim(),
revocationStatement: z.string().trim(),
ca: z.string().optional()
});
export const DynamicSecretElasticSearchSchema = z.object({
host: z.string().trim().min(1),
port: z.number(),
roles: z.array(z.string().trim().min(1)).min(1),
// two auth types "user, apikey"
auth: z.discriminatedUnion("type", [
z.object({
type: z.literal(ElasticSearchAuthTypes.User),
username: z.string().trim(),
password: z.string().trim()
}),
z.object({
type: z.literal(ElasticSearchAuthTypes.ApiKey),
apiKey: z.string().trim(),
apiKeyId: z.string().trim()
})
]),
ca: z.string().optional()
});
export const DynamicSecretSqlDBSchema = z.object({
client: z.nativeEnum(SqlProviders),
host: z.string().trim().toLowerCase(),
@@ -44,16 +94,61 @@ export const DynamicSecretAwsIamSchema = z.object({
policyArns: z.string().trim().optional()
});
export const DynamicSecretMongoAtlasSchema = z.object({
adminPublicKey: z.string().trim().min(1).describe("Admin user public api key"),
adminPrivateKey: z.string().trim().min(1).describe("Admin user private api key"),
groupId: z
.string()
.trim()
.min(1)
.describe("Unique 24-hexadecimal digit string that identifies your project. This is same as project id"),
roles: z
.object({
collectionName: z.string().optional().describe("Collection on which this role applies."),
databaseName: z.string().min(1).describe("Database to which the user is granted access privileges."),
roleName: z
.string()
.min(1)
.describe(
' Enum: "atlasAdmin" "backup" "clusterMonitor" "dbAdmin" "dbAdminAnyDatabase" "enableSharding" "read" "readAnyDatabase" "readWrite" "readWriteAnyDatabase" "<a custom role name>".Human-readable label that identifies a group of privileges assigned to a database user. This value can either be a built-in role or a custom role.'
)
})
.array()
.min(1),
scopes: z
.object({
name: z
.string()
.min(1)
.describe(
"Human-readable label that identifies the cluster or MongoDB Atlas Data Lake that this database user can access."
),
type: z
.string()
.min(1)
.describe("Category of resource that this database user can access. Enum: CLUSTER, DATA_LAKE, STREAM")
})
.array()
});
export enum DynamicSecretProviders {
SqlDatabase = "sql-database",
Cassandra = "cassandra",
AwsIam = "aws-iam"
AwsIam = "aws-iam",
Redis = "redis",
AwsElastiCache = "aws-elasticache",
MongoAtlas = "mongo-db-atlas",
ElasticSearch = "elastic-search"
}
export const DynamicSecretProviderSchema = z.discriminatedUnion("type", [
z.object({ type: z.literal(DynamicSecretProviders.SqlDatabase), inputs: DynamicSecretSqlDBSchema }),
z.object({ type: z.literal(DynamicSecretProviders.Cassandra), inputs: DynamicSecretCassandraSchema }),
z.object({ type: z.literal(DynamicSecretProviders.AwsIam), inputs: DynamicSecretAwsIamSchema })
z.object({ type: z.literal(DynamicSecretProviders.AwsIam), inputs: DynamicSecretAwsIamSchema }),
z.object({ type: z.literal(DynamicSecretProviders.Redis), inputs: DynamicSecretRedisDBSchema }),
z.object({ type: z.literal(DynamicSecretProviders.AwsElastiCache), inputs: DynamicSecretAwsElastiCacheSchema }),
z.object({ type: z.literal(DynamicSecretProviders.MongoAtlas), inputs: DynamicSecretMongoAtlasSchema }),
z.object({ type: z.literal(DynamicSecretProviders.ElasticSearch), inputs: DynamicSecretElasticSearchSchema })
]);
export type TDynamicProviderFns = {

View File

@@ -0,0 +1,146 @@
import axios, { AxiosError } from "axios";
import { customAlphabet } from "nanoid";
import { z } from "zod";
import { createDigestAuthRequestInterceptor } from "@app/lib/axios/digest-auth";
import { alphaNumericNanoId } from "@app/lib/nanoid";
import { DynamicSecretMongoAtlasSchema, TDynamicProviderFns } from "./models";
const generatePassword = (size = 48) => {
const charset = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-_.~!*$#";
return customAlphabet(charset, 48)(size);
};
const generateUsername = () => {
return alphaNumericNanoId(32);
};
export const MongoAtlasProvider = (): TDynamicProviderFns => {
const validateProviderInputs = async (inputs: unknown) => {
const providerInputs = await DynamicSecretMongoAtlasSchema.parseAsync(inputs);
return providerInputs;
};
const getClient = async (providerInputs: z.infer<typeof DynamicSecretMongoAtlasSchema>) => {
const client = axios.create({
baseURL: "https://cloud.mongodb.com/api/atlas",
headers: {
Accept: "application/vnd.atlas.2023-02-01+json",
"Content-Type": "application/json"
}
});
const digestAuth = createDigestAuthRequestInterceptor(
client,
providerInputs.adminPublicKey,
providerInputs.adminPrivateKey
);
return digestAuth;
};
const validateConnection = async (inputs: unknown) => {
const providerInputs = await validateProviderInputs(inputs);
const client = await getClient(providerInputs);
const isConnected = await client({
method: "GET",
url: `v2/groups/${providerInputs.groupId}/databaseUsers`,
params: { itemsPerPage: 1 }
})
.then(() => true)
.catch((error) => {
if ((error as AxiosError).response) {
throw new Error(JSON.stringify((error as AxiosError).response?.data));
}
throw error;
});
return isConnected;
};
const create = async (inputs: unknown, expireAt: number) => {
const providerInputs = await validateProviderInputs(inputs);
const client = await getClient(providerInputs);
const username = generateUsername();
const password = generatePassword();
const expiration = new Date(expireAt).toISOString();
await client({
method: "POST",
url: `/v2/groups/${providerInputs.groupId}/databaseUsers`,
data: {
roles: providerInputs.roles,
scopes: providerInputs.scopes,
deleteAfterDate: expiration,
username,
password,
databaseName: "admin",
groupId: providerInputs.groupId
}
}).catch((error) => {
if ((error as AxiosError).response) {
throw new Error(JSON.stringify((error as AxiosError).response?.data));
}
throw error;
});
return { entityId: username, data: { DB_USERNAME: username, DB_PASSWORD: password } };
};
const revoke = async (inputs: unknown, entityId: string) => {
const providerInputs = await validateProviderInputs(inputs);
const client = await getClient(providerInputs);
const username = entityId;
const isExisting = await client({
method: "GET",
url: `/v2/groups/${providerInputs.groupId}/databaseUsers/admin/${username}`
}).catch((err) => {
if ((err as AxiosError).response?.status === 404) return false;
throw err;
});
if (isExisting) {
await client({
method: "DELETE",
url: `/v2/groups/${providerInputs.groupId}/databaseUsers/admin/${username}`
}).catch((error) => {
if ((error as AxiosError).response) {
throw new Error(JSON.stringify((error as AxiosError).response?.data));
}
throw error;
});
}
return { entityId: username };
};
const renew = async (inputs: unknown, entityId: string, expireAt: number) => {
const providerInputs = await validateProviderInputs(inputs);
const client = await getClient(providerInputs);
const username = entityId;
const expiration = new Date(expireAt).toISOString();
await client({
method: "PATCH",
url: `/v2/groups/${providerInputs.groupId}/databaseUsers/admin/${username}`,
data: {
deleteAfterDate: expiration,
databaseName: "admin",
groupId: providerInputs.groupId
}
}).catch((error) => {
if ((error as AxiosError).response) {
throw new Error(JSON.stringify((error as AxiosError).response?.data));
}
throw error;
});
return { entityId: username };
};
return {
validateProviderInputs,
validateConnection,
create,
revoke,
renew
};
};

View File

@@ -0,0 +1,182 @@
import handlebars from "handlebars";
import { Redis } from "ioredis";
import { customAlphabet } from "nanoid";
import { z } from "zod";
import { getConfig } from "@app/lib/config/env";
import { BadRequestError } from "@app/lib/errors";
import { getDbConnectionHost } from "@app/lib/knex";
import { alphaNumericNanoId } from "@app/lib/nanoid";
import { DynamicSecretRedisDBSchema, TDynamicProviderFns } from "./models";
const generatePassword = () => {
const charset = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-_.~!*$#";
return customAlphabet(charset, 64)();
};
const generateUsername = () => {
return alphaNumericNanoId(32);
};
const executeTransactions = async (connection: Redis, commands: string[]): Promise<(string | null)[] | null> => {
// Initiate a transaction
const pipeline = connection.multi();
// Add all commands to the pipeline
for (const command of commands) {
const args = command
.split(" ")
.map((arg) => arg.trim())
.filter((arg) => arg.length > 0);
pipeline.call(args[0], ...args.slice(1));
}
// Execute the transaction
const results = await pipeline.exec();
if (!results) {
throw new BadRequestError({ message: "Redis transaction failed: No results returned" });
}
// Check for errors in the results
const errors = results.filter(([err]) => err !== null);
if (errors.length > 0) {
throw new BadRequestError({ message: "Redis transaction failed with errors" });
}
// eslint-disable-next-line @typescript-eslint/no-unused-vars
return results.map(([_, result]) => result as string | null);
};
export const RedisDatabaseProvider = (): TDynamicProviderFns => {
const validateProviderInputs = async (inputs: unknown) => {
const appCfg = getConfig();
const isCloud = Boolean(appCfg.LICENSE_SERVER_KEY); // quick and dirty way to check if its cloud or not
const dbHost = appCfg.DB_HOST || getDbConnectionHost(appCfg.DB_CONNECTION_URI);
const providerInputs = await DynamicSecretRedisDBSchema.parseAsync(inputs);
if (
isCloud &&
// localhost
// internal ips
(providerInputs.host === "host.docker.internal" ||
providerInputs.host.match(/^10\.\d+\.\d+\.\d+/) ||
providerInputs.host.match(/^192\.168\.\d+\.\d+/))
)
throw new BadRequestError({ message: "Invalid db host" });
if (providerInputs.host === "localhost" || providerInputs.host === "127.0.0.1" || dbHost === providerInputs.host)
throw new BadRequestError({ message: "Invalid db host" });
return providerInputs;
};
const getClient = async (providerInputs: z.infer<typeof DynamicSecretRedisDBSchema>) => {
let connection: Redis | null = null;
try {
connection = new Redis({
username: providerInputs.username,
host: providerInputs.host,
port: providerInputs.port,
password: providerInputs.password,
...(providerInputs.ca && {
tls: {
rejectUnauthorized: false,
ca: providerInputs.ca
}
})
});
let result: string;
if (providerInputs.password) {
result = await connection.auth(providerInputs.username, providerInputs.password, () => {});
} else {
result = await connection.auth(providerInputs.username, () => {});
}
if (result !== "OK") {
throw new BadRequestError({ message: `Invalid credentials, Redis returned ${result} status` });
}
return connection;
} catch (err) {
if (connection) await connection.quit();
throw err;
}
};
const validateConnection = async (inputs: unknown) => {
const providerInputs = await validateProviderInputs(inputs);
const connection = await getClient(providerInputs);
const pingResponse = await connection
.ping()
.then(() => true)
.catch(() => false);
return pingResponse;
};
const create = async (inputs: unknown, expireAt: number) => {
const providerInputs = await validateProviderInputs(inputs);
const connection = await getClient(providerInputs);
const username = generateUsername();
const password = generatePassword();
const expiration = new Date(expireAt).toISOString();
const creationStatement = handlebars.compile(providerInputs.creationStatement, { noEscape: true })({
username,
password,
expiration
});
const queries = creationStatement.toString().split(";").filter(Boolean);
await executeTransactions(connection, queries);
await connection.quit();
return { entityId: username, data: { DB_USERNAME: username, DB_PASSWORD: password } };
};
const revoke = async (inputs: unknown, entityId: string) => {
const providerInputs = await validateProviderInputs(inputs);
const connection = await getClient(providerInputs);
const username = entityId;
const revokeStatement = handlebars.compile(providerInputs.revocationStatement)({ username });
const queries = revokeStatement.toString().split(";").filter(Boolean);
await executeTransactions(connection, queries);
await connection.quit();
return { entityId: username };
};
const renew = async (inputs: unknown, entityId: string, expireAt: number) => {
const providerInputs = await validateProviderInputs(inputs);
const connection = await getClient(providerInputs);
const username = entityId;
const expiration = new Date(expireAt).toISOString();
const renewStatement = handlebars.compile(providerInputs.renewStatement)({ username, expiration });
if (renewStatement) {
const queries = renewStatement.toString().split(";").filter(Boolean);
await executeTransactions(connection, queries);
}
await connection.quit();
return { entityId: username };
};
return {
validateProviderInputs,
validateConnection,
create,
revoke,
renew
};
};

View File

@@ -26,8 +26,10 @@ export const getDefaultOnPremFeatures = () => {
status: null,
trial_end: null,
has_used_trial: true,
secretApproval: false,
secretApproval: true,
secretRotation: true,
caCrl: false
};
};
export const setupLicenseRequestWithStore = () => {};

View File

@@ -45,18 +45,19 @@ export const getDefaultOnPremFeatures = (): TFeatureSet => ({
readLimit: 60,
writeLimit: 200,
secretsLimit: 40
}
},
pkiEst: false
});
export const setupLicenceRequestWithStore = (baseURL: string, refreshUrl: string, licenseKey: string) => {
export const setupLicenseRequestWithStore = (baseURL: string, refreshUrl: string, licenseKey: string) => {
let token: string;
const licenceReq = axios.create({
const licenseReq = axios.create({
baseURL,
timeout: 35 * 1000
// signal: AbortSignal.timeout(60 * 1000)
});
const refreshLicence = async () => {
const refreshLicense = async () => {
const appCfg = getConfig();
const {
data: { token: authToken }
@@ -74,7 +75,7 @@ export const setupLicenceRequestWithStore = (baseURL: string, refreshUrl: string
return token;
};
licenceReq.interceptors.request.use(
licenseReq.interceptors.request.use(
(config) => {
if (token && config.headers) {
// eslint-disable-next-line no-param-reassign
@@ -85,7 +86,7 @@ export const setupLicenceRequestWithStore = (baseURL: string, refreshUrl: string
(err) => Promise.reject(err)
);
licenceReq.interceptors.response.use(
licenseReq.interceptors.response.use(
(response) => response,
async (err) => {
const originalRequest = (err as AxiosError).config;
@@ -96,15 +97,15 @@ export const setupLicenceRequestWithStore = (baseURL: string, refreshUrl: string
(originalRequest as any)._retry = true; // injected
// refresh
await refreshLicence();
await refreshLicense();
licenceReq.defaults.headers.common.Authorization = `Bearer ${token}`;
return licenceReq(originalRequest!);
licenseReq.defaults.headers.common.Authorization = `Bearer ${token}`;
return licenseReq(originalRequest!);
}
return Promise.reject(err);
}
);
return { request: licenceReq, refreshLicence };
return { request: licenseReq, refreshLicense };
};

View File

@@ -16,8 +16,8 @@ import { TOrgDALFactory } from "@app/services/org/org-dal";
import { OrgPermissionActions, OrgPermissionSubjects } from "../permission/org-permission";
import { TPermissionServiceFactory } from "../permission/permission-service";
import { getDefaultOnPremFeatures, setupLicenceRequestWithStore } from "./licence-fns";
import { TLicenseDALFactory } from "./license-dal";
import { getDefaultOnPremFeatures, setupLicenseRequestWithStore } from "./license-fns";
import {
InstanceType,
TAddOrgPmtMethodDTO,
@@ -64,13 +64,13 @@ export const licenseServiceFactory = ({
let onPremFeatures: TFeatureSet = getDefaultOnPremFeatures();
const appCfg = getConfig();
const licenseServerCloudApi = setupLicenceRequestWithStore(
const licenseServerCloudApi = setupLicenseRequestWithStore(
appCfg.LICENSE_SERVER_URL || "",
LICENSE_SERVER_CLOUD_LOGIN,
appCfg.LICENSE_SERVER_KEY || ""
);
const licenseServerOnPremApi = setupLicenceRequestWithStore(
const licenseServerOnPremApi = setupLicenseRequestWithStore(
appCfg.LICENSE_SERVER_URL || "",
LICENSE_SERVER_ON_PREM_LOGIN,
appCfg.LICENSE_KEY || ""
@@ -79,7 +79,7 @@ export const licenseServiceFactory = ({
const init = async () => {
try {
if (appCfg.LICENSE_SERVER_KEY) {
const token = await licenseServerCloudApi.refreshLicence();
const token = await licenseServerCloudApi.refreshLicense();
if (token) instanceType = InstanceType.Cloud;
logger.info(`Instance type: ${InstanceType.Cloud}`);
isValidLicense = true;
@@ -87,7 +87,7 @@ export const licenseServiceFactory = ({
}
if (appCfg.LICENSE_KEY) {
const token = await licenseServerOnPremApi.refreshLicence();
const token = await licenseServerOnPremApi.refreshLicense();
if (token) {
const {
data: { currentPlan }

View File

@@ -63,6 +63,7 @@ export type TFeatureSet = {
writeLimit: number;
secretsLimit: number;
};
pkiEst: boolean;
};
export type TOrgPlansTableDTO = {

View File

@@ -44,19 +44,18 @@ export const buildScimUser = ({
email,
firstName,
lastName,
groups = [],
active
active,
createdAt,
updatedAt
}: {
orgMembershipId: string;
username: string;
email?: string | null;
firstName: string;
lastName: string;
groups?: {
value: string;
display: string;
}[];
firstName: string | null | undefined;
lastName: string | null | undefined;
active: boolean;
createdAt: Date;
updatedAt: Date;
}): TScimUser => {
const scimUser = {
schemas: ["urn:ietf:params:scim:schemas:core:2.0:User"],
@@ -64,9 +63,9 @@ export const buildScimUser = ({
userName: username,
displayName: `${firstName} ${lastName}`,
name: {
givenName: firstName,
givenName: firstName || "",
middleName: null,
familyName: lastName
familyName: lastName || ""
},
emails: email
? [
@@ -78,10 +77,10 @@ export const buildScimUser = ({
]
: [],
active,
groups,
meta: {
resourceType: "User",
location: null
created: createdAt,
lastModified: updatedAt
}
};
@@ -109,14 +108,18 @@ export const buildScimGroupList = ({
export const buildScimGroup = ({
groupId,
name,
members
members,
updatedAt,
createdAt
}: {
groupId: string;
name: string;
members: {
value: string;
display: string;
display?: string;
}[];
createdAt: Date;
updatedAt: Date;
}): TScimGroup => {
const scimGroup = {
schemas: ["urn:ietf:params:scim:schemas:core:2.0:Group"],
@@ -125,7 +128,8 @@ export const buildScimGroup = ({
members,
meta: {
resourceType: "Group",
location: null
created: createdAt,
lastModified: updatedAt
}
};

View File

@@ -1,6 +1,7 @@
import { ForbiddenError } from "@casl/ability";
import slugify from "@sindresorhus/slugify";
import jwt from "jsonwebtoken";
import { scimPatch } from "scim-patch";
import { OrgMembershipRole, OrgMembershipStatus, TableName, TOrgMemberships, TUsers } from "@app/db/schemas";
import { TGroupDALFactory } from "@app/ee/services/group/group-dal";
@@ -9,7 +10,6 @@ import { TUserGroupMembershipDALFactory } from "@app/ee/services/group/user-grou
import { TScimDALFactory } from "@app/ee/services/scim/scim-dal";
import { getConfig } from "@app/lib/config/env";
import { BadRequestError, ScimRequestError, UnauthorizedError } from "@app/lib/errors";
import { logger } from "@app/lib/logger";
import { alphaNumericNanoId } from "@app/lib/nanoid";
import { TOrgPermission } from "@app/lib/types";
import { AuthTokenType } from "@app/services/auth/auth-type";
@@ -32,14 +32,7 @@ import { TLicenseServiceFactory } from "../license/license-service";
import { OrgPermissionActions, OrgPermissionSubjects } from "../permission/org-permission";
import { TPermissionServiceFactory } from "../permission/permission-service";
import { TProjectUserAdditionalPrivilegeDALFactory } from "../project-user-additional-privilege/project-user-additional-privilege-dal";
import {
buildScimGroup,
buildScimGroupList,
buildScimUser,
buildScimUserList,
extractScimValueFromPath,
parseScimFilter
} from "./scim-fns";
import { buildScimGroup, buildScimGroupList, buildScimUser, buildScimUserList, parseScimFilter } from "./scim-fns";
import {
TCreateScimGroupDTO,
TCreateScimTokenDTO,
@@ -64,12 +57,18 @@ type TScimServiceFactoryDep = {
scimDAL: Pick<TScimDALFactory, "create" | "find" | "findById" | "deleteById">;
userDAL: Pick<
TUserDALFactory,
"find" | "findOne" | "create" | "transaction" | "findUserEncKeyByUserIdsBatch" | "findById"
"find" | "findOne" | "create" | "transaction" | "findUserEncKeyByUserIdsBatch" | "findById" | "updateById"
>;
userAliasDAL: Pick<TUserAliasDALFactory, "findOne" | "create" | "delete">;
userAliasDAL: Pick<TUserAliasDALFactory, "findOne" | "create" | "delete" | "update">;
orgDAL: Pick<
TOrgDALFactory,
"createMembership" | "findById" | "findMembership" | "deleteMembershipById" | "transaction" | "updateMembershipById"
| "createMembership"
| "findById"
| "findMembership"
| "findMembershipWithScimFilter"
| "deleteMembershipById"
| "transaction"
| "updateMembershipById"
>;
orgMembershipDAL: Pick<TOrgMembershipDALFactory, "find" | "findOne" | "create" | "updateById" | "findById">;
projectDAL: Pick<TProjectDALFactory, "find" | "findProjectGhostUser">;
@@ -193,7 +192,12 @@ export const scimServiceFactory = ({
};
// SCIM server endpoints
const listScimUsers = async ({ startIndex, limit, filter, orgId }: TListScimUsersDTO): Promise<TListScimUsers> => {
const listScimUsers = async ({
startIndex = 0,
limit = 100,
filter,
orgId
}: TListScimUsersDTO): Promise<TListScimUsers> => {
const org = await orgDAL.findById(orgId);
if (!org.scimEnabled)
@@ -207,23 +211,20 @@ export const scimServiceFactory = ({
...(limit && { limit })
};
const users = await orgDAL.findMembership(
{
[`${TableName.OrgMembership}.orgId` as "id"]: orgId,
...parseScimFilter(filter)
},
findOpts
);
const users = await orgDAL.findMembershipWithScimFilter(orgId, filter, findOpts);
const scimUsers = users.map(({ id, externalId, username, firstName, lastName, email, isActive }) =>
buildScimUser({
orgMembershipId: id ?? "",
username: externalId ?? username,
firstName: firstName ?? "",
lastName: lastName ?? "",
email,
active: isActive
})
const scimUsers = users.map(
({ id, externalId, username, firstName, lastName, email, isActive, createdAt, updatedAt }) =>
buildScimUser({
orgMembershipId: id ?? "",
username: externalId ?? username,
firstName: firstName ?? "",
lastName: lastName ?? "",
email,
active: isActive,
createdAt,
updatedAt
})
);
return buildScimUserList({
@@ -258,22 +259,15 @@ export const scimServiceFactory = ({
status: 403
});
const groupMembershipsInOrg = await userGroupMembershipDAL.findGroupMembershipsByUserIdInOrg(
membership.userId,
orgId
);
return buildScimUser({
orgMembershipId: membership.id,
username: membership.externalId ?? membership.username,
email: membership.email ?? "",
firstName: membership.firstName as string,
lastName: membership.lastName as string,
firstName: membership.firstName,
lastName: membership.lastName,
active: membership.isActive,
groups: groupMembershipsInOrg.map((group) => ({
value: group.groupId,
display: group.groupName
}))
createdAt: membership.createdAt,
updatedAt: membership.updatedAt
});
};
@@ -322,7 +316,7 @@ export const scimServiceFactory = ({
userId: userAlias.userId,
inviteEmail: email,
orgId,
role: OrgMembershipRole.Member,
role: OrgMembershipRole.NoAccess,
status: user.isAccepted ? OrgMembershipStatus.Accepted : OrgMembershipStatus.Invited, // if user is fully completed, then set status to accepted, otherwise set it to invited so we can update it later
isActive: true
},
@@ -349,7 +343,11 @@ export const scimServiceFactory = ({
}
if (!user) {
const uniqueUsername = await normalizeUsername(`${firstName}-${lastName}`, userDAL);
const uniqueUsername = await normalizeUsername(
// external id is username
`${firstName}-${lastName}`,
userDAL
);
user = await userDAL.create(
{
username: serverCfg.trustSamlEmails ? email : uniqueUsername,
@@ -427,13 +425,16 @@ export const scimServiceFactory = ({
return buildScimUser({
orgMembershipId: createdOrgMembership.id,
username: externalId,
firstName: createdUser.firstName as string,
lastName: createdUser.lastName as string,
firstName: createdUser.firstName,
lastName: createdUser.lastName,
email: createdUser.email ?? "",
active: createdOrgMembership.isActive
active: createdOrgMembership.isActive,
createdAt: createdOrgMembership.createdAt,
updatedAt: createdOrgMembership.updatedAt
});
};
// partial
const updateScimUser = async ({ orgMembershipId, orgId, operations }: TUpdateScimUserDTO) => {
const [membership] = await orgDAL
.findMembership({
@@ -459,37 +460,52 @@ export const scimServiceFactory = ({
status: 403
});
let active = true;
operations.forEach((operation) => {
if (operation.op.toLowerCase() === "replace") {
if (operation.path === "active" && operation.value === "False") {
// azure scim op format
active = false;
} else if (typeof operation.value === "object" && operation.value.active === false) {
// okta scim op format
active = false;
}
}
});
if (!active) {
await orgMembershipDAL.updateById(membership.id, {
isActive: false
});
}
return buildScimUser({
const scimUser = buildScimUser({
orgMembershipId: membership.id,
username: membership.externalId ?? membership.username,
email: membership.email,
firstName: membership.firstName as string,
lastName: membership.lastName as string,
active
lastName: membership.lastName,
firstName: membership.firstName,
active: membership.isActive,
username: membership.externalId ?? membership.username,
createdAt: membership.createdAt,
updatedAt: membership.updatedAt
});
scimPatch(scimUser, operations);
const serverCfg = await getServerCfg();
await userDAL.transaction(async (tx) => {
await orgMembershipDAL.updateById(
membership.id,
{
isActive: scimUser.active
},
tx
);
const hasEmailChanged = scimUser.emails[0].value !== membership.email;
await userDAL.updateById(
membership.userId,
{
firstName: scimUser.name.givenName,
email: scimUser.emails[0].value,
lastName: scimUser.name.familyName,
isEmailVerified: hasEmailChanged ? serverCfg.trustSamlEmails : true
},
tx
);
});
return scimUser;
};
const replaceScimUser = async ({ orgMembershipId, active, orgId }: TReplaceScimUserDTO) => {
const replaceScimUser = async ({
orgMembershipId,
active,
orgId,
lastName,
firstName,
email,
externalId
}: TReplaceScimUserDTO) => {
const [membership] = await orgDAL
.findMembership({
[`${TableName.OrgMembership}.id` as "id"]: orgMembershipId,
@@ -514,26 +530,47 @@ export const scimServiceFactory = ({
status: 403
});
await orgMembershipDAL.updateById(membership.id, {
isActive: active
const serverCfg = await getServerCfg();
await userDAL.transaction(async (tx) => {
await userAliasDAL.update(
{
orgId,
aliasType: UserAliasType.SAML,
userId: membership.userId
},
{
externalId
},
tx
);
await orgMembershipDAL.updateById(
membership.id,
{
isActive: active
},
tx
);
await userDAL.updateById(
membership.userId,
{
firstName,
email,
lastName,
isEmailVerified: serverCfg.trustSamlEmails
},
tx
);
});
const groupMembershipsInOrg = await userGroupMembershipDAL.findGroupMembershipsByUserIdInOrg(
membership.userId,
orgId
);
return buildScimUser({
orgMembershipId: membership.id,
username: membership.externalId ?? membership.username,
username: externalId,
email: membership.email,
firstName: membership.firstName as string,
lastName: membership.lastName as string,
firstName: membership.firstName,
lastName: membership.lastName,
active,
groups: groupMembershipsInOrg.map((group) => ({
value: group.groupId,
display: group.groupName
}))
createdAt: membership.createdAt,
updatedAt: membership.updatedAt
});
};
@@ -570,7 +607,7 @@ export const scimServiceFactory = ({
return {}; // intentionally return empty object upon success
};
const listScimGroups = async ({ orgId, startIndex, limit, filter }: TListScimGroupsDTO) => {
const listScimGroups = async ({ orgId, startIndex, limit, filter, isMembersExcluded }: TListScimGroupsDTO) => {
const plan = await licenseService.getPlan(orgId);
if (!plan.groups)
throw new BadRequestError({
@@ -603,6 +640,21 @@ export const scimServiceFactory = ({
);
const scimGroups: TScimGroup[] = [];
if (isMembersExcluded) {
return buildScimGroupList({
scimGroups: groups.map((group) =>
buildScimGroup({
groupId: group.id,
name: group.name,
members: [],
createdAt: group.createdAt,
updatedAt: group.updatedAt
})
),
startIndex,
limit
});
}
for await (const group of groups) {
const members = await userGroupMembershipDAL.findGroupMembershipsByGroupIdInOrg(group.id, orgId);
@@ -612,7 +664,9 @@ export const scimServiceFactory = ({
members: members.map((member) => ({
value: member.orgMembershipId,
display: `${member.firstName ?? ""} ${member.lastName ?? ""}`
}))
})),
createdAt: group.createdAt,
updatedAt: group.updatedAt
});
scimGroups.push(scimGroup);
}
@@ -696,7 +750,9 @@ export const scimServiceFactory = ({
members: orgMemberships.map(({ id, firstName, lastName }) => ({
value: id,
display: `${firstName} ${lastName}`
}))
})),
createdAt: newGroup.group.createdAt,
updatedAt: newGroup.group.updatedAt
});
};
@@ -739,31 +795,17 @@ export const scimServiceFactory = ({
members: orgMemberships.map(({ id, firstName, lastName }) => ({
value: id,
display: `${firstName} ${lastName}`
}))
})),
createdAt: group.createdAt,
updatedAt: group.updatedAt
});
};
const updateScimGroupNamePut = async ({ groupId, orgId, displayName, members }: TUpdateScimGroupNamePutDTO) => {
const plan = await licenseService.getPlan(orgId);
if (!plan.groups)
throw new BadRequestError({
message: "Failed to update SCIM group due to plan restriction. Upgrade plan to update SCIM group."
});
const org = await orgDAL.findById(orgId);
if (!org) {
throw new ScimRequestError({
detail: "Organization Not Found",
status: 404
});
}
if (!org.scimEnabled)
throw new ScimRequestError({
detail: "SCIM is disabled for the organization",
status: 403
});
const $replaceGroupDAL = async (
groupId: string,
orgId: string,
{ displayName, members = [] }: { displayName: string; members: { value: string }[] }
) => {
const updatedGroup = await groupDAL.transaction(async (tx) => {
const [group] = await groupDAL.update(
{
@@ -782,74 +824,96 @@ export const scimServiceFactory = ({
});
}
if (members) {
const orgMemberships = await orgMembershipDAL.find({
$in: {
id: members.map((member) => member.value)
}
const orgMemberships = members.length
? await orgMembershipDAL.find({
$in: {
id: members.map((member) => member.value)
}
})
: [];
const membersIdsSet = new Set(orgMemberships.map((orgMembership) => orgMembership.userId));
const userGroupMembers = await userGroupMembershipDAL.find({
groupId: group.id
});
const directMemberUserIds = userGroupMembers.filter((el) => !el.isPending).map((membership) => membership.userId);
const pendingGroupAdditionsUserIds = userGroupMembers
.filter((el) => el.isPending)
.map((pendingGroupAddition) => pendingGroupAddition.userId);
const allMembersUserIds = directMemberUserIds.concat(pendingGroupAdditionsUserIds);
const allMembersUserIdsSet = new Set(allMembersUserIds);
const toAddUserIds = orgMemberships.filter((member) => !allMembersUserIdsSet.has(member.userId as string));
const toRemoveUserIds = allMembersUserIds.filter((userId) => !membersIdsSet.has(userId));
if (toAddUserIds.length) {
await addUsersToGroupByUserIds({
group,
userIds: toAddUserIds.map((member) => member.userId as string),
userDAL,
userGroupMembershipDAL,
orgDAL,
groupProjectDAL,
projectKeyDAL,
projectDAL,
projectBotDAL,
tx
});
}
const membersIdsSet = new Set(orgMemberships.map((orgMembership) => orgMembership.userId));
const directMemberUserIds = (
await userGroupMembershipDAL.find({
groupId: group.id,
isPending: false
})
).map((membership) => membership.userId);
const pendingGroupAdditionsUserIds = (
await userGroupMembershipDAL.find({
groupId: group.id,
isPending: true
})
).map((pendingGroupAddition) => pendingGroupAddition.userId);
const allMembersUserIds = directMemberUserIds.concat(pendingGroupAdditionsUserIds);
const allMembersUserIdsSet = new Set(allMembersUserIds);
const toAddUserIds = orgMemberships.filter((member) => !allMembersUserIdsSet.has(member.userId as string));
const toRemoveUserIds = allMembersUserIds.filter((userId) => !membersIdsSet.has(userId));
if (toAddUserIds.length) {
await addUsersToGroupByUserIds({
group,
userIds: toAddUserIds.map((member) => member.userId as string),
userDAL,
userGroupMembershipDAL,
orgDAL,
groupProjectDAL,
projectKeyDAL,
projectDAL,
projectBotDAL,
tx
});
}
if (toRemoveUserIds.length) {
await removeUsersFromGroupByUserIds({
group,
userIds: toRemoveUserIds,
userDAL,
userGroupMembershipDAL,
groupProjectDAL,
projectKeyDAL,
tx
});
}
if (toRemoveUserIds.length) {
await removeUsersFromGroupByUserIds({
group,
userIds: toRemoveUserIds,
userDAL,
userGroupMembershipDAL,
groupProjectDAL,
projectKeyDAL,
tx
});
}
return group;
});
return updatedGroup;
};
const replaceScimGroup = async ({ groupId, orgId, displayName, members }: TUpdateScimGroupNamePutDTO) => {
const plan = await licenseService.getPlan(orgId);
if (!plan.groups)
throw new BadRequestError({
message: "Failed to update SCIM group due to plan restriction. Upgrade plan to update SCIM group."
});
const org = await orgDAL.findById(orgId);
if (!org) {
throw new ScimRequestError({
detail: "Organization Not Found",
status: 404
});
}
if (!org.scimEnabled)
throw new ScimRequestError({
detail: "SCIM is disabled for the organization",
status: 403
});
const updatedGroup = await $replaceGroupDAL(groupId, orgId, { displayName, members });
return buildScimGroup({
groupId: updatedGroup.id,
name: updatedGroup.name,
members
members,
updatedAt: updatedGroup.updatedAt,
createdAt: updatedGroup.createdAt
});
};
const updateScimGroupNamePatch = async ({ groupId, orgId, operations }: TUpdateScimGroupNamePatchDTO) => {
const updateScimGroup = async ({ groupId, orgId, operations }: TUpdateScimGroupNamePatchDTO) => {
const plan = await licenseService.getPlan(orgId);
if (!plan.groups)
throw new BadRequestError({
@@ -871,7 +935,7 @@ export const scimServiceFactory = ({
status: 403
});
let group = await groupDAL.findOne({
const group = await groupDAL.findOne({
id: groupId,
orgId
});
@@ -883,73 +947,28 @@ export const scimServiceFactory = ({
});
}
for await (const operation of operations) {
switch (operation.op) {
case "replace": {
group = await groupDAL.updateById(group.id, {
name: operation.value.displayName
});
break;
}
case "add": {
try {
const orgMemberships = await orgMembershipDAL.find({
$in: {
id: operation.value.map((member) => member.value)
}
});
await addUsersToGroupByUserIds({
group,
userIds: orgMemberships.map((membership) => membership.userId as string),
userDAL,
userGroupMembershipDAL,
orgDAL,
groupProjectDAL,
projectKeyDAL,
projectDAL,
projectBotDAL
});
} catch {
logger.info("Repeat SCIM user-group add operation");
}
break;
}
case "remove": {
const orgMembershipId = extractScimValueFromPath(operation.path);
if (!orgMembershipId) throw new ScimRequestError({ detail: "Invalid path value", status: 400 });
const orgMembership = await orgMembershipDAL.findById(orgMembershipId);
if (!orgMembership) throw new ScimRequestError({ detail: "Org Membership Not Found", status: 400 });
await removeUsersFromGroupByUserIds({
group,
userIds: [orgMembership.userId as string],
userDAL,
userGroupMembershipDAL,
groupProjectDAL,
projectKeyDAL
});
break;
}
default: {
throw new ScimRequestError({
detail: "Invalid Operation",
status: 400
});
}
}
}
const members = await userGroupMembershipDAL.findGroupMembershipsByGroupIdInOrg(group.id, orgId);
return buildScimGroup({
const scimGroup = buildScimGroup({
groupId: group.id,
name: group.name,
members: members.map((member) => ({
value: member.orgMembershipId
})),
createdAt: group.createdAt,
updatedAt: group.updatedAt
});
scimPatch(scimGroup, operations);
// remove members is a weird case not following scim convention
await $replaceGroupDAL(groupId, orgId, { displayName: scimGroup.displayName, members: scimGroup.members });
const updatedScimMembers = await userGroupMembershipDAL.findGroupMembershipsByGroupIdInOrg(group.id, orgId);
return {
...scimGroup,
members: updatedScimMembers.map((member) => ({
value: member.orgMembershipId,
display: `${member.firstName ?? ""} ${member.lastName ?? ""}`
}))
});
};
};
const deleteScimGroup = async ({ groupId, orgId }: TDeleteScimGroupDTO) => {
@@ -1025,8 +1044,8 @@ export const scimServiceFactory = ({
createScimGroup,
getScimGroup,
deleteScimGroup,
updateScimGroupNamePut,
updateScimGroupNamePatch,
replaceScimGroup,
updateScimGroup,
fnValidateScimToken
};
};

View File

@@ -1,3 +1,5 @@
import { ScimPatchOperation } from "scim-patch";
import { TOrgPermission } from "@app/lib/types";
export type TCreateScimTokenDTO = {
@@ -34,29 +36,25 @@ export type TGetScimUserDTO = {
export type TCreateScimUserDTO = {
externalId: string;
email?: string;
firstName: string;
lastName: string;
firstName?: string;
lastName?: string;
orgId: string;
};
export type TUpdateScimUserDTO = {
orgMembershipId: string;
orgId: string;
operations: {
op: string;
path?: string;
value?:
| string
| {
active: boolean;
};
}[];
operations: ScimPatchOperation[];
};
export type TReplaceScimUserDTO = {
orgMembershipId: string;
active: boolean;
orgId: string;
email?: string;
firstName?: string;
lastName?: string;
externalId: string;
};
export type TDeleteScimUserDTO = {
@@ -69,6 +67,7 @@ export type TListScimGroupsDTO = {
filter?: string;
limit: number;
orgId: string;
isMembersExcluded?: boolean;
};
export type TListScimGroups = {
@@ -107,29 +106,7 @@ export type TUpdateScimGroupNamePutDTO = {
export type TUpdateScimGroupNamePatchDTO = {
groupId: string;
orgId: string;
operations: (TRemoveOp | TReplaceOp | TAddOp)[];
};
type TReplaceOp = {
op: "replace";
value: {
id: string;
displayName: string;
};
};
type TRemoveOp = {
op: "remove";
path: string;
};
type TAddOp = {
op: "add";
path: string;
value: {
value: string;
display?: string;
}[];
operations: ScimPatchOperation[];
};
export type TDeleteScimGroupDTO = {
@@ -158,13 +135,10 @@ export type TScimUser = {
type: string;
}[];
active: boolean;
groups: {
value: string;
display: string;
}[];
meta: {
resourceType: string;
location: null;
created: Date;
lastModified: Date;
};
};
@@ -174,10 +148,11 @@ export type TScimGroup = {
displayName: string;
members: {
value: string;
display: string;
display?: string;
}[];
meta: {
resourceType: string;
location: null;
created: Date;
lastModified: Date;
};
};

View File

@@ -20,7 +20,15 @@ export const secretApprovalPolicyDALFactory = (db: TDbClient) => {
`${TableName.SecretApprovalPolicy}.id`,
`${TableName.SecretApprovalPolicyApprover}.policyId`
)
.select(tx.ref("approverUserId").withSchema(TableName.SecretApprovalPolicyApprover))
.leftJoin(TableName.Users, `${TableName.SecretApprovalPolicyApprover}.approverUserId`, `${TableName.Users}.id`)
.select(
tx.ref("approverUserId").withSchema(TableName.SecretApprovalPolicyApprover),
tx.ref("email").withSchema(TableName.Users).as("approverEmail"),
tx.ref("firstName").withSchema(TableName.Users).as("approverFirstName"),
tx.ref("lastName").withSchema(TableName.Users).as("approverLastName")
)
.select(
tx.ref("name").withSchema(TableName.Environment).as("envName"),
tx.ref("slug").withSchema(TableName.Environment).as("envSlug"),
@@ -47,8 +55,11 @@ export const secretApprovalPolicyDALFactory = (db: TDbClient) => {
{
key: "approverUserId",
label: "userApprovers" as const,
mapper: ({ approverUserId }) => ({
userId: approverUserId
mapper: ({ approverUserId, approverEmail, approverFirstName, approverLastName }) => ({
userId: approverUserId,
email: approverEmail,
firstName: approverFirstName,
lastName: approverLastName
})
}
]

View File

@@ -0,0 +1,44 @@
import { TSecretApprovalRequests } from "@app/db/schemas";
import { getConfig } from "@app/lib/config/env";
import { TProjectDALFactory } from "@app/services/project/project-dal";
import { SmtpTemplates, TSmtpService } from "@app/services/smtp/smtp-service";
import { TSecretApprovalPolicyDALFactory } from "../secret-approval-policy/secret-approval-policy-dal";
type TSendApprovalEmails = {
secretApprovalPolicyDAL: Pick<TSecretApprovalPolicyDALFactory, "findById">;
projectDAL: Pick<TProjectDALFactory, "findProjectWithOrg">;
smtpService: Pick<TSmtpService, "sendMail">;
projectId: string;
secretApprovalRequest: TSecretApprovalRequests;
};
export const sendApprovalEmailsFn = async ({
secretApprovalPolicyDAL,
projectDAL,
smtpService,
projectId,
secretApprovalRequest
}: TSendApprovalEmails) => {
const cfg = getConfig();
const policy = await secretApprovalPolicyDAL.findById(secretApprovalRequest.policyId);
const project = await projectDAL.findProjectWithOrg(projectId);
// now we need to go through each of the reviewers and print out all the commits that they need to approve
for await (const reviewerUser of policy.userApprovers) {
await smtpService.sendMail({
recipients: [reviewerUser?.email as string],
subjectLine: "Infisical Secret Change Request",
substitutions: {
firstName: reviewerUser.firstName,
projectName: project.name,
organizationName: project.organization.name,
approvalUrl: `${cfg.SITE_URL}/project/${project.id}/approval?requestId=${secretApprovalRequest.id}`
},
template: SmtpTemplates.SecretApprovalRequestNeedsReview
});
}
};

View File

@@ -53,8 +53,10 @@ import { TUserDALFactory } from "@app/services/user/user-dal";
import { TLicenseServiceFactory } from "../license/license-service";
import { TPermissionServiceFactory } from "../permission/permission-service";
import { ProjectPermissionActions, ProjectPermissionSub } from "../permission/project-permission";
import { TSecretApprovalPolicyDALFactory } from "../secret-approval-policy/secret-approval-policy-dal";
import { TSecretSnapshotServiceFactory } from "../secret-snapshot/secret-snapshot-service";
import { TSecretApprovalRequestDALFactory } from "./secret-approval-request-dal";
import { sendApprovalEmailsFn } from "./secret-approval-request-fns";
import { TSecretApprovalRequestReviewerDALFactory } from "./secret-approval-request-reviewer-dal";
import { TSecretApprovalRequestSecretDALFactory } from "./secret-approval-request-secret-dal";
import {
@@ -89,7 +91,10 @@ type TSecretApprovalRequestServiceFactoryDep = {
smtpService: Pick<TSmtpService, "sendMail">;
userDAL: Pick<TUserDALFactory, "find" | "findOne">;
projectEnvDAL: Pick<TProjectEnvDALFactory, "findOne">;
projectDAL: Pick<TProjectDALFactory, "checkProjectUpgradeStatus" | "findById" | "findProjectById">;
projectDAL: Pick<
TProjectDALFactory,
"checkProjectUpgradeStatus" | "findById" | "findProjectById" | "findProjectWithOrg"
>;
secretQueueService: Pick<TSecretQueueFactory, "syncSecrets" | "removeSecretReminder">;
kmsService: Pick<TKmsServiceFactory, "createCipherPairWithDataKey" | "encryptWithInputKey" | "decryptWithInputKey">;
secretV2BridgeDAL: Pick<
@@ -98,6 +103,7 @@ type TSecretApprovalRequestServiceFactoryDep = {
>;
secretVersionV2BridgeDAL: Pick<TSecretVersionV2DALFactory, "insertMany" | "findLatestVersionMany">;
secretVersionTagV2BridgeDAL: Pick<TSecretVersionV2TagDALFactory, "insertMany">;
secretApprovalPolicyDAL: Pick<TSecretApprovalPolicyDALFactory, "findById">;
licenseService: Pick<TLicenseServiceFactory, "getPlan">;
};
@@ -121,6 +127,7 @@ export const secretApprovalRequestServiceFactory = ({
smtpService,
userDAL,
projectEnvDAL,
secretApprovalPolicyDAL,
kmsService,
secretV2BridgeDAL,
secretVersionV2BridgeDAL,
@@ -1061,6 +1068,15 @@ export const secretApprovalRequestServiceFactory = ({
}
return { ...doc, commits: approvalCommits };
});
await sendApprovalEmailsFn({
projectDAL,
secretApprovalPolicyDAL,
secretApprovalRequest,
smtpService,
projectId
});
return secretApprovalRequest;
};
@@ -1311,8 +1327,17 @@ export const secretApprovalRequestServiceFactory = ({
tx
);
}
return { ...doc, commits: approvalCommits };
});
await sendApprovalEmailsFn({
projectDAL,
secretApprovalPolicyDAL,
secretApprovalRequest,
smtpService,
projectId
});
return secretApprovalRequest;
};

View File

@@ -16,6 +16,7 @@ import {
import { DatabaseError } from "@app/lib/errors";
import { ormify, selectAllTableCols, sqlNestRelationships } from "@app/lib/knex";
import { logger } from "@app/lib/logger";
import { QueueName } from "@app/queue";
export type TSnapshotDALFactory = ReturnType<typeof snapshotDALFactory>;
@@ -599,6 +600,7 @@ export const snapshotDALFactory = (db: TDbClient) => {
const pruneExcessSnapshots = async () => {
const PRUNE_FOLDER_BATCH_SIZE = 10000;
logger.info(`${QueueName.DailyResourceCleanUp}: pruning secret snapshots started`);
try {
let uuidOffset = "00000000-0000-0000-0000-000000000000";
// cleanup snapshots from current folders
@@ -714,6 +716,7 @@ export const snapshotDALFactory = (db: TDbClient) => {
} catch (error) {
throw new DatabaseError({ error, name: "SnapshotPrune" });
}
logger.info(`${QueueName.DailyResourceCleanUp}: pruning secret snapshots completed`);
};
// special query for migration for secret v2

View File

@@ -964,6 +964,10 @@ export const INTEGRATION = {
shouldAutoRedeploy: "Used by Render to trigger auto deploy.",
secretGCPLabel: "The label for GCP secrets.",
secretAWSTag: "The tags for AWS secrets.",
githubVisibility:
"Define where the secrets from the Github Integration should be visible. Option 'selected' lets you directly define which repositories to sync secrets to.",
githubVisibilityRepoIds:
"The repository IDs to sync secrets to when using the Github Integration. Only applicable when using Organization scope, and visibility is set to 'selected'",
kmsKeyId: "The ID of the encryption key from AWS KMS.",
shouldDisableDelete: "The flag to disable deletion of secrets in AWS Parameter Store.",
shouldMaskSecrets: "Specifies if the secrets synced from Infisical to Gitlab should be marked as 'Masked'.",

View File

@@ -0,0 +1,57 @@
import crypto from "node:crypto";
import { AxiosError, AxiosInstance, AxiosRequestConfig } from "axios";
export const createDigestAuthRequestInterceptor = (
axiosInstance: AxiosInstance,
username: string,
password: string
) => {
let nc = 0;
return async (opts: AxiosRequestConfig) => {
try {
return await axiosInstance.request(opts);
} catch (err) {
const error = err as AxiosError;
const authHeader = (error?.response?.headers?.["www-authenticate"] as string) || "";
if (error?.response?.status !== 401 || !authHeader?.includes("nonce")) {
return Promise.reject(error.message);
}
if (!error.config) {
return Promise.reject(error);
}
const authDetails = authHeader.split(",").map((el) => el.split("="));
nc += 1;
const nonceCount = nc.toString(16).padStart(8, "0");
const cnonce = crypto.randomBytes(24).toString("hex");
const realm = authDetails.find((el) => el[0].toLowerCase().indexOf("realm") > -1)?.[1].replace(/"/g, "");
const nonce = authDetails.find((el) => el[0].toLowerCase().indexOf("nonce") > -1)?.[1].replace(/"/g, "");
const ha1 = crypto.createHash("md5").update(`${username}:${realm}:${password}`).digest("hex");
const path = opts.url;
const ha2 = crypto
.createHash("md5")
.update(`${opts.method ?? "GET"}:${path}`)
.digest("hex");
const response = crypto
.createHash("md5")
.update(`${ha1}:${nonce}:${nonceCount}:${cnonce}:auth:${ha2}`)
.digest("hex");
const authorization = `Digest username="${username}",realm="${realm}",nonce="${nonce}",uri="${path}",qop="auth",algorithm="MD5",response="${response}",nc="${nonceCount}",cnonce="${cnonce}"`;
if (opts.headers) {
// eslint-disable-next-line
opts.headers.authorization = authorization;
} else {
// eslint-disable-next-line
opts.headers = { authorization };
}
return axiosInstance.request(opts);
}
};
};

View File

@@ -1,6 +1,7 @@
import { Logger } from "pino";
import { z } from "zod";
import { removeTrailingSlash } from "../fn";
import { zpStr } from "../zod";
export const GITLAB_URL = "https://gitlab.com";
@@ -63,7 +64,9 @@ const envSchema = z
.string()
.min(32)
.default("#5VihU%rbXHcHwWwCot5L3vyPsx$7dWYw^iGk!EJg2bC*f$PD$%KCqx^R@#^LSEf"),
SITE_URL: zpStr(z.string().optional()),
// Ensure that the SITE_URL never ends with a trailing slash
SITE_URL: zpStr(z.string().transform((val) => (val ? removeTrailingSlash(val) : val))).optional(),
// Telemetry
TELEMETRY_ENABLED: zodStrBool.default("true"),
POSTHOG_HOST: zpStr(z.string().optional().default("https://app.posthog.com")),
@@ -142,7 +145,8 @@ const envSchema = z
CAPTCHA_SECRET: zpStr(z.string().optional()),
PLAIN_API_KEY: zpStr(z.string().optional()),
PLAIN_WISH_LABEL_IDS: zpStr(z.string().optional()),
DISABLE_AUDIT_LOG_GENERATION: zodStrBool.default("false")
DISABLE_AUDIT_LOG_GENERATION: zodStrBool.default("false"),
SSL_CLIENT_CERTIFICATE_HEADER_KEY: zpStr(z.string().optional()).default("x-ssl-client-cert")
})
.transform((data) => ({
...data,

View File

@@ -0,0 +1,121 @@
import { Knex } from "knex";
import { Compare, Filter, parse } from "scim2-parse-filter";
const appendParentToGroupingOperator = (parentPath: string, filter: Filter) => {
if (filter.op !== "[]" && filter.op !== "and" && filter.op !== "or" && filter.op !== "not") {
return { ...filter, attrPath: `${parentPath}.${(filter as Compare).attrPath}` };
}
return filter;
};
export const generateKnexQueryFromScim = (
rootQuery: Knex.QueryBuilder,
rootScimFilter: string,
getAttributeField: (attr: string) => string | null
) => {
const scimRootFilterAst = parse(rootScimFilter);
const stack = [
{
scimFilterAst: scimRootFilterAst,
query: rootQuery
}
];
while (stack.length) {
const { scimFilterAst, query } = stack.pop()!;
switch (scimFilterAst.op) {
case "eq": {
const attrPath = getAttributeField(scimFilterAst.attrPath);
if (attrPath) void query.where(attrPath, scimFilterAst.compValue);
break;
}
case "pr": {
const attrPath = getAttributeField(scimFilterAst.attrPath);
if (attrPath) void query.whereNotNull(attrPath);
break;
}
case "gt": {
const attrPath = getAttributeField(scimFilterAst.attrPath);
if (attrPath) void query.where(attrPath, ">", scimFilterAst.compValue);
break;
}
case "ge": {
const attrPath = getAttributeField(scimFilterAst.attrPath);
if (attrPath) void query.where(attrPath, ">=", scimFilterAst.compValue);
break;
}
case "lt": {
const attrPath = getAttributeField(scimFilterAst.attrPath);
if (attrPath) void query.where(attrPath, "<", scimFilterAst.compValue);
break;
}
case "le": {
const attrPath = getAttributeField(scimFilterAst.attrPath);
if (attrPath) void query.where(attrPath, "<=", scimFilterAst.compValue);
break;
}
case "sw": {
const attrPath = getAttributeField(scimFilterAst.attrPath);
if (attrPath) void query.whereILike(attrPath, `${scimFilterAst.compValue}%`);
break;
}
case "ew": {
const attrPath = getAttributeField(scimFilterAst.attrPath);
if (attrPath) void query.whereILike(attrPath, `%${scimFilterAst.compValue}`);
break;
}
case "co": {
const attrPath = getAttributeField(scimFilterAst.attrPath);
if (attrPath) void query.whereILike(attrPath, `%${scimFilterAst.compValue}%`);
break;
}
case "ne": {
const attrPath = getAttributeField(scimFilterAst.attrPath);
if (attrPath) void query.whereNot(attrPath, "=", scimFilterAst.compValue);
break;
}
case "and": {
void query.andWhere((subQueryBuilder) => {
scimFilterAst.filters.forEach((el) => {
stack.push({
query: subQueryBuilder,
scimFilterAst: el
});
});
});
break;
}
case "or": {
void query.orWhere((subQueryBuilder) => {
scimFilterAst.filters.forEach((el) => {
stack.push({
query: subQueryBuilder,
scimFilterAst: el
});
});
});
break;
}
case "not": {
void query.whereNot((subQueryBuilder) => {
stack.push({
query: subQueryBuilder,
scimFilterAst: scimFilterAst.filter
});
});
break;
}
case "[]": {
void query.whereNot((subQueryBuilder) => {
stack.push({
query: subQueryBuilder,
scimFilterAst: appendParentToGroupingOperator(scimFilterAst.attrPath, scimFilterAst.valFilter)
});
});
break;
}
default:
break;
}
}
};

View File

@@ -1,2 +1,2 @@
export { isDisposableEmail } from "./validate-email";
export { validateLocalIps } from "./validate-url";
export { blockLocalAndPrivateIpAddresses } from "./validate-url";

View File

@@ -1,7 +1,7 @@
import { getConfig } from "../config/env";
import { BadRequestError } from "../errors";
export const validateLocalIps = (url: string) => {
export const blockLocalAndPrivateIpAddresses = (url: string) => {
const validUrl = new URL(url);
const appCfg = getConfig();
// on cloud local ips are not allowed

View File

@@ -49,6 +49,21 @@ export const main = async ({ db, smtp, logger, queue, keyStore }: TMain) => {
server.setValidatorCompiler(validatorCompiler);
server.setSerializerCompiler(serializerCompiler);
server.addContentTypeParser("application/scim+json", { parseAs: "string" }, (_, body, done) => {
try {
const strBody = body instanceof Buffer ? body.toString() : body;
if (!strBody) {
done(null, undefined);
return;
}
const json: unknown = JSON.parse(strBody);
done(null, json);
} catch (err) {
const error = err as Error;
done(error, undefined);
}
});
try {
await server.register<FastifyCookieOptions>(cookie, {
secret: appCfg.COOKIE_SECRET_SIGN_KEY

View File

@@ -57,7 +57,6 @@ const extractAuth = async (req: FastifyRequest, jwtSecret: string) => {
return { authMode: AuthMode.API_KEY, token: apiKey, actor: ActorType.USER } as const;
}
const authHeader = req.headers?.authorization;
if (!authHeader) return { authMode: null, token: null };
const authTokenValue = authHeader.slice(7); // slice of after Bearer
@@ -103,12 +102,13 @@ export const injectIdentity = fp(async (server: FastifyZodProvider) => {
server.decorateRequest("auth", null);
server.addHook("onRequest", async (req) => {
const appCfg = getConfig();
const { authMode, token, actor } = await extractAuth(req, appCfg.AUTH_SECRET);
if (req.url.includes("/api/v3/auth/")) {
if (req.url.includes(".well-known/est") || req.url.includes("/api/v3/auth/")) {
return;
}
const { authMode, token, actor } = await extractAuth(req, appCfg.AUTH_SECRET);
if (!authMode) return;
switch (authMode) {

View File

@@ -1,7 +1,9 @@
import { CronJob } from "cron";
import { Redis } from "ioredis";
import { Knex } from "knex";
import { z } from "zod";
import { registerCertificateEstRouter } from "@app/ee/routes/est/certificate-est-router";
import { registerV1EERoutes } from "@app/ee/routes/v1";
import { accessApprovalPolicyApproverDALFactory } from "@app/ee/services/access-approval-policy/access-approval-policy-approver-dal";
import { accessApprovalPolicyDALFactory } from "@app/ee/services/access-approval-policy/access-approval-policy-dal";
@@ -16,6 +18,7 @@ import { auditLogStreamDALFactory } from "@app/ee/services/audit-log-stream/audi
import { auditLogStreamServiceFactory } from "@app/ee/services/audit-log-stream/audit-log-stream-service";
import { certificateAuthorityCrlDALFactory } from "@app/ee/services/certificate-authority-crl/certificate-authority-crl-dal";
import { certificateAuthorityCrlServiceFactory } from "@app/ee/services/certificate-authority-crl/certificate-authority-crl-service";
import { certificateEstServiceFactory } from "@app/ee/services/certificate-est/certificate-est-service";
import { dynamicSecretDALFactory } from "@app/ee/services/dynamic-secret/dynamic-secret-dal";
import { dynamicSecretServiceFactory } from "@app/ee/services/dynamic-secret/dynamic-secret-service";
import { buildDynamicSecretProviders } from "@app/ee/services/dynamic-secret/providers";
@@ -71,6 +74,7 @@ import { trustedIpDALFactory } from "@app/ee/services/trusted-ip/trusted-ip-dal"
import { trustedIpServiceFactory } from "@app/ee/services/trusted-ip/trusted-ip-service";
import { TKeyStoreFactory } from "@app/keystore/keystore";
import { getConfig } from "@app/lib/config/env";
import { logger } from "@app/lib/logger";
import { TQueueServiceFactory } from "@app/queue";
import { readLimit } from "@app/server/config/rateLimiter";
import { accessTokenQueueServiceFactory } from "@app/services/access-token-queue/access-token-queue";
@@ -91,6 +95,7 @@ import { certificateAuthorityQueueFactory } from "@app/services/certificate-auth
import { certificateAuthoritySecretDALFactory } from "@app/services/certificate-authority/certificate-authority-secret-dal";
import { certificateAuthorityServiceFactory } from "@app/services/certificate-authority/certificate-authority-service";
import { certificateTemplateDALFactory } from "@app/services/certificate-template/certificate-template-dal";
import { certificateTemplateEstConfigDALFactory } from "@app/services/certificate-template/certificate-template-est-config-dal";
import { certificateTemplateServiceFactory } from "@app/services/certificate-template/certificate-template-service";
import { groupProjectDALFactory } from "@app/services/group-project/group-project-dal";
import { groupProjectMembershipRoleDALFactory } from "@app/services/group-project/group-project-membership-role-dal";
@@ -600,6 +605,7 @@ export const registerRoutes = async (
const certificateAuthoritySecretDAL = certificateAuthoritySecretDALFactory(db);
const certificateAuthorityCrlDAL = certificateAuthorityCrlDALFactory(db);
const certificateTemplateDAL = certificateTemplateDALFactory(db);
const certificateTemplateEstConfigDAL = certificateTemplateEstConfigDALFactory(db);
const certificateDAL = certificateDALFactory(db);
const certificateBodyDAL = certificateBodyDALFactory(db);
@@ -657,8 +663,23 @@ export const registerRoutes = async (
const certificateTemplateService = certificateTemplateServiceFactory({
certificateTemplateDAL,
certificateTemplateEstConfigDAL,
certificateAuthorityDAL,
permissionService
permissionService,
kmsService,
projectDAL,
licenseService
});
const certificateEstService = certificateEstServiceFactory({
certificateAuthorityService,
certificateTemplateService,
certificateTemplateDAL,
certificateAuthorityCertDAL,
certificateAuthorityDAL,
projectDAL,
kmsService,
licenseService
});
const pkiAlertService = pkiAlertServiceFactory({
@@ -845,6 +866,7 @@ export const registerRoutes = async (
secretQueueService,
kmsService,
secretV2BridgeDAL,
secretApprovalPolicyDAL,
secretVersionV2BridgeDAL,
secretVersionTagV2BridgeDAL,
smtpService,
@@ -1195,6 +1217,7 @@ export const registerRoutes = async (
certificateAuthority: certificateAuthorityService,
certificateTemplate: certificateTemplateService,
certificateAuthorityCrl: certificateAuthorityCrlService,
certificateEst: certificateEstService,
pkiAlert: pkiAlertService,
pkiCollection: pkiCollectionService,
secretScanning: secretScanningService,
@@ -1238,7 +1261,7 @@ export const registerRoutes = async (
response: {
200: z.object({
date: z.date(),
message: z.literal("Ok"),
message: z.string().optional(),
emailConfigured: z.boolean().optional(),
inviteOnlySignup: z.boolean().optional(),
redisConfigured: z.boolean().optional(),
@@ -1247,12 +1270,37 @@ export const registerRoutes = async (
})
}
},
handler: async () => {
handler: async (request, reply) => {
const cfg = getConfig();
const serverCfg = await getServerCfg();
try {
await db.raw("SELECT NOW()");
} catch (err) {
logger.error("Health check: database connection failed", err);
return reply.code(503).send({
date: new Date(),
message: "Service unavailable"
});
}
if (cfg.isRedisConfigured) {
const redis = new Redis(cfg.REDIS_URL);
try {
await redis.ping();
redis.disconnect();
} catch (err) {
logger.error("Health check: redis connection failed", err);
return reply.code(503).send({
date: new Date(),
message: "Service unavailable"
});
}
}
return {
date: new Date(),
message: "Ok" as const,
message: "Ok",
emailConfigured: cfg.isSmtpConfigured,
inviteOnlySignup: Boolean(serverCfg.allowSignUp),
redisConfigured: cfg.isRedisConfigured,
@@ -1262,6 +1310,9 @@ export const registerRoutes = async (
}
});
// register special routes
await server.register(registerCertificateEstRouter, { prefix: "/.well-known/est" });
// register routes for v1
await server.register(
async (v1Server) => {

View File

@@ -669,6 +669,7 @@ export const registerCaRouter = async (server: FastifyZodProvider) => {
handler: async (req) => {
const { certificate, certificateChain, issuingCaCertificate, serialNumber, ca } =
await server.services.certificateAuthority.signCertFromCa({
isInternal: false,
caId: req.params.caId,
actor: req.permission.type,
actorId: req.permission.id,
@@ -691,7 +692,7 @@ export const registerCaRouter = async (server: FastifyZodProvider) => {
});
return {
certificate,
certificate: certificate.toString("pem"),
certificateChain,
issuingCaCertificate,
serialNumber

View File

@@ -210,6 +210,7 @@ export const registerCertRouter = async (server: FastifyZodProvider) => {
handler: async (req) => {
const { certificate, certificateChain, issuingCaCertificate, serialNumber, ca } =
await server.services.certificateAuthority.signCertFromCa({
isInternal: false,
actor: req.permission.type,
actorId: req.permission.id,
actorAuthMethod: req.permission.authMethod,
@@ -231,7 +232,7 @@ export const registerCertRouter = async (server: FastifyZodProvider) => {
});
return {
certificate,
certificate: certificate.toString("pem"),
certificateChain,
issuingCaCertificate,
serialNumber

View File

@@ -1,6 +1,7 @@
import ms from "ms";
import { z } from "zod";
import { CertificateTemplateEstConfigsSchema } from "@app/db/schemas";
import { EventType } from "@app/ee/services/audit-log/audit-log-types";
import { CERTIFICATE_TEMPLATES } from "@app/lib/api-docs";
import { readLimit, writeLimit } from "@app/server/config/rateLimiter";
@@ -9,6 +10,12 @@ import { AuthMode } from "@app/services/auth/auth-type";
import { sanitizedCertificateTemplate } from "@app/services/certificate-template/certificate-template-schema";
import { validateTemplateRegexField } from "@app/services/certificate-template/certificate-template-validators";
const sanitizedEstConfig = CertificateTemplateEstConfigsSchema.pick({
id: true,
certificateTemplateId: true,
isEnabled: true
});
export const registerCertificateTemplateRouter = async (server: FastifyZodProvider) => {
server.route({
method: "GET",
@@ -202,4 +209,141 @@ export const registerCertificateTemplateRouter = async (server: FastifyZodProvid
return certificateTemplate;
}
});
server.route({
method: "POST",
url: "/:certificateTemplateId/est-config",
config: {
rateLimit: writeLimit
},
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
schema: {
description: "Create Certificate Template EST configuration",
params: z.object({
certificateTemplateId: z.string().trim()
}),
body: z.object({
caChain: z.string().trim().min(1),
passphrase: z.string().min(1),
isEnabled: z.boolean().default(true)
}),
response: {
200: sanitizedEstConfig
}
},
handler: async (req) => {
const estConfig = await server.services.certificateTemplate.createEstConfiguration({
certificateTemplateId: req.params.certificateTemplateId,
actor: req.permission.type,
actorId: req.permission.id,
actorAuthMethod: req.permission.authMethod,
actorOrgId: req.permission.orgId,
...req.body
});
await server.services.auditLog.createAuditLog({
...req.auditLogInfo,
projectId: estConfig.projectId,
event: {
type: EventType.CREATE_CERTIFICATE_TEMPLATE_EST_CONFIG,
metadata: {
certificateTemplateId: estConfig.certificateTemplateId,
isEnabled: estConfig.isEnabled as boolean
}
}
});
return estConfig;
}
});
server.route({
method: "PATCH",
url: "/:certificateTemplateId/est-config",
config: {
rateLimit: writeLimit
},
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
schema: {
description: "Update Certificate Template EST configuration",
params: z.object({
certificateTemplateId: z.string().trim()
}),
body: z.object({
caChain: z.string().trim().min(1).optional(),
passphrase: z.string().min(1).optional(),
isEnabled: z.boolean().optional()
}),
response: {
200: sanitizedEstConfig
}
},
handler: async (req) => {
const estConfig = await server.services.certificateTemplate.updateEstConfiguration({
certificateTemplateId: req.params.certificateTemplateId,
actor: req.permission.type,
actorId: req.permission.id,
actorAuthMethod: req.permission.authMethod,
actorOrgId: req.permission.orgId,
...req.body
});
await server.services.auditLog.createAuditLog({
...req.auditLogInfo,
projectId: estConfig.projectId,
event: {
type: EventType.UPDATE_CERTIFICATE_TEMPLATE_EST_CONFIG,
metadata: {
certificateTemplateId: estConfig.certificateTemplateId,
isEnabled: estConfig.isEnabled as boolean
}
}
});
return estConfig;
}
});
server.route({
method: "GET",
url: "/:certificateTemplateId/est-config",
config: {
rateLimit: readLimit
},
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
schema: {
description: "Get Certificate Template EST configuration",
params: z.object({
certificateTemplateId: z.string().trim()
}),
response: {
200: sanitizedEstConfig.extend({
caChain: z.string()
})
}
},
handler: async (req) => {
const estConfig = await server.services.certificateTemplate.getEstConfiguration({
isInternal: false,
certificateTemplateId: req.params.certificateTemplateId,
actor: req.permission.type,
actorId: req.permission.id,
actorAuthMethod: req.permission.authMethod,
actorOrgId: req.permission.orgId
});
await server.services.auditLog.createAuditLog({
...req.auditLogInfo,
projectId: estConfig.projectId,
event: {
type: EventType.GET_CERTIFICATE_TEMPLATE_EST_CONFIG,
metadata: {
certificateTemplateId: estConfig.certificateTemplateId
}
}
});
return estConfig;
}
});
};

View File

@@ -48,7 +48,7 @@ export const registerSecretSharingRouter = async (server: FastifyZodProvider) =>
});
server.route({
method: "GET",
method: "POST",
url: "/public/:id",
config: {
rateLimit: publicEndpointLimit
@@ -57,38 +57,37 @@ export const registerSecretSharingRouter = async (server: FastifyZodProvider) =>
params: z.object({
id: z.string().uuid()
}),
querystring: z.object({
hashedHex: z.string().min(1)
body: z.object({
hashedHex: z.string().min(1),
password: z.string().optional()
}),
response: {
200: SecretSharingSchema.pick({
encryptedValue: true,
iv: true,
tag: true,
expiresAt: true,
expiresAfterViews: true,
accessType: true
}).extend({
orgName: z.string().optional()
200: z.object({
isPasswordProtected: z.boolean(),
secret: SecretSharingSchema.pick({
encryptedValue: true,
iv: true,
tag: true,
expiresAt: true,
expiresAfterViews: true,
accessType: true
})
.extend({
orgName: z.string().optional()
})
.optional()
})
}
},
handler: async (req) => {
const sharedSecret = await req.server.services.secretSharing.getActiveSharedSecretById({
const sharedSecret = await req.server.services.secretSharing.getSharedSecretById({
sharedSecretId: req.params.id,
hashedHex: req.query.hashedHex,
hashedHex: req.body.hashedHex,
password: req.body.password,
orgId: req.permission?.orgId
});
if (!sharedSecret) return undefined;
return {
encryptedValue: sharedSecret.encryptedValue,
iv: sharedSecret.iv,
tag: sharedSecret.tag,
expiresAt: sharedSecret.expiresAt,
expiresAfterViews: sharedSecret.expiresAfterViews,
accessType: sharedSecret.accessType,
orgName: sharedSecret.orgName
};
return sharedSecret;
}
});
@@ -101,6 +100,7 @@ export const registerSecretSharingRouter = async (server: FastifyZodProvider) =>
schema: {
body: z.object({
encryptedValue: z.string(),
password: z.string().optional(),
hashedHex: z.string(),
iv: z.string(),
tag: z.string(),
@@ -131,6 +131,7 @@ export const registerSecretSharingRouter = async (server: FastifyZodProvider) =>
schema: {
body: z.object({
name: z.string().max(50).optional(),
password: z.string().optional(),
encryptedValue: z.string(),
hashedHex: z.string(),
iv: z.string(),

View File

@@ -213,7 +213,6 @@ export const certificateAuthorityServiceFactory = ({
keys,
extensions: [
new x509.BasicConstraintsExtension(true, maxPathLength === -1 ? undefined : maxPathLength, true),
new x509.ExtendedKeyUsageExtension(["1.2.3.4.5.6.7", "2.3.4.5.6.7.8"], true),
// eslint-disable-next-line no-bitwise
new x509.KeyUsagesExtension(x509.KeyUsageFlags.keyCertSign | x509.KeyUsageFlags.cRLSign, true),
await x509.SubjectKeyIdentifierExtension.create(keys.publicKey)
@@ -496,7 +495,6 @@ export const certificateAuthorityServiceFactory = ({
ca.maxPathLength === -1 || !ca.maxPathLength ? undefined : ca.maxPathLength,
true
),
new x509.ExtendedKeyUsageExtension(["1.2.3.4.5.6.7", "2.3.4.5.6.7.8"], true),
// eslint-disable-next-line no-bitwise
new x509.KeyUsagesExtension(x509.KeyUsageFlags.keyCertSign | x509.KeyUsageFlags.cRLSign, true),
await x509.SubjectKeyIdentifierExtension.create(caPublicKey)
@@ -1295,24 +1293,23 @@ export const certificateAuthorityServiceFactory = ({
* Return new leaf certificate issued by CA with id [caId].
* Note: CSR is generated externally and submitted to Infisical.
*/
const signCertFromCa = async ({
caId,
certificateTemplateId,
csr,
pkiCollectionId,
friendlyName,
commonName,
altNames,
ttl,
notBefore,
notAfter,
actorId,
actorAuthMethod,
actor,
actorOrgId
}: TSignCertFromCaDTO) => {
const signCertFromCa = async (dto: TSignCertFromCaDTO) => {
let ca: TCertificateAuthorities | undefined;
let certificateTemplate: TCertificateTemplates | undefined;
const {
caId,
certificateTemplateId,
csr,
pkiCollectionId,
friendlyName,
commonName,
altNames,
ttl,
notBefore,
notAfter
} = dto;
let collectionId = pkiCollectionId;
if (caId) {
@@ -1333,15 +1330,20 @@ export const certificateAuthorityServiceFactory = ({
throw new BadRequestError({ message: "CA not found" });
}
const { permission } = await permissionService.getProjectPermission(
actor,
actorId,
ca.projectId,
actorAuthMethod,
actorOrgId
);
if (!dto.isInternal) {
const { permission } = await permissionService.getProjectPermission(
dto.actor,
dto.actorId,
ca.projectId,
dto.actorAuthMethod,
dto.actorOrgId
);
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Create, ProjectPermissionSub.Certificates);
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Create,
ProjectPermissionSub.Certificates
);
}
if (ca.status === CaStatus.DISABLED) throw new BadRequestError({ message: "CA is disabled" });
if (!ca.activeCaCertId) throw new BadRequestError({ message: "CA does not have a certificate installed" });
@@ -1382,6 +1384,8 @@ export const certificateAuthorityServiceFactory = ({
notAfterDate = new Date(notAfter);
} else if (ttl) {
notAfterDate = new Date(new Date().getTime() + ms(ttl));
} else if (certificateTemplate?.ttl) {
notAfterDate = new Date(new Date().getTime() + ms(certificateTemplate.ttl));
}
const caCertNotBeforeDate = new Date(caCertObj.notBefore);
@@ -1426,6 +1430,7 @@ export const certificateAuthorityServiceFactory = ({
await x509.SubjectKeyIdentifierExtension.create(csrObj.publicKey)
];
let altNamesFromCsr: string = "";
let altNamesArray: {
type: "email" | "dns";
value: string;
@@ -1454,7 +1459,24 @@ export const certificateAuthorityServiceFactory = ({
// If altName is neither a valid email nor a valid hostname, throw an error or handle it accordingly
throw new Error(`Invalid altName: ${altName}`);
});
} else {
// attempt to read from CSR if altNames is not explicitly provided
const sanExtension = csrObj.extensions.find((ext) => ext.type === "2.5.29.17");
if (sanExtension) {
const sanNames = new x509.GeneralNames(sanExtension.value);
altNamesArray = sanNames.items
.filter((value) => value.type === "email" || value.type === "dns")
.map((name) => ({
type: name.type as "email" | "dns",
value: name.value
}));
altNamesFromCsr = sanNames.items.map((item) => item.value).join(",");
}
}
if (altNamesArray.length) {
const altNamesExtension = new x509.SubjectAlternativeNameExtension(altNamesArray, false);
extensions.push(altNamesExtension);
}
@@ -1500,7 +1522,7 @@ export const certificateAuthorityServiceFactory = ({
status: CertStatus.ACTIVE,
friendlyName: friendlyName || csrObj.subject,
commonName: cn,
altNames,
altNames: altNamesFromCsr || altNames,
serialNumber,
notBefore: notBeforeDate,
notAfter: notAfterDate
@@ -1538,7 +1560,7 @@ export const certificateAuthorityServiceFactory = ({
});
return {
certificate: leafCert.toString("pem"),
certificate: leafCert,
certificateChain: `${issuingCaCertificate}\n${caCertChain}`.trim(),
issuingCaCertificate,
serialNumber,

View File

@@ -97,18 +97,33 @@ export type TIssueCertFromCaDTO = {
notAfter?: string;
} & Omit<TProjectPermission, "projectId">;
export type TSignCertFromCaDTO = {
caId?: string;
csr: string;
certificateTemplateId?: string;
pkiCollectionId?: string;
friendlyName?: string;
commonName?: string;
altNames: string;
ttl: string;
notBefore?: string;
notAfter?: string;
} & Omit<TProjectPermission, "projectId">;
export type TSignCertFromCaDTO =
| {
isInternal: true;
caId?: string;
csr: string;
certificateTemplateId?: string;
pkiCollectionId?: string;
friendlyName?: string;
commonName?: string;
altNames?: string;
ttl?: string;
notBefore?: string;
notAfter?: string;
}
| ({
isInternal: false;
caId?: string;
csr: string;
certificateTemplateId?: string;
pkiCollectionId?: string;
friendlyName?: string;
commonName?: string;
altNames: string;
ttl: string;
notBefore?: string;
notAfter?: string;
} & Omit<TProjectPermission, "projectId">);
export type TDNParts = {
commonName?: string;

View File

@@ -1,3 +1,5 @@
import { Knex } from "knex";
import { TDbClient } from "@app/db";
import { TableName } from "@app/db/schemas";
import { DatabaseError } from "@app/lib/errors";
@@ -30,20 +32,21 @@ export const certificateTemplateDALFactory = (db: TDbClient) => {
}
};
const getById = async (id: string) => {
const getById = async (id: string, tx?: Knex) => {
try {
const certTemplate = await db
.replicaNode()(TableName.CertificateTemplate)
const certTemplate = await (tx || db.replicaNode())(TableName.CertificateTemplate)
.join(
TableName.CertificateAuthority,
`${TableName.CertificateAuthority}.id`,
`${TableName.CertificateTemplate}.caId`
)
.join(TableName.Project, `${TableName.Project}.id`, `${TableName.CertificateAuthority}.projectId`)
.where(`${TableName.CertificateTemplate}.id`, "=", id)
.select(selectAllTableCols(TableName.CertificateTemplate))
.select(
db.ref("projectId").withSchema(TableName.CertificateAuthority),
db.ref("friendlyName").as("caName").withSchema(TableName.CertificateAuthority)
db.ref("friendlyName").as("caName").withSchema(TableName.CertificateAuthority),
db.ref("orgId").withSchema(TableName.Project)
)
.first();

View File

@@ -0,0 +1,11 @@
import { TDbClient } from "@app/db";
import { TableName } from "@app/db/schemas";
import { ormify } from "@app/lib/knex";
export type TCertificateTemplateEstConfigDALFactory = ReturnType<typeof certificateTemplateEstConfigDALFactory>;
export const certificateTemplateEstConfigDALFactory = (db: TDbClient) => {
const certificateTemplateEstConfigOrm = ormify(db, TableName.CertificateTemplateEstConfig);
return certificateTemplateEstConfigOrm;
};

View File

@@ -1,30 +1,51 @@
import { ForbiddenError } from "@casl/ability";
import * as x509 from "@peculiar/x509";
import bcrypt from "bcrypt";
import { TCertificateTemplateEstConfigsUpdate } from "@app/db/schemas";
import { TLicenseServiceFactory } from "@app/ee/services/license/license-service";
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service";
import { ProjectPermissionActions, ProjectPermissionSub } from "@app/ee/services/permission/project-permission";
import { getConfig } from "@app/lib/config/env";
import { BadRequestError, NotFoundError } from "@app/lib/errors";
import { isCertChainValid } from "../certificate/certificate-fns";
import { TCertificateAuthorityDALFactory } from "../certificate-authority/certificate-authority-dal";
import { TKmsServiceFactory } from "../kms/kms-service";
import { TProjectDALFactory } from "../project/project-dal";
import { getProjectKmsCertificateKeyId } from "../project/project-fns";
import { TCertificateTemplateDALFactory } from "./certificate-template-dal";
import { TCertificateTemplateEstConfigDALFactory } from "./certificate-template-est-config-dal";
import {
TCreateCertTemplateDTO,
TCreateEstConfigurationDTO,
TDeleteCertTemplateDTO,
TGetCertTemplateDTO,
TUpdateCertTemplateDTO
TGetEstConfigurationDTO,
TUpdateCertTemplateDTO,
TUpdateEstConfigurationDTO
} from "./certificate-template-types";
type TCertificateTemplateServiceFactoryDep = {
certificateTemplateDAL: TCertificateTemplateDALFactory;
certificateTemplateEstConfigDAL: TCertificateTemplateEstConfigDALFactory;
projectDAL: Pick<TProjectDALFactory, "findProjectBySlug" | "findOne" | "updateById" | "findById" | "transaction">;
kmsService: Pick<TKmsServiceFactory, "generateKmsKey" | "encryptWithKmsKey" | "decryptWithKmsKey">;
certificateAuthorityDAL: Pick<TCertificateAuthorityDALFactory, "findById">;
permissionService: Pick<TPermissionServiceFactory, "getProjectPermission">;
licenseService: Pick<TLicenseServiceFactory, "getPlan">;
};
export type TCertificateTemplateServiceFactory = ReturnType<typeof certificateTemplateServiceFactory>;
export const certificateTemplateServiceFactory = ({
certificateTemplateDAL,
certificateTemplateEstConfigDAL,
certificateAuthorityDAL,
permissionService
permissionService,
kmsService,
projectDAL,
licenseService
}: TCertificateTemplateServiceFactoryDep) => {
const createCertTemplate = async ({
caId,
@@ -57,23 +78,28 @@ export const certificateTemplateServiceFactory = ({
ProjectPermissionSub.CertificateTemplates
);
const { id } = await certificateTemplateDAL.create({
caId,
pkiCollectionId,
name,
commonName,
subjectAlternativeName,
ttl
return certificateTemplateDAL.transaction(async (tx) => {
const { id } = await certificateTemplateDAL.create(
{
caId,
pkiCollectionId,
name,
commonName,
subjectAlternativeName,
ttl
},
tx
);
const certificateTemplate = await certificateTemplateDAL.getById(id, tx);
if (!certificateTemplate) {
throw new NotFoundError({
message: "Certificate template not found"
});
}
return certificateTemplate;
});
const certificateTemplate = await certificateTemplateDAL.getById(id);
if (!certificateTemplate) {
throw new NotFoundError({
message: "Certificate template not found"
});
}
return certificateTemplate;
};
const updateCertTemplate = async ({
@@ -118,23 +144,29 @@ export const certificateTemplateServiceFactory = ({
}
}
await certificateTemplateDAL.updateById(certTemplate.id, {
caId,
pkiCollectionId,
commonName,
subjectAlternativeName,
name,
ttl
return certificateTemplateDAL.transaction(async (tx) => {
await certificateTemplateDAL.updateById(
certTemplate.id,
{
caId,
pkiCollectionId,
commonName,
subjectAlternativeName,
name,
ttl
},
tx
);
const updatedTemplate = await certificateTemplateDAL.getById(id, tx);
if (!updatedTemplate) {
throw new NotFoundError({
message: "Certificate template not found"
});
}
return updatedTemplate;
});
const updatedTemplate = await certificateTemplateDAL.getById(id);
if (!updatedTemplate) {
throw new NotFoundError({
message: "Certificate template not found"
});
}
return updatedTemplate;
};
const deleteCertTemplate = async ({ id, actorId, actorAuthMethod, actor, actorOrgId }: TDeleteCertTemplateDTO) => {
@@ -187,10 +219,243 @@ export const certificateTemplateServiceFactory = ({
return certTemplate;
};
const createEstConfiguration = async ({
certificateTemplateId,
caChain,
passphrase,
isEnabled,
actorId,
actorAuthMethod,
actor,
actorOrgId
}: TCreateEstConfigurationDTO) => {
const plan = await licenseService.getPlan(actorOrgId);
if (!plan.pkiEst) {
throw new BadRequestError({
message: "Failed to create EST configuration due to plan restriction. Upgrade to the Enterprise plan."
});
}
const certTemplate = await certificateTemplateDAL.getById(certificateTemplateId);
if (!certTemplate) {
throw new NotFoundError({
message: "Certificate template not found."
});
}
const { permission } = await permissionService.getProjectPermission(
actor,
actorId,
certTemplate.projectId,
actorAuthMethod,
actorOrgId
);
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Edit,
ProjectPermissionSub.CertificateTemplates
);
const appCfg = getConfig();
const certificateManagerKmsId = await getProjectKmsCertificateKeyId({
projectId: certTemplate.projectId,
projectDAL,
kmsService
});
// validate CA chain
const certificates = caChain
.match(/-----BEGIN CERTIFICATE-----[\s\S]+?-----END CERTIFICATE-----/g)
?.map((cert) => new x509.X509Certificate(cert));
if (!certificates) {
throw new BadRequestError({ message: "Failed to parse certificate chain" });
}
if (!(await isCertChainValid(certificates))) {
throw new BadRequestError({ message: "Invalid certificate chain" });
}
const kmsEncryptor = await kmsService.encryptWithKmsKey({
kmsId: certificateManagerKmsId
});
const { cipherTextBlob: encryptedCaChain } = await kmsEncryptor({
plainText: Buffer.from(caChain)
});
const hashedPassphrase = await bcrypt.hash(passphrase, appCfg.SALT_ROUNDS);
const estConfig = await certificateTemplateEstConfigDAL.create({
certificateTemplateId,
hashedPassphrase,
encryptedCaChain,
isEnabled
});
return { ...estConfig, projectId: certTemplate.projectId };
};
const updateEstConfiguration = async ({
certificateTemplateId,
caChain,
passphrase,
isEnabled,
actorId,
actorAuthMethod,
actor,
actorOrgId
}: TUpdateEstConfigurationDTO) => {
const plan = await licenseService.getPlan(actorOrgId);
if (!plan.pkiEst) {
throw new BadRequestError({
message: "Failed to update EST configuration due to plan restriction. Upgrade to the Enterprise plan."
});
}
const certTemplate = await certificateTemplateDAL.getById(certificateTemplateId);
if (!certTemplate) {
throw new NotFoundError({
message: "Certificate template not found."
});
}
const { permission } = await permissionService.getProjectPermission(
actor,
actorId,
certTemplate.projectId,
actorAuthMethod,
actorOrgId
);
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Edit,
ProjectPermissionSub.CertificateTemplates
);
const originalCaEstConfig = await certificateTemplateEstConfigDAL.findOne({
certificateTemplateId
});
if (!originalCaEstConfig) {
throw new NotFoundError({
message: "EST configuration not found"
});
}
const appCfg = getConfig();
const certificateManagerKmsId = await getProjectKmsCertificateKeyId({
projectId: certTemplate.projectId,
projectDAL,
kmsService
});
const updatedData: TCertificateTemplateEstConfigsUpdate = {
isEnabled
};
if (caChain) {
const certificates = caChain
.match(/-----BEGIN CERTIFICATE-----[\s\S]+?-----END CERTIFICATE-----/g)
?.map((cert) => new x509.X509Certificate(cert));
if (!certificates) {
throw new BadRequestError({ message: "Failed to parse certificate chain" });
}
if (!(await isCertChainValid(certificates))) {
throw new BadRequestError({ message: "Invalid certificate chain" });
}
const kmsEncryptor = await kmsService.encryptWithKmsKey({
kmsId: certificateManagerKmsId
});
const { cipherTextBlob: encryptedCaChain } = await kmsEncryptor({
plainText: Buffer.from(caChain)
});
updatedData.encryptedCaChain = encryptedCaChain;
}
if (passphrase) {
const hashedPassphrase = await bcrypt.hash(passphrase, appCfg.SALT_ROUNDS);
updatedData.hashedPassphrase = hashedPassphrase;
}
const estConfig = await certificateTemplateEstConfigDAL.updateById(originalCaEstConfig.id, updatedData);
return { ...estConfig, projectId: certTemplate.projectId };
};
const getEstConfiguration = async (dto: TGetEstConfigurationDTO) => {
const { certificateTemplateId } = dto;
const certTemplate = await certificateTemplateDAL.getById(certificateTemplateId);
if (!certTemplate) {
throw new NotFoundError({
message: "Certificate template not found."
});
}
if (!dto.isInternal) {
const { permission } = await permissionService.getProjectPermission(
dto.actor,
dto.actorId,
certTemplate.projectId,
dto.actorAuthMethod,
dto.actorOrgId
);
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Edit,
ProjectPermissionSub.CertificateTemplates
);
}
const estConfig = await certificateTemplateEstConfigDAL.findOne({
certificateTemplateId
});
if (!estConfig) {
throw new NotFoundError({
message: "EST configuration not found"
});
}
const certificateManagerKmsId = await getProjectKmsCertificateKeyId({
projectId: certTemplate.projectId,
projectDAL,
kmsService
});
const kmsDecryptor = await kmsService.decryptWithKmsKey({
kmsId: certificateManagerKmsId
});
const decryptedCaChain = await kmsDecryptor({
cipherTextBlob: estConfig.encryptedCaChain
});
return {
certificateTemplateId,
id: estConfig.id,
isEnabled: estConfig.isEnabled,
caChain: decryptedCaChain.toString(),
hashedPassphrase: estConfig.hashedPassphrase,
projectId: certTemplate.projectId,
orgId: certTemplate.orgId
};
};
return {
createCertTemplate,
getCertTemplate,
deleteCertTemplate,
updateCertTemplate
updateCertTemplate,
createEstConfiguration,
updateEstConfiguration,
getEstConfiguration
};
};

View File

@@ -26,3 +26,27 @@ export type TGetCertTemplateDTO = {
export type TDeleteCertTemplateDTO = {
id: string;
} & Omit<TProjectPermission, "projectId">;
export type TCreateEstConfigurationDTO = {
certificateTemplateId: string;
caChain: string;
passphrase: string;
isEnabled: boolean;
} & Omit<TProjectPermission, "projectId">;
export type TUpdateEstConfigurationDTO = {
certificateTemplateId: string;
caChain?: string;
passphrase?: string;
isEnabled?: boolean;
} & Omit<TProjectPermission, "projectId">;
export type TGetEstConfigurationDTO =
| {
isInternal: true;
certificateTemplateId: string;
}
| ({
isInternal: false;
certificateTemplateId: string;
} & Omit<TProjectPermission, "projectId">);

View File

@@ -24,3 +24,19 @@ export const revocationReasonToCrlCode = (crlReason: CrlReason) => {
return x509.X509CrlReason.unspecified;
}
};
export const isCertChainValid = async (certificates: x509.X509Certificate[]) => {
if (certificates.length === 1) {
return true;
}
const leafCert = certificates[0];
const chain = new x509.X509ChainBuilder({
certificates: certificates.slice(1)
});
const chainItems = await chain.build(leafCert);
// chain.build() implicitly verifies the chain
return chainItems.length === certificates.length;
};

View File

@@ -4,6 +4,8 @@ import { TDbClient } from "@app/db";
import { IdentityAuthMethod, TableName, TIdentityAccessTokens } from "@app/db/schemas";
import { DatabaseError } from "@app/lib/errors";
import { ormify, selectAllTableCols } from "@app/lib/knex";
import { logger } from "@app/lib/logger";
import { QueueName } from "@app/queue";
export type TIdentityAccessTokenDALFactory = ReturnType<typeof identityAccessTokenDALFactory>;
@@ -95,6 +97,7 @@ export const identityAccessTokenDALFactory = (db: TDbClient) => {
};
const removeExpiredTokens = async (tx?: Knex) => {
logger.info(`${QueueName.DailyResourceCleanUp}: remove expired access token started`);
try {
const docs = (tx || db)(TableName.IdentityAccessToken)
.where({
@@ -131,7 +134,8 @@ export const identityAccessTokenDALFactory = (db: TDbClient) => {
});
})
.delete();
return await docs;
await docs;
logger.info(`${QueueName.DailyResourceCleanUp}: remove expired access token completed`);
} catch (error) {
throw new DatabaseError({ error, name: "IdentityAccessTokenPrune" });
}

View File

@@ -0,0 +1,4 @@
import picomatch from "picomatch";
export const doesFieldValueMatchOidcPolicy = (fieldValue: string, policyValue: string) =>
policyValue === fieldValue || picomatch.isMatch(fieldValue, policyValue);

View File

@@ -28,6 +28,7 @@ import { TIdentityAccessTokenDALFactory } from "../identity-access-token/identit
import { TIdentityAccessTokenJwtPayload } from "../identity-access-token/identity-access-token-types";
import { TOrgBotDALFactory } from "../org/org-bot-dal";
import { TIdentityOidcAuthDALFactory } from "./identity-oidc-auth-dal";
import { doesFieldValueMatchOidcPolicy } from "./identity-oidc-auth-fns";
import {
TAttachOidcAuthDTO,
TGetOidcAuthDTO,
@@ -123,7 +124,7 @@ export const identityOidcAuthServiceFactory = ({
}) as Record<string, string>;
if (identityOidcAuth.boundSubject) {
if (tokenData.sub !== identityOidcAuth.boundSubject) {
if (!doesFieldValueMatchOidcPolicy(tokenData.sub, identityOidcAuth.boundSubject)) {
throw new ForbiddenRequestError({
message: "Access denied: OIDC subject not allowed."
});
@@ -131,7 +132,11 @@ export const identityOidcAuthServiceFactory = ({
}
if (identityOidcAuth.boundAudiences) {
if (!identityOidcAuth.boundAudiences.split(", ").includes(tokenData.aud)) {
if (
!identityOidcAuth.boundAudiences
.split(", ")
.some((policyValue) => doesFieldValueMatchOidcPolicy(tokenData.aud, policyValue))
) {
throw new ForbiddenRequestError({
message: "Access denied: OIDC audience not allowed."
});
@@ -142,7 +147,9 @@ export const identityOidcAuthServiceFactory = ({
Object.keys(identityOidcAuth.boundClaims).forEach((claimKey) => {
const claimValue = (identityOidcAuth.boundClaims as Record<string, string>)[claimKey];
// handle both single and multi-valued claims
if (!claimValue.split(", ").some((claimEntry) => tokenData[claimKey] === claimEntry)) {
if (
!claimValue.split(", ").some((claimEntry) => doesFieldValueMatchOidcPolicy(tokenData[claimKey], claimEntry))
) {
throw new ForbiddenRequestError({
message: "Access denied: OIDC claim not allowed."
});

View File

@@ -5,6 +5,7 @@ import { TableName } from "@app/db/schemas";
import { DatabaseError } from "@app/lib/errors";
import { ormify } from "@app/lib/knex";
import { logger } from "@app/lib/logger";
import { QueueName } from "@app/queue";
export type TIdentityUaClientSecretDALFactory = ReturnType<typeof identityUaClientSecretDALFactory>;
@@ -30,7 +31,9 @@ export const identityUaClientSecretDALFactory = (db: TDbClient) => {
let deletedClientSecret: { id: string }[] = [];
let numberOfRetryOnFailure = 0;
let isRetrying = false;
logger.info(`${QueueName.DailyResourceCleanUp}: remove expired univesal auth client secret started`);
do {
try {
const findExpiredClientSecretQuery = (tx || db)(TableName.IdentityUaClientSecret)
@@ -39,7 +42,7 @@ export const identityUaClientSecretDALFactory = (db: TDbClient) => {
})
.orWhere((qb) => {
void qb
.where("clientSecretNumUses", ">", 0)
.where("clientSecretNumUsesLimit", ">", 0)
.andWhere(
"clientSecretNumUses",
">=",
@@ -71,7 +74,9 @@ export const identityUaClientSecretDALFactory = (db: TDbClient) => {
setTimeout(resolve, 10); // time to breathe for db
});
}
} while (deletedClientSecret.length > 0 || numberOfRetryOnFailure < MAX_RETRY_ON_FAILURE);
isRetrying = numberOfRetryOnFailure > 0;
} while (deletedClientSecret.length > 0 || (isRetrying && numberOfRetryOnFailure < MAX_RETRY_ON_FAILURE));
logger.info(`${QueueName.DailyResourceCleanUp}: remove expired univesal auth client secret completed`);
};
return { ...uaClientSecretOrm, incrementUsage, removeExpiredClientSecrets };

View File

@@ -460,16 +460,21 @@ const getAppsFlyio = async ({ accessToken }: { accessToken: string }) => {
*/
const getAppsCircleCI = async ({ accessToken }: { accessToken: string }) => {
const res = (
await request.get<{ reponame: string }[]>(`${IntegrationUrls.CIRCLECI_API_URL}/v1.1/projects`, {
headers: {
"Circle-Token": accessToken,
"Accept-Encoding": "application/json"
await request.get<{ reponame: string; username: string; vcs_url: string }[]>(
`${IntegrationUrls.CIRCLECI_API_URL}/v1.1/projects`,
{
headers: {
"Circle-Token": accessToken,
"Accept-Encoding": "application/json"
}
}
})
)
).data;
const apps = res?.map((a) => ({
name: a?.reponame
const apps = res.map((a) => ({
owner: a.username, // username maps to unique organization name in CircleCI
name: a.reponame, // reponame maps to project name within an organization in CircleCI
appId: a.vcs_url.split("/").pop() // vcs_url maps to the project id in CircleCI
}));
return apps;

View File

@@ -30,6 +30,7 @@ const getIntegrationSecretsV2 = async (
environment: string;
folderId: string;
depth: number;
secretPath: string;
decryptor: (value: Buffer | null | undefined) => string;
},
secretV2BridgeDAL: Pick<TSecretV2BridgeDALFactory, "find" | "findByFolderId">,
@@ -306,6 +307,7 @@ export const deleteIntegrationSecrets = async ({
? await getIntegrationSecretsV2(
{
environment: integration.environment.id,
secretPath: integration.secretPath,
projectId: integration.projectId,
folderId: folder.id,
depth: 1,

View File

@@ -1625,7 +1625,11 @@ const syncSecretsGitHub = async ({
await octokit.request("PUT /orgs/{org}/actions/secrets/{secret_name}", {
org: integration.owner as string,
secret_name: key,
visibility: "all",
visibility: metadata.githubVisibility ?? "all",
...(metadata.githubVisibility === "selected" && {
// we need to map the githubVisibilityRepoIds to numbers
selected_repository_ids: metadata.githubVisibilityRepoIds?.map(Number) ?? []
}),
encrypted_value: encryptedSecret,
key_id: repoPublicKey.key_id
});
@@ -1925,22 +1929,62 @@ const syncSecretsCircleCI = async ({
secrets: Record<string, { value: string; comment?: string }>;
accessToken: string;
}) => {
const circleciOrganizationDetail = (
await request.get(`${IntegrationUrls.CIRCLECI_API_URL}/v2/me/collaborations`, {
const getProjectSlug = async () => {
const requestConfig = {
headers: {
"Circle-Token": accessToken,
"Accept-Encoding": "application/json"
}
})
).data[0];
};
const { slug } = circleciOrganizationDetail;
try {
const projectDetails = (
await request.get<{ slug: string }>(
`${IntegrationUrls.CIRCLECI_API_URL}/v2/project/${integration.appId}`,
requestConfig
)
).data;
return projectDetails.slug;
} catch (err) {
if (err instanceof AxiosError) {
if (err.response?.data?.message !== "Not Found") {
throw new Error("Failed to get project slug from CircleCI during first attempt.");
}
}
}
// For backwards compatibility with old CircleCI integrations where we don't keep track of the organization name, so we can't filter by organization
try {
const circleCiOrganization = (
await request.get<{ slug: string; name: string }[]>(
`${IntegrationUrls.CIRCLECI_API_URL}/v2/me/collaborations`,
requestConfig
)
).data;
// Case 1: This is a new integration where the organization name is stored under `integration.owner`
if (integration.owner) {
const org = circleCiOrganization.find((o) => o.name === integration.owner);
if (org) {
return `${org.slug}/${integration.app}`;
}
}
// Case 2: This is an old integration where the organization name is not stored, so we have to assume the first organization is the correct one
return `${circleCiOrganization[0].slug}/${integration.app}`;
} catch (err) {
throw new Error("Failed to get project slug from CircleCI during second attempt.");
}
};
const projectSlug = await getProjectSlug();
// sync secrets to CircleCI
await Promise.all(
Object.keys(secrets).map(async (key) =>
request.post(
`${IntegrationUrls.CIRCLECI_API_URL}/v2/project/${slug}/${integration.app}/envvar`,
`${IntegrationUrls.CIRCLECI_API_URL}/v2/project/${projectSlug}/envvar`,
{
name: key,
value: secrets[key].value
@@ -1958,7 +2002,7 @@ const syncSecretsCircleCI = async ({
// get secrets from CircleCI
const getSecretsRes = (
await request.get<{ items: { name: string }[] }>(
`${IntegrationUrls.CIRCLECI_API_URL}/v2/project/${slug}/${integration.app}/envvar`,
`${IntegrationUrls.CIRCLECI_API_URL}/v2/project/${projectSlug}/envvar`,
{
headers: {
"Circle-Token": accessToken,
@@ -1972,15 +2016,12 @@ const syncSecretsCircleCI = async ({
await Promise.all(
getSecretsRes.map(async (sec) => {
if (!(sec.name in secrets)) {
return request.delete(
`${IntegrationUrls.CIRCLECI_API_URL}/v2/project/${slug}/${integration.app}/envvar/${sec.name}`,
{
headers: {
"Circle-Token": accessToken,
"Content-Type": "application/json"
}
return request.delete(`${IntegrationUrls.CIRCLECI_API_URL}/v2/project/${projectSlug}/envvar/${sec.name}`, {
headers: {
"Circle-Token": accessToken,
"Content-Type": "application/json"
}
);
});
}
})
);

View File

@@ -5,14 +5,18 @@ import { INTEGRATION } from "@app/lib/api-docs";
import { IntegrationMappingBehavior } from "../integration-auth/integration-list";
export const IntegrationMetadataSchema = z.object({
initialSyncBehavior: z.string().optional().describe(INTEGRATION.CREATE.metadata.initialSyncBehavoir),
secretPrefix: z.string().optional().describe(INTEGRATION.CREATE.metadata.secretPrefix),
secretSuffix: z.string().optional().describe(INTEGRATION.CREATE.metadata.secretSuffix),
initialSyncBehavior: z.string().optional().describe(INTEGRATION.CREATE.metadata.initialSyncBehavoir),
mappingBehavior: z
.nativeEnum(IntegrationMappingBehavior)
.optional()
.describe(INTEGRATION.CREATE.metadata.mappingBehavior),
shouldAutoRedeploy: z.boolean().optional().describe(INTEGRATION.CREATE.metadata.shouldAutoRedeploy),
secretGCPLabel: z
.object({
labelName: z.string(),
@@ -20,6 +24,7 @@ export const IntegrationMetadataSchema = z.object({
})
.optional()
.describe(INTEGRATION.CREATE.metadata.secretGCPLabel),
secretAWSTag: z
.array(
z.object({
@@ -29,7 +34,15 @@ export const IntegrationMetadataSchema = z.object({
)
.optional()
.describe(INTEGRATION.CREATE.metadata.secretAWSTag),
githubVisibility: z
.union([z.literal("selected"), z.literal("private"), z.literal("all")])
.optional()
.describe(INTEGRATION.CREATE.metadata.githubVisibility),
githubVisibilityRepoIds: z.array(z.string()).optional().describe(INTEGRATION.CREATE.metadata.githubVisibilityRepoIds),
kmsKeyId: z.string().optional().describe(INTEGRATION.CREATE.metadata.kmsKeyId),
shouldDisableDelete: z.boolean().optional().describe(INTEGRATION.CREATE.metadata.shouldDisableDelete),
shouldEnableDelete: z.boolean().optional().describe(INTEGRATION.CREATE.metadata.shouldEnableDelete),
shouldMaskSecrets: z.boolean().optional().describe(INTEGRATION.CREATE.metadata.shouldMaskSecrets),

View File

@@ -27,6 +27,10 @@ export type TCreateIntegrationDTO = {
key: string;
value: string;
}[];
githubVisibility?: string;
githubVisibilityRepoIds?: string[];
kmsKeyId?: string;
shouldDisableDelete?: boolean;
shouldMaskSecrets?: boolean;

View File

@@ -12,6 +12,7 @@ import {
} from "@app/db/schemas";
import { DatabaseError } from "@app/lib/errors";
import { buildFindFilter, ormify, selectAllTableCols, TFindFilter, TFindOpt, withTransaction } from "@app/lib/knex";
import { generateKnexQueryFromScim } from "@app/lib/knex/scim";
export type TOrgDALFactory = ReturnType<typeof orgDALFactory>;
@@ -280,6 +281,67 @@ export const orgDALFactory = (db: TDbClient) => {
.select(
selectAllTableCols(TableName.OrgMembership),
db.ref("email").withSchema(TableName.Users),
db.ref("isEmailVerified").withSchema(TableName.Users),
db.ref("username").withSchema(TableName.Users),
db.ref("firstName").withSchema(TableName.Users),
db.ref("lastName").withSchema(TableName.Users),
db.ref("scimEnabled").withSchema(TableName.Organization),
db.ref("externalId").withSchema(TableName.UserAliases)
)
.where({ isGhost: false });
if (limit) void query.limit(limit);
if (offset) void query.offset(offset);
if (sort) {
void query.orderBy(sort.map(([column, order, nulls]) => ({ column: column as string, order, nulls })));
}
const res = await query;
return res;
} catch (error) {
throw new DatabaseError({ error, name: "Find one" });
}
};
const findMembershipWithScimFilter = async (
orgId: string,
scimFilter: string | undefined,
{ offset, limit, sort, tx }: TFindOpt<TOrgMemberships> = {}
) => {
try {
const query = (tx || db.replicaNode())(TableName.OrgMembership)
// eslint-disable-next-line
.where(`${TableName.OrgMembership}.orgId`, orgId)
.where((qb) => {
if (scimFilter) {
void generateKnexQueryFromScim(qb, scimFilter, (attrPath) => {
switch (attrPath) {
case "active":
return `${TableName.OrgMembership}.isActive`;
case "userName":
return `${TableName.UserAliases}.externalId`;
case "name.givenName":
return `${TableName.Users}.firstName`;
case "name.familyName":
return `${TableName.Users}.lastName`;
case "email.value":
return `${TableName.Users}.email`;
default:
return null;
}
});
}
})
.join(TableName.Users, `${TableName.Users}.id`, `${TableName.OrgMembership}.userId`)
.join(TableName.Organization, `${TableName.Organization}.id`, `${TableName.OrgMembership}.orgId`)
.leftJoin(TableName.UserAliases, function joinUserAlias() {
this.on(`${TableName.UserAliases}.userId`, "=", `${TableName.OrgMembership}.userId`)
.andOn(`${TableName.UserAliases}.orgId`, "=", `${TableName.OrgMembership}.orgId`)
.andOn(`${TableName.UserAliases}.aliasType`, "=", (tx || db).raw("?", ["saml"]));
})
.select(
selectAllTableCols(TableName.OrgMembership),
db.ref("email").withSchema(TableName.Users),
db.ref("isEmailVerified").withSchema(TableName.Users),
db.ref("username").withSchema(TableName.Users),
db.ref("firstName").withSchema(TableName.Users),
db.ref("lastName").withSchema(TableName.Users),
@@ -314,6 +376,7 @@ export const orgDALFactory = (db: TDbClient) => {
updateById,
deleteById,
findMembership,
findMembershipWithScimFilter,
createMembership,
updateMembershipById,
deleteMembershipById,

View File

@@ -279,6 +279,34 @@ export const projectDALFactory = (db: TDbClient) => {
}
};
const findProjectWithOrg = async (projectId: string) => {
// we just need the project, and we need to include a new .organization field that includes the org from the orgId reference
const project = await db(TableName.Project)
.where({ [`${TableName.Project}.id` as "id"]: projectId })
.join(TableName.Organization, `${TableName.Organization}.id`, `${TableName.Project}.orgId`)
.select(
db.ref("id").withSchema(TableName.Organization).as("organizationId"),
db.ref("name").withSchema(TableName.Organization).as("organizationName")
)
.select(selectAllTableCols(TableName.Project))
.first();
if (!project) {
throw new BadRequestError({ message: "Project not found" });
}
return {
...ProjectsSchema.parse(project),
organization: {
id: project.organizationId,
name: project.organizationName
}
};
};
return {
...projectOrm,
findAllProjects,
@@ -288,6 +316,7 @@ export const projectDALFactory = (db: TDbClient) => {
findProjectById,
findProjectByFilter,
findProjectBySlug,
findProjectWithOrg,
checkProjectUpgradeStatus
};
};

View File

@@ -4,6 +4,8 @@ import { TDbClient } from "@app/db";
import { TableName, TSecretFolderVersions } from "@app/db/schemas";
import { DatabaseError } from "@app/lib/errors";
import { ormify, selectAllTableCols } from "@app/lib/knex";
import { logger } from "@app/lib/logger";
import { QueueName } from "@app/queue";
export type TSecretFolderVersionDALFactory = ReturnType<typeof secretFolderVersionDALFactory>;
@@ -65,6 +67,7 @@ export const secretFolderVersionDALFactory = (db: TDbClient) => {
};
const pruneExcessVersions = async () => {
logger.info(`${QueueName.DailyResourceCleanUp}: pruning secret folder versions started`);
try {
await db(TableName.SecretFolderVersion)
.with("folder_cte", (qb) => {
@@ -89,6 +92,7 @@ export const secretFolderVersionDALFactory = (db: TDbClient) => {
name: "Secret Folder Version Prune"
});
}
logger.info(`${QueueName.DailyResourceCleanUp}: pruning secret folder versions completed`);
};
return { ...secretFolderVerOrm, findLatestFolderVersions, findLatestVersionByFolderId, pruneExcessVersions };

View File

@@ -158,9 +158,12 @@ export const fnSecretsV2FromImports = async ({
depth?: number;
cyclicDetector?: Set<string>;
decryptor: (value?: Buffer | null) => string;
expandSecretReferences?: (
secrets: Record<string, { value?: string; comment?: string; skipMultilineEncoding?: boolean | null }>
) => Promise<Record<string, { value?: string; comment?: string; skipMultilineEncoding?: boolean | null }>>;
expandSecretReferences?: (inputSecret: {
value?: string;
skipMultilineEncoding?: boolean | null;
secretPath: string;
environment: string;
}) => Promise<string | undefined>;
}) => {
// avoid going more than a depth
if (depth >= LEVEL_BREAK) return [];
@@ -244,26 +247,21 @@ export const fnSecretsV2FromImports = async ({
});
if (expandSecretReferences) {
await Promise.all(
processedImports.map(async (processedImport) => {
const secretsGroupByKey = processedImport.secrets.reduce(
(acc, item) => {
acc[item.secretKey] = {
value: item.secretValue,
comment: item.secretComment,
skipMultilineEncoding: item.skipMultilineEncoding
};
return acc;
},
{} as Record<string, { value: string; comment?: string; skipMultilineEncoding?: boolean | null }>
);
// eslint-disable-next-line
await expandSecretReferences(secretsGroupByKey);
processedImport.secrets.forEach((decryptedSecret) => {
// eslint-disable-next-line no-param-reassign
decryptedSecret.secretValue = secretsGroupByKey[decryptedSecret.secretKey].value;
});
})
await Promise.allSettled(
processedImports.map((processedImport) =>
Promise.allSettled(
processedImport.secrets.map(async (decryptedSecret, index) => {
const expandedSecretValue = await expandSecretReferences({
value: decryptedSecret.secretValue,
secretPath: processedImport.secretPath,
environment: processedImport.environment,
skipMultilineEncoding: decryptedSecret.skipMultilineEncoding
});
// eslint-disable-next-line no-param-reassign
processedImport.secrets[index].secretValue = expandedSecretValue || "";
})
)
)
);
}

View File

@@ -4,6 +4,8 @@ import { TDbClient } from "@app/db";
import { TableName, TSecretSharing } from "@app/db/schemas";
import { DatabaseError } from "@app/lib/errors";
import { ormify, selectAllTableCols } from "@app/lib/knex";
import { logger } from "@app/lib/logger";
import { QueueName } from "@app/queue";
export type TSecretSharingDALFactory = ReturnType<typeof secretSharingDALFactory>;
@@ -30,6 +32,7 @@ export const secretSharingDALFactory = (db: TDbClient) => {
};
const pruneExpiredSharedSecrets = async (tx?: Knex) => {
logger.info(`${QueueName.DailyResourceCleanUp}: pruning expired shared secret started`);
try {
const today = new Date();
const docs = await (tx || db)(TableName.SecretSharing)
@@ -40,6 +43,7 @@ export const secretSharingDALFactory = (db: TDbClient) => {
tag: "",
iv: ""
});
logger.info(`${QueueName.DailyResourceCleanUp}: pruning expired shared secret completed`);
return docs;
} catch (error) {
throw new DatabaseError({ error, name: "pruneExpiredSharedSecrets" });

View File

@@ -1,3 +1,6 @@
import bcrypt from "bcrypt";
import { TSecretSharing } from "@app/db/schemas";
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service";
import { BadRequestError, ForbiddenRequestError, NotFoundError, UnauthorizedError } from "@app/lib/errors";
import { SecretSharingAccessType } from "@app/lib/types";
@@ -36,6 +39,7 @@ export const secretSharingServiceFactory = ({
iv,
tag,
name,
password,
accessType,
expiresAt,
expiresAfterViews
@@ -60,8 +64,10 @@ export const secretSharingServiceFactory = ({
throw new BadRequestError({ message: "Shared secret value too long" });
}
const hashedPassword = password ? await bcrypt.hash(password, 10) : null;
const newSharedSecret = await secretSharingDAL.create({
name,
password: hashedPassword,
encryptedValue,
hashedHex,
iv,
@@ -77,6 +83,7 @@ export const secretSharingServiceFactory = ({
};
const createPublicSharedSecret = async ({
password,
encryptedValue,
hashedHex,
iv,
@@ -102,7 +109,9 @@ export const secretSharingServiceFactory = ({
throw new BadRequestError({ message: "Shared secret value too long" });
}
const hashedPassword = password ? await bcrypt.hash(password, 10) : null;
const newSharedSecret = await secretSharingDAL.create({
password: hashedPassword,
encryptedValue,
hashedHex,
iv,
@@ -111,6 +120,7 @@ export const secretSharingServiceFactory = ({
expiresAfterViews,
accessType
});
return { id: newSharedSecret.id };
};
@@ -152,7 +162,21 @@ export const secretSharingServiceFactory = ({
};
};
const getActiveSharedSecretById = async ({ sharedSecretId, hashedHex, orgId }: TGetActiveSharedSecretByIdDTO) => {
const $decrementSecretViewCount = async (sharedSecret: TSecretSharing, sharedSecretId: string) => {
const { expiresAfterViews } = sharedSecret;
if (expiresAfterViews) {
// decrement view count if view count expiry set
await secretSharingDAL.updateById(sharedSecretId, { $decr: { expiresAfterViews: 1 } });
}
await secretSharingDAL.updateById(sharedSecretId, {
lastViewedAt: new Date()
});
};
/** Get's passwordless secret. validates all secret's requested (must be fresh). */
const getSharedSecretById = async ({ sharedSecretId, hashedHex, orgId, password }: TGetActiveSharedSecretByIdDTO) => {
const sharedSecret = await secretSharingDAL.findOne({
id: sharedSecretId,
hashedHex
@@ -169,6 +193,8 @@ export const secretSharingServiceFactory = ({
if (accessType === SecretSharingAccessType.Organization && orgId !== sharedSecret.orgId)
throw new UnauthorizedError();
// all secrets pass through here, meaning we check if its expired first and then check if it needs verification
// or can be safely sent to the client.
if (expiresAt !== null && expiresAt < new Date()) {
// check lifetime expiry
await secretSharingDAL.softDeleteById(sharedSecretId);
@@ -185,21 +211,29 @@ export const secretSharingServiceFactory = ({
});
}
if (expiresAfterViews) {
// decrement view count if view count expiry set
await secretSharingDAL.updateById(sharedSecretId, { $decr: { expiresAfterViews: 1 } });
const isPasswordProtected = Boolean(sharedSecret.password);
const hasProvidedPassword = Boolean(password);
if (isPasswordProtected) {
if (hasProvidedPassword) {
const isMatch = await bcrypt.compare(password as string, sharedSecret.password as string);
if (!isMatch) throw new UnauthorizedError({ message: "Invalid credentials" });
} else {
return { isPasswordProtected };
}
}
await secretSharingDAL.updateById(sharedSecretId, {
lastViewedAt: new Date()
});
// decrement when we are sure the user will view secret.
await $decrementSecretViewCount(sharedSecret, sharedSecretId);
return {
...sharedSecret,
orgName:
sharedSecret.accessType === SecretSharingAccessType.Organization && orgId === sharedSecret.orgId
? orgName
: undefined
isPasswordProtected,
secret: {
...sharedSecret,
orgName:
sharedSecret.accessType === SecretSharingAccessType.Organization && orgId === sharedSecret.orgId
? orgName
: undefined
}
};
};
@@ -216,6 +250,6 @@ export const secretSharingServiceFactory = ({
createPublicSharedSecret,
getSharedSecrets,
deleteSharedSecretById,
getActiveSharedSecretById
getSharedSecretById
};
};

View File

@@ -15,6 +15,7 @@ export type TSharedSecretPermission = {
orgId: string;
accessType?: SecretSharingAccessType;
name?: string;
password?: string;
};
export type TCreatePublicSharedSecretDTO = {
@@ -24,6 +25,7 @@ export type TCreatePublicSharedSecretDTO = {
tag: string;
expiresAt: string;
expiresAfterViews?: number;
password?: string;
accessType: SecretSharingAccessType;
};
@@ -31,6 +33,11 @@ export type TGetActiveSharedSecretByIdDTO = {
sharedSecretId: string;
hashedHex: string;
orgId?: string;
password?: string;
};
export type TValidateActiveSharedSecretDTO = TGetActiveSharedSecretByIdDTO & {
password: string;
};
export type TCreateSharedSecretDTO = TSharedSecretPermission & TCreatePublicSharedSecretDTO;

View File

@@ -377,150 +377,116 @@ type TInterpolateSecretArg = {
folderDAL: Pick<TSecretFolderDALFactory, "findBySecretPath">;
};
const MAX_SECRET_REFERENCE_DEPTH = 10;
export const expandSecretReferencesFactory = ({
projectId,
decryptSecretValue: decryptSecret,
secretDAL,
folderDAL
}: TInterpolateSecretArg) => {
const fetchSecretFactory = () => {
const secretCache: Record<string, Record<string, string>> = {};
const secretCache: Record<string, Record<string, string>> = {};
const getCacheUniqueKey = (environment: string, secretPath: string) => `${environment}-${secretPath}`;
return async (secRefEnv: string, secRefPath: string[], secRefKey: string) => {
const referredSecretPathURL = path.join("/", ...secRefPath);
const uniqueKey = `${secRefEnv}-${referredSecretPathURL}`;
const fetchSecret = async (environment: string, secretPath: string, secretKey: string) => {
const cacheKey = getCacheUniqueKey(environment, secretPath);
if (secretCache?.[uniqueKey]) {
return secretCache[uniqueKey][secRefKey];
}
if (secretCache?.[cacheKey]) {
return secretCache[cacheKey][secretKey] || "";
}
const folder = await folderDAL.findBySecretPath(projectId, secRefEnv, referredSecretPathURL);
if (!folder) return "";
const secrets = await secretDAL.findByFolderId(folder.id);
const folder = await folderDAL.findBySecretPath(projectId, environment, secretPath);
if (!folder) return "";
const secrets = await secretDAL.findByFolderId(folder.id);
const decryptedSecret = secrets.reduce<Record<string, string>>((prev, secret) => {
// eslint-disable-next-line
prev[secret.key] = decryptSecret(secret.encryptedValue) || "";
return prev;
}, {});
const decryptedSecret = secrets.reduce<Record<string, string>>((prev, secret) => {
// eslint-disable-next-line no-param-reassign
prev[secret.key] = decryptSecret(secret.encryptedValue) || "";
return prev;
}, {});
secretCache[uniqueKey] = decryptedSecret;
secretCache[cacheKey] = decryptedSecret;
return secretCache[uniqueKey][secRefKey];
};
return secretCache[cacheKey][secretKey] || "";
};
const recursivelyExpandSecret = async (
expandedSec: Record<string, string | undefined>,
interpolatedSec: Record<string, string | undefined>,
fetchSecret: (env: string, secPath: string[], secKey: string) => Promise<string>,
recursionChainBreaker: Record<string, boolean>,
key: string
): Promise<string | undefined> => {
if (expandedSec?.[key] !== undefined) {
return expandedSec[key];
}
if (recursionChainBreaker?.[key]) {
return "";
}
// eslint-disable-next-line
recursionChainBreaker[key] = true;
const recursivelyExpandSecret = async (dto: { value?: string; secretPath: string; environment: string }) => {
if (!dto.value) return "";
let interpolatedValue = interpolatedSec[key];
if (!interpolatedValue) {
// eslint-disable-next-line no-console
console.error(`Couldn't find referenced value - ${key}`);
return "";
}
const stack = [{ ...dto, depth: 0 }];
let expandedValue = dto.value;
const refs = interpolatedValue.match(INTERPOLATION_SYNTAX_REG);
if (refs) {
for (const interpolationSyntax of refs) {
const interpolationKey = interpolationSyntax.slice(2, interpolationSyntax.length - 1);
const entities = interpolationKey.trim().split(".");
while (stack.length) {
const { value, secretPath, environment, depth } = stack.pop()!;
// eslint-disable-next-line no-continue
if (depth > MAX_SECRET_REFERENCE_DEPTH) continue;
const refs = value?.match(INTERPOLATION_SYNTAX_REG);
if (entities.length === 1) {
// eslint-disable-next-line
const val = await recursivelyExpandSecret(
expandedSec,
interpolatedSec,
fetchSecret,
recursionChainBreaker,
interpolationKey
);
if (val) {
interpolatedValue = interpolatedValue.replaceAll(interpolationSyntax, val);
}
// eslint-disable-next-line
continue;
}
if (refs) {
for (const interpolationSyntax of refs) {
const interpolationKey = interpolationSyntax.slice(2, interpolationSyntax.length - 1);
const entities = interpolationKey.trim().split(".");
if (entities.length > 1) {
const secRefEnv = entities[0];
const secRefPath = entities.slice(1, entities.length - 1);
const secRefKey = entities[entities.length - 1];
// eslint-disable-next-line no-continue
if (!entities.length) continue;
// eslint-disable-next-line
const val = await fetchSecret(secRefEnv, secRefPath, secRefKey);
if (val) {
interpolatedValue = interpolatedValue.replaceAll(interpolationSyntax, val);
if (entities.length === 1) {
const [secretKey] = entities;
// eslint-disable-next-line no-continue,no-await-in-loop
const referedValue = await fetchSecret(environment, secretPath, secretKey);
const cacheKey = getCacheUniqueKey(environment, secretPath);
secretCache[cacheKey][secretKey] = referedValue;
if (INTERPOLATION_SYNTAX_REG.test(referedValue)) {
stack.push({
value: referedValue,
secretPath,
environment,
depth: depth + 1
});
}
expandedValue = expandedValue.replaceAll(interpolationSyntax, referedValue);
} else {
const secretReferenceEnvironment = entities[0];
const secretReferencePath = path.join("/", ...entities.slice(1, entities.length - 1));
const secretReferenceKey = entities[entities.length - 1];
// eslint-disable-next-line no-await-in-loop
const referedValue = await fetchSecret(secretReferenceEnvironment, secretReferencePath, secretReferenceKey);
const cacheKey = getCacheUniqueKey(secretReferenceEnvironment, secretReferencePath);
secretCache[cacheKey][secretReferenceKey] = referedValue;
if (INTERPOLATION_SYNTAX_REG.test(referedValue)) {
stack.push({
value: referedValue,
secretPath: secretReferencePath,
environment: secretReferenceEnvironment,
depth: depth + 1
});
}
expandedValue = expandedValue.replaceAll(interpolationSyntax, referedValue);
}
}
}
}
// eslint-disable-next-line
expandedSec[key] = interpolatedValue;
return interpolatedValue;
return expandedValue;
};
const fetchSecret = fetchSecretFactory();
const expandSecrets = async (
inputSecrets: Record<string, { value?: string; comment?: string; skipMultilineEncoding?: boolean | null }>
) => {
const expandedSecrets: Record<string, string | undefined> = {};
const toBeExpandedSecrets: Record<string, string | undefined> = {};
const expandSecret = async (inputSecret: {
value?: string;
skipMultilineEncoding?: boolean | null;
secretPath: string;
environment: string;
}) => {
if (!inputSecret.value) return inputSecret.value;
Object.keys(inputSecrets).forEach((key) => {
if (inputSecrets[key].value?.match(INTERPOLATION_SYNTAX_REG)) {
toBeExpandedSecrets[key] = inputSecrets[key].value;
} else {
expandedSecrets[key] = inputSecrets[key].value;
}
});
const shouldExpand = Boolean(inputSecret.value?.match(INTERPOLATION_SYNTAX_REG));
if (!shouldExpand) return inputSecret.value;
for (const key of Object.keys(inputSecrets)) {
if (expandedSecrets?.[key]) {
// should not do multi line encoding if user has set it to skip
// eslint-disable-next-line
inputSecrets[key].value = inputSecrets[key].skipMultilineEncoding
? formatMultiValueEnv(expandedSecrets[key])
: expandedSecrets[key];
// eslint-disable-next-line
continue;
}
// this is to avoid recursion loop. So the graph should be direct graph rather than cyclic
// so for any recursion building if there is an entity two times same key meaning it will be looped
const recursionChainBreaker: Record<string, boolean> = {};
// eslint-disable-next-line
const expandedVal = await recursivelyExpandSecret(
expandedSecrets,
toBeExpandedSecrets,
fetchSecret,
recursionChainBreaker,
key
);
// eslint-disable-next-line
inputSecrets[key].value = inputSecrets[key].skipMultilineEncoding
? formatMultiValueEnv(expandedVal)
: expandedVal;
}
return inputSecrets;
const expandedSecretValue = await recursivelyExpandSecret(inputSecret);
return inputSecret.skipMultilineEncoding ? formatMultiValueEnv(expandedSecretValue) : expandedSecretValue;
};
return expandSecrets;
return expandSecret;
};
export const reshapeBridgeSecret = (

View File

@@ -521,27 +521,22 @@ export const secretV2BridgeServiceFactory = ({
if (shouldExpandSecretReferences) {
const secretsGroupByPath = groupBy(filteredSecrets, (i) => i.secretPath);
for (const secretPathKey in secretsGroupByPath) {
if (Object.hasOwn(secretsGroupByPath, secretPathKey)) {
const secretsGroupByKey = secretsGroupByPath[secretPathKey].reduce(
(acc, item) => {
acc[item.secretKey] = {
value: item.secretValue,
comment: item.secretComment,
skipMultilineEncoding: item.skipMultilineEncoding
};
return acc;
},
{} as Record<string, { value?: string; comment?: string; skipMultilineEncoding?: boolean | null }>
);
// eslint-disable-next-line
await expandSecretReferences(secretsGroupByKey);
secretsGroupByPath[secretPathKey].forEach((decryptedSecret) => {
// eslint-disable-next-line no-param-reassign
decryptedSecret.secretValue = secretsGroupByKey[decryptedSecret.secretKey].value || "";
});
}
}
await Promise.allSettled(
Object.keys(secretsGroupByPath).map((groupedPath) =>
Promise.allSettled(
secretsGroupByPath[groupedPath].map(async (decryptedSecret, index) => {
const expandedSecretValue = await expandSecretReferences({
value: decryptedSecret.secretValue,
secretPath: groupedPath,
environment,
skipMultilineEncoding: decryptedSecret.skipMultilineEncoding
});
// eslint-disable-next-line no-param-reassign
secretsGroupByPath[groupedPath][index].secretValue = expandedSecretValue || "";
})
)
)
);
}
if (!includeImports) {
@@ -693,12 +688,14 @@ export const secretV2BridgeServiceFactory = ({
? secretManagerDecryptor({ cipherTextBlob: secret.encryptedValue }).toString()
: "";
if (shouldExpandSecretReferences && secretValue) {
const secretReferenceExpandedRecord = {
[secret.key]: { value: secretValue }
};
// eslint-disable-next-line
await expandSecretReferences(secretReferenceExpandedRecord);
secretValue = secretReferenceExpandedRecord[secret.key].value;
const expandedSecretValue = await expandSecretReferences({
environment,
secretPath: path,
value: secretValue,
skipMultilineEncoding: secret.skipMultilineEncoding
});
secretValue = expandedSecretValue || "";
}
return reshapeBridgeSecret(projectId, environment, path, {

View File

@@ -4,6 +4,8 @@ import { TDbClient } from "@app/db";
import { TableName, TSecretVersionsV2, TSecretVersionsV2Update } from "@app/db/schemas";
import { BadRequestError, DatabaseError } from "@app/lib/errors";
import { ormify, selectAllTableCols } from "@app/lib/knex";
import { logger } from "@app/lib/logger";
import { QueueName } from "@app/queue";
export type TSecretVersionV2DALFactory = ReturnType<typeof secretVersionV2BridgeDALFactory>;
@@ -87,6 +89,7 @@ export const secretVersionV2BridgeDALFactory = (db: TDbClient) => {
};
const pruneExcessVersions = async () => {
logger.info(`${QueueName.DailyResourceCleanUp}: pruning secret version v2 started`);
try {
await db(TableName.SecretVersionV2)
.with("version_cte", (qb) => {
@@ -112,6 +115,7 @@ export const secretVersionV2BridgeDALFactory = (db: TDbClient) => {
name: "Secret Version Prune"
});
}
logger.info(`${QueueName.DailyResourceCleanUp}: pruning secret version v2 completed`);
};
return {

View File

@@ -196,6 +196,13 @@ export const recursivelyGetSecretPaths = ({
return getPaths;
};
// used to convert multi line ones to quotes ones with \n
const formatMultiValueEnv = (val?: string) => {
if (!val) return "";
if (!val.match("\n")) return val;
return `"${val.replace(/\n/g, "\\n")}"`;
};
type TInterpolateSecretArg = {
projectId: string;
secretEncKey: string;
@@ -203,162 +210,128 @@ type TInterpolateSecretArg = {
folderDAL: Pick<TSecretFolderDALFactory, "findBySecretPath">;
};
const MAX_SECRET_REFERENCE_DEPTH = 5;
const INTERPOLATION_SYNTAX_REG = /\${([^}]+)}/g;
export const interpolateSecrets = ({ projectId, secretEncKey, secretDAL, folderDAL }: TInterpolateSecretArg) => {
const fetchSecretsCrossEnv = () => {
const fetchCache: Record<string, Record<string, string>> = {};
const secretCache: Record<string, Record<string, string>> = {};
const getCacheUniqueKey = (environment: string, secretPath: string) => `${environment}-${secretPath}`;
return async (secRefEnv: string, secRefPath: string[], secRefKey: string) => {
const secRefPathUrl = path.join("/", ...secRefPath);
const uniqKey = `${secRefEnv}-${secRefPathUrl}`;
const fetchSecret = async (environment: string, secretPath: string, secretKey: string) => {
const cacheKey = getCacheUniqueKey(environment, secretPath);
const uniqKey = `${environment}-${cacheKey}`;
if (fetchCache?.[uniqKey]) {
return fetchCache[uniqKey][secRefKey];
}
if (secretCache?.[uniqKey]) {
return secretCache[uniqKey][secretKey] || "";
}
const folder = await folderDAL.findBySecretPath(projectId, secRefEnv, secRefPathUrl);
if (!folder) return "";
const secrets = await secretDAL.findByFolderId(folder.id);
const folder = await folderDAL.findBySecretPath(projectId, environment, secretPath);
if (!folder) return "";
const secrets = await secretDAL.findByFolderId(folder.id);
const decryptedSec = secrets.reduce<Record<string, string>>((prev, secret) => {
const secretKey = decryptSymmetric128BitHexKeyUTF8({
ciphertext: secret.secretKeyCiphertext,
iv: secret.secretKeyIV,
tag: secret.secretKeyTag,
key: secretEncKey
});
const secretValue = decryptSymmetric128BitHexKeyUTF8({
ciphertext: secret.secretValueCiphertext,
iv: secret.secretValueIV,
tag: secret.secretValueTag,
key: secretEncKey
});
const decryptedSec = secrets.reduce<Record<string, string>>((prev, secret) => {
const decryptedSecretKey = decryptSymmetric128BitHexKeyUTF8({
ciphertext: secret.secretKeyCiphertext,
iv: secret.secretKeyIV,
tag: secret.secretKeyTag,
key: secretEncKey
});
const decryptedSecretValue = decryptSymmetric128BitHexKeyUTF8({
ciphertext: secret.secretValueCiphertext,
iv: secret.secretValueIV,
tag: secret.secretValueTag,
key: secretEncKey
});
// eslint-disable-next-line
prev[secretKey] = secretValue;
return prev;
}, {});
// eslint-disable-next-line
prev[decryptedSecretKey] = decryptedSecretValue;
return prev;
}, {});
fetchCache[uniqKey] = decryptedSec;
secretCache[uniqKey] = decryptedSec;
return fetchCache[uniqKey][secRefKey];
};
return secretCache[uniqKey][secretKey] || "";
};
const recursivelyExpandSecret = async (
expandedSec: Record<string, string>,
interpolatedSec: Record<string, string>,
fetchCrossEnv: (env: string, secPath: string[], secKey: string) => Promise<string>,
recursionChainBreaker: Record<string, boolean>,
key: string
) => {
if (expandedSec?.[key] !== undefined) {
return expandedSec[key];
}
if (recursionChainBreaker?.[key]) {
return "";
}
// eslint-disable-next-line
recursionChainBreaker[key] = true;
const recursivelyExpandSecret = async ({
value,
secretPath,
environment,
depth = 0
}: {
value?: string;
secretPath: string;
environment: string;
depth?: number;
}) => {
if (!value) return "";
if (depth > MAX_SECRET_REFERENCE_DEPTH) return "";
let interpolatedValue = interpolatedSec[key];
if (!interpolatedValue) {
// eslint-disable-next-line no-console
console.error(`Couldn't find referenced value - ${key}`);
return "";
}
const refs = interpolatedValue.match(INTERPOLATION_SYNTAX_REG);
const refs = value.match(INTERPOLATION_SYNTAX_REG);
let expandedValue = value;
if (refs) {
for (const interpolationSyntax of refs) {
const interpolationKey = interpolationSyntax.slice(2, interpolationSyntax.length - 1);
const entities = interpolationKey.trim().split(".");
if (entities.length === 1) {
const val = await recursivelyExpandSecret(
expandedSec,
interpolatedSec,
fetchCrossEnv,
recursionChainBreaker,
interpolationKey
);
if (val) {
interpolatedValue = interpolatedValue.replaceAll(interpolationSyntax, val);
}
const [secretKey] = entities;
// eslint-disable-next-line
continue;
let referenceValue = await fetchSecret(environment, secretPath, secretKey);
if (INTERPOLATION_SYNTAX_REG.test(referenceValue)) {
// eslint-disable-next-line
referenceValue = await recursivelyExpandSecret({
environment,
secretPath,
value: referenceValue,
depth: depth + 1
});
}
const cacheKey = getCacheUniqueKey(environment, secretPath);
secretCache[cacheKey][secretKey] = referenceValue;
expandedValue = expandedValue.replaceAll(interpolationSyntax, referenceValue);
}
if (entities.length > 1) {
const secRefEnv = entities[0];
const secRefPath = entities.slice(1, entities.length - 1);
const secRefKey = entities[entities.length - 1];
const secretReferenceEnvironment = entities[0];
const secretReferencePath = path.join("/", ...entities.slice(1, entities.length - 1));
const secretReferenceKey = entities[entities.length - 1];
const val = await fetchCrossEnv(secRefEnv, secRefPath, secRefKey);
if (val) {
interpolatedValue = interpolatedValue.replaceAll(interpolationSyntax, val);
// eslint-disable-next-line
let referenceValue = await fetchSecret(secretReferenceEnvironment, secretReferencePath, secretReferenceKey);
if (INTERPOLATION_SYNTAX_REG.test(referenceValue)) {
// eslint-disable-next-line
referenceValue = await recursivelyExpandSecret({
environment: secretReferenceEnvironment,
secretPath: secretReferencePath,
value: referenceValue,
depth: depth + 1
});
}
const cacheKey = getCacheUniqueKey(secretReferenceEnvironment, secretReferencePath);
secretCache[cacheKey][secretReferenceKey] = referenceValue;
expandedValue = expandedValue.replaceAll(interpolationSyntax, referenceValue);
}
}
}
// eslint-disable-next-line
expandedSec[key] = interpolatedValue;
return interpolatedValue;
return expandedValue;
};
// used to convert multi line ones to quotes ones with \n
const formatMultiValueEnv = (val?: string) => {
if (!val) return "";
if (!val.match("\n")) return val;
return `"${val.replace(/\n/g, "\\n")}"`;
const expandSecret = async (inputSecret: {
value?: string;
skipMultilineEncoding?: boolean | null;
secretPath: string;
environment: string;
}) => {
if (!inputSecret.value) return inputSecret.value;
const shouldExpand = Boolean(inputSecret.value?.match(INTERPOLATION_SYNTAX_REG));
if (!shouldExpand) return inputSecret.value;
const expandedSecretValue = await recursivelyExpandSecret(inputSecret);
return inputSecret.skipMultilineEncoding ? formatMultiValueEnv(expandedSecretValue) : expandedSecretValue;
};
const expandSecrets = async (
secrets: Record<string, { value: string; comment?: string; skipMultilineEncoding?: boolean | null }>
) => {
const expandedSec: Record<string, string> = {};
const interpolatedSec: Record<string, string> = {};
const crossSecEnvFetch = fetchSecretsCrossEnv();
Object.keys(secrets).forEach((key) => {
if (secrets[key].value.match(INTERPOLATION_SYNTAX_REG)) {
interpolatedSec[key] = secrets[key].value;
} else {
expandedSec[key] = secrets[key].value;
}
});
for (const key of Object.keys(secrets)) {
if (expandedSec?.[key]) {
// should not do multi line encoding if user has set it to skip
// eslint-disable-next-line
secrets[key].value = secrets[key].skipMultilineEncoding
? formatMultiValueEnv(expandedSec[key])
: expandedSec[key];
// eslint-disable-next-line
continue;
}
// this is to avoid recursion loop. So the graph should be direct graph rather than cyclic
// so for any recursion building if there is an entity two times same key meaning it will be looped
const recursionChainBreaker: Record<string, boolean> = {};
const expandedVal = await recursivelyExpandSecret(
expandedSec,
interpolatedSec,
crossSecEnvFetch,
recursionChainBreaker,
key
);
// eslint-disable-next-line
secrets[key].value = secrets[key].skipMultilineEncoding ? formatMultiValueEnv(expandedVal) : expandedVal;
}
return secrets;
};
return expandSecrets;
return expandSecret;
};
export const decryptSecretRaw = (

View File

@@ -258,6 +258,7 @@ export const secretQueueFactory = ({
const getIntegrationSecretsV2 = async (dto: {
projectId: string;
environment: string;
secretPath: string;
folderId: string;
depth: number;
decryptor: (value: Buffer | null | undefined) => string;
@@ -269,30 +270,36 @@ export const secretQueueFactory = ({
);
return content;
}
// process secrets in current folder
const secrets = await secretV2BridgeDAL.findByFolderId(dto.folderId);
secrets.forEach((secret) => {
const secretKey = secret.key;
const secretValue = dto.decryptor(secret.encryptedValue);
content[secretKey] = { value: secretValue };
if (secret.encryptedComment) {
const commentValue = dto.decryptor(secret.encryptedComment);
content[secretKey].comment = commentValue;
}
content[secretKey].skipMultilineEncoding = Boolean(secret.skipMultilineEncoding);
});
const expandSecretReferences = expandSecretReferencesFactory({
decryptSecretValue: dto.decryptor,
secretDAL: secretV2BridgeDAL,
folderDAL,
projectId: dto.projectId
});
// process secrets in current folder
const secrets = await secretV2BridgeDAL.findByFolderId(dto.folderId);
await Promise.allSettled(
secrets.map(async (secret) => {
const secretKey = secret.key;
const secretValue = dto.decryptor(secret.encryptedValue);
const expandedSecretValue = await expandSecretReferences({
environment: dto.environment,
secretPath: dto.secretPath,
skipMultilineEncoding: secret.skipMultilineEncoding,
value: secretValue
});
content[secretKey] = { value: expandedSecretValue || "" };
if (secret.encryptedComment) {
const commentValue = dto.decryptor(secret.encryptedComment);
content[secretKey].comment = commentValue;
}
content[secretKey].skipMultilineEncoding = Boolean(secret.skipMultilineEncoding);
})
);
await expandSecretReferences(content);
// check if current folder has any imports from other folders
const secretImports = await secretImportDAL.find({ folderId: dto.folderId, isReplication: false });
@@ -329,6 +336,7 @@ export const secretQueueFactory = ({
const getIntegrationSecrets = async (dto: {
projectId: string;
environment: string;
secretPath: string;
folderId: string;
key: string;
depth: number;
@@ -341,46 +349,52 @@ export const secretQueueFactory = ({
return content;
}
// process secrets in current folder
const secrets = await secretDAL.findByFolderId(dto.folderId);
secrets.forEach((secret) => {
const secretKey = decryptSymmetric128BitHexKeyUTF8({
ciphertext: secret.secretKeyCiphertext,
iv: secret.secretKeyIV,
tag: secret.secretKeyTag,
key: dto.key
});
const secretValue = decryptSymmetric128BitHexKeyUTF8({
ciphertext: secret.secretValueCiphertext,
iv: secret.secretValueIV,
tag: secret.secretValueTag,
key: dto.key
});
content[secretKey] = { value: secretValue };
if (secret.secretCommentCiphertext && secret.secretCommentIV && secret.secretCommentTag) {
const commentValue = decryptSymmetric128BitHexKeyUTF8({
ciphertext: secret.secretCommentCiphertext,
iv: secret.secretCommentIV,
tag: secret.secretCommentTag,
key: dto.key
});
content[secretKey].comment = commentValue;
}
content[secretKey].skipMultilineEncoding = Boolean(secret.skipMultilineEncoding);
});
const expandSecrets = interpolateSecrets({
const expandSecretReferences = interpolateSecrets({
projectId: dto.projectId,
secretEncKey: dto.key,
folderDAL,
secretDAL
});
await expandSecrets(content);
// process secrets in current folder
const secrets = await secretDAL.findByFolderId(dto.folderId);
await Promise.allSettled(
secrets.map(async (secret) => {
const secretKey = decryptSymmetric128BitHexKeyUTF8({
ciphertext: secret.secretKeyCiphertext,
iv: secret.secretKeyIV,
tag: secret.secretKeyTag,
key: dto.key
});
const secretValue = decryptSymmetric128BitHexKeyUTF8({
ciphertext: secret.secretValueCiphertext,
iv: secret.secretValueIV,
tag: secret.secretValueTag,
key: dto.key
});
const expandedSecretValue = await expandSecretReferences({
environment: dto.environment,
secretPath: dto.secretPath,
skipMultilineEncoding: secret.skipMultilineEncoding,
value: secretValue
});
content[secretKey] = { value: expandedSecretValue || "" };
if (secret.secretCommentCiphertext && secret.secretCommentIV && secret.secretCommentTag) {
const commentValue = decryptSymmetric128BitHexKeyUTF8({
ciphertext: secret.secretCommentCiphertext,
iv: secret.secretCommentIV,
tag: secret.secretCommentTag,
key: dto.key
});
content[secretKey].comment = commentValue;
}
content[secretKey].skipMultilineEncoding = Boolean(secret.skipMultilineEncoding);
})
);
// check if current folder has any imports from other folders
const secretImport = await secretImportDAL.find({ folderId: dto.folderId, isReplication: false });
@@ -404,7 +418,8 @@ export const secretQueueFactory = ({
projectId: dto.projectId,
folderId: folder.id,
key: dto.key,
depth: dto.depth + 1
depth: dto.depth + 1,
secretPath: dto.secretPath
});
// add the imported secrets to the current folder secrets
@@ -686,6 +701,7 @@ export const secretQueueFactory = ({
projectId,
folderId: folder.id,
depth: 1,
secretPath,
decryptor: (value) => (value ? secretManagerDecryptor({ cipherTextBlob: value }).toString() : "")
})
: await getIntegrationSecrets({
@@ -693,7 +709,8 @@ export const secretQueueFactory = ({
projectId,
folderId: folder.id,
key: botKey as string,
depth: 1
depth: 1,
secretPath
});
for (const integration of toBeSyncedIntegrations) {

View File

@@ -482,7 +482,7 @@ export const secretServiceFactory = ({
projectId,
environmentSlug: folder.environment.slug
});
// TODO(akhilmhdh-pg): licence check, posthog service and snapshot
// TODO(akhilmhdh-pg): license check, posthog service and snapshot
return { ...deletedSecret[0], _id: deletedSecret[0].id, workspace: projectId, environment, secretPath: path };
};
@@ -1047,74 +1047,47 @@ export const secretServiceFactory = ({
};
});
const expandSecret = interpolateSecrets({
folderDAL,
projectId,
secretDAL,
secretEncKey: botKey
});
if (expandSecretReferences) {
const expandSecrets = interpolateSecrets({
folderDAL,
projectId,
secretDAL,
secretEncKey: botKey
});
const batchSecretsExpand = async (
secretBatch: {
secretKey: string;
secretValue: string;
secretComment?: string;
secretPath: string;
skipMultilineEncoding: boolean | null | undefined;
}[]
) => {
// Group secrets by secretPath
const secretsByPath: Record<
string,
{
secretKey: string;
secretValue: string;
secretComment?: string;
skipMultilineEncoding: boolean | null | undefined;
}[]
> = {};
secretBatch.forEach((secret) => {
if (!secretsByPath[secret.secretPath]) {
secretsByPath[secret.secretPath] = [];
}
secretsByPath[secret.secretPath].push(secret);
});
// Expand secrets for each group
for (const secPath in secretsByPath) {
if (!Object.hasOwn(secretsByPath, path)) {
// eslint-disable-next-line no-continue
continue;
}
const secretRecord: Record<
string,
{ value: string; comment?: string; skipMultilineEncoding: boolean | null | undefined }
> = {};
secretsByPath[secPath].forEach((decryptedSecret) => {
secretRecord[decryptedSecret.secretKey] = {
value: decryptedSecret.secretValue,
comment: decryptedSecret.secretComment,
skipMultilineEncoding: decryptedSecret.skipMultilineEncoding
};
});
await expandSecrets(secretRecord);
secretsByPath[secPath].forEach((decryptedSecret) => {
// eslint-disable-next-line no-param-reassign
decryptedSecret.secretValue = secretRecord[decryptedSecret.secretKey].value;
});
}
};
// expand secrets
await batchSecretsExpand(filteredSecrets);
// expand imports by batch
await Promise.all(processedImports.map((processedImport) => batchSecretsExpand(processedImport.secrets)));
const secretsGroupByPath = groupBy(filteredSecrets, (i) => i.secretPath);
await Promise.allSettled(
Object.keys(secretsGroupByPath).map((groupedPath) =>
Promise.allSettled(
secretsGroupByPath[groupedPath].map(async (decryptedSecret, index) => {
const expandedSecretValue = await expandSecret({
value: decryptedSecret.secretValue,
secretPath: groupedPath,
environment,
skipMultilineEncoding: decryptedSecret.skipMultilineEncoding
});
// eslint-disable-next-line no-param-reassign
secretsGroupByPath[groupedPath][index].secretValue = expandedSecretValue || "";
})
)
)
);
await Promise.allSettled(
processedImports.map((processedImport) =>
Promise.allSettled(
processedImport.secrets.map(async (decryptedSecret, index) => {
const expandedSecretValue = await expandSecret({
value: decryptedSecret.secretValue,
secretPath: path,
environment,
skipMultilineEncoding: decryptedSecret.skipMultilineEncoding
});
// eslint-disable-next-line no-param-reassign
processedImport.secrets[index].secretValue = expandedSecretValue || "";
})
)
)
);
}
return {
@@ -1177,40 +1150,19 @@ export const secretServiceFactory = ({
const decryptedSecret = decryptSecretRaw(encryptedSecret, botKey);
if (expandSecretReferences) {
const expandSecrets = interpolateSecrets({
const expandSecret = interpolateSecrets({
folderDAL,
projectId,
secretDAL,
secretEncKey: botKey
});
const expandSingleSecret = async (secret: {
secretKey: string;
secretValue: string;
secretComment?: string;
secretPath: string;
skipMultilineEncoding: boolean | null | undefined;
}) => {
const secretRecord: Record<
string,
{ value: string; comment?: string; skipMultilineEncoding: boolean | null | undefined }
> = {
[secret.secretKey]: {
value: secret.secretValue,
comment: secret.secretComment,
skipMultilineEncoding: secret.skipMultilineEncoding
}
};
await expandSecrets(secretRecord);
// Update the secret with the expanded value
// eslint-disable-next-line no-param-reassign
secret.secretValue = secretRecord[secret.secretKey].value;
};
// Expand the secret
await expandSingleSecret(decryptedSecret);
const expandedSecretValue = await expandSecret({
environment,
secretPath: path,
value: decryptedSecret.secretValue,
skipMultilineEncoding: decryptedSecret.skipMultilineEncoding
});
decryptedSecret.secretValue = expandedSecretValue || "";
}
return decryptedSecret;

View File

@@ -4,6 +4,8 @@ import { TDbClient } from "@app/db";
import { TableName, TSecretVersions, TSecretVersionsUpdate } from "@app/db/schemas";
import { BadRequestError, DatabaseError } from "@app/lib/errors";
import { ormify, selectAllTableCols } from "@app/lib/knex";
import { logger } from "@app/lib/logger";
import { QueueName } from "@app/queue";
export type TSecretVersionDALFactory = ReturnType<typeof secretVersionDALFactory>;
@@ -112,6 +114,7 @@ export const secretVersionDALFactory = (db: TDbClient) => {
};
const pruneExcessVersions = async () => {
logger.info(`${QueueName.DailyResourceCleanUp}: pruning secret version v1 started`);
try {
await db(TableName.SecretVersion)
.with("version_cte", (qb) => {
@@ -137,6 +140,7 @@ export const secretVersionDALFactory = (db: TDbClient) => {
name: "Secret Version Prune"
});
}
logger.info(`${QueueName.DailyResourceCleanUp}: pruning secret version v1 completed`);
};
return {

View File

@@ -25,6 +25,7 @@ export enum SmtpTemplates {
UnlockAccount = "unlockAccount.handlebars",
AccessApprovalRequest = "accessApprovalRequest.handlebars",
AccessSecretRequestBypassed = "accessSecretRequestBypassed.handlebars",
SecretApprovalRequestNeedsReview = "secretApprovalRequestNeedsReview.handlebars",
HistoricalSecretList = "historicalSecretLeakIncident.handlebars",
NewDeviceJoin = "newDevice.handlebars",
OrgInvite = "organizationInvitation.handlebars",

View File

@@ -0,0 +1,22 @@
<html>
<head>
<meta charset="utf-8" />
<meta http-equiv="x-ua-compatible" content="ie=edge" />
<title>Secret Change Approval Request</title>
</head>
<body>
<h2>Hi {{firstName}},</h2>
<h2>New secret change requests are pending review.</h2>
<br />
<p>You have a secret change request pending your review in project "{{projectName}}", in the "{{organizationName}}"
organization.</p>
<p>
View the request and approve or deny it
<a href="{{approvalUrl}}">here</a>.
</p>
</body>
</html>

View File

@@ -1,4 +1,4 @@
import tsconfigPaths from "vite-tsconfig-paths"; // only if you are using custom tsconfig paths
import path from "path";
import { defineConfig } from "vitest/config";
export default defineConfig({
@@ -15,7 +15,14 @@ export default defineConfig({
useAtomics: true,
isolate: false
}
},
alias: {
"./license-fns": path.resolve(__dirname, "./src/ee/services/license/__mocks__/license-fns")
}
},
plugins: [tsconfigPaths()] // only if you are using custom tsconfig paths,
resolve: {
alias: {
"@app": path.resolve(__dirname, "./src")
}
}
});

View File

@@ -11,12 +11,25 @@ sinks:
config:
path: "access-token"
templates:
- source-path: my-dot-ev-secret-template
- template-content: |
{{- with secret "202f04d7-e4cb-43d4-a292-e893712d61fc" "dev" "/" }}
{{- range . }}
{{ .Key }}={{ .Value }}
{{- end }}
{{- end }}
destination-path: my-dot-env-0.env
config:
polling-interval: 60s
execute:
command: docker-compose -f docker-compose.prod.yml down && docker-compose -f docker-compose.prod.yml up -d
- base64-template-content: e3stIHdpdGggc2VjcmV0ICIyMDJmMDRkNy1lNGNiLTQzZDQtYTI5Mi1lODkzNzEyZDYxZmMiICJkZXYiICIvIiB9fQp7ey0gcmFuZ2UgLiB9fQp7eyAuS2V5IH19PXt7IC5WYWx1ZSB9fQp7ey0gZW5kIH19Cnt7LSBlbmQgfX0=
destination-path: my-dot-env.env
config:
polling-interval: 60s
execute:
command: docker-compose -f docker-compose.prod.yml down && docker-compose -f docker-compose.prod.yml up -d
- source-path: my-dot-ev-secret-template1
destination-path: my-dot-env-1.env
config:

View File

@@ -95,6 +95,7 @@ type Template struct {
SourcePath string `yaml:"source-path"`
Base64TemplateContent string `yaml:"base64-template-content"`
DestinationPath string `yaml:"destination-path"`
TemplateContent string `yaml:"template-content"`
Config struct { // Configurations for the template
PollingInterval string `yaml:"polling-interval"` // How often to poll for changes in the secret
@@ -432,6 +433,30 @@ func ProcessBase64Template(templateId int, encodedTemplate string, data interfac
return &buf, nil
}
func ProcessLiteralTemplate(templateId int, templateString string, data interface{}, accessToken string, existingEtag string, currentEtag *string, dynamicSecretLeaser *DynamicSecretLeaseManager) (*bytes.Buffer, error) {
secretFunction := secretTemplateFunction(accessToken, existingEtag, currentEtag) // TODO: Fix this
dynamicSecretFunction := dynamicSecretTemplateFunction(accessToken, dynamicSecretLeaser, templateId)
funcs := template.FuncMap{
"secret": secretFunction,
"dynamic_secret": dynamicSecretFunction,
}
templateName := "literalTemplate"
tmpl, err := template.New(templateName).Funcs(funcs).Parse(templateString)
if err != nil {
return nil, err
}
var buf bytes.Buffer
if err := tmpl.Execute(&buf, data); err != nil {
return nil, err
}
return &buf, nil
}
type AgentManager struct {
accessToken string
accessTokenTTL time.Duration
@@ -820,6 +845,8 @@ func (tm *AgentManager) MonitorSecretChanges(secretTemplate Template, templateId
if secretTemplate.SourcePath != "" {
processedTemplate, err = ProcessTemplate(templateId, secretTemplate.SourcePath, nil, token, existingEtag, &currentEtag, tm.dynamicSecretLeases)
} else if secretTemplate.TemplateContent != "" {
processedTemplate, err = ProcessLiteralTemplate(templateId, secretTemplate.TemplateContent, nil, token, existingEtag, &currentEtag, tm.dynamicSecretLeases)
} else {
processedTemplate, err = ProcessBase64Template(templateId, secretTemplate.Base64TemplateContent, nil, token, existingEtag, &currentEtag, tm.dynamicSecretLeases)
}

View File

@@ -8,6 +8,7 @@ import (
"encoding/hex"
"encoding/json"
"os"
"slices"
"strings"
"time"
@@ -152,6 +153,28 @@ var loginCmd = &cobra.Command{
DisableFlagsInUseLine: true,
Run: func(cmd *cobra.Command, args []string) {
clearSelfHostedDomains, err := cmd.Flags().GetBool("clear-domains")
if err != nil {
util.HandleError(err)
}
if clearSelfHostedDomains {
infisicalConfig, err := util.GetConfigFile()
if err != nil {
util.HandleError(err)
}
infisicalConfig.Domains = []string{}
err = util.WriteConfigFile(&infisicalConfig)
if err != nil {
util.HandleError(err)
}
fmt.Println("Cleared all self-hosted domains from the config file")
return
}
infisicalClient := infisicalSdk.NewInfisicalClient(infisicalSdk.Config{
SiteUrl: config.INFISICAL_URL,
UserAgent: api.USER_AGENT,
@@ -464,6 +487,7 @@ func cliDefaultLogin(userCredentialsToBeStored *models.UserCredentials) {
func init() {
rootCmd.AddCommand(loginCmd)
loginCmd.Flags().Bool("clear-domains", false, "clear all self-hosting domains from the config file")
loginCmd.Flags().BoolP("interactive", "i", false, "login via the command line")
loginCmd.Flags().String("method", "user", "login method [user, universal-auth]")
loginCmd.Flags().Bool("plain", false, "only output the token without any formatting")
@@ -499,10 +523,12 @@ func DomainOverridePrompt() (bool, error) {
}
func askForDomain() error {
//query user to choose between Infisical cloud or self hosting
// query user to choose between Infisical cloud or self hosting
const (
INFISICAL_CLOUD = "Infisical Cloud"
SELF_HOSTING = "Self Hosting"
ADD_NEW_DOMAIN = "Add a new domain"
)
options := []string{INFISICAL_CLOUD, SELF_HOSTING}
@@ -524,6 +550,36 @@ func askForDomain() error {
return nil
}
infisicalConfig, err := util.GetConfigFile()
if err != nil {
return fmt.Errorf("askForDomain: unable to get config file because [err=%s]", err)
}
if infisicalConfig.Domains != nil && len(infisicalConfig.Domains) > 0 {
// If domains are present in the config, let the user select from the list or select to add a new domain
items := append(infisicalConfig.Domains, ADD_NEW_DOMAIN)
prompt := promptui.Select{
Label: "Which domain would you like to use?",
Items: items,
Size: 5,
}
_, selectedOption, err := prompt.Run()
if err != nil {
return err
}
if selectedOption != ADD_NEW_DOMAIN {
config.INFISICAL_URL = fmt.Sprintf("%s/api", selectedOption)
config.INFISICAL_LOGIN_URL = fmt.Sprintf("%s/login", selectedOption)
return nil
}
}
urlValidation := func(input string) error {
_, err := url.ParseRequestURI(input)
if err != nil {
@@ -542,12 +598,23 @@ func askForDomain() error {
if err != nil {
return err
}
//trimmed the '/' from the end of the self hosting url
// Trimmed the '/' from the end of the self hosting url, and set the api & login url
domain = strings.TrimRight(domain, "/")
//set api and login url
config.INFISICAL_URL = fmt.Sprintf("%s/api", domain)
config.INFISICAL_LOGIN_URL = fmt.Sprintf("%s/login", domain)
//return nil
// Write the new domain to the config file, to allow the user to select it in the future if needed
// First check if infiscialConfig.Domains already includes the domain, if it does, do not add it again
if !slices.Contains(infisicalConfig.Domains, domain) {
infisicalConfig.Domains = append(infisicalConfig.Domains, domain)
err = util.WriteConfigFile(&infisicalConfig)
if err != nil {
return fmt.Errorf("askForDomain: unable to write domains to config file because [err=%s]", err)
}
}
return nil
}

View File

@@ -16,6 +16,7 @@ type ConfigFile struct {
LoggedInUsers []LoggedInUser `json:"loggedInUsers,omitempty"`
VaultBackendType string `json:"vaultBackendType,omitempty"`
VaultBackendPassphrase string `json:"vaultBackendPassphrase,omitempty"`
Domains []string `json:"domains,omitempty"`
}
type LoggedInUser struct {

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