From abc2ffca574f89761deb73a84b2bef53297f7ee8 Mon Sep 17 00:00:00 2001
From: Scott Wilson
Date: Thu, 6 Mar 2025 15:41:27 -0800
Subject: [PATCH 01/71] improvement: add organization name constraint
---
backend/src/server/lib/schemas.ts | 7 +
.../server/routes/v1/organization-router.ts | 4 +-
.../server/routes/v2/organization-router.ts | 3 +-
backend/src/server/routes/v3/signup-router.ts | 3 +-
frontend/src/components/auth/UserInfoStep.tsx | 2 +
.../CreateOrgModal/CreateOrgModal.tsx | 2 +-
frontend/src/hooks/api/reactQuery.tsx | 320 +++++++++---------
7 files changed, 174 insertions(+), 167 deletions(-)
diff --git a/backend/src/server/lib/schemas.ts b/backend/src/server/lib/schemas.ts
index ed97cb7d0..28e6b0697 100644
--- a/backend/src/server/lib/schemas.ts
+++ b/backend/src/server/lib/schemas.ts
@@ -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 OrganizationNameSchema = 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");
diff --git a/backend/src/server/routes/v1/organization-router.ts b/backend/src/server/routes/v1/organization-router.ts
index db0008ebe..4e8b120f8 100644
--- a/backend/src/server/routes/v1/organization-router.ts
+++ b/backend/src/server/routes/v1/organization-router.ts
@@ -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 { OrganizationNameSchema, 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: OrganizationNameSchema.optional(),
slug: slugSchema({ max: 64 }).optional(),
authEnforced: z.boolean().optional(),
scimEnabled: z.boolean().optional(),
diff --git a/backend/src/server/routes/v2/organization-router.ts b/backend/src/server/routes/v2/organization-router.ts
index 8ca105ad4..e4f04af52 100644
--- a/backend/src/server/routes/v2/organization-router.ts
+++ b/backend/src/server/routes/v2/organization-router.ts
@@ -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 { OrganizationNameSchema } 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: OrganizationNameSchema
}),
response: {
200: z.object({
diff --git a/backend/src/server/routes/v3/signup-router.ts b/backend/src/server/routes/v3/signup-router.ts
index e95254816..b1c7c7ddc 100644
--- a/backend/src/server/routes/v3/signup-router.ts
+++ b/backend/src/server/routes/v3/signup-router.ts
@@ -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 { OrganizationNameSchema } 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: OrganizationNameSchema,
providerAuthToken: z.string().trim().optional().nullish(),
attributionSource: z.string().trim().optional(),
password: z.string()
diff --git a/frontend/src/components/auth/UserInfoStep.tsx b/frontend/src/components/auth/UserInfoStep.tsx
index 2b7761d1e..582c2a850 100644
--- a/frontend/src/components/auth/UserInfoStep.tsx
+++ b/frontend/src/components/auth/UserInfoStep.tsx
@@ -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);
}
diff --git a/frontend/src/components/organization/CreateOrgModal/CreateOrgModal.tsx b/frontend/src/components/organization/CreateOrgModal/CreateOrgModal.tsx
index b5712e596..c91d8280c 100644
--- a/frontend/src/components/organization/CreateOrgModal/CreateOrgModal.tsx
+++ b/frontend/src/components/organization/CreateOrgModal/CreateOrgModal.tsx
@@ -78,7 +78,7 @@ export const CreateOrgModal: FC = ({ isOpen, onClose }) =>
};
return (
-
+
{
return input.replace(/([a-z])([A-Z])/g, "$1 $2");
};
-export const queryClient = new QueryClient({
- mutationCache: new MutationCache({
- onError: (error) => {
- if (axios.isAxiosError(error)) {
- const serverResponse = error.response?.data as TApiErrors;
- if (serverResponse?.error === ApiErrorTypes.ValidationError) {
- createNotification(
- {
- title: "Validation Error",
- type: "error",
- text: "Please check the input and try again.",
- callToAction: (
-
-
-
-
-
-
-
-
-
- | Field |
- Issue |
-
-
-
- {serverResponse.message?.map(({ message, path }) => (
-
- | {path.join(".")} |
- {message.toLowerCase()} |
-
- ))}
-
-
-
-
-
- ),
- copyActions: [
- {
- value: serverResponse.reqId,
- name: "Request ID",
- label: `Request ID: ${serverResponse.reqId}`
- }
- ]
- },
- { closeOnClick: false }
- );
- return;
- }
- if (serverResponse?.error === ApiErrorTypes.ForbiddenError) {
- createNotification(
- {
- title: "Forbidden Access",
- type: "error",
- text: `${serverResponse.message}.`,
- callToAction: serverResponse?.details?.length ? (
-
-
-
-
-
-
- {serverResponse.details?.map((el, index) => {
- const hasConditions = Boolean(Object.keys(el.conditions || {}).length);
- return (
-
-
- {el.inverted ? "Cannot" : "Can"}{" "}
-
- {el.action.toString().replaceAll(",", ", ")}
- {" "}
- {el.subject.toString()} {hasConditions && "with conditions:"}
-
- {hasConditions && (
-
- {Object.keys(el.conditions || {}).flatMap((field, fieldIndex) => {
- const operators = (
- el.conditions as Record<
- string,
- | string
- | { [K in PermissionConditionOperators]: string | string[] }
- >
- )[field];
-
- const formattedFieldName = camelCaseToSpaces(field).toLowerCase();
- if (typeof operators === "string") {
- return (
- -
-
- {formattedFieldName}
- {" "}
- equal to{" "}
- {operators}
-
- );
- }
-
- return Object.keys(operators).map((operator, operatorIndex) => (
- -
-
- {formattedFieldName}
- {" "}
-
- {
- formatedConditionsOperatorNames[
- operator as PermissionConditionOperators
- ]
- }
- {" "}
-
- {operators[
- operator as PermissionConditionOperators
- ].toString()}
-
-
- ));
- })}
-
- )}
-
- );
- })}
-
-
-
- ) : undefined,
- copyActions: [
- {
- value: serverResponse.reqId,
- name: "Request ID",
- label: `Request ID: ${serverResponse.reqId}`
- }
- ]
- },
- { closeOnClick: false }
- );
- return;
- }
- createNotification({
- title: "Bad Request",
+export const onRequestError = (error: unknown) => {
+ if (axios.isAxiosError(error)) {
+ const serverResponse = error.response?.data as TApiErrors;
+ if (serverResponse?.error === ApiErrorTypes.ValidationError) {
+ createNotification(
+ {
+ title: "Validation Error",
type: "error",
- text: `${serverResponse.message}${serverResponse.message?.endsWith(".") ? "" : "."}`,
+ text: "Please check the input and try again.",
+ callToAction: (
+
+
+
+
+
+
+
+
+
+ | Field |
+ Issue |
+
+
+
+ {serverResponse.message?.map(({ message, path }) => (
+
+ | {path.join(".")} |
+ {message.toLowerCase()} |
+
+ ))}
+
+
+
+
+
+ ),
copyActions: [
{
value: serverResponse.reqId,
@@ -190,9 +67,128 @@ export const queryClient = new QueryClient({
label: `Request ID: ${serverResponse.reqId}`
}
]
- });
- }
+ },
+ { closeOnClick: false }
+ );
+ return;
}
+ if (serverResponse?.error === ApiErrorTypes.ForbiddenError) {
+ createNotification(
+ {
+ title: "Forbidden Access",
+ type: "error",
+ text: `${serverResponse.message}.`,
+ callToAction: serverResponse?.details?.length ? (
+
+
+
+
+
+
+ {serverResponse.details?.map((el, index) => {
+ const hasConditions = Boolean(Object.keys(el.conditions || {}).length);
+ return (
+
+
+ {el.inverted ? "Cannot" : "Can"}{" "}
+
+ {el.action.toString().replaceAll(",", ", ")}
+ {" "}
+ {el.subject.toString()} {hasConditions && "with conditions:"}
+
+ {hasConditions && (
+
+ {Object.keys(el.conditions || {}).flatMap((field, fieldIndex) => {
+ const operators = (
+ el.conditions as Record<
+ string,
+ | string
+ | { [K in PermissionConditionOperators]: string | string[] }
+ >
+ )[field];
+
+ const formattedFieldName = camelCaseToSpaces(field).toLowerCase();
+ if (typeof operators === "string") {
+ return (
+ -
+
+ {formattedFieldName}
+ {" "}
+ equal to{" "}
+ {operators}
+
+ );
+ }
+
+ return Object.keys(operators).map((operator, operatorIndex) => (
+ -
+ {formattedFieldName}{" "}
+
+ {
+ formatedConditionsOperatorNames[
+ operator as PermissionConditionOperators
+ ]
+ }
+ {" "}
+
+ {operators[operator as PermissionConditionOperators].toString()}
+
+
+ ));
+ })}
+
+ )}
+
+ );
+ })}
+
+
+
+ ) : undefined,
+ copyActions: [
+ {
+ value: serverResponse.reqId,
+ name: "Request ID",
+ label: `Request ID: ${serverResponse.reqId}`
+ }
+ ]
+ },
+ { closeOnClick: false }
+ );
+ return;
+ }
+ createNotification({
+ title: "Bad Request",
+ type: "error",
+ text: `${serverResponse.message}${serverResponse.message?.endsWith(".") ? "" : "."}`,
+ copyActions: [
+ {
+ value: serverResponse.reqId,
+ name: "Request ID",
+ label: `Request ID: ${serverResponse.reqId}`
+ }
+ ]
+ });
+ }
+};
+
+export const queryClient = new QueryClient({
+ mutationCache: new MutationCache({
+ onError: onRequestError
}),
defaultOptions: {
queries: {
From 104edca6f11d56c51f9ee2a30f048266053c1217 Mon Sep 17 00:00:00 2001
From: Daniel Hougaard
Date: Fri, 7 Mar 2025 04:34:34 +0400
Subject: [PATCH 02/71] feat: reset password without emergency kit
---
.../src/server/routes/v1/password-router.ts | 13 +-
backend/src/server/routes/v2/index.ts | 2 +
.../src/server/routes/v2/password-router.ts | 53 +++
backend/src/services/auth/auth-fns.ts | 24 +
.../services/auth/auth-password-service.ts | 145 +++++-
.../src/services/auth/auth-password-type.ts | 12 +
backend/src/services/user/user-service.ts | 3 +
.../components/auth/DonwloadBackupPDFStep.tsx | 93 ----
.../checks/password/PasswordCheck.ts | 4 +-
frontend/src/hooks/api/auth/index.tsx | 2 +
frontend/src/hooks/api/auth/queries.tsx | 28 +-
frontend/src/hooks/api/auth/types.ts | 15 +
.../src/pages/admin/SignUpPage/SignUpPage.tsx | 194 +++-----
.../DownloadBackupKeys/DownloadBackupKeys.tsx | 56 ---
.../components/DownloadBackupKeys/index.tsx | 1 -
.../PasswordResetPage/PasswordResetPage.tsx | 445 +++---------------
.../components/ConfirmEmailStep.tsx | 54 +++
.../components/EnterPasswordStep.tsx | 325 +++++++++++++
.../components/InputBackupKeyStep.tsx | 87 ++++
.../SignUpInvitePage/SignUpInvitePage.tsx | 50 +-
.../src/pages/auth/SignUpPage/SignUpPage.tsx | 14 +-
.../auth/SignUpSsoPage/SignUpSsoPage.tsx | 6 -
.../BackupPDFStep/BackupPDFStep.tsx | 71 ---
.../components/BackupPDFStep/index.tsx | 1 -
.../UserInfoSSOStep/UserInfoSSOStep.tsx | 9 +-
.../auth/VerifyEmailPage/VerifyEmailPage.tsx | 5 +-
.../ChangePasswordSection.tsx | 23 +-
.../PersonalGeneralTab/PersonalGeneralTab.tsx | 8 +-
28 files changed, 910 insertions(+), 833 deletions(-)
create mode 100644 backend/src/server/routes/v2/password-router.ts
delete mode 100644 frontend/src/components/auth/DonwloadBackupPDFStep.tsx
delete mode 100644 frontend/src/pages/admin/SignUpPage/components/DownloadBackupKeys/DownloadBackupKeys.tsx
delete mode 100644 frontend/src/pages/admin/SignUpPage/components/DownloadBackupKeys/index.tsx
create mode 100644 frontend/src/pages/auth/PasswordResetPage/components/ConfirmEmailStep.tsx
create mode 100644 frontend/src/pages/auth/PasswordResetPage/components/EnterPasswordStep.tsx
create mode 100644 frontend/src/pages/auth/PasswordResetPage/components/InputBackupKeyStep.tsx
delete mode 100644 frontend/src/pages/auth/SignUpSsoPage/components/BackupPDFStep/BackupPDFStep.tsx
delete mode 100644 frontend/src/pages/auth/SignUpSsoPage/components/BackupPDFStep/index.tsx
diff --git a/backend/src/server/routes/v1/password-router.ts b/backend/src/server/routes/v1/password-router.ts
index e96a577d9..724468e02 100644
--- a/backend/src/server/routes/v1/password-router.ts
+++ b/backend/src/server/routes/v1/password-router.ts
@@ -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;
}
});
diff --git a/backend/src/server/routes/v2/index.ts b/backend/src/server/routes/v2/index.ts
index 3d7581a70..cece502da 100644
--- a/backend/src/server/routes/v2/index.ts
+++ b/backend/src/server/routes/v2/index.ts
@@ -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);
diff --git a/backend/src/server/routes/v2/password-router.ts b/backend/src/server/routes/v2/password-router.ts
new file mode 100644
index 000000000..165130dec
--- /dev/null
+++ b/backend/src/server/routes/v2/password-router.ts
@@ -0,0 +1,53 @@
+import { z } from "zod";
+
+import { authRateLimit } from "@app/server/config/rateLimiter";
+import { validatePasswordResetAuthorization } from "@app/services/auth/auth-fns";
+import { AuthMode } from "@app/services/auth/auth-type";
+import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
+import { ResetPasswordV2Type } from "@app/services/auth/auth-password-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
+ });
+ }
+ });
+};
diff --git a/backend/src/services/auth/auth-fns.ts b/backend/src/services/auth/auth-fns.ts
index 5f7aca812..c72f6c6a7 100644
--- a/backend/src/services/auth/auth-fns.ts
+++ b/backend/src/services/auth/auth-fns.ts
@@ -45,6 +45,30 @@ 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;
+
+ return decodedToken;
+};
+
export const enforceUserLockStatus = (isLocked: boolean, temporaryLockDateEnd?: Date | null) => {
if (isLocked) {
throw new ForbiddenRequestError({
diff --git a/backend/src/services/auth/auth-password-service.ts b/backend/src/services/auth/auth-password-service.ts
index 9f004eafc..349bcb3d0 100644
--- a/backend/src/services/auth/auth-password-service.ts
+++ b/backend/src/services/auth/auth-password-service.ts
@@ -4,6 +4,8 @@ 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 { OrgServiceActor } from "@app/lib/types";
@@ -12,10 +14,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 +119,35 @@ export const authPaswordServiceFactory = ({
* Email password reset flow via email. Step 1 send email
*/
const sendPasswordResetEmail = async (email: string) => {
+ const startTime = Date.now();
+
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 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` : ""
+ }
+ });
+ }
+
+ const elapsedTime = Date.now() - startTime;
+ // daniel: ensure each request takes 8 seconds to prevent timing attacks
+ if (elapsedTime < 8_000) {
+ // eslint-disable-next-line no-promise-executor-return
+ await new Promise((resolve) => setTimeout(resolve, 8_000 - elapsedTime));
+ }
};
/*
@@ -142,6 +156,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 +181,95 @@ 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.transaction(async (tx) => {
+ 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
+ },
+ tx
+ );
+ });
+ };
+
/*
* Reset password of a user via backup key
* */
@@ -391,6 +497,7 @@ export const authPaswordServiceFactory = ({
createBackupPrivateKey,
getBackupPrivateKeyOfUser,
sendPasswordSetupEmail,
- setupPassword
+ setupPassword,
+ resetPasswordV2
};
};
diff --git a/backend/src/services/auth/auth-password-type.ts b/backend/src/services/auth/auth-password-type.ts
index 7c67c0934..b3b14c3b4 100644
--- a/backend/src/services/auth/auth-password-type.ts
+++ b/backend/src/services/auth/auth-password-type.ts
@@ -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;
diff --git a/backend/src/services/user/user-service.ts b/backend/src/services/user/user-service.ts
index 5da5d493c..d272845c4 100644
--- a/backend/src/services/user/user-service.ts
+++ b/backend/src/services/user/user-service.ts
@@ -3,7 +3,9 @@ import { ForbiddenError } from "@casl/ability";
import { SecretKeyEncoding } from "@app/db/schemas";
import { OrgPermissionActions, OrgPermissionSubjects } from "@app/ee/services/permission/org-permission";
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service";
+
import { infisicalSymmetricDecrypt } from "@app/lib/crypto/encryption";
+
import { BadRequestError, ForbiddenRequestError, NotFoundError } from "@app/lib/errors";
import { TAuthTokenServiceFactory } from "@app/services/auth-token/auth-token-service";
import { TokenType } from "@app/services/auth-token/auth-token-types";
@@ -31,6 +33,7 @@ type TUserServiceFactoryDep = {
| "createUserAction"
| "findUserEncKeyByUserId"
| "delete"
+ | "updateUserEncryptionByUserId"
>;
userAliasDAL: Pick;
groupProjectDAL: Pick;
diff --git a/frontend/src/components/auth/DonwloadBackupPDFStep.tsx b/frontend/src/components/auth/DonwloadBackupPDFStep.tsx
deleted file mode 100644
index b11ca8bd2..000000000
--- a/frontend/src/components/auth/DonwloadBackupPDFStep.tsx
+++ /dev/null
@@ -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 (
-
-
-
- {t("signup.step4-message")}
-
-
-
-
- {t("signup.step4-description1")} {t("signup.step4-description3")}
-
-
-
-
-
- );
-}
diff --git a/frontend/src/components/utilities/checks/password/PasswordCheck.ts b/frontend/src/components/utilities/checks/password/PasswordCheck.ts
index e37abd475..fb5186220 100644
--- a/frontend/src/components/utilities/checks/password/PasswordCheck.ts
+++ b/frontend/src/components/utilities/checks/password/PasswordCheck.ts
@@ -34,12 +34,12 @@ const passwordCheck = async ({
const tests = [
{
name: "tooShort",
- validator: (pwd: string) => pwd.length >= 14,
+ validator: (pwd: string) => pwd?.length >= 14,
setError: setPasswordErrorTooShort
},
{
name: "tooLong",
- validator: (pwd: string) => pwd.length < 101,
+ validator: (pwd: string) => pwd?.length < 101,
setError: setPasswordErrorTooLong
},
{
diff --git a/frontend/src/hooks/api/auth/index.tsx b/frontend/src/hooks/api/auth/index.tsx
index 66688cbdc..392f7b1f5 100644
--- a/frontend/src/hooks/api/auth/index.tsx
+++ b/frontend/src/hooks/api/auth/index.tsx
@@ -2,6 +2,8 @@ export {
useGetAuthToken,
useOauthTokenExchange,
useResetPassword,
+ useResetPasswordV2,
+ useResetUserPasswordV2,
useSelectOrganization,
useSendMfaToken,
useSendPasswordResetEmail,
diff --git a/frontend/src/hooks/api/auth/queries.tsx b/frontend/src/hooks/api/auth/queries.tsx
index 9b8afbac7..796fd3152 100644
--- a/frontend/src/hooks/api/auth/queries.tsx
+++ b/frontend/src/hooks/api/auth/queries.tsx
@@ -22,12 +22,15 @@ import {
LoginLDAPRes,
MfaMethod,
ResetPasswordDTO,
+ ResetPasswordV2DTO,
+ ResetUserPasswordV2DTO,
SendMfaTokenDTO,
SetupPasswordDTO,
SRP1DTO,
SRPR1Res,
TOauthTokenExchangeDTO,
UserAgentType,
+ UserEncryptionVersion,
VerifyMfaTokenDTO,
VerifyMfaTokenRes,
VerifySignupInviteDTO
@@ -247,7 +250,10 @@ export const useSendPasswordResetEmail = () => {
export const useVerifyPasswordResetCode = () => {
return useMutation({
mutationFn: async ({ email, code }: { email: string; code: string }) => {
- const { data } = await apiRequest.post("/api/v1/password/email/password-reset-verify", {
+ const { data } = await apiRequest.post<{
+ token: string;
+ userEncryptionVersion: UserEncryptionVersion;
+ }>("/api/v1/password/email/password-reset-verify", {
email,
code
});
@@ -302,6 +308,26 @@ export const useResetPassword = () => {
});
};
+export const useResetPasswordV2 = () => {
+ return useMutation({
+ mutationFn: async (details: ResetPasswordV2DTO) => {
+ await apiRequest.post("/api/v2/password/password-reset", details, {
+ headers: {
+ Authorization: `Bearer ${details.verificationToken}`
+ }
+ });
+ }
+ });
+};
+
+export const useResetUserPasswordV2 = () => {
+ return useMutation({
+ mutationFn: async (details: ResetUserPasswordV2DTO) => {
+ await apiRequest.post("/api/v2/password/user/password-reset", details);
+ }
+ });
+};
+
export const changePassword = async (details: ChangePasswordDTO) => {
const { data } = await apiRequest.post("/api/v1/password/change-password", details);
return data;
diff --git a/frontend/src/hooks/api/auth/types.ts b/frontend/src/hooks/api/auth/types.ts
index 036897fed..32610c28d 100644
--- a/frontend/src/hooks/api/auth/types.ts
+++ b/frontend/src/hooks/api/auth/types.ts
@@ -3,6 +3,11 @@ export type GetAuthTokenAPI = {
organizationId?: string;
};
+export enum UserEncryptionVersion {
+ V1 = 1,
+ V2 = 2
+}
+
export type SendMfaTokenDTO = {
email: string;
};
@@ -136,6 +141,16 @@ export type ResetPasswordDTO = {
password: string;
};
+export type ResetPasswordV2DTO = {
+ newPassword: string;
+ verificationToken: string;
+};
+
+export type ResetUserPasswordV2DTO = {
+ oldPassword: string;
+ newPassword: string;
+};
+
export type SetupPasswordDTO = {
protectedKey: string;
protectedKeyIV: string;
diff --git a/frontend/src/pages/admin/SignUpPage/SignUpPage.tsx b/frontend/src/pages/admin/SignUpPage/SignUpPage.tsx
index f1c70d028..97b363558 100644
--- a/frontend/src/pages/admin/SignUpPage/SignUpPage.tsx
+++ b/frontend/src/pages/admin/SignUpPage/SignUpPage.tsx
@@ -1,4 +1,3 @@
-import { useState } from "react";
import { Helmet } from "react-helmet";
import { Controller, useForm } from "react-hook-form";
import { useTranslation } from "react-i18next";
@@ -8,16 +7,13 @@ import { AnimatePresence, motion } from "framer-motion";
import { z } from "zod";
import { createNotification } from "@app/components/notifications";
-import { generateBackupPDFAsync } from "@app/components/utilities/generateBackupPDF";
// TODO(akhilmhdh): rewrite this into module functions in lib
import { saveTokenToLocalStorage } from "@app/components/utilities/saveTokenToLocalStorage";
import SecurityClient from "@app/components/utilities/SecurityClient";
import { Button, ContentLoader, FormControl, Input } from "@app/components/v2";
import { useServerConfig } from "@app/context";
import { useCreateAdminUser, useSelectOrganization } from "@app/hooks/api";
-import { generateUserBackupKey, generateUserPassKey } from "@app/lib/crypto";
-
-import { DownloadBackupKeys } from "./components/DownloadBackupKeys";
+import { generateUserPassKey } from "@app/lib/crypto";
const formSchema = z
.object({
@@ -34,25 +30,17 @@ const formSchema = z
type TFormSchema = z.infer;
-enum SignupSteps {
- DetailsForm = "details-form",
- BackupKey = "backup-key"
-}
-
export const SignUpPage = () => {
const { t } = useTranslation();
const navigate = useNavigate();
const {
control,
handleSubmit,
- getValues,
formState: { isSubmitting }
} = useForm({
resolver: zodResolver(formSchema)
});
- const [step, setStep] = useState(SignupSteps.DetailsForm);
-
const { config } = useServerConfig();
const { mutateAsync: createAdminUser } = useCreateAdminUser();
const { mutateAsync: selectOrganization } = useSelectOrganization();
@@ -84,7 +72,7 @@ export const SignUpPage = () => {
// Will be refactored in next iteration to make it url based rather than local storage ones
// Part of migration to nextjs 14
localStorage.setItem("orgData.id", res.organization.id);
- setStep(SignupSteps.BackupKey);
+ navigate({ to: "/admin" });
} catch (err) {
console.log(err);
createNotification({
@@ -94,27 +82,7 @@ export const SignUpPage = () => {
}
};
- const handleBackupKeyGenerate = async () => {
- try {
- const { email, password, firstName, lastName } = getValues();
- const generatedKey = await generateUserBackupKey(email, password);
- await generateBackupPDFAsync({
- generatedKey,
- personalEmail: email,
- personalName: `${firstName} ${lastName}`
- });
- navigate({ to: "/admin" });
- } catch (err) {
- console.log(err);
- createNotification({
- type: "error",
- text: "Failed to generate backup"
- });
- }
- };
-
- if (config?.initialized && step === SignupSteps.DetailsForm)
- return ;
+ if (config?.initialized) return ;
return (
@@ -127,56 +95,28 @@ export const SignUpPage = () => {
- {step === SignupSteps.DetailsForm && (
-
-
-

-
Welcome to Infisical
-
Create your first Super Admin Account
-
-
diff --git a/frontend/src/pages/admin/SignUpPage/components/DownloadBackupKeys/DownloadBackupKeys.tsx b/frontend/src/pages/admin/SignUpPage/components/DownloadBackupKeys/DownloadBackupKeys.tsx
deleted file mode 100644
index 253031a91..000000000
--- a/frontend/src/pages/admin/SignUpPage/components/DownloadBackupKeys/DownloadBackupKeys.tsx
+++ /dev/null
@@ -1,56 +0,0 @@
-import { useTranslation } from "react-i18next";
-import { faWarning } from "@fortawesome/free-solid-svg-icons";
-import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
-
-import { Button } from "@app/components/v2";
-import { useToggle } from "@app/hooks";
-
-type Props = {
- onGenerate: () => Promise
;
-};
-
-export const DownloadBackupKeys = ({ onGenerate }: Props): JSX.Element => {
- const { t } = useTranslation();
- const [isLoading, setIsLoading] = useToggle();
-
- return (
-
-
-
- {t("signup.step4-message")}
-
-
-
-
- {t("signup.step4-description1")} {t("signup.step4-description3")}
-
-
-
-
-
-
-
-
-
- );
-};
diff --git a/frontend/src/pages/admin/SignUpPage/components/DownloadBackupKeys/index.tsx b/frontend/src/pages/admin/SignUpPage/components/DownloadBackupKeys/index.tsx
deleted file mode 100644
index bbbd9aad9..000000000
--- a/frontend/src/pages/admin/SignUpPage/components/DownloadBackupKeys/index.tsx
+++ /dev/null
@@ -1 +0,0 @@
-export { DownloadBackupKeys } from "./DownloadBackupKeys";
diff --git a/frontend/src/pages/auth/PasswordResetPage/PasswordResetPage.tsx b/frontend/src/pages/auth/PasswordResetPage/PasswordResetPage.tsx
index 3361dd961..8b6be6b9a 100644
--- a/frontend/src/pages/auth/PasswordResetPage/PasswordResetPage.tsx
+++ b/frontend/src/pages/auth/PasswordResetPage/PasswordResetPage.tsx
@@ -1,396 +1,75 @@
-import crypto from "crypto";
+import { useState } from "react";
+import { useForm } from "react-hook-form";
+import { zodResolver } from "@hookform/resolvers/zod";
+import { useNavigate } from "@tanstack/react-router";
+import { z } from "zod";
-import { FormEvent, useState } from "react";
-import { faCheck, faX } from "@fortawesome/free-solid-svg-icons";
-import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
-import { useNavigate, useSearch } from "@tanstack/react-router";
-import jsrp from "jsrp";
+import { UserEncryptionVersion } from "@app/hooks/api/auth/types";
-import InputField from "@app/components/basic/InputField";
-import passwordCheck from "@app/components/utilities/checks/password/PasswordCheck";
-import Aes256Gcm from "@app/components/utilities/cryptography/aes-256-gcm";
-import { deriveArgonKey } from "@app/components/utilities/cryptography/crypto";
-import { Button } from "@app/components/v2";
-import { ROUTE_PATHS } from "@app/const/routes";
-import { useResetPassword, useVerifyPasswordResetCode } from "@app/hooks/api";
-import { getBackupEncryptedPrivateKey } from "@app/hooks/api/auth/queries";
+import { ConfirmEmailStep } from "./components/ConfirmEmailStep";
+import { EnterPasswordStep } from "./components/EnterPasswordStep";
+import { InputBackupKeyStep } from "./components/InputBackupKeyStep";
-// eslint-disable-next-line new-cap
-const client = new jsrp.client();
+enum Steps {
+ ConfirmEmail = 1,
+ InputBackupKey = 2,
+ EnterNewPassword = 3
+}
+
+const formData = z.object({
+ verificationToken: z.string(),
+ privateKey: z.string(),
+ userEncryptionVersion: z.nativeEnum(UserEncryptionVersion)
+});
+type TFormData = z.infer;
export const PasswordResetPage = () => {
- const [verificationToken, setVerificationToken] = useState("");
- const [step, setStep] = useState(1);
- const [loading, setLoading] = useState(false);
- const [backupKey, setBackupKey] = useState("");
- const [privateKey, setPrivateKey] = useState("");
- const [newPassword, setNewPassword] = useState("");
- const [backupKeyError, setBackupKeyError] = useState(false);
- const [passwordErrorTooShort, setPasswordErrorTooShort] = useState(false);
- const [passwordErrorTooLong, setPasswordErrorTooLong] = useState(false);
- const [passwordErrorNoLetterChar, setPasswordErrorNoLetterChar] = useState(false);
- const [passwordErrorNoNumOrSpecialChar, setPasswordErrorNoNumOrSpecialChar] = useState(false);
- const [passwordErrorRepeatedChar, setPasswordErrorRepeatedChar] = useState(false);
- const [passwordErrorEscapeChar, setPasswordErrorEscapeChar] = useState(false);
- const [passwordErrorLowEntropy, setPasswordErrorLowEntropy] = useState(false);
- const [passwordErrorBreached, setPasswordErrorBreached] = useState(false);
+ const { watch, setValue } = useForm({
+ resolver: zodResolver(formData)
+ });
+ const verificationToken = watch("verificationToken");
+ const encryptionVersion = watch("userEncryptionVersion");
+ const privateKey = watch("privateKey");
+
+ const [step, setStep] = useState(Steps.ConfirmEmail);
const navigate = useNavigate();
- const search = useSearch({ from: ROUTE_PATHS.Auth.PasswordResetPage.id });
-
- const {
- mutateAsync: verifyPasswordResetCodeMutateAsync,
- isPending: isVerifyPasswordResetLoading
- } = useVerifyPasswordResetCode();
- const { mutateAsync: resetPasswordMutateAsync } = useResetPassword();
-
- const parsedUrl = search;
- const token = parsedUrl.token as string;
- const email = (parsedUrl.to as string)?.replace(" ", "+").trim();
-
- // Decrypt the private key with a backup key
- const getEncryptedKeyHandler = async (e: FormEvent) => {
- e.preventDefault();
- try {
- const result = await getBackupEncryptedPrivateKey({ verificationToken });
-
- setPrivateKey(
- Aes256Gcm.decrypt({
- ciphertext: result.encryptedPrivateKey,
- iv: result.iv,
- tag: result.tag,
- secret: backupKey
- })
- );
- setStep(3);
- } catch (err) {
- console.error(err);
- setBackupKeyError(true);
- }
- };
-
- // If everything is correct, reset the password
- const resetPasswordHandler = async (e: FormEvent) => {
- e.preventDefault();
- const errorCheck = await passwordCheck({
- password: newPassword,
- setPasswordErrorTooShort,
- setPasswordErrorTooLong,
- setPasswordErrorNoLetterChar,
- setPasswordErrorNoNumOrSpecialChar,
- setPasswordErrorRepeatedChar,
- setPasswordErrorEscapeChar,
- setPasswordErrorLowEntropy,
- setPasswordErrorBreached
- });
-
- if (!errorCheck) {
- client.init(
- {
- username: email,
- password: newPassword
- },
- async () => {
- client.createVerifier(async (_err: any, result: { salt: string; verifier: string }) => {
- const derivedKey = await deriveArgonKey({
- password: newPassword,
- salt: result.salt,
- mem: 65536,
- time: 3,
- parallelism: 1,
- hashLen: 32
- });
-
- if (!derivedKey) throw new Error("Failed to derive key from password");
-
- const key = crypto.randomBytes(32);
-
- // create encrypted private key by encrypting the private
- // key with the symmetric key [key]
- const {
- ciphertext: encryptedPrivateKey,
- iv: encryptedPrivateKeyIV,
- tag: encryptedPrivateKeyTag
- } = Aes256Gcm.encrypt({
- text: privateKey,
- secret: key
- });
-
- // create the protected key by encrypting the symmetric key
- // [key] with the derived key
- const {
- ciphertext: protectedKey,
- iv: protectedKeyIV,
- tag: protectedKeyTag
- } = Aes256Gcm.encrypt({
- text: key.toString("hex"),
- secret: Buffer.from(derivedKey.hash)
- });
-
- await resetPasswordMutateAsync({
- protectedKey,
- protectedKeyIV,
- protectedKeyTag,
- encryptedPrivateKey,
- encryptedPrivateKeyIV,
- encryptedPrivateKeyTag,
- salt: result.salt,
- verifier: result.verifier,
- verificationToken,
- password: newPassword
- });
-
- navigate({ to: "/login" });
-
- setLoading(false);
- });
- }
- );
- }
- };
-
- // Click a button to confirm email
- const stepConfirmEmail = (
-
-
- Confirm your email
-
-

-
-
-
-
- );
-
- // Input backup key
- const stepInputBackupKey = (
-
- );
-
- // Enter new password
- const stepEnterNewPassword = (
-
- );
return (
- {step === 1 && stepConfirmEmail}
- {step === 2 && stepInputBackupKey}
- {step === 3 && stepEnterNewPassword}
+ {step === Steps.ConfirmEmail && (
+ {
+ setValue("verificationToken", verifyToken);
+ setValue("userEncryptionVersion", userEncryptionVersion);
+
+ if (userEncryptionVersion === UserEncryptionVersion.V2) {
+ setStep(Steps.EnterNewPassword);
+ } else {
+ setStep(Steps.InputBackupKey);
+ }
+ }}
+ />
+ )}
+ {step === Steps.InputBackupKey && (
+ {
+ setValue("privateKey", key);
+ setStep(Steps.EnterNewPassword);
+ }}
+ />
+ )}
+ {step === Steps.EnterNewPassword && (
+ {
+ navigate({ to: "/login" });
+ }}
+ />
+ )}
);
};
diff --git a/frontend/src/pages/auth/PasswordResetPage/components/ConfirmEmailStep.tsx b/frontend/src/pages/auth/PasswordResetPage/components/ConfirmEmailStep.tsx
new file mode 100644
index 000000000..1f1a79490
--- /dev/null
+++ b/frontend/src/pages/auth/PasswordResetPage/components/ConfirmEmailStep.tsx
@@ -0,0 +1,54 @@
+import { useNavigate, useSearch } from "@tanstack/react-router";
+
+import { Button } from "@app/components/v2";
+import { ROUTE_PATHS } from "@app/const/routes";
+import { useVerifyPasswordResetCode } from "@app/hooks/api";
+import { UserEncryptionVersion } from "@app/hooks/api/auth/types";
+
+type Props = {
+ onComplete: (verificationToken: string, encryptionVersion: UserEncryptionVersion) => void;
+};
+
+export const ConfirmEmailStep = ({ onComplete }: Props) => {
+ const navigate = useNavigate();
+ const search = useSearch({ from: ROUTE_PATHS.Auth.PasswordResetPage.id });
+ const { token, to: email } = search;
+
+ const {
+ mutateAsync: verifyPasswordResetCodeMutateAsync,
+ isPending: isVerifyPasswordResetLoading
+ } = useVerifyPasswordResetCode();
+ return (
+
+
+ Confirm your email
+
+

+
+
+
+
+ );
+};
diff --git a/frontend/src/pages/auth/PasswordResetPage/components/EnterPasswordStep.tsx b/frontend/src/pages/auth/PasswordResetPage/components/EnterPasswordStep.tsx
new file mode 100644
index 000000000..de7bcd3f3
--- /dev/null
+++ b/frontend/src/pages/auth/PasswordResetPage/components/EnterPasswordStep.tsx
@@ -0,0 +1,325 @@
+import crypto from "crypto";
+
+import { Controller, useForm } from "react-hook-form";
+import { faCheck, faX } from "@fortawesome/free-solid-svg-icons";
+import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
+import { zodResolver } from "@hookform/resolvers/zod";
+import { useSearch } from "@tanstack/react-router";
+import jsrp from "jsrp";
+import { z } from "zod";
+
+import passwordCheck from "@app/components/utilities/checks/password/PasswordCheck";
+import Aes256Gcm from "@app/components/utilities/cryptography/aes-256-gcm";
+import { deriveArgonKey } from "@app/components/utilities/cryptography/crypto";
+import { Button, FormControl, Input } from "@app/components/v2";
+import { ROUTE_PATHS } from "@app/const/routes";
+import { useResetPassword, useResetPasswordV2 } from "@app/hooks/api";
+import { UserEncryptionVersion } from "@app/hooks/api/auth/types";
+
+const formData = z.object({
+ password: z.string(),
+ passwordErrorTooShort: z.boolean().optional(),
+ passwordErrorTooLong: z.boolean().optional(),
+ passwordErrorNoLetterChar: z.boolean().optional(),
+ passwordErrorNoNumOrSpecialChar: z.boolean().optional(),
+ passwordErrorRepeatedChar: z.boolean().optional(),
+ passwordErrorEscapeChar: z.boolean().optional(),
+ passwordErrorLowEntropy: z.boolean().optional(),
+ passwordErrorBreached: z.boolean()
+});
+type TFormData = z.infer;
+
+type Props = {
+ verificationToken: string;
+ privateKey: string;
+ encryptionVersion: UserEncryptionVersion;
+ onComplete: () => void;
+};
+
+export const EnterPasswordStep = ({
+ verificationToken,
+ encryptionVersion,
+ privateKey,
+ onComplete
+}: Props) => {
+ const search = useSearch({ from: ROUTE_PATHS.Auth.PasswordResetPage.id });
+ const { to: email } = search;
+
+ const {
+ control,
+ watch,
+ handleSubmit,
+ setValue,
+ formState: { isSubmitting }
+ } = useForm({
+ resolver: zodResolver(formData)
+ });
+ const { mutateAsync: resetPassword, isPending: isLoading } = useResetPassword();
+ const { mutateAsync: resetPasswordV2, isPending: isLoadingV2 } = useResetPasswordV2();
+
+ const passwordErrorTooShort = watch("passwordErrorTooShort");
+ const passwordErrorTooLong = watch("passwordErrorTooLong");
+ const passwordErrorNoLetterChar = watch("passwordErrorNoLetterChar");
+ const passwordErrorNoNumOrSpecialChar = watch("passwordErrorNoNumOrSpecialChar");
+ const passwordErrorRepeatedChar = watch("passwordErrorRepeatedChar");
+ const passwordErrorEscapeChar = watch("passwordErrorEscapeChar");
+ const passwordErrorLowEntropy = watch("passwordErrorLowEntropy");
+ const passwordErrorBreached = watch("passwordErrorBreached");
+
+ const isPasswordError =
+ passwordErrorTooShort ||
+ passwordErrorTooLong ||
+ passwordErrorNoLetterChar ||
+ passwordErrorNoNumOrSpecialChar ||
+ passwordErrorRepeatedChar ||
+ passwordErrorEscapeChar ||
+ passwordErrorLowEntropy ||
+ passwordErrorBreached;
+
+ const handlePasswordCheck = async (checkPassword: string) => {
+ const errorCheck = await passwordCheck({
+ password: checkPassword,
+ setPasswordErrorTooShort: (v) => setValue("passwordErrorTooShort", v),
+ setPasswordErrorTooLong: (v) => setValue("passwordErrorTooLong", v),
+ setPasswordErrorNoLetterChar: (v) => setValue("passwordErrorNoLetterChar", v),
+ setPasswordErrorNoNumOrSpecialChar: (v) => setValue("passwordErrorNoNumOrSpecialChar", v),
+ setPasswordErrorRepeatedChar: (v) => setValue("passwordErrorRepeatedChar", v),
+ setPasswordErrorEscapeChar: (v) => setValue("passwordErrorEscapeChar", v),
+ setPasswordErrorLowEntropy: (v) => setValue("passwordErrorLowEntropy", v),
+ setPasswordErrorBreached: (v) => setValue("passwordErrorBreached", v)
+ });
+
+ return errorCheck;
+ };
+
+ const resetPasswordHandler = async (data: TFormData) => {
+ const errorCheck = await handlePasswordCheck(data.password);
+
+ if (errorCheck) return;
+
+ if (encryptionVersion === UserEncryptionVersion.V2) {
+ await resetPasswordV2({
+ newPassword: data.password,
+ verificationToken
+ });
+ } else {
+ // eslint-disable-next-line new-cap
+ const client = new jsrp.client();
+ client.init(
+ {
+ username: email,
+ password: data.password
+ },
+ async () => {
+ client.createVerifier(async (_err: any, result: { salt: string; verifier: string }) => {
+ const derivedKey = await deriveArgonKey({
+ password: data.password,
+ salt: result.salt,
+ mem: 65536,
+ time: 3,
+ parallelism: 1,
+ hashLen: 32
+ });
+
+ if (!derivedKey) throw new Error("Failed to derive key from password");
+
+ const key = crypto.randomBytes(32);
+
+ // create encrypted private key by encrypting the private
+ // key with the symmetric key [key]
+ const {
+ ciphertext: encryptedPrivateKey,
+ iv: encryptedPrivateKeyIV,
+ tag: encryptedPrivateKeyTag
+ } = Aes256Gcm.encrypt({
+ text: privateKey,
+ secret: key
+ });
+
+ // create the protected key by encrypting the symmetric key
+ // [key] with the derived key
+ const {
+ ciphertext: protectedKey,
+ iv: protectedKeyIV,
+ tag: protectedKeyTag
+ } = Aes256Gcm.encrypt({
+ text: key.toString("hex"),
+ secret: Buffer.from(derivedKey.hash)
+ });
+
+ await resetPassword({
+ protectedKey,
+ protectedKeyIV,
+ protectedKeyTag,
+ encryptedPrivateKey,
+ encryptedPrivateKeyIV,
+ encryptedPrivateKeyTag,
+ salt: result.salt,
+ verifier: result.verifier,
+ verificationToken,
+ password: data.password
+ });
+ });
+ }
+ );
+ }
+ onComplete();
+ };
+
+ return (
+
+ );
+};
diff --git a/frontend/src/pages/auth/PasswordResetPage/components/InputBackupKeyStep.tsx b/frontend/src/pages/auth/PasswordResetPage/components/InputBackupKeyStep.tsx
new file mode 100644
index 000000000..54a5d5e9d
--- /dev/null
+++ b/frontend/src/pages/auth/PasswordResetPage/components/InputBackupKeyStep.tsx
@@ -0,0 +1,87 @@
+import { Controller, useForm } from "react-hook-form";
+import { zodResolver } from "@hookform/resolvers/zod";
+import { z } from "zod";
+
+import Aes256Gcm from "@app/components/utilities/cryptography/aes-256-gcm";
+import { Button, FormControl, Input } from "@app/components/v2";
+import { getBackupEncryptedPrivateKey } from "@app/hooks/api/auth/queries";
+
+type Props = {
+ verificationToken: string;
+ onComplete: (privateKey: string) => void;
+};
+
+const formData = z.object({
+ backupKey: z.string()
+});
+type TFormData = z.infer;
+
+export const InputBackupKeyStep = ({ verificationToken, onComplete }: Props) => {
+ const { control, handleSubmit, setError } = useForm({
+ resolver: zodResolver(formData)
+ });
+
+ const getEncryptedKeyHandler = async (data: z.infer) => {
+ try {
+ const result = await getBackupEncryptedPrivateKey({ verificationToken });
+
+ const privateKey = Aes256Gcm.decrypt({
+ ciphertext: result.encryptedPrivateKey,
+ iv: result.iv,
+ tag: result.tag,
+ secret: data.backupKey
+ });
+
+ onComplete(privateKey);
+ // setStep(3);
+ } catch (err) {
+ console.error(err);
+ setError("backupKey", { message: "Failed to decrypt private key" });
+ }
+ };
+
+ return (
+
+ );
+};
diff --git a/frontend/src/pages/auth/SignUpInvitePage/SignUpInvitePage.tsx b/frontend/src/pages/auth/SignUpInvitePage/SignUpInvitePage.tsx
index d3106e341..7cf4ad572 100644
--- a/frontend/src/pages/auth/SignUpInvitePage/SignUpInvitePage.tsx
+++ b/frontend/src/pages/auth/SignUpInvitePage/SignUpInvitePage.tsx
@@ -4,7 +4,7 @@ import crypto from "crypto";
import { useState } from "react";
import { Helmet } from "react-helmet";
-import { faWarning, faXmark } from "@fortawesome/free-solid-svg-icons";
+import { faXmark } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { Link, useNavigate, useSearch } from "@tanstack/react-router";
import jsrp from "jsrp";
@@ -16,7 +16,6 @@ import InputField from "@app/components/basic/InputField";
import checkPassword from "@app/components/utilities/checks/password/checkPassword";
import Aes256Gcm from "@app/components/utilities/cryptography/aes-256-gcm";
import { deriveArgonKey } from "@app/components/utilities/cryptography/crypto";
-import issueBackupKey from "@app/components/utilities/cryptography/issueBackupKey";
import { saveTokenToLocalStorage } from "@app/components/utilities/saveTokenToLocalStorage";
import SecurityClient from "@app/components/utilities/SecurityClient";
import { Button } from "@app/components/v2";
@@ -54,8 +53,6 @@ export const SignupInvitePage = () => {
const [lastNameError, setLastNameError] = useState(false);
const [isLoading, setIsLoading] = useState(false);
const [step, setStep] = useState(1);
- const [, setBackupKeyError] = useState(false);
- const [, setBackupKeyIssued] = useState(false);
const [errors, setErrors] = useState({});
const [shouldShowMfa, toggleShowMfa] = useToggle(false);
@@ -205,7 +202,9 @@ export const SignupInvitePage = () => {
localStorage.setItem("orgData.id", orgId);
- setStep(3);
+ navigate({
+ to: `/organization/${ProjectType.SecretManager}/overview` as const
+ });
};
await completeSignupFlow();
@@ -367,44 +366,6 @@ export const SignupInvitePage = () => {
);
- // Step 4 of the sign up process (download the emergency kit pdf)
- const step4 = (
-
-
- Save your Emergency Kit
-
-
-
- If you get locked out of your account, your Emergency Kit is the only way to sign in.
-
-
We recommend you download it and keep it somewhere safe.
-
-
-
- It contains your Secret Key which we cannot access or recover for you if you lose it.
-
-
-
-
-
- );
-
return (
@@ -425,7 +386,8 @@ export const SignupInvitePage = () => {
- {step === 1 ? stepConfirmEmail : step === 2 ? main : step4}
+ {step === 1 && stepConfirmEmail}
+ {step === 2 && main}
>
)}
diff --git a/frontend/src/pages/auth/SignUpPage/SignUpPage.tsx b/frontend/src/pages/auth/SignUpPage/SignUpPage.tsx
index c1fcc30c4..75af4ff48 100644
--- a/frontend/src/pages/auth/SignUpPage/SignUpPage.tsx
+++ b/frontend/src/pages/auth/SignUpPage/SignUpPage.tsx
@@ -5,7 +5,6 @@ import { useTranslation } from "react-i18next";
import { useNavigate } from "@tanstack/react-router";
import CodeInputStep from "@app/components/auth/CodeInputStep";
-import DownloadBackupPDF from "@app/components/auth/DonwloadBackupPDFStep";
import EnterEmailStep from "@app/components/auth/EnterEmailStep";
import InitialSignupStep from "@app/components/auth/InitialSignupStep";
import TeamInviteStep from "@app/components/auth/TeamInviteStep";
@@ -72,7 +71,7 @@ export const SignUpPage = () => {
incrementStep();
}
- if (!serverDetails?.emailConfigured && step === 5) {
+ if (!serverDetails?.emailConfigured && step === 4) {
navigate({
to: `/organization/${ProjectType.SecretManager}/overview` as const
});
@@ -119,17 +118,6 @@ export const SignUpPage = () => {
);
}
- if (registerStep === 4) {
- return (
-
- );
- }
-
if (serverDetails?.emailConfigured) {
return ;
}
diff --git a/frontend/src/pages/auth/SignUpSsoPage/SignUpSsoPage.tsx b/frontend/src/pages/auth/SignUpSsoPage/SignUpSsoPage.tsx
index a13cfcfc6..40075ae54 100644
--- a/frontend/src/pages/auth/SignUpSsoPage/SignUpSsoPage.tsx
+++ b/frontend/src/pages/auth/SignUpSsoPage/SignUpSsoPage.tsx
@@ -6,7 +6,6 @@ import { jwtDecode } from "jwt-decode";
import { ROUTE_PATHS } from "@app/const/routes";
-import { BackupPDFStep } from "./components/BackupPDFStep";
import { EmailConfirmationStep } from "./components/EmailConfirmationStep";
import { UserInfoSSOStep } from "./components/UserInfoSSOStep";
@@ -57,14 +56,9 @@ export const SignupSsoPage = () => {
providerOrganizationName={organizationName}
password={password}
setPassword={setPassword}
- setStep={setStep}
providerAuthToken={token}
/>
);
- case 2:
- return (
-
- );
default:
return ;
}
diff --git a/frontend/src/pages/auth/SignUpSsoPage/components/BackupPDFStep/BackupPDFStep.tsx b/frontend/src/pages/auth/SignUpSsoPage/components/BackupPDFStep/BackupPDFStep.tsx
deleted file mode 100644
index 9a2369170..000000000
--- a/frontend/src/pages/auth/SignUpSsoPage/components/BackupPDFStep/BackupPDFStep.tsx
+++ /dev/null
@@ -1,71 +0,0 @@
-import { useTranslation } from "react-i18next";
-import { faWarning } from "@fortawesome/free-solid-svg-icons";
-import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
-import { useNavigate } from "@tanstack/react-router";
-
-import issueBackupKey from "@app/components/utilities/cryptography/issueBackupKey";
-import { Button } from "@app/components/v2";
-import { ProjectType } from "@app/hooks/api/workspace/types";
-
-interface DownloadBackupPDFStepProps {
- 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 const BackupPDFStep = ({ email, password, name }: DownloadBackupPDFStepProps) => {
- const { t } = useTranslation();
- const navigate = useNavigate();
-
- return (
-
-
-
- {t("signup.step4-message")}
-
-
-
-
- {t("signup.step4-description1")} {t("signup.step4-description3")}
-
-
-
-
-
-
-
-
-
- );
-};
diff --git a/frontend/src/pages/auth/SignUpSsoPage/components/BackupPDFStep/index.tsx b/frontend/src/pages/auth/SignUpSsoPage/components/BackupPDFStep/index.tsx
deleted file mode 100644
index 01f7745cd..000000000
--- a/frontend/src/pages/auth/SignUpSsoPage/components/BackupPDFStep/index.tsx
+++ /dev/null
@@ -1 +0,0 @@
-export { BackupPDFStep } from "./BackupPDFStep";
diff --git a/frontend/src/pages/auth/SignUpSsoPage/components/UserInfoSSOStep/UserInfoSSOStep.tsx b/frontend/src/pages/auth/SignUpSsoPage/components/UserInfoSSOStep/UserInfoSSOStep.tsx
index c3d131708..a6ab0623b 100644
--- a/frontend/src/pages/auth/SignUpSsoPage/components/UserInfoSSOStep/UserInfoSSOStep.tsx
+++ b/frontend/src/pages/auth/SignUpSsoPage/components/UserInfoSSOStep/UserInfoSSOStep.tsx
@@ -2,6 +2,7 @@ import crypto from "crypto";
import { useEffect, useState } from "react";
import { useTranslation } from "react-i18next";
+import { useNavigate } from "@tanstack/react-router";
import jsrp from "jsrp";
import nacl from "tweetnacl";
import { encodeBase64 } from "tweetnacl-util";
@@ -17,12 +18,12 @@ import { useToggle } from "@app/hooks";
import { completeAccountSignup, useSelectOrganization } from "@app/hooks/api/auth/queries";
import { MfaMethod } from "@app/hooks/api/auth/types";
import { fetchOrganizations } from "@app/hooks/api/organization/queries";
+import { ProjectType } from "@app/hooks/api/workspace/types";
// eslint-disable-next-line new-cap
const client = new jsrp.client();
type Props = {
- setStep: (step: number) => void;
username: string;
password: string;
setPassword: (value: string) => void;
@@ -50,7 +51,6 @@ export const UserInfoSSOStep = ({
providerOrganizationName,
password,
setPassword,
- setStep,
providerAuthToken
}: Props) => {
const [nameError, setNameError] = useState(false);
@@ -63,6 +63,7 @@ export const UserInfoSSOStep = ({
const { t } = useTranslation();
const { mutateAsync: selectOrganization } = useSelectOrganization();
const [mfaSuccessCallback, setMfaSuccessCallback] = useState<() => void>(() => {});
+ const navigate = useNavigate();
useEffect(() => {
const randomPassword = crypto.randomBytes(32).toString("hex");
@@ -202,7 +203,9 @@ export const UserInfoSSOStep = ({
}
localStorage.setItem("orgData.id", orgId);
- setStep(2);
+ navigate({
+ to: `/organization/${ProjectType.SecretManager}/overview` as const
+ });
} catch (error) {
setIsLoading(false);
console.error(error);
diff --git a/frontend/src/pages/auth/VerifyEmailPage/VerifyEmailPage.tsx b/frontend/src/pages/auth/VerifyEmailPage/VerifyEmailPage.tsx
index 418529bdc..bc0bdf1ff 100644
--- a/frontend/src/pages/auth/VerifyEmailPage/VerifyEmailPage.tsx
+++ b/frontend/src/pages/auth/VerifyEmailPage/VerifyEmailPage.tsx
@@ -72,8 +72,9 @@ export const VerifyEmailPage = () => {
Forgot your password?
-
- You will need your emergency kit. Enter your email to start account recovery.
+
+ Enter your email to start the password reset process. You will receive an email with
+ instructions.
diff --git a/frontend/src/pages/user/PersonalSettingsPage/components/ChangePasswordSection/ChangePasswordSection.tsx b/frontend/src/pages/user/PersonalSettingsPage/components/ChangePasswordSection/ChangePasswordSection.tsx
index 9c34693df..8a2fd7010 100644
--- a/frontend/src/pages/user/PersonalSettingsPage/components/ChangePasswordSection/ChangePasswordSection.tsx
+++ b/frontend/src/pages/user/PersonalSettingsPage/components/ChangePasswordSection/ChangePasswordSection.tsx
@@ -11,7 +11,8 @@ import attemptChangePassword from "@app/components/utilities/attemptChangePasswo
import checkPassword from "@app/components/utilities/checks/password/checkPassword";
import { Button, FormControl, Input } from "@app/components/v2";
import { useUser } from "@app/context";
-import { useSendPasswordSetupEmail } from "@app/hooks/api/auth/queries";
+import { useResetUserPasswordV2, useSendPasswordSetupEmail } from "@app/hooks/api/auth/queries";
+import { UserEncryptionVersion } from "@app/hooks/api/auth/types";
type Errors = {
tooShort?: string;
@@ -47,6 +48,7 @@ export const ChangePasswordSection = () => {
const [errors, setErrors] = useState
({});
const [isLoading, setIsLoading] = useState(false);
const sendSetupPasswordEmail = useSendPasswordSetupEmail();
+ const { mutateAsync: resetPasswordV2 } = useResetUserPasswordV2();
const onFormSubmit = async ({ oldPassword, newPassword }: FormData) => {
try {
@@ -56,13 +58,20 @@ export const ChangePasswordSection = () => {
});
if (errorCheck) return;
-
setIsLoading(true);
- await attemptChangePassword({
- email: user.username,
- currentPassword: oldPassword,
- newPassword
- });
+
+ if (user.encryptionVersion === UserEncryptionVersion.V2) {
+ await resetPasswordV2({
+ oldPassword,
+ newPassword
+ });
+ } else {
+ await attemptChangePassword({
+ email: user.username,
+ currentPassword: oldPassword,
+ newPassword
+ });
+ }
setIsLoading(false);
createNotification({
diff --git a/frontend/src/pages/user/PersonalSettingsPage/components/PersonalGeneralTab/PersonalGeneralTab.tsx b/frontend/src/pages/user/PersonalSettingsPage/components/PersonalGeneralTab/PersonalGeneralTab.tsx
index ac4c5bea2..dcf35e672 100644
--- a/frontend/src/pages/user/PersonalSettingsPage/components/PersonalGeneralTab/PersonalGeneralTab.tsx
+++ b/frontend/src/pages/user/PersonalSettingsPage/components/PersonalGeneralTab/PersonalGeneralTab.tsx
@@ -1,14 +1,20 @@
+import { useUser } from "@app/context";
+import { UserEncryptionVersion } from "@app/hooks/api/auth/types";
+
import { DeleteAccountSection } from "../DeleteAccountSection";
import { EmergencyKitSection } from "../EmergencyKitSection";
import { SessionsSection } from "../SessionsSection";
import { UserNameSection } from "../UserNameSection";
export const PersonalGeneralTab = () => {
+ const { user } = useUser();
+ const encryptionVersion = user?.encryptionVersion ?? UserEncryptionVersion.V2;
+
return (
-
+ {encryptionVersion === UserEncryptionVersion.V1 && }
);
From 7003ad608a1fa1bf54d65aefc744e104d5a136c5 Mon Sep 17 00:00:00 2001
From: Daniel Hougaard
Date: Fri, 7 Mar 2025 04:37:08 +0400
Subject: [PATCH 03/71] Update user-service.ts
---
backend/src/services/user/user-service.ts | 3 ---
1 file changed, 3 deletions(-)
diff --git a/backend/src/services/user/user-service.ts b/backend/src/services/user/user-service.ts
index d272845c4..5da5d493c 100644
--- a/backend/src/services/user/user-service.ts
+++ b/backend/src/services/user/user-service.ts
@@ -3,9 +3,7 @@ import { ForbiddenError } from "@casl/ability";
import { SecretKeyEncoding } from "@app/db/schemas";
import { OrgPermissionActions, OrgPermissionSubjects } from "@app/ee/services/permission/org-permission";
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service";
-
import { infisicalSymmetricDecrypt } from "@app/lib/crypto/encryption";
-
import { BadRequestError, ForbiddenRequestError, NotFoundError } from "@app/lib/errors";
import { TAuthTokenServiceFactory } from "@app/services/auth-token/auth-token-service";
import { TokenType } from "@app/services/auth-token/auth-token-types";
@@ -33,7 +31,6 @@ type TUserServiceFactoryDep = {
| "createUserAction"
| "findUserEncKeyByUserId"
| "delete"
- | "updateUserEncryptionByUserId"
>;
userAliasDAL: Pick;
groupProjectDAL: Pick;
From c48c9ae628ab64c407cb29bb8716b8a664f0c2e6 Mon Sep 17 00:00:00 2001
From: Daniel Hougaard
Date: Fri, 7 Mar 2025 04:55:18 +0400
Subject: [PATCH 04/71] cleanup
---
.../services/auth/auth-password-service.ts | 38 +++++++++----------
.../ChangePasswordSection.tsx | 4 +-
2 files changed, 20 insertions(+), 22 deletions(-)
diff --git a/backend/src/services/auth/auth-password-service.ts b/backend/src/services/auth/auth-password-service.ts
index 349bcb3d0..4d01870f6 100644
--- a/backend/src/services/auth/auth-password-service.ts
+++ b/backend/src/services/auth/auth-password-service.ts
@@ -243,31 +243,27 @@ export const authPaswordServiceFactory = ({
const { tag, iv, ciphertext, encoding } = infisicalSymmetricEncypt(privateKey);
- await userDAL.transaction(async (tx) => {
- await userDAL.updateUserEncryptionByUserId(
- userId,
- {
- hashedPassword: newHashedPassword,
+ await userDAL.updateUserEncryptionByUserId(userId, {
+ hashedPassword: newHashedPassword,
- // srp params
- salt: encKeys.salt,
- verifier: encKeys.verifier,
+ // 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,
+ 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
- },
- tx
- );
+ serverEncryptedPrivateKey: ciphertext,
+ serverEncryptedPrivateKeyIV: iv,
+ serverEncryptedPrivateKeyTag: tag,
+ serverEncryptedPrivateKeyEncoding: encoding
});
+
+ await tokenService.revokeAllMySessions(userId);
};
/*
diff --git a/frontend/src/pages/user/PersonalSettingsPage/components/ChangePasswordSection/ChangePasswordSection.tsx b/frontend/src/pages/user/PersonalSettingsPage/components/ChangePasswordSection/ChangePasswordSection.tsx
index 8a2fd7010..d6842c755 100644
--- a/frontend/src/pages/user/PersonalSettingsPage/components/ChangePasswordSection/ChangePasswordSection.tsx
+++ b/frontend/src/pages/user/PersonalSettingsPage/components/ChangePasswordSection/ChangePasswordSection.tsx
@@ -13,6 +13,7 @@ import { Button, FormControl, Input } from "@app/components/v2";
import { useUser } from "@app/context";
import { useResetUserPasswordV2, useSendPasswordSetupEmail } from "@app/hooks/api/auth/queries";
import { UserEncryptionVersion } from "@app/hooks/api/auth/types";
+import { useNavigate } from "@tanstack/react-router";
type Errors = {
tooShort?: string;
@@ -36,6 +37,7 @@ export type FormData = z.infer;
export const ChangePasswordSection = () => {
const { t } = useTranslation();
+ const navigate = useNavigate();
const { user } = useUser();
const { reset, control, handleSubmit } = useForm({
@@ -80,7 +82,7 @@ export const ChangePasswordSection = () => {
});
reset();
- window.location.href = "/login";
+ navigate({ to: "/login" });
} catch (err) {
console.error(err);
setIsLoading(false);
From 214f837041057d33eef5f31a63b38e8ed3432d26 Mon Sep 17 00:00:00 2001
From: carlosmonastyrski
Date: Fri, 7 Mar 2025 11:42:15 -0300
Subject: [PATCH 05/71] Add is-admin filter to Server Admin Console and add a
component to show the server admins on side panel
---
backend/src/server/routes/v1/admin-router.ts | 7 +-
.../super-admin/super-admin-service.ts | 5 +-
.../services/super-admin/super-admin-types.ts | 1 +
backend/src/services/user/user-dal.ts | 9 +-
frontend/src/hooks/api/admin/types.ts | 1 +
.../MinimizedOrgSidebar.tsx | 27 ++++++
.../ServerAdminsPanel/ServerAdminsPanel.tsx | 85 +++++++++++++++++++
.../OverviewPage/components/UserPanel.tsx | 79 ++++++++++++++---
8 files changed, 197 insertions(+), 17 deletions(-)
create mode 100644 frontend/src/layouts/OrganizationLayout/components/ServerAdminsPanel/ServerAdminsPanel.tsx
diff --git a/backend/src/server/routes/v1/admin-router.ts b/backend/src/server/routes/v1/admin-router.ts
index 076b33e54..b63550d2c 100644
--- a/backend/src/server/routes/v1/admin-router.ts
+++ b/backend/src/server/routes/v1/admin-router.ts
@@ -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({
diff --git a/backend/src/services/super-admin/super-admin-service.ts b/backend/src/services/super-admin/super-admin-service.ts
index 166f73317..8efdfd7f9 100644
--- a/backend/src/services/super-admin/super-admin-service.ts
+++ b/backend/src/services/super-admin/super-admin-service.ts
@@ -271,12 +271,13 @@ 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
});
};
diff --git a/backend/src/services/super-admin/super-admin-types.ts b/backend/src/services/super-admin/super-admin-types.ts
index 2d10941b4..d6de67e59 100644
--- a/backend/src/services/super-admin/super-admin-types.ts
+++ b/backend/src/services/super-admin/super-admin-types.ts
@@ -20,6 +20,7 @@ export type TAdminGetUsersDTO = {
offset: number;
limit: number;
searchTerm: string;
+ adminsOnly: boolean;
};
export enum LoginMethod {
diff --git a/backend/src/services/user/user-dal.ts b/backend/src/services/user/user-dal.ts
index 99f403e84..eba497f0f 100644
--- a/backend/src/services/user/user-dal.ts
+++ b/backend/src/services/user/user-dal.ts
@@ -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);
}
diff --git a/frontend/src/hooks/api/admin/types.ts b/frontend/src/hooks/api/admin/types.ts
index e5d281fc4..80e3edc92 100644
--- a/frontend/src/hooks/api/admin/types.ts
+++ b/frontend/src/hooks/api/admin/types.ts
@@ -50,6 +50,7 @@ export type TUpdateAdminSlackConfigDTO = {
export type AdminGetUsersFilters = {
limit: number;
searchTerm: string;
+ adminsOnly: boolean;
};
export type AdminSlackConfig = {
diff --git a/frontend/src/layouts/OrganizationLayout/components/MinimizedOrgSidebar/MinimizedOrgSidebar.tsx b/frontend/src/layouts/OrganizationLayout/components/MinimizedOrgSidebar/MinimizedOrgSidebar.tsx
index 4a255acad..c1c4297ca 100644
--- a/frontend/src/layouts/OrganizationLayout/components/MinimizedOrgSidebar/MinimizedOrgSidebar.tsx
+++ b/frontend/src/layouts/OrganizationLayout/components/MinimizedOrgSidebar/MinimizedOrgSidebar.tsx
@@ -50,6 +50,7 @@ import { ProjectType } from "@app/hooks/api/workspace/types";
import { navigateUserToOrg } from "@app/pages/auth/LoginPage/Login.utils";
import { MenuIconButton } from "../MenuIconButton";
+import { ServerAdminsPanel } from "../ServerAdminsPanel/ServerAdminsPanel";
const getPlan = (subscription: SubscriptionPlan) => {
if (subscription.dynamicSecret) return "Enterprise Plan";
@@ -89,6 +90,7 @@ export const MinimizedOrgSidebar = () => {
const [openSupport, setOpenSupport] = useState(false);
const [openUser, setOpenUser] = useState(false);
const [openOrg, setOpenOrg] = useState(false);
+ const [openAdmins, setOpenAdmins] = useState(false);
const { user } = useUser();
const { mutateAsync } = useGetOrgTrialUrl();
@@ -392,6 +394,31 @@ export const MinimizedOrgSidebar = () => {
: "mb-4"
} flex w-full cursor-default flex-col items-center px-1 text-sm text-mineshaft-400`}
>
+
+ setOpenAdmins(true)}
+ onMouseLeave={() => setOpenAdmins(false)}
+ asChild
+ >
+
+
+ Admins
+
+
+
+ setOpenAdmins(true)}
+ onMouseLeave={() => setOpenAdmins(false)}
+ align="start"
+ side="right"
+ className="mb-2 w-[60vh] p-1"
+ >
+ Server Administrators
+
+
+
+
+
setOpenSupport(true)}
diff --git a/frontend/src/layouts/OrganizationLayout/components/ServerAdminsPanel/ServerAdminsPanel.tsx b/frontend/src/layouts/OrganizationLayout/components/ServerAdminsPanel/ServerAdminsPanel.tsx
new file mode 100644
index 000000000..69eecec40
--- /dev/null
+++ b/frontend/src/layouts/OrganizationLayout/components/ServerAdminsPanel/ServerAdminsPanel.tsx
@@ -0,0 +1,85 @@
+import { useState } from "react";
+import { faMagnifyingGlass } from "@fortawesome/free-solid-svg-icons";
+import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
+
+import {
+ Input,
+ Table,
+ TableContainer,
+ TableSkeleton,
+ TBody,
+ Td,
+ Th,
+ THead,
+ Tr
+} from "@app/components/v2";
+import { useOrganization } from "@app/context";
+import { useDebounce } from "@app/hooks";
+import { useGetOrgUsers } from "@app/hooks/api";
+
+export const ServerAdminsPanel = () => {
+ const [searchUserFilter, setSearchUserFilter] = useState("");
+ const [debounedSearchTerm] = useDebounce(searchUserFilter, 500);
+ const { currentOrg } = useOrganization();
+
+ const { data: orgUsers, isPending } = useGetOrgUsers(currentOrg?.id || "");
+
+ const adminUsers = orgUsers?.filter((orgUser) => {
+ const isSuperAdmin = orgUser.user.superAdmin;
+ const matchesSearch = debounedSearchTerm
+ ? orgUser.user.email?.toLowerCase().includes(debounedSearchTerm.toLowerCase()) ||
+ orgUser.user.firstName?.toLowerCase().includes(debounedSearchTerm.toLowerCase()) ||
+ orgUser.user.lastName?.toLowerCase().includes(debounedSearchTerm.toLowerCase())
+ : true;
+ return isSuperAdmin && matchesSearch;
+ });
+
+ const isEmpty = !isPending && (!adminUsers || adminUsers.length === 0);
+
+ return (
+
+
+ setSearchUserFilter(e.target.value)}
+ leftIcon={}
+ placeholder="Search server admins..."
+ className="w-full"
+ />
+
+
+
+
+
+
+ | Name |
+ Email |
+
+
+
+ {isPending && }
+ {!isPending &&
+ adminUsers?.map(({ user }) => {
+ const name =
+ user.firstName || user.lastName
+ ? `${user.firstName} ${user.lastName}`
+ : user.username;
+ return (
+
+ | {name} |
+ {user.email} |
+
+ );
+ })}
+
+
+ {isEmpty && (
+
+ No server administrators found
+
+ )}
+
+
+
+ );
+};
diff --git a/frontend/src/pages/admin/OverviewPage/components/UserPanel.tsx b/frontend/src/pages/admin/OverviewPage/components/UserPanel.tsx
index df51216a6..9f6524cf7 100644
--- a/frontend/src/pages/admin/OverviewPage/components/UserPanel.tsx
+++ b/frontend/src/pages/admin/OverviewPage/components/UserPanel.tsx
@@ -1,6 +1,13 @@
import { useState } from "react";
-import { faMagnifyingGlass, faUsers, faEllipsis } from "@fortawesome/free-solid-svg-icons";
+import {
+ faCheckCircle,
+ faEllipsis,
+ faFilter,
+ faMagnifyingGlass,
+ faUsers
+} from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
+import { twMerge } from "tailwind-merge";
import { UpgradePlanModal } from "@app/components/license/UpgradePlanModal";
import { createNotification } from "@app/components/notifications";
@@ -8,8 +15,15 @@ import {
Badge,
Button,
DeleteActionModal,
+ DropdownMenu,
+ DropdownMenuContent,
+ DropdownMenuItem,
+ DropdownMenuLabel,
+ DropdownMenuTrigger,
EmptyState,
+ IconButton,
Input,
+ Switch,
Table,
TableContainer,
TableSkeleton,
@@ -17,11 +31,7 @@ import {
Td,
Th,
THead,
- Tr,
- DropdownMenu,
- DropdownMenuContent,
- DropdownMenuItem,
- DropdownMenuTrigger
+ Tr
} from "@app/components/v2";
import { useSubscription, useUser } from "@app/context";
import { useDebounce, usePopUp } from "@app/hooks";
@@ -48,6 +58,7 @@ const UserPanelTable = ({
) => void;
}) => {
const [searchUserFilter, setSearchUserFilter] = useState("");
+ const [adminsOnly, setAdminsOnly] = useState(false);
const { user } = useUser();
const userId = user?.id || "";
const [debounedSearchTerm] = useDebounce(searchUserFilter, 500);
@@ -55,18 +66,60 @@ const UserPanelTable = ({
const { data, isPending, isFetchingNextPage, hasNextPage, fetchNextPage } = useAdminGetUsers({
limit: 20,
- searchTerm: debounedSearchTerm
+ searchTerm: debounedSearchTerm,
+ adminsOnly
});
const isEmpty = !isPending && !data?.pages?.[0].length;
+ const isTableFiltered = Boolean(adminsOnly);
+
return (
<>
- setSearchUserFilter(e.target.value)}
- leftIcon={}
- placeholder="Search users..."
- />
+
+
setSearchUserFilter(e.target.value)}
+ leftIcon={
}
+ placeholder="Search users..."
+ className="flex-1"
+ />
+
+
+
+
+
+
+
+ Filter Users
+ {
+ e.preventDefault();
+ setAdminsOnly(!adminsOnly);
+ }}
+ icon={adminsOnly && }
+ iconPos="right"
+ >
+
+ Admins Only
+ setAdminsOnly(checked)}
+ className="data-[state=checked]:bg-primary-400"
+ />
+
+
+
+
+
From 67f2e4671a61a759af0a4b77b6cf8423b5b49822 Mon Sep 17 00:00:00 2001
From: Daniel Hougaard
Date: Fri, 7 Mar 2025 19:59:29 +0400
Subject: [PATCH 06/71] requested changes
---
.../services/auth/auth-password-service.ts | 48 +++++++++----------
.../auth/VerifyEmailPage/VerifyEmailPage.tsx | 6 ++-
2 files changed, 26 insertions(+), 28 deletions(-)
diff --git a/backend/src/services/auth/auth-password-service.ts b/backend/src/services/auth/auth-password-service.ts
index 4d01870f6..27d3bdf2d 100644
--- a/backend/src/services/auth/auth-password-service.ts
+++ b/backend/src/services/auth/auth-password-service.ts
@@ -119,35 +119,31 @@ export const authPaswordServiceFactory = ({
* Email password reset flow via email. Step 1 send email
*/
const sendPasswordResetEmail = async (email: string) => {
- const startTime = Date.now();
+ const sendEmail = async () => {
+ const user = await userDAL.findUserByUsername(email);
- const user = await userDAL.findUserByUsername(email);
+ if (user && user.isAccepted) {
+ 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` : ""
- }
- });
- }
-
- const elapsedTime = Date.now() - startTime;
- // daniel: ensure each request takes 8 seconds to prevent timing attacks
- if (elapsedTime < 8_000) {
- // eslint-disable-next-line no-promise-executor-return
- await new Promise((resolve) => setTimeout(resolve, 8_000 - elapsedTime));
- }
+ // note(daniel): run in background to prevent timing attacks
+ void sendEmail();
};
/*
diff --git a/frontend/src/pages/auth/VerifyEmailPage/VerifyEmailPage.tsx b/frontend/src/pages/auth/VerifyEmailPage/VerifyEmailPage.tsx
index bc0bdf1ff..47012e2cb 100644
--- a/frontend/src/pages/auth/VerifyEmailPage/VerifyEmailPage.tsx
+++ b/frontend/src/pages/auth/VerifyEmailPage/VerifyEmailPage.tsx
@@ -103,8 +103,10 @@ export const VerifyEmailPage = () => {
Look for an email in your inbox.
-
- An email with instructions has been sent to {email}.
+
+ If the email is in our system, you will receive an email at{" "}
+ {email} with instructions on how to reset your
+ password.
From 795d9e44139348e11cca7ce171752d66f00c784c Mon Sep 17 00:00:00 2001
From: Daniel Hougaard
Date: Fri, 7 Mar 2025 20:15:30 +0400
Subject: [PATCH 07/71] Update auth-password-service.ts
---
backend/src/services/auth/auth-password-service.ts | 3 ++-
1 file changed, 2 insertions(+), 1 deletion(-)
diff --git a/backend/src/services/auth/auth-password-service.ts b/backend/src/services/auth/auth-password-service.ts
index 27d3bdf2d..f5f42a236 100644
--- a/backend/src/services/auth/auth-password-service.ts
+++ b/backend/src/services/auth/auth-password-service.ts
@@ -25,6 +25,7 @@ import {
TSetupPasswordViaBackupKeyDTO
} from "./auth-password-type";
import { ActorType, AuthMethod, AuthTokenType } from "./auth-type";
+import { logger } from "@app/lib/logger";
type TAuthPasswordServiceFactoryDep = {
authDAL: TAuthDALFactory;
@@ -143,7 +144,7 @@ export const authPaswordServiceFactory = ({
};
// note(daniel): run in background to prevent timing attacks
- void sendEmail();
+ void sendEmail().catch((err) => logger.error(err, "Failed to send password reset email"));
};
/*
From 1dea0248809e158e051c8cdb0a0c223bba0641ae Mon Sep 17 00:00:00 2001
From: carlosmonastyrski
Date: Fri, 7 Mar 2025 19:19:55 -0300
Subject: [PATCH 08/71] Improvement on admin visibility UI components
---
.../MinimizedOrgSidebar.tsx | 80 ++++++++++---------
.../ServerAdminsPanel/ServerAdminsPanel.tsx | 18 ++---
.../OverviewPage/components/UserPanel.tsx | 21 ++---
3 files changed, 60 insertions(+), 59 deletions(-)
diff --git a/frontend/src/layouts/OrganizationLayout/components/MinimizedOrgSidebar/MinimizedOrgSidebar.tsx b/frontend/src/layouts/OrganizationLayout/components/MinimizedOrgSidebar/MinimizedOrgSidebar.tsx
index c1c4297ca..95e254ccb 100644
--- a/frontend/src/layouts/OrganizationLayout/components/MinimizedOrgSidebar/MinimizedOrgSidebar.tsx
+++ b/frontend/src/layouts/OrganizationLayout/components/MinimizedOrgSidebar/MinimizedOrgSidebar.tsx
@@ -30,6 +30,8 @@ import {
DropdownMenuItem,
DropdownMenuLabel,
DropdownMenuTrigger,
+ Modal,
+ ModalContent,
Tooltip
} from "@app/components/v2";
import { envConfig } from "@app/config/env";
@@ -78,6 +80,11 @@ export const INFISICAL_SUPPORT_OPTIONS = [
,
"Email Support",
"mailto:support@infisical.com"
+ ],
+ [
+ ,
+ "Instance Admins",
+ "server-admins"
]
];
@@ -90,7 +97,7 @@ export const MinimizedOrgSidebar = () => {
const [openSupport, setOpenSupport] = useState(false);
const [openUser, setOpenUser] = useState(false);
const [openOrg, setOpenOrg] = useState(false);
- const [openAdmins, setOpenAdmins] = useState(false);
+ const [showAdminsModal, setShowAdminsModal] = useState(false);
const { user } = useUser();
const { mutateAsync } = useGetOrgTrialUrl();
@@ -394,31 +401,6 @@ export const MinimizedOrgSidebar = () => {
: "mb-4"
} flex w-full cursor-default flex-col items-center px-1 text-sm text-mineshaft-400`}
>
-
- setOpenAdmins(true)}
- onMouseLeave={() => setOpenAdmins(false)}
- asChild
- >
-
-
- Admins
-
-
-
- setOpenAdmins(true)}
- onMouseLeave={() => setOpenAdmins(false)}
- align="start"
- side="right"
- className="mb-2 w-[60vh] p-1"
- >
- Server Administrators
-
-
-
-
-
setOpenSupport(true)}
@@ -439,17 +421,30 @@ export const MinimizedOrgSidebar = () => {
>
{INFISICAL_SUPPORT_OPTIONS.map(([icon, text, url]) => (
-
-
-
+ {url === "server-admins" ? (
+
+ ) : (
+
+
+
+ )}
))}
{envConfig.PLATFORM_VERSION && (
@@ -567,6 +562,17 @@ export const MinimizedOrgSidebar = () => {
+
+
+
+
+
+
+
handlePopUpToggle("createOrg", false)}
diff --git a/frontend/src/layouts/OrganizationLayout/components/ServerAdminsPanel/ServerAdminsPanel.tsx b/frontend/src/layouts/OrganizationLayout/components/ServerAdminsPanel/ServerAdminsPanel.tsx
index 69eecec40..fed2a5a8b 100644
--- a/frontend/src/layouts/OrganizationLayout/components/ServerAdminsPanel/ServerAdminsPanel.tsx
+++ b/frontend/src/layouts/OrganizationLayout/components/ServerAdminsPanel/ServerAdminsPanel.tsx
@@ -37,7 +37,7 @@ export const ServerAdminsPanel = () => {
const isEmpty = !isPending && (!adminUsers || adminUsers.length === 0);
return (
-
+
{
className="w-full"
/>
-
-
-
-
+
+
+
+
| Name |
Email |
-
+
{isPending && }
{!isPending &&
adminUsers?.map(({ user }) => {
@@ -65,9 +65,9 @@ export const ServerAdminsPanel = () => {
? `${user.firstName} ${user.lastName}`
: user.username;
return (
-
- | {name} |
- {user.email} |
+
+ | {name} |
+ {user.email} |
);
})}
diff --git a/frontend/src/pages/admin/OverviewPage/components/UserPanel.tsx b/frontend/src/pages/admin/OverviewPage/components/UserPanel.tsx
index 9f6524cf7..6654439de 100644
--- a/frontend/src/pages/admin/OverviewPage/components/UserPanel.tsx
+++ b/frontend/src/pages/admin/OverviewPage/components/UserPanel.tsx
@@ -4,7 +4,8 @@ import {
faEllipsis,
faFilter,
faMagnifyingGlass,
- faUsers
+ faUsers,
+ faUserShield
} from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { twMerge } from "tailwind-merge";
@@ -23,7 +24,6 @@ import {
EmptyState,
IconButton,
Input,
- Switch,
Table,
TableContainer,
TableSkeleton,
@@ -97,24 +97,19 @@ const UserPanelTable = ({
-
- Filter Users
+
+ Filter By
{
e.preventDefault();
setAdminsOnly(!adminsOnly);
}}
- icon={adminsOnly && }
+ icon={adminsOnly && }
iconPos="right"
>
-
-
Admins Only
-
setAdminsOnly(checked)}
- className="data-[state=checked]:bg-primary-400"
- />
+
+
+ Server Admins
From 2bd681d58f99a7c0e73ae1b8a9a281af01081a20 Mon Sep 17 00:00:00 2001
From: Maidul Islam
Date: Sun, 9 Mar 2025 16:07:33 -0400
Subject: [PATCH 09/71] add system md service for gateway
---
cli/packages/cmd/gateway.go | 93 +++++++++++++++++++++++++++++++++----
1 file changed, 84 insertions(+), 9 deletions(-)
diff --git a/cli/packages/cmd/gateway.go b/cli/packages/cmd/gateway.go
index d796ffede..fbe2f4d5d 100644
--- a/cli/packages/cmd/gateway.go
+++ b/cli/packages/cmd/gateway.go
@@ -1,29 +1,94 @@
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"
+ "path/filepath"
+ "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/spf13/cobra"
)
+const systemdServiceTemplate = `[Unit]
+Description=Infisical Gateway Service
+After=network.target
+
+[Service]
+Type=simple
+EnvironmentFile=/etc/infisical/gateway.conf
+ExecStart=/usr/local/bin/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 installSystemdService(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
+}
+
var gatewayCmd = &cobra.Command{
Example: `infisical gateway`,
Short: "Used to infisical gateway",
@@ -40,6 +105,16 @@ var gatewayCmd = &cobra.Command{
util.HandleError(fmt.Errorf("Token not found"))
}
+ domain, err := cmd.Flags().GetString("domain")
+ if err != nil {
+ util.HandleError(err, "Unable to parse domain flag")
+ }
+
+ // Try to install systemd service if possible
+ if err := installSystemdService(token.Token, domain); err != nil {
+ log.Warn().Msgf("Failed to install systemd service: %v", err)
+ }
+
Telemetry.CaptureEvent("cli-command:gateway", posthog.NewProperties().Set("version", util.CLI_VERSION))
sigCh := make(chan os.Signal, 1)
From 004a8b71a20d5d599bda2302d5d0bf9eb0f127e2 Mon Sep 17 00:00:00 2001
From: =
Date: Mon, 10 Mar 2025 16:03:51 +0530
Subject: [PATCH 10/71] feat: refactored the systemd service to seperate
package file
---
cli/packages/cmd/gateway.go | 78 +------------------------------
cli/packages/gateway/systemd.go | 82 +++++++++++++++++++++++++++++++++
2 files changed, 84 insertions(+), 76 deletions(-)
create mode 100644 cli/packages/gateway/systemd.go
diff --git a/cli/packages/cmd/gateway.go b/cli/packages/cmd/gateway.go
index fbe2f4d5d..d41c2c20e 100644
--- a/cli/packages/cmd/gateway.go
+++ b/cli/packages/cmd/gateway.go
@@ -4,91 +4,17 @@ import (
"context"
"fmt"
"os"
- "os/exec"
"os/signal"
- "path/filepath"
- "runtime"
"syscall"
"time"
"github.com/Infisical/infisical-merge/packages/gateway"
"github.com/Infisical/infisical-merge/packages/util"
- "github.com/rs/zerolog/log"
"github.com/posthog/posthog-go"
+ "github.com/rs/zerolog/log"
"github.com/spf13/cobra"
)
-const systemdServiceTemplate = `[Unit]
-Description=Infisical Gateway Service
-After=network.target
-
-[Service]
-Type=simple
-EnvironmentFile=/etc/infisical/gateway.conf
-ExecStart=/usr/local/bin/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 installSystemdService(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
-}
-
var gatewayCmd = &cobra.Command{
Example: `infisical gateway`,
Short: "Used to infisical gateway",
@@ -111,7 +37,7 @@ var gatewayCmd = &cobra.Command{
}
// Try to install systemd service if possible
- if err := installSystemdService(token.Token, domain); err != nil {
+ if err := gateway.InstallGatewaySystemdService(token.Token, domain); err != nil {
log.Warn().Msgf("Failed to install systemd service: %v", err)
}
diff --git a/cli/packages/gateway/systemd.go b/cli/packages/gateway/systemd.go
new file mode 100644
index 000000000..8183f3535
--- /dev/null
+++ b/cli/packages/gateway/systemd.go
@@ -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=/usr/local/bin/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
+}
From 66817a40db59f55a2c33e4782e984b380c712e69 Mon Sep 17 00:00:00 2001
From: carlosmonastyrski
Date: Mon, 10 Mar 2025 08:31:19 -0300
Subject: [PATCH 11/71] Adjust modal width to match the rest of the modals
---
.../MinimizedOrgSidebar/MinimizedOrgSidebar.tsx | 8 ++------
.../components/ServerAdminsPanel/ServerAdminsPanel.tsx | 2 +-
2 files changed, 3 insertions(+), 7 deletions(-)
diff --git a/frontend/src/layouts/OrganizationLayout/components/MinimizedOrgSidebar/MinimizedOrgSidebar.tsx b/frontend/src/layouts/OrganizationLayout/components/MinimizedOrgSidebar/MinimizedOrgSidebar.tsx
index 95e254ccb..453a080ad 100644
--- a/frontend/src/layouts/OrganizationLayout/components/MinimizedOrgSidebar/MinimizedOrgSidebar.tsx
+++ b/frontend/src/layouts/OrganizationLayout/components/MinimizedOrgSidebar/MinimizedOrgSidebar.tsx
@@ -563,12 +563,8 @@ export const MinimizedOrgSidebar = () => {
-
-
+
+
diff --git a/frontend/src/layouts/OrganizationLayout/components/ServerAdminsPanel/ServerAdminsPanel.tsx b/frontend/src/layouts/OrganizationLayout/components/ServerAdminsPanel/ServerAdminsPanel.tsx
index fed2a5a8b..c834daad8 100644
--- a/frontend/src/layouts/OrganizationLayout/components/ServerAdminsPanel/ServerAdminsPanel.tsx
+++ b/frontend/src/layouts/OrganizationLayout/components/ServerAdminsPanel/ServerAdminsPanel.tsx
@@ -48,7 +48,7 @@ export const ServerAdminsPanel = () => {
/>
-
+
From a814f459aba5776279a5655f249c5474f042bc2e Mon Sep 17 00:00:00 2001
From: carlosmonastyrski
Date: Mon, 10 Mar 2025 10:58:39 -0300
Subject: [PATCH 12/71] Add condition to hide Instance Admins on cloud
instances
---
.../MinimizedOrgSidebar.tsx | 62 ++++++++++---------
1 file changed, 34 insertions(+), 28 deletions(-)
diff --git a/frontend/src/layouts/OrganizationLayout/components/MinimizedOrgSidebar/MinimizedOrgSidebar.tsx b/frontend/src/layouts/OrganizationLayout/components/MinimizedOrgSidebar/MinimizedOrgSidebar.tsx
index 453a080ad..3cc104f48 100644
--- a/frontend/src/layouts/OrganizationLayout/components/MinimizedOrgSidebar/MinimizedOrgSidebar.tsx
+++ b/frontend/src/layouts/OrganizationLayout/components/MinimizedOrgSidebar/MinimizedOrgSidebar.tsx
@@ -36,6 +36,7 @@ import {
} from "@app/components/v2";
import { envConfig } from "@app/config/env";
import { useOrganization, useSubscription, useUser } from "@app/context";
+import { isInfisicalCloud } from "@app/helpers/platform";
import { usePopUp, useToggle } from "@app/hooks";
import {
useGetOrganizations,
@@ -419,34 +420,39 @@ export const MinimizedOrgSidebar = () => {
side="right"
className="p-1"
>
- {INFISICAL_SUPPORT_OPTIONS.map(([icon, text, url]) => (
-
- {url === "server-admins" ? (
-
- ) : (
-
-
-
- )}
-
- ))}
+ {INFISICAL_SUPPORT_OPTIONS.map(([icon, text, url]) => {
+ if (url === "server-admins" && isInfisicalCloud()) {
+ return null;
+ }
+ return (
+
+ {url === "server-admins" ? (
+
+ ) : (
+
+
+
+ )}
+
+ );
+ })}
{envConfig.PLATFORM_VERSION && (
From 0057404562695da90e8e9d3eaa2cf96ce44e8b50 Mon Sep 17 00:00:00 2001
From: =
Date: Mon, 10 Mar 2025 22:22:58 +0530
Subject: [PATCH 13/71] feat: removed ca pool from dialing
---
cli/packages/gateway/gateway.go | 4 ----
1 file changed, 4 deletions(-)
diff --git a/cli/packages/gateway/gateway.go b/cli/packages/gateway/gateway.go
index bcf977d6d..772da8c40 100644
--- a/cli/packages/gateway/gateway.go
+++ b/cli/packages/gateway/gateway.go
@@ -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)
From 9458c8b04f8a5a90f0a844f7d81e74c410ea87c0 Mon Sep 17 00:00:00 2001
From: Daniel Hougaard
Date: Mon, 10 Mar 2025 22:15:30 +0400
Subject: [PATCH 14/71] Update auth-fns.ts
---
backend/src/services/auth/auth-fns.ts | 6 ++++++
1 file changed, 6 insertions(+)
diff --git a/backend/src/services/auth/auth-fns.ts b/backend/src/services/auth/auth-fns.ts
index c72f6c6a7..ec6e0a303 100644
--- a/backend/src/services/auth/auth-fns.ts
+++ b/backend/src/services/auth/auth-fns.ts
@@ -66,6 +66,12 @@ export const validatePasswordResetAuthorization = (token?: string) => {
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;
};
From 29e39b558bbbacfb2ecb13ab7ffc9576aaa6e67f Mon Sep 17 00:00:00 2001
From: =
Date: Mon, 10 Mar 2025 23:59:17 +0530
Subject: [PATCH 15/71] feat: changed to permission check
---
cli/packages/gateway/gateway.go | 15 +++------------
1 file changed, 3 insertions(+), 12 deletions(-)
diff --git a/cli/packages/gateway/gateway.go b/cli/packages/gateway/gateway.go
index 772da8c40..7a5403a12 100644
--- a/cli/packages/gateway/gateway.go
+++ b/cli/packages/gateway/gateway.go
@@ -189,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")
@@ -326,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
@@ -341,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")
@@ -357,9 +351,6 @@ func (g *Gateway) registerRelayIsActive(ctx context.Context, addr string, errCh
}
continue
}
- if conn != nil {
- defer conn.CloseWithError(0, "All good")
- }
}
}
}()
From 97567d06d4b0ae0e0e2be036f49c3a8510beb609 Mon Sep 17 00:00:00 2001
From: Daniel Hougaard <62331820+DanielHougaard@users.noreply.github.com>
Date: Wed, 5 Mar 2025 23:47:40 +0400
Subject: [PATCH 16/71] Revert "Revert "feat(api/secrets): view secret value
permission""
---
.../e2e-test/routes/v2/service-token.spec.ts | 10 +-
backend/e2e-test/vitest-environment-knex.ts | 1 -
...kfill-secret-permissions-with-readvalue.ts | 313 +++++++++
.../v1/secret-approval-request-router.ts | 21 +-
backend/src/ee/routes/v1/secret-router.ts | 4 +-
.../src/ee/routes/v1/secret-version-router.ts | 7 +-
backend/src/ee/routes/v1/snapshot-router.ts | 12 +-
.../services/permission/project-permission.ts | 60 +-
.../secret-approval-request-service.ts | 20 +-
.../secret-replication-service.ts | 1 +
.../secret-rotation-service.ts | 8 +-
.../secret-snapshot-service.ts | 144 ++--
backend/src/lib/api-docs/constants.ts | 3 +
backend/src/lib/errors/index.ts | 1 +
backend/src/server/routes/sanitizedSchemas.ts | 9 +
.../src/server/routes/v1/dashboard-router.ts | 90 +--
.../server/routes/v2/service-token-router.ts | 2 +-
backend/src/server/routes/v3/secret-router.ts | 148 ++--
.../external-migration-fns.ts | 4 +-
.../external-migration-queue.ts | 4 +-
.../integration-delete-secret.ts | 3 +-
.../integration/integration-service.ts | 10 +-
.../src/services/project/project-service.ts | 11 +-
.../secret-import/secret-import-fns.ts | 29 +-
.../secret-import/secret-import-service.ts | 22 +-
.../services/secret-sync/secret-sync-queue.ts | 3 +-
.../secret-sync/secret-sync-service.ts | 6 +-
.../src/services/secret-tag/secret-tag-dal.ts | 1 +
.../secret-v2-bridge/secret-v2-bridge-fns.ts | 44 +-
.../secret-v2-bridge-service.ts | 654 +++++++++++++-----
.../secret-v2-bridge-types.ts | 18 +-
.../secret-v2-bridge/secret-version-dal.ts | 59 +-
backend/src/services/secret/secret-dal.ts | 45 +-
backend/src/services/secret/secret-fns.ts | 42 +-
backend/src/services/secret/secret-queue.ts | 3 +-
backend/src/services/secret/secret-service.ts | 276 ++++++--
backend/src/services/secret/secret-types.ts | 7 +-
.../src/services/secret/secret-version-dal.ts | 49 +-
.../service-token/service-token-service.ts | 8 +-
.../service-token/service-token-types.ts | 2 +-
.../SecretReferenceDetails.tsx | 45 +-
frontend/src/components/v2/Blur/Blur.tsx | 22 +
frontend/src/components/v2/Blur/index.tsx | 1 +
.../InfisicalSecretInput.tsx | 1 +
.../context/ProjectPermissionContext/types.ts | 10 +-
frontend/src/hooks/api/dashboard/queries.tsx | 3 +
frontend/src/hooks/api/dashboard/types.ts | 1 +
.../api/secretApprovalRequest/queries.tsx | 1 +
.../src/hooks/api/secretImports/queries.tsx | 2 +
.../src/hooks/api/secretSnapshots/queries.tsx | 1 +
frontend/src/hooks/api/secrets/queries.tsx | 24 +-
frontend/src/hooks/api/secrets/types.ts | 5 +
frontend/src/hooks/api/types.ts | 9 +-
.../AddServiceTokenModal.tsx | 8 +-
.../components/GeneralPermissionPolicies.tsx | 27 +-
.../ProjectRoleModifySection.utils.tsx | 36 +-
.../CreateSecretForm/CreateSecretForm.tsx | 3 +-
.../SecretOverviewTableRow/SecretEditRow.tsx | 43 +-
.../SecretNoAccessOverviewTableRow.tsx | 3 +-
.../SecretOverviewTableRow.tsx | 9 +-
.../SecretRenameRow.tsx | 12 +-
.../components/QuickSearchSecretItem.tsx | 62 +-
.../SelectionPanel/SelectionPanel.tsx | 5 +-
.../MoveSecretsDialog/MoveSecretsDialog.tsx | 5 +-
.../SecretDashboardPage.tsx | 13 +-
.../components/ActionBar/ActionBar.tsx | 103 +--
.../SecretListView/SecretDetailSidebar.tsx | 174 +++--
.../components/SecretListView/SecretItem.tsx | 24 +-
.../SecretListView/SecretNoAccessListView.tsx | 9 +-
.../components/SnapshotView/SecretItem.tsx | 19 +-
70 files changed, 2105 insertions(+), 729 deletions(-)
create mode 100644 backend/src/db/migrations/20250218020306_backfill-secret-permissions-with-readvalue.ts
create mode 100644 frontend/src/components/v2/Blur/Blur.tsx
create mode 100644 frontend/src/components/v2/Blur/index.tsx
diff --git a/backend/e2e-test/routes/v2/service-token.spec.ts b/backend/e2e-test/routes/v2/service-token.spec.ts
index 6cc8f6e34..bfe043523 100644
--- a/backend/e2e-test/routes/v2/service-token.spec.ts
+++ b/backend/e2e-test/routes/v2/service-token.spec.ts
@@ -6,7 +6,7 @@ import { decryptAsymmetric, decryptSymmetric128BitHexKeyUTF8, encryptSymmetric12
const createServiceToken = async (
scopes: { environment: string; secretPath: string }[],
- permissions: ("read" | "write")[]
+ permissions: ("read" | "write" | "readValue")[]
) => {
const projectKeyRes = await testServer.inject({
method: "GET",
@@ -139,7 +139,7 @@ describe("Service token secret ops", async () => {
beforeAll(async () => {
serviceToken = await createServiceToken(
[{ secretPath: "/**", environment: seedData1.environment.slug }],
- ["read", "write"]
+ ["read", "write", "readValue"]
);
// this is ensure cli service token decryptiong working fine
@@ -496,7 +496,7 @@ describe("Service token fail cases", async () => {
test("Unauthorized secret path access", async () => {
const serviceToken = await createServiceToken(
[{ secretPath: "/", environment: seedData1.environment.slug }],
- ["read", "write"]
+ ["read", "readValue", "write"]
);
const fetchSecrets = await testServer.inject({
method: "GET",
@@ -518,7 +518,7 @@ describe("Service token fail cases", async () => {
test("Unauthorized secret environment access", async () => {
const serviceToken = await createServiceToken(
[{ secretPath: "/", environment: seedData1.environment.slug }],
- ["read", "write"]
+ ["read", "readValue", "write"]
);
const fetchSecrets = await testServer.inject({
method: "GET",
@@ -540,7 +540,7 @@ describe("Service token fail cases", async () => {
test("Unauthorized write operation", async () => {
const serviceToken = await createServiceToken(
[{ secretPath: "/", environment: seedData1.environment.slug }],
- ["read"]
+ ["read", "readValue"]
);
const writeSecrets = await testServer.inject({
method: "POST",
diff --git a/backend/e2e-test/vitest-environment-knex.ts b/backend/e2e-test/vitest-environment-knex.ts
index 9dfb38aeb..46b322349 100644
--- a/backend/e2e-test/vitest-environment-knex.ts
+++ b/backend/e2e-test/vitest-environment-knex.ts
@@ -120,4 +120,3 @@ export default {
};
}
};
-
diff --git a/backend/src/db/migrations/20250218020306_backfill-secret-permissions-with-readvalue.ts b/backend/src/db/migrations/20250218020306_backfill-secret-permissions-with-readvalue.ts
new file mode 100644
index 000000000..9e567cefe
--- /dev/null
+++ b/backend/src/db/migrations/20250218020306_backfill-secret-permissions-with-readvalue.ts
@@ -0,0 +1,313 @@
+import { MongoAbility, RawRuleOf } from "@casl/ability";
+import { PackRule, packRules, unpackRules } from "@casl/ability/extra";
+import { Knex } from "knex";
+import { z } from "zod";
+
+import { selectAllTableCols } from "@app/lib/knex";
+
+import { TableName } from "../schemas";
+
+enum ProjectPermissionSub {
+ Secrets = "secrets"
+}
+
+enum SecretActions {
+ Read = "read",
+ ReadValue = "readValue"
+}
+
+const UnpackedPermissionSchema = z.object({
+ subject: z
+ .union([z.string().min(1), z.string().array()])
+ .transform((el) => (typeof el !== "string" ? el[0] : el))
+ .optional(),
+ action: z.union([z.string().min(1), z.string().array()]).transform((el) => (typeof el === "string" ? [el] : el)),
+ conditions: z.unknown().optional(),
+ inverted: z.boolean().optional()
+});
+
+const $unpackPermissions = (permissions: unknown) =>
+ UnpackedPermissionSchema.array().parse(unpackRules((permissions || []) as PackRule>[]));
+
+const $updatePermissionsUp = (permissions: unknown) => {
+ const parsedPermissions = $unpackPermissions(permissions);
+ let shouldUpdate = false;
+
+ for (let i = 0; i < parsedPermissions.length; i += 1) {
+ const parsedPermission = parsedPermissions[i];
+ const { subject, action } = parsedPermission;
+
+ if (subject === ProjectPermissionSub.Secrets) {
+ if (action.includes(SecretActions.Read) && !action.includes(SecretActions.ReadValue)) {
+ action.push(SecretActions.ReadValue);
+ parsedPermissions[i] = { ...parsedPermission, action };
+ shouldUpdate = true;
+ }
+ }
+ }
+
+ return {
+ parsedPermissions,
+ shouldUpdate
+ };
+};
+
+const $updatePermissionsDown = (permissions: unknown) => {
+ const parsedPermissions = $unpackPermissions(permissions);
+
+ let shouldUpdate = false;
+ for (let i = 0; i < parsedPermissions.length; i += 1) {
+ const parsedPermission = parsedPermissions[i];
+
+ const { subject, action } = parsedPermission;
+
+ if (subject === ProjectPermissionSub.Secrets) {
+ const readValueIndex = action.indexOf(SecretActions.ReadValue);
+
+ if (action.includes(SecretActions.ReadValue) && readValueIndex !== -1) {
+ action.splice(readValueIndex, 1);
+ parsedPermissions[i] = { ...parsedPermission, action };
+
+ shouldUpdate = true;
+ }
+ }
+ }
+
+ const repackedPermissions = packRules(parsedPermissions);
+
+ return {
+ repackedPermissions,
+ shouldUpdate
+ };
+};
+
+const CHUNK_SIZE = 1000;
+
+export async function up(knex: Knex): Promise {
+ const projectRoles = await knex(TableName.ProjectRoles).select(selectAllTableCols(TableName.ProjectRoles));
+ const projectIdentityAdditionalPrivileges = await knex(TableName.IdentityProjectAdditionalPrivilege).select(
+ selectAllTableCols(TableName.IdentityProjectAdditionalPrivilege)
+ );
+ const projectUserAdditionalPrivileges = await knex(TableName.ProjectUserAdditionalPrivilege).select(
+ selectAllTableCols(TableName.ProjectUserAdditionalPrivilege)
+ );
+
+ const serviceTokens = await knex(TableName.ServiceToken).select(selectAllTableCols(TableName.ServiceToken));
+
+ const updatedServiceTokens = serviceTokens.reduce((acc, serviceToken) => {
+ const { permissions } = serviceToken; // Service tokens are special, and include an array of actions only.
+
+ if (permissions.includes(SecretActions.Read) && !permissions.includes(SecretActions.ReadValue)) {
+ permissions.push(SecretActions.ReadValue);
+ acc.push({
+ ...serviceToken,
+ permissions
+ });
+ }
+ return acc;
+ }, []);
+
+ if (updatedServiceTokens.length > 0) {
+ for (let i = 0; i < updatedServiceTokens.length; i += CHUNK_SIZE) {
+ const chunk = updatedServiceTokens.slice(i, i + CHUNK_SIZE);
+
+ // eslint-disable-next-line no-await-in-loop
+ await knex(TableName.ServiceToken)
+ .whereIn(
+ "id",
+ chunk.map((t) => t.id)
+ )
+ .update({
+ // @ts-expect-error -- raw query
+ permissions: knex.raw(
+ `CASE id
+ ${chunk.map((t) => `WHEN '${t.id}' THEN ?::text[]`).join(" ")}
+ END`,
+ chunk.map((t) => t.permissions)
+ )
+ });
+ }
+ }
+
+ const updatedRoles = projectRoles.reduce((acc, projectRole) => {
+ const { shouldUpdate, parsedPermissions } = $updatePermissionsUp(projectRole.permissions);
+
+ if (shouldUpdate) {
+ acc.push({
+ ...projectRole,
+ permissions: JSON.stringify(packRules(parsedPermissions))
+ });
+ }
+ return acc;
+ }, []);
+
+ const updatedIdentityAdditionalPrivileges = projectIdentityAdditionalPrivileges.reduce<
+ typeof projectIdentityAdditionalPrivileges
+ >((acc, identityAdditionalPrivilege) => {
+ const { shouldUpdate, parsedPermissions } = $updatePermissionsUp(identityAdditionalPrivilege.permissions);
+
+ if (shouldUpdate) {
+ acc.push({
+ ...identityAdditionalPrivilege,
+ permissions: JSON.stringify(packRules(parsedPermissions))
+ });
+ }
+ return acc;
+ }, []);
+
+ const updatedUserAdditionalPrivileges = projectUserAdditionalPrivileges.reduce<
+ typeof projectUserAdditionalPrivileges
+ >((acc, userAdditionalPrivilege) => {
+ const { shouldUpdate, parsedPermissions } = $updatePermissionsUp(userAdditionalPrivilege.permissions);
+
+ if (shouldUpdate) {
+ acc.push({
+ ...userAdditionalPrivilege,
+ permissions: JSON.stringify(packRules(parsedPermissions))
+ });
+ }
+ return acc;
+ }, []);
+
+ if (updatedRoles.length > 0) {
+ for (let i = 0; i < updatedRoles.length; i += CHUNK_SIZE) {
+ const chunk = updatedRoles.slice(i, i + CHUNK_SIZE);
+
+ // eslint-disable-next-line no-await-in-loop
+ await knex(TableName.ProjectRoles).insert(chunk).onConflict("id").merge(["permissions"]);
+ }
+ }
+
+ if (updatedIdentityAdditionalPrivileges.length > 0) {
+ for (let i = 0; i < updatedIdentityAdditionalPrivileges.length; i += CHUNK_SIZE) {
+ const chunk = updatedIdentityAdditionalPrivileges.slice(i, i + CHUNK_SIZE);
+
+ // eslint-disable-next-line no-await-in-loop
+ await knex(TableName.IdentityProjectAdditionalPrivilege).insert(chunk).onConflict("id").merge(["permissions"]);
+ }
+ }
+
+ if (updatedUserAdditionalPrivileges.length > 0) {
+ for (let i = 0; i < updatedUserAdditionalPrivileges.length; i += CHUNK_SIZE) {
+ const chunk = updatedUserAdditionalPrivileges.slice(i, i + CHUNK_SIZE);
+
+ // eslint-disable-next-line no-await-in-loop
+ await knex(TableName.ProjectUserAdditionalPrivilege).insert(chunk).onConflict("id").merge(["permissions"]);
+ }
+ }
+}
+
+export async function down(knex: Knex): Promise {
+ const projectRoles = await knex(TableName.ProjectRoles).select(selectAllTableCols(TableName.ProjectRoles));
+ const identityAdditionalPrivileges = await knex(TableName.IdentityProjectAdditionalPrivilege).select(
+ selectAllTableCols(TableName.IdentityProjectAdditionalPrivilege)
+ );
+ const userAdditionalPrivileges = await knex(TableName.ProjectUserAdditionalPrivilege).select(
+ selectAllTableCols(TableName.ProjectUserAdditionalPrivilege)
+ );
+ const serviceTokens = await knex(TableName.ServiceToken).select(selectAllTableCols(TableName.ServiceToken));
+
+ const updatedServiceTokens = serviceTokens.reduce((acc, serviceToken) => {
+ const { permissions } = serviceToken;
+
+ if (permissions.includes(SecretActions.ReadValue)) {
+ permissions.splice(permissions.indexOf(SecretActions.ReadValue), 1);
+ acc.push({
+ ...serviceToken,
+ permissions
+ });
+ }
+ return acc;
+ }, []);
+
+ if (updatedServiceTokens.length > 0) {
+ for (let i = 0; i < updatedServiceTokens.length; i += CHUNK_SIZE) {
+ const chunk = updatedServiceTokens.slice(i, i + CHUNK_SIZE);
+
+ // eslint-disable-next-line no-await-in-loop
+ await knex(TableName.ServiceToken)
+ .whereIn(
+ "id",
+ chunk.map((t) => t.id)
+ )
+ .update({
+ // @ts-expect-error -- raw query
+ permissions: knex.raw(
+ `CASE id
+ ${chunk.map((t) => `WHEN '${t.id}' THEN ?::text[]`).join(" ")}
+ END`,
+ chunk.map((t) => t.permissions)
+ )
+ });
+ }
+ }
+
+ const updatedRoles = projectRoles.reduce((acc, projectRole) => {
+ const { shouldUpdate, repackedPermissions } = $updatePermissionsDown(projectRole.permissions);
+
+ if (shouldUpdate) {
+ acc.push({
+ ...projectRole,
+ permissions: JSON.stringify(repackedPermissions)
+ });
+ }
+ return acc;
+ }, []);
+
+ const updatedIdentityAdditionalPrivileges = identityAdditionalPrivileges.reduce(
+ (acc, identityAdditionalPrivilege) => {
+ const { shouldUpdate, repackedPermissions } = $updatePermissionsDown(identityAdditionalPrivilege.permissions);
+
+ if (shouldUpdate) {
+ acc.push({
+ ...identityAdditionalPrivilege,
+ permissions: JSON.stringify(repackedPermissions)
+ });
+ }
+ return acc;
+ },
+ []
+ );
+
+ const updatedUserAdditionalPrivileges = userAdditionalPrivileges.reduce(
+ (acc, userAdditionalPrivilege) => {
+ const { shouldUpdate, repackedPermissions } = $updatePermissionsDown(userAdditionalPrivilege.permissions);
+
+ if (shouldUpdate) {
+ acc.push({
+ ...userAdditionalPrivilege,
+ permissions: JSON.stringify(repackedPermissions)
+ });
+ }
+ return acc;
+ },
+ []
+ );
+
+ if (updatedRoles.length > 0) {
+ for (let i = 0; i < updatedRoles.length; i += CHUNK_SIZE) {
+ const chunk = updatedRoles.slice(i, i + CHUNK_SIZE);
+
+ // eslint-disable-next-line no-await-in-loop
+ await knex(TableName.ProjectRoles).insert(chunk).onConflict("id").merge(["permissions"]);
+ }
+ }
+
+ if (updatedIdentityAdditionalPrivileges.length > 0) {
+ for (let i = 0; i < updatedIdentityAdditionalPrivileges.length; i += CHUNK_SIZE) {
+ const chunk = updatedIdentityAdditionalPrivileges.slice(i, i + CHUNK_SIZE);
+
+ // eslint-disable-next-line no-await-in-loop
+ await knex(TableName.IdentityProjectAdditionalPrivilege).insert(chunk).onConflict("id").merge(["permissions"]);
+ }
+ }
+
+ if (updatedUserAdditionalPrivileges.length > 0) {
+ for (let i = 0; i < updatedUserAdditionalPrivileges.length; i += CHUNK_SIZE) {
+ const chunk = updatedUserAdditionalPrivileges.slice(i, i + CHUNK_SIZE);
+
+ // eslint-disable-next-line no-await-in-loop
+ await knex(TableName.ProjectUserAdditionalPrivilege).insert(chunk).onConflict("id").merge(["permissions"]);
+ }
+ }
+}
diff --git a/backend/src/ee/routes/v1/secret-approval-request-router.ts b/backend/src/ee/routes/v1/secret-approval-request-router.ts
index 653d04d4f..1ceb6019b 100644
--- a/backend/src/ee/routes/v1/secret-approval-request-router.ts
+++ b/backend/src/ee/routes/v1/secret-approval-request-router.ts
@@ -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()
diff --git a/backend/src/ee/routes/v1/secret-router.ts b/backend/src/ee/routes/v1/secret-router.ts
index 4c249afe0..a964eb1b8 100644
--- a/backend/src/ee/routes/v1/secret-router.ts
+++ b/backend/src/ee/routes/v1/secret-router.ts
@@ -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()
diff --git a/backend/src/ee/routes/v1/secret-version-router.ts b/backend/src/ee/routes/v1/secret-version-router.ts
index 11443ebfe..a09a05c91 100644
--- a/backend/src/ee/routes/v1/secret-version-router.ts
+++ b/backend/src/ee/routes/v1/secret-version-router.ts
@@ -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 };
}
});
diff --git a/backend/src/ee/routes/v1/snapshot-router.ts b/backend/src/ee/routes/v1/snapshot-router.ts
index fc2d25712..283b9b31e 100644
--- a/backend/src/ee/routes/v1/snapshot-router.ts
+++ b/backend/src/ee/routes/v1/snapshot-router.ts
@@ -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 };
}
});
diff --git a/backend/src/ee/services/permission/project-permission.ts b/backend/src/ee/services/permission/project-permission.ts
index 4389c3866..3f69c481a 100644
--- a/backend/src/ee/services/permission/project-permission.ts
+++ b/backend/src/ee/services/permission/project-permission.ts
@@ -17,6 +17,14 @@ export enum ProjectPermissionActions {
Delete = "delete"
}
+export enum ProjectPermissionSecretActions {
+ DescribeSecret = "read",
+ ReadValue = "readValue",
+ Create = "create",
+ Edit = "edit",
+ Delete = "delete"
+}
+
export enum ProjectPermissionCmekActions {
Read = "read",
Create = "create",
@@ -115,7 +123,7 @@ export type IdentityManagementSubjectFields = {
export type ProjectPermissionSet =
| [
- ProjectPermissionActions,
+ ProjectPermissionSecretActions,
ProjectPermissionSub.Secrets | (ForcedSubject & SecretSubjectFields)
]
| [
@@ -429,6 +437,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 +469,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 +526,6 @@ const buildAdminPermissionRules = () => {
// Admins get full access to everything
[
- ProjectPermissionSub.Secrets,
ProjectPermissionSub.SecretFolders,
ProjectPermissionSub.SecretImports,
ProjectPermissionSub.SecretApproval,
@@ -550,10 +558,21 @@ const buildAdminPermissionRules = () => {
ProjectPermissionActions.Create,
ProjectPermissionActions.Delete
],
- el as ProjectPermissionSub
+ el
);
});
+ can(
+ [
+ ProjectPermissionSecretActions.DescribeSecret,
+ ProjectPermissionSecretActions.ReadValue,
+ ProjectPermissionSecretActions.Create,
+ ProjectPermissionSecretActions.Edit,
+ ProjectPermissionSecretActions.Delete
+ ],
+ ProjectPermissionSub.Secrets
+ );
+
can(
[
ProjectPermissionDynamicSecretActions.ReadRootCredential,
@@ -613,10 +632,11 @@ const buildMemberPermissionRules = () => {
can(
[
- ProjectPermissionActions.Read,
- ProjectPermissionActions.Edit,
- ProjectPermissionActions.Create,
- ProjectPermissionActions.Delete
+ ProjectPermissionSecretActions.DescribeSecret,
+ ProjectPermissionSecretActions.ReadValue,
+ ProjectPermissionSecretActions.Edit,
+ ProjectPermissionSecretActions.Create,
+ ProjectPermissionSecretActions.Delete
],
ProjectPermissionSub.Secrets
);
@@ -788,7 +808,8 @@ export const projectMemberPermissions = buildMemberPermissionRules();
const buildViewerPermissionRules = () => {
const { can, rules } = new AbilityBuilder>(createMongoAbility);
- can(ProjectPermissionActions.Read, 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);
@@ -831,6 +852,8 @@ export const buildServiceTokenProjectPermission = (
) => {
const canWrite = permission.includes("write");
const canRead = permission.includes("read");
+ const canReadValue = permission.includes("readValue");
+
const { can, build } = new AbilityBuilder>(createMongoAbility);
scopes.forEach(({ secretPath, environment }) => {
[ProjectPermissionSub.Secrets, ProjectPermissionSub.SecretImports, ProjectPermissionSub.SecretFolders].forEach(
@@ -860,6 +883,14 @@ export const buildServiceTokenProjectPermission = (
environment
});
}
+
+ if (subject === ProjectPermissionSub.Secrets && canReadValue) {
+ // @ts-expect-error type
+ can(ProjectPermissionSecretActions.ReadValue, subject as ProjectPermissionSub.Secrets, {
+ secretPath: { $glob: secretPath },
+ environment
+ });
+ }
}
);
});
@@ -916,7 +947,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 +999,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
);
diff --git a/backend/src/ee/services/secret-approval-request/secret-approval-request-service.ts b/backend/src/ee/services/secret-approval-request/secret-approval-request-service.ts
index d296d7975..8efae75e6 100644
--- a/backend/src/ee/services/secret-approval-request/secret-approval-request-service.ts
+++ b/backend/src/ee/services/secret-approval-request/secret-approval-request-service.ts
@@ -58,7 +58,7 @@ import { TUserDALFactory } from "@app/services/user/user-dal";
import { TLicenseServiceFactory } from "../license/license-service";
import { TPermissionServiceFactory } from "../permission/permission-service";
-import { ProjectPermissionActions, ProjectPermissionSub } from "../permission/project-permission";
+import { 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 +88,12 @@ type TSecretApprovalRequestServiceFactoryDep = {
secretDAL: TSecretDALFactory;
secretTagDAL: Pick<
TSecretTagDALFactory,
- "findManyTagsById" | "saveTagsToSecret" | "deleteTagsManySecret" | "saveTagsToSecretV2" | "deleteTagsToSecretV2"
+ | "findManyTagsById"
+ | "saveTagsToSecret"
+ | "deleteTagsManySecret"
+ | "saveTagsToSecretV2"
+ | "deleteTagsToSecretV2"
+ | "find"
>;
secretBlindIndexDAL: Pick;
snapshotService: Pick;
@@ -106,7 +111,7 @@ type TSecretApprovalRequestServiceFactoryDep = {
kmsService: Pick;
secretV2BridgeDAL: Pick<
TSecretV2BridgeDALFactory,
- "insertMany" | "upsertSecretReferences" | "findBySecretKeys" | "bulkUpdate" | "deleteMany"
+ "insertMany" | "upsertSecretReferences" | "findBySecretKeys" | "bulkUpdate" | "deleteMany" | "find"
>;
secretVersionV2BridgeDAL: Pick;
secretVersionTagV2BridgeDAL: Pick;
@@ -913,7 +918,7 @@ export const secretApprovalRequestServiceFactory = ({
actionProjectType: ActionProjectType.SecretManager
});
ForbiddenError.from(permission).throwUnlessCan(
- ProjectPermissionActions.Read,
+ ProjectPermissionSecretActions.ReadValue,
subject(ProjectPermissionSub.Secrets, { environment, secretPath })
);
@@ -1000,6 +1005,7 @@ export const secretApprovalRequestServiceFactory = ({
: keyName2BlindIndex[secretName];
// add tags
if (tagIds?.length) commitTagIds[keyName2BlindIndex[secretName]] = tagIds;
+
return {
...latestSecretVersions[secretId],
...el,
@@ -1363,9 +1369,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) action = ProjectPermissionSecretActions.Delete;
ForbiddenError.from(permission).throwUnlessCan(
action,
diff --git a/backend/src/ee/services/secret-replication/secret-replication-service.ts b/backend/src/ee/services/secret-replication/secret-replication-service.ts
index 3c25db98c..5fae2675d 100644
--- a/backend/src/ee/services/secret-replication/secret-replication-service.ts
+++ b/backend/src/ee/services/secret-replication/secret-replication-service.ts
@@ -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
diff --git a/backend/src/ee/services/secret-rotation/secret-rotation-service.ts b/backend/src/ee/services/secret-rotation/secret-rotation-service.ts
index 02da4b7ea..8d458111a 100644
--- a/backend/src/ee/services/secret-rotation/secret-rotation-service.ts
+++ b/backend/src/ee/services/secret-rotation/secret-rotation-service.ts
@@ -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 })
);
diff --git a/backend/src/ee/services/secret-snapshot/secret-snapshot-service.ts b/backend/src/ee/services/secret-snapshot/secret-snapshot-service.ts
index dc2cff456..397824904 100644
--- a/backend/src/ee/services/secret-snapshot/secret-snapshot-service.ts
+++ b/backend/src/ee/services/secret-snapshot/secret-snapshot-service.ts
@@ -23,7 +23,11 @@ import { TSecretVersionV2TagDALFactory } from "@app/services/secret-v2-bridge/se
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 {
TGetSnapshotDataDTO,
TProjectSnapshotCountDTO,
@@ -35,6 +39,7 @@ import { TSnapshotFolderDALFactory } from "./snapshot-folder-dal";
import { TSnapshotSecretDALFactory } from "./snapshot-secret-dal";
import { TSnapshotSecretV2DALFactory } from "./snapshot-secret-v2-dal";
import { getFullFolderPath } from "./snapshot-service-fns";
+import { INFISICAL_SECRET_VALUE_HIDDEN_MASK } from "@app/services/secret/secret-fns";
type TSecretSnapshotServiceFactoryDep = {
snapshotDAL: TSnapshotDALFactory;
@@ -98,7 +103,7 @@ export const secretSnapshotServiceFactory = ({
// 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,
+ ProjectPermissionSecretActions.DescribeSecret,
subject(ProjectPermissionSub.Secrets, { environment, secretPath: path })
);
@@ -135,7 +140,7 @@ export const secretSnapshotServiceFactory = ({
// 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,
+ ProjectPermissionSecretActions.DescribeSecret,
subject(ProjectPermissionSub.Secrets, { environment, secretPath: path })
);
@@ -162,6 +167,7 @@ export const secretSnapshotServiceFactory = ({
});
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.SecretRollback);
+
const shouldUseBridge = snapshot.projectVersion === 3;
let snapshotDetails;
if (shouldUseBridge) {
@@ -170,68 +176,110 @@ 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 = permission.can(
+ ProjectPermissionSecretActions.ReadValue,
+ subject(ProjectPermissionSub.Secrets, {
+ 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 = permission.can(
+ ProjectPermissionSecretActions.ReadValue,
+ subject(ProjectPermissionSub.Secrets, {
+ 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;
};
diff --git a/backend/src/lib/api-docs/constants.ts b/backend/src/lib/api-docs/constants.ts
index 94076cf25..fe893d5ab 100644
--- a/backend/src/lib/api-docs/constants.ts
+++ b/backend/src/lib/api-docs/constants.ts
@@ -667,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."
},
@@ -690,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:
@@ -718,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: {
diff --git a/backend/src/lib/errors/index.ts b/backend/src/lib/errors/index.ts
index f3dc83f26..950bfd29f 100644
--- a/backend/src/lib/errors/index.ts
+++ b/backend/src/lib/errors/index.ts
@@ -1,4 +1,5 @@
/* eslint-disable max-classes-per-file */
+
export class DatabaseError extends Error {
name: string;
diff --git a/backend/src/server/routes/sanitizedSchemas.ts b/backend/src/server/routes/sanitizedSchemas.ts
index a6cc8bae0..77149eb1b 100644
--- a/backend/src/server/routes/sanitizedSchemas.ts
+++ b/backend/src/server/routes/sanitizedSchemas.ts
@@ -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()
+});
diff --git a/backend/src/server/routes/v1/dashboard-router.ts b/backend/src/server/routes/v1/dashboard-router.ts
index d96c45f04..27ba3f8a3 100644
--- a/backend/src/server/routes/v1/dashboard-router.ts
+++ b/backend/src/server/routes/v1/dashboard-router.ts
@@ -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: {
@@ -862,22 +850,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 +869,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 +878,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,
diff --git a/backend/src/server/routes/v2/service-token-router.ts b/backend/src/server/routes/v2/service-token-router.ts
index fb10f17db..1a84c9123 100644
--- a/backend/src/server/routes/v2/service-token-router.ts
+++ b/backend/src/server/routes/v2/service-token-router.ts
@@ -94,7 +94,7 @@ export const registerServiceTokenRouter = async (server: FastifyZodProvider) =>
iv: z.string().trim(),
tag: z.string().trim(),
expiresIn: z.number().nullable(),
- permissions: z.enum(["read", "write"]).array()
+ permissions: z.enum(["read", "write", "readValue"]).array()
}),
response: {
200: z.object({
diff --git a/backend/src/server/routes/v3/secret-router.ts b/backend/src/server/routes/v3/secret-router.ts
index 4935345dc..b68d042f0 100644
--- a/backend/src/server/routes/v3/secret-router.ts
+++ b/backend/src/server/routes/v3/secret-router.ts
@@ -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 & { children: TSecretReferenceNode[] };
const SecretReferenceNodeTree: z.ZodType = 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 };
}
});
@@ -445,28 +419,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 +459,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 +666,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 +764,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 +788,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 +851,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 +947,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 +1218,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 +1388,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 +1705,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 +1820,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 +2086,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 +2208,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")
])
diff --git a/backend/src/services/external-migration/external-migration-fns.ts b/backend/src/services/external-migration/external-migration-fns.ts
index f4a54f0db..856b39012 100644
--- a/backend/src/services/external-migration/external-migration-fns.ts
+++ b/backend/src/services/external-migration/external-migration-fns.ts
@@ -31,9 +31,9 @@ export type TImportDataIntoInfisicalDTO = {
projectEnvDAL: Pick;
kmsService: Pick;
- secretDAL: Pick;
+ secretDAL: Pick;
secretVersionDAL: Pick;
- secretTagDAL: Pick;
+ secretTagDAL: Pick;
secretVersionTagDAL: Pick;
resourceMetadataDAL: Pick;
diff --git a/backend/src/services/external-migration/external-migration-queue.ts b/backend/src/services/external-migration/external-migration-queue.ts
index e3d77e832..8aa46b94c 100644
--- a/backend/src/services/external-migration/external-migration-queue.ts
+++ b/backend/src/services/external-migration/external-migration-queue.ts
@@ -27,9 +27,9 @@ export type TExternalMigrationQueueFactoryDep = {
projectEnvDAL: Pick;
kmsService: Pick;
- secretDAL: Pick;
+ secretDAL: Pick;
secretVersionDAL: Pick;
- secretTagDAL: Pick;
+ secretTagDAL: Pick;
secretVersionTagDAL: Pick;
folderDAL: Pick;
diff --git a/backend/src/services/integration-auth/integration-delete-secret.ts b/backend/src/services/integration-auth/integration-delete-secret.ts
index fdefd0e62..4b07245ac 100644
--- a/backend/src/services/integration-auth/integration-delete-secret.ts
+++ b/backend/src/services/integration-auth/integration-delete-secret.ts
@@ -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) {
diff --git a/backend/src/services/integration/integration-service.ts b/backend/src/services/integration/integration-service.ts
index 35994433a..f72357964 100644
--- a/backend/src/services/integration/integration-service.ts
+++ b/backend/src/services/integration/integration-service.ts
@@ -2,7 +2,11 @@ import { ForbiddenError, subject } from "@casl/ability";
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 { NotFoundError } from "@app/lib/errors";
import { TProjectPermission } from "@app/lib/types";
@@ -92,7 +96,7 @@ export const integrationServiceFactory = ({
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Create, ProjectPermissionSub.Integrations);
ForbiddenError.from(permission).throwUnlessCan(
- ProjectPermissionActions.Read,
+ ProjectPermissionSecretActions.ReadValue,
subject(ProjectPermissionSub.Secrets, {
environment: sourceEnvironment,
secretPath
@@ -175,7 +179,7 @@ export const integrationServiceFactory = ({
if (environment || secretPath) {
ForbiddenError.from(permission).throwUnlessCan(
- ProjectPermissionActions.Read,
+ ProjectPermissionSecretActions.ReadValue,
subject(ProjectPermissionSub.Secrets, {
environment: newEnvironment,
secretPath: newSecretPath
diff --git a/backend/src/services/project/project-service.ts b/backend/src/services/project/project-service.ts
index 4b110ebd2..5367a70cd 100644
--- a/backend/src/services/project/project-service.ts
+++ b/backend/src/services/project/project-service.ts
@@ -11,7 +11,11 @@ import {
import { TLicenseServiceFactory } from "@app/ee/services/license/license-service";
import { OrgPermissionActions, OrgPermissionSubjects } from "@app/ee/services/permission/org-permission";
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service";
-import { 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";
@@ -760,7 +764,10 @@ export const projectServiceFactory = ({
actorOrgId,
actionProjectType: ActionProjectType.Any
});
- ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.Secrets);
+ ForbiddenError.from(permission).throwUnlessCan(
+ ProjectPermissionSecretActions.DescribeSecret,
+ ProjectPermissionSub.Secrets
+ );
const project = await projectDAL.findProjectById(projectId);
diff --git a/backend/src/services/secret-import/secret-import-fns.ts b/backend/src/services/secret-import/secret-import-fns.ts
index d21ad3b9c..e5a450441 100644
--- a/backend/src/services/secret-import/secret-import-fns.ts
+++ b/backend/src/services/secret-import/secret-import-fns.ts
@@ -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 & {
importEnv: { id: string; slug: string; name: string };
})[];
folderDAL: Pick;
+ viewSecretValue: boolean;
secretDAL: Pick;
secretImportDAL: Pick;
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,
diff --git a/backend/src/services/secret-import/secret-import-service.ts b/backend/src/services/secret-import/secret-import-service.ts
index e8fde04d1..6c412edc0 100644
--- a/backend/src/services/secret-import/secret-import-service.ts
+++ b/backend/src/services/secret-import/secret-import-service.ts
@@ -5,7 +5,11 @@ import { ForbiddenError, subject } from "@casl/ability";
import { ActionProjectType, TableName } from "@app/db/schemas";
import { TLicenseServiceFactory } from "@app/ee/services/license/license-service";
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service";
-import { ProjectPermissionActions, ProjectPermissionSub } from "@app/ee/services/permission/project-permission";
+import {
+ 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";
@@ -90,7 +94,7 @@ export const secretImportServiceFactory = ({
// check if user has permission to import from target path
ForbiddenError.from(permission).throwUnlessCan(
- ProjectPermissionActions.Read,
+ ProjectPermissionSecretActions.DescribeSecret,
subject(ProjectPermissionSub.Secrets, {
environment: data.environment,
secretPath: data.path
@@ -402,7 +406,7 @@ export const secretImportServiceFactory = ({
// check if user has permission to import from target path
ForbiddenError.from(permission).throwUnlessCan(
- ProjectPermissionActions.Read,
+ ProjectPermissionSecretActions.DescribeSecret,
subject(ProjectPermissionSub.Secrets, {
environment: secretImportDoc.importEnv.slug,
secretPath: secretImportDoc.importPath
@@ -596,7 +600,7 @@ export const secretImportServiceFactory = ({
const secretImports = await secretImportDAL.find({ folderId: folder.id, isReplication: false });
const allowedImports = secretImports.filter((el) =>
permission.can(
- ProjectPermissionActions.Read,
+ ProjectPermissionSecretActions.ReadValue,
subject(ProjectPermissionSub.Secrets, {
environment: el.importEnv.slug,
secretPath: el.importPath
@@ -642,12 +646,13 @@ 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,
+ ProjectPermissionSecretActions.ReadValue,
subject(ProjectPermissionSub.Secrets, {
environment: expandEnvironment,
secretPath: expandSecretPath,
@@ -667,7 +672,7 @@ export const secretImportServiceFactory = ({
const allowedImports = secretImports.filter((el) =>
permission.can(
- ProjectPermissionActions.Read,
+ ProjectPermissionSecretActions.ReadValue,
subject(ProjectPermissionSub.Secrets, {
environment: el.importEnv.slug,
secretPath: el.importPath
@@ -683,7 +688,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
+ )
)
}));
};
diff --git a/backend/src/services/secret-sync/secret-sync-queue.ts b/backend/src/services/secret-sync/secret-sync-queue.ts
index cdc9540da..8afcf6416 100644
--- a/backend/src/services/secret-sync/secret-sync-queue.ts
+++ b/backend/src/services/secret-sync/secret-sync-queue.ts
@@ -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) {
diff --git a/backend/src/services/secret-sync/secret-sync-service.ts b/backend/src/services/secret-sync/secret-sync-service.ts
index 8211180e8..7b92a463c 100644
--- a/backend/src/services/secret-sync/secret-sync-service.ts
+++ b/backend/src/services/secret-sync/secret-sync-service.ts
@@ -3,7 +3,7 @@ import { ForbiddenError, subject } from "@casl/ability";
import { ActionProjectType } from "@app/db/schemas";
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service";
import {
- ProjectPermissionActions,
+ ProjectPermissionSecretActions,
ProjectPermissionSecretSyncActions,
ProjectPermissionSub
} from "@app/ee/services/permission/project-permission";
@@ -179,7 +179,7 @@ export const secretSyncServiceFactory = ({
);
ForbiddenError.from(projectPermission).throwUnlessCan(
- ProjectPermissionActions.Read,
+ ProjectPermissionSecretActions.ReadValue,
subject(ProjectPermissionSub.Secrets, {
environment,
secretPath
@@ -270,7 +270,7 @@ export const secretSyncServiceFactory = ({
throw new BadRequestError({ message: "Must specify both source environment and secret path" });
ForbiddenError.from(permission).throwUnlessCan(
- ProjectPermissionActions.Read,
+ ProjectPermissionSecretActions.ReadValue,
subject(ProjectPermissionSub.Secrets, {
environment: updatedEnvironment,
secretPath: updatedSecretPath
diff --git a/backend/src/services/secret-tag/secret-tag-dal.ts b/backend/src/services/secret-tag/secret-tag-dal.ts
index 1df64afa2..3b9151557 100644
--- a/backend/src/services/secret-tag/secret-tag-dal.ts
+++ b/backend/src/services/secret-tag/secret-tag-dal.ts
@@ -47,6 +47,7 @@ export const secretTagDALFactory = (db: TDbClient) => {
throw new DatabaseError({ error, name: "Find all by ids" });
}
};
+
return {
...secretTagOrm,
saveTagsToSecret: secretJnTagOrm.insertMany,
diff --git a/backend/src/services/secret-v2-bridge/secret-v2-bridge-fns.ts b/backend/src/services/secret-v2-bridge/secret-v2-bridge-fns.ts
index 751235cde..8e1de2d91 100644
--- a/backend/src/services/secret-v2-bridge/secret-v2-bridge-fns.ts
+++ b/backend/src/services/secret-v2-bridge/secret-v2-bridge-fns.ts
@@ -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";
@@ -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 ({
@@ -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
+ })
});
diff --git a/backend/src/services/secret-v2-bridge/secret-v2-bridge-service.ts b/backend/src/services/secret-v2-bridge/secret-v2-bridge-service.ts
index d6ba02856..3b7f04907 100644
--- a/backend/src/services/secret-v2-bridge/secret-v2-bridge-service.ts
+++ b/backend/src/services/secret-v2-bridge/secret-v2-bridge-service.ts
@@ -11,7 +11,11 @@ import {
TSecretsV2
} 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 { 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";
@@ -259,7 +263,7 @@ export const secretV2BridgeServiceFactory = ({
const { secretName, type, ...inputSecretData } = inputSecret;
ForbiddenError.from(permission).throwUnlessCan(
- ProjectPermissionActions.Create,
+ ProjectPermissionSecretActions.Create,
subject(ProjectPermissionSub.Secrets, {
environment,
secretPath,
@@ -278,8 +282,8 @@ export const secretV2BridgeServiceFactory = ({
type: KmsDataKey.SecretManager,
projectId
});
- const secret = await secretDAL.transaction((tx) =>
- fnSecretBulkInsert({
+ const secret = await secretDAL.transaction(async (tx) => {
+ const [createdSecret] = await fnSecretBulkInsert({
folderId,
orgId: actorOrgId,
inputSecrets: [
@@ -313,8 +317,10 @@ export const secretV2BridgeServiceFactory = ({
actorId
},
tx
- })
- );
+ });
+
+ return createdSecret;
+ });
if (inputSecret.type === SecretType.Shared) {
await snapshotService.performSnapshot(folderId);
@@ -328,11 +334,17 @@ export const secretV2BridgeServiceFactory = ({
});
}
- return reshapeBridgeSecret(projectId, environment, secretPath, {
- ...secret[0],
- value: inputSecret.secretValue,
- comment: inputSecret.secretComment || ""
- });
+ return reshapeBridgeSecret(
+ projectId,
+ environment,
+ secretPath,
+ {
+ ...secret,
+ value: inputSecret.secretValue,
+ comment: inputSecret.secretComment || ""
+ },
+ false
+ );
};
const updateSecret = async ({
@@ -401,7 +413,7 @@ export const secretV2BridgeServiceFactory = ({
}
ForbiddenError.from(permission).throwUnlessCan(
- ProjectPermissionActions.Edit,
+ ProjectPermissionSecretActions.Edit,
subject(ProjectPermissionSub.Secrets, {
environment,
secretPath,
@@ -412,18 +424,22 @@ export const secretV2BridgeServiceFactory = ({
// validate tags
// fetch all tags and if not same count throw error meaning one was invalid tags
- const tags = inputSecret.tagIds ? await secretTagDAL.find({ projectId, $in: { id: inputSecret.tagIds } }) : [];
- if ((inputSecret.tagIds || []).length !== tags.length)
- throw new NotFoundError({ message: `Tag not found. Found ${tags.map((el) => el.slug).join(",")}` });
+ const newTags = inputSecret.tagIds ? await secretTagDAL.find({ projectId, $in: { id: inputSecret.tagIds } }) : [];
+ if ((inputSecret.tagIds || []).length !== newTags.length)
+ throw new NotFoundError({ message: `Tag not found. Found ${newTags.map((el) => el.slug).join(",")}` });
+
+ const tagsToCheck = inputSecret.tagIds ? newTags : secret.tags;
// now check with new ids
ForbiddenError.from(permission).throwUnlessCan(
- ProjectPermissionActions.Edit,
+ ProjectPermissionSecretActions.Edit,
subject(ProjectPermissionSub.Secrets, {
environment,
secretPath,
secretName: inputSecret.secretName,
- secretTags: tags?.map((el) => el.slug)
+ ...(tagsToCheck.length && {
+ secretTags: tagsToCheck.map((el) => el.slug)
+ })
})
);
@@ -435,12 +451,14 @@ export const secretV2BridgeServiceFactory = ({
});
if (doesNewNameSecretExist) throw new BadRequestError({ message: "Secret with the new name already exist" });
ForbiddenError.from(permission).throwUnlessCan(
- ProjectPermissionActions.Edit,
+ ProjectPermissionSecretActions.Edit,
subject(ProjectPermissionSub.Secrets, {
environment,
secretPath,
secretName: inputSecret.newSecretName,
- secretTags: tags?.map((el) => el.slug)
+ ...(tagsToCheck.length && {
+ secretTags: tagsToCheck.map((el) => el.slug)
+ })
})
);
}
@@ -522,11 +540,29 @@ export const secretV2BridgeServiceFactory = ({
});
}
- return reshapeBridgeSecret(projectId, environment, secretPath, {
- ...updatedSecret[0],
- value: inputSecret.secretValue || "",
- comment: inputSecret.secretComment || ""
- });
+ const secretValueHidden = !permission.can(
+ ProjectPermissionSecretActions.ReadValue,
+ subject(ProjectPermissionSub.Secrets, {
+ environment,
+ secretPath,
+ secretName: inputSecret.secretName,
+ ...(tagsToCheck.length && {
+ secretTags: tagsToCheck.map((el) => el.slug)
+ })
+ })
+ );
+
+ return reshapeBridgeSecret(
+ projectId,
+ environment,
+ secretPath,
+ {
+ ...updatedSecret[0],
+ value: inputSecret.secretValue || "",
+ comment: inputSecret.secretComment || ""
+ },
+ secretValueHidden
+ );
};
const deleteSecret = async ({
@@ -572,7 +608,7 @@ export const secretV2BridgeServiceFactory = ({
});
if (!secretToDelete) throw new NotFoundError({ message: "Secret not found" });
ForbiddenError.from(permission).throwUnlessCan(
- ProjectPermissionActions.Delete,
+ ProjectPermissionSecretActions.Delete,
subject(ProjectPermissionSub.Secrets, {
environment,
secretPath,
@@ -614,15 +650,32 @@ export const secretV2BridgeServiceFactory = ({
type: KmsDataKey.SecretManager,
projectId
});
- return reshapeBridgeSecret(projectId, environment, secretPath, {
- ...deletedSecret[0],
- value: deletedSecret[0].encryptedValue
- ? secretManagerDecryptor({ cipherTextBlob: deletedSecret[0].encryptedValue }).toString()
- : "",
- comment: deletedSecret[0].encryptedComment
- ? secretManagerDecryptor({ cipherTextBlob: deletedSecret[0].encryptedComment }).toString()
- : ""
- });
+
+ const secretValueHidden = !permission.can(
+ ProjectPermissionSecretActions.ReadValue,
+ subject(ProjectPermissionSub.Secrets, {
+ environment,
+ secretPath,
+ secretName: secretToDelete.key,
+ secretTags: secretToDelete.tags?.map((el) => el.slug)
+ })
+ );
+
+ return reshapeBridgeSecret(
+ projectId,
+ environment,
+ secretPath,
+ {
+ ...deletedSecret[0],
+ value: deletedSecret[0].encryptedValue
+ ? secretManagerDecryptor({ cipherTextBlob: deletedSecret[0].encryptedValue }).toString()
+ : "",
+ comment: deletedSecret[0].encryptedComment
+ ? secretManagerDecryptor({ cipherTextBlob: deletedSecret[0].encryptedComment }).toString()
+ : ""
+ },
+ secretValueHidden
+ );
};
// get unique secrets count for multiple envs
@@ -650,7 +703,10 @@ export const secretV2BridgeServiceFactory = ({
actionProjectType: ActionProjectType.SecretManager
});
- ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.Secrets);
+ ForbiddenError.from(permission).throwUnlessCan(
+ ProjectPermissionSecretActions.DescribeSecret,
+ ProjectPermissionSub.Secrets
+ );
}
const folders = await folderDAL.findBySecretPathMultiEnv(projectId, environments, path);
@@ -697,7 +753,10 @@ export const secretV2BridgeServiceFactory = ({
actionProjectType: ActionProjectType.SecretManager
});
- ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.Secrets);
+ ForbiddenError.from(permission).throwUnlessCan(
+ ProjectPermissionSecretActions.DescribeSecret,
+ ProjectPermissionSub.Secrets
+ );
const folder = await folderDAL.findBySecretPath(projectId, environment, path);
if (!folder) return 0;
@@ -708,7 +767,13 @@ export const secretV2BridgeServiceFactory = ({
};
const getSecretsByFolderMappings = async (
- { projectId, userId, filters, folderMappings }: TGetSecretsRawByFolderMappingsDTO,
+ {
+ projectId,
+ userId,
+ filters,
+ folderMappings,
+ filterByAction = ProjectPermissionSecretActions.ReadValue
+ }: TGetSecretsRawByFolderMappingsDTO,
projectPermission: Awaited>["permission"]
) => {
const groupedFolderMappings = groupBy(folderMappings, (folderMapping) => folderMapping.folderId);
@@ -728,7 +793,7 @@ export const secretV2BridgeServiceFactory = ({
const decryptedSecrets = secrets
.filter((el) =>
projectPermission.can(
- ProjectPermissionActions.Read,
+ filterByAction,
subject(ProjectPermissionSub.Secrets, {
environment: groupedFolderMappings[el.folderId][0].environment,
secretPath: groupedFolderMappings[el.folderId][0].path,
@@ -737,8 +802,19 @@ export const secretV2BridgeServiceFactory = ({
})
)
)
- .map((secret) =>
- reshapeBridgeSecret(
+ .map((secret) => {
+ // Note(Daniel): This is only relevant if the filterAction isn't set to ReadValue. This is needed for the frontend.
+ const secretValueHidden = !projectPermission.can(
+ ProjectPermissionSecretActions.ReadValue,
+ subject(ProjectPermissionSub.Secrets, {
+ environment: groupedFolderMappings[secret.folderId][0].environment,
+ secretPath: groupedFolderMappings[secret.folderId][0].path,
+ secretName: secret.key,
+ secretTags: secret.tags.map((i) => i.slug)
+ })
+ );
+
+ return reshapeBridgeSecret(
projectId,
groupedFolderMappings[secret.folderId][0].environment,
groupedFolderMappings[secret.folderId][0].path,
@@ -750,9 +826,10 @@ export const secretV2BridgeServiceFactory = ({
comment: secret.encryptedComment
? secretManagerDecryptor({ cipherTextBlob: secret.encryptedComment }).toString()
: ""
- }
- )
- );
+ },
+ secretValueHidden
+ );
+ });
return decryptedSecrets;
};
@@ -781,7 +858,10 @@ export const secretV2BridgeServiceFactory = ({
actionProjectType: ActionProjectType.SecretManager
});
if (!isInternal) {
- ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.Secrets);
+ ForbiddenError.from(permission).throwUnlessCan(
+ ProjectPermissionSecretActions.DescribeSecret,
+ ProjectPermissionSub.Secrets
+ );
}
const folders = await folderDAL.findBySecretPathMultiEnv(projectId, environments, path);
@@ -801,7 +881,8 @@ export const secretV2BridgeServiceFactory = ({
projectId,
folderMappings,
filters: params,
- userId: actorId
+ userId: actorId,
+ filterByAction: ProjectPermissionSecretActions.DescribeSecret
},
permission
);
@@ -816,10 +897,12 @@ export const secretV2BridgeServiceFactory = ({
projectId,
actor,
actorOrgId,
+ viewSecretValue,
actorAuthMethod,
includeImports,
recursive,
expandSecretReferences: shouldExpandSecretReferences,
+ throwOnMissingReadValuePermission = true,
...params
}: TGetSecretsDTO) => {
const { permission } = await permissionService.getProjectPermission({
@@ -831,7 +914,14 @@ export const secretV2BridgeServiceFactory = ({
actionProjectType: ActionProjectType.SecretManager
});
- ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.Secrets);
+ ForbiddenError.from(permission).throwUnlessCan(
+ ProjectPermissionSecretActions.DescribeSecret,
+ subject(ProjectPermissionSub.Secrets, {
+ environment,
+ secretPath: path,
+ secretTags: params.tagSlugs
+ })
+ );
let paths: { folderId: string; path: string }[] = [];
@@ -869,28 +959,87 @@ export const secretV2BridgeServiceFactory = ({
});
const decryptedSecrets = secrets
- .filter((el) =>
- permission.can(
- ProjectPermissionActions.Read,
+ .filter((el) => {
+ const canDescribeSecret = permission.can(
+ ProjectPermissionSecretActions.DescribeSecret,
subject(ProjectPermissionSub.Secrets, {
environment,
secretPath: groupedPaths[el.folderId][0].path,
secretName: el.key,
secretTags: el.tags.map((i) => i.slug)
})
- )
- )
- .map((secret) =>
- reshapeBridgeSecret(projectId, environment, groupedPaths[secret.folderId][0].path, {
- ...secret,
- value: secret.encryptedValue
- ? secretManagerDecryptor({ cipherTextBlob: secret.encryptedValue }).toString()
- : "",
- comment: secret.encryptedComment
- ? secretManagerDecryptor({ cipherTextBlob: secret.encryptedComment }).toString()
- : ""
- })
- );
+ );
+
+ if (!canDescribeSecret) {
+ return false;
+ }
+
+ if (viewSecretValue) {
+ // Recursive secret, should be filtered out
+ if (groupedPaths[el.folderId][0].path !== path) {
+ const canReadRecursiveSecretValue = permission.can(
+ ProjectPermissionSecretActions.ReadValue,
+ subject(ProjectPermissionSub.Secrets, {
+ environment,
+ secretPath: groupedPaths[el.folderId][0].path,
+ secretName: el.key,
+ secretTags: el.tags.map((i) => i.slug)
+ })
+ );
+
+ if (!canReadRecursiveSecretValue) {
+ return false;
+ }
+ }
+
+ if (throwOnMissingReadValuePermission) {
+ ForbiddenError.from(permission).throwUnlessCan(
+ ProjectPermissionSecretActions.ReadValue,
+ subject(ProjectPermissionSub.Secrets, {
+ environment,
+ secretPath: groupedPaths[el.folderId][0].path,
+ secretName: el.key,
+ secretTags: el.tags.map((i) => i.slug)
+ })
+ );
+ }
+ // Else, we do nothing. Because we don't want to filter out the secret, OR throw an error.
+ // If the user doesn't have access to read the value, in the below map function, we mask the secret value and return the secret with a hidden value.
+ }
+
+ return canDescribeSecret;
+ })
+ .map((secret) => {
+ const isPersonalSecret = secret.userId === actorId && secret.type === SecretType.Personal;
+
+ const secretValueHidden =
+ !viewSecretValue ||
+ !permission.can(
+ ProjectPermissionSecretActions.ReadValue,
+ subject(ProjectPermissionSub.Secrets, {
+ environment,
+ secretPath: groupedPaths[secret.folderId][0].path,
+ secretName: secret.key,
+ secretTags: secret.tags.map((i) => i.slug)
+ })
+ );
+
+ return reshapeBridgeSecret(
+ projectId,
+ environment,
+ groupedPaths[secret.folderId][0].path,
+ {
+ ...secret,
+ value: secret.encryptedValue
+ ? secretManagerDecryptor({ cipherTextBlob: secret.encryptedValue }).toString()
+ : "",
+ comment: secret.encryptedComment
+ ? secretManagerDecryptor({ cipherTextBlob: secret.encryptedComment }).toString()
+ : ""
+ },
+ secretValueHidden && !isPersonalSecret
+ );
+ });
const { expandSecretReferences } = expandSecretReferencesFactory({
projectId,
@@ -899,7 +1048,7 @@ export const secretV2BridgeServiceFactory = ({
decryptSecretValue: (value) => (value ? secretManagerDecryptor({ cipherTextBlob: value }).toString() : undefined),
canExpandValue: (expandEnvironment, expandSecretPath, expandSecretKey, expandSecretTags) =>
permission.can(
- ProjectPermissionActions.Read,
+ ProjectPermissionSecretActions.ReadValue,
subject(ProjectPermissionSub.Secrets, {
environment: expandEnvironment,
secretPath: expandSecretPath,
@@ -938,22 +1087,36 @@ export const secretV2BridgeServiceFactory = ({
const secretImports = await secretImportDAL.findByFolderIds(paths.map((p) => p.folderId));
const allowedImports = secretImports.filter(({ isReplication }) => !isReplication);
const importedSecrets = await fnSecretsV2FromImports({
+ viewSecretValue,
secretImports: allowedImports,
secretDAL,
folderDAL,
secretImportDAL,
expandSecretReferences,
decryptor: (value) => (value ? secretManagerDecryptor({ cipherTextBlob: value }).toString() : ""),
- hasSecretAccess: (expandEnvironment, expandSecretPath, expandSecretKey, expandSecretTags) =>
- permission.can(
- ProjectPermissionActions.Read,
+ hasSecretAccess: (expandEnvironment, expandSecretPath, expandSecretKey, expandSecretTags) => {
+ const canDescribe = permission.can(
+ ProjectPermissionSecretActions.DescribeSecret,
subject(ProjectPermissionSub.Secrets, {
environment: expandEnvironment,
secretPath: expandSecretPath,
secretName: expandSecretKey,
secretTags: expandSecretTags
})
- )
+ );
+
+ const canReadValue = permission.can(
+ ProjectPermissionSecretActions.ReadValue,
+ subject(ProjectPermissionSub.Secrets, {
+ environment: expandEnvironment,
+ secretPath: expandSecretPath,
+ secretName: expandSecretKey,
+ secretTags: expandSecretTags
+ })
+ );
+
+ return viewSecretValue ? canDescribe && canReadValue : canDescribe;
+ }
});
return {
@@ -1040,6 +1203,7 @@ export const secretV2BridgeServiceFactory = ({
type,
secretName,
version,
+ viewSecretValue,
includeImports,
expandSecretReferences: shouldExpandSecretReferences
}: TGetASecretDTO) => {
@@ -1101,7 +1265,7 @@ export const secretV2BridgeServiceFactory = ({
));
ForbiddenError.from(permission).throwUnlessCan(
- ProjectPermissionActions.Read,
+ ProjectPermissionSecretActions.DescribeSecret,
subject(ProjectPermissionSub.Secrets, {
environment,
secretPath: path,
@@ -1110,56 +1274,93 @@ export const secretV2BridgeServiceFactory = ({
})
);
+ // this will throw if the user doesn't have read value permission no matter what
+ // because if its an expansion, it will fully depend on the value.
const { expandSecretReferences } = expandSecretReferencesFactory({
projectId,
folderDAL,
secretDAL,
decryptSecretValue: (value) => (value ? secretManagerDecryptor({ cipherTextBlob: value }).toString() : undefined),
- canExpandValue: (expandEnvironment, expandSecretPath, expandSecretKey, expandSecretTags) =>
- permission.can(
- ProjectPermissionActions.Read,
+ canExpandValue: (expandEnvironment, expandSecretPath, expandSecretKey, expandSecretTags) => {
+ return permission.can(
+ ProjectPermissionSecretActions.ReadValue,
subject(ProjectPermissionSub.Secrets, {
environment: expandEnvironment,
secretPath: expandSecretPath,
secretName: expandSecretKey,
secretTags: expandSecretTags
})
- )
+ );
+ }
});
// now if secret is not found
// then search for imported secrets
// here we consider the import order also thus starting from bottom
+
+ // currently filters out the secrets that the user doesn't have access to read value on
if (!secret && includeImports) {
const secretImports = await secretImportDAL.find({ folderId, isReplication: false });
const importedSecrets = await fnSecretsV2FromImports({
secretImports,
+ viewSecretValue,
secretDAL,
folderDAL,
secretImportDAL,
decryptor: (value) => (value ? secretManagerDecryptor({ cipherTextBlob: value }).toString() : ""),
expandSecretReferences: shouldExpandSecretReferences ? expandSecretReferences : undefined,
- hasSecretAccess: (expandEnvironment, expandSecretPath, expandSecretKey, expandSecretTags) =>
- permission.can(
- ProjectPermissionActions.Read,
+ hasSecretAccess: (expandEnvironment, expandSecretPath, expandSecretKey, expandSecretTags) => {
+ return permission.can(
+ ProjectPermissionSecretActions.DescribeSecret,
subject(ProjectPermissionSub.Secrets, {
environment: expandEnvironment,
secretPath: expandSecretPath,
secretName: expandSecretKey,
secretTags: expandSecretTags
})
- )
+ );
+ }
});
for (let i = importedSecrets.length - 1; i >= 0; i -= 1) {
for (let j = 0; j < importedSecrets[i].secrets.length; j += 1) {
const importedSecret = importedSecrets[i].secrets[j];
if (secretName === importedSecret.key) {
- return reshapeBridgeSecret(projectId, importedSecrets[i].environment, importedSecrets[i].secretPath, {
- ...importedSecret,
- value: importedSecret.secretValue || "",
- comment: importedSecret.secretComment || ""
- });
+ let secretValueHidden = true;
+
+ if (viewSecretValue) {
+ if (
+ !permission.can(
+ ProjectPermissionSecretActions.ReadValue,
+ subject(ProjectPermissionSub.Secrets, {
+ environment: importedSecret.environment,
+ secretPath: importedSecrets[i].secretPath,
+ secretName: importedSecret.key,
+ secretTags: (importedSecret.secretTags || []).map((el) => el.slug)
+ })
+ ) &&
+ secretType !== SecretType.Personal
+ ) {
+ throw new ForbiddenRequestError({
+ message: `You do not have permission to view secret import value on secret with name '${secretName}'`,
+ name: "ForbiddenReadSecretError"
+ });
+ }
+
+ secretValueHidden = false;
+ }
+
+ return reshapeBridgeSecret(
+ projectId,
+ importedSecrets[i].environment,
+ importedSecrets[i].secretPath,
+ {
+ ...importedSecret,
+ value: importedSecret.secretValue || "",
+ comment: importedSecret.secretComment || ""
+ },
+ secretValueHidden
+ );
}
}
}
@@ -1181,13 +1382,43 @@ export const secretV2BridgeServiceFactory = ({
secretValue = expandedSecretValue || "";
}
- return reshapeBridgeSecret(projectId, environment, path, {
- ...secret,
- value: secretValue,
- comment: secret.encryptedComment
- ? secretManagerDecryptor({ cipherTextBlob: secret.encryptedComment }).toString()
- : ""
- });
+ let secretValueHidden = true;
+
+ if (viewSecretValue) {
+ if (
+ !permission.can(
+ ProjectPermissionSecretActions.ReadValue,
+ subject(ProjectPermissionSub.Secrets, {
+ environment,
+ secretPath: path,
+ secretName,
+ secretTags: (secret?.tags || []).map((el) => el.slug)
+ })
+ ) &&
+ secretType !== SecretType.Personal
+ ) {
+ throw new ForbiddenRequestError({
+ message: `You do not have permission to view secret value on secret with name '${secretName}'`,
+ name: "ForbiddenReadSecretError"
+ });
+ }
+
+ secretValueHidden = false;
+ }
+
+ return reshapeBridgeSecret(
+ projectId,
+ environment,
+ path,
+ {
+ ...secret,
+ value: secretValue,
+ comment: secret.encryptedComment
+ ? secretManagerDecryptor({ cipherTextBlob: secret.encryptedComment }).toString()
+ : ""
+ },
+ secretValueHidden
+ );
};
const createManySecret = async ({
@@ -1255,7 +1486,7 @@ export const secretV2BridgeServiceFactory = ({
inputSecrets.forEach((el) => {
ForbiddenError.from(permission).throwUnlessCan(
- ProjectPermissionActions.Create,
+ ProjectPermissionSecretActions.Create,
subject(ProjectPermissionSub.Secrets, {
environment,
secretPath,
@@ -1330,13 +1561,29 @@ export const secretV2BridgeServiceFactory = ({
environmentSlug: folder.environment.slug
});
- return newSecrets.map((el) =>
- reshapeBridgeSecret(projectId, environment, secretPath, {
- ...el,
- value: el.encryptedValue ? secretManagerDecryptor({ cipherTextBlob: el.encryptedValue }).toString() : "",
- comment: el.encryptedComment ? secretManagerDecryptor({ cipherTextBlob: el.encryptedComment }).toString() : ""
- })
- );
+ return newSecrets.map((el) => {
+ const secretValueHidden = !permission.can(
+ ProjectPermissionSecretActions.ReadValue,
+ subject(ProjectPermissionSub.Secrets, {
+ environment,
+ secretPath,
+ secretName: el.key,
+ secretTags: el.tags?.map((i) => i.slug)
+ })
+ );
+
+ return reshapeBridgeSecret(
+ projectId,
+ environment,
+ secretPath,
+ {
+ ...el,
+ value: el.encryptedValue ? secretManagerDecryptor({ cipherTextBlob: el.encryptedValue }).toString() : "",
+ comment: el.encryptedComment ? secretManagerDecryptor({ cipherTextBlob: el.encryptedComment }).toString() : ""
+ },
+ secretValueHidden
+ );
+ });
};
const updateManySecret = async ({
@@ -1379,7 +1626,17 @@ export const secretV2BridgeServiceFactory = ({
const { encryptor: secretManagerEncryptor, decryptor: secretManagerDecryptor } =
await kmsService.createCipherPairWithDataKey({ type: KmsDataKey.SecretManager, projectId });
- const updatedSecrets: Array = [];
+ const updatedSecrets: Array<
+ TSecretsV2 & {
+ secretPath: string;
+ tags: {
+ id: string;
+ slug: string;
+ color?: string | null;
+ name: string;
+ }[];
+ }
+ > = [];
await secretDAL.transaction(async (tx) => {
for await (const folder of folders) {
if (!folder) throw new NotFoundError({ message: "Folder not found" });
@@ -1430,7 +1687,7 @@ export const secretV2BridgeServiceFactory = ({
secretsToUpdateInDB.forEach((el) => {
ForbiddenError.from(permission).throwUnlessCan(
- ProjectPermissionActions.Edit,
+ ProjectPermissionSecretActions.Edit,
subject(ProjectPermissionSub.Secrets, {
environment,
secretPath,
@@ -1450,7 +1707,7 @@ export const secretV2BridgeServiceFactory = ({
if (updateMode === SecretUpdateMode.Upsert) {
secretsToCreate.forEach((el) => {
ForbiddenError.from(permission).throwUnlessCan(
- ProjectPermissionActions.Create,
+ ProjectPermissionSecretActions.Create,
subject(ProjectPermissionSub.Secrets, {
environment,
secretPath,
@@ -1464,7 +1721,7 @@ export const secretV2BridgeServiceFactory = ({
// check again to avoid non authorized tags are removed
secretsToUpdate.forEach((el) => {
ForbiddenError.from(permission).throwUnlessCan(
- ProjectPermissionActions.Edit,
+ ProjectPermissionSecretActions.Edit,
subject(ProjectPermissionSub.Secrets, {
environment,
secretPath,
@@ -1516,7 +1773,7 @@ export const secretV2BridgeServiceFactory = ({
secretsWithNewName.forEach((el) => {
ForbiddenError.from(permission).throwUnlessCan(
- ProjectPermissionActions.Create,
+ ProjectPermissionSecretActions.Create,
subject(ProjectPermissionSub.Secrets, {
environment,
secretPath,
@@ -1582,6 +1839,7 @@ export const secretV2BridgeServiceFactory = ({
},
resourceMetadataDAL
});
+
updatedSecrets.push(...bulkUpdatedSecrets.map((el) => ({ ...el, secretPath: folder.path })));
if (updateMode === SecretUpdateMode.Upsert) {
const bulkInsertedSecrets = await fnSecretBulkInsert({
@@ -1618,6 +1876,7 @@ export const secretV2BridgeServiceFactory = ({
},
tx
});
+
updatedSecrets.push(...bulkInsertedSecrets.map((el) => ({ ...el, secretPath: folder.path })));
}
}
@@ -1639,13 +1898,33 @@ export const secretV2BridgeServiceFactory = ({
)
);
- return updatedSecrets.map((el) =>
- reshapeBridgeSecret(projectId, environment, el.secretPath, {
- ...el,
- value: el.encryptedValue ? secretManagerDecryptor({ cipherTextBlob: el.encryptedValue }).toString() : "",
- comment: el.encryptedComment ? secretManagerDecryptor({ cipherTextBlob: el.encryptedComment }).toString() : ""
- })
- );
+ return updatedSecrets.map((el) => {
+ const secretValueHidden = !permission.can(
+ ProjectPermissionSecretActions.ReadValue,
+ subject(ProjectPermissionSub.Secrets, {
+ environment,
+ secretPath: el.secretPath,
+ secretName: el.key,
+ secretTags: el.tags.map((i) => i.slug)
+ })
+ );
+
+ return {
+ ...reshapeBridgeSecret(
+ projectId,
+ environment,
+ el.secretPath,
+ {
+ ...el,
+ value: el.encryptedValue ? secretManagerDecryptor({ cipherTextBlob: el.encryptedValue }).toString() : "",
+ comment: el.encryptedComment
+ ? secretManagerDecryptor({ cipherTextBlob: el.encryptedComment }).toString()
+ : ""
+ },
+ secretValueHidden
+ )
+ };
+ });
};
const deleteManySecret = async ({
@@ -1707,7 +1986,7 @@ export const secretV2BridgeServiceFactory = ({
});
secretsToDelete.forEach((el) => {
ForbiddenError.from(permission).throwUnlessCan(
- ProjectPermissionActions.Delete,
+ ProjectPermissionSecretActions.Delete,
subject(ProjectPermissionSub.Secrets, {
environment,
secretPath,
@@ -1746,13 +2025,35 @@ export const secretV2BridgeServiceFactory = ({
type: KmsDataKey.SecretManager,
projectId
});
- return secretsDeleted.map((el) =>
- reshapeBridgeSecret(projectId, environment, secretPath, {
- ...el,
- value: el.encryptedValue ? secretManagerDecryptor({ cipherTextBlob: el.encryptedValue }).toString() : "",
- comment: el.encryptedComment ? secretManagerDecryptor({ cipherTextBlob: el.encryptedComment }).toString() : ""
- })
- );
+ return secretsDeleted.map((el) => {
+ const secretToDeleteMatch = secretsToDelete.find(
+ (i) => i.key === el.key && (i.type || SecretType.Shared) === el.type
+ );
+
+ const secretValueHidden =
+ !secretToDeleteMatch ||
+ !permission.can(
+ ProjectPermissionSecretActions.ReadValue,
+ subject(ProjectPermissionSub.Secrets, {
+ environment,
+ secretPath,
+ secretName: el.key,
+ secretTags: secretToDeleteMatch.tags?.map((i) => i.slug)
+ })
+ );
+
+ return reshapeBridgeSecret(
+ projectId,
+ environment,
+ secretPath,
+ {
+ ...el,
+ value: el.encryptedValue ? secretManagerDecryptor({ cipherTextBlob: el.encryptedValue }).toString() : "",
+ comment: el.encryptedComment ? secretManagerDecryptor({ cipherTextBlob: el.encryptedComment }).toString() : ""
+ },
+ secretValueHidden
+ );
+ });
};
const getSecretVersions = async ({
@@ -1765,11 +2066,18 @@ export const secretV2BridgeServiceFactory = ({
secretId
}: TGetSecretVersionsDTO) => {
const secret = await secretDAL.findById(secretId);
+
if (!secret) throw new NotFoundError({ message: `Secret with ID '${secretId}' not found` });
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 { permission } = await permissionService.getProjectPermission({
actor,
actorId,
@@ -1788,13 +2096,29 @@ export const secretV2BridgeServiceFactory = ({
limit,
sort: [["createdAt", "desc"]]
});
-
return secretVersions.map((el) => {
- return reshapeBridgeSecret(folder.projectId, folder.environment.envSlug, "/", {
- ...el,
- value: el.encryptedValue ? secretManagerDecryptor({ cipherTextBlob: el.encryptedValue }).toString() : "",
- comment: el.encryptedComment ? secretManagerDecryptor({ cipherTextBlob: el.encryptedComment }).toString() : ""
- });
+ const secretValueHidden = permission.cannot(
+ ProjectPermissionSecretActions.ReadValue,
+ subject(ProjectPermissionSub.Secrets, {
+ environment: folder.environment.envSlug,
+ secretPath: folderWithPath.path,
+ secretName: el.key,
+ ...(el.tags?.length && {
+ secretTags: el.tags.map((tag) => tag.slug)
+ })
+ })
+ );
+ return reshapeBridgeSecret(
+ folder.projectId,
+ folder.environment.envSlug,
+ folderWithPath.path,
+ {
+ ...el,
+ value: el.encryptedValue ? secretManagerDecryptor({ cipherTextBlob: el.encryptedValue }).toString() : "",
+ comment: el.encryptedComment ? secretManagerDecryptor({ cipherTextBlob: el.encryptedComment }).toString() : ""
+ },
+ secretValueHidden
+ );
});
};
@@ -1890,16 +2214,26 @@ export const secretV2BridgeServiceFactory = ({
[`${TableName.SecretV2}.id` as "id"]: secretIds
}
});
+
+ const sourceActions = [
+ ProjectPermissionSecretActions.Delete,
+ ProjectPermissionSecretActions.ReadValue,
+ ProjectPermissionSecretActions.DescribeSecret
+ ] as const;
+ const destinationActions = [ProjectPermissionSecretActions.Create, ProjectPermissionSecretActions.Edit] as const;
+
sourceSecrets.forEach((secret) => {
- ForbiddenError.from(permission).throwUnlessCan(
- ProjectPermissionActions.Delete,
- subject(ProjectPermissionSub.Secrets, {
- environment: sourceEnvironment,
- secretPath: sourceSecretPath,
- secretName: secret.key,
- secretTags: secret.tags.map((el) => el.slug)
- })
- );
+ for (const sourceAction of sourceActions) {
+ ForbiddenError.from(permission).throwUnlessCan(
+ sourceAction,
+ subject(ProjectPermissionSub.Secrets, {
+ environment: sourceEnvironment,
+ secretPath: sourceSecretPath,
+ secretName: secret.key,
+ secretTags: secret.tags.map((el) => el.slug)
+ })
+ );
+ }
});
if (sourceSecrets.length !== secretIds.length) {
@@ -1974,27 +2308,17 @@ export const secretV2BridgeServiceFactory = ({
// permission check whether can create or edit the ones in the destination folder
locallyCreatedSecrets.forEach((secret) => {
- ForbiddenError.from(permission).throwUnlessCan(
- ProjectPermissionActions.Create,
- subject(ProjectPermissionSub.Secrets, {
- environment: destinationEnvironment,
- secretPath: destinationEnvironment,
- secretName: secret.key,
- secretTags: secret.tags.map((el) => el.slug)
- })
- );
- });
-
- locallyUpdatedSecrets.forEach((secret) => {
- ForbiddenError.from(permission).throwUnlessCan(
- ProjectPermissionActions.Edit,
- subject(ProjectPermissionSub.Secrets, {
- environment: destinationEnvironment,
- secretPath: destinationEnvironment,
- secretName: secret.key,
- secretTags: secret.tags.map((el) => el.slug)
- })
- );
+ for (const destinationAction of destinationActions) {
+ ForbiddenError.from(permission).throwUnlessCan(
+ destinationAction,
+ subject(ProjectPermissionSub.Secrets, {
+ environment: destinationEnvironment,
+ secretPath: destinationFolder.path,
+ secretName: secret.key,
+ secretTags: secret.tags.map((el) => el.slug)
+ })
+ );
+ }
});
const destinationFolderPolicy = await secretApprovalPolicyService.getSecretApprovalPolicy(
@@ -2232,7 +2556,7 @@ export const secretV2BridgeServiceFactory = ({
});
ForbiddenError.from(permission).throwUnlessCan(
- ProjectPermissionActions.Read,
+ ProjectPermissionSecretActions.DescribeSecret,
subject(ProjectPermissionSub.Secrets, { environment, secretPath })
);
@@ -2256,7 +2580,7 @@ export const secretV2BridgeServiceFactory = ({
});
ForbiddenError.from(permission).throwUnlessCan(
- ProjectPermissionActions.Read,
+ ProjectPermissionSecretActions.DescribeSecret,
subject(ProjectPermissionSub.Secrets, {
environment,
secretPath,
@@ -2265,7 +2589,7 @@ export const secretV2BridgeServiceFactory = ({
})
);
- const secretValue = secret.encryptedValue
+ const decryptedSecretValue = secret.encryptedValue
? secretManagerDecryptor({ cipherTextBlob: secret.encryptedValue }).toString()
: "";
@@ -2276,7 +2600,7 @@ export const secretV2BridgeServiceFactory = ({
decryptSecretValue: (value) => (value ? secretManagerDecryptor({ cipherTextBlob: value }).toString() : undefined),
canExpandValue: (expandEnvironment, expandSecretPath, expandSecretName, expandSecretTags) =>
permission.can(
- ProjectPermissionActions.Read,
+ ProjectPermissionSecretActions.ReadValue,
subject(ProjectPermissionSub.Secrets, {
environment: expandEnvironment,
secretPath: expandSecretPath,
@@ -2286,10 +2610,26 @@ export const secretV2BridgeServiceFactory = ({
)
});
+ if (
+ !permission.can(
+ ProjectPermissionSecretActions.ReadValue,
+ subject(ProjectPermissionSub.Secrets, {
+ environment,
+ secretPath,
+ secretName,
+ secretTags: (secret?.tags || []).map((el) => el.slug)
+ })
+ )
+ ) {
+ throw new ForbiddenRequestError({
+ message: `Unable to get secret reference tree for secret with key '${secretName}', because you don't have permission to view secret value.`
+ });
+ }
+
const { expandedValue, stackTrace } = await getExpandedSecretStackTrace({
environment,
secretPath,
- value: secretValue
+ value: decryptedSecretValue
});
return { tree: stackTrace, value: expandedValue };
diff --git a/backend/src/services/secret-v2-bridge/secret-v2-bridge-types.ts b/backend/src/services/secret-v2-bridge/secret-v2-bridge-types.ts
index 22956463d..510181d0c 100644
--- a/backend/src/services/secret-v2-bridge/secret-v2-bridge-types.ts
+++ b/backend/src/services/secret-v2-bridge/secret-v2-bridge-types.ts
@@ -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;
export type TCreateSecretDTO = TProjectPermission & {
@@ -164,9 +173,9 @@ export type TFnSecretBulkInsert = {
}
>;
resourceMetadataDAL: Pick;
- secretDAL: Pick;
+ secretDAL: Pick;
secretVersionDAL: Pick;
- secretTagDAL: Pick;
+ secretTagDAL: Pick;
secretVersionTagDAL: Pick;
actor?: {
type: string;
@@ -192,9 +201,9 @@ export type TFnSecretBulkUpdate = {
data: TRequireReferenceIfValue & { tags?: string[]; secretMetadata?: ResourceMetadataDTO };
}[];
resourceMetadataDAL: Pick;
- secretDAL: Pick;
+ secretDAL: Pick;
secretVersionDAL: Pick;
- secretTagDAL: Pick;
+ secretTagDAL: Pick;
secretVersionTagDAL: Pick;
actor?: {
type: string;
@@ -340,4 +349,5 @@ export type TGetSecretsRawByFolderMappingsDTO = {
folderMappings: { folderId: string; path: string; environment: string }[];
userId: string;
filters: TFindSecretsByFolderIdsFilter;
+ filterByAction?: ProjectPermissionSecretActions;
};
diff --git a/backend/src/services/secret-v2-bridge/secret-version-dal.ts b/backend/src/services/secret-v2-bridge/secret-version-dal.ts
index d06aa1472..12c54cd7c 100644
--- a/backend/src/services/secret-v2-bridge/secret-version-dal.ts
+++ b/backend/src/services/secret-v2-bridge/secret-version-dal.ts
@@ -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 {
const secretVersionV2Orm = ormify(db, TableName.SecretVersionV2);
+ const findBySecretId = async (secretId: string, { offset, limit, sort, tx }: TFindOpt = {}) => {
+ 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 {
@@ -181,6 +233,7 @@ export const secretVersionV2BridgeDALFactory = (db: TDbClient) => {
findLatestVersionMany,
bulkUpdate,
findLatestVersionByFolderId,
- findVersionsBySecretIdWithActors
+ findVersionsBySecretIdWithActors,
+ findBySecretId
};
};
diff --git a/backend/src/services/secret/secret-dal.ts b/backend/src/services/secret/secret-dal.ts
index cbaf7ddcd..dc41d129f 100644
--- a/backend/src/services/secret/secret-dal.ts
+++ b/backend/src/services/secret/secret-dal.ts
@@ -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
};
};
diff --git a/backend/src/services/secret/secret-fns.ts b/backend/src/services/secret/secret-fns.ts
index d7c9b86fb..4556528a8 100644
--- a/backend/src/services/secret/secret-fns.ts
+++ b/backend/src/services/secret/secret-fns.ts
@@ -13,7 +13,7 @@ import {
TSecrets
} from "@app/db/schemas";
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service";
-import { ProjectPermissionActions, ProjectPermissionSub } from "@app/ee/services/permission/project-permission";
+import { ProjectPermissionSecretActions, ProjectPermissionSub } 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 = "";
+
export const generateSecretBlindIndexBySalt = async (secretName: string, secretBlindIndexDoc: TSecretBlindIndexes) => {
const appCfg = getConfig();
const secretBlindIndex = await buildSecretBlindIndexFromName({
@@ -190,7 +192,7 @@ export const recursivelyGetSecretPaths = ({
const allowedPaths = paths.filter(
(folder) =>
permission.can(
- ProjectPermissionActions.Read,
+ ProjectPermissionSecretActions.ReadValue,
subject(ProjectPermissionSub.Secrets, {
environment,
secretPath: folder.path
@@ -344,6 +346,7 @@ export const interpolateSecrets = ({ projectId, secretEncKey, secretDAL, folderD
export const decryptSecretRaw = (
secret: TSecrets & {
+ secretValueHidden: boolean;
workspace: string;
environment: string;
secretPath: string;
@@ -362,12 +365,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 +390,7 @@ export const decryptSecretRaw = (
secretPath: secret.secretPath,
workspace: secret.workspace,
environment: secret.environment,
+ secretValueHidden: secret.secretValueHidden,
secretValue,
secretComment,
version: secret.version,
@@ -1198,3 +1204,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
+ };
+};
diff --git a/backend/src/services/secret/secret-queue.ts b/backend/src/services/secret/secret-queue.ts
index c84fe5ae0..76a486b46 100644
--- a/backend/src/services/secret/secret-queue.ts
+++ b/backend/src/services/secret/secret-queue.ts
@@ -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) {
diff --git a/backend/src/services/secret/secret-service.ts b/backend/src/services/secret/secret-service.ts
index cfb47d1dd..8af0deb2b 100644
--- a/backend/src/services/secret/secret-service.ts
+++ b/backend/src/services/secret/secret-service.ts
@@ -6,6 +6,7 @@ import {
ActionProjectType,
ProjectMembershipRole,
ProjectUpgradeStatus,
+ ProjectVersion,
SecretEncryptionAlgo,
SecretKeyEncoding,
SecretsSchema,
@@ -13,7 +14,11 @@ import {
} from "@app/db/schemas";
import { TLicenseServiceFactory } from "@app/ee/services/license/license-service";
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service";
-import { ProjectPermissionActions, ProjectPermissionSub } from "@app/ee/services/permission/project-permission";
+import {
+ 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 +53,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,
@@ -205,7 +211,7 @@ export const secretServiceFactory = ({
});
ForbiddenError.from(permission).throwUnlessCan(
- ProjectPermissionActions.Create,
+ ProjectPermissionSecretActions.Create,
subject(ProjectPermissionSub.Secrets, { environment, secretPath: path })
);
@@ -323,7 +329,7 @@ export const secretServiceFactory = ({
});
ForbiddenError.from(permission).throwUnlessCan(
- ProjectPermissionActions.Edit,
+ ProjectPermissionSecretActions.Edit,
subject(ProjectPermissionSub.Secrets, { environment, secretPath: path })
);
@@ -445,7 +451,22 @@ export const secretServiceFactory = ({
environmentSlug: folder.environment.slug
});
}
- return { ...updatedSecret[0], workspace: projectId, environment, secretPath: path };
+
+ const secretValueHidden = !permission.can(
+ ProjectPermissionSecretActions.ReadValue,
+ subject(ProjectPermissionSub.Secrets, {
+ environment,
+ secretPath: path
+ })
+ );
+
+ return {
+ ...updatedSecret[0],
+ ...conditionallyHideSecretValue(secretValueHidden, updatedSecret[0]),
+ workspace: projectId,
+ environment,
+ secretPath: path
+ };
};
const deleteSecret = async ({
@@ -468,7 +489,7 @@ export const secretServiceFactory = ({
});
ForbiddenError.from(permission).throwUnlessCan(
- ProjectPermissionActions.Delete,
+ ProjectPermissionSecretActions.Delete,
subject(ProjectPermissionSub.Secrets, { environment, secretPath: path })
);
@@ -541,7 +562,19 @@ export const secretServiceFactory = ({
});
}
- return { ...deletedSecret[0], _id: deletedSecret[0].id, workspace: projectId, environment, secretPath: path };
+ const secretValueHidden = !permission.can(
+ ProjectPermissionSecretActions.ReadValue,
+ subject(ProjectPermissionSub.Secrets, { environment, secretPath: path })
+ );
+
+ return {
+ ...deletedSecret[0],
+ ...conditionallyHideSecretValue(secretValueHidden, deletedSecret[0]),
+ _id: deletedSecret[0].id,
+ workspace: projectId,
+ environment,
+ secretPath: path
+ };
};
const getSecrets = async ({
@@ -590,7 +623,7 @@ export const secretServiceFactory = ({
paths = deepPaths.map(({ folderId, path: p }) => ({ folderId, path: p }));
} else {
ForbiddenError.from(permission).throwUnlessCan(
- ProjectPermissionActions.Read,
+ ProjectPermissionSecretActions.ReadValue,
subject(ProjectPermissionSub.Secrets, { environment, secretPath: path })
);
@@ -615,7 +648,7 @@ export const secretServiceFactory = ({
actor === ActorType.SERVICE
? true
: permission.can(
- ProjectPermissionActions.Read,
+ ProjectPermissionSecretActions.ReadValue,
subject(ProjectPermissionSub.Secrets, {
environment: importEnv.slug,
secretPath: importPath
@@ -672,7 +705,7 @@ export const secretServiceFactory = ({
actionProjectType: ActionProjectType.SecretManager
});
ForbiddenError.from(permission).throwUnlessCan(
- ProjectPermissionActions.Read,
+ ProjectPermissionSecretActions.ReadValue,
subject(ProjectPermissionSub.Secrets, { environment, secretPath: path })
);
const folder = await folderDAL.findBySecretPath(projectId, environment, path);
@@ -722,7 +755,7 @@ export const secretServiceFactory = ({
actor === ActorType.SERVICE
? true
: permission.can(
- ProjectPermissionActions.Read,
+ ProjectPermissionSecretActions.ReadValue,
subject(ProjectPermissionSub.Secrets, {
environment: importEnv.slug,
secretPath: importPath
@@ -740,6 +773,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 +784,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 +812,7 @@ export const secretServiceFactory = ({
actionProjectType: ActionProjectType.SecretManager
});
ForbiddenError.from(permission).throwUnlessCan(
- ProjectPermissionActions.Create,
+ ProjectPermissionSecretActions.Create,
subject(ProjectPermissionSub.Secrets, { environment, secretPath: path })
);
@@ -860,7 +900,7 @@ export const secretServiceFactory = ({
});
ForbiddenError.from(permission).throwUnlessCan(
- ProjectPermissionActions.Edit,
+ ProjectPermissionSecretActions.Edit,
subject(ProjectPermissionSub.Secrets, { environment, secretPath: path })
);
@@ -902,8 +942,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 +973,18 @@ export const secretServiceFactory = ({
secretVersionDAL,
secretTagDAL,
secretVersionTagDAL
- })
- );
+ });
+
+ const secretValueHidden = !permission.can(
+ ProjectPermissionSecretActions.ReadValue,
+ subject(ProjectPermissionSub.Secrets, { environment, secretPath: path })
+ );
+
+ return updatedSecrets.map((secret) => ({
+ ...secret,
+ ...conditionallyHideSecretValue(secretValueHidden, secret)
+ }));
+ });
await snapshotService.performSnapshot(folderId);
await secretQueueService.syncSecrets({
@@ -968,7 +1018,7 @@ export const secretServiceFactory = ({
actionProjectType: ActionProjectType.SecretManager
});
ForbiddenError.from(permission).throwUnlessCan(
- ProjectPermissionActions.Delete,
+ ProjectPermissionSecretActions.Delete,
subject(ProjectPermissionSub.Secrets, { environment, secretPath: path })
);
@@ -1020,7 +1070,15 @@ export const secretServiceFactory = ({
}
}
- return secrets;
+ const secretValueHidden = !permission.can(
+ ProjectPermissionSecretActions.ReadValue,
+ subject(ProjectPermissionSub.Secrets, { environment, secretPath: path })
+ );
+
+ return secrets.map((secret) => ({
+ ...secret,
+ ...conditionallyHideSecretValue(secretValueHidden, secret)
+ }));
});
await snapshotService.performSnapshot(folderId);
@@ -1181,6 +1239,7 @@ export const secretServiceFactory = ({
secretName,
path: secretPath,
environment,
+ viewSecretValue: false,
type: "shared"
});
@@ -1195,10 +1254,11 @@ export const secretServiceFactory = ({
| (typeof groupPermissions)[number]
) => {
const allowedActions = [
- ProjectPermissionActions.Read,
- ProjectPermissionActions.Delete,
- ProjectPermissionActions.Create,
- ProjectPermissionActions.Edit
+ ProjectPermissionSecretActions.DescribeSecret,
+ ProjectPermissionSecretActions.ReadValue,
+ ProjectPermissionSecretActions.Delete,
+ ProjectPermissionSecretActions.Create,
+ ProjectPermissionSecretActions.Edit
].filter((action) =>
entityPermission.permission.can(
action,
@@ -1235,11 +1295,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 +1312,8 @@ export const secretServiceFactory = ({
actorId,
actor,
actorOrgId,
+ viewSecretValue,
+ throwOnMissingReadValuePermission,
environment,
path,
recursive,
@@ -1258,6 +1322,7 @@ export const secretServiceFactory = ({
tagSlugs,
...paramsV2
});
+
return { secrets, imports };
}
@@ -1286,14 +1351,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 +1375,7 @@ export const secretServiceFactory = ({
const importedEntries = decryptedImportSecrets.reduce(
(
accum: {
+ secretValueHidden: boolean;
secretKey: string;
secretPath: string;
workspace: string;
@@ -1347,6 +1419,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 +1436,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 +1474,7 @@ export const secretServiceFactory = ({
path,
actor,
environment,
+ viewSecretValue,
projectId: workspaceId,
expandSecretReferences,
projectSlug,
@@ -1419,6 +1494,7 @@ export const secretServiceFactory = ({
includeImports,
actorAuthMethod,
path,
+ viewSecretValue,
actorOrgId,
actor,
actorId,
@@ -1449,6 +1525,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 +1544,10 @@ export const secretServiceFactory = ({
decryptedSecret.secretValue = expandedSecretValue || "";
}
- return { secretMetadata: undefined, ...decryptedSecret };
+ return {
+ secretMetadata: undefined,
+ ...decryptedSecret
+ };
};
const createSecretRaw = async ({
@@ -1618,7 +1698,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 +2103,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 +2392,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 +2411,42 @@ 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 = permission.cannot(
+ ProjectPermissionSecretActions.ReadValue,
+ subject(ProjectPermissionSub.Secrets, {
+ 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 +2472,7 @@ export const secretServiceFactory = ({
});
ForbiddenError.from(permission).throwUnlessCan(
- ProjectPermissionActions.Edit,
+ ProjectPermissionSecretActions.Edit,
subject(ProjectPermissionSub.Secrets, { environment, secretPath })
);
@@ -2459,7 +2578,7 @@ export const secretServiceFactory = ({
});
ForbiddenError.from(permission).throwUnlessCan(
- ProjectPermissionActions.Edit,
+ ProjectPermissionSecretActions.Edit,
subject(ProjectPermissionSub.Secrets, { environment, secretPath })
);
@@ -2625,7 +2744,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 +2769,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 +2796,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 +2807,52 @@ 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) {
+ 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;
diff --git a/backend/src/services/secret/secret-types.ts b/backend/src/services/secret/secret-types.ts
index 46aedc2ee..c7b8a067d 100644
--- a/backend/src/services/secret/secret-types.ts
+++ b/backend/src/services/secret/secret-types.ts
@@ -184,6 +184,8 @@ export type TGetSecretsRawDTO = {
expandSecretReferences?: boolean;
path: string;
environment: string;
+ viewSecretValue: boolean;
+ throwOnMissingReadValuePermission?: boolean;
includeImports?: boolean;
recursive?: boolean;
tagSlugs?: string[];
@@ -209,6 +211,7 @@ export type TGetASecretRawDTO = {
secretName: string;
path: string;
environment: string;
+ viewSecretValue: boolean;
expandSecretReferences?: boolean;
type: "shared" | "personal";
includeImports?: boolean;
@@ -417,7 +420,7 @@ export type TCreateManySecretsRawFnFactory = {
kmsService: Pick;
secretV2BridgeDAL: Pick<
TSecretV2BridgeDALFactory,
- "insertMany" | "upsertSecretReferences" | "findBySecretKeys" | "bulkUpdate" | "deleteMany"
+ "insertMany" | "upsertSecretReferences" | "findBySecretKeys" | "bulkUpdate" | "deleteMany" | "find"
>;
secretVersionV2BridgeDAL: Pick;
secretVersionTagV2BridgeDAL: Pick;
@@ -454,7 +457,7 @@ export type TUpdateManySecretsRawFnFactory = {
kmsService: Pick;
secretV2BridgeDAL: Pick<
TSecretV2BridgeDALFactory,
- "insertMany" | "upsertSecretReferences" | "findBySecretKeys" | "bulkUpdate" | "deleteMany"
+ "insertMany" | "upsertSecretReferences" | "findBySecretKeys" | "bulkUpdate" | "deleteMany" | "find"
>;
secretVersionV2BridgeDAL: Pick;
secretVersionTagV2BridgeDAL: Pick;
diff --git a/backend/src/services/secret/secret-version-dal.ts b/backend/src/services/secret/secret-version-dal.ts
index 8e77858a5..8e4544c19 100644
--- a/backend/src/services/secret/secret-version-dal.ts
+++ b/backend/src/services/secret/secret-version-dal.ts
@@ -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 {
const secretVersionOrm = ormify(db, TableName.SecretVersion);
+ const findBySecretId = async (secretId: string, { offset, limit, sort, tx }: TFindOpt = {}) => {
+ 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
};
};
diff --git a/backend/src/services/service-token/service-token-service.ts b/backend/src/services/service-token/service-token-service.ts
index 654917feb..9b87c29f8 100644
--- a/backend/src/services/service-token/service-token-service.ts
+++ b/backend/src/services/service-token/service-token-service.ts
@@ -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 })
);
});
diff --git a/backend/src/services/service-token/service-token-types.ts b/backend/src/services/service-token/service-token-types.ts
index f1a700908..d06d90a9d 100644
--- a/backend/src/services/service-token/service-token-types.ts
+++ b/backend/src/services/service-token/service-token-types.ts
@@ -7,7 +7,7 @@ export type TCreateServiceTokenDTO = {
iv: string;
tag: string;
expiresIn?: number | null;
- permissions: ("read" | "write")[];
+ permissions: ("read" | "write" | "readValue")[];
} & TProjectPermission;
export type TGetServiceTokenInfoDTO = Omit;
diff --git a/frontend/src/components/secrets/SecretReferenceDetails/SecretReferenceDetails.tsx b/frontend/src/components/secrets/SecretReferenceDetails/SecretReferenceDetails.tsx
index 8164353d8..b8724f5dc 100644
--- a/frontend/src/components/secrets/SecretReferenceDetails/SecretReferenceDetails.tsx
+++ b/frontend/src/components/secrets/SecretReferenceDetails/SecretReferenceDetails.tsx
@@ -1,13 +1,21 @@
-import { useState } from "react";
-import { faChevronRight, faEye, faEyeSlash } from "@fortawesome/free-solid-svg-icons";
+/* eslint-disable no-nested-ternary */
+import { useEffect, useState } from "react";
+import {
+ faChevronRight,
+ faExclamationTriangle,
+ faEye,
+ faEyeSlash
+} from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import * as Collapsible from "@radix-ui/react-collapsible";
+import { AxiosError } from "axios";
import { twMerge } from "tailwind-merge";
+import { createNotification } from "@app/components/notifications";
import { FormControl, FormLabel, SecretInput, Spinner, Tooltip } from "@app/components/v2";
import { useWorkspace } from "@app/context";
import { useGetSecretReferenceTree } from "@app/hooks/api";
-import { TSecretReferenceTraceNode } from "@app/hooks/api/types";
+import { ApiErrorTypes, TApiErrors, TSecretReferenceTraceNode } from "@app/hooks/api/types";
import style from "./SecretReferenceDetails.module.css";
@@ -84,7 +92,7 @@ export const SecretReferenceTree = ({ secretPath, environment, secretKey }: Prop
const { currentWorkspace } = useWorkspace();
const projectId = currentWorkspace?.id || "";
- const { data, isPending } = useGetSecretReferenceTree({
+ const { data, isPending, isError, error } = useGetSecretReferenceTree({
secretPath,
environmentSlug: environment,
projectId,
@@ -94,6 +102,26 @@ export const SecretReferenceTree = ({ secretPath, environment, secretKey }: Prop
const tree = data?.tree;
const secretValue = data?.value;
+ useEffect(() => {
+ if (error instanceof AxiosError) {
+ const err = error?.response?.data as TApiErrors;
+
+ if (err?.error === ApiErrorTypes.CustomForbiddenError) {
+ createNotification({
+ title: "You don't have permission to view reference tree",
+ text: "You don't have permission to view one or more of the referenced secrets.",
+ type: "error"
+ });
+ return;
+ }
+ createNotification({
+ title: "Error fetching secret reference tree",
+ text: "Please try again later.",
+ type: "error"
+ });
+ }
+ }, [error]);
+
if (isPending) {
return (
@@ -114,11 +142,16 @@ export const SecretReferenceTree = ({ secretPath, environment, secretKey }: Prop
- {tree && (
+ {isError ? (
+
+
+
Error fetching secret reference tree
+
+ ) : tree ? (
- )}
+ ) : null}
Click a secret key to view its sub-references.
diff --git a/frontend/src/components/v2/Blur/Blur.tsx b/frontend/src/components/v2/Blur/Blur.tsx
new file mode 100644
index 000000000..bd1ded40a
--- /dev/null
+++ b/frontend/src/components/v2/Blur/Blur.tsx
@@ -0,0 +1,22 @@
+import { twMerge } from "tailwind-merge";
+
+import { Tooltip } from "../Tooltip/Tooltip";
+
+interface IProps {
+ className?: string;
+ tooltipText?: string;
+}
+
+export const Blur = ({ className, tooltipText }: IProps) => {
+ return (
+
+
+ ********
+
+
+ );
+};
diff --git a/frontend/src/components/v2/Blur/index.tsx b/frontend/src/components/v2/Blur/index.tsx
new file mode 100644
index 000000000..50fb7ccc6
--- /dev/null
+++ b/frontend/src/components/v2/Blur/index.tsx
@@ -0,0 +1 @@
+export { Blur } from "./Blur";
diff --git a/frontend/src/components/v2/InfisicalSecretInput/InfisicalSecretInput.tsx b/frontend/src/components/v2/InfisicalSecretInput/InfisicalSecretInput.tsx
index 6fbe96238..dd6d3575e 100644
--- a/frontend/src/components/v2/InfisicalSecretInput/InfisicalSecretInput.tsx
+++ b/frontend/src/components/v2/InfisicalSecretInput/InfisicalSecretInput.tsx
@@ -120,6 +120,7 @@ export const InfisicalSecretInput = forwardRef
(
const isPopupOpen = Boolean(suggestionSource.isOpen) && isFocused;
const { data: secrets } = useGetProjectSecrets({
+ viewSecretValue: false,
environment: suggestionSource.environment || "",
secretPath: suggestionSource.secretPath || "",
workspaceId,
diff --git a/frontend/src/context/ProjectPermissionContext/types.ts b/frontend/src/context/ProjectPermissionContext/types.ts
index d368a949f..260ffce09 100644
--- a/frontend/src/context/ProjectPermissionContext/types.ts
+++ b/frontend/src/context/ProjectPermissionContext/types.ts
@@ -7,6 +7,14 @@ export enum ProjectPermissionActions {
Delete = "delete"
}
+export enum ProjectPermissionSecretActions {
+ DescribeSecret = "read",
+ ReadValue = "readValue",
+ Create = "create",
+ Edit = "edit",
+ Delete = "delete"
+}
+
export enum ProjectPermissionDynamicSecretActions {
ReadRootCredential = "read-root-credential",
CreateRootCredential = "create-root-credential",
@@ -138,7 +146,7 @@ export type SecretImportSubjectFields = {
export type ProjectPermissionSet =
| [
- ProjectPermissionActions,
+ ProjectPermissionSecretActions,
(
| ProjectPermissionSub.Secrets
| (ForcedSubject & SecretSubjectFields)
diff --git a/frontend/src/hooks/api/dashboard/queries.tsx b/frontend/src/hooks/api/dashboard/queries.tsx
index 4b63be988..f704081a9 100644
--- a/frontend/src/hooks/api/dashboard/queries.tsx
+++ b/frontend/src/hooks/api/dashboard/queries.tsx
@@ -207,6 +207,7 @@ export const useGetProjectSecretsDetails = (
search = "",
includeSecrets,
includeFolders,
+ viewSecretValue,
includeImports,
includeDynamicSecrets,
tags
@@ -231,6 +232,7 @@ export const useGetProjectSecretsDetails = (
limit,
orderBy,
orderDirection,
+ viewSecretValue,
offset,
projectId,
environment,
@@ -247,6 +249,7 @@ export const useGetProjectSecretsDetails = (
limit,
orderBy,
orderDirection,
+ viewSecretValue,
offset,
projectId,
environment,
diff --git a/frontend/src/hooks/api/dashboard/types.ts b/frontend/src/hooks/api/dashboard/types.ts
index 786b2b43a..4808b463c 100644
--- a/frontend/src/hooks/api/dashboard/types.ts
+++ b/frontend/src/hooks/api/dashboard/types.ts
@@ -69,6 +69,7 @@ export type TGetDashboardProjectSecretsDetailsDTO = Omit<
TGetDashboardProjectSecretsOverviewDTO,
"environments"
> & {
+ viewSecretValue: boolean;
environment: string;
includeImports?: boolean;
tags: Record;
diff --git a/frontend/src/hooks/api/secretApprovalRequest/queries.tsx b/frontend/src/hooks/api/secretApprovalRequest/queries.tsx
index 74acf744b..39fa1058a 100644
--- a/frontend/src/hooks/api/secretApprovalRequest/queries.tsx
+++ b/frontend/src/hooks/api/secretApprovalRequest/queries.tsx
@@ -79,6 +79,7 @@ export const decryptSecrets = (
id: encSecret.id,
env: encSecret.environment,
key: secretKey,
+ secretValueHidden: encSecret.secretValueHidden,
value: secretValue,
tags: encSecret.tags,
comment: secretComment,
diff --git a/frontend/src/hooks/api/secretImports/queries.tsx b/frontend/src/hooks/api/secretImports/queries.tsx
index c2ddd2fbe..dfc8c6497 100644
--- a/frontend/src/hooks/api/secretImports/queries.tsx
+++ b/frontend/src/hooks/api/secretImports/queries.tsx
@@ -137,6 +137,7 @@ export const useGetImportedSecretsSingleEnv = ({
env: encSecret.environment,
key: encSecret.secretKey,
value: encSecret.secretValue,
+ secretValueHidden: encSecret.secretValueHidden,
tags: encSecret.tags,
comment: encSecret.secretComment,
createdAt: encSecret.createdAt,
@@ -176,6 +177,7 @@ export const useGetImportedSecretsAllEnvs = ({
env: encSecret.environment,
key: encSecret.secretKey,
value: encSecret.secretValue,
+ secretValueHidden: encSecret.secretValueHidden,
tags: encSecret.tags,
comment: encSecret.secretComment,
createdAt: encSecret.createdAt,
diff --git a/frontend/src/hooks/api/secretSnapshots/queries.tsx b/frontend/src/hooks/api/secretSnapshots/queries.tsx
index 8a16b4180..d273bd5ea 100644
--- a/frontend/src/hooks/api/secretSnapshots/queries.tsx
+++ b/frontend/src/hooks/api/secretSnapshots/queries.tsx
@@ -75,6 +75,7 @@ export const useGetSnapshotSecrets = ({ snapshotId }: TSnapshotDataProps) =>
id: secretVersion.secretId,
env: data.environment.slug,
key: secretVersion.secretKey,
+ secretValueHidden: secretVersion.secretValueHidden,
value: secretVersion.secretValue || "",
tags: secretVersion.tags,
comment: secretVersion.secretComment,
diff --git a/frontend/src/hooks/api/secrets/queries.tsx b/frontend/src/hooks/api/secrets/queries.tsx
index a6dd959b0..3e82c3b90 100644
--- a/frontend/src/hooks/api/secrets/queries.tsx
+++ b/frontend/src/hooks/api/secrets/queries.tsx
@@ -26,8 +26,13 @@ import {
export const secretKeys = {
// this is also used in secretSnapshot part
- getProjectSecret: ({ workspaceId, environment, secretPath }: TGetProjectSecretsKey) =>
- [{ workspaceId, environment, secretPath }, "secrets"] as const,
+ getProjectSecret: ({
+ workspaceId,
+ environment,
+ secretPath,
+ viewSecretValue
+ }: TGetProjectSecretsKey) =>
+ [{ workspaceId, environment, secretPath, viewSecretValue }, "secrets"] as const,
getSecretVersion: (secretId: string) => [{ secretId }, "secret-versions"] as const,
getSecretAccessList: ({
workspaceId,
@@ -44,13 +49,15 @@ export const fetchProjectSecrets = async ({
environment,
secretPath,
includeImports,
- expandSecretReferences
+ expandSecretReferences,
+ viewSecretValue
}: TGetProjectSecretsKey) => {
const { data } = await apiRequest.get("/api/v3/secrets/raw", {
params: {
environment,
workspaceId,
secretPath,
+ viewSecretValue,
expandSecretReferences,
include_imports: includeImports
}
@@ -68,6 +75,7 @@ export const mergePersonalSecrets = (rawSecrets: SecretV3Raw[]) => {
env: el.environment,
key: el.secretKey,
value: el.secretValue,
+ secretValueHidden: el.secretValueHidden,
tags: el.tags || [],
comment: el.secretComment || "",
reminderRepeatDays: el.secretReminderRepeatDays,
@@ -107,6 +115,7 @@ export const useGetProjectSecrets = ({
workspaceId,
environment,
secretPath,
+ viewSecretValue,
options
}: TGetProjectSecretsDTO & {
options?: Omit<
@@ -123,8 +132,13 @@ export const useGetProjectSecrets = ({
...options,
// wait for all values to be available
enabled: Boolean(workspaceId && environment) && (options?.enabled ?? true),
- queryKey: secretKeys.getProjectSecret({ workspaceId, environment, secretPath }),
- queryFn: () => fetchProjectSecrets({ workspaceId, environment, secretPath }),
+ queryKey: secretKeys.getProjectSecret({
+ workspaceId,
+ environment,
+ secretPath,
+ viewSecretValue
+ }),
+ queryFn: () => fetchProjectSecrets({ workspaceId, environment, secretPath, viewSecretValue }),
select: useCallback(
(data: Awaited>) => mergePersonalSecrets(data.secrets),
[]
diff --git a/frontend/src/hooks/api/secrets/types.ts b/frontend/src/hooks/api/secrets/types.ts
index 9e0a82dda..5c4d4ffc6 100644
--- a/frontend/src/hooks/api/secrets/types.ts
+++ b/frontend/src/hooks/api/secrets/types.ts
@@ -19,6 +19,7 @@ export type EncryptedSecret = {
secretValueCiphertext: string;
secretValueIV: string;
secretValueTag: string;
+ secretValueHidden: boolean;
__v: number;
createdAt: string;
updatedAt: string;
@@ -37,6 +38,7 @@ export type SecretV3RawSanitized = {
version: number;
key: string;
value?: string;
+ secretValueHidden: boolean;
comment?: string;
reminderRepeatDays?: number | null;
reminderNote?: string | null;
@@ -61,6 +63,7 @@ export type SecretV3Raw = {
environment: string;
version: number;
type: string;
+ secretValueHidden: boolean;
secretKey: string;
secretPath: string;
secretValue?: string;
@@ -95,6 +98,7 @@ export type SecretVersions = {
envId: string;
secretKey: string;
secretValue?: string;
+ secretValueHidden: boolean;
secretComment?: string;
tags: WsTag[];
__v: number;
@@ -115,6 +119,7 @@ export type TGetProjectSecretsKey = {
environment: string;
secretPath?: string;
includeImports?: boolean;
+ viewSecretValue?: boolean;
expandSecretReferences?: boolean;
};
diff --git a/frontend/src/hooks/api/types.ts b/frontend/src/hooks/api/types.ts
index cda34ad60..8f2acbbd7 100644
--- a/frontend/src/hooks/api/types.ts
+++ b/frontend/src/hooks/api/types.ts
@@ -47,7 +47,8 @@ export enum ApiErrorTypes {
PermissionBoundaryError = "PermissionBoundaryError",
BadRequestError = "BadRequest",
UnauthorizedError = "UnauthorizedError",
- ForbiddenError = "PermissionDenied"
+ ForbiddenError = "PermissionDenied",
+ CustomForbiddenError = "ForbiddenError"
}
export type TApiErrors =
@@ -70,6 +71,12 @@ export type TApiErrors =
details: PureAbility["rules"];
statusCode: 403;
}
+ | {
+ reqId: string;
+ error: ApiErrorTypes.CustomForbiddenError;
+ message: string;
+ statusCode: 403;
+ }
| {
reqId: string;
statusCode: 400;
diff --git a/frontend/src/pages/project/AccessControlPage/components/ServiceTokenTab/components/ServiceTokenSection/AddServiceTokenModal.tsx b/frontend/src/pages/project/AccessControlPage/components/ServiceTokenTab/components/ServiceTokenSection/AddServiceTokenModal.tsx
index 46a9c5680..a669a8360 100644
--- a/frontend/src/pages/project/AccessControlPage/components/ServiceTokenTab/components/ServiceTokenSection/AddServiceTokenModal.tsx
+++ b/frontend/src/pages/project/AccessControlPage/components/ServiceTokenTab/components/ServiceTokenSection/AddServiceTokenModal.tsx
@@ -58,6 +58,7 @@ const schema = z.object({
permissions: z
.object({
read: z.boolean(),
+ readValue: z.boolean(),
write: z.boolean()
})
.required()
@@ -296,14 +297,19 @@ export const AddServiceTokenModal = ({ popUp, handlePopUpToggle }: Props) => {
name="permissions"
defaultValue={{
read: true,
+ readValue: false,
write: false
}}
render={({ field: { onChange, value }, fieldState: { error } }) => {
const options = [
{
- label: "Read (default)",
+ label: "Describe Secret (default)",
value: "read"
},
+ {
+ label: "Read Value (optional)",
+ value: "readValue"
+ },
{
label: "Write (optional)",
value: "write"
diff --git a/frontend/src/pages/project/RoleDetailsBySlugPage/components/GeneralPermissionPolicies.tsx b/frontend/src/pages/project/RoleDetailsBySlugPage/components/GeneralPermissionPolicies.tsx
index de2231fc9..8aca40a34 100644
--- a/frontend/src/pages/project/RoleDetailsBySlugPage/components/GeneralPermissionPolicies.tsx
+++ b/frontend/src/pages/project/RoleDetailsBySlugPage/components/GeneralPermissionPolicies.tsx
@@ -117,24 +117,27 @@ export const GeneralPermissionPolicies =
{actions.map(({ label, value }) => {
if (typeof value !== "string") return undefined;
+
return (
(
-
-
- {label}
-
-
- )}
+ render={({ field }) => {
+ return (
+
+
+ {label}
+
+
+ );
+ }}
/>
);
})}
diff --git a/frontend/src/pages/project/RoleDetailsBySlugPage/components/ProjectRoleModifySection.utils.tsx b/frontend/src/pages/project/RoleDetailsBySlugPage/components/ProjectRoleModifySection.utils.tsx
index 7dcb118fd..e26ec8bc4 100644
--- a/frontend/src/pages/project/RoleDetailsBySlugPage/components/ProjectRoleModifySection.utils.tsx
+++ b/frontend/src/pages/project/RoleDetailsBySlugPage/components/ProjectRoleModifySection.utils.tsx
@@ -9,6 +9,7 @@ import {
PermissionConditionOperators,
ProjectPermissionDynamicSecretActions,
ProjectPermissionKmipActions,
+ ProjectPermissionSecretActions,
ProjectPermissionSecretSyncActions,
TPermissionCondition,
TPermissionConditionOperators
@@ -22,6 +23,14 @@ const GeneralPolicyActionSchema = z.object({
create: z.boolean().optional()
});
+const SecretPolicyActionSchema = z.object({
+ read: z.boolean().optional(), // describe secret
+ edit: z.boolean().optional(),
+ delete: z.boolean().optional(),
+ create: z.boolean().optional(),
+ readValue: z.boolean().optional()
+});
+
const CmekPolicyActionSchema = z.object({
read: z.boolean().optional(),
edit: z.boolean().optional(),
@@ -114,7 +123,7 @@ export const projectRoleFormSchema = z.object({
.refine((val) => val !== "custom", { message: "Cannot use custom as its a keyword" }),
permissions: z
.object({
- [ProjectPermissionSub.Secrets]: GeneralPolicyActionSchema.extend({
+ [ProjectPermissionSub.Secrets]: SecretPolicyActionSchema.extend({
inverted: z.boolean().optional(),
conditions: ConditionSchema
})
@@ -283,6 +292,28 @@ export const rolePermission2Form = (permissions: TProjectPermission[] = []) => {
});
return;
}
+
+ if (subject === ProjectPermissionSub.Secrets) {
+ const canRead = action.includes(ProjectPermissionSecretActions.DescribeSecret);
+ const canEdit = action.includes(ProjectPermissionSecretActions.Edit);
+ const canDelete = action.includes(ProjectPermissionSecretActions.Delete);
+ const canCreate = action.includes(ProjectPermissionSecretActions.Create);
+ const canReadValue = action.includes(ProjectPermissionSecretActions.ReadValue);
+
+ // from above statement we are sure it won't be undefined
+ formVal[subject]!.push({
+ read: canRead,
+ create: canCreate,
+ edit: canEdit,
+ delete: canDelete,
+ readValue: canReadValue,
+ conditions: conditions ? convertCaslConditionToFormOperator(conditions) : [],
+ inverted
+ });
+
+ return;
+ }
+
// for other subjects
const canRead = action.includes(ProjectPermissionActions.Read);
const canEdit = action.includes(ProjectPermissionActions.Edit);
@@ -483,8 +514,9 @@ export const PROJECT_PERMISSION_OBJECT: TProjectPermissionObject = {
[ProjectPermissionSub.Secrets]: {
title: "Secrets",
actions: [
- { label: "Read", value: "read" },
+ { label: "Describe Secret", value: "read" },
{ label: "Create", value: "create" },
+ { label: "Read Value", value: "readValue" },
{ label: "Modify", value: "edit" },
{ label: "Remove", value: "delete" }
]
diff --git a/frontend/src/pages/secret-manager/OverviewPage/components/CreateSecretForm/CreateSecretForm.tsx b/frontend/src/pages/secret-manager/OverviewPage/components/CreateSecretForm/CreateSecretForm.tsx
index c894d6b14..4847feb69 100644
--- a/frontend/src/pages/secret-manager/OverviewPage/components/CreateSecretForm/CreateSecretForm.tsx
+++ b/frontend/src/pages/secret-manager/OverviewPage/components/CreateSecretForm/CreateSecretForm.tsx
@@ -19,6 +19,7 @@ import {
import { getKeyValue } from "@app/helpers/parseEnvVar";
import { useCreateFolder, useCreateSecretV3, useCreateWsTag, useGetWsTags } from "@app/hooks/api";
import { SecretType } from "@app/hooks/api/types";
+import { ProjectPermissionSecretActions } from "@app/context/ProjectPermissionContext/types";
const typeSchema = z
.object({
@@ -275,7 +276,7 @@ export const CreateSecretForm = ({ secretPath = "/", onClose }: Props) => {
isMulti
options={environments.filter((environment) =>
permission.can(
- ProjectPermissionActions.Create,
+ ProjectPermissionSecretActions.Create,
subject(ProjectPermissionSub.Secrets, {
environment: environment.slug,
secretPath,
diff --git a/frontend/src/pages/secret-manager/OverviewPage/components/SecretOverviewTableRow/SecretEditRow.tsx b/frontend/src/pages/secret-manager/OverviewPage/components/SecretOverviewTableRow/SecretEditRow.tsx
index d3dfd49e7..4887d4fd7 100644
--- a/frontend/src/pages/secret-manager/OverviewPage/components/SecretOverviewTableRow/SecretEditRow.tsx
+++ b/frontend/src/pages/secret-manager/OverviewPage/components/SecretOverviewTableRow/SecretEditRow.tsx
@@ -25,6 +25,7 @@ import {
ModalTrigger,
Tooltip
} from "@app/components/v2";
+import { Blur } from "@app/components/v2/Blur";
import { InfisicalSecretInput } from "@app/components/v2/InfisicalSecretInput";
import { ProjectPermissionActions, ProjectPermissionSub } from "@app/context";
import { useToggle } from "@app/hooks";
@@ -39,6 +40,7 @@ type Props = {
isVisible?: boolean;
isImportedSecret: boolean;
environment: string;
+ secretValueHidden: boolean;
secretPath: string;
onSecretCreate: (env: string, key: string, value: string) => Promise;
onSecretUpdate: (
@@ -58,6 +60,7 @@ export const SecretEditRow = ({
isImportedSecret,
onSecretUpdate,
secretName,
+ secretValueHidden,
onSecretCreate,
onSecretDelete,
environment,
@@ -140,24 +143,29 @@ export const SecretEditRow = ({
/>
- (
-
- )}
- />
+ {secretValueHidden ? (
+
+ ) : (
+ (
+
+ )}
+ />
+ )}
+
- NO ACCESS
+
diff --git a/frontend/src/pages/secret-manager/OverviewPage/components/SecretOverviewTableRow/SecretOverviewTableRow.tsx b/frontend/src/pages/secret-manager/OverviewPage/components/SecretOverviewTableRow/SecretOverviewTableRow.tsx
index b9376f6b4..3a66f6896 100644
--- a/frontend/src/pages/secret-manager/OverviewPage/components/SecretOverviewTableRow/SecretOverviewTableRow.tsx
+++ b/frontend/src/pages/secret-manager/OverviewPage/components/SecretOverviewTableRow/SecretOverviewTableRow.tsx
@@ -221,10 +221,13 @@ export const SecretOverviewTableRow = ({
secretPath={secretPath}
isVisible={isSecretVisible}
secretName={secretKey}
+ secretValueHidden={secret?.secretValueHidden || false}
defaultValue={
- secret?.valueOverride ||
- secret?.value ||
- importedSecret?.secret?.value
+ secret?.secretValueHidden
+ ? ""
+ : secret?.valueOverride ||
+ secret?.value ||
+ importedSecret?.secret?.value
}
secretId={secret?.id}
isOverride={Boolean(secret?.valueOverride)}
diff --git a/frontend/src/pages/secret-manager/OverviewPage/components/SecretOverviewTableRow/SecretRenameRow.tsx b/frontend/src/pages/secret-manager/OverviewPage/components/SecretOverviewTableRow/SecretRenameRow.tsx
index ae1916b86..3b05f10fa 100644
--- a/frontend/src/pages/secret-manager/OverviewPage/components/SecretOverviewTableRow/SecretRenameRow.tsx
+++ b/frontend/src/pages/secret-manager/OverviewPage/components/SecretOverviewTableRow/SecretRenameRow.tsx
@@ -10,12 +10,8 @@ import { z } from "zod";
import { createNotification } from "@app/components/notifications";
import { IconButton, Input, Spinner, Tooltip } from "@app/components/v2";
-import {
- ProjectPermissionActions,
- ProjectPermissionSub,
- useProjectPermission,
- useWorkspace
-} from "@app/context";
+import { ProjectPermissionSub, useProjectPermission, useWorkspace } from "@app/context";
+import { ProjectPermissionSecretActions } from "@app/context/ProjectPermissionContext/types";
import { useToggle } from "@app/hooks";
import { useUpdateSecretV3 } from "@app/hooks/api";
import { SecretType, SecretV3RawSanitized } from "@app/hooks/api/types";
@@ -55,8 +51,8 @@ function SecretRenameRow({ environments, getSecretByKey, secretKey, secretPath }
secretTags: (secretDetails?.tags || []).map((i) => i.slug)
});
const isSecretInEnvReadOnly =
- permission.can(ProjectPermissionActions.Read, secretPermissionSubject) &&
- permission.cannot(ProjectPermissionActions.Edit, secretPermissionSubject);
+ permission.can(ProjectPermissionSecretActions.DescribeSecret, secretPermissionSubject) &&
+ permission.cannot(ProjectPermissionSecretActions.Edit, secretPermissionSubject);
if (isSecretInEnvReadOnly) {
return true;
}
diff --git a/frontend/src/pages/secret-manager/OverviewPage/components/SecretSearchInput/components/QuickSearchSecretItem.tsx b/frontend/src/pages/secret-manager/OverviewPage/components/SecretSearchInput/components/QuickSearchSecretItem.tsx
index e901a4ff7..9943de5b6 100644
--- a/frontend/src/pages/secret-manager/OverviewPage/components/SecretSearchInput/components/QuickSearchSecretItem.tsx
+++ b/frontend/src/pages/secret-manager/OverviewPage/components/SecretSearchInput/components/QuickSearchSecretItem.tsx
@@ -110,21 +110,31 @@ export const QuickSearchSecretItem = ({
)}
{isSingleEnv ? (
- {
- e.stopPropagation();
- const el = envSlugMap.get(groupSecret.env)?.name;
- if (el) {
- handleCopy(groupSecret.value!, el);
- }
- }}
+
-
-
+ {
+ e.stopPropagation();
+ const el = envSlugMap.get(groupSecret.env)?.name;
+ if (el) {
+ handleCopy(groupSecret.value!, el);
+ }
+ }}
+ >
+
+
+
) : (
@@ -158,14 +168,24 @@ export const QuickSearchSecretItem = ({
)}
-
-
-
+
+
+
+
Hover to Reveal...
diff --git a/frontend/src/pages/secret-manager/OverviewPage/components/SelectionPanel/SelectionPanel.tsx b/frontend/src/pages/secret-manager/OverviewPage/components/SelectionPanel/SelectionPanel.tsx
index 4738e411e..4e048b941 100644
--- a/frontend/src/pages/secret-manager/OverviewPage/components/SelectionPanel/SelectionPanel.tsx
+++ b/frontend/src/pages/secret-manager/OverviewPage/components/SelectionPanel/SelectionPanel.tsx
@@ -11,6 +11,7 @@ import {
useProjectPermission,
useWorkspace
} from "@app/context";
+import { ProjectPermissionSecretActions } from "@app/context/ProjectPermissionContext/types";
import { usePopUp } from "@app/hooks";
import { useDeleteFolder, useDeleteSecretBatch } from "@app/hooks/api";
import {
@@ -58,7 +59,7 @@ export const SelectionPanel = ({ secretPath, resetSelectedEntries, selectedEntri
// user should have the ability to delete secrets/folders in at least one of the envs
const shouldShowDelete = userAvailableEnvs.some((env) =>
permission.can(
- ProjectPermissionActions.Delete,
+ ProjectPermissionSecretActions.Delete,
subject(ProjectPermissionSub.Secrets, {
environment: env.slug,
secretPath,
@@ -110,7 +111,7 @@ export const SelectionPanel = ({ secretPath, resetSelectedEntries, selectedEntri
(accum: TDeleteSecretBatchDTO["secrets"], secretRecord) => {
const entry = secretRecord[env.slug];
const canDeleteSecret = permission.can(
- ProjectPermissionActions.Delete,
+ ProjectPermissionSecretActions.Delete,
subject(ProjectPermissionSub.Secrets, {
environment: env.slug,
secretPath,
diff --git a/frontend/src/pages/secret-manager/OverviewPage/components/SelectionPanel/components/MoveSecretsDialog/MoveSecretsDialog.tsx b/frontend/src/pages/secret-manager/OverviewPage/components/SelectionPanel/components/MoveSecretsDialog/MoveSecretsDialog.tsx
index 8a27a24c0..a81fa3e53 100644
--- a/frontend/src/pages/secret-manager/OverviewPage/components/SelectionPanel/components/MoveSecretsDialog/MoveSecretsDialog.tsx
+++ b/frontend/src/pages/secret-manager/OverviewPage/components/SelectionPanel/components/MoveSecretsDialog/MoveSecretsDialog.tsx
@@ -25,7 +25,8 @@ import {
Spinner,
Switch
} from "@app/components/v2";
-import { ProjectPermissionActions, ProjectPermissionSub, useProjectPermission } from "@app/context";
+import { ProjectPermissionSub, useProjectPermission } from "@app/context";
+import { ProjectPermissionSecretActions } from "@app/context/ProjectPermissionContext/types";
import { useDebounce } from "@app/hooks";
import { useMoveSecrets } from "@app/hooks/api";
import { useGetProjectSecretsQuickSearch } from "@app/hooks/api/dashboard";
@@ -95,7 +96,7 @@ const Content = ({
env.slug,
{
missingPermissions: permission.cannot(
- ProjectPermissionActions.Delete,
+ ProjectPermissionSecretActions.Delete,
subject(ProjectPermissionSub.Secrets, {
environment: env.slug,
secretPath: sourceSecretPath,
diff --git a/frontend/src/pages/secret-manager/SecretDashboardPage/SecretDashboardPage.tsx b/frontend/src/pages/secret-manager/SecretDashboardPage/SecretDashboardPage.tsx
index daf875a78..5cbefe9c3 100644
--- a/frontend/src/pages/secret-manager/SecretDashboardPage/SecretDashboardPage.tsx
+++ b/frontend/src/pages/secret-manager/SecretDashboardPage/SecretDashboardPage.tsx
@@ -25,6 +25,7 @@ import {
useProjectPermission,
useWorkspace
} from "@app/context";
+import { ProjectPermissionSecretActions } from "@app/context/ProjectPermissionContext/types";
import { useDebounce, usePagination, usePopUp, useResetPageHelper } from "@app/hooks";
import {
useGetImportedSecretsSingleEnv,
@@ -103,7 +104,16 @@ const Page = () => {
const projectSlug = currentWorkspace?.slug || "";
const secretPath = (routerQueryParams.secretPath as string) || "/";
const canReadSecret = permission.can(
- ProjectPermissionActions.Read,
+ ProjectPermissionSecretActions.DescribeSecret,
+ subject(ProjectPermissionSub.Secrets, {
+ environment,
+ secretPath,
+ secretName: "*",
+ secretTags: ["*"]
+ })
+ );
+ const canReadSecretValue = permission.can(
+ ProjectPermissionSecretActions.ReadValue,
subject(ProjectPermissionSub.Secrets, {
environment,
secretPath,
@@ -176,6 +186,7 @@ const Page = () => {
orderDirection,
includeImports: canReadSecretImports && filter.include.import,
includeFolders: filter.include.folder,
+ viewSecretValue: canReadSecretValue,
includeDynamicSecrets: canReadDynamicSecret && filter.include.dynamic,
includeSecrets: canReadSecret && filter.include.secret,
tags: filter.tags
diff --git a/frontend/src/pages/secret-manager/SecretDashboardPage/components/ActionBar/ActionBar.tsx b/frontend/src/pages/secret-manager/SecretDashboardPage/components/ActionBar/ActionBar.tsx
index 03ff3dd57..d545ba748 100644
--- a/frontend/src/pages/secret-manager/SecretDashboardPage/components/ActionBar/ActionBar.tsx
+++ b/frontend/src/pages/secret-manager/SecretDashboardPage/components/ActionBar/ActionBar.tsx
@@ -21,6 +21,7 @@ import {
faTrash
} from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
+import { AxiosError } from "axios";
import FileSaver from "file-saver";
import { twMerge } from "tailwind-merge";
@@ -54,7 +55,7 @@ import {
import { usePopUp } from "@app/hooks";
import { useCreateFolder, useDeleteSecretBatch, useMoveSecrets } from "@app/hooks/api";
import { fetchProjectSecrets } from "@app/hooks/api/secrets/queries";
-import { SecretType, WsTag } from "@app/hooks/api/types";
+import { ApiErrorTypes, SecretType, TApiErrors, WsTag } from "@app/hooks/api/types";
import { SecretSearchInput } from "@app/pages/secret-manager/OverviewPage/components/SecretSearchInput";
import {
@@ -152,51 +153,71 @@ export const ActionBar = ({
};
const handleSecretDownload = async () => {
- const { secrets: localSecrets, imports: localImportedSecrets } = await fetchProjectSecrets({
- workspaceId,
- expandSecretReferences: true,
- includeImports: true,
- environment,
- secretPath
- });
- const secretsPicked = new Set();
- const secretsToDownload: { key: string; value?: string; comment?: string }[] = [];
- localSecrets.forEach((el) => {
- secretsPicked.add(el.secretKey);
- secretsToDownload.push({
- key: el.secretKey,
- value: el.secretValue,
- comment: el.secretComment
+ try {
+ const { secrets: localSecrets, imports: localImportedSecrets } = await fetchProjectSecrets({
+ workspaceId,
+ expandSecretReferences: true,
+ includeImports: true,
+ environment,
+ secretPath
+ });
+ const secretsPicked = new Set();
+ const secretsToDownload: { key: string; value?: string; comment?: string }[] = [];
+ localSecrets.forEach((el) => {
+ secretsPicked.add(el.secretKey);
+ secretsToDownload.push({
+ key: el.secretKey,
+ value: el.secretValue,
+ comment: el.secretComment
+ });
});
- });
- for (let i = localImportedSecrets.length - 1; i >= 0; i -= 1) {
- for (let j = localImportedSecrets[i].secrets.length - 1; j >= 0; j -= 1) {
- const secret = localImportedSecrets[i].secrets[j];
- if (!secretsPicked.has(secret.secretKey)) {
- secretsToDownload.push({
- key: secret.secretKey,
- value: secret.secretValue,
- comment: secret.secretComment
- });
+ for (let i = localImportedSecrets.length - 1; i >= 0; i -= 1) {
+ for (let j = localImportedSecrets[i].secrets.length - 1; j >= 0; j -= 1) {
+ const secret = localImportedSecrets[i].secrets[j];
+ if (!secretsPicked.has(secret.secretKey)) {
+ secretsToDownload.push({
+ key: secret.secretKey,
+ value: secret.secretValue,
+ comment: secret.secretComment
+ });
+ }
+ secretsPicked.add(secret.secretKey);
}
- secretsPicked.add(secret.secretKey);
}
+
+ const file = secretsToDownload
+ .sort((a, b) => a.key.toLowerCase().localeCompare(b.key.toLowerCase()))
+ .reduce(
+ (prev, { key, comment, value }, index) =>
+ prev +
+ (comment
+ ? `${index === 0 ? "#" : "\n#"} ${comment}\n${key}=${value}\n`
+ : `${key}=${value}\n`),
+ ""
+ );
+
+ const blob = new Blob([file], { type: "text/plain;charset=utf-8" });
+ FileSaver.saveAs(blob, `${environment}.env`);
+ } catch (err) {
+ if (err instanceof AxiosError) {
+ const error = err?.response?.data as TApiErrors;
+
+ if (error?.error === ApiErrorTypes.ForbiddenError && error.message.includes("readValue")) {
+ createNotification({
+ title: "You don't have permission to download secrets",
+ text: "You don't have permission to view one or more of the secrets in the current folder. Please contact your administrator.",
+ type: "error"
+ });
+ return;
+ }
+ }
+ createNotification({
+ title: "Failed to download secrets",
+ text: "Please try again later.",
+ type: "error"
+ });
}
-
- const file = secretsToDownload
- .sort((a, b) => a.key.toLowerCase().localeCompare(b.key.toLowerCase()))
- .reduce(
- (prev, { key, comment, value }, index) =>
- prev +
- (comment
- ? `${index === 0 ? "#" : "\n#"} ${comment}\n${key}=${value}\n`
- : `${key}=${value}\n`),
- ""
- );
-
- const blob = new Blob([file], { type: "text/plain;charset=utf-8" });
- FileSaver.saveAs(blob, `${environment}.env`);
};
const handleSecretBulkDelete = async () => {
diff --git a/frontend/src/pages/secret-manager/SecretDashboardPage/components/SecretListView/SecretDetailSidebar.tsx b/frontend/src/pages/secret-manager/SecretDashboardPage/components/SecretListView/SecretDetailSidebar.tsx
index f557c53b3..d6da791bb 100644
--- a/frontend/src/pages/secret-manager/SecretDashboardPage/components/SecretListView/SecretDetailSidebar.tsx
+++ b/frontend/src/pages/secret-manager/SecretDashboardPage/components/SecretListView/SecretDetailSidebar.tsx
@@ -13,12 +13,14 @@ import {
faShare,
faTag,
faTrash,
- faUser
+ faUser,
+ faTriangleExclamation
} from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { zodResolver } from "@hookform/resolvers/zod";
import { Link, useNavigate } from "@tanstack/react-router";
import { format } from "date-fns";
+import { twMerge } from "tailwind-merge";
import { UpgradePlanModal } from "@app/components/license/UpgradePlanModal";
import { createNotification } from "@app/components/notifications";
@@ -48,6 +50,7 @@ import {
useProjectPermission,
useWorkspace
} from "@app/context";
+import { ProjectPermissionSecretActions } from "@app/context/ProjectPermissionContext/types";
import { usePopUp, useToggle } from "@app/hooks";
import { useGetSecretVersion } from "@app/hooks/api";
import { ActorType } from "@app/hooks/api/auditLogs/enums";
@@ -128,7 +131,7 @@ export const SecretDetailSidebar = ({
const navigate = useNavigate();
const cannotEditSecret = permission.cannot(
- ProjectPermissionActions.Edit,
+ ProjectPermissionSecretActions.Edit,
subject(ProjectPermissionSub.Secrets, {
environment,
secretPath,
@@ -136,16 +139,29 @@ export const SecretDetailSidebar = ({
secretTags: selectTagSlugs
})
);
+
+ const cannotReadSecretValue = permission.cannot(
+ ProjectPermissionSecretActions.ReadValue,
+ subject(ProjectPermissionSub.Secrets, {
+ environment,
+ secretPath,
+ secretName: secretKey,
+ secretTags: selectTagSlugs
+ })
+ );
+
const isReadOnly =
permission.can(
- ProjectPermissionActions.Read,
+ ProjectPermissionSecretActions.DescribeSecret,
subject(ProjectPermissionSub.Secrets, {
environment,
secretPath,
secretName: secretKey,
secretTags: selectTagSlugs
})
- ) && cannotEditSecret;
+ ) &&
+ cannotEditSecret &&
+ cannotReadSecretValue;
const overrideAction = watch("overrideAction");
const isOverridden =
@@ -325,38 +341,63 @@ export const SecretDetailSidebar = ({
key="secret-value"
control={control}
render={({ field }) => (
-
-
-
+
+
+
+
+ The value of this secret is hidden because you do not have the
+ read secret value permission.
+
+
+ ) : undefined
+ }
+ label="Value"
+ >
+
+
+
+ }
+ onClick={() => {
+ const value = secret?.valueOverride ?? secret?.value;
+ if (value) {
+ handleSecretShare(value);
+ }
+ }}
+ >
+ Share
+
+
+
+
+
)}
/>
)}
-
- }
- onClick={() => {
- const value = secret?.valueOverride ?? secret?.value;
- if (value) {
- handleSecretShare(value);
- }
- }}
- >
- Share
-
-
@@ -683,14 +724,17 @@ export const SecretDetailSidebar = ({
Version History
- {secretVersion?.map(({ createdAt, secretValue, version, id, actor }) => (
-
-
-
-
-
- v{version}
+ {secretVersion?.map(
+ ({ createdAt, secretValue, version, id, secretValueHidden, actor }, index) => (
+
+
+
+
+
{format(new Date(createdAt), "Pp")}
{format(new Date(createdAt), "Pp")}
@@ -734,6 +778,8 @@ export const SecretDetailSidebar = ({
type="button"
className="select-none text-left"
onClick={(e) => {
+ if (secretValueHidden) return;
+
navigator.clipboard.writeText(secretValue || "");
const target = e.currentTarget;
target.style.borderBottom = "1px dashed";
@@ -753,6 +799,8 @@ export const SecretDetailSidebar = ({
}, 3000);
}}
onKeyDown={(e) => {
+ if (secretValueHidden) return;
+
if (e.key === "Enter" || e.key === " ") {
navigator.clipboard.writeText(secretValue || "");
const target = e.currentTarget;
@@ -774,7 +822,19 @@ export const SecretDetailSidebar = ({
}
}}
>
- {secretValue}
+
+
+ {secretValueHidden ? "Hidden" : secretValue}
+
+
- {secretValue?.replace(/./g, "*")}
+ {secretValueHidden ? "******" : secretValue?.replace(/./g, "*")}
+
+
+ setValue("value", secretValue)}
+ >
+
+
+
+
-
-
- setValue("value", secretValue)}
- >
-
-
-
-
-
- ))}
+ )
+ )}
diff --git a/frontend/src/pages/secret-manager/SecretDashboardPage/components/SecretListView/SecretItem.tsx b/frontend/src/pages/secret-manager/SecretDashboardPage/components/SecretListView/SecretItem.tsx
index 86905f351..02f7a0aa9 100644
--- a/frontend/src/pages/secret-manager/SecretDashboardPage/components/SecretListView/SecretItem.tsx
+++ b/frontend/src/pages/secret-manager/SecretDashboardPage/components/SecretListView/SecretItem.tsx
@@ -1,3 +1,4 @@
+/* eslint-disable no-nested-ternary */
/* eslint-disable simple-import-sort/imports */
import { ProjectPermissionCan } from "@app/components/permissions";
import {
@@ -44,6 +45,8 @@ import {
SecretReferenceTree
} from "@app/components/secrets/SecretReferenceDetails";
+import { ProjectPermissionSecretActions } from "@app/context/ProjectPermissionContext/types";
+import { Blur } from "@app/components/v2/Blur";
import {
FontAwesomeSpriteName,
formSchema,
@@ -99,8 +102,14 @@ export const SecretItem = memo(
trigger,
formState: { isDirty, isSubmitting, errors }
} = useForm
({
- defaultValues: secret,
- values: secret,
+ defaultValues: {
+ ...secret,
+ value: secret.secretValueHidden ? "" : secret.value
+ },
+ values: {
+ ...secret,
+ value: secret.secretValueHidden ? "" : secret.value
+ },
resolver: zodResolver(formSchema)
});
@@ -123,7 +132,7 @@ export const SecretItem = memo(
const isReadOnly =
permission.can(
- ProjectPermissionActions.Read,
+ ProjectPermissionSecretActions.DescribeSecret,
subject(ProjectPermissionSub.Secrets, {
environment,
secretPath,
@@ -132,7 +141,7 @@ export const SecretItem = memo(
})
) &&
permission.cannot(
- ProjectPermissionActions.Edit,
+ ProjectPermissionSecretActions.Edit,
subject(ProjectPermissionSub.Secrets, {
environment,
secretPath,
@@ -141,6 +150,8 @@ export const SecretItem = memo(
})
);
+ const { secretValueHidden } = secret;
+
const [isSecValueCopied, setIsSecValueCopied] = useToggle(false);
useEffect(() => {
let timer: NodeJS.Timeout;
@@ -272,6 +283,8 @@ export const SecretItem = memo(
/>
)}
/>
+ ) : secretValueHidden ? (
+
) : (
)}
@@ -293,6 +307,7 @@ export const SecretItem = memo(
{
className="w-full px-0 blur-sm placeholder:text-red-500 focus:text-bunker-100 focus:ring-transparent"
/>
-
- ********
-
+
))}
diff --git a/frontend/src/pages/secret-manager/SecretDashboardPage/components/SnapshotView/SecretItem.tsx b/frontend/src/pages/secret-manager/SecretDashboardPage/components/SnapshotView/SecretItem.tsx
index d6680bcef..076cf3b6d 100644
--- a/frontend/src/pages/secret-manager/SecretDashboardPage/components/SnapshotView/SecretItem.tsx
+++ b/frontend/src/pages/secret-manager/SecretDashboardPage/components/SnapshotView/SecretItem.tsx
@@ -20,6 +20,7 @@ import {
Tooltip,
Tr
} from "@app/components/v2";
+import { Blur } from "@app/components/v2/Blur";
import { useToggle } from "@app/hooks";
import { SecretV3RawSanitized } from "@app/hooks/api/secrets/types";
@@ -120,11 +121,25 @@ export const SecretItem = ({ mode, preSecret, postSecret }: Props) => {
Value |
{isModified && (
-
+ {preSecret?.secretValueHidden ? (
+
+ ) : (
+
+ )}
|
)}
-
+ {postSecret?.secretValueHidden ? (
+
+ ) : (
+
+ )}
|
{Boolean(preSecret?.idOverride || postSecret?.idOverride) && (
From d74197aeb4b90423355a25fc6488b1b01f960fdb Mon Sep 17 00:00:00 2001
From: Maidul Islam
Date: Wed, 5 Mar 2025 17:53:07 -0500
Subject: [PATCH 17/71] Revert "use forked pion turn server"
This reverts commit bd66411d754df79fb22a0b333ea5205e90affef4.
---
cli/go.sum | 6 ++++--
1 file changed, 4 insertions(+), 2 deletions(-)
diff --git a/cli/go.sum b/cli/go.sum
index ab46476d7..d209fd1ca 100644
--- a/cli/go.sum
+++ b/cli/go.sum
@@ -367,6 +367,8 @@ github.com/pion/stun/v3 v3.0.0 h1:4h1gwhWLWuZWOJIJR9s2ferRO+W3zA/b6ijOI6mKzUw=
github.com/pion/stun/v3 v3.0.0/go.mod h1:HvCN8txt8mwi4FBvS3EmDghW6aQJ24T+y+1TKjB5jyU=
github.com/pion/transport/v3 v3.0.7 h1:iRbMH05BzSNwhILHoBoAPxoB9xQgOaJk+591KC9P1o0=
github.com/pion/transport/v3 v3.0.7/go.mod h1:YleKiTZ4vqNxVwh77Z0zytYi7rXHl7j6uPLGhhz9rwo=
+github.com/pion/turn/v4 v4.0.0 h1:qxplo3Rxa9Yg1xXDxxH8xaqcyGUtbHYw4QSCvmFWvhM=
+github.com/pion/turn/v4 v4.0.0/go.mod h1:MuPDkm15nYSklKpN8vWJ9W2M0PlyQZqYt1McGuxG7mA=
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
@@ -425,8 +427,8 @@ github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
-github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
-github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
+github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
+github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
github.com/subosito/gotenv v1.2.0 h1:Slr1R9HxAlEKefgq5jn9U+DnETlIUa6HfgEzj0g5d7s=
github.com/subosito/gotenv v1.2.0/go.mod h1:N0PQaV/YGNqwC0u51sEeR/aUtSLEXKX9iv69rRypqCw=
github.com/tidwall/pretty v1.0.0 h1:HsD+QiTn7sK6flMKIvNmpqz1qrpP3Ps6jOKIKMooyg4=
From f1ba64aa66d2e453bd35ecf4dd136fa26d5e5953 Mon Sep 17 00:00:00 2001
From: Daniel Hougaard
Date: Sat, 8 Mar 2025 03:40:51 +0400
Subject: [PATCH 18/71] fix(view-secret-value): backwards compatibility for
`read`
---
...kfill-secret-permissions-with-readvalue.ts | 313 --------------
.../src/ee/routes/v2/project-role-router.ts | 6 +-
.../ee/services/permission/permission-fns.ts | 103 ++++-
.../services/permission/project-permission.ts | 17 +-
.../secret-approval-request-service.ts | 10 +-
.../secret-snapshot-service.ts | 51 +--
.../server/routes/v2/service-token-router.ts | 2 +-
.../integration/integration-service.ts | 25 +-
.../src/services/project/project-service.ts | 6 +-
.../secret-import/secret-import-service.ts | 63 ++-
.../secret-sync/secret-sync-service.ts | 25 +-
.../secret-v2-bridge-service.ts | 409 ++++++++----------
backend/src/services/secret/secret-fns.ts | 15 +-
backend/src/services/secret/secret-service.ts | 134 +++---
.../service-token/service-token-types.ts | 2 +-
.../context/ProjectPermissionContext/types.ts | 3 +-
frontend/src/lib/fn/permission.ts | 36 ++
.../AddServiceTokenModal.tsx | 6 +-
.../components/GeneralPermissionPolicies.tsx | 236 +++++-----
.../ProjectRoleModifySection.utils.tsx | 64 ++-
.../SecretRenameRow.tsx | 8 +-
.../SecretDashboardPage.tsx | 17 +-
.../SecretListView/SecretDetailSidebar.tsx | 23 +-
.../components/SecretListView/SecretItem.tsx | 16 +-
24 files changed, 708 insertions(+), 882 deletions(-)
delete mode 100644 backend/src/db/migrations/20250218020306_backfill-secret-permissions-with-readvalue.ts
create mode 100644 frontend/src/lib/fn/permission.ts
diff --git a/backend/src/db/migrations/20250218020306_backfill-secret-permissions-with-readvalue.ts b/backend/src/db/migrations/20250218020306_backfill-secret-permissions-with-readvalue.ts
deleted file mode 100644
index 9e567cefe..000000000
--- a/backend/src/db/migrations/20250218020306_backfill-secret-permissions-with-readvalue.ts
+++ /dev/null
@@ -1,313 +0,0 @@
-import { MongoAbility, RawRuleOf } from "@casl/ability";
-import { PackRule, packRules, unpackRules } from "@casl/ability/extra";
-import { Knex } from "knex";
-import { z } from "zod";
-
-import { selectAllTableCols } from "@app/lib/knex";
-
-import { TableName } from "../schemas";
-
-enum ProjectPermissionSub {
- Secrets = "secrets"
-}
-
-enum SecretActions {
- Read = "read",
- ReadValue = "readValue"
-}
-
-const UnpackedPermissionSchema = z.object({
- subject: z
- .union([z.string().min(1), z.string().array()])
- .transform((el) => (typeof el !== "string" ? el[0] : el))
- .optional(),
- action: z.union([z.string().min(1), z.string().array()]).transform((el) => (typeof el === "string" ? [el] : el)),
- conditions: z.unknown().optional(),
- inverted: z.boolean().optional()
-});
-
-const $unpackPermissions = (permissions: unknown) =>
- UnpackedPermissionSchema.array().parse(unpackRules((permissions || []) as PackRule>[]));
-
-const $updatePermissionsUp = (permissions: unknown) => {
- const parsedPermissions = $unpackPermissions(permissions);
- let shouldUpdate = false;
-
- for (let i = 0; i < parsedPermissions.length; i += 1) {
- const parsedPermission = parsedPermissions[i];
- const { subject, action } = parsedPermission;
-
- if (subject === ProjectPermissionSub.Secrets) {
- if (action.includes(SecretActions.Read) && !action.includes(SecretActions.ReadValue)) {
- action.push(SecretActions.ReadValue);
- parsedPermissions[i] = { ...parsedPermission, action };
- shouldUpdate = true;
- }
- }
- }
-
- return {
- parsedPermissions,
- shouldUpdate
- };
-};
-
-const $updatePermissionsDown = (permissions: unknown) => {
- const parsedPermissions = $unpackPermissions(permissions);
-
- let shouldUpdate = false;
- for (let i = 0; i < parsedPermissions.length; i += 1) {
- const parsedPermission = parsedPermissions[i];
-
- const { subject, action } = parsedPermission;
-
- if (subject === ProjectPermissionSub.Secrets) {
- const readValueIndex = action.indexOf(SecretActions.ReadValue);
-
- if (action.includes(SecretActions.ReadValue) && readValueIndex !== -1) {
- action.splice(readValueIndex, 1);
- parsedPermissions[i] = { ...parsedPermission, action };
-
- shouldUpdate = true;
- }
- }
- }
-
- const repackedPermissions = packRules(parsedPermissions);
-
- return {
- repackedPermissions,
- shouldUpdate
- };
-};
-
-const CHUNK_SIZE = 1000;
-
-export async function up(knex: Knex): Promise {
- const projectRoles = await knex(TableName.ProjectRoles).select(selectAllTableCols(TableName.ProjectRoles));
- const projectIdentityAdditionalPrivileges = await knex(TableName.IdentityProjectAdditionalPrivilege).select(
- selectAllTableCols(TableName.IdentityProjectAdditionalPrivilege)
- );
- const projectUserAdditionalPrivileges = await knex(TableName.ProjectUserAdditionalPrivilege).select(
- selectAllTableCols(TableName.ProjectUserAdditionalPrivilege)
- );
-
- const serviceTokens = await knex(TableName.ServiceToken).select(selectAllTableCols(TableName.ServiceToken));
-
- const updatedServiceTokens = serviceTokens.reduce((acc, serviceToken) => {
- const { permissions } = serviceToken; // Service tokens are special, and include an array of actions only.
-
- if (permissions.includes(SecretActions.Read) && !permissions.includes(SecretActions.ReadValue)) {
- permissions.push(SecretActions.ReadValue);
- acc.push({
- ...serviceToken,
- permissions
- });
- }
- return acc;
- }, []);
-
- if (updatedServiceTokens.length > 0) {
- for (let i = 0; i < updatedServiceTokens.length; i += CHUNK_SIZE) {
- const chunk = updatedServiceTokens.slice(i, i + CHUNK_SIZE);
-
- // eslint-disable-next-line no-await-in-loop
- await knex(TableName.ServiceToken)
- .whereIn(
- "id",
- chunk.map((t) => t.id)
- )
- .update({
- // @ts-expect-error -- raw query
- permissions: knex.raw(
- `CASE id
- ${chunk.map((t) => `WHEN '${t.id}' THEN ?::text[]`).join(" ")}
- END`,
- chunk.map((t) => t.permissions)
- )
- });
- }
- }
-
- const updatedRoles = projectRoles.reduce((acc, projectRole) => {
- const { shouldUpdate, parsedPermissions } = $updatePermissionsUp(projectRole.permissions);
-
- if (shouldUpdate) {
- acc.push({
- ...projectRole,
- permissions: JSON.stringify(packRules(parsedPermissions))
- });
- }
- return acc;
- }, []);
-
- const updatedIdentityAdditionalPrivileges = projectIdentityAdditionalPrivileges.reduce<
- typeof projectIdentityAdditionalPrivileges
- >((acc, identityAdditionalPrivilege) => {
- const { shouldUpdate, parsedPermissions } = $updatePermissionsUp(identityAdditionalPrivilege.permissions);
-
- if (shouldUpdate) {
- acc.push({
- ...identityAdditionalPrivilege,
- permissions: JSON.stringify(packRules(parsedPermissions))
- });
- }
- return acc;
- }, []);
-
- const updatedUserAdditionalPrivileges = projectUserAdditionalPrivileges.reduce<
- typeof projectUserAdditionalPrivileges
- >((acc, userAdditionalPrivilege) => {
- const { shouldUpdate, parsedPermissions } = $updatePermissionsUp(userAdditionalPrivilege.permissions);
-
- if (shouldUpdate) {
- acc.push({
- ...userAdditionalPrivilege,
- permissions: JSON.stringify(packRules(parsedPermissions))
- });
- }
- return acc;
- }, []);
-
- if (updatedRoles.length > 0) {
- for (let i = 0; i < updatedRoles.length; i += CHUNK_SIZE) {
- const chunk = updatedRoles.slice(i, i + CHUNK_SIZE);
-
- // eslint-disable-next-line no-await-in-loop
- await knex(TableName.ProjectRoles).insert(chunk).onConflict("id").merge(["permissions"]);
- }
- }
-
- if (updatedIdentityAdditionalPrivileges.length > 0) {
- for (let i = 0; i < updatedIdentityAdditionalPrivileges.length; i += CHUNK_SIZE) {
- const chunk = updatedIdentityAdditionalPrivileges.slice(i, i + CHUNK_SIZE);
-
- // eslint-disable-next-line no-await-in-loop
- await knex(TableName.IdentityProjectAdditionalPrivilege).insert(chunk).onConflict("id").merge(["permissions"]);
- }
- }
-
- if (updatedUserAdditionalPrivileges.length > 0) {
- for (let i = 0; i < updatedUserAdditionalPrivileges.length; i += CHUNK_SIZE) {
- const chunk = updatedUserAdditionalPrivileges.slice(i, i + CHUNK_SIZE);
-
- // eslint-disable-next-line no-await-in-loop
- await knex(TableName.ProjectUserAdditionalPrivilege).insert(chunk).onConflict("id").merge(["permissions"]);
- }
- }
-}
-
-export async function down(knex: Knex): Promise {
- const projectRoles = await knex(TableName.ProjectRoles).select(selectAllTableCols(TableName.ProjectRoles));
- const identityAdditionalPrivileges = await knex(TableName.IdentityProjectAdditionalPrivilege).select(
- selectAllTableCols(TableName.IdentityProjectAdditionalPrivilege)
- );
- const userAdditionalPrivileges = await knex(TableName.ProjectUserAdditionalPrivilege).select(
- selectAllTableCols(TableName.ProjectUserAdditionalPrivilege)
- );
- const serviceTokens = await knex(TableName.ServiceToken).select(selectAllTableCols(TableName.ServiceToken));
-
- const updatedServiceTokens = serviceTokens.reduce((acc, serviceToken) => {
- const { permissions } = serviceToken;
-
- if (permissions.includes(SecretActions.ReadValue)) {
- permissions.splice(permissions.indexOf(SecretActions.ReadValue), 1);
- acc.push({
- ...serviceToken,
- permissions
- });
- }
- return acc;
- }, []);
-
- if (updatedServiceTokens.length > 0) {
- for (let i = 0; i < updatedServiceTokens.length; i += CHUNK_SIZE) {
- const chunk = updatedServiceTokens.slice(i, i + CHUNK_SIZE);
-
- // eslint-disable-next-line no-await-in-loop
- await knex(TableName.ServiceToken)
- .whereIn(
- "id",
- chunk.map((t) => t.id)
- )
- .update({
- // @ts-expect-error -- raw query
- permissions: knex.raw(
- `CASE id
- ${chunk.map((t) => `WHEN '${t.id}' THEN ?::text[]`).join(" ")}
- END`,
- chunk.map((t) => t.permissions)
- )
- });
- }
- }
-
- const updatedRoles = projectRoles.reduce((acc, projectRole) => {
- const { shouldUpdate, repackedPermissions } = $updatePermissionsDown(projectRole.permissions);
-
- if (shouldUpdate) {
- acc.push({
- ...projectRole,
- permissions: JSON.stringify(repackedPermissions)
- });
- }
- return acc;
- }, []);
-
- const updatedIdentityAdditionalPrivileges = identityAdditionalPrivileges.reduce(
- (acc, identityAdditionalPrivilege) => {
- const { shouldUpdate, repackedPermissions } = $updatePermissionsDown(identityAdditionalPrivilege.permissions);
-
- if (shouldUpdate) {
- acc.push({
- ...identityAdditionalPrivilege,
- permissions: JSON.stringify(repackedPermissions)
- });
- }
- return acc;
- },
- []
- );
-
- const updatedUserAdditionalPrivileges = userAdditionalPrivileges.reduce(
- (acc, userAdditionalPrivilege) => {
- const { shouldUpdate, repackedPermissions } = $updatePermissionsDown(userAdditionalPrivilege.permissions);
-
- if (shouldUpdate) {
- acc.push({
- ...userAdditionalPrivilege,
- permissions: JSON.stringify(repackedPermissions)
- });
- }
- return acc;
- },
- []
- );
-
- if (updatedRoles.length > 0) {
- for (let i = 0; i < updatedRoles.length; i += CHUNK_SIZE) {
- const chunk = updatedRoles.slice(i, i + CHUNK_SIZE);
-
- // eslint-disable-next-line no-await-in-loop
- await knex(TableName.ProjectRoles).insert(chunk).onConflict("id").merge(["permissions"]);
- }
- }
-
- if (updatedIdentityAdditionalPrivileges.length > 0) {
- for (let i = 0; i < updatedIdentityAdditionalPrivileges.length; i += CHUNK_SIZE) {
- const chunk = updatedIdentityAdditionalPrivileges.slice(i, i + CHUNK_SIZE);
-
- // eslint-disable-next-line no-await-in-loop
- await knex(TableName.IdentityProjectAdditionalPrivilege).insert(chunk).onConflict("id").merge(["permissions"]);
- }
- }
-
- if (updatedUserAdditionalPrivileges.length > 0) {
- for (let i = 0; i < updatedUserAdditionalPrivileges.length; i += CHUNK_SIZE) {
- const chunk = updatedUserAdditionalPrivileges.slice(i, i + CHUNK_SIZE);
-
- // eslint-disable-next-line no-await-in-loop
- await knex(TableName.ProjectUserAdditionalPrivilege).insert(chunk).onConflict("id").merge(["permissions"]);
- }
- }
-}
diff --git a/backend/src/ee/routes/v2/project-role-router.ts b/backend/src/ee/routes/v2/project-role-router.ts
index 2d3b1984d..c3b3c11f2 100644
--- a/backend/src/ee/routes/v2/project-role-router.ts
+++ b/backend/src/ee/routes/v2/project-role-router.ts
@@ -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";
@@ -92,7 +93,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({
diff --git a/backend/src/ee/services/permission/permission-fns.ts b/backend/src/ee/services/permission/permission-fns.ts
index 80a58db0a..97113a562 100644
--- a/backend/src/ee/services/permission/permission-fns.ts
+++ b/backend/src/ee/services/permission/permission-fns.ts
@@ -1,7 +1,108 @@
+/* eslint-disable no-nested-ternary */
+import { ForbiddenError, MongoAbility, 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 CheckForbiddenErrorSecretsSubject(
+ permission: MongoAbility,
+ action: Extract<
+ ProjectPermissionSecretActions,
+ ProjectPermissionSecretActions.ReadValue | ProjectPermissionSecretActions.DescribeSecret
+ >,
+ subjectFields?: SecretSubjectFields
+) {
+ try {
+ if (subjectFields) {
+ ForbiddenError.from(permission).throwUnlessCan(action, subject(ProjectPermissionSub.Secrets, subjectFields));
+ } else {
+ ForbiddenError.from(permission).throwUnlessCan(action, ProjectPermissionSub.Secrets);
+ }
+ } catch {
+ if (subjectFields) {
+ ForbiddenError.from(permission).throwUnlessCan(
+ ProjectPermissionSecretActions.DescribeAndReadValue,
+ subject(ProjectPermissionSub.Secrets, subjectFields)
+ );
+ } else {
+ ForbiddenError.from(permission).throwUnlessCan(
+ ProjectPermissionSecretActions.DescribeAndReadValue,
+ ProjectPermissionSub.Secrets
+ );
+ }
+ }
+}
+
+export function CheckCanSecretsSubject(
+ permission: MongoAbility,
+ 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) {
+ 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);
+
+ if (!hasReadValue && !hasDescribeSecret) return;
+
+ const hasBothDescribeAndReadValue =
+ permission.action.includes(ProjectPermissionSecretActions.DescribeSecret) &&
+ permission.action.includes(ProjectPermissionSecretActions.ReadValue);
+
+ throw new BadRequestError({
+ message: `You have selected Full Read Access, 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 Full Read Access.`
+ });
+ }
+ }
+ }
+}
+
function isAuthMethodSaml(actorAuthMethod: ActorAuthMethod) {
if (!actorAuthMethod) return false;
diff --git a/backend/src/ee/services/permission/project-permission.ts b/backend/src/ee/services/permission/project-permission.ts
index 3f69c481a..6256c9965 100644
--- a/backend/src/ee/services/permission/project-permission.ts
+++ b/backend/src/ee/services/permission/project-permission.ts
@@ -18,7 +18,8 @@ export enum ProjectPermissionActions {
}
export enum ProjectPermissionSecretActions {
- DescribeSecret = "read",
+ DescribeAndReadValue = "read",
+ DescribeSecret = "describeSecret",
ReadValue = "readValue",
Create = "create",
Edit = "edit",
@@ -564,6 +565,7 @@ const buildAdminPermissionRules = () => {
can(
[
+ // not adding DescribeAndReadValue, because it's already covered by DescribeSecret and ReadValue
ProjectPermissionSecretActions.DescribeSecret,
ProjectPermissionSecretActions.ReadValue,
ProjectPermissionSecretActions.Create,
@@ -632,6 +634,7 @@ const buildMemberPermissionRules = () => {
can(
[
+ // not adding DescribeAndReadValue, because it's already covered by DescribeSecret and ReadValue
ProjectPermissionSecretActions.DescribeSecret,
ProjectPermissionSecretActions.ReadValue,
ProjectPermissionSecretActions.Edit,
@@ -808,6 +811,7 @@ export const projectMemberPermissions = buildMemberPermissionRules();
const buildViewerPermissionRules = () => {
const { can, rules } = new AbilityBuilder>(createMongoAbility);
+ // not adding DescribeAndReadValue, because it's already covered by DescribeSecret and ReadValue
can(ProjectPermissionSecretActions.DescribeSecret, ProjectPermissionSub.Secrets);
can(ProjectPermissionSecretActions.ReadValue, ProjectPermissionSub.Secrets);
can(ProjectPermissionActions.Read, ProjectPermissionSub.SecretFolders);
@@ -852,7 +856,6 @@ export const buildServiceTokenProjectPermission = (
) => {
const canWrite = permission.includes("write");
const canRead = permission.includes("read");
- const canReadValue = permission.includes("readValue");
const { can, build } = new AbilityBuilder>(createMongoAbility);
scopes.forEach(({ secretPath, environment }) => {
@@ -876,7 +879,7 @@ export const buildServiceTokenProjectPermission = (
environment
});
}
- if (canRead) {
+ if (canRead && subject !== ProjectPermissionSub.Secrets) {
can(ProjectPermissionActions.Read, subject, {
// @ts-expect-error type
secretPath: { $glob: secretPath },
@@ -884,12 +887,18 @@ export const buildServiceTokenProjectPermission = (
});
}
- if (subject === ProjectPermissionSub.Secrets && canReadValue) {
+ if (subject === ProjectPermissionSub.Secrets && canRead) {
// @ts-expect-error type
can(ProjectPermissionSecretActions.ReadValue, subject as ProjectPermissionSub.Secrets, {
secretPath: { $glob: secretPath },
environment
});
+
+ // @ts-expect-error type
+ can(ProjectPermissionSecretActions.DescribeSecret, subject as ProjectPermissionSub.Secrets, {
+ secretPath: { $glob: secretPath },
+ environment
+ });
}
}
);
diff --git a/backend/src/ee/services/secret-approval-request/secret-approval-request-service.ts b/backend/src/ee/services/secret-approval-request/secret-approval-request-service.ts
index 8efae75e6..9f3ec7111 100644
--- a/backend/src/ee/services/secret-approval-request/secret-approval-request-service.ts
+++ b/backend/src/ee/services/secret-approval-request/secret-approval-request-service.ts
@@ -77,6 +77,7 @@ import {
TSecretApprovalDetailsDTO,
TStatusChangeDTO
} from "./secret-approval-request-types";
+import { CheckForbiddenErrorSecretsSubject } from "../permission/permission-fns";
type TSecretApprovalRequestServiceFactoryDep = {
permissionService: Pick;
@@ -917,10 +918,11 @@ export const secretApprovalRequestServiceFactory = ({
actorOrgId,
actionProjectType: ActionProjectType.SecretManager
});
- ForbiddenError.from(permission).throwUnlessCan(
- ProjectPermissionSecretActions.ReadValue,
- subject(ProjectPermissionSub.Secrets, { environment, secretPath })
- );
+
+ CheckForbiddenErrorSecretsSubject(permission, ProjectPermissionSecretActions.ReadValue, {
+ environment,
+ secretPath
+ });
await projectDAL.checkProjectUpgradeStatus(projectId);
diff --git a/backend/src/ee/services/secret-snapshot/secret-snapshot-service.ts b/backend/src/ee/services/secret-snapshot/secret-snapshot-service.ts
index 397824904..6afc6bca3 100644
--- a/backend/src/ee/services/secret-snapshot/secret-snapshot-service.ts
+++ b/backend/src/ee/services/secret-snapshot/secret-snapshot-service.ts
@@ -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,6 +23,7 @@ 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 { CheckCanSecretsSubject, CheckForbiddenErrorSecretsSubject } from "../permission/permission-fns";
import { TPermissionServiceFactory } from "../permission/permission-service";
import {
ProjectPermissionActions,
@@ -39,7 +41,6 @@ import { TSnapshotFolderDALFactory } from "./snapshot-folder-dal";
import { TSnapshotSecretDALFactory } from "./snapshot-secret-dal";
import { TSnapshotSecretV2DALFactory } from "./snapshot-secret-v2-dal";
import { getFullFolderPath } from "./snapshot-service-fns";
-import { INFISICAL_SECRET_VALUE_HIDDEN_MASK } from "@app/services/secret/secret-fns";
type TSecretSnapshotServiceFactoryDep = {
snapshotDAL: TSnapshotDALFactory;
@@ -102,10 +103,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(
- ProjectPermissionSecretActions.DescribeSecret,
- subject(ProjectPermissionSub.Secrets, { environment, secretPath: path })
- );
+ CheckForbiddenErrorSecretsSubject(permission, ProjectPermissionSecretActions.DescribeSecret, {
+ environment,
+ secretPath: path
+ });
const folder = await folderDAL.findBySecretPath(projectId, environment, path);
if (!folder) {
@@ -139,10 +140,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(
- ProjectPermissionSecretActions.DescribeSecret,
- subject(ProjectPermissionSub.Secrets, { environment, secretPath: path })
- );
+ CheckForbiddenErrorSecretsSubject(permission, ProjectPermissionSecretActions.DescribeSecret, {
+ environment,
+ secretPath: path
+ });
const folder = await folderDAL.findBySecretPath(projectId, environment, path);
if (!folder)
@@ -186,15 +187,12 @@ export const secretSnapshotServiceFactory = ({
snapshotDetails = {
...encryptedSnapshotDetails,
secretVersions: encryptedSnapshotDetails.secretVersions.map((el) => {
- const canReadValue = permission.can(
- ProjectPermissionSecretActions.ReadValue,
- subject(ProjectPermissionSub.Secrets, {
- environment: encryptedSnapshotDetails.environment.slug,
- secretPath: fullFolderPath,
- secretName: el.key,
- secretTags: el.tags.length ? el.tags.map((tag) => tag.slug) : undefined
- })
- );
+ const canReadValue = CheckCanSecretsSubject(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) {
@@ -238,15 +236,12 @@ export const secretSnapshotServiceFactory = ({
key: botKey
});
- const canReadValue = permission.can(
- ProjectPermissionSecretActions.ReadValue,
- subject(ProjectPermissionSub.Secrets, {
- environment: encryptedSnapshotDetails.environment.slug,
- secretPath: fullFolderPath,
- secretName: secretKey,
- secretTags: el.tags.length ? el.tags.map((tag) => tag.slug) : undefined
- })
- );
+ const canReadValue = CheckCanSecretsSubject(permission, ProjectPermissionSecretActions.ReadValue, {
+ environment: encryptedSnapshotDetails.environment.slug,
+ secretPath: fullFolderPath,
+ secretName: secretKey,
+ secretTags: el.tags.length ? el.tags.map((tag) => tag.slug) : undefined
+ });
let secretValue = "";
diff --git a/backend/src/server/routes/v2/service-token-router.ts b/backend/src/server/routes/v2/service-token-router.ts
index 1a84c9123..fb10f17db 100644
--- a/backend/src/server/routes/v2/service-token-router.ts
+++ b/backend/src/server/routes/v2/service-token-router.ts
@@ -94,7 +94,7 @@ export const registerServiceTokenRouter = async (server: FastifyZodProvider) =>
iv: z.string().trim(),
tag: z.string().trim(),
expiresIn: z.number().nullable(),
- permissions: z.enum(["read", "write", "readValue"]).array()
+ permissions: z.enum(["read", "write"]).array()
}),
response: {
200: z.object({
diff --git a/backend/src/services/integration/integration-service.ts b/backend/src/services/integration/integration-service.ts
index f72357964..3f7c55896 100644
--- a/backend/src/services/integration/integration-service.ts
+++ b/backend/src/services/integration/integration-service.ts
@@ -1,6 +1,7 @@
-import { ForbiddenError, subject } from "@casl/ability";
+import { ForbiddenError } from "@casl/ability";
import { ActionProjectType } from "@app/db/schemas";
+import { CheckForbiddenErrorSecretsSubject } from "@app/ee/services/permission/permission-fns";
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service";
import {
ProjectPermissionActions,
@@ -95,13 +96,10 @@ export const integrationServiceFactory = ({
});
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Create, ProjectPermissionSub.Integrations);
- ForbiddenError.from(permission).throwUnlessCan(
- ProjectPermissionSecretActions.ReadValue,
- subject(ProjectPermissionSub.Secrets, {
- environment: sourceEnvironment,
- secretPath
- })
- );
+ CheckForbiddenErrorSecretsSubject(permission, ProjectPermissionSecretActions.ReadValue, {
+ environment: sourceEnvironment,
+ secretPath
+ });
const folder = await folderDAL.findBySecretPath(integrationAuth.projectId, sourceEnvironment, secretPath);
if (!folder) {
@@ -178,13 +176,10 @@ export const integrationServiceFactory = ({
const newSecretPath = secretPath || integration.secretPath;
if (environment || secretPath) {
- ForbiddenError.from(permission).throwUnlessCan(
- ProjectPermissionSecretActions.ReadValue,
- subject(ProjectPermissionSub.Secrets, {
- environment: newEnvironment,
- secretPath: newSecretPath
- })
- );
+ CheckForbiddenErrorSecretsSubject(permission, ProjectPermissionSecretActions.ReadValue, {
+ environment: newEnvironment,
+ secretPath: newSecretPath
+ });
}
const folder = await folderDAL.findBySecretPath(integration.projectId, newEnvironment, newSecretPath);
diff --git a/backend/src/services/project/project-service.ts b/backend/src/services/project/project-service.ts
index 5367a70cd..b21cc99fe 100644
--- a/backend/src/services/project/project-service.ts
+++ b/backend/src/services/project/project-service.ts
@@ -10,6 +10,7 @@ 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 { CheckForbiddenErrorSecretsSubject } from "@app/ee/services/permission/permission-fns";
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service";
import {
ProjectPermissionActions,
@@ -764,10 +765,7 @@ export const projectServiceFactory = ({
actorOrgId,
actionProjectType: ActionProjectType.Any
});
- ForbiddenError.from(permission).throwUnlessCan(
- ProjectPermissionSecretActions.DescribeSecret,
- ProjectPermissionSub.Secrets
- );
+ CheckForbiddenErrorSecretsSubject(permission, ProjectPermissionSecretActions.DescribeSecret);
const project = await projectDAL.findProjectById(projectId);
diff --git a/backend/src/services/secret-import/secret-import-service.ts b/backend/src/services/secret-import/secret-import-service.ts
index 6c412edc0..27ed12c84 100644
--- a/backend/src/services/secret-import/secret-import-service.ts
+++ b/backend/src/services/secret-import/secret-import-service.ts
@@ -4,6 +4,7 @@ import { ForbiddenError, subject } from "@casl/ability";
import { ActionProjectType, TableName } from "@app/db/schemas";
import { TLicenseServiceFactory } from "@app/ee/services/license/license-service";
+import { CheckCanSecretsSubject, CheckForbiddenErrorSecretsSubject } from "@app/ee/services/permission/permission-fns";
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service";
import {
ProjectPermissionActions,
@@ -93,13 +94,11 @@ export const secretImportServiceFactory = ({
);
// check if user has permission to import from target path
- ForbiddenError.from(permission).throwUnlessCan(
- ProjectPermissionSecretActions.DescribeSecret,
- subject(ProjectPermissionSub.Secrets, {
- environment: data.environment,
- secretPath: data.path
- })
- );
+ CheckForbiddenErrorSecretsSubject(permission, ProjectPermissionSecretActions.DescribeSecret, {
+ environment: data.environment,
+ secretPath: data.path
+ });
+
if (isReplication) {
const plan = await licenseService.getPlan(actorOrgId);
if (!plan.secretApproval) {
@@ -405,13 +404,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(
- ProjectPermissionSecretActions.DescribeSecret,
- subject(ProjectPermissionSub.Secrets, {
- environment: secretImportDoc.importEnv.slug,
- secretPath: secretImportDoc.importPath
- })
- );
+ CheckForbiddenErrorSecretsSubject(permission, ProjectPermissionSecretActions.DescribeSecret, {
+ environment: secretImportDoc.importEnv.slug,
+ secretPath: secretImportDoc.importPath
+ });
await projectDAL.checkProjectUpgradeStatus(projectId);
@@ -599,14 +595,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(
- ProjectPermissionSecretActions.ReadValue,
- subject(ProjectPermissionSub.Secrets, {
- environment: el.importEnv.slug,
- secretPath: el.importPath
- })
- )
+ CheckCanSecretsSubject(permission, ProjectPermissionSecretActions.ReadValue, {
+ environment: el.importEnv.slug,
+ secretPath: el.importPath
+ })
);
+
return fnSecretsFromImports({ allowedImports, folderDAL, secretDAL, secretImportDAL });
};
@@ -651,16 +645,14 @@ export const secretImportServiceFactory = ({
secretImportDAL,
decryptor: (value) => (value ? secretManagerDecryptor({ cipherTextBlob: value }).toString() : ""),
hasSecretAccess: (expandEnvironment, expandSecretPath, expandSecretKey, expandSecretTags) =>
- permission.can(
- ProjectPermissionSecretActions.ReadValue,
- subject(ProjectPermissionSub.Secrets, {
- environment: expandEnvironment,
- secretPath: expandSecretPath,
- secretName: expandSecretKey,
- secretTags: expandSecretTags
- })
- )
+ CheckCanSecretsSubject(permission, ProjectPermissionSecretActions.ReadValue, {
+ environment: expandEnvironment,
+ secretPath: expandSecretPath,
+ secretName: expandSecretKey,
+ secretTags: expandSecretTags
+ })
});
+
return importedSecrets;
}
@@ -671,13 +663,10 @@ export const secretImportServiceFactory = ({
});
const allowedImports = secretImports.filter((el) =>
- permission.can(
- ProjectPermissionSecretActions.ReadValue,
- subject(ProjectPermissionSub.Secrets, {
- environment: el.importEnv.slug,
- secretPath: el.importPath
- })
- )
+ CheckCanSecretsSubject(permission, ProjectPermissionSecretActions.ReadValue, {
+ environment: el.importEnv.slug,
+ secretPath: el.importPath
+ })
);
const importedSecrets = await fnSecretsFromImports({
allowedImports,
diff --git a/backend/src/services/secret-sync/secret-sync-service.ts b/backend/src/services/secret-sync/secret-sync-service.ts
index 7b92a463c..d74ea120e 100644
--- a/backend/src/services/secret-sync/secret-sync-service.ts
+++ b/backend/src/services/secret-sync/secret-sync-service.ts
@@ -1,6 +1,7 @@
-import { ForbiddenError, subject } from "@casl/ability";
+import { ForbiddenError } from "@casl/ability";
import { ActionProjectType } from "@app/db/schemas";
+import { CheckForbiddenErrorSecretsSubject } from "@app/ee/services/permission/permission-fns";
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service";
import {
ProjectPermissionSecretActions,
@@ -178,13 +179,10 @@ export const secretSyncServiceFactory = ({
ProjectPermissionSub.SecretSyncs
);
- ForbiddenError.from(projectPermission).throwUnlessCan(
- ProjectPermissionSecretActions.ReadValue,
- subject(ProjectPermissionSub.Secrets, {
- environment,
- secretPath
- })
- );
+ CheckForbiddenErrorSecretsSubject(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(
- ProjectPermissionSecretActions.ReadValue,
- subject(ProjectPermissionSub.Secrets, {
- environment: updatedEnvironment,
- secretPath: updatedSecretPath
- })
- );
+ CheckForbiddenErrorSecretsSubject(permission, ProjectPermissionSecretActions.ReadValue, {
+ environment: updatedEnvironment,
+ secretPath: updatedSecretPath
+ });
const newFolder = await folderDAL.findBySecretPath(secretSync.projectId, updatedEnvironment, updatedSecretPath);
diff --git a/backend/src/services/secret-v2-bridge/secret-v2-bridge-service.ts b/backend/src/services/secret-v2-bridge/secret-v2-bridge-service.ts
index 3b7f04907..8ebc0a582 100644
--- a/backend/src/services/secret-v2-bridge/secret-v2-bridge-service.ts
+++ b/backend/src/services/secret-v2-bridge/secret-v2-bridge-service.ts
@@ -10,6 +10,7 @@ import {
TableName,
TSecretsV2
} from "@app/db/schemas";
+import { CheckCanSecretsSubject, CheckForbiddenErrorSecretsSubject } from "@app/ee/services/permission/permission-fns";
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service";
import {
ProjectPermissionActions,
@@ -540,17 +541,14 @@ export const secretV2BridgeServiceFactory = ({
});
}
- const secretValueHidden = !permission.can(
- ProjectPermissionSecretActions.ReadValue,
- subject(ProjectPermissionSub.Secrets, {
- environment,
- secretPath,
- secretName: inputSecret.secretName,
- ...(tagsToCheck.length && {
- secretTags: tagsToCheck.map((el) => el.slug)
- })
+ const secretValueHidden = !CheckCanSecretsSubject(permission, ProjectPermissionSecretActions.ReadValue, {
+ environment,
+ secretPath,
+ secretName: inputSecret.secretName,
+ ...(tagsToCheck.length && {
+ secretTags: tagsToCheck.map((el) => el.slug)
})
- );
+ });
return reshapeBridgeSecret(
projectId,
@@ -651,15 +649,12 @@ export const secretV2BridgeServiceFactory = ({
projectId
});
- const secretValueHidden = !permission.can(
- ProjectPermissionSecretActions.ReadValue,
- subject(ProjectPermissionSub.Secrets, {
- environment,
- secretPath,
- secretName: secretToDelete.key,
- secretTags: secretToDelete.tags?.map((el) => el.slug)
- })
- );
+ const secretValueHidden = !CheckCanSecretsSubject(permission, ProjectPermissionSecretActions.ReadValue, {
+ environment,
+ secretPath,
+ secretName: secretToDelete.key,
+ secretTags: secretToDelete.tags?.map((el) => el.slug)
+ });
return reshapeBridgeSecret(
projectId,
@@ -702,11 +697,7 @@ export const secretV2BridgeServiceFactory = ({
actorOrgId,
actionProjectType: ActionProjectType.SecretManager
});
-
- ForbiddenError.from(permission).throwUnlessCan(
- ProjectPermissionSecretActions.DescribeSecret,
- ProjectPermissionSub.Secrets
- );
+ CheckForbiddenErrorSecretsSubject(permission, ProjectPermissionSecretActions.DescribeSecret);
}
const folders = await folderDAL.findBySecretPathMultiEnv(projectId, environments, path);
@@ -752,11 +743,7 @@ export const secretV2BridgeServiceFactory = ({
actorOrgId,
actionProjectType: ActionProjectType.SecretManager
});
-
- ForbiddenError.from(permission).throwUnlessCan(
- ProjectPermissionSecretActions.DescribeSecret,
- ProjectPermissionSub.Secrets
- );
+ CheckForbiddenErrorSecretsSubject(permission, ProjectPermissionSecretActions.DescribeSecret);
const folder = await folderDAL.findBySecretPath(projectId, environment, path);
if (!folder) return 0;
@@ -791,8 +778,20 @@ export const secretV2BridgeServiceFactory = ({
});
const decryptedSecrets = secrets
- .filter((el) =>
- projectPermission.can(
+ .filter((el) => {
+ if (
+ filterByAction === ProjectPermissionSecretActions.ReadValue ||
+ filterByAction === ProjectPermissionSecretActions.DescribeSecret
+ ) {
+ return CheckCanSecretsSubject(projectPermission, filterByAction, {
+ environment: groupedFolderMappings[el.folderId][0].environment,
+ secretPath: groupedFolderMappings[el.folderId][0].path,
+ secretName: el.key,
+ secretTags: el.tags.map((i) => i.slug)
+ });
+ }
+
+ return projectPermission.can(
filterByAction,
subject(ProjectPermissionSub.Secrets, {
environment: groupedFolderMappings[el.folderId][0].environment,
@@ -800,19 +799,16 @@ export const secretV2BridgeServiceFactory = ({
secretName: el.key,
secretTags: el.tags.map((i) => i.slug)
})
- )
- )
+ );
+ })
.map((secret) => {
// Note(Daniel): This is only relevant if the filterAction isn't set to ReadValue. This is needed for the frontend.
- const secretValueHidden = !projectPermission.can(
- ProjectPermissionSecretActions.ReadValue,
- subject(ProjectPermissionSub.Secrets, {
- environment: groupedFolderMappings[secret.folderId][0].environment,
- secretPath: groupedFolderMappings[secret.folderId][0].path,
- secretName: secret.key,
- secretTags: secret.tags.map((i) => i.slug)
- })
- );
+ const secretValueHidden = !CheckCanSecretsSubject(projectPermission, ProjectPermissionSecretActions.ReadValue, {
+ environment: groupedFolderMappings[secret.folderId][0].environment,
+ secretPath: groupedFolderMappings[secret.folderId][0].path,
+ secretName: secret.key,
+ secretTags: secret.tags.map((i) => i.slug)
+ });
return reshapeBridgeSecret(
projectId,
@@ -858,10 +854,7 @@ export const secretV2BridgeServiceFactory = ({
actionProjectType: ActionProjectType.SecretManager
});
if (!isInternal) {
- ForbiddenError.from(permission).throwUnlessCan(
- ProjectPermissionSecretActions.DescribeSecret,
- ProjectPermissionSub.Secrets
- );
+ CheckForbiddenErrorSecretsSubject(permission, ProjectPermissionSecretActions.DescribeSecret);
}
const folders = await folderDAL.findBySecretPathMultiEnv(projectId, environments, path);
@@ -913,15 +906,11 @@ export const secretV2BridgeServiceFactory = ({
actorOrgId,
actionProjectType: ActionProjectType.SecretManager
});
-
- ForbiddenError.from(permission).throwUnlessCan(
- ProjectPermissionSecretActions.DescribeSecret,
- subject(ProjectPermissionSub.Secrets, {
- environment,
- secretPath: path,
- secretTags: params.tagSlugs
- })
- );
+ CheckForbiddenErrorSecretsSubject(permission, ProjectPermissionSecretActions.DescribeSecret, {
+ environment,
+ secretPath: path,
+ secretTags: params.tagSlugs
+ });
let paths: { folderId: string; path: string }[] = [];
@@ -960,15 +949,12 @@ export const secretV2BridgeServiceFactory = ({
const decryptedSecrets = secrets
.filter((el) => {
- const canDescribeSecret = permission.can(
- ProjectPermissionSecretActions.DescribeSecret,
- subject(ProjectPermissionSub.Secrets, {
- environment,
- secretPath: groupedPaths[el.folderId][0].path,
- secretName: el.key,
- secretTags: el.tags.map((i) => i.slug)
- })
- );
+ const canDescribeSecret = CheckCanSecretsSubject(permission, ProjectPermissionSecretActions.DescribeSecret, {
+ environment,
+ secretPath: groupedPaths[el.folderId][0].path,
+ secretName: el.key,
+ secretTags: el.tags.map((i) => i.slug)
+ });
if (!canDescribeSecret) {
return false;
@@ -977,14 +963,15 @@ export const secretV2BridgeServiceFactory = ({
if (viewSecretValue) {
// Recursive secret, should be filtered out
if (groupedPaths[el.folderId][0].path !== path) {
- const canReadRecursiveSecretValue = permission.can(
+ const canReadRecursiveSecretValue = CheckCanSecretsSubject(
+ permission,
ProjectPermissionSecretActions.ReadValue,
- subject(ProjectPermissionSub.Secrets, {
+ {
environment,
secretPath: groupedPaths[el.folderId][0].path,
secretName: el.key,
secretTags: el.tags.map((i) => i.slug)
- })
+ }
);
if (!canReadRecursiveSecretValue) {
@@ -993,15 +980,12 @@ export const secretV2BridgeServiceFactory = ({
}
if (throwOnMissingReadValuePermission) {
- ForbiddenError.from(permission).throwUnlessCan(
- ProjectPermissionSecretActions.ReadValue,
- subject(ProjectPermissionSub.Secrets, {
- environment,
- secretPath: groupedPaths[el.folderId][0].path,
- secretName: el.key,
- secretTags: el.tags.map((i) => i.slug)
- })
- );
+ CheckForbiddenErrorSecretsSubject(permission, ProjectPermissionSecretActions.ReadValue, {
+ environment,
+ secretPath: groupedPaths[el.folderId][0].path,
+ secretName: el.key,
+ secretTags: el.tags.map((i) => i.slug)
+ });
}
// Else, we do nothing. Because we don't want to filter out the secret, OR throw an error.
// If the user doesn't have access to read the value, in the below map function, we mask the secret value and return the secret with a hidden value.
@@ -1014,15 +998,12 @@ export const secretV2BridgeServiceFactory = ({
const secretValueHidden =
!viewSecretValue ||
- !permission.can(
- ProjectPermissionSecretActions.ReadValue,
- subject(ProjectPermissionSub.Secrets, {
- environment,
- secretPath: groupedPaths[secret.folderId][0].path,
- secretName: secret.key,
- secretTags: secret.tags.map((i) => i.slug)
- })
- );
+ !CheckCanSecretsSubject(permission, ProjectPermissionSecretActions.ReadValue, {
+ environment,
+ secretPath: groupedPaths[secret.folderId][0].path,
+ secretName: secret.key,
+ secretTags: secret.tags.map((i) => i.slug)
+ });
return reshapeBridgeSecret(
projectId,
@@ -1047,15 +1028,12 @@ export const secretV2BridgeServiceFactory = ({
secretDAL,
decryptSecretValue: (value) => (value ? secretManagerDecryptor({ cipherTextBlob: value }).toString() : undefined),
canExpandValue: (expandEnvironment, expandSecretPath, expandSecretKey, expandSecretTags) =>
- permission.can(
- ProjectPermissionSecretActions.ReadValue,
- subject(ProjectPermissionSub.Secrets, {
- environment: expandEnvironment,
- secretPath: expandSecretPath,
- secretName: expandSecretKey,
- secretTags: expandSecretTags
- })
- )
+ CheckCanSecretsSubject(permission, ProjectPermissionSecretActions.ReadValue, {
+ environment: expandEnvironment,
+ secretPath: expandSecretPath,
+ secretName: expandSecretKey,
+ secretTags: expandSecretTags
+ })
});
if (shouldExpandSecretReferences) {
@@ -1095,25 +1073,19 @@ export const secretV2BridgeServiceFactory = ({
expandSecretReferences,
decryptor: (value) => (value ? secretManagerDecryptor({ cipherTextBlob: value }).toString() : ""),
hasSecretAccess: (expandEnvironment, expandSecretPath, expandSecretKey, expandSecretTags) => {
- const canDescribe = permission.can(
- ProjectPermissionSecretActions.DescribeSecret,
- subject(ProjectPermissionSub.Secrets, {
- environment: expandEnvironment,
- secretPath: expandSecretPath,
- secretName: expandSecretKey,
- secretTags: expandSecretTags
- })
- );
+ const canDescribe = CheckCanSecretsSubject(permission, ProjectPermissionSecretActions.DescribeSecret, {
+ environment: expandEnvironment,
+ secretPath: expandSecretPath,
+ secretName: expandSecretKey,
+ secretTags: expandSecretTags
+ });
- const canReadValue = permission.can(
- ProjectPermissionSecretActions.ReadValue,
- subject(ProjectPermissionSub.Secrets, {
- environment: expandEnvironment,
- secretPath: expandSecretPath,
- secretName: expandSecretKey,
- secretTags: expandSecretTags
- })
- );
+ const canReadValue = CheckCanSecretsSubject(permission, ProjectPermissionSecretActions.ReadValue, {
+ environment: expandEnvironment,
+ secretPath: expandSecretPath,
+ secretName: expandSecretKey,
+ secretTags: expandSecretTags
+ });
return viewSecretValue ? canDescribe && canReadValue : canDescribe;
}
@@ -1264,15 +1236,12 @@ export const secretV2BridgeServiceFactory = ({
})
));
- ForbiddenError.from(permission).throwUnlessCan(
- ProjectPermissionSecretActions.DescribeSecret,
- subject(ProjectPermissionSub.Secrets, {
- environment,
- secretPath: path,
- secretName,
- secretTags: (secret?.tags || []).map((el) => el.slug)
- })
- );
+ CheckForbiddenErrorSecretsSubject(permission, ProjectPermissionSecretActions.DescribeSecret, {
+ environment,
+ secretPath: path,
+ secretName,
+ secretTags: (secret?.tags || []).map((el) => el.slug)
+ });
// this will throw if the user doesn't have read value permission no matter what
// because if its an expansion, it will fully depend on the value.
@@ -1282,15 +1251,12 @@ export const secretV2BridgeServiceFactory = ({
secretDAL,
decryptSecretValue: (value) => (value ? secretManagerDecryptor({ cipherTextBlob: value }).toString() : undefined),
canExpandValue: (expandEnvironment, expandSecretPath, expandSecretKey, expandSecretTags) => {
- return permission.can(
- ProjectPermissionSecretActions.ReadValue,
- subject(ProjectPermissionSub.Secrets, {
- environment: expandEnvironment,
- secretPath: expandSecretPath,
- secretName: expandSecretKey,
- secretTags: expandSecretTags
- })
- );
+ return CheckCanSecretsSubject(permission, ProjectPermissionSecretActions.ReadValue, {
+ environment: expandEnvironment,
+ secretPath: expandSecretPath,
+ secretName: expandSecretKey,
+ secretTags: expandSecretTags
+ });
}
});
@@ -1310,15 +1276,12 @@ export const secretV2BridgeServiceFactory = ({
decryptor: (value) => (value ? secretManagerDecryptor({ cipherTextBlob: value }).toString() : ""),
expandSecretReferences: shouldExpandSecretReferences ? expandSecretReferences : undefined,
hasSecretAccess: (expandEnvironment, expandSecretPath, expandSecretKey, expandSecretTags) => {
- return permission.can(
- ProjectPermissionSecretActions.DescribeSecret,
- subject(ProjectPermissionSub.Secrets, {
- environment: expandEnvironment,
- secretPath: expandSecretPath,
- secretName: expandSecretKey,
- secretTags: expandSecretTags
- })
- );
+ return CheckCanSecretsSubject(permission, ProjectPermissionSecretActions.DescribeSecret, {
+ environment: expandEnvironment,
+ secretPath: expandSecretPath,
+ secretName: expandSecretKey,
+ secretTags: expandSecretTags
+ });
}
});
@@ -1330,15 +1293,12 @@ export const secretV2BridgeServiceFactory = ({
if (viewSecretValue) {
if (
- !permission.can(
- ProjectPermissionSecretActions.ReadValue,
- subject(ProjectPermissionSub.Secrets, {
- environment: importedSecret.environment,
- secretPath: importedSecrets[i].secretPath,
- secretName: importedSecret.key,
- secretTags: (importedSecret.secretTags || []).map((el) => el.slug)
- })
- ) &&
+ !CheckCanSecretsSubject(permission, ProjectPermissionSecretActions.ReadValue, {
+ environment: importedSecret.environment,
+ secretPath: importedSecrets[i].secretPath,
+ secretName: importedSecret.key,
+ secretTags: (importedSecret.secretTags || []).map((el) => el.slug)
+ }) &&
secretType !== SecretType.Personal
) {
throw new ForbiddenRequestError({
@@ -1386,15 +1346,12 @@ export const secretV2BridgeServiceFactory = ({
if (viewSecretValue) {
if (
- !permission.can(
- ProjectPermissionSecretActions.ReadValue,
- subject(ProjectPermissionSub.Secrets, {
- environment,
- secretPath: path,
- secretName,
- secretTags: (secret?.tags || []).map((el) => el.slug)
- })
- ) &&
+ !CheckCanSecretsSubject(permission, ProjectPermissionSecretActions.ReadValue, {
+ environment,
+ secretPath: path,
+ secretName,
+ secretTags: (secret?.tags || []).map((el) => el.slug)
+ }) &&
secretType !== SecretType.Personal
) {
throw new ForbiddenRequestError({
@@ -1562,15 +1519,12 @@ export const secretV2BridgeServiceFactory = ({
});
return newSecrets.map((el) => {
- const secretValueHidden = !permission.can(
- ProjectPermissionSecretActions.ReadValue,
- subject(ProjectPermissionSub.Secrets, {
- environment,
- secretPath,
- secretName: el.key,
- secretTags: el.tags?.map((i) => i.slug)
- })
- );
+ const secretValueHidden = !CheckCanSecretsSubject(permission, ProjectPermissionSecretActions.ReadValue, {
+ environment,
+ secretPath,
+ secretName: el.key,
+ secretTags: el.tags?.map((i) => i.slug)
+ });
return reshapeBridgeSecret(
projectId,
@@ -1899,15 +1853,12 @@ export const secretV2BridgeServiceFactory = ({
);
return updatedSecrets.map((el) => {
- const secretValueHidden = !permission.can(
- ProjectPermissionSecretActions.ReadValue,
- subject(ProjectPermissionSub.Secrets, {
- environment,
- secretPath: el.secretPath,
- secretName: el.key,
- secretTags: el.tags.map((i) => i.slug)
- })
- );
+ const secretValueHidden = !CheckCanSecretsSubject(permission, ProjectPermissionSecretActions.ReadValue, {
+ environment,
+ secretPath: el.secretPath,
+ secretName: el.key,
+ secretTags: el.tags.map((i) => i.slug)
+ });
return {
...reshapeBridgeSecret(
@@ -2032,15 +1983,12 @@ export const secretV2BridgeServiceFactory = ({
const secretValueHidden =
!secretToDeleteMatch ||
- !permission.can(
- ProjectPermissionSecretActions.ReadValue,
- subject(ProjectPermissionSub.Secrets, {
- environment,
- secretPath,
- secretName: el.key,
- secretTags: secretToDeleteMatch.tags?.map((i) => i.slug)
- })
- );
+ !CheckCanSecretsSubject(permission, ProjectPermissionSecretActions.ReadValue, {
+ environment,
+ secretPath,
+ secretName: el.key,
+ secretTags: secretToDeleteMatch.tags?.map((i) => i.slug)
+ });
return reshapeBridgeSecret(
projectId,
@@ -2097,17 +2045,15 @@ export const secretV2BridgeServiceFactory = ({
sort: [["createdAt", "desc"]]
});
return secretVersions.map((el) => {
- const secretValueHidden = permission.cannot(
- ProjectPermissionSecretActions.ReadValue,
- subject(ProjectPermissionSub.Secrets, {
- environment: folder.environment.envSlug,
- secretPath: folderWithPath.path,
- secretName: el.key,
- ...(el.tags?.length && {
- secretTags: el.tags.map((tag) => tag.slug)
- })
+ const secretValueHidden = !CheckCanSecretsSubject(permission, ProjectPermissionSecretActions.ReadValue, {
+ environment: folder.environment.envSlug,
+ secretPath: folderWithPath.path,
+ secretName: el.key,
+ ...(el.tags?.length && {
+ secretTags: el.tags.map((tag) => tag.slug)
})
- );
+ });
+
return reshapeBridgeSecret(
folder.projectId,
folder.environment.envSlug,
@@ -2224,15 +2170,27 @@ export const secretV2BridgeServiceFactory = ({
sourceSecrets.forEach((secret) => {
for (const sourceAction of sourceActions) {
- ForbiddenError.from(permission).throwUnlessCan(
- sourceAction,
- subject(ProjectPermissionSub.Secrets, {
+ if (
+ sourceAction === ProjectPermissionSecretActions.DescribeSecret ||
+ sourceAction === ProjectPermissionSecretActions.ReadValue
+ ) {
+ CheckForbiddenErrorSecretsSubject(permission, sourceAction, {
environment: sourceEnvironment,
secretPath: sourceSecretPath,
secretName: secret.key,
secretTags: secret.tags.map((el) => el.slug)
- })
- );
+ });
+ } else {
+ ForbiddenError.from(permission).throwUnlessCan(
+ sourceAction,
+ subject(ProjectPermissionSub.Secrets, {
+ environment: sourceEnvironment,
+ secretPath: sourceSecretPath,
+ secretName: secret.key,
+ secretTags: secret.tags.map((el) => el.slug)
+ })
+ );
+ }
}
});
@@ -2555,10 +2513,10 @@ export const secretV2BridgeServiceFactory = ({
actionProjectType: ActionProjectType.SecretManager
});
- ForbiddenError.from(permission).throwUnlessCan(
- ProjectPermissionSecretActions.DescribeSecret,
- subject(ProjectPermissionSub.Secrets, { environment, secretPath })
- );
+ CheckForbiddenErrorSecretsSubject(permission, ProjectPermissionSecretActions.DescribeSecret, {
+ environment,
+ secretPath
+ });
const folder = await folderDAL.findBySecretPath(projectId, environment, secretPath);
if (!folder)
@@ -2579,15 +2537,12 @@ export const secretV2BridgeServiceFactory = ({
type: SecretType.Shared
});
- ForbiddenError.from(permission).throwUnlessCan(
- ProjectPermissionSecretActions.DescribeSecret,
- subject(ProjectPermissionSub.Secrets, {
- environment,
- secretPath,
- secretName,
- secretTags: (secret?.tags || []).map((el) => el.slug)
- })
- );
+ CheckForbiddenErrorSecretsSubject(permission, ProjectPermissionSecretActions.DescribeSecret, {
+ environment,
+ secretPath,
+ secretName,
+ secretTags: (secret?.tags || []).map((el) => el.slug)
+ });
const decryptedSecretValue = secret.encryptedValue
? secretManagerDecryptor({ cipherTextBlob: secret.encryptedValue }).toString()
@@ -2599,27 +2554,21 @@ export const secretV2BridgeServiceFactory = ({
secretDAL,
decryptSecretValue: (value) => (value ? secretManagerDecryptor({ cipherTextBlob: value }).toString() : undefined),
canExpandValue: (expandEnvironment, expandSecretPath, expandSecretName, expandSecretTags) =>
- permission.can(
- ProjectPermissionSecretActions.ReadValue,
- subject(ProjectPermissionSub.Secrets, {
- environment: expandEnvironment,
- secretPath: expandSecretPath,
- secretName: expandSecretName,
- secretTags: expandSecretTags
- })
- )
+ CheckCanSecretsSubject(permission, ProjectPermissionSecretActions.ReadValue, {
+ environment: expandEnvironment,
+ secretPath: expandSecretPath,
+ secretName: expandSecretName,
+ secretTags: expandSecretTags
+ })
});
if (
- !permission.can(
- ProjectPermissionSecretActions.ReadValue,
- subject(ProjectPermissionSub.Secrets, {
- environment,
- secretPath,
- secretName,
- secretTags: (secret?.tags || []).map((el) => el.slug)
- })
- )
+ !CheckCanSecretsSubject(permission, ProjectPermissionSecretActions.ReadValue, {
+ environment,
+ secretPath,
+ secretName,
+ secretTags: (secret?.tags || []).map((el) => el.slug)
+ })
) {
throw new ForbiddenRequestError({
message: `Unable to get secret reference tree for secret with key '${secretName}', because you don't have permission to view secret value.`
diff --git a/backend/src/services/secret/secret-fns.ts b/backend/src/services/secret/secret-fns.ts
index 4556528a8..ddaa054ab 100644
--- a/backend/src/services/secret/secret-fns.ts
+++ b/backend/src/services/secret/secret-fns.ts
@@ -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 { CheckCanSecretsSubject } from "@app/ee/services/permission/permission-fns";
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service";
-import { ProjectPermissionSecretActions, 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,
@@ -191,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(
- ProjectPermissionSecretActions.ReadValue,
- subject(ProjectPermissionSub.Secrets, {
- environment,
- secretPath: folder.path
- })
- ) && folder.path.startsWith(currentPath === "/" ? "" : currentPath)
+ CheckCanSecretsSubject(permission, ProjectPermissionSecretActions.ReadValue, {
+ environment,
+ secretPath: folder.path
+ }) && folder.path.startsWith(currentPath === "/" ? "" : currentPath)
);
return allowedPaths;
diff --git a/backend/src/services/secret/secret-service.ts b/backend/src/services/secret/secret-service.ts
index 8af0deb2b..e3eb75bda 100644
--- a/backend/src/services/secret/secret-service.ts
+++ b/backend/src/services/secret/secret-service.ts
@@ -13,6 +13,7 @@ import {
SecretType
} from "@app/db/schemas";
import { TLicenseServiceFactory } from "@app/ee/services/license/license-service";
+import { CheckCanSecretsSubject, CheckForbiddenErrorSecretsSubject } from "@app/ee/services/permission/permission-fns";
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service";
import {
ProjectPermissionActions,
@@ -452,13 +453,10 @@ export const secretServiceFactory = ({
});
}
- const secretValueHidden = !permission.can(
- ProjectPermissionSecretActions.ReadValue,
- subject(ProjectPermissionSub.Secrets, {
- environment,
- secretPath: path
- })
- );
+ const secretValueHidden = !CheckCanSecretsSubject(permission, ProjectPermissionSecretActions.ReadValue, {
+ environment,
+ secretPath: path
+ });
return {
...updatedSecret[0],
@@ -562,10 +560,10 @@ export const secretServiceFactory = ({
});
}
- const secretValueHidden = !permission.can(
- ProjectPermissionSecretActions.ReadValue,
- subject(ProjectPermissionSub.Secrets, { environment, secretPath: path })
- );
+ const secretValueHidden = !CheckCanSecretsSubject(permission, ProjectPermissionSecretActions.ReadValue, {
+ environment,
+ secretPath: path
+ });
return {
...deletedSecret[0],
@@ -622,10 +620,10 @@ export const secretServiceFactory = ({
paths = deepPaths.map(({ folderId, path: p }) => ({ folderId, path: p }));
} else {
- ForbiddenError.from(permission).throwUnlessCan(
- ProjectPermissionSecretActions.ReadValue,
- subject(ProjectPermissionSub.Secrets, { environment, secretPath: path })
- );
+ CheckForbiddenErrorSecretsSubject(permission, ProjectPermissionSecretActions.ReadValue, {
+ environment,
+ secretPath: path
+ });
const folder = await folderDAL.findBySecretPath(projectId, environment, path);
if (!folder) return { secrets: [], imports: [] };
@@ -647,13 +645,10 @@ export const secretServiceFactory = ({
// if its service token allow full access over imported one
actor === ActorType.SERVICE
? true
- : permission.can(
- ProjectPermissionSecretActions.ReadValue,
- subject(ProjectPermissionSub.Secrets, {
- environment: importEnv.slug,
- secretPath: importPath
- })
- )
+ : CheckCanSecretsSubject(permission, ProjectPermissionSecretActions.ReadValue, {
+ environment: importEnv.slug,
+ secretPath: importPath
+ })
);
const importedSecrets = await fnSecretsFromImports({
allowedImports,
@@ -704,10 +699,11 @@ export const secretServiceFactory = ({
actorOrgId,
actionProjectType: ActionProjectType.SecretManager
});
- ForbiddenError.from(permission).throwUnlessCan(
- ProjectPermissionSecretActions.ReadValue,
- subject(ProjectPermissionSub.Secrets, { environment, secretPath: path })
- );
+ CheckForbiddenErrorSecretsSubject(permission, ProjectPermissionSecretActions.ReadValue, {
+ environment,
+ secretPath: path
+ });
+
const folder = await folderDAL.findBySecretPath(projectId, environment, path);
if (!folder)
throw new NotFoundError({
@@ -754,14 +750,12 @@ export const secretServiceFactory = ({
// if its service token allow full access over imported one
actor === ActorType.SERVICE
? true
- : permission.can(
- ProjectPermissionSecretActions.ReadValue,
- subject(ProjectPermissionSub.Secrets, {
- environment: importEnv.slug,
- secretPath: importPath
- })
- )
+ : CheckCanSecretsSubject(permission, ProjectPermissionSecretActions.ReadValue, {
+ environment: importEnv.slug,
+ secretPath: importPath
+ })
);
+
const importedSecrets = await fnSecretsFromImports({
allowedImports,
secretDAL,
@@ -975,10 +969,10 @@ export const secretServiceFactory = ({
secretVersionTagDAL
});
- const secretValueHidden = !permission.can(
- ProjectPermissionSecretActions.ReadValue,
- subject(ProjectPermissionSub.Secrets, { environment, secretPath: path })
- );
+ const secretValueHidden = !CheckCanSecretsSubject(permission, ProjectPermissionSecretActions.ReadValue, {
+ environment,
+ secretPath: path
+ });
return updatedSecrets.map((secret) => ({
...secret,
@@ -1069,11 +1063,10 @@ export const secretServiceFactory = ({
});
}
}
-
- const secretValueHidden = !permission.can(
- ProjectPermissionSecretActions.ReadValue,
- subject(ProjectPermissionSub.Secrets, { environment, secretPath: path })
- );
+ const secretValueHidden = !CheckCanSecretsSubject(permission, ProjectPermissionSecretActions.ReadValue, {
+ environment,
+ secretPath: path
+ });
return secrets.map((secret) => ({
...secret,
@@ -1259,8 +1252,20 @@ export const secretServiceFactory = ({
ProjectPermissionSecretActions.Delete,
ProjectPermissionSecretActions.Create,
ProjectPermissionSecretActions.Edit
- ].filter((action) =>
- entityPermission.permission.can(
+ ].filter((action) => {
+ if (
+ action === ProjectPermissionSecretActions.DescribeSecret ||
+ action === ProjectPermissionSecretActions.ReadValue
+ ) {
+ return CheckCanSecretsSubject(entityPermission.permission, action, {
+ environment,
+ secretPath,
+ secretName,
+ secretTags: secret?.tags?.map((el) => el.slug)
+ });
+ }
+
+ return entityPermission.permission.can(
action,
subject(ProjectPermissionSub.Secrets, {
environment,
@@ -1268,8 +1273,8 @@ export const secretServiceFactory = ({
secretName,
secretTags: secret?.tags?.map((el) => el.slug)
})
- )
- );
+ );
+ });
return {
...entityPermission,
@@ -2424,17 +2429,14 @@ export const secretServiceFactory = ({
key: botKey
});
- const secretValueHidden = permission.cannot(
- ProjectPermissionSecretActions.ReadValue,
- subject(ProjectPermissionSub.Secrets, {
- environment: folder.environment.envSlug,
- secretPath: folderWithPath.path,
- secretName: secretKey,
- ...(el.tags?.length && {
- secretTags: el.tags.map((tag) => tag.slug)
- })
+ const secretValueHidden = !CheckCanSecretsSubject(permission, ProjectPermissionSecretActions.ReadValue, {
+ environment: folder.environment.envSlug,
+ secretPath: folderWithPath.path,
+ secretName: secretKey,
+ ...(el.tags?.length && {
+ secretTags: el.tags.map((tag) => tag.slug)
})
- );
+ });
return decryptSecretRaw(
{
@@ -2833,13 +2835,23 @@ export const secretServiceFactory = ({
}
for (const sourceAction of sourceActions) {
- ForbiddenError.from(permission).throwUnlessCan(
- sourceAction,
- subject(ProjectPermissionSub.Secrets, {
+ if (
+ sourceAction === ProjectPermissionSecretActions.ReadValue ||
+ sourceAction === ProjectPermissionSecretActions.DescribeSecret
+ ) {
+ CheckForbiddenErrorSecretsSubject(permission, sourceAction, {
environment: sourceEnvironment,
secretPath: sourceSecretPath
- })
- );
+ });
+ } else {
+ ForbiddenError.from(permission).throwUnlessCan(
+ sourceAction,
+ subject(ProjectPermissionSub.Secrets, {
+ environment: sourceEnvironment,
+ secretPath: sourceSecretPath
+ })
+ );
+ }
}
return {
diff --git a/backend/src/services/service-token/service-token-types.ts b/backend/src/services/service-token/service-token-types.ts
index d06d90a9d..f1a700908 100644
--- a/backend/src/services/service-token/service-token-types.ts
+++ b/backend/src/services/service-token/service-token-types.ts
@@ -7,7 +7,7 @@ export type TCreateServiceTokenDTO = {
iv: string;
tag: string;
expiresIn?: number | null;
- permissions: ("read" | "write" | "readValue")[];
+ permissions: ("read" | "write")[];
} & TProjectPermission;
export type TGetServiceTokenInfoDTO = Omit;
diff --git a/frontend/src/context/ProjectPermissionContext/types.ts b/frontend/src/context/ProjectPermissionContext/types.ts
index 260ffce09..495055977 100644
--- a/frontend/src/context/ProjectPermissionContext/types.ts
+++ b/frontend/src/context/ProjectPermissionContext/types.ts
@@ -8,7 +8,8 @@ export enum ProjectPermissionActions {
}
export enum ProjectPermissionSecretActions {
- DescribeSecret = "read",
+ DescribeAndReadValue = "read",
+ DescribeSecret = "describeSecret",
ReadValue = "readValue",
Create = "create",
Edit = "edit",
diff --git a/frontend/src/lib/fn/permission.ts b/frontend/src/lib/fn/permission.ts
new file mode 100644
index 000000000..f2077d76b
--- /dev/null
+++ b/frontend/src/lib/fn/permission.ts
@@ -0,0 +1,36 @@
+import { MongoAbility, subject } from "@casl/ability";
+
+import { ProjectPermissionSet } from "@app/context/ProjectPermissionContext";
+import {
+ ProjectPermissionSecretActions,
+ ProjectPermissionSub,
+ SecretSubjectFields
+} from "@app/context/ProjectPermissionContext/types";
+
+export function secretsPermissionCan(
+ permission: MongoAbility,
+ 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;
+}
diff --git a/frontend/src/pages/project/AccessControlPage/components/ServiceTokenTab/components/ServiceTokenSection/AddServiceTokenModal.tsx b/frontend/src/pages/project/AccessControlPage/components/ServiceTokenTab/components/ServiceTokenSection/AddServiceTokenModal.tsx
index a669a8360..fa9500a53 100644
--- a/frontend/src/pages/project/AccessControlPage/components/ServiceTokenTab/components/ServiceTokenSection/AddServiceTokenModal.tsx
+++ b/frontend/src/pages/project/AccessControlPage/components/ServiceTokenTab/components/ServiceTokenSection/AddServiceTokenModal.tsx
@@ -303,13 +303,9 @@ export const AddServiceTokenModal = ({ popUp, handlePopUpToggle }: Props) => {
render={({ field: { onChange, value }, fieldState: { error } }) => {
const options = [
{
- label: "Describe Secret (default)",
+ label: "Read (default)",
value: "read"
},
- {
- label: "Read Value (optional)",
- value: "readValue"
- },
{
label: "Write (optional)",
value: "write"
diff --git a/frontend/src/pages/project/RoleDetailsBySlugPage/components/GeneralPermissionPolicies.tsx b/frontend/src/pages/project/RoleDetailsBySlugPage/components/GeneralPermissionPolicies.tsx
index 8aca40a34..a6e472976 100644
--- a/frontend/src/pages/project/RoleDetailsBySlugPage/components/GeneralPermissionPolicies.tsx
+++ b/frontend/src/pages/project/RoleDetailsBySlugPage/components/GeneralPermissionPolicies.tsx
@@ -35,12 +35,13 @@ export const GeneralPermissionPolicies = ) => {
- const { control } = useFormContext();
+ const { control, watch } = useFormContext();
const items = useFieldArray({
control,
name: `permissions.${subject}`
});
const [isOpen, setIsOpen] = useToggle();
+ // const [hideFullReadAccess, setHideFullReadAccess] = useState(false);
if (!items.fields.length) return ;
@@ -71,119 +72,138 @@ export const GeneralPermissionPolicies =
{isOpen && (
- {items.fields.map((el, rootIndex) => (
-
- {isConditionalSubjects(subject) && (
-
-
Permission
-
- (
-
- )}
- />
-
-
-
-
- Whether to allow or forbid the selected actions when the following
- conditions (if any) are met.
-
- Forbid rules must come after allow rules.
- >
- }
- >
-
-
-
-
- )}
-
-
Actions
-
- {actions.map(({ label, value }) => {
- if (typeof value !== "string") return undefined;
+ {items.fields.map((el, rootIndex) => {
+ let isFullReadAccessEnabled = false;
- return (
+ if (subject === ProjectPermissionSub.Secrets) {
+ isFullReadAccessEnabled = watch(`permissions.${subject}.${rootIndex}.read` as any);
+ }
+
+ return (
+
+ {isConditionalSubjects(subject) && (
+
+
Permission
+
{
- return (
-
-
- {label}
-
-
- );
- }}
+ defaultValue={false as any}
+ name={`permissions.${subject}.${rootIndex}.inverted`}
+ render={({ field }) => (
+
+ )}
/>
- );
+
+
+
+
+ Whether to allow or forbid the selected actions when the following
+ conditions (if any) are met.
+
+ Forbid rules must come after allow rules.
+ >
+ }
+ >
+
+
+
+
+ )}
+
+
Actions
+
+ {actions.map(({ label, value }, index) => {
+ if (typeof value !== "string") return undefined;
+
+ if (
+ subject === ProjectPermissionSub.Secrets &&
+ value === "read" &&
+ !isFullReadAccessEnabled
+ ) {
+ return null;
+ }
+
+ return (
+
{
+ return (
+
+
+ {label}
+
+
+ );
+ }}
+ />
+ );
+ })}
+
+
+ {children &&
+ cloneElement(children, {
+ position: rootIndex
})}
+
+ {!isDisabled && isConditionalSubjects(subject) && (
+ }
+ variant="star"
+ size="xs"
+ className="mt-2"
+ onClick={() => {
+ items.insert(rootIndex + 1, [
+ { read: false, edit: false, create: false, delete: false } as any
+ ]);
+ }}
+ isDisabled={isDisabled}
+ >
+ Add policy
+
+ )}
+ {!isDisabled && (
+ }
+ variant="outline_bg"
+ size="xs"
+ className="mt-2 hover:border-red"
+ onClick={() => items.remove(rootIndex)}
+ isDisabled={isDisabled}
+ >
+ Remove policy
+
+ )}{" "}
- {children &&
- cloneElement(children, {
- position: rootIndex
- })}
-
- {!isDisabled && isConditionalSubjects(subject) && (
- }
- variant="star"
- size="xs"
- className="mt-2"
- onClick={() => {
- items.insert(rootIndex + 1, [
- { read: false, edit: false, create: false, delete: false } as any
- ]);
- }}
- isDisabled={isDisabled}
- >
- Add policy
-
- )}
- {!isDisabled && (
- }
- variant="outline_bg"
- size="xs"
- className="mt-2 hover:border-red"
- onClick={() => items.remove(rootIndex)}
- isDisabled={isDisabled}
- >
- Remove policy
-
- )}{" "}
-
-
- ))}
+ );
+ })}
)}
diff --git a/frontend/src/pages/project/RoleDetailsBySlugPage/components/ProjectRoleModifySection.utils.tsx b/frontend/src/pages/project/RoleDetailsBySlugPage/components/ProjectRoleModifySection.utils.tsx
index e26ec8bc4..9f13b53a8 100644
--- a/frontend/src/pages/project/RoleDetailsBySlugPage/components/ProjectRoleModifySection.utils.tsx
+++ b/frontend/src/pages/project/RoleDetailsBySlugPage/components/ProjectRoleModifySection.utils.tsx
@@ -1,5 +1,9 @@
+import { ReactNode } from "react";
+import { faWarning } from "@fortawesome/free-solid-svg-icons";
+import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { z } from "zod";
+import { Tooltip } from "@app/components/v2";
import {
ProjectPermissionActions,
ProjectPermissionCmekActions,
@@ -24,11 +28,12 @@ const GeneralPolicyActionSchema = z.object({
});
const SecretPolicyActionSchema = z.object({
- read: z.boolean().optional(), // describe secret
- edit: z.boolean().optional(),
- delete: z.boolean().optional(),
- create: z.boolean().optional(),
- readValue: z.boolean().optional()
+ [ProjectPermissionSecretActions.DescribeAndReadValue]: z.boolean().optional(), // existing read, gives both describe and read value
+ [ProjectPermissionSecretActions.DescribeSecret]: z.boolean().optional(), // describe secret, cannot read value
+ [ProjectPermissionSecretActions.ReadValue]: z.boolean().optional(), // read value
+ [ProjectPermissionSecretActions.Edit]: z.boolean().optional(), // edit secret
+ [ProjectPermissionSecretActions.Delete]: z.boolean().optional(), // delete secret
+ [ProjectPermissionSecretActions.Create]: z.boolean().optional() // create secret
});
const CmekPolicyActionSchema = z.object({
@@ -294,19 +299,24 @@ export const rolePermission2Form = (permissions: TProjectPermission[] = []) => {
}
if (subject === ProjectPermissionSub.Secrets) {
- const canRead = action.includes(ProjectPermissionSecretActions.DescribeSecret);
+ const canDescribeAndReadValue = action.includes(
+ ProjectPermissionSecretActions.DescribeAndReadValue
+ );
+ const canDescribe = action.includes(ProjectPermissionSecretActions.DescribeSecret);
+ const canReadValue = action.includes(ProjectPermissionSecretActions.ReadValue);
+
const canEdit = action.includes(ProjectPermissionSecretActions.Edit);
const canDelete = action.includes(ProjectPermissionSecretActions.Delete);
const canCreate = action.includes(ProjectPermissionSecretActions.Create);
- const canReadValue = action.includes(ProjectPermissionSecretActions.ReadValue);
// from above statement we are sure it won't be undefined
formVal[subject]!.push({
- read: canRead,
+ describeSecret: canDescribe,
+ read: canDescribeAndReadValue,
+ readValue: canReadValue,
create: canCreate,
edit: canEdit,
delete: canDelete,
- readValue: canReadValue,
conditions: conditions ? convertCaslConditionToFormOperator(conditions) : [],
inverted
});
@@ -501,7 +511,7 @@ export type TProjectPermissionObject = {
[K in ProjectPermissionSub]: {
title: string;
actions: {
- label: string;
+ label: string | ReactNode;
value: keyof Omit<
NonNullable
[K]>[number],
"conditions" | "inverted"
@@ -514,11 +524,35 @@ export const PROJECT_PERMISSION_OBJECT: TProjectPermissionObject = {
[ProjectPermissionSub.Secrets]: {
title: "Secrets",
actions: [
- { label: "Describe Secret", value: "read" },
- { label: "Create", value: "create" },
- { label: "Read Value", value: "readValue" },
- { label: "Modify", value: "edit" },
- { label: "Remove", value: "delete" }
+ {
+ label: (
+
+
+ Read (legacy)
+
+
+ This is a legacy action and will be removed in the future.
+
+
You should instead use the{" "}
+ Describe Secret and{" "}
+ Read Value actions.
+
+ }
+ >
+
+
+
+ ),
+ value: ProjectPermissionSecretActions.DescribeAndReadValue
+ },
+ { label: "Describe Secret", value: ProjectPermissionSecretActions.DescribeSecret },
+ { label: "Read Value", value: ProjectPermissionSecretActions.ReadValue },
+ { label: "Modify", value: ProjectPermissionSecretActions.Edit },
+ { label: "Remove", value: ProjectPermissionSecretActions.Delete },
+ { label: "Create", value: ProjectPermissionSecretActions.Create }
]
},
[ProjectPermissionSub.SecretFolders]: {
diff --git a/frontend/src/pages/secret-manager/OverviewPage/components/SecretOverviewTableRow/SecretRenameRow.tsx b/frontend/src/pages/secret-manager/OverviewPage/components/SecretOverviewTableRow/SecretRenameRow.tsx
index 3b05f10fa..536f75542 100644
--- a/frontend/src/pages/secret-manager/OverviewPage/components/SecretOverviewTableRow/SecretRenameRow.tsx
+++ b/frontend/src/pages/secret-manager/OverviewPage/components/SecretOverviewTableRow/SecretRenameRow.tsx
@@ -15,6 +15,7 @@ import { ProjectPermissionSecretActions } from "@app/context/ProjectPermissionCo
import { useToggle } from "@app/hooks";
import { useUpdateSecretV3 } from "@app/hooks/api";
import { SecretType, SecretV3RawSanitized } from "@app/hooks/api/types";
+import { secretsPermissionCan } from "@app/lib/fn/permission";
enum SecretActionType {
Created = "created",
@@ -51,8 +52,11 @@ function SecretRenameRow({ environments, getSecretByKey, secretKey, secretPath }
secretTags: (secretDetails?.tags || []).map((i) => i.slug)
});
const isSecretInEnvReadOnly =
- permission.can(ProjectPermissionSecretActions.DescribeSecret, secretPermissionSubject) &&
- permission.cannot(ProjectPermissionSecretActions.Edit, secretPermissionSubject);
+ secretsPermissionCan(
+ permission,
+ ProjectPermissionSecretActions.DescribeSecret,
+ secretPermissionSubject
+ ) && permission.cannot(ProjectPermissionSecretActions.Edit, secretPermissionSubject);
if (isSecretInEnvReadOnly) {
return true;
}
diff --git a/frontend/src/pages/secret-manager/SecretDashboardPage/SecretDashboardPage.tsx b/frontend/src/pages/secret-manager/SecretDashboardPage/SecretDashboardPage.tsx
index 5cbefe9c3..4dba1c2a8 100644
--- a/frontend/src/pages/secret-manager/SecretDashboardPage/SecretDashboardPage.tsx
+++ b/frontend/src/pages/secret-manager/SecretDashboardPage/SecretDashboardPage.tsx
@@ -38,6 +38,7 @@ import { useGetProjectSecretsDetails } from "@app/hooks/api/dashboard";
import { DashboardSecretsOrderBy } from "@app/hooks/api/dashboard/types";
import { OrderByDirection } from "@app/hooks/api/generic/types";
import { ProjectType } from "@app/hooks/api/workspace/types";
+import { secretsPermissionCan } from "@app/lib/fn/permission";
import { SecretTableResourceCount } from "../OverviewPage/components/SecretTableResourceCount";
import { SecretV2MigrationSection } from "../OverviewPage/components/SecretV2MigrationSection";
@@ -103,23 +104,27 @@ const Page = () => {
const workspaceId = currentWorkspace?.id || "";
const projectSlug = currentWorkspace?.slug || "";
const secretPath = (routerQueryParams.secretPath as string) || "/";
- const canReadSecret = permission.can(
+
+ const canReadSecret = secretsPermissionCan(
+ permission,
ProjectPermissionSecretActions.DescribeSecret,
- subject(ProjectPermissionSub.Secrets, {
+ {
environment,
secretPath,
secretName: "*",
secretTags: ["*"]
- })
+ }
);
- const canReadSecretValue = permission.can(
+
+ const canReadSecretValue = secretsPermissionCan(
+ permission,
ProjectPermissionSecretActions.ReadValue,
- subject(ProjectPermissionSub.Secrets, {
+ {
environment,
secretPath,
secretName: "*",
secretTags: ["*"]
- })
+ }
);
const canReadSecretImports = permission.can(
diff --git a/frontend/src/pages/secret-manager/SecretDashboardPage/components/SecretListView/SecretDetailSidebar.tsx b/frontend/src/pages/secret-manager/SecretDashboardPage/components/SecretListView/SecretDetailSidebar.tsx
index d6da791bb..3ad7159d7 100644
--- a/frontend/src/pages/secret-manager/SecretDashboardPage/components/SecretListView/SecretDetailSidebar.tsx
+++ b/frontend/src/pages/secret-manager/SecretDashboardPage/components/SecretListView/SecretDetailSidebar.tsx
@@ -57,6 +57,7 @@ import { ActorType } from "@app/hooks/api/auditLogs/enums";
import { useGetSecretAccessList } from "@app/hooks/api/secrets/queries";
import { SecretV3RawSanitized, WsTag } from "@app/hooks/api/types";
import { ProjectType } from "@app/hooks/api/workspace/types";
+import { secretsPermissionCan } from "@app/lib/fn/permission";
import { CreateReminderForm } from "./CreateReminderForm";
import { formSchema, SecretActionType, TFormSchema } from "./SecretListView.utils";
@@ -140,26 +141,24 @@ export const SecretDetailSidebar = ({
})
);
- const cannotReadSecretValue = permission.cannot(
+ const cannotReadSecretValue = !secretsPermissionCan(
+ permission,
ProjectPermissionSecretActions.ReadValue,
- subject(ProjectPermissionSub.Secrets, {
+ {
environment,
secretPath,
secretName: secretKey,
secretTags: selectTagSlugs
- })
+ }
);
const isReadOnly =
- permission.can(
- ProjectPermissionSecretActions.DescribeSecret,
- subject(ProjectPermissionSub.Secrets, {
- environment,
- secretPath,
- secretName: secretKey,
- secretTags: selectTagSlugs
- })
- ) &&
+ secretsPermissionCan(permission, ProjectPermissionSecretActions.DescribeSecret, {
+ environment,
+ secretPath,
+ secretName: secretKey,
+ secretTags: selectTagSlugs
+ }) &&
cannotEditSecret &&
cannotReadSecretValue;
diff --git a/frontend/src/pages/secret-manager/SecretDashboardPage/components/SecretListView/SecretItem.tsx b/frontend/src/pages/secret-manager/SecretDashboardPage/components/SecretListView/SecretItem.tsx
index 02f7a0aa9..0271bffef 100644
--- a/frontend/src/pages/secret-manager/SecretDashboardPage/components/SecretListView/SecretItem.tsx
+++ b/frontend/src/pages/secret-manager/SecretDashboardPage/components/SecretListView/SecretItem.tsx
@@ -47,6 +47,7 @@ import {
import { ProjectPermissionSecretActions } from "@app/context/ProjectPermissionContext/types";
import { Blur } from "@app/components/v2/Blur";
+import { secretsPermissionCan } from "@app/lib/fn/permission";
import {
FontAwesomeSpriteName,
formSchema,
@@ -131,15 +132,12 @@ export const SecretItem = memo(
});
const isReadOnly =
- permission.can(
- ProjectPermissionSecretActions.DescribeSecret,
- subject(ProjectPermissionSub.Secrets, {
- environment,
- secretPath,
- secretName,
- secretTags: selectedTagSlugs
- })
- ) &&
+ secretsPermissionCan(permission, ProjectPermissionSecretActions.DescribeSecret, {
+ environment,
+ secretPath,
+ secretName,
+ secretTags: selectedTagSlugs
+ }) &&
permission.cannot(
ProjectPermissionSecretActions.Edit,
subject(ProjectPermissionSub.Secrets, {
From 955657e17234278a21c7cd5c6434d0cc1a5ba1bc Mon Sep 17 00:00:00 2001
From: Daniel Hougaard
Date: Sat, 8 Mar 2025 03:58:15 +0400
Subject: [PATCH 19/71] fix: legacy permission check
---
.../secret-v2-bridge/secret-v2-bridge-service.ts | 15 ++++++---------
1 file changed, 6 insertions(+), 9 deletions(-)
diff --git a/backend/src/services/secret-v2-bridge/secret-v2-bridge-service.ts b/backend/src/services/secret-v2-bridge/secret-v2-bridge-service.ts
index 8ebc0a582..d6115cdcc 100644
--- a/backend/src/services/secret-v2-bridge/secret-v2-bridge-service.ts
+++ b/backend/src/services/secret-v2-bridge/secret-v2-bridge-service.ts
@@ -1127,15 +1127,12 @@ export const secretV2BridgeServiceFactory = ({
actionProjectType: ActionProjectType.SecretManager
});
- ForbiddenError.from(permission).throwUnlessCan(
- ProjectPermissionActions.Read,
- subject(ProjectPermissionSub.Secrets, {
- environment: folderWithPath.environmentSlug,
- secretPath: folderWithPath.path,
- secretName: secret.key,
- secretTags: secret.tags.map((i) => i.slug)
- })
- );
+ CheckForbiddenErrorSecretsSubject(permission, ProjectPermissionSecretActions.ReadValue, {
+ environment: folderWithPath.environmentSlug,
+ secretPath: folderWithPath.path,
+ secretName: secret.key,
+ secretTags: secret.tags.map((i) => i.slug)
+ });
if (secret.type === SecretType.Personal && secret.userId !== actorId) {
throw new ForbiddenRequestError({
From dc88115d4370ad5cd75c6e90031116d75991541a Mon Sep 17 00:00:00 2001
From: Daniel Hougaard
Date: Sat, 8 Mar 2025 03:59:34 +0400
Subject: [PATCH 20/71] fix: tests failing
---
backend/e2e-test/routes/v2/service-token.spec.ts | 10 +++++-----
1 file changed, 5 insertions(+), 5 deletions(-)
diff --git a/backend/e2e-test/routes/v2/service-token.spec.ts b/backend/e2e-test/routes/v2/service-token.spec.ts
index bfe043523..6cc8f6e34 100644
--- a/backend/e2e-test/routes/v2/service-token.spec.ts
+++ b/backend/e2e-test/routes/v2/service-token.spec.ts
@@ -6,7 +6,7 @@ import { decryptAsymmetric, decryptSymmetric128BitHexKeyUTF8, encryptSymmetric12
const createServiceToken = async (
scopes: { environment: string; secretPath: string }[],
- permissions: ("read" | "write" | "readValue")[]
+ permissions: ("read" | "write")[]
) => {
const projectKeyRes = await testServer.inject({
method: "GET",
@@ -139,7 +139,7 @@ describe("Service token secret ops", async () => {
beforeAll(async () => {
serviceToken = await createServiceToken(
[{ secretPath: "/**", environment: seedData1.environment.slug }],
- ["read", "write", "readValue"]
+ ["read", "write"]
);
// this is ensure cli service token decryptiong working fine
@@ -496,7 +496,7 @@ describe("Service token fail cases", async () => {
test("Unauthorized secret path access", async () => {
const serviceToken = await createServiceToken(
[{ secretPath: "/", environment: seedData1.environment.slug }],
- ["read", "readValue", "write"]
+ ["read", "write"]
);
const fetchSecrets = await testServer.inject({
method: "GET",
@@ -518,7 +518,7 @@ describe("Service token fail cases", async () => {
test("Unauthorized secret environment access", async () => {
const serviceToken = await createServiceToken(
[{ secretPath: "/", environment: seedData1.environment.slug }],
- ["read", "readValue", "write"]
+ ["read", "write"]
);
const fetchSecrets = await testServer.inject({
method: "GET",
@@ -540,7 +540,7 @@ describe("Service token fail cases", async () => {
test("Unauthorized write operation", async () => {
const serviceToken = await createServiceToken(
[{ secretPath: "/", environment: seedData1.environment.slug }],
- ["read", "readValue"]
+ ["read"]
);
const writeSecrets = await testServer.inject({
method: "POST",
From 46ce46b5a04e97d7c8d953698702d328af0df088 Mon Sep 17 00:00:00 2001
From: Daniel Hougaard
Date: Sat, 8 Mar 2025 04:04:52 +0400
Subject: [PATCH 21/71] fix: get secret by ID using legacy permissions
---
backend/src/server/routes/v3/secret-router.ts | 9 +--------
.../secret-v2-bridge/secret-v2-bridge-service.ts | 16 +++++++++++-----
2 files changed, 12 insertions(+), 13 deletions(-)
diff --git a/backend/src/server/routes/v3/secret-router.ts b/backend/src/server/routes/v3/secret-router.ts
index b68d042f0..f854baade 100644
--- a/backend/src/server/routes/v3/secret-router.ts
+++ b/backend/src/server/routes/v3/secret-router.ts
@@ -368,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()
})
})
diff --git a/backend/src/services/secret-v2-bridge/secret-v2-bridge-service.ts b/backend/src/services/secret-v2-bridge/secret-v2-bridge-service.ts
index d6115cdcc..ec2728966 100644
--- a/backend/src/services/secret-v2-bridge/secret-v2-bridge-service.ts
+++ b/backend/src/services/secret-v2-bridge/secret-v2-bridge-service.ts
@@ -1154,11 +1154,17 @@ export const secretV2BridgeServiceFactory = ({
? secretManagerDecryptor({ cipherTextBlob: secret.encryptedComment }).toString()
: "";
- return reshapeBridgeSecret(secret.projectId, folderWithPath.environmentSlug, folderWithPath.path, {
- ...secret,
- value: secretValue,
- comment: secretComment
- });
+ return reshapeBridgeSecret(
+ secret.projectId,
+ folderWithPath.environmentSlug,
+ folderWithPath.path,
+ {
+ ...secret,
+ value: secretValue,
+ comment: secretComment
+ },
+ false
+ );
};
const getSecretByName = async ({
From a8fc0e540a92997b0948cead7ad07c4b6217cd0e Mon Sep 17 00:00:00 2001
From: Daniel Hougaard
Date: Mon, 10 Mar 2025 17:50:43 +0400
Subject: [PATCH 22/71] fix: tests and missing tags permission check
---
backend/package.json | 1 +
.../ee/services/permission/permission-fns.ts | 4 ++--
.../secret-v2-bridge-service.ts | 23 ++++++++++---------
3 files changed, 15 insertions(+), 13 deletions(-)
diff --git a/backend/package.json b/backend/package.json
index 5c2c805bc..25380f896 100644
--- a/backend/package.json
+++ b/backend/package.json
@@ -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": [],
diff --git a/backend/src/ee/services/permission/permission-fns.ts b/backend/src/ee/services/permission/permission-fns.ts
index 97113a562..c9f3b826e 100644
--- a/backend/src/ee/services/permission/permission-fns.ts
+++ b/backend/src/ee/services/permission/permission-fns.ts
@@ -1,5 +1,5 @@
/* eslint-disable no-nested-ternary */
-import { ForbiddenError, MongoAbility, subject } from "@casl/ability";
+import { ForbiddenError, MongoAbility, PureAbility, subject } from "@casl/ability";
import { z } from "zod";
import { TOrganizations } from "@app/db/schemas";
@@ -15,7 +15,7 @@ import {
} from "./project-permission";
export function CheckForbiddenErrorSecretsSubject(
- permission: MongoAbility,
+ permission: MongoAbility | PureAbility,
action: Extract<
ProjectPermissionSecretActions,
ProjectPermissionSecretActions.ReadValue | ProjectPermissionSecretActions.DescribeSecret
diff --git a/backend/src/services/secret-v2-bridge/secret-v2-bridge-service.ts b/backend/src/services/secret-v2-bridge/secret-v2-bridge-service.ts
index ec2728966..3b3a147bd 100644
--- a/backend/src/services/secret-v2-bridge/secret-v2-bridge-service.ts
+++ b/backend/src/services/secret-v2-bridge/secret-v2-bridge-service.ts
@@ -1,4 +1,4 @@
-import { ForbiddenError, PureAbility, subject } from "@casl/ability";
+import { ForbiddenError, MongoAbility, subject } from "@casl/ability";
import { Knex } from "knex";
import { z } from "zod";
@@ -15,6 +15,7 @@ import { TPermissionServiceFactory } from "@app/ee/services/permission/permissio
import {
ProjectPermissionActions,
ProjectPermissionSecretActions,
+ ProjectPermissionSet,
ProjectPermissionSub
} from "@app/ee/services/permission/project-permission";
import { TSecretApprovalPolicyServiceFactory } from "@app/ee/services/secret-approval-policy/secret-approval-policy-service";
@@ -123,7 +124,7 @@ export const secretV2BridgeServiceFactory = ({
}: TSecretV2BridgeServiceFactoryDep) => {
const $validateSecretReferences = async (
projectId: string,
- permission: PureAbility,
+ permission: MongoAbility,
references: ReturnType["nestedReferences"],
tx?: Knex
) => {
@@ -131,6 +132,7 @@ export const secretV2BridgeServiceFactory = ({
const uniqueReferenceEnvironmentSlugs = Array.from(new Set(references.map((el) => el.environment)));
const referencesEnvironments = await projectEnvDAL.findBySlugs(projectId, uniqueReferenceEnvironmentSlugs, tx);
+
if (referencesEnvironments.length !== uniqueReferenceEnvironmentSlugs.length)
throw new BadRequestError({
message: `Referenced environment not found. Missing ${diff(
@@ -147,6 +149,7 @@ export const secretV2BridgeServiceFactory = ({
})),
tx
);
+
const referencesFolderGroupByPath = groupBy(referredFolders.filter(Boolean), (i) => `${i?.envId}-${i?.path}`);
const referredSecrets = await secretDAL.find(
{
@@ -194,15 +197,12 @@ export const secretV2BridgeServiceFactory = ({
const referredSecretsGroupBySecretKey = groupBy(referredSecrets, (i) => i.key);
references.forEach((el) => {
- ForbiddenError.from(permission).throwUnlessCan(
- ProjectPermissionActions.Read,
- subject(ProjectPermissionSub.Secrets, {
- environment: el.environment,
- secretPath: el.secretPath,
- secretName: el.secretKey,
- tags: referredSecretsGroupBySecretKey[el.secretKey][0]?.tags?.map((i) => i.slug)
- })
- );
+ CheckForbiddenErrorSecretsSubject(permission, ProjectPermissionSecretActions.ReadValue, {
+ environment: el.environment,
+ secretPath: el.secretPath,
+ secretName: el.secretKey,
+ secretTags: referredSecretsGroupBySecretKey[el.secretKey][0]?.tags?.map((i) => i.slug)
+ });
});
return referredSecrets;
@@ -277,6 +277,7 @@ export const secretV2BridgeServiceFactory = ({
const allSecretReferences = nestedReferences.concat(
localReferences.map((el) => ({ secretKey: el, secretPath, environment }))
);
+
await $validateSecretReferences(projectId, permission, allSecretReferences);
const { encryptor: secretManagerEncryptor } = await kmsService.createCipherPairWithDataKey({
From 09df4406131926d6aabb60e1c010b1f99bd6d0d6 Mon Sep 17 00:00:00 2001
From: Daniel Hougaard
Date: Tue, 11 Mar 2025 00:18:42 +0400
Subject: [PATCH 23/71] Update secret-version-dal.ts
---
.../secret-v2-bridge/secret-version-dal.ts | 55 +++++++++++++++----
1 file changed, 44 insertions(+), 11 deletions(-)
diff --git a/backend/src/services/secret-v2-bridge/secret-version-dal.ts b/backend/src/services/secret-v2-bridge/secret-version-dal.ts
index 12c54cd7c..b54b073a6 100644
--- a/backend/src/services/secret-v2-bridge/secret-version-dal.ts
+++ b/backend/src/services/secret-v2-bridge/secret-version-dal.ts
@@ -187,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);
@@ -197,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);
@@ -214,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" });
}
From 78c349c09a9f12b8197e315db69b9e23ecd22f20 Mon Sep 17 00:00:00 2001
From: Daniel Hougaard
Date: Tue, 11 Mar 2025 00:31:21 +0400
Subject: [PATCH 24/71] fix(view-secret-value): requested changes
---
.../ee/services/permission/permission-fns.ts | 12 +-
.../services/permission/project-permission.ts | 6 +-
.../secret-approval-request-service.ts | 4 +-
.../secret-snapshot-service.ts | 41 ++--
.../integration/integration-service.ts | 6 +-
.../src/services/project/project-service.ts | 4 +-
.../secret-import/secret-import-service.ts | 15 +-
.../secret-sync/secret-sync-service.ts | 6 +-
.../secret-v2-bridge-service.ts | 229 ++++++++++--------
.../secret-v2-bridge-types.ts | 2 +-
backend/src/services/secret/secret-fns.ts | 4 +-
backend/src/services/secret/secret-service.ts | 85 ++++---
frontend/src/lib/fn/permission.ts | 2 +-
.../ProjectRoleModifySection.utils.tsx | 10 +-
.../SecretRenameRow.tsx | 4 +-
.../SecretDashboardPage.tsx | 6 +-
.../SecretListView/SecretDetailSidebar.tsx | 24 +-
.../components/SecretListView/SecretItem.tsx | 18 +-
18 files changed, 273 insertions(+), 205 deletions(-)
diff --git a/backend/src/ee/services/permission/permission-fns.ts b/backend/src/ee/services/permission/permission-fns.ts
index c9f3b826e..ecc8b676e 100644
--- a/backend/src/ee/services/permission/permission-fns.ts
+++ b/backend/src/ee/services/permission/permission-fns.ts
@@ -14,7 +14,7 @@ import {
SecretSubjectFields
} from "./project-permission";
-export function CheckForbiddenErrorSecretsSubject(
+export function throwIfMissingSecretReadValueOrDescribePermission(
permission: MongoAbility | PureAbility,
action: Extract<
ProjectPermissionSecretActions,
@@ -43,7 +43,7 @@ export function CheckForbiddenErrorSecretsSubject(
}
}
-export function CheckCanSecretsSubject(
+export function hasSecretReadValueOrDescribePermission(
permission: MongoAbility,
action: Extract<
ProjectPermissionSecretActions,
@@ -83,12 +83,10 @@ export function checkForInvalidPermissionCombination(permissions: z.infer {
can(
[
- // not adding DescribeAndReadValue, because it's already covered by DescribeSecret and ReadValue
+ ProjectPermissionSecretActions.DescribeAndReadValue,
ProjectPermissionSecretActions.DescribeSecret,
ProjectPermissionSecretActions.ReadValue,
ProjectPermissionSecretActions.Create,
@@ -634,7 +634,7 @@ const buildMemberPermissionRules = () => {
can(
[
- // not adding DescribeAndReadValue, because it's already covered by DescribeSecret and ReadValue
+ ProjectPermissionSecretActions.DescribeAndReadValue,
ProjectPermissionSecretActions.DescribeSecret,
ProjectPermissionSecretActions.ReadValue,
ProjectPermissionSecretActions.Edit,
@@ -811,7 +811,7 @@ export const projectMemberPermissions = buildMemberPermissionRules();
const buildViewerPermissionRules = () => {
const { can, rules } = new AbilityBuilder>(createMongoAbility);
- // not adding DescribeAndReadValue, because it's already covered by DescribeSecret and ReadValue
+ can(ProjectPermissionSecretActions.DescribeAndReadValue, ProjectPermissionSub.Secrets);
can(ProjectPermissionSecretActions.DescribeSecret, ProjectPermissionSub.Secrets);
can(ProjectPermissionSecretActions.ReadValue, ProjectPermissionSub.Secrets);
can(ProjectPermissionActions.Read, ProjectPermissionSub.SecretFolders);
diff --git a/backend/src/ee/services/secret-approval-request/secret-approval-request-service.ts b/backend/src/ee/services/secret-approval-request/secret-approval-request-service.ts
index 9f3ec7111..72d36c26b 100644
--- a/backend/src/ee/services/secret-approval-request/secret-approval-request-service.ts
+++ b/backend/src/ee/services/secret-approval-request/secret-approval-request-service.ts
@@ -77,7 +77,7 @@ import {
TSecretApprovalDetailsDTO,
TStatusChangeDTO
} from "./secret-approval-request-types";
-import { CheckForbiddenErrorSecretsSubject } from "../permission/permission-fns";
+import { throwIfMissingSecretReadValueOrDescribePermission } from "../permission/permission-fns";
type TSecretApprovalRequestServiceFactoryDep = {
permissionService: Pick;
@@ -919,7 +919,7 @@ export const secretApprovalRequestServiceFactory = ({
actionProjectType: ActionProjectType.SecretManager
});
- CheckForbiddenErrorSecretsSubject(permission, ProjectPermissionSecretActions.ReadValue, {
+ throwIfMissingSecretReadValueOrDescribePermission(permission, ProjectPermissionSecretActions.ReadValue, {
environment,
secretPath
});
diff --git a/backend/src/ee/services/secret-snapshot/secret-snapshot-service.ts b/backend/src/ee/services/secret-snapshot/secret-snapshot-service.ts
index 6afc6bca3..5fbb3f598 100644
--- a/backend/src/ee/services/secret-snapshot/secret-snapshot-service.ts
+++ b/backend/src/ee/services/secret-snapshot/secret-snapshot-service.ts
@@ -23,7 +23,10 @@ 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 { CheckCanSecretsSubject, CheckForbiddenErrorSecretsSubject } from "../permission/permission-fns";
+import {
+ hasSecretReadValueOrDescribePermission,
+ throwIfMissingSecretReadValueOrDescribePermission
+} from "../permission/permission-fns";
import { TPermissionServiceFactory } from "../permission/permission-service";
import {
ProjectPermissionActions,
@@ -103,7 +106,7 @@ 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.
- CheckForbiddenErrorSecretsSubject(permission, ProjectPermissionSecretActions.DescribeSecret, {
+ throwIfMissingSecretReadValueOrDescribePermission(permission, ProjectPermissionSecretActions.DescribeSecret, {
environment,
secretPath: path
});
@@ -140,7 +143,7 @@ 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.
- CheckForbiddenErrorSecretsSubject(permission, ProjectPermissionSecretActions.DescribeSecret, {
+ throwIfMissingSecretReadValueOrDescribePermission(permission, ProjectPermissionSecretActions.DescribeSecret, {
environment,
secretPath: path
});
@@ -187,12 +190,16 @@ export const secretSnapshotServiceFactory = ({
snapshotDetails = {
...encryptedSnapshotDetails,
secretVersions: encryptedSnapshotDetails.secretVersions.map((el) => {
- const canReadValue = CheckCanSecretsSubject(permission, ProjectPermissionSecretActions.ReadValue, {
- environment: encryptedSnapshotDetails.environment.slug,
- secretPath: fullFolderPath,
- secretName: el.key,
- secretTags: el.tags.length ? el.tags.map((tag) => tag.slug) : undefined
- });
+ 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) {
@@ -236,12 +243,16 @@ export const secretSnapshotServiceFactory = ({
key: botKey
});
- const canReadValue = CheckCanSecretsSubject(permission, ProjectPermissionSecretActions.ReadValue, {
- environment: encryptedSnapshotDetails.environment.slug,
- secretPath: fullFolderPath,
- secretName: secretKey,
- secretTags: el.tags.length ? el.tags.map((tag) => tag.slug) : undefined
- });
+ 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 = "";
diff --git a/backend/src/services/integration/integration-service.ts b/backend/src/services/integration/integration-service.ts
index 3f7c55896..ad08e89d1 100644
--- a/backend/src/services/integration/integration-service.ts
+++ b/backend/src/services/integration/integration-service.ts
@@ -1,7 +1,7 @@
import { ForbiddenError } from "@casl/ability";
import { ActionProjectType } from "@app/db/schemas";
-import { CheckForbiddenErrorSecretsSubject } from "@app/ee/services/permission/permission-fns";
+import { throwIfMissingSecretReadValueOrDescribePermission } from "@app/ee/services/permission/permission-fns";
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service";
import {
ProjectPermissionActions,
@@ -96,7 +96,7 @@ export const integrationServiceFactory = ({
});
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Create, ProjectPermissionSub.Integrations);
- CheckForbiddenErrorSecretsSubject(permission, ProjectPermissionSecretActions.ReadValue, {
+ throwIfMissingSecretReadValueOrDescribePermission(permission, ProjectPermissionSecretActions.ReadValue, {
environment: sourceEnvironment,
secretPath
});
@@ -176,7 +176,7 @@ export const integrationServiceFactory = ({
const newSecretPath = secretPath || integration.secretPath;
if (environment || secretPath) {
- CheckForbiddenErrorSecretsSubject(permission, ProjectPermissionSecretActions.ReadValue, {
+ throwIfMissingSecretReadValueOrDescribePermission(permission, ProjectPermissionSecretActions.ReadValue, {
environment: newEnvironment,
secretPath: newSecretPath
});
diff --git a/backend/src/services/project/project-service.ts b/backend/src/services/project/project-service.ts
index b21cc99fe..dbef33c10 100644
--- a/backend/src/services/project/project-service.ts
+++ b/backend/src/services/project/project-service.ts
@@ -10,7 +10,7 @@ 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 { CheckForbiddenErrorSecretsSubject } from "@app/ee/services/permission/permission-fns";
+import { throwIfMissingSecretReadValueOrDescribePermission } from "@app/ee/services/permission/permission-fns";
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service";
import {
ProjectPermissionActions,
@@ -765,7 +765,7 @@ export const projectServiceFactory = ({
actorOrgId,
actionProjectType: ActionProjectType.Any
});
- CheckForbiddenErrorSecretsSubject(permission, ProjectPermissionSecretActions.DescribeSecret);
+ throwIfMissingSecretReadValueOrDescribePermission(permission, ProjectPermissionSecretActions.DescribeSecret);
const project = await projectDAL.findProjectById(projectId);
diff --git a/backend/src/services/secret-import/secret-import-service.ts b/backend/src/services/secret-import/secret-import-service.ts
index 27ed12c84..b8e8b2fa0 100644
--- a/backend/src/services/secret-import/secret-import-service.ts
+++ b/backend/src/services/secret-import/secret-import-service.ts
@@ -4,7 +4,10 @@ import { ForbiddenError, subject } from "@casl/ability";
import { ActionProjectType, TableName } from "@app/db/schemas";
import { TLicenseServiceFactory } from "@app/ee/services/license/license-service";
-import { CheckCanSecretsSubject, CheckForbiddenErrorSecretsSubject } from "@app/ee/services/permission/permission-fns";
+import {
+ hasSecretReadValueOrDescribePermission,
+ throwIfMissingSecretReadValueOrDescribePermission
+} from "@app/ee/services/permission/permission-fns";
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service";
import {
ProjectPermissionActions,
@@ -94,7 +97,7 @@ export const secretImportServiceFactory = ({
);
// check if user has permission to import from target path
- CheckForbiddenErrorSecretsSubject(permission, ProjectPermissionSecretActions.DescribeSecret, {
+ throwIfMissingSecretReadValueOrDescribePermission(permission, ProjectPermissionSecretActions.DescribeSecret, {
environment: data.environment,
secretPath: data.path
});
@@ -404,7 +407,7 @@ 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
- CheckForbiddenErrorSecretsSubject(permission, ProjectPermissionSecretActions.DescribeSecret, {
+ throwIfMissingSecretReadValueOrDescribePermission(permission, ProjectPermissionSecretActions.DescribeSecret, {
environment: secretImportDoc.importEnv.slug,
secretPath: secretImportDoc.importPath
});
@@ -595,7 +598,7 @@ 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) =>
- CheckCanSecretsSubject(permission, ProjectPermissionSecretActions.ReadValue, {
+ hasSecretReadValueOrDescribePermission(permission, ProjectPermissionSecretActions.ReadValue, {
environment: el.importEnv.slug,
secretPath: el.importPath
})
@@ -645,7 +648,7 @@ export const secretImportServiceFactory = ({
secretImportDAL,
decryptor: (value) => (value ? secretManagerDecryptor({ cipherTextBlob: value }).toString() : ""),
hasSecretAccess: (expandEnvironment, expandSecretPath, expandSecretKey, expandSecretTags) =>
- CheckCanSecretsSubject(permission, ProjectPermissionSecretActions.ReadValue, {
+ hasSecretReadValueOrDescribePermission(permission, ProjectPermissionSecretActions.ReadValue, {
environment: expandEnvironment,
secretPath: expandSecretPath,
secretName: expandSecretKey,
@@ -663,7 +666,7 @@ export const secretImportServiceFactory = ({
});
const allowedImports = secretImports.filter((el) =>
- CheckCanSecretsSubject(permission, ProjectPermissionSecretActions.ReadValue, {
+ hasSecretReadValueOrDescribePermission(permission, ProjectPermissionSecretActions.ReadValue, {
environment: el.importEnv.slug,
secretPath: el.importPath
})
diff --git a/backend/src/services/secret-sync/secret-sync-service.ts b/backend/src/services/secret-sync/secret-sync-service.ts
index d74ea120e..5c4a7e850 100644
--- a/backend/src/services/secret-sync/secret-sync-service.ts
+++ b/backend/src/services/secret-sync/secret-sync-service.ts
@@ -1,7 +1,7 @@
import { ForbiddenError } from "@casl/ability";
import { ActionProjectType } from "@app/db/schemas";
-import { CheckForbiddenErrorSecretsSubject } from "@app/ee/services/permission/permission-fns";
+import { throwIfMissingSecretReadValueOrDescribePermission } from "@app/ee/services/permission/permission-fns";
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service";
import {
ProjectPermissionSecretActions,
@@ -179,7 +179,7 @@ export const secretSyncServiceFactory = ({
ProjectPermissionSub.SecretSyncs
);
- CheckForbiddenErrorSecretsSubject(projectPermission, ProjectPermissionSecretActions.ReadValue, {
+ throwIfMissingSecretReadValueOrDescribePermission(projectPermission, ProjectPermissionSecretActions.ReadValue, {
environment,
secretPath
});
@@ -267,7 +267,7 @@ export const secretSyncServiceFactory = ({
if (!updatedEnvironment || !updatedSecretPath)
throw new BadRequestError({ message: "Must specify both source environment and secret path" });
- CheckForbiddenErrorSecretsSubject(permission, ProjectPermissionSecretActions.ReadValue, {
+ throwIfMissingSecretReadValueOrDescribePermission(permission, ProjectPermissionSecretActions.ReadValue, {
environment: updatedEnvironment,
secretPath: updatedSecretPath
});
diff --git a/backend/src/services/secret-v2-bridge/secret-v2-bridge-service.ts b/backend/src/services/secret-v2-bridge/secret-v2-bridge-service.ts
index 3b3a147bd..45c6493d6 100644
--- a/backend/src/services/secret-v2-bridge/secret-v2-bridge-service.ts
+++ b/backend/src/services/secret-v2-bridge/secret-v2-bridge-service.ts
@@ -10,7 +10,10 @@ import {
TableName,
TSecretsV2
} from "@app/db/schemas";
-import { CheckCanSecretsSubject, CheckForbiddenErrorSecretsSubject } from "@app/ee/services/permission/permission-fns";
+import {
+ hasSecretReadValueOrDescribePermission,
+ throwIfMissingSecretReadValueOrDescribePermission
+} from "@app/ee/services/permission/permission-fns";
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service";
import {
ProjectPermissionActions,
@@ -197,7 +200,7 @@ export const secretV2BridgeServiceFactory = ({
const referredSecretsGroupBySecretKey = groupBy(referredSecrets, (i) => i.key);
references.forEach((el) => {
- CheckForbiddenErrorSecretsSubject(permission, ProjectPermissionSecretActions.ReadValue, {
+ throwIfMissingSecretReadValueOrDescribePermission(permission, ProjectPermissionSecretActions.ReadValue, {
environment: el.environment,
secretPath: el.secretPath,
secretName: el.secretKey,
@@ -542,14 +545,18 @@ export const secretV2BridgeServiceFactory = ({
});
}
- const secretValueHidden = !CheckCanSecretsSubject(permission, ProjectPermissionSecretActions.ReadValue, {
- environment,
- secretPath,
- secretName: inputSecret.secretName,
- ...(tagsToCheck.length && {
- secretTags: tagsToCheck.map((el) => el.slug)
- })
- });
+ const secretValueHidden = !hasSecretReadValueOrDescribePermission(
+ permission,
+ ProjectPermissionSecretActions.ReadValue,
+ {
+ environment,
+ secretPath,
+ secretName: inputSecret.secretName,
+ ...(tagsToCheck.length && {
+ secretTags: tagsToCheck.map((el) => el.slug)
+ })
+ }
+ );
return reshapeBridgeSecret(
projectId,
@@ -650,12 +657,16 @@ export const secretV2BridgeServiceFactory = ({
projectId
});
- const secretValueHidden = !CheckCanSecretsSubject(permission, ProjectPermissionSecretActions.ReadValue, {
- environment,
- secretPath,
- secretName: secretToDelete.key,
- secretTags: secretToDelete.tags?.map((el) => el.slug)
- });
+ const secretValueHidden = !hasSecretReadValueOrDescribePermission(
+ permission,
+ ProjectPermissionSecretActions.ReadValue,
+ {
+ environment,
+ secretPath,
+ secretName: secretToDelete.key,
+ secretTags: secretToDelete.tags?.map((el) => el.slug)
+ }
+ );
return reshapeBridgeSecret(
projectId,
@@ -698,7 +709,7 @@ export const secretV2BridgeServiceFactory = ({
actorOrgId,
actionProjectType: ActionProjectType.SecretManager
});
- CheckForbiddenErrorSecretsSubject(permission, ProjectPermissionSecretActions.DescribeSecret);
+ throwIfMissingSecretReadValueOrDescribePermission(permission, ProjectPermissionSecretActions.DescribeSecret);
}
const folders = await folderDAL.findBySecretPathMultiEnv(projectId, environments, path);
@@ -744,7 +755,7 @@ export const secretV2BridgeServiceFactory = ({
actorOrgId,
actionProjectType: ActionProjectType.SecretManager
});
- CheckForbiddenErrorSecretsSubject(permission, ProjectPermissionSecretActions.DescribeSecret);
+ throwIfMissingSecretReadValueOrDescribePermission(permission, ProjectPermissionSecretActions.DescribeSecret);
const folder = await folderDAL.findBySecretPath(projectId, environment, path);
if (!folder) return 0;
@@ -779,37 +790,27 @@ export const secretV2BridgeServiceFactory = ({
});
const decryptedSecrets = secrets
- .filter((el) => {
- if (
- filterByAction === ProjectPermissionSecretActions.ReadValue ||
- filterByAction === ProjectPermissionSecretActions.DescribeSecret
- ) {
- return CheckCanSecretsSubject(projectPermission, filterByAction, {
- environment: groupedFolderMappings[el.folderId][0].environment,
- secretPath: groupedFolderMappings[el.folderId][0].path,
- secretName: el.key,
- secretTags: el.tags.map((i) => i.slug)
- });
- }
+ .filter((el) =>
+ hasSecretReadValueOrDescribePermission(projectPermission, filterByAction, {
+ environment: groupedFolderMappings[el.folderId][0].environment,
+ secretPath: groupedFolderMappings[el.folderId][0].path,
+ secretName: el.key,
+ secretTags: el.tags.map((i) => i.slug)
+ })
+ )
- return projectPermission.can(
- filterByAction,
- subject(ProjectPermissionSub.Secrets, {
- environment: groupedFolderMappings[el.folderId][0].environment,
- secretPath: groupedFolderMappings[el.folderId][0].path,
- secretName: el.key,
- secretTags: el.tags.map((i) => i.slug)
- })
- );
- })
.map((secret) => {
// Note(Daniel): This is only relevant if the filterAction isn't set to ReadValue. This is needed for the frontend.
- const secretValueHidden = !CheckCanSecretsSubject(projectPermission, ProjectPermissionSecretActions.ReadValue, {
- environment: groupedFolderMappings[secret.folderId][0].environment,
- secretPath: groupedFolderMappings[secret.folderId][0].path,
- secretName: secret.key,
- secretTags: secret.tags.map((i) => i.slug)
- });
+ const secretValueHidden = !hasSecretReadValueOrDescribePermission(
+ projectPermission,
+ ProjectPermissionSecretActions.ReadValue,
+ {
+ environment: groupedFolderMappings[secret.folderId][0].environment,
+ secretPath: groupedFolderMappings[secret.folderId][0].path,
+ secretName: secret.key,
+ secretTags: secret.tags.map((i) => i.slug)
+ }
+ );
return reshapeBridgeSecret(
projectId,
@@ -855,7 +856,7 @@ export const secretV2BridgeServiceFactory = ({
actionProjectType: ActionProjectType.SecretManager
});
if (!isInternal) {
- CheckForbiddenErrorSecretsSubject(permission, ProjectPermissionSecretActions.DescribeSecret);
+ throwIfMissingSecretReadValueOrDescribePermission(permission, ProjectPermissionSecretActions.DescribeSecret);
}
const folders = await folderDAL.findBySecretPathMultiEnv(projectId, environments, path);
@@ -907,7 +908,7 @@ export const secretV2BridgeServiceFactory = ({
actorOrgId,
actionProjectType: ActionProjectType.SecretManager
});
- CheckForbiddenErrorSecretsSubject(permission, ProjectPermissionSecretActions.DescribeSecret, {
+ throwIfMissingSecretReadValueOrDescribePermission(permission, ProjectPermissionSecretActions.DescribeSecret, {
environment,
secretPath: path,
secretTags: params.tagSlugs
@@ -950,12 +951,16 @@ export const secretV2BridgeServiceFactory = ({
const decryptedSecrets = secrets
.filter((el) => {
- const canDescribeSecret = CheckCanSecretsSubject(permission, ProjectPermissionSecretActions.DescribeSecret, {
- environment,
- secretPath: groupedPaths[el.folderId][0].path,
- secretName: el.key,
- secretTags: el.tags.map((i) => i.slug)
- });
+ const canDescribeSecret = hasSecretReadValueOrDescribePermission(
+ permission,
+ ProjectPermissionSecretActions.DescribeSecret,
+ {
+ environment,
+ secretPath: groupedPaths[el.folderId][0].path,
+ secretName: el.key,
+ secretTags: el.tags.map((i) => i.slug)
+ }
+ );
if (!canDescribeSecret) {
return false;
@@ -964,7 +969,7 @@ export const secretV2BridgeServiceFactory = ({
if (viewSecretValue) {
// Recursive secret, should be filtered out
if (groupedPaths[el.folderId][0].path !== path) {
- const canReadRecursiveSecretValue = CheckCanSecretsSubject(
+ const canReadRecursiveSecretValue = hasSecretReadValueOrDescribePermission(
permission,
ProjectPermissionSecretActions.ReadValue,
{
@@ -981,7 +986,7 @@ export const secretV2BridgeServiceFactory = ({
}
if (throwOnMissingReadValuePermission) {
- CheckForbiddenErrorSecretsSubject(permission, ProjectPermissionSecretActions.ReadValue, {
+ throwIfMissingSecretReadValueOrDescribePermission(permission, ProjectPermissionSecretActions.ReadValue, {
environment,
secretPath: groupedPaths[el.folderId][0].path,
secretName: el.key,
@@ -999,7 +1004,7 @@ export const secretV2BridgeServiceFactory = ({
const secretValueHidden =
!viewSecretValue ||
- !CheckCanSecretsSubject(permission, ProjectPermissionSecretActions.ReadValue, {
+ !hasSecretReadValueOrDescribePermission(permission, ProjectPermissionSecretActions.ReadValue, {
environment,
secretPath: groupedPaths[secret.folderId][0].path,
secretName: secret.key,
@@ -1029,7 +1034,7 @@ export const secretV2BridgeServiceFactory = ({
secretDAL,
decryptSecretValue: (value) => (value ? secretManagerDecryptor({ cipherTextBlob: value }).toString() : undefined),
canExpandValue: (expandEnvironment, expandSecretPath, expandSecretKey, expandSecretTags) =>
- CheckCanSecretsSubject(permission, ProjectPermissionSecretActions.ReadValue, {
+ hasSecretReadValueOrDescribePermission(permission, ProjectPermissionSecretActions.ReadValue, {
environment: expandEnvironment,
secretPath: expandSecretPath,
secretName: expandSecretKey,
@@ -1074,19 +1079,27 @@ export const secretV2BridgeServiceFactory = ({
expandSecretReferences,
decryptor: (value) => (value ? secretManagerDecryptor({ cipherTextBlob: value }).toString() : ""),
hasSecretAccess: (expandEnvironment, expandSecretPath, expandSecretKey, expandSecretTags) => {
- const canDescribe = CheckCanSecretsSubject(permission, ProjectPermissionSecretActions.DescribeSecret, {
- environment: expandEnvironment,
- secretPath: expandSecretPath,
- secretName: expandSecretKey,
- secretTags: expandSecretTags
- });
+ const canDescribe = hasSecretReadValueOrDescribePermission(
+ permission,
+ ProjectPermissionSecretActions.DescribeSecret,
+ {
+ environment: expandEnvironment,
+ secretPath: expandSecretPath,
+ secretName: expandSecretKey,
+ secretTags: expandSecretTags
+ }
+ );
- const canReadValue = CheckCanSecretsSubject(permission, ProjectPermissionSecretActions.ReadValue, {
- environment: expandEnvironment,
- secretPath: expandSecretPath,
- secretName: expandSecretKey,
- secretTags: expandSecretTags
- });
+ const canReadValue = hasSecretReadValueOrDescribePermission(
+ permission,
+ ProjectPermissionSecretActions.ReadValue,
+ {
+ environment: expandEnvironment,
+ secretPath: expandSecretPath,
+ secretName: expandSecretKey,
+ secretTags: expandSecretTags
+ }
+ );
return viewSecretValue ? canDescribe && canReadValue : canDescribe;
}
@@ -1128,7 +1141,7 @@ export const secretV2BridgeServiceFactory = ({
actionProjectType: ActionProjectType.SecretManager
});
- CheckForbiddenErrorSecretsSubject(permission, ProjectPermissionSecretActions.ReadValue, {
+ throwIfMissingSecretReadValueOrDescribePermission(permission, ProjectPermissionSecretActions.ReadValue, {
environment: folderWithPath.environmentSlug,
secretPath: folderWithPath.path,
secretName: secret.key,
@@ -1240,7 +1253,7 @@ export const secretV2BridgeServiceFactory = ({
})
));
- CheckForbiddenErrorSecretsSubject(permission, ProjectPermissionSecretActions.DescribeSecret, {
+ throwIfMissingSecretReadValueOrDescribePermission(permission, ProjectPermissionSecretActions.DescribeSecret, {
environment,
secretPath: path,
secretName,
@@ -1255,7 +1268,7 @@ export const secretV2BridgeServiceFactory = ({
secretDAL,
decryptSecretValue: (value) => (value ? secretManagerDecryptor({ cipherTextBlob: value }).toString() : undefined),
canExpandValue: (expandEnvironment, expandSecretPath, expandSecretKey, expandSecretTags) => {
- return CheckCanSecretsSubject(permission, ProjectPermissionSecretActions.ReadValue, {
+ return hasSecretReadValueOrDescribePermission(permission, ProjectPermissionSecretActions.ReadValue, {
environment: expandEnvironment,
secretPath: expandSecretPath,
secretName: expandSecretKey,
@@ -1280,7 +1293,7 @@ export const secretV2BridgeServiceFactory = ({
decryptor: (value) => (value ? secretManagerDecryptor({ cipherTextBlob: value }).toString() : ""),
expandSecretReferences: shouldExpandSecretReferences ? expandSecretReferences : undefined,
hasSecretAccess: (expandEnvironment, expandSecretPath, expandSecretKey, expandSecretTags) => {
- return CheckCanSecretsSubject(permission, ProjectPermissionSecretActions.DescribeSecret, {
+ return hasSecretReadValueOrDescribePermission(permission, ProjectPermissionSecretActions.DescribeSecret, {
environment: expandEnvironment,
secretPath: expandSecretPath,
secretName: expandSecretKey,
@@ -1297,7 +1310,7 @@ export const secretV2BridgeServiceFactory = ({
if (viewSecretValue) {
if (
- !CheckCanSecretsSubject(permission, ProjectPermissionSecretActions.ReadValue, {
+ !hasSecretReadValueOrDescribePermission(permission, ProjectPermissionSecretActions.ReadValue, {
environment: importedSecret.environment,
secretPath: importedSecrets[i].secretPath,
secretName: importedSecret.key,
@@ -1350,7 +1363,7 @@ export const secretV2BridgeServiceFactory = ({
if (viewSecretValue) {
if (
- !CheckCanSecretsSubject(permission, ProjectPermissionSecretActions.ReadValue, {
+ !hasSecretReadValueOrDescribePermission(permission, ProjectPermissionSecretActions.ReadValue, {
environment,
secretPath: path,
secretName,
@@ -1523,12 +1536,16 @@ export const secretV2BridgeServiceFactory = ({
});
return newSecrets.map((el) => {
- const secretValueHidden = !CheckCanSecretsSubject(permission, ProjectPermissionSecretActions.ReadValue, {
- environment,
- secretPath,
- secretName: el.key,
- secretTags: el.tags?.map((i) => i.slug)
- });
+ const secretValueHidden = !hasSecretReadValueOrDescribePermission(
+ permission,
+ ProjectPermissionSecretActions.ReadValue,
+ {
+ environment,
+ secretPath,
+ secretName: el.key,
+ secretTags: el.tags?.map((i) => i.slug)
+ }
+ );
return reshapeBridgeSecret(
projectId,
@@ -1857,12 +1874,16 @@ export const secretV2BridgeServiceFactory = ({
);
return updatedSecrets.map((el) => {
- const secretValueHidden = !CheckCanSecretsSubject(permission, ProjectPermissionSecretActions.ReadValue, {
- environment,
- secretPath: el.secretPath,
- secretName: el.key,
- secretTags: el.tags.map((i) => i.slug)
- });
+ const secretValueHidden = !hasSecretReadValueOrDescribePermission(
+ permission,
+ ProjectPermissionSecretActions.ReadValue,
+ {
+ environment,
+ secretPath: el.secretPath,
+ secretName: el.key,
+ secretTags: el.tags.map((i) => i.slug)
+ }
+ );
return {
...reshapeBridgeSecret(
@@ -1987,7 +2008,7 @@ export const secretV2BridgeServiceFactory = ({
const secretValueHidden =
!secretToDeleteMatch ||
- !CheckCanSecretsSubject(permission, ProjectPermissionSecretActions.ReadValue, {
+ !hasSecretReadValueOrDescribePermission(permission, ProjectPermissionSecretActions.ReadValue, {
environment,
secretPath,
secretName: el.key,
@@ -2049,14 +2070,18 @@ export const secretV2BridgeServiceFactory = ({
sort: [["createdAt", "desc"]]
});
return secretVersions.map((el) => {
- const secretValueHidden = !CheckCanSecretsSubject(permission, ProjectPermissionSecretActions.ReadValue, {
- environment: folder.environment.envSlug,
- secretPath: folderWithPath.path,
- secretName: el.key,
- ...(el.tags?.length && {
- secretTags: el.tags.map((tag) => tag.slug)
- })
- });
+ const secretValueHidden = !hasSecretReadValueOrDescribePermission(
+ permission,
+ ProjectPermissionSecretActions.ReadValue,
+ {
+ environment: folder.environment.envSlug,
+ secretPath: folderWithPath.path,
+ secretName: el.key,
+ ...(el.tags?.length && {
+ secretTags: el.tags.map((tag) => tag.slug)
+ })
+ }
+ );
return reshapeBridgeSecret(
folder.projectId,
@@ -2178,7 +2203,7 @@ export const secretV2BridgeServiceFactory = ({
sourceAction === ProjectPermissionSecretActions.DescribeSecret ||
sourceAction === ProjectPermissionSecretActions.ReadValue
) {
- CheckForbiddenErrorSecretsSubject(permission, sourceAction, {
+ throwIfMissingSecretReadValueOrDescribePermission(permission, sourceAction, {
environment: sourceEnvironment,
secretPath: sourceSecretPath,
secretName: secret.key,
@@ -2517,7 +2542,7 @@ export const secretV2BridgeServiceFactory = ({
actionProjectType: ActionProjectType.SecretManager
});
- CheckForbiddenErrorSecretsSubject(permission, ProjectPermissionSecretActions.DescribeSecret, {
+ throwIfMissingSecretReadValueOrDescribePermission(permission, ProjectPermissionSecretActions.DescribeSecret, {
environment,
secretPath
});
@@ -2541,7 +2566,7 @@ export const secretV2BridgeServiceFactory = ({
type: SecretType.Shared
});
- CheckForbiddenErrorSecretsSubject(permission, ProjectPermissionSecretActions.DescribeSecret, {
+ throwIfMissingSecretReadValueOrDescribePermission(permission, ProjectPermissionSecretActions.DescribeSecret, {
environment,
secretPath,
secretName,
@@ -2558,7 +2583,7 @@ export const secretV2BridgeServiceFactory = ({
secretDAL,
decryptSecretValue: (value) => (value ? secretManagerDecryptor({ cipherTextBlob: value }).toString() : undefined),
canExpandValue: (expandEnvironment, expandSecretPath, expandSecretName, expandSecretTags) =>
- CheckCanSecretsSubject(permission, ProjectPermissionSecretActions.ReadValue, {
+ hasSecretReadValueOrDescribePermission(permission, ProjectPermissionSecretActions.ReadValue, {
environment: expandEnvironment,
secretPath: expandSecretPath,
secretName: expandSecretName,
@@ -2567,7 +2592,7 @@ export const secretV2BridgeServiceFactory = ({
});
if (
- !CheckCanSecretsSubject(permission, ProjectPermissionSecretActions.ReadValue, {
+ !hasSecretReadValueOrDescribePermission(permission, ProjectPermissionSecretActions.ReadValue, {
environment,
secretPath,
secretName,
diff --git a/backend/src/services/secret-v2-bridge/secret-v2-bridge-types.ts b/backend/src/services/secret-v2-bridge/secret-v2-bridge-types.ts
index 510181d0c..473d1ed83 100644
--- a/backend/src/services/secret-v2-bridge/secret-v2-bridge-types.ts
+++ b/backend/src/services/secret-v2-bridge/secret-v2-bridge-types.ts
@@ -349,5 +349,5 @@ export type TGetSecretsRawByFolderMappingsDTO = {
folderMappings: { folderId: string; path: string; environment: string }[];
userId: string;
filters: TFindSecretsByFolderIdsFilter;
- filterByAction?: ProjectPermissionSecretActions;
+ filterByAction?: ProjectPermissionSecretActions.DescribeSecret | ProjectPermissionSecretActions.ReadValue;
};
diff --git a/backend/src/services/secret/secret-fns.ts b/backend/src/services/secret/secret-fns.ts
index ddaa054ab..aa8cec2bc 100644
--- a/backend/src/services/secret/secret-fns.ts
+++ b/backend/src/services/secret/secret-fns.ts
@@ -11,7 +11,7 @@ import {
TSecretFolders,
TSecrets
} from "@app/db/schemas";
-import { CheckCanSecretsSubject } from "@app/ee/services/permission/permission-fns";
+import { hasSecretReadValueOrDescribePermission } from "@app/ee/services/permission/permission-fns";
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service";
import { ProjectPermissionSecretActions } from "@app/ee/services/permission/project-permission";
import { getConfig } from "@app/lib/config/env";
@@ -191,7 +191,7 @@ 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) =>
- CheckCanSecretsSubject(permission, ProjectPermissionSecretActions.ReadValue, {
+ hasSecretReadValueOrDescribePermission(permission, ProjectPermissionSecretActions.ReadValue, {
environment,
secretPath: folder.path
}) && folder.path.startsWith(currentPath === "/" ? "" : currentPath)
diff --git a/backend/src/services/secret/secret-service.ts b/backend/src/services/secret/secret-service.ts
index e3eb75bda..3593adf88 100644
--- a/backend/src/services/secret/secret-service.ts
+++ b/backend/src/services/secret/secret-service.ts
@@ -13,7 +13,10 @@ import {
SecretType
} from "@app/db/schemas";
import { TLicenseServiceFactory } from "@app/ee/services/license/license-service";
-import { CheckCanSecretsSubject, CheckForbiddenErrorSecretsSubject } from "@app/ee/services/permission/permission-fns";
+import {
+ hasSecretReadValueOrDescribePermission,
+ throwIfMissingSecretReadValueOrDescribePermission
+} from "@app/ee/services/permission/permission-fns";
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service";
import {
ProjectPermissionActions,
@@ -453,10 +456,14 @@ export const secretServiceFactory = ({
});
}
- const secretValueHidden = !CheckCanSecretsSubject(permission, ProjectPermissionSecretActions.ReadValue, {
- environment,
- secretPath: path
- });
+ const secretValueHidden = !hasSecretReadValueOrDescribePermission(
+ permission,
+ ProjectPermissionSecretActions.ReadValue,
+ {
+ environment,
+ secretPath: path
+ }
+ );
return {
...updatedSecret[0],
@@ -560,10 +567,14 @@ export const secretServiceFactory = ({
});
}
- const secretValueHidden = !CheckCanSecretsSubject(permission, ProjectPermissionSecretActions.ReadValue, {
- environment,
- secretPath: path
- });
+ const secretValueHidden = !hasSecretReadValueOrDescribePermission(
+ permission,
+ ProjectPermissionSecretActions.ReadValue,
+ {
+ environment,
+ secretPath: path
+ }
+ );
return {
...deletedSecret[0],
@@ -620,7 +631,7 @@ export const secretServiceFactory = ({
paths = deepPaths.map(({ folderId, path: p }) => ({ folderId, path: p }));
} else {
- CheckForbiddenErrorSecretsSubject(permission, ProjectPermissionSecretActions.ReadValue, {
+ throwIfMissingSecretReadValueOrDescribePermission(permission, ProjectPermissionSecretActions.ReadValue, {
environment,
secretPath: path
});
@@ -645,7 +656,7 @@ export const secretServiceFactory = ({
// if its service token allow full access over imported one
actor === ActorType.SERVICE
? true
- : CheckCanSecretsSubject(permission, ProjectPermissionSecretActions.ReadValue, {
+ : hasSecretReadValueOrDescribePermission(permission, ProjectPermissionSecretActions.ReadValue, {
environment: importEnv.slug,
secretPath: importPath
})
@@ -699,7 +710,7 @@ export const secretServiceFactory = ({
actorOrgId,
actionProjectType: ActionProjectType.SecretManager
});
- CheckForbiddenErrorSecretsSubject(permission, ProjectPermissionSecretActions.ReadValue, {
+ throwIfMissingSecretReadValueOrDescribePermission(permission, ProjectPermissionSecretActions.ReadValue, {
environment,
secretPath: path
});
@@ -750,7 +761,7 @@ export const secretServiceFactory = ({
// if its service token allow full access over imported one
actor === ActorType.SERVICE
? true
- : CheckCanSecretsSubject(permission, ProjectPermissionSecretActions.ReadValue, {
+ : hasSecretReadValueOrDescribePermission(permission, ProjectPermissionSecretActions.ReadValue, {
environment: importEnv.slug,
secretPath: importPath
})
@@ -969,10 +980,14 @@ export const secretServiceFactory = ({
secretVersionTagDAL
});
- const secretValueHidden = !CheckCanSecretsSubject(permission, ProjectPermissionSecretActions.ReadValue, {
- environment,
- secretPath: path
- });
+ const secretValueHidden = !hasSecretReadValueOrDescribePermission(
+ permission,
+ ProjectPermissionSecretActions.ReadValue,
+ {
+ environment,
+ secretPath: path
+ }
+ );
return updatedSecrets.map((secret) => ({
...secret,
@@ -1063,10 +1078,14 @@ export const secretServiceFactory = ({
});
}
}
- const secretValueHidden = !CheckCanSecretsSubject(permission, ProjectPermissionSecretActions.ReadValue, {
- environment,
- secretPath: path
- });
+ const secretValueHidden = !hasSecretReadValueOrDescribePermission(
+ permission,
+ ProjectPermissionSecretActions.ReadValue,
+ {
+ environment,
+ secretPath: path
+ }
+ );
return secrets.map((secret) => ({
...secret,
@@ -1257,7 +1276,7 @@ export const secretServiceFactory = ({
action === ProjectPermissionSecretActions.DescribeSecret ||
action === ProjectPermissionSecretActions.ReadValue
) {
- return CheckCanSecretsSubject(entityPermission.permission, action, {
+ return hasSecretReadValueOrDescribePermission(entityPermission.permission, action, {
environment,
secretPath,
secretName,
@@ -2429,14 +2448,18 @@ export const secretServiceFactory = ({
key: botKey
});
- const secretValueHidden = !CheckCanSecretsSubject(permission, ProjectPermissionSecretActions.ReadValue, {
- environment: folder.environment.envSlug,
- secretPath: folderWithPath.path,
- secretName: secretKey,
- ...(el.tags?.length && {
- secretTags: el.tags.map((tag) => tag.slug)
- })
- });
+ 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(
{
@@ -2839,7 +2862,7 @@ export const secretServiceFactory = ({
sourceAction === ProjectPermissionSecretActions.ReadValue ||
sourceAction === ProjectPermissionSecretActions.DescribeSecret
) {
- CheckForbiddenErrorSecretsSubject(permission, sourceAction, {
+ throwIfMissingSecretReadValueOrDescribePermission(permission, sourceAction, {
environment: sourceEnvironment,
secretPath: sourceSecretPath
});
diff --git a/frontend/src/lib/fn/permission.ts b/frontend/src/lib/fn/permission.ts
index f2077d76b..c3937a0ff 100644
--- a/frontend/src/lib/fn/permission.ts
+++ b/frontend/src/lib/fn/permission.ts
@@ -7,7 +7,7 @@ import {
SecretSubjectFields
} from "@app/context/ProjectPermissionContext/types";
-export function secretsPermissionCan(
+export function hasSecretReadValueOrDescribePermission(
permission: MongoAbility,
action: Extract<
ProjectPermissionSecretActions,
diff --git a/frontend/src/pages/project/RoleDetailsBySlugPage/components/ProjectRoleModifySection.utils.tsx b/frontend/src/pages/project/RoleDetailsBySlugPage/components/ProjectRoleModifySection.utils.tsx
index 9f13b53a8..a47a64614 100644
--- a/frontend/src/pages/project/RoleDetailsBySlugPage/components/ProjectRoleModifySection.utils.tsx
+++ b/frontend/src/pages/project/RoleDetailsBySlugPage/components/ProjectRoleModifySection.utils.tsx
@@ -29,11 +29,11 @@ const GeneralPolicyActionSchema = z.object({
const SecretPolicyActionSchema = z.object({
[ProjectPermissionSecretActions.DescribeAndReadValue]: z.boolean().optional(), // existing read, gives both describe and read value
- [ProjectPermissionSecretActions.DescribeSecret]: z.boolean().optional(), // describe secret, cannot read value
- [ProjectPermissionSecretActions.ReadValue]: z.boolean().optional(), // read value
- [ProjectPermissionSecretActions.Edit]: z.boolean().optional(), // edit secret
- [ProjectPermissionSecretActions.Delete]: z.boolean().optional(), // delete secret
- [ProjectPermissionSecretActions.Create]: z.boolean().optional() // create secret
+ [ProjectPermissionSecretActions.DescribeSecret]: z.boolean().optional(),
+ [ProjectPermissionSecretActions.ReadValue]: z.boolean().optional(),
+ [ProjectPermissionSecretActions.Edit]: z.boolean().optional(),
+ [ProjectPermissionSecretActions.Delete]: z.boolean().optional(),
+ [ProjectPermissionSecretActions.Create]: z.boolean().optional()
});
const CmekPolicyActionSchema = z.object({
diff --git a/frontend/src/pages/secret-manager/OverviewPage/components/SecretOverviewTableRow/SecretRenameRow.tsx b/frontend/src/pages/secret-manager/OverviewPage/components/SecretOverviewTableRow/SecretRenameRow.tsx
index 536f75542..ce2e43f67 100644
--- a/frontend/src/pages/secret-manager/OverviewPage/components/SecretOverviewTableRow/SecretRenameRow.tsx
+++ b/frontend/src/pages/secret-manager/OverviewPage/components/SecretOverviewTableRow/SecretRenameRow.tsx
@@ -15,7 +15,7 @@ import { ProjectPermissionSecretActions } from "@app/context/ProjectPermissionCo
import { useToggle } from "@app/hooks";
import { useUpdateSecretV3 } from "@app/hooks/api";
import { SecretType, SecretV3RawSanitized } from "@app/hooks/api/types";
-import { secretsPermissionCan } from "@app/lib/fn/permission";
+import { hasSecretReadValueOrDescribePermission } from "@app/lib/fn/permission";
enum SecretActionType {
Created = "created",
@@ -52,7 +52,7 @@ function SecretRenameRow({ environments, getSecretByKey, secretKey, secretPath }
secretTags: (secretDetails?.tags || []).map((i) => i.slug)
});
const isSecretInEnvReadOnly =
- secretsPermissionCan(
+ hasSecretReadValueOrDescribePermission(
permission,
ProjectPermissionSecretActions.DescribeSecret,
secretPermissionSubject
diff --git a/frontend/src/pages/secret-manager/SecretDashboardPage/SecretDashboardPage.tsx b/frontend/src/pages/secret-manager/SecretDashboardPage/SecretDashboardPage.tsx
index 4dba1c2a8..e6fc5eadf 100644
--- a/frontend/src/pages/secret-manager/SecretDashboardPage/SecretDashboardPage.tsx
+++ b/frontend/src/pages/secret-manager/SecretDashboardPage/SecretDashboardPage.tsx
@@ -38,7 +38,7 @@ import { useGetProjectSecretsDetails } from "@app/hooks/api/dashboard";
import { DashboardSecretsOrderBy } from "@app/hooks/api/dashboard/types";
import { OrderByDirection } from "@app/hooks/api/generic/types";
import { ProjectType } from "@app/hooks/api/workspace/types";
-import { secretsPermissionCan } from "@app/lib/fn/permission";
+import { hasSecretReadValueOrDescribePermission } from "@app/lib/fn/permission";
import { SecretTableResourceCount } from "../OverviewPage/components/SecretTableResourceCount";
import { SecretV2MigrationSection } from "../OverviewPage/components/SecretV2MigrationSection";
@@ -105,7 +105,7 @@ const Page = () => {
const projectSlug = currentWorkspace?.slug || "";
const secretPath = (routerQueryParams.secretPath as string) || "/";
- const canReadSecret = secretsPermissionCan(
+ const canReadSecret = hasSecretReadValueOrDescribePermission(
permission,
ProjectPermissionSecretActions.DescribeSecret,
{
@@ -116,7 +116,7 @@ const Page = () => {
}
);
- const canReadSecretValue = secretsPermissionCan(
+ const canReadSecretValue = hasSecretReadValueOrDescribePermission(
permission,
ProjectPermissionSecretActions.ReadValue,
{
diff --git a/frontend/src/pages/secret-manager/SecretDashboardPage/components/SecretListView/SecretDetailSidebar.tsx b/frontend/src/pages/secret-manager/SecretDashboardPage/components/SecretListView/SecretDetailSidebar.tsx
index 3ad7159d7..410d49e5f 100644
--- a/frontend/src/pages/secret-manager/SecretDashboardPage/components/SecretListView/SecretDetailSidebar.tsx
+++ b/frontend/src/pages/secret-manager/SecretDashboardPage/components/SecretListView/SecretDetailSidebar.tsx
@@ -57,7 +57,7 @@ import { ActorType } from "@app/hooks/api/auditLogs/enums";
import { useGetSecretAccessList } from "@app/hooks/api/secrets/queries";
import { SecretV3RawSanitized, WsTag } from "@app/hooks/api/types";
import { ProjectType } from "@app/hooks/api/workspace/types";
-import { secretsPermissionCan } from "@app/lib/fn/permission";
+import { hasSecretReadValueOrDescribePermission } from "@app/lib/fn/permission";
import { CreateReminderForm } from "./CreateReminderForm";
import { formSchema, SecretActionType, TFormSchema } from "./SecretListView.utils";
@@ -141,7 +141,7 @@ export const SecretDetailSidebar = ({
})
);
- const cannotReadSecretValue = !secretsPermissionCan(
+ const cannotReadSecretValue = !hasSecretReadValueOrDescribePermission(
permission,
ProjectPermissionSecretActions.ReadValue,
{
@@ -153,12 +153,16 @@ export const SecretDetailSidebar = ({
);
const isReadOnly =
- secretsPermissionCan(permission, ProjectPermissionSecretActions.DescribeSecret, {
- environment,
- secretPath,
- secretName: secretKey,
- secretTags: selectTagSlugs
- }) &&
+ hasSecretReadValueOrDescribePermission(
+ permission,
+ ProjectPermissionSecretActions.DescribeSecret,
+ {
+ environment,
+ secretPath,
+ secretName: secretKey,
+ secretTags: selectTagSlugs
+ }
+ ) &&
cannotEditSecret &&
cannotReadSecretValue;
@@ -361,11 +365,11 @@ export const SecretDetailSidebar = ({
>
Date: Tue, 11 Mar 2025 00:33:11 +0400
Subject: [PATCH 25/71] Update go.sum
---
cli/go.sum | 8 +++-----
1 file changed, 3 insertions(+), 5 deletions(-)
diff --git a/cli/go.sum b/cli/go.sum
index d209fd1ca..b72858e9d 100644
--- a/cli/go.sum
+++ b/cli/go.sum
@@ -367,8 +367,6 @@ github.com/pion/stun/v3 v3.0.0 h1:4h1gwhWLWuZWOJIJR9s2ferRO+W3zA/b6ijOI6mKzUw=
github.com/pion/stun/v3 v3.0.0/go.mod h1:HvCN8txt8mwi4FBvS3EmDghW6aQJ24T+y+1TKjB5jyU=
github.com/pion/transport/v3 v3.0.7 h1:iRbMH05BzSNwhILHoBoAPxoB9xQgOaJk+591KC9P1o0=
github.com/pion/transport/v3 v3.0.7/go.mod h1:YleKiTZ4vqNxVwh77Z0zytYi7rXHl7j6uPLGhhz9rwo=
-github.com/pion/turn/v4 v4.0.0 h1:qxplo3Rxa9Yg1xXDxxH8xaqcyGUtbHYw4QSCvmFWvhM=
-github.com/pion/turn/v4 v4.0.0/go.mod h1:MuPDkm15nYSklKpN8vWJ9W2M0PlyQZqYt1McGuxG7mA=
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
@@ -427,8 +425,8 @@ github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
-github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
-github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
+github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
+github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
github.com/subosito/gotenv v1.2.0 h1:Slr1R9HxAlEKefgq5jn9U+DnETlIUa6HfgEzj0g5d7s=
github.com/subosito/gotenv v1.2.0/go.mod h1:N0PQaV/YGNqwC0u51sEeR/aUtSLEXKX9iv69rRypqCw=
github.com/tidwall/pretty v1.0.0 h1:HsD+QiTn7sK6flMKIvNmpqz1qrpP3Ps6jOKIKMooyg4=
@@ -870,4 +868,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=
\ No newline at end of file
From 4eff7d8ea54237e6cb3bb11fb8c8d2efc5c94012 Mon Sep 17 00:00:00 2001
From: Mahyar Mirrashed
Date: Mon, 10 Mar 2025 11:16:19 -0700
Subject: [PATCH 26/71] fix(audit): change dd-mm-yy to mm-dd-yy
---
.../organization/AuditLogsPage/components/LogsTableRow.tsx | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/frontend/src/pages/organization/AuditLogsPage/components/LogsTableRow.tsx b/frontend/src/pages/organization/AuditLogsPage/components/LogsTableRow.tsx
index 5c3337938..cc67e5319 100644
--- a/frontend/src/pages/organization/AuditLogsPage/components/LogsTableRow.tsx
+++ b/frontend/src/pages/organization/AuditLogsPage/components/LogsTableRow.tsx
@@ -83,7 +83,7 @@ export const LogsTableRow = ({ auditLog, isOrgAuditLogs, showActorColumn }: Prop
hours %= 12;
hours = hours || 12; // the hour '0' should be '12'
- const formattedDate = `${day}-${month}-${year} at ${hours}:${minutes} ${period}`;
+ const formattedDate = `${month}-${day}-${year} at ${hours}:${minutes} ${period}`;
return formattedDate;
};
From ace2500885c0e1544a14f371b05eb1f8f86d1d4e Mon Sep 17 00:00:00 2001
From: Mahyar Mirrashed
Date: Mon, 10 Mar 2025 14:29:01 -0700
Subject: [PATCH 27/71] feat(audit): add timestamp format to column header
---
.../pages/organization/AuditLogsPage/components/LogsTable.tsx | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/frontend/src/pages/organization/AuditLogsPage/components/LogsTable.tsx b/frontend/src/pages/organization/AuditLogsPage/components/LogsTable.tsx
index f37482548..a931cb606 100644
--- a/frontend/src/pages/organization/AuditLogsPage/components/LogsTable.tsx
+++ b/frontend/src/pages/organization/AuditLogsPage/components/LogsTable.tsx
@@ -28,7 +28,7 @@ type Props = {
const AUDIT_LOG_LIMIT = 15;
-const TABLE_HEADERS = ["Timestamp", "Event", "Project", "Actor", "Source", "Metadata"] as const;
+const TABLE_HEADERS = ["Timestamp (MM/DD/YYYY)", "Event", "Project", "Actor", "Source", "Metadata"] as const;
export type TAuditLogTableHeader = (typeof TABLE_HEADERS)[number];
export const LogsTable = ({
From b9b76579ac14a3b63cdb2b0e0bb255d138073f61 Mon Sep 17 00:00:00 2001
From: Daniel Hougaard
Date: Tue, 11 Mar 2025 02:07:38 +0400
Subject: [PATCH 28/71] requested changes
---
.../src/ee/routes/v1/user-additional-privilege-router.ts | 8 ++++++--
.../v2/identity-project-additional-privilege-router.ts | 8 ++++++--
backend/src/ee/routes/v2/project-role-router.ts | 4 +++-
backend/src/ee/services/permission/permission-fns.ts | 5 ++++-
4 files changed, 19 insertions(+), 6 deletions(-)
diff --git a/backend/src/ee/routes/v1/user-additional-privilege-router.ts b/backend/src/ee/routes/v1/user-additional-privilege-router.ts
index de37a4cde..f8b4e922c 100644
--- a/backend/src/ee/routes/v1/user-additional-privilege-router.ts
+++ b/backend/src/ee/routes/v1/user-additional-privilege-router.ts
@@ -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({
diff --git a/backend/src/ee/routes/v2/identity-project-additional-privilege-router.ts b/backend/src/ee/routes/v2/identity-project-additional-privilege-router.ts
index d9c3a05b5..7cc9155cd 100644
--- a/backend/src/ee/routes/v2/identity-project-additional-privilege-router.ts
+++ b/backend/src/ee/routes/v2/identity-project-additional-privilege-router.ts
@@ -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({
diff --git a/backend/src/ee/routes/v2/project-role-router.ts b/backend/src/ee/routes/v2/project-role-router.ts
index c3b3c11f2..0bb83b8d4 100644
--- a/backend/src/ee/routes/v2/project-role-router.ts
+++ b/backend/src/ee/routes/v2/project-role-router.ts
@@ -38,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({
diff --git a/backend/src/ee/services/permission/permission-fns.ts b/backend/src/ee/services/permission/permission-fns.ts
index ecc8b676e..9397df939 100644
--- a/backend/src/ee/services/permission/permission-fns.ts
+++ b/backend/src/ee/services/permission/permission-fns.ts
@@ -81,7 +81,8 @@ export function checkForInvalidPermissionCombination(permissions: z.infer
Date: Mon, 10 Mar 2025 16:26:18 -0700
Subject: [PATCH 29/71] chore(nix): add support for flakes
---
.envrc | 1 +
flake.lock | 27 +++++++++++++++++++++++++++
flake.nix | 35 +++++++++++++++++++++++++++++++++++
3 files changed, 63 insertions(+)
create mode 100644 .envrc
create mode 100644 flake.lock
create mode 100644 flake.nix
diff --git a/.envrc b/.envrc
new file mode 100644
index 000000000..3550a30f2
--- /dev/null
+++ b/.envrc
@@ -0,0 +1 @@
+use flake
diff --git a/flake.lock b/flake.lock
new file mode 100644
index 000000000..3104f340d
--- /dev/null
+++ b/flake.lock
@@ -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
+}
diff --git a/flake.nix b/flake.nix
new file mode 100644
index 000000000..ffed5545c
--- /dev/null
+++ b/flake.nix
@@ -0,0 +1,35 @@
+{
+ 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
+
+ nodejs_20
+ ];
+
+ shellHook = ''
+ # Define a writable directory for global npm packages
+ export NPM_CONFIG_PREFIX="$HOME/.npm-global"
+ export PATH="$NPM_CONFIG_PREFIX/bin:$PATH"
+
+ # Ensure the directory exists
+ mkdir -p "$NPM_CONFIG_PREFIX"
+
+ # Install Infisical CLI only if it's not already installed
+ if ! command -v infisical &>/dev/null; then
+ npm install -g @infisical/cli
+ fi
+ '';
+ };
+ };
+}
From 17cf602a65b3c21a5fa6f9e05a22a7ab6642364f Mon Sep 17 00:00:00 2001
From: Mahyar Mirrashed
Date: Mon, 10 Mar 2025 16:26:39 -0700
Subject: [PATCH 30/71] style: remove blank line
---
backend/src/server/routes/v1/index.ts | 1 -
1 file changed, 1 deletion(-)
diff --git a/backend/src/server/routes/v1/index.ts b/backend/src/server/routes/v1/index.ts
index 087bd9afd..50fd33840 100644
--- a/backend/src/server/routes/v1/index.ts
+++ b/backend/src/server/routes/v1/index.ts
@@ -91,7 +91,6 @@ export const registerV1Routes = async (server: FastifyZodProvider) => {
await projectRouter.register(registerProjectMembershipRouter);
await projectRouter.register(registerSecretTagRouter);
},
-
{ prefix: "/workspace" }
);
From 483fb458dd7681af59f656d714ca8452ae93c9c3 Mon Sep 17 00:00:00 2001
From: Daniel Hougaard
Date: Tue, 11 Mar 2025 04:52:12 +0400
Subject: [PATCH 31/71] requested changes
---
.../ee/services/permission/permission-fns.ts | 12 +-
.../src/server/routes/v1/dashboard-router.ts | 45 ++++
.../secret-v2-bridge-service.ts | 90 +++++++-
.../secret-v2-bridge-types.ts | 7 +
backend/src/services/secret/secret-service.ts | 37 ++-
backend/src/services/secret/secret-types.ts | 7 +
frontend/src/hooks/api/dashboard/index.ts | 1 +
frontend/src/hooks/api/dashboard/queries.tsx | 57 ++++-
frontend/src/hooks/api/dashboard/types.ts | 10 +
.../SecretOverviewTableRow/SecretEditRow.tsx | 89 +++----
.../SecretDropzone/CopySecretsFromBoard.tsx | 60 ++---
.../SecretListView/SecretDetailSidebar.tsx | 217 +++++++++---------
12 files changed, 441 insertions(+), 191 deletions(-)
diff --git a/backend/src/ee/services/permission/permission-fns.ts b/backend/src/ee/services/permission/permission-fns.ts
index 9397df939..cccadb86e 100644
--- a/backend/src/ee/services/permission/permission-fns.ts
+++ b/backend/src/ee/services/permission/permission-fns.ts
@@ -23,12 +23,6 @@ export function throwIfMissingSecretReadValueOrDescribePermission(
subjectFields?: SecretSubjectFields
) {
try {
- if (subjectFields) {
- ForbiddenError.from(permission).throwUnlessCan(action, subject(ProjectPermissionSub.Secrets, subjectFields));
- } else {
- ForbiddenError.from(permission).throwUnlessCan(action, ProjectPermissionSub.Secrets);
- }
- } catch {
if (subjectFields) {
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionSecretActions.DescribeAndReadValue,
@@ -40,6 +34,12 @@ export function throwIfMissingSecretReadValueOrDescribePermission(
ProjectPermissionSub.Secrets
);
}
+ } catch {
+ if (subjectFields) {
+ ForbiddenError.from(permission).throwUnlessCan(action, subject(ProjectPermissionSub.Secrets, subjectFields));
+ } else {
+ ForbiddenError.from(permission).throwUnlessCan(action, ProjectPermissionSub.Secrets);
+ }
}
}
diff --git a/backend/src/server/routes/v1/dashboard-router.ts b/backend/src/server/routes/v1/dashboard-router.ts
index 27ba3f8a3..392fcf5d9 100644
--- a/backend/src/server/routes/v1/dashboard-router.ts
+++ b/backend/src/server/routes/v1/dashboard-router.ts
@@ -834,6 +834,51 @@ 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()
+ })
+ .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",
diff --git a/backend/src/services/secret-v2-bridge/secret-v2-bridge-service.ts b/backend/src/services/secret-v2-bridge/secret-v2-bridge-service.ts
index 45c6493d6..c66fdddf1 100644
--- a/backend/src/services/secret-v2-bridge/secret-v2-bridge-service.ts
+++ b/backend/src/services/secret-v2-bridge/secret-v2-bridge-service.ts
@@ -60,6 +60,7 @@ import {
TCreateSecretDTO,
TDeleteManySecretDTO,
TDeleteSecretDTO,
+ TGetAccessibleSecretsDTO,
TGetASecretDTO,
TGetSecretReferencesTreeDTO,
TGetSecretsDTO,
@@ -200,7 +201,7 @@ export const secretV2BridgeServiceFactory = ({
const referredSecretsGroupBySecretKey = groupBy(referredSecrets, (i) => i.key);
references.forEach((el) => {
- throwIfMissingSecretReadValueOrDescribePermission(permission, ProjectPermissionSecretActions.ReadValue, {
+ throwIfMissingSecretReadValueOrDescribePermission(permission, ProjectPermissionSecretActions.DescribeSecret, {
environment: el.environment,
secretPath: el.secretPath,
secretName: el.secretKey,
@@ -1291,7 +1292,7 @@ export const secretV2BridgeServiceFactory = ({
folderDAL,
secretImportDAL,
decryptor: (value) => (value ? secretManagerDecryptor({ cipherTextBlob: value }).toString() : ""),
- expandSecretReferences: shouldExpandSecretReferences ? expandSecretReferences : undefined,
+ expandSecretReferences: shouldExpandSecretReferences && viewSecretValue ? expandSecretReferences : undefined,
hasSecretAccess: (expandEnvironment, expandSecretPath, expandSecretKey, expandSecretTags) => {
return hasSecretReadValueOrDescribePermission(permission, ProjectPermissionSecretActions.DescribeSecret, {
environment: expandEnvironment,
@@ -1347,7 +1348,7 @@ export const secretV2BridgeServiceFactory = ({
let secretValue = secret.encryptedValue
? secretManagerDecryptor({ cipherTextBlob: secret.encryptedValue }).toString()
: "";
- if (shouldExpandSecretReferences && secretValue) {
+ if (shouldExpandSecretReferences && secretValue && viewSecretValue) {
// eslint-disable-next-line
const expandedSecretValue = await expandSecretReferences({
environment,
@@ -2613,6 +2614,86 @@ export const secretV2BridgeServiceFactory = ({
return { tree: stackTrace, value: expandedValue };
};
+ const getAccessibleSecrets = async ({
+ projectId,
+ secretPath,
+ environment,
+ filterByAction,
+ actorId,
+ actor,
+ actorAuthMethod,
+ actorOrgId
+ }: TGetAccessibleSecretsDTO) => {
+ const { permission } = await permissionService.getProjectPermission({
+ actor,
+ actorId,
+ projectId,
+ actorAuthMethod,
+ actorOrgId,
+ actionProjectType: ActionProjectType.SecretManager
+ });
+ throwIfMissingSecretReadValueOrDescribePermission(permission, ProjectPermissionSecretActions.DescribeSecret, {
+ environment,
+ secretPath
+ });
+
+ const folder = await folderDAL.findBySecretPath(projectId, environment, secretPath);
+ if (!folder) return { secrets: [] };
+
+ const secrets = await secretDAL.findByFolderIds([folder.id]);
+
+ const { decryptor: secretManagerDecryptor } = await kmsService.createCipherPairWithDataKey({
+ type: KmsDataKey.SecretManager,
+ projectId
+ });
+
+ const decryptedSecrets = secrets
+ .filter((el) => {
+ if (
+ !hasSecretReadValueOrDescribePermission(permission, ProjectPermissionSecretActions.DescribeSecret, {
+ environment,
+ secretPath,
+ secretName: el.key,
+ secretTags: el.tags.map((i) => i.slug)
+ })
+ ) {
+ return false;
+ }
+
+ if (filterByAction === ProjectPermissionSecretActions.ReadValue) {
+ return hasSecretReadValueOrDescribePermission(permission, ProjectPermissionSecretActions.ReadValue, {
+ environment,
+ secretPath,
+ secretName: el.key,
+ secretTags: el.tags.map((i) => i.slug)
+ });
+ }
+
+ return true;
+ })
+ .map((secret) => {
+ return reshapeBridgeSecret(
+ projectId,
+ environment,
+ secretPath,
+ {
+ ...secret,
+ value: secret.encryptedValue
+ ? secretManagerDecryptor({ cipherTextBlob: secret.encryptedValue }).toString()
+ : "",
+ comment: secret.encryptedComment
+ ? secretManagerDecryptor({ cipherTextBlob: secret.encryptedComment }).toString()
+ : ""
+ },
+ false
+ );
+ });
+
+ return {
+ secrets: decryptedSecrets
+ };
+ };
+
return {
createSecret,
deleteSecret,
@@ -2630,6 +2711,7 @@ export const secretV2BridgeServiceFactory = ({
getSecretsMultiEnv,
getSecretReferenceTree,
getSecretsByFolderMappings,
- getSecretById
+ getSecretById,
+ getAccessibleSecrets
};
};
diff --git a/backend/src/services/secret-v2-bridge/secret-v2-bridge-types.ts b/backend/src/services/secret-v2-bridge/secret-v2-bridge-types.ts
index 473d1ed83..7f415bff8 100644
--- a/backend/src/services/secret-v2-bridge/secret-v2-bridge-types.ts
+++ b/backend/src/services/secret-v2-bridge/secret-v2-bridge-types.ts
@@ -351,3 +351,10 @@ export type TGetSecretsRawByFolderMappingsDTO = {
filters: TFindSecretsByFolderIdsFilter;
filterByAction?: ProjectPermissionSecretActions.DescribeSecret | ProjectPermissionSecretActions.ReadValue;
};
+
+export type TGetAccessibleSecretsDTO = {
+ environment: string;
+ projectId: string;
+ secretPath: string;
+ filterByAction: ProjectPermissionSecretActions.DescribeSecret | ProjectPermissionSecretActions.ReadValue;
+} & TProjectPermission;
diff --git a/backend/src/services/secret/secret-service.ts b/backend/src/services/secret/secret-service.ts
index 3593adf88..13a5c9736 100644
--- a/backend/src/services/secret/secret-service.ts
+++ b/backend/src/services/secret/secret-service.ts
@@ -81,6 +81,7 @@ import {
TDeleteManySecretRawDTO,
TDeleteSecretDTO,
TDeleteSecretRawDTO,
+ TGetAccessibleSecretsDTO,
TGetASecretByIdRawDTO,
TGetASecretDTO,
TGetASecretRawDTO,
@@ -1312,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,
@@ -3261,6 +3295,7 @@ export const secretServiceFactory = ({
getSecretReferenceTree,
getSecretsRawByFolderMappings,
getSecretAccessList,
- getSecretByIdRaw
+ getSecretByIdRaw,
+ getAccessibleSecrets
};
};
diff --git a/backend/src/services/secret/secret-types.ts b/backend/src/services/secret/secret-types.ts
index c7b8a067d..f42f70b78 100644
--- a/backend/src/services/secret/secret-types.ts
+++ b/backend/src/services/secret/secret-types.ts
@@ -20,6 +20,7 @@ import { TSecretV2BridgeDALFactory } from "../secret-v2-bridge/secret-v2-bridge-
import { SecretUpdateMode } from "../secret-v2-bridge/secret-v2-bridge-types";
import { TSecretVersionV2DALFactory } from "../secret-v2-bridge/secret-version-dal";
import { TSecretVersionV2TagDALFactory } from "../secret-v2-bridge/secret-version-tag-dal";
+import { ProjectPermissionSecretActions } from "@app/ee/services/permission/project-permission";
type TPartialSecret = Pick;
@@ -180,6 +181,12 @@ 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;
diff --git a/frontend/src/hooks/api/dashboard/index.ts b/frontend/src/hooks/api/dashboard/index.ts
index 83206bdf8..bbd70e306 100644
--- a/frontend/src/hooks/api/dashboard/index.ts
+++ b/frontend/src/hooks/api/dashboard/index.ts
@@ -1,4 +1,5 @@
export {
+ useGetAccessibleSecrets,
useGetProjectSecretsDetails,
useGetProjectSecretsOverview,
useGetProjectSecretsQuickSearch
diff --git a/frontend/src/hooks/api/dashboard/queries.tsx b/frontend/src/hooks/api/dashboard/queries.tsx
index f704081a9..049238202 100644
--- a/frontend/src/hooks/api/dashboard/queries.tsx
+++ b/frontend/src/hooks/api/dashboard/queries.tsx
@@ -11,6 +11,7 @@ import {
DashboardSecretsOrderBy,
TDashboardProjectSecretsQuickSearch,
TDashboardProjectSecretsQuickSearchResponse,
+ TGetAccessibleSecretsDTO,
TGetDashboardProjectSecretsByKeys,
TGetDashboardProjectSecretsDetailsDTO,
TGetDashboardProjectSecretsOverviewDTO,
@@ -20,6 +21,8 @@ import { OrderByDirection } from "@app/hooks/api/generic/types";
import { mergePersonalSecrets } from "@app/hooks/api/secrets/queries";
import { groupBy, unique } from "@app/lib/fn/array";
+import { SecretV3Raw } from "../types";
+
export const dashboardKeys = {
all: () => ["dashboard"] as const,
getDashboardSecrets: ({
@@ -58,7 +61,14 @@ export const dashboardKeys = {
...dashboardKeys.getDashboardSecrets({ projectId, secretPath }),
"quick-search",
params
- ] as const
+ ] as const,
+ getAccessibleSecrets: ({
+ projectId,
+ secretPath,
+ environment,
+ filterByAction
+ }: TGetAccessibleSecretsDTO) =>
+ [...dashboardKeys.all(), { projectId, secretPath, environment, filterByAction }] as const
};
export const fetchProjectSecretsOverview = async ({
@@ -295,6 +305,22 @@ export const fetchProjectSecretsQuickSearch = async ({
return data;
};
+const fetchAccessibleSecrets = async ({
+ projectId,
+ secretPath,
+ environment,
+ filterByAction
+}: TGetAccessibleSecretsDTO) => {
+ const { data } = await apiRequest.get<{ secrets: SecretV3Raw[] }>(
+ "/api/v1/dashboard/accessible-secrets",
+ {
+ params: { projectId, secretPath, environment, filterByAction }
+ }
+ );
+
+ return data.secrets;
+};
+
export const useGetProjectSecretsQuickSearch = (
{
projectId,
@@ -357,3 +383,32 @@ export const useGetProjectSecretsQuickSearch = (
placeholderData: (previousData) => previousData
});
};
+
+export const useGetAccessibleSecrets = ({
+ projectId,
+ secretPath,
+ environment,
+ filterByAction,
+ options
+}: TGetAccessibleSecretsDTO & {
+ options?: Omit<
+ UseQueryOptions<
+ SecretV3Raw[],
+ unknown,
+ SecretV3Raw[],
+ ReturnType
+ >,
+ "queryKey" | "queryFn"
+ >;
+}) => {
+ return useQuery({
+ ...options,
+ queryKey: dashboardKeys.getAccessibleSecrets({
+ projectId,
+ secretPath,
+ environment,
+ filterByAction
+ }),
+ queryFn: () => fetchAccessibleSecrets({ projectId, secretPath, environment, filterByAction })
+ });
+};
diff --git a/frontend/src/hooks/api/dashboard/types.ts b/frontend/src/hooks/api/dashboard/types.ts
index 4808b463c..9540c4ae6 100644
--- a/frontend/src/hooks/api/dashboard/types.ts
+++ b/frontend/src/hooks/api/dashboard/types.ts
@@ -1,3 +1,4 @@
+import { ProjectPermissionSecretActions } from "@app/context/ProjectPermissionContext/types";
import { TDynamicSecret } from "@app/hooks/api/dynamicSecret/types";
import { OrderByDirection } from "@app/hooks/api/generic/types";
import { TSecretFolder } from "@app/hooks/api/secretFolders/types";
@@ -101,3 +102,12 @@ export type TGetDashboardProjectSecretsByKeys = {
environment: string;
keys: string[];
};
+
+export type TGetAccessibleSecretsDTO = {
+ projectId: string;
+ secretPath: string;
+ environment: string;
+ filterByAction:
+ | ProjectPermissionSecretActions.DescribeSecret
+ | ProjectPermissionSecretActions.ReadValue;
+};
diff --git a/frontend/src/pages/secret-manager/OverviewPage/components/SecretOverviewTableRow/SecretEditRow.tsx b/frontend/src/pages/secret-manager/OverviewPage/components/SecretOverviewTableRow/SecretEditRow.tsx
index 4887d4fd7..705d7e177 100644
--- a/frontend/src/pages/secret-manager/OverviewPage/components/SecretOverviewTableRow/SecretEditRow.tsx
+++ b/frontend/src/pages/secret-manager/OverviewPage/components/SecretOverviewTableRow/SecretEditRow.tsx
@@ -27,9 +27,11 @@ import {
} from "@app/components/v2";
import { Blur } from "@app/components/v2/Blur";
import { InfisicalSecretInput } from "@app/components/v2/InfisicalSecretInput";
-import { ProjectPermissionActions, ProjectPermissionSub } from "@app/context";
+import { ProjectPermissionActions, ProjectPermissionSub, useProjectPermission } from "@app/context";
+import { ProjectPermissionSecretActions } from "@app/context/ProjectPermissionContext/types";
import { useToggle } from "@app/hooks";
import { SecretType } from "@app/hooks/api/types";
+import { hasSecretReadValueOrDescribePermission } from "@app/lib/fn/permission";
type Props = {
defaultValue?: string | null;
@@ -79,6 +81,9 @@ export const SecretEditRow = ({
value: defaultValue || null
}
});
+
+ const { permission } = useProjectPermission();
+
const [isDeleting, setIsDeleting] = useToggle();
const [isModalOpen, setIsModalOpen] = useState(false);
@@ -120,6 +125,11 @@ export const SecretEditRow = ({
reset({ value });
};
+ const canReadSecretValue = hasSecretReadValueOrDescribePermission(
+ permission,
+ ProjectPermissionSecretActions.ReadValue
+ );
+
const handleDeleteSecret = useCallback(async () => {
setIsDeleting.on();
setIsModalOpen(false);
@@ -229,48 +239,43 @@ export const SecretEditRow = ({
-
- {(isAllowed) => (
-
-
-
-
-
-
-
-
-
-
-
- e.preventDefault()} // prevents secret input from displaying value on open
+
+
+
+
+
+
-
-
-
-
- )}
-
+
+
+
+
+
+
+ e.preventDefault()} // prevents secret input from displaying value on open
+ >
+
+
+
+
+
1 ? val.slice(0, -1) : val
),
secrets: z
- .object({ key: z.string(), value: z.string().optional() })
+ .object({ secretKey: z.string(), secretValue: z.string().optional() })
.array()
.min(1, "Select one or more secrets to copy")
});
@@ -78,34 +79,38 @@ export const CopySecretsFromBoard = ({
const selectedEnvSlug = watch("environment");
const [debouncedEnvCopySecretPath] = useDebounce(envCopySecPath);
- const { data: secrets, isPending: isSecretsLoading } = useGetProjectSecrets({
- workspaceId,
- environment: selectedEnvSlug.slug,
- secretPath: debouncedEnvCopySecretPath,
- options: {
- enabled:
- Boolean(workspaceId) &&
- Boolean(selectedEnvSlug) &&
- Boolean(debouncedEnvCopySecretPath) &&
- isOpen
- }
- });
+ const { data: accessibleSecrets, isPending: isAccessibleSecretsLoading } =
+ useGetAccessibleSecrets({
+ projectId: workspaceId,
+ secretPath: debouncedEnvCopySecretPath,
+ environment: selectedEnvSlug.slug,
+ filterByAction: shouldIncludeValues
+ ? ProjectPermissionSecretActions.ReadValue
+ : ProjectPermissionSecretActions.DescribeSecret,
+ options: {
+ enabled:
+ Boolean(workspaceId) &&
+ Boolean(selectedEnvSlug) &&
+ Boolean(debouncedEnvCopySecretPath) &&
+ isOpen
+ }
+ });
useEffect(() => {
setValue("secrets", []);
}, [debouncedEnvCopySecretPath, selectedEnvSlug]);
const handleSecSelectAll = () => {
- if (secrets) {
- setValue("secrets", secrets, { shouldDirty: true });
+ if (accessibleSecrets) {
+ setValue("secrets", accessibleSecrets, { shouldDirty: true });
}
};
const handleFormSubmit = async (data: TFormSchema) => {
const secretsToBePulled: Record = {};
- data.secrets.forEach(({ key, value }) => {
- secretsToBePulled[key] = {
- value: (shouldIncludeValues && value) || "",
+ data.secrets.forEach(({ secretKey, secretValue }) => {
+ secretsToBePulled[secretKey] = {
+ value: (shouldIncludeValues && secretValue) || "",
comments: [""]
};
});
@@ -202,19 +207,19 @@ export const CopySecretsFromBoard = ({
option.key}
- getOptionLabel={(option) => option.key}
+ getOptionValue={(option) => option.secretKey}
+ getOptionLabel={(option) => option.secretKey}
/>
)}
@@ -235,7 +240,10 @@ export const CopySecretsFromBoard = ({
setShouldIncludeValues(isChecked as boolean)}
+ onCheckedChange={(isChecked) => {
+ setValue("secrets", []);
+ setShouldIncludeValues(isChecked as boolean);
+ }}
>
Include secret values
diff --git a/frontend/src/pages/secret-manager/SecretDashboardPage/components/SecretListView/SecretDetailSidebar.tsx b/frontend/src/pages/secret-manager/SecretDashboardPage/components/SecretListView/SecretDetailSidebar.tsx
index 410d49e5f..641a4a723 100644
--- a/frontend/src/pages/secret-manager/SecretDashboardPage/components/SecretListView/SecretDetailSidebar.tsx
+++ b/frontend/src/pages/secret-manager/SecretDashboardPage/components/SecretListView/SecretDetailSidebar.tsx
@@ -13,8 +13,8 @@ import {
faShare,
faTag,
faTrash,
- faUser,
- faTriangleExclamation
+ faTriangleExclamation,
+ faUser
} from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { zodResolver } from "@hookform/resolvers/zod";
@@ -728,8 +728,8 @@ export const SecretDetailSidebar = ({
Version History
{secretVersion?.map(
- ({ createdAt, secretValue, version, id, secretValueHidden, actor }, index) => (
-
+ ({ createdAt, secretValue, secretValueHidden, version, id, actor }) => (
+
@@ -739,72 +739,50 @@ export const SecretDetailSidebar = ({
{format(new Date(createdAt), "Pp")}
-
{format(new Date(createdAt), "Pp")}
-
-
-
-
- {actor && (
-
-
- Modified by:
-
- {/* eslint-disable-next-line jsx-a11y/click-events-have-key-events, jsx-a11y/no-static-element-interactions */}
-
- onModifyHistoryClick(
- actor.actorId,
- actor.actorType,
- actor.membershipId
- )
- }
- className="cursor-pointer"
+
+
+
+ {actor && (
+
+
+ Modified by:
+
-
-
-
+ {/* eslint-disable-next-line jsx-a11y/click-events-have-key-events, jsx-a11y/no-static-element-interactions */}
+
+ onModifyHistoryClick(
+ actor.actorId,
+ actor.actorType,
+ actor.membershipId
+ )
+ }
+ className="cursor-pointer"
+ >
+
+
+
+
-
- )}
-
-
- Value:
-
-
-
-
From 497761a0e5d24c47face9c416caaaa2d86fe6a56 Mon Sep 17 00:00:00 2001
From: Daniel Hougaard
Date: Tue, 11 Mar 2025 05:44:28 +0400
Subject: [PATCH 32/71] fix: missing permision check
---
backend/src/server/routes/v1/dashboard-router.ts | 3 ++-
.../secret-v2-bridge/secret-v2-bridge-service.ts | 11 ++++++++++-
2 files changed, 12 insertions(+), 2 deletions(-)
diff --git a/backend/src/server/routes/v1/dashboard-router.ts b/backend/src/server/routes/v1/dashboard-router.ts
index 392fcf5d9..db61594a2 100644
--- a/backend/src/server/routes/v1/dashboard-router.ts
+++ b/backend/src/server/routes/v1/dashboard-router.ts
@@ -853,7 +853,8 @@ export const registerDashboardRouter = async (server: FastifyZodProvider) => {
200: z.object({
secrets: secretRawSchema
.extend({
- secretPath: z.string().optional()
+ secretPath: z.string().optional(),
+ secretValueHidden: z.boolean()
})
.array()
.optional()
diff --git a/backend/src/services/secret-v2-bridge/secret-v2-bridge-service.ts b/backend/src/services/secret-v2-bridge/secret-v2-bridge-service.ts
index c66fdddf1..09b3a50cb 100644
--- a/backend/src/services/secret-v2-bridge/secret-v2-bridge-service.ts
+++ b/backend/src/services/secret-v2-bridge/secret-v2-bridge-service.ts
@@ -2672,6 +2672,15 @@ export const secretV2BridgeServiceFactory = ({
return true;
})
.map((secret) => {
+ const secretValueHidden =
+ filterByAction === ProjectPermissionSecretActions.DescribeSecret &&
+ !hasSecretReadValueOrDescribePermission(permission, ProjectPermissionSecretActions.ReadValue, {
+ environment,
+ secretPath,
+ secretName: secret.key,
+ secretTags: secret.tags.map((i) => i.slug)
+ });
+
return reshapeBridgeSecret(
projectId,
environment,
@@ -2685,7 +2694,7 @@ export const secretV2BridgeServiceFactory = ({
? secretManagerDecryptor({ cipherTextBlob: secret.encryptedComment }).toString()
: ""
},
- false
+ secretValueHidden
);
});
From ad1dd55b8bf1cbff714132be1e9e2376df0dee18 Mon Sep 17 00:00:00 2001
From: Daniel Hougaard
Date: Tue, 11 Mar 2025 06:01:21 +0400
Subject: [PATCH 33/71] chore: requested changes
---
frontend/src/hooks/api/dashboard/queries.tsx | 6 +++++-
frontend/src/hooks/api/reactQuery.tsx | 6 +-----
frontend/src/lib/fn/string.ts | 4 ++++
.../SecretListView/SecretDetailSidebar.tsx | 12 +++++-------
4 files changed, 15 insertions(+), 13 deletions(-)
diff --git a/frontend/src/hooks/api/dashboard/queries.tsx b/frontend/src/hooks/api/dashboard/queries.tsx
index 049238202..1fb456325 100644
--- a/frontend/src/hooks/api/dashboard/queries.tsx
+++ b/frontend/src/hooks/api/dashboard/queries.tsx
@@ -68,7 +68,11 @@ export const dashboardKeys = {
environment,
filterByAction
}: TGetAccessibleSecretsDTO) =>
- [...dashboardKeys.all(), { projectId, secretPath, environment, filterByAction }] as const
+ [
+ ...dashboardKeys.all(),
+ "accessible-secrets",
+ { projectId, secretPath, environment, filterByAction }
+ ] as const
};
export const fetchProjectSecretsOverview = async ({
diff --git a/frontend/src/hooks/api/reactQuery.tsx b/frontend/src/hooks/api/reactQuery.tsx
index be9dadbec..cd8622976 100644
--- a/frontend/src/hooks/api/reactQuery.tsx
+++ b/frontend/src/hooks/api/reactQuery.tsx
@@ -10,18 +10,14 @@ import {
formatedConditionsOperatorNames,
PermissionConditionOperators
} from "@app/context/ProjectPermissionContext/types";
+import { camelCaseToSpaces } from "@app/lib/fn/string";
import { ApiErrorTypes, TApiErrors } from "./types";
-
// this is saved in react-query cache
export const SIGNUP_TEMP_TOKEN_CACHE_KEY = ["infisical__signup-temp-token"];
export const MFA_TEMP_TOKEN_CACHE_KEY = ["infisical__mfa-temp-token"];
export const AUTH_TOKEN_CACHE_KEY = ["infisical__auth-token"];
-const camelCaseToSpaces = (input: string) => {
- return input.replace(/([a-z])([A-Z])/g, "$1 $2");
-};
-
export const queryClient = new QueryClient({
mutationCache: new MutationCache({
onError: (error) => {
diff --git a/frontend/src/lib/fn/string.ts b/frontend/src/lib/fn/string.ts
index 9d3d01cc5..f4c4a43db 100644
--- a/frontend/src/lib/fn/string.ts
+++ b/frontend/src/lib/fn/string.ts
@@ -7,3 +7,7 @@ export const formatReservedPaths = (secretPath: string) => {
}
return secretPath;
};
+
+export const camelCaseToSpaces = (input: string) => {
+ return input.replace(/([a-z])([A-Z])/g, "$1 $2");
+};
diff --git a/frontend/src/pages/secret-manager/SecretDashboardPage/components/SecretListView/SecretDetailSidebar.tsx b/frontend/src/pages/secret-manager/SecretDashboardPage/components/SecretListView/SecretDetailSidebar.tsx
index 641a4a723..382b574c8 100644
--- a/frontend/src/pages/secret-manager/SecretDashboardPage/components/SecretListView/SecretDetailSidebar.tsx
+++ b/frontend/src/pages/secret-manager/SecretDashboardPage/components/SecretListView/SecretDetailSidebar.tsx
@@ -61,6 +61,7 @@ import { hasSecretReadValueOrDescribePermission } from "@app/lib/fn/permission";
import { CreateReminderForm } from "./CreateReminderForm";
import { formSchema, SecretActionType, TFormSchema } from "./SecretListView.utils";
+import { camelCaseToSpaces } from "@app/lib/fn/string";
type Props = {
isOpen?: boolean;
@@ -729,8 +730,8 @@ export const SecretDetailSidebar = ({
{secretVersion?.map(
({ createdAt, secretValue, secretValueHidden, version, id, actor }) => (
-
-
+
+
@@ -936,12 +937,9 @@ export const SecretDetailSidebar = ({
- action.charAt(0).toUpperCase() + action.slice(1).toLowerCase()
- )
+ .map((action) => camelCaseToSpaces(action))
.join(", ")}
- className="z-[100]"
+ className="z-[100] capitalize"
>
Date: Tue, 11 Mar 2025 07:11:14 +0400
Subject: [PATCH 34/71] chore: rolled back service token permission changes
---
.../services/permission/project-permission.ts | 18 +-----------------
1 file changed, 1 insertion(+), 17 deletions(-)
diff --git a/backend/src/ee/services/permission/project-permission.ts b/backend/src/ee/services/permission/project-permission.ts
index 43f832d22..c622e65a7 100644
--- a/backend/src/ee/services/permission/project-permission.ts
+++ b/backend/src/ee/services/permission/project-permission.ts
@@ -856,14 +856,12 @@ export const buildServiceTokenProjectPermission = (
) => {
const canWrite = permission.includes("write");
const canRead = permission.includes("read");
-
const { can, build } = new AbilityBuilder>(createMongoAbility);
scopes.forEach(({ secretPath, environment }) => {
[ProjectPermissionSub.Secrets, ProjectPermissionSub.SecretImports, ProjectPermissionSub.SecretFolders].forEach(
(subject) => {
if (canWrite) {
can(ProjectPermissionActions.Edit, subject, {
- // TODO: @Akhi
// @ts-expect-error type
secretPath: { $glob: secretPath },
environment
@@ -879,27 +877,13 @@ export const buildServiceTokenProjectPermission = (
environment
});
}
- if (canRead && subject !== ProjectPermissionSub.Secrets) {
+ if (canRead) {
can(ProjectPermissionActions.Read, subject, {
// @ts-expect-error type
secretPath: { $glob: secretPath },
environment
});
}
-
- if (subject === ProjectPermissionSub.Secrets && canRead) {
- // @ts-expect-error type
- can(ProjectPermissionSecretActions.ReadValue, subject as ProjectPermissionSub.Secrets, {
- secretPath: { $glob: secretPath },
- environment
- });
-
- // @ts-expect-error type
- can(ProjectPermissionSecretActions.DescribeSecret, subject as ProjectPermissionSub.Secrets, {
- secretPath: { $glob: secretPath },
- environment
- });
- }
}
);
});
From 16d3bbb67ad84456117123d48ae5e4db3b11873d Mon Sep 17 00:00:00 2001
From: Maidul Islam
Date: Mon, 10 Mar 2025 23:46:04 -0400
Subject: [PATCH 35/71] 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
---
.../dynamic-secret/providers/models.ts | 38 +++
.../dynamic-secret/providers/sql-database.ts | 90 ++++++-
frontend/src/hooks/api/dynamicSecret/types.ts | 1 +
.../SqlDatabaseInputForm.tsx | 247 +++++++++++++++++-
.../EditDynamicSecretSqlProviderForm.tsx | 243 ++++++++++++++++-
5 files changed, 604 insertions(+), 15 deletions(-)
diff --git a/backend/src/ee/services/dynamic-secret/providers/models.ts b/backend/src/ee/services/dynamic-secret/providers/models.ts
index 621c3c631..93af4eabc 100644
--- a/backend/src/ee/services/dynamic-secret/providers/models.ts
+++ b/backend/src/ee/services/dynamic-secret/providers/models.ts
@@ -1,5 +1,22 @@
import { z } from "zod";
+export type PasswordRequirements = {
+ minLength: number;
+ maxLength: number;
+ required: {
+ lowercase: number;
+ uppercase: number;
+ digits: number;
+ symbols: number;
+ };
+ allowedCharacters?: {
+ lowercase?: string;
+ uppercase?: string;
+ digits?: string;
+ symbols?: string;
+ };
+};
+
export enum SqlProviders {
Postgres = "postgres",
MySQL = "mysql2",
@@ -100,6 +117,27 @@ export const DynamicSecretSqlDBSchema = z.object({
database: z.string().trim(),
username: z.string().trim(),
password: z.string().trim(),
+ passwordRequirements: z
+ .object({
+ minLength: z.number().min(1).max(100),
+ maxLength: z.number().min(1).max(100),
+ required: z.object({
+ lowercase: z.number().min(0),
+ uppercase: z.number().min(0),
+ digits: z.number().min(0),
+ symbols: z.number().min(0)
+ }),
+ allowedCharacters: z
+ .object({
+ lowercase: z.string().optional(),
+ uppercase: z.string().optional(),
+ digits: z.string().optional(),
+ symbols: z.string().optional()
+ })
+ .optional()
+ })
+ .optional()
+ .describe('Password generation requirements'),
creationStatement: z.string().trim(),
revocationStatement: z.string().trim(),
renewStatement: z.string().trim().optional(),
diff --git a/backend/src/ee/services/dynamic-secret/providers/sql-database.ts b/backend/src/ee/services/dynamic-secret/providers/sql-database.ts
index 68089ea4c..7d76db697 100644
--- a/backend/src/ee/services/dynamic-secret/providers/sql-database.ts
+++ b/backend/src/ee/services/dynamic-secret/providers/sql-database.ts
@@ -8,16 +8,92 @@ 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 = {
+ minLength: 48,
+ maxLength: 48,
+ required: {
+ lowercase: 1,
+ uppercase: 1,
+ digits: 1,
+ symbols: 0
+ },
+ allowedCharacters: {
+ lowercase: 'abcdefghijklmnopqrstuvwxyz',
+ uppercase: 'ABCDEFGHIJKLMNOPQRSTUVWXYZ',
+ digits: '0123456789',
+ symbols: '-_.~!*'
+ }
+};
- const charset = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-_.~!*";
- return customAlphabet(charset, 48)(size);
+const ORACLE_PASSWORD_REQUIREMENTS = {
+ ...DEFAULT_PASSWORD_REQUIREMENTS,
+ minLength: 30,
+ maxLength: 30
+};
+
+const generatePassword = (provider: SqlProviders, requirements?: PasswordRequirements) => {
+ const defaultReqs = provider === SqlProviders.Oracle ? ORACLE_PASSWORD_REQUIREMENTS : DEFAULT_PASSWORD_REQUIREMENTS;
+ const finalReqs = requirements || defaultReqs;
+
+ try {
+ const { minLength, maxLength, required, allowedCharacters = {} } = finalReqs;
+
+ const chars = {
+ lowercase: allowedCharacters.lowercase || 'abcdefghijklmnopqrstuvwxyz',
+ uppercase: allowedCharacters.uppercase || 'ABCDEFGHIJKLMNOPQRSTUVWXYZ',
+ digits: allowedCharacters.digits || '0123456789',
+ symbols: allowedCharacters.symbols || '-_.~!*'
+ };
+
+ const parts: string[] = [];
+
+ if (required.lowercase > 0) {
+ parts.push(...Array(required.lowercase).fill(0).map(() =>
+ chars.lowercase[Math.floor(Math.random() * chars.lowercase.length)]
+ ));
+ }
+
+ if (required.uppercase > 0) {
+ parts.push(...Array(required.uppercase).fill(0).map(() =>
+ chars.uppercase[Math.floor(Math.random() * chars.uppercase.length)]
+ ));
+ }
+
+ if (required.digits > 0) {
+ parts.push(...Array(required.digits).fill(0).map(() =>
+ chars.digits[Math.floor(Math.random() * chars.digits.length)]
+ ));
+ }
+
+ if (required.symbols > 0) {
+ parts.push(...Array(required.symbols).fill(0).map(() =>
+ chars.symbols[Math.floor(Math.random() * chars.symbols.length)]
+ ));
+ }
+
+ const requiredTotal = Object.values(required).reduce((a, b) => a + b, 0);
+ const remainingLength = Math.max(minLength - requiredTotal, 0);
+
+ const allChars = Object.values(chars).join('');
+ parts.push(...Array(remainingLength).fill(0).map(() =>
+ allChars[Math.floor(Math.random() * allChars.length)]
+ ));
+
+ // shuffle the array to mix up the characters
+ for (let i = parts.length - 1; i > 0; i--) {
+ const j = Math.floor(Math.random() * (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 +191,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 {
diff --git a/frontend/src/hooks/api/dynamicSecret/types.ts b/frontend/src/hooks/api/dynamicSecret/types.ts
index 7d8c6920a..154449d5b 100644
--- a/frontend/src/hooks/api/dynamicSecret/types.ts
+++ b/frontend/src/hooks/api/dynamicSecret/types.ts
@@ -50,6 +50,7 @@ export type TDynamicSecretProvider =
database: string;
username: string;
password: string;
+ passwordRegex?: string;
creationStatement: string;
revocationStatement: string;
renewStatement?: string;
diff --git a/frontend/src/pages/secret-manager/SecretDashboardPage/components/ActionBar/CreateDynamicSecretForm/SqlDatabaseInputForm.tsx b/frontend/src/pages/secret-manager/SecretDashboardPage/components/ActionBar/CreateDynamicSecretForm/SqlDatabaseInputForm.tsx
index a4962d10c..354f226f8 100644
--- a/frontend/src/pages/secret-manager/SecretDashboardPage/components/ActionBar/CreateDynamicSecretForm/SqlDatabaseInputForm.tsx
+++ b/frontend/src/pages/secret-manager/SecretDashboardPage/components/ActionBar/CreateDynamicSecretForm/SqlDatabaseInputForm.tsx
@@ -23,6 +23,25 @@ import { useWorkspace } from "@app/context";
import { gatewaysQueryKeys, useCreateDynamicSecret } from "@app/hooks/api";
import { DynamicSecretProviders, SqlProviders } from "@app/hooks/api/dynamicSecret/types";
+const passwordRequirementsSchema = z.object({
+ minLength: z.number().min(1).max(100),
+ maxLength: z.number().min(1).max(100),
+ required: z.object({
+ lowercase: z.number().min(0),
+ uppercase: z.number().min(0),
+ digits: z.number().min(0),
+ symbols: z.number().min(0)
+ }),
+ allowedCharacters: z
+ .object({
+ lowercase: z.string().optional(),
+ uppercase: z.string().optional(),
+ digits: z.string().optional(),
+ symbols: z.string().optional()
+ })
+ .optional()
+});
+
const formSchema = z.object({
provider: z.object({
client: z.nativeEnum(SqlProviders),
@@ -31,6 +50,7 @@ const formSchema = z.object({
database: z.string().min(1),
username: z.string().min(1),
password: z.string().min(1),
+ passwordRequirements: passwordRequirementsSchema.optional(),
creationStatement: z.string().min(1),
revocationStatement: z.string().min(1),
renewStatement: z.string().optional(),
@@ -137,7 +157,19 @@ export const SqlDatabaseInputForm = ({
} = useForm({
resolver: zodResolver(formSchema),
defaultValues: {
- provider: getSqlStatements(SqlProviders.Postgres)
+ provider: {
+ ...getSqlStatements(SqlProviders.Postgres),
+ passwordRequirements: {
+ minLength: 48,
+ maxLength: 48,
+ required: {
+ lowercase: 1,
+ uppercase: 1,
+ digits: 1,
+ symbols: 0
+ }
+ }
+ }
}
});
@@ -174,6 +206,11 @@ export const SqlDatabaseInputForm = ({
setValue("provider.renewStatement", sqlStatment.renewStatement);
setValue("provider.revocationStatement", sqlStatment.revocationStatement);
setValue("provider.port", getDefaultPort(type));
+
+ // Update password requirements based on provider
+ const minMaxLength = type === SqlProviders.Oracle ? 30 : 48;
+ setValue("provider.passwordRequirements.minLength", minMaxLength);
+ setValue("provider.passwordRequirements.maxLength", minMaxLength);
};
return (
@@ -197,6 +234,7 @@ export const SqlDatabaseInputForm = ({
)}
/>
+
)}
/>
-
-
- Modify SQL Statements
+
+
+ Creation, Revocation & Renew Statements (optional)
+
+ Customize SQL statements for managing database user lifecycle
+
+
+
+ Password Configuration (optional)
+
+
+ Set constraints on the generated database password
+
+
+
+
+
diff --git a/frontend/src/pages/secret-manager/SecretDashboardPage/components/DynamicSecretListView/EditDynamicSecretForm/EditDynamicSecretSqlProviderForm.tsx b/frontend/src/pages/secret-manager/SecretDashboardPage/components/DynamicSecretListView/EditDynamicSecretForm/EditDynamicSecretSqlProviderForm.tsx
index 39655e431..5cf5c6524 100644
--- a/frontend/src/pages/secret-manager/SecretDashboardPage/components/DynamicSecretListView/EditDynamicSecretForm/EditDynamicSecretSqlProviderForm.tsx
+++ b/frontend/src/pages/secret-manager/SecretDashboardPage/components/DynamicSecretListView/EditDynamicSecretForm/EditDynamicSecretSqlProviderForm.tsx
@@ -23,6 +23,25 @@ import { useWorkspace } from "@app/context";
import { gatewaysQueryKeys, useUpdateDynamicSecret } from "@app/hooks/api";
import { SqlProviders, TDynamicSecret } from "@app/hooks/api/dynamicSecret/types";
+const passwordRequirementsSchema = z.object({
+ minLength: z.number().min(1).max(100),
+ maxLength: z.number().min(1).max(100),
+ required: z.object({
+ lowercase: z.number().min(0),
+ uppercase: z.number().min(0),
+ digits: z.number().min(0),
+ symbols: z.number().min(0)
+ }),
+ allowedCharacters: z
+ .object({
+ lowercase: z.string().optional(),
+ uppercase: z.string().optional(),
+ digits: z.string().optional(),
+ symbols: z.string().optional()
+ })
+ .optional()
+});
+
const formSchema = z.object({
inputs: z
.object({
@@ -32,6 +51,7 @@ const formSchema = z.object({
database: z.string().min(1),
username: z.string().min(1),
password: z.string().min(1),
+ passwordRequirements: passwordRequirementsSchema.optional(),
creationStatement: z.string().min(1),
revocationStatement: z.string().min(1),
renewStatement: z.string().optional(),
@@ -82,6 +102,17 @@ export const EditDynamicSecretSqlProviderForm = ({
secretPath,
projectSlug
}: Props) => {
+ const getDefaultPasswordRequirements = (provider: SqlProviders) => ({
+ minLength: provider === SqlProviders.Oracle ? 30 : 48,
+ maxLength: provider === SqlProviders.Oracle ? 30 : 48,
+ required: {
+ lowercase: 1,
+ uppercase: 1,
+ digits: 1,
+ symbols: 0
+ }
+ });
+
const {
control,
watch,
@@ -94,10 +125,13 @@ export const EditDynamicSecretSqlProviderForm = ({
maxTTL: dynamicSecret.maxTTL,
newName: dynamicSecret.name,
inputs: {
- ...(dynamicSecret.inputs as TForm["inputs"])
+ ...(dynamicSecret.inputs as TForm["inputs"]),
+ passwordRequirements: (dynamicSecret.inputs as TForm["inputs"])?.passwordRequirements ||
+ getDefaultPasswordRequirements((dynamicSecret.inputs as TForm["inputs"])?.client || SqlProviders.Postgres)
}
}
});
+
const { currentWorkspace } = useWorkspace();
const { data: projectGateways, isPending: isProjectGatewaysLoading } = useQuery(
gatewaysQueryKeys.listProjectGateways({ projectId: currentWorkspace.id })
@@ -347,10 +381,13 @@ export const EditDynamicSecretSqlProviderForm = ({
)}
/>
-
-
- Modify SQL Statements
+
+
+ Creation, Revocation & Renew Statements (optional)
+
+ Customize SQL statements for managing database user lifecycle
+
+
+
+ Password Configuration (optional)
+
+
+ Set constraints on the generated database password
+
+
+
+
+
From 719a18c218049d865cf05168d5f29ae6b450d4c8 Mon Sep 17 00:00:00 2001
From: =
Date: Tue, 11 Mar 2025 16:03:39 +0530
Subject: [PATCH 36/71] feat: removed user paywall for user management and
fixed a type error
---
.../secret-v2-bridge/secret-v2-bridge-service.ts | 4 +++-
backend/src/services/super-admin/super-admin-service.ts | 6 ------
.../pages/admin/OverviewPage/components/UserPanel.tsx | 9 ---------
3 files changed, 3 insertions(+), 16 deletions(-)
diff --git a/backend/src/services/secret-v2-bridge/secret-v2-bridge-service.ts b/backend/src/services/secret-v2-bridge/secret-v2-bridge-service.ts
index 09b3a50cb..8238f3467 100644
--- a/backend/src/services/secret-v2-bridge/secret-v2-bridge-service.ts
+++ b/backend/src/services/secret-v2-bridge/secret-v2-bridge-service.ts
@@ -2567,6 +2567,8 @@ export const secretV2BridgeServiceFactory = ({
type: SecretType.Shared
});
+ if (!secret) throw new NotFoundError({ message: `Secret with name '${secretName}' not found` });
+
throwIfMissingSecretReadValueOrDescribePermission(permission, ProjectPermissionSecretActions.DescribeSecret, {
environment,
secretPath,
@@ -2574,7 +2576,7 @@ export const secretV2BridgeServiceFactory = ({
secretTags: (secret?.tags || []).map((el) => el.slug)
});
- const decryptedSecretValue = secret.encryptedValue
+ const decryptedSecretValue = secret?.encryptedValue
? secretManagerDecryptor({ cipherTextBlob: secret.encryptedValue }).toString()
: "";
diff --git a/backend/src/services/super-admin/super-admin-service.ts b/backend/src/services/super-admin/super-admin-service.ts
index 8efdfd7f9..dfa698137 100644
--- a/backend/src/services/super-admin/super-admin-service.ts
+++ b/backend/src/services/super-admin/super-admin-service.ts
@@ -282,12 +282,6 @@ export const superAdminServiceFactory = ({
};
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;
};
diff --git a/frontend/src/pages/admin/OverviewPage/components/UserPanel.tsx b/frontend/src/pages/admin/OverviewPage/components/UserPanel.tsx
index 6654439de..7977c4f15 100644
--- a/frontend/src/pages/admin/OverviewPage/components/UserPanel.tsx
+++ b/frontend/src/pages/admin/OverviewPage/components/UserPanel.tsx
@@ -42,7 +42,6 @@ import {
} from "@app/hooks/api";
import { UsePopUpState } from "@app/hooks/usePopUp";
-const deleteUserUpgradePlanMessage = "Deleting users via Admin UI";
const addServerAdminUpgradePlanMessage = "Granting another user Server Admin permissions";
const UserPanelTable = ({
@@ -156,14 +155,6 @@ const UserPanelTable = ({
{
e.stopPropagation();
- if (!subscription?.instanceUserManagement) {
- handlePopUpOpen("upgradePlan", {
- username,
- id,
- message: deleteUserUpgradePlanMessage
- });
- return;
- }
handlePopUpOpen("removeUser", { username, id });
}}
>
From 5e4b478b74631040cdbe682c35f08a2b6502cdfe Mon Sep 17 00:00:00 2001
From: Mahyar Mirrashed
Date: Tue, 11 Mar 2025 08:17:07 -0700
Subject: [PATCH 37/71] refactor(nix): replace shell hook with infisical
dependency
---
flake.nix | 15 +--------------
1 file changed, 1 insertion(+), 14 deletions(-)
diff --git a/flake.nix b/flake.nix
index ffed5545c..3fd21f871 100644
--- a/flake.nix
+++ b/flake.nix
@@ -15,21 +15,8 @@
lazygit
nodejs_20
+ infisical
];
-
- shellHook = ''
- # Define a writable directory for global npm packages
- export NPM_CONFIG_PREFIX="$HOME/.npm-global"
- export PATH="$NPM_CONFIG_PREFIX/bin:$PATH"
-
- # Ensure the directory exists
- mkdir -p "$NPM_CONFIG_PREFIX"
-
- # Install Infisical CLI only if it's not already installed
- if ! command -v infisical &>/dev/null; then
- npm install -g @infisical/cli
- fi
- '';
};
};
}
From c9da8477c867ab0dca101b687690e1cfa0d73e97 Mon Sep 17 00:00:00 2001
From: Mahyar Mirrashed
Date: Tue, 11 Mar 2025 08:54:15 -0700
Subject: [PATCH 38/71] chore(nix): add prettier to list of dependencies
---
flake.nix | 1 +
1 file changed, 1 insertion(+)
diff --git a/flake.nix b/flake.nix
index 3fd21f871..9d995a551 100644
--- a/flake.nix
+++ b/flake.nix
@@ -15,6 +15,7 @@
lazygit
nodejs_20
+ nodePackages.prettier
infisical
];
};
From 6fa41a609bc985ee2dd2ddadba1f3eedef126682 Mon Sep 17 00:00:00 2001
From: Maidul Islam
Date: Tue, 11 Mar 2025 12:28:48 -0400
Subject: [PATCH 39/71] remove char and digit rangs and other requested
changes/improvments
---
.../dynamic-secret/providers/models.ts | 31 ++-
.../dynamic-secret/providers/sql-database.ts | 50 +++--
.../SqlDatabaseInputForm.tsx | 165 ++++++---------
.../EditDynamicSecretSqlProviderForm.tsx | 197 +++++++-----------
4 files changed, 168 insertions(+), 275 deletions(-)
diff --git a/backend/src/ee/services/dynamic-secret/providers/models.ts b/backend/src/ee/services/dynamic-secret/providers/models.ts
index 93af4eabc..1808057e7 100644
--- a/backend/src/ee/services/dynamic-secret/providers/models.ts
+++ b/backend/src/ee/services/dynamic-secret/providers/models.ts
@@ -1,20 +1,14 @@
import { z } from "zod";
export type PasswordRequirements = {
- minLength: number;
- maxLength: number;
+ length: number;
required: {
lowercase: number;
uppercase: number;
digits: number;
symbols: number;
};
- allowedCharacters?: {
- lowercase?: string;
- uppercase?: string;
- digits?: string;
- symbols?: string;
- };
+ allowedSymbols?: string;
};
export enum SqlProviders {
@@ -119,23 +113,22 @@ export const DynamicSecretSqlDBSchema = z.object({
password: z.string().trim(),
passwordRequirements: z
.object({
- minLength: z.number().min(1).max(100),
- maxLength: z.number().min(1).max(100),
+ 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)
- }),
- allowedCharacters: z
- .object({
- lowercase: z.string().optional(),
- uppercase: z.string().optional(),
- digits: z.string().optional(),
- symbols: z.string().optional()
- })
- .optional()
+ }).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(),
diff --git a/backend/src/ee/services/dynamic-secret/providers/sql-database.ts b/backend/src/ee/services/dynamic-secret/providers/sql-database.ts
index 7d76db697..bb9240139 100644
--- a/backend/src/ee/services/dynamic-secret/providers/sql-database.ts
+++ b/backend/src/ee/services/dynamic-secret/providers/sql-database.ts
@@ -2,6 +2,7 @@ import handlebars from "handlebars";
import knex from "knex";
import { customAlphabet } from "nanoid";
import { z } from "zod";
+import { randomInt } from 'crypto';
import { withGatewayProxy } from "@app/lib/gateway";
import { alphaNumericNanoId } from "@app/lib/nanoid";
@@ -13,26 +14,19 @@ import { DynamicSecretSqlDBSchema, PasswordRequirements, SqlProviders, TDynamicP
const EXTERNAL_REQUEST_TIMEOUT = 10 * 1000;
const DEFAULT_PASSWORD_REQUIREMENTS = {
- minLength: 48,
- maxLength: 48,
+ length: 48,
required: {
lowercase: 1,
uppercase: 1,
digits: 1,
symbols: 0
},
- allowedCharacters: {
- lowercase: 'abcdefghijklmnopqrstuvwxyz',
- uppercase: 'ABCDEFGHIJKLMNOPQRSTUVWXYZ',
- digits: '0123456789',
- symbols: '-_.~!*'
- }
+ allowedSymbols: '-_.~!*'
};
const ORACLE_PASSWORD_REQUIREMENTS = {
...DEFAULT_PASSWORD_REQUIREMENTS,
- minLength: 30,
- maxLength: 30
+ length: 30
};
const generatePassword = (provider: SqlProviders, requirements?: PasswordRequirements) => {
@@ -40,52 +34,56 @@ const generatePassword = (provider: SqlProviders, requirements?: PasswordRequire
const finalReqs = requirements || defaultReqs;
try {
- const { minLength, maxLength, required, allowedCharacters = {} } = finalReqs;
+ const { length, required, allowedSymbols } = finalReqs;
const chars = {
- lowercase: allowedCharacters.lowercase || 'abcdefghijklmnopqrstuvwxyz',
- uppercase: allowedCharacters.uppercase || 'ABCDEFGHIJKLMNOPQRSTUVWXYZ',
- digits: allowedCharacters.digits || '0123456789',
- symbols: allowedCharacters.symbols || '-_.~!*'
+ 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[Math.floor(Math.random() * chars.lowercase.length)]
+ chars.lowercase[randomInt(chars.lowercase.length)]
));
}
-
+
if (required.uppercase > 0) {
parts.push(...Array(required.uppercase).fill(0).map(() =>
- chars.uppercase[Math.floor(Math.random() * chars.uppercase.length)]
+ chars.uppercase[randomInt(chars.uppercase.length)]
));
}
-
+
if (required.digits > 0) {
parts.push(...Array(required.digits).fill(0).map(() =>
- chars.digits[Math.floor(Math.random() * chars.digits.length)]
+ chars.digits[randomInt(chars.digits.length)]
));
}
-
+
if (required.symbols > 0) {
parts.push(...Array(required.symbols).fill(0).map(() =>
- chars.symbols[Math.floor(Math.random() * chars.symbols.length)]
+ chars.symbols[randomInt(chars.symbols.length)]
));
}
const requiredTotal = Object.values(required).reduce((a, b) => a + b, 0);
- const remainingLength = Math.max(minLength - requiredTotal, 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('');
- const allChars = Object.values(chars).join('');
parts.push(...Array(remainingLength).fill(0).map(() =>
- allChars[Math.floor(Math.random() * allChars.length)]
+ allowedChars[randomInt(allowedChars.length)]
));
// shuffle the array to mix up the characters
for (let i = parts.length - 1; i > 0; i--) {
- const j = Math.floor(Math.random() * (i + 1));
+ const j = randomInt(i + 1);
[parts[i], parts[j]] = [parts[j], parts[i]];
}
diff --git a/frontend/src/pages/secret-manager/SecretDashboardPage/components/ActionBar/CreateDynamicSecretForm/SqlDatabaseInputForm.tsx b/frontend/src/pages/secret-manager/SecretDashboardPage/components/ActionBar/CreateDynamicSecretForm/SqlDatabaseInputForm.tsx
index 354f226f8..063b2d08b 100644
--- a/frontend/src/pages/secret-manager/SecretDashboardPage/components/ActionBar/CreateDynamicSecretForm/SqlDatabaseInputForm.tsx
+++ b/frontend/src/pages/secret-manager/SecretDashboardPage/components/ActionBar/CreateDynamicSecretForm/SqlDatabaseInputForm.tsx
@@ -24,23 +24,21 @@ import { gatewaysQueryKeys, useCreateDynamicSecret } from "@app/hooks/api";
import { DynamicSecretProviders, SqlProviders } from "@app/hooks/api/dynamicSecret/types";
const passwordRequirementsSchema = z.object({
- minLength: z.number().min(1).max(100),
- maxLength: z.number().min(1).max(100),
+ 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)
- }),
- allowedCharacters: z
- .object({
- lowercase: z.string().optional(),
- uppercase: z.string().optional(),
- digits: z.string().optional(),
- symbols: z.string().optional()
- })
- .optional()
-});
+ }).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");
const formSchema = z.object({
provider: z.object({
@@ -153,21 +151,22 @@ export const SqlDatabaseInputForm = ({
control,
setValue,
formState: { isSubmitting },
- handleSubmit
+ handleSubmit,
+ watch
} = useForm({
resolver: zodResolver(formSchema),
defaultValues: {
provider: {
...getSqlStatements(SqlProviders.Postgres),
passwordRequirements: {
- minLength: 48,
- maxLength: 48,
+ length: 48,
required: {
lowercase: 1,
uppercase: 1,
digits: 1,
symbols: 0
- }
+ },
+ allowedSymbols: '-_.~!*'
}
}
}
@@ -208,9 +207,8 @@ export const SqlDatabaseInputForm = ({
setValue("provider.port", getDefaultPort(type));
// Update password requirements based on provider
- const minMaxLength = type === SqlProviders.Oracle ? 30 : 48;
- setValue("provider.passwordRequirements.minLength", minMaxLength);
- setValue("provider.passwordRequirements.maxLength", minMaxLength);
+ const length = type === SqlProviders.Oracle ? 30 : 48;
+ setValue("provider.passwordRequirements.length", length);
};
return (
@@ -499,41 +497,21 @@ export const SqlDatabaseInputForm = ({
Set constraints on the generated database password
-
+
(
field.onChange(Number(e.target.value))}
- />
-
- )}
- />
- (
-
- field.onChange(Number(e.target.value))}
/>
@@ -543,7 +521,19 @@ export const SqlDatabaseInputForm = ({
-
Required Characters
+
Minimum Required Character Counts
+
+ {(() => {
+ const total = Object.values(watch("provider.passwordRequirements.required") || {}).reduce((sum, count) => sum + Number(count || 0), 0);
+ const length = watch("provider.passwordRequirements.length") || 0;
+ const isError = total > length;
+ return (
+
+ Total required characters: {total} {isError ? `(exceeds length of ${length})` : ""}
+
+ );
+ })()}
+
diff --git a/frontend/src/pages/secret-manager/SecretDashboardPage/components/DynamicSecretListView/EditDynamicSecretForm/EditDynamicSecretSqlProviderForm.tsx b/frontend/src/pages/secret-manager/SecretDashboardPage/components/DynamicSecretListView/EditDynamicSecretForm/EditDynamicSecretSqlProviderForm.tsx
index 5cf5c6524..85fb6456e 100644
--- a/frontend/src/pages/secret-manager/SecretDashboardPage/components/DynamicSecretListView/EditDynamicSecretForm/EditDynamicSecretSqlProviderForm.tsx
+++ b/frontend/src/pages/secret-manager/SecretDashboardPage/components/DynamicSecretListView/EditDynamicSecretForm/EditDynamicSecretSqlProviderForm.tsx
@@ -24,23 +24,21 @@ import { gatewaysQueryKeys, useUpdateDynamicSecret } from "@app/hooks/api";
import { SqlProviders, TDynamicSecret } from "@app/hooks/api/dynamicSecret/types";
const passwordRequirementsSchema = z.object({
- minLength: z.number().min(1).max(100),
- maxLength: z.number().min(1).max(100),
+ 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)
- }),
- allowedCharacters: z
- .object({
- lowercase: z.string().optional(),
- uppercase: z.string().optional(),
- digits: z.string().optional(),
- symbols: z.string().optional()
- })
- .optional()
-});
+ }).refine((data) => {
+ const total = Object.values(data).reduce((sum, count) => sum + count, 0);
+ return total <= 250; // Sanity check for individual validation
+ }, "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");
const formSchema = z.object({
inputs: z
@@ -103,14 +101,14 @@ export const EditDynamicSecretSqlProviderForm = ({
projectSlug
}: Props) => {
const getDefaultPasswordRequirements = (provider: SqlProviders) => ({
- minLength: provider === SqlProviders.Oracle ? 30 : 48,
- maxLength: provider === SqlProviders.Oracle ? 30 : 48,
+ length: provider === SqlProviders.Oracle ? 30 : 48,
required: {
lowercase: 1,
uppercase: 1,
digits: 1,
symbols: 0
- }
+ },
+ allowedSymbols: '-_.~!*'
});
const {
@@ -463,41 +461,21 @@ export const EditDynamicSecretSqlProviderForm = ({
Set constraints on the generated database password
-
+
(
field.onChange(Number(e.target.value))}
- />
-
- )}
- />
- (
-
- field.onChange(Number(e.target.value))}
/>
@@ -507,7 +485,19 @@ export const EditDynamicSecretSqlProviderForm = ({
-
Required Characters
+
Minimum Required Character Counts
+
+ {(() => {
+ const total = Object.values(watch("inputs.passwordRequirements.required") || {}).reduce((sum, count) => sum + Number(count || 0), 0);
+ const length = watch("inputs.passwordRequirements.length") || 0;
+ const isError = total > length;
+ return (
+
+ Total required characters: {total} {isError ? `(exceeds length of ${length})` : ""}
+
+ );
+ })()}
+
(
field.onChange(Number(e.target.value))}
- />
+ type="number"
+ min={0}
+ {...field}
+ onChange={(e) => field.onChange(Number(e.target.value))}
+ />
)}
/>
@@ -534,16 +525,17 @@ export const EditDynamicSecretSqlProviderForm = ({
defaultValue={1}
render={({ field, fieldState: { error } }) => (
field.onChange(Number(e.target.value))}
- />
+ type="number"
+ min={0}
+ {...field}
+ onChange={(e) => field.onChange(Number(e.target.value))}
+ />
)}
/>
@@ -553,16 +545,17 @@ export const EditDynamicSecretSqlProviderForm = ({
defaultValue={1}
render={({ field, fieldState: { error } }) => (
field.onChange(Number(e.target.value))}
- />
+ type="number"
+ min={0}
+ {...field}
+ onChange={(e) => field.onChange(Number(e.target.value))}
+ />
)}
/>
@@ -572,16 +565,17 @@ export const EditDynamicSecretSqlProviderForm = ({
defaultValue={0}
render={({ field, fieldState: { error } }) => (
field.onChange(Number(e.target.value))}
- />
+ type="number"
+ min={0}
+ {...field}
+ onChange={(e) => field.onChange(Number(e.target.value))}
+ />
)}
/>
@@ -589,65 +583,22 @@ export const EditDynamicSecretSqlProviderForm = ({
From ed37b997567748badf5fc869cc7b2142ee51442b Mon Sep 17 00:00:00 2001
From: Daniel Hougaard
Date: Tue, 11 Mar 2025 20:34:35 +0400
Subject: [PATCH 40/71] fix: list secrets permissioning bug
---
.../services/secret-v2-bridge/secret-v2-bridge-service.ts | 6 +-----
1 file changed, 1 insertion(+), 5 deletions(-)
diff --git a/backend/src/services/secret-v2-bridge/secret-v2-bridge-service.ts b/backend/src/services/secret-v2-bridge/secret-v2-bridge-service.ts
index 8238f3467..dc975854f 100644
--- a/backend/src/services/secret-v2-bridge/secret-v2-bridge-service.ts
+++ b/backend/src/services/secret-v2-bridge/secret-v2-bridge-service.ts
@@ -909,11 +909,7 @@ export const secretV2BridgeServiceFactory = ({
actorOrgId,
actionProjectType: ActionProjectType.SecretManager
});
- throwIfMissingSecretReadValueOrDescribePermission(permission, ProjectPermissionSecretActions.DescribeSecret, {
- environment,
- secretPath: path,
- secretTags: params.tagSlugs
- });
+ throwIfMissingSecretReadValueOrDescribePermission(permission, ProjectPermissionSecretActions.DescribeSecret);
let paths: { folderId: string; path: string }[] = [];
From 4dc56033b1214e579a72da6bdfe216559a6c2087 Mon Sep 17 00:00:00 2001
From: Sheen Capadngan
Date: Wed, 12 Mar 2025 00:41:05 +0800
Subject: [PATCH 41/71] misc: improve support for jwks via http
---
.../identity-jwt-auth-service.ts | 24 ++++++++++++-------
1 file changed, 16 insertions(+), 8 deletions(-)
diff --git a/backend/src/services/identity-jwt-auth/identity-jwt-auth-service.ts b/backend/src/services/identity-jwt-auth/identity-jwt-auth-service.ts
index 2c9b6306e..555dc00a6 100644
--- a/backend/src/services/identity-jwt-auth/identity-jwt-auth-service.ts
+++ b/backend/src/services/identity-jwt-auth/identity-jwt-auth-service.ts
@@ -78,14 +78,22 @@ export const identityJwtAuthServiceFactory = ({
let tokenData: Record = {};
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);
From 75e0a68b685f284096e2c28a9acfe8250187da9a Mon Sep 17 00:00:00 2001
From: Maidul Islam
Date: Tue, 11 Mar 2025 12:46:43 -0400
Subject: [PATCH 42/71] remove password regex
---
backend/src/ee/services/license/license-fns.ts | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/backend/src/ee/services/license/license-fns.ts b/backend/src/ee/services/license/license-fns.ts
index 21d378802..ba89f965a 100644
--- a/backend/src/ee/services/license/license-fns.ts
+++ b/backend/src/ee/services/license/license-fns.ts
@@ -17,7 +17,7 @@ export const getDefaultOnPremFeatures = (): TFeatureSet => ({
environmentsUsed: 0,
identityLimit: null,
identitiesUsed: 0,
- dynamicSecret: false,
+ dynamicSecret: true,
secretVersioning: true,
pitRecovery: false,
ipAllowlisting: false,
From f5749e326a023211cbe6f9bd65ccc95d4a1f3a17 Mon Sep 17 00:00:00 2001
From: Maidul Islam
Date: Tue, 11 Mar 2025 12:49:55 -0400
Subject: [PATCH 43/71] remove regex and fix lint
---
backend/src/ee/services/license/license-fns.ts | 2 +-
frontend/src/hooks/api/dynamicSecret/types.ts | 1 -
2 files changed, 1 insertion(+), 2 deletions(-)
diff --git a/backend/src/ee/services/license/license-fns.ts b/backend/src/ee/services/license/license-fns.ts
index ba89f965a..21d378802 100644
--- a/backend/src/ee/services/license/license-fns.ts
+++ b/backend/src/ee/services/license/license-fns.ts
@@ -17,7 +17,7 @@ export const getDefaultOnPremFeatures = (): TFeatureSet => ({
environmentsUsed: 0,
identityLimit: null,
identitiesUsed: 0,
- dynamicSecret: true,
+ dynamicSecret: false,
secretVersioning: true,
pitRecovery: false,
ipAllowlisting: false,
diff --git a/frontend/src/hooks/api/dynamicSecret/types.ts b/frontend/src/hooks/api/dynamicSecret/types.ts
index 154449d5b..7d8c6920a 100644
--- a/frontend/src/hooks/api/dynamicSecret/types.ts
+++ b/frontend/src/hooks/api/dynamicSecret/types.ts
@@ -50,7 +50,6 @@ export type TDynamicSecretProvider =
database: string;
username: string;
password: string;
- passwordRegex?: string;
creationStatement: string;
revocationStatement: string;
renewStatement?: string;
From edf6a37fe54f791db4fc22f524a10f80dc698002 Mon Sep 17 00:00:00 2001
From: Maidul Islam
Date: Tue, 11 Mar 2025 13:08:04 -0400
Subject: [PATCH 44/71] fix lint
---
.../dynamic-secret/providers/models.ts | 22 ++++---
.../dynamic-secret/providers/sql-database.ts | 65 +++++++++++--------
.../src/server/routes/v2/password-router.ts | 4 +-
.../services/auth/auth-password-service.ts | 2 +-
4 files changed, 52 insertions(+), 41 deletions(-)
diff --git a/backend/src/ee/services/dynamic-secret/providers/models.ts b/backend/src/ee/services/dynamic-secret/providers/models.ts
index 1808057e7..449f6d8f6 100644
--- a/backend/src/ee/services/dynamic-secret/providers/models.ts
+++ b/backend/src/ee/services/dynamic-secret/providers/models.ts
@@ -114,15 +114,17 @@ export const DynamicSecretSqlDBSchema = z.object({
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"),
+ 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) => {
@@ -130,7 +132,7 @@ export const DynamicSecretSqlDBSchema = z.object({
return total <= data.length;
}, "Sum of required characters cannot exceed the total length")
.optional()
- .describe('Password generation requirements'),
+ .describe("Password generation requirements"),
creationStatement: z.string().trim(),
revocationStatement: z.string().trim(),
renewStatement: z.string().trim().optional(),
diff --git a/backend/src/ee/services/dynamic-secret/providers/sql-database.ts b/backend/src/ee/services/dynamic-secret/providers/sql-database.ts
index bb9240139..eea9fef94 100644
--- a/backend/src/ee/services/dynamic-secret/providers/sql-database.ts
+++ b/backend/src/ee/services/dynamic-secret/providers/sql-database.ts
@@ -1,8 +1,7 @@
+import { randomInt } from "crypto";
import handlebars from "handlebars";
import knex from "knex";
-import { customAlphabet } from "nanoid";
import { z } from "zod";
-import { randomInt } from 'crypto';
import { withGatewayProxy } from "@app/lib/gateway";
import { alphaNumericNanoId } from "@app/lib/nanoid";
@@ -21,7 +20,7 @@ const DEFAULT_PASSWORD_REQUIREMENTS = {
digits: 1,
symbols: 0
},
- allowedSymbols: '-_.~!*'
+ allowedSymbols: "-_.~!*"
};
const ORACLE_PASSWORD_REQUIREMENTS = {
@@ -35,38 +34,46 @@ const generatePassword = (provider: SqlProviders, requirements?: PasswordRequire
try {
const { length, required, allowedSymbols } = finalReqs;
-
+
const chars = {
- lowercase: 'abcdefghijklmnopqrstuvwxyz',
- uppercase: 'ABCDEFGHIJKLMNOPQRSTUVWXYZ',
- digits: '0123456789',
- symbols: allowedSymbols || '-_.~!*'
+ 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)]
- ));
+ 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)]
- ));
+ 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)]
- ));
+ 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)]
- ));
+ parts.push(
+ ...Array(required.symbols)
+ .fill(0)
+ .map(() => chars.symbols[randomInt(chars.symbols.length)])
+ );
}
const requiredTotal = Object.values(required).reduce((a, b) => a + b, 0);
@@ -75,21 +82,23 @@ const generatePassword = (provider: SqlProviders, requirements?: PasswordRequire
const allowedChars = Object.entries(chars)
.filter(([key]) => required[key as keyof typeof required] > 0)
.map(([, value]) => value)
- .join('');
+ .join("");
- parts.push(...Array(remainingLength).fill(0).map(() =>
- allowedChars[randomInt(allowedChars.length)]
- ));
+ 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--) {
+ 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('');
+ return parts.join("");
} catch (error: unknown) {
- const message = error instanceof Error ? error.message : 'Unknown error';
+ const message = error instanceof Error ? error.message : "Unknown error";
throw new Error(`Failed to generate password: ${message}`);
}
};
diff --git a/backend/src/server/routes/v2/password-router.ts b/backend/src/server/routes/v2/password-router.ts
index 165130dec..63b6d8aac 100644
--- a/backend/src/server/routes/v2/password-router.ts
+++ b/backend/src/server/routes/v2/password-router.ts
@@ -1,10 +1,10 @@
import { z } from "zod";
import { authRateLimit } from "@app/server/config/rateLimiter";
-import { validatePasswordResetAuthorization } from "@app/services/auth/auth-fns";
-import { AuthMode } from "@app/services/auth/auth-type";
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({
diff --git a/backend/src/services/auth/auth-password-service.ts b/backend/src/services/auth/auth-password-service.ts
index f5f42a236..14fb58258 100644
--- a/backend/src/services/auth/auth-password-service.ts
+++ b/backend/src/services/auth/auth-password-service.ts
@@ -7,6 +7,7 @@ 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";
@@ -25,7 +26,6 @@ import {
TSetupPasswordViaBackupKeyDTO
} from "./auth-password-type";
import { ActorType, AuthMethod, AuthTokenType } from "./auth-type";
-import { logger } from "@app/lib/logger";
type TAuthPasswordServiceFactoryDep = {
authDAL: TAuthDALFactory;
From e7c512999e8312b85bb65a08cd54819cff09b2a0 Mon Sep 17 00:00:00 2001
From: Mahyar Mirrashed
Date: Tue, 11 Mar 2025 13:30:45 -0700
Subject: [PATCH 45/71] feat(admin): add ability to view machine identities
---
backend/src/server/routes/index.ts | 1 +
backend/src/server/routes/v1/admin-router.ts | 39 +++++++-
backend/src/services/identity/identity-dal.ts | 38 +++++++-
.../super-admin/super-admin-service.ts | 19 +++-
.../services/super-admin/super-admin-types.ts | 6 ++
frontend/src/hooks/api/admin/queries.ts | 29 +++++-
frontend/src/hooks/api/admin/types.ts | 5 +
.../pages/admin/OverviewPage/OverviewPage.tsx | 6 ++
.../OverviewPage/components/IdentityPanel.tsx | 95 +++++++++++++++++++
.../OverviewPage/components/UserPanel.tsx | 4 +-
10 files changed, 232 insertions(+), 10 deletions(-)
create mode 100644 frontend/src/pages/admin/OverviewPage/components/IdentityPanel.tsx
diff --git a/backend/src/server/routes/index.ts b/backend/src/server/routes/index.ts
index efc1cb865..b9f47cb7e 100644
--- a/backend/src/server/routes/index.ts
+++ b/backend/src/server/routes/index.ts
@@ -635,6 +635,7 @@ export const registerRoutes = async (
});
const superAdminService = superAdminServiceFactory({
userDAL,
+ identityDAL,
userAliasDAL,
authService: loginService,
serverCfgDAL: superAdminDAL,
diff --git a/backend/src/server/routes/v1/admin-router.ts b/backend/src/server/routes/v1/admin-router.ts
index b63550d2c..a1c433650 100644
--- a/backend/src/server/routes/v1/admin-router.ts
+++ b/backend/src/server/routes/v1/admin-router.ts
@@ -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";
@@ -154,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",
diff --git a/backend/src/services/identity/identity-dal.ts b/backend/src/services/identity/identity-dal.ts
index a74a84ce3..8b7fccab3 100644
--- a/backend/src/services/identity/identity-dal.ts
+++ b/backend/src/services/identity/identity-dal.ts
@@ -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;
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 };
};
diff --git a/backend/src/services/super-admin/super-admin-service.ts b/backend/src/services/super-admin/super-admin-service.ts
index 8efdfd7f9..e8e01a44f 100644
--- a/backend/src/services/super-admin/super-admin-service.ts
+++ b/backend/src/services/super-admin/super-admin-service.ts
@@ -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;
serverCfgDAL: TSuperAdminDALFactory;
userDAL: TUserDALFactory;
userAliasDAL: Pick;
@@ -51,6 +53,7 @@ const ADMIN_CONFIG_DB_UUID = "00000000-0000-0000-0000-000000000000";
export const superAdminServiceFactory = ({
serverCfgDAL,
userDAL,
+ identityDAL,
userAliasDAL,
authService,
orgService,
@@ -143,8 +146,8 @@ export const superAdminServiceFactory = ({
const canServerAdminAccessAfterApply =
data.enabledLoginMethods.some((loginMethod) =>
- loginMethodToAuthMethod[loginMethod as LoginMethod].some(
- (authMethod) => superAdminUser.authMethods?.includes(authMethod)
+ loginMethodToAuthMethod[loginMethod as LoginMethod].some((authMethod) =>
+ superAdminUser.authMethods?.includes(authMethod)
)
) ||
isUserSamlAccessEnabled ||
@@ -292,6 +295,15 @@ export const superAdminServiceFactory = ({
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({
@@ -389,6 +401,7 @@ export const superAdminServiceFactory = ({
adminSignUp,
getUsers,
deleteUser,
+ getIdentities,
getAdminSlackConfig,
updateRootEncryptionStrategy,
getConfiguredEncryptionStrategies,
diff --git a/backend/src/services/super-admin/super-admin-types.ts b/backend/src/services/super-admin/super-admin-types.ts
index d6de67e59..54a42c2ca 100644
--- a/backend/src/services/super-admin/super-admin-types.ts
+++ b/backend/src/services/super-admin/super-admin-types.ts
@@ -23,6 +23,12 @@ export type TAdminGetUsersDTO = {
adminsOnly: boolean;
};
+export type TAdminGetIdentitiesDTO = {
+ offset: number;
+ limit: number;
+ searchTerm: string;
+};
+
export enum LoginMethod {
EMAIL = "email",
GOOGLE = "google",
diff --git a/frontend/src/hooks/api/admin/queries.ts b/frontend/src/hooks/api/admin/queries.ts
index 12ae1cf64..7e2d60831 100644
--- a/frontend/src/hooks/api/admin/queries.ts
+++ b/frontend/src/hooks/api/admin/queries.ts
@@ -4,19 +4,24 @@ import { apiRequest } from "@app/config/request";
import { User } from "../types";
import {
+ AdminGetIdentitiesFilters,
AdminGetUsersFilters,
AdminSlackConfig,
TGetServerRootKmsEncryptionDetails,
TServerConfig
} from "./types";
+import { Identity } from "@app/hooks/api/identities/types.ts";
export const adminStandaloneKeys = {
- getUsers: "get-users"
+ getUsers: "get-users",
+ getIdentities: "get-identities"
};
export const adminQueryKeys = {
serverConfig: () => ["server-config"] as const,
getUsers: (filters: AdminGetUsersFilters) => [adminStandaloneKeys.getUsers, { filters }] as const,
+ getIdentities: (filters: AdminGetIdentitiesFilters) =>
+ [adminStandaloneKeys.getIdentities, { filters }] as const,
getAdminSlackConfig: () => ["admin-slack-config"] as const,
getServerEncryptionStrategies: () => ["server-encryption-strategies"] as const
};
@@ -68,6 +73,28 @@ export const useAdminGetUsers = (filters: AdminGetUsersFilters) => {
});
};
+export const useAdminGetIdentities = (filters: AdminGetIdentitiesFilters) => {
+ return useInfiniteQuery({
+ initialPageParam: 0,
+ queryKey: adminQueryKeys.getIdentities(filters),
+ queryFn: async ({ pageParam }) => {
+ const { data } = await apiRequest.get<{ identities: Identity[] }>(
+ "/api/v1/admin/identity-management/identities",
+ {
+ params: {
+ ...filters,
+ offset: pageParam
+ }
+ }
+ );
+
+ return data.identities;
+ },
+ getNextPageParam: (lastPage, pages) =>
+ lastPage.length !== 0 ? pages.length * filters.limit : undefined
+ });
+};
+
export const useGetAdminSlackConfig = () => {
return useQuery({
queryKey: adminQueryKeys.getAdminSlackConfig(),
diff --git a/frontend/src/hooks/api/admin/types.ts b/frontend/src/hooks/api/admin/types.ts
index 80e3edc92..11f2cf44f 100644
--- a/frontend/src/hooks/api/admin/types.ts
+++ b/frontend/src/hooks/api/admin/types.ts
@@ -53,6 +53,11 @@ export type AdminGetUsersFilters = {
adminsOnly: boolean;
};
+export type AdminGetIdentitiesFilters = {
+ limit: number;
+ searchTerm: string;
+};
+
export type AdminSlackConfig = {
clientId: string;
clientSecret: string;
diff --git a/frontend/src/pages/admin/OverviewPage/OverviewPage.tsx b/frontend/src/pages/admin/OverviewPage/OverviewPage.tsx
index ad07b71ce..f48665653 100644
--- a/frontend/src/pages/admin/OverviewPage/OverviewPage.tsx
+++ b/frontend/src/pages/admin/OverviewPage/OverviewPage.tsx
@@ -34,6 +34,7 @@ import { EncryptionPanel } from "./components/EncryptionPanel";
import { IntegrationPanel } from "./components/IntegrationPanel";
import { RateLimitPanel } from "./components/RateLimitPanel";
import { UserPanel } from "./components/UserPanel";
+import { IdentityPanel } from "@app/pages/admin/OverviewPage/components/IdentityPanel.tsx";
enum TabSections {
Settings = "settings",
@@ -42,6 +43,7 @@ enum TabSections {
RateLimit = "rate-limit",
Integrations = "integrations",
Users = "users",
+ Identities = "identities",
Kmip = "kmip"
}
@@ -164,6 +166,7 @@ export const OverviewPage = () => {
Rate Limit
Integrations
Users
+ Identities
@@ -409,6 +412,9 @@ export const OverviewPage = () => {
+
+
+
)}
diff --git a/frontend/src/pages/admin/OverviewPage/components/IdentityPanel.tsx b/frontend/src/pages/admin/OverviewPage/components/IdentityPanel.tsx
new file mode 100644
index 000000000..4851db656
--- /dev/null
+++ b/frontend/src/pages/admin/OverviewPage/components/IdentityPanel.tsx
@@ -0,0 +1,95 @@
+import { useState } from "react";
+import { faMagnifyingGlass, faServer } from "@fortawesome/free-solid-svg-icons";
+import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
+
+import {
+ Button,
+ EmptyState,
+ Input,
+ Table,
+ TableContainer,
+ TableSkeleton,
+ TBody,
+ Td,
+ Th,
+ THead,
+ Tr
+} from "@app/components/v2";
+import { useDebounce } from "@app/hooks";
+import { useAdminGetIdentities } from "@app/hooks/api/admin/queries.ts";
+
+const IdentityPanelTable = () => {
+ const [searchIdentityFilter, setSearchIdentityFilter] = useState("");
+ const [debouncedSearchTerm] = useDebounce(searchIdentityFilter, 500);
+
+ const { data, isPending, isFetchingNextPage, hasNextPage, fetchNextPage } = useAdminGetIdentities(
+ {
+ limit: 20,
+ searchTerm: debouncedSearchTerm
+ }
+ );
+
+ const isEmpty = !isPending && !data?.pages?.[0].length;
+
+ return (
+ <>
+
+ setSearchIdentityFilter(e.target.value)}
+ leftIcon={}
+ placeholder="Search identities by name..."
+ className="flex-1"
+ />
+
+
+
+
+
+
+ | Name |
+ Auth Method |
+
+
+
+ {isPending && }
+ {!isPending &&
+ data?.pages?.map((identities) =>
+ identities.map(({ name, authMethod, id }) => (
+
+ | {name} |
+
+ {authMethod || None}
+ |
+
+ ))
+ )}
+
+
+ {!isPending && isEmpty && }
+
+ {!isEmpty && (
+
fetchNextPage()}
+ >
+ {hasNextPage ? "Load More" : "End of list"}
+
+ )}
+
+ >
+ );
+};
+
+export const IdentityPanel = () => (
+
+);
diff --git a/frontend/src/pages/admin/OverviewPage/components/UserPanel.tsx b/frontend/src/pages/admin/OverviewPage/components/UserPanel.tsx
index 6654439de..0e5173188 100644
--- a/frontend/src/pages/admin/OverviewPage/components/UserPanel.tsx
+++ b/frontend/src/pages/admin/OverviewPage/components/UserPanel.tsx
@@ -61,12 +61,12 @@ const UserPanelTable = ({
const [adminsOnly, setAdminsOnly] = useState(false);
const { user } = useUser();
const userId = user?.id || "";
- const [debounedSearchTerm] = useDebounce(searchUserFilter, 500);
+ const [debouncedSearchTerm] = useDebounce(searchUserFilter, 500);
const { subscription } = useSubscription();
const { data, isPending, isFetchingNextPage, hasNextPage, fetchNextPage } = useAdminGetUsers({
limit: 20,
- searchTerm: debounedSearchTerm,
+ searchTerm: debouncedSearchTerm,
adminsOnly
});
From 39b7a4a11117a98467df9750d85e7467ef112e92 Mon Sep 17 00:00:00 2001
From: Mahyar Mirrashed
Date: Tue, 11 Mar 2025 13:31:23 -0700
Subject: [PATCH 46/71] chore(nix): add python312 to list of dependencies
---
flake.nix | 1 +
1 file changed, 1 insertion(+)
diff --git a/flake.nix b/flake.nix
index 9d995a551..094cfedc6 100644
--- a/flake.nix
+++ b/flake.nix
@@ -14,6 +14,7 @@
git
lazygit
+ python312Full
nodejs_20
nodePackages.prettier
infisical
From 627e17b3aee4a632881906282134deab639f7c8a Mon Sep 17 00:00:00 2001
From: Mahyar Mirrashed
Date: Tue, 11 Mar 2025 14:10:08 -0700
Subject: [PATCH 47/71] fix(admin): return back auth method from schema too
---
backend/src/server/routes/v1/admin-router.ts | 1 +
1 file changed, 1 insertion(+)
diff --git a/backend/src/server/routes/v1/admin-router.ts b/backend/src/server/routes/v1/admin-router.ts
index a1c433650..bb62f2e1b 100644
--- a/backend/src/server/routes/v1/admin-router.ts
+++ b/backend/src/server/routes/v1/admin-router.ts
@@ -170,6 +170,7 @@ export const registerAdminRouter = async (server: FastifyZodProvider) => {
200: z.object({
identities: IdentitiesSchema.pick({
name: true,
+ authMethod: true,
id: true
}).array()
})
From 7e14c589319118d52c4a6cdc7384203335a18db7 Mon Sep 17 00:00:00 2001
From: Scott Wilson
Date: Tue, 11 Mar 2025 14:12:33 -0700
Subject: [PATCH 48/71] improvement: clarify databricks native integration
behavior and suggest desingated scope for sync/native integration
---
docs/integrations/cloud/databricks.mdx | 6 ++++++
docs/integrations/secret-syncs/databricks.mdx | 2 ++
docs/mint.json | 3 +--
.../SecretSyncDestinationFields/DatabricksSyncFields.tsx | 2 ++
.../DatabricksConfigurePage/DatabricksConfigurePage.tsx | 9 +++++++--
5 files changed, 18 insertions(+), 4 deletions(-)
diff --git a/docs/integrations/cloud/databricks.mdx b/docs/integrations/cloud/databricks.mdx
index 7fee3acd3..1971190de 100644
--- a/docs/integrations/cloud/databricks.mdx
+++ b/docs/integrations/cloud/databricks.mdx
@@ -7,6 +7,12 @@ Prerequisites:
- Set up and add secrets to [Infisical Cloud](https://app.infisical.com)
+
+ 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.
+
+
Obtain a Personal Access Token in **User Settings** > **Developer** > **Access Tokens**.
diff --git a/docs/integrations/secret-syncs/databricks.mdx b/docs/integrations/secret-syncs/databricks.mdx
index 8f305ecd6..148542708 100644
--- a/docs/integrations/secret-syncs/databricks.mdx
+++ b/docs/integrations/secret-syncs/databricks.mdx
@@ -34,6 +34,8 @@ description: "Learn how to configure a Databricks Sync for Infisical."
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.
5. Configure the **Sync Options** to specify how secrets should be synced, then click **Next**.
diff --git a/docs/mint.json b/docs/mint.json
index f604ff310..bf40a9778 100644
--- a/docs/mint.json
+++ b/docs/mint.json
@@ -644,8 +644,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"
]
},
{
diff --git a/frontend/src/components/secret-syncs/forms/SecretSyncDestinationFields/DatabricksSyncFields.tsx b/frontend/src/components/secret-syncs/forms/SecretSyncDestinationFields/DatabricksSyncFields.tsx
index c6cfff0e5..533d0d2cf 100644
--- a/frontend/src/components/secret-syncs/forms/SecretSyncDestinationFields/DatabricksSyncFields.tsx
+++ b/frontend/src/components/secret-syncs/forms/SecretSyncDestinationFields/DatabricksSyncFields.tsx
@@ -40,6 +40,8 @@ export const DatabricksSyncFields = () => {
isError={Boolean(error)}
errorText={error?.message}
label="Secret Scope"
+ tooltipClassName="max-w-md"
+ tooltipText="Infisical recommends creating a designated Databricks secret scope for your sync to prevent removal of secrets not managed by Infisical."
helperText={
{
Pro Tip
- After creating an integration, your secrets will start syncing immediately. This might
- cause an unexpected override of current secrets in Databricks with secrets from Infisical.
+ 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.
From 1a06b3e1f59b63c5c9161a70f6fdf84408d9eee3 Mon Sep 17 00:00:00 2001
From: Mahyar Mirrashed
Date: Tue, 11 Mar 2025 14:30:04 -0700
Subject: [PATCH 49/71] fix(admin): stop returning auth method on table
---
backend/src/server/routes/v1/admin-router.ts | 1 -
.../admin/OverviewPage/components/IdentityPanel.tsx | 10 +++-------
2 files changed, 3 insertions(+), 8 deletions(-)
diff --git a/backend/src/server/routes/v1/admin-router.ts b/backend/src/server/routes/v1/admin-router.ts
index bb62f2e1b..a1c433650 100644
--- a/backend/src/server/routes/v1/admin-router.ts
+++ b/backend/src/server/routes/v1/admin-router.ts
@@ -170,7 +170,6 @@ export const registerAdminRouter = async (server: FastifyZodProvider) => {
200: z.object({
identities: IdentitiesSchema.pick({
name: true,
- authMethod: true,
id: true
}).array()
})
diff --git a/frontend/src/pages/admin/OverviewPage/components/IdentityPanel.tsx b/frontend/src/pages/admin/OverviewPage/components/IdentityPanel.tsx
index 4851db656..3b89a011d 100644
--- a/frontend/src/pages/admin/OverviewPage/components/IdentityPanel.tsx
+++ b/frontend/src/pages/admin/OverviewPage/components/IdentityPanel.tsx
@@ -47,20 +47,16 @@ const IdentityPanelTable = () => {
- | Name |
- Auth Method |
+ Name |
{isPending && }
{!isPending &&
data?.pages?.map((identities) =>
- identities.map(({ name, authMethod, id }) => (
+ identities.map(({ name, id }) => (
- | {name} |
-
- {authMethod || None}
- |
+ {name} |
))
)}
From 3887ce800bb45ce71e1271e3bcfe1974b43a236d Mon Sep 17 00:00:00 2001
From: Mahyar Mirrashed
Date: Tue, 11 Mar 2025 14:34:14 -0700
Subject: [PATCH 50/71] refactor(admin): fix spelling for variable
---
.../components/ServerAdminsPanel/ServerAdminsPanel.tsx | 10 +++++-----
1 file changed, 5 insertions(+), 5 deletions(-)
diff --git a/frontend/src/layouts/OrganizationLayout/components/ServerAdminsPanel/ServerAdminsPanel.tsx b/frontend/src/layouts/OrganizationLayout/components/ServerAdminsPanel/ServerAdminsPanel.tsx
index c834daad8..82231c0e4 100644
--- a/frontend/src/layouts/OrganizationLayout/components/ServerAdminsPanel/ServerAdminsPanel.tsx
+++ b/frontend/src/layouts/OrganizationLayout/components/ServerAdminsPanel/ServerAdminsPanel.tsx
@@ -19,17 +19,17 @@ import { useGetOrgUsers } from "@app/hooks/api";
export const ServerAdminsPanel = () => {
const [searchUserFilter, setSearchUserFilter] = useState("");
- const [debounedSearchTerm] = useDebounce(searchUserFilter, 500);
+ const [debouncedSearchTerm] = useDebounce(searchUserFilter, 500);
const { currentOrg } = useOrganization();
const { data: orgUsers, isPending } = useGetOrgUsers(currentOrg?.id || "");
const adminUsers = orgUsers?.filter((orgUser) => {
const isSuperAdmin = orgUser.user.superAdmin;
- const matchesSearch = debounedSearchTerm
- ? orgUser.user.email?.toLowerCase().includes(debounedSearchTerm.toLowerCase()) ||
- orgUser.user.firstName?.toLowerCase().includes(debounedSearchTerm.toLowerCase()) ||
- orgUser.user.lastName?.toLowerCase().includes(debounedSearchTerm.toLowerCase())
+ const matchesSearch = debouncedSearchTerm
+ ? orgUser.user.email?.toLowerCase().includes(debouncedSearchTerm.toLowerCase()) ||
+ orgUser.user.firstName?.toLowerCase().includes(debouncedSearchTerm.toLowerCase()) ||
+ orgUser.user.lastName?.toLowerCase().includes(debouncedSearchTerm.toLowerCase())
: true;
return isSuperAdmin && matchesSearch;
});
From 387780aa942a00c8ddebef431458f27eab6dc501 Mon Sep 17 00:00:00 2001
From: Mahyar Mirrashed
Date: Tue, 11 Mar 2025 14:44:22 -0700
Subject: [PATCH 51/71] fix(lint): remove file extension from imports
JetBrains accidentally added these when I ran the auto-complete. Weird.
---
frontend/src/hooks/api/admin/queries.ts | 2 +-
frontend/src/pages/admin/OverviewPage/OverviewPage.tsx | 2 +-
.../src/pages/admin/OverviewPage/components/IdentityPanel.tsx | 2 +-
3 files changed, 3 insertions(+), 3 deletions(-)
diff --git a/frontend/src/hooks/api/admin/queries.ts b/frontend/src/hooks/api/admin/queries.ts
index 7e2d60831..496990abe 100644
--- a/frontend/src/hooks/api/admin/queries.ts
+++ b/frontend/src/hooks/api/admin/queries.ts
@@ -10,7 +10,7 @@ import {
TGetServerRootKmsEncryptionDetails,
TServerConfig
} from "./types";
-import { Identity } from "@app/hooks/api/identities/types.ts";
+import { Identity } from "@app/hooks/api/identities/types";
export const adminStandaloneKeys = {
getUsers: "get-users",
diff --git a/frontend/src/pages/admin/OverviewPage/OverviewPage.tsx b/frontend/src/pages/admin/OverviewPage/OverviewPage.tsx
index f48665653..776c5e20f 100644
--- a/frontend/src/pages/admin/OverviewPage/OverviewPage.tsx
+++ b/frontend/src/pages/admin/OverviewPage/OverviewPage.tsx
@@ -34,7 +34,7 @@ import { EncryptionPanel } from "./components/EncryptionPanel";
import { IntegrationPanel } from "./components/IntegrationPanel";
import { RateLimitPanel } from "./components/RateLimitPanel";
import { UserPanel } from "./components/UserPanel";
-import { IdentityPanel } from "@app/pages/admin/OverviewPage/components/IdentityPanel.tsx";
+import { IdentityPanel } from "@app/pages/admin/OverviewPage/components/IdentityPanel";
enum TabSections {
Settings = "settings",
diff --git a/frontend/src/pages/admin/OverviewPage/components/IdentityPanel.tsx b/frontend/src/pages/admin/OverviewPage/components/IdentityPanel.tsx
index 3b89a011d..ee2166a4e 100644
--- a/frontend/src/pages/admin/OverviewPage/components/IdentityPanel.tsx
+++ b/frontend/src/pages/admin/OverviewPage/components/IdentityPanel.tsx
@@ -16,7 +16,7 @@ import {
Tr
} from "@app/components/v2";
import { useDebounce } from "@app/hooks";
-import { useAdminGetIdentities } from "@app/hooks/api/admin/queries.ts";
+import { useAdminGetIdentities } from "@app/hooks/api/admin/queries";
const IdentityPanelTable = () => {
const [searchIdentityFilter, setSearchIdentityFilter] = useState("");
From de5a4327457150638e84ed201a94b33cb5e1a8dd Mon Sep 17 00:00:00 2001
From: Mahyar Mirrashed
Date: Tue, 11 Mar 2025 14:54:03 -0700
Subject: [PATCH 52/71] fix(lint): appease the linter
There is a conflict between this and our Prettier configuration.
---
backend/src/services/super-admin/super-admin-service.ts | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/backend/src/services/super-admin/super-admin-service.ts b/backend/src/services/super-admin/super-admin-service.ts
index e8e01a44f..ee3247632 100644
--- a/backend/src/services/super-admin/super-admin-service.ts
+++ b/backend/src/services/super-admin/super-admin-service.ts
@@ -146,8 +146,8 @@ export const superAdminServiceFactory = ({
const canServerAdminAccessAfterApply =
data.enabledLoginMethods.some((loginMethod) =>
- loginMethodToAuthMethod[loginMethod as LoginMethod].some((authMethod) =>
- superAdminUser.authMethods?.includes(authMethod)
+ loginMethodToAuthMethod[loginMethod as LoginMethod].some(
+ (authMethod) => superAdminUser.authMethods?.includes(authMethod)
)
) ||
isUserSamlAccessEnabled ||
From 159c27ac6747da7f8733f63c2ae79920071b8e66 Mon Sep 17 00:00:00 2001
From: Maidul Islam
Date: Tue, 11 Mar 2025 18:43:18 -0400
Subject: [PATCH 53/71] 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```
---
cli/go.mod | 2 +-
cli/go.sum | 10 -----
cli/packages/cmd/gateway.go | 65 +++++++++++++++++++++++++++------
cli/packages/gateway/systemd.go | 2 +-
4 files changed, 55 insertions(+), 24 deletions(-)
diff --git a/cli/go.mod b/cli/go.mod
index bdb8839d0..b2d0c83ad 100644
--- a/cli/go.mod
+++ b/cli/go.mod
@@ -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
diff --git a/cli/go.sum b/cli/go.sum
index ab46476d7..ad70297bf 100644
--- a/cli/go.sum
+++ b/cli/go.sum
@@ -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=
diff --git a/cli/packages/cmd/gateway.go b/cli/packages/cmd/gateway.go
index d41c2c20e..ef4cdc944 100644
--- a/cli/packages/cmd/gateway.go
+++ b/cli/packages/cmd/gateway.go
@@ -4,7 +4,9 @@ import (
"context"
"fmt"
"os"
+ "os/exec"
"os/signal"
+ "runtime"
"syscall"
"time"
@@ -16,9 +18,11 @@ import (
)
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=
+ sudo infisical gateway install --token= --domain=`,
DisableFlagsInUseLine: true,
Args: cobra.NoArgs,
Run: func(cmd *cobra.Command, args []string) {
@@ -31,16 +35,6 @@ var gatewayCmd = &cobra.Command{
util.HandleError(fmt.Errorf("Token not found"))
}
- domain, err := cmd.Flags().GetString("domain")
- if err != nil {
- util.HandleError(err, "Unable to parse domain flag")
- }
-
- // Try to install systemd service if possible
- if err := gateway.InstallGatewaySystemdService(token.Token, domain); err != nil {
- log.Warn().Msgf("Failed to install systemd service: %v", err)
- }
-
Telemetry.CaptureEvent("cli-command:gateway", posthog.NewProperties().Set("version", util.CLI_VERSION))
sigCh := make(chan os.Signal, 1)
@@ -110,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= --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",
@@ -139,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)
}
diff --git a/cli/packages/gateway/systemd.go b/cli/packages/gateway/systemd.go
index 8183f3535..601cb9e90 100644
--- a/cli/packages/gateway/systemd.go
+++ b/cli/packages/gateway/systemd.go
@@ -17,7 +17,7 @@ After=network.target
[Service]
Type=simple
EnvironmentFile=/etc/infisical/gateway.conf
-ExecStart=/usr/local/bin/infisical gateway
+ExecStart=infisical gateway
Restart=on-failure
InaccessibleDirectories=/home
PrivateTmp=yes
From 84cc7bcd6cbff089627dfd656aefd77653ded662 Mon Sep 17 00:00:00 2001
From: Maidul Islam
Date: Tue, 11 Mar 2025 19:01:47 -0400
Subject: [PATCH 54/71] add docs + fix nit
---
cli/packages/cmd/gateway.go | 2 +-
docs/cli/commands/gateway.mdx | 107 ++++++++++++++++++++++++++++++++++
docs/mint.json | 1 +
3 files changed, 109 insertions(+), 1 deletion(-)
create mode 100644 docs/cli/commands/gateway.mdx
diff --git a/cli/packages/cmd/gateway.go b/cli/packages/cmd/gateway.go
index ef4cdc944..81baf3910 100644
--- a/cli/packages/cmd/gateway.go
+++ b/cli/packages/cmd/gateway.go
@@ -28,7 +28,7 @@ var gatewayCmd = &cobra.Command{
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 {
diff --git a/docs/cli/commands/gateway.mdx b/docs/cli/commands/gateway.mdx
new file mode 100644
index 000000000..fd035f1fd
--- /dev/null
+++ b/docs/cli/commands/gateway.mdx
@@ -0,0 +1,107 @@
+---
+title: "infisical gateway"
+description: "Run the Infisical gateway or manage its systemd service"
+---
+
+
+
+ ```bash
+ infisical gateway --token=
+ ```
+
+
+ ```bash
+ sudo infisical gateway install --token= --domain=
+ ```
+
+
+
+## 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
+
+
+ Run the Infisical gateway in the foreground. The gateway will connect to the relay service and maintain a persistent connection.
+
+ ```bash
+ infisical gateway --token= --domain=
+ ```
+
+ ### Flags
+
+
+ The machine identity access token to authenticate with Infisical.
+
+ ```bash
+ # Example
+ infisical gateway --token=
+ ```
+
+ You may also expose the token to the CLI by setting the environment variable `INFISICAL_TOKEN` before executing the gateway command.
+
+
+
+ Domain of your self-hosted Infisical instance.
+
+ ```bash
+ # Example
+ sudo infisical gateway install --domain=https://app.your-domain.com
+ ```
+
+
+
+
+ Install and enable the gateway as a systemd service. This command must be run with sudo on Linux.
+
+ ```bash
+ sudo infisical gateway install --token= --domain=
+ ```
+
+ ### Requirements
+ - Must be run on Linux
+ - Must be run with root/sudo privileges
+ - Requires systemd
+
+ ### Flags
+
+
+ The machine identity access token to authenticate with Infisical.
+
+ ```bash
+ # Example
+ sudo infisical gateway install --token=
+ ```
+
+ You may also expose the token to the CLI by setting the environment variable `INFISICAL_TOKEN` before executing the install command.
+
+
+
+ Domain of your self-hosted Infisical instance.
+
+ ```bash
+ # Example
+ sudo infisical gateway install --domain=https://app.your-domain.com
+ ```
+
+
+ ### 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
+ ```
+
diff --git a/docs/mint.json b/docs/mint.json
index f604ff310..5c97790bf 100644
--- a/docs/mint.json
+++ b/docs/mint.json
@@ -339,6 +339,7 @@
"cli/commands/secrets",
"cli/commands/dynamic-secrets",
"cli/commands/ssh",
+ "cli/commands/gateway",
"cli/commands/export",
"cli/commands/token",
"cli/commands/service-token",
From 3c59f7f350913476d450ec0654d3e96f49a09dbe Mon Sep 17 00:00:00 2001
From: Maidul Islam
Date: Tue, 11 Mar 2025 19:21:32 -0400
Subject: [PATCH 55/71] update deployment docs
---
.../platform/gateways/overview.mdx | 61 ++++++++++++++-----
1 file changed, 47 insertions(+), 14 deletions(-)
diff --git a/docs/documentation/platform/gateways/overview.mdx b/docs/documentation/platform/gateways/overview.mdx
index d263dc278..3f19e6f8d 100644
--- a/docs/documentation/platform/gateways/overview.mdx
+++ b/docs/documentation/platform/gateways/overview.mdx
@@ -45,19 +45,53 @@ Once authenticated, the Gateway establishes a secure connection with Infisical t
- 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
- ```
- Or set it as an environment variable:
- ```bash
- export INFISICAL_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:
+
+
+
+ For production deployments on Linux, install the Gateway as a systemd service:
+ ```bash
+ sudo infisical gateway install --token --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`
+
+
+ The install command requires:
+ - Linux operating system
+ - Root/sudo privileges
+ - Systemd
+
+
+
+
+ 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
+ ```
+
+ Or set it as an environment variable:
+ ```bash
+ export INFISICAL_TOKEN=
+ infisical gateway
+ ```
+
+
+
+ For detailed information about the gateway command and its options, see the [gateway command documentation](/cli/commands/gateway).
+
Ensure the deployed Gateway has network access to the private resources you intend to connect with Infisical.
@@ -78,4 +112,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.
-
From 38cf43176e46a4ab98aad263c4c2115c05126a80 Mon Sep 17 00:00:00 2001
From: Maidul Islam
Date: Tue, 11 Mar 2025 20:13:39 -0400
Subject: [PATCH 56/71] add gateway diagram
---
.../images/gateway-highlevel-diagram.png | Bin 0 -> 332077 bytes
.../platform/gateways/overview.mdx | 2 ++
2 files changed, 2 insertions(+)
create mode 100644 docs/documentation/platform/gateways/images/gateway-highlevel-diagram.png
diff --git a/docs/documentation/platform/gateways/images/gateway-highlevel-diagram.png b/docs/documentation/platform/gateways/images/gateway-highlevel-diagram.png
new file mode 100644
index 0000000000000000000000000000000000000000..5f942bcf0c3049587e4dd117e5b337cecc1d803e
GIT binary patch
literal 332077
zcmeFabySsW*FK5}N~jp-C?OyXOB$qFyE{=@6y6QAz>n
zkZw5h(Y@dI8|RDj`P=K9G0qt5Kimt};(4C?p7XxuHFNnYDM+2fBgezQz&I%*Eq)6F
zgNOtJ}tj<>V;C&1XK@sPBcMU9z
z9BA~7Ow6o>=oU*W>1fOhh3M3|<=N%$i5Z!iNxRw^DZ47D7`R#(@EOvH2;&Jl^TP$K
zj2!OLI9pj-+w(gM(fztEKfFeM%|=J_>mv>pLUbDPN;G0Nc1ASUSlL4foU1nmrs
z`EQ9!{`JT3lMtP$gTp<3HZ~_GCsrp;RvSALHV!^MJ~sBNY*(*dflpkqcd>T3>wLx9
z{sMA|-`5Z~vNy0ZyXRnLV@-ox^RB*)qk|9~9lTHT*To&ojQ@RgYx}>J4;#UTyu-%9
z%Fgy5KW^k~_TPLQdFS6BNB)>!I-YKog#8Cl!^zAP^f@Bi&>jsNgA*R`wv;cZoWBRer0EBIr$stwQb{)jW_AmLt5C75tBWn=59mqGh$u9{51Fz6bOiW2eOpHd!&c@ix
z(g*`X+BY=pxRSCaS>2-BG;KV+I7)Wv{4`b&ktn_ne?N}Q0~8JiHg+EEbC#kr;^HWs
z4%H*r6cTgK^tDLb^nxVBHOcU=sZ<<4ao4GGtHP#&(|&SyqRjqaV^CnRf{y^>9f#EA
zGu0UwhQ5ZuX(&BcQGKHGJUBF{V_4rXPVj1{zd3jA93ElhvR5I2(@V?G)?fTB_I6ed
z%*_QWUg2Yq_};@)Y-|zTJd45p&5!;nh7!)v#G0#1ydzR?n6KlBJDWyzF`O035t9!$
zoDo-$Bf-CGkMZ;3DXHLN7!A0-UG_z0&rV%Bc30rEB8_Mf;oaPe$5NFPtEV9#y&ifKH0WpiYiM
zZ-!c^CU}mirX-!{jJ+RU@HF{&c%Gj#e{6Pp+l@fq*ip*s+C>Q;nx}%MDPPmEzLdJO
zUQLF*t&mF*$NTj1wB9=F9hQm4PJ@RelU3n4v26MZ7P7=71xuTr`(1o_e(rhA>Upm<
z&ZgH-;vXfoHB)%6pL4>6FLhFqxt0A9F(qFO4SD?o%AXG~43F~v6e}Zn=1q7HmE&J2
zeyxI5?E|kmo%D^cj_~^Iv92$4IoaGpA6q}3d$1%RxaC)tC_gCw?7k8IhmtcGZDR%o
z28<~NA9*l%qfTJtneiJ7Q)BcT!C;LZZ=~l7$@49Jazs21GYy45b>zaGQKJ;EV8O7s!43VWIcU%E;^h(OJoP6_kc!z*z_b6BMxJ?{}x9=Y=EBJXk5hne4w
z6&>ivAA{!_05xuEI_v%FQbvo%&Kg2TQ
ziPA_i9^b!S(0+G{*Wpyj2}d!4I9*dhS9}leo9wtL)g^B)3E~@9$?;;n@Sgl+_LGj6
zfc0_TC(?T--7&16w0%_nE^v=v7mw=tkyk{1Pp+cA#GPe2!;BgFSXM$tsp)1-nw`qY
zj3YfyX2gd?*W#Wi#dc_LVarPm#0j-upVHtWE5|9PEI*~=I~-^&=@e%Z@7D2YYVqt<
z$>Rn-wsmhgaNlq>>&u-^q0A@idR+XV_|bHodzF43SJt^+Qi{j&pN;Q?>G$0&G@!^c
z$s6hyp2Ioyw5wY4yP)l9-ct%sSgNf)ov(`iIP`gKO!#}~&u!=9?ij+jY}I7nnwA_l
zyEY>?kL)sC54|ILk5Qb^`LTBOv1%$&itqgKvFx%c7a~pz`EqNS
zK}M!QsX^ZCMKH=N`Ib*7ZSoD-a@km!nTt1wo^u6WWXxuqzYt9?L{oktq#?zS+Kxz5
zQdcpDK9*60-s@Hh(*SdRs7oWutISuCp*GBwYP4x@(%4ncq>ZP)%A`~+&bgW-tHiBp
zly0XImZ_FbrCy%OpCEZNU;VjK(=Dx3wbc8X^FI6vLn=dh(W$y=6VMEsT#3mMM!UOC%#KR*G)8Y-JHx(LrmwB%moDlU&+wgO952me#Wu}U60?Ne1GA6
z>3cR#2NQOapC(z+9qeNh{(%vk+#Xz85j7D35l&o{rX2;l1y>8MnH87{bx2xhnYVT5
zW|fQWJX~*=n~@6)9}KVI3~Y8!J36Z|BR+k(=S#M|yotEFB9D@KR;m0(*+LMNe47-T
zSgzdS`{6iYiIE$6ae66Uk_YrUk6M%Pr%6{&ZNK$?aXrX;@J`tEu#ai+;dis{df!d_
zPJAKZ^e3jCwC1!S(|(N+-e%sBqT$VW+>ej%23x=DN$MI;Ob<6JnKJ&mJu&?Bz9BEYaJnG*%agBX`akqn#25*(nDwS;M(0-y@D1S3ulEbRQ!ga=!1mTA
z-)r{Hbd_V8UD3Ss{S)`JzxU5<);2WyF24UXUBOwy*X$BtU%J9WrcUO2<55#wW}d}6
zi-K99W1*)TH0p}RpV+QAYJI=oIiFwpytH}m*-k9gHL7qbM=Cbs$d-w=Vb0RFuJ(|z
z;->t%=L?~8Yx(Mdh2A`MrL{ZZfiDT#Omf>Wx*mpq6s&Tex0qyEvRdA&w==n75WOb0
zg}qI>-GZOp@k!LOrqlTYI`ml@=E&
zBkEA8)~x@{{7r3u+52F$B5(JoOvnVavyF2`S%D)=P3pu~MLr+fZOrq$YZc75b3p
z-1Tz;oWjPEh6_!U#I^Vo`9C!qH0va0669`5eb8MGp0myiEs`G0ulMM9E%7OsutE3I
z#b`FG(3>xfry~Zn_aBw<)Ul*8Pj-zK)kuY;jMGs?Bh<)Qsw}jBfk2X^*zE(
zF^`vaFO#N*$pg6syu=TfHuEg9$G%!wRFbXu<-Y0WwfgK;O8*^^u=wxq)1
zQPY$26nPX96pPl&!|y9qvbl2K+t~L{Ok8!Kf^Q_)yaNWf5Z
z{?1lRkx+CcyJz}PW_h9EVxdQZhtN*qx>I*aZ*T8(w^iEH*&l0I|VXJdvu
zwo^7*>QJk}I8qZdl^oEAqSopL9I7x$kZuRd-=H6cjp@qU?O!gh(chw+D^
zV@&U^@sgH7;ElF)-O6E$wuD%rnX&cqZJq6%zWwUS@yQ=Yw~rREQR+rfo;X{!WVg_?
zuRf}tC??^Rws&*y>+V{fh{t}}wJ7$%1IcCMx%m>O+l%YvPOc-uM10GuV!cm00&8O5
z3(p@kZw_wJ?>ALVjpi2Rl8#)iDDWWI+IPNeVJX-A@*czOBP3V9b*o?uFC2Xkg@b`T
zP5aEA(QOX*5yJsK1I{jE%-ko&X6y&YJ_i&^W#wZ~iawc^#ZYm_IBOH2?~XAym3CHn
z@2B{pu(ifGTgu11I~B2G%ConBDn`!D9^Bdx5mHLqZ#lr9k|5Y5Z5b}cz(8Thh+kK6
zJ~B6$ccbUzS?q5-dxWA%H0&~jvd@lj#LIkcp&|eLnqjXAi!sd9?+o_&$5=1f<1I@b
z(@2lg1>ez4CpdpQ7=^nUvs1*RzPRQTv(h?Q+tJaHyDael;|L}e4j~N+Eic}`M@iif
zDSyg{p3D9HSglwm@c6LM_?(0^gfw@QnBv4wqiyNPvp%>^%Y)vPW5UX@Uol~#Q%HpD
zOykGUx^86U@ehb3(4<9wiT#+*AX=4^5C>LHqLhGxK7EHLt%?hxK<~PbU>@_qlqK;-
z>*CLn*d0adx{;MDp?vB7KBMSSs7IpKXaS;sJrx$tSrR;^o9G3PJ4*E@|8)m+yZ~80
z5#hcQjG0P>-GjviG1b)pa5b2Kup!J+c+&dD#_Z0j%EZLP(b2J|C6Xtzw6xUK)pe#fTdQw;TJRV=!anOArKv?>
z&l?Y_NO3J~Y}6Aw(%AGWx~JO&1Oy67OSSYWJ+sTp%lUbEixL=DY;gav-9*I=Ve#|x
z&$LDh^b8DWd3bpAjEv|VJ$lsfV1Li`=eO%Z?wdd7Hs&Yj*p6WKDSrDk;%0V3Bjy=~
z_kAMggMAMxOUv$w31g4l%{*bReeI`DpU#WZu%IE7|Gubdx*h&gI!Qp#S6?qJ%oxVD~~UZ2dQCyt5?RPu~dpVR)}#lLts%xqP|1v-8#!Ha7MA
zFM`cgnpnyw}gLGj$K|-BA42
ziP>VDZ){>>VQrlw?6K`I-L@q8KX3DH+i)~WMpf7MXl1|+%rqt(QLY3gIwD)&(?rKl
zNYd`WPAEMe^&fz!^_nqr}$BBD_$sA>#l8SB-u2^g?0y`q+VTh;MxCR(-`6@Yz8XtllQ
z+e7>Wn~@6coMgCj&6T)#CELZ^p40W=ReA^8eMQqLk@|D}d3kp~J&Ed3efD2N>K~f^
zdbvB5d+X0XJ~}Q{>A83H2BE|QR0FOllZu^|^+&8uT=(S$Mh?62`m@~TDwcyq96Ikz
z|7Y?3FA`KV=XhY8#lk%{K4jRPDVLi
zlsDp<6?y4mh1+}c9_9!h8>)se2Kk83rv&B*Pf}d-pD!E{Cg(C$!cpcvj)td8OU0b+
z%MC2B=&PNK^`gpD&wUXnvd=5L`R$QLv7OQGTCP`s)36I;V^b5pzT;DBPiJke}z{7g^7;MW2Rvg79+lpLnoX@hf~
zSq*#$xMuZLg^M5~1ozx4bcodbI*r-o)rm-(+S%Mns^?^EFR<~?d@gfY(Xa6%)q9pv
zE}|$f|GsFsk%eWUBe(o!fcOIxEgnexS*gAP){OMG0G{Er%?CjsitI$&vD%L08)3Yo
z&n_g`Pqr>aFS<*O#eJ>jIfVv;Hm>q5yKvYqOxN|2uKQAr^LHPz(Lk?V1BF?2^*KM5
zoM+>8*B_v!k!LG$v-yElagp71b?U6JyR+rj_tQ5fSC+oL{dDR%7=t>FI!a{;mOr(8xNM8%KdHL^?rjD;nzoS2$=+r
zobLJ)LoSM!W7vJArA)#0Tb1`xrrsX&hcCvFWiJwWk6`-H1BBe2fBJD;@r0GFyG$3K5Di9KKLy5{GxF~nmt#w3Rh!#opN<$sP-e|K|fX<%gkn()qCewkdQ
z$Idzj$ZZOEo$d^a>;u$y1Yexp^d+&DS>J+Io)vVyCkT-1a&9&&ztxX=Z%WFxnWJtz
zxgr5AyQP5zwB6D=T7s+XVtjgwOLf%Lg@blC+{Y+J
z>Rqg`aS53y5ydiLYkrIx#`fH5(yQ%J*Ng75yEJF*jVrlR^NB;-l5GxZ609WczxNI!n%ldp=35ED)=Jz@ss4Qx+45e
z(GfI5jC1KP&BF=%f9xXYcj
zdeGIzHyKp~_+Sw0vECpfB=Lu)`oCU*cQZDaLKWK)ug*J6WKzJ3um`I2s
z$HF0b@ZgCk_FLQ(6b&^dF@xMQU}@t?4s|cik8RK91`;a@axkuu(Ej6MX#{oOk)7qx
z7e{ven)TotkCz86g1bLIjTKhzS-G!7j%3+h!Nws>bMrptD1yyNgQDrfbQ*G0Z;im!
zv8^HHuo)I?d2ClMGlLFUd0)mxz)qvg*>-jA3uhDFfwbFna@gqRSYRO7!`-o@N_dn)
zOrsw^enig9&&fw&cwfP)eQ}-w%+d6j$*vgQxwoH2Ybk8}FTCKTv&wKr1F@h%#l6Pz
zgq`>-L8e18+nb%)@vLxJZR;0k!551UOFtGu#%vSqWO->R(>4P?5Td1`a?-qvVDO-cq1hS4z!~Fg@MG>mpzZ
zcz?W79<3qM%er~K0Sl))8l~ytvXxRoF)@If6Jf7V|J{Jb?8OC9UEZPpuN`t`k;Tm;vpioJOo6CE+SBbbf|5Dpbl
zgrZupf*DXpkDtoAS$^c`vCmfu2X7^k!~UV+NxZ8G5e44;`BcQe*nZMjD1OLkL@362
z)Yr5xN2izjB5>v%K){}g;?ua}a(Aj9<94Jf#*+6?@YxvxwN8@cM$?Y?(7*NE+vc5&
zvLB6qO4aD9kBBTa#+CX0fK_3Zh4e*K+)0eB?#fD=2z4E$yB{-7VbTv!^LSioL*M-+AD~#VUbE_u
z1NCSrQOP%M(FXxW;2K?FdWjZxNZ@@lQL<*OPcL@OFxG2-3xWB+pTN?-hE!c!r&Np_
z0um88mi=#O6@=EhHQpJ@s2SZZv_2ogsQ6;hbE8!A#|=8P!0t>KDvVVp0UV5_&%}y`
z+)FZ1#ZMx(PWIr;Ul3fA=7}sjf{1Z0p
zvsr5NYt?WdcXh8WDRU7w$|@55Az=KMO3lRT4Qzzdb2%i|upn)HexR;7oHG#==hs4M
zDltzkYw0=y%G3TL!MykGs3k{mTbvt^J%^$L$3I~`2+F*CpC%w*I02n6rrasuLTUu^nGf~xF9&cD0TJ+?})JDrwJV4pv
zIm~ol0yNOC^*@(Rw)xc}w+3*V{OS4o8Z+9sfa>aTS!HGJ%AQY^qLFiR@%$cQz_Voi
zsYQe#mKLg-a}hlzcoX~BiL-=-!>$%;_XbG>H%sWi+nuy
zeIpn=l{-`I@tUDXLNp7mWHBJn-TeSSxpzyQhar9G_2jrKTd!hYsj`l~WbPXOF}ZG>-tI;W;JvvntCD
zuqOzi8+NnllYdEI0h@fhBFPVtz_&)0T)81RrgJOn12%nfX(^k_=rHOiU%b`XKDOIi00oLea`j*aO2m<&>;lE{>yKH
zZ}CDv%V2+ZD^k&DwbZETb>6-2)uw|*wvtN^aR}=JQIB?CsfJ>BTVm%FPegR5!%=->
zESlZ}`hpH!&?9AHVR;Tx_FQu)2vpC~INhh~W>sK|3HQzgpJi)JH5$e2O<_}(moT84
zk#vAd1#Z??k|lJ&BE;)uu}kjHldJ2x7$x^*@Q1I8AXH10HV0JE@fJ8}=DBz%VtB0JN{zR>cR^i@8{N;OT!};F{w;5_;j6LerNd&;(RLq=Hgi$5A!nm0!Q5>!lfew=!`f_LD!D`d+q-#-6+0=7r_mCngYzYMr#MCZnBdrKmk#nI98-dMcncfFc!bD^ByRI%g7f8d#>h>gfK?V&K94
z;(!o7^R8h=Q?C1b92mB4o
znA`-yvEH3=KG@q(uKaTHEISSPHOn_^Z-AJi#iVeP@{aj)CV=*&Hv+FX`fJ5O<#3$8
z3^ZH}DyR}85PHR5=ykB4ZjUA_hGlmp=)>`9Wkn%UoANcaoYZ)#BbcFxr-SRKjJ15_
z=g&_BspJWO>Um(^lF_En^-ocduS)>(kS;5-Tz}#BQlpWj7B|qrQb%i^8?;98r7yRE
z6QZ&N)&wnEyhUURaFjIf!+y8=vAevK4klIwOt&DtGNNP6LCCX4Xa1>48l|ydCp5j4{r1s$~Gm
zo%p6quZHE0d1qm#$b|lk^dQP82-W
z{)p$%D6%ykqKv-#rl8tlgp^P6Sbcr}OIM^B!RGO^bs)gB{>8*#{%gUxNov=r_&iW2
zLrS(Fv~WBcr2$OFsC%Lrv?2TWr+{D~cip#CW%x<&4N2zx8-bZ_?QmpfM77cM#a7+v
zsGars3tJ7VNYc7Fl83G9o-Gzm5((fi{hNE>h$;Cqr>RLv0}Xt-kwtV
zlrsM@D5K~OCOyI^d_|62u3X15Zi|Utx`QO_t%pjC(6_Vdm}YJlA-0m5lVYxoDZvJJ
z?6AqhKJ&1OCAW+S0{rI`A&1R+t`6@anshh0>ZKG_xcMdoWmEjfxT(L^MIg0QlirzKCsu4F9bk
z_ZJ5Fet`x$*cwXbSJ0;Cr&k2ldDP%aKhp#~0
z1c~$j3#}E~lM?O7Jlt+ZBmvQ79d1!hs!??BZmqq|zz3_jcTFHkd~`{&po;b>Ww1Hk
z^D|duN!fr7HY4swvkVEK?Xb&av_`qBW9`;?f&0z8YBXpPCn44;{;4?dew&<`z4ljP
zIbV=M?D}6Sq=sozwj`NRcIrHT5~5sa!Jjk>Z>tYq7&=$wqz3}ybNWo&G1nx(d~Ux}
zp3t8|x03{AdD4Y+8g6K2K&oCFuRunSK3e36~(04#+agyf4-@0!t
zi@y25N65qsYx6S~xROGMJq&df{;54lG9mp&VLDe|kV&IQj!Y!pLK}meJu026hSI7U
z?A}CYpw{Ry&q$|*)Eyw)=Lp`{QMnmXvY^XsNn@JrFeFVkC`r$9kzx8EaRJ)J7saGL
zRZzbbyTpI{gJrmF777hv5X5Dn#YHN-dK~`K>Q>m0h!vtrwMV%jQ#_{OjShPSaUfA5
zL_+$Ihi!6}rS@38S(}Fmp%*zg9L-VUr|V_EjRlG*g!a8R>p~y5^tl+6;#AET>wZgG
znTHHhf)=**re&f8Nk-L7Po}bbwoC}aFG`OVgM}wUBft3gy~jB`v*-!YI3>u`F?UCE
zZ$
zn2_((sqK;(2%{aV#>9Q~76696Z0mK5mv;aKPYQX`^`i$DmA!G%!;2cDh^>d%iWblE5L^F0!PZFb
z$1Z=*0yxy%L(N4;Nb%4$IP|!Xgy5mab?C?*#s~i23A=b(-Ef|Nt?=K+o0Y`89cO!M
zg6VJ2Z4Kq3mA~nhdOAPW;t&I6)t|zX_VJ#
zp@4K_L!oPLmL{>fo`)k+f~rS8-mR<{pk-Z553Ox$$tzTUT&sZEM;@gz@ANMtg}R9k
z9io?z7HwODMTk>VTlD1wOteHEjI=?+f_nFCAul|#A#+ffS_PEEE$
zT-JlUT|}e5uOox+PbI_;c}t!2>zzdjXDRSJAVs{?Aw^vSZK1c9LIj7Qcsv2AorIon
ziBC^XvaH<;1|wHM`>S69dPEH-n!~kD@Ys%i_FKU}b2)9(MqCSv_xgV?o&Sbvxe!C%
z*5*CluL4-_QDW(c`PJ^~}{celpW5-1{uSeN6URo+&
z^kDli)GHN~(X4#|6}OL%GeVfo!3E$xC!{59iZ4%RdqS-fI>}f4j>z#W_
zhlcrTupo<=wheR17*MNE3@M#c?Ol1$OqttetpdZ
zBD?G+_5<2e&75tQ&+d%7utVcxi*jby2=r4cUoS4(TOV+oxqA;e3~4&vq%3Nm({3h?
z#yvXa2UVNwtQDK1MF;yknN|$1n(bC5pFy7HGL!*RU(x#DZxASSZ2?pf61xQEPPCFB
zrg1DPkJS$IKKBui4WtM%BY^n7QH}q6`2O{qVnl14UNisd?eI<@c7~d-M8#O)4#-?f
zlj~g8t0=3rkh69!U+dLM)GD;@$Tw;8w+_x;io=7J0Cr3`H&@;iUnJhzU1_tw8VhN3
zNu<>TDqda=vTKDHbRLY6)Ri_V)nZD4+$_jPZE7x9B!#38ZRzU-cKFA>8OTNjZ!<$eR^;#LmXcCdV
zX~hd>yx;ylBvLe6tVrtZv#PpIXrDyDSq{4O5w8PhIBF~WFGjI&UZtQy804?sc7{e|
zPCsJtKSqG!)BV?Yj8yeEUg!gJb6ljk>Ip0UR02#iM>KqfV7OI)W?BoX9R_i0WK`
z4gZJ(^_vVxU#v6aU|0B41r~}fq{v5}VJrJ7iX_`|4KDq%6$gaQs~u8f1%0|Zb-rx7
zJ&-=}+jzhhwan=4k8KSKa+duLs&RMvO2$b9_)++!Wn$mX>tA38sR``%={ICJIpXK4Dkt@_O3UmE(qeGy~;=>)qI
z8I-?_O3p1o$N;eVDR
z;a!u9NEnxGG`EFXa}*!v`k;MVS)d69(yaMSy>eSMOS|}8rwrx2sv}4dy=-nWp9}OS
z);`81&X6l!|7ww0wg63o|J$|xb=DAIMka5KzBP^urU@CBq0Vb&J#CFr}V?9JxYh0_-j@HB({+P?Ohnd@x{Fn*;2zR
z?NAF=1pOI>5pY^3k)a_UT!*m#bZO9DQ1xN!QYQ27O;H-bVufB8*
zq}-~(;s^AjbCO>x_P%C@>G1iql(?qmmot1&ZRH{emf5!MywhixAXHei-$M-Ge$O2s
z=S35A9c6|I{$;B0RPL%(QF#o+IQHYSLqtkdM~AcGnZn`{j^XK8W?8yEu60jCYAU1iqY
zVQ#Yhb9h^3fdsOrhw5(*w+a|da&3YDY7Ny*;glx;?kcTDs`%9+gutOgCPzX%w?lr9s6|18stj_=H7}G
zF@tsVF7=nq|%t7wdvR!gaTU4*y
zoo1i}Gnr+3bR#>d!y;DLV>G`_ICJw#6=$ZJOhleR^IV1*-RfULhR8VY)ND{&mnJ{S5P%zU>}|tjP-ad^##f@;
z%6-=wXF%wTYifUNJ%6*|z0;;jUMc1&zhMj|jCF=hmr~}De+Gg-7j$6;PF5tYN8UOV
zznzAjTR#|VJR{R{
z=Ll|N>^Tn|&*`k6;Y9x4ls_^i-mVvw@&$BeJ4EF}f6G2i`q9F{
z7}B-!2Q!E~RGc-|{%qww3v2mmr}=S3VG)0FZf5BC%m|RDXa_@#G~vEC?MP_s228rj
zpcte5uU+_4gcMm!2N0MnjMD4UlmIO+8v
zBI=_LQl|$c;G_n1FNVxlo7gHTVPI6dTJGdbHT1-c0WLfz5Z#>Y)3CqG3@ofV>7!N@XeP5(`V_#S-IG
zu6ghCo+%k#vxL;HGG5eUIZ9G`zCF+AEYONOS
z50+ola*I(k*;RGaUfN5Z@8D4TP1-HTEEnW@T(Jtj{hW=BE!WvY&XaM`qrq+C!@!w+
zuhF!YjiB2D{(>i8xz}z@B?dBk2Gdq-wb)1kDPG~ay?3P)=1(|7BM|eMAou%h4vFL3
z3|q~6(6Lho!@iW3Y9xCL3$>c6Uo=cXX-N-;&1YAP
zmlp-Ls6xv6ka0TC5cV*uYI>x>WNmya%AM>+Ok0DS2^bwZ>Jfaq^7ZZYirBM^BOinN
zijrh{Aw&>n5ggTbLw7Owpdv_6DtK}OY_-~wV8a#)&r{~xqSt|2jr|YA6tOD`Gxk39
z3emg$`9Ltu=0LKN-z)GBGo3IIrMhw;XCa{Zw@*NlUDcU
z@k(}M9yx*I$MSuN8Ec`5b`((GpZ><{&u|JWYS@u({yBIgQjutWN53vbk#dE|wTnE1
zNU?C9^U|pEY<3~tm&!(Rzd7KIq&!0~@naaEGEh1q_+pExo$W;)iD6iJ=xbT)&ROWU
zyErzvTm%5eLO!FSmJdxe9WW8Ck=4qUwfMWIMT=%I#Vsf>*D_o2|GYhNus1R$aKqz&-QDy|wslhKd38fF|W0f+)zqoi;IaMK>gw|yUX+&Tyf6euM
zYj4p@Zz&?bV590Hhl|w>U4uX{?Zho<_g(_b<
zvot$o{#ANv3^_2CHck7`ukHjg!z0M^X5V7WJqe8$qiVT*tD>rlr2zQZ&cc>f{+7|f
zt`yfSpCMftF)kC~&NJz$%zKuv=WGEv>9+h|b43ZlbPV&sW-G8mOCSifk|dbJpyuki
z(g2aIM
zgO$kTjnZ(Exui_l>E#bL3P%p6%ZFNZEn@B0D>eoPoP^pu7RueS%h$ig=GQfClkNx&
zZvX~F0v)N#lyweo-GX+b7B|lx7s5sDc9%@I;27dnmjbJSUI9j;VGU^(Xb7;`*vg!3
zd5Dcax}LS7+D9%_I>XsIZ3G9aRR@Vwl;0sS`4mJB{h96LOi3+rvBE>z_da6NS}j!$
zO_jjTVl58M!s+MQ?sN$&p4#oT)dDAEF%yiO@)S%g`}zoXNm8@Z9*GS
z;oKIl7u~#jySQcBrXhq@;&gC=h08*(c(KU|W)2K{=2*u$dkv)DO7`Ph
zC>krfGfPA-n^d;fe148)(sW{P!U>BIg>mE$|B+Vo7CZ=-E@BaSTAM~#zl9kgPQ*2i
zauYDDC$tAIVPf~Z2>~IBrVd9g@>)ye=Lw!W@}nx>P_HDu3umfjFAqIt81PxkZLsJ}
zV|cK>I3i+A=)Fj@Unxrb9t@auPT8{Tfy|W5I6)@?_5X5i7e}B#t
zkA#WPFto@D`T@)UEXfKL@Q6dw9~94bZceOEktpzHhEK8mbr!PS(
zSklXcMZfZSSA$|;0|-ULaJgILv6JNe5k9vHJ$CLRjrwl!k6d+SJ}}8816oKPd6NDr
ztL&-!){ih~wB)b&)FU(rRp*>|pYThk-gM-L>D_$rXukN$&D8$;4)c9NLu{4|y{y9<
z5Lj@YWuS=Jgs@j6i~_Ek<$qu
zVm%W+z_!}l8^6zL2k=_!F)Tct6w(ZQhsenh+n%Y2;k+y`CWfEM<+<})?t8IQJ>z`4NAvk8iDhrC9lb@+~yKnOy5guXRdq;
zQMeN4vDPB&p+E;#MthM??ou$$DG;w?=(!U==sBW3wfKoZ#B!oFI`XB+&o+L~T%a@c
z)fS^C4S^@pLh28e_!T`YezrE@JK;Z6}$I}d*?Z$;)W)LJFpd5bi?
zTkBlKhe=E33<^8wm^hCagShjbRa3AD(LTVy#G!eBdWIQMZ_eU*&{%F2Ej~+Ptqjc=BWzmrV(nH4
zCD0{QY6zxpu__#zpimf_Q6$D%ES{u-aiQ4uYdz|bFjBfyO(tEJO%_~uP5^Fw*}J0T
zh{qqmKxpwUYUJYFt7AI{EQ|el7yPZ?B@_72*Z4hrufY5LG3u^po+Hs-&dLp16#st6
zN#f3Hu=|EE62s1>vz+0Z@nE2Mow%EqHxMxABh(T7Ic(T`(>I78tf?kSo;(N*$@)^z
z6ticJUoUGHlJ&X?>#A-D8LRe`{4uYo_@}`!KXdK`K_u!+>BKH$6fe=0^BZ^_UDQWN
zPX)_4i`9Auoa3yGf=ok-YNnboJp8##(LKJg0-heXXA30ESoAf_sb=)JFIsq~}_Ny1*
zA=0^TF^6Xu3G_edQycGkl*33LpI-KD?nTca%Qr!Ho}Q#IhNdyo#myZ!z!wU7z0%5l
zR^yj{tA&my_T(|`?`klpa(NhE&vlx9%=P#)hdR0`!7gJ`x|+=`XaQ?6d)5
zb-Q$W}W
z8$RS`bO95J08&Ygo&8u7=)k$Ixa*JVt8t-^dQSecf|00E*|DfJ3p&=0x3Ru1%PZG#Hbs;Nn;%2PE@xi<+M@PWIgDwjNSF+$isqBA?X-K?pCCl>doV#lHEMLClno|E$1|msrT+Q!-
zK!VE1SczVL1XEUq8;ZB<`_8m;58B5}z)d3hVQi;8vNF-ai3~VN8l&Q|j^YCfD;kS)
zj$##B6!pO>zP7+5)!!*#^E`kN8`-4@W4=9`E{?+Ff6)qE&Fvf{{wY7XhYG8iU_Ny|
zkGJP|{mZ)!lLK`ygvRT40?&zfMMvUY
z##AfK9lXmU2W+k3$*?91c<_AZP(hW#vjX^TNXPq)_G7e4EJ3rdseEh#`L*C#eDMC`g9=Bx1Ajcwli2deUnHbc6BQy-xjBkIO9*a`
zH4IBOgGm#nR$;86+hlW!a0nhhQHIthd$7h4fREwe{52r+W8-BkAT4?It2IP-iN9Lc
z*20z=REIE=;yCu-eH#AGsEJ&@UGJTtujbo3*K4h_MHC~r8X_?S&AXW_NkSsofUPk~2j+u$LnO4B*S$g)ur4K1ZjEAabG^Ck&xeOHeUG#4f`?z!=nQJCg_2
zHOu~QQS~`8wTLoAphTP&v_}|9HdJ$UhbSR9Yzz=N?4kHw7;h2hHe%s@g1Z5{rnyZJMI(Mu?L>M+;e
zF(o{&UK>bl6TxLRsviB*PTjfIhfMd)z1VQ|_DLB1Wsw+%rPVR%2C|IABV~RAsgp1G
z$L)l2jpr2WLNEm&m#YjLy{D*GQb%Q1PkT;%WG{a=`)B^WC@o|jJ`||+;_xE1v
zSq@&anGbRAEXTqvSK_^8&HU&%!8IhGB3eq~wvhozH?qNa6K$Ud^mc5f;Hqyy&ch$G
zzxpy_Ec1pa^f%(b+Jr-jBLeca5#WzAI1qGts4aTJBwQT(so|}tg^fLM^1}hG7~`F+
zbP^r2ySUZw^Q$>z<8PNGSUi0=flO~qDtGi}BwtV2ScRl!=M?WqnM*#;gm_H8gK(5>
z?Kxw>K*_pjy$){fFFS8WT=T4^7b$?e1?)v6j~MlW+Em5nA7jK95}Ddse{l3v|K6$S
z`VnaorruOV>Xmje0-uUXzVBZISEEzJ{?OC><(t-DJF=6NVM%d@;h57_{)8VQ!9zS|HiIoN1X(c2ke6fFHTB;h0(A{Cj3>D+
z=^VktdyU8Wil+TVlI8`iH|+r9HV!$a)t=yW$b*;N43Yj8h-0>ZTU!41=)eLKB{Eg2
zko${HUxEa_{YnduTZq({M+M#s7&%+y0&$io?;}7&R$gR6%+}KBU^j>p3%owV^HTsg
zbMpFy*Anbvk;%iKb&o}udbDu&IeQ!6i9J(?(54A88@+0y3klmy@5IKiSkls+t4n{i&)i0t<4%C-na$DoT&9p}&`%KPKZ9I7IAfB?QJJ7k(0_8?FB>F}xFS64x=!V!F
z%LZg&^n007mb2BWyCJ@8$2F3fvywO210QNWB$*G1VcBiJg2`IL$={>qEqD4xX!kvoY>_?AFO$YIGZWSp*%wU5Kh{01A
z@NApGdohC$ewn(*1UAf2OAZ(D^1S8Mr1Um0N9Vd9rYn3cix~_WTr9mJWHP;f{^DCn
z+C04YVi;rP&T{~DM`t~1Sl+GRhj*<>ACa2Az1sYZXT8~=H!q>mVk=saUl-!J=esJs
zyf{r-qna}p*Gj+YKl$$B)_9!j7m(Oqn@Svb5{2N<%&BeB*s!+TW9)LV24Kk
zhB_qwI>xsVLR;F6)tXze-H5O9jEo`al;4>v=8#h#
zm48Pespbi4bD$Ei2&pDVq7lwx1q%KNU4a{F@cX7`yDG
zHRR?b;08yKD11>yh0MW$Q|E+8nmzF7NMZ%vkCe{eyd~@WMX9)GweM=>*fZ4|N_$km
z>Eyx-11Xe&qmu(PR9B-mLp9TX+mRIeRMPC$4EavQ?cBC?@D>Ue_QCWRXQvW*J7eP6
zDmh<=w5h+|o1k9hdl91KFt)}VB1I&*nL@9p_fy~Ii#I`dVpKlZDLeJ&F`rXr0-i^h
zXD&cKWdicJSHUo*ld;=^@zX3-JL_{ffjNCx%#Y;KC^pe%fZ|vzhrts)N71Gc=dqI(
zZa#cIH%GA_Bc@6h)efZCa#gbqpSbgbqh9w)8+@UHX}8*jyn2VnGK@UA&)u}X4K)OH
z_)ZQdlaQF9H#Y?C3S#iVxw-~6`n}VE&0tYprdDEH0FWa70VC_Ud!J}N(!dYv!cU>r
zrkO`h+Joio&FhqS14``13YT-nOA!6zlNEeAnV9=fI(C
zXR1nuDd~AN6(b|AvsYGN-sw*7)0^=op4czTAk%~R+#s~~=gX<529=i8BpACag
ztLIkbNEY`)i?cBw76iH$7W%Ny^4<9ET+(~#Tfge6ZXDr&NBUI6>5C+au>r$<)Ly%`
zBkZ)r)6RS_P`Cvu8;**I!Iw#VoZ6=}k}0(3YV=69`9HD`uHI#ul`l>7*dxxTy6=G@Cm^Gk-eRY
z{YRpBBx-RDkx)XW;A;Tmn2kR_JKZZx($4`T#~w;OGzW~bE-S@7xLMx^S&T^AC0!AV
zWtQ)pZh1-bnDVWQ-RU8Y?j=-ADi0r`=rBuS0?rHcx==Uur{?(JHR2PU<#1UZ_s`__
z`oCy<>wu`%Eo@v76b2hmLKGB{5NQ+?1QY}T>5>vj=^A=KP()A=MWtJ52I&q_QaXkP
z3F%Jh-`aZaz2|uDz2BAJ_x*Pc&dlEX-S4~NSAr
z_hB}vB;$)|^fWJCqqjcExOIAjf4EtKHOp5Ird8n9Bj3RJPBfS2-(ZMm^p0+fgF-@8
zsBPB+A~Z}mz;GJlE|So3F^SoW2vfa?oQEL#dD$fskth;`%%xZJI*q$K1`D)E(a~7E
zM6BOp(M<%_Kx~lv%ri{T_f&x^~
z>`r&B+@fVg-Q))vh)LZ_;?=JY_Z8lYg2_CyiUAI8eLJMJ$3>dBMYXvB*gDcvB;14r
z-ME-lX5};uCymMciI(Z&)J6`El3Z_hXQ$-}J=BSdV@CM}nnHIR0}|D8W@hx}U1VgM
z0FR(ft+DHT##(F$lLLm;Qc=AZ(4mvzDHfk1-wS)7^nsr;0u)y?HrjZwSk49ZydbR;
zBqOOOKLm_zgBlTGCkBw=&D!DwsjW^;;3!(55xMQd$^pIN$Y
z!bAFOqe0E+Z&2V`q9-^FT1*It(f5v3id^%q){1sZg}kpmJa%lKkC%!}O+g1e-gEGPu_S>5|FY
zi>j{_c2tX+5jb0XAej90w|2mz(;eIKOr`z3(VLyOA=>hxPr4-=&DGFh7yZ##cFMR>
zKsi;DdOR(#NyJS~mQ>231k8=pSs}+X%P|$W&c^+cb(i+fQ#ZZKB-HmZp-#(0Rsnq_
zK&-u>+bFuY{(`b!HP9VwQ~f87X!qUVdJE&KswutSrGYFC%X{rqm9#QqsinzbuxQc@
z9UoI8YQK>A@>q{Z&Qfq#YxwmuD<(o!K(Iko+f$T)LaUMIO91O-$0u229u5=Requ5r
z$=3;v3P1!B3uZV?#InSg%F&GI?rE3r+6MB|0cEdV<*me9`#1o>sebyM{39e8;0wKp
z7nMw{XZ;OAL>qF
zxRLzXNe%v{ul>)%nC^9;&opj0=NFgg_5`rG;QG%mZJIKp%5hg^(?wj>n&t;lQ4`%c
zscVp7`1d}absV?%T|fL5wVQ2Pm4??;D_oa$>fos#S*9CCITH8p|@l+@}>Z;FmlUtwMzpsFcw
zSYJ!Du1r9_ADezeJ2j~>B$=&=2O%7+%oVYPA(8TX7xe5DE+r)ENbe*GBZ@DBi~#Q-
zC0Et(mw*Obt!mul8U*}irg0BHf4CjEdeleU?M{Bh1{nck{SQ>S(W-qb3eo|$b@9xX
zWyfj)SR=-aoHBtAm%O@}S6OzYBzJ|qo=G*kYWZUP*Y|ALLnyj*{sR*fG7T(3-tL}D
zYn|T0lx5_rNbUeN7ya(o=EVAClb33E9zg%QF}l(F^w>EF+!N3ExnEYNSYcX6-udFxqe5F-G;ieIi5)X&OvFoE%6UX1
zvZ}0hFKr#E=N6W@;*U4rL5_uLD!+Yq+w)ml0obu?B%0_qH5mCv$~Y$z^2fgdN`
zZf)~IXn`0hQ~WMoF^mFxXH{NVb?4`IkNTEEDrFE3^=Avd4QWRuX3o=vwA|a
zJl6E-6e47&D7TJn_!=8xmc9}t=#R-WJZlVyzVJB<4;8~`E1<&4^=XD*b11a_f@?BP
zEz(cw8=c_dwTGk&HW84Ri)0PmQy2&lnxJTe-5pk7Lu=gW_YkW%Q@)i3^jcApSa}wx
zs}*Ds6A+tqrk8ioE9KoUM=e5v_SwZ}LxW^V{*c-;bn>JlAZI!yGs_qV4fMa&ym4ZY
zwh`)B_3Kz6%hYGa3Q~6GCuT_Xv@hNP$X{K9PQ4$3K^Z*q%mJuuI=Q{g7Gh9f*LVg@
zA7J2mSY2YM(y#KPReFLTUw6+WZG!C!qkWCylBH217m13?W_!n$eN5>LRxnE8hVUm!
zgT$@e`UK#mk$bp2Rs_dFYpjg=jN%Nu<#q|n1ozcFJbTTTGPu|cpM%ko1|
zp$8{1Vo}d6)2HeBJzX}NLs*At`zJT+RHg4BlhZwxWpzia@lS~R#J%CT+
z$R0_qwCI{h1uilNt0V|Uv3&udW7BCfts|szr9vtqZnBV@rkmTXJ0qfG{p&ZG4+B%S
zCF;h3{x%y;&triD1$&uZooJ*pCcrWdS1E};C)Y-xNn>GZJSU<|lTNEtz3PjN+>^up
z(%D!_u6JL^?pi@vC^(~@?@NBlO=k0gX`V?YNb@pr2NDi0o_=NN)Rv@t8B_vma}8fa
z-14E+15iD?GCi_ntoRBmc04}^VgrQ)W36K6H4YZ3nRW=9RFpSi>R|G+
zms0EyrXO7g@}#MZve7rA3nsez$OeZ|7{yAUP^3~ohB(WkFb|LabJF*-3wy2qF7rCe#nq-8=5B_F7^+zt-43kfyGS?W$34a=<
z@pAwiUkHSOM}d$zt6org&vAb_buJpg@&K6&f>oyL4Wn=B-fsI2u}m=1H8Z|Flro0m
zvmiK?AgxU0!P-fmT}~BX9ONDi4O9jQcYbvew5iePm&TKTM;aq~kBuwRZnD|p-#|d_
zGF1JKm^w{Var4dtNQ^=|*}Tm!1%0`~hPo4%Vp9z7I{_Rb*H6PgC6AX+|N5dTM@hi3hEWG(DZ2aq7L_k!YMOm9QT<5PFuDvVu{O|?jzfRZ&yj%jDp3k@>k5&d5_k%ln)1xeJ=A;r`L=CW&
z)>}92(q>ptO9x|s1quq5g#t{+Q3spBRwuFyXaT&E