diff --git a/backend-pg/src/ee/routes/v1/index.ts b/backend-pg/src/ee/routes/v1/index.ts index 41e5817b7..2ed439316 100644 --- a/backend-pg/src/ee/routes/v1/index.ts +++ b/backend-pg/src/ee/routes/v1/index.ts @@ -8,6 +8,7 @@ import { registerSecretApprovalRequestRouter } from "./secret-approval-request-r import { registerSecretRotationProviderRouter } from "./secret-rotation-provider-router"; import { registerSecretRotationRouter } from "./secret-rotation-router"; import { registerSecretScanningRouter } from "./secret-scanning-router"; +import { registerSecretVersionRouter } from "./secret-version-router"; import { registerSnapshotRouter } from "./snapshot-router"; import { registerTrustedIpRouter } from "./trusted-ip-router"; @@ -34,4 +35,5 @@ export const registerV1EERoutes = async (server: FastifyZodProvider) => { await server.register(registerSamlRouter, { prefix: "/sso" }); await server.register(registerSecretScanningRouter, { prefix: "/secret-scanning" }); await server.register(registerSecretRotationRouter, { prefix: "/secret-rotations" }); + await server.register(registerSecretVersionRouter, { prefix: "/secret" }); }; diff --git a/backend-pg/src/ee/routes/v1/secret-version-router.ts b/backend-pg/src/ee/routes/v1/secret-version-router.ts new file mode 100644 index 000000000..269ed8636 --- /dev/null +++ b/backend-pg/src/ee/routes/v1/secret-version-router.ts @@ -0,0 +1,37 @@ +import { z } from "zod"; + +import { SecretVersionsSchema } from "@app/db/schemas"; +import { verifyAuth } from "@app/server/plugins/auth/verify-auth"; +import { AuthMode } from "@app/services/auth/auth-type"; + +export const registerSecretVersionRouter = async (server: FastifyZodProvider) => { + server.route({ + url: "/:secretId/secret-versions", + method: "GET", + schema: { + params: z.object({ + secretId: z.string() + }), + querystring: z.object({ + offset: z.coerce.number(), + limit: z.coerce.number() + }), + response: { + 200: z.object({ + secretVersions: SecretVersionsSchema.omit({ secretBlindIndex: true }).array() + }) + } + }, + onRequest: verifyAuth([AuthMode.JWT]), + handler: async (req) => { + const secretVersions = await server.services.secret.getSecretVersions({ + actor: req.permission.type, + actorId: req.permission.id, + limit: req.query.limit, + offset: req.query.offset, + secretId: req.params.secretId + }); + return { secretVersions }; + } + }); +}; diff --git a/backend-pg/src/ee/services/secret-scanning/secret-scanning-queue/secret-scanning-queue.ts b/backend-pg/src/ee/services/secret-scanning/secret-scanning-queue/secret-scanning-queue.ts index e40a1daca..70dd6ada4 100644 --- a/backend-pg/src/ee/services/secret-scanning/secret-scanning-queue/secret-scanning-queue.ts +++ b/backend-pg/src/ee/services/secret-scanning/secret-scanning-queue/secret-scanning-queue.ts @@ -6,12 +6,12 @@ import { logger } from "@app/lib/logger"; import { QueueJobs, QueueName, TQueueServiceFactory } from "@app/queue"; import { TOrgDalFactory } from "@app/services/org/org-dal"; import { SmtpTemplates, TSmtpService } from "@app/services/smtp/smtp-service"; -import { TUserDalFactory } from "@app/services/user/user-dal"; import { TSecretScanningDalFactory } from "../secret-scanning-dal"; import { scanContentAndGetFindings, - scanFullRepoContentAndGetFindings} from "./secret-scanning-fns"; + scanFullRepoContentAndGetFindings +} from "./secret-scanning-fns"; import { SecretMatch, TScanFullRepoEventPayload, @@ -23,7 +23,6 @@ type TSecretScanningQueueFactoryDep = { secretScanningDal: TSecretScanningDalFactory; smtpService: Pick; orgMembershipDal: Pick; - userDal: Pick; }; export type TSecretScanningQueueFactory = ReturnType; @@ -33,7 +32,6 @@ export const secretScanningQueueFactory = ({ secretScanningDal, smtpService, orgMembershipDal: orgMemberDal, - userDal }: TSecretScanningQueueFactoryDep) => { const startFullRepoScan = async (payload: TScanFullRepoEventPayload) => { await queueService.queue(QueueName.SecretFullRepoScan, QueueJobs.SecretScan, payload, { @@ -69,12 +67,7 @@ export const secretScanningQueueFactory = ({ orgId: organizationId, role: OrgMembershipRole.Admin }); - const userEmails = await userDal.find({ - $in: { - id: adminsOfWork.map(({ userId }) => userId).filter(Boolean) as string[] - } - }); - return userEmails.map((userObject) => userObject.email); + return adminsOfWork.map((userObject) => userObject.email); }; queueService.start(QueueName.SecretPushEventScan, async (job) => { diff --git a/backend-pg/src/server/routes/index.ts b/backend-pg/src/server/routes/index.ts index b5d14c40f..c9154c2dd 100644 --- a/backend-pg/src/server/routes/index.ts +++ b/backend-pg/src/server/routes/index.ts @@ -251,7 +251,6 @@ export const registerRoutes = async ( const apiKeyService = apiKeyServiceFactory({ apiKeyDal, userDal }); const secretScanningQueue = secretScanningQueueFactory({ - userDal, smtpService, secretScanningDal, queueService, diff --git a/backend-pg/src/server/routes/sanitizedSchemas.ts b/backend-pg/src/server/routes/sanitizedSchemas.ts index 6abe65785..cc9dedd45 100644 --- a/backend-pg/src/server/routes/sanitizedSchemas.ts +++ b/backend-pg/src/server/routes/sanitizedSchemas.ts @@ -31,6 +31,8 @@ export const sapPubSchema = SecretApprovalPoliciesSchema.merge( export const secretRawSchema = z.object({ id: z.string(), _id: z.string(), + workspace: z.string(), + environment: z.string(), version: z.number(), type: z.string(), secretKey: z.string(), diff --git a/backend-pg/src/server/routes/v1/password-router.ts b/backend-pg/src/server/routes/v1/password-router.ts index ae1a36117..3073fba91 100644 --- a/backend-pg/src/server/routes/v1/password-router.ts +++ b/backend-pg/src/server/routes/v1/password-router.ts @@ -1,8 +1,9 @@ import { z } from "zod"; -import { BackupPrivateKeySchema } from "@app/db/schemas"; +import { BackupPrivateKeySchema, UsersSchema } from "@app/db/schemas"; import { getConfig } from "@app/lib/config/env"; 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"; export const registerPasswordRouter = async (server: FastifyZodProvider) => { @@ -66,6 +67,58 @@ export const registerPasswordRouter = async (server: FastifyZodProvider) => { } }); + server.route({ + method: "POST", + url: "/email/password-reset", + schema: { + body: z.object({ + email: z.string().email().trim() + }), + response: { + 200: z.object({ + message: z.string() + }) + } + }, + handler: async (req) => { + await server.services.password.sendPasswordResetEmail(req.body.email); + + return { + message: "If an account exists with this email, a password reset link has been sent" + }; + } + }); + + server.route({ + method: "POST", + url: "/email/password-reset-verify", + schema: { + body: z.object({ + email: z.string().email().trim(), + code: z.string().trim() + }), + response: { + 200: z.object({ + message: z.string(), + user: UsersSchema, + token: z.string() + }) + } + }, + handler: async (req) => { + const { token, user } = await server.services.password.verifyPasswordResetEmail( + req.body.email, + req.body.code + ); + + return { + message: "Successfully verified email", + user, + token + }; + } + }); + server.route({ method: "POST", url: "/backup-private-key", @@ -87,9 +140,10 @@ export const registerPasswordRouter = async (server: FastifyZodProvider) => { } }, handler: async (req) => { + const token = validateSignUpAuthorization(req.headers.authorization as string, "",false)! const backupPrivateKey = await server.services.password.createBackupPrivateKey({ ...req.body, - userId: req.permission.id + userId: token.userId, }); if (!backupPrivateKey) throw new Error("Failed to create backup key"); @@ -100,7 +154,6 @@ export const registerPasswordRouter = async (server: FastifyZodProvider) => { server.route({ method: "GET", url: "/backup-private-key", - onRequest: verifyAuth([AuthMode.JWT]), schema: { response: { 200: z.object({ @@ -110,8 +163,9 @@ export const registerPasswordRouter = async (server: FastifyZodProvider) => { } }, handler: async (req) => { + const token = validateSignUpAuthorization(req.headers.authorization as string, "",false)! const backupPrivateKey = await server.services.password.getBackupPrivateKeyOfUser( - req.permission.id + token.userId ); if (!backupPrivateKey) throw new Error("Failed to find backup key"); @@ -121,8 +175,7 @@ export const registerPasswordRouter = async (server: FastifyZodProvider) => { server.route({ method: "POST", - url: "/email/password-reset", - onRequest: verifyAuth([AuthMode.JWT]), + url: "/password-reset", schema: { body: z.object({ protectedKey: z.string().trim(), @@ -141,9 +194,10 @@ export const registerPasswordRouter = async (server: FastifyZodProvider) => { } }, handler: async (req) => { + const token = validateSignUpAuthorization(req.headers.authorization as string, "",false)! await server.services.password.resetPasswordByBackupKey({ ...req.body, - userId: req.permission.id + userId: token.userId, }); return { message: "Successfully updated backup private key" }; diff --git a/backend-pg/src/server/routes/v3/signup-router.ts b/backend-pg/src/server/routes/v3/signup-router.ts index f7f7e9ecb..a3cb94258 100644 --- a/backend-pg/src/server/routes/v3/signup-router.ts +++ b/backend-pg/src/server/routes/v3/signup-router.ts @@ -65,7 +65,7 @@ export const registerSignupRouter = async (server: FastifyZodProvider) => { encryptedPrivateKeyTag: z.string().trim(), salt: z.string().trim(), verifier: z.string().trim(), - organizationName: z.string().trim(), + organizationName: z.string().trim().min(1), providerAuthToken: z.string().trim().optional().nullish(), attributionSource: z.string().trim().optional() }), diff --git a/backend-pg/src/services/auth/auth-fns.ts b/backend-pg/src/services/auth/auth-fns.ts index 8a5882bfc..6550251c0 100644 --- a/backend-pg/src/services/auth/auth-fns.ts +++ b/backend-pg/src/services/auth/auth-fns.ts @@ -21,7 +21,7 @@ export const validateProviderAuthToken = (providerToken: string, email: string) if (decodedToken.email !== email) throw new Error("Invalid auth credentials"); }; -export const validateSignUpAuthorization = async (token: string, userId: string) => { +export const validateSignUpAuthorization = (token: string, userId: string, validate = true) => { const appCfg = getConfig(); const [AUTH_TOKEN_TYPE, AUTH_TOKEN_VALUE] = <[string, string]>token?.split(" ", 2) ?? [ null, @@ -45,6 +45,7 @@ export const validateSignUpAuthorization = async (token: string, userId: string) AUTH_TOKEN_VALUE, appCfg.JWT_AUTH_SECRET ) as AuthModeProviderSignUpTokenPayload; + if (!validate) return decodedToken; if (decodedToken.authTokenType !== AuthTokenType.SIGNUP_TOKEN) throw new UnauthorizedError(); if (decodedToken.userId !== userId) throw new UnauthorizedError(); diff --git a/backend-pg/src/services/org/org-dal.ts b/backend-pg/src/services/org/org-dal.ts index 8f916e48b..e9921b8a7 100644 --- a/backend-pg/src/services/org/org-dal.ts +++ b/backend-pg/src/services/org/org-dal.ts @@ -10,7 +10,13 @@ import { TOrgMembershipsUpdate } from "@app/db/schemas"; import { DatabaseError } from "@app/lib/errors"; -import { ormify, withTransaction } from "@app/lib/knex"; +import { + buildFindFilter, + selectAllTableCols, + TFindFilter, + TFindOpt, + withTransaction +} from "@app/lib/knex"; export type TOrgDalFactory = ReturnType; @@ -109,7 +115,7 @@ export const orgDalFactory = (db: TDbClient) => { // MEMBERSHIP OPERATIONS // -------------------- - const orgMembershipOrm = ormify(db, TableName.OrgMembership); + // const orgMembershipOrm = ormify(db, TableName.OrgMembership); const createMembership = async (data: TOrgMembershipsInsert, tx?: Knex) => { try { @@ -160,6 +166,32 @@ export const orgDalFactory = (db: TDbClient) => { } }; + const findMembership = async ( + filter: TFindFilter, + { offset, limit, sort, tx }: TFindOpt = {} + ) => { + try { + const query = (tx || db)(TableName.OrgMembership) + .where(buildFindFilter(filter)) + .join(TableName.Users, `${TableName.Users}.id`, `${TableName.OrgMembership}.userId`) + .select( + selectAllTableCols(TableName.OrgMembership), + db.ref("email").withSchema(TableName.Users) + ); + if (limit) query.limit(limit); + if (offset) query.offset(offset); + if (sort) { + query.orderBy( + sort.map(([column, order, nulls]) => ({ column: column as string, order, nulls })) + ); + } + const res = await query; + return res; + } catch (error) { + throw new DatabaseError({ error, name: "Find one" }); + } + }; + return withTransaction(db, { findAllOrgMembers, findOrgById, @@ -167,7 +199,7 @@ export const orgDalFactory = (db: TDbClient) => { create, updateById, deleteById, - findMembership: orgMembershipOrm.find, + findMembership, createMembership, updateMembershipById, deleteMembershipById, diff --git a/backend-pg/src/services/project-membership/project-membership-service.ts b/backend-pg/src/services/project-membership/project-membership-service.ts index 86066c7bb..742179509 100644 --- a/backend-pg/src/services/project-membership/project-membership-service.ts +++ b/backend-pg/src/services/project-membership/project-membership-service.ts @@ -1,6 +1,6 @@ import { ForbiddenError } from "@casl/ability"; -import { OrgMembershipStatus, ProjectMembershipRole } from "@app/db/schemas"; +import { OrgMembershipStatus, ProjectMembershipRole, TableName } from "@app/db/schemas"; import { TLicenseServiceFactory } from "@app/ee/services/license/license-service"; import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service"; import { @@ -142,7 +142,11 @@ export const projectMembershipServiceFactory = ({ ); const orgMembers = await orgDal.findMembership({ orgId: project.orgId, - $in: { id: members.map(({ orgMembershipId }) => orgMembershipId) } + $in: { + [`${TableName.OrgMembership}.id` as "id"]: members.map( + ({ orgMembershipId }) => orgMembershipId + ) + } }); if (orgMembers.length !== members.length) throw new BadRequestError({ message: "Some users are not part of org" }); @@ -180,7 +184,7 @@ export const projectMembershipServiceFactory = ({ await smtpService.sendMail({ template: SmtpTemplates.WorkspaceInvite, subjectLine: "Infisical workspace invitation", - recipients: orgMembers.map(({ userId }) => userId).filter(Boolean) as string[], + recipients: orgMembers.map(({ email }) => email).filter(Boolean) as string[], substitutions: { inviterFirstName: sender.firstName, inviterEmail: sender.email, diff --git a/backend-pg/src/services/secret/secret-fns.ts b/backend-pg/src/services/secret/secret-fns.ts index 58bb9680e..34439a583 100644 --- a/backend-pg/src/services/secret/secret-fns.ts +++ b/backend-pg/src/services/secret/secret-fns.ts @@ -197,7 +197,10 @@ export const interpolateSecrets = ({ return expandSecrets; }; -export const decryptSecretRaw = (secret: TSecrets, key: string) => { +export const decryptSecretRaw = ( + secret: TSecrets & { workspace: string; environment: string }, + key: string +) => { const secretKey = decryptSymmetric128BitHexKeyUTF8({ ciphertext: secret.secretKeyCiphertext, iv: secret.secretKeyIV, @@ -225,6 +228,8 @@ export const decryptSecretRaw = (secret: TSecrets, key: string) => { return { secretKey, + workspace: secret.workspace, + environment: secret.environment, secretValue, secretComment, version: secret.version, diff --git a/backend-pg/src/services/secret/secret-service.ts b/backend-pg/src/services/secret/secret-service.ts index 40fcce50c..8b56f7d97 100644 --- a/backend-pg/src/services/secret/secret-service.ts +++ b/backend-pg/src/services/secret/secret-service.ts @@ -38,6 +38,7 @@ import { TGetASecretRawDTO, TGetSecretsDTO, TGetSecretsRawDTO, + TGetSecretVersionsDTO, TListSecretVersionDTO, TUpdateBulkSecretDTO, TUpdateSecretDTO, @@ -326,7 +327,7 @@ export const secretServiceFactory = ({ await snapshotService.performSnapshot(folderId); await secretQueueService.syncSecrets({ secretPath: path, projectId, environment }); // TODO(akhilmhdh-pg): licence check, posthog service and snapshot - return { ...secret[0], tags }; + return { ...secret[0], environment, workspace: projectId, tags }; }; const updateSecret = async ({ @@ -419,7 +420,7 @@ export const secretServiceFactory = ({ await snapshotService.performSnapshot(folderId); await secretQueueService.syncSecrets({ secretPath: path, projectId, environment }); // TODO(akhilmhdh-pg): licence check, posthog service and snapshot - return updatedSecret[0]; + return { ...updatedSecret[0], workspace: projectId, environment }; }; const deleteSecret = async ({ @@ -474,7 +475,7 @@ export const secretServiceFactory = ({ await secretQueueService.syncSecrets({ secretPath: path, projectId, environment }); // TODO(akhilmhdh-pg): licence check, posthog service and snapshot - return deletedSecret[0]; + return { ...deletedSecret[0], workspace: projectId, environment }; }; const getSecrets = async ({ @@ -515,9 +516,12 @@ export const secretServiceFactory = ({ secretDal, folderDal }); - return { secrets, imports: importedSecrets }; + return { + secrets: secrets.map((el) => ({ ...el, workspace: projectId, environment })), + imports: importedSecrets + }; } - return { secrets }; + return { secrets: secrets.map((el) => ({ ...el, workspace: projectId, environment })) }; }; const getASecret = async ({ @@ -572,14 +576,18 @@ export const secretServiceFactory = ({ for (let i = importedSecrets.length - 1; i >= 0; i -= 1) { for (let j = 0; j < importedSecrets[i].secrets.length; j += 1) { if (secretBlindIndex === importedSecrets[i].secrets[j].secretBlindIndex) { - return importedSecrets[i].secrets[j]; + return { + ...importedSecrets[i].secrets[j], + workspace: projectId, + environment: importedSecrets[i].environment.slug + }; } } } } if (!secret) throw new BadRequestError({ message: "Secret not found" }); - return secret; + return { ...secret, workspace: projectId, environment }; }; const createManySecret = async ({ @@ -813,7 +821,12 @@ export const secretServiceFactory = ({ secrets: secrets.map((el) => decryptSecretRaw(el, botKey)), imports: (imports || [])?.map(({ secrets: importedSecrets, ...el }) => ({ ...el, - secrets: importedSecrets.map((sec) => decryptSecretRaw(sec, botKey)) + secrets: importedSecrets.map((sec) => + decryptSecretRaw( + { ...sec, environment: el.environment.slug, workspace: projectId }, + botKey + ) + ) })) }; }; @@ -953,6 +966,36 @@ export const secretServiceFactory = ({ return decryptSecretRaw(secret, botKey); }; + const getSecretVersions = async ({ + actorId, + actor, + limit = 20, + offset = 0, + secretId + }: TGetSecretVersionsDTO) => { + const secret = await secretDal.findById(secretId); + if (!secret) throw new BadRequestError({ message: "Failed to find secret" }); + + const folder = await folderDal.findById(secret.folderId); + if (!folder) throw new BadRequestError({ message: "Failed to find secret" }); + + const { permission } = await permissionService.getProjectPermission( + actor, + actorId, + folder.projectId + ); + ForbiddenError.from(permission).throwUnlessCan( + ProjectPermissionActions.Read, + ProjectPermissionSub.SecretRollback + ); + + const secretVersions = await secretVersionDal.find( + { secretId }, + { offset, limit, sort: [["createdAt", "desc"]] } + ); + return secretVersions; + }; + return { createSecret, deleteSecret, @@ -968,6 +1011,7 @@ export const secretServiceFactory = ({ updateSecretRaw, deleteSecretRaw, listSecretVersionsBySecretId, + getSecretVersions, // external services function fnSecretBulkDelete, fnSecretBulkUpdate, diff --git a/backend-pg/src/services/secret/secret-types.ts b/backend-pg/src/services/secret/secret-types.ts index fc0ae8a01..57012b504 100644 --- a/backend-pg/src/services/secret/secret-types.ts +++ b/backend-pg/src/services/secret/secret-types.ts @@ -167,6 +167,12 @@ export type TDeleteSecretRawDTO = TProjectPermission & { type: SecretType; }; +export type TGetSecretVersionsDTO = Omit & { + limit?: number; + offset?: number; + secretId: string; +}; + export type TFnSecretBulkInsert = { folderId: string; tx?: Knex; diff --git a/docker-compose.pg.yml b/docker-compose.pg.yml index 7f4d029f4..44ae02059 100644 --- a/docker-compose.pg.yml +++ b/docker-compose.pg.yml @@ -74,6 +74,16 @@ services: - NEXT_PUBLIC_ENV=development - INFISICAL_TELEMETRY_ENABLED=false + smtp-server: + container_name: infisical-dev-smtp-server + image: lytrax/mailhog:latest # https://github.com/mailhog/MailHog/issues/353#issuecomment-821137362 + restart: always + logging: + driver: "none" # disable saving logs + ports: + - 1025:1025 # SMTP server + - 8025:8025 # Web UI + volumes: postgres-data: driver: local diff --git a/frontend/src/hooks/api/roles/types.ts b/frontend/src/hooks/api/roles/types.ts index 0a24389a1..5f205e585 100644 --- a/frontend/src/hooks/api/roles/types.ts +++ b/frontend/src/hooks/api/roles/types.ts @@ -1,3 +1,11 @@ +export enum ProjectMembershipRole { + Admin = "admin", + Member = "member", + Custom = "custom", + Viewer = "viewer", + NoAccess = "no-access" +} + export type TGetProjectRolesDTO = { workspaceId?: string; }; diff --git a/frontend/src/views/Org/MembersPage/components/OrgRoleTabSection/OrgRoleModifySection/OrgRoleModifySection.utils.ts b/frontend/src/views/Org/MembersPage/components/OrgRoleTabSection/OrgRoleModifySection/OrgRoleModifySection.utils.ts index 8bc4d8633..3590b2e20 100644 --- a/frontend/src/views/Org/MembersPage/components/OrgRoleTabSection/OrgRoleModifySection/OrgRoleModifySection.utils.ts +++ b/frontend/src/views/Org/MembersPage/components/OrgRoleTabSection/OrgRoleModifySection/OrgRoleModifySection.utils.ts @@ -15,7 +15,10 @@ const generalPermissionSchema = z export const formSchema = z.object({ name: z.string().trim(), description: z.string().trim().optional(), - slug: z.string().trim(), + slug: z + .string() + .trim() + .refine((val) => val !== "custom", { message: "Cannot use custom as its a keyword" }), permissions: z .object({ workspace: z diff --git a/frontend/src/views/Project/MembersPage/components/MemberListTab/MemberListTab.tsx b/frontend/src/views/Project/MembersPage/components/MemberListTab/MemberListTab.tsx index 2c26e557e..6e0e239d0 100644 --- a/frontend/src/views/Project/MembersPage/components/MemberListTab/MemberListTab.tsx +++ b/frontend/src/views/Project/MembersPage/components/MemberListTab/MemberListTab.tsx @@ -53,6 +53,7 @@ import { useUpdateUserWorkspaceRole, useUploadWsKey } from "@app/hooks/api"; +import { ProjectMembershipRole } from "@app/hooks/api/roles/types"; const addMemberFormSchema = z.object({ orgMembershipId: z.string().trim() @@ -158,7 +159,6 @@ export const MemberListTab = () => { () => members?.find(({ user: u }) => userId === u?.id)?.role === "owner", [userId, members] ); - console.log(members); const findRoleFromId = useCallback( (roleId: string) => { @@ -171,7 +171,9 @@ export const MemberListTab = () => { if (!currentOrg?.id) return; try { - const isCustomRole = !["admin", "member", "viewer"].includes(role); + const isCustomRole = !Object.values(ProjectMembershipRole).includes( + role as ProjectMembershipRole + ); if (isCustomRole && subscription && !subscription?.rbac) { handlePopUpOpen("upgradePlan", { diff --git a/frontend/src/views/Project/MembersPage/components/ProjectRoleListTab/components/ProjectRoleModifySection/ProjectRoleModifySection.utils.ts b/frontend/src/views/Project/MembersPage/components/ProjectRoleListTab/components/ProjectRoleModifySection/ProjectRoleModifySection.utils.ts index 35c41ded0..ac0350d76 100644 --- a/frontend/src/views/Project/MembersPage/components/ProjectRoleListTab/components/ProjectRoleModifySection/ProjectRoleModifySection.utils.ts +++ b/frontend/src/views/Project/MembersPage/components/ProjectRoleListTab/components/ProjectRoleModifySection/ProjectRoleModifySection.utils.ts @@ -28,7 +28,10 @@ const PERMISSION_ACTIONS = ["read", "create", "edit", "delete"] as const; export const formSchema = z.object({ name: z.string().trim(), description: z.string().trim().optional(), - slug: z.string().trim(), + slug: z + .string() + .trim() + .refine((val) => val !== "custom", { message: "Cannot use custom as its a keyword" }), permissions: z .object({ secrets: z.record(multiEnvPermissionSchema).optional(),