chore: resolve merge conflicts

This commit is contained in:
Scott Wilson
2025-01-21 21:30:18 -08:00
248 changed files with 6864 additions and 4051 deletions

View File

@@ -0,0 +1,49 @@
import { Knex } from "knex";
import { TableName } from "../schemas";
export async function up(knex: Knex): Promise<void> {
// find any duplicate group names within organizations
const duplicates = await knex(TableName.Groups)
.select("orgId", "name")
.count("* as count")
.groupBy("orgId", "name")
.having(knex.raw("count(*) > 1"));
// for each set of duplicates, update all but one with a numbered suffix
for await (const duplicate of duplicates) {
const groups = await knex(TableName.Groups)
.select("id", "name")
.where({
orgId: duplicate.orgId,
name: duplicate.name
})
.orderBy("createdAt", "asc"); // keep original name for oldest group
// skip the first (oldest) group, rename others with numbered suffix
for (let i = 1; i < groups.length; i += 1) {
// eslint-disable-next-line no-await-in-loop
await knex(TableName.Groups)
.where("id", groups[i].id)
.update({
name: `${groups[i].name} (${i})`,
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore TS doesn't know about Knex's timestamp types
updatedAt: new Date()
});
}
}
// add the unique constraint
await knex.schema.alterTable(TableName.Groups, (t) => {
t.unique(["orgId", "name"]);
});
}
export async function down(knex: Knex): Promise<void> {
// Remove the unique constraint
await knex.schema.alterTable(TableName.Groups, (t) => {
t.dropUnique(["orgId", "name"]);
});
}

View File

@@ -0,0 +1,33 @@
import { Knex } from "knex";
import { TableName } from "../schemas";
export async function up(knex: Knex): Promise<void> {
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<void> {
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();
}
});
}

View File

@@ -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<typeof ProjectsSchema>;

View File

@@ -93,7 +93,7 @@ export const auditLogStreamServiceFactory = ({
}
)
.catch((err) => {
throw new Error(`Failed to connect with the source ${(err as Error)?.message}`);
throw new BadRequestError({ message: `Failed to connect with upstream source: ${(err as Error)?.message}` });
});
const encryptedHeaders = headers ? infisicalSymmetricEncypt(JSON.stringify(headers)) : undefined;
const logStream = await auditLogStreamDAL.create({

View File

@@ -32,7 +32,7 @@ type TGroupServiceFactoryDep = {
userDAL: Pick<TUserDALFactory, "find" | "findUserEncKeyByUserIdsBatch" | "transaction" | "findOne">;
groupDAL: Pick<
TGroupDALFactory,
"create" | "findOne" | "update" | "delete" | "findAllGroupPossibleMembers" | "findById"
"create" | "findOne" | "update" | "delete" | "findAllGroupPossibleMembers" | "findById" | "transaction"
>;
groupProjectDAL: Pick<TGroupProjectDALFactory, "find">;
orgDAL: Pick<TOrgDALFactory, "findMembership" | "countAllOrgMembers">;
@@ -88,12 +88,26 @@ export const groupServiceFactory = ({
if (!hasRequiredPriviledges)
throw new ForbiddenRequestError({ message: "Failed to create a more privileged group" });
const group = await groupDAL.create({
name,
slug: slug || slugify(`${name}-${alphaNumericNanoId(4)}`),
orgId: actorOrgId,
role: isCustomRole ? OrgMembershipRole.Custom : role,
roleId: customRole?.id
const group = await groupDAL.transaction(async (tx) => {
const existingGroup = await groupDAL.findOne({ orgId: actorOrgId, name }, tx);
if (existingGroup) {
throw new BadRequestError({
message: `Failed to create group with name '${name}'. Group with the same name already exists`
});
}
const newGroup = await groupDAL.create(
{
name,
slug: slug || slugify(`${name}-${alphaNumericNanoId(4)}`),
orgId: actorOrgId,
role: isCustomRole ? OrgMembershipRole.Custom : role,
roleId: customRole?.id
},
tx
);
return newGroup;
});
return group;
@@ -145,21 +159,36 @@ export const groupServiceFactory = ({
if (isCustomRole) customRole = customOrgRole;
}
const [updatedGroup] = await groupDAL.update(
{
id: group.id
},
{
name,
slug: slug ? slugify(slug) : undefined,
...(role
? {
role: customRole ? OrgMembershipRole.Custom : role,
roleId: customRole?.id ?? null
}
: {})
const updatedGroup = await groupDAL.transaction(async (tx) => {
if (name) {
const existingGroup = await groupDAL.findOne({ orgId: actorOrgId, name }, tx);
if (existingGroup && existingGroup.id !== id) {
throw new BadRequestError({
message: `Failed to update group with name '${name}'. Group with the same name already exists`
});
}
}
);
const [updated] = await groupDAL.update(
{
id: group.id
},
{
name,
slug: slug ? slugify(slug) : undefined,
...(role
? {
role: customRole ? OrgMembershipRole.Custom : role,
roleId: customRole?.id ?? null
}
: {})
},
tx
);
return updated;
});
return updatedGroup;
};

View File

@@ -476,14 +476,14 @@ export const ldapConfigServiceFactory = ({
});
} else {
const plan = await licenseService.getPlan(orgId);
if (plan?.memberLimit && plan.membersUsed >= plan.memberLimit) {
if (plan?.slug !== "enterprise" && plan?.memberLimit && plan.membersUsed >= plan.memberLimit) {
// limit imposed on number of members allowed / number of members used exceeds the number of members allowed
throw new BadRequestError({
message: "Failed to create new member via LDAP due to member limit reached. Upgrade plan to add more members."
});
}
if (plan?.identityLimit && plan.identitiesUsed >= plan.identityLimit) {
if (plan?.slug !== "enterprise" && plan?.identityLimit && plan.identitiesUsed >= plan.identityLimit) {
// limit imposed on number of identities allowed / number of identities used exceeds the number of identities allowed
throw new BadRequestError({
message: "Failed to create new member via LDAP due to member limit reached. Upgrade plan to add more members."

View File

@@ -421,14 +421,14 @@ export const samlConfigServiceFactory = ({
});
} else {
const plan = await licenseService.getPlan(orgId);
if (plan?.memberLimit && plan.membersUsed >= plan.memberLimit) {
if (plan?.slug !== "enterprise" && plan?.memberLimit && plan.membersUsed >= plan.memberLimit) {
// limit imposed on number of members allowed / number of members used exceeds the number of members allowed
throw new BadRequestError({
message: "Failed to create new member via SAML due to member limit reached. Upgrade plan to add more members."
});
}
if (plan?.identityLimit && plan.identitiesUsed >= plan.identityLimit) {
if (plan?.slug !== "enterprise" && plan?.identityLimit && plan.identitiesUsed >= plan.identityLimit) {
// limit imposed on number of identities allowed / number of identities used exceeds the number of identities allowed
throw new BadRequestError({
message: "Failed to create new member via SAML due to member limit reached. Upgrade plan to add more members."

View File

@@ -531,7 +531,7 @@ export const scimServiceFactory = ({
firstName: scimUser.name.givenName,
email: scimUser.emails[0].value,
lastName: scimUser.name.familyName,
isEmailVerified: hasEmailChanged ? trustScimEmails : true
isEmailVerified: hasEmailChanged ? trustScimEmails : undefined
},
tx
);
@@ -790,6 +790,21 @@ export const scimServiceFactory = ({
});
const newGroup = await groupDAL.transaction(async (tx) => {
const conflictingGroup = await groupDAL.findOne(
{
name: displayName,
orgId
},
tx
);
if (conflictingGroup) {
throw new ScimRequestError({
detail: `Group with name '${displayName}' already exists in the organization`,
status: 409
});
}
const group = await groupDAL.create(
{
name: displayName,

View File

@@ -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(",")}`
});
}

View File

@@ -476,7 +476,7 @@ export const PROJECTS = {
},
ADD_GROUP_TO_PROJECT: {
projectId: "The ID of the project to add the group to.",
groupId: "The ID of the group to add to the project.",
groupIdOrName: "The ID or name of the group to add to the project.",
role: "The role for the group to assume in the project."
},
UPDATE_GROUP_IN_PROJECT: {

View File

@@ -1,3 +1,4 @@
export { isDisposableEmail } from "./validate-email";
export { isValidFolderName, isValidSecretPath } from "./validate-folder-name";
export { blockLocalAndPrivateIpAddresses } from "./validate-url";
export { isUuidV4 } from "./validate-uuid";

View File

@@ -0,0 +1,3 @@
import { z } from "zod";
export const isUuidV4 = (uuid: string) => z.string().uuid().safeParse(uuid).success;

View File

@@ -923,7 +923,8 @@ export const registerRoutes = async (
certificateTemplateDAL,
projectSlackConfigDAL,
slackIntegrationDAL,
projectTemplateService
projectTemplateService,
groupProjectDAL
});
const projectEnvService = projectEnvServiceFactory({

View File

@@ -73,6 +73,40 @@ export const registerInviteOrgRouter = async (server: FastifyZodProvider) => {
}
});
server.route({
url: "/signup-resend",
config: {
rateLimit: inviteUserRateLimit
},
method: "POST",
schema: {
body: z.object({
membershipId: z.string()
}),
response: {
200: z.object({
signupToken: z
.object({
email: z.string(),
link: z.string()
})
.optional()
})
}
},
onRequest: verifyAuth([AuthMode.JWT]),
handler: async (req) => {
return server.services.org.resendOrgMemberInvitation({
orgId: req.permission.orgId,
actor: req.permission.type,
actorId: req.permission.id,
actorAuthMethod: req.permission.authMethod,
actorOrgId: req.permission.orgId,
membershipId: req.body.membershipId
});
}
});
server.route({
url: "/verify",
method: "POST",

View File

@@ -16,7 +16,7 @@ import { ProjectUserMembershipTemporaryMode } from "@app/services/project-member
export const registerGroupProjectRouter = async (server: FastifyZodProvider) => {
server.route({
method: "POST",
url: "/:projectId/groups/:groupId",
url: "/:projectId/groups/:groupIdOrName",
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
config: {
rateLimit: writeLimit
@@ -30,7 +30,7 @@ export const registerGroupProjectRouter = async (server: FastifyZodProvider) =>
],
params: z.object({
projectId: z.string().trim().describe(PROJECTS.ADD_GROUP_TO_PROJECT.projectId),
groupId: z.string().trim().describe(PROJECTS.ADD_GROUP_TO_PROJECT.groupId)
groupIdOrName: z.string().trim().describe(PROJECTS.ADD_GROUP_TO_PROJECT.groupIdOrName)
}),
body: z
.object({
@@ -76,7 +76,7 @@ export const registerGroupProjectRouter = async (server: FastifyZodProvider) =>
actorOrgId: req.permission.orgId,
roles: req.body.roles || [{ role: req.body.role }],
projectId: req.params.projectId,
groupId: req.params.groupId
groupIdOrName: req.params.groupIdOrName
});
return { groupMembership };

View File

@@ -1,7 +1,7 @@
import { ForbiddenError } from "@casl/ability";
import ms from "ms";
import { ActionProjectType, ProjectMembershipRole, SecretKeyEncoding } from "@app/db/schemas";
import { ActionProjectType, ProjectMembershipRole, SecretKeyEncoding, TGroups } from "@app/db/schemas";
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service";
import { ProjectPermissionActions, ProjectPermissionSub } from "@app/ee/services/permission/project-permission";
import { isAtLeastAsPrivileged } from "@app/lib/casl";
@@ -9,6 +9,7 @@ import { decryptAsymmetric, encryptAsymmetric } from "@app/lib/crypto";
import { infisicalSymmetricDecrypt } from "@app/lib/crypto/encryption";
import { BadRequestError, ForbiddenRequestError, NotFoundError } from "@app/lib/errors";
import { groupBy } from "@app/lib/fn";
import { isUuidV4 } from "@app/lib/validator";
import { TGroupDALFactory } from "../../ee/services/group/group-dal";
import { TUserGroupMembershipDALFactory } from "../../ee/services/group/user-group-membership-dal";
@@ -62,7 +63,7 @@ export const groupProjectServiceFactory = ({
actorAuthMethod,
roles,
projectId,
groupId
groupIdOrName
}: TCreateProjectGroupDTO) => {
const project = await projectDAL.findById(projectId);
@@ -79,13 +80,20 @@ export const groupProjectServiceFactory = ({
});
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Create, ProjectPermissionSub.Groups);
const group = await groupDAL.findOne({ orgId: actorOrgId, id: groupId });
if (!group) throw new NotFoundError({ message: `Failed to find group with ID ${groupId}` });
let group: TGroups | null = null;
if (isUuidV4(groupIdOrName)) {
group = await groupDAL.findOne({ orgId: actorOrgId, id: groupIdOrName });
}
if (!group) {
group = await groupDAL.findOne({ orgId: actorOrgId, name: groupIdOrName });
}
if (!group) throw new NotFoundError({ message: `Failed to find group with ID or name ${groupIdOrName}` });
const existingGroup = await groupProjectDAL.findOne({ groupId: group.id, projectId: project.id });
if (existingGroup)
throw new BadRequestError({
message: `Group with ID ${groupId} already exists in project with id ${project.id}`
message: `Group with ID ${group.id} already exists in project with id ${project.id}`
});
for await (const { role: requestedRoleChange } of roles) {
@@ -128,7 +136,7 @@ export const groupProjectServiceFactory = ({
const projectGroup = await groupProjectDAL.transaction(async (tx) => {
const groupProjectMembership = await groupProjectDAL.create(
{
groupId: group.id,
groupId: group!.id,
projectId: project.id
},
tx
@@ -163,7 +171,7 @@ export const groupProjectServiceFactory = ({
// share project key with users in group that have not
// individually been added to the project and that are not part of
// other groups that are in the project
const groupMembers = await userGroupMembershipDAL.findGroupMembersNotInProject(group.id, project.id, tx);
const groupMembers = await userGroupMembershipDAL.findGroupMembersNotInProject(group!.id, project.id, tx);
if (groupMembers.length) {
const ghostUser = await projectDAL.findProjectGhostUser(project.id, tx);

View File

@@ -3,7 +3,7 @@ import { TProjectPermission } from "@app/lib/types";
import { ProjectUserMembershipTemporaryMode } from "../project-membership/project-membership-types";
export type TCreateProjectGroupDTO = {
groupId: string;
groupIdOrName: string;
roles: (
| {
role: string;

View File

@@ -932,15 +932,22 @@ const syncSecretsAWSParameterStore = async ({
logger.info(
`getIntegrationSecrets: create secret in AWS SSM for [projectId=${projectId}] [environment=${integration.environment.slug}] [secretPath=${integration.secretPath}]`
);
await ssm
.putParameter({
Name: `${integration.path}${key}`,
Type: "SecureString",
Value: secrets[key].value,
...(metadata.kmsKeyId && { KeyId: metadata.kmsKeyId }),
Overwrite: true
})
.promise();
try {
await ssm
.putParameter({
Name: `${integration.path}${key}`,
Type: "SecureString",
Value: secrets[key].value,
...(metadata.kmsKeyId && { KeyId: metadata.kmsKeyId }),
Overwrite: true
})
.promise();
} catch (error) {
(error as { secretKey: string }).secretKey = key;
throw error;
}
if (metadata.secretAWSTag?.length) {
try {
await ssm
@@ -987,15 +994,20 @@ const syncSecretsAWSParameterStore = async ({
// we ensure that the KMS key configured in the integration is applied for ALL parameters on AWS
if (secrets[key].value && (shouldUpdateKms || awsParameterStoreSecretsObj[key].Value !== secrets[key].value)) {
await ssm
.putParameter({
Name: `${integration.path}${key}`,
Type: "SecureString",
Value: secrets[key].value,
Overwrite: true,
...(metadata.kmsKeyId && { KeyId: metadata.kmsKeyId })
})
.promise();
try {
await ssm
.putParameter({
Name: `${integration.path}${key}`,
Type: "SecureString",
Value: secrets[key].value,
Overwrite: true,
...(metadata.kmsKeyId && { KeyId: metadata.kmsKeyId })
})
.promise();
} catch (error) {
(error as { secretKey: string }).secretKey = key;
throw error;
}
}
if (awsParameterStoreSecretsObj[key].Name) {

View File

@@ -69,6 +69,7 @@ import {
TGetOrgMembershipDTO,
TInviteUserToOrgDTO,
TListProjectMembershipsByOrgMembershipIdDTO,
TResendOrgMemberInvitationDTO,
TUpdateOrgDTO,
TUpdateOrgMembershipDTO,
TVerifyUserToOrgDTO
@@ -584,6 +585,66 @@ export const orgServiceFactory = ({
});
return membership;
};
const resendOrgMemberInvitation = async ({
orgId,
actorId,
actor,
actorAuthMethod,
actorOrgId,
membershipId
}: TResendOrgMemberInvitationDTO) => {
const appCfg = getConfig();
const { permission } = await permissionService.getOrgPermission(actor, actorId, orgId, actorAuthMethod, actorOrgId);
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Create, OrgPermissionSubjects.Member);
const org = await orgDAL.findOrgById(orgId);
const [inviteeOrgMembership] = await orgDAL.findMembership({
[`${TableName.OrgMembership}.orgId` as "orgId"]: orgId,
[`${TableName.OrgMembership}.id` as "id"]: membershipId
});
if (inviteeOrgMembership.status !== OrgMembershipStatus.Invited) {
throw new BadRequestError({
message: "Organization invitation already accepted"
});
}
const token = await tokenService.createTokenForUser({
type: TokenType.TOKEN_EMAIL_ORG_INVITATION,
userId: inviteeOrgMembership.userId,
orgId
});
if (!appCfg.isSmtpConfigured) {
return {
signupToken: {
email: inviteeOrgMembership.email as string,
link: `${appCfg.SITE_URL}/signupinvite?token=${token}&to=${inviteeOrgMembership.email}&organization_id=${org?.id}`
}
};
}
await smtpService.sendMail({
template: SmtpTemplates.OrgInvite,
subjectLine: "Infisical organization invitation",
recipients: [inviteeOrgMembership.email as string],
substitutions: {
inviterFirstName: inviteeOrgMembership.firstName,
inviterUsername: inviteeOrgMembership.email,
organizationName: org?.name,
email: inviteeOrgMembership.email,
organizationId: org?.id.toString(),
token,
callback_url: `${appCfg.SITE_URL}/signupinvite`
}
});
return { signupToken: undefined };
};
/*
* Invite user to organization
*/
@@ -627,6 +688,7 @@ export const orgServiceFactory = ({
}
})
: [];
if (projectsToInvite.length !== invitedProjects?.length) {
throw new ForbiddenRequestError({
message: "Access denied to one or more of the specified projects"
@@ -1221,6 +1283,7 @@ export const orgServiceFactory = ({
deleteIncidentContact,
getOrgGroups,
listProjectMembershipsByOrgMembershipId,
findOrgBySlug
findOrgBySlug,
resendOrgMemberInvitation
};
};

View File

@@ -35,6 +35,10 @@ export type TInviteUserToOrgDTO = {
}[];
} & TOrgPermission;
export type TResendOrgMemberInvitationDTO = {
membershipId: string;
} & TOrgPermission;
export type TVerifyUserToOrgDTO = {
email: string;
orgId: string;

View File

@@ -29,6 +29,7 @@ import { ActorType } from "../auth/auth-type";
import { TCertificateDALFactory } from "../certificate/certificate-dal";
import { TCertificateAuthorityDALFactory } from "../certificate-authority/certificate-authority-dal";
import { TCertificateTemplateDALFactory } from "../certificate-template/certificate-template-dal";
import { TGroupProjectDALFactory } from "../group-project/group-project-dal";
import { TIdentityOrgDALFactory } from "../identity/identity-org-dal";
import { TIdentityProjectDALFactory } from "../identity-project/identity-project-dal";
import { TIdentityProjectMembershipRoleDALFactory } from "../identity-project/identity-project-membership-role-dal";
@@ -100,7 +101,8 @@ type TProjectServiceFactoryDep = {
identityProjectDAL: TIdentityProjectDALFactory;
identityProjectMembershipRoleDAL: Pick<TIdentityProjectMembershipRoleDALFactory, "create">;
projectKeyDAL: Pick<TProjectKeyDALFactory, "create" | "findLatestProjectKey" | "delete" | "find" | "insertMany">;
projectMembershipDAL: Pick<TProjectMembershipDALFactory, "create" | "findProjectGhostUser" | "findOne">;
projectMembershipDAL: Pick<TProjectMembershipDALFactory, "create" | "findProjectGhostUser" | "findOne" | "delete">;
groupProjectDAL: Pick<TGroupProjectDALFactory, "delete">;
projectSlackConfigDAL: Pick<TProjectSlackConfigDALFactory, "findOne" | "transaction" | "updateById" | "create">;
slackIntegrationDAL: Pick<TSlackIntegrationDALFactory, "findById" | "findByIdWithWorkflowIntegrationDetails">;
projectUserMembershipRoleDAL: Pick<TProjectUserMembershipRoleDALFactory, "create">;
@@ -120,7 +122,7 @@ type TProjectServiceFactoryDep = {
orgDAL: Pick<TOrgDALFactory, "findOne">;
keyStore: Pick<TKeyStoreFactory, "deleteItem">;
projectBotDAL: Pick<TProjectBotDALFactory, "create">;
projectRoleDAL: Pick<TProjectRoleDALFactory, "find" | "insertMany">;
projectRoleDAL: Pick<TProjectRoleDALFactory, "find" | "insertMany" | "delete">;
kmsService: Pick<
TKmsServiceFactory,
| "updateProjectSecretManagerKmsKey"
@@ -169,7 +171,8 @@ export const projectServiceFactory = ({
projectBotDAL,
projectSlackConfigDAL,
slackIntegrationDAL,
projectTemplateService
projectTemplateService,
groupProjectDAL
}: TProjectServiceFactoryDep) => {
/*
* Create workspace. Make user the admin
@@ -444,13 +447,32 @@ export const projectServiceFactory = ({
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Delete, ProjectPermissionSub.Project);
const deletedProject = await projectDAL.transaction(async (tx) => {
// delete these so that project custom roles can be deleted in cascade effect
// direct deletion of project without these will cause fk error
await projectMembershipDAL.delete({ projectId: project.id }, tx);
await groupProjectDAL.delete({ projectId: project.id }, tx);
const delProject = await projectDAL.deleteById(project.id, tx);
const projectGhostUser = await projectMembershipDAL.findProjectGhostUser(project.id, tx).catch(() => null);
// akhilmhdh: before removing those kms checking any other project uses it
// happened due to project split
if (delProject.kmsCertificateKeyId) {
await kmsService.deleteInternalKms(delProject.kmsCertificateKeyId, delProject.orgId, tx);
const projectsLinkedToForiegnKey = await projectDAL.find(
{ kmsCertificateKeyId: delProject.kmsCertificateKeyId },
{ tx }
);
if (!projectsLinkedToForiegnKey.length) {
await kmsService.deleteInternalKms(delProject.kmsCertificateKeyId, delProject.orgId, tx);
}
}
if (delProject.kmsSecretManagerKeyId) {
await kmsService.deleteInternalKms(delProject.kmsSecretManagerKeyId, delProject.orgId, tx);
const projectsLinkedToForiegnKey = await projectDAL.find(
{ kmsSecretManagerKeyId: delProject.kmsSecretManagerKeyId },
{ tx }
);
if (!projectsLinkedToForiegnKey.length) {
await kmsService.deleteInternalKms(delProject.kmsSecretManagerKeyId, delProject.orgId, tx);
}
}
// Delete the org membership for the ghost user if it's found.
if (projectGhostUser) {
@@ -544,8 +566,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 +591,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;
};

View File

@@ -1,12 +1,12 @@
import crypto from "node:crypto";
import bcrypt from "bcrypt";
import { z } from "zod";
import { TSecretSharing } from "@app/db/schemas";
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service";
import { BadRequestError, ForbiddenRequestError, NotFoundError, UnauthorizedError } from "@app/lib/errors";
import { SecretSharingAccessType } from "@app/lib/types";
import { isUuidV4 } from "@app/lib/validator";
import { TKmsServiceFactory } from "../kms/kms-service";
import { TOrgDALFactory } from "../org/org-dal";
@@ -28,8 +28,6 @@ type TSecretSharingServiceFactoryDep = {
export type TSecretSharingServiceFactory = ReturnType<typeof secretSharingServiceFactory>;
const isUuidV4 = (uuid: string) => z.string().uuid().safeParse(uuid).success;
export const secretSharingServiceFactory = ({
permissionService,
secretSharingDAL,

View File

@@ -88,7 +88,7 @@ type TSecretServiceFactoryDep = {
secretDAL: TSecretDALFactory;
secretTagDAL: TSecretTagDALFactory;
secretVersionDAL: TSecretVersionDALFactory;
projectDAL: Pick<TProjectDALFactory, "checkProjectUpgradeStatus" | "findProjectBySlug">;
projectDAL: Pick<TProjectDALFactory, "checkProjectUpgradeStatus" | "findProjectBySlug" | "findById">;
projectEnvDAL: Pick<TProjectEnvDALFactory, "findOne">;
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 name 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 name 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,