1
0
mirror of https://github.com/Infisical/infisical.git synced 2025-03-28 15:29:21 +00:00

Compare commits

...

122 Commits

Author SHA1 Message Date
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
a500f00a49 fix(run): compare environment slug to environment slug 2025-03-13 13:21:12 -07:00
ad207786e2 refactor: clean up empty line 2025-03-13 12:18:54 -07: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
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
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
2bd681d58f add system md service for gateway 2025-03-09 16:07:33 -04: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
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
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
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
abc2ffca57 improvement: add organization name constraint 2025-03-06 15:41:27 -08:00
229 changed files with 7411 additions and 3433 deletions
.envrc.gitignore
backend
e2e-test
package.json
src
ee
lib
api-docs
errors
server
services
cli
docs
cli/commands
documentation/platform
images/platform/secret-scanning
integrations
mint.json
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

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

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 {
};
}
};

@ -71,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 {

@ -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;

@ -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
);

@ -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">;
@ -912,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);
@ -1000,6 +1008,7 @@ export const secretApprovalRequestServiceFactory = ({
: keyName2BlindIndex[secretName];
// add tags
if (tagIds?.length) commitTagIds[keyName2BlindIndex[secretName]] = tagIds;
return {
...latestSecretVersions[secretId],
...el,
@ -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,

@ -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: {

@ -1,4 +1,5 @@
/* eslint-disable max-classes-per-file */
export class DatabaseError extends Error {
name: string;

@ -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");

@ -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">;

@ -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) => {

@ -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);

@ -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 };
};

@ -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);

@ -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
};
};

@ -403,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) {

@ -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 })
);
});

@ -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);
}

@ -20,6 +20,7 @@ require (
github.com/muesli/reflow v0.3.0
github.com/muesli/roff v0.1.0
github.com/petar-dambovaliev/aho-corasick v0.0.0-20211021192214-5ab2d9280aa9
github.com/pion/dtls/v3 v3.0.4
github.com/pion/logging v0.2.3
github.com/pion/turn/v4 v4.0.0
github.com/posthog/posthog-go v0.0.0-20221221115252-24dfed35d71a
@ -90,7 +91,6 @@ require (
github.com/oklog/ulid v1.3.1 // indirect
github.com/onsi/ginkgo/v2 v2.22.2 // indirect
github.com/pelletier/go-toml v1.9.3 // indirect
github.com/pion/dtls/v3 v3.0.4 // indirect
github.com/pion/randutil v0.1.0 // indirect
github.com/pion/stun/v3 v3.0.0 // indirect
github.com/pion/transport/v3 v3.0.7 // indirect

@ -484,8 +484,6 @@ golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8U
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20211215165025-cf75a172585e/go.mod h1:P+XmwS30IXTQdn5tA2iutPOUgjI07+tq3H3K9MVA1s8=
golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
golang.org/x/crypto v0.35.0 h1:b15kiHdrGCHrP6LvwaQ3c03kgNhhiMgvlhxHQhmg2Xs=
golang.org/x/crypto v0.35.0/go.mod h1:dy7dXNW32cAb/6/PRuTNsix8T+vJAqvuIy5Bli/x0YQ=
golang.org/x/crypto v0.36.0 h1:AnAEvhDddvBdpY+uR+MyHmuZzzNqXSe/GvuDeob5L34=
golang.org/x/crypto v0.36.0/go.mod h1:Y4J0ReaxCR1IMaabaSMugxJES1EpwhBHhv2bDHklZvc=
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
@ -592,8 +590,6 @@ golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJ
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.11.0 h1:GGz8+XQP4FvTTrjZPzNKTMFtSXH80RAzG+5ghFPgK9w=
golang.org/x/sync v0.11.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sync v0.12.0 h1:MHc5BpPuC30uJk597Ri8TV3CNZcTLu6B6z4lJy+g6Jw=
golang.org/x/sync v0.12.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
golang.org/x/sys v0.0.0-20180823144017-11551d06cbcc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
@ -644,13 +640,9 @@ golang.org/x/sys v0.0.0-20210809222454-d867a43fc93e/go.mod h1:oPkhp1MJrh7nUepCBc
golang.org/x/sys v0.0.0-20220310020820-b874c991c1a5/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.30.0 h1:QjkSwP/36a20jFYWkSue1YwXzLmsV5Gfq7Eiy72C1uc=
golang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.31.0 h1:ioabZlmFYtWhL+TRYpcnNlLwhyxaM9kWTDEmfnprqik=
golang.org/x/sys v0.31.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.29.0 h1:L6pJp37ocefwRRtYPKSWOWzOtWSxVajvz2ldH/xi3iU=
golang.org/x/term v0.29.0/go.mod h1:6bl4lRlvVuDgSf3179VpIxBF0o10JUpXWOnI7nErv7s=
golang.org/x/term v0.30.0 h1:PQ39fJZ+mfadBm0y5WlL4vlM7Sx1Hgf13sMIY2+QS9Y=
golang.org/x/term v0.30.0/go.mod h1:NYYFdzHoI5wRh/h5tDMdMqCqPJZEuNqVR5xJLd/n67g=
golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
@ -662,8 +654,6 @@ golang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.5/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
golang.org/x/text v0.22.0 h1:bofq7m3/HAFvbF51jz3Q9wLg3jkvSPuiZu/pD1XwgtM=
golang.org/x/text v0.22.0/go.mod h1:YRoo4H8PVmsu+E3Ou7cqLVH8oXWIHVoX0jqUWALQhfY=
golang.org/x/text v0.23.0 h1:D71I7dUrlY+VX0gQShAThNGHFxZ13dGLBHQLVl1mJlY=
golang.org/x/text v0.23.0/go.mod h1:/BLNzu4aZCJ1+kcD0DNRotWKage4q2rGVAg4o22unh4=
golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
@ -868,4 +858,4 @@ honnef.co/go/tools v0.0.1-2020.1.3/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9
honnef.co/go/tools v0.0.1-2020.1.4/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k=
rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8=
rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0=
rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA=
rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA=

@ -2,6 +2,12 @@ package api
import "time"
type Environment struct {
Name string `json:"name"`
Slug string `json:"slug"`
ID string `json:"id"`
}
// Stores info for login one
type LoginOneRequest struct {
Email string `json:"email"`
@ -14,7 +20,6 @@ type LoginOneResponse struct {
}
// Stores info for login two
type LoginTwoRequest struct {
Email string `json:"email"`
ClientProof string `json:"clientProof"`
@ -168,9 +173,10 @@ type Secret struct {
}
type Project struct {
ID string `json:"id"`
Name string `json:"name"`
Slug string `json:"slug"`
ID string `json:"id"`
Name string `json:"name"`
Slug string `json:"slug"`
Environments []Environment `json:"environments"`
}
type RawSecret struct {

@ -1,39 +1,34 @@
package cmd
import (
// "fmt"
// "github.com/Infisical/infisical-merge/packages/api"
// "github.com/Infisical/infisical-merge/packages/models"
"context"
"fmt"
"os"
"os/exec"
"os/signal"
"runtime"
"syscall"
"time"
"github.com/Infisical/infisical-merge/packages/gateway"
"github.com/Infisical/infisical-merge/packages/util"
"github.com/rs/zerolog/log"
// "github.com/Infisical/infisical-merge/packages/visualize"
// "github.com/rs/zerolog/log"
// "github.com/go-resty/resty/v2"
"github.com/posthog/posthog-go"
"github.com/rs/zerolog/log"
"github.com/spf13/cobra"
)
var gatewayCmd = &cobra.Command{
Example: `infisical gateway`,
Short: "Used to infisical gateway",
Use: "gateway",
Short: "Run the Infisical gateway or manage its systemd service",
Long: "Run the Infisical gateway in the foreground or manage its systemd service installation. Use 'gateway install' to set up the systemd service.",
Example: `infisical gateway --token=<token>
sudo infisical gateway install --token=<token> --domain=<domain>`,
DisableFlagsInUseLine: true,
Args: cobra.NoArgs,
Run: func(cmd *cobra.Command, args []string) {
token, err := util.GetInfisicalToken(cmd)
if err != nil {
util.HandleError(err, "Unable to parse flag")
util.HandleError(err, "Unable to parse token flag")
}
if token == nil {
@ -109,6 +104,50 @@ var gatewayCmd = &cobra.Command{
},
}
var gatewayInstallCmd = &cobra.Command{
Use: "install",
Short: "Install and enable systemd service for the gateway (requires sudo)",
Long: "Install and enable systemd service for the gateway. Must be run with sudo on Linux.",
Example: "sudo infisical gateway install --token=<token> --domain=<domain>",
DisableFlagsInUseLine: true,
Args: cobra.NoArgs,
Run: func(cmd *cobra.Command, args []string) {
if runtime.GOOS != "linux" {
util.HandleError(fmt.Errorf("systemd service installation is only supported on Linux"))
}
if os.Geteuid() != 0 {
util.HandleError(fmt.Errorf("systemd service installation requires root/sudo privileges"))
}
token, err := util.GetInfisicalToken(cmd)
if err != nil {
util.HandleError(err, "Unable to parse flag")
}
if token == nil {
util.HandleError(fmt.Errorf("Token not found"))
}
domain, err := cmd.Flags().GetString("domain")
if err != nil {
util.HandleError(err, "Unable to parse domain flag")
}
if err := gateway.InstallGatewaySystemdService(token.Token, domain); err != nil {
util.HandleError(err, "Failed to install systemd service")
}
enableCmd := exec.Command("systemctl", "enable", "infisical-gateway")
if err := enableCmd.Run(); err != nil {
util.HandleError(err, "Failed to enable systemd service")
}
log.Info().Msg("Successfully installed and enabled infisical-gateway service")
log.Info().Msg("To start the service, run: sudo systemctl start infisical-gateway")
},
}
var gatewayRelayCmd = &cobra.Command{
Example: `infisical gateway relay`,
Short: "Used to run infisical gateway relay",
@ -138,9 +177,12 @@ var gatewayRelayCmd = &cobra.Command{
func init() {
gatewayCmd.Flags().String("token", "", "Connect with Infisical using machine identity access token")
gatewayInstallCmd.Flags().String("token", "", "Connect with Infisical using machine identity access token")
gatewayInstallCmd.Flags().String("domain", "", "Domain of your self-hosted Infisical instance")
gatewayRelayCmd.Flags().String("config", "", "Relay config yaml file path")
gatewayCmd.AddCommand(gatewayInstallCmd)
gatewayCmd.AddCommand(gatewayRelayCmd)
rootCmd.AddCommand(gatewayCmd)
}

@ -15,6 +15,9 @@ import (
"syscall"
"time"
"github.com/Infisical/infisical-merge/packages/api"
"github.com/go-resty/resty/v2"
"github.com/Infisical/infisical-merge/packages/models"
"github.com/Infisical/infisical-merge/packages/util"
"github.com/fatih/color"
@ -59,11 +62,11 @@ var runCmd = &cobra.Command{
return nil
},
Run: func(cmd *cobra.Command, args []string) {
environmentName, _ := cmd.Flags().GetString("env")
environmentSlug, _ := cmd.Flags().GetString("env")
if !cmd.Flags().Changed("env") {
environmentFromWorkspace := util.GetEnvFromWorkspaceFile()
if environmentFromWorkspace != "" {
environmentName = environmentFromWorkspace
environmentSlug = environmentFromWorkspace
}
}
@ -136,8 +139,20 @@ var runCmd = &cobra.Command{
util.HandleError(err, "Unable to parse flag")
}
log.Debug().Msgf("Confirming selected environment is valid: %s", environmentSlug)
hasEnvironment, err := confirmProjectHasEnvironment(environmentSlug, projectId, token)
if err != nil {
util.HandleError(err, "Could not confirm project has environment")
}
if !hasEnvironment {
util.HandleError(fmt.Errorf("project does not have environment '%s'", environmentSlug))
}
log.Debug().Msgf("Project '%s' has environment '%s'", projectId, environmentSlug)
request := models.GetAllSecretsParameters{
Environment: environmentName,
Environment: environmentSlug,
WorkspaceId: projectId,
TagSlugs: tagSlugs,
SecretsPath: secretsPath,
@ -308,7 +323,6 @@ func waitForExitCommand(cmd *exec.Cmd) (int, error) {
}
func executeCommandWithWatchMode(commandFlag string, args []string, watchModeInterval int, request models.GetAllSecretsParameters, projectConfigDir string, secretOverriding bool, token *models.TokenDetails) {
var cmd *exec.Cmd
var err error
var lastSecretsFetch time.Time
@ -439,8 +453,53 @@ func executeCommandWithWatchMode(commandFlag string, args []string, watchModeInt
}
}
func fetchAndFormatSecretsForShell(request models.GetAllSecretsParameters, projectConfigDir string, secretOverriding bool, token *models.TokenDetails) (models.InjectableEnvironmentResult, error) {
func confirmProjectHasEnvironment(environmentSlug, projectId string, token *models.TokenDetails) (bool, error) {
var accessToken string
if token != nil && (token.Type == util.SERVICE_TOKEN_IDENTIFIER || token.Type == util.UNIVERSAL_AUTH_TOKEN_IDENTIFIER) {
accessToken = token.Token
} else {
util.RequireLogin()
util.RequireLocalWorkspaceFile()
loggedInUserDetails, err := util.GetCurrentLoggedInUserDetails(true)
if err != nil {
util.HandleError(err, "Unable to authenticate")
}
if loggedInUserDetails.LoginExpired {
util.PrintErrorMessageAndExit("Your login session has expired, please run [infisical login] and try again")
}
accessToken = loggedInUserDetails.UserCredentials.JTWToken
}
if projectId == "" {
workspaceFile, err := util.GetWorkSpaceFromFile()
if err != nil {
util.HandleError(err, "Unable to get local project details")
}
projectId = workspaceFile.WorkspaceId
}
httpClient := resty.New()
httpClient.SetAuthToken(accessToken).
SetHeader("Accept", "application/json")
project, err := api.CallGetProjectById(httpClient, projectId)
if err != nil {
return false, err
}
for _, env := range project.Environments {
if env.Slug == environmentSlug {
return true, nil
}
}
return false, nil
}
func fetchAndFormatSecretsForShell(request models.GetAllSecretsParameters, projectConfigDir string, secretOverriding bool, token *models.TokenDetails) (models.InjectableEnvironmentResult, error) {
if token != nil && token.Type == util.SERVICE_TOKEN_IDENTIFIER {
request.InfisicalToken = token.Token
} else if token != nil && token.Type == util.UNIVERSAL_AUTH_TOKEN_IDENTIFIER {

@ -79,13 +79,9 @@ func (g *Gateway) ConnectWithRelay() error {
// Dial TURN Server
if relayPort == "5349" {
caCertPool := x509.NewCertPool()
caCertPool.AppendCertsFromPEM([]byte(g.config.CertificateChain))
log.Info().Msgf("Provided relay port %s. Using TLS", relayPort)
conn, err := dtls.Dial("udp", turnAddr, &dtls.Config{
ServerName: relayAddress,
RootCAs: caCertPool,
})
if err != nil {
return fmt.Errorf("Failed to connect with relay server: %w", err)
@ -193,7 +189,7 @@ func (g *Gateway) Listen(ctx context.Context) error {
log.Printf("Listener started on %s", quicListener.Addr())
g.registerRelayIsActive(ctx, quicListener.Addr().String(), errCh)
g.registerRelayIsActive(ctx, errCh)
log.Info().Msg("Gateway started successfully")
@ -330,7 +326,7 @@ func (g *Gateway) createPermissionForStaticIps(staticIps string) error {
return nil
}
func (g *Gateway) registerRelayIsActive(ctx context.Context, addr string, errCh chan error) error {
func (g *Gateway) registerRelayIsActive(ctx context.Context, errCh chan error) error {
ticker := time.NewTicker(15 * time.Second)
maxFailures := 3
failures := 0
@ -345,13 +341,7 @@ func (g *Gateway) registerRelayIsActive(ctx context.Context, addr string, errCh
return
case <-ticker.C:
log.Debug().Msg("Performing relay connection health check")
ctxTimeout, cancel := context.WithTimeout(ctx, 5*time.Second)
defer cancel()
// Try to establish a QUIC connection
conn, err := quic.DialAddr(ctxTimeout, addr, &tls.Config{
InsecureSkipVerify: false, // Skip certificate verification
NextProtos: []string{"infisical-gateway"},
}, nil)
err := g.createPermissionForStaticIps(g.config.InfisicalStaticIp)
if err != nil && !strings.Contains(err.Error(), "tls:") {
failures++
log.Warn().Err(err).Int("failures", failures).Msg("Failed to refresh TURN permissions")
@ -361,9 +351,6 @@ func (g *Gateway) registerRelayIsActive(ctx context.Context, addr string, errCh
}
continue
}
if conn != nil {
defer conn.CloseWithError(0, "All good")
}
}
}
}()

@ -0,0 +1,82 @@
package gateway
import (
"fmt"
"os"
"os/exec"
"path/filepath"
"runtime"
"github.com/rs/zerolog/log"
)
const systemdServiceTemplate = `[Unit]
Description=Infisical Gateway Service
After=network.target
[Service]
Type=simple
EnvironmentFile=/etc/infisical/gateway.conf
ExecStart=infisical gateway
Restart=on-failure
InaccessibleDirectories=/home
PrivateTmp=yes
LimitCORE=infinity
LimitNOFILE=1000000
LimitNPROC=60000
LimitRTPRIO=infinity
LimitRTTIME=7000000
[Install]
WantedBy=multi-user.target
`
func InstallGatewaySystemdService(token string, domain string) error {
if runtime.GOOS != "linux" {
log.Info().Msg("Skipping systemd service installation - not on Linux")
return nil
}
if os.Geteuid() != 0 {
log.Info().Msg("Skipping systemd service installation - not running as root/sudo")
return nil
}
configDir := "/etc/infisical"
if err := os.MkdirAll(configDir, 0755); err != nil {
return fmt.Errorf("failed to create config directory: %v", err)
}
configContent := fmt.Sprintf("INFISICAL_UNIVERSAL_AUTH_ACCESS_TOKEN=%s\n", token)
if domain != "" {
configContent += fmt.Sprintf("INFISICAL_API_URL=%s\n", domain)
} else {
configContent += "INFISICAL_API_URL=\n"
}
configPath := filepath.Join(configDir, "gateway.conf")
if err := os.WriteFile(configPath, []byte(configContent), 0600); err != nil {
return fmt.Errorf("failed to write config file: %v", err)
}
servicePath := "/etc/systemd/system/infisical-gateway.service"
if _, err := os.Stat(servicePath); err == nil {
log.Info().Msg("Systemd service file already exists")
return nil
}
if err := os.WriteFile(servicePath, []byte(systemdServiceTemplate), 0644); err != nil {
return fmt.Errorf("failed to write systemd service file: %v", err)
}
reloadCmd := exec.Command("systemctl", "daemon-reload")
if err := reloadCmd.Run(); err != nil {
return fmt.Errorf("failed to reload systemd: %v", err)
}
log.Info().Msg("Successfully installed systemd service")
log.Info().Msg("To start the service, run: sudo systemctl start infisical-gateway")
log.Info().Msg("To enable the service on boot, run: sudo systemctl enable infisical-gateway")
return nil
}

@ -232,7 +232,6 @@ func FilterSecretsByTag(plainTextSecrets []models.SingleEnvironmentVariable, tag
func GetAllEnvironmentVariables(params models.GetAllSecretsParameters, projectConfigFilePath string) ([]models.SingleEnvironmentVariable, error) {
var secretsToReturn []models.SingleEnvironmentVariable
// var serviceTokenDetails api.GetServiceTokenDetailsResponse
var errorToReturn error
if params.InfisicalToken == "" && params.UniversalAuthAccessToken == "" {

@ -76,7 +76,6 @@ func TestUniversalAuth_SecretsGetWrongEnvironment(t *testing.T) {
if err != nil {
t.Fatalf("snapshot failed: %v", err)
}
}
func TestUserAuth_SecretsGetAll(t *testing.T) {

@ -0,0 +1,107 @@
---
title: "infisical gateway"
description: "Run the Infisical gateway or manage its systemd service"
---
<Tabs>
<Tab title="Run gateway">
```bash
infisical gateway --token=<token>
```
</Tab>
<Tab title="Install service">
```bash
sudo infisical gateway install --token=<token> --domain=<domain>
```
</Tab>
</Tabs>
## Description
Run the Infisical gateway in the foreground or manage its systemd service installation. The gateway allows secure communication between your self-hosted Infisical instance and client applications.
## Subcommands & flags
<Accordion title="infisical gateway" defaultOpen="true">
Run the Infisical gateway in the foreground. The gateway will connect to the relay service and maintain a persistent connection.
```bash
infisical gateway --token=<token> --domain=<domain>
```
### Flags
<Accordion title="--token">
The machine identity access token to authenticate with Infisical.
```bash
# Example
infisical gateway --token=<token>
```
You may also expose the token to the CLI by setting the environment variable `INFISICAL_TOKEN` before executing the gateway command.
</Accordion>
<Accordion title="--domain">
Domain of your self-hosted Infisical instance.
```bash
# Example
sudo infisical gateway install --domain=https://app.your-domain.com
```
</Accordion>
</Accordion>
<Accordion title="infisical gateway install">
Install and enable the gateway as a systemd service. This command must be run with sudo on Linux.
```bash
sudo infisical gateway install --token=<token> --domain=<domain>
```
### Requirements
- Must be run on Linux
- Must be run with root/sudo privileges
- Requires systemd
### Flags
<Accordion title="--token">
The machine identity access token to authenticate with Infisical.
```bash
# Example
sudo infisical gateway install --token=<token>
```
You may also expose the token to the CLI by setting the environment variable `INFISICAL_TOKEN` before executing the install command.
</Accordion>
<Accordion title="--domain">
Domain of your self-hosted Infisical instance.
```bash
# Example
sudo infisical gateway install --domain=https://app.your-domain.com
```
</Accordion>
### Service Details
The systemd service is installed with secure defaults:
- Service file: `/etc/systemd/system/infisical-gateway.service`
- Config file: `/etc/infisical/gateway.conf`
- Runs with restricted privileges:
- InaccessibleDirectories=/home
- PrivateTmp=yes
- Resource limits configured for stability
- Automatically restarts on failure
- Enabled to start on boot
After installation, manage the service with standard systemd commands:
```bash
sudo systemctl start infisical-gateway # Start the service
sudo systemctl stop infisical-gateway # Stop the service
sudo systemctl status infisical-gateway # Check service status
sudo systemctl disable infisical-gateway # Disable auto-start on boot
```
</Accordion>

Binary file not shown.

After

(image error) Size: 324 KiB

@ -4,6 +4,8 @@ sidebarTitle: "Overview"
description: "How to access private network resources from Infisical"
---
![Alt text](/documentation/platform/gateways/images/gateway-highlevel-diagram.png)
The Infisical Gateway provides secure access to private resources within your network without needing direct inbound connections to your environment.
This method keeps your resources fully protected from external access while enabling Infisical to securely interact with resources like databases.
Common use cases include generating dynamic credentials or rotating credentials for private databases.
@ -45,19 +47,53 @@ Once authenticated, the Gateway establishes a secure connection with Infisical t
</Step>
<Step title="Deploy the Gateway">
Use the Infisical CLI to deploy the Gateway. You can log in with your machine identity and start the Gateway in one command. The example below demonstrates how to deploy the Gateway using the Universal Auth method:
```bash
infisical gateway --token $(infisical login --method=universal-auth --client-id=<> --client-secret=<> --plain)
```
Alternatively, if you already have the token, use it directly with the `--token` flag:
```bash
infisical gateway --token <your-machine-identity-token>
```
Or set it as an environment variable:
```bash
export INFISICAL_TOKEN=<your-machine-identity-token>
infisical gateway
```
Use the Infisical CLI to deploy the Gateway. You can run it directly or install it as a systemd service for production:
<Tabs>
<Tab title="Production (systemd)">
For production deployments on Linux, install the Gateway as a systemd service:
```bash
sudo infisical gateway install --token <your-machine-identity-token> --domain <your-infisical-domain>
sudo systemctl start infisical-gateway
```
This will install and start the Gateway as a secure systemd service that:
- Runs with restricted privileges:
- Runs as root user (required for secure token management)
- Restricted access to home directories
- Private temporary directory
- Automatically restarts on failure
- Starts on system boot
- Manages token and domain configuration securely in `/etc/infisical/gateway.conf`
<Warning>
The install command requires:
- Linux operating system
- Root/sudo privileges
- Systemd
</Warning>
</Tab>
<Tab title="Development (direct)">
For development or testing, you can run the Gateway directly. Log in with your machine identity and start the Gateway in one command:
```bash
infisical gateway --token $(infisical login --method=universal-auth --client-id=<> --client-secret=<> --plain)
```
Alternatively, if you already have the token, use it directly with the `--token` flag:
```bash
infisical gateway --token <your-machine-identity-token>
```
Or set it as an environment variable:
```bash
export INFISICAL_TOKEN=<your-machine-identity-token>
infisical gateway
```
</Tab>
</Tabs>
For detailed information about the gateway command and its options, see the [gateway command documentation](/cli/commands/gateway).
<Note>
Ensure the deployed Gateway has network access to the private resources you intend to connect with Infisical.
</Note>
@ -78,4 +114,3 @@ Once authenticated, the Gateway establishes a secure connection with Infisical t
Once added to a project, the Gateway becomes available for use by any feature that supports Gateways within that project.
</Step>
</Steps>

@ -0,0 +1,68 @@
---
title: 'Secret Scanning'
description: "Scan and prevent secret leaks in your code repositories"
---
The Infisical Secret Scanner allows you to keep an overview and stay alert of exposed secrets across your entire GitHub organization and repositories.
To further enhance security, we recommend you also use our [CLI Secret Scanner](/cli/scanning-overview#automatically-scan-changes-before-you-commit) to scan for exposed secrets prior to pushing your changes.
## Code Scanning
![Scanning Overview](/images/platform/secret-scanning/overview.png)
Secret scans are built on event-driven architecture. This means that every time a push is made to one of your selected repositories, Infisical will scan the modified files for any exposed secrets.
If one or more exposed secrets are detected, it will be displayed in your Infisical dashboard. An exposed secret is known as a **"Risk"**. Each risk has the following data associated with it:
- **Date**: When the risk was first detected.
- **Secret Type**: Which type of secret was detected.
- **Info**: Information about the secret, such as the repository, file name, and the committer who made the change.
Once an exposed secret is detected, all organization admins will be sent an e-mail notification containing details about the exposed secret.
<Tip>
Each risk also contains a "View Exposed Secret" button, which will take you directly to the GitHub commit and to the line where the secret was exposed.
</Tip>
![Exposed Secret](/images/platform/secret-scanning/exposed-secret.png)
## Responding to Exposed Secrets
After an exposed secret is detected, it will be marked as `Needs Attention`. When there are risks marked as needs attention, it's important to address them as soon as possible.
You can mark the risk as `Resolved` by changing the status to one of the following states:
- **This Is a False Positive**: The secret was not exposed, but was detected by the scanner.
- **I Have Rotated The Secret**: The secret was exposed, but it has now been removed.
- **No Rotation Needed**: You are choosing to ignore this risk. You may choose to do this if the risk is non-sensitive or otherwise not a security risk.
![Needs Attention](/images/platform/secret-scanning/needs-attention.png)
## Ignoring Known Secrets
If you're intentionally committing a test secret that the secret scanner might flag, you can instruct Infisical to overlook that secret with the methods listed below.
### infisical-scan:ignore
To ignore a secret contained in line of code, simply add `infisical-scan:ignore ` at the end of the line as comment in the given programming.
```js example.js
function helloWorld() {
console.log("8dyfuiRyq=vVc3RRr_edRk-fK__JItpZ"); // infisical-scan:ignore
}
```
### .infisicalignore
An alternative method to exclude specific findings involves creating a .infisicalignore file at your repository's root.
You can then add the fingerprints of the findings you wish to exclude. The [Infisical scan](/cli/scanning-overview) report provides a unique Fingerprint for each secret found.
By incorporating these Fingerprints into the .infisicalignore file, Infisical will skip the corresponding secret findings in subsequent scans.
```.ignore .infisicalignore
bea0ff6e05a4de73a5db625d4ae181a015b50855:frontend/components/utilities/attemptLogin.js:stripe-access-token:147
bea0ff6e05a4de73a5db625d4ae181a015b50855:backend/src/json/integrations.json:generic-api-key:5
1961b92340e5d2613acae528b886c842427ce5d0:frontend/components/utilities/attemptLogin.js:stripe-access-token:148
```

Binary file not shown.

After

(image error) Size: 138 KiB

Binary file not shown.

After

(image error) Size: 160 KiB

Binary file not shown.

After

(image error) Size: 115 KiB

@ -7,6 +7,12 @@ Prerequisites:
- Set up and add secrets to [Infisical Cloud](https://app.infisical.com)
<Note>
When integrating with Databricks, Infisical is intended to be the source of truth for the secrets in the configured Databricks scope.
Any secrets not present in Infisical will be removed from the specified scope. To prevent removal of secrets not managed by Infisical, Infisical recommends creating a designated secret scope for your integration.
</Note>
<Steps>
<Step title="Authorize Infisical for Databricks">
Obtain a Personal Access Token in **User Settings** > **Developer** > **Access Tokens**.

@ -34,6 +34,8 @@ description: "Learn how to configure a Databricks Sync for Infisical."
<Note>
You must create a secret scope in your Databricks workspace prior to configuration. Ensure your service principal has [Write permissions](https://docs.databricks.com/en/security/auth/access-control/index.html#secret-acls) for the specified secret scope.
Infisical recommends creating a designated Databricks secret scope for your sync to prevent removal of secrets not managed by Infisical.
</Note>
5. Configure the **Sync Options** to specify how secrets should be synced, then click **Next**.

@ -220,7 +220,8 @@
"documentation/platform/admin-panel/org-admin-console"
]
},
"documentation/platform/secret-sharing"
"documentation/platform/secret-sharing",
"documentation/platform/secret-scanning"
]
},
{
@ -339,6 +340,7 @@
"cli/commands/secrets",
"cli/commands/dynamic-secrets",
"cli/commands/ssh",
"cli/commands/gateway",
"cli/commands/export",
"cli/commands/token",
"cli/commands/service-token",
@ -644,8 +646,7 @@
"api-reference/endpoints/oidc-auth/attach",
"api-reference/endpoints/oidc-auth/retrieve",
"api-reference/endpoints/oidc-auth/update",
"api-reference/endpoints/oidc-auth/revoke",
"integrations/frameworks/terraform-cloud"
"api-reference/endpoints/oidc-auth/revoke"
]
},
{

27
flake.lock generated Normal file

@ -0,0 +1,27 @@
{
"nodes": {
"nixpkgs": {
"locked": {
"lastModified": 1741445498,
"narHash": "sha256-F5Em0iv/CxkN5mZ9hRn3vPknpoWdcdCyR0e4WklHwiE=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "52e3095f6d812b91b22fb7ad0bfc1ab416453634",
"type": "github"
},
"original": {
"owner": "NixOS",
"ref": "nixos-24.11",
"repo": "nixpkgs",
"type": "github"
}
},
"root": {
"inputs": {
"nixpkgs": "nixpkgs"
}
}
},
"root": "root",
"version": 7
}

34
flake.nix Normal file

@ -0,0 +1,34 @@
{
description = "Flake for github:Infisical/infisical repository.";
inputs = {
nixpkgs.url = "github:NixOS/nixpkgs/nixos-24.11";
};
outputs = { self, nixpkgs }: {
devShells.aarch64-darwin.default = let
pkgs = nixpkgs.legacyPackages.aarch64-darwin;
in
pkgs.mkShell {
packages = with pkgs; [
git
lazygit
go
python312Full
nodejs_20
nodePackages.prettier
infisical
];
env = {
GOROOT = "${pkgs.go}/share/go";
};
shellHook = ''
export GOPATH="$(pwd)/.go"
mkdir -p "$GOPATH"
'';
};
};
}

@ -10,6 +10,7 @@
"dependencies": {
"@casl/ability": "^6.7.2",
"@casl/react": "^4.0.0",
"@dagrejs/dagre": "^1.1.4",
"@dnd-kit/core": "^6.3.1",
"@dnd-kit/modifiers": "^9.0.0",
"@dnd-kit/sortable": "^10.0.0",
@ -47,8 +48,10 @@
"@tanstack/react-router": "^1.95.1",
"@tanstack/virtual-file-routes": "^1.87.6",
"@tanstack/zod-adapter": "^1.91.0",
"@types/dagre": "^0.7.52",
"@types/nprogress": "^0.2.3",
"@ucast/mongo2js": "^1.3.4",
"@xyflow/react": "^12.4.4",
"argon2-browser": "^1.18.0",
"axios": "^1.7.9",
"classnames": "^2.5.1",
@ -507,6 +510,24 @@
"react": "^16.0.0 || ^17.0.0 || ^18.0.0"
}
},
"node_modules/@dagrejs/dagre": {
"version": "1.1.4",
"resolved": "https://registry.npmjs.org/@dagrejs/dagre/-/dagre-1.1.4.tgz",
"integrity": "sha512-QUTc54Cg/wvmlEUxB+uvoPVKFazM1H18kVHBQNmK2NbrDR5ihOCR6CXLnDSZzMcSQKJtabPUWridBOlJM3WkDg==",
"license": "MIT",
"dependencies": {
"@dagrejs/graphlib": "2.2.4"
}
},
"node_modules/@dagrejs/graphlib": {
"version": "2.2.4",
"resolved": "https://registry.npmjs.org/@dagrejs/graphlib/-/graphlib-2.2.4.tgz",
"integrity": "sha512-mepCf/e9+SKYy1d02/UkvSy6+6MoyXhVxP8lLDfA7BPE1X1d4dR0sZznmbM8/XVJ1GPM+Svnx7Xj6ZweByWUkw==",
"license": "MIT",
"engines": {
"node": ">17.0.0"
}
},
"node_modules/@date-fns/tz": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/@date-fns/tz/-/tz-1.2.0.tgz",
@ -3955,6 +3976,61 @@
"@babel/types": "^7.20.7"
}
},
"node_modules/@types/d3-color": {
"version": "3.1.3",
"resolved": "https://registry.npmjs.org/@types/d3-color/-/d3-color-3.1.3.tgz",
"integrity": "sha512-iO90scth9WAbmgv7ogoq57O9YpKmFBbmoEoCHDB2xMBY0+/KVrqAaCDyCE16dUspeOvIxFFRI+0sEtqDqy2b4A==",
"license": "MIT"
},
"node_modules/@types/d3-drag": {
"version": "3.0.7",
"resolved": "https://registry.npmjs.org/@types/d3-drag/-/d3-drag-3.0.7.tgz",
"integrity": "sha512-HE3jVKlzU9AaMazNufooRJ5ZpWmLIoc90A37WU2JMmeq28w1FQqCZswHZ3xR+SuxYftzHq6WU6KJHvqxKzTxxQ==",
"license": "MIT",
"dependencies": {
"@types/d3-selection": "*"
}
},
"node_modules/@types/d3-interpolate": {
"version": "3.0.4",
"resolved": "https://registry.npmjs.org/@types/d3-interpolate/-/d3-interpolate-3.0.4.tgz",
"integrity": "sha512-mgLPETlrpVV1YRJIglr4Ez47g7Yxjl1lj7YKsiMCb27VJH9W8NVM6Bb9d8kkpG/uAQS5AmbA48q2IAolKKo1MA==",
"license": "MIT",
"dependencies": {
"@types/d3-color": "*"
}
},
"node_modules/@types/d3-selection": {
"version": "3.0.11",
"resolved": "https://registry.npmjs.org/@types/d3-selection/-/d3-selection-3.0.11.tgz",
"integrity": "sha512-bhAXu23DJWsrI45xafYpkQ4NtcKMwWnAC/vKrd2l+nxMFuvOT3XMYTIj2opv8vq8AO5Yh7Qac/nSeP/3zjTK0w==",
"license": "MIT"
},
"node_modules/@types/d3-transition": {
"version": "3.0.9",
"resolved": "https://registry.npmjs.org/@types/d3-transition/-/d3-transition-3.0.9.tgz",
"integrity": "sha512-uZS5shfxzO3rGlu0cC3bjmMFKsXv+SmZZcgp0KD22ts4uGXp5EVYGzu/0YdwZeKmddhcAccYtREJKkPfXkZuCg==",
"license": "MIT",
"dependencies": {
"@types/d3-selection": "*"
}
},
"node_modules/@types/d3-zoom": {
"version": "3.0.8",
"resolved": "https://registry.npmjs.org/@types/d3-zoom/-/d3-zoom-3.0.8.tgz",
"integrity": "sha512-iqMC4/YlFCSlO8+2Ii1GGGliCAY4XdeG748w5vQUbevlbDu0zSjH/+jojorQVBK/se0j6DUFNPBGSqD3YWYnDw==",
"license": "MIT",
"dependencies": {
"@types/d3-interpolate": "*",
"@types/d3-selection": "*"
}
},
"node_modules/@types/dagre": {
"version": "0.7.52",
"resolved": "https://registry.npmjs.org/@types/dagre/-/dagre-0.7.52.tgz",
"integrity": "sha512-XKJdy+OClLk3hketHi9Qg6gTfe1F3y+UFnHxKA2rn9Dw+oXa4Gb378Ztz9HlMgZKSxpPmn4BNVh9wgkpvrK1uw==",
"license": "MIT"
},
"node_modules/@types/debug": {
"version": "4.1.12",
"resolved": "https://registry.npmjs.org/@types/debug/-/debug-4.1.12.tgz",
@ -4382,6 +4458,64 @@
"vite": "^4 || ^5 || ^6"
}
},
"node_modules/@xyflow/react": {
"version": "12.4.4",
"resolved": "https://registry.npmjs.org/@xyflow/react/-/react-12.4.4.tgz",
"integrity": "sha512-9RZ9dgKZNJOlbrXXST5HPb5TcXPOIDGondjwcjDro44OQRPl1E0ZRPTeWPGaQtVjbg4WpR4BUYwOeshNI2TuVg==",
"license": "MIT",
"dependencies": {
"@xyflow/system": "0.0.52",
"classcat": "^5.0.3",
"zustand": "^4.4.0"
},
"peerDependencies": {
"react": ">=17",
"react-dom": ">=17"
}
},
"node_modules/@xyflow/react/node_modules/zustand": {
"version": "4.5.6",
"resolved": "https://registry.npmjs.org/zustand/-/zustand-4.5.6.tgz",
"integrity": "sha512-ibr/n1hBzLLj5Y+yUcU7dYw8p6WnIVzdJbnX+1YpaScvZVF2ziugqHs+LAmHw4lWO9c/zRj+K1ncgWDQuthEdQ==",
"license": "MIT",
"dependencies": {
"use-sync-external-store": "^1.2.2"
},
"engines": {
"node": ">=12.7.0"
},
"peerDependencies": {
"@types/react": ">=16.8",
"immer": ">=9.0.6",
"react": ">=16.8"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
},
"immer": {
"optional": true
},
"react": {
"optional": true
}
}
},
"node_modules/@xyflow/system": {
"version": "0.0.52",
"resolved": "https://registry.npmjs.org/@xyflow/system/-/system-0.0.52.tgz",
"integrity": "sha512-pJBMaoh/GEebIABWEIxAai0yf57dm+kH7J/Br+LnLFPuJL87Fhcmm4KFWd/bCUy/kCWUg+2/yFAGY0AUHRPOnQ==",
"license": "MIT",
"dependencies": {
"@types/d3-drag": "^3.0.7",
"@types/d3-selection": "^3.0.10",
"@types/d3-transition": "^3.0.8",
"@types/d3-zoom": "^3.0.8",
"d3-drag": "^3.0.0",
"d3-selection": "^3.0.0",
"d3-zoom": "^3.0.0"
}
},
"node_modules/acorn": {
"version": "8.14.0",
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.14.0.tgz",
@ -5456,6 +5590,12 @@
"node": ">= 0.10"
}
},
"node_modules/classcat": {
"version": "5.0.5",
"resolved": "https://registry.npmjs.org/classcat/-/classcat-5.0.5.tgz",
"integrity": "sha512-JhZUT7JFcQy/EzW605k/ktHtncoo9vnyW/2GspNYwFlN1C/WmjuV/xtS04e9SOkL2sTdw0VAZ2UGCcQ9lR6p6w==",
"license": "MIT"
},
"node_modules/classnames": {
"version": "2.5.1",
"resolved": "https://registry.npmjs.org/classnames/-/classnames-2.5.1.tgz",
@ -5808,6 +5948,111 @@
"url": "https://polar.sh/cva"
}
},
"node_modules/d3-color": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/d3-color/-/d3-color-3.1.0.tgz",
"integrity": "sha512-zg/chbXyeBtMQ1LbD/WSoW2DpC3I0mpmPdW+ynRTj/x2DAWYrIY7qeZIHidozwV24m4iavr15lNwIwLxRmOxhA==",
"license": "ISC",
"engines": {
"node": ">=12"
}
},
"node_modules/d3-dispatch": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/d3-dispatch/-/d3-dispatch-3.0.1.tgz",
"integrity": "sha512-rzUyPU/S7rwUflMyLc1ETDeBj0NRuHKKAcvukozwhshr6g6c5d8zh4c2gQjY2bZ0dXeGLWc1PF174P2tVvKhfg==",
"license": "ISC",
"engines": {
"node": ">=12"
}
},
"node_modules/d3-drag": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/d3-drag/-/d3-drag-3.0.0.tgz",
"integrity": "sha512-pWbUJLdETVA8lQNJecMxoXfH6x+mO2UQo8rSmZ+QqxcbyA3hfeprFgIT//HW2nlHChWeIIMwS2Fq+gEARkhTkg==",
"license": "ISC",
"dependencies": {
"d3-dispatch": "1 - 3",
"d3-selection": "3"
},
"engines": {
"node": ">=12"
}
},
"node_modules/d3-ease": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/d3-ease/-/d3-ease-3.0.1.tgz",
"integrity": "sha512-wR/XK3D3XcLIZwpbvQwQ5fK+8Ykds1ip7A2Txe0yxncXSdq1L9skcG7blcedkOX+ZcgxGAmLX1FrRGbADwzi0w==",
"license": "BSD-3-Clause",
"engines": {
"node": ">=12"
}
},
"node_modules/d3-interpolate": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/d3-interpolate/-/d3-interpolate-3.0.1.tgz",
"integrity": "sha512-3bYs1rOD33uo8aqJfKP3JWPAibgw8Zm2+L9vBKEHJ2Rg+viTR7o5Mmv5mZcieN+FRYaAOWX5SJATX6k1PWz72g==",
"license": "ISC",
"dependencies": {
"d3-color": "1 - 3"
},
"engines": {
"node": ">=12"
}
},
"node_modules/d3-selection": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/d3-selection/-/d3-selection-3.0.0.tgz",
"integrity": "sha512-fmTRWbNMmsmWq6xJV8D19U/gw/bwrHfNXxrIN+HfZgnzqTHp9jOmKMhsTUjXOJnZOdZY9Q28y4yebKzqDKlxlQ==",
"license": "ISC",
"engines": {
"node": ">=12"
}
},
"node_modules/d3-timer": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/d3-timer/-/d3-timer-3.0.1.tgz",
"integrity": "sha512-ndfJ/JxxMd3nw31uyKoY2naivF+r29V+Lc0svZxe1JvvIRmi8hUsrMvdOwgS1o6uBHmiz91geQ0ylPP0aj1VUA==",
"license": "ISC",
"engines": {
"node": ">=12"
}
},
"node_modules/d3-transition": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/d3-transition/-/d3-transition-3.0.1.tgz",
"integrity": "sha512-ApKvfjsSR6tg06xrL434C0WydLr7JewBB3V+/39RMHsaXTOG0zmt/OAXeng5M5LBm0ojmxJrpomQVZ1aPvBL4w==",
"license": "ISC",
"dependencies": {
"d3-color": "1 - 3",
"d3-dispatch": "1 - 3",
"d3-ease": "1 - 3",
"d3-interpolate": "1 - 3",
"d3-timer": "1 - 3"
},
"engines": {
"node": ">=12"
},
"peerDependencies": {
"d3-selection": "2 - 3"
}
},
"node_modules/d3-zoom": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/d3-zoom/-/d3-zoom-3.0.0.tgz",
"integrity": "sha512-b8AmV3kfQaqWAuacbPuNbL6vahnOJflOhexLzMMNLga62+/nh0JzvJ0aO/5a5MVgUFGS7Hu1P9P03o3fJkDCyw==",
"license": "ISC",
"dependencies": {
"d3-dispatch": "1 - 3",
"d3-drag": "2 - 3",
"d3-interpolate": "1 - 3",
"d3-selection": "2 - 3",
"d3-transition": "2 - 3"
},
"engines": {
"node": ">=12"
}
},
"node_modules/damerau-levenshtein": {
"version": "1.0.8",
"resolved": "https://registry.npmjs.org/damerau-levenshtein/-/damerau-levenshtein-1.0.8.tgz",

@ -14,6 +14,7 @@
"dependencies": {
"@casl/ability": "^6.7.2",
"@casl/react": "^4.0.0",
"@dagrejs/dagre": "^1.1.4",
"@dnd-kit/core": "^6.3.1",
"@dnd-kit/modifiers": "^9.0.0",
"@dnd-kit/sortable": "^10.0.0",
@ -51,8 +52,10 @@
"@tanstack/react-router": "^1.95.1",
"@tanstack/virtual-file-routes": "^1.87.6",
"@tanstack/zod-adapter": "^1.91.0",
"@types/dagre": "^0.7.52",
"@types/nprogress": "^0.2.3",
"@ucast/mongo2js": "^1.3.4",
"@xyflow/react": "^12.4.4",
"argon2-browser": "^1.18.0",
"axios": "^1.7.9",
"classnames": "^2.5.1",

@ -1,93 +0,0 @@
import { useTranslation } from "react-i18next";
import { faWarning } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { useToggle } from "@app/hooks";
import { generateUserBackupKey } from "@app/lib/crypto";
import { createNotification } from "../notifications";
import { generateBackupPDFAsync } from "../utilities/generateBackupPDF";
import { Button } from "../v2";
interface DownloadBackupPDFStepProps {
incrementStep: () => void;
email: string;
password: string;
name: string;
}
/**
* This is the step of the signup flow where the user downloads the backup pdf
* @param {object} obj
* @param {function} obj.incrementStep - function that moves the user on to the next stage of signup
* @param {string} obj.email - user's email
* @param {string} obj.password - user's password
* @param {string} obj.name - user's name
* @returns
*/
export default function DonwloadBackupPDFStep({
incrementStep,
email,
password,
name
}: DownloadBackupPDFStepProps): JSX.Element {
const { t } = useTranslation();
const [isLoading, setIsLoading] = useToggle();
const handleBackupKeyGenerate = async () => {
try {
setIsLoading.on();
const generatedKey = await generateUserBackupKey(email, password);
await generateBackupPDFAsync({
generatedKey,
personalEmail: email,
personalName: name
});
incrementStep();
} catch (err) {
console.log(err);
createNotification({
type: "error",
text: "Failed to generate backup key"
});
} finally {
setIsLoading.off();
}
};
return (
<div className="mx-auto mb-36 flex h-full w-full flex-col items-center md:mb-16 md:px-6">
<p className="flex flex-col items-center justify-center bg-gradient-to-b from-white to-bunker-200 bg-clip-text text-center text-xl font-medium text-transparent">
<FontAwesomeIcon
icon={faWarning}
className="mb-6 ml-2 mr-3 pt-1 text-6xl text-bunker-200"
/>
{t("signup.step4-message")}
</p>
<div className="text-md mt-8 flex w-full max-w-md flex-col items-center justify-center rounded-md border border-mineshaft-600 bg-mineshaft-800 pb-2 text-center text-bunker-300 md:min-w-[24rem] lg:w-1/6">
<div className="m-2 mx-auto mt-4 flex w-full flex-row items-center rounded-md px-3 text-center text-bunker-300 md:mt-8 md:min-w-[23rem] lg:w-1/6">
<span className="mb-2">
{t("signup.step4-description1")} {t("signup.step4-description3")}
</span>
</div>
<div className="mx-auto mb-2 mt-2 flex w-full flex-col items-center justify-center px-3 text-center text-sm md:mb-4 md:mt-4 md:min-w-[20rem] md:max-w-md md:text-left lg:w-1/6">
<div className="text-l w-full py-1 text-lg">
<Button
onClick={handleBackupKeyGenerate}
size="sm"
isFullWidth
isLoading={isLoading}
isDisabled={isLoading}
className="h-12"
colorSchema="primary"
variant="outline_bg"
>
Download PDF
</Button>
</div>
</div>
</div>
</div>
);
}

@ -11,6 +11,7 @@ import { encodeBase64 } from "tweetnacl-util";
import { initProjectHelper } from "@app/helpers/project";
import { completeAccountSignup, useSelectOrganization } from "@app/hooks/api/auth/queries";
import { fetchOrganizations } from "@app/hooks/api/organization/queries";
import { onRequestError } from "@app/hooks/api/reactQuery";
import InputField from "../basic/InputField";
import checkPassword from "../utilities/checks/password/checkPassword";
@ -206,6 +207,7 @@ export default function UserInfoStep({
incrementStep();
} catch (error) {
onRequestError(error);
setIsLoading(false);
console.error(error);
}

@ -8,10 +8,11 @@ import { createNotification } from "@app/components/notifications";
import { Button, FormControl, Input, Modal, ModalContent } from "@app/components/v2";
import { useCreateOrg, useSelectOrganization } from "@app/hooks/api";
import { ProjectType } from "@app/hooks/api/workspace/types";
import { GenericResourceNameSchema } from "@app/lib/schemas";
const schema = z
.object({
name: z.string().nonempty({ message: "Name is required" })
name: GenericResourceNameSchema.nonempty({ message: "Name is required" })
})
.required();
@ -78,7 +79,7 @@ export const CreateOrgModal: FC<CreateOrgModalProps> = ({ isOpen, onClose }) =>
};
return (
<Modal isOpen={isOpen}>
<Modal modal={false} isOpen={isOpen}>
<ModalContent
title="Create Organization"
subTitle="Looks like you're not part of any organizations. Create one to start using Infisical"

@ -0,0 +1,211 @@
import { useCallback, useEffect } from "react";
import { MongoAbility, MongoQuery } from "@casl/ability";
import {
faArrowUpRightFromSquare,
faUpRightAndDownLeftFromCenter,
faWindowRestore
} from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import {
Background,
BackgroundVariant,
ConnectionLineType,
Controls,
Node,
NodeMouseHandler,
Panel,
ReactFlow,
ReactFlowProvider,
useReactFlow
} from "@xyflow/react";
import { twMerge } from "tailwind-merge";
import { Button, IconButton, Spinner, Tooltip } from "@app/components/v2";
import { ProjectPermissionSet } from "@app/context/ProjectPermissionContext";
import { AccessTreeErrorBoundary, AccessTreeProvider, PermissionSimulation } from "./components";
import { BasePermissionEdge } from "./edges";
import { useAccessTree } from "./hooks";
import { FolderNode, RoleNode } from "./nodes";
import { ViewMode } from "./types";
export type AccessTreeProps = {
permissions: MongoAbility<ProjectPermissionSet, MongoQuery>;
};
const EdgeTypes = { base: BasePermissionEdge };
const NodeTypes = { role: RoleNode, folder: FolderNode };
const AccessTreeContent = ({ permissions }: AccessTreeProps) => {
const accessTreeData = useAccessTree(permissions);
const { edges, nodes, isLoading, viewMode, setViewMode } = accessTreeData;
const { fitView, getViewport, setCenter } = useReactFlow();
const onNodeClick: NodeMouseHandler<Node> = useCallback(
(_, node) => {
setCenter(
node.position.x + (node.width ? node.width / 2 : 0),
node.position.y + (node.height ? node.height / 2 + 50 : 50),
{ duration: 1000, zoom: 1 }
);
},
[setCenter]
);
useEffect(() => {
setTimeout(() => {
fitView({
padding: 0.2,
duration: 1000,
maxZoom: 1
});
}, 1);
}, [fitView, nodes, edges, getViewport()]);
const handleToggleModalView = () =>
setViewMode((prev) => (prev === ViewMode.Modal ? ViewMode.Docked : ViewMode.Modal));
const handleToggleUndockedView = () =>
setViewMode((prev) => (prev === ViewMode.Undocked ? ViewMode.Docked : ViewMode.Undocked));
const undockButtonLabel = `${viewMode === ViewMode.Undocked ? "Dock" : "Undock"} View`;
const windowButtonLabel = `${viewMode === ViewMode.Modal ? "Dock" : "Expand"} View`;
return (
<div
className={twMerge(
"w-full",
viewMode === ViewMode.Modal && "fixed inset-0 z-50 p-10",
viewMode === ViewMode.Undocked &&
"fixed bottom-4 left-20 z-50 h-[40%] w-[38%] min-w-[32rem] lg:w-[34%]"
)}
>
<div
className={twMerge(
"mb-4 h-full w-full rounded-lg border border-mineshaft-600 bg-mineshaft-900 transition-transform duration-500",
viewMode === ViewMode.Docked ? "relative p-4" : "relative p-0"
)}
>
{viewMode === ViewMode.Docked && (
<div className="mb-4 flex items-start justify-between border-b border-mineshaft-400 pb-4">
<div>
<h3 className="text-lg font-semibold text-mineshaft-100">Access Tree</h3>
<p className="text-sm leading-3 text-mineshaft-400">
Visual access policies for the configured role.
</p>
</div>
<div className="whitespace-nowrap">
<Button
variant="outline_bg"
colorSchema="secondary"
type="submit"
className="h-10 rounded-r-none bg-mineshaft-700"
leftIcon={<FontAwesomeIcon icon={faWindowRestore} />}
onClick={handleToggleUndockedView}
>
Undock
</Button>
<Button
variant="outline_bg"
colorSchema="secondary"
type="submit"
className="h-10 rounded-l-none bg-mineshaft-600"
leftIcon={<FontAwesomeIcon icon={faUpRightAndDownLeftFromCenter} />}
onClick={handleToggleModalView}
>
Expand
</Button>
</div>
</div>
)}
<div
className={twMerge(
"flex items-center space-x-4",
viewMode === ViewMode.Docked ? "h-96" : "h-full"
)}
>
<div className="h-full w-full">
<ReactFlow
className="rounded-md border border-mineshaft"
nodes={nodes}
edges={edges}
edgeTypes={EdgeTypes}
nodeTypes={NodeTypes}
fitView
onNodeClick={onNodeClick}
colorMode="dark"
nodesDraggable={false}
edgesReconnectable={false}
nodesConnectable={false}
connectionLineType={ConnectionLineType.SmoothStep}
proOptions={{
hideAttribution: false // we need pro license if we want to hide
}}
>
{isLoading && (
<Panel className="flex h-full w-full items-center justify-center">
<Spinner />
</Panel>
)}
{viewMode !== ViewMode.Docked && (
<Panel position="top-right" className="flex gap-1.5">
<Tooltip position="bottom" align="center" content={undockButtonLabel}>
<IconButton
className="mr-1 rounded"
colorSchema="secondary"
variant="plain"
onClick={handleToggleUndockedView}
ariaLabel={undockButtonLabel}
>
<FontAwesomeIcon
icon={
viewMode === ViewMode.Undocked
? faArrowUpRightFromSquare
: faWindowRestore
}
/>
</IconButton>
</Tooltip>
<Tooltip align="end" position="bottom" content={windowButtonLabel}>
<IconButton
className="rounded"
colorSchema="secondary"
variant="plain"
onClick={handleToggleModalView}
ariaLabel={windowButtonLabel}
>
<FontAwesomeIcon
icon={
viewMode === ViewMode.Modal
? faArrowUpRightFromSquare
: faUpRightAndDownLeftFromCenter
}
/>
</IconButton>
</Tooltip>
</Panel>
)}
<PermissionSimulation {...accessTreeData} />
<Background color="#5d5f64" bgColor="#111419" variant={BackgroundVariant.Dots} />
<Controls position="bottom-left" />
</ReactFlow>
</div>
</div>
</div>
</div>
);
};
export const AccessTree = (props: AccessTreeProps) => {
return (
<AccessTreeErrorBoundary {...props}>
<AccessTreeProvider>
<ReactFlowProvider>
<AccessTreeContent {...props} />
</ReactFlowProvider>
</AccessTreeProvider>
</AccessTreeErrorBoundary>
);
};

@ -0,0 +1,51 @@
import React, {
createContext,
Dispatch,
ReactNode,
SetStateAction,
useContext,
useMemo,
useState
} from "react";
import { ViewMode } from "../types";
export interface AccessTreeContextProps {
secretName: string;
setSecretName: Dispatch<SetStateAction<string>>;
viewMode: ViewMode;
setViewMode: Dispatch<SetStateAction<ViewMode>>;
}
const AccessTreeContext = createContext<AccessTreeContextProps | undefined>(undefined);
interface AccessTreeProviderProps {
children: ReactNode;
}
export const AccessTreeProvider: React.FC<AccessTreeProviderProps> = ({ children }) => {
const [secretName, setSecretName] = useState("");
const [viewMode, setViewMode] = useState(ViewMode.Docked);
const value = useMemo(
() => ({
secretName,
setSecretName,
viewMode,
setViewMode
}),
[secretName, setSecretName, viewMode, setViewMode]
);
return <AccessTreeContext.Provider value={value}>{children}</AccessTreeContext.Provider>;
};
export const useAccessTreeContext = (): AccessTreeContextProps => {
const context = useContext(AccessTreeContext);
if (!context) {
throw new Error("useAccessTreeContext must be used within a AccessTreeProvider");
}
return context;
};

@ -0,0 +1,105 @@
import React, { ErrorInfo, ReactNode } from "react";
import { MongoAbility, MongoQuery } from "@casl/ability";
import { faCheck, faCopy, faExclamationTriangle } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { IconButton } from "@app/components/v2";
import { SessionStorageKeys } from "@app/const";
import { ProjectPermissionSet } from "@app/context/ProjectPermissionContext";
import { useTimedReset } from "@app/hooks";
interface ErrorBoundaryProps {
children: ReactNode;
permissions: MongoAbility<ProjectPermissionSet, MongoQuery>;
}
interface ErrorBoundaryState {
hasError: boolean;
error: Error | null;
}
const ErrorDisplay = ({
error,
permissions
}: {
error: Error | null;
permissions: MongoAbility<ProjectPermissionSet, MongoQuery>;
}) => {
const display = JSON.stringify({ errorMessage: error?.message, permissions }, null, 2);
const [isCopied, , setIsCopied] = useTimedReset<boolean>({
initialState: false
});
const copyToClipboard = () => {
navigator.clipboard.writeText(display);
setIsCopied(true);
sessionStorage.removeItem(SessionStorageKeys.CLI_TERMINAL_TOKEN);
};
return (
<div className="flex h-full w-full flex-col gap-2">
<div className="flex items-center gap-2 text-mineshaft-100">
<FontAwesomeIcon icon={faExclamationTriangle} className="text-red" />
<p>
Error displaying access tree. Please contact{" "}
<a
className="inline cursor-pointer text-mineshaft-200 underline decoration-primary-500 underline-offset-4 duration-200 hover:text-mineshaft-100"
target="_blank"
rel="noopener noreferrer"
href="mailto:support@infisical.com"
>
support@infisical.com
</a>{" "}
with the following information.
</p>
</div>
<div className="relative flex flex-1 flex-col overflow-hidden">
<pre className="thin-scrollbar w-full flex-1 overflow-y-auto whitespace-pre-wrap rounded bg-mineshaft-700 p-2 text-xs text-mineshaft-100">
{display}
</pre>
<IconButton
variant="plain"
colorSchema="secondary"
className="absolute right-4 top-2"
ariaLabel="Copy secret value"
onClick={copyToClipboard}
>
<FontAwesomeIcon icon={isCopied ? faCheck : faCopy} />
</IconButton>
</div>
</div>
);
};
class ErrorBoundary extends React.Component<ErrorBoundaryProps, ErrorBoundaryState> {
constructor(props: ErrorBoundaryProps) {
super(props);
this.state = {
hasError: false,
error: null
};
}
static getDerivedStateFromError(error: Error): ErrorBoundaryState {
return { hasError: true, error };
}
componentDidCatch(error: Error, errorInfo: ErrorInfo): void {
console.error("Error caught by ErrorBoundary:", error, errorInfo, this.props);
}
render(): ReactNode {
const { hasError, error } = this.state;
const { children, permissions } = this.props;
if (hasError) {
return <ErrorDisplay error={error} permissions={permissions} />;
}
return children;
}
}
export const AccessTreeErrorBoundary = ({ children, permissions }: ErrorBoundaryProps) => {
return <ErrorBoundary permissions={permissions}>{children}</ErrorBoundary>;
};

@ -0,0 +1,141 @@
import { Dispatch, SetStateAction, useState } from "react";
import { faChevronDown, faChevronUp } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { Panel } from "@xyflow/react";
import { Button, FormLabel, IconButton, Input, Select, SelectItem } from "@app/components/v2";
import { ProjectPermissionSub } from "@app/context";
import { ViewMode } from "../types";
type TProps = {
secretName: string;
setSecretName: Dispatch<SetStateAction<string>>;
viewMode: ViewMode;
setViewMode: Dispatch<SetStateAction<ViewMode>>;
setEnvironment: Dispatch<SetStateAction<string>>;
environment: string;
subject: ProjectPermissionSub;
setSubject: Dispatch<SetStateAction<ProjectPermissionSub>>;
environments: { name: string; slug: string }[];
};
export const PermissionSimulation = ({
setEnvironment,
environment,
subject,
setSubject,
environments,
setViewMode,
viewMode,
secretName,
setSecretName
}: TProps) => {
const [expand, setExpand] = useState(false);
const handlePermissionSimulation = () => {
setExpand(true);
setViewMode(ViewMode.Modal);
};
if (viewMode !== ViewMode.Modal)
return (
<Panel position="top-left">
<Button
size="xs"
className="mr-1 rounded"
colorSchema="secondary"
onClick={handlePermissionSimulation}
>
Permission Simulation
</Button>
</Panel>
);
return (
<Panel
onClick={handlePermissionSimulation}
position="top-left"
className={`group flex flex-col gap-2 pb-4 pr-4 ${expand ? "" : "cursor-pointer"}`}
>
<div className="flex w-[20rem] flex-col gap-1.5 rounded border border-mineshaft-600 bg-mineshaft-800 p-2 font-inter text-gray-200">
<div>
<div className="flex w-full items-center justify-between">
<span className="text-sm">Permission Simulation</span>
<IconButton
variant="plain"
ariaLabel={expand ? "Collapse" : "Expand"}
onClick={(e) => {
e.stopPropagation();
setExpand((prev) => !prev);
}}
>
<FontAwesomeIcon icon={expand ? faChevronUp : faChevronDown} />
</IconButton>
</div>
{expand && (
<p className="mb-2 mt-1 text-xs text-mineshaft-400">
Evaluate conditional policies to see what permissions will be granted given a secret
name or tags
</p>
)}
</div>
{expand && (
<>
<div>
<FormLabel label="Subject" />
<Select
value={subject}
onValueChange={(value) => setSubject(value as ProjectPermissionSub)}
className="w-full border border-mineshaft-500 capitalize"
position="popper"
dropdownContainerClassName="max-w-none"
>
{[
ProjectPermissionSub.Secrets,
ProjectPermissionSub.SecretFolders,
ProjectPermissionSub.DynamicSecrets,
ProjectPermissionSub.SecretImports
].map((sub) => {
return (
<SelectItem className="capitalize" value={sub} key={sub}>
{sub.replace("-", " ")}
</SelectItem>
);
})}
</Select>
</div>
<div>
<FormLabel label="Environment" />
<Select
value={environment}
onValueChange={setEnvironment}
className="w-full border border-mineshaft-500 capitalize"
position="popper"
dropdownContainerClassName="max-w-[19rem]"
>
{environments.map(({ name, slug }) => {
return (
<SelectItem value={slug} key={slug}>
{name}
</SelectItem>
);
})}
</Select>
</div>
{subject === ProjectPermissionSub.Secrets && (
<div>
<FormLabel label="Secret Name" />
<Input
placeholder="*"
value={secretName}
onChange={(e) => setSecretName(e.target.value)}
/>
</div>
)}
</>
)}
</div>
</Panel>
);
};

@ -0,0 +1,3 @@
export * from "./AccessTreeContext";
export * from "./AccessTreeErrorBoundary";
export * from "./PermissionSimulation";

@ -0,0 +1,34 @@
import { BaseEdge, BaseEdgeProps, EdgeProps, getSmoothStepPath } from "@xyflow/react";
export const BasePermissionEdge = ({
id,
sourceX,
sourceY,
targetX,
targetY,
markerStart,
markerEnd,
style
}: Omit<BaseEdgeProps, "path"> & EdgeProps) => {
const [edgePath] = getSmoothStepPath({
sourceX,
sourceY,
targetX,
targetY
});
return (
<BaseEdge
id={id}
markerStart={markerStart}
markerEnd={markerEnd}
style={{
strokeDasharray: "5",
strokeWidth: 1,
stroke: "#707174",
...style
}}
path={edgePath}
/>
);
};

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