1
0
mirror of https://github.com/Infisical/infisical.git synced 2025-03-21 09:44:08 +00:00

Compare commits

..

170 Commits

Author SHA1 Message Date
ad6f285b59 Throw if file and args secrets are used on secret set command 2025-03-18 09:58:10 -03:00
d4842dd273 Improve secret set --file message and make it mutually exclusive with manual secrets args 2025-03-18 08:32:31 -03:00
838c1af448 Add file option to secret set CLI command to retrieve secrets from .env or .yaml files 2025-03-17 09:51:21 -03:00
5ebf142e3e Merge pull request from Infisical/daniel/k8s-config-map
feat(k8s): configmap support
2025-03-14 20:01:52 +04:00
bdceea4c91 requested changes 2025-03-14 06:59:04 +04:00
32fa6866e4 Merge pull request from Infisical/feat/ENG-2320-echo-environment-being-used-in-cli
feat: confirm environment exists when running `run` command
2025-03-14 03:58:05 +04:00
b4faef797c fix: address comment 2025-03-14 03:47:25 +04:00
08732cab62 refactor(projects): move rest api call directly into run command module 2025-03-13 16:36:41 -07:00
81d5f639ae revert: "refactor: clean smelly code"
This reverts commit c04b97c689d86069b008687d22322ae52a8b9a61.
2025-03-13 16:33:26 -07:00
25b83d4b86 docs: fix formatting 2025-03-14 02:45:59 +04:00
a500f00a49 fix(run): compare environment slug to environment slug 2025-03-13 13:21:12 -07:00
6842f7aa8b docs(k8s): config map support 2025-03-13 23:44:32 +04:00
ad207786e2 refactor: clean up empty line 2025-03-13 12:18:54 -07:00
ace8c37c25 docs: fix formatting 2025-03-13 23:11:50 +04:00
4c82408b51 fix(run): grap workspace id from workspace file if not defined on the cli 2025-03-13 11:43:00 -07:00
8146dcef16 refactor(run): call it project instead of workspace 2025-03-13 11:43:00 -07:00
2e90addbc5 refactor(run): do not report project id in error message 2025-03-13 11:43:00 -07:00
427201a634 refactor(run): set up variable before call 2025-03-13 11:43:00 -07:00
0b55ac141c refactor(projects): rename workspace to project 2025-03-13 11:43:00 -07:00
aecfa268ae fix(run): handle case where we require a login 2025-03-13 11:43:00 -07:00
fdfc020efc refactor: clean up more smelly code 2025-03-13 11:43:00 -07:00
62aa80a104 feat(run): ensure that the project has the requested environment 2025-03-13 11:43:00 -07:00
cf9d8035bd feat(run): add function to confirm project has the requested environment 2025-03-13 11:43:00 -07:00
d0c9f1ca53 feat(projects): add new module in util package for getting project details 2025-03-13 11:43:00 -07:00
2ecc7424d9 feat(models): add model for environments 2025-03-13 11:43:00 -07:00
c04b97c689 refactor: clean smelly code 2025-03-13 11:43:00 -07:00
7600a86dfc fix(nix): set gopath for usage by IDEs 2025-03-13 11:43:00 -07:00
8924eaf251 chore: ignore direnv folder 2025-03-13 11:43:00 -07:00
82e9504285 chore: ignore .idea and .go folders 2025-03-13 11:43:00 -07:00
c4e10df754 fix(nix): set the goroot for tools like jetbrains
JetBrains needs to know the GOROOT environment variables. For the sake
of other tooling, we will just set these in the flake rather than only
in the `.envrc` file. It also keeps all environment configuration
localized to our project flake.
2025-03-13 11:43:00 -07:00
ce60e96008 chore(nix): add golang dependency 2025-03-13 11:43:00 -07:00
930b59cb4f chore: helm 2025-03-13 20:20:43 +04:00
ec363a5ad4 feat(infisicalsecret-crd): added configmap support 2025-03-13 20:20:43 +04:00
de7e92ccfc Merge pull request from akhilmhdh/fix/renew-token
Resolved renew token not renewing
2025-03-13 20:12:26 +05:30
522d81ae1a Merge pull request from akhilmhdh/feat/metadata-oidc
Resolved create and update failing for service token
2025-03-13 19:47:51 +05:30
=
02153ffb32 fix: resolved create and update failing for service token 2025-03-13 19:41:33 +05:30
d9d62384e7 Merge pull request from Infisical/org-name-constraint
Improvement: Add Organization Name Constraint
2025-03-12 19:02:38 -07:00
76f34501dc improvements: address feedback 2025-03-12 17:20:53 -07:00
7415bb93b8 Merge branch 'main' into org-name-constraint 2025-03-12 17:07:12 -07:00
7a1c08a7f2 Merge pull request from Infisical/feat/ENG-2352-view-machine-identities-in-admin-console
feat: add ability to view machine identities in admin console
2025-03-12 16:31:54 -07:00
84f9eb5f9f Merge pull request from Infisical/fix/ENG-2341-fix-ui-glitch-hovering-on-comment
fix: ui glitching on hover
2025-03-12 16:55:52 -04:00
=
87ac723fcb feat: resolved renew token not renewing 2025-03-13 01:45:49 +05:30
a6dab47552 Merge pull request from akhilmhdh/fix/delete-secret-approval
Resolved approval rejecting on delete secret
2025-03-13 01:34:44 +05:30
08bac83bcc chore(nix): add comments linking to documentation 2025-03-12 12:23:12 -07:00
46c90f03f0 refactor: use flexbox gap instead of individual margin right 2025-03-12 12:04:28 -07:00
d7722f7587 fix: set pointer events to none for arrow part of popover 2025-03-12 12:04:12 -07:00
a42bcb3393 Merge pull request from Infisical/access-tree
Feature: Role Access Tree
2025-03-12 11:38:35 -07:00
192dba04a5 improvement: update conditions description 2025-03-12 11:34:22 -07:00
0cc3240956 improvements: final feedback 2025-03-12 11:28:38 -07:00
667580546b improvement: check env folders exists 2025-03-12 10:43:45 -07:00
9fd662b7f7 improvements: address feedback 2025-03-12 10:33:56 -07:00
=
a56cbbc02f feat: resolved approval rejecting on delete secret 2025-03-12 14:28:50 +05:30
dc30465afb chore: refactor to avoid dep cycle 2025-03-11 22:04:56 -07:00
f1caab2d00 chore: revert license fns 2025-03-11 22:00:50 -07:00
1d186b1950 feature: access tree 2025-03-11 22:00:25 -07:00
9cf5908cc1 Merge pull request from Infisical/daniel/secret-scanning-docs
docs(platform): secret scanning
2025-03-12 00:09:42 -04:00
f1b6c3764f Update secret-scanning.mdx 2025-03-12 08:07:20 +04:00
4e6c860c69 Update secret-scanning.mdx 2025-03-12 07:46:29 +04:00
eda9ed257e docs: secret scanning 2025-03-12 07:31:25 +04:00
38cf43176e add gateway diagram 2025-03-11 20:13:39 -04:00
f5c7943f2f Merge pull request from Infisical/support-systemd
Add proper support for systemd
2025-03-11 19:21:54 -04:00
3c59f7f350 update deployment docs 2025-03-11 19:21:32 -04:00
84cc7bcd6c add docs + fix nit 2025-03-11 19:01:47 -04:00
159c27ac67 Add proper support for systemd
There wasn't a great way to start the gateway with systemd so that it can run in the background and be managed by systemd. This pr addeds a install sub command that decouples install from running. The goal was so you can run something like this in your IaC:

```infisical gateway install --token=<> --domain=<> && systemctl start infisical-gateway```
2025-03-11 18:43:18 -04:00
de5a432745 fix(lint): appease the linter
There is a conflict between this and our Prettier configuration.
2025-03-11 14:54:03 -07:00
387780aa94 fix(lint): remove file extension from imports
JetBrains accidentally added these when I ran the auto-complete. Weird.
2025-03-11 14:44:22 -07:00
3887ce800b refactor(admin): fix spelling for variable 2025-03-11 14:34:14 -07:00
1a06b3e1f5 fix(admin): stop returning auth method on table 2025-03-11 14:30:04 -07:00
5f0dd31334 Merge pull request from Infisical/databricks-native-integration-disclaimer
Improvement: Databrick Integration Doc Improvements
2025-03-11 14:29:26 -07:00
7e14c58931 improvement: clarify databricks native integration behavior and suggest desingated scope for sync/native integration 2025-03-11 14:12:33 -07:00
627e17b3ae fix(admin): return back auth method from schema too 2025-03-11 14:10:08 -07:00
39b7a4a111 chore(nix): add python312 to list of dependencies 2025-03-11 13:31:23 -07:00
e7c512999e feat(admin): add ability to view machine identities 2025-03-11 13:30:45 -07:00
c19016e6e6 Merge pull request from Infisical/misc/improve-support-for-jwks-via-http
misc: improve support for jwks via http
2025-03-11 23:02:17 +05:30
20477ce2b0 Merge pull request from Infisical/daniel/list-secrets-permissioning-bug
fix: list secrets permissioning bug
2025-03-11 13:18:08 -04:00
e04b2220be Merge pull request from Infisical/password-reqs
feat: Add password requirements to dyanmic secret
2025-03-11 13:16:26 -04:00
edf6a37fe5 fix lint 2025-03-11 13:08:04 -04:00
f5749e326a remove regex and fix lint 2025-03-11 12:49:55 -04:00
75e0a68b68 remove password regex 2025-03-11 12:46:43 -04:00
4dc56033b1 misc: improve support for jwks via http 2025-03-12 00:41:05 +08:00
ed37b99756 fix: list secrets permissioning bug 2025-03-11 20:34:35 +04:00
6fa41a609b remove char and digit rangs and other requested changes/improvments 2025-03-11 12:28:48 -04:00
c9da8477c8 chore(nix): add prettier to list of dependencies 2025-03-11 08:54:15 -07:00
5e4b478b74 refactor(nix): replace shell hook with infisical dependency 2025-03-11 08:17:07 -07:00
765be2d99d Merge pull request from akhilmhdh/fix/remove-user-removal-paywall
feat: removed user paywall for user management and fixed a type error
2025-03-11 19:43:03 +05:30
=
719a18c218 feat: removed user paywall for user management and fixed a type error 2025-03-11 16:03:39 +05:30
16d3bbb67a Add password requirements to dyanmic secret
This will add a new accordion to add custom requirements for the generated password for DB drivers. We can use this pattern for other dynamic secrets too
2025-03-10 23:46:04 -04:00
872a3fe48d Merge pull request from Infisical/revert-3189-revert-3128-daniel/view-secret-value-permission
feat(api/secrets): view secret value permission 2
2025-03-10 23:19:39 -04:00
c7414e00f9 chore: rolled back service token permission changes 2025-03-11 07:11:14 +04:00
ad1dd55b8b chore: requested changes 2025-03-11 06:01:21 +04:00
497761a0e5 fix: missing permision check 2025-03-11 05:44:28 +04:00
483fb458dd requested changes 2025-03-11 04:52:12 +04:00
17cf602a65 style: remove blank line 2025-03-10 16:26:39 -07:00
23f6f5dfd4 chore(nix): add support for flakes 2025-03-10 16:26:18 -07:00
b9b76579ac requested changes 2025-03-11 02:07:38 +04:00
761965696b Merge pull request from Infisical/feat/ENG-2325-change-timestamp-format
fix: change dd-mm-yy to mm-dd-yy
2025-03-10 17:32:31 -04:00
ace2500885 feat(audit): add timestamp format to column header 2025-03-10 14:29:34 -07:00
4eff7d8ea5 fix(audit): change dd-mm-yy to mm-dd-yy 2025-03-10 14:29:34 -07:00
c4512ae111 Update go.sum 2025-03-11 00:33:11 +04:00
78c349c09a fix(view-secret-value): requested changes 2025-03-11 00:31:21 +04:00
09df440613 Update secret-version-dal.ts 2025-03-11 00:18:42 +04:00
a8fc0e540a fix: tests and missing tags permission check 2025-03-11 00:09:00 +04:00
46ce46b5a0 fix: get secret by ID using legacy permissions 2025-03-11 00:09:00 +04:00
dc88115d43 fix: tests failing 2025-03-11 00:08:59 +04:00
955657e172 fix: legacy permission check 2025-03-11 00:08:59 +04:00
f1ba64aa66 fix(view-secret-value): backwards compatibility for read 2025-03-11 00:08:59 +04:00
d74197aeb4 Revert "use forked pion turn server"
This reverts commit bd66411d754df79fb22a0b333ea5205e90affef4.
2025-03-11 00:08:59 +04:00
97567d06d4 Revert "Revert "feat(api/secrets): view secret value permission"" 2025-03-11 00:07:47 +04:00
3986df8e8a Merge pull request from akhilmhdh/fix/gateway-cert-error
feat: changed to permission check
2025-03-10 14:59:16 -04:00
3fcd84b592 Merge pull request from Infisical/daniel/reset-password-serverside
Daniel/reset password serverside
2025-03-10 22:31:22 +04:00
=
29e39b558b feat: changed to permission check 2025-03-10 23:59:17 +05:30
9458c8b04f Update auth-fns.ts 2025-03-10 22:15:30 +04:00
3b95c5d859 Merge pull request from Infisical/add-systemmd-service
add system md service for gateway
2025-03-10 14:07:18 -04:00
de8f315211 Merge pull request from Infisical/feat/addMoreVisibilityToServerAdmins
Add is-admin filter to Server Admin Console and add a component to sh…
2025-03-10 14:06:08 -04:00
9960d58e1b Merge pull request from akhilmhdh/fix/gateway-cert-error
feat: removed ca pool from dialing
2025-03-10 13:02:34 -04:00
=
0057404562 feat: removed ca pool from dialing 2025-03-10 22:22:58 +05:30
47ca1b3011 Merge branch 'main' into feat/addMoreVisibilityToServerAdmins 2025-03-10 11:57:15 -03:00
716cd090c4 Merge pull request from Infisical/daniel/breaking-change-check-fix
fix: breaking change check fix
2025-03-10 18:55:30 +04:00
e870bb3ade Update check-api-for-breaking-changes.yml 2025-03-10 18:53:01 +04:00
98c9e98082 Merge pull request from Infisical/feat/allowProjectSlugEdition
Allow project slug edition
2025-03-10 11:32:29 -03:00
a814f459ab Add condition to hide Instance Admins on cloud instances 2025-03-10 10:58:39 -03:00
66817a40db Adjust modal width to match the rest of the modals 2025-03-10 08:31:19 -03:00
20bd2ca71c Improve slug description, regex and replace useState with watch 2025-03-10 08:18:43 -03:00
=
004a8b71a2 feat: refactored the systemd service to seperate package file 2025-03-10 16:03:51 +05:30
f0fce3086e Merge pull request from Infisical/fix/TagsDeleteButtonNotWorking
Use slug to check tag on remove icon click
2025-03-09 22:32:36 -04:00
a9e7db6fc0 Merge pull request from akhilmhdh/fix/permission-scope
Permission boundary check
2025-03-09 22:25:16 -04:00
2bd681d58f add system md service for gateway 2025-03-09 16:07:33 -04:00
51fef3ce60 Merge pull request from akhilmhdh/fix/gateway-patch-up
Gateway patch up
2025-03-09 14:03:21 -04:00
=
df9e7bf6ee feat: renamed timeout 2025-03-09 22:06:27 +05:30
=
04479bb70a fix: removed cert read to load 2025-03-09 21:37:28 +05:30
=
cdc90411e5 feat: updated gateway to use dtls 2025-03-09 21:15:10 +05:30
=
dcb05a3093 feat: resolved not able to edit sql form due to gateway change 2025-03-09 21:15:10 +05:30
=
b055cda64d feat: increased turn cred duration, and fixed gateway crashing 2025-03-09 21:15:10 +05:30
f68602280e Merge pull request from Infisical/gateway-arch
add gateway security docs
2025-03-07 20:15:49 -05:00
f9483afe95 Merge pull request from akoullick1/patch-13
Update meetings.mdx
2025-03-07 18:31:16 -05:00
d742534f6a Update meetings.mdx
ECD detail
2025-03-07 14:54:38 -08:00
99eb8eb8ed Use slug to check tag on remove icon click 2025-03-07 19:45:10 -03:00
1dea024880 Improvement on admin visibility UI components 2025-03-07 19:19:55 -03:00
699e03c1a9 Allow project slug edition and refactor frontend components to reduce duplicated code 2025-03-07 17:49:30 -03:00
f6372249b4 Merge pull request from Infisical/fix/removeInviteAllOnProjectCreation
Remove addAllMembers option from project creation modal
2025-03-07 17:16:12 -03:00
0f42fcd688 Remove addAllMembers option from project creation modal 2025-03-07 16:59:12 -03:00
2e02f8bea8 Merge pull request from akhilmhdh/feat/webhook-reminder
Added webhook trigger for secret reminder
2025-03-07 14:17:11 -05:00
8203158c63 Merge pull request from Infisical/feat/addSecretNameToSlackNotification
Feat/add secret name to slack notification
2025-03-07 15:39:06 -03:00
ada04ed4fc Update meetings.mdx
Added daily standup
2025-03-07 10:19:54 -08:00
cc9cc70125 Merge pull request from Infisical/misc/add-uncaught-exception-handler
misc: add uncaught exception handler
2025-03-08 00:36:08 +08:00
045debeaf3 misc: added unhandled rejection handler 2025-03-08 00:29:23 +08:00
3fb8ad2fac misc: add uncaught exception handler 2025-03-08 00:22:27 +08:00
795d9e4413 Update auth-password-service.ts 2025-03-07 20:15:30 +04:00
67f2e4671a requested changes 2025-03-07 19:59:29 +04:00
cbe3acde74 Merge pull request from Infisical/fix/address-unhandled-promise-rejects-causing-502
fix: address unhandled promise rejects causing 502s
2025-03-07 23:48:43 +08:00
de480b5771 Merge pull request from Infisical/daniel/id-get-secret
feat: get secret by ID
2025-03-07 19:35:52 +04:00
57f54440d6 misc: added support for type 2025-03-07 23:15:05 +08:00
9711e73a06 fix: address unhandled promise rejects causing 502s 2025-03-07 23:05:47 +08:00
214f837041 Add is-admin filter to Server Admin Console and add a component to show the server admins on side panel 2025-03-07 11:42:15 -03:00
65ddddb6de Change slack notification label from key to secret key 2025-03-07 08:03:02 -03:00
=
a55b26164a feat: updated doc 2025-03-07 15:14:09 +05:30
=
6cd448b8a5 feat: webhook on secret reminder trigger 2025-03-07 15:01:14 +05:30
c48c9ae628 cleanup 2025-03-07 04:55:18 +04:00
7003ad608a Update user-service.ts 2025-03-07 04:37:08 +04:00
104edca6f1 feat: reset password without emergency kit 2025-03-07 04:34:34 +04:00
75345d91c0 add gateway security docs 2025-03-06 18:49:57 -05:00
abc2ffca57 improvement: add organization name constraint 2025-03-06 15:41:27 -08:00
7f6715643d Change label from Secret to Key for consistency with the UI 2025-03-06 15:31:37 -03:00
28c2f1874e Add secret name to slack notification 2025-03-06 12:46:43 -03:00
=
c54eafc128 fix: resolved typo 2025-02-01 01:55:49 +05:30
=
757942aefc feat: resolved nits 2025-02-01 01:55:49 +05:30
=
1d57629036 feat: added unit test in github action 2025-02-01 01:55:49 +05:30
=
8061066e27 feat: added detail description in ui notification 2025-02-01 01:55:48 +05:30
=
c993b1bbe3 feat: completed new permission boundary check 2025-02-01 01:55:48 +05:30
=
2cbf33ac14 feat: added new permission check 2025-02-01 01:55:11 +05:30
286 changed files with 9986 additions and 3994 deletions
.envrc
.github/workflows
.gitignore
backend
e2e-test
package.json
src
ee
lib
main.ts
queue
server
services
vitest.unit.config.ts
cli
company/handbook
docs
flake.lockflake.nix
frontend
package-lock.jsonpackage.json
src
components
context/ProjectPermissionContext
helpers
hooks
layouts/OrganizationLayout/components
MenuIconButton
MinimizedOrgSidebar
ServerAdminsPanel
lib
main.tsx
pages
admin
OverviewPage
SignUpPage
auth
cert-manager/SettingsPage/components
ProjectGeneralTab
ProjectOverviewChangeSection
index.tsx
kms/SettingsPage/components
organization
AuditLogsPage/components
SettingsPage/components/OrgNameChangeSection
project
public/ShareSecretPage/components
secret-manager
OverviewPage
SecretDashboardPage
SettingsPage/components
integrations/DatabricksConfigurePage
ssh/SettingsPage/components
user/PersonalSettingsPage/components
ChangePasswordSection
PersonalGeneralTab
helm-charts/secrets-operator
k8-operator

3
.envrc Normal file

@ -0,0 +1,3 @@
# Learn more at https://direnv.net
# We instruct direnv to use our Nix flake for a consistent development environment.
use flake

@ -92,7 +92,7 @@ jobs:
exit 1
fi
- name: Install openapi-diff
run: go install github.com/tufin/oasdiff@latest
run: go install github.com/oasdiff/oasdiff@latest
- name: Running OpenAPI Spec diff action
run: oasdiff breaking https://app.infisical.com/api/docs/json http://localhost:4000/api/docs/json --fail-on ERR
- name: cleanup

@ -34,7 +34,10 @@ jobs:
working-directory: backend
- name: Start postgres and redis
run: touch .env && docker compose -f docker-compose.dev.yml up -d db redis
- name: Start integration test
- name: Run unit test
run: npm run test:unit
working-directory: backend
- name: Run integration test
run: npm run test:e2e
working-directory: backend
env:
@ -44,4 +47,5 @@ jobs:
ENCRYPTION_KEY: 4bnfe4e407b8921c104518903515b218
- name: cleanup
run: |
docker compose -f "docker-compose.dev.yml" down
docker compose -f "docker-compose.dev.yml" down

8
.gitignore vendored

@ -1,3 +1,5 @@
.direnv/
# backend
node_modules
.env
@ -26,8 +28,6 @@ node_modules
/.pnp
.pnp.js
.env
# testing
coverage
reports
@ -63,10 +63,12 @@ yarn-error.log*
# Editor specific
.vscode/*
.idea/*
**/.idea/*
frontend-build
# cli
.go/
*.tgz
cli/infisical-merge
cli/test/infisical-merge

@ -120,4 +120,3 @@ export default {
};
}
};

@ -40,6 +40,7 @@
"type:check": "tsc --noEmit",
"lint:fix": "eslint --fix --ext js,ts ./src",
"lint": "eslint 'src/**/*.ts'",
"test:unit": "vitest run -c vitest.unit.config.ts",
"test:e2e": "vitest run -c vitest.e2e.config.ts --bail=1",
"test:e2e-watch": "vitest -c vitest.e2e.config.ts --bail=1",
"test:e2e-coverage": "vitest run --coverage -c vitest.e2e.config.ts",
@ -70,6 +71,7 @@
"migrate:org": "tsx ./scripts/migrate-organization.ts",
"seed:new": "tsx ./scripts/create-seed-file.ts",
"seed": "knex --knexfile ./dist/db/knexfile.ts --client pg seed:run",
"seed-dev": "knex --knexfile ./src/db/knexfile.ts --client pg seed:run",
"db:reset": "npm run migration:rollback -- --all && npm run migration:latest"
},
"keywords": [],

@ -1,16 +1,11 @@
import { z } from "zod";
import {
SecretApprovalRequestsReviewersSchema,
SecretApprovalRequestsSchema,
SecretTagsSchema,
UsersSchema
} from "@app/db/schemas";
import { SecretApprovalRequestsReviewersSchema, SecretApprovalRequestsSchema, UsersSchema } from "@app/db/schemas";
import { EventType } from "@app/ee/services/audit-log/audit-log-types";
import { ApprovalStatus, RequestState } from "@app/ee/services/secret-approval-request/secret-approval-request-types";
import { readLimit, writeLimit } from "@app/server/config/rateLimiter";
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
import { secretRawSchema } from "@app/server/routes/sanitizedSchemas";
import { SanitizedTagSchema, secretRawSchema } from "@app/server/routes/sanitizedSchemas";
import { AuthMode } from "@app/services/auth/auth-type";
import { ResourceMetadataSchema } from "@app/services/resource-metadata/resource-metadata-schema";
@ -250,14 +245,6 @@ export const registerSecretApprovalRequestRouter = async (server: FastifyZodProv
}
});
const tagSchema = SecretTagsSchema.pick({
id: true,
slug: true,
color: true
})
.array()
.optional();
server.route({
method: "GET",
url: "/:id",
@ -291,7 +278,7 @@ export const registerSecretApprovalRequestRouter = async (server: FastifyZodProv
.omit({ _id: true, environment: true, workspace: true, type: true, version: true })
.extend({
op: z.string(),
tags: tagSchema,
tags: SanitizedTagSchema.array().optional(),
secretMetadata: ResourceMetadataSchema.nullish(),
secret: z
.object({
@ -310,7 +297,7 @@ export const registerSecretApprovalRequestRouter = async (server: FastifyZodProv
secretKey: z.string(),
secretValue: z.string().optional(),
secretComment: z.string().optional(),
tags: tagSchema,
tags: SanitizedTagSchema.array().optional(),
secretMetadata: ResourceMetadataSchema.nullish()
})
.optional()

@ -1,6 +1,6 @@
import z from "zod";
import { ProjectPermissionActions } from "@app/ee/services/permission/project-permission";
import { ProjectPermissionSecretActions } from "@app/ee/services/permission/project-permission";
import { RAW_SECRETS } from "@app/lib/api-docs";
import { removeTrailingSlash } from "@app/lib/fn";
import { readLimit } from "@app/server/config/rateLimiter";
@ -9,7 +9,7 @@ import { AuthMode } from "@app/services/auth/auth-type";
const AccessListEntrySchema = z
.object({
allowedActions: z.nativeEnum(ProjectPermissionActions).array(),
allowedActions: z.nativeEnum(ProjectPermissionSecretActions).array(),
id: z.string(),
membershipId: z.string(),
name: z.string()

@ -22,7 +22,11 @@ export const registerSecretVersionRouter = async (server: FastifyZodProvider) =>
}),
response: {
200: z.object({
secretVersions: secretRawSchema.array()
secretVersions: secretRawSchema
.extend({
secretValueHidden: z.boolean()
})
.array()
})
}
},
@ -37,6 +41,7 @@ export const registerSecretVersionRouter = async (server: FastifyZodProvider) =>
offset: req.query.offset,
secretId: req.params.secretId
});
return { secretVersions };
}
});

@ -1,10 +1,10 @@
import { z } from "zod";
import { SecretSnapshotsSchema, SecretTagsSchema } from "@app/db/schemas";
import { SecretSnapshotsSchema } from "@app/db/schemas";
import { PROJECTS } from "@app/lib/api-docs";
import { readLimit, writeLimit } from "@app/server/config/rateLimiter";
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
import { secretRawSchema } from "@app/server/routes/sanitizedSchemas";
import { SanitizedTagSchema, secretRawSchema } from "@app/server/routes/sanitizedSchemas";
import { AuthMode } from "@app/services/auth/auth-type";
export const registerSnapshotRouter = async (server: FastifyZodProvider) => {
@ -31,12 +31,9 @@ export const registerSnapshotRouter = async (server: FastifyZodProvider) => {
secretVersions: secretRawSchema
.omit({ _id: true, environment: true, workspace: true, type: true })
.extend({
secretValueHidden: z.boolean(),
secretId: z.string(),
tags: SecretTagsSchema.pick({
id: true,
slug: true,
color: true
}).array()
tags: SanitizedTagSchema.array()
})
.array(),
folderVersion: z.object({ id: z.string(), name: z.string() }).array(),
@ -55,6 +52,7 @@ export const registerSnapshotRouter = async (server: FastifyZodProvider) => {
actorOrgId: req.permission.orgId,
id: req.params.secretSnapshotId
});
return { secretSnapshot };
}
});

@ -2,6 +2,7 @@ import slugify from "@sindresorhus/slugify";
import ms from "ms";
import { z } from "zod";
import { checkForInvalidPermissionCombination } from "@app/ee/services/permission/permission-fns";
import { ProjectPermissionV2Schema } from "@app/ee/services/permission/project-permission";
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";
@ -23,7 +24,9 @@ export const registerUserAdditionalPrivilegeRouter = async (server: FastifyZodPr
body: z.object({
projectMembershipId: z.string().min(1).describe(PROJECT_USER_ADDITIONAL_PRIVILEGE.CREATE.projectMembershipId),
slug: slugSchema({ min: 1, max: 60 }).optional().describe(PROJECT_USER_ADDITIONAL_PRIVILEGE.CREATE.slug),
permissions: ProjectPermissionV2Schema.array().describe(PROJECT_USER_ADDITIONAL_PRIVILEGE.CREATE.permissions),
permissions: ProjectPermissionV2Schema.array()
.describe(PROJECT_USER_ADDITIONAL_PRIVILEGE.CREATE.permissions)
.refine(checkForInvalidPermissionCombination),
type: z.discriminatedUnion("isTemporary", [
z.object({
isTemporary: z.literal(false)
@ -81,7 +84,8 @@ export const registerUserAdditionalPrivilegeRouter = async (server: FastifyZodPr
slug: slugSchema({ min: 1, max: 60 }).describe(PROJECT_USER_ADDITIONAL_PRIVILEGE.UPDATE.slug),
permissions: ProjectPermissionV2Schema.array()
.optional()
.describe(PROJECT_USER_ADDITIONAL_PRIVILEGE.UPDATE.permissions),
.describe(PROJECT_USER_ADDITIONAL_PRIVILEGE.UPDATE.permissions)
.refine(checkForInvalidPermissionCombination),
type: z.discriminatedUnion("isTemporary", [
z.object({ isTemporary: z.literal(false).describe(PROJECT_USER_ADDITIONAL_PRIVILEGE.UPDATE.isTemporary) }),
z.object({

@ -3,6 +3,7 @@ import ms from "ms";
import { z } from "zod";
import { IdentityProjectAdditionalPrivilegeTemporaryMode } from "@app/ee/services/identity-project-additional-privilege-v2/identity-project-additional-privilege-v2-types";
import { checkForInvalidPermissionCombination } from "@app/ee/services/permission/permission-fns";
import { ProjectPermissionV2Schema } from "@app/ee/services/permission/project-permission";
import { IDENTITY_ADDITIONAL_PRIVILEGE_V2 } from "@app/lib/api-docs";
import { alphaNumericNanoId } from "@app/lib/nanoid";
@ -30,7 +31,9 @@ export const registerIdentityProjectAdditionalPrivilegeRouter = async (server: F
identityId: z.string().min(1).describe(IDENTITY_ADDITIONAL_PRIVILEGE_V2.CREATE.identityId),
projectId: z.string().min(1).describe(IDENTITY_ADDITIONAL_PRIVILEGE_V2.CREATE.projectId),
slug: slugSchema({ min: 1, max: 60 }).optional().describe(IDENTITY_ADDITIONAL_PRIVILEGE_V2.CREATE.slug),
permissions: ProjectPermissionV2Schema.array().describe(IDENTITY_ADDITIONAL_PRIVILEGE_V2.CREATE.permission),
permissions: ProjectPermissionV2Schema.array()
.describe(IDENTITY_ADDITIONAL_PRIVILEGE_V2.CREATE.permission)
.refine(checkForInvalidPermissionCombination),
type: z.discriminatedUnion("isTemporary", [
z.object({
isTemporary: z.literal(false)
@ -94,7 +97,8 @@ export const registerIdentityProjectAdditionalPrivilegeRouter = async (server: F
slug: slugSchema({ min: 1, max: 60 }).describe(IDENTITY_ADDITIONAL_PRIVILEGE_V2.UPDATE.slug),
permissions: ProjectPermissionV2Schema.array()
.optional()
.describe(IDENTITY_ADDITIONAL_PRIVILEGE_V2.UPDATE.privilegePermission),
.describe(IDENTITY_ADDITIONAL_PRIVILEGE_V2.UPDATE.privilegePermission)
.refine(checkForInvalidPermissionCombination),
type: z.discriminatedUnion("isTemporary", [
z.object({ isTemporary: z.literal(false).describe(IDENTITY_ADDITIONAL_PRIVILEGE_V2.UPDATE.isTemporary) }),
z.object({

@ -2,6 +2,7 @@ import { packRules } from "@casl/ability/extra";
import { z } from "zod";
import { ProjectMembershipRole, ProjectRolesSchema } from "@app/db/schemas";
import { checkForInvalidPermissionCombination } from "@app/ee/services/permission/permission-fns";
import { ProjectPermissionV2Schema } from "@app/ee/services/permission/project-permission";
import { PROJECT_ROLE } from "@app/lib/api-docs";
import { readLimit, writeLimit } from "@app/server/config/rateLimiter";
@ -37,7 +38,9 @@ export const registerProjectRoleRouter = async (server: FastifyZodProvider) => {
.describe(PROJECT_ROLE.CREATE.slug),
name: z.string().min(1).trim().describe(PROJECT_ROLE.CREATE.name),
description: z.string().trim().nullish().describe(PROJECT_ROLE.CREATE.description),
permissions: ProjectPermissionV2Schema.array().describe(PROJECT_ROLE.CREATE.permissions)
permissions: ProjectPermissionV2Schema.array()
.describe(PROJECT_ROLE.CREATE.permissions)
.refine(checkForInvalidPermissionCombination)
}),
response: {
200: z.object({
@ -92,7 +95,10 @@ export const registerProjectRoleRouter = async (server: FastifyZodProvider) => {
.describe(PROJECT_ROLE.UPDATE.slug),
name: z.string().trim().optional().describe(PROJECT_ROLE.UPDATE.name),
description: z.string().trim().nullish().describe(PROJECT_ROLE.UPDATE.description),
permissions: ProjectPermissionV2Schema.array().describe(PROJECT_ROLE.UPDATE.permissions).optional()
permissions: ProjectPermissionV2Schema.array()
.describe(PROJECT_ROLE.UPDATE.permissions)
.optional()
.superRefine(checkForInvalidPermissionCombination)
}),
response: {
200: z.object({

@ -1,5 +1,16 @@
import { z } from "zod";
export type PasswordRequirements = {
length: number;
required: {
lowercase: number;
uppercase: number;
digits: number;
symbols: number;
};
allowedSymbols?: string;
};
export enum SqlProviders {
Postgres = "postgres",
MySQL = "mysql2",
@ -100,6 +111,28 @@ export const DynamicSecretSqlDBSchema = z.object({
database: z.string().trim(),
username: z.string().trim(),
password: z.string().trim(),
passwordRequirements: z
.object({
length: z.number().min(1).max(250),
required: z
.object({
lowercase: z.number().min(0),
uppercase: z.number().min(0),
digits: z.number().min(0),
symbols: z.number().min(0)
})
.refine((data) => {
const total = Object.values(data).reduce((sum, count) => sum + count, 0);
return total <= 250;
}, "Sum of required characters cannot exceed 250"),
allowedSymbols: z.string().optional()
})
.refine((data) => {
const total = Object.values(data.required).reduce((sum, count) => sum + count, 0);
return total <= data.length;
}, "Sum of required characters cannot exceed the total length")
.optional()
.describe("Password generation requirements"),
creationStatement: z.string().trim(),
revocationStatement: z.string().trim(),
renewStatement: z.string().trim().optional(),

@ -1,6 +1,6 @@
import { randomInt } from "crypto";
import handlebars from "handlebars";
import knex from "knex";
import { customAlphabet } from "nanoid";
import { z } from "zod";
import { withGatewayProxy } from "@app/lib/gateway";
@ -8,16 +8,99 @@ import { alphaNumericNanoId } from "@app/lib/nanoid";
import { TGatewayServiceFactory } from "../../gateway/gateway-service";
import { verifyHostInputValidity } from "../dynamic-secret-fns";
import { DynamicSecretSqlDBSchema, SqlProviders, TDynamicProviderFns } from "./models";
import { DynamicSecretSqlDBSchema, PasswordRequirements, SqlProviders, TDynamicProviderFns } from "./models";
const EXTERNAL_REQUEST_TIMEOUT = 10 * 1000;
const generatePassword = (provider: SqlProviders) => {
// oracle has limit of 48 password length
const size = provider === SqlProviders.Oracle ? 30 : 48;
const DEFAULT_PASSWORD_REQUIREMENTS = {
length: 48,
required: {
lowercase: 1,
uppercase: 1,
digits: 1,
symbols: 0
},
allowedSymbols: "-_.~!*"
};
const charset = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-_.~!*";
return customAlphabet(charset, 48)(size);
const ORACLE_PASSWORD_REQUIREMENTS = {
...DEFAULT_PASSWORD_REQUIREMENTS,
length: 30
};
const generatePassword = (provider: SqlProviders, requirements?: PasswordRequirements) => {
const defaultReqs = provider === SqlProviders.Oracle ? ORACLE_PASSWORD_REQUIREMENTS : DEFAULT_PASSWORD_REQUIREMENTS;
const finalReqs = requirements || defaultReqs;
try {
const { length, required, allowedSymbols } = finalReqs;
const chars = {
lowercase: "abcdefghijklmnopqrstuvwxyz",
uppercase: "ABCDEFGHIJKLMNOPQRSTUVWXYZ",
digits: "0123456789",
symbols: allowedSymbols || "-_.~!*"
};
const parts: string[] = [];
if (required.lowercase > 0) {
parts.push(
...Array(required.lowercase)
.fill(0)
.map(() => chars.lowercase[randomInt(chars.lowercase.length)])
);
}
if (required.uppercase > 0) {
parts.push(
...Array(required.uppercase)
.fill(0)
.map(() => chars.uppercase[randomInt(chars.uppercase.length)])
);
}
if (required.digits > 0) {
parts.push(
...Array(required.digits)
.fill(0)
.map(() => chars.digits[randomInt(chars.digits.length)])
);
}
if (required.symbols > 0) {
parts.push(
...Array(required.symbols)
.fill(0)
.map(() => chars.symbols[randomInt(chars.symbols.length)])
);
}
const requiredTotal = Object.values(required).reduce<number>((a, b) => a + b, 0);
const remainingLength = Math.max(length - requiredTotal, 0);
const allowedChars = Object.entries(chars)
.filter(([key]) => required[key as keyof typeof required] > 0)
.map(([, value]) => value)
.join("");
parts.push(
...Array(remainingLength)
.fill(0)
.map(() => allowedChars[randomInt(allowedChars.length)])
);
// shuffle the array to mix up the characters
for (let i = parts.length - 1; i > 0; i -= 1) {
const j = randomInt(i + 1);
[parts[i], parts[j]] = [parts[j], parts[i]];
}
return parts.join("");
} catch (error: unknown) {
const message = error instanceof Error ? error.message : "Unknown error";
throw new Error(`Failed to generate password: ${message}`);
}
};
const generateUsername = (provider: SqlProviders) => {
@ -115,7 +198,7 @@ export const SqlDatabaseProvider = ({ gatewayService }: TSqlDatabaseProviderDTO)
const create = async (inputs: unknown, expireAt: number) => {
const providerInputs = await validateProviderInputs(inputs);
const username = generateUsername(providerInputs.client);
const password = generatePassword(providerInputs.client);
const password = generatePassword(providerInputs.client, providerInputs.passwordRequirements);
const gatewayCallback = async (host = providerInputs.host, port = providerInputs.port) => {
const db = await $getClient({ ...providerInputs, port, host });
try {

@ -3,7 +3,7 @@ import slugify from "@sindresorhus/slugify";
import { OrgMembershipRole, TOrgRoles } from "@app/db/schemas";
import { TOidcConfigDALFactory } from "@app/ee/services/oidc/oidc-config-dal";
import { isAtLeastAsPrivileged } from "@app/lib/casl";
import { validatePermissionBoundary } from "@app/lib/casl/boundary";
import { BadRequestError, ForbiddenRequestError, NotFoundError, UnauthorizedError } from "@app/lib/errors";
import { alphaNumericNanoId } from "@app/lib/nanoid";
import { TGroupProjectDALFactory } from "@app/services/group-project/group-project-dal";
@ -87,9 +87,14 @@ export const groupServiceFactory = ({
actorOrgId
);
const isCustomRole = Boolean(customRole);
const hasRequiredPriviledges = isAtLeastAsPrivileged(permission, rolePermission);
if (!hasRequiredPriviledges)
throw new ForbiddenRequestError({ message: "Failed to create a more privileged group" });
const permissionBoundary = validatePermissionBoundary(permission, rolePermission);
if (!permissionBoundary.isValid)
throw new ForbiddenRequestError({
name: "PermissionBoundaryError",
message: "Failed to create a more privileged group",
details: { missingPermissions: permissionBoundary.missingPermissions }
});
const group = await groupDAL.transaction(async (tx) => {
const existingGroup = await groupDAL.findOne({ orgId: actorOrgId, name }, tx);
@ -156,9 +161,13 @@ export const groupServiceFactory = ({
);
const isCustomRole = Boolean(customOrgRole);
const hasRequiredNewRolePermission = isAtLeastAsPrivileged(permission, rolePermission);
if (!hasRequiredNewRolePermission)
throw new ForbiddenRequestError({ message: "Failed to create a more privileged group" });
const permissionBoundary = validatePermissionBoundary(permission, rolePermission);
if (!permissionBoundary.isValid)
throw new ForbiddenRequestError({
name: "PermissionBoundaryError",
message: "Failed to update a more privileged group",
details: { missingPermissions: permissionBoundary.missingPermissions }
});
if (isCustomRole) customRole = customOrgRole;
}
@ -329,9 +338,13 @@ export const groupServiceFactory = ({
const { permission: groupRolePermission } = await permissionService.getOrgPermissionByRole(group.role, actorOrgId);
// check if user has broader or equal to privileges than group
const hasRequiredPrivileges = isAtLeastAsPrivileged(permission, groupRolePermission);
if (!hasRequiredPrivileges)
throw new ForbiddenRequestError({ message: "Failed to add user to more privileged group" });
const permissionBoundary = validatePermissionBoundary(permission, groupRolePermission);
if (!permissionBoundary.isValid)
throw new ForbiddenRequestError({
name: "PermissionBoundaryError",
message: "Failed to add user to more privileged group",
details: { missingPermissions: permissionBoundary.missingPermissions }
});
const user = await userDAL.findOne({ username });
if (!user) throw new NotFoundError({ message: `Failed to find user with username ${username}` });
@ -396,9 +409,13 @@ export const groupServiceFactory = ({
const { permission: groupRolePermission } = await permissionService.getOrgPermissionByRole(group.role, actorOrgId);
// check if user has broader or equal to privileges than group
const hasRequiredPrivileges = isAtLeastAsPrivileged(permission, groupRolePermission);
if (!hasRequiredPrivileges)
throw new ForbiddenRequestError({ message: "Failed to delete user from more privileged group" });
const permissionBoundary = validatePermissionBoundary(permission, groupRolePermission);
if (!permissionBoundary.isValid)
throw new ForbiddenRequestError({
name: "PermissionBoundaryError",
message: "Failed to delete user from more privileged group",
details: { missingPermissions: permissionBoundary.missingPermissions }
});
const user = await userDAL.findOne({ username });
if (!user) throw new NotFoundError({ message: `Failed to find user with username ${username}` });

@ -3,7 +3,7 @@ import { packRules } from "@casl/ability/extra";
import ms from "ms";
import { ActionProjectType, TableName } from "@app/db/schemas";
import { isAtLeastAsPrivileged } from "@app/lib/casl";
import { validatePermissionBoundary } from "@app/lib/casl/boundary";
import { BadRequestError, ForbiddenRequestError, NotFoundError } from "@app/lib/errors";
import { unpackPermissions } from "@app/server/routes/sanitizedSchema/permission";
import { ActorType } from "@app/services/auth/auth-type";
@ -79,9 +79,13 @@ export const identityProjectAdditionalPrivilegeV2ServiceFactory = ({
// we need to validate that the privilege given is not higher than the assigning users permission
// @ts-expect-error this is expected error because of one being really accurate rule definition other being a bit more broader. Both are valid casl rules
targetIdentityPermission.update(targetIdentityPermission.rules.concat(customPermission));
const hasRequiredPriviledges = isAtLeastAsPrivileged(permission, targetIdentityPermission);
if (!hasRequiredPriviledges)
throw new ForbiddenRequestError({ message: "Failed to update more privileged identity" });
const permissionBoundary = validatePermissionBoundary(permission, targetIdentityPermission);
if (!permissionBoundary.isValid)
throw new ForbiddenRequestError({
name: "PermissionBoundaryError",
message: "Failed to update more privileged identity",
details: { missingPermissions: permissionBoundary.missingPermissions }
});
const existingSlug = await identityProjectAdditionalPrivilegeDAL.findOne({
slug,
@ -161,9 +165,13 @@ export const identityProjectAdditionalPrivilegeV2ServiceFactory = ({
// we need to validate that the privilege given is not higher than the assigning users permission
// @ts-expect-error this is expected error because of one being really accurate rule definition other being a bit more broader. Both are valid casl rules
targetIdentityPermission.update(targetIdentityPermission.rules.concat(data.permissions || []));
const hasRequiredPriviledges = isAtLeastAsPrivileged(permission, targetIdentityPermission);
if (!hasRequiredPriviledges)
throw new ForbiddenRequestError({ message: "Failed to update more privileged identity" });
const permissionBoundary = validatePermissionBoundary(permission, targetIdentityPermission);
if (!permissionBoundary.isValid)
throw new ForbiddenRequestError({
name: "PermissionBoundaryError",
message: "Failed to update more privileged identity",
details: { missingPermissions: permissionBoundary.missingPermissions }
});
if (data?.slug) {
const existingSlug = await identityProjectAdditionalPrivilegeDAL.findOne({
@ -239,9 +247,13 @@ export const identityProjectAdditionalPrivilegeV2ServiceFactory = ({
actorOrgId,
actionProjectType: ActionProjectType.Any
});
const hasRequiredPriviledges = isAtLeastAsPrivileged(permission, identityRolePermission);
if (!hasRequiredPriviledges)
throw new ForbiddenRequestError({ message: "Failed to update more privileged identity" });
const permissionBoundary = validatePermissionBoundary(permission, identityRolePermission);
if (!permissionBoundary.isValid)
throw new ForbiddenRequestError({
name: "PermissionBoundaryError",
message: "Failed to update more privileged identity",
details: { missingPermissions: permissionBoundary.missingPermissions }
});
const deletedPrivilege = await identityProjectAdditionalPrivilegeDAL.deleteById(identityPrivilege.id);
return {

@ -3,7 +3,7 @@ import { PackRule, packRules, unpackRules } from "@casl/ability/extra";
import ms from "ms";
import { ActionProjectType } from "@app/db/schemas";
import { isAtLeastAsPrivileged } from "@app/lib/casl";
import { validatePermissionBoundary } from "@app/lib/casl/boundary";
import { BadRequestError, ForbiddenRequestError, NotFoundError } from "@app/lib/errors";
import { UnpackedPermissionSchema } from "@app/server/routes/sanitizedSchema/permission";
import { ActorType } from "@app/services/auth/auth-type";
@ -88,9 +88,13 @@ export const identityProjectAdditionalPrivilegeServiceFactory = ({
// we need to validate that the privilege given is not higher than the assigning users permission
// @ts-expect-error this is expected error because of one being really accurate rule definition other being a bit more broader. Both are valid casl rules
targetIdentityPermission.update(targetIdentityPermission.rules.concat(customPermission));
const hasRequiredPriviledges = isAtLeastAsPrivileged(permission, targetIdentityPermission);
if (!hasRequiredPriviledges)
throw new ForbiddenRequestError({ message: "Failed to update more privileged identity" });
const permissionBoundary = validatePermissionBoundary(permission, targetIdentityPermission);
if (!permissionBoundary.isValid)
throw new ForbiddenRequestError({
name: "PermissionBoundaryError",
message: "Failed to update more privileged identity",
details: { missingPermissions: permissionBoundary.missingPermissions }
});
const existingSlug = await identityProjectAdditionalPrivilegeDAL.findOne({
slug,
@ -172,9 +176,13 @@ export const identityProjectAdditionalPrivilegeServiceFactory = ({
// we need to validate that the privilege given is not higher than the assigning users permission
// @ts-expect-error this is expected error because of one being really accurate rule definition other being a bit more broader. Both are valid casl rules
targetIdentityPermission.update(targetIdentityPermission.rules.concat(data.permissions || []));
const hasRequiredPriviledges = isAtLeastAsPrivileged(permission, targetIdentityPermission);
if (!hasRequiredPriviledges)
throw new ForbiddenRequestError({ message: "Failed to update more privileged identity" });
const permissionBoundary = validatePermissionBoundary(permission, targetIdentityPermission);
if (!permissionBoundary.isValid)
throw new ForbiddenRequestError({
name: "PermissionBoundaryError",
message: "Failed to update more privileged identity",
details: { missingPermissions: permissionBoundary.missingPermissions }
});
const identityPrivilege = await identityProjectAdditionalPrivilegeDAL.findOne({
slug,
@ -268,9 +276,13 @@ export const identityProjectAdditionalPrivilegeServiceFactory = ({
actorOrgId,
actionProjectType: ActionProjectType.Any
});
const hasRequiredPriviledges = isAtLeastAsPrivileged(permission, identityRolePermission);
if (!hasRequiredPriviledges)
throw new ForbiddenRequestError({ message: "Failed to edit more privileged identity" });
const permissionBoundary = validatePermissionBoundary(permission, identityRolePermission);
if (!permissionBoundary.isValid)
throw new ForbiddenRequestError({
name: "PermissionBoundaryError",
message: "Failed to edit more privileged identity",
details: { missingPermissions: permissionBoundary.missingPermissions }
});
const identityPrivilege = await identityProjectAdditionalPrivilegeDAL.findOne({
slug,

@ -1,7 +1,109 @@
/* eslint-disable no-nested-ternary */
import { ForbiddenError, MongoAbility, PureAbility, subject } from "@casl/ability";
import { z } from "zod";
import { TOrganizations } from "@app/db/schemas";
import { ForbiddenRequestError, UnauthorizedError } from "@app/lib/errors";
import { BadRequestError, ForbiddenRequestError, UnauthorizedError } from "@app/lib/errors";
import { ActorAuthMethod, AuthMethod } from "@app/services/auth/auth-type";
import {
ProjectPermissionSecretActions,
ProjectPermissionSet,
ProjectPermissionSub,
ProjectPermissionV2Schema,
SecretSubjectFields
} from "./project-permission";
export function throwIfMissingSecretReadValueOrDescribePermission(
permission: MongoAbility<ProjectPermissionSet> | PureAbility,
action: Extract<
ProjectPermissionSecretActions,
ProjectPermissionSecretActions.ReadValue | ProjectPermissionSecretActions.DescribeSecret
>,
subjectFields?: SecretSubjectFields
) {
try {
if (subjectFields) {
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionSecretActions.DescribeAndReadValue,
subject(ProjectPermissionSub.Secrets, subjectFields)
);
} else {
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionSecretActions.DescribeAndReadValue,
ProjectPermissionSub.Secrets
);
}
} catch {
if (subjectFields) {
ForbiddenError.from(permission).throwUnlessCan(action, subject(ProjectPermissionSub.Secrets, subjectFields));
} else {
ForbiddenError.from(permission).throwUnlessCan(action, ProjectPermissionSub.Secrets);
}
}
}
export function hasSecretReadValueOrDescribePermission(
permission: MongoAbility<ProjectPermissionSet>,
action: Extract<
ProjectPermissionSecretActions,
ProjectPermissionSecretActions.DescribeSecret | ProjectPermissionSecretActions.ReadValue
>,
subjectFields?: SecretSubjectFields
) {
let canNewPermission = false;
let canOldPermission = false;
if (subjectFields) {
canNewPermission = permission.can(action, subject(ProjectPermissionSub.Secrets, subjectFields));
canOldPermission = permission.can(
ProjectPermissionSecretActions.DescribeAndReadValue,
subject(ProjectPermissionSub.Secrets, subjectFields)
);
} else {
canNewPermission = permission.can(action, ProjectPermissionSub.Secrets);
canOldPermission = permission.can(
ProjectPermissionSecretActions.DescribeAndReadValue,
ProjectPermissionSub.Secrets
);
}
return canNewPermission || canOldPermission;
}
const OptionalArrayPermissionSchema = ProjectPermissionV2Schema.array().optional();
export function checkForInvalidPermissionCombination(permissions: z.infer<typeof OptionalArrayPermissionSchema>) {
if (!permissions) return;
for (const permission of permissions) {
if (permission.subject === ProjectPermissionSub.Secrets) {
if (permission.action.includes(ProjectPermissionSecretActions.DescribeAndReadValue)) {
const hasReadValue = permission.action.includes(ProjectPermissionSecretActions.ReadValue);
const hasDescribeSecret = permission.action.includes(ProjectPermissionSecretActions.DescribeSecret);
// eslint-disable-next-line no-continue
if (!hasReadValue && !hasDescribeSecret) continue;
const hasBothDescribeAndReadValue = hasReadValue && hasDescribeSecret;
throw new BadRequestError({
message: `You have selected Read, and ${
hasBothDescribeAndReadValue
? "both Read Value and Describe Secret"
: hasReadValue
? "Read Value"
: hasDescribeSecret
? "Describe Secret"
: ""
}. You cannot select Read Value or Describe Secret if you have selected Read. The Read permission is a legacy action which has been replaced by Describe Secret and Read Value.`
});
}
}
}
return true;
}
function isAuthMethodSaml(actorAuthMethod: ActorAuthMethod) {
if (!actorAuthMethod) return false;

@ -5,22 +5,6 @@ import { PermissionConditionOperators } from "@app/lib/casl";
export const PermissionConditionSchema = {
[PermissionConditionOperators.$IN]: z.string().trim().min(1).array(),
[PermissionConditionOperators.$ALL]: z.string().trim().min(1).array(),
[PermissionConditionOperators.$REGEX]: z
.string()
.min(1)
.refine(
(el) => {
try {
// eslint-disable-next-line no-new
new RegExp(el);
return true;
} catch {
return false;
}
},
{ message: "Invalid regex pattern" }
),
[PermissionConditionOperators.$EQ]: z.string().min(1),
[PermissionConditionOperators.$NEQ]: z.string().min(1),
[PermissionConditionOperators.$GLOB]: z

@ -17,6 +17,15 @@ export enum ProjectPermissionActions {
Delete = "delete"
}
export enum ProjectPermissionSecretActions {
DescribeAndReadValue = "read",
DescribeSecret = "describeSecret",
ReadValue = "readValue",
Create = "create",
Edit = "edit",
Delete = "delete"
}
export enum ProjectPermissionCmekActions {
Read = "read",
Create = "create",
@ -115,7 +124,7 @@ export type IdentityManagementSubjectFields = {
export type ProjectPermissionSet =
| [
ProjectPermissionActions,
ProjectPermissionSecretActions,
ProjectPermissionSub.Secrets | (ForcedSubject<ProjectPermissionSub.Secrets> & SecretSubjectFields)
]
| [
@ -429,6 +438,7 @@ const GeneralPermissionSchema = [
})
];
// Do not update this schema anymore, as it's kept purely for backwards compatability. Update V2 schema only.
export const ProjectPermissionV1Schema = z.discriminatedUnion("subject", [
z.object({
subject: z.literal(ProjectPermissionSub.Secrets).describe("The entity this permission pertains to."),
@ -460,7 +470,7 @@ export const ProjectPermissionV2Schema = z.discriminatedUnion("subject", [
z.object({
subject: z.literal(ProjectPermissionSub.Secrets).describe("The entity this permission pertains to."),
inverted: z.boolean().optional().describe("Whether rule allows or forbids."),
action: CASL_ACTION_SCHEMA_NATIVE_ENUM(ProjectPermissionActions).describe(
action: CASL_ACTION_SCHEMA_NATIVE_ENUM(ProjectPermissionSecretActions).describe(
"Describe what action an entity can take."
),
conditions: SecretConditionV2Schema.describe(
@ -517,7 +527,6 @@ const buildAdminPermissionRules = () => {
// Admins get full access to everything
[
ProjectPermissionSub.Secrets,
ProjectPermissionSub.SecretFolders,
ProjectPermissionSub.SecretImports,
ProjectPermissionSub.SecretApproval,
@ -550,10 +559,22 @@ const buildAdminPermissionRules = () => {
ProjectPermissionActions.Create,
ProjectPermissionActions.Delete
],
el as ProjectPermissionSub
el
);
});
can(
[
ProjectPermissionSecretActions.DescribeAndReadValue,
ProjectPermissionSecretActions.DescribeSecret,
ProjectPermissionSecretActions.ReadValue,
ProjectPermissionSecretActions.Create,
ProjectPermissionSecretActions.Edit,
ProjectPermissionSecretActions.Delete
],
ProjectPermissionSub.Secrets
);
can(
[
ProjectPermissionDynamicSecretActions.ReadRootCredential,
@ -613,10 +634,12 @@ const buildMemberPermissionRules = () => {
can(
[
ProjectPermissionActions.Read,
ProjectPermissionActions.Edit,
ProjectPermissionActions.Create,
ProjectPermissionActions.Delete
ProjectPermissionSecretActions.DescribeAndReadValue,
ProjectPermissionSecretActions.DescribeSecret,
ProjectPermissionSecretActions.ReadValue,
ProjectPermissionSecretActions.Edit,
ProjectPermissionSecretActions.Create,
ProjectPermissionSecretActions.Delete
],
ProjectPermissionSub.Secrets
);
@ -788,7 +811,9 @@ export const projectMemberPermissions = buildMemberPermissionRules();
const buildViewerPermissionRules = () => {
const { can, rules } = new AbilityBuilder<MongoAbility<ProjectPermissionSet>>(createMongoAbility);
can(ProjectPermissionActions.Read, ProjectPermissionSub.Secrets);
can(ProjectPermissionSecretActions.DescribeAndReadValue, ProjectPermissionSub.Secrets);
can(ProjectPermissionSecretActions.DescribeSecret, ProjectPermissionSub.Secrets);
can(ProjectPermissionSecretActions.ReadValue, ProjectPermissionSub.Secrets);
can(ProjectPermissionActions.Read, ProjectPermissionSub.SecretFolders);
can(ProjectPermissionDynamicSecretActions.ReadRootCredential, ProjectPermissionSub.DynamicSecrets);
can(ProjectPermissionActions.Read, ProjectPermissionSub.SecretImports);
@ -837,7 +862,6 @@ export const buildServiceTokenProjectPermission = (
(subject) => {
if (canWrite) {
can(ProjectPermissionActions.Edit, subject, {
// TODO: @Akhi
// @ts-expect-error type
secretPath: { $glob: secretPath },
environment
@ -916,7 +940,17 @@ export const backfillPermissionV1SchemaToV2Schema = (
subject: ProjectPermissionSub.SecretImports as const
}));
const secretPolicies = secretSubjects.map(({ subject, ...el }) => ({
subject: ProjectPermissionSub.Secrets as const,
...el,
action:
el.action.includes(ProjectPermissionActions.Read) && !el.action.includes(ProjectPermissionSecretActions.ReadValue)
? el.action.concat(ProjectPermissionSecretActions.ReadValue)
: el.action
}));
const secretFolderPolicies = secretSubjects
.map(({ subject, ...el }) => ({
...el,
// read permission is not needed anymore
@ -958,6 +992,7 @@ export const backfillPermissionV1SchemaToV2Schema = (
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore-error this is valid ts
secretImportPolicies,
secretPolicies,
dynamicSecretPolicies,
hasReadOnlyFolder.length ? [] : secretFolderPolicies
);

@ -3,7 +3,7 @@ import { PackRule, packRules, unpackRules } from "@casl/ability/extra";
import ms from "ms";
import { ActionProjectType, TableName } from "@app/db/schemas";
import { isAtLeastAsPrivileged } from "@app/lib/casl";
import { validatePermissionBoundary } from "@app/lib/casl/boundary";
import { BadRequestError, ForbiddenRequestError, NotFoundError } from "@app/lib/errors";
import { UnpackedPermissionSchema } from "@app/server/routes/sanitizedSchema/permission";
import { ActorType } from "@app/services/auth/auth-type";
@ -76,9 +76,13 @@ export const projectUserAdditionalPrivilegeServiceFactory = ({
// we need to validate that the privilege given is not higher than the assigning users permission
// @ts-expect-error this is expected error because of one being really accurate rule definition other being a bit more broader. Both are valid casl rules
targetUserPermission.update(targetUserPermission.rules.concat(customPermission));
const hasRequiredPriviledges = isAtLeastAsPrivileged(permission, targetUserPermission);
if (!hasRequiredPriviledges)
throw new ForbiddenRequestError({ message: "Failed to update more privileged identity" });
const permissionBoundary = validatePermissionBoundary(permission, targetUserPermission);
if (!permissionBoundary.isValid)
throw new ForbiddenRequestError({
name: "PermissionBoundaryError",
message: "Failed to update more privileged user",
details: { missingPermissions: permissionBoundary.missingPermissions }
});
const existingSlug = await projectUserAdditionalPrivilegeDAL.findOne({
slug,
@ -163,9 +167,13 @@ export const projectUserAdditionalPrivilegeServiceFactory = ({
// we need to validate that the privilege given is not higher than the assigning users permission
// @ts-expect-error this is expected error because of one being really accurate rule definition other being a bit more broader. Both are valid casl rules
targetUserPermission.update(targetUserPermission.rules.concat(dto.permissions || []));
const hasRequiredPriviledges = isAtLeastAsPrivileged(permission, targetUserPermission);
if (!hasRequiredPriviledges)
throw new ForbiddenRequestError({ message: "Failed to update more privileged identity" });
const permissionBoundary = validatePermissionBoundary(permission, targetUserPermission);
if (!permissionBoundary.isValid)
throw new ForbiddenRequestError({
name: "PermissionBoundaryError",
message: "Failed to update more privileged identity",
details: { missingPermissions: permissionBoundary.missingPermissions }
});
if (dto?.slug) {
const existingSlug = await projectUserAdditionalPrivilegeDAL.findOne({

@ -6,6 +6,7 @@ import {
SecretEncryptionAlgo,
SecretKeyEncoding,
SecretType,
TableName,
TSecretApprovalRequestsSecretsInsert,
TSecretApprovalRequestsSecretsV2Insert
} from "@app/db/schemas";
@ -57,8 +58,9 @@ import { SmtpTemplates, TSmtpService } from "@app/services/smtp/smtp-service";
import { TUserDALFactory } from "@app/services/user/user-dal";
import { TLicenseServiceFactory } from "../license/license-service";
import { throwIfMissingSecretReadValueOrDescribePermission } from "../permission/permission-fns";
import { TPermissionServiceFactory } from "../permission/permission-service";
import { ProjectPermissionActions, ProjectPermissionSub } from "../permission/project-permission";
import { ProjectPermissionSecretActions, ProjectPermissionSub } from "../permission/project-permission";
import { TSecretApprovalPolicyDALFactory } from "../secret-approval-policy/secret-approval-policy-dal";
import { TSecretSnapshotServiceFactory } from "../secret-snapshot/secret-snapshot-service";
import { TSecretApprovalRequestDALFactory } from "./secret-approval-request-dal";
@ -88,7 +90,12 @@ type TSecretApprovalRequestServiceFactoryDep = {
secretDAL: TSecretDALFactory;
secretTagDAL: Pick<
TSecretTagDALFactory,
"findManyTagsById" | "saveTagsToSecret" | "deleteTagsManySecret" | "saveTagsToSecretV2" | "deleteTagsToSecretV2"
| "findManyTagsById"
| "saveTagsToSecret"
| "deleteTagsManySecret"
| "saveTagsToSecretV2"
| "deleteTagsToSecretV2"
| "find"
>;
secretBlindIndexDAL: Pick<TSecretBlindIndexDALFactory, "findOne">;
snapshotService: Pick<TSecretSnapshotServiceFactory, "performSnapshot">;
@ -106,7 +113,7 @@ type TSecretApprovalRequestServiceFactoryDep = {
kmsService: Pick<TKmsServiceFactory, "createCipherPairWithDataKey" | "encryptWithInputKey" | "decryptWithInputKey">;
secretV2BridgeDAL: Pick<
TSecretV2BridgeDALFactory,
"insertMany" | "upsertSecretReferences" | "findBySecretKeys" | "bulkUpdate" | "deleteMany"
"insertMany" | "upsertSecretReferences" | "findBySecretKeys" | "bulkUpdate" | "deleteMany" | "find"
>;
secretVersionV2BridgeDAL: Pick<TSecretVersionV2DALFactory, "insertMany" | "findLatestVersionMany">;
secretVersionTagV2BridgeDAL: Pick<TSecretVersionV2TagDALFactory, "insertMany">;
@ -503,7 +510,7 @@ export const secretApprovalRequestServiceFactory = ({
if (!hasMinApproval && !isSoftEnforcement)
throw new BadRequestError({ message: "Doesn't have minimum approvals needed" });
const { botKey, shouldUseSecretV2Bridge } = await projectBotService.getBotKey(projectId);
const { botKey, shouldUseSecretV2Bridge, project } = await projectBotService.getBotKey(projectId);
let mergeStatus;
if (shouldUseSecretV2Bridge) {
// this cycle if for bridged secrets
@ -861,7 +868,6 @@ export const secretApprovalRequestServiceFactory = ({
if (isSoftEnforcement) {
const cfg = getConfig();
const project = await projectDAL.findProjectById(projectId);
const env = await projectEnvDAL.findOne({ id: policy.envId });
const requestedByUser = await userDAL.findOne({ id: actorId });
const approverUsers = await userDAL.find({
@ -913,10 +919,11 @@ export const secretApprovalRequestServiceFactory = ({
actorOrgId,
actionProjectType: ActionProjectType.SecretManager
});
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Read,
subject(ProjectPermissionSub.Secrets, { environment, secretPath })
);
throwIfMissingSecretReadValueOrDescribePermission(permission, ProjectPermissionSecretActions.ReadValue, {
environment,
secretPath
});
await projectDAL.checkProjectUpgradeStatus(projectId);
@ -1001,6 +1008,7 @@ export const secretApprovalRequestServiceFactory = ({
: keyName2BlindIndex[secretName];
// add tags
if (tagIds?.length) commitTagIds[keyName2BlindIndex[secretName]] = tagIds;
return {
...latestSecretVersions[secretId],
...el,
@ -1156,7 +1164,8 @@ export const secretApprovalRequestServiceFactory = ({
environment: env.name,
secretPath,
projectId,
requestId: secretApprovalRequest.id
requestId: secretApprovalRequest.id,
secretKeys: [...new Set(Object.values(data).flatMap((arr) => arr?.map((item) => item.secretName) ?? []))]
}
}
});
@ -1327,17 +1336,48 @@ export const secretApprovalRequestServiceFactory = ({
// deleted secrets
const deletedSecrets = data[SecretOperations.Delete];
if (deletedSecrets && deletedSecrets.length) {
const secretsToDeleteInDB = await secretV2BridgeDAL.findBySecretKeys(
const secretsToDeleteInDB = await secretV2BridgeDAL.find({
folderId,
deletedSecrets.map((el) => ({
key: el.secretKey,
type: SecretType.Shared
}))
);
$complex: {
operator: "and",
value: [
{
operator: "or",
value: deletedSecrets.map((el) => ({
operator: "and",
value: [
{
operator: "eq",
field: `${TableName.SecretV2}.key` as "key",
value: el.secretKey
},
{
operator: "eq",
field: "type",
value: SecretType.Shared
}
]
}))
}
]
}
});
if (secretsToDeleteInDB.length !== deletedSecrets.length)
throw new NotFoundError({
message: `Secret does not exist: ${secretsToDeleteInDB.map((el) => el.key).join(",")}`
});
secretsToDeleteInDB.forEach((el) => {
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionSecretActions.Delete,
subject(ProjectPermissionSub.Secrets, {
environment,
secretPath,
secretName: el.key,
secretTags: el.tags?.map((i) => i.slug)
})
);
});
const secretsGroupedByKey = groupBy(secretsToDeleteInDB, (i) => i.key);
const deletedSecretIds = deletedSecrets.map((el) => secretsGroupedByKey[el.secretKey][0].id);
const latestSecretVersions = await secretVersionV2BridgeDAL.findLatestVersionMany(folderId, deletedSecretIds);
@ -1363,9 +1403,9 @@ export const secretApprovalRequestServiceFactory = ({
const tagsGroupById = groupBy(tags, (i) => i.id);
commits.forEach((commit) => {
let action = ProjectPermissionActions.Create;
if (commit.op === SecretOperations.Update) action = ProjectPermissionActions.Edit;
if (commit.op === SecretOperations.Delete) action = ProjectPermissionActions.Delete;
let action = ProjectPermissionSecretActions.Create;
if (commit.op === SecretOperations.Update) action = ProjectPermissionSecretActions.Edit;
if (commit.op === SecretOperations.Delete) return; // we do the validation on top
ForbiddenError.from(permission).throwUnlessCan(
action,
@ -1456,7 +1496,8 @@ export const secretApprovalRequestServiceFactory = ({
environment: env.name,
secretPath,
projectId,
requestId: secretApprovalRequest.id
requestId: secretApprovalRequest.id,
secretKeys: [...new Set(Object.values(data).flatMap((arr) => arr?.map((item) => item.secretKey) ?? []))]
}
}
});

@ -265,6 +265,7 @@ export const secretReplicationServiceFactory = ({
folderDAL,
secretImportDAL,
decryptor: (value) => (value ? secretManagerDecryptor({ cipherTextBlob: value }).toString() : ""),
viewSecretValue: true,
hasSecretAccess: () => true
});
// secrets that gets replicated across imports

@ -15,7 +15,11 @@ import { TSecretV2BridgeDALFactory } from "@app/services/secret-v2-bridge/secret
import { TLicenseServiceFactory } from "../license/license-service";
import { TPermissionServiceFactory } from "../permission/permission-service";
import { ProjectPermissionActions, ProjectPermissionSub } from "../permission/project-permission";
import {
ProjectPermissionActions,
ProjectPermissionSecretActions,
ProjectPermissionSub
} from "../permission/project-permission";
import { TSecretRotationDALFactory } from "./secret-rotation-dal";
import { TSecretRotationQueueFactory } from "./secret-rotation-queue";
import { TSecretRotationEncData } from "./secret-rotation-queue/secret-rotation-queue-types";
@ -106,7 +110,7 @@ export const secretRotationServiceFactory = ({
});
}
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Edit,
ProjectPermissionSecretActions.Edit,
subject(ProjectPermissionSub.Secrets, { environment, secretPath })
);

@ -1,6 +1,6 @@
/* eslint-disable @typescript-eslint/no-unsafe-assignment,@typescript-eslint/no-unsafe-member-access,@typescript-eslint/no-unsafe-argument */
// akhilmhdh: I did this, quite strange bug with eslint. Everything do have a type stil has this error
import { ForbiddenError, subject } from "@casl/ability";
import { ForbiddenError } from "@casl/ability";
import { ActionProjectType, TableName, TSecretTagJunctionInsert, TSecretV2TagJunctionInsert } from "@app/db/schemas";
import { decryptSymmetric128BitHexKeyUTF8 } from "@app/lib/crypto";
@ -12,6 +12,7 @@ import { TKmsServiceFactory } from "@app/services/kms/kms-service";
import { KmsDataKey } from "@app/services/kms/kms-types";
import { TProjectBotServiceFactory } from "@app/services/project-bot/project-bot-service";
import { TSecretDALFactory } from "@app/services/secret/secret-dal";
import { INFISICAL_SECRET_VALUE_HIDDEN_MASK } from "@app/services/secret/secret-fns";
import { TSecretVersionDALFactory } from "@app/services/secret/secret-version-dal";
import { TSecretVersionTagDALFactory } from "@app/services/secret/secret-version-tag-dal";
import { TSecretFolderDALFactory } from "@app/services/secret-folder/secret-folder-dal";
@ -22,8 +23,16 @@ import { TSecretVersionV2DALFactory } from "@app/services/secret-v2-bridge/secre
import { TSecretVersionV2TagDALFactory } from "@app/services/secret-v2-bridge/secret-version-tag-dal";
import { TLicenseServiceFactory } from "../license/license-service";
import {
hasSecretReadValueOrDescribePermission,
throwIfMissingSecretReadValueOrDescribePermission
} from "../permission/permission-fns";
import { TPermissionServiceFactory } from "../permission/permission-service";
import { ProjectPermissionActions, ProjectPermissionSub } from "../permission/project-permission";
import {
ProjectPermissionActions,
ProjectPermissionSecretActions,
ProjectPermissionSub
} from "../permission/project-permission";
import {
TGetSnapshotDataDTO,
TProjectSnapshotCountDTO,
@ -97,10 +106,10 @@ export const secretSnapshotServiceFactory = ({
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.SecretRollback);
// We need to check if the user has access to the secrets in the folder. If we don't do this, a user could theoretically access snapshot secret values even if they don't have read access to the secrets in the folder.
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Read,
subject(ProjectPermissionSub.Secrets, { environment, secretPath: path })
);
throwIfMissingSecretReadValueOrDescribePermission(permission, ProjectPermissionSecretActions.DescribeSecret, {
environment,
secretPath: path
});
const folder = await folderDAL.findBySecretPath(projectId, environment, path);
if (!folder) {
@ -134,10 +143,10 @@ export const secretSnapshotServiceFactory = ({
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.SecretRollback);
// We need to check if the user has access to the secrets in the folder. If we don't do this, a user could theoretically access snapshot secret values even if they don't have read access to the secrets in the folder.
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Read,
subject(ProjectPermissionSub.Secrets, { environment, secretPath: path })
);
throwIfMissingSecretReadValueOrDescribePermission(permission, ProjectPermissionSecretActions.DescribeSecret, {
environment,
secretPath: path
});
const folder = await folderDAL.findBySecretPath(projectId, environment, path);
if (!folder)
@ -162,6 +171,7 @@ export const secretSnapshotServiceFactory = ({
});
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.SecretRollback);
const shouldUseBridge = snapshot.projectVersion === 3;
let snapshotDetails;
if (shouldUseBridge) {
@ -170,68 +180,112 @@ export const secretSnapshotServiceFactory = ({
projectId: snapshot.projectId
});
const encryptedSnapshotDetails = await snapshotDAL.findSecretSnapshotV2DataById(id);
const fullFolderPath = await getFullFolderPath({
folderDAL,
folderId: encryptedSnapshotDetails.folderId,
envId: encryptedSnapshotDetails.environment.id
});
snapshotDetails = {
...encryptedSnapshotDetails,
secretVersions: encryptedSnapshotDetails.secretVersions.map((el) => ({
...el,
secretKey: el.key,
secretValue: el.encryptedValue
? secretManagerDecryptor({ cipherTextBlob: el.encryptedValue }).toString()
: "",
secretComment: el.encryptedComment
? secretManagerDecryptor({ cipherTextBlob: el.encryptedComment }).toString()
: ""
}))
secretVersions: encryptedSnapshotDetails.secretVersions.map((el) => {
const canReadValue = hasSecretReadValueOrDescribePermission(
permission,
ProjectPermissionSecretActions.ReadValue,
{
environment: encryptedSnapshotDetails.environment.slug,
secretPath: fullFolderPath,
secretName: el.key,
secretTags: el.tags.length ? el.tags.map((tag) => tag.slug) : undefined
}
);
let secretValue = "";
if (canReadValue) {
secretValue = el.encryptedValue
? secretManagerDecryptor({ cipherTextBlob: el.encryptedValue }).toString()
: "";
} else {
secretValue = INFISICAL_SECRET_VALUE_HIDDEN_MASK;
}
return {
...el,
secretKey: el.key,
secretValueHidden: !canReadValue,
secretValue,
secretComment: el.encryptedComment
? secretManagerDecryptor({ cipherTextBlob: el.encryptedComment }).toString()
: ""
};
})
};
} else {
const encryptedSnapshotDetails = await snapshotDAL.findSecretSnapshotDataById(id);
const fullFolderPath = await getFullFolderPath({
folderDAL,
folderId: encryptedSnapshotDetails.folderId,
envId: encryptedSnapshotDetails.environment.id
});
const { botKey } = await projectBotService.getBotKey(snapshot.projectId);
if (!botKey)
throw new NotFoundError({ message: `Project bot key not found for project with ID '${snapshot.projectId}'` });
snapshotDetails = {
...encryptedSnapshotDetails,
secretVersions: encryptedSnapshotDetails.secretVersions.map((el) => ({
...el,
secretKey: decryptSymmetric128BitHexKeyUTF8({
secretVersions: encryptedSnapshotDetails.secretVersions.map((el) => {
const secretKey = decryptSymmetric128BitHexKeyUTF8({
ciphertext: el.secretKeyCiphertext,
iv: el.secretKeyIV,
tag: el.secretKeyTag,
key: botKey
}),
secretValue: decryptSymmetric128BitHexKeyUTF8({
ciphertext: el.secretValueCiphertext,
iv: el.secretValueIV,
tag: el.secretValueTag,
key: botKey
}),
secretComment:
el.secretCommentTag && el.secretCommentIV && el.secretCommentCiphertext
? decryptSymmetric128BitHexKeyUTF8({
ciphertext: el.secretCommentCiphertext,
iv: el.secretCommentIV,
tag: el.secretCommentTag,
key: botKey
})
: ""
}))
});
const canReadValue = hasSecretReadValueOrDescribePermission(
permission,
ProjectPermissionSecretActions.ReadValue,
{
environment: encryptedSnapshotDetails.environment.slug,
secretPath: fullFolderPath,
secretName: secretKey,
secretTags: el.tags.length ? el.tags.map((tag) => tag.slug) : undefined
}
);
let secretValue = "";
if (canReadValue) {
secretValue = decryptSymmetric128BitHexKeyUTF8({
ciphertext: el.secretValueCiphertext,
iv: el.secretValueIV,
tag: el.secretValueTag,
key: botKey
});
} else {
secretValue = INFISICAL_SECRET_VALUE_HIDDEN_MASK;
}
return {
...el,
secretKey,
secretValueHidden: !canReadValue,
secretValue,
secretComment:
el.secretCommentTag && el.secretCommentIV && el.secretCommentCiphertext
? decryptSymmetric128BitHexKeyUTF8({
ciphertext: el.secretCommentCiphertext,
iv: el.secretCommentIV,
tag: el.secretCommentTag,
key: botKey
})
: ""
};
})
};
}
const fullFolderPath = await getFullFolderPath({
folderDAL,
folderId: snapshotDetails.folderId,
envId: snapshotDetails.environment.id
});
// We need to check if the user has access to the secrets in the folder. If we don't do this, a user could theoretically access snapshot secret values even if they don't have read access to the secrets in the folder.
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Read,
subject(ProjectPermissionSub.Secrets, {
environment: snapshotDetails.environment.slug,
secretPath: fullFolderPath
})
);
return snapshotDetails;
};

@ -459,7 +459,8 @@ export const PROJECTS = {
workspaceId: "The ID of the project to update.",
name: "The new name of the project.",
projectDescription: "An optional description label for the project.",
autoCapitalization: "Disable or enable auto-capitalization for the project."
autoCapitalization: "Disable or enable auto-capitalization for the project.",
slug: "An optional slug for the project. (must be unique within the organization)"
},
GET_KEY: {
workspaceId: "The ID of the project to get the key from."
@ -666,6 +667,7 @@ export const SECRETS = {
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",
viewSecretValue: "Whether or not to retrieve the secret value.",
projectSlug: "The slug of the project where the secret is located.",
tagSlugs: "An array of existing tag slugs to attach to the secret."
},
@ -689,6 +691,7 @@ export const RAW_SECRETS = {
"The slug of the project to list secrets from. This parameter is only applicable by machine identities.",
environment: "The slug of the environment to list secrets from.",
secretPath: "The secret path to list secrets from.",
viewSecretValue: "Whether or not to retrieve the secret value.",
includeImports: "Weather to include imported secrets or not.",
tagSlugs: "The comma separated tag slugs to filter secrets.",
metadataFilter:
@ -717,6 +720,7 @@ export const RAW_SECRETS = {
secretPath: "The path of the secret to get.",
version: "The version of the secret to get.",
type: "The type of the secret to get.",
viewSecretValue: "Whether or not to retrieve the secret value.",
includeImports: "Weather to include imported secrets or not."
},
UPDATE: {

@ -0,0 +1,669 @@
import { createMongoAbility } from "@casl/ability";
import { PermissionConditionOperators } from ".";
import { validatePermissionBoundary } from "./boundary";
describe("Validate Permission Boundary Function", () => {
test.each([
{
title: "child with equal privilege",
parentPermission: createMongoAbility([
{
action: ["create", "edit", "delete", "read"],
subject: "secrets"
}
]),
childPermission: createMongoAbility([
{
action: ["create", "edit", "delete", "read"],
subject: "secrets"
}
]),
expectValid: true,
missingPermissions: []
},
{
title: "child with less privilege",
parentPermission: createMongoAbility([
{
action: ["create", "edit", "delete", "read"],
subject: "secrets"
}
]),
childPermission: createMongoAbility([
{
action: ["create", "edit"],
subject: "secrets"
}
]),
expectValid: true,
missingPermissions: []
},
{
title: "child with more privilege",
parentPermission: createMongoAbility([
{
action: ["create"],
subject: "secrets"
}
]),
childPermission: createMongoAbility([
{
action: ["create", "edit"],
subject: "secrets"
}
]),
expectValid: false,
missingPermissions: [{ action: "edit", subject: "secrets" }]
},
{
title: "parent with multiple and child with multiple",
parentPermission: createMongoAbility([
{
action: ["create"],
subject: "secrets"
},
{
action: ["create", "edit"],
subject: "members"
}
]),
childPermission: createMongoAbility([
{
action: ["create"],
subject: "members"
},
{
action: ["create"],
subject: "secrets"
}
]),
expectValid: true,
missingPermissions: []
},
{
title: "Child with no access",
parentPermission: createMongoAbility([
{
action: ["create"],
subject: "secrets"
},
{
action: ["create", "edit"],
subject: "members"
}
]),
childPermission: createMongoAbility([]),
expectValid: true,
missingPermissions: []
},
{
title: "Parent and child disjoint set",
parentPermission: createMongoAbility([
{
action: ["create", "edit", "delete", "read"],
subject: "secrets",
conditions: {
environment: { [PermissionConditionOperators.$EQ]: "dev" }
}
}
]),
childPermission: createMongoAbility([
{
action: ["create", "edit", "delete", "read"],
subject: "secrets",
conditions: {
secretPath: { [PermissionConditionOperators.$EQ]: "dev" }
}
}
]),
expectValid: false,
missingPermissions: ["create", "edit", "delete", "read"].map((el) => ({
action: el,
subject: "secrets",
conditions: {
secretPath: { [PermissionConditionOperators.$EQ]: "dev" }
}
}))
},
{
title: "Parent with inverted rules",
parentPermission: createMongoAbility([
{
action: ["create", "edit", "delete", "read"],
subject: "secrets",
conditions: {
environment: { [PermissionConditionOperators.$EQ]: "dev" }
}
},
{
action: "read",
subject: "secrets",
inverted: true,
conditions: {
environment: { [PermissionConditionOperators.$EQ]: "dev" },
secretPath: { [PermissionConditionOperators.$GLOB]: "/hello/**" }
}
}
]),
childPermission: createMongoAbility([
{
action: "read",
subject: "secrets",
conditions: {
environment: { [PermissionConditionOperators.$EQ]: "dev" },
secretPath: { [PermissionConditionOperators.$EQ]: "/" }
}
}
]),
expectValid: true,
missingPermissions: []
},
{
title: "Parent with inverted rules - child accessing invalid one",
parentPermission: createMongoAbility([
{
action: ["create", "edit", "delete", "read"],
subject: "secrets",
conditions: {
environment: { [PermissionConditionOperators.$EQ]: "dev" }
}
},
{
action: "read",
subject: "secrets",
inverted: true,
conditions: {
environment: { [PermissionConditionOperators.$EQ]: "dev" },
secretPath: { [PermissionConditionOperators.$GLOB]: "/hello/**" }
}
}
]),
childPermission: createMongoAbility([
{
action: "read",
subject: "secrets",
conditions: {
environment: { [PermissionConditionOperators.$EQ]: "dev" },
secretPath: { [PermissionConditionOperators.$EQ]: "/hello/world" }
}
}
]),
expectValid: false,
missingPermissions: [
{
action: "read",
subject: "secrets",
conditions: {
environment: { [PermissionConditionOperators.$EQ]: "dev" },
secretPath: { [PermissionConditionOperators.$EQ]: "/hello/world" }
}
}
]
}
])("Check permission: $title", ({ parentPermission, childPermission, expectValid, missingPermissions }) => {
const permissionBoundary = validatePermissionBoundary(parentPermission, childPermission);
if (expectValid) {
expect(permissionBoundary.isValid).toBeTruthy();
} else {
expect(permissionBoundary.isValid).toBeFalsy();
expect(permissionBoundary.missingPermissions).toEqual(expect.arrayContaining(missingPermissions));
}
});
});
describe("Validate Permission Boundary: Checking Parent $eq operator", () => {
const parentPermission = createMongoAbility([
{
action: ["create", "read"],
subject: "secrets",
conditions: {
environment: { [PermissionConditionOperators.$EQ]: "dev" }
}
}
]);
test.each([
{
operator: PermissionConditionOperators.$EQ,
childPermission: createMongoAbility([
{
action: ["create"],
subject: "secrets",
conditions: {
environment: { [PermissionConditionOperators.$EQ]: "dev" }
}
}
])
},
{
operator: PermissionConditionOperators.$IN,
childPermission: createMongoAbility([
{
action: ["create"],
subject: "secrets",
conditions: {
environment: { [PermissionConditionOperators.$IN]: ["dev"] }
}
}
])
},
{
operator: PermissionConditionOperators.$GLOB,
childPermission: createMongoAbility([
{
action: ["create"],
subject: "secrets",
conditions: {
environment: { [PermissionConditionOperators.$GLOB]: "dev" }
}
}
])
}
])("Child $operator truthy cases", ({ childPermission }) => {
const permissionBoundary = validatePermissionBoundary(parentPermission, childPermission);
expect(permissionBoundary.isValid).toBeTruthy();
});
test.each([
{
operator: PermissionConditionOperators.$EQ,
childPermission: createMongoAbility([
{
action: ["create"],
subject: "secrets",
conditions: {
environment: { [PermissionConditionOperators.$EQ]: "prod" }
}
}
])
},
{
operator: PermissionConditionOperators.$IN,
childPermission: createMongoAbility([
{
action: ["create"],
subject: "secrets",
conditions: {
environment: { [PermissionConditionOperators.$IN]: ["dev", "prod"] }
}
}
])
},
{
operator: PermissionConditionOperators.$GLOB,
childPermission: createMongoAbility([
{
action: ["create"],
subject: "secrets",
conditions: {
environment: { [PermissionConditionOperators.$GLOB]: "dev**" }
}
}
])
},
{
operator: PermissionConditionOperators.$NEQ,
childPermission: createMongoAbility([
{
action: ["create"],
subject: "secrets",
conditions: {
environment: { [PermissionConditionOperators.$GLOB]: "staging" }
}
}
])
}
])("Child $operator falsy cases", ({ childPermission }) => {
const permissionBoundary = validatePermissionBoundary(parentPermission, childPermission);
expect(permissionBoundary.isValid).toBeFalsy();
});
});
describe("Validate Permission Boundary: Checking Parent $neq operator", () => {
const parentPermission = createMongoAbility([
{
action: ["create", "read"],
subject: "secrets",
conditions: {
secretPath: { [PermissionConditionOperators.$NEQ]: "/hello" }
}
}
]);
test.each([
{
operator: PermissionConditionOperators.$EQ,
childPermission: createMongoAbility([
{
action: ["create"],
subject: "secrets",
conditions: {
secretPath: { [PermissionConditionOperators.$EQ]: "/" }
}
}
])
},
{
operator: PermissionConditionOperators.$NEQ,
childPermission: createMongoAbility([
{
action: ["create"],
subject: "secrets",
conditions: {
secretPath: { [PermissionConditionOperators.$NEQ]: "/hello" }
}
}
])
},
{
operator: PermissionConditionOperators.$IN,
childPermission: createMongoAbility([
{
action: ["create"],
subject: "secrets",
conditions: {
secretPath: { [PermissionConditionOperators.$IN]: ["/", "/staging"] }
}
}
])
},
{
operator: PermissionConditionOperators.$GLOB,
childPermission: createMongoAbility([
{
action: ["create"],
subject: "secrets",
conditions: {
secretPath: { [PermissionConditionOperators.$GLOB]: "/dev**" }
}
}
])
}
])("Child $operator truthy cases", ({ childPermission }) => {
const permissionBoundary = validatePermissionBoundary(parentPermission, childPermission);
expect(permissionBoundary.isValid).toBeTruthy();
});
test.each([
{
operator: PermissionConditionOperators.$EQ,
childPermission: createMongoAbility([
{
action: ["create"],
subject: "secrets",
conditions: {
secretPath: { [PermissionConditionOperators.$EQ]: "/hello" }
}
}
])
},
{
operator: PermissionConditionOperators.$NEQ,
childPermission: createMongoAbility([
{
action: ["create"],
subject: "secrets",
conditions: {
secretPath: { [PermissionConditionOperators.$NEQ]: "/" }
}
}
])
},
{
operator: PermissionConditionOperators.$IN,
childPermission: createMongoAbility([
{
action: ["create"],
subject: "secrets",
conditions: {
secretPath: { [PermissionConditionOperators.$IN]: ["/", "/hello"] }
}
}
])
},
{
operator: PermissionConditionOperators.$GLOB,
childPermission: createMongoAbility([
{
action: ["create"],
subject: "secrets",
conditions: {
secretPath: { [PermissionConditionOperators.$GLOB]: "/hello**" }
}
}
])
}
])("Child $operator falsy cases", ({ childPermission }) => {
const permissionBoundary = validatePermissionBoundary(parentPermission, childPermission);
expect(permissionBoundary.isValid).toBeFalsy();
});
});
describe("Validate Permission Boundary: Checking Parent $IN operator", () => {
const parentPermission = createMongoAbility([
{
action: ["edit"],
subject: "secrets",
conditions: {
environment: { [PermissionConditionOperators.$IN]: ["dev", "staging"] }
}
}
]);
test.each([
{
operator: PermissionConditionOperators.$EQ,
childPermission: createMongoAbility([
{
action: ["edit"],
subject: "secrets",
conditions: {
environment: { [PermissionConditionOperators.$EQ]: "dev" }
}
}
])
},
{
operator: PermissionConditionOperators.$IN,
childPermission: createMongoAbility([
{
action: ["edit"],
subject: "secrets",
conditions: {
environment: { [PermissionConditionOperators.$IN]: ["dev"] }
}
}
])
},
{
operator: `${PermissionConditionOperators.$IN} - 2`,
childPermission: createMongoAbility([
{
action: ["edit"],
subject: "secrets",
conditions: {
environment: { [PermissionConditionOperators.$IN]: ["dev", "staging"] }
}
}
])
},
{
operator: PermissionConditionOperators.$GLOB,
childPermission: createMongoAbility([
{
action: ["edit"],
subject: "secrets",
conditions: {
environment: { [PermissionConditionOperators.$GLOB]: "dev" }
}
}
])
}
])("Child $operator truthy cases", ({ childPermission }) => {
const permissionBoundary = validatePermissionBoundary(parentPermission, childPermission);
expect(permissionBoundary.isValid).toBeTruthy();
});
test.each([
{
operator: PermissionConditionOperators.$EQ,
childPermission: createMongoAbility([
{
action: ["edit"],
subject: "secrets",
conditions: {
environment: { [PermissionConditionOperators.$EQ]: "prod" }
}
}
])
},
{
operator: PermissionConditionOperators.$NEQ,
childPermission: createMongoAbility([
{
action: ["edit"],
subject: "secrets",
conditions: {
environment: { [PermissionConditionOperators.$NEQ]: "dev" }
}
}
])
},
{
operator: PermissionConditionOperators.$IN,
childPermission: createMongoAbility([
{
action: ["edit"],
subject: "secrets",
conditions: {
environment: { [PermissionConditionOperators.$IN]: ["dev", "prod"] }
}
}
])
},
{
operator: PermissionConditionOperators.$GLOB,
childPermission: createMongoAbility([
{
action: ["edit"],
subject: "secrets",
conditions: {
environment: { [PermissionConditionOperators.$GLOB]: "dev**" }
}
}
])
}
])("Child $operator falsy cases", ({ childPermission }) => {
const permissionBoundary = validatePermissionBoundary(parentPermission, childPermission);
expect(permissionBoundary.isValid).toBeFalsy();
});
});
describe("Validate Permission Boundary: Checking Parent $GLOB operator", () => {
const parentPermission = createMongoAbility([
{
action: ["create", "read"],
subject: "secrets",
conditions: {
secretPath: { [PermissionConditionOperators.$GLOB]: "/hello/**" }
}
}
]);
test.each([
{
operator: PermissionConditionOperators.$EQ,
childPermission: createMongoAbility([
{
action: ["create"],
subject: "secrets",
conditions: {
secretPath: { [PermissionConditionOperators.$EQ]: "/hello/world" }
}
}
])
},
{
operator: PermissionConditionOperators.$IN,
childPermission: createMongoAbility([
{
action: ["create"],
subject: "secrets",
conditions: {
secretPath: { [PermissionConditionOperators.$IN]: ["/hello/world", "/hello/world2"] }
}
}
])
},
{
operator: PermissionConditionOperators.$GLOB,
childPermission: createMongoAbility([
{
action: ["create"],
subject: "secrets",
conditions: {
secretPath: { [PermissionConditionOperators.$GLOB]: "/hello/**/world" }
}
}
])
}
])("Child $operator truthy cases", ({ childPermission }) => {
const permissionBoundary = validatePermissionBoundary(parentPermission, childPermission);
expect(permissionBoundary.isValid).toBeTruthy();
});
test.each([
{
operator: PermissionConditionOperators.$EQ,
childPermission: createMongoAbility([
{
action: ["create"],
subject: "secrets",
conditions: {
secretPath: { [PermissionConditionOperators.$EQ]: "/print" }
}
}
])
},
{
operator: PermissionConditionOperators.$NEQ,
childPermission: createMongoAbility([
{
action: ["create"],
subject: "secrets",
conditions: {
secretPath: { [PermissionConditionOperators.$NEQ]: "/hello/world" }
}
}
])
},
{
operator: PermissionConditionOperators.$IN,
childPermission: createMongoAbility([
{
action: ["create"],
subject: "secrets",
conditions: {
secretPath: { [PermissionConditionOperators.$IN]: ["/", "/hello"] }
}
}
])
},
{
operator: PermissionConditionOperators.$GLOB,
childPermission: createMongoAbility([
{
action: ["create"],
subject: "secrets",
conditions: {
secretPath: { [PermissionConditionOperators.$GLOB]: "/hello**" }
}
}
])
}
])("Child $operator falsy cases", ({ childPermission }) => {
const permissionBoundary = validatePermissionBoundary(parentPermission, childPermission);
expect(permissionBoundary.isValid).toBeFalsy();
});
});

@ -0,0 +1,249 @@
import { MongoAbility } from "@casl/ability";
import { MongoQuery } from "@ucast/mongo2js";
import picomatch from "picomatch";
import { PermissionConditionOperators } from "./index";
type TMissingPermission = {
action: string;
subject: string;
conditions?: MongoQuery;
};
type TPermissionConditionShape = {
[PermissionConditionOperators.$EQ]: string;
[PermissionConditionOperators.$NEQ]: string;
[PermissionConditionOperators.$GLOB]: string;
[PermissionConditionOperators.$IN]: string[];
};
const getPermissionSetID = (action: string, subject: string) => `${action}:${subject}`;
const invertTheOperation = (shouldInvert: boolean, operation: boolean) => (shouldInvert ? !operation : operation);
const formatConditionOperator = (condition: TPermissionConditionShape | string) => {
return (
typeof condition === "string" ? { [PermissionConditionOperators.$EQ]: condition } : condition
) as TPermissionConditionShape;
};
const isOperatorsASubset = (parentSet: TPermissionConditionShape, subset: TPermissionConditionShape) => {
// we compute each operator against each other in left hand side and right hand side
if (subset[PermissionConditionOperators.$EQ] || subset[PermissionConditionOperators.$NEQ]) {
const subsetOperatorValue = subset[PermissionConditionOperators.$EQ] || subset[PermissionConditionOperators.$NEQ];
const isInverted = !subset[PermissionConditionOperators.$EQ];
if (
parentSet[PermissionConditionOperators.$EQ] &&
invertTheOperation(isInverted, parentSet[PermissionConditionOperators.$EQ] !== subsetOperatorValue)
) {
return false;
}
if (
parentSet[PermissionConditionOperators.$NEQ] &&
invertTheOperation(isInverted, parentSet[PermissionConditionOperators.$NEQ] === subsetOperatorValue)
) {
return false;
}
if (
parentSet[PermissionConditionOperators.$IN] &&
invertTheOperation(isInverted, !parentSet[PermissionConditionOperators.$IN].includes(subsetOperatorValue))
) {
return false;
}
// ne and glob cannot match each other
if (parentSet[PermissionConditionOperators.$GLOB] && isInverted) {
return false;
}
if (
parentSet[PermissionConditionOperators.$GLOB] &&
!picomatch.isMatch(subsetOperatorValue, parentSet[PermissionConditionOperators.$GLOB], { strictSlashes: false })
) {
return false;
}
}
if (subset[PermissionConditionOperators.$IN]) {
const subsetOperatorValue = subset[PermissionConditionOperators.$IN];
if (
parentSet[PermissionConditionOperators.$EQ] &&
(subsetOperatorValue.length !== 1 || subsetOperatorValue[0] !== parentSet[PermissionConditionOperators.$EQ])
) {
return false;
}
if (
parentSet[PermissionConditionOperators.$NEQ] &&
subsetOperatorValue.includes(parentSet[PermissionConditionOperators.$NEQ])
) {
return false;
}
if (
parentSet[PermissionConditionOperators.$IN] &&
!subsetOperatorValue.every((el) => parentSet[PermissionConditionOperators.$IN].includes(el))
) {
return false;
}
if (
parentSet[PermissionConditionOperators.$GLOB] &&
!subsetOperatorValue.every((el) =>
picomatch.isMatch(el, parentSet[PermissionConditionOperators.$GLOB], {
strictSlashes: false
})
)
) {
return false;
}
}
if (subset[PermissionConditionOperators.$GLOB]) {
const subsetOperatorValue = subset[PermissionConditionOperators.$GLOB];
const { isGlob } = picomatch.scan(subsetOperatorValue);
// if it's glob, all other fixed operators would make this superset because glob is powerful. like eq
// example: $in [dev, prod] => glob: dev** could mean anything starting with dev: thus is bigger
if (
isGlob &&
Object.keys(parentSet).some(
(el) => el !== PermissionConditionOperators.$GLOB && el !== PermissionConditionOperators.$NEQ
)
) {
return false;
}
if (
parentSet[PermissionConditionOperators.$EQ] &&
parentSet[PermissionConditionOperators.$EQ] !== subsetOperatorValue
) {
return false;
}
if (
parentSet[PermissionConditionOperators.$NEQ] &&
picomatch.isMatch(parentSet[PermissionConditionOperators.$NEQ], subsetOperatorValue, {
strictSlashes: false
})
) {
return false;
}
// if parent set is IN, glob cannot be used for children - It's a bigger scope
if (
parentSet[PermissionConditionOperators.$IN] &&
!parentSet[PermissionConditionOperators.$IN].includes(subsetOperatorValue)
) {
return false;
}
if (
parentSet[PermissionConditionOperators.$GLOB] &&
!picomatch.isMatch(subsetOperatorValue, parentSet[PermissionConditionOperators.$GLOB], {
strictSlashes: false
})
) {
return false;
}
}
return true;
};
const isSubsetForSamePermissionSubjectAction = (
parentSetRules: ReturnType<MongoAbility["possibleRulesFor"]>,
subsetRules: ReturnType<MongoAbility["possibleRulesFor"]>,
appendToMissingPermission: (condition?: MongoQuery) => void
) => {
const isMissingConditionInParent = parentSetRules.every((el) => !el.conditions);
if (isMissingConditionInParent) return true;
// all subset rules must pass in comparison to parent rul
return subsetRules.every((subsetRule) => {
const subsetRuleConditions = subsetRule.conditions as Record<string, TPermissionConditionShape | string>;
// compare subset rule with all parent rules
const isSubsetOfNonInvertedParentSet = parentSetRules
.filter((el) => !el.inverted)
.some((parentSetRule) => {
// get conditions and iterate
const parentSetRuleConditions = parentSetRule?.conditions as Record<string, TPermissionConditionShape | string>;
if (!parentSetRuleConditions) return true;
return Object.keys(parentSetRuleConditions).every((parentConditionField) => {
// if parent condition is missing then it's never a subset
if (!subsetRuleConditions?.[parentConditionField]) return false;
// standardize the conditions plain string operator => $eq function
const parentRuleConditionOperators = formatConditionOperator(parentSetRuleConditions[parentConditionField]);
const selectedSubsetRuleCondition = subsetRuleConditions?.[parentConditionField];
const subsetRuleConditionOperators = formatConditionOperator(selectedSubsetRuleCondition);
return isOperatorsASubset(parentRuleConditionOperators, subsetRuleConditionOperators);
});
});
const invertedParentSetRules = parentSetRules.filter((el) => el.inverted);
const isNotSubsetOfInvertedParentSet = invertedParentSetRules.length
? !invertedParentSetRules.some((parentSetRule) => {
// get conditions and iterate
const parentSetRuleConditions = parentSetRule?.conditions as Record<
string,
TPermissionConditionShape | string
>;
if (!parentSetRuleConditions) return true;
return Object.keys(parentSetRuleConditions).every((parentConditionField) => {
// if parent condition is missing then it's never a subset
if (!subsetRuleConditions?.[parentConditionField]) return false;
// standardize the conditions plain string operator => $eq function
const parentRuleConditionOperators = formatConditionOperator(parentSetRuleConditions[parentConditionField]);
const selectedSubsetRuleCondition = subsetRuleConditions?.[parentConditionField];
const subsetRuleConditionOperators = formatConditionOperator(selectedSubsetRuleCondition);
return isOperatorsASubset(parentRuleConditionOperators, subsetRuleConditionOperators);
});
})
: true;
const isSubset = isSubsetOfNonInvertedParentSet && isNotSubsetOfInvertedParentSet;
if (!isSubset) {
appendToMissingPermission(subsetRule.conditions);
}
return isSubset;
});
};
export const validatePermissionBoundary = (parentSetPermissions: MongoAbility, subsetPermissions: MongoAbility) => {
const checkedPermissionRules = new Set<string>();
const missingPermissions: TMissingPermission[] = [];
subsetPermissions.rules.forEach((subsetPermissionRules) => {
const subsetPermissionSubject = subsetPermissionRules.subject.toString();
let subsetPermissionActions: string[] = [];
// actions can be string or string[]
if (typeof subsetPermissionRules.action === "string") {
subsetPermissionActions.push(subsetPermissionRules.action);
} else {
subsetPermissionRules.action.forEach((subsetPermissionAction) => {
subsetPermissionActions.push(subsetPermissionAction);
});
}
// if action is already processed ignore
subsetPermissionActions = subsetPermissionActions.filter(
(el) => !checkedPermissionRules.has(getPermissionSetID(el, subsetPermissionSubject))
);
if (!subsetPermissionActions.length) return;
subsetPermissionActions.forEach((subsetPermissionAction) => {
const parentSetRulesOfSubset = parentSetPermissions.possibleRulesFor(
subsetPermissionAction,
subsetPermissionSubject
);
const nonInveretedOnes = parentSetRulesOfSubset.filter((el) => !el.inverted);
if (!nonInveretedOnes.length) {
missingPermissions.push({ action: subsetPermissionAction, subject: subsetPermissionSubject });
return;
}
const subsetRules = subsetPermissions.possibleRulesFor(subsetPermissionAction, subsetPermissionSubject);
isSubsetForSamePermissionSubjectAction(parentSetRulesOfSubset, subsetRules, (conditions) => {
missingPermissions.push({ action: subsetPermissionAction, subject: subsetPermissionSubject, conditions });
});
});
subsetPermissionActions.forEach((el) =>
checkedPermissionRules.add(getPermissionSetID(el, subsetPermissionSubject))
);
});
if (missingPermissions.length) {
return { isValid: false as const, missingPermissions };
}
return { isValid: true };
};

@ -1,5 +1,5 @@
/* eslint-disable @typescript-eslint/no-unsafe-assignment */
import { buildMongoQueryMatcher, MongoAbility } from "@casl/ability";
import { buildMongoQueryMatcher } from "@casl/ability";
import { FieldCondition, FieldInstruction, JsInterpreter } from "@ucast/mongo2js";
import picomatch from "picomatch";
@ -20,45 +20,8 @@ const glob: JsInterpreter<FieldCondition<string>> = (node, object, context) => {
export const conditionsMatcher = buildMongoQueryMatcher({ $glob }, { glob });
/**
* Extracts and formats permissions from a CASL Ability object or a raw permission set.
*/
const extractPermissions = (ability: MongoAbility) => {
const permissions: string[] = [];
ability.rules.forEach((permission) => {
if (typeof permission.action === "string") {
permissions.push(`${permission.action}_${permission.subject as string}`);
} else {
permission.action.forEach((permissionAction) => {
permissions.push(`${permissionAction}_${permission.subject as string}`);
});
}
});
return permissions;
};
/**
* Compares two sets of permissions to determine if the first set is at least as privileged as the second set.
* The function checks if all permissions in the second set are contained within the first set and if the first set has equal or more permissions.
*
*/
export const isAtLeastAsPrivileged = (permissions1: MongoAbility, permissions2: MongoAbility) => {
const set1 = new Set(extractPermissions(permissions1));
const set2 = new Set(extractPermissions(permissions2));
for (const perm of set2) {
if (!set1.has(perm)) {
return false;
}
}
return set1.size >= set2.size;
};
export enum PermissionConditionOperators {
$IN = "$in",
$ALL = "$all",
$REGEX = "$regex",
$EQ = "$eq",
$NEQ = "$ne",
$GLOB = "$glob"

@ -1,4 +1,5 @@
/* eslint-disable max-classes-per-file */
export class DatabaseError extends Error {
name: string;
@ -52,10 +53,18 @@ export class ForbiddenRequestError extends Error {
error: unknown;
constructor({ name, error, message }: { message?: string; name?: string; error?: unknown } = {}) {
details?: unknown;
constructor({
name,
error,
message,
details
}: { message?: string; name?: string; error?: unknown; details?: unknown } = {}) {
super(message ?? "You are not allowed to access this resource");
this.name = name || "ForbiddenError";
this.error = error;
this.details = details;
}
}

@ -96,6 +96,7 @@ export const pingGatewayAndVerify = async ({
error: err as Error
});
});
for (let attempt = 1; attempt <= maxRetries; attempt += 1) {
try {
const stream = quicClient.connection.newStream("bidi");
@ -108,17 +109,13 @@ export const pingGatewayAndVerify = async ({
const { value, done } = await reader.read();
if (done) {
throw new BadRequestError({
message: "Gateway closed before receiving PONG"
});
throw new Error("Gateway closed before receiving PONG");
}
const response = Buffer.from(value).toString();
if (response !== "PONG\n" && response !== "PONG") {
throw new BadRequestError({
message: `Failed to Ping. Unexpected response: ${response}`
});
throw new Error(`Failed to Ping. Unexpected response: ${response}`);
}
reader.releaseLock();
@ -146,6 +143,7 @@ interface TProxyServer {
server: net.Server;
port: number;
cleanup: () => Promise<void>;
getProxyError: () => string;
}
const setupProxyServer = async ({
@ -170,6 +168,7 @@ const setupProxyServer = async ({
error: err as Error
});
});
const proxyErrorMsg = [""];
return new Promise((resolve, reject) => {
const server = net.createServer();
@ -185,31 +184,33 @@ const setupProxyServer = async ({
const forwardWriter = stream.writable.getWriter();
await forwardWriter.write(Buffer.from(`FORWARD-TCP ${targetHost}:${targetPort}\n`));
forwardWriter.releaseLock();
/* eslint-disable @typescript-eslint/no-misused-promises */
// Set up bidirectional copy
const setupCopy = async () => {
const setupCopy = () => {
// Client to QUIC
// eslint-disable-next-line
(async () => {
try {
const writer = stream.writable.getWriter();
const writer = stream.writable.getWriter();
// Create a handler for client data
clientConn.on("data", async (chunk) => {
await writer.write(chunk);
// Create a handler for client data
clientConn.on("data", (chunk) => {
writer.write(chunk).catch((err) => {
proxyErrorMsg.push((err as Error)?.message);
});
});
// Handle client connection close
clientConn.on("end", async () => {
await writer.close();
// Handle client connection close
clientConn.on("end", () => {
writer.close().catch((err) => {
logger.error(err);
});
});
clientConn.on("error", async (err) => {
await writer.abort(err);
clientConn.on("error", (clientConnErr) => {
writer.abort(clientConnErr?.message).catch((err) => {
proxyErrorMsg.push((err as Error)?.message);
});
} catch (err) {
clientConn.destroy();
}
});
})();
// QUIC to Client
@ -238,15 +239,18 @@ const setupProxyServer = async ({
}
}
} catch (err) {
proxyErrorMsg.push((err as Error)?.message);
clientConn.destroy();
}
})();
};
await setupCopy();
//
setupCopy();
// Handle connection closure
clientConn.on("close", async () => {
await stream.destroy();
clientConn.on("close", () => {
stream.destroy().catch((err) => {
proxyErrorMsg.push((err as Error)?.message);
});
});
const cleanup = async () => {
@ -254,13 +258,18 @@ const setupProxyServer = async ({
await stream.destroy();
};
clientConn.on("error", (err) => {
logger.error(err, "Client socket error");
void cleanup();
reject(err);
clientConn.on("error", (clientConnErr) => {
logger.error(clientConnErr, "Client socket error");
cleanup().catch((err) => {
logger.error(err, "Client conn cleanup");
});
});
clientConn.on("end", cleanup);
clientConn.on("end", () => {
cleanup().catch((err) => {
logger.error(err, "Client conn end");
});
});
} catch (err) {
logger.error(err, "Failed to establish target connection:");
clientConn.end();
@ -272,12 +281,12 @@ const setupProxyServer = async ({
reject(err);
});
server.on("close", async () => {
await quicClient?.destroy();
server.on("close", () => {
quicClient?.destroy().catch((err) => {
logger.error(err, "Failed to destroy quic client");
});
});
/* eslint-enable */
server.listen(0, () => {
const address = server.address();
if (!address || typeof address === "string") {
@ -293,7 +302,8 @@ const setupProxyServer = async ({
cleanup: async () => {
server.close();
await quicClient?.destroy();
}
},
getProxyError: () => proxyErrorMsg.join(",")
});
});
});
@ -316,7 +326,7 @@ export const withGatewayProxy = async (
const { relayHost, relayPort, targetHost, targetPort, tlsOptions, identityId, orgId } = options;
// Setup the proxy server
const { port, cleanup } = await setupProxyServer({
const { port, cleanup, getProxyError } = await setupProxyServer({
targetHost,
targetPort,
relayPort,
@ -330,8 +340,12 @@ export const withGatewayProxy = async (
// Execute the callback with the allocated port
await callback(port);
} catch (err) {
logger.error(err, "Failed to proxy");
throw new BadRequestError({ message: (err as Error)?.message });
const proxyErrorMessage = getProxyError();
if (proxyErrorMessage) {
logger.error(new Error(proxyErrorMessage), "Failed to proxy");
}
logger.error(err, "Failed to do gateway");
throw new BadRequestError({ message: proxyErrorMessage || (err as Error)?.message });
} finally {
// Ensure cleanup happens regardless of success or failure
await cleanup();

@ -1,6 +1,6 @@
import crypto from "node:crypto";
const TURN_TOKEN_TTL = 60 * 60 * 1000; // 24 hours in milliseconds
const TURN_TOKEN_TTL = 24 * 60 * 60 * 1000; // 24 hours in milliseconds
export const getTurnCredentials = (id: string, authSecret: string, ttl = TURN_TOKEN_TTL) => {
const timestamp = Math.floor((Date.now() + ttl) / 1000);
const username = `${timestamp}:${id}`;

@ -83,6 +83,14 @@ const run = async () => {
process.exit(0);
});
process.on("uncaughtException", (error) => {
logger.error(error, "CRITICAL ERROR: Uncaught Exception");
});
process.on("unhandledRejection", (error) => {
logger.error(error, "CRITICAL ERROR: Unhandled Promise Rejection");
});
await server.listen({
port: envConfig.PORT,
host: envConfig.HOST,

@ -21,6 +21,7 @@ import {
TQueueSecretSyncSyncSecretsByIdDTO,
TQueueSendSecretSyncActionFailedNotificationsDTO
} from "@app/services/secret-sync/secret-sync-types";
import { TWebhookPayloads } from "@app/services/webhook/webhook-types";
export enum QueueName {
SecretRotation = "secret-rotation",
@ -107,7 +108,7 @@ export type TQueueJobTypes = {
};
[QueueName.SecretWebhook]: {
name: QueueJobs.SecWebhook;
payload: { projectId: string; environment: string; secretPath: string; depth?: number };
payload: TWebhookPayloads;
};
[QueueName.AccessTokenStatusUpdate]:

@ -21,3 +21,10 @@ export const slugSchema = ({ min = 1, max = 32, field = "Slug" }: SlugSchemaInpu
message: `${field} field can only contain lowercase letters, numbers, and hyphens`
});
};
export const GenericResourceNameSchema = z
.string()
.trim()
.min(1, { message: "Name must be at least 1 character" })
.max(64, { message: "Name must be 64 or fewer characters" })
.regex(/^[a-zA-Z0-9\-_\s]+$/, "Name can only contain alphanumeric characters, dashes, underscores, and spaces");

@ -122,7 +122,8 @@ export const fastifyErrHandler = fastifyPlugin(async (server: FastifyZodProvider
reqId: req.id,
statusCode: HttpStatusCodes.Forbidden,
message: error.message,
error: error.name
error: error.name,
details: error?.details
});
} else if (error instanceof RateLimitError) {
void res.status(HttpStatusCodes.TooManyRequests).send({

@ -635,6 +635,7 @@ export const registerRoutes = async (
});
const superAdminService = superAdminServiceFactory({
userDAL,
identityDAL,
userAliasDAL,
authService: loginService,
serverCfgDAL: superAdminDAL,

@ -7,6 +7,7 @@ import {
ProjectRolesSchema,
ProjectsSchema,
SecretApprovalPoliciesSchema,
SecretTagsSchema,
UsersSchema
} from "@app/db/schemas";
import { ProjectPermissionActions, ProjectPermissionSub } from "@app/ee/services/permission/project-permission";
@ -241,3 +242,11 @@ export const SanitizedProjectSchema = ProjectsSchema.pick({
kmsCertificateKeyId: true,
auditLogsRetentionDays: true
});
export const SanitizedTagSchema = SecretTagsSchema.pick({
id: true,
slug: true,
color: true
}).extend({
name: z.string()
});

@ -1,7 +1,7 @@
import DOMPurify from "isomorphic-dompurify";
import { z } from "zod";
import { OrganizationsSchema, SuperAdminSchema, UsersSchema } from "@app/db/schemas";
import { IdentitiesSchema, OrganizationsSchema, SuperAdminSchema, UsersSchema } from "@app/db/schemas";
import { getConfig } from "@app/lib/config/env";
import { BadRequestError } from "@app/lib/errors";
import { readLimit, writeLimit } from "@app/server/config/rateLimiter";
@ -118,7 +118,12 @@ export const registerAdminRouter = async (server: FastifyZodProvider) => {
querystring: z.object({
searchTerm: z.string().default(""),
offset: z.coerce.number().default(0),
limit: z.coerce.number().max(100).default(20)
limit: z.coerce.number().max(100).default(20),
// TODO: remove this once z.coerce.boolean() is supported
adminsOnly: z
.string()
.transform((val) => val === "true")
.default("false")
}),
response: {
200: z.object({
@ -149,6 +154,43 @@ export const registerAdminRouter = async (server: FastifyZodProvider) => {
}
});
server.route({
method: "GET",
url: "/identity-management/identities",
config: {
rateLimit: readLimit
},
schema: {
querystring: z.object({
searchTerm: z.string().default(""),
offset: z.coerce.number().default(0),
limit: z.coerce.number().max(100).default(20)
}),
response: {
200: z.object({
identities: IdentitiesSchema.pick({
name: true,
id: true
}).array()
})
}
},
onRequest: (req, res, done) => {
verifyAuth([AuthMode.JWT])(req, res, () => {
verifySuperAdmin(req, res, done);
});
},
handler: async (req) => {
const identities = await server.services.superAdmin.getIdentities({
...req.query
});
return {
identities
};
}
});
server.route({
method: "GET",
url: "/integrations/slack/config",

@ -1,10 +1,11 @@
import { ForbiddenError, subject } from "@casl/ability";
import { z } from "zod";
import { ActionProjectType, SecretFoldersSchema, SecretImportsSchema, SecretTagsSchema } from "@app/db/schemas";
import { ActionProjectType, SecretFoldersSchema, SecretImportsSchema } from "@app/db/schemas";
import { EventType, UserAgentType } from "@app/ee/services/audit-log/audit-log-types";
import {
ProjectPermissionDynamicSecretActions,
ProjectPermissionSecretActions,
ProjectPermissionSub
} from "@app/ee/services/permission/project-permission";
import { DASHBOARD } from "@app/lib/api-docs";
@ -15,7 +16,7 @@ import { secretsLimit } from "@app/server/config/rateLimiter";
import { getTelemetryDistinctId } from "@app/server/lib/telemetry";
import { getUserAgentType } from "@app/server/plugins/audit-log";
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
import { SanitizedDynamicSecretSchema, secretRawSchema } from "@app/server/routes/sanitizedSchemas";
import { SanitizedDynamicSecretSchema, SanitizedTagSchema, secretRawSchema } from "@app/server/routes/sanitizedSchemas";
import { AuthMode } from "@app/services/auth/auth-type";
import { ResourceMetadataSchema } from "@app/services/resource-metadata/resource-metadata-schema";
import { SecretsOrderBy } from "@app/services/secret/secret-types";
@ -116,16 +117,10 @@ export const registerDashboardRouter = async (server: FastifyZodProvider) => {
dynamicSecrets: SanitizedDynamicSecretSchema.extend({ environment: z.string() }).array().optional(),
secrets: secretRawSchema
.extend({
secretValueHidden: z.boolean(),
secretPath: z.string().optional(),
secretMetadata: ResourceMetadataSchema.optional(),
tags: SecretTagsSchema.pick({
id: true,
slug: true,
color: true
})
.extend({ name: z.string() })
.array()
.optional()
tags: SanitizedTagSchema.array().optional()
})
.array()
.optional(),
@ -294,6 +289,7 @@ export const registerDashboardRouter = async (server: FastifyZodProvider) => {
if (remainingLimit > 0 && totalSecretCount > adjustedOffset) {
secrets = await server.services.secret.getSecretsRawMultiEnv({
viewSecretValue: true,
actorId: req.permission.id,
actor: req.permission.type,
actorOrgId: req.permission.orgId,
@ -393,6 +389,7 @@ export const registerDashboardRouter = async (server: FastifyZodProvider) => {
.optional(),
search: z.string().trim().describe(DASHBOARD.SECRET_DETAILS_LIST.search).optional(),
tags: z.string().trim().transform(decodeURIComponent).describe(DASHBOARD.SECRET_DETAILS_LIST.tags).optional(),
viewSecretValue: booleanSchema.default(true),
includeSecrets: booleanSchema.describe(DASHBOARD.SECRET_DETAILS_LIST.includeSecrets),
includeFolders: booleanSchema.describe(DASHBOARD.SECRET_DETAILS_LIST.includeFolders),
includeDynamicSecrets: booleanSchema.describe(DASHBOARD.SECRET_DETAILS_LIST.includeDynamicSecrets),
@ -410,16 +407,10 @@ export const registerDashboardRouter = async (server: FastifyZodProvider) => {
dynamicSecrets: SanitizedDynamicSecretSchema.array().optional(),
secrets: secretRawSchema
.extend({
secretValueHidden: z.boolean(),
secretPath: z.string().optional(),
secretMetadata: ResourceMetadataSchema.optional(),
tags: SecretTagsSchema.pick({
id: true,
slug: true,
color: true
})
.extend({ name: z.string() })
.array()
.optional()
tags: SanitizedTagSchema.array().optional()
})
.array()
.optional(),
@ -601,23 +592,25 @@ export const registerDashboardRouter = async (server: FastifyZodProvider) => {
});
if (remainingLimit > 0 && totalSecretCount > adjustedOffset) {
const secretsRaw = await server.services.secret.getSecretsRaw({
actorId: req.permission.id,
actor: req.permission.type,
actorOrgId: req.permission.orgId,
environment,
actorAuthMethod: req.permission.authMethod,
projectId,
path: secretPath,
orderBy,
orderDirection,
search,
limit: remainingLimit,
offset: adjustedOffset,
tagSlugs: tags
});
secrets = secretsRaw.secrets;
secrets = (
await server.services.secret.getSecretsRaw({
actorId: req.permission.id,
actor: req.permission.type,
viewSecretValue: req.query.viewSecretValue,
throwOnMissingReadValuePermission: false,
actorOrgId: req.permission.orgId,
environment,
actorAuthMethod: req.permission.authMethod,
projectId,
path: secretPath,
orderBy,
orderDirection,
search,
limit: remainingLimit,
offset: adjustedOffset,
tagSlugs: tags
})
).secrets;
await server.services.auditLog.createAuditLog({
projectId,
@ -696,16 +689,10 @@ export const registerDashboardRouter = async (server: FastifyZodProvider) => {
.optional(),
secrets: secretRawSchema
.extend({
secretValueHidden: z.boolean(),
secretPath: z.string().optional(),
secretMetadata: ResourceMetadataSchema.optional(),
tags: SecretTagsSchema.pick({
id: true,
slug: true,
color: true
})
.extend({ name: z.string() })
.array()
.optional()
tags: SanitizedTagSchema.array().optional()
})
.array()
.optional()
@ -749,6 +736,7 @@ export const registerDashboardRouter = async (server: FastifyZodProvider) => {
const secrets = await server.services.secret.getSecretsRawByFolderMappings(
{
filterByAction: ProjectPermissionSecretActions.DescribeSecret,
projectId,
folderMappings,
filters: {
@ -846,6 +834,52 @@ export const registerDashboardRouter = async (server: FastifyZodProvider) => {
}
});
server.route({
method: "GET",
url: "/accessible-secrets",
config: {
rateLimit: secretsLimit
},
schema: {
querystring: z.object({
projectId: z.string().trim(),
environment: z.string().trim(),
secretPath: z.string().trim().default("/").transform(removeTrailingSlash),
filterByAction: z
.enum([ProjectPermissionSecretActions.DescribeSecret, ProjectPermissionSecretActions.ReadValue])
.default(ProjectPermissionSecretActions.ReadValue)
}),
response: {
200: z.object({
secrets: secretRawSchema
.extend({
secretPath: z.string().optional(),
secretValueHidden: z.boolean()
})
.array()
.optional()
})
}
},
onRequest: verifyAuth([AuthMode.JWT]),
handler: async (req) => {
const { projectId, environment, secretPath, filterByAction } = req.query;
const { secrets } = await server.services.secret.getAccessibleSecrets({
actorId: req.permission.id,
actor: req.permission.type,
actorAuthMethod: req.permission.authMethod,
actorOrgId: req.permission.orgId,
environment,
secretPath,
projectId,
filterByAction
});
return { secrets };
}
});
server.route({
method: "GET",
url: "/secrets-by-keys",
@ -862,22 +896,17 @@ export const registerDashboardRouter = async (server: FastifyZodProvider) => {
projectId: z.string().trim(),
environment: z.string().trim(),
secretPath: z.string().trim().default("/").transform(removeTrailingSlash),
keys: z.string().trim().transform(decodeURIComponent)
keys: z.string().trim().transform(decodeURIComponent),
viewSecretValue: booleanSchema.default(false)
}),
response: {
200: z.object({
secrets: secretRawSchema
.extend({
secretValueHidden: z.boolean(),
secretPath: z.string().optional(),
secretMetadata: ResourceMetadataSchema.optional(),
tags: SecretTagsSchema.pick({
id: true,
slug: true,
color: true
})
.extend({ name: z.string() })
.array()
.optional()
tags: SanitizedTagSchema.array().optional()
})
.array()
.optional()
@ -886,7 +915,7 @@ export const registerDashboardRouter = async (server: FastifyZodProvider) => {
},
onRequest: verifyAuth([AuthMode.JWT]),
handler: async (req) => {
const { secretPath, projectId, environment } = req.query;
const { secretPath, projectId, environment, viewSecretValue } = req.query;
const keys = req.query.keys?.split(",").filter((key) => Boolean(key.trim())) ?? [];
if (!keys.length) throw new BadRequestError({ message: "One or more keys required" });
@ -895,6 +924,7 @@ export const registerDashboardRouter = async (server: FastifyZodProvider) => {
actorId: req.permission.id,
actor: req.permission.type,
actorOrgId: req.permission.orgId,
viewSecretValue,
environment,
actorAuthMethod: req.permission.authMethod,
projectId,

@ -91,7 +91,6 @@ export const registerV1Routes = async (server: FastifyZodProvider) => {
await projectRouter.register(registerProjectMembershipRouter);
await projectRouter.register(registerSecretTagRouter);
},
{ prefix: "/workspace" }
);

@ -13,7 +13,7 @@ import { EventType, UserAgentType } from "@app/ee/services/audit-log/audit-log-t
import { AUDIT_LOGS, ORGANIZATIONS } from "@app/lib/api-docs";
import { getLastMidnightDateISO, removeTrailingSlash } from "@app/lib/fn";
import { readLimit, writeLimit } from "@app/server/config/rateLimiter";
import { slugSchema } from "@app/server/lib/schemas";
import { GenericResourceNameSchema, slugSchema } from "@app/server/lib/schemas";
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
import { ActorType, AuthMode, MfaMethod } from "@app/services/auth/auth-type";
import { sanitizedOrganizationSchema } from "@app/services/org/org-schema";
@ -251,7 +251,7 @@ export const registerOrgRouter = async (server: FastifyZodProvider) => {
schema: {
params: z.object({ organizationId: z.string().trim() }),
body: z.object({
name: z.string().trim().max(64, { message: "Name must be 64 or fewer characters" }).optional(),
name: GenericResourceNameSchema.optional(),
slug: slugSchema({ max: 64 }).optional(),
authEnforced: z.boolean().optional(),
scimEnabled: z.boolean().optional(),

@ -6,6 +6,7 @@ import { authRateLimit } from "@app/server/config/rateLimiter";
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
import { validateSignUpAuthorization } from "@app/services/auth/auth-fns";
import { AuthMode } from "@app/services/auth/auth-type";
import { UserEncryption } from "@app/services/user/user-types";
export const registerPasswordRouter = async (server: FastifyZodProvider) => {
server.route({
@ -113,20 +114,16 @@ export const registerPasswordRouter = async (server: FastifyZodProvider) => {
}),
response: {
200: z.object({
message: z.string(),
user: UsersSchema,
token: z.string()
token: z.string(),
userEncryptionVersion: z.nativeEnum(UserEncryption)
})
}
},
handler: async (req) => {
const { token, user } = await server.services.password.verifyPasswordResetEmail(req.body.email, req.body.code);
const passwordReset = await server.services.password.verifyPasswordResetEmail(req.body.email, req.body.code);
return {
message: "Successfully verified email",
user,
token
};
return passwordReset;
}
});

@ -2,10 +2,12 @@ import { z } from "zod";
import {
IntegrationsSchema,
ProjectEnvironmentsSchema,
ProjectMembershipsSchema,
ProjectRolesSchema,
ProjectSlackConfigsSchema,
ProjectType,
SecretFoldersSchema,
UserEncryptionKeysSchema,
UsersSchema
} from "@app/db/schemas";
@ -307,7 +309,17 @@ export const registerProjectRouter = async (server: FastifyZodProvider) => {
.max(256, { message: "Description must be 256 or fewer characters" })
.optional()
.describe(PROJECTS.UPDATE.projectDescription),
autoCapitalization: z.boolean().optional().describe(PROJECTS.UPDATE.autoCapitalization)
autoCapitalization: z.boolean().optional().describe(PROJECTS.UPDATE.autoCapitalization),
slug: z
.string()
.trim()
.regex(
/^[a-z0-9]+(?:[_-][a-z0-9]+)*$/,
"Project slug can only contain lowercase letters and numbers, with optional single hyphens (-) or underscores (_) between words. Cannot start or end with a hyphen or underscore."
)
.max(64, { message: "Slug must be 64 characters or fewer" })
.optional()
.describe(PROJECTS.UPDATE.slug)
}),
response: {
200: z.object({
@ -325,7 +337,8 @@ export const registerProjectRouter = async (server: FastifyZodProvider) => {
update: {
name: req.body.name,
description: req.body.description,
autoCapitalization: req.body.autoCapitalization
autoCapitalization: req.body.autoCapitalization,
slug: req.body.slug
},
actorAuthMethod: req.permission.authMethod,
actorId: req.permission.id,
@ -664,4 +677,31 @@ export const registerProjectRouter = async (server: FastifyZodProvider) => {
return slackConfig;
}
});
server.route({
method: "GET",
url: "/:workspaceId/environment-folder-tree",
config: {
rateLimit: readLimit
},
schema: {
params: z.object({
workspaceId: z.string().trim()
}),
response: {
200: z.record(
ProjectEnvironmentsSchema.extend({ folders: SecretFoldersSchema.extend({ path: z.string() }).array() })
)
}
},
onRequest: verifyAuth([AuthMode.JWT]),
handler: async (req) => {
const environmentsFolders = await server.services.folder.getProjectEnvironmentsFolders(
req.params.workspaceId,
req.permission
);
return environmentsFolders;
}
});
};

@ -3,6 +3,7 @@ import { registerIdentityOrgRouter } from "./identity-org-router";
import { registerIdentityProjectRouter } from "./identity-project-router";
import { registerMfaRouter } from "./mfa-router";
import { registerOrgRouter } from "./organization-router";
import { registerPasswordRouter } from "./password-router";
import { registerProjectMembershipRouter } from "./project-membership-router";
import { registerProjectRouter } from "./project-router";
import { registerServiceTokenRouter } from "./service-token-router";
@ -12,6 +13,7 @@ export const registerV2Routes = async (server: FastifyZodProvider) => {
await server.register(registerMfaRouter, { prefix: "/auth" });
await server.register(registerUserRouter, { prefix: "/users" });
await server.register(registerServiceTokenRouter, { prefix: "/service-token" });
await server.register(registerPasswordRouter, { prefix: "/password" });
await server.register(
async (orgRouter) => {
await orgRouter.register(registerOrgRouter);

@ -12,6 +12,7 @@ import {
import { ORGANIZATIONS } from "@app/lib/api-docs";
import { getConfig } from "@app/lib/config/env";
import { readLimit, writeLimit } from "@app/server/config/rateLimiter";
import { GenericResourceNameSchema } from "@app/server/lib/schemas";
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
import { ActorType, AuthMode } from "@app/services/auth/auth-type";
@ -330,7 +331,7 @@ export const registerOrgRouter = async (server: FastifyZodProvider) => {
},
schema: {
body: z.object({
name: z.string().trim()
name: GenericResourceNameSchema
}),
response: {
200: z.object({

@ -0,0 +1,53 @@
import { z } from "zod";
import { authRateLimit } from "@app/server/config/rateLimiter";
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
import { validatePasswordResetAuthorization } from "@app/services/auth/auth-fns";
import { ResetPasswordV2Type } from "@app/services/auth/auth-password-type";
import { AuthMode } from "@app/services/auth/auth-type";
export const registerPasswordRouter = async (server: FastifyZodProvider) => {
server.route({
method: "POST",
url: "/password-reset",
config: {
rateLimit: authRateLimit
},
schema: {
body: z.object({
newPassword: z.string().trim()
})
},
handler: async (req) => {
const token = validatePasswordResetAuthorization(req.headers.authorization);
await server.services.password.resetPasswordV2({
type: ResetPasswordV2Type.Recovery,
newPassword: req.body.newPassword,
userId: token.userId
});
}
});
server.route({
method: "POST",
url: "/user/password-reset",
schema: {
body: z.object({
oldPassword: z.string().trim(),
newPassword: z.string().trim()
})
},
config: {
rateLimit: authRateLimit
},
onRequest: verifyAuth([AuthMode.JWT], { requireOrg: false }),
handler: async (req) => {
await server.services.password.resetPasswordV2({
type: ResetPasswordV2Type.LoggedInReset,
userId: req.permission.id,
newPassword: req.body.newPassword,
oldPassword: req.body.oldPassword
});
}
});
};

@ -1,13 +1,7 @@
import picomatch from "picomatch";
import { z } from "zod";
import {
SecretApprovalRequestsSchema,
SecretsSchema,
SecretTagsSchema,
SecretType,
ServiceTokenScopes
} from "@app/db/schemas";
import { SecretApprovalRequestsSchema, SecretsSchema, SecretType, ServiceTokenScopes } from "@app/db/schemas";
import { EventType, UserAgentType } from "@app/ee/services/audit-log/audit-log-types";
import { RAW_SECRETS, SECRETS } from "@app/lib/api-docs";
import { BadRequestError, NotFoundError } from "@app/lib/errors";
@ -23,7 +17,7 @@ import { SecretOperations, SecretProtectionType } from "@app/services/secret/sec
import { SecretUpdateMode } from "@app/services/secret-v2-bridge/secret-v2-bridge-types";
import { PostHogEventTypes } from "@app/services/telemetry/telemetry-types";
import { secretRawSchema } from "../sanitizedSchemas";
import { SanitizedTagSchema, secretRawSchema } from "../sanitizedSchemas";
const SecretReferenceNode = z.object({
key: z.string(),
@ -31,6 +25,14 @@ const SecretReferenceNode = z.object({
environment: z.string(),
secretPath: z.string()
});
const convertStringBoolean = (defaultValue: boolean = false) => {
return z
.enum(["true", "false"])
.default(defaultValue ? "true" : "false")
.transform((value) => value === "true");
};
type TSecretReferenceNode = z.infer<typeof SecretReferenceNode> & { children: TSecretReferenceNode[] };
const SecretReferenceNodeTree: z.ZodType<TSecretReferenceNode> = SecretReferenceNode.extend({
@ -75,17 +77,9 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => {
}),
response: {
200: z.object({
secret: SecretsSchema.omit({ secretBlindIndex: true }).merge(
z.object({
tags: SecretTagsSchema.pick({
id: true,
slug: true,
color: true
})
.extend({ name: z.string() })
.array()
})
)
secret: SecretsSchema.omit({ secretBlindIndex: true }).extend({
tags: SanitizedTagSchema.array()
})
})
}
},
@ -139,13 +133,7 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => {
response: {
200: z.object({
secret: SecretsSchema.omit({ secretBlindIndex: true }).extend({
tags: SecretTagsSchema.pick({
id: true,
slug: true,
color: true
})
.extend({ name: z.string() })
.array()
tags: SanitizedTagSchema.array()
})
})
}
@ -247,21 +235,10 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => {
workspaceSlug: z.string().trim().optional().describe(RAW_SECRETS.LIST.workspaceSlug),
environment: z.string().trim().optional().describe(RAW_SECRETS.LIST.environment),
secretPath: z.string().trim().default("/").transform(removeTrailingSlash).describe(RAW_SECRETS.LIST.secretPath),
expandSecretReferences: z
.enum(["true", "false"])
.default("false")
.transform((value) => value === "true")
.describe(RAW_SECRETS.LIST.expand),
recursive: z
.enum(["true", "false"])
.default("false")
.transform((value) => value === "true")
.describe(RAW_SECRETS.LIST.recursive),
include_imports: z
.enum(["true", "false"])
.default("false")
.transform((value) => value === "true")
.describe(RAW_SECRETS.LIST.includeImports),
viewSecretValue: convertStringBoolean(true).describe(RAW_SECRETS.LIST.viewSecretValue),
expandSecretReferences: convertStringBoolean().describe(RAW_SECRETS.LIST.expand),
recursive: convertStringBoolean().describe(RAW_SECRETS.LIST.recursive),
include_imports: convertStringBoolean().describe(RAW_SECRETS.LIST.includeImports),
tagSlugs: z
.string()
.describe(RAW_SECRETS.LIST.tagSlugs)
@ -274,15 +251,9 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => {
secrets: secretRawSchema
.extend({
secretPath: z.string().optional(),
secretValueHidden: z.boolean(),
secretMetadata: ResourceMetadataSchema.optional(),
tags: SecretTagsSchema.pick({
id: true,
slug: true,
color: true
})
.extend({ name: z.string() })
.array()
.optional()
tags: SanitizedTagSchema.array().optional()
})
.array(),
imports: z
@ -293,6 +264,7 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => {
secrets: secretRawSchema
.omit({ createdAt: true, updatedAt: true })
.extend({
secretValueHidden: z.boolean(),
secretMetadata: ResourceMetadataSchema.optional()
})
.array()
@ -342,6 +314,7 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => {
expandSecretReferences: req.query.expandSecretReferences,
actorAuthMethod: req.permission.authMethod,
projectId: workspaceId,
viewSecretValue: req.query.viewSecretValue,
path: secretPath,
metadataFilter: req.query.metadataFilter,
includeImports: req.query.include_imports,
@ -376,6 +349,7 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => {
}
});
}
return { secrets, imports };
}
});
@ -394,14 +368,7 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => {
200: z.object({
secret: secretRawSchema.extend({
secretPath: z.string(),
tags: SecretTagsSchema.pick({
id: true,
slug: true,
color: true
})
.extend({ name: z.string() })
.array()
.optional(),
tags: SanitizedTagSchema.array().optional(),
secretMetadata: ResourceMetadataSchema.optional()
})
})
@ -445,28 +412,15 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => {
secretPath: z.string().trim().default("/").transform(removeTrailingSlash).describe(RAW_SECRETS.GET.secretPath),
version: z.coerce.number().optional().describe(RAW_SECRETS.GET.version),
type: z.nativeEnum(SecretType).default(SecretType.Shared).describe(RAW_SECRETS.GET.type),
expandSecretReferences: z
.enum(["true", "false"])
.default("false")
.transform((value) => value === "true")
.describe(RAW_SECRETS.GET.expand),
include_imports: z
.enum(["true", "false"])
.default("false")
.transform((value) => value === "true")
.describe(RAW_SECRETS.GET.includeImports)
viewSecretValue: convertStringBoolean(true).describe(RAW_SECRETS.GET.viewSecretValue),
expandSecretReferences: convertStringBoolean().describe(RAW_SECRETS.GET.expand),
include_imports: convertStringBoolean().describe(RAW_SECRETS.GET.includeImports)
}),
response: {
200: z.object({
secret: secretRawSchema.extend({
tags: SecretTagsSchema.pick({
id: true,
slug: true,
color: true
})
.extend({ name: z.string() })
.array()
.optional(),
secretValueHidden: z.boolean(),
tags: SanitizedTagSchema.array().optional(),
secretMetadata: ResourceMetadataSchema.optional()
})
})
@ -498,6 +452,7 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => {
expandSecretReferences: req.query.expandSecretReferences,
environment,
projectId: workspaceId,
viewSecretValue: req.query.viewSecretValue,
projectSlug: workspaceSlug,
path: secretPath,
secretName: req.params.secretName,
@ -704,7 +659,9 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => {
response: {
200: z.union([
z.object({
secret: secretRawSchema
secret: secretRawSchema.extend({
secretValueHidden: z.boolean()
})
}),
z.object({ approval: SecretApprovalRequestsSchema }).describe("When secret protection policy is enabled")
])
@ -800,7 +757,9 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => {
response: {
200: z.union([
z.object({
secret: secretRawSchema
secret: secretRawSchema.extend({
secretValueHidden: z.boolean()
})
}),
z.object({ approval: SecretApprovalRequestsSchema }).describe("When secret protection policy is enabled")
])
@ -822,6 +781,7 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => {
if (secretOperation.type === SecretProtectionType.Approval) {
return { approval: secretOperation.approval };
}
const { secret } = secretOperation;
await server.services.auditLog.createAuditLog({
@ -884,13 +844,7 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => {
workspace: z.string(),
environment: z.string(),
secretPath: z.string().optional(),
tags: SecretTagsSchema.pick({
id: true,
slug: true,
color: true
})
.extend({ name: z.string() })
.array()
tags: SanitizedTagSchema.array()
})
.array(),
imports: z
@ -986,10 +940,7 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => {
secretPath: z.string().trim().default("/").transform(removeTrailingSlash),
type: z.nativeEnum(SecretType).default(SecretType.Shared),
version: z.coerce.number().optional(),
include_imports: z
.enum(["true", "false"])
.default("false")
.transform((value) => value === "true")
include_imports: convertStringBoolean()
}),
response: {
200: z.object({
@ -1260,6 +1211,7 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => {
z.object({
secret: SecretsSchema.omit({ secretBlindIndex: true }).merge(
z.object({
secretValueHidden: z.boolean(),
_id: z.string(),
workspace: z.string(),
environment: z.string()
@ -1429,13 +1381,12 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => {
response: {
200: z.union([
z.object({
secret: SecretsSchema.omit({ secretBlindIndex: true }).merge(
z.object({
_id: z.string(),
workspace: z.string(),
environment: z.string()
})
)
secret: SecretsSchema.omit({ secretBlindIndex: true }).extend({
_id: z.string(),
secretValueHidden: z.boolean(),
workspace: z.string(),
environment: z.string()
})
}),
z.object({ approval: SecretApprovalRequestsSchema }).describe("When secret protection policy is enabled")
])
@ -1747,7 +1698,7 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => {
response: {
200: z.union([
z.object({
secrets: SecretsSchema.omit({ secretBlindIndex: true }).array()
secrets: SecretsSchema.omit({ secretBlindIndex: true }).extend({ secretValueHidden: z.boolean() }).array()
}),
z.object({ approval: SecretApprovalRequestsSchema }).describe("When secret protection policy is enabled")
])
@ -1862,7 +1813,11 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => {
response: {
200: z.union([
z.object({
secrets: SecretsSchema.omit({ secretBlindIndex: true }).array()
secrets: SecretsSchema.omit({ secretBlindIndex: true })
.extend({
secretValueHidden: z.boolean()
})
.array()
}),
z.object({ approval: SecretApprovalRequestsSchema }).describe("When secret protection policy is enabled")
])
@ -2124,7 +2079,7 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => {
response: {
200: z.union([
z.object({
secrets: secretRawSchema.array()
secrets: secretRawSchema.extend({ secretValueHidden: z.boolean() }).array()
}),
z.object({ approval: SecretApprovalRequestsSchema }).describe("When secret protection policy is enabled")
])
@ -2246,7 +2201,11 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => {
response: {
200: z.union([
z.object({
secrets: secretRawSchema.array()
secrets: secretRawSchema
.extend({
secretValueHidden: z.boolean()
})
.array()
}),
z.object({ approval: SecretApprovalRequestsSchema }).describe("When secret protection policy is enabled")
])

@ -4,6 +4,7 @@ import { UsersSchema } from "@app/db/schemas";
import { getConfig } from "@app/lib/config/env";
import { ForbiddenRequestError } from "@app/lib/errors";
import { authRateLimit } from "@app/server/config/rateLimiter";
import { GenericResourceNameSchema } from "@app/server/lib/schemas";
import { getServerCfg } from "@app/services/super-admin/super-admin-service";
import { PostHogEventTypes } from "@app/services/telemetry/telemetry-types";
@ -100,7 +101,7 @@ export const registerSignupRouter = async (server: FastifyZodProvider) => {
encryptedPrivateKeyTag: z.string().trim(),
salt: z.string().trim(),
verifier: z.string().trim(),
organizationName: z.string().trim().min(1),
organizationName: GenericResourceNameSchema,
providerAuthToken: z.string().trim().optional().nullish(),
attributionSource: z.string().trim().optional(),
password: z.string()

@ -45,6 +45,36 @@ export const validateSignUpAuthorization = (token: string, userId: string, valid
if (decodedToken.userId !== userId) throw new UnauthorizedError();
};
export const validatePasswordResetAuthorization = (token?: string) => {
if (!token) throw new UnauthorizedError();
const appCfg = getConfig();
const [AUTH_TOKEN_TYPE, AUTH_TOKEN_VALUE] = <[string, string]>token?.split(" ", 2) ?? [null, null];
if (AUTH_TOKEN_TYPE === null) {
throw new UnauthorizedError({ message: "Missing Authorization Header in the request header." });
}
if (AUTH_TOKEN_TYPE.toLowerCase() !== "bearer") {
throw new UnauthorizedError({
message: `The provided authentication type '${AUTH_TOKEN_TYPE}' is not supported.`
});
}
if (AUTH_TOKEN_VALUE === null) {
throw new UnauthorizedError({
message: "Missing Authorization Body in the request header"
});
}
const decodedToken = jwt.verify(AUTH_TOKEN_VALUE, appCfg.AUTH_SECRET) as AuthModeProviderSignUpTokenPayload;
if (decodedToken.authTokenType !== AuthTokenType.SIGNUP_TOKEN) {
throw new UnauthorizedError({
message: `The provided authentication token type is not supported.`
});
}
return decodedToken;
};
export const enforceUserLockStatus = (isLocked: boolean, temporaryLockDateEnd?: Date | null) => {
if (isLocked) {
throw new ForbiddenRequestError({

@ -4,7 +4,10 @@ import jwt from "jsonwebtoken";
import { SecretEncryptionAlgo, SecretKeyEncoding } from "@app/db/schemas";
import { getConfig } from "@app/lib/config/env";
import { generateSrpServerKey, srpCheckClientProof } from "@app/lib/crypto";
import { infisicalSymmetricDecrypt, infisicalSymmetricEncypt } from "@app/lib/crypto/encryption";
import { generateUserSrpKeys } from "@app/lib/crypto/srp";
import { BadRequestError } from "@app/lib/errors";
import { logger } from "@app/lib/logger";
import { OrgServiceActor } from "@app/lib/types";
import { TAuthTokenServiceFactory } from "../auth-token/auth-token-service";
@ -12,10 +15,13 @@ import { TokenType } from "../auth-token/auth-token-types";
import { SmtpTemplates, TSmtpService } from "../smtp/smtp-service";
import { TTotpConfigDALFactory } from "../totp/totp-config-dal";
import { TUserDALFactory } from "../user/user-dal";
import { UserEncryption } from "../user/user-types";
import { TAuthDALFactory } from "./auth-dal";
import {
ResetPasswordV2Type,
TChangePasswordDTO,
TCreateBackupPrivateKeyDTO,
TResetPasswordV2DTO,
TResetPasswordViaBackupKeyDTO,
TSetupPasswordViaBackupKeyDTO
} from "./auth-password-type";
@ -114,26 +120,31 @@ export const authPaswordServiceFactory = ({
* Email password reset flow via email. Step 1 send email
*/
const sendPasswordResetEmail = async (email: string) => {
const user = await userDAL.findUserByUsername(email);
// ignore as user is not found to avoid an outside entity to identify infisical registered accounts
if (!user || (user && !user.isAccepted)) return;
const sendEmail = async () => {
const user = await userDAL.findUserByUsername(email);
const cfg = getConfig();
const token = await tokenService.createTokenForUser({
type: TokenType.TOKEN_EMAIL_PASSWORD_RESET,
userId: user.id
});
if (user && user.isAccepted) {
const cfg = getConfig();
const token = await tokenService.createTokenForUser({
type: TokenType.TOKEN_EMAIL_PASSWORD_RESET,
userId: user.id
});
await smtpService.sendMail({
template: SmtpTemplates.ResetPassword,
recipients: [email],
subjectLine: "Infisical password reset",
substitutions: {
email,
token,
callback_url: cfg.SITE_URL ? `${cfg.SITE_URL}/password-reset` : ""
await smtpService.sendMail({
template: SmtpTemplates.ResetPassword,
recipients: [email],
subjectLine: "Infisical password reset",
substitutions: {
email,
token,
callback_url: cfg.SITE_URL ? `${cfg.SITE_URL}/password-reset` : ""
}
});
}
});
};
// note(daniel): run in background to prevent timing attacks
void sendEmail().catch((err) => logger.error(err, "Failed to send password reset email"));
};
/*
@ -142,6 +153,11 @@ export const authPaswordServiceFactory = ({
const verifyPasswordResetEmail = async (email: string, code: string) => {
const cfg = getConfig();
const user = await userDAL.findUserByUsername(email);
const userEnc = await userDAL.findUserEncKeyByUserId(user.id);
if (!userEnc) throw new BadRequestError({ message: "Failed to find user encryption data" });
// ignore as user is not found to avoid an outside entity to identify infisical registered accounts
if (!user || (user && !user.isAccepted)) {
throw new Error("Failed email verification for pass reset");
@ -162,8 +178,91 @@ export const authPaswordServiceFactory = ({
{ expiresIn: cfg.JWT_SIGNUP_LIFETIME }
);
return { token, user };
return { token, user, userEncryptionVersion: userEnc.encryptionVersion as UserEncryption };
};
const resetPasswordV2 = async ({ userId, newPassword, type, oldPassword }: TResetPasswordV2DTO) => {
const cfg = getConfig();
const user = await userDAL.findUserEncKeyByUserId(userId);
if (!user) {
throw new BadRequestError({ message: `User encryption key not found for user with ID '${userId}'` });
}
if (!user.hashedPassword) {
throw new BadRequestError({ message: "Unable to reset password, no password is set" });
}
if (!user.authMethods?.includes(AuthMethod.EMAIL)) {
throw new BadRequestError({ message: "Unable to reset password, no email authentication method is configured" });
}
// we check the old password if the user is resetting their password while logged in
if (type === ResetPasswordV2Type.LoggedInReset) {
if (!oldPassword) {
throw new BadRequestError({ message: "Current password is required." });
}
const isValid = await bcrypt.compare(oldPassword, user.hashedPassword);
if (!isValid) {
throw new BadRequestError({ message: "Incorrect current password." });
}
}
const newHashedPassword = await bcrypt.hash(newPassword, cfg.BCRYPT_SALT_ROUND);
// we need to get the original private key first for v2
let privateKey: string;
if (
user.serverEncryptedPrivateKey &&
user.serverEncryptedPrivateKeyTag &&
user.serverEncryptedPrivateKeyIV &&
user.serverEncryptedPrivateKeyEncoding &&
user.encryptionVersion === UserEncryption.V2
) {
privateKey = infisicalSymmetricDecrypt({
iv: user.serverEncryptedPrivateKeyIV,
tag: user.serverEncryptedPrivateKeyTag,
ciphertext: user.serverEncryptedPrivateKey,
keyEncoding: user.serverEncryptedPrivateKeyEncoding as SecretKeyEncoding
});
} else {
throw new BadRequestError({
message: "Cannot reset password without current credentials or recovery method",
name: "Reset password"
});
}
const encKeys = await generateUserSrpKeys(user.username, newPassword, {
publicKey: user.publicKey,
privateKey
});
const { tag, iv, ciphertext, encoding } = infisicalSymmetricEncypt(privateKey);
await userDAL.updateUserEncryptionByUserId(userId, {
hashedPassword: newHashedPassword,
// srp params
salt: encKeys.salt,
verifier: encKeys.verifier,
protectedKey: encKeys.protectedKey,
protectedKeyIV: encKeys.protectedKeyIV,
protectedKeyTag: encKeys.protectedKeyTag,
encryptedPrivateKey: encKeys.encryptedPrivateKey,
iv: encKeys.encryptedPrivateKeyIV,
tag: encKeys.encryptedPrivateKeyTag,
serverEncryptedPrivateKey: ciphertext,
serverEncryptedPrivateKeyIV: iv,
serverEncryptedPrivateKeyTag: tag,
serverEncryptedPrivateKeyEncoding: encoding
});
await tokenService.revokeAllMySessions(userId);
};
/*
* Reset password of a user via backup key
* */
@ -391,6 +490,7 @@ export const authPaswordServiceFactory = ({
createBackupPrivateKey,
getBackupPrivateKeyOfUser,
sendPasswordSetupEmail,
setupPassword
setupPassword,
resetPasswordV2
};
};

@ -13,6 +13,18 @@ export type TChangePasswordDTO = {
password: string;
};
export enum ResetPasswordV2Type {
Recovery = "recovery",
LoggedInReset = "logged-in-reset"
}
export type TResetPasswordV2DTO = {
type: ResetPasswordV2Type;
userId: string;
newPassword: string;
oldPassword?: string;
};
export type TResetPasswordViaBackupKeyDTO = {
userId: string;
protectedKey: string;

@ -31,9 +31,9 @@ export type TImportDataIntoInfisicalDTO = {
projectEnvDAL: Pick<TProjectEnvDALFactory, "find" | "findLastEnvPosition" | "create" | "findOne">;
kmsService: Pick<TKmsServiceFactory, "createCipherPairWithDataKey">;
secretDAL: Pick<TSecretV2BridgeDALFactory, "insertMany" | "upsertSecretReferences" | "findBySecretKeys">;
secretDAL: Pick<TSecretV2BridgeDALFactory, "insertMany" | "upsertSecretReferences" | "findBySecretKeys" | "find">;
secretVersionDAL: Pick<TSecretVersionV2DALFactory, "insertMany" | "create">;
secretTagDAL: Pick<TSecretTagDALFactory, "saveTagsToSecretV2" | "create">;
secretTagDAL: Pick<TSecretTagDALFactory, "saveTagsToSecretV2" | "create" | "find">;
secretVersionTagDAL: Pick<TSecretVersionV2TagDALFactory, "insertMany" | "create">;
resourceMetadataDAL: Pick<TResourceMetadataDALFactory, "insertMany">;

@ -27,9 +27,9 @@ export type TExternalMigrationQueueFactoryDep = {
projectEnvDAL: Pick<TProjectEnvDALFactory, "find" | "findLastEnvPosition" | "create" | "findOne">;
kmsService: Pick<TKmsServiceFactory, "createCipherPairWithDataKey">;
secretDAL: Pick<TSecretV2BridgeDALFactory, "insertMany" | "upsertSecretReferences" | "findBySecretKeys">;
secretDAL: Pick<TSecretV2BridgeDALFactory, "insertMany" | "upsertSecretReferences" | "findBySecretKeys" | "find">;
secretVersionDAL: Pick<TSecretVersionV2DALFactory, "insertMany" | "create">;
secretTagDAL: Pick<TSecretTagDALFactory, "saveTagsToSecretV2" | "create">;
secretTagDAL: Pick<TSecretTagDALFactory, "saveTagsToSecretV2" | "create" | "find">;
secretVersionTagDAL: Pick<TSecretVersionV2TagDALFactory, "insertMany" | "create">;
folderDAL: Pick<TSecretFolderDALFactory, "create" | "findBySecretPath" | "findOne" | "findById">;

@ -4,7 +4,7 @@ import ms from "ms";
import { ActionProjectType, ProjectMembershipRole, SecretKeyEncoding, TGroups } from "@app/db/schemas";
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service";
import { ProjectPermissionActions, ProjectPermissionSub } from "@app/ee/services/permission/project-permission";
import { isAtLeastAsPrivileged } from "@app/lib/casl";
import { validatePermissionBoundary } from "@app/lib/casl/boundary";
import { decryptAsymmetric, encryptAsymmetric } from "@app/lib/crypto";
import { infisicalSymmetricDecrypt } from "@app/lib/crypto/encryption";
import { BadRequestError, ForbiddenRequestError, NotFoundError } from "@app/lib/errors";
@ -102,11 +102,13 @@ export const groupProjectServiceFactory = ({
project.id
);
const hasRequiredPrivileges = isAtLeastAsPrivileged(permission, rolePermission);
if (!hasRequiredPrivileges) {
throw new ForbiddenRequestError({ message: "Failed to assign group to a more privileged role" });
}
const permissionBoundary = validatePermissionBoundary(permission, rolePermission);
if (!permissionBoundary.isValid)
throw new ForbiddenRequestError({
name: "PermissionBoundaryError",
message: "Failed to assign group to a more privileged role",
details: { missingPermissions: permissionBoundary.missingPermissions }
});
}
// validate custom roles input
@ -267,12 +269,13 @@ export const groupProjectServiceFactory = ({
requestedRoleChange,
project.id
);
const hasRequiredPrivileges = isAtLeastAsPrivileged(permission, rolePermission);
if (!hasRequiredPrivileges) {
throw new ForbiddenRequestError({ message: "Failed to assign group to a more privileged role" });
}
const permissionBoundary = validatePermissionBoundary(permission, rolePermission);
if (!permissionBoundary.isValid)
throw new ForbiddenRequestError({
name: "PermissionBoundaryError",
message: "Failed to assign group to a more privileged role",
details: { missingPermissions: permissionBoundary.missingPermissions }
});
}
// validate custom roles input

@ -78,9 +78,7 @@ export const identityAccessTokenServiceFactory = ({
const renewAccessToken = async ({ accessToken }: TRenewAccessTokenDTO) => {
const appCfg = getConfig();
const decodedToken = jwt.verify(accessToken, appCfg.AUTH_SECRET) as JwtPayload & {
identityAccessTokenId: string;
};
const decodedToken = jwt.verify(accessToken, appCfg.AUTH_SECRET) as TIdentityAccessTokenJwtPayload;
if (decodedToken.authTokenType !== AuthTokenType.IDENTITY_ACCESS_TOKEN) {
throw new BadRequestError({ message: "Only identity access tokens can be renewed" });
}
@ -127,7 +125,23 @@ export const identityAccessTokenServiceFactory = ({
accessTokenLastRenewedAt: new Date()
});
return { accessToken, identityAccessToken: updatedIdentityAccessToken };
const renewedToken = jwt.sign(
{
identityId: decodedToken.identityId,
clientSecretId: decodedToken.clientSecretId,
identityAccessTokenId: decodedToken.identityAccessTokenId,
authTokenType: AuthTokenType.IDENTITY_ACCESS_TOKEN
} as TIdentityAccessTokenJwtPayload,
appCfg.AUTH_SECRET,
// akhilmhdh: for non-expiry tokens you should not even set the value, including undefined. Even for undefined jsonwebtoken throws error
Number(identityAccessToken.accessTokenTTL) === 0
? undefined
: {
expiresIn: Number(identityAccessToken.accessTokenTTL)
}
);
return { accessToken: renewedToken, identityAccessToken: updatedIdentityAccessToken };
};
const revokeAccessToken = async (accessToken: string) => {

@ -7,7 +7,7 @@ import { IdentityAuthMethod } from "@app/db/schemas";
import { TLicenseServiceFactory } from "@app/ee/services/license/license-service";
import { OrgPermissionActions, OrgPermissionSubjects } from "@app/ee/services/permission/org-permission";
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service";
import { isAtLeastAsPrivileged } from "@app/lib/casl";
import { validatePermissionBoundary } from "@app/lib/casl/boundary";
import { getConfig } from "@app/lib/config/env";
import { BadRequestError, ForbiddenRequestError, NotFoundError, UnauthorizedError } from "@app/lib/errors";
import { extractIPDetails, isValidIpOrCidr } from "@app/lib/ip";
@ -339,9 +339,12 @@ export const identityAwsAuthServiceFactory = ({
actorOrgId
);
if (!isAtLeastAsPrivileged(permission, rolePermission))
const permissionBoundary = validatePermissionBoundary(permission, rolePermission);
if (!permissionBoundary.isValid)
throw new ForbiddenRequestError({
message: "Failed to revoke aws auth of identity with more privileged role"
name: "PermissionBoundaryError",
message: "Failed to revoke aws auth of identity with more privileged role",
details: { missingPermissions: permissionBoundary.missingPermissions }
});
const revokedIdentityAwsAuth = await identityAwsAuthDAL.transaction(async (tx) => {

@ -5,7 +5,7 @@ import { IdentityAuthMethod } from "@app/db/schemas";
import { TLicenseServiceFactory } from "@app/ee/services/license/license-service";
import { OrgPermissionActions, OrgPermissionSubjects } from "@app/ee/services/permission/org-permission";
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service";
import { isAtLeastAsPrivileged } from "@app/lib/casl";
import { validatePermissionBoundary } from "@app/lib/casl/boundary";
import { getConfig } from "@app/lib/config/env";
import { BadRequestError, ForbiddenRequestError, NotFoundError, UnauthorizedError } from "@app/lib/errors";
import { extractIPDetails, isValidIpOrCidr } from "@app/lib/ip";
@ -312,9 +312,12 @@ export const identityAzureAuthServiceFactory = ({
actorAuthMethod,
actorOrgId
);
if (!isAtLeastAsPrivileged(permission, rolePermission))
const permissionBoundary = validatePermissionBoundary(permission, rolePermission);
if (!permissionBoundary.isValid)
throw new ForbiddenRequestError({
message: "Failed to revoke azure auth of identity with more privileged role"
name: "PermissionBoundaryError",
message: "Failed to revoke azure auth of identity with more privileged role",
details: { missingPermissions: permissionBoundary.missingPermissions }
});
const revokedIdentityAzureAuth = await identityAzureAuthDAL.transaction(async (tx) => {

@ -5,7 +5,7 @@ import { IdentityAuthMethod } from "@app/db/schemas";
import { TLicenseServiceFactory } from "@app/ee/services/license/license-service";
import { OrgPermissionActions, OrgPermissionSubjects } from "@app/ee/services/permission/org-permission";
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service";
import { isAtLeastAsPrivileged } from "@app/lib/casl";
import { validatePermissionBoundary } from "@app/lib/casl/boundary";
import { getConfig } from "@app/lib/config/env";
import { BadRequestError, ForbiddenRequestError, NotFoundError, UnauthorizedError } from "@app/lib/errors";
import { extractIPDetails, isValidIpOrCidr } from "@app/lib/ip";
@ -358,9 +358,12 @@ export const identityGcpAuthServiceFactory = ({
actorAuthMethod,
actorOrgId
);
if (!isAtLeastAsPrivileged(permission, rolePermission))
const permissionBoundary = validatePermissionBoundary(permission, rolePermission);
if (!permissionBoundary.isValid)
throw new ForbiddenRequestError({
message: "Failed to revoke gcp auth of identity with more privileged role"
name: "PermissionBoundaryError",
message: "Failed to revoke gcp auth of identity with more privileged role",
details: { missingPermissions: permissionBoundary.missingPermissions }
});
const revokedIdentityGcpAuth = await identityGcpAuthDAL.transaction(async (tx) => {

@ -7,7 +7,7 @@ import { IdentityAuthMethod, TIdentityJwtAuthsUpdate } from "@app/db/schemas";
import { TLicenseServiceFactory } from "@app/ee/services/license/license-service";
import { OrgPermissionActions, OrgPermissionSubjects } from "@app/ee/services/permission/org-permission";
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service";
import { isAtLeastAsPrivileged } from "@app/lib/casl";
import { validatePermissionBoundary } from "@app/lib/casl/boundary";
import { getConfig } from "@app/lib/config/env";
import { BadRequestError, ForbiddenRequestError, NotFoundError, UnauthorizedError } from "@app/lib/errors";
import { extractIPDetails, isValidIpOrCidr } from "@app/lib/ip";
@ -78,14 +78,22 @@ export const identityJwtAuthServiceFactory = ({
let tokenData: Record<string, string | boolean | number> = {};
if (identityJwtAuth.configurationType === JwtConfigurationType.JWKS) {
const decryptedJwksCaCert = orgDataKeyDecryptor({
cipherTextBlob: identityJwtAuth.encryptedJwksCaCert
}).toString();
const requestAgent = new https.Agent({ ca: decryptedJwksCaCert, rejectUnauthorized: !!decryptedJwksCaCert });
const client = new JwksClient({
jwksUri: identityJwtAuth.jwksUrl,
requestAgent
});
let client: JwksClient;
if (identityJwtAuth.jwksUrl.includes("https:")) {
const decryptedJwksCaCert = orgDataKeyDecryptor({
cipherTextBlob: identityJwtAuth.encryptedJwksCaCert
}).toString();
const requestAgent = new https.Agent({ ca: decryptedJwksCaCert, rejectUnauthorized: !!decryptedJwksCaCert });
client = new JwksClient({
jwksUri: identityJwtAuth.jwksUrl,
requestAgent
});
} else {
client = new JwksClient({
jwksUri: identityJwtAuth.jwksUrl
});
}
const { kid } = decodedToken.header;
const jwtSigningKey = await client.getSigningKey(kid);
@ -508,11 +516,13 @@ export const identityJwtAuthServiceFactory = ({
actorOrgId
);
if (!isAtLeastAsPrivileged(permission, rolePermission)) {
const permissionBoundary = validatePermissionBoundary(permission, rolePermission);
if (!permissionBoundary.isValid)
throw new ForbiddenRequestError({
message: "Failed to revoke JWT auth of identity with more privileged role"
name: "PermissionBoundaryError",
message: "Failed to revoke jwt auth of identity with more privileged role",
details: { missingPermissions: permissionBoundary.missingPermissions }
});
}
const revokedIdentityJwtAuth = await identityJwtAuthDAL.transaction(async (tx) => {
const deletedJwtAuth = await identityJwtAuthDAL.delete({ identityId }, tx);

@ -7,7 +7,7 @@ import { IdentityAuthMethod, TIdentityKubernetesAuthsUpdate } from "@app/db/sche
import { TLicenseServiceFactory } from "@app/ee/services/license/license-service";
import { OrgPermissionActions, OrgPermissionSubjects } from "@app/ee/services/permission/org-permission";
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service";
import { isAtLeastAsPrivileged } from "@app/lib/casl";
import { validatePermissionBoundary } from "@app/lib/casl/boundary";
import { getConfig } from "@app/lib/config/env";
import { BadRequestError, ForbiddenRequestError, NotFoundError, UnauthorizedError } from "@app/lib/errors";
import { extractIPDetails, isValidIpOrCidr } from "@app/lib/ip";
@ -487,9 +487,12 @@ export const identityKubernetesAuthServiceFactory = ({
actorAuthMethod,
actorOrgId
);
if (!isAtLeastAsPrivileged(permission, rolePermission))
const permissionBoundary = validatePermissionBoundary(permission, rolePermission);
if (!permissionBoundary.isValid)
throw new ForbiddenRequestError({
message: "Failed to revoke kubernetes auth of identity with more privileged role"
name: "PermissionBoundaryError",
message: "Failed to revoke kubernetes auth of identity with more privileged role",
details: { missingPermissions: permissionBoundary.missingPermissions }
});
const revokedIdentityKubernetesAuth = await identityKubernetesAuthDAL.transaction(async (tx) => {

@ -8,7 +8,7 @@ import { IdentityAuthMethod, TIdentityOidcAuthsUpdate } from "@app/db/schemas";
import { TLicenseServiceFactory } from "@app/ee/services/license/license-service";
import { OrgPermissionActions, OrgPermissionSubjects } from "@app/ee/services/permission/org-permission";
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service";
import { isAtLeastAsPrivileged } from "@app/lib/casl";
import { validatePermissionBoundary } from "@app/lib/casl/boundary";
import { getConfig } from "@app/lib/config/env";
import { BadRequestError, ForbiddenRequestError, NotFoundError, UnauthorizedError } from "@app/lib/errors";
import { extractIPDetails, isValidIpOrCidr } from "@app/lib/ip";
@ -428,11 +428,13 @@ export const identityOidcAuthServiceFactory = ({
actorOrgId
);
if (!isAtLeastAsPrivileged(permission, rolePermission)) {
const permissionBoundary = validatePermissionBoundary(permission, rolePermission);
if (!permissionBoundary.isValid)
throw new ForbiddenRequestError({
message: "Failed to revoke OIDC auth of identity with more privileged role"
name: "PermissionBoundaryError",
message: "Failed to revoke oidc auth of identity with more privileged role",
details: { missingPermissions: permissionBoundary.missingPermissions }
});
}
const revokedIdentityOidcAuth = await identityOidcAuthDAL.transaction(async (tx) => {
const deletedOidcAuth = await identityOidcAuthDAL.delete({ identityId }, tx);

@ -4,7 +4,7 @@ import ms from "ms";
import { ActionProjectType, ProjectMembershipRole } from "@app/db/schemas";
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service";
import { ProjectPermissionActions, ProjectPermissionSub } from "@app/ee/services/permission/project-permission";
import { isAtLeastAsPrivileged } from "@app/lib/casl";
import { validatePermissionBoundary } from "@app/lib/casl/boundary";
import { BadRequestError, ForbiddenRequestError, NotFoundError } from "@app/lib/errors";
import { groupBy } from "@app/lib/fn";
@ -91,11 +91,13 @@ export const identityProjectServiceFactory = ({
projectId
);
const hasRequiredPriviledges = isAtLeastAsPrivileged(permission, rolePermission);
if (!hasRequiredPriviledges) {
throw new ForbiddenRequestError({ message: "Failed to change to a more privileged role" });
}
const permissionBoundary = validatePermissionBoundary(permission, rolePermission);
if (!permissionBoundary.isValid)
throw new ForbiddenRequestError({
name: "PermissionBoundaryError",
message: "Failed to assign to a more privileged role",
details: { missingPermissions: permissionBoundary.missingPermissions }
});
}
// validate custom roles input
@ -185,9 +187,13 @@ export const identityProjectServiceFactory = ({
projectId
);
if (!isAtLeastAsPrivileged(permission, rolePermission)) {
throw new ForbiddenRequestError({ message: "Failed to change to a more privileged role" });
}
const permissionBoundary = validatePermissionBoundary(permission, rolePermission);
if (!permissionBoundary.isValid)
throw new ForbiddenRequestError({
name: "PermissionBoundaryError",
message: "Failed to change to a more privileged role",
details: { missingPermissions: permissionBoundary.missingPermissions }
});
}
// validate custom roles input
@ -277,8 +283,13 @@ export const identityProjectServiceFactory = ({
actorOrgId,
actionProjectType: ActionProjectType.Any
});
if (!isAtLeastAsPrivileged(permission, identityRolePermission))
throw new ForbiddenRequestError({ message: "Failed to delete more privileged identity" });
const permissionBoundary = validatePermissionBoundary(permission, identityRolePermission);
if (!permissionBoundary.isValid)
throw new ForbiddenRequestError({
name: "PermissionBoundaryError",
message: "Failed to remove more privileged identity",
details: { missingPermissions: permissionBoundary.missingPermissions }
});
const [deletedIdentity] = await identityProjectDAL.delete({ identityId, projectId });
return deletedIdentity;

@ -5,7 +5,7 @@ import { IdentityAuthMethod, TableName } from "@app/db/schemas";
import { TLicenseServiceFactory } from "@app/ee/services/license/license-service";
import { OrgPermissionActions, OrgPermissionSubjects } from "@app/ee/services/permission/org-permission";
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service";
import { isAtLeastAsPrivileged } from "@app/lib/casl";
import { validatePermissionBoundary } from "@app/lib/casl/boundary";
import { getConfig } from "@app/lib/config/env";
import { BadRequestError, ForbiddenRequestError, NotFoundError } from "@app/lib/errors";
import { extractIPDetails, isValidIpOrCidr } from "@app/lib/ip";
@ -245,11 +245,13 @@ export const identityTokenAuthServiceFactory = ({
actorOrgId
);
if (!isAtLeastAsPrivileged(permission, rolePermission)) {
const permissionBoundary = validatePermissionBoundary(permission, rolePermission);
if (!permissionBoundary.isValid)
throw new ForbiddenRequestError({
message: "Failed to revoke Token Auth of identity with more privileged role"
name: "PermissionBoundaryError",
message: "Failed to revoke token auth of identity with more privileged role",
details: { missingPermissions: permissionBoundary.missingPermissions }
});
}
const revokedIdentityTokenAuth = await identityTokenAuthDAL.transaction(async (tx) => {
const deletedTokenAuth = await identityTokenAuthDAL.delete({ identityId }, tx);
@ -295,10 +297,12 @@ export const identityTokenAuthServiceFactory = ({
actorAuthMethod,
actorOrgId
);
const hasPriviledge = isAtLeastAsPrivileged(permission, rolePermission);
if (!hasPriviledge)
const permissionBoundary = validatePermissionBoundary(permission, rolePermission);
if (!permissionBoundary.isValid)
throw new ForbiddenRequestError({
message: "Failed to create token for identity with more privileged role"
name: "PermissionBoundaryError",
message: "Failed to create token for identity with more privileged role",
details: { missingPermissions: permissionBoundary.missingPermissions }
});
const identityTokenAuth = await identityTokenAuthDAL.findOne({ identityId });
@ -415,10 +419,12 @@ export const identityTokenAuthServiceFactory = ({
actorAuthMethod,
actorOrgId
);
const hasPriviledge = isAtLeastAsPrivileged(permission, rolePermission);
if (!hasPriviledge)
const permissionBoundary = validatePermissionBoundary(permission, rolePermission);
if (!permissionBoundary.isValid)
throw new ForbiddenRequestError({
message: "Failed to update token for identity with more privileged role"
name: "PermissionBoundaryError",
message: "Failed to update token for identity with more privileged role",
details: { missingPermissions: permissionBoundary.missingPermissions }
});
const [token] = await identityAccessTokenDAL.update(

@ -8,7 +8,7 @@ import { IdentityAuthMethod } from "@app/db/schemas";
import { TLicenseServiceFactory } from "@app/ee/services/license/license-service";
import { OrgPermissionActions, OrgPermissionSubjects } from "@app/ee/services/permission/org-permission";
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service";
import { isAtLeastAsPrivileged } from "@app/lib/casl";
import { validatePermissionBoundary } from "@app/lib/casl/boundary";
import { getConfig } from "@app/lib/config/env";
import { BadRequestError, ForbiddenRequestError, NotFoundError, UnauthorizedError } from "@app/lib/errors";
import { checkIPAgainstBlocklist, extractIPDetails, isValidIpOrCidr, TIp } from "@app/lib/ip";
@ -367,9 +367,12 @@ export const identityUaServiceFactory = ({
actorAuthMethod,
actorOrgId
);
if (!isAtLeastAsPrivileged(permission, rolePermission))
const permissionBoundary = validatePermissionBoundary(permission, rolePermission);
if (!permissionBoundary.isValid)
throw new ForbiddenRequestError({
message: "Failed to revoke universal auth of identity with more privileged role"
name: "PermissionBoundaryError",
message: "Failed to revoke universal auth of identity with more privileged role",
details: { missingPermissions: permissionBoundary.missingPermissions }
});
const revokedIdentityUniversalAuth = await identityUaDAL.transaction(async (tx) => {
@ -414,10 +417,12 @@ export const identityUaServiceFactory = ({
actorAuthMethod,
actorOrgId
);
const hasPriviledge = isAtLeastAsPrivileged(permission, rolePermission);
if (!hasPriviledge)
const permissionBoundary = validatePermissionBoundary(permission, rolePermission);
if (!permissionBoundary.isValid)
throw new ForbiddenRequestError({
message: "Failed to add identity to project with more privileged role"
name: "PermissionBoundaryError",
message: "Failed to create client secret for a more privileged identity.",
details: { missingPermissions: permissionBoundary.missingPermissions }
});
const appCfg = getConfig();
@ -475,9 +480,12 @@ export const identityUaServiceFactory = ({
actorOrgId
);
if (!isAtLeastAsPrivileged(permission, rolePermission))
const permissionBoundary = validatePermissionBoundary(permission, rolePermission);
if (!permissionBoundary.isValid)
throw new ForbiddenRequestError({
message: "Failed to add identity to project with more privileged role"
name: "PermissionBoundaryError",
message: "Failed to get identity client secret with more privileged role",
details: { missingPermissions: permissionBoundary.missingPermissions }
});
const identityUniversalAuth = await identityUaDAL.findOne({
@ -524,9 +532,12 @@ export const identityUaServiceFactory = ({
actorAuthMethod,
actorOrgId
);
if (!isAtLeastAsPrivileged(permission, rolePermission))
const permissionBoundary = validatePermissionBoundary(permission, rolePermission);
if (!permissionBoundary.isValid)
throw new ForbiddenRequestError({
message: "Failed to read identity client secret of project with more privileged role"
name: "PermissionBoundaryError",
message: "Failed to read identity client secret of identity with more privileged role",
details: { missingPermissions: permissionBoundary.missingPermissions }
});
const clientSecret = await identityUaClientSecretDAL.findById(clientSecretId);
@ -566,10 +577,12 @@ export const identityUaServiceFactory = ({
actorAuthMethod,
actorOrgId
);
if (!isAtLeastAsPrivileged(permission, rolePermission))
const permissionBoundary = validatePermissionBoundary(permission, rolePermission);
if (!permissionBoundary.isValid)
throw new ForbiddenRequestError({
message: "Failed to revoke identity client secret with more privileged role"
name: "PermissionBoundaryError",
message: "Failed to revoke identity client secret with more privileged role",
details: { missingPermissions: permissionBoundary.missingPermissions }
});
const clientSecret = await identityUaClientSecretDAL.updateById(clientSecretId, {

@ -1,10 +1,42 @@
import { TDbClient } from "@app/db";
import { TableName } from "@app/db/schemas";
import { ormify } from "@app/lib/knex";
import { TableName, TIdentities } from "@app/db/schemas";
import { ormify, selectAllTableCols } from "@app/lib/knex";
import { DatabaseError } from "@app/lib/errors";
export type TIdentityDALFactory = ReturnType<typeof identityDALFactory>;
export const identityDALFactory = (db: TDbClient) => {
const identityOrm = ormify(db, TableName.Identity);
return identityOrm;
const getIdentitiesByFilter = async ({
limit,
offset,
searchTerm,
sortBy
}: {
limit: number;
offset: number;
searchTerm: string;
sortBy?: keyof TIdentities;
}) => {
try {
let query = db.replicaNode()(TableName.Identity);
if (searchTerm) {
query = query.where((qb) => {
void qb.whereILike("name", `%${searchTerm}%`);
});
}
if (sortBy) {
query = query.orderBy(sortBy);
}
return await query.limit(limit).offset(offset).select(selectAllTableCols(TableName.Identity));
} catch (error) {
throw new DatabaseError({ error, name: "Get identities by filter" });
}
};
return { ...identityOrm, getIdentitiesByFilter };
};

@ -4,7 +4,7 @@ import { OrgMembershipRole, TableName, TOrgRoles } from "@app/db/schemas";
import { TLicenseServiceFactory } from "@app/ee/services/license/license-service";
import { OrgPermissionActions, OrgPermissionSubjects } from "@app/ee/services/permission/org-permission";
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service";
import { isAtLeastAsPrivileged } from "@app/lib/casl";
import { validatePermissionBoundary } from "@app/lib/casl/boundary";
import { BadRequestError, ForbiddenRequestError, NotFoundError } from "@app/lib/errors";
import { TIdentityProjectDALFactory } from "@app/services/identity-project/identity-project-dal";
@ -58,9 +58,13 @@ export const identityServiceFactory = ({
orgId
);
const isCustomRole = Boolean(customRole);
const hasRequiredPriviledges = isAtLeastAsPrivileged(permission, rolePermission);
if (!hasRequiredPriviledges)
throw new ForbiddenRequestError({ message: "Failed to create a more privileged identity" });
const permissionBoundary = validatePermissionBoundary(permission, rolePermission);
if (!permissionBoundary.isValid)
throw new ForbiddenRequestError({
name: "PermissionBoundaryError",
message: "Failed to create a more privileged identity",
details: { missingPermissions: permissionBoundary.missingPermissions }
});
const plan = await licenseService.getPlan(orgId);
@ -129,9 +133,13 @@ export const identityServiceFactory = ({
actorAuthMethod,
actorOrgId
);
const hasRequiredPriviledges = isAtLeastAsPrivileged(permission, identityRolePermission);
if (!hasRequiredPriviledges)
throw new ForbiddenRequestError({ message: "Failed to delete more privileged identity" });
const permissionBoundary = validatePermissionBoundary(permission, identityRolePermission);
if (!permissionBoundary.isValid)
throw new ForbiddenRequestError({
name: "PermissionBoundaryError",
message: "Failed to update a more privileged identity",
details: { missingPermissions: permissionBoundary.missingPermissions }
});
let customRole: TOrgRoles | undefined;
if (role) {
@ -141,9 +149,13 @@ export const identityServiceFactory = ({
);
const isCustomRole = Boolean(customOrgRole);
const hasRequiredNewRolePermission = isAtLeastAsPrivileged(permission, rolePermission);
if (!hasRequiredNewRolePermission)
throw new ForbiddenRequestError({ message: "Failed to create a more privileged identity" });
const appliedRolePermissionBoundary = validatePermissionBoundary(permission, rolePermission);
if (!appliedRolePermissionBoundary.isValid)
throw new ForbiddenRequestError({
name: "PermissionBoundaryError",
message: "Failed to create a more privileged identity",
details: { missingPermissions: appliedRolePermissionBoundary.missingPermissions }
});
if (isCustomRole) customRole = customOrgRole;
}
@ -216,9 +228,13 @@ export const identityServiceFactory = ({
actorAuthMethod,
actorOrgId
);
const hasRequiredPriviledges = isAtLeastAsPrivileged(permission, identityRolePermission);
if (!hasRequiredPriviledges)
throw new ForbiddenRequestError({ message: "Failed to delete more privileged identity" });
const permissionBoundary = validatePermissionBoundary(permission, identityRolePermission);
if (!permissionBoundary.isValid)
throw new ForbiddenRequestError({
name: "PermissionBoundaryError",
message: "Failed to delete more privileged identity",
details: { missingPermissions: permissionBoundary.missingPermissions }
});
const deletedIdentity = await identityDAL.deleteById(id);

@ -114,20 +114,27 @@ export const integrationAuthServiceFactory = ({
const listOrgIntegrationAuth = async ({ actorId, actor, actorOrgId, actorAuthMethod }: TGenericPermission) => {
const authorizations = await integrationAuthDAL.getByOrg(actorOrgId as string);
return Promise.all(
authorizations.filter(async (auth) => {
const { permission } = await permissionService.getProjectPermission({
actor,
actorId,
projectId: auth.projectId,
actorAuthMethod,
actorOrgId,
actionProjectType: ActionProjectType.SecretManager
});
const filteredAuthorizations = await Promise.all(
authorizations.map(async (auth) => {
try {
const { permission } = await permissionService.getProjectPermission({
actor,
actorId,
projectId: auth.projectId,
actorAuthMethod,
actorOrgId,
actionProjectType: ActionProjectType.SecretManager
});
return permission.can(ProjectPermissionActions.Read, ProjectPermissionSub.Integrations);
return permission.can(ProjectPermissionActions.Read, ProjectPermissionSub.Integrations) ? auth : null;
} catch (error) {
// user does not belong to the project that the integration auth belongs to
return null;
}
})
);
return filteredAuthorizations.filter((auth): auth is NonNullable<typeof auth> => auth !== null);
};
const getIntegrationAuth = async ({ actor, id, actorId, actorAuthMethod, actorOrgId }: TGetIntegrationAuthDTO) => {

@ -68,7 +68,8 @@ const getIntegrationSecretsV2 = async (
secretDAL: secretV2BridgeDAL,
secretImportDAL,
secretImports,
hasSecretAccess: () => true
hasSecretAccess: () => true,
viewSecretValue: true
});
for (let i = importedSecrets.length - 1; i >= 0; i -= 1) {

@ -1,8 +1,13 @@
import { ForbiddenError, subject } from "@casl/ability";
import { ForbiddenError } from "@casl/ability";
import { ActionProjectType } from "@app/db/schemas";
import { throwIfMissingSecretReadValueOrDescribePermission } from "@app/ee/services/permission/permission-fns";
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service";
import { ProjectPermissionActions, ProjectPermissionSub } from "@app/ee/services/permission/project-permission";
import {
ProjectPermissionActions,
ProjectPermissionSecretActions,
ProjectPermissionSub
} from "@app/ee/services/permission/project-permission";
import { NotFoundError } from "@app/lib/errors";
import { TProjectPermission } from "@app/lib/types";
@ -91,13 +96,10 @@ export const integrationServiceFactory = ({
});
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Create, ProjectPermissionSub.Integrations);
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Read,
subject(ProjectPermissionSub.Secrets, {
environment: sourceEnvironment,
secretPath
})
);
throwIfMissingSecretReadValueOrDescribePermission(permission, ProjectPermissionSecretActions.ReadValue, {
environment: sourceEnvironment,
secretPath
});
const folder = await folderDAL.findBySecretPath(integrationAuth.projectId, sourceEnvironment, secretPath);
if (!folder) {
@ -174,13 +176,10 @@ export const integrationServiceFactory = ({
const newSecretPath = secretPath || integration.secretPath;
if (environment || secretPath) {
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Read,
subject(ProjectPermissionSub.Secrets, {
environment: newEnvironment,
secretPath: newSecretPath
})
);
throwIfMissingSecretReadValueOrDescribePermission(permission, ProjectPermissionSecretActions.ReadValue, {
environment: newEnvironment,
secretPath: newSecretPath
});
}
const folder = await folderDAL.findBySecretPath(integration.projectId, newEnvironment, newSecretPath);

@ -7,7 +7,7 @@ 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 { TProjectUserAdditionalPrivilegeDALFactory } from "@app/ee/services/project-user-additional-privilege/project-user-additional-privilege-dal";
import { isAtLeastAsPrivileged } from "@app/lib/casl";
import { validatePermissionBoundary } from "@app/lib/casl/boundary";
import { getConfig } from "@app/lib/config/env";
import { BadRequestError, ForbiddenRequestError, NotFoundError } from "@app/lib/errors";
import { groupBy } from "@app/lib/fn";
@ -274,13 +274,13 @@ export const projectMembershipServiceFactory = ({
projectId
);
const hasRequiredPriviledges = isAtLeastAsPrivileged(permission, rolePermission);
if (!hasRequiredPriviledges) {
const permissionBoundary = validatePermissionBoundary(permission, rolePermission);
if (!permissionBoundary.isValid)
throw new ForbiddenRequestError({
message: `Failed to change to a more privileged role ${requestedRoleChange}`
name: "PermissionBoundaryError",
message: `Failed to change to a more privileged role ${requestedRoleChange}`,
details: { missingPermissions: permissionBoundary.missingPermissions }
});
}
}
// validate custom roles input

@ -10,8 +10,13 @@ import {
} from "@app/db/schemas";
import { TLicenseServiceFactory } from "@app/ee/services/license/license-service";
import { OrgPermissionActions, OrgPermissionSubjects } from "@app/ee/services/permission/org-permission";
import { throwIfMissingSecretReadValueOrDescribePermission } from "@app/ee/services/permission/permission-fns";
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service";
import { ProjectPermissionActions, ProjectPermissionSub } from "@app/ee/services/permission/project-permission";
import {
ProjectPermissionActions,
ProjectPermissionSecretActions,
ProjectPermissionSub
} from "@app/ee/services/permission/project-permission";
import { TProjectTemplateServiceFactory } from "@app/ee/services/project-template/project-template-service";
import { InfisicalProjectTemplate } from "@app/ee/services/project-template/project-template-types";
import { TSshCertificateAuthorityDALFactory } from "@app/ee/services/ssh/ssh-certificate-authority-dal";
@ -563,11 +568,24 @@ export const projectServiceFactory = ({
});
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Edit, ProjectPermissionSub.Settings);
if (update.slug) {
const existingProject = await projectDAL.findOne({
slug: update.slug,
orgId: actorOrgId
});
if (existingProject && existingProject.id !== project.id) {
throw new BadRequestError({
message: `Failed to update project slug. The project "${existingProject.name}" with the slug "${existingProject.slug}" already exists in your organization. Please choose a unique slug for your project.`
});
}
}
const updatedProject = await projectDAL.updateById(project.id, {
name: update.name,
description: update.description,
autoCapitalization: update.autoCapitalization,
enforceCapitalization: update.autoCapitalization
enforceCapitalization: update.autoCapitalization,
slug: update.slug
});
return updatedProject;
@ -747,7 +765,7 @@ export const projectServiceFactory = ({
actorOrgId,
actionProjectType: ActionProjectType.Any
});
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.Secrets);
throwIfMissingSecretReadValueOrDescribePermission(permission, ProjectPermissionSecretActions.DescribeSecret);
const project = await projectDAL.findProjectById(projectId);

@ -82,6 +82,7 @@ export type TUpdateProjectDTO = {
name?: string;
description?: string;
autoCapitalization?: boolean;
slug?: string;
};
} & Omit<TProjectPermission, "projectId">;

@ -0,0 +1,17 @@
import { TSecretFolders } from "@app/db/schemas";
import { InternalServerError } from "@app/lib/errors";
export const buildFolderPath = (
folder: TSecretFolders,
foldersMap: Record<string, TSecretFolders>,
depth: number = 0
): string => {
if (depth > 20) {
throw new InternalServerError({ message: "Maximum folder depth of 20 exceeded" });
}
if (!folder.parentId) {
return depth === 0 ? "/" : "";
}
return `${buildFolderPath(foldersMap[folder.parentId], foldersMap, depth + 1)}/${folder.name}`;
};

@ -8,6 +8,7 @@ import { ProjectPermissionActions, ProjectPermissionSub } from "@app/ee/services
import { TSecretSnapshotServiceFactory } from "@app/ee/services/secret-snapshot/secret-snapshot-service";
import { BadRequestError, NotFoundError } from "@app/lib/errors";
import { OrderByDirection, OrgServiceActor } from "@app/lib/types";
import { buildFolderPath } from "@app/services/secret-folder/secret-folder-fns";
import { TProjectDALFactory } from "../project/project-dal";
import { TProjectEnvDALFactory } from "../project-env/project-env-dal";
@ -27,7 +28,7 @@ type TSecretFolderServiceFactoryDep = {
permissionService: Pick<TPermissionServiceFactory, "getProjectPermission">;
snapshotService: Pick<TSecretSnapshotServiceFactory, "performSnapshot">;
folderDAL: TSecretFolderDALFactory;
projectEnvDAL: Pick<TProjectEnvDALFactory, "findOne" | "findBySlugs">;
projectEnvDAL: Pick<TProjectEnvDALFactory, "findOne" | "findBySlugs" | "find">;
folderVersionDAL: TSecretFolderVersionDALFactory;
projectDAL: Pick<TProjectDALFactory, "findProjectBySlug">;
};
@ -580,6 +581,44 @@ export const secretFolderServiceFactory = ({
return folders;
};
const getProjectEnvironmentsFolders = async (projectId: string, actor: OrgServiceActor) => {
// folder list is allowed to be read by anyone
// permission is to check if user has access
await permissionService.getProjectPermission({
actor: actor.type,
actorId: actor.id,
projectId,
actorAuthMethod: actor.authMethod,
actorOrgId: actor.orgId,
actionProjectType: ActionProjectType.SecretManager
});
const environments = await projectEnvDAL.find({ projectId });
const folders = await folderDAL.find({
$in: {
envId: environments.map((env) => env.id)
},
isReserved: false
});
const environmentFolders = Object.fromEntries(
environments.map((env) => {
const relevantFolders = folders.filter((folder) => folder.envId === env.id);
const foldersMap = Object.fromEntries(relevantFolders.map((folder) => [folder.id, folder]));
const foldersWithPath = relevantFolders.map((folder) => ({
...folder,
path: buildFolderPath(folder, foldersMap)
}));
return [env.slug, { ...env, folders: foldersWithPath }];
})
);
return environmentFolders;
};
return {
createFolder,
updateFolder,
@ -589,6 +628,7 @@ export const secretFolderServiceFactory = ({
getFolderById,
getProjectFolderCount,
getFoldersMultiEnv,
getFoldersDeepByEnvs
getFoldersDeepByEnvs,
getProjectEnvironmentsFolders
};
};

@ -3,6 +3,7 @@ import { groupBy, unique } from "@app/lib/fn";
import { ResourceMetadataDTO } from "../resource-metadata/resource-metadata-schema";
import { TSecretDALFactory } from "../secret/secret-dal";
import { INFISICAL_SECRET_VALUE_HIDDEN_MASK } from "../secret/secret-fns";
import { TSecretFolderDALFactory } from "../secret-folder/secret-folder-dal";
import { TSecretV2BridgeDALFactory } from "../secret-v2-bridge/secret-v2-bridge-dal";
import { TSecretImportDALFactory } from "./secret-import-dal";
@ -32,6 +33,12 @@ type TSecretImportSecretsV2 = {
folderId: string | undefined;
importFolderId: string;
secrets: (TSecretsV2 & {
secretTags: {
slug: string;
name: string;
color?: string | null;
id: string;
}[];
workspace: string;
environment: string;
_id: string;
@ -39,6 +46,7 @@ type TSecretImportSecretsV2 = {
// akhilmhdh: yes i know you can put ?.
// But for somereason ts consider ? and undefined explicit as different just ts things
secretValue: string;
secretValueHidden: boolean;
secretComment: string;
secretMetadata?: ResourceMetadataDTO;
})[];
@ -150,12 +158,14 @@ export const fnSecretsV2FromImports = async ({
secretImportDAL,
decryptor,
expandSecretReferences,
hasSecretAccess
hasSecretAccess,
viewSecretValue
}: {
secretImports: (Omit<TSecretImports, "importEnv"> & {
importEnv: { id: string; slug: string; name: string };
})[];
folderDAL: Pick<TSecretFolderDALFactory, "findByManySecretPath">;
viewSecretValue: boolean;
secretDAL: Pick<TSecretV2BridgeDALFactory, "find">;
secretImportDAL: Pick<TSecretImportDALFactory, "findByFolderIds">;
decryptor: (value?: Buffer | null) => string;
@ -168,9 +178,14 @@ export const fnSecretsV2FromImports = async ({
hasSecretAccess: (environment: string, secretPath: string, secretName: string, secretTagSlugs: string[]) => boolean;
}) => {
const cyclicDetector = new Set();
const stack: { secretImports: typeof rootSecretImports; depth: number; parentImportedSecrets: TSecretsV2[] }[] = [
{ secretImports: rootSecretImports, depth: 0, parentImportedSecrets: [] }
];
const stack: {
secretImports: typeof rootSecretImports;
depth: number;
parentImportedSecrets: (TSecretsV2 & {
secretValueHidden: boolean;
secretTags: { slug: string; name: string; id: string; color?: string | null }[];
})[];
}[] = [{ secretImports: rootSecretImports, depth: 0, parentImportedSecrets: [] }];
const processedImports: TSecretImportSecretsV2[] = [];
@ -229,7 +244,9 @@ export const fnSecretsV2FromImports = async ({
.map((item) => ({
...item,
secretKey: item.key,
secretValue: decryptor(item.encryptedValue),
secretValue: viewSecretValue ? decryptor(item.encryptedValue) : INFISICAL_SECRET_VALUE_HIDDEN_MASK,
secretValueHidden: !viewSecretValue,
secretTags: item.tags,
secretComment: decryptor(item.encryptedComment),
environment: importEnv.slug,
workspace: "", // This field should not be used, it's only here to keep the older Python SDK versions backwards compatible with the new Postgres backend.
@ -267,6 +284,8 @@ export const fnSecretsV2FromImports = async ({
processedImport.secrets = unique(processedImport.secrets, (i) => i.key);
return Promise.allSettled(
processedImport.secrets.map(async (decryptedSecret, index) => {
if (decryptedSecret.secretValueHidden) return;
const expandedSecretValue = await expandSecretReferences({
value: decryptedSecret.secretValue,
secretPath: processedImport.secretPath,

@ -4,8 +4,16 @@ import { ForbiddenError, subject } from "@casl/ability";
import { ActionProjectType, TableName } from "@app/db/schemas";
import { TLicenseServiceFactory } from "@app/ee/services/license/license-service";
import {
hasSecretReadValueOrDescribePermission,
throwIfMissingSecretReadValueOrDescribePermission
} from "@app/ee/services/permission/permission-fns";
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service";
import { ProjectPermissionActions, ProjectPermissionSub } from "@app/ee/services/permission/project-permission";
import {
ProjectPermissionActions,
ProjectPermissionSecretActions,
ProjectPermissionSub
} from "@app/ee/services/permission/project-permission";
import { getReplicationFolderName } from "@app/ee/services/secret-replication/secret-replication-service";
import { BadRequestError, NotFoundError } from "@app/lib/errors";
@ -89,13 +97,11 @@ export const secretImportServiceFactory = ({
);
// check if user has permission to import from target path
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Read,
subject(ProjectPermissionSub.Secrets, {
environment: data.environment,
secretPath: data.path
})
);
throwIfMissingSecretReadValueOrDescribePermission(permission, ProjectPermissionSecretActions.DescribeSecret, {
environment: data.environment,
secretPath: data.path
});
if (isReplication) {
const plan = await licenseService.getPlan(actorOrgId);
if (!plan.secretApproval) {
@ -401,13 +407,10 @@ export const secretImportServiceFactory = ({
if (!secretImportDoc.isReplication) throw new BadRequestError({ message: "Import is not in replication mode" });
// check if user has permission to import from target path
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Read,
subject(ProjectPermissionSub.Secrets, {
environment: secretImportDoc.importEnv.slug,
secretPath: secretImportDoc.importPath
})
);
throwIfMissingSecretReadValueOrDescribePermission(permission, ProjectPermissionSecretActions.DescribeSecret, {
environment: secretImportDoc.importEnv.slug,
secretPath: secretImportDoc.importPath
});
await projectDAL.checkProjectUpgradeStatus(projectId);
@ -595,14 +598,12 @@ export const secretImportServiceFactory = ({
// so anything based on this order will also be in right position
const secretImports = await secretImportDAL.find({ folderId: folder.id, isReplication: false });
const allowedImports = secretImports.filter((el) =>
permission.can(
ProjectPermissionActions.Read,
subject(ProjectPermissionSub.Secrets, {
environment: el.importEnv.slug,
secretPath: el.importPath
})
)
hasSecretReadValueOrDescribePermission(permission, ProjectPermissionSecretActions.ReadValue, {
environment: el.importEnv.slug,
secretPath: el.importPath
})
);
return fnSecretsFromImports({ allowedImports, folderDAL, secretDAL, secretImportDAL });
};
@ -642,20 +643,19 @@ export const secretImportServiceFactory = ({
const importedSecrets = await fnSecretsV2FromImports({
secretImports,
folderDAL,
viewSecretValue: true,
secretDAL: secretV2BridgeDAL,
secretImportDAL,
decryptor: (value) => (value ? secretManagerDecryptor({ cipherTextBlob: value }).toString() : ""),
hasSecretAccess: (expandEnvironment, expandSecretPath, expandSecretKey, expandSecretTags) =>
permission.can(
ProjectPermissionActions.Read,
subject(ProjectPermissionSub.Secrets, {
environment: expandEnvironment,
secretPath: expandSecretPath,
secretName: expandSecretKey,
secretTags: expandSecretTags
})
)
hasSecretReadValueOrDescribePermission(permission, ProjectPermissionSecretActions.ReadValue, {
environment: expandEnvironment,
secretPath: expandSecretPath,
secretName: expandSecretKey,
secretTags: expandSecretTags
})
});
return importedSecrets;
}
@ -666,13 +666,10 @@ export const secretImportServiceFactory = ({
});
const allowedImports = secretImports.filter((el) =>
permission.can(
ProjectPermissionActions.Read,
subject(ProjectPermissionSub.Secrets, {
environment: el.importEnv.slug,
secretPath: el.importPath
})
)
hasSecretReadValueOrDescribePermission(permission, ProjectPermissionSecretActions.ReadValue, {
environment: el.importEnv.slug,
secretPath: el.importPath
})
);
const importedSecrets = await fnSecretsFromImports({
allowedImports,
@ -683,7 +680,10 @@ export const secretImportServiceFactory = ({
return importedSecrets.map((el) => ({
...el,
secrets: el.secrets.map((encryptedSecret) =>
decryptSecretRaw({ ...encryptedSecret, workspace: projectId, environment, secretPath }, botKey)
decryptSecretRaw(
{ ...encryptedSecret, workspace: projectId, environment, secretPath, secretValueHidden: false },
botKey
)
)
}));
};

@ -249,7 +249,8 @@ export const secretSyncQueueFactory = ({
expandSecretReferences,
secretImportDAL,
secretImports,
hasSecretAccess: () => true
hasSecretAccess: () => true,
viewSecretValue: true
});
for (let i = importedSecrets.length - 1; i >= 0; i -= 1) {

@ -1,9 +1,10 @@
import { ForbiddenError, subject } from "@casl/ability";
import { ForbiddenError } from "@casl/ability";
import { ActionProjectType } from "@app/db/schemas";
import { throwIfMissingSecretReadValueOrDescribePermission } from "@app/ee/services/permission/permission-fns";
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service";
import {
ProjectPermissionActions,
ProjectPermissionSecretActions,
ProjectPermissionSecretSyncActions,
ProjectPermissionSub
} from "@app/ee/services/permission/project-permission";
@ -178,13 +179,10 @@ export const secretSyncServiceFactory = ({
ProjectPermissionSub.SecretSyncs
);
ForbiddenError.from(projectPermission).throwUnlessCan(
ProjectPermissionActions.Read,
subject(ProjectPermissionSub.Secrets, {
environment,
secretPath
})
);
throwIfMissingSecretReadValueOrDescribePermission(projectPermission, ProjectPermissionSecretActions.ReadValue, {
environment,
secretPath
});
const folder = await folderDAL.findBySecretPath(projectId, environment, secretPath);
@ -269,13 +267,10 @@ export const secretSyncServiceFactory = ({
if (!updatedEnvironment || !updatedSecretPath)
throw new BadRequestError({ message: "Must specify both source environment and secret path" });
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Read,
subject(ProjectPermissionSub.Secrets, {
environment: updatedEnvironment,
secretPath: updatedSecretPath
})
);
throwIfMissingSecretReadValueOrDescribePermission(permission, ProjectPermissionSecretActions.ReadValue, {
environment: updatedEnvironment,
secretPath: updatedSecretPath
});
const newFolder = await folderDAL.findBySecretPath(secretSync.projectId, updatedEnvironment, updatedSecretPath);

@ -47,6 +47,7 @@ export const secretTagDALFactory = (db: TDbClient) => {
throw new DatabaseError({ error, name: "Find all by ids" });
}
};
return {
...secretTagOrm,
saveTagsToSecret: secretJnTagOrm.insertMany,

@ -8,6 +8,7 @@ import { logger } from "@app/lib/logger";
import { ActorType } from "../auth/auth-type";
import { TProjectEnvDALFactory } from "../project-env/project-env-dal";
import { ResourceMetadataDTO } from "../resource-metadata/resource-metadata-schema";
import { INFISICAL_SECRET_VALUE_HIDDEN_MASK } from "../secret/secret-fns";
import { TSecretFolderDALFactory } from "../secret-folder/secret-folder-dal";
import { TSecretV2BridgeDALFactory } from "./secret-v2-bridge-dal";
import { TFnSecretBulkDelete, TFnSecretBulkInsert, TFnSecretBulkUpdate } from "./secret-v2-bridge-types";
@ -93,7 +94,7 @@ export const fnSecretBulkInsert = async ({
);
const userActorId = actor && actor.type === ActorType.USER ? actor.actorId : undefined;
const identityActorId = actor && actor.type !== ActorType.USER ? actor.actorId : undefined;
const identityActorId = actor && actor.type === ActorType.IDENTITY ? actor.actorId : undefined;
const actorType = actor?.type || ActorType.PLATFORM;
const newSecrets = await secretDAL.insertMany(
@ -108,6 +109,7 @@ export const fnSecretBulkInsert = async ({
[`${TableName.SecretV2}Id` as const]: newSecretGroupedByKeyName[key][0].id
}))
);
const secretVersions = await secretVersionDAL.insertMany(
sanitizedInputSecrets.map((el) => ({
...el,
@ -146,6 +148,7 @@ export const fnSecretBulkInsert = async ({
if (newSecretTags.length) {
const secTags = await secretTagDAL.saveTagsToSecretV2(newSecretTags, tx);
const secVersionsGroupBySecId = groupBy(secretVersions, (i) => i.secretId);
const newSecretVersionTags = secTags.flatMap(({ secrets_v2Id, secret_tagsId }) => ({
[`${TableName.SecretVersionV2}Id` as const]: secVersionsGroupBySecId[secrets_v2Id][0].id,
[`${TableName.SecretTag}Id` as const]: secret_tagsId
@ -154,7 +157,16 @@ export const fnSecretBulkInsert = async ({
await secretVersionTagDAL.insertMany(newSecretVersionTags, tx);
}
return newSecrets.map((secret) => ({ ...secret, _id: secret.id }));
const secretsWithTags = await secretDAL.find(
{
$in: {
[`${TableName.SecretV2}.id` as "id"]: newSecrets.map((s) => s.id)
}
},
{ tx }
);
return secretsWithTags.map((secret) => ({ ...secret, _id: secret.id }));
};
export const fnSecretBulkUpdate = async ({
@ -170,7 +182,7 @@ export const fnSecretBulkUpdate = async ({
actor
}: TFnSecretBulkUpdate) => {
const userActorId = actor && actor?.type === ActorType.USER ? actor?.actorId : undefined;
const identityActorId = actor && actor?.type !== ActorType.USER ? actor?.actorId : undefined;
const identityActorId = actor && actor?.type === ActorType.IDENTITY ? actor?.actorId : undefined;
const actorType = actor?.type || ActorType.PLATFORM;
const sanitizedInputSecrets = inputSecrets.map(
@ -300,7 +312,15 @@ export const fnSecretBulkUpdate = async ({
tx
);
return newSecrets.map((secret) => ({ ...secret, _id: secret.id }));
const secretsWithTags = await secretDAL.find(
{
$in: {
[`${TableName.SecretV2}.id` as "id"]: newSecrets.map((s) => s.id)
}
},
{ tx }
);
return secretsWithTags.map((secret) => ({ ...secret, _id: secret.id }));
};
export const fnSecretBulkDelete = async ({
@ -533,7 +553,7 @@ export const expandSecretReferencesFactory = ({
const referredValue = await fetchSecret(environment, secretPath, secretKey);
if (!canExpandValue(environment, secretPath, secretKey, referredValue.tags))
throw new ForbiddenRequestError({
message: `You are attempting to reference secret named ${secretKey} from environment ${environment} in path ${secretPath} which you do not have access to.`
message: `You are attempting to reference secret named ${secretKey} from environment ${environment} in path ${secretPath} which you do not have access to read value on.`
});
const cacheKey = getCacheUniqueKey(environment, secretPath);
@ -552,7 +572,7 @@ export const expandSecretReferencesFactory = ({
const referedValue = await fetchSecret(secretReferenceEnvironment, secretReferencePath, secretReferenceKey);
if (!canExpandValue(secretReferenceEnvironment, secretReferencePath, secretReferenceKey, referedValue.tags))
throw new ForbiddenRequestError({
message: `You are attempting to reference secret named ${secretReferenceKey} from environment ${secretReferenceEnvironment} in path ${secretReferencePath} which you do not have access to.`
message: `You are attempting to reference secret named ${secretReferenceKey} from environment ${secretReferenceEnvironment} in path ${secretReferencePath} which you do not have access to read value on.`
});
const cacheKey = getCacheUniqueKey(secretReferenceEnvironment, secretReferencePath);
@ -646,13 +666,13 @@ export const reshapeBridgeSecret = (
name: string;
}[];
secretMetadata?: ResourceMetadataDTO;
}
},
secretValueHidden: boolean
) => ({
secretKey: secret.key,
secretPath,
workspace: workspaceId,
environment,
secretValue: secret.value || "",
secretComment: secret.comment || "",
version: secret.version,
type: secret.type,
@ -674,5 +694,15 @@ export const reshapeBridgeSecret = (
metadata: secret.metadata,
secretMetadata: secret.secretMetadata,
createdAt: secret.createdAt,
updatedAt: secret.updatedAt
updatedAt: secret.updatedAt,
...(secretValueHidden
? {
secretValue: INFISICAL_SECRET_VALUE_HIDDEN_MASK,
secretValueHidden: true
}
: {
secretValue: secret.value || "",
secretValueHidden: false
})
});

File diff suppressed because it is too large Load Diff

@ -1,6 +1,7 @@
import { Knex } from "knex";
import { SecretType, TSecretsV2, TSecretsV2Insert, TSecretsV2Update } from "@app/db/schemas";
import { ProjectPermissionSecretActions } from "@app/ee/services/permission/project-permission";
import { OrderByDirection, TProjectPermission } from "@app/lib/types";
import { TProjectDALFactory } from "@app/services/project/project-dal";
import { SecretsOrderBy } from "@app/services/secret/secret-types";
@ -36,6 +37,8 @@ export type TGetSecretsDTO = {
includeImports?: boolean;
recursive?: boolean;
tagSlugs?: string[];
viewSecretValue: boolean;
throwOnMissingReadValuePermission?: boolean;
metadataFilter?: {
key?: string;
value?: string;
@ -48,6 +51,11 @@ export type TGetSecretsDTO = {
keys?: string[];
} & TProjectPermission;
export type TGetSecretsMissingReadValuePermissionDTO = Omit<
TGetSecretsDTO,
"viewSecretValue" | "recursive" | "expandSecretReferences"
>;
export type TGetASecretDTO = {
secretName: string;
path: string;
@ -57,6 +65,7 @@ export type TGetASecretDTO = {
includeImports?: boolean;
version?: number;
projectId: string;
viewSecretValue: boolean;
} & Omit<TProjectPermission, "projectId">;
export type TCreateSecretDTO = TProjectPermission & {
@ -164,9 +173,9 @@ export type TFnSecretBulkInsert = {
}
>;
resourceMetadataDAL: Pick<TResourceMetadataDALFactory, "insertMany">;
secretDAL: Pick<TSecretV2BridgeDALFactory, "insertMany" | "upsertSecretReferences">;
secretDAL: Pick<TSecretV2BridgeDALFactory, "insertMany" | "upsertSecretReferences" | "find">;
secretVersionDAL: Pick<TSecretVersionV2DALFactory, "insertMany">;
secretTagDAL: Pick<TSecretTagDALFactory, "saveTagsToSecretV2">;
secretTagDAL: Pick<TSecretTagDALFactory, "saveTagsToSecretV2" | "find">;
secretVersionTagDAL: Pick<TSecretVersionV2TagDALFactory, "insertMany">;
actor?: {
type: string;
@ -192,9 +201,9 @@ export type TFnSecretBulkUpdate = {
data: TRequireReferenceIfValue & { tags?: string[]; secretMetadata?: ResourceMetadataDTO };
}[];
resourceMetadataDAL: Pick<TResourceMetadataDALFactory, "insertMany" | "delete">;
secretDAL: Pick<TSecretV2BridgeDALFactory, "bulkUpdate" | "upsertSecretReferences">;
secretDAL: Pick<TSecretV2BridgeDALFactory, "bulkUpdate" | "upsertSecretReferences" | "find">;
secretVersionDAL: Pick<TSecretVersionV2DALFactory, "insertMany">;
secretTagDAL: Pick<TSecretTagDALFactory, "saveTagsToSecretV2" | "deleteTagsToSecretV2">;
secretTagDAL: Pick<TSecretTagDALFactory, "saveTagsToSecretV2" | "deleteTagsToSecretV2" | "find">;
secretVersionTagDAL: Pick<TSecretVersionV2TagDALFactory, "insertMany">;
actor?: {
type: string;
@ -340,4 +349,12 @@ export type TGetSecretsRawByFolderMappingsDTO = {
folderMappings: { folderId: string; path: string; environment: string }[];
userId: string;
filters: TFindSecretsByFolderIdsFilter;
filterByAction?: ProjectPermissionSecretActions.DescribeSecret | ProjectPermissionSecretActions.ReadValue;
};
export type TGetAccessibleSecretsDTO = {
environment: string;
projectId: string;
secretPath: string;
filterByAction: ProjectPermissionSecretActions.DescribeSecret | ProjectPermissionSecretActions.ReadValue;
} & TProjectPermission;

@ -2,9 +2,9 @@
import { Knex } from "knex";
import { TDbClient } from "@app/db";
import { TableName, TSecretVersionsV2, TSecretVersionsV2Update } from "@app/db/schemas";
import { SecretVersionsV2Schema, TableName, TSecretVersionsV2, TSecretVersionsV2Update } from "@app/db/schemas";
import { BadRequestError, DatabaseError } from "@app/lib/errors";
import { ormify, selectAllTableCols, TFindOpt } from "@app/lib/knex";
import { ormify, selectAllTableCols, sqlNestRelationships, TFindOpt } from "@app/lib/knex";
import { logger } from "@app/lib/logger";
import { QueueName } from "@app/queue";
@ -13,6 +13,58 @@ export type TSecretVersionV2DALFactory = ReturnType<typeof secretVersionV2Bridge
export const secretVersionV2BridgeDALFactory = (db: TDbClient) => {
const secretVersionV2Orm = ormify(db, TableName.SecretVersionV2);
const findBySecretId = async (secretId: string, { offset, limit, sort, tx }: TFindOpt<TSecretVersionsV2> = {}) => {
try {
const query = (tx || db.replicaNode())(TableName.SecretVersionV2)
.where(`${TableName.SecretVersionV2}.secretId`, secretId)
.leftJoin(TableName.SecretV2, `${TableName.SecretVersionV2}.secretId`, `${TableName.SecretV2}.id`)
.leftJoin(
TableName.SecretV2JnTag,
`${TableName.SecretV2}.id`,
`${TableName.SecretV2JnTag}.${TableName.SecretV2}Id`
)
.leftJoin(
TableName.SecretTag,
`${TableName.SecretV2JnTag}.${TableName.SecretTag}Id`,
`${TableName.SecretTag}.id`
)
.select(selectAllTableCols(TableName.SecretVersionV2))
.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"));
if (limit) void query.limit(limit);
if (offset) void query.offset(offset);
if (sort) {
void query.orderBy(sort.map(([column, order, nulls]) => ({ column: column as string, order, nulls })));
}
const docs = await query;
const data = sqlNestRelationships({
data: docs,
key: "id",
parentMapper: (el) => ({ _id: el.id, ...SecretVersionsV2Schema.parse(el) }),
childrenMapper: [
{
key: "tagId",
label: "tags" as const,
mapper: ({ tagId: id, tagColor: color, tagSlug: slug }) => ({
id,
color,
slug,
name: slug
})
}
]
});
return data;
} catch (error) {
throw new DatabaseError({ error, name: `${TableName.SecretVersionV2}: FindBySecretId` });
}
};
// This will fetch all latest secret versions from a folder
const findLatestVersionByFolderId = async (folderId: string, tx?: Knex) => {
try {
@ -135,6 +187,17 @@ export const secretVersionV2BridgeDALFactory = (db: TDbClient) => {
`${TableName.SecretVersionV2}.userActorId`
)
.leftJoin(TableName.Identity, `${TableName.Identity}.id`, `${TableName.SecretVersionV2}.identityActorId`)
.leftJoin(TableName.SecretV2, `${TableName.SecretVersionV2}.secretId`, `${TableName.SecretV2}.id`)
.leftJoin(
TableName.SecretV2JnTag,
`${TableName.SecretV2}.id`,
`${TableName.SecretV2JnTag}.${TableName.SecretV2}Id`
)
.leftJoin(
TableName.SecretTag,
`${TableName.SecretV2JnTag}.${TableName.SecretTag}Id`,
`${TableName.SecretTag}.id`
)
.where((qb) => {
void qb.where(`${TableName.SecretVersionV2}.secretId`, secretId);
void qb.where(`${TableName.ProjectMembership}.projectId`, projectId);
@ -145,9 +208,12 @@ export const secretVersionV2BridgeDALFactory = (db: TDbClient) => {
})
.select(
selectAllTableCols(TableName.SecretVersionV2),
`${TableName.Users}.username as userActorName`,
`${TableName.Identity}.name as identityActorName`,
`${TableName.ProjectMembership}.id as membershipId`
db.ref("username").withSchema(TableName.Users).as("userActorName"),
db.ref("name").withSchema(TableName.Identity).as("identityActorName"),
db.ref("id").withSchema(TableName.ProjectMembership).as("membershipId"),
db.ref("id").withSchema(TableName.SecretTag).as("tagId"),
db.ref("color").withSchema(TableName.SecretTag).as("tagColor"),
db.ref("slug").withSchema(TableName.SecretTag).as("tagSlug")
);
if (limit) void query.limit(limit);
@ -162,14 +228,33 @@ export const secretVersionV2BridgeDALFactory = (db: TDbClient) => {
);
}
const docs: Array<
TSecretVersionsV2 & {
userActorName: string | undefined | null;
identityActorName: string | undefined | null;
membershipId: string | undefined | null;
}
> = await query;
return docs;
const docs = await query;
const data = sqlNestRelationships({
data: docs,
key: "id",
parentMapper: (el) => ({
_id: el.id,
...SecretVersionsV2Schema.parse(el),
userActorName: el.userActorName,
identityActorName: el.identityActorName,
membershipId: el.membershipId
}),
childrenMapper: [
{
key: "tagId",
label: "tags" as const,
mapper: ({ tagId: id, tagColor: color, tagSlug: slug }) => ({
id,
color,
slug,
name: slug
})
}
]
});
return data;
} catch (error) {
throw new DatabaseError({ error, name: "FindVersionsBySecretIdWithActors" });
}
@ -181,6 +266,7 @@ export const secretVersionV2BridgeDALFactory = (db: TDbClient) => {
findLatestVersionMany,
bulkUpdate,
findLatestVersionByFolderId,
findVersionsBySecretIdWithActors
findVersionsBySecretIdWithActors,
findBySecretId
};
};

@ -169,6 +169,48 @@ export const secretDALFactory = (db: TDbClient) => {
}
};
const findManySecretsWithTags = async (
filter: {
secretIds: string[];
type: SecretType;
},
tx?: Knex
) => {
try {
const secrets = await (tx || db.replicaNode())(TableName.Secret)
.whereIn(`${TableName.Secret}.id` as "id", filter.secretIds)
.where("type", filter.type)
.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"));
const data = sqlNestRelationships({
data: secrets,
key: "id",
parentMapper: (el) => ({ _id: el.id, ...SecretsSchema.parse(el) }),
childrenMapper: [
{
key: "tagId",
label: "tags" as const,
mapper: ({ tagId: id, tagColor: color, tagSlug: slug }) => ({
id,
color,
slug,
name: slug
})
}
]
});
return data;
} catch (error) {
throw new DatabaseError({ error, name: "get many secrets with 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)
@ -443,6 +485,7 @@ export const secretDALFactory = (db: TDbClient) => {
upsertSecretReferences,
findReferencedSecretReferences,
findAllProjectSecretValues,
pruneSecretReminders
pruneSecretReminders,
findManySecretsWithTags
};
};

@ -1,5 +1,4 @@
/* eslint-disable no-await-in-loop */
import { subject } from "@casl/ability";
import path from "path";
import {
@ -12,8 +11,9 @@ import {
TSecretFolders,
TSecrets
} from "@app/db/schemas";
import { hasSecretReadValueOrDescribePermission } from "@app/ee/services/permission/permission-fns";
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service";
import { ProjectPermissionActions, ProjectPermissionSub } from "@app/ee/services/permission/project-permission";
import { ProjectPermissionSecretActions } from "@app/ee/services/permission/project-permission";
import { getConfig } from "@app/lib/config/env";
import {
buildSecretBlindIndexFromName,
@ -51,6 +51,8 @@ import {
TUpdateManySecretsRawFnFactory
} from "./secret-types";
export const INFISICAL_SECRET_VALUE_HIDDEN_MASK = "<hidden-by-infisical>";
export const generateSecretBlindIndexBySalt = async (secretName: string, secretBlindIndexDoc: TSecretBlindIndexes) => {
const appCfg = getConfig();
const secretBlindIndex = await buildSecretBlindIndexFromName({
@ -189,13 +191,10 @@ export const recursivelyGetSecretPaths = ({
// 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)
hasSecretReadValueOrDescribePermission(permission, ProjectPermissionSecretActions.ReadValue, {
environment,
secretPath: folder.path
}) && folder.path.startsWith(currentPath === "/" ? "" : currentPath)
);
return allowedPaths;
@ -344,6 +343,7 @@ export const interpolateSecrets = ({ projectId, secretEncKey, secretDAL, folderD
export const decryptSecretRaw = (
secret: TSecrets & {
secretValueHidden: boolean;
workspace: string;
environment: string;
secretPath: string;
@ -362,12 +362,14 @@ export const decryptSecretRaw = (
key
});
const secretValue = decryptSymmetric128BitHexKeyUTF8({
ciphertext: secret.secretValueCiphertext,
iv: secret.secretValueIV,
tag: secret.secretValueTag,
key
});
const secretValue = !secret.secretValueHidden
? decryptSymmetric128BitHexKeyUTF8({
ciphertext: secret.secretValueCiphertext,
iv: secret.secretValueIV,
tag: secret.secretValueTag,
key
})
: INFISICAL_SECRET_VALUE_HIDDEN_MASK;
let secretComment = "";
@ -385,6 +387,7 @@ export const decryptSecretRaw = (
secretPath: secret.secretPath,
workspace: secret.workspace,
environment: secret.environment,
secretValueHidden: secret.secretValueHidden,
secretValue,
secretComment,
version: secret.version,
@ -1198,3 +1201,23 @@ export const fnDeleteProjectSecretReminders = async (
}
}
};
export const conditionallyHideSecretValue = (
shouldHideValue: boolean,
{
secretValueCiphertext,
secretValueIV,
secretValueTag
}: {
secretValueCiphertext: string;
secretValueIV: string;
secretValueTag: string;
}
) => {
return {
secretValueCiphertext: shouldHideValue ? INFISICAL_SECRET_VALUE_HIDDEN_MASK : secretValueCiphertext,
secretValueIV: shouldHideValue ? INFISICAL_SECRET_VALUE_HIDDEN_MASK : secretValueIV,
secretValueTag: shouldHideValue ? INFISICAL_SECRET_VALUE_HIDDEN_MASK : secretValueTag,
secretValueHidden: shouldHideValue
};
};

@ -61,6 +61,7 @@ import { SmtpTemplates, TSmtpService } from "../smtp/smtp-service";
import { TUserDALFactory } from "../user/user-dal";
import { TWebhookDALFactory } from "../webhook/webhook-dal";
import { fnTriggerWebhook } from "../webhook/webhook-fns";
import { WebhookEvents } from "../webhook/webhook-types";
import { TSecretDALFactory } from "./secret-dal";
import { interpolateSecrets } from "./secret-fns";
import {
@ -402,7 +403,8 @@ export const secretQueueFactory = ({
expandSecretReferences,
secretImportDAL,
secretImports,
hasSecretAccess: () => true
hasSecretAccess: () => true,
viewSecretValue: true
});
for (let i = importedSecrets.length - 1; i >= 0; i -= 1) {
@ -623,7 +625,14 @@ export const secretQueueFactory = ({
await queueService.queue(
QueueName.SecretWebhook,
QueueJobs.SecWebhook,
{ environment, projectId, secretPath },
{
type: WebhookEvents.SecretModified,
payload: {
environment,
projectId,
secretPath
}
},
{
jobId: `secret-webhook-${environment}-${projectId}-${secretPath}`,
removeOnFail: { count: 5 },
@ -1055,6 +1064,8 @@ export const secretQueueFactory = ({
const organization = await orgDAL.findOrgByProjectId(projectId);
const project = await projectDAL.findById(projectId);
const secret = await secretV2BridgeDAL.findById(data.secretId);
const [folder] = await folderDAL.findSecretPathByFolderIds(project.id, [secret.folderId]);
if (!organization) {
logger.info(`secretReminderQueue.process: [secretDocument=${data.secretId}] no organization found`);
@ -1083,6 +1094,19 @@ export const secretQueueFactory = ({
organizationName: organization.name
}
});
await queueService.queue(QueueName.SecretWebhook, QueueJobs.SecWebhook, {
type: WebhookEvents.SecretReminderExpired,
payload: {
projectName: project.name,
projectId: project.id,
secretPath: folder?.path,
environment: folder?.environmentSlug || "",
reminderNote: data.note,
secretName: secret?.key,
secretId: data.secretId
}
});
});
const startSecretV2Migration = async (projectId: string) => {
@ -1490,14 +1514,17 @@ export const secretQueueFactory = ({
queueService.start(QueueName.SecretWebhook, async (job) => {
const { decryptor: secretManagerDecryptor } = await kmsService.createCipherPairWithDataKey({
type: KmsDataKey.SecretManager,
projectId: job.data.projectId
projectId: job.data.payload.projectId
});
await fnTriggerWebhook({
...job.data,
projectId: job.data.payload.projectId,
environment: job.data.payload.environment,
secretPath: job.data.payload.secretPath || "/",
projectEnvDAL,
webhookDAL,
projectDAL,
webhookDAL,
event: job.data,
secretManagerDecryptor: (value) => secretManagerDecryptor({ cipherTextBlob: value }).toString()
});
});

@ -6,14 +6,23 @@ import {
ActionProjectType,
ProjectMembershipRole,
ProjectUpgradeStatus,
ProjectVersion,
SecretEncryptionAlgo,
SecretKeyEncoding,
SecretsSchema,
SecretType
} from "@app/db/schemas";
import { TLicenseServiceFactory } from "@app/ee/services/license/license-service";
import {
hasSecretReadValueOrDescribePermission,
throwIfMissingSecretReadValueOrDescribePermission
} from "@app/ee/services/permission/permission-fns";
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service";
import { ProjectPermissionActions, ProjectPermissionSub } from "@app/ee/services/permission/project-permission";
import {
ProjectPermissionActions,
ProjectPermissionSecretActions,
ProjectPermissionSub
} from "@app/ee/services/permission/project-permission";
import { TSecretApprovalPolicyServiceFactory } from "@app/ee/services/secret-approval-policy/secret-approval-policy-service";
import { TSecretApprovalRequestDALFactory } from "@app/ee/services/secret-approval-request/secret-approval-request-dal";
import { TSecretApprovalRequestSecretDALFactory } from "@app/ee/services/secret-approval-request/secret-approval-request-secret-dal";
@ -48,6 +57,7 @@ import { TSecretV2BridgeServiceFactory } from "../secret-v2-bridge/secret-v2-bri
import { TGetSecretReferencesTreeDTO } from "../secret-v2-bridge/secret-v2-bridge-types";
import { TSecretDALFactory } from "./secret-dal";
import {
conditionallyHideSecretValue,
decryptSecretRaw,
fnSecretBlindIndexCheck,
fnSecretBulkDelete,
@ -71,6 +81,7 @@ import {
TDeleteManySecretRawDTO,
TDeleteSecretDTO,
TDeleteSecretRawDTO,
TGetAccessibleSecretsDTO,
TGetASecretByIdRawDTO,
TGetASecretDTO,
TGetASecretRawDTO,
@ -205,7 +216,7 @@ export const secretServiceFactory = ({
});
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Create,
ProjectPermissionSecretActions.Create,
subject(ProjectPermissionSub.Secrets, { environment, secretPath: path })
);
@ -323,7 +334,7 @@ export const secretServiceFactory = ({
});
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Edit,
ProjectPermissionSecretActions.Edit,
subject(ProjectPermissionSub.Secrets, { environment, secretPath: path })
);
@ -445,7 +456,23 @@ export const secretServiceFactory = ({
environmentSlug: folder.environment.slug
});
}
return { ...updatedSecret[0], workspace: projectId, environment, secretPath: path };
const secretValueHidden = !hasSecretReadValueOrDescribePermission(
permission,
ProjectPermissionSecretActions.ReadValue,
{
environment,
secretPath: path
}
);
return {
...updatedSecret[0],
...conditionallyHideSecretValue(secretValueHidden, updatedSecret[0]),
workspace: projectId,
environment,
secretPath: path
};
};
const deleteSecret = async ({
@ -468,7 +495,7 @@ export const secretServiceFactory = ({
});
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Delete,
ProjectPermissionSecretActions.Delete,
subject(ProjectPermissionSub.Secrets, { environment, secretPath: path })
);
@ -541,7 +568,23 @@ export const secretServiceFactory = ({
});
}
return { ...deletedSecret[0], _id: deletedSecret[0].id, workspace: projectId, environment, secretPath: path };
const secretValueHidden = !hasSecretReadValueOrDescribePermission(
permission,
ProjectPermissionSecretActions.ReadValue,
{
environment,
secretPath: path
}
);
return {
...deletedSecret[0],
...conditionallyHideSecretValue(secretValueHidden, deletedSecret[0]),
_id: deletedSecret[0].id,
workspace: projectId,
environment,
secretPath: path
};
};
const getSecrets = async ({
@ -589,10 +632,10 @@ export const secretServiceFactory = ({
paths = deepPaths.map(({ folderId, path: p }) => ({ folderId, path: p }));
} else {
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Read,
subject(ProjectPermissionSub.Secrets, { environment, secretPath: path })
);
throwIfMissingSecretReadValueOrDescribePermission(permission, ProjectPermissionSecretActions.ReadValue, {
environment,
secretPath: path
});
const folder = await folderDAL.findBySecretPath(projectId, environment, path);
if (!folder) return { secrets: [], imports: [] };
@ -614,13 +657,10 @@ export const secretServiceFactory = ({
// if its service token allow full access over imported one
actor === ActorType.SERVICE
? true
: permission.can(
ProjectPermissionActions.Read,
subject(ProjectPermissionSub.Secrets, {
environment: importEnv.slug,
secretPath: importPath
})
)
: hasSecretReadValueOrDescribePermission(permission, ProjectPermissionSecretActions.ReadValue, {
environment: importEnv.slug,
secretPath: importPath
})
);
const importedSecrets = await fnSecretsFromImports({
allowedImports,
@ -671,10 +711,11 @@ export const secretServiceFactory = ({
actorOrgId,
actionProjectType: ActionProjectType.SecretManager
});
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Read,
subject(ProjectPermissionSub.Secrets, { environment, secretPath: path })
);
throwIfMissingSecretReadValueOrDescribePermission(permission, ProjectPermissionSecretActions.ReadValue, {
environment,
secretPath: path
});
const folder = await folderDAL.findBySecretPath(projectId, environment, path);
if (!folder)
throw new NotFoundError({
@ -721,14 +762,12 @@ export const secretServiceFactory = ({
// if its service token allow full access over imported one
actor === ActorType.SERVICE
? true
: permission.can(
ProjectPermissionActions.Read,
subject(ProjectPermissionSub.Secrets, {
environment: importEnv.slug,
secretPath: importPath
})
)
: hasSecretReadValueOrDescribePermission(permission, ProjectPermissionSecretActions.ReadValue, {
environment: importEnv.slug,
secretPath: importPath
})
);
const importedSecrets = await fnSecretsFromImports({
allowedImports,
secretDAL,
@ -740,6 +779,7 @@ export const secretServiceFactory = ({
if (secretBlindIndex === importedSecrets[i].secrets[j].secretBlindIndex) {
return {
...importedSecrets[i].secrets[j],
secretValueHidden: false,
workspace: projectId,
environment: importedSecrets[i].environment,
secretPath: importedSecrets[i].secretPath
@ -750,7 +790,13 @@ export const secretServiceFactory = ({
}
if (!secret) throw new NotFoundError({ message: `Secret with name '${secretName}' not found` });
return { ...secret, workspace: projectId, environment, secretPath: path };
return {
...secret,
secretValueHidden: false, // Always false because we check permission at the beginning of the function
workspace: projectId,
environment,
secretPath: path
};
};
const createManySecret = async ({
@ -772,7 +818,7 @@ export const secretServiceFactory = ({
actionProjectType: ActionProjectType.SecretManager
});
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Create,
ProjectPermissionSecretActions.Create,
subject(ProjectPermissionSub.Secrets, { environment, secretPath: path })
);
@ -860,7 +906,7 @@ export const secretServiceFactory = ({
});
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Edit,
ProjectPermissionSecretActions.Edit,
subject(ProjectPermissionSub.Secrets, { environment, secretPath: path })
);
@ -902,8 +948,8 @@ export const secretServiceFactory = ({
if (tagIds.length !== tags.length) throw new NotFoundError({ message: "One or more tags not found" });
const references = await getSecretReference(projectId);
const secrets = await secretDAL.transaction(async (tx) =>
fnSecretBulkUpdate({
const secrets = await secretDAL.transaction(async (tx) => {
const updatedSecrets = await fnSecretBulkUpdate({
folderId,
projectId,
tx,
@ -933,8 +979,22 @@ export const secretServiceFactory = ({
secretVersionDAL,
secretTagDAL,
secretVersionTagDAL
})
);
});
const secretValueHidden = !hasSecretReadValueOrDescribePermission(
permission,
ProjectPermissionSecretActions.ReadValue,
{
environment,
secretPath: path
}
);
return updatedSecrets.map((secret) => ({
...secret,
...conditionallyHideSecretValue(secretValueHidden, secret)
}));
});
await snapshotService.performSnapshot(folderId);
await secretQueueService.syncSecrets({
@ -968,7 +1028,7 @@ export const secretServiceFactory = ({
actionProjectType: ActionProjectType.SecretManager
});
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Delete,
ProjectPermissionSecretActions.Delete,
subject(ProjectPermissionSub.Secrets, { environment, secretPath: path })
);
@ -1019,8 +1079,19 @@ export const secretServiceFactory = ({
});
}
}
const secretValueHidden = !hasSecretReadValueOrDescribePermission(
permission,
ProjectPermissionSecretActions.ReadValue,
{
environment,
secretPath: path
}
);
return secrets;
return secrets.map((secret) => ({
...secret,
...conditionallyHideSecretValue(secretValueHidden, secret)
}));
});
await snapshotService.performSnapshot(folderId);
@ -1181,6 +1252,7 @@ export const secretServiceFactory = ({
secretName,
path: secretPath,
environment,
viewSecretValue: false,
type: "shared"
});
@ -1195,12 +1267,25 @@ export const secretServiceFactory = ({
| (typeof groupPermissions)[number]
) => {
const allowedActions = [
ProjectPermissionActions.Read,
ProjectPermissionActions.Delete,
ProjectPermissionActions.Create,
ProjectPermissionActions.Edit
].filter((action) =>
entityPermission.permission.can(
ProjectPermissionSecretActions.DescribeSecret,
ProjectPermissionSecretActions.ReadValue,
ProjectPermissionSecretActions.Delete,
ProjectPermissionSecretActions.Create,
ProjectPermissionSecretActions.Edit
].filter((action) => {
if (
action === ProjectPermissionSecretActions.DescribeSecret ||
action === ProjectPermissionSecretActions.ReadValue
) {
return hasSecretReadValueOrDescribePermission(entityPermission.permission, action, {
environment,
secretPath,
secretName,
secretTags: secret?.tags?.map((el) => el.slug)
});
}
return entityPermission.permission.can(
action,
subject(ProjectPermissionSub.Secrets, {
environment,
@ -1208,8 +1293,8 @@ export const secretServiceFactory = ({
secretName,
secretTags: secret?.tags?.map((el) => el.slug)
})
)
);
);
});
return {
...entityPermission,
@ -1228,6 +1313,39 @@ export const secretServiceFactory = ({
return { users: usersWithAccess, identities: identitiesWithAccess, groups: groupsWithAccess };
};
const getAccessibleSecrets = async ({
projectId,
secretPath,
actor,
actorId,
actorOrgId,
actorAuthMethod,
environment,
filterByAction
}: TGetAccessibleSecretsDTO) => {
const { shouldUseSecretV2Bridge } = await projectBotService.getBotKey(projectId);
if (!shouldUseSecretV2Bridge) {
throw new BadRequestError({
message: "Project version does not support this endpoint.",
name: "ProjectVersionNotSupported"
});
}
const secrets = await secretV2BridgeService.getAccessibleSecrets({
projectId,
secretPath,
environment,
filterByAction,
actor,
actorId,
actorOrgId,
actorAuthMethod
});
return secrets;
};
const getSecretsRaw = async ({
projectId,
path,
@ -1235,11 +1353,13 @@ export const secretServiceFactory = ({
actorId,
actorOrgId,
actorAuthMethod,
viewSecretValue,
environment,
includeImports,
expandSecretReferences,
recursive,
tagSlugs = [],
throwOnMissingReadValuePermission = true,
...paramsV2
}: TGetSecretsRawDTO) => {
const { botKey, shouldUseSecretV2Bridge } = await projectBotService.getBotKey(projectId);
@ -1250,6 +1370,8 @@ export const secretServiceFactory = ({
actorId,
actor,
actorOrgId,
viewSecretValue,
throwOnMissingReadValuePermission,
environment,
path,
recursive,
@ -1258,6 +1380,7 @@ export const secretServiceFactory = ({
tagSlugs,
...paramsV2
});
return { secrets, imports };
}
@ -1286,14 +1409,20 @@ export const secretServiceFactory = ({
recursive
});
const decryptedSecrets = secrets.map((el) => decryptSecretRaw(el, botKey));
const decryptedSecrets = secrets.map((el) => decryptSecretRaw({ ...el, secretValueHidden: false }, botKey));
const filteredSecrets = tagSlugs.length
? decryptedSecrets.filter((secret) => Boolean(secret.tags?.find((el) => tagSlugs.includes(el.slug))))
: decryptedSecrets;
const processedImports = (imports || [])?.map(({ secrets: importedSecrets, ...el }) => {
const decryptedImportSecrets = importedSecrets.map((sec) =>
decryptSecretRaw(
{ ...sec, environment: el.environment, workspace: projectId, secretPath: el.secretPath },
{
...sec,
environment: el.environment,
workspace: projectId,
secretPath: el.secretPath,
secretValueHidden: false
},
botKey
)
);
@ -1304,6 +1433,7 @@ export const secretServiceFactory = ({
const importedEntries = decryptedImportSecrets.reduce(
(
accum: {
secretValueHidden: boolean;
secretKey: string;
secretPath: string;
workspace: string;
@ -1347,6 +1477,7 @@ export const secretServiceFactory = ({
Object.keys(secretsGroupByPath).map((groupedPath) =>
Promise.allSettled(
secretsGroupByPath[groupedPath].map(async (decryptedSecret, index) => {
if (decryptedSecret.secretValueHidden) return;
const expandedSecretValue = await expandSecret({
value: decryptedSecret.secretValue,
secretPath: groupedPath,
@ -1363,6 +1494,7 @@ export const secretServiceFactory = ({
processedImports.map((processedImport) =>
Promise.allSettled(
processedImport.secrets.map(async (decryptedSecret, index) => {
if (decryptedSecret.secretValueHidden) return;
const expandedSecretValue = await expandSecret({
value: decryptedSecret.secretValue,
secretPath: path,
@ -1400,6 +1532,7 @@ export const secretServiceFactory = ({
path,
actor,
environment,
viewSecretValue,
projectId: workspaceId,
expandSecretReferences,
projectSlug,
@ -1419,6 +1552,7 @@ export const secretServiceFactory = ({
includeImports,
actorAuthMethod,
path,
viewSecretValue,
actorOrgId,
actor,
actorId,
@ -1449,6 +1583,7 @@ export const secretServiceFactory = ({
message: `Project bot for project with ID '${projectId}' not found. Please upgrade your project.`,
name: "bot_not_found_error"
});
const decryptedSecret = decryptSecretRaw(encryptedSecret, botKey);
if (expandSecretReferences) {
@ -1467,7 +1602,10 @@ export const secretServiceFactory = ({
decryptedSecret.secretValue = expandedSecretValue || "";
}
return { secretMetadata: undefined, ...decryptedSecret };
return {
secretMetadata: undefined,
...decryptedSecret
};
};
const createSecretRaw = async ({
@ -1618,7 +1756,16 @@ export const secretServiceFactory = ({
tags: tagIds
});
return { type: SecretProtectionType.Direct as const, secret: decryptSecretRaw(secret, botKey) };
return {
type: SecretProtectionType.Direct as const,
secret: decryptSecretRaw(
{
...secret,
secretValueHidden: false
},
botKey
)
};
};
const updateSecretRaw = async ({
@ -2014,7 +2161,7 @@ export const secretServiceFactory = ({
return {
type: SecretProtectionType.Direct as const,
secrets: secrets.map((secret) =>
decryptSecretRaw({ ...secret, workspace: projectId, environment, secretPath }, botKey)
decryptSecretRaw({ ...secret, workspace: projectId, environment, secretPath, secretValueHidden: false }, botKey)
)
};
};
@ -2303,6 +2450,12 @@ export const secretServiceFactory = ({
const folder = await folderDAL.findById(secret.folderId);
if (!folder) throw new NotFoundError({ message: `Folder with ID '${secret.folderId}' not found` });
const [folderWithPath] = await folderDAL.findSecretPathByFolderIds(folder.projectId, [folder.id]);
if (!folderWithPath) {
throw new NotFoundError({ message: `Folder with ID '${folder.id}' not found` });
}
const { botKey } = await projectBotService.getBotKey(folder.projectId);
if (!botKey)
throw new NotFoundError({ message: `Project bot for project with ID '${folder.projectId}' not found` });
@ -2316,18 +2469,43 @@ export const secretServiceFactory = ({
actionProjectType: ActionProjectType.SecretManager
});
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.SecretRollback);
const secretVersions = await secretVersionDAL.find({ secretId }, { offset, limit, sort: [["createdAt", "desc"]] });
return secretVersions.map((el) =>
decryptSecretRaw(
const secretVersions = await secretVersionDAL.findBySecretId(secretId, {
offset,
limit,
sort: [["createdAt", "desc"]]
});
return secretVersions.map((el) => {
const secretKey = decryptSymmetric128BitHexKeyUTF8({
ciphertext: secret.secretKeyCiphertext,
iv: secret.secretKeyIV,
tag: secret.secretKeyTag,
key: botKey
});
const secretValueHidden = !hasSecretReadValueOrDescribePermission(
permission,
ProjectPermissionSecretActions.ReadValue,
{
environment: folder.environment.envSlug,
secretPath: folderWithPath.path,
secretName: secretKey,
...(el.tags?.length && {
secretTags: el.tags.map((tag) => tag.slug)
})
}
);
return decryptSecretRaw(
{
secretValueHidden,
...el,
workspace: folder.projectId,
environment: folder.environment.envSlug,
secretPath: "/"
secretPath: folderWithPath.path
},
botKey
)
);
);
});
};
const attachTags = async ({
@ -2353,7 +2531,7 @@ export const secretServiceFactory = ({
});
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Edit,
ProjectPermissionSecretActions.Edit,
subject(ProjectPermissionSub.Secrets, { environment, secretPath })
);
@ -2459,7 +2637,7 @@ export const secretServiceFactory = ({
});
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Edit,
ProjectPermissionSecretActions.Edit,
subject(ProjectPermissionSub.Secrets, { environment, secretPath })
);
@ -2625,7 +2803,7 @@ export const secretServiceFactory = ({
message: `Project with slug '${projectSlug}' not found`
});
}
if (project.version === 3) {
if (project.version === ProjectVersion.V3) {
return secretV2BridgeService.moveSecrets({
sourceEnvironment,
sourceSecretPath,
@ -2650,30 +2828,6 @@ export const secretServiceFactory = ({
actionProjectType: ActionProjectType.SecretManager
});
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Delete,
subject(ProjectPermissionSub.Secrets, {
environment: sourceEnvironment,
secretPath: sourceSecretPath
})
);
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Create,
subject(ProjectPermissionSub.Secrets, {
environment: destinationEnvironment,
secretPath: destinationSecretPath
})
);
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Edit,
subject(ProjectPermissionSub.Secrets, {
environment: destinationEnvironment,
secretPath: destinationSecretPath
})
);
const { botKey } = await projectBotService.getBotKey(project.id);
if (!botKey) {
throw new NotFoundError({
@ -2701,11 +2855,9 @@ export const secretServiceFactory = ({
});
}
const sourceSecrets = await secretDAL.find({
const sourceSecrets = await secretDAL.findManySecretsWithTags({
type: SecretType.Shared,
$in: {
id: secretIds
}
secretIds
});
if (sourceSecrets.length !== secretIds.length) {
@ -2714,21 +2866,62 @@ export const secretServiceFactory = ({
});
}
const decryptedSourceSecrets = sourceSecrets.map((secret) => ({
...secret,
secretKey: decryptSymmetric128BitHexKeyUTF8({
const sourceActions = [
ProjectPermissionSecretActions.Delete,
ProjectPermissionSecretActions.DescribeSecret,
ProjectPermissionSecretActions.ReadValue
] as const;
const destinationActions = [ProjectPermissionSecretActions.Create, ProjectPermissionSecretActions.Edit] as const;
const decryptedSourceSecrets = sourceSecrets.map((secret) => {
const secretKey = decryptSymmetric128BitHexKeyUTF8({
ciphertext: secret.secretKeyCiphertext,
iv: secret.secretKeyIV,
tag: secret.secretKeyTag,
key: botKey
}),
secretValue: decryptSymmetric128BitHexKeyUTF8({
ciphertext: secret.secretValueCiphertext,
iv: secret.secretValueIV,
tag: secret.secretValueTag,
key: botKey
})
}));
});
for (const destinationAction of destinationActions) {
ForbiddenError.from(permission).throwUnlessCan(
destinationAction,
subject(ProjectPermissionSub.Secrets, {
environment: destinationEnvironment,
secretPath: destinationSecretPath
})
);
}
for (const sourceAction of sourceActions) {
if (
sourceAction === ProjectPermissionSecretActions.ReadValue ||
sourceAction === ProjectPermissionSecretActions.DescribeSecret
) {
throwIfMissingSecretReadValueOrDescribePermission(permission, sourceAction, {
environment: sourceEnvironment,
secretPath: sourceSecretPath
});
} else {
ForbiddenError.from(permission).throwUnlessCan(
sourceAction,
subject(ProjectPermissionSub.Secrets, {
environment: sourceEnvironment,
secretPath: sourceSecretPath
})
);
}
}
return {
...secret,
secretKey,
secretValue: decryptSymmetric128BitHexKeyUTF8({
ciphertext: secret.secretValueCiphertext,
iv: secret.secretValueIV,
tag: secret.secretValueTag,
key: botKey
})
};
});
let isSourceUpdated = false;
let isDestinationUpdated = false;
@ -3102,6 +3295,7 @@ export const secretServiceFactory = ({
getSecretReferenceTree,
getSecretsRawByFolderMappings,
getSecretAccessList,
getSecretByIdRaw
getSecretByIdRaw,
getAccessibleSecrets
};
};

@ -2,6 +2,7 @@ import { Knex } from "knex";
import { z } from "zod";
import { SecretType, TSecretBlindIndexes, TSecrets, TSecretsInsert, TSecretsUpdate } from "@app/db/schemas";
import { ProjectPermissionSecretActions } from "@app/ee/services/permission/project-permission";
import { OrderByDirection, TProjectPermission } from "@app/lib/types";
import { TProjectDALFactory } from "@app/services/project/project-dal";
import { TProjectBotDALFactory } from "@app/services/project-bot/project-bot-dal";
@ -180,10 +181,18 @@ export enum SecretsOrderBy {
Name = "name" // "key" for secrets but using name for use across resources
}
export type TGetAccessibleSecretsDTO = {
secretPath: string;
environment: string;
filterByAction: ProjectPermissionSecretActions.DescribeSecret | ProjectPermissionSecretActions.ReadValue;
} & TProjectPermission;
export type TGetSecretsRawDTO = {
expandSecretReferences?: boolean;
path: string;
environment: string;
viewSecretValue: boolean;
throwOnMissingReadValuePermission?: boolean;
includeImports?: boolean;
recursive?: boolean;
tagSlugs?: string[];
@ -209,6 +218,7 @@ export type TGetASecretRawDTO = {
secretName: string;
path: string;
environment: string;
viewSecretValue: boolean;
expandSecretReferences?: boolean;
type: "shared" | "personal";
includeImports?: boolean;
@ -417,7 +427,7 @@ export type TCreateManySecretsRawFnFactory = {
kmsService: Pick<TKmsServiceFactory, "createCipherPairWithDataKey">;
secretV2BridgeDAL: Pick<
TSecretV2BridgeDALFactory,
"insertMany" | "upsertSecretReferences" | "findBySecretKeys" | "bulkUpdate" | "deleteMany"
"insertMany" | "upsertSecretReferences" | "findBySecretKeys" | "bulkUpdate" | "deleteMany" | "find"
>;
secretVersionV2BridgeDAL: Pick<TSecretVersionV2DALFactory, "insertMany" | "findLatestVersionMany">;
secretVersionTagV2BridgeDAL: Pick<TSecretVersionV2TagDALFactory, "insertMany">;
@ -454,7 +464,7 @@ export type TUpdateManySecretsRawFnFactory = {
kmsService: Pick<TKmsServiceFactory, "createCipherPairWithDataKey">;
secretV2BridgeDAL: Pick<
TSecretV2BridgeDALFactory,
"insertMany" | "upsertSecretReferences" | "findBySecretKeys" | "bulkUpdate" | "deleteMany"
"insertMany" | "upsertSecretReferences" | "findBySecretKeys" | "bulkUpdate" | "deleteMany" | "find"
>;
secretVersionV2BridgeDAL: Pick<TSecretVersionV2DALFactory, "insertMany" | "findLatestVersionMany">;
secretVersionTagV2BridgeDAL: Pick<TSecretVersionV2TagDALFactory, "insertMany">;

@ -1,9 +1,9 @@
import { Knex } from "knex";
import { TDbClient } from "@app/db";
import { TableName, TSecretVersions, TSecretVersionsUpdate } from "@app/db/schemas";
import { SecretVersionsSchema, TableName, TSecretVersions, TSecretVersionsUpdate } from "@app/db/schemas";
import { BadRequestError, DatabaseError, NotFoundError } from "@app/lib/errors";
import { ormify, selectAllTableCols } from "@app/lib/knex";
import { ormify, selectAllTableCols, sqlNestRelationships, TFindOpt } from "@app/lib/knex";
import { logger } from "@app/lib/logger";
import { QueueName } from "@app/queue";
@ -12,6 +12,50 @@ export type TSecretVersionDALFactory = ReturnType<typeof secretVersionDALFactory
export const secretVersionDALFactory = (db: TDbClient) => {
const secretVersionOrm = ormify(db, TableName.SecretVersion);
const findBySecretId = async (secretId: string, { offset, limit, sort, tx }: TFindOpt<TSecretVersions> = {}) => {
try {
const query = (tx || db.replicaNode())(TableName.SecretVersion)
.where(`${TableName.SecretVersion}.secretId`, secretId)
.leftJoin(TableName.Secret, `${TableName.SecretVersion}.secretId`, `${TableName.Secret}.id`)
.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.SecretVersion))
.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"));
if (limit) void query.limit(limit);
if (offset) void query.offset(offset);
if (sort) {
void query.orderBy(sort.map(([column, order, nulls]) => ({ column: column as string, order, nulls })));
}
const docs = await query;
const data = sqlNestRelationships({
data: docs,
key: "id",
parentMapper: (el) => ({ _id: el.id, ...SecretVersionsSchema.parse(el) }),
childrenMapper: [
{
key: "tagId",
label: "tags" as const,
mapper: ({ tagId: id, tagColor: color, tagSlug: slug }) => ({
id,
color,
slug,
name: slug
})
}
]
});
return data;
} catch (error) {
throw new DatabaseError({ error, name: `${TableName.SecretVersion}: FindBySecretId` });
}
};
// This will fetch all latest secret versions from a folder
const findLatestVersionByFolderId = async (folderId: string, tx?: Knex) => {
try {
@ -149,6 +193,7 @@ export const secretVersionDALFactory = (db: TDbClient) => {
findLatestVersionMany,
bulkUpdate,
findLatestVersionByFolderId,
findBySecretId,
bulkUpdateNoVersionIncrement
};
};

@ -5,7 +5,11 @@ import bcrypt from "bcrypt";
import { ActionProjectType } from "@app/db/schemas";
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service";
import { ProjectPermissionActions, ProjectPermissionSub } from "@app/ee/services/permission/project-permission";
import {
ProjectPermissionActions,
ProjectPermissionSecretActions,
ProjectPermissionSub
} from "@app/ee/services/permission/project-permission";
import { getConfig } from "@app/lib/config/env";
import { ForbiddenRequestError, NotFoundError, UnauthorizedError } from "@app/lib/errors";
@ -67,7 +71,7 @@ export const serviceTokenServiceFactory = ({
scopes.forEach(({ environment, secretPath }) => {
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Create,
ProjectPermissionSecretActions.Create,
subject(ProjectPermissionSub.Secrets, { environment, secretPath })
);
});

@ -50,6 +50,7 @@ const buildSlackPayload = (notification: TSlackNotification) => {
const messageBody = `A secret approval request has been opened by ${payload.userEmail}.
*Environment*: ${payload.environment}
*Secret path*: ${payload.secretPath || "/"}
*Secret Key${payload.secretKeys.length > 1 ? "s" : ""}*: ${payload.secretKeys.join(", ")}
View the complete details <${appCfg.SITE_URL}/secret-manager/${payload.projectId}/approval?requestId=${
payload.requestId

@ -62,6 +62,7 @@ export type TSlackNotification =
secretPath: string;
requestId: string;
projectId: string;
secretKeys: string[];
};
}
| {

@ -19,9 +19,11 @@ import { TUserDALFactory } from "../user/user-dal";
import { TUserAliasDALFactory } from "../user-alias/user-alias-dal";
import { UserAliasType } from "../user-alias/user-alias-types";
import { TSuperAdminDALFactory } from "./super-admin-dal";
import { LoginMethod, TAdminGetUsersDTO, TAdminSignUpDTO } from "./super-admin-types";
import { LoginMethod, TAdminGetIdentitiesDTO, TAdminGetUsersDTO, TAdminSignUpDTO } from "./super-admin-types";
import { TIdentityDALFactory } from "@app/services/identity/identity-dal";
type TSuperAdminServiceFactoryDep = {
identityDAL: Pick<TIdentityDALFactory, "getIdentitiesByFilter">;
serverCfgDAL: TSuperAdminDALFactory;
userDAL: TUserDALFactory;
userAliasDAL: Pick<TUserAliasDALFactory, "findOne">;
@ -51,6 +53,7 @@ const ADMIN_CONFIG_DB_UUID = "00000000-0000-0000-0000-000000000000";
export const superAdminServiceFactory = ({
serverCfgDAL,
userDAL,
identityDAL,
userAliasDAL,
authService,
orgService,
@ -271,26 +274,30 @@ export const superAdminServiceFactory = ({
return { token, user: userInfo, organization };
};
const getUsers = ({ offset, limit, searchTerm }: TAdminGetUsersDTO) => {
const getUsers = ({ offset, limit, searchTerm, adminsOnly }: TAdminGetUsersDTO) => {
return userDAL.getUsersByFilter({
limit,
offset,
searchTerm,
sortBy: "username"
sortBy: "username",
adminsOnly
});
};
const deleteUser = async (userId: string) => {
if (!licenseService.onPremFeatures?.instanceUserManagement) {
throw new BadRequestError({
message: "Failed to delete user due to plan restriction. Upgrade to Infisical's Pro plan."
});
}
const user = await userDAL.deleteById(userId);
return user;
};
const getIdentities = ({ offset, limit, searchTerm }: TAdminGetIdentitiesDTO) => {
return identityDAL.getIdentitiesByFilter({
limit,
offset,
searchTerm,
sortBy: "name"
});
};
const grantServerAdminAccessToUser = async (userId: string) => {
if (!licenseService.onPremFeatures?.instanceUserManagement) {
throw new BadRequestError({
@ -388,6 +395,7 @@ export const superAdminServiceFactory = ({
adminSignUp,
getUsers,
deleteUser,
getIdentities,
getAdminSlackConfig,
updateRootEncryptionStrategy,
getConfiguredEncryptionStrategies,

@ -20,6 +20,13 @@ export type TAdminGetUsersDTO = {
offset: number;
limit: number;
searchTerm: string;
adminsOnly: boolean;
};
export type TAdminGetIdentitiesDTO = {
offset: number;
limit: number;
searchTerm: string;
};
export enum LoginMethod {

@ -23,15 +23,18 @@ export const userDALFactory = (db: TDbClient) => {
limit,
offset,
searchTerm,
sortBy
sortBy,
adminsOnly
}: {
limit: number;
offset: number;
searchTerm: string;
sortBy?: keyof TUsers;
adminsOnly: boolean;
}) => {
try {
let query = db.replicaNode()(TableName.Users).where("isGhost", "=", false);
if (searchTerm) {
query = query.where((qb) => {
void qb
@ -42,6 +45,10 @@ export const userDALFactory = (db: TDbClient) => {
});
}
if (adminsOnly) {
query = query.where("superAdmin", true);
}
if (sortBy) {
query = query.orderBy(sortBy);
}

@ -11,7 +11,7 @@ import { logger } from "@app/lib/logger";
import { TProjectDALFactory } from "../project/project-dal";
import { TProjectEnvDALFactory } from "../project-env/project-env-dal";
import { TWebhookDALFactory } from "./webhook-dal";
import { WebhookType } from "./webhook-types";
import { TWebhookPayloads, WebhookEvents, WebhookType } from "./webhook-types";
const WEBHOOK_TRIGGER_TIMEOUT = 15 * 1000;
@ -54,29 +54,64 @@ export const triggerWebhookRequest = async (
return req;
};
export const getWebhookPayload = (
eventName: string,
details: {
workspaceName: string;
workspaceId: string;
environment: string;
secretPath?: string;
type?: string | null;
export const getWebhookPayload = (event: TWebhookPayloads) => {
if (event.type === WebhookEvents.SecretModified) {
const { projectName, projectId, environment, secretPath, type } = event.payload;
switch (type) {
case WebhookType.SLACK:
return {
text: "A secret value has been added or modified.",
attachments: [
{
color: "#E7F256",
fields: [
{
title: "Project",
value: projectName,
short: false
},
{
title: "Environment",
value: environment,
short: false
},
{
title: "Secret Path",
value: secretPath,
short: false
}
]
}
]
};
case WebhookType.GENERAL:
default:
return {
event: event.type,
project: {
workspaceId: projectId,
projectName,
environment,
secretPath
}
};
}
}
) => {
const { workspaceName, workspaceId, environment, secretPath, type } = details;
const { projectName, projectId, environment, secretPath, type, reminderNote, secretName } = event.payload;
switch (type) {
case WebhookType.SLACK:
return {
text: "A secret value has been added or modified.",
text: "You have a secret reminder",
attachments: [
{
color: "#E7F256",
fields: [
{
title: "Project",
value: workspaceName,
value: projectName,
short: false
},
{
@ -88,6 +123,16 @@ export const getWebhookPayload = (
title: "Secret Path",
value: secretPath,
short: false
},
{
title: "Secret Name",
value: secretName,
short: false
},
{
title: "Reminder Note",
value: reminderNote,
short: false
}
]
}
@ -96,11 +141,14 @@ export const getWebhookPayload = (
case WebhookType.GENERAL:
default:
return {
event: eventName,
event: event.type,
project: {
workspaceId,
workspaceId: projectId,
projectName,
environment,
secretPath
secretPath,
secretName,
reminderNote
}
};
}
@ -110,6 +158,7 @@ export type TFnTriggerWebhookDTO = {
projectId: string;
secretPath: string;
environment: string;
event: TWebhookPayloads;
webhookDAL: Pick<TWebhookDALFactory, "findAllWebhooks" | "transaction" | "update" | "bulkUpdate">;
projectEnvDAL: Pick<TProjectEnvDALFactory, "findOne">;
projectDAL: Pick<TProjectDALFactory, "findById">;
@ -124,8 +173,9 @@ export const fnTriggerWebhook = async ({
projectId,
webhookDAL,
projectEnvDAL,
projectDAL,
secretManagerDecryptor
event,
secretManagerDecryptor,
projectDAL
}: TFnTriggerWebhookDTO) => {
const webhooks = await webhookDAL.findAllWebhooks(projectId, environment);
const toBeTriggeredHooks = webhooks.filter(
@ -134,21 +184,20 @@ export const fnTriggerWebhook = async ({
);
if (!toBeTriggeredHooks.length) return;
logger.info({ environment, secretPath, projectId }, "Secret webhook job started");
const project = await projectDAL.findById(projectId);
let { projectName } = event.payload;
if (!projectName) {
const project = await projectDAL.findById(event.payload.projectId);
projectName = project.name;
}
const webhooksTriggered = await Promise.allSettled(
toBeTriggeredHooks.map((hook) =>
triggerWebhookRequest(
hook,
secretManagerDecryptor,
getWebhookPayload("secrets.modified", {
workspaceName: project.name,
workspaceId: projectId,
environment,
secretPath,
type: hook.type
})
)
)
toBeTriggeredHooks.map((hook) => {
const formattedEvent = {
type: event.type,
payload: { ...event.payload, type: hook.type, projectName }
} as TWebhookPayloads;
return triggerWebhookRequest(hook, secretManagerDecryptor, getWebhookPayload(formattedEvent));
})
);
// filter hooks by status

@ -16,7 +16,8 @@ import {
TDeleteWebhookDTO,
TListWebhookDTO,
TTestWebhookDTO,
TUpdateWebhookDTO
TUpdateWebhookDTO,
WebhookEvents
} from "./webhook-types";
type TWebhookServiceFactoryDep = {
@ -144,12 +145,15 @@ export const webhookServiceFactory = ({
await triggerWebhookRequest(
webhook,
(value) => secretManagerDecryptor({ cipherTextBlob: value }).toString(),
getWebhookPayload("test", {
workspaceName: project.name,
workspaceId: webhook.projectId,
environment: webhook.environment.slug,
secretPath: webhook.secretPath,
type: webhook.type
getWebhookPayload({
type: "test" as WebhookEvents.SecretModified,
payload: {
projectName: project.name,
projectId: webhook.projectId,
environment: webhook.environment.slug,
secretPath: webhook.secretPath,
type: webhook.type
}
})
);
} catch (err) {

@ -30,3 +30,36 @@ export enum WebhookType {
GENERAL = "general",
SLACK = "slack"
}
export enum WebhookEvents {
SecretModified = "secrets.modified",
SecretReminderExpired = "secrets.reminder-expired",
TestEvent = "test"
}
type TWebhookSecretModifiedEventPayload = {
type: WebhookEvents.SecretModified;
payload: {
projectName?: string;
projectId: string;
environment: string;
secretPath?: string;
type?: string | null;
};
};
type TWebhookSecretReminderEventPayload = {
type: WebhookEvents.SecretReminderExpired;
payload: {
projectName?: string;
projectId: string;
environment: string;
secretPath?: string;
type?: string | null;
secretName: string;
secretId: string;
reminderNote?: string | null;
};
};
export type TWebhookPayloads = TWebhookSecretModifiedEventPayload | TWebhookSecretReminderEventPayload;

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