Compare commits

...

161 Commits

Author SHA1 Message Date
b176f13392 doc: add dynamic secrets to api references 2024-08-15 15:21:49 +08:00
4570de09ae Merge pull request #2290 from akhilmhdh/feat/replication-test
feat: resolved getSecretByName empty value from imported in kms arch
2024-08-14 16:05:01 -04:00
=
4feff5b4ca feat: resolved getSecretByName empty value from imported in kms arch 2024-08-15 01:24:54 +05:30
6081e2927e Merge pull request #2280 from rhythmbhiwani/fix-pagination-disappear
Fixed Pagination Disappearing on Secret Sharing Page
2024-08-14 14:54:37 -04:00
0b42f29916 Merge pull request #2289 from akhilmhdh/feat/replication-test
feat: added log point for aws tag and check for delete secret in bridge
2024-08-14 12:25:28 -04:00
=
b60d0992f4 feat: added log point for aws tag and check for delete secret in bridge 2024-08-14 21:42:07 +05:30
a8a68f600c Merge pull request #2287 from akhilmhdh/feat/replication-test
feat(ui): resolved a race condition in ui
2024-08-13 14:48:08 -04:00
=
742f5f6621 feat(ui): resolved a race condition in ui 2024-08-14 00:13:55 +05:30
f3cd7efe0e Merge pull request #2285 from akhilmhdh/feat/replication-test
feat: added more endpoints for delete
2024-08-13 12:41:54 -04:00
2b16c19b70 improve logs for aws ssm debug 2024-08-13 12:40:02 -04:00
=
943b540383 feat: added more endpoints for delete 2024-08-13 21:48:03 +05:30
e180021aa6 Merge pull request #2283 from akhilmhdh/feat/replication-test
feat: added debug points to test ssm integration in replication
2024-08-13 11:23:25 -04:00
=
8e08c443ad feat: added log to print operation based keys 2024-08-13 20:50:19 +05:30
=
dae26daeeb feat: added debug points to test ssm integration in replication 2024-08-13 20:40:53 +05:30
170f8d9add Merge pull request #2248 from Infisical/misc/addressed-reported-cli-behaviors
misc: addressed reported flaws with CLI usage
2024-08-13 12:49:20 +08:00
8d41ef198a Merge pull request #2282 from akhilmhdh/feat/client-secret-cleanup
fix: resolved secret approval broken due to tag name removal
2024-08-12 16:51:55 -04:00
=
69d60a227a fix: resolved secret approval broken due to tag name removal 2024-08-13 02:16:57 +05:30
c8eefcfbf9 Merge pull request #2281 from akhilmhdh/feat/client-secret-cleanup
feat: switched to ssm update as overwrite with tag as seperate operation
2024-08-12 16:38:57 -04:00
=
53cec754cc feat: switched to ssm update as overwrite with tag as seperate operation 2024-08-13 02:04:55 +05:30
5db3e177eb Fixed Pagination Disappearing on Secret Sharing Page 2024-08-13 02:01:25 +05:30
3fcc3ccff4 fix spending money tpyo 2024-08-12 12:41:15 -04:00
df07d7b6d7 update spending docs 2024-08-12 11:34:32 -04:00
28a655bef1 Merge pull request #2276 from akhilmhdh/feat/client-secret-cleanup
Client secret cleanup on resource cleanup queue
2024-08-12 11:01:46 -04:00
=
5f2cd04f46 feat: removed not needed condition 2024-08-12 20:29:05 +05:30
=
897ce1f267 chore: new reviewable command in root make file to check all the entities lint and type error 2024-08-12 13:19:55 +05:30
=
6afc17b84b feat: implemented universal auth client secret cleanup in resource cleanup queue 2024-08-12 13:19:25 +05:30
9017a5e838 Update spending-money.mdx 2024-08-12 01:29:45 -04:00
cb8e4d884e add equipment details to handbook 2024-08-11 23:17:58 -04:00
16807c3dd6 update k8s helm chart image tag 2024-08-11 13:09:22 -04:00
61791e385c update chart version of k8 2024-08-11 10:34:23 -04:00
bbd7bfb0f5 Merge pull request #2274 from MohamadTahir/fix-operator-bugs
Bug Fixes
2024-08-11 10:33:26 -04:00
4de8c48b2c Merge pull request #2270 from Ayush-Dutt-Sharma/ayush/minor-bug-#2269
replaced "creditnals" to "credentials"
2024-08-11 19:18:43 +05:30
a4bbe2c612 fix the client site url & the creation of new variable instead of updating the previous initiated variable 2024-08-11 16:46:48 +03:00
541a2e7d05 replaced "creditnals" to "credentials" 2024-08-11 14:10:49 +05:30
ea4e51d826 Merge pull request #2268 from Ayush-Dutt-Sharma/ayush/bug-2267-backend
better logging and while loop for ask propmt again
2024-08-10 19:48:05 +05:30
3bc920c593 better logging and while loop for ask propmt again 2024-08-10 15:36:43 +05:30
df38c761ad Merge pull request #2265 from akhilmhdh/fix/migration-switch-batch-insert
Secret migration switched to chunking based batch insert
2024-08-09 11:46:19 -04:00
=
32a84471f2 feat: added a new batch insert operation to convert inserts into chunks and updated secret migration 2024-08-09 21:02:26 +05:30
ea14df2cbd Merge pull request #2242 from akhilmhdh/fix/tag-filter-secret-api
Tag based filtering for secret endpoint
2024-08-09 20:33:43 +05:30
6bd6cac366 Merge pull request #2264 from Infisical/misc/addressed-misleading-google-saml-setup
misc: addressed misleading docs and placeholder values for Google SAML
2024-08-09 07:43:56 -07:00
45294253aa Merge pull request #2194 from GLEF1X/bugfix/yaml-exporting
fix(cli): make yaml exporting reliable and standardized
2024-08-09 10:01:26 -04:00
635fbdc80b misc: addressedm misleading docs and placeholder values for google saml 2024-08-09 21:29:33 +08:00
d20c48b7cf Merge pull request #2263 from Ayush-Dutt-Sharma/ayush/document-fixes
kubernetes operators integration doc fix
2024-08-09 14:59:50 +05:30
=
1fc18fe23b feat: added name in attach tag 2024-08-09 14:38:15 +05:30
99403e122b kubernetes operators integration doc fix 2024-08-09 14:33:29 +05:30
5176e70437 rephrase error messages 2024-08-08 18:15:13 -04:00
82b2b0af97 Merge pull request #2255 from akhilmhdh/feat/secret-get-personal
fix: resolved cli failign to get overriden secret in get command
2024-08-08 15:08:39 -04:00
e313c866a2 remove backup test for temp 2024-08-08 14:25:12 -04:00
2d81606049 update test with typo fix 2024-08-08 14:03:52 -04:00
718f4ef129 Merge pull request #2256 from Infisical/maidu-2321e
remove INFISICAL_VAULT_FILE_PASSPHRASE because it is being auto generated now
2024-08-08 13:52:05 -04:00
a42f3b3763 remove INFISICAL_VAULT_FILE_PASSPHRASE because it is being auto generated now 2024-08-08 13:50:34 -04:00
f7d882a6fc Merge pull request #2254 from akhilmhdh/fix/backup
Resolved keyring dataset too big by keeping only the encryption key
2024-08-08 13:19:50 -04:00
385afdfcf8 generate random string fn 2024-08-08 13:03:45 -04:00
281d703cc3 removeed vault use command and auto generated passphrase 2024-08-08 13:02:08 -04:00
6f56ed5474 add missing error logs on secrets backup 2024-08-08 13:01:14 -04:00
=
809e4eeba1 fix: resolved cli failign to get overriden secret in get command 2024-08-08 21:23:04 +05:30
=
254446c895 fix: resolved keyring dataset too big by keeping only the encryption key 2024-08-08 13:04:33 +05:30
bb52e2beb4 Update secret-tag-router.ts 2024-08-08 00:31:41 -04:00
2739b08e59 revert bb934ef7b1 2024-08-07 22:15:06 -04:00
ba5e877a3b Revert "add base64 package"
This reverts commit 4892eea009.
2024-08-07 22:14:08 -04:00
d2752216f6 Merge pull request #2253 from Infisical/revert-2252-maidul-dhusduqwdhj
Revert "Patch CLI auto select file vault "
2024-08-07 22:13:00 -04:00
d91fb0db02 Revert "Patch CLI auto select file vault " 2024-08-07 22:12:50 -04:00
4892eea009 add base64 package 2024-08-07 19:06:25 -04:00
09c6fcb73b Merge pull request #2252 from Infisical/maidul-dhusduqwdhj
Patch CLI auto select file vault
2024-08-07 19:03:38 -04:00
79181a1e3d remove os 2024-08-07 23:03:14 +00:00
bb934ef7b1 set vault type when auto selection enabled 2024-08-07 23:02:35 +00:00
cd9316537d prevent auto saving passphrase to disk 2024-08-07 18:56:15 -04:00
942e5f2f65 update phrase 2024-08-07 18:35:57 -04:00
353d231a4e Patch CLI auto select file vault
# Description 📣

When we auto select file vault, we also need to set it's type. When we set the type, we don't need to fall back to file vault in the `GetValueInKeyring` and `DeleteValueInKeyring` because `currentVaultBackend` will be `file`.

Also rephrased the text asking the user to eneter a passphrase.
2024-08-07 18:35:07 -04:00
68e05b7198 add debug log to print keyring error 2024-08-07 14:51:55 -04:00
4f998e3940 Merge pull request #2251 from akhilmhdh/fix/replication
fix: resolved replication secret not getting deleted
2024-08-07 11:57:14 -04:00
=
1248840dc8 fix: resolved replication secret not getting deleted 2024-08-07 21:23:22 +05:30
64c8125e4b add external secrets operator mention in k8s docs 2024-08-07 11:13:02 -04:00
=
c109fbab3e feat: removed tag name used in queries 2024-08-07 13:24:22 +05:30
=
15fb01089b feat: name removal in tag respective changes in frontend 2024-08-07 13:15:53 +05:30
=
6f4be3e25a feat: removed name from tag and stricter slugification for tag endpoint 2024-08-07 13:14:39 +05:30
8d33647739 Merge pull request #2249 from Infisical/maidul-sqhdqwdgvqwjf
patch findProjectUserWorkspaceKey
2024-08-06 22:12:03 +05:30
d1c142e5b1 patch findProjectUserWorkspaceKey 2024-08-06 12:39:06 -04:00
bb1cad0c5b Merge pull request #2223 from Infisical/misc/add-org-level-rate-limit
misc: moved to license-plan-based rate limits
2024-08-06 10:42:57 -04:00
2a1cfe15b4 update text when secrets deleted after integ delete 2024-08-06 10:07:41 -04:00
881d70bc64 Merge pull request #2238 from Infisical/feat/enabled-secrets-deletion-on-integ-removal
feat: added secrets deletion feature on integration removal
2024-08-06 09:54:15 -04:00
14c1b4f07b misc: hide not found text when flag plain is enabled 2024-08-06 21:21:45 +08:00
3028bdd424 misc: made local workspace file not required if using auth token 2024-08-06 21:06:14 +08:00
902a0b0ed4 Merge pull request #2243 from akhilmhdh/fix/missing-coment-field 2024-08-06 08:18:18 -04:00
ba92192537 misc: removed creation limits completely 2024-08-06 19:41:09 +08:00
26ed8df73c misc: finalized list of license rate limits 2024-08-06 19:14:49 +08:00
c1decab912 misc: addressed comments 2024-08-06 18:58:07 +08:00
=
216c073290 fix: missing comment key in updated project 2024-08-06 16:14:25 +05:30
=
8626bce632 feat: added tag support for secret operation in cli 2024-08-06 15:36:03 +05:30
=
c5a2b0321f feat: completed secret v3 raw to support tag based filtering 2024-08-06 15:35:00 +05:30
1070954bdd misc: used destructuring 2024-08-06 02:05:13 +08:00
cc689d3178 feat: added secrets deletion feature on integration removal 2024-08-06 01:52:58 +08:00
e6848828f2 Merge pull request #2184 from Infisical/daniel/keyring-cli-improvements
feat(cli): persistant `file` vault passphrase
2024-08-05 13:13:29 -04:00
c8b93e4467 Update doc to show correct command 2024-08-05 13:11:40 -04:00
0bca24bb00 Merge pull request #2235 from Infisical/handbook-update
add meetings article to handbook
2024-08-05 12:42:07 -04:00
c563ada50f Merge pull request #2237 from akhilmhdh/fix/bot-creation-failing
fix: resolved auto bot create failing on update
2024-08-05 11:15:25 -04:00
=
26d1616e22 fix: resolved auto bot create failing on update 2024-08-05 20:41:19 +05:30
5fd071d1de Merge pull request #2225 from akhilmhdh/feat/org-project-management
Feat/org project management
2024-08-05 10:21:09 -04:00
a6ac78356b rename org admin console subject name 2024-08-05 10:03:33 -04:00
e4a2137991 update permission action name for org admin console 2024-08-05 10:01:15 -04:00
9721d7a15e add meetings article to handbook 2024-08-04 14:04:09 -07:00
93db5c4555 Merge pull request #2234 from Infisical/maidul-mdjhquwqjhd
update broken image in ksm docs
2024-08-04 11:48:16 -04:00
ad4393fdef update broken image in ksm docs 2024-08-04 11:46:58 -04:00
cd06e4e7f3 hot patch 2024-08-03 19:05:34 -04:00
711a4179ce rename admin panel 2024-08-03 07:52:35 -04:00
=
b4a2a477d3 feat: brought back workspace permission and made requested changes 2024-08-03 14:55:30 +05:30
8e53a1b171 Merge pull request #2232 from Infisical/daniel/fix-lint
Fix: Linting
2024-08-02 22:00:28 -04:00
71af463ad8 fix format 2024-08-03 03:49:47 +02:00
7abd18b11c Merge pull request #2219 from LemmyMwaura/parse-secret-on-paste
feat: parse secrets (key,value) on paste
2024-08-03 03:33:17 +02:00
1aee50a751 Fix: Parser improvements and lint fixes 2024-08-03 03:29:45 +02:00
0f23b7e1d3 misc: added check for undefined orgId 2024-08-03 02:10:47 +08:00
e9b37a1f98 Merge pull request #2227 from Vishvsalvi/deleteActionModal-Placeholder
Placeholder value is same as it's label
2024-08-02 14:04:40 -04:00
33193a47ae misc: updated default onprem rate limits 2024-08-03 01:52:04 +08:00
43fded2350 refactor: take into account other delimiters 2024-08-02 20:41:47 +03:00
7b6f4d810d Placeholder value is same as it's label 2024-08-02 20:51:08 +05:30
1ad286ca87 misc: name updates and more comments 2024-08-02 22:58:53 +08:00
be7c11a3f5 Merge remote-tracking branch 'origin/main' into misc/add-org-level-rate-limit 2024-08-02 22:42:23 +08:00
=
b97bbe5beb feat: text change in sidebar 2024-08-02 19:54:43 +05:30
=
cf5260b383 feat: minor bug fix on access operation 2024-08-02 19:54:42 +05:30
=
13e0dd8e0f feat: completed org admin based project access feature 2024-08-02 19:54:42 +05:30
7f9150e60e Merge pull request #2226 from Infisical/maidul-wdqwdwf
Update docker-compose to docker compose in GHA
2024-08-02 19:54:17 +05:30
995f0360fb update docker-compsoe to docker compose 2024-08-02 10:22:21 -04:00
ecab69a7ab Merge pull request #2213 from Infisical/issue-cert-csr
Add Sign Certificate Endpoint for Certificate Issuance
2024-08-02 07:16:17 -07:00
cca36ab106 Merge remote-tracking branch 'origin' into issue-cert-csr 2024-08-02 07:06:58 -07:00
76311a1b5f Update DN parsing fn 2024-08-02 07:00:36 -07:00
55a6740714 misc: moved to plan-based rate limit 2024-08-02 21:37:48 +08:00
a0490d0fde Merge pull request #2220 from Infisical/feat/added-secret-folder-rbac
feat: added secret folder permissions
2024-08-02 19:05:12 +08:00
78e41a51c0 update workspace to project 2024-08-01 17:29:33 -04:00
8414f04e94 Merge pull request #2221 from akhilmhdh/feat/remove-migration-webhooks
feat: resolved invite failing and removed all unused things from frontend for previous upgrade
2024-08-01 11:18:50 -04:00
=
79e414ea9f feat: resolved invite failing and removed all unused things from frontend on previous upgrade 2024-08-01 20:12:23 +05:30
83772c1770 Merge pull request #2218 from GLEF1X/refactor/required-key-secret-input
refactor(secret-key-input): pass `isRequired` prop to secret key input
2024-08-01 10:35:23 -04:00
09928efba3 feat: added secret folder rbac' 2024-08-01 22:24:35 +08:00
48eb4e772f Merge pull request #2217 from akhilmhdh/feat/remove-migration-webhooks
feat: removed all the migration done for webhook and dynamic secret to KMS
2024-08-01 09:26:49 -04:00
7467a05fc4 fix(lint): fix triple equal strict check 2024-08-01 14:42:15 +03:00
afba636850 feat: parse full env secrets (key,value) when pasted from clipboard 2024-08-01 14:22:22 +03:00
96cc315762 refactor(secret-key-input): pass isRequired prop to secret key input 2024-08-01 06:22:49 -04:00
=
e95d7e55c1 feat: removed all the migration done for webhook and dynamic secret towards kms encryption 2024-08-01 13:39:41 +05:30
520c068ac4 Merge pull request #2209 from Infisical/doc/add-documentation-for-kms-with-aws-hsm
doc: added documentation for using AWS HSM
2024-07-31 21:37:23 -04:00
9f0d7c6d11 Correct sign-certificate endpoint ref in docs 2024-07-31 14:04:52 -07:00
683e3dd7be Add sign certificate endpoint 2024-07-31 13:57:47 -07:00
46ca3856b3 change upgrade btn based on admin 2024-07-31 10:59:36 -04:00
891cb06de0 Update keyringwrapper.go 2024-07-31 16:55:53 +02:00
02e8f20cbf remove extra : 2024-07-31 03:14:06 +00:00
dbe771dba0 refactor: remove unnecessary comment 2024-07-30 05:30:13 -04:00
273fd6c98f refactor: remove deprecated errors package
- Replace errors.Wrap with fmt.Errorf and %w verb
2024-07-30 05:23:43 -04:00
d5f4ce4376 Update vault.go 2024-07-30 10:22:15 +02:00
18aac6508b fix(cli): make yaml exporting reliable and standardized 2024-07-29 22:38:10 -04:00
85653a90d5 update phrasing 2024-07-29 22:06:03 -04:00
879ef2c178 Update keyringwrapper.go 2024-07-29 12:37:58 +02:00
8777cfe680 Update keyringwrapper.go 2024-07-29 12:34:35 +02:00
2b630f75aa Update keyringwrapper.go 2024-07-29 12:31:02 +02:00
91cee20cc8 Minor improvemnets 2024-07-29 12:21:38 +02:00
4249ec6030 Update login.go 2024-07-29 12:21:31 +02:00
e7a95e6af2 Update login.go 2024-07-29 12:15:53 +02:00
a9f04a3c1f Update keyringwrapper.go 2024-07-29 12:13:40 +02:00
3d380710ee Update keyringwrapper.go 2024-07-29 12:10:42 +02:00
2177ec6bcc Update vault.go 2024-07-29 12:04:34 +02:00
070eb2aacd Update keyringwrapper.go 2024-07-26 22:47:46 +02:00
e619cfa313 feat(cli): set persistent file vault password 2024-07-26 22:47:37 +02:00
c3038e3ca1 docs: passphrase command 2024-07-26 22:47:07 +02:00
ff0e7feeee feat(cli): CLI Keyring improvements 2024-07-26 19:14:21 +02:00
194 changed files with 4034 additions and 1962 deletions

View File

@ -22,14 +22,14 @@ jobs:
# uncomment this when testing locally using nektos/act
- uses: KengoTODA/actions-setup-docker-compose@v1
if: ${{ env.ACT }}
name: Install `docker-compose` for local simulations
name: Install `docker compose` for local simulations
with:
version: "2.14.2"
- name: 📦Build the latest image
run: docker build --tag infisical-api .
working-directory: backend
- name: Start postgres and redis
run: touch .env && docker-compose -f docker-compose.dev.yml up -d db redis
run: touch .env && docker compose -f docker-compose.dev.yml up -d db redis
- name: Start the server
run: |
echo "SECRET_SCANNING_GIT_APP_ID=793712" >> .env
@ -72,6 +72,6 @@ jobs:
run: oasdiff breaking https://app.infisical.com/api/docs/json http://localhost:4000/api/docs/json --fail-on ERR
- name: cleanup
run: |
docker-compose -f "docker-compose.dev.yml" down
docker compose -f "docker-compose.dev.yml" down
docker stop infisical-api
docker remove infisical-api

View File

@ -20,7 +20,7 @@ jobs:
uses: actions/checkout@v3
- uses: KengoTODA/actions-setup-docker-compose@v1
if: ${{ env.ACT }}
name: Install `docker-compose` for local simulations
name: Install `docker compose` for local simulations
with:
version: "2.14.2"
- name: 🔧 Setup Node 20
@ -33,7 +33,7 @@ jobs:
run: npm install
working-directory: backend
- name: Start postgres and redis
run: touch .env && docker-compose -f docker-compose.dev.yml up -d db redis
run: touch .env && docker compose -f docker-compose.dev.yml up -d db redis
- name: Start integration test
run: npm run test:e2e
working-directory: backend
@ -44,4 +44,4 @@ jobs:
ENCRYPTION_KEY: 4bnfe4e407b8921c104518903515b218
- name: cleanup
run: |
docker-compose -f "docker-compose.dev.yml" down
docker compose -f "docker-compose.dev.yml" down

View File

@ -50,6 +50,6 @@ jobs:
CLI_TESTS_ENV_SLUG: ${{ secrets.CLI_TESTS_ENV_SLUG }}
CLI_TESTS_USER_EMAIL: ${{ secrets.CLI_TESTS_USER_EMAIL }}
CLI_TESTS_USER_PASSWORD: ${{ secrets.CLI_TESTS_USER_PASSWORD }}
INFISICAL_VAULT_FILE_PASSPHRASE: ${{ secrets.CLI_TESTS_INFISICAL_VAULT_FILE_PASSPHRASE }}
# INFISICAL_VAULT_FILE_PASSPHRASE: ${{ secrets.CLI_TESTS_INFISICAL_VAULT_FILE_PASSPHRASE }}
run: go test -v -count=1 ./test

View File

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

View File

@ -25,6 +25,7 @@
"@fastify/swagger": "^8.14.0",
"@fastify/swagger-ui": "^2.1.0",
"@node-saml/passport-saml": "^4.0.4",
"@octokit/plugin-retry": "^5.0.5",
"@octokit/rest": "^20.0.2",
"@octokit/webhooks-types": "^7.3.1",
"@peculiar/asn1-schema": "^2.3.8",
@ -7812,19 +7813,45 @@
}
},
"node_modules/@octokit/plugin-retry": {
"version": "6.0.1",
"resolved": "https://registry.npmjs.org/@octokit/plugin-retry/-/plugin-retry-6.0.1.tgz",
"integrity": "sha512-SKs+Tz9oj0g4p28qkZwl/topGcb0k0qPNX/i7vBKmDsjoeqnVfFUquqrE/O9oJY7+oLzdCtkiWSXLpLjvl6uog==",
"version": "5.0.5",
"resolved": "https://registry.npmjs.org/@octokit/plugin-retry/-/plugin-retry-5.0.5.tgz",
"integrity": "sha512-sB1RWMhSrre02Atv95K6bhESlJ/sPdZkK/wE/w1IdSCe0yM6FxSjksLa6T7aAvxvxlLKzQEC4KIiqpqyov1Tbg==",
"dependencies": {
"@octokit/request-error": "^5.0.0",
"@octokit/types": "^12.0.0",
"@octokit/request-error": "^4.0.1",
"@octokit/types": "^10.0.0",
"bottleneck": "^2.15.3"
},
"engines": {
"node": ">= 18"
},
"peerDependencies": {
"@octokit/core": ">=5"
"@octokit/core": ">=3"
}
},
"node_modules/@octokit/plugin-retry/node_modules/@octokit/openapi-types": {
"version": "18.1.1",
"resolved": "https://registry.npmjs.org/@octokit/openapi-types/-/openapi-types-18.1.1.tgz",
"integrity": "sha512-VRaeH8nCDtF5aXWnjPuEMIYf1itK/s3JYyJcWFJT8X9pSNnBtriDf7wlEWsGuhPLl4QIH4xM8fqTXDwJ3Mu6sw=="
},
"node_modules/@octokit/plugin-retry/node_modules/@octokit/request-error": {
"version": "4.0.2",
"resolved": "https://registry.npmjs.org/@octokit/request-error/-/request-error-4.0.2.tgz",
"integrity": "sha512-uqwUEmZw3x4I9DGYq9fODVAAvcLsPQv97NRycP6syEFu5916M189VnNBW2zANNwqg3OiligNcAey7P0SET843w==",
"dependencies": {
"@octokit/types": "^10.0.0",
"deprecation": "^2.0.0",
"once": "^1.4.0"
},
"engines": {
"node": ">= 18"
}
},
"node_modules/@octokit/plugin-retry/node_modules/@octokit/types": {
"version": "10.0.0",
"resolved": "https://registry.npmjs.org/@octokit/types/-/types-10.0.0.tgz",
"integrity": "sha512-Vm8IddVmhCgU1fxC1eyinpwqzXPEYu0NrYzD3YZjlGjyftdLBTeqNblRC0jmJmgxbJIsQlyogVeGnrNaaMVzIg==",
"dependencies": {
"@octokit/openapi-types": "^18.0.0"
}
},
"node_modules/@octokit/plugin-throttling": {
@ -17396,6 +17423,22 @@
"node": ">=18"
}
},
"node_modules/probot/node_modules/@octokit/plugin-retry": {
"version": "6.0.1",
"resolved": "https://registry.npmjs.org/@octokit/plugin-retry/-/plugin-retry-6.0.1.tgz",
"integrity": "sha512-SKs+Tz9oj0g4p28qkZwl/topGcb0k0qPNX/i7vBKmDsjoeqnVfFUquqrE/O9oJY7+oLzdCtkiWSXLpLjvl6uog==",
"dependencies": {
"@octokit/request-error": "^5.0.0",
"@octokit/types": "^12.0.0",
"bottleneck": "^2.15.3"
},
"engines": {
"node": ">= 18"
},
"peerDependencies": {
"@octokit/core": ">=5"
}
},
"node_modules/probot/node_modules/commander": {
"version": "11.1.0",
"resolved": "https://registry.npmjs.org/commander/-/commander-11.1.0.tgz",

View File

@ -121,6 +121,7 @@
"@fastify/swagger": "^8.14.0",
"@fastify/swagger-ui": "^2.1.0",
"@node-saml/passport-saml": "^4.0.4",
"@octokit/plugin-retry": "^5.0.5",
"@octokit/rest": "^20.0.2",
"@octokit/webhooks-types": "^7.3.1",
"@peculiar/asn1-schema": "^2.3.8",

View File

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

View File

@ -18,6 +18,7 @@ import { TOidcConfigServiceFactory } from "@app/ee/services/oidc/oidc-config-ser
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service";
import { TProjectUserAdditionalPrivilegeServiceFactory } from "@app/ee/services/project-user-additional-privilege/project-user-additional-privilege-service";
import { TRateLimitServiceFactory } from "@app/ee/services/rate-limit/rate-limit-service";
import { RateLimitConfiguration } from "@app/ee/services/rate-limit/rate-limit-types";
import { TSamlConfigServiceFactory } from "@app/ee/services/saml-config/saml-config-service";
import { TScimServiceFactory } from "@app/ee/services/scim/scim-service";
import { TSecretApprovalPolicyServiceFactory } from "@app/ee/services/secret-approval-policy/secret-approval-policy-service";
@ -50,6 +51,7 @@ import { TIntegrationServiceFactory } from "@app/services/integration/integratio
import { TIntegrationAuthServiceFactory } from "@app/services/integration-auth/integration-auth-service";
import { TOrgRoleServiceFactory } from "@app/services/org/org-role-service";
import { TOrgServiceFactory } from "@app/services/org/org-service";
import { TOrgAdminServiceFactory } from "@app/services/org-admin/org-admin-service";
import { TProjectServiceFactory } from "@app/services/project/project-service";
import { TProjectBotServiceFactory } from "@app/services/project-bot/project-bot-service";
import { TProjectEnvServiceFactory } from "@app/services/project-env/project-env-service";
@ -88,6 +90,7 @@ declare module "fastify" {
id: string;
orgId: string;
};
rateLimits: RateLimitConfiguration;
// passport data
passportUser: {
isUserCompleted: string;
@ -165,6 +168,7 @@ declare module "fastify" {
rateLimit: TRateLimitServiceFactory;
userEngagement: TUserEngagementServiceFactory;
externalKms: TExternalKmsServiceFactory;
orgAdmin: TOrgAdminServiceFactory;
};
// this is exclusive use for middlewares in which we need to inject data
// everywhere else access using service layer

View File

@ -1,178 +1,8 @@
/* eslint-disable @typescript-eslint/ban-ts-comment */
import { Knex } from "knex";
import { infisicalSymmetricDecrypt } from "@app/lib/crypto/encryption";
import { selectAllTableCols } from "@app/lib/knex/select";
import { SecretKeyEncoding, SecretType, TableName } from "../schemas";
import { SecretType, TableName } from "../schemas";
import { createJunctionTable, createOnUpdateTrigger, dropOnUpdateTrigger } from "../utils";
import { getSecretManagerDataKey } from "./utils/kms";
const backfillWebhooks = async (knex: Knex) => {
const hasEncryptedSecretKeyWithKms = await knex.schema.hasColumn(TableName.Webhook, "encryptedSecretKeyWithKms");
const hasEncryptedWebhookUrl = await knex.schema.hasColumn(TableName.Webhook, "encryptedUrl");
const hasUrlCipherText = await knex.schema.hasColumn(TableName.Webhook, "urlCipherText");
const hasUrlIV = await knex.schema.hasColumn(TableName.Webhook, "urlIV");
const hasUrlTag = await knex.schema.hasColumn(TableName.Webhook, "urlTag");
const hasEncryptedSecretKey = await knex.schema.hasColumn(TableName.Webhook, "encryptedSecretKey");
const hasIV = await knex.schema.hasColumn(TableName.Webhook, "iv");
const hasTag = await knex.schema.hasColumn(TableName.Webhook, "tag");
const hasKeyEncoding = await knex.schema.hasColumn(TableName.Webhook, "keyEncoding");
const hasAlgorithm = await knex.schema.hasColumn(TableName.Webhook, "algorithm");
const hasUrl = await knex.schema.hasColumn(TableName.Webhook, "url");
await knex.schema.alterTable(TableName.Webhook, (t) => {
if (!hasEncryptedSecretKeyWithKms) t.binary("encryptedSecretKeyWithKms");
if (!hasEncryptedWebhookUrl) t.binary("encryptedUrl");
if (hasUrl) t.string("url").nullable().alter();
});
const kmsEncryptorGroupByProjectId: Record<string, Awaited<ReturnType<typeof getSecretManagerDataKey>>["encryptor"]> =
{};
if (hasUrlCipherText && hasUrlIV && hasUrlTag && hasEncryptedSecretKey && hasIV && hasTag) {
// eslint-disable-next-line
const webhooksToFill = await knex(TableName.Webhook)
.join(TableName.Environment, `${TableName.Environment}.id`, `${TableName.Webhook}.envId`)
.whereNull("encryptedUrl")
// eslint-disable-next-line
// @ts-ignore knex migration fails
.select(selectAllTableCols(TableName.Webhook))
.select("projectId");
const updatedWebhooks = [];
for (const webhook of webhooksToFill) {
if (!kmsEncryptorGroupByProjectId[webhook.projectId]) {
// eslint-disable-next-line
const { encryptor } = await getSecretManagerDataKey(knex, webhook.projectId);
kmsEncryptorGroupByProjectId[webhook.projectId] = encryptor;
}
const kmsEncryptor = kmsEncryptorGroupByProjectId[webhook.projectId];
// @ts-ignore post migration fails
let webhookUrl = webhook.url;
let webhookSecretKey;
// @ts-ignore post migration fails
if (webhook.urlTag && webhook.urlCipherText && webhook.urlIV) {
webhookUrl = infisicalSymmetricDecrypt({
// @ts-ignore post migration fails
keyEncoding: webhook.keyEncoding as SecretKeyEncoding,
// @ts-ignore post migration fails
ciphertext: webhook.urlCipherText,
// @ts-ignore post migration fails
iv: webhook.urlIV,
// @ts-ignore post migration fails
tag: webhook.urlTag
});
}
// @ts-ignore post migration fails
if (webhook.encryptedSecretKey && webhook.iv && webhook.tag) {
webhookSecretKey = infisicalSymmetricDecrypt({
// @ts-ignore post migration fails
keyEncoding: webhook.keyEncoding as SecretKeyEncoding,
// @ts-ignore post migration fails
ciphertext: webhook.encryptedSecretKey,
// @ts-ignore post migration fails
iv: webhook.iv,
// @ts-ignore post migration fails
tag: webhook.tag
});
}
const { projectId, ...el } = webhook;
updatedWebhooks.push({
...el,
encryptedSecretKeyWithKms: webhookSecretKey
? kmsEncryptor({ plainText: Buffer.from(webhookSecretKey) }).cipherTextBlob
: null,
encryptedUrl: kmsEncryptor({ plainText: Buffer.from(webhookUrl) }).cipherTextBlob
});
}
if (updatedWebhooks.length) {
// eslint-disable-next-line
await knex(TableName.Webhook).insert(updatedWebhooks).onConflict("id").merge();
}
}
await knex.schema.alterTable(TableName.Webhook, (t) => {
t.binary("encryptedUrl").notNullable().alter();
if (hasUrlIV) t.dropColumn("urlIV");
if (hasUrlCipherText) t.dropColumn("urlCipherText");
if (hasUrlTag) t.dropColumn("urlTag");
if (hasIV) t.dropColumn("iv");
if (hasTag) t.dropColumn("tag");
if (hasEncryptedSecretKey) t.dropColumn("encryptedSecretKey");
if (hasKeyEncoding) t.dropColumn("keyEncoding");
if (hasAlgorithm) t.dropColumn("algorithm");
if (hasUrl) t.dropColumn("url");
});
};
const backfillDynamicSecretConfigs = async (knex: Knex) => {
const hasEncryptedConfig = await knex.schema.hasColumn(TableName.DynamicSecret, "encryptedConfig");
const hasInputCipherText = await knex.schema.hasColumn(TableName.DynamicSecret, "inputCiphertext");
const hasInputIV = await knex.schema.hasColumn(TableName.DynamicSecret, "inputIV");
const hasInputTag = await knex.schema.hasColumn(TableName.DynamicSecret, "inputTag");
const hasKeyEncoding = await knex.schema.hasColumn(TableName.DynamicSecret, "keyEncoding");
const hasAlgorithm = await knex.schema.hasColumn(TableName.DynamicSecret, "algorithm");
await knex.schema.alterTable(TableName.DynamicSecret, (t) => {
if (!hasEncryptedConfig) t.binary("encryptedConfig");
});
const kmsEncryptorGroupByProjectId: Record<string, Awaited<ReturnType<typeof getSecretManagerDataKey>>["encryptor"]> =
{};
if (hasInputCipherText && hasInputIV && hasInputTag) {
// eslint-disable-next-line
const dynamicSecretConfigs = await knex(TableName.DynamicSecret)
.join(TableName.SecretFolder, `${TableName.SecretFolder}.id`, `${TableName.DynamicSecret}.folderId`)
.join(TableName.Environment, `${TableName.SecretFolder}.envId`, `${TableName.Environment}.id`)
.whereNull("encryptedConfig")
// @ts-ignore post migration fails
.select(selectAllTableCols(TableName.DynamicSecret))
.select("projectId");
const updatedConfigs = [];
for (const dynamicSecretConfig of dynamicSecretConfigs) {
if (!kmsEncryptorGroupByProjectId[dynamicSecretConfig.projectId]) {
// eslint-disable-next-line
const { encryptor } = await getSecretManagerDataKey(knex, dynamicSecretConfig.projectId);
kmsEncryptorGroupByProjectId[dynamicSecretConfig.projectId] = encryptor;
}
const kmsEncryptor = kmsEncryptorGroupByProjectId[dynamicSecretConfig.projectId];
const inputConfig = infisicalSymmetricDecrypt({
// @ts-ignore post migration fails
keyEncoding: dynamicSecretConfig.keyEncoding as SecretKeyEncoding,
// @ts-ignore post migration fails
ciphertext: dynamicSecretConfig.inputCiphertext as string,
// @ts-ignore post migration fails
iv: dynamicSecretConfig.inputIV as string,
// @ts-ignore post migration fails
tag: dynamicSecretConfig.inputTag as string
});
const { projectId, ...el } = dynamicSecretConfig;
updatedConfigs.push({
...el,
encryptedConfig: kmsEncryptor({ plainText: Buffer.from(inputConfig) }).cipherTextBlob
});
}
if (updatedConfigs.length) {
// eslint-disable-next-line
await knex(TableName.DynamicSecret).insert(updatedConfigs).onConflict("id").merge();
}
}
await knex.schema.alterTable(TableName.DynamicSecret, (t) => {
t.binary("encryptedConfig").notNullable().alter();
if (hasInputTag) t.dropColumn("inputTag");
if (hasInputIV) t.dropColumn("inputIV");
if (hasInputCipherText) t.dropColumn("inputCiphertext");
if (hasKeyEncoding) t.dropColumn("keyEncoding");
if (hasAlgorithm) t.dropColumn("algorithm");
});
};
export async function up(knex: Knex): Promise<void> {
const doesSecretV2TableExist = await knex.schema.hasTable(TableName.SecretV2);
@ -314,14 +144,6 @@ export async function up(knex: Knex): Promise<void> {
t.foreign("rotationId").references("id").inTable(TableName.SecretRotation).onDelete("CASCADE");
});
}
if (await knex.schema.hasTable(TableName.Webhook)) {
await backfillWebhooks(knex);
}
if (await knex.schema.hasTable(TableName.DynamicSecret)) {
await backfillDynamicSecretConfigs(knex);
}
}
export async function down(knex: Knex): Promise<void> {
@ -356,49 +178,4 @@ export async function down(knex: Knex): Promise<void> {
if (hasEncryptedAwsIamAssumRole) t.dropColumn("encryptedAwsAssumeIamRoleArn");
});
}
if (await knex.schema.hasTable(TableName.Webhook)) {
const hasEncryptedWebhookSecretKey = await knex.schema.hasColumn(TableName.Webhook, "encryptedSecretKeyWithKms");
const hasEncryptedWebhookUrl = await knex.schema.hasColumn(TableName.Webhook, "encryptedUrl");
const hasUrlCipherText = await knex.schema.hasColumn(TableName.Webhook, "urlCipherText");
const hasUrlIV = await knex.schema.hasColumn(TableName.Webhook, "urlIV");
const hasUrlTag = await knex.schema.hasColumn(TableName.Webhook, "urlTag");
const hasEncryptedSecretKey = await knex.schema.hasColumn(TableName.Webhook, "encryptedSecretKey");
const hasIV = await knex.schema.hasColumn(TableName.Webhook, "iv");
const hasTag = await knex.schema.hasColumn(TableName.Webhook, "tag");
const hasKeyEncoding = await knex.schema.hasColumn(TableName.Webhook, "keyEncoding");
const hasAlgorithm = await knex.schema.hasColumn(TableName.Webhook, "algorithm");
const hasUrl = await knex.schema.hasColumn(TableName.Webhook, "url");
await knex.schema.alterTable(TableName.Webhook, (t) => {
if (hasEncryptedWebhookSecretKey) t.dropColumn("encryptedSecretKeyWithKms");
if (hasEncryptedWebhookUrl) t.dropColumn("encryptedUrl");
if (!hasUrl) t.string("url");
if (!hasEncryptedSecretKey) t.string("encryptedSecretKey");
if (!hasIV) t.string("iv");
if (!hasTag) t.string("tag");
if (!hasAlgorithm) t.string("algorithm");
if (!hasKeyEncoding) t.string("keyEncoding");
if (!hasUrlCipherText) t.string("urlCipherText");
if (!hasUrlIV) t.string("urlIV");
if (!hasUrlTag) t.string("urlTag");
});
}
if (await knex.schema.hasTable(TableName.DynamicSecret)) {
const hasEncryptedConfig = await knex.schema.hasColumn(TableName.DynamicSecret, "encryptedConfig");
const hasInputIV = await knex.schema.hasColumn(TableName.DynamicSecret, "inputIV");
const hasInputCipherText = await knex.schema.hasColumn(TableName.DynamicSecret, "inputCiphertext");
const hasInputTag = await knex.schema.hasColumn(TableName.DynamicSecret, "inputTag");
const hasAlgorithm = await knex.schema.hasColumn(TableName.DynamicSecret, "algorithm");
const hasKeyEncoding = await knex.schema.hasColumn(TableName.DynamicSecret, "keyEncoding");
await knex.schema.alterTable(TableName.DynamicSecret, (t) => {
if (hasEncryptedConfig) t.dropColumn("encryptedConfig");
if (!hasInputIV) t.string("inputIV");
if (!hasInputCipherText) t.text("inputCiphertext");
if (!hasInputTag) t.string("inputTag");
if (!hasAlgorithm) t.string("algorithm");
if (!hasKeyEncoding) t.string("keyEncoding");
});
}
}

View File

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

View File

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

View File

@ -5,8 +5,6 @@
import { z } from "zod";
import { zodBuffer } from "@app/lib/zod";
import { TImmutableDBKeys } from "./models";
export const DynamicSecretsSchema = z.object({
@ -16,12 +14,16 @@ export const DynamicSecretsSchema = z.object({
type: z.string(),
defaultTTL: z.string(),
maxTTL: z.string().nullable().optional(),
inputIV: z.string(),
inputCiphertext: z.string(),
inputTag: z.string(),
algorithm: z.string().default("aes-256-gcm"),
keyEncoding: z.string().default("utf8"),
folderId: z.string().uuid(),
status: z.string().nullable().optional(),
statusDetails: z.string().nullable().optional(),
createdAt: z.date(),
updatedAt: z.date(),
encryptedConfig: zodBuffer
updatedAt: z.date()
});
export type TDynamicSecrets = z.infer<typeof DynamicSecretsSchema>;

View File

@ -15,7 +15,6 @@ export const RateLimitSchema = z.object({
authRateLimit: z.number().default(60),
inviteUserRateLimit: z.number().default(30),
mfaRateLimit: z.number().default(20),
creationLimit: z.number().default(30),
publicEndpointLimit: z.number().default(30),
createdAt: z.date(),
updatedAt: z.date()

View File

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

View File

@ -5,22 +5,27 @@
import { z } from "zod";
import { zodBuffer } from "@app/lib/zod";
import { TImmutableDBKeys } from "./models";
export const WebhooksSchema = z.object({
id: z.string().uuid(),
secretPath: z.string().default("/"),
url: z.string(),
lastStatus: z.string().nullable().optional(),
lastRunErrorMessage: z.string().nullable().optional(),
isDisabled: z.boolean().default(false),
encryptedSecretKey: z.string().nullable().optional(),
iv: z.string().nullable().optional(),
tag: z.string().nullable().optional(),
algorithm: z.string().nullable().optional(),
keyEncoding: z.string().nullable().optional(),
createdAt: z.date(),
updatedAt: z.date(),
envId: z.string().uuid(),
type: z.string().default("general").nullable().optional(),
encryptedSecretKeyWithKms: zodBuffer.nullable().optional(),
encryptedUrl: zodBuffer
urlCipherText: z.string().nullable().optional(),
urlIV: z.string().nullable().optional(),
urlTag: z.string().nullable().optional(),
type: z.string().default("general").nullable().optional()
});
export type TWebhooks = z.infer<typeof WebhooksSchema>;

View File

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

View File

@ -58,7 +58,6 @@ export const registerRateLimitRouter = async (server: FastifyZodProvider) => {
authRateLimit: z.number(),
inviteUserRateLimit: z.number(),
mfaRateLimit: z.number(),
creationLimit: z.number(),
publicEndpointLimit: z.number()
}),
response: {

View File

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

View File

@ -136,6 +136,7 @@ export enum EventType {
IMPORT_CA_CERT = "import-certificate-authority-cert",
GET_CA_CRL = "get-certificate-authority-crl",
ISSUE_CERT = "issue-cert",
SIGN_CERT = "sign-cert",
GET_CERT = "get-cert",
DELETE_CERT = "delete-cert",
REVOKE_CERT = "revoke-cert",
@ -146,7 +147,8 @@ export enum EventType {
GET_KMS = "get-kms",
UPDATE_PROJECT_KMS = "update-project-kms",
GET_PROJECT_KMS_BACKUP = "get-project-kms-backup",
LOAD_PROJECT_KMS_BACKUP = "load-project-kms-backup"
LOAD_PROJECT_KMS_BACKUP = "load-project-kms-backup",
ORG_ADMIN_ACCESS_PROJECT = "org-admin-accessed-project"
}
interface UserActorMetadata {
@ -336,6 +338,7 @@ interface DeleteIntegrationEvent {
targetServiceId?: string;
path?: string;
region?: string;
shouldDeleteIntegrationSecrets?: boolean;
};
}
@ -1143,6 +1146,15 @@ interface IssueCert {
};
}
interface SignCert {
type: EventType.SIGN_CERT;
metadata: {
caId: string;
dn: string;
serialNumber: string;
};
}
interface GetCert {
type: EventType.GET_CERT;
metadata: {
@ -1235,6 +1247,16 @@ interface LoadProjectKmsBackupEvent {
metadata: Record<string, string>; // no metadata yet
}
interface OrgAdminAccessProjectEvent {
type: EventType.ORG_ADMIN_ACCESS_PROJECT;
metadata: {
userId: string;
username: string;
email: string;
projectId: string;
}; // no metadata yet
}
export type Event =
| GetSecretsEvent
| GetSecretEvent
@ -1333,6 +1355,7 @@ export type Event =
| ImportCaCert
| GetCaCrl
| IssueCert
| SignCert
| GetCert
| DeleteCert
| RevokeCert
@ -1343,4 +1366,5 @@ export type Event =
| GetKmsEvent
| UpdateProjectKmsEvent
| GetProjectKmsBackupEvent
| LoadProjectKmsBackupEvent;
| LoadProjectKmsBackupEvent
| OrgAdminAccessProjectEvent;

View File

@ -12,10 +12,7 @@ export const dynamicSecretLeaseDALFactory = (db: TDbClient) => {
const countLeasesForDynamicSecret = async (dynamicSecretId: string, tx?: Knex) => {
try {
const doc = await (tx || db.replicaNode())(TableName.DynamicSecretLease)
.count("*")
.where({ dynamicSecretId })
.first();
const doc = await (tx || db)(TableName.DynamicSecretLease).count("*").where({ dynamicSecretId }).first();
return parseInt(doc || "0", 10);
} catch (error) {
throw new DatabaseError({ error, name: "DynamicSecretCountLeases" });
@ -24,7 +21,7 @@ export const dynamicSecretLeaseDALFactory = (db: TDbClient) => {
const findById = async (id: string, tx?: Knex) => {
try {
const doc = await (tx || db.replicaNode())(TableName.DynamicSecretLease)
const doc = await (tx || db)(TableName.DynamicSecretLease)
.where({ [`${TableName.DynamicSecretLease}.id` as "id"]: id })
.first()
.join(
@ -40,10 +37,14 @@ export const dynamicSecretLeaseDALFactory = (db: TDbClient) => {
db.ref("type").withSchema(TableName.DynamicSecret).as("dynType"),
db.ref("defaultTTL").withSchema(TableName.DynamicSecret).as("dynDefaultTTL"),
db.ref("maxTTL").withSchema(TableName.DynamicSecret).as("dynMaxTTL"),
db.ref("inputIV").withSchema(TableName.DynamicSecret).as("dynInputIV"),
db.ref("inputTag").withSchema(TableName.DynamicSecret).as("dynInputTag"),
db.ref("inputCiphertext").withSchema(TableName.DynamicSecret).as("dynInputCiphertext"),
db.ref("algorithm").withSchema(TableName.DynamicSecret).as("dynAlgorithm"),
db.ref("keyEncoding").withSchema(TableName.DynamicSecret).as("dynKeyEncoding"),
db.ref("folderId").withSchema(TableName.DynamicSecret).as("dynFolderId"),
db.ref("status").withSchema(TableName.DynamicSecret).as("dynStatus"),
db.ref("statusDetails").withSchema(TableName.DynamicSecret).as("dynStatusDetails"),
db.ref("encryptedConfig").withSchema(TableName.DynamicSecret).as("dynEncryptedConfig"),
db.ref("createdAt").withSchema(TableName.DynamicSecret).as("dynCreatedAt"),
db.ref("updatedAt").withSchema(TableName.DynamicSecret).as("dynUpdatedAt")
);
@ -58,12 +59,16 @@ export const dynamicSecretLeaseDALFactory = (db: TDbClient) => {
type: doc.dynType,
defaultTTL: doc.dynDefaultTTL,
maxTTL: doc.dynMaxTTL,
inputIV: doc.dynInputIV,
inputTag: doc.dynInputTag,
inputCiphertext: doc.dynInputCiphertext,
algorithm: doc.dynAlgorithm,
keyEncoding: doc.dynKeyEncoding,
folderId: doc.dynFolderId,
status: doc.dynStatus,
statusDetails: doc.dynStatusDetails,
createdAt: doc.dynCreatedAt,
updatedAt: doc.dynUpdatedAt,
encryptedConfig: doc.dynEncryptedConfig
updatedAt: doc.dynUpdatedAt
}
};
} catch (error) {

View File

@ -1,9 +1,8 @@
import { SecretKeyEncoding } from "@app/db/schemas";
import { DisableRotationErrors } from "@app/ee/services/secret-rotation/secret-rotation-queue";
import { infisicalSymmetricDecrypt } from "@app/lib/crypto/encryption";
import { logger } from "@app/lib/logger";
import { QueueJobs, QueueName, TQueueServiceFactory } from "@app/queue";
import { TKmsServiceFactory } from "@app/services/kms/kms-service";
import { KmsDataKey } from "@app/services/kms/kms-types";
import { TSecretFolderDALFactory } from "@app/services/secret-folder/secret-folder-dal";
import { TDynamicSecretDALFactory } from "../dynamic-secret/dynamic-secret-dal";
import { DynamicSecretStatus } from "../dynamic-secret/dynamic-secret-types";
@ -15,8 +14,6 @@ type TDynamicSecretLeaseQueueServiceFactoryDep = {
dynamicSecretLeaseDAL: Pick<TDynamicSecretLeaseDALFactory, "findById" | "deleteById" | "find" | "updateById">;
dynamicSecretDAL: Pick<TDynamicSecretDALFactory, "findById" | "deleteById" | "updateById">;
dynamicSecretProviders: Record<DynamicSecretProviders, TDynamicProviderFns>;
kmsService: Pick<TKmsServiceFactory, "createCipherPairWithDataKey">;
folderDAL: Pick<TSecretFolderDALFactory, "findById">;
};
export type TDynamicSecretLeaseQueueServiceFactory = ReturnType<typeof dynamicSecretLeaseQueueServiceFactory>;
@ -25,9 +22,7 @@ export const dynamicSecretLeaseQueueServiceFactory = ({
queueService,
dynamicSecretDAL,
dynamicSecretProviders,
dynamicSecretLeaseDAL,
kmsService,
folderDAL
dynamicSecretLeaseDAL
}: TDynamicSecretLeaseQueueServiceFactoryDep) => {
const pruneDynamicSecret = async (dynamicSecretCfgId: string) => {
await queueService.queue(
@ -82,20 +77,15 @@ export const dynamicSecretLeaseQueueServiceFactory = ({
if (!dynamicSecretLease) throw new DisableRotationErrors({ message: "Dynamic secret lease not found" });
const dynamicSecretCfg = dynamicSecretLease.dynamicSecret;
const folder = await folderDAL.findById(dynamicSecretCfg.folderId);
if (!folder) throw new DisableRotationErrors({ message: "Folder not found" });
const { projectId } = folder;
const { decryptor: secretManagerDecryptor } = await kmsService.createCipherPairWithDataKey({
type: KmsDataKey.SecretManager,
projectId
});
const dynamicSecretInputConfig = secretManagerDecryptor({
cipherTextBlob: dynamicSecretCfg.encryptedConfig
}).toString();
const selectedProvider = dynamicSecretProviders[dynamicSecretCfg.type as DynamicSecretProviders];
const decryptedStoredInput = JSON.parse(dynamicSecretInputConfig) as object;
const decryptedStoredInput = JSON.parse(
infisicalSymmetricDecrypt({
keyEncoding: dynamicSecretCfg.keyEncoding as SecretKeyEncoding,
ciphertext: dynamicSecretCfg.inputCiphertext,
tag: dynamicSecretCfg.inputTag,
iv: dynamicSecretCfg.inputIV
})
) as object;
await selectedProvider.revoke(decryptedStoredInput, dynamicSecretLease.externalEntityId);
await dynamicSecretLeaseDAL.deleteById(dynamicSecretLease.id);
@ -110,22 +100,17 @@ export const dynamicSecretLeaseQueueServiceFactory = ({
if ((dynamicSecretCfg.status as DynamicSecretStatus) !== DynamicSecretStatus.Deleting)
throw new DisableRotationErrors({ message: "Document not deleted" });
const folder = await folderDAL.findById(dynamicSecretCfg.folderId);
if (!folder) throw new DisableRotationErrors({ message: "Folder not found" });
const { projectId } = folder;
const { decryptor: secretManagerDecryptor } = await kmsService.createCipherPairWithDataKey({
type: KmsDataKey.SecretManager,
projectId
});
const dynamicSecretLeases = await dynamicSecretLeaseDAL.find({ dynamicSecretId: dynamicSecretCfgId });
if (dynamicSecretLeases.length) {
const selectedProvider = dynamicSecretProviders[dynamicSecretCfg.type as DynamicSecretProviders];
const dynamicSecretInputConfig = secretManagerDecryptor({
cipherTextBlob: dynamicSecretCfg.encryptedConfig
}).toString();
const decryptedStoredInput = JSON.parse(dynamicSecretInputConfig) as object;
const decryptedStoredInput = JSON.parse(
infisicalSymmetricDecrypt({
keyEncoding: dynamicSecretCfg.keyEncoding as SecretKeyEncoding,
ciphertext: dynamicSecretCfg.inputCiphertext,
tag: dynamicSecretCfg.inputTag,
iv: dynamicSecretCfg.inputIV
})
) as object;
await Promise.all(dynamicSecretLeases.map(({ id }) => unsetLeaseRevocation(id)));
await Promise.all(

View File

@ -1,14 +1,14 @@
import { ForbiddenError, subject } from "@casl/ability";
import ms from "ms";
import { SecretKeyEncoding } 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 { infisicalSymmetricDecrypt } from "@app/lib/crypto/encryption";
import { BadRequestError } from "@app/lib/errors";
import { logger } from "@app/lib/logger";
import { TKmsServiceFactory } from "@app/services/kms/kms-service";
import { KmsDataKey } from "@app/services/kms/kms-types";
import { TProjectDALFactory } from "@app/services/project/project-dal";
import { TSecretFolderDALFactory } from "@app/services/secret-folder/secret-folder-dal";
@ -34,7 +34,6 @@ type TDynamicSecretLeaseServiceFactoryDep = {
folderDAL: Pick<TSecretFolderDALFactory, "findBySecretPath">;
permissionService: Pick<TPermissionServiceFactory, "getProjectPermission">;
projectDAL: Pick<TProjectDALFactory, "findProjectBySlug">;
kmsService: Pick<TKmsServiceFactory, "createCipherPairWithDataKey">;
};
export type TDynamicSecretLeaseServiceFactory = ReturnType<typeof dynamicSecretLeaseServiceFactory>;
@ -47,8 +46,7 @@ export const dynamicSecretLeaseServiceFactory = ({
permissionService,
dynamicSecretQueueService,
projectDAL,
licenseService,
kmsService
licenseService
}: TDynamicSecretLeaseServiceFactoryDep) => {
const create = async ({
environmentSlug,
@ -96,12 +94,14 @@ export const dynamicSecretLeaseServiceFactory = ({
throw new BadRequestError({ message: `Max lease limit reached. Limit: ${appCfg.MAX_LEASE_LIMIT}` });
const selectedProvider = dynamicSecretProviders[dynamicSecretCfg.type as DynamicSecretProviders];
const { decryptor: kmsDecryptor } = await kmsService.createCipherPairWithDataKey({
type: KmsDataKey.SecretManager,
projectId
});
const decryptedStoredInputJson = kmsDecryptor({ cipherTextBlob: dynamicSecretCfg.encryptedConfig }).toString();
const decryptedStoredInput = JSON.parse(decryptedStoredInputJson) as object;
const decryptedStoredInput = JSON.parse(
infisicalSymmetricDecrypt({
keyEncoding: dynamicSecretCfg.keyEncoding as SecretKeyEncoding,
ciphertext: dynamicSecretCfg.inputCiphertext,
tag: dynamicSecretCfg.inputTag,
iv: dynamicSecretCfg.inputIV
})
) as object;
const selectedTTL = ttl ?? dynamicSecretCfg.defaultTTL;
const { maxTTL } = dynamicSecretCfg;
@ -164,12 +164,14 @@ export const dynamicSecretLeaseServiceFactory = ({
const dynamicSecretCfg = dynamicSecretLease.dynamicSecret;
const selectedProvider = dynamicSecretProviders[dynamicSecretCfg.type as DynamicSecretProviders];
const { decryptor: kmsDecryptor } = await kmsService.createCipherPairWithDataKey({
type: KmsDataKey.SecretManager,
projectId
});
const decryptedStoredInputJson = kmsDecryptor({ cipherTextBlob: dynamicSecretCfg.encryptedConfig }).toString();
const decryptedStoredInput = JSON.parse(decryptedStoredInputJson) as object;
const decryptedStoredInput = JSON.parse(
infisicalSymmetricDecrypt({
keyEncoding: dynamicSecretCfg.keyEncoding as SecretKeyEncoding,
ciphertext: dynamicSecretCfg.inputCiphertext,
tag: dynamicSecretCfg.inputTag,
iv: dynamicSecretCfg.inputIV
})
) as object;
const selectedTTL = ttl ?? dynamicSecretCfg.defaultTTL;
const { maxTTL } = dynamicSecretCfg;
@ -229,12 +231,14 @@ export const dynamicSecretLeaseServiceFactory = ({
const dynamicSecretCfg = dynamicSecretLease.dynamicSecret;
const selectedProvider = dynamicSecretProviders[dynamicSecretCfg.type as DynamicSecretProviders];
const { decryptor: kmsDecryptor } = await kmsService.createCipherPairWithDataKey({
type: KmsDataKey.SecretManager,
projectId
});
const decryptedStoredInputJson = kmsDecryptor({ cipherTextBlob: dynamicSecretCfg.encryptedConfig }).toString();
const decryptedStoredInput = JSON.parse(decryptedStoredInputJson) as object;
const decryptedStoredInput = JSON.parse(
infisicalSymmetricDecrypt({
keyEncoding: dynamicSecretCfg.keyEncoding as SecretKeyEncoding,
ciphertext: dynamicSecretCfg.inputCiphertext,
tag: dynamicSecretCfg.inputTag,
iv: dynamicSecretCfg.inputIV
})
) as object;
const revokeResponse = await selectedProvider
.revoke(decryptedStoredInput, dynamicSecretLease.externalEntityId)

View File

@ -1,11 +1,11 @@
import { ForbiddenError, subject } from "@casl/ability";
import { SecretKeyEncoding } 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 { infisicalSymmetricDecrypt, infisicalSymmetricEncypt } from "@app/lib/crypto/encryption";
import { BadRequestError } from "@app/lib/errors";
import { TKmsServiceFactory } from "@app/services/kms/kms-service";
import { KmsDataKey } from "@app/services/kms/kms-types";
import { TProjectDALFactory } from "@app/services/project/project-dal";
import { TSecretFolderDALFactory } from "@app/services/secret-folder/secret-folder-dal";
@ -34,7 +34,6 @@ type TDynamicSecretServiceFactoryDep = {
folderDAL: Pick<TSecretFolderDALFactory, "findBySecretPath">;
projectDAL: Pick<TProjectDALFactory, "findProjectBySlug">;
permissionService: Pick<TPermissionServiceFactory, "getProjectPermission">;
kmsService: Pick<TKmsServiceFactory, "createCipherPairWithDataKey">;
};
export type TDynamicSecretServiceFactory = ReturnType<typeof dynamicSecretServiceFactory>;
@ -47,8 +46,7 @@ export const dynamicSecretServiceFactory = ({
dynamicSecretProviders,
permissionService,
dynamicSecretQueueService,
projectDAL,
kmsService
projectDAL
}: TDynamicSecretServiceFactoryDep) => {
const create = async ({
path,
@ -98,16 +96,16 @@ export const dynamicSecretServiceFactory = ({
const isConnected = await selectedProvider.validateConnection(provider.inputs);
if (!isConnected) throw new BadRequestError({ message: "Provider connection failed" });
const { encryptor: secretManagerEncryptor } = await kmsService.createCipherPairWithDataKey({
type: KmsDataKey.SecretManager,
projectId
});
const encryptedConfig = secretManagerEncryptor({ plainText: Buffer.from(JSON.stringify(inputs)) }).cipherTextBlob;
const encryptedInput = infisicalSymmetricEncypt(JSON.stringify(inputs));
const dynamicSecretCfg = await dynamicSecretDAL.create({
type: provider.type,
version: 1,
encryptedConfig,
inputIV: encryptedInput.iv,
inputTag: encryptedInput.tag,
inputCiphertext: encryptedInput.ciphertext,
algorithm: encryptedInput.algorithm,
keyEncoding: encryptedInput.encoding,
maxTTL,
defaultTTL,
folderId: folder.id,
@ -167,28 +165,27 @@ export const dynamicSecretServiceFactory = ({
}
const selectedProvider = dynamicSecretProviders[dynamicSecretCfg.type as DynamicSecretProviders];
const { encryptor: secretManagerEncryptor, decryptor: secretManagerDecryptor } =
await kmsService.createCipherPairWithDataKey({
type: KmsDataKey.SecretManager,
projectId
});
const dynamicSecretInputConfig = secretManagerDecryptor({
cipherTextBlob: dynamicSecretCfg.encryptedConfig
}).toString();
const decryptedStoredInput = JSON.parse(dynamicSecretInputConfig) as object;
const decryptedStoredInput = JSON.parse(
infisicalSymmetricDecrypt({
keyEncoding: dynamicSecretCfg.keyEncoding as SecretKeyEncoding,
ciphertext: dynamicSecretCfg.inputCiphertext,
tag: dynamicSecretCfg.inputTag,
iv: dynamicSecretCfg.inputIV
})
) as object;
const newInput = { ...decryptedStoredInput, ...(inputs || {}) };
const updatedInput = await selectedProvider.validateProviderInputs(newInput);
const isConnected = await selectedProvider.validateConnection(newInput);
if (!isConnected) throw new BadRequestError({ message: "Provider connection failed" });
const encryptedConfig = secretManagerEncryptor({
plainText: Buffer.from(JSON.stringify(updatedInput))
}).cipherTextBlob;
const encryptedInput = infisicalSymmetricEncypt(JSON.stringify(updatedInput));
const updatedDynamicCfg = await dynamicSecretDAL.updateById(dynamicSecretCfg.id, {
encryptedConfig,
inputIV: encryptedInput.iv,
inputTag: encryptedInput.tag,
inputCiphertext: encryptedInput.ciphertext,
algorithm: encryptedInput.algorithm,
keyEncoding: encryptedInput.encoding,
maxTTL,
defaultTTL,
name: newName ?? name,
@ -289,16 +286,14 @@ export const dynamicSecretServiceFactory = ({
const dynamicSecretCfg = await dynamicSecretDAL.findOne({ name, folderId: folder.id });
if (!dynamicSecretCfg) throw new BadRequestError({ message: "Dynamic secret not found" });
const { decryptor: secretManagerDecryptor } = await kmsService.createCipherPairWithDataKey({
type: KmsDataKey.SecretManager,
projectId
});
const dynamicSecretInputConfig = secretManagerDecryptor({
cipherTextBlob: dynamicSecretCfg.encryptedConfig
}).toString();
const decryptedStoredInput = JSON.parse(dynamicSecretInputConfig) as object;
const decryptedStoredInput = JSON.parse(
infisicalSymmetricDecrypt({
keyEncoding: dynamicSecretCfg.keyEncoding as SecretKeyEncoding,
ciphertext: dynamicSecretCfg.inputCiphertext,
tag: dynamicSecretCfg.inputTag,
iv: dynamicSecretCfg.inputIV
})
) as object;
const selectedProvider = dynamicSecretProviders[dynamicSecretCfg.type as DynamicSecretProviders];
const providerInputs = (await selectedProvider.validateProviderInputs(decryptedStoredInput)) as object;
return { ...dynamicSecretCfg, inputs: providerInputs };

View File

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

View File

@ -58,6 +58,11 @@ export type TFeatureSet = {
caCrl: false;
instanceUserManagement: false;
externalKms: false;
rateLimits: {
readLimit: number;
writeLimit: number;
secretsLimit: number;
};
};
export type TOrgPlansTableDTO = {

View File

@ -9,6 +9,10 @@ export enum OrgPermissionActions {
Delete = "delete"
}
export enum OrgPermissionAdminConsoleAction {
AccessAllProjects = "access-all-projects"
}
export enum OrgPermissionSubjects {
Workspace = "workspace",
Role = "role",
@ -22,7 +26,8 @@ export enum OrgPermissionSubjects {
Billing = "billing",
SecretScanning = "secret-scanning",
Identity = "identity",
Kms = "kms"
Kms = "kms",
AdminConsole = "organization-admin-console"
}
export type OrgPermissionSet =
@ -39,7 +44,8 @@ export type OrgPermissionSet =
| [OrgPermissionActions, OrgPermissionSubjects.SecretScanning]
| [OrgPermissionActions, OrgPermissionSubjects.Billing]
| [OrgPermissionActions, OrgPermissionSubjects.Identity]
| [OrgPermissionActions, OrgPermissionSubjects.Kms];
| [OrgPermissionActions, OrgPermissionSubjects.Kms]
| [OrgPermissionAdminConsoleAction, OrgPermissionSubjects.AdminConsole];
const buildAdminPermission = () => {
const { can, build } = new AbilityBuilder<MongoAbility<OrgPermissionSet>>(createMongoAbility);
@ -107,6 +113,8 @@ const buildAdminPermission = () => {
can(OrgPermissionActions.Edit, OrgPermissionSubjects.Kms);
can(OrgPermissionActions.Delete, OrgPermissionSubjects.Kms);
can(OrgPermissionAdminConsoleAction.AccessAllProjects, OrgPermissionSubjects.AdminConsole);
return build({ conditionsMatcher });
};

View File

@ -23,6 +23,7 @@ export enum ProjectPermissionSub {
IpAllowList = "ip-allowlist",
Project = "workspace",
Secrets = "secrets",
SecretFolders = "secret-folders",
SecretRollback = "secret-rollback",
SecretApproval = "secret-approval",
SecretRotation = "secret-rotation",
@ -42,6 +43,10 @@ export type ProjectPermissionSet =
ProjectPermissionActions,
ProjectPermissionSub.Secrets | (ForcedSubject<ProjectPermissionSub.Secrets> & SubjectFields)
]
| [
ProjectPermissionActions,
ProjectPermissionSub.SecretFolders | (ForcedSubject<ProjectPermissionSub.SecretFolders> & SubjectFields)
]
| [ProjectPermissionActions, ProjectPermissionSub.Role]
| [ProjectPermissionActions, ProjectPermissionSub.Tags]
| [ProjectPermissionActions, ProjectPermissionSub.Member]

View File

@ -4,17 +4,16 @@ import { logger } from "@app/lib/logger";
import { TLicenseServiceFactory } from "../license/license-service";
import { TRateLimitDALFactory } from "./rate-limit-dal";
import { TRateLimit, TRateLimitUpdateDTO } from "./rate-limit-types";
import { RateLimitConfiguration, TRateLimit, TRateLimitUpdateDTO } from "./rate-limit-types";
let rateLimitMaxConfiguration = {
let rateLimitMaxConfiguration: RateLimitConfiguration = {
readLimit: 60,
publicEndpointLimit: 30,
writeLimit: 200,
secretsLimit: 60,
authRateLimit: 60,
inviteUserRateLimit: 30,
mfaRateLimit: 20,
creationLimit: 30
mfaRateLimit: 20
};
Object.freeze(rateLimitMaxConfiguration);
@ -67,8 +66,7 @@ export const rateLimitServiceFactory = ({ rateLimitDAL, licenseService }: TRateL
secretsLimit: rateLimit.secretsRateLimit,
authRateLimit: rateLimit.authRateLimit,
inviteUserRateLimit: rateLimit.inviteUserRateLimit,
mfaRateLimit: rateLimit.mfaRateLimit,
creationLimit: rateLimit.creationLimit
mfaRateLimit: rateLimit.mfaRateLimit
};
logger.info(`syncRateLimitConfiguration: rate limit configuration: %o`, newRateLimitMaxConfiguration);

View File

@ -5,7 +5,6 @@ export type TRateLimitUpdateDTO = {
authRateLimit: number;
inviteUserRateLimit: number;
mfaRateLimit: number;
creationLimit: number;
publicEndpointLimit: number;
};
@ -14,3 +13,13 @@ export type TRateLimit = {
createdAt: Date;
updatedAt: Date;
} & TRateLimitUpdateDTO;
export type RateLimitConfiguration = {
readLimit: number;
publicEndpointLimit: number;
writeLimit: number;
secretsLimit: number;
authRateLimit: number;
inviteUserRateLimit: number;
mfaRateLimit: number;
};

View File

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

View File

@ -224,12 +224,10 @@ export const secretApprovalRequestServiceFactory = ({
secretKey: el.key,
id: el.id,
version: el.version,
secretValue: el.encryptedValue
? secretManagerDecryptor({ cipherTextBlob: el.encryptedValue }).toString()
: undefined,
secretValue: el.encryptedValue ? secretManagerDecryptor({ cipherTextBlob: el.encryptedValue }).toString() : "",
secretComment: el.encryptedComment
? secretManagerDecryptor({ cipherTextBlob: el.encryptedComment }).toString()
: undefined,
: "",
secret: el.secret
? {
secretKey: el.secret.key,
@ -237,10 +235,10 @@ export const secretApprovalRequestServiceFactory = ({
version: el.secret.version,
secretValue: el.secret.encryptedValue
? secretManagerDecryptor({ cipherTextBlob: el.secret.encryptedValue }).toString()
: undefined,
: "",
secretComment: el.secret.encryptedComment
? secretManagerDecryptor({ cipherTextBlob: el.secret.encryptedComment }).toString()
: undefined
: ""
}
: undefined,
secretVersion: el.secretVersion
@ -250,10 +248,10 @@ export const secretApprovalRequestServiceFactory = ({
version: el.secretVersion.version,
secretValue: el.secretVersion.encryptedValue
? secretManagerDecryptor({ cipherTextBlob: el.secretVersion.encryptedValue }).toString()
: undefined,
: "",
secretComment: el.secretVersion.encryptedComment
? secretManagerDecryptor({ cipherTextBlob: el.secretVersion.encryptedComment }).toString()
: undefined
: ""
}
: undefined
}));

View File

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

View File

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

View File

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

View File

@ -596,7 +596,8 @@ export const RAW_SECRETS = {
"The slug of the project to list secrets from. This parameter is only applicable by machine identities.",
environment: "The slug of the environment to list secrets from.",
secretPath: "The secret path to list secrets from.",
includeImports: "Weather to include imported secrets or not."
includeImports: "Weather to include imported secrets or not.",
tagSlugs: "The comma separated tag slugs to filter secrets"
},
CREATE: {
secretName: "The name of the secret to create.",
@ -1056,7 +1057,7 @@ export const CERTIFICATE_AUTHORITIES = {
},
SIGN_INTERMEDIATE: {
caId: "The ID of the CA to sign the intermediate certificate with",
csr: "The CSR to sign with the CA",
csr: "The pem-encoded CSR to sign with the CA",
notBefore: "The date and time when the intermediate CA becomes valid in YYYY-MM-DDTHH:mm:ss.sssZ format",
notAfter: "The date and time when the intermediate CA expires in YYYY-MM-DDTHH:mm:ss.sssZ format",
maxPathLength:
@ -1086,6 +1087,21 @@ export const CERTIFICATE_AUTHORITIES = {
privateKey: "The private key of the issued certificate",
serialNumber: "The serial number of the issued certificate"
},
SIGN_CERT: {
caId: "The ID of the CA to issue the certificate from",
csr: "The pem-encoded CSR to sign with the CA to be used for certificate issuance",
friendlyName: "A friendly name for the certificate",
commonName: "The common name (CN) for the certificate",
altNames:
"A comma-delimited list of Subject Alternative Names (SANs) for the certificate; these can be host names or email addresses.",
ttl: "The time to live for the certificate such as 1m, 1h, 1d, 1y, ...",
notBefore: "The date and time when the certificate becomes valid in YYYY-MM-DDTHH:mm:ss.sssZ format",
notAfter: "The date and time when the certificate expires in YYYY-MM-DDTHH:mm:ss.sssZ format",
certificate: "The issued certificate",
issuingCaCertificate: "The certificate of the issuing CA",
certificateChain: "The certificate chain of the issued certificate",
serialNumber: "The serial number of the issued certificate"
},
GET_CRL: {
caId: "The ID of the CA to get the certificate revocation list (CRL) for",
crl: "The certificate revocation list (CRL) of the CA"

View File

@ -19,23 +19,43 @@ export const withTransaction = <K extends object>(db: Knex, dal: K) => ({
export type TFindFilter<R extends object = object> = Partial<R> & {
$in?: Partial<{ [k in keyof R]: R[k][] }>;
$search?: Partial<{ [k in keyof R]: R[k] }>;
};
export const buildFindFilter =
<R extends object = object>({ $in, ...filter }: TFindFilter<R>) =>
<R extends object = object>({ $in, $search, ...filter }: TFindFilter<R>) =>
(bd: Knex.QueryBuilder<R, R>) => {
void bd.where(filter);
if ($in) {
Object.entries($in).forEach(([key, val]) => {
void bd.whereIn(key as never, val as never);
if (val) {
void bd.whereIn(key as never, val as never);
}
});
}
if ($search) {
Object.entries($search).forEach(([key, val]) => {
if (val) {
void bd.whereILike(key as never, val as never);
}
});
}
return bd;
};
export type TFindOpt<R extends object = object> = {
export type TFindReturn<TQuery extends Knex.QueryBuilder, TCount extends boolean = false> = Array<
Awaited<TQuery>[0] &
(TCount extends true
? {
count: string;
}
: unknown)
>;
export type TFindOpt<R extends object = object, TCount extends boolean = boolean> = {
limit?: number;
offset?: number;
sort?: Array<[keyof R, "asc" | "desc"] | [keyof R, "asc" | "desc", "first" | "last"]>;
count?: TCount;
tx?: Knex;
};
@ -66,18 +86,22 @@ export const ormify = <DbOps extends object, Tname extends keyof Tables>(db: Kne
throw new DatabaseError({ error, name: "Find one" });
}
},
find: async (
find: async <TCount extends boolean = false>(
filter: TFindFilter<Tables[Tname]["base"]>,
{ offset, limit, sort, tx }: TFindOpt<Tables[Tname]["base"]> = {}
{ offset, limit, sort, count, tx }: TFindOpt<Tables[Tname]["base"], TCount> = {}
) => {
try {
const query = (tx || db.replicaNode())(tableName).where(buildFindFilter(filter));
if (count) {
void query.select(db.raw("COUNT(*) OVER() AS count"));
void query.select("*");
}
if (limit) void query.limit(limit);
if (offset) void query.offset(offset);
if (sort) {
void query.orderBy(sort.map(([column, order, nulls]) => ({ column: column as string, order, nulls })));
}
const res = await query;
const res = (await query) as TFindReturn<typeof query, TCount>;
return res;
} catch (error) {
throw new DatabaseError({ error, name: "Find one" });
@ -104,6 +128,16 @@ export const ormify = <DbOps extends object, Tname extends keyof Tables>(db: Kne
throw new DatabaseError({ error, name: "Create" });
}
},
// This spilit the insert into multiple chunk
batchInsert: async (data: readonly Tables[Tname]["insert"][], tx?: Knex) => {
try {
if (!data.length) return [];
const res = await (tx || db).batchInsert(tableName, data as never).returning("*");
return res as Tables[Tname]["base"][];
} catch (error) {
throw new DatabaseError({ error, name: "batchInsert" });
}
},
upsert: async (data: readonly Tables[Tname]["insert"][], onConflictField: keyof Tables[Tname]["base"], tx?: Knex) => {
try {
if (!data.length) return [];

View File

@ -1,7 +1,6 @@
import type { RateLimitOptions, RateLimitPluginOptions } from "@fastify/rate-limit";
import { Redis } from "ioredis";
import { getRateLimiterConfig } from "@app/ee/services/rate-limit/rate-limit-service";
import { getConfig } from "@app/lib/config/env";
export const globalRateLimiterCfg = (): RateLimitPluginOptions => {
@ -22,14 +21,16 @@ export const globalRateLimiterCfg = (): RateLimitPluginOptions => {
// GET endpoints
export const readLimit: RateLimitOptions = {
timeWindow: 60 * 1000,
max: () => getRateLimiterConfig().readLimit,
hook: "preValidation",
max: (req) => req.rateLimits.readLimit,
keyGenerator: (req) => req.realIp
};
// POST, PATCH, PUT, DELETE endpoints
export const writeLimit: RateLimitOptions = {
timeWindow: 60 * 1000,
max: () => getRateLimiterConfig().writeLimit,
hook: "preValidation",
max: (req) => req.rateLimits.writeLimit,
keyGenerator: (req) => req.realIp
};
@ -37,42 +38,40 @@ export const writeLimit: RateLimitOptions = {
export const secretsLimit: RateLimitOptions = {
// secrets, folders, secret imports
timeWindow: 60 * 1000,
max: () => getRateLimiterConfig().secretsLimit,
hook: "preValidation",
max: (req) => req.rateLimits.secretsLimit,
keyGenerator: (req) => req.realIp
};
export const authRateLimit: RateLimitOptions = {
timeWindow: 60 * 1000,
max: () => getRateLimiterConfig().authRateLimit,
hook: "preValidation",
max: (req) => req.rateLimits.authRateLimit,
keyGenerator: (req) => req.realIp
};
export const inviteUserRateLimit: RateLimitOptions = {
timeWindow: 60 * 1000,
max: () => getRateLimiterConfig().inviteUserRateLimit,
hook: "preValidation",
max: (req) => req.rateLimits.inviteUserRateLimit,
keyGenerator: (req) => req.realIp
};
export const mfaRateLimit: RateLimitOptions = {
timeWindow: 60 * 1000,
max: () => getRateLimiterConfig().mfaRateLimit,
hook: "preValidation",
max: (req) => req.rateLimits.mfaRateLimit,
keyGenerator: (req) => {
return req.headers.authorization?.split(" ")[1] || req.realIp;
}
};
export const creationLimit: RateLimitOptions = {
// identity, project, org
timeWindow: 60 * 1000,
max: () => getRateLimiterConfig().creationLimit,
keyGenerator: (req) => req.realIp
};
// Public endpoints to avoid brute force attacks
export const publicEndpointLimit: RateLimitOptions = {
// Read Shared Secrets
timeWindow: 60 * 1000,
max: () => getRateLimiterConfig().publicEndpointLimit,
hook: "preValidation",
max: (req) => req.rateLimits.publicEndpointLimit,
keyGenerator: (req) => req.realIp
};

View File

@ -0,0 +1,38 @@
import fp from "fastify-plugin";
import { getRateLimiterConfig } from "@app/ee/services/rate-limit/rate-limit-service";
import { getConfig } from "@app/lib/config/env";
export const injectRateLimits = fp(async (server) => {
server.decorateRequest("rateLimits", null);
server.addHook("onRequest", async (req) => {
const appCfg = getConfig();
const instanceRateLimiterConfig = getRateLimiterConfig();
if (!req.auth?.orgId) {
// for public endpoints, we always use the instance-wide default rate limits
req.rateLimits = instanceRateLimiterConfig;
return;
}
const { rateLimits, customRateLimits } = await server.services.license.getPlan(req.auth.orgId);
if (customRateLimits && !appCfg.isCloud) {
// we do this because for self-hosted/dedicated instances, we want custom rate limits to be based on admin configuration
// note that the syncing of custom rate limit happens on the instanceRateLimiterConfig object
req.rateLimits = instanceRateLimiterConfig;
return;
}
// we're using the null coalescing operator in order to handle outdated licenses
req.rateLimits = {
readLimit: rateLimits?.readLimit ?? instanceRateLimiterConfig.readLimit,
writeLimit: rateLimits?.writeLimit ?? instanceRateLimiterConfig.writeLimit,
secretsLimit: rateLimits?.secretsLimit ?? instanceRateLimiterConfig.secretsLimit,
publicEndpointLimit: instanceRateLimiterConfig.publicEndpointLimit,
authRateLimit: instanceRateLimiterConfig.authRateLimit,
inviteUserRateLimit: instanceRateLimiterConfig.inviteUserRateLimit,
mfaRateLimit: instanceRateLimiterConfig.mfaRateLimit
};
});
});

View File

@ -129,6 +129,7 @@ import { orgDALFactory } from "@app/services/org/org-dal";
import { orgRoleDALFactory } from "@app/services/org/org-role-dal";
import { orgRoleServiceFactory } from "@app/services/org/org-role-service";
import { orgServiceFactory } from "@app/services/org/org-service";
import { orgAdminServiceFactory } from "@app/services/org-admin/org-admin-service";
import { orgMembershipDALFactory } from "@app/services/org-membership/org-membership-dal";
import { projectDALFactory } from "@app/services/project/project-dal";
import { projectQueueFactory } from "@app/services/project/project-queue";
@ -183,6 +184,7 @@ import { webhookServiceFactory } from "@app/services/webhook/webhook-service";
import { injectAuditLogInfo } from "../plugins/audit-log";
import { injectIdentity } from "../plugins/auth/inject-identity";
import { injectPermission } from "../plugins/auth/inject-permission";
import { injectRateLimits } from "../plugins/inject-rate-limits";
import { registerSecretScannerGhApp } from "../plugins/secret-scanner";
import { registerV1Routes } from "./v1";
import { registerV2Routes } from "./v2";
@ -498,6 +500,16 @@ export const registerRoutes = async (
keyStore,
licenseService
});
const orgAdminService = orgAdminServiceFactory({
projectDAL,
permissionService,
projectUserMembershipRoleDAL,
userDAL,
projectBotDAL,
projectKeyDAL,
projectMembershipDAL
});
const rateLimitService = rateLimitServiceFactory({
rateLimitDAL,
licenseService
@ -635,7 +647,8 @@ export const registerRoutes = async (
projectUserMembershipRoleDAL,
identityProjectMembershipRoleDAL,
keyStore,
kmsService
kmsService,
projectBotDAL
});
const projectEnvService = projectEnvServiceFactory({
@ -677,8 +690,7 @@ export const registerRoutes = async (
permissionService,
webhookDAL,
projectEnvDAL,
projectDAL,
kmsService
projectDAL
});
const secretTagService = secretTagServiceFactory({ secretTagDAL, permissionService });
@ -885,8 +897,15 @@ export const registerRoutes = async (
folderDAL,
integrationDAL,
integrationAuthDAL,
secretQueueService
secretQueueService,
integrationAuthService,
projectBotService,
secretV2BridgeDAL,
secretImportDAL,
secretDAL,
kmsService
});
const serviceTokenService = serviceTokenServiceFactory({
projectEnvDAL,
serviceTokenDAL,
@ -988,9 +1007,7 @@ export const registerRoutes = async (
queueService,
dynamicSecretLeaseDAL,
dynamicSecretProviders,
dynamicSecretDAL,
kmsService,
folderDAL
dynamicSecretDAL
});
const dynamicSecretService = dynamicSecretServiceFactory({
projectDAL,
@ -1000,8 +1017,7 @@ export const registerRoutes = async (
dynamicSecretProviders,
folderDAL,
permissionService,
licenseService,
kmsService
licenseService
});
const dynamicSecretLeaseService = dynamicSecretLeaseServiceFactory({
projectDAL,
@ -1011,8 +1027,7 @@ export const registerRoutes = async (
dynamicSecretLeaseDAL,
dynamicSecretProviders,
folderDAL,
licenseService,
kmsService
licenseService
});
const dailyResourceCleanUp = dailyResourceCleanUpQueueServiceFactory({
auditLogDAL,
@ -1022,7 +1037,8 @@ export const registerRoutes = async (
snapshotDAL,
identityAccessTokenDAL,
secretSharingDAL,
secretVersionV2DAL: secretVersionV2BridgeDAL
secretVersionV2DAL: secretVersionV2BridgeDAL,
identityUniversalAuthClientSecretDAL: identityUaClientSecretDAL
});
const oidcService = oidcConfigServiceFactory({
@ -1117,7 +1133,8 @@ export const registerRoutes = async (
identityProjectAdditionalPrivilege: identityProjectAdditionalPrivilegeService,
secretSharing: secretSharingService,
userEngagement: userEngagementService,
externalKms: externalKmsService
externalKms: externalKmsService,
orgAdmin: orgAdminService
});
const cronJobs: CronJob[] = [];
@ -1134,6 +1151,7 @@ export const registerRoutes = async (
await server.register(injectIdentity, { userDAL, serviceTokenDAL });
await server.register(injectPermission);
await server.register(injectRateLimits);
await server.register(injectAuditLogInfo);
server.route({

View File

@ -63,8 +63,8 @@ export const secretRawSchema = z.object({
version: z.number(),
type: z.string(),
secretKey: z.string(),
secretValue: z.string().optional(),
secretComment: z.string().optional(),
secretValue: z.string(),
secretComment: z.string(),
secretReminderNote: z.string().nullable().optional(),
secretReminderRepeatDays: z.number().nullable().optional(),
skipMultilineEncoding: z.boolean().default(false).nullable().optional(),
@ -129,7 +129,11 @@ export const SanitizedRoleSchema = ProjectRolesSchema.extend({
});
export const SanitizedDynamicSecretSchema = DynamicSecretsSchema.omit({
encryptedConfig: true
inputIV: true,
inputTag: true,
inputCiphertext: true,
keyEncoding: true,
algorithm: true
});
export const SanitizedAuditLogStreamSchema = z.object({

View File

@ -337,7 +337,7 @@ export const registerCaRouter = async (server: FastifyZodProvider) => {
caId: z.string().trim().describe(CERTIFICATE_AUTHORITIES.SIGN_INTERMEDIATE.caId)
}),
body: z.object({
csr: z.string().trim().describe(CERTIFICATE_AUTHORITIES.SIGN_INTERMEDIATE.csr),
csr: z.string().trim().min(1).describe(CERTIFICATE_AUTHORITIES.SIGN_INTERMEDIATE.csr),
notBefore: validateCaDateField.optional().describe(CERTIFICATE_AUTHORITIES.SIGN_INTERMEDIATE.notBefore),
notAfter: validateCaDateField.describe(CERTIFICATE_AUTHORITIES.SIGN_INTERMEDIATE.notAfter),
maxPathLength: z.number().min(-1).default(-1).describe(CERTIFICATE_AUTHORITIES.SIGN_INTERMEDIATE.maxPathLength)
@ -453,7 +453,7 @@ export const registerCaRouter = async (server: FastifyZodProvider) => {
}),
body: z
.object({
friendlyName: z.string().optional().describe(CERTIFICATE_AUTHORITIES.ISSUE_CERT.friendlyName),
friendlyName: z.string().trim().optional().describe(CERTIFICATE_AUTHORITIES.ISSUE_CERT.friendlyName),
commonName: z.string().trim().min(1).describe(CERTIFICATE_AUTHORITIES.ISSUE_CERT.commonName),
altNames: validateAltNamesField.describe(CERTIFICATE_AUTHORITIES.ISSUE_CERT.altNames),
ttl: z
@ -516,4 +516,81 @@ export const registerCaRouter = async (server: FastifyZodProvider) => {
};
}
});
server.route({
method: "POST",
url: "/:caId/sign-certificate",
config: {
rateLimit: writeLimit
},
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
schema: {
description: "Sign certificate from CA",
params: z.object({
caId: z.string().trim().describe(CERTIFICATE_AUTHORITIES.SIGN_CERT.caId)
}),
body: z
.object({
csr: z.string().trim().min(1).describe(CERTIFICATE_AUTHORITIES.SIGN_CERT.csr),
friendlyName: z.string().trim().optional().describe(CERTIFICATE_AUTHORITIES.SIGN_CERT.friendlyName),
commonName: z.string().trim().min(1).optional().describe(CERTIFICATE_AUTHORITIES.SIGN_CERT.commonName),
altNames: validateAltNamesField.describe(CERTIFICATE_AUTHORITIES.SIGN_CERT.altNames),
ttl: z
.string()
.refine((val) => ms(val) > 0, "TTL must be a positive number")
.describe(CERTIFICATE_AUTHORITIES.SIGN_CERT.ttl),
notBefore: validateCaDateField.optional().describe(CERTIFICATE_AUTHORITIES.SIGN_CERT.notBefore),
notAfter: validateCaDateField.optional().describe(CERTIFICATE_AUTHORITIES.SIGN_CERT.notAfter)
})
.refine(
(data) => {
const { ttl, notAfter } = data;
return (ttl !== undefined && notAfter === undefined) || (ttl === undefined && notAfter !== undefined);
},
{
message: "Either ttl or notAfter must be present, but not both",
path: ["ttl", "notAfter"]
}
),
response: {
200: z.object({
certificate: z.string().trim().describe(CERTIFICATE_AUTHORITIES.SIGN_CERT.certificate),
issuingCaCertificate: z.string().trim().describe(CERTIFICATE_AUTHORITIES.ISSUE_CERT.issuingCaCertificate),
certificateChain: z.string().trim().describe(CERTIFICATE_AUTHORITIES.ISSUE_CERT.certificateChain),
serialNumber: z.string().trim().describe(CERTIFICATE_AUTHORITIES.ISSUE_CERT.serialNumber)
})
}
},
handler: async (req) => {
const { certificate, certificateChain, issuingCaCertificate, serialNumber, ca } =
await server.services.certificateAuthority.signCertFromCa({
caId: req.params.caId,
actor: req.permission.type,
actorId: req.permission.id,
actorAuthMethod: req.permission.authMethod,
actorOrgId: req.permission.orgId,
...req.body
});
await server.services.auditLog.createAuditLog({
...req.auditLogInfo,
projectId: ca.projectId,
event: {
type: EventType.SIGN_CERT,
metadata: {
caId: ca.id,
dn: ca.dn,
serialNumber
}
}
});
return {
certificate,
certificateChain,
issuingCaCertificate,
serialNumber
};
}
});
};

View File

@ -3,7 +3,7 @@ import { z } from "zod";
import { IdentitiesSchema, IdentityOrgMembershipsSchema, OrgMembershipRole, OrgRolesSchema } from "@app/db/schemas";
import { EventType } from "@app/ee/services/audit-log/audit-log-types";
import { IDENTITIES } from "@app/lib/api-docs";
import { creationLimit, readLimit, writeLimit } from "@app/server/config/rateLimiter";
import { readLimit, writeLimit } from "@app/server/config/rateLimiter";
import { getTelemetryDistinctId } from "@app/server/lib/telemetry";
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
import { AuthMode } from "@app/services/auth/auth-type";
@ -16,7 +16,7 @@ export const registerIdentityRouter = async (server: FastifyZodProvider) => {
method: "POST",
url: "/",
config: {
rateLimit: creationLimit
rateLimit: writeLimit
},
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
schema: {

View File

@ -15,6 +15,7 @@ import { registerIdentityUaRouter } from "./identity-universal-auth-router";
import { registerIntegrationAuthRouter } from "./integration-auth-router";
import { registerIntegrationRouter } from "./integration-router";
import { registerInviteOrgRouter } from "./invite-org-router";
import { registerOrgAdminRouter } from "./org-admin-router";
import { registerOrgRouter } from "./organization-router";
import { registerPasswordRouter } from "./password-router";
import { registerProjectEnvRouter } from "./project-env-router";
@ -50,6 +51,7 @@ export const registerV1Routes = async (server: FastifyZodProvider) => {
await server.register(registerPasswordRouter, { prefix: "/password" });
await server.register(registerOrgRouter, { prefix: "/organization" });
await server.register(registerAdminRouter, { prefix: "/admin" });
await server.register(registerOrgAdminRouter, { prefix: "/organization-admin" });
await server.register(registerUserRouter, { prefix: "/user" });
await server.register(registerInviteOrgRouter, { prefix: "/invite-org" });
await server.register(registerUserActionRouter, { prefix: "/user-action" });

View File

@ -170,6 +170,12 @@ export const registerIntegrationRouter = async (server: FastifyZodProvider) => {
params: z.object({
integrationId: z.string().trim().describe(INTEGRATION.DELETE.integrationId)
}),
querystring: z.object({
shouldDeleteIntegrationSecrets: z
.enum(["true", "false"])
.optional()
.transform((val) => val === "true")
}),
response: {
200: z.object({
integration: IntegrationsSchema
@ -183,7 +189,8 @@ export const registerIntegrationRouter = async (server: FastifyZodProvider) => {
actorAuthMethod: req.permission.authMethod,
actor: req.permission.type,
actorOrgId: req.permission.orgId,
id: req.params.integrationId
id: req.params.integrationId,
shouldDeleteIntegrationSecrets: req.query.shouldDeleteIntegrationSecrets
});
await server.services.auditLog.createAuditLog({
@ -205,7 +212,8 @@ export const registerIntegrationRouter = async (server: FastifyZodProvider) => {
targetService: integration.targetService,
targetServiceId: integration.targetServiceId,
path: integration.path,
region: integration.region
region: integration.region,
shouldDeleteIntegrationSecrets: req.query.shouldDeleteIntegrationSecrets
// eslint-disable-next-line
}) as any
}

View File

@ -0,0 +1,90 @@
import { z } from "zod";
import { ProjectMembershipsSchema } from "@app/db/schemas";
import { EventType } from "@app/ee/services/audit-log/audit-log-types";
import { readLimit } from "@app/server/config/rateLimiter";
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
import { AuthMode } from "@app/services/auth/auth-type";
import { SanitizedProjectSchema } from "../sanitizedSchemas";
export const registerOrgAdminRouter = async (server: FastifyZodProvider) => {
server.route({
method: "GET",
url: "/projects",
config: {
rateLimit: readLimit
},
schema: {
querystring: z.object({
search: z.string().optional(),
offset: z.coerce.number().default(0),
limit: z.coerce.number().max(100).default(50)
}),
response: {
200: z.object({
projects: SanitizedProjectSchema.array(),
count: z.coerce.number()
})
}
},
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
handler: async (req) => {
const { projects, count } = await server.services.orgAdmin.listOrgProjects({
limit: req.query.limit,
offset: req.query.offset,
search: req.query.search,
actorOrgId: req.permission.orgId,
actorAuthMethod: req.permission.authMethod,
actorId: req.permission.id,
actor: req.permission.type
});
return { projects, count };
}
});
server.route({
method: "POST",
url: "/projects/:projectId/grant-admin-access",
config: {
rateLimit: readLimit
},
schema: {
params: z.object({
projectId: z.string()
}),
response: {
200: z.object({
membership: ProjectMembershipsSchema
})
}
},
onRequest: verifyAuth([AuthMode.JWT]),
handler: async (req) => {
const { membership } = await server.services.orgAdmin.grantProjectAdminAccess({
actorOrgId: req.permission.orgId,
actorAuthMethod: req.permission.authMethod,
actorId: req.permission.id,
actor: req.permission.type,
projectId: req.params.projectId
});
if (req.auth.authMode === AuthMode.JWT) {
await server.services.auditLog.createAuditLog({
...req.auditLogInfo,
projectId: req.params.projectId,
event: {
type: EventType.ORG_ADMIN_ACCESS_PROJECT,
metadata: {
projectId: req.params.projectId,
username: req.auth.user.username,
email: req.auth.user.email || "",
userId: req.auth.userId
}
}
});
}
return { membership };
}
});
};

View File

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

View File

@ -9,7 +9,7 @@ import {
UsersSchema
} from "@app/db/schemas";
import { ORGANIZATIONS } from "@app/lib/api-docs";
import { creationLimit, readLimit, writeLimit } from "@app/server/config/rateLimiter";
import { readLimit, writeLimit } from "@app/server/config/rateLimiter";
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
import { ActorType, AuthMode } from "@app/services/auth/auth-type";
@ -307,7 +307,7 @@ export const registerOrgRouter = async (server: FastifyZodProvider) => {
method: "POST",
url: "/",
config: {
rateLimit: creationLimit
rateLimit: writeLimit
},
schema: {
body: z.object({

View File

@ -4,7 +4,7 @@ import { z } from "zod";
import { CertificateAuthoritiesSchema, CertificatesSchema, ProjectKeysSchema } from "@app/db/schemas";
import { EventType } from "@app/ee/services/audit-log/audit-log-types";
import { PROJECTS } from "@app/lib/api-docs";
import { creationLimit, readLimit, writeLimit } from "@app/server/config/rateLimiter";
import { readLimit, writeLimit } from "@app/server/config/rateLimiter";
import { getTelemetryDistinctId } from "@app/server/lib/telemetry";
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
import { AuthMode } from "@app/services/auth/auth-type";
@ -142,7 +142,7 @@ export const registerProjectRouter = async (server: FastifyZodProvider) => {
method: "POST",
url: "/",
config: {
rateLimit: creationLimit
rateLimit: writeLimit
},
schema: {
description: "Create a new project",

View File

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

View File

@ -18,6 +18,40 @@ export const createDistinguishedName = (parts: TDNParts) => {
return dnParts.join(", ");
};
export const parseDistinguishedName = (dn: string): TDNParts => {
const parts: TDNParts = {};
const dnParts = dn.split(/,\s*/);
for (const part of dnParts) {
const [key, value] = part.split("=");
switch (key.toUpperCase()) {
case "C":
parts.country = value;
break;
case "O":
parts.organization = value;
break;
case "OU":
parts.ou = value;
break;
case "ST":
parts.province = value;
break;
case "CN":
parts.commonName = value;
break;
case "L":
parts.locality = value;
break;
default:
// Ignore unrecognized keys
break;
}
}
return parts;
};
export const keyAlgorithmToAlgCfg = (keyAlgorithm: CertKeyAlgorithm) => {
switch (keyAlgorithm) {
case CertKeyAlgorithm.RSA_4096:

View File

@ -22,7 +22,8 @@ import {
createDistinguishedName,
getCaCertChain,
getCaCredentials,
keyAlgorithmToAlgCfg
keyAlgorithmToAlgCfg,
parseDistinguishedName
} from "./certificate-authority-fns";
import { TCertificateAuthorityQueueFactory } from "./certificate-authority-queue";
import { TCertificateAuthoritySecretDALFactory } from "./certificate-authority-secret-dal";
@ -36,6 +37,7 @@ import {
TGetCaDTO,
TImportCertToCaDTO,
TIssueCertFromCaDTO,
TSignCertFromCaDTO,
TSignIntermediateDTO,
TUpdateCaDTO
} from "./certificate-authority-types";
@ -651,7 +653,8 @@ export const certificateAuthorityServiceFactory = ({
};
/**
* Return new leaf certificate issued by CA with id [caId]
* Return new leaf certificate issued by CA with id [caId] and private key.
* Note: private key and CSR are generated within Infisical.
*/
const issueCertFromCa = async ({
caId,
@ -851,6 +854,204 @@ 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,
csr,
friendlyName,
commonName,
altNames,
ttl,
notBefore,
notAfter,
actorId,
actorAuthMethod,
actor,
actorOrgId
}: TSignCertFromCaDTO) => {
const ca = await certificateAuthorityDAL.findById(caId);
if (!ca) throw new BadRequestError({ message: "CA not found" });
const { permission } = await permissionService.getProjectPermission(
actor,
actorId,
ca.projectId,
actorAuthMethod,
actorOrgId
);
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Create, ProjectPermissionSub.Certificates);
if (ca.status === CaStatus.DISABLED) throw new BadRequestError({ message: "CA is disabled" });
const caCert = await certificateAuthorityCertDAL.findOne({ caId: ca.id });
if (!caCert) throw new BadRequestError({ message: "CA does not have a certificate installed" });
const certificateManagerKmsId = await getProjectKmsCertificateKeyId({
projectId: ca.projectId,
projectDAL,
kmsService
});
const kmsDecryptor = await kmsService.decryptWithKmsKey({
kmsId: certificateManagerKmsId
});
const decryptedCaCert = await kmsDecryptor({
cipherTextBlob: caCert.encryptedCertificate
});
const caCertObj = new x509.X509Certificate(decryptedCaCert);
const notBeforeDate = notBefore ? new Date(notBefore) : new Date();
let notAfterDate = new Date(new Date().setFullYear(new Date().getFullYear() + 1));
if (notAfter) {
notAfterDate = new Date(notAfter);
} else if (ttl) {
notAfterDate = new Date(new Date().getTime() + ms(ttl));
}
const caCertNotBeforeDate = new Date(caCertObj.notBefore);
const caCertNotAfterDate = new Date(caCertObj.notAfter);
// check not before constraint
if (notBeforeDate < caCertNotBeforeDate) {
throw new BadRequestError({ message: "notBefore date is before CA certificate's notBefore date" });
}
if (notBeforeDate > notAfterDate) throw new BadRequestError({ message: "notBefore date is after notAfter date" });
// check not after constraint
if (notAfterDate > caCertNotAfterDate) {
throw new BadRequestError({ message: "notAfter date is after CA certificate's notAfter date" });
}
const alg = keyAlgorithmToAlgCfg(ca.keyAlgorithm as CertKeyAlgorithm);
const csrObj = new x509.Pkcs10CertificateRequest(csr);
const dn = parseDistinguishedName(csrObj.subject);
const cn = commonName || dn.commonName;
if (!cn)
throw new BadRequestError({
message: "A common name (CN) is required in the CSR or as a parameter to this endpoint"
});
const { caPrivateKey } = await getCaCredentials({
caId: ca.id,
certificateAuthorityDAL,
certificateAuthoritySecretDAL,
projectDAL,
kmsService
});
const extensions: x509.Extension[] = [
new x509.KeyUsagesExtension(x509.KeyUsageFlags.digitalSignature | x509.KeyUsageFlags.keyEncipherment, true),
new x509.BasicConstraintsExtension(false),
await x509.AuthorityKeyIdentifierExtension.create(caCertObj, false),
await x509.SubjectKeyIdentifierExtension.create(csrObj.publicKey)
];
if (altNames) {
const altNamesArray: {
type: "email" | "dns";
value: string;
}[] = altNames
.split(",")
.map((name) => name.trim())
.map((altName) => {
// check if the altName is a valid email
if (z.string().email().safeParse(altName).success) {
return {
type: "email",
value: altName
};
}
// check if the altName is a valid hostname
if (hostnameRegex.test(altName)) {
return {
type: "dns",
value: altName
};
}
// If altName is neither a valid email nor a valid hostname, throw an error or handle it accordingly
throw new Error(`Invalid altName: ${altName}`);
});
const altNamesExtension = new x509.SubjectAlternativeNameExtension(altNamesArray, false);
extensions.push(altNamesExtension);
}
const serialNumber = crypto.randomBytes(32).toString("hex");
const leafCert = await x509.X509CertificateGenerator.create({
serialNumber,
subject: csrObj.subject,
issuer: caCertObj.subject,
notBefore: notBeforeDate,
notAfter: notAfterDate,
signingKey: caPrivateKey,
publicKey: csrObj.publicKey,
signingAlgorithm: alg,
extensions
});
const kmsEncryptor = await kmsService.encryptWithKmsKey({
kmsId: certificateManagerKmsId
});
const { cipherTextBlob: encryptedCertificate } = await kmsEncryptor({
plainText: Buffer.from(new Uint8Array(leafCert.rawData))
});
await certificateDAL.transaction(async (tx) => {
const cert = await certificateDAL.create(
{
caId: ca.id,
status: CertStatus.ACTIVE,
friendlyName: friendlyName || csrObj.subject,
commonName: cn,
altNames,
serialNumber,
notBefore: notBeforeDate,
notAfter: notAfterDate
},
tx
);
await certificateBodyDAL.create(
{
certId: cert.id,
encryptedCertificate
},
tx
);
return cert;
});
const { caCert: issuingCaCertificate, caCertChain } = await getCaCertChain({
caId: ca.id,
certificateAuthorityDAL,
certificateAuthorityCertDAL,
projectDAL,
kmsService
});
return {
certificate: leafCert.toString("pem"),
certificateChain: `${issuingCaCertificate}\n${caCertChain}`.trim(),
issuingCaCertificate,
serialNumber,
ca
};
};
return {
createCa,
getCaById,
@ -860,6 +1061,7 @@ export const certificateAuthorityServiceFactory = ({
getCaCert,
signIntermediate,
importCertToCa,
issueCertFromCa
issueCertFromCa,
signCertFromCa
};
};

View File

@ -81,6 +81,17 @@ export type TIssueCertFromCaDTO = {
notAfter?: string;
} & Omit<TProjectPermission, "projectId">;
export type TSignCertFromCaDTO = {
caId: string;
csr: string;
friendlyName?: string;
commonName?: string;
altNames: string;
ttl: string;
notBefore?: string;
notAfter?: string;
} & Omit<TProjectPermission, "projectId">;
export type TDNParts = {
commonName?: string;
organization?: string;

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -256,7 +256,6 @@ export const projectMembershipServiceFactory = ({
}
const bot = await projectBotDAL.findOne({ projectId });
if (!bot) {
throw new BadRequestError({
message: "Failed to find bot"
@ -540,7 +539,7 @@ export const projectMembershipServiceFactory = ({
const project = await projectDAL.findById(projectId);
if (!project) throw new BadRequestError({ message: "Project not found" });
if (project.version !== ProjectVersion.V2) {
if (project.version === ProjectVersion.V1) {
throw new BadRequestError({
message: "Please ask your project administrator to upgrade the project before leaving."
});

View File

@ -22,6 +22,7 @@ import { TIdentityProjectMembershipRoleDALFactory } from "../identity-project/id
import { TKmsServiceFactory } from "../kms/kms-service";
import { TOrgDALFactory } from "../org/org-dal";
import { TOrgServiceFactory } from "../org/org-service";
import { TProjectBotDALFactory } from "../project-bot/project-bot-dal";
import { TProjectEnvDALFactory } from "../project-env/project-env-dal";
import { TProjectKeyDALFactory } from "../project-key/project-key-dal";
import { TProjectMembershipDALFactory } from "../project-membership/project-membership-dal";
@ -74,6 +75,7 @@ type TProjectServiceFactoryDep = {
licenseService: Pick<TLicenseServiceFactory, "getPlan">;
orgDAL: Pick<TOrgDALFactory, "findOne">;
keyStore: Pick<TKeyStoreFactory, "deleteItem">;
projectBotDAL: Pick<TProjectBotDALFactory, "create">;
kmsService: Pick<
TKmsServiceFactory,
| "updateProjectSecretManagerKmsKey"
@ -106,7 +108,8 @@ export const projectServiceFactory = ({
certificateAuthorityDAL,
certificateDAL,
keyStore,
kmsService
kmsService,
projectBotDAL
}: TProjectServiceFactoryDep) => {
/*
* Create workspace. Make user the admin
@ -206,7 +209,26 @@ export const projectServiceFactory = ({
tx
);
// const { iv, tag, ciphertext, encoding, algorithm } = infisicalSymmetricEncypt(ghostUser.keys.plainPrivateKey);
const { iv, tag, ciphertext, encoding, algorithm } = infisicalSymmetricEncypt(ghostUser.keys.plainPrivateKey);
// 5. Create & a bot for the project
await projectBotDAL.create(
{
name: "Infisical Bot (Ghost)",
projectId: project.id,
tag,
iv,
encryptedProjectKey,
encryptedProjectKeyNonce: encryptedProjectKeyIv,
encryptedPrivateKey: ciphertext,
isActive: true,
publicKey: ghostUser.keys.publicKey,
senderId: ghostUser.user.id,
algorithm,
keyEncoding: encoding
},
tx
);
// Find the ghost users latest key
const latestKey = await projectKeyDAL.findLatestProjectKey(ghostUser.user.id, project.id, tx);

View File

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

View File

@ -0,0 +1,6 @@
import { RawRule } from "@casl/ability";
import { ProjectPermissionSub } from "@app/ee/services/permission/project-permission";
export const shouldCheckFolderPermission = (rules: RawRule[]) =>
rules.some((rule) => (rule.subject as ProjectPermissionSub[]).includes(ProjectPermissionSub.SecretFolders));

View File

@ -11,6 +11,7 @@ import { BadRequestError, NotFoundError } from "@app/lib/errors";
import { TProjectDALFactory } from "../project/project-dal";
import { TProjectEnvDALFactory } from "../project-env/project-env-dal";
import { TSecretFolderDALFactory } from "./secret-folder-dal";
import { shouldCheckFolderPermission } from "./secret-folder-fns";
import {
TCreateFolderDTO,
TDeleteFolderDTO,
@ -57,10 +58,21 @@ export const secretFolderServiceFactory = ({
actorAuthMethod,
actorOrgId
);
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Create,
subject(ProjectPermissionSub.Secrets, { environment, secretPath })
);
// we do this because we've split Secret and SecretFolder resources
// previously, if one can create/update/read/delete secrets then they can do the same for folders
// for backwards compatibility, we handle authorization only when SecretFolders subject is used
if (shouldCheckFolderPermission(permission.rules)) {
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Create,
subject(ProjectPermissionSub.SecretFolders, { environment, secretPath })
);
} else {
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Create,
subject(ProjectPermissionSub.Secrets, { environment, secretPath })
);
}
const env = await projectEnvDAL.findOne({ projectId, slug: environment });
if (!env) throw new BadRequestError({ message: "Environment not found", name: "Create folder" });
@ -148,10 +160,20 @@ export const secretFolderServiceFactory = ({
);
folders.forEach(({ environment, path: secretPath }) => {
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Edit,
subject(ProjectPermissionSub.Secrets, { environment, secretPath })
);
// we do this because we've split Secret and SecretFolder resources
// previously, if one can create/update/read/delete secrets then they can do the same for folders
// for backwards compatibility, we handle authorization only when SecretFolders subject is used
if (shouldCheckFolderPermission(permission.rules)) {
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Edit,
subject(ProjectPermissionSub.SecretFolders, { environment, secretPath })
);
} else {
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Edit,
subject(ProjectPermissionSub.Secrets, { environment, secretPath })
);
}
});
const result = await folderDAL.transaction(async (tx) =>
@ -243,10 +265,21 @@ export const secretFolderServiceFactory = ({
actorAuthMethod,
actorOrgId
);
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Edit,
subject(ProjectPermissionSub.Secrets, { environment, secretPath })
);
// we do this because we've split Secret and SecretFolder resources
// previously, if one can create/update/read/delete secrets then they can do the same for folders
// for backwards compatibility, we handle authorization differently only when SecretFolders subject is used
if (shouldCheckFolderPermission(permission.rules)) {
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Edit,
subject(ProjectPermissionSub.SecretFolders, { environment, secretPath })
);
} else {
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Edit,
subject(ProjectPermissionSub.Secrets, { environment, secretPath })
);
}
const parentFolder = await folderDAL.findBySecretPath(projectId, environment, secretPath);
if (!parentFolder) throw new BadRequestError({ message: "Secret path not found" });
@ -316,10 +349,21 @@ export const secretFolderServiceFactory = ({
actorAuthMethod,
actorOrgId
);
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Delete,
subject(ProjectPermissionSub.Secrets, { environment, secretPath })
);
// we do this because we've split Secret and SecretFolder resources
// previously, if one can create/update/read/delete secrets then they can do the same for folders
// for backwards compatibility, we handle authorization differently only when SecretFolders subject is used
if (shouldCheckFolderPermission(permission.rules)) {
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Delete,
subject(ProjectPermissionSub.SecretFolders, { environment, secretPath })
);
} else {
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Delete,
subject(ProjectPermissionSub.Secrets, { environment, secretPath })
);
}
const env = await projectEnvDAL.findOne({ projectId, slug: environment });
if (!env) throw new BadRequestError({ message: "Environment not found", name: "Create folder" });

View File

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

View File

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

View File

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

View File

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

View File

@ -1,14 +1,12 @@
import { TProjectPermission } from "@app/lib/types";
export type TCreateTagDTO = {
name: string;
color: string;
slug: string;
} & TProjectPermission;
export type TUpdateTagDTO = {
id: string;
name?: string;
slug?: string;
color?: string;
} & Omit<TProjectPermission, "projectId">;

View File

@ -136,7 +136,6 @@ export const secretV2BridgeDALFactory = (db: TDbClient) => {
.select(db.ref("id").withSchema(TableName.SecretTag).as("tagId"))
.select(db.ref("color").withSchema(TableName.SecretTag).as("tagColor"))
.select(db.ref("slug").withSchema(TableName.SecretTag).as("tagSlug"))
.select(db.ref("name").withSchema(TableName.SecretTag).as("tagName"))
.orderBy("id", "asc");
const data = sqlNestRelationships({
@ -147,11 +146,11 @@ export const secretV2BridgeDALFactory = (db: TDbClient) => {
{
key: "tagId",
label: "tags" as const,
mapper: ({ tagId: id, tagColor: color, tagSlug: slug, tagName: name }) => ({
mapper: ({ tagId: id, tagColor: color, tagSlug: slug }) => ({
id,
color,
slug,
name
name: slug
})
}
]
@ -169,14 +168,13 @@ export const secretV2BridgeDALFactory = (db: TDbClient) => {
.where({ [`${TableName.SecretV2}Id` as const]: secretId })
.select(db.ref("id").withSchema(TableName.SecretTag).as("tagId"))
.select(db.ref("color").withSchema(TableName.SecretTag).as("tagColor"))
.select(db.ref("slug").withSchema(TableName.SecretTag).as("tagSlug"))
.select(db.ref("name").withSchema(TableName.SecretTag).as("tagName"));
.select(db.ref("slug").withSchema(TableName.SecretTag).as("tagSlug"));
return tags.map((el) => ({
id: el.tagId,
color: el.tagColor,
slug: el.tagSlug,
name: el.tagName
name: el.tagSlug
}));
} catch (error) {
throw new DatabaseError({ error, name: "get secret tags" });
@ -210,7 +208,6 @@ export const secretV2BridgeDALFactory = (db: TDbClient) => {
.select(db.ref("id").withSchema(TableName.SecretTag).as("tagId"))
.select(db.ref("color").withSchema(TableName.SecretTag).as("tagColor"))
.select(db.ref("slug").withSchema(TableName.SecretTag).as("tagSlug"))
.select(db.ref("name").withSchema(TableName.SecretTag).as("tagName"))
.orderBy("id", "asc");
const data = sqlNestRelationships({
@ -221,11 +218,11 @@ export const secretV2BridgeDALFactory = (db: TDbClient) => {
{
key: "tagId",
label: "tags" as const,
mapper: ({ tagId: id, tagColor: color, tagSlug: slug, tagName: name }) => ({
mapper: ({ tagId: id, tagColor: color, tagSlug: slug }) => ({
id,
color,
slug,
name
name: slug
})
}
]
@ -290,7 +287,7 @@ export const secretV2BridgeDALFactory = (db: TDbClient) => {
}))
);
if (!newSecretReferences.length) return;
const secretReferences = await (tx || db)(TableName.SecretReferenceV2).insert(newSecretReferences);
const secretReferences = await (tx || db).batchInsert(TableName.SecretReferenceV2, newSecretReferences);
return secretReferences;
} catch (error) {
throw new DatabaseError({ error, name: "UpsertSecretReference" });
@ -350,8 +347,7 @@ export const secretV2BridgeDALFactory = (db: TDbClient) => {
.select(selectAllTableCols(TableName.SecretV2))
.select(db.ref("id").withSchema(TableName.SecretTag).as("tagId"))
.select(db.ref("color").withSchema(TableName.SecretTag).as("tagColor"))
.select(db.ref("slug").withSchema(TableName.SecretTag).as("tagSlug"))
.select(db.ref("name").withSchema(TableName.SecretTag).as("tagName"));
.select(db.ref("slug").withSchema(TableName.SecretTag).as("tagSlug"));
const docs = sqlNestRelationships({
data: rawDocs,
key: "id",
@ -360,11 +356,11 @@ export const secretV2BridgeDALFactory = (db: TDbClient) => {
{
key: "tagId",
label: "tags" as const,
mapper: ({ tagId: id, tagColor: color, tagSlug: slug, tagName: name }) => ({
mapper: ({ tagId: id, tagColor: color, tagSlug: slug }) => ({
id,
color,
slug,
name
name: slug
})
}
]

View File

@ -528,8 +528,8 @@ export const reshapeBridgeSecret = (
environment: string,
secretPath: string,
secret: Omit<TSecretsV2, "encryptedValue" | "encryptedComment"> & {
value?: string;
comment?: string;
value: string;
comment: string;
tags?: {
id: string;
slug: string;
@ -542,8 +542,8 @@ export const reshapeBridgeSecret = (
secretPath,
workspace: workspaceId,
environment,
secretValue: secret.value,
secretComment: secret.comment,
secretValue: secret.value || "",
secretComment: secret.comment || "",
version: secret.version,
type: secret.type,
_id: secret.id,

View File

@ -196,7 +196,7 @@ export const secretV2BridgeServiceFactory = ({
return reshapeBridgeSecret(projectId, environment, secretPath, {
...secret[0],
value: inputSecret.secretValue,
comment: inputSecret.secretComment
comment: inputSecret.secretComment || ""
});
};
@ -339,8 +339,8 @@ export const secretV2BridgeServiceFactory = ({
});
return reshapeBridgeSecret(projectId, environment, secretPath, {
...updatedSecret[0],
value: inputSecret.secretValue,
comment: inputSecret.secretComment
value: inputSecret.secretValue || "",
comment: inputSecret.secretComment || ""
});
};
@ -378,6 +378,18 @@ export const secretV2BridgeServiceFactory = ({
throw new BadRequestError({ message: "Must be user to delete personal secret" });
}
const secretToDelete = await secretDAL.findOne({
key: inputSecret.secretName,
folderId,
...(inputSecret.type === SecretType.Shared
? {}
: {
type: SecretType.Personal,
userId: actorId
})
});
if (!secretToDelete) throw new NotFoundError({ message: "Secret not found" });
const deletedSecret = await secretDAL.transaction(async (tx) =>
fnSecretBulkDelete({
projectId,
@ -412,10 +424,10 @@ export const secretV2BridgeServiceFactory = ({
...deletedSecret[0],
value: deletedSecret[0].encryptedValue
? secretManagerDecryptor({ cipherTextBlob: deletedSecret[0].encryptedValue }).toString()
: undefined,
: "",
comment: deletedSecret[0].encryptedComment
? secretManagerDecryptor({ cipherTextBlob: deletedSecret[0].encryptedComment }).toString()
: undefined
: ""
});
};
@ -429,6 +441,7 @@ export const secretV2BridgeServiceFactory = ({
actorAuthMethod,
includeImports,
recursive,
tagSlugs = [],
expandSecretReferences: shouldExpandSecretReferences
}: TGetSecretsDTO) => {
const { permission } = await permissionService.getProjectPermission(
@ -490,12 +503,15 @@ export const secretV2BridgeServiceFactory = ({
...secret,
value: secret.encryptedValue
? secretManagerDecryptor({ cipherTextBlob: secret.encryptedValue }).toString()
: undefined,
: "",
comment: secret.encryptedComment
? secretManagerDecryptor({ cipherTextBlob: secret.encryptedComment }).toString()
: undefined
: ""
})
);
const filteredSecrets = tagSlugs.length
? decryptedSecrets.filter((secret) => Boolean(secret.tags?.find((el) => tagSlugs.includes(el.slug))))
: decryptedSecrets;
const expandSecretReferences = expandSecretReferencesFactory({
projectId,
folderDAL,
@ -504,7 +520,7 @@ export const secretV2BridgeServiceFactory = ({
});
if (shouldExpandSecretReferences) {
const secretsGroupByPath = groupBy(decryptedSecrets, (i) => i.secretPath);
const secretsGroupByPath = groupBy(filteredSecrets, (i) => i.secretPath);
for (const secretPathKey in secretsGroupByPath) {
if (Object.hasOwn(secretsGroupByPath, secretPathKey)) {
const secretsGroupByKey = secretsGroupByPath[secretPathKey].reduce(
@ -522,7 +538,7 @@ export const secretV2BridgeServiceFactory = ({
await expandSecretReferences(secretsGroupByKey);
secretsGroupByPath[secretPathKey].forEach((decryptedSecret) => {
// eslint-disable-next-line no-param-reassign
decryptedSecret.secretValue = secretsGroupByKey[decryptedSecret.secretKey].value;
decryptedSecret.secretValue = secretsGroupByKey[decryptedSecret.secretKey].value || "";
});
}
}
@ -530,7 +546,7 @@ export const secretV2BridgeServiceFactory = ({
if (!includeImports) {
return {
secrets: decryptedSecrets
secrets: filteredSecrets
};
}
@ -554,11 +570,11 @@ export const secretV2BridgeServiceFactory = ({
folderDAL,
secretImportDAL,
expandSecretReferences,
decryptor: (value) => (value ? secretManagerDecryptor({ cipherTextBlob: value }).toString() : undefined)
decryptor: (value) => (value ? secretManagerDecryptor({ cipherTextBlob: value }).toString() : "")
});
return {
secrets: decryptedSecrets,
secrets: filteredSecrets,
imports: importedSecrets
};
};
@ -654,7 +670,7 @@ export const secretV2BridgeServiceFactory = ({
secretDAL,
folderDAL,
secretImportDAL,
decryptor: (value) => (value ? secretManagerDecryptor({ cipherTextBlob: value }).toString() : undefined),
decryptor: (value) => (value ? secretManagerDecryptor({ cipherTextBlob: value }).toString() : ""),
expandSecretReferences: shouldExpandSecretReferences ? expandSecretReferences : undefined
});
@ -662,12 +678,11 @@ export const secretV2BridgeServiceFactory = ({
for (let j = 0; j < importedSecrets[i].secrets.length; j += 1) {
const importedSecret = importedSecrets[i].secrets[j];
if (secretName === importedSecret.key) {
return reshapeBridgeSecret(
projectId,
importedSecrets[i].environment,
importedSecrets[i].secretPath,
importedSecret
);
return reshapeBridgeSecret(projectId, importedSecrets[i].environment, importedSecrets[i].secretPath, {
...importedSecret,
value: importedSecret.secretValue || "",
comment: importedSecret.secretComment || ""
});
}
}
}
@ -676,7 +691,7 @@ export const secretV2BridgeServiceFactory = ({
let secretValue = secret.encryptedValue
? secretManagerDecryptor({ cipherTextBlob: secret.encryptedValue }).toString()
: undefined;
: "";
if (shouldExpandSecretReferences && secretValue) {
const secretReferenceExpandedRecord = {
[secret.key]: { value: secretValue }
@ -691,7 +706,7 @@ export const secretV2BridgeServiceFactory = ({
value: secretValue,
comment: secret.encryptedComment
? secretManagerDecryptor({ cipherTextBlob: secret.encryptedComment }).toString()
: undefined
: ""
});
};
@ -781,10 +796,8 @@ export const secretV2BridgeServiceFactory = ({
return newSecrets.map((el) =>
reshapeBridgeSecret(projectId, environment, secretPath, {
...el,
value: el.encryptedValue ? secretManagerDecryptor({ cipherTextBlob: el.encryptedValue }).toString() : undefined,
comment: el.encryptedComment
? secretManagerDecryptor({ cipherTextBlob: el.encryptedComment }).toString()
: undefined
value: el.encryptedValue ? secretManagerDecryptor({ cipherTextBlob: el.encryptedValue }).toString() : "",
comment: el.encryptedComment ? secretManagerDecryptor({ cipherTextBlob: el.encryptedComment }).toString() : ""
})
);
};
@ -902,10 +915,8 @@ export const secretV2BridgeServiceFactory = ({
return secrets.map((el) =>
reshapeBridgeSecret(projectId, environment, secretPath, {
...el,
value: el.encryptedValue ? secretManagerDecryptor({ cipherTextBlob: el.encryptedValue }).toString() : undefined,
comment: el.encryptedComment
? secretManagerDecryptor({ cipherTextBlob: el.encryptedComment }).toString()
: undefined
value: el.encryptedValue ? secretManagerDecryptor({ cipherTextBlob: el.encryptedValue }).toString() : "",
comment: el.encryptedComment ? secretManagerDecryptor({ cipherTextBlob: el.encryptedComment }).toString() : ""
})
);
};
@ -981,10 +992,8 @@ export const secretV2BridgeServiceFactory = ({
return secretsDeleted.map((el) =>
reshapeBridgeSecret(projectId, environment, secretPath, {
...el,
value: el.encryptedValue ? secretManagerDecryptor({ cipherTextBlob: el.encryptedValue }).toString() : undefined,
comment: el.encryptedComment
? secretManagerDecryptor({ cipherTextBlob: el.encryptedComment }).toString()
: undefined
value: el.encryptedValue ? secretManagerDecryptor({ cipherTextBlob: el.encryptedValue }).toString() : "",
comment: el.encryptedComment ? secretManagerDecryptor({ cipherTextBlob: el.encryptedComment }).toString() : ""
})
);
};
@ -1020,10 +1029,8 @@ export const secretV2BridgeServiceFactory = ({
return secretVersions.map((el) =>
reshapeBridgeSecret(folder.projectId, folder.environment.envSlug, "/", {
...el,
value: el.encryptedValue ? secretManagerDecryptor({ cipherTextBlob: el.encryptedValue }).toString() : undefined,
comment: el.encryptedComment
? secretManagerDecryptor({ cipherTextBlob: el.encryptedComment }).toString()
: undefined
value: el.encryptedValue ? secretManagerDecryptor({ cipherTextBlob: el.encryptedValue }).toString() : "",
comment: el.encryptedComment ? secretManagerDecryptor({ cipherTextBlob: el.encryptedComment }).toString() : ""
})
);
};

View File

@ -20,6 +20,7 @@ export type TGetSecretsDTO = {
environment: string;
includeImports?: boolean;
recursive?: boolean;
tagSlugs?: string[];
} & TProjectPermission;
export type TGetASecretDTO = {

View File

@ -123,7 +123,6 @@ export const secretDALFactory = (db: TDbClient) => {
.select(db.ref("id").withSchema(TableName.SecretTag).as("tagId"))
.select(db.ref("color").withSchema(TableName.SecretTag).as("tagColor"))
.select(db.ref("slug").withSchema(TableName.SecretTag).as("tagSlug"))
.select(db.ref("name").withSchema(TableName.SecretTag).as("tagName"))
.orderBy("id", "asc");
const data = sqlNestRelationships({
data: secs,
@ -133,11 +132,11 @@ export const secretDALFactory = (db: TDbClient) => {
{
key: "tagId",
label: "tags" as const,
mapper: ({ tagId: id, tagColor: color, tagSlug: slug, tagName: name }) => ({
mapper: ({ tagId: id, tagColor: color, tagSlug: slug }) => ({
id,
color,
slug,
name
name: slug
})
}
]
@ -155,14 +154,13 @@ export const secretDALFactory = (db: TDbClient) => {
.where({ [`${TableName.Secret}Id` as const]: secretId })
.select(db.ref("id").withSchema(TableName.SecretTag).as("tagId"))
.select(db.ref("color").withSchema(TableName.SecretTag).as("tagColor"))
.select(db.ref("slug").withSchema(TableName.SecretTag).as("tagSlug"))
.select(db.ref("name").withSchema(TableName.SecretTag).as("tagName"));
.select(db.ref("slug").withSchema(TableName.SecretTag).as("tagSlug"));
return tags.map((el) => ({
id: el.tagId,
color: el.tagColor,
slug: el.tagSlug,
name: el.tagName
name: el.tagSlug
}));
} catch (error) {
throw new DatabaseError({ error, name: "get secret tags" });
@ -188,7 +186,6 @@ export const secretDALFactory = (db: TDbClient) => {
.select(db.ref("id").withSchema(TableName.SecretTag).as("tagId"))
.select(db.ref("color").withSchema(TableName.SecretTag).as("tagColor"))
.select(db.ref("slug").withSchema(TableName.SecretTag).as("tagSlug"))
.select(db.ref("name").withSchema(TableName.SecretTag).as("tagName"))
.orderBy("id", "asc");
const data = sqlNestRelationships({
data: secs,
@ -198,11 +195,11 @@ export const secretDALFactory = (db: TDbClient) => {
{
key: "tagId",
label: "tags" as const,
mapper: ({ tagId: id, tagColor: color, tagSlug: slug, tagName: name }) => ({
mapper: ({ tagId: id, tagColor: color, tagSlug: slug }) => ({
id,
color,
slug,
name
name: slug
})
}
]
@ -318,8 +315,7 @@ export const secretDALFactory = (db: TDbClient) => {
.select(selectAllTableCols(TableName.Secret))
.select(db.ref("id").withSchema(TableName.SecretTag).as("tagId"))
.select(db.ref("color").withSchema(TableName.SecretTag).as("tagColor"))
.select(db.ref("slug").withSchema(TableName.SecretTag).as("tagSlug"))
.select(db.ref("name").withSchema(TableName.SecretTag).as("tagName"));
.select(db.ref("slug").withSchema(TableName.SecretTag).as("tagSlug"));
const docs = sqlNestRelationships({
data: rawDocs,
key: "id",
@ -328,11 +324,11 @@ export const secretDALFactory = (db: TDbClient) => {
{
key: "tagId",
label: "tags" as const,
mapper: ({ tagId: id, tagColor: color, tagSlug: slug, tagName: name }) => ({
mapper: ({ tagId: id, tagColor: color, tagSlug: slug }) => ({
id,
color,
slug,
name
name: slug
})
}
]

View File

@ -370,7 +370,6 @@ export const decryptSecretRaw = (
id: string;
slug: string;
color?: string | null;
name: string;
}[];
},
key: string
@ -412,7 +411,7 @@ export const decryptSecretRaw = (
_id: secret.id,
id: secret.id,
user: secret.userId,
tags: secret.tags,
tags: secret.tags?.map((el) => ({ ...el, name: el.slug })),
skipMultilineEncoding: secret.skipMultilineEncoding,
secretReminderRepeatDays: secret.secretReminderRepeatDays,
secretReminderNote: secret.secretReminderNote,

View File

@ -73,12 +73,12 @@ type TSecretQueueFactoryDep = {
secretVersionTagDAL: TSecretVersionTagDALFactory;
kmsService: Pick<TKmsServiceFactory, "createCipherPairWithDataKey">;
secretV2BridgeDAL: TSecretV2BridgeDALFactory;
secretVersionV2BridgeDAL: Pick<TSecretVersionV2DALFactory, "insertMany" | "findLatestVersionMany">;
secretVersionTagV2BridgeDAL: Pick<TSecretVersionV2TagDALFactory, "insertMany">;
secretVersionV2BridgeDAL: Pick<TSecretVersionV2DALFactory, "batchInsert" | "insertMany" | "findLatestVersionMany">;
secretVersionTagV2BridgeDAL: Pick<TSecretVersionV2TagDALFactory, "insertMany" | "batchInsert">;
secretRotationDAL: Pick<TSecretRotationDALFactory, "secretOutputV2InsertMany" | "find">;
secretApprovalRequestDAL: Pick<TSecretApprovalRequestDALFactory, "deleteByProjectId">;
snapshotDAL: Pick<TSnapshotDALFactory, "findNSecretV1SnapshotByFolderId" | "deleteSnapshotsAboveLimit">;
snapshotSecretV2BridgeDAL: Pick<TSnapshotSecretV2DALFactory, "insertMany">;
snapshotSecretV2BridgeDAL: Pick<TSnapshotSecretV2DALFactory, "insertMany" | "batchInsert">;
};
export type TGetSecrets = {
@ -728,7 +728,10 @@ export const secretQueueFactory = ({
isSynced: response?.isSynced ?? true
});
} catch (err) {
logger.info("Secret integration sync error: %o", err);
logger.error(
err,
`Secret integration sync error [projectId=${job.data.projectId}] [environment=${job.data.environment}] [secretPath=${job.data.secretPath}]`
);
const message =
(err instanceof AxiosError ? JSON.stringify(err?.response?.data) : (err as Error)?.message) ||
@ -828,7 +831,7 @@ export const secretQueueFactory = ({
secretId: string;
references: { environment: string; secretPath: string; secretKey: string }[];
}[] = [];
await secretV2BridgeDAL.insertMany(
await secretV2BridgeDAL.batchInsert(
projectV1Secrets.map((el) => {
const key = decryptSymmetric128BitHexKeyUTF8({
ciphertext: el.secretKeyCiphertext,
@ -1004,14 +1007,14 @@ export const secretQueueFactory = ({
const projectV3SecretVersions = Object.values(projectV3SecretVersionsGroupById);
if (projectV3SecretVersions.length) {
await secretVersionV2BridgeDAL.insertMany(projectV3SecretVersions, tx);
await secretVersionV2BridgeDAL.batchInsert(projectV3SecretVersions, tx);
}
if (projectV3SecretVersionTags.length) {
await secretVersionTagV2BridgeDAL.insertMany(projectV3SecretVersionTags, tx);
await secretVersionTagV2BridgeDAL.batchInsert(projectV3SecretVersionTags, tx);
}
if (projectV3SnapshotSecrets.length) {
await snapshotSecretV2BridgeDAL.insertMany(projectV3SnapshotSecrets, tx);
await snapshotSecretV2BridgeDAL.batchInsert(projectV3SnapshotSecrets, tx);
}
await snapshotDAL.deleteSnapshotsAboveLimit(folderId, SNAPSHOT_BATCH_SIZE, tx);
}
@ -1133,7 +1136,7 @@ export const secretQueueFactory = ({
});
queueService.start(QueueName.SecretWebhook, async (job) => {
await fnTriggerWebhook({ ...job.data, projectEnvDAL, webhookDAL, projectDAL, kmsService });
await fnTriggerWebhook({ ...job.data, projectEnvDAL, webhookDAL, projectDAL });
});
return {

View File

@ -964,7 +964,8 @@ export const secretServiceFactory = ({
environment,
includeImports,
expandSecretReferences,
recursive
recursive,
tagSlugs = []
}: TGetSecretsRawDTO) => {
const { botKey, shouldUseSecretV2Bridge } = await projectBotService.getBotKey(projectId);
if (shouldUseSecretV2Bridge) {
@ -978,7 +979,8 @@ export const secretServiceFactory = ({
path,
recursive,
actorAuthMethod,
includeImports
includeImports,
tagSlugs
});
return { secrets, imports };
}
@ -998,6 +1000,9 @@ export const secretServiceFactory = ({
});
const decryptedSecrets = secrets.map((el) => decryptSecretRaw(el, botKey));
const filteredSecrets = tagSlugs.length
? decryptedSecrets.filter((secret) => Boolean(secret.tags?.find((el) => tagSlugs.includes(el.slug))))
: decryptedSecrets;
const processedImports = (imports || [])?.map(({ secrets: importedSecrets, ...el }) => {
const decryptedImportSecrets = importedSecrets.map((sec) =>
decryptSecretRaw(
@ -1106,14 +1111,14 @@ export const secretServiceFactory = ({
};
// expand secrets
await batchSecretsExpand(decryptedSecrets);
await batchSecretsExpand(filteredSecrets);
// expand imports by batch
await Promise.all(processedImports.map((processedImport) => batchSecretsExpand(processedImport.secrets)));
}
return {
secrets: decryptedSecrets,
secrets: filteredSecrets,
imports: processedImports
};
};
@ -1149,6 +1154,7 @@ export const secretServiceFactory = ({
type,
secretName
});
return secret;
}
@ -2081,7 +2087,7 @@ export const secretServiceFactory = ({
return {
...updatedSecret[0],
tags: [...existingSecretTags, ...tags].map((t) => ({ id: t.id, slug: t.slug, name: t.name, color: t.color }))
tags: [...existingSecretTags, ...tags].map((t) => ({ id: t.id, slug: t.slug, name: t.slug, color: t.color }))
};
};

View File

@ -149,6 +149,7 @@ export type TGetSecretsRawDTO = {
environment: string;
includeImports?: boolean;
recursive?: boolean;
tagSlugs?: string[];
} & TProjectPermission;
export type TGetASecretRawDTO = {

View File

@ -3,12 +3,12 @@ import crypto from "node:crypto";
import { AxiosError } from "axios";
import picomatch from "picomatch";
import { SecretKeyEncoding, TWebhooks } from "@app/db/schemas";
import { request } from "@app/lib/config/request";
import { infisicalSymmetricDecrypt } from "@app/lib/crypto/encryption";
import { BadRequestError } from "@app/lib/errors";
import { logger } from "@app/lib/logger";
import { TKmsServiceFactory } from "../kms/kms-service";
import { KmsDataKey } from "../kms/kms-types";
import { TProjectDALFactory } from "../project/project-dal";
import { TProjectEnvDALFactory } from "../project-env/project-env-dal";
import { TWebhookDALFactory } from "./webhook-dal";
@ -16,12 +16,40 @@ import { WebhookType } from "./webhook-types";
const WEBHOOK_TRIGGER_TIMEOUT = 15 * 1000;
export const triggerWebhookRequest = async (
{ webhookSecretKey: secretKey, webhookUrl: url }: { webhookSecretKey?: string; webhookUrl: string },
data: Record<string, unknown>
) => {
export const decryptWebhookDetails = (webhook: TWebhooks) => {
const { keyEncoding, iv, encryptedSecretKey, tag, urlCipherText, urlIV, urlTag, url } = webhook;
let decryptedSecretKey = "";
let decryptedUrl = url;
if (encryptedSecretKey) {
decryptedSecretKey = infisicalSymmetricDecrypt({
keyEncoding: keyEncoding as SecretKeyEncoding,
ciphertext: encryptedSecretKey,
iv: iv as string,
tag: tag as string
});
}
if (urlCipherText) {
decryptedUrl = infisicalSymmetricDecrypt({
keyEncoding: keyEncoding as SecretKeyEncoding,
ciphertext: urlCipherText,
iv: urlIV as string,
tag: urlTag as string
});
}
return {
secretKey: decryptedSecretKey,
url: decryptedUrl
};
};
export const triggerWebhookRequest = async (webhook: TWebhooks, data: Record<string, unknown>) => {
const headers: Record<string, string> = {};
const payload = { ...data, timestamp: Date.now() };
const { secretKey, url } = decryptWebhookDetails(webhook);
if (secretKey) {
const webhookSign = crypto.createHmac("sha256", secretKey).update(JSON.stringify(payload)).digest("hex");
@ -96,7 +124,6 @@ export type TFnTriggerWebhookDTO = {
webhookDAL: Pick<TWebhookDALFactory, "findAllWebhooks" | "transaction" | "update" | "bulkUpdate">;
projectEnvDAL: Pick<TProjectEnvDALFactory, "findOne">;
projectDAL: Pick<TProjectDALFactory, "findById">;
kmsService: Pick<TKmsServiceFactory, "createCipherPairWithDataKey">;
};
// this is reusable function
@ -107,8 +134,7 @@ export const fnTriggerWebhook = async ({
projectId,
webhookDAL,
projectEnvDAL,
projectDAL,
kmsService
projectDAL
}: TFnTriggerWebhookDTO) => {
const webhooks = await webhookDAL.findAllWebhooks(projectId, environment);
const toBeTriggeredHooks = webhooks.filter(
@ -118,20 +144,10 @@ export const fnTriggerWebhook = async ({
if (!toBeTriggeredHooks.length) return;
logger.info("Secret webhook job started", { environment, secretPath, projectId });
const project = await projectDAL.findById(projectId);
const { decryptor: kmsDataKeyDecryptor } = await kmsService.createCipherPairWithDataKey({
projectId,
type: KmsDataKey.SecretManager
});
const webhooksTriggered = await Promise.allSettled(
toBeTriggeredHooks.map((hook) => {
const webhookUrl = kmsDataKeyDecryptor({ cipherTextBlob: hook.encryptedUrl }).toString();
const webhookSecretKey = hook.encryptedSecretKeyWithKms
? kmsDataKeyDecryptor({ cipherTextBlob: hook.encryptedSecretKeyWithKms }).toString()
: undefined;
return triggerWebhookRequest(
{ webhookUrl, webhookSecretKey },
toBeTriggeredHooks.map((hook) =>
triggerWebhookRequest(
hook,
getWebhookPayload("secrets.modified", {
workspaceName: project.name,
workspaceId: projectId,
@ -139,8 +155,8 @@ export const fnTriggerWebhook = async ({
secretPath,
type: hook.type
})
);
})
)
)
);
// filter hooks by status

View File

@ -1,15 +1,15 @@
import { ForbiddenError } from "@casl/ability";
import { TWebhooksInsert } from "@app/db/schemas";
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service";
import { ProjectPermissionActions, ProjectPermissionSub } from "@app/ee/services/permission/project-permission";
import { infisicalSymmetricEncypt } from "@app/lib/crypto/encryption";
import { BadRequestError } from "@app/lib/errors";
import { TKmsServiceFactory } from "../kms/kms-service";
import { KmsDataKey } from "../kms/kms-types";
import { TProjectDALFactory } from "../project/project-dal";
import { TProjectEnvDALFactory } from "../project-env/project-env-dal";
import { TWebhookDALFactory } from "./webhook-dal";
import { getWebhookPayload, triggerWebhookRequest } from "./webhook-fns";
import { decryptWebhookDetails, getWebhookPayload, triggerWebhookRequest } from "./webhook-fns";
import {
TCreateWebhookDTO,
TDeleteWebhookDTO,
@ -23,7 +23,6 @@ type TWebhookServiceFactoryDep = {
projectEnvDAL: TProjectEnvDALFactory;
projectDAL: Pick<TProjectDALFactory, "findById">;
permissionService: Pick<TPermissionServiceFactory, "getProjectPermission">;
kmsService: Pick<TKmsServiceFactory, "createCipherPairWithDataKey">;
};
export type TWebhookServiceFactory = ReturnType<typeof webhookServiceFactory>;
@ -32,8 +31,7 @@ export const webhookServiceFactory = ({
webhookDAL,
projectEnvDAL,
permissionService,
projectDAL,
kmsService
projectDAL
}: TWebhookServiceFactoryDep) => {
const createWebhook = async ({
actor,
@ -58,28 +56,33 @@ export const webhookServiceFactory = ({
const env = await projectEnvDAL.findOne({ projectId, slug: environment });
if (!env) throw new BadRequestError({ message: "Env not found" });
const { encryptor: secretManagerEncryptor } = await kmsService.createCipherPairWithDataKey({
projectId,
type: KmsDataKey.SecretManager
});
const encryptedSecretKeyWithKms = webhookSecretKey
? secretManagerEncryptor({
plainText: Buffer.from(webhookSecretKey)
}).cipherTextBlob
: null;
const encryptedUrl = secretManagerEncryptor({
plainText: Buffer.from(webhookUrl)
}).cipherTextBlob;
const webhook = await webhookDAL.create({
encryptedUrl,
encryptedSecretKeyWithKms,
const insertDoc: TWebhooksInsert = {
url: "", // deprecated - we are moving away from plaintext URLs
envId: env.id,
isDisabled: false,
secretPath: secretPath || "/",
type
});
};
if (webhookSecretKey) {
const { ciphertext, iv, tag, algorithm, encoding } = infisicalSymmetricEncypt(webhookSecretKey);
insertDoc.encryptedSecretKey = ciphertext;
insertDoc.iv = iv;
insertDoc.tag = tag;
insertDoc.algorithm = algorithm;
insertDoc.keyEncoding = encoding;
}
if (webhookUrl) {
const { ciphertext, iv, tag, algorithm, encoding } = infisicalSymmetricEncypt(webhookUrl);
insertDoc.urlCipherText = ciphertext;
insertDoc.urlIV = iv;
insertDoc.urlTag = tag;
insertDoc.algorithm = algorithm;
insertDoc.keyEncoding = encoding;
}
const webhook = await webhookDAL.create(insertDoc);
return { ...webhook, projectId, environment: env };
};
@ -133,18 +136,9 @@ export const webhookServiceFactory = ({
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.Webhooks);
let webhookError: string | undefined;
const { decryptor: kmsDataKeyDecryptor } = await kmsService.createCipherPairWithDataKey({
projectId: project.id,
type: KmsDataKey.SecretManager
});
const webhookUrl = kmsDataKeyDecryptor({ cipherTextBlob: webhook.encryptedUrl }).toString();
const webhookSecretKey = webhook.encryptedSecretKeyWithKms
? kmsDataKeyDecryptor({ cipherTextBlob: webhook.encryptedSecretKeyWithKms }).toString()
: undefined;
try {
await triggerWebhookRequest(
{ webhookUrl, webhookSecretKey },
webhook,
getWebhookPayload("test", {
workspaceName: project.name,
workspaceId: webhook.projectId,
@ -183,15 +177,11 @@ export const webhookServiceFactory = ({
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.Webhooks);
const webhooks = await webhookDAL.findAllWebhooks(projectId, environment, secretPath);
const { decryptor: kmsDataKeyDecryptor } = await kmsService.createCipherPairWithDataKey({
type: KmsDataKey.SecretManager,
projectId
});
return webhooks.map((w) => {
const decryptedUrl = kmsDataKeyDecryptor({ cipherTextBlob: w.encryptedUrl }).toString();
const { url } = decryptWebhookDetails(w);
return {
...w,
url: decryptedUrl
url
};
});
};

View File

@ -404,6 +404,10 @@ func CallGetRawSecretsV3(httpClient *resty.Client, request GetRawSecretsV3Reques
SetQueryParam("environment", request.Environment).
SetQueryParam("secretPath", request.SecretPath)
if request.TagSlugs != "" {
req.SetQueryParam("tagSlugs", request.TagSlugs)
}
if request.IncludeImport {
req.SetQueryParam("include_imports", "true")
}

View File

@ -574,6 +574,7 @@ type GetRawSecretsV3Request struct {
SecretPath string `json:"secretPath"`
IncludeImport bool `json:"include_imports"`
Recursive bool `json:"recursive"`
TagSlugs string `json:"tagSlugs,omitempty"`
}
type GetRawSecretsV3Response struct {

View File

@ -312,7 +312,7 @@ func ParseAgentConfig(configFile []byte) (*Config, error) {
func secretTemplateFunction(accessToken string, existingEtag string, currentEtag *string) func(string, string, string) ([]models.SingleEnvironmentVariable, error) {
return func(projectID, envSlug, secretPath string) ([]models.SingleEnvironmentVariable, error) {
res, err := util.GetPlainTextSecretsV3(accessToken, projectID, envSlug, secretPath, false, false)
res, err := util.GetPlainTextSecretsV3(accessToken, projectID, envSlug, secretPath, false, false, "")
if err != nil {
return nil, err
}

View File

@ -14,6 +14,7 @@ import (
"github.com/Infisical/infisical-merge/packages/util"
"github.com/rs/zerolog/log"
"github.com/spf13/cobra"
"gopkg.in/yaml.v2"
)
const (
@ -188,7 +189,7 @@ func formatEnvs(envs []models.SingleEnvironmentVariable, format string) (string,
case FormatCSV:
return formatAsCSV(envs), nil
case FormatYaml:
return formatAsYaml(envs), nil
return formatAsYaml(envs)
default:
return "", fmt.Errorf("invalid format type: %s. Available format types are [%s]", format, []string{FormatDotenv, FormatJson, FormatCSV, FormatYaml, FormatDotEnvExport})
}
@ -224,12 +225,18 @@ func formatAsDotEnvExport(envs []models.SingleEnvironmentVariable) string {
return dotenv
}
func formatAsYaml(envs []models.SingleEnvironmentVariable) string {
var dotenv string
func formatAsYaml(envs []models.SingleEnvironmentVariable) (string, error) {
m := make(map[string]string)
for _, env := range envs {
dotenv += fmt.Sprintf("%s: %s\n", env.Key, env.Value)
m[env.Key] = env.Value
}
return dotenv
yamlBytes, err := yaml.Marshal(m)
if err != nil {
return "", fmt.Errorf("failed to format environment variables as YAML: %w", err)
}
return string(yamlBytes), nil
}
// Format environment variables as a JSON file

View File

@ -0,0 +1,79 @@
package cmd
import (
"testing"
"github.com/Infisical/infisical-merge/packages/models"
"github.com/stretchr/testify/assert"
"gopkg.in/yaml.v2"
)
func TestFormatAsYaml(t *testing.T) {
tests := []struct {
name string
input []models.SingleEnvironmentVariable
expected string
}{
{
name: "Empty input",
input: []models.SingleEnvironmentVariable{},
expected: "{}\n",
},
{
name: "Single environment variable",
input: []models.SingleEnvironmentVariable{
{Key: "KEY1", Value: "VALUE1"},
},
expected: "KEY1: VALUE1\n",
},
{
name: "Multiple environment variables",
input: []models.SingleEnvironmentVariable{
{Key: "KEY1", Value: "VALUE1"},
{Key: "KEY2", Value: "VALUE2"},
{Key: "KEY3", Value: "VALUE3"},
},
expected: "KEY1: VALUE1\nKEY2: VALUE2\nKEY3: VALUE3\n",
},
{
name: "Overwriting duplicate keys",
input: []models.SingleEnvironmentVariable{
{Key: "KEY1", Value: "VALUE1"},
{Key: "KEY1", Value: "VALUE2"},
},
expected: "KEY1: VALUE2\n",
},
{
name: "Special characters in values",
input: []models.SingleEnvironmentVariable{
{Key: "KEY1", Value: "Value with spaces"},
{Key: "KEY2", Value: "Value:with:colons"},
{Key: "KEY3", Value: "Value\nwith\nnewlines"},
},
expected: "KEY1: Value with spaces\nKEY2: Value:with:colons\nKEY3: |-\n Value\n with\n newlines\n",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
result, err := formatAsYaml(tt.input)
assert.NoError(t, err)
// Compare the result with the expected output
assert.Equal(t, tt.expected, result)
// Additionally, parse the result back into a map to ensure it's valid YAML
var resultMap map[string]string
err = yaml.Unmarshal([]byte(result), &resultMap)
assert.NoError(t, err)
// Create an expected map from the input
expectedMap := make(map[string]string)
for _, env := range tt.input {
expectedMap[env.Key] = env.Value
}
assert.Equal(t, expectedMap, resultMap)
})
}
}

View File

@ -155,22 +155,24 @@ var secretsSetCmd = &cobra.Command{
DisableFlagsInUseLine: true,
Args: cobra.MinimumNArgs(1),
Run: func(cmd *cobra.Command, args []string) {
util.RequireLocalWorkspaceFile()
environmentName, _ := cmd.Flags().GetString("env")
if !cmd.Flags().Changed("env") {
environmentFromWorkspace := util.GetEnvFromWorkspaceFile()
if environmentFromWorkspace != "" {
environmentName = environmentFromWorkspace
}
}
token, err := util.GetInfisicalToken(cmd)
if err != nil {
util.HandleError(err, "Unable to parse flag")
}
projectId, err := cmd.Flags().GetString("projectId")
if (token == nil) {
util.RequireLocalWorkspaceFile()
}
environmentName, _ := cmd.Flags().GetString("env")
if !cmd.Flags().Changed("env") {
environmentFromWorkspace := util.GetEnvFromWorkspaceFile()
if environmentFromWorkspace != "" {
environmentName = environmentFromWorkspace
}
}
projectId, err := cmd.Flags().GetString("projectId")
if err != nil {
util.HandleError(err, "Unable to parse flag")
}
@ -374,6 +376,11 @@ func getSecretsByNames(cmd *cobra.Command, args []string) {
util.HandleError(err, "Unable to parse flag")
}
secretOverriding, err := cmd.Flags().GetBool("secret-overriding")
if err != nil {
util.HandleError(err, "Unable to parse flag")
}
request := models.GetAllSecretsParameters{
Environment: environmentName,
WorkspaceId: projectId,
@ -394,6 +401,12 @@ func getSecretsByNames(cmd *cobra.Command, args []string) {
util.HandleError(err, "To fetch all secrets")
}
if secretOverriding {
secrets = util.OverrideSecrets(secrets, util.SECRET_TYPE_PERSONAL)
} else {
secrets = util.OverrideSecrets(secrets, util.SECRET_TYPE_SHARED)
}
if shouldExpand {
authParams := models.ExpandSecretsAuthentication{}
if token != nil && token.Type == util.SERVICE_TOKEN_IDENTIFIER {
@ -413,11 +426,13 @@ func getSecretsByNames(cmd *cobra.Command, args []string) {
if value, ok := secretsMap[secretKeyFromArg]; ok {
requestedSecrets = append(requestedSecrets, value)
} else {
requestedSecrets = append(requestedSecrets, models.SingleEnvironmentVariable{
Key: secretKeyFromArg,
Type: "*not found*",
Value: "*not found*",
})
if !(plainOutput || showOnlyValue) {
requestedSecrets = append(requestedSecrets, models.SingleEnvironmentVariable{
Key: secretKeyFromArg,
Type: "*not found*",
Value: "*not found*",
})
}
}
}
@ -688,6 +703,7 @@ func init() {
secretsGetCmd.Flags().Bool("include-imports", true, "Imported linked secrets ")
secretsGetCmd.Flags().Bool("expand", true, "Parse shell parameter expansions in your secrets, and process your referenced secrets")
secretsGetCmd.Flags().Bool("recursive", false, "Fetch secrets from all sub-folders")
secretsGetCmd.Flags().Bool("secret-overriding", true, "Prioritizes personal secrets, if any, with the same name over shared secrets")
secretsCmd.AddCommand(secretsGetCmd)
secretsCmd.Flags().Bool("secret-overriding", true, "Prioritizes personal secrets, if any, with the same name over shared secrets")
secretsCmd.AddCommand(secretsSetCmd)

View File

@ -4,6 +4,7 @@ Copyright (c) 2023 Infisical Inc.
package cmd
import (
"encoding/base64"
"fmt"
"strings"
@ -13,13 +14,26 @@ import (
"github.com/spf13/cobra"
)
var AvailableVaultsAndDescriptions = []string{"auto (automatically select native vault on system)", "file (encrypted file vault)"}
var AvailableVaults = []string{"auto", "file"}
type VaultBackendType struct {
Name string
Description string
}
var AvailableVaults = []VaultBackendType{
{
Name: "auto",
Description: "automatically select the system keyring",
},
{
Name: "file",
Description: "encrypted file vault",
},
}
var vaultSetCmd = &cobra.Command{
Example: `infisical vault set pass`,
Use: "set [vault-name]",
Short: "Used to set the vault backend to store your login details securely at rest",
Example: `infisical vault set file`,
Use: "set [file|auto]",
Short: "Used to configure the vault backends",
DisableFlagsInUseLine: true,
Args: cobra.MinimumNArgs(1),
Run: func(cmd *cobra.Command, args []string) {
@ -35,15 +49,16 @@ var vaultSetCmd = &cobra.Command{
return
}
if wantedVaultTypeName == "auto" || wantedVaultTypeName == "file" {
if wantedVaultTypeName == util.VAULT_BACKEND_AUTO_MODE || wantedVaultTypeName == util.VAULT_BACKEND_FILE_MODE {
configFile, err := util.GetConfigFile()
if err != nil {
log.Error().Msgf("Unable to set vault to [%s] because of [err=%s]", wantedVaultTypeName, err)
return
}
configFile.VaultBackendType = wantedVaultTypeName // save selected vault
configFile.LoggedInUserEmail = "" // reset the logged in user to prompt them to re login
configFile.VaultBackendType = wantedVaultTypeName
configFile.LoggedInUserEmail = ""
configFile.VaultBackendPassphrase = base64.StdEncoding.EncodeToString([]byte(util.GenerateRandomString(10)))
err = util.WriteConfigFile(&configFile)
if err != nil {
@ -55,7 +70,11 @@ var vaultSetCmd = &cobra.Command{
Telemetry.CaptureEvent("cli-command:vault set", posthog.NewProperties().Set("currentVault", currentVaultBackend).Set("wantedVault", wantedVaultTypeName).Set("version", util.CLI_VERSION))
} else {
log.Error().Msgf("The requested vault type [%s] is not available on this system. Only the following vault backends are available for you system: %s", wantedVaultTypeName, strings.Join(AvailableVaults, ", "))
var availableVaultsNames []string
for _, vault := range AvailableVaults {
availableVaultsNames = append(availableVaultsNames, vault.Name)
}
log.Error().Msgf("The requested vault type [%s] is not available on this system. Only the following vault backends are available for you system: %s", wantedVaultTypeName, strings.Join(availableVaultsNames, ", "))
}
},
}
@ -73,8 +92,8 @@ var vaultCmd = &cobra.Command{
func printAvailableVaultBackends() {
fmt.Printf("Vaults are used to securely store your login details locally. Available vaults:")
for _, backend := range AvailableVaultsAndDescriptions {
fmt.Printf("\n- %s", backend)
for _, vaultType := range AvailableVaults {
fmt.Printf("\n- %s (%s)", vaultType.Name, vaultType.Description)
}
currentVaultBackend, err := util.GetCurrentVaultBackend()
@ -89,5 +108,6 @@ func printAvailableVaultBackends() {
func init() {
vaultCmd.AddCommand(vaultSetCmd)
rootCmd.AddCommand(vaultCmd)
}

View File

@ -11,10 +11,11 @@ type UserCredentials struct {
// The file struct for Infisical config file
type ConfigFile struct {
LoggedInUserEmail string `json:"loggedInUserEmail"`
LoggedInUserDomain string `json:"LoggedInUserDomain,omitempty"`
LoggedInUsers []LoggedInUser `json:"loggedInUsers,omitempty"`
VaultBackendType string `json:"vaultBackendType,omitempty"`
LoggedInUserEmail string `json:"loggedInUserEmail"`
LoggedInUserDomain string `json:"LoggedInUserDomain,omitempty"`
LoggedInUsers []LoggedInUser `json:"loggedInUsers,omitempty"`
VaultBackendType string `json:"vaultBackendType,omitempty"`
VaultBackendPassphrase string `json:"vaultBackendPassphrase,omitempty"`
}
type LoggedInUser struct {

View File

@ -1,6 +1,7 @@
package util
import (
"encoding/base64"
"encoding/json"
"errors"
"fmt"
@ -50,10 +51,11 @@ func WriteInitalConfig(userCredentials *models.UserCredentials) error {
}
configFile := models.ConfigFile{
LoggedInUserEmail: userCredentials.Email,
LoggedInUserDomain: config.INFISICAL_URL,
LoggedInUsers: existingConfigFile.LoggedInUsers,
VaultBackendType: existingConfigFile.VaultBackendType,
LoggedInUserEmail: userCredentials.Email,
LoggedInUserDomain: config.INFISICAL_URL,
LoggedInUsers: existingConfigFile.LoggedInUsers,
VaultBackendType: existingConfigFile.VaultBackendType,
VaultBackendPassphrase: existingConfigFile.VaultBackendPassphrase,
}
configFileMarshalled, err := json.Marshal(configFile)
@ -215,6 +217,14 @@ func GetConfigFile() (models.ConfigFile, error) {
return models.ConfigFile{}, err
}
if configFile.VaultBackendPassphrase != "" {
decodedPassphrase, err := base64.StdEncoding.DecodeString(configFile.VaultBackendPassphrase)
if err != nil {
return models.ConfigFile{}, fmt.Errorf("GetConfigFile: Unable to decode base64 passphrase [err=%s]", err)
}
os.Setenv("INFISICAL_VAULT_FILE_PASSPHRASE", string(decodedPassphrase))
}
return configFile, nil
}

View File

@ -8,6 +8,10 @@ const (
INFISICAL_WORKSPACE_CONFIG_FILE_NAME = ".infisical.json"
INFISICAL_TOKEN_NAME = "INFISICAL_TOKEN"
INFISICAL_UNIVERSAL_AUTH_ACCESS_TOKEN_NAME = "INFISICAL_UNIVERSAL_AUTH_ACCESS_TOKEN"
INFISICAL_VAULT_FILE_PASSPHRASE_ENV_NAME = "INFISICAL_VAULT_FILE_PASSPHRASE" // This works because we've forked the keyring package and added support for this env variable. This explains why you won't find any occurrences of it in the CLI codebase.
VAULT_BACKEND_AUTO_MODE = "auto"
VAULT_BACKEND_FILE_MODE = "file"
// Universal Auth
INFISICAL_UNIVERSAL_AUTH_CLIENT_ID_NAME = "INFISICAL_UNIVERSAL_AUTH_CLIENT_ID"
@ -34,7 +38,8 @@ const (
SERVICE_TOKEN_IDENTIFIER = "service-token"
UNIVERSAL_AUTH_TOKEN_IDENTIFIER = "universal-auth-token"
INFISICAL_BACKUP_SECRET = "infisical-backup-secrets"
INFISICAL_BACKUP_SECRET = "infisical-backup-secrets" // akhilmhdh: @depreciated remove in version v0.30
INFISICAL_BACKUP_SECRET_ENCRYPTION_KEY = "infisical-backup-secret-encryption-key"
)
var (

View File

@ -71,7 +71,7 @@ func GetCurrentLoggedInUserDetails() (LoggedInUserDetails, error) {
if strings.Contains(err.Error(), "credentials not found in system keyring") {
return LoggedInUserDetails{}, errors.New("we couldn't find your logged in details, try running [infisical login] then try again")
} else {
return LoggedInUserDetails{}, fmt.Errorf("failed to fetch creditnals from keyring because [err=%s]", err)
return LoggedInUserDetails{}, fmt.Errorf("failed to fetch credentials from keyring because [err=%s]", err)
}
}

View File

@ -5,6 +5,7 @@ import (
"crypto/sha256"
"encoding/base64"
"fmt"
"math/rand"
"os"
"os/exec"
"path"
@ -25,6 +26,8 @@ type DecodedSymmetricEncryptionDetails = struct {
Key []byte
}
const charset = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"
func GetBase64DecodedSymmetricEncryptionDetails(key string, cipher string, IV string, tag string) (DecodedSymmetricEncryptionDetails, error) {
cipherx, err := base64.StdEncoding.DecodeString(cipher)
if err != nil {
@ -287,3 +290,11 @@ func GetCmdFlagOrEnv(cmd *cobra.Command, flag, envName string) (string, error) {
}
return value, nil
}
func GenerateRandomString(length int) string {
b := make([]byte, length)
for i := range b {
b[i] = charset[rand.Intn(len(charset))]
}
return string(b)
}

View File

@ -1,6 +1,10 @@
package util
import (
"encoding/base64"
"fmt"
"github.com/rs/zerolog/log"
"github.com/zalando/go-keyring"
)
@ -20,16 +24,39 @@ func SetValueInKeyring(key, value string) error {
PrintErrorAndExit(1, err, "Unable to get current vault. Tip: run [infisical rest] then try again")
}
return keyring.Set(currentVaultBackend, MAIN_KEYRING_SERVICE, key, value)
err = keyring.Set(currentVaultBackend, MAIN_KEYRING_SERVICE, key, value)
if err != nil {
log.Debug().Msg(fmt.Sprintf("Error while setting default keyring: %v", err))
configFile, _ := GetConfigFile()
if configFile.VaultBackendPassphrase == "" {
encodedPassphrase := base64.StdEncoding.EncodeToString([]byte(GenerateRandomString(10))) // generate random passphrase
configFile.VaultBackendPassphrase = encodedPassphrase
configFile.VaultBackendType = VAULT_BACKEND_FILE_MODE
err = WriteConfigFile(&configFile)
if err != nil {
return err
}
// We call this function at last to trigger the environment variable to be set
GetConfigFile()
}
err = keyring.Set(VAULT_BACKEND_FILE_MODE, MAIN_KEYRING_SERVICE, key, value)
log.Debug().Msg(fmt.Sprintf("Error while setting file keyring: %v", err))
}
return err
}
func GetValueInKeyring(key string) (string, error) {
currentVaultBackend, err := GetCurrentVaultBackend()
if err != nil {
PrintErrorAndExit(1, err, "Unable to get current vault. Tip: run [infisical rest] then try again")
PrintErrorAndExit(1, err, "Unable to get current vault. Tip: run [infisical reset] then try again")
}
return keyring.Get(currentVaultBackend, MAIN_KEYRING_SERVICE, key)
}
func DeleteValueInKeyring(key string) error {

View File

@ -1,14 +1,15 @@
package util
import (
"crypto/rand"
"encoding/base64"
"encoding/hex"
"encoding/json"
"errors"
"fmt"
"os"
"path"
"regexp"
"slices"
"strings"
"unicode"
@ -20,7 +21,7 @@ import (
"github.com/zalando/go-keyring"
)
func GetPlainTextSecretsViaServiceToken(fullServiceToken string, environment string, secretPath string, includeImports bool, recursive bool) ([]models.SingleEnvironmentVariable, error) {
func GetPlainTextSecretsViaServiceToken(fullServiceToken string, environment string, secretPath string, includeImports bool, recursive bool, tagSlugs string) ([]models.SingleEnvironmentVariable, error) {
serviceTokenParts := strings.SplitN(fullServiceToken, ".", 4)
if len(serviceTokenParts) < 4 {
return nil, fmt.Errorf("invalid service token entered. Please double check your service token and try again")
@ -53,6 +54,7 @@ func GetPlainTextSecretsViaServiceToken(fullServiceToken string, environment str
SecretPath: secretPath,
IncludeImport: includeImports,
Recursive: recursive,
TagSlugs: tagSlugs,
})
if err != nil {
@ -76,7 +78,7 @@ func GetPlainTextSecretsViaServiceToken(fullServiceToken string, environment str
}
func GetPlainTextSecretsV3(accessToken string, workspaceId string, environmentName string, secretsPath string, includeImports bool, recursive bool) (models.PlaintextSecretResult, error) {
func GetPlainTextSecretsV3(accessToken string, workspaceId string, environmentName string, secretsPath string, includeImports bool, recursive bool, tagSlugs string) (models.PlaintextSecretResult, error) {
httpClient := resty.New()
httpClient.SetAuthToken(accessToken).
SetHeader("Accept", "application/json")
@ -86,7 +88,7 @@ func GetPlainTextSecretsV3(accessToken string, workspaceId string, environmentNa
Environment: environmentName,
IncludeImport: includeImports,
Recursive: recursive,
// TagSlugs: tagSlugs,
TagSlugs: tagSlugs,
}
if secretsPath != "" {
@ -281,29 +283,36 @@ func GetAllEnvironmentVariables(params models.GetAllSecretsParameters, projectCo
}
res, err := GetPlainTextSecretsV3(loggedInUserDetails.UserCredentials.JTWToken, infisicalDotJson.WorkspaceId,
params.Environment, params.SecretsPath, params.IncludeImport, params.Recursive)
params.Environment, params.SecretsPath, params.IncludeImport, params.Recursive, params.TagSlugs)
log.Debug().Msgf("GetAllEnvironmentVariables: Trying to fetch secrets JTW token [err=%s]", err)
if err == nil {
WriteBackupSecrets(infisicalDotJson.WorkspaceId, params.Environment, params.SecretsPath, res.Secrets)
backupEncryptionKey, err := GetBackupEncryptionKey()
if err != nil {
return nil, err
}
WriteBackupSecrets(infisicalDotJson.WorkspaceId, params.Environment, params.SecretsPath, backupEncryptionKey, res.Secrets)
}
secretsToReturn = res.Secrets
errorToReturn = err
// only attempt to serve cached secrets if no internet connection and if at least one secret cached
if !isConnected {
backedSecrets, err := ReadBackupSecrets(infisicalDotJson.WorkspaceId, params.Environment, params.SecretsPath)
if len(backedSecrets) > 0 {
PrintWarning("Unable to fetch latest secret(s) due to connection error, serving secrets from last successful fetch. For more info, run with --debug")
secretsToReturn = backedSecrets
errorToReturn = err
backupEncryptionKey, _ := GetBackupEncryptionKey()
if backupEncryptionKey != nil {
backedUpSecrets, err := ReadBackupSecrets(infisicalDotJson.WorkspaceId, params.Environment, params.SecretsPath, backupEncryptionKey)
if len(backedUpSecrets) > 0 {
PrintWarning("Unable to fetch the latest secret(s) due to connection error, serving secrets from last successful fetch. For more info, run with --debug")
secretsToReturn = backedUpSecrets
errorToReturn = err
}
}
}
} else {
if params.InfisicalToken != "" {
log.Debug().Msg("Trying to fetch secrets using service token")
secretsToReturn, errorToReturn = GetPlainTextSecretsViaServiceToken(params.InfisicalToken, params.Environment, params.SecretsPath, params.IncludeImport, params.Recursive)
secretsToReturn, errorToReturn = GetPlainTextSecretsViaServiceToken(params.InfisicalToken, params.Environment, params.SecretsPath, params.IncludeImport, params.Recursive, params.TagSlugs)
} else if params.UniversalAuthAccessToken != "" {
if params.WorkspaceId == "" {
@ -311,7 +320,7 @@ func GetAllEnvironmentVariables(params models.GetAllSecretsParameters, projectCo
}
log.Debug().Msg("Trying to fetch secrets using universal auth")
res, err := GetPlainTextSecretsV3(params.UniversalAuthAccessToken, params.WorkspaceId, params.Environment, params.SecretsPath, params.IncludeImport, params.Recursive)
res, err := GetPlainTextSecretsV3(params.UniversalAuthAccessToken, params.WorkspaceId, params.Environment, params.SecretsPath, params.IncludeImport, params.Recursive, params.TagSlugs)
errorToReturn = err
secretsToReturn = res.Secrets
@ -476,71 +485,99 @@ func OverrideSecrets(secrets []models.SingleEnvironmentVariable, secretType stri
return secretsToReturn
}
func WriteBackupSecrets(workspace string, environment string, secretsPath string, secrets []models.SingleEnvironmentVariable) error {
var backedUpSecrets []models.BackupSecretKeyRing
secretValueInKeyRing, err := GetValueInKeyring(INFISICAL_BACKUP_SECRET)
func GetBackupEncryptionKey() ([]byte, error) {
encryptionKey, err := GetValueInKeyring(INFISICAL_BACKUP_SECRET_ENCRYPTION_KEY)
if err != nil {
if err == keyring.ErrUnsupportedPlatform {
return errors.New("your OS does not support keyring. Consider using a service token https://infisical.com/docs/documentation/platform/token")
} else if err != keyring.ErrNotFound {
return fmt.Errorf("something went wrong, failed to retrieve value from system keyring [error=%v]", err)
return nil, errors.New("your OS does not support keyring. Consider using a service token https://infisical.com/docs/documentation/platform/token")
} else if err == keyring.ErrNotFound {
// generate a new key
randomizedKey := make([]byte, 16)
rand.Read(randomizedKey)
encryptionKey = hex.EncodeToString(randomizedKey)
if err := SetValueInKeyring(INFISICAL_BACKUP_SECRET_ENCRYPTION_KEY, encryptionKey); err != nil {
return nil, err
}
return []byte(encryptionKey), nil
} else {
return nil, fmt.Errorf("something went wrong, failed to retrieve value from system keyring [error=%v]", err)
}
}
_ = json.Unmarshal([]byte(secretValueInKeyRing), &backedUpSecrets)
return []byte(encryptionKey), nil
}
backedUpSecrets = slices.DeleteFunc(backedUpSecrets, func(e models.BackupSecretKeyRing) bool {
return e.SecretPath == secretsPath && e.ProjectID == workspace && e.Environment == environment
})
newBackupSecret := models.BackupSecretKeyRing{
ProjectID: workspace,
Environment: environment,
SecretPath: secretsPath,
Secrets: secrets,
}
backedUpSecrets = append(backedUpSecrets, newBackupSecret)
func WriteBackupSecrets(workspace string, environment string, secretsPath string, encryptionKey []byte, secrets []models.SingleEnvironmentVariable) error {
formattedPath := strings.ReplaceAll(secretsPath, "/", "-")
fileName := fmt.Sprintf("project_secrets_%s_%s_%s.json", workspace, environment, formattedPath)
secrets_backup_folder_name := "secrets-backup"
listOfSecretsMarshalled, err := json.Marshal(backedUpSecrets)
_, fullConfigFileDirPath, err := GetFullConfigFilePath()
if err != nil {
return err
return fmt.Errorf("WriteBackupSecrets: unable to get full config folder path [err=%s]", err)
}
err = SetValueInKeyring(INFISICAL_BACKUP_SECRET, string(listOfSecretsMarshalled))
// create secrets backup directory
fullPathToSecretsBackupFolder := fmt.Sprintf("%s/%s", fullConfigFileDirPath, secrets_backup_folder_name)
if _, err := os.Stat(fullPathToSecretsBackupFolder); errors.Is(err, os.ErrNotExist) {
err := os.Mkdir(fullPathToSecretsBackupFolder, os.ModePerm)
if err != nil {
return err
}
}
marshaledSecrets, _ := json.Marshal(secrets)
result, err := crypto.EncryptSymmetric(marshaledSecrets, encryptionKey)
if err != nil {
return fmt.Errorf("StoreUserCredsInKeyRing: unable to store user credentials because [err=%s]", err)
return fmt.Errorf("WriteBackupSecrets: Unable to encrypt local secret backup to file [err=%s]", err)
}
listOfSecretsMarshalled, _ := json.Marshal(result)
err = os.WriteFile(fmt.Sprintf("%s/%s", fullPathToSecretsBackupFolder, fileName), listOfSecretsMarshalled, 0600)
if err != nil {
return fmt.Errorf("WriteBackupSecrets: Unable to write backup secrets to file [err=%s]", err)
}
return nil
}
func ReadBackupSecrets(workspace string, environment string, secretsPath string) ([]models.SingleEnvironmentVariable, error) {
secretValueInKeyRing, err := GetValueInKeyring(INFISICAL_BACKUP_SECRET)
func ReadBackupSecrets(workspace string, environment string, secretsPath string, encryptionKey []byte) ([]models.SingleEnvironmentVariable, error) {
formattedPath := strings.ReplaceAll(secretsPath, "/", "-")
fileName := fmt.Sprintf("project_secrets_%s_%s_%s.json", workspace, environment, formattedPath)
secrets_backup_folder_name := "secrets-backup"
_, fullConfigFileDirPath, err := GetFullConfigFilePath()
if err != nil {
if err == keyring.ErrUnsupportedPlatform {
return nil, errors.New("your OS does not support keyring. Consider using a service token https://infisical.com/docs/documentation/platform/token")
} else if err == keyring.ErrNotFound {
return nil, errors.New("credentials not found in system keyring")
} else {
return nil, fmt.Errorf("something went wrong, failed to retrieve value from system keyring [error=%v]", err)
}
return nil, fmt.Errorf("ReadBackupSecrets: unable to write config file because an error occurred when getting config file path [err=%s]", err)
}
var backedUpSecrets []models.BackupSecretKeyRing
err = json.Unmarshal([]byte(secretValueInKeyRing), &backedUpSecrets)
fullPathToSecretsBackupFolder := fmt.Sprintf("%s/%s", fullConfigFileDirPath, secrets_backup_folder_name)
if _, err := os.Stat(fullPathToSecretsBackupFolder); errors.Is(err, os.ErrNotExist) {
return nil, nil
}
encryptedBackupSecretsFilePath := fmt.Sprintf("%s/%s", fullPathToSecretsBackupFolder, fileName)
encryptedBackupSecretsAsBytes, err := os.ReadFile(encryptedBackupSecretsFilePath)
if err != nil {
return nil, fmt.Errorf("getUserCredsFromKeyRing: Something went wrong when unmarshalling user creds [err=%s]", err)
return nil, err
}
for _, backupSecret := range backedUpSecrets {
if backupSecret.Environment == environment && backupSecret.ProjectID == workspace && backupSecret.SecretPath == secretsPath {
return backupSecret.Secrets, nil
}
var encryptedBackUpSecrets models.SymmetricEncryptionResult
err = json.Unmarshal(encryptedBackupSecretsAsBytes, &encryptedBackUpSecrets)
if err != nil {
return nil, fmt.Errorf("ReadBackupSecrets: unable to parse encrypted backup secrets. The secrets backup may be malformed [err=%s]", err)
}
return nil, nil
result, err := crypto.DecryptSymmetric(encryptionKey, encryptedBackUpSecrets.CipherText, encryptedBackUpSecrets.AuthTag, encryptedBackUpSecrets.Nonce)
if err != nil {
return nil, fmt.Errorf("ReadBackupSecrets: unable to decrypt encrypted backup secrets [err=%s]", err)
}
var plainTextSecrets []models.SingleEnvironmentVariable
_ = json.Unmarshal(result, &plainTextSecrets)
return plainTextSecrets, nil
}
func DeleteBackupSecrets() error {
// keeping this logic for now. Need to remove it later as more users migrate keyring would be used and this folder will be removed completely by then
secrets_backup_folder_name := "secrets-backup"
_, fullConfigFileDirPath, err := GetFullConfigFilePath()
@ -549,8 +586,8 @@ func DeleteBackupSecrets() error {
}
fullPathToSecretsBackupFolder := fmt.Sprintf("%s/%s", fullConfigFileDirPath, secrets_backup_folder_name)
DeleteValueInKeyring(INFISICAL_BACKUP_SECRET)
DeleteValueInKeyring(INFISICAL_BACKUP_SECRET_ENCRYPTION_KEY)
return os.RemoveAll(fullPathToSecretsBackupFolder)
}

View File

@ -11,11 +11,11 @@ func GetCurrentVaultBackend() (string, error) {
}
if configFile.VaultBackendType == "" {
return "auto", nil
return VAULT_BACKEND_AUTO_MODE, nil
}
if configFile.VaultBackendType != "auto" && configFile.VaultBackendType != "file" {
return "auto", nil
if configFile.VaultBackendType != VAULT_BACKEND_AUTO_MODE && configFile.VaultBackendType != VAULT_BACKEND_FILE_MODE {
return VAULT_BACKEND_AUTO_MODE, nil
}
return configFile.VaultBackendType, nil

View File

@ -1,4 +1,4 @@
Warning: Unable to fetch latest secret(s) due to connection error, serving secrets from last successful fetch. For more info, run with --debug
Warning: Unable to fetch the latest secret(s) due to connection error, serving secrets from last successful fetch. For more info, run with --debug
┌───────────────┬──────────────┬─────────────┐
│ SECRET NAME │ SECRET VALUE │ SECRET TYPE │
├───────────────┼──────────────┼─────────────┤

View File

@ -7,7 +7,6 @@ import (
"github.com/bradleyjkemp/cupaloy/v2"
)
func TestServiceToken_SecretsGetWithImportsAndRecursiveCmd(t *testing.T) {
output, err := ExecuteCliCommand(FORMATTED_CLI_NAME, "secrets", "--token", creds.ServiceToken, "--projectId", creds.ProjectID, "--env", creds.EnvSlug, "--recursive", "--silent")
@ -94,7 +93,7 @@ func TestUserAuth_SecretsGetAll(t *testing.T) {
}
// explicitly called here because it should happen directly after successful secretsGetAll
testUserAuth_SecretsGetAllWithoutConnection(t)
// testUserAuth_SecretsGetAllWithoutConnection(t)
}
func testUserAuth_SecretsGetAllWithoutConnection(t *testing.T) {
@ -107,7 +106,7 @@ func testUserAuth_SecretsGetAllWithoutConnection(t *testing.T) {
// set it to a URL that will always be unreachable
newConfigFile.LoggedInUserDomain = "http://localhost:4999"
util.WriteConfigFile(&newConfigFile)
// restore config file
defer util.WriteConfigFile(&originalConfigFile)
@ -121,4 +120,4 @@ func testUserAuth_SecretsGetAllWithoutConnection(t *testing.T) {
if err != nil {
t.Fatalf("snapshot failed: %v", err)
}
}
}

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