diff --git a/backend/src/db/migrations/20250116092245_add-enforce-capitalization-project-flag.ts b/backend/src/db/migrations/20250116092245_add-enforce-capitalization-project-flag.ts new file mode 100644 index 000000000..1cb6644f5 --- /dev/null +++ b/backend/src/db/migrations/20250116092245_add-enforce-capitalization-project-flag.ts @@ -0,0 +1,33 @@ +import { Knex } from "knex"; + +import { TableName } from "../schemas"; + +export async function up(knex: Knex): Promise { + const hasEnforceCapitalizationCol = await knex.schema.hasColumn(TableName.Project, "enforceCapitalization"); + const hasAutoCapitalizationCol = await knex.schema.hasColumn(TableName.Project, "autoCapitalization"); + + await knex.schema.alterTable(TableName.Project, (t) => { + if (!hasEnforceCapitalizationCol) { + t.boolean("enforceCapitalization").defaultTo(false).notNullable(); + } + + if (hasAutoCapitalizationCol) { + t.boolean("autoCapitalization").defaultTo(false).alter(); + } + }); +} + +export async function down(knex: Knex): Promise { + const hasEnforceCapitalizationCol = await knex.schema.hasColumn(TableName.Project, "enforceCapitalization"); + const hasAutoCapitalizationCol = await knex.schema.hasColumn(TableName.Project, "autoCapitalization"); + + await knex.schema.alterTable(TableName.Project, (t) => { + if (hasEnforceCapitalizationCol) { + t.dropColumn("enforceCapitalization"); + } + + if (hasAutoCapitalizationCol) { + t.boolean("autoCapitalization").defaultTo(true).alter(); + } + }); +} diff --git a/backend/src/db/schemas/projects.ts b/backend/src/db/schemas/projects.ts index ec43be292..8c1a0386c 100644 --- a/backend/src/db/schemas/projects.ts +++ b/backend/src/db/schemas/projects.ts @@ -13,7 +13,7 @@ export const ProjectsSchema = z.object({ id: z.string(), name: z.string(), slug: z.string(), - autoCapitalization: z.boolean().default(true).nullable().optional(), + autoCapitalization: z.boolean().default(false).nullable().optional(), orgId: z.string().uuid(), createdAt: z.date(), updatedAt: z.date(), @@ -25,7 +25,8 @@ export const ProjectsSchema = z.object({ kmsSecretManagerKeyId: z.string().uuid().nullable().optional(), kmsSecretManagerEncryptedDataKey: zodBuffer.nullable().optional(), description: z.string().nullable().optional(), - type: z.string() + type: z.string(), + enforceCapitalization: z.boolean().default(false) }); export type TProjects = z.infer; 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 5b5ea6ce3..0869b6374 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 @@ -1267,9 +1267,10 @@ export const secretApprovalRequestServiceFactory = ({ type: SecretType.Shared })) ); - if (secrets.length) + + if (secrets.length !== secretsWithNewName.length) throw new NotFoundError({ - message: `Secret does not exist: ${secretsToUpdateStoredInDB.map((el) => el.key).join(",")}` + message: `Secret does not exist: ${secrets.map((el) => el.key).join(",")}` }); } diff --git a/backend/src/services/project/project-service.ts b/backend/src/services/project/project-service.ts index 2d5b492f1..b5046be63 100644 --- a/backend/src/services/project/project-service.ts +++ b/backend/src/services/project/project-service.ts @@ -544,8 +544,10 @@ export const projectServiceFactory = ({ const updatedProject = await projectDAL.updateById(project.id, { name: update.name, description: update.description, - autoCapitalization: update.autoCapitalization + autoCapitalization: update.autoCapitalization, + enforceCapitalization: update.autoCapitalization }); + return updatedProject; }; @@ -567,7 +569,11 @@ export const projectServiceFactory = ({ }); ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Edit, ProjectPermissionSub.Settings); - const updatedProject = await projectDAL.updateById(projectId, { autoCapitalization }); + const updatedProject = await projectDAL.updateById(projectId, { + autoCapitalization, + enforceCapitalization: autoCapitalization + }); + return updatedProject; }; diff --git a/backend/src/services/secret/secret-service.ts b/backend/src/services/secret/secret-service.ts index 66e53c612..d4759c2be 100644 --- a/backend/src/services/secret/secret-service.ts +++ b/backend/src/services/secret/secret-service.ts @@ -88,7 +88,7 @@ type TSecretServiceFactoryDep = { secretDAL: TSecretDALFactory; secretTagDAL: TSecretTagDALFactory; secretVersionDAL: TSecretVersionDALFactory; - projectDAL: Pick; + projectDAL: Pick; projectEnvDAL: Pick; folderDAL: Pick< TSecretFolderDALFactory, @@ -1466,6 +1466,16 @@ export const secretServiceFactory = ({ secretMetadata }: TCreateSecretRawDTO) => { const { botKey, shouldUseSecretV2Bridge } = await projectBotService.getBotKey(projectId); + const project = await projectDAL.findById(projectId); + if (project.enforceCapitalization) { + if (secretName !== secretName.toUpperCase()) { + throw new BadRequestError({ + message: + "Secret names must be in UPPERCASE per project requirements. You can disable this requirement in project settings." + }); + } + } + const policy = actor === ActorType.USER && type === SecretType.Shared ? await secretApprovalPolicyService.getSecretApprovalPolicy(projectId, environment, secretPath) @@ -1609,6 +1619,16 @@ export const secretServiceFactory = ({ secretMetadata }: TUpdateSecretRawDTO) => { const { botKey, shouldUseSecretV2Bridge } = await projectBotService.getBotKey(projectId); + const project = await projectDAL.findById(projectId); + if (project.enforceCapitalization) { + if (newSecretName && newSecretName !== newSecretName.toUpperCase()) { + throw new BadRequestError({ + message: + "Secret names must be in UPPERCASE per project requirements. You can disable this requirement in project settings." + }); + } + } + const policy = actor === ActorType.USER && type === SecretType.Shared ? await secretApprovalPolicyService.getSecretApprovalPolicy(projectId, environment, secretPath) @@ -1858,7 +1878,23 @@ export const secretServiceFactory = ({ actor === ActorType.USER ? await secretApprovalPolicyService.getSecretApprovalPolicy(projectId, environment, secretPath) : undefined; + if (shouldUseSecretV2Bridge) { + const project = await projectDAL.findById(projectId); + if (project.enforceCapitalization) { + const caseViolatingSecretKeys = inputSecrets + .filter((sec) => sec.secretKey !== sec.secretKey.toUpperCase()) + .map((sec) => sec.secretKey); + + if (caseViolatingSecretKeys.length) { + throw new BadRequestError({ + message: `Secret names must be in UPPERCASE per project requirements: ${caseViolatingSecretKeys.join( + ", " + )}. You can disable this requirement in project settings` + }); + } + } + if (policy) { const approval = await secretApprovalRequestService.generateSecretApprovalRequestV2Bridge({ policy, @@ -1987,6 +2023,21 @@ export const secretServiceFactory = ({ ? await secretApprovalPolicyService.getSecretApprovalPolicy(projectId, environment, secretPath) : undefined; if (shouldUseSecretV2Bridge) { + const project = await projectDAL.findById(projectId); + if (project.enforceCapitalization) { + const caseViolatingSecretKeys = inputSecrets + .filter((sec) => sec.newSecretName && sec.newSecretName !== sec.newSecretName.toUpperCase()) + .map((sec) => sec.newSecretName); + + if (caseViolatingSecretKeys.length) { + throw new BadRequestError({ + message: `Secret names must be in UPPERCASE per project requirements: ${caseViolatingSecretKeys.join( + ", " + )}. You can disable this requirement in project settings` + }); + } + } + if (policy) { const approval = await secretApprovalRequestService.generateSecretApprovalRequestV2Bridge({ policy, diff --git a/frontend/public/locales/en/translations.json b/frontend/public/locales/en/translations.json index b1465f04d..5c68e440d 100644 --- a/frontend/public/locales/en/translations.json +++ b/frontend/public/locales/en/translations.json @@ -301,8 +301,8 @@ "project-id-description2": "For more guidance, including code snipets for various languages and frameworks, see ", "auto-generated": "This is your project's auto-generated unique identifier. It can't be changed.", "docs": "Infisical Docs", - "auto-capitalization": "Auto Capitalization", - "auto-capitalization-description": "According to standards, Infisical will automatically capitalize your keys. If you want to disable this feature, you can do so here." + "enforce-capitalization": "Enforce Capitalization", + "enforce-capitalization-description": "According to standards, Infisical enforces uppercase secret keys. If you want to disable this feature, you can do so here." } }, "signup": { diff --git a/frontend/public/locales/es/translations.json b/frontend/public/locales/es/translations.json index 44da9a8ce..d7ee331f4 100644 --- a/frontend/public/locales/es/translations.json +++ b/frontend/public/locales/es/translations.json @@ -289,8 +289,8 @@ "project-id-description2": "Para más guías, incluyendo ejemplos de código en diferentes lenguajes y frameworks, visita ", "auto-generated": "Este es el ID único y autogenerado de proyecto. No se puede modificar.", "docs": "Documentación de Infisical", - "auto-capitalization": "Mayúsculas automáticas", - "auto-capitalization-description": "De acuerdo con los estándares, Infisical pondrá en mayúsculas tus claves. Si quieres desactivar esta funcionalidad, lo puedes hacer aquí." + "enforce-capitalization": "Hacer cumplir la capitalización", + "enforce-capitalization-description": "Según los estándares, Infisical aplica claves secretas en mayúsculas. Si desea desactivar esta función, puede hacerlo aquí." } }, "signup": { diff --git a/frontend/public/locales/pt-BR/translations.json b/frontend/public/locales/pt-BR/translations.json index ba324849b..a491d3d74 100644 --- a/frontend/public/locales/pt-BR/translations.json +++ b/frontend/public/locales/pt-BR/translations.json @@ -266,8 +266,8 @@ "project-id-description2": "Para obter mais orientações, incluindo trechos de código para várias linguagens e frameworks, consulte ", "auto-generated": "Este é o identificador exclusivo - gerado automaticamente - do seu projeto. Não pode ser alterado.", "docs": "Documentação do Infisical", - "auto-capitalization": "Converter em caixa alta automaticamente", - "auto-capitalization-description": "Por padrão, Infisical converte automaticamente as chaves em caixa alta. Se você quiser desativar essa funcionalidade, pode fazê-lo aqui." + "enforce-capitalization": "Aplicar capitalização", + "enforce-capitalization-description": "De acordo com os padrões, o Infisical impõe chaves secretas em letras maiúsculas. Se quiser desabilitar esse recurso, você pode fazer isso aqui." } }, "signup": { 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 702ef21b6..c894d6b14 100644 --- a/frontend/src/pages/secret-manager/OverviewPage/components/CreateSecretForm/CreateSecretForm.tsx +++ b/frontend/src/pages/secret-manager/OverviewPage/components/CreateSecretForm/CreateSecretForm.tsx @@ -169,8 +169,8 @@ export const CreateSecretForm = ({ secretPath = "/", onClose }: Props) => { if (!secretKey || isWholeKeyHighlighted) { e.preventDefault(); - - setValue("key", key); + const keyStr = currentWorkspace.autoCapitalization ? key.toUpperCase() : key; + setValue("key", keyStr); if (value) { setValue("value", value); } diff --git a/frontend/src/pages/secret-manager/SecretDashboardPage/components/CreateSecretForm/CreateSecretForm.tsx b/frontend/src/pages/secret-manager/SecretDashboardPage/components/CreateSecretForm/CreateSecretForm.tsx index 1c10f6909..dea2f8a5e 100644 --- a/frontend/src/pages/secret-manager/SecretDashboardPage/components/CreateSecretForm/CreateSecretForm.tsx +++ b/frontend/src/pages/secret-manager/SecretDashboardPage/components/CreateSecretForm/CreateSecretForm.tsx @@ -125,8 +125,8 @@ export const CreateSecretForm = ({ if (!secretKey || isWholeKeyHighlighted) { e.preventDefault(); - - setValue("key", key); + const keyStr = autoCapitalize ? key.toUpperCase() : key; + setValue("key", keyStr); if (value) { setValue("value", value); } diff --git a/frontend/src/pages/secret-manager/SettingsPage/components/AutoCapitalizationSection/AutoCapitalizationSection.tsx b/frontend/src/pages/secret-manager/SettingsPage/components/AutoCapitalizationSection/AutoCapitalizationSection.tsx index bc60acd39..f2d86153f 100644 --- a/frontend/src/pages/secret-manager/SettingsPage/components/AutoCapitalizationSection/AutoCapitalizationSection.tsx +++ b/frontend/src/pages/secret-manager/SettingsPage/components/AutoCapitalizationSection/AutoCapitalizationSection.tsx @@ -37,7 +37,7 @@ export const AutoCapitalizationSection = () => { return (
-

{t("settings.project.auto-capitalization")}

+

{t("settings.project.enforce-capitalization")}

{(isAllowed) => (
@@ -50,7 +50,7 @@ export const AutoCapitalizationSection = () => { handleToggleCapitalizationToggle(state as boolean); }} > - {t("settings.project.auto-capitalization-description")} + {t("settings.project.enforce-capitalization-description")}
)}