From 104edca6f11d56c51f9ee2a30f048266053c1217 Mon Sep 17 00:00:00 2001
From: Daniel Hougaard
Date: Fri, 7 Mar 2025 04:34:34 +0400
Subject: [PATCH 01/34] 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 02/34] 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 03/34] 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 04/34] 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 05/34] 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 06/34] 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 699e03c1a9169055fcd3c54cdb3e6de1e4e83473 Mon Sep 17 00:00:00 2001
From: carlosmonastyrski
Date: Fri, 7 Mar 2025 17:49:30 -0300
Subject: [PATCH 07/34] Allow project slug edition and refactor frontend
components to reduce duplicated code
---
backend/src/lib/api-docs/constants.ts | 3 +-
.../src/server/routes/v1/project-router.ts | 11 +-
.../src/services/project/project-service.ts | 15 +-
backend/src/services/project/project-types.ts | 1 +
.../project}/ProjectOverviewChangeSection.tsx | 114 +++++++++---
frontend/src/hooks/api/workspace/queries.tsx | 5 +-
frontend/src/hooks/api/workspace/types.ts | 1 +
.../ProjectGeneralTab/ProjectGeneralTab.tsx | 5 +-
.../CopyButton.tsx | 51 ------
.../ProjectOverviewChangeSection.tsx | 168 ------------------
.../ProjectOverviewChangeSection/index.tsx | 1 -
.../SettingsPage/components/index.tsx | 1 -
.../ProjectGeneralTab/ProjectGeneralTab.tsx | 5 +-
.../CopyButton.tsx | 51 ------
.../ProjectOverviewChangeSection/index.tsx | 1 -
.../kms/SettingsPage/components/index.tsx | 1 -
.../ProjectGeneralTab/ProjectGeneralTab.tsx | 4 +-
.../CopyButton.tsx | 51 ------
.../ProjectOverviewChangeSection.tsx | 168 ------------------
.../ProjectOverviewChangeSection/index.tsx | 1 -
.../SettingsPage/components/index.tsx | 1 -
.../ProjectGeneralTab/ProjectGeneralTab.tsx | 5 +-
.../CopyButton.tsx | 51 ------
.../ProjectOverviewChangeSection.tsx | 168 ------------------
.../ProjectOverviewChangeSection/index.tsx | 1 -
.../ssh/SettingsPage/components/index.tsx | 1 -
26 files changed, 133 insertions(+), 752 deletions(-)
rename frontend/src/{pages/kms/SettingsPage/components/ProjectOverviewChangeSection => components/project}/ProjectOverviewChangeSection.tsx (58%)
delete mode 100644 frontend/src/pages/cert-manager/SettingsPage/components/ProjectOverviewChangeSection/CopyButton.tsx
delete mode 100644 frontend/src/pages/cert-manager/SettingsPage/components/ProjectOverviewChangeSection/ProjectOverviewChangeSection.tsx
delete mode 100644 frontend/src/pages/cert-manager/SettingsPage/components/ProjectOverviewChangeSection/index.tsx
delete mode 100644 frontend/src/pages/kms/SettingsPage/components/ProjectOverviewChangeSection/CopyButton.tsx
delete mode 100644 frontend/src/pages/kms/SettingsPage/components/ProjectOverviewChangeSection/index.tsx
delete mode 100644 frontend/src/pages/secret-manager/SettingsPage/components/ProjectOverviewChangeSection/CopyButton.tsx
delete mode 100644 frontend/src/pages/secret-manager/SettingsPage/components/ProjectOverviewChangeSection/ProjectOverviewChangeSection.tsx
delete mode 100644 frontend/src/pages/secret-manager/SettingsPage/components/ProjectOverviewChangeSection/index.tsx
delete mode 100644 frontend/src/pages/ssh/SettingsPage/components/ProjectOverviewChangeSection/CopyButton.tsx
delete mode 100644 frontend/src/pages/ssh/SettingsPage/components/ProjectOverviewChangeSection/ProjectOverviewChangeSection.tsx
delete mode 100644 frontend/src/pages/ssh/SettingsPage/components/ProjectOverviewChangeSection/index.tsx
diff --git a/backend/src/lib/api-docs/constants.ts b/backend/src/lib/api-docs/constants.ts
index 1b458175d..821f91ddc 100644
--- a/backend/src/lib/api-docs/constants.ts
+++ b/backend/src/lib/api-docs/constants.ts
@@ -459,7 +459,8 @@ export const PROJECTS = {
workspaceId: "The ID of the project to update.",
name: "The new name of the project.",
projectDescription: "An optional description label for the project.",
- autoCapitalization: "Disable or enable auto-capitalization for the project."
+ autoCapitalization: "Disable or enable auto-capitalization for the project.",
+ slug: "An optional slug for the project. (must be unique within the server)"
},
GET_KEY: {
workspaceId: "The ID of the project to get the key from."
diff --git a/backend/src/server/routes/v1/project-router.ts b/backend/src/server/routes/v1/project-router.ts
index 68d13842c..5209dadcf 100644
--- a/backend/src/server/routes/v1/project-router.ts
+++ b/backend/src/server/routes/v1/project-router.ts
@@ -307,7 +307,13 @@ export const registerProjectRouter = async (server: FastifyZodProvider) => {
.max(256, { message: "Description must be 256 or fewer characters" })
.optional()
.describe(PROJECTS.UPDATE.projectDescription),
- autoCapitalization: z.boolean().optional().describe(PROJECTS.UPDATE.autoCapitalization)
+ autoCapitalization: z.boolean().optional().describe(PROJECTS.UPDATE.autoCapitalization),
+ slug: z
+ .string()
+ .trim()
+ .max(64, { message: "Slug must be 64 characters or fewer" })
+ .optional()
+ .describe(PROJECTS.UPDATE.slug)
}),
response: {
200: z.object({
@@ -325,7 +331,8 @@ export const registerProjectRouter = async (server: FastifyZodProvider) => {
update: {
name: req.body.name,
description: req.body.description,
- autoCapitalization: req.body.autoCapitalization
+ autoCapitalization: req.body.autoCapitalization,
+ slug: req.body.slug
},
actorAuthMethod: req.permission.authMethod,
actorId: req.permission.id,
diff --git a/backend/src/services/project/project-service.ts b/backend/src/services/project/project-service.ts
index e1653d371..4b110ebd2 100644
--- a/backend/src/services/project/project-service.ts
+++ b/backend/src/services/project/project-service.ts
@@ -563,11 +563,24 @@ export const projectServiceFactory = ({
});
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Edit, ProjectPermissionSub.Settings);
+ if (update.slug) {
+ const existingProject = await projectDAL.findOne({
+ slug: update.slug,
+ orgId: actorOrgId
+ });
+ if (existingProject && existingProject.id !== project.id) {
+ throw new BadRequestError({
+ message: `Failed to update project slug. The project "${existingProject.name}" with the slug "${existingProject.slug}" already exists in your organization. Please choose a unique slug for your project.`
+ });
+ }
+ }
+
const updatedProject = await projectDAL.updateById(project.id, {
name: update.name,
description: update.description,
autoCapitalization: update.autoCapitalization,
- enforceCapitalization: update.autoCapitalization
+ enforceCapitalization: update.autoCapitalization,
+ slug: update.slug
});
return updatedProject;
diff --git a/backend/src/services/project/project-types.ts b/backend/src/services/project/project-types.ts
index 83a59b6af..5ccf33d23 100644
--- a/backend/src/services/project/project-types.ts
+++ b/backend/src/services/project/project-types.ts
@@ -82,6 +82,7 @@ export type TUpdateProjectDTO = {
name?: string;
description?: string;
autoCapitalization?: boolean;
+ slug?: string;
};
} & Omit;
diff --git a/frontend/src/pages/kms/SettingsPage/components/ProjectOverviewChangeSection/ProjectOverviewChangeSection.tsx b/frontend/src/components/project/ProjectOverviewChangeSection.tsx
similarity index 58%
rename from frontend/src/pages/kms/SettingsPage/components/ProjectOverviewChangeSection/ProjectOverviewChangeSection.tsx
rename to frontend/src/components/project/ProjectOverviewChangeSection.tsx
index d82415c0d..767f17334 100644
--- a/frontend/src/pages/kms/SettingsPage/components/ProjectOverviewChangeSection/ProjectOverviewChangeSection.tsx
+++ b/frontend/src/components/project/ProjectOverviewChangeSection.tsx
@@ -1,4 +1,4 @@
-import { useEffect } from "react";
+import { useEffect, useState } from "react";
import { Controller, useForm } from "react-hook-form";
import { zodResolver } from "@hookform/resolvers/zod";
import { z } from "zod";
@@ -9,9 +9,7 @@ import { Button, FormControl, Input, TextArea } from "@app/components/v2";
import { ProjectPermissionActions, ProjectPermissionSub, useWorkspace } from "@app/context";
import { useUpdateProject } from "@app/hooks/api";
-import { CopyButton } from "./CopyButton";
-
-const formSchema = z.object({
+const baseFormSchema = z.object({
name: z.string().min(1, "Required").max(64, "Too long, maximum length is 64 characters"),
description: z
.string()
@@ -20,33 +18,59 @@ const formSchema = z.object({
.optional()
});
-type FormData = z.infer;
+const formSchemaWithSlug = baseFormSchema.extend({
+ slug: z
+ .string()
+ .min(1, "Required")
+ .max(64, "Too long, maximum length is 64 characters")
+ .regex(/^[a-zA-Z0-9-]+$/, "Only letters, numbers and hyphens are allowed")
+});
-export const ProjectOverviewChangeSection = () => {
+type BaseFormData = z.infer;
+type FormDataWithSlug = z.infer;
+
+type Props = {
+ showSlugField?: boolean;
+};
+
+export const ProjectOverviewChangeSection = ({ showSlugField = false }: Props) => {
const { currentWorkspace } = useWorkspace();
+ const [currentSlug, setCurrentSlug] = useState(currentWorkspace?.slug);
const { mutateAsync, isPending } = useUpdateProject();
- const { handleSubmit, control, reset } = useForm({ resolver: zodResolver(formSchema) });
+ const { handleSubmit, control, reset } = useForm({
+ resolver: zodResolver(showSlugField ? formSchemaWithSlug : baseFormSchema)
+ });
useEffect(() => {
if (currentWorkspace) {
reset({
name: currentWorkspace.name,
- description: currentWorkspace.description ?? ""
+ description: currentWorkspace.description ?? "",
+ ...(showSlugField && { slug: currentWorkspace.slug })
});
+ setCurrentSlug(currentWorkspace.slug);
}
- }, [currentWorkspace]);
+ }, [currentWorkspace, showSlugField]);
- const onFormSubmit = async ({ name, description }: FormData) => {
+ const onFormSubmit = async (data: BaseFormData | FormDataWithSlug) => {
try {
if (!currentWorkspace?.id) return;
await mutateAsync({
projectID: currentWorkspace.id,
- newProjectName: name,
- newProjectDescription: description
+ newProjectName: data.name,
+ newProjectDescription: data.description,
+ ...(showSlugField &&
+ "slug" in data && {
+ newSlug: data.slug !== currentWorkspace.slug ? data.slug : undefined
+ })
});
+ if (showSlugField && "slug" in data) {
+ setCurrentSlug(data.slug);
+ }
+
createNotification({
text: "Successfully updated project overview",
type: "success"
@@ -65,20 +89,34 @@ export const ProjectOverviewChangeSection = () => {
Project Overview
- {
+ navigator.clipboard.writeText(currentSlug || "");
+ createNotification({
+ text: "Copied project slug to clipboard",
+ type: "success"
+ });
+ }}
+ title="Click to copy project slug"
>
Copy Project Slug
-
-
+
+
@@ -113,6 +151,38 @@ export const ProjectOverviewChangeSection = () => {
+ {showSlugField && (
+
+ )}
{
const queryClient = useQueryClient();
return useMutation({
- mutationFn: async ({ projectID, newProjectName, newProjectDescription }) => {
+ mutationFn: async ({ projectID, newProjectName, newProjectDescription, newSlug }) => {
const { data } = await apiRequest.patch<{ workspace: Workspace }>(
`/api/v1/workspace/${projectID}`,
{
name: newProjectName,
- description: newProjectDescription
+ description: newProjectDescription,
+ slug: newSlug
}
);
return data.workspace;
diff --git a/frontend/src/hooks/api/workspace/types.ts b/frontend/src/hooks/api/workspace/types.ts
index 0510bdbe7..0980bc715 100644
--- a/frontend/src/hooks/api/workspace/types.ts
+++ b/frontend/src/hooks/api/workspace/types.ts
@@ -74,6 +74,7 @@ export type UpdateProjectDTO = {
projectID: string;
newProjectName: string;
newProjectDescription?: string;
+ newSlug?: string;
};
export type UpdatePitVersionLimitDTO = { projectSlug: string; pitVersionLimit: number };
diff --git a/frontend/src/pages/cert-manager/SettingsPage/components/ProjectGeneralTab/ProjectGeneralTab.tsx b/frontend/src/pages/cert-manager/SettingsPage/components/ProjectGeneralTab/ProjectGeneralTab.tsx
index ef684a21b..41e8107e0 100644
--- a/frontend/src/pages/cert-manager/SettingsPage/components/ProjectGeneralTab/ProjectGeneralTab.tsx
+++ b/frontend/src/pages/cert-manager/SettingsPage/components/ProjectGeneralTab/ProjectGeneralTab.tsx
@@ -1,11 +1,12 @@
+import { ProjectOverviewChangeSection } from "@app/components/project/ProjectOverviewChangeSection";
+
import { AuditLogsRetentionSection } from "../AuditLogsRetentionSection";
import { DeleteProjectSection } from "../DeleteProjectSection";
-import { ProjectOverviewChangeSection } from "../ProjectOverviewChangeSection";
export const ProjectGeneralTab = () => {
return (
diff --git a/frontend/src/pages/cert-manager/SettingsPage/components/ProjectOverviewChangeSection/CopyButton.tsx b/frontend/src/pages/cert-manager/SettingsPage/components/ProjectOverviewChangeSection/CopyButton.tsx
deleted file mode 100644
index 34fe365ec..000000000
--- a/frontend/src/pages/cert-manager/SettingsPage/components/ProjectOverviewChangeSection/CopyButton.tsx
+++ /dev/null
@@ -1,51 +0,0 @@
-import { useCallback } from "react";
-import { faCheck, faCopy } from "@fortawesome/free-solid-svg-icons";
-import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
-
-import { createNotification } from "@app/components/notifications";
-import { Button } from "@app/components/v2";
-import { useToggle } from "@app/hooks";
-
-type Props = {
- value: string;
- hoverText: string;
- notificationText: string;
- children: React.ReactNode;
-};
-
-export const CopyButton = ({ value, children, hoverText, notificationText }: Props) => {
- const [isProjectIdCopied, setIsProjectIdCopied] = useToggle(false);
-
- const copyToClipboard = useCallback(() => {
- if (isProjectIdCopied) {
- return;
- }
-
- setIsProjectIdCopied.on();
- navigator.clipboard.writeText(value);
-
- createNotification({
- text: notificationText,
- type: "success"
- });
-
- const timer = setTimeout(() => setIsProjectIdCopied.off(), 2000);
-
- // eslint-disable-next-line consistent-return
- return () => clearTimeout(timer);
- }, [isProjectIdCopied]);
-
- return (
- }
- onClick={copyToClipboard}
- >
- {children}
-
- {hoverText}
-
-
- );
-};
diff --git a/frontend/src/pages/cert-manager/SettingsPage/components/ProjectOverviewChangeSection/ProjectOverviewChangeSection.tsx b/frontend/src/pages/cert-manager/SettingsPage/components/ProjectOverviewChangeSection/ProjectOverviewChangeSection.tsx
deleted file mode 100644
index d82415c0d..000000000
--- a/frontend/src/pages/cert-manager/SettingsPage/components/ProjectOverviewChangeSection/ProjectOverviewChangeSection.tsx
+++ /dev/null
@@ -1,168 +0,0 @@
-import { useEffect } from "react";
-import { Controller, useForm } from "react-hook-form";
-import { zodResolver } from "@hookform/resolvers/zod";
-import { z } from "zod";
-
-import { createNotification } from "@app/components/notifications";
-import { ProjectPermissionCan } from "@app/components/permissions";
-import { Button, FormControl, Input, TextArea } from "@app/components/v2";
-import { ProjectPermissionActions, ProjectPermissionSub, useWorkspace } from "@app/context";
-import { useUpdateProject } from "@app/hooks/api";
-
-import { CopyButton } from "./CopyButton";
-
-const formSchema = z.object({
- name: z.string().min(1, "Required").max(64, "Too long, maximum length is 64 characters"),
- description: z
- .string()
- .trim()
- .max(256, "Description too long, max length is 256 characters")
- .optional()
-});
-
-type FormData = z.infer;
-
-export const ProjectOverviewChangeSection = () => {
- const { currentWorkspace } = useWorkspace();
- const { mutateAsync, isPending } = useUpdateProject();
-
- const { handleSubmit, control, reset } = useForm({ resolver: zodResolver(formSchema) });
-
- useEffect(() => {
- if (currentWorkspace) {
- reset({
- name: currentWorkspace.name,
- description: currentWorkspace.description ?? ""
- });
- }
- }, [currentWorkspace]);
-
- const onFormSubmit = async ({ name, description }: FormData) => {
- try {
- if (!currentWorkspace?.id) return;
-
- await mutateAsync({
- projectID: currentWorkspace.id,
- newProjectName: name,
- newProjectDescription: description
- });
-
- createNotification({
- text: "Successfully updated project overview",
- type: "success"
- });
- } catch (err) {
- console.error(err);
- createNotification({
- text: "Failed to update project overview",
- type: "error"
- });
- }
- };
-
- return (
-
-
-
Project Overview
-
-
- Copy Project Slug
-
-
- Copy Project ID
-
-
-
-
-
-
-
- );
-};
diff --git a/frontend/src/pages/cert-manager/SettingsPage/components/ProjectOverviewChangeSection/index.tsx b/frontend/src/pages/cert-manager/SettingsPage/components/ProjectOverviewChangeSection/index.tsx
deleted file mode 100644
index 66120d7fa..000000000
--- a/frontend/src/pages/cert-manager/SettingsPage/components/ProjectOverviewChangeSection/index.tsx
+++ /dev/null
@@ -1 +0,0 @@
-export { ProjectOverviewChangeSection } from "./ProjectOverviewChangeSection";
diff --git a/frontend/src/pages/cert-manager/SettingsPage/components/index.tsx b/frontend/src/pages/cert-manager/SettingsPage/components/index.tsx
index 2f355dc94..04a624ec2 100644
--- a/frontend/src/pages/cert-manager/SettingsPage/components/index.tsx
+++ b/frontend/src/pages/cert-manager/SettingsPage/components/index.tsx
@@ -1,2 +1 @@
export { DeleteProjectSection } from "./DeleteProjectSection";
-export { ProjectOverviewChangeSection } from "./ProjectOverviewChangeSection";
diff --git a/frontend/src/pages/kms/SettingsPage/components/ProjectGeneralTab/ProjectGeneralTab.tsx b/frontend/src/pages/kms/SettingsPage/components/ProjectGeneralTab/ProjectGeneralTab.tsx
index ef684a21b..41e8107e0 100644
--- a/frontend/src/pages/kms/SettingsPage/components/ProjectGeneralTab/ProjectGeneralTab.tsx
+++ b/frontend/src/pages/kms/SettingsPage/components/ProjectGeneralTab/ProjectGeneralTab.tsx
@@ -1,11 +1,12 @@
+import { ProjectOverviewChangeSection } from "@app/components/project/ProjectOverviewChangeSection";
+
import { AuditLogsRetentionSection } from "../AuditLogsRetentionSection";
import { DeleteProjectSection } from "../DeleteProjectSection";
-import { ProjectOverviewChangeSection } from "../ProjectOverviewChangeSection";
export const ProjectGeneralTab = () => {
return (
diff --git a/frontend/src/pages/kms/SettingsPage/components/ProjectOverviewChangeSection/CopyButton.tsx b/frontend/src/pages/kms/SettingsPage/components/ProjectOverviewChangeSection/CopyButton.tsx
deleted file mode 100644
index 34fe365ec..000000000
--- a/frontend/src/pages/kms/SettingsPage/components/ProjectOverviewChangeSection/CopyButton.tsx
+++ /dev/null
@@ -1,51 +0,0 @@
-import { useCallback } from "react";
-import { faCheck, faCopy } from "@fortawesome/free-solid-svg-icons";
-import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
-
-import { createNotification } from "@app/components/notifications";
-import { Button } from "@app/components/v2";
-import { useToggle } from "@app/hooks";
-
-type Props = {
- value: string;
- hoverText: string;
- notificationText: string;
- children: React.ReactNode;
-};
-
-export const CopyButton = ({ value, children, hoverText, notificationText }: Props) => {
- const [isProjectIdCopied, setIsProjectIdCopied] = useToggle(false);
-
- const copyToClipboard = useCallback(() => {
- if (isProjectIdCopied) {
- return;
- }
-
- setIsProjectIdCopied.on();
- navigator.clipboard.writeText(value);
-
- createNotification({
- text: notificationText,
- type: "success"
- });
-
- const timer = setTimeout(() => setIsProjectIdCopied.off(), 2000);
-
- // eslint-disable-next-line consistent-return
- return () => clearTimeout(timer);
- }, [isProjectIdCopied]);
-
- return (
- }
- onClick={copyToClipboard}
- >
- {children}
-
- {hoverText}
-
-
- );
-};
diff --git a/frontend/src/pages/kms/SettingsPage/components/ProjectOverviewChangeSection/index.tsx b/frontend/src/pages/kms/SettingsPage/components/ProjectOverviewChangeSection/index.tsx
deleted file mode 100644
index 66120d7fa..000000000
--- a/frontend/src/pages/kms/SettingsPage/components/ProjectOverviewChangeSection/index.tsx
+++ /dev/null
@@ -1 +0,0 @@
-export { ProjectOverviewChangeSection } from "./ProjectOverviewChangeSection";
diff --git a/frontend/src/pages/kms/SettingsPage/components/index.tsx b/frontend/src/pages/kms/SettingsPage/components/index.tsx
index 2f355dc94..04a624ec2 100644
--- a/frontend/src/pages/kms/SettingsPage/components/index.tsx
+++ b/frontend/src/pages/kms/SettingsPage/components/index.tsx
@@ -1,2 +1 @@
export { DeleteProjectSection } from "./DeleteProjectSection";
-export { ProjectOverviewChangeSection } from "./ProjectOverviewChangeSection";
diff --git a/frontend/src/pages/secret-manager/SettingsPage/components/ProjectGeneralTab/ProjectGeneralTab.tsx b/frontend/src/pages/secret-manager/SettingsPage/components/ProjectGeneralTab/ProjectGeneralTab.tsx
index 698665e47..660eb1dbc 100644
--- a/frontend/src/pages/secret-manager/SettingsPage/components/ProjectGeneralTab/ProjectGeneralTab.tsx
+++ b/frontend/src/pages/secret-manager/SettingsPage/components/ProjectGeneralTab/ProjectGeneralTab.tsx
@@ -1,3 +1,4 @@
+import { ProjectOverviewChangeSection } from "@app/components/project/ProjectOverviewChangeSection";
import { useWorkspace } from "@app/context";
import { ProjectType, ProjectVersion } from "@app/hooks/api/workspace/types";
@@ -7,7 +8,6 @@ import { BackfillSecretReferenceSecretion } from "../BackfillSecretReferenceSect
import { DeleteProjectSection } from "../DeleteProjectSection";
import { EnvironmentSection } from "../EnvironmentSection";
import { PointInTimeVersionLimitSection } from "../PointInTimeVersionLimitSection";
-import { ProjectOverviewChangeSection } from "../ProjectOverviewChangeSection";
import { RebuildSecretIndicesSection } from "../RebuildSecretIndicesSection/RebuildSecretIndicesSection";
import { SecretTagsSection } from "../SecretTagsSection";
@@ -17,7 +17,7 @@ export const ProjectGeneralTab = () => {
return (
-
+
{isSecretManager &&
}
{isSecretManager &&
}
{isSecretManager &&
}
diff --git a/frontend/src/pages/secret-manager/SettingsPage/components/ProjectOverviewChangeSection/CopyButton.tsx b/frontend/src/pages/secret-manager/SettingsPage/components/ProjectOverviewChangeSection/CopyButton.tsx
deleted file mode 100644
index 34fe365ec..000000000
--- a/frontend/src/pages/secret-manager/SettingsPage/components/ProjectOverviewChangeSection/CopyButton.tsx
+++ /dev/null
@@ -1,51 +0,0 @@
-import { useCallback } from "react";
-import { faCheck, faCopy } from "@fortawesome/free-solid-svg-icons";
-import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
-
-import { createNotification } from "@app/components/notifications";
-import { Button } from "@app/components/v2";
-import { useToggle } from "@app/hooks";
-
-type Props = {
- value: string;
- hoverText: string;
- notificationText: string;
- children: React.ReactNode;
-};
-
-export const CopyButton = ({ value, children, hoverText, notificationText }: Props) => {
- const [isProjectIdCopied, setIsProjectIdCopied] = useToggle(false);
-
- const copyToClipboard = useCallback(() => {
- if (isProjectIdCopied) {
- return;
- }
-
- setIsProjectIdCopied.on();
- navigator.clipboard.writeText(value);
-
- createNotification({
- text: notificationText,
- type: "success"
- });
-
- const timer = setTimeout(() => setIsProjectIdCopied.off(), 2000);
-
- // eslint-disable-next-line consistent-return
- return () => clearTimeout(timer);
- }, [isProjectIdCopied]);
-
- return (
-
}
- onClick={copyToClipboard}
- >
- {children}
-
- {hoverText}
-
-
- );
-};
diff --git a/frontend/src/pages/secret-manager/SettingsPage/components/ProjectOverviewChangeSection/ProjectOverviewChangeSection.tsx b/frontend/src/pages/secret-manager/SettingsPage/components/ProjectOverviewChangeSection/ProjectOverviewChangeSection.tsx
deleted file mode 100644
index d82415c0d..000000000
--- a/frontend/src/pages/secret-manager/SettingsPage/components/ProjectOverviewChangeSection/ProjectOverviewChangeSection.tsx
+++ /dev/null
@@ -1,168 +0,0 @@
-import { useEffect } from "react";
-import { Controller, useForm } from "react-hook-form";
-import { zodResolver } from "@hookform/resolvers/zod";
-import { z } from "zod";
-
-import { createNotification } from "@app/components/notifications";
-import { ProjectPermissionCan } from "@app/components/permissions";
-import { Button, FormControl, Input, TextArea } from "@app/components/v2";
-import { ProjectPermissionActions, ProjectPermissionSub, useWorkspace } from "@app/context";
-import { useUpdateProject } from "@app/hooks/api";
-
-import { CopyButton } from "./CopyButton";
-
-const formSchema = z.object({
- name: z.string().min(1, "Required").max(64, "Too long, maximum length is 64 characters"),
- description: z
- .string()
- .trim()
- .max(256, "Description too long, max length is 256 characters")
- .optional()
-});
-
-type FormData = z.infer
;
-
-export const ProjectOverviewChangeSection = () => {
- const { currentWorkspace } = useWorkspace();
- const { mutateAsync, isPending } = useUpdateProject();
-
- const { handleSubmit, control, reset } = useForm({ resolver: zodResolver(formSchema) });
-
- useEffect(() => {
- if (currentWorkspace) {
- reset({
- name: currentWorkspace.name,
- description: currentWorkspace.description ?? ""
- });
- }
- }, [currentWorkspace]);
-
- const onFormSubmit = async ({ name, description }: FormData) => {
- try {
- if (!currentWorkspace?.id) return;
-
- await mutateAsync({
- projectID: currentWorkspace.id,
- newProjectName: name,
- newProjectDescription: description
- });
-
- createNotification({
- text: "Successfully updated project overview",
- type: "success"
- });
- } catch (err) {
- console.error(err);
- createNotification({
- text: "Failed to update project overview",
- type: "error"
- });
- }
- };
-
- return (
-
-
-
Project Overview
-
-
- Copy Project Slug
-
-
- Copy Project ID
-
-
-
-
-
-
-
- );
-};
diff --git a/frontend/src/pages/secret-manager/SettingsPage/components/ProjectOverviewChangeSection/index.tsx b/frontend/src/pages/secret-manager/SettingsPage/components/ProjectOverviewChangeSection/index.tsx
deleted file mode 100644
index 66120d7fa..000000000
--- a/frontend/src/pages/secret-manager/SettingsPage/components/ProjectOverviewChangeSection/index.tsx
+++ /dev/null
@@ -1 +0,0 @@
-export { ProjectOverviewChangeSection } from "./ProjectOverviewChangeSection";
diff --git a/frontend/src/pages/secret-manager/SettingsPage/components/index.tsx b/frontend/src/pages/secret-manager/SettingsPage/components/index.tsx
index 6fb72e4c4..92bda9d3b 100644
--- a/frontend/src/pages/secret-manager/SettingsPage/components/index.tsx
+++ b/frontend/src/pages/secret-manager/SettingsPage/components/index.tsx
@@ -2,5 +2,4 @@ export { AutoCapitalizationSection } from "./AutoCapitalizationSection";
export { BackfillSecretReferenceSecretion } from "./BackfillSecretReferenceSection";
export { DeleteProjectSection } from "./DeleteProjectSection";
export { EnvironmentSection } from "./EnvironmentSection";
-export { ProjectOverviewChangeSection } from "./ProjectOverviewChangeSection";
export { SecretTagsSection } from "./SecretTagsSection";
diff --git a/frontend/src/pages/ssh/SettingsPage/components/ProjectGeneralTab/ProjectGeneralTab.tsx b/frontend/src/pages/ssh/SettingsPage/components/ProjectGeneralTab/ProjectGeneralTab.tsx
index ef684a21b..41e8107e0 100644
--- a/frontend/src/pages/ssh/SettingsPage/components/ProjectGeneralTab/ProjectGeneralTab.tsx
+++ b/frontend/src/pages/ssh/SettingsPage/components/ProjectGeneralTab/ProjectGeneralTab.tsx
@@ -1,11 +1,12 @@
+import { ProjectOverviewChangeSection } from "@app/components/project/ProjectOverviewChangeSection";
+
import { AuditLogsRetentionSection } from "../AuditLogsRetentionSection";
import { DeleteProjectSection } from "../DeleteProjectSection";
-import { ProjectOverviewChangeSection } from "../ProjectOverviewChangeSection";
export const ProjectGeneralTab = () => {
return (
diff --git a/frontend/src/pages/ssh/SettingsPage/components/ProjectOverviewChangeSection/CopyButton.tsx b/frontend/src/pages/ssh/SettingsPage/components/ProjectOverviewChangeSection/CopyButton.tsx
deleted file mode 100644
index 34fe365ec..000000000
--- a/frontend/src/pages/ssh/SettingsPage/components/ProjectOverviewChangeSection/CopyButton.tsx
+++ /dev/null
@@ -1,51 +0,0 @@
-import { useCallback } from "react";
-import { faCheck, faCopy } from "@fortawesome/free-solid-svg-icons";
-import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
-
-import { createNotification } from "@app/components/notifications";
-import { Button } from "@app/components/v2";
-import { useToggle } from "@app/hooks";
-
-type Props = {
- value: string;
- hoverText: string;
- notificationText: string;
- children: React.ReactNode;
-};
-
-export const CopyButton = ({ value, children, hoverText, notificationText }: Props) => {
- const [isProjectIdCopied, setIsProjectIdCopied] = useToggle(false);
-
- const copyToClipboard = useCallback(() => {
- if (isProjectIdCopied) {
- return;
- }
-
- setIsProjectIdCopied.on();
- navigator.clipboard.writeText(value);
-
- createNotification({
- text: notificationText,
- type: "success"
- });
-
- const timer = setTimeout(() => setIsProjectIdCopied.off(), 2000);
-
- // eslint-disable-next-line consistent-return
- return () => clearTimeout(timer);
- }, [isProjectIdCopied]);
-
- return (
- }
- onClick={copyToClipboard}
- >
- {children}
-
- {hoverText}
-
-
- );
-};
diff --git a/frontend/src/pages/ssh/SettingsPage/components/ProjectOverviewChangeSection/ProjectOverviewChangeSection.tsx b/frontend/src/pages/ssh/SettingsPage/components/ProjectOverviewChangeSection/ProjectOverviewChangeSection.tsx
deleted file mode 100644
index d82415c0d..000000000
--- a/frontend/src/pages/ssh/SettingsPage/components/ProjectOverviewChangeSection/ProjectOverviewChangeSection.tsx
+++ /dev/null
@@ -1,168 +0,0 @@
-import { useEffect } from "react";
-import { Controller, useForm } from "react-hook-form";
-import { zodResolver } from "@hookform/resolvers/zod";
-import { z } from "zod";
-
-import { createNotification } from "@app/components/notifications";
-import { ProjectPermissionCan } from "@app/components/permissions";
-import { Button, FormControl, Input, TextArea } from "@app/components/v2";
-import { ProjectPermissionActions, ProjectPermissionSub, useWorkspace } from "@app/context";
-import { useUpdateProject } from "@app/hooks/api";
-
-import { CopyButton } from "./CopyButton";
-
-const formSchema = z.object({
- name: z.string().min(1, "Required").max(64, "Too long, maximum length is 64 characters"),
- description: z
- .string()
- .trim()
- .max(256, "Description too long, max length is 256 characters")
- .optional()
-});
-
-type FormData = z.infer;
-
-export const ProjectOverviewChangeSection = () => {
- const { currentWorkspace } = useWorkspace();
- const { mutateAsync, isPending } = useUpdateProject();
-
- const { handleSubmit, control, reset } = useForm({ resolver: zodResolver(formSchema) });
-
- useEffect(() => {
- if (currentWorkspace) {
- reset({
- name: currentWorkspace.name,
- description: currentWorkspace.description ?? ""
- });
- }
- }, [currentWorkspace]);
-
- const onFormSubmit = async ({ name, description }: FormData) => {
- try {
- if (!currentWorkspace?.id) return;
-
- await mutateAsync({
- projectID: currentWorkspace.id,
- newProjectName: name,
- newProjectDescription: description
- });
-
- createNotification({
- text: "Successfully updated project overview",
- type: "success"
- });
- } catch (err) {
- console.error(err);
- createNotification({
- text: "Failed to update project overview",
- type: "error"
- });
- }
- };
-
- return (
-
-
-
Project Overview
-
-
- Copy Project Slug
-
-
- Copy Project ID
-
-
-
-
-
-
-
- );
-};
diff --git a/frontend/src/pages/ssh/SettingsPage/components/ProjectOverviewChangeSection/index.tsx b/frontend/src/pages/ssh/SettingsPage/components/ProjectOverviewChangeSection/index.tsx
deleted file mode 100644
index 66120d7fa..000000000
--- a/frontend/src/pages/ssh/SettingsPage/components/ProjectOverviewChangeSection/index.tsx
+++ /dev/null
@@ -1 +0,0 @@
-export { ProjectOverviewChangeSection } from "./ProjectOverviewChangeSection";
diff --git a/frontend/src/pages/ssh/SettingsPage/components/index.tsx b/frontend/src/pages/ssh/SettingsPage/components/index.tsx
index 2f355dc94..04a624ec2 100644
--- a/frontend/src/pages/ssh/SettingsPage/components/index.tsx
+++ b/frontend/src/pages/ssh/SettingsPage/components/index.tsx
@@ -1,2 +1 @@
export { DeleteProjectSection } from "./DeleteProjectSection";
-export { ProjectOverviewChangeSection } from "./ProjectOverviewChangeSection";
From 1dea0248809e158e051c8cdb0a0c223bba0641ae Mon Sep 17 00:00:00 2001
From: carlosmonastyrski
Date: Fri, 7 Mar 2025 19:19:55 -0300
Subject: [PATCH 08/34] 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/34] 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/34] 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 20bd2ca71c09b9917b26985cb843102fedcba0f7 Mon Sep 17 00:00:00 2001
From: carlosmonastyrski
Date: Mon, 10 Mar 2025 08:18:43 -0300
Subject: [PATCH 11/34] Improve slug description, regex and replace useState
with watch
---
backend/src/lib/api-docs/constants.ts | 2 +-
backend/src/server/routes/v1/project-router.ts | 4 ++++
.../project/ProjectOverviewChangeSection.tsx | 18 ++++++++----------
3 files changed, 13 insertions(+), 11 deletions(-)
diff --git a/backend/src/lib/api-docs/constants.ts b/backend/src/lib/api-docs/constants.ts
index 821f91ddc..94076cf25 100644
--- a/backend/src/lib/api-docs/constants.ts
+++ b/backend/src/lib/api-docs/constants.ts
@@ -460,7 +460,7 @@ export const PROJECTS = {
name: "The new name of the project.",
projectDescription: "An optional description label for the project.",
autoCapitalization: "Disable or enable auto-capitalization for the project.",
- slug: "An optional slug for the project. (must be unique within the server)"
+ slug: "An optional slug for the project. (must be unique within the organization)"
},
GET_KEY: {
workspaceId: "The ID of the project to get the key from."
diff --git a/backend/src/server/routes/v1/project-router.ts b/backend/src/server/routes/v1/project-router.ts
index 5209dadcf..dcedf5fa5 100644
--- a/backend/src/server/routes/v1/project-router.ts
+++ b/backend/src/server/routes/v1/project-router.ts
@@ -311,6 +311,10 @@ export const registerProjectRouter = async (server: FastifyZodProvider) => {
slug: z
.string()
.trim()
+ .regex(
+ /^[a-z0-9]+(?:[_-][a-z0-9]+)*$/,
+ "Project slug can only contain lowercase letters and numbers, with optional single hyphens (-) or underscores (_) between words. Cannot start or end with a hyphen or underscore."
+ )
.max(64, { message: "Slug must be 64 characters or fewer" })
.optional()
.describe(PROJECTS.UPDATE.slug)
diff --git a/frontend/src/components/project/ProjectOverviewChangeSection.tsx b/frontend/src/components/project/ProjectOverviewChangeSection.tsx
index 767f17334..0f88ec2e9 100644
--- a/frontend/src/components/project/ProjectOverviewChangeSection.tsx
+++ b/frontend/src/components/project/ProjectOverviewChangeSection.tsx
@@ -1,4 +1,4 @@
-import { useEffect, useState } from "react";
+import { useEffect } from "react";
import { Controller, useForm } from "react-hook-form";
import { zodResolver } from "@hookform/resolvers/zod";
import { z } from "zod";
@@ -23,7 +23,10 @@ const formSchemaWithSlug = baseFormSchema.extend({
.string()
.min(1, "Required")
.max(64, "Too long, maximum length is 64 characters")
- .regex(/^[a-zA-Z0-9-]+$/, "Only letters, numbers and hyphens are allowed")
+ .regex(
+ /^[a-z0-9]+(?:[_-][a-z0-9]+)*$/,
+ "Project slug can only contain lowercase letters and numbers, with optional single hyphens (-) or underscores (_) between words. Cannot start or end with a hyphen or underscore."
+ )
});
type BaseFormData = z.infer;
@@ -35,13 +38,13 @@ type Props = {
export const ProjectOverviewChangeSection = ({ showSlugField = false }: Props) => {
const { currentWorkspace } = useWorkspace();
- const [currentSlug, setCurrentSlug] = useState(currentWorkspace?.slug);
const { mutateAsync, isPending } = useUpdateProject();
-
- const { handleSubmit, control, reset } = useForm({
+ const { handleSubmit, control, reset, watch } = useForm({
resolver: zodResolver(showSlugField ? formSchemaWithSlug : baseFormSchema)
});
+ const currentSlug = showSlugField ? watch("slug") : currentWorkspace?.slug;
+
useEffect(() => {
if (currentWorkspace) {
reset({
@@ -49,7 +52,6 @@ export const ProjectOverviewChangeSection = ({ showSlugField = false }: Props) =
description: currentWorkspace.description ?? "",
...(showSlugField && { slug: currentWorkspace.slug })
});
- setCurrentSlug(currentWorkspace.slug);
}
}, [currentWorkspace, showSlugField]);
@@ -67,10 +69,6 @@ export const ProjectOverviewChangeSection = ({ showSlugField = false }: Props) =
})
});
- if (showSlugField && "slug" in data) {
- setCurrentSlug(data.slug);
- }
-
createNotification({
text: "Successfully updated project overview",
type: "success"
From 66817a40db59f55a2c33e4782e984b380c712e69 Mon Sep 17 00:00:00 2001
From: carlosmonastyrski
Date: Mon, 10 Mar 2025 08:31:19 -0300
Subject: [PATCH 12/34] 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 13/34] 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 e870bb3ade709f742c146ed278bf76ae3bce1598 Mon Sep 17 00:00:00 2001
From: Daniel Hougaard
Date: Mon, 10 Mar 2025 18:53:01 +0400
Subject: [PATCH 14/34] Update check-api-for-breaking-changes.yml
---
.github/workflows/check-api-for-breaking-changes.yml | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/.github/workflows/check-api-for-breaking-changes.yml b/.github/workflows/check-api-for-breaking-changes.yml
index a4bdeb29e..3f85326f4 100644
--- a/.github/workflows/check-api-for-breaking-changes.yml
+++ b/.github/workflows/check-api-for-breaking-changes.yml
@@ -92,7 +92,7 @@ jobs:
exit 1
fi
- name: Install openapi-diff
- run: go install github.com/tufin/oasdiff@latest
+ run: go install github.com/oasdiff/oasdiff@latest
- name: Running OpenAPI Spec diff action
run: oasdiff breaking https://app.infisical.com/api/docs/json http://localhost:4000/api/docs/json --fail-on ERR
- name: cleanup
From 0057404562695da90e8e9d3eaa2cf96ce44e8b50 Mon Sep 17 00:00:00 2001
From: =
Date: Mon, 10 Mar 2025 22:22:58 +0530
Subject: [PATCH 15/34] 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 16/34] 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 17/34] 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 18/34] 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 19/34] 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 20/34] 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 21/34] 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 22/34] 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 23/34] 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 24/34] 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 25/34] 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 26/34] 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 27/34] 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 28/34] 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 29/34] 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 30/34] 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: Tue, 11 Mar 2025 04:52:12 +0400
Subject: [PATCH 31/34] 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/34] 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/34] 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/34] 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
- });
- }
}
);
});