From abc2ffca574f89761deb73a84b2bef53297f7ee8 Mon Sep 17 00:00:00 2001 From: Scott Wilson Date: Thu, 6 Mar 2025 15:41:27 -0800 Subject: [PATCH 01/71] improvement: add organization name constraint --- backend/src/server/lib/schemas.ts | 7 + .../server/routes/v1/organization-router.ts | 4 +- .../server/routes/v2/organization-router.ts | 3 +- backend/src/server/routes/v3/signup-router.ts | 3 +- frontend/src/components/auth/UserInfoStep.tsx | 2 + .../CreateOrgModal/CreateOrgModal.tsx | 2 +- frontend/src/hooks/api/reactQuery.tsx | 320 +++++++++--------- 7 files changed, 174 insertions(+), 167 deletions(-) diff --git a/backend/src/server/lib/schemas.ts b/backend/src/server/lib/schemas.ts index ed97cb7d0..28e6b0697 100644 --- a/backend/src/server/lib/schemas.ts +++ b/backend/src/server/lib/schemas.ts @@ -21,3 +21,10 @@ export const slugSchema = ({ min = 1, max = 32, field = "Slug" }: SlugSchemaInpu message: `${field} field can only contain lowercase letters, numbers, and hyphens` }); }; + +export const OrganizationNameSchema = z + .string() + .trim() + .min(1, { message: "Name must be at least 1 character" }) + .max(64, { message: "Name must be 64 or fewer characters" }) + .regex(/^[a-zA-Z0-9\-_\s]+$/, "Name can only contain alphanumeric characters, dashes, underscores, and spaces"); diff --git a/backend/src/server/routes/v1/organization-router.ts b/backend/src/server/routes/v1/organization-router.ts index db0008ebe..4e8b120f8 100644 --- a/backend/src/server/routes/v1/organization-router.ts +++ b/backend/src/server/routes/v1/organization-router.ts @@ -13,7 +13,7 @@ import { EventType, UserAgentType } from "@app/ee/services/audit-log/audit-log-t import { AUDIT_LOGS, ORGANIZATIONS } from "@app/lib/api-docs"; import { getLastMidnightDateISO, removeTrailingSlash } from "@app/lib/fn"; import { readLimit, writeLimit } from "@app/server/config/rateLimiter"; -import { slugSchema } from "@app/server/lib/schemas"; +import { OrganizationNameSchema, slugSchema } from "@app/server/lib/schemas"; import { verifyAuth } from "@app/server/plugins/auth/verify-auth"; import { ActorType, AuthMode, MfaMethod } from "@app/services/auth/auth-type"; import { sanitizedOrganizationSchema } from "@app/services/org/org-schema"; @@ -251,7 +251,7 @@ export const registerOrgRouter = async (server: FastifyZodProvider) => { schema: { params: z.object({ organizationId: z.string().trim() }), body: z.object({ - name: z.string().trim().max(64, { message: "Name must be 64 or fewer characters" }).optional(), + name: OrganizationNameSchema.optional(), slug: slugSchema({ max: 64 }).optional(), authEnforced: z.boolean().optional(), scimEnabled: z.boolean().optional(), diff --git a/backend/src/server/routes/v2/organization-router.ts b/backend/src/server/routes/v2/organization-router.ts index 8ca105ad4..e4f04af52 100644 --- a/backend/src/server/routes/v2/organization-router.ts +++ b/backend/src/server/routes/v2/organization-router.ts @@ -12,6 +12,7 @@ import { import { ORGANIZATIONS } from "@app/lib/api-docs"; import { getConfig } from "@app/lib/config/env"; import { readLimit, writeLimit } from "@app/server/config/rateLimiter"; +import { OrganizationNameSchema } from "@app/server/lib/schemas"; import { verifyAuth } from "@app/server/plugins/auth/verify-auth"; import { ActorType, AuthMode } from "@app/services/auth/auth-type"; @@ -330,7 +331,7 @@ export const registerOrgRouter = async (server: FastifyZodProvider) => { }, schema: { body: z.object({ - name: z.string().trim() + name: OrganizationNameSchema }), response: { 200: z.object({ diff --git a/backend/src/server/routes/v3/signup-router.ts b/backend/src/server/routes/v3/signup-router.ts index e95254816..b1c7c7ddc 100644 --- a/backend/src/server/routes/v3/signup-router.ts +++ b/backend/src/server/routes/v3/signup-router.ts @@ -4,6 +4,7 @@ import { UsersSchema } from "@app/db/schemas"; import { getConfig } from "@app/lib/config/env"; import { ForbiddenRequestError } from "@app/lib/errors"; import { authRateLimit } from "@app/server/config/rateLimiter"; +import { OrganizationNameSchema } from "@app/server/lib/schemas"; import { getServerCfg } from "@app/services/super-admin/super-admin-service"; import { PostHogEventTypes } from "@app/services/telemetry/telemetry-types"; @@ -100,7 +101,7 @@ export const registerSignupRouter = async (server: FastifyZodProvider) => { encryptedPrivateKeyTag: z.string().trim(), salt: z.string().trim(), verifier: z.string().trim(), - organizationName: z.string().trim().min(1), + organizationName: OrganizationNameSchema, providerAuthToken: z.string().trim().optional().nullish(), attributionSource: z.string().trim().optional(), password: z.string() diff --git a/frontend/src/components/auth/UserInfoStep.tsx b/frontend/src/components/auth/UserInfoStep.tsx index 2b7761d1e..582c2a850 100644 --- a/frontend/src/components/auth/UserInfoStep.tsx +++ b/frontend/src/components/auth/UserInfoStep.tsx @@ -11,6 +11,7 @@ import { encodeBase64 } from "tweetnacl-util"; import { initProjectHelper } from "@app/helpers/project"; import { completeAccountSignup, useSelectOrganization } from "@app/hooks/api/auth/queries"; import { fetchOrganizations } from "@app/hooks/api/organization/queries"; +import { onRequestError } from "@app/hooks/api/reactQuery"; import InputField from "../basic/InputField"; import checkPassword from "../utilities/checks/password/checkPassword"; @@ -206,6 +207,7 @@ export default function UserInfoStep({ incrementStep(); } catch (error) { + onRequestError(error); setIsLoading(false); console.error(error); } diff --git a/frontend/src/components/organization/CreateOrgModal/CreateOrgModal.tsx b/frontend/src/components/organization/CreateOrgModal/CreateOrgModal.tsx index b5712e596..c91d8280c 100644 --- a/frontend/src/components/organization/CreateOrgModal/CreateOrgModal.tsx +++ b/frontend/src/components/organization/CreateOrgModal/CreateOrgModal.tsx @@ -78,7 +78,7 @@ export const CreateOrgModal: FC = ({ isOpen, onClose }) => }; return ( - + { return input.replace(/([a-z])([A-Z])/g, "$1 $2"); }; -export const queryClient = new QueryClient({ - mutationCache: new MutationCache({ - onError: (error) => { - if (axios.isAxiosError(error)) { - const serverResponse = error.response?.data as TApiErrors; - if (serverResponse?.error === ApiErrorTypes.ValidationError) { - createNotification( - { - title: "Validation Error", - type: "error", - text: "Please check the input and try again.", - callToAction: ( - - - - - - - - - - - - - - - {serverResponse.message?.map(({ message, path }) => ( - - - - - ))} - -
FieldIssue
{path.join(".")}{message.toLowerCase()}
-
-
-
- ), - copyActions: [ - { - value: serverResponse.reqId, - name: "Request ID", - label: `Request ID: ${serverResponse.reqId}` - } - ] - }, - { closeOnClick: false } - ); - return; - } - if (serverResponse?.error === ApiErrorTypes.ForbiddenError) { - createNotification( - { - title: "Forbidden Access", - type: "error", - text: `${serverResponse.message}.`, - callToAction: serverResponse?.details?.length ? ( - - - - - -
- {serverResponse.details?.map((el, index) => { - const hasConditions = Boolean(Object.keys(el.conditions || {}).length); - return ( -
-
- {el.inverted ? "Cannot" : "Can"}{" "} - - {el.action.toString().replaceAll(",", ", ")} - {" "} - {el.subject.toString()} {hasConditions && "with conditions:"} -
- {hasConditions && ( -
    - {Object.keys(el.conditions || {}).flatMap((field, fieldIndex) => { - const operators = ( - el.conditions as Record< - string, - | string - | { [K in PermissionConditionOperators]: string | string[] } - > - )[field]; - - const formattedFieldName = camelCaseToSpaces(field).toLowerCase(); - if (typeof operators === "string") { - return ( -
  • - - {formattedFieldName} - {" "} - equal to{" "} - {operators} -
  • - ); - } - - return Object.keys(operators).map((operator, operatorIndex) => ( -
  • - - {formattedFieldName} - {" "} - - { - formatedConditionsOperatorNames[ - operator as PermissionConditionOperators - ] - } - {" "} - - {operators[ - operator as PermissionConditionOperators - ].toString()} - -
  • - )); - })} -
- )} -
- ); - })} -
-
-
- ) : undefined, - copyActions: [ - { - value: serverResponse.reqId, - name: "Request ID", - label: `Request ID: ${serverResponse.reqId}` - } - ] - }, - { closeOnClick: false } - ); - return; - } - createNotification({ - title: "Bad Request", +export const onRequestError = (error: unknown) => { + if (axios.isAxiosError(error)) { + const serverResponse = error.response?.data as TApiErrors; + if (serverResponse?.error === ApiErrorTypes.ValidationError) { + createNotification( + { + title: "Validation Error", type: "error", - text: `${serverResponse.message}${serverResponse.message?.endsWith(".") ? "" : "."}`, + text: "Please check the input and try again.", + callToAction: ( + + + + + + + + + + + + + + + {serverResponse.message?.map(({ message, path }) => ( + + + + + ))} + +
FieldIssue
{path.join(".")}{message.toLowerCase()}
+
+
+
+ ), copyActions: [ { value: serverResponse.reqId, @@ -190,9 +67,128 @@ export const queryClient = new QueryClient({ label: `Request ID: ${serverResponse.reqId}` } ] - }); - } + }, + { closeOnClick: false } + ); + return; } + if (serverResponse?.error === ApiErrorTypes.ForbiddenError) { + createNotification( + { + title: "Forbidden Access", + type: "error", + text: `${serverResponse.message}.`, + callToAction: serverResponse?.details?.length ? ( + + + + + +
+ {serverResponse.details?.map((el, index) => { + const hasConditions = Boolean(Object.keys(el.conditions || {}).length); + return ( +
+
+ {el.inverted ? "Cannot" : "Can"}{" "} + + {el.action.toString().replaceAll(",", ", ")} + {" "} + {el.subject.toString()} {hasConditions && "with conditions:"} +
+ {hasConditions && ( +
    + {Object.keys(el.conditions || {}).flatMap((field, fieldIndex) => { + const operators = ( + el.conditions as Record< + string, + | string + | { [K in PermissionConditionOperators]: string | string[] } + > + )[field]; + + const formattedFieldName = camelCaseToSpaces(field).toLowerCase(); + if (typeof operators === "string") { + return ( +
  • + + {formattedFieldName} + {" "} + equal to{" "} + {operators} +
  • + ); + } + + return Object.keys(operators).map((operator, operatorIndex) => ( +
  • + {formattedFieldName}{" "} + + { + formatedConditionsOperatorNames[ + operator as PermissionConditionOperators + ] + } + {" "} + + {operators[operator as PermissionConditionOperators].toString()} + +
  • + )); + })} +
+ )} +
+ ); + })} +
+
+
+ ) : undefined, + copyActions: [ + { + value: serverResponse.reqId, + name: "Request ID", + label: `Request ID: ${serverResponse.reqId}` + } + ] + }, + { closeOnClick: false } + ); + return; + } + createNotification({ + title: "Bad Request", + type: "error", + text: `${serverResponse.message}${serverResponse.message?.endsWith(".") ? "" : "."}`, + copyActions: [ + { + value: serverResponse.reqId, + name: "Request ID", + label: `Request ID: ${serverResponse.reqId}` + } + ] + }); + } +}; + +export const queryClient = new QueryClient({ + mutationCache: new MutationCache({ + onError: onRequestError }), defaultOptions: { queries: { From 104edca6f11d56c51f9ee2a30f048266053c1217 Mon Sep 17 00:00:00 2001 From: Daniel Hougaard Date: Fri, 7 Mar 2025 04:34:34 +0400 Subject: [PATCH 02/71] feat: reset password without emergency kit --- .../src/server/routes/v1/password-router.ts | 13 +- backend/src/server/routes/v2/index.ts | 2 + .../src/server/routes/v2/password-router.ts | 53 +++ backend/src/services/auth/auth-fns.ts | 24 + .../services/auth/auth-password-service.ts | 145 +++++- .../src/services/auth/auth-password-type.ts | 12 + backend/src/services/user/user-service.ts | 3 + .../components/auth/DonwloadBackupPDFStep.tsx | 93 ---- .../checks/password/PasswordCheck.ts | 4 +- frontend/src/hooks/api/auth/index.tsx | 2 + frontend/src/hooks/api/auth/queries.tsx | 28 +- frontend/src/hooks/api/auth/types.ts | 15 + .../src/pages/admin/SignUpPage/SignUpPage.tsx | 194 +++----- .../DownloadBackupKeys/DownloadBackupKeys.tsx | 56 --- .../components/DownloadBackupKeys/index.tsx | 1 - .../PasswordResetPage/PasswordResetPage.tsx | 445 +++--------------- .../components/ConfirmEmailStep.tsx | 54 +++ .../components/EnterPasswordStep.tsx | 325 +++++++++++++ .../components/InputBackupKeyStep.tsx | 87 ++++ .../SignUpInvitePage/SignUpInvitePage.tsx | 50 +- .../src/pages/auth/SignUpPage/SignUpPage.tsx | 14 +- .../auth/SignUpSsoPage/SignUpSsoPage.tsx | 6 - .../BackupPDFStep/BackupPDFStep.tsx | 71 --- .../components/BackupPDFStep/index.tsx | 1 - .../UserInfoSSOStep/UserInfoSSOStep.tsx | 9 +- .../auth/VerifyEmailPage/VerifyEmailPage.tsx | 5 +- .../ChangePasswordSection.tsx | 23 +- .../PersonalGeneralTab/PersonalGeneralTab.tsx | 8 +- 28 files changed, 910 insertions(+), 833 deletions(-) create mode 100644 backend/src/server/routes/v2/password-router.ts delete mode 100644 frontend/src/components/auth/DonwloadBackupPDFStep.tsx delete mode 100644 frontend/src/pages/admin/SignUpPage/components/DownloadBackupKeys/DownloadBackupKeys.tsx delete mode 100644 frontend/src/pages/admin/SignUpPage/components/DownloadBackupKeys/index.tsx create mode 100644 frontend/src/pages/auth/PasswordResetPage/components/ConfirmEmailStep.tsx create mode 100644 frontend/src/pages/auth/PasswordResetPage/components/EnterPasswordStep.tsx create mode 100644 frontend/src/pages/auth/PasswordResetPage/components/InputBackupKeyStep.tsx delete mode 100644 frontend/src/pages/auth/SignUpSsoPage/components/BackupPDFStep/BackupPDFStep.tsx delete mode 100644 frontend/src/pages/auth/SignUpSsoPage/components/BackupPDFStep/index.tsx diff --git a/backend/src/server/routes/v1/password-router.ts b/backend/src/server/routes/v1/password-router.ts index e96a577d9..724468e02 100644 --- a/backend/src/server/routes/v1/password-router.ts +++ b/backend/src/server/routes/v1/password-router.ts @@ -6,6 +6,7 @@ import { authRateLimit } from "@app/server/config/rateLimiter"; import { verifyAuth } from "@app/server/plugins/auth/verify-auth"; import { validateSignUpAuthorization } from "@app/services/auth/auth-fns"; import { AuthMode } from "@app/services/auth/auth-type"; +import { UserEncryption } from "@app/services/user/user-types"; export const registerPasswordRouter = async (server: FastifyZodProvider) => { server.route({ @@ -113,20 +114,16 @@ export const registerPasswordRouter = async (server: FastifyZodProvider) => { }), response: { 200: z.object({ - message: z.string(), user: UsersSchema, - token: z.string() + token: z.string(), + userEncryptionVersion: z.nativeEnum(UserEncryption) }) } }, handler: async (req) => { - const { token, user } = await server.services.password.verifyPasswordResetEmail(req.body.email, req.body.code); + const passwordReset = await server.services.password.verifyPasswordResetEmail(req.body.email, req.body.code); - return { - message: "Successfully verified email", - user, - token - }; + return passwordReset; } }); diff --git a/backend/src/server/routes/v2/index.ts b/backend/src/server/routes/v2/index.ts index 3d7581a70..cece502da 100644 --- a/backend/src/server/routes/v2/index.ts +++ b/backend/src/server/routes/v2/index.ts @@ -3,6 +3,7 @@ import { registerIdentityOrgRouter } from "./identity-org-router"; import { registerIdentityProjectRouter } from "./identity-project-router"; import { registerMfaRouter } from "./mfa-router"; import { registerOrgRouter } from "./organization-router"; +import { registerPasswordRouter } from "./password-router"; import { registerProjectMembershipRouter } from "./project-membership-router"; import { registerProjectRouter } from "./project-router"; import { registerServiceTokenRouter } from "./service-token-router"; @@ -12,6 +13,7 @@ export const registerV2Routes = async (server: FastifyZodProvider) => { await server.register(registerMfaRouter, { prefix: "/auth" }); await server.register(registerUserRouter, { prefix: "/users" }); await server.register(registerServiceTokenRouter, { prefix: "/service-token" }); + await server.register(registerPasswordRouter, { prefix: "/password" }); await server.register( async (orgRouter) => { await orgRouter.register(registerOrgRouter); diff --git a/backend/src/server/routes/v2/password-router.ts b/backend/src/server/routes/v2/password-router.ts new file mode 100644 index 000000000..165130dec --- /dev/null +++ b/backend/src/server/routes/v2/password-router.ts @@ -0,0 +1,53 @@ +import { z } from "zod"; + +import { authRateLimit } from "@app/server/config/rateLimiter"; +import { validatePasswordResetAuthorization } from "@app/services/auth/auth-fns"; +import { AuthMode } from "@app/services/auth/auth-type"; +import { verifyAuth } from "@app/server/plugins/auth/verify-auth"; +import { ResetPasswordV2Type } from "@app/services/auth/auth-password-type"; + +export const registerPasswordRouter = async (server: FastifyZodProvider) => { + server.route({ + method: "POST", + url: "/password-reset", + config: { + rateLimit: authRateLimit + }, + schema: { + body: z.object({ + newPassword: z.string().trim() + }) + }, + handler: async (req) => { + const token = validatePasswordResetAuthorization(req.headers.authorization); + await server.services.password.resetPasswordV2({ + type: ResetPasswordV2Type.Recovery, + newPassword: req.body.newPassword, + userId: token.userId + }); + } + }); + + server.route({ + method: "POST", + url: "/user/password-reset", + schema: { + body: z.object({ + oldPassword: z.string().trim(), + newPassword: z.string().trim() + }) + }, + config: { + rateLimit: authRateLimit + }, + onRequest: verifyAuth([AuthMode.JWT], { requireOrg: false }), + handler: async (req) => { + await server.services.password.resetPasswordV2({ + type: ResetPasswordV2Type.LoggedInReset, + userId: req.permission.id, + newPassword: req.body.newPassword, + oldPassword: req.body.oldPassword + }); + } + }); +}; diff --git a/backend/src/services/auth/auth-fns.ts b/backend/src/services/auth/auth-fns.ts index 5f7aca812..c72f6c6a7 100644 --- a/backend/src/services/auth/auth-fns.ts +++ b/backend/src/services/auth/auth-fns.ts @@ -45,6 +45,30 @@ export const validateSignUpAuthorization = (token: string, userId: string, valid if (decodedToken.userId !== userId) throw new UnauthorizedError(); }; +export const validatePasswordResetAuthorization = (token?: string) => { + if (!token) throw new UnauthorizedError(); + + const appCfg = getConfig(); + const [AUTH_TOKEN_TYPE, AUTH_TOKEN_VALUE] = <[string, string]>token?.split(" ", 2) ?? [null, null]; + if (AUTH_TOKEN_TYPE === null) { + throw new UnauthorizedError({ message: "Missing Authorization Header in the request header." }); + } + if (AUTH_TOKEN_TYPE.toLowerCase() !== "bearer") { + throw new UnauthorizedError({ + message: `The provided authentication type '${AUTH_TOKEN_TYPE}' is not supported.` + }); + } + if (AUTH_TOKEN_VALUE === null) { + throw new UnauthorizedError({ + message: "Missing Authorization Body in the request header" + }); + } + + const decodedToken = jwt.verify(AUTH_TOKEN_VALUE, appCfg.AUTH_SECRET) as AuthModeProviderSignUpTokenPayload; + + return decodedToken; +}; + export const enforceUserLockStatus = (isLocked: boolean, temporaryLockDateEnd?: Date | null) => { if (isLocked) { throw new ForbiddenRequestError({ diff --git a/backend/src/services/auth/auth-password-service.ts b/backend/src/services/auth/auth-password-service.ts index 9f004eafc..349bcb3d0 100644 --- a/backend/src/services/auth/auth-password-service.ts +++ b/backend/src/services/auth/auth-password-service.ts @@ -4,6 +4,8 @@ import jwt from "jsonwebtoken"; import { SecretEncryptionAlgo, SecretKeyEncoding } from "@app/db/schemas"; import { getConfig } from "@app/lib/config/env"; import { generateSrpServerKey, srpCheckClientProof } from "@app/lib/crypto"; +import { infisicalSymmetricDecrypt, infisicalSymmetricEncypt } from "@app/lib/crypto/encryption"; +import { generateUserSrpKeys } from "@app/lib/crypto/srp"; import { BadRequestError } from "@app/lib/errors"; import { OrgServiceActor } from "@app/lib/types"; @@ -12,10 +14,13 @@ import { TokenType } from "../auth-token/auth-token-types"; import { SmtpTemplates, TSmtpService } from "../smtp/smtp-service"; import { TTotpConfigDALFactory } from "../totp/totp-config-dal"; import { TUserDALFactory } from "../user/user-dal"; +import { UserEncryption } from "../user/user-types"; import { TAuthDALFactory } from "./auth-dal"; import { + ResetPasswordV2Type, TChangePasswordDTO, TCreateBackupPrivateKeyDTO, + TResetPasswordV2DTO, TResetPasswordViaBackupKeyDTO, TSetupPasswordViaBackupKeyDTO } from "./auth-password-type"; @@ -114,26 +119,35 @@ export const authPaswordServiceFactory = ({ * Email password reset flow via email. Step 1 send email */ const sendPasswordResetEmail = async (email: string) => { + const startTime = Date.now(); + const user = await userDAL.findUserByUsername(email); - // ignore as user is not found to avoid an outside entity to identify infisical registered accounts - if (!user || (user && !user.isAccepted)) return; - const cfg = getConfig(); - const token = await tokenService.createTokenForUser({ - type: TokenType.TOKEN_EMAIL_PASSWORD_RESET, - userId: user.id - }); + if (user && user.isAccepted) { + const cfg = getConfig(); + const token = await tokenService.createTokenForUser({ + type: TokenType.TOKEN_EMAIL_PASSWORD_RESET, + userId: user.id + }); - await smtpService.sendMail({ - template: SmtpTemplates.ResetPassword, - recipients: [email], - subjectLine: "Infisical password reset", - substitutions: { - email, - token, - callback_url: cfg.SITE_URL ? `${cfg.SITE_URL}/password-reset` : "" - } - }); + await smtpService.sendMail({ + template: SmtpTemplates.ResetPassword, + recipients: [email], + subjectLine: "Infisical password reset", + substitutions: { + email, + token, + callback_url: cfg.SITE_URL ? `${cfg.SITE_URL}/password-reset` : "" + } + }); + } + + const elapsedTime = Date.now() - startTime; + // daniel: ensure each request takes 8 seconds to prevent timing attacks + if (elapsedTime < 8_000) { + // eslint-disable-next-line no-promise-executor-return + await new Promise((resolve) => setTimeout(resolve, 8_000 - elapsedTime)); + } }; /* @@ -142,6 +156,11 @@ export const authPaswordServiceFactory = ({ const verifyPasswordResetEmail = async (email: string, code: string) => { const cfg = getConfig(); const user = await userDAL.findUserByUsername(email); + + const userEnc = await userDAL.findUserEncKeyByUserId(user.id); + + if (!userEnc) throw new BadRequestError({ message: "Failed to find user encryption data" }); + // ignore as user is not found to avoid an outside entity to identify infisical registered accounts if (!user || (user && !user.isAccepted)) { throw new Error("Failed email verification for pass reset"); @@ -162,8 +181,95 @@ export const authPaswordServiceFactory = ({ { expiresIn: cfg.JWT_SIGNUP_LIFETIME } ); - return { token, user }; + return { token, user, userEncryptionVersion: userEnc.encryptionVersion as UserEncryption }; }; + + const resetPasswordV2 = async ({ userId, newPassword, type, oldPassword }: TResetPasswordV2DTO) => { + const cfg = getConfig(); + + const user = await userDAL.findUserEncKeyByUserId(userId); + if (!user) { + throw new BadRequestError({ message: `User encryption key not found for user with ID '${userId}'` }); + } + + if (!user.hashedPassword) { + throw new BadRequestError({ message: "Unable to reset password, no password is set" }); + } + + if (!user.authMethods?.includes(AuthMethod.EMAIL)) { + throw new BadRequestError({ message: "Unable to reset password, no email authentication method is configured" }); + } + + // we check the old password if the user is resetting their password while logged in + if (type === ResetPasswordV2Type.LoggedInReset) { + if (!oldPassword) { + throw new BadRequestError({ message: "Current password is required." }); + } + + const isValid = await bcrypt.compare(oldPassword, user.hashedPassword); + if (!isValid) { + throw new BadRequestError({ message: "Incorrect current password." }); + } + } + + const newHashedPassword = await bcrypt.hash(newPassword, cfg.BCRYPT_SALT_ROUND); + + // we need to get the original private key first for v2 + let privateKey: string; + if ( + user.serverEncryptedPrivateKey && + user.serverEncryptedPrivateKeyTag && + user.serverEncryptedPrivateKeyIV && + user.serverEncryptedPrivateKeyEncoding && + user.encryptionVersion === UserEncryption.V2 + ) { + privateKey = infisicalSymmetricDecrypt({ + iv: user.serverEncryptedPrivateKeyIV, + tag: user.serverEncryptedPrivateKeyTag, + ciphertext: user.serverEncryptedPrivateKey, + keyEncoding: user.serverEncryptedPrivateKeyEncoding as SecretKeyEncoding + }); + } else { + throw new BadRequestError({ + message: "Cannot reset password without current credentials or recovery method", + name: "Reset password" + }); + } + + const encKeys = await generateUserSrpKeys(user.username, newPassword, { + publicKey: user.publicKey, + privateKey + }); + + const { tag, iv, ciphertext, encoding } = infisicalSymmetricEncypt(privateKey); + + await userDAL.transaction(async (tx) => { + await userDAL.updateUserEncryptionByUserId( + userId, + { + hashedPassword: newHashedPassword, + + // srp params + salt: encKeys.salt, + verifier: encKeys.verifier, + + protectedKey: encKeys.protectedKey, + protectedKeyIV: encKeys.protectedKeyIV, + protectedKeyTag: encKeys.protectedKeyTag, + encryptedPrivateKey: encKeys.encryptedPrivateKey, + iv: encKeys.encryptedPrivateKeyIV, + tag: encKeys.encryptedPrivateKeyTag, + + serverEncryptedPrivateKey: ciphertext, + serverEncryptedPrivateKeyIV: iv, + serverEncryptedPrivateKeyTag: tag, + serverEncryptedPrivateKeyEncoding: encoding + }, + tx + ); + }); + }; + /* * Reset password of a user via backup key * */ @@ -391,6 +497,7 @@ export const authPaswordServiceFactory = ({ createBackupPrivateKey, getBackupPrivateKeyOfUser, sendPasswordSetupEmail, - setupPassword + setupPassword, + resetPasswordV2 }; }; diff --git a/backend/src/services/auth/auth-password-type.ts b/backend/src/services/auth/auth-password-type.ts index 7c67c0934..b3b14c3b4 100644 --- a/backend/src/services/auth/auth-password-type.ts +++ b/backend/src/services/auth/auth-password-type.ts @@ -13,6 +13,18 @@ export type TChangePasswordDTO = { password: string; }; +export enum ResetPasswordV2Type { + Recovery = "recovery", + LoggedInReset = "logged-in-reset" +} + +export type TResetPasswordV2DTO = { + type: ResetPasswordV2Type; + userId: string; + newPassword: string; + oldPassword?: string; +}; + export type TResetPasswordViaBackupKeyDTO = { userId: string; protectedKey: string; diff --git a/backend/src/services/user/user-service.ts b/backend/src/services/user/user-service.ts index 5da5d493c..d272845c4 100644 --- a/backend/src/services/user/user-service.ts +++ b/backend/src/services/user/user-service.ts @@ -3,7 +3,9 @@ import { ForbiddenError } from "@casl/ability"; import { SecretKeyEncoding } from "@app/db/schemas"; import { OrgPermissionActions, OrgPermissionSubjects } from "@app/ee/services/permission/org-permission"; import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service"; + import { infisicalSymmetricDecrypt } from "@app/lib/crypto/encryption"; + import { BadRequestError, ForbiddenRequestError, NotFoundError } from "@app/lib/errors"; import { TAuthTokenServiceFactory } from "@app/services/auth-token/auth-token-service"; import { TokenType } from "@app/services/auth-token/auth-token-types"; @@ -31,6 +33,7 @@ type TUserServiceFactoryDep = { | "createUserAction" | "findUserEncKeyByUserId" | "delete" + | "updateUserEncryptionByUserId" >; userAliasDAL: Pick; groupProjectDAL: Pick; diff --git a/frontend/src/components/auth/DonwloadBackupPDFStep.tsx b/frontend/src/components/auth/DonwloadBackupPDFStep.tsx deleted file mode 100644 index b11ca8bd2..000000000 --- a/frontend/src/components/auth/DonwloadBackupPDFStep.tsx +++ /dev/null @@ -1,93 +0,0 @@ -import { useTranslation } from "react-i18next"; -import { faWarning } from "@fortawesome/free-solid-svg-icons"; -import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; - -import { useToggle } from "@app/hooks"; -import { generateUserBackupKey } from "@app/lib/crypto"; - -import { createNotification } from "../notifications"; -import { generateBackupPDFAsync } from "../utilities/generateBackupPDF"; -import { Button } from "../v2"; - -interface DownloadBackupPDFStepProps { - incrementStep: () => void; - email: string; - password: string; - name: string; -} - -/** - * This is the step of the signup flow where the user downloads the backup pdf - * @param {object} obj - * @param {function} obj.incrementStep - function that moves the user on to the next stage of signup - * @param {string} obj.email - user's email - * @param {string} obj.password - user's password - * @param {string} obj.name - user's name - * @returns - */ -export default function DonwloadBackupPDFStep({ - incrementStep, - email, - password, - name -}: DownloadBackupPDFStepProps): JSX.Element { - const { t } = useTranslation(); - - const [isLoading, setIsLoading] = useToggle(); - - const handleBackupKeyGenerate = async () => { - try { - setIsLoading.on(); - const generatedKey = await generateUserBackupKey(email, password); - await generateBackupPDFAsync({ - generatedKey, - personalEmail: email, - personalName: name - }); - incrementStep(); - } catch (err) { - console.log(err); - createNotification({ - type: "error", - text: "Failed to generate backup key" - }); - } finally { - setIsLoading.off(); - } - }; - - return ( -
-

- - {t("signup.step4-message")} -

-
-
- - {t("signup.step4-description1")} {t("signup.step4-description3")} - -
-
-
- -
-
-
-
- ); -} diff --git a/frontend/src/components/utilities/checks/password/PasswordCheck.ts b/frontend/src/components/utilities/checks/password/PasswordCheck.ts index e37abd475..fb5186220 100644 --- a/frontend/src/components/utilities/checks/password/PasswordCheck.ts +++ b/frontend/src/components/utilities/checks/password/PasswordCheck.ts @@ -34,12 +34,12 @@ const passwordCheck = async ({ const tests = [ { name: "tooShort", - validator: (pwd: string) => pwd.length >= 14, + validator: (pwd: string) => pwd?.length >= 14, setError: setPasswordErrorTooShort }, { name: "tooLong", - validator: (pwd: string) => pwd.length < 101, + validator: (pwd: string) => pwd?.length < 101, setError: setPasswordErrorTooLong }, { diff --git a/frontend/src/hooks/api/auth/index.tsx b/frontend/src/hooks/api/auth/index.tsx index 66688cbdc..392f7b1f5 100644 --- a/frontend/src/hooks/api/auth/index.tsx +++ b/frontend/src/hooks/api/auth/index.tsx @@ -2,6 +2,8 @@ export { useGetAuthToken, useOauthTokenExchange, useResetPassword, + useResetPasswordV2, + useResetUserPasswordV2, useSelectOrganization, useSendMfaToken, useSendPasswordResetEmail, diff --git a/frontend/src/hooks/api/auth/queries.tsx b/frontend/src/hooks/api/auth/queries.tsx index 9b8afbac7..796fd3152 100644 --- a/frontend/src/hooks/api/auth/queries.tsx +++ b/frontend/src/hooks/api/auth/queries.tsx @@ -22,12 +22,15 @@ import { LoginLDAPRes, MfaMethod, ResetPasswordDTO, + ResetPasswordV2DTO, + ResetUserPasswordV2DTO, SendMfaTokenDTO, SetupPasswordDTO, SRP1DTO, SRPR1Res, TOauthTokenExchangeDTO, UserAgentType, + UserEncryptionVersion, VerifyMfaTokenDTO, VerifyMfaTokenRes, VerifySignupInviteDTO @@ -247,7 +250,10 @@ export const useSendPasswordResetEmail = () => { export const useVerifyPasswordResetCode = () => { return useMutation({ mutationFn: async ({ email, code }: { email: string; code: string }) => { - const { data } = await apiRequest.post("/api/v1/password/email/password-reset-verify", { + const { data } = await apiRequest.post<{ + token: string; + userEncryptionVersion: UserEncryptionVersion; + }>("/api/v1/password/email/password-reset-verify", { email, code }); @@ -302,6 +308,26 @@ export const useResetPassword = () => { }); }; +export const useResetPasswordV2 = () => { + return useMutation({ + mutationFn: async (details: ResetPasswordV2DTO) => { + await apiRequest.post("/api/v2/password/password-reset", details, { + headers: { + Authorization: `Bearer ${details.verificationToken}` + } + }); + } + }); +}; + +export const useResetUserPasswordV2 = () => { + return useMutation({ + mutationFn: async (details: ResetUserPasswordV2DTO) => { + await apiRequest.post("/api/v2/password/user/password-reset", details); + } + }); +}; + export const changePassword = async (details: ChangePasswordDTO) => { const { data } = await apiRequest.post("/api/v1/password/change-password", details); return data; diff --git a/frontend/src/hooks/api/auth/types.ts b/frontend/src/hooks/api/auth/types.ts index 036897fed..32610c28d 100644 --- a/frontend/src/hooks/api/auth/types.ts +++ b/frontend/src/hooks/api/auth/types.ts @@ -3,6 +3,11 @@ export type GetAuthTokenAPI = { organizationId?: string; }; +export enum UserEncryptionVersion { + V1 = 1, + V2 = 2 +} + export type SendMfaTokenDTO = { email: string; }; @@ -136,6 +141,16 @@ export type ResetPasswordDTO = { password: string; }; +export type ResetPasswordV2DTO = { + newPassword: string; + verificationToken: string; +}; + +export type ResetUserPasswordV2DTO = { + oldPassword: string; + newPassword: string; +}; + export type SetupPasswordDTO = { protectedKey: string; protectedKeyIV: string; diff --git a/frontend/src/pages/admin/SignUpPage/SignUpPage.tsx b/frontend/src/pages/admin/SignUpPage/SignUpPage.tsx index f1c70d028..97b363558 100644 --- a/frontend/src/pages/admin/SignUpPage/SignUpPage.tsx +++ b/frontend/src/pages/admin/SignUpPage/SignUpPage.tsx @@ -1,4 +1,3 @@ -import { useState } from "react"; import { Helmet } from "react-helmet"; import { Controller, useForm } from "react-hook-form"; import { useTranslation } from "react-i18next"; @@ -8,16 +7,13 @@ import { AnimatePresence, motion } from "framer-motion"; import { z } from "zod"; import { createNotification } from "@app/components/notifications"; -import { generateBackupPDFAsync } from "@app/components/utilities/generateBackupPDF"; // TODO(akhilmhdh): rewrite this into module functions in lib import { saveTokenToLocalStorage } from "@app/components/utilities/saveTokenToLocalStorage"; import SecurityClient from "@app/components/utilities/SecurityClient"; import { Button, ContentLoader, FormControl, Input } from "@app/components/v2"; import { useServerConfig } from "@app/context"; import { useCreateAdminUser, useSelectOrganization } from "@app/hooks/api"; -import { generateUserBackupKey, generateUserPassKey } from "@app/lib/crypto"; - -import { DownloadBackupKeys } from "./components/DownloadBackupKeys"; +import { generateUserPassKey } from "@app/lib/crypto"; const formSchema = z .object({ @@ -34,25 +30,17 @@ const formSchema = z type TFormSchema = z.infer; -enum SignupSteps { - DetailsForm = "details-form", - BackupKey = "backup-key" -} - export const SignUpPage = () => { const { t } = useTranslation(); const navigate = useNavigate(); const { control, handleSubmit, - getValues, formState: { isSubmitting } } = useForm({ resolver: zodResolver(formSchema) }); - const [step, setStep] = useState(SignupSteps.DetailsForm); - const { config } = useServerConfig(); const { mutateAsync: createAdminUser } = useCreateAdminUser(); const { mutateAsync: selectOrganization } = useSelectOrganization(); @@ -84,7 +72,7 @@ export const SignUpPage = () => { // Will be refactored in next iteration to make it url based rather than local storage ones // Part of migration to nextjs 14 localStorage.setItem("orgData.id", res.organization.id); - setStep(SignupSteps.BackupKey); + navigate({ to: "/admin" }); } catch (err) { console.log(err); createNotification({ @@ -94,27 +82,7 @@ export const SignUpPage = () => { } }; - const handleBackupKeyGenerate = async () => { - try { - const { email, password, firstName, lastName } = getValues(); - const generatedKey = await generateUserBackupKey(email, password); - await generateBackupPDFAsync({ - generatedKey, - personalEmail: email, - personalName: `${firstName} ${lastName}` - }); - navigate({ to: "/admin" }); - } catch (err) { - console.log(err); - createNotification({ - type: "error", - text: "Failed to generate backup" - }); - } - }; - - if (config?.initialized && step === SignupSteps.DetailsForm) - return ; + if (config?.initialized) return ; return (
@@ -127,56 +95,28 @@ export const SignUpPage = () => {
- {step === SignupSteps.DetailsForm && ( - -
- Infisical logo -
Welcome to Infisical
-
Create your first Super Admin Account
-
-
-
-
- ( - - - - )} - /> - ( - - - - )} - /> -
+ +
+ Infisical logo +
Welcome to Infisical
+
Create your first Super Admin Account
+
+ +
+
( @@ -186,56 +126,66 @@ export const SignUpPage = () => { /> ( - - - )} - /> - ( - - + )} />
- - - - )} - {step === SignupSteps.BackupKey && ( - - - - )} + ( + + + + )} + /> + ( + + + + )} + /> + ( + + + + )} + /> +
+ + +
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 -

- verify email -
- -
-
- ); - - // Input backup key - const stepInputBackupKey = ( -
-

- Enter your backup key -

-
-

- You can find it in your emergency kit. You had to download the emergency kit during - signup. -

-
-
- -
-
-
- -
-
-
- ); - - // Enter new password - const stepEnterNewPassword = ( -
-

- Enter new password -

-
-

- Make sure you save it somewhere safe. -

-
-
- { - setNewPassword(password); - passwordCheck({ - password, - setPasswordErrorTooShort, - setPasswordErrorTooLong, - setPasswordErrorNoLetterChar, - setPasswordErrorNoNumOrSpecialChar, - setPasswordErrorRepeatedChar, - setPasswordErrorEscapeChar, - setPasswordErrorLowEntropy, - setPasswordErrorBreached - }); - }} - type="password" - value={newPassword} - isRequired - error={ - passwordErrorTooShort && - passwordErrorTooLong && - passwordErrorNoLetterChar && - passwordErrorNoNumOrSpecialChar && - passwordErrorRepeatedChar && - passwordErrorEscapeChar && - passwordErrorLowEntropy && - passwordErrorBreached - } - autoComplete="new-password" - id="new-password" - /> -
- {passwordErrorTooShort || - passwordErrorTooLong || - passwordErrorNoLetterChar || - passwordErrorNoNumOrSpecialChar || - passwordErrorRepeatedChar || - passwordErrorEscapeChar || - passwordErrorLowEntropy || - passwordErrorBreached ? ( -
-
Password should contain:
-
- {passwordErrorTooShort ? ( - - ) : ( - - )} -
- at least 14 characters -
-
-
- {passwordErrorTooLong ? ( - - ) : ( - - )} -
- at most 100 characters -
-
-
- {passwordErrorNoLetterChar ? ( - - ) : ( - - )} -
- at least 1 letter character -
-
-
- {passwordErrorNoNumOrSpecialChar ? ( - - ) : ( - - )} -
- at least 1 number or special character -
-
-
- {passwordErrorRepeatedChar ? ( - - ) : ( - - )} -
- at most 3 repeated, consecutive characters -
-
-
- {passwordErrorEscapeChar ? ( - - ) : ( - - )} -
- No escape characters allowed. -
-
-
- {passwordErrorLowEntropy ? ( - - ) : ( - - )} -
- Password contains personal info. -
-
-
- {passwordErrorBreached ? ( - - ) : ( - - )} -
- Password was found in a data breach. -
-
-
- ) : ( -
- )} -
-
- -
-
- - ); 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 +

+ verify 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 ( +
+

+ Enter new password +

+
+

+ Make sure you save it somewhere safe. +

+
+
+ ( + + { + field.onChange(e); + handlePasswordCheck(e.target.value); + }} + type="password" + /> + + )} + /> +
+ {passwordErrorTooShort || + passwordErrorTooLong || + passwordErrorNoLetterChar || + passwordErrorNoNumOrSpecialChar || + passwordErrorRepeatedChar || + passwordErrorEscapeChar || + passwordErrorLowEntropy || + passwordErrorBreached ? ( +
+
Password should contain:
+
+ {passwordErrorTooShort ? ( + + ) : ( + + )} +
+ at least 14 characters +
+
+
+ {passwordErrorTooLong ? ( + + ) : ( + + )} +
+ at most 100 characters +
+
+
+ {passwordErrorNoLetterChar ? ( + + ) : ( + + )} +
+ at least 1 letter character +
+
+
+ {passwordErrorNoNumOrSpecialChar ? ( + + ) : ( + + )} +
+ at least 1 number or special character +
+
+
+ {passwordErrorRepeatedChar ? ( + + ) : ( + + )} +
+ at most 3 repeated, consecutive characters +
+
+
+ {passwordErrorEscapeChar ? ( + + ) : ( + + )} +
+ No escape characters allowed. +
+
+
+ {passwordErrorLowEntropy ? ( + + ) : ( + + )} +
+ Password contains personal info. +
+
+
+ {passwordErrorBreached ? ( + + ) : ( + + )} +
+ Password was found in a data breach. +
+
+
+ ) : ( +
+ )} +
+
+ +
+
+ + ); +}; 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 ( +
+

+ Enter your backup key +

+
+

+ You can find it in your emergency kit. You had to download the emergency kit during + signup. +

+
+
+ ( + + + + )} + /> +
+
+
+ +
+
+
+ ); +}; 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 = () => { Infisical Logo
- {step === 1 ? stepConfirmEmail : step === 2 ? main : step4} + {step === 1 && stepConfirmEmail} + {step === 2 && main} )}
diff --git a/frontend/src/pages/auth/SignUpPage/SignUpPage.tsx b/frontend/src/pages/auth/SignUpPage/SignUpPage.tsx index c1fcc30c4..75af4ff48 100644 --- a/frontend/src/pages/auth/SignUpPage/SignUpPage.tsx +++ b/frontend/src/pages/auth/SignUpPage/SignUpPage.tsx @@ -5,7 +5,6 @@ import { useTranslation } from "react-i18next"; import { useNavigate } from "@tanstack/react-router"; import CodeInputStep from "@app/components/auth/CodeInputStep"; -import DownloadBackupPDF from "@app/components/auth/DonwloadBackupPDFStep"; import EnterEmailStep from "@app/components/auth/EnterEmailStep"; import InitialSignupStep from "@app/components/auth/InitialSignupStep"; import TeamInviteStep from "@app/components/auth/TeamInviteStep"; @@ -72,7 +71,7 @@ export const SignUpPage = () => { incrementStep(); } - if (!serverDetails?.emailConfigured && step === 5) { + if (!serverDetails?.emailConfigured && step === 4) { navigate({ to: `/organization/${ProjectType.SecretManager}/overview` as const }); @@ -119,17 +118,6 @@ export const SignUpPage = () => { ); } - if (registerStep === 4) { - return ( - - ); - } - if (serverDetails?.emailConfigured) { return ; } diff --git a/frontend/src/pages/auth/SignUpSsoPage/SignUpSsoPage.tsx b/frontend/src/pages/auth/SignUpSsoPage/SignUpSsoPage.tsx index a13cfcfc6..40075ae54 100644 --- a/frontend/src/pages/auth/SignUpSsoPage/SignUpSsoPage.tsx +++ b/frontend/src/pages/auth/SignUpSsoPage/SignUpSsoPage.tsx @@ -6,7 +6,6 @@ import { jwtDecode } from "jwt-decode"; import { ROUTE_PATHS } from "@app/const/routes"; -import { BackupPDFStep } from "./components/BackupPDFStep"; import { EmailConfirmationStep } from "./components/EmailConfirmationStep"; import { UserInfoSSOStep } from "./components/UserInfoSSOStep"; @@ -57,14 +56,9 @@ export const SignupSsoPage = () => { providerOrganizationName={organizationName} password={password} setPassword={setPassword} - setStep={setStep} providerAuthToken={token} /> ); - case 2: - return ( - - ); default: return
; } diff --git a/frontend/src/pages/auth/SignUpSsoPage/components/BackupPDFStep/BackupPDFStep.tsx b/frontend/src/pages/auth/SignUpSsoPage/components/BackupPDFStep/BackupPDFStep.tsx deleted file mode 100644 index 9a2369170..000000000 --- a/frontend/src/pages/auth/SignUpSsoPage/components/BackupPDFStep/BackupPDFStep.tsx +++ /dev/null @@ -1,71 +0,0 @@ -import { useTranslation } from "react-i18next"; -import { faWarning } from "@fortawesome/free-solid-svg-icons"; -import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; -import { useNavigate } from "@tanstack/react-router"; - -import issueBackupKey from "@app/components/utilities/cryptography/issueBackupKey"; -import { Button } from "@app/components/v2"; -import { ProjectType } from "@app/hooks/api/workspace/types"; - -interface DownloadBackupPDFStepProps { - email: string; - password: string; - name: string; -} - -/** - * This is the step of the signup flow where the user downloads the backup pdf - * @param {object} obj - * @param {function} obj.incrementStep - function that moves the user on to the next stage of signup - * @param {string} obj.email - user's email - * @param {string} obj.password - user's password - * @param {string} obj.name - user's name - * @returns - */ -export const BackupPDFStep = ({ email, password, name }: DownloadBackupPDFStepProps) => { - const { t } = useTranslation(); - const navigate = useNavigate(); - - return ( -
-

- - {t("signup.step4-message")} -

-
-
- - {t("signup.step4-description1")} {t("signup.step4-description3")} - -
-
-
- -
-
-
-
- ); -}; diff --git a/frontend/src/pages/auth/SignUpSsoPage/components/BackupPDFStep/index.tsx b/frontend/src/pages/auth/SignUpSsoPage/components/BackupPDFStep/index.tsx deleted file mode 100644 index 01f7745cd..000000000 --- a/frontend/src/pages/auth/SignUpSsoPage/components/BackupPDFStep/index.tsx +++ /dev/null @@ -1 +0,0 @@ -export { BackupPDFStep } from "./BackupPDFStep"; diff --git a/frontend/src/pages/auth/SignUpSsoPage/components/UserInfoSSOStep/UserInfoSSOStep.tsx b/frontend/src/pages/auth/SignUpSsoPage/components/UserInfoSSOStep/UserInfoSSOStep.tsx index c3d131708..a6ab0623b 100644 --- a/frontend/src/pages/auth/SignUpSsoPage/components/UserInfoSSOStep/UserInfoSSOStep.tsx +++ b/frontend/src/pages/auth/SignUpSsoPage/components/UserInfoSSOStep/UserInfoSSOStep.tsx @@ -2,6 +2,7 @@ import crypto from "crypto"; import { useEffect, useState } from "react"; import { useTranslation } from "react-i18next"; +import { useNavigate } from "@tanstack/react-router"; import jsrp from "jsrp"; import nacl from "tweetnacl"; import { encodeBase64 } from "tweetnacl-util"; @@ -17,12 +18,12 @@ import { useToggle } from "@app/hooks"; import { completeAccountSignup, useSelectOrganization } from "@app/hooks/api/auth/queries"; import { MfaMethod } from "@app/hooks/api/auth/types"; import { fetchOrganizations } from "@app/hooks/api/organization/queries"; +import { ProjectType } from "@app/hooks/api/workspace/types"; // eslint-disable-next-line new-cap const client = new jsrp.client(); type Props = { - setStep: (step: number) => void; username: string; password: string; setPassword: (value: string) => void; @@ -50,7 +51,6 @@ export const UserInfoSSOStep = ({ providerOrganizationName, password, setPassword, - setStep, providerAuthToken }: Props) => { const [nameError, setNameError] = useState(false); @@ -63,6 +63,7 @@ export const UserInfoSSOStep = ({ const { t } = useTranslation(); const { mutateAsync: selectOrganization } = useSelectOrganization(); const [mfaSuccessCallback, setMfaSuccessCallback] = useState<() => void>(() => {}); + const navigate = useNavigate(); useEffect(() => { const randomPassword = crypto.randomBytes(32).toString("hex"); @@ -202,7 +203,9 @@ export const UserInfoSSOStep = ({ } localStorage.setItem("orgData.id", orgId); - setStep(2); + navigate({ + to: `/organization/${ProjectType.SecretManager}/overview` as const + }); } catch (error) { setIsLoading(false); console.error(error); diff --git a/frontend/src/pages/auth/VerifyEmailPage/VerifyEmailPage.tsx b/frontend/src/pages/auth/VerifyEmailPage/VerifyEmailPage.tsx index 418529bdc..bc0bdf1ff 100644 --- a/frontend/src/pages/auth/VerifyEmailPage/VerifyEmailPage.tsx +++ b/frontend/src/pages/auth/VerifyEmailPage/VerifyEmailPage.tsx @@ -72,8 +72,9 @@ export const VerifyEmailPage = () => { Forgot your password?

-

- You will need your emergency kit. Enter your email to start account recovery. +

+ Enter your email to start the password reset process. You will receive an email with + instructions.

diff --git a/frontend/src/pages/user/PersonalSettingsPage/components/ChangePasswordSection/ChangePasswordSection.tsx b/frontend/src/pages/user/PersonalSettingsPage/components/ChangePasswordSection/ChangePasswordSection.tsx index 9c34693df..8a2fd7010 100644 --- a/frontend/src/pages/user/PersonalSettingsPage/components/ChangePasswordSection/ChangePasswordSection.tsx +++ b/frontend/src/pages/user/PersonalSettingsPage/components/ChangePasswordSection/ChangePasswordSection.tsx @@ -11,7 +11,8 @@ import attemptChangePassword from "@app/components/utilities/attemptChangePasswo import checkPassword from "@app/components/utilities/checks/password/checkPassword"; import { Button, FormControl, Input } from "@app/components/v2"; import { useUser } from "@app/context"; -import { useSendPasswordSetupEmail } from "@app/hooks/api/auth/queries"; +import { useResetUserPasswordV2, useSendPasswordSetupEmail } from "@app/hooks/api/auth/queries"; +import { UserEncryptionVersion } from "@app/hooks/api/auth/types"; type Errors = { tooShort?: string; @@ -47,6 +48,7 @@ export const ChangePasswordSection = () => { const [errors, setErrors] = useState({}); const [isLoading, setIsLoading] = useState(false); const sendSetupPasswordEmail = useSendPasswordSetupEmail(); + const { mutateAsync: resetPasswordV2 } = useResetUserPasswordV2(); const onFormSubmit = async ({ oldPassword, newPassword }: FormData) => { try { @@ -56,13 +58,20 @@ export const ChangePasswordSection = () => { }); if (errorCheck) return; - setIsLoading(true); - await attemptChangePassword({ - email: user.username, - currentPassword: oldPassword, - newPassword - }); + + if (user.encryptionVersion === UserEncryptionVersion.V2) { + await resetPasswordV2({ + oldPassword, + newPassword + }); + } else { + await attemptChangePassword({ + email: user.username, + currentPassword: oldPassword, + newPassword + }); + } setIsLoading(false); createNotification({ diff --git a/frontend/src/pages/user/PersonalSettingsPage/components/PersonalGeneralTab/PersonalGeneralTab.tsx b/frontend/src/pages/user/PersonalSettingsPage/components/PersonalGeneralTab/PersonalGeneralTab.tsx index ac4c5bea2..dcf35e672 100644 --- a/frontend/src/pages/user/PersonalSettingsPage/components/PersonalGeneralTab/PersonalGeneralTab.tsx +++ b/frontend/src/pages/user/PersonalSettingsPage/components/PersonalGeneralTab/PersonalGeneralTab.tsx @@ -1,14 +1,20 @@ +import { useUser } from "@app/context"; +import { UserEncryptionVersion } from "@app/hooks/api/auth/types"; + import { DeleteAccountSection } from "../DeleteAccountSection"; import { EmergencyKitSection } from "../EmergencyKitSection"; import { SessionsSection } from "../SessionsSection"; import { UserNameSection } from "../UserNameSection"; export const PersonalGeneralTab = () => { + const { user } = useUser(); + const encryptionVersion = user?.encryptionVersion ?? UserEncryptionVersion.V2; + return (
- + {encryptionVersion === UserEncryptionVersion.V1 && }
); From 7003ad608a1fa1bf54d65aefc744e104d5a136c5 Mon Sep 17 00:00:00 2001 From: Daniel Hougaard Date: Fri, 7 Mar 2025 04:37:08 +0400 Subject: [PATCH 03/71] Update user-service.ts --- backend/src/services/user/user-service.ts | 3 --- 1 file changed, 3 deletions(-) diff --git a/backend/src/services/user/user-service.ts b/backend/src/services/user/user-service.ts index d272845c4..5da5d493c 100644 --- a/backend/src/services/user/user-service.ts +++ b/backend/src/services/user/user-service.ts @@ -3,9 +3,7 @@ import { ForbiddenError } from "@casl/ability"; import { SecretKeyEncoding } from "@app/db/schemas"; import { OrgPermissionActions, OrgPermissionSubjects } from "@app/ee/services/permission/org-permission"; import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service"; - import { infisicalSymmetricDecrypt } from "@app/lib/crypto/encryption"; - import { BadRequestError, ForbiddenRequestError, NotFoundError } from "@app/lib/errors"; import { TAuthTokenServiceFactory } from "@app/services/auth-token/auth-token-service"; import { TokenType } from "@app/services/auth-token/auth-token-types"; @@ -33,7 +31,6 @@ type TUserServiceFactoryDep = { | "createUserAction" | "findUserEncKeyByUserId" | "delete" - | "updateUserEncryptionByUserId" >; userAliasDAL: Pick; groupProjectDAL: Pick; From c48c9ae628ab64c407cb29bb8716b8a664f0c2e6 Mon Sep 17 00:00:00 2001 From: Daniel Hougaard Date: Fri, 7 Mar 2025 04:55:18 +0400 Subject: [PATCH 04/71] cleanup --- .../services/auth/auth-password-service.ts | 38 +++++++++---------- .../ChangePasswordSection.tsx | 4 +- 2 files changed, 20 insertions(+), 22 deletions(-) diff --git a/backend/src/services/auth/auth-password-service.ts b/backend/src/services/auth/auth-password-service.ts index 349bcb3d0..4d01870f6 100644 --- a/backend/src/services/auth/auth-password-service.ts +++ b/backend/src/services/auth/auth-password-service.ts @@ -243,31 +243,27 @@ export const authPaswordServiceFactory = ({ const { tag, iv, ciphertext, encoding } = infisicalSymmetricEncypt(privateKey); - await userDAL.transaction(async (tx) => { - await userDAL.updateUserEncryptionByUserId( - userId, - { - hashedPassword: newHashedPassword, + await userDAL.updateUserEncryptionByUserId(userId, { + hashedPassword: newHashedPassword, - // srp params - salt: encKeys.salt, - verifier: encKeys.verifier, + // srp params + salt: encKeys.salt, + verifier: encKeys.verifier, - protectedKey: encKeys.protectedKey, - protectedKeyIV: encKeys.protectedKeyIV, - protectedKeyTag: encKeys.protectedKeyTag, - encryptedPrivateKey: encKeys.encryptedPrivateKey, - iv: encKeys.encryptedPrivateKeyIV, - tag: encKeys.encryptedPrivateKeyTag, + protectedKey: encKeys.protectedKey, + protectedKeyIV: encKeys.protectedKeyIV, + protectedKeyTag: encKeys.protectedKeyTag, + encryptedPrivateKey: encKeys.encryptedPrivateKey, + iv: encKeys.encryptedPrivateKeyIV, + tag: encKeys.encryptedPrivateKeyTag, - serverEncryptedPrivateKey: ciphertext, - serverEncryptedPrivateKeyIV: iv, - serverEncryptedPrivateKeyTag: tag, - serverEncryptedPrivateKeyEncoding: encoding - }, - tx - ); + serverEncryptedPrivateKey: ciphertext, + serverEncryptedPrivateKeyIV: iv, + serverEncryptedPrivateKeyTag: tag, + serverEncryptedPrivateKeyEncoding: encoding }); + + await tokenService.revokeAllMySessions(userId); }; /* diff --git a/frontend/src/pages/user/PersonalSettingsPage/components/ChangePasswordSection/ChangePasswordSection.tsx b/frontend/src/pages/user/PersonalSettingsPage/components/ChangePasswordSection/ChangePasswordSection.tsx index 8a2fd7010..d6842c755 100644 --- a/frontend/src/pages/user/PersonalSettingsPage/components/ChangePasswordSection/ChangePasswordSection.tsx +++ b/frontend/src/pages/user/PersonalSettingsPage/components/ChangePasswordSection/ChangePasswordSection.tsx @@ -13,6 +13,7 @@ import { Button, FormControl, Input } from "@app/components/v2"; import { useUser } from "@app/context"; import { useResetUserPasswordV2, useSendPasswordSetupEmail } from "@app/hooks/api/auth/queries"; import { UserEncryptionVersion } from "@app/hooks/api/auth/types"; +import { useNavigate } from "@tanstack/react-router"; type Errors = { tooShort?: string; @@ -36,6 +37,7 @@ export type FormData = z.infer; export const ChangePasswordSection = () => { const { t } = useTranslation(); + const navigate = useNavigate(); const { user } = useUser(); const { reset, control, handleSubmit } = useForm({ @@ -80,7 +82,7 @@ export const ChangePasswordSection = () => { }); reset(); - window.location.href = "/login"; + navigate({ to: "/login" }); } catch (err) { console.error(err); setIsLoading(false); From 214f837041057d33eef5f31a63b38e8ed3432d26 Mon Sep 17 00:00:00 2001 From: carlosmonastyrski Date: Fri, 7 Mar 2025 11:42:15 -0300 Subject: [PATCH 05/71] Add is-admin filter to Server Admin Console and add a component to show the server admins on side panel --- backend/src/server/routes/v1/admin-router.ts | 7 +- .../super-admin/super-admin-service.ts | 5 +- .../services/super-admin/super-admin-types.ts | 1 + backend/src/services/user/user-dal.ts | 9 +- frontend/src/hooks/api/admin/types.ts | 1 + .../MinimizedOrgSidebar.tsx | 27 ++++++ .../ServerAdminsPanel/ServerAdminsPanel.tsx | 85 +++++++++++++++++++ .../OverviewPage/components/UserPanel.tsx | 79 ++++++++++++++--- 8 files changed, 197 insertions(+), 17 deletions(-) create mode 100644 frontend/src/layouts/OrganizationLayout/components/ServerAdminsPanel/ServerAdminsPanel.tsx diff --git a/backend/src/server/routes/v1/admin-router.ts b/backend/src/server/routes/v1/admin-router.ts index 076b33e54..b63550d2c 100644 --- a/backend/src/server/routes/v1/admin-router.ts +++ b/backend/src/server/routes/v1/admin-router.ts @@ -118,7 +118,12 @@ export const registerAdminRouter = async (server: FastifyZodProvider) => { querystring: z.object({ searchTerm: z.string().default(""), offset: z.coerce.number().default(0), - limit: z.coerce.number().max(100).default(20) + limit: z.coerce.number().max(100).default(20), + // TODO: remove this once z.coerce.boolean() is supported + adminsOnly: z + .string() + .transform((val) => val === "true") + .default("false") }), response: { 200: z.object({ diff --git a/backend/src/services/super-admin/super-admin-service.ts b/backend/src/services/super-admin/super-admin-service.ts index 166f73317..8efdfd7f9 100644 --- a/backend/src/services/super-admin/super-admin-service.ts +++ b/backend/src/services/super-admin/super-admin-service.ts @@ -271,12 +271,13 @@ export const superAdminServiceFactory = ({ return { token, user: userInfo, organization }; }; - const getUsers = ({ offset, limit, searchTerm }: TAdminGetUsersDTO) => { + const getUsers = ({ offset, limit, searchTerm, adminsOnly }: TAdminGetUsersDTO) => { return userDAL.getUsersByFilter({ limit, offset, searchTerm, - sortBy: "username" + sortBy: "username", + adminsOnly }); }; diff --git a/backend/src/services/super-admin/super-admin-types.ts b/backend/src/services/super-admin/super-admin-types.ts index 2d10941b4..d6de67e59 100644 --- a/backend/src/services/super-admin/super-admin-types.ts +++ b/backend/src/services/super-admin/super-admin-types.ts @@ -20,6 +20,7 @@ export type TAdminGetUsersDTO = { offset: number; limit: number; searchTerm: string; + adminsOnly: boolean; }; export enum LoginMethod { diff --git a/backend/src/services/user/user-dal.ts b/backend/src/services/user/user-dal.ts index 99f403e84..eba497f0f 100644 --- a/backend/src/services/user/user-dal.ts +++ b/backend/src/services/user/user-dal.ts @@ -23,15 +23,18 @@ export const userDALFactory = (db: TDbClient) => { limit, offset, searchTerm, - sortBy + sortBy, + adminsOnly }: { limit: number; offset: number; searchTerm: string; sortBy?: keyof TUsers; + adminsOnly: boolean; }) => { try { let query = db.replicaNode()(TableName.Users).where("isGhost", "=", false); + if (searchTerm) { query = query.where((qb) => { void qb @@ -42,6 +45,10 @@ export const userDALFactory = (db: TDbClient) => { }); } + if (adminsOnly) { + query = query.where("superAdmin", true); + } + if (sortBy) { query = query.orderBy(sortBy); } diff --git a/frontend/src/hooks/api/admin/types.ts b/frontend/src/hooks/api/admin/types.ts index e5d281fc4..80e3edc92 100644 --- a/frontend/src/hooks/api/admin/types.ts +++ b/frontend/src/hooks/api/admin/types.ts @@ -50,6 +50,7 @@ export type TUpdateAdminSlackConfigDTO = { export type AdminGetUsersFilters = { limit: number; searchTerm: string; + adminsOnly: boolean; }; export type AdminSlackConfig = { diff --git a/frontend/src/layouts/OrganizationLayout/components/MinimizedOrgSidebar/MinimizedOrgSidebar.tsx b/frontend/src/layouts/OrganizationLayout/components/MinimizedOrgSidebar/MinimizedOrgSidebar.tsx index 4a255acad..c1c4297ca 100644 --- a/frontend/src/layouts/OrganizationLayout/components/MinimizedOrgSidebar/MinimizedOrgSidebar.tsx +++ b/frontend/src/layouts/OrganizationLayout/components/MinimizedOrgSidebar/MinimizedOrgSidebar.tsx @@ -50,6 +50,7 @@ import { ProjectType } from "@app/hooks/api/workspace/types"; import { navigateUserToOrg } from "@app/pages/auth/LoginPage/Login.utils"; import { MenuIconButton } from "../MenuIconButton"; +import { ServerAdminsPanel } from "../ServerAdminsPanel/ServerAdminsPanel"; const getPlan = (subscription: SubscriptionPlan) => { if (subscription.dynamicSecret) return "Enterprise Plan"; @@ -89,6 +90,7 @@ export const MinimizedOrgSidebar = () => { const [openSupport, setOpenSupport] = useState(false); const [openUser, setOpenUser] = useState(false); const [openOrg, setOpenOrg] = useState(false); + const [openAdmins, setOpenAdmins] = useState(false); const { user } = useUser(); const { mutateAsync } = useGetOrgTrialUrl(); @@ -392,6 +394,31 @@ export const MinimizedOrgSidebar = () => { : "mb-4" } flex w-full cursor-default flex-col items-center px-1 text-sm text-mineshaft-400`} > + + setOpenAdmins(true)} + onMouseLeave={() => setOpenAdmins(false)} + asChild + > +
+ + Admins + +
+
+ setOpenAdmins(true)} + onMouseLeave={() => setOpenAdmins(false)} + align="start" + side="right" + className="mb-2 w-[60vh] p-1" + > + Server Administrators +
+ +
+
+
setOpenSupport(true)} diff --git a/frontend/src/layouts/OrganizationLayout/components/ServerAdminsPanel/ServerAdminsPanel.tsx b/frontend/src/layouts/OrganizationLayout/components/ServerAdminsPanel/ServerAdminsPanel.tsx new file mode 100644 index 000000000..69eecec40 --- /dev/null +++ b/frontend/src/layouts/OrganizationLayout/components/ServerAdminsPanel/ServerAdminsPanel.tsx @@ -0,0 +1,85 @@ +import { useState } from "react"; +import { faMagnifyingGlass } from "@fortawesome/free-solid-svg-icons"; +import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; + +import { + Input, + Table, + TableContainer, + TableSkeleton, + TBody, + Td, + Th, + THead, + Tr +} from "@app/components/v2"; +import { useOrganization } from "@app/context"; +import { useDebounce } from "@app/hooks"; +import { useGetOrgUsers } from "@app/hooks/api"; + +export const ServerAdminsPanel = () => { + const [searchUserFilter, setSearchUserFilter] = useState(""); + const [debounedSearchTerm] = useDebounce(searchUserFilter, 500); + const { currentOrg } = useOrganization(); + + const { data: orgUsers, isPending } = useGetOrgUsers(currentOrg?.id || ""); + + const adminUsers = orgUsers?.filter((orgUser) => { + const isSuperAdmin = orgUser.user.superAdmin; + const matchesSearch = debounedSearchTerm + ? orgUser.user.email?.toLowerCase().includes(debounedSearchTerm.toLowerCase()) || + orgUser.user.firstName?.toLowerCase().includes(debounedSearchTerm.toLowerCase()) || + orgUser.user.lastName?.toLowerCase().includes(debounedSearchTerm.toLowerCase()) + : true; + return isSuperAdmin && matchesSearch; + }); + + const isEmpty = !isPending && (!adminUsers || adminUsers.length === 0); + + return ( +
+
+ setSearchUserFilter(e.target.value)} + leftIcon={} + placeholder="Search server admins..." + className="w-full" + /> +
+
+ + + + + + + + + + {isPending && } + {!isPending && + adminUsers?.map(({ user }) => { + const name = + user.firstName || user.lastName + ? `${user.firstName} ${user.lastName}` + : user.username; + return ( + + + + + ); + })} + +
NameEmail
{name}{user.email}
+ {isEmpty && ( +
+ No server administrators found +
+ )} +
+
+
+ ); +}; diff --git a/frontend/src/pages/admin/OverviewPage/components/UserPanel.tsx b/frontend/src/pages/admin/OverviewPage/components/UserPanel.tsx index df51216a6..9f6524cf7 100644 --- a/frontend/src/pages/admin/OverviewPage/components/UserPanel.tsx +++ b/frontend/src/pages/admin/OverviewPage/components/UserPanel.tsx @@ -1,6 +1,13 @@ import { useState } from "react"; -import { faMagnifyingGlass, faUsers, faEllipsis } from "@fortawesome/free-solid-svg-icons"; +import { + faCheckCircle, + faEllipsis, + faFilter, + faMagnifyingGlass, + faUsers +} from "@fortawesome/free-solid-svg-icons"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; +import { twMerge } from "tailwind-merge"; import { UpgradePlanModal } from "@app/components/license/UpgradePlanModal"; import { createNotification } from "@app/components/notifications"; @@ -8,8 +15,15 @@ import { Badge, Button, DeleteActionModal, + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuLabel, + DropdownMenuTrigger, EmptyState, + IconButton, Input, + Switch, Table, TableContainer, TableSkeleton, @@ -17,11 +31,7 @@ import { Td, Th, THead, - Tr, - DropdownMenu, - DropdownMenuContent, - DropdownMenuItem, - DropdownMenuTrigger + Tr } from "@app/components/v2"; import { useSubscription, useUser } from "@app/context"; import { useDebounce, usePopUp } from "@app/hooks"; @@ -48,6 +58,7 @@ const UserPanelTable = ({ ) => void; }) => { const [searchUserFilter, setSearchUserFilter] = useState(""); + const [adminsOnly, setAdminsOnly] = useState(false); const { user } = useUser(); const userId = user?.id || ""; const [debounedSearchTerm] = useDebounce(searchUserFilter, 500); @@ -55,18 +66,60 @@ const UserPanelTable = ({ const { data, isPending, isFetchingNextPage, hasNextPage, fetchNextPage } = useAdminGetUsers({ limit: 20, - searchTerm: debounedSearchTerm + searchTerm: debounedSearchTerm, + adminsOnly }); const isEmpty = !isPending && !data?.pages?.[0].length; + const isTableFiltered = Boolean(adminsOnly); + return ( <> - setSearchUserFilter(e.target.value)} - leftIcon={} - placeholder="Search users..." - /> +
+ setSearchUserFilter(e.target.value)} + leftIcon={} + placeholder="Search users..." + className="flex-1" + /> + + + + + + + + Filter Users + { + e.preventDefault(); + setAdminsOnly(!adminsOnly); + }} + icon={adminsOnly && } + iconPos="right" + > +
+ Admins Only + setAdminsOnly(checked)} + className="data-[state=checked]:bg-primary-400" + /> +
+
+
+
+
From 67f2e4671a61a759af0a4b77b6cf8423b5b49822 Mon Sep 17 00:00:00 2001 From: Daniel Hougaard Date: Fri, 7 Mar 2025 19:59:29 +0400 Subject: [PATCH 06/71] requested changes --- .../services/auth/auth-password-service.ts | 48 +++++++++---------- .../auth/VerifyEmailPage/VerifyEmailPage.tsx | 6 ++- 2 files changed, 26 insertions(+), 28 deletions(-) diff --git a/backend/src/services/auth/auth-password-service.ts b/backend/src/services/auth/auth-password-service.ts index 4d01870f6..27d3bdf2d 100644 --- a/backend/src/services/auth/auth-password-service.ts +++ b/backend/src/services/auth/auth-password-service.ts @@ -119,35 +119,31 @@ export const authPaswordServiceFactory = ({ * Email password reset flow via email. Step 1 send email */ const sendPasswordResetEmail = async (email: string) => { - const startTime = Date.now(); + const sendEmail = async () => { + const user = await userDAL.findUserByUsername(email); - const user = await userDAL.findUserByUsername(email); + if (user && user.isAccepted) { + const cfg = getConfig(); + const token = await tokenService.createTokenForUser({ + type: TokenType.TOKEN_EMAIL_PASSWORD_RESET, + userId: user.id + }); - if (user && user.isAccepted) { - const cfg = getConfig(); - const token = await tokenService.createTokenForUser({ - type: TokenType.TOKEN_EMAIL_PASSWORD_RESET, - userId: user.id - }); + await smtpService.sendMail({ + template: SmtpTemplates.ResetPassword, + recipients: [email], + subjectLine: "Infisical password reset", + substitutions: { + email, + token, + callback_url: cfg.SITE_URL ? `${cfg.SITE_URL}/password-reset` : "" + } + }); + } + }; - await smtpService.sendMail({ - template: SmtpTemplates.ResetPassword, - recipients: [email], - subjectLine: "Infisical password reset", - substitutions: { - email, - token, - callback_url: cfg.SITE_URL ? `${cfg.SITE_URL}/password-reset` : "" - } - }); - } - - const elapsedTime = Date.now() - startTime; - // daniel: ensure each request takes 8 seconds to prevent timing attacks - if (elapsedTime < 8_000) { - // eslint-disable-next-line no-promise-executor-return - await new Promise((resolve) => setTimeout(resolve, 8_000 - elapsedTime)); - } + // note(daniel): run in background to prevent timing attacks + void sendEmail(); }; /* diff --git a/frontend/src/pages/auth/VerifyEmailPage/VerifyEmailPage.tsx b/frontend/src/pages/auth/VerifyEmailPage/VerifyEmailPage.tsx index bc0bdf1ff..47012e2cb 100644 --- a/frontend/src/pages/auth/VerifyEmailPage/VerifyEmailPage.tsx +++ b/frontend/src/pages/auth/VerifyEmailPage/VerifyEmailPage.tsx @@ -103,8 +103,10 @@ export const VerifyEmailPage = () => { Look for an email in your inbox.

-

- An email with instructions has been sent to {email}. +

+ If the email is in our system, you will receive an email at{" "} + {email} with instructions on how to reset your + password.

From 795d9e44139348e11cca7ce171752d66f00c784c Mon Sep 17 00:00:00 2001 From: Daniel Hougaard Date: Fri, 7 Mar 2025 20:15:30 +0400 Subject: [PATCH 07/71] Update auth-password-service.ts --- backend/src/services/auth/auth-password-service.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/backend/src/services/auth/auth-password-service.ts b/backend/src/services/auth/auth-password-service.ts index 27d3bdf2d..f5f42a236 100644 --- a/backend/src/services/auth/auth-password-service.ts +++ b/backend/src/services/auth/auth-password-service.ts @@ -25,6 +25,7 @@ import { TSetupPasswordViaBackupKeyDTO } from "./auth-password-type"; import { ActorType, AuthMethod, AuthTokenType } from "./auth-type"; +import { logger } from "@app/lib/logger"; type TAuthPasswordServiceFactoryDep = { authDAL: TAuthDALFactory; @@ -143,7 +144,7 @@ export const authPaswordServiceFactory = ({ }; // note(daniel): run in background to prevent timing attacks - void sendEmail(); + void sendEmail().catch((err) => logger.error(err, "Failed to send password reset email")); }; /* From 1dea0248809e158e051c8cdb0a0c223bba0641ae Mon Sep 17 00:00:00 2001 From: carlosmonastyrski Date: Fri, 7 Mar 2025 19:19:55 -0300 Subject: [PATCH 08/71] Improvement on admin visibility UI components --- .../MinimizedOrgSidebar.tsx | 80 ++++++++++--------- .../ServerAdminsPanel/ServerAdminsPanel.tsx | 18 ++--- .../OverviewPage/components/UserPanel.tsx | 21 ++--- 3 files changed, 60 insertions(+), 59 deletions(-) diff --git a/frontend/src/layouts/OrganizationLayout/components/MinimizedOrgSidebar/MinimizedOrgSidebar.tsx b/frontend/src/layouts/OrganizationLayout/components/MinimizedOrgSidebar/MinimizedOrgSidebar.tsx index c1c4297ca..95e254ccb 100644 --- a/frontend/src/layouts/OrganizationLayout/components/MinimizedOrgSidebar/MinimizedOrgSidebar.tsx +++ b/frontend/src/layouts/OrganizationLayout/components/MinimizedOrgSidebar/MinimizedOrgSidebar.tsx @@ -30,6 +30,8 @@ import { DropdownMenuItem, DropdownMenuLabel, DropdownMenuTrigger, + Modal, + ModalContent, Tooltip } from "@app/components/v2"; import { envConfig } from "@app/config/env"; @@ -78,6 +80,11 @@ export const INFISICAL_SUPPORT_OPTIONS = [ , "Email Support", "mailto:support@infisical.com" + ], + [ + , + "Instance Admins", + "server-admins" ] ]; @@ -90,7 +97,7 @@ export const MinimizedOrgSidebar = () => { const [openSupport, setOpenSupport] = useState(false); const [openUser, setOpenUser] = useState(false); const [openOrg, setOpenOrg] = useState(false); - const [openAdmins, setOpenAdmins] = useState(false); + const [showAdminsModal, setShowAdminsModal] = useState(false); const { user } = useUser(); const { mutateAsync } = useGetOrgTrialUrl(); @@ -394,31 +401,6 @@ export const MinimizedOrgSidebar = () => { : "mb-4" } flex w-full cursor-default flex-col items-center px-1 text-sm text-mineshaft-400`} > - - setOpenAdmins(true)} - onMouseLeave={() => setOpenAdmins(false)} - asChild - > -
- - Admins - -
-
- setOpenAdmins(true)} - onMouseLeave={() => setOpenAdmins(false)} - align="start" - side="right" - className="mb-2 w-[60vh] p-1" - > - Server Administrators -
- -
-
-
setOpenSupport(true)} @@ -439,17 +421,30 @@ export const MinimizedOrgSidebar = () => { > {INFISICAL_SUPPORT_OPTIONS.map(([icon, text, url]) => ( - -
- {icon} -
{text}
-
-
+ {url === "server-admins" ? ( + + ) : ( + +
+ {icon} +
{text}
+
+
+ )}
))} {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" />
-
- -
- +
+ +
+ - + {isPending && } {!isPending && adminUsers?.map(({ user }) => { @@ -65,9 +65,9 @@ export const ServerAdminsPanel = () => { ? `${user.firstName} ${user.lastName}` : user.username; return ( - - - + + + ); })} diff --git a/frontend/src/pages/admin/OverviewPage/components/UserPanel.tsx b/frontend/src/pages/admin/OverviewPage/components/UserPanel.tsx index 9f6524cf7..6654439de 100644 --- a/frontend/src/pages/admin/OverviewPage/components/UserPanel.tsx +++ b/frontend/src/pages/admin/OverviewPage/components/UserPanel.tsx @@ -4,7 +4,8 @@ import { faEllipsis, faFilter, faMagnifyingGlass, - faUsers + faUsers, + faUserShield } from "@fortawesome/free-solid-svg-icons"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { twMerge } from "tailwind-merge"; @@ -23,7 +24,6 @@ import { EmptyState, IconButton, Input, - Switch, Table, TableContainer, TableSkeleton, @@ -97,24 +97,19 @@ const UserPanelTable = ({ - - Filter Users + + Filter By { e.preventDefault(); setAdminsOnly(!adminsOnly); }} - icon={adminsOnly && } + icon={adminsOnly && } iconPos="right" > -
- Admins Only - setAdminsOnly(checked)} - className="data-[state=checked]:bg-primary-400" - /> +
+ + Server Admins
From 2bd681d58f99a7c0e73ae1b8a9a281af01081a20 Mon Sep 17 00:00:00 2001 From: Maidul Islam Date: Sun, 9 Mar 2025 16:07:33 -0400 Subject: [PATCH 09/71] add system md service for gateway --- cli/packages/cmd/gateway.go | 93 +++++++++++++++++++++++++++++++++---- 1 file changed, 84 insertions(+), 9 deletions(-) diff --git a/cli/packages/cmd/gateway.go b/cli/packages/cmd/gateway.go index d796ffede..fbe2f4d5d 100644 --- a/cli/packages/cmd/gateway.go +++ b/cli/packages/cmd/gateway.go @@ -1,29 +1,94 @@ package cmd import ( - // "fmt" - - // "github.com/Infisical/infisical-merge/packages/api" - // "github.com/Infisical/infisical-merge/packages/models" "context" "fmt" "os" + "os/exec" "os/signal" + "path/filepath" + "runtime" "syscall" "time" "github.com/Infisical/infisical-merge/packages/gateway" "github.com/Infisical/infisical-merge/packages/util" "github.com/rs/zerolog/log" - - // "github.com/Infisical/infisical-merge/packages/visualize" - // "github.com/rs/zerolog/log" - - // "github.com/go-resty/resty/v2" "github.com/posthog/posthog-go" "github.com/spf13/cobra" ) +const systemdServiceTemplate = `[Unit] +Description=Infisical Gateway Service +After=network.target + +[Service] +Type=simple +EnvironmentFile=/etc/infisical/gateway.conf +ExecStart=/usr/local/bin/infisical gateway +Restart=on-failure +InaccessibleDirectories=/home +PrivateTmp=yes +LimitCORE=infinity +LimitNOFILE=1000000 +LimitNPROC=60000 +LimitRTPRIO=infinity +LimitRTTIME=7000000 + +[Install] +WantedBy=multi-user.target +` + +func installSystemdService(token string, domain string) error { + if runtime.GOOS != "linux" { + log.Info().Msg("Skipping systemd service installation - not on Linux") + return nil + } + + if os.Geteuid() != 0 { + log.Info().Msg("Skipping systemd service installation - not running as root/sudo") + return nil + } + + configDir := "/etc/infisical" + if err := os.MkdirAll(configDir, 0755); err != nil { + return fmt.Errorf("failed to create config directory: %v", err) + } + + configContent := fmt.Sprintf("INFISICAL_UNIVERSAL_AUTH_ACCESS_TOKEN=%s\n", token) + if domain != "" { + configContent += fmt.Sprintf("INFISICAL_API_URL=%s\n", domain) + } else { + configContent += "INFISICAL_API_URL=\n" + } + + configPath := filepath.Join(configDir, "gateway.conf") + if err := os.WriteFile(configPath, []byte(configContent), 0600); err != nil { + return fmt.Errorf("failed to write config file: %v", err) + } + + servicePath := "/etc/systemd/system/infisical-gateway.service" + if _, err := os.Stat(servicePath); err == nil { + log.Info().Msg("Systemd service file already exists") + return nil + } + + if err := os.WriteFile(servicePath, []byte(systemdServiceTemplate), 0644); err != nil { + return fmt.Errorf("failed to write systemd service file: %v", err) + } + + reloadCmd := exec.Command("systemctl", "daemon-reload") + if err := reloadCmd.Run(); err != nil { + return fmt.Errorf("failed to reload systemd: %v", err) + } + + log.Info().Msg("Successfully installed systemd service") + log.Info().Msg("To start the service, run: sudo systemctl start infisical-gateway") + log.Info().Msg("To enable the service on boot, run: sudo systemctl enable infisical-gateway") + + return nil +} + var gatewayCmd = &cobra.Command{ Example: `infisical gateway`, Short: "Used to infisical gateway", @@ -40,6 +105,16 @@ var gatewayCmd = &cobra.Command{ util.HandleError(fmt.Errorf("Token not found")) } + domain, err := cmd.Flags().GetString("domain") + if err != nil { + util.HandleError(err, "Unable to parse domain flag") + } + + // Try to install systemd service if possible + if err := installSystemdService(token.Token, domain); err != nil { + log.Warn().Msgf("Failed to install systemd service: %v", err) + } + Telemetry.CaptureEvent("cli-command:gateway", posthog.NewProperties().Set("version", util.CLI_VERSION)) sigCh := make(chan os.Signal, 1) From 004a8b71a20d5d599bda2302d5d0bf9eb0f127e2 Mon Sep 17 00:00:00 2001 From: = Date: Mon, 10 Mar 2025 16:03:51 +0530 Subject: [PATCH 10/71] feat: refactored the systemd service to seperate package file --- cli/packages/cmd/gateway.go | 78 +------------------------------ cli/packages/gateway/systemd.go | 82 +++++++++++++++++++++++++++++++++ 2 files changed, 84 insertions(+), 76 deletions(-) create mode 100644 cli/packages/gateway/systemd.go diff --git a/cli/packages/cmd/gateway.go b/cli/packages/cmd/gateway.go index fbe2f4d5d..d41c2c20e 100644 --- a/cli/packages/cmd/gateway.go +++ b/cli/packages/cmd/gateway.go @@ -4,91 +4,17 @@ import ( "context" "fmt" "os" - "os/exec" "os/signal" - "path/filepath" - "runtime" "syscall" "time" "github.com/Infisical/infisical-merge/packages/gateway" "github.com/Infisical/infisical-merge/packages/util" - "github.com/rs/zerolog/log" "github.com/posthog/posthog-go" + "github.com/rs/zerolog/log" "github.com/spf13/cobra" ) -const systemdServiceTemplate = `[Unit] -Description=Infisical Gateway Service -After=network.target - -[Service] -Type=simple -EnvironmentFile=/etc/infisical/gateway.conf -ExecStart=/usr/local/bin/infisical gateway -Restart=on-failure -InaccessibleDirectories=/home -PrivateTmp=yes -LimitCORE=infinity -LimitNOFILE=1000000 -LimitNPROC=60000 -LimitRTPRIO=infinity -LimitRTTIME=7000000 - -[Install] -WantedBy=multi-user.target -` - -func installSystemdService(token string, domain string) error { - if runtime.GOOS != "linux" { - log.Info().Msg("Skipping systemd service installation - not on Linux") - return nil - } - - if os.Geteuid() != 0 { - log.Info().Msg("Skipping systemd service installation - not running as root/sudo") - return nil - } - - configDir := "/etc/infisical" - if err := os.MkdirAll(configDir, 0755); err != nil { - return fmt.Errorf("failed to create config directory: %v", err) - } - - configContent := fmt.Sprintf("INFISICAL_UNIVERSAL_AUTH_ACCESS_TOKEN=%s\n", token) - if domain != "" { - configContent += fmt.Sprintf("INFISICAL_API_URL=%s\n", domain) - } else { - configContent += "INFISICAL_API_URL=\n" - } - - configPath := filepath.Join(configDir, "gateway.conf") - if err := os.WriteFile(configPath, []byte(configContent), 0600); err != nil { - return fmt.Errorf("failed to write config file: %v", err) - } - - servicePath := "/etc/systemd/system/infisical-gateway.service" - if _, err := os.Stat(servicePath); err == nil { - log.Info().Msg("Systemd service file already exists") - return nil - } - - if err := os.WriteFile(servicePath, []byte(systemdServiceTemplate), 0644); err != nil { - return fmt.Errorf("failed to write systemd service file: %v", err) - } - - reloadCmd := exec.Command("systemctl", "daemon-reload") - if err := reloadCmd.Run(); err != nil { - return fmt.Errorf("failed to reload systemd: %v", err) - } - - log.Info().Msg("Successfully installed systemd service") - log.Info().Msg("To start the service, run: sudo systemctl start infisical-gateway") - log.Info().Msg("To enable the service on boot, run: sudo systemctl enable infisical-gateway") - - return nil -} - var gatewayCmd = &cobra.Command{ Example: `infisical gateway`, Short: "Used to infisical gateway", @@ -111,7 +37,7 @@ var gatewayCmd = &cobra.Command{ } // Try to install systemd service if possible - if err := installSystemdService(token.Token, domain); err != nil { + if err := gateway.InstallGatewaySystemdService(token.Token, domain); err != nil { log.Warn().Msgf("Failed to install systemd service: %v", err) } diff --git a/cli/packages/gateway/systemd.go b/cli/packages/gateway/systemd.go new file mode 100644 index 000000000..8183f3535 --- /dev/null +++ b/cli/packages/gateway/systemd.go @@ -0,0 +1,82 @@ +package gateway + +import ( + "fmt" + "os" + "os/exec" + "path/filepath" + "runtime" + + "github.com/rs/zerolog/log" +) + +const systemdServiceTemplate = `[Unit] +Description=Infisical Gateway Service +After=network.target + +[Service] +Type=simple +EnvironmentFile=/etc/infisical/gateway.conf +ExecStart=/usr/local/bin/infisical gateway +Restart=on-failure +InaccessibleDirectories=/home +PrivateTmp=yes +LimitCORE=infinity +LimitNOFILE=1000000 +LimitNPROC=60000 +LimitRTPRIO=infinity +LimitRTTIME=7000000 + +[Install] +WantedBy=multi-user.target +` + +func InstallGatewaySystemdService(token string, domain string) error { + if runtime.GOOS != "linux" { + log.Info().Msg("Skipping systemd service installation - not on Linux") + return nil + } + + if os.Geteuid() != 0 { + log.Info().Msg("Skipping systemd service installation - not running as root/sudo") + return nil + } + + configDir := "/etc/infisical" + if err := os.MkdirAll(configDir, 0755); err != nil { + return fmt.Errorf("failed to create config directory: %v", err) + } + + configContent := fmt.Sprintf("INFISICAL_UNIVERSAL_AUTH_ACCESS_TOKEN=%s\n", token) + if domain != "" { + configContent += fmt.Sprintf("INFISICAL_API_URL=%s\n", domain) + } else { + configContent += "INFISICAL_API_URL=\n" + } + + configPath := filepath.Join(configDir, "gateway.conf") + if err := os.WriteFile(configPath, []byte(configContent), 0600); err != nil { + return fmt.Errorf("failed to write config file: %v", err) + } + + servicePath := "/etc/systemd/system/infisical-gateway.service" + if _, err := os.Stat(servicePath); err == nil { + log.Info().Msg("Systemd service file already exists") + return nil + } + + if err := os.WriteFile(servicePath, []byte(systemdServiceTemplate), 0644); err != nil { + return fmt.Errorf("failed to write systemd service file: %v", err) + } + + reloadCmd := exec.Command("systemctl", "daemon-reload") + if err := reloadCmd.Run(); err != nil { + return fmt.Errorf("failed to reload systemd: %v", err) + } + + log.Info().Msg("Successfully installed systemd service") + log.Info().Msg("To start the service, run: sudo systemctl start infisical-gateway") + log.Info().Msg("To enable the service on boot, run: sudo systemctl enable infisical-gateway") + + return nil +} From 66817a40db59f55a2c33e4782e984b380c712e69 Mon Sep 17 00:00:00 2001 From: carlosmonastyrski Date: Mon, 10 Mar 2025 08:31:19 -0300 Subject: [PATCH 11/71] Adjust modal width to match the rest of the modals --- .../MinimizedOrgSidebar/MinimizedOrgSidebar.tsx | 8 ++------ .../components/ServerAdminsPanel/ServerAdminsPanel.tsx | 2 +- 2 files changed, 3 insertions(+), 7 deletions(-) diff --git a/frontend/src/layouts/OrganizationLayout/components/MinimizedOrgSidebar/MinimizedOrgSidebar.tsx b/frontend/src/layouts/OrganizationLayout/components/MinimizedOrgSidebar/MinimizedOrgSidebar.tsx index 95e254ccb..453a080ad 100644 --- a/frontend/src/layouts/OrganizationLayout/components/MinimizedOrgSidebar/MinimizedOrgSidebar.tsx +++ b/frontend/src/layouts/OrganizationLayout/components/MinimizedOrgSidebar/MinimizedOrgSidebar.tsx @@ -563,12 +563,8 @@ export const MinimizedOrgSidebar = () => { - -
+ +
diff --git a/frontend/src/layouts/OrganizationLayout/components/ServerAdminsPanel/ServerAdminsPanel.tsx b/frontend/src/layouts/OrganizationLayout/components/ServerAdminsPanel/ServerAdminsPanel.tsx index fed2a5a8b..c834daad8 100644 --- a/frontend/src/layouts/OrganizationLayout/components/ServerAdminsPanel/ServerAdminsPanel.tsx +++ b/frontend/src/layouts/OrganizationLayout/components/ServerAdminsPanel/ServerAdminsPanel.tsx @@ -48,7 +48,7 @@ export const ServerAdminsPanel = () => { />
- +
Name Email
{name}{user.email}
{name}{user.email}
From a814f459aba5776279a5655f249c5474f042bc2e Mon Sep 17 00:00:00 2001 From: carlosmonastyrski Date: Mon, 10 Mar 2025 10:58:39 -0300 Subject: [PATCH 12/71] Add condition to hide Instance Admins on cloud instances --- .../MinimizedOrgSidebar.tsx | 62 ++++++++++--------- 1 file changed, 34 insertions(+), 28 deletions(-) diff --git a/frontend/src/layouts/OrganizationLayout/components/MinimizedOrgSidebar/MinimizedOrgSidebar.tsx b/frontend/src/layouts/OrganizationLayout/components/MinimizedOrgSidebar/MinimizedOrgSidebar.tsx index 453a080ad..3cc104f48 100644 --- a/frontend/src/layouts/OrganizationLayout/components/MinimizedOrgSidebar/MinimizedOrgSidebar.tsx +++ b/frontend/src/layouts/OrganizationLayout/components/MinimizedOrgSidebar/MinimizedOrgSidebar.tsx @@ -36,6 +36,7 @@ import { } from "@app/components/v2"; import { envConfig } from "@app/config/env"; import { useOrganization, useSubscription, useUser } from "@app/context"; +import { isInfisicalCloud } from "@app/helpers/platform"; import { usePopUp, useToggle } from "@app/hooks"; import { useGetOrganizations, @@ -419,34 +420,39 @@ export const MinimizedOrgSidebar = () => { side="right" className="p-1" > - {INFISICAL_SUPPORT_OPTIONS.map(([icon, text, url]) => ( - - {url === "server-admins" ? ( - - ) : ( - -
- {icon} -
{text}
-
-
- )} -
- ))} + {INFISICAL_SUPPORT_OPTIONS.map(([icon, text, url]) => { + if (url === "server-admins" && isInfisicalCloud()) { + return null; + } + return ( + + {url === "server-admins" ? ( + + ) : ( + +
+ {icon} +
{text}
+
+
+ )} +
+ ); + })} {envConfig.PLATFORM_VERSION && (
From 0057404562695da90e8e9d3eaa2cf96ce44e8b50 Mon Sep 17 00:00:00 2001 From: = Date: Mon, 10 Mar 2025 22:22:58 +0530 Subject: [PATCH 13/71] feat: removed ca pool from dialing --- cli/packages/gateway/gateway.go | 4 ---- 1 file changed, 4 deletions(-) diff --git a/cli/packages/gateway/gateway.go b/cli/packages/gateway/gateway.go index bcf977d6d..772da8c40 100644 --- a/cli/packages/gateway/gateway.go +++ b/cli/packages/gateway/gateway.go @@ -79,13 +79,9 @@ func (g *Gateway) ConnectWithRelay() error { // Dial TURN Server if relayPort == "5349" { - caCertPool := x509.NewCertPool() - caCertPool.AppendCertsFromPEM([]byte(g.config.CertificateChain)) - log.Info().Msgf("Provided relay port %s. Using TLS", relayPort) conn, err := dtls.Dial("udp", turnAddr, &dtls.Config{ ServerName: relayAddress, - RootCAs: caCertPool, }) if err != nil { return fmt.Errorf("Failed to connect with relay server: %w", err) From 9458c8b04f8a5a90f0a844f7d81e74c410ea87c0 Mon Sep 17 00:00:00 2001 From: Daniel Hougaard Date: Mon, 10 Mar 2025 22:15:30 +0400 Subject: [PATCH 14/71] Update auth-fns.ts --- backend/src/services/auth/auth-fns.ts | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/backend/src/services/auth/auth-fns.ts b/backend/src/services/auth/auth-fns.ts index c72f6c6a7..ec6e0a303 100644 --- a/backend/src/services/auth/auth-fns.ts +++ b/backend/src/services/auth/auth-fns.ts @@ -66,6 +66,12 @@ export const validatePasswordResetAuthorization = (token?: string) => { const decodedToken = jwt.verify(AUTH_TOKEN_VALUE, appCfg.AUTH_SECRET) as AuthModeProviderSignUpTokenPayload; + if (decodedToken.authTokenType !== AuthTokenType.SIGNUP_TOKEN) { + throw new UnauthorizedError({ + message: `The provided authentication token type is not supported.` + }); + } + return decodedToken; }; From 29e39b558bbbacfb2ecb13ab7ffc9576aaa6e67f Mon Sep 17 00:00:00 2001 From: = Date: Mon, 10 Mar 2025 23:59:17 +0530 Subject: [PATCH 15/71] feat: changed to permission check --- cli/packages/gateway/gateway.go | 15 +++------------ 1 file changed, 3 insertions(+), 12 deletions(-) diff --git a/cli/packages/gateway/gateway.go b/cli/packages/gateway/gateway.go index 772da8c40..7a5403a12 100644 --- a/cli/packages/gateway/gateway.go +++ b/cli/packages/gateway/gateway.go @@ -189,7 +189,7 @@ func (g *Gateway) Listen(ctx context.Context) error { log.Printf("Listener started on %s", quicListener.Addr()) - g.registerRelayIsActive(ctx, quicListener.Addr().String(), errCh) + g.registerRelayIsActive(ctx, errCh) log.Info().Msg("Gateway started successfully") @@ -326,7 +326,7 @@ func (g *Gateway) createPermissionForStaticIps(staticIps string) error { return nil } -func (g *Gateway) registerRelayIsActive(ctx context.Context, addr string, errCh chan error) error { +func (g *Gateway) registerRelayIsActive(ctx context.Context, errCh chan error) error { ticker := time.NewTicker(15 * time.Second) maxFailures := 3 failures := 0 @@ -341,13 +341,7 @@ func (g *Gateway) registerRelayIsActive(ctx context.Context, addr string, errCh return case <-ticker.C: log.Debug().Msg("Performing relay connection health check") - ctxTimeout, cancel := context.WithTimeout(ctx, 5*time.Second) - defer cancel() - // Try to establish a QUIC connection - conn, err := quic.DialAddr(ctxTimeout, addr, &tls.Config{ - InsecureSkipVerify: false, // Skip certificate verification - NextProtos: []string{"infisical-gateway"}, - }, nil) + err := g.createPermissionForStaticIps(g.config.InfisicalStaticIp) if err != nil && !strings.Contains(err.Error(), "tls:") { failures++ log.Warn().Err(err).Int("failures", failures).Msg("Failed to refresh TURN permissions") @@ -357,9 +351,6 @@ func (g *Gateway) registerRelayIsActive(ctx context.Context, addr string, errCh } continue } - if conn != nil { - defer conn.CloseWithError(0, "All good") - } } } }() From 97567d06d4b0ae0e0e2be036f49c3a8510beb609 Mon Sep 17 00:00:00 2001 From: Daniel Hougaard <62331820+DanielHougaard@users.noreply.github.com> Date: Wed, 5 Mar 2025 23:47:40 +0400 Subject: [PATCH 16/71] Revert "Revert "feat(api/secrets): view secret value permission"" --- .../e2e-test/routes/v2/service-token.spec.ts | 10 +- backend/e2e-test/vitest-environment-knex.ts | 1 - ...kfill-secret-permissions-with-readvalue.ts | 313 +++++++++ .../v1/secret-approval-request-router.ts | 21 +- backend/src/ee/routes/v1/secret-router.ts | 4 +- .../src/ee/routes/v1/secret-version-router.ts | 7 +- backend/src/ee/routes/v1/snapshot-router.ts | 12 +- .../services/permission/project-permission.ts | 60 +- .../secret-approval-request-service.ts | 20 +- .../secret-replication-service.ts | 1 + .../secret-rotation-service.ts | 8 +- .../secret-snapshot-service.ts | 144 ++-- backend/src/lib/api-docs/constants.ts | 3 + backend/src/lib/errors/index.ts | 1 + backend/src/server/routes/sanitizedSchemas.ts | 9 + .../src/server/routes/v1/dashboard-router.ts | 90 +-- .../server/routes/v2/service-token-router.ts | 2 +- backend/src/server/routes/v3/secret-router.ts | 148 ++-- .../external-migration-fns.ts | 4 +- .../external-migration-queue.ts | 4 +- .../integration-delete-secret.ts | 3 +- .../integration/integration-service.ts | 10 +- .../src/services/project/project-service.ts | 11 +- .../secret-import/secret-import-fns.ts | 29 +- .../secret-import/secret-import-service.ts | 22 +- .../services/secret-sync/secret-sync-queue.ts | 3 +- .../secret-sync/secret-sync-service.ts | 6 +- .../src/services/secret-tag/secret-tag-dal.ts | 1 + .../secret-v2-bridge/secret-v2-bridge-fns.ts | 44 +- .../secret-v2-bridge-service.ts | 654 +++++++++++++----- .../secret-v2-bridge-types.ts | 18 +- .../secret-v2-bridge/secret-version-dal.ts | 59 +- backend/src/services/secret/secret-dal.ts | 45 +- backend/src/services/secret/secret-fns.ts | 42 +- backend/src/services/secret/secret-queue.ts | 3 +- backend/src/services/secret/secret-service.ts | 276 ++++++-- backend/src/services/secret/secret-types.ts | 7 +- .../src/services/secret/secret-version-dal.ts | 49 +- .../service-token/service-token-service.ts | 8 +- .../service-token/service-token-types.ts | 2 +- .../SecretReferenceDetails.tsx | 45 +- frontend/src/components/v2/Blur/Blur.tsx | 22 + frontend/src/components/v2/Blur/index.tsx | 1 + .../InfisicalSecretInput.tsx | 1 + .../context/ProjectPermissionContext/types.ts | 10 +- frontend/src/hooks/api/dashboard/queries.tsx | 3 + frontend/src/hooks/api/dashboard/types.ts | 1 + .../api/secretApprovalRequest/queries.tsx | 1 + .../src/hooks/api/secretImports/queries.tsx | 2 + .../src/hooks/api/secretSnapshots/queries.tsx | 1 + frontend/src/hooks/api/secrets/queries.tsx | 24 +- frontend/src/hooks/api/secrets/types.ts | 5 + frontend/src/hooks/api/types.ts | 9 +- .../AddServiceTokenModal.tsx | 8 +- .../components/GeneralPermissionPolicies.tsx | 27 +- .../ProjectRoleModifySection.utils.tsx | 36 +- .../CreateSecretForm/CreateSecretForm.tsx | 3 +- .../SecretOverviewTableRow/SecretEditRow.tsx | 43 +- .../SecretNoAccessOverviewTableRow.tsx | 3 +- .../SecretOverviewTableRow.tsx | 9 +- .../SecretRenameRow.tsx | 12 +- .../components/QuickSearchSecretItem.tsx | 62 +- .../SelectionPanel/SelectionPanel.tsx | 5 +- .../MoveSecretsDialog/MoveSecretsDialog.tsx | 5 +- .../SecretDashboardPage.tsx | 13 +- .../components/ActionBar/ActionBar.tsx | 103 +-- .../SecretListView/SecretDetailSidebar.tsx | 174 +++-- .../components/SecretListView/SecretItem.tsx | 24 +- .../SecretListView/SecretNoAccessListView.tsx | 9 +- .../components/SnapshotView/SecretItem.tsx | 19 +- 70 files changed, 2105 insertions(+), 729 deletions(-) create mode 100644 backend/src/db/migrations/20250218020306_backfill-secret-permissions-with-readvalue.ts create mode 100644 frontend/src/components/v2/Blur/Blur.tsx create mode 100644 frontend/src/components/v2/Blur/index.tsx diff --git a/backend/e2e-test/routes/v2/service-token.spec.ts b/backend/e2e-test/routes/v2/service-token.spec.ts index 6cc8f6e34..bfe043523 100644 --- a/backend/e2e-test/routes/v2/service-token.spec.ts +++ b/backend/e2e-test/routes/v2/service-token.spec.ts @@ -6,7 +6,7 @@ import { decryptAsymmetric, decryptSymmetric128BitHexKeyUTF8, encryptSymmetric12 const createServiceToken = async ( scopes: { environment: string; secretPath: string }[], - permissions: ("read" | "write")[] + permissions: ("read" | "write" | "readValue")[] ) => { const projectKeyRes = await testServer.inject({ method: "GET", @@ -139,7 +139,7 @@ describe("Service token secret ops", async () => { beforeAll(async () => { serviceToken = await createServiceToken( [{ secretPath: "/**", environment: seedData1.environment.slug }], - ["read", "write"] + ["read", "write", "readValue"] ); // this is ensure cli service token decryptiong working fine @@ -496,7 +496,7 @@ describe("Service token fail cases", async () => { test("Unauthorized secret path access", async () => { const serviceToken = await createServiceToken( [{ secretPath: "/", environment: seedData1.environment.slug }], - ["read", "write"] + ["read", "readValue", "write"] ); const fetchSecrets = await testServer.inject({ method: "GET", @@ -518,7 +518,7 @@ describe("Service token fail cases", async () => { test("Unauthorized secret environment access", async () => { const serviceToken = await createServiceToken( [{ secretPath: "/", environment: seedData1.environment.slug }], - ["read", "write"] + ["read", "readValue", "write"] ); const fetchSecrets = await testServer.inject({ method: "GET", @@ -540,7 +540,7 @@ describe("Service token fail cases", async () => { test("Unauthorized write operation", async () => { const serviceToken = await createServiceToken( [{ secretPath: "/", environment: seedData1.environment.slug }], - ["read"] + ["read", "readValue"] ); const writeSecrets = await testServer.inject({ method: "POST", diff --git a/backend/e2e-test/vitest-environment-knex.ts b/backend/e2e-test/vitest-environment-knex.ts index 9dfb38aeb..46b322349 100644 --- a/backend/e2e-test/vitest-environment-knex.ts +++ b/backend/e2e-test/vitest-environment-knex.ts @@ -120,4 +120,3 @@ export default { }; } }; - diff --git a/backend/src/db/migrations/20250218020306_backfill-secret-permissions-with-readvalue.ts b/backend/src/db/migrations/20250218020306_backfill-secret-permissions-with-readvalue.ts new file mode 100644 index 000000000..9e567cefe --- /dev/null +++ b/backend/src/db/migrations/20250218020306_backfill-secret-permissions-with-readvalue.ts @@ -0,0 +1,313 @@ +import { MongoAbility, RawRuleOf } from "@casl/ability"; +import { PackRule, packRules, unpackRules } from "@casl/ability/extra"; +import { Knex } from "knex"; +import { z } from "zod"; + +import { selectAllTableCols } from "@app/lib/knex"; + +import { TableName } from "../schemas"; + +enum ProjectPermissionSub { + Secrets = "secrets" +} + +enum SecretActions { + Read = "read", + ReadValue = "readValue" +} + +const UnpackedPermissionSchema = z.object({ + subject: z + .union([z.string().min(1), z.string().array()]) + .transform((el) => (typeof el !== "string" ? el[0] : el)) + .optional(), + action: z.union([z.string().min(1), z.string().array()]).transform((el) => (typeof el === "string" ? [el] : el)), + conditions: z.unknown().optional(), + inverted: z.boolean().optional() +}); + +const $unpackPermissions = (permissions: unknown) => + UnpackedPermissionSchema.array().parse(unpackRules((permissions || []) as PackRule>[])); + +const $updatePermissionsUp = (permissions: unknown) => { + const parsedPermissions = $unpackPermissions(permissions); + let shouldUpdate = false; + + for (let i = 0; i < parsedPermissions.length; i += 1) { + const parsedPermission = parsedPermissions[i]; + const { subject, action } = parsedPermission; + + if (subject === ProjectPermissionSub.Secrets) { + if (action.includes(SecretActions.Read) && !action.includes(SecretActions.ReadValue)) { + action.push(SecretActions.ReadValue); + parsedPermissions[i] = { ...parsedPermission, action }; + shouldUpdate = true; + } + } + } + + return { + parsedPermissions, + shouldUpdate + }; +}; + +const $updatePermissionsDown = (permissions: unknown) => { + const parsedPermissions = $unpackPermissions(permissions); + + let shouldUpdate = false; + for (let i = 0; i < parsedPermissions.length; i += 1) { + const parsedPermission = parsedPermissions[i]; + + const { subject, action } = parsedPermission; + + if (subject === ProjectPermissionSub.Secrets) { + const readValueIndex = action.indexOf(SecretActions.ReadValue); + + if (action.includes(SecretActions.ReadValue) && readValueIndex !== -1) { + action.splice(readValueIndex, 1); + parsedPermissions[i] = { ...parsedPermission, action }; + + shouldUpdate = true; + } + } + } + + const repackedPermissions = packRules(parsedPermissions); + + return { + repackedPermissions, + shouldUpdate + }; +}; + +const CHUNK_SIZE = 1000; + +export async function up(knex: Knex): Promise { + const projectRoles = await knex(TableName.ProjectRoles).select(selectAllTableCols(TableName.ProjectRoles)); + const projectIdentityAdditionalPrivileges = await knex(TableName.IdentityProjectAdditionalPrivilege).select( + selectAllTableCols(TableName.IdentityProjectAdditionalPrivilege) + ); + const projectUserAdditionalPrivileges = await knex(TableName.ProjectUserAdditionalPrivilege).select( + selectAllTableCols(TableName.ProjectUserAdditionalPrivilege) + ); + + const serviceTokens = await knex(TableName.ServiceToken).select(selectAllTableCols(TableName.ServiceToken)); + + const updatedServiceTokens = serviceTokens.reduce((acc, serviceToken) => { + const { permissions } = serviceToken; // Service tokens are special, and include an array of actions only. + + if (permissions.includes(SecretActions.Read) && !permissions.includes(SecretActions.ReadValue)) { + permissions.push(SecretActions.ReadValue); + acc.push({ + ...serviceToken, + permissions + }); + } + return acc; + }, []); + + if (updatedServiceTokens.length > 0) { + for (let i = 0; i < updatedServiceTokens.length; i += CHUNK_SIZE) { + const chunk = updatedServiceTokens.slice(i, i + CHUNK_SIZE); + + // eslint-disable-next-line no-await-in-loop + await knex(TableName.ServiceToken) + .whereIn( + "id", + chunk.map((t) => t.id) + ) + .update({ + // @ts-expect-error -- raw query + permissions: knex.raw( + `CASE id + ${chunk.map((t) => `WHEN '${t.id}' THEN ?::text[]`).join(" ")} + END`, + chunk.map((t) => t.permissions) + ) + }); + } + } + + const updatedRoles = projectRoles.reduce((acc, projectRole) => { + const { shouldUpdate, parsedPermissions } = $updatePermissionsUp(projectRole.permissions); + + if (shouldUpdate) { + acc.push({ + ...projectRole, + permissions: JSON.stringify(packRules(parsedPermissions)) + }); + } + return acc; + }, []); + + const updatedIdentityAdditionalPrivileges = projectIdentityAdditionalPrivileges.reduce< + typeof projectIdentityAdditionalPrivileges + >((acc, identityAdditionalPrivilege) => { + const { shouldUpdate, parsedPermissions } = $updatePermissionsUp(identityAdditionalPrivilege.permissions); + + if (shouldUpdate) { + acc.push({ + ...identityAdditionalPrivilege, + permissions: JSON.stringify(packRules(parsedPermissions)) + }); + } + return acc; + }, []); + + const updatedUserAdditionalPrivileges = projectUserAdditionalPrivileges.reduce< + typeof projectUserAdditionalPrivileges + >((acc, userAdditionalPrivilege) => { + const { shouldUpdate, parsedPermissions } = $updatePermissionsUp(userAdditionalPrivilege.permissions); + + if (shouldUpdate) { + acc.push({ + ...userAdditionalPrivilege, + permissions: JSON.stringify(packRules(parsedPermissions)) + }); + } + return acc; + }, []); + + if (updatedRoles.length > 0) { + for (let i = 0; i < updatedRoles.length; i += CHUNK_SIZE) { + const chunk = updatedRoles.slice(i, i + CHUNK_SIZE); + + // eslint-disable-next-line no-await-in-loop + await knex(TableName.ProjectRoles).insert(chunk).onConflict("id").merge(["permissions"]); + } + } + + if (updatedIdentityAdditionalPrivileges.length > 0) { + for (let i = 0; i < updatedIdentityAdditionalPrivileges.length; i += CHUNK_SIZE) { + const chunk = updatedIdentityAdditionalPrivileges.slice(i, i + CHUNK_SIZE); + + // eslint-disable-next-line no-await-in-loop + await knex(TableName.IdentityProjectAdditionalPrivilege).insert(chunk).onConflict("id").merge(["permissions"]); + } + } + + if (updatedUserAdditionalPrivileges.length > 0) { + for (let i = 0; i < updatedUserAdditionalPrivileges.length; i += CHUNK_SIZE) { + const chunk = updatedUserAdditionalPrivileges.slice(i, i + CHUNK_SIZE); + + // eslint-disable-next-line no-await-in-loop + await knex(TableName.ProjectUserAdditionalPrivilege).insert(chunk).onConflict("id").merge(["permissions"]); + } + } +} + +export async function down(knex: Knex): Promise { + const projectRoles = await knex(TableName.ProjectRoles).select(selectAllTableCols(TableName.ProjectRoles)); + const identityAdditionalPrivileges = await knex(TableName.IdentityProjectAdditionalPrivilege).select( + selectAllTableCols(TableName.IdentityProjectAdditionalPrivilege) + ); + const userAdditionalPrivileges = await knex(TableName.ProjectUserAdditionalPrivilege).select( + selectAllTableCols(TableName.ProjectUserAdditionalPrivilege) + ); + const serviceTokens = await knex(TableName.ServiceToken).select(selectAllTableCols(TableName.ServiceToken)); + + const updatedServiceTokens = serviceTokens.reduce((acc, serviceToken) => { + const { permissions } = serviceToken; + + if (permissions.includes(SecretActions.ReadValue)) { + permissions.splice(permissions.indexOf(SecretActions.ReadValue), 1); + acc.push({ + ...serviceToken, + permissions + }); + } + return acc; + }, []); + + if (updatedServiceTokens.length > 0) { + for (let i = 0; i < updatedServiceTokens.length; i += CHUNK_SIZE) { + const chunk = updatedServiceTokens.slice(i, i + CHUNK_SIZE); + + // eslint-disable-next-line no-await-in-loop + await knex(TableName.ServiceToken) + .whereIn( + "id", + chunk.map((t) => t.id) + ) + .update({ + // @ts-expect-error -- raw query + permissions: knex.raw( + `CASE id + ${chunk.map((t) => `WHEN '${t.id}' THEN ?::text[]`).join(" ")} + END`, + chunk.map((t) => t.permissions) + ) + }); + } + } + + const updatedRoles = projectRoles.reduce((acc, projectRole) => { + const { shouldUpdate, repackedPermissions } = $updatePermissionsDown(projectRole.permissions); + + if (shouldUpdate) { + acc.push({ + ...projectRole, + permissions: JSON.stringify(repackedPermissions) + }); + } + return acc; + }, []); + + const updatedIdentityAdditionalPrivileges = identityAdditionalPrivileges.reduce( + (acc, identityAdditionalPrivilege) => { + const { shouldUpdate, repackedPermissions } = $updatePermissionsDown(identityAdditionalPrivilege.permissions); + + if (shouldUpdate) { + acc.push({ + ...identityAdditionalPrivilege, + permissions: JSON.stringify(repackedPermissions) + }); + } + return acc; + }, + [] + ); + + const updatedUserAdditionalPrivileges = userAdditionalPrivileges.reduce( + (acc, userAdditionalPrivilege) => { + const { shouldUpdate, repackedPermissions } = $updatePermissionsDown(userAdditionalPrivilege.permissions); + + if (shouldUpdate) { + acc.push({ + ...userAdditionalPrivilege, + permissions: JSON.stringify(repackedPermissions) + }); + } + return acc; + }, + [] + ); + + if (updatedRoles.length > 0) { + for (let i = 0; i < updatedRoles.length; i += CHUNK_SIZE) { + const chunk = updatedRoles.slice(i, i + CHUNK_SIZE); + + // eslint-disable-next-line no-await-in-loop + await knex(TableName.ProjectRoles).insert(chunk).onConflict("id").merge(["permissions"]); + } + } + + if (updatedIdentityAdditionalPrivileges.length > 0) { + for (let i = 0; i < updatedIdentityAdditionalPrivileges.length; i += CHUNK_SIZE) { + const chunk = updatedIdentityAdditionalPrivileges.slice(i, i + CHUNK_SIZE); + + // eslint-disable-next-line no-await-in-loop + await knex(TableName.IdentityProjectAdditionalPrivilege).insert(chunk).onConflict("id").merge(["permissions"]); + } + } + + if (updatedUserAdditionalPrivileges.length > 0) { + for (let i = 0; i < updatedUserAdditionalPrivileges.length; i += CHUNK_SIZE) { + const chunk = updatedUserAdditionalPrivileges.slice(i, i + CHUNK_SIZE); + + // eslint-disable-next-line no-await-in-loop + await knex(TableName.ProjectUserAdditionalPrivilege).insert(chunk).onConflict("id").merge(["permissions"]); + } + } +} diff --git a/backend/src/ee/routes/v1/secret-approval-request-router.ts b/backend/src/ee/routes/v1/secret-approval-request-router.ts index 653d04d4f..1ceb6019b 100644 --- a/backend/src/ee/routes/v1/secret-approval-request-router.ts +++ b/backend/src/ee/routes/v1/secret-approval-request-router.ts @@ -1,16 +1,11 @@ import { z } from "zod"; -import { - SecretApprovalRequestsReviewersSchema, - SecretApprovalRequestsSchema, - SecretTagsSchema, - UsersSchema -} from "@app/db/schemas"; +import { SecretApprovalRequestsReviewersSchema, SecretApprovalRequestsSchema, UsersSchema } from "@app/db/schemas"; import { EventType } from "@app/ee/services/audit-log/audit-log-types"; import { ApprovalStatus, RequestState } from "@app/ee/services/secret-approval-request/secret-approval-request-types"; import { readLimit, writeLimit } from "@app/server/config/rateLimiter"; import { verifyAuth } from "@app/server/plugins/auth/verify-auth"; -import { secretRawSchema } from "@app/server/routes/sanitizedSchemas"; +import { SanitizedTagSchema, secretRawSchema } from "@app/server/routes/sanitizedSchemas"; import { AuthMode } from "@app/services/auth/auth-type"; import { ResourceMetadataSchema } from "@app/services/resource-metadata/resource-metadata-schema"; @@ -250,14 +245,6 @@ export const registerSecretApprovalRequestRouter = async (server: FastifyZodProv } }); - const tagSchema = SecretTagsSchema.pick({ - id: true, - slug: true, - color: true - }) - .array() - .optional(); - server.route({ method: "GET", url: "/:id", @@ -291,7 +278,7 @@ export const registerSecretApprovalRequestRouter = async (server: FastifyZodProv .omit({ _id: true, environment: true, workspace: true, type: true, version: true }) .extend({ op: z.string(), - tags: tagSchema, + tags: SanitizedTagSchema.array().optional(), secretMetadata: ResourceMetadataSchema.nullish(), secret: z .object({ @@ -310,7 +297,7 @@ export const registerSecretApprovalRequestRouter = async (server: FastifyZodProv secretKey: z.string(), secretValue: z.string().optional(), secretComment: z.string().optional(), - tags: tagSchema, + tags: SanitizedTagSchema.array().optional(), secretMetadata: ResourceMetadataSchema.nullish() }) .optional() diff --git a/backend/src/ee/routes/v1/secret-router.ts b/backend/src/ee/routes/v1/secret-router.ts index 4c249afe0..a964eb1b8 100644 --- a/backend/src/ee/routes/v1/secret-router.ts +++ b/backend/src/ee/routes/v1/secret-router.ts @@ -1,6 +1,6 @@ import z from "zod"; -import { ProjectPermissionActions } from "@app/ee/services/permission/project-permission"; +import { ProjectPermissionSecretActions } from "@app/ee/services/permission/project-permission"; import { RAW_SECRETS } from "@app/lib/api-docs"; import { removeTrailingSlash } from "@app/lib/fn"; import { readLimit } from "@app/server/config/rateLimiter"; @@ -9,7 +9,7 @@ import { AuthMode } from "@app/services/auth/auth-type"; const AccessListEntrySchema = z .object({ - allowedActions: z.nativeEnum(ProjectPermissionActions).array(), + allowedActions: z.nativeEnum(ProjectPermissionSecretActions).array(), id: z.string(), membershipId: z.string(), name: z.string() diff --git a/backend/src/ee/routes/v1/secret-version-router.ts b/backend/src/ee/routes/v1/secret-version-router.ts index 11443ebfe..a09a05c91 100644 --- a/backend/src/ee/routes/v1/secret-version-router.ts +++ b/backend/src/ee/routes/v1/secret-version-router.ts @@ -22,7 +22,11 @@ export const registerSecretVersionRouter = async (server: FastifyZodProvider) => }), response: { 200: z.object({ - secretVersions: secretRawSchema.array() + secretVersions: secretRawSchema + .extend({ + secretValueHidden: z.boolean() + }) + .array() }) } }, @@ -37,6 +41,7 @@ export const registerSecretVersionRouter = async (server: FastifyZodProvider) => offset: req.query.offset, secretId: req.params.secretId }); + return { secretVersions }; } }); diff --git a/backend/src/ee/routes/v1/snapshot-router.ts b/backend/src/ee/routes/v1/snapshot-router.ts index fc2d25712..283b9b31e 100644 --- a/backend/src/ee/routes/v1/snapshot-router.ts +++ b/backend/src/ee/routes/v1/snapshot-router.ts @@ -1,10 +1,10 @@ import { z } from "zod"; -import { SecretSnapshotsSchema, SecretTagsSchema } from "@app/db/schemas"; +import { SecretSnapshotsSchema } from "@app/db/schemas"; import { PROJECTS } from "@app/lib/api-docs"; import { readLimit, writeLimit } from "@app/server/config/rateLimiter"; import { verifyAuth } from "@app/server/plugins/auth/verify-auth"; -import { secretRawSchema } from "@app/server/routes/sanitizedSchemas"; +import { SanitizedTagSchema, secretRawSchema } from "@app/server/routes/sanitizedSchemas"; import { AuthMode } from "@app/services/auth/auth-type"; export const registerSnapshotRouter = async (server: FastifyZodProvider) => { @@ -31,12 +31,9 @@ export const registerSnapshotRouter = async (server: FastifyZodProvider) => { secretVersions: secretRawSchema .omit({ _id: true, environment: true, workspace: true, type: true }) .extend({ + secretValueHidden: z.boolean(), secretId: z.string(), - tags: SecretTagsSchema.pick({ - id: true, - slug: true, - color: true - }).array() + tags: SanitizedTagSchema.array() }) .array(), folderVersion: z.object({ id: z.string(), name: z.string() }).array(), @@ -55,6 +52,7 @@ export const registerSnapshotRouter = async (server: FastifyZodProvider) => { actorOrgId: req.permission.orgId, id: req.params.secretSnapshotId }); + return { secretSnapshot }; } }); diff --git a/backend/src/ee/services/permission/project-permission.ts b/backend/src/ee/services/permission/project-permission.ts index 4389c3866..3f69c481a 100644 --- a/backend/src/ee/services/permission/project-permission.ts +++ b/backend/src/ee/services/permission/project-permission.ts @@ -17,6 +17,14 @@ export enum ProjectPermissionActions { Delete = "delete" } +export enum ProjectPermissionSecretActions { + DescribeSecret = "read", + ReadValue = "readValue", + Create = "create", + Edit = "edit", + Delete = "delete" +} + export enum ProjectPermissionCmekActions { Read = "read", Create = "create", @@ -115,7 +123,7 @@ export type IdentityManagementSubjectFields = { export type ProjectPermissionSet = | [ - ProjectPermissionActions, + ProjectPermissionSecretActions, ProjectPermissionSub.Secrets | (ForcedSubject & SecretSubjectFields) ] | [ @@ -429,6 +437,7 @@ const GeneralPermissionSchema = [ }) ]; +// Do not update this schema anymore, as it's kept purely for backwards compatability. Update V2 schema only. export const ProjectPermissionV1Schema = z.discriminatedUnion("subject", [ z.object({ subject: z.literal(ProjectPermissionSub.Secrets).describe("The entity this permission pertains to."), @@ -460,7 +469,7 @@ export const ProjectPermissionV2Schema = z.discriminatedUnion("subject", [ z.object({ subject: z.literal(ProjectPermissionSub.Secrets).describe("The entity this permission pertains to."), inverted: z.boolean().optional().describe("Whether rule allows or forbids."), - action: CASL_ACTION_SCHEMA_NATIVE_ENUM(ProjectPermissionActions).describe( + action: CASL_ACTION_SCHEMA_NATIVE_ENUM(ProjectPermissionSecretActions).describe( "Describe what action an entity can take." ), conditions: SecretConditionV2Schema.describe( @@ -517,7 +526,6 @@ const buildAdminPermissionRules = () => { // Admins get full access to everything [ - ProjectPermissionSub.Secrets, ProjectPermissionSub.SecretFolders, ProjectPermissionSub.SecretImports, ProjectPermissionSub.SecretApproval, @@ -550,10 +558,21 @@ const buildAdminPermissionRules = () => { ProjectPermissionActions.Create, ProjectPermissionActions.Delete ], - el as ProjectPermissionSub + el ); }); + can( + [ + ProjectPermissionSecretActions.DescribeSecret, + ProjectPermissionSecretActions.ReadValue, + ProjectPermissionSecretActions.Create, + ProjectPermissionSecretActions.Edit, + ProjectPermissionSecretActions.Delete + ], + ProjectPermissionSub.Secrets + ); + can( [ ProjectPermissionDynamicSecretActions.ReadRootCredential, @@ -613,10 +632,11 @@ const buildMemberPermissionRules = () => { can( [ - ProjectPermissionActions.Read, - ProjectPermissionActions.Edit, - ProjectPermissionActions.Create, - ProjectPermissionActions.Delete + ProjectPermissionSecretActions.DescribeSecret, + ProjectPermissionSecretActions.ReadValue, + ProjectPermissionSecretActions.Edit, + ProjectPermissionSecretActions.Create, + ProjectPermissionSecretActions.Delete ], ProjectPermissionSub.Secrets ); @@ -788,7 +808,8 @@ export const projectMemberPermissions = buildMemberPermissionRules(); const buildViewerPermissionRules = () => { const { can, rules } = new AbilityBuilder>(createMongoAbility); - can(ProjectPermissionActions.Read, ProjectPermissionSub.Secrets); + can(ProjectPermissionSecretActions.DescribeSecret, ProjectPermissionSub.Secrets); + can(ProjectPermissionSecretActions.ReadValue, ProjectPermissionSub.Secrets); can(ProjectPermissionActions.Read, ProjectPermissionSub.SecretFolders); can(ProjectPermissionDynamicSecretActions.ReadRootCredential, ProjectPermissionSub.DynamicSecrets); can(ProjectPermissionActions.Read, ProjectPermissionSub.SecretImports); @@ -831,6 +852,8 @@ export const buildServiceTokenProjectPermission = ( ) => { const canWrite = permission.includes("write"); const canRead = permission.includes("read"); + const canReadValue = permission.includes("readValue"); + const { can, build } = new AbilityBuilder>(createMongoAbility); scopes.forEach(({ secretPath, environment }) => { [ProjectPermissionSub.Secrets, ProjectPermissionSub.SecretImports, ProjectPermissionSub.SecretFolders].forEach( @@ -860,6 +883,14 @@ export const buildServiceTokenProjectPermission = ( environment }); } + + if (subject === ProjectPermissionSub.Secrets && canReadValue) { + // @ts-expect-error type + can(ProjectPermissionSecretActions.ReadValue, subject as ProjectPermissionSub.Secrets, { + secretPath: { $glob: secretPath }, + environment + }); + } } ); }); @@ -916,7 +947,17 @@ export const backfillPermissionV1SchemaToV2Schema = ( subject: ProjectPermissionSub.SecretImports as const })); + const secretPolicies = secretSubjects.map(({ subject, ...el }) => ({ + subject: ProjectPermissionSub.Secrets as const, + ...el, + action: + el.action.includes(ProjectPermissionActions.Read) && !el.action.includes(ProjectPermissionSecretActions.ReadValue) + ? el.action.concat(ProjectPermissionSecretActions.ReadValue) + : el.action + })); + const secretFolderPolicies = secretSubjects + .map(({ subject, ...el }) => ({ ...el, // read permission is not needed anymore @@ -958,6 +999,7 @@ export const backfillPermissionV1SchemaToV2Schema = ( // eslint-disable-next-line @typescript-eslint/ban-ts-comment // @ts-ignore-error this is valid ts secretImportPolicies, + secretPolicies, dynamicSecretPolicies, hasReadOnlyFolder.length ? [] : secretFolderPolicies ); diff --git a/backend/src/ee/services/secret-approval-request/secret-approval-request-service.ts b/backend/src/ee/services/secret-approval-request/secret-approval-request-service.ts index d296d7975..8efae75e6 100644 --- a/backend/src/ee/services/secret-approval-request/secret-approval-request-service.ts +++ b/backend/src/ee/services/secret-approval-request/secret-approval-request-service.ts @@ -58,7 +58,7 @@ import { TUserDALFactory } from "@app/services/user/user-dal"; import { TLicenseServiceFactory } from "../license/license-service"; import { TPermissionServiceFactory } from "../permission/permission-service"; -import { ProjectPermissionActions, ProjectPermissionSub } from "../permission/project-permission"; +import { ProjectPermissionSecretActions, ProjectPermissionSub } from "../permission/project-permission"; import { TSecretApprovalPolicyDALFactory } from "../secret-approval-policy/secret-approval-policy-dal"; import { TSecretSnapshotServiceFactory } from "../secret-snapshot/secret-snapshot-service"; import { TSecretApprovalRequestDALFactory } from "./secret-approval-request-dal"; @@ -88,7 +88,12 @@ type TSecretApprovalRequestServiceFactoryDep = { secretDAL: TSecretDALFactory; secretTagDAL: Pick< TSecretTagDALFactory, - "findManyTagsById" | "saveTagsToSecret" | "deleteTagsManySecret" | "saveTagsToSecretV2" | "deleteTagsToSecretV2" + | "findManyTagsById" + | "saveTagsToSecret" + | "deleteTagsManySecret" + | "saveTagsToSecretV2" + | "deleteTagsToSecretV2" + | "find" >; secretBlindIndexDAL: Pick; snapshotService: Pick; @@ -106,7 +111,7 @@ type TSecretApprovalRequestServiceFactoryDep = { kmsService: Pick; secretV2BridgeDAL: Pick< TSecretV2BridgeDALFactory, - "insertMany" | "upsertSecretReferences" | "findBySecretKeys" | "bulkUpdate" | "deleteMany" + "insertMany" | "upsertSecretReferences" | "findBySecretKeys" | "bulkUpdate" | "deleteMany" | "find" >; secretVersionV2BridgeDAL: Pick; secretVersionTagV2BridgeDAL: Pick; @@ -913,7 +918,7 @@ export const secretApprovalRequestServiceFactory = ({ actionProjectType: ActionProjectType.SecretManager }); ForbiddenError.from(permission).throwUnlessCan( - ProjectPermissionActions.Read, + ProjectPermissionSecretActions.ReadValue, subject(ProjectPermissionSub.Secrets, { environment, secretPath }) ); @@ -1000,6 +1005,7 @@ export const secretApprovalRequestServiceFactory = ({ : keyName2BlindIndex[secretName]; // add tags if (tagIds?.length) commitTagIds[keyName2BlindIndex[secretName]] = tagIds; + return { ...latestSecretVersions[secretId], ...el, @@ -1363,9 +1369,9 @@ export const secretApprovalRequestServiceFactory = ({ const tagsGroupById = groupBy(tags, (i) => i.id); commits.forEach((commit) => { - let action = ProjectPermissionActions.Create; - if (commit.op === SecretOperations.Update) action = ProjectPermissionActions.Edit; - if (commit.op === SecretOperations.Delete) action = ProjectPermissionActions.Delete; + let action = ProjectPermissionSecretActions.Create; + if (commit.op === SecretOperations.Update) action = ProjectPermissionSecretActions.Edit; + if (commit.op === SecretOperations.Delete) action = ProjectPermissionSecretActions.Delete; ForbiddenError.from(permission).throwUnlessCan( action, diff --git a/backend/src/ee/services/secret-replication/secret-replication-service.ts b/backend/src/ee/services/secret-replication/secret-replication-service.ts index 3c25db98c..5fae2675d 100644 --- a/backend/src/ee/services/secret-replication/secret-replication-service.ts +++ b/backend/src/ee/services/secret-replication/secret-replication-service.ts @@ -265,6 +265,7 @@ export const secretReplicationServiceFactory = ({ folderDAL, secretImportDAL, decryptor: (value) => (value ? secretManagerDecryptor({ cipherTextBlob: value }).toString() : ""), + viewSecretValue: true, hasSecretAccess: () => true }); // secrets that gets replicated across imports diff --git a/backend/src/ee/services/secret-rotation/secret-rotation-service.ts b/backend/src/ee/services/secret-rotation/secret-rotation-service.ts index 02da4b7ea..8d458111a 100644 --- a/backend/src/ee/services/secret-rotation/secret-rotation-service.ts +++ b/backend/src/ee/services/secret-rotation/secret-rotation-service.ts @@ -15,7 +15,11 @@ import { TSecretV2BridgeDALFactory } from "@app/services/secret-v2-bridge/secret import { TLicenseServiceFactory } from "../license/license-service"; import { TPermissionServiceFactory } from "../permission/permission-service"; -import { ProjectPermissionActions, ProjectPermissionSub } from "../permission/project-permission"; +import { + ProjectPermissionActions, + ProjectPermissionSecretActions, + ProjectPermissionSub +} from "../permission/project-permission"; import { TSecretRotationDALFactory } from "./secret-rotation-dal"; import { TSecretRotationQueueFactory } from "./secret-rotation-queue"; import { TSecretRotationEncData } from "./secret-rotation-queue/secret-rotation-queue-types"; @@ -106,7 +110,7 @@ export const secretRotationServiceFactory = ({ }); } ForbiddenError.from(permission).throwUnlessCan( - ProjectPermissionActions.Edit, + ProjectPermissionSecretActions.Edit, subject(ProjectPermissionSub.Secrets, { environment, secretPath }) ); diff --git a/backend/src/ee/services/secret-snapshot/secret-snapshot-service.ts b/backend/src/ee/services/secret-snapshot/secret-snapshot-service.ts index dc2cff456..397824904 100644 --- a/backend/src/ee/services/secret-snapshot/secret-snapshot-service.ts +++ b/backend/src/ee/services/secret-snapshot/secret-snapshot-service.ts @@ -23,7 +23,11 @@ import { TSecretVersionV2TagDALFactory } from "@app/services/secret-v2-bridge/se import { TLicenseServiceFactory } from "../license/license-service"; import { TPermissionServiceFactory } from "../permission/permission-service"; -import { ProjectPermissionActions, ProjectPermissionSub } from "../permission/project-permission"; +import { + ProjectPermissionActions, + ProjectPermissionSecretActions, + ProjectPermissionSub +} from "../permission/project-permission"; import { TGetSnapshotDataDTO, TProjectSnapshotCountDTO, @@ -35,6 +39,7 @@ import { TSnapshotFolderDALFactory } from "./snapshot-folder-dal"; import { TSnapshotSecretDALFactory } from "./snapshot-secret-dal"; import { TSnapshotSecretV2DALFactory } from "./snapshot-secret-v2-dal"; import { getFullFolderPath } from "./snapshot-service-fns"; +import { INFISICAL_SECRET_VALUE_HIDDEN_MASK } from "@app/services/secret/secret-fns"; type TSecretSnapshotServiceFactoryDep = { snapshotDAL: TSnapshotDALFactory; @@ -98,7 +103,7 @@ export const secretSnapshotServiceFactory = ({ // We need to check if the user has access to the secrets in the folder. If we don't do this, a user could theoretically access snapshot secret values even if they don't have read access to the secrets in the folder. ForbiddenError.from(permission).throwUnlessCan( - ProjectPermissionActions.Read, + ProjectPermissionSecretActions.DescribeSecret, subject(ProjectPermissionSub.Secrets, { environment, secretPath: path }) ); @@ -135,7 +140,7 @@ export const secretSnapshotServiceFactory = ({ // We need to check if the user has access to the secrets in the folder. If we don't do this, a user could theoretically access snapshot secret values even if they don't have read access to the secrets in the folder. ForbiddenError.from(permission).throwUnlessCan( - ProjectPermissionActions.Read, + ProjectPermissionSecretActions.DescribeSecret, subject(ProjectPermissionSub.Secrets, { environment, secretPath: path }) ); @@ -162,6 +167,7 @@ export const secretSnapshotServiceFactory = ({ }); ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.SecretRollback); + const shouldUseBridge = snapshot.projectVersion === 3; let snapshotDetails; if (shouldUseBridge) { @@ -170,68 +176,110 @@ export const secretSnapshotServiceFactory = ({ projectId: snapshot.projectId }); const encryptedSnapshotDetails = await snapshotDAL.findSecretSnapshotV2DataById(id); + + const fullFolderPath = await getFullFolderPath({ + folderDAL, + folderId: encryptedSnapshotDetails.folderId, + envId: encryptedSnapshotDetails.environment.id + }); + snapshotDetails = { ...encryptedSnapshotDetails, - secretVersions: encryptedSnapshotDetails.secretVersions.map((el) => ({ - ...el, - secretKey: el.key, - secretValue: el.encryptedValue - ? secretManagerDecryptor({ cipherTextBlob: el.encryptedValue }).toString() - : "", - secretComment: el.encryptedComment - ? secretManagerDecryptor({ cipherTextBlob: el.encryptedComment }).toString() - : "" - })) + secretVersions: encryptedSnapshotDetails.secretVersions.map((el) => { + const canReadValue = permission.can( + ProjectPermissionSecretActions.ReadValue, + subject(ProjectPermissionSub.Secrets, { + environment: encryptedSnapshotDetails.environment.slug, + secretPath: fullFolderPath, + secretName: el.key, + secretTags: el.tags.length ? el.tags.map((tag) => tag.slug) : undefined + }) + ); + + let secretValue = ""; + if (canReadValue) { + secretValue = el.encryptedValue + ? secretManagerDecryptor({ cipherTextBlob: el.encryptedValue }).toString() + : ""; + } else { + secretValue = INFISICAL_SECRET_VALUE_HIDDEN_MASK; + } + + return { + ...el, + secretKey: el.key, + secretValueHidden: !canReadValue, + secretValue, + secretComment: el.encryptedComment + ? secretManagerDecryptor({ cipherTextBlob: el.encryptedComment }).toString() + : "" + }; + }) }; } else { const encryptedSnapshotDetails = await snapshotDAL.findSecretSnapshotDataById(id); + + const fullFolderPath = await getFullFolderPath({ + folderDAL, + folderId: encryptedSnapshotDetails.folderId, + envId: encryptedSnapshotDetails.environment.id + }); + const { botKey } = await projectBotService.getBotKey(snapshot.projectId); if (!botKey) throw new NotFoundError({ message: `Project bot key not found for project with ID '${snapshot.projectId}'` }); snapshotDetails = { ...encryptedSnapshotDetails, - secretVersions: encryptedSnapshotDetails.secretVersions.map((el) => ({ - ...el, - secretKey: decryptSymmetric128BitHexKeyUTF8({ + secretVersions: encryptedSnapshotDetails.secretVersions.map((el) => { + const secretKey = decryptSymmetric128BitHexKeyUTF8({ ciphertext: el.secretKeyCiphertext, iv: el.secretKeyIV, tag: el.secretKeyTag, key: botKey - }), - secretValue: decryptSymmetric128BitHexKeyUTF8({ - ciphertext: el.secretValueCiphertext, - iv: el.secretValueIV, - tag: el.secretValueTag, - key: botKey - }), - secretComment: - el.secretCommentTag && el.secretCommentIV && el.secretCommentCiphertext - ? decryptSymmetric128BitHexKeyUTF8({ - ciphertext: el.secretCommentCiphertext, - iv: el.secretCommentIV, - tag: el.secretCommentTag, - key: botKey - }) - : "" - })) + }); + + const canReadValue = permission.can( + ProjectPermissionSecretActions.ReadValue, + subject(ProjectPermissionSub.Secrets, { + environment: encryptedSnapshotDetails.environment.slug, + secretPath: fullFolderPath, + secretName: secretKey, + secretTags: el.tags.length ? el.tags.map((tag) => tag.slug) : undefined + }) + ); + + let secretValue = ""; + + if (canReadValue) { + secretValue = decryptSymmetric128BitHexKeyUTF8({ + ciphertext: el.secretValueCiphertext, + iv: el.secretValueIV, + tag: el.secretValueTag, + key: botKey + }); + } else { + secretValue = INFISICAL_SECRET_VALUE_HIDDEN_MASK; + } + + return { + ...el, + secretKey, + secretValueHidden: !canReadValue, + secretValue, + secretComment: + el.secretCommentTag && el.secretCommentIV && el.secretCommentCiphertext + ? decryptSymmetric128BitHexKeyUTF8({ + ciphertext: el.secretCommentCiphertext, + iv: el.secretCommentIV, + tag: el.secretCommentTag, + key: botKey + }) + : "" + }; + }) }; } - const fullFolderPath = await getFullFolderPath({ - folderDAL, - folderId: snapshotDetails.folderId, - envId: snapshotDetails.environment.id - }); - - // We need to check if the user has access to the secrets in the folder. If we don't do this, a user could theoretically access snapshot secret values even if they don't have read access to the secrets in the folder. - ForbiddenError.from(permission).throwUnlessCan( - ProjectPermissionActions.Read, - subject(ProjectPermissionSub.Secrets, { - environment: snapshotDetails.environment.slug, - secretPath: fullFolderPath - }) - ); - return snapshotDetails; }; diff --git a/backend/src/lib/api-docs/constants.ts b/backend/src/lib/api-docs/constants.ts index 94076cf25..fe893d5ab 100644 --- a/backend/src/lib/api-docs/constants.ts +++ b/backend/src/lib/api-docs/constants.ts @@ -667,6 +667,7 @@ export const SECRETS = { secretPath: "The path of the secret to attach tags to.", type: "The type of the secret to attach tags to. (shared/personal)", environment: "The slug of the environment where the secret is located", + viewSecretValue: "Whether or not to retrieve the secret value.", projectSlug: "The slug of the project where the secret is located.", tagSlugs: "An array of existing tag slugs to attach to the secret." }, @@ -690,6 +691,7 @@ export const RAW_SECRETS = { "The slug of the project to list secrets from. This parameter is only applicable by machine identities.", environment: "The slug of the environment to list secrets from.", secretPath: "The secret path to list secrets from.", + viewSecretValue: "Whether or not to retrieve the secret value.", includeImports: "Weather to include imported secrets or not.", tagSlugs: "The comma separated tag slugs to filter secrets.", metadataFilter: @@ -718,6 +720,7 @@ export const RAW_SECRETS = { secretPath: "The path of the secret to get.", version: "The version of the secret to get.", type: "The type of the secret to get.", + viewSecretValue: "Whether or not to retrieve the secret value.", includeImports: "Weather to include imported secrets or not." }, UPDATE: { diff --git a/backend/src/lib/errors/index.ts b/backend/src/lib/errors/index.ts index f3dc83f26..950bfd29f 100644 --- a/backend/src/lib/errors/index.ts +++ b/backend/src/lib/errors/index.ts @@ -1,4 +1,5 @@ /* eslint-disable max-classes-per-file */ + export class DatabaseError extends Error { name: string; diff --git a/backend/src/server/routes/sanitizedSchemas.ts b/backend/src/server/routes/sanitizedSchemas.ts index a6cc8bae0..77149eb1b 100644 --- a/backend/src/server/routes/sanitizedSchemas.ts +++ b/backend/src/server/routes/sanitizedSchemas.ts @@ -7,6 +7,7 @@ import { ProjectRolesSchema, ProjectsSchema, SecretApprovalPoliciesSchema, + SecretTagsSchema, UsersSchema } from "@app/db/schemas"; import { ProjectPermissionActions, ProjectPermissionSub } from "@app/ee/services/permission/project-permission"; @@ -241,3 +242,11 @@ export const SanitizedProjectSchema = ProjectsSchema.pick({ kmsCertificateKeyId: true, auditLogsRetentionDays: true }); + +export const SanitizedTagSchema = SecretTagsSchema.pick({ + id: true, + slug: true, + color: true +}).extend({ + name: z.string() +}); diff --git a/backend/src/server/routes/v1/dashboard-router.ts b/backend/src/server/routes/v1/dashboard-router.ts index d96c45f04..27ba3f8a3 100644 --- a/backend/src/server/routes/v1/dashboard-router.ts +++ b/backend/src/server/routes/v1/dashboard-router.ts @@ -1,10 +1,11 @@ import { ForbiddenError, subject } from "@casl/ability"; import { z } from "zod"; -import { ActionProjectType, SecretFoldersSchema, SecretImportsSchema, SecretTagsSchema } from "@app/db/schemas"; +import { ActionProjectType, SecretFoldersSchema, SecretImportsSchema } from "@app/db/schemas"; import { EventType, UserAgentType } from "@app/ee/services/audit-log/audit-log-types"; import { ProjectPermissionDynamicSecretActions, + ProjectPermissionSecretActions, ProjectPermissionSub } from "@app/ee/services/permission/project-permission"; import { DASHBOARD } from "@app/lib/api-docs"; @@ -15,7 +16,7 @@ import { secretsLimit } from "@app/server/config/rateLimiter"; import { getTelemetryDistinctId } from "@app/server/lib/telemetry"; import { getUserAgentType } from "@app/server/plugins/audit-log"; import { verifyAuth } from "@app/server/plugins/auth/verify-auth"; -import { SanitizedDynamicSecretSchema, secretRawSchema } from "@app/server/routes/sanitizedSchemas"; +import { SanitizedDynamicSecretSchema, SanitizedTagSchema, secretRawSchema } from "@app/server/routes/sanitizedSchemas"; import { AuthMode } from "@app/services/auth/auth-type"; import { ResourceMetadataSchema } from "@app/services/resource-metadata/resource-metadata-schema"; import { SecretsOrderBy } from "@app/services/secret/secret-types"; @@ -116,16 +117,10 @@ export const registerDashboardRouter = async (server: FastifyZodProvider) => { dynamicSecrets: SanitizedDynamicSecretSchema.extend({ environment: z.string() }).array().optional(), secrets: secretRawSchema .extend({ + secretValueHidden: z.boolean(), secretPath: z.string().optional(), secretMetadata: ResourceMetadataSchema.optional(), - tags: SecretTagsSchema.pick({ - id: true, - slug: true, - color: true - }) - .extend({ name: z.string() }) - .array() - .optional() + tags: SanitizedTagSchema.array().optional() }) .array() .optional(), @@ -294,6 +289,7 @@ export const registerDashboardRouter = async (server: FastifyZodProvider) => { if (remainingLimit > 0 && totalSecretCount > adjustedOffset) { secrets = await server.services.secret.getSecretsRawMultiEnv({ + viewSecretValue: true, actorId: req.permission.id, actor: req.permission.type, actorOrgId: req.permission.orgId, @@ -393,6 +389,7 @@ export const registerDashboardRouter = async (server: FastifyZodProvider) => { .optional(), search: z.string().trim().describe(DASHBOARD.SECRET_DETAILS_LIST.search).optional(), tags: z.string().trim().transform(decodeURIComponent).describe(DASHBOARD.SECRET_DETAILS_LIST.tags).optional(), + viewSecretValue: booleanSchema.default(true), includeSecrets: booleanSchema.describe(DASHBOARD.SECRET_DETAILS_LIST.includeSecrets), includeFolders: booleanSchema.describe(DASHBOARD.SECRET_DETAILS_LIST.includeFolders), includeDynamicSecrets: booleanSchema.describe(DASHBOARD.SECRET_DETAILS_LIST.includeDynamicSecrets), @@ -410,16 +407,10 @@ export const registerDashboardRouter = async (server: FastifyZodProvider) => { dynamicSecrets: SanitizedDynamicSecretSchema.array().optional(), secrets: secretRawSchema .extend({ + secretValueHidden: z.boolean(), secretPath: z.string().optional(), secretMetadata: ResourceMetadataSchema.optional(), - tags: SecretTagsSchema.pick({ - id: true, - slug: true, - color: true - }) - .extend({ name: z.string() }) - .array() - .optional() + tags: SanitizedTagSchema.array().optional() }) .array() .optional(), @@ -601,23 +592,25 @@ export const registerDashboardRouter = async (server: FastifyZodProvider) => { }); if (remainingLimit > 0 && totalSecretCount > adjustedOffset) { - const secretsRaw = await server.services.secret.getSecretsRaw({ - actorId: req.permission.id, - actor: req.permission.type, - actorOrgId: req.permission.orgId, - environment, - actorAuthMethod: req.permission.authMethod, - projectId, - path: secretPath, - orderBy, - orderDirection, - search, - limit: remainingLimit, - offset: adjustedOffset, - tagSlugs: tags - }); - - secrets = secretsRaw.secrets; + secrets = ( + await server.services.secret.getSecretsRaw({ + actorId: req.permission.id, + actor: req.permission.type, + viewSecretValue: req.query.viewSecretValue, + throwOnMissingReadValuePermission: false, + actorOrgId: req.permission.orgId, + environment, + actorAuthMethod: req.permission.authMethod, + projectId, + path: secretPath, + orderBy, + orderDirection, + search, + limit: remainingLimit, + offset: adjustedOffset, + tagSlugs: tags + }) + ).secrets; await server.services.auditLog.createAuditLog({ projectId, @@ -696,16 +689,10 @@ export const registerDashboardRouter = async (server: FastifyZodProvider) => { .optional(), secrets: secretRawSchema .extend({ + secretValueHidden: z.boolean(), secretPath: z.string().optional(), secretMetadata: ResourceMetadataSchema.optional(), - tags: SecretTagsSchema.pick({ - id: true, - slug: true, - color: true - }) - .extend({ name: z.string() }) - .array() - .optional() + tags: SanitizedTagSchema.array().optional() }) .array() .optional() @@ -749,6 +736,7 @@ export const registerDashboardRouter = async (server: FastifyZodProvider) => { const secrets = await server.services.secret.getSecretsRawByFolderMappings( { + filterByAction: ProjectPermissionSecretActions.DescribeSecret, projectId, folderMappings, filters: { @@ -862,22 +850,17 @@ export const registerDashboardRouter = async (server: FastifyZodProvider) => { projectId: z.string().trim(), environment: z.string().trim(), secretPath: z.string().trim().default("/").transform(removeTrailingSlash), - keys: z.string().trim().transform(decodeURIComponent) + keys: z.string().trim().transform(decodeURIComponent), + viewSecretValue: booleanSchema.default(false) }), response: { 200: z.object({ secrets: secretRawSchema .extend({ + secretValueHidden: z.boolean(), secretPath: z.string().optional(), secretMetadata: ResourceMetadataSchema.optional(), - tags: SecretTagsSchema.pick({ - id: true, - slug: true, - color: true - }) - .extend({ name: z.string() }) - .array() - .optional() + tags: SanitizedTagSchema.array().optional() }) .array() .optional() @@ -886,7 +869,7 @@ export const registerDashboardRouter = async (server: FastifyZodProvider) => { }, onRequest: verifyAuth([AuthMode.JWT]), handler: async (req) => { - const { secretPath, projectId, environment } = req.query; + const { secretPath, projectId, environment, viewSecretValue } = req.query; const keys = req.query.keys?.split(",").filter((key) => Boolean(key.trim())) ?? []; if (!keys.length) throw new BadRequestError({ message: "One or more keys required" }); @@ -895,6 +878,7 @@ export const registerDashboardRouter = async (server: FastifyZodProvider) => { actorId: req.permission.id, actor: req.permission.type, actorOrgId: req.permission.orgId, + viewSecretValue, environment, actorAuthMethod: req.permission.authMethod, projectId, diff --git a/backend/src/server/routes/v2/service-token-router.ts b/backend/src/server/routes/v2/service-token-router.ts index fb10f17db..1a84c9123 100644 --- a/backend/src/server/routes/v2/service-token-router.ts +++ b/backend/src/server/routes/v2/service-token-router.ts @@ -94,7 +94,7 @@ export const registerServiceTokenRouter = async (server: FastifyZodProvider) => iv: z.string().trim(), tag: z.string().trim(), expiresIn: z.number().nullable(), - permissions: z.enum(["read", "write"]).array() + permissions: z.enum(["read", "write", "readValue"]).array() }), response: { 200: z.object({ diff --git a/backend/src/server/routes/v3/secret-router.ts b/backend/src/server/routes/v3/secret-router.ts index 4935345dc..b68d042f0 100644 --- a/backend/src/server/routes/v3/secret-router.ts +++ b/backend/src/server/routes/v3/secret-router.ts @@ -1,13 +1,7 @@ import picomatch from "picomatch"; import { z } from "zod"; -import { - SecretApprovalRequestsSchema, - SecretsSchema, - SecretTagsSchema, - SecretType, - ServiceTokenScopes -} from "@app/db/schemas"; +import { SecretApprovalRequestsSchema, SecretsSchema, SecretType, ServiceTokenScopes } from "@app/db/schemas"; import { EventType, UserAgentType } from "@app/ee/services/audit-log/audit-log-types"; import { RAW_SECRETS, SECRETS } from "@app/lib/api-docs"; import { BadRequestError, NotFoundError } from "@app/lib/errors"; @@ -23,7 +17,7 @@ import { SecretOperations, SecretProtectionType } from "@app/services/secret/sec import { SecretUpdateMode } from "@app/services/secret-v2-bridge/secret-v2-bridge-types"; import { PostHogEventTypes } from "@app/services/telemetry/telemetry-types"; -import { secretRawSchema } from "../sanitizedSchemas"; +import { SanitizedTagSchema, secretRawSchema } from "../sanitizedSchemas"; const SecretReferenceNode = z.object({ key: z.string(), @@ -31,6 +25,14 @@ const SecretReferenceNode = z.object({ environment: z.string(), secretPath: z.string() }); + +const convertStringBoolean = (defaultValue: boolean = false) => { + return z + .enum(["true", "false"]) + .default(defaultValue ? "true" : "false") + .transform((value) => value === "true"); +}; + type TSecretReferenceNode = z.infer & { children: TSecretReferenceNode[] }; const SecretReferenceNodeTree: z.ZodType = SecretReferenceNode.extend({ @@ -75,17 +77,9 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => { }), response: { 200: z.object({ - secret: SecretsSchema.omit({ secretBlindIndex: true }).merge( - z.object({ - tags: SecretTagsSchema.pick({ - id: true, - slug: true, - color: true - }) - .extend({ name: z.string() }) - .array() - }) - ) + secret: SecretsSchema.omit({ secretBlindIndex: true }).extend({ + tags: SanitizedTagSchema.array() + }) }) } }, @@ -139,13 +133,7 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => { response: { 200: z.object({ secret: SecretsSchema.omit({ secretBlindIndex: true }).extend({ - tags: SecretTagsSchema.pick({ - id: true, - slug: true, - color: true - }) - .extend({ name: z.string() }) - .array() + tags: SanitizedTagSchema.array() }) }) } @@ -247,21 +235,10 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => { workspaceSlug: z.string().trim().optional().describe(RAW_SECRETS.LIST.workspaceSlug), environment: z.string().trim().optional().describe(RAW_SECRETS.LIST.environment), secretPath: z.string().trim().default("/").transform(removeTrailingSlash).describe(RAW_SECRETS.LIST.secretPath), - expandSecretReferences: z - .enum(["true", "false"]) - .default("false") - .transform((value) => value === "true") - .describe(RAW_SECRETS.LIST.expand), - recursive: z - .enum(["true", "false"]) - .default("false") - .transform((value) => value === "true") - .describe(RAW_SECRETS.LIST.recursive), - include_imports: z - .enum(["true", "false"]) - .default("false") - .transform((value) => value === "true") - .describe(RAW_SECRETS.LIST.includeImports), + viewSecretValue: convertStringBoolean(true).describe(RAW_SECRETS.LIST.viewSecretValue), + expandSecretReferences: convertStringBoolean().describe(RAW_SECRETS.LIST.expand), + recursive: convertStringBoolean().describe(RAW_SECRETS.LIST.recursive), + include_imports: convertStringBoolean().describe(RAW_SECRETS.LIST.includeImports), tagSlugs: z .string() .describe(RAW_SECRETS.LIST.tagSlugs) @@ -274,15 +251,9 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => { secrets: secretRawSchema .extend({ secretPath: z.string().optional(), + secretValueHidden: z.boolean(), secretMetadata: ResourceMetadataSchema.optional(), - tags: SecretTagsSchema.pick({ - id: true, - slug: true, - color: true - }) - .extend({ name: z.string() }) - .array() - .optional() + tags: SanitizedTagSchema.array().optional() }) .array(), imports: z @@ -293,6 +264,7 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => { secrets: secretRawSchema .omit({ createdAt: true, updatedAt: true }) .extend({ + secretValueHidden: z.boolean(), secretMetadata: ResourceMetadataSchema.optional() }) .array() @@ -342,6 +314,7 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => { expandSecretReferences: req.query.expandSecretReferences, actorAuthMethod: req.permission.authMethod, projectId: workspaceId, + viewSecretValue: req.query.viewSecretValue, path: secretPath, metadataFilter: req.query.metadataFilter, includeImports: req.query.include_imports, @@ -376,6 +349,7 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => { } }); } + return { secrets, imports }; } }); @@ -445,28 +419,15 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => { secretPath: z.string().trim().default("/").transform(removeTrailingSlash).describe(RAW_SECRETS.GET.secretPath), version: z.coerce.number().optional().describe(RAW_SECRETS.GET.version), type: z.nativeEnum(SecretType).default(SecretType.Shared).describe(RAW_SECRETS.GET.type), - expandSecretReferences: z - .enum(["true", "false"]) - .default("false") - .transform((value) => value === "true") - .describe(RAW_SECRETS.GET.expand), - include_imports: z - .enum(["true", "false"]) - .default("false") - .transform((value) => value === "true") - .describe(RAW_SECRETS.GET.includeImports) + viewSecretValue: convertStringBoolean(true).describe(RAW_SECRETS.GET.viewSecretValue), + expandSecretReferences: convertStringBoolean().describe(RAW_SECRETS.GET.expand), + include_imports: convertStringBoolean().describe(RAW_SECRETS.GET.includeImports) }), response: { 200: z.object({ secret: secretRawSchema.extend({ - tags: SecretTagsSchema.pick({ - id: true, - slug: true, - color: true - }) - .extend({ name: z.string() }) - .array() - .optional(), + secretValueHidden: z.boolean(), + tags: SanitizedTagSchema.array().optional(), secretMetadata: ResourceMetadataSchema.optional() }) }) @@ -498,6 +459,7 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => { expandSecretReferences: req.query.expandSecretReferences, environment, projectId: workspaceId, + viewSecretValue: req.query.viewSecretValue, projectSlug: workspaceSlug, path: secretPath, secretName: req.params.secretName, @@ -704,7 +666,9 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => { response: { 200: z.union([ z.object({ - secret: secretRawSchema + secret: secretRawSchema.extend({ + secretValueHidden: z.boolean() + }) }), z.object({ approval: SecretApprovalRequestsSchema }).describe("When secret protection policy is enabled") ]) @@ -800,7 +764,9 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => { response: { 200: z.union([ z.object({ - secret: secretRawSchema + secret: secretRawSchema.extend({ + secretValueHidden: z.boolean() + }) }), z.object({ approval: SecretApprovalRequestsSchema }).describe("When secret protection policy is enabled") ]) @@ -822,6 +788,7 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => { if (secretOperation.type === SecretProtectionType.Approval) { return { approval: secretOperation.approval }; } + const { secret } = secretOperation; await server.services.auditLog.createAuditLog({ @@ -884,13 +851,7 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => { workspace: z.string(), environment: z.string(), secretPath: z.string().optional(), - tags: SecretTagsSchema.pick({ - id: true, - slug: true, - color: true - }) - .extend({ name: z.string() }) - .array() + tags: SanitizedTagSchema.array() }) .array(), imports: z @@ -986,10 +947,7 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => { secretPath: z.string().trim().default("/").transform(removeTrailingSlash), type: z.nativeEnum(SecretType).default(SecretType.Shared), version: z.coerce.number().optional(), - include_imports: z - .enum(["true", "false"]) - .default("false") - .transform((value) => value === "true") + include_imports: convertStringBoolean() }), response: { 200: z.object({ @@ -1260,6 +1218,7 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => { z.object({ secret: SecretsSchema.omit({ secretBlindIndex: true }).merge( z.object({ + secretValueHidden: z.boolean(), _id: z.string(), workspace: z.string(), environment: z.string() @@ -1429,13 +1388,12 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => { response: { 200: z.union([ z.object({ - secret: SecretsSchema.omit({ secretBlindIndex: true }).merge( - z.object({ - _id: z.string(), - workspace: z.string(), - environment: z.string() - }) - ) + secret: SecretsSchema.omit({ secretBlindIndex: true }).extend({ + _id: z.string(), + secretValueHidden: z.boolean(), + workspace: z.string(), + environment: z.string() + }) }), z.object({ approval: SecretApprovalRequestsSchema }).describe("When secret protection policy is enabled") ]) @@ -1747,7 +1705,7 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => { response: { 200: z.union([ z.object({ - secrets: SecretsSchema.omit({ secretBlindIndex: true }).array() + secrets: SecretsSchema.omit({ secretBlindIndex: true }).extend({ secretValueHidden: z.boolean() }).array() }), z.object({ approval: SecretApprovalRequestsSchema }).describe("When secret protection policy is enabled") ]) @@ -1862,7 +1820,11 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => { response: { 200: z.union([ z.object({ - secrets: SecretsSchema.omit({ secretBlindIndex: true }).array() + secrets: SecretsSchema.omit({ secretBlindIndex: true }) + .extend({ + secretValueHidden: z.boolean() + }) + .array() }), z.object({ approval: SecretApprovalRequestsSchema }).describe("When secret protection policy is enabled") ]) @@ -2124,7 +2086,7 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => { response: { 200: z.union([ z.object({ - secrets: secretRawSchema.array() + secrets: secretRawSchema.extend({ secretValueHidden: z.boolean() }).array() }), z.object({ approval: SecretApprovalRequestsSchema }).describe("When secret protection policy is enabled") ]) @@ -2246,7 +2208,11 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => { response: { 200: z.union([ z.object({ - secrets: secretRawSchema.array() + secrets: secretRawSchema + .extend({ + secretValueHidden: z.boolean() + }) + .array() }), z.object({ approval: SecretApprovalRequestsSchema }).describe("When secret protection policy is enabled") ]) diff --git a/backend/src/services/external-migration/external-migration-fns.ts b/backend/src/services/external-migration/external-migration-fns.ts index f4a54f0db..856b39012 100644 --- a/backend/src/services/external-migration/external-migration-fns.ts +++ b/backend/src/services/external-migration/external-migration-fns.ts @@ -31,9 +31,9 @@ export type TImportDataIntoInfisicalDTO = { projectEnvDAL: Pick; kmsService: Pick; - secretDAL: Pick; + secretDAL: Pick; secretVersionDAL: Pick; - secretTagDAL: Pick; + secretTagDAL: Pick; secretVersionTagDAL: Pick; resourceMetadataDAL: Pick; diff --git a/backend/src/services/external-migration/external-migration-queue.ts b/backend/src/services/external-migration/external-migration-queue.ts index e3d77e832..8aa46b94c 100644 --- a/backend/src/services/external-migration/external-migration-queue.ts +++ b/backend/src/services/external-migration/external-migration-queue.ts @@ -27,9 +27,9 @@ export type TExternalMigrationQueueFactoryDep = { projectEnvDAL: Pick; kmsService: Pick; - secretDAL: Pick; + secretDAL: Pick; secretVersionDAL: Pick; - secretTagDAL: Pick; + secretTagDAL: Pick; secretVersionTagDAL: Pick; folderDAL: Pick; diff --git a/backend/src/services/integration-auth/integration-delete-secret.ts b/backend/src/services/integration-auth/integration-delete-secret.ts index fdefd0e62..4b07245ac 100644 --- a/backend/src/services/integration-auth/integration-delete-secret.ts +++ b/backend/src/services/integration-auth/integration-delete-secret.ts @@ -68,7 +68,8 @@ const getIntegrationSecretsV2 = async ( secretDAL: secretV2BridgeDAL, secretImportDAL, secretImports, - hasSecretAccess: () => true + hasSecretAccess: () => true, + viewSecretValue: true }); for (let i = importedSecrets.length - 1; i >= 0; i -= 1) { diff --git a/backend/src/services/integration/integration-service.ts b/backend/src/services/integration/integration-service.ts index 35994433a..f72357964 100644 --- a/backend/src/services/integration/integration-service.ts +++ b/backend/src/services/integration/integration-service.ts @@ -2,7 +2,11 @@ import { ForbiddenError, subject } from "@casl/ability"; import { ActionProjectType } from "@app/db/schemas"; import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service"; -import { ProjectPermissionActions, ProjectPermissionSub } from "@app/ee/services/permission/project-permission"; +import { + ProjectPermissionActions, + ProjectPermissionSecretActions, + ProjectPermissionSub +} from "@app/ee/services/permission/project-permission"; import { NotFoundError } from "@app/lib/errors"; import { TProjectPermission } from "@app/lib/types"; @@ -92,7 +96,7 @@ export const integrationServiceFactory = ({ ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Create, ProjectPermissionSub.Integrations); ForbiddenError.from(permission).throwUnlessCan( - ProjectPermissionActions.Read, + ProjectPermissionSecretActions.ReadValue, subject(ProjectPermissionSub.Secrets, { environment: sourceEnvironment, secretPath @@ -175,7 +179,7 @@ export const integrationServiceFactory = ({ if (environment || secretPath) { ForbiddenError.from(permission).throwUnlessCan( - ProjectPermissionActions.Read, + ProjectPermissionSecretActions.ReadValue, subject(ProjectPermissionSub.Secrets, { environment: newEnvironment, secretPath: newSecretPath diff --git a/backend/src/services/project/project-service.ts b/backend/src/services/project/project-service.ts index 4b110ebd2..5367a70cd 100644 --- a/backend/src/services/project/project-service.ts +++ b/backend/src/services/project/project-service.ts @@ -11,7 +11,11 @@ import { import { TLicenseServiceFactory } from "@app/ee/services/license/license-service"; import { OrgPermissionActions, OrgPermissionSubjects } from "@app/ee/services/permission/org-permission"; import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service"; -import { ProjectPermissionActions, ProjectPermissionSub } from "@app/ee/services/permission/project-permission"; +import { + ProjectPermissionActions, + ProjectPermissionSecretActions, + ProjectPermissionSub +} from "@app/ee/services/permission/project-permission"; import { TProjectTemplateServiceFactory } from "@app/ee/services/project-template/project-template-service"; import { InfisicalProjectTemplate } from "@app/ee/services/project-template/project-template-types"; import { TSshCertificateAuthorityDALFactory } from "@app/ee/services/ssh/ssh-certificate-authority-dal"; @@ -760,7 +764,10 @@ export const projectServiceFactory = ({ actorOrgId, actionProjectType: ActionProjectType.Any }); - ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.Secrets); + ForbiddenError.from(permission).throwUnlessCan( + ProjectPermissionSecretActions.DescribeSecret, + ProjectPermissionSub.Secrets + ); const project = await projectDAL.findProjectById(projectId); diff --git a/backend/src/services/secret-import/secret-import-fns.ts b/backend/src/services/secret-import/secret-import-fns.ts index d21ad3b9c..e5a450441 100644 --- a/backend/src/services/secret-import/secret-import-fns.ts +++ b/backend/src/services/secret-import/secret-import-fns.ts @@ -3,6 +3,7 @@ import { groupBy, unique } from "@app/lib/fn"; import { ResourceMetadataDTO } from "../resource-metadata/resource-metadata-schema"; import { TSecretDALFactory } from "../secret/secret-dal"; +import { INFISICAL_SECRET_VALUE_HIDDEN_MASK } from "../secret/secret-fns"; import { TSecretFolderDALFactory } from "../secret-folder/secret-folder-dal"; import { TSecretV2BridgeDALFactory } from "../secret-v2-bridge/secret-v2-bridge-dal"; import { TSecretImportDALFactory } from "./secret-import-dal"; @@ -32,6 +33,12 @@ type TSecretImportSecretsV2 = { folderId: string | undefined; importFolderId: string; secrets: (TSecretsV2 & { + secretTags: { + slug: string; + name: string; + color?: string | null; + id: string; + }[]; workspace: string; environment: string; _id: string; @@ -39,6 +46,7 @@ type TSecretImportSecretsV2 = { // akhilmhdh: yes i know you can put ?. // But for somereason ts consider ? and undefined explicit as different just ts things secretValue: string; + secretValueHidden: boolean; secretComment: string; secretMetadata?: ResourceMetadataDTO; })[]; @@ -150,12 +158,14 @@ export const fnSecretsV2FromImports = async ({ secretImportDAL, decryptor, expandSecretReferences, - hasSecretAccess + hasSecretAccess, + viewSecretValue }: { secretImports: (Omit & { importEnv: { id: string; slug: string; name: string }; })[]; folderDAL: Pick; + viewSecretValue: boolean; secretDAL: Pick; secretImportDAL: Pick; decryptor: (value?: Buffer | null) => string; @@ -168,9 +178,14 @@ export const fnSecretsV2FromImports = async ({ hasSecretAccess: (environment: string, secretPath: string, secretName: string, secretTagSlugs: string[]) => boolean; }) => { const cyclicDetector = new Set(); - const stack: { secretImports: typeof rootSecretImports; depth: number; parentImportedSecrets: TSecretsV2[] }[] = [ - { secretImports: rootSecretImports, depth: 0, parentImportedSecrets: [] } - ]; + const stack: { + secretImports: typeof rootSecretImports; + depth: number; + parentImportedSecrets: (TSecretsV2 & { + secretValueHidden: boolean; + secretTags: { slug: string; name: string; id: string; color?: string | null }[]; + })[]; + }[] = [{ secretImports: rootSecretImports, depth: 0, parentImportedSecrets: [] }]; const processedImports: TSecretImportSecretsV2[] = []; @@ -229,7 +244,9 @@ export const fnSecretsV2FromImports = async ({ .map((item) => ({ ...item, secretKey: item.key, - secretValue: decryptor(item.encryptedValue), + secretValue: viewSecretValue ? decryptor(item.encryptedValue) : INFISICAL_SECRET_VALUE_HIDDEN_MASK, + secretValueHidden: !viewSecretValue, + secretTags: item.tags, secretComment: decryptor(item.encryptedComment), environment: importEnv.slug, workspace: "", // This field should not be used, it's only here to keep the older Python SDK versions backwards compatible with the new Postgres backend. @@ -267,6 +284,8 @@ export const fnSecretsV2FromImports = async ({ processedImport.secrets = unique(processedImport.secrets, (i) => i.key); return Promise.allSettled( processedImport.secrets.map(async (decryptedSecret, index) => { + if (decryptedSecret.secretValueHidden) return; + const expandedSecretValue = await expandSecretReferences({ value: decryptedSecret.secretValue, secretPath: processedImport.secretPath, diff --git a/backend/src/services/secret-import/secret-import-service.ts b/backend/src/services/secret-import/secret-import-service.ts index e8fde04d1..6c412edc0 100644 --- a/backend/src/services/secret-import/secret-import-service.ts +++ b/backend/src/services/secret-import/secret-import-service.ts @@ -5,7 +5,11 @@ import { ForbiddenError, subject } from "@casl/ability"; import { ActionProjectType, TableName } from "@app/db/schemas"; import { TLicenseServiceFactory } from "@app/ee/services/license/license-service"; import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service"; -import { ProjectPermissionActions, ProjectPermissionSub } from "@app/ee/services/permission/project-permission"; +import { + ProjectPermissionActions, + ProjectPermissionSecretActions, + ProjectPermissionSub +} from "@app/ee/services/permission/project-permission"; import { getReplicationFolderName } from "@app/ee/services/secret-replication/secret-replication-service"; import { BadRequestError, NotFoundError } from "@app/lib/errors"; @@ -90,7 +94,7 @@ export const secretImportServiceFactory = ({ // check if user has permission to import from target path ForbiddenError.from(permission).throwUnlessCan( - ProjectPermissionActions.Read, + ProjectPermissionSecretActions.DescribeSecret, subject(ProjectPermissionSub.Secrets, { environment: data.environment, secretPath: data.path @@ -402,7 +406,7 @@ export const secretImportServiceFactory = ({ // check if user has permission to import from target path ForbiddenError.from(permission).throwUnlessCan( - ProjectPermissionActions.Read, + ProjectPermissionSecretActions.DescribeSecret, subject(ProjectPermissionSub.Secrets, { environment: secretImportDoc.importEnv.slug, secretPath: secretImportDoc.importPath @@ -596,7 +600,7 @@ export const secretImportServiceFactory = ({ const secretImports = await secretImportDAL.find({ folderId: folder.id, isReplication: false }); const allowedImports = secretImports.filter((el) => permission.can( - ProjectPermissionActions.Read, + ProjectPermissionSecretActions.ReadValue, subject(ProjectPermissionSub.Secrets, { environment: el.importEnv.slug, secretPath: el.importPath @@ -642,12 +646,13 @@ export const secretImportServiceFactory = ({ const importedSecrets = await fnSecretsV2FromImports({ secretImports, folderDAL, + viewSecretValue: true, secretDAL: secretV2BridgeDAL, secretImportDAL, decryptor: (value) => (value ? secretManagerDecryptor({ cipherTextBlob: value }).toString() : ""), hasSecretAccess: (expandEnvironment, expandSecretPath, expandSecretKey, expandSecretTags) => permission.can( - ProjectPermissionActions.Read, + ProjectPermissionSecretActions.ReadValue, subject(ProjectPermissionSub.Secrets, { environment: expandEnvironment, secretPath: expandSecretPath, @@ -667,7 +672,7 @@ export const secretImportServiceFactory = ({ const allowedImports = secretImports.filter((el) => permission.can( - ProjectPermissionActions.Read, + ProjectPermissionSecretActions.ReadValue, subject(ProjectPermissionSub.Secrets, { environment: el.importEnv.slug, secretPath: el.importPath @@ -683,7 +688,10 @@ export const secretImportServiceFactory = ({ return importedSecrets.map((el) => ({ ...el, secrets: el.secrets.map((encryptedSecret) => - decryptSecretRaw({ ...encryptedSecret, workspace: projectId, environment, secretPath }, botKey) + decryptSecretRaw( + { ...encryptedSecret, workspace: projectId, environment, secretPath, secretValueHidden: false }, + botKey + ) ) })); }; diff --git a/backend/src/services/secret-sync/secret-sync-queue.ts b/backend/src/services/secret-sync/secret-sync-queue.ts index cdc9540da..8afcf6416 100644 --- a/backend/src/services/secret-sync/secret-sync-queue.ts +++ b/backend/src/services/secret-sync/secret-sync-queue.ts @@ -249,7 +249,8 @@ export const secretSyncQueueFactory = ({ expandSecretReferences, secretImportDAL, secretImports, - hasSecretAccess: () => true + hasSecretAccess: () => true, + viewSecretValue: true }); for (let i = importedSecrets.length - 1; i >= 0; i -= 1) { diff --git a/backend/src/services/secret-sync/secret-sync-service.ts b/backend/src/services/secret-sync/secret-sync-service.ts index 8211180e8..7b92a463c 100644 --- a/backend/src/services/secret-sync/secret-sync-service.ts +++ b/backend/src/services/secret-sync/secret-sync-service.ts @@ -3,7 +3,7 @@ import { ForbiddenError, subject } from "@casl/ability"; import { ActionProjectType } from "@app/db/schemas"; import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service"; import { - ProjectPermissionActions, + ProjectPermissionSecretActions, ProjectPermissionSecretSyncActions, ProjectPermissionSub } from "@app/ee/services/permission/project-permission"; @@ -179,7 +179,7 @@ export const secretSyncServiceFactory = ({ ); ForbiddenError.from(projectPermission).throwUnlessCan( - ProjectPermissionActions.Read, + ProjectPermissionSecretActions.ReadValue, subject(ProjectPermissionSub.Secrets, { environment, secretPath @@ -270,7 +270,7 @@ export const secretSyncServiceFactory = ({ throw new BadRequestError({ message: "Must specify both source environment and secret path" }); ForbiddenError.from(permission).throwUnlessCan( - ProjectPermissionActions.Read, + ProjectPermissionSecretActions.ReadValue, subject(ProjectPermissionSub.Secrets, { environment: updatedEnvironment, secretPath: updatedSecretPath diff --git a/backend/src/services/secret-tag/secret-tag-dal.ts b/backend/src/services/secret-tag/secret-tag-dal.ts index 1df64afa2..3b9151557 100644 --- a/backend/src/services/secret-tag/secret-tag-dal.ts +++ b/backend/src/services/secret-tag/secret-tag-dal.ts @@ -47,6 +47,7 @@ export const secretTagDALFactory = (db: TDbClient) => { throw new DatabaseError({ error, name: "Find all by ids" }); } }; + return { ...secretTagOrm, saveTagsToSecret: secretJnTagOrm.insertMany, diff --git a/backend/src/services/secret-v2-bridge/secret-v2-bridge-fns.ts b/backend/src/services/secret-v2-bridge/secret-v2-bridge-fns.ts index 751235cde..8e1de2d91 100644 --- a/backend/src/services/secret-v2-bridge/secret-v2-bridge-fns.ts +++ b/backend/src/services/secret-v2-bridge/secret-v2-bridge-fns.ts @@ -8,6 +8,7 @@ import { logger } from "@app/lib/logger"; import { ActorType } from "../auth/auth-type"; import { TProjectEnvDALFactory } from "../project-env/project-env-dal"; import { ResourceMetadataDTO } from "../resource-metadata/resource-metadata-schema"; +import { INFISICAL_SECRET_VALUE_HIDDEN_MASK } from "../secret/secret-fns"; import { TSecretFolderDALFactory } from "../secret-folder/secret-folder-dal"; import { TSecretV2BridgeDALFactory } from "./secret-v2-bridge-dal"; import { TFnSecretBulkDelete, TFnSecretBulkInsert, TFnSecretBulkUpdate } from "./secret-v2-bridge-types"; @@ -108,6 +109,7 @@ export const fnSecretBulkInsert = async ({ [`${TableName.SecretV2}Id` as const]: newSecretGroupedByKeyName[key][0].id })) ); + const secretVersions = await secretVersionDAL.insertMany( sanitizedInputSecrets.map((el) => ({ ...el, @@ -146,6 +148,7 @@ export const fnSecretBulkInsert = async ({ if (newSecretTags.length) { const secTags = await secretTagDAL.saveTagsToSecretV2(newSecretTags, tx); const secVersionsGroupBySecId = groupBy(secretVersions, (i) => i.secretId); + const newSecretVersionTags = secTags.flatMap(({ secrets_v2Id, secret_tagsId }) => ({ [`${TableName.SecretVersionV2}Id` as const]: secVersionsGroupBySecId[secrets_v2Id][0].id, [`${TableName.SecretTag}Id` as const]: secret_tagsId @@ -154,7 +157,16 @@ export const fnSecretBulkInsert = async ({ await secretVersionTagDAL.insertMany(newSecretVersionTags, tx); } - return newSecrets.map((secret) => ({ ...secret, _id: secret.id })); + const secretsWithTags = await secretDAL.find( + { + $in: { + [`${TableName.SecretV2}.id` as "id"]: newSecrets.map((s) => s.id) + } + }, + { tx } + ); + + return secretsWithTags.map((secret) => ({ ...secret, _id: secret.id })); }; export const fnSecretBulkUpdate = async ({ @@ -300,7 +312,15 @@ export const fnSecretBulkUpdate = async ({ tx ); - return newSecrets.map((secret) => ({ ...secret, _id: secret.id })); + const secretsWithTags = await secretDAL.find( + { + $in: { + [`${TableName.SecretV2}.id` as "id"]: newSecrets.map((s) => s.id) + } + }, + { tx } + ); + return secretsWithTags.map((secret) => ({ ...secret, _id: secret.id })); }; export const fnSecretBulkDelete = async ({ @@ -533,7 +553,7 @@ export const expandSecretReferencesFactory = ({ const referredValue = await fetchSecret(environment, secretPath, secretKey); if (!canExpandValue(environment, secretPath, secretKey, referredValue.tags)) throw new ForbiddenRequestError({ - message: `You are attempting to reference secret named ${secretKey} from environment ${environment} in path ${secretPath} which you do not have access to.` + message: `You are attempting to reference secret named ${secretKey} from environment ${environment} in path ${secretPath} which you do not have access to read value on.` }); const cacheKey = getCacheUniqueKey(environment, secretPath); @@ -552,7 +572,7 @@ export const expandSecretReferencesFactory = ({ const referedValue = await fetchSecret(secretReferenceEnvironment, secretReferencePath, secretReferenceKey); if (!canExpandValue(secretReferenceEnvironment, secretReferencePath, secretReferenceKey, referedValue.tags)) throw new ForbiddenRequestError({ - message: `You are attempting to reference secret named ${secretReferenceKey} from environment ${secretReferenceEnvironment} in path ${secretReferencePath} which you do not have access to.` + message: `You are attempting to reference secret named ${secretReferenceKey} from environment ${secretReferenceEnvironment} in path ${secretReferencePath} which you do not have access to read value on.` }); const cacheKey = getCacheUniqueKey(secretReferenceEnvironment, secretReferencePath); @@ -646,13 +666,13 @@ export const reshapeBridgeSecret = ( name: string; }[]; secretMetadata?: ResourceMetadataDTO; - } + }, + secretValueHidden: boolean ) => ({ secretKey: secret.key, secretPath, workspace: workspaceId, environment, - secretValue: secret.value || "", secretComment: secret.comment || "", version: secret.version, type: secret.type, @@ -674,5 +694,15 @@ export const reshapeBridgeSecret = ( metadata: secret.metadata, secretMetadata: secret.secretMetadata, createdAt: secret.createdAt, - updatedAt: secret.updatedAt + updatedAt: secret.updatedAt, + + ...(secretValueHidden + ? { + secretValue: INFISICAL_SECRET_VALUE_HIDDEN_MASK, + secretValueHidden: true + } + : { + secretValue: secret.value || "", + secretValueHidden: false + }) }); diff --git a/backend/src/services/secret-v2-bridge/secret-v2-bridge-service.ts b/backend/src/services/secret-v2-bridge/secret-v2-bridge-service.ts index d6ba02856..3b7f04907 100644 --- a/backend/src/services/secret-v2-bridge/secret-v2-bridge-service.ts +++ b/backend/src/services/secret-v2-bridge/secret-v2-bridge-service.ts @@ -11,7 +11,11 @@ import { TSecretsV2 } from "@app/db/schemas"; import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service"; -import { ProjectPermissionActions, ProjectPermissionSub } from "@app/ee/services/permission/project-permission"; +import { + ProjectPermissionActions, + ProjectPermissionSecretActions, + ProjectPermissionSub +} from "@app/ee/services/permission/project-permission"; import { TSecretApprovalPolicyServiceFactory } from "@app/ee/services/secret-approval-policy/secret-approval-policy-service"; import { TSecretApprovalRequestDALFactory } from "@app/ee/services/secret-approval-request/secret-approval-request-dal"; import { TSecretApprovalRequestSecretDALFactory } from "@app/ee/services/secret-approval-request/secret-approval-request-secret-dal"; @@ -259,7 +263,7 @@ export const secretV2BridgeServiceFactory = ({ const { secretName, type, ...inputSecretData } = inputSecret; ForbiddenError.from(permission).throwUnlessCan( - ProjectPermissionActions.Create, + ProjectPermissionSecretActions.Create, subject(ProjectPermissionSub.Secrets, { environment, secretPath, @@ -278,8 +282,8 @@ export const secretV2BridgeServiceFactory = ({ type: KmsDataKey.SecretManager, projectId }); - const secret = await secretDAL.transaction((tx) => - fnSecretBulkInsert({ + const secret = await secretDAL.transaction(async (tx) => { + const [createdSecret] = await fnSecretBulkInsert({ folderId, orgId: actorOrgId, inputSecrets: [ @@ -313,8 +317,10 @@ export const secretV2BridgeServiceFactory = ({ actorId }, tx - }) - ); + }); + + return createdSecret; + }); if (inputSecret.type === SecretType.Shared) { await snapshotService.performSnapshot(folderId); @@ -328,11 +334,17 @@ export const secretV2BridgeServiceFactory = ({ }); } - return reshapeBridgeSecret(projectId, environment, secretPath, { - ...secret[0], - value: inputSecret.secretValue, - comment: inputSecret.secretComment || "" - }); + return reshapeBridgeSecret( + projectId, + environment, + secretPath, + { + ...secret, + value: inputSecret.secretValue, + comment: inputSecret.secretComment || "" + }, + false + ); }; const updateSecret = async ({ @@ -401,7 +413,7 @@ export const secretV2BridgeServiceFactory = ({ } ForbiddenError.from(permission).throwUnlessCan( - ProjectPermissionActions.Edit, + ProjectPermissionSecretActions.Edit, subject(ProjectPermissionSub.Secrets, { environment, secretPath, @@ -412,18 +424,22 @@ export const secretV2BridgeServiceFactory = ({ // validate tags // fetch all tags and if not same count throw error meaning one was invalid tags - const tags = inputSecret.tagIds ? await secretTagDAL.find({ projectId, $in: { id: inputSecret.tagIds } }) : []; - if ((inputSecret.tagIds || []).length !== tags.length) - throw new NotFoundError({ message: `Tag not found. Found ${tags.map((el) => el.slug).join(",")}` }); + const newTags = inputSecret.tagIds ? await secretTagDAL.find({ projectId, $in: { id: inputSecret.tagIds } }) : []; + if ((inputSecret.tagIds || []).length !== newTags.length) + throw new NotFoundError({ message: `Tag not found. Found ${newTags.map((el) => el.slug).join(",")}` }); + + const tagsToCheck = inputSecret.tagIds ? newTags : secret.tags; // now check with new ids ForbiddenError.from(permission).throwUnlessCan( - ProjectPermissionActions.Edit, + ProjectPermissionSecretActions.Edit, subject(ProjectPermissionSub.Secrets, { environment, secretPath, secretName: inputSecret.secretName, - secretTags: tags?.map((el) => el.slug) + ...(tagsToCheck.length && { + secretTags: tagsToCheck.map((el) => el.slug) + }) }) ); @@ -435,12 +451,14 @@ export const secretV2BridgeServiceFactory = ({ }); if (doesNewNameSecretExist) throw new BadRequestError({ message: "Secret with the new name already exist" }); ForbiddenError.from(permission).throwUnlessCan( - ProjectPermissionActions.Edit, + ProjectPermissionSecretActions.Edit, subject(ProjectPermissionSub.Secrets, { environment, secretPath, secretName: inputSecret.newSecretName, - secretTags: tags?.map((el) => el.slug) + ...(tagsToCheck.length && { + secretTags: tagsToCheck.map((el) => el.slug) + }) }) ); } @@ -522,11 +540,29 @@ export const secretV2BridgeServiceFactory = ({ }); } - return reshapeBridgeSecret(projectId, environment, secretPath, { - ...updatedSecret[0], - value: inputSecret.secretValue || "", - comment: inputSecret.secretComment || "" - }); + const secretValueHidden = !permission.can( + ProjectPermissionSecretActions.ReadValue, + subject(ProjectPermissionSub.Secrets, { + environment, + secretPath, + secretName: inputSecret.secretName, + ...(tagsToCheck.length && { + secretTags: tagsToCheck.map((el) => el.slug) + }) + }) + ); + + return reshapeBridgeSecret( + projectId, + environment, + secretPath, + { + ...updatedSecret[0], + value: inputSecret.secretValue || "", + comment: inputSecret.secretComment || "" + }, + secretValueHidden + ); }; const deleteSecret = async ({ @@ -572,7 +608,7 @@ export const secretV2BridgeServiceFactory = ({ }); if (!secretToDelete) throw new NotFoundError({ message: "Secret not found" }); ForbiddenError.from(permission).throwUnlessCan( - ProjectPermissionActions.Delete, + ProjectPermissionSecretActions.Delete, subject(ProjectPermissionSub.Secrets, { environment, secretPath, @@ -614,15 +650,32 @@ export const secretV2BridgeServiceFactory = ({ type: KmsDataKey.SecretManager, projectId }); - return reshapeBridgeSecret(projectId, environment, secretPath, { - ...deletedSecret[0], - value: deletedSecret[0].encryptedValue - ? secretManagerDecryptor({ cipherTextBlob: deletedSecret[0].encryptedValue }).toString() - : "", - comment: deletedSecret[0].encryptedComment - ? secretManagerDecryptor({ cipherTextBlob: deletedSecret[0].encryptedComment }).toString() - : "" - }); + + const secretValueHidden = !permission.can( + ProjectPermissionSecretActions.ReadValue, + subject(ProjectPermissionSub.Secrets, { + environment, + secretPath, + secretName: secretToDelete.key, + secretTags: secretToDelete.tags?.map((el) => el.slug) + }) + ); + + return reshapeBridgeSecret( + projectId, + environment, + secretPath, + { + ...deletedSecret[0], + value: deletedSecret[0].encryptedValue + ? secretManagerDecryptor({ cipherTextBlob: deletedSecret[0].encryptedValue }).toString() + : "", + comment: deletedSecret[0].encryptedComment + ? secretManagerDecryptor({ cipherTextBlob: deletedSecret[0].encryptedComment }).toString() + : "" + }, + secretValueHidden + ); }; // get unique secrets count for multiple envs @@ -650,7 +703,10 @@ export const secretV2BridgeServiceFactory = ({ actionProjectType: ActionProjectType.SecretManager }); - ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.Secrets); + ForbiddenError.from(permission).throwUnlessCan( + ProjectPermissionSecretActions.DescribeSecret, + ProjectPermissionSub.Secrets + ); } const folders = await folderDAL.findBySecretPathMultiEnv(projectId, environments, path); @@ -697,7 +753,10 @@ export const secretV2BridgeServiceFactory = ({ actionProjectType: ActionProjectType.SecretManager }); - ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.Secrets); + ForbiddenError.from(permission).throwUnlessCan( + ProjectPermissionSecretActions.DescribeSecret, + ProjectPermissionSub.Secrets + ); const folder = await folderDAL.findBySecretPath(projectId, environment, path); if (!folder) return 0; @@ -708,7 +767,13 @@ export const secretV2BridgeServiceFactory = ({ }; const getSecretsByFolderMappings = async ( - { projectId, userId, filters, folderMappings }: TGetSecretsRawByFolderMappingsDTO, + { + projectId, + userId, + filters, + folderMappings, + filterByAction = ProjectPermissionSecretActions.ReadValue + }: TGetSecretsRawByFolderMappingsDTO, projectPermission: Awaited>["permission"] ) => { const groupedFolderMappings = groupBy(folderMappings, (folderMapping) => folderMapping.folderId); @@ -728,7 +793,7 @@ export const secretV2BridgeServiceFactory = ({ const decryptedSecrets = secrets .filter((el) => projectPermission.can( - ProjectPermissionActions.Read, + filterByAction, subject(ProjectPermissionSub.Secrets, { environment: groupedFolderMappings[el.folderId][0].environment, secretPath: groupedFolderMappings[el.folderId][0].path, @@ -737,8 +802,19 @@ export const secretV2BridgeServiceFactory = ({ }) ) ) - .map((secret) => - reshapeBridgeSecret( + .map((secret) => { + // Note(Daniel): This is only relevant if the filterAction isn't set to ReadValue. This is needed for the frontend. + const secretValueHidden = !projectPermission.can( + ProjectPermissionSecretActions.ReadValue, + subject(ProjectPermissionSub.Secrets, { + environment: groupedFolderMappings[secret.folderId][0].environment, + secretPath: groupedFolderMappings[secret.folderId][0].path, + secretName: secret.key, + secretTags: secret.tags.map((i) => i.slug) + }) + ); + + return reshapeBridgeSecret( projectId, groupedFolderMappings[secret.folderId][0].environment, groupedFolderMappings[secret.folderId][0].path, @@ -750,9 +826,10 @@ export const secretV2BridgeServiceFactory = ({ comment: secret.encryptedComment ? secretManagerDecryptor({ cipherTextBlob: secret.encryptedComment }).toString() : "" - } - ) - ); + }, + secretValueHidden + ); + }); return decryptedSecrets; }; @@ -781,7 +858,10 @@ export const secretV2BridgeServiceFactory = ({ actionProjectType: ActionProjectType.SecretManager }); if (!isInternal) { - ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.Secrets); + ForbiddenError.from(permission).throwUnlessCan( + ProjectPermissionSecretActions.DescribeSecret, + ProjectPermissionSub.Secrets + ); } const folders = await folderDAL.findBySecretPathMultiEnv(projectId, environments, path); @@ -801,7 +881,8 @@ export const secretV2BridgeServiceFactory = ({ projectId, folderMappings, filters: params, - userId: actorId + userId: actorId, + filterByAction: ProjectPermissionSecretActions.DescribeSecret }, permission ); @@ -816,10 +897,12 @@ export const secretV2BridgeServiceFactory = ({ projectId, actor, actorOrgId, + viewSecretValue, actorAuthMethod, includeImports, recursive, expandSecretReferences: shouldExpandSecretReferences, + throwOnMissingReadValuePermission = true, ...params }: TGetSecretsDTO) => { const { permission } = await permissionService.getProjectPermission({ @@ -831,7 +914,14 @@ export const secretV2BridgeServiceFactory = ({ actionProjectType: ActionProjectType.SecretManager }); - ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.Secrets); + ForbiddenError.from(permission).throwUnlessCan( + ProjectPermissionSecretActions.DescribeSecret, + subject(ProjectPermissionSub.Secrets, { + environment, + secretPath: path, + secretTags: params.tagSlugs + }) + ); let paths: { folderId: string; path: string }[] = []; @@ -869,28 +959,87 @@ export const secretV2BridgeServiceFactory = ({ }); const decryptedSecrets = secrets - .filter((el) => - permission.can( - ProjectPermissionActions.Read, + .filter((el) => { + const canDescribeSecret = permission.can( + ProjectPermissionSecretActions.DescribeSecret, subject(ProjectPermissionSub.Secrets, { environment, secretPath: groupedPaths[el.folderId][0].path, secretName: el.key, secretTags: el.tags.map((i) => i.slug) }) - ) - ) - .map((secret) => - reshapeBridgeSecret(projectId, environment, groupedPaths[secret.folderId][0].path, { - ...secret, - value: secret.encryptedValue - ? secretManagerDecryptor({ cipherTextBlob: secret.encryptedValue }).toString() - : "", - comment: secret.encryptedComment - ? secretManagerDecryptor({ cipherTextBlob: secret.encryptedComment }).toString() - : "" - }) - ); + ); + + if (!canDescribeSecret) { + return false; + } + + if (viewSecretValue) { + // Recursive secret, should be filtered out + if (groupedPaths[el.folderId][0].path !== path) { + const canReadRecursiveSecretValue = permission.can( + ProjectPermissionSecretActions.ReadValue, + subject(ProjectPermissionSub.Secrets, { + environment, + secretPath: groupedPaths[el.folderId][0].path, + secretName: el.key, + secretTags: el.tags.map((i) => i.slug) + }) + ); + + if (!canReadRecursiveSecretValue) { + return false; + } + } + + if (throwOnMissingReadValuePermission) { + ForbiddenError.from(permission).throwUnlessCan( + ProjectPermissionSecretActions.ReadValue, + subject(ProjectPermissionSub.Secrets, { + environment, + secretPath: groupedPaths[el.folderId][0].path, + secretName: el.key, + secretTags: el.tags.map((i) => i.slug) + }) + ); + } + // Else, we do nothing. Because we don't want to filter out the secret, OR throw an error. + // If the user doesn't have access to read the value, in the below map function, we mask the secret value and return the secret with a hidden value. + } + + return canDescribeSecret; + }) + .map((secret) => { + const isPersonalSecret = secret.userId === actorId && secret.type === SecretType.Personal; + + const secretValueHidden = + !viewSecretValue || + !permission.can( + ProjectPermissionSecretActions.ReadValue, + subject(ProjectPermissionSub.Secrets, { + environment, + secretPath: groupedPaths[secret.folderId][0].path, + secretName: secret.key, + secretTags: secret.tags.map((i) => i.slug) + }) + ); + + return reshapeBridgeSecret( + projectId, + environment, + groupedPaths[secret.folderId][0].path, + { + ...secret, + value: secret.encryptedValue + ? secretManagerDecryptor({ cipherTextBlob: secret.encryptedValue }).toString() + : "", + comment: secret.encryptedComment + ? secretManagerDecryptor({ cipherTextBlob: secret.encryptedComment }).toString() + : "" + }, + secretValueHidden && !isPersonalSecret + ); + }); const { expandSecretReferences } = expandSecretReferencesFactory({ projectId, @@ -899,7 +1048,7 @@ export const secretV2BridgeServiceFactory = ({ decryptSecretValue: (value) => (value ? secretManagerDecryptor({ cipherTextBlob: value }).toString() : undefined), canExpandValue: (expandEnvironment, expandSecretPath, expandSecretKey, expandSecretTags) => permission.can( - ProjectPermissionActions.Read, + ProjectPermissionSecretActions.ReadValue, subject(ProjectPermissionSub.Secrets, { environment: expandEnvironment, secretPath: expandSecretPath, @@ -938,22 +1087,36 @@ export const secretV2BridgeServiceFactory = ({ const secretImports = await secretImportDAL.findByFolderIds(paths.map((p) => p.folderId)); const allowedImports = secretImports.filter(({ isReplication }) => !isReplication); const importedSecrets = await fnSecretsV2FromImports({ + viewSecretValue, secretImports: allowedImports, secretDAL, folderDAL, secretImportDAL, expandSecretReferences, decryptor: (value) => (value ? secretManagerDecryptor({ cipherTextBlob: value }).toString() : ""), - hasSecretAccess: (expandEnvironment, expandSecretPath, expandSecretKey, expandSecretTags) => - permission.can( - ProjectPermissionActions.Read, + hasSecretAccess: (expandEnvironment, expandSecretPath, expandSecretKey, expandSecretTags) => { + const canDescribe = permission.can( + ProjectPermissionSecretActions.DescribeSecret, subject(ProjectPermissionSub.Secrets, { environment: expandEnvironment, secretPath: expandSecretPath, secretName: expandSecretKey, secretTags: expandSecretTags }) - ) + ); + + const canReadValue = permission.can( + ProjectPermissionSecretActions.ReadValue, + subject(ProjectPermissionSub.Secrets, { + environment: expandEnvironment, + secretPath: expandSecretPath, + secretName: expandSecretKey, + secretTags: expandSecretTags + }) + ); + + return viewSecretValue ? canDescribe && canReadValue : canDescribe; + } }); return { @@ -1040,6 +1203,7 @@ export const secretV2BridgeServiceFactory = ({ type, secretName, version, + viewSecretValue, includeImports, expandSecretReferences: shouldExpandSecretReferences }: TGetASecretDTO) => { @@ -1101,7 +1265,7 @@ export const secretV2BridgeServiceFactory = ({ )); ForbiddenError.from(permission).throwUnlessCan( - ProjectPermissionActions.Read, + ProjectPermissionSecretActions.DescribeSecret, subject(ProjectPermissionSub.Secrets, { environment, secretPath: path, @@ -1110,56 +1274,93 @@ export const secretV2BridgeServiceFactory = ({ }) ); + // this will throw if the user doesn't have read value permission no matter what + // because if its an expansion, it will fully depend on the value. const { expandSecretReferences } = expandSecretReferencesFactory({ projectId, folderDAL, secretDAL, decryptSecretValue: (value) => (value ? secretManagerDecryptor({ cipherTextBlob: value }).toString() : undefined), - canExpandValue: (expandEnvironment, expandSecretPath, expandSecretKey, expandSecretTags) => - permission.can( - ProjectPermissionActions.Read, + canExpandValue: (expandEnvironment, expandSecretPath, expandSecretKey, expandSecretTags) => { + return permission.can( + ProjectPermissionSecretActions.ReadValue, subject(ProjectPermissionSub.Secrets, { environment: expandEnvironment, secretPath: expandSecretPath, secretName: expandSecretKey, secretTags: expandSecretTags }) - ) + ); + } }); // now if secret is not found // then search for imported secrets // here we consider the import order also thus starting from bottom + + // currently filters out the secrets that the user doesn't have access to read value on if (!secret && includeImports) { const secretImports = await secretImportDAL.find({ folderId, isReplication: false }); const importedSecrets = await fnSecretsV2FromImports({ secretImports, + viewSecretValue, secretDAL, folderDAL, secretImportDAL, decryptor: (value) => (value ? secretManagerDecryptor({ cipherTextBlob: value }).toString() : ""), expandSecretReferences: shouldExpandSecretReferences ? expandSecretReferences : undefined, - hasSecretAccess: (expandEnvironment, expandSecretPath, expandSecretKey, expandSecretTags) => - permission.can( - ProjectPermissionActions.Read, + hasSecretAccess: (expandEnvironment, expandSecretPath, expandSecretKey, expandSecretTags) => { + return permission.can( + ProjectPermissionSecretActions.DescribeSecret, subject(ProjectPermissionSub.Secrets, { environment: expandEnvironment, secretPath: expandSecretPath, secretName: expandSecretKey, secretTags: expandSecretTags }) - ) + ); + } }); for (let i = importedSecrets.length - 1; i >= 0; i -= 1) { for (let j = 0; j < importedSecrets[i].secrets.length; j += 1) { const importedSecret = importedSecrets[i].secrets[j]; if (secretName === importedSecret.key) { - return reshapeBridgeSecret(projectId, importedSecrets[i].environment, importedSecrets[i].secretPath, { - ...importedSecret, - value: importedSecret.secretValue || "", - comment: importedSecret.secretComment || "" - }); + let secretValueHidden = true; + + if (viewSecretValue) { + if ( + !permission.can( + ProjectPermissionSecretActions.ReadValue, + subject(ProjectPermissionSub.Secrets, { + environment: importedSecret.environment, + secretPath: importedSecrets[i].secretPath, + secretName: importedSecret.key, + secretTags: (importedSecret.secretTags || []).map((el) => el.slug) + }) + ) && + secretType !== SecretType.Personal + ) { + throw new ForbiddenRequestError({ + message: `You do not have permission to view secret import value on secret with name '${secretName}'`, + name: "ForbiddenReadSecretError" + }); + } + + secretValueHidden = false; + } + + return reshapeBridgeSecret( + projectId, + importedSecrets[i].environment, + importedSecrets[i].secretPath, + { + ...importedSecret, + value: importedSecret.secretValue || "", + comment: importedSecret.secretComment || "" + }, + secretValueHidden + ); } } } @@ -1181,13 +1382,43 @@ export const secretV2BridgeServiceFactory = ({ secretValue = expandedSecretValue || ""; } - return reshapeBridgeSecret(projectId, environment, path, { - ...secret, - value: secretValue, - comment: secret.encryptedComment - ? secretManagerDecryptor({ cipherTextBlob: secret.encryptedComment }).toString() - : "" - }); + let secretValueHidden = true; + + if (viewSecretValue) { + if ( + !permission.can( + ProjectPermissionSecretActions.ReadValue, + subject(ProjectPermissionSub.Secrets, { + environment, + secretPath: path, + secretName, + secretTags: (secret?.tags || []).map((el) => el.slug) + }) + ) && + secretType !== SecretType.Personal + ) { + throw new ForbiddenRequestError({ + message: `You do not have permission to view secret value on secret with name '${secretName}'`, + name: "ForbiddenReadSecretError" + }); + } + + secretValueHidden = false; + } + + return reshapeBridgeSecret( + projectId, + environment, + path, + { + ...secret, + value: secretValue, + comment: secret.encryptedComment + ? secretManagerDecryptor({ cipherTextBlob: secret.encryptedComment }).toString() + : "" + }, + secretValueHidden + ); }; const createManySecret = async ({ @@ -1255,7 +1486,7 @@ export const secretV2BridgeServiceFactory = ({ inputSecrets.forEach((el) => { ForbiddenError.from(permission).throwUnlessCan( - ProjectPermissionActions.Create, + ProjectPermissionSecretActions.Create, subject(ProjectPermissionSub.Secrets, { environment, secretPath, @@ -1330,13 +1561,29 @@ export const secretV2BridgeServiceFactory = ({ environmentSlug: folder.environment.slug }); - return newSecrets.map((el) => - reshapeBridgeSecret(projectId, environment, secretPath, { - ...el, - value: el.encryptedValue ? secretManagerDecryptor({ cipherTextBlob: el.encryptedValue }).toString() : "", - comment: el.encryptedComment ? secretManagerDecryptor({ cipherTextBlob: el.encryptedComment }).toString() : "" - }) - ); + return newSecrets.map((el) => { + const secretValueHidden = !permission.can( + ProjectPermissionSecretActions.ReadValue, + subject(ProjectPermissionSub.Secrets, { + environment, + secretPath, + secretName: el.key, + secretTags: el.tags?.map((i) => i.slug) + }) + ); + + return reshapeBridgeSecret( + projectId, + environment, + secretPath, + { + ...el, + value: el.encryptedValue ? secretManagerDecryptor({ cipherTextBlob: el.encryptedValue }).toString() : "", + comment: el.encryptedComment ? secretManagerDecryptor({ cipherTextBlob: el.encryptedComment }).toString() : "" + }, + secretValueHidden + ); + }); }; const updateManySecret = async ({ @@ -1379,7 +1626,17 @@ export const secretV2BridgeServiceFactory = ({ const { encryptor: secretManagerEncryptor, decryptor: secretManagerDecryptor } = await kmsService.createCipherPairWithDataKey({ type: KmsDataKey.SecretManager, projectId }); - const updatedSecrets: Array = []; + const updatedSecrets: Array< + TSecretsV2 & { + secretPath: string; + tags: { + id: string; + slug: string; + color?: string | null; + name: string; + }[]; + } + > = []; await secretDAL.transaction(async (tx) => { for await (const folder of folders) { if (!folder) throw new NotFoundError({ message: "Folder not found" }); @@ -1430,7 +1687,7 @@ export const secretV2BridgeServiceFactory = ({ secretsToUpdateInDB.forEach((el) => { ForbiddenError.from(permission).throwUnlessCan( - ProjectPermissionActions.Edit, + ProjectPermissionSecretActions.Edit, subject(ProjectPermissionSub.Secrets, { environment, secretPath, @@ -1450,7 +1707,7 @@ export const secretV2BridgeServiceFactory = ({ if (updateMode === SecretUpdateMode.Upsert) { secretsToCreate.forEach((el) => { ForbiddenError.from(permission).throwUnlessCan( - ProjectPermissionActions.Create, + ProjectPermissionSecretActions.Create, subject(ProjectPermissionSub.Secrets, { environment, secretPath, @@ -1464,7 +1721,7 @@ export const secretV2BridgeServiceFactory = ({ // check again to avoid non authorized tags are removed secretsToUpdate.forEach((el) => { ForbiddenError.from(permission).throwUnlessCan( - ProjectPermissionActions.Edit, + ProjectPermissionSecretActions.Edit, subject(ProjectPermissionSub.Secrets, { environment, secretPath, @@ -1516,7 +1773,7 @@ export const secretV2BridgeServiceFactory = ({ secretsWithNewName.forEach((el) => { ForbiddenError.from(permission).throwUnlessCan( - ProjectPermissionActions.Create, + ProjectPermissionSecretActions.Create, subject(ProjectPermissionSub.Secrets, { environment, secretPath, @@ -1582,6 +1839,7 @@ export const secretV2BridgeServiceFactory = ({ }, resourceMetadataDAL }); + updatedSecrets.push(...bulkUpdatedSecrets.map((el) => ({ ...el, secretPath: folder.path }))); if (updateMode === SecretUpdateMode.Upsert) { const bulkInsertedSecrets = await fnSecretBulkInsert({ @@ -1618,6 +1876,7 @@ export const secretV2BridgeServiceFactory = ({ }, tx }); + updatedSecrets.push(...bulkInsertedSecrets.map((el) => ({ ...el, secretPath: folder.path }))); } } @@ -1639,13 +1898,33 @@ export const secretV2BridgeServiceFactory = ({ ) ); - return updatedSecrets.map((el) => - reshapeBridgeSecret(projectId, environment, el.secretPath, { - ...el, - value: el.encryptedValue ? secretManagerDecryptor({ cipherTextBlob: el.encryptedValue }).toString() : "", - comment: el.encryptedComment ? secretManagerDecryptor({ cipherTextBlob: el.encryptedComment }).toString() : "" - }) - ); + return updatedSecrets.map((el) => { + const secretValueHidden = !permission.can( + ProjectPermissionSecretActions.ReadValue, + subject(ProjectPermissionSub.Secrets, { + environment, + secretPath: el.secretPath, + secretName: el.key, + secretTags: el.tags.map((i) => i.slug) + }) + ); + + return { + ...reshapeBridgeSecret( + projectId, + environment, + el.secretPath, + { + ...el, + value: el.encryptedValue ? secretManagerDecryptor({ cipherTextBlob: el.encryptedValue }).toString() : "", + comment: el.encryptedComment + ? secretManagerDecryptor({ cipherTextBlob: el.encryptedComment }).toString() + : "" + }, + secretValueHidden + ) + }; + }); }; const deleteManySecret = async ({ @@ -1707,7 +1986,7 @@ export const secretV2BridgeServiceFactory = ({ }); secretsToDelete.forEach((el) => { ForbiddenError.from(permission).throwUnlessCan( - ProjectPermissionActions.Delete, + ProjectPermissionSecretActions.Delete, subject(ProjectPermissionSub.Secrets, { environment, secretPath, @@ -1746,13 +2025,35 @@ export const secretV2BridgeServiceFactory = ({ type: KmsDataKey.SecretManager, projectId }); - return secretsDeleted.map((el) => - reshapeBridgeSecret(projectId, environment, secretPath, { - ...el, - value: el.encryptedValue ? secretManagerDecryptor({ cipherTextBlob: el.encryptedValue }).toString() : "", - comment: el.encryptedComment ? secretManagerDecryptor({ cipherTextBlob: el.encryptedComment }).toString() : "" - }) - ); + return secretsDeleted.map((el) => { + const secretToDeleteMatch = secretsToDelete.find( + (i) => i.key === el.key && (i.type || SecretType.Shared) === el.type + ); + + const secretValueHidden = + !secretToDeleteMatch || + !permission.can( + ProjectPermissionSecretActions.ReadValue, + subject(ProjectPermissionSub.Secrets, { + environment, + secretPath, + secretName: el.key, + secretTags: secretToDeleteMatch.tags?.map((i) => i.slug) + }) + ); + + return reshapeBridgeSecret( + projectId, + environment, + secretPath, + { + ...el, + value: el.encryptedValue ? secretManagerDecryptor({ cipherTextBlob: el.encryptedValue }).toString() : "", + comment: el.encryptedComment ? secretManagerDecryptor({ cipherTextBlob: el.encryptedComment }).toString() : "" + }, + secretValueHidden + ); + }); }; const getSecretVersions = async ({ @@ -1765,11 +2066,18 @@ export const secretV2BridgeServiceFactory = ({ secretId }: TGetSecretVersionsDTO) => { const secret = await secretDAL.findById(secretId); + if (!secret) throw new NotFoundError({ message: `Secret with ID '${secretId}' not found` }); const folder = await folderDAL.findById(secret.folderId); if (!folder) throw new NotFoundError({ message: `Folder with ID '${secret.folderId}' not found` }); + const [folderWithPath] = await folderDAL.findSecretPathByFolderIds(folder.projectId, [folder.id]); + + if (!folderWithPath) { + throw new NotFoundError({ message: `Folder with ID '${folder.id}' not found` }); + } + const { permission } = await permissionService.getProjectPermission({ actor, actorId, @@ -1788,13 +2096,29 @@ export const secretV2BridgeServiceFactory = ({ limit, sort: [["createdAt", "desc"]] }); - return secretVersions.map((el) => { - return reshapeBridgeSecret(folder.projectId, folder.environment.envSlug, "/", { - ...el, - value: el.encryptedValue ? secretManagerDecryptor({ cipherTextBlob: el.encryptedValue }).toString() : "", - comment: el.encryptedComment ? secretManagerDecryptor({ cipherTextBlob: el.encryptedComment }).toString() : "" - }); + const secretValueHidden = permission.cannot( + ProjectPermissionSecretActions.ReadValue, + subject(ProjectPermissionSub.Secrets, { + environment: folder.environment.envSlug, + secretPath: folderWithPath.path, + secretName: el.key, + ...(el.tags?.length && { + secretTags: el.tags.map((tag) => tag.slug) + }) + }) + ); + return reshapeBridgeSecret( + folder.projectId, + folder.environment.envSlug, + folderWithPath.path, + { + ...el, + value: el.encryptedValue ? secretManagerDecryptor({ cipherTextBlob: el.encryptedValue }).toString() : "", + comment: el.encryptedComment ? secretManagerDecryptor({ cipherTextBlob: el.encryptedComment }).toString() : "" + }, + secretValueHidden + ); }); }; @@ -1890,16 +2214,26 @@ export const secretV2BridgeServiceFactory = ({ [`${TableName.SecretV2}.id` as "id"]: secretIds } }); + + const sourceActions = [ + ProjectPermissionSecretActions.Delete, + ProjectPermissionSecretActions.ReadValue, + ProjectPermissionSecretActions.DescribeSecret + ] as const; + const destinationActions = [ProjectPermissionSecretActions.Create, ProjectPermissionSecretActions.Edit] as const; + sourceSecrets.forEach((secret) => { - ForbiddenError.from(permission).throwUnlessCan( - ProjectPermissionActions.Delete, - subject(ProjectPermissionSub.Secrets, { - environment: sourceEnvironment, - secretPath: sourceSecretPath, - secretName: secret.key, - secretTags: secret.tags.map((el) => el.slug) - }) - ); + for (const sourceAction of sourceActions) { + ForbiddenError.from(permission).throwUnlessCan( + sourceAction, + subject(ProjectPermissionSub.Secrets, { + environment: sourceEnvironment, + secretPath: sourceSecretPath, + secretName: secret.key, + secretTags: secret.tags.map((el) => el.slug) + }) + ); + } }); if (sourceSecrets.length !== secretIds.length) { @@ -1974,27 +2308,17 @@ export const secretV2BridgeServiceFactory = ({ // permission check whether can create or edit the ones in the destination folder locallyCreatedSecrets.forEach((secret) => { - ForbiddenError.from(permission).throwUnlessCan( - ProjectPermissionActions.Create, - subject(ProjectPermissionSub.Secrets, { - environment: destinationEnvironment, - secretPath: destinationEnvironment, - secretName: secret.key, - secretTags: secret.tags.map((el) => el.slug) - }) - ); - }); - - locallyUpdatedSecrets.forEach((secret) => { - ForbiddenError.from(permission).throwUnlessCan( - ProjectPermissionActions.Edit, - subject(ProjectPermissionSub.Secrets, { - environment: destinationEnvironment, - secretPath: destinationEnvironment, - secretName: secret.key, - secretTags: secret.tags.map((el) => el.slug) - }) - ); + for (const destinationAction of destinationActions) { + ForbiddenError.from(permission).throwUnlessCan( + destinationAction, + subject(ProjectPermissionSub.Secrets, { + environment: destinationEnvironment, + secretPath: destinationFolder.path, + secretName: secret.key, + secretTags: secret.tags.map((el) => el.slug) + }) + ); + } }); const destinationFolderPolicy = await secretApprovalPolicyService.getSecretApprovalPolicy( @@ -2232,7 +2556,7 @@ export const secretV2BridgeServiceFactory = ({ }); ForbiddenError.from(permission).throwUnlessCan( - ProjectPermissionActions.Read, + ProjectPermissionSecretActions.DescribeSecret, subject(ProjectPermissionSub.Secrets, { environment, secretPath }) ); @@ -2256,7 +2580,7 @@ export const secretV2BridgeServiceFactory = ({ }); ForbiddenError.from(permission).throwUnlessCan( - ProjectPermissionActions.Read, + ProjectPermissionSecretActions.DescribeSecret, subject(ProjectPermissionSub.Secrets, { environment, secretPath, @@ -2265,7 +2589,7 @@ export const secretV2BridgeServiceFactory = ({ }) ); - const secretValue = secret.encryptedValue + const decryptedSecretValue = secret.encryptedValue ? secretManagerDecryptor({ cipherTextBlob: secret.encryptedValue }).toString() : ""; @@ -2276,7 +2600,7 @@ export const secretV2BridgeServiceFactory = ({ decryptSecretValue: (value) => (value ? secretManagerDecryptor({ cipherTextBlob: value }).toString() : undefined), canExpandValue: (expandEnvironment, expandSecretPath, expandSecretName, expandSecretTags) => permission.can( - ProjectPermissionActions.Read, + ProjectPermissionSecretActions.ReadValue, subject(ProjectPermissionSub.Secrets, { environment: expandEnvironment, secretPath: expandSecretPath, @@ -2286,10 +2610,26 @@ export const secretV2BridgeServiceFactory = ({ ) }); + if ( + !permission.can( + ProjectPermissionSecretActions.ReadValue, + subject(ProjectPermissionSub.Secrets, { + environment, + secretPath, + secretName, + secretTags: (secret?.tags || []).map((el) => el.slug) + }) + ) + ) { + throw new ForbiddenRequestError({ + message: `Unable to get secret reference tree for secret with key '${secretName}', because you don't have permission to view secret value.` + }); + } + const { expandedValue, stackTrace } = await getExpandedSecretStackTrace({ environment, secretPath, - value: secretValue + value: decryptedSecretValue }); return { tree: stackTrace, value: expandedValue }; diff --git a/backend/src/services/secret-v2-bridge/secret-v2-bridge-types.ts b/backend/src/services/secret-v2-bridge/secret-v2-bridge-types.ts index 22956463d..510181d0c 100644 --- a/backend/src/services/secret-v2-bridge/secret-v2-bridge-types.ts +++ b/backend/src/services/secret-v2-bridge/secret-v2-bridge-types.ts @@ -1,6 +1,7 @@ import { Knex } from "knex"; import { SecretType, TSecretsV2, TSecretsV2Insert, TSecretsV2Update } from "@app/db/schemas"; +import { ProjectPermissionSecretActions } from "@app/ee/services/permission/project-permission"; import { OrderByDirection, TProjectPermission } from "@app/lib/types"; import { TProjectDALFactory } from "@app/services/project/project-dal"; import { SecretsOrderBy } from "@app/services/secret/secret-types"; @@ -36,6 +37,8 @@ export type TGetSecretsDTO = { includeImports?: boolean; recursive?: boolean; tagSlugs?: string[]; + viewSecretValue: boolean; + throwOnMissingReadValuePermission?: boolean; metadataFilter?: { key?: string; value?: string; @@ -48,6 +51,11 @@ export type TGetSecretsDTO = { keys?: string[]; } & TProjectPermission; +export type TGetSecretsMissingReadValuePermissionDTO = Omit< + TGetSecretsDTO, + "viewSecretValue" | "recursive" | "expandSecretReferences" +>; + export type TGetASecretDTO = { secretName: string; path: string; @@ -57,6 +65,7 @@ export type TGetASecretDTO = { includeImports?: boolean; version?: number; projectId: string; + viewSecretValue: boolean; } & Omit; export type TCreateSecretDTO = TProjectPermission & { @@ -164,9 +173,9 @@ export type TFnSecretBulkInsert = { } >; resourceMetadataDAL: Pick; - secretDAL: Pick; + secretDAL: Pick; secretVersionDAL: Pick; - secretTagDAL: Pick; + secretTagDAL: Pick; secretVersionTagDAL: Pick; actor?: { type: string; @@ -192,9 +201,9 @@ export type TFnSecretBulkUpdate = { data: TRequireReferenceIfValue & { tags?: string[]; secretMetadata?: ResourceMetadataDTO }; }[]; resourceMetadataDAL: Pick; - secretDAL: Pick; + secretDAL: Pick; secretVersionDAL: Pick; - secretTagDAL: Pick; + secretTagDAL: Pick; secretVersionTagDAL: Pick; actor?: { type: string; @@ -340,4 +349,5 @@ export type TGetSecretsRawByFolderMappingsDTO = { folderMappings: { folderId: string; path: string; environment: string }[]; userId: string; filters: TFindSecretsByFolderIdsFilter; + filterByAction?: ProjectPermissionSecretActions; }; diff --git a/backend/src/services/secret-v2-bridge/secret-version-dal.ts b/backend/src/services/secret-v2-bridge/secret-version-dal.ts index d06aa1472..12c54cd7c 100644 --- a/backend/src/services/secret-v2-bridge/secret-version-dal.ts +++ b/backend/src/services/secret-v2-bridge/secret-version-dal.ts @@ -2,9 +2,9 @@ import { Knex } from "knex"; import { TDbClient } from "@app/db"; -import { TableName, TSecretVersionsV2, TSecretVersionsV2Update } from "@app/db/schemas"; +import { SecretVersionsV2Schema, TableName, TSecretVersionsV2, TSecretVersionsV2Update } from "@app/db/schemas"; import { BadRequestError, DatabaseError } from "@app/lib/errors"; -import { ormify, selectAllTableCols, TFindOpt } from "@app/lib/knex"; +import { ormify, selectAllTableCols, sqlNestRelationships, TFindOpt } from "@app/lib/knex"; import { logger } from "@app/lib/logger"; import { QueueName } from "@app/queue"; @@ -13,6 +13,58 @@ export type TSecretVersionV2DALFactory = ReturnType { const secretVersionV2Orm = ormify(db, TableName.SecretVersionV2); + const findBySecretId = async (secretId: string, { offset, limit, sort, tx }: TFindOpt = {}) => { + try { + const query = (tx || db.replicaNode())(TableName.SecretVersionV2) + .where(`${TableName.SecretVersionV2}.secretId`, secretId) + .leftJoin(TableName.SecretV2, `${TableName.SecretVersionV2}.secretId`, `${TableName.SecretV2}.id`) + .leftJoin( + TableName.SecretV2JnTag, + `${TableName.SecretV2}.id`, + `${TableName.SecretV2JnTag}.${TableName.SecretV2}Id` + ) + .leftJoin( + TableName.SecretTag, + `${TableName.SecretV2JnTag}.${TableName.SecretTag}Id`, + `${TableName.SecretTag}.id` + ) + .select(selectAllTableCols(TableName.SecretVersionV2)) + .select(db.ref("id").withSchema(TableName.SecretTag).as("tagId")) + .select(db.ref("color").withSchema(TableName.SecretTag).as("tagColor")) + .select(db.ref("slug").withSchema(TableName.SecretTag).as("tagSlug")); + + if (limit) void query.limit(limit); + if (offset) void query.offset(offset); + if (sort) { + void query.orderBy(sort.map(([column, order, nulls]) => ({ column: column as string, order, nulls }))); + } + + const docs = await query; + + const data = sqlNestRelationships({ + data: docs, + key: "id", + parentMapper: (el) => ({ _id: el.id, ...SecretVersionsV2Schema.parse(el) }), + childrenMapper: [ + { + key: "tagId", + label: "tags" as const, + mapper: ({ tagId: id, tagColor: color, tagSlug: slug }) => ({ + id, + color, + slug, + name: slug + }) + } + ] + }); + + return data; + } catch (error) { + throw new DatabaseError({ error, name: `${TableName.SecretVersionV2}: FindBySecretId` }); + } + }; + // This will fetch all latest secret versions from a folder const findLatestVersionByFolderId = async (folderId: string, tx?: Knex) => { try { @@ -181,6 +233,7 @@ export const secretVersionV2BridgeDALFactory = (db: TDbClient) => { findLatestVersionMany, bulkUpdate, findLatestVersionByFolderId, - findVersionsBySecretIdWithActors + findVersionsBySecretIdWithActors, + findBySecretId }; }; diff --git a/backend/src/services/secret/secret-dal.ts b/backend/src/services/secret/secret-dal.ts index cbaf7ddcd..dc41d129f 100644 --- a/backend/src/services/secret/secret-dal.ts +++ b/backend/src/services/secret/secret-dal.ts @@ -169,6 +169,48 @@ export const secretDALFactory = (db: TDbClient) => { } }; + const findManySecretsWithTags = async ( + filter: { + secretIds: string[]; + type: SecretType; + }, + tx?: Knex + ) => { + try { + const secrets = await (tx || db.replicaNode())(TableName.Secret) + .whereIn(`${TableName.Secret}.id` as "id", filter.secretIds) + .where("type", filter.type) + .leftJoin(TableName.JnSecretTag, `${TableName.Secret}.id`, `${TableName.JnSecretTag}.${TableName.Secret}Id`) + .leftJoin(TableName.SecretTag, `${TableName.JnSecretTag}.${TableName.SecretTag}Id`, `${TableName.SecretTag}.id`) + .select(selectAllTableCols(TableName.Secret)) + .select(db.ref("id").withSchema(TableName.SecretTag).as("tagId")) + .select(db.ref("color").withSchema(TableName.SecretTag).as("tagColor")) + .select(db.ref("slug").withSchema(TableName.SecretTag).as("tagSlug")); + + const data = sqlNestRelationships({ + data: secrets, + key: "id", + parentMapper: (el) => ({ _id: el.id, ...SecretsSchema.parse(el) }), + childrenMapper: [ + { + key: "tagId", + label: "tags" as const, + mapper: ({ tagId: id, tagColor: color, tagSlug: slug }) => ({ + id, + color, + slug, + name: slug + }) + } + ] + }); + + return data; + } catch (error) { + throw new DatabaseError({ error, name: "get many secrets with tags" }); + } + }; + const findByFolderIds = async (folderIds: string[], userId?: string, tx?: Knex) => { try { // check if not uui then userId id is null (corner case because service token's ID is not UUI in effort to keep backwards compatibility from mongo) @@ -443,6 +485,7 @@ export const secretDALFactory = (db: TDbClient) => { upsertSecretReferences, findReferencedSecretReferences, findAllProjectSecretValues, - pruneSecretReminders + pruneSecretReminders, + findManySecretsWithTags }; }; diff --git a/backend/src/services/secret/secret-fns.ts b/backend/src/services/secret/secret-fns.ts index d7c9b86fb..4556528a8 100644 --- a/backend/src/services/secret/secret-fns.ts +++ b/backend/src/services/secret/secret-fns.ts @@ -13,7 +13,7 @@ import { TSecrets } from "@app/db/schemas"; import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service"; -import { ProjectPermissionActions, ProjectPermissionSub } from "@app/ee/services/permission/project-permission"; +import { ProjectPermissionSecretActions, ProjectPermissionSub } from "@app/ee/services/permission/project-permission"; import { getConfig } from "@app/lib/config/env"; import { buildSecretBlindIndexFromName, @@ -51,6 +51,8 @@ import { TUpdateManySecretsRawFnFactory } from "./secret-types"; +export const INFISICAL_SECRET_VALUE_HIDDEN_MASK = ""; + export const generateSecretBlindIndexBySalt = async (secretName: string, secretBlindIndexDoc: TSecretBlindIndexes) => { const appCfg = getConfig(); const secretBlindIndex = await buildSecretBlindIndexFromName({ @@ -190,7 +192,7 @@ export const recursivelyGetSecretPaths = ({ const allowedPaths = paths.filter( (folder) => permission.can( - ProjectPermissionActions.Read, + ProjectPermissionSecretActions.ReadValue, subject(ProjectPermissionSub.Secrets, { environment, secretPath: folder.path @@ -344,6 +346,7 @@ export const interpolateSecrets = ({ projectId, secretEncKey, secretDAL, folderD export const decryptSecretRaw = ( secret: TSecrets & { + secretValueHidden: boolean; workspace: string; environment: string; secretPath: string; @@ -362,12 +365,14 @@ export const decryptSecretRaw = ( key }); - const secretValue = decryptSymmetric128BitHexKeyUTF8({ - ciphertext: secret.secretValueCiphertext, - iv: secret.secretValueIV, - tag: secret.secretValueTag, - key - }); + const secretValue = !secret.secretValueHidden + ? decryptSymmetric128BitHexKeyUTF8({ + ciphertext: secret.secretValueCiphertext, + iv: secret.secretValueIV, + tag: secret.secretValueTag, + key + }) + : INFISICAL_SECRET_VALUE_HIDDEN_MASK; let secretComment = ""; @@ -385,6 +390,7 @@ export const decryptSecretRaw = ( secretPath: secret.secretPath, workspace: secret.workspace, environment: secret.environment, + secretValueHidden: secret.secretValueHidden, secretValue, secretComment, version: secret.version, @@ -1198,3 +1204,23 @@ export const fnDeleteProjectSecretReminders = async ( } } }; + +export const conditionallyHideSecretValue = ( + shouldHideValue: boolean, + { + secretValueCiphertext, + secretValueIV, + secretValueTag + }: { + secretValueCiphertext: string; + secretValueIV: string; + secretValueTag: string; + } +) => { + return { + secretValueCiphertext: shouldHideValue ? INFISICAL_SECRET_VALUE_HIDDEN_MASK : secretValueCiphertext, + secretValueIV: shouldHideValue ? INFISICAL_SECRET_VALUE_HIDDEN_MASK : secretValueIV, + secretValueTag: shouldHideValue ? INFISICAL_SECRET_VALUE_HIDDEN_MASK : secretValueTag, + secretValueHidden: shouldHideValue + }; +}; diff --git a/backend/src/services/secret/secret-queue.ts b/backend/src/services/secret/secret-queue.ts index c84fe5ae0..76a486b46 100644 --- a/backend/src/services/secret/secret-queue.ts +++ b/backend/src/services/secret/secret-queue.ts @@ -403,7 +403,8 @@ export const secretQueueFactory = ({ expandSecretReferences, secretImportDAL, secretImports, - hasSecretAccess: () => true + hasSecretAccess: () => true, + viewSecretValue: true }); for (let i = importedSecrets.length - 1; i >= 0; i -= 1) { diff --git a/backend/src/services/secret/secret-service.ts b/backend/src/services/secret/secret-service.ts index cfb47d1dd..8af0deb2b 100644 --- a/backend/src/services/secret/secret-service.ts +++ b/backend/src/services/secret/secret-service.ts @@ -6,6 +6,7 @@ import { ActionProjectType, ProjectMembershipRole, ProjectUpgradeStatus, + ProjectVersion, SecretEncryptionAlgo, SecretKeyEncoding, SecretsSchema, @@ -13,7 +14,11 @@ import { } from "@app/db/schemas"; import { TLicenseServiceFactory } from "@app/ee/services/license/license-service"; import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service"; -import { ProjectPermissionActions, ProjectPermissionSub } from "@app/ee/services/permission/project-permission"; +import { + ProjectPermissionActions, + ProjectPermissionSecretActions, + ProjectPermissionSub +} from "@app/ee/services/permission/project-permission"; import { TSecretApprovalPolicyServiceFactory } from "@app/ee/services/secret-approval-policy/secret-approval-policy-service"; import { TSecretApprovalRequestDALFactory } from "@app/ee/services/secret-approval-request/secret-approval-request-dal"; import { TSecretApprovalRequestSecretDALFactory } from "@app/ee/services/secret-approval-request/secret-approval-request-secret-dal"; @@ -48,6 +53,7 @@ import { TSecretV2BridgeServiceFactory } from "../secret-v2-bridge/secret-v2-bri import { TGetSecretReferencesTreeDTO } from "../secret-v2-bridge/secret-v2-bridge-types"; import { TSecretDALFactory } from "./secret-dal"; import { + conditionallyHideSecretValue, decryptSecretRaw, fnSecretBlindIndexCheck, fnSecretBulkDelete, @@ -205,7 +211,7 @@ export const secretServiceFactory = ({ }); ForbiddenError.from(permission).throwUnlessCan( - ProjectPermissionActions.Create, + ProjectPermissionSecretActions.Create, subject(ProjectPermissionSub.Secrets, { environment, secretPath: path }) ); @@ -323,7 +329,7 @@ export const secretServiceFactory = ({ }); ForbiddenError.from(permission).throwUnlessCan( - ProjectPermissionActions.Edit, + ProjectPermissionSecretActions.Edit, subject(ProjectPermissionSub.Secrets, { environment, secretPath: path }) ); @@ -445,7 +451,22 @@ export const secretServiceFactory = ({ environmentSlug: folder.environment.slug }); } - return { ...updatedSecret[0], workspace: projectId, environment, secretPath: path }; + + const secretValueHidden = !permission.can( + ProjectPermissionSecretActions.ReadValue, + subject(ProjectPermissionSub.Secrets, { + environment, + secretPath: path + }) + ); + + return { + ...updatedSecret[0], + ...conditionallyHideSecretValue(secretValueHidden, updatedSecret[0]), + workspace: projectId, + environment, + secretPath: path + }; }; const deleteSecret = async ({ @@ -468,7 +489,7 @@ export const secretServiceFactory = ({ }); ForbiddenError.from(permission).throwUnlessCan( - ProjectPermissionActions.Delete, + ProjectPermissionSecretActions.Delete, subject(ProjectPermissionSub.Secrets, { environment, secretPath: path }) ); @@ -541,7 +562,19 @@ export const secretServiceFactory = ({ }); } - return { ...deletedSecret[0], _id: deletedSecret[0].id, workspace: projectId, environment, secretPath: path }; + const secretValueHidden = !permission.can( + ProjectPermissionSecretActions.ReadValue, + subject(ProjectPermissionSub.Secrets, { environment, secretPath: path }) + ); + + return { + ...deletedSecret[0], + ...conditionallyHideSecretValue(secretValueHidden, deletedSecret[0]), + _id: deletedSecret[0].id, + workspace: projectId, + environment, + secretPath: path + }; }; const getSecrets = async ({ @@ -590,7 +623,7 @@ export const secretServiceFactory = ({ paths = deepPaths.map(({ folderId, path: p }) => ({ folderId, path: p })); } else { ForbiddenError.from(permission).throwUnlessCan( - ProjectPermissionActions.Read, + ProjectPermissionSecretActions.ReadValue, subject(ProjectPermissionSub.Secrets, { environment, secretPath: path }) ); @@ -615,7 +648,7 @@ export const secretServiceFactory = ({ actor === ActorType.SERVICE ? true : permission.can( - ProjectPermissionActions.Read, + ProjectPermissionSecretActions.ReadValue, subject(ProjectPermissionSub.Secrets, { environment: importEnv.slug, secretPath: importPath @@ -672,7 +705,7 @@ export const secretServiceFactory = ({ actionProjectType: ActionProjectType.SecretManager }); ForbiddenError.from(permission).throwUnlessCan( - ProjectPermissionActions.Read, + ProjectPermissionSecretActions.ReadValue, subject(ProjectPermissionSub.Secrets, { environment, secretPath: path }) ); const folder = await folderDAL.findBySecretPath(projectId, environment, path); @@ -722,7 +755,7 @@ export const secretServiceFactory = ({ actor === ActorType.SERVICE ? true : permission.can( - ProjectPermissionActions.Read, + ProjectPermissionSecretActions.ReadValue, subject(ProjectPermissionSub.Secrets, { environment: importEnv.slug, secretPath: importPath @@ -740,6 +773,7 @@ export const secretServiceFactory = ({ if (secretBlindIndex === importedSecrets[i].secrets[j].secretBlindIndex) { return { ...importedSecrets[i].secrets[j], + secretValueHidden: false, workspace: projectId, environment: importedSecrets[i].environment, secretPath: importedSecrets[i].secretPath @@ -750,7 +784,13 @@ export const secretServiceFactory = ({ } if (!secret) throw new NotFoundError({ message: `Secret with name '${secretName}' not found` }); - return { ...secret, workspace: projectId, environment, secretPath: path }; + return { + ...secret, + secretValueHidden: false, // Always false because we check permission at the beginning of the function + workspace: projectId, + environment, + secretPath: path + }; }; const createManySecret = async ({ @@ -772,7 +812,7 @@ export const secretServiceFactory = ({ actionProjectType: ActionProjectType.SecretManager }); ForbiddenError.from(permission).throwUnlessCan( - ProjectPermissionActions.Create, + ProjectPermissionSecretActions.Create, subject(ProjectPermissionSub.Secrets, { environment, secretPath: path }) ); @@ -860,7 +900,7 @@ export const secretServiceFactory = ({ }); ForbiddenError.from(permission).throwUnlessCan( - ProjectPermissionActions.Edit, + ProjectPermissionSecretActions.Edit, subject(ProjectPermissionSub.Secrets, { environment, secretPath: path }) ); @@ -902,8 +942,8 @@ export const secretServiceFactory = ({ if (tagIds.length !== tags.length) throw new NotFoundError({ message: "One or more tags not found" }); const references = await getSecretReference(projectId); - const secrets = await secretDAL.transaction(async (tx) => - fnSecretBulkUpdate({ + const secrets = await secretDAL.transaction(async (tx) => { + const updatedSecrets = await fnSecretBulkUpdate({ folderId, projectId, tx, @@ -933,8 +973,18 @@ export const secretServiceFactory = ({ secretVersionDAL, secretTagDAL, secretVersionTagDAL - }) - ); + }); + + const secretValueHidden = !permission.can( + ProjectPermissionSecretActions.ReadValue, + subject(ProjectPermissionSub.Secrets, { environment, secretPath: path }) + ); + + return updatedSecrets.map((secret) => ({ + ...secret, + ...conditionallyHideSecretValue(secretValueHidden, secret) + })); + }); await snapshotService.performSnapshot(folderId); await secretQueueService.syncSecrets({ @@ -968,7 +1018,7 @@ export const secretServiceFactory = ({ actionProjectType: ActionProjectType.SecretManager }); ForbiddenError.from(permission).throwUnlessCan( - ProjectPermissionActions.Delete, + ProjectPermissionSecretActions.Delete, subject(ProjectPermissionSub.Secrets, { environment, secretPath: path }) ); @@ -1020,7 +1070,15 @@ export const secretServiceFactory = ({ } } - return secrets; + const secretValueHidden = !permission.can( + ProjectPermissionSecretActions.ReadValue, + subject(ProjectPermissionSub.Secrets, { environment, secretPath: path }) + ); + + return secrets.map((secret) => ({ + ...secret, + ...conditionallyHideSecretValue(secretValueHidden, secret) + })); }); await snapshotService.performSnapshot(folderId); @@ -1181,6 +1239,7 @@ export const secretServiceFactory = ({ secretName, path: secretPath, environment, + viewSecretValue: false, type: "shared" }); @@ -1195,10 +1254,11 @@ export const secretServiceFactory = ({ | (typeof groupPermissions)[number] ) => { const allowedActions = [ - ProjectPermissionActions.Read, - ProjectPermissionActions.Delete, - ProjectPermissionActions.Create, - ProjectPermissionActions.Edit + ProjectPermissionSecretActions.DescribeSecret, + ProjectPermissionSecretActions.ReadValue, + ProjectPermissionSecretActions.Delete, + ProjectPermissionSecretActions.Create, + ProjectPermissionSecretActions.Edit ].filter((action) => entityPermission.permission.can( action, @@ -1235,11 +1295,13 @@ export const secretServiceFactory = ({ actorId, actorOrgId, actorAuthMethod, + viewSecretValue, environment, includeImports, expandSecretReferences, recursive, tagSlugs = [], + throwOnMissingReadValuePermission = true, ...paramsV2 }: TGetSecretsRawDTO) => { const { botKey, shouldUseSecretV2Bridge } = await projectBotService.getBotKey(projectId); @@ -1250,6 +1312,8 @@ export const secretServiceFactory = ({ actorId, actor, actorOrgId, + viewSecretValue, + throwOnMissingReadValuePermission, environment, path, recursive, @@ -1258,6 +1322,7 @@ export const secretServiceFactory = ({ tagSlugs, ...paramsV2 }); + return { secrets, imports }; } @@ -1286,14 +1351,20 @@ export const secretServiceFactory = ({ recursive }); - const decryptedSecrets = secrets.map((el) => decryptSecretRaw(el, botKey)); + const decryptedSecrets = secrets.map((el) => decryptSecretRaw({ ...el, secretValueHidden: false }, botKey)); const filteredSecrets = tagSlugs.length ? decryptedSecrets.filter((secret) => Boolean(secret.tags?.find((el) => tagSlugs.includes(el.slug)))) : decryptedSecrets; const processedImports = (imports || [])?.map(({ secrets: importedSecrets, ...el }) => { const decryptedImportSecrets = importedSecrets.map((sec) => decryptSecretRaw( - { ...sec, environment: el.environment, workspace: projectId, secretPath: el.secretPath }, + { + ...sec, + environment: el.environment, + workspace: projectId, + secretPath: el.secretPath, + secretValueHidden: false + }, botKey ) ); @@ -1304,6 +1375,7 @@ export const secretServiceFactory = ({ const importedEntries = decryptedImportSecrets.reduce( ( accum: { + secretValueHidden: boolean; secretKey: string; secretPath: string; workspace: string; @@ -1347,6 +1419,7 @@ export const secretServiceFactory = ({ Object.keys(secretsGroupByPath).map((groupedPath) => Promise.allSettled( secretsGroupByPath[groupedPath].map(async (decryptedSecret, index) => { + if (decryptedSecret.secretValueHidden) return; const expandedSecretValue = await expandSecret({ value: decryptedSecret.secretValue, secretPath: groupedPath, @@ -1363,6 +1436,7 @@ export const secretServiceFactory = ({ processedImports.map((processedImport) => Promise.allSettled( processedImport.secrets.map(async (decryptedSecret, index) => { + if (decryptedSecret.secretValueHidden) return; const expandedSecretValue = await expandSecret({ value: decryptedSecret.secretValue, secretPath: path, @@ -1400,6 +1474,7 @@ export const secretServiceFactory = ({ path, actor, environment, + viewSecretValue, projectId: workspaceId, expandSecretReferences, projectSlug, @@ -1419,6 +1494,7 @@ export const secretServiceFactory = ({ includeImports, actorAuthMethod, path, + viewSecretValue, actorOrgId, actor, actorId, @@ -1449,6 +1525,7 @@ export const secretServiceFactory = ({ message: `Project bot for project with ID '${projectId}' not found. Please upgrade your project.`, name: "bot_not_found_error" }); + const decryptedSecret = decryptSecretRaw(encryptedSecret, botKey); if (expandSecretReferences) { @@ -1467,7 +1544,10 @@ export const secretServiceFactory = ({ decryptedSecret.secretValue = expandedSecretValue || ""; } - return { secretMetadata: undefined, ...decryptedSecret }; + return { + secretMetadata: undefined, + ...decryptedSecret + }; }; const createSecretRaw = async ({ @@ -1618,7 +1698,16 @@ export const secretServiceFactory = ({ tags: tagIds }); - return { type: SecretProtectionType.Direct as const, secret: decryptSecretRaw(secret, botKey) }; + return { + type: SecretProtectionType.Direct as const, + secret: decryptSecretRaw( + { + ...secret, + secretValueHidden: false + }, + botKey + ) + }; }; const updateSecretRaw = async ({ @@ -2014,7 +2103,7 @@ export const secretServiceFactory = ({ return { type: SecretProtectionType.Direct as const, secrets: secrets.map((secret) => - decryptSecretRaw({ ...secret, workspace: projectId, environment, secretPath }, botKey) + decryptSecretRaw({ ...secret, workspace: projectId, environment, secretPath, secretValueHidden: false }, botKey) ) }; }; @@ -2303,6 +2392,12 @@ export const secretServiceFactory = ({ const folder = await folderDAL.findById(secret.folderId); if (!folder) throw new NotFoundError({ message: `Folder with ID '${secret.folderId}' not found` }); + const [folderWithPath] = await folderDAL.findSecretPathByFolderIds(folder.projectId, [folder.id]); + + if (!folderWithPath) { + throw new NotFoundError({ message: `Folder with ID '${folder.id}' not found` }); + } + const { botKey } = await projectBotService.getBotKey(folder.projectId); if (!botKey) throw new NotFoundError({ message: `Project bot for project with ID '${folder.projectId}' not found` }); @@ -2316,18 +2411,42 @@ export const secretServiceFactory = ({ actionProjectType: ActionProjectType.SecretManager }); ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.SecretRollback); - const secretVersions = await secretVersionDAL.find({ secretId }, { offset, limit, sort: [["createdAt", "desc"]] }); - return secretVersions.map((el) => - decryptSecretRaw( + const secretVersions = await secretVersionDAL.findBySecretId(secretId, { + offset, + limit, + sort: [["createdAt", "desc"]] + }); + return secretVersions.map((el) => { + const secretKey = decryptSymmetric128BitHexKeyUTF8({ + ciphertext: secret.secretKeyCiphertext, + iv: secret.secretKeyIV, + tag: secret.secretKeyTag, + key: botKey + }); + + const secretValueHidden = permission.cannot( + ProjectPermissionSecretActions.ReadValue, + subject(ProjectPermissionSub.Secrets, { + environment: folder.environment.envSlug, + secretPath: folderWithPath.path, + secretName: secretKey, + ...(el.tags?.length && { + secretTags: el.tags.map((tag) => tag.slug) + }) + }) + ); + + return decryptSecretRaw( { + secretValueHidden, ...el, workspace: folder.projectId, environment: folder.environment.envSlug, - secretPath: "/" + secretPath: folderWithPath.path }, botKey - ) - ); + ); + }); }; const attachTags = async ({ @@ -2353,7 +2472,7 @@ export const secretServiceFactory = ({ }); ForbiddenError.from(permission).throwUnlessCan( - ProjectPermissionActions.Edit, + ProjectPermissionSecretActions.Edit, subject(ProjectPermissionSub.Secrets, { environment, secretPath }) ); @@ -2459,7 +2578,7 @@ export const secretServiceFactory = ({ }); ForbiddenError.from(permission).throwUnlessCan( - ProjectPermissionActions.Edit, + ProjectPermissionSecretActions.Edit, subject(ProjectPermissionSub.Secrets, { environment, secretPath }) ); @@ -2625,7 +2744,7 @@ export const secretServiceFactory = ({ message: `Project with slug '${projectSlug}' not found` }); } - if (project.version === 3) { + if (project.version === ProjectVersion.V3) { return secretV2BridgeService.moveSecrets({ sourceEnvironment, sourceSecretPath, @@ -2650,30 +2769,6 @@ export const secretServiceFactory = ({ actionProjectType: ActionProjectType.SecretManager }); - ForbiddenError.from(permission).throwUnlessCan( - ProjectPermissionActions.Delete, - subject(ProjectPermissionSub.Secrets, { - environment: sourceEnvironment, - secretPath: sourceSecretPath - }) - ); - - ForbiddenError.from(permission).throwUnlessCan( - ProjectPermissionActions.Create, - subject(ProjectPermissionSub.Secrets, { - environment: destinationEnvironment, - secretPath: destinationSecretPath - }) - ); - - ForbiddenError.from(permission).throwUnlessCan( - ProjectPermissionActions.Edit, - subject(ProjectPermissionSub.Secrets, { - environment: destinationEnvironment, - secretPath: destinationSecretPath - }) - ); - const { botKey } = await projectBotService.getBotKey(project.id); if (!botKey) { throw new NotFoundError({ @@ -2701,11 +2796,9 @@ export const secretServiceFactory = ({ }); } - const sourceSecrets = await secretDAL.find({ + const sourceSecrets = await secretDAL.findManySecretsWithTags({ type: SecretType.Shared, - $in: { - id: secretIds - } + secretIds }); if (sourceSecrets.length !== secretIds.length) { @@ -2714,21 +2807,52 @@ export const secretServiceFactory = ({ }); } - const decryptedSourceSecrets = sourceSecrets.map((secret) => ({ - ...secret, - secretKey: decryptSymmetric128BitHexKeyUTF8({ + const sourceActions = [ + ProjectPermissionSecretActions.Delete, + ProjectPermissionSecretActions.DescribeSecret, + ProjectPermissionSecretActions.ReadValue + ] as const; + const destinationActions = [ProjectPermissionSecretActions.Create, ProjectPermissionSecretActions.Edit] as const; + + const decryptedSourceSecrets = sourceSecrets.map((secret) => { + const secretKey = decryptSymmetric128BitHexKeyUTF8({ ciphertext: secret.secretKeyCiphertext, iv: secret.secretKeyIV, tag: secret.secretKeyTag, key: botKey - }), - secretValue: decryptSymmetric128BitHexKeyUTF8({ - ciphertext: secret.secretValueCiphertext, - iv: secret.secretValueIV, - tag: secret.secretValueTag, - key: botKey - }) - })); + }); + + for (const destinationAction of destinationActions) { + ForbiddenError.from(permission).throwUnlessCan( + destinationAction, + subject(ProjectPermissionSub.Secrets, { + environment: destinationEnvironment, + secretPath: destinationSecretPath + }) + ); + } + + for (const sourceAction of sourceActions) { + ForbiddenError.from(permission).throwUnlessCan( + sourceAction, + subject(ProjectPermissionSub.Secrets, { + environment: sourceEnvironment, + secretPath: sourceSecretPath + }) + ); + } + + return { + ...secret, + secretKey, + secretValue: decryptSymmetric128BitHexKeyUTF8({ + ciphertext: secret.secretValueCiphertext, + iv: secret.secretValueIV, + tag: secret.secretValueTag, + key: botKey + }) + }; + }); let isSourceUpdated = false; let isDestinationUpdated = false; diff --git a/backend/src/services/secret/secret-types.ts b/backend/src/services/secret/secret-types.ts index 46aedc2ee..c7b8a067d 100644 --- a/backend/src/services/secret/secret-types.ts +++ b/backend/src/services/secret/secret-types.ts @@ -184,6 +184,8 @@ export type TGetSecretsRawDTO = { expandSecretReferences?: boolean; path: string; environment: string; + viewSecretValue: boolean; + throwOnMissingReadValuePermission?: boolean; includeImports?: boolean; recursive?: boolean; tagSlugs?: string[]; @@ -209,6 +211,7 @@ export type TGetASecretRawDTO = { secretName: string; path: string; environment: string; + viewSecretValue: boolean; expandSecretReferences?: boolean; type: "shared" | "personal"; includeImports?: boolean; @@ -417,7 +420,7 @@ export type TCreateManySecretsRawFnFactory = { kmsService: Pick; secretV2BridgeDAL: Pick< TSecretV2BridgeDALFactory, - "insertMany" | "upsertSecretReferences" | "findBySecretKeys" | "bulkUpdate" | "deleteMany" + "insertMany" | "upsertSecretReferences" | "findBySecretKeys" | "bulkUpdate" | "deleteMany" | "find" >; secretVersionV2BridgeDAL: Pick; secretVersionTagV2BridgeDAL: Pick; @@ -454,7 +457,7 @@ export type TUpdateManySecretsRawFnFactory = { kmsService: Pick; secretV2BridgeDAL: Pick< TSecretV2BridgeDALFactory, - "insertMany" | "upsertSecretReferences" | "findBySecretKeys" | "bulkUpdate" | "deleteMany" + "insertMany" | "upsertSecretReferences" | "findBySecretKeys" | "bulkUpdate" | "deleteMany" | "find" >; secretVersionV2BridgeDAL: Pick; secretVersionTagV2BridgeDAL: Pick; diff --git a/backend/src/services/secret/secret-version-dal.ts b/backend/src/services/secret/secret-version-dal.ts index 8e77858a5..8e4544c19 100644 --- a/backend/src/services/secret/secret-version-dal.ts +++ b/backend/src/services/secret/secret-version-dal.ts @@ -1,9 +1,9 @@ import { Knex } from "knex"; import { TDbClient } from "@app/db"; -import { TableName, TSecretVersions, TSecretVersionsUpdate } from "@app/db/schemas"; +import { SecretVersionsSchema, TableName, TSecretVersions, TSecretVersionsUpdate } from "@app/db/schemas"; import { BadRequestError, DatabaseError, NotFoundError } from "@app/lib/errors"; -import { ormify, selectAllTableCols } from "@app/lib/knex"; +import { ormify, selectAllTableCols, sqlNestRelationships, TFindOpt } from "@app/lib/knex"; import { logger } from "@app/lib/logger"; import { QueueName } from "@app/queue"; @@ -12,6 +12,50 @@ export type TSecretVersionDALFactory = ReturnType { const secretVersionOrm = ormify(db, TableName.SecretVersion); + const findBySecretId = async (secretId: string, { offset, limit, sort, tx }: TFindOpt = {}) => { + try { + const query = (tx || db.replicaNode())(TableName.SecretVersion) + .where(`${TableName.SecretVersion}.secretId`, secretId) + .leftJoin(TableName.Secret, `${TableName.SecretVersion}.secretId`, `${TableName.Secret}.id`) + .leftJoin(TableName.JnSecretTag, `${TableName.Secret}.id`, `${TableName.JnSecretTag}.${TableName.Secret}Id`) + .leftJoin(TableName.SecretTag, `${TableName.JnSecretTag}.${TableName.SecretTag}Id`, `${TableName.SecretTag}.id`) + .select(selectAllTableCols(TableName.SecretVersion)) + .select(db.ref("id").withSchema(TableName.SecretTag).as("tagId")) + .select(db.ref("color").withSchema(TableName.SecretTag).as("tagColor")) + .select(db.ref("slug").withSchema(TableName.SecretTag).as("tagSlug")); + + if (limit) void query.limit(limit); + if (offset) void query.offset(offset); + if (sort) { + void query.orderBy(sort.map(([column, order, nulls]) => ({ column: column as string, order, nulls }))); + } + + const docs = await query; + + const data = sqlNestRelationships({ + data: docs, + key: "id", + parentMapper: (el) => ({ _id: el.id, ...SecretVersionsSchema.parse(el) }), + childrenMapper: [ + { + key: "tagId", + label: "tags" as const, + mapper: ({ tagId: id, tagColor: color, tagSlug: slug }) => ({ + id, + color, + slug, + name: slug + }) + } + ] + }); + + return data; + } catch (error) { + throw new DatabaseError({ error, name: `${TableName.SecretVersion}: FindBySecretId` }); + } + }; + // This will fetch all latest secret versions from a folder const findLatestVersionByFolderId = async (folderId: string, tx?: Knex) => { try { @@ -149,6 +193,7 @@ export const secretVersionDALFactory = (db: TDbClient) => { findLatestVersionMany, bulkUpdate, findLatestVersionByFolderId, + findBySecretId, bulkUpdateNoVersionIncrement }; }; diff --git a/backend/src/services/service-token/service-token-service.ts b/backend/src/services/service-token/service-token-service.ts index 654917feb..9b87c29f8 100644 --- a/backend/src/services/service-token/service-token-service.ts +++ b/backend/src/services/service-token/service-token-service.ts @@ -5,7 +5,11 @@ import bcrypt from "bcrypt"; import { ActionProjectType } from "@app/db/schemas"; import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service"; -import { ProjectPermissionActions, ProjectPermissionSub } from "@app/ee/services/permission/project-permission"; +import { + ProjectPermissionActions, + ProjectPermissionSecretActions, + ProjectPermissionSub +} from "@app/ee/services/permission/project-permission"; import { getConfig } from "@app/lib/config/env"; import { ForbiddenRequestError, NotFoundError, UnauthorizedError } from "@app/lib/errors"; @@ -67,7 +71,7 @@ export const serviceTokenServiceFactory = ({ scopes.forEach(({ environment, secretPath }) => { ForbiddenError.from(permission).throwUnlessCan( - ProjectPermissionActions.Create, + ProjectPermissionSecretActions.Create, subject(ProjectPermissionSub.Secrets, { environment, secretPath }) ); }); diff --git a/backend/src/services/service-token/service-token-types.ts b/backend/src/services/service-token/service-token-types.ts index f1a700908..d06d90a9d 100644 --- a/backend/src/services/service-token/service-token-types.ts +++ b/backend/src/services/service-token/service-token-types.ts @@ -7,7 +7,7 @@ export type TCreateServiceTokenDTO = { iv: string; tag: string; expiresIn?: number | null; - permissions: ("read" | "write")[]; + permissions: ("read" | "write" | "readValue")[]; } & TProjectPermission; export type TGetServiceTokenInfoDTO = Omit; diff --git a/frontend/src/components/secrets/SecretReferenceDetails/SecretReferenceDetails.tsx b/frontend/src/components/secrets/SecretReferenceDetails/SecretReferenceDetails.tsx index 8164353d8..b8724f5dc 100644 --- a/frontend/src/components/secrets/SecretReferenceDetails/SecretReferenceDetails.tsx +++ b/frontend/src/components/secrets/SecretReferenceDetails/SecretReferenceDetails.tsx @@ -1,13 +1,21 @@ -import { useState } from "react"; -import { faChevronRight, faEye, faEyeSlash } from "@fortawesome/free-solid-svg-icons"; +/* eslint-disable no-nested-ternary */ +import { useEffect, useState } from "react"; +import { + faChevronRight, + faExclamationTriangle, + faEye, + faEyeSlash +} from "@fortawesome/free-solid-svg-icons"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import * as Collapsible from "@radix-ui/react-collapsible"; +import { AxiosError } from "axios"; import { twMerge } from "tailwind-merge"; +import { createNotification } from "@app/components/notifications"; import { FormControl, FormLabel, SecretInput, Spinner, Tooltip } from "@app/components/v2"; import { useWorkspace } from "@app/context"; import { useGetSecretReferenceTree } from "@app/hooks/api"; -import { TSecretReferenceTraceNode } from "@app/hooks/api/types"; +import { ApiErrorTypes, TApiErrors, TSecretReferenceTraceNode } from "@app/hooks/api/types"; import style from "./SecretReferenceDetails.module.css"; @@ -84,7 +92,7 @@ export const SecretReferenceTree = ({ secretPath, environment, secretKey }: Prop const { currentWorkspace } = useWorkspace(); const projectId = currentWorkspace?.id || ""; - const { data, isPending } = useGetSecretReferenceTree({ + const { data, isPending, isError, error } = useGetSecretReferenceTree({ secretPath, environmentSlug: environment, projectId, @@ -94,6 +102,26 @@ export const SecretReferenceTree = ({ secretPath, environment, secretKey }: Prop const tree = data?.tree; const secretValue = data?.value; + useEffect(() => { + if (error instanceof AxiosError) { + const err = error?.response?.data as TApiErrors; + + if (err?.error === ApiErrorTypes.CustomForbiddenError) { + createNotification({ + title: "You don't have permission to view reference tree", + text: "You don't have permission to view one or more of the referenced secrets.", + type: "error" + }); + return; + } + createNotification({ + title: "Error fetching secret reference tree", + text: "Please try again later.", + type: "error" + }); + } + }, [error]); + if (isPending) { return (
@@ -114,11 +142,16 @@ export const SecretReferenceTree = ({ secretPath, environment, secretKey }: Prop
- {tree && ( + {isError ? ( +
+ +

Error fetching secret reference tree

+
+ ) : tree ? (
- )} + ) : null}
Click a secret key to view its sub-references. diff --git a/frontend/src/components/v2/Blur/Blur.tsx b/frontend/src/components/v2/Blur/Blur.tsx new file mode 100644 index 000000000..bd1ded40a --- /dev/null +++ b/frontend/src/components/v2/Blur/Blur.tsx @@ -0,0 +1,22 @@ +import { twMerge } from "tailwind-merge"; + +import { Tooltip } from "../Tooltip/Tooltip"; + +interface IProps { + className?: string; + tooltipText?: string; +} + +export const Blur = ({ className, tooltipText }: IProps) => { + return ( + +
+ ******** +
+
+ ); +}; diff --git a/frontend/src/components/v2/Blur/index.tsx b/frontend/src/components/v2/Blur/index.tsx new file mode 100644 index 000000000..50fb7ccc6 --- /dev/null +++ b/frontend/src/components/v2/Blur/index.tsx @@ -0,0 +1 @@ +export { Blur } from "./Blur"; diff --git a/frontend/src/components/v2/InfisicalSecretInput/InfisicalSecretInput.tsx b/frontend/src/components/v2/InfisicalSecretInput/InfisicalSecretInput.tsx index 6fbe96238..dd6d3575e 100644 --- a/frontend/src/components/v2/InfisicalSecretInput/InfisicalSecretInput.tsx +++ b/frontend/src/components/v2/InfisicalSecretInput/InfisicalSecretInput.tsx @@ -120,6 +120,7 @@ export const InfisicalSecretInput = forwardRef( const isPopupOpen = Boolean(suggestionSource.isOpen) && isFocused; const { data: secrets } = useGetProjectSecrets({ + viewSecretValue: false, environment: suggestionSource.environment || "", secretPath: suggestionSource.secretPath || "", workspaceId, diff --git a/frontend/src/context/ProjectPermissionContext/types.ts b/frontend/src/context/ProjectPermissionContext/types.ts index d368a949f..260ffce09 100644 --- a/frontend/src/context/ProjectPermissionContext/types.ts +++ b/frontend/src/context/ProjectPermissionContext/types.ts @@ -7,6 +7,14 @@ export enum ProjectPermissionActions { Delete = "delete" } +export enum ProjectPermissionSecretActions { + DescribeSecret = "read", + ReadValue = "readValue", + Create = "create", + Edit = "edit", + Delete = "delete" +} + export enum ProjectPermissionDynamicSecretActions { ReadRootCredential = "read-root-credential", CreateRootCredential = "create-root-credential", @@ -138,7 +146,7 @@ export type SecretImportSubjectFields = { export type ProjectPermissionSet = | [ - ProjectPermissionActions, + ProjectPermissionSecretActions, ( | ProjectPermissionSub.Secrets | (ForcedSubject & SecretSubjectFields) diff --git a/frontend/src/hooks/api/dashboard/queries.tsx b/frontend/src/hooks/api/dashboard/queries.tsx index 4b63be988..f704081a9 100644 --- a/frontend/src/hooks/api/dashboard/queries.tsx +++ b/frontend/src/hooks/api/dashboard/queries.tsx @@ -207,6 +207,7 @@ export const useGetProjectSecretsDetails = ( search = "", includeSecrets, includeFolders, + viewSecretValue, includeImports, includeDynamicSecrets, tags @@ -231,6 +232,7 @@ export const useGetProjectSecretsDetails = ( limit, orderBy, orderDirection, + viewSecretValue, offset, projectId, environment, @@ -247,6 +249,7 @@ export const useGetProjectSecretsDetails = ( limit, orderBy, orderDirection, + viewSecretValue, offset, projectId, environment, diff --git a/frontend/src/hooks/api/dashboard/types.ts b/frontend/src/hooks/api/dashboard/types.ts index 786b2b43a..4808b463c 100644 --- a/frontend/src/hooks/api/dashboard/types.ts +++ b/frontend/src/hooks/api/dashboard/types.ts @@ -69,6 +69,7 @@ export type TGetDashboardProjectSecretsDetailsDTO = Omit< TGetDashboardProjectSecretsOverviewDTO, "environments" > & { + viewSecretValue: boolean; environment: string; includeImports?: boolean; tags: Record; diff --git a/frontend/src/hooks/api/secretApprovalRequest/queries.tsx b/frontend/src/hooks/api/secretApprovalRequest/queries.tsx index 74acf744b..39fa1058a 100644 --- a/frontend/src/hooks/api/secretApprovalRequest/queries.tsx +++ b/frontend/src/hooks/api/secretApprovalRequest/queries.tsx @@ -79,6 +79,7 @@ export const decryptSecrets = ( id: encSecret.id, env: encSecret.environment, key: secretKey, + secretValueHidden: encSecret.secretValueHidden, value: secretValue, tags: encSecret.tags, comment: secretComment, diff --git a/frontend/src/hooks/api/secretImports/queries.tsx b/frontend/src/hooks/api/secretImports/queries.tsx index c2ddd2fbe..dfc8c6497 100644 --- a/frontend/src/hooks/api/secretImports/queries.tsx +++ b/frontend/src/hooks/api/secretImports/queries.tsx @@ -137,6 +137,7 @@ export const useGetImportedSecretsSingleEnv = ({ env: encSecret.environment, key: encSecret.secretKey, value: encSecret.secretValue, + secretValueHidden: encSecret.secretValueHidden, tags: encSecret.tags, comment: encSecret.secretComment, createdAt: encSecret.createdAt, @@ -176,6 +177,7 @@ export const useGetImportedSecretsAllEnvs = ({ env: encSecret.environment, key: encSecret.secretKey, value: encSecret.secretValue, + secretValueHidden: encSecret.secretValueHidden, tags: encSecret.tags, comment: encSecret.secretComment, createdAt: encSecret.createdAt, diff --git a/frontend/src/hooks/api/secretSnapshots/queries.tsx b/frontend/src/hooks/api/secretSnapshots/queries.tsx index 8a16b4180..d273bd5ea 100644 --- a/frontend/src/hooks/api/secretSnapshots/queries.tsx +++ b/frontend/src/hooks/api/secretSnapshots/queries.tsx @@ -75,6 +75,7 @@ export const useGetSnapshotSecrets = ({ snapshotId }: TSnapshotDataProps) => id: secretVersion.secretId, env: data.environment.slug, key: secretVersion.secretKey, + secretValueHidden: secretVersion.secretValueHidden, value: secretVersion.secretValue || "", tags: secretVersion.tags, comment: secretVersion.secretComment, diff --git a/frontend/src/hooks/api/secrets/queries.tsx b/frontend/src/hooks/api/secrets/queries.tsx index a6dd959b0..3e82c3b90 100644 --- a/frontend/src/hooks/api/secrets/queries.tsx +++ b/frontend/src/hooks/api/secrets/queries.tsx @@ -26,8 +26,13 @@ import { export const secretKeys = { // this is also used in secretSnapshot part - getProjectSecret: ({ workspaceId, environment, secretPath }: TGetProjectSecretsKey) => - [{ workspaceId, environment, secretPath }, "secrets"] as const, + getProjectSecret: ({ + workspaceId, + environment, + secretPath, + viewSecretValue + }: TGetProjectSecretsKey) => + [{ workspaceId, environment, secretPath, viewSecretValue }, "secrets"] as const, getSecretVersion: (secretId: string) => [{ secretId }, "secret-versions"] as const, getSecretAccessList: ({ workspaceId, @@ -44,13 +49,15 @@ export const fetchProjectSecrets = async ({ environment, secretPath, includeImports, - expandSecretReferences + expandSecretReferences, + viewSecretValue }: TGetProjectSecretsKey) => { const { data } = await apiRequest.get("/api/v3/secrets/raw", { params: { environment, workspaceId, secretPath, + viewSecretValue, expandSecretReferences, include_imports: includeImports } @@ -68,6 +75,7 @@ export const mergePersonalSecrets = (rawSecrets: SecretV3Raw[]) => { env: el.environment, key: el.secretKey, value: el.secretValue, + secretValueHidden: el.secretValueHidden, tags: el.tags || [], comment: el.secretComment || "", reminderRepeatDays: el.secretReminderRepeatDays, @@ -107,6 +115,7 @@ export const useGetProjectSecrets = ({ workspaceId, environment, secretPath, + viewSecretValue, options }: TGetProjectSecretsDTO & { options?: Omit< @@ -123,8 +132,13 @@ export const useGetProjectSecrets = ({ ...options, // wait for all values to be available enabled: Boolean(workspaceId && environment) && (options?.enabled ?? true), - queryKey: secretKeys.getProjectSecret({ workspaceId, environment, secretPath }), - queryFn: () => fetchProjectSecrets({ workspaceId, environment, secretPath }), + queryKey: secretKeys.getProjectSecret({ + workspaceId, + environment, + secretPath, + viewSecretValue + }), + queryFn: () => fetchProjectSecrets({ workspaceId, environment, secretPath, viewSecretValue }), select: useCallback( (data: Awaited>) => mergePersonalSecrets(data.secrets), [] diff --git a/frontend/src/hooks/api/secrets/types.ts b/frontend/src/hooks/api/secrets/types.ts index 9e0a82dda..5c4d4ffc6 100644 --- a/frontend/src/hooks/api/secrets/types.ts +++ b/frontend/src/hooks/api/secrets/types.ts @@ -19,6 +19,7 @@ export type EncryptedSecret = { secretValueCiphertext: string; secretValueIV: string; secretValueTag: string; + secretValueHidden: boolean; __v: number; createdAt: string; updatedAt: string; @@ -37,6 +38,7 @@ export type SecretV3RawSanitized = { version: number; key: string; value?: string; + secretValueHidden: boolean; comment?: string; reminderRepeatDays?: number | null; reminderNote?: string | null; @@ -61,6 +63,7 @@ export type SecretV3Raw = { environment: string; version: number; type: string; + secretValueHidden: boolean; secretKey: string; secretPath: string; secretValue?: string; @@ -95,6 +98,7 @@ export type SecretVersions = { envId: string; secretKey: string; secretValue?: string; + secretValueHidden: boolean; secretComment?: string; tags: WsTag[]; __v: number; @@ -115,6 +119,7 @@ export type TGetProjectSecretsKey = { environment: string; secretPath?: string; includeImports?: boolean; + viewSecretValue?: boolean; expandSecretReferences?: boolean; }; diff --git a/frontend/src/hooks/api/types.ts b/frontend/src/hooks/api/types.ts index cda34ad60..8f2acbbd7 100644 --- a/frontend/src/hooks/api/types.ts +++ b/frontend/src/hooks/api/types.ts @@ -47,7 +47,8 @@ export enum ApiErrorTypes { PermissionBoundaryError = "PermissionBoundaryError", BadRequestError = "BadRequest", UnauthorizedError = "UnauthorizedError", - ForbiddenError = "PermissionDenied" + ForbiddenError = "PermissionDenied", + CustomForbiddenError = "ForbiddenError" } export type TApiErrors = @@ -70,6 +71,12 @@ export type TApiErrors = details: PureAbility["rules"]; statusCode: 403; } + | { + reqId: string; + error: ApiErrorTypes.CustomForbiddenError; + message: string; + statusCode: 403; + } | { reqId: string; statusCode: 400; diff --git a/frontend/src/pages/project/AccessControlPage/components/ServiceTokenTab/components/ServiceTokenSection/AddServiceTokenModal.tsx b/frontend/src/pages/project/AccessControlPage/components/ServiceTokenTab/components/ServiceTokenSection/AddServiceTokenModal.tsx index 46a9c5680..a669a8360 100644 --- a/frontend/src/pages/project/AccessControlPage/components/ServiceTokenTab/components/ServiceTokenSection/AddServiceTokenModal.tsx +++ b/frontend/src/pages/project/AccessControlPage/components/ServiceTokenTab/components/ServiceTokenSection/AddServiceTokenModal.tsx @@ -58,6 +58,7 @@ const schema = z.object({ permissions: z .object({ read: z.boolean(), + readValue: z.boolean(), write: z.boolean() }) .required() @@ -296,14 +297,19 @@ export const AddServiceTokenModal = ({ popUp, handlePopUpToggle }: Props) => { name="permissions" defaultValue={{ read: true, + readValue: false, write: false }} render={({ field: { onChange, value }, fieldState: { error } }) => { const options = [ { - label: "Read (default)", + label: "Describe Secret (default)", value: "read" }, + { + label: "Read Value (optional)", + value: "readValue" + }, { label: "Write (optional)", value: "write" diff --git a/frontend/src/pages/project/RoleDetailsBySlugPage/components/GeneralPermissionPolicies.tsx b/frontend/src/pages/project/RoleDetailsBySlugPage/components/GeneralPermissionPolicies.tsx index de2231fc9..8aca40a34 100644 --- a/frontend/src/pages/project/RoleDetailsBySlugPage/components/GeneralPermissionPolicies.tsx +++ b/frontend/src/pages/project/RoleDetailsBySlugPage/components/GeneralPermissionPolicies.tsx @@ -117,24 +117,27 @@ export const GeneralPermissionPolicies = {actions.map(({ label, value }) => { if (typeof value !== "string") return undefined; + return ( ( -
- - {label} - -
- )} + render={({ field }) => { + return ( +
+ + {label} + +
+ ); + }} /> ); })} diff --git a/frontend/src/pages/project/RoleDetailsBySlugPage/components/ProjectRoleModifySection.utils.tsx b/frontend/src/pages/project/RoleDetailsBySlugPage/components/ProjectRoleModifySection.utils.tsx index 7dcb118fd..e26ec8bc4 100644 --- a/frontend/src/pages/project/RoleDetailsBySlugPage/components/ProjectRoleModifySection.utils.tsx +++ b/frontend/src/pages/project/RoleDetailsBySlugPage/components/ProjectRoleModifySection.utils.tsx @@ -9,6 +9,7 @@ import { PermissionConditionOperators, ProjectPermissionDynamicSecretActions, ProjectPermissionKmipActions, + ProjectPermissionSecretActions, ProjectPermissionSecretSyncActions, TPermissionCondition, TPermissionConditionOperators @@ -22,6 +23,14 @@ const GeneralPolicyActionSchema = z.object({ create: z.boolean().optional() }); +const SecretPolicyActionSchema = z.object({ + read: z.boolean().optional(), // describe secret + edit: z.boolean().optional(), + delete: z.boolean().optional(), + create: z.boolean().optional(), + readValue: z.boolean().optional() +}); + const CmekPolicyActionSchema = z.object({ read: z.boolean().optional(), edit: z.boolean().optional(), @@ -114,7 +123,7 @@ export const projectRoleFormSchema = z.object({ .refine((val) => val !== "custom", { message: "Cannot use custom as its a keyword" }), permissions: z .object({ - [ProjectPermissionSub.Secrets]: GeneralPolicyActionSchema.extend({ + [ProjectPermissionSub.Secrets]: SecretPolicyActionSchema.extend({ inverted: z.boolean().optional(), conditions: ConditionSchema }) @@ -283,6 +292,28 @@ export const rolePermission2Form = (permissions: TProjectPermission[] = []) => { }); return; } + + if (subject === ProjectPermissionSub.Secrets) { + const canRead = action.includes(ProjectPermissionSecretActions.DescribeSecret); + const canEdit = action.includes(ProjectPermissionSecretActions.Edit); + const canDelete = action.includes(ProjectPermissionSecretActions.Delete); + const canCreate = action.includes(ProjectPermissionSecretActions.Create); + const canReadValue = action.includes(ProjectPermissionSecretActions.ReadValue); + + // from above statement we are sure it won't be undefined + formVal[subject]!.push({ + read: canRead, + create: canCreate, + edit: canEdit, + delete: canDelete, + readValue: canReadValue, + conditions: conditions ? convertCaslConditionToFormOperator(conditions) : [], + inverted + }); + + return; + } + // for other subjects const canRead = action.includes(ProjectPermissionActions.Read); const canEdit = action.includes(ProjectPermissionActions.Edit); @@ -483,8 +514,9 @@ export const PROJECT_PERMISSION_OBJECT: TProjectPermissionObject = { [ProjectPermissionSub.Secrets]: { title: "Secrets", actions: [ - { label: "Read", value: "read" }, + { label: "Describe Secret", value: "read" }, { label: "Create", value: "create" }, + { label: "Read Value", value: "readValue" }, { label: "Modify", value: "edit" }, { label: "Remove", value: "delete" } ] diff --git a/frontend/src/pages/secret-manager/OverviewPage/components/CreateSecretForm/CreateSecretForm.tsx b/frontend/src/pages/secret-manager/OverviewPage/components/CreateSecretForm/CreateSecretForm.tsx index c894d6b14..4847feb69 100644 --- a/frontend/src/pages/secret-manager/OverviewPage/components/CreateSecretForm/CreateSecretForm.tsx +++ b/frontend/src/pages/secret-manager/OverviewPage/components/CreateSecretForm/CreateSecretForm.tsx @@ -19,6 +19,7 @@ import { import { getKeyValue } from "@app/helpers/parseEnvVar"; import { useCreateFolder, useCreateSecretV3, useCreateWsTag, useGetWsTags } from "@app/hooks/api"; import { SecretType } from "@app/hooks/api/types"; +import { ProjectPermissionSecretActions } from "@app/context/ProjectPermissionContext/types"; const typeSchema = z .object({ @@ -275,7 +276,7 @@ export const CreateSecretForm = ({ secretPath = "/", onClose }: Props) => { isMulti options={environments.filter((environment) => permission.can( - ProjectPermissionActions.Create, + ProjectPermissionSecretActions.Create, subject(ProjectPermissionSub.Secrets, { environment: environment.slug, secretPath, diff --git a/frontend/src/pages/secret-manager/OverviewPage/components/SecretOverviewTableRow/SecretEditRow.tsx b/frontend/src/pages/secret-manager/OverviewPage/components/SecretOverviewTableRow/SecretEditRow.tsx index d3dfd49e7..4887d4fd7 100644 --- a/frontend/src/pages/secret-manager/OverviewPage/components/SecretOverviewTableRow/SecretEditRow.tsx +++ b/frontend/src/pages/secret-manager/OverviewPage/components/SecretOverviewTableRow/SecretEditRow.tsx @@ -25,6 +25,7 @@ import { ModalTrigger, Tooltip } from "@app/components/v2"; +import { Blur } from "@app/components/v2/Blur"; import { InfisicalSecretInput } from "@app/components/v2/InfisicalSecretInput"; import { ProjectPermissionActions, ProjectPermissionSub } from "@app/context"; import { useToggle } from "@app/hooks"; @@ -39,6 +40,7 @@ type Props = { isVisible?: boolean; isImportedSecret: boolean; environment: string; + secretValueHidden: boolean; secretPath: string; onSecretCreate: (env: string, key: string, value: string) => Promise; onSecretUpdate: ( @@ -58,6 +60,7 @@ export const SecretEditRow = ({ isImportedSecret, onSecretUpdate, secretName, + secretValueHidden, onSecretCreate, onSecretDelete, environment, @@ -140,24 +143,29 @@ export const SecretEditRow = ({ />
- ( - - )} - /> + {secretValueHidden ? ( + + ) : ( + ( + + )} + /> + )}
+
-
NO ACCESS
+
diff --git a/frontend/src/pages/secret-manager/OverviewPage/components/SecretOverviewTableRow/SecretOverviewTableRow.tsx b/frontend/src/pages/secret-manager/OverviewPage/components/SecretOverviewTableRow/SecretOverviewTableRow.tsx index b9376f6b4..3a66f6896 100644 --- a/frontend/src/pages/secret-manager/OverviewPage/components/SecretOverviewTableRow/SecretOverviewTableRow.tsx +++ b/frontend/src/pages/secret-manager/OverviewPage/components/SecretOverviewTableRow/SecretOverviewTableRow.tsx @@ -221,10 +221,13 @@ export const SecretOverviewTableRow = ({ secretPath={secretPath} isVisible={isSecretVisible} secretName={secretKey} + secretValueHidden={secret?.secretValueHidden || false} defaultValue={ - secret?.valueOverride || - secret?.value || - importedSecret?.secret?.value + secret?.secretValueHidden + ? "" + : secret?.valueOverride || + secret?.value || + importedSecret?.secret?.value } secretId={secret?.id} isOverride={Boolean(secret?.valueOverride)} diff --git a/frontend/src/pages/secret-manager/OverviewPage/components/SecretOverviewTableRow/SecretRenameRow.tsx b/frontend/src/pages/secret-manager/OverviewPage/components/SecretOverviewTableRow/SecretRenameRow.tsx index ae1916b86..3b05f10fa 100644 --- a/frontend/src/pages/secret-manager/OverviewPage/components/SecretOverviewTableRow/SecretRenameRow.tsx +++ b/frontend/src/pages/secret-manager/OverviewPage/components/SecretOverviewTableRow/SecretRenameRow.tsx @@ -10,12 +10,8 @@ import { z } from "zod"; import { createNotification } from "@app/components/notifications"; import { IconButton, Input, Spinner, Tooltip } from "@app/components/v2"; -import { - ProjectPermissionActions, - ProjectPermissionSub, - useProjectPermission, - useWorkspace -} from "@app/context"; +import { ProjectPermissionSub, useProjectPermission, useWorkspace } from "@app/context"; +import { ProjectPermissionSecretActions } from "@app/context/ProjectPermissionContext/types"; import { useToggle } from "@app/hooks"; import { useUpdateSecretV3 } from "@app/hooks/api"; import { SecretType, SecretV3RawSanitized } from "@app/hooks/api/types"; @@ -55,8 +51,8 @@ function SecretRenameRow({ environments, getSecretByKey, secretKey, secretPath } secretTags: (secretDetails?.tags || []).map((i) => i.slug) }); const isSecretInEnvReadOnly = - permission.can(ProjectPermissionActions.Read, secretPermissionSubject) && - permission.cannot(ProjectPermissionActions.Edit, secretPermissionSubject); + permission.can(ProjectPermissionSecretActions.DescribeSecret, secretPermissionSubject) && + permission.cannot(ProjectPermissionSecretActions.Edit, secretPermissionSubject); if (isSecretInEnvReadOnly) { return true; } diff --git a/frontend/src/pages/secret-manager/OverviewPage/components/SecretSearchInput/components/QuickSearchSecretItem.tsx b/frontend/src/pages/secret-manager/OverviewPage/components/SecretSearchInput/components/QuickSearchSecretItem.tsx index e901a4ff7..9943de5b6 100644 --- a/frontend/src/pages/secret-manager/OverviewPage/components/SecretSearchInput/components/QuickSearchSecretItem.tsx +++ b/frontend/src/pages/secret-manager/OverviewPage/components/SecretSearchInput/components/QuickSearchSecretItem.tsx @@ -110,21 +110,31 @@ export const QuickSearchSecretItem = ({ )} {isSingleEnv ? ( - { - e.stopPropagation(); - const el = envSlugMap.get(groupSecret.env)?.name; - if (el) { - handleCopy(groupSecret.value!, el); - } - }} + - - + { + e.stopPropagation(); + const el = envSlugMap.get(groupSecret.env)?.name; + if (el) { + handleCopy(groupSecret.value!, el); + } + }} + > + + + ) : ( @@ -158,14 +168,24 @@ export const QuickSearchSecretItem = ({ )} - - - + + + + Hover to Reveal... diff --git a/frontend/src/pages/secret-manager/OverviewPage/components/SelectionPanel/SelectionPanel.tsx b/frontend/src/pages/secret-manager/OverviewPage/components/SelectionPanel/SelectionPanel.tsx index 4738e411e..4e048b941 100644 --- a/frontend/src/pages/secret-manager/OverviewPage/components/SelectionPanel/SelectionPanel.tsx +++ b/frontend/src/pages/secret-manager/OverviewPage/components/SelectionPanel/SelectionPanel.tsx @@ -11,6 +11,7 @@ import { useProjectPermission, useWorkspace } from "@app/context"; +import { ProjectPermissionSecretActions } from "@app/context/ProjectPermissionContext/types"; import { usePopUp } from "@app/hooks"; import { useDeleteFolder, useDeleteSecretBatch } from "@app/hooks/api"; import { @@ -58,7 +59,7 @@ export const SelectionPanel = ({ secretPath, resetSelectedEntries, selectedEntri // user should have the ability to delete secrets/folders in at least one of the envs const shouldShowDelete = userAvailableEnvs.some((env) => permission.can( - ProjectPermissionActions.Delete, + ProjectPermissionSecretActions.Delete, subject(ProjectPermissionSub.Secrets, { environment: env.slug, secretPath, @@ -110,7 +111,7 @@ export const SelectionPanel = ({ secretPath, resetSelectedEntries, selectedEntri (accum: TDeleteSecretBatchDTO["secrets"], secretRecord) => { const entry = secretRecord[env.slug]; const canDeleteSecret = permission.can( - ProjectPermissionActions.Delete, + ProjectPermissionSecretActions.Delete, subject(ProjectPermissionSub.Secrets, { environment: env.slug, secretPath, diff --git a/frontend/src/pages/secret-manager/OverviewPage/components/SelectionPanel/components/MoveSecretsDialog/MoveSecretsDialog.tsx b/frontend/src/pages/secret-manager/OverviewPage/components/SelectionPanel/components/MoveSecretsDialog/MoveSecretsDialog.tsx index 8a27a24c0..a81fa3e53 100644 --- a/frontend/src/pages/secret-manager/OverviewPage/components/SelectionPanel/components/MoveSecretsDialog/MoveSecretsDialog.tsx +++ b/frontend/src/pages/secret-manager/OverviewPage/components/SelectionPanel/components/MoveSecretsDialog/MoveSecretsDialog.tsx @@ -25,7 +25,8 @@ import { Spinner, Switch } from "@app/components/v2"; -import { ProjectPermissionActions, ProjectPermissionSub, useProjectPermission } from "@app/context"; +import { ProjectPermissionSub, useProjectPermission } from "@app/context"; +import { ProjectPermissionSecretActions } from "@app/context/ProjectPermissionContext/types"; import { useDebounce } from "@app/hooks"; import { useMoveSecrets } from "@app/hooks/api"; import { useGetProjectSecretsQuickSearch } from "@app/hooks/api/dashboard"; @@ -95,7 +96,7 @@ const Content = ({ env.slug, { missingPermissions: permission.cannot( - ProjectPermissionActions.Delete, + ProjectPermissionSecretActions.Delete, subject(ProjectPermissionSub.Secrets, { environment: env.slug, secretPath: sourceSecretPath, diff --git a/frontend/src/pages/secret-manager/SecretDashboardPage/SecretDashboardPage.tsx b/frontend/src/pages/secret-manager/SecretDashboardPage/SecretDashboardPage.tsx index daf875a78..5cbefe9c3 100644 --- a/frontend/src/pages/secret-manager/SecretDashboardPage/SecretDashboardPage.tsx +++ b/frontend/src/pages/secret-manager/SecretDashboardPage/SecretDashboardPage.tsx @@ -25,6 +25,7 @@ import { useProjectPermission, useWorkspace } from "@app/context"; +import { ProjectPermissionSecretActions } from "@app/context/ProjectPermissionContext/types"; import { useDebounce, usePagination, usePopUp, useResetPageHelper } from "@app/hooks"; import { useGetImportedSecretsSingleEnv, @@ -103,7 +104,16 @@ const Page = () => { const projectSlug = currentWorkspace?.slug || ""; const secretPath = (routerQueryParams.secretPath as string) || "/"; const canReadSecret = permission.can( - ProjectPermissionActions.Read, + ProjectPermissionSecretActions.DescribeSecret, + subject(ProjectPermissionSub.Secrets, { + environment, + secretPath, + secretName: "*", + secretTags: ["*"] + }) + ); + const canReadSecretValue = permission.can( + ProjectPermissionSecretActions.ReadValue, subject(ProjectPermissionSub.Secrets, { environment, secretPath, @@ -176,6 +186,7 @@ const Page = () => { orderDirection, includeImports: canReadSecretImports && filter.include.import, includeFolders: filter.include.folder, + viewSecretValue: canReadSecretValue, includeDynamicSecrets: canReadDynamicSecret && filter.include.dynamic, includeSecrets: canReadSecret && filter.include.secret, tags: filter.tags diff --git a/frontend/src/pages/secret-manager/SecretDashboardPage/components/ActionBar/ActionBar.tsx b/frontend/src/pages/secret-manager/SecretDashboardPage/components/ActionBar/ActionBar.tsx index 03ff3dd57..d545ba748 100644 --- a/frontend/src/pages/secret-manager/SecretDashboardPage/components/ActionBar/ActionBar.tsx +++ b/frontend/src/pages/secret-manager/SecretDashboardPage/components/ActionBar/ActionBar.tsx @@ -21,6 +21,7 @@ import { faTrash } from "@fortawesome/free-solid-svg-icons"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; +import { AxiosError } from "axios"; import FileSaver from "file-saver"; import { twMerge } from "tailwind-merge"; @@ -54,7 +55,7 @@ import { import { usePopUp } from "@app/hooks"; import { useCreateFolder, useDeleteSecretBatch, useMoveSecrets } from "@app/hooks/api"; import { fetchProjectSecrets } from "@app/hooks/api/secrets/queries"; -import { SecretType, WsTag } from "@app/hooks/api/types"; +import { ApiErrorTypes, SecretType, TApiErrors, WsTag } from "@app/hooks/api/types"; import { SecretSearchInput } from "@app/pages/secret-manager/OverviewPage/components/SecretSearchInput"; import { @@ -152,51 +153,71 @@ export const ActionBar = ({ }; const handleSecretDownload = async () => { - const { secrets: localSecrets, imports: localImportedSecrets } = await fetchProjectSecrets({ - workspaceId, - expandSecretReferences: true, - includeImports: true, - environment, - secretPath - }); - const secretsPicked = new Set(); - const secretsToDownload: { key: string; value?: string; comment?: string }[] = []; - localSecrets.forEach((el) => { - secretsPicked.add(el.secretKey); - secretsToDownload.push({ - key: el.secretKey, - value: el.secretValue, - comment: el.secretComment + try { + const { secrets: localSecrets, imports: localImportedSecrets } = await fetchProjectSecrets({ + workspaceId, + expandSecretReferences: true, + includeImports: true, + environment, + secretPath + }); + const secretsPicked = new Set(); + const secretsToDownload: { key: string; value?: string; comment?: string }[] = []; + localSecrets.forEach((el) => { + secretsPicked.add(el.secretKey); + secretsToDownload.push({ + key: el.secretKey, + value: el.secretValue, + comment: el.secretComment + }); }); - }); - for (let i = localImportedSecrets.length - 1; i >= 0; i -= 1) { - for (let j = localImportedSecrets[i].secrets.length - 1; j >= 0; j -= 1) { - const secret = localImportedSecrets[i].secrets[j]; - if (!secretsPicked.has(secret.secretKey)) { - secretsToDownload.push({ - key: secret.secretKey, - value: secret.secretValue, - comment: secret.secretComment - }); + for (let i = localImportedSecrets.length - 1; i >= 0; i -= 1) { + for (let j = localImportedSecrets[i].secrets.length - 1; j >= 0; j -= 1) { + const secret = localImportedSecrets[i].secrets[j]; + if (!secretsPicked.has(secret.secretKey)) { + secretsToDownload.push({ + key: secret.secretKey, + value: secret.secretValue, + comment: secret.secretComment + }); + } + secretsPicked.add(secret.secretKey); } - secretsPicked.add(secret.secretKey); } + + const file = secretsToDownload + .sort((a, b) => a.key.toLowerCase().localeCompare(b.key.toLowerCase())) + .reduce( + (prev, { key, comment, value }, index) => + prev + + (comment + ? `${index === 0 ? "#" : "\n#"} ${comment}\n${key}=${value}\n` + : `${key}=${value}\n`), + "" + ); + + const blob = new Blob([file], { type: "text/plain;charset=utf-8" }); + FileSaver.saveAs(blob, `${environment}.env`); + } catch (err) { + if (err instanceof AxiosError) { + const error = err?.response?.data as TApiErrors; + + if (error?.error === ApiErrorTypes.ForbiddenError && error.message.includes("readValue")) { + createNotification({ + title: "You don't have permission to download secrets", + text: "You don't have permission to view one or more of the secrets in the current folder. Please contact your administrator.", + type: "error" + }); + return; + } + } + createNotification({ + title: "Failed to download secrets", + text: "Please try again later.", + type: "error" + }); } - - const file = secretsToDownload - .sort((a, b) => a.key.toLowerCase().localeCompare(b.key.toLowerCase())) - .reduce( - (prev, { key, comment, value }, index) => - prev + - (comment - ? `${index === 0 ? "#" : "\n#"} ${comment}\n${key}=${value}\n` - : `${key}=${value}\n`), - "" - ); - - const blob = new Blob([file], { type: "text/plain;charset=utf-8" }); - FileSaver.saveAs(blob, `${environment}.env`); }; const handleSecretBulkDelete = async () => { diff --git a/frontend/src/pages/secret-manager/SecretDashboardPage/components/SecretListView/SecretDetailSidebar.tsx b/frontend/src/pages/secret-manager/SecretDashboardPage/components/SecretListView/SecretDetailSidebar.tsx index f557c53b3..d6da791bb 100644 --- a/frontend/src/pages/secret-manager/SecretDashboardPage/components/SecretListView/SecretDetailSidebar.tsx +++ b/frontend/src/pages/secret-manager/SecretDashboardPage/components/SecretListView/SecretDetailSidebar.tsx @@ -13,12 +13,14 @@ import { faShare, faTag, faTrash, - faUser + faUser, + faTriangleExclamation } from "@fortawesome/free-solid-svg-icons"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { zodResolver } from "@hookform/resolvers/zod"; import { Link, useNavigate } from "@tanstack/react-router"; import { format } from "date-fns"; +import { twMerge } from "tailwind-merge"; import { UpgradePlanModal } from "@app/components/license/UpgradePlanModal"; import { createNotification } from "@app/components/notifications"; @@ -48,6 +50,7 @@ import { useProjectPermission, useWorkspace } from "@app/context"; +import { ProjectPermissionSecretActions } from "@app/context/ProjectPermissionContext/types"; import { usePopUp, useToggle } from "@app/hooks"; import { useGetSecretVersion } from "@app/hooks/api"; import { ActorType } from "@app/hooks/api/auditLogs/enums"; @@ -128,7 +131,7 @@ export const SecretDetailSidebar = ({ const navigate = useNavigate(); const cannotEditSecret = permission.cannot( - ProjectPermissionActions.Edit, + ProjectPermissionSecretActions.Edit, subject(ProjectPermissionSub.Secrets, { environment, secretPath, @@ -136,16 +139,29 @@ export const SecretDetailSidebar = ({ secretTags: selectTagSlugs }) ); + + const cannotReadSecretValue = permission.cannot( + ProjectPermissionSecretActions.ReadValue, + subject(ProjectPermissionSub.Secrets, { + environment, + secretPath, + secretName: secretKey, + secretTags: selectTagSlugs + }) + ); + const isReadOnly = permission.can( - ProjectPermissionActions.Read, + ProjectPermissionSecretActions.DescribeSecret, subject(ProjectPermissionSub.Secrets, { environment, secretPath, secretName: secretKey, secretTags: selectTagSlugs }) - ) && cannotEditSecret; + ) && + cannotEditSecret && + cannotReadSecretValue; const overrideAction = watch("overrideAction"); const isOverridden = @@ -325,38 +341,63 @@ export const SecretDetailSidebar = ({ key="secret-value" control={control} render={({ field }) => ( - - - +
+ + + + The value of this secret is hidden because you do not have the + read secret value permission. + +
+ ) : undefined + } + label="Value" + > +
+ + + + +
+ +
)} /> )} -
- -
@@ -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) => ( +
+
+
+
+
+ v{version} +
+
{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) => {
{isModified && ( )} {Boolean(preSecret?.idOverride || postSecret?.idOverride) && ( From d74197aeb4b90423355a25fc6488b1b01f960fdb Mon Sep 17 00:00:00 2001 From: Maidul Islam Date: Wed, 5 Mar 2025 17:53:07 -0500 Subject: [PATCH 17/71] Revert "use forked pion turn server" This reverts commit bd66411d754df79fb22a0b333ea5205e90affef4. --- cli/go.sum | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/cli/go.sum b/cli/go.sum index ab46476d7..d209fd1ca 100644 --- a/cli/go.sum +++ b/cli/go.sum @@ -367,6 +367,8 @@ github.com/pion/stun/v3 v3.0.0 h1:4h1gwhWLWuZWOJIJR9s2ferRO+W3zA/b6ijOI6mKzUw= github.com/pion/stun/v3 v3.0.0/go.mod h1:HvCN8txt8mwi4FBvS3EmDghW6aQJ24T+y+1TKjB5jyU= github.com/pion/transport/v3 v3.0.7 h1:iRbMH05BzSNwhILHoBoAPxoB9xQgOaJk+591KC9P1o0= github.com/pion/transport/v3 v3.0.7/go.mod h1:YleKiTZ4vqNxVwh77Z0zytYi7rXHl7j6uPLGhhz9rwo= +github.com/pion/turn/v4 v4.0.0 h1:qxplo3Rxa9Yg1xXDxxH8xaqcyGUtbHYw4QSCvmFWvhM= +github.com/pion/turn/v4 v4.0.0/go.mod h1:MuPDkm15nYSklKpN8vWJ9W2M0PlyQZqYt1McGuxG7mA= github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= @@ -425,8 +427,8 @@ github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/ github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= -github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= -github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= +github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= github.com/subosito/gotenv v1.2.0 h1:Slr1R9HxAlEKefgq5jn9U+DnETlIUa6HfgEzj0g5d7s= github.com/subosito/gotenv v1.2.0/go.mod h1:N0PQaV/YGNqwC0u51sEeR/aUtSLEXKX9iv69rRypqCw= github.com/tidwall/pretty v1.0.0 h1:HsD+QiTn7sK6flMKIvNmpqz1qrpP3Ps6jOKIKMooyg4= From f1ba64aa66d2e453bd35ecf4dd136fa26d5e5953 Mon Sep 17 00:00:00 2001 From: Daniel Hougaard Date: Sat, 8 Mar 2025 03:40:51 +0400 Subject: [PATCH 18/71] fix(view-secret-value): backwards compatibility for `read` --- ...kfill-secret-permissions-with-readvalue.ts | 313 -------------- .../src/ee/routes/v2/project-role-router.ts | 6 +- .../ee/services/permission/permission-fns.ts | 103 ++++- .../services/permission/project-permission.ts | 17 +- .../secret-approval-request-service.ts | 10 +- .../secret-snapshot-service.ts | 51 +-- .../server/routes/v2/service-token-router.ts | 2 +- .../integration/integration-service.ts | 25 +- .../src/services/project/project-service.ts | 6 +- .../secret-import/secret-import-service.ts | 63 ++- .../secret-sync/secret-sync-service.ts | 25 +- .../secret-v2-bridge-service.ts | 409 ++++++++---------- backend/src/services/secret/secret-fns.ts | 15 +- backend/src/services/secret/secret-service.ts | 134 +++--- .../service-token/service-token-types.ts | 2 +- .../context/ProjectPermissionContext/types.ts | 3 +- frontend/src/lib/fn/permission.ts | 36 ++ .../AddServiceTokenModal.tsx | 6 +- .../components/GeneralPermissionPolicies.tsx | 236 +++++----- .../ProjectRoleModifySection.utils.tsx | 64 ++- .../SecretRenameRow.tsx | 8 +- .../SecretDashboardPage.tsx | 17 +- .../SecretListView/SecretDetailSidebar.tsx | 23 +- .../components/SecretListView/SecretItem.tsx | 16 +- 24 files changed, 708 insertions(+), 882 deletions(-) delete mode 100644 backend/src/db/migrations/20250218020306_backfill-secret-permissions-with-readvalue.ts create mode 100644 frontend/src/lib/fn/permission.ts diff --git a/backend/src/db/migrations/20250218020306_backfill-secret-permissions-with-readvalue.ts b/backend/src/db/migrations/20250218020306_backfill-secret-permissions-with-readvalue.ts deleted file mode 100644 index 9e567cefe..000000000 --- a/backend/src/db/migrations/20250218020306_backfill-secret-permissions-with-readvalue.ts +++ /dev/null @@ -1,313 +0,0 @@ -import { MongoAbility, RawRuleOf } from "@casl/ability"; -import { PackRule, packRules, unpackRules } from "@casl/ability/extra"; -import { Knex } from "knex"; -import { z } from "zod"; - -import { selectAllTableCols } from "@app/lib/knex"; - -import { TableName } from "../schemas"; - -enum ProjectPermissionSub { - Secrets = "secrets" -} - -enum SecretActions { - Read = "read", - ReadValue = "readValue" -} - -const UnpackedPermissionSchema = z.object({ - subject: z - .union([z.string().min(1), z.string().array()]) - .transform((el) => (typeof el !== "string" ? el[0] : el)) - .optional(), - action: z.union([z.string().min(1), z.string().array()]).transform((el) => (typeof el === "string" ? [el] : el)), - conditions: z.unknown().optional(), - inverted: z.boolean().optional() -}); - -const $unpackPermissions = (permissions: unknown) => - UnpackedPermissionSchema.array().parse(unpackRules((permissions || []) as PackRule>[])); - -const $updatePermissionsUp = (permissions: unknown) => { - const parsedPermissions = $unpackPermissions(permissions); - let shouldUpdate = false; - - for (let i = 0; i < parsedPermissions.length; i += 1) { - const parsedPermission = parsedPermissions[i]; - const { subject, action } = parsedPermission; - - if (subject === ProjectPermissionSub.Secrets) { - if (action.includes(SecretActions.Read) && !action.includes(SecretActions.ReadValue)) { - action.push(SecretActions.ReadValue); - parsedPermissions[i] = { ...parsedPermission, action }; - shouldUpdate = true; - } - } - } - - return { - parsedPermissions, - shouldUpdate - }; -}; - -const $updatePermissionsDown = (permissions: unknown) => { - const parsedPermissions = $unpackPermissions(permissions); - - let shouldUpdate = false; - for (let i = 0; i < parsedPermissions.length; i += 1) { - const parsedPermission = parsedPermissions[i]; - - const { subject, action } = parsedPermission; - - if (subject === ProjectPermissionSub.Secrets) { - const readValueIndex = action.indexOf(SecretActions.ReadValue); - - if (action.includes(SecretActions.ReadValue) && readValueIndex !== -1) { - action.splice(readValueIndex, 1); - parsedPermissions[i] = { ...parsedPermission, action }; - - shouldUpdate = true; - } - } - } - - const repackedPermissions = packRules(parsedPermissions); - - return { - repackedPermissions, - shouldUpdate - }; -}; - -const CHUNK_SIZE = 1000; - -export async function up(knex: Knex): Promise { - const projectRoles = await knex(TableName.ProjectRoles).select(selectAllTableCols(TableName.ProjectRoles)); - const projectIdentityAdditionalPrivileges = await knex(TableName.IdentityProjectAdditionalPrivilege).select( - selectAllTableCols(TableName.IdentityProjectAdditionalPrivilege) - ); - const projectUserAdditionalPrivileges = await knex(TableName.ProjectUserAdditionalPrivilege).select( - selectAllTableCols(TableName.ProjectUserAdditionalPrivilege) - ); - - const serviceTokens = await knex(TableName.ServiceToken).select(selectAllTableCols(TableName.ServiceToken)); - - const updatedServiceTokens = serviceTokens.reduce((acc, serviceToken) => { - const { permissions } = serviceToken; // Service tokens are special, and include an array of actions only. - - if (permissions.includes(SecretActions.Read) && !permissions.includes(SecretActions.ReadValue)) { - permissions.push(SecretActions.ReadValue); - acc.push({ - ...serviceToken, - permissions - }); - } - return acc; - }, []); - - if (updatedServiceTokens.length > 0) { - for (let i = 0; i < updatedServiceTokens.length; i += CHUNK_SIZE) { - const chunk = updatedServiceTokens.slice(i, i + CHUNK_SIZE); - - // eslint-disable-next-line no-await-in-loop - await knex(TableName.ServiceToken) - .whereIn( - "id", - chunk.map((t) => t.id) - ) - .update({ - // @ts-expect-error -- raw query - permissions: knex.raw( - `CASE id - ${chunk.map((t) => `WHEN '${t.id}' THEN ?::text[]`).join(" ")} - END`, - chunk.map((t) => t.permissions) - ) - }); - } - } - - const updatedRoles = projectRoles.reduce((acc, projectRole) => { - const { shouldUpdate, parsedPermissions } = $updatePermissionsUp(projectRole.permissions); - - if (shouldUpdate) { - acc.push({ - ...projectRole, - permissions: JSON.stringify(packRules(parsedPermissions)) - }); - } - return acc; - }, []); - - const updatedIdentityAdditionalPrivileges = projectIdentityAdditionalPrivileges.reduce< - typeof projectIdentityAdditionalPrivileges - >((acc, identityAdditionalPrivilege) => { - const { shouldUpdate, parsedPermissions } = $updatePermissionsUp(identityAdditionalPrivilege.permissions); - - if (shouldUpdate) { - acc.push({ - ...identityAdditionalPrivilege, - permissions: JSON.stringify(packRules(parsedPermissions)) - }); - } - return acc; - }, []); - - const updatedUserAdditionalPrivileges = projectUserAdditionalPrivileges.reduce< - typeof projectUserAdditionalPrivileges - >((acc, userAdditionalPrivilege) => { - const { shouldUpdate, parsedPermissions } = $updatePermissionsUp(userAdditionalPrivilege.permissions); - - if (shouldUpdate) { - acc.push({ - ...userAdditionalPrivilege, - permissions: JSON.stringify(packRules(parsedPermissions)) - }); - } - return acc; - }, []); - - if (updatedRoles.length > 0) { - for (let i = 0; i < updatedRoles.length; i += CHUNK_SIZE) { - const chunk = updatedRoles.slice(i, i + CHUNK_SIZE); - - // eslint-disable-next-line no-await-in-loop - await knex(TableName.ProjectRoles).insert(chunk).onConflict("id").merge(["permissions"]); - } - } - - if (updatedIdentityAdditionalPrivileges.length > 0) { - for (let i = 0; i < updatedIdentityAdditionalPrivileges.length; i += CHUNK_SIZE) { - const chunk = updatedIdentityAdditionalPrivileges.slice(i, i + CHUNK_SIZE); - - // eslint-disable-next-line no-await-in-loop - await knex(TableName.IdentityProjectAdditionalPrivilege).insert(chunk).onConflict("id").merge(["permissions"]); - } - } - - if (updatedUserAdditionalPrivileges.length > 0) { - for (let i = 0; i < updatedUserAdditionalPrivileges.length; i += CHUNK_SIZE) { - const chunk = updatedUserAdditionalPrivileges.slice(i, i + CHUNK_SIZE); - - // eslint-disable-next-line no-await-in-loop - await knex(TableName.ProjectUserAdditionalPrivilege).insert(chunk).onConflict("id").merge(["permissions"]); - } - } -} - -export async function down(knex: Knex): Promise { - const projectRoles = await knex(TableName.ProjectRoles).select(selectAllTableCols(TableName.ProjectRoles)); - const identityAdditionalPrivileges = await knex(TableName.IdentityProjectAdditionalPrivilege).select( - selectAllTableCols(TableName.IdentityProjectAdditionalPrivilege) - ); - const userAdditionalPrivileges = await knex(TableName.ProjectUserAdditionalPrivilege).select( - selectAllTableCols(TableName.ProjectUserAdditionalPrivilege) - ); - const serviceTokens = await knex(TableName.ServiceToken).select(selectAllTableCols(TableName.ServiceToken)); - - const updatedServiceTokens = serviceTokens.reduce((acc, serviceToken) => { - const { permissions } = serviceToken; - - if (permissions.includes(SecretActions.ReadValue)) { - permissions.splice(permissions.indexOf(SecretActions.ReadValue), 1); - acc.push({ - ...serviceToken, - permissions - }); - } - return acc; - }, []); - - if (updatedServiceTokens.length > 0) { - for (let i = 0; i < updatedServiceTokens.length; i += CHUNK_SIZE) { - const chunk = updatedServiceTokens.slice(i, i + CHUNK_SIZE); - - // eslint-disable-next-line no-await-in-loop - await knex(TableName.ServiceToken) - .whereIn( - "id", - chunk.map((t) => t.id) - ) - .update({ - // @ts-expect-error -- raw query - permissions: knex.raw( - `CASE id - ${chunk.map((t) => `WHEN '${t.id}' THEN ?::text[]`).join(" ")} - END`, - chunk.map((t) => t.permissions) - ) - }); - } - } - - const updatedRoles = projectRoles.reduce((acc, projectRole) => { - const { shouldUpdate, repackedPermissions } = $updatePermissionsDown(projectRole.permissions); - - if (shouldUpdate) { - acc.push({ - ...projectRole, - permissions: JSON.stringify(repackedPermissions) - }); - } - return acc; - }, []); - - const updatedIdentityAdditionalPrivileges = identityAdditionalPrivileges.reduce( - (acc, identityAdditionalPrivilege) => { - const { shouldUpdate, repackedPermissions } = $updatePermissionsDown(identityAdditionalPrivilege.permissions); - - if (shouldUpdate) { - acc.push({ - ...identityAdditionalPrivilege, - permissions: JSON.stringify(repackedPermissions) - }); - } - return acc; - }, - [] - ); - - const updatedUserAdditionalPrivileges = userAdditionalPrivileges.reduce( - (acc, userAdditionalPrivilege) => { - const { shouldUpdate, repackedPermissions } = $updatePermissionsDown(userAdditionalPrivilege.permissions); - - if (shouldUpdate) { - acc.push({ - ...userAdditionalPrivilege, - permissions: JSON.stringify(repackedPermissions) - }); - } - return acc; - }, - [] - ); - - if (updatedRoles.length > 0) { - for (let i = 0; i < updatedRoles.length; i += CHUNK_SIZE) { - const chunk = updatedRoles.slice(i, i + CHUNK_SIZE); - - // eslint-disable-next-line no-await-in-loop - await knex(TableName.ProjectRoles).insert(chunk).onConflict("id").merge(["permissions"]); - } - } - - if (updatedIdentityAdditionalPrivileges.length > 0) { - for (let i = 0; i < updatedIdentityAdditionalPrivileges.length; i += CHUNK_SIZE) { - const chunk = updatedIdentityAdditionalPrivileges.slice(i, i + CHUNK_SIZE); - - // eslint-disable-next-line no-await-in-loop - await knex(TableName.IdentityProjectAdditionalPrivilege).insert(chunk).onConflict("id").merge(["permissions"]); - } - } - - if (updatedUserAdditionalPrivileges.length > 0) { - for (let i = 0; i < updatedUserAdditionalPrivileges.length; i += CHUNK_SIZE) { - const chunk = updatedUserAdditionalPrivileges.slice(i, i + CHUNK_SIZE); - - // eslint-disable-next-line no-await-in-loop - await knex(TableName.ProjectUserAdditionalPrivilege).insert(chunk).onConflict("id").merge(["permissions"]); - } - } -} diff --git a/backend/src/ee/routes/v2/project-role-router.ts b/backend/src/ee/routes/v2/project-role-router.ts index 2d3b1984d..c3b3c11f2 100644 --- a/backend/src/ee/routes/v2/project-role-router.ts +++ b/backend/src/ee/routes/v2/project-role-router.ts @@ -2,6 +2,7 @@ import { packRules } from "@casl/ability/extra"; import { z } from "zod"; import { ProjectMembershipRole, ProjectRolesSchema } from "@app/db/schemas"; +import { checkForInvalidPermissionCombination } from "@app/ee/services/permission/permission-fns"; import { ProjectPermissionV2Schema } from "@app/ee/services/permission/project-permission"; import { PROJECT_ROLE } from "@app/lib/api-docs"; import { readLimit, writeLimit } from "@app/server/config/rateLimiter"; @@ -92,7 +93,10 @@ export const registerProjectRoleRouter = async (server: FastifyZodProvider) => { .describe(PROJECT_ROLE.UPDATE.slug), name: z.string().trim().optional().describe(PROJECT_ROLE.UPDATE.name), description: z.string().trim().nullish().describe(PROJECT_ROLE.UPDATE.description), - permissions: ProjectPermissionV2Schema.array().describe(PROJECT_ROLE.UPDATE.permissions).optional() + permissions: ProjectPermissionV2Schema.array() + .describe(PROJECT_ROLE.UPDATE.permissions) + .optional() + .superRefine(checkForInvalidPermissionCombination) }), response: { 200: z.object({ diff --git a/backend/src/ee/services/permission/permission-fns.ts b/backend/src/ee/services/permission/permission-fns.ts index 80a58db0a..97113a562 100644 --- a/backend/src/ee/services/permission/permission-fns.ts +++ b/backend/src/ee/services/permission/permission-fns.ts @@ -1,7 +1,108 @@ +/* eslint-disable no-nested-ternary */ +import { ForbiddenError, MongoAbility, subject } from "@casl/ability"; +import { z } from "zod"; + import { TOrganizations } from "@app/db/schemas"; -import { ForbiddenRequestError, UnauthorizedError } from "@app/lib/errors"; +import { BadRequestError, ForbiddenRequestError, UnauthorizedError } from "@app/lib/errors"; import { ActorAuthMethod, AuthMethod } from "@app/services/auth/auth-type"; +import { + ProjectPermissionSecretActions, + ProjectPermissionSet, + ProjectPermissionSub, + ProjectPermissionV2Schema, + SecretSubjectFields +} from "./project-permission"; + +export function CheckForbiddenErrorSecretsSubject( + permission: MongoAbility, + action: Extract< + ProjectPermissionSecretActions, + ProjectPermissionSecretActions.ReadValue | ProjectPermissionSecretActions.DescribeSecret + >, + subjectFields?: SecretSubjectFields +) { + try { + if (subjectFields) { + ForbiddenError.from(permission).throwUnlessCan(action, subject(ProjectPermissionSub.Secrets, subjectFields)); + } else { + ForbiddenError.from(permission).throwUnlessCan(action, ProjectPermissionSub.Secrets); + } + } catch { + if (subjectFields) { + ForbiddenError.from(permission).throwUnlessCan( + ProjectPermissionSecretActions.DescribeAndReadValue, + subject(ProjectPermissionSub.Secrets, subjectFields) + ); + } else { + ForbiddenError.from(permission).throwUnlessCan( + ProjectPermissionSecretActions.DescribeAndReadValue, + ProjectPermissionSub.Secrets + ); + } + } +} + +export function CheckCanSecretsSubject( + permission: MongoAbility, + action: Extract< + ProjectPermissionSecretActions, + ProjectPermissionSecretActions.DescribeSecret | ProjectPermissionSecretActions.ReadValue + >, + subjectFields?: SecretSubjectFields +) { + let canNewPermission = false; + let canOldPermission = false; + + if (subjectFields) { + canNewPermission = permission.can(action, subject(ProjectPermissionSub.Secrets, subjectFields)); + canOldPermission = permission.can( + ProjectPermissionSecretActions.DescribeAndReadValue, + subject(ProjectPermissionSub.Secrets, subjectFields) + ); + } else { + canNewPermission = permission.can(action, ProjectPermissionSub.Secrets); + canOldPermission = permission.can( + ProjectPermissionSecretActions.DescribeAndReadValue, + ProjectPermissionSub.Secrets + ); + } + + return canNewPermission || canOldPermission; +} + +const OptionalArrayPermissionSchema = ProjectPermissionV2Schema.array().optional(); +export function checkForInvalidPermissionCombination(permissions: z.infer) { + if (!permissions) return; + + for (const permission of permissions) { + if (permission.subject === ProjectPermissionSub.Secrets) { + if (permission.action.includes(ProjectPermissionSecretActions.DescribeAndReadValue)) { + const hasReadValue = permission.action.includes(ProjectPermissionSecretActions.ReadValue); + const hasDescribeSecret = permission.action.includes(ProjectPermissionSecretActions.DescribeSecret); + + if (!hasReadValue && !hasDescribeSecret) return; + + const hasBothDescribeAndReadValue = + permission.action.includes(ProjectPermissionSecretActions.DescribeSecret) && + permission.action.includes(ProjectPermissionSecretActions.ReadValue); + + throw new BadRequestError({ + message: `You have selected Full Read Access, and ${ + hasBothDescribeAndReadValue + ? "both Read Value and Describe Secret" + : hasReadValue + ? "Read Value" + : hasDescribeSecret + ? "Describe Secret" + : "" + }. You cannot select Read Value or Describe Secret if you have selected Full Read Access.` + }); + } + } + } +} + function isAuthMethodSaml(actorAuthMethod: ActorAuthMethod) { if (!actorAuthMethod) return false; diff --git a/backend/src/ee/services/permission/project-permission.ts b/backend/src/ee/services/permission/project-permission.ts index 3f69c481a..6256c9965 100644 --- a/backend/src/ee/services/permission/project-permission.ts +++ b/backend/src/ee/services/permission/project-permission.ts @@ -18,7 +18,8 @@ export enum ProjectPermissionActions { } export enum ProjectPermissionSecretActions { - DescribeSecret = "read", + DescribeAndReadValue = "read", + DescribeSecret = "describeSecret", ReadValue = "readValue", Create = "create", Edit = "edit", @@ -564,6 +565,7 @@ const buildAdminPermissionRules = () => { can( [ + // not adding DescribeAndReadValue, because it's already covered by DescribeSecret and ReadValue ProjectPermissionSecretActions.DescribeSecret, ProjectPermissionSecretActions.ReadValue, ProjectPermissionSecretActions.Create, @@ -632,6 +634,7 @@ const buildMemberPermissionRules = () => { can( [ + // not adding DescribeAndReadValue, because it's already covered by DescribeSecret and ReadValue ProjectPermissionSecretActions.DescribeSecret, ProjectPermissionSecretActions.ReadValue, ProjectPermissionSecretActions.Edit, @@ -808,6 +811,7 @@ export const projectMemberPermissions = buildMemberPermissionRules(); const buildViewerPermissionRules = () => { const { can, rules } = new AbilityBuilder>(createMongoAbility); + // not adding DescribeAndReadValue, because it's already covered by DescribeSecret and ReadValue can(ProjectPermissionSecretActions.DescribeSecret, ProjectPermissionSub.Secrets); can(ProjectPermissionSecretActions.ReadValue, ProjectPermissionSub.Secrets); can(ProjectPermissionActions.Read, ProjectPermissionSub.SecretFolders); @@ -852,7 +856,6 @@ export const buildServiceTokenProjectPermission = ( ) => { const canWrite = permission.includes("write"); const canRead = permission.includes("read"); - const canReadValue = permission.includes("readValue"); const { can, build } = new AbilityBuilder>(createMongoAbility); scopes.forEach(({ secretPath, environment }) => { @@ -876,7 +879,7 @@ export const buildServiceTokenProjectPermission = ( environment }); } - if (canRead) { + if (canRead && subject !== ProjectPermissionSub.Secrets) { can(ProjectPermissionActions.Read, subject, { // @ts-expect-error type secretPath: { $glob: secretPath }, @@ -884,12 +887,18 @@ export const buildServiceTokenProjectPermission = ( }); } - if (subject === ProjectPermissionSub.Secrets && canReadValue) { + if (subject === ProjectPermissionSub.Secrets && canRead) { // @ts-expect-error type can(ProjectPermissionSecretActions.ReadValue, subject as ProjectPermissionSub.Secrets, { secretPath: { $glob: secretPath }, environment }); + + // @ts-expect-error type + can(ProjectPermissionSecretActions.DescribeSecret, subject as ProjectPermissionSub.Secrets, { + secretPath: { $glob: secretPath }, + environment + }); } } ); diff --git a/backend/src/ee/services/secret-approval-request/secret-approval-request-service.ts b/backend/src/ee/services/secret-approval-request/secret-approval-request-service.ts index 8efae75e6..9f3ec7111 100644 --- a/backend/src/ee/services/secret-approval-request/secret-approval-request-service.ts +++ b/backend/src/ee/services/secret-approval-request/secret-approval-request-service.ts @@ -77,6 +77,7 @@ import { TSecretApprovalDetailsDTO, TStatusChangeDTO } from "./secret-approval-request-types"; +import { CheckForbiddenErrorSecretsSubject } from "../permission/permission-fns"; type TSecretApprovalRequestServiceFactoryDep = { permissionService: Pick; @@ -917,10 +918,11 @@ export const secretApprovalRequestServiceFactory = ({ actorOrgId, actionProjectType: ActionProjectType.SecretManager }); - ForbiddenError.from(permission).throwUnlessCan( - ProjectPermissionSecretActions.ReadValue, - subject(ProjectPermissionSub.Secrets, { environment, secretPath }) - ); + + CheckForbiddenErrorSecretsSubject(permission, ProjectPermissionSecretActions.ReadValue, { + environment, + secretPath + }); await projectDAL.checkProjectUpgradeStatus(projectId); diff --git a/backend/src/ee/services/secret-snapshot/secret-snapshot-service.ts b/backend/src/ee/services/secret-snapshot/secret-snapshot-service.ts index 397824904..6afc6bca3 100644 --- a/backend/src/ee/services/secret-snapshot/secret-snapshot-service.ts +++ b/backend/src/ee/services/secret-snapshot/secret-snapshot-service.ts @@ -1,6 +1,6 @@ /* eslint-disable @typescript-eslint/no-unsafe-assignment,@typescript-eslint/no-unsafe-member-access,@typescript-eslint/no-unsafe-argument */ // akhilmhdh: I did this, quite strange bug with eslint. Everything do have a type stil has this error -import { ForbiddenError, subject } from "@casl/ability"; +import { ForbiddenError } from "@casl/ability"; import { ActionProjectType, TableName, TSecretTagJunctionInsert, TSecretV2TagJunctionInsert } from "@app/db/schemas"; import { decryptSymmetric128BitHexKeyUTF8 } from "@app/lib/crypto"; @@ -12,6 +12,7 @@ import { TKmsServiceFactory } from "@app/services/kms/kms-service"; import { KmsDataKey } from "@app/services/kms/kms-types"; import { TProjectBotServiceFactory } from "@app/services/project-bot/project-bot-service"; import { TSecretDALFactory } from "@app/services/secret/secret-dal"; +import { INFISICAL_SECRET_VALUE_HIDDEN_MASK } from "@app/services/secret/secret-fns"; import { TSecretVersionDALFactory } from "@app/services/secret/secret-version-dal"; import { TSecretVersionTagDALFactory } from "@app/services/secret/secret-version-tag-dal"; import { TSecretFolderDALFactory } from "@app/services/secret-folder/secret-folder-dal"; @@ -22,6 +23,7 @@ import { TSecretVersionV2DALFactory } from "@app/services/secret-v2-bridge/secre import { TSecretVersionV2TagDALFactory } from "@app/services/secret-v2-bridge/secret-version-tag-dal"; import { TLicenseServiceFactory } from "../license/license-service"; +import { CheckCanSecretsSubject, CheckForbiddenErrorSecretsSubject } from "../permission/permission-fns"; import { TPermissionServiceFactory } from "../permission/permission-service"; import { ProjectPermissionActions, @@ -39,7 +41,6 @@ import { TSnapshotFolderDALFactory } from "./snapshot-folder-dal"; import { TSnapshotSecretDALFactory } from "./snapshot-secret-dal"; import { TSnapshotSecretV2DALFactory } from "./snapshot-secret-v2-dal"; import { getFullFolderPath } from "./snapshot-service-fns"; -import { INFISICAL_SECRET_VALUE_HIDDEN_MASK } from "@app/services/secret/secret-fns"; type TSecretSnapshotServiceFactoryDep = { snapshotDAL: TSnapshotDALFactory; @@ -102,10 +103,10 @@ export const secretSnapshotServiceFactory = ({ ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.SecretRollback); // We need to check if the user has access to the secrets in the folder. If we don't do this, a user could theoretically access snapshot secret values even if they don't have read access to the secrets in the folder. - ForbiddenError.from(permission).throwUnlessCan( - ProjectPermissionSecretActions.DescribeSecret, - subject(ProjectPermissionSub.Secrets, { environment, secretPath: path }) - ); + CheckForbiddenErrorSecretsSubject(permission, ProjectPermissionSecretActions.DescribeSecret, { + environment, + secretPath: path + }); const folder = await folderDAL.findBySecretPath(projectId, environment, path); if (!folder) { @@ -139,10 +140,10 @@ export const secretSnapshotServiceFactory = ({ ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.SecretRollback); // We need to check if the user has access to the secrets in the folder. If we don't do this, a user could theoretically access snapshot secret values even if they don't have read access to the secrets in the folder. - ForbiddenError.from(permission).throwUnlessCan( - ProjectPermissionSecretActions.DescribeSecret, - subject(ProjectPermissionSub.Secrets, { environment, secretPath: path }) - ); + CheckForbiddenErrorSecretsSubject(permission, ProjectPermissionSecretActions.DescribeSecret, { + environment, + secretPath: path + }); const folder = await folderDAL.findBySecretPath(projectId, environment, path); if (!folder) @@ -186,15 +187,12 @@ export const secretSnapshotServiceFactory = ({ snapshotDetails = { ...encryptedSnapshotDetails, secretVersions: encryptedSnapshotDetails.secretVersions.map((el) => { - const canReadValue = permission.can( - ProjectPermissionSecretActions.ReadValue, - subject(ProjectPermissionSub.Secrets, { - environment: encryptedSnapshotDetails.environment.slug, - secretPath: fullFolderPath, - secretName: el.key, - secretTags: el.tags.length ? el.tags.map((tag) => tag.slug) : undefined - }) - ); + const canReadValue = CheckCanSecretsSubject(permission, ProjectPermissionSecretActions.ReadValue, { + environment: encryptedSnapshotDetails.environment.slug, + secretPath: fullFolderPath, + secretName: el.key, + secretTags: el.tags.length ? el.tags.map((tag) => tag.slug) : undefined + }); let secretValue = ""; if (canReadValue) { @@ -238,15 +236,12 @@ export const secretSnapshotServiceFactory = ({ key: botKey }); - const canReadValue = permission.can( - ProjectPermissionSecretActions.ReadValue, - subject(ProjectPermissionSub.Secrets, { - environment: encryptedSnapshotDetails.environment.slug, - secretPath: fullFolderPath, - secretName: secretKey, - secretTags: el.tags.length ? el.tags.map((tag) => tag.slug) : undefined - }) - ); + const canReadValue = CheckCanSecretsSubject(permission, ProjectPermissionSecretActions.ReadValue, { + environment: encryptedSnapshotDetails.environment.slug, + secretPath: fullFolderPath, + secretName: secretKey, + secretTags: el.tags.length ? el.tags.map((tag) => tag.slug) : undefined + }); let secretValue = ""; diff --git a/backend/src/server/routes/v2/service-token-router.ts b/backend/src/server/routes/v2/service-token-router.ts index 1a84c9123..fb10f17db 100644 --- a/backend/src/server/routes/v2/service-token-router.ts +++ b/backend/src/server/routes/v2/service-token-router.ts @@ -94,7 +94,7 @@ export const registerServiceTokenRouter = async (server: FastifyZodProvider) => iv: z.string().trim(), tag: z.string().trim(), expiresIn: z.number().nullable(), - permissions: z.enum(["read", "write", "readValue"]).array() + permissions: z.enum(["read", "write"]).array() }), response: { 200: z.object({ diff --git a/backend/src/services/integration/integration-service.ts b/backend/src/services/integration/integration-service.ts index f72357964..3f7c55896 100644 --- a/backend/src/services/integration/integration-service.ts +++ b/backend/src/services/integration/integration-service.ts @@ -1,6 +1,7 @@ -import { ForbiddenError, subject } from "@casl/ability"; +import { ForbiddenError } from "@casl/ability"; import { ActionProjectType } from "@app/db/schemas"; +import { CheckForbiddenErrorSecretsSubject } from "@app/ee/services/permission/permission-fns"; import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service"; import { ProjectPermissionActions, @@ -95,13 +96,10 @@ export const integrationServiceFactory = ({ }); ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Create, ProjectPermissionSub.Integrations); - ForbiddenError.from(permission).throwUnlessCan( - ProjectPermissionSecretActions.ReadValue, - subject(ProjectPermissionSub.Secrets, { - environment: sourceEnvironment, - secretPath - }) - ); + CheckForbiddenErrorSecretsSubject(permission, ProjectPermissionSecretActions.ReadValue, { + environment: sourceEnvironment, + secretPath + }); const folder = await folderDAL.findBySecretPath(integrationAuth.projectId, sourceEnvironment, secretPath); if (!folder) { @@ -178,13 +176,10 @@ export const integrationServiceFactory = ({ const newSecretPath = secretPath || integration.secretPath; if (environment || secretPath) { - ForbiddenError.from(permission).throwUnlessCan( - ProjectPermissionSecretActions.ReadValue, - subject(ProjectPermissionSub.Secrets, { - environment: newEnvironment, - secretPath: newSecretPath - }) - ); + CheckForbiddenErrorSecretsSubject(permission, ProjectPermissionSecretActions.ReadValue, { + environment: newEnvironment, + secretPath: newSecretPath + }); } const folder = await folderDAL.findBySecretPath(integration.projectId, newEnvironment, newSecretPath); diff --git a/backend/src/services/project/project-service.ts b/backend/src/services/project/project-service.ts index 5367a70cd..b21cc99fe 100644 --- a/backend/src/services/project/project-service.ts +++ b/backend/src/services/project/project-service.ts @@ -10,6 +10,7 @@ import { } from "@app/db/schemas"; import { TLicenseServiceFactory } from "@app/ee/services/license/license-service"; import { OrgPermissionActions, OrgPermissionSubjects } from "@app/ee/services/permission/org-permission"; +import { CheckForbiddenErrorSecretsSubject } from "@app/ee/services/permission/permission-fns"; import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service"; import { ProjectPermissionActions, @@ -764,10 +765,7 @@ export const projectServiceFactory = ({ actorOrgId, actionProjectType: ActionProjectType.Any }); - ForbiddenError.from(permission).throwUnlessCan( - ProjectPermissionSecretActions.DescribeSecret, - ProjectPermissionSub.Secrets - ); + CheckForbiddenErrorSecretsSubject(permission, ProjectPermissionSecretActions.DescribeSecret); const project = await projectDAL.findProjectById(projectId); diff --git a/backend/src/services/secret-import/secret-import-service.ts b/backend/src/services/secret-import/secret-import-service.ts index 6c412edc0..27ed12c84 100644 --- a/backend/src/services/secret-import/secret-import-service.ts +++ b/backend/src/services/secret-import/secret-import-service.ts @@ -4,6 +4,7 @@ import { ForbiddenError, subject } from "@casl/ability"; import { ActionProjectType, TableName } from "@app/db/schemas"; import { TLicenseServiceFactory } from "@app/ee/services/license/license-service"; +import { CheckCanSecretsSubject, CheckForbiddenErrorSecretsSubject } from "@app/ee/services/permission/permission-fns"; import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service"; import { ProjectPermissionActions, @@ -93,13 +94,11 @@ export const secretImportServiceFactory = ({ ); // check if user has permission to import from target path - ForbiddenError.from(permission).throwUnlessCan( - ProjectPermissionSecretActions.DescribeSecret, - subject(ProjectPermissionSub.Secrets, { - environment: data.environment, - secretPath: data.path - }) - ); + CheckForbiddenErrorSecretsSubject(permission, ProjectPermissionSecretActions.DescribeSecret, { + environment: data.environment, + secretPath: data.path + }); + if (isReplication) { const plan = await licenseService.getPlan(actorOrgId); if (!plan.secretApproval) { @@ -405,13 +404,10 @@ export const secretImportServiceFactory = ({ if (!secretImportDoc.isReplication) throw new BadRequestError({ message: "Import is not in replication mode" }); // check if user has permission to import from target path - ForbiddenError.from(permission).throwUnlessCan( - ProjectPermissionSecretActions.DescribeSecret, - subject(ProjectPermissionSub.Secrets, { - environment: secretImportDoc.importEnv.slug, - secretPath: secretImportDoc.importPath - }) - ); + CheckForbiddenErrorSecretsSubject(permission, ProjectPermissionSecretActions.DescribeSecret, { + environment: secretImportDoc.importEnv.slug, + secretPath: secretImportDoc.importPath + }); await projectDAL.checkProjectUpgradeStatus(projectId); @@ -599,14 +595,12 @@ export const secretImportServiceFactory = ({ // so anything based on this order will also be in right position const secretImports = await secretImportDAL.find({ folderId: folder.id, isReplication: false }); const allowedImports = secretImports.filter((el) => - permission.can( - ProjectPermissionSecretActions.ReadValue, - subject(ProjectPermissionSub.Secrets, { - environment: el.importEnv.slug, - secretPath: el.importPath - }) - ) + CheckCanSecretsSubject(permission, ProjectPermissionSecretActions.ReadValue, { + environment: el.importEnv.slug, + secretPath: el.importPath + }) ); + return fnSecretsFromImports({ allowedImports, folderDAL, secretDAL, secretImportDAL }); }; @@ -651,16 +645,14 @@ export const secretImportServiceFactory = ({ secretImportDAL, decryptor: (value) => (value ? secretManagerDecryptor({ cipherTextBlob: value }).toString() : ""), hasSecretAccess: (expandEnvironment, expandSecretPath, expandSecretKey, expandSecretTags) => - permission.can( - ProjectPermissionSecretActions.ReadValue, - subject(ProjectPermissionSub.Secrets, { - environment: expandEnvironment, - secretPath: expandSecretPath, - secretName: expandSecretKey, - secretTags: expandSecretTags - }) - ) + CheckCanSecretsSubject(permission, ProjectPermissionSecretActions.ReadValue, { + environment: expandEnvironment, + secretPath: expandSecretPath, + secretName: expandSecretKey, + secretTags: expandSecretTags + }) }); + return importedSecrets; } @@ -671,13 +663,10 @@ export const secretImportServiceFactory = ({ }); const allowedImports = secretImports.filter((el) => - permission.can( - ProjectPermissionSecretActions.ReadValue, - subject(ProjectPermissionSub.Secrets, { - environment: el.importEnv.slug, - secretPath: el.importPath - }) - ) + CheckCanSecretsSubject(permission, ProjectPermissionSecretActions.ReadValue, { + environment: el.importEnv.slug, + secretPath: el.importPath + }) ); const importedSecrets = await fnSecretsFromImports({ allowedImports, diff --git a/backend/src/services/secret-sync/secret-sync-service.ts b/backend/src/services/secret-sync/secret-sync-service.ts index 7b92a463c..d74ea120e 100644 --- a/backend/src/services/secret-sync/secret-sync-service.ts +++ b/backend/src/services/secret-sync/secret-sync-service.ts @@ -1,6 +1,7 @@ -import { ForbiddenError, subject } from "@casl/ability"; +import { ForbiddenError } from "@casl/ability"; import { ActionProjectType } from "@app/db/schemas"; +import { CheckForbiddenErrorSecretsSubject } from "@app/ee/services/permission/permission-fns"; import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service"; import { ProjectPermissionSecretActions, @@ -178,13 +179,10 @@ export const secretSyncServiceFactory = ({ ProjectPermissionSub.SecretSyncs ); - ForbiddenError.from(projectPermission).throwUnlessCan( - ProjectPermissionSecretActions.ReadValue, - subject(ProjectPermissionSub.Secrets, { - environment, - secretPath - }) - ); + CheckForbiddenErrorSecretsSubject(projectPermission, ProjectPermissionSecretActions.ReadValue, { + environment, + secretPath + }); const folder = await folderDAL.findBySecretPath(projectId, environment, secretPath); @@ -269,13 +267,10 @@ export const secretSyncServiceFactory = ({ if (!updatedEnvironment || !updatedSecretPath) throw new BadRequestError({ message: "Must specify both source environment and secret path" }); - ForbiddenError.from(permission).throwUnlessCan( - ProjectPermissionSecretActions.ReadValue, - subject(ProjectPermissionSub.Secrets, { - environment: updatedEnvironment, - secretPath: updatedSecretPath - }) - ); + CheckForbiddenErrorSecretsSubject(permission, ProjectPermissionSecretActions.ReadValue, { + environment: updatedEnvironment, + secretPath: updatedSecretPath + }); const newFolder = await folderDAL.findBySecretPath(secretSync.projectId, updatedEnvironment, updatedSecretPath); diff --git a/backend/src/services/secret-v2-bridge/secret-v2-bridge-service.ts b/backend/src/services/secret-v2-bridge/secret-v2-bridge-service.ts index 3b7f04907..8ebc0a582 100644 --- a/backend/src/services/secret-v2-bridge/secret-v2-bridge-service.ts +++ b/backend/src/services/secret-v2-bridge/secret-v2-bridge-service.ts @@ -10,6 +10,7 @@ import { TableName, TSecretsV2 } from "@app/db/schemas"; +import { CheckCanSecretsSubject, CheckForbiddenErrorSecretsSubject } from "@app/ee/services/permission/permission-fns"; import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service"; import { ProjectPermissionActions, @@ -540,17 +541,14 @@ export const secretV2BridgeServiceFactory = ({ }); } - const secretValueHidden = !permission.can( - ProjectPermissionSecretActions.ReadValue, - subject(ProjectPermissionSub.Secrets, { - environment, - secretPath, - secretName: inputSecret.secretName, - ...(tagsToCheck.length && { - secretTags: tagsToCheck.map((el) => el.slug) - }) + const secretValueHidden = !CheckCanSecretsSubject(permission, ProjectPermissionSecretActions.ReadValue, { + environment, + secretPath, + secretName: inputSecret.secretName, + ...(tagsToCheck.length && { + secretTags: tagsToCheck.map((el) => el.slug) }) - ); + }); return reshapeBridgeSecret( projectId, @@ -651,15 +649,12 @@ export const secretV2BridgeServiceFactory = ({ projectId }); - const secretValueHidden = !permission.can( - ProjectPermissionSecretActions.ReadValue, - subject(ProjectPermissionSub.Secrets, { - environment, - secretPath, - secretName: secretToDelete.key, - secretTags: secretToDelete.tags?.map((el) => el.slug) - }) - ); + const secretValueHidden = !CheckCanSecretsSubject(permission, ProjectPermissionSecretActions.ReadValue, { + environment, + secretPath, + secretName: secretToDelete.key, + secretTags: secretToDelete.tags?.map((el) => el.slug) + }); return reshapeBridgeSecret( projectId, @@ -702,11 +697,7 @@ export const secretV2BridgeServiceFactory = ({ actorOrgId, actionProjectType: ActionProjectType.SecretManager }); - - ForbiddenError.from(permission).throwUnlessCan( - ProjectPermissionSecretActions.DescribeSecret, - ProjectPermissionSub.Secrets - ); + CheckForbiddenErrorSecretsSubject(permission, ProjectPermissionSecretActions.DescribeSecret); } const folders = await folderDAL.findBySecretPathMultiEnv(projectId, environments, path); @@ -752,11 +743,7 @@ export const secretV2BridgeServiceFactory = ({ actorOrgId, actionProjectType: ActionProjectType.SecretManager }); - - ForbiddenError.from(permission).throwUnlessCan( - ProjectPermissionSecretActions.DescribeSecret, - ProjectPermissionSub.Secrets - ); + CheckForbiddenErrorSecretsSubject(permission, ProjectPermissionSecretActions.DescribeSecret); const folder = await folderDAL.findBySecretPath(projectId, environment, path); if (!folder) return 0; @@ -791,8 +778,20 @@ export const secretV2BridgeServiceFactory = ({ }); const decryptedSecrets = secrets - .filter((el) => - projectPermission.can( + .filter((el) => { + if ( + filterByAction === ProjectPermissionSecretActions.ReadValue || + filterByAction === ProjectPermissionSecretActions.DescribeSecret + ) { + return CheckCanSecretsSubject(projectPermission, filterByAction, { + environment: groupedFolderMappings[el.folderId][0].environment, + secretPath: groupedFolderMappings[el.folderId][0].path, + secretName: el.key, + secretTags: el.tags.map((i) => i.slug) + }); + } + + return projectPermission.can( filterByAction, subject(ProjectPermissionSub.Secrets, { environment: groupedFolderMappings[el.folderId][0].environment, @@ -800,19 +799,16 @@ export const secretV2BridgeServiceFactory = ({ secretName: el.key, secretTags: el.tags.map((i) => i.slug) }) - ) - ) + ); + }) .map((secret) => { // Note(Daniel): This is only relevant if the filterAction isn't set to ReadValue. This is needed for the frontend. - const secretValueHidden = !projectPermission.can( - ProjectPermissionSecretActions.ReadValue, - subject(ProjectPermissionSub.Secrets, { - environment: groupedFolderMappings[secret.folderId][0].environment, - secretPath: groupedFolderMappings[secret.folderId][0].path, - secretName: secret.key, - secretTags: secret.tags.map((i) => i.slug) - }) - ); + const secretValueHidden = !CheckCanSecretsSubject(projectPermission, ProjectPermissionSecretActions.ReadValue, { + environment: groupedFolderMappings[secret.folderId][0].environment, + secretPath: groupedFolderMappings[secret.folderId][0].path, + secretName: secret.key, + secretTags: secret.tags.map((i) => i.slug) + }); return reshapeBridgeSecret( projectId, @@ -858,10 +854,7 @@ export const secretV2BridgeServiceFactory = ({ actionProjectType: ActionProjectType.SecretManager }); if (!isInternal) { - ForbiddenError.from(permission).throwUnlessCan( - ProjectPermissionSecretActions.DescribeSecret, - ProjectPermissionSub.Secrets - ); + CheckForbiddenErrorSecretsSubject(permission, ProjectPermissionSecretActions.DescribeSecret); } const folders = await folderDAL.findBySecretPathMultiEnv(projectId, environments, path); @@ -913,15 +906,11 @@ export const secretV2BridgeServiceFactory = ({ actorOrgId, actionProjectType: ActionProjectType.SecretManager }); - - ForbiddenError.from(permission).throwUnlessCan( - ProjectPermissionSecretActions.DescribeSecret, - subject(ProjectPermissionSub.Secrets, { - environment, - secretPath: path, - secretTags: params.tagSlugs - }) - ); + CheckForbiddenErrorSecretsSubject(permission, ProjectPermissionSecretActions.DescribeSecret, { + environment, + secretPath: path, + secretTags: params.tagSlugs + }); let paths: { folderId: string; path: string }[] = []; @@ -960,15 +949,12 @@ export const secretV2BridgeServiceFactory = ({ const decryptedSecrets = secrets .filter((el) => { - const canDescribeSecret = permission.can( - ProjectPermissionSecretActions.DescribeSecret, - subject(ProjectPermissionSub.Secrets, { - environment, - secretPath: groupedPaths[el.folderId][0].path, - secretName: el.key, - secretTags: el.tags.map((i) => i.slug) - }) - ); + const canDescribeSecret = CheckCanSecretsSubject(permission, ProjectPermissionSecretActions.DescribeSecret, { + environment, + secretPath: groupedPaths[el.folderId][0].path, + secretName: el.key, + secretTags: el.tags.map((i) => i.slug) + }); if (!canDescribeSecret) { return false; @@ -977,14 +963,15 @@ export const secretV2BridgeServiceFactory = ({ if (viewSecretValue) { // Recursive secret, should be filtered out if (groupedPaths[el.folderId][0].path !== path) { - const canReadRecursiveSecretValue = permission.can( + const canReadRecursiveSecretValue = CheckCanSecretsSubject( + permission, ProjectPermissionSecretActions.ReadValue, - subject(ProjectPermissionSub.Secrets, { + { environment, secretPath: groupedPaths[el.folderId][0].path, secretName: el.key, secretTags: el.tags.map((i) => i.slug) - }) + } ); if (!canReadRecursiveSecretValue) { @@ -993,15 +980,12 @@ export const secretV2BridgeServiceFactory = ({ } if (throwOnMissingReadValuePermission) { - ForbiddenError.from(permission).throwUnlessCan( - ProjectPermissionSecretActions.ReadValue, - subject(ProjectPermissionSub.Secrets, { - environment, - secretPath: groupedPaths[el.folderId][0].path, - secretName: el.key, - secretTags: el.tags.map((i) => i.slug) - }) - ); + CheckForbiddenErrorSecretsSubject(permission, ProjectPermissionSecretActions.ReadValue, { + environment, + secretPath: groupedPaths[el.folderId][0].path, + secretName: el.key, + secretTags: el.tags.map((i) => i.slug) + }); } // Else, we do nothing. Because we don't want to filter out the secret, OR throw an error. // If the user doesn't have access to read the value, in the below map function, we mask the secret value and return the secret with a hidden value. @@ -1014,15 +998,12 @@ export const secretV2BridgeServiceFactory = ({ const secretValueHidden = !viewSecretValue || - !permission.can( - ProjectPermissionSecretActions.ReadValue, - subject(ProjectPermissionSub.Secrets, { - environment, - secretPath: groupedPaths[secret.folderId][0].path, - secretName: secret.key, - secretTags: secret.tags.map((i) => i.slug) - }) - ); + !CheckCanSecretsSubject(permission, ProjectPermissionSecretActions.ReadValue, { + environment, + secretPath: groupedPaths[secret.folderId][0].path, + secretName: secret.key, + secretTags: secret.tags.map((i) => i.slug) + }); return reshapeBridgeSecret( projectId, @@ -1047,15 +1028,12 @@ export const secretV2BridgeServiceFactory = ({ secretDAL, decryptSecretValue: (value) => (value ? secretManagerDecryptor({ cipherTextBlob: value }).toString() : undefined), canExpandValue: (expandEnvironment, expandSecretPath, expandSecretKey, expandSecretTags) => - permission.can( - ProjectPermissionSecretActions.ReadValue, - subject(ProjectPermissionSub.Secrets, { - environment: expandEnvironment, - secretPath: expandSecretPath, - secretName: expandSecretKey, - secretTags: expandSecretTags - }) - ) + CheckCanSecretsSubject(permission, ProjectPermissionSecretActions.ReadValue, { + environment: expandEnvironment, + secretPath: expandSecretPath, + secretName: expandSecretKey, + secretTags: expandSecretTags + }) }); if (shouldExpandSecretReferences) { @@ -1095,25 +1073,19 @@ export const secretV2BridgeServiceFactory = ({ expandSecretReferences, decryptor: (value) => (value ? secretManagerDecryptor({ cipherTextBlob: value }).toString() : ""), hasSecretAccess: (expandEnvironment, expandSecretPath, expandSecretKey, expandSecretTags) => { - const canDescribe = permission.can( - ProjectPermissionSecretActions.DescribeSecret, - subject(ProjectPermissionSub.Secrets, { - environment: expandEnvironment, - secretPath: expandSecretPath, - secretName: expandSecretKey, - secretTags: expandSecretTags - }) - ); + const canDescribe = CheckCanSecretsSubject(permission, ProjectPermissionSecretActions.DescribeSecret, { + environment: expandEnvironment, + secretPath: expandSecretPath, + secretName: expandSecretKey, + secretTags: expandSecretTags + }); - const canReadValue = permission.can( - ProjectPermissionSecretActions.ReadValue, - subject(ProjectPermissionSub.Secrets, { - environment: expandEnvironment, - secretPath: expandSecretPath, - secretName: expandSecretKey, - secretTags: expandSecretTags - }) - ); + const canReadValue = CheckCanSecretsSubject(permission, ProjectPermissionSecretActions.ReadValue, { + environment: expandEnvironment, + secretPath: expandSecretPath, + secretName: expandSecretKey, + secretTags: expandSecretTags + }); return viewSecretValue ? canDescribe && canReadValue : canDescribe; } @@ -1264,15 +1236,12 @@ export const secretV2BridgeServiceFactory = ({ }) )); - ForbiddenError.from(permission).throwUnlessCan( - ProjectPermissionSecretActions.DescribeSecret, - subject(ProjectPermissionSub.Secrets, { - environment, - secretPath: path, - secretName, - secretTags: (secret?.tags || []).map((el) => el.slug) - }) - ); + CheckForbiddenErrorSecretsSubject(permission, ProjectPermissionSecretActions.DescribeSecret, { + environment, + secretPath: path, + secretName, + secretTags: (secret?.tags || []).map((el) => el.slug) + }); // this will throw if the user doesn't have read value permission no matter what // because if its an expansion, it will fully depend on the value. @@ -1282,15 +1251,12 @@ export const secretV2BridgeServiceFactory = ({ secretDAL, decryptSecretValue: (value) => (value ? secretManagerDecryptor({ cipherTextBlob: value }).toString() : undefined), canExpandValue: (expandEnvironment, expandSecretPath, expandSecretKey, expandSecretTags) => { - return permission.can( - ProjectPermissionSecretActions.ReadValue, - subject(ProjectPermissionSub.Secrets, { - environment: expandEnvironment, - secretPath: expandSecretPath, - secretName: expandSecretKey, - secretTags: expandSecretTags - }) - ); + return CheckCanSecretsSubject(permission, ProjectPermissionSecretActions.ReadValue, { + environment: expandEnvironment, + secretPath: expandSecretPath, + secretName: expandSecretKey, + secretTags: expandSecretTags + }); } }); @@ -1310,15 +1276,12 @@ export const secretV2BridgeServiceFactory = ({ decryptor: (value) => (value ? secretManagerDecryptor({ cipherTextBlob: value }).toString() : ""), expandSecretReferences: shouldExpandSecretReferences ? expandSecretReferences : undefined, hasSecretAccess: (expandEnvironment, expandSecretPath, expandSecretKey, expandSecretTags) => { - return permission.can( - ProjectPermissionSecretActions.DescribeSecret, - subject(ProjectPermissionSub.Secrets, { - environment: expandEnvironment, - secretPath: expandSecretPath, - secretName: expandSecretKey, - secretTags: expandSecretTags - }) - ); + return CheckCanSecretsSubject(permission, ProjectPermissionSecretActions.DescribeSecret, { + environment: expandEnvironment, + secretPath: expandSecretPath, + secretName: expandSecretKey, + secretTags: expandSecretTags + }); } }); @@ -1330,15 +1293,12 @@ export const secretV2BridgeServiceFactory = ({ if (viewSecretValue) { if ( - !permission.can( - ProjectPermissionSecretActions.ReadValue, - subject(ProjectPermissionSub.Secrets, { - environment: importedSecret.environment, - secretPath: importedSecrets[i].secretPath, - secretName: importedSecret.key, - secretTags: (importedSecret.secretTags || []).map((el) => el.slug) - }) - ) && + !CheckCanSecretsSubject(permission, ProjectPermissionSecretActions.ReadValue, { + environment: importedSecret.environment, + secretPath: importedSecrets[i].secretPath, + secretName: importedSecret.key, + secretTags: (importedSecret.secretTags || []).map((el) => el.slug) + }) && secretType !== SecretType.Personal ) { throw new ForbiddenRequestError({ @@ -1386,15 +1346,12 @@ export const secretV2BridgeServiceFactory = ({ if (viewSecretValue) { if ( - !permission.can( - ProjectPermissionSecretActions.ReadValue, - subject(ProjectPermissionSub.Secrets, { - environment, - secretPath: path, - secretName, - secretTags: (secret?.tags || []).map((el) => el.slug) - }) - ) && + !CheckCanSecretsSubject(permission, ProjectPermissionSecretActions.ReadValue, { + environment, + secretPath: path, + secretName, + secretTags: (secret?.tags || []).map((el) => el.slug) + }) && secretType !== SecretType.Personal ) { throw new ForbiddenRequestError({ @@ -1562,15 +1519,12 @@ export const secretV2BridgeServiceFactory = ({ }); return newSecrets.map((el) => { - const secretValueHidden = !permission.can( - ProjectPermissionSecretActions.ReadValue, - subject(ProjectPermissionSub.Secrets, { - environment, - secretPath, - secretName: el.key, - secretTags: el.tags?.map((i) => i.slug) - }) - ); + const secretValueHidden = !CheckCanSecretsSubject(permission, ProjectPermissionSecretActions.ReadValue, { + environment, + secretPath, + secretName: el.key, + secretTags: el.tags?.map((i) => i.slug) + }); return reshapeBridgeSecret( projectId, @@ -1899,15 +1853,12 @@ export const secretV2BridgeServiceFactory = ({ ); return updatedSecrets.map((el) => { - const secretValueHidden = !permission.can( - ProjectPermissionSecretActions.ReadValue, - subject(ProjectPermissionSub.Secrets, { - environment, - secretPath: el.secretPath, - secretName: el.key, - secretTags: el.tags.map((i) => i.slug) - }) - ); + const secretValueHidden = !CheckCanSecretsSubject(permission, ProjectPermissionSecretActions.ReadValue, { + environment, + secretPath: el.secretPath, + secretName: el.key, + secretTags: el.tags.map((i) => i.slug) + }); return { ...reshapeBridgeSecret( @@ -2032,15 +1983,12 @@ export const secretV2BridgeServiceFactory = ({ const secretValueHidden = !secretToDeleteMatch || - !permission.can( - ProjectPermissionSecretActions.ReadValue, - subject(ProjectPermissionSub.Secrets, { - environment, - secretPath, - secretName: el.key, - secretTags: secretToDeleteMatch.tags?.map((i) => i.slug) - }) - ); + !CheckCanSecretsSubject(permission, ProjectPermissionSecretActions.ReadValue, { + environment, + secretPath, + secretName: el.key, + secretTags: secretToDeleteMatch.tags?.map((i) => i.slug) + }); return reshapeBridgeSecret( projectId, @@ -2097,17 +2045,15 @@ export const secretV2BridgeServiceFactory = ({ sort: [["createdAt", "desc"]] }); return secretVersions.map((el) => { - const secretValueHidden = permission.cannot( - ProjectPermissionSecretActions.ReadValue, - subject(ProjectPermissionSub.Secrets, { - environment: folder.environment.envSlug, - secretPath: folderWithPath.path, - secretName: el.key, - ...(el.tags?.length && { - secretTags: el.tags.map((tag) => tag.slug) - }) + const secretValueHidden = !CheckCanSecretsSubject(permission, ProjectPermissionSecretActions.ReadValue, { + environment: folder.environment.envSlug, + secretPath: folderWithPath.path, + secretName: el.key, + ...(el.tags?.length && { + secretTags: el.tags.map((tag) => tag.slug) }) - ); + }); + return reshapeBridgeSecret( folder.projectId, folder.environment.envSlug, @@ -2224,15 +2170,27 @@ export const secretV2BridgeServiceFactory = ({ sourceSecrets.forEach((secret) => { for (const sourceAction of sourceActions) { - ForbiddenError.from(permission).throwUnlessCan( - sourceAction, - subject(ProjectPermissionSub.Secrets, { + if ( + sourceAction === ProjectPermissionSecretActions.DescribeSecret || + sourceAction === ProjectPermissionSecretActions.ReadValue + ) { + CheckForbiddenErrorSecretsSubject(permission, sourceAction, { environment: sourceEnvironment, secretPath: sourceSecretPath, secretName: secret.key, secretTags: secret.tags.map((el) => el.slug) - }) - ); + }); + } else { + ForbiddenError.from(permission).throwUnlessCan( + sourceAction, + subject(ProjectPermissionSub.Secrets, { + environment: sourceEnvironment, + secretPath: sourceSecretPath, + secretName: secret.key, + secretTags: secret.tags.map((el) => el.slug) + }) + ); + } } }); @@ -2555,10 +2513,10 @@ export const secretV2BridgeServiceFactory = ({ actionProjectType: ActionProjectType.SecretManager }); - ForbiddenError.from(permission).throwUnlessCan( - ProjectPermissionSecretActions.DescribeSecret, - subject(ProjectPermissionSub.Secrets, { environment, secretPath }) - ); + CheckForbiddenErrorSecretsSubject(permission, ProjectPermissionSecretActions.DescribeSecret, { + environment, + secretPath + }); const folder = await folderDAL.findBySecretPath(projectId, environment, secretPath); if (!folder) @@ -2579,15 +2537,12 @@ export const secretV2BridgeServiceFactory = ({ type: SecretType.Shared }); - ForbiddenError.from(permission).throwUnlessCan( - ProjectPermissionSecretActions.DescribeSecret, - subject(ProjectPermissionSub.Secrets, { - environment, - secretPath, - secretName, - secretTags: (secret?.tags || []).map((el) => el.slug) - }) - ); + CheckForbiddenErrorSecretsSubject(permission, ProjectPermissionSecretActions.DescribeSecret, { + environment, + secretPath, + secretName, + secretTags: (secret?.tags || []).map((el) => el.slug) + }); const decryptedSecretValue = secret.encryptedValue ? secretManagerDecryptor({ cipherTextBlob: secret.encryptedValue }).toString() @@ -2599,27 +2554,21 @@ export const secretV2BridgeServiceFactory = ({ secretDAL, decryptSecretValue: (value) => (value ? secretManagerDecryptor({ cipherTextBlob: value }).toString() : undefined), canExpandValue: (expandEnvironment, expandSecretPath, expandSecretName, expandSecretTags) => - permission.can( - ProjectPermissionSecretActions.ReadValue, - subject(ProjectPermissionSub.Secrets, { - environment: expandEnvironment, - secretPath: expandSecretPath, - secretName: expandSecretName, - secretTags: expandSecretTags - }) - ) + CheckCanSecretsSubject(permission, ProjectPermissionSecretActions.ReadValue, { + environment: expandEnvironment, + secretPath: expandSecretPath, + secretName: expandSecretName, + secretTags: expandSecretTags + }) }); if ( - !permission.can( - ProjectPermissionSecretActions.ReadValue, - subject(ProjectPermissionSub.Secrets, { - environment, - secretPath, - secretName, - secretTags: (secret?.tags || []).map((el) => el.slug) - }) - ) + !CheckCanSecretsSubject(permission, ProjectPermissionSecretActions.ReadValue, { + environment, + secretPath, + secretName, + secretTags: (secret?.tags || []).map((el) => el.slug) + }) ) { throw new ForbiddenRequestError({ message: `Unable to get secret reference tree for secret with key '${secretName}', because you don't have permission to view secret value.` diff --git a/backend/src/services/secret/secret-fns.ts b/backend/src/services/secret/secret-fns.ts index 4556528a8..ddaa054ab 100644 --- a/backend/src/services/secret/secret-fns.ts +++ b/backend/src/services/secret/secret-fns.ts @@ -1,5 +1,4 @@ /* eslint-disable no-await-in-loop */ -import { subject } from "@casl/ability"; import path from "path"; import { @@ -12,8 +11,9 @@ import { TSecretFolders, TSecrets } from "@app/db/schemas"; +import { CheckCanSecretsSubject } from "@app/ee/services/permission/permission-fns"; import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service"; -import { ProjectPermissionSecretActions, ProjectPermissionSub } from "@app/ee/services/permission/project-permission"; +import { ProjectPermissionSecretActions } from "@app/ee/services/permission/project-permission"; import { getConfig } from "@app/lib/config/env"; import { buildSecretBlindIndexFromName, @@ -191,13 +191,10 @@ export const recursivelyGetSecretPaths = ({ // Filter out paths that the user does not have permission to access, and paths that are not in the current path const allowedPaths = paths.filter( (folder) => - permission.can( - ProjectPermissionSecretActions.ReadValue, - subject(ProjectPermissionSub.Secrets, { - environment, - secretPath: folder.path - }) - ) && folder.path.startsWith(currentPath === "/" ? "" : currentPath) + CheckCanSecretsSubject(permission, ProjectPermissionSecretActions.ReadValue, { + environment, + secretPath: folder.path + }) && folder.path.startsWith(currentPath === "/" ? "" : currentPath) ); return allowedPaths; diff --git a/backend/src/services/secret/secret-service.ts b/backend/src/services/secret/secret-service.ts index 8af0deb2b..e3eb75bda 100644 --- a/backend/src/services/secret/secret-service.ts +++ b/backend/src/services/secret/secret-service.ts @@ -13,6 +13,7 @@ import { SecretType } from "@app/db/schemas"; import { TLicenseServiceFactory } from "@app/ee/services/license/license-service"; +import { CheckCanSecretsSubject, CheckForbiddenErrorSecretsSubject } from "@app/ee/services/permission/permission-fns"; import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service"; import { ProjectPermissionActions, @@ -452,13 +453,10 @@ export const secretServiceFactory = ({ }); } - const secretValueHidden = !permission.can( - ProjectPermissionSecretActions.ReadValue, - subject(ProjectPermissionSub.Secrets, { - environment, - secretPath: path - }) - ); + const secretValueHidden = !CheckCanSecretsSubject(permission, ProjectPermissionSecretActions.ReadValue, { + environment, + secretPath: path + }); return { ...updatedSecret[0], @@ -562,10 +560,10 @@ export const secretServiceFactory = ({ }); } - const secretValueHidden = !permission.can( - ProjectPermissionSecretActions.ReadValue, - subject(ProjectPermissionSub.Secrets, { environment, secretPath: path }) - ); + const secretValueHidden = !CheckCanSecretsSubject(permission, ProjectPermissionSecretActions.ReadValue, { + environment, + secretPath: path + }); return { ...deletedSecret[0], @@ -622,10 +620,10 @@ export const secretServiceFactory = ({ paths = deepPaths.map(({ folderId, path: p }) => ({ folderId, path: p })); } else { - ForbiddenError.from(permission).throwUnlessCan( - ProjectPermissionSecretActions.ReadValue, - subject(ProjectPermissionSub.Secrets, { environment, secretPath: path }) - ); + CheckForbiddenErrorSecretsSubject(permission, ProjectPermissionSecretActions.ReadValue, { + environment, + secretPath: path + }); const folder = await folderDAL.findBySecretPath(projectId, environment, path); if (!folder) return { secrets: [], imports: [] }; @@ -647,13 +645,10 @@ export const secretServiceFactory = ({ // if its service token allow full access over imported one actor === ActorType.SERVICE ? true - : permission.can( - ProjectPermissionSecretActions.ReadValue, - subject(ProjectPermissionSub.Secrets, { - environment: importEnv.slug, - secretPath: importPath - }) - ) + : CheckCanSecretsSubject(permission, ProjectPermissionSecretActions.ReadValue, { + environment: importEnv.slug, + secretPath: importPath + }) ); const importedSecrets = await fnSecretsFromImports({ allowedImports, @@ -704,10 +699,11 @@ export const secretServiceFactory = ({ actorOrgId, actionProjectType: ActionProjectType.SecretManager }); - ForbiddenError.from(permission).throwUnlessCan( - ProjectPermissionSecretActions.ReadValue, - subject(ProjectPermissionSub.Secrets, { environment, secretPath: path }) - ); + CheckForbiddenErrorSecretsSubject(permission, ProjectPermissionSecretActions.ReadValue, { + environment, + secretPath: path + }); + const folder = await folderDAL.findBySecretPath(projectId, environment, path); if (!folder) throw new NotFoundError({ @@ -754,14 +750,12 @@ export const secretServiceFactory = ({ // if its service token allow full access over imported one actor === ActorType.SERVICE ? true - : permission.can( - ProjectPermissionSecretActions.ReadValue, - subject(ProjectPermissionSub.Secrets, { - environment: importEnv.slug, - secretPath: importPath - }) - ) + : CheckCanSecretsSubject(permission, ProjectPermissionSecretActions.ReadValue, { + environment: importEnv.slug, + secretPath: importPath + }) ); + const importedSecrets = await fnSecretsFromImports({ allowedImports, secretDAL, @@ -975,10 +969,10 @@ export const secretServiceFactory = ({ secretVersionTagDAL }); - const secretValueHidden = !permission.can( - ProjectPermissionSecretActions.ReadValue, - subject(ProjectPermissionSub.Secrets, { environment, secretPath: path }) - ); + const secretValueHidden = !CheckCanSecretsSubject(permission, ProjectPermissionSecretActions.ReadValue, { + environment, + secretPath: path + }); return updatedSecrets.map((secret) => ({ ...secret, @@ -1069,11 +1063,10 @@ export const secretServiceFactory = ({ }); } } - - const secretValueHidden = !permission.can( - ProjectPermissionSecretActions.ReadValue, - subject(ProjectPermissionSub.Secrets, { environment, secretPath: path }) - ); + const secretValueHidden = !CheckCanSecretsSubject(permission, ProjectPermissionSecretActions.ReadValue, { + environment, + secretPath: path + }); return secrets.map((secret) => ({ ...secret, @@ -1259,8 +1252,20 @@ export const secretServiceFactory = ({ ProjectPermissionSecretActions.Delete, ProjectPermissionSecretActions.Create, ProjectPermissionSecretActions.Edit - ].filter((action) => - entityPermission.permission.can( + ].filter((action) => { + if ( + action === ProjectPermissionSecretActions.DescribeSecret || + action === ProjectPermissionSecretActions.ReadValue + ) { + return CheckCanSecretsSubject(entityPermission.permission, action, { + environment, + secretPath, + secretName, + secretTags: secret?.tags?.map((el) => el.slug) + }); + } + + return entityPermission.permission.can( action, subject(ProjectPermissionSub.Secrets, { environment, @@ -1268,8 +1273,8 @@ export const secretServiceFactory = ({ secretName, secretTags: secret?.tags?.map((el) => el.slug) }) - ) - ); + ); + }); return { ...entityPermission, @@ -2424,17 +2429,14 @@ export const secretServiceFactory = ({ key: botKey }); - const secretValueHidden = permission.cannot( - ProjectPermissionSecretActions.ReadValue, - subject(ProjectPermissionSub.Secrets, { - environment: folder.environment.envSlug, - secretPath: folderWithPath.path, - secretName: secretKey, - ...(el.tags?.length && { - secretTags: el.tags.map((tag) => tag.slug) - }) + const secretValueHidden = !CheckCanSecretsSubject(permission, ProjectPermissionSecretActions.ReadValue, { + environment: folder.environment.envSlug, + secretPath: folderWithPath.path, + secretName: secretKey, + ...(el.tags?.length && { + secretTags: el.tags.map((tag) => tag.slug) }) - ); + }); return decryptSecretRaw( { @@ -2833,13 +2835,23 @@ export const secretServiceFactory = ({ } for (const sourceAction of sourceActions) { - ForbiddenError.from(permission).throwUnlessCan( - sourceAction, - subject(ProjectPermissionSub.Secrets, { + if ( + sourceAction === ProjectPermissionSecretActions.ReadValue || + sourceAction === ProjectPermissionSecretActions.DescribeSecret + ) { + CheckForbiddenErrorSecretsSubject(permission, sourceAction, { environment: sourceEnvironment, secretPath: sourceSecretPath - }) - ); + }); + } else { + ForbiddenError.from(permission).throwUnlessCan( + sourceAction, + subject(ProjectPermissionSub.Secrets, { + environment: sourceEnvironment, + secretPath: sourceSecretPath + }) + ); + } } return { diff --git a/backend/src/services/service-token/service-token-types.ts b/backend/src/services/service-token/service-token-types.ts index d06d90a9d..f1a700908 100644 --- a/backend/src/services/service-token/service-token-types.ts +++ b/backend/src/services/service-token/service-token-types.ts @@ -7,7 +7,7 @@ export type TCreateServiceTokenDTO = { iv: string; tag: string; expiresIn?: number | null; - permissions: ("read" | "write" | "readValue")[]; + permissions: ("read" | "write")[]; } & TProjectPermission; export type TGetServiceTokenInfoDTO = Omit; diff --git a/frontend/src/context/ProjectPermissionContext/types.ts b/frontend/src/context/ProjectPermissionContext/types.ts index 260ffce09..495055977 100644 --- a/frontend/src/context/ProjectPermissionContext/types.ts +++ b/frontend/src/context/ProjectPermissionContext/types.ts @@ -8,7 +8,8 @@ export enum ProjectPermissionActions { } export enum ProjectPermissionSecretActions { - DescribeSecret = "read", + DescribeAndReadValue = "read", + DescribeSecret = "describeSecret", ReadValue = "readValue", Create = "create", Edit = "edit", diff --git a/frontend/src/lib/fn/permission.ts b/frontend/src/lib/fn/permission.ts new file mode 100644 index 000000000..f2077d76b --- /dev/null +++ b/frontend/src/lib/fn/permission.ts @@ -0,0 +1,36 @@ +import { MongoAbility, subject } from "@casl/ability"; + +import { ProjectPermissionSet } from "@app/context/ProjectPermissionContext"; +import { + ProjectPermissionSecretActions, + ProjectPermissionSub, + SecretSubjectFields +} from "@app/context/ProjectPermissionContext/types"; + +export function secretsPermissionCan( + permission: MongoAbility, + action: Extract< + ProjectPermissionSecretActions, + ProjectPermissionSecretActions.DescribeSecret | ProjectPermissionSecretActions.ReadValue + >, + subjectFields?: SecretSubjectFields +) { + let canNewPermission = false; + let canOldPermission = false; + + if (subjectFields) { + canNewPermission = permission.can(action, subject(ProjectPermissionSub.Secrets, subjectFields)); + canOldPermission = permission.can( + ProjectPermissionSecretActions.DescribeAndReadValue, + subject(ProjectPermissionSub.Secrets, subjectFields) + ); + } else { + canNewPermission = permission.can(action, ProjectPermissionSub.Secrets); + canOldPermission = permission.can( + ProjectPermissionSecretActions.DescribeAndReadValue, + ProjectPermissionSub.Secrets + ); + } + + return canNewPermission || canOldPermission; +} diff --git a/frontend/src/pages/project/AccessControlPage/components/ServiceTokenTab/components/ServiceTokenSection/AddServiceTokenModal.tsx b/frontend/src/pages/project/AccessControlPage/components/ServiceTokenTab/components/ServiceTokenSection/AddServiceTokenModal.tsx index a669a8360..fa9500a53 100644 --- a/frontend/src/pages/project/AccessControlPage/components/ServiceTokenTab/components/ServiceTokenSection/AddServiceTokenModal.tsx +++ b/frontend/src/pages/project/AccessControlPage/components/ServiceTokenTab/components/ServiceTokenSection/AddServiceTokenModal.tsx @@ -303,13 +303,9 @@ export const AddServiceTokenModal = ({ popUp, handlePopUpToggle }: Props) => { render={({ field: { onChange, value }, fieldState: { error } }) => { const options = [ { - label: "Describe Secret (default)", + label: "Read (default)", value: "read" }, - { - label: "Read Value (optional)", - value: "readValue" - }, { label: "Write (optional)", value: "write" diff --git a/frontend/src/pages/project/RoleDetailsBySlugPage/components/GeneralPermissionPolicies.tsx b/frontend/src/pages/project/RoleDetailsBySlugPage/components/GeneralPermissionPolicies.tsx index 8aca40a34..a6e472976 100644 --- a/frontend/src/pages/project/RoleDetailsBySlugPage/components/GeneralPermissionPolicies.tsx +++ b/frontend/src/pages/project/RoleDetailsBySlugPage/components/GeneralPermissionPolicies.tsx @@ -35,12 +35,13 @@ export const GeneralPermissionPolicies = ) => { - const { control } = useFormContext(); + const { control, watch } = useFormContext(); const items = useFieldArray({ control, name: `permissions.${subject}` }); const [isOpen, setIsOpen] = useToggle(); + // const [hideFullReadAccess, setHideFullReadAccess] = useState(false); if (!items.fields.length) return
; @@ -71,119 +72,138 @@ export const GeneralPermissionPolicies = {isOpen && (
- {items.fields.map((el, rootIndex) => ( -
- {isConditionalSubjects(subject) && ( -
-
Permission
-
- ( - - )} - /> -
-
- -

- Whether to allow or forbid the selected actions when the following - conditions (if any) are met. -

-

Forbid rules must come after allow rules.

- - } - > - -
-
-
- )} -
-
Actions
-
- {actions.map(({ label, value }) => { - if (typeof value !== "string") return undefined; + {items.fields.map((el, rootIndex) => { + let isFullReadAccessEnabled = false; - return ( + if (subject === ProjectPermissionSub.Secrets) { + isFullReadAccessEnabled = watch(`permissions.${subject}.${rootIndex}.read` as any); + } + + return ( +
+ {isConditionalSubjects(subject) && ( +
+
Permission
+
{ - return ( -
- - {label} - -
- ); - }} + defaultValue={false as any} + name={`permissions.${subject}.${rootIndex}.inverted`} + render={({ field }) => ( + + )} /> - ); +
+
+ +

+ Whether to allow or forbid the selected actions when the following + conditions (if any) are met. +

+

Forbid rules must come after allow rules.

+ + } + > + +
+
+
+ )} +
+
Actions
+
+ {actions.map(({ label, value }, index) => { + if (typeof value !== "string") return undefined; + + if ( + subject === ProjectPermissionSub.Secrets && + value === "read" && + !isFullReadAccessEnabled + ) { + return null; + } + + return ( + { + return ( +
+ + {label} + +
+ ); + }} + /> + ); + })} +
+
+ {children && + cloneElement(children, { + position: rootIndex })} +
+ {!isDisabled && isConditionalSubjects(subject) && ( + + )} + {!isDisabled && ( + + )}{" "}
- {children && - cloneElement(children, { - position: rootIndex - })} -
- {!isDisabled && isConditionalSubjects(subject) && ( - - )} - {!isDisabled && ( - - )}{" "} -
-
- ))} + ); + })}
)}
diff --git a/frontend/src/pages/project/RoleDetailsBySlugPage/components/ProjectRoleModifySection.utils.tsx b/frontend/src/pages/project/RoleDetailsBySlugPage/components/ProjectRoleModifySection.utils.tsx index e26ec8bc4..9f13b53a8 100644 --- a/frontend/src/pages/project/RoleDetailsBySlugPage/components/ProjectRoleModifySection.utils.tsx +++ b/frontend/src/pages/project/RoleDetailsBySlugPage/components/ProjectRoleModifySection.utils.tsx @@ -1,5 +1,9 @@ +import { ReactNode } from "react"; +import { faWarning } from "@fortawesome/free-solid-svg-icons"; +import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { z } from "zod"; +import { Tooltip } from "@app/components/v2"; import { ProjectPermissionActions, ProjectPermissionCmekActions, @@ -24,11 +28,12 @@ const GeneralPolicyActionSchema = z.object({ }); const SecretPolicyActionSchema = z.object({ - read: z.boolean().optional(), // describe secret - edit: z.boolean().optional(), - delete: z.boolean().optional(), - create: z.boolean().optional(), - readValue: z.boolean().optional() + [ProjectPermissionSecretActions.DescribeAndReadValue]: z.boolean().optional(), // existing read, gives both describe and read value + [ProjectPermissionSecretActions.DescribeSecret]: z.boolean().optional(), // describe secret, cannot read value + [ProjectPermissionSecretActions.ReadValue]: z.boolean().optional(), // read value + [ProjectPermissionSecretActions.Edit]: z.boolean().optional(), // edit secret + [ProjectPermissionSecretActions.Delete]: z.boolean().optional(), // delete secret + [ProjectPermissionSecretActions.Create]: z.boolean().optional() // create secret }); const CmekPolicyActionSchema = z.object({ @@ -294,19 +299,24 @@ export const rolePermission2Form = (permissions: TProjectPermission[] = []) => { } if (subject === ProjectPermissionSub.Secrets) { - const canRead = action.includes(ProjectPermissionSecretActions.DescribeSecret); + const canDescribeAndReadValue = action.includes( + ProjectPermissionSecretActions.DescribeAndReadValue + ); + const canDescribe = action.includes(ProjectPermissionSecretActions.DescribeSecret); + const canReadValue = action.includes(ProjectPermissionSecretActions.ReadValue); + const canEdit = action.includes(ProjectPermissionSecretActions.Edit); const canDelete = action.includes(ProjectPermissionSecretActions.Delete); const canCreate = action.includes(ProjectPermissionSecretActions.Create); - const canReadValue = action.includes(ProjectPermissionSecretActions.ReadValue); // from above statement we are sure it won't be undefined formVal[subject]!.push({ - read: canRead, + describeSecret: canDescribe, + read: canDescribeAndReadValue, + readValue: canReadValue, create: canCreate, edit: canEdit, delete: canDelete, - readValue: canReadValue, conditions: conditions ? convertCaslConditionToFormOperator(conditions) : [], inverted }); @@ -501,7 +511,7 @@ export type TProjectPermissionObject = { [K in ProjectPermissionSub]: { title: string; actions: { - label: string; + label: string | ReactNode; value: keyof Omit< NonNullable[K]>[number], "conditions" | "inverted" @@ -514,11 +524,35 @@ export const PROJECT_PERMISSION_OBJECT: TProjectPermissionObject = { [ProjectPermissionSub.Secrets]: { title: "Secrets", actions: [ - { label: "Describe Secret", value: "read" }, - { label: "Create", value: "create" }, - { label: "Read Value", value: "readValue" }, - { label: "Modify", value: "edit" }, - { label: "Remove", value: "delete" } + { + label: ( +
+

+ Read (legacy) +

+ + This is a legacy action and will be removed in the future. +
+
You should instead use the{" "} + Describe Secret and{" "} + Read Value actions. +
+ } + > + + +
+ ), + value: ProjectPermissionSecretActions.DescribeAndReadValue + }, + { label: "Describe Secret", value: ProjectPermissionSecretActions.DescribeSecret }, + { label: "Read Value", value: ProjectPermissionSecretActions.ReadValue }, + { label: "Modify", value: ProjectPermissionSecretActions.Edit }, + { label: "Remove", value: ProjectPermissionSecretActions.Delete }, + { label: "Create", value: ProjectPermissionSecretActions.Create } ] }, [ProjectPermissionSub.SecretFolders]: { diff --git a/frontend/src/pages/secret-manager/OverviewPage/components/SecretOverviewTableRow/SecretRenameRow.tsx b/frontend/src/pages/secret-manager/OverviewPage/components/SecretOverviewTableRow/SecretRenameRow.tsx index 3b05f10fa..536f75542 100644 --- a/frontend/src/pages/secret-manager/OverviewPage/components/SecretOverviewTableRow/SecretRenameRow.tsx +++ b/frontend/src/pages/secret-manager/OverviewPage/components/SecretOverviewTableRow/SecretRenameRow.tsx @@ -15,6 +15,7 @@ import { ProjectPermissionSecretActions } from "@app/context/ProjectPermissionCo import { useToggle } from "@app/hooks"; import { useUpdateSecretV3 } from "@app/hooks/api"; import { SecretType, SecretV3RawSanitized } from "@app/hooks/api/types"; +import { secretsPermissionCan } from "@app/lib/fn/permission"; enum SecretActionType { Created = "created", @@ -51,8 +52,11 @@ function SecretRenameRow({ environments, getSecretByKey, secretKey, secretPath } secretTags: (secretDetails?.tags || []).map((i) => i.slug) }); const isSecretInEnvReadOnly = - permission.can(ProjectPermissionSecretActions.DescribeSecret, secretPermissionSubject) && - permission.cannot(ProjectPermissionSecretActions.Edit, secretPermissionSubject); + secretsPermissionCan( + permission, + ProjectPermissionSecretActions.DescribeSecret, + secretPermissionSubject + ) && permission.cannot(ProjectPermissionSecretActions.Edit, secretPermissionSubject); if (isSecretInEnvReadOnly) { return true; } diff --git a/frontend/src/pages/secret-manager/SecretDashboardPage/SecretDashboardPage.tsx b/frontend/src/pages/secret-manager/SecretDashboardPage/SecretDashboardPage.tsx index 5cbefe9c3..4dba1c2a8 100644 --- a/frontend/src/pages/secret-manager/SecretDashboardPage/SecretDashboardPage.tsx +++ b/frontend/src/pages/secret-manager/SecretDashboardPage/SecretDashboardPage.tsx @@ -38,6 +38,7 @@ import { useGetProjectSecretsDetails } from "@app/hooks/api/dashboard"; import { DashboardSecretsOrderBy } from "@app/hooks/api/dashboard/types"; import { OrderByDirection } from "@app/hooks/api/generic/types"; import { ProjectType } from "@app/hooks/api/workspace/types"; +import { secretsPermissionCan } from "@app/lib/fn/permission"; import { SecretTableResourceCount } from "../OverviewPage/components/SecretTableResourceCount"; import { SecretV2MigrationSection } from "../OverviewPage/components/SecretV2MigrationSection"; @@ -103,23 +104,27 @@ const Page = () => { const workspaceId = currentWorkspace?.id || ""; const projectSlug = currentWorkspace?.slug || ""; const secretPath = (routerQueryParams.secretPath as string) || "/"; - const canReadSecret = permission.can( + + const canReadSecret = secretsPermissionCan( + permission, ProjectPermissionSecretActions.DescribeSecret, - subject(ProjectPermissionSub.Secrets, { + { environment, secretPath, secretName: "*", secretTags: ["*"] - }) + } ); - const canReadSecretValue = permission.can( + + const canReadSecretValue = secretsPermissionCan( + permission, ProjectPermissionSecretActions.ReadValue, - subject(ProjectPermissionSub.Secrets, { + { environment, secretPath, secretName: "*", secretTags: ["*"] - }) + } ); const canReadSecretImports = permission.can( diff --git a/frontend/src/pages/secret-manager/SecretDashboardPage/components/SecretListView/SecretDetailSidebar.tsx b/frontend/src/pages/secret-manager/SecretDashboardPage/components/SecretListView/SecretDetailSidebar.tsx index d6da791bb..3ad7159d7 100644 --- a/frontend/src/pages/secret-manager/SecretDashboardPage/components/SecretListView/SecretDetailSidebar.tsx +++ b/frontend/src/pages/secret-manager/SecretDashboardPage/components/SecretListView/SecretDetailSidebar.tsx @@ -57,6 +57,7 @@ import { ActorType } from "@app/hooks/api/auditLogs/enums"; import { useGetSecretAccessList } from "@app/hooks/api/secrets/queries"; import { SecretV3RawSanitized, WsTag } from "@app/hooks/api/types"; import { ProjectType } from "@app/hooks/api/workspace/types"; +import { secretsPermissionCan } from "@app/lib/fn/permission"; import { CreateReminderForm } from "./CreateReminderForm"; import { formSchema, SecretActionType, TFormSchema } from "./SecretListView.utils"; @@ -140,26 +141,24 @@ export const SecretDetailSidebar = ({ }) ); - const cannotReadSecretValue = permission.cannot( + const cannotReadSecretValue = !secretsPermissionCan( + permission, ProjectPermissionSecretActions.ReadValue, - subject(ProjectPermissionSub.Secrets, { + { environment, secretPath, secretName: secretKey, secretTags: selectTagSlugs - }) + } ); const isReadOnly = - permission.can( - ProjectPermissionSecretActions.DescribeSecret, - subject(ProjectPermissionSub.Secrets, { - environment, - secretPath, - secretName: secretKey, - secretTags: selectTagSlugs - }) - ) && + secretsPermissionCan(permission, ProjectPermissionSecretActions.DescribeSecret, { + environment, + secretPath, + secretName: secretKey, + secretTags: selectTagSlugs + }) && cannotEditSecret && cannotReadSecretValue; diff --git a/frontend/src/pages/secret-manager/SecretDashboardPage/components/SecretListView/SecretItem.tsx b/frontend/src/pages/secret-manager/SecretDashboardPage/components/SecretListView/SecretItem.tsx index 02f7a0aa9..0271bffef 100644 --- a/frontend/src/pages/secret-manager/SecretDashboardPage/components/SecretListView/SecretItem.tsx +++ b/frontend/src/pages/secret-manager/SecretDashboardPage/components/SecretListView/SecretItem.tsx @@ -47,6 +47,7 @@ import { import { ProjectPermissionSecretActions } from "@app/context/ProjectPermissionContext/types"; import { Blur } from "@app/components/v2/Blur"; +import { secretsPermissionCan } from "@app/lib/fn/permission"; import { FontAwesomeSpriteName, formSchema, @@ -131,15 +132,12 @@ export const SecretItem = memo( }); const isReadOnly = - permission.can( - ProjectPermissionSecretActions.DescribeSecret, - subject(ProjectPermissionSub.Secrets, { - environment, - secretPath, - secretName, - secretTags: selectedTagSlugs - }) - ) && + secretsPermissionCan(permission, ProjectPermissionSecretActions.DescribeSecret, { + environment, + secretPath, + secretName, + secretTags: selectedTagSlugs + }) && permission.cannot( ProjectPermissionSecretActions.Edit, subject(ProjectPermissionSub.Secrets, { From 955657e17234278a21c7cd5c6434d0cc1a5ba1bc Mon Sep 17 00:00:00 2001 From: Daniel Hougaard Date: Sat, 8 Mar 2025 03:58:15 +0400 Subject: [PATCH 19/71] fix: legacy permission check --- .../secret-v2-bridge/secret-v2-bridge-service.ts | 15 ++++++--------- 1 file changed, 6 insertions(+), 9 deletions(-) diff --git a/backend/src/services/secret-v2-bridge/secret-v2-bridge-service.ts b/backend/src/services/secret-v2-bridge/secret-v2-bridge-service.ts index 8ebc0a582..d6115cdcc 100644 --- a/backend/src/services/secret-v2-bridge/secret-v2-bridge-service.ts +++ b/backend/src/services/secret-v2-bridge/secret-v2-bridge-service.ts @@ -1127,15 +1127,12 @@ export const secretV2BridgeServiceFactory = ({ actionProjectType: ActionProjectType.SecretManager }); - ForbiddenError.from(permission).throwUnlessCan( - ProjectPermissionActions.Read, - subject(ProjectPermissionSub.Secrets, { - environment: folderWithPath.environmentSlug, - secretPath: folderWithPath.path, - secretName: secret.key, - secretTags: secret.tags.map((i) => i.slug) - }) - ); + CheckForbiddenErrorSecretsSubject(permission, ProjectPermissionSecretActions.ReadValue, { + environment: folderWithPath.environmentSlug, + secretPath: folderWithPath.path, + secretName: secret.key, + secretTags: secret.tags.map((i) => i.slug) + }); if (secret.type === SecretType.Personal && secret.userId !== actorId) { throw new ForbiddenRequestError({ From dc88115d4370ad5cd75c6e90031116d75991541a Mon Sep 17 00:00:00 2001 From: Daniel Hougaard Date: Sat, 8 Mar 2025 03:59:34 +0400 Subject: [PATCH 20/71] fix: tests failing --- backend/e2e-test/routes/v2/service-token.spec.ts | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/backend/e2e-test/routes/v2/service-token.spec.ts b/backend/e2e-test/routes/v2/service-token.spec.ts index bfe043523..6cc8f6e34 100644 --- a/backend/e2e-test/routes/v2/service-token.spec.ts +++ b/backend/e2e-test/routes/v2/service-token.spec.ts @@ -6,7 +6,7 @@ import { decryptAsymmetric, decryptSymmetric128BitHexKeyUTF8, encryptSymmetric12 const createServiceToken = async ( scopes: { environment: string; secretPath: string }[], - permissions: ("read" | "write" | "readValue")[] + permissions: ("read" | "write")[] ) => { const projectKeyRes = await testServer.inject({ method: "GET", @@ -139,7 +139,7 @@ describe("Service token secret ops", async () => { beforeAll(async () => { serviceToken = await createServiceToken( [{ secretPath: "/**", environment: seedData1.environment.slug }], - ["read", "write", "readValue"] + ["read", "write"] ); // this is ensure cli service token decryptiong working fine @@ -496,7 +496,7 @@ describe("Service token fail cases", async () => { test("Unauthorized secret path access", async () => { const serviceToken = await createServiceToken( [{ secretPath: "/", environment: seedData1.environment.slug }], - ["read", "readValue", "write"] + ["read", "write"] ); const fetchSecrets = await testServer.inject({ method: "GET", @@ -518,7 +518,7 @@ describe("Service token fail cases", async () => { test("Unauthorized secret environment access", async () => { const serviceToken = await createServiceToken( [{ secretPath: "/", environment: seedData1.environment.slug }], - ["read", "readValue", "write"] + ["read", "write"] ); const fetchSecrets = await testServer.inject({ method: "GET", @@ -540,7 +540,7 @@ describe("Service token fail cases", async () => { test("Unauthorized write operation", async () => { const serviceToken = await createServiceToken( [{ secretPath: "/", environment: seedData1.environment.slug }], - ["read", "readValue"] + ["read"] ); const writeSecrets = await testServer.inject({ method: "POST", From 46ce46b5a04e97d7c8d953698702d328af0df088 Mon Sep 17 00:00:00 2001 From: Daniel Hougaard Date: Sat, 8 Mar 2025 04:04:52 +0400 Subject: [PATCH 21/71] fix: get secret by ID using legacy permissions --- backend/src/server/routes/v3/secret-router.ts | 9 +-------- .../secret-v2-bridge/secret-v2-bridge-service.ts | 16 +++++++++++----- 2 files changed, 12 insertions(+), 13 deletions(-) diff --git a/backend/src/server/routes/v3/secret-router.ts b/backend/src/server/routes/v3/secret-router.ts index b68d042f0..f854baade 100644 --- a/backend/src/server/routes/v3/secret-router.ts +++ b/backend/src/server/routes/v3/secret-router.ts @@ -368,14 +368,7 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => { 200: z.object({ secret: secretRawSchema.extend({ secretPath: z.string(), - tags: SecretTagsSchema.pick({ - id: true, - slug: true, - color: true - }) - .extend({ name: z.string() }) - .array() - .optional(), + tags: SanitizedTagSchema.array().optional(), secretMetadata: ResourceMetadataSchema.optional() }) }) diff --git a/backend/src/services/secret-v2-bridge/secret-v2-bridge-service.ts b/backend/src/services/secret-v2-bridge/secret-v2-bridge-service.ts index d6115cdcc..ec2728966 100644 --- a/backend/src/services/secret-v2-bridge/secret-v2-bridge-service.ts +++ b/backend/src/services/secret-v2-bridge/secret-v2-bridge-service.ts @@ -1154,11 +1154,17 @@ export const secretV2BridgeServiceFactory = ({ ? secretManagerDecryptor({ cipherTextBlob: secret.encryptedComment }).toString() : ""; - return reshapeBridgeSecret(secret.projectId, folderWithPath.environmentSlug, folderWithPath.path, { - ...secret, - value: secretValue, - comment: secretComment - }); + return reshapeBridgeSecret( + secret.projectId, + folderWithPath.environmentSlug, + folderWithPath.path, + { + ...secret, + value: secretValue, + comment: secretComment + }, + false + ); }; const getSecretByName = async ({ From a8fc0e540a92997b0948cead7ad07c4b6217cd0e Mon Sep 17 00:00:00 2001 From: Daniel Hougaard Date: Mon, 10 Mar 2025 17:50:43 +0400 Subject: [PATCH 22/71] fix: tests and missing tags permission check --- backend/package.json | 1 + .../ee/services/permission/permission-fns.ts | 4 ++-- .../secret-v2-bridge-service.ts | 23 ++++++++++--------- 3 files changed, 15 insertions(+), 13 deletions(-) diff --git a/backend/package.json b/backend/package.json index 5c2c805bc..25380f896 100644 --- a/backend/package.json +++ b/backend/package.json @@ -71,6 +71,7 @@ "migrate:org": "tsx ./scripts/migrate-organization.ts", "seed:new": "tsx ./scripts/create-seed-file.ts", "seed": "knex --knexfile ./dist/db/knexfile.ts --client pg seed:run", + "seed-dev": "knex --knexfile ./src/db/knexfile.ts --client pg seed:run", "db:reset": "npm run migration:rollback -- --all && npm run migration:latest" }, "keywords": [], diff --git a/backend/src/ee/services/permission/permission-fns.ts b/backend/src/ee/services/permission/permission-fns.ts index 97113a562..c9f3b826e 100644 --- a/backend/src/ee/services/permission/permission-fns.ts +++ b/backend/src/ee/services/permission/permission-fns.ts @@ -1,5 +1,5 @@ /* eslint-disable no-nested-ternary */ -import { ForbiddenError, MongoAbility, subject } from "@casl/ability"; +import { ForbiddenError, MongoAbility, PureAbility, subject } from "@casl/ability"; import { z } from "zod"; import { TOrganizations } from "@app/db/schemas"; @@ -15,7 +15,7 @@ import { } from "./project-permission"; export function CheckForbiddenErrorSecretsSubject( - permission: MongoAbility, + permission: MongoAbility | PureAbility, action: Extract< ProjectPermissionSecretActions, ProjectPermissionSecretActions.ReadValue | ProjectPermissionSecretActions.DescribeSecret diff --git a/backend/src/services/secret-v2-bridge/secret-v2-bridge-service.ts b/backend/src/services/secret-v2-bridge/secret-v2-bridge-service.ts index ec2728966..3b3a147bd 100644 --- a/backend/src/services/secret-v2-bridge/secret-v2-bridge-service.ts +++ b/backend/src/services/secret-v2-bridge/secret-v2-bridge-service.ts @@ -1,4 +1,4 @@ -import { ForbiddenError, PureAbility, subject } from "@casl/ability"; +import { ForbiddenError, MongoAbility, subject } from "@casl/ability"; import { Knex } from "knex"; import { z } from "zod"; @@ -15,6 +15,7 @@ import { TPermissionServiceFactory } from "@app/ee/services/permission/permissio import { ProjectPermissionActions, ProjectPermissionSecretActions, + ProjectPermissionSet, ProjectPermissionSub } from "@app/ee/services/permission/project-permission"; import { TSecretApprovalPolicyServiceFactory } from "@app/ee/services/secret-approval-policy/secret-approval-policy-service"; @@ -123,7 +124,7 @@ export const secretV2BridgeServiceFactory = ({ }: TSecretV2BridgeServiceFactoryDep) => { const $validateSecretReferences = async ( projectId: string, - permission: PureAbility, + permission: MongoAbility, references: ReturnType["nestedReferences"], tx?: Knex ) => { @@ -131,6 +132,7 @@ export const secretV2BridgeServiceFactory = ({ const uniqueReferenceEnvironmentSlugs = Array.from(new Set(references.map((el) => el.environment))); const referencesEnvironments = await projectEnvDAL.findBySlugs(projectId, uniqueReferenceEnvironmentSlugs, tx); + if (referencesEnvironments.length !== uniqueReferenceEnvironmentSlugs.length) throw new BadRequestError({ message: `Referenced environment not found. Missing ${diff( @@ -147,6 +149,7 @@ export const secretV2BridgeServiceFactory = ({ })), tx ); + const referencesFolderGroupByPath = groupBy(referredFolders.filter(Boolean), (i) => `${i?.envId}-${i?.path}`); const referredSecrets = await secretDAL.find( { @@ -194,15 +197,12 @@ export const secretV2BridgeServiceFactory = ({ const referredSecretsGroupBySecretKey = groupBy(referredSecrets, (i) => i.key); references.forEach((el) => { - ForbiddenError.from(permission).throwUnlessCan( - ProjectPermissionActions.Read, - subject(ProjectPermissionSub.Secrets, { - environment: el.environment, - secretPath: el.secretPath, - secretName: el.secretKey, - tags: referredSecretsGroupBySecretKey[el.secretKey][0]?.tags?.map((i) => i.slug) - }) - ); + CheckForbiddenErrorSecretsSubject(permission, ProjectPermissionSecretActions.ReadValue, { + environment: el.environment, + secretPath: el.secretPath, + secretName: el.secretKey, + secretTags: referredSecretsGroupBySecretKey[el.secretKey][0]?.tags?.map((i) => i.slug) + }); }); return referredSecrets; @@ -277,6 +277,7 @@ export const secretV2BridgeServiceFactory = ({ const allSecretReferences = nestedReferences.concat( localReferences.map((el) => ({ secretKey: el, secretPath, environment })) ); + await $validateSecretReferences(projectId, permission, allSecretReferences); const { encryptor: secretManagerEncryptor } = await kmsService.createCipherPairWithDataKey({ From 09df4406131926d6aabb60e1c010b1f99bd6d0d6 Mon Sep 17 00:00:00 2001 From: Daniel Hougaard Date: Tue, 11 Mar 2025 00:18:42 +0400 Subject: [PATCH 23/71] Update secret-version-dal.ts --- .../secret-v2-bridge/secret-version-dal.ts | 55 +++++++++++++++---- 1 file changed, 44 insertions(+), 11 deletions(-) diff --git a/backend/src/services/secret-v2-bridge/secret-version-dal.ts b/backend/src/services/secret-v2-bridge/secret-version-dal.ts index 12c54cd7c..b54b073a6 100644 --- a/backend/src/services/secret-v2-bridge/secret-version-dal.ts +++ b/backend/src/services/secret-v2-bridge/secret-version-dal.ts @@ -187,6 +187,17 @@ export const secretVersionV2BridgeDALFactory = (db: TDbClient) => { `${TableName.SecretVersionV2}.userActorId` ) .leftJoin(TableName.Identity, `${TableName.Identity}.id`, `${TableName.SecretVersionV2}.identityActorId`) + .leftJoin(TableName.SecretV2, `${TableName.SecretVersionV2}.secretId`, `${TableName.SecretV2}.id`) + .leftJoin( + TableName.SecretV2JnTag, + `${TableName.SecretV2}.id`, + `${TableName.SecretV2JnTag}.${TableName.SecretV2}Id` + ) + .leftJoin( + TableName.SecretTag, + `${TableName.SecretV2JnTag}.${TableName.SecretTag}Id`, + `${TableName.SecretTag}.id` + ) .where((qb) => { void qb.where(`${TableName.SecretVersionV2}.secretId`, secretId); void qb.where(`${TableName.ProjectMembership}.projectId`, projectId); @@ -197,9 +208,12 @@ export const secretVersionV2BridgeDALFactory = (db: TDbClient) => { }) .select( selectAllTableCols(TableName.SecretVersionV2), - `${TableName.Users}.username as userActorName`, - `${TableName.Identity}.name as identityActorName`, - `${TableName.ProjectMembership}.id as membershipId` + db.ref("username").withSchema(TableName.Users).as("userActorName"), + db.ref("name").withSchema(TableName.Identity).as("identityActorName"), + db.ref("id").withSchema(TableName.ProjectMembership).as("membershipId"), + db.ref("id").withSchema(TableName.SecretTag).as("tagId"), + db.ref("color").withSchema(TableName.SecretTag).as("tagColor"), + db.ref("slug").withSchema(TableName.SecretTag).as("tagSlug") ); if (limit) void query.limit(limit); @@ -214,14 +228,33 @@ export const secretVersionV2BridgeDALFactory = (db: TDbClient) => { ); } - const docs: Array< - TSecretVersionsV2 & { - userActorName: string | undefined | null; - identityActorName: string | undefined | null; - membershipId: string | undefined | null; - } - > = await query; - return docs; + const docs = await query; + + const data = sqlNestRelationships({ + data: docs, + key: "id", + parentMapper: (el) => ({ + _id: el.id, + ...SecretVersionsV2Schema.parse(el), + userActorName: el.userActorName, + identityActorName: el.identityActorName, + membershipId: el.membershipId + }), + childrenMapper: [ + { + key: "tagId", + label: "tags" as const, + mapper: ({ tagId: id, tagColor: color, tagSlug: slug }) => ({ + id, + color, + slug, + name: slug + }) + } + ] + }); + + return data; } catch (error) { throw new DatabaseError({ error, name: "FindVersionsBySecretIdWithActors" }); } From 78c349c09a9f12b8197e315db69b9e23ecd22f20 Mon Sep 17 00:00:00 2001 From: Daniel Hougaard Date: Tue, 11 Mar 2025 00:31:21 +0400 Subject: [PATCH 24/71] fix(view-secret-value): requested changes --- .../ee/services/permission/permission-fns.ts | 12 +- .../services/permission/project-permission.ts | 6 +- .../secret-approval-request-service.ts | 4 +- .../secret-snapshot-service.ts | 41 ++-- .../integration/integration-service.ts | 6 +- .../src/services/project/project-service.ts | 4 +- .../secret-import/secret-import-service.ts | 15 +- .../secret-sync/secret-sync-service.ts | 6 +- .../secret-v2-bridge-service.ts | 229 ++++++++++-------- .../secret-v2-bridge-types.ts | 2 +- backend/src/services/secret/secret-fns.ts | 4 +- backend/src/services/secret/secret-service.ts | 85 ++++--- frontend/src/lib/fn/permission.ts | 2 +- .../ProjectRoleModifySection.utils.tsx | 10 +- .../SecretRenameRow.tsx | 4 +- .../SecretDashboardPage.tsx | 6 +- .../SecretListView/SecretDetailSidebar.tsx | 24 +- .../components/SecretListView/SecretItem.tsx | 18 +- 18 files changed, 273 insertions(+), 205 deletions(-) diff --git a/backend/src/ee/services/permission/permission-fns.ts b/backend/src/ee/services/permission/permission-fns.ts index c9f3b826e..ecc8b676e 100644 --- a/backend/src/ee/services/permission/permission-fns.ts +++ b/backend/src/ee/services/permission/permission-fns.ts @@ -14,7 +14,7 @@ import { SecretSubjectFields } from "./project-permission"; -export function CheckForbiddenErrorSecretsSubject( +export function throwIfMissingSecretReadValueOrDescribePermission( permission: MongoAbility | PureAbility, action: Extract< ProjectPermissionSecretActions, @@ -43,7 +43,7 @@ export function CheckForbiddenErrorSecretsSubject( } } -export function CheckCanSecretsSubject( +export function hasSecretReadValueOrDescribePermission( permission: MongoAbility, action: Extract< ProjectPermissionSecretActions, @@ -83,12 +83,10 @@ export function checkForInvalidPermissionCombination(permissions: z.infer { can( [ - // not adding DescribeAndReadValue, because it's already covered by DescribeSecret and ReadValue + ProjectPermissionSecretActions.DescribeAndReadValue, ProjectPermissionSecretActions.DescribeSecret, ProjectPermissionSecretActions.ReadValue, ProjectPermissionSecretActions.Create, @@ -634,7 +634,7 @@ const buildMemberPermissionRules = () => { can( [ - // not adding DescribeAndReadValue, because it's already covered by DescribeSecret and ReadValue + ProjectPermissionSecretActions.DescribeAndReadValue, ProjectPermissionSecretActions.DescribeSecret, ProjectPermissionSecretActions.ReadValue, ProjectPermissionSecretActions.Edit, @@ -811,7 +811,7 @@ export const projectMemberPermissions = buildMemberPermissionRules(); const buildViewerPermissionRules = () => { const { can, rules } = new AbilityBuilder>(createMongoAbility); - // not adding DescribeAndReadValue, because it's already covered by DescribeSecret and ReadValue + can(ProjectPermissionSecretActions.DescribeAndReadValue, ProjectPermissionSub.Secrets); can(ProjectPermissionSecretActions.DescribeSecret, ProjectPermissionSub.Secrets); can(ProjectPermissionSecretActions.ReadValue, ProjectPermissionSub.Secrets); can(ProjectPermissionActions.Read, ProjectPermissionSub.SecretFolders); diff --git a/backend/src/ee/services/secret-approval-request/secret-approval-request-service.ts b/backend/src/ee/services/secret-approval-request/secret-approval-request-service.ts index 9f3ec7111..72d36c26b 100644 --- a/backend/src/ee/services/secret-approval-request/secret-approval-request-service.ts +++ b/backend/src/ee/services/secret-approval-request/secret-approval-request-service.ts @@ -77,7 +77,7 @@ import { TSecretApprovalDetailsDTO, TStatusChangeDTO } from "./secret-approval-request-types"; -import { CheckForbiddenErrorSecretsSubject } from "../permission/permission-fns"; +import { throwIfMissingSecretReadValueOrDescribePermission } from "../permission/permission-fns"; type TSecretApprovalRequestServiceFactoryDep = { permissionService: Pick; @@ -919,7 +919,7 @@ export const secretApprovalRequestServiceFactory = ({ actionProjectType: ActionProjectType.SecretManager }); - CheckForbiddenErrorSecretsSubject(permission, ProjectPermissionSecretActions.ReadValue, { + throwIfMissingSecretReadValueOrDescribePermission(permission, ProjectPermissionSecretActions.ReadValue, { environment, secretPath }); diff --git a/backend/src/ee/services/secret-snapshot/secret-snapshot-service.ts b/backend/src/ee/services/secret-snapshot/secret-snapshot-service.ts index 6afc6bca3..5fbb3f598 100644 --- a/backend/src/ee/services/secret-snapshot/secret-snapshot-service.ts +++ b/backend/src/ee/services/secret-snapshot/secret-snapshot-service.ts @@ -23,7 +23,10 @@ import { TSecretVersionV2DALFactory } from "@app/services/secret-v2-bridge/secre import { TSecretVersionV2TagDALFactory } from "@app/services/secret-v2-bridge/secret-version-tag-dal"; import { TLicenseServiceFactory } from "../license/license-service"; -import { CheckCanSecretsSubject, CheckForbiddenErrorSecretsSubject } from "../permission/permission-fns"; +import { + hasSecretReadValueOrDescribePermission, + throwIfMissingSecretReadValueOrDescribePermission +} from "../permission/permission-fns"; import { TPermissionServiceFactory } from "../permission/permission-service"; import { ProjectPermissionActions, @@ -103,7 +106,7 @@ export const secretSnapshotServiceFactory = ({ ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.SecretRollback); // We need to check if the user has access to the secrets in the folder. If we don't do this, a user could theoretically access snapshot secret values even if they don't have read access to the secrets in the folder. - CheckForbiddenErrorSecretsSubject(permission, ProjectPermissionSecretActions.DescribeSecret, { + throwIfMissingSecretReadValueOrDescribePermission(permission, ProjectPermissionSecretActions.DescribeSecret, { environment, secretPath: path }); @@ -140,7 +143,7 @@ export const secretSnapshotServiceFactory = ({ ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.SecretRollback); // We need to check if the user has access to the secrets in the folder. If we don't do this, a user could theoretically access snapshot secret values even if they don't have read access to the secrets in the folder. - CheckForbiddenErrorSecretsSubject(permission, ProjectPermissionSecretActions.DescribeSecret, { + throwIfMissingSecretReadValueOrDescribePermission(permission, ProjectPermissionSecretActions.DescribeSecret, { environment, secretPath: path }); @@ -187,12 +190,16 @@ export const secretSnapshotServiceFactory = ({ snapshotDetails = { ...encryptedSnapshotDetails, secretVersions: encryptedSnapshotDetails.secretVersions.map((el) => { - const canReadValue = CheckCanSecretsSubject(permission, ProjectPermissionSecretActions.ReadValue, { - environment: encryptedSnapshotDetails.environment.slug, - secretPath: fullFolderPath, - secretName: el.key, - secretTags: el.tags.length ? el.tags.map((tag) => tag.slug) : undefined - }); + const canReadValue = hasSecretReadValueOrDescribePermission( + permission, + ProjectPermissionSecretActions.ReadValue, + { + environment: encryptedSnapshotDetails.environment.slug, + secretPath: fullFolderPath, + secretName: el.key, + secretTags: el.tags.length ? el.tags.map((tag) => tag.slug) : undefined + } + ); let secretValue = ""; if (canReadValue) { @@ -236,12 +243,16 @@ export const secretSnapshotServiceFactory = ({ key: botKey }); - const canReadValue = CheckCanSecretsSubject(permission, ProjectPermissionSecretActions.ReadValue, { - environment: encryptedSnapshotDetails.environment.slug, - secretPath: fullFolderPath, - secretName: secretKey, - secretTags: el.tags.length ? el.tags.map((tag) => tag.slug) : undefined - }); + const canReadValue = hasSecretReadValueOrDescribePermission( + permission, + ProjectPermissionSecretActions.ReadValue, + { + environment: encryptedSnapshotDetails.environment.slug, + secretPath: fullFolderPath, + secretName: secretKey, + secretTags: el.tags.length ? el.tags.map((tag) => tag.slug) : undefined + } + ); let secretValue = ""; diff --git a/backend/src/services/integration/integration-service.ts b/backend/src/services/integration/integration-service.ts index 3f7c55896..ad08e89d1 100644 --- a/backend/src/services/integration/integration-service.ts +++ b/backend/src/services/integration/integration-service.ts @@ -1,7 +1,7 @@ import { ForbiddenError } from "@casl/ability"; import { ActionProjectType } from "@app/db/schemas"; -import { CheckForbiddenErrorSecretsSubject } from "@app/ee/services/permission/permission-fns"; +import { throwIfMissingSecretReadValueOrDescribePermission } from "@app/ee/services/permission/permission-fns"; import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service"; import { ProjectPermissionActions, @@ -96,7 +96,7 @@ export const integrationServiceFactory = ({ }); ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Create, ProjectPermissionSub.Integrations); - CheckForbiddenErrorSecretsSubject(permission, ProjectPermissionSecretActions.ReadValue, { + throwIfMissingSecretReadValueOrDescribePermission(permission, ProjectPermissionSecretActions.ReadValue, { environment: sourceEnvironment, secretPath }); @@ -176,7 +176,7 @@ export const integrationServiceFactory = ({ const newSecretPath = secretPath || integration.secretPath; if (environment || secretPath) { - CheckForbiddenErrorSecretsSubject(permission, ProjectPermissionSecretActions.ReadValue, { + throwIfMissingSecretReadValueOrDescribePermission(permission, ProjectPermissionSecretActions.ReadValue, { environment: newEnvironment, secretPath: newSecretPath }); diff --git a/backend/src/services/project/project-service.ts b/backend/src/services/project/project-service.ts index b21cc99fe..dbef33c10 100644 --- a/backend/src/services/project/project-service.ts +++ b/backend/src/services/project/project-service.ts @@ -10,7 +10,7 @@ import { } from "@app/db/schemas"; import { TLicenseServiceFactory } from "@app/ee/services/license/license-service"; import { OrgPermissionActions, OrgPermissionSubjects } from "@app/ee/services/permission/org-permission"; -import { CheckForbiddenErrorSecretsSubject } from "@app/ee/services/permission/permission-fns"; +import { throwIfMissingSecretReadValueOrDescribePermission } from "@app/ee/services/permission/permission-fns"; import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service"; import { ProjectPermissionActions, @@ -765,7 +765,7 @@ export const projectServiceFactory = ({ actorOrgId, actionProjectType: ActionProjectType.Any }); - CheckForbiddenErrorSecretsSubject(permission, ProjectPermissionSecretActions.DescribeSecret); + throwIfMissingSecretReadValueOrDescribePermission(permission, ProjectPermissionSecretActions.DescribeSecret); const project = await projectDAL.findProjectById(projectId); diff --git a/backend/src/services/secret-import/secret-import-service.ts b/backend/src/services/secret-import/secret-import-service.ts index 27ed12c84..b8e8b2fa0 100644 --- a/backend/src/services/secret-import/secret-import-service.ts +++ b/backend/src/services/secret-import/secret-import-service.ts @@ -4,7 +4,10 @@ import { ForbiddenError, subject } from "@casl/ability"; import { ActionProjectType, TableName } from "@app/db/schemas"; import { TLicenseServiceFactory } from "@app/ee/services/license/license-service"; -import { CheckCanSecretsSubject, CheckForbiddenErrorSecretsSubject } from "@app/ee/services/permission/permission-fns"; +import { + hasSecretReadValueOrDescribePermission, + throwIfMissingSecretReadValueOrDescribePermission +} from "@app/ee/services/permission/permission-fns"; import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service"; import { ProjectPermissionActions, @@ -94,7 +97,7 @@ export const secretImportServiceFactory = ({ ); // check if user has permission to import from target path - CheckForbiddenErrorSecretsSubject(permission, ProjectPermissionSecretActions.DescribeSecret, { + throwIfMissingSecretReadValueOrDescribePermission(permission, ProjectPermissionSecretActions.DescribeSecret, { environment: data.environment, secretPath: data.path }); @@ -404,7 +407,7 @@ export const secretImportServiceFactory = ({ if (!secretImportDoc.isReplication) throw new BadRequestError({ message: "Import is not in replication mode" }); // check if user has permission to import from target path - CheckForbiddenErrorSecretsSubject(permission, ProjectPermissionSecretActions.DescribeSecret, { + throwIfMissingSecretReadValueOrDescribePermission(permission, ProjectPermissionSecretActions.DescribeSecret, { environment: secretImportDoc.importEnv.slug, secretPath: secretImportDoc.importPath }); @@ -595,7 +598,7 @@ export const secretImportServiceFactory = ({ // so anything based on this order will also be in right position const secretImports = await secretImportDAL.find({ folderId: folder.id, isReplication: false }); const allowedImports = secretImports.filter((el) => - CheckCanSecretsSubject(permission, ProjectPermissionSecretActions.ReadValue, { + hasSecretReadValueOrDescribePermission(permission, ProjectPermissionSecretActions.ReadValue, { environment: el.importEnv.slug, secretPath: el.importPath }) @@ -645,7 +648,7 @@ export const secretImportServiceFactory = ({ secretImportDAL, decryptor: (value) => (value ? secretManagerDecryptor({ cipherTextBlob: value }).toString() : ""), hasSecretAccess: (expandEnvironment, expandSecretPath, expandSecretKey, expandSecretTags) => - CheckCanSecretsSubject(permission, ProjectPermissionSecretActions.ReadValue, { + hasSecretReadValueOrDescribePermission(permission, ProjectPermissionSecretActions.ReadValue, { environment: expandEnvironment, secretPath: expandSecretPath, secretName: expandSecretKey, @@ -663,7 +666,7 @@ export const secretImportServiceFactory = ({ }); const allowedImports = secretImports.filter((el) => - CheckCanSecretsSubject(permission, ProjectPermissionSecretActions.ReadValue, { + hasSecretReadValueOrDescribePermission(permission, ProjectPermissionSecretActions.ReadValue, { environment: el.importEnv.slug, secretPath: el.importPath }) diff --git a/backend/src/services/secret-sync/secret-sync-service.ts b/backend/src/services/secret-sync/secret-sync-service.ts index d74ea120e..5c4a7e850 100644 --- a/backend/src/services/secret-sync/secret-sync-service.ts +++ b/backend/src/services/secret-sync/secret-sync-service.ts @@ -1,7 +1,7 @@ import { ForbiddenError } from "@casl/ability"; import { ActionProjectType } from "@app/db/schemas"; -import { CheckForbiddenErrorSecretsSubject } from "@app/ee/services/permission/permission-fns"; +import { throwIfMissingSecretReadValueOrDescribePermission } from "@app/ee/services/permission/permission-fns"; import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service"; import { ProjectPermissionSecretActions, @@ -179,7 +179,7 @@ export const secretSyncServiceFactory = ({ ProjectPermissionSub.SecretSyncs ); - CheckForbiddenErrorSecretsSubject(projectPermission, ProjectPermissionSecretActions.ReadValue, { + throwIfMissingSecretReadValueOrDescribePermission(projectPermission, ProjectPermissionSecretActions.ReadValue, { environment, secretPath }); @@ -267,7 +267,7 @@ export const secretSyncServiceFactory = ({ if (!updatedEnvironment || !updatedSecretPath) throw new BadRequestError({ message: "Must specify both source environment and secret path" }); - CheckForbiddenErrorSecretsSubject(permission, ProjectPermissionSecretActions.ReadValue, { + throwIfMissingSecretReadValueOrDescribePermission(permission, ProjectPermissionSecretActions.ReadValue, { environment: updatedEnvironment, secretPath: updatedSecretPath }); diff --git a/backend/src/services/secret-v2-bridge/secret-v2-bridge-service.ts b/backend/src/services/secret-v2-bridge/secret-v2-bridge-service.ts index 3b3a147bd..45c6493d6 100644 --- a/backend/src/services/secret-v2-bridge/secret-v2-bridge-service.ts +++ b/backend/src/services/secret-v2-bridge/secret-v2-bridge-service.ts @@ -10,7 +10,10 @@ import { TableName, TSecretsV2 } from "@app/db/schemas"; -import { CheckCanSecretsSubject, CheckForbiddenErrorSecretsSubject } from "@app/ee/services/permission/permission-fns"; +import { + hasSecretReadValueOrDescribePermission, + throwIfMissingSecretReadValueOrDescribePermission +} from "@app/ee/services/permission/permission-fns"; import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service"; import { ProjectPermissionActions, @@ -197,7 +200,7 @@ export const secretV2BridgeServiceFactory = ({ const referredSecretsGroupBySecretKey = groupBy(referredSecrets, (i) => i.key); references.forEach((el) => { - CheckForbiddenErrorSecretsSubject(permission, ProjectPermissionSecretActions.ReadValue, { + throwIfMissingSecretReadValueOrDescribePermission(permission, ProjectPermissionSecretActions.ReadValue, { environment: el.environment, secretPath: el.secretPath, secretName: el.secretKey, @@ -542,14 +545,18 @@ export const secretV2BridgeServiceFactory = ({ }); } - const secretValueHidden = !CheckCanSecretsSubject(permission, ProjectPermissionSecretActions.ReadValue, { - environment, - secretPath, - secretName: inputSecret.secretName, - ...(tagsToCheck.length && { - secretTags: tagsToCheck.map((el) => el.slug) - }) - }); + const secretValueHidden = !hasSecretReadValueOrDescribePermission( + permission, + ProjectPermissionSecretActions.ReadValue, + { + environment, + secretPath, + secretName: inputSecret.secretName, + ...(tagsToCheck.length && { + secretTags: tagsToCheck.map((el) => el.slug) + }) + } + ); return reshapeBridgeSecret( projectId, @@ -650,12 +657,16 @@ export const secretV2BridgeServiceFactory = ({ projectId }); - const secretValueHidden = !CheckCanSecretsSubject(permission, ProjectPermissionSecretActions.ReadValue, { - environment, - secretPath, - secretName: secretToDelete.key, - secretTags: secretToDelete.tags?.map((el) => el.slug) - }); + const secretValueHidden = !hasSecretReadValueOrDescribePermission( + permission, + ProjectPermissionSecretActions.ReadValue, + { + environment, + secretPath, + secretName: secretToDelete.key, + secretTags: secretToDelete.tags?.map((el) => el.slug) + } + ); return reshapeBridgeSecret( projectId, @@ -698,7 +709,7 @@ export const secretV2BridgeServiceFactory = ({ actorOrgId, actionProjectType: ActionProjectType.SecretManager }); - CheckForbiddenErrorSecretsSubject(permission, ProjectPermissionSecretActions.DescribeSecret); + throwIfMissingSecretReadValueOrDescribePermission(permission, ProjectPermissionSecretActions.DescribeSecret); } const folders = await folderDAL.findBySecretPathMultiEnv(projectId, environments, path); @@ -744,7 +755,7 @@ export const secretV2BridgeServiceFactory = ({ actorOrgId, actionProjectType: ActionProjectType.SecretManager }); - CheckForbiddenErrorSecretsSubject(permission, ProjectPermissionSecretActions.DescribeSecret); + throwIfMissingSecretReadValueOrDescribePermission(permission, ProjectPermissionSecretActions.DescribeSecret); const folder = await folderDAL.findBySecretPath(projectId, environment, path); if (!folder) return 0; @@ -779,37 +790,27 @@ export const secretV2BridgeServiceFactory = ({ }); const decryptedSecrets = secrets - .filter((el) => { - if ( - filterByAction === ProjectPermissionSecretActions.ReadValue || - filterByAction === ProjectPermissionSecretActions.DescribeSecret - ) { - return CheckCanSecretsSubject(projectPermission, filterByAction, { - environment: groupedFolderMappings[el.folderId][0].environment, - secretPath: groupedFolderMappings[el.folderId][0].path, - secretName: el.key, - secretTags: el.tags.map((i) => i.slug) - }); - } + .filter((el) => + hasSecretReadValueOrDescribePermission(projectPermission, filterByAction, { + environment: groupedFolderMappings[el.folderId][0].environment, + secretPath: groupedFolderMappings[el.folderId][0].path, + secretName: el.key, + secretTags: el.tags.map((i) => i.slug) + }) + ) - return projectPermission.can( - filterByAction, - subject(ProjectPermissionSub.Secrets, { - environment: groupedFolderMappings[el.folderId][0].environment, - secretPath: groupedFolderMappings[el.folderId][0].path, - secretName: el.key, - secretTags: el.tags.map((i) => i.slug) - }) - ); - }) .map((secret) => { // Note(Daniel): This is only relevant if the filterAction isn't set to ReadValue. This is needed for the frontend. - const secretValueHidden = !CheckCanSecretsSubject(projectPermission, ProjectPermissionSecretActions.ReadValue, { - environment: groupedFolderMappings[secret.folderId][0].environment, - secretPath: groupedFolderMappings[secret.folderId][0].path, - secretName: secret.key, - secretTags: secret.tags.map((i) => i.slug) - }); + const secretValueHidden = !hasSecretReadValueOrDescribePermission( + projectPermission, + ProjectPermissionSecretActions.ReadValue, + { + environment: groupedFolderMappings[secret.folderId][0].environment, + secretPath: groupedFolderMappings[secret.folderId][0].path, + secretName: secret.key, + secretTags: secret.tags.map((i) => i.slug) + } + ); return reshapeBridgeSecret( projectId, @@ -855,7 +856,7 @@ export const secretV2BridgeServiceFactory = ({ actionProjectType: ActionProjectType.SecretManager }); if (!isInternal) { - CheckForbiddenErrorSecretsSubject(permission, ProjectPermissionSecretActions.DescribeSecret); + throwIfMissingSecretReadValueOrDescribePermission(permission, ProjectPermissionSecretActions.DescribeSecret); } const folders = await folderDAL.findBySecretPathMultiEnv(projectId, environments, path); @@ -907,7 +908,7 @@ export const secretV2BridgeServiceFactory = ({ actorOrgId, actionProjectType: ActionProjectType.SecretManager }); - CheckForbiddenErrorSecretsSubject(permission, ProjectPermissionSecretActions.DescribeSecret, { + throwIfMissingSecretReadValueOrDescribePermission(permission, ProjectPermissionSecretActions.DescribeSecret, { environment, secretPath: path, secretTags: params.tagSlugs @@ -950,12 +951,16 @@ export const secretV2BridgeServiceFactory = ({ const decryptedSecrets = secrets .filter((el) => { - const canDescribeSecret = CheckCanSecretsSubject(permission, ProjectPermissionSecretActions.DescribeSecret, { - environment, - secretPath: groupedPaths[el.folderId][0].path, - secretName: el.key, - secretTags: el.tags.map((i) => i.slug) - }); + const canDescribeSecret = hasSecretReadValueOrDescribePermission( + permission, + ProjectPermissionSecretActions.DescribeSecret, + { + environment, + secretPath: groupedPaths[el.folderId][0].path, + secretName: el.key, + secretTags: el.tags.map((i) => i.slug) + } + ); if (!canDescribeSecret) { return false; @@ -964,7 +969,7 @@ export const secretV2BridgeServiceFactory = ({ if (viewSecretValue) { // Recursive secret, should be filtered out if (groupedPaths[el.folderId][0].path !== path) { - const canReadRecursiveSecretValue = CheckCanSecretsSubject( + const canReadRecursiveSecretValue = hasSecretReadValueOrDescribePermission( permission, ProjectPermissionSecretActions.ReadValue, { @@ -981,7 +986,7 @@ export const secretV2BridgeServiceFactory = ({ } if (throwOnMissingReadValuePermission) { - CheckForbiddenErrorSecretsSubject(permission, ProjectPermissionSecretActions.ReadValue, { + throwIfMissingSecretReadValueOrDescribePermission(permission, ProjectPermissionSecretActions.ReadValue, { environment, secretPath: groupedPaths[el.folderId][0].path, secretName: el.key, @@ -999,7 +1004,7 @@ export const secretV2BridgeServiceFactory = ({ const secretValueHidden = !viewSecretValue || - !CheckCanSecretsSubject(permission, ProjectPermissionSecretActions.ReadValue, { + !hasSecretReadValueOrDescribePermission(permission, ProjectPermissionSecretActions.ReadValue, { environment, secretPath: groupedPaths[secret.folderId][0].path, secretName: secret.key, @@ -1029,7 +1034,7 @@ export const secretV2BridgeServiceFactory = ({ secretDAL, decryptSecretValue: (value) => (value ? secretManagerDecryptor({ cipherTextBlob: value }).toString() : undefined), canExpandValue: (expandEnvironment, expandSecretPath, expandSecretKey, expandSecretTags) => - CheckCanSecretsSubject(permission, ProjectPermissionSecretActions.ReadValue, { + hasSecretReadValueOrDescribePermission(permission, ProjectPermissionSecretActions.ReadValue, { environment: expandEnvironment, secretPath: expandSecretPath, secretName: expandSecretKey, @@ -1074,19 +1079,27 @@ export const secretV2BridgeServiceFactory = ({ expandSecretReferences, decryptor: (value) => (value ? secretManagerDecryptor({ cipherTextBlob: value }).toString() : ""), hasSecretAccess: (expandEnvironment, expandSecretPath, expandSecretKey, expandSecretTags) => { - const canDescribe = CheckCanSecretsSubject(permission, ProjectPermissionSecretActions.DescribeSecret, { - environment: expandEnvironment, - secretPath: expandSecretPath, - secretName: expandSecretKey, - secretTags: expandSecretTags - }); + const canDescribe = hasSecretReadValueOrDescribePermission( + permission, + ProjectPermissionSecretActions.DescribeSecret, + { + environment: expandEnvironment, + secretPath: expandSecretPath, + secretName: expandSecretKey, + secretTags: expandSecretTags + } + ); - const canReadValue = CheckCanSecretsSubject(permission, ProjectPermissionSecretActions.ReadValue, { - environment: expandEnvironment, - secretPath: expandSecretPath, - secretName: expandSecretKey, - secretTags: expandSecretTags - }); + const canReadValue = hasSecretReadValueOrDescribePermission( + permission, + ProjectPermissionSecretActions.ReadValue, + { + environment: expandEnvironment, + secretPath: expandSecretPath, + secretName: expandSecretKey, + secretTags: expandSecretTags + } + ); return viewSecretValue ? canDescribe && canReadValue : canDescribe; } @@ -1128,7 +1141,7 @@ export const secretV2BridgeServiceFactory = ({ actionProjectType: ActionProjectType.SecretManager }); - CheckForbiddenErrorSecretsSubject(permission, ProjectPermissionSecretActions.ReadValue, { + throwIfMissingSecretReadValueOrDescribePermission(permission, ProjectPermissionSecretActions.ReadValue, { environment: folderWithPath.environmentSlug, secretPath: folderWithPath.path, secretName: secret.key, @@ -1240,7 +1253,7 @@ export const secretV2BridgeServiceFactory = ({ }) )); - CheckForbiddenErrorSecretsSubject(permission, ProjectPermissionSecretActions.DescribeSecret, { + throwIfMissingSecretReadValueOrDescribePermission(permission, ProjectPermissionSecretActions.DescribeSecret, { environment, secretPath: path, secretName, @@ -1255,7 +1268,7 @@ export const secretV2BridgeServiceFactory = ({ secretDAL, decryptSecretValue: (value) => (value ? secretManagerDecryptor({ cipherTextBlob: value }).toString() : undefined), canExpandValue: (expandEnvironment, expandSecretPath, expandSecretKey, expandSecretTags) => { - return CheckCanSecretsSubject(permission, ProjectPermissionSecretActions.ReadValue, { + return hasSecretReadValueOrDescribePermission(permission, ProjectPermissionSecretActions.ReadValue, { environment: expandEnvironment, secretPath: expandSecretPath, secretName: expandSecretKey, @@ -1280,7 +1293,7 @@ export const secretV2BridgeServiceFactory = ({ decryptor: (value) => (value ? secretManagerDecryptor({ cipherTextBlob: value }).toString() : ""), expandSecretReferences: shouldExpandSecretReferences ? expandSecretReferences : undefined, hasSecretAccess: (expandEnvironment, expandSecretPath, expandSecretKey, expandSecretTags) => { - return CheckCanSecretsSubject(permission, ProjectPermissionSecretActions.DescribeSecret, { + return hasSecretReadValueOrDescribePermission(permission, ProjectPermissionSecretActions.DescribeSecret, { environment: expandEnvironment, secretPath: expandSecretPath, secretName: expandSecretKey, @@ -1297,7 +1310,7 @@ export const secretV2BridgeServiceFactory = ({ if (viewSecretValue) { if ( - !CheckCanSecretsSubject(permission, ProjectPermissionSecretActions.ReadValue, { + !hasSecretReadValueOrDescribePermission(permission, ProjectPermissionSecretActions.ReadValue, { environment: importedSecret.environment, secretPath: importedSecrets[i].secretPath, secretName: importedSecret.key, @@ -1350,7 +1363,7 @@ export const secretV2BridgeServiceFactory = ({ if (viewSecretValue) { if ( - !CheckCanSecretsSubject(permission, ProjectPermissionSecretActions.ReadValue, { + !hasSecretReadValueOrDescribePermission(permission, ProjectPermissionSecretActions.ReadValue, { environment, secretPath: path, secretName, @@ -1523,12 +1536,16 @@ export const secretV2BridgeServiceFactory = ({ }); return newSecrets.map((el) => { - const secretValueHidden = !CheckCanSecretsSubject(permission, ProjectPermissionSecretActions.ReadValue, { - environment, - secretPath, - secretName: el.key, - secretTags: el.tags?.map((i) => i.slug) - }); + const secretValueHidden = !hasSecretReadValueOrDescribePermission( + permission, + ProjectPermissionSecretActions.ReadValue, + { + environment, + secretPath, + secretName: el.key, + secretTags: el.tags?.map((i) => i.slug) + } + ); return reshapeBridgeSecret( projectId, @@ -1857,12 +1874,16 @@ export const secretV2BridgeServiceFactory = ({ ); return updatedSecrets.map((el) => { - const secretValueHidden = !CheckCanSecretsSubject(permission, ProjectPermissionSecretActions.ReadValue, { - environment, - secretPath: el.secretPath, - secretName: el.key, - secretTags: el.tags.map((i) => i.slug) - }); + const secretValueHidden = !hasSecretReadValueOrDescribePermission( + permission, + ProjectPermissionSecretActions.ReadValue, + { + environment, + secretPath: el.secretPath, + secretName: el.key, + secretTags: el.tags.map((i) => i.slug) + } + ); return { ...reshapeBridgeSecret( @@ -1987,7 +2008,7 @@ export const secretV2BridgeServiceFactory = ({ const secretValueHidden = !secretToDeleteMatch || - !CheckCanSecretsSubject(permission, ProjectPermissionSecretActions.ReadValue, { + !hasSecretReadValueOrDescribePermission(permission, ProjectPermissionSecretActions.ReadValue, { environment, secretPath, secretName: el.key, @@ -2049,14 +2070,18 @@ export const secretV2BridgeServiceFactory = ({ sort: [["createdAt", "desc"]] }); return secretVersions.map((el) => { - const secretValueHidden = !CheckCanSecretsSubject(permission, ProjectPermissionSecretActions.ReadValue, { - environment: folder.environment.envSlug, - secretPath: folderWithPath.path, - secretName: el.key, - ...(el.tags?.length && { - secretTags: el.tags.map((tag) => tag.slug) - }) - }); + const secretValueHidden = !hasSecretReadValueOrDescribePermission( + permission, + ProjectPermissionSecretActions.ReadValue, + { + environment: folder.environment.envSlug, + secretPath: folderWithPath.path, + secretName: el.key, + ...(el.tags?.length && { + secretTags: el.tags.map((tag) => tag.slug) + }) + } + ); return reshapeBridgeSecret( folder.projectId, @@ -2178,7 +2203,7 @@ export const secretV2BridgeServiceFactory = ({ sourceAction === ProjectPermissionSecretActions.DescribeSecret || sourceAction === ProjectPermissionSecretActions.ReadValue ) { - CheckForbiddenErrorSecretsSubject(permission, sourceAction, { + throwIfMissingSecretReadValueOrDescribePermission(permission, sourceAction, { environment: sourceEnvironment, secretPath: sourceSecretPath, secretName: secret.key, @@ -2517,7 +2542,7 @@ export const secretV2BridgeServiceFactory = ({ actionProjectType: ActionProjectType.SecretManager }); - CheckForbiddenErrorSecretsSubject(permission, ProjectPermissionSecretActions.DescribeSecret, { + throwIfMissingSecretReadValueOrDescribePermission(permission, ProjectPermissionSecretActions.DescribeSecret, { environment, secretPath }); @@ -2541,7 +2566,7 @@ export const secretV2BridgeServiceFactory = ({ type: SecretType.Shared }); - CheckForbiddenErrorSecretsSubject(permission, ProjectPermissionSecretActions.DescribeSecret, { + throwIfMissingSecretReadValueOrDescribePermission(permission, ProjectPermissionSecretActions.DescribeSecret, { environment, secretPath, secretName, @@ -2558,7 +2583,7 @@ export const secretV2BridgeServiceFactory = ({ secretDAL, decryptSecretValue: (value) => (value ? secretManagerDecryptor({ cipherTextBlob: value }).toString() : undefined), canExpandValue: (expandEnvironment, expandSecretPath, expandSecretName, expandSecretTags) => - CheckCanSecretsSubject(permission, ProjectPermissionSecretActions.ReadValue, { + hasSecretReadValueOrDescribePermission(permission, ProjectPermissionSecretActions.ReadValue, { environment: expandEnvironment, secretPath: expandSecretPath, secretName: expandSecretName, @@ -2567,7 +2592,7 @@ export const secretV2BridgeServiceFactory = ({ }); if ( - !CheckCanSecretsSubject(permission, ProjectPermissionSecretActions.ReadValue, { + !hasSecretReadValueOrDescribePermission(permission, ProjectPermissionSecretActions.ReadValue, { environment, secretPath, secretName, diff --git a/backend/src/services/secret-v2-bridge/secret-v2-bridge-types.ts b/backend/src/services/secret-v2-bridge/secret-v2-bridge-types.ts index 510181d0c..473d1ed83 100644 --- a/backend/src/services/secret-v2-bridge/secret-v2-bridge-types.ts +++ b/backend/src/services/secret-v2-bridge/secret-v2-bridge-types.ts @@ -349,5 +349,5 @@ export type TGetSecretsRawByFolderMappingsDTO = { folderMappings: { folderId: string; path: string; environment: string }[]; userId: string; filters: TFindSecretsByFolderIdsFilter; - filterByAction?: ProjectPermissionSecretActions; + filterByAction?: ProjectPermissionSecretActions.DescribeSecret | ProjectPermissionSecretActions.ReadValue; }; diff --git a/backend/src/services/secret/secret-fns.ts b/backend/src/services/secret/secret-fns.ts index ddaa054ab..aa8cec2bc 100644 --- a/backend/src/services/secret/secret-fns.ts +++ b/backend/src/services/secret/secret-fns.ts @@ -11,7 +11,7 @@ import { TSecretFolders, TSecrets } from "@app/db/schemas"; -import { CheckCanSecretsSubject } from "@app/ee/services/permission/permission-fns"; +import { hasSecretReadValueOrDescribePermission } from "@app/ee/services/permission/permission-fns"; import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service"; import { ProjectPermissionSecretActions } from "@app/ee/services/permission/project-permission"; import { getConfig } from "@app/lib/config/env"; @@ -191,7 +191,7 @@ export const recursivelyGetSecretPaths = ({ // Filter out paths that the user does not have permission to access, and paths that are not in the current path const allowedPaths = paths.filter( (folder) => - CheckCanSecretsSubject(permission, ProjectPermissionSecretActions.ReadValue, { + hasSecretReadValueOrDescribePermission(permission, ProjectPermissionSecretActions.ReadValue, { environment, secretPath: folder.path }) && folder.path.startsWith(currentPath === "/" ? "" : currentPath) diff --git a/backend/src/services/secret/secret-service.ts b/backend/src/services/secret/secret-service.ts index e3eb75bda..3593adf88 100644 --- a/backend/src/services/secret/secret-service.ts +++ b/backend/src/services/secret/secret-service.ts @@ -13,7 +13,10 @@ import { SecretType } from "@app/db/schemas"; import { TLicenseServiceFactory } from "@app/ee/services/license/license-service"; -import { CheckCanSecretsSubject, CheckForbiddenErrorSecretsSubject } from "@app/ee/services/permission/permission-fns"; +import { + hasSecretReadValueOrDescribePermission, + throwIfMissingSecretReadValueOrDescribePermission +} from "@app/ee/services/permission/permission-fns"; import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service"; import { ProjectPermissionActions, @@ -453,10 +456,14 @@ export const secretServiceFactory = ({ }); } - const secretValueHidden = !CheckCanSecretsSubject(permission, ProjectPermissionSecretActions.ReadValue, { - environment, - secretPath: path - }); + const secretValueHidden = !hasSecretReadValueOrDescribePermission( + permission, + ProjectPermissionSecretActions.ReadValue, + { + environment, + secretPath: path + } + ); return { ...updatedSecret[0], @@ -560,10 +567,14 @@ export const secretServiceFactory = ({ }); } - const secretValueHidden = !CheckCanSecretsSubject(permission, ProjectPermissionSecretActions.ReadValue, { - environment, - secretPath: path - }); + const secretValueHidden = !hasSecretReadValueOrDescribePermission( + permission, + ProjectPermissionSecretActions.ReadValue, + { + environment, + secretPath: path + } + ); return { ...deletedSecret[0], @@ -620,7 +631,7 @@ export const secretServiceFactory = ({ paths = deepPaths.map(({ folderId, path: p }) => ({ folderId, path: p })); } else { - CheckForbiddenErrorSecretsSubject(permission, ProjectPermissionSecretActions.ReadValue, { + throwIfMissingSecretReadValueOrDescribePermission(permission, ProjectPermissionSecretActions.ReadValue, { environment, secretPath: path }); @@ -645,7 +656,7 @@ export const secretServiceFactory = ({ // if its service token allow full access over imported one actor === ActorType.SERVICE ? true - : CheckCanSecretsSubject(permission, ProjectPermissionSecretActions.ReadValue, { + : hasSecretReadValueOrDescribePermission(permission, ProjectPermissionSecretActions.ReadValue, { environment: importEnv.slug, secretPath: importPath }) @@ -699,7 +710,7 @@ export const secretServiceFactory = ({ actorOrgId, actionProjectType: ActionProjectType.SecretManager }); - CheckForbiddenErrorSecretsSubject(permission, ProjectPermissionSecretActions.ReadValue, { + throwIfMissingSecretReadValueOrDescribePermission(permission, ProjectPermissionSecretActions.ReadValue, { environment, secretPath: path }); @@ -750,7 +761,7 @@ export const secretServiceFactory = ({ // if its service token allow full access over imported one actor === ActorType.SERVICE ? true - : CheckCanSecretsSubject(permission, ProjectPermissionSecretActions.ReadValue, { + : hasSecretReadValueOrDescribePermission(permission, ProjectPermissionSecretActions.ReadValue, { environment: importEnv.slug, secretPath: importPath }) @@ -969,10 +980,14 @@ export const secretServiceFactory = ({ secretVersionTagDAL }); - const secretValueHidden = !CheckCanSecretsSubject(permission, ProjectPermissionSecretActions.ReadValue, { - environment, - secretPath: path - }); + const secretValueHidden = !hasSecretReadValueOrDescribePermission( + permission, + ProjectPermissionSecretActions.ReadValue, + { + environment, + secretPath: path + } + ); return updatedSecrets.map((secret) => ({ ...secret, @@ -1063,10 +1078,14 @@ export const secretServiceFactory = ({ }); } } - const secretValueHidden = !CheckCanSecretsSubject(permission, ProjectPermissionSecretActions.ReadValue, { - environment, - secretPath: path - }); + const secretValueHidden = !hasSecretReadValueOrDescribePermission( + permission, + ProjectPermissionSecretActions.ReadValue, + { + environment, + secretPath: path + } + ); return secrets.map((secret) => ({ ...secret, @@ -1257,7 +1276,7 @@ export const secretServiceFactory = ({ action === ProjectPermissionSecretActions.DescribeSecret || action === ProjectPermissionSecretActions.ReadValue ) { - return CheckCanSecretsSubject(entityPermission.permission, action, { + return hasSecretReadValueOrDescribePermission(entityPermission.permission, action, { environment, secretPath, secretName, @@ -2429,14 +2448,18 @@ export const secretServiceFactory = ({ key: botKey }); - const secretValueHidden = !CheckCanSecretsSubject(permission, ProjectPermissionSecretActions.ReadValue, { - environment: folder.environment.envSlug, - secretPath: folderWithPath.path, - secretName: secretKey, - ...(el.tags?.length && { - secretTags: el.tags.map((tag) => tag.slug) - }) - }); + const secretValueHidden = !hasSecretReadValueOrDescribePermission( + permission, + ProjectPermissionSecretActions.ReadValue, + { + environment: folder.environment.envSlug, + secretPath: folderWithPath.path, + secretName: secretKey, + ...(el.tags?.length && { + secretTags: el.tags.map((tag) => tag.slug) + }) + } + ); return decryptSecretRaw( { @@ -2839,7 +2862,7 @@ export const secretServiceFactory = ({ sourceAction === ProjectPermissionSecretActions.ReadValue || sourceAction === ProjectPermissionSecretActions.DescribeSecret ) { - CheckForbiddenErrorSecretsSubject(permission, sourceAction, { + throwIfMissingSecretReadValueOrDescribePermission(permission, sourceAction, { environment: sourceEnvironment, secretPath: sourceSecretPath }); diff --git a/frontend/src/lib/fn/permission.ts b/frontend/src/lib/fn/permission.ts index f2077d76b..c3937a0ff 100644 --- a/frontend/src/lib/fn/permission.ts +++ b/frontend/src/lib/fn/permission.ts @@ -7,7 +7,7 @@ import { SecretSubjectFields } from "@app/context/ProjectPermissionContext/types"; -export function secretsPermissionCan( +export function hasSecretReadValueOrDescribePermission( permission: MongoAbility, action: Extract< ProjectPermissionSecretActions, diff --git a/frontend/src/pages/project/RoleDetailsBySlugPage/components/ProjectRoleModifySection.utils.tsx b/frontend/src/pages/project/RoleDetailsBySlugPage/components/ProjectRoleModifySection.utils.tsx index 9f13b53a8..a47a64614 100644 --- a/frontend/src/pages/project/RoleDetailsBySlugPage/components/ProjectRoleModifySection.utils.tsx +++ b/frontend/src/pages/project/RoleDetailsBySlugPage/components/ProjectRoleModifySection.utils.tsx @@ -29,11 +29,11 @@ const GeneralPolicyActionSchema = z.object({ const SecretPolicyActionSchema = z.object({ [ProjectPermissionSecretActions.DescribeAndReadValue]: z.boolean().optional(), // existing read, gives both describe and read value - [ProjectPermissionSecretActions.DescribeSecret]: z.boolean().optional(), // describe secret, cannot read value - [ProjectPermissionSecretActions.ReadValue]: z.boolean().optional(), // read value - [ProjectPermissionSecretActions.Edit]: z.boolean().optional(), // edit secret - [ProjectPermissionSecretActions.Delete]: z.boolean().optional(), // delete secret - [ProjectPermissionSecretActions.Create]: z.boolean().optional() // create secret + [ProjectPermissionSecretActions.DescribeSecret]: z.boolean().optional(), + [ProjectPermissionSecretActions.ReadValue]: z.boolean().optional(), + [ProjectPermissionSecretActions.Edit]: z.boolean().optional(), + [ProjectPermissionSecretActions.Delete]: z.boolean().optional(), + [ProjectPermissionSecretActions.Create]: z.boolean().optional() }); const CmekPolicyActionSchema = z.object({ diff --git a/frontend/src/pages/secret-manager/OverviewPage/components/SecretOverviewTableRow/SecretRenameRow.tsx b/frontend/src/pages/secret-manager/OverviewPage/components/SecretOverviewTableRow/SecretRenameRow.tsx index 536f75542..ce2e43f67 100644 --- a/frontend/src/pages/secret-manager/OverviewPage/components/SecretOverviewTableRow/SecretRenameRow.tsx +++ b/frontend/src/pages/secret-manager/OverviewPage/components/SecretOverviewTableRow/SecretRenameRow.tsx @@ -15,7 +15,7 @@ import { ProjectPermissionSecretActions } from "@app/context/ProjectPermissionCo import { useToggle } from "@app/hooks"; import { useUpdateSecretV3 } from "@app/hooks/api"; import { SecretType, SecretV3RawSanitized } from "@app/hooks/api/types"; -import { secretsPermissionCan } from "@app/lib/fn/permission"; +import { hasSecretReadValueOrDescribePermission } from "@app/lib/fn/permission"; enum SecretActionType { Created = "created", @@ -52,7 +52,7 @@ function SecretRenameRow({ environments, getSecretByKey, secretKey, secretPath } secretTags: (secretDetails?.tags || []).map((i) => i.slug) }); const isSecretInEnvReadOnly = - secretsPermissionCan( + hasSecretReadValueOrDescribePermission( permission, ProjectPermissionSecretActions.DescribeSecret, secretPermissionSubject diff --git a/frontend/src/pages/secret-manager/SecretDashboardPage/SecretDashboardPage.tsx b/frontend/src/pages/secret-manager/SecretDashboardPage/SecretDashboardPage.tsx index 4dba1c2a8..e6fc5eadf 100644 --- a/frontend/src/pages/secret-manager/SecretDashboardPage/SecretDashboardPage.tsx +++ b/frontend/src/pages/secret-manager/SecretDashboardPage/SecretDashboardPage.tsx @@ -38,7 +38,7 @@ import { useGetProjectSecretsDetails } from "@app/hooks/api/dashboard"; import { DashboardSecretsOrderBy } from "@app/hooks/api/dashboard/types"; import { OrderByDirection } from "@app/hooks/api/generic/types"; import { ProjectType } from "@app/hooks/api/workspace/types"; -import { secretsPermissionCan } from "@app/lib/fn/permission"; +import { hasSecretReadValueOrDescribePermission } from "@app/lib/fn/permission"; import { SecretTableResourceCount } from "../OverviewPage/components/SecretTableResourceCount"; import { SecretV2MigrationSection } from "../OverviewPage/components/SecretV2MigrationSection"; @@ -105,7 +105,7 @@ const Page = () => { const projectSlug = currentWorkspace?.slug || ""; const secretPath = (routerQueryParams.secretPath as string) || "/"; - const canReadSecret = secretsPermissionCan( + const canReadSecret = hasSecretReadValueOrDescribePermission( permission, ProjectPermissionSecretActions.DescribeSecret, { @@ -116,7 +116,7 @@ const Page = () => { } ); - const canReadSecretValue = secretsPermissionCan( + const canReadSecretValue = hasSecretReadValueOrDescribePermission( permission, ProjectPermissionSecretActions.ReadValue, { diff --git a/frontend/src/pages/secret-manager/SecretDashboardPage/components/SecretListView/SecretDetailSidebar.tsx b/frontend/src/pages/secret-manager/SecretDashboardPage/components/SecretListView/SecretDetailSidebar.tsx index 3ad7159d7..410d49e5f 100644 --- a/frontend/src/pages/secret-manager/SecretDashboardPage/components/SecretListView/SecretDetailSidebar.tsx +++ b/frontend/src/pages/secret-manager/SecretDashboardPage/components/SecretListView/SecretDetailSidebar.tsx @@ -57,7 +57,7 @@ import { ActorType } from "@app/hooks/api/auditLogs/enums"; import { useGetSecretAccessList } from "@app/hooks/api/secrets/queries"; import { SecretV3RawSanitized, WsTag } from "@app/hooks/api/types"; import { ProjectType } from "@app/hooks/api/workspace/types"; -import { secretsPermissionCan } from "@app/lib/fn/permission"; +import { hasSecretReadValueOrDescribePermission } from "@app/lib/fn/permission"; import { CreateReminderForm } from "./CreateReminderForm"; import { formSchema, SecretActionType, TFormSchema } from "./SecretListView.utils"; @@ -141,7 +141,7 @@ export const SecretDetailSidebar = ({ }) ); - const cannotReadSecretValue = !secretsPermissionCan( + const cannotReadSecretValue = !hasSecretReadValueOrDescribePermission( permission, ProjectPermissionSecretActions.ReadValue, { @@ -153,12 +153,16 @@ export const SecretDetailSidebar = ({ ); const isReadOnly = - secretsPermissionCan(permission, ProjectPermissionSecretActions.DescribeSecret, { - environment, - secretPath, - secretName: secretKey, - secretTags: selectTagSlugs - }) && + hasSecretReadValueOrDescribePermission( + permission, + ProjectPermissionSecretActions.DescribeSecret, + { + environment, + secretPath, + secretName: secretKey, + secretTags: selectTagSlugs + } + ) && cannotEditSecret && cannotReadSecretValue; @@ -361,11 +365,11 @@ export const SecretDetailSidebar = ({ >
Date: Tue, 11 Mar 2025 00:33:11 +0400 Subject: [PATCH 25/71] Update go.sum --- cli/go.sum | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/cli/go.sum b/cli/go.sum index d209fd1ca..b72858e9d 100644 --- a/cli/go.sum +++ b/cli/go.sum @@ -367,8 +367,6 @@ github.com/pion/stun/v3 v3.0.0 h1:4h1gwhWLWuZWOJIJR9s2ferRO+W3zA/b6ijOI6mKzUw= github.com/pion/stun/v3 v3.0.0/go.mod h1:HvCN8txt8mwi4FBvS3EmDghW6aQJ24T+y+1TKjB5jyU= github.com/pion/transport/v3 v3.0.7 h1:iRbMH05BzSNwhILHoBoAPxoB9xQgOaJk+591KC9P1o0= github.com/pion/transport/v3 v3.0.7/go.mod h1:YleKiTZ4vqNxVwh77Z0zytYi7rXHl7j6uPLGhhz9rwo= -github.com/pion/turn/v4 v4.0.0 h1:qxplo3Rxa9Yg1xXDxxH8xaqcyGUtbHYw4QSCvmFWvhM= -github.com/pion/turn/v4 v4.0.0/go.mod h1:MuPDkm15nYSklKpN8vWJ9W2M0PlyQZqYt1McGuxG7mA= github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= @@ -427,8 +425,8 @@ github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/ github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= -github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= -github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= +github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= github.com/subosito/gotenv v1.2.0 h1:Slr1R9HxAlEKefgq5jn9U+DnETlIUa6HfgEzj0g5d7s= github.com/subosito/gotenv v1.2.0/go.mod h1:N0PQaV/YGNqwC0u51sEeR/aUtSLEXKX9iv69rRypqCw= github.com/tidwall/pretty v1.0.0 h1:HsD+QiTn7sK6flMKIvNmpqz1qrpP3Ps6jOKIKMooyg4= @@ -870,4 +868,4 @@ honnef.co/go/tools v0.0.1-2020.1.3/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9 honnef.co/go/tools v0.0.1-2020.1.4/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k= rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8= rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0= -rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA= +rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA= \ No newline at end of file From 4eff7d8ea54237e6cb3bb11fb8c8d2efc5c94012 Mon Sep 17 00:00:00 2001 From: Mahyar Mirrashed Date: Mon, 10 Mar 2025 11:16:19 -0700 Subject: [PATCH 26/71] fix(audit): change dd-mm-yy to mm-dd-yy --- .../organization/AuditLogsPage/components/LogsTableRow.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/src/pages/organization/AuditLogsPage/components/LogsTableRow.tsx b/frontend/src/pages/organization/AuditLogsPage/components/LogsTableRow.tsx index 5c3337938..cc67e5319 100644 --- a/frontend/src/pages/organization/AuditLogsPage/components/LogsTableRow.tsx +++ b/frontend/src/pages/organization/AuditLogsPage/components/LogsTableRow.tsx @@ -83,7 +83,7 @@ export const LogsTableRow = ({ auditLog, isOrgAuditLogs, showActorColumn }: Prop hours %= 12; hours = hours || 12; // the hour '0' should be '12' - const formattedDate = `${day}-${month}-${year} at ${hours}:${minutes} ${period}`; + const formattedDate = `${month}-${day}-${year} at ${hours}:${minutes} ${period}`; return formattedDate; }; From ace2500885c0e1544a14f371b05eb1f8f86d1d4e Mon Sep 17 00:00:00 2001 From: Mahyar Mirrashed Date: Mon, 10 Mar 2025 14:29:01 -0700 Subject: [PATCH 27/71] feat(audit): add timestamp format to column header --- .../pages/organization/AuditLogsPage/components/LogsTable.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/src/pages/organization/AuditLogsPage/components/LogsTable.tsx b/frontend/src/pages/organization/AuditLogsPage/components/LogsTable.tsx index f37482548..a931cb606 100644 --- a/frontend/src/pages/organization/AuditLogsPage/components/LogsTable.tsx +++ b/frontend/src/pages/organization/AuditLogsPage/components/LogsTable.tsx @@ -28,7 +28,7 @@ type Props = { const AUDIT_LOG_LIMIT = 15; -const TABLE_HEADERS = ["Timestamp", "Event", "Project", "Actor", "Source", "Metadata"] as const; +const TABLE_HEADERS = ["Timestamp (MM/DD/YYYY)", "Event", "Project", "Actor", "Source", "Metadata"] as const; export type TAuditLogTableHeader = (typeof TABLE_HEADERS)[number]; export const LogsTable = ({ From b9b76579ac14a3b63cdb2b0e0bb255d138073f61 Mon Sep 17 00:00:00 2001 From: Daniel Hougaard Date: Tue, 11 Mar 2025 02:07:38 +0400 Subject: [PATCH 28/71] requested changes --- .../src/ee/routes/v1/user-additional-privilege-router.ts | 8 ++++++-- .../v2/identity-project-additional-privilege-router.ts | 8 ++++++-- backend/src/ee/routes/v2/project-role-router.ts | 4 +++- backend/src/ee/services/permission/permission-fns.ts | 5 ++++- 4 files changed, 19 insertions(+), 6 deletions(-) diff --git a/backend/src/ee/routes/v1/user-additional-privilege-router.ts b/backend/src/ee/routes/v1/user-additional-privilege-router.ts index de37a4cde..f8b4e922c 100644 --- a/backend/src/ee/routes/v1/user-additional-privilege-router.ts +++ b/backend/src/ee/routes/v1/user-additional-privilege-router.ts @@ -2,6 +2,7 @@ import slugify from "@sindresorhus/slugify"; import ms from "ms"; import { z } from "zod"; +import { checkForInvalidPermissionCombination } from "@app/ee/services/permission/permission-fns"; import { ProjectPermissionV2Schema } from "@app/ee/services/permission/project-permission"; import { ProjectUserAdditionalPrivilegeTemporaryMode } from "@app/ee/services/project-user-additional-privilege/project-user-additional-privilege-types"; import { PROJECT_USER_ADDITIONAL_PRIVILEGE } from "@app/lib/api-docs"; @@ -23,7 +24,9 @@ export const registerUserAdditionalPrivilegeRouter = async (server: FastifyZodPr body: z.object({ projectMembershipId: z.string().min(1).describe(PROJECT_USER_ADDITIONAL_PRIVILEGE.CREATE.projectMembershipId), slug: slugSchema({ min: 1, max: 60 }).optional().describe(PROJECT_USER_ADDITIONAL_PRIVILEGE.CREATE.slug), - permissions: ProjectPermissionV2Schema.array().describe(PROJECT_USER_ADDITIONAL_PRIVILEGE.CREATE.permissions), + permissions: ProjectPermissionV2Schema.array() + .describe(PROJECT_USER_ADDITIONAL_PRIVILEGE.CREATE.permissions) + .refine(checkForInvalidPermissionCombination), type: z.discriminatedUnion("isTemporary", [ z.object({ isTemporary: z.literal(false) @@ -81,7 +84,8 @@ export const registerUserAdditionalPrivilegeRouter = async (server: FastifyZodPr slug: slugSchema({ min: 1, max: 60 }).describe(PROJECT_USER_ADDITIONAL_PRIVILEGE.UPDATE.slug), permissions: ProjectPermissionV2Schema.array() .optional() - .describe(PROJECT_USER_ADDITIONAL_PRIVILEGE.UPDATE.permissions), + .describe(PROJECT_USER_ADDITIONAL_PRIVILEGE.UPDATE.permissions) + .refine(checkForInvalidPermissionCombination), type: z.discriminatedUnion("isTemporary", [ z.object({ isTemporary: z.literal(false).describe(PROJECT_USER_ADDITIONAL_PRIVILEGE.UPDATE.isTemporary) }), z.object({ diff --git a/backend/src/ee/routes/v2/identity-project-additional-privilege-router.ts b/backend/src/ee/routes/v2/identity-project-additional-privilege-router.ts index d9c3a05b5..7cc9155cd 100644 --- a/backend/src/ee/routes/v2/identity-project-additional-privilege-router.ts +++ b/backend/src/ee/routes/v2/identity-project-additional-privilege-router.ts @@ -3,6 +3,7 @@ import ms from "ms"; import { z } from "zod"; import { IdentityProjectAdditionalPrivilegeTemporaryMode } from "@app/ee/services/identity-project-additional-privilege-v2/identity-project-additional-privilege-v2-types"; +import { checkForInvalidPermissionCombination } from "@app/ee/services/permission/permission-fns"; import { ProjectPermissionV2Schema } from "@app/ee/services/permission/project-permission"; import { IDENTITY_ADDITIONAL_PRIVILEGE_V2 } from "@app/lib/api-docs"; import { alphaNumericNanoId } from "@app/lib/nanoid"; @@ -30,7 +31,9 @@ export const registerIdentityProjectAdditionalPrivilegeRouter = async (server: F identityId: z.string().min(1).describe(IDENTITY_ADDITIONAL_PRIVILEGE_V2.CREATE.identityId), projectId: z.string().min(1).describe(IDENTITY_ADDITIONAL_PRIVILEGE_V2.CREATE.projectId), slug: slugSchema({ min: 1, max: 60 }).optional().describe(IDENTITY_ADDITIONAL_PRIVILEGE_V2.CREATE.slug), - permissions: ProjectPermissionV2Schema.array().describe(IDENTITY_ADDITIONAL_PRIVILEGE_V2.CREATE.permission), + permissions: ProjectPermissionV2Schema.array() + .describe(IDENTITY_ADDITIONAL_PRIVILEGE_V2.CREATE.permission) + .refine(checkForInvalidPermissionCombination), type: z.discriminatedUnion("isTemporary", [ z.object({ isTemporary: z.literal(false) @@ -94,7 +97,8 @@ export const registerIdentityProjectAdditionalPrivilegeRouter = async (server: F slug: slugSchema({ min: 1, max: 60 }).describe(IDENTITY_ADDITIONAL_PRIVILEGE_V2.UPDATE.slug), permissions: ProjectPermissionV2Schema.array() .optional() - .describe(IDENTITY_ADDITIONAL_PRIVILEGE_V2.UPDATE.privilegePermission), + .describe(IDENTITY_ADDITIONAL_PRIVILEGE_V2.UPDATE.privilegePermission) + .refine(checkForInvalidPermissionCombination), type: z.discriminatedUnion("isTemporary", [ z.object({ isTemporary: z.literal(false).describe(IDENTITY_ADDITIONAL_PRIVILEGE_V2.UPDATE.isTemporary) }), z.object({ diff --git a/backend/src/ee/routes/v2/project-role-router.ts b/backend/src/ee/routes/v2/project-role-router.ts index c3b3c11f2..0bb83b8d4 100644 --- a/backend/src/ee/routes/v2/project-role-router.ts +++ b/backend/src/ee/routes/v2/project-role-router.ts @@ -38,7 +38,9 @@ export const registerProjectRoleRouter = async (server: FastifyZodProvider) => { .describe(PROJECT_ROLE.CREATE.slug), name: z.string().min(1).trim().describe(PROJECT_ROLE.CREATE.name), description: z.string().trim().nullish().describe(PROJECT_ROLE.CREATE.description), - permissions: ProjectPermissionV2Schema.array().describe(PROJECT_ROLE.CREATE.permissions) + permissions: ProjectPermissionV2Schema.array() + .describe(PROJECT_ROLE.CREATE.permissions) + .refine(checkForInvalidPermissionCombination) }), response: { 200: z.object({ diff --git a/backend/src/ee/services/permission/permission-fns.ts b/backend/src/ee/services/permission/permission-fns.ts index ecc8b676e..9397df939 100644 --- a/backend/src/ee/services/permission/permission-fns.ts +++ b/backend/src/ee/services/permission/permission-fns.ts @@ -81,7 +81,8 @@ export function checkForInvalidPermissionCombination(permissions: z.infer Date: Mon, 10 Mar 2025 16:26:18 -0700 Subject: [PATCH 29/71] chore(nix): add support for flakes --- .envrc | 1 + flake.lock | 27 +++++++++++++++++++++++++++ flake.nix | 35 +++++++++++++++++++++++++++++++++++ 3 files changed, 63 insertions(+) create mode 100644 .envrc create mode 100644 flake.lock create mode 100644 flake.nix diff --git a/.envrc b/.envrc new file mode 100644 index 000000000..3550a30f2 --- /dev/null +++ b/.envrc @@ -0,0 +1 @@ +use flake diff --git a/flake.lock b/flake.lock new file mode 100644 index 000000000..3104f340d --- /dev/null +++ b/flake.lock @@ -0,0 +1,27 @@ +{ + "nodes": { + "nixpkgs": { + "locked": { + "lastModified": 1741445498, + "narHash": "sha256-F5Em0iv/CxkN5mZ9hRn3vPknpoWdcdCyR0e4WklHwiE=", + "owner": "NixOS", + "repo": "nixpkgs", + "rev": "52e3095f6d812b91b22fb7ad0bfc1ab416453634", + "type": "github" + }, + "original": { + "owner": "NixOS", + "ref": "nixos-24.11", + "repo": "nixpkgs", + "type": "github" + } + }, + "root": { + "inputs": { + "nixpkgs": "nixpkgs" + } + } + }, + "root": "root", + "version": 7 +} diff --git a/flake.nix b/flake.nix new file mode 100644 index 000000000..ffed5545c --- /dev/null +++ b/flake.nix @@ -0,0 +1,35 @@ +{ + description = "Flake for github:Infisical/infisical repository."; + + inputs = { + nixpkgs.url = "github:NixOS/nixpkgs/nixos-24.11"; + }; + + outputs = { self, nixpkgs }: { + devShells.aarch64-darwin.default = let + pkgs = nixpkgs.legacyPackages.aarch64-darwin; + in + pkgs.mkShell { + packages = with pkgs; [ + git + lazygit + + nodejs_20 + ]; + + shellHook = '' + # Define a writable directory for global npm packages + export NPM_CONFIG_PREFIX="$HOME/.npm-global" + export PATH="$NPM_CONFIG_PREFIX/bin:$PATH" + + # Ensure the directory exists + mkdir -p "$NPM_CONFIG_PREFIX" + + # Install Infisical CLI only if it's not already installed + if ! command -v infisical &>/dev/null; then + npm install -g @infisical/cli + fi + ''; + }; + }; +} From 17cf602a65b3c21a5fa6f9e05a22a7ab6642364f Mon Sep 17 00:00:00 2001 From: Mahyar Mirrashed Date: Mon, 10 Mar 2025 16:26:39 -0700 Subject: [PATCH 30/71] style: remove blank line --- backend/src/server/routes/v1/index.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/backend/src/server/routes/v1/index.ts b/backend/src/server/routes/v1/index.ts index 087bd9afd..50fd33840 100644 --- a/backend/src/server/routes/v1/index.ts +++ b/backend/src/server/routes/v1/index.ts @@ -91,7 +91,6 @@ export const registerV1Routes = async (server: FastifyZodProvider) => { await projectRouter.register(registerProjectMembershipRouter); await projectRouter.register(registerSecretTagRouter); }, - { prefix: "/workspace" } ); From 483fb458dd7681af59f656d714ca8452ae93c9c3 Mon Sep 17 00:00:00 2001 From: Daniel Hougaard Date: Tue, 11 Mar 2025 04:52:12 +0400 Subject: [PATCH 31/71] requested changes --- .../ee/services/permission/permission-fns.ts | 12 +- .../src/server/routes/v1/dashboard-router.ts | 45 ++++ .../secret-v2-bridge-service.ts | 90 +++++++- .../secret-v2-bridge-types.ts | 7 + backend/src/services/secret/secret-service.ts | 37 ++- backend/src/services/secret/secret-types.ts | 7 + frontend/src/hooks/api/dashboard/index.ts | 1 + frontend/src/hooks/api/dashboard/queries.tsx | 57 ++++- frontend/src/hooks/api/dashboard/types.ts | 10 + .../SecretOverviewTableRow/SecretEditRow.tsx | 89 +++---- .../SecretDropzone/CopySecretsFromBoard.tsx | 60 ++--- .../SecretListView/SecretDetailSidebar.tsx | 217 +++++++++--------- 12 files changed, 441 insertions(+), 191 deletions(-) diff --git a/backend/src/ee/services/permission/permission-fns.ts b/backend/src/ee/services/permission/permission-fns.ts index 9397df939..cccadb86e 100644 --- a/backend/src/ee/services/permission/permission-fns.ts +++ b/backend/src/ee/services/permission/permission-fns.ts @@ -23,12 +23,6 @@ export function throwIfMissingSecretReadValueOrDescribePermission( subjectFields?: SecretSubjectFields ) { try { - if (subjectFields) { - ForbiddenError.from(permission).throwUnlessCan(action, subject(ProjectPermissionSub.Secrets, subjectFields)); - } else { - ForbiddenError.from(permission).throwUnlessCan(action, ProjectPermissionSub.Secrets); - } - } catch { if (subjectFields) { ForbiddenError.from(permission).throwUnlessCan( ProjectPermissionSecretActions.DescribeAndReadValue, @@ -40,6 +34,12 @@ export function throwIfMissingSecretReadValueOrDescribePermission( ProjectPermissionSub.Secrets ); } + } catch { + if (subjectFields) { + ForbiddenError.from(permission).throwUnlessCan(action, subject(ProjectPermissionSub.Secrets, subjectFields)); + } else { + ForbiddenError.from(permission).throwUnlessCan(action, ProjectPermissionSub.Secrets); + } } } diff --git a/backend/src/server/routes/v1/dashboard-router.ts b/backend/src/server/routes/v1/dashboard-router.ts index 27ba3f8a3..392fcf5d9 100644 --- a/backend/src/server/routes/v1/dashboard-router.ts +++ b/backend/src/server/routes/v1/dashboard-router.ts @@ -834,6 +834,51 @@ export const registerDashboardRouter = async (server: FastifyZodProvider) => { } }); + server.route({ + method: "GET", + url: "/accessible-secrets", + config: { + rateLimit: secretsLimit + }, + schema: { + querystring: z.object({ + projectId: z.string().trim(), + environment: z.string().trim(), + secretPath: z.string().trim().default("/").transform(removeTrailingSlash), + filterByAction: z + .enum([ProjectPermissionSecretActions.DescribeSecret, ProjectPermissionSecretActions.ReadValue]) + .default(ProjectPermissionSecretActions.ReadValue) + }), + response: { + 200: z.object({ + secrets: secretRawSchema + .extend({ + secretPath: z.string().optional() + }) + .array() + .optional() + }) + } + }, + onRequest: verifyAuth([AuthMode.JWT]), + handler: async (req) => { + const { projectId, environment, secretPath, filterByAction } = req.query; + + const { secrets } = await server.services.secret.getAccessibleSecrets({ + actorId: req.permission.id, + actor: req.permission.type, + actorAuthMethod: req.permission.authMethod, + actorOrgId: req.permission.orgId, + environment, + secretPath, + projectId, + filterByAction + }); + + return { secrets }; + } + }); + server.route({ method: "GET", url: "/secrets-by-keys", diff --git a/backend/src/services/secret-v2-bridge/secret-v2-bridge-service.ts b/backend/src/services/secret-v2-bridge/secret-v2-bridge-service.ts index 45c6493d6..c66fdddf1 100644 --- a/backend/src/services/secret-v2-bridge/secret-v2-bridge-service.ts +++ b/backend/src/services/secret-v2-bridge/secret-v2-bridge-service.ts @@ -60,6 +60,7 @@ import { TCreateSecretDTO, TDeleteManySecretDTO, TDeleteSecretDTO, + TGetAccessibleSecretsDTO, TGetASecretDTO, TGetSecretReferencesTreeDTO, TGetSecretsDTO, @@ -200,7 +201,7 @@ export const secretV2BridgeServiceFactory = ({ const referredSecretsGroupBySecretKey = groupBy(referredSecrets, (i) => i.key); references.forEach((el) => { - throwIfMissingSecretReadValueOrDescribePermission(permission, ProjectPermissionSecretActions.ReadValue, { + throwIfMissingSecretReadValueOrDescribePermission(permission, ProjectPermissionSecretActions.DescribeSecret, { environment: el.environment, secretPath: el.secretPath, secretName: el.secretKey, @@ -1291,7 +1292,7 @@ export const secretV2BridgeServiceFactory = ({ folderDAL, secretImportDAL, decryptor: (value) => (value ? secretManagerDecryptor({ cipherTextBlob: value }).toString() : ""), - expandSecretReferences: shouldExpandSecretReferences ? expandSecretReferences : undefined, + expandSecretReferences: shouldExpandSecretReferences && viewSecretValue ? expandSecretReferences : undefined, hasSecretAccess: (expandEnvironment, expandSecretPath, expandSecretKey, expandSecretTags) => { return hasSecretReadValueOrDescribePermission(permission, ProjectPermissionSecretActions.DescribeSecret, { environment: expandEnvironment, @@ -1347,7 +1348,7 @@ export const secretV2BridgeServiceFactory = ({ let secretValue = secret.encryptedValue ? secretManagerDecryptor({ cipherTextBlob: secret.encryptedValue }).toString() : ""; - if (shouldExpandSecretReferences && secretValue) { + if (shouldExpandSecretReferences && secretValue && viewSecretValue) { // eslint-disable-next-line const expandedSecretValue = await expandSecretReferences({ environment, @@ -2613,6 +2614,86 @@ export const secretV2BridgeServiceFactory = ({ return { tree: stackTrace, value: expandedValue }; }; + const getAccessibleSecrets = async ({ + projectId, + secretPath, + environment, + filterByAction, + actorId, + actor, + actorAuthMethod, + actorOrgId + }: TGetAccessibleSecretsDTO) => { + const { permission } = await permissionService.getProjectPermission({ + actor, + actorId, + projectId, + actorAuthMethod, + actorOrgId, + actionProjectType: ActionProjectType.SecretManager + }); + throwIfMissingSecretReadValueOrDescribePermission(permission, ProjectPermissionSecretActions.DescribeSecret, { + environment, + secretPath + }); + + const folder = await folderDAL.findBySecretPath(projectId, environment, secretPath); + if (!folder) return { secrets: [] }; + + const secrets = await secretDAL.findByFolderIds([folder.id]); + + const { decryptor: secretManagerDecryptor } = await kmsService.createCipherPairWithDataKey({ + type: KmsDataKey.SecretManager, + projectId + }); + + const decryptedSecrets = secrets + .filter((el) => { + if ( + !hasSecretReadValueOrDescribePermission(permission, ProjectPermissionSecretActions.DescribeSecret, { + environment, + secretPath, + secretName: el.key, + secretTags: el.tags.map((i) => i.slug) + }) + ) { + return false; + } + + if (filterByAction === ProjectPermissionSecretActions.ReadValue) { + return hasSecretReadValueOrDescribePermission(permission, ProjectPermissionSecretActions.ReadValue, { + environment, + secretPath, + secretName: el.key, + secretTags: el.tags.map((i) => i.slug) + }); + } + + return true; + }) + .map((secret) => { + return reshapeBridgeSecret( + projectId, + environment, + secretPath, + { + ...secret, + value: secret.encryptedValue + ? secretManagerDecryptor({ cipherTextBlob: secret.encryptedValue }).toString() + : "", + comment: secret.encryptedComment + ? secretManagerDecryptor({ cipherTextBlob: secret.encryptedComment }).toString() + : "" + }, + false + ); + }); + + return { + secrets: decryptedSecrets + }; + }; + return { createSecret, deleteSecret, @@ -2630,6 +2711,7 @@ export const secretV2BridgeServiceFactory = ({ getSecretsMultiEnv, getSecretReferenceTree, getSecretsByFolderMappings, - getSecretById + getSecretById, + getAccessibleSecrets }; }; diff --git a/backend/src/services/secret-v2-bridge/secret-v2-bridge-types.ts b/backend/src/services/secret-v2-bridge/secret-v2-bridge-types.ts index 473d1ed83..7f415bff8 100644 --- a/backend/src/services/secret-v2-bridge/secret-v2-bridge-types.ts +++ b/backend/src/services/secret-v2-bridge/secret-v2-bridge-types.ts @@ -351,3 +351,10 @@ export type TGetSecretsRawByFolderMappingsDTO = { filters: TFindSecretsByFolderIdsFilter; filterByAction?: ProjectPermissionSecretActions.DescribeSecret | ProjectPermissionSecretActions.ReadValue; }; + +export type TGetAccessibleSecretsDTO = { + environment: string; + projectId: string; + secretPath: string; + filterByAction: ProjectPermissionSecretActions.DescribeSecret | ProjectPermissionSecretActions.ReadValue; +} & TProjectPermission; diff --git a/backend/src/services/secret/secret-service.ts b/backend/src/services/secret/secret-service.ts index 3593adf88..13a5c9736 100644 --- a/backend/src/services/secret/secret-service.ts +++ b/backend/src/services/secret/secret-service.ts @@ -81,6 +81,7 @@ import { TDeleteManySecretRawDTO, TDeleteSecretDTO, TDeleteSecretRawDTO, + TGetAccessibleSecretsDTO, TGetASecretByIdRawDTO, TGetASecretDTO, TGetASecretRawDTO, @@ -1312,6 +1313,39 @@ export const secretServiceFactory = ({ return { users: usersWithAccess, identities: identitiesWithAccess, groups: groupsWithAccess }; }; + const getAccessibleSecrets = async ({ + projectId, + secretPath, + actor, + actorId, + actorOrgId, + actorAuthMethod, + environment, + filterByAction + }: TGetAccessibleSecretsDTO) => { + const { shouldUseSecretV2Bridge } = await projectBotService.getBotKey(projectId); + + if (!shouldUseSecretV2Bridge) { + throw new BadRequestError({ + message: "Project version does not support this endpoint.", + name: "ProjectVersionNotSupported" + }); + } + + const secrets = await secretV2BridgeService.getAccessibleSecrets({ + projectId, + secretPath, + environment, + filterByAction, + actor, + actorId, + actorOrgId, + actorAuthMethod + }); + + return secrets; + }; + const getSecretsRaw = async ({ projectId, path, @@ -3261,6 +3295,7 @@ export const secretServiceFactory = ({ getSecretReferenceTree, getSecretsRawByFolderMappings, getSecretAccessList, - getSecretByIdRaw + getSecretByIdRaw, + getAccessibleSecrets }; }; diff --git a/backend/src/services/secret/secret-types.ts b/backend/src/services/secret/secret-types.ts index c7b8a067d..f42f70b78 100644 --- a/backend/src/services/secret/secret-types.ts +++ b/backend/src/services/secret/secret-types.ts @@ -20,6 +20,7 @@ import { TSecretV2BridgeDALFactory } from "../secret-v2-bridge/secret-v2-bridge- import { SecretUpdateMode } from "../secret-v2-bridge/secret-v2-bridge-types"; import { TSecretVersionV2DALFactory } from "../secret-v2-bridge/secret-version-dal"; import { TSecretVersionV2TagDALFactory } from "../secret-v2-bridge/secret-version-tag-dal"; +import { ProjectPermissionSecretActions } from "@app/ee/services/permission/project-permission"; type TPartialSecret = Pick; @@ -180,6 +181,12 @@ export enum SecretsOrderBy { Name = "name" // "key" for secrets but using name for use across resources } +export type TGetAccessibleSecretsDTO = { + secretPath: string; + environment: string; + filterByAction: ProjectPermissionSecretActions.DescribeSecret | ProjectPermissionSecretActions.ReadValue; +} & TProjectPermission; + export type TGetSecretsRawDTO = { expandSecretReferences?: boolean; path: string; diff --git a/frontend/src/hooks/api/dashboard/index.ts b/frontend/src/hooks/api/dashboard/index.ts index 83206bdf8..bbd70e306 100644 --- a/frontend/src/hooks/api/dashboard/index.ts +++ b/frontend/src/hooks/api/dashboard/index.ts @@ -1,4 +1,5 @@ export { + useGetAccessibleSecrets, useGetProjectSecretsDetails, useGetProjectSecretsOverview, useGetProjectSecretsQuickSearch diff --git a/frontend/src/hooks/api/dashboard/queries.tsx b/frontend/src/hooks/api/dashboard/queries.tsx index f704081a9..049238202 100644 --- a/frontend/src/hooks/api/dashboard/queries.tsx +++ b/frontend/src/hooks/api/dashboard/queries.tsx @@ -11,6 +11,7 @@ import { DashboardSecretsOrderBy, TDashboardProjectSecretsQuickSearch, TDashboardProjectSecretsQuickSearchResponse, + TGetAccessibleSecretsDTO, TGetDashboardProjectSecretsByKeys, TGetDashboardProjectSecretsDetailsDTO, TGetDashboardProjectSecretsOverviewDTO, @@ -20,6 +21,8 @@ import { OrderByDirection } from "@app/hooks/api/generic/types"; import { mergePersonalSecrets } from "@app/hooks/api/secrets/queries"; import { groupBy, unique } from "@app/lib/fn/array"; +import { SecretV3Raw } from "../types"; + export const dashboardKeys = { all: () => ["dashboard"] as const, getDashboardSecrets: ({ @@ -58,7 +61,14 @@ export const dashboardKeys = { ...dashboardKeys.getDashboardSecrets({ projectId, secretPath }), "quick-search", params - ] as const + ] as const, + getAccessibleSecrets: ({ + projectId, + secretPath, + environment, + filterByAction + }: TGetAccessibleSecretsDTO) => + [...dashboardKeys.all(), { projectId, secretPath, environment, filterByAction }] as const }; export const fetchProjectSecretsOverview = async ({ @@ -295,6 +305,22 @@ export const fetchProjectSecretsQuickSearch = async ({ return data; }; +const fetchAccessibleSecrets = async ({ + projectId, + secretPath, + environment, + filterByAction +}: TGetAccessibleSecretsDTO) => { + const { data } = await apiRequest.get<{ secrets: SecretV3Raw[] }>( + "/api/v1/dashboard/accessible-secrets", + { + params: { projectId, secretPath, environment, filterByAction } + } + ); + + return data.secrets; +}; + export const useGetProjectSecretsQuickSearch = ( { projectId, @@ -357,3 +383,32 @@ export const useGetProjectSecretsQuickSearch = ( placeholderData: (previousData) => previousData }); }; + +export const useGetAccessibleSecrets = ({ + projectId, + secretPath, + environment, + filterByAction, + options +}: TGetAccessibleSecretsDTO & { + options?: Omit< + UseQueryOptions< + SecretV3Raw[], + unknown, + SecretV3Raw[], + ReturnType + >, + "queryKey" | "queryFn" + >; +}) => { + return useQuery({ + ...options, + queryKey: dashboardKeys.getAccessibleSecrets({ + projectId, + secretPath, + environment, + filterByAction + }), + queryFn: () => fetchAccessibleSecrets({ projectId, secretPath, environment, filterByAction }) + }); +}; diff --git a/frontend/src/hooks/api/dashboard/types.ts b/frontend/src/hooks/api/dashboard/types.ts index 4808b463c..9540c4ae6 100644 --- a/frontend/src/hooks/api/dashboard/types.ts +++ b/frontend/src/hooks/api/dashboard/types.ts @@ -1,3 +1,4 @@ +import { ProjectPermissionSecretActions } from "@app/context/ProjectPermissionContext/types"; import { TDynamicSecret } from "@app/hooks/api/dynamicSecret/types"; import { OrderByDirection } from "@app/hooks/api/generic/types"; import { TSecretFolder } from "@app/hooks/api/secretFolders/types"; @@ -101,3 +102,12 @@ export type TGetDashboardProjectSecretsByKeys = { environment: string; keys: string[]; }; + +export type TGetAccessibleSecretsDTO = { + projectId: string; + secretPath: string; + environment: string; + filterByAction: + | ProjectPermissionSecretActions.DescribeSecret + | ProjectPermissionSecretActions.ReadValue; +}; diff --git a/frontend/src/pages/secret-manager/OverviewPage/components/SecretOverviewTableRow/SecretEditRow.tsx b/frontend/src/pages/secret-manager/OverviewPage/components/SecretOverviewTableRow/SecretEditRow.tsx index 4887d4fd7..705d7e177 100644 --- a/frontend/src/pages/secret-manager/OverviewPage/components/SecretOverviewTableRow/SecretEditRow.tsx +++ b/frontend/src/pages/secret-manager/OverviewPage/components/SecretOverviewTableRow/SecretEditRow.tsx @@ -27,9 +27,11 @@ import { } from "@app/components/v2"; import { Blur } from "@app/components/v2/Blur"; import { InfisicalSecretInput } from "@app/components/v2/InfisicalSecretInput"; -import { ProjectPermissionActions, ProjectPermissionSub } from "@app/context"; +import { ProjectPermissionActions, ProjectPermissionSub, useProjectPermission } from "@app/context"; +import { ProjectPermissionSecretActions } from "@app/context/ProjectPermissionContext/types"; import { useToggle } from "@app/hooks"; import { SecretType } from "@app/hooks/api/types"; +import { hasSecretReadValueOrDescribePermission } from "@app/lib/fn/permission"; type Props = { defaultValue?: string | null; @@ -79,6 +81,9 @@ export const SecretEditRow = ({ value: defaultValue || null } }); + + const { permission } = useProjectPermission(); + const [isDeleting, setIsDeleting] = useToggle(); const [isModalOpen, setIsModalOpen] = useState(false); @@ -120,6 +125,11 @@ export const SecretEditRow = ({ reset({ value }); }; + const canReadSecretValue = hasSecretReadValueOrDescribePermission( + permission, + ProjectPermissionSecretActions.ReadValue + ); + const handleDeleteSecret = useCallback(async () => { setIsDeleting.on(); setIsModalOpen(false); @@ -229,48 +239,43 @@ export const SecretEditRow = ({
- - {(isAllowed) => ( -
- - -
- - - - - -
-
- e.preventDefault()} // prevents secret input from displaying value on open + +
+ + +
+ - - - -
- )} - + + + + +
+ + e.preventDefault()} // prevents secret input from displaying value on open + > + + +
+
+ 1 ? val.slice(0, -1) : val ), secrets: z - .object({ key: z.string(), value: z.string().optional() }) + .object({ secretKey: z.string(), secretValue: z.string().optional() }) .array() .min(1, "Select one or more secrets to copy") }); @@ -78,34 +79,38 @@ export const CopySecretsFromBoard = ({ const selectedEnvSlug = watch("environment"); const [debouncedEnvCopySecretPath] = useDebounce(envCopySecPath); - const { data: secrets, isPending: isSecretsLoading } = useGetProjectSecrets({ - workspaceId, - environment: selectedEnvSlug.slug, - secretPath: debouncedEnvCopySecretPath, - options: { - enabled: - Boolean(workspaceId) && - Boolean(selectedEnvSlug) && - Boolean(debouncedEnvCopySecretPath) && - isOpen - } - }); + const { data: accessibleSecrets, isPending: isAccessibleSecretsLoading } = + useGetAccessibleSecrets({ + projectId: workspaceId, + secretPath: debouncedEnvCopySecretPath, + environment: selectedEnvSlug.slug, + filterByAction: shouldIncludeValues + ? ProjectPermissionSecretActions.ReadValue + : ProjectPermissionSecretActions.DescribeSecret, + options: { + enabled: + Boolean(workspaceId) && + Boolean(selectedEnvSlug) && + Boolean(debouncedEnvCopySecretPath) && + isOpen + } + }); useEffect(() => { setValue("secrets", []); }, [debouncedEnvCopySecretPath, selectedEnvSlug]); const handleSecSelectAll = () => { - if (secrets) { - setValue("secrets", secrets, { shouldDirty: true }); + if (accessibleSecrets) { + setValue("secrets", accessibleSecrets, { shouldDirty: true }); } }; const handleFormSubmit = async (data: TFormSchema) => { const secretsToBePulled: Record = {}; - data.secrets.forEach(({ key, value }) => { - secretsToBePulled[key] = { - value: (shouldIncludeValues && value) || "", + data.secrets.forEach(({ secretKey, secretValue }) => { + secretsToBePulled[secretKey] = { + value: (shouldIncludeValues && secretValue) || "", comments: [""] }; }); @@ -202,19 +207,19 @@ export const CopySecretsFromBoard = ({ option.key} - getOptionLabel={(option) => option.key} + getOptionValue={(option) => option.secretKey} + getOptionLabel={(option) => option.secretKey} /> )} @@ -235,7 +240,10 @@ export const CopySecretsFromBoard = ({ setShouldIncludeValues(isChecked as boolean)} + onCheckedChange={(isChecked) => { + setValue("secrets", []); + setShouldIncludeValues(isChecked as boolean); + }} > Include secret values diff --git a/frontend/src/pages/secret-manager/SecretDashboardPage/components/SecretListView/SecretDetailSidebar.tsx b/frontend/src/pages/secret-manager/SecretDashboardPage/components/SecretListView/SecretDetailSidebar.tsx index 410d49e5f..641a4a723 100644 --- a/frontend/src/pages/secret-manager/SecretDashboardPage/components/SecretListView/SecretDetailSidebar.tsx +++ b/frontend/src/pages/secret-manager/SecretDashboardPage/components/SecretListView/SecretDetailSidebar.tsx @@ -13,8 +13,8 @@ import { faShare, faTag, faTrash, - faUser, - faTriangleExclamation + faTriangleExclamation, + faUser } from "@fortawesome/free-solid-svg-icons"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { zodResolver } from "@hookform/resolvers/zod"; @@ -728,8 +728,8 @@ export const SecretDetailSidebar = ({
Version History
{secretVersion?.map( - ({ createdAt, secretValue, version, id, secretValueHidden, actor }, index) => ( -
+ ({ createdAt, secretValue, secretValueHidden, version, id, actor }) => ( +
@@ -739,72 +739,50 @@ export const SecretDetailSidebar = ({
{format(new Date(createdAt), "Pp")}
-
{format(new Date(createdAt), "Pp")}
-
-
-
-
-
-
- {actor && ( -
-
- Modified by: - - {/* eslint-disable-next-line jsx-a11y/click-events-have-key-events, jsx-a11y/no-static-element-interactions */} -
- onModifyHistoryClick( - actor.actorId, - actor.actorType, - actor.membershipId - ) - } - className="cursor-pointer" +
+
+
+
+
+ {actor && ( +
+
+ Modified by: + - -
- + {/* eslint-disable-next-line jsx-a11y/click-events-have-key-events, jsx-a11y/no-static-element-interactions */} +
+ onModifyHistoryClick( + actor.actorId, + actor.actorType, + actor.membershipId + ) + } + className="cursor-pointer" + > + +
+ +
-
- )} -
-
- Value: -
-
-
- - + -
- - {secretValueHidden ? "******" : secretValue?.replace(/./g, "*")} - +
+ + {secretValueHidden ? "******" : secretValue?.replace(/./g, "*")} + - + }} + onKeyDown={(e) => { + if (e.key === "Enter" || e.key === " ") { + e.currentTarget + .closest(".group") + ?.classList.add("show-value"); + } + }} + > + + + +
From 497761a0e5d24c47face9c416caaaa2d86fe6a56 Mon Sep 17 00:00:00 2001 From: Daniel Hougaard Date: Tue, 11 Mar 2025 05:44:28 +0400 Subject: [PATCH 32/71] fix: missing permision check --- backend/src/server/routes/v1/dashboard-router.ts | 3 ++- .../secret-v2-bridge/secret-v2-bridge-service.ts | 11 ++++++++++- 2 files changed, 12 insertions(+), 2 deletions(-) diff --git a/backend/src/server/routes/v1/dashboard-router.ts b/backend/src/server/routes/v1/dashboard-router.ts index 392fcf5d9..db61594a2 100644 --- a/backend/src/server/routes/v1/dashboard-router.ts +++ b/backend/src/server/routes/v1/dashboard-router.ts @@ -853,7 +853,8 @@ export const registerDashboardRouter = async (server: FastifyZodProvider) => { 200: z.object({ secrets: secretRawSchema .extend({ - secretPath: z.string().optional() + secretPath: z.string().optional(), + secretValueHidden: z.boolean() }) .array() .optional() diff --git a/backend/src/services/secret-v2-bridge/secret-v2-bridge-service.ts b/backend/src/services/secret-v2-bridge/secret-v2-bridge-service.ts index c66fdddf1..09b3a50cb 100644 --- a/backend/src/services/secret-v2-bridge/secret-v2-bridge-service.ts +++ b/backend/src/services/secret-v2-bridge/secret-v2-bridge-service.ts @@ -2672,6 +2672,15 @@ export const secretV2BridgeServiceFactory = ({ return true; }) .map((secret) => { + const secretValueHidden = + filterByAction === ProjectPermissionSecretActions.DescribeSecret && + !hasSecretReadValueOrDescribePermission(permission, ProjectPermissionSecretActions.ReadValue, { + environment, + secretPath, + secretName: secret.key, + secretTags: secret.tags.map((i) => i.slug) + }); + return reshapeBridgeSecret( projectId, environment, @@ -2685,7 +2694,7 @@ export const secretV2BridgeServiceFactory = ({ ? secretManagerDecryptor({ cipherTextBlob: secret.encryptedComment }).toString() : "" }, - false + secretValueHidden ); }); From ad1dd55b8bf1cbff714132be1e9e2376df0dee18 Mon Sep 17 00:00:00 2001 From: Daniel Hougaard Date: Tue, 11 Mar 2025 06:01:21 +0400 Subject: [PATCH 33/71] chore: requested changes --- frontend/src/hooks/api/dashboard/queries.tsx | 6 +++++- frontend/src/hooks/api/reactQuery.tsx | 6 +----- frontend/src/lib/fn/string.ts | 4 ++++ .../SecretListView/SecretDetailSidebar.tsx | 12 +++++------- 4 files changed, 15 insertions(+), 13 deletions(-) diff --git a/frontend/src/hooks/api/dashboard/queries.tsx b/frontend/src/hooks/api/dashboard/queries.tsx index 049238202..1fb456325 100644 --- a/frontend/src/hooks/api/dashboard/queries.tsx +++ b/frontend/src/hooks/api/dashboard/queries.tsx @@ -68,7 +68,11 @@ export const dashboardKeys = { environment, filterByAction }: TGetAccessibleSecretsDTO) => - [...dashboardKeys.all(), { projectId, secretPath, environment, filterByAction }] as const + [ + ...dashboardKeys.all(), + "accessible-secrets", + { projectId, secretPath, environment, filterByAction } + ] as const }; export const fetchProjectSecretsOverview = async ({ diff --git a/frontend/src/hooks/api/reactQuery.tsx b/frontend/src/hooks/api/reactQuery.tsx index be9dadbec..cd8622976 100644 --- a/frontend/src/hooks/api/reactQuery.tsx +++ b/frontend/src/hooks/api/reactQuery.tsx @@ -10,18 +10,14 @@ import { formatedConditionsOperatorNames, PermissionConditionOperators } from "@app/context/ProjectPermissionContext/types"; +import { camelCaseToSpaces } from "@app/lib/fn/string"; import { ApiErrorTypes, TApiErrors } from "./types"; - // this is saved in react-query cache export const SIGNUP_TEMP_TOKEN_CACHE_KEY = ["infisical__signup-temp-token"]; export const MFA_TEMP_TOKEN_CACHE_KEY = ["infisical__mfa-temp-token"]; export const AUTH_TOKEN_CACHE_KEY = ["infisical__auth-token"]; -const camelCaseToSpaces = (input: string) => { - return input.replace(/([a-z])([A-Z])/g, "$1 $2"); -}; - export const queryClient = new QueryClient({ mutationCache: new MutationCache({ onError: (error) => { diff --git a/frontend/src/lib/fn/string.ts b/frontend/src/lib/fn/string.ts index 9d3d01cc5..f4c4a43db 100644 --- a/frontend/src/lib/fn/string.ts +++ b/frontend/src/lib/fn/string.ts @@ -7,3 +7,7 @@ export const formatReservedPaths = (secretPath: string) => { } return secretPath; }; + +export const camelCaseToSpaces = (input: string) => { + return input.replace(/([a-z])([A-Z])/g, "$1 $2"); +}; diff --git a/frontend/src/pages/secret-manager/SecretDashboardPage/components/SecretListView/SecretDetailSidebar.tsx b/frontend/src/pages/secret-manager/SecretDashboardPage/components/SecretListView/SecretDetailSidebar.tsx index 641a4a723..382b574c8 100644 --- a/frontend/src/pages/secret-manager/SecretDashboardPage/components/SecretListView/SecretDetailSidebar.tsx +++ b/frontend/src/pages/secret-manager/SecretDashboardPage/components/SecretListView/SecretDetailSidebar.tsx @@ -61,6 +61,7 @@ import { hasSecretReadValueOrDescribePermission } from "@app/lib/fn/permission"; import { CreateReminderForm } from "./CreateReminderForm"; import { formSchema, SecretActionType, TFormSchema } from "./SecretListView.utils"; +import { camelCaseToSpaces } from "@app/lib/fn/string"; type Props = { isOpen?: boolean; @@ -729,8 +730,8 @@ export const SecretDetailSidebar = ({
{secretVersion?.map( ({ createdAt, secretValue, secretValueHidden, version, id, actor }) => ( -
-
+
+
@@ -936,12 +937,9 @@ export const SecretDetailSidebar = ({
- action.charAt(0).toUpperCase() + action.slice(1).toLowerCase() - ) + .map((action) => camelCaseToSpaces(action)) .join(", ")} - className="z-[100]" + className="z-[100] capitalize" > Date: Tue, 11 Mar 2025 07:11:14 +0400 Subject: [PATCH 34/71] chore: rolled back service token permission changes --- .../services/permission/project-permission.ts | 18 +----------------- 1 file changed, 1 insertion(+), 17 deletions(-) diff --git a/backend/src/ee/services/permission/project-permission.ts b/backend/src/ee/services/permission/project-permission.ts index 43f832d22..c622e65a7 100644 --- a/backend/src/ee/services/permission/project-permission.ts +++ b/backend/src/ee/services/permission/project-permission.ts @@ -856,14 +856,12 @@ export const buildServiceTokenProjectPermission = ( ) => { const canWrite = permission.includes("write"); const canRead = permission.includes("read"); - const { can, build } = new AbilityBuilder>(createMongoAbility); scopes.forEach(({ secretPath, environment }) => { [ProjectPermissionSub.Secrets, ProjectPermissionSub.SecretImports, ProjectPermissionSub.SecretFolders].forEach( (subject) => { if (canWrite) { can(ProjectPermissionActions.Edit, subject, { - // TODO: @Akhi // @ts-expect-error type secretPath: { $glob: secretPath }, environment @@ -879,27 +877,13 @@ export const buildServiceTokenProjectPermission = ( environment }); } - if (canRead && subject !== ProjectPermissionSub.Secrets) { + if (canRead) { can(ProjectPermissionActions.Read, subject, { // @ts-expect-error type secretPath: { $glob: secretPath }, environment }); } - - if (subject === ProjectPermissionSub.Secrets && canRead) { - // @ts-expect-error type - can(ProjectPermissionSecretActions.ReadValue, subject as ProjectPermissionSub.Secrets, { - secretPath: { $glob: secretPath }, - environment - }); - - // @ts-expect-error type - can(ProjectPermissionSecretActions.DescribeSecret, subject as ProjectPermissionSub.Secrets, { - secretPath: { $glob: secretPath }, - environment - }); - } } ); }); From 16d3bbb67ad84456117123d48ae5e4db3b11873d Mon Sep 17 00:00:00 2001 From: Maidul Islam Date: Mon, 10 Mar 2025 23:46:04 -0400 Subject: [PATCH 35/71] Add password requirements to dyanmic secret This will add a new accordion to add custom requirements for the generated password for DB drivers. We can use this pattern for other dynamic secrets too --- .../dynamic-secret/providers/models.ts | 38 +++ .../dynamic-secret/providers/sql-database.ts | 90 ++++++- frontend/src/hooks/api/dynamicSecret/types.ts | 1 + .../SqlDatabaseInputForm.tsx | 247 +++++++++++++++++- .../EditDynamicSecretSqlProviderForm.tsx | 243 ++++++++++++++++- 5 files changed, 604 insertions(+), 15 deletions(-) diff --git a/backend/src/ee/services/dynamic-secret/providers/models.ts b/backend/src/ee/services/dynamic-secret/providers/models.ts index 621c3c631..93af4eabc 100644 --- a/backend/src/ee/services/dynamic-secret/providers/models.ts +++ b/backend/src/ee/services/dynamic-secret/providers/models.ts @@ -1,5 +1,22 @@ import { z } from "zod"; +export type PasswordRequirements = { + minLength: number; + maxLength: number; + required: { + lowercase: number; + uppercase: number; + digits: number; + symbols: number; + }; + allowedCharacters?: { + lowercase?: string; + uppercase?: string; + digits?: string; + symbols?: string; + }; +}; + export enum SqlProviders { Postgres = "postgres", MySQL = "mysql2", @@ -100,6 +117,27 @@ export const DynamicSecretSqlDBSchema = z.object({ database: z.string().trim(), username: z.string().trim(), password: z.string().trim(), + passwordRequirements: z + .object({ + minLength: z.number().min(1).max(100), + maxLength: z.number().min(1).max(100), + required: z.object({ + lowercase: z.number().min(0), + uppercase: z.number().min(0), + digits: z.number().min(0), + symbols: z.number().min(0) + }), + allowedCharacters: z + .object({ + lowercase: z.string().optional(), + uppercase: z.string().optional(), + digits: z.string().optional(), + symbols: z.string().optional() + }) + .optional() + }) + .optional() + .describe('Password generation requirements'), creationStatement: z.string().trim(), revocationStatement: z.string().trim(), renewStatement: z.string().trim().optional(), diff --git a/backend/src/ee/services/dynamic-secret/providers/sql-database.ts b/backend/src/ee/services/dynamic-secret/providers/sql-database.ts index 68089ea4c..7d76db697 100644 --- a/backend/src/ee/services/dynamic-secret/providers/sql-database.ts +++ b/backend/src/ee/services/dynamic-secret/providers/sql-database.ts @@ -8,16 +8,92 @@ import { alphaNumericNanoId } from "@app/lib/nanoid"; import { TGatewayServiceFactory } from "../../gateway/gateway-service"; import { verifyHostInputValidity } from "../dynamic-secret-fns"; -import { DynamicSecretSqlDBSchema, SqlProviders, TDynamicProviderFns } from "./models"; +import { DynamicSecretSqlDBSchema, PasswordRequirements, SqlProviders, TDynamicProviderFns } from "./models"; const EXTERNAL_REQUEST_TIMEOUT = 10 * 1000; -const generatePassword = (provider: SqlProviders) => { - // oracle has limit of 48 password length - const size = provider === SqlProviders.Oracle ? 30 : 48; +const DEFAULT_PASSWORD_REQUIREMENTS = { + minLength: 48, + maxLength: 48, + required: { + lowercase: 1, + uppercase: 1, + digits: 1, + symbols: 0 + }, + allowedCharacters: { + lowercase: 'abcdefghijklmnopqrstuvwxyz', + uppercase: 'ABCDEFGHIJKLMNOPQRSTUVWXYZ', + digits: '0123456789', + symbols: '-_.~!*' + } +}; - const charset = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-_.~!*"; - return customAlphabet(charset, 48)(size); +const ORACLE_PASSWORD_REQUIREMENTS = { + ...DEFAULT_PASSWORD_REQUIREMENTS, + minLength: 30, + maxLength: 30 +}; + +const generatePassword = (provider: SqlProviders, requirements?: PasswordRequirements) => { + const defaultReqs = provider === SqlProviders.Oracle ? ORACLE_PASSWORD_REQUIREMENTS : DEFAULT_PASSWORD_REQUIREMENTS; + const finalReqs = requirements || defaultReqs; + + try { + const { minLength, maxLength, required, allowedCharacters = {} } = finalReqs; + + const chars = { + lowercase: allowedCharacters.lowercase || 'abcdefghijklmnopqrstuvwxyz', + uppercase: allowedCharacters.uppercase || 'ABCDEFGHIJKLMNOPQRSTUVWXYZ', + digits: allowedCharacters.digits || '0123456789', + symbols: allowedCharacters.symbols || '-_.~!*' + }; + + const parts: string[] = []; + + if (required.lowercase > 0) { + parts.push(...Array(required.lowercase).fill(0).map(() => + chars.lowercase[Math.floor(Math.random() * chars.lowercase.length)] + )); + } + + if (required.uppercase > 0) { + parts.push(...Array(required.uppercase).fill(0).map(() => + chars.uppercase[Math.floor(Math.random() * chars.uppercase.length)] + )); + } + + if (required.digits > 0) { + parts.push(...Array(required.digits).fill(0).map(() => + chars.digits[Math.floor(Math.random() * chars.digits.length)] + )); + } + + if (required.symbols > 0) { + parts.push(...Array(required.symbols).fill(0).map(() => + chars.symbols[Math.floor(Math.random() * chars.symbols.length)] + )); + } + + const requiredTotal = Object.values(required).reduce((a, b) => a + b, 0); + const remainingLength = Math.max(minLength - requiredTotal, 0); + + const allChars = Object.values(chars).join(''); + parts.push(...Array(remainingLength).fill(0).map(() => + allChars[Math.floor(Math.random() * allChars.length)] + )); + + // shuffle the array to mix up the characters + for (let i = parts.length - 1; i > 0; i--) { + const j = Math.floor(Math.random() * (i + 1)); + [parts[i], parts[j]] = [parts[j], parts[i]]; + } + + return parts.join(''); + } catch (error: unknown) { + const message = error instanceof Error ? error.message : 'Unknown error'; + throw new Error(`Failed to generate password: ${message}`); + } }; const generateUsername = (provider: SqlProviders) => { @@ -115,7 +191,7 @@ export const SqlDatabaseProvider = ({ gatewayService }: TSqlDatabaseProviderDTO) const create = async (inputs: unknown, expireAt: number) => { const providerInputs = await validateProviderInputs(inputs); const username = generateUsername(providerInputs.client); - const password = generatePassword(providerInputs.client); + const password = generatePassword(providerInputs.client, providerInputs.passwordRequirements); const gatewayCallback = async (host = providerInputs.host, port = providerInputs.port) => { const db = await $getClient({ ...providerInputs, port, host }); try { diff --git a/frontend/src/hooks/api/dynamicSecret/types.ts b/frontend/src/hooks/api/dynamicSecret/types.ts index 7d8c6920a..154449d5b 100644 --- a/frontend/src/hooks/api/dynamicSecret/types.ts +++ b/frontend/src/hooks/api/dynamicSecret/types.ts @@ -50,6 +50,7 @@ export type TDynamicSecretProvider = database: string; username: string; password: string; + passwordRegex?: string; creationStatement: string; revocationStatement: string; renewStatement?: string; diff --git a/frontend/src/pages/secret-manager/SecretDashboardPage/components/ActionBar/CreateDynamicSecretForm/SqlDatabaseInputForm.tsx b/frontend/src/pages/secret-manager/SecretDashboardPage/components/ActionBar/CreateDynamicSecretForm/SqlDatabaseInputForm.tsx index a4962d10c..354f226f8 100644 --- a/frontend/src/pages/secret-manager/SecretDashboardPage/components/ActionBar/CreateDynamicSecretForm/SqlDatabaseInputForm.tsx +++ b/frontend/src/pages/secret-manager/SecretDashboardPage/components/ActionBar/CreateDynamicSecretForm/SqlDatabaseInputForm.tsx @@ -23,6 +23,25 @@ import { useWorkspace } from "@app/context"; import { gatewaysQueryKeys, useCreateDynamicSecret } from "@app/hooks/api"; import { DynamicSecretProviders, SqlProviders } from "@app/hooks/api/dynamicSecret/types"; +const passwordRequirementsSchema = z.object({ + minLength: z.number().min(1).max(100), + maxLength: z.number().min(1).max(100), + required: z.object({ + lowercase: z.number().min(0), + uppercase: z.number().min(0), + digits: z.number().min(0), + symbols: z.number().min(0) + }), + allowedCharacters: z + .object({ + lowercase: z.string().optional(), + uppercase: z.string().optional(), + digits: z.string().optional(), + symbols: z.string().optional() + }) + .optional() +}); + const formSchema = z.object({ provider: z.object({ client: z.nativeEnum(SqlProviders), @@ -31,6 +50,7 @@ const formSchema = z.object({ database: z.string().min(1), username: z.string().min(1), password: z.string().min(1), + passwordRequirements: passwordRequirementsSchema.optional(), creationStatement: z.string().min(1), revocationStatement: z.string().min(1), renewStatement: z.string().optional(), @@ -137,7 +157,19 @@ export const SqlDatabaseInputForm = ({ } = useForm({ resolver: zodResolver(formSchema), defaultValues: { - provider: getSqlStatements(SqlProviders.Postgres) + provider: { + ...getSqlStatements(SqlProviders.Postgres), + passwordRequirements: { + minLength: 48, + maxLength: 48, + required: { + lowercase: 1, + uppercase: 1, + digits: 1, + symbols: 0 + } + } + } } }); @@ -174,6 +206,11 @@ export const SqlDatabaseInputForm = ({ setValue("provider.renewStatement", sqlStatment.renewStatement); setValue("provider.revocationStatement", sqlStatment.revocationStatement); setValue("provider.port", getDefaultPort(type)); + + // Update password requirements based on provider + const minMaxLength = type === SqlProviders.Oracle ? 30 : 48; + setValue("provider.passwordRequirements.minLength", minMaxLength); + setValue("provider.passwordRequirements.maxLength", minMaxLength); }; return ( @@ -197,6 +234,7 @@ export const SqlDatabaseInputForm = ({ )} />
+
)} /> - - - Modify SQL Statements + + + Creation, Revocation & Renew Statements (optional) +
+ Customize SQL statements for managing database user lifecycle +
+ + + Password Configuration (optional) + +
+ Set constraints on the generated database password +
+
+
+ ( + + field.onChange(Number(e.target.value))} + /> + + )} + /> + ( + + field.onChange(Number(e.target.value))} + /> + + )} + /> +
+ +
+

Required Characters

+
+ ( + + field.onChange(Number(e.target.value))} + /> + + )} + /> + ( + + field.onChange(Number(e.target.value))} + /> + + )} + /> + ( + + field.onChange(Number(e.target.value))} + /> + + )} + /> + ( + + field.onChange(Number(e.target.value))} + /> + + )} + /> +
+
+ +
+

Allowed Characters (Optional)

+
+ ( + + + + )} + /> + ( + + + + )} + /> + ( + + + + )} + /> + ( + + + + )} + /> +
+
+
+
+
+
diff --git a/frontend/src/pages/secret-manager/SecretDashboardPage/components/DynamicSecretListView/EditDynamicSecretForm/EditDynamicSecretSqlProviderForm.tsx b/frontend/src/pages/secret-manager/SecretDashboardPage/components/DynamicSecretListView/EditDynamicSecretForm/EditDynamicSecretSqlProviderForm.tsx index 39655e431..5cf5c6524 100644 --- a/frontend/src/pages/secret-manager/SecretDashboardPage/components/DynamicSecretListView/EditDynamicSecretForm/EditDynamicSecretSqlProviderForm.tsx +++ b/frontend/src/pages/secret-manager/SecretDashboardPage/components/DynamicSecretListView/EditDynamicSecretForm/EditDynamicSecretSqlProviderForm.tsx @@ -23,6 +23,25 @@ import { useWorkspace } from "@app/context"; import { gatewaysQueryKeys, useUpdateDynamicSecret } from "@app/hooks/api"; import { SqlProviders, TDynamicSecret } from "@app/hooks/api/dynamicSecret/types"; +const passwordRequirementsSchema = z.object({ + minLength: z.number().min(1).max(100), + maxLength: z.number().min(1).max(100), + required: z.object({ + lowercase: z.number().min(0), + uppercase: z.number().min(0), + digits: z.number().min(0), + symbols: z.number().min(0) + }), + allowedCharacters: z + .object({ + lowercase: z.string().optional(), + uppercase: z.string().optional(), + digits: z.string().optional(), + symbols: z.string().optional() + }) + .optional() +}); + const formSchema = z.object({ inputs: z .object({ @@ -32,6 +51,7 @@ const formSchema = z.object({ database: z.string().min(1), username: z.string().min(1), password: z.string().min(1), + passwordRequirements: passwordRequirementsSchema.optional(), creationStatement: z.string().min(1), revocationStatement: z.string().min(1), renewStatement: z.string().optional(), @@ -82,6 +102,17 @@ export const EditDynamicSecretSqlProviderForm = ({ secretPath, projectSlug }: Props) => { + const getDefaultPasswordRequirements = (provider: SqlProviders) => ({ + minLength: provider === SqlProviders.Oracle ? 30 : 48, + maxLength: provider === SqlProviders.Oracle ? 30 : 48, + required: { + lowercase: 1, + uppercase: 1, + digits: 1, + symbols: 0 + } + }); + const { control, watch, @@ -94,10 +125,13 @@ export const EditDynamicSecretSqlProviderForm = ({ maxTTL: dynamicSecret.maxTTL, newName: dynamicSecret.name, inputs: { - ...(dynamicSecret.inputs as TForm["inputs"]) + ...(dynamicSecret.inputs as TForm["inputs"]), + passwordRequirements: (dynamicSecret.inputs as TForm["inputs"])?.passwordRequirements || + getDefaultPasswordRequirements((dynamicSecret.inputs as TForm["inputs"])?.client || SqlProviders.Postgres) } } }); + const { currentWorkspace } = useWorkspace(); const { data: projectGateways, isPending: isProjectGatewaysLoading } = useQuery( gatewaysQueryKeys.listProjectGateways({ projectId: currentWorkspace.id }) @@ -347,10 +381,13 @@ export const EditDynamicSecretSqlProviderForm = ({ )} /> - - - Modify SQL Statements + + + Creation, Revocation & Renew Statements (optional) +
+ Customize SQL statements for managing database user lifecycle +
+ + + Password Configuration (optional) + +
+ Set constraints on the generated database password +
+
+
+ ( + + field.onChange(Number(e.target.value))} + /> + + )} + /> + ( + + field.onChange(Number(e.target.value))} + /> + + )} + /> +
+ +
+

Required Characters

+
+ ( + + field.onChange(Number(e.target.value))} + /> + + )} + /> + ( + + field.onChange(Number(e.target.value))} + /> + + )} + /> + ( + + field.onChange(Number(e.target.value))} + /> + + )} + /> + ( + + field.onChange(Number(e.target.value))} + /> + + )} + /> +
+
+ +
+

Allowed Characters (Optional)

+
+ ( + + + + )} + /> + ( + + + + )} + /> + ( + + + + )} + /> + ( + + + + )} + /> +
+
+
+
+
+
From 719a18c218049d865cf05168d5f29ae6b450d4c8 Mon Sep 17 00:00:00 2001 From: = Date: Tue, 11 Mar 2025 16:03:39 +0530 Subject: [PATCH 36/71] feat: removed user paywall for user management and fixed a type error --- .../secret-v2-bridge/secret-v2-bridge-service.ts | 4 +++- backend/src/services/super-admin/super-admin-service.ts | 6 ------ .../pages/admin/OverviewPage/components/UserPanel.tsx | 9 --------- 3 files changed, 3 insertions(+), 16 deletions(-) diff --git a/backend/src/services/secret-v2-bridge/secret-v2-bridge-service.ts b/backend/src/services/secret-v2-bridge/secret-v2-bridge-service.ts index 09b3a50cb..8238f3467 100644 --- a/backend/src/services/secret-v2-bridge/secret-v2-bridge-service.ts +++ b/backend/src/services/secret-v2-bridge/secret-v2-bridge-service.ts @@ -2567,6 +2567,8 @@ export const secretV2BridgeServiceFactory = ({ type: SecretType.Shared }); + if (!secret) throw new NotFoundError({ message: `Secret with name '${secretName}' not found` }); + throwIfMissingSecretReadValueOrDescribePermission(permission, ProjectPermissionSecretActions.DescribeSecret, { environment, secretPath, @@ -2574,7 +2576,7 @@ export const secretV2BridgeServiceFactory = ({ secretTags: (secret?.tags || []).map((el) => el.slug) }); - const decryptedSecretValue = secret.encryptedValue + const decryptedSecretValue = secret?.encryptedValue ? secretManagerDecryptor({ cipherTextBlob: secret.encryptedValue }).toString() : ""; diff --git a/backend/src/services/super-admin/super-admin-service.ts b/backend/src/services/super-admin/super-admin-service.ts index 8efdfd7f9..dfa698137 100644 --- a/backend/src/services/super-admin/super-admin-service.ts +++ b/backend/src/services/super-admin/super-admin-service.ts @@ -282,12 +282,6 @@ export const superAdminServiceFactory = ({ }; const deleteUser = async (userId: string) => { - if (!licenseService.onPremFeatures?.instanceUserManagement) { - throw new BadRequestError({ - message: "Failed to delete user due to plan restriction. Upgrade to Infisical's Pro plan." - }); - } - const user = await userDAL.deleteById(userId); return user; }; diff --git a/frontend/src/pages/admin/OverviewPage/components/UserPanel.tsx b/frontend/src/pages/admin/OverviewPage/components/UserPanel.tsx index 6654439de..7977c4f15 100644 --- a/frontend/src/pages/admin/OverviewPage/components/UserPanel.tsx +++ b/frontend/src/pages/admin/OverviewPage/components/UserPanel.tsx @@ -42,7 +42,6 @@ import { } from "@app/hooks/api"; import { UsePopUpState } from "@app/hooks/usePopUp"; -const deleteUserUpgradePlanMessage = "Deleting users via Admin UI"; const addServerAdminUpgradePlanMessage = "Granting another user Server Admin permissions"; const UserPanelTable = ({ @@ -156,14 +155,6 @@ const UserPanelTable = ({ { e.stopPropagation(); - if (!subscription?.instanceUserManagement) { - handlePopUpOpen("upgradePlan", { - username, - id, - message: deleteUserUpgradePlanMessage - }); - return; - } handlePopUpOpen("removeUser", { username, id }); }} > From 5e4b478b74631040cdbe682c35f08a2b6502cdfe Mon Sep 17 00:00:00 2001 From: Mahyar Mirrashed Date: Tue, 11 Mar 2025 08:17:07 -0700 Subject: [PATCH 37/71] refactor(nix): replace shell hook with infisical dependency --- flake.nix | 15 +-------------- 1 file changed, 1 insertion(+), 14 deletions(-) diff --git a/flake.nix b/flake.nix index ffed5545c..3fd21f871 100644 --- a/flake.nix +++ b/flake.nix @@ -15,21 +15,8 @@ lazygit nodejs_20 + infisical ]; - - shellHook = '' - # Define a writable directory for global npm packages - export NPM_CONFIG_PREFIX="$HOME/.npm-global" - export PATH="$NPM_CONFIG_PREFIX/bin:$PATH" - - # Ensure the directory exists - mkdir -p "$NPM_CONFIG_PREFIX" - - # Install Infisical CLI only if it's not already installed - if ! command -v infisical &>/dev/null; then - npm install -g @infisical/cli - fi - ''; }; }; } From c9da8477c867ab0dca101b687690e1cfa0d73e97 Mon Sep 17 00:00:00 2001 From: Mahyar Mirrashed Date: Tue, 11 Mar 2025 08:54:15 -0700 Subject: [PATCH 38/71] chore(nix): add prettier to list of dependencies --- flake.nix | 1 + 1 file changed, 1 insertion(+) diff --git a/flake.nix b/flake.nix index 3fd21f871..9d995a551 100644 --- a/flake.nix +++ b/flake.nix @@ -15,6 +15,7 @@ lazygit nodejs_20 + nodePackages.prettier infisical ]; }; From 6fa41a609bc985ee2dd2ddadba1f3eedef126682 Mon Sep 17 00:00:00 2001 From: Maidul Islam Date: Tue, 11 Mar 2025 12:28:48 -0400 Subject: [PATCH 39/71] remove char and digit rangs and other requested changes/improvments --- .../dynamic-secret/providers/models.ts | 31 ++- .../dynamic-secret/providers/sql-database.ts | 50 +++-- .../SqlDatabaseInputForm.tsx | 165 ++++++--------- .../EditDynamicSecretSqlProviderForm.tsx | 197 +++++++----------- 4 files changed, 168 insertions(+), 275 deletions(-) diff --git a/backend/src/ee/services/dynamic-secret/providers/models.ts b/backend/src/ee/services/dynamic-secret/providers/models.ts index 93af4eabc..1808057e7 100644 --- a/backend/src/ee/services/dynamic-secret/providers/models.ts +++ b/backend/src/ee/services/dynamic-secret/providers/models.ts @@ -1,20 +1,14 @@ import { z } from "zod"; export type PasswordRequirements = { - minLength: number; - maxLength: number; + length: number; required: { lowercase: number; uppercase: number; digits: number; symbols: number; }; - allowedCharacters?: { - lowercase?: string; - uppercase?: string; - digits?: string; - symbols?: string; - }; + allowedSymbols?: string; }; export enum SqlProviders { @@ -119,23 +113,22 @@ export const DynamicSecretSqlDBSchema = z.object({ password: z.string().trim(), passwordRequirements: z .object({ - minLength: z.number().min(1).max(100), - maxLength: z.number().min(1).max(100), + length: z.number().min(1).max(250), required: z.object({ lowercase: z.number().min(0), uppercase: z.number().min(0), digits: z.number().min(0), symbols: z.number().min(0) - }), - allowedCharacters: z - .object({ - lowercase: z.string().optional(), - uppercase: z.string().optional(), - digits: z.string().optional(), - symbols: z.string().optional() - }) - .optional() + }).refine((data) => { + const total = Object.values(data).reduce((sum, count) => sum + count, 0); + return total <= 250; + }, "Sum of required characters cannot exceed 250"), + allowedSymbols: z.string().optional() }) + .refine((data) => { + const total = Object.values(data.required).reduce((sum, count) => sum + count, 0); + return total <= data.length; + }, "Sum of required characters cannot exceed the total length") .optional() .describe('Password generation requirements'), creationStatement: z.string().trim(), diff --git a/backend/src/ee/services/dynamic-secret/providers/sql-database.ts b/backend/src/ee/services/dynamic-secret/providers/sql-database.ts index 7d76db697..bb9240139 100644 --- a/backend/src/ee/services/dynamic-secret/providers/sql-database.ts +++ b/backend/src/ee/services/dynamic-secret/providers/sql-database.ts @@ -2,6 +2,7 @@ import handlebars from "handlebars"; import knex from "knex"; import { customAlphabet } from "nanoid"; import { z } from "zod"; +import { randomInt } from 'crypto'; import { withGatewayProxy } from "@app/lib/gateway"; import { alphaNumericNanoId } from "@app/lib/nanoid"; @@ -13,26 +14,19 @@ import { DynamicSecretSqlDBSchema, PasswordRequirements, SqlProviders, TDynamicP const EXTERNAL_REQUEST_TIMEOUT = 10 * 1000; const DEFAULT_PASSWORD_REQUIREMENTS = { - minLength: 48, - maxLength: 48, + length: 48, required: { lowercase: 1, uppercase: 1, digits: 1, symbols: 0 }, - allowedCharacters: { - lowercase: 'abcdefghijklmnopqrstuvwxyz', - uppercase: 'ABCDEFGHIJKLMNOPQRSTUVWXYZ', - digits: '0123456789', - symbols: '-_.~!*' - } + allowedSymbols: '-_.~!*' }; const ORACLE_PASSWORD_REQUIREMENTS = { ...DEFAULT_PASSWORD_REQUIREMENTS, - minLength: 30, - maxLength: 30 + length: 30 }; const generatePassword = (provider: SqlProviders, requirements?: PasswordRequirements) => { @@ -40,52 +34,56 @@ const generatePassword = (provider: SqlProviders, requirements?: PasswordRequire const finalReqs = requirements || defaultReqs; try { - const { minLength, maxLength, required, allowedCharacters = {} } = finalReqs; + const { length, required, allowedSymbols } = finalReqs; const chars = { - lowercase: allowedCharacters.lowercase || 'abcdefghijklmnopqrstuvwxyz', - uppercase: allowedCharacters.uppercase || 'ABCDEFGHIJKLMNOPQRSTUVWXYZ', - digits: allowedCharacters.digits || '0123456789', - symbols: allowedCharacters.symbols || '-_.~!*' + lowercase: 'abcdefghijklmnopqrstuvwxyz', + uppercase: 'ABCDEFGHIJKLMNOPQRSTUVWXYZ', + digits: '0123456789', + symbols: allowedSymbols || '-_.~!*' }; const parts: string[] = []; if (required.lowercase > 0) { parts.push(...Array(required.lowercase).fill(0).map(() => - chars.lowercase[Math.floor(Math.random() * chars.lowercase.length)] + chars.lowercase[randomInt(chars.lowercase.length)] )); } - + if (required.uppercase > 0) { parts.push(...Array(required.uppercase).fill(0).map(() => - chars.uppercase[Math.floor(Math.random() * chars.uppercase.length)] + chars.uppercase[randomInt(chars.uppercase.length)] )); } - + if (required.digits > 0) { parts.push(...Array(required.digits).fill(0).map(() => - chars.digits[Math.floor(Math.random() * chars.digits.length)] + chars.digits[randomInt(chars.digits.length)] )); } - + if (required.symbols > 0) { parts.push(...Array(required.symbols).fill(0).map(() => - chars.symbols[Math.floor(Math.random() * chars.symbols.length)] + chars.symbols[randomInt(chars.symbols.length)] )); } const requiredTotal = Object.values(required).reduce((a, b) => a + b, 0); - const remainingLength = Math.max(minLength - requiredTotal, 0); + const remainingLength = Math.max(length - requiredTotal, 0); + + const allowedChars = Object.entries(chars) + .filter(([key]) => required[key as keyof typeof required] > 0) + .map(([, value]) => value) + .join(''); - const allChars = Object.values(chars).join(''); parts.push(...Array(remainingLength).fill(0).map(() => - allChars[Math.floor(Math.random() * allChars.length)] + allowedChars[randomInt(allowedChars.length)] )); // shuffle the array to mix up the characters for (let i = parts.length - 1; i > 0; i--) { - const j = Math.floor(Math.random() * (i + 1)); + const j = randomInt(i + 1); [parts[i], parts[j]] = [parts[j], parts[i]]; } diff --git a/frontend/src/pages/secret-manager/SecretDashboardPage/components/ActionBar/CreateDynamicSecretForm/SqlDatabaseInputForm.tsx b/frontend/src/pages/secret-manager/SecretDashboardPage/components/ActionBar/CreateDynamicSecretForm/SqlDatabaseInputForm.tsx index 354f226f8..063b2d08b 100644 --- a/frontend/src/pages/secret-manager/SecretDashboardPage/components/ActionBar/CreateDynamicSecretForm/SqlDatabaseInputForm.tsx +++ b/frontend/src/pages/secret-manager/SecretDashboardPage/components/ActionBar/CreateDynamicSecretForm/SqlDatabaseInputForm.tsx @@ -24,23 +24,21 @@ import { gatewaysQueryKeys, useCreateDynamicSecret } from "@app/hooks/api"; import { DynamicSecretProviders, SqlProviders } from "@app/hooks/api/dynamicSecret/types"; const passwordRequirementsSchema = z.object({ - minLength: z.number().min(1).max(100), - maxLength: z.number().min(1).max(100), + length: z.number().min(1).max(250), required: z.object({ lowercase: z.number().min(0), uppercase: z.number().min(0), digits: z.number().min(0), symbols: z.number().min(0) - }), - allowedCharacters: z - .object({ - lowercase: z.string().optional(), - uppercase: z.string().optional(), - digits: z.string().optional(), - symbols: z.string().optional() - }) - .optional() -}); + }).refine((data) => { + const total = Object.values(data).reduce((sum, count) => sum + count, 0); + return total <= 250; + }, "Sum of required characters cannot exceed 250"), + allowedSymbols: z.string().optional() +}).refine((data) => { + const total = Object.values(data.required).reduce((sum, count) => sum + count, 0); + return total <= data.length; +}, "Sum of required characters cannot exceed the total length"); const formSchema = z.object({ provider: z.object({ @@ -153,21 +151,22 @@ export const SqlDatabaseInputForm = ({ control, setValue, formState: { isSubmitting }, - handleSubmit + handleSubmit, + watch } = useForm({ resolver: zodResolver(formSchema), defaultValues: { provider: { ...getSqlStatements(SqlProviders.Postgres), passwordRequirements: { - minLength: 48, - maxLength: 48, + length: 48, required: { lowercase: 1, uppercase: 1, digits: 1, symbols: 0 - } + }, + allowedSymbols: '-_.~!*' } } } @@ -208,9 +207,8 @@ export const SqlDatabaseInputForm = ({ setValue("provider.port", getDefaultPort(type)); // Update password requirements based on provider - const minMaxLength = type === SqlProviders.Oracle ? 30 : 48; - setValue("provider.passwordRequirements.minLength", minMaxLength); - setValue("provider.passwordRequirements.maxLength", minMaxLength); + const length = type === SqlProviders.Oracle ? 30 : 48; + setValue("provider.passwordRequirements.length", length); }; return ( @@ -499,41 +497,21 @@ export const SqlDatabaseInputForm = ({ Set constraints on the generated database password
-
+
( field.onChange(Number(e.target.value))} - /> - - )} - /> - ( - - field.onChange(Number(e.target.value))} /> @@ -543,7 +521,19 @@ export const SqlDatabaseInputForm = ({
-

Required Characters

+

Minimum Required Character Counts

+
+ {(() => { + const total = Object.values(watch("provider.passwordRequirements.required") || {}).reduce((sum, count) => sum + Number(count || 0), 0); + const length = watch("provider.passwordRequirements.length") || 0; + const isError = total > length; + return ( + + Total required characters: {total} {isError ? `(exceeds length of ${length})` : ""} + + ); + })()} +
( ( ( (
-

Allowed Characters (Optional)

-
- ( - - - - )} - /> - ( - - - - )} - /> - ( - - - - )} - /> - ( - - - - )} - /> -
+

Allowed Symbols

+ ( + + + + )} + />
diff --git a/frontend/src/pages/secret-manager/SecretDashboardPage/components/DynamicSecretListView/EditDynamicSecretForm/EditDynamicSecretSqlProviderForm.tsx b/frontend/src/pages/secret-manager/SecretDashboardPage/components/DynamicSecretListView/EditDynamicSecretForm/EditDynamicSecretSqlProviderForm.tsx index 5cf5c6524..85fb6456e 100644 --- a/frontend/src/pages/secret-manager/SecretDashboardPage/components/DynamicSecretListView/EditDynamicSecretForm/EditDynamicSecretSqlProviderForm.tsx +++ b/frontend/src/pages/secret-manager/SecretDashboardPage/components/DynamicSecretListView/EditDynamicSecretForm/EditDynamicSecretSqlProviderForm.tsx @@ -24,23 +24,21 @@ import { gatewaysQueryKeys, useUpdateDynamicSecret } from "@app/hooks/api"; import { SqlProviders, TDynamicSecret } from "@app/hooks/api/dynamicSecret/types"; const passwordRequirementsSchema = z.object({ - minLength: z.number().min(1).max(100), - maxLength: z.number().min(1).max(100), + length: z.number().min(1).max(250), required: z.object({ lowercase: z.number().min(0), uppercase: z.number().min(0), digits: z.number().min(0), symbols: z.number().min(0) - }), - allowedCharacters: z - .object({ - lowercase: z.string().optional(), - uppercase: z.string().optional(), - digits: z.string().optional(), - symbols: z.string().optional() - }) - .optional() -}); + }).refine((data) => { + const total = Object.values(data).reduce((sum, count) => sum + count, 0); + return total <= 250; // Sanity check for individual validation + }, "Sum of required characters cannot exceed 250"), + allowedSymbols: z.string().optional() +}).refine((data) => { + const total = Object.values(data.required).reduce((sum, count) => sum + count, 0); + return total <= data.length; +}, "Sum of required characters cannot exceed the total length"); const formSchema = z.object({ inputs: z @@ -103,14 +101,14 @@ export const EditDynamicSecretSqlProviderForm = ({ projectSlug }: Props) => { const getDefaultPasswordRequirements = (provider: SqlProviders) => ({ - minLength: provider === SqlProviders.Oracle ? 30 : 48, - maxLength: provider === SqlProviders.Oracle ? 30 : 48, + length: provider === SqlProviders.Oracle ? 30 : 48, required: { lowercase: 1, uppercase: 1, digits: 1, symbols: 0 - } + }, + allowedSymbols: '-_.~!*' }); const { @@ -463,41 +461,21 @@ export const EditDynamicSecretSqlProviderForm = ({ Set constraints on the generated database password
-
+
( field.onChange(Number(e.target.value))} - /> - - )} - /> - ( - - field.onChange(Number(e.target.value))} /> @@ -507,7 +485,19 @@ export const EditDynamicSecretSqlProviderForm = ({
-

Required Characters

+

Minimum Required Character Counts

+
+ {(() => { + const total = Object.values(watch("inputs.passwordRequirements.required") || {}).reduce((sum, count) => sum + Number(count || 0), 0); + const length = watch("inputs.passwordRequirements.length") || 0; + const isError = total > length; + return ( + + Total required characters: {total} {isError ? `(exceeds length of ${length})` : ""} + + ); + })()} +
( field.onChange(Number(e.target.value))} - /> + type="number" + min={0} + {...field} + onChange={(e) => field.onChange(Number(e.target.value))} + /> )} /> @@ -534,16 +525,17 @@ export const EditDynamicSecretSqlProviderForm = ({ defaultValue={1} render={({ field, fieldState: { error } }) => ( field.onChange(Number(e.target.value))} - /> + type="number" + min={0} + {...field} + onChange={(e) => field.onChange(Number(e.target.value))} + /> )} /> @@ -553,16 +545,17 @@ export const EditDynamicSecretSqlProviderForm = ({ defaultValue={1} render={({ field, fieldState: { error } }) => ( field.onChange(Number(e.target.value))} - /> + type="number" + min={0} + {...field} + onChange={(e) => field.onChange(Number(e.target.value))} + /> )} /> @@ -572,16 +565,17 @@ export const EditDynamicSecretSqlProviderForm = ({ defaultValue={0} render={({ field, fieldState: { error } }) => ( field.onChange(Number(e.target.value))} - /> + type="number" + min={0} + {...field} + onChange={(e) => field.onChange(Number(e.target.value))} + /> )} /> @@ -589,65 +583,22 @@ export const EditDynamicSecretSqlProviderForm = ({
-

Allowed Characters (Optional)

-
- ( - - - - )} - /> - ( - - - - )} - /> - ( - - - - )} - /> - ( - - - - )} - /> -
+

Allowed Symbols

+ ( + + + + )} + />
From ed37b997567748badf5fc869cc7b2142ee51442b Mon Sep 17 00:00:00 2001 From: Daniel Hougaard Date: Tue, 11 Mar 2025 20:34:35 +0400 Subject: [PATCH 40/71] fix: list secrets permissioning bug --- .../services/secret-v2-bridge/secret-v2-bridge-service.ts | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/backend/src/services/secret-v2-bridge/secret-v2-bridge-service.ts b/backend/src/services/secret-v2-bridge/secret-v2-bridge-service.ts index 8238f3467..dc975854f 100644 --- a/backend/src/services/secret-v2-bridge/secret-v2-bridge-service.ts +++ b/backend/src/services/secret-v2-bridge/secret-v2-bridge-service.ts @@ -909,11 +909,7 @@ export const secretV2BridgeServiceFactory = ({ actorOrgId, actionProjectType: ActionProjectType.SecretManager }); - throwIfMissingSecretReadValueOrDescribePermission(permission, ProjectPermissionSecretActions.DescribeSecret, { - environment, - secretPath: path, - secretTags: params.tagSlugs - }); + throwIfMissingSecretReadValueOrDescribePermission(permission, ProjectPermissionSecretActions.DescribeSecret); let paths: { folderId: string; path: string }[] = []; From 4dc56033b1214e579a72da6bdfe216559a6c2087 Mon Sep 17 00:00:00 2001 From: Sheen Capadngan Date: Wed, 12 Mar 2025 00:41:05 +0800 Subject: [PATCH 41/71] misc: improve support for jwks via http --- .../identity-jwt-auth-service.ts | 24 ++++++++++++------- 1 file changed, 16 insertions(+), 8 deletions(-) diff --git a/backend/src/services/identity-jwt-auth/identity-jwt-auth-service.ts b/backend/src/services/identity-jwt-auth/identity-jwt-auth-service.ts index 2c9b6306e..555dc00a6 100644 --- a/backend/src/services/identity-jwt-auth/identity-jwt-auth-service.ts +++ b/backend/src/services/identity-jwt-auth/identity-jwt-auth-service.ts @@ -78,14 +78,22 @@ export const identityJwtAuthServiceFactory = ({ let tokenData: Record = {}; if (identityJwtAuth.configurationType === JwtConfigurationType.JWKS) { - const decryptedJwksCaCert = orgDataKeyDecryptor({ - cipherTextBlob: identityJwtAuth.encryptedJwksCaCert - }).toString(); - const requestAgent = new https.Agent({ ca: decryptedJwksCaCert, rejectUnauthorized: !!decryptedJwksCaCert }); - const client = new JwksClient({ - jwksUri: identityJwtAuth.jwksUrl, - requestAgent - }); + let client: JwksClient; + if (identityJwtAuth.jwksUrl.includes("https:")) { + const decryptedJwksCaCert = orgDataKeyDecryptor({ + cipherTextBlob: identityJwtAuth.encryptedJwksCaCert + }).toString(); + + const requestAgent = new https.Agent({ ca: decryptedJwksCaCert, rejectUnauthorized: !!decryptedJwksCaCert }); + client = new JwksClient({ + jwksUri: identityJwtAuth.jwksUrl, + requestAgent + }); + } else { + client = new JwksClient({ + jwksUri: identityJwtAuth.jwksUrl + }); + } const { kid } = decodedToken.header; const jwtSigningKey = await client.getSigningKey(kid); From 75e0a68b685f284096e2c28a9acfe8250187da9a Mon Sep 17 00:00:00 2001 From: Maidul Islam Date: Tue, 11 Mar 2025 12:46:43 -0400 Subject: [PATCH 42/71] remove password regex --- backend/src/ee/services/license/license-fns.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/backend/src/ee/services/license/license-fns.ts b/backend/src/ee/services/license/license-fns.ts index 21d378802..ba89f965a 100644 --- a/backend/src/ee/services/license/license-fns.ts +++ b/backend/src/ee/services/license/license-fns.ts @@ -17,7 +17,7 @@ export const getDefaultOnPremFeatures = (): TFeatureSet => ({ environmentsUsed: 0, identityLimit: null, identitiesUsed: 0, - dynamicSecret: false, + dynamicSecret: true, secretVersioning: true, pitRecovery: false, ipAllowlisting: false, From f5749e326a023211cbe6f9bd65ccc95d4a1f3a17 Mon Sep 17 00:00:00 2001 From: Maidul Islam Date: Tue, 11 Mar 2025 12:49:55 -0400 Subject: [PATCH 43/71] remove regex and fix lint --- backend/src/ee/services/license/license-fns.ts | 2 +- frontend/src/hooks/api/dynamicSecret/types.ts | 1 - 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/backend/src/ee/services/license/license-fns.ts b/backend/src/ee/services/license/license-fns.ts index ba89f965a..21d378802 100644 --- a/backend/src/ee/services/license/license-fns.ts +++ b/backend/src/ee/services/license/license-fns.ts @@ -17,7 +17,7 @@ export const getDefaultOnPremFeatures = (): TFeatureSet => ({ environmentsUsed: 0, identityLimit: null, identitiesUsed: 0, - dynamicSecret: true, + dynamicSecret: false, secretVersioning: true, pitRecovery: false, ipAllowlisting: false, diff --git a/frontend/src/hooks/api/dynamicSecret/types.ts b/frontend/src/hooks/api/dynamicSecret/types.ts index 154449d5b..7d8c6920a 100644 --- a/frontend/src/hooks/api/dynamicSecret/types.ts +++ b/frontend/src/hooks/api/dynamicSecret/types.ts @@ -50,7 +50,6 @@ export type TDynamicSecretProvider = database: string; username: string; password: string; - passwordRegex?: string; creationStatement: string; revocationStatement: string; renewStatement?: string; From edf6a37fe54f791db4fc22f524a10f80dc698002 Mon Sep 17 00:00:00 2001 From: Maidul Islam Date: Tue, 11 Mar 2025 13:08:04 -0400 Subject: [PATCH 44/71] fix lint --- .../dynamic-secret/providers/models.ts | 22 ++++--- .../dynamic-secret/providers/sql-database.ts | 65 +++++++++++-------- .../src/server/routes/v2/password-router.ts | 4 +- .../services/auth/auth-password-service.ts | 2 +- 4 files changed, 52 insertions(+), 41 deletions(-) diff --git a/backend/src/ee/services/dynamic-secret/providers/models.ts b/backend/src/ee/services/dynamic-secret/providers/models.ts index 1808057e7..449f6d8f6 100644 --- a/backend/src/ee/services/dynamic-secret/providers/models.ts +++ b/backend/src/ee/services/dynamic-secret/providers/models.ts @@ -114,15 +114,17 @@ export const DynamicSecretSqlDBSchema = z.object({ passwordRequirements: z .object({ length: z.number().min(1).max(250), - required: z.object({ - lowercase: z.number().min(0), - uppercase: z.number().min(0), - digits: z.number().min(0), - symbols: z.number().min(0) - }).refine((data) => { - const total = Object.values(data).reduce((sum, count) => sum + count, 0); - return total <= 250; - }, "Sum of required characters cannot exceed 250"), + required: z + .object({ + lowercase: z.number().min(0), + uppercase: z.number().min(0), + digits: z.number().min(0), + symbols: z.number().min(0) + }) + .refine((data) => { + const total = Object.values(data).reduce((sum, count) => sum + count, 0); + return total <= 250; + }, "Sum of required characters cannot exceed 250"), allowedSymbols: z.string().optional() }) .refine((data) => { @@ -130,7 +132,7 @@ export const DynamicSecretSqlDBSchema = z.object({ return total <= data.length; }, "Sum of required characters cannot exceed the total length") .optional() - .describe('Password generation requirements'), + .describe("Password generation requirements"), creationStatement: z.string().trim(), revocationStatement: z.string().trim(), renewStatement: z.string().trim().optional(), diff --git a/backend/src/ee/services/dynamic-secret/providers/sql-database.ts b/backend/src/ee/services/dynamic-secret/providers/sql-database.ts index bb9240139..eea9fef94 100644 --- a/backend/src/ee/services/dynamic-secret/providers/sql-database.ts +++ b/backend/src/ee/services/dynamic-secret/providers/sql-database.ts @@ -1,8 +1,7 @@ +import { randomInt } from "crypto"; import handlebars from "handlebars"; import knex from "knex"; -import { customAlphabet } from "nanoid"; import { z } from "zod"; -import { randomInt } from 'crypto'; import { withGatewayProxy } from "@app/lib/gateway"; import { alphaNumericNanoId } from "@app/lib/nanoid"; @@ -21,7 +20,7 @@ const DEFAULT_PASSWORD_REQUIREMENTS = { digits: 1, symbols: 0 }, - allowedSymbols: '-_.~!*' + allowedSymbols: "-_.~!*" }; const ORACLE_PASSWORD_REQUIREMENTS = { @@ -35,38 +34,46 @@ const generatePassword = (provider: SqlProviders, requirements?: PasswordRequire try { const { length, required, allowedSymbols } = finalReqs; - + const chars = { - lowercase: 'abcdefghijklmnopqrstuvwxyz', - uppercase: 'ABCDEFGHIJKLMNOPQRSTUVWXYZ', - digits: '0123456789', - symbols: allowedSymbols || '-_.~!*' + lowercase: "abcdefghijklmnopqrstuvwxyz", + uppercase: "ABCDEFGHIJKLMNOPQRSTUVWXYZ", + digits: "0123456789", + symbols: allowedSymbols || "-_.~!*" }; const parts: string[] = []; - + if (required.lowercase > 0) { - parts.push(...Array(required.lowercase).fill(0).map(() => - chars.lowercase[randomInt(chars.lowercase.length)] - )); + parts.push( + ...Array(required.lowercase) + .fill(0) + .map(() => chars.lowercase[randomInt(chars.lowercase.length)]) + ); } if (required.uppercase > 0) { - parts.push(...Array(required.uppercase).fill(0).map(() => - chars.uppercase[randomInt(chars.uppercase.length)] - )); + parts.push( + ...Array(required.uppercase) + .fill(0) + .map(() => chars.uppercase[randomInt(chars.uppercase.length)]) + ); } if (required.digits > 0) { - parts.push(...Array(required.digits).fill(0).map(() => - chars.digits[randomInt(chars.digits.length)] - )); + parts.push( + ...Array(required.digits) + .fill(0) + .map(() => chars.digits[randomInt(chars.digits.length)]) + ); } if (required.symbols > 0) { - parts.push(...Array(required.symbols).fill(0).map(() => - chars.symbols[randomInt(chars.symbols.length)] - )); + parts.push( + ...Array(required.symbols) + .fill(0) + .map(() => chars.symbols[randomInt(chars.symbols.length)]) + ); } const requiredTotal = Object.values(required).reduce((a, b) => a + b, 0); @@ -75,21 +82,23 @@ const generatePassword = (provider: SqlProviders, requirements?: PasswordRequire const allowedChars = Object.entries(chars) .filter(([key]) => required[key as keyof typeof required] > 0) .map(([, value]) => value) - .join(''); + .join(""); - parts.push(...Array(remainingLength).fill(0).map(() => - allowedChars[randomInt(allowedChars.length)] - )); + parts.push( + ...Array(remainingLength) + .fill(0) + .map(() => allowedChars[randomInt(allowedChars.length)]) + ); // shuffle the array to mix up the characters - for (let i = parts.length - 1; i > 0; i--) { + for (let i = parts.length - 1; i > 0; i -= 1) { const j = randomInt(i + 1); [parts[i], parts[j]] = [parts[j], parts[i]]; } - return parts.join(''); + return parts.join(""); } catch (error: unknown) { - const message = error instanceof Error ? error.message : 'Unknown error'; + const message = error instanceof Error ? error.message : "Unknown error"; throw new Error(`Failed to generate password: ${message}`); } }; diff --git a/backend/src/server/routes/v2/password-router.ts b/backend/src/server/routes/v2/password-router.ts index 165130dec..63b6d8aac 100644 --- a/backend/src/server/routes/v2/password-router.ts +++ b/backend/src/server/routes/v2/password-router.ts @@ -1,10 +1,10 @@ import { z } from "zod"; import { authRateLimit } from "@app/server/config/rateLimiter"; -import { validatePasswordResetAuthorization } from "@app/services/auth/auth-fns"; -import { AuthMode } from "@app/services/auth/auth-type"; import { verifyAuth } from "@app/server/plugins/auth/verify-auth"; +import { validatePasswordResetAuthorization } from "@app/services/auth/auth-fns"; import { ResetPasswordV2Type } from "@app/services/auth/auth-password-type"; +import { AuthMode } from "@app/services/auth/auth-type"; export const registerPasswordRouter = async (server: FastifyZodProvider) => { server.route({ diff --git a/backend/src/services/auth/auth-password-service.ts b/backend/src/services/auth/auth-password-service.ts index f5f42a236..14fb58258 100644 --- a/backend/src/services/auth/auth-password-service.ts +++ b/backend/src/services/auth/auth-password-service.ts @@ -7,6 +7,7 @@ import { generateSrpServerKey, srpCheckClientProof } from "@app/lib/crypto"; import { infisicalSymmetricDecrypt, infisicalSymmetricEncypt } from "@app/lib/crypto/encryption"; import { generateUserSrpKeys } from "@app/lib/crypto/srp"; import { BadRequestError } from "@app/lib/errors"; +import { logger } from "@app/lib/logger"; import { OrgServiceActor } from "@app/lib/types"; import { TAuthTokenServiceFactory } from "../auth-token/auth-token-service"; @@ -25,7 +26,6 @@ import { TSetupPasswordViaBackupKeyDTO } from "./auth-password-type"; import { ActorType, AuthMethod, AuthTokenType } from "./auth-type"; -import { logger } from "@app/lib/logger"; type TAuthPasswordServiceFactoryDep = { authDAL: TAuthDALFactory; From e7c512999e8312b85bb65a08cd54819cff09b2a0 Mon Sep 17 00:00:00 2001 From: Mahyar Mirrashed Date: Tue, 11 Mar 2025 13:30:45 -0700 Subject: [PATCH 45/71] feat(admin): add ability to view machine identities --- backend/src/server/routes/index.ts | 1 + backend/src/server/routes/v1/admin-router.ts | 39 +++++++- backend/src/services/identity/identity-dal.ts | 38 +++++++- .../super-admin/super-admin-service.ts | 19 +++- .../services/super-admin/super-admin-types.ts | 6 ++ frontend/src/hooks/api/admin/queries.ts | 29 +++++- frontend/src/hooks/api/admin/types.ts | 5 + .../pages/admin/OverviewPage/OverviewPage.tsx | 6 ++ .../OverviewPage/components/IdentityPanel.tsx | 95 +++++++++++++++++++ .../OverviewPage/components/UserPanel.tsx | 4 +- 10 files changed, 232 insertions(+), 10 deletions(-) create mode 100644 frontend/src/pages/admin/OverviewPage/components/IdentityPanel.tsx diff --git a/backend/src/server/routes/index.ts b/backend/src/server/routes/index.ts index efc1cb865..b9f47cb7e 100644 --- a/backend/src/server/routes/index.ts +++ b/backend/src/server/routes/index.ts @@ -635,6 +635,7 @@ export const registerRoutes = async ( }); const superAdminService = superAdminServiceFactory({ userDAL, + identityDAL, userAliasDAL, authService: loginService, serverCfgDAL: superAdminDAL, diff --git a/backend/src/server/routes/v1/admin-router.ts b/backend/src/server/routes/v1/admin-router.ts index b63550d2c..a1c433650 100644 --- a/backend/src/server/routes/v1/admin-router.ts +++ b/backend/src/server/routes/v1/admin-router.ts @@ -1,7 +1,7 @@ import DOMPurify from "isomorphic-dompurify"; import { z } from "zod"; -import { OrganizationsSchema, SuperAdminSchema, UsersSchema } from "@app/db/schemas"; +import { IdentitiesSchema, OrganizationsSchema, SuperAdminSchema, UsersSchema } from "@app/db/schemas"; import { getConfig } from "@app/lib/config/env"; import { BadRequestError } from "@app/lib/errors"; import { readLimit, writeLimit } from "@app/server/config/rateLimiter"; @@ -154,6 +154,43 @@ export const registerAdminRouter = async (server: FastifyZodProvider) => { } }); + server.route({ + method: "GET", + url: "/identity-management/identities", + config: { + rateLimit: readLimit + }, + schema: { + querystring: z.object({ + searchTerm: z.string().default(""), + offset: z.coerce.number().default(0), + limit: z.coerce.number().max(100).default(20) + }), + response: { + 200: z.object({ + identities: IdentitiesSchema.pick({ + name: true, + id: true + }).array() + }) + } + }, + onRequest: (req, res, done) => { + verifyAuth([AuthMode.JWT])(req, res, () => { + verifySuperAdmin(req, res, done); + }); + }, + handler: async (req) => { + const identities = await server.services.superAdmin.getIdentities({ + ...req.query + }); + + return { + identities + }; + } + }); + server.route({ method: "GET", url: "/integrations/slack/config", diff --git a/backend/src/services/identity/identity-dal.ts b/backend/src/services/identity/identity-dal.ts index a74a84ce3..8b7fccab3 100644 --- a/backend/src/services/identity/identity-dal.ts +++ b/backend/src/services/identity/identity-dal.ts @@ -1,10 +1,42 @@ import { TDbClient } from "@app/db"; -import { TableName } from "@app/db/schemas"; -import { ormify } from "@app/lib/knex"; +import { TableName, TIdentities } from "@app/db/schemas"; +import { ormify, selectAllTableCols } from "@app/lib/knex"; +import { DatabaseError } from "@app/lib/errors"; export type TIdentityDALFactory = ReturnType; export const identityDALFactory = (db: TDbClient) => { const identityOrm = ormify(db, TableName.Identity); - return identityOrm; + + const getIdentitiesByFilter = async ({ + limit, + offset, + searchTerm, + sortBy + }: { + limit: number; + offset: number; + searchTerm: string; + sortBy?: keyof TIdentities; + }) => { + try { + let query = db.replicaNode()(TableName.Identity); + + if (searchTerm) { + query = query.where((qb) => { + void qb.whereILike("name", `%${searchTerm}%`); + }); + } + + if (sortBy) { + query = query.orderBy(sortBy); + } + + return await query.limit(limit).offset(offset).select(selectAllTableCols(TableName.Identity)); + } catch (error) { + throw new DatabaseError({ error, name: "Get identities by filter" }); + } + }; + + return { ...identityOrm, getIdentitiesByFilter }; }; diff --git a/backend/src/services/super-admin/super-admin-service.ts b/backend/src/services/super-admin/super-admin-service.ts index 8efdfd7f9..e8e01a44f 100644 --- a/backend/src/services/super-admin/super-admin-service.ts +++ b/backend/src/services/super-admin/super-admin-service.ts @@ -19,9 +19,11 @@ import { TUserDALFactory } from "../user/user-dal"; import { TUserAliasDALFactory } from "../user-alias/user-alias-dal"; import { UserAliasType } from "../user-alias/user-alias-types"; import { TSuperAdminDALFactory } from "./super-admin-dal"; -import { LoginMethod, TAdminGetUsersDTO, TAdminSignUpDTO } from "./super-admin-types"; +import { LoginMethod, TAdminGetIdentitiesDTO, TAdminGetUsersDTO, TAdminSignUpDTO } from "./super-admin-types"; +import { TIdentityDALFactory } from "@app/services/identity/identity-dal"; type TSuperAdminServiceFactoryDep = { + identityDAL: Pick; serverCfgDAL: TSuperAdminDALFactory; userDAL: TUserDALFactory; userAliasDAL: Pick; @@ -51,6 +53,7 @@ const ADMIN_CONFIG_DB_UUID = "00000000-0000-0000-0000-000000000000"; export const superAdminServiceFactory = ({ serverCfgDAL, userDAL, + identityDAL, userAliasDAL, authService, orgService, @@ -143,8 +146,8 @@ export const superAdminServiceFactory = ({ const canServerAdminAccessAfterApply = data.enabledLoginMethods.some((loginMethod) => - loginMethodToAuthMethod[loginMethod as LoginMethod].some( - (authMethod) => superAdminUser.authMethods?.includes(authMethod) + loginMethodToAuthMethod[loginMethod as LoginMethod].some((authMethod) => + superAdminUser.authMethods?.includes(authMethod) ) ) || isUserSamlAccessEnabled || @@ -292,6 +295,15 @@ export const superAdminServiceFactory = ({ return user; }; + const getIdentities = ({ offset, limit, searchTerm }: TAdminGetIdentitiesDTO) => { + return identityDAL.getIdentitiesByFilter({ + limit, + offset, + searchTerm, + sortBy: "name" + }); + }; + const grantServerAdminAccessToUser = async (userId: string) => { if (!licenseService.onPremFeatures?.instanceUserManagement) { throw new BadRequestError({ @@ -389,6 +401,7 @@ export const superAdminServiceFactory = ({ adminSignUp, getUsers, deleteUser, + getIdentities, getAdminSlackConfig, updateRootEncryptionStrategy, getConfiguredEncryptionStrategies, diff --git a/backend/src/services/super-admin/super-admin-types.ts b/backend/src/services/super-admin/super-admin-types.ts index d6de67e59..54a42c2ca 100644 --- a/backend/src/services/super-admin/super-admin-types.ts +++ b/backend/src/services/super-admin/super-admin-types.ts @@ -23,6 +23,12 @@ export type TAdminGetUsersDTO = { adminsOnly: boolean; }; +export type TAdminGetIdentitiesDTO = { + offset: number; + limit: number; + searchTerm: string; +}; + export enum LoginMethod { EMAIL = "email", GOOGLE = "google", diff --git a/frontend/src/hooks/api/admin/queries.ts b/frontend/src/hooks/api/admin/queries.ts index 12ae1cf64..7e2d60831 100644 --- a/frontend/src/hooks/api/admin/queries.ts +++ b/frontend/src/hooks/api/admin/queries.ts @@ -4,19 +4,24 @@ import { apiRequest } from "@app/config/request"; import { User } from "../types"; import { + AdminGetIdentitiesFilters, AdminGetUsersFilters, AdminSlackConfig, TGetServerRootKmsEncryptionDetails, TServerConfig } from "./types"; +import { Identity } from "@app/hooks/api/identities/types.ts"; export const adminStandaloneKeys = { - getUsers: "get-users" + getUsers: "get-users", + getIdentities: "get-identities" }; export const adminQueryKeys = { serverConfig: () => ["server-config"] as const, getUsers: (filters: AdminGetUsersFilters) => [adminStandaloneKeys.getUsers, { filters }] as const, + getIdentities: (filters: AdminGetIdentitiesFilters) => + [adminStandaloneKeys.getIdentities, { filters }] as const, getAdminSlackConfig: () => ["admin-slack-config"] as const, getServerEncryptionStrategies: () => ["server-encryption-strategies"] as const }; @@ -68,6 +73,28 @@ export const useAdminGetUsers = (filters: AdminGetUsersFilters) => { }); }; +export const useAdminGetIdentities = (filters: AdminGetIdentitiesFilters) => { + return useInfiniteQuery({ + initialPageParam: 0, + queryKey: adminQueryKeys.getIdentities(filters), + queryFn: async ({ pageParam }) => { + const { data } = await apiRequest.get<{ identities: Identity[] }>( + "/api/v1/admin/identity-management/identities", + { + params: { + ...filters, + offset: pageParam + } + } + ); + + return data.identities; + }, + getNextPageParam: (lastPage, pages) => + lastPage.length !== 0 ? pages.length * filters.limit : undefined + }); +}; + export const useGetAdminSlackConfig = () => { return useQuery({ queryKey: adminQueryKeys.getAdminSlackConfig(), diff --git a/frontend/src/hooks/api/admin/types.ts b/frontend/src/hooks/api/admin/types.ts index 80e3edc92..11f2cf44f 100644 --- a/frontend/src/hooks/api/admin/types.ts +++ b/frontend/src/hooks/api/admin/types.ts @@ -53,6 +53,11 @@ export type AdminGetUsersFilters = { adminsOnly: boolean; }; +export type AdminGetIdentitiesFilters = { + limit: number; + searchTerm: string; +}; + export type AdminSlackConfig = { clientId: string; clientSecret: string; diff --git a/frontend/src/pages/admin/OverviewPage/OverviewPage.tsx b/frontend/src/pages/admin/OverviewPage/OverviewPage.tsx index ad07b71ce..f48665653 100644 --- a/frontend/src/pages/admin/OverviewPage/OverviewPage.tsx +++ b/frontend/src/pages/admin/OverviewPage/OverviewPage.tsx @@ -34,6 +34,7 @@ import { EncryptionPanel } from "./components/EncryptionPanel"; import { IntegrationPanel } from "./components/IntegrationPanel"; import { RateLimitPanel } from "./components/RateLimitPanel"; import { UserPanel } from "./components/UserPanel"; +import { IdentityPanel } from "@app/pages/admin/OverviewPage/components/IdentityPanel.tsx"; enum TabSections { Settings = "settings", @@ -42,6 +43,7 @@ enum TabSections { RateLimit = "rate-limit", Integrations = "integrations", Users = "users", + Identities = "identities", Kmip = "kmip" } @@ -164,6 +166,7 @@ export const OverviewPage = () => { Rate Limit Integrations Users + Identities
@@ -409,6 +412,9 @@ export const OverviewPage = () => { + + +
)} diff --git a/frontend/src/pages/admin/OverviewPage/components/IdentityPanel.tsx b/frontend/src/pages/admin/OverviewPage/components/IdentityPanel.tsx new file mode 100644 index 000000000..4851db656 --- /dev/null +++ b/frontend/src/pages/admin/OverviewPage/components/IdentityPanel.tsx @@ -0,0 +1,95 @@ +import { useState } from "react"; +import { faMagnifyingGlass, faServer } from "@fortawesome/free-solid-svg-icons"; +import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; + +import { + Button, + EmptyState, + Input, + Table, + TableContainer, + TableSkeleton, + TBody, + Td, + Th, + THead, + Tr +} from "@app/components/v2"; +import { useDebounce } from "@app/hooks"; +import { useAdminGetIdentities } from "@app/hooks/api/admin/queries.ts"; + +const IdentityPanelTable = () => { + const [searchIdentityFilter, setSearchIdentityFilter] = useState(""); + const [debouncedSearchTerm] = useDebounce(searchIdentityFilter, 500); + + const { data, isPending, isFetchingNextPage, hasNextPage, fetchNextPage } = useAdminGetIdentities( + { + limit: 20, + searchTerm: debouncedSearchTerm + } + ); + + const isEmpty = !isPending && !data?.pages?.[0].length; + + return ( + <> +
+ setSearchIdentityFilter(e.target.value)} + leftIcon={} + placeholder="Search identities by name..." + className="flex-1" + /> +
+
+ +
Value - + {preSecret?.secretValueHidden ? ( + + ) : ( + + )} - + {postSecret?.secretValueHidden ? ( + + ) : ( + + )}
+ + + + + + + + {isPending && } + {!isPending && + data?.pages?.map((identities) => + identities.map(({ name, authMethod, id }) => ( + + + + + )) + )} + +
NameAuth Method
{name} + {authMethod || None} +
+ {!isPending && isEmpty && } +
+ {!isEmpty && ( + + )} +
+ + ); +}; + +export const IdentityPanel = () => ( +
+
+

Identities

+
+ +
+); diff --git a/frontend/src/pages/admin/OverviewPage/components/UserPanel.tsx b/frontend/src/pages/admin/OverviewPage/components/UserPanel.tsx index 6654439de..0e5173188 100644 --- a/frontend/src/pages/admin/OverviewPage/components/UserPanel.tsx +++ b/frontend/src/pages/admin/OverviewPage/components/UserPanel.tsx @@ -61,12 +61,12 @@ const UserPanelTable = ({ const [adminsOnly, setAdminsOnly] = useState(false); const { user } = useUser(); const userId = user?.id || ""; - const [debounedSearchTerm] = useDebounce(searchUserFilter, 500); + const [debouncedSearchTerm] = useDebounce(searchUserFilter, 500); const { subscription } = useSubscription(); const { data, isPending, isFetchingNextPage, hasNextPage, fetchNextPage } = useAdminGetUsers({ limit: 20, - searchTerm: debounedSearchTerm, + searchTerm: debouncedSearchTerm, adminsOnly }); From 39b7a4a11117a98467df9750d85e7467ef112e92 Mon Sep 17 00:00:00 2001 From: Mahyar Mirrashed Date: Tue, 11 Mar 2025 13:31:23 -0700 Subject: [PATCH 46/71] chore(nix): add python312 to list of dependencies --- flake.nix | 1 + 1 file changed, 1 insertion(+) diff --git a/flake.nix b/flake.nix index 9d995a551..094cfedc6 100644 --- a/flake.nix +++ b/flake.nix @@ -14,6 +14,7 @@ git lazygit + python312Full nodejs_20 nodePackages.prettier infisical From 627e17b3aee4a632881906282134deab639f7c8a Mon Sep 17 00:00:00 2001 From: Mahyar Mirrashed Date: Tue, 11 Mar 2025 14:10:08 -0700 Subject: [PATCH 47/71] fix(admin): return back auth method from schema too --- backend/src/server/routes/v1/admin-router.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/backend/src/server/routes/v1/admin-router.ts b/backend/src/server/routes/v1/admin-router.ts index a1c433650..bb62f2e1b 100644 --- a/backend/src/server/routes/v1/admin-router.ts +++ b/backend/src/server/routes/v1/admin-router.ts @@ -170,6 +170,7 @@ export const registerAdminRouter = async (server: FastifyZodProvider) => { 200: z.object({ identities: IdentitiesSchema.pick({ name: true, + authMethod: true, id: true }).array() }) From 7e14c589319118d52c4a6cdc7384203335a18db7 Mon Sep 17 00:00:00 2001 From: Scott Wilson Date: Tue, 11 Mar 2025 14:12:33 -0700 Subject: [PATCH 48/71] improvement: clarify databricks native integration behavior and suggest desingated scope for sync/native integration --- docs/integrations/cloud/databricks.mdx | 6 ++++++ docs/integrations/secret-syncs/databricks.mdx | 2 ++ docs/mint.json | 3 +-- .../SecretSyncDestinationFields/DatabricksSyncFields.tsx | 2 ++ .../DatabricksConfigurePage/DatabricksConfigurePage.tsx | 9 +++++++-- 5 files changed, 18 insertions(+), 4 deletions(-) diff --git a/docs/integrations/cloud/databricks.mdx b/docs/integrations/cloud/databricks.mdx index 7fee3acd3..1971190de 100644 --- a/docs/integrations/cloud/databricks.mdx +++ b/docs/integrations/cloud/databricks.mdx @@ -7,6 +7,12 @@ Prerequisites: - Set up and add secrets to [Infisical Cloud](https://app.infisical.com) + + When integrating with Databricks, Infisical is intended to be the source of truth for the secrets in the configured Databricks scope. + + Any secrets not present in Infisical will be removed from the specified scope. To prevent removal of secrets not managed by Infisical, Infisical recommends creating a designated secret scope for your integration. + + Obtain a Personal Access Token in **User Settings** > **Developer** > **Access Tokens**. diff --git a/docs/integrations/secret-syncs/databricks.mdx b/docs/integrations/secret-syncs/databricks.mdx index 8f305ecd6..148542708 100644 --- a/docs/integrations/secret-syncs/databricks.mdx +++ b/docs/integrations/secret-syncs/databricks.mdx @@ -34,6 +34,8 @@ description: "Learn how to configure a Databricks Sync for Infisical." You must create a secret scope in your Databricks workspace prior to configuration. Ensure your service principal has [Write permissions](https://docs.databricks.com/en/security/auth/access-control/index.html#secret-acls) for the specified secret scope. + + Infisical recommends creating a designated Databricks secret scope for your sync to prevent removal of secrets not managed by Infisical. 5. Configure the **Sync Options** to specify how secrets should be synced, then click **Next**. diff --git a/docs/mint.json b/docs/mint.json index f604ff310..bf40a9778 100644 --- a/docs/mint.json +++ b/docs/mint.json @@ -644,8 +644,7 @@ "api-reference/endpoints/oidc-auth/attach", "api-reference/endpoints/oidc-auth/retrieve", "api-reference/endpoints/oidc-auth/update", - "api-reference/endpoints/oidc-auth/revoke", - "integrations/frameworks/terraform-cloud" + "api-reference/endpoints/oidc-auth/revoke" ] }, { diff --git a/frontend/src/components/secret-syncs/forms/SecretSyncDestinationFields/DatabricksSyncFields.tsx b/frontend/src/components/secret-syncs/forms/SecretSyncDestinationFields/DatabricksSyncFields.tsx index c6cfff0e5..533d0d2cf 100644 --- a/frontend/src/components/secret-syncs/forms/SecretSyncDestinationFields/DatabricksSyncFields.tsx +++ b/frontend/src/components/secret-syncs/forms/SecretSyncDestinationFields/DatabricksSyncFields.tsx @@ -40,6 +40,8 @@ export const DatabricksSyncFields = () => { isError={Boolean(error)} errorText={error?.message} label="Secret Scope" + tooltipClassName="max-w-md" + tooltipText="Infisical recommends creating a designated Databricks secret scope for your sync to prevent removal of secrets not managed by Infisical." helperText={ { Pro Tip
- After creating an integration, your secrets will start syncing immediately. This might - cause an unexpected override of current secrets in Databricks with secrets from Infisical. + When integrating with Databricks, Infisical is intended to be the source of truth for the + secrets in the configured Databricks scope. + + + Any secrets not present in Infisical will be removed from the specified scope. To prevent + removal of secrets not managed by Infisical, Infisical recommends creating a designated + secret scope for your integration.
From 1a06b3e1f59b63c5c9161a70f6fdf84408d9eee3 Mon Sep 17 00:00:00 2001 From: Mahyar Mirrashed Date: Tue, 11 Mar 2025 14:30:04 -0700 Subject: [PATCH 49/71] fix(admin): stop returning auth method on table --- backend/src/server/routes/v1/admin-router.ts | 1 - .../admin/OverviewPage/components/IdentityPanel.tsx | 10 +++------- 2 files changed, 3 insertions(+), 8 deletions(-) diff --git a/backend/src/server/routes/v1/admin-router.ts b/backend/src/server/routes/v1/admin-router.ts index bb62f2e1b..a1c433650 100644 --- a/backend/src/server/routes/v1/admin-router.ts +++ b/backend/src/server/routes/v1/admin-router.ts @@ -170,7 +170,6 @@ export const registerAdminRouter = async (server: FastifyZodProvider) => { 200: z.object({ identities: IdentitiesSchema.pick({ name: true, - authMethod: true, id: true }).array() }) diff --git a/frontend/src/pages/admin/OverviewPage/components/IdentityPanel.tsx b/frontend/src/pages/admin/OverviewPage/components/IdentityPanel.tsx index 4851db656..3b89a011d 100644 --- a/frontend/src/pages/admin/OverviewPage/components/IdentityPanel.tsx +++ b/frontend/src/pages/admin/OverviewPage/components/IdentityPanel.tsx @@ -47,20 +47,16 @@ const IdentityPanelTable = () => { - - + {isPending && } {!isPending && data?.pages?.map((identities) => - identities.map(({ name, authMethod, id }) => ( + identities.map(({ name, id }) => ( - - + )) )} From 3887ce800bb45ce71e1271e3bcfe1974b43a236d Mon Sep 17 00:00:00 2001 From: Mahyar Mirrashed Date: Tue, 11 Mar 2025 14:34:14 -0700 Subject: [PATCH 50/71] refactor(admin): fix spelling for variable --- .../components/ServerAdminsPanel/ServerAdminsPanel.tsx | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/frontend/src/layouts/OrganizationLayout/components/ServerAdminsPanel/ServerAdminsPanel.tsx b/frontend/src/layouts/OrganizationLayout/components/ServerAdminsPanel/ServerAdminsPanel.tsx index c834daad8..82231c0e4 100644 --- a/frontend/src/layouts/OrganizationLayout/components/ServerAdminsPanel/ServerAdminsPanel.tsx +++ b/frontend/src/layouts/OrganizationLayout/components/ServerAdminsPanel/ServerAdminsPanel.tsx @@ -19,17 +19,17 @@ import { useGetOrgUsers } from "@app/hooks/api"; export const ServerAdminsPanel = () => { const [searchUserFilter, setSearchUserFilter] = useState(""); - const [debounedSearchTerm] = useDebounce(searchUserFilter, 500); + const [debouncedSearchTerm] = useDebounce(searchUserFilter, 500); const { currentOrg } = useOrganization(); const { data: orgUsers, isPending } = useGetOrgUsers(currentOrg?.id || ""); const adminUsers = orgUsers?.filter((orgUser) => { const isSuperAdmin = orgUser.user.superAdmin; - const matchesSearch = debounedSearchTerm - ? orgUser.user.email?.toLowerCase().includes(debounedSearchTerm.toLowerCase()) || - orgUser.user.firstName?.toLowerCase().includes(debounedSearchTerm.toLowerCase()) || - orgUser.user.lastName?.toLowerCase().includes(debounedSearchTerm.toLowerCase()) + const matchesSearch = debouncedSearchTerm + ? orgUser.user.email?.toLowerCase().includes(debouncedSearchTerm.toLowerCase()) || + orgUser.user.firstName?.toLowerCase().includes(debouncedSearchTerm.toLowerCase()) || + orgUser.user.lastName?.toLowerCase().includes(debouncedSearchTerm.toLowerCase()) : true; return isSuperAdmin && matchesSearch; }); From 387780aa942a00c8ddebef431458f27eab6dc501 Mon Sep 17 00:00:00 2001 From: Mahyar Mirrashed Date: Tue, 11 Mar 2025 14:44:22 -0700 Subject: [PATCH 51/71] fix(lint): remove file extension from imports JetBrains accidentally added these when I ran the auto-complete. Weird. --- frontend/src/hooks/api/admin/queries.ts | 2 +- frontend/src/pages/admin/OverviewPage/OverviewPage.tsx | 2 +- .../src/pages/admin/OverviewPage/components/IdentityPanel.tsx | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/frontend/src/hooks/api/admin/queries.ts b/frontend/src/hooks/api/admin/queries.ts index 7e2d60831..496990abe 100644 --- a/frontend/src/hooks/api/admin/queries.ts +++ b/frontend/src/hooks/api/admin/queries.ts @@ -10,7 +10,7 @@ import { TGetServerRootKmsEncryptionDetails, TServerConfig } from "./types"; -import { Identity } from "@app/hooks/api/identities/types.ts"; +import { Identity } from "@app/hooks/api/identities/types"; export const adminStandaloneKeys = { getUsers: "get-users", diff --git a/frontend/src/pages/admin/OverviewPage/OverviewPage.tsx b/frontend/src/pages/admin/OverviewPage/OverviewPage.tsx index f48665653..776c5e20f 100644 --- a/frontend/src/pages/admin/OverviewPage/OverviewPage.tsx +++ b/frontend/src/pages/admin/OverviewPage/OverviewPage.tsx @@ -34,7 +34,7 @@ import { EncryptionPanel } from "./components/EncryptionPanel"; import { IntegrationPanel } from "./components/IntegrationPanel"; import { RateLimitPanel } from "./components/RateLimitPanel"; import { UserPanel } from "./components/UserPanel"; -import { IdentityPanel } from "@app/pages/admin/OverviewPage/components/IdentityPanel.tsx"; +import { IdentityPanel } from "@app/pages/admin/OverviewPage/components/IdentityPanel"; enum TabSections { Settings = "settings", diff --git a/frontend/src/pages/admin/OverviewPage/components/IdentityPanel.tsx b/frontend/src/pages/admin/OverviewPage/components/IdentityPanel.tsx index 3b89a011d..ee2166a4e 100644 --- a/frontend/src/pages/admin/OverviewPage/components/IdentityPanel.tsx +++ b/frontend/src/pages/admin/OverviewPage/components/IdentityPanel.tsx @@ -16,7 +16,7 @@ import { Tr } from "@app/components/v2"; import { useDebounce } from "@app/hooks"; -import { useAdminGetIdentities } from "@app/hooks/api/admin/queries.ts"; +import { useAdminGetIdentities } from "@app/hooks/api/admin/queries"; const IdentityPanelTable = () => { const [searchIdentityFilter, setSearchIdentityFilter] = useState(""); From de5a4327457150638e84ed201a94b33cb5e1a8dd Mon Sep 17 00:00:00 2001 From: Mahyar Mirrashed Date: Tue, 11 Mar 2025 14:54:03 -0700 Subject: [PATCH 52/71] fix(lint): appease the linter There is a conflict between this and our Prettier configuration. --- backend/src/services/super-admin/super-admin-service.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/backend/src/services/super-admin/super-admin-service.ts b/backend/src/services/super-admin/super-admin-service.ts index e8e01a44f..ee3247632 100644 --- a/backend/src/services/super-admin/super-admin-service.ts +++ b/backend/src/services/super-admin/super-admin-service.ts @@ -146,8 +146,8 @@ export const superAdminServiceFactory = ({ const canServerAdminAccessAfterApply = data.enabledLoginMethods.some((loginMethod) => - loginMethodToAuthMethod[loginMethod as LoginMethod].some((authMethod) => - superAdminUser.authMethods?.includes(authMethod) + loginMethodToAuthMethod[loginMethod as LoginMethod].some( + (authMethod) => superAdminUser.authMethods?.includes(authMethod) ) ) || isUserSamlAccessEnabled || From 159c27ac6747da7f8733f63c2ae79920071b8e66 Mon Sep 17 00:00:00 2001 From: Maidul Islam Date: Tue, 11 Mar 2025 18:43:18 -0400 Subject: [PATCH 53/71] Add proper support for systemd There wasn't a great way to start the gateway with systemd so that it can run in the background and be managed by systemd. This pr addeds a install sub command that decouples install from running. The goal was so you can run something like this in your IaC: ```infisical gateway install --token=<> --domain=<> && systemctl start infisical-gateway``` --- cli/go.mod | 2 +- cli/go.sum | 10 ----- cli/packages/cmd/gateway.go | 65 +++++++++++++++++++++++++++------ cli/packages/gateway/systemd.go | 2 +- 4 files changed, 55 insertions(+), 24 deletions(-) diff --git a/cli/go.mod b/cli/go.mod index bdb8839d0..b2d0c83ad 100644 --- a/cli/go.mod +++ b/cli/go.mod @@ -20,6 +20,7 @@ require ( github.com/muesli/reflow v0.3.0 github.com/muesli/roff v0.1.0 github.com/petar-dambovaliev/aho-corasick v0.0.0-20211021192214-5ab2d9280aa9 + github.com/pion/dtls/v3 v3.0.4 github.com/pion/logging v0.2.3 github.com/pion/turn/v4 v4.0.0 github.com/posthog/posthog-go v0.0.0-20221221115252-24dfed35d71a @@ -90,7 +91,6 @@ require ( github.com/oklog/ulid v1.3.1 // indirect github.com/onsi/ginkgo/v2 v2.22.2 // indirect github.com/pelletier/go-toml v1.9.3 // indirect - github.com/pion/dtls/v3 v3.0.4 // indirect github.com/pion/randutil v0.1.0 // indirect github.com/pion/stun/v3 v3.0.0 // indirect github.com/pion/transport/v3 v3.0.7 // indirect diff --git a/cli/go.sum b/cli/go.sum index ab46476d7..ad70297bf 100644 --- a/cli/go.sum +++ b/cli/go.sum @@ -484,8 +484,6 @@ golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8U golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20211215165025-cf75a172585e/go.mod h1:P+XmwS30IXTQdn5tA2iutPOUgjI07+tq3H3K9MVA1s8= golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= -golang.org/x/crypto v0.35.0 h1:b15kiHdrGCHrP6LvwaQ3c03kgNhhiMgvlhxHQhmg2Xs= -golang.org/x/crypto v0.35.0/go.mod h1:dy7dXNW32cAb/6/PRuTNsix8T+vJAqvuIy5Bli/x0YQ= golang.org/x/crypto v0.36.0 h1:AnAEvhDddvBdpY+uR+MyHmuZzzNqXSe/GvuDeob5L34= golang.org/x/crypto v0.36.0/go.mod h1:Y4J0ReaxCR1IMaabaSMugxJES1EpwhBHhv2bDHklZvc= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= @@ -592,8 +590,6 @@ golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJ golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.11.0 h1:GGz8+XQP4FvTTrjZPzNKTMFtSXH80RAzG+5ghFPgK9w= -golang.org/x/sync v0.11.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sync v0.12.0 h1:MHc5BpPuC30uJk597Ri8TV3CNZcTLu6B6z4lJy+g6Jw= golang.org/x/sync v0.12.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= golang.org/x/sys v0.0.0-20180823144017-11551d06cbcc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= @@ -644,13 +640,9 @@ golang.org/x/sys v0.0.0-20210809222454-d867a43fc93e/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.0.0-20220310020820-b874c991c1a5/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.30.0 h1:QjkSwP/36a20jFYWkSue1YwXzLmsV5Gfq7Eiy72C1uc= -golang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.31.0 h1:ioabZlmFYtWhL+TRYpcnNlLwhyxaM9kWTDEmfnprqik= golang.org/x/sys v0.31.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= -golang.org/x/term v0.29.0 h1:L6pJp37ocefwRRtYPKSWOWzOtWSxVajvz2ldH/xi3iU= -golang.org/x/term v0.29.0/go.mod h1:6bl4lRlvVuDgSf3179VpIxBF0o10JUpXWOnI7nErv7s= golang.org/x/term v0.30.0 h1:PQ39fJZ+mfadBm0y5WlL4vlM7Sx1Hgf13sMIY2+QS9Y= golang.org/x/term v0.30.0/go.mod h1:NYYFdzHoI5wRh/h5tDMdMqCqPJZEuNqVR5xJLd/n67g= golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= @@ -662,8 +654,6 @@ golang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.5/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= -golang.org/x/text v0.22.0 h1:bofq7m3/HAFvbF51jz3Q9wLg3jkvSPuiZu/pD1XwgtM= -golang.org/x/text v0.22.0/go.mod h1:YRoo4H8PVmsu+E3Ou7cqLVH8oXWIHVoX0jqUWALQhfY= golang.org/x/text v0.23.0 h1:D71I7dUrlY+VX0gQShAThNGHFxZ13dGLBHQLVl1mJlY= golang.org/x/text v0.23.0/go.mod h1:/BLNzu4aZCJ1+kcD0DNRotWKage4q2rGVAg4o22unh4= golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= diff --git a/cli/packages/cmd/gateway.go b/cli/packages/cmd/gateway.go index d41c2c20e..ef4cdc944 100644 --- a/cli/packages/cmd/gateway.go +++ b/cli/packages/cmd/gateway.go @@ -4,7 +4,9 @@ import ( "context" "fmt" "os" + "os/exec" "os/signal" + "runtime" "syscall" "time" @@ -16,9 +18,11 @@ import ( ) var gatewayCmd = &cobra.Command{ - Example: `infisical gateway`, - Short: "Used to infisical gateway", Use: "gateway", + Short: "Run the Infisical gateway or manage its systemd service", + Long: "Run the Infisical gateway in the foreground or manage its systemd service installation. Use 'gateway install' to set up the systemd service.", + Example: `infisical gateway --token= + sudo infisical gateway install --token= --domain=`, DisableFlagsInUseLine: true, Args: cobra.NoArgs, Run: func(cmd *cobra.Command, args []string) { @@ -31,16 +35,6 @@ var gatewayCmd = &cobra.Command{ util.HandleError(fmt.Errorf("Token not found")) } - domain, err := cmd.Flags().GetString("domain") - if err != nil { - util.HandleError(err, "Unable to parse domain flag") - } - - // Try to install systemd service if possible - if err := gateway.InstallGatewaySystemdService(token.Token, domain); err != nil { - log.Warn().Msgf("Failed to install systemd service: %v", err) - } - Telemetry.CaptureEvent("cli-command:gateway", posthog.NewProperties().Set("version", util.CLI_VERSION)) sigCh := make(chan os.Signal, 1) @@ -110,6 +104,50 @@ var gatewayCmd = &cobra.Command{ }, } +var gatewayInstallCmd = &cobra.Command{ + Use: "install", + Short: "Install and enable systemd service for the gateway (requires sudo)", + Long: "Install and enable systemd service for the gateway. Must be run with sudo on Linux.", + Example: "sudo infisical gateway install --token= --domain=", + DisableFlagsInUseLine: true, + Args: cobra.NoArgs, + Run: func(cmd *cobra.Command, args []string) { + if runtime.GOOS != "linux" { + util.HandleError(fmt.Errorf("systemd service installation is only supported on Linux")) + } + + if os.Geteuid() != 0 { + util.HandleError(fmt.Errorf("systemd service installation requires root/sudo privileges")) + } + + token, err := util.GetInfisicalToken(cmd) + if err != nil { + util.HandleError(err, "Unable to parse flag") + } + + if token == nil { + util.HandleError(fmt.Errorf("Token not found")) + } + + domain, err := cmd.Flags().GetString("domain") + if err != nil { + util.HandleError(err, "Unable to parse domain flag") + } + + if err := gateway.InstallGatewaySystemdService(token.Token, domain); err != nil { + util.HandleError(err, "Failed to install systemd service") + } + + enableCmd := exec.Command("systemctl", "enable", "infisical-gateway") + if err := enableCmd.Run(); err != nil { + util.HandleError(err, "Failed to enable systemd service") + } + + log.Info().Msg("Successfully installed and enabled infisical-gateway service") + log.Info().Msg("To start the service, run: sudo systemctl start infisical-gateway") + }, +} + var gatewayRelayCmd = &cobra.Command{ Example: `infisical gateway relay`, Short: "Used to run infisical gateway relay", @@ -139,9 +177,12 @@ var gatewayRelayCmd = &cobra.Command{ func init() { gatewayCmd.Flags().String("token", "", "Connect with Infisical using machine identity access token") + gatewayInstallCmd.Flags().String("token", "", "Connect with Infisical using machine identity access token") + gatewayInstallCmd.Flags().String("domain", "", "Domain of your self-hosted Infisical instance") gatewayRelayCmd.Flags().String("config", "", "Relay config yaml file path") + gatewayCmd.AddCommand(gatewayInstallCmd) gatewayCmd.AddCommand(gatewayRelayCmd) rootCmd.AddCommand(gatewayCmd) } diff --git a/cli/packages/gateway/systemd.go b/cli/packages/gateway/systemd.go index 8183f3535..601cb9e90 100644 --- a/cli/packages/gateway/systemd.go +++ b/cli/packages/gateway/systemd.go @@ -17,7 +17,7 @@ After=network.target [Service] Type=simple EnvironmentFile=/etc/infisical/gateway.conf -ExecStart=/usr/local/bin/infisical gateway +ExecStart=infisical gateway Restart=on-failure InaccessibleDirectories=/home PrivateTmp=yes From 84cc7bcd6cbff089627dfd656aefd77653ded662 Mon Sep 17 00:00:00 2001 From: Maidul Islam Date: Tue, 11 Mar 2025 19:01:47 -0400 Subject: [PATCH 54/71] add docs + fix nit --- cli/packages/cmd/gateway.go | 2 +- docs/cli/commands/gateway.mdx | 107 ++++++++++++++++++++++++++++++++++ docs/mint.json | 1 + 3 files changed, 109 insertions(+), 1 deletion(-) create mode 100644 docs/cli/commands/gateway.mdx diff --git a/cli/packages/cmd/gateway.go b/cli/packages/cmd/gateway.go index ef4cdc944..81baf3910 100644 --- a/cli/packages/cmd/gateway.go +++ b/cli/packages/cmd/gateway.go @@ -28,7 +28,7 @@ var gatewayCmd = &cobra.Command{ Run: func(cmd *cobra.Command, args []string) { token, err := util.GetInfisicalToken(cmd) if err != nil { - util.HandleError(err, "Unable to parse flag") + util.HandleError(err, "Unable to parse token flag") } if token == nil { diff --git a/docs/cli/commands/gateway.mdx b/docs/cli/commands/gateway.mdx new file mode 100644 index 000000000..fd035f1fd --- /dev/null +++ b/docs/cli/commands/gateway.mdx @@ -0,0 +1,107 @@ +--- +title: "infisical gateway" +description: "Run the Infisical gateway or manage its systemd service" +--- + + + + ```bash + infisical gateway --token= + ``` + + + ```bash + sudo infisical gateway install --token= --domain= + ``` + + + +## Description + +Run the Infisical gateway in the foreground or manage its systemd service installation. The gateway allows secure communication between your self-hosted Infisical instance and client applications. + +## Subcommands & flags + + + Run the Infisical gateway in the foreground. The gateway will connect to the relay service and maintain a persistent connection. + + ```bash + infisical gateway --token= --domain= + ``` + + ### Flags + + + The machine identity access token to authenticate with Infisical. + + ```bash + # Example + infisical gateway --token= + ``` + + You may also expose the token to the CLI by setting the environment variable `INFISICAL_TOKEN` before executing the gateway command. + + + + Domain of your self-hosted Infisical instance. + + ```bash + # Example + sudo infisical gateway install --domain=https://app.your-domain.com + ``` + + + + + Install and enable the gateway as a systemd service. This command must be run with sudo on Linux. + + ```bash + sudo infisical gateway install --token= --domain= + ``` + + ### Requirements + - Must be run on Linux + - Must be run with root/sudo privileges + - Requires systemd + + ### Flags + + + The machine identity access token to authenticate with Infisical. + + ```bash + # Example + sudo infisical gateway install --token= + ``` + + You may also expose the token to the CLI by setting the environment variable `INFISICAL_TOKEN` before executing the install command. + + + + Domain of your self-hosted Infisical instance. + + ```bash + # Example + sudo infisical gateway install --domain=https://app.your-domain.com + ``` + + + ### Service Details + The systemd service is installed with secure defaults: + - Service file: `/etc/systemd/system/infisical-gateway.service` + - Config file: `/etc/infisical/gateway.conf` + - Runs with restricted privileges: + - InaccessibleDirectories=/home + - PrivateTmp=yes + - Resource limits configured for stability + - Automatically restarts on failure + - Enabled to start on boot + + After installation, manage the service with standard systemd commands: + ```bash + sudo systemctl start infisical-gateway # Start the service + sudo systemctl stop infisical-gateway # Stop the service + sudo systemctl status infisical-gateway # Check service status + sudo systemctl disable infisical-gateway # Disable auto-start on boot + ``` + diff --git a/docs/mint.json b/docs/mint.json index f604ff310..5c97790bf 100644 --- a/docs/mint.json +++ b/docs/mint.json @@ -339,6 +339,7 @@ "cli/commands/secrets", "cli/commands/dynamic-secrets", "cli/commands/ssh", + "cli/commands/gateway", "cli/commands/export", "cli/commands/token", "cli/commands/service-token", From 3c59f7f350913476d450ec0654d3e96f49a09dbe Mon Sep 17 00:00:00 2001 From: Maidul Islam Date: Tue, 11 Mar 2025 19:21:32 -0400 Subject: [PATCH 55/71] update deployment docs --- .../platform/gateways/overview.mdx | 61 ++++++++++++++----- 1 file changed, 47 insertions(+), 14 deletions(-) diff --git a/docs/documentation/platform/gateways/overview.mdx b/docs/documentation/platform/gateways/overview.mdx index d263dc278..3f19e6f8d 100644 --- a/docs/documentation/platform/gateways/overview.mdx +++ b/docs/documentation/platform/gateways/overview.mdx @@ -45,19 +45,53 @@ Once authenticated, the Gateway establishes a secure connection with Infisical t - Use the Infisical CLI to deploy the Gateway. You can log in with your machine identity and start the Gateway in one command. The example below demonstrates how to deploy the Gateway using the Universal Auth method: - ```bash - infisical gateway --token $(infisical login --method=universal-auth --client-id=<> --client-secret=<> --plain) - ``` - Alternatively, if you already have the token, use it directly with the `--token` flag: - ```bash - infisical gateway --token - ``` - Or set it as an environment variable: - ```bash - export INFISICAL_TOKEN= - infisical gateway - ``` + Use the Infisical CLI to deploy the Gateway. You can run it directly or install it as a systemd service for production: + + + + For production deployments on Linux, install the Gateway as a systemd service: + ```bash + sudo infisical gateway install --token --domain + sudo systemctl start infisical-gateway + ``` + This will install and start the Gateway as a secure systemd service that: + - Runs with restricted privileges: + - Runs as root user (required for secure token management) + - Restricted access to home directories + - Private temporary directory + - Automatically restarts on failure + - Starts on system boot + - Manages token and domain configuration securely in `/etc/infisical/gateway.conf` + + + The install command requires: + - Linux operating system + - Root/sudo privileges + - Systemd + + + + + For development or testing, you can run the Gateway directly. Log in with your machine identity and start the Gateway in one command: + ```bash + infisical gateway --token $(infisical login --method=universal-auth --client-id=<> --client-secret=<> --plain) + ``` + + Alternatively, if you already have the token, use it directly with the `--token` flag: + ```bash + infisical gateway --token + ``` + + Or set it as an environment variable: + ```bash + export INFISICAL_TOKEN= + infisical gateway + ``` + + + + For detailed information about the gateway command and its options, see the [gateway command documentation](/cli/commands/gateway). + Ensure the deployed Gateway has network access to the private resources you intend to connect with Infisical. @@ -78,4 +112,3 @@ Once authenticated, the Gateway establishes a secure connection with Infisical t Once added to a project, the Gateway becomes available for use by any feature that supports Gateways within that project. - From 38cf43176e46a4ab98aad263c4c2115c05126a80 Mon Sep 17 00:00:00 2001 From: Maidul Islam Date: Tue, 11 Mar 2025 20:13:39 -0400 Subject: [PATCH 56/71] add gateway diagram --- .../images/gateway-highlevel-diagram.png | Bin 0 -> 332077 bytes .../platform/gateways/overview.mdx | 2 ++ 2 files changed, 2 insertions(+) create mode 100644 docs/documentation/platform/gateways/images/gateway-highlevel-diagram.png diff --git a/docs/documentation/platform/gateways/images/gateway-highlevel-diagram.png b/docs/documentation/platform/gateways/images/gateway-highlevel-diagram.png new file mode 100644 index 0000000000000000000000000000000000000000..5f942bcf0c3049587e4dd117e5b337cecc1d803e GIT binary patch literal 332077 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

Vl7cl*;1 zWdlC8lfFW$Yr@Bt0rT#y-j`Zh3VUb@+S7R>x&}|R^!Bw#LNkiJm3@Lmxe6LZunVpd zNZCGg996kbm&tyn*3aS@!fM%EkAQDlgSZFy<28Pdy7b81JCgN90?HUF50ym3{?_JOoavbYO z&*JXDQ+M#|j>oh|^?^qh?Edl==l|+F`ju3EJ{$ZFcNd53Bx*JSSJkYgkha9tP_TH z!ebpzKXAfho$y#EJk|-1bs`in#){a{s~^n|6NYuduud4(3Bx*JSSJkYM2vOZG7JzQ zortmiKaH`TXu$ep*+^;wB(Gnuzt|sSeSQ_Srbq%5sPODLEmeN-VsA);FD?<69DTF9 z5pt1Q+X-bxwPvyAwen$B|G|0q2d^Qi8QzT!F>TaUS7qs5PL7bmh#^+@%BV{;^jHmT6k zJ&$!+al0^x7)L`M*0PRi#Zm6N75UA#~B zvOZvpRu6UMQ>p`y+1)Yhjbkge{Xu=EvjHN%OgC^UPAmE%X*o2|)soU^jiHkP%p3!{ z%fef+5YL)$9}!AD|Q2o znziG%3AgYV>q(W%U1YP0o{D*Z^6<8o9vL@a7g~RbJh<5g;Kt)<8RN&Sr;5zoh2eJY zhI0{yBx$cr2h3zqDBQ*CFxUWhW472}kc&j7GA$?kFIMY62jY4T=l|vcz;a*sTAOn} z;(oIL zUw{}LpDOMoL=b_%X7=XJ=d&cIBkLEYGA8_%A1nZ~S2PibL_G}|KK7M&k?h#0fy-b) zz|iJS_P}~-{(B4O^JM;~5iEeeSOPgk9^?H|<$@P?cIp{@?~1#>|5M+`A-k{`%@n)M z<92Qkmtvnhd1AJE2;feu(Y6~srkr_O&vW8`EYU+{04>3DXl+v0mu z7aF8ZnRexz+o`49F>hMDEu*)WOF;LejL@~2+s-+>OUX$=g zFn3=$Z`^$rCjk^qCQ`m9^LJiR-}Eha>p0GK2L6sWJy4d0IqAop$K8W{s%tfHF`Nl+ zp0jyfUJRm`AIJT29Dk~@D9w?51tCN^`N@Rb;syNdP^!1@XUw!M_Z77+}c#8zlq(Erpu8#E1(dF6#D zKio#5D$272b48_$e`We8zU;uF8g*o+;KA6HdlOcK#V+O&HE<>Au^D0~N$}f+v1B@U zs!;igRpMZ8BVw)^$0;vry&2HI)b!S!62>bc3eZ8HLANU;B&5^!%RFu3szw?sO? zwJ#Xzsf|dt*dGx7=+UD#%W2~@_a4Un!LAe+K+|7(%sds#`2vXVNSg*{hq>G1{%{$N zhZ}*xzW}12o56rJlROvb%H@7OobRz^bxKsR&3A zRU@>v+~xw!dZ|ee9pV5QbypX}jk)CL?egZb1jGgMs#blhjU_Y-hM&R+@drL5VCI?P z-&chP0=(v%nmypv2?1tG2=<5yv$!kq^6VuuPgbSaN$8|sJJays{$QiE=J+T+9V72b zqte*mM~~zR!VHVU@*b*$lT=#)kpnxJ7C3+f&zNs6H=SEzlc@`L|EFnJ4B5u%-NzQU z-j^I$ZU@Xm#Z`3~+1KW;Z`TR1g|59$xB(XW$K0Z~xkn7%8&m=8bp87NJyo+*Le<&f z;?T#J(n-PO`Y(#;`jqIjuG}kjJJ44Fr^mu8jWE6MvoFVNUvNsv29V|Vl-`fm10F8Y z7k;A0fA4`w?AB_pdKC#=Dtr3c?%M^((&yb@`xROCWeMx# z#XnylNht-Sw0=NVJdbyrY_2!6YQpIgh~J^@(bkL8ufXQw4ciBC@6U9*n2LAtVd&b~ z`Gtg>Oe@oQjUFE_iHhwgI>1`Id|V7K9led>r})OOzABYz{Xlb9>ARYB=DT+wP!{N? z+V!G(CBA|Q(YfS3*3WstS!VWHC=!@9UYr_ZQzFgw0~Y%wCSXz0!D||<)o~iK0-nAM z#PwDAbtVgs!hHMJk64I1kDDZ-gj$PJ z2E}UMoI|9XAI{w4Q*c4sFi7_dBYsF|rCoowL2+2w&|rv+y(F*#`98aRte|w!_?!W! zFhEFnbwzmM>eJ1o-e;b)*tS^b!8}QV=6A;JR8}rbu81yjpMBv^CFYhAPJ1{6WB0`;cJMJwtVTPTs;j%~d zgA%sjnQRT;IZltMu^lA3yweW_!m|A|mYd5#Tw<7)lq&64alk&kf%vh*5nP)V4HaF zTSbmdq3qViHA=y`wlaJNr!Yqd_VYl15T)br%pe!_n6KnPTMZncF zZ=kp1DzF;WDus`eJT4J|nnwq&8S7`6Lrie{+k3msQ6Lg$3fD;XVigMvmfcI-R2z{H z-uH)N+ZjB#7vR8{hp3E?F3vB-fZKBw7!#{Ey#U^+g`2HA-^8J2xis*Km27m@G4lr# zA>ziF!EJ9Nzxm--2dj<-c6WF8^@S_OCuuTM9*<~ad%Ic!zE)nEK9>ckG4>LIjnse8wbl?PGtVjNkq@|^^H?zSR z-iP5;R#xsiHl)4|{|-R`Td&`_A3%Bl_y0i}tvjWQ1lECgnIk zZI-jsymdAGmv7v-LE?!6&+#?jc>qU-3?bSm>=ZZ@4s3{~7#NH%j~rKb9VWd#WqEs1>w;A57dTh2U-r!4%A zL>IenL37sV$(1K%I4_uo#Hj;T)v_!9?>*}u+&nlF`2C5cappm{Z!flkoLSAOooZ@o zn}Ga0@BW?u(uxy|P#`@toMU#>KUr%1$~YUBXBpPdjeb$pJzK}+|u-N0Y&Vb^=3!Dl*X-TvE%xkxAEtT^$NQ` zTw?*E`%67PEd%!Y&2`<|q-(SNbbt*!58}jo`d1rMf4e4!h&@uify0yF+;xz!3p?uiK69hf*d?$i?TQ%Jth zT@l*{JOiSOa0EM~-rUx$j2LNF#E&d^;E5pc0;;)g1V+B3ScoN6zF^&RP3^HB`UXZQa9p~pk0@(>cyeOZsfL2suh|oRIf7n5_);_LvM)Q%uN7okt0RhHx&8_2cmbk9P_k69*80h-Q z4Sv{)OoL(#>l1R`tMBqBiSdGN!L=Ynv#*s46C;3PII%$X=y72%J=*(va>H6^Hpj9Yq zHSpmI=j>o*Jg=Xo$?xz1qG5w z^Nsm5wh^-F5D_}^3^qaBKc@4dj`R9u+2WPg>frbS=W~(#hWb0|2_MGgomb)*l#TMJ+C#jA!pkLE+E@Cf1kCDa9CaAmXrt?YT%rVDM4;d)4(l%;(f;cM$I9| zE3AC!Zp-&cbjY0;B!I7&muUIbpy*KvKxQp*7GDf(?GfD-oGEj_d2`-6bN$lH%*@-- z(a~!Is)ffv9d>vV{Wn`zKw-%>2w-G@f2<9@P9%8k@8VAxCiXQaL_+ut;(XT{6awQ1 z3n5X8Jfi7bqQPMS6?Wh}UV(g`RTO;=kAfyPgmpIG9{>4|^|C_3_k0_V@$tAW$3LIB zQP3#%^TmObHDa-c8JNWh@X9GH>%m)#A)>7T=jbZ^?D;(ZzB|ATz%8J6^YqMs2zTd@ z1Y}F1^km6Jz^`T<|;|PtzKgJA%Ry{9E~rLQ`ig zoR7HLuN&w4Pr^ror?BhwvaBtHnOH=rT$ti1qN#By+(c|VHja2VcGs?56~Hm=1W}c= zx%$4Qt+S3mwB{JX7!EZ{xQ2_zYCufs3>anY=F+&mYHHrihj;&i)Aa8Iz{sEbVBdWd z*Q}_xD&pMU-i|q4 zTh4)UG(cQGfBb*lOjsC24RU!9)%IqqZ8)LG_B=TFFpz%!Cd`2+$FGPP7QFy}h*Ko7RVuN&;%XaQh8Av`at4m~f0s$O3+~jJ z+~VZU7y}4t`I==eH-(Ir41|BN^gVC4oW+GAykun~Pw+`u@%E4#ii8N%nT|pgh|`x} zP61~T*+>NBqzxL=HrUrve8*`_DB!fyArLT^xb4n}o_H^cf_~sgoJ2Iyhyokv!H-89 zm2H5;utIL1D8hzSgjL&_<_pvz*;rH!*$riM3ky@X!rAHAki%`)ec0w(-u6hSYu{iW zFl`)eL&Ve-3JVyO_V4F_!|$4m6PNVOWxH=ft~})V1&OY>oCS&JE!R)Rcmuq?+ZC~S zV`HZHg+}KKEBJIy&KehcFshwVQ2ETybYSfgT0#6`5Tk$iyC3c#TIU9p;|MZ%92T7% zTz`2E&OpKACD+FBnLMsmXy0i@P96{Ud?X0~_Zf^sRD|vtT1@AE89A;2?)`V|(x#42 z6q76$ydWHTna@13#;LfL98-9J3O0J#=Z#ZM9{D7hP}B)Uolw*Xi~8NK{Ff67ok|lH zb>irsIQl1!{(texMqIvOTQuRLPWY%3m#K-%6pdN<^Hl4PwrJuqHF4{oxb;tjr~bu; zP+OIW@YF)ihUsnh}> literal 0 HcmV?d00001 diff --git a/docs/documentation/platform/gateways/overview.mdx b/docs/documentation/platform/gateways/overview.mdx index 3f19e6f8d..02d9c863a 100644 --- a/docs/documentation/platform/gateways/overview.mdx +++ b/docs/documentation/platform/gateways/overview.mdx @@ -4,6 +4,8 @@ sidebarTitle: "Overview" description: "How to access private network resources from Infisical" --- +![Alt text](/documentation/platform/gateways/images/gateway-highlevel-diagram.png) + The Infisical Gateway provides secure access to private resources within your network without needing direct inbound connections to your environment. This method keeps your resources fully protected from external access while enabling Infisical to securely interact with resources like databases. Common use cases include generating dynamic credentials or rotating credentials for private databases. From eda9ed257e6cb80ec4d50e2dd1d1b32327f6c2fe Mon Sep 17 00:00:00 2001 From: Daniel Hougaard Date: Wed, 12 Mar 2025 07:31:25 +0400 Subject: [PATCH 57/71] docs: secret scanning --- .../platform/secret-scanning.mdx | 68 ++++++++++++++++++ .../secret-scanning/exposed-secret.png | Bin 0 -> 141436 bytes .../secret-scanning/needs-attention.png | Bin 0 -> 163751 bytes .../platform/secret-scanning/overview.png | Bin 0 -> 117811 bytes docs/mint.json | 3 +- 5 files changed, 70 insertions(+), 1 deletion(-) create mode 100644 docs/documentation/platform/secret-scanning.mdx create mode 100644 docs/images/platform/secret-scanning/exposed-secret.png create mode 100644 docs/images/platform/secret-scanning/needs-attention.png create mode 100644 docs/images/platform/secret-scanning/overview.png diff --git a/docs/documentation/platform/secret-scanning.mdx b/docs/documentation/platform/secret-scanning.mdx new file mode 100644 index 000000000..7d885f6ca --- /dev/null +++ b/docs/documentation/platform/secret-scanning.mdx @@ -0,0 +1,68 @@ +--- +title: 'Secret Scanning' +description: "Scan and prevent secret leaks in your code repositories" +--- + +The Infisical Secret Scanner allows you to keep an overview and stay alert of exposed secrets across your entire GitHub organization and repositories. + +The Infisical Secret Scanner helps you detect exposed secrets that have already made it into your remote repositories. To further enhance security, we recommend you also use our [CLI Secret Scanner](/cli/scanning-overview#automatically-scan-changes-before-you-commit) to scan for exposed secrets prior to pushing your changes. + +## Code Scanning + +![Scanning Overview](/images/platform/secret-scanning/overview.png) + +Secret scans are built on event-driven architecture. This means that every time a push is made to one of your selected repositories, Infisical will scan the modified files for any exposed secrets. + +If one or more exposed secrets are detected, it will be displayed in your Infisical dashboard. An exposed secret is known as a **"Risk"**. Each risk has the following data associated with it: +- **Date**: When the risk was first detected. +- **Secret Type**: Which type of secret was detected. +- **Info**: Information about the secret, such as the repository, file name, and the committer who made the change. + +Once an exposed secret is detected, all organization admins will be sent an e-mail notification containing details about the exposed secret. + + + Each risk also contains a "View Exposed Secret" button, which will take you directly to the GitHub commit and to the line where the secret was exposed. + + + + +![Exposed Secret](/images/platform/secret-scanning/exposed-secret.png) + + +## Responding to Exposed Secrets + +After an exposed secret is detected, it will be marked as `Needs Attention`. When there are risks marked as needs attention, it's important to address them as soon as possible. + +You can mark the risk as `Resolved` by changing the status to one of the following states: +- **This Is a False Positive**: The secret was not exposed, but was detected by the scanner. +- **I Have Rotated The Secret**: The secret was exposed, but it has now been removed. +- **No Rotate Needed**: You are choosing to ignore this risk. You may choose to do this if the risk is non-sensitive or otherwise not a security risk. + +![Needs Attention](/images/platform/secret-scanning/needs-attention.png) + + + + +## Ignoring Known Secrets +If you're intentionally committing a test secret that the secret scanner might flag, you can instruct Infisical to overlook that secret with the methods listed below. + +### infisical-scan:ignore + +To ignore a secret contained in line of code, simply add `infisical-scan:ignore ` at the end of the line as comment in the given programming. + +```js example.js +function helloWorld() { + console.log("8dyfuiRyq=vVc3RRr_edRk-fK__JItpZ"); // infisical-scan:ignore +} +``` + +### .infisicalignore +An alternative method to exclude specific findings involves creating a .infisicalignore file at your repository's root. +You can then add the fingerprints of the findings you wish to exclude. The [Infisical scan](/cli/scanning-overview) report provides a unique Fingerprint for each secret found. +By incorporating these Fingerprints into the .infisicalignore file, Infisical will skip the corresponding secret findings in subsequent scans. + +```.ignore .infisicalignore +bea0ff6e05a4de73a5db625d4ae181a015b50855:frontend/components/utilities/attemptLogin.js:stripe-access-token:147 +bea0ff6e05a4de73a5db625d4ae181a015b50855:backend/src/json/integrations.json:generic-api-key:5 +1961b92340e5d2613acae528b886c842427ce5d0:frontend/components/utilities/attemptLogin.js:stripe-access-token:148 +``` diff --git a/docs/images/platform/secret-scanning/exposed-secret.png b/docs/images/platform/secret-scanning/exposed-secret.png new file mode 100644 index 0000000000000000000000000000000000000000..72776529210b5cef52562fd1135609c320e5096d GIT binary patch literal 141436 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�w#8akinJ$p-AH{Y9R) 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 literal 0 HcmV?d00001 diff --git a/docs/images/platform/secret-scanning/needs-attention.png b/docs/images/platform/secret-scanning/needs-attention.png new file mode 100644 index 0000000000000000000000000000000000000000..6ac664ead3cf5b6a26ab33be7f627a645d01c388 GIT binary patch literal 163751 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%0Dc;CM;6xFV_*9ev5~xJ!e(sKq#mGJcGE| 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

    VR`=?^ey2A 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 E06OHlxc~qF literal 0 HcmV?d00001 diff --git a/docs/images/platform/secret-scanning/overview.png b/docs/images/platform/secret-scanning/overview.png new file mode 100644 index 0000000000000000000000000000000000000000..19981fa11987dd44752c45d2c23a9d112ae7e441 GIT binary patch literal 117811 zcmagGbyQSQ+diy_N=Qj5NGly8GBhYsN(my}E!{PQbcukVAV^5}kVAL3ptOcm1GI=!YBFf8GK-sDKCXne>}C zZUN6X|C~$5`k(*a_D;X`Kd-S)|GZIOpJV67jTbjwOTAR{xQR-+UGu&Z)4@LmdH%EElONQ)2SjCaI_noK!ed+~V7wjeD*7A!G5MHyLA;oX^HhDJ?+I+C#EK3;o7aQbtG)tB8* zv9^Dl6@ulUkl5J~bRpsY0(pC?WeDXu*duY)rnlaZ2YImBLKxt(-aZ_tSgE9hW@D<^ zpau!7@6+GB_F(P9F6Y#qWYlkjQ|rHf9OGp~ zX@WcQ9K@a~5$dq8?XErNacE_}cI3cCkqKdG zh4}`v7Tjhm6bU`C4#-E##@3r#R7Eu5f>S7`Wc7Wd9G*GBy2fNsZL*bgk%#=4N|O2y zDBCpgB>|^&80FRs#x(F+%L9`VA?UA|)DDANb`(pmN_JG;$F2Lod+|fIc zF*Q5TZIyx9z znomLL1(JnGeB0t;S3)%N_%z-IVCQL~>C*7Q_Gx0rb}?+Uu*8o9a|LNLK2t@k$1kou zbZ+?TI-eB$vLY#0i4rG^oaNse$FHb`>#W%IMUL5~G!|!8(K*oaQs6?NCDLcw#TmS7 zErixAD4#6*71$163bBoK1@rZ5gx;S~h935)iP)rFjfnRp1toPvPKj-7HN#z=(ws5@ z|4AvG@tm%q5+dX;EeWyygsrKiBN#NKr;TAszr?829q$ zr^NhV^x-##g~?nF&QBiIg6D)cP+WsSQy9609>@ZVv;ouLKiq{OKK4ZFlZI-4MRYTz@BWtv_EV`5*Tuh4g)o-g#3l%fu+^5swThu{dqw{mNy9jQ>9t;Jl zFz`eXTCu+KJJHwoXfj|}V5^qBc3C_wt;HpiJ>+0C;=-HUgm)2_;foY9VE|7NATuH3 zMin0E`5@A&nYJ#2>9E)t3oN0mhuvhulgETnV_FrM_)0a#U*_u)WCF^4I!O#xT(1II zQC#as02d=Cn=UBKA3zZcv6QV!g$EveSL}N#&Qt!L2Nz7^b(5_98g@uwfUA2R)q%k~%C_&5n@4X}+B1SnGl*i(ck^ z=C_|5KPC^pBS^pL`7UfhWUDe$4Wh63<{PIL+1qv6J!zTjH(w85)R)FZc~JWT_ZCbv2x zb02u#0sXdG{%*(ll%sO++@WWSadV=OB{$8tAfEkwU$XDj#beLS{9LoaB>twI1)Jx# z(^8k`2Rz@O8cBAUMT_p^2%$<}U06SYY!Hc`ycIupc-tj(HTfbLRGe)$*XYXAe07Ow zuHuCKeP8%rJ@+qC$jFWUd^=Sx{Rqq9W6?~$S+-*RsA{A6H&b$lw=3GY6^dz4^$-GW zHl8EQ7=8;h&5&9=iPFS3t%QJ=Z;U_W0AF52pbUn_Ej#!bO6S_mG+8y`x-puay}+m3 zJFk?*u@yAzd4TBOs$O8DBY0TQBtdo9WK*#PeP%bS;bb`jnm(RA;OUvIJ;*vP z*>+{tdHCByKJ&gvo~FmdEXQ;11{?DiQbHS9+xw`B3y=qdny<_UgD!X3`0^>hzCGmb zn|imN*WQmhS)v2VIiry7UwSp4u!!rP2v zxuPd`FJUF;P>&C#^L~M_X}&=3Z}#)N`Tor8hoz8;+pOc=53}p^o~zlED>ci?_Y#KO z>*=KztR@OJ&R&Jw8$ovUCi0q??k#ny)*YUOE!-j4>>rV(d$QV};4X<$rK}a!6u&xm zZg$5h4(pqn3||6%Ai3f4ELp#(5s_NIg3qGXezyyJ+@z`w(dV*A^$Zy=X-sIMwHW%r zrYwFMU!+^Bx?Qsj@+WS*K(DSeVYVGFu1KE}%5P0q9l88&IX0WCbDj2GjuJ4NESlOD zrtlp5fqq}>WG1xyY3!)hA4kIHEPDTBSvgNaib%j}T#i}k$M2HDd{Z;C-e`%Tm{3|T zLFMHzQM-{P;@Y!KYZo5F-HqHdwp$Gpjww8ihvf%XRiEt}k0#k0H1buY4c@hf_;B8R zqAp)~Ho`3?a4Q$9h;LQmIL!@JVL; zjx9W?al-%9+?G*w0(M(8`(hMP0T{f;z>OlmCHznTjrA``iKRev0+y9W8!akTR;Ed&JWj~gq zNQyJZ3PzQMMpRffAJ4x~ton#!xRNjZVFp@UY42Af1A6sQufn{zLPGCg)A$h#f1&yO zne0367pSd~Wzl>MdpZ`X=-5~$+1G$;TKyu=9a7tHFa$2Qnqb$0+SJ~Au=_bOBen2i zF>q^Z+8UEbC#u1^+!Ym<=69(zBeaE&rZkux6O%PNOzMhGqxVR0+4A}^#NT?_a*HVb zW#FAcxK5^w>y+Qs#erZ`)r*cQtUJXz)lRYFeAvcz;rE}coGjN*mmY4q=BpKJzn3=p zRPAEGNuT=+-70xaw2KkIqKHpg%vvCQ(!spTdHleYjW*0H|ort@3sHt;$nuSF*nW~24N zr{YrnLrMJd!xqDrudSsFGwAzVvPs!0%HOODsS>9D?$Vz& zbdxXb{cffEu<+%Hd_1sqW8qq;`}iLx~3p_!Hr%1<4vXb zm8~Hv(_|!cU5lkel0LX}dad+G!kH-NoI^3o&0}LqLe-cDO-{ZLQ6hWWP+%?CkdHHq zfArZ4l2hrJ&bUv<-Uf6(>zS*+x+Kt@_P^tV zDoaVhsjX-L97*}GxGO*7(HHqiKI{r0V&jgQy=b8k_YQKeby{Vw#cYGmg{a1IvKhli zKq^NuuafHdkw5#T%k2tXqp0}zfA}2_3M_m%Uhp#@6kex~Y6xDS6&r$$6UVVA54jpR zuMJGBugtkG=-G0p=aP;~?I=%O}Pmy}73MC;ifTl8VA+i*3rj-FVjd{VyTN8Y6d8Loxj`p#q% zV6LzFq*&4!ecB5l=mbi=5_KSZDUd5p;$OkAzHbXTAwYb@Wdy6x``U-W3*+W)b@Ydm zvTUjpsIk?au2Fv#!8YLNL0aF2aI7>`so@d#IJ5uMv}5C<;5vJ+UL+U35J}177z=O4 zW%N0CN6*dl1e*{CDlqGilB4wWCI47cjLB&iH3XI&BjIs*?#X?-bGwXs^9kbeV0fXd z7+9BlZY+y~XP1{?F&?S(Oq<3db?xPw-ciLn*A|cTvRDPPA(HVCId&10;P#E!(um}I z+me7P*Jh=AZTbvnR^d|mGk!JAzi0zMZ%|3EmL(?%r3cRE$GIdD0?2Ok>y2;CgJH1< zj96x^Tl9uYWu3=mXTtt+-wb0n&;E)27=k?^4H`0^RRpdkecxd{UXy>mYBycQo1NaF zpJi=1SNB!d?}A`k#yB6xVg;VUXYqM0zLPcv2fb!Mz!zd%-E_c71vHB(waysi~#b_yNr zpsV8rwhi%=7Tb)bC>qa0UUm@3ZdUHIyTG>ORpYT(`l*#OmeIjr*ARo1{xs5woL*UU z(L?AcKF%zv|K==mpTqPtLTyMhIc}j2uvifW*icr&fgF$qNqgTA>Pi})LzN^GSkRP8 zrVNsxhr1>{bUv@xtpD3+^^W3bt+U0Pu+vkf6s&!6CV?GKL`or}(}NKzeI_Stri|kt z(z-W5h&&9jP0*W^KNEDE&CsRf)@7~Vf+o{bI(=sqlUXKicgesS;<6;M&j>4cKZXl;|KPjgwnaFW zhxD^0)eXDGrZ=!2I-h7~6-5N7nDPWQu%^0I=-1&z5ZiDSz_z+NISGEI^)3jbD1?HxDX)CiRoom~(-XSSh zO2MxpS{|0T3V;GWoq$cFnzq5; zrO{^ns%*3&gfyFsp9*{Ob5k>fJeyC~k`p7N#i=re-|@NMY5;Oen$@ z2ZOL}>FK*!_^oAa;kz}l)EOZv3N^5}bp~&aRdL#(fq9StI=kcJ%^e&&>>8lT$^RwN z)9B_g21U)nU;RA?5F%lPTC7?jt5jK^D#-=Mm;`*N>&;|F#g||Vkk)lOl6oA>>q$3k zTnXz(kpIQ^QhJ_?+(r7i^VqFXe4FihHN#b9o6pteLV4S@7|4cU)L|Fr z2g6u5L3(T&b|R-^b3xp5f*C!$b{V#Hr@c+%iVJ;p@l;cpsf$FXL0pQjoVbS=W*P54 zFmN155#g~_vr)nXT$P*kXX#1Ic_n8MVMGu=DxS0Kmm1L_?^jt(+|MfZ0Mg);FfrqI zobqXZM@&jPN#3>!=w<0ob3|b;5|DfEpfHi5Zdcx(OQ)NoCCh{#5_;q<;=J%x^Qki~ zf?}IC){sX+FJEsJeZGgE{5#q13dQ7qh2>u{EG}=qA1Y@udyi0Mt>*iTqQKF_qu#6A zcN%2=Uf#nsvAo7WQn27BpFVbm5#3aR2Rl1iLv)U#5B>Hu_^Ef#zC_*kzVOYw_}yh& z<|Bzi_-1r||GcX>0M`b<+s={Lm&eVy&kcC(d_(gP%Hj{XGiCh>1TYwwK>O|2f5leQ z!|wmN*dWMJK5J69-1LtJpQsP&IWJv{QEKAy;@r$Ez|ckDav!azJxg<8@L}0MaNC%cZ&ELA8%Pa=+~xko zXkI)Ssg(7mha+sF@RB6UQLy0Pvnu3LUA4;Vp(i5hiDydcAR&147N(08Z3eCoPRs^b zLFeZg6L0Z>{%)QBnk-$8RE~Nv60Q;XF{-oG&QJ}Nx5%*)y#RaNt&~q)}`Dh}RpIN^7 z`%eAu`VLO|IA6*2a0BlpnMxV4LKW0ftMI__b@LT;DA!(ar;^Tu?+UW>Na(02qO7Um z$H|2ZhSx}AOgNqA4U$*}U9ixi58){9?@y}yJN)rqtkAzriQYDd2V~1wFyX~OO&{ZX zKH@Z-?y_NQd{;Pj&1mh@S6@*G0qQkTHQgNDf9&s&14<-?XAWLhYy1a@)w%y4+ptl~ zoNY?mzX;mVxp=qnsOhrI-_%TNpt#3uOydnwW}&yGI^oT5zuO??yPZx(H@#w#`k~0^ z*-p*Jn@K*;o(UR6j$RXT|KAh{{rQq&B6l@l}JB5j`mRQ1a1}-87Y{z zQ9J6HYQ3G7=f$eQ!MbhZcj--S0wWDzsucnolTs#QqpDzFv&erZJq z$dHM3IO~^5*EOV7sCSLR)(qX(y4N~RPPyD7y~-8py(>GH3WTd3C16(ybrXX+0~F67 z?Vu8}XL1HnbN}u?*Xug*e>^ld-1S0ndcXWgh19Jta*Da)L-|fyyuH+(=EN0mLjjWer8w+_eL|EYlj|aW5Vot6=V}jtwK8X?;4l=h_ z3~&^G0c=Vus+RNbp7p=TdTBN>;Br2B98sF9l;)9Ql~zwF5X|v(%o`d|R9N$OJ;QBD z{xGBa?O%ojvH5ETL5j(8#DDcnv3>#R`RXPg*iVW}3Md9awr@$6a`g7huUs|?7EOBp zgOs87&rocUJt-)q){T_D$RU-6m5}tZKYVZ%saUji9M9Y)FsE(^fgw-~6f;%X6=rnT zlwJQR6Ug3Ub(T3sCAvzoT8U+dhh+)h$xJ^Nw6%3W?|^dat6UB2v}Ovev(O*-p)1qs zMP{&!14*6#gL05g{D{{S<`G4+9dIaGaB(D^e?Npkf3^N{tE==S2Y(x-N+sN9dMZSW%)_;9p9~9>%X`h z@C&l9w2#DrS`%b`tPWy{gI&*xJl|j=e=`k6wszW#;Elead*eG>|I2Ym^MKkB(~1{q472C7)&8%^#~ z)-!+0A(tM*X)w2k(IzK!(Jpb>sQs>bT{7tjZ~`(0YUP8G-cvP`4^gg-hMb^KmVa%~ zw?ViajNU$vi}Z+tMzV-Ml}Tk4Mb|{36wi;^Z0>*~5373gnu{tlj3(EDegBYMzH>c8 zkc7xaz1-sNf3Pwiv7y*lNG9*Oi5E`?)fpZ6K@^AiPpOq6x{Blx*&7E3ZM>BX^L4^d z+8@$B6c39oZ(-nGv5iwL2_*?#^K1VHi=;$PyJ)RJikoRvaF=>iI3h1NEW$Xpj%o_o z)izGdUMYiSLmr{aJU3ZaNrXI2o=huu_2>i4ZOvs1; z*QC&HQMi2n2P^uRIC?jV(SVC_vIM8ik~3hE(UcTD#puR#SKJu3CVgRUA6xG$W|523 z+PeG0G_px<{0Q^BSp^6}h;~9BT(Ha;)!WH`%q$Q~XPr-EG?1AlbDp4^f{*Wq7aAqt zmP{gUYq*??XOMl|pro7-1~H-%xV8-a}Mefgx(XP$QcE^Q?Xxu;dr{|D_4y@3_X z9?Iikb;ylvA?D}`S2{YwG0^r*u|I42GwsM2jyz#(e-*bCqw}nm_@=+NLP{Cy$XmNuRKJlh!}| z6$wkJcqoKaCN7`4%{nkFBEuC>C`dY3k=-%>zPX2R5{FrNqIQ-xIs1%S`L7G1*wAqopwvH_)epi)Mrty=%T!60vCZ{K0PErq| zU0WfzFfXr`(<2X>!zOnrGq0pO*nugcx+l&4Noh-e|G2hWm8To&EnOU1|7?L$|4EQq ze4aY>z(~osrfHctqhKk_GgfR`n8-P^{cJ!Y=L|Z?r$ZBw5yZPj$ki(x)mW`K_?zi^ zPY4JUK44KTT9cHE8w-%!#5Yo7a-GYzvRzi$LOjGH)yZ_GD@*=C8Kr|=qF8-lnw(HN z3^2i`p)1yy3APd6XHyv0seguWMuqg>&L=s6i`X#MZFSjKFG0)$)3Yz04m}@Mct`lE za9j=^XWQ7~H$bF(9r^Uq37EiC~j zB#SKqCXZnPE8Zf3V_fy<{vBsOVDJBE7jRpoV4$8z!iQCnytejqgxEt=++7kTP$jL!301D8wdd_X;annZpHLl4}%wO_*J52V3 zQ5d#GjxZ_nI-+JTe za(VzFH9k7z-d9Kyl>r>uD2=*{Wi|t{%`)qMwbF8nFa|(oUjt-;DH@e#x&&M3SszL3 zYdMyP0T?kG5?DPO&r+Q=Mh+s+^KGd!vj1>HqVFaFwSC{}dh*`~d^v_4v87d;gzw@i zVCI}rFi99H{mcCEt()YGHjlIH!o)f>1}cQ1>TP~DoO2D{aHN;N4-j%iNbKF5!UugX ze+3@*v$41jyV5bqe*U7A5*_D?oLse%4{RWT*DKMSTJB=Xx1cj?f<{N16Hb!Vai;K6 zWv5CE50T`Gp5PTFn^t*Nx5Hj`!s}+=VT?2Ax>bSq`D2btDUMb#$$RTio$lyhYe|&! zOIv7O6-U7Y{gd?qy*7P-r2exrm_QW8BxfIKBBqeORZLaxRh^n$lIU! z^v`^G(PN_FjqpQ;(B}zkam{gkUynJT!%AyG&#b%DzDpdl_@V0U8@=ssGh%WxLlNxg zHb(#%TPYSe>sJ)eP1te0hLSX*2D1oFAZU^{jHgf>DOR*$diVl(&nd?60{&qbisR$ z8THFcjQuHqnBR6SSIc{=A42f(2kLS-#U3ChRiQRoekcGzKQ;bU^nvuHDSw7)oY`u@ z`+b;!-;?^IbAZVDV#ksiU2Ix1)*)5t?a}i?cnUXU`Y% zNP$b}_+>68_WR>Sw)$XFnb-?cQE2~OdApKG|29}{qHdW)wjUTjMMdEH%qo1SOgwcZ zx<$`@F4nd!y_K$as9tTrt6^BF2hH8c6D>=mZH!Zu^1bO4OrLbC{!>0qX&@H_ue2!j@B6m)Q#x0?=Bb&ZX;Hp~S=S@0 z31giW#a8p-OSVZ17GefW^JR#O@EI#|6w#Z8O^`*s?tDaE-c1X}Zp>fH>Wy376o>Wh zE;5xzY26L#tJgb3lF*2}0i11-4os)UGWHXl#LFR}>d+$1vbN}-dsFKquAe?p@lOx;@8}~Z=X7KU>2eM(4yZh-cPEkJNo29vy?K_+ zmg6`O0zH#1@7U*i2$-HJ?&dQQ2y61KX3j?Z&?ouf?8BMr-TDGk$z zj1M+MVn+4Yur(_jZN(xt$K~kn=iZypzrAS5JzYn5E)|-z+3uAl%!0zVCADU`FwxdL z=bxf&YaN7NTAiQ3H*477y)3SgN5L$J<8`+BZT%^_+%!)&!J@4M1227A@Zrx&W1f5H zl6SJftsq-%v*gInfmERQ*9(gACL(mC07!FZ&k+$p&i~~;sF%9iw%JFhx36zsc4o48 zrNMcaf)u>S#7SKav#p-$!VvlVjJlXQ^9&CQ{z&x=(8AzkXpVh-BLB@YA}YpD6*c$3 zO_NjwUc+F3X`c~lbqP&`NAB6qRxzobB}2OL_lZJp3&>Ph%M#2`8mzO-)u#{xzkgRK zQpA1x@q+Kg`*lgYw!4q)>h-*NR{OplARikr5uoQZ=E29d`StE8z_;Pb={#F+_jhXB z)4TAtM%5x{_Xj&1sj7Yqw0=TdW~5T~bicYQ53tRhH7}$r{*J!+bA`W9*sWTBIbPPT zS4M{qnV#nPo@s5?d`yxacc{nCVyH92MzTd5LlD$)Lx@=HDV7VP7$%! zhjKA^9dshnzGg>KT2^iqLK^a7KTX{WB+L>P%*TYkjPBWA?O**Yvh&3))IA=&Q*Yq# zN{}NA>%#KaUEZ>-BbYu(n75HujYF*HWyjUZ)pi|=P8}{>-}&hMPtR%e4w6CKiSOtNe4ltLZrFW*UihVUWc7ltEUouC z2|($agW_lrX$i$VQ!2#bU&P}Oq%6M0;0}WQ7$-)~FybAzrM)Kn4=+ZuIZpSw9|j=E zY7^s5OD;^WNs-N3ey*5(?k%^Z9LbB6Nr5jszS7 zZ38a|KjHStA)Z>Tbf1yBCws5H_fh-RDejxZKeyP$7sC#4+ObLCEt>v=v)Kw|{EpWg z#J4!Mv=o(N*KVq08r7ofs8;|%9YMXR$+6YYH!iIQGdm;QF0ntHNM{;Kw>^!BYO{9~ zjWoJI*oQ2s-f!$0w>~E-zAU8{UH2KBvHl!NfRPe*56^wpazL6R`#@luFzoDMsozaJ%y942@f! zx3E{uydlRU`-yUCk4>rKP!nUGJC5y8BJl@g6-X~#N->FpPh%!aY{EqLeo_i3X@h8d zp8K=9LwLUG22JNmZd8R9gGnihsNmMFZGLEvOzMkIO{Iv=fJcqB^dMcHYT|a~|FyAq9_Ipg+1B=Y$;&vGGg3 zJPG4U6g&lFXuz04fJu6crg~*CNX7s5Lw@zJ_~iImvVBujc;osxdS+(@I>N;o# zDh250e=FdnQA})@h%gMvxOtWVIY%Mf>flkMpDwkyN@ypl>N7zW>I-@ehH9+tff#h3 zjvlpLNcm`Xm5^?=sl{0R^a*9+Z*0@DNag+`J5?*x=Kp5LFv}xV=YF?=EzFWe$SI!i ziTx+Hm(d3+Cnep%W0f>A?inVqIn|LApnM6RD$%WalcSVOfo$)k^E+oCcl#{6XU-QP zyvujtVrFujG=%6nY}{Sy54e@j>}4=14=8ULgyv=ofo$(6#ISe4v<9mHnVle#=!1xAEK9??W};?I(Ak2>pmj|7)p0%R!;V^v%>z4v1u z=3y3v#6;Anb68Ey%apIGFFSTi!q5BUD8H15&g=;N(P@nlltur&sDnPh(jsnR0ZsOS}I#g*ROcb~>LVNUWb#4FuM3DQQG&!VV7V7WMO2 z)G47&_k1(+gLAYi%%fyP#L*{lb-C>C+52(3-M?mj7FONdrlB<5dw0aay^|fl)6tui zNTA|6ELPojG!2ckJO8TC`P6UzTof1XNa`@031&o)w-EC;IKTi5zV`gArLg-7?SE4Nx)waJPn z0hv%l`Ga;(Lm(nQt=9zf`wh?0(9aW#x?A{&7LCei2c8o7<81yUl~)k?(4uZmJol)0 zoSF^oxSY_QRz*URl~n@#{rC)1v#&Vy2NUYPb|DiW&v$c&1@veu@N7K4X7g2RsMCIa z?w@*xd{%)|>+_cPCpU8J`if!UcR&fJMsIBIl%_L!%1-H-%GoJz>U+VGmb{{S4IuwQ zg(c?eHrA*#3Xo4sEH?b2y>BH{|cw%XhOnGw5hXvkg<9BI% zZem^`wE0e1OslUp4XNXK_T4w>S?f?c)v5bV{ngFd&A#!d!lxmYvQlS^UwE~=^j*UH zr!cR|MP^PTRyiu|=Vv|3%H}fEh{}er1lhtGDOx~TfIP6*D75ORq$%zBYv=B>&*^)B z?5qEwMJf}`oZ?KVP#Ul{ei`iR)f>VE@OB*^vmAr{&XY!NjXN8(fS{~eQy|D4Cy=$L zmO3JqoLw!(k*9&IqM9_UL*3>wKL1^@ymi*omO=OzEZg2MPs}!n7J5N5Hp^Sc9oXCb zICevm<4wx>u&##|KbR4zac>rG2*??f%C2rs**TfZ$2+KGWzSZHcJ9yHan^6m4gUVZ z##HAb#*lo&$8p$Wd(MI9DjPk_4cpVNvO1OD{`IqQ)(N?FIZK5|b%yd^18cTW$fwt= zRYY+QPv@D0QWJHHM#-g5lvxsHIFjf^HpM9mX)!CGshbZaa`VlSOj^$HdhQ>O7SDv| zdnQ`TuHcKezshvC<<#zJv2N{d7GOi#p7!Uz0WIH ziJtMR8vmrPJk2MgN!rJChf9?LwBY@4S+k9$+m|PnyLYXw#>a*`)|O3seiKz3&AB4B zlIkfuRUGM+ZG1cQ1!v)n<}+&JD)wo0-$Tleh^WzyMMRS5`ke+gK&TXTG}B8OEs-%Y z?)I5Vfa2AZrd#NvQH2ZmE&W^rZ{8uLs*scU8pmbL1;3-mo_sG>s++c_#d#m~gnDa1 zgQKgw^Eimy%X>uB3j-fPA6(XLk_f6x0s`gf9;=+Bf@Mz3s5$J(6X_s`7w z)EROK$HGBvpn6t%+(8fe*0$t5LZ%>ZQRjYKw|Vc&G$@eYg*H7r|Gz9*0bAo zUMv;yCm8ya)G9SO3w#mT?D_0e9eGE2zGy)CB%1&12(15k$4eJgz|)LUg~6l&Ni*yF zLiJdmA17(v%^l3U*KjjXVl1Uy#qgZPqsO=PKmAm;_E8X(G6`D`?xDVXL}R?ucsxg! z7DH^fcX0AYdauAz^7DMJJI%>?L1!g9L0@T$+Xy1_(g4cgU@~>)qPlt0g#GbdpWvV2 z)a5DOssi~y*MyA-7?#f@=)?|mg4F9eZyn9An6NZ_e!!~=Xtp|zUuHGz%-0|8ro3DR z#J}IFu|`g6N?7_j_6)ihwPKOz?fPvT^J05O8tf8==4^U&1i#;fYb*+=czjRA(C}A( zitRC7wG!=j^l)?blP`<=^BR3jL=q23?=TcF_6fhssyeN|@(+C;6_R3qPee*?`SjkH z>5CP*!^IcQ!@~5tQwWpt#!22!2)eu16h`L>IAqF{N9I1-OkXAmWxJw-nc6zRmg{?uxf!4Hm;s*6$zhCw z_ucFxRQ%WwnG|GA9oP$8wnWxE3%(dVyPOZ*0&WloFQn~8u;mJ?Or?w6YIcIb*Y+8@ zpT^d9(8UwJVRT$mby0LrC%mg%fCR}ef9%nZT5mk|tz8Ij($09RQ_M&= z^iVS7ol3i<>?*CmPAYgO4p8cUaLFuwSV7?Jk*1Oc08`EDvQhWP0r%FI?jXwe23^TF z^d(MNOmA)Ad!_)9d^O*!+P&o(q4!AWmaf|2*?FURbn;LB50jGHbr^lEChuC;ZUA4` z=no_kW8}BmN>O|ovIyQGh!2TM>Oq)vl~_FVXrb!Sn=!l!!CDmJbh{;p)QJmp&?N>3 z8)AO7N3Xq3hl)Kx@|rZqZ1P(n2}bGDbHP#RL?+AK$}tqh(|y^EUMzF#r(LDgCzp3e zw|ALL7u%$#&^H{LbdKNY7KD4uFE;DNmyYRn}tO=A;c`L>ZFfM@RJReM<&-G z{}GJ-#$fdlLbK=Hjjw)7W))2iJxg&_SI7OnxBUF2%Z`sy%^>^tufpym^>P~@^QK)6 zN*#9kdEz3u>waCF*WoE;yYB$f@-HIFB1B8%j6r;1nwl&?%OcI>biSlCWt0&0prj7a zk0aX})li^O$SiSjSmFK5yGGuiRB!3cSv$Fu2aqh}!g zE9{;mPi6}@0O;9MWlCOm(+>o%Iz+v*F>l!^KVa4biN z$(ATW+352Qr7H2WMDE(P2hobZasSSqkvU=APH2Zoz0ZY8Iw;D>WDHYQ9MR<8v7sLi zkw#=!5S z$A+UW>-jqbqSYq4mj>5p{NP`3$TSHTj)a^@yp#Jdh3Sm9VL`gZBq`LiBq|aVKXlU8 zma2pv$chwkAMHHHGI1-dj87GU(l%ZihBN2=~?$mtMPCeaq*0 zROrq9D6^Q7;|}np5bhb z1REcmKfs=H&^OHtD6OzocKe@u0Sw8BnN||5wcR(bGD1(?F*bl;OqR55fh-uIZBcHIYB zjNTM}^hmW`IZH#=r;oqstWH~( zd;?MrO#b9A&)CL@8W8a~rJl>r)R(Bnjhv=Ba~plrLXyTIjhr5RIuaDBu{5%Z$)D1P zb*hAKVo>()YwW*VIO~`Ha;QWxRUC53?XWvz6S4F*0ql?J>RWrWUN7`kei$=d$y3?W zdiwSWXd)_X9%x5=ozjR5o*(Yr+iU zR^dSR{(~{BGRh|facE`*FeaX#?(fXd|F5|1;cpXA*0{SkK`z>bt+l0iKs+@gS8{M4 z{SYCIRf=0@o=|k*HPi&$JmTSEGIoda^~UmNd`+)R(!`$5s13TW7Q4@&o*}0lnOOCx z{%0d@tF{id-AV_lSP2g*-Ug$HLK~sAO$Hdf^k{T=$_1Kz)53ic!T+`j87|o{^ZXJ! z<4af<{Z59hD-=Hpb4U|@no!+)O!h@q%15uQzD&;>S;)5?Z{7)#YZP?UN4KGMro%q zG*j7BY|Z&4mIHULZQA;ewvdE52J)z>W~fr7Ijl&LAg3TNO249-RTsD)K>ll$EITs) zLT6ry-Qaayv5Y2oLl5YKJrd(7%yyIxZ52xXhqmf5fDoKKY6>elTCyY~M`k*6y#dv! z!Wlpm0r``l)E^e63cssPhq$*y;YOTy!8#gxPf)x$Ps-?;hkNEb4D9U>{;z=pA{VbR znBFJ6I;H0X!E-D8fOvB~$EK;GJ8DM3<0znrFJg72>a~AkvGuFn*b<_)veVGw z>TnbT`b>Q16Tv^{x$ciD)C0E{@I|=D4pp4|sUW?u!jcouHO_K@MRKe{ykfwIm8;O@ zSeinp``Owk-I}B>T-&{ql$5zd<(r91fw*)R7cE8I#*`L1?7LL_@xA5DgK{n%31+cs) z5`(E)51{R0WHpLSt)YnCq*9QvY|9L7Cq=?9TrRZTs@ z%)^PAfx@MsiXK5Go6P0a(9=WLwuGrpD>1ePqU!n6kF%IxSEjl9?`(ZY#R)XgZ&>(vJB`_Hp zqtxv-VH>D5w#Jq1^wmmw;~B^cLhPh0g7mJv*bZH(OWui>2OBWC&t28%`dk#3&$+EO zTs7?1AuqdE_6zNH58CcDJ#7KoH{YTPnG=G#Ln?DC#fkzk89%)~yCqvWm@MdQ%tyXX zOSamfUOs+%Ja4zKw;tuF@_3CR7{MqI<9Q+Q;hnd>JziR^9#8lGj(Ud9_j@!57yck8 zcek6B`{LP_11utY0YGXc0N<-bxq?`F%=q4uqjf8s%Jo-Uc(lAZA83BXb~PDyMmTNP zBIFjDs!tr@-D^fA96H*B24S9N)zfbvTQT2kSGNT8N8EgAt;P$uQlgQ}fXw3yfD3&~ z%TFEvWHBuHJNC?i^eQDpKeu?j~X`L%9ad{aN3Hh?&lKF=i*T3 zOla6=S1+o0v7(@XIw-8EU}HD?A7e=ecYdoG=31;}{dC1_8xA4YC;i_`=i|rdT>?KU z+pSU=Odm%{4BNZhHST=Xxrp4OpF_pvknRI62hT6Z@8y~1z1S86%3!!xn zQKu~-Q3A*=g>yWC*b*Lz<)2#`%Uyji(ym~=rj!}sc?W#E7h%-GCqCZ~`ju~ceU%R9 z@`VzW9YZ9p;*pRhHGSX3ho1H=)T75Cg{DgeLRZBlLt^uBi z`7y6!PC9uClmZl{A`&po7-SM;u-5lwvEEzXPp@;%7PMK$&U!D~plK_Y9!meavFD27 zPoD}}_FXwQ&>{Cw!YE*SD-@8k1mJ`F0PZJ)qz0x3j&ec4UrNsam0;}5PR+YFf z5ofQ9wfW6bBA2{>``=xi&@hN=r6c&Zpm#Yac0Bv8&Q0v)=43I4hOT36JeQ7oj_}LV ztT?4ui!@Jd*y(;G@91uNmlJ}fS{^xBTnX{Jf~}U+@4N*dO4%Bv`|YTc@9DH(#Nxg*w8U!6>axL}n+2eZwmEcYf!PL$H@mkVV`Y*J@8@ zeb24qS>{k0-yr!EhmZP$dvi(?atq*{1}%!(N6`0$6ET@0?qA=4)!ZIjy7^kv<1A@3 zUR@B`Jnl|$C~v@^kxqG}Bf6h0$frxyr^h=lTM_UP?U}ch#p9H7LbvT6g_T5e%)RvBK!HlMXcl)F3?J{Q@&R zp)6^PZJJgG*-2!3`Z$_4;(#&D?dl z2-9uZ<@kHaXwJ#4@w~WZ->1i6K?KsBn;Ep-Xlf`UnCkXXzsxJ)OL}Mj1QlviKEFcEa>UnBanF7~7^9-cPf7aa&vMZGs8f!;1iaP^9 zvpvUF8Y|iBX|X?iud9e`lyNSN~ApZDF(d%yS)FRi~U~MDM ziPeuq&#J6*Q|~kqp;~*_*0aTzcL-)L%vI{n&e>r?>O~7x7mF8n%vM;Wy>DDst3jo$ zP}jNXDqg!S;v=c1o8LV@o^c@oKEN@YM_<%!?m*bF&f2)iYl^o8Mf6#Kd%q}n|3+dH zdf|P98mw474BqFj8PKNe+TuEEfq~MX=MFHsMv#&7g%ZXmMFaz~8qY})J=?hAb?S89 zU+-2n%z9H1>o{*%vjH6@mI*DnQyYwO_JAHxU5Pb@hE_r)&+zA;A#YjxcG z1>TiLipz76i_{=Qk)sVeydW)jquD~w8v{Rkzc4d$m^aXqjd`-( zfVk1aOl^vX&DxOdn>XG_&WZtC$ss z;M5MPXWebt#@~*h#{@Pmq2otU)D1dre(R{?3aC6kM#zs_(&uJZ&;xZ6XKIwU_@Z>Q ztZRFlEKNf~Iqg?d`6+5pqERHPNmATeX3!tSfiaNS_EGwTspC;AU&hyTK#AHK%oY89-q3;(19mgEcJp&4vC$e=;2>Uf;|hy(a-bLN+)c;y=i zqioHBSgLR+dTPDLJnkgcG-C6y3kIog-+1Ub<$^HG8s+Kg0hBr(i+LuOs8yNw#JKv# z6h#4N!+LAnIt&Q#j2s*mI1fzCv2BI&}9V zum(gsDti(10<6Cx6V>0}vyUxyoz^c4+wz)RByMhc@f>Q0$FV_}2?;dgkYuIgc}QxU z0i(!EH45wu5t(eRE;mHdl%#V|GgLV;Y<29#U^F(t+HV9|(}+YNqm<;WS3Elj4 zRuZ;B;gVc^Hyb6JTeq=&3oUxOtbNp$JC${HpR6p%h#Rf6ckugScH4($YoeGb1cUru zqIMBv@8S#fOWnOx)f|Uly(_iip4k3iB zPYeP?ChWW|p|BuGQvyQ)4f||6QQaH0Z=SaVChxQdznS+gy7IleZgAw2T*}fI)S3Vo ztB02@giB&yipDce($$f8LZdl~vH5%QvGlnrqwCITmXVL9hx%kgYN z$uf+|)3o^p49?o})q_}nDnXard~R^7PbKQerwXGT{ekm1jA1spbQV3+fElqu&e=C; ze;*y(N>4L(pJQ6O`WV+Bm`k&cPJ}EYja;_&m?ad?l@B~ZxV1dd^>49tIBrGJNiFgi zD#v6i#CBu$#DcinBKCcL`A{s1g${}~-m6b_{{r_#wLW|%%?01F_TDSynvg4AC*a=P zbt;%y=e%5kg{^~1j2kEobJkn^@u)!cgASry(R0FSb=;rd#NcOtIxbN6cL zh}(|y?!-z{0X1;5a=`G!mrLUuBk;txV>$IRjB;MZ$noU-9*mmxVjh26?;Aa4MMUgj zJlTo2+C2mR&~Ht-4{ty3&vyM>>l;MYG)jI+9p<&*D-_j`c9C(%W?`P#i_1scGr-4Z zX}$;CvaTyEvBUfiu;Fv7V)N@$6KMYAq-_SoS~U~L_+;AVY8B8Rivh=hnZyPpR0hPi z6*tH7D`b%$09C4Ixyg55R6!;h6Ly;BeU6TLOKK&K*Sguc$}vB*!6$09e@QF!r3f17 zRlk(E37Li5-1EC5^%hW0T3L>icys0$$sWtCeTX3ftzPUk>vBl=a$S zeyif(d$}Ar!$Ma#Pf&azJb$_jin3LaI4pG|mR#`VvZh##=VFmZQP|yG+8+`C(yUZRmYcZSm_%n6=`Y>1^_ zSYbIJ@H5yT4x4!2Ae7jG4{b_sKKrCjN&ZX|bc?|Ee(^yOMdL76+cN2Q=CnGX4^egc z*l=F2KT9}I6h&-E^qxJW8Fm)L?)|v1pf_t$}vL8M8ni*Gx4iv82(1C4hBiAvdmUXbti@m-#kf)K#(6% z02N{YSuVV96iSeG=akp{<_>l5dRm041PS?}ZU}c=tDohd;=cAJ?;Rsoo!io)5@`+H zhtAj~XN`?Uhu5v!w@M%nW!@|++`}zowu=3^O~0@C7>y zqwluFS#s*pvPt{18n5(J=NaS1yNfL=WL)gSmEvBlT%HKdnVSjt%w9-fbUEL5FZF;lC5ggq3JU~_4wa?O{;HLK)bgOprS;ntuY{W;j z2dIwqEc;wHl>7Su0kId@N7U@weJ_ks3h#A6+4<{-GCnd%|DqtXHaz8bJ3Syhy2^TEA2Bc@m*xL*YeU&lw*<`9gL$|) zu`9#En|n0-%La3+Opu>huXmUJx=5$j`s-OWrQI6Uu*RvC@LiP<80#r>qxp8*ain_K zm$@OkJ@^G38$+QO{$r(Zlv zbnT%e%T%Bigd+pZ@LCz+oZNoEWOw?)SEgjK8FVLPOgi8R6PX&r$jal>)(`Dn((eue z5yUBKW)KxQvK%I>?OS;6Tk@<`!pHm=gp;G|E_~OS$ajfWw5=dnIfNqzOj{ULOCtV3SP~C=NAc@%r{oYn5i2|sT5Q1>$lo(ZXzb$OlQ4W~@m4?*5A z&s5sfwdsjL4$Rbdj_g@}h~-&NXfC5s7T>1B@M>bj?M>S6Piinq;Z*sUb|_b%Ke-o= z!_@2bNvl-rto>%tt8gOwUw+(Rl)T7ivBi^Q#1TN3t`9kGbKzntj+(xMy<}4V9f4sV ziHn~kkT^BD=Lb32s^VLsct&}k*8)=0V(_D19wA%NIyf^oB5FZPV^RYa!UiKtzfy^h zr7fLA`eCp#-WL@*8V$TU?SL{1=J4C(c=aiqekxg+UJU17zdWiC2Nu=u2tNcb*exWW z6D7KaQsW|Ti!6)bL?`$q6;Jb1g0cvJV^AcdJB-%H=DM@a zCR1wlqgIl%P1aMUtj3EY8+Ht(=CmPmYG&!eyu@WIwo&0?7!MFT1al=?st1WW+wFUY z_9dr|WdA;q0yF^5dKNF12nO+vP)xq|XDL0~^ua*w7L%>K(MmWAu#1VV&aS12#Tc5n zz#OBwDsclTA~t3am?v<$c)d>)w~pyPDNJS+IUgN-H@kgvzRlZ`9kSu1MBhJCZ@tQP z<=<~*y=Uu)LcmciSaXrL&msY?n8r+tAHR{<* z{*1f>IW|WYuv42|J!1#~>!~T&dcdo%8 zP)BJ~X2fRVZsg9=uoKDoz!jl#a0|!JtVbxK9>E^jl{r#4jGBO`E z@vscK7iR$r2^_vG?NVg<#=+fkGCfb$8RwthHNt&ydiD}Ops+MR%6c*_-)>g1k*DDd zwQfmvW}$S@F&OW}o>u>kNgi9RVL+S;F9gx4Z4?pem7SV-hEj+3gWx}#Pxi~V_a=Vi zPG=;(pu9Ojq_GK0_YwDA?A2AgW9Xl=;ZD$Ftn44_bUO5{nfqqB!^My&98UXBhJ`45jB5;k4CuBVe0)bUlkepk&t7IO=cqgKbursdig11 z>`iy1^Dru;UY3f9#$609vfv_V#`~X2uU*x`BhUl9ECE-H7^dfk_OUC|9#3KrAaSO# zGKaZmZML6oH`6o3=&yOKyVH(L0Q6tSy^Es|DKAAn^jWZRz2X3`2jHM^7#%3>2T-;W z9+zcubM$%nmJKyXPX!3F+WqhWr3l(-)4g4d3v^J-Wxc6E{;(^YI_?3qOl|#6+$V{M zHY({y1lZvD-0)V+`#PS(uBNhA)PB?fRAII41G~hw4PQpo9%rTG7LX2Crd)(tm%n;P z1euPe8Qk^4aWfUXRHRb#BvEr{OP)8#+UVc=VCqU+ch+2sM@a65@H>YUJ2~95Ev+2eQ&D;HUJ7Y+d z0sMnamko#5gPxc1zF!dD*LXv?*K3#WZ@%_cV)-mZrVUAtu^XlPB@#wvY0Qbbg)G%!GL=9!XIzIwv3ISIO4x zYhz_F($5uwVZW5h;sm`u3f^W&d~w#H@G_zrwq8iT|0WCn^H5wqhqF+m8rVDEXqtQa z+XvhyEIHk75eDtvbN$!XM0aKT6nu<>;NhI7yu?jGpB{xbPS%D}GL^qGimq&oYU999@9OHc5J8F0Pk0{|VVF9DOsGvJ( zSKTS$NtAjm;oK5VRxtqSea~ljNM@eCHi6#9`=EDFB7yQ>3Kp!qclmCSNf8n~EXX9A z5B9;W#u2o7YFO{=BJ@_jHW`+?nX+N*j22pd+pA@l1+eN@LYO0C$CVXz7BfnwX_3ge zpk&IF1IuJXbq17Zq_%0=Ti2MvPw8Ge#!)GFVL;lj) zLpp5rtqjsQOZL@Ud>JFDthcee*}ab&E`{tL2m48MxA|>wt{es~gtPL=urCpzU-%{$ zV==TJT*gEVtJFlg2S9LLTY~UERWJkf{=T1D+}T$RPCuovzElzb%AO} zM^u_^qN4XHFF)(0>^O!#_00*t%p{4m7)*_t&=3Y&5jt%IbcYTSH)25Ul2T#QMV$u|_Tu^0RQVt{o?= z<;}OkX(XK_N**!2H(yKy;}iv(7?33g1dAePQ-MIg0yp`{Zu&2>!=1PXk02w3*@;(t z>{mAT{3T@5UWYh#p&&T+Ba@XL>?x7K$??+`w?>`-_wSaYTa!c&I9g>vxoq*__glj0 z4k5TlJ5mdjqrXF?_YVN?G`$W`fPU9-DkZn zU;}QN$vK?BuKyTX6nOJA+l9h;t&=1z-(6w>g@^d++x5|3Jxq(pU{gta-e;No>IZ@P z9s{u${%NJl&OyQ#UyW}cR$;gEAF_3F9k=~Uh4mpe)|BJvG`;zx=roCNtN8I?Lc=vn z+D+Dd%~ydTfel}_Mf==vR>nJqRS?F%u+{2F4E(9o^Z+$#4e0(T!bUk79VL)#%ecB$Ms~czlbFL?79I0q`tF4bmCzkb}k$d$p?+C)X>d#FHwdJP%tSo}^!e~sXCF1b%Nxj9&GH9k>?uH7VT|f-YP~u#J_I50K z*FUAWuS*b4wAm!n#V^6q<+3a<24j&0o6V(6JK2+&^pX97DsUhLP zh#;S%AfNv$o`V!YL3bfs*V8(lVoJeLX}CcMO>I79ZL(WSHm^&MSvYQRf8=QHR{sVE z<^veisd9B#-x~fi$oEpscAAAt@cG^E?8R`kZ`*YlVXUGk4=BCz-9@pm6j{FYD zFtUUfks*P+IQpxT+K9W#L_t&9n(wu-`qtH>>9tjFf6*#;Zd!-2GC$GkQz=|4`EN^D zB|PkPu$n$1L60^I!Y`}hRb^%=zt<-RWfgJPmOsNlzPURkV@e6*$6^|JhUX|!D*03 z1S%ifjjuhYl^tWpM}JK!IN5W`gwhhnN!*-EV}iLab5xRk2}m1^3HWe+Mm&c%N>@G~ zkzVjV9^KIuw`iaOw!*`dz4@#k>^Jz0W{|bck9`&evq&vAjmD>>RpH!n2d`fp-Egj5 z0qFV<>RH0q4x_`Adni&t-<^fxIzV09thCoZ0wFu&N;`HrQ=+kB%k}S#fbFh90QcjO z6H@qMr-tc}j>fW9TW4gpvij&4;-kO9Aveh%(6G9^5k(TF|id zZlk6RhH1!E>2h(P#w{Mkee}drm(o$GTcHv`Q3k7)YsiZ;po`n|QUe|Dc72^0i?xjw z*WY+oPUjvN%y+fr(=FB9w%hIgiKvGJheAa+7)ST?6~A*p{4e)YBKxZO&Z|79<++ShYZVrEtSOV}baaA#`vnmWnkwlrdr z&INMZGDKM5Aw2Bb{sDOxBkr;&P$El!xafU8d%n)?dbZjqnfIfY>nzTm=0OwZ-v9|OndC1YG*-XTdm-kX*C;ba~5+j0a^#Scl!HJ2uG&zIY*K01JHlRJM zX8*a;o3JMaI2J<2qzK87xsa>tG#l_xfdb|4D)mti&4PIkoo; zcrLxj8A)*-c{igT`i_Kq9e{V4U~|Sz#)fcjV>F2wKaFDBOO!ZIB7 zrAUH+yyiB|#i}p~(gps@Phmzjhl`A-@RjQwtF;Ih8XFSm;+e>0hEq)FZrrg;Rt&sC zJlgh}{K$P-!8l~_-sSB|^PA5-k2rQt6_2Sgq05+Gz1$e`oWm= z3%WdC)>+5psyfrh!=&2()H3;rCd)(Qqqg=04NgbPm$KA|)v8K0hq(5L(NC%r!s)9O zDuZd;kuk7vn(7MPe$-@UaZ#A|X$Q#Yao=_ie*5gxz0a=$P1IT&2- zvue3K!QHW&ZM!^ZesXzhFzzG4-sKqzAHQgWpiGd_xy}DY%5z`$UMaqUx6?MPCS&Ly zHYv^bjh){-br%(ML2*8QtL5qyAn z$6=n1@kB1X)I)}<2M%D=2Z}zXW^-7agnoPQ5}_h~jeB(`Uq>Pg3J_Jxzz z3$*4GswI4>+XVXeBq$DorFjNEKs?IeDDS7b7hWDbLA4Z&GOx`ske6VWV^VaN`9G;w zrVOg!k`b5}|WAwevvV(*c`P;y+bt|P!}(^2xraKJ3mo19g=S<6Lpq~ifi#~fC6*00nOU2l--xAA-Cn&emt za}sBruZ3Qc*Qb(OQ%?k5FmVvaF^*6`adRg?XC)=y-qS?Hfw?B!;DcGt5}PL`_rnas zMy||@wB_h^ugbsvlPqbS`09tr>DFV$L?*^A0u$ThWgJJTZ~P*?hTg(KC0ccY^F-cO z#vRu@$_CU4wQc~UQ^2s%iQ8-YAZQ(d|*YBwR%^dst z>#N&X4)QpoHZU5!pLou5vu(4Duk`Z*Lu<;yK_5_6&)%mAfqTT~>;ez5F6wDU7-1;} z|FgKve;u9ZRO4C7KhMJp#2w)Tc5E?5`a z7{O+ElYpx9T>BGwvGgIy`}_0R?^ztl{<(tuZ$|F_e!X?q;J%TW(2X`~_H};!m2l*= zOoCRs%LlU94s?33J)exIEC6i}nu1~}qP5q*Z5hqzCiF@}skIAuC`?c9W!1|6lh5*h zepD$RnI*0mpXV6m@tlDzcy1$a#43kEhf$W4Am4ibi^(&=2in6)Rbl;%$i#y$OyrhP z`c1L9J5`1TLHE&Y8x&=jitt@85kzpcN!w=TM4SAJmGk#| zxx~L>3M#Ra(>p$)YbfjDXn&_vrdr@v%6?$-%VS}!Ot5U6pu%IP|3&S$+!FkTL(xhj zMBLPcN9WbWq11)DM*RN}KK~s`uMAmK8G|6TlIa!@om8D5_pR@63*2Tr6@C7%JDIWr zC<+tX+2Gg??=x3p!&!-)BG8JvN94lKfx8Q(zBZuPsoK7OGSL1$zdyOiSRb_s?oMqB z@=<_M9_VvdhdZb-{wPgsJ?*YX@KjdNpTgsP8Q$}teG_IgoWBpqKd)6qaGwW3b|;CG z|3w%4I}wvZ;4?lQTvtpBcaDULvC&^aT%fpkrN@f{T;*rn$_u(cY#A?s;f9v$SoDBEj%j_QqUcdM?+|UwqxNlrQC< zy|E+bY65BHZ$?C1_99|8jM7<<-hW0R>h}23$)m(66nYZZo!B$uJ zH-di#z~BFms=;`VMZKInt<{N6=-}1qW$E}0%BrvFaw=gSTZy;yB}pR0AVtI#H3l>3 zywxbWmI;Pb74bpvKIvDkS%vi^IoxWlKM%ekGcfpq*>hXyfL8lylmI8XldzG6Yl1Ab z=J3{2QX4vgYVhQ^Y!rxF#-L&xzATJ7R*LCh7f#hj?+(dv90ZxeIsY8><WP98~ST z39Ch#3ZhE`H>jVI2N-?^>6wb4y7@2g=^7kZkfw;~j@1v4)CPeZ%%lq%{7KfFgC+?U zZi6bF9VA05{%Md(V@UEA7L%$B(}N*eVm*q&2ZNkcz}Qg+W(x(#0GQ6N{0)aSUG6vGm~K^(b98P~(i;de zd_Ylv;KKjnd8$|7PItERLJZ27m0`&Fmc)Ig2C|=1rMfnd!1=;A%_!_cSN`7v>>t-L zQn!SSUgUbW=A$n5o3wA&#<-3flewQh^<#c~K@>Qrvv6{+%#!MDZhsPgw49s-#|w*r z?>wg9SimCfi8Oa@=UjtZ9HA#V225@D7&u$`E{366lID#k2@^+oo+iEzqxs#gA7>-- zurAJCQ`C3z)FV-BdgA-tGPlnfH|sCU;{Kr5{?q=!r(kYyI5l@JQL09cVtyIn8;?`j zC96OZqOv^eMQn~5-5@&nhVji61|a!R^eFqgM%6j3C+c#!nQV7QF|RCzagUcrD+{Y} z!8fpk7TcqlsHwT_(^DJjCbl%cjz5ouWC&9@nLc|7qF~%Z5VRO)O1M)R(=1FWlwpXG zq%^7jOnm-(QT`Qrs}lZ{FcH79DG`641Q@UyET-aYe5_K=z)mVM|Xnur&G+-{l1oUm^&wgwR7aBCMD5R^O=sx;rueEm6C}sa1 zvL;}pf0OA{>&E=`&L0+x<`L=lsZJq*Xl)HYi1$zbF8kBB%Y2c@A=$X)>S?(lTplQC z;;WfJZyfXoB1z1KX~Yi<5^AI@vYR$i;@e~(@+e)bd?DNXV4_OMbC;#*cpCNB8FulhPB7KK(?oa1NVJW09|O=Oz3Ln*+-_r@4j_lHOldlP_yBfYMSDCW?X{&n0 z{~+(pF89GfV4LmAW|-X@MXc0H$%HB0Vi#*tkEPDAD4gP|u!{+S;+MxR{iFp{s~i$MP)x)|!?u|M#R>Ld04^~(;W+ld;|{!C{&8FZ`tCbV_2k%D6)FjK`b7&;#(DmbeKLHmmj_H2PFx8;&(T|AVfD@Nw;R}2aCAOiK&%Hf(?5r5mfL$J?yJF782-bVw>#-hKfs$`d4c)nTj`! z!9!`Y*E#$7g4%B5TbGO47TJ)Z;kiCvc+7A!ZAaE;q1ptcC$?$SaSNy_m_OUMdqU*R!{z% zrwlv@5%cOd7I^roZPoJP-K5Ks)nnYQnfp=W*7u+_|Z**r#CH{=yfCFYhym2Hm9hEAz_uYv~IyiPQ2Yi#t3t{MKY|<+GIpHG`G#EBOgKStVEF=;$rexx_U83oT ztu&kzFD16KP55Ih06D4&u@R~_{77}7LpD(kA}n~zdS6TNXNn*sm5+m6Lk%4zCyNIT zy?a|g-lspb#@syIY;U#>pmVUu4%W074!*JOHco{x$#pt-<4eaPR*9 z_Or0uPoI~jxHUPSKX@K?cIxF;mqxDWk=aH0^4TZbR$I|qO;=itVF{GM)`}mxqQ88V znJ~1V)ph8IIBLP6{{>UNw`<}*1nw>5+B51erI|Dxp58=TN!laJp1t@d{_*#Y!#vd8 z$6&+6hwR(9NRB5cLh+hQNtZ6*qF_(Bn>o)sDI{1E67ajEzZusf<=4HVs zh8{iEpqG&0jKH1B(Fn&uG0g8=vZpd94w=r1;Caw=eU>q;7bAYg^=K*_*@mdkCG~K6YDPnTbiCiN1t#6jwvf?TdYGF;Db2*>Eay z3LV3l&C%Rmz>4W9GaVHrUiC&VtnoQDCXz>SNNGin6lNvTH&bK{tqcz|e<4l#haK}4 z9*5H12RvT=P5}Qx&qG*{1eol`=B@VedEc~W2(TM0;W0|JF?{C9eTIShhp+}wyZtTA zZbC;p5`u;g`!iiv9>nvtkiMipBrc!s#!>)A2h*K0SeB2YDIQ2me@4`VgUTO|4S@so zY+?pXTeQU7{Drw6=k~nMU8Ip(6woOi;@Xg~NtML>e?`vw(DnnRuny#lmz}0O9-NQ; zK>?$54bPyc*^?O4LBr026Pen-C;tDqF1=3{VtT3U8pqH zL02_BK>1YEzMl*+oz7fi&|)fRpS$$jw)G(?a%qT&$-w~R+VL--?f(k2 z?Ym=bHDH+h2EU5IOrCsWKelnHXRowK3{aU(bkNm)h)n$kl!o?m1Qlhlj%m&1`%w1a zmxUD-t`31G5V{rt_{sK+uG}9Ny7$i*egdTot&rTL{H=(dw$<8p${;N6xBV408C6== z(%d_;gq3~B2fNRs7_1s5*#%i7`K@{ zsJC{DnEz{M=xVCF^Z#lA{9`u$`z=tO@dZpKj><89eww9GRI#gJEI?sdR45fOU@(HI zUB>sbpJX*R;tPV<(i1MpI-pt}XR2jJl--X)0L-45GtuNbaeo3FfU)|<62vVt-(4ZN zd7I`(l0}J~FY8^F6_b!ct_1H&iqNw3gI}$M6&huN)}P$c#-tH<^^E{e2lvC5TIT4r zDVyc(_<;}}ukeR|Xb}CJ9s0_f#rJ%dXML)BTac~GQCK%&%>l-(Ky6AZuxF0mAni@; zLWioEK6{c+BSi_>H^yvmVY-6h$0sFi1%^Ytt*h#&@)%o7@yk)B&np#n} z@0|bD4`qE43uyV+JI+;U6PzO6=}n_ulpGWyN8g`ljU4ZbnboBmj%tRUsP8@qKPu$Y z$#mgc1KX^Yz32I#XZbPS!>@}rIZxN7>pl9;XW1Cag(3Qr7M|ywu4|hGe)c`^ysqNE zDtQTxp4l&P?#muJH&)R7O&eMcX^)BzN~=-9^xwG*>rab1uRTu}Q<;1sIk6J(&NW>sJD|!xwEd*~U7t3V%E~^76B{ zyh|N-EY>E@*!RnLMBM1UQAjmhh4!|8?E^uh6*X=x((&}Z8fneln(V#1y=F>!9O z`5H~pe~H%u$_|Wx%V5Dy{Wce`(;A1+S&W7acC2rz=a+%ZZwoVSy_~ClsGC*SZ6=S) zLl!eY5T(2$Hc4TSPAoZ3OnqXjzPc|>qDjj(Qfa9>Ga{-8hbYD()qCJetPXe!&K2k~ z@faA0f)chLCcHHLt^X?x0kid6UU2MXV zJ*FjeHF?q^aqr?^@AVAjMy`gvjq3xW^5o}HQ!`$D$iWw>kr2>v6$b}k?5HCQXbS_~ z{b9BvP>rVFAR}G_q$i4h$~buU7sn|RM|*1^fp?lNLH49sRj+n6Fvpc!^eib%Kyj7E zWh8vbTesOsZB|fpuNxqH{Uu%7gYYWrQJ9M5NGdZmB5yp(E)+PkCHSGe4lgbiGViac zKTkazj58me#SUepP)>-_!LWNq9okI&b%z=baQ)*8E^%Sbf7fBaC$WxLp^xCzMI`oz zQsuK-t-MA4cJG7D1Ma&opJ<<)Y)!_cfYv;w8&&p^yyzc1Q4BlNyYQ~PBfx_6%{*Mc z2jDh7b(?p|Eg8KRFFo5d$fI@XM;NP?`>Yz%t7c`k9P8cib8ylup}XE$*RbP~8-B@D z29vlx%}oB-DzOqoZLb((|_|@R4s94QHAUud*G34Abg1G@QTQ(idpRBWoPOz%U=essS(a zMDz(z#9P}#a<5T*bw|Gn%xAmk^`nlRx+YPBVnt5P21ec|<|lUr{Ot`JdU{R4L+67m zDr0t%jfM)C9CF_JnBtcT|YkpkB zUjL1=-;{ZQbzv4~=&Duy=mqgS976Q^_cSsce}kDHNn{3_@T>_&6a zNRHk+%l?IENt9l-?XyTR;kV*Rr=j-yK!X9x#ePl6#G+l*k)zOct?Aq~WUKcJCrA8g z7{}=jqTOb=u)Cq5!$n`AcgF3Tthe@$Rc?BQqP=-ty}6nSJ`q>usB=}L%m5Nn&SJVe zmm;7-)|;(O;X@YB{vz|py!QPbTVu-&e5@PWWWsJ$*})cie*6|j5uQEuJ4wD*#C7aY z-+;eVYW^hg$D4}i?Jn0HD^H z48HINz+0VK>fu;W!%EjOm>temY+yiq^+D@y+4Z^OQfOCLXvrk{g5F)Vxc;0f@;10! zWUto04{)UhLw8*WuRgZob^Rt;=MM|0RuL7~dp!!MRx2Yt)@iMfjdjTm=T?6?To))c zkWaq>NN9Kc=2axF9^YKdi9CnI+L?io)SynO@MpntsO>3 zb@<6XdffM7_g!7P2dAm%lcQy9xum#(MlM7yML;76h?Kb*R4&pcDzFMv6{6U$}2DAI-yf zD%77{)RWje-yOGMzTK-mC*EA=7Z00^3!Ii@wcoGiQMA)*cv-o}=%SP;Fue2^CbwN! zQ7!TTIj%}$Zf*`Vt6OZlKd(K^p`LYF{wTB>6WwP2tI?oIi7X#cshCJNr9o9#?&WM} zsjAW=ps(8_u=VpwfEAK|_-WhCmLh!l`r)tnUuAq}y`JRlvz|&+Z%3Iu-@;a@t&9k@ z?75U8r&Ot2ZIXn#Jj1EFk3bj~QIr73j&$n~KXO@8WH8o7_AMYuzR1|7GGeUv6p-$ zZ0I)Nss@&`Yryq&6%c9u$P;z3dFP;dM%<+S3O$q0YPco-R^@)5wxNsW+4**f3-rcy za?x|=D33tP?1ZRY^YG(Y$tP!@L*8-MtOJ(;5mqrtZ;ZN#+7%-Sy!&ymtG; zM;F*71qVrck=y@sm8lx?iD2_5yv*8^uB5xoeEu`L+(`YTtXoXACaYdSWY1Mf1*;X6A!EvBCjykU zC)GzQ(JFPpmHhz1D3XWSpe~90A!Zmtm<1%MHrG?_gM7}nT5)U}4?B4+1`JM?0_Ao( zsWIwFWXhs%oEp8+Qx+q2jbZrF@F7ly;^J)ebAcHXp}UJnzoiH`W6<P#BZ7YS$ko2eb5KP&%0}j?J4RZ8R$D7pbE1 z#MjH-9L)?5{_dle_*R~}n?1Z%AP-wpdl`7nE|dD(*taR|A`xA+P%v+Xo{MtVZ}N7< z{Svg@37$kzywy$nk#$H^@H=BltrpMysCA}B5)rbUk$^IrP@=aKUGjh^yPz%wXkz~Z z@Sqy=32K>i-tzSRxV``vUR*z}If){_Ti~=myL34cJ>y0{Wxg)SMxva++Yu_1X->vM zwS>0Yc9LUx93JtS&dzps7<6~)r;6w&LhlB3iF?xkWkkR_6>T0UiHN1 z#u+r{h&JcwLy{W6mRRH5zAoFqeG&Vp*`L|VcBZI?j@3PTblS#kR23Js&ixJ06v;FtrDll*$cRF z=f5cvYFX2bi`@)EuU*7~>H5KcB7OlUiD~+F8a> zlq&w@r!K(MX~rLHO>{-xw2L)vic<>OCkNeAjn*-kvKksVLNw8_<<>t+F$GgGj~Xhf zD?C@@p};0FkJ@bLUB2dz?ETh)oVK!ec?R9?IzOqK;$9VMpWE9mX6LSr|NV5V6?OjU z4$*h*u883w+=3E#{k+k7itpzjTX|Dp5rmla#6YkQElD#$bV=Dy1K3}&jk)D}+by?m zr1_q`MgkhD^LJah8@(ZQfcM5nf5J+yfn*^rQ7NCnvCK$e^$u`p?Rlcj>Tbhdf*rnscrbiBOZG0aWv=*%cSJg56jY*?;#7ZJZA#K>HUAT2eJ5GbWWBKzvNqe`$ zNG!s#p9}EWCUurUi{=zeJG(u%AwM0wNRu9=uFlvugeN-<=&}tZ8D=}KQ#N#a3~BK% z-nJ)!g8JiGoaj;)0wy-8zN9bekw;R8mxTqxIKRF#r^mQ`zF>a#FlG5S$E8Co-$%M` z+&erM1Krd*1xw--UOa|Wg%fOSjt>59D;Gorb&CJ{ZnF+Q##-G#)oQefo6R;Nx7GE-Q=q6yV!4#gLMO|et=~b z9@$e%A{7Hs+#<}=qzz%@!mub%vWycCTum5B5}2?W>rMzPe>njp}1%vD#{<)IoYk^4CpP)Xo7 zk$6x;5V}pNXL+0TbwpRdVwatPC#x23G51`of?SYof?P`O=!}xm{YmhQ zF8876gWWLT&Lc7lPQ`-DK^Co`G$T4-F$~x zYC3dP{ap5rdkCa<@!o*KC0E^cF?3dx?#;13`Bx{PpzOIp`bg7x9WW;Bv~Q(~O@Kso z?&A?_N}nSdZr1#L^vzF65i9qoC}#TDgn{TPw|%9R9`?6CMaM=sKZ3_c;?0~jc9Lu^D zA8c$_TKqAE&)?P4qK6;5(G)i92_N$@}Ww)n$MPCrz=rU8o{6oHvRM@(=433Bs_%||^-WevY4tk1Gm zU26{2-B`Mw*q>D=!7>>>7AikLOqF+cY#Z{~MNBu(xJN;68Ni-$EfM_(!2I3!S8bK? zVQ4j7DJdmaYJy*-{u>3Y&w9QYz)@ULUEp^lKO`S|jN=6-*;fZnOMkr@3l)P2L@ebn zb(W}X5HNjN&HLE<==SbXRAb#l0_;Zy=l80%4Ctm-I+q>sOtvVX$9`vq*kWWgEe+<1J~>F!B&ZO*kGYIsXjA&Wg+JPrLh zZyz}NHNTgX)1QwsNCsRt;o-UDKBlAg!&^b{A)5n+r0I0_==uf7Fb!pmUSS5+xSUAZ5p4Ha%T;Cr7DV-s&S%NQo#2L)(+GOx%TbFEH$ z0G#NQ))DZ$$CD4)b0|nD7agx~l!*ECqep=wnseM!I?sneyq@NV{HwxsG5TrLXYEf? zcH)_GCm*HSmCTdugf4L1`xSV;HYI<2xRD?-db%gaVoYEK>adWhDIF0@Kbt_^{tXn| zqxKoRvMa0=x%12}&#dzJ<;{uWidB9=ZOkpybAm_M$@(PX_50l#laUa(Jv=1g_)=EZ?%8A`iD`+a#RGt)i>vxYXziSa#Mjv0@UXcq8Z z=VQziug-bX1|*1&WN#?0!k;Sj5JFj$zjrDV;BxDPlU$8*?%{Q{FkCX1t>yyvC;vjo zj5<45okjP7=%H4>&bDu!A>!mW4Oh5>>V4%i{d)_E^L196wW{mRa0BAcZvhN+--?&g zV{qVHWnAl|xBbx$U6xMKJ<=b3_2|Y-t}nyQeGkoi-+|t}R!54U6f=K2ropV{n1-0^ z$Xs41{)X_D(!p?j2^4TIm-S;6f2-PpI^9ohWUu+(-W>nv4;HWmCzTqbKav=gKVKO~ zmb1Pb>Dd`)vK}-gHnY7>-kQSuwaw5?i2u8Bae{(Ljal!k&mvrU~uJ!#O8AxrC)0Ja;2=(z^sQn^>LKBF;iXZm+PRO z0aS8|tkXyJ&y4gEKRzoz(0$k*L1hg*#Shf%QuW&`A7kcnKGoLRynfrY@us=ydev;W=?VVkf1jC(!vf0 zE~tBamfXYUclT5qp`Z-y&x7n`3qIBXbB8ILx_T#J^b3(DI+jcg_+*!4M+=Ql?Ho4^ z?3gIN&<3<$n{sTb7!^uIIx`@GM2#}4R`~xLl>SeAeN2@gIMP*1WgvQhH7T6HE$Pb@ zT2$(Cu&Tx#0_E! zDBnev43*x?tT5@A4Ko0(e_AO8Xf6iYW(GDKO@W394GddX*9A=DnfI5mCP2aaXHJQf z`sctO4URbYz3+8SUY0IiHl(enuc2wpTGn-tISX)1vPfm0a2&;^RS6v&U4BqX?WxyhUnmEZ}}-^5;in##7N@br`Ww zDr`&sgg)i`$lBIiyFw)KfXksi&*}UL^J{T zLR{LEb9Ubzpqwv*4CzA#s6$FYecAK}xneFxvaXA!$s=-3wvd}c6_KS=q+U}pb#Uvo zZ!7jEZcqMv_v}0XH)m@7=A#EI7OY2`N}t2g<*yrToHM>8nKZyq4$=m8O2shLCnSs{ zb)~E>W*O6a4X!cwMk$rt?(V${v?B-@WFi*Aq)T zFUW1k+S-TW3Frpe^*|$4ni45k(#OAVGjZY#oU`PAsCL68550}wrp1eY|MTzvgN2iJ z%YmbQ}|t zGPe2YbRH(r{0L&s+SQ}>KRQ@s`@%lY)mT8XBi zjkjEUw?6BWcJL2Z=Esrj`{E7jRoKy-?qh~J-%u*C;YMYeGmY@ZPkdkEKm0mrfqVx> z7bnqcNr1KFj&3o_ojyClW&RSxub~f~|CnpZbgKHwTXnSjPJN`&%Mp%f;DXoe_6n@k zQL;SfiKaWs{N-PPVmihq*v;$v;-}O}sM+`u?LTbh)RpC@=y%Ih@AtjGe%rB3R8$K! zQQJ@gfUEq?;Va)3`h9>&QC?@xpWrd8EaekNX|4pcau=nU*6{SRMz6IZ^gG2+@Aqcj zlal5iRA9-%W-q>Jo=CJp(JDs^B^D-b>~}z02#Fb869CMaHLe2yoc?pSG~EaT{o4`` zx4XYA>)D_U8)X6V1%-tdgr zPRRd9yZ>_$kw2xmMCZe=h?m?E$4bSz<%ZBMr5Y+vR|^XF$SJ}k?lKZ<2H*I#ng)}) z>&gme)$a4Yeq& z;3pvQ0Li(f5AYZ(SaU2HQQe<__yBX?;sAU!)z~IT%%Z94{kZM8P>i*G-gqmQV~gNx z_o`*%=Zi05%~JbFD1U&~O5qvOEwf1g!xFSpsE>wvH!n+P;+)A;X#RSxB{}Z81(25R zP%!onp>OG&f4!aA1BsXAUF-@-<1^O>EN`ANgEig*+|1c!nNoBD&@U4DYUzM*g$mt7 z9?YJ6Lh~+5Wm&Hat)*#iDHI#dr0ZtZ zZ+R~j!%1Y6=(kauAC0`=0Gk3GuUR63p%mMUEG#?9)KgRO4B&<9hV48I8dl2MI10`C z{wwUxgFqC*G1}hi%?Ht6d;5$m`3d_!&x=NRXGl>RKl87pmNX|XRr&XS5;q5zGiB9N zRZ0t3<1g=Q0HYhf-UAg177x8=5zf=`ic5_*(91U*ZwSDT8-y4%>y8KF@U~0i*)(PRv$TJG$uL9c2yF8H_E6l z>$hQ;TD`YAn$dLUqyzdRh*=|EW;crBbwwpG3H*?hFiymx; zzl@jNA2!j%21q+ky@Vai_*j4Md1pMArA$iIa@S$Z4z`qNAk%8uPCYE)2^`!b9yp?& zAYTJrVo@W{0nrzo3)Y8+pm3~ zcz0Z&5{65c6alZ?aua}I>YURf7YXp$z*RSm^S-#!MOGSI`OO}9bRK}yr=5tEL$-Mb zWycY~mcJN(dujmmKWruEUlHCr(CCh1KWlE6P+wYFVu3K60)DLNS8sCcE>PETa;tB) z{JK2p3^yb0zIk2lbh|wSA|Q|pCl#v~;&k8tLa{Nz81P*~BCwU=gw=T-7PlyS4HCV9 zezpvbrdl=x&lNxHZvfvn|A*-&-BBYmY^WnK{q+gjME0(%SI#k!QMA* z9Q-iD3zf1dk@7SC;MwskFxGF++XywI;PhB->e{fRk0{-!QgKGIA;?>(&EtUMh>vVxa8Suf4xBum8ss1PuVD()>nvQ*M^b5=uO0il8sdgGI%buZi(yf_u< zc`cLMJn$&jOCaiY3{FnQg+{_6=m55#G3~d7oE0|ZTbeeXUSqV9Tr6W`1F1_q1W}WxJkR@K%h&rxq|APv*o2MH+xE$w%(Wl+V~|Th z6B(X^k}lM9J%C$vXx^v0VuQs1TB>{49~r!X;S4;~IgmUC;0^apU!O=0ze5}od3P6} zVkkx{moS~oTFpJ$&i8@AY|qveS^bw!SLDSHho1LSt(;C&mM_n!T`!-&&(#sGI36~* zoLm-xRmXYl#nKx1ZOM*B-u`0y3+PTM=-DX!V2MeUfo@xz2@@?()IEzI2j~_3nK{7H z3JKI!r_o>ctVwVd01X9E)dnQsk(E#90*<-%28E@}{LdHt9|y^6x@y$Nt!WHp$nLQL zLzOv~fHCYE{w;ia0k_w?liULr?~~2d0v#)>GhsgfH~e`(KK(UfXc$0zo|Y@4BL1V8 z6eK5zVQNPQ7(HxDUhWVO{w9Smy0UIY=$z9~jNb2om<(8kdeu<;qf=;Pp5<(qx6vl{ z!;@L#kl@M=rF=Hm1U7uhZ?mC0sC6#r*VzN`4I)}HBv9OV(nw7`a}K7Ef3~e$3ur>f zeF2%zvn-}?Xg#yjF@9~0qXISfx|SZ;H*UNGIfX%A9$qGMzoKL+h3!6(KX-qhYQDFK zc=TbZ#(eJ#NbEfT^M8c5=W3o6s>JB0aC8}DmX@e(tRl-9f~zougoFSi&XD<-s7OWU zmNReiR5$CKSg}#k0)^|H&P}w^+gyc^_%rz>YLhW;Z?2AfAQqp6ySug0MC|Sk@(20@ z)B83_XN(ElIWR6X@h5BMGx6JycQrLXtBC} zFruR!52za3yeLnw+Cj%3&h0}z0T5%~cRE!1StU*Dn+)Hj##q4i#A z+G?gZ?WUb$4a%Fqlb$UYc8O30(_x}8-!8V(4zEiqNS=O*4fpg=wVP$d6Qn>)z*By3 zngVHMk+EA^>2fe@*=>uY#K)2c8j=w)8}z4>k&AR_UEmfoQn+ze-BNW~M%8(~X-eu> z=;P>P7NZ-tKkZ{9IUU%cid@S4OPB^RiUZ}0n5ZV64mB_g(&qlW}cZ||fNuH4^*DqR^mQaUL} zSd*{2dDZ~#t&Ce_$PAv)_g@Y;-GRm`Ht%$?-j)a#sAyfeOE>7uraiBp0%35kYQ^Wo z%Z0K^p^ji z;AJs@71%?a%kj7W^4^##+6K0M9}l2nbYO0FTD0Wa&Q|ar z>%Dbq7qU_UtIoqr#h(ugP>3B?UgM1#qV;Io1KR#;@z$qy0h6%WPxb6{%yP84U>VI^67Z3*rYmjCX7=g?X5`6XrL(=T5_SLW^6 zeGA)wfd{C!^C=OWo!W9o32V~3sLp1|qwgQ^Y0qySkX$8Jy1f3jV#7Agy>Zjzw4#D& zWFH{Zq4c8e03nd(%h&q+L3##^*N0Vl&~@GK6xsq1$;PfhxU{%jeBhjVk!&7O^i^oo z;_bP>NAZv#u`jsMjySyOo5|2LQf{IfP#=?!gR2Oax7)OQGPiR54!q9X9leb%h$;EF z7U1Mq>$~@3gk{YYRWfO`e7s1b#CIsW{e{qh@ZFW2a`>c;$?cw&HKLW#1IIg_pcnUF z^jI*!%j*{&Gw_ITL+@h&&yPp1aLi(~;=l0axG}6 ziCcl(30UN|m53NQ05VZT+0wA`uWW+!vKNRY*7AENGC3F(oo2}KK0v48=<)eUvkpuk zTl2`aL=e1VBhUD`{OF2Efo7`H)iJ}?K+|=zCO{ms%6WOaB!VfRa0Mb0p3U4r80ATu zzs?f7c2@!z-lD`tX5yG?(i=4d{o`*3`hDRAvu z)rXU!pZEuIV+V<%eCk~ma3nz;iXE*h@n=`bbcXl+^z%PzRC>1D>vqcu2!Yg?z{{kl z1cB5Shc8+4(U&)Istk`_sW<7|9BcJrL)QRM$m&kwxUiZ2B;?X;4eh}W>7oWGc4U*w zbfJ0|$f|&oJ=(qB)PJW_>+boZc3HERBjNlgEAxdRLI2x|lMcm+j;X!=uE9Rin{O-S zKkj@sgwC@tr4lu}e#fXKHk?Oht{pBSCMu~}q17R1z{>NlZ@~&wS4GU*()G75MMOn; zd*8mY8Ank3Ob){7w=M!(PB{ht>XcK{kc^KpQ{K2QcZRII<^nt^9QDfMO59h81=DUH z5&vI^^{OC8k%>j=YlEJqJ~07vts)NO&e}3NDzY{soXta6!!94;(-do;N-XoWLk>be z-hYc~4BrziTyM?`Qlg7Z%3<5(BN}~yWNZ}of2UYwYA~I8rLj!Qv!vU5@Tq3MoU!MI zh{9Wo_{J+6R;fz+uK|2a9_%33I^5U4;0&Kx4pIn{FTlPF*&BQQ?p2g^RKVd4>hfOT zfucDr`9M6aIoz2OG5gyg0EBALL353)&@yD@bvQj)AB7NP)`-Tgm(GR)PmjWqB6vt$@@t1%f+ZUnG+o+pV0p> zfx9ej+>@rfVqk1<%|NTUuF4>z?v5y_>g5m4mC!0b_98Q<^+y{Q7c;f`TXzv4USfZ~ z1a|hX6d=TBPjMIKWi77Q!C-*sf8@Xx)Xm|SR%T$+lC5~LQXwbgN{k4ucTenq^V+zs zV7EKTkodHm6Zwh+I2_oN+ofr5z+WSOxQ-Q!1PX^AYoqmSGV?Kc=wja|Ch~{R0nS$_ zwMP(GCKo(RdmqQs&%Ux#Z@3=L9>zd#P~p<8SBbk?mwSuNhqeNHL?Uuu3Z30_l}jb8 za9E^fA)4LmSF0atRA7@PcE%espdYhLh6y3sAx7)TnBsuCe$=3{i`G*JE9SXpJ=thE?O6PH6$0t`>Kb6o32}=Op#lc=AWJafaatM+r10k1)yHJn)m98`YWT)p7SxBunAdXqJf3sr*iBEHe$7(SE?iv^Xbv{cdej2T zv(*E3f0D(^?Z6mH_aJ@KYywbM=Yghweyfj4SSf6y7QLs%V~YL{VX(!c3ptV07W%K{ zqQxQ$6PkPW-bhWYjj`Ux*&s>7YL^|=7K8HGJ&I$&70F=EnChA2qJjfcX_G4N#*9yF zt0n9nx2n-H)|^HvVyMC1q;WR+gq@pLPN?PMgi5P=iyG}h-X~(D!X!?(ZM?#FQ|?nl z8dh+wZ(vrex~#MgD^bQQ_@*GGddlR6Bby z6p1`RWh||wV7R=3P6VgiJB#$`%^D{54jZV&Lgb^5qia9CuMZtYY_wT-463Gm2a2xe zs*c5>;+%*rE(am4HkzBb63UZS8dwK2)pNe7iMk)TcOLXszv0x6WcU7fUH}5gKb>s9 zPdzJU*@8kqx~Y8;xd*SuFCtr0;$d?GQ&lU?qrU(QGzRF=bjJxCW0byi8`&khHW<#; zmCgkuzZqcx-`~Zj>w!}OGREZuff?rdy&}$aJy-_P%NJF3m)(XWG3x{k22| zig#|hlmiCrBoleV_XZ)*+&02WL;jbP*rAY;fC$H{tB)ViW^zL374eO#6W0-L0i2?% zS8rQ=KR|^|1h1KW{KC8<(FOJz|1p#vN-cHw!HBh~<8H2-pCu$neZKM1knCUPL7Kko z`SHx6S7xKrPwo!*u<0In34GKi-ul_gu~uhh{|*n7o(5SIR=RgGD-&f&hHtk~l_0&v z&v5iU!SkM^A|4dEFdTn$D+QwpcX){z^hx3_(i#}lJN5E-?}sq)+U?Wo$=)kV-kML$ zT{VtOK}&|+nIGnp&QF6Uz(%d`d)*Gl6YBT*_9cyb>y}creM|NZx}F_t9q9B*opUL* z9k3xr$xjF}W^?`LT5SshGV)qXTH9HI1OW}LhWSKGdCPeOMY(s7ulj$8S`}*P+yK`A z(0k1s(6HpAvE~}XIA6g?^O{<(u}9M}0?sq*M;C+*V2DlyptI*P zhrxDtgswes(EXJBwpPtW{u9ciyy@3&%vrqKWp(G75Bb1AZ$AdB8Vhn{F7fDH@C%6- zK!brEoUoj`-%cYbY~xB0Q(5id)(+EEmxzSE;bbM_dBr)I)W+#w&`y}|Cfw?HeYQoS zWl(Zu{e2+9B-2Qe=mP-pm_s1Or!T78g&Jx7xQW_Lh%v#WH!bD$c(J6Vu)VO2d7}^1 zb5~IY-pp;o+t~JTNIp{98a`RUGF6 z^eHPMVw9Y6!_XP+68_R_1aB3PO4`i6g5qvd*5w%=lLL9V)Z>}t8=D$Q)fTv)^u$Z8 zsx$5WwXpH+qu*4q7@O^*wD?r#x|C=nkmhS=y@J;W_Q{p1ba^c|?aPB|)XWbdg=0kT z{MRQck5|4vWW|qGJb>Cwqs@aBAyIiJRg3opN!3hOxJefM+os(})qI72;w$2n`u5eH z!C)(4DdDt_uaO9J4hDQg)d8y`^IV2n{#)SjPg`UjHvZu0M5jnhSimbsj%SUTdn>-y z@t3EYsk4|W{jQ!9E&_AsNu~6?kb290eG73766jyg;=KH4A?DvP28k&_UL3_21;%8Z z6?)hoqyKpBXo`^^zU2%sh~jAk7^W8EnEkc+@F&ztZu4d2GO6>xV4U4-*)lcT3|L-Z zemm^d7S|ynMK0hIO*cba-+zcG3xo;qJ2`z|oqS1GumGG5Z&>%DPk#$HL7kMJ>0K04 z>Qh56AnYY?D*l#AY}ydDF{|7eb2DDXRLh-)Atb#n))P(#TPJBqvRtnhtkD*1l2bCN z*8Z-@dDpi0>PLy4`(}!#n7iCAx9C(<>A9{B4bcUr&EJ_z)TFI)>tj=q>lmV>SJ9D? zAtVPY8QEjoen9)_sH@kjd*|`tm9!?K&!B#piQH;}DjJ81vDY)y>xCYq&+mkDUUas~ zq9gTN!ei)PKY;Dt5-56$p|OG{DNqI<0>@P8TQcUFh*!7oyRb)~*}|?(2y3-@EAF+y zya1q9kJfU2fRcN;tZCC5zO1Ex8R3X_dCjdc2|BzQc@&Y+HbDxU(qgQ=axW8-6b!$@ z>|DLiTv@o&et&K!8Wa1@ZLN_fNm3y14N};Gq7^>2OGZE#Kx8AS zO_7Dc$)(Eu)SaSf+O)qmv6b%DT!e z$GtDS&toMD-deycwA#(H-K%P*^|-W>4Li(Yb85X_lH74rf4o}!bDK8zYSkq)7^I@FMZ zu5LWe^}AhYq?aBK_OvV0oGb{ygI$q?1RR~fOmK_VO(5~wna*^(8CwWRw7z+KB10Wd z!Io1WXL7GA)upkJ;b$!Y4FRzL*%nGWYY1P&MktYgmqdooo#G7ez|h6s@6;}erpWbo zEwn;U2~MlU-~bFRYq&BlQt<|NR~?T(Z&GqH#^EdMe*`uw5ZL>$=Re9`z5L}peZh=# z-%wN+I-xf0Jz(F9*MB2Ug~#Sn|_-g$hAxnE1S}I)D)As|JNc>@A6}2 zOme0Kyj*FuaIG}Gd2MxIcK5T?E}1uRGa#=Y_BJw)7$$M8`eBa`qVH$|n0f$S6@r=H zP~#5#f{nRLbaz6bBW&1^AzrWpDFalzFY?p9pV3H1fwl=Exd=f9ZzOG=c>7Dn{AwK6 zH)+`E`%cW1Q(I-<7N)Id_(cc@XosTXoXz*Y1z=IDRaFlGZ25yI<1LkIGLy{NI{LoA zJV?D;icWCxS*S9xi42w%F1V1?>`z`v1LUZH17qz$862f+ME*!BoTtO*FCqA}(js?K zqUg0#K9gx88Zem&XG1<1^Py5i6POLJg)uxw^j+0!r z3s7Jox&lScN19l3Qh#J^SfD#5x=+TO>3?sQ9WtZ4{$;wF&MGzJW?VNwvvEM^L_Hj7 zPXmwe>dllJEiwp%9_(8J#4?D>;$a#v(_wE7tsU+zO+vfI%z=^^+$L2x*%$axnloV= zbruzNSSpd{=6J`YFd`oe6x-B6FDFFaU|KZDMc#^nH8*fpPjp(kSj_em6C}ETYU&2} z)WUR_tpVTem%zOrI)k2TWC06O`^sm>`01U-!st9s|Lv^!N5(0*t0BBHFDidkS=j*` ztXng-q-+%}sPdZ8=oB~yBru$A|NfwXG}^6{tg}VQxs*CcYToj_1S9PSpIO9_ud}jj z*<|=A0GuycC6zC$yqm+bbvFRb7HvEFijEyh)@?N>>(h4;nEL9E*H~_sv@7$w_fvZZ zu`Gm#$=`J|fi>&)HkhY(Z?9wqWo63JW?!Y@2ny5}n-ski1b$#YsvG{23}@N419T+h zkvCreS!r}gx8q$mr%F_JY?!oaG(cFvUss@Xn?&$k-1{zBKa?7e?GxhQyDy(n+^cXu^?Sfs<2O2z%oY-gBn z!hl)D}v zzC+-&Ztd0YNmPc|eT;7m;ksuxthH)H07?gVii50XL1%b=fXS{I#_;95iwUC9uxOFR zurz=j@B*rlRVGX5C2W#JM`YMU4Co8CbbX>$9VL&b-?jkySQqT|Xfgd{sp#y8DHLdx zK!x}XA&5mrlG&=9yuV>5TZ~3dqODVAeHG#CqdTPQ)=D_A%hPcqqCSE%l~x0=z{PA< z^+gmLB-Zu~Og`?AO+VLfSpMXwM;UI*pRpgiBiQ0hYn~XnlHpg-eY4}rt+o4pEd{UPt##9|6*HDH zfU|Lae3OZrq+-Tf!+Y$;kqcx3@uBvkvIiT~B}7uigiL^zgX{ftpx=I=vczj_JLsEO zlr(i6?Y=pHQwL-nJUejgY*77BEt&Vc)%56}UI1&A8MAVySs^A`*l`N&XJjK1vp~6i zIf!-jxwhh6|3|+{zW84ipj}~zV_u_i|JHhWN5*~dM&jrJpa%NczQ+Y!ydGh9I;?!N zHXsXLvQtzw<5U__Yfe^mf*<-ynNI4Zqw=Ri;pxI1KFozJw13nVC*Vf9>g3#!F)~vv0e_yDg@Tmmx_*@}Ss*62i4;hf|)sKO~a_%$0)BWSS@S17ubcO<{4?u(8&aO9(=4 zHtcD4b0@y-<13Li^n4>#78Aj>4imCuC|7Z=RpweQ2O(B0+m#;XzWEWJ7n_oZsWURG z$p9uvigh-_Kb~c)e%-pI?*iebS;?%8e~diOKM}q-k7yqDhY0O6#8HGj|02RRAoNv{ zA{6e{U&K+Y=o45uv0`@zBWC{$>sT-#i;RE#la;=+_G-Yqws;tM-(xY7)e8SkhQsEQ zhUmuneTDV2)#CCRHZkD5LVlvc8dY~lNQ^e_AO}G0)IC4y^8|J=r;mZQA)X>v=7wyN-%Yd+OVti1Yntzhni#U=6;Si`rLDdmgsc14%eT zPUj3L3=fw1)JhhAehb2ZBbq(Pg@IJ8(#hH+D(zVC?117}>9Ks&o#LUQBya(P8CBEeDHU^>%)dILL+}K#DD;x*Eng zN_9l}cdOZ@x-xgmlnnq4(lAf*Or~%-ZMTvdyI@?Y2O3o(`9+!TO!ja-^jwOHn=FV= zsa}j`v80YqU}?I%408R31T<-P@~vw#U-!xv*Omdkde=%b2{gChvPQ0dnZfdP1t+Ht zrOwRaD9^@Q^n)!{b0$Ui0-FPL$Jc`HbW5F^Di-y_l3X!GYlfXW&;Rabyrdd{Q}KaN z;$WG&Gqy?CG>V*^n0W_}VUi zSqihqH9D@~PM%j!#kL~=;cF1UhH}ch@oM-%eGUN_L#7t?afVDSWQ%hp-6zA3%lcgY z-g7he+Grv!wu7xXZDbSo%XE+4!q?I!MH51)|K8pJ(yc-|&jm|4220Ux%tUB_`zs+j z2$6GbiGQr|9%#D@FWEE!*f!=jm<$@cgwwPu54t3pPoB-UwM>{dn&i2~>;YLsz;l{V zd4b%HG?1|@=*^q!NXyad%?0feidP^y!L6 zE_QDWWgBsuGFKTJpqc<&(k{8y07v+yO;>fHX@W3C#eNH!Q>vYL?8qA^itMIxCEUjQ zdfPqx2F5^E_v$ebO(f*Tf0{vErYm43z|)SlQq&iUKxV{lZ|AK#i(U)S`y4Ca(z3xA zuze+?Cl+)Jb?f%qY5~Qj=XXo3l`L}jjz2$n_y*ZD9bfIfxGPg}OZ@7_Bt@Hx5Zlo5 zqLe0CPqPeQP6@YGk9@%K?gIVy~ornQt@%#@DGpd&_IpXVL?Sm&)h0F&# z8>D_Af_{e511(h2qZUYfa{i2R_;SN_FH0qv5_X>n@P(0~w%pD~IW}y~x1j`>`}!kL z|1xj(z<@IRn)X~67n|#67>S8IHm&|QO0vHx!(NFB%66Xt12Q|=sWkNoP4&Fj8IsPpGW|)Kgvbgv8%5dFpy(?!h~vlpXFo5%A9J&LO^f+t)iESL-_c{A zWso~9p?F*A?whv8`k(~W_g*hpr>QsWUWmM)e_eu{_kGGznD}1`cQx&T2=ez11_I^ zjyh@aEoJlq{<(fqKd+(Md7>h#KfYbu&=qRP+A(`)E^v+Goaq1a#MM+CEelnbb5g0p< zN8T2m3Iv!MquHtylg?*GY$0$iZO@sq*9lgZ9M3+d3zN<^=r4CutW{82 zqL)ySOU-6anS9T(GydcJ`R^_CpFYg1_ktsa)gbc5~=wwV6 zJzE`7!ZZf*YMnj-hz)t^VP|K6aG#$;ndlrSp@zAt+_z{g(cq@kCvMK~vOh`0Z;Z(q z>a)T*24t$J`bPJs+#6j5Ui{fTI}LEp289{oP*m`~ULl&b3Mc0WJ^_fs(d$1aDns{v zev>v>SvCm*aC)=&n`RLmyn&;;ut9nItb(i?1Vr2D)1 zN`?@|1)kHNDz3f80d)&|04}Q(G_F^FSa0oUfGT*G=DBZlZx^eUeU}DRxwJgaS`J~8 z2OB1*R%FHukZabaSMOL`V(MLgD-8KZ{22gCp3P#Q8og`h`7|;Cx2}m&tJOjJjyIAx ztnmxiZ(G>|PBy2D9g71`7zGWvfRoY*sarJ%!27Qo9e*jObZD5H(lNTC!0~Q*=>-CF+qvcykRd^Jk zHNYZwGZTCdV0KE>M}6kjZ~VSsLc%FLXKZ2<`P-eI^n_2CSb8fO8Rl<^vO17En8xjv_*wJJ35#DAeIASw zy72s~ge^1Tw`*G$0&y2G{~yzM9}xk~T#qf|l%13+LHW^Go}=elN=CDLB=d}tk$$)l zrolyb-pe|xRJhyQrX0rMmrivYm9VDX`jGoL%9EW`Rfl};k9KZ_7gpG@1Uw3?nvb(* z3B{wUit&6g>mF?VgC50?$QsLrDN1Rr&F-Cqt(2iuTxnhPT;eu_l3eBdipznaB;Tyg zjZZB2e&Z4Lx3CE)vj7!R9=x@$3UJvrjIb)d(#TBUHj#tRgRNHr+NLb$aBJu~Nw}f@ zWr?0phbM#O%Skt~CZ}M8!vD9-`1dzoo+$y(o^`E)T!aZ@&rx4DkTt=I%HPn1#)i*_ z4YT;IenKA94A6$uro;BT=9xg$@t%VB<2=}$1sp1 zbdVrQCZ#MfMreT7wB--`)Pk6efUP@z&P)fc1XZx@Z0Z{>;mtZE?XdbK z+kF1?Y7P0x03Y=n1@hsCAKn+h79#L`xQu361G9V5eX+FbPljt{Txf0GPfQ6-MyLJO zyA}=K%oga~I_72R@Mf6meE0^N`ni+g_x})_8>T=^GdJzMbZY*M;I0#5f)$i{DUJ4} zXFAN)cw=0~6;h6wR#yyl8<~W~o4r{9i^VNvHTdF`_EeixRBPz!h6>J=qm?p(dWa+kvAQFRC$Rq@Hnl8H5e)&>V%jZhMr^0O3+4`}{~00tmfO+)2s zN7{YPPme|`ER_`pRsOqv?;@X8yreC#A1IfrP$Z2758h;?_#_y=`ZTPuSo= zICOHw7l&$w08+DpXivaz-ehw6Eoq-ksElb{^O#sXa^Hkv)~5d>w{(R2ru+!dRAvUt0l`QIV?Po>|#z7Z6= zY!~pi0d|o-jrrEV8p3|O5A7Td>YK`*SQFdZcAhOs@%_mw<5lWBpIZ!W;_Ei-2}x#L zGZ}Q0aNcsRc&Y)=j)Oo{riPKg5Jd-Qr13o=t9dr z^wt98Xn%b~`5c$I_8kx%B>uA9l9?|pD3e8iOr=7&_@W=H3059}D=gozFl68Pr1HGq z@6iL8c8*Bi+xYd#$a6W(&n2{PDWeY_ zbdV-neK2L+>^In-@~gxMmvevUiuv)v4pw?jlBdQV!t%1@M5BJSsHMn}-cyv=&n@+e zzmjp>SICh~w#zf8ebtRo5BPy$v;7Xs(6tsHfYhA@a8I%v(Pdel$*XUUSTfn@UtJ>N zx^$UL;SvGu|NOYh!=Se~m!XPF+o@b=tfFylE4FN#fY4H8K6-+hGb z)!j}+zD9}+-7dAR9z@$4;R=h>S9jBEKU)XTqBpH(Yrd5X=@-LS? z7~K`|{jj^>d$2$Lu~~2j|K{5-ai`{^itX|8^lLz^Vc|vha%Cu!vw44dv!mqg{5}xH$$GVRJwTpot6J)dLAnJzS!zfg zQh@~;iPde^v1s<$^9#brC4ckIxGn|v^3W2`bu<~I&SxMV8MV^X&0C~S@OnQ2L>T&N zpqT+NRu)DOm<{j*E*u;^r)w%MZxDOAH7iTW@qmvlXxbgyu+n8aHr8F3fA+=dh~Il7 zoGF9z{Aj4f7ThETe-VW|Irr6S2fuwzoYDII-bq+$=1X8_*DEH|P_C z3;}FN7hxa2q3Y%;=8FFs+W)w;&%5M?>X97~1FiDc1};@b%i@kPORIXzkNPwgKCE2$ z_Qmx1%^op@<-|$d>)M@}K2At40oaaf?X9Hj*MMmi*Sh3?A;f6{PF$|JG#}+vv{PeJ zk%(7`%LD1Dur1@@u70EwwYyosih0$DuN>U_sH!G5`cH90c$waR=|OP<^gC~#`v6mw zjJaz@osnkM+ry~{x04N@Frc@02OtBgqVgRW;t!I7eo3#E>0!@FIVwn8fbrX|VF1$H zy>Jj0u&{G|6dU4phRP>@_<#;k5Dn9u3NRwq?RmjH0B~(M!4>{{8E~+SwCmJL& z)_eS>R&c;sh>?lcg6FH?0^-?8iP@~2*`RF|dXXl0#C*s<2Ihm4@^!$dG;F|7`cBNsx>SG zsvOP4Y7-p?dQ$c`h*F(cl0SG1X{@D3qitcW!%`0vC0K4FyNI{MH@KKpNxl;8?(_a& z{ofM(->adG`~^qzS8w$Y(MO^fLHiuyfCnG!Vpaoq zwkFrCjmoqVMyFAuagJn-udR2%R_3mh=A7N#kFTwT)R)}pNbbOXGWkIHRr>dN>G7?n z-?6Dt$k7)JXoH4FIqs709?n3};$v&{GT-`B9Y zs`hB3R2X$_^2DM+2Dgs)woiu$%BTv(U}bIRc4EPrQiZFhro0io zcTN@sqUuQp_M>05e+G3LdDEj%gsOW}o5}$1oYoDuKuQzgO3>;@hX1k~{=ZNy4O4g* z>Dqc#Y$rWY>k|_cIV%eV-oxv^-@{9833{7vaUXOjGMe%fx*HKl@{S33xb~-&@24yub}szD{=8z{7&;NSL?y1~k_-!{=E-T$EKn~3l z^N0V5Q($HC6vd6D=xgu}@|r*btLuK?>0I#7cBl16#X;4kb&^d=t__Ugan5u$iyMfc zG?v38D8YrGs$LRqvQJ zq*eoYgXCpN^JJm?8;;Ac)G5F6jN~ayV=6+w*FKDI1%sw*9@Tx)^mhu>h_HqYY>B7 zmta(iXn};44or!Jf&Q(Y<@FhqO;Tn;7~mj zk2l66YoBBGmaI`N@Ew_r4ULYeka(w~W47oTBnGz0 zxH{xlJ>4w;G>qmol2#zQ#=-CpQLM=tvOc(dIhRqLDx_+9HNo$bpc1(u<=l(S&GF(| z4<);2Am?$Umm_8w@!6zU)7bK6?rfd0Fx?AB=QQY!ua{1fM@Ca+KfK4Yb^zpO{--pO z_^NZ;>*sy1gjmdtNxil*$$lGxa&$7_>Ot}|6@43dP3j)Eg!eP$yIRuTC)tLGLL^0N z(us;c;$^n#h6nxoNLQU+43l2PA1>X4yhK#Qf5ZG%mH~)o=!59&@3%%>nPA>G*fqKO zcdz}{lfG*SaPDUHK;Rv6xIUq)w)f-B4yN(mRWW@X+g_u;y{j)%DxV)xn%n1#DhXeI zH+El4So|-rCHroh%N^aVfnD8#(!S<`0EaDb<7)eo&1>YW59*=y*NE|wzBh*d^UUGS>n$@s&E1!Rj`@-WV|~y1vPb+~dOi26zP9Xop)?&by&sYMK8EyY z3s~;D{eOJDbzD?i_dl+PQVL2dDbkG!C>^2#0wOUq0@5j6Lx+HbAf1AA3^_y1APoZ2 zIixg0$I#60;Qjob>%Gs{=RaQKIOm+b_gQ=G^^Wy&2*ecW{yHEDIUMEgwhWvlP6ezd z0)Rekfq=;z-+BYa%K-4HBLH0Z3Fgzi#UfP6pi|DtrLH?aw_8Yzn*E8yNYmWcR(ajN ze=hYr-nA!TWiRxA_NyWv@VrHqNDbO=xR;D_yh;~?zilD@a|p#}dOhs@?d@tt9KMjb z`huE_o8A~#!3U14=ixUW5Q;Lcmo}pf0Kbs3wS1M_Ky6z%*O@S4+H+5V12t+pXt!|Z zMZklICn7g#MaI4_^(0fIfzdVF>BucyKwp;?8w&<9vVuJ+8_n_B7O@#qAaA|nxhJ6! z^rvXa9w+gm8MY)q>NpTY@GXV~P7RzlZ*SIZU2HE@=QgSA@H>nYech{+#%pskfNnjf z3cR$$ST9F$&waZV989=@4CaDOFz*fZAeWKD3GWS>WYpXwtE&xVB(}L2-YkCyJQK5U z(4pIaW^2^^1=F7frql#iHGXd-)+UKDauJ0x&&Nha-zK!#0elAoLP%HwA9*>(1-j-L z#5z-4tDs6{!$&Ud4k_C#Qnq(Fo9r%ot`)H&f zJbHP_m#XWtQu>9-nB$;A*xJ+&{CVwZua&6#a~?CNTif%%f!$%Ly9{cJIF4(bJup|z z&a@p7;~ul}%oIx>`nImNv@zfwf{@v$Zzlh2iA?uex@%Mi_>BhA7@2*{iV$JY0ryn3 zSt}?Wr8PV7k}6oHmm8qPR-3w6LFoZo8ymUnT*g?(Gq6+u-*0k7YOu zozIB|0j~{R=?2#CT_jc`MJ`QaEa^A*mU}@}A=gjW;h>dicwOo&fFV8=5%j7b-Y9F| zG#DQ+p39EcCk*yR^Ku}a;&qz{k+yUtHE1F2Eu>3U>A5p<+;jN_WC4^@Z-_3~KZc*9 zKbE$VCkL}Z>WOQM2YcGwR_20>;nU6&=It50bfL$u{4QTR-xEA)&yGfC>iPpHvZ>x} zucynU;T%m6+-BdP0n_!=SgWsw*yhb5v)XG7;!|BB$_{6wwu>?PKG*;Gw)m1uXDBqD z3b;4O@rTPAYbowl_9-%1LJqys?j2U*g&^BV!Km8|4Fr)m;=m=`J6t~08 z()kEUhQ(^iLlB9bM^AbM060Vp1KHbT7pIay_xii(ftUaglW1Q_ZLQEp%J*iZ zDjtD=X908lU3fkWX@e{sj?+$32qvVF0$eIyt%K)UDVhQBk5n{A5U-hNY-a5Mx+QiR z&=W*>D<=0577w+6VZeE>s<+0vw>1OsP-5%{5D_k;b-1fFIUVG%LTX2lT53Ca68C$7WF>Ll8LH-4$?QX!f~! zZjJ0_MVNcx8%zHGqr4)ecW5)!gtD!_0|>gMNlZ<9y~Hn>duV#q1G{d3kUa0tjRvoV zwFe)JZdubDMxGvi;!EiZVew@0G#JV&1=QvmHJgaLUf-@lJs0kzhn=F|9rsE9l-8V= zL3q9qZgm9>cwJ;%Bf81rx!8rC5D)!PTmACa<$v3Le3u^F7WD*|THRm1jY3qE=hsb( zdZxRf?}qKwB1`ImSh7}FJ{=%Dwg`7;MF51!y0nqlV{x1cCw>f|EU@Atkz_z{$#Y~dR%m+dX##)41O39L~%0pUj0683} zPe38uf&N#OP2wCou06~VKV(1Al~ZGXl|eXXjgV?XU-aS>pyf~j9xH-%d(SDZEncq& z%vA!;50RrHUhR?IV*8QpRBrnSK%Oy|>dL2hx={?BcnAn)3Fgj;#7aXPR|lRtt`C)& zy-A&)dOBgtu^K@d(itg+*6_Sy8C+^9Qc|ciWiIvKlXm~M(UBbHb5X(Pm)UA+A*yS% zwFOG+-$#>$@<)_k>Xy#N@S1d!%n|5Ql5xY=?FpFdFxQ9zR1oJ}dq3;Z6W&Z|@P2he zNPsj`pbsU##XDC3yKc&q346tg){JdiP%ydE9u^ZF9#TCbzq^FyH^ht>rd!swQFyfW z-4dP0E03;+?%x){tCoYSL>B29t?MWe%f&9--RNx9E0(X0oh^*IO>7WL`ld0a>BY-e zfGv%S>&C&T!s9-?li5c9rhUPg$8*E;;_k^4yFdXuvArbnf$$W5(bMn3nzDvvVQ?+b z3^DPIThwNLTi<-I73s}!{8h#4v3rQ!MV>%0umCPk{dT74XB9ulg!@?fiAmP{H;=8J zh|@j6Zb!h2;)``H1_I$a_}5lK-}e|h)bNN|_c@Qoo)148KUMZDsWw9F(MJOBqxtN) zpngoK-zMK2z>>U0p|f2G1SQ=k#|sc-@mGAMsKfQx_6@+;gJNOoI9U9wzm11h)LARR zhpR5Pq-EySm546QI^t8+GVWdSh$Sv=_(LZP*RvfyW;88@%pRdKu0H8?O(qJ3w*ZsQV)a6LY=LlwGJ<0LmY-IuDNzXREl(VLT&20%vhIs zh24wQRHj=zi8G)3>3gBwD*N7x>7gan4MIxF9K~cUt8e#yMeZTlw~c<+$qPYWoCHF? zcsk~O_?eH|4984*y%>kqUVBDyR+%0TgEaA(iLbXOm4~mfr3h@O?cu8@;0DqRT<91u z|5u^4&l5W{*j9}l+vJTo^N&SQ7l$_%<0n$$_eBnd2FV3xg>zGg0qd8Mm4iC^v)#$P zOv>uV)Wq``@Ij$|uZRb~#VDl*(1YP52rsp_6)C4nt(uyxTHg~e^~6-(eAD^X&U&mh z^V21nxv%Zyd)#yC)Q1z6y{^gD;(MiUUbU6Fl^(&CuG5$n-oCjTfo91=z0-Z~>Er zuZZ(o3M{dA+f1cTIPs->j+i#25RO(@Wb(585t#N5Tk-}>g__&?3+1JD^mYRDX@`YA#j})3 zF|F6MZ=y3ESB-Lg+x-p($lB5{*80d$Jxz}mcQ+5VZhvgu5S(mT3>99D`%zlhca>a) zM~%)~lt>rs?;(zn>)L~N|7P!YjY%T?E+Uqw0zH>}+?(Ti%H7NgA*nH%_c zs!6*Pt2c=5Sy9ZF(1E30Ij1!rUK}sW_gL$?d~h8IY)%vPI4Woj?p6OO70=4eQPd|* zBu;#p4H3v!E|Pcp&+a;N{=5G7ZS6rH^hNK1SCZ?NiC4L@&Z`@rI}$iKAaJoVj-kRT z+N0h6l*pz|7VuO6^8I{TxmdTrh=;fMS))05-Qf#yAr6tFzGjQ0q4BXYrA70^MUzk6 zW2nyn^spyn8^hCubPgqYq(Y61i-Jzr%afRp9gYmL@Gm4g}<4UVhj8I)ay z8Ay*s&7$1OLa)9IUvhsC(W!Dx%hX~Q=-m!od@I!d#x4Bh)?-=LsL^dd${3eDgXHmR zlaJHQm9c407oH%gi{+nr?PSM$?v?9&VY#utMW+`^MB=^$KP&aUYqdz|oU~&-8R3t2W@vRIcVEw+V}x zAZZ>n3(%2^MHqY_E%|``TbAz~%w`l6ey25^4Mdp-TmGWKc7>AEFVlKFi!2N8xwam{(gs~veJFT21YAiH!K_OqBr^JeyUNum_OcY%qB(VI zOjl4(^pMvjbca;F>t7jq(uFe10@ndnTJ2?uuKvHpz+Xp`VMTiPBf=1No3bbl_+yK5 zXDp5QO0D?5wbuLlkAB<)1i7-3n;!COrBK{>ZR2AL#m-TN!KBuxEjExE^mZa$x9m;IO9f4r?i zyvk4X$wzhUO3|UwAhqtU(XKa6dg%-1IOa>nHp?}zDiO_)vTGECV$8H@{(I^p-s z*Ah8GDyyeqNa-he2u2IPWDOf*SUDIz!s! zZ&cMERFr>ow|~~JH&-s4`jS4K*k&5sg zj(wi=wMAR7AEc(i9zLR*PT^6-Sth8zl=Y6gG0-=gHJR0NBh#RN#U_49V5(Z@htcRn zzM{XIu)d-|QPF7WtC4pFB_)IaO@NRij)#DNf)jTyo|~=LMnN&U>{kKyZ8q+TtA_3r zx8vGSKlfTsny4_RZmnbXNkOqP%w|d|o?C~vD4b}~GQ!6Tx$4*McXVnpEyC?uRD;2O z@SmIZ&0*=wdJ)-ItbOQ;Pzj$m3nTM7>)9~=Wl=UQf68h44>)AEO!FVxzO}jxRXtJ?6zIy{QA7hDT9!NK)`14tA2Ck66r!)r9F|y~z>NxZn0Dp!Up&)_U@$Wm*gKiMUCZ5;APYuUaXa zqPy70p|9=D^q}TJ(OSJ&XV)mjdZ_6dfaae*)DBM|e6w&(hjL8Bw?}>B&FL9uvF=?D zx7!}6K*k2)NLx+PR_UHrqVTF+V*b|g*YYgFzPvkcwV0+S38p+0H$h}f8_6L)+^#E+ z3VXSK3tAZu@mN3li+IT1dO)vkIHJS0x2;g`ORPWUcdbaog~V0|0@Bk4zX5H-6(&e0 zJiu}7#obnwt>*3OCi7#nCPvn8PT$3az{7K#Ye420n=Nq1NPAx?*L~Ry{d-2}zbfW$ zH|R6K2=sYj5^04?e^X%L#I23{eMQ*&iNmJxzYk9$HfcpXFX1iBOh{}3KgeR3&r<>W z$0w^!KF`HgD+Wg!#9y^a2^16XgENce?AmPo8brs8k{QvskB6^E%1xLo;r}Nm&?31i z`XXTH306-h=iYWWclj!(XfL&mk=xoGOLBfxkApfbwEUHWw(LxOx4qlw(mFnTQOlJM zsKbx^IPFd}ZL;|>jqEIXUgwE9_WG;h>{}K0ZQGwPzr%Xj&p_q^IdUS45;RcJ+15Zw zV*Oo41qzjCah4)SOYh>`j@WOOpfW<9S}kbHvOjt3PJ zG?g62%9cGj46OT^JG%R$G~xnnsh#sl`PbB{P9{vwv$}scR0#HED4nwJg9_8=k>m5 zha#rkMF;Pz5KUF9$C6m6<@f92LhE^z)lAX+6Eb!7=#dZqBeNh=%uslU0ceK0vj>#$ zkRh!>!4Z^_=tl5dcZC;qt@A1|_Y+M0c4Px4-3bGD2nPETiaox}h$;DgM%j((xR zPmPbFl-0?@#c|8WzDMPE1}9(ed^gVhJaKAYys?Wm(Mhx&9v(KxLV%mewXN<5j5`kY z0b0W6)-Th=z)}!B_`P8hYrrXCKD-&IGg+mNR;6A?5?#fuB*aP;a>Ca?8=zL*9x@(GF{Q;5oMuwN7Q}(bqkK8ZlJsf*tTZ&LG&9YUX|}!L&uqcBen9l5WzoQ zM+p>5>9U?jpiu;k=DgF&hdagI^PqWh^+MaQPgD-88TN4sQd}t0pS^R!eA8Ri z@0B!JDD<`qw}A;wC@lh26>wL~On!!E(c?Z(rsQDd!sC*l#o@pm!XfFLLsDEkH%XG!b;HL+kotx=T;3+n{l5KA84 z2N5Do0qQi;Z6EKF0Bpa{jYr)h>1DO1>23nA(nOwm0^DCB-Jdd30N7|E;KXHBHw85q z^hyo_K)r{TSSI)2_IR^66%;_7?7FyML;r^G`rL7;!QFfYQ%)X3WAx^_E>mu%;k!$m zluy?PBqP}DEmFn3y&MSP-;H9!=aTo?Av3H=&y6cpXAhM34uHm_ls1%L-J=m0f+UR! zqeJ=FE+erAaLm{78mE&2poZo=9%?$qn z&7rci!H6b@eF*Q0T|6lYXid$GUj=}=^=8Oe$8O&}uUTIM7;D0e=b(a&RkD%CzW3A< z{4bA8y3L0Dw#F!-3;C1~En!-wkOsh|;;NsmgJ%cjHLTkjT-Xcag>*c-KScqj>*jF5 z>9Ke7XM1#*%QKLJ#`caMGb$$H9|*Va$cx$0hFV^skK8>1uN0jvg>E4|6Q(q0@*S4H zlX#Lyf~MJ+0L=Mk>v1*OdT_;bHDU>+94^aB1X>|h6Jvqosz>|*mIpjeRo}q#+GJ%rdvG9nR2=gr_z9H2G+V*l00(??z!T!R>rNm=c?p!8r!>}Y2v^yGTDh4usm>YovAAEZYR&^y*uf1!Jve9Jm*gh!dz){YVQQon7GI6L%O)nja<3b_~h_I+$V@I zEH^{lBQ`&S&R6_FG|LKCrQtpshBOaRCeGG7 z7%lkW%UVafF;CZrENHRU$(eEu#qXgHPAa_Z7590k9=Y$*Qd}<*U8l>q$bFSY+)Q+n zD41G^0f0Tf05kz2LfZz>X6)%ymvOFFvdvFBoL}u{OacN?#zp~VDiw!q|PQnzH8Q5iQ;FjtyzHV5D$RapqA5(C}|C^U8fmO@<=w zZ0fw)7PoHQnAl+%rQXW4>z!)pc0#D_a3p&+Fh$S}{jErfYE{S%kjTj}NRYu{Q zv$ZD1ZWU={9JpZ?@rvg4l}ebNNbi&L7p7X?7H5O$=aWBF7CH`1 zZt?@gt%D{Q0xXx~=0*n$zW&i{Gf{KPN~PdMjcJ8<^L}2VEKcFqci+hh0P<@mwqVj{O%o3en2~X9*UF6&*hYOrFD#c`{ zJ*=VJEM0&JTu>Kx-B9J1qD(Au_4`4#KMy#p)tXezzsH+vW?CbPg1}mDMe`emrm<(= z%nuxxrKkJG!BdxI6ux)E@(@_+SO+Jg?Y zO~Dl-;hS57V5T0A7m?g}^(PNq-*jAKI^QAw}6J4x#j+S;=j| zX|klTEJXooOo94&3fr+)P_i%@j$xPrU27>WH~Ra<25kD_^IAg{8{x??D5$r(H}L;&2{jAOjdp$M(`QQPbgxVBYIpzf{Fz$%)bJJPc4t3XQ|kL>IEG%TcDa44fM zh|eDa-_)v;a6>iN-r{Mm6BAjLnkB+42SeG*1ij0O5zuLz6oHZ7| zj`(J*2#gG#O%pdEzpiF~`mT6P&p5FZMWURQFgY#aVWgWMY`b(PB>}WwCz7QXl9uP* ziF*F!8fjYNb&Zd>`;B3`R@~NWcB4=9 zDLnuQ$Xp{0y@gf2Smr?zV3dk9aZpJ-Oea(S;U^+P^r6hb=@gS(qkTa(A}M?SCaH;p z?0xcGf!*kXD14JBL??|NtV@gr9wf>Z;E&)Xe#PWNcu!;u8+n}|I=9#*GOZ~zER(OC zvBWrfX);?rKBJUyFiS8odJ;ty1Iz#r@OX*|AiJs~riy#qFq(-QOow`#v_{M9v|A*6 zX_aD-U3c}DAI|(|ERta-W4sZ$G@l51Hra}iI4#vsMn1h)3&R{=^e`j<>$1-t@8ycn z(u&c#VR9S?oB0g0GKk{X3tf?JtTLKP0nnM;pPVFQfx>3%dIXRyula2Q2Ni{T&@u66 z7ty8XXe1J$9@B@=GTgTHI;9F5_pqNec9l&FWJi1xdsSfS3snY^H4LH!et9OFI0#+3p z0ze`RFp0b7vK5BHrwivdTRfMoQFoH}pj^f^okm9%&5l3S7}s+cGQNKnfyEw~puel- zid2OKy)S;7UeuYP(bOa2ERWPgjUmKNhf3Z&B;qOETft+$ugtQFY~{t?C02WR zjS25-xpH*4&NvyNk(U2~CU#j6jK z07Gs4ss%CA0sSMlOayJ9Tu33iiff3Q!}7>BgYzd1r6-?FAuWcf^@7Z@31%Gvvg=bu zy`mfPQ;+OFy)M>{iw%W75Q|c1T8|LkG{b_NjGSyA*$IVbKdGA#3t4dMHAS(42Qg9% zC3?Fl&Jm$_oJt0wt_suMp{*z^I|SHE(|~4wLAG_PC+vm0&j9U?_ZMX1B8~2hJ3=Y$ z{uSS0C3*~4_dJWg2Ny^rAi9)w^Jw9pQ`oX8E?b%|Z&^28!Jxh=#FrX$u4$T6PGCLI zg|2JehBUBI8JC*E3M_s+Pw9aAURZLICDrqdXDRA4C6(86xHXj!(Yku7kJw)A*k81= z|C?Y5l{-%kD(Og9nFkEG>-Hnb?>VNsUN8i=5b!(^KigV0O{kdUYmey0*6A^TP$nrx zgjp9NpTo_yh1*>XyD1$u2lwIyrd?y~mLhuzx1NFJ-QxyC`AlqeWwjy1)Bh0 zdrI0!qTG_t?=Jd;`sE%gFLLL&Ycg~&3jj65Psc*GTQydmt1nQRx)X`vn+~t_ z+VljDt6%uUN`C-2L{e07j z@|BUJT^*H=Z_9dEmeY7*SW8l)q63VSLW?I6(!qUvKVapx&fMs(a&tXuu-DQ2E7ANUB;0=-5I^-Jk?BT z9K|_0^J}n<$JF91QOjv7kC(yevJmow`WJKdKLuXjP=?uuVYE>>yxA8Md~3RLxor+O zPa`h)g#un`pPVyMAdh`3nVv?ux*spbuaBz}dpK?G00P(w;oR(o6dD>ON7!l93Js@$ z#`)CAmiEJ*u2a??GGPs^_$b!n&reD}POBt^hslVGbMUgSzKpTj4dxE6&# zdb{4d0d)B%AGYKLZ;*v@J-4Lt&s+rkVsyRtqKx(rl$!oMH=0rOZD>$8Y%(F+xXOC4 zF__vAd8~1>l8bwndXVso&Vcg=P3r^0oTIwkq!ONd$eI7dI|!OM!Sb}>GfG96^S}WE zJ|t7K^w>Keg6R!iO3gAPv4iLUjxT1q7P2jnm-U?TK+D@-P3%VT`cBrJK`Z;g@Oc$` zrZsog)V7OCCx~)eBB_>v;(;mKQ+DElp%xG zEF&K%gBKAyg!pWdX(7*y=kJqQRCLCp80$+^*{znW4;l&-)?;<0my)+2(8VCD80^j3 zH@e6#&f&)l@u)(+4Z)9JxKG;Rof#s3PrL@$#cXjHxwA@^6qGUax z@}FS5me|#)M&0DG!tufjxcg&>z4mHvH#=w)5If9jlgVhrD=hLPk4nbayPY%Vzt-Qz zR&3E5mhV=RbDBu-Xi!fXuG4*^Wfn!@T+F@Ey8*sO9wV6OlW0yl9NTtLB{xjQydV>2 zik8VPQhj(eq@DnE$6fm;n(b|M%XGY+XW5Tb75NllhyQ1R-_p-Vio4?!3ax{Uwxnn^ zB`9wnJgH?)B=5=3h%t$_P){GVmk-pv8#_{GYTKhcz&&__`3}Vm=y${M>M=3C56IVw zS0ZA^U0X?_g>*$aE49nNGiSUej7=aVNc;`X`b{CyXNMK9b5<8b1(As(A7~Uc0~JNV zAdY>{W}SNVAxaB_Qed5js(~i~DKMU57vYf7xHkqcfnL4jSC2WXT=<_R8dJ-}sLMIP zTzd5`0ik%fRk?GG(EkvpsyTN>dyVRxFQo(y0BV0DdF}n4N7DK) zsWhT+xmZHW7x|^c@)UNi1X3cV6`6NpdkR{_Q2O9c|0p#6<4ToSSWa!*3XCe|B_Yc; z4Cy1Ju3f`*-#DJ@=_d&{)Mr>?6$6K3%{r-T`sE^Ft=bdZRSZG4cevI6;v|vX$=fru z;FwSp$acEZfl;*5)ovU4JN#Ao`=kdK(P$$dYqOd3#1?q$uMg`ORdu|NvkfR3>l7toY>L5>(M56!z6!ugru}@1KIM@z&Rla zF|R(E1%Q_YX?t@x?~4orYi=ulVP%yP$URw`>KndrCtI&505-^{}I5gJQj;FK3F}_Op9|% z#jETLosM~$?NA|Lp4js^(Un27i`hKmd7!h~=~?*%G-`oXi|d`xE1-l7+>-hu-<(~$ z6hT`(GZOE2PHU5H{JjaA_W|$N1vsgd2F&XmOm_7=XwY;}ENiEtpcm+Z_n$+4MmZkPZu!Bqc{O8)N33d^&)A1g z8vLf^!B8@4-NhYtw#ud}QtR%%+0@=F{^w|e$G3+ut=b@i$B z?t>xZ6zx?5&H@oVunx!Y;%=bo@3=YHDpL82@Q{pi?H9!NZHlDxeQeQ5jg;u z*{l=$wx{;2_Y0{jh7ts4?YvC?{>J}l_bTnM(0x#Hu@^Zw|A-I&d6$2EdW+s?7J21m z!aqO#&+Gyh%)`dQMZ|9F8%F&8>Hl5i|Nh%Oi7Sn;t5LkZe?!s#`~v-JJm9uD<{Db4 z|Ezj^>~qD>_8Psx|Iknp-;H0|LZhU)SuUgDCfjx#gthQ}!3KbrA+;K}Quj&?WfeqihYDJZ}P`t>nXs3hOD zh0`M3f8rT$um${m$$vfT72gRwRVQuOXy`;C73hJjZU~%&`PI!s@$I)GN3o!spkx~a zlXfr9M-g-auUX%>qLmClIS1hX0f^+A9pBZLrGORJ1Q_jsmT&Lh`s3tfXyQILe~E*K z07Bctvcoe1ui=bal;GxY8Rtvb72pbUup3?f`%V2Xj=~yC-@<*GH9BH~|1mBaG{D4x zaMVuz{Jf<%a3AN-)&UwwUi00Bj08AGa*kpuIkRF)WqFVJpI6`8Vy3>YMAn7RLPaWz zi7}SlcmH&=tS>TTMLoN2N!QaETav1W=I1>lNV_xLhEyOLmhK&ja5p zi6I6y%>PVbd27wox8S#Rsr&#GmY}5E30ncj6**Y${XY(wZ&mE&)hs!lL8<;U@;(ys zml1;af}s1iocv!)NrL2do{gVaId>e7J}qEZA7j_&+br$m^aTadOT2eUCpU@Lf8DJY z_}TrU`_38ZEEQLm7&Wf#^TpA-1Kj}mGyCy!#Y1anm7+9*UnK@9AUntTGq|I>48 z6as5Hc&i{v8){{~(ht|xsC6{Yvl-<^S^Rk6AdQ4_nIw(Zs~<`MIg9kNB8~JZ;f6F) zz#4RDw+~Plep{Bm{aM7|3#_Za)-dv5C33HOK+04f3NO~#BhUH@&9@*~@~)?6?QRZL zq6h?($Yb_uochQ&fPhV^4!zMdz$D)k-5S;$U3KdU@*icUzc1#C&vbjSrw@KAq&%uk zy!0b^bX~jOaA>H2b-M&t&A{y@?-x%^9U!5o!E5MIG`>YRR`DXTx<`!}UD9aI`}j`- zSQ?GJ(7_h2q^0kWOa9K=XV|)Xq*d6=4r}qg_5VI80DEE6Y+YBQ#A%rpqF4SyitLNeKwqqslwR4eB!`RWk*Q64zwaYZ;~w){#`upv zeMh7v{_*t*7c;TghP3?Ge~g}w@B1zdUEKCe-ll`G>jGz9XSJk3@;|KxvoF|5DyP!h z)?etba{h4Fz@%rC0kM)w{-(#@Q}XYn!1Wo};|R{%dAa`lop2`Ljngn3`(s484cyN? zrX#5BjF{s}-1{1>Tlrw4< zAup+0N;mlaHNIJ%M}z`8Zeq!?HPg^AoNqgRllsZGlk}rqg1Jw@w79*ZkAAHG7Xkja z`SQJekMT&){Wu-ij+2A~v53 z&%5yz-_H{MhDVCYXp1qoQPCz%)3iGgPkuVg$?J=Vr2vIR>2}sxxiR=rw77N??74MT!Y*wXpt{gJo{x;g8=Uccbao<05hzXc^a)49nUXglK#{?Pot=&V!r2!% zf$zucn#Bwz3L!xNZ-J#OGD4EJAq@bg^@A3>;{R84_pM5k@E7qEoUoI==F}GX17En6 z5^?F>`edVWOWQRN*n@|CT-#PLB-|$(kY>QroTN>J51OZEaR2k=-jca{oq&Aoq$yAq zUhf)vY5ErTY{zwAfek30+m=6s-$$FU{ty3(x7fdCb#<$G#aq`>4@PV#IQI4&;Ap>? z`}6|vO{{gqyKHRXvZ7FCy)_3d%1QBEaSsnH!k7@r$vY>c9&~SL} zH1~r3{t7F$vp8t7s*Sha}Bhc{)s$CopgqgRMB$8K25_YjUTJ80sm6>@u z7uuk6_riUVZ0Y@}`Tq3z44|(7dOi^~)U61juRxWII&hRY)Qh%TGwY$dn3K&zKiYlt zA`h7FrI{GUPoJ#nGTF;}g;o3d>$yygF!sH28H)zHIVq8IDl7sjAUZMN0@0#zDgU)5 zBd)~TMcfsSuch`Jr>BOC&$HFC@AT>Bx!-n^e??88_0v1>X)H0okF@SHt=ccycvII_ zggX1Nbz<{M*b1n&|CQf@+{mD>k^x;<(zjgz<{}$jBbdomq6=$8krA7b`5zX;rqP+P zKTh|@>FW=t*M}K4fi6S6*lV$AIwbt-6^%EPuV{}wgELJ5eWC>B#y+*1fY)%gg}3TS zXgy~eFDZ2I7PWvXyGpY=s@T*R7zdX*z;y$6=EL*^(BjJcQ!1qX?mTa#hrNz6MlTm< zOV@sf2_Z-5`w>Vow&>8~F6kNIplC5u%;6}8U?TV9NmJf-Vy`E21<<(6s69y7rlFa3 zEU$O3)Sy}F6unWbmA;HBf&VyRvxdX~a4DGQY@QBNN_UWbaC`y$yY{l_d8oMl+|rKW zsL}?Os(O^RD^O3Dnh8G%GDs#O0KiRyo7N7WhSzZV6HQaik(YYC_TaZ@jtTW1r;%<= z0noXf(EP_oQln0_OYVx~824fQW}sn?dR7V+ZqWudf{YHU4p>y}Q7XN`RJPT8&2uxS zfm=J-9}w5*>M@dN?Kd-`Ps3Sh zT+ZGb03^VhfImYXSm`@he{ZH8h*(oz`Y(B34{U{L0|%XzIwe0J+?C1wqxndpiDh*) zwQ&S^Z76>(sStuE)P6pF*Z|_l6F<8kk|es#ym60Z-5&5^K-CZd0l6fe)^O5hSh9AyF*(%7@DtT z7Q)XbJ$m5<%#s;~xQ^>%Z1p6OY(1aukq$JnFkH==3<)HB>prU`w%RpAY6v;;1iDm! z!p4%*Z7uajIy38o9Hq`;Nf5+PptY`5j8iPKRgY}#*&FkLud)UcL+0pmzoR7-a411% zX88NYbENYcNU67DVW;>Iju9*DKk(RH=yVSD5p$_6jZ&)hr^mP@T|Bqn1&GK9T7YtC zdD>XgTTyZaB{j$fc%`nihp<$6^NX!3i&F|rnl;;fXUlbsU0gh|T`|?AsXyE85@^t@ zFwyj$z#My;*3;05xB-qP4FjFcP^7h`Kybv9p_b`mzE?dlM(LFg;m0}$G!w2d;mt6m zW9YD2Rere+auv)GFA9`<@?>WLL)fw=F6dZh-W!S!pVQog@(<-Uf<+b^j3xx(2P4{O znvrHB!3nbmkEnzk^A>BzDgn32iCrJxYn3pxbj}Pmh;7=1kQRiujT@xNNdd=_$ za)+UY_5;c|`&@2lfKQ7>vi6GVrl6)d)yVzVY}gTzM1doRC69;E;Ncw&a|rwp{}SRc zSOO3&(Nn#Z!$2>=p{45ALN`#w>EHU>scC|21ctZ^%=IDQ3iQSc8mG@w{7dbAk;~pz zy!*m~U5C3 z7PH@cHAn3=jwePNObj7j^D81UiXzQK)HbJcZC(`nHKT##sn=F@%$7bTj?4ha6BXj5 z3Su&UB4yo<3t<815*EZ}uU`fy^|0nMVoltwGwrkCo8C1DrXdFiQgh5Y=E8tE2n&+h zO>%#0X66~co2nSO(PLEhewTx{`SHvJTCnSY`KpqvxW(?GI-oj~!?hBydc@(j(@N2E zO^L;+o!aA7o{hGyqu(K5_nIWAgm43h)$0{(k#%9-E@f`LYdHItq>e8hhnq6f<6jD4qj?)r!ICB3ysR+p_tN&g$;k$28#+0M^Ua+^cgb z_wz{F)GhWSoiht}!0baRS26V=l^eLaG=;1jNDznUx108OE`5la>M;NbnS{hT7FEae zejjkiWv}g9rCIS)SUm(eZFRf1*DEkmqWsW7tId{buw(LMBn@|MUe;Gc7Zb({2vTp7 z8~ti^9T5`7BE+=vfDTwa>b#3>Q8Gg`29?zM{E-xJ`J&WOhA7)1HZ;RKv+!XfJ> zLujz*tk1RXixY3%bTHk)sZ?eoKt+8=du}ZJ)vlO)Zsf9m8DeT?!0boa68u2 zv811b!)?~#>-)Ofbp%8rD-tCBPTYbgCkrXhT?|(uJ_5SaM=5|-Jp6FZF3sw zrnayF7;xDf-)zp*==^p5s4DG2{M^5zt zugx(1vrMkYyN}wWKx6o}x6PQa_s<7hi~+iD$=s){kKK&Z7&)sTMLRY#y*2F${Na=8S5~{82di<<@xwN z&$qgN&+Yfmyu8Ni^_kCgUFV$ZocB5BeW)fX^G`an-iT)QB?A@viWTyer+i0Vo@=$# zn-KX*dh$YLzaRK^ZChi?wWu*Kp!AoyuGmoZvEI5Y1$^#-giM*bfY=Ak?EDX0s+9{` z!hAEK@7=c^f9rcSS(SYNw)MCS@E4{_9S8C;k5(m8JzC#|!xYt*Efzh4f4W@Y@w9fBxZ-=d7k z;mZ6A?03BCSkNx83?;3L1i+GiS21~ix;W^kLv=G(Za-b>eNRI8t2r;^^P}&-ImDVjX_{KPmX(o9W z4S;@mlh>DQv`fK^EmWLQ`3_@YeIad9+(-L-Vu1ROHP={HLiyNM{iKVr;q}<#YwEkL zXPf|t?LawumFZRdHQ6a(EK_N2YK5>rm3*VmsmkK{0a%19Fux<2@ljFjToiM#JF}Rv z`oi29OQ6-nyCNm8Uvlc}D!?E;y3qFW+oTp)nVdFXswSw#dwxNSOe>#W1Li8X^72Ye zQtqrh$lHM<45W01AXg5u-dgBRR`h}Pvj6X%m2-AxmnivD4qi2J50w6Qa5ZV(qqAPLtHQ$o{;R>PB!LQ3+X3iyy zJ`D^f2MuoV*xn&Crh9p<7U@{oox`jMmJ=$lY@iI^PtL>s-c5MTSr0Sd+ngsHDftRihupg7Ho^Av}jS!6=XU}M48FICuQN5NEl>5rZm-`Zm+ohN7 zO;l#X)W@!Yv$ozJY~qf3!5pm5v`(2@gl+Z*o*2`t*}Xjj3Q1oGrTtpZ74yly#+wa3!#=^tS`W@d{;H~=Ze58rgm}S00F3t9+VuDV*nWBzmf9Hj| zzE>+*TS?j^tIh!AslvweL3Ft)>!Oo!*q2^yjk5y6c6Xs>+H4<(v8;*I56U~S=U2G+ zde&1Vm3bzvs`mPPtGTJ=bMD+Yb7kp+LBj3nB@NAKUd?DeOJQ3q;L|D09GrBy_p?O$ z(0Hk)cwKX+I5+yj-Mu6DaI0e_`FM!@d9me{8GQePCl zDNjFX5-j()eiv9~skGNQqZxC7ZQgZJSCO|M2Q5T9W^BfKPBmIMkb3Ia1RGuSR^*Rl zaNu}tznUBEK5d@1G^8MoJU{X*ep!v-$3ZNS(HAk0(35>>zoO>Nk-Xe@x!9>w!4cOU zmQ#pGOLr|xYPRqp+i=`Lt`sgiNynP}MHX#iJ2dtzA76W7=JWe;zzQDP^*tkO*;r4_ zJ_Pzd4hX4*yk_2y;PE^T<>hQyZRfwb@qib(^rEF)-ylV1+yi~l;%tiRvw^bAMVYOG zMA>Vy6vh;rMgHe$<9npd&(AdsjdWoMLvmF7=J5SVt6;BMhN>+b8aB?Q<_M()HRXi?1yS6We+?|2nrx zt<>^*uT|0Vu^0C!91mXpefldTy#k0ulTII6DE=0d*8dS;2+BJGus}WY(;5S+xkcpc zz8_A4dHeOSIFFqlmfE&R$HOs@Q z&-qrKBcjqY@e`(68YvNjM!b)=nl5{I-@ZYaS@O+O;SA(X>}XQ6QxaaD_W0f42@BG= zy%lCAx4kZT^8n4=>AYFMA#*R9VJWPuZeM8KOU;kM_YgniNhhw~4yp*f?DBmrJ_usu zOD(3ilj2GTzx0MrVpG9~mg1viWTWbZ*(1+>KgkS9{Q}H3rwD4-grB>S8ZYeoc{Ylm z*;_Rckg+JcWm^U`OF5`I7WMmlZHAYYt57+rcCPbWH{-oFas!^!&< zfd9&-+yn9!YnD#vky9Mc>g$tDgx;;!cFU&+mb-d+>ZTui?8dw_$6goQ5f_5g;&kdF zhbAG%pDXNwP^O&w2Zm7zr~KeRwMa1XjOg)eRZ^SPtxb$;DB>LrY*zEQ200{?>4AqM zHt?^>;Tuj>mP$OA8-jvmA;_oFrehx>S{mL0He7l(g!o{qgM-=hWN_?!UaU{k74O_w zKh6$IuX~w#0I+nI$tzU$bCIQKV_b09z3PKNxG2V?>(}u>tM7;TskT?lK%?Q$R|D-v zATiPRhS*3<_Jcu}hp`77Ccrhgm{aef>~*(HXq$PmJNq3^0rX=bo{#y?=KcfX{{xo= z8bp1Wx+d#Z6Tc20Hq7+1Y?*JV=DVyyw10mm2SZN&NmPenhWZOpO6=GsT4*r(UJbOboxJ_8IGVv@2%ZL0m%eURi~e>r^z z)?6i4VjnMQW2HBb?vL=!95w@l3_9Kig-n{eiIl44_vp_;}--^ecK2{9xi)uFC;j zPooYGTsr@K%0odC)h=z+dU{W^9w>RaH%v)(y!O?DqaCC$BEmM+!KT2+mX)}+uQ3&Q`bcl^h`Ke!N_5{}XOcvB!= zxg#qLD?d4K<+y9Yy8B6c0=e%5ZCpcQ>Xw^Afu2;y(Wp&tb}rQULrL9s!URsl;|;V|egOt3+1ty(3aYKwB(8F>r2#!Y7MHyA`mTYsHp(ZuB!IohHxtE0WQFan#>1(XoZnRzo<}FYi#}#P(v#eq8At%=nw|eq!vCivYM?}40AvR#*M~Pu zyYn7^bq|oBSf4*!!32xd;vO*gOaPQ zJbqJI2iGo11sopyZh&ojDdp#qs7N$W?CtxjuOkNJsM{!-`@j7Be;Cchv6ABPj|~k^ z7x_C{gd)DNf5~J-oXfqB0baz*VHd`95b(iWSgMFY~w0HV0p^Mi>RqHKhPUZ9Tj84JecZ4*1g6w<{0#ngwT z7mk1qG5jOSBHlSvzP^VN*LxF=QJjmsRGnv->1|T+ldS4vm~{7YPb6+)%YSAo;eW~O z9bmRP3~PM_O|D+?MShZe|}7RD*Z+j5HL`fDukD)M?3_8*wBd&Un7y7cXuc6#stC z`t|+%0|&m=Lrz@1dX-b7-TA&*AEcs>v|O~&^ZfK_^flK<9Yn~ZdzYVmzZCiG8C#ei zCudejG=^we;@w5IOAnCYi%*$!2~uAns+Z*0leX)@&KT%VE%v4a&xDu|XY9Osk{nzm zWK3h=GE2Y zDnFOq-20Wq??pf^D^(~FV=Sybx6M@(%DXA*SVrS1&wxx!6iq1=>KFMd9{DM8&_<&d z=Gf`$ znEDfbUH#0z{Ep~TjW~AIqQ~u$0nfKz%QC7-tM@9Bh=Gqpl+r-H;c?B~K9!TWx%ec! z7=9Kp%K@%h?p+l$0qvM0fB{p8vw@<(04Q4u&f3o5PP%b)`+Q+k_b1t{)Ih)V;wQ1C zvgSV)-*;rq_hu=bzw`Y6U*><7zJ%AvokJT}QlGqJ?!t(O8n+cs;Cdd!B+&ign zgAsFNFq2RAB8{Nj&N#|JwZWB<*7Oe%rYc-Gh3^VfP(~9J+>8YtVk-~LP_8lJtUOOF zOSWIOFEhEgQ_!_wE*$yqzulVtH05c2PM$HvAru4IwC}SlcoRHW0s_?B*9{QME~vvaFac?Q-j&PWqw{3J-jESh_#`Y`QYOIA6NaE$%S> zXbWxaX(dbM8CLxF;}vb&VDb-hkq$%JTWG}+9bnD4%zG--`0FK2<~n&tmsUbqx$48u zrolhxOHysPG0|^;bbyn&2k-U`7u46nKHOE2StowX4DF`lM?+r4e-$Cor(Yf)|C<$- z9nH9$BMuD11Yyo?P0wHYI=NWV0kK)H`SK1SVji@k!6(SqMjpkyh9-8ETr#qH{`@b? zoC3OJcb0*VCOa;~6ANm*wzpTRI552{^4B>) ziVz9UiXZmTBxZL(kV^%5AtZSGYainH737iYKp-J}>$&`V0J~u{@J|*z8{% z>NsuTEE}AxT~5-2<)+FJOPV!TEqwEPY%-o4P2WpesZV-!1>`U7Ul8aPV|S~Zp(SiEl`5lA zox2du5|QnSxS!n%y#;-AqOwDc&GG5BLHEJi`y8%eQ#$xyX_RDPqu5d5y<`R}Bnd6IEawF&7 z{rtnB))bPlm!OR)?HENhi!sO+nDVn@x>(=kFP=nug|uqa{n%PkR9aNug24jb9=*=a9px6ahpsMVX`ri( z+~2v=*eo_uK)%x+d2Ngv`I!&|!DwMr)$xr#Qfd6^6D5_2ucQ_?5u=#9?1>RWrghGy zqz0W1?Dq%F(!KTcvmSd`xkF2N8io|JpwPl=M!+Va^|Q$D3sQ`n;x>i~-ANnvPA}Wg zj3ki`TTh*NXv@g-?S0s2Qr=~P?|XN#l|Agah93J*N%)rTt-vtMk8j*x44u#i^_>;S zX7#w7Ze1i!i1i_+O&2|@$yFRvn^%95iu%<2Ts%?XUgmpi7`htkzi+S0S!0&{0XN}h z;AH|*{P7m!x@8?7VRKlYd!+ZBYXKYlYrre1-+q2!js+f=KMolRW*!~ z;>{i23^8!&wB&U9xY^`94*T|9NvP-7SZmRa>TC_AB^rwnRqc2#+ND-eV=u{xW(p3} zBKjZCE%u&8#Q*)H(O4vjUfdvlDw)){IM7Tbde#UW_oX~hlpS?CgkM#sM0a0+0)cFDW9)w7H^urB<|A8c2!pKP@+)d&k4i1c>)>$4> zc>PQD7PHh8;$l}ZBhmu1#8A+iQ*kn?^>uh<6p9BT<=|>PAes2t%tMesG5!>G ztI2s;>-xv;0ACQ#*-*jO3kopz0<>Xyi5;L0y$wB)+ssa#^&@1iuWMyeK5qC{mxzwN zuD_)Wjr)}3m2{8~x)l^;Kykd&ZAzO3n9+nrQvywW+oQ4^o-OYtU1wEoy^Zm{Zn#xV z-cYq=_|2(SR%rSlq#!w+tl#6nZ>M{rxj*)XtEq8(rMp`gMrlsq->u*_o_RY%4n8=) ze5R(?q3YsP#h^pA!*Y-l9;PCw65x$|jnt;tx*Pe5TY{9#9X!y}U%qI(5IIusrfzi0 zH=bl>@~QlhY?(ePeT2W)rr>h5WPkwGOS)<=4^o)-oFsgaAMbtf*@zxGo)n*}G!xOi zJ9J1>1aWz{pz+dDsCAr*`qIh@kQT-u7$8d&y7VxI+LG&)O|)jnr!@ad@b#T7vu?Ze zO5}85vowdu`rKe>Yv3rjMk3jfGfOXBn6S*ig!={IK=85aB8RCBaU4K0F5t?gRm2e?qkxc_Hhf z*H*UWq*Lk9B3mJWSk(m~%h^iUaiWRc^hIy98$f18Ix-8q<6H>_aR$EhQG^w?Ih<4t zT((*H?Z6166+As;;!PqvaiBXB?Lj@6pkX@wz*h(H!sUb-P|)IE7rKPYRZkl zLIjgH@G!Ez)$*Bnf-60Bu{xNgQW}`&Bfm?SbO^{oLAwzv137Gct%#Xd0Av65U6iL{ zpJ&lrTMb7jN6_jS$`EkDCzJ z`dA@>Z%~f#?>Jj`L?tW%6j#6Iy2Vy0_jBc`9GvGSYG=fz!x{aT8wyJ zZ2pIWN8iu@!JXb2&aI1q0@3zXC4KBxnaFe?-)(O=eHOeHMCB*2gu%8aj3@)M>QL)T zmg61bCt>(U5fTr5`YjvgkaBl9%p3`RxV2GO{c4Bt6Hx2l)=3wL-heZ)tki;BOEP!Y z!VNuR_Y@k??G5?S@qR6-q2r*}9X!eJ$pa_o$#mYKqrcTBSpK?3Jif@bm2H;u)75zw)qUdfIup>*_C8a8=H0f@=Y$-5g1NyBB8e-%jj+)*vG+yX+`19i&k zgT#R+qem$~l}KDrzOX>xYK_PfaTmdtA}43_a0_g@Gie*>9afR8Zg2=N7g`cvV7|vy z|D(y`&1+i{u}yMlmB|o99r2Kb(tC7z%>(~H1@glVeSdA)NOo88A-mbFy$txnsF@xZ zPAWCA!HlSTd`12Is;&Lgs$-C6=jW$wDLCa-Yk0Fv#SPw|;fS-Mb4`66r!3NC4%wy5cj0Clm2VMOTYRd_%ydK&>znUd-%~ti z?L<=kunmeKwfM@w`m+`%KCXuRb|}8sb3fKuZb?;96#nE*w;$l;`0YN89SJ7rD)wn# z%5%di8NOZkbW^=pc*SVZZ8Ep$u3&}g1>-qfxV_VxRpgg20q0}FzC&O09eXYbk)n0% zf`WpK!H~n?)uHFg5=R8p3z9XBA|3|4EXBSqCXZZnA>-YN_GRhnPNv4ik16(k3LL*Z zd1E=|a`6EA_M@4%1y{X;f4%Hn3Nw%q6cIkRn9}+L`BJxo4JPmwgrF61*J?-p#0X+v zFPRN)t}P;7l-l5VZi@i*2ZRT*=6NuY^IcWI^yR0*b@y}*eX29F)4l+iNkjto6h_dD z9B>-9{Yd@PZ!{-=;=t|YJb#b0zyJHw(YJzM&mo(j73=F4gd{%+eJimFO?f`q2y4tF zQq{4*g}hc%$B4E-+*OS{C2r67I-b-}iMEo8u!zZI+3*dL(^^lIIqh;vR*m@H^wpn! z1|;rFG=Hkya+B=Q2g`YBK^$qnpp~VF=+F%=83FQNlUBWFmIVNWDWeinq0B{TD>jZp0{4e7zHTMr{kwA zw08Dg_-(1)0s7*AtkC~~8t>%G@9j3HIIu$F0uDIINo%Tpqs`-Ee~3szCA<-VMO}4N z8QnuVpw@VzPGn`Z;&P*n%W;>;5!XInq3<$(NcT2#Fk+=o@%!IegZVCp&Y)Y6^MJbM zsCi|0I3%q8-sz_roB*OyRi4^D`NTkHv}w%z0?4D68XxcjiC1nG$yiLDFRaEYW8a^SSj{jai4~viu!?nMrgrE(fnD0?4$tZee5IAo(j%)CQQF9#=Gn91|zM+ zpE%GN{-7;=OS5{XQpOS9qyicoLu4?TaWRXv1=#YZhjk{ud35ScbiUp#szgzTG5z(p z7NkNDtrLq!Y&B%d9mV826%hrZqhpFWahz9k+UfjdC;0NaQ>$DY{Ne4s3ll+#2veGs49s%$q z{tD4(mT#6L^S3@~4C$SCQ9CpKDHUp#nY)xK`&=%-;R^gW+-p3}^j88gd4UyHat^L* z0R>H-$wj)2ODm*lGt{&9C1$vqu%=kME1Kg^rXDn{bM5Ne%Hor$SQs$sb83mEX3sf& znB)I-T0?rr#~|Y>)}8!9nyPaLoPr`RdmR6NKNd_rZk)+YfCz%d?_-8ah> zV0zI5nFrQPvz!b-sT72d>8m$T4M8f}RL70=K85Lm;!^zIGi0-Xzn8V5{|LH+>DS*$ zjZe0o>Y3#HL#9AxfRLK^f#BAxxrUAP#F^>&?57ZG)0@e|gtC{lHn{+zOJ8Lp_V)Uj z3%el7!?xqM>Oh856L?=(38!S>qm-80R2xa&UlOyA33#N`xK5#u4X8-?*Vh{70qKlS zp_pMjr!T;#qjmBw$4&Ai1!|#*RKou3j*<-L0K7upq@;SzeS5OWCISmmP2bCPPUGe? z_v!Fgt5`!8%!}*rV4`h||N14izZUx+Q8k`sdcG(UaM}5qq}L*?uIQ#7A;!n%D*Avj zoy7il(hcVLjSVW2(Hdp9rERu!#4j1DQO}|1(&3*FZXPN6>#+WN0sUVMZv!!b)(*o0 zrU5ii?sl&sXC32&qn85aQMiHmdXu$KM%Br)A|sM=Em8n{NqW7yM>Zq~%G6rJPq0xh zO&gN$9pxu5PWGD?DvEnMAhu@{a(w02%$75m^Dz@n^3p##?JZ$7^v>Vg`^(t;#~i9% z0D#33V8FJqC~CZI)fDRIfELQRxPUVn%`-mb;U0>?%+7D7_Cgo+nHaE|U`oh9}F*@^XLt;m7@B8X}KTO#_PU%*}?qdfV zL9SZ>bW$Q57ZB_yHzSV)0HMB=@RG|;a%t&CdersJoF9M57l1U?A4EKipoMy67_FB| zJVQvC{$WA>6qEOu<|o8n+Y3F&y}t`g48AcMTc?}9cTf{v`RJbPhFU_hS%}Oc2}HW)<;&DH zKbvP|MI9uN55qSMciS)IdEQIj(l|fM!KceV(}$lw{%vCa+`4_i1;7{k|pCXvHiwqdN zQ<9yO5tA=ef->rzo6$rNfX#rbudW-+qdD5W#oV;kiIRxQNG#%O>_-8yaTJoEjw$_1 z{hvzn+mWc00boMyVMxm`vRTq|L*HS`rvIqcH#f`|qWhCCR59D#nJ+g(lU{&Wwuq>H%x`+5KP3&>Y}mV_HnP=urb2Q^3kR{sMSaqnh= zD#XfZzG|b5rRG#PUREMRihsAO-*s7OkS$$p+~aM&5L@w`F>e97N*fWX0rHpMZ+=(1 zpfKvs_~p+tdwBT}fPB%KeQokq{AieH+x>b@ zy>c_R3IKrAfx8^S1$?YUlg+<6xM|I4GhH!SFs+If;2Y!E*&Yu%5-?q6x7yATbYgq8 z+qxirr7i!8gu6?{mbdPvcKkANu%9PU%8aK)X(O4E3(N*K0EQFnLhY0V5azJR=1}-^ z!L=bHLkcK4I%&3=`u13?NKUcm$xl6_R&aC&>Fl1i`QwduIdi&o`Rk?-Q8VyI9~>?~ zaJ6@i&o?2Be@NTh1GekYHX_-s6Sw^l7ge^9Rf(guL3b*##}ROTRVKjYhrPC z3hmstGeL(r)K?$A_UPrU`I&7{lYA7YSlwE>1}m@BWiOTR?z1h9PXB+gWobLA*k!%= z$^4;cOR^7g=I`*|0nwkT_@(I>pQ1C|S9Da}sIE&!sqF!8a&JmB@^wSy4{J3EwBcX7 zRHl}duZOlUJIY!cGi57K-DPb!AUQ{OW??(xnEAy*(Ji7F*to2_3f1)UH}`+Yfw#DdA?Y(1JE$C26@fF!j1ql z5;s>H!0n8k<>#{;EIYKxYQ-u)+tr099@Rh~K9y|X%{l^zw*Y-8Cj7qz5yDKN>nG2njgR3)`V#Y>8Ha~WI3 z4lZA9u*6z;=MiB`-{syesg92rRLjj1%BLt@=6!KfjesxiJlJj}|vb$+463 zXZombs!=TccZT<24Cj@FZ)Z?di)ok`wWYS}l!TqlM%Ni7x;miBWFk`gq1Jld_ed%* zb=ba^l}?|oGv@o{`VZ;x@LhQXgYm{2unoW}7t)$*c<*kxpaEH*I-kDtto|r?stp*` z9`Cw9*UKip5Xc@yxuDM^E|(;nBmvj6vF}Zc$Wkq zG`(f(NdwMkyAD(%T+y&%wh>Ah2PV1lrUYHZ0n;q2fo^DCXvr+fb8|3o%!R@4aX0A901uGC1kk(tfm!d4Is@Uw}&6Zry4>1 zY;eov4qhf&&tlD*TMh*F9d}_DaKXFbmO6Bv=ay}+PlkVtRnDQ#Pv%uP`?dxQe%$($ z?(J_Z9L2W`+z$C3o6eE2oplmr(|A*qbYD{>vk|s+Z!_MSFIHvyitY!fTOBuLG}(nX z;w^iFs|Vxv!=bt+PE}BvusWRoxc#&)QQHtBR$Hf;2N1Pi0Ne&}=LWWFYx?c??Jk+1 z%vsS0SHD=J{W@@}#!)Ip7S{r>!@Dw`Eh-Lk;AYigbc_)E8DnST|;?f>s&mD(9mZ zbshP<2Y+EL;)jqqq%!au6n8Fm&^ur^z34Pn%X{~5QCun-kq%y4qyW=_eg-Ic7;oEd zK`%gG$cQ9nT$&NX@7%?9O6U-}HKZ!`^#Sa~@jFQ400>{E4;aa(#iIa*|Yr4Lq z3Np9tjH=w`a2D+GX0LBb?+Rf>EoN{tD!Jw>27Dz~-dhSC%~JuWk_PG3{*6k1@D^sK z7PdX9QYO|er3KTv7B`~i$@xb{5|aJp-H9K0#+HVJj;5}&gZVsMUx1XelxzwW>bC)? zMW{j55^ZiJXoWI|NncNzi(Hjzc9)nf8(jCkf@m?y0q03I_VY^Unks&e9gK#LC3Ccc zPh%p3@|^{C7Ay5?i+%!?I4@`FmZ=-sXia@9Bd+QKy@X1IueW?}x?1CHbDHOcKmA4p zjL$HS@EuSZ)!uyu(ZViBX)Fr z$8}()Vbup^dPP;|aa$yhh1fu~?Nurzd2lr=XzFohuL);ogK&T&cR~)V?BKzJ)vmMm zb%%c!Up+*!Ed{(sl^8Z%cCH2z3Iij!xOoU?_3{}HU?A7#+@i74T#oN(x8AVGIKZ0i zeMM}W?nOt;s!(VJC%?p3o|@VMzQ7H92?~9DzKs0etW3rwX2~W)J$@ zx?&eF>51upDi;(9%KHjQ+l8hF- ziDvkTT=i(2Q>D6DCCvszKcK_NP4NTSk+j8UPq`>(cyW&>QAIc7N2cwzC*@2nE@-u1 zKdL15ZLQoXX$lq_d`?E|)`mDMtn5*A%JC_1d>hce7)YzJpEjiuJ6Zu=4NrOKXro~E ziDlm+tFqGD)aPPev<3Qxe_}*snR+rfY$>BYdovOL#!I}x1lL}@o=&mXk^`$viBh^p3d3sxY zpBUg|GXkF0+%uw8PU@e?Sk;LqaCSn}1K!D!C!SN`Jx^;hXxDy$QpRTvjJT*|YanGb z6J#DQ>TK7)s?CHe^<*ZQPYhUg>MC}f;m?V+^DAnv7Z~SFOdT`sS`*`bmp{Nx6R^Ni2~TH9vwogn=wG#H1r;k0^BihjF{DswmS1-~m}zB#8Be z2XOGW{$aEy4k)+GNqM~UmeIPuHr`pvpJXkP+e92*U~5j4iU~S2y4W-lXQ;*8yk=Qs zpbVMG)QhiFALObnytSot^P1u|#hz;OfPmLCz+F;`?Y!Bsim$l*39bPRQ~3qV1_KunW?XM+sVp!eYGv z&u`-8@%R_W)@`4xTuJ&OG_Z$eS){AjskXISTU(ffB}}6&*O$gAee>t!vO@;^lhIyW zK*!zV2HO#xF0oS=xNGT%9aY^EI>ROSk?#W*K0pw*RQ~)~N=Ze}X!5vV!3(iPprII3 zvl6NK*&=9w;lG@h@?^8T3U0CK5O5Qx7%01df-)or8URiXe&+AugtmnU7jG;ANK4X`KS9-TE87+KF{D~CUc&Wb8=(e%5PRF%GK8FY9vAvM> z)*1wC9v48+0SmDh7NM%NrtoL!CZBAH3Ce|fb#=Mr{lF4OX?mea{} z1)D_%|3b=YLcNZP^JWM*oAX9m<)u7euvlqec0-eKviyHz%3$cR7ao2|4#OUI=x7?evluE_LT8^;r8{swyF^ z+SLYxSGt^qm!O=5yM4Od&KuK?U^yQ)fXs8Z9>elQi0fj!SfQAsO{I``|0zeIoo2q{QZ?CVak z@adA#OPm-0VetA`(TFDzQE$Y)(Pmp0TfyMXb<(o9&m!-4j$K;&dSS#=yX)c)#qiK5 zo*YcdkqfOa>46LotSe*oHrX2JS0&xRiDlNs{5r-}4{o6NgDcJaW)KI%sdNo;v;A7$ z2=`{;J>s_Pgxd~FQ(fTzHU4=Ci`HAidKw7)2@xoN>BuWDWuxp{Uj47A$%_(b#WE_m zXwvv0x96R>9hT+~t1R+=#H`;Aj9lmeDAArUCcfDnQPU?ToM9I$TXU4jeuk7$PO$HJ zX})m5@9v_iRXV7SE#a&lF~g&ePf62=lcMi8m9&HgsOkJwLpOQ7e(1!y8w0Upc|A>N z#5iJj=XM)ogr64l(kM^mXTY~WF~62yMq*QmJz79fT@`T1P$8lcaBQpzMCZ$BEec$r zwjV^Dnh=KZvqE)$)`vEM6}B2nmU|?gMe$lBg}B7T{}ERHw(f^S^W6_XY^oJgDU&jy z^!fElpic9W@8?=)IzOTC8kAmEhv1E$lH#h;r?tJ^cbDVmbaSOmN=esR;aS*&Ui{a> zq`h#^cXkI3M&TcS4{i!(S&Nr>RMfmKK10nMq{fFJQT!_>Pz_rd7CPOdHQc?!HdZ9+ z4~R%k>km!Uv79I1@*Y0SD2+8xb5AJfWboA>E|4#h+2w96(Rx8E$0^;cpR`bPBwSK*Z7MwKtr!=O(-#wfnAJ8NB&@pN^fwX`B-rb`rDbdGn7VgWnI{ z{i{-@%{>g5;-$9#g5QJe2?lQoGr9qd<=3mMOIhDs_Pu*-qX=$Z@Q!Bj_v-OzMK(TwczlOE4 z$nVhmzvk>eF#%xHJ_AA~P%2})sHcCyX> zD(59y@KK6|QrgCaf@G??0;Bf)no`%bu6OIoF*0tbTXg~=uT3q^OZC6pm*}TXFn_+M z7>6H7`y>aiWa{Sp0;M=K4%QlW63f5Xz zKD1Zi-ofDQ*MVL5PrP_jP=qO_RLI2q;v;A5+7Fz#M~PA3xVQ(l$3GbtrYZ*>q>}w} z1&tVM?+TG6aNQ@?cj$@MdslOt1J)q3n#cyNEYUX4&Mgs{T_J8{Rt_(uhc32kz--P@ zAu)Ztwykfu30u=1_HGe={iX)}_9&@pe!CPNA}nZ&h>MBTL$cI0liDqs-e&7rFEnF{ zWmAo!iwxn%_E?Z;TOZTBPG9ab67;$~O#H}1S4lQ@-8gO*zxX|WmmAq;VdSFHwkiN7 zb-~Z)OwZ1eL|`pBt{c^_WuIF%#*3G)zLvGF8Lo?BYy(#sN5D*pkGaPiwJY0+UO77I zwIOJuMaY&r^)CZdXXN}ggbblNM9uC06=ePojj#aJ_=nvX5K46sIuecO{CFPl@ZZ?b zK9?P**UDb@z8igUhts#Fw3Mgwv4TA04Ki0XP(O;JlLJth(&clTbG2N0AwU#T2T)kP z<^?)c2>Ad|VPCx>q$wmM>_0}C`zQkCgKkG6s_npw>2Gh4Jv(*fpTH-wVTWN`y(CX5 z%p97>S!*gQxCD4z#1mDmLG2d;zx=O%yf{$sUz0)sc@+Xl?8bzqxTVI{7CE~xcA4^X z=7DS6mQ{$AuI^prcs5`V28=*X=QAf z2+Zb{jlOklRa*&50PloebneI5^8@?XGJ^E*qn(ZYY}NI6x>#@F>19Zt`{BLvn?R@G zWgwixW|Ij1!MTRZ+NNGNuT14*$A)CKSF+W$;=FJGLS{WbwUEQM^2To1q_WOkC-Fj& z@l?WMP^LB(QUF4In&<=izs9$!L-i)*GGLtL>I8pRzXSzT+^dbtWN#_?+LGy4a%I13 z0{^r@`~a-h|YolYv`a%X|Sj@eBErQ;Jv@-VmnLHoI88&U6W?fJXms z;_=R<=m$2|uhoB|bMfQafHGD8LUKbF^^#u83ev+zBxrM9l0TVLr1HjP(z8XT!T%C= z%Arz^y~#J5EaY#K-3=>1O@zqi?=beCLUQZ*mt#Qtgs^T`VZnJWT!)Jm#r8vY{SjVl zNG>`((#0XMrjA;}xN7*!$o5mHa(tyQ2&&;Nr8T%dVnrykxUL zMU=3S3pu;jJ;d{~?xD0TF<*d&gX#I#-iBqDzJi3*tAu4~yb5 zwK&Xl+xHo<*Su`f8XntV0Uh>U6CD4n%;4ut3dxN@Dl|Ab$M)ftahQ)zrbSZ;8xsq5 zw*`VcZ@QqaqE>9@<)W?!1#QxY{{29M@1GG>0yl1m*<>j+q6Qk~yobs|R(CtwEj8T=kJR}tjhaBFIG7n?z&iD@ zV$zKk{7F7sE8Ht%R_?VTOq-ZO5QKZ@EGd3yF=oxuF~1M3?s zm+5EAstmon&~qh6ZY!7Men-|~Zs6UqYH1)Rgt@6Mz3C)*7G1Z6ZKz}fx@5wnojOgG ziGY#XA9zHT9G=TM{j5QR>YdL4)dsp!l_5i-i=Uux2@1_GJ8e`d$|!(> z9Ax9n`uIsEvSah5_7>v;(rKi|)+EBhLWxr)&h{__LDQ(hhWGOIJYT_Fo&Ff7_Mg%L zpwz$l4Z)$Uirwt$KOKbstBDWAnA~kKBg@eGq!fosN=p$V9aqQNllL7}nuUc&FoSXW$m)I9<$(&gC(e(fH_2uzUukZWi@F`S^ zBdO4-P=r%L_I4p@V=xSbWEqUfZm3S1!W5ybZ4$HDX2BTB7GjEK#yV(1jD3t@Y`@3( z{=VPS`Ml1Y{!)3(^1Pqte(w9auj{%8q&u_1rl$%t(1?9xJHSH>v6k>Zmpmw5fG!j# z>lkD#?o2Bjh zJCuF*ilvL#tzN2+HKJ4{(k|te7O{NXa6tx;$-zeUA#Hi`vR}EkTA1m$JgGTMuWQ}g zO}1=(4(%Mj-9NUc1jY?>efksC@c+))>UaBrxRw6_l313ggLCz~Uu&x1>)u2Gx=CMd z32vBCmiTr|IrY1r?iXlpIJ=EsMTk!(*8^mOr#^lbCVNX)$X;HeU&7j(QHp$0CXW<8qn{Lr`pH`$9 z+G?P(%!nh7N{&4t=+xM&UB~nqc%;H+RMrMk;V`-8(rxmHEi&pYdAm*>s8rKx{)LUQ zzu(sqO8CzI?t}k~tFO3d0O?Dy>~$VCcJ9_h@{k8i)1`w@E`Q+FZ@OngMup-lf*&dE z@HhftJW1qIvwwZpGMViBPQKo%)h>Fkl8;@*{+Q0~I<8%H4H+htab2j$?ySty8w_1% zOOK;b`??Z_-bm=(Js_BuctqwtJGE~0<`DZM-R6J20R9#V|EFI4Tr9Sf*iQTa@okE< z7KYU295O)jj2tJDy7POj(>eI>2w#h>bZ)z_C`B-DRCJM%s{9tSp!|kWk)t2~@*m>^ z*wUx}I4YCTkJ;aKS+9GSZE`vBwQ^%4`-rn<6w2u!%pO-8wL#=n`C81k4+JuKzdQ#5 zGv2eb5o$eZWt-pK%xSGWPOkBHr{dBEKmS~9|NON~wO>fO8cRc3>XOO(xv;Sft}zx( zuTxM14(U1#PSD&^d!m&uHN&0$gg?SvkYabz7CP&5N@uO6$a^E(J{aj}ADS6i{#7Iw z(?I-iKUcm8=E#M*7QTZ0!kxOjZwD_o#GWwZnLg$G9Ey8qU=~B6Okkh@lVRK2#_mX@ zX%up0&f?I7%1pXQiFw4OT}s1lM10;Fd+94&!X?68zBnPSaKRrjoLb`J8VnmTwRPiS60l$Kl$Z`=wR7r0$^}<)3SNkXKu}eU>HIbJL`+Xvu4Ye>pU+ zGnhZQKSAtP(`Xba+9n06i6pX9(K)A$XRPSc1!H0r1`v+5H$DJAV^xEbm(p zzlW7wt^UuY-O(2;aVG%g%TZy^x$;#hyZKgq&d+@bW671uba`6K#cXqMu0oMCQr0Uz zh0M70O?((5TTXlVRXCTwyIsBYA8%v;CYc@pU{ZkNEv{W9|D-RTsbuD_pUG1d_%O;0 z-*?}bk%HybYGrpAX@{iP781EA2DQY&koFMqm%!z3+}ZUr@2Vbq%?XV=3O6lVCe-3` ztt@19G`g~u$IYSLlIkSn+@nuzg8IB`8Th31%3h&Zdi}juVr6Redq@X1UZS+`FSgrc8 z`HY2;0~~9|ZY-@xk!tpuA=M*2@hqkjz~*EW1dZK`8Nw*^Q1EmI^}_@r027nwAM4UG ziF-i#=hJ=bi~4!vY8H$T4a3WSOd%HVDfEWJo_FyA<`AT!uq&uwAT8XlvFmMdbX$_& z3ECZ!XAa>_R`A`!+64Qv4blpk-J+gOh1TjTyQ)HdIMF}Se-Nk4%;mq<-N9Yn*cB_~ zeF|&` zDP6WQWlakr^(kVyO>3O>QDD-bFn(qYOHDLHVzahoa;!PVAVdTke*KO0 zf%gK|Nhj6q=70sQJZ+uSL2}; z8}ocslH+xo^y~#WJTu}h0c{=L;h^N~3J|!IscBlAbt^=UT6hU!?MsSh1$`Gv&s{7L zwK{YuAn31XWoiThFxSo)qcegae1+RjIyKtmN0>hG<)urqFH?5Z6YE_VoaNHzHT*D! zuw{b;k zAslN%UgBLKS>2B|!DGp6qli@1ai;DLKwO zMH=ivIuDz&YgRYfbo@s1-mxPP!*P5VfxnbZLT)o{~{v% zy(;)&x~l1xF?}cHwnM(`M7`F2XWNbn7 z5)~zsU_&EOi7xEd>zC)OT8UUHanu)R|0y#=x)B2~ya`@eVAXle1S0HQ{2w)=^{Bo^ z$WnOO{vOEUMFVdRnpAqD{r*qP$G;~lraE9N4u%dNc&J6^B-i+#&DexEJrRhAOvLRk zWjUSlC&s_z4D2i=sNTfrO~>zz9B5_gw-1!+Cx|qQAV?n8h=MQd0SUCUC(s3=X!Xbe zJkLSM;J}`wPZ!wjCMgoGHC26^o@d*?TrEff>*suS8~oSC`QN`Domll;y%Ph_S@PI{FuHuf2ac!S@i#LMjGn7ugI`!2^_(432pZUa3M)&3=TtMnVj9i`Cne4_rw?Zwl@al}pWVe=G@_7V5VksG5a?R$ z#%Jy1Y0Z=z2(nw#pLYaW>9}?@B;`|c9yHq(y^!m?c9i3nT87YqFz*yZ&FsJYFIEJ* zZ(RnyJszRcp*WrXzq|YAasm9?Q5`9hdfh0kVdEm*sD-P?aOz%gSWB@}r9yXSp^V)X z|6~eCjVBwUhB2xkuW3Muy?m_o4nCR2I4W}G+45(=cS+P+W*Rg|+RguHa4M%abOt5F zJ@LTXEj6Ag^-~&dLOl8!L$2l0hckNPtP>$}WqCOB-{7aWOSacxlj%QOCj1;fs`hOL zuN_f~g^YKmhkN?Bpq;vVyErMM$%}cHd4neT^%dXr>$#H7Af>y9j=T+-{l-hjO%!8C z86_ot7qtV%4ezrN)_qZKCF%ORqaK^9Lxh`D z$c41y6(BiRo>B_uE{VF#fnexIYQDq%8^o)u%k%m5x(=0>ds3!Mw9$MDC^|+ho=q^0XZ?YQKNrzEqB z)JR>56)hJMi1}`Y%-C4r?5yQoZOh$QlRD@Ocyoy5x=JibUo?xglogN$8 zLpiTI&*elZWL@yL7DuPVoMDU7mtHrlhP1v(aT`1EDw!9=MOZQi5qxpn6sf++aG+u( z=_x8GTdu}zH!f?P&o?rtJ@&L6i7YNTeUMDk5GinF%YBw|#YHh5LJ6@%rba-MI3fEB z6difjAODFTd|>S;D3^_$sb?n}b+v4`$>lBG#3Gh;QrpqU%9$MhLV}f0ziz%v)b0Yu zc0+`)7&hv&|H5#Rhp%Gs0*IWw;T{Og=ipZ@^UYz=*OZ}Z{-_)hX(uEnrR{b&k@S4# zpOrfxm($!VM=QN;AM`3E3`EPwgM1 zxO+PhCix^qGQ-21w(>P%m(Axp1=tDEr(xgbA4jTLiNqACQuQa93krf)dp6eaf0y zDR)A5@U^_JT5_M11*DzoD5c~ISkUM%BDa+2w34-Gph`JHc1|>4@KI+`POF!d@0As-w2SbU1=AF2n9qrjFEMiD+W(;Fng zaD;jua=UBmpnOFhe<>Llnw3!u@o7~ep}xpfYf#4@J_4}ZEJe$1SqfuAJCiCcQU!G2HI zyaA8G_wm&oBns5SOufO;P6UuAVUV{}OeZ+Wypix+)#`CXo(&o48yWq+6IX)^4EsBy zsv|P7aTr%nqVJsQyHJ{!>!g~J-KI>q#y!rTS8mhS`g(RvXk4sPnW_t6>%cFfvY=uy z`t%0ZRSH%~;*I81$Yg}AfYQtbXrz}+5xpYHht_GNFg`8O~X|rW9Sx;`2&M{}t+dMSzln4l)aG&{j z@7Z&veVz3#t@hG1`YVxm7xcxEYT)MSl!Jq`CY0P}(LrO6of*=7@Kc(p=BIA&dV5tH zvD^oHQLEN=5n`0*U-=ZBPLOac!VeF3ofTi$Eplbi(lc;cEweewJ1Qqa$q!HBlaD`tR*9&YDOnR|Wn9a9k0~xV(gW&PnG}x&guZslTX06)G;w zc?i+)MF!*iU_MuQ==usR_n?U-wpDMj&1~Z(+ocaf)qrIED=SHFY#x@Qi5P2Tehbey z$;&~uZZ}f97o`-yyDp-k2RJ^zfe;r0!$YQ=xboE2K}t3*DVbj-F+efzP*ufF#o8V4nljgsf`vE<(W^m4NmNub?DCL+}K-7Tl z@a0Tb)Lf$*osKDcLLD-VQ976H?#g89pQe3<4`q6dAWMu`%G>S>vl=TLasuj&yb~Zi zp%J@rj_OqJXY_M^_fxfZE9=(>uAM0uZ{Citm}|Hrq*0!s{_D8Z*V7U6{>+2*-(_6J z-!uw(Se3b3h+bCOp%e|5SnK1V?Q~_b|H_<7<(w(RS^Hm7cRQmP z@_e@`%Y@)bR0+;olD)WiHNU6v&C_;9P2tSjyJ-#m38g;?ihfQH0mP|M&`O_FYP>Q# z>RwXD(hR)6T~pFSF}2?6Hkd>`q0GVt?Hoa(lma}HscEuMnCF&JW^ej z!~~S0W{eq9WZIj5)TH&LEyi2*n1&^EzR-g}290U{5Q3S_k~~JEjG0a>`6|v~@p5b) zOerApN&LsovX`AkAkWzP)DgIePqFKVA1eEv`zEsf=K!B$-{jJZ@|yBxupHkB)nw{G zco{HnKQQjzRBF&2c&QypfYj)sh8)5k^aiB>1L&o*y;L;79e}gy?19MZm1&$CWbdRN zFCyWTj);4z;H`9xZWQtqc5moFV#d2)KL`~o^7iS^GTPoKvn;1l>rvF2p-+YDRPuaOwpT3Fu{z5Y)0fPqTp8wx6U^Tjwk(v#3yz4oz&}3{ z+hbM@Ni4`aqEE}+3qy+Ot3Swmp-hNyC*yeC-@-Fz=RLdSssJOg1GFD5-4RiX0uO@1 z50DlLPW?Q9Z}tYOTKXageaRG6e^B6UND(hYW9Wj$u72=>!sV!FMIE_vJMz=|IwWP>)^$0U2EFe7>|48t?pFrGPqFhe*@?{F*jrcGsL9ljI-K2)PA)?;yh2|e@`K5NNoHsUYWef1 zuN>@2%Mq;x^%ciKSt3iN(dv0d@rUsq@dNu;eOr7VoD9^vCxY8|^hLe+RUncqO;0MF zR!ceBEKg*6#X_=;FH>g<5YyEE>3%xr{>uSF(ID>v}SsZ{@b5OOZg>gFN63eGB0s%j)5oLLmcxHB^^Pd}y%A z?@>de$m;&O?6$jJb6bh!iSh}l5TX4_U?O5XaXr3HbiMiv*hZua;6#F_1R*mPd;?E7 z7V=}zFlh9IIky1@H$jhy{RLYcmGl3fInHh0Dwm^s@86ymf{P$ zM3wE}x#mmtW;|zH%9AedVJF55^Gbg>l7^4Fm^ zg%(JRBzvx~`&3scE&M;Bf88U0`w7%Rpml)s#O0hu4qyQ|D;<%8uk^>yphyTPWDuZS zuDzN_V13&Y(cR^rT&_&ctu-A~;6Yx9Oe0~y@B$tqXZ)Hao0p=FLt`L)A%hnzWXbZ# zyZ(;2bl;%#sf=Czqx)py!dE+H+)2rQF8@y#uQ~&1cxCq}(pJk$H*4U(3#wo(PKfg# zFG0&h4tm=FvRy_=(CE@IMfx*q?i{cDaQbwIC`0-T_&+@Q5XktT0r^xQ0+O36#%19);zLf|B594%fQ zApIM=)2NyZyTG0B0Ql8yyXiM_>a<PjPxzxxl zX(s!?x&lBg)jgK7hB@?9W@ep8gA&l&)rllawVRoqJ~poUT*DmZs5u4(v8zN!O`Yn0 zB~6z}T^h~pN2;SvbHX#4lM2mxSw+EAT8IY%72*7mCD7;r#^+vRNBsxI1H|}vgl2CB44aszCXZx?iTP`UD6+|pu662EJn+ej}{@@ zfpF+{e3BJZ=SwqBm8qp2*z+yH2N~?KvwSz8ld7Ff*Vo&57$YZp2IL9MOrIb~Td+HX7R#%!EIFzp^;@yV-I#n4ErPws1n~ ze$CXx(`89!%%hol@gkEU;3>O7Mc5^C55Zwd{prAajafJq?L60iO5x6`Y#vZ!Et6=| zFEDtj2~byS&OC4S>A7}@#j(Q_eIiof`Qgfal`XKDR~tV7 zNp&H`-*wgUHq|xy)Lhdat||$}%PzosHYaVgIZ{jD-{8|xp&ku)M#arG>=LyZ+@7un zENUCC?m8bRVU{Qul^%-b#vDd0d{kN4mSDb22}_xf`C!DwYR+tMy&@h|&jhYWONQMX&>Y*O1umHD`5* z2!t5{6}$b48mvEnQS3_Wm1UVbOEA<;dpdANFld^5)}36FuXS)DD?S^sN1d( za$%j~x?@|82ktm!eIQfAV(YP^|LH#M*mzMf_@w?8`P(=5ecT!+_u;U{wY928ckRub zp(5!MROG}$#W`}qC1Dw@GJkP=p$AD8+^Rg6S@vfi|93<2{}7KeM-VunUQPhUj4KE^ zN}H*rH|6D0jpH-zTpC_Le#b`n-Q56TP@n;(C$2T8?E(3;b?Eqrvr7mVXOCSj=(6@` zeEOT=_s4$>yk8gdGCgP6R1jsE{5f+s0V`CnTLQCUBZCPOJda+Kz{Y(B->Yh^Y(*D( zCYS$G1m@9W>B^5rT+G8v8;52)HDG9Z-Q@o{#{O4Acf-Z)<3ra9G15w~Ni=(YsA^4^UMW6vq;79|CAoabf)H6!1d;o^M6Bd*Ru)yYw@7aoH3=1@m|Wj`ljz1w zq|M^H;N(n`+4jbfhxdZ{O}@@6+n|?w6B-X%sHoP{=G!&iN8b7L%|@J*x{ZoOiY~3%AC_)e^r-tSC)Kl z4BXbKOI|X&P7RCm&7PQ|_T=ZAz5QUI(Epdu=}!R3TJbWCf+xmNyf=BK79Z?f$#K4) z%!OEas*rBxDKVi-kNXNa5@wPO9e@4Afm(`IP1`fh{SN%owd@;Sn}}jNB%OE>n)Ao9 zrEa+e3)Uq2?n#&y=a*kRZwKG_`b{6gdhXzyL==9R55HSL7f@gZS2&|_u*6YB1^%U0 zmJvn=&B`6Ect+rGL|XM}OaB@=7oiF-e#jK+xonT^o6aFt#tlh!4@PBkB|YPZBK0}p zS4k-6z0c|&BJUG0BC_%-5BY6HHCPr6cpQ_$#X=XuXbE(IRnNOV&~-RAiXea#zd$vN zIKq-GRTiwQ4;OP-P}?p*L`%7%MWtKMHi)4@=T3Y2Uv7@@BvcmIeMy<37KQ9a*bLTX zIA~6^c^TW*EN$paP!JTR?P7Wa=w28_h{}Q84y_06^)zP&8p7<%3k!>iX zO(=l+BVqVG2ll+{?4&-2@2B((6-6TlzdMK@9}=nCdU2HSfo1C-+q!J>Zq~Qe;9IyD zOSlI{upe?ErjbqkExTsJuyPB6Y#X`OEds++m?@t<8YpS`=>Li<|NpO5d*yxrcmwLF zN`SPSzReP94W|d?df$c!Pv6C(i^YM_8Z1r|m}_*!Ei5aegB!&Ipw0^;nmZ$OzH_L2 zvPh1j+8nc4!XK?YT@eXgwjqU#14yhS<`x)j{Dz{ z??!{=MXchpPhvYtt09`j~Bj`oS_?Z)p z;{Gd(4(d#_sp*JhU*V9z>fxoF0yPwGq}SoDNH$}am>d-l+b$;Mr;HMrvXl$&eWwoZ ziwD>Ozy64I-|3uy$ZpNJ*N0g3r^MP@`WH@9&y3Dm-31`sQiiCMsmf#X$MH#qN1l3M zI>BDeVFBaEr}YFaJIGfZB!^oNnpJd^)r*TqCP)u^s5+I^2Imwtr+)Hk|63dU?0;^3 z+vHkl&C&?iqvzA$jB*)klj^-D7WN{v_JpE9iv4bsNKs7*PEdt|_Htc^u$eLu(9nG( zn{Im_L#unZaK!wS-sfDAoR>_6>)>HcP=Fn9jM-P&LO5cFFXB?H6l_TeVerVqchnsj z6>VCi>}3Aqq4IbpORj>gG*y<{c_xV#&L`B1j2U?8%ks2vv9FppDj09_YR4Y_2k17x zWktnH^&A>cF542SpFT15uCJMs7B$;D`$F=nF}ei}3L0>#smt1FaI19?I8_oz?fl)4$?pVmR3$~hQ)35AS*NAMg zi;@trYmdHI5K9`}FGI{@rTF^HwyhVEsG8nSD=kI0`XH_ggMaCrQk9ZEFD zdwA4nl_;sw?~3AeVE7coS1k-D#*+4^n6^(Mfc{ydOZo)Dde-Ng9VVQp1p>+rPUF%7 z=gCp#6F6dwSA(wy%mtCgv0%@pNV!@+9gKBEU9fIViM6+#NstfSWjkq)dGm%qnpa0d zTgm4BLsLs_34Gb;gm*IxUW)J6KQQa!9V-y*CXqlT{8ky5arH6tkL$pW{NK?MGiIkmyc^82S253WZW5ON@;qvPNAVSM}{R& zzVMuz0oMOv_tZBtZQIEaD|W9RN*4lfl)vVW#?bZv6b`~bwB?)!+( zwZnL7y>!L{zA?Xhex6#{@m7E_3Z9u!gf>l11|rvsq^Pt=f-jwNp2)LM#a5!0o4LY@!e9BBg#1B{ENJ33RF4nf8VZw}< zI?7=9DP;%XngtamB#(xR;q*whIxsoO@*{N|mSznVm_|isKv_j-!t25Zk%yho;XAfu zi7F4qrOtL-8ywZ;&qb>cDjwSR`3zvmTgZr2b(}(mKA1{Jy-b@W2E3UsyN7e18Y9Qh zc1BGeI}|$Z96sHU>%kvk1XCjbHHtxq(Tcn*5Yi^^>O2OHRF1CB0v58itU1EF?`=7-(LQq=eku2Fa;m%1#;m&de3uA&q zj?}Qi#qm%y_tWm^PhTuPUa(N!e_tv3sR=M6+W)?sZWh_3P#KuF!F*b?6u-O&u0Qd_ zB3et+9{Z~jlI3DPk-2B=8BOLJa)o!;Z5z_KMUa2imSyQ zMcog${ADEF`}U1R7sed6sX-jYLA!4>t zQ%(__)~5F+?Lh4CLMXX&PLg^;60pvtMdWie>(=Fd1ZF*Z1?n^Z6JL`Mj1EYv;=okx4X4{Iq79Byq&*wb`Qa{qt6I_h|!#o zmf8a4jc)JoEB3BH&0Mw%(ED6B7zri%L%ZuNOl-z+uj$)|M78J6ZUsb9j1GrUIJ=Wk zY_AHwy26x3;-F9P#HZ)Fkz5C7ci}{O?_vs7U@Gka1}_CiF|m$^R=!^Po{w494SOE#0LwBdnIZ1=AMO^*?}0W z)<#QLeLOafLCcXbKd^3RK}DX5rvt#^F-0e4 zpy;p#vJi3a%MC^~nYQE_Q51oUhqeHVbFIw?m1lsnM+Y0c0)9~CgvbYBrS~KaBJrp@w09|}dSh=A zl0N@5W2PnPlHNoz#97UJX_)@~F=RR3WO;5P!b(4&CBrai(87E-#Cb|Sye*x*+t#Ge zN}D>V8#FHuX#{zTAl4^`FzELvzh^YTHT!WrZ^#7Sz$QHtZK+VEHo305zbm^hK9wkr zU(N#7tj-Y)wbL_gwoRZhpi}8Ub!a)l*>fsLPnN8|OtsC{g2IW(SSb0Ny*V%LQ+DO9 zcr>CgdeqzC@QnIR-j-QvVU2d^+-&3*5$Ck$*KLALuDEZE8jP9^j-kL1_gmye!}&6# z3QkREBXAlYsMU!s?4DUl_o%-24CC%+lU^CU^dM^GtxDtCb)(rDH;3rv727L+8&lxi zILF7NU0oTCW|JuK#gt4#__M)WJO_ zMtu30Mi;ysp3in6`8_w8ta_!fWzn>BWqBdV{draVsyHqzPLn>d6x;Xm3yYGhGF3e=SIA-uflkM}PnP;e2aqXdn&I+op)f$3Jx&2$4o{}=zt0(n4 z-Uy*V3=1z@_^e@vN1F{M_egf#G};rpLOCK&E8Q!q5$}2H<-Un#fO77XbVSuIQz|K{ zH=Dw;e)WGegw!)Ta^x94`f z^n2<3n(e~jcX>@rH18=QQau|pw4XkN2yowP`_sbw4CovUmB=rbn{ITpNf>GR0eQZD z7P3<_OFgh_(14TjtF~NWRuxgVc6pvnI+sCs(qwZ&>wTAbFUwNL5fB2!SqPCx?j|Yl zs7E^Qon@4jcilkg^(uDQ0@6vV=YjCrI`U;)=X!mH?doWTgTotkRx*k)RO!R|y z_0|o~EgmTJTKXzWA8L>E1Q_iZk#~jzkT1oH2RN<;(xoST{6+^A1xaV(gU0Sd?K}M}Ths%~1&vn&4Hwh*+y)oI zXMQV`i>|@ek7ZWeW=V`Xty{@SChD6f(xO=bhP|A5CF<-bZ^?w|^vyoIiBg$rr!8>p z@0&XE+rMlGaCt4M9`?>(*?r4rm3eQk>1&`I(8Aj5kCo&4y6%09LO+N^k2!+Tm|wzp zaws@h_g)9dzQKo!f6ZU|J7iY2x8Mu761_2&%tLE;B$ubnz|qjFq&Pq^SWu!ljd3lZ z=?WZduVzeoa%uP>R*O5Ytv{bo_^mlO%0wjYaq5=&dnH-TAOKF)xn}d<_A`^~OiTpbRb|!aadc+Z>g-c^Rn+3nlj##+~Mv}ZO^lo`!wFez;vnB7Jpf6O3?&XdJdMF^R(jU<~z1Rks$Hn~73s;1RG)G22?uuqmn5aiD`%abyO4LY)LyU*6ydlXNOn=H{ z#=0GZN(HTm^PrEuZ~pVTl~dZKGp!?n-Sh3wRgz!6{LK(}_qQFjVbi|dgnYbYYpz-e zR%rkXy2J`2AI&lIh{k_t#R33`2}ciE|9F0*k|rK#_CRE_%6%13hG2Y_@a)Bn#5k6a z70|w`c<7Urd}*hF@9Y6ZMMcZ0M`jC-hC|&*&-+RtkLfYvvWC+S_IwXdH`h^hW)W1L z@b_@PFZ5Pgj>q)fDw(^tG3_zKqRSxIp1vGgeAWbyT;$N%5#}O~sOK$7c0V`+J1VOM zWHd0BLrs@^be7~5%@3wGnJdLmS8eMW2CKcI&fj~xp#jQ{$XFC4ss>HIU)P`%+P$A{ zD#(bMPV#DauChQmn|;tFS}?~U=0LU$fl_(K4ELEoMBU!LFdgQrN06DhE0T(~N&3wl zg54)T`Lz2GaTiBRxBGC~**)A@6QPi?%pTEZYYo&*^z1Tljxwv!V0&GKt*Ha~7<^*wqnPK*54vRh%PuTOER1;$SIek3YQ(1xc9gbC z&iuOnElzVHc! zW_Xg?uRgkAS3SbOU`n^L>?gRHhRAFD`mPjtyVBJX1#nRs^X)JfD+%sHpXZmdkBqk? zpt+{wnj?|6*@Z+B$p5 zs5lm$vwr+ITE@K6oHjJ`E5q=>F{nx|gaJBF=MIk#G3P@P4y_$U7;hALM#&NvKLBla z%4SCDegGFmFuh_~iyf|J!o%J>B%LrqINtC{SVr#3StD~+Rka9+h5czFh-OS-MLA7EE7 z4Q|%d6a--sj_wNHc-n`z+X9S1B`oqAIcN5!PJw2MUV3tc^2B(I*|~(jHDdls;O+pp z7)Y5@4#)o{^gyMAfu8c^nfRFd0kD@)T*lkZ#R%evSBCm*uY*bA2Qw=cvpiPM+j(4Z zPnbyP1`R$fH@SETbbs&SE5ow-7&Sht&7FuFI^&h<+tIwRoB~|mYV(X1p=~`mat7Rz zz`df`$`zU{k|*9J7JN5=r#JnuV|7(Ypd7tzZ*F9fRB(`dwkkn^03iM=i`05QB2O6$o$(>! z6WY-D97$(i)Az<-DA}5``vroqQFn;RSIgdgTh0yjiF{^7U}2&5gHG~r>NC~rxFFJ) znX_%*NZcX+Qzh>8aP)Fmc-Eoup=m325PchW%IG~NxA>R&pNDJyEq|+evjyzf*L{q{ zw@-d2N;!jJxBj~08p?uR(B(6QPno>uB6@W+!npEnf7Dkb8nn0%1=Mjn_M$Rxi@1T+ zo`6qi%ry4)FA1+bVa5!~Z3fie&&!Ka>nmoDvAAf9-kNWubEP!A6TVGFkO}ugJ*K9M zyepD`PaL80nC^S7d&(^BH`EM1iUUVCHHdyEq6kz82mS+dIV_dWdQiC)B%V zPjBjdu#hg2YS$zGPBW^hv8(8!zU`!beFtVJxd;^6Ju6B_3`IcNjvxt#1Cg^h0rI00 z1A=ws+{y7D{K8IBGhMDhS-?9fK_Fa}yoLq`-F|16(b!Z($Wyg{331`K4r)d%H6JkW zk^EZ7d$WZuN2c9_tY4T|4i;i=HwD+B$v7N9z1>8kNoP;rVoLQ6D$%0X;ni%%+Rjcc zuUuZU5Zw$~pArSxIyAcktQ3hc#RZ0w$oP@-<2J47hl)Jo3Y1{h{2=IW`vHB$a(e18EsM3yxYa~GqN<; zOJAItL(?6sVBv`Mi-_a;{t~IkQ;F%Qmx4yYp;G>Sp=-`2XfldC)1l`D1pl=~(l^^w2D&c^GHEk>wFE zQ@i-1ICxq-uv72eTj2jYRwdFEU|zg6c@yk6u6%J8mhnAb^(=52UPGNGDTXSe7bXUk zD&UAN0CLdVU~B!n+7zRgkDxt)GOV6s;%MJ)EHdp#kY3bDj5EJb_wsnd8t}*R)VY)R I<8HVAADDZ#umAu6 literal 0 HcmV?d00001 diff --git a/docs/mint.json b/docs/mint.json index bf40a9778..08534e46d 100644 --- a/docs/mint.json +++ b/docs/mint.json @@ -220,7 +220,8 @@ "documentation/platform/admin-panel/org-admin-console" ] }, - "documentation/platform/secret-sharing" + "documentation/platform/secret-sharing", + "documentation/platform/secret-scanning" ] }, { From 4e6c860c69872d7969bcb7abcca02f6332b20163 Mon Sep 17 00:00:00 2001 From: Daniel Hougaard Date: Wed, 12 Mar 2025 07:46:29 +0400 Subject: [PATCH 58/71] Update secret-scanning.mdx --- docs/documentation/platform/secret-scanning.mdx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/documentation/platform/secret-scanning.mdx b/docs/documentation/platform/secret-scanning.mdx index 7d885f6ca..bb1ab6bde 100644 --- a/docs/documentation/platform/secret-scanning.mdx +++ b/docs/documentation/platform/secret-scanning.mdx @@ -36,7 +36,7 @@ After an exposed secret is detected, it will be marked as `Needs Attention`. Whe You can mark the risk as `Resolved` by changing the status to one of the following states: - **This Is a False Positive**: The secret was not exposed, but was detected by the scanner. - **I Have Rotated The Secret**: The secret was exposed, but it has now been removed. -- **No Rotate Needed**: You are choosing to ignore this risk. You may choose to do this if the risk is non-sensitive or otherwise not a security risk. +- **No Rotation Needed**: You are choosing to ignore this risk. You may choose to do this if the risk is non-sensitive or otherwise not a security risk. ![Needs Attention](/images/platform/secret-scanning/needs-attention.png) From f1b6c3764f3a1dcfc2039c71e8dc3e26301b823b Mon Sep 17 00:00:00 2001 From: Daniel Hougaard Date: Wed, 12 Mar 2025 08:07:20 +0400 Subject: [PATCH 59/71] Update secret-scanning.mdx --- docs/documentation/platform/secret-scanning.mdx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/documentation/platform/secret-scanning.mdx b/docs/documentation/platform/secret-scanning.mdx index bb1ab6bde..4f030e882 100644 --- a/docs/documentation/platform/secret-scanning.mdx +++ b/docs/documentation/platform/secret-scanning.mdx @@ -5,7 +5,7 @@ description: "Scan and prevent secret leaks in your code repositories" The Infisical Secret Scanner allows you to keep an overview and stay alert of exposed secrets across your entire GitHub organization and repositories. -The Infisical Secret Scanner helps you detect exposed secrets that have already made it into your remote repositories. To further enhance security, we recommend you also use our [CLI Secret Scanner](/cli/scanning-overview#automatically-scan-changes-before-you-commit) to scan for exposed secrets prior to pushing your changes. +To further enhance security, we recommend you also use our [CLI Secret Scanner](/cli/scanning-overview#automatically-scan-changes-before-you-commit) to scan for exposed secrets prior to pushing your changes. ## Code Scanning From 1d186b19502c94560417842a6767e5f19777bc09 Mon Sep 17 00:00:00 2001 From: Scott Wilson Date: Tue, 11 Mar 2025 22:00:25 -0700 Subject: [PATCH 60/71] feature: access tree --- .../src/ee/services/license/license-fns.ts | 2 +- .../secret-approval-request-service.ts | 2 +- .../src/server/routes/v1/project-router.ts | 29 +++ .../secret-folder/secret-folder-service.ts | 52 +++- backend/src/services/secret/secret-types.ts | 2 +- frontend/package-lock.json | 245 ++++++++++++++++++ frontend/package.json | 3 + .../permissions/AccessTree/AccessTree.tsx | 213 +++++++++++++++ .../components/AccessTreeContext.tsx | 51 ++++ .../components/AccessTreeErrorBoundary.tsx | 105 ++++++++ .../components/PermissionSimulation.tsx | 137 ++++++++++ .../AccessTree/components/index.ts | 3 + .../AccessTree/edges/BasePermissionEdge.tsx | 34 +++ .../permissions/AccessTree/edges/index.ts | 1 + .../permissions/AccessTree/hooks/index.ts | 91 +++++++ .../permissions/AccessTree/index.ts | 1 + .../nodes/FolderNode/FolderNode.tsx | 77 ++++++ .../components/FolderNodeTooltipContent.tsx | 131 ++++++++++ .../nodes/FolderNode/components/index.ts | 1 + .../AccessTree/nodes/FolderNode/index.ts | 1 + .../permissions/AccessTree/nodes/RoleNode.tsx | 30 +++ .../permissions/AccessTree/nodes/index.ts | 2 + .../permissions/AccessTree/types/index.ts | 21 ++ .../AccessTree/utils/createBaseEdge.ts | 26 ++ .../AccessTree/utils/createFolderNode.ts | 180 +++++++++++++ .../AccessTree/utils/createRoleNode.ts | 19 ++ .../AccessTree/utils/formatActionName.ts | 3 + .../AccessTree/utils/getActionRuleMap.ts | 27 ++ .../permissions/AccessTree/utils/index.ts | 6 + .../AccessTree/utils/positionElements.ts | 28 ++ frontend/src/components/permissions/index.tsx | 1 + frontend/src/components/v2/Select/Select.tsx | 6 +- .../ProjectPermissionContext.tsx | 39 +-- frontend/src/helpers/permissions.ts | 39 +++ frontend/src/hooks/api/admin/index.ts | 4 +- .../src/hooks/api/secretFolders/queries.tsx | 28 +- frontend/src/hooks/api/secretFolders/types.ts | 9 + frontend/src/hooks/utils/secrets-overview.tsx | 9 +- .../MenuIconButton/MenuIconButton.tsx | 4 +- .../AuditLogsPage/components/LogsTable.tsx | 9 +- .../components/RolePermissionsSection.tsx | 194 +++++++------- .../components/ShareSecretForm.tsx | 20 +- .../OverviewPage/OverviewPage.tsx | 11 +- .../CreateSecretForm/CreateSecretForm.tsx | 2 +- .../SqlDatabaseInputForm.tsx | 85 +++--- .../components/ActionBar/FolderForm.tsx | 59 ++--- .../EditDynamicSecretSqlProviderForm.tsx | 90 ++++--- .../FolderListView/FolderListView.tsx | 13 +- .../components/PitDrawer/PitDrawer.tsx | 2 +- .../SecretListView/SecretDetailSidebar.tsx | 2 +- .../ChangePasswordSection.tsx | 2 +- 51 files changed, 1889 insertions(+), 262 deletions(-) create mode 100644 frontend/src/components/permissions/AccessTree/AccessTree.tsx create mode 100644 frontend/src/components/permissions/AccessTree/components/AccessTreeContext.tsx create mode 100644 frontend/src/components/permissions/AccessTree/components/AccessTreeErrorBoundary.tsx create mode 100644 frontend/src/components/permissions/AccessTree/components/PermissionSimulation.tsx create mode 100644 frontend/src/components/permissions/AccessTree/components/index.ts create mode 100644 frontend/src/components/permissions/AccessTree/edges/BasePermissionEdge.tsx create mode 100644 frontend/src/components/permissions/AccessTree/edges/index.ts create mode 100644 frontend/src/components/permissions/AccessTree/hooks/index.ts create mode 100644 frontend/src/components/permissions/AccessTree/index.ts create mode 100644 frontend/src/components/permissions/AccessTree/nodes/FolderNode/FolderNode.tsx create mode 100644 frontend/src/components/permissions/AccessTree/nodes/FolderNode/components/FolderNodeTooltipContent.tsx create mode 100644 frontend/src/components/permissions/AccessTree/nodes/FolderNode/components/index.ts create mode 100644 frontend/src/components/permissions/AccessTree/nodes/FolderNode/index.ts create mode 100644 frontend/src/components/permissions/AccessTree/nodes/RoleNode.tsx create mode 100644 frontend/src/components/permissions/AccessTree/nodes/index.ts create mode 100644 frontend/src/components/permissions/AccessTree/types/index.ts create mode 100644 frontend/src/components/permissions/AccessTree/utils/createBaseEdge.ts create mode 100644 frontend/src/components/permissions/AccessTree/utils/createFolderNode.ts create mode 100644 frontend/src/components/permissions/AccessTree/utils/createRoleNode.ts create mode 100644 frontend/src/components/permissions/AccessTree/utils/formatActionName.ts create mode 100644 frontend/src/components/permissions/AccessTree/utils/getActionRuleMap.ts create mode 100644 frontend/src/components/permissions/AccessTree/utils/index.ts create mode 100644 frontend/src/components/permissions/AccessTree/utils/positionElements.ts create mode 100644 frontend/src/helpers/permissions.ts diff --git a/backend/src/ee/services/license/license-fns.ts b/backend/src/ee/services/license/license-fns.ts index 21d378802..114a6b217 100644 --- a/backend/src/ee/services/license/license-fns.ts +++ b/backend/src/ee/services/license/license-fns.ts @@ -21,7 +21,7 @@ export const getDefaultOnPremFeatures = (): TFeatureSet => ({ secretVersioning: true, pitRecovery: false, ipAllowlisting: false, - rbac: false, + rbac: true, customRateLimits: false, customAlerts: false, secretAccessInsights: false, 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 72d36c26b..49ee44ab2 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 @@ -57,6 +57,7 @@ import { SmtpTemplates, TSmtpService } from "@app/services/smtp/smtp-service"; import { TUserDALFactory } from "@app/services/user/user-dal"; import { TLicenseServiceFactory } from "../license/license-service"; +import { throwIfMissingSecretReadValueOrDescribePermission } from "../permission/permission-fns"; import { TPermissionServiceFactory } from "../permission/permission-service"; import { ProjectPermissionSecretActions, ProjectPermissionSub } from "../permission/project-permission"; import { TSecretApprovalPolicyDALFactory } from "../secret-approval-policy/secret-approval-policy-dal"; @@ -77,7 +78,6 @@ import { TSecretApprovalDetailsDTO, TStatusChangeDTO } from "./secret-approval-request-types"; -import { throwIfMissingSecretReadValueOrDescribePermission } from "../permission/permission-fns"; type TSecretApprovalRequestServiceFactoryDep = { permissionService: Pick; diff --git a/backend/src/server/routes/v1/project-router.ts b/backend/src/server/routes/v1/project-router.ts index dcedf5fa5..97317c62e 100644 --- a/backend/src/server/routes/v1/project-router.ts +++ b/backend/src/server/routes/v1/project-router.ts @@ -2,10 +2,12 @@ import { z } from "zod"; import { IntegrationsSchema, + ProjectEnvironmentsSchema, ProjectMembershipsSchema, ProjectRolesSchema, ProjectSlackConfigsSchema, ProjectType, + SecretFoldersSchema, UserEncryptionKeysSchema, UsersSchema } from "@app/db/schemas"; @@ -675,4 +677,31 @@ export const registerProjectRouter = async (server: FastifyZodProvider) => { return slackConfig; } }); + + server.route({ + method: "GET", + url: "/:workspaceId/folders/project-environments", + config: { + rateLimit: readLimit + }, + schema: { + params: z.object({ + workspaceId: z.string().trim() + }), + response: { + 200: z.record( + ProjectEnvironmentsSchema.extend({ folders: SecretFoldersSchema.extend({ path: z.string() }).array() }) + ) + } + }, + onRequest: verifyAuth([AuthMode.JWT]), + handler: async (req) => { + const environmentsFolders = await server.services.folder.getProjectEnvironmentsFolders( + req.params.workspaceId, + req.permission + ); + + return environmentsFolders; + } + }); }; diff --git a/backend/src/services/secret-folder/secret-folder-service.ts b/backend/src/services/secret-folder/secret-folder-service.ts index aabc35683..1cc275cad 100644 --- a/backend/src/services/secret-folder/secret-folder-service.ts +++ b/backend/src/services/secret-folder/secret-folder-service.ts @@ -2,7 +2,7 @@ import { ForbiddenError, subject } from "@casl/ability"; import path from "path"; import { v4 as uuidv4, validate as uuidValidate } from "uuid"; -import { ActionProjectType, TSecretFoldersInsert } from "@app/db/schemas"; +import { ActionProjectType, TSecretFolders, TSecretFoldersInsert } from "@app/db/schemas"; import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service"; import { ProjectPermissionActions, ProjectPermissionSub } from "@app/ee/services/permission/project-permission"; import { TSecretSnapshotServiceFactory } from "@app/ee/services/secret-snapshot/secret-snapshot-service"; @@ -27,7 +27,7 @@ type TSecretFolderServiceFactoryDep = { permissionService: Pick; snapshotService: Pick; folderDAL: TSecretFolderDALFactory; - projectEnvDAL: Pick; + projectEnvDAL: Pick; folderVersionDAL: TSecretFolderVersionDALFactory; projectDAL: Pick; }; @@ -580,6 +580,51 @@ export const secretFolderServiceFactory = ({ return folders; }; + const getProjectEnvironmentsFolders = async (projectId: string, actor: OrgServiceActor) => { + // folder list is allowed to be read by anyone + // permission is to check if user has access + await permissionService.getProjectPermission({ + actor: actor.type, + actorId: actor.id, + projectId, + actorAuthMethod: actor.authMethod, + actorOrgId: actor.orgId, + actionProjectType: ActionProjectType.SecretManager + }); + + const environments = await projectEnvDAL.find({ projectId }); + + const folders = await folderDAL.find({ + $in: { + envId: environments.map((env) => env.id) + }, + isReserved: false + }); + + const environmentFolders = Object.fromEntries( + environments.map((env) => { + const relevantFolders = folders.filter((folder) => folder.envId === env.id); + const foldersMap = Object.fromEntries(relevantFolders.map((folder) => [folder.id, folder])); + + const foldersWithPath = relevantFolders.map((folder) => ({ + ...folder, + path: folder.parentId + ? (function buildPath(f: TSecretFolders): string { + if (!f.parentId) { + return ""; + } + return `${buildPath(foldersMap[f.parentId])}/${f.name}`; + })(folder) + : "/" + })); + + return [env.slug, { ...env, folders: foldersWithPath }]; + }) + ); + + return environmentFolders; + }; + return { createFolder, updateFolder, @@ -589,6 +634,7 @@ export const secretFolderServiceFactory = ({ getFolderById, getProjectFolderCount, getFoldersMultiEnv, - getFoldersDeepByEnvs + getFoldersDeepByEnvs, + getProjectEnvironmentsFolders }; }; diff --git a/backend/src/services/secret/secret-types.ts b/backend/src/services/secret/secret-types.ts index f42f70b78..6a5c097ed 100644 --- a/backend/src/services/secret/secret-types.ts +++ b/backend/src/services/secret/secret-types.ts @@ -2,6 +2,7 @@ import { Knex } from "knex"; import { z } from "zod"; import { SecretType, TSecretBlindIndexes, TSecrets, TSecretsInsert, TSecretsUpdate } from "@app/db/schemas"; +import { ProjectPermissionSecretActions } from "@app/ee/services/permission/project-permission"; import { OrderByDirection, TProjectPermission } from "@app/lib/types"; import { TProjectDALFactory } from "@app/services/project/project-dal"; import { TProjectBotDALFactory } from "@app/services/project-bot/project-bot-dal"; @@ -20,7 +21,6 @@ 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; diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 8fdfeea1f..a7e9ca7e7 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -10,6 +10,7 @@ "dependencies": { "@casl/ability": "^6.7.2", "@casl/react": "^4.0.0", + "@dagrejs/dagre": "^1.1.4", "@dnd-kit/core": "^6.3.1", "@dnd-kit/modifiers": "^9.0.0", "@dnd-kit/sortable": "^10.0.0", @@ -47,8 +48,10 @@ "@tanstack/react-router": "^1.95.1", "@tanstack/virtual-file-routes": "^1.87.6", "@tanstack/zod-adapter": "^1.91.0", + "@types/dagre": "^0.7.52", "@types/nprogress": "^0.2.3", "@ucast/mongo2js": "^1.3.4", + "@xyflow/react": "^12.4.4", "argon2-browser": "^1.18.0", "axios": "^1.7.9", "classnames": "^2.5.1", @@ -507,6 +510,24 @@ "react": "^16.0.0 || ^17.0.0 || ^18.0.0" } }, + "node_modules/@dagrejs/dagre": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/@dagrejs/dagre/-/dagre-1.1.4.tgz", + "integrity": "sha512-QUTc54Cg/wvmlEUxB+uvoPVKFazM1H18kVHBQNmK2NbrDR5ihOCR6CXLnDSZzMcSQKJtabPUWridBOlJM3WkDg==", + "license": "MIT", + "dependencies": { + "@dagrejs/graphlib": "2.2.4" + } + }, + "node_modules/@dagrejs/graphlib": { + "version": "2.2.4", + "resolved": "https://registry.npmjs.org/@dagrejs/graphlib/-/graphlib-2.2.4.tgz", + "integrity": "sha512-mepCf/e9+SKYy1d02/UkvSy6+6MoyXhVxP8lLDfA7BPE1X1d4dR0sZznmbM8/XVJ1GPM+Svnx7Xj6ZweByWUkw==", + "license": "MIT", + "engines": { + "node": ">17.0.0" + } + }, "node_modules/@date-fns/tz": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/@date-fns/tz/-/tz-1.2.0.tgz", @@ -3955,6 +3976,61 @@ "@babel/types": "^7.20.7" } }, + "node_modules/@types/d3-color": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/@types/d3-color/-/d3-color-3.1.3.tgz", + "integrity": "sha512-iO90scth9WAbmgv7ogoq57O9YpKmFBbmoEoCHDB2xMBY0+/KVrqAaCDyCE16dUspeOvIxFFRI+0sEtqDqy2b4A==", + "license": "MIT" + }, + "node_modules/@types/d3-drag": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/@types/d3-drag/-/d3-drag-3.0.7.tgz", + "integrity": "sha512-HE3jVKlzU9AaMazNufooRJ5ZpWmLIoc90A37WU2JMmeq28w1FQqCZswHZ3xR+SuxYftzHq6WU6KJHvqxKzTxxQ==", + "license": "MIT", + "dependencies": { + "@types/d3-selection": "*" + } + }, + "node_modules/@types/d3-interpolate": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/d3-interpolate/-/d3-interpolate-3.0.4.tgz", + "integrity": "sha512-mgLPETlrpVV1YRJIglr4Ez47g7Yxjl1lj7YKsiMCb27VJH9W8NVM6Bb9d8kkpG/uAQS5AmbA48q2IAolKKo1MA==", + "license": "MIT", + "dependencies": { + "@types/d3-color": "*" + } + }, + "node_modules/@types/d3-selection": { + "version": "3.0.11", + "resolved": "https://registry.npmjs.org/@types/d3-selection/-/d3-selection-3.0.11.tgz", + "integrity": "sha512-bhAXu23DJWsrI45xafYpkQ4NtcKMwWnAC/vKrd2l+nxMFuvOT3XMYTIj2opv8vq8AO5Yh7Qac/nSeP/3zjTK0w==", + "license": "MIT" + }, + "node_modules/@types/d3-transition": { + "version": "3.0.9", + "resolved": "https://registry.npmjs.org/@types/d3-transition/-/d3-transition-3.0.9.tgz", + "integrity": "sha512-uZS5shfxzO3rGlu0cC3bjmMFKsXv+SmZZcgp0KD22ts4uGXp5EVYGzu/0YdwZeKmddhcAccYtREJKkPfXkZuCg==", + "license": "MIT", + "dependencies": { + "@types/d3-selection": "*" + } + }, + "node_modules/@types/d3-zoom": { + "version": "3.0.8", + "resolved": "https://registry.npmjs.org/@types/d3-zoom/-/d3-zoom-3.0.8.tgz", + "integrity": "sha512-iqMC4/YlFCSlO8+2Ii1GGGliCAY4XdeG748w5vQUbevlbDu0zSjH/+jojorQVBK/se0j6DUFNPBGSqD3YWYnDw==", + "license": "MIT", + "dependencies": { + "@types/d3-interpolate": "*", + "@types/d3-selection": "*" + } + }, + "node_modules/@types/dagre": { + "version": "0.7.52", + "resolved": "https://registry.npmjs.org/@types/dagre/-/dagre-0.7.52.tgz", + "integrity": "sha512-XKJdy+OClLk3hketHi9Qg6gTfe1F3y+UFnHxKA2rn9Dw+oXa4Gb378Ztz9HlMgZKSxpPmn4BNVh9wgkpvrK1uw==", + "license": "MIT" + }, "node_modules/@types/debug": { "version": "4.1.12", "resolved": "https://registry.npmjs.org/@types/debug/-/debug-4.1.12.tgz", @@ -4382,6 +4458,64 @@ "vite": "^4 || ^5 || ^6" } }, + "node_modules/@xyflow/react": { + "version": "12.4.4", + "resolved": "https://registry.npmjs.org/@xyflow/react/-/react-12.4.4.tgz", + "integrity": "sha512-9RZ9dgKZNJOlbrXXST5HPb5TcXPOIDGondjwcjDro44OQRPl1E0ZRPTeWPGaQtVjbg4WpR4BUYwOeshNI2TuVg==", + "license": "MIT", + "dependencies": { + "@xyflow/system": "0.0.52", + "classcat": "^5.0.3", + "zustand": "^4.4.0" + }, + "peerDependencies": { + "react": ">=17", + "react-dom": ">=17" + } + }, + "node_modules/@xyflow/react/node_modules/zustand": { + "version": "4.5.6", + "resolved": "https://registry.npmjs.org/zustand/-/zustand-4.5.6.tgz", + "integrity": "sha512-ibr/n1hBzLLj5Y+yUcU7dYw8p6WnIVzdJbnX+1YpaScvZVF2ziugqHs+LAmHw4lWO9c/zRj+K1ncgWDQuthEdQ==", + "license": "MIT", + "dependencies": { + "use-sync-external-store": "^1.2.2" + }, + "engines": { + "node": ">=12.7.0" + }, + "peerDependencies": { + "@types/react": ">=16.8", + "immer": ">=9.0.6", + "react": ">=16.8" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "immer": { + "optional": true + }, + "react": { + "optional": true + } + } + }, + "node_modules/@xyflow/system": { + "version": "0.0.52", + "resolved": "https://registry.npmjs.org/@xyflow/system/-/system-0.0.52.tgz", + "integrity": "sha512-pJBMaoh/GEebIABWEIxAai0yf57dm+kH7J/Br+LnLFPuJL87Fhcmm4KFWd/bCUy/kCWUg+2/yFAGY0AUHRPOnQ==", + "license": "MIT", + "dependencies": { + "@types/d3-drag": "^3.0.7", + "@types/d3-selection": "^3.0.10", + "@types/d3-transition": "^3.0.8", + "@types/d3-zoom": "^3.0.8", + "d3-drag": "^3.0.0", + "d3-selection": "^3.0.0", + "d3-zoom": "^3.0.0" + } + }, "node_modules/acorn": { "version": "8.14.0", "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.14.0.tgz", @@ -5456,6 +5590,12 @@ "node": ">= 0.10" } }, + "node_modules/classcat": { + "version": "5.0.5", + "resolved": "https://registry.npmjs.org/classcat/-/classcat-5.0.5.tgz", + "integrity": "sha512-JhZUT7JFcQy/EzW605k/ktHtncoo9vnyW/2GspNYwFlN1C/WmjuV/xtS04e9SOkL2sTdw0VAZ2UGCcQ9lR6p6w==", + "license": "MIT" + }, "node_modules/classnames": { "version": "2.5.1", "resolved": "https://registry.npmjs.org/classnames/-/classnames-2.5.1.tgz", @@ -5808,6 +5948,111 @@ "url": "https://polar.sh/cva" } }, + "node_modules/d3-color": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-color/-/d3-color-3.1.0.tgz", + "integrity": "sha512-zg/chbXyeBtMQ1LbD/WSoW2DpC3I0mpmPdW+ynRTj/x2DAWYrIY7qeZIHidozwV24m4iavr15lNwIwLxRmOxhA==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-dispatch": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-dispatch/-/d3-dispatch-3.0.1.tgz", + "integrity": "sha512-rzUyPU/S7rwUflMyLc1ETDeBj0NRuHKKAcvukozwhshr6g6c5d8zh4c2gQjY2bZ0dXeGLWc1PF174P2tVvKhfg==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-drag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/d3-drag/-/d3-drag-3.0.0.tgz", + "integrity": "sha512-pWbUJLdETVA8lQNJecMxoXfH6x+mO2UQo8rSmZ+QqxcbyA3hfeprFgIT//HW2nlHChWeIIMwS2Fq+gEARkhTkg==", + "license": "ISC", + "dependencies": { + "d3-dispatch": "1 - 3", + "d3-selection": "3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-ease": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-ease/-/d3-ease-3.0.1.tgz", + "integrity": "sha512-wR/XK3D3XcLIZwpbvQwQ5fK+8Ykds1ip7A2Txe0yxncXSdq1L9skcG7blcedkOX+ZcgxGAmLX1FrRGbADwzi0w==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-interpolate": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-interpolate/-/d3-interpolate-3.0.1.tgz", + "integrity": "sha512-3bYs1rOD33uo8aqJfKP3JWPAibgw8Zm2+L9vBKEHJ2Rg+viTR7o5Mmv5mZcieN+FRYaAOWX5SJATX6k1PWz72g==", + "license": "ISC", + "dependencies": { + "d3-color": "1 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-selection": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/d3-selection/-/d3-selection-3.0.0.tgz", + "integrity": "sha512-fmTRWbNMmsmWq6xJV8D19U/gw/bwrHfNXxrIN+HfZgnzqTHp9jOmKMhsTUjXOJnZOdZY9Q28y4yebKzqDKlxlQ==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-timer": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-timer/-/d3-timer-3.0.1.tgz", + "integrity": "sha512-ndfJ/JxxMd3nw31uyKoY2naivF+r29V+Lc0svZxe1JvvIRmi8hUsrMvdOwgS1o6uBHmiz91geQ0ylPP0aj1VUA==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-transition": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-transition/-/d3-transition-3.0.1.tgz", + "integrity": "sha512-ApKvfjsSR6tg06xrL434C0WydLr7JewBB3V+/39RMHsaXTOG0zmt/OAXeng5M5LBm0ojmxJrpomQVZ1aPvBL4w==", + "license": "ISC", + "dependencies": { + "d3-color": "1 - 3", + "d3-dispatch": "1 - 3", + "d3-ease": "1 - 3", + "d3-interpolate": "1 - 3", + "d3-timer": "1 - 3" + }, + "engines": { + "node": ">=12" + }, + "peerDependencies": { + "d3-selection": "2 - 3" + } + }, + "node_modules/d3-zoom": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/d3-zoom/-/d3-zoom-3.0.0.tgz", + "integrity": "sha512-b8AmV3kfQaqWAuacbPuNbL6vahnOJflOhexLzMMNLga62+/nh0JzvJ0aO/5a5MVgUFGS7Hu1P9P03o3fJkDCyw==", + "license": "ISC", + "dependencies": { + "d3-dispatch": "1 - 3", + "d3-drag": "2 - 3", + "d3-interpolate": "1 - 3", + "d3-selection": "2 - 3", + "d3-transition": "2 - 3" + }, + "engines": { + "node": ">=12" + } + }, "node_modules/damerau-levenshtein": { "version": "1.0.8", "resolved": "https://registry.npmjs.org/damerau-levenshtein/-/damerau-levenshtein-1.0.8.tgz", diff --git a/frontend/package.json b/frontend/package.json index d62d11242..95ad59c7d 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -14,6 +14,7 @@ "dependencies": { "@casl/ability": "^6.7.2", "@casl/react": "^4.0.0", + "@dagrejs/dagre": "^1.1.4", "@dnd-kit/core": "^6.3.1", "@dnd-kit/modifiers": "^9.0.0", "@dnd-kit/sortable": "^10.0.0", @@ -51,8 +52,10 @@ "@tanstack/react-router": "^1.95.1", "@tanstack/virtual-file-routes": "^1.87.6", "@tanstack/zod-adapter": "^1.91.0", + "@types/dagre": "^0.7.52", "@types/nprogress": "^0.2.3", "@ucast/mongo2js": "^1.3.4", + "@xyflow/react": "^12.4.4", "argon2-browser": "^1.18.0", "axios": "^1.7.9", "classnames": "^2.5.1", diff --git a/frontend/src/components/permissions/AccessTree/AccessTree.tsx b/frontend/src/components/permissions/AccessTree/AccessTree.tsx new file mode 100644 index 000000000..adc2d8868 --- /dev/null +++ b/frontend/src/components/permissions/AccessTree/AccessTree.tsx @@ -0,0 +1,213 @@ +import { useCallback, useEffect } from "react"; +import { MongoAbility, MongoQuery } from "@casl/ability"; +import { + faArrowUpRightFromSquare, + faUpRightAndDownLeftFromCenter, + faWindowRestore +} from "@fortawesome/free-solid-svg-icons"; +import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; +import { + Background, + BackgroundVariant, + ConnectionLineType, + Controls, + Node, + NodeMouseHandler, + Panel, + ReactFlow, + ReactFlowProvider, + useReactFlow +} from "@xyflow/react"; +import { twMerge } from "tailwind-merge"; + +import { Button, IconButton, Spinner, Tooltip } from "@app/components/v2"; +import { ProjectPermissionSet } from "@app/context/ProjectPermissionContext"; + +import { AccessTreeErrorBoundary, AccessTreeProvider, PermissionSimulation } from "./components"; +import { BasePermissionEdge } from "./edges"; +import { useAccessTree } from "./hooks"; +import { FolderNode, RoleNode } from "./nodes"; +import { ViewMode } from "./types"; + +import "@xyflow/react/dist/style.css"; + +export type AccessTreeProps = { + permissions: MongoAbility; +}; + +const EdgeTypes = { base: BasePermissionEdge }; + +const NodeTypes = { role: RoleNode, folder: FolderNode }; + +const AccessTreeContent = ({ permissions }: AccessTreeProps) => { + const accessTreeData = useAccessTree(permissions); + const { edges, nodes, isLoading, viewMode, setViewMode } = accessTreeData; + + const { fitView, getViewport, setCenter } = useReactFlow(); + + const onNodeClick: NodeMouseHandler = useCallback( + (_, node) => { + setCenter( + node.position.x + (node.width ? node.width / 2 : 0), + node.position.y + (node.height ? node.height / 2 + 50 : 50), + { duration: 1000, zoom: 1 } + ); + }, + [setCenter] + ); + + useEffect(() => { + setTimeout(() => { + fitView({ + padding: 0.2, + duration: 1000, + maxZoom: 1 + }); + }, 5); + }, [fitView, nodes, edges, getViewport()]); + + const handleToggleModalView = () => + setViewMode((prev) => (prev === ViewMode.Modal ? ViewMode.Docked : ViewMode.Modal)); + + const handleToggleUndockedView = () => + setViewMode((prev) => (prev === ViewMode.Undocked ? ViewMode.Docked : ViewMode.Undocked)); + + const undockButtonLabel = `${viewMode === ViewMode.Undocked ? "Dock" : "Undock"} View`; + const windowButtonLabel = `${viewMode === ViewMode.Modal ? "Dock" : "Expand"} View`; + + return ( +
    +
    + {viewMode === ViewMode.Docked && ( +
    +
    +

    Access Tree

    +

    + Visual access policies for the configured role. +

    +
    +
    + + +
    +
    + )} +
    +
    + + {isLoading && ( + + + + )} + {viewMode !== ViewMode.Docked && ( + + + + + + + + + + + + + )} + + + + +
    +
    +
    +
    + ); +}; + +export const AccessTree = (props: AccessTreeProps) => { + return ( + + + + + + + + ); +}; diff --git a/frontend/src/components/permissions/AccessTree/components/AccessTreeContext.tsx b/frontend/src/components/permissions/AccessTree/components/AccessTreeContext.tsx new file mode 100644 index 000000000..fbac4a85e --- /dev/null +++ b/frontend/src/components/permissions/AccessTree/components/AccessTreeContext.tsx @@ -0,0 +1,51 @@ +import React, { + createContext, + Dispatch, + ReactNode, + SetStateAction, + useContext, + useMemo, + useState +} from "react"; + +import { ViewMode } from "../types"; + +export interface AccessTreeContextProps { + secretName: string; + setSecretName: Dispatch>; + viewMode: ViewMode; + setViewMode: Dispatch>; +} + +const AccessTreeContext = createContext(undefined); + +interface AccessTreeProviderProps { + children: ReactNode; +} + +export const AccessTreeProvider: React.FC = ({ children }) => { + const [secretName, setSecretName] = useState("*"); + const [viewMode, setViewMode] = useState(ViewMode.Docked); + + const value = useMemo( + () => ({ + secretName, + setSecretName, + viewMode, + setViewMode + }), + [secretName, setSecretName, viewMode, setViewMode] + ); + + return {children}; +}; + +export const useAccessTreeContext = (): AccessTreeContextProps => { + const context = useContext(AccessTreeContext); + + if (!context) { + throw new Error("useAccessTreeContext must be used within a AccessTreeProvider"); + } + + return context; +}; diff --git a/frontend/src/components/permissions/AccessTree/components/AccessTreeErrorBoundary.tsx b/frontend/src/components/permissions/AccessTree/components/AccessTreeErrorBoundary.tsx new file mode 100644 index 000000000..c30eb09cb --- /dev/null +++ b/frontend/src/components/permissions/AccessTree/components/AccessTreeErrorBoundary.tsx @@ -0,0 +1,105 @@ +import React, { ErrorInfo, ReactNode } from "react"; +import { MongoAbility, MongoQuery } from "@casl/ability"; +import { faCheck, faCopy, faExclamationTriangle } from "@fortawesome/free-solid-svg-icons"; +import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; + +import { IconButton } from "@app/components/v2"; +import { SessionStorageKeys } from "@app/const"; +import { ProjectPermissionSet } from "@app/context/ProjectPermissionContext"; +import { useTimedReset } from "@app/hooks"; + +interface ErrorBoundaryProps { + children: ReactNode; + permissions: MongoAbility; +} + +interface ErrorBoundaryState { + hasError: boolean; + error: Error | null; +} + +const ErrorDisplay = ({ + error, + permissions +}: { + error: Error | null; + permissions: MongoAbility; +}) => { + const display = JSON.stringify({ errorMessage: error?.message, permissions }, null, 2); + + const [isCopied, , setIsCopied] = useTimedReset({ + initialState: false + }); + + const copyToClipboard = () => { + navigator.clipboard.writeText(display); + setIsCopied(true); + sessionStorage.removeItem(SessionStorageKeys.CLI_TERMINAL_TOKEN); + }; + + return ( +
    + +
    +
    +          {display}
    +        
    + + + +
    +
    + ); +}; + +class ErrorBoundary extends React.Component { + constructor(props: ErrorBoundaryProps) { + super(props); + this.state = { + hasError: false, + error: null + }; + } + + static getDerivedStateFromError(error: Error): ErrorBoundaryState { + return { hasError: true, error }; + } + + componentDidCatch(error: Error, errorInfo: ErrorInfo): void { + console.error("Error caught by ErrorBoundary:", error, errorInfo, this.props); + } + + render(): ReactNode { + const { hasError, error } = this.state; + const { children, permissions } = this.props; + + if (hasError) { + return ; + } + return children; + } +} + +export const AccessTreeErrorBoundary = ({ children, permissions }: ErrorBoundaryProps) => { + return {children}; +}; diff --git a/frontend/src/components/permissions/AccessTree/components/PermissionSimulation.tsx b/frontend/src/components/permissions/AccessTree/components/PermissionSimulation.tsx new file mode 100644 index 000000000..8bc3b1b9d --- /dev/null +++ b/frontend/src/components/permissions/AccessTree/components/PermissionSimulation.tsx @@ -0,0 +1,137 @@ +import { Dispatch, SetStateAction, useState } from "react"; +import { faChevronDown, faChevronUp } from "@fortawesome/free-solid-svg-icons"; +import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; +import { Panel } from "@xyflow/react"; + +import { Button, FormLabel, IconButton, Input, Select, SelectItem } from "@app/components/v2"; +import { ProjectPermissionSub } from "@app/context"; + +import { ViewMode } from "../types"; + +type TProps = { + secretName: string; + setSecretName: Dispatch>; + viewMode: ViewMode; + setViewMode: Dispatch>; + setEnvironment: Dispatch>; + environment: string; + subject: ProjectPermissionSub; + setSubject: Dispatch>; + environments: { name: string; slug: string }[]; +}; + +export const PermissionSimulation = ({ + setEnvironment, + environment, + subject, + setSubject, + environments, + setViewMode, + viewMode, + secretName, + setSecretName +}: TProps) => { + const [expand, setExpand] = useState(false); + + const handlePermissionSimulation = () => { + setExpand(true); + setViewMode(ViewMode.Modal); + }; + + if (viewMode !== ViewMode.Modal) + return ( + + + + ); + + return ( + +
    +
    +
    + Permission Simulation + { + e.stopPropagation(); + setExpand((prev) => !prev); + }} + > + + +
    + {expand && ( +

    + Evaluate conditional policies to see what permissions will be granted given a secret + name or tags +

    + )} +
    + {expand && ( + <> +
    + + +
    +
    + + +
    + {subject === ProjectPermissionSub.Secrets && ( +
    + + setSecretName(e.target.value)} /> +
    + )} + + )} +
    +
    + ); +}; diff --git a/frontend/src/components/permissions/AccessTree/components/index.ts b/frontend/src/components/permissions/AccessTree/components/index.ts new file mode 100644 index 000000000..cce3f73c4 --- /dev/null +++ b/frontend/src/components/permissions/AccessTree/components/index.ts @@ -0,0 +1,3 @@ +export * from "./AccessTreeContext"; +export * from "./AccessTreeErrorBoundary"; +export * from "./PermissionSimulation"; diff --git a/frontend/src/components/permissions/AccessTree/edges/BasePermissionEdge.tsx b/frontend/src/components/permissions/AccessTree/edges/BasePermissionEdge.tsx new file mode 100644 index 000000000..ee8b4b531 --- /dev/null +++ b/frontend/src/components/permissions/AccessTree/edges/BasePermissionEdge.tsx @@ -0,0 +1,34 @@ +import { BaseEdge, BaseEdgeProps, EdgeProps, getSmoothStepPath } from "@xyflow/react"; + +export const BasePermissionEdge = ({ + id, + sourceX, + sourceY, + targetX, + targetY, + markerStart, + markerEnd, + style +}: Omit & EdgeProps) => { + const [edgePath] = getSmoothStepPath({ + sourceX, + sourceY, + targetX, + targetY + }); + + return ( + + ); +}; diff --git a/frontend/src/components/permissions/AccessTree/edges/index.ts b/frontend/src/components/permissions/AccessTree/edges/index.ts new file mode 100644 index 000000000..566a0f3ee --- /dev/null +++ b/frontend/src/components/permissions/AccessTree/edges/index.ts @@ -0,0 +1 @@ +export * from "./BasePermissionEdge"; diff --git a/frontend/src/components/permissions/AccessTree/hooks/index.ts b/frontend/src/components/permissions/AccessTree/hooks/index.ts new file mode 100644 index 000000000..7de33cb14 --- /dev/null +++ b/frontend/src/components/permissions/AccessTree/hooks/index.ts @@ -0,0 +1,91 @@ +import { useEffect, useState } from "react"; +import { MongoAbility, MongoQuery } from "@casl/ability"; +import { Edge, Node, useEdgesState, useNodesState } from "@xyflow/react"; + +import { ProjectPermissionSub, useWorkspace } from "@app/context"; +import { ProjectPermissionSet } from "@app/context/ProjectPermissionContext"; +import { useListProjectEnvironmentsFolders } from "@app/hooks/api/secretFolders/queries"; + +import { useAccessTreeContext } from "../components"; +import { PermissionAccess } from "../types"; +import { + createBaseEdge, + createFolderNode, + createRoleNode, + getSubjectActionRuleMap, + positionElements +} from "../utils"; + +export const useAccessTree = (permissions: MongoAbility) => { + const { currentWorkspace } = useWorkspace(); + const { secretName, setSecretName, setViewMode, viewMode } = useAccessTreeContext(); + const [nodes, setNodes] = useNodesState([]); + const [edges, setEdges] = useEdgesState([]); + const [subject, setSubject] = useState(ProjectPermissionSub.Secrets); + const [environment, setEnvironment] = useState(currentWorkspace.environments[0].slug); + const { data: environmentsFolders, isPending } = useListProjectEnvironmentsFolders( + currentWorkspace.id + ); + + useEffect(() => { + if (!environmentsFolders || !permissions) return; + + const { folders, name } = environmentsFolders[environment]; + + const roleNode = createRoleNode({ + subject, + environment: name + }); + + const actionRuleMap = getSubjectActionRuleMap(subject, permissions); + + const folderNodes = folders.map((folder) => + createFolderNode({ + folder, + permissions, + environment, + subject, + secretName, + actionRuleMap + }) + ); + + const folderEdges = folderNodes.map(({ data: folder }) => { + const actions = Object.values(folder.actions); + + let access: PermissionAccess; + if (Object.values(actions).some((action) => action === PermissionAccess.Full)) { + access = PermissionAccess.Full; + } else if (Object.values(actions).some((action) => action === PermissionAccess.Partial)) { + access = PermissionAccess.Partial; + } else { + access = PermissionAccess.None; + } + + return createBaseEdge({ + source: folder.parentId ?? roleNode.id, + target: folder.id, + access + }); + }); + + const init = positionElements([roleNode, ...folderNodes], [...folderEdges]); + setNodes(init.nodes); + setEdges(init.edges); + }, [permissions, environmentsFolders, environment, subject, secretName, setNodes, setEdges]); + + return { + nodes, + edges, + subject, + environment, + setEnvironment, + setSubject, + isLoading: isPending, + environments: currentWorkspace.environments, + secretName, + setSecretName, + viewMode, + setViewMode + }; +}; diff --git a/frontend/src/components/permissions/AccessTree/index.ts b/frontend/src/components/permissions/AccessTree/index.ts new file mode 100644 index 000000000..b04249099 --- /dev/null +++ b/frontend/src/components/permissions/AccessTree/index.ts @@ -0,0 +1 @@ +export * from "./AccessTree"; diff --git a/frontend/src/components/permissions/AccessTree/nodes/FolderNode/FolderNode.tsx b/frontend/src/components/permissions/AccessTree/nodes/FolderNode/FolderNode.tsx new file mode 100644 index 000000000..2b049fc04 --- /dev/null +++ b/frontend/src/components/permissions/AccessTree/nodes/FolderNode/FolderNode.tsx @@ -0,0 +1,77 @@ +import { + faCheckCircle, + faCircleMinus, + faCircleXmark, + faFolder +} from "@fortawesome/free-solid-svg-icons"; +import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; +import { Handle, NodeProps, Position } from "@xyflow/react"; + +import { Tooltip } from "@app/components/v2"; + +import { PermissionAccess } from "../../types"; +import { createFolderNode, formatActionName } from "../../utils"; +import { FolderNodeTooltipContent } from "./components"; + +const AccessMap = { + [PermissionAccess.Full]: { className: "text-green", icon: faCheckCircle }, + [PermissionAccess.Partial]: { className: "text-yellow", icon: faCircleMinus }, + [PermissionAccess.None]: { className: "text-red", icon: faCircleXmark } +}; + +export const FolderNode = ({ + data +}: NodeProps & { data: ReturnType["data"] }) => { + const { name, actions, actionRuleMap, parentId, subject } = data; + + const hasMinimalAccess = Object.values(actions).some( + (action) => action === PermissionAccess.Full || action === PermissionAccess.Partial + ); + + return ( + <> + +
    +
    + + {parentId ? `/${name}` : "/"} +
    +
    + {Object.entries(actions).map(([action, access]) => { + const { className, icon } = AccessMap[access]; + + return ( + + } + > +
    + + {formatActionName(action)} +
    +
    + ); + })} +
    +
    + + + ); +}; diff --git a/frontend/src/components/permissions/AccessTree/nodes/FolderNode/components/FolderNodeTooltipContent.tsx b/frontend/src/components/permissions/AccessTree/nodes/FolderNode/components/FolderNodeTooltipContent.tsx new file mode 100644 index 000000000..807f7c0bf --- /dev/null +++ b/frontend/src/components/permissions/AccessTree/nodes/FolderNode/components/FolderNodeTooltipContent.tsx @@ -0,0 +1,131 @@ +import { ReactElement } from "react"; +import { faCheckCircle, faCircleMinus, faCircleXmark } from "@fortawesome/free-solid-svg-icons"; +import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; +import { NodeToolbar, Position } from "@xyflow/react"; + +import { + formatedConditionsOperatorNames, + PermissionConditionOperators +} from "@app/context/ProjectPermissionContext/types"; +import { camelCaseToSpaces } from "@app/lib/fn/string"; + +import { PermissionAccess } from "../../../types"; +import { createFolderNode, formatActionName } from "../../../utils"; + +type Props = { + action: string; + access: PermissionAccess; +} & Pick["data"], "actionRuleMap" | "subject">; + +export const FolderNodeTooltipContent = ({ action, access, actionRuleMap, subject }: Props) => { + let component: ReactElement; + + switch (access) { + case PermissionAccess.Full: + component = ( + <> +
    + + Full {formatActionName(action)} Permissions +
    +

    + Policy grants unconditional{" "} + + {formatActionName(action).toLowerCase()} + {" "} + permission for {subject.replaceAll("-", " ")} in this folder. +

    + + ); + break; + case PermissionAccess.Partial: + component = ( + <> +
    + + Conditional {formatActionName(action)} Permissions +
    +

    + Policy conditionally allows{" "} + + {formatActionName(action).toLowerCase()} + {" "} + permission for {subject.replaceAll("-", " ")} in this folder. +

    +
      + {actionRuleMap.map((ruleMap, index) => { + const rule = ruleMap[action]; + + if ( + !rule || + !rule.conditions || + (!rule.conditions.secretName && !rule.conditions.secretTags) + ) + return null; + + return ( +
    • + + {rule.inverted ? "Forbids" : "Allows"} + + when: + {Object.entries(rule.conditions).map(([key, condition]) => ( +
        + {Object.entries(condition as object).map(([operator, value]) => ( +
      • + + {camelCaseToSpaces(key)} + {" "} + + { + formatedConditionsOperatorNames[ + operator as PermissionConditionOperators + ] + } + {" "} + + {typeof value === "string" ? value : value.join(", ")} + + . +
      • + ))} +
      + ))} +
    • + ); + })} +
    + + ); + break; + case PermissionAccess.None: + component = ( + <> +
    + + No {formatActionName(action)} Permissions +
    +

    + Policy always forbids{" "} + + {formatActionName(action).toLowerCase()} + {" "} + permission for {subject.replaceAll("-", " ")} in this folder. +

    + + ); + break; + default: + throw new Error(`Unhandled access type: ${access}`); + } + + return ( + + {component} + + ); +}; diff --git a/frontend/src/components/permissions/AccessTree/nodes/FolderNode/components/index.ts b/frontend/src/components/permissions/AccessTree/nodes/FolderNode/components/index.ts new file mode 100644 index 000000000..79380317f --- /dev/null +++ b/frontend/src/components/permissions/AccessTree/nodes/FolderNode/components/index.ts @@ -0,0 +1 @@ +export * from "./FolderNodeTooltipContent"; diff --git a/frontend/src/components/permissions/AccessTree/nodes/FolderNode/index.ts b/frontend/src/components/permissions/AccessTree/nodes/FolderNode/index.ts new file mode 100644 index 000000000..c1c0583b8 --- /dev/null +++ b/frontend/src/components/permissions/AccessTree/nodes/FolderNode/index.ts @@ -0,0 +1 @@ +export * from "./FolderNode"; diff --git a/frontend/src/components/permissions/AccessTree/nodes/RoleNode.tsx b/frontend/src/components/permissions/AccessTree/nodes/RoleNode.tsx new file mode 100644 index 000000000..d38d896f2 --- /dev/null +++ b/frontend/src/components/permissions/AccessTree/nodes/RoleNode.tsx @@ -0,0 +1,30 @@ +import { Handle, NodeProps, Position } from "@xyflow/react"; + +import { createRoleNode } from "../utils"; + +export const RoleNode = ({ + data: { subject, environment } +}: NodeProps & { data: ReturnType["data"] }) => { + return ( + <> + +
    +
    + {subject.replace("-", " ")} Access +
    +

    {environment}

    +
    +
    +
    + + + ); +}; diff --git a/frontend/src/components/permissions/AccessTree/nodes/index.ts b/frontend/src/components/permissions/AccessTree/nodes/index.ts new file mode 100644 index 000000000..a67b58c18 --- /dev/null +++ b/frontend/src/components/permissions/AccessTree/nodes/index.ts @@ -0,0 +1,2 @@ +export * from "./FolderNode/FolderNode"; +export * from "./RoleNode"; diff --git a/frontend/src/components/permissions/AccessTree/types/index.ts b/frontend/src/components/permissions/AccessTree/types/index.ts new file mode 100644 index 000000000..9536d7f86 --- /dev/null +++ b/frontend/src/components/permissions/AccessTree/types/index.ts @@ -0,0 +1,21 @@ +export enum PermissionAccess { + Full = "full", + Partial = "partial", + None = "None" +} + +export enum PermissionNode { + Role = "role", + Folder = "folder", + Environment = "environment" +} + +export enum PermissionEdge { + Base = "base" +} + +export enum ViewMode { + Docked = "docked", + Modal = "modal", + Undocked = "undocked" +} diff --git a/frontend/src/components/permissions/AccessTree/utils/createBaseEdge.ts b/frontend/src/components/permissions/AccessTree/utils/createBaseEdge.ts new file mode 100644 index 000000000..cc53360c8 --- /dev/null +++ b/frontend/src/components/permissions/AccessTree/utils/createBaseEdge.ts @@ -0,0 +1,26 @@ +import { MarkerType } from "@xyflow/react"; + +import { PermissionAccess, PermissionEdge } from "../types"; + +export const createBaseEdge = ({ + source, + target, + access +}: { + source: string; + target: string; + access: PermissionAccess; +}) => { + const color = access === PermissionAccess.None ? "#707174" : "#ccccce"; + return { + id: `e-${source}-${target}`, + source, + target, + type: PermissionEdge.Base, + markerEnd: { + type: MarkerType.ArrowClosed, + color + }, + style: { stroke: color } + }; +}; diff --git a/frontend/src/components/permissions/AccessTree/utils/createFolderNode.ts b/frontend/src/components/permissions/AccessTree/utils/createFolderNode.ts new file mode 100644 index 000000000..3978bec31 --- /dev/null +++ b/frontend/src/components/permissions/AccessTree/utils/createFolderNode.ts @@ -0,0 +1,180 @@ +import { MongoAbility, MongoQuery, subject as abilitySubject } from "@casl/ability"; +import picomatch from "picomatch"; + +import { + ProjectPermissionActions, + ProjectPermissionDynamicSecretActions, + ProjectPermissionSet, + ProjectPermissionSub +} from "@app/context/ProjectPermissionContext"; +import { + PermissionConditionOperators, + ProjectPermissionSecretActions +} from "@app/context/ProjectPermissionContext/types"; +import { TSecretFolderWithPath } from "@app/hooks/api/secretFolders/types"; +import { hasSecretReadValueOrDescribePermission } from "@app/lib/fn/permission"; + +import { PermissionAccess, PermissionNode } from "../types"; +import { TActionRuleMap } from "."; + +const ACTION_MAP: Record = { + [ProjectPermissionSub.Secrets]: [ + ProjectPermissionSecretActions.DescribeSecret, + ProjectPermissionSecretActions.ReadValue, + ProjectPermissionSecretActions.Create, + ProjectPermissionSecretActions.Edit, + ProjectPermissionSecretActions.Delete + ], + [ProjectPermissionSub.DynamicSecrets]: Object.values(ProjectPermissionDynamicSecretActions), + [ProjectPermissionSub.SecretFolders]: [ + ProjectPermissionSecretActions.Create, + ProjectPermissionSecretActions.Edit, + ProjectPermissionSecretActions.Delete + ] +}; + +const evaluateCondition = ( + value: string, + operator: PermissionConditionOperators, + comparison: string | string[] +) => { + switch (operator) { + case PermissionConditionOperators.$EQ: + return value === comparison; + case PermissionConditionOperators.$NEQ: + return value !== comparison; + case PermissionConditionOperators.$GLOB: + return picomatch.isMatch(value, comparison); + case PermissionConditionOperators.$IN: + return (comparison as string[]).map((v: string) => v.trim()).includes(value); + default: + throw new Error(`Unhandled operator: ${operator}`); + } +}; + +export const createFolderNode = ({ + folder, + permissions, + environment, + subject, + secretName, + actionRuleMap +}: { + folder: TSecretFolderWithPath; + permissions: MongoAbility; + environment: string; + subject: ProjectPermissionSub; + secretName: string; + actionRuleMap: TActionRuleMap; +}) => { + const actions = Object.fromEntries( + Object.values(ACTION_MAP[subject] ?? Object.values(ProjectPermissionActions)).map((action) => { + let access: PermissionAccess; + + // wrapped in try because while editing certain conditions, if their values are empty it throws an error + try { + let hasPermission: boolean; + + const subjectFields = { + secretPath: folder.path, + environment, + secretName: secretName || "*", + secretTags: ["*"] + }; + + if ( + subject === ProjectPermissionSub.Secrets && + (action === ProjectPermissionSecretActions.ReadValue || + action === ProjectPermissionSecretActions.DescribeSecret) + ) { + hasPermission = hasSecretReadValueOrDescribePermission( + permissions, + action, + subjectFields + ); + } else { + hasPermission = permissions.can( + // @ts-expect-error we are not specifying which so can't resolve if valid + action, + abilitySubject(subject, subjectFields) + ); + } + + if (hasPermission) { + // we want to show yellow/conditional access if user hasn't specified secret name to fully resolve access + if ( + (!secretName || secretName === "*") && + actionRuleMap.some((el) => { + // we only show conditional if secretName/secretTags are present - environment and path can be directly determined + if (!el[action]?.conditions?.secretName && !el[action]?.conditions?.secretTags) + return false; + + // make sure condition applies to env + if (el[action]?.conditions?.environment) { + if ( + !Object.entries(el[action]?.conditions?.environment).every(([operator, value]) => + evaluateCondition(environment, operator as PermissionConditionOperators, value) + ) + ) { + return false; + } + } + + // and applies to path + if (el[action]?.conditions?.secretPath) { + if ( + !Object.entries(el[action]?.conditions?.secretPath).every(([operator, value]) => + evaluateCondition(folder.path, operator as PermissionConditionOperators, value) + ) + ) { + return false; + } + } + + return true; + }) + ) { + access = PermissionAccess.Partial; + } else { + access = PermissionAccess.Full; + } + } else { + access = PermissionAccess.None; + } + } catch (e) { + console.error(e); + access = PermissionAccess.None; + } + + return [action, access]; + }) + ); + + let height = 84; + + switch (subject) { + case ProjectPermissionSub.DynamicSecrets: + height = 130; + break; + case ProjectPermissionSub.Secrets: + height = 85; + break; + default: + height = 64; + } + + return { + type: PermissionNode.Folder, + id: folder.id, + data: { + ...folder, + actions, + environment, + actionRuleMap, + subject + }, + position: { x: 0, y: 0 }, + width: 264, + height + }; +}; diff --git a/frontend/src/components/permissions/AccessTree/utils/createRoleNode.ts b/frontend/src/components/permissions/AccessTree/utils/createRoleNode.ts new file mode 100644 index 000000000..354a69482 --- /dev/null +++ b/frontend/src/components/permissions/AccessTree/utils/createRoleNode.ts @@ -0,0 +1,19 @@ +import { PermissionNode } from "../types"; + +export const createRoleNode = ({ + subject, + environment +}: { + subject: string; + environment: string; +}) => ({ + id: `role-${subject}-${environment}`, + position: { x: 0, y: 0 }, + data: { + subject, + environment + }, + type: PermissionNode.Role, + height: 48, + width: 264 +}); diff --git a/frontend/src/components/permissions/AccessTree/utils/formatActionName.ts b/frontend/src/components/permissions/AccessTree/utils/formatActionName.ts new file mode 100644 index 000000000..c89adea8b --- /dev/null +++ b/frontend/src/components/permissions/AccessTree/utils/formatActionName.ts @@ -0,0 +1,3 @@ +import { camelCaseToSpaces } from "@app/lib/fn/string"; + +export const formatActionName = (action: string) => camelCaseToSpaces(action.replaceAll("-", " ")); diff --git a/frontend/src/components/permissions/AccessTree/utils/getActionRuleMap.ts b/frontend/src/components/permissions/AccessTree/utils/getActionRuleMap.ts new file mode 100644 index 000000000..40b723d22 --- /dev/null +++ b/frontend/src/components/permissions/AccessTree/utils/getActionRuleMap.ts @@ -0,0 +1,27 @@ +import { MongoAbility, MongoQuery } from "@casl/ability"; + +import { ProjectPermissionSet, ProjectPermissionSub } from "@app/context/ProjectPermissionContext"; + +export type TActionRuleMap = ReturnType; + +export const getSubjectActionRuleMap = ( + subject: ProjectPermissionSub, + permissions: MongoAbility +) => { + const rules = permissions.rules.filter((rule) => { + const ruleSubject = typeof rule.subject === "string" ? rule.subject : rule.subject[0]; + + return ruleSubject === subject; + }); + + const actionRuleMap: Record[] = []; + rules.forEach((rule) => { + if (typeof rule.action === "string") { + actionRuleMap.push({ [rule.action]: rule }); + } else { + actionRuleMap.push(Object.fromEntries(rule.action.map((action) => [action, rule]))); + } + }); + + return actionRuleMap; +}; diff --git a/frontend/src/components/permissions/AccessTree/utils/index.ts b/frontend/src/components/permissions/AccessTree/utils/index.ts new file mode 100644 index 000000000..88e8fcceb --- /dev/null +++ b/frontend/src/components/permissions/AccessTree/utils/index.ts @@ -0,0 +1,6 @@ +export * from "./createBaseEdge"; +export * from "./createFolderNode"; +export * from "./createRoleNode"; +export * from "./formatActionName"; +export * from "./getActionRuleMap"; +export * from "./positionElements"; diff --git a/frontend/src/components/permissions/AccessTree/utils/positionElements.ts b/frontend/src/components/permissions/AccessTree/utils/positionElements.ts new file mode 100644 index 000000000..523b402d1 --- /dev/null +++ b/frontend/src/components/permissions/AccessTree/utils/positionElements.ts @@ -0,0 +1,28 @@ +import Dagre from "@dagrejs/dagre"; +import { Edge, Node } from "@xyflow/react"; + +export const positionElements = (nodes: Node[], edges: Edge[]) => { + const dagre = new Dagre.graphlib.Graph({ directed: true }) + .setDefaultEdgeLabel(() => ({})) + .setGraph({ rankdir: "TB" }); + + edges.forEach((edge) => dagre.setEdge(edge.source, edge.target)); + nodes.forEach((node) => dagre.setNode(node.id, node)); + + Dagre.layout(dagre, {}); + + return { + nodes: nodes.map((node) => { + const { x, y } = dagre.node(node.id); + + return { + ...node, + position: { + x: x - (node.width ? node.width / 2 : 0), + y: y - (node.height ? node.height / 2 : 0) + } + }; + }), + edges + }; +}; diff --git a/frontend/src/components/permissions/index.tsx b/frontend/src/components/permissions/index.tsx index 5103b0f73..c40079a4f 100644 --- a/frontend/src/components/permissions/index.tsx +++ b/frontend/src/components/permissions/index.tsx @@ -1,3 +1,4 @@ +export * from "./AccessTree"; export { GlobPermissionInfo } from "./GlobPermissionInfo"; export { OrgPermissionCan } from "./OrgPermissionCan"; export { PermissionDeniedBanner } from "./PermissionDeniedBanner"; diff --git a/frontend/src/components/v2/Select/Select.tsx b/frontend/src/components/v2/Select/Select.tsx index 7259c8157..e0dc90186 100644 --- a/frontend/src/components/v2/Select/Select.tsx +++ b/frontend/src/components/v2/Select/Select.tsx @@ -59,7 +59,9 @@ export const Select = forwardRef( >
    {props.icon && } - +
    + +
    @@ -122,7 +124,7 @@ export const SelectItem = forwardRef( { staleTime: Infinity, select: (data) => { const rule = unpackRules>>(data.permissions); - const negatedRules = groupBy( - rule.filter((i) => i.inverted && i.conditions), - (i) => `${i.subject}-${JSON.stringify(i.conditions)}` - ); - const ability = createMongoAbility(rule, { - // this allows in frontend to skip some rules using * - conditionsMatcher: (rules) => { - return (entity) => { - // skip validation if its negated rules - const isNegatedRule = - // eslint-disable-next-line no-underscore-dangle - negatedRules?.[`${entity.__caslSubjectType__}-${JSON.stringify(rules)}`]; - if (isNegatedRule) { - const baseMatcher = conditionsMatcher(rules); - return baseMatcher(entity); - } - - const rulesStrippedOfWildcard = omit( - rules, - Object.keys(entity).filter((el) => entity[el]?.includes("*")) - ); - const baseMatcher = conditionsMatcher(rulesStrippedOfWildcard); - return baseMatcher(entity); - }; - } - }); - + const ability = evaluatePermissionsAbility(rule); return { permission: ability, membership: { diff --git a/frontend/src/helpers/permissions.ts b/frontend/src/helpers/permissions.ts new file mode 100644 index 000000000..2f07ee05c --- /dev/null +++ b/frontend/src/helpers/permissions.ts @@ -0,0 +1,39 @@ +import { createMongoAbility, MongoAbility, MongoQuery, RawRuleOf } from "@casl/ability"; + +import { ProjectPermissionSet } from "@app/context/ProjectPermissionContext"; +import { conditionsMatcher } from "@app/hooks/api/roles/queries"; +import { groupBy } from "@app/lib/fn/array"; +import { omit } from "@app/lib/fn/object"; + +export const evaluatePermissionsAbility = ( + rule: RawRuleOf>[] +) => { + const negatedRules = groupBy( + rule.filter((i) => i.inverted && i.conditions), + (i) => `${i.subject}-${JSON.stringify(i.conditions)}` + ); + const ability = createMongoAbility(rule, { + // this allows in frontend to skip some rules using * + conditionsMatcher: (rules) => { + return (entity) => { + // skip validation if its negated rules + const isNegatedRule = + // eslint-disable-next-line no-underscore-dangle + negatedRules?.[`${entity.__caslSubjectType__}-${JSON.stringify(rules)}`]; + if (isNegatedRule) { + const baseMatcher = conditionsMatcher(rules); + return baseMatcher(entity); + } + + const rulesStrippedOfWildcard = omit( + rules, + Object.keys(entity).filter((el) => entity[el]?.includes("*")) + ); + const baseMatcher = conditionsMatcher(rulesStrippedOfWildcard); + return baseMatcher(entity); + }; + } + }); + + return ability; +}; diff --git a/frontend/src/hooks/api/admin/index.ts b/frontend/src/hooks/api/admin/index.ts index 4d9f05bb1..5eb6c6732 100644 --- a/frontend/src/hooks/api/admin/index.ts +++ b/frontend/src/hooks/api/admin/index.ts @@ -1,10 +1,10 @@ export { useAdminDeleteUser, + useAdminGrantServerAdminAccess, useCreateAdminUser, useUpdateAdminSlackConfig, useUpdateServerConfig, - useUpdateServerEncryptionStrategy, - useAdminGrantServerAdminAccess + useUpdateServerEncryptionStrategy } from "./mutation"; export { useAdminGetUsers, diff --git a/frontend/src/hooks/api/secretFolders/queries.tsx b/frontend/src/hooks/api/secretFolders/queries.tsx index 10f835da8..afc71434d 100644 --- a/frontend/src/hooks/api/secretFolders/queries.tsx +++ b/frontend/src/hooks/api/secretFolders/queries.tsx @@ -16,6 +16,7 @@ import { TDeleteFolderDTO, TGetFoldersByEnvDTO, TGetProjectFoldersDTO, + TProjectEnvironmentsFolders, TSecretFolder, TUpdateFolderBatchDTO, TUpdateFolderDTO @@ -23,7 +24,9 @@ import { export const folderQueryKeys = { getSecretFolders: ({ projectId, environment, path }: TGetProjectFoldersDTO) => - ["secret-folders", { projectId, environment, path }] as const + ["secret-folders", { projectId, environment, path }] as const, + getProjectEnvironmentsFolders: (projectId: string) => + ["secret-folders", "environment", projectId] as const }; const fetchProjectFolders = async (workspaceId: string, environment: string, path = "/") => { @@ -37,6 +40,29 @@ const fetchProjectFolders = async (workspaceId: string, environment: string, pat return data.folders; }; +export const useListProjectEnvironmentsFolders = ( + projectId: string, + options?: Omit< + UseQueryOptions< + TProjectEnvironmentsFolders, + unknown, + TProjectEnvironmentsFolders, + ReturnType + >, + "queryKey" | "queryFn" + > +) => + useQuery({ + queryKey: folderQueryKeys.getProjectEnvironmentsFolders(projectId), + queryFn: async () => { + const { data } = await apiRequest.get( + `/api/v1/workspace/${projectId}/folders/project-environments` + ); + return data; + }, + ...options + }); + export const useGetProjectFolders = ({ projectId, environment, diff --git a/frontend/src/hooks/api/secretFolders/types.ts b/frontend/src/hooks/api/secretFolders/types.ts index 454e159e1..33653ff72 100644 --- a/frontend/src/hooks/api/secretFolders/types.ts +++ b/frontend/src/hooks/api/secretFolders/types.ts @@ -1,3 +1,5 @@ +import { WorkspaceEnv } from "@app/hooks/api/workspace/types"; + export enum ReservedFolders { SecretReplication = "__reserve_replication_" } @@ -6,6 +8,13 @@ export type TSecretFolder = { id: string; name: string; description?: string; + parentId?: string | null; +}; + +export type TSecretFolderWithPath = TSecretFolder & { path: string }; + +export type TProjectEnvironmentsFolders = { + [key: string]: WorkspaceEnv & { folders: TSecretFolderWithPath[] }; }; export type TGetProjectFoldersDTO = { diff --git a/frontend/src/hooks/utils/secrets-overview.tsx b/frontend/src/hooks/utils/secrets-overview.tsx index a3e5f8faf..89fc679c4 100644 --- a/frontend/src/hooks/utils/secrets-overview.tsx +++ b/frontend/src/hooks/utils/secrets-overview.tsx @@ -10,13 +10,16 @@ type FolderNameAndDescription = { export const useFolderOverview = (folders: DashboardProjectSecretsOverview["folders"]) => { const folderNamesAndDescriptions = useMemo(() => { const namesAndDescriptions = new Map(); - + folders?.forEach((folder) => { if (!namesAndDescriptions.has(folder.name)) { - namesAndDescriptions.set(folder.name, { name: folder.name, description: folder.description }); + namesAndDescriptions.set(folder.name, { + name: folder.name, + description: folder.description + }); } }); - + return Array.from(namesAndDescriptions.values()); }, [folders]); diff --git a/frontend/src/layouts/OrganizationLayout/components/MenuIconButton/MenuIconButton.tsx b/frontend/src/layouts/OrganizationLayout/components/MenuIconButton/MenuIconButton.tsx index 7da236ef4..99d24ece6 100644 --- a/frontend/src/layouts/OrganizationLayout/components/MenuIconButton/MenuIconButton.tsx +++ b/frontend/src/layouts/OrganizationLayout/components/MenuIconButton/MenuIconButton.tsx @@ -25,8 +25,8 @@ export const MenuIconButton = ({ type="button" role="menuitem" className={twMerge( - "group relative flex w-full cursor-pointer flex-col items-center justify-center rounded my-1 p-2 font-inter text-sm text-bunker-100 transition-all duration-150 hover:bg-mineshaft-700", - isSelected && "bg-bunker-800 hover:bg-mineshaft-600 rounded-none", + "group relative my-1 flex w-full cursor-pointer flex-col items-center justify-center rounded p-2 font-inter text-sm text-bunker-100 transition-all duration-150 hover:bg-mineshaft-700", + isSelected && "rounded-none bg-bunker-800 hover:bg-mineshaft-600", isDisabled && "cursor-not-allowed hover:bg-transparent", className )} diff --git a/frontend/src/pages/organization/AuditLogsPage/components/LogsTable.tsx b/frontend/src/pages/organization/AuditLogsPage/components/LogsTable.tsx index a931cb606..4b3f2b270 100644 --- a/frontend/src/pages/organization/AuditLogsPage/components/LogsTable.tsx +++ b/frontend/src/pages/organization/AuditLogsPage/components/LogsTable.tsx @@ -28,7 +28,14 @@ type Props = { const AUDIT_LOG_LIMIT = 15; -const TABLE_HEADERS = ["Timestamp (MM/DD/YYYY)", "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 = ({ diff --git a/frontend/src/pages/project/RoleDetailsBySlugPage/components/RolePermissionsSection.tsx b/frontend/src/pages/project/RoleDetailsBySlugPage/components/RolePermissionsSection.tsx index 716b6ba5e..d44927edf 100644 --- a/frontend/src/pages/project/RoleDetailsBySlugPage/components/RolePermissionsSection.tsx +++ b/frontend/src/pages/project/RoleDetailsBySlugPage/components/RolePermissionsSection.tsx @@ -1,10 +1,13 @@ +import { useMemo } from "react"; import { FormProvider, useForm } from "react-hook-form"; +import { MongoAbility, MongoQuery, RawRuleOf } from "@casl/ability"; import { faPlus, faSave } from "@fortawesome/free-solid-svg-icons"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { zodResolver } from "@hookform/resolvers/zod"; import { twMerge } from "tailwind-merge"; import { createNotification } from "@app/components/notifications"; +import { AccessTree } from "@app/components/permissions"; import { Button, DropdownMenu, @@ -13,6 +16,8 @@ import { DropdownMenuTrigger } from "@app/components/v2"; import { ProjectPermissionSub, useWorkspace } from "@app/context"; +import { ProjectPermissionSet } from "@app/context/ProjectPermissionContext"; +import { evaluatePermissionsAbility } from "@app/helpers/permissions"; import { useGetProjectRoleBySlug, useUpdateProjectRole } from "@app/hooks/api"; import { GeneralPermissionConditions } from "./GeneralPermissionConditions"; @@ -115,94 +120,109 @@ export const RolePermissionsSection = ({ roleSlug, isDisabled }: Props) => { } }; + const permissions = form.watch("permissions"); + + const formattedPermissions = useMemo( + () => + evaluatePermissionsAbility( + formRolePermission2API(permissions) as RawRuleOf< + MongoAbility + >[] + ), + [JSON.stringify(permissions)] + ); + return ( -
    - -
    -

    Policies

    -
    - {isCustomRole && ( - <> - {isDirty && ( - - )} -
    - - - - - - - {Object.keys(PROJECT_PERMISSION_OBJECT) - .sort((a, b) => - PROJECT_PERMISSION_OBJECT[ - a as keyof typeof PROJECT_PERMISSION_OBJECT - ].title - .toLowerCase() - .localeCompare( - PROJECT_PERMISSION_OBJECT[ - b as keyof typeof PROJECT_PERMISSION_OBJECT - ].title.toLowerCase() - ) - ) - .map((subject) => ( - onNewPolicy(subject as ProjectPermissionSub)} - > - {PROJECT_PERMISSION_OBJECT[subject as ProjectPermissionSub].title} - - ))} - - -
    - - )} +
    + + + +
    +

    Policies

    +
    + {isCustomRole && ( + <> + {isDirty && ( + + )} +
    + + + + + + + {Object.keys(PROJECT_PERMISSION_OBJECT) + .sort((a, b) => + PROJECT_PERMISSION_OBJECT[ + a as keyof typeof PROJECT_PERMISSION_OBJECT + ].title + .toLowerCase() + .localeCompare( + PROJECT_PERMISSION_OBJECT[ + b as keyof typeof PROJECT_PERMISSION_OBJECT + ].title.toLowerCase() + ) + ) + .map((subject) => ( + onNewPolicy(subject as ProjectPermissionSub)} + > + {PROJECT_PERMISSION_OBJECT[subject as ProjectPermissionSub].title} + + ))} + + +
    + + )} +
    -
    -
    - {!isPending && } - {(Object.keys(PROJECT_PERMISSION_OBJECT) as ProjectPermissionSub[]).map((subject) => ( - - {renderConditionalComponents(subject, isDisabled)} - - ))} -
    - - +
    + {!isPending && } + {(Object.keys(PROJECT_PERMISSION_OBJECT) as ProjectPermissionSub[]).map((subject) => ( + + {renderConditionalComponents(subject, isDisabled)} + + ))} +
    + + +
    ); }; diff --git a/frontend/src/pages/public/ShareSecretPage/components/ShareSecretForm.tsx b/frontend/src/pages/public/ShareSecretPage/components/ShareSecretForm.tsx index 25866778e..8e3d34b5a 100644 --- a/frontend/src/pages/public/ShareSecretPage/components/ShareSecretForm.tsx +++ b/frontend/src/pages/public/ShareSecretPage/components/ShareSecretForm.tsx @@ -120,7 +120,14 @@ export const ShareSecretForm = ({ isPublic, value }: Props) => { isError={Boolean(error)} errorText={error?.message} > - + )} /> @@ -155,7 +162,16 @@ export const ShareSecretForm = ({ isPublic, value }: Props) => { errorText={error?.message} isOptional > - + )} /> diff --git a/frontend/src/pages/secret-manager/OverviewPage/OverviewPage.tsx b/frontend/src/pages/secret-manager/OverviewPage/OverviewPage.tsx index f96aabf53..78a2380d0 100644 --- a/frontend/src/pages/secret-manager/OverviewPage/OverviewPage.tsx +++ b/frontend/src/pages/secret-manager/OverviewPage/OverviewPage.tsx @@ -228,7 +228,8 @@ export const OverviewPage = () => { setPage }); - const { folderNamesAndDescriptions, getFolderByNameAndEnv, isFolderPresentInEnv } = useFolderOverview(folders); + const { folderNamesAndDescriptions, getFolderByNameAndEnv, isFolderPresentInEnv } = + useFolderOverview(folders); const { dynamicSecretNames, isDynamicSecretPresentInEnv } = useDynamicSecretOverview(dynamicSecrets); @@ -251,7 +252,7 @@ export const OverviewPage = () => { "updateFolder" ] as const); - const handleFolderCreate = async (folderName: string, description: string | null) => { + const handleFolderCreate = async (folderName: string, description: string | null) => { const promises = userAvailableEnvs.map((env) => { const environment = env.slug; return createFolder({ @@ -1029,7 +1030,7 @@ export const OverviewPage = () => { )} {!isOverviewLoading && visibleEnvs.length > 0 && ( <> - {folderNamesAndDescriptions.map(({name: folderName, description}, index) => ( + {folderNamesAndDescriptions.map(({ name: folderName, description }, index) => ( { )?.name} - defaultDescription={(popUp.updateFolder?.data as Pick)?.description} + defaultDescription={ + (popUp.updateFolder?.data as Pick)?.description + } onUpdateFolder={handleFolderUpdate} showDescriptionOverwriteWarning /> 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 4847feb69..7248bc19f 100644 --- a/frontend/src/pages/secret-manager/OverviewPage/components/CreateSecretForm/CreateSecretForm.tsx +++ b/frontend/src/pages/secret-manager/OverviewPage/components/CreateSecretForm/CreateSecretForm.tsx @@ -16,10 +16,10 @@ import { useProjectPermission, useWorkspace } from "@app/context"; +import { ProjectPermissionSecretActions } from "@app/context/ProjectPermissionContext/types"; 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({ diff --git a/frontend/src/pages/secret-manager/SecretDashboardPage/components/ActionBar/CreateDynamicSecretForm/SqlDatabaseInputForm.tsx b/frontend/src/pages/secret-manager/SecretDashboardPage/components/ActionBar/CreateDynamicSecretForm/SqlDatabaseInputForm.tsx index 063b2d08b..4af92b210 100644 --- a/frontend/src/pages/secret-manager/SecretDashboardPage/components/ActionBar/CreateDynamicSecretForm/SqlDatabaseInputForm.tsx +++ b/frontend/src/pages/secret-manager/SecretDashboardPage/components/ActionBar/CreateDynamicSecretForm/SqlDatabaseInputForm.tsx @@ -23,22 +23,26 @@ import { useWorkspace } from "@app/context"; import { gatewaysQueryKeys, useCreateDynamicSecret } from "@app/hooks/api"; import { DynamicSecretProviders, SqlProviders } from "@app/hooks/api/dynamicSecret/types"; -const passwordRequirementsSchema = z.object({ - length: z.number().min(1).max(250), - required: z.object({ - lowercase: z.number().min(0), - uppercase: z.number().min(0), - digits: z.number().min(0), - symbols: z.number().min(0) - }).refine((data) => { - const total = Object.values(data).reduce((sum, count) => sum + count, 0); - return total <= 250; - }, "Sum of required characters cannot exceed 250"), - allowedSymbols: z.string().optional() -}).refine((data) => { - const total = Object.values(data.required).reduce((sum, count) => sum + count, 0); - return total <= data.length; -}, "Sum of required characters cannot exceed the total length"); +const passwordRequirementsSchema = z + .object({ + length: z.number().min(1).max(250), + required: z + .object({ + lowercase: z.number().min(0), + uppercase: z.number().min(0), + digits: z.number().min(0), + symbols: z.number().min(0) + }) + .refine((data) => { + const total = Object.values(data).reduce((sum, count) => sum + count, 0); + return total <= 250; + }, "Sum of required characters cannot exceed 250"), + allowedSymbols: z.string().optional() + }) + .refine((data) => { + const total = Object.values(data.required).reduce((sum, count) => sum + count, 0); + return total <= data.length; + }, "Sum of required characters cannot exceed the total length"); const formSchema = z.object({ provider: z.object({ @@ -166,7 +170,7 @@ export const SqlDatabaseInputForm = ({ digits: 1, symbols: 0 }, - allowedSymbols: '-_.~!*' + allowedSymbols: "-_.~!*" } } } @@ -205,7 +209,7 @@ export const SqlDatabaseInputForm = ({ setValue("provider.renewStatement", sqlStatment.renewStatement); setValue("provider.revocationStatement", sqlStatment.revocationStatement); setValue("provider.port", getDefaultPort(type)); - + // Update password requirements based on provider const length = type === SqlProviders.Oracle ? 30 : 48; setValue("provider.passwordRequirements.length", length); @@ -424,7 +428,9 @@ export const SqlDatabaseInputForm = ({ /> - Creation, Revocation & Renew Statements (optional) + + Creation, Revocation & Renew Statements (optional) +
    Customize SQL statements for managing database user lifecycle @@ -508,10 +514,10 @@ export const SqlDatabaseInputForm = ({ isError={Boolean(error)} errorText={error?.message} > - field.onChange(Number(e.target.value))} /> @@ -519,17 +525,20 @@ export const SqlDatabaseInputForm = ({ )} />
    - +

    Minimum Required Character Counts

    {(() => { - const total = Object.values(watch("provider.passwordRequirements.required") || {}).reduce((sum, count) => sum + Number(count || 0), 0); + const total = Object.values( + watch("provider.passwordRequirements.required") || {} + ).reduce((sum, count) => sum + Number(count || 0), 0); const length = watch("provider.passwordRequirements.length") || 0; const isError = total > length; return ( - Total required characters: {total} {isError ? `(exceeds length of ${length})` : ""} + Total required characters: {total}{" "} + {isError ? `(exceeds length of ${length})` : ""} ); })()} @@ -546,9 +555,9 @@ export const SqlDatabaseInputForm = ({ errorText={error?.message} helperText="Minimum number of lowercase letters" > - field.onChange(Number(e.target.value))} /> @@ -566,9 +575,9 @@ export const SqlDatabaseInputForm = ({ errorText={error?.message} helperText="Minimum number of uppercase letters" > - field.onChange(Number(e.target.value))} /> @@ -586,9 +595,9 @@ export const SqlDatabaseInputForm = ({ errorText={error?.message} helperText="Minimum number of digits" > - field.onChange(Number(e.target.value))} /> @@ -606,9 +615,9 @@ export const SqlDatabaseInputForm = ({ errorText={error?.message} helperText="Minimum number of symbols" > - field.onChange(Number(e.target.value))} /> diff --git a/frontend/src/pages/secret-manager/SecretDashboardPage/components/ActionBar/FolderForm.tsx b/frontend/src/pages/secret-manager/SecretDashboardPage/components/ActionBar/FolderForm.tsx index 78d075b4f..885838498 100644 --- a/frontend/src/pages/secret-manager/SecretDashboardPage/components/ActionBar/FolderForm.tsx +++ b/frontend/src/pages/secret-manager/SecretDashboardPage/components/ActionBar/FolderForm.tsx @@ -15,7 +15,8 @@ type Props = { showDescriptionOverwriteWarning?: boolean; }; -const descriptionOverwriteWarningMessage = "Warning: Any changes made here will overwrite any custom edits in individual environment folders." +const descriptionOverwriteWarningMessage = + "Warning: Any changes made here will overwrite any custom edits in individual environment folders."; const formSchema = z.object({ name: z @@ -25,9 +26,7 @@ const formSchema = z.object({ /^[a-zA-Z0-9-_]+$/, "Folder name can only contain letters, numbers, dashes, and underscores" ), - description: z - .string() - .optional() + description: z.string().optional() }); type TFormData = z.infer; @@ -59,7 +58,7 @@ export const FolderForm = ({ if (textarea) { const lines = textarea.value.split("\n"); const maxDescriptionLines = 10; - + if (lines.length > maxDescriptionLines) { textarea.value = lines.slice(0, maxDescriptionLines).join("\n"); } @@ -90,30 +89,32 @@ export const FolderForm = ({ )} /> ( - -
NameAuth MethodName
{name} - {authMethod || None} - {name}