Compare commits

...

298 Commits

Author SHA1 Message Date
Maidul Islam
9128461409 Merge pull request #1658 from Infisical/daniel/delete-duplicate-org-memberships-migration
Feat: Delete duplicate memberships migration
2024-04-04 19:19:39 -07:00
Daniel Hougaard
893235c40f Update 20240405000045_org-memberships-unique-constraint.ts 2024-04-04 18:43:32 -07:00
BlackMagiq
e0f655ae30 Merge pull request #1656 from Infisical/fix/duplicate-org-memberships
Fix: Duplicate organization memberships
2024-04-04 17:10:55 -07:00
Daniel Hougaard
93aeca3a38 Fix: Add unique constraint on orgId and userId 2024-04-04 17:04:23 -07:00
Daniel Hougaard
1edebdf8a5 Fix: Improve create migration script 2024-04-04 17:04:06 -07:00
BlackMagiq
1017707642 Merge pull request #1655 from Infisical/project-limit
Remove plan cache upon create/delete project
2024-04-04 13:21:32 -07:00
Tuan Dang
5639306303 Remove plan cache upon create/delete project 2024-04-04 13:17:46 -07:00
BlackMagiq
72f50ec399 Merge pull request #1654 from Infisical/fix-additional-privilege-slug
Move default slug init for users/identities out of fastify schema
2024-04-04 12:22:31 -07:00
Tuan Dang
effc7a3627 Move default slug init for users/identities out of fastify schema 2024-04-04 12:18:10 -07:00
Maidul Islam
510c91cef1 Update infisical-agent.mdx 2024-04-04 09:34:31 -07:00
Vladyslav Matsiiako
9be5d89fcf added docs images 2024-04-03 22:55:47 -07:00
Vladyslav Matsiiako
94f4497903 update access request docs 2024-04-03 22:51:48 -07:00
BlackMagiq
b5af5646ee Merge pull request #1653 from Infisical/pentest
Add separate rate limit to invite user to org
2024-04-03 18:48:24 -07:00
Tuan Dang
1554618167 Add separate rate limit to invite user 2024-04-03 18:46:29 -07:00
Maidul Islam
5fbfcdda30 Merge pull request #1651 from Infisical/daniel/cli-secrets-get-fix
Fix: CLI get secrets by name
2024-04-03 10:43:42 -07:00
Daniel Hougaard
cdbb3b9c47 Update secrets.go 2024-04-03 10:36:25 -07:00
Vladyslav Matsiiako
0042a95b21 update docs image 2024-04-03 09:08:53 -07:00
BlackMagiq
53233e05d4 Merge pull request #1648 from Infisical/keycloak
Add documentation + option for Keycloak SAML (self-hosted)
2024-04-02 16:42:41 -07:00
Tuan Dang
4f15f9c8d3 Add support for keycloak saml on self-hosted infisical 2024-04-02 16:35:37 -07:00
Maidul Islam
97223fabe6 Merge pull request #1617 from Infisical/daniel/improve-create-project
Feat: Recursively get all secrets from all folders in specified path
2024-04-02 13:50:16 -07:00
Maidul Islam
04b312cbe4 Merge pull request #1646 from akhilmhdh/fix/disable-role-button
fix(ui): resolved multi role modal button hiding clickable
2024-04-02 13:06:57 -07:00
Maidul Islam
97e5069cf5 Merge pull request #1645 from akhilmhdh/chore/specific-privilege-api-doc
docs: added api reference for specific privilege identity
2024-04-02 12:03:48 -07:00
Akhil Mohan
93146fcd96 fix(ui): resolved multi role modal button hiding clickable 2024-04-03 00:12:34 +05:30
Akhil Mohan
87d98de4c1 docs: added api reference for specific privilege identity 2024-04-02 23:54:51 +05:30
Maidul Islam
26f647b948 Merge pull request #1644 from akhilmhdh/chore/aws-ssm-api
AWS SSM integration api documentation
2024-04-02 09:22:50 -07:00
Maidul Islam
80b3cdd128 add examples to integration auth docs 2024-04-02 09:21:32 -07:00
Maidul Islam
8dd85a0d65 Update requirements.mdx 2024-04-02 07:07:36 -07:00
Akhil Mohan
17995d301a feat(doc): added integration and integration auth to api reference doc 2024-04-02 16:30:53 +05:30
Akhil Mohan
094b48a2b1 feat(server): updated integration and integration auth with description 2024-04-02 16:29:41 +05:30
Maidul Islam
7b8bfe38f0 Merge pull request #1574 from akhilmhdh/feat/additional-privilege
feat: additional privilege for users and identity
2024-04-01 11:09:05 -07:00
Akhil Mohan
9903f7c4a0 feat: fixed wrong permission type in bulk api op 2024-04-01 23:34:25 +05:30
Akhil Mohan
42cd98d4d9 feat: changed update patch function to privilegeDetails for identity privilege 2024-04-01 23:13:08 +05:30
vmatsiiako
4b203e9ad3 Update postgresql.mdx 2024-04-01 10:26:25 -07:00
Daniel Hougaard
36bf1b2abc Fix: Renamed deep parameter to recursive 2024-04-01 10:10:49 -07:00
Daniel Hougaard
42fb732955 Fix: Renamed deep parameter to recursive 2024-04-01 10:10:34 -07:00
Daniel Hougaard
da2dcb347a Fix: Restructured recursive path functions as suggested by Akhil 2024-04-01 09:58:13 -07:00
Daniel Hougaard
b9482966cf Fix: Replaced merge with extend as proposed by Akhil 2024-04-01 09:52:49 -07:00
Vladyslav Matsiiako
1e4b4591ed fix images in docs 2024-04-01 09:15:04 -07:00
Vladyslav Matsiiako
4a325d6d96 fix image links 2024-04-01 00:08:20 -07:00
Vladyslav Matsiiako
5e20573110 fix docs eyebrow 2024-03-31 23:44:35 -07:00
Vladyslav Matsiiako
f623c8159d documentation revamp 2024-03-31 23:37:57 -07:00
Maidul Islam
4323407da7 Update introduction.mdx 2024-03-30 08:43:10 -04:00
Daniel Hougaard
4c496d5e3d Update secret-service.ts 2024-03-30 08:40:43 +01:00
Vladyslav Matsiiako
d68dc4c3e0 add type=password to integration api keys 2024-03-29 16:50:18 -07:00
Vladyslav Matsiiako
e64c579dfd update aws sm docs 2024-03-29 16:39:51 -07:00
Akhil Mohan
d0c0d5835c feat: splitted privilege create route into two for permanent and temp to get params shape in api doc 2024-03-30 01:31:17 +05:30
Akhil Mohan
af2dcdd0c7 feat: updated api description and changed slug to privilege slug 2024-03-29 23:51:26 +05:30
vmatsiiako
6c628a7265 Update aws-secret-manager.mdx 2024-03-29 10:49:10 -07:00
Akhil Mohan
00f2d40803 feat(ui): changed back to relative time distance with tooltip of detailed time 2024-03-29 22:36:46 +05:30
Vladyslav Matsiiako
0a66cbe729 updated docs 2024-03-29 00:10:29 -07:00
Vladyslav Matsiiako
7fec7c9bf5 update docs 2024-03-28 23:11:16 -07:00
Vladyslav Matsiiako
d1afec4f9a inject secrets from secret imports into integrations 2024-03-28 20:13:46 -07:00
Vladyslav Matsiiako
31ad6b0c86 update style 2024-03-28 18:17:59 -07:00
Akhil Mohan
e46256f45b feat: added description for all api endpoints 2024-03-28 19:55:12 +05:30
Akhil Mohan
64e868a151 feat(ui): updated ui with identity privilege hooks and new role form 2024-03-28 19:55:12 +05:30
Akhil Mohan
c8cbcaf10c feat(server): added identity privilege route changes with project slug 2024-03-28 19:55:12 +05:30
Akhil Mohan
51716336c2 feat(ui): updated ui with new role form for users 2024-03-28 19:55:12 +05:30
Akhil Mohan
6b51c7269a feat(server): removed name and description and fixed api for user privileges 2024-03-28 19:55:12 +05:30
Akhil Mohan
f551a4158d feat: resolved upstream rebase conflict 2024-03-28 19:55:12 +05:30
Vladyslav Matsiiako
e850b82fb3 improved admin dashboard UI 2024-03-28 19:55:12 +05:30
Akhil Mohan
8f85f292db feat: improved slug with a default generator for ui and server 2024-03-28 19:55:12 +05:30
Akhil Mohan
5f84de039f feat(ui): finished ui for identity additional privilege 2024-03-28 19:55:12 +05:30
Akhil Mohan
8529fac098 feat(server): completed identity additional privilege 2024-03-28 19:55:12 +05:30
Akhil Mohan
81cf19cb4a feat(ui): completed ui for user additional privilege 2024-03-28 19:54:00 +05:30
Akhil Mohan
edbe1c8eae feat(ui): hook for new user additional privilege 2024-03-28 19:54:00 +05:30
Akhil Mohan
a5039494cd feat(server): completed routes for user additional privilege 2024-03-28 19:54:00 +05:30
Akhil Mohan
a908471e66 feat(server): completed user additional privilege services 2024-03-28 19:54:00 +05:30
Akhil Mohan
84204c3c37 feat(server): added new user additional migration and schemas 2024-03-28 19:54:00 +05:30
Vladyslav Matsiiako
4931e8579c fix image link 2024-03-27 23:13:07 -07:00
Maidul Islam
20dc243fd9 Merge pull request #1634 from Infisical/maintenanceMode
add maintenance mode
2024-03-27 21:23:51 -04:00
Maidul Islam
785a1389d9 add maintenance mode 2024-03-27 21:19:21 -04:00
Maidul Islam
5a3fc3568a fix typo for maintenance 2024-03-27 18:55:27 -04:00
Maidul Islam
497601e398 Update overview.mdx 2024-03-27 15:59:04 -04:00
Maidul Islam
8db019d2fe update dynamic secret doc 2024-03-27 13:53:41 -04:00
Maidul Islam
07d1d91110 Merge pull request #1631 from akhilmhdh/fix/dyn-superuser-remove
fix(server): resolved failing to use dynamic secret due to superuser
2024-03-27 11:19:51 -04:00
Maidul Islam
bb506fff9f remove assign statement 2024-03-27 11:11:10 -04:00
Akhil Mohan
7a561bcbdf feat(server): moved dynamic secret to ee 2024-03-27 15:00:16 +05:30
Akhil Mohan
8784f80fc1 fix(ui): updated error message on create dynamic secret 2024-03-27 14:25:56 +05:30
Akhil Mohan
0793e70c26 fix(server): resolved failing to use dynamic secret due to superuser 2024-03-27 14:25:39 +05:30
Vladyslav Matsiiako
99f8799ff4 Merge branch 'main' of https://github.com/Infisical/infisical 2024-03-26 22:55:57 -07:00
Vladyslav Matsiiako
3f05c8b7ae updated dynamic secrets docs 2024-03-26 22:55:47 -07:00
Maidul Islam
6bd624a0f6 fix dynamic secret config edit 2024-03-26 22:33:55 -04:00
Vladyslav Matsiiako
4a11096ea8 update dynamic secrets docs 2024-03-26 18:58:02 -07:00
Vladyslav Matsiiako
1589eb8e03 fix link typo 2024-03-26 18:56:14 -07:00
Vladyslav Matsiiako
b370d6e415 fix link typos 2024-03-26 18:37:00 -07:00
Vladyslav Matsiiako
65937d6a17 update docs and fix typos 2024-03-26 18:26:18 -07:00
Maidul Islam
d20bc1b38a turn paywall on dynamic secret 2024-03-26 17:53:47 -04:00
Maidul Islam
882ad8729c Merge pull request #1629 from Infisical/dynamic-1
allow viewer to generate and list dynamic secret
2024-03-26 17:51:26 -04:00
Maidul Islam
0fdf5032f9 allow viewer to create and list dynamic secret 2024-03-26 17:50:06 -04:00
Maidul Islam
75d9463ceb Merge pull request #1628 from Infisical/maintenanceApril2024
add maintenance notice
2024-03-26 15:41:40 -04:00
Maidul Islam
e258b84796 move to weekend 2024-03-26 15:37:24 -04:00
Maidul Islam
1ab6b21b25 add maintenance notice for april 2024-03-26 15:22:55 -04:00
Maidul Islam
775037539e update docs to include HA for infisical cloud 2024-03-26 14:48:41 -04:00
Daniel Hougaard
4f05e4ce93 Fix test case 2024-03-26 19:41:48 +01:00
Daniel Hougaard
2e8680c5d4 Update secret-service.ts 2024-03-26 19:36:18 +01:00
Daniel Hougaard
e5136c9ef5 Feat: Recursively get all secrets 2024-03-26 19:36:18 +01:00
Daniel Hougaard
812fe5cf31 Feat: Recursively get all secrets 2024-03-26 19:36:18 +01:00
Daniel Hougaard
50082e192c Feat: Recursively get all secrets, findByFolderIds DLA 2024-03-26 19:35:45 +01:00
Daniel Hougaard
1e1b5d655e Fix: Refactored secret fetching to be more performant 2024-03-26 19:35:45 +01:00
Daniel Hougaard
3befd90723 Fix: Refactor to in-memory approach 2024-03-26 19:35:45 +01:00
Daniel Hougaard
88549f4030 Feat: Deep search support 2024-03-26 19:35:45 +01:00
Daniel Hougaard
46a638cc63 FIx: Rename parameter from recursive to deep 2024-03-26 19:35:45 +01:00
Daniel Hougaard
566f7e4c61 Feat: Recursively get all secrets from inside path 2024-03-26 19:35:45 +01:00
Daniel Hougaard
9ff3210ed6 Feat: Recursively get all secrets from inside path 2024-03-26 19:35:12 +01:00
Daniel Hougaard
f91a6683c2 Fix: Rename parameter 2024-03-26 19:35:12 +01:00
Daniel Hougaard
c29cb667d7 Feat: Recursively get secrets from all nested secret paths 2024-03-26 19:35:12 +01:00
Maidul Islam
7c623562e1 Merge pull request #1627 from akhilmhdh/feat/dyn-sec-overview
feat: added dynamic secret to overview page
2024-03-26 14:14:02 -04:00
Akhil Mohan
aef8d79101 feat: added dynamic secret to overview page 2024-03-26 23:40:41 +05:30
Maidul Islam
d735ec71b8 Merge pull request #1626 from jacobwgillespie/optimize-docker-builds
chore(ci): optimize Depot build workflows
2024-03-26 12:36:15 -04:00
Jacob Gillespie
84651d473b chore(ci): optimize Depot build workflows 2024-03-26 16:22:07 +00:00
Daniel Hougaard
9501386882 Merge pull request #1625 from Infisical/daniel/missing-changes
Fix: Minor naming changes
2024-03-26 17:11:03 +01:00
Daniel Hougaard
d11f958443 Requested changes 2024-03-26 17:07:25 +01:00
Maidul Islam
087a4bb7d2 Merge pull request #1624 from akhilmhdh/feat/ui-notification-errors
New toast notification for ui
2024-03-26 11:41:10 -04:00
Maidul Islam
750210e6c3 update doc item title 2024-03-26 11:22:11 -04:00
Maidul Islam
90cf4e9137 update license docs 2024-03-26 11:21:40 -04:00
Maidul Islam
17bb2e8a7d set dynamic secret to true 2024-03-26 10:26:22 -04:00
Akhil Mohan
b912cd585c feat(ui): resolved dynamic secret merge conflict 2024-03-26 19:45:52 +05:30
Akhil Mohan
282434de8e feat(ui): changed to a better toast for ui and a global error handler for all server error messages 2024-03-26 19:37:13 +05:30
Maidul Islam
1f939a5e58 Merge pull request #1611 from akhilmhdh/feat/pg-dynamic-secret
Dynamic secret #Postgres
2024-03-26 09:57:31 -04:00
Maidul Islam
ac0f5369de inject license server and fix wording 2024-03-26 09:53:43 -04:00
Maidul Islam
6eba64c975 fix merge 2024-03-26 09:41:32 -04:00
Maidul Islam
12515c1866 Merge branch 'main' into feat/pg-dynamic-secret 2024-03-26 09:30:30 -04:00
Akhil Mohan
c882da2e1a feat: added license check for dynamic secret 2024-03-26 14:35:19 +05:30
Akhil Mohan
8a7774f9ac feat(ui): updated api changes made 2024-03-26 14:10:25 +05:30
Akhil Mohan
a7d2ec80c6 feat(server): updated dynamic secret names from feedback, added describe and fixed login not working 2024-03-26 13:18:31 +05:30
Maidul Islam
494543ec53 Delete .github/workflows/build-staging-and-deploy.yml 2024-03-26 00:01:39 -04:00
Maidul Islam
b7b875b6a7 add prod creds to pipeline 2024-03-25 21:23:53 -04:00
Maidul Islam
3ddd06a3d1 Revert "Update build-staging-and-deploy-aws.yml"
This reverts commit a1a8364cd1.
2024-03-25 21:16:33 -04:00
Maidul Islam
a1a8364cd1 Update build-staging-and-deploy-aws.yml 2024-03-25 21:12:50 -04:00
Daniel Hougaard
3e51fcb546 Merge pull request #1623 from Infisical/daniel/secret-tags-docs
Feat: Standalone tag attaching & detaching
2024-03-26 01:14:00 +01:00
Daniel Hougaard
c52a16cc47 Update constants.ts 2024-03-25 21:56:53 +01:00
Daniel Hougaard
f91c77baa3 Docs: Attach / Detach tags 2024-03-25 21:53:39 +01:00
Daniel Hougaard
e7c2f6f88c Docs: Expose tags endpoints 2024-03-25 21:48:54 +01:00
Daniel Hougaard
f7c2d38aef Feat: (Standalone) Attach / Detach tags from secrets 2024-03-25 21:48:14 +01:00
Daniel Hougaard
cfb497dd58 Feat: Get secret tags by secret ID 2024-03-25 21:47:23 +01:00
Daniel Hougaard
f7122c21fd Fix: Allow query function to be called with undefined orgId, and handle it as an error 2024-03-25 21:43:08 +01:00
Daniel Hougaard
b23deca8e4 Feat: Attach/Detach tags to secret 2024-03-25 21:42:32 +01:00
Daniel Hougaard
b606990dfb Update secret-tag-router.ts 2024-03-25 21:41:12 +01:00
Daniel Hougaard
2240277243 Feat: Standalone tags documentation 2024-03-25 21:40:45 +01:00
Maidul Islam
c8c5caba62 Update Chart.yaml 2024-03-25 13:48:17 -04:00
Maidul Islam
f408a6f60c Update values.yaml 2024-03-25 13:48:01 -04:00
Maidul Islam
391ed0ed74 Update build-staging-and-deploy-aws.yml 2024-03-25 11:15:39 -04:00
Daniel Hougaard
aef40212d2 Merge pull request #1528 from rhythmbhiwani/cli-fix-update-vars
Fixed CLI issue of updating variables using `infisical secrets set`
2024-03-25 15:30:47 +01:00
Akhil Mohan
5aa7cd46c1 Merge pull request #1594 from Salman2301/feat-cloudflare-secret-path
feat: add support for secret path for cloudflare page
2024-03-25 11:37:00 +05:30
Maidul Islam
6c0b916ad8 set version to short commit sha 2024-03-25 01:54:53 -04:00
Akhil Mohan
d7bc80308d Merge pull request #1566 from Salman2301/fix-typo-input
fix class name typo
2024-03-25 11:14:55 +05:30
Akhil Mohan
b7c7b242e8 Merge pull request #1578 from Salman2301/fix-select-key-nav
fix: add highlighted style for select component
2024-03-25 11:13:48 +05:30
Vladyslav Matsiiako
b592f4cb6d update ui 2024-03-24 22:00:30 -07:00
Akhil Mohan
cd0e1a87cf feat(server): resolved lint issue 2024-03-25 01:06:43 +05:30
Akhil Mohan
b5d7699b8d feat(ui): made changes from feedback and force delete feature 2024-03-25 00:50:26 +05:30
Akhil Mohan
69297bc16e feat(server): added limit to leases creation and support for force delete when external system fails to comply 2024-03-25 00:49:48 +05:30
Maidul Islam
37827367ed Merge pull request #1622 from Infisical/daniel/mi-project-creation-bug
Fix: Creating projects with Machine Identities that aren't org admins
2024-03-24 14:46:23 -04:00
Maidul Islam
403b1ce993 Merge pull request #1620 from Infisical/daniel/e2ee-button
Feat: Deprecate E2EE mode switching
2024-03-24 14:33:44 -04:00
Daniel Hougaard
c3c0006a25 Update project-service.ts 2024-03-24 17:15:10 +01:00
Maidul Islam
2241908d0a fix lining for gha 2024-03-24 00:52:21 -04:00
Maidul Islam
59b822510c update job names gha 2024-03-24 00:50:40 -04:00
Maidul Islam
d1408aff35 update pipeline 2024-03-24 00:49:47 -04:00
Maidul Islam
c67084f08d combine migration job with deploy 2024-03-24 00:47:46 -04:00
Maidul Islam
a280e002ed add prod deploy 2024-03-24 00:35:10 -04:00
Maidul Islam
76c4a8660f Merge pull request #1621 from redcubie/patch-1
Move DB_CONNECTION_URI to make sure DB credentials are initialized
2024-03-23 12:59:27 -04:00
redcubie
8c54dd611e Move DB_CONNECTION_URI to make sure DB credentials are initialized 2024-03-23 18:49:15 +02:00
Akhil Mohan
98ea2c1828 feat(agent): removed 5s polling algorithm to a simple one 2024-03-23 21:10:42 +05:30
Maidul Islam
5c75f526e7 Update build-staging-and-deploy-aws.yml 2024-03-22 17:18:45 -04:00
Maidul Islam
113e777b25 add wait for ecs 2024-03-22 16:16:22 -04:00
Maidul Islam
2a93449ffe add needs[] for gamm deploy gha 2024-03-22 14:56:38 -04:00
Maidul Islam
1ef1c042da add back other build steps 2024-03-22 14:55:55 -04:00
Daniel Hougaard
b64672a921 Update E2EESection.tsx 2024-03-22 19:33:53 +01:00
Daniel Hougaard
227e013502 Feat: Deprecate E2EE mode switching 2024-03-22 19:31:41 +01:00
Akhil Mohan
88f7e4255e feat(agent): added dynamic secret change based re-trigger 2024-03-22 23:59:08 +05:30
Maidul Islam
44ca8c315e remove all stages except deploy in gha 2024-03-22 14:12:13 -04:00
Daniel Hougaard
7766a7f4dd Merge pull request #1619 from Infisical/daniel/mi-ux-fix
Update IdentityModal.tsx
2024-03-22 18:56:07 +01:00
Daniel Hougaard
3cb150a749 Update IdentityModal.tsx 2024-03-22 18:27:57 +01:00
Maidul Islam
9e9ce261c8 give gha permission to update git token 2024-03-22 12:59:51 -04:00
Maidul Islam
fab7167850 update oidc audience 2024-03-22 12:54:27 -04:00
Akhil Mohan
c7de9aab4e Merge pull request #1618 from Infisical/gha-aws-pipeline
deploy to ecs using OIDC with aws
2024-03-22 22:13:09 +05:30
Maidul Islam
3560346f85 update step name 2024-03-22 12:42:32 -04:00
Maidul Islam
f0bf2f8dd0 seperate aws rds uri 2024-03-22 12:38:10 -04:00
Maidul Islam
2a6216b8fc deploy to ecs using OIDC with aws 2024-03-22 12:29:07 -04:00
Akhil Mohan
a07d055347 feat(agent): added agent template support to pull dynamic secret 2024-03-22 19:29:24 +05:30
Akhil Mohan
c05230f667 Merge pull request #1616 from Infisical/wait-for-job-helm
Update Chart.yaml
2024-03-22 19:03:32 +05:30
Maidul Islam
d68055a264 Update Chart.yaml
Update to multi arch and rootless
2024-03-22 09:28:44 -04:00
Akhil Mohan
e3e62430ba feat: changed the whole api from projectid to slug 2024-03-22 14:18:12 +05:30
Maidul Islam
dc6056b564 Merge pull request #1614 from francodalmau/fix-environment-popups-cancel-action
Fix add and update environment popups cancel button
2024-03-21 21:28:28 -04:00
franco_dalmau
94f0811661 Fix add and update environment popups cancel button 2024-03-21 20:09:57 -03:00
Maidul Islam
7b84ae6173 Update Chart.yaml 2024-03-21 15:08:38 -04:00
Maidul Islam
5710a304f8 Merge pull request #1533 from Infisical/daniel/k8-operator-machine-identities
Feat: K8 Operator Machine Identity Support
2024-03-21 15:01:50 -04:00
Daniel Hougaard
91e3bbba34 Fix: Requested changes 2024-03-21 19:58:10 +01:00
Daniel Hougaard
02112ede07 Fix: Requested changes 2024-03-21 19:53:21 +01:00
Daniel Hougaard
08cfbf64e4 Fix: Error handing 2024-03-21 19:37:12 +01:00
Daniel Hougaard
18da522b45 Chore: Helm charts 2024-03-21 18:35:00 +01:00
Daniel Hougaard
8cf68fbd9c Generated 2024-03-21 17:12:42 +01:00
Daniel Hougaard
d6b82dfaa4 Fix: Rebase sample conflicts 2024-03-21 17:09:41 +01:00
Daniel Hougaard
7bd4eed328 Chore: Generate K8 helm charts 2024-03-21 17:08:01 +01:00
Daniel Hougaard
0341c32da0 Fix: Change credentials -> credentialsRef 2024-03-21 17:08:01 +01:00
Daniel Hougaard
caea055281 Feat: Improve K8 docs 2024-03-21 17:08:01 +01:00
Daniel Hougaard
c08c78de8d Feat: Rename universalAuthMachineIdentity to universalAuth 2024-03-21 17:08:01 +01:00
Daniel Hougaard
3765a14246 Fix: Generate new types 2024-03-21 17:08:01 +01:00
Daniel Hougaard
c5a11e839b Feat: Deprecate Service Accounts 2024-03-21 17:08:01 +01:00
Daniel Hougaard
93bd3d8270 Docs: Simplified docs more 2024-03-21 17:07:56 +01:00
Daniel Hougaard
b9601dd418 Update kubernetes.mdx 2024-03-21 17:07:56 +01:00
Daniel Hougaard
ae3bc04b07 Docs 2024-03-21 17:07:31 +01:00
Daniel Hougaard
11edefa66f Feat: Added project slug support 2024-03-21 17:07:31 +01:00
Daniel Hougaard
f71459ede0 Slugs 2024-03-21 17:07:31 +01:00
Daniel Hougaard
33324a5a3c Type generation 2024-03-21 17:07:31 +01:00
Daniel Hougaard
5c6781a705 Update machine-identity-token.go 2024-03-21 17:07:31 +01:00
Daniel Hougaard
71e31518d7 Feat: Add machine identity token handler 2024-03-21 17:07:31 +01:00
Daniel Hougaard
f6f6db2898 Fix: Moved update attributes type to models 2024-03-21 17:07:31 +01:00
Daniel Hougaard
55780b65d3 Feat: Machine Identity support (token refreshing logic) 2024-03-21 17:07:31 +01:00
Daniel Hougaard
83bbf9599d Feat: Machine Identity support 2024-03-21 17:07:31 +01:00
Daniel Hougaard
f8f2b2574d Feat: Machine Identity support (types) 2024-03-21 17:07:31 +01:00
Daniel Hougaard
318d12addd Feat: Machine Identity support 2024-03-21 17:07:31 +01:00
Daniel Hougaard
872a28d02a Feat: Machine Identity support for K8 2024-03-21 17:06:49 +01:00
Daniel Hougaard
6f53a5631c Fix: Double prints 2024-03-21 17:06:49 +01:00
Daniel Hougaard
ff2098408d Update sample.yaml 2024-03-21 17:06:48 +01:00
Daniel Hougaard
9e85d9bbf0 Example 2024-03-21 17:05:46 +01:00
Daniel Hougaard
0f3a48bb32 Generated 2024-03-21 17:05:46 +01:00
Daniel Hougaard
f869def8ea Added new types 2024-03-21 17:05:46 +01:00
Maidul Islam
378bc57a88 Merge pull request #1480 from akhilmhdh/docs/rotation-doc-update
docs: improved secret rotation documentation with better understanding
2024-03-21 11:17:19 -04:00
Maidul Islam
242179598b fix types, rephrase, and revise rotation docs 2024-03-21 11:03:41 -04:00
Akhil Mohan
70fe80414d feat(server): added some more validation and feedback for deletion etc 2024-03-21 20:23:44 +05:30
Akhil Mohan
e201e80a06 feat(ui): completed ui for dynamic secret 2024-03-21 20:23:44 +05:30
Akhil Mohan
177cd385cc feat(ui): added dynamic secret and lease api hook 2024-03-21 20:22:30 +05:30
Akhil Mohan
ab48c3b4fe feat(server): completed integration of dynamic server routes 2024-03-21 20:22:30 +05:30
Akhil Mohan
69f36d1df6 feat(server): service for dynamic secret lease and queue service for revocation 2024-03-21 20:22:30 +05:30
Akhil Mohan
11c7b5c674 feat(server): service for dynamic secret config 2024-03-21 20:22:30 +05:30
Akhil Mohan
ee29577e6d feat(server): added dynamic secret database schema 2024-03-21 20:22:30 +05:30
Maidul Islam
e3e049b66c Update build-staging-and-deploy.yml 2024-03-20 22:14:46 -04:00
Maidul Islam
878e4a79e7 Merge pull request #1606 from Infisical/daniel/ui-imported-folders-fix
Fix: UI indicator for imports
2024-03-20 22:08:50 -04:00
Daniel Hougaard
609ce8e5cc Fix: Improved UI import indicators 2024-03-21 03:06:14 +01:00
Maidul Islam
04c1ea9b11 Update build-staging-and-deploy.yml 2024-03-20 18:03:49 -04:00
Maidul Islam
3baca73e53 add seperate step for ecr build 2024-03-20 16:25:54 -04:00
Daniel Hougaard
36adf6863b Fix: UI secret import indicator 2024-03-20 21:09:54 +01:00
Daniel Hougaard
6363e7d30a Update index.ts 2024-03-20 20:26:28 +01:00
Daniel Hougaard
f9621fad8e Fix: Remove duplicate type 2024-03-20 20:26:00 +01:00
Daniel Hougaard
90be28b87a Feat: Import indicator 2024-03-20 20:25:48 +01:00
Daniel Hougaard
671adee4d7 Feat: Indicator for wether or not secrets are imported 2024-03-20 20:24:06 +01:00
Daniel Hougaard
c9cb90c98e Feat: Add center property to tooltip 2024-03-20 20:23:21 +01:00
Akhil Mohan
9f691df395 Merge pull request #1607 from Infisical/fix-secrets-by-name
set imports=true for get secret by name
2024-03-20 23:50:15 +05:30
Maidul Islam
d702a61586 set imports=true for get secret by name 2024-03-20 14:06:16 -04:00
Maidul Islam
1c16f406a7 remove debug 2024-03-20 13:06:29 -04:00
Maidul Islam
90f739caa6 correct repo name env 2024-03-20 13:05:59 -04:00
Maidul Islam
ede8b6f286 add .env context for ecr tag 2024-03-20 13:00:06 -04:00
Maidul Islam
232c547d75 correct ecr image tag 2024-03-20 11:54:33 -04:00
Maidul Islam
fe08bbb691 push to ecr 2024-03-20 11:46:47 -04:00
Maidul Islam
2bd06ecde4 login into ecr 2024-03-20 11:31:39 -04:00
Daniel Hougaard
08b79d65ea Fix: Remove unused lint disable 2024-03-20 14:55:02 +01:00
Daniel Hougaard
4e1733ba6c Fix: More reverting 2024-03-20 14:44:40 +01:00
Daniel Hougaard
a4e495ea1c Fix: Restructured frontend 2024-03-20 14:42:34 +01:00
Daniel Hougaard
a750d68363 Fix: Reverted backend changes 2024-03-20 14:40:24 +01:00
Daniel Hougaard
d7161a353d Fix: Better variable naming 2024-03-20 13:15:51 +01:00
Daniel Hougaard
12c414817f Fix: Remove debugging logs 2024-03-20 13:14:18 +01:00
Daniel Hougaard
e5e494d0ee Fix: Also display imported folder indicator for nested folders 2024-03-20 13:13:50 +01:00
Daniel Hougaard
5a21b85e9e Fix: Removed overlap from other working branch 2024-03-20 13:13:19 +01:00
Daniel Hougaard
348fdf6429 Feat: Visualize imported folders in overview page (include imported folders in response) 2024-03-20 12:56:11 +01:00
Daniel Hougaard
88e609cb66 Feat: New types for imported folders 2024-03-20 12:55:40 +01:00
Daniel Hougaard
78058d691a Enhancement: Add disabled prop to Tooltip component 2024-03-20 12:55:08 +01:00
Daniel Hougaard
1d465a50c3 Feat: Visualize imported folders in overview page 2024-03-20 12:54:44 +01:00
Maidul Islam
ffc7249c7c update diagram 2024-03-19 23:44:12 -04:00
Maidul Islam
90bcf23097 Update README.md 2024-03-19 23:36:07 -04:00
Maidul Islam
5fa4d9029d Merge pull request #1577 from Salman2301/fix-notification-z-index
fix: notification error behind detail sidebar
2024-03-19 18:56:14 -04:00
Maidul Islam
7160cf58ee Merge branch 'main' into fix-notification-z-index 2024-03-19 18:50:58 -04:00
Maidul Islam
6b2d757e39 remove outdated healthcheck 2024-03-19 17:21:46 -04:00
Daniel Hougaard
c075fcceca Merge pull request #1591 from Infisical/daniel/prettier-fix
Chore: Prettier formatting
2024-03-19 21:23:11 +01:00
Maidul Islam
e25f5dd65f Merge pull request #1605 from Infisical/creation-policy-k8s
add managed secret creation policy
2024-03-19 15:23:16 -04:00
Maidul Islam
3eef023c30 add managed secret creation policy 2024-03-19 14:58:17 -04:00
Tuan Dang
e63deb0860 Patch org role slug validation 2024-03-19 10:23:00 -07:00
Maidul Islam
02b2851990 Merge pull request #1601 from Infisical/fix/db-host
fix(server): updated secret rotation to pick on db host in validation
2024-03-19 10:03:12 -04:00
Akhil Mohan
cb828200e1 fix(server): updated secret rotation to pick on db host in validation 2024-03-19 13:56:21 +00:00
Akhil Mohan
77d068ae2c Merge pull request #1599 from Infisical/daniel/improve-create-project
Fix: Remove required org slug from create project route
2024-03-19 18:31:16 +05:30
Daniel Hougaard
8702af671d Fix: Typings error 2024-03-19 13:45:57 +01:00
Daniel Hougaard
31c0fd96ea Update UserInfoStep.tsx 2024-03-19 13:39:34 +01:00
Daniel Hougaard
2c539697df Feat: Remove orgSlug from create project endpoint 2024-03-19 13:39:24 +01:00
Daniel Hougaard
ae97b74933 Feat: Improve create project, remove organization slug from frontend 2024-03-19 13:38:58 +01:00
Akhil Mohan
3e6af2dae5 Merge pull request #1597 from Infisical/daniel/api-endpoint-fix
Fix: Mintlify interactive API docs defaulting to localhost server
2024-03-19 16:32:49 +05:30
Daniel Hougaard
3c91e1127f Fix: Mintlify docs defaulting to localhost endpoint 2024-03-19 11:58:01 +01:00
vmatsiiako
0e31a9146a Update ee.mdx 2024-03-18 22:10:09 -07:00
Salman
d2a93eb1d2 feat: add support for secret path for cloudflare page 2024-03-18 21:31:21 +05:30
Daniel Hougaard
fa1b28b33f Update .eslintrc.js 2024-03-18 16:07:49 +01:00
Daniel Hougaard
415cf31b2d Fix: Lint bug (Cannot read properties of undefined (reading 'getTokens')) 2024-03-18 16:01:14 +01:00
Daniel Hougaard
9002e6cb33 Fix: Format entire frontend properly 2024-03-18 16:00:03 +01:00
Daniel Hougaard
1ede551c3e Fix: Format entire frontend properly 2024-03-18 15:59:47 +01:00
Daniel Hougaard
b7b43858f6 Fix: Format entire frontend properly 2024-03-18 15:55:01 +01:00
Akhil Mohan
c91789e6d0 Merge pull request #1590 from Infisical/daniel/ts-comments
Fix: Github warnings / Lint warnings
2024-03-18 20:19:26 +05:30
Daniel Hougaard
db0ba4be10 Fix: Github warnings / Lint warnings 2024-03-18 15:47:03 +01:00
Akhil Mohan
f73c807aa0 Merge pull request #1589 from Infisical/daniel/ui-improvements
Fix: Select organization UX & project card enhancements
2024-03-18 20:14:39 +05:30
Daniel Hougaard
d1dacd81aa Chore: UX improvements to project cards 2024-03-18 15:36:59 +01:00
Daniel Hougaard
e8b635ce37 Fix: Code cleanup & truncate organization names 2024-03-18 15:36:01 +01:00
Daniel Hougaard
1d3e03e308 Merge pull request #1583 from Infisical/daniel/saml-cli-fix
Fix: SAML CLI login
2024-03-18 10:44:48 +01:00
Daniel Hougaard
88e2eff7eb Fix: SAML CLI login 2024-03-18 10:37:49 +01:00
Maidul Islam
cd192ee228 Update sdks.mdx 2024-03-17 15:04:14 -04:00
Salman
203e00216f fix: add highlighted style for select component 2024-03-16 17:53:48 +05:30
Salman
ee215bccfa fix: notification error behind detail sidebar 2024-03-16 08:34:21 +05:30
Salman
7a3a6663f1 fix class name typo 2024-03-14 03:37:54 +05:30
Akhil Mohan
8c491668dc docs: updated images of inputs in secret rotation 2024-03-07 23:17:32 +05:30
Akhil Mohan
c873e2cba8 docs: updated secret rotation doc with images 2024-03-05 15:42:53 +05:30
Maidul Islam
1bc045a7fa update overview sendgrid 2024-03-05 15:42:53 +05:30
Akhil Mohan
533de93199 docs: improved secret rotation documentation with better understanding 2024-03-05 15:42:53 +05:30
Rhythm Bhiwani
115b4664bf Fixed CLI issue of updating variables using infisical secrets set 2024-03-05 03:43:06 +05:30
697 changed files with 17159 additions and 6538 deletions

View File

@@ -3,9 +3,6 @@
# THIS IS A SAMPLE ENCRYPTION KEY AND SHOULD NEVER BE USED FOR PRODUCTION # THIS IS A SAMPLE ENCRYPTION KEY AND SHOULD NEVER BE USED FOR PRODUCTION
ENCRYPTION_KEY=6c1fe4e407b8911c104518103505b218 ENCRYPTION_KEY=6c1fe4e407b8911c104518103505b218
# Required
DB_CONNECTION_URI=postgres://${POSTGRES_USER}:${POSTGRES_PASSWORD}@db:5432/${POSTGRES_DB}
# JWT # JWT
# Required secrets to sign JWT tokens # Required secrets to sign JWT tokens
# THIS IS A SAMPLE AUTH_SECRET KEY AND SHOULD NEVER BE USED FOR PRODUCTION # THIS IS A SAMPLE AUTH_SECRET KEY AND SHOULD NEVER BE USED FOR PRODUCTION
@@ -16,6 +13,9 @@ POSTGRES_PASSWORD=infisical
POSTGRES_USER=infisical POSTGRES_USER=infisical
POSTGRES_DB=infisical POSTGRES_DB=infisical
# Required
DB_CONNECTION_URI=postgres://${POSTGRES_USER}:${POSTGRES_PASSWORD}@db:5432/${POSTGRES_DB}
# Redis # Redis
REDIS_URL=redis://redis:6379 REDIS_URL=redis://redis:6379

View File

@@ -41,6 +41,7 @@ jobs:
load: true load: true
context: backend context: backend
tags: infisical/infisical:test tags: infisical/infisical:test
platforms: linux/amd64,linux/arm64
- name: ⏻ Spawn backend container and dependencies - name: ⏻ Spawn backend container and dependencies
run: | run: |
docker compose -f .github/resources/docker-compose.be-test.yml up --wait --quiet-pull docker compose -f .github/resources/docker-compose.be-test.yml up --wait --quiet-pull
@@ -92,6 +93,7 @@ jobs:
project: 64mmf0n610 project: 64mmf0n610
context: frontend context: frontend
tags: infisical/frontend:test tags: infisical/frontend:test
platforms: linux/amd64,linux/arm64
build-args: | build-args: |
POSTHOG_API_KEY=${{ secrets.PUBLIC_POSTHOG_API_KEY }} POSTHOG_API_KEY=${{ secrets.PUBLIC_POSTHOG_API_KEY }}
NEXT_INFISICAL_PLATFORM_VERSION=${{ steps.extract_version.outputs.version }} NEXT_INFISICAL_PLATFORM_VERSION=${{ steps.extract_version.outputs.version }}

View File

@@ -0,0 +1,140 @@
name: Deployment pipeline
on: [workflow_dispatch]
permissions:
id-token: write
contents: read
jobs:
infisical-image:
name: Build backend image
runs-on: ubuntu-latest
steps:
- name: ☁️ Checkout source
uses: actions/checkout@v3
- name: 📦 Install dependencies to test all dependencies
run: npm ci --only-production
working-directory: backend
- name: Save commit hashes for tag
id: commit
uses: pr-mpt/actions-commit-hash@v2
- name: 🔧 Set up Docker Buildx
uses: docker/setup-buildx-action@v2
- name: 🐋 Login to Docker Hub
uses: docker/login-action@v2
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Set up Depot CLI
uses: depot/setup-action@v1
- name: 🏗️ Build backend and push to docker hub
uses: depot/build-push-action@v1
with:
project: 64mmf0n610
token: ${{ secrets.DEPOT_PROJECT_TOKEN }}
push: true
context: .
file: Dockerfile.standalone-infisical
tags: |
infisical/staging_infisical:${{ steps.commit.outputs.short }}
infisical/staging_infisical:latest
platforms: linux/amd64,linux/arm64
build-args: |
POSTHOG_API_KEY=${{ secrets.PUBLIC_POSTHOG_API_KEY }}
INFISICAL_PLATFORM_VERSION=${{ steps.commit.outputs.short }}
gamma-deployment:
name: Deploy to gamma
runs-on: ubuntu-latest
needs: [infisical-image]
environment:
name: Gamma
steps:
- name: Checkout code
uses: actions/checkout@v2
- name: Setup Node.js environment
uses: actions/setup-node@v2
with:
node-version: "20"
- name: Change directory to backend and install dependencies
env:
DB_CONNECTION_URI: ${{ secrets.DB_CONNECTION_URI }}
run: |
cd backend
npm install
npm run migration:latest
- name: Configure AWS Credentials
uses: aws-actions/configure-aws-credentials@v4
with:
audience: sts.amazonaws.com
aws-region: us-east-1
role-to-assume: arn:aws:iam::905418227878:role/deploy-new-ecs-img
- name: Save commit hashes for tag
id: commit
uses: pr-mpt/actions-commit-hash@v2
- name: Download task definition
run: |
aws ecs describe-task-definition --task-definition infisical-prod-platform --query taskDefinition > task-definition.json
- name: Render Amazon ECS task definition
id: render-web-container
uses: aws-actions/amazon-ecs-render-task-definition@v1
with:
task-definition: task-definition.json
container-name: infisical-prod-platform
image: infisical/staging_infisical:${{ steps.commit.outputs.short }}
environment-variables: "LOG_LEVEL=info"
- name: Deploy to Amazon ECS service
uses: aws-actions/amazon-ecs-deploy-task-definition@v1
with:
task-definition: ${{ steps.render-web-container.outputs.task-definition }}
service: infisical-prod-platform
cluster: infisical-prod-platform
wait-for-service-stability: true
production-postgres-deployment:
name: Deploy to production
runs-on: ubuntu-latest
needs: [gamma-deployment]
environment:
name: Production
steps:
- name: Checkout code
uses: actions/checkout@v2
- name: Setup Node.js environment
uses: actions/setup-node@v2
with:
node-version: "20"
- name: Change directory to backend and install dependencies
env:
DB_CONNECTION_URI: ${{ secrets.DB_CONNECTION_URI }}
run: |
cd backend
npm install
npm run migration:latest
- name: Configure AWS Credentials
uses: aws-actions/configure-aws-credentials@v4
with:
audience: sts.amazonaws.com
aws-region: us-east-1
role-to-assume: arn:aws:iam::381492033652:role/gha-make-prod-deployment
- name: Save commit hashes for tag
id: commit
uses: pr-mpt/actions-commit-hash@v2
- name: Download task definition
run: |
aws ecs describe-task-definition --task-definition infisical-prod-platform --query taskDefinition > task-definition.json
- name: Render Amazon ECS task definition
id: render-web-container
uses: aws-actions/amazon-ecs-render-task-definition@v1
with:
task-definition: task-definition.json
container-name: infisical-prod-platform
image: infisical/staging_infisical:${{ steps.commit.outputs.short }}
environment-variables: "LOG_LEVEL=info"
- name: Deploy to Amazon ECS service
uses: aws-actions/amazon-ecs-deploy-task-definition@v1
with:
task-definition: ${{ steps.render-web-container.outputs.task-definition }}
service: infisical-prod-platform
cluster: infisical-prod-platform
wait-for-service-stability: true

View File

@@ -1,120 +0,0 @@
name: Build, Publish and Deploy to Gamma
on: [workflow_dispatch]
jobs:
infisical-image:
name: Build backend image
runs-on: ubuntu-latest
steps:
- name: ☁️ Checkout source
uses: actions/checkout@v3
- name: 📦 Install dependencies to test all dependencies
run: npm ci --only-production
working-directory: backend
# - name: 🧪 Run tests
# run: npm run test:ci
# working-directory: backend
- name: Save commit hashes for tag
id: commit
uses: pr-mpt/actions-commit-hash@v2
- name: 🔧 Set up Docker Buildx
uses: docker/setup-buildx-action@v2
- name: 🐋 Login to Docker Hub
uses: docker/login-action@v2
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Set up Depot CLI
uses: depot/setup-action@v1
- name: 📦 Build backend and export to Docker
uses: depot/build-push-action@v1
with:
project: 64mmf0n610
token: ${{ secrets.DEPOT_PROJECT_TOKEN }}
load: true
context: .
file: Dockerfile.standalone-infisical
tags: infisical/infisical:test
# - name: ⏻ Spawn backend container and dependencies
# run: |
# docker compose -f .github/resources/docker-compose.be-test.yml up --wait --quiet-pull
# - name: 🧪 Test backend image
# run: |
# ./.github/resources/healthcheck.sh infisical-backend-test
# - name: ⏻ Shut down backend container and dependencies
# run: |
# docker compose -f .github/resources/docker-compose.be-test.yml down
- name: 🏗️ Build backend and push
uses: depot/build-push-action@v1
with:
project: 64mmf0n610
token: ${{ secrets.DEPOT_PROJECT_TOKEN }}
push: true
context: .
file: Dockerfile.standalone-infisical
tags: |
infisical/staging_infisical:${{ steps.commit.outputs.short }}
infisical/staging_infisical:latest
platforms: linux/amd64,linux/arm64
build-args: |
POSTHOG_API_KEY=${{ secrets.PUBLIC_POSTHOG_API_KEY }}
INFISICAL_PLATFORM_VERSION=${{ steps.extract_version.outputs.version }}
postgres-migration:
name: Run latest migration files
runs-on: ubuntu-latest
needs: [infisical-image]
steps:
- name: Checkout code
uses: actions/checkout@v2
- name: Setup Node.js environment
uses: actions/setup-node@v2
with:
node-version: "20"
- name: Change directory to backend and install dependencies
env:
DB_CONNECTION_URI: ${{ secrets.DB_CONNECTION_URI }}
run: |
cd backend
npm install
npm run migration:latest
# - name: Run postgres DB migration files
# env:
# DB_CONNECTION_URI: ${{ secrets.DB_CONNECTION_URI }}
# run: npm run migration:latest
gamma-deployment:
name: Deploy to gamma
runs-on: ubuntu-latest
needs: [postgres-migration]
steps:
- name: ☁️ Checkout source
uses: actions/checkout@v3
- name: Install Helm
uses: azure/setup-helm@v3
with:
version: v3.10.0
- name: Install infisical helm chart
run: |
helm repo add infisical-helm-charts 'https://dl.cloudsmith.io/public/infisical/helm-charts/helm/charts/'
helm repo update
- name: Install kubectl
uses: azure/setup-kubectl@v3
- name: Install doctl
uses: digitalocean/action-doctl@v2
with:
token: ${{ secrets.DIGITALOCEAN_ACCESS_TOKEN }}
- name: Save DigitalOcean kubeconfig with short-lived credentials
run: doctl kubernetes cluster kubeconfig save --expiry-seconds 600 infisical-gamma-postgres
- name: switch to gamma namespace
run: kubectl config set-context --current --namespace=gamma
- name: test kubectl
run: kubectl get ingress
- name: Download helm values to file and upgrade gamma deploy
run: |
wget https://raw.githubusercontent.com/Infisical/infisical/main/.github/values.yaml
helm upgrade infisical infisical-helm-charts/infisical-standalone --values values.yaml --wait --install
if [[ $(helm status infisical) == *"FAILED"* ]]; then
echo "Helm upgrade failed"
exit 1
else
echo "Helm upgrade was successful"
fi

View File

@@ -118,9 +118,6 @@ WORKDIR /backend
ENV TELEMETRY_ENABLED true ENV TELEMETRY_ENABLED true
HEALTHCHECK --interval=10s --timeout=3s --start-period=10s \
CMD node healthcheck.js
EXPOSE 8080 EXPOSE 8080
EXPOSE 443 EXPOSE 443

View File

@@ -10,7 +10,8 @@
<a href="https://infisical.com/">Infisical Cloud</a> | <a href="https://infisical.com/">Infisical Cloud</a> |
<a href="https://infisical.com/docs/self-hosting/overview">Self-Hosting</a> | <a href="https://infisical.com/docs/self-hosting/overview">Self-Hosting</a> |
<a href="https://infisical.com/docs/documentation/getting-started/introduction">Docs</a> | <a href="https://infisical.com/docs/documentation/getting-started/introduction">Docs</a> |
<a href="https://www.infisical.com">Website</a> <a href="https://www.infisical.com">Website</a> |
<a href="https://infisical.com/careers">Hiring (Remote/SF)</a>
</h4> </h4>
<p align="center"> <p align="center">

View File

@@ -7,10 +7,10 @@ const prompt = promptSync({ sigint: true });
const migrationName = prompt("Enter name for migration: "); const migrationName = prompt("Enter name for migration: ");
// Remove spaces from migration name and replace with hyphens
const formattedMigrationName = migrationName.replace(/\s+/g, "-");
execSync( execSync(
`npx knex migrate:make --knexfile ${path.join( `npx knex migrate:make --knexfile ${path.join(__dirname, "../src/db/knexfile.ts")} -x ts ${formattedMigrationName}`,
__dirname,
"../src/db/knexfile.ts"
)} -x ts ${migrationName}`,
{ stdio: "inherit" } { stdio: "inherit" }
); );

View File

@@ -3,9 +3,13 @@ import "fastify";
import { TUsers } from "@app/db/schemas"; import { TUsers } from "@app/db/schemas";
import { TAuditLogServiceFactory } from "@app/ee/services/audit-log/audit-log-service"; import { TAuditLogServiceFactory } from "@app/ee/services/audit-log/audit-log-service";
import { TCreateAuditLogDTO } from "@app/ee/services/audit-log/audit-log-types"; import { TCreateAuditLogDTO } from "@app/ee/services/audit-log/audit-log-types";
import { TDynamicSecretServiceFactory } from "@app/ee/services/dynamic-secret/dynamic-secret-service";
import { TDynamicSecretLeaseServiceFactory } from "@app/ee/services/dynamic-secret-lease/dynamic-secret-lease-service";
import { TIdentityProjectAdditionalPrivilegeServiceFactory } from "@app/ee/services/identity-project-additional-privilege/identity-project-additional-privilege-service";
import { TLdapConfigServiceFactory } from "@app/ee/services/ldap-config/ldap-config-service"; import { TLdapConfigServiceFactory } from "@app/ee/services/ldap-config/ldap-config-service";
import { TLicenseServiceFactory } from "@app/ee/services/license/license-service"; import { TLicenseServiceFactory } from "@app/ee/services/license/license-service";
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service"; 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 { TSamlConfigServiceFactory } from "@app/ee/services/saml-config/saml-config-service"; import { TSamlConfigServiceFactory } from "@app/ee/services/saml-config/saml-config-service";
import { TScimServiceFactory } from "@app/ee/services/scim/scim-service"; import { TScimServiceFactory } from "@app/ee/services/scim/scim-service";
import { TSecretApprovalPolicyServiceFactory } from "@app/ee/services/secret-approval-policy/secret-approval-policy-service"; import { TSecretApprovalPolicyServiceFactory } from "@app/ee/services/secret-approval-policy/secret-approval-policy-service";
@@ -62,7 +66,7 @@ declare module "fastify" {
authMethod: ActorAuthMethod; authMethod: ActorAuthMethod;
type: ActorType; type: ActorType;
id: string; id: string;
orgId?: string; orgId: string;
}; };
// passport data // passport data
passportUser: { passportUser: {
@@ -117,6 +121,10 @@ declare module "fastify" {
trustedIp: TTrustedIpServiceFactory; trustedIp: TTrustedIpServiceFactory;
secretBlindIndex: TSecretBlindIndexServiceFactory; secretBlindIndex: TSecretBlindIndexServiceFactory;
telemetry: TTelemetryServiceFactory; telemetry: TTelemetryServiceFactory;
dynamicSecret: TDynamicSecretServiceFactory;
dynamicSecretLease: TDynamicSecretLeaseServiceFactory;
projectUserAdditionalPrivilege: TProjectUserAdditionalPrivilegeServiceFactory;
identityProjectAdditionalPrivilege: TIdentityProjectAdditionalPrivilegeServiceFactory;
}; };
// this is exclusive use for middlewares in which we need to inject data // this is exclusive use for middlewares in which we need to inject data
// everywhere else access using service layer // everywhere else access using service layer

View File

@@ -17,6 +17,12 @@ import {
TBackupPrivateKey, TBackupPrivateKey,
TBackupPrivateKeyInsert, TBackupPrivateKeyInsert,
TBackupPrivateKeyUpdate, TBackupPrivateKeyUpdate,
TDynamicSecretLeases,
TDynamicSecretLeasesInsert,
TDynamicSecretLeasesUpdate,
TDynamicSecrets,
TDynamicSecretsInsert,
TDynamicSecretsUpdate,
TGitAppInstallSessions, TGitAppInstallSessions,
TGitAppInstallSessionsInsert, TGitAppInstallSessionsInsert,
TGitAppInstallSessionsUpdate, TGitAppInstallSessionsUpdate,
@@ -32,6 +38,9 @@ import {
TIdentityOrgMemberships, TIdentityOrgMemberships,
TIdentityOrgMembershipsInsert, TIdentityOrgMembershipsInsert,
TIdentityOrgMembershipsUpdate, TIdentityOrgMembershipsUpdate,
TIdentityProjectAdditionalPrivilege,
TIdentityProjectAdditionalPrivilegeInsert,
TIdentityProjectAdditionalPrivilegeUpdate,
TIdentityProjectMembershipRole, TIdentityProjectMembershipRole,
TIdentityProjectMembershipRoleInsert, TIdentityProjectMembershipRoleInsert,
TIdentityProjectMembershipRoleUpdate, TIdentityProjectMembershipRoleUpdate,
@@ -86,6 +95,9 @@ import {
TProjects, TProjects,
TProjectsInsert, TProjectsInsert,
TProjectsUpdate, TProjectsUpdate,
TProjectUserAdditionalPrivilege,
TProjectUserAdditionalPrivilegeInsert,
TProjectUserAdditionalPrivilegeUpdate,
TProjectUserMembershipRoles, TProjectUserMembershipRoles,
TProjectUserMembershipRolesInsert, TProjectUserMembershipRolesInsert,
TProjectUserMembershipRolesUpdate, TProjectUserMembershipRolesUpdate,
@@ -233,6 +245,11 @@ declare module "knex/types/tables" {
TProjectUserMembershipRolesUpdate TProjectUserMembershipRolesUpdate
>; >;
[TableName.ProjectRoles]: Knex.CompositeTableType<TProjectRoles, TProjectRolesInsert, TProjectRolesUpdate>; [TableName.ProjectRoles]: Knex.CompositeTableType<TProjectRoles, TProjectRolesInsert, TProjectRolesUpdate>;
[TableName.ProjectUserAdditionalPrivilege]: Knex.CompositeTableType<
TProjectUserAdditionalPrivilege,
TProjectUserAdditionalPrivilegeInsert,
TProjectUserAdditionalPrivilegeUpdate
>;
[TableName.ProjectKeys]: Knex.CompositeTableType<TProjectKeys, TProjectKeysInsert, TProjectKeysUpdate>; [TableName.ProjectKeys]: Knex.CompositeTableType<TProjectKeys, TProjectKeysInsert, TProjectKeysUpdate>;
[TableName.Secret]: Knex.CompositeTableType<TSecrets, TSecretsInsert, TSecretsUpdate>; [TableName.Secret]: Knex.CompositeTableType<TSecrets, TSecretsInsert, TSecretsUpdate>;
[TableName.SecretBlindIndex]: Knex.CompositeTableType< [TableName.SecretBlindIndex]: Knex.CompositeTableType<
@@ -288,6 +305,11 @@ declare module "knex/types/tables" {
TIdentityProjectMembershipRoleInsert, TIdentityProjectMembershipRoleInsert,
TIdentityProjectMembershipRoleUpdate TIdentityProjectMembershipRoleUpdate
>; >;
[TableName.IdentityProjectAdditionalPrivilege]: Knex.CompositeTableType<
TIdentityProjectAdditionalPrivilege,
TIdentityProjectAdditionalPrivilegeInsert,
TIdentityProjectAdditionalPrivilegeUpdate
>;
[TableName.ScimToken]: Knex.CompositeTableType<TScimTokens, TScimTokensInsert, TScimTokensUpdate>; [TableName.ScimToken]: Knex.CompositeTableType<TScimTokens, TScimTokensInsert, TScimTokensUpdate>;
[TableName.SecretApprovalPolicy]: Knex.CompositeTableType< [TableName.SecretApprovalPolicy]: Knex.CompositeTableType<
TSecretApprovalPolicies, TSecretApprovalPolicies,
@@ -340,6 +362,12 @@ declare module "knex/types/tables" {
TSecretSnapshotFoldersInsert, TSecretSnapshotFoldersInsert,
TSecretSnapshotFoldersUpdate TSecretSnapshotFoldersUpdate
>; >;
[TableName.DynamicSecret]: Knex.CompositeTableType<TDynamicSecrets, TDynamicSecretsInsert, TDynamicSecretsUpdate>;
[TableName.DynamicSecretLease]: Knex.CompositeTableType<
TDynamicSecretLeases,
TDynamicSecretLeasesInsert,
TDynamicSecretLeasesUpdate
>;
[TableName.SamlConfig]: Knex.CompositeTableType<TSamlConfigs, TSamlConfigsInsert, TSamlConfigsUpdate>; [TableName.SamlConfig]: Knex.CompositeTableType<TSamlConfigs, TSamlConfigsInsert, TSamlConfigsUpdate>;
[TableName.LdapConfig]: Knex.CompositeTableType<TLdapConfigs, TLdapConfigsInsert, TLdapConfigsUpdate>; [TableName.LdapConfig]: Knex.CompositeTableType<TLdapConfigs, TLdapConfigsInsert, TLdapConfigsUpdate>;
[TableName.OrgBot]: Knex.CompositeTableType<TOrgBots, TOrgBotsInsert, TOrgBotsUpdate>; [TableName.OrgBot]: Knex.CompositeTableType<TOrgBots, TOrgBotsInsert, TOrgBotsUpdate>;

View File

@@ -0,0 +1,58 @@
import { Knex } from "knex";
import { SecretEncryptionAlgo, SecretKeyEncoding, TableName } from "../schemas";
import { createOnUpdateTrigger, dropOnUpdateTrigger } from "../utils";
export async function up(knex: Knex): Promise<void> {
const doesTableExist = await knex.schema.hasTable(TableName.DynamicSecret);
if (!doesTableExist) {
await knex.schema.createTable(TableName.DynamicSecret, (t) => {
t.uuid("id", { primaryKey: true }).defaultTo(knex.fn.uuid());
t.string("name").notNullable();
t.integer("version").notNullable();
t.string("type").notNullable();
t.string("defaultTTL").notNullable();
t.string("maxTTL");
t.string("inputIV").notNullable();
t.text("inputCiphertext").notNullable();
t.string("inputTag").notNullable();
t.string("algorithm").notNullable().defaultTo(SecretEncryptionAlgo.AES_256_GCM);
t.string("keyEncoding").notNullable().defaultTo(SecretKeyEncoding.UTF8);
t.uuid("folderId").notNullable();
// for background process communication
t.string("status");
t.string("statusDetails");
t.foreign("folderId").references("id").inTable(TableName.SecretFolder).onDelete("CASCADE");
t.unique(["name", "folderId"]);
t.timestamps(true, true, true);
});
}
await createOnUpdateTrigger(knex, TableName.DynamicSecret);
const doesTableDynamicSecretLease = await knex.schema.hasTable(TableName.DynamicSecretLease);
if (!doesTableDynamicSecretLease) {
await knex.schema.createTable(TableName.DynamicSecretLease, (t) => {
t.uuid("id", { primaryKey: true }).defaultTo(knex.fn.uuid());
t.integer("version").notNullable();
t.string("externalEntityId").notNullable();
t.datetime("expireAt").notNullable();
// for background process communication
t.string("status");
t.string("statusDetails");
t.uuid("dynamicSecretId").notNullable();
t.foreign("dynamicSecretId").references("id").inTable(TableName.DynamicSecret).onDelete("CASCADE");
t.timestamps(true, true, true);
});
}
await createOnUpdateTrigger(knex, TableName.DynamicSecretLease);
}
export async function down(knex: Knex): Promise<void> {
await dropOnUpdateTrigger(knex, TableName.DynamicSecretLease);
await knex.schema.dropTableIfExists(TableName.DynamicSecretLease);
await dropOnUpdateTrigger(knex, TableName.DynamicSecret);
await knex.schema.dropTableIfExists(TableName.DynamicSecret);
}

View File

@@ -0,0 +1,29 @@
import { Knex } from "knex";
import { TableName } from "../schemas";
import { createOnUpdateTrigger, dropOnUpdateTrigger } from "../utils";
export async function up(knex: Knex): Promise<void> {
if (!(await knex.schema.hasTable(TableName.ProjectUserAdditionalPrivilege))) {
await knex.schema.createTable(TableName.ProjectUserAdditionalPrivilege, (t) => {
t.uuid("id", { primaryKey: true }).defaultTo(knex.fn.uuid());
t.string("slug", 60).notNullable();
t.uuid("projectMembershipId").notNullable();
t.foreign("projectMembershipId").references("id").inTable(TableName.ProjectMembership).onDelete("CASCADE");
t.boolean("isTemporary").notNullable().defaultTo(false);
t.string("temporaryMode");
t.string("temporaryRange"); // could be cron or relative time like 1H or 1minute etc
t.datetime("temporaryAccessStartTime");
t.datetime("temporaryAccessEndTime");
t.jsonb("permissions").notNullable();
t.timestamps(true, true, true);
});
}
await createOnUpdateTrigger(knex, TableName.ProjectUserAdditionalPrivilege);
}
export async function down(knex: Knex): Promise<void> {
await dropOnUpdateTrigger(knex, TableName.ProjectUserAdditionalPrivilege);
await knex.schema.dropTableIfExists(TableName.ProjectUserAdditionalPrivilege);
}

View File

@@ -0,0 +1,32 @@
import { Knex } from "knex";
import { TableName } from "../schemas";
import { createOnUpdateTrigger, dropOnUpdateTrigger } from "../utils";
export async function up(knex: Knex): Promise<void> {
if (!(await knex.schema.hasTable(TableName.IdentityProjectAdditionalPrivilege))) {
await knex.schema.createTable(TableName.IdentityProjectAdditionalPrivilege, (t) => {
t.uuid("id", { primaryKey: true }).defaultTo(knex.fn.uuid());
t.string("slug", 60).notNullable();
t.uuid("projectMembershipId").notNullable();
t.foreign("projectMembershipId")
.references("id")
.inTable(TableName.IdentityProjectMembership)
.onDelete("CASCADE");
t.boolean("isTemporary").notNullable().defaultTo(false);
t.string("temporaryMode");
t.string("temporaryRange"); // could be cron or relative time like 1H or 1minute etc
t.datetime("temporaryAccessStartTime");
t.datetime("temporaryAccessEndTime");
t.jsonb("permissions").notNullable();
t.timestamps(true, true, true);
});
}
await createOnUpdateTrigger(knex, TableName.IdentityProjectAdditionalPrivilege);
}
export async function down(knex: Knex): Promise<void> {
await dropOnUpdateTrigger(knex, TableName.IdentityProjectAdditionalPrivilege);
await knex.schema.dropTableIfExists(TableName.IdentityProjectAdditionalPrivilege);
}

View File

@@ -0,0 +1,111 @@
import { Knex } from "knex";
import { z } from "zod";
import { TableName, TOrgMemberships } from "../schemas";
const validateOrgMembership = (membershipToValidate: TOrgMemberships, firstMembership: TOrgMemberships) => {
const firstOrgId = firstMembership.orgId;
const firstUserId = firstMembership.userId;
if (membershipToValidate.id === firstMembership.id) {
return;
}
if (membershipToValidate.inviteEmail !== firstMembership.inviteEmail) {
throw new Error(`Invite emails are different for the same userId and orgId: ${firstUserId}, ${firstOrgId}`);
}
if (membershipToValidate.orgId !== firstMembership.orgId) {
throw new Error(`OrgIds are different for the same userId and orgId: ${firstUserId}, ${firstOrgId}`);
}
if (membershipToValidate.role !== firstMembership.role) {
throw new Error(`Roles are different for the same userId and orgId: ${firstUserId}, ${firstOrgId}`);
}
if (membershipToValidate.roleId !== firstMembership.roleId) {
throw new Error(`RoleIds are different for the same userId and orgId: ${firstUserId}, ${firstOrgId}`);
}
if (membershipToValidate.status !== firstMembership.status) {
throw new Error(`Statuses are different for the same userId and orgId: ${firstUserId}, ${firstOrgId}`);
}
if (membershipToValidate.userId !== firstMembership.userId) {
throw new Error(`UserIds are different for the same userId and orgId: ${firstUserId}, ${firstOrgId}`);
}
};
export async function up(knex: Knex): Promise<void> {
const RowSchema = z.object({
userId: z.string(),
orgId: z.string(),
cnt: z.string()
});
// Transactional find and delete duplicate rows
await knex.transaction(async (tx) => {
const duplicateRows = await tx(TableName.OrgMembership)
.select("userId", "orgId") // Select the userId and orgId so we can group by them
.count("* as cnt") // Count the number of rows for each userId and orgId, so we can make sure there are more than 1 row (a duplicate)
.groupBy("userId", "orgId")
.havingRaw("count(*) > ?", [1]); // Using havingRaw for direct SQL expressions
// Parse the rows to ensure they are in the correct format, and for type safety
const parsedRows = RowSchema.array().parse(duplicateRows);
// For each of the duplicate rows, loop through and find the actual memberships to delete
for (const row of parsedRows) {
const count = Number(row.cnt);
// An extra check to ensure that the count is actually a number, and the number is greater than 2
if (typeof count !== "number" || count < 2) {
// eslint-disable-next-line no-continue
continue;
}
// Find all the organization memberships that have the same userId and orgId
// eslint-disable-next-line no-await-in-loop
const rowsToDelete = await tx(TableName.OrgMembership).where({
userId: row.userId,
orgId: row.orgId
});
// Ensure that all the rows have exactly the same value, except id, createdAt, updatedAt
for (const rowToDelete of rowsToDelete) {
validateOrgMembership(rowToDelete, rowsToDelete[0]);
}
// Find the row with the latest createdAt, which we will keep
let lowestCreatedAt: number | null = null;
let latestCreatedRow: TOrgMemberships | null = null;
for (const rowToDelete of rowsToDelete) {
if (lowestCreatedAt === null || rowToDelete.createdAt.getTime() < lowestCreatedAt) {
lowestCreatedAt = rowToDelete.createdAt.getTime();
latestCreatedRow = rowToDelete;
}
}
if (!latestCreatedRow) {
throw new Error("Failed to find last created membership");
}
// Filter out the latest row from the rows to delete
const membershipIdsToDelete = rowsToDelete.map((r) => r.id).filter((id) => id !== latestCreatedRow!.id);
// eslint-disable-next-line no-await-in-loop
const numberOfRowsDeleted = await tx(TableName.OrgMembership).whereIn("id", membershipIdsToDelete).delete();
// eslint-disable-next-line no-console
console.log(
`Deleted ${numberOfRowsDeleted} duplicate organization memberships for ${row.userId} and ${row.orgId}`
);
}
});
await knex.schema.alterTable(TableName.OrgMembership, (table) => {
table.unique(["userId", "orgId"]);
});
}
export async function down(knex: Knex): Promise<void> {
await knex.schema.alterTable(TableName.OrgMembership, (table) => {
table.dropUnique(["userId", "orgId"]);
});
}

View File

@@ -0,0 +1,24 @@
// Code generated by automation script, DO NOT EDIT.
// Automated by pulling database and generating zod schema
// To update. Just run npm run generate:schema
// Written by akhilmhdh.
import { z } from "zod";
import { TImmutableDBKeys } from "./models";
export const DynamicSecretLeasesSchema = z.object({
id: z.string().uuid(),
version: z.number(),
externalEntityId: z.string(),
expireAt: z.date(),
status: z.string().nullable().optional(),
statusDetails: z.string().nullable().optional(),
dynamicSecretId: z.string().uuid(),
createdAt: z.date(),
updatedAt: z.date()
});
export type TDynamicSecretLeases = z.infer<typeof DynamicSecretLeasesSchema>;
export type TDynamicSecretLeasesInsert = Omit<z.input<typeof DynamicSecretLeasesSchema>, TImmutableDBKeys>;
export type TDynamicSecretLeasesUpdate = Partial<Omit<z.input<typeof DynamicSecretLeasesSchema>, TImmutableDBKeys>>;

View File

@@ -0,0 +1,31 @@
// Code generated by automation script, DO NOT EDIT.
// Automated by pulling database and generating zod schema
// To update. Just run npm run generate:schema
// Written by akhilmhdh.
import { z } from "zod";
import { TImmutableDBKeys } from "./models";
export const DynamicSecretsSchema = z.object({
id: z.string().uuid(),
name: z.string(),
version: z.number(),
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()
});
export type TDynamicSecrets = z.infer<typeof DynamicSecretsSchema>;
export type TDynamicSecretsInsert = Omit<z.input<typeof DynamicSecretsSchema>, TImmutableDBKeys>;
export type TDynamicSecretsUpdate = Partial<Omit<z.input<typeof DynamicSecretsSchema>, TImmutableDBKeys>>;

View File

@@ -0,0 +1,31 @@
// Code generated by automation script, DO NOT EDIT.
// Automated by pulling database and generating zod schema
// To update. Just run npm run generate:schema
// Written by akhilmhdh.
import { z } from "zod";
import { TImmutableDBKeys } from "./models";
export const IdentityProjectAdditionalPrivilegeSchema = z.object({
id: z.string().uuid(),
slug: z.string(),
projectMembershipId: z.string().uuid(),
isTemporary: z.boolean().default(false),
temporaryMode: z.string().nullable().optional(),
temporaryRange: z.string().nullable().optional(),
temporaryAccessStartTime: z.date().nullable().optional(),
temporaryAccessEndTime: z.date().nullable().optional(),
permissions: z.unknown(),
createdAt: z.date(),
updatedAt: z.date()
});
export type TIdentityProjectAdditionalPrivilege = z.infer<typeof IdentityProjectAdditionalPrivilegeSchema>;
export type TIdentityProjectAdditionalPrivilegeInsert = Omit<
z.input<typeof IdentityProjectAdditionalPrivilegeSchema>,
TImmutableDBKeys
>;
export type TIdentityProjectAdditionalPrivilegeUpdate = Partial<
Omit<z.input<typeof IdentityProjectAdditionalPrivilegeSchema>, TImmutableDBKeys>
>;

View File

@@ -3,11 +3,14 @@ export * from "./audit-logs";
export * from "./auth-token-sessions"; export * from "./auth-token-sessions";
export * from "./auth-tokens"; export * from "./auth-tokens";
export * from "./backup-private-key"; export * from "./backup-private-key";
export * from "./dynamic-secret-leases";
export * from "./dynamic-secrets";
export * from "./git-app-install-sessions"; export * from "./git-app-install-sessions";
export * from "./git-app-org"; export * from "./git-app-org";
export * from "./identities"; export * from "./identities";
export * from "./identity-access-tokens"; export * from "./identity-access-tokens";
export * from "./identity-org-memberships"; export * from "./identity-org-memberships";
export * from "./identity-project-additional-privilege";
export * from "./identity-project-membership-role"; export * from "./identity-project-membership-role";
export * from "./identity-project-memberships"; export * from "./identity-project-memberships";
export * from "./identity-ua-client-secrets"; export * from "./identity-ua-client-secrets";
@@ -26,6 +29,7 @@ export * from "./project-environments";
export * from "./project-keys"; export * from "./project-keys";
export * from "./project-memberships"; export * from "./project-memberships";
export * from "./project-roles"; export * from "./project-roles";
export * from "./project-user-additional-privilege";
export * from "./project-user-membership-roles"; export * from "./project-user-membership-roles";
export * from "./projects"; export * from "./projects";
export * from "./saml-configs"; export * from "./saml-configs";

View File

@@ -20,6 +20,7 @@ export enum TableName {
Environment = "project_environments", Environment = "project_environments",
ProjectMembership = "project_memberships", ProjectMembership = "project_memberships",
ProjectRoles = "project_roles", ProjectRoles = "project_roles",
ProjectUserAdditionalPrivilege = "project_user_additional_privilege",
ProjectUserMembershipRole = "project_user_membership_roles", ProjectUserMembershipRole = "project_user_membership_roles",
ProjectKeys = "project_keys", ProjectKeys = "project_keys",
Secret = "secrets", Secret = "secrets",
@@ -43,6 +44,7 @@ export enum TableName {
IdentityOrgMembership = "identity_org_memberships", IdentityOrgMembership = "identity_org_memberships",
IdentityProjectMembership = "identity_project_memberships", IdentityProjectMembership = "identity_project_memberships",
IdentityProjectMembershipRole = "identity_project_membership_role", IdentityProjectMembershipRole = "identity_project_membership_role",
IdentityProjectAdditionalPrivilege = "identity_project_additional_privilege",
ScimToken = "scim_tokens", ScimToken = "scim_tokens",
SecretApprovalPolicy = "secret_approval_policies", SecretApprovalPolicy = "secret_approval_policies",
SecretApprovalPolicyApprover = "secret_approval_policies_approvers", SecretApprovalPolicyApprover = "secret_approval_policies_approvers",
@@ -59,6 +61,8 @@ export enum TableName {
GitAppOrg = "git_app_org", GitAppOrg = "git_app_org",
SecretScanningGitRisk = "secret_scanning_git_risks", SecretScanningGitRisk = "secret_scanning_git_risks",
TrustedIps = "trusted_ips", TrustedIps = "trusted_ips",
DynamicSecret = "dynamic_secrets",
DynamicSecretLease = "dynamic_secret_leases",
// junction tables with tags // junction tables with tags
JnSecretTag = "secret_tag_junction", JnSecretTag = "secret_tag_junction",
SecretVersionTag = "secret_version_tag_junction" SecretVersionTag = "secret_version_tag_junction"

View File

@@ -0,0 +1,31 @@
// Code generated by automation script, DO NOT EDIT.
// Automated by pulling database and generating zod schema
// To update. Just run npm run generate:schema
// Written by akhilmhdh.
import { z } from "zod";
import { TImmutableDBKeys } from "./models";
export const ProjectUserAdditionalPrivilegeSchema = z.object({
id: z.string().uuid(),
slug: z.string(),
projectMembershipId: z.string().uuid(),
isTemporary: z.boolean().default(false),
temporaryMode: z.string().nullable().optional(),
temporaryRange: z.string().nullable().optional(),
temporaryAccessStartTime: z.date().nullable().optional(),
temporaryAccessEndTime: z.date().nullable().optional(),
permissions: z.unknown(),
createdAt: z.date(),
updatedAt: z.date()
});
export type TProjectUserAdditionalPrivilege = z.infer<typeof ProjectUserAdditionalPrivilegeSchema>;
export type TProjectUserAdditionalPrivilegeInsert = Omit<
z.input<typeof ProjectUserAdditionalPrivilegeSchema>,
TImmutableDBKeys
>;
export type TProjectUserAdditionalPrivilegeUpdate = Partial<
Omit<z.input<typeof ProjectUserAdditionalPrivilegeSchema>, TImmutableDBKeys>
>;

View File

@@ -0,0 +1,184 @@
import ms from "ms";
import { z } from "zod";
import { DynamicSecretLeasesSchema } from "@app/db/schemas";
import { DYNAMIC_SECRET_LEASES } from "@app/lib/api-docs";
import { daysToMillisecond } from "@app/lib/dates";
import { removeTrailingSlash } from "@app/lib/fn";
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
import { SanitizedDynamicSecretSchema } from "@app/server/routes/sanitizedSchemas";
import { AuthMode } from "@app/services/auth/auth-type";
export const registerDynamicSecretLeaseRouter = async (server: FastifyZodProvider) => {
server.route({
url: "/",
method: "POST",
schema: {
body: z.object({
dynamicSecretName: z.string().min(1).describe(DYNAMIC_SECRET_LEASES.CREATE.dynamicSecretName).toLowerCase(),
projectSlug: z.string().min(1).describe(DYNAMIC_SECRET_LEASES.CREATE.projectSlug),
ttl: z
.string()
.optional()
.describe(DYNAMIC_SECRET_LEASES.CREATE.ttl)
.superRefine((val, ctx) => {
if (!val) return;
const valMs = ms(val);
if (valMs < 60 * 1000)
ctx.addIssue({ code: z.ZodIssueCode.custom, message: "TTL must be a greater than 1min" });
if (valMs > daysToMillisecond(1))
ctx.addIssue({ code: z.ZodIssueCode.custom, message: "TTL must be less than a day" });
}),
path: z.string().trim().default("/").transform(removeTrailingSlash).describe(DYNAMIC_SECRET_LEASES.CREATE.path),
environmentSlug: z.string().min(1).describe(DYNAMIC_SECRET_LEASES.CREATE.path)
}),
response: {
200: z.object({
lease: DynamicSecretLeasesSchema,
dynamicSecret: SanitizedDynamicSecretSchema,
data: z.unknown()
})
}
},
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
handler: async (req) => {
const { data, lease, dynamicSecret } = await server.services.dynamicSecretLease.create({
actor: req.permission.type,
actorId: req.permission.id,
actorAuthMethod: req.permission.authMethod,
actorOrgId: req.permission.orgId,
name: req.body.dynamicSecretName,
...req.body
});
return { lease, data, dynamicSecret };
}
});
server.route({
url: "/:leaseId",
method: "DELETE",
schema: {
params: z.object({
leaseId: z.string().min(1).describe(DYNAMIC_SECRET_LEASES.DELETE.leaseId)
}),
body: z.object({
projectSlug: z.string().min(1).describe(DYNAMIC_SECRET_LEASES.DELETE.projectSlug),
path: z
.string()
.min(1)
.trim()
.default("/")
.transform(removeTrailingSlash)
.describe(DYNAMIC_SECRET_LEASES.DELETE.path),
environmentSlug: z.string().min(1).describe(DYNAMIC_SECRET_LEASES.DELETE.environmentSlug),
isForced: z.boolean().default(false).describe(DYNAMIC_SECRET_LEASES.DELETE.isForced)
}),
response: {
200: z.object({
lease: DynamicSecretLeasesSchema
})
}
},
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
handler: async (req) => {
const lease = await server.services.dynamicSecretLease.revokeLease({
actor: req.permission.type,
actorId: req.permission.id,
actorAuthMethod: req.permission.authMethod,
actorOrgId: req.permission.orgId,
leaseId: req.params.leaseId,
...req.body
});
return { lease };
}
});
server.route({
url: "/:leaseId/renew",
method: "POST",
schema: {
params: z.object({
leaseId: z.string().min(1).describe(DYNAMIC_SECRET_LEASES.RENEW.leaseId)
}),
body: z.object({
ttl: z
.string()
.describe(DYNAMIC_SECRET_LEASES.RENEW.ttl)
.optional()
.superRefine((val, ctx) => {
if (!val) return;
const valMs = ms(val);
if (valMs < 60 * 1000)
ctx.addIssue({ code: z.ZodIssueCode.custom, message: "TTL must be a greater than 1min" });
if (valMs > daysToMillisecond(1))
ctx.addIssue({ code: z.ZodIssueCode.custom, message: "TTL must be less than a day" });
}),
projectSlug: z.string().min(1).describe(DYNAMIC_SECRET_LEASES.RENEW.projectSlug),
path: z
.string()
.min(1)
.trim()
.default("/")
.transform(removeTrailingSlash)
.describe(DYNAMIC_SECRET_LEASES.RENEW.path),
environmentSlug: z.string().min(1).describe(DYNAMIC_SECRET_LEASES.RENEW.ttl)
}),
response: {
200: z.object({
lease: DynamicSecretLeasesSchema
})
}
},
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
handler: async (req) => {
const lease = await server.services.dynamicSecretLease.renewLease({
actor: req.permission.type,
actorId: req.permission.id,
actorAuthMethod: req.permission.authMethod,
actorOrgId: req.permission.orgId,
leaseId: req.params.leaseId,
...req.body
});
return { lease };
}
});
server.route({
url: "/:leaseId",
method: "GET",
schema: {
params: z.object({
leaseId: z.string().min(1).describe(DYNAMIC_SECRET_LEASES.GET_BY_LEASEID.leaseId)
}),
querystring: z.object({
projectSlug: z.string().min(1).describe(DYNAMIC_SECRET_LEASES.GET_BY_LEASEID.projectSlug),
path: z
.string()
.trim()
.default("/")
.transform(removeTrailingSlash)
.describe(DYNAMIC_SECRET_LEASES.GET_BY_LEASEID.path),
environmentSlug: z.string().min(1).describe(DYNAMIC_SECRET_LEASES.GET_BY_LEASEID.environmentSlug)
}),
response: {
200: z.object({
lease: DynamicSecretLeasesSchema.extend({
dynamicSecret: SanitizedDynamicSecretSchema
})
})
}
},
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
handler: async (req) => {
const lease = await server.services.dynamicSecretLease.getLeaseDetails({
actor: req.permission.type,
actorId: req.permission.id,
actorAuthMethod: req.permission.authMethod,
actorOrgId: req.permission.orgId,
leaseId: req.params.leaseId,
...req.query
});
return { lease };
}
});
};

View File

@@ -0,0 +1,271 @@
import slugify from "@sindresorhus/slugify";
import ms from "ms";
import { z } from "zod";
import { DynamicSecretLeasesSchema } from "@app/db/schemas";
import { DynamicSecretProviderSchema } from "@app/ee/services/dynamic-secret/providers/models";
import { DYNAMIC_SECRETS } from "@app/lib/api-docs";
import { daysToMillisecond } from "@app/lib/dates";
import { removeTrailingSlash } from "@app/lib/fn";
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
import { SanitizedDynamicSecretSchema } from "@app/server/routes/sanitizedSchemas";
import { AuthMode } from "@app/services/auth/auth-type";
export const registerDynamicSecretRouter = async (server: FastifyZodProvider) => {
server.route({
url: "/",
method: "POST",
schema: {
body: z.object({
projectSlug: z.string().min(1).describe(DYNAMIC_SECRETS.CREATE.projectSlug),
provider: DynamicSecretProviderSchema.describe(DYNAMIC_SECRETS.CREATE.provider),
defaultTTL: z
.string()
.describe(DYNAMIC_SECRETS.CREATE.defaultTTL)
.superRefine((val, ctx) => {
const valMs = ms(val);
if (valMs < 60 * 1000)
ctx.addIssue({ code: z.ZodIssueCode.custom, message: "TTL must be a greater than 1min" });
if (valMs > daysToMillisecond(1))
ctx.addIssue({ code: z.ZodIssueCode.custom, message: "TTL must be less than a day" });
}),
maxTTL: z
.string()
.describe(DYNAMIC_SECRETS.CREATE.maxTTL)
.optional()
.superRefine((val, ctx) => {
if (!val) return;
const valMs = ms(val);
if (valMs < 60 * 1000)
ctx.addIssue({ code: z.ZodIssueCode.custom, message: "TTL must be a greater than 1min" });
if (valMs > daysToMillisecond(1))
ctx.addIssue({ code: z.ZodIssueCode.custom, message: "TTL must be less than a day" });
})
.nullable(),
path: z.string().describe(DYNAMIC_SECRETS.CREATE.path).trim().default("/").transform(removeTrailingSlash),
environmentSlug: z.string().describe(DYNAMIC_SECRETS.CREATE.environmentSlug).min(1),
name: z
.string()
.describe(DYNAMIC_SECRETS.CREATE.name)
.min(1)
.toLowerCase()
.max(64)
.refine((v) => slugify(v) === v, {
message: "Slug must be a valid"
})
}),
response: {
200: z.object({
dynamicSecret: SanitizedDynamicSecretSchema
})
}
},
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
handler: async (req) => {
const dynamicSecretCfg = await server.services.dynamicSecret.create({
actor: req.permission.type,
actorId: req.permission.id,
actorAuthMethod: req.permission.authMethod,
actorOrgId: req.permission.orgId,
...req.body
});
return { dynamicSecret: dynamicSecretCfg };
}
});
server.route({
url: "/:name",
method: "PATCH",
schema: {
params: z.object({
name: z.string().toLowerCase().describe(DYNAMIC_SECRETS.UPDATE.name)
}),
body: z.object({
projectSlug: z.string().min(1).describe(DYNAMIC_SECRETS.UPDATE.projectSlug),
path: z.string().trim().default("/").transform(removeTrailingSlash).describe(DYNAMIC_SECRETS.UPDATE.path),
environmentSlug: z.string().min(1).describe(DYNAMIC_SECRETS.UPDATE.environmentSlug),
data: z.object({
inputs: z.any().optional().describe(DYNAMIC_SECRETS.UPDATE.inputs),
defaultTTL: z
.string()
.describe(DYNAMIC_SECRETS.UPDATE.defaultTTL)
.optional()
.superRefine((val, ctx) => {
if (!val) return;
const valMs = ms(val);
if (valMs < 60 * 1000)
ctx.addIssue({ code: z.ZodIssueCode.custom, message: "TTL must be a greater than 1min" });
if (valMs > daysToMillisecond(1))
ctx.addIssue({ code: z.ZodIssueCode.custom, message: "TTL must be less than a day" });
}),
maxTTL: z
.string()
.describe(DYNAMIC_SECRETS.UPDATE.maxTTL)
.optional()
.superRefine((val, ctx) => {
if (!val) return;
const valMs = ms(val);
if (valMs < 60 * 1000)
ctx.addIssue({ code: z.ZodIssueCode.custom, message: "TTL must be a greater than 1min" });
if (valMs > daysToMillisecond(1))
ctx.addIssue({ code: z.ZodIssueCode.custom, message: "TTL must be less than a day" });
})
.nullable(),
newName: z.string().describe(DYNAMIC_SECRETS.UPDATE.newName).optional()
})
}),
response: {
200: z.object({
dynamicSecret: SanitizedDynamicSecretSchema
})
}
},
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
handler: async (req) => {
const dynamicSecretCfg = await server.services.dynamicSecret.updateByName({
actor: req.permission.type,
actorId: req.permission.id,
actorAuthMethod: req.permission.authMethod,
actorOrgId: req.permission.orgId,
name: req.params.name,
path: req.body.path,
projectSlug: req.body.projectSlug,
environmentSlug: req.body.environmentSlug,
...req.body.data
});
return { dynamicSecret: dynamicSecretCfg };
}
});
server.route({
url: "/:name",
method: "DELETE",
schema: {
params: z.object({
name: z.string().toLowerCase().describe(DYNAMIC_SECRETS.DELETE.name)
}),
body: z.object({
projectSlug: z.string().min(1).describe(DYNAMIC_SECRETS.DELETE.projectSlug),
path: z.string().trim().default("/").transform(removeTrailingSlash).describe(DYNAMIC_SECRETS.DELETE.path),
environmentSlug: z.string().min(1).describe(DYNAMIC_SECRETS.DELETE.environmentSlug),
isForced: z.boolean().default(false).describe(DYNAMIC_SECRETS.DELETE.isForced)
}),
response: {
200: z.object({
dynamicSecret: SanitizedDynamicSecretSchema
})
}
},
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
handler: async (req) => {
const dynamicSecretCfg = await server.services.dynamicSecret.deleteByName({
actor: req.permission.type,
actorId: req.permission.id,
actorAuthMethod: req.permission.authMethod,
actorOrgId: req.permission.orgId,
name: req.params.name,
...req.body
});
return { dynamicSecret: dynamicSecretCfg };
}
});
server.route({
url: "/:name",
method: "GET",
schema: {
params: z.object({
name: z.string().min(1).describe(DYNAMIC_SECRETS.GET_BY_NAME.name)
}),
querystring: z.object({
projectSlug: z.string().min(1).describe(DYNAMIC_SECRETS.GET_BY_NAME.projectSlug),
path: z.string().trim().default("/").transform(removeTrailingSlash).describe(DYNAMIC_SECRETS.GET_BY_NAME.path),
environmentSlug: z.string().min(1).describe(DYNAMIC_SECRETS.GET_BY_NAME.environmentSlug)
}),
response: {
200: z.object({
dynamicSecret: SanitizedDynamicSecretSchema.extend({
inputs: z.unknown()
})
})
}
},
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
handler: async (req) => {
const dynamicSecretCfg = await server.services.dynamicSecret.getDetails({
actor: req.permission.type,
actorId: req.permission.id,
actorAuthMethod: req.permission.authMethod,
actorOrgId: req.permission.orgId,
name: req.params.name,
...req.query
});
return { dynamicSecret: dynamicSecretCfg };
}
});
server.route({
url: "/",
method: "GET",
schema: {
querystring: z.object({
projectSlug: z.string().min(1).describe(DYNAMIC_SECRETS.LIST.projectSlug),
path: z.string().trim().default("/").transform(removeTrailingSlash).describe(DYNAMIC_SECRETS.LIST.path),
environmentSlug: z.string().min(1).describe(DYNAMIC_SECRETS.LIST.environmentSlug)
}),
response: {
200: z.object({
dynamicSecrets: SanitizedDynamicSecretSchema.array()
})
}
},
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
handler: async (req) => {
const dynamicSecretCfgs = await server.services.dynamicSecret.list({
actor: req.permission.type,
actorId: req.permission.id,
actorAuthMethod: req.permission.authMethod,
actorOrgId: req.permission.orgId,
...req.query
});
return { dynamicSecrets: dynamicSecretCfgs };
}
});
server.route({
url: "/:name/leases",
method: "GET",
schema: {
params: z.object({
name: z.string().min(1).describe(DYNAMIC_SECRETS.LIST_LEAES_BY_NAME.name)
}),
querystring: z.object({
projectSlug: z.string().min(1).describe(DYNAMIC_SECRETS.LIST_LEAES_BY_NAME.projectSlug),
path: z
.string()
.trim()
.default("/")
.transform(removeTrailingSlash)
.describe(DYNAMIC_SECRETS.LIST_LEAES_BY_NAME.path),
environmentSlug: z.string().min(1).describe(DYNAMIC_SECRETS.LIST_LEAES_BY_NAME.environmentSlug)
}),
response: {
200: z.object({
leases: DynamicSecretLeasesSchema.array()
})
}
},
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
handler: async (req) => {
const leases = await server.services.dynamicSecretLease.listLeases({
actor: req.permission.type,
actorId: req.permission.id,
actorAuthMethod: req.permission.authMethod,
actorOrgId: req.permission.orgId,
name: req.params.name,
...req.query
});
return { leases };
}
});
};

View File

@@ -0,0 +1,310 @@
import { MongoAbility, RawRuleOf } from "@casl/ability";
import { PackRule, packRules, unpackRules } from "@casl/ability/extra";
import slugify from "@sindresorhus/slugify";
import ms from "ms";
import { z } from "zod";
import { IdentityProjectAdditionalPrivilegeSchema } from "@app/db/schemas";
import { IdentityProjectAdditionalPrivilegeTemporaryMode } from "@app/ee/services/identity-project-additional-privilege/identity-project-additional-privilege-types";
import { ProjectPermissionSet } from "@app/ee/services/permission/project-permission";
import { IDENTITY_ADDITIONAL_PRIVILEGE } from "@app/lib/api-docs";
import { alphaNumericNanoId } from "@app/lib/nanoid";
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
import { AuthMode } from "@app/services/auth/auth-type";
export const registerIdentityProjectAdditionalPrivilegeRouter = async (server: FastifyZodProvider) => {
server.route({
url: "/permanent",
method: "POST",
schema: {
description: "Create a permanent or a non expiry specific privilege for identity.",
security: [
{
bearerAuth: []
}
],
body: z.object({
identityId: z.string().min(1).describe(IDENTITY_ADDITIONAL_PRIVILEGE.CREATE.identityId),
projectSlug: z.string().min(1).describe(IDENTITY_ADDITIONAL_PRIVILEGE.CREATE.projectSlug),
slug: z
.string()
.min(1)
.max(60)
.trim()
.refine((val) => val.toLowerCase() === val, "Must be lowercase")
.refine((v) => slugify(v) === v, {
message: "Slug must be a valid slug"
})
.optional()
.describe(IDENTITY_ADDITIONAL_PRIVILEGE.CREATE.slug),
permissions: z.any().array().describe(IDENTITY_ADDITIONAL_PRIVILEGE.CREATE.permissions)
}),
response: {
200: z.object({
privilege: IdentityProjectAdditionalPrivilegeSchema
})
}
},
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
handler: async (req) => {
const privilege = await server.services.identityProjectAdditionalPrivilege.create({
actorId: req.permission.id,
actor: req.permission.type,
actorOrgId: req.permission.orgId,
actorAuthMethod: req.permission.authMethod,
...req.body,
slug: req.body.slug ? slugify(req.body.slug) : slugify(alphaNumericNanoId(12)),
isTemporary: false,
permissions: JSON.stringify(packRules(req.body.permissions))
});
return { privilege };
}
});
server.route({
url: "/temporary",
method: "POST",
schema: {
description: "Create a temporary or a expiring specific privilege for identity.",
security: [
{
bearerAuth: []
}
],
body: z.object({
identityId: z.string().min(1).describe(IDENTITY_ADDITIONAL_PRIVILEGE.CREATE.identityId),
projectSlug: z.string().min(1).describe(IDENTITY_ADDITIONAL_PRIVILEGE.CREATE.projectSlug),
slug: z
.string()
.min(1)
.max(60)
.trim()
.refine((val) => val.toLowerCase() === val, "Must be lowercase")
.refine((v) => slugify(v) === v, {
message: "Slug must be a valid slug"
})
.optional()
.describe(IDENTITY_ADDITIONAL_PRIVILEGE.CREATE.slug),
permissions: z.any().array().describe(IDENTITY_ADDITIONAL_PRIVILEGE.CREATE.permissions),
temporaryMode: z
.nativeEnum(IdentityProjectAdditionalPrivilegeTemporaryMode)
.describe(IDENTITY_ADDITIONAL_PRIVILEGE.CREATE.temporaryMode),
temporaryRange: z
.string()
.refine((val) => ms(val) > 0, "Temporary range must be a positive number")
.describe(IDENTITY_ADDITIONAL_PRIVILEGE.CREATE.temporaryRange),
temporaryAccessStartTime: z
.string()
.datetime()
.describe(IDENTITY_ADDITIONAL_PRIVILEGE.CREATE.temporaryAccessStartTime)
}),
response: {
200: z.object({
privilege: IdentityProjectAdditionalPrivilegeSchema
})
}
},
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
handler: async (req) => {
const privilege = await server.services.identityProjectAdditionalPrivilege.create({
actorId: req.permission.id,
actor: req.permission.type,
actorOrgId: req.permission.orgId,
actorAuthMethod: req.permission.authMethod,
...req.body,
slug: req.body.slug ? slugify(req.body.slug) : slugify(alphaNumericNanoId(12)),
isTemporary: true,
permissions: JSON.stringify(packRules(req.body.permissions))
});
return { privilege };
}
});
server.route({
url: "/",
method: "PATCH",
schema: {
description: "Update a specific privilege of an identity.",
security: [
{
bearerAuth: []
}
],
body: z.object({
// disallow empty string
privilegeSlug: z.string().min(1).describe(IDENTITY_ADDITIONAL_PRIVILEGE.UPDATE.slug),
identityId: z.string().min(1).describe(IDENTITY_ADDITIONAL_PRIVILEGE.UPDATE.identityId),
projectSlug: z.string().min(1).describe(IDENTITY_ADDITIONAL_PRIVILEGE.UPDATE.projectSlug),
privilegeDetails: z
.object({
slug: z
.string()
.min(1)
.max(60)
.trim()
.refine((val) => val.toLowerCase() === val, "Must be lowercase")
.refine((v) => slugify(v) === v, {
message: "Slug must be a valid slug"
})
.describe(IDENTITY_ADDITIONAL_PRIVILEGE.UPDATE.newSlug),
permissions: z.any().array().describe(IDENTITY_ADDITIONAL_PRIVILEGE.UPDATE.permissions),
isTemporary: z.boolean().describe(IDENTITY_ADDITIONAL_PRIVILEGE.UPDATE.isTemporary),
temporaryMode: z
.nativeEnum(IdentityProjectAdditionalPrivilegeTemporaryMode)
.describe(IDENTITY_ADDITIONAL_PRIVILEGE.UPDATE.temporaryMode),
temporaryRange: z
.string()
.refine((val) => ms(val) > 0, "Temporary range must be a positive number")
.describe(IDENTITY_ADDITIONAL_PRIVILEGE.UPDATE.temporaryRange),
temporaryAccessStartTime: z
.string()
.datetime()
.describe(IDENTITY_ADDITIONAL_PRIVILEGE.UPDATE.temporaryAccessStartTime)
})
.partial()
}),
response: {
200: z.object({
privilege: IdentityProjectAdditionalPrivilegeSchema
})
}
},
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
handler: async (req) => {
const updatedInfo = req.body.privilegeDetails;
const privilege = await server.services.identityProjectAdditionalPrivilege.updateBySlug({
actorId: req.permission.id,
actor: req.permission.type,
actorOrgId: req.permission.orgId,
actorAuthMethod: req.permission.authMethod,
slug: req.body.privilegeSlug,
identityId: req.body.identityId,
projectSlug: req.body.projectSlug,
data: {
...updatedInfo,
permissions: updatedInfo?.permissions ? JSON.stringify(packRules(updatedInfo.permissions)) : undefined
}
});
return { privilege };
}
});
server.route({
url: "/",
method: "DELETE",
schema: {
description: "Delete a specific privilege of an identity.",
security: [
{
bearerAuth: []
}
],
body: z.object({
privilegeSlug: z.string().min(1).describe(IDENTITY_ADDITIONAL_PRIVILEGE.DELETE.slug),
identityId: z.string().min(1).describe(IDENTITY_ADDITIONAL_PRIVILEGE.DELETE.identityId),
projectSlug: z.string().min(1).describe(IDENTITY_ADDITIONAL_PRIVILEGE.DELETE.projectSlug)
}),
response: {
200: z.object({
privilege: IdentityProjectAdditionalPrivilegeSchema
})
}
},
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
handler: async (req) => {
const privilege = await server.services.identityProjectAdditionalPrivilege.deleteBySlug({
actorId: req.permission.id,
actor: req.permission.type,
actorAuthMethod: req.permission.authMethod,
actorOrgId: req.permission.orgId,
slug: req.body.privilegeSlug,
identityId: req.body.identityId,
projectSlug: req.body.projectSlug
});
return { privilege };
}
});
server.route({
url: "/:privilegeSlug",
method: "GET",
schema: {
description: "Retrieve details of a specific privilege by privilege slug.",
security: [
{
bearerAuth: []
}
],
params: z.object({
privilegeSlug: z.string().min(1).describe(IDENTITY_ADDITIONAL_PRIVILEGE.GET_BY_SLUG.slug)
}),
querystring: z.object({
identityId: z.string().min(1).describe(IDENTITY_ADDITIONAL_PRIVILEGE.GET_BY_SLUG.identityId),
projectSlug: z.string().min(1).describe(IDENTITY_ADDITIONAL_PRIVILEGE.GET_BY_SLUG.projectSlug)
}),
response: {
200: z.object({
privilege: IdentityProjectAdditionalPrivilegeSchema
})
}
},
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
handler: async (req) => {
const privilege = await server.services.identityProjectAdditionalPrivilege.getPrivilegeDetailsBySlug({
actorId: req.permission.id,
actorAuthMethod: req.permission.authMethod,
actor: req.permission.type,
actorOrgId: req.permission.orgId,
slug: req.params.privilegeSlug,
...req.query
});
return { privilege };
}
});
server.route({
url: "/",
method: "GET",
schema: {
description: "List of a specific privilege of an identity in a project.",
security: [
{
bearerAuth: []
}
],
querystring: z.object({
identityId: z.string().min(1).describe(IDENTITY_ADDITIONAL_PRIVILEGE.LIST.identityId),
projectSlug: z.string().min(1).describe(IDENTITY_ADDITIONAL_PRIVILEGE.LIST.projectSlug),
unpacked: z
.enum(["false", "true"])
.transform((el) => el === "true")
.default("true")
.describe(IDENTITY_ADDITIONAL_PRIVILEGE.LIST.unpacked)
}),
response: {
200: z.object({
privileges: IdentityProjectAdditionalPrivilegeSchema.array()
})
}
},
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
handler: async (req) => {
const privileges = await server.services.identityProjectAdditionalPrivilege.listIdentityProjectPrivileges({
actorId: req.permission.id,
actor: req.permission.type,
actorAuthMethod: req.permission.authMethod,
actorOrgId: req.permission.orgId,
...req.query
});
if (req.query.unpacked) {
return {
privileges: privileges.map(({ permissions, ...el }) => ({
...el,
permissions: unpackRules(permissions as PackRule<RawRuleOf<MongoAbility<ProjectPermissionSet>>>[])
}))
};
}
return { privileges };
}
});
};

View File

@@ -1,3 +1,6 @@
import { registerDynamicSecretLeaseRouter } from "./dynamic-secret-lease-router";
import { registerDynamicSecretRouter } from "./dynamic-secret-router";
import { registerIdentityProjectAdditionalPrivilegeRouter } from "./identity-project-additional-privilege-router";
import { registerLdapRouter } from "./ldap-router"; import { registerLdapRouter } from "./ldap-router";
import { registerLicenseRouter } from "./license-router"; import { registerLicenseRouter } from "./license-router";
import { registerOrgRoleRouter } from "./org-role-router"; import { registerOrgRoleRouter } from "./org-role-router";
@@ -13,6 +16,7 @@ import { registerSecretScanningRouter } from "./secret-scanning-router";
import { registerSecretVersionRouter } from "./secret-version-router"; import { registerSecretVersionRouter } from "./secret-version-router";
import { registerSnapshotRouter } from "./snapshot-router"; import { registerSnapshotRouter } from "./snapshot-router";
import { registerTrustedIpRouter } from "./trusted-ip-router"; import { registerTrustedIpRouter } from "./trusted-ip-router";
import { registerUserAdditionalPrivilegeRouter } from "./user-additional-privilege-router";
export const registerV1EERoutes = async (server: FastifyZodProvider) => { export const registerV1EERoutes = async (server: FastifyZodProvider) => {
// org role starts with organization // org role starts with organization
@@ -34,10 +38,26 @@ export const registerV1EERoutes = async (server: FastifyZodProvider) => {
await server.register(registerSecretRotationProviderRouter, { await server.register(registerSecretRotationProviderRouter, {
prefix: "/secret-rotation-providers" prefix: "/secret-rotation-providers"
}); });
await server.register(
async (dynamicSecretRouter) => {
await dynamicSecretRouter.register(registerDynamicSecretRouter);
await dynamicSecretRouter.register(registerDynamicSecretLeaseRouter, { prefix: "/leases" });
},
{ prefix: "/dynamic-secrets" }
);
await server.register(registerSamlRouter, { prefix: "/sso" }); await server.register(registerSamlRouter, { prefix: "/sso" });
await server.register(registerScimRouter, { prefix: "/scim" }); await server.register(registerScimRouter, { prefix: "/scim" });
await server.register(registerLdapRouter, { prefix: "/ldap" }); await server.register(registerLdapRouter, { prefix: "/ldap" });
await server.register(registerSecretScanningRouter, { prefix: "/secret-scanning" }); await server.register(registerSecretScanningRouter, { prefix: "/secret-scanning" });
await server.register(registerSecretRotationRouter, { prefix: "/secret-rotations" }); await server.register(registerSecretRotationRouter, { prefix: "/secret-rotations" });
await server.register(registerSecretVersionRouter, { prefix: "/secret" }); await server.register(registerSecretVersionRouter, { prefix: "/secret" });
await server.register(
async (privilegeRouter) => {
await privilegeRouter.register(registerUserAdditionalPrivilegeRouter, { prefix: "/users" });
await privilegeRouter.register(registerIdentityProjectAdditionalPrivilegeRouter, { prefix: "/identity" });
},
{ prefix: "/additional-privilege" }
);
}; };

View File

@@ -19,7 +19,7 @@ export const registerOrgRoleRouter = async (server: FastifyZodProvider) => {
.min(1) .min(1)
.trim() .trim()
.refine( .refine(
(val) => Object.keys(OrgMembershipRole).includes(val), (val) => !Object.keys(OrgMembershipRole).includes(val),
"Please choose a different slug, the slug you have entered is reserved" "Please choose a different slug, the slug you have entered is reserved"
) )
.refine((v) => slugify(v) === v, { .refine((v) => slugify(v) === v, {

View File

@@ -146,7 +146,7 @@ export const registerScimRouter = async (server: FastifyZodProvider) => {
offset: req.query.startIndex, offset: req.query.startIndex,
limit: req.query.count, limit: req.query.count,
filter: req.query.filter, filter: req.query.filter,
orgId: req.permission.orgId as string orgId: req.permission.orgId
}); });
return users; return users;
} }
@@ -184,7 +184,7 @@ export const registerScimRouter = async (server: FastifyZodProvider) => {
handler: async (req) => { handler: async (req) => {
const user = await req.server.services.scim.getScimUser({ const user = await req.server.services.scim.getScimUser({
userId: req.params.userId, userId: req.params.userId,
orgId: req.permission.orgId as string orgId: req.permission.orgId
}); });
return user; return user;
} }
@@ -243,7 +243,7 @@ export const registerScimRouter = async (server: FastifyZodProvider) => {
email: primaryEmail, email: primaryEmail,
firstName: req.body.name.givenName, firstName: req.body.name.givenName,
lastName: req.body.name.familyName, lastName: req.body.name.familyName,
orgId: req.permission.orgId as string orgId: req.permission.orgId
}); });
return user; return user;
@@ -280,7 +280,7 @@ export const registerScimRouter = async (server: FastifyZodProvider) => {
handler: async (req) => { handler: async (req) => {
const user = await req.server.services.scim.updateScimUser({ const user = await req.server.services.scim.updateScimUser({
userId: req.params.userId, userId: req.params.userId,
orgId: req.permission.orgId as string, orgId: req.permission.orgId,
operations: req.body.Operations operations: req.body.Operations
}); });
return user; return user;
@@ -330,7 +330,7 @@ export const registerScimRouter = async (server: FastifyZodProvider) => {
handler: async (req) => { handler: async (req) => {
const user = await req.server.services.scim.replaceScimUser({ const user = await req.server.services.scim.replaceScimUser({
userId: req.params.userId, userId: req.params.userId,
orgId: req.permission.orgId as string, orgId: req.permission.orgId,
active: req.body.active active: req.body.active
}); });
return user; return user;

View File

@@ -0,0 +1,237 @@
import slugify from "@sindresorhus/slugify";
import ms from "ms";
import { z } from "zod";
import { ProjectUserAdditionalPrivilegeSchema } from "@app/db/schemas";
import { ProjectUserAdditionalPrivilegeTemporaryMode } from "@app/ee/services/project-user-additional-privilege/project-user-additional-privilege-types";
import { PROJECT_USER_ADDITIONAL_PRIVILEGE } from "@app/lib/api-docs";
import { alphaNumericNanoId } from "@app/lib/nanoid";
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
import { AuthMode } from "@app/services/auth/auth-type";
export const registerUserAdditionalPrivilegeRouter = async (server: FastifyZodProvider) => {
server.route({
url: "/permanent",
method: "POST",
schema: {
body: z.object({
projectMembershipId: z.string().min(1).describe(PROJECT_USER_ADDITIONAL_PRIVILEGE.CREATE.projectMembershipId),
slug: z
.string()
.min(1)
.max(60)
.trim()
.refine((v) => v.toLowerCase() === v, "Slug must be lowercase")
.refine((v) => slugify(v) === v, {
message: "Slug must be a valid slug"
})
.optional()
.describe(PROJECT_USER_ADDITIONAL_PRIVILEGE.CREATE.slug),
permissions: z.any().array().describe(PROJECT_USER_ADDITIONAL_PRIVILEGE.CREATE.permissions)
}),
response: {
200: z.object({
privilege: ProjectUserAdditionalPrivilegeSchema
})
}
},
onRequest: verifyAuth([AuthMode.JWT]),
handler: async (req) => {
const privilege = await server.services.projectUserAdditionalPrivilege.create({
actorId: req.permission.id,
actor: req.permission.type,
actorOrgId: req.permission.orgId,
actorAuthMethod: req.permission.authMethod,
...req.body,
slug: req.body.slug ? slugify(req.body.slug) : slugify(alphaNumericNanoId(12)),
isTemporary: false,
permissions: JSON.stringify(req.body.permissions)
});
return { privilege };
}
});
server.route({
url: "/temporary",
method: "POST",
schema: {
body: z.object({
projectMembershipId: z.string().min(1).describe(PROJECT_USER_ADDITIONAL_PRIVILEGE.CREATE.projectMembershipId),
slug: z
.string()
.min(1)
.max(60)
.trim()
.refine((v) => v.toLowerCase() === v, "Slug must be lowercase")
.refine((v) => slugify(v) === v, {
message: "Slug must be a valid slug"
})
.optional()
.describe(PROJECT_USER_ADDITIONAL_PRIVILEGE.CREATE.slug),
permissions: z.any().array().describe(PROJECT_USER_ADDITIONAL_PRIVILEGE.CREATE.permissions),
temporaryMode: z
.nativeEnum(ProjectUserAdditionalPrivilegeTemporaryMode)
.describe(PROJECT_USER_ADDITIONAL_PRIVILEGE.CREATE.temporaryMode),
temporaryRange: z
.string()
.refine((val) => ms(val) > 0, "Temporary range must be a positive number")
.describe(PROJECT_USER_ADDITIONAL_PRIVILEGE.CREATE.temporaryRange),
temporaryAccessStartTime: z
.string()
.datetime()
.describe(PROJECT_USER_ADDITIONAL_PRIVILEGE.CREATE.temporaryAccessStartTime)
}),
response: {
200: z.object({
privilege: ProjectUserAdditionalPrivilegeSchema
})
}
},
onRequest: verifyAuth([AuthMode.JWT]),
handler: async (req) => {
const privilege = await server.services.projectUserAdditionalPrivilege.create({
actorId: req.permission.id,
actor: req.permission.type,
actorOrgId: req.permission.orgId,
actorAuthMethod: req.permission.authMethod,
...req.body,
slug: req.body.slug ? slugify(req.body.slug) : `privilege-${slugify(alphaNumericNanoId(12))}`,
isTemporary: true,
permissions: JSON.stringify(req.body.permissions)
});
return { privilege };
}
});
server.route({
url: "/:privilegeId",
method: "PATCH",
schema: {
params: z.object({
privilegeId: z.string().min(1).describe(PROJECT_USER_ADDITIONAL_PRIVILEGE.UPDATE.privilegeId)
}),
body: z
.object({
slug: z
.string()
.max(60)
.trim()
.refine((v) => v.toLowerCase() === v, "Slug must be lowercase")
.refine((v) => slugify(v) === v, {
message: "Slug must be a valid slug"
})
.describe(PROJECT_USER_ADDITIONAL_PRIVILEGE.UPDATE.slug),
permissions: z.any().array().describe(PROJECT_USER_ADDITIONAL_PRIVILEGE.UPDATE.permissions),
isTemporary: z.boolean().describe(PROJECT_USER_ADDITIONAL_PRIVILEGE.UPDATE.isTemporary),
temporaryMode: z
.nativeEnum(ProjectUserAdditionalPrivilegeTemporaryMode)
.describe(PROJECT_USER_ADDITIONAL_PRIVILEGE.UPDATE.temporaryMode),
temporaryRange: z
.string()
.refine((val) => ms(val) > 0, "Temporary range must be a positive number")
.describe(PROJECT_USER_ADDITIONAL_PRIVILEGE.UPDATE.temporaryRange),
temporaryAccessStartTime: z
.string()
.datetime()
.describe(PROJECT_USER_ADDITIONAL_PRIVILEGE.UPDATE.temporaryAccessStartTime)
})
.partial(),
response: {
200: z.object({
privilege: ProjectUserAdditionalPrivilegeSchema
})
}
},
onRequest: verifyAuth([AuthMode.JWT]),
handler: async (req) => {
const privilege = await server.services.projectUserAdditionalPrivilege.updateById({
actorId: req.permission.id,
actor: req.permission.type,
actorOrgId: req.permission.orgId,
actorAuthMethod: req.permission.authMethod,
...req.body,
permissions: req.body.permissions ? JSON.stringify(req.body.permissions) : undefined,
privilegeId: req.params.privilegeId
});
return { privilege };
}
});
server.route({
url: "/:privilegeId",
method: "DELETE",
schema: {
params: z.object({
privilegeId: z.string().describe(PROJECT_USER_ADDITIONAL_PRIVILEGE.DELETE.privilegeId)
}),
response: {
200: z.object({
privilege: ProjectUserAdditionalPrivilegeSchema
})
}
},
onRequest: verifyAuth([AuthMode.JWT]),
handler: async (req) => {
const privilege = await server.services.projectUserAdditionalPrivilege.deleteById({
actorId: req.permission.id,
actor: req.permission.type,
actorOrgId: req.permission.orgId,
actorAuthMethod: req.permission.authMethod,
privilegeId: req.params.privilegeId
});
return { privilege };
}
});
server.route({
url: "/",
method: "GET",
schema: {
querystring: z.object({
projectMembershipId: z.string().describe(PROJECT_USER_ADDITIONAL_PRIVILEGE.LIST.projectMembershipId)
}),
response: {
200: z.object({
privileges: ProjectUserAdditionalPrivilegeSchema.array()
})
}
},
onRequest: verifyAuth([AuthMode.JWT]),
handler: async (req) => {
const privileges = await server.services.projectUserAdditionalPrivilege.listPrivileges({
actorId: req.permission.id,
actor: req.permission.type,
actorOrgId: req.permission.orgId,
actorAuthMethod: req.permission.authMethod,
projectMembershipId: req.query.projectMembershipId
});
return { privileges };
}
});
server.route({
url: "/:privilegeId",
method: "GET",
schema: {
params: z.object({
privilegeId: z.string().describe(PROJECT_USER_ADDITIONAL_PRIVILEGE.GET_BY_PRIVILEGEID.privilegeId)
}),
response: {
200: z.object({
privilege: ProjectUserAdditionalPrivilegeSchema
})
}
},
onRequest: verifyAuth([AuthMode.JWT]),
handler: async (req) => {
const privilege = await server.services.projectUserAdditionalPrivilege.getPrivilegeDetailsById({
actorId: req.permission.id,
actor: req.permission.type,
actorOrgId: req.permission.orgId,
actorAuthMethod: req.permission.authMethod,
privilegeId: req.params.privilegeId
});
return { privilege };
}
});
};

View File

@@ -0,0 +1,80 @@
import { Knex } from "knex";
import { TDbClient } from "@app/db";
import { DynamicSecretLeasesSchema, TableName } from "@app/db/schemas";
import { DatabaseError } from "@app/lib/errors";
import { ormify, selectAllTableCols } from "@app/lib/knex";
export type TDynamicSecretLeaseDALFactory = ReturnType<typeof dynamicSecretLeaseDALFactory>;
export const dynamicSecretLeaseDALFactory = (db: TDbClient) => {
const orm = ormify(db, TableName.DynamicSecretLease);
const countLeasesForDynamicSecret = async (dynamicSecretId: string, tx?: Knex) => {
try {
const doc = await (tx || db)(TableName.DynamicSecretLease).count("*").where({ dynamicSecretId }).first();
return parseInt(doc || "0", 10);
} catch (error) {
throw new DatabaseError({ error, name: "DynamicSecretCountLeases" });
}
};
const findById = async (id: string, tx?: Knex) => {
try {
const doc = await (tx || db)(TableName.DynamicSecretLease)
.where({ [`${TableName.DynamicSecretLease}.id` as "id"]: id })
.first()
.join(
TableName.DynamicSecret,
`${TableName.DynamicSecretLease}.dynamicSecretId`,
`${TableName.DynamicSecret}.id`
)
.select(selectAllTableCols(TableName.DynamicSecretLease))
.select(
db.ref("id").withSchema(TableName.DynamicSecret).as("dynId"),
db.ref("name").withSchema(TableName.DynamicSecret).as("dynName"),
db.ref("version").withSchema(TableName.DynamicSecret).as("dynVersion"),
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("createdAt").withSchema(TableName.DynamicSecret).as("dynCreatedAt"),
db.ref("updatedAt").withSchema(TableName.DynamicSecret).as("dynUpdatedAt")
);
if (!doc) return;
return {
...DynamicSecretLeasesSchema.parse(doc),
dynamicSecret: {
id: doc.dynId,
name: doc.dynName,
version: doc.dynVersion,
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
}
};
} catch (error) {
throw new DatabaseError({ error, name: "DynamicSecretLeaseFindById" });
}
};
return { ...orm, findById, countLeasesForDynamicSecret };
};

View File

@@ -0,0 +1,159 @@
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 { TDynamicSecretDALFactory } from "../dynamic-secret/dynamic-secret-dal";
import { DynamicSecretStatus } from "../dynamic-secret/dynamic-secret-types";
import { DynamicSecretProviders, TDynamicProviderFns } from "../dynamic-secret/providers/models";
import { TDynamicSecretLeaseDALFactory } from "./dynamic-secret-lease-dal";
type TDynamicSecretLeaseQueueServiceFactoryDep = {
queueService: TQueueServiceFactory;
dynamicSecretLeaseDAL: Pick<TDynamicSecretLeaseDALFactory, "findById" | "deleteById" | "find" | "updateById">;
dynamicSecretDAL: Pick<TDynamicSecretDALFactory, "findById" | "deleteById" | "updateById">;
dynamicSecretProviders: Record<DynamicSecretProviders, TDynamicProviderFns>;
};
export type TDynamicSecretLeaseQueueServiceFactory = ReturnType<typeof dynamicSecretLeaseQueueServiceFactory>;
export const dynamicSecretLeaseQueueServiceFactory = ({
queueService,
dynamicSecretDAL,
dynamicSecretProviders,
dynamicSecretLeaseDAL
}: TDynamicSecretLeaseQueueServiceFactoryDep) => {
const pruneDynamicSecret = async (dynamicSecretCfgId: string) => {
await queueService.queue(
QueueName.DynamicSecretRevocation,
QueueJobs.DynamicSecretPruning,
{ dynamicSecretCfgId },
{
jobId: dynamicSecretCfgId,
backoff: {
type: "exponential",
delay: 3000
},
removeOnFail: {
count: 3
},
removeOnComplete: true
}
);
};
const setLeaseRevocation = async (leaseId: string, expiry: number) => {
await queueService.queue(
QueueName.DynamicSecretRevocation,
QueueJobs.DynamicSecretRevocation,
{ leaseId },
{
jobId: leaseId,
backoff: {
type: "exponential",
delay: 3000
},
delay: expiry,
removeOnFail: {
count: 3
},
removeOnComplete: true
}
);
};
const unsetLeaseRevocation = async (leaseId: string) => {
await queueService.stopJobById(QueueName.DynamicSecretRevocation, leaseId);
};
queueService.start(QueueName.DynamicSecretRevocation, async (job) => {
try {
if (job.name === QueueJobs.DynamicSecretRevocation) {
const { leaseId } = job.data as { leaseId: string };
logger.info("Dynamic secret lease revocation started: ", leaseId, job.id);
const dynamicSecretLease = await dynamicSecretLeaseDAL.findById(leaseId);
if (!dynamicSecretLease) throw new DisableRotationErrors({ message: "Dynamic secret lease not found" });
const dynamicSecretCfg = dynamicSecretLease.dynamicSecret;
const selectedProvider = dynamicSecretProviders[dynamicSecretCfg.type as DynamicSecretProviders];
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);
return;
}
if (job.name === QueueJobs.DynamicSecretPruning) {
const { dynamicSecretCfgId } = job.data as { dynamicSecretCfgId: string };
logger.info("Dynamic secret pruning started: ", dynamicSecretCfgId, job.id);
const dynamicSecretCfg = await dynamicSecretDAL.findById(dynamicSecretCfgId);
if (!dynamicSecretCfg) throw new DisableRotationErrors({ message: "Dynamic secret not found" });
if ((dynamicSecretCfg.status as DynamicSecretStatus) !== DynamicSecretStatus.Deleting)
throw new DisableRotationErrors({ message: "Document not deleted" });
const dynamicSecretLeases = await dynamicSecretLeaseDAL.find({ dynamicSecretId: dynamicSecretCfgId });
if (dynamicSecretLeases.length) {
const selectedProvider = dynamicSecretProviders[dynamicSecretCfg.type as DynamicSecretProviders];
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(
dynamicSecretLeases.map(({ externalEntityId }) =>
selectedProvider.revoke(decryptedStoredInput, externalEntityId)
)
);
}
await dynamicSecretDAL.deleteById(dynamicSecretCfgId);
}
logger.info("Finished dynamic secret job", job.id);
} catch (error) {
logger.error(error);
if (job?.name === QueueJobs.DynamicSecretPruning) {
const { dynamicSecretCfgId } = job.data as { dynamicSecretCfgId: string };
await dynamicSecretDAL.updateById(dynamicSecretCfgId, {
status: DynamicSecretStatus.FailedDeletion,
statusDetails: (error as Error)?.message?.slice(0, 255)
});
}
if (job?.name === QueueJobs.DynamicSecretRevocation) {
const { leaseId } = job.data as { leaseId: string };
await dynamicSecretLeaseDAL.updateById(leaseId, {
status: DynamicSecretStatus.FailedDeletion,
statusDetails: (error as Error)?.message?.slice(0, 255)
});
}
if (error instanceof DisableRotationErrors) {
if (job.id) {
await queueService.stopRepeatableJobByJobId(QueueName.DynamicSecretRevocation, job.id);
}
}
// propogate to next part
throw error;
}
});
return {
pruneDynamicSecret,
setLeaseRevocation,
unsetLeaseRevocation
};
};

View File

@@ -0,0 +1,343 @@
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 { TProjectDALFactory } from "@app/services/project/project-dal";
import { TSecretFolderDALFactory } from "@app/services/secret-folder/secret-folder-dal";
import { TDynamicSecretDALFactory } from "../dynamic-secret/dynamic-secret-dal";
import { DynamicSecretProviders, TDynamicProviderFns } from "../dynamic-secret/providers/models";
import { TDynamicSecretLeaseDALFactory } from "./dynamic-secret-lease-dal";
import { TDynamicSecretLeaseQueueServiceFactory } from "./dynamic-secret-lease-queue";
import {
DynamicSecretLeaseStatus,
TCreateDynamicSecretLeaseDTO,
TDeleteDynamicSecretLeaseDTO,
TDetailsDynamicSecretLeaseDTO,
TListDynamicSecretLeasesDTO,
TRenewDynamicSecretLeaseDTO
} from "./dynamic-secret-lease-types";
type TDynamicSecretLeaseServiceFactoryDep = {
dynamicSecretLeaseDAL: TDynamicSecretLeaseDALFactory;
dynamicSecretDAL: Pick<TDynamicSecretDALFactory, "findOne">;
dynamicSecretProviders: Record<DynamicSecretProviders, TDynamicProviderFns>;
dynamicSecretQueueService: TDynamicSecretLeaseQueueServiceFactory;
licenseService: Pick<TLicenseServiceFactory, "getPlan">;
folderDAL: Pick<TSecretFolderDALFactory, "findBySecretPath">;
permissionService: Pick<TPermissionServiceFactory, "getProjectPermission">;
projectDAL: Pick<TProjectDALFactory, "findProjectBySlug">;
};
export type TDynamicSecretLeaseServiceFactory = ReturnType<typeof dynamicSecretLeaseServiceFactory>;
export const dynamicSecretLeaseServiceFactory = ({
dynamicSecretLeaseDAL,
dynamicSecretProviders,
dynamicSecretDAL,
folderDAL,
permissionService,
dynamicSecretQueueService,
projectDAL,
licenseService
}: TDynamicSecretLeaseServiceFactoryDep) => {
const create = async ({
environmentSlug,
path,
name,
projectSlug,
actor,
actorId,
actorOrgId,
actorAuthMethod,
ttl
}: TCreateDynamicSecretLeaseDTO) => {
const appCfg = getConfig();
const project = await projectDAL.findProjectBySlug(projectSlug, actorOrgId);
if (!project) throw new BadRequestError({ message: "Project not found" });
const projectId = project.id;
const { permission } = await permissionService.getProjectPermission(
actor,
actorId,
projectId,
actorAuthMethod,
actorOrgId
);
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Read,
subject(ProjectPermissionSub.Secrets, { environment: environmentSlug, secretPath: path })
);
const plan = await licenseService.getPlan(actorOrgId);
if (!plan?.dynamicSecret) {
throw new BadRequestError({
message: "Failed to create lease due to plan restriction. Upgrade plan to create dynamic secret."
});
}
const folder = await folderDAL.findBySecretPath(projectId, environmentSlug, path);
if (!folder) throw new BadRequestError({ message: "Folder not found" });
const dynamicSecretCfg = await dynamicSecretDAL.findOne({ name, folderId: folder.id });
if (!dynamicSecretCfg) throw new BadRequestError({ message: "Dynamic secret not found" });
const totalLeasesTaken = await dynamicSecretLeaseDAL.countLeasesForDynamicSecret(dynamicSecretCfg.id);
if (totalLeasesTaken >= appCfg.MAX_LEASE_LIMIT)
throw new BadRequestError({ message: `Max lease limit reached. Limit: ${appCfg.MAX_LEASE_LIMIT}` });
const selectedProvider = dynamicSecretProviders[dynamicSecretCfg.type as DynamicSecretProviders];
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;
const expireAt = new Date(new Date().getTime() + ms(selectedTTL));
if (maxTTL) {
const maxExpiryDate = new Date(new Date().getTime() + ms(maxTTL));
if (expireAt > maxExpiryDate) throw new BadRequestError({ message: "TTL cannot be larger than max TTL" });
}
const { entityId, data } = await selectedProvider.create(decryptedStoredInput, expireAt.getTime());
const dynamicSecretLease = await dynamicSecretLeaseDAL.create({
expireAt,
version: 1,
dynamicSecretId: dynamicSecretCfg.id,
externalEntityId: entityId
});
await dynamicSecretQueueService.setLeaseRevocation(dynamicSecretLease.id, Number(expireAt) - Number(new Date()));
return { lease: dynamicSecretLease, dynamicSecret: dynamicSecretCfg, data };
};
const renewLease = async ({
ttl,
actorAuthMethod,
actorOrgId,
actorId,
actor,
projectSlug,
path,
environmentSlug,
leaseId
}: TRenewDynamicSecretLeaseDTO) => {
const project = await projectDAL.findProjectBySlug(projectSlug, actorOrgId);
if (!project) throw new BadRequestError({ message: "Project not found" });
const projectId = project.id;
const { permission } = await permissionService.getProjectPermission(
actor,
actorId,
projectId,
actorAuthMethod,
actorOrgId
);
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Edit,
subject(ProjectPermissionSub.Secrets, { environment: environmentSlug, secretPath: path })
);
const plan = await licenseService.getPlan(actorOrgId);
if (!plan?.dynamicSecret) {
throw new BadRequestError({
message: "Failed to renew lease due to plan restriction. Upgrade plan to create dynamic secret."
});
}
const folder = await folderDAL.findBySecretPath(projectId, environmentSlug, path);
if (!folder) throw new BadRequestError({ message: "Folder not found" });
const dynamicSecretLease = await dynamicSecretLeaseDAL.findById(leaseId);
if (!dynamicSecretLease) throw new BadRequestError({ message: "Dynamic secret lease not found" });
const dynamicSecretCfg = dynamicSecretLease.dynamicSecret;
const selectedProvider = dynamicSecretProviders[dynamicSecretCfg.type as DynamicSecretProviders];
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;
const expireAt = new Date(dynamicSecretLease.expireAt.getTime() + ms(selectedTTL));
if (maxTTL) {
const maxExpiryDate = new Date(dynamicSecretLease.createdAt.getTime() + ms(maxTTL));
if (expireAt > maxExpiryDate) throw new BadRequestError({ message: "TTL cannot be larger than max ttl" });
}
const { entityId } = await selectedProvider.renew(
decryptedStoredInput,
dynamicSecretLease.externalEntityId,
expireAt.getTime()
);
await dynamicSecretQueueService.unsetLeaseRevocation(dynamicSecretLease.id);
await dynamicSecretQueueService.setLeaseRevocation(dynamicSecretLease.id, Number(expireAt) - Number(new Date()));
const updatedDynamicSecretLease = await dynamicSecretLeaseDAL.updateById(dynamicSecretLease.id, {
expireAt,
externalEntityId: entityId
});
return updatedDynamicSecretLease;
};
const revokeLease = async ({
leaseId,
environmentSlug,
path,
projectSlug,
actor,
actorId,
actorOrgId,
actorAuthMethod,
isForced
}: TDeleteDynamicSecretLeaseDTO) => {
const project = await projectDAL.findProjectBySlug(projectSlug, actorOrgId);
if (!project) throw new BadRequestError({ message: "Project not found" });
const projectId = project.id;
const { permission } = await permissionService.getProjectPermission(
actor,
actorId,
projectId,
actorAuthMethod,
actorOrgId
);
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Delete,
subject(ProjectPermissionSub.Secrets, { environment: environmentSlug, secretPath: path })
);
const folder = await folderDAL.findBySecretPath(projectId, environmentSlug, path);
if (!folder) throw new BadRequestError({ message: "Folder not found" });
const dynamicSecretLease = await dynamicSecretLeaseDAL.findById(leaseId);
if (!dynamicSecretLease) throw new BadRequestError({ message: "Dynamic secret lease not found" });
const dynamicSecretCfg = dynamicSecretLease.dynamicSecret;
const selectedProvider = dynamicSecretProviders[dynamicSecretCfg.type as DynamicSecretProviders];
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)
.catch(async (err) => {
// only propogate this error if forced is false
if (!isForced) return { error: err as Error };
});
if ((revokeResponse as { error?: Error })?.error) {
const { error } = revokeResponse as { error?: Error };
logger.error("Failed to revoke lease", { error: error?.message });
const deletedDynamicSecretLease = await dynamicSecretLeaseDAL.updateById(dynamicSecretLease.id, {
status: DynamicSecretLeaseStatus.FailedDeletion,
statusDetails: error?.message?.slice(0, 255)
});
return deletedDynamicSecretLease;
}
await dynamicSecretQueueService.unsetLeaseRevocation(dynamicSecretLease.id);
const deletedDynamicSecretLease = await dynamicSecretLeaseDAL.deleteById(dynamicSecretLease.id);
return deletedDynamicSecretLease;
};
const listLeases = async ({
path,
name,
actor,
actorId,
projectSlug,
actorOrgId,
environmentSlug,
actorAuthMethod
}: TListDynamicSecretLeasesDTO) => {
const project = await projectDAL.findProjectBySlug(projectSlug, actorOrgId);
if (!project) throw new BadRequestError({ message: "Project not found" });
const projectId = project.id;
const { permission } = await permissionService.getProjectPermission(
actor,
actorId,
projectId,
actorAuthMethod,
actorOrgId
);
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Read,
subject(ProjectPermissionSub.Secrets, { environment: environmentSlug, secretPath: path })
);
const folder = await folderDAL.findBySecretPath(projectId, environmentSlug, path);
if (!folder) throw new BadRequestError({ message: "Folder not found" });
const dynamicSecretCfg = await dynamicSecretDAL.findOne({ name, folderId: folder.id });
if (!dynamicSecretCfg) throw new BadRequestError({ message: "Dynamic secret not found" });
const dynamicSecretLeases = await dynamicSecretLeaseDAL.find({ dynamicSecretId: dynamicSecretCfg.id });
return dynamicSecretLeases;
};
const getLeaseDetails = async ({
projectSlug,
actorOrgId,
path,
environmentSlug,
actor,
actorId,
leaseId,
actorAuthMethod
}: TDetailsDynamicSecretLeaseDTO) => {
const project = await projectDAL.findProjectBySlug(projectSlug, actorOrgId);
if (!project) throw new BadRequestError({ message: "Project not found" });
const projectId = project.id;
const { permission } = await permissionService.getProjectPermission(
actor,
actorId,
projectId,
actorAuthMethod,
actorOrgId
);
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Read,
subject(ProjectPermissionSub.Secrets, { environment: environmentSlug, secretPath: path })
);
const folder = await folderDAL.findBySecretPath(projectId, environmentSlug, path);
if (!folder) throw new BadRequestError({ message: "Folder not found" });
const dynamicSecretLease = await dynamicSecretLeaseDAL.findById(leaseId);
if (!dynamicSecretLease) throw new BadRequestError({ message: "Dynamic secret lease not found" });
return dynamicSecretLease;
};
return {
create,
listLeases,
revokeLease,
renewLease,
getLeaseDetails
};
};

View File

@@ -0,0 +1,43 @@
import { TProjectPermission } from "@app/lib/types";
export enum DynamicSecretLeaseStatus {
FailedDeletion = "Failed to delete"
}
export type TCreateDynamicSecretLeaseDTO = {
name: string;
path: string;
environmentSlug: string;
ttl?: string;
projectSlug: string;
} & Omit<TProjectPermission, "projectId">;
export type TDetailsDynamicSecretLeaseDTO = {
leaseId: string;
path: string;
environmentSlug: string;
projectSlug: string;
} & Omit<TProjectPermission, "projectId">;
export type TListDynamicSecretLeasesDTO = {
name: string;
path: string;
environmentSlug: string;
projectSlug: string;
} & Omit<TProjectPermission, "projectId">;
export type TDeleteDynamicSecretLeaseDTO = {
leaseId: string;
path: string;
environmentSlug: string;
projectSlug: string;
isForced?: boolean;
} & Omit<TProjectPermission, "projectId">;
export type TRenewDynamicSecretLeaseDTO = {
leaseId: string;
path: string;
environmentSlug: string;
ttl?: string;
projectSlug: string;
} & Omit<TProjectPermission, "projectId">;

View File

@@ -0,0 +1,10 @@
import { TDbClient } from "@app/db";
import { TableName } from "@app/db/schemas";
import { ormify } from "@app/lib/knex";
export type TDynamicSecretDALFactory = ReturnType<typeof dynamicSecretDALFactory>;
export const dynamicSecretDALFactory = (db: TDbClient) => {
const orm = ormify(db, TableName.DynamicSecret);
return orm;
};

View File

@@ -0,0 +1,341 @@
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 { TProjectDALFactory } from "@app/services/project/project-dal";
import { TSecretFolderDALFactory } from "@app/services/secret-folder/secret-folder-dal";
import { TDynamicSecretLeaseDALFactory } from "../dynamic-secret-lease/dynamic-secret-lease-dal";
import { TDynamicSecretLeaseQueueServiceFactory } from "../dynamic-secret-lease/dynamic-secret-lease-queue";
import { TDynamicSecretDALFactory } from "./dynamic-secret-dal";
import {
DynamicSecretStatus,
TCreateDynamicSecretDTO,
TDeleteDynamicSecretDTO,
TDetailsDynamicSecretDTO,
TListDynamicSecretsDTO,
TUpdateDynamicSecretDTO
} from "./dynamic-secret-types";
import { DynamicSecretProviders, TDynamicProviderFns } from "./providers/models";
type TDynamicSecretServiceFactoryDep = {
dynamicSecretDAL: TDynamicSecretDALFactory;
dynamicSecretLeaseDAL: Pick<TDynamicSecretLeaseDALFactory, "find">;
dynamicSecretProviders: Record<DynamicSecretProviders, TDynamicProviderFns>;
dynamicSecretQueueService: Pick<
TDynamicSecretLeaseQueueServiceFactory,
"pruneDynamicSecret" | "unsetLeaseRevocation"
>;
licenseService: Pick<TLicenseServiceFactory, "getPlan">;
folderDAL: Pick<TSecretFolderDALFactory, "findBySecretPath">;
projectDAL: Pick<TProjectDALFactory, "findProjectBySlug">;
permissionService: Pick<TPermissionServiceFactory, "getProjectPermission">;
};
export type TDynamicSecretServiceFactory = ReturnType<typeof dynamicSecretServiceFactory>;
export const dynamicSecretServiceFactory = ({
dynamicSecretDAL,
dynamicSecretLeaseDAL,
licenseService,
folderDAL,
dynamicSecretProviders,
permissionService,
dynamicSecretQueueService,
projectDAL
}: TDynamicSecretServiceFactoryDep) => {
const create = async ({
path,
actor,
name,
actorId,
maxTTL,
provider,
environmentSlug,
projectSlug,
actorOrgId,
defaultTTL,
actorAuthMethod
}: TCreateDynamicSecretDTO) => {
const project = await projectDAL.findProjectBySlug(projectSlug, actorOrgId);
if (!project) throw new BadRequestError({ message: "Project not found" });
const projectId = project.id;
const { permission } = await permissionService.getProjectPermission(
actor,
actorId,
projectId,
actorAuthMethod,
actorOrgId
);
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Create,
subject(ProjectPermissionSub.Secrets, { environment: environmentSlug, secretPath: path })
);
const plan = await licenseService.getPlan(actorOrgId);
if (!plan?.dynamicSecret) {
throw new BadRequestError({
message: "Failed to create dynamic secret due to plan restriction. Upgrade plan to create dynamic secret."
});
}
const folder = await folderDAL.findBySecretPath(projectId, environmentSlug, path);
if (!folder) throw new BadRequestError({ message: "Folder not found" });
const existingDynamicSecret = await dynamicSecretDAL.findOne({ name, folderId: folder.id });
if (existingDynamicSecret)
throw new BadRequestError({ message: "Provided dynamic secret already exist under the folder" });
const selectedProvider = dynamicSecretProviders[provider.type];
const inputs = await selectedProvider.validateProviderInputs(provider.inputs);
const isConnected = await selectedProvider.validateConnection(provider.inputs);
if (!isConnected) throw new BadRequestError({ message: "Provider connection failed" });
const encryptedInput = infisicalSymmetricEncypt(JSON.stringify(inputs));
const dynamicSecretCfg = await dynamicSecretDAL.create({
type: provider.type,
version: 1,
inputIV: encryptedInput.iv,
inputTag: encryptedInput.tag,
inputCiphertext: encryptedInput.ciphertext,
algorithm: encryptedInput.algorithm,
keyEncoding: encryptedInput.encoding,
maxTTL,
defaultTTL,
folderId: folder.id,
name
});
return dynamicSecretCfg;
};
const updateByName = async ({
name,
maxTTL,
defaultTTL,
inputs,
environmentSlug,
projectSlug,
path,
actor,
actorId,
newName,
actorOrgId,
actorAuthMethod
}: TUpdateDynamicSecretDTO) => {
const project = await projectDAL.findProjectBySlug(projectSlug, actorOrgId);
if (!project) throw new BadRequestError({ message: "Project not found" });
const projectId = project.id;
const { permission } = await permissionService.getProjectPermission(
actor,
actorId,
projectId,
actorAuthMethod,
actorOrgId
);
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Edit,
subject(ProjectPermissionSub.Secrets, { environment: environmentSlug, secretPath: path })
);
const plan = await licenseService.getPlan(actorOrgId);
if (!plan?.dynamicSecret) {
throw new BadRequestError({
message: "Failed to update dynamic secret due to plan restriction. Upgrade plan to create dynamic secret."
});
}
const folder = await folderDAL.findBySecretPath(projectId, environmentSlug, path);
if (!folder) throw new BadRequestError({ message: "Folder not found" });
const dynamicSecretCfg = await dynamicSecretDAL.findOne({ name, folderId: folder.id });
if (!dynamicSecretCfg) throw new BadRequestError({ message: "Dynamic secret not found" });
if (newName) {
const existingDynamicSecret = await dynamicSecretDAL.findOne({ name: newName, folderId: folder.id });
if (existingDynamicSecret)
throw new BadRequestError({ message: "Provided dynamic secret already exist under the folder" });
}
const selectedProvider = dynamicSecretProviders[dynamicSecretCfg.type as DynamicSecretProviders];
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 encryptedInput = infisicalSymmetricEncypt(JSON.stringify(updatedInput));
const updatedDynamicCfg = await dynamicSecretDAL.updateById(dynamicSecretCfg.id, {
inputIV: encryptedInput.iv,
inputTag: encryptedInput.tag,
inputCiphertext: encryptedInput.ciphertext,
algorithm: encryptedInput.algorithm,
keyEncoding: encryptedInput.encoding,
maxTTL,
defaultTTL,
name: newName ?? name,
status: null,
statusDetails: null
});
return updatedDynamicCfg;
};
const deleteByName = async ({
actorAuthMethod,
actorOrgId,
actorId,
actor,
projectSlug,
name,
path,
environmentSlug,
isForced
}: TDeleteDynamicSecretDTO) => {
const project = await projectDAL.findProjectBySlug(projectSlug, actorOrgId);
if (!project) throw new BadRequestError({ message: "Project not found" });
const projectId = project.id;
const { permission } = await permissionService.getProjectPermission(
actor,
actorId,
projectId,
actorAuthMethod,
actorOrgId
);
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Edit,
subject(ProjectPermissionSub.Secrets, { environment: environmentSlug, secretPath: path })
);
const folder = await folderDAL.findBySecretPath(projectId, environmentSlug, path);
if (!folder) throw new BadRequestError({ message: "Folder not found" });
const dynamicSecretCfg = await dynamicSecretDAL.findOne({ name, folderId: folder.id });
if (!dynamicSecretCfg) throw new BadRequestError({ message: "Dynamic secret not found" });
const leases = await dynamicSecretLeaseDAL.find({ dynamicSecretId: dynamicSecretCfg.id });
// when not forced we check with the external system to first remove the things
// we introduce a forced concept because consider the external lease got deleted by some other external like a human or another system
// this allows user to clean up it from infisical
if (isForced) {
// clear all queues for lease revocations
await Promise.all(leases.map(({ id: leaseId }) => dynamicSecretQueueService.unsetLeaseRevocation(leaseId)));
const deletedDynamicSecretCfg = await dynamicSecretDAL.deleteById(dynamicSecretCfg.id);
return deletedDynamicSecretCfg;
}
// if leases exist we should flag it as deleting and then remove leases in background
// then delete the main one
if (leases.length) {
const updatedDynamicSecretCfg = await dynamicSecretDAL.updateById(dynamicSecretCfg.id, {
status: DynamicSecretStatus.Deleting
});
await dynamicSecretQueueService.pruneDynamicSecret(updatedDynamicSecretCfg.id);
return updatedDynamicSecretCfg;
}
// if no leases just delete the config
const deletedDynamicSecretCfg = await dynamicSecretDAL.deleteById(dynamicSecretCfg.id);
return deletedDynamicSecretCfg;
};
const getDetails = async ({
name,
projectSlug,
path,
environmentSlug,
actorAuthMethod,
actorOrgId,
actorId,
actor
}: TDetailsDynamicSecretDTO) => {
const project = await projectDAL.findProjectBySlug(projectSlug, actorOrgId);
if (!project) throw new BadRequestError({ message: "Project not found" });
const projectId = project.id;
const { permission } = await permissionService.getProjectPermission(
actor,
actorId,
projectId,
actorAuthMethod,
actorOrgId
);
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Edit,
subject(ProjectPermissionSub.Secrets, { environment: environmentSlug, secretPath: path })
);
const folder = await folderDAL.findBySecretPath(projectId, environmentSlug, path);
if (!folder) throw new BadRequestError({ message: "Folder not found" });
const dynamicSecretCfg = await dynamicSecretDAL.findOne({ name, folderId: folder.id });
if (!dynamicSecretCfg) throw new BadRequestError({ message: "Dynamic secret not found" });
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 };
};
const list = async ({
actorAuthMethod,
actorOrgId,
actorId,
actor,
projectSlug,
path,
environmentSlug
}: TListDynamicSecretsDTO) => {
const project = await projectDAL.findProjectBySlug(projectSlug, actorOrgId);
if (!project) throw new BadRequestError({ message: "Project not found" });
const projectId = project.id;
const { permission } = await permissionService.getProjectPermission(
actor,
actorId,
projectId,
actorAuthMethod,
actorOrgId
);
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Read,
subject(ProjectPermissionSub.Secrets, { environment: environmentSlug, secretPath: path })
);
const folder = await folderDAL.findBySecretPath(projectId, environmentSlug, path);
if (!folder) throw new BadRequestError({ message: "Folder not found" });
const dynamicSecretCfg = await dynamicSecretDAL.find({ folderId: folder.id });
return dynamicSecretCfg;
};
return {
create,
updateByName,
deleteByName,
getDetails,
list
};
};

View File

@@ -0,0 +1,54 @@
import { z } from "zod";
import { TProjectPermission } from "@app/lib/types";
import { DynamicSecretProviderSchema } from "./providers/models";
// various status for dynamic secret that happens in background
export enum DynamicSecretStatus {
Deleting = "Revocation in process",
FailedDeletion = "Failed to delete"
}
type TProvider = z.infer<typeof DynamicSecretProviderSchema>;
export type TCreateDynamicSecretDTO = {
provider: TProvider;
defaultTTL: string;
maxTTL?: string | null;
path: string;
environmentSlug: string;
name: string;
projectSlug: string;
} & Omit<TProjectPermission, "projectId">;
export type TUpdateDynamicSecretDTO = {
name: string;
newName?: string;
defaultTTL?: string;
maxTTL?: string | null;
path: string;
environmentSlug: string;
inputs?: TProvider["inputs"];
projectSlug: string;
} & Omit<TProjectPermission, "projectId">;
export type TDeleteDynamicSecretDTO = {
name: string;
path: string;
environmentSlug: string;
projectSlug: string;
isForced?: boolean;
} & Omit<TProjectPermission, "projectId">;
export type TDetailsDynamicSecretDTO = {
name: string;
path: string;
environmentSlug: string;
projectSlug: string;
} & Omit<TProjectPermission, "projectId">;
export type TListDynamicSecretsDTO = {
path: string;
environmentSlug: string;
projectSlug: string;
} & Omit<TProjectPermission, "projectId">;

View File

@@ -0,0 +1,6 @@
import { DynamicSecretProviders } from "./models";
import { SqlDatabaseProvider } from "./sql-database";
export const buildDynamicSecretProviders = () => ({
[DynamicSecretProviders.SqlDatabase]: SqlDatabaseProvider()
});

View File

@@ -0,0 +1,34 @@
import { z } from "zod";
export enum SqlProviders {
Postgres = "postgres"
}
export const DynamicSecretSqlDBSchema = z.object({
client: z.nativeEnum(SqlProviders),
host: z.string().toLowerCase(),
port: z.number(),
database: z.string(),
username: z.string(),
password: z.string(),
creationStatement: z.string(),
revocationStatement: z.string(),
renewStatement: z.string(),
ca: z.string().optional()
});
export enum DynamicSecretProviders {
SqlDatabase = "sql-database"
}
export const DynamicSecretProviderSchema = z.discriminatedUnion("type", [
z.object({ type: z.literal(DynamicSecretProviders.SqlDatabase), inputs: DynamicSecretSqlDBSchema })
]);
export type TDynamicProviderFns = {
create: (inputs: unknown, expireAt: number) => Promise<{ entityId: string; data: unknown }>;
validateConnection: (inputs: unknown) => Promise<boolean>;
validateProviderInputs: (inputs: object) => Promise<unknown>;
revoke: (inputs: unknown, entityId: string) => Promise<{ entityId: string }>;
renew: (inputs: unknown, entityId: string, expireAt: number) => Promise<{ entityId: string }>;
};

View File

@@ -0,0 +1,123 @@
import handlebars from "handlebars";
import knex from "knex";
import { customAlphabet } from "nanoid";
import { z } from "zod";
import { getConfig } from "@app/lib/config/env";
import { BadRequestError } from "@app/lib/errors";
import { getDbConnectionHost } from "@app/lib/knex";
import { alphaNumericNanoId } from "@app/lib/nanoid";
import { DynamicSecretSqlDBSchema, TDynamicProviderFns } from "./models";
const EXTERNAL_REQUEST_TIMEOUT = 10 * 1000;
const generatePassword = (size?: number) => {
const charset = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-_.~!*$#";
return customAlphabet(charset, 48)(size);
};
export const SqlDatabaseProvider = (): TDynamicProviderFns => {
const validateProviderInputs = async (inputs: unknown) => {
const appCfg = getConfig();
const dbHost = appCfg.DB_HOST || getDbConnectionHost(appCfg.DB_CONNECTION_URI);
const providerInputs = await DynamicSecretSqlDBSchema.parseAsync(inputs);
if (
// localhost
providerInputs.host === "localhost" ||
providerInputs.host === "127.0.0.1" ||
// database infisical uses
dbHost === providerInputs.host ||
// internal ips
providerInputs.host === "host.docker.internal" ||
providerInputs.host.match(/^10\.\d+\.\d+\.\d+/) ||
providerInputs.host.match(/^192\.168\.\d+\.\d+/)
)
throw new BadRequestError({ message: "Invalid db host" });
return providerInputs;
};
const getClient = async (providerInputs: z.infer<typeof DynamicSecretSqlDBSchema>) => {
const ssl = providerInputs.ca ? { rejectUnauthorized: false, ca: providerInputs.ca } : undefined;
const db = knex({
client: providerInputs.client,
connection: {
database: providerInputs.database,
port: providerInputs.port,
host: providerInputs.host,
user: providerInputs.username,
password: providerInputs.password,
connectionTimeoutMillis: EXTERNAL_REQUEST_TIMEOUT,
ssl,
pool: { min: 0, max: 1 }
}
});
return db;
};
const validateConnection = async (inputs: unknown) => {
const providerInputs = await validateProviderInputs(inputs);
const db = await getClient(providerInputs);
const isConnected = await db
.raw("SELECT NOW()")
.then(() => true)
.catch(() => false);
await db.destroy();
return isConnected;
};
const create = async (inputs: unknown, expireAt: number) => {
const providerInputs = await validateProviderInputs(inputs);
const db = await getClient(providerInputs);
const username = alphaNumericNanoId(32);
const password = generatePassword();
const expiration = new Date(expireAt).toISOString();
const creationStatement = handlebars.compile(providerInputs.creationStatement, { noEscape: true })({
username,
password,
expiration
});
await db.raw(creationStatement.toString());
await db.destroy();
return { entityId: username, data: { DB_USERNAME: username, DB_PASSWORD: password } };
};
const revoke = async (inputs: unknown, entityId: string) => {
const providerInputs = await validateProviderInputs(inputs);
const db = await getClient(providerInputs);
const username = entityId;
const revokeStatement = handlebars.compile(providerInputs.revocationStatement)({ username });
await db.raw(revokeStatement);
await db.destroy();
return { entityId: username };
};
const renew = async (inputs: unknown, entityId: string, expireAt: number) => {
const providerInputs = await validateProviderInputs(inputs);
const db = await getClient(providerInputs);
const username = entityId;
const expiration = new Date(expireAt).toISOString();
const renewStatement = handlebars.compile(providerInputs.renewStatement)({ username, expiration });
await db.raw(renewStatement);
await db.destroy();
return { entityId: username };
};
return {
validateProviderInputs,
validateConnection,
create,
revoke,
renew
};
};

View File

@@ -0,0 +1,12 @@
import { TDbClient } from "@app/db";
import { TableName } from "@app/db/schemas";
import { ormify } from "@app/lib/knex";
export type TIdentityProjectAdditionalPrivilegeDALFactory = ReturnType<
typeof identityProjectAdditionalPrivilegeDALFactory
>;
export const identityProjectAdditionalPrivilegeDALFactory = (db: TDbClient) => {
const orm = ormify(db, TableName.IdentityProjectAdditionalPrivilege);
return orm;
};

View File

@@ -0,0 +1,297 @@
import { ForbiddenError } from "@casl/ability";
import ms from "ms";
import { isAtLeastAsPrivileged } from "@app/lib/casl";
import { BadRequestError, ForbiddenRequestError } from "@app/lib/errors";
import { ActorType } from "@app/services/auth/auth-type";
import { TIdentityProjectDALFactory } from "@app/services/identity-project/identity-project-dal";
import { TProjectDALFactory } from "@app/services/project/project-dal";
import { TPermissionServiceFactory } from "../permission/permission-service";
import { ProjectPermissionActions, ProjectPermissionSub } from "../permission/project-permission";
import { TIdentityProjectAdditionalPrivilegeDALFactory } from "./identity-project-additional-privilege-dal";
import {
IdentityProjectAdditionalPrivilegeTemporaryMode,
TCreateIdentityPrivilegeDTO,
TDeleteIdentityPrivilegeDTO,
TGetIdentityPrivilegeDetailsDTO,
TListIdentityPrivilegesDTO,
TUpdateIdentityPrivilegeDTO
} from "./identity-project-additional-privilege-types";
type TIdentityProjectAdditionalPrivilegeServiceFactoryDep = {
identityProjectAdditionalPrivilegeDAL: TIdentityProjectAdditionalPrivilegeDALFactory;
identityProjectDAL: Pick<TIdentityProjectDALFactory, "findOne" | "findById">;
projectDAL: Pick<TProjectDALFactory, "findProjectBySlug">;
permissionService: Pick<TPermissionServiceFactory, "getProjectPermission">;
};
export type TIdentityProjectAdditionalPrivilegeServiceFactory = ReturnType<
typeof identityProjectAdditionalPrivilegeServiceFactory
>;
export const identityProjectAdditionalPrivilegeServiceFactory = ({
identityProjectAdditionalPrivilegeDAL,
identityProjectDAL,
permissionService,
projectDAL
}: TIdentityProjectAdditionalPrivilegeServiceFactoryDep) => {
const create = async ({
slug,
actor,
actorId,
identityId,
projectSlug,
permissions: customPermission,
actorOrgId,
actorAuthMethod,
...dto
}: TCreateIdentityPrivilegeDTO) => {
const project = await projectDAL.findProjectBySlug(projectSlug, actorOrgId);
if (!project) throw new BadRequestError({ message: "Project not found" });
const projectId = project.id;
const identityProjectMembership = await identityProjectDAL.findOne({ identityId, projectId });
if (!identityProjectMembership)
throw new BadRequestError({ message: `Failed to find identity with id ${identityId}` });
const { permission } = await permissionService.getProjectPermission(
actor,
actorId,
identityProjectMembership.projectId,
actorAuthMethod,
actorOrgId
);
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Edit, ProjectPermissionSub.Identity);
const { permission: identityRolePermission } = await permissionService.getProjectPermission(
ActorType.IDENTITY,
identityId,
identityProjectMembership.projectId,
actorAuthMethod,
actorOrgId
);
const hasRequiredPriviledges = isAtLeastAsPrivileged(permission, identityRolePermission);
if (!hasRequiredPriviledges)
throw new ForbiddenRequestError({ message: "Failed to update more privileged identity" });
const existingSlug = await identityProjectAdditionalPrivilegeDAL.findOne({
slug,
projectMembershipId: identityProjectMembership.id
});
if (existingSlug) throw new BadRequestError({ message: "Additional privilege of provided slug exist" });
if (!dto.isTemporary) {
const additionalPrivilege = await identityProjectAdditionalPrivilegeDAL.create({
projectMembershipId: identityProjectMembership.id,
slug,
permissions: customPermission
});
return additionalPrivilege;
}
const relativeTempAllocatedTimeInMs = ms(dto.temporaryRange);
const additionalPrivilege = await identityProjectAdditionalPrivilegeDAL.create({
projectMembershipId: identityProjectMembership.id,
slug,
permissions: customPermission,
isTemporary: true,
temporaryMode: IdentityProjectAdditionalPrivilegeTemporaryMode.Relative,
temporaryRange: dto.temporaryRange,
temporaryAccessStartTime: new Date(dto.temporaryAccessStartTime),
temporaryAccessEndTime: new Date(new Date(dto.temporaryAccessStartTime).getTime() + relativeTempAllocatedTimeInMs)
});
return additionalPrivilege;
};
const updateBySlug = async ({
projectSlug,
slug,
identityId,
data,
actorOrgId,
actor,
actorId,
actorAuthMethod
}: TUpdateIdentityPrivilegeDTO) => {
const project = await projectDAL.findProjectBySlug(projectSlug, actorOrgId);
if (!project) throw new BadRequestError({ message: "Project not found" });
const projectId = project.id;
const identityProjectMembership = await identityProjectDAL.findOne({ identityId, projectId });
if (!identityProjectMembership)
throw new BadRequestError({ message: `Failed to find identity with id ${identityId}` });
const { permission } = await permissionService.getProjectPermission(
actor,
actorId,
identityProjectMembership.projectId,
actorAuthMethod,
actorOrgId
);
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Edit, ProjectPermissionSub.Identity);
const { permission: identityRolePermission } = await permissionService.getProjectPermission(
ActorType.IDENTITY,
identityProjectMembership.identityId,
identityProjectMembership.projectId,
actorAuthMethod,
actorOrgId
);
const hasRequiredPriviledges = isAtLeastAsPrivileged(permission, identityRolePermission);
if (!hasRequiredPriviledges)
throw new ForbiddenRequestError({ message: "Failed to update more privileged identity" });
const identityPrivilege = await identityProjectAdditionalPrivilegeDAL.findOne({
slug,
projectMembershipId: identityProjectMembership.id
});
if (!identityPrivilege) throw new BadRequestError({ message: "Identity additional privilege not found" });
if (data?.slug) {
const existingSlug = await identityProjectAdditionalPrivilegeDAL.findOne({
slug: data.slug,
projectMembershipId: identityProjectMembership.id
});
if (existingSlug && existingSlug.id !== identityPrivilege.id)
throw new BadRequestError({ message: "Additional privilege of provided slug exist" });
}
const isTemporary = typeof data?.isTemporary !== "undefined" ? data.isTemporary : identityPrivilege.isTemporary;
if (isTemporary) {
const temporaryAccessStartTime = data?.temporaryAccessStartTime || identityPrivilege?.temporaryAccessStartTime;
const temporaryRange = data?.temporaryRange || identityPrivilege?.temporaryRange;
const additionalPrivilege = await identityProjectAdditionalPrivilegeDAL.updateById(identityPrivilege.id, {
...data,
temporaryAccessStartTime: new Date(temporaryAccessStartTime || ""),
temporaryAccessEndTime: new Date(new Date(temporaryAccessStartTime || "").getTime() + ms(temporaryRange || ""))
});
return additionalPrivilege;
}
const additionalPrivilege = await identityProjectAdditionalPrivilegeDAL.updateById(identityPrivilege.id, {
...data,
isTemporary: false,
temporaryAccessStartTime: null,
temporaryAccessEndTime: null,
temporaryRange: null,
temporaryMode: null
});
return additionalPrivilege;
};
const deleteBySlug = async ({
actorId,
slug,
identityId,
projectSlug,
actor,
actorOrgId,
actorAuthMethod
}: TDeleteIdentityPrivilegeDTO) => {
const project = await projectDAL.findProjectBySlug(projectSlug, actorOrgId);
if (!project) throw new BadRequestError({ message: "Project not found" });
const projectId = project.id;
const identityProjectMembership = await identityProjectDAL.findOne({ identityId, projectId });
if (!identityProjectMembership)
throw new BadRequestError({ message: `Failed to find identity with id ${identityId}` });
const { permission } = await permissionService.getProjectPermission(
actor,
actorId,
identityProjectMembership.projectId,
actorAuthMethod,
actorOrgId
);
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Edit, ProjectPermissionSub.Identity);
const { permission: identityRolePermission } = await permissionService.getProjectPermission(
ActorType.IDENTITY,
identityProjectMembership.identityId,
identityProjectMembership.projectId,
actorAuthMethod,
actorOrgId
);
const hasRequiredPriviledges = isAtLeastAsPrivileged(permission, identityRolePermission);
if (!hasRequiredPriviledges)
throw new ForbiddenRequestError({ message: "Failed to edit more privileged identity" });
const identityPrivilege = await identityProjectAdditionalPrivilegeDAL.findOne({
slug,
projectMembershipId: identityProjectMembership.id
});
if (!identityPrivilege) throw new BadRequestError({ message: "Identity additional privilege not found" });
const deletedPrivilege = await identityProjectAdditionalPrivilegeDAL.deleteById(identityPrivilege.id);
return deletedPrivilege;
};
const getPrivilegeDetailsBySlug = async ({
projectSlug,
identityId,
slug,
actorOrgId,
actor,
actorId,
actorAuthMethod
}: TGetIdentityPrivilegeDetailsDTO) => {
const project = await projectDAL.findProjectBySlug(projectSlug, actorOrgId);
if (!project) throw new BadRequestError({ message: "Project not found" });
const projectId = project.id;
const identityProjectMembership = await identityProjectDAL.findOne({ identityId, projectId });
if (!identityProjectMembership)
throw new BadRequestError({ message: `Failed to find identity with id ${identityId}` });
const { permission } = await permissionService.getProjectPermission(
actor,
actorId,
identityProjectMembership.projectId,
actorAuthMethod,
actorOrgId
);
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Edit, ProjectPermissionSub.Identity);
const identityPrivilege = await identityProjectAdditionalPrivilegeDAL.findOne({
slug,
projectMembershipId: identityProjectMembership.id
});
if (!identityPrivilege) throw new BadRequestError({ message: "Identity additional privilege not found" });
return identityPrivilege;
};
const listIdentityProjectPrivileges = async ({
identityId,
actorOrgId,
actor,
actorId,
actorAuthMethod,
projectSlug
}: TListIdentityPrivilegesDTO) => {
const project = await projectDAL.findProjectBySlug(projectSlug, actorOrgId);
if (!project) throw new BadRequestError({ message: "Project not found" });
const projectId = project.id;
const identityProjectMembership = await identityProjectDAL.findOne({ identityId, projectId });
if (!identityProjectMembership)
throw new BadRequestError({ message: `Failed to find identity with id ${identityId}` });
const { permission } = await permissionService.getProjectPermission(
actor,
actorId,
identityProjectMembership.projectId,
actorAuthMethod,
actorOrgId
);
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Edit, ProjectPermissionSub.Identity);
const identityPrivileges = await identityProjectAdditionalPrivilegeDAL.find({
projectMembershipId: identityProjectMembership.id
});
return identityPrivileges;
};
return {
create,
updateBySlug,
deleteBySlug,
getPrivilegeDetailsBySlug,
listIdentityProjectPrivileges
};
};

View File

@@ -0,0 +1,54 @@
import { TProjectPermission } from "@app/lib/types";
export enum IdentityProjectAdditionalPrivilegeTemporaryMode {
Relative = "relative"
}
export type TCreateIdentityPrivilegeDTO = {
permissions: unknown;
identityId: string;
projectSlug: string;
slug: string;
} & (
| {
isTemporary: false;
}
| {
isTemporary: true;
temporaryMode: IdentityProjectAdditionalPrivilegeTemporaryMode.Relative;
temporaryRange: string;
temporaryAccessStartTime: string;
}
) &
Omit<TProjectPermission, "projectId">;
export type TUpdateIdentityPrivilegeDTO = { slug: string; identityId: string; projectSlug: string } & Omit<
TProjectPermission,
"projectId"
> & {
data: Partial<{
permissions: unknown;
slug: string;
isTemporary: boolean;
temporaryMode: IdentityProjectAdditionalPrivilegeTemporaryMode.Relative;
temporaryRange: string;
temporaryAccessStartTime: string;
}>;
};
export type TDeleteIdentityPrivilegeDTO = Omit<TProjectPermission, "projectId"> & {
slug: string;
identityId: string;
projectSlug: string;
};
export type TGetIdentityPrivilegeDetailsDTO = Omit<TProjectPermission, "projectId"> & {
slug: string;
identityId: string;
projectSlug: string;
};
export type TListIdentityPrivilegesDTO = Omit<TProjectPermission, "projectId"> & {
identityId: string;
projectSlug: string;
};

View File

@@ -15,6 +15,7 @@ export const getDefaultOnPremFeatures = (): TFeatureSet => ({
membersUsed: 0, membersUsed: 0,
environmentLimit: null, environmentLimit: null,
environmentsUsed: 0, environmentsUsed: 0,
dynamicSecret: false,
secretVersioning: true, secretVersioning: true,
pitRecovery: false, pitRecovery: false,
ipAllowlisting: false, ipAllowlisting: false,

View File

@@ -27,6 +27,7 @@ export type TFeatureSet = {
tier: -1; tier: -1;
workspaceLimit: null; workspaceLimit: null;
workspacesUsed: 0; workspacesUsed: 0;
dynamicSecret: false;
memberLimit: null; memberLimit: null;
membersUsed: 0; membersUsed: 0;
environmentLimit: null; environmentLimit: null;

View File

@@ -56,6 +56,11 @@ export const permissionDALFactory = (db: TDbClient) => {
`${TableName.ProjectUserMembershipRole}.customRoleId`, `${TableName.ProjectUserMembershipRole}.customRoleId`,
`${TableName.ProjectRoles}.id` `${TableName.ProjectRoles}.id`
) )
.leftJoin(
TableName.ProjectUserAdditionalPrivilege,
`${TableName.ProjectUserAdditionalPrivilege}.projectMembershipId`,
`${TableName.ProjectMembership}.id`
)
.join(TableName.Project, `${TableName.ProjectMembership}.projectId`, `${TableName.Project}.id`) .join(TableName.Project, `${TableName.ProjectMembership}.projectId`, `${TableName.Project}.id`)
.join(TableName.Organization, `${TableName.Project}.orgId`, `${TableName.Organization}.id`) .join(TableName.Organization, `${TableName.Project}.orgId`, `${TableName.Organization}.id`)
.where("userId", userId) .where("userId", userId)
@@ -69,9 +74,22 @@ export const permissionDALFactory = (db: TDbClient) => {
db.ref("updatedAt").withSchema(TableName.ProjectMembership).as("membershipUpdatedAt"), db.ref("updatedAt").withSchema(TableName.ProjectMembership).as("membershipUpdatedAt"),
db.ref("authEnforced").withSchema(TableName.Organization).as("orgAuthEnforced"), db.ref("authEnforced").withSchema(TableName.Organization).as("orgAuthEnforced"),
db.ref("orgId").withSchema(TableName.Project), db.ref("orgId").withSchema(TableName.Project),
db.ref("slug").withSchema(TableName.ProjectRoles).as("customRoleSlug") db.ref("slug").withSchema(TableName.ProjectRoles).as("customRoleSlug"),
) db.ref("permissions").withSchema(TableName.ProjectRoles),
.select("permissions"); db.ref("id").withSchema(TableName.ProjectUserAdditionalPrivilege).as("userApId"),
db.ref("permissions").withSchema(TableName.ProjectUserAdditionalPrivilege).as("userApPermissions"),
db.ref("temporaryMode").withSchema(TableName.ProjectUserAdditionalPrivilege).as("userApTemporaryMode"),
db.ref("isTemporary").withSchema(TableName.ProjectUserAdditionalPrivilege).as("userApIsTemporary"),
db.ref("temporaryRange").withSchema(TableName.ProjectUserAdditionalPrivilege).as("userApTemporaryRange"),
db
.ref("temporaryAccessStartTime")
.withSchema(TableName.ProjectUserAdditionalPrivilege)
.as("userApTemporaryAccessStartTime"),
db
.ref("temporaryAccessEndTime")
.withSchema(TableName.ProjectUserAdditionalPrivilege)
.as("userApTemporaryAccessEndTime")
);
const permission = sqlNestRelationships({ const permission = sqlNestRelationships({
data: docs, data: docs,
@@ -102,15 +120,44 @@ export const permissionDALFactory = (db: TDbClient) => {
permissions: z.unknown(), permissions: z.unknown(),
customRoleSlug: z.string().optional().nullable() customRoleSlug: z.string().optional().nullable()
}).parse(data) }).parse(data)
},
{
key: "userApId",
label: "additionalPrivileges" as const,
mapper: ({
userApId,
userApPermissions,
userApIsTemporary,
userApTemporaryMode,
userApTemporaryRange,
userApTemporaryAccessEndTime,
userApTemporaryAccessStartTime
}) => ({
id: userApId,
permissions: userApPermissions,
temporaryRange: userApTemporaryRange,
temporaryMode: userApTemporaryMode,
temporaryAccessEndTime: userApTemporaryAccessEndTime,
temporaryAccessStartTime: userApTemporaryAccessStartTime,
isTemporary: userApIsTemporary
})
} }
] ]
}); });
if (!permission?.[0]) return undefined;
// when introducting cron mode change it here // when introducting cron mode change it here
const activeRoles = permission?.[0]?.roles.filter( const activeRoles = permission?.[0]?.roles?.filter(
({ isTemporary, temporaryAccessEndTime }) => ({ isTemporary, temporaryAccessEndTime }) =>
!isTemporary || (isTemporary && temporaryAccessEndTime && new Date() < temporaryAccessEndTime) !isTemporary || (isTemporary && temporaryAccessEndTime && new Date() < temporaryAccessEndTime)
); );
return permission?.[0] ? { ...permission[0], roles: activeRoles } : undefined;
const activeAdditionalPrivileges = permission?.[0]?.additionalPrivileges?.filter(
({ isTemporary, temporaryAccessEndTime }) =>
!isTemporary || (isTemporary && temporaryAccessEndTime && new Date() < temporaryAccessEndTime)
);
return { ...permission[0], roles: activeRoles, additionalPrivileges: activeAdditionalPrivileges };
} catch (error) { } catch (error) {
throw new DatabaseError({ error, name: "GetProjectPermission" }); throw new DatabaseError({ error, name: "GetProjectPermission" });
} }
@@ -129,6 +176,11 @@ export const permissionDALFactory = (db: TDbClient) => {
`${TableName.IdentityProjectMembershipRole}.customRoleId`, `${TableName.IdentityProjectMembershipRole}.customRoleId`,
`${TableName.ProjectRoles}.id` `${TableName.ProjectRoles}.id`
) )
.leftJoin(
TableName.IdentityProjectAdditionalPrivilege,
`${TableName.IdentityProjectAdditionalPrivilege}.projectMembershipId`,
`${TableName.IdentityProjectMembership}.id`
)
.join( .join(
// Join the Project table to later select orgId // Join the Project table to later select orgId
TableName.Project, TableName.Project,
@@ -144,9 +196,28 @@ export const permissionDALFactory = (db: TDbClient) => {
db.ref("role").withSchema(TableName.IdentityProjectMembership).as("oldRoleField"), db.ref("role").withSchema(TableName.IdentityProjectMembership).as("oldRoleField"),
db.ref("createdAt").withSchema(TableName.IdentityProjectMembership).as("membershipCreatedAt"), db.ref("createdAt").withSchema(TableName.IdentityProjectMembership).as("membershipCreatedAt"),
db.ref("updatedAt").withSchema(TableName.IdentityProjectMembership).as("membershipUpdatedAt"), db.ref("updatedAt").withSchema(TableName.IdentityProjectMembership).as("membershipUpdatedAt"),
db.ref("slug").withSchema(TableName.ProjectRoles).as("customRoleSlug") db.ref("slug").withSchema(TableName.ProjectRoles).as("customRoleSlug"),
) db.ref("permissions").withSchema(TableName.ProjectRoles),
.select("permissions"); db.ref("id").withSchema(TableName.IdentityProjectAdditionalPrivilege).as("identityApId"),
db.ref("permissions").withSchema(TableName.IdentityProjectAdditionalPrivilege).as("identityApPermissions"),
db
.ref("temporaryMode")
.withSchema(TableName.IdentityProjectAdditionalPrivilege)
.as("identityApTemporaryMode"),
db.ref("isTemporary").withSchema(TableName.IdentityProjectAdditionalPrivilege).as("identityApIsTemporary"),
db
.ref("temporaryRange")
.withSchema(TableName.IdentityProjectAdditionalPrivilege)
.as("identityApTemporaryRange"),
db
.ref("temporaryAccessStartTime")
.withSchema(TableName.IdentityProjectAdditionalPrivilege)
.as("identityApTemporaryAccessStartTime"),
db
.ref("temporaryAccessEndTime")
.withSchema(TableName.IdentityProjectAdditionalPrivilege)
.as("identityApTemporaryAccessEndTime")
);
const permission = sqlNestRelationships({ const permission = sqlNestRelationships({
data: docs, data: docs,
@@ -171,16 +242,44 @@ export const permissionDALFactory = (db: TDbClient) => {
permissions: z.unknown(), permissions: z.unknown(),
customRoleSlug: z.string().optional().nullable() customRoleSlug: z.string().optional().nullable()
}).parse(data) }).parse(data)
},
{
key: "identityApId",
label: "additionalPrivileges" as const,
mapper: ({
identityApId,
identityApPermissions,
identityApIsTemporary,
identityApTemporaryMode,
identityApTemporaryRange,
identityApTemporaryAccessEndTime,
identityApTemporaryAccessStartTime
}) => ({
id: identityApId,
permissions: identityApPermissions,
temporaryRange: identityApTemporaryRange,
temporaryMode: identityApTemporaryMode,
temporaryAccessEndTime: identityApTemporaryAccessEndTime,
temporaryAccessStartTime: identityApTemporaryAccessStartTime,
isTemporary: identityApIsTemporary
})
} }
] ]
}); });
if (!permission?.[0]) return undefined;
// when introducting cron mode change it here // when introducting cron mode change it here
const activeRoles = permission?.[0]?.roles.filter( const activeRoles = permission?.[0]?.roles.filter(
({ isTemporary, temporaryAccessEndTime }) => ({ isTemporary, temporaryAccessEndTime }) =>
!isTemporary || (isTemporary && temporaryAccessEndTime && new Date() < temporaryAccessEndTime) !isTemporary || (isTemporary && temporaryAccessEndTime && new Date() < temporaryAccessEndTime)
); );
return permission?.[0] ? { ...permission[0], roles: activeRoles } : undefined; const activeAdditionalPrivileges = permission?.[0]?.additionalPrivileges?.filter(
({ isTemporary, temporaryAccessEndTime }) =>
!isTemporary || (isTemporary && temporaryAccessEndTime && new Date() < temporaryAccessEndTime)
);
return { ...permission[0], roles: activeRoles, additionalPrivileges: activeAdditionalPrivileges };
} catch (error) { } catch (error) {
throw new DatabaseError({ error, name: "GetProjectIdentityPermission" }); throw new DatabaseError({ error, name: "GetProjectIdentityPermission" });
} }

View File

@@ -5,9 +5,13 @@ import { ActorAuthMethod, AuthMethod } from "@app/services/auth/auth-type";
function isAuthMethodSaml(actorAuthMethod: ActorAuthMethod) { function isAuthMethodSaml(actorAuthMethod: ActorAuthMethod) {
if (!actorAuthMethod) return false; if (!actorAuthMethod) return false;
return [AuthMethod.AZURE_SAML, AuthMethod.OKTA_SAML, AuthMethod.JUMPCLOUD_SAML, AuthMethod.GOOGLE_SAML].includes( return [
actorAuthMethod AuthMethod.AZURE_SAML,
); AuthMethod.OKTA_SAML,
AuthMethod.JUMPCLOUD_SAML,
AuthMethod.GOOGLE_SAML,
AuthMethod.KEYCLOAK_SAML
].includes(actorAuthMethod);
} }
function validateOrgSAML(actorAuthMethod: ActorAuthMethod, isSamlEnforced: TOrganizations["authEnforced"]) { function validateOrgSAML(actorAuthMethod: ActorAuthMethod, isSamlEnforced: TOrganizations["authEnforced"]) {

View File

@@ -180,10 +180,12 @@ export const permissionServiceFactory = ({
authMethod: ActorAuthMethod, authMethod: ActorAuthMethod,
userOrgId?: string userOrgId?: string
): Promise<TProjectPermissionRT<ActorType.USER>> => { ): Promise<TProjectPermissionRT<ActorType.USER>> => {
const membership = await permissionDAL.getProjectPermission(userId, projectId); const userProjectPermission = await permissionDAL.getProjectPermission(userId, projectId);
if (!membership) throw new UnauthorizedError({ name: "User not in project" }); if (!userProjectPermission) throw new UnauthorizedError({ name: "User not in project" });
if (membership.roles.some(({ role, permissions }) => role === ProjectMembershipRole.Custom && !permissions)) { if (
userProjectPermission.roles.some(({ role, permissions }) => role === ProjectMembershipRole.Custom && !permissions)
) {
throw new BadRequestError({ name: "Custom permission not found" }); throw new BadRequestError({ name: "Custom permission not found" });
} }
@@ -192,17 +194,27 @@ export const permissionServiceFactory = ({
// Extra: This means that when users are using API keys to make requests, they can't use slug-based routes. // Extra: This means that when users are using API keys to make requests, they can't use slug-based routes.
// Slug-based routes depend on the organization ID being present on the request, since project slugs aren't globally unique, and we need a way to filter by organization. // Slug-based routes depend on the organization ID being present on the request, since project slugs aren't globally unique, and we need a way to filter by organization.
if (userOrgId !== "API_KEY" && membership.orgId !== userOrgId) { if (userOrgId !== "API_KEY" && userProjectPermission.orgId !== userOrgId) {
throw new UnauthorizedError({ name: "You are not logged into this organization" }); throw new UnauthorizedError({ name: "You are not logged into this organization" });
} }
validateOrgSAML(authMethod, membership.orgAuthEnforced); validateOrgSAML(authMethod, userProjectPermission.orgAuthEnforced);
// join two permissions and pass to build the final permission set
const rolePermissions = userProjectPermission.roles?.map(({ role, permissions }) => ({ role, permissions })) || [];
const additionalPrivileges =
userProjectPermission.additionalPrivileges?.map(({ permissions }) => ({
role: ProjectMembershipRole.Custom,
permissions
})) || [];
return { return {
permission: buildProjectPermission(membership.roles), permission: buildProjectPermission(rolePermissions.concat(additionalPrivileges)),
membership, membership: userProjectPermission,
hasRole: (role: string) => hasRole: (role: string) =>
membership.roles.findIndex(({ role: slug, customRoleSlug }) => role === slug || slug === customRoleSlug) !== -1 userProjectPermission.roles.findIndex(
({ role: slug, customRoleSlug }) => role === slug || slug === customRoleSlug
) !== -1
}; };
}; };
@@ -226,8 +238,16 @@ export const permissionServiceFactory = ({
throw new UnauthorizedError({ name: "You are not a member of this organization" }); throw new UnauthorizedError({ name: "You are not a member of this organization" });
} }
const rolePermissions =
identityProjectPermission.roles?.map(({ role, permissions }) => ({ role, permissions })) || [];
const additionalPrivileges =
identityProjectPermission.additionalPrivileges?.map(({ permissions }) => ({
role: ProjectMembershipRole.Custom,
permissions
})) || [];
return { return {
permission: buildProjectPermission(identityProjectPermission.roles), permission: buildProjectPermission(rolePermissions.concat(additionalPrivileges)),
membership: identityProjectPermission, membership: identityProjectPermission,
hasRole: (role: string) => hasRole: (role: string) =>
identityProjectPermission.roles.findIndex( identityProjectPermission.roles.findIndex(

View File

@@ -0,0 +1,10 @@
import { TDbClient } from "@app/db";
import { TableName } from "@app/db/schemas";
import { ormify } from "@app/lib/knex";
export type TProjectUserAdditionalPrivilegeDALFactory = ReturnType<typeof projectUserAdditionalPrivilegeDALFactory>;
export const projectUserAdditionalPrivilegeDALFactory = (db: TDbClient) => {
const orm = ormify(db, TableName.ProjectUserAdditionalPrivilege);
return orm;
};

View File

@@ -0,0 +1,212 @@
import { ForbiddenError } from "@casl/ability";
import ms from "ms";
import { BadRequestError } from "@app/lib/errors";
import { TProjectMembershipDALFactory } from "@app/services/project-membership/project-membership-dal";
import { TPermissionServiceFactory } from "../permission/permission-service";
import { ProjectPermissionActions, ProjectPermissionSub } from "../permission/project-permission";
import { TProjectUserAdditionalPrivilegeDALFactory } from "./project-user-additional-privilege-dal";
import {
ProjectUserAdditionalPrivilegeTemporaryMode,
TCreateUserPrivilegeDTO,
TDeleteUserPrivilegeDTO,
TGetUserPrivilegeDetailsDTO,
TListUserPrivilegesDTO,
TUpdateUserPrivilegeDTO
} from "./project-user-additional-privilege-types";
type TProjectUserAdditionalPrivilegeServiceFactoryDep = {
projectUserAdditionalPrivilegeDAL: TProjectUserAdditionalPrivilegeDALFactory;
projectMembershipDAL: Pick<TProjectMembershipDALFactory, "findById">;
permissionService: Pick<TPermissionServiceFactory, "getProjectPermission">;
};
export type TProjectUserAdditionalPrivilegeServiceFactory = ReturnType<
typeof projectUserAdditionalPrivilegeServiceFactory
>;
export const projectUserAdditionalPrivilegeServiceFactory = ({
projectUserAdditionalPrivilegeDAL,
projectMembershipDAL,
permissionService
}: TProjectUserAdditionalPrivilegeServiceFactoryDep) => {
const create = async ({
slug,
actor,
actorId,
permissions: customPermission,
actorOrgId,
actorAuthMethod,
projectMembershipId,
...dto
}: TCreateUserPrivilegeDTO) => {
const projectMembership = await projectMembershipDAL.findById(projectMembershipId);
if (!projectMembership) throw new BadRequestError({ message: "Project membership not found" });
const { permission } = await permissionService.getProjectPermission(
actor,
actorId,
projectMembership.projectId,
actorAuthMethod,
actorOrgId
);
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Edit, ProjectPermissionSub.Member);
const existingSlug = await projectUserAdditionalPrivilegeDAL.findOne({ slug, projectMembershipId });
if (existingSlug) throw new BadRequestError({ message: "Additional privilege of provided slug exist" });
if (!dto.isTemporary) {
const additionalPrivilege = await projectUserAdditionalPrivilegeDAL.create({
projectMembershipId,
slug,
permissions: customPermission
});
return additionalPrivilege;
}
const relativeTempAllocatedTimeInMs = ms(dto.temporaryRange);
const additionalPrivilege = await projectUserAdditionalPrivilegeDAL.create({
projectMembershipId,
slug,
permissions: customPermission,
isTemporary: true,
temporaryMode: ProjectUserAdditionalPrivilegeTemporaryMode.Relative,
temporaryRange: dto.temporaryRange,
temporaryAccessStartTime: new Date(dto.temporaryAccessStartTime),
temporaryAccessEndTime: new Date(new Date(dto.temporaryAccessStartTime).getTime() + relativeTempAllocatedTimeInMs)
});
return additionalPrivilege;
};
const updateById = async ({
privilegeId,
actorOrgId,
actor,
actorId,
actorAuthMethod,
...dto
}: TUpdateUserPrivilegeDTO) => {
const userPrivilege = await projectUserAdditionalPrivilegeDAL.findById(privilegeId);
if (!userPrivilege) throw new BadRequestError({ message: "User additional privilege not found" });
const projectMembership = await projectMembershipDAL.findById(userPrivilege.projectMembershipId);
if (!projectMembership) throw new BadRequestError({ message: "Project membership not found" });
const { permission } = await permissionService.getProjectPermission(
actor,
actorId,
projectMembership.projectId,
actorAuthMethod,
actorOrgId
);
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Edit, ProjectPermissionSub.Member);
if (dto?.slug) {
const existingSlug = await projectUserAdditionalPrivilegeDAL.findOne({
slug: dto.slug,
projectMembershipId: projectMembership.id
});
if (existingSlug && existingSlug.id !== userPrivilege.id)
throw new BadRequestError({ message: "Additional privilege of provided slug exist" });
}
const isTemporary = typeof dto?.isTemporary !== "undefined" ? dto.isTemporary : userPrivilege.isTemporary;
if (isTemporary) {
const temporaryAccessStartTime = dto?.temporaryAccessStartTime || userPrivilege?.temporaryAccessStartTime;
const temporaryRange = dto?.temporaryRange || userPrivilege?.temporaryRange;
const additionalPrivilege = await projectUserAdditionalPrivilegeDAL.updateById(userPrivilege.id, {
...dto,
temporaryAccessStartTime: new Date(temporaryAccessStartTime || ""),
temporaryAccessEndTime: new Date(new Date(temporaryAccessStartTime || "").getTime() + ms(temporaryRange || ""))
});
return additionalPrivilege;
}
const additionalPrivilege = await projectUserAdditionalPrivilegeDAL.updateById(userPrivilege.id, {
...dto,
isTemporary: false,
temporaryAccessStartTime: null,
temporaryAccessEndTime: null,
temporaryRange: null,
temporaryMode: null
});
return additionalPrivilege;
};
const deleteById = async ({ actorId, actor, actorOrgId, actorAuthMethod, privilegeId }: TDeleteUserPrivilegeDTO) => {
const userPrivilege = await projectUserAdditionalPrivilegeDAL.findById(privilegeId);
if (!userPrivilege) throw new BadRequestError({ message: "User additional privilege not found" });
const projectMembership = await projectMembershipDAL.findById(userPrivilege.projectMembershipId);
if (!projectMembership) throw new BadRequestError({ message: "Project membership not found" });
const { permission } = await permissionService.getProjectPermission(
actor,
actorId,
projectMembership.projectId,
actorAuthMethod,
actorOrgId
);
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Edit, ProjectPermissionSub.Member);
const deletedPrivilege = await projectUserAdditionalPrivilegeDAL.deleteById(userPrivilege.id);
return deletedPrivilege;
};
const getPrivilegeDetailsById = async ({
privilegeId,
actorOrgId,
actor,
actorId,
actorAuthMethod
}: TGetUserPrivilegeDetailsDTO) => {
const userPrivilege = await projectUserAdditionalPrivilegeDAL.findById(privilegeId);
if (!userPrivilege) throw new BadRequestError({ message: "User additional privilege not found" });
const projectMembership = await projectMembershipDAL.findById(userPrivilege.projectMembershipId);
if (!projectMembership) throw new BadRequestError({ message: "Project membership not found" });
const { permission } = await permissionService.getProjectPermission(
actor,
actorId,
projectMembership.projectId,
actorAuthMethod,
actorOrgId
);
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.Member);
return userPrivilege;
};
const listPrivileges = async ({
projectMembershipId,
actorOrgId,
actor,
actorId,
actorAuthMethod
}: TListUserPrivilegesDTO) => {
const projectMembership = await projectMembershipDAL.findById(projectMembershipId);
if (!projectMembership) throw new BadRequestError({ message: "Project membership not found" });
const { permission } = await permissionService.getProjectPermission(
actor,
actorId,
projectMembership.projectId,
actorAuthMethod,
actorOrgId
);
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.Member);
const userPrivileges = await projectUserAdditionalPrivilegeDAL.find({ projectMembershipId });
return userPrivileges;
};
return {
create,
updateById,
deleteById,
getPrivilegeDetailsById,
listPrivileges
};
};

View File

@@ -0,0 +1,40 @@
import { TProjectPermission } from "@app/lib/types";
export enum ProjectUserAdditionalPrivilegeTemporaryMode {
Relative = "relative"
}
export type TCreateUserPrivilegeDTO = (
| {
permissions: unknown;
projectMembershipId: string;
slug: string;
isTemporary: false;
}
| {
permissions: unknown;
projectMembershipId: string;
slug: string;
isTemporary: true;
temporaryMode: ProjectUserAdditionalPrivilegeTemporaryMode.Relative;
temporaryRange: string;
temporaryAccessStartTime: string;
}
) &
Omit<TProjectPermission, "projectId">;
export type TUpdateUserPrivilegeDTO = { privilegeId: string } & Omit<TProjectPermission, "projectId"> &
Partial<{
permissions: unknown;
slug: string;
isTemporary: boolean;
temporaryMode: ProjectUserAdditionalPrivilegeTemporaryMode.Relative;
temporaryRange: string;
temporaryAccessStartTime: string;
}>;
export type TDeleteUserPrivilegeDTO = Omit<TProjectPermission, "projectId"> & { privilegeId: string };
export type TGetUserPrivilegeDetailsDTO = Omit<TProjectPermission, "projectId"> & { privilegeId: string };
export type TListUserPrivilegesDTO = Omit<TProjectPermission, "projectId"> & { projectMembershipId: string };

View File

@@ -319,6 +319,11 @@ export const samlConfigServiceFactory = ({
const organization = await orgDAL.findOrgById(orgId); const organization = await orgDAL.findOrgById(orgId);
if (!organization) throw new BadRequestError({ message: "Org not found" }); if (!organization) throw new BadRequestError({ message: "Org not found" });
// TODO(dangtony98): remove this after aliases update
if (authProvider === AuthMethod.KEYCLOAK_SAML && appCfg.LICENSE_SERVER_KEY) {
throw new BadRequestError({ message: "Keycloak SAML is not yet available on Infisical Cloud" });
}
if (user) { if (user) {
await userDAL.transaction(async (tx) => { await userDAL.transaction(async (tx) => {
const [orgMembership] = await orgDAL.findMembership( const [orgMembership] = await orgDAL.findMembership(

View File

@@ -5,7 +5,8 @@ export enum SamlProviders {
OKTA_SAML = "okta-saml", OKTA_SAML = "okta-saml",
AZURE_SAML = "azure-saml", AZURE_SAML = "azure-saml",
JUMPCLOUD_SAML = "jumpcloud-saml", JUMPCLOUD_SAML = "jumpcloud-saml",
GOOGLE_SAML = "google-saml" GOOGLE_SAML = "google-saml",
KEYCLOAK_SAML = "keycloak-saml"
} }
export type TCreateSamlCfgDTO = { export type TCreateSamlCfgDTO = {

View File

@@ -9,6 +9,7 @@ import jmespath from "jmespath";
import knex from "knex"; import knex from "knex";
import { getConfig } from "@app/lib/config/env"; import { getConfig } from "@app/lib/config/env";
import { getDbConnectionHost } from "@app/lib/knex";
import { alphaNumericNanoId } from "@app/lib/nanoid"; import { alphaNumericNanoId } from "@app/lib/nanoid";
import { TAssignOp, TDbProviderClients, TDirectAssignOp, THttpProviderFunction } from "../templates/types"; import { TAssignOp, TDbProviderClients, TDirectAssignOp, THttpProviderFunction } from "../templates/types";
@@ -89,7 +90,17 @@ export const secretRotationDbFn = async ({
const appCfg = getConfig(); const appCfg = getConfig();
const ssl = ca ? { rejectUnauthorized: false, ca } : undefined; const ssl = ca ? { rejectUnauthorized: false, ca } : undefined;
if (host === "localhost" || host === "127.0.0.1" || appCfg.DB_CONNECTION_URI.includes(host)) const dbHost = appCfg.DB_HOST || getDbConnectionHost(appCfg.DB_CONNECTION_URI);
if (
host === "localhost" ||
host === "127.0.0.1" ||
// database infisical uses
dbHost === host ||
// internal ips
host === "host.docker.internal" ||
host.match(/^10\.\d+\.\d+\.\d+/) ||
host.match(/^192\.168\.\d+\.\d+/)
)
throw new Error("Invalid db host"); throw new Error("Invalid db host");
const db = knex({ const db = knex({

View File

@@ -194,8 +194,28 @@ export const FOLDERS = {
} }
} as const; } as const;
export const SECRETS = {
ATTACH_TAGS: {
secretName: "The name of the secret to attach tags to.",
secretPath: "The path of the secret to attach tags to.",
type: "The type of the secret to attach tags to. (shared/personal)",
environment: "The slug of the environment where the secret is located",
projectSlug: "The slug of the project where the secret is located",
tagSlugs: "An array of existing tag slugs to attach to the secret."
},
DETACH_TAGS: {
secretName: "The name of the secret to detach tags from.",
secretPath: "The path of the secret to detach tags from.",
type: "The type of the secret to attach tags to. (shared/personal)",
environment: "The slug of the environment where the secret is located",
projectSlug: "The slug of the project where the secret is located",
tagSlugs: "An array of existing tag slugs to detach from the secret."
}
} as const;
export const RAW_SECRETS = { export const RAW_SECRETS = {
LIST: { LIST: {
recursive: "Whether or not to fetch all secrets from the specified base path, and all of its subdirectories.",
workspaceId: "The ID of the project to list secrets from.", workspaceId: "The ID of the project to list secrets from.",
workspaceSlug: "The slug of the project to list secrets from. This parameter is only usable by machine identities.", workspaceSlug: "The slug of the project to list secrets from. This parameter is only usable by machine identities.",
environment: "The slug of the environment to list secrets from.", environment: "The slug of the environment to list secrets from.",
@@ -285,3 +305,251 @@ export const AUDIT_LOGS = {
actor: "The actor to filter the audit logs by." actor: "The actor to filter the audit logs by."
} }
} as const; } as const;
export const DYNAMIC_SECRETS = {
LIST: {
projectSlug: "The slug of the project to create dynamic secret in.",
environmentSlug: "The slug of the environment to list folders from.",
path: "The path to list folders from."
},
LIST_LEAES_BY_NAME: {
projectSlug: "The slug of the project to create dynamic secret in.",
environmentSlug: "The slug of the environment to list folders from.",
path: "The path to list folders from.",
name: "The name of the dynamic secret."
},
GET_BY_NAME: {
projectSlug: "The slug of the project to create dynamic secret in.",
environmentSlug: "The slug of the environment to list folders from.",
path: "The path to list folders from.",
name: "The name of the dynamic secret."
},
CREATE: {
projectSlug: "The slug of the project to create dynamic secret in.",
environmentSlug: "The slug of the environment to create the dynamic secret in.",
path: "The path to create the dynamic secret in.",
name: "The name of the dynamic secret.",
provider: "The type of dynamic secret.",
defaultTTL: "The default TTL that will be applied for all the leases.",
maxTTL: "The maximum limit a TTL can be leases or renewed."
},
UPDATE: {
projectSlug: "The slug of the project to update dynamic secret in.",
environmentSlug: "The slug of the environment to update the dynamic secret in.",
path: "The path to update the dynamic secret in.",
name: "The name of the dynamic secret.",
inputs: "The new partial values for the configurated provider of the dynamic secret",
defaultTTL: "The default TTL that will be applied for all the leases.",
maxTTL: "The maximum limit a TTL can be leases or renewed.",
newName: "The new name for the dynamic secret."
},
DELETE: {
projectSlug: "The slug of the project to delete dynamic secret in.",
environmentSlug: "The slug of the environment to delete the dynamic secret in.",
path: "The path to delete the dynamic secret in.",
name: "The name of the dynamic secret.",
isForced:
"A boolean flag to delete the the dynamic secret from infisical without trying to remove it from external provider. Used when the dynamic secret got modified externally."
}
} as const;
export const DYNAMIC_SECRET_LEASES = {
GET_BY_LEASEID: {
projectSlug: "The slug of the project to create dynamic secret in.",
environmentSlug: "The slug of the environment to list folders from.",
path: "The path to list folders from.",
leaseId: "The ID of the dynamic secret lease."
},
CREATE: {
projectSlug: "The slug of the project of the dynamic secret in.",
environmentSlug: "The slug of the environment of the dynamic secret in.",
path: "The path of the dynamic secret in.",
dynamicSecretName: "The name of the dynamic secret.",
ttl: "The lease lifetime ttl. If not provided the default TTL of dynamic secret will be used."
},
RENEW: {
projectSlug: "The slug of the project of the dynamic secret in.",
environmentSlug: "The slug of the environment of the dynamic secret in.",
path: "The path of the dynamic secret in.",
leaseId: "The ID of the dynamic secret lease.",
ttl: "The renew TTL that gets added with current expiry (ensure it's below max TTL) for a total less than creation time + max TTL."
},
DELETE: {
projectSlug: "The slug of the project of the dynamic secret in.",
environmentSlug: "The slug of the environment of the dynamic secret in.",
path: "The path of the dynamic secret in.",
leaseId: "The ID of the dynamic secret lease.",
isForced:
"A boolean flag to delete the the dynamic secret from infisical without trying to remove it from external provider. Used when the dynamic secret got modified externally."
}
} as const;
export const SECRET_TAGS = {
LIST: {
projectId: "The ID of the project to list tags from."
},
CREATE: {
projectId: "The ID of the project to create the tag in.",
name: "The name of the tag to create.",
slug: "The slug of the tag to create.",
color: "The color of the tag to create."
},
DELETE: {
tagId: "The ID of the tag to delete.",
projectId: "The ID of the project to delete the tag from."
}
} as const;
export const IDENTITY_ADDITIONAL_PRIVILEGE = {
CREATE: {
projectSlug: "The slug of the project of the identity in.",
identityId: "The ID of the identity to delete.",
slug: "The slug of the privilege to create.",
permissions: `The permission object for the privilege.
1. [["read", "secrets", {environment: "dev", secretPath: {$glob: "/"}}]]
2. [["read", "secrets", {environment: "dev"}], ["create", "secrets", {environment: "dev"}]]
2. [["read", "secrets", {environment: "dev"}]]
`,
isPackPermission: "Whether the server should pack(compact) the permission object.",
isTemporary: "Whether the privilege is temporary.",
temporaryMode: "Type of temporary access given. Types: relative",
temporaryRange: "TTL for the temporay time. Eg: 1m, 1h, 1d",
temporaryAccessStartTime: "ISO time for which temporary access should begin."
},
UPDATE: {
projectSlug: "The slug of the project of the identity in.",
identityId: "The ID of the identity to update.",
slug: "The slug of the privilege to update.",
newSlug: "The new slug of the privilege to update.",
permissions: `The permission object for the privilege.
1. [["read", "secrets", {environment: "dev", secretPath: {$glob: "/"}}]]
2. [["read", "secrets", {environment: "dev"}], ["create", "secrets", {environment: "dev"}]]
2. [["read", "secrets", {environment: "dev"}]]
`,
isPackPermission: "Whether the server should pack(compact) the permission object.",
isTemporary: "Whether the privilege is temporary.",
temporaryMode: "Type of temporary access given. Types: relative",
temporaryRange: "TTL for the temporay time. Eg: 1m, 1h, 1d",
temporaryAccessStartTime: "ISO time for which temporary access should begin."
},
DELETE: {
projectSlug: "The slug of the project of the identity in.",
identityId: "The ID of the identity to delete.",
slug: "The slug of the privilege to delete."
},
GET_BY_SLUG: {
projectSlug: "The slug of the project of the identity in.",
identityId: "The ID of the identity to list.",
slug: "The slug of the privilege."
},
LIST: {
projectSlug: "The slug of the project of the identity in.",
identityId: "The ID of the identity to list.",
unpacked: "Whether the system should send the permissions as unpacked"
}
};
export const PROJECT_USER_ADDITIONAL_PRIVILEGE = {
CREATE: {
projectMembershipId: "Project membership id of user",
slug: "The slug of the privilege to create.",
permissions:
"The permission object for the privilege. Refer https://casl.js.org/v6/en/guide/define-rules#the-shape-of-raw-rule to understand the shape",
isPackPermission: "Whether the server should pack(compact) the permission object.",
isTemporary: "Whether the privilege is temporary.",
temporaryMode: "Type of temporary access given. Types: relative",
temporaryRange: "TTL for the temporay time. Eg: 1m, 1h, 1d",
temporaryAccessStartTime: "ISO time for which temporary access should begin."
},
UPDATE: {
privilegeId: "The id of privilege object",
slug: "The slug of the privilege to create.",
newSlug: "The new slug of the privilege to create.",
permissions:
"The permission object for the privilege. Refer https://casl.js.org/v6/en/guide/define-rules#the-shape-of-raw-rule to understand the shape",
isPackPermission: "Whether the server should pack(compact) the permission object.",
isTemporary: "Whether the privilege is temporary.",
temporaryMode: "Type of temporary access given. Types: relative",
temporaryRange: "TTL for the temporay time. Eg: 1m, 1h, 1d",
temporaryAccessStartTime: "ISO time for which temporary access should begin."
},
DELETE: {
privilegeId: "The id of privilege object"
},
GET_BY_PRIVILEGEID: {
privilegeId: "The id of privilege object"
},
LIST: {
projectMembershipId: "Project membership id of user"
}
};
export const INTEGRATION_AUTH = {
GET: {
integrationAuthId: "The id of integration authentication object."
},
DELETE: {
integration: "The slug of the integration to be unauthorized.",
projectId: "The ID of the project to delete the integration auth from."
},
DELETE_BY_ID: {
integrationAuthId: "The id of integration authentication object to delete."
},
CREATE_ACCESS_TOKEN: {
workspaceId: "The ID of the project to create the integration auth for.",
integration: "The slug of integration for the auth object.",
accessId: "The unique authorized access id of the external integration provider.",
accessToken: "The unique authorized access token of the external integration provider.",
url: "",
namespace: "",
refreshToken: "The refresh token for integration authorization."
},
LIST_AUTHORIZATION: {
workspaceId: "The ID of the project to list integration auths for."
}
};
export const INTEGRATION = {
CREATE: {
integrationAuthId: "The ID of the integration auth object to link with integration.",
app: "The name of the external integration providers app entity that you want to sync secrets with. Used in Netlify, GitHub, Vercel integrations.",
isActive: "Whether the integration should be active or disabled.",
appId:
"The ID of the external integration providers app entity that you want to sync secrets with. Used in Netlify, GitHub, Vercel integrations.",
secretPath: "The path of the secrets to sync secrets from.",
sourceEnvironment: "The environment to sync secret from.",
targetEnvironment:
"The target environment of the integration provider. Used in cloudflare pages, TeamCity, Gitlab integrations.",
targetEnvironmentId:
"The target environment id of the integration provider. Used in cloudflare pages, teamcity, gitlab integrations.",
targetService:
"The service based grouping identifier of the external provider. Used in Terraform cloud, Checkly, Railway and NorthFlank",
targetServiceId:
"The service based grouping identifier ID of the external provider. Used in Terraform cloud, Checkly, Railway and NorthFlank",
owner: "External integration providers service entity owner. Used in Github.",
path: "Path to save the synced secrets. Used by Gitlab, AWS Parameter Store, Vault",
region: "AWS region to sync secrets to.",
scope: "Scope of the provider. Used by Github, Qovery",
metadata: {
secretPrefix: "The prefix for the saved secret. Used by GCP",
secretSuffix: "The suffix for the saved secret. Used by GCP",
initialSyncBehavoir: "Type of syncing behavoir with the integration",
shouldAutoRedeploy: "Used by Render to trigger auto deploy",
secretGCPLabel: "The label for the GCP secrets"
}
},
UPDATE: {
integrationId: "The ID of the integration object.",
app: "The name of the external integration providers app entity that you want to sync secrets with. Used in Netlify, GitHub, Vercel integrations.",
appId:
"The ID of the external integration providers app entity that you want to sync secrets with. Used in Netlify, GitHub, Vercel integrations.",
isActive: "Whether the integration should be active or disabled.",
secretPath: "The path of the secrets to sync secrets from.",
owner: "External integration providers service entity owner. Used in Github.",
targetEnvironment:
"The target environment of the integration provider. Used in cloudflare pages, TeamCity, Gitlab integrations.",
environment: "The environment to sync secrets from."
},
DELETE: {
integrationId: "The ID of the integration object."
}
};

View File

@@ -18,6 +18,7 @@ const envSchema = z
DB_CONNECTION_URI: zpStr(z.string().describe("Postgres database connection string")).default( DB_CONNECTION_URI: zpStr(z.string().describe("Postgres database connection string")).default(
`postgresql://${process.env.DB_USER}:${process.env.DB_PASSWORD}@${process.env.DB_HOST}:${process.env.DB_PORT}/${process.env.DB_NAME}` `postgresql://${process.env.DB_USER}:${process.env.DB_PASSWORD}@${process.env.DB_HOST}:${process.env.DB_PORT}/${process.env.DB_NAME}`
), ),
MAX_LEASE_LIMIT: z.coerce.number().default(10000),
DB_ROOT_CERT: zpStr(z.string().describe("Postgres database base64-encoded CA cert").optional()), DB_ROOT_CERT: zpStr(z.string().describe("Postgres database base64-encoded CA cert").optional()),
DB_HOST: zpStr(z.string().describe("Postgres database host").optional()), DB_HOST: zpStr(z.string().describe("Postgres database host").optional()),
DB_PORT: zpStr(z.string().describe("Postgres database port").optional()).default("5432"), DB_PORT: zpStr(z.string().describe("Postgres database port").optional()).default("5432"),
@@ -113,7 +114,8 @@ const envSchema = z
.enum(["true", "false"]) .enum(["true", "false"])
.transform((val) => val === "true") .transform((val) => val === "true")
.optional(), .optional(),
INFISICAL_CLOUD: zodStrBool.default("false") INFISICAL_CLOUD: zodStrBool.default("false"),
MAINTENANCE_MODE: zodStrBool.default("false")
}) })
.transform((data) => ({ .transform((data) => ({
...data, ...data,

View File

@@ -59,6 +59,18 @@ export class BadRequestError extends Error {
} }
} }
export class DisableRotationErrors extends Error {
name: string;
error: unknown;
constructor({ name, error, message }: { message: string; name?: string; error?: unknown }) {
super(message);
this.name = name || "DisableRotationErrors";
this.error = error;
}
}
export class ScimRequestError extends Error { export class ScimRequestError extends Error {
name: string; name: string;

View File

@@ -0,0 +1,11 @@
import { URL } from "url"; // Import the URL class
export const getDbConnectionHost = (urlString: string) => {
try {
const url = new URL(urlString);
// Split hostname and port (if provided)
return url.hostname.split(":")[0];
} catch (error) {
return null;
}
};

View File

@@ -4,6 +4,7 @@ import { Tables } from "knex/types/tables";
import { DatabaseError } from "../errors"; import { DatabaseError } from "../errors";
export * from "./connection";
export * from "./join"; export * from "./join";
export * from "./select"; export * from "./select";

View File

@@ -13,7 +13,7 @@ export type TProjectPermission = {
actorId: string; actorId: string;
projectId: string; projectId: string;
actorAuthMethod: ActorAuthMethod; actorAuthMethod: ActorAuthMethod;
actorOrgId: string | undefined; actorOrgId: string;
}; };
export type RequiredKeys<T> = { export type RequiredKeys<T> = {

View File

@@ -18,7 +18,8 @@ export enum QueueName {
SecretWebhook = "secret-webhook", SecretWebhook = "secret-webhook",
SecretFullRepoScan = "secret-full-repo-scan", SecretFullRepoScan = "secret-full-repo-scan",
SecretPushEventScan = "secret-push-event-scan", SecretPushEventScan = "secret-push-event-scan",
UpgradeProjectToGhost = "upgrade-project-to-ghost" UpgradeProjectToGhost = "upgrade-project-to-ghost",
DynamicSecretRevocation = "dynamic-secret-revocation"
} }
export enum QueueJobs { export enum QueueJobs {
@@ -30,7 +31,9 @@ export enum QueueJobs {
TelemetryInstanceStats = "telemetry-self-hosted-stats", TelemetryInstanceStats = "telemetry-self-hosted-stats",
IntegrationSync = "secret-integration-pull", IntegrationSync = "secret-integration-pull",
SecretScan = "secret-scan", SecretScan = "secret-scan",
UpgradeProjectToGhost = "upgrade-project-to-ghost-job" UpgradeProjectToGhost = "upgrade-project-to-ghost-job",
DynamicSecretRevocation = "dynamic-secret-revocation",
DynamicSecretPruning = "dynamic-secret-pruning"
} }
export type TQueueJobTypes = { export type TQueueJobTypes = {
@@ -86,6 +89,19 @@ export type TQueueJobTypes = {
name: QueueJobs.TelemetryInstanceStats; name: QueueJobs.TelemetryInstanceStats;
payload: undefined; payload: undefined;
}; };
[QueueName.DynamicSecretRevocation]:
| {
name: QueueJobs.DynamicSecretRevocation;
payload: {
leaseId: string;
};
}
| {
name: QueueJobs.DynamicSecretPruning;
payload: {
dynamicSecretCfgId: string;
};
};
}; };
export type TQueueServiceFactory = ReturnType<typeof queueServiceFactory>; export type TQueueServiceFactory = ReturnType<typeof queueServiceFactory>;

View File

@@ -24,6 +24,7 @@ import { fastifyErrHandler } from "./plugins/error-handler";
import { registerExternalNextjs } from "./plugins/external-nextjs"; import { registerExternalNextjs } from "./plugins/external-nextjs";
import { serializerCompiler, validatorCompiler, ZodTypeProvider } from "./plugins/fastify-zod"; import { serializerCompiler, validatorCompiler, ZodTypeProvider } from "./plugins/fastify-zod";
import { fastifyIp } from "./plugins/ip"; import { fastifyIp } from "./plugins/ip";
import { maintenanceMode } from "./plugins/maintenanceMode";
import { fastifySwagger } from "./plugins/swagger"; import { fastifySwagger } from "./plugins/swagger";
import { registerRoutes } from "./routes"; import { registerRoutes } from "./routes";
@@ -72,6 +73,8 @@ export const main = async ({ db, smtp, logger, queue, keyStore }: TMain) => {
} }
await server.register(helmet, { contentSecurityPolicy: false }); await server.register(helmet, { contentSecurityPolicy: false });
await server.register(maintenanceMode);
await server.register(registerRoutes, { smtp, queue, db, keyStore }); await server.register(registerRoutes, { smtp, queue, db, keyStore });
if (appCfg.isProductionMode) { if (appCfg.isProductionMode) {

View File

@@ -20,7 +20,13 @@ export const globalRateLimiterCfg = (): RateLimitPluginOptions => {
export const authRateLimit: RateLimitOptions = { export const authRateLimit: RateLimitOptions = {
timeWindow: 60 * 1000, timeWindow: 60 * 1000,
max: 600, max: 60,
keyGenerator: (req) => req.realIp
};
export const inviteUserRateLimit: RateLimitOptions = {
timeWindow: 60 * 1000,
max: 10,
keyGenerator: (req) => req.realIp keyGenerator: (req) => req.realIp
}; };

View File

@@ -16,7 +16,7 @@ export type TAuthMode =
userId: string; userId: string;
tokenVersionId: string; // the session id of token used tokenVersionId: string; // the session id of token used
user: TUsers; user: TUsers;
orgId?: string; orgId: string;
authMethod: AuthMethod; authMethod: AuthMethod;
} }
| { | {
@@ -119,7 +119,7 @@ export const injectIdentity = fp(async (server: FastifyZodProvider) => {
userId: user.id, userId: user.id,
tokenVersionId, tokenVersionId,
actor, actor,
orgId, orgId: orgId as string,
authMethod: token.authMethod authMethod: token.authMethod
}; };
break; break;

View File

@@ -0,0 +1,12 @@
import fp from "fastify-plugin";
import { getConfig } from "@app/lib/config/env";
export const maintenanceMode = fp(async (fastify) => {
fastify.addHook("onRequest", async (req) => {
const serverEnvs = getConfig();
if (req.url !== "/api/v1/auth/checkAuth" && req.method !== "GET" && serverEnvs.MAINTENANCE_MODE) {
throw new Error("Infisical is in maintenance mode. Please try again later.");
}
});
});

View File

@@ -14,13 +14,13 @@ export const fastifySwagger = fp(async (fastify) => {
version: "0.0.1" version: "0.0.1"
}, },
servers: [ servers: [
{
url: "http://localhost:8080",
description: "Local server"
},
{ {
url: "https://app.infisical.com", url: "https://app.infisical.com",
description: "Production server" description: "Production server"
},
{
url: "http://localhost:8080",
description: "Local server"
} }
], ],
components: { components: {

View File

@@ -5,12 +5,22 @@ import { registerV1EERoutes } from "@app/ee/routes/v1";
import { auditLogDALFactory } from "@app/ee/services/audit-log/audit-log-dal"; import { auditLogDALFactory } from "@app/ee/services/audit-log/audit-log-dal";
import { auditLogQueueServiceFactory } from "@app/ee/services/audit-log/audit-log-queue"; import { auditLogQueueServiceFactory } from "@app/ee/services/audit-log/audit-log-queue";
import { auditLogServiceFactory } from "@app/ee/services/audit-log/audit-log-service"; import { auditLogServiceFactory } from "@app/ee/services/audit-log/audit-log-service";
import { dynamicSecretDALFactory } from "@app/ee/services/dynamic-secret/dynamic-secret-dal";
import { dynamicSecretServiceFactory } from "@app/ee/services/dynamic-secret/dynamic-secret-service";
import { buildDynamicSecretProviders } from "@app/ee/services/dynamic-secret/providers";
import { dynamicSecretLeaseDALFactory } from "@app/ee/services/dynamic-secret-lease/dynamic-secret-lease-dal";
import { dynamicSecretLeaseQueueServiceFactory } from "@app/ee/services/dynamic-secret-lease/dynamic-secret-lease-queue";
import { dynamicSecretLeaseServiceFactory } from "@app/ee/services/dynamic-secret-lease/dynamic-secret-lease-service";
import { identityProjectAdditionalPrivilegeDALFactory } from "@app/ee/services/identity-project-additional-privilege/identity-project-additional-privilege-dal";
import { identityProjectAdditionalPrivilegeServiceFactory } from "@app/ee/services/identity-project-additional-privilege/identity-project-additional-privilege-service";
import { ldapConfigDALFactory } from "@app/ee/services/ldap-config/ldap-config-dal"; import { ldapConfigDALFactory } from "@app/ee/services/ldap-config/ldap-config-dal";
import { ldapConfigServiceFactory } from "@app/ee/services/ldap-config/ldap-config-service"; import { ldapConfigServiceFactory } from "@app/ee/services/ldap-config/ldap-config-service";
import { licenseDALFactory } from "@app/ee/services/license/license-dal"; import { licenseDALFactory } from "@app/ee/services/license/license-dal";
import { licenseServiceFactory } from "@app/ee/services/license/license-service"; import { licenseServiceFactory } from "@app/ee/services/license/license-service";
import { permissionDALFactory } from "@app/ee/services/permission/permission-dal"; import { permissionDALFactory } from "@app/ee/services/permission/permission-dal";
import { permissionServiceFactory } from "@app/ee/services/permission/permission-service"; import { permissionServiceFactory } from "@app/ee/services/permission/permission-service";
import { projectUserAdditionalPrivilegeDALFactory } from "@app/ee/services/project-user-additional-privilege/project-user-additional-privilege-dal";
import { projectUserAdditionalPrivilegeServiceFactory } from "@app/ee/services/project-user-additional-privilege/project-user-additional-privilege-service";
import { samlConfigDALFactory } from "@app/ee/services/saml-config/saml-config-dal"; import { samlConfigDALFactory } from "@app/ee/services/saml-config/saml-config-dal";
import { samlConfigServiceFactory } from "@app/ee/services/saml-config/saml-config-service"; import { samlConfigServiceFactory } from "@app/ee/services/saml-config/saml-config-service";
import { scimDALFactory } from "@app/ee/services/scim/scim-dal"; import { scimDALFactory } from "@app/ee/services/scim/scim-dal";
@@ -143,6 +153,7 @@ export const registerRoutes = async (
const projectDAL = projectDALFactory(db); const projectDAL = projectDALFactory(db);
const projectMembershipDAL = projectMembershipDALFactory(db); const projectMembershipDAL = projectMembershipDALFactory(db);
const projectUserAdditionalPrivilegeDAL = projectUserAdditionalPrivilegeDALFactory(db);
const projectUserMembershipRoleDAL = projectUserMembershipRoleDALFactory(db); const projectUserMembershipRoleDAL = projectUserMembershipRoleDALFactory(db);
const projectRoleDAL = projectRoleDALFactory(db); const projectRoleDAL = projectRoleDALFactory(db);
const projectEnvDAL = projectEnvDALFactory(db); const projectEnvDAL = projectEnvDALFactory(db);
@@ -168,6 +179,7 @@ export const registerRoutes = async (
const identityOrgMembershipDAL = identityOrgDALFactory(db); const identityOrgMembershipDAL = identityOrgDALFactory(db);
const identityProjectDAL = identityProjectDALFactory(db); const identityProjectDAL = identityProjectDALFactory(db);
const identityProjectMembershipRoleDAL = identityProjectMembershipRoleDALFactory(db); const identityProjectMembershipRoleDAL = identityProjectMembershipRoleDALFactory(db);
const identityProjectAdditionalPrivilegeDAL = identityProjectAdditionalPrivilegeDALFactory(db);
const identityUaDAL = identityUaDALFactory(db); const identityUaDAL = identityUaDALFactory(db);
const identityUaClientSecretDAL = identityUaClientSecretDALFactory(db); const identityUaClientSecretDAL = identityUaClientSecretDALFactory(db);
@@ -196,6 +208,8 @@ export const registerRoutes = async (
const gitAppOrgDAL = gitAppDALFactory(db); const gitAppOrgDAL = gitAppDALFactory(db);
const secretScanningDAL = secretScanningDALFactory(db); const secretScanningDAL = secretScanningDALFactory(db);
const licenseDAL = licenseDALFactory(db); const licenseDAL = licenseDALFactory(db);
const dynamicSecretDAL = dynamicSecretDALFactory(db);
const dynamicSecretLeaseDAL = dynamicSecretLeaseDALFactory(db);
const permissionService = permissionServiceFactory({ const permissionService = permissionServiceFactory({
permissionDAL, permissionDAL,
@@ -337,6 +351,11 @@ export const registerRoutes = async (
projectRoleDAL, projectRoleDAL,
licenseService licenseService
}); });
const projectUserAdditionalPrivilegeService = projectUserAdditionalPrivilegeServiceFactory({
permissionService,
projectMembershipDAL,
projectUserAdditionalPrivilegeDAL
});
const projectKeyService = projectKeyServiceFactory({ const projectKeyService = projectKeyServiceFactory({
permissionService, permissionService,
projectKeyDAL, projectKeyDAL,
@@ -379,7 +398,8 @@ export const registerRoutes = async (
folderDAL, folderDAL,
licenseService, licenseService,
projectUserMembershipRoleDAL, projectUserMembershipRoleDAL,
identityProjectMembershipRoleDAL identityProjectMembershipRoleDAL,
keyStore
}); });
const projectEnvService = projectEnvServiceFactory({ const projectEnvService = projectEnvServiceFactory({
@@ -471,6 +491,7 @@ export const registerRoutes = async (
snapshotService, snapshotService,
secretQueueService, secretQueueService,
secretImportDAL, secretImportDAL,
projectEnvDAL,
projectBotService projectBotService
}); });
const sarService = secretApprovalRequestServiceFactory({ const sarService = secretApprovalRequestServiceFactory({
@@ -540,6 +561,12 @@ export const registerRoutes = async (
identityProjectMembershipRoleDAL, identityProjectMembershipRoleDAL,
projectRoleDAL projectRoleDAL
}); });
const identityProjectAdditionalPrivilegeService = identityProjectAdditionalPrivilegeServiceFactory({
projectDAL,
identityProjectAdditionalPrivilegeDAL,
permissionService,
identityProjectDAL
});
const identityUaService = identityUaServiceFactory({ const identityUaService = identityUaServiceFactory({
identityOrgMembershipDAL, identityOrgMembershipDAL,
permissionService, permissionService,
@@ -550,6 +577,34 @@ export const registerRoutes = async (
licenseService licenseService
}); });
const dynamicSecretProviders = buildDynamicSecretProviders();
const dynamicSecretQueueService = dynamicSecretLeaseQueueServiceFactory({
queueService,
dynamicSecretLeaseDAL,
dynamicSecretProviders,
dynamicSecretDAL
});
const dynamicSecretService = dynamicSecretServiceFactory({
projectDAL,
dynamicSecretQueueService,
dynamicSecretDAL,
dynamicSecretLeaseDAL,
dynamicSecretProviders,
folderDAL,
permissionService,
licenseService
});
const dynamicSecretLeaseService = dynamicSecretLeaseServiceFactory({
projectDAL,
permissionService,
dynamicSecretQueueService,
dynamicSecretDAL,
dynamicSecretLeaseDAL,
dynamicSecretProviders,
folderDAL,
licenseService
});
await superAdminService.initServerCfg(); await superAdminService.initServerCfg();
// //
// setup the communication with license key server // setup the communication with license key server
@@ -591,6 +646,8 @@ export const registerRoutes = async (
secretApprovalPolicy: sapService, secretApprovalPolicy: sapService,
secretApprovalRequest: sarService, secretApprovalRequest: sarService,
secretRotation: secretRotationService, secretRotation: secretRotationService,
dynamicSecret: dynamicSecretService,
dynamicSecretLease: dynamicSecretLeaseService,
snapshot: snapshotService, snapshot: snapshotService,
saml: samlService, saml: samlService,
ldap: ldapService, ldap: ldapService,
@@ -600,7 +657,9 @@ export const registerRoutes = async (
trustedIp: trustedIpService, trustedIp: trustedIpService,
scim: scimService, scim: scimService,
secretBlindIndex: secretBlindIndexService, secretBlindIndex: secretBlindIndexService,
telemetry: telemetryService telemetry: telemetryService,
projectUserAdditionalPrivilege: projectUserAdditionalPrivilegeService,
identityProjectAdditionalPrivilege: identityProjectAdditionalPrivilegeService
}); });
server.decorate<FastifyZodProvider["store"]>("store", { server.decorate<FastifyZodProvider["store"]>("store", {

View File

@@ -1,6 +1,11 @@
import { z } from "zod"; import { z } from "zod";
import { IntegrationAuthsSchema, SecretApprovalPoliciesSchema, UsersSchema } from "@app/db/schemas"; import {
DynamicSecretsSchema,
IntegrationAuthsSchema,
SecretApprovalPoliciesSchema,
UsersSchema
} from "@app/db/schemas";
// sometimes the return data must be santizied to avoid leaking important values // sometimes the return data must be santizied to avoid leaking important values
// always prefer pick over omit in zod // always prefer pick over omit in zod
@@ -56,3 +61,11 @@ export const secretRawSchema = z.object({
secretValue: z.string(), secretValue: z.string(),
secretComment: z.string().optional() secretComment: z.string().optional()
}); });
export const SanitizedDynamicSecretSchema = DynamicSecretsSchema.omit({
inputIV: true,
inputTag: true,
inputCiphertext: true,
keyEncoding: true,
algorithm: true
});

View File

@@ -16,13 +16,16 @@ export const registerAdminRouter = async (server: FastifyZodProvider) => {
schema: { schema: {
response: { response: {
200: z.object({ 200: z.object({
config: SuperAdminSchema.omit({ createdAt: true, updatedAt: true }) config: SuperAdminSchema.omit({ createdAt: true, updatedAt: true }).merge(
z.object({ isMigrationModeOn: z.boolean() })
)
}) })
} }
}, },
handler: async () => { handler: async () => {
const config = await getServerCfg(); const config = await getServerCfg();
return { config }; const serverEnvs = getConfig();
return { config: { ...config, isMigrationModeOn: serverEnvs.MAINTENANCE_MODE } };
} }
}); });

View File

@@ -1,6 +1,7 @@
import { z } from "zod"; import { z } from "zod";
import { EventType } from "@app/ee/services/audit-log/audit-log-types"; import { EventType } from "@app/ee/services/audit-log/audit-log-types";
import { INTEGRATION_AUTH } from "@app/lib/api-docs";
import { verifyAuth } from "@app/server/plugins/auth/verify-auth"; import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
import { AuthMode } from "@app/services/auth/auth-type"; import { AuthMode } from "@app/services/auth/auth-type";
@@ -10,8 +11,14 @@ export const registerIntegrationAuthRouter = async (server: FastifyZodProvider)
server.route({ server.route({
url: "/integration-options", url: "/integration-options",
method: "GET", method: "GET",
onRequest: verifyAuth([AuthMode.JWT]), onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
schema: { schema: {
description: "List of integrations available.",
security: [
{
bearerAuth: []
}
],
response: { response: {
200: z.object({ 200: z.object({
integrationOptions: z integrationOptions: z
@@ -38,10 +45,16 @@ export const registerIntegrationAuthRouter = async (server: FastifyZodProvider)
server.route({ server.route({
url: "/:integrationAuthId", url: "/:integrationAuthId",
method: "GET", method: "GET",
onRequest: verifyAuth([AuthMode.JWT]), onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
schema: { schema: {
description: "Get details of an integration authorization by auth object id.",
security: [
{
bearerAuth: []
}
],
params: z.object({ params: z.object({
integrationAuthId: z.string().trim() integrationAuthId: z.string().trim().describe(INTEGRATION_AUTH.GET.integrationAuthId)
}), }),
response: { response: {
200: z.object({ 200: z.object({
@@ -64,11 +77,17 @@ export const registerIntegrationAuthRouter = async (server: FastifyZodProvider)
server.route({ server.route({
url: "/", url: "/",
method: "DELETE", method: "DELETE",
onRequest: verifyAuth([AuthMode.JWT]), onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
schema: { schema: {
description: "Remove all integration's auth object from the project.",
security: [
{
bearerAuth: []
}
],
querystring: z.object({ querystring: z.object({
integration: z.string().trim(), integration: z.string().trim().describe(INTEGRATION_AUTH.DELETE.integration),
projectId: z.string().trim() projectId: z.string().trim().describe(INTEGRATION_AUTH.DELETE.projectId)
}), }),
response: { response: {
200: z.object({ 200: z.object({
@@ -104,10 +123,16 @@ export const registerIntegrationAuthRouter = async (server: FastifyZodProvider)
server.route({ server.route({
url: "/:integrationAuthId", url: "/:integrationAuthId",
method: "DELETE", method: "DELETE",
onRequest: verifyAuth([AuthMode.JWT]), onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
schema: { schema: {
description: "Remove an integration auth object by object id.",
security: [
{
bearerAuth: []
}
],
params: z.object({ params: z.object({
integrationAuthId: z.string().trim() integrationAuthId: z.string().trim().describe(INTEGRATION_AUTH.DELETE_BY_ID.integrationAuthId)
}), }),
response: { response: {
200: z.object({ 200: z.object({
@@ -183,16 +208,22 @@ export const registerIntegrationAuthRouter = async (server: FastifyZodProvider)
server.route({ server.route({
url: "/access-token", url: "/access-token",
method: "POST", method: "POST",
onRequest: verifyAuth([AuthMode.JWT]), onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
schema: { schema: {
description: "Create the integration authentication object required for syncing secrets.",
security: [
{
bearerAuth: []
}
],
body: z.object({ body: z.object({
workspaceId: z.string().trim(), workspaceId: z.string().trim().describe(INTEGRATION_AUTH.CREATE_ACCESS_TOKEN.workspaceId),
integration: z.string().trim(), integration: z.string().trim().describe(INTEGRATION_AUTH.CREATE_ACCESS_TOKEN.integration),
accessId: z.string().trim().optional(), accessId: z.string().trim().optional().describe(INTEGRATION_AUTH.CREATE_ACCESS_TOKEN.accessId),
accessToken: z.string().trim().optional(), accessToken: z.string().trim().optional().describe(INTEGRATION_AUTH.CREATE_ACCESS_TOKEN.accessToken),
url: z.string().url().trim().optional(), url: z.string().url().trim().optional().describe(INTEGRATION_AUTH.CREATE_ACCESS_TOKEN.url),
namespace: z.string().trim().optional(), namespace: z.string().trim().optional().describe(INTEGRATION_AUTH.CREATE_ACCESS_TOKEN.namespace),
refreshToken: z.string().trim().optional() refreshToken: z.string().trim().optional().describe(INTEGRATION_AUTH.CREATE_ACCESS_TOKEN.refreshToken)
}), }),
response: { response: {
200: z.object({ 200: z.object({

View File

@@ -2,6 +2,7 @@ import { z } from "zod";
import { IntegrationsSchema } from "@app/db/schemas"; import { IntegrationsSchema } from "@app/db/schemas";
import { EventType } from "@app/ee/services/audit-log/audit-log-types"; import { EventType } from "@app/ee/services/audit-log/audit-log-types";
import { INTEGRATION } from "@app/lib/api-docs";
import { removeTrailingSlash, shake } from "@app/lib/fn"; import { removeTrailingSlash, shake } from "@app/lib/fn";
import { getTelemetryDistinctId } from "@app/server/lib/telemetry"; import { getTelemetryDistinctId } from "@app/server/lib/telemetry";
import { verifyAuth } from "@app/server/plugins/auth/verify-auth"; import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
@@ -13,33 +14,45 @@ export const registerIntegrationRouter = async (server: FastifyZodProvider) => {
url: "/", url: "/",
method: "POST", method: "POST",
schema: { schema: {
description: "Create an integration to sync secrets.",
security: [
{
bearerAuth: []
}
],
body: z.object({ body: z.object({
integrationAuthId: z.string().trim(), integrationAuthId: z.string().trim().describe(INTEGRATION.CREATE.integrationAuthId),
app: z.string().trim().optional(), app: z.string().trim().optional().describe(INTEGRATION.CREATE.app),
isActive: z.boolean(), isActive: z.boolean().describe(INTEGRATION.CREATE.isActive).default(true),
appId: z.string().trim().optional(), appId: z.string().trim().optional().describe(INTEGRATION.CREATE.appId),
secretPath: z.string().trim().default("/").transform(removeTrailingSlash), secretPath: z
sourceEnvironment: z.string().trim(), .string()
targetEnvironment: z.string().trim().optional(), .trim()
targetEnvironmentId: z.string().trim().optional(), .default("/")
targetService: z.string().trim().optional(), .transform(removeTrailingSlash)
targetServiceId: z.string().trim().optional(), .describe(INTEGRATION.CREATE.secretPath),
owner: z.string().trim().optional(), sourceEnvironment: z.string().trim().describe(INTEGRATION.CREATE.sourceEnvironment),
path: z.string().trim().optional(), targetEnvironment: z.string().trim().optional().describe(INTEGRATION.CREATE.targetEnvironment),
region: z.string().trim().optional(), targetEnvironmentId: z.string().trim().optional().describe(INTEGRATION.CREATE.targetEnvironmentId),
scope: z.string().trim().optional(), targetService: z.string().trim().optional().describe(INTEGRATION.CREATE.targetService),
targetServiceId: z.string().trim().optional().describe(INTEGRATION.CREATE.targetServiceId),
owner: z.string().trim().optional().describe(INTEGRATION.CREATE.owner),
path: z.string().trim().optional().describe(INTEGRATION.CREATE.path),
region: z.string().trim().optional().describe(INTEGRATION.CREATE.region),
scope: z.string().trim().optional().describe(INTEGRATION.CREATE.scope),
metadata: z metadata: z
.object({ .object({
secretPrefix: z.string().optional(), secretPrefix: z.string().optional().describe(INTEGRATION.CREATE.metadata.secretPrefix),
secretSuffix: z.string().optional(), secretSuffix: z.string().optional().describe(INTEGRATION.CREATE.metadata.secretSuffix),
initialSyncBehavior: z.string().optional(), initialSyncBehavior: z.string().optional().describe(INTEGRATION.CREATE.metadata.initialSyncBehavoir),
shouldAutoRedeploy: z.boolean().optional(), shouldAutoRedeploy: z.boolean().optional().describe(INTEGRATION.CREATE.metadata.shouldAutoRedeploy),
secretGCPLabel: z secretGCPLabel: z
.object({ .object({
labelName: z.string(), labelName: z.string(),
labelValue: z.string() labelValue: z.string()
}) })
.optional() .optional()
.describe(INTEGRATION.CREATE.metadata.secretGCPLabel)
}) })
.optional() .optional()
}), }),
@@ -49,7 +62,7 @@ export const registerIntegrationRouter = async (server: FastifyZodProvider) => {
}) })
} }
}, },
onRequest: verifyAuth([AuthMode.JWT]), onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
handler: async (req) => { handler: async (req) => {
const { integration, integrationAuth } = await server.services.integration.createIntegration({ const { integration, integrationAuth } = await server.services.integration.createIntegration({
actorId: req.permission.id, actorId: req.permission.id,
@@ -102,17 +115,28 @@ export const registerIntegrationRouter = async (server: FastifyZodProvider) => {
url: "/:integrationId", url: "/:integrationId",
method: "PATCH", method: "PATCH",
schema: { schema: {
description: "Update an integration by integration id",
security: [
{
bearerAuth: []
}
],
params: z.object({ params: z.object({
integrationId: z.string().trim() integrationId: z.string().trim().describe(INTEGRATION.UPDATE.integrationId)
}), }),
body: z.object({ body: z.object({
app: z.string().trim(), app: z.string().trim().describe(INTEGRATION.UPDATE.app),
appId: z.string().trim(), appId: z.string().trim().describe(INTEGRATION.UPDATE.appId),
isActive: z.boolean(), isActive: z.boolean().describe(INTEGRATION.UPDATE.isActive),
secretPath: z.string().trim().default("/").transform(removeTrailingSlash), secretPath: z
targetEnvironment: z.string().trim(), .string()
owner: z.string().trim(), .trim()
environment: z.string().trim() .default("/")
.transform(removeTrailingSlash)
.describe(INTEGRATION.UPDATE.secretPath),
targetEnvironment: z.string().trim().describe(INTEGRATION.UPDATE.targetEnvironment),
owner: z.string().trim().describe(INTEGRATION.UPDATE.owner),
environment: z.string().trim().describe(INTEGRATION.UPDATE.environment)
}), }),
response: { response: {
200: z.object({ 200: z.object({
@@ -120,7 +144,7 @@ export const registerIntegrationRouter = async (server: FastifyZodProvider) => {
}) })
} }
}, },
onRequest: verifyAuth([AuthMode.JWT]), onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
handler: async (req) => { handler: async (req) => {
const integration = await server.services.integration.updateIntegration({ const integration = await server.services.integration.updateIntegration({
actorId: req.permission.id, actorId: req.permission.id,
@@ -138,8 +162,14 @@ export const registerIntegrationRouter = async (server: FastifyZodProvider) => {
url: "/:integrationId", url: "/:integrationId",
method: "DELETE", method: "DELETE",
schema: { schema: {
description: "Remove an integration using the integration object ID",
security: [
{
bearerAuth: []
}
],
params: z.object({ params: z.object({
integrationId: z.string().trim() integrationId: z.string().trim().describe(INTEGRATION.DELETE.integrationId)
}), }),
response: { response: {
200: z.object({ 200: z.object({
@@ -147,7 +177,7 @@ export const registerIntegrationRouter = async (server: FastifyZodProvider) => {
}) })
} }
}, },
onRequest: verifyAuth([AuthMode.JWT]), onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
handler: async (req) => { handler: async (req) => {
const integration = await server.services.integration.deleteIntegration({ const integration = await server.services.integration.deleteIntegration({
actorId: req.permission.id, actorId: req.permission.id,

View File

@@ -1,6 +1,7 @@
import { z } from "zod"; import { z } from "zod";
import { UsersSchema } from "@app/db/schemas"; import { UsersSchema } from "@app/db/schemas";
import { inviteUserRateLimit } from "@app/server/config/rateLimiter";
import { getTelemetryDistinctId } from "@app/server/lib/telemetry"; import { getTelemetryDistinctId } from "@app/server/lib/telemetry";
import { verifyAuth } from "@app/server/plugins/auth/verify-auth"; import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
import { ActorType, AuthMode } from "@app/services/auth/auth-type"; import { ActorType, AuthMode } from "@app/services/auth/auth-type";
@@ -9,6 +10,9 @@ import { PostHogEventTypes } from "@app/services/telemetry/telemetry-types";
export const registerInviteOrgRouter = async (server: FastifyZodProvider) => { export const registerInviteOrgRouter = async (server: FastifyZodProvider) => {
server.route({ server.route({
url: "/signup", url: "/signup",
config: {
rateLimit: inviteUserRateLimit
},
method: "POST", method: "POST",
schema: { schema: {
body: z.object({ body: z.object({

View File

@@ -158,7 +158,7 @@ export const registerProjectMembershipRouter = async (server: FastifyZodProvider
]) ])
) )
.min(1) .min(1)
.refine((data) => data.some(({ isTemporary }) => !isTemporary), "At least long lived role is required") .refine((data) => data.some(({ isTemporary }) => !isTemporary), "At least one long lived role is required")
.describe(PROJECTS.UPDATE_USER_MEMBERSHIP.roles) .describe(PROJECTS.UPDATE_USER_MEMBERSHIP.roles)
}), }),
response: { response: {

View File

@@ -7,7 +7,7 @@ import {
UserEncryptionKeysSchema, UserEncryptionKeysSchema,
UsersSchema UsersSchema
} from "@app/db/schemas"; } from "@app/db/schemas";
import { PROJECTS } from "@app/lib/api-docs"; import { INTEGRATION_AUTH, PROJECTS } from "@app/lib/api-docs";
import { verifyAuth } from "@app/server/plugins/auth/verify-auth"; import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
import { AuthMode } from "@app/services/auth/auth-type"; import { AuthMode } from "@app/services/auth/auth-type";
import { ProjectFilterType } from "@app/services/project/project-types"; import { ProjectFilterType } from "@app/services/project/project-types";
@@ -332,8 +332,14 @@ export const registerProjectRouter = async (server: FastifyZodProvider) => {
url: "/:workspaceId/authorizations", url: "/:workspaceId/authorizations",
method: "GET", method: "GET",
schema: { schema: {
description: "List integration auth objects for a workspace.",
security: [
{
bearerAuth: []
}
],
params: z.object({ params: z.object({
workspaceId: z.string().trim() workspaceId: z.string().trim().describe(INTEGRATION_AUTH.LIST_AUTHORIZATION.workspaceId)
}), }),
response: { response: {
200: z.object({ 200: z.object({
@@ -341,7 +347,7 @@ export const registerProjectRouter = async (server: FastifyZodProvider) => {
}) })
} }
}, },
onRequest: verifyAuth([AuthMode.JWT]), onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
handler: async (req) => { handler: async (req) => {
const authorizations = await server.services.integrationAuth.listIntegrationAuthByProjectId({ const authorizations = await server.services.integrationAuth.listIntegrationAuthByProjectId({
actorId: req.permission.id, actorId: req.permission.id,

View File

@@ -1,6 +1,7 @@
import { z } from "zod"; import { z } from "zod";
import { SecretTagsSchema } from "@app/db/schemas"; import { SecretTagsSchema } from "@app/db/schemas";
import { SECRET_TAGS } from "@app/lib/api-docs";
import { verifyAuth } from "@app/server/plugins/auth/verify-auth"; import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
import { AuthMode } from "@app/services/auth/auth-type"; import { AuthMode } from "@app/services/auth/auth-type";
@@ -10,7 +11,7 @@ export const registerSecretTagRouter = async (server: FastifyZodProvider) => {
method: "GET", method: "GET",
schema: { schema: {
params: z.object({ params: z.object({
projectId: z.string().trim() projectId: z.string().trim().describe(SECRET_TAGS.LIST.projectId)
}), }),
response: { response: {
200: z.object({ 200: z.object({
@@ -36,12 +37,12 @@ export const registerSecretTagRouter = async (server: FastifyZodProvider) => {
method: "POST", method: "POST",
schema: { schema: {
params: z.object({ params: z.object({
projectId: z.string().trim() projectId: z.string().trim().describe(SECRET_TAGS.CREATE.projectId)
}), }),
body: z.object({ body: z.object({
name: z.string().trim(), name: z.string().trim().describe(SECRET_TAGS.CREATE.name),
slug: z.string().trim(), slug: z.string().trim().describe(SECRET_TAGS.CREATE.slug),
color: z.string() color: z.string().trim().describe(SECRET_TAGS.CREATE.color)
}), }),
response: { response: {
200: z.object({ 200: z.object({
@@ -68,8 +69,8 @@ export const registerSecretTagRouter = async (server: FastifyZodProvider) => {
method: "DELETE", method: "DELETE",
schema: { schema: {
params: z.object({ params: z.object({
projectId: z.string().trim(), projectId: z.string().trim().describe(SECRET_TAGS.DELETE.projectId),
tagId: z.string().trim() tagId: z.string().trim().describe(SECRET_TAGS.DELETE.tagId)
}), }),
response: { response: {
200: z.object({ 200: z.object({

View File

@@ -4,7 +4,6 @@ import { z } from "zod";
import { ProjectKeysSchema, ProjectsSchema } from "@app/db/schemas"; import { ProjectKeysSchema, ProjectsSchema } from "@app/db/schemas";
import { EventType } from "@app/ee/services/audit-log/audit-log-types"; import { EventType } from "@app/ee/services/audit-log/audit-log-types";
import { PROJECTS } from "@app/lib/api-docs"; import { PROJECTS } from "@app/lib/api-docs";
import { authRateLimit } from "@app/server/config/rateLimiter";
import { getTelemetryDistinctId } from "@app/server/lib/telemetry"; import { getTelemetryDistinctId } from "@app/server/lib/telemetry";
import { verifyAuth } from "@app/server/plugins/auth/verify-auth"; import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
import { AuthMode } from "@app/services/auth/auth-type"; import { AuthMode } from "@app/services/auth/auth-type";
@@ -136,9 +135,6 @@ export const registerProjectRouter = async (server: FastifyZodProvider) => {
server.route({ server.route({
method: "POST", method: "POST",
url: "/", url: "/",
config: {
rateLimit: authRateLimit
},
schema: { schema: {
body: z.object({ body: z.object({
projectName: z.string().trim().describe(PROJECTS.CREATE.projectName), projectName: z.string().trim().describe(PROJECTS.CREATE.projectName),
@@ -150,8 +146,7 @@ export const registerProjectRouter = async (server: FastifyZodProvider) => {
message: "Slug must be a valid slug" message: "Slug must be a valid slug"
}) })
.optional() .optional()
.describe(PROJECTS.CREATE.slug), .describe(PROJECTS.CREATE.slug)
organizationSlug: z.string().trim().describe(PROJECTS.CREATE.organizationSlug)
}), }),
response: { response: {
200: z.object({ 200: z.object({
@@ -166,7 +161,6 @@ export const registerProjectRouter = async (server: FastifyZodProvider) => {
actor: req.permission.type, actor: req.permission.type,
actorOrgId: req.permission.orgId, actorOrgId: req.permission.orgId,
actorAuthMethod: req.permission.authMethod, actorAuthMethod: req.permission.authMethod,
orgSlug: req.body.organizationSlug,
workspaceName: req.body.projectName, workspaceName: req.body.projectName,
slug: req.body.slug slug: req.body.slug
}); });

View File

@@ -10,7 +10,7 @@ import {
} from "@app/db/schemas"; } from "@app/db/schemas";
import { EventType } from "@app/ee/services/audit-log/audit-log-types"; import { EventType } from "@app/ee/services/audit-log/audit-log-types";
import { CommitType } from "@app/ee/services/secret-approval-request/secret-approval-request-types"; import { CommitType } from "@app/ee/services/secret-approval-request/secret-approval-request-types";
import { RAW_SECRETS } from "@app/lib/api-docs"; import { RAW_SECRETS, SECRETS } from "@app/lib/api-docs";
import { BadRequestError } from "@app/lib/errors"; import { BadRequestError } from "@app/lib/errors";
import { removeTrailingSlash } from "@app/lib/fn"; import { removeTrailingSlash } from "@app/lib/fn";
import { getTelemetryDistinctId } from "@app/server/lib/telemetry"; import { getTelemetryDistinctId } from "@app/server/lib/telemetry";
@@ -23,6 +23,124 @@ import { PostHogEventTypes } from "@app/services/telemetry/telemetry-types";
import { secretRawSchema } from "../sanitizedSchemas"; import { secretRawSchema } from "../sanitizedSchemas";
export const registerSecretRouter = async (server: FastifyZodProvider) => { export const registerSecretRouter = async (server: FastifyZodProvider) => {
server.route({
url: "/tags/:secretName",
method: "POST",
schema: {
description: "Attach tags to a secret",
security: [
{
bearerAuth: []
}
],
params: z.object({
secretName: z.string().trim().describe(SECRETS.ATTACH_TAGS.secretName)
}),
body: z.object({
projectSlug: z.string().trim().describe(SECRETS.ATTACH_TAGS.projectSlug),
environment: z.string().trim().describe(SECRETS.ATTACH_TAGS.environment),
secretPath: z
.string()
.trim()
.default("/")
.transform(removeTrailingSlash)
.describe(SECRETS.ATTACH_TAGS.secretPath),
type: z.nativeEnum(SecretType).default(SecretType.Shared).describe(SECRETS.ATTACH_TAGS.type),
tagSlugs: z.string().array().min(1).describe(SECRETS.ATTACH_TAGS.tagSlugs)
}),
response: {
200: z.object({
secret: SecretsSchema.omit({ secretBlindIndex: true }).merge(
z.object({
tags: SecretTagsSchema.pick({
id: true,
slug: true,
name: true,
color: true
}).array()
})
)
})
}
},
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
handler: async (req) => {
const secret = await server.services.secret.attachTags({
secretName: req.params.secretName,
tagSlugs: req.body.tagSlugs,
path: req.body.secretPath,
environment: req.body.environment,
type: req.body.type,
projectSlug: req.body.projectSlug,
actorId: req.permission.id,
actor: req.permission.type,
actorAuthMethod: req.permission.authMethod,
actorOrgId: req.permission.orgId
});
return { secret };
}
});
server.route({
url: "/tags/:secretName",
method: "DELETE",
schema: {
description: "Detach tags from a secret",
security: [
{
bearerAuth: []
}
],
params: z.object({
secretName: z.string().trim().describe(SECRETS.DETACH_TAGS.secretName)
}),
body: z.object({
projectSlug: z.string().trim().describe(SECRETS.DETACH_TAGS.projectSlug),
environment: z.string().trim().describe(SECRETS.DETACH_TAGS.environment),
secretPath: z
.string()
.trim()
.default("/")
.transform(removeTrailingSlash)
.describe(SECRETS.DETACH_TAGS.secretPath),
type: z.nativeEnum(SecretType).default(SecretType.Shared).describe(SECRETS.DETACH_TAGS.type),
tagSlugs: z.string().array().min(1).describe(SECRETS.DETACH_TAGS.tagSlugs)
}),
response: {
200: z.object({
secret: SecretsSchema.omit({ secretBlindIndex: true }).merge(
z.object({
tags: SecretTagsSchema.pick({
id: true,
slug: true,
name: true,
color: true
}).array()
})
)
})
}
},
onRequest: verifyAuth([AuthMode.IDENTITY_ACCESS_TOKEN]),
handler: async (req) => {
const secret = await server.services.secret.detachTags({
secretName: req.params.secretName,
tagSlugs: req.body.tagSlugs,
path: req.body.secretPath,
environment: req.body.environment,
type: req.body.type,
projectSlug: req.body.projectSlug,
actorId: req.permission.id,
actor: req.permission.type,
actorAuthMethod: req.permission.authMethod,
actorOrgId: req.permission.orgId
});
return { secret };
}
});
server.route({ server.route({
url: "/raw", url: "/raw",
method: "GET", method: "GET",
@@ -39,6 +157,11 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => {
workspaceSlug: z.string().trim().optional().describe(RAW_SECRETS.LIST.workspaceSlug), workspaceSlug: z.string().trim().optional().describe(RAW_SECRETS.LIST.workspaceSlug),
environment: z.string().trim().optional().describe(RAW_SECRETS.LIST.environment), environment: z.string().trim().optional().describe(RAW_SECRETS.LIST.environment),
secretPath: z.string().trim().default("/").transform(removeTrailingSlash).describe(RAW_SECRETS.LIST.secretPath), secretPath: z.string().trim().default("/").transform(removeTrailingSlash).describe(RAW_SECRETS.LIST.secretPath),
recursive: z
.enum(["true", "false"])
.default("false")
.transform((value) => value === "true")
.describe(RAW_SECRETS.LIST.recursive),
include_imports: z include_imports: z
.enum(["true", "false"]) .enum(["true", "false"])
.default("false") .default("false")
@@ -47,7 +170,11 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => {
}), }),
response: { response: {
200: z.object({ 200: z.object({
secrets: secretRawSchema.array(), secrets: secretRawSchema
.extend({
secretPath: z.string().optional()
})
.array(),
imports: z imports: z
.object({ .object({
secretPath: z.string(), secretPath: z.string(),
@@ -100,7 +227,8 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => {
actorAuthMethod: req.permission.authMethod, actorAuthMethod: req.permission.authMethod,
projectId: workspaceId, projectId: workspaceId,
path: secretPath, path: secretPath,
includeImports: req.query.include_imports includeImports: req.query.include_imports,
recursive: req.query.recursive
}); });
await server.services.auditLog.createAuditLog({ await server.services.auditLog.createAuditLog({
@@ -478,6 +606,10 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => {
workspaceId: z.string().trim(), workspaceId: z.string().trim(),
environment: z.string().trim(), environment: z.string().trim(),
secretPath: z.string().trim().default("/").transform(removeTrailingSlash), secretPath: z.string().trim().default("/").transform(removeTrailingSlash),
recursive: z
.enum(["true", "false"])
.default("false")
.transform((value) => value === "true"),
include_imports: z include_imports: z
.enum(["true", "false"]) .enum(["true", "false"])
.default("false") .default("false")
@@ -486,19 +618,18 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => {
response: { response: {
200: z.object({ 200: z.object({
secrets: SecretsSchema.omit({ secretBlindIndex: true }) secrets: SecretsSchema.omit({ secretBlindIndex: true })
.merge( .extend({
z.object({ _id: z.string(),
_id: z.string(), workspace: z.string(),
workspace: z.string(), environment: z.string(),
environment: z.string(), secretPath: z.string().optional(),
tags: SecretTagsSchema.pick({ tags: SecretTagsSchema.pick({
id: true, id: true,
slug: true, slug: true,
name: true, name: true,
color: true color: true
}).array() }).array()
}) })
)
.array(), .array(),
imports: z imports: z
.object({ .object({
@@ -530,7 +661,8 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => {
environment: req.query.environment, environment: req.query.environment,
projectId: req.query.workspaceId, projectId: req.query.workspaceId,
path: req.query.secretPath, path: req.query.secretPath,
includeImports: req.query.include_imports includeImports: req.query.include_imports,
recursive: req.query.recursive
}); });
await server.services.auditLog.createAuditLog({ await server.services.auditLog.createAuditLog({

View File

@@ -7,6 +7,7 @@ export enum AuthMethod {
AZURE_SAML = "azure-saml", AZURE_SAML = "azure-saml",
JUMPCLOUD_SAML = "jumpcloud-saml", JUMPCLOUD_SAML = "jumpcloud-saml",
GOOGLE_SAML = "google-saml", GOOGLE_SAML = "google-saml",
KEYCLOAK_SAML = "keycloak-saml",
LDAP = "ldap" LDAP = "ldap"
} }

View File

@@ -25,6 +25,11 @@ export const identityProjectDALFactory = (db: TDbClient) => {
`${TableName.IdentityProjectMembershipRole}.customRoleId`, `${TableName.IdentityProjectMembershipRole}.customRoleId`,
`${TableName.ProjectRoles}.id` `${TableName.ProjectRoles}.id`
) )
.leftJoin(
TableName.IdentityProjectAdditionalPrivilege,
`${TableName.IdentityProjectMembership}.id`,
`${TableName.IdentityProjectAdditionalPrivilege}.projectMembershipId`
)
.select( .select(
db.ref("id").withSchema(TableName.IdentityProjectMembership), db.ref("id").withSchema(TableName.IdentityProjectMembership),
db.ref("createdAt").withSchema(TableName.IdentityProjectMembership), db.ref("createdAt").withSchema(TableName.IdentityProjectMembership),

View File

@@ -328,7 +328,7 @@ export const projectMembershipServiceFactory = ({
); );
const hasCustomRole = Boolean(customInputRoles.length); const hasCustomRole = Boolean(customInputRoles.length);
if (hasCustomRole) { if (hasCustomRole) {
const plan = await licenseService.getPlan(actorOrgId as string); const plan = await licenseService.getPlan(actorOrgId);
if (!plan?.rbac) if (!plan?.rbac)
throw new BadRequestError({ throw new BadRequestError({
message: "Failed to assign custom role due to RBAC restriction. Upgrade plan to assign custom role to member." message: "Failed to assign custom role due to RBAC restriction. Upgrade plan to assign custom role to member."

View File

@@ -168,8 +168,12 @@ export const projectDALFactory = (db: TDbClient) => {
} }
}; };
const findProjectBySlug = async (slug: string, orgId: string) => { const findProjectBySlug = async (slug: string, orgId: string | undefined) => {
try { try {
if (!orgId) {
throw new BadRequestError({ message: "Organization ID is required when querying with slugs" });
}
const projects = await db(TableName.ProjectMembership) const projects = await db(TableName.ProjectMembership)
.where(`${TableName.Project}.slug`, slug) .where(`${TableName.Project}.slug`, slug)
.where(`${TableName.Project}.orgId`, orgId) .where(`${TableName.Project}.orgId`, orgId)

View File

@@ -1,11 +1,12 @@
import { ForbiddenError } from "@casl/ability"; import { ForbiddenError } from "@casl/ability";
import slugify from "@sindresorhus/slugify"; import slugify from "@sindresorhus/slugify";
import { ProjectMembershipRole, ProjectVersion } from "@app/db/schemas"; import { OrgMembershipRole, ProjectMembershipRole, ProjectVersion } from "@app/db/schemas";
import { TLicenseServiceFactory } from "@app/ee/services/license/license-service"; import { TLicenseServiceFactory } from "@app/ee/services/license/license-service";
import { OrgPermissionActions, OrgPermissionSubjects } from "@app/ee/services/permission/org-permission"; import { OrgPermissionActions, OrgPermissionSubjects } from "@app/ee/services/permission/org-permission";
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service"; import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service";
import { ProjectPermissionActions, ProjectPermissionSub } from "@app/ee/services/permission/project-permission"; import { ProjectPermissionActions, ProjectPermissionSub } from "@app/ee/services/permission/project-permission";
import { TKeyStoreFactory } from "@app/keystore/keystore";
import { isAtLeastAsPrivileged } from "@app/lib/casl"; import { isAtLeastAsPrivileged } from "@app/lib/casl";
import { getConfig } from "@app/lib/config/env"; import { getConfig } from "@app/lib/config/env";
import { createSecretBlindIndex } from "@app/lib/crypto"; import { createSecretBlindIndex } from "@app/lib/crypto";
@@ -65,6 +66,7 @@ type TProjectServiceFactoryDep = {
orgService: Pick<TOrgServiceFactory, "addGhostUser">; orgService: Pick<TOrgServiceFactory, "addGhostUser">;
licenseService: Pick<TLicenseServiceFactory, "getPlan">; licenseService: Pick<TLicenseServiceFactory, "getPlan">;
orgDAL: Pick<TOrgDALFactory, "findOne">; orgDAL: Pick<TOrgDALFactory, "findOne">;
keyStore: Pick<TKeyStoreFactory, "deleteItem">;
}; };
export type TProjectServiceFactory = ReturnType<typeof projectServiceFactory>; export type TProjectServiceFactory = ReturnType<typeof projectServiceFactory>;
@@ -86,13 +88,13 @@ export const projectServiceFactory = ({
projectEnvDAL, projectEnvDAL,
licenseService, licenseService,
projectUserMembershipRoleDAL, projectUserMembershipRoleDAL,
identityProjectMembershipRoleDAL identityProjectMembershipRoleDAL,
keyStore
}: TProjectServiceFactoryDep) => { }: TProjectServiceFactoryDep) => {
/* /*
* Create workspace. Make user the admin * Create workspace. Make user the admin
* */ * */
const createProject = async ({ const createProject = async ({
orgSlug,
actor, actor,
actorId, actorId,
actorOrgId, actorOrgId,
@@ -100,13 +102,7 @@ export const projectServiceFactory = ({
workspaceName, workspaceName,
slug: projectSlug slug: projectSlug
}: TCreateProjectDTO) => { }: TCreateProjectDTO) => {
if (!orgSlug) { const organization = await orgDAL.findOne({ id: actorOrgId });
throw new BadRequestError({
message: "Must provide organization slug to create project"
});
}
const organization = await orgDAL.findOne({ slug: orgSlug });
const { permission, membership: orgMembership } = await permissionService.getOrgPermission( const { permission, membership: orgMembership } = await permissionService.getOrgPermission(
actor, actor,
@@ -291,10 +287,11 @@ export const projectServiceFactory = ({
// Get the role permission for the identity // Get the role permission for the identity
const { permission: rolePermission, role: customRole } = await permissionService.getOrgPermissionByRole( const { permission: rolePermission, role: customRole } = await permissionService.getOrgPermissionByRole(
ProjectMembershipRole.Admin, OrgMembershipRole.Member,
organization.id organization.id
); );
// Identity has to be at least a member in order to create projects
const hasPrivilege = isAtLeastAsPrivileged(permission, rolePermission); const hasPrivilege = isAtLeastAsPrivileged(permission, rolePermission);
if (!hasPrivilege) if (!hasPrivilege)
throw new ForbiddenRequestError({ throw new ForbiddenRequestError({
@@ -329,6 +326,7 @@ export const projectServiceFactory = ({
}; };
}); });
await keyStore.deleteItem(`infisical-cloud-plan-${actorOrgId}`);
return results; return results;
}; };
@@ -356,6 +354,7 @@ export const projectServiceFactory = ({
return delProject; return delProject;
}); });
await keyStore.deleteItem(`infisical-cloud-plan-${actorOrgId}`);
return deletedProject; return deletedProject;
}; };

View File

@@ -24,7 +24,6 @@ export type TCreateProjectDTO = {
actorAuthMethod: ActorAuthMethod; actorAuthMethod: ActorAuthMethod;
actorId: string; actorId: string;
actorOrgId?: string; actorOrgId?: string;
orgSlug: string;
workspaceName: string; workspaceName: string;
slug?: string; slug?: string;
}; };

View File

@@ -232,6 +232,7 @@ export const secretFolderServiceFactory = ({
if (!parentFolder) return []; if (!parentFolder) return [];
const folders = await folderDAL.find({ envId: env.id, parentId: parentFolder.id }); const folders = await folderDAL.find({ envId: env.id, parentId: parentFolder.id });
return folders; return folders;
}; };

View File

@@ -70,9 +70,31 @@ export const secretImportDALFactory = (db: TDbClient) => {
} }
}; };
const findByFolderIds = async (folderIds: string[], tx?: Knex) => {
try {
const docs = await (tx || db)(TableName.SecretImport)
.whereIn("folderId", folderIds)
.join(TableName.Environment, `${TableName.SecretImport}.importEnv`, `${TableName.Environment}.id`)
.select(
db.ref("*").withSchema(TableName.SecretImport) as unknown as keyof TSecretImports,
db.ref("slug").withSchema(TableName.Environment),
db.ref("name").withSchema(TableName.Environment),
db.ref("id").withSchema(TableName.Environment).as("envId")
)
.orderBy("position", "asc");
return docs.map(({ envId, slug, name, ...el }) => ({
...el,
importEnv: { id: envId, slug, name }
}));
} catch (error) {
throw new DatabaseError({ error, name: "Find secret imports" });
}
};
return { return {
...secretImportOrm, ...secretImportOrm,
find, find,
findByFolderIds,
findLastImportPosition, findLastImportPosition,
updateAllPosition updateAllPosition
}; };

View File

@@ -150,6 +150,71 @@ export const secretDALFactory = (db: TDbClient) => {
} }
}; };
const getSecretTags = async (secretId: string, tx?: Knex) => {
try {
const tags = await (tx || db)(TableName.JnSecretTag)
.join(TableName.SecretTag, `${TableName.JnSecretTag}.${TableName.SecretTag}Id`, `${TableName.SecretTag}.id`)
.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"));
return tags.map((el) => ({
id: el.tagId,
color: el.tagColor,
slug: el.tagSlug,
name: el.tagName
}));
} catch (error) {
throw new DatabaseError({ error, name: "get secret tags" });
}
};
const findByFolderIds = async (folderIds: string[], userId?: string, tx?: Knex) => {
try {
// check if not uui then userId id is null (corner case because service token's ID is not UUI in effort to keep backwards compatibility from mongo)
if (userId && !uuidValidate(userId)) {
// eslint-disable-next-line no-param-reassign
userId = undefined;
}
const secs = await (tx || db)(TableName.Secret)
.whereIn("folderId", folderIds)
.where((bd) => {
void bd.whereNull("userId").orWhere({ userId: userId || null });
})
.leftJoin(TableName.JnSecretTag, `${TableName.Secret}.id`, `${TableName.JnSecretTag}.${TableName.Secret}Id`)
.leftJoin(TableName.SecretTag, `${TableName.JnSecretTag}.${TableName.SecretTag}Id`, `${TableName.SecretTag}.id`)
.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"))
.orderBy("id", "asc");
const data = sqlNestRelationships({
data: secs,
key: "id",
parentMapper: (el) => ({ _id: el.id, ...SecretsSchema.parse(el) }),
childrenMapper: [
{
key: "tagId",
label: "tags" as const,
mapper: ({ tagId: id, tagColor: color, tagSlug: slug, tagName: name }) => ({
id,
color,
slug,
name
})
}
]
});
return data;
} catch (error) {
throw new DatabaseError({ error, name: "get all secret" });
}
};
const findByBlindIndexes = async ( const findByBlindIndexes = async (
folderId: string, folderId: string,
blindIndexes: Array<{ blindIndex: string; type: SecretType }>, blindIndexes: Array<{ blindIndex: string; type: SecretType }>,
@@ -184,7 +249,9 @@ export const secretDALFactory = (db: TDbClient) => {
bulkUpdate, bulkUpdate,
deleteMany, deleteMany,
bulkUpdateNoVersionIncrement, bulkUpdateNoVersionIncrement,
getSecretTags,
findByFolderId, findByFolderId,
findByFolderIds,
findByBlindIndexes findByBlindIndexes
}; };
}; };

View File

@@ -1,4 +1,5 @@
/* eslint-disable no-await-in-loop */ /* eslint-disable no-await-in-loop */
import { subject } from "@casl/ability";
import path from "path"; import path from "path";
import { import {
@@ -7,8 +8,11 @@ import {
SecretType, SecretType,
TableName, TableName,
TSecretBlindIndexes, TSecretBlindIndexes,
TSecretFolders,
TSecrets TSecrets
} from "@app/db/schemas"; } from "@app/db/schemas";
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 { getConfig } from "@app/lib/config/env";
import { import {
buildSecretBlindIndexFromName, buildSecretBlindIndexFromName,
@@ -18,7 +22,9 @@ import {
import { BadRequestError } from "@app/lib/errors"; import { BadRequestError } from "@app/lib/errors";
import { groupBy, unique } from "@app/lib/fn"; import { groupBy, unique } from "@app/lib/fn";
import { ActorAuthMethod, ActorType } from "../auth/auth-type";
import { getBotKeyFnFactory } from "../project-bot/project-bot-fns"; import { getBotKeyFnFactory } from "../project-bot/project-bot-fns";
import { TProjectEnvDALFactory } from "../project-env/project-env-dal";
import { TSecretFolderDALFactory } from "../secret-folder/secret-folder-dal"; import { TSecretFolderDALFactory } from "../secret-folder/secret-folder-dal";
import { TSecretDALFactory } from "./secret-dal"; import { TSecretDALFactory } from "./secret-dal";
import { import {
@@ -45,6 +51,133 @@ export const generateSecretBlindIndexBySalt = async (secretName: string, secretB
return secretBlindIndex; return secretBlindIndex;
}; };
type TRecursivelyFetchSecretsFromFoldersArg = {
permissionService: Pick<TPermissionServiceFactory, "getProjectPermission">;
folderDAL: Pick<TSecretFolderDALFactory, "findBySecretPath" | "find">;
projectEnvDAL: Pick<TProjectEnvDALFactory, "findOne">;
};
type TGetPathsDTO = {
projectId: string;
environment: string;
currentPath: string;
auth: {
actor: ActorType;
actorId: string;
actorAuthMethod: ActorAuthMethod;
actorOrgId: string | undefined;
};
};
// Introduce a new interface for mapping parent IDs to their children
interface FolderMap {
[parentId: string]: TSecretFolders[];
}
const buildHierarchy = (folders: TSecretFolders[]): FolderMap => {
const map: FolderMap = {};
map.null = []; // Initialize mapping for root directory
folders.forEach((folder) => {
const parentId = folder.parentId || "null";
if (!map[parentId]) {
map[parentId] = [];
}
map[parentId].push(folder);
});
return map;
};
const generatePaths = (
map: FolderMap,
parentId: string = "null",
basePath: string = ""
): { path: string; folderId: string }[] => {
const children = map[parentId || "null"] || [];
let paths: { path: string; folderId: string }[] = [];
children.forEach((child) => {
// Determine if this is the root folder of the environment. If no parentId is present and the name is root, it's the root folder
const isRootFolder = child.name === "root" && !child.parentId;
// Form the current path based on the base path and the current child
// eslint-disable-next-line no-nested-ternary
const currPath = basePath === "" ? (isRootFolder ? "/" : `/${child.name}`) : `${basePath}/${child.name}`;
paths.push({
path: currPath,
folderId: child.id
}); // Add the current path
// Recursively generate paths for children, passing down the formatted pathh
const childPaths = generatePaths(map, child.id, currPath);
paths = paths.concat(
childPaths.map((p) => ({
path: p.path,
folderId: p.folderId
}))
);
});
return paths;
};
export const recursivelyGetSecretPaths = ({
folderDAL,
projectEnvDAL,
permissionService
}: TRecursivelyFetchSecretsFromFoldersArg) => {
const getPaths = async ({ projectId, environment, currentPath, auth }: TGetPathsDTO) => {
const env = await projectEnvDAL.findOne({
projectId,
slug: environment
});
if (!env) {
throw new Error(`'${environment}' environment not found in project with ID ${projectId}`);
}
// Fetch all folders in env once with a single query
const folders = await folderDAL.find({
envId: env.id
});
// Build the folder hierarchy map
const folderMap = buildHierarchy(folders);
// Generate the paths paths and normalize the root path to /
const paths = generatePaths(folderMap).map((p) => ({
path: p.path === "/" ? p.path : p.path.substring(1),
folderId: p.folderId
}));
const { permission } = await permissionService.getProjectPermission(
auth.actor,
auth.actorId,
projectId,
auth.actorAuthMethod,
auth.actorOrgId
);
// Filter out paths that the user does not have permission to access, and paths that are not in the current path
const allowedPaths = paths.filter(
(folder) =>
permission.can(
ProjectPermissionActions.Read,
subject(ProjectPermissionSub.Secrets, {
environment,
secretPath: folder.path
})
) && folder.path.startsWith(currentPath === "/" ? "" : currentPath)
);
return allowedPaths;
};
return getPaths;
};
type TInterpolateSecretArg = { type TInterpolateSecretArg = {
projectId: string; projectId: string;
secretEncKey: string; secretEncKey: string;
@@ -202,9 +335,7 @@ export const interpolateSecrets = ({ projectId, secretEncKey, secretDAL, folderD
); );
// eslint-disable-next-line // eslint-disable-next-line
secrets[key].value = secrets[key].skipMultilineEncoding secrets[key].value = secrets[key].skipMultilineEncoding ? expandedVal : formatMultiValueEnv(expandedVal);
? expandedVal
: formatMultiValueEnv(expandedVal);
} }
return secrets; return secrets;
@@ -212,7 +343,10 @@ export const interpolateSecrets = ({ projectId, secretEncKey, secretDAL, folderD
return expandSecrets; return expandSecrets;
}; };
export const decryptSecretRaw = (secret: TSecrets & { workspace: string; environment: string }, key: string) => { export const decryptSecretRaw = (
secret: TSecrets & { workspace: string; environment: string; secretPath?: string },
key: string
) => {
const secretKey = decryptSymmetric128BitHexKeyUTF8({ const secretKey = decryptSymmetric128BitHexKeyUTF8({
ciphertext: secret.secretKeyCiphertext, ciphertext: secret.secretKeyCiphertext,
iv: secret.secretKeyIV, iv: secret.secretKeyIV,
@@ -240,6 +374,7 @@ export const decryptSecretRaw = (secret: TSecrets & { workspace: string; environ
return { return {
secretKey, secretKey,
secretPath: secret.secretPath,
workspace: secret.workspace, workspace: secret.workspace,
environment: secret.environment, environment: secret.environment,
secretValue, secretValue,

View File

@@ -229,7 +229,6 @@ export const secretQueueFactory = ({
const getIntegrationSecrets = async (dto: TGetSecrets & { folderId: string }, key: string) => { const getIntegrationSecrets = async (dto: TGetSecrets & { folderId: string }, key: string) => {
const secrets = await secretDAL.findByFolderId(dto.folderId); const secrets = await secretDAL.findByFolderId(dto.folderId);
if (!secrets.length) return {};
// get imported secrets // get imported secrets
const secretImport = await secretImportDAL.find({ folderId: dto.folderId }); const secretImport = await secretImportDAL.find({ folderId: dto.folderId });
@@ -238,6 +237,9 @@ export const secretQueueFactory = ({
secretDAL, secretDAL,
folderDAL folderDAL
}); });
if (!secrets.length && !importedSecrets.length) return {};
const content: Record<string, { value: string; comment?: string; skipMultilineEncoding?: boolean }> = {}; const content: Record<string, { value: string; comment?: string; skipMultilineEncoding?: boolean }> = {};
importedSecrets.forEach(({ secrets: secs }) => { importedSecrets.forEach(({ secrets: secs }) => {

View File

@@ -1,3 +1,5 @@
/* eslint-disable no-unreachable-loop */
/* eslint-disable no-await-in-loop */
import { ForbiddenError, subject } from "@casl/ability"; import { ForbiddenError, subject } from "@casl/ability";
import { SecretEncryptionAlgo, SecretKeyEncoding, SecretsSchema, SecretType } from "@app/db/schemas"; import { SecretEncryptionAlgo, SecretKeyEncoding, SecretsSchema, SecretType } from "@app/db/schemas";
@@ -13,15 +15,23 @@ import { logger } from "@app/lib/logger";
import { ActorType } from "../auth/auth-type"; import { ActorType } from "../auth/auth-type";
import { TProjectDALFactory } from "../project/project-dal"; import { TProjectDALFactory } from "../project/project-dal";
import { TProjectBotServiceFactory } from "../project-bot/project-bot-service"; import { TProjectBotServiceFactory } from "../project-bot/project-bot-service";
import { TProjectEnvDALFactory } from "../project-env/project-env-dal";
import { TSecretBlindIndexDALFactory } from "../secret-blind-index/secret-blind-index-dal"; import { TSecretBlindIndexDALFactory } from "../secret-blind-index/secret-blind-index-dal";
import { TSecretFolderDALFactory } from "../secret-folder/secret-folder-dal"; import { TSecretFolderDALFactory } from "../secret-folder/secret-folder-dal";
import { TSecretImportDALFactory } from "../secret-import/secret-import-dal"; import { TSecretImportDALFactory } from "../secret-import/secret-import-dal";
import { fnSecretsFromImports } from "../secret-import/secret-import-fns"; import { fnSecretsFromImports } from "../secret-import/secret-import-fns";
import { TSecretTagDALFactory } from "../secret-tag/secret-tag-dal"; import { TSecretTagDALFactory } from "../secret-tag/secret-tag-dal";
import { TSecretDALFactory } from "./secret-dal"; import { TSecretDALFactory } from "./secret-dal";
import { decryptSecretRaw, fnSecretBlindIndexCheck, fnSecretBulkInsert, fnSecretBulkUpdate } from "./secret-fns"; import {
decryptSecretRaw,
fnSecretBlindIndexCheck,
fnSecretBulkInsert,
fnSecretBulkUpdate,
recursivelyGetSecretPaths
} from "./secret-fns";
import { TSecretQueueFactory } from "./secret-queue"; import { TSecretQueueFactory } from "./secret-queue";
import { import {
TAttachSecretTagsDTO,
TCreateBulkSecretDTO, TCreateBulkSecretDTO,
TCreateSecretDTO, TCreateSecretDTO,
TCreateSecretRawDTO, TCreateSecretRawDTO,
@@ -46,20 +56,25 @@ type TSecretServiceFactoryDep = {
secretDAL: TSecretDALFactory; secretDAL: TSecretDALFactory;
secretTagDAL: TSecretTagDALFactory; secretTagDAL: TSecretTagDALFactory;
secretVersionDAL: TSecretVersionDALFactory; secretVersionDAL: TSecretVersionDALFactory;
folderDAL: Pick<TSecretFolderDALFactory, "findBySecretPath" | "updateById" | "findById" | "findByManySecretPath">; projectDAL: Pick<TProjectDALFactory, "checkProjectUpgradeStatus" | "findProjectBySlug">;
projectDAL: Pick<TProjectDALFactory, "checkProjectUpgradeStatus">; projectEnvDAL: Pick<TProjectEnvDALFactory, "findOne">;
folderDAL: Pick<
TSecretFolderDALFactory,
"findBySecretPath" | "updateById" | "findById" | "findByManySecretPath" | "find"
>;
secretBlindIndexDAL: TSecretBlindIndexDALFactory; secretBlindIndexDAL: TSecretBlindIndexDALFactory;
permissionService: Pick<TPermissionServiceFactory, "getProjectPermission">; permissionService: Pick<TPermissionServiceFactory, "getProjectPermission">;
snapshotService: Pick<TSecretSnapshotServiceFactory, "performSnapshot">; snapshotService: Pick<TSecretSnapshotServiceFactory, "performSnapshot">;
secretQueueService: Pick<TSecretQueueFactory, "syncSecrets" | "handleSecretReminder" | "removeSecretReminder">; secretQueueService: Pick<TSecretQueueFactory, "syncSecrets" | "handleSecretReminder" | "removeSecretReminder">;
projectBotService: Pick<TProjectBotServiceFactory, "getBotKey">; projectBotService: Pick<TProjectBotServiceFactory, "getBotKey">;
secretImportDAL: Pick<TSecretImportDALFactory, "find">; secretImportDAL: Pick<TSecretImportDALFactory, "find" | "findByFolderIds">;
secretVersionTagDAL: Pick<TSecretVersionTagDALFactory, "insertMany">; secretVersionTagDAL: Pick<TSecretVersionTagDALFactory, "insertMany">;
}; };
export type TSecretServiceFactory = ReturnType<typeof secretServiceFactory>; export type TSecretServiceFactory = ReturnType<typeof secretServiceFactory>;
export const secretServiceFactory = ({ export const secretServiceFactory = ({
secretDAL, secretDAL,
projectEnvDAL,
secretTagDAL, secretTagDAL,
secretVersionDAL, secretVersionDAL,
folderDAL, folderDAL,
@@ -307,6 +322,7 @@ export const secretServiceFactory = ({
if ((inputSecret.tags || []).length !== tags.length) throw new BadRequestError({ message: "Tag not found" }); if ((inputSecret.tags || []).length !== tags.length) throw new BadRequestError({ message: "Tag not found" });
const { secretName, ...el } = inputSecret; const { secretName, ...el } = inputSecret;
const updatedSecret = await secretDAL.transaction(async (tx) => const updatedSecret = await secretDAL.transaction(async (tx) =>
fnSecretBulkUpdate({ fnSecretBulkUpdate({
folderId, folderId,
@@ -423,7 +439,8 @@ export const secretServiceFactory = ({
actor, actor,
actorOrgId, actorOrgId,
actorAuthMethod, actorAuthMethod,
includeImports includeImports,
recursive
}: TGetSecretsDTO) => { }: TGetSecretsDTO) => {
const { permission } = await permissionService.getProjectPermission( const { permission } = await permissionService.getProjectPermission(
actor, actor,
@@ -432,18 +449,52 @@ export const secretServiceFactory = ({
actorAuthMethod, actorAuthMethod,
actorOrgId actorOrgId
); );
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Read, let paths: { folderId: string; path: string }[] = [];
subject(ProjectPermissionSub.Secrets, { environment, secretPath: path })
if (recursive) {
const getPaths = recursivelyGetSecretPaths({
permissionService,
folderDAL,
projectEnvDAL
});
const deepPaths = await getPaths({
projectId,
environment,
currentPath: path,
auth: {
actor,
actorId,
actorAuthMethod,
actorOrgId
}
});
if (!deepPaths) return { secrets: [], imports: [] };
paths = deepPaths.map(({ folderId, path: p }) => ({ folderId, path: p }));
} else {
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Read,
subject(ProjectPermissionSub.Secrets, { environment, secretPath: path })
);
const folder = await folderDAL.findBySecretPath(projectId, environment, path);
if (!folder) return { secrets: [], imports: [] };
paths = [{ folderId: folder.id, path }];
}
const groupedPaths = groupBy(paths, (p) => p.folderId);
const secrets = await secretDAL.findByFolderIds(
paths.map((p) => p.folderId),
actorId
); );
const folder = await folderDAL.findBySecretPath(projectId, environment, path);
if (!folder) return { secrets: [], imports: [] };
const folderId = folder.id;
const secrets = await secretDAL.findByFolderId(folderId, actorId);
if (includeImports) { if (includeImports) {
const secretImports = await secretImportDAL.find({ folderId }); const secretImports = await secretImportDAL.findByFolderIds(paths.map((p) => p.folderId));
const allowedImports = secretImports.filter(({ importEnv, importPath }) => const allowedImports = secretImports.filter(({ importEnv, importPath }) =>
// if its service token allow full access over imported one // if its service token allow full access over imported one
actor === ActorType.SERVICE actor === ActorType.SERVICE
@@ -461,12 +512,26 @@ export const secretServiceFactory = ({
secretDAL, secretDAL,
folderDAL folderDAL
}); });
return { return {
secrets: secrets.map((el) => ({ ...el, workspace: projectId, environment })), secrets: secrets.map((secret) => ({
...secret,
workspace: projectId,
environment,
secretPath: groupedPaths[secret.folderId][0].path
})),
imports: importedSecrets imports: importedSecrets
}; };
} }
return { secrets: secrets.map((el) => ({ ...el, workspace: projectId, environment })) };
return {
secrets: secrets.map((secret) => ({
...secret,
workspace: projectId,
environment,
secretPath: groupedPaths[secret.folderId][0].path
}))
};
}; };
const getSecretByName = async ({ const getSecretByName = async ({
@@ -652,7 +717,7 @@ export const secretServiceFactory = ({
actorOrgId actorOrgId
); );
ForbiddenError.from(permission).throwUnlessCan( ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Create, ProjectPermissionActions.Edit,
subject(ProjectPermissionSub.Secrets, { environment, secretPath: path }) subject(ProjectPermissionSub.Secrets, { environment, secretPath: path })
); );
@@ -738,7 +803,7 @@ export const secretServiceFactory = ({
actorOrgId actorOrgId
); );
ForbiddenError.from(permission).throwUnlessCan( ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Create, ProjectPermissionActions.Delete,
subject(ProjectPermissionSub.Secrets, { environment, secretPath: path }) subject(ProjectPermissionSub.Secrets, { environment, secretPath: path })
); );
@@ -786,7 +851,8 @@ export const secretServiceFactory = ({
actorOrgId, actorOrgId,
actorAuthMethod, actorAuthMethod,
environment, environment,
includeImports includeImports,
recursive
}: TGetSecretsRawDTO) => { }: TGetSecretsRawDTO) => {
const botKey = await projectBotService.getBotKey(projectId); const botKey = await projectBotService.getBotKey(projectId);
if (!botKey) throw new BadRequestError({ message: "Project bot not found", name: "bot_not_found_error" }); if (!botKey) throw new BadRequestError({ message: "Project bot not found", name: "bot_not_found_error" });
@@ -799,7 +865,8 @@ export const secretServiceFactory = ({
actorOrgId, actorOrgId,
actorAuthMethod, actorAuthMethod,
path, path,
includeImports includeImports,
recursive
}); });
return { return {
@@ -807,7 +874,10 @@ export const secretServiceFactory = ({
imports: (imports || [])?.map(({ secrets: importedSecrets, ...el }) => ({ imports: (imports || [])?.map(({ secrets: importedSecrets, ...el }) => ({
...el, ...el,
secrets: importedSecrets.map((sec) => secrets: importedSecrets.map((sec) =>
decryptSecretRaw({ ...sec, environment: el.environment, workspace: projectId }, botKey) decryptSecretRaw(
{ ...sec, environment: el.environment, workspace: projectId, secretPath: el.secretPath },
botKey
)
) )
})) }))
}; };
@@ -994,7 +1064,209 @@ export const secretServiceFactory = ({
return secretVersions; return secretVersions;
}; };
const attachTags = async ({
secretName,
tagSlugs,
path: secretPath,
environment,
type,
projectSlug,
actor,
actorAuthMethod,
actorOrgId,
actorId
}: TAttachSecretTagsDTO) => {
const project = await projectDAL.findProjectBySlug(projectSlug, actorOrgId);
const { permission } = await permissionService.getProjectPermission(
actor,
actorId,
project.id,
actorAuthMethod,
actorOrgId
);
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Edit,
subject(ProjectPermissionSub.Secrets, { environment, secretPath })
);
await projectDAL.checkProjectUpgradeStatus(project.id);
const secret = await getSecretByName({
actorId,
actor,
actorOrgId,
actorAuthMethod,
projectId: project.id,
environment,
path: secretPath,
secretName,
type
});
if (!secret) {
throw new BadRequestError({ message: "Secret not found" });
}
const folder = await folderDAL.findBySecretPath(project.id, environment, secretPath);
if (!folder) {
throw new BadRequestError({ message: "Folder not found" });
}
const tags = await secretTagDAL.find({
projectId: project.id,
$in: {
slug: tagSlugs
}
});
if (tags.length !== tagSlugs.length) {
throw new BadRequestError({ message: "One or more tags not found." });
}
const existingSecretTags = await secretDAL.getSecretTags(secret.id);
if (existingSecretTags.some((tag) => tagSlugs.includes(tag.slug))) {
throw new BadRequestError({ message: "One or more tags already exist on the secret" });
}
const combinedTags = new Set([...existingSecretTags.map((tag) => tag.id), ...tags.map((el) => el.id)]);
const updatedSecret = await secretDAL.transaction(async (tx) =>
fnSecretBulkUpdate({
folderId: folder.id,
projectId: project.id,
inputSecrets: [
{
filter: { id: secret.id },
data: {
tags: Array.from(combinedTags)
}
}
],
secretDAL,
secretVersionDAL,
secretTagDAL,
secretVersionTagDAL,
tx
})
);
await snapshotService.performSnapshot(folder.id);
await secretQueueService.syncSecrets({ secretPath, projectId: project.id, environment });
return {
...updatedSecret[0],
tags: [...existingSecretTags, ...tags].map((t) => ({ id: t.id, slug: t.slug, name: t.name, color: t.color }))
};
};
const detachTags = async ({
secretName,
tagSlugs,
path: secretPath,
environment,
type,
projectSlug,
actor,
actorAuthMethod,
actorOrgId,
actorId
}: TAttachSecretTagsDTO) => {
const project = await projectDAL.findProjectBySlug(projectSlug, actorOrgId);
const { permission } = await permissionService.getProjectPermission(
actor,
actorId,
project.id,
actorAuthMethod,
actorOrgId
);
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Edit,
subject(ProjectPermissionSub.Secrets, { environment, secretPath })
);
await projectDAL.checkProjectUpgradeStatus(project.id);
const secret = await getSecretByName({
actorId,
actor,
actorOrgId,
actorAuthMethod,
projectId: project.id,
environment,
path: secretPath,
secretName,
type
});
if (!secret) {
throw new BadRequestError({ message: "Secret not found" });
}
const folder = await folderDAL.findBySecretPath(project.id, environment, secretPath);
if (!folder) {
throw new BadRequestError({ message: "Folder not found" });
}
const tags = await secretTagDAL.find({
projectId: project.id,
$in: {
slug: tagSlugs
}
});
if (tags.length !== tagSlugs.length) {
throw new BadRequestError({ message: "One or more tags not found." });
}
const existingSecretTags = await secretDAL.getSecretTags(secret.id);
// Make sure all the tags exist on the secret
const tagIdsToRemove = tags.map((tag) => tag.id);
const secretTagIds = existingSecretTags.map((tag) => tag.id);
if (!tagIdsToRemove.every((el) => secretTagIds.includes(el))) {
throw new BadRequestError({ message: "One or more tags not found on the secret" });
}
const newTags = existingSecretTags.filter((tag) => !tagIdsToRemove.includes(tag.id));
const updatedSecret = await secretDAL.transaction(async (tx) =>
fnSecretBulkUpdate({
folderId: folder.id,
projectId: project.id,
inputSecrets: [
{
filter: { id: secret.id },
data: {
tags: newTags.map((tag) => tag.id)
}
}
],
secretDAL,
secretVersionDAL,
secretTagDAL,
secretVersionTagDAL,
tx
})
);
await snapshotService.performSnapshot(folder.id);
await secretQueueService.syncSecrets({ secretPath, projectId: project.id, environment });
return {
...updatedSecret[0],
tags: newTags
};
};
return { return {
attachTags,
detachTags,
createSecret, createSecret,
deleteSecret, deleteSecret,
updateSecret, updateSecret,

View File

@@ -74,6 +74,7 @@ export type TGetSecretsDTO = {
path: string; path: string;
environment: string; environment: string;
includeImports?: boolean; includeImports?: boolean;
recursive?: boolean;
} & TProjectPermission; } & TProjectPermission;
export type TGetASecretDTO = { export type TGetASecretDTO = {
@@ -140,6 +141,7 @@ export type TGetSecretsRawDTO = {
path: string; path: string;
environment: string; environment: string;
includeImports?: boolean; includeImports?: boolean;
recursive?: boolean;
} & TProjectPermission; } & TProjectPermission;
export type TGetASecretRawDTO = { export type TGetASecretRawDTO = {
@@ -206,6 +208,15 @@ export type TFnSecretBulkUpdate = {
tx?: Knex; tx?: Knex;
}; };
export type TAttachSecretTagsDTO = {
projectSlug: string;
secretName: string;
tagSlugs: string[];
environment: string;
path: string;
type: SecretType;
} & Omit<TProjectPermission, "projectId">;
export type TFnSecretBulkDelete = { export type TFnSecretBulkDelete = {
folderId: string; folderId: string;
projectId: string; projectId: string;

1
cli/.gitignore vendored
View File

@@ -1,2 +1,3 @@
.infisical.json .infisical.json
dist/ dist/
agent-config.test.yaml

View File

@@ -277,6 +277,10 @@ func CallGetSecretsV3(httpClient *resty.Client, request GetEncryptedSecretsV3Req
SetQueryParam("environment", request.Environment). SetQueryParam("environment", request.Environment).
SetQueryParam("workspaceId", request.WorkspaceId) SetQueryParam("workspaceId", request.WorkspaceId)
if request.Recursive {
httpRequest.SetQueryParam("recursive", "true")
}
if request.IncludeImport { if request.IncludeImport {
httpRequest.SetQueryParam("include_imports", "true") httpRequest.SetQueryParam("include_imports", "true")
} }
@@ -406,14 +410,14 @@ func CallDeleteSecretsV3(httpClient *resty.Client, request DeleteSecretV3Request
return nil return nil
} }
func CallUpdateSecretsV3(httpClient *resty.Client, request UpdateSecretByNameV3Request) error { func CallUpdateSecretsV3(httpClient *resty.Client, request UpdateSecretByNameV3Request, secretName string) error {
var secretsResponse GetEncryptedSecretsV3Response var secretsResponse GetEncryptedSecretsV3Response
response, err := httpClient. response, err := httpClient.
R(). R().
SetResult(&secretsResponse). SetResult(&secretsResponse).
SetHeader("User-Agent", USER_AGENT). SetHeader("User-Agent", USER_AGENT).
SetBody(request). SetBody(request).
Patch(fmt.Sprintf("%v/v3/secrets/%s", config.INFISICAL_URL, request.SecretName)) Patch(fmt.Sprintf("%v/v3/secrets/%s", config.INFISICAL_URL, secretName))
if err != nil { if err != nil {
return fmt.Errorf("CallUpdateSecretsV3: Unable to complete api request [err=%s]", err) return fmt.Errorf("CallUpdateSecretsV3: Unable to complete api request [err=%s]", err)
@@ -535,3 +539,23 @@ func CallGetRawSecretsV3(httpClient *resty.Client, request GetRawSecretsV3Reques
return getRawSecretsV3Response, nil return getRawSecretsV3Response, nil
} }
func CallCreateDynamicSecretLeaseV1(httpClient *resty.Client, request CreateDynamicSecretLeaseV1Request) (CreateDynamicSecretLeaseV1Response, error) {
var createDynamicSecretLeaseResponse CreateDynamicSecretLeaseV1Response
response, err := httpClient.
R().
SetResult(&createDynamicSecretLeaseResponse).
SetHeader("User-Agent", USER_AGENT).
SetBody(request).
Post(fmt.Sprintf("%v/v1/dynamic-secrets/leases", config.INFISICAL_URL))
if err != nil {
return CreateDynamicSecretLeaseV1Response{}, fmt.Errorf("CreateDynamicSecretLeaseV1: Unable to complete api request [err=%w]", err)
}
if response.IsError() {
return CreateDynamicSecretLeaseV1Response{}, fmt.Errorf("CreateDynamicSecretLeaseV1: Unsuccessful response [%v %v] [status-code=%v] [response=%v]", response.Request.Method, response.Request.URL, response.StatusCode(), response.String())
}
return createDynamicSecretLeaseResponse, nil
}

View File

@@ -291,6 +291,7 @@ type GetEncryptedSecretsV3Request struct {
WorkspaceId string `json:"workspaceId"` WorkspaceId string `json:"workspaceId"`
SecretPath string `json:"secretPath"` SecretPath string `json:"secretPath"`
IncludeImport bool `json:"include_imports"` IncludeImport bool `json:"include_imports"`
Recursive bool `json:"recursive"`
} }
type GetFoldersV1Request struct { type GetFoldersV1Request struct {
@@ -401,7 +402,6 @@ type DeleteSecretV3Request struct {
} }
type UpdateSecretByNameV3Request struct { type UpdateSecretByNameV3Request struct {
SecretName string `json:"secretName"`
WorkspaceID string `json:"workspaceId"` WorkspaceID string `json:"workspaceId"`
Environment string `json:"environment"` Environment string `json:"environment"`
Type string `json:"type"` Type string `json:"type"`
@@ -501,6 +501,28 @@ type UniversalAuthRefreshResponse struct {
AccessTokenMaxTTL int `json:"accessTokenMaxTTL"` AccessTokenMaxTTL int `json:"accessTokenMaxTTL"`
} }
type CreateDynamicSecretLeaseV1Request struct {
Environment string `json:"environment"`
ProjectSlug string `json:"projectSlug"`
SecretPath string `json:"secretPath,omitempty"`
Slug string `json:"slug"`
TTL string `json:"ttl,omitempty"`
}
type CreateDynamicSecretLeaseV1Response struct {
Lease struct {
Id string `json:"id"`
ExpireAt time.Time `json:"expireAt"`
} `json:"lease"`
DynamicSecret struct {
Id string `json:"id"`
DefaultTTL string `json:"defaultTTL"`
MaxTTL string `json:"maxTTL"`
Type string `json:"type"`
} `json:"dynamicSecret"`
Data map[string]interface{} `json:"data"`
}
type GetRawSecretsV3Request struct { type GetRawSecretsV3Request struct {
Environment string `json:"environment"` Environment string `json:"environment"`
WorkspaceId string `json:"workspaceId"` WorkspaceId string `json:"workspaceId"`

View File

@@ -14,6 +14,7 @@ import (
"os/signal" "os/signal"
"path" "path"
"runtime" "runtime"
"slices"
"strings" "strings"
"sync" "sync"
"syscall" "syscall"
@@ -33,6 +34,9 @@ import (
const DEFAULT_INFISICAL_CLOUD_URL = "https://app.infisical.com" const DEFAULT_INFISICAL_CLOUD_URL = "https://app.infisical.com"
// duration to reduce from expiry of dynamic leases so that it gets triggered before expiry
const DYNAMIC_SECRET_PRUNE_EXPIRE_BUFFER = -15
type Config struct { type Config struct {
Infisical InfisicalConfig `yaml:"infisical"` Infisical InfisicalConfig `yaml:"infisical"`
Auth AuthConfig `yaml:"auth"` Auth AuthConfig `yaml:"auth"`
@@ -84,6 +88,115 @@ type Template struct {
} `yaml:"config"` } `yaml:"config"`
} }
func newAgentTemplateChannels(templates []Template) map[string]chan bool {
// we keep each destination as an identifier for various channel
templateChannel := make(map[string]chan bool)
for _, template := range templates {
templateChannel[template.DestinationPath] = make(chan bool)
}
return templateChannel
}
type DynamicSecretLease struct {
LeaseID string
ExpireAt time.Time
Environment string
SecretPath string
Slug string
ProjectSlug string
Data map[string]interface{}
TemplateIDs []int
}
type DynamicSecretLeaseManager struct {
leases []DynamicSecretLease
mutex sync.Mutex
}
func (d *DynamicSecretLeaseManager) Prune() {
d.mutex.Lock()
defer d.mutex.Unlock()
d.leases = slices.DeleteFunc(d.leases, func(s DynamicSecretLease) bool {
return time.Now().After(s.ExpireAt.Add(DYNAMIC_SECRET_PRUNE_EXPIRE_BUFFER * time.Second))
})
}
func (d *DynamicSecretLeaseManager) Append(lease DynamicSecretLease) {
d.mutex.Lock()
defer d.mutex.Unlock()
index := slices.IndexFunc(d.leases, func(s DynamicSecretLease) bool {
if lease.SecretPath == s.SecretPath && lease.Environment == s.Environment && lease.ProjectSlug == s.ProjectSlug && lease.Slug == s.Slug {
return true
}
return false
})
if index != -1 {
d.leases[index].TemplateIDs = append(d.leases[index].TemplateIDs, lease.TemplateIDs...)
return
}
d.leases = append(d.leases, lease)
}
func (d *DynamicSecretLeaseManager) RegisterTemplate(projectSlug, environment, secretPath, slug string, templateId int) {
d.mutex.Lock()
defer d.mutex.Unlock()
index := slices.IndexFunc(d.leases, func(lease DynamicSecretLease) bool {
if lease.SecretPath == secretPath && lease.Environment == environment && lease.ProjectSlug == projectSlug && lease.Slug == slug {
return true
}
return false
})
if index != -1 {
d.leases[index].TemplateIDs = append(d.leases[index].TemplateIDs, templateId)
}
}
func (d *DynamicSecretLeaseManager) GetLease(projectSlug, environment, secretPath, slug string) *DynamicSecretLease {
d.mutex.Lock()
defer d.mutex.Unlock()
for _, lease := range d.leases {
if lease.SecretPath == secretPath && lease.Environment == environment && lease.ProjectSlug == projectSlug && lease.Slug == slug {
return &lease
}
}
return nil
}
// for a given template find the first expiring lease
// The bool indicates whether it contains valid expiry list
func (d *DynamicSecretLeaseManager) GetFirstExpiringLeaseTime(templateId int) (time.Time, bool) {
d.mutex.Lock()
defer d.mutex.Unlock()
if len(d.leases) == 0 {
return time.Time{}, false
}
var firstExpiry time.Time
for i, el := range d.leases {
if i == 0 {
firstExpiry = el.ExpireAt
}
newLeaseTime := el.ExpireAt.Add(DYNAMIC_SECRET_PRUNE_EXPIRE_BUFFER * time.Second)
if newLeaseTime.Before(firstExpiry) {
firstExpiry = newLeaseTime
}
}
return firstExpiry, true
}
func NewDynamicSecretLeaseManager(sigChan chan os.Signal) *DynamicSecretLeaseManager {
manager := &DynamicSecretLeaseManager{}
return manager
}
func ReadFile(filePath string) ([]byte, error) { func ReadFile(filePath string) ([]byte, error) {
return ioutil.ReadFile(filePath) return ioutil.ReadFile(filePath)
} }
@@ -219,7 +332,7 @@ func ParseAgentConfig(configFile []byte) (*Config, error) {
func secretTemplateFunction(accessToken string, existingEtag string, currentEtag *string) func(string, string, string) ([]models.SingleEnvironmentVariable, 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) { return func(projectID, envSlug, secretPath string) ([]models.SingleEnvironmentVariable, error) {
res, err := util.GetPlainTextSecretsViaMachineIdentity(accessToken, projectID, envSlug, secretPath, false) res, err := util.GetPlainTextSecretsViaMachineIdentity(accessToken, projectID, envSlug, secretPath, false, false)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@@ -234,15 +347,43 @@ func secretTemplateFunction(accessToken string, existingEtag string, currentEtag
} }
} }
func ProcessTemplate(templatePath string, data interface{}, accessToken string, existingEtag string, currentEtag *string) (*bytes.Buffer, error) { func dynamicSecretTemplateFunction(accessToken string, dynamicSecretManager *DynamicSecretLeaseManager, templateId int) func(...string) (map[string]interface{}, error) {
return func(args ...string) (map[string]interface{}, error) {
argLength := len(args)
if argLength != 4 && argLength != 5 {
return nil, fmt.Errorf("Invalid arguments found for dynamic-secret function. Check template %i", templateId)
}
projectSlug, envSlug, secretPath, slug, ttl := args[0], args[1], args[2], args[3], ""
if argLength == 5 {
ttl = args[4]
}
dynamicSecretData := dynamicSecretManager.GetLease(projectSlug, envSlug, secretPath, slug)
if dynamicSecretData != nil {
dynamicSecretManager.RegisterTemplate(projectSlug, envSlug, secretPath, slug, templateId)
return dynamicSecretData.Data, nil
}
res, err := util.CreateDynamicSecretLease(accessToken, projectSlug, envSlug, secretPath, slug, ttl)
if err != nil {
return nil, err
}
dynamicSecretManager.Append(DynamicSecretLease{LeaseID: res.Lease.Id, ExpireAt: res.Lease.ExpireAt, Environment: envSlug, SecretPath: secretPath, Slug: slug, ProjectSlug: projectSlug, Data: res.Data, TemplateIDs: []int{templateId}})
return res.Data, nil
}
}
func ProcessTemplate(templateId int, templatePath string, data interface{}, accessToken string, existingEtag string, currentEtag *string, dynamicSecretManager *DynamicSecretLeaseManager) (*bytes.Buffer, error) {
// custom template function to fetch secrets from Infisical // custom template function to fetch secrets from Infisical
secretFunction := secretTemplateFunction(accessToken, existingEtag, currentEtag) secretFunction := secretTemplateFunction(accessToken, existingEtag, currentEtag)
dynamicSecretFunction := dynamicSecretTemplateFunction(accessToken, dynamicSecretManager, templateId)
funcs := template.FuncMap{ funcs := template.FuncMap{
"secret": secretFunction, "secret": secretFunction,
"dynamic_secret": dynamicSecretFunction,
} }
templateName := path.Base(templatePath) templateName := path.Base(templatePath)
tmpl, err := template.New(templateName).Funcs(funcs).ParseFiles(templatePath) tmpl, err := template.New(templateName).Funcs(funcs).ParseFiles(templatePath)
if err != nil { if err != nil {
return nil, err return nil, err
@@ -256,7 +397,7 @@ func ProcessTemplate(templatePath string, data interface{}, accessToken string,
return &buf, nil return &buf, nil
} }
func ProcessBase64Template(encodedTemplate string, data interface{}, accessToken string, existingEtag string, currentEtag *string) (*bytes.Buffer, error) { func ProcessBase64Template(templateId int, encodedTemplate string, data interface{}, accessToken string, existingEtag string, currentEtag *string, dynamicSecretLeaser *DynamicSecretLeaseManager) (*bytes.Buffer, error) {
// custom template function to fetch secrets from Infisical // custom template function to fetch secrets from Infisical
decoded, err := base64.StdEncoding.DecodeString(encodedTemplate) decoded, err := base64.StdEncoding.DecodeString(encodedTemplate)
if err != nil { if err != nil {
@@ -266,8 +407,10 @@ func ProcessBase64Template(encodedTemplate string, data interface{}, accessToken
templateString := string(decoded) templateString := string(decoded)
secretFunction := secretTemplateFunction(accessToken, existingEtag, currentEtag) // TODO: Fix this secretFunction := secretTemplateFunction(accessToken, existingEtag, currentEtag) // TODO: Fix this
dynamicSecretFunction := dynamicSecretTemplateFunction(accessToken, dynamicSecretLeaser, templateId)
funcs := template.FuncMap{ funcs := template.FuncMap{
"secret": secretFunction, "secret": secretFunction,
"dynamic_secret": dynamicSecretFunction,
} }
templateName := "base64Template" templateName := "base64Template"
@@ -285,7 +428,7 @@ func ProcessBase64Template(encodedTemplate string, data interface{}, accessToken
return &buf, nil return &buf, nil
} }
type TokenManager struct { type AgentManager struct {
accessToken string accessToken string
accessTokenTTL time.Duration accessTokenTTL time.Duration
accessTokenMaxTTL time.Duration accessTokenMaxTTL time.Duration
@@ -294,6 +437,7 @@ type TokenManager struct {
mutex sync.Mutex mutex sync.Mutex
filePaths []Sink // Store file paths if needed filePaths []Sink // Store file paths if needed
templates []Template templates []Template
dynamicSecretLeases *DynamicSecretLeaseManager
clientIdPath string clientIdPath string
clientSecretPath string clientSecretPath string
newAccessTokenNotificationChan chan bool newAccessTokenNotificationChan chan bool
@@ -302,8 +446,8 @@ type TokenManager struct {
exitAfterAuth bool exitAfterAuth bool
} }
func NewTokenManager(fileDeposits []Sink, templates []Template, clientIdPath string, clientSecretPath string, newAccessTokenNotificationChan chan bool, removeClientSecretOnRead bool, exitAfterAuth bool) *TokenManager { func NewAgentManager(fileDeposits []Sink, templates []Template, clientIdPath string, clientSecretPath string, newAccessTokenNotificationChan chan bool, removeClientSecretOnRead bool, exitAfterAuth bool) *AgentManager {
return &TokenManager{ return &AgentManager{
filePaths: fileDeposits, filePaths: fileDeposits,
templates: templates, templates: templates,
clientIdPath: clientIdPath, clientIdPath: clientIdPath,
@@ -315,7 +459,7 @@ func NewTokenManager(fileDeposits []Sink, templates []Template, clientIdPath str
} }
func (tm *TokenManager) SetToken(token string, accessTokenTTL time.Duration, accessTokenMaxTTL time.Duration) { func (tm *AgentManager) SetToken(token string, accessTokenTTL time.Duration, accessTokenMaxTTL time.Duration) {
tm.mutex.Lock() tm.mutex.Lock()
defer tm.mutex.Unlock() defer tm.mutex.Unlock()
@@ -326,7 +470,7 @@ func (tm *TokenManager) SetToken(token string, accessTokenTTL time.Duration, acc
tm.newAccessTokenNotificationChan <- true tm.newAccessTokenNotificationChan <- true
} }
func (tm *TokenManager) GetToken() string { func (tm *AgentManager) GetToken() string {
tm.mutex.Lock() tm.mutex.Lock()
defer tm.mutex.Unlock() defer tm.mutex.Unlock()
@@ -334,7 +478,7 @@ func (tm *TokenManager) GetToken() string {
} }
// Fetches a new access token using client credentials // Fetches a new access token using client credentials
func (tm *TokenManager) FetchNewAccessToken() error { func (tm *AgentManager) FetchNewAccessToken() error {
clientID := os.Getenv("INFISICAL_UNIVERSAL_AUTH_CLIENT_ID") clientID := os.Getenv("INFISICAL_UNIVERSAL_AUTH_CLIENT_ID")
if clientID == "" { if clientID == "" {
clientIDAsByte, err := ReadFile(tm.clientIdPath) clientIDAsByte, err := ReadFile(tm.clientIdPath)
@@ -384,7 +528,7 @@ func (tm *TokenManager) FetchNewAccessToken() error {
} }
// Refreshes the existing access token // Refreshes the existing access token
func (tm *TokenManager) RefreshAccessToken() error { func (tm *AgentManager) RefreshAccessToken() error {
httpClient := resty.New() httpClient := resty.New()
httpClient.SetRetryCount(10000). httpClient.SetRetryCount(10000).
SetRetryMaxWaitTime(20 * time.Second). SetRetryMaxWaitTime(20 * time.Second).
@@ -405,7 +549,7 @@ func (tm *TokenManager) RefreshAccessToken() error {
return nil return nil
} }
func (tm *TokenManager) ManageTokenLifecycle() { func (tm *AgentManager) ManageTokenLifecycle() {
for { for {
accessTokenMaxTTLExpiresInTime := tm.accessTokenFetchedTime.Add(tm.accessTokenMaxTTL - (5 * time.Second)) accessTokenMaxTTLExpiresInTime := tm.accessTokenFetchedTime.Add(tm.accessTokenMaxTTL - (5 * time.Second))
accessTokenRefreshedTime := tm.accessTokenRefreshedTime accessTokenRefreshedTime := tm.accessTokenRefreshedTime
@@ -473,7 +617,7 @@ func (tm *TokenManager) ManageTokenLifecycle() {
} }
} }
func (tm *TokenManager) WriteTokenToFiles() { func (tm *AgentManager) WriteTokenToFiles() {
token := tm.GetToken() token := tm.GetToken()
for _, sinkFile := range tm.filePaths { for _, sinkFile := range tm.filePaths {
if sinkFile.Type == "file" { if sinkFile.Type == "file" {
@@ -490,7 +634,7 @@ func (tm *TokenManager) WriteTokenToFiles() {
} }
} }
func (tm *TokenManager) WriteTemplateToFile(bytes *bytes.Buffer, template *Template) { func (tm *AgentManager) WriteTemplateToFile(bytes *bytes.Buffer, template *Template) {
if err := WriteBytesToFile(bytes, template.DestinationPath); err != nil { if err := WriteBytesToFile(bytes, template.DestinationPath); err != nil {
log.Error().Msgf("template engine: unable to write secrets to path because %s. Will try again on next cycle", err) log.Error().Msgf("template engine: unable to write secrets to path because %s. Will try again on next cycle", err)
return return
@@ -498,7 +642,7 @@ func (tm *TokenManager) WriteTemplateToFile(bytes *bytes.Buffer, template *Templ
log.Info().Msgf("template engine: secret template at path %s has been rendered and saved to path %s", template.SourcePath, template.DestinationPath) log.Info().Msgf("template engine: secret template at path %s has been rendered and saved to path %s", template.SourcePath, template.DestinationPath)
} }
func (tm *TokenManager) MonitorSecretChanges(secretTemplate Template, sigChan chan os.Signal) { func (tm *AgentManager) MonitorSecretChanges(secretTemplate Template, templateId int, sigChan chan os.Signal) {
pollingInterval := time.Duration(5 * time.Minute) pollingInterval := time.Duration(5 * time.Minute)
@@ -523,47 +667,61 @@ func (tm *TokenManager) MonitorSecretChanges(secretTemplate Template, sigChan ch
execCommand := secretTemplate.Config.Execute.Command execCommand := secretTemplate.Config.Execute.Command
for { for {
token := tm.GetToken() select {
case <-sigChan:
return
default:
{
tm.dynamicSecretLeases.Prune()
token := tm.GetToken()
if token != "" {
var processedTemplate *bytes.Buffer
var err error
if token != "" { if secretTemplate.SourcePath != "" {
processedTemplate, err = ProcessTemplate(templateId, secretTemplate.SourcePath, nil, token, existingEtag, &currentEtag, tm.dynamicSecretLeases)
} else {
processedTemplate, err = ProcessBase64Template(templateId, secretTemplate.Base64TemplateContent, nil, token, existingEtag, &currentEtag, tm.dynamicSecretLeases)
}
var processedTemplate *bytes.Buffer if err != nil {
var err error log.Error().Msgf("unable to process template because %v", err)
} else {
if (existingEtag != currentEtag) || firstRun {
if secretTemplate.SourcePath != "" { tm.WriteTemplateToFile(processedTemplate, &secretTemplate)
processedTemplate, err = ProcessTemplate(secretTemplate.SourcePath, nil, token, existingEtag, &currentEtag) existingEtag = currentEtag
} else {
processedTemplate, err = ProcessBase64Template(secretTemplate.Base64TemplateContent, nil, token, existingEtag, &currentEtag)
}
if err != nil { if !firstRun && execCommand != "" {
log.Error().Msgf("unable to process template because %v", err) log.Info().Msgf("executing command: %s", execCommand)
} else { err := ExecuteCommandWithTimeout(execCommand, execTimeout)
if (existingEtag != currentEtag) || firstRun {
tm.WriteTemplateToFile(processedTemplate, &secretTemplate) if err != nil {
existingEtag = currentEtag log.Error().Msgf("unable to execute command because %v", err)
}
if !firstRun && execCommand != "" { }
log.Info().Msgf("executing command: %s", execCommand) if firstRun {
err := ExecuteCommandWithTimeout(execCommand, execTimeout) firstRun = false
}
if err != nil {
log.Error().Msgf("unable to execute command because %v", err)
} }
}
// now the idea is we pick the next sleep time in which the one shorter out of
// - polling time
// - first lease that's gonna get expired in the template
firstLeaseExpiry, isValid := tm.dynamicSecretLeases.GetFirstExpiringLeaseTime(templateId)
var waitTime = pollingInterval
if isValid && firstLeaseExpiry.Sub(time.Now()) < pollingInterval {
waitTime = firstLeaseExpiry.Sub(time.Now())
} }
if firstRun { time.Sleep(waitTime)
firstRun = false } else {
} // It fails to get the access token. So we will re-try in 3 seconds. We do this because if we don't, the user will have to wait for the next polling interval to get the first secret render.
time.Sleep(3 * time.Second)
} }
} }
time.Sleep(pollingInterval)
} else {
// It fails to get the access token. So we will re-try in 3 seconds. We do this because if we don't, the user will have to wait for the next polling interval to get the first secret render.
time.Sleep(3 * time.Second)
} }
} }
} }
@@ -645,13 +803,14 @@ var agentCmd = &cobra.Command{
signal.Notify(sigChan, syscall.SIGINT, syscall.SIGTERM) signal.Notify(sigChan, syscall.SIGINT, syscall.SIGTERM)
filePaths := agentConfig.Sinks filePaths := agentConfig.Sinks
tm := NewTokenManager(filePaths, agentConfig.Templates, configUniversalAuthType.ClientIDPath, configUniversalAuthType.ClientSecretPath, tokenRefreshNotifier, configUniversalAuthType.RemoveClientSecretOnRead, agentConfig.Infisical.ExitAfterAuth) tm := NewAgentManager(filePaths, agentConfig.Templates, configUniversalAuthType.ClientIDPath, configUniversalAuthType.ClientSecretPath, tokenRefreshNotifier, configUniversalAuthType.RemoveClientSecretOnRead, agentConfig.Infisical.ExitAfterAuth)
tm.dynamicSecretLeases = NewDynamicSecretLeaseManager(sigChan)
go tm.ManageTokenLifecycle() go tm.ManageTokenLifecycle()
for i, template := range agentConfig.Templates { for i, template := range agentConfig.Templates {
log.Info().Msgf("template engine started for template %v...", i+1) log.Info().Msgf("template engine started for template %v...", i+1)
go tm.MonitorSecretChanges(template, sigChan) go tm.MonitorSecretChanges(template, i, sigChan)
} }
for { for {

View File

@@ -98,7 +98,12 @@ var runCmd = &cobra.Command{
util.HandleError(err, "Unable to parse flag") util.HandleError(err, "Unable to parse flag")
} }
secrets, err := util.GetAllEnvironmentVariables(models.GetAllSecretsParameters{Environment: environmentName, InfisicalToken: infisicalToken, TagSlugs: tagSlugs, SecretsPath: secretsPath, IncludeImport: includeImports}, projectConfigDir) recursive, err := cmd.Flags().GetBool("recursive")
if err != nil {
util.HandleError(err, "Unable to parse flag")
}
secrets, err := util.GetAllEnvironmentVariables(models.GetAllSecretsParameters{Environment: environmentName, InfisicalToken: infisicalToken, TagSlugs: tagSlugs, SecretsPath: secretsPath, IncludeImport: includeImports, Recursive: recursive}, projectConfigDir)
if err != nil { if err != nil {
util.HandleError(err, "Could not fetch secrets", "If you are using a service token to fetch secrets, please ensure it is valid") util.HandleError(err, "Could not fetch secrets", "If you are using a service token to fetch secrets, please ensure it is valid")
@@ -202,6 +207,7 @@ func init() {
runCmd.Flags().StringP("env", "e", "dev", "Set the environment (dev, prod, etc.) from which your secrets should be pulled from") runCmd.Flags().StringP("env", "e", "dev", "Set the environment (dev, prod, etc.) from which your secrets should be pulled from")
runCmd.Flags().Bool("expand", true, "Parse shell parameter expansions in your secrets") runCmd.Flags().Bool("expand", true, "Parse shell parameter expansions in your secrets")
runCmd.Flags().Bool("include-imports", true, "Import linked secrets ") runCmd.Flags().Bool("include-imports", true, "Import linked secrets ")
runCmd.Flags().Bool("recursive", false, "Fetch secrets from all sub-folders")
runCmd.Flags().Bool("secret-overriding", true, "Prioritizes personal secrets, if any, with the same name over shared secrets") runCmd.Flags().Bool("secret-overriding", true, "Prioritizes personal secrets, if any, with the same name over shared secrets")
runCmd.Flags().StringP("command", "c", "", "chained commands to execute (e.g. \"npm install && npm run dev; echo ...\")") runCmd.Flags().StringP("command", "c", "", "chained commands to execute (e.g. \"npm install && npm run dev; echo ...\")")
runCmd.Flags().StringP("tags", "t", "", "filter secrets by tag slugs ") runCmd.Flags().StringP("tags", "t", "", "filter secrets by tag slugs ")

View File

@@ -63,6 +63,11 @@ var secretsCmd = &cobra.Command{
util.HandleError(err) util.HandleError(err)
} }
recursive, err := cmd.Flags().GetBool("recursive")
if err != nil {
util.HandleError(err)
}
tagSlugs, err := cmd.Flags().GetString("tags") tagSlugs, err := cmd.Flags().GetString("tags")
if err != nil { if err != nil {
util.HandleError(err, "Unable to parse flag") util.HandleError(err, "Unable to parse flag")
@@ -73,7 +78,7 @@ var secretsCmd = &cobra.Command{
util.HandleError(err, "Unable to parse flag") util.HandleError(err, "Unable to parse flag")
} }
secrets, err := util.GetAllEnvironmentVariables(models.GetAllSecretsParameters{Environment: environmentName, InfisicalToken: infisicalToken, TagSlugs: tagSlugs, SecretsPath: secretsPath, IncludeImport: includeImports}, "") secrets, err := util.GetAllEnvironmentVariables(models.GetAllSecretsParameters{Environment: environmentName, InfisicalToken: infisicalToken, TagSlugs: tagSlugs, SecretsPath: secretsPath, IncludeImport: includeImports, Recursive: recursive}, "")
if err != nil { if err != nil {
util.HandleError(err) util.HandleError(err)
} }
@@ -297,7 +302,6 @@ var secretsSetCmd = &cobra.Command{
updateSecretRequest := api.UpdateSecretByNameV3Request{ updateSecretRequest := api.UpdateSecretByNameV3Request{
WorkspaceID: workspaceFile.WorkspaceId, WorkspaceID: workspaceFile.WorkspaceId,
Environment: environmentName, Environment: environmentName,
SecretName: secret.PlainTextKey,
SecretValueCiphertext: secret.SecretValueCiphertext, SecretValueCiphertext: secret.SecretValueCiphertext,
SecretValueIV: secret.SecretValueIV, SecretValueIV: secret.SecretValueIV,
SecretValueTag: secret.SecretValueTag, SecretValueTag: secret.SecretValueTag,
@@ -305,7 +309,7 @@ var secretsSetCmd = &cobra.Command{
SecretPath: secretsPath, SecretPath: secretsPath,
} }
err = api.CallUpdateSecretsV3(httpClient, updateSecretRequest) err = api.CallUpdateSecretsV3(httpClient, updateSecretRequest, secret.PlainTextKey)
if err != nil { if err != nil {
util.HandleError(err, "Unable to process secret update request") util.HandleError(err, "Unable to process secret update request")
return return
@@ -414,12 +418,17 @@ func getSecretsByNames(cmd *cobra.Command, args []string) {
util.HandleError(err, "Unable to parse path flag") util.HandleError(err, "Unable to parse path flag")
} }
recursive, err := cmd.Flags().GetBool("recursive")
if err != nil {
util.HandleError(err, "Unable to parse recursive flag")
}
showOnlyValue, err := cmd.Flags().GetBool("raw-value") showOnlyValue, err := cmd.Flags().GetBool("raw-value")
if err != nil { if err != nil {
util.HandleError(err, "Unable to parse path flag") util.HandleError(err, "Unable to parse path flag")
} }
secrets, err := util.GetAllEnvironmentVariables(models.GetAllSecretsParameters{Environment: environmentName, InfisicalToken: infisicalToken, TagSlugs: tagSlugs, SecretsPath: secretsPath}, "") secrets, err := util.GetAllEnvironmentVariables(models.GetAllSecretsParameters{Environment: environmentName, InfisicalToken: infisicalToken, TagSlugs: tagSlugs, SecretsPath: secretsPath, IncludeImport: true, Recursive: recursive}, "")
if err != nil { if err != nil {
util.HandleError(err, "To fetch all secrets") util.HandleError(err, "To fetch all secrets")
} }
@@ -477,7 +486,7 @@ func generateExampleEnv(cmd *cobra.Command, args []string) {
util.HandleError(err, "Unable to parse flag") util.HandleError(err, "Unable to parse flag")
} }
secrets, err := util.GetAllEnvironmentVariables(models.GetAllSecretsParameters{Environment: environmentName, InfisicalToken: infisicalToken, TagSlugs: tagSlugs, SecretsPath: secretsPath}, "") secrets, err := util.GetAllEnvironmentVariables(models.GetAllSecretsParameters{Environment: environmentName, InfisicalToken: infisicalToken, TagSlugs: tagSlugs, SecretsPath: secretsPath, IncludeImport: true}, "")
if err != nil { if err != nil {
util.HandleError(err, "To fetch all secrets") util.HandleError(err, "To fetch all secrets")
} }
@@ -684,6 +693,7 @@ func init() {
secretsCmd.AddCommand(secretsGetCmd) secretsCmd.AddCommand(secretsGetCmd)
secretsGetCmd.Flags().String("path", "/", "get secrets within a folder path") secretsGetCmd.Flags().String("path", "/", "get secrets within a folder path")
secretsGetCmd.Flags().Bool("raw-value", false, "Returns only the value of secret, only works with one secret") secretsGetCmd.Flags().Bool("raw-value", false, "Returns only the value of secret, only works with one secret")
secretsGetCmd.Flags().Bool("recursive", false, "Fetch secrets from all sub-folders")
secretsCmd.Flags().Bool("secret-overriding", true, "Prioritizes personal secrets, if any, with the same name over shared secrets") secretsCmd.Flags().Bool("secret-overriding", true, "Prioritizes personal secrets, if any, with the same name over shared secrets")
secretsCmd.AddCommand(secretsSetCmd) secretsCmd.AddCommand(secretsSetCmd)
@@ -728,6 +738,7 @@ func init() {
secretsCmd.PersistentFlags().String("env", "dev", "Used to select the environment name on which actions should be taken on") secretsCmd.PersistentFlags().String("env", "dev", "Used to select the environment name on which actions should be taken on")
secretsCmd.Flags().Bool("expand", true, "Parse shell parameter expansions in your secrets") secretsCmd.Flags().Bool("expand", true, "Parse shell parameter expansions in your secrets")
secretsCmd.Flags().Bool("include-imports", true, "Imported linked secrets ") secretsCmd.Flags().Bool("include-imports", true, "Imported linked secrets ")
secretsCmd.Flags().Bool("recursive", false, "Fetch secrets from all sub-folders")
secretsCmd.PersistentFlags().StringP("tags", "t", "", "filter secrets by tag slugs") secretsCmd.PersistentFlags().StringP("tags", "t", "", "filter secrets by tag slugs")
secretsCmd.Flags().String("path", "/", "get secrets within a folder path") secretsCmd.Flags().String("path", "/", "get secrets within a folder path")
rootCmd.AddCommand(secretsCmd) rootCmd.AddCommand(secretsCmd)

View File

@@ -1,5 +1,7 @@
package models package models
import "time"
type UserCredentials struct { type UserCredentials struct {
Email string `json:"email"` Email string `json:"email"`
PrivateKey string `json:"privateKey"` PrivateKey string `json:"privateKey"`
@@ -40,6 +42,23 @@ type PlaintextSecretResult struct {
Etag string Etag string
} }
type DynamicSecret struct {
Id string `json:"id"`
DefaultTTL string `json:"defaultTTL"`
MaxTTL string `json:"maxTTL"`
Type string `json:"type"`
}
type DynamicSecretLease struct {
Lease struct {
Id string `json:"id"`
ExpireAt time.Time `json:"expireAt"`
} `json:"lease"`
DynamicSecret DynamicSecret `json:"dynamicSecret"`
// this is a varying dict based on provider
Data map[string]interface{} `json:"data"`
}
type SingleFolder struct { type SingleFolder struct {
ID string `json:"_id"` ID string `json:"_id"`
Name string `json:"name"` Name string `json:"name"`
@@ -74,6 +93,7 @@ type GetAllSecretsParameters struct {
WorkspaceId string WorkspaceId string
SecretsPath string SecretsPath string
IncludeImport bool IncludeImport bool
Recursive bool
} }
type GetAllFoldersParameters struct { type GetAllFoldersParameters struct {

View File

@@ -17,7 +17,7 @@ import (
"github.com/rs/zerolog/log" "github.com/rs/zerolog/log"
) )
func GetPlainTextSecretsViaServiceToken(fullServiceToken string, environment string, secretPath string, includeImports bool) ([]models.SingleEnvironmentVariable, api.GetServiceTokenDetailsResponse, error) { func GetPlainTextSecretsViaServiceToken(fullServiceToken string, environment string, secretPath string, includeImports bool, recursive bool) ([]models.SingleEnvironmentVariable, api.GetServiceTokenDetailsResponse, error) {
serviceTokenParts := strings.SplitN(fullServiceToken, ".", 4) serviceTokenParts := strings.SplitN(fullServiceToken, ".", 4)
if len(serviceTokenParts) < 4 { if len(serviceTokenParts) < 4 {
return nil, api.GetServiceTokenDetailsResponse{}, fmt.Errorf("invalid service token entered. Please double check your service token and try again") return nil, api.GetServiceTokenDetailsResponse{}, fmt.Errorf("invalid service token entered. Please double check your service token and try again")
@@ -49,6 +49,7 @@ func GetPlainTextSecretsViaServiceToken(fullServiceToken string, environment str
Environment: environment, Environment: environment,
SecretPath: secretPath, SecretPath: secretPath,
IncludeImport: includeImports, IncludeImport: includeImports,
Recursive: recursive,
}) })
if err != nil { if err != nil {
@@ -80,7 +81,7 @@ func GetPlainTextSecretsViaServiceToken(fullServiceToken string, environment str
return plainTextSecrets, serviceTokenDetails, nil return plainTextSecrets, serviceTokenDetails, nil
} }
func GetPlainTextSecretsViaJTW(JTWToken string, receiversPrivateKey string, workspaceId string, environmentName string, tagSlugs string, secretsPath string, includeImports bool) ([]models.SingleEnvironmentVariable, error) { func GetPlainTextSecretsViaJTW(JTWToken string, receiversPrivateKey string, workspaceId string, environmentName string, tagSlugs string, secretsPath string, includeImports bool, recursive bool) ([]models.SingleEnvironmentVariable, error) {
httpClient := resty.New() httpClient := resty.New()
httpClient.SetAuthToken(JTWToken). httpClient.SetAuthToken(JTWToken).
SetHeader("Accept", "application/json") SetHeader("Accept", "application/json")
@@ -125,6 +126,7 @@ func GetPlainTextSecretsViaJTW(JTWToken string, receiversPrivateKey string, work
WorkspaceId: workspaceId, WorkspaceId: workspaceId,
Environment: environmentName, Environment: environmentName,
IncludeImport: includeImports, IncludeImport: includeImports,
Recursive: recursive,
// TagSlugs: tagSlugs, // TagSlugs: tagSlugs,
} }
@@ -152,7 +154,7 @@ func GetPlainTextSecretsViaJTW(JTWToken string, receiversPrivateKey string, work
return plainTextSecrets, nil return plainTextSecrets, nil
} }
func GetPlainTextSecretsViaMachineIdentity(accessToken string, workspaceId string, environmentName string, secretsPath string, includeImports bool) (models.PlaintextSecretResult, error) { func GetPlainTextSecretsViaMachineIdentity(accessToken string, workspaceId string, environmentName string, secretsPath string, includeImports bool, recursive bool) (models.PlaintextSecretResult, error) {
httpClient := resty.New() httpClient := resty.New()
httpClient.SetAuthToken(accessToken). httpClient.SetAuthToken(accessToken).
SetHeader("Accept", "application/json") SetHeader("Accept", "application/json")
@@ -161,6 +163,7 @@ func GetPlainTextSecretsViaMachineIdentity(accessToken string, workspaceId strin
WorkspaceId: workspaceId, WorkspaceId: workspaceId,
Environment: environmentName, Environment: environmentName,
IncludeImport: includeImports, IncludeImport: includeImports,
Recursive: recursive,
// TagSlugs: tagSlugs, // TagSlugs: tagSlugs,
} }
@@ -195,6 +198,31 @@ func GetPlainTextSecretsViaMachineIdentity(accessToken string, workspaceId strin
}, nil }, nil
} }
func CreateDynamicSecretLease(accessToken string, projectSlug string, environmentName string, secretsPath string, slug string, ttl string) (models.DynamicSecretLease, error) {
httpClient := resty.New()
httpClient.SetAuthToken(accessToken).
SetHeader("Accept", "application/json")
dynamicSecretRequest := api.CreateDynamicSecretLeaseV1Request{
ProjectSlug: projectSlug,
Environment: environmentName,
SecretPath: secretsPath,
Slug: slug,
TTL: ttl,
}
dynamicSecret, err := api.CallCreateDynamicSecretLeaseV1(httpClient, dynamicSecretRequest)
if err != nil {
return models.DynamicSecretLease{}, err
}
return models.DynamicSecretLease{
Lease: dynamicSecret.Lease,
Data: dynamicSecret.Data,
DynamicSecret: dynamicSecret.DynamicSecret,
}, nil
}
func InjectImportedSecret(plainTextWorkspaceKey []byte, secrets []models.SingleEnvironmentVariable, importedSecrets []api.ImportedSecretV3) ([]models.SingleEnvironmentVariable, error) { func InjectImportedSecret(plainTextWorkspaceKey []byte, secrets []models.SingleEnvironmentVariable, importedSecrets []api.ImportedSecretV3) ([]models.SingleEnvironmentVariable, error) {
if importedSecrets == nil { if importedSecrets == nil {
return secrets, nil return secrets, nil
@@ -304,7 +332,7 @@ func GetAllEnvironmentVariables(params models.GetAllSecretsParameters, projectCo
} }
secretsToReturn, errorToReturn = GetPlainTextSecretsViaJTW(loggedInUserDetails.UserCredentials.JTWToken, loggedInUserDetails.UserCredentials.PrivateKey, infisicalDotJson.WorkspaceId, secretsToReturn, errorToReturn = GetPlainTextSecretsViaJTW(loggedInUserDetails.UserCredentials.JTWToken, loggedInUserDetails.UserCredentials.PrivateKey, infisicalDotJson.WorkspaceId,
params.Environment, params.TagSlugs, params.SecretsPath, params.IncludeImport) params.Environment, params.TagSlugs, params.SecretsPath, params.IncludeImport, params.Recursive)
log.Debug().Msgf("GetAllEnvironmentVariables: Trying to fetch secrets JTW token [err=%s]", errorToReturn) log.Debug().Msgf("GetAllEnvironmentVariables: Trying to fetch secrets JTW token [err=%s]", errorToReturn)
backupSecretsEncryptionKey := []byte(loggedInUserDetails.UserCredentials.PrivateKey)[0:32] backupSecretsEncryptionKey := []byte(loggedInUserDetails.UserCredentials.PrivateKey)[0:32]
@@ -325,10 +353,10 @@ func GetAllEnvironmentVariables(params models.GetAllSecretsParameters, projectCo
} else { } else {
if params.InfisicalToken != "" { if params.InfisicalToken != "" {
log.Debug().Msg("Trying to fetch secrets using service token") log.Debug().Msg("Trying to fetch secrets using service token")
secretsToReturn, _, errorToReturn = GetPlainTextSecretsViaServiceToken(params.InfisicalToken, params.Environment, params.SecretsPath, params.IncludeImport) secretsToReturn, _, errorToReturn = GetPlainTextSecretsViaServiceToken(params.InfisicalToken, params.Environment, params.SecretsPath, params.IncludeImport, params.Recursive)
} else if params.UniversalAuthAccessToken != "" { } else if params.UniversalAuthAccessToken != "" {
log.Debug().Msg("Trying to fetch secrets using universal auth") log.Debug().Msg("Trying to fetch secrets using universal auth")
res, err := GetPlainTextSecretsViaMachineIdentity(params.UniversalAuthAccessToken, params.WorkspaceId, params.Environment, params.SecretsPath, params.IncludeImport) res, err := GetPlainTextSecretsViaMachineIdentity(params.UniversalAuthAccessToken, params.WorkspaceId, params.Environment, params.SecretsPath, params.IncludeImport, params.Recursive)
errorToReturn = err errorToReturn = err
secretsToReturn = res.Secrets secretsToReturn = res.Secrets

View File

@@ -0,0 +1,4 @@
---
title: "Create Permanent"
openapi: "POST /api/v1/additional-privilege/identity/permanent"
---

View File

@@ -0,0 +1,4 @@
---
title: "Create Temporary"
openapi: "POST /api/v1/additional-privilege/identity/temporary"
---

View File

@@ -0,0 +1,4 @@
---
title: "Delete"
openapi: "DELETE /api/v1/additional-privilege/identity"
---

View File

@@ -0,0 +1,4 @@
---
title: "Find By Privilege Slug"
openapi: "GET /api/v1/additional-privilege/identity/{privilegeSlug}"
---

View File

@@ -0,0 +1,4 @@
---
title: "List"
openapi: "GET /api/v1/additional-privilege/identity"
---

View File

@@ -0,0 +1,4 @@
---
title: "Update"
openapi: "PATCH /api/v1/additional-privilege/identity"
---

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