mirror of
https://github.com/awatertrevi/infisical.git
synced 2026-10-11 20:29:01 +00:00
Add org option to block duplicate destinations on secret syncs
This commit is contained in:
@@ -0,0 +1,27 @@
|
||||
import { Knex } from "knex";
|
||||
|
||||
import { TableName } from "../schemas";
|
||||
|
||||
export async function up(knex: Knex): Promise<void> {
|
||||
const hasOrgBlockDuplicateColumn = await knex.schema.hasColumn(
|
||||
TableName.Organization,
|
||||
"blockDuplicateSecretSyncDestinations"
|
||||
);
|
||||
if (!hasOrgBlockDuplicateColumn) {
|
||||
await knex.schema.table(TableName.Organization, (table) => {
|
||||
table.boolean("blockDuplicateSecretSyncDestinations").notNullable().defaultTo(false);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export async function down(knex: Knex): Promise<void> {
|
||||
const hasOrgBlockDuplicateColumn = await knex.schema.hasColumn(
|
||||
TableName.Organization,
|
||||
"blockDuplicateSecretSyncDestinations"
|
||||
);
|
||||
if (hasOrgBlockDuplicateColumn) {
|
||||
await knex.schema.table(TableName.Organization, (table) => {
|
||||
table.dropColumn("blockDuplicateSecretSyncDestinations");
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -40,7 +40,8 @@ export const OrganizationsSchema = z.object({
|
||||
googleSsoAuthEnforced: z.boolean().default(false),
|
||||
googleSsoAuthLastUsed: z.date().nullable().optional(),
|
||||
parentOrgId: z.string().uuid().nullable().optional(),
|
||||
rootOrgId: z.string().uuid().nullable().optional()
|
||||
rootOrgId: z.string().uuid().nullable().optional(),
|
||||
blockDuplicateSecretSyncDestinations: z.boolean().default(false)
|
||||
});
|
||||
|
||||
export type TOrganizations = z.infer<typeof OrganizationsSchema>;
|
||||
|
||||
@@ -1960,6 +1960,8 @@ export const registerRoutes = async (
|
||||
secretImportDAL,
|
||||
permissionService,
|
||||
appConnectionService,
|
||||
projectDAL,
|
||||
orgDAL,
|
||||
folderDAL,
|
||||
secretSyncQueue,
|
||||
projectBotService,
|
||||
|
||||
@@ -323,7 +323,11 @@ export const registerOrgRouter = async (server: FastifyZodProvider) => {
|
||||
.min(1, "Max Shared Secret view count cannot be lower than 1")
|
||||
.max(1000, "Max Shared Secret view count cannot exceed 1000")
|
||||
.nullable()
|
||||
.optional(),
|
||||
blockDuplicateSecretSyncDestinations: z
|
||||
.boolean()
|
||||
.optional()
|
||||
.describe("Block duplicate secret sync destinations across the organization")
|
||||
}),
|
||||
response: {
|
||||
200: z.object({
|
||||
|
||||
@@ -27,5 +27,6 @@ export const sanitizedOrganizationSchema = OrganizationsSchema.pick({
|
||||
scannerProductEnabled: true,
|
||||
shareSecretsProductEnabled: true,
|
||||
maxSharedSecretLifetime: true,
|
||||
maxSharedSecretViewLimit: true
|
||||
maxSharedSecretViewLimit: true,
|
||||
blockDuplicateSecretSyncDestinations: true
|
||||
});
|
||||
|
||||
@@ -408,7 +408,8 @@ export const orgServiceFactory = ({
|
||||
scannerProductEnabled,
|
||||
shareSecretsProductEnabled,
|
||||
maxSharedSecretLifetime,
|
||||
maxSharedSecretViewLimit
|
||||
maxSharedSecretViewLimit,
|
||||
blockDuplicateSecretSyncDestinations
|
||||
}
|
||||
}: TUpdateOrgDTO) => {
|
||||
const appCfg = getConfig();
|
||||
@@ -592,7 +593,8 @@ export const orgServiceFactory = ({
|
||||
scannerProductEnabled,
|
||||
shareSecretsProductEnabled,
|
||||
maxSharedSecretLifetime,
|
||||
maxSharedSecretViewLimit
|
||||
maxSharedSecretViewLimit,
|
||||
blockDuplicateSecretSyncDestinations
|
||||
});
|
||||
if (!org) throw new NotFoundError({ message: `Organization with ID '${orgId}' not found` });
|
||||
return org;
|
||||
|
||||
@@ -90,6 +90,7 @@ export type TUpdateOrgDTO = {
|
||||
shareSecretsProductEnabled: boolean;
|
||||
maxSharedSecretLifetime: number;
|
||||
maxSharedSecretViewLimit: number | null;
|
||||
blockDuplicateSecretSyncDestinations: boolean;
|
||||
}>;
|
||||
} & TOrgPermission;
|
||||
|
||||
|
||||
@@ -15,6 +15,8 @@ import { BadRequestError, DatabaseError, NotFoundError } from "@app/lib/errors";
|
||||
import { deepEqualSkipFields } from "@app/lib/fn/object";
|
||||
import { OrgServiceActor } from "@app/lib/types";
|
||||
import { TAppConnectionServiceFactory } from "@app/services/app-connection/app-connection-service";
|
||||
import { TOrgDALFactory } from "@app/services/org/org-dal";
|
||||
import { TProjectDALFactory } from "@app/services/project/project-dal";
|
||||
import { TProjectBotServiceFactory } from "@app/services/project-bot/project-bot-service";
|
||||
import { TSecretFolderDALFactory } from "@app/services/secret-folder/secret-folder-dal";
|
||||
import { SecretSync } from "@app/services/secret-sync/secret-sync-enums";
|
||||
@@ -50,6 +52,8 @@ type TSecretSyncServiceFactoryDep = {
|
||||
secretImportDAL: TSecretImportDALFactory;
|
||||
appConnectionService: Pick<TAppConnectionServiceFactory, "validateAppConnectionUsageById">;
|
||||
permissionService: Pick<TPermissionServiceFactory, "getProjectPermission" | "getOrgPermission">;
|
||||
projectDAL: Pick<TProjectDALFactory, "findById">;
|
||||
orgDAL: Pick<TOrgDALFactory, "findById">;
|
||||
projectBotService: Pick<TProjectBotServiceFactory, "getBotKey">;
|
||||
folderDAL: Pick<TSecretFolderDALFactory, "findByProjectId" | "findById" | "findBySecretPath">;
|
||||
keyStore: Pick<TKeyStoreFactory, "getItem">;
|
||||
@@ -68,6 +72,8 @@ export const secretSyncServiceFactory = ({
|
||||
secretImportDAL,
|
||||
permissionService,
|
||||
appConnectionService,
|
||||
projectDAL,
|
||||
orgDAL,
|
||||
projectBotService,
|
||||
secretSyncQueue,
|
||||
keyStore,
|
||||
@@ -225,6 +231,61 @@ export const secretSyncServiceFactory = ({
|
||||
return secretSync as TSecretSync;
|
||||
};
|
||||
|
||||
const checkDuplicateDestination = async (
|
||||
{ destination, destinationConfig, excludeSyncId, projectId }: TCheckDuplicateDestinationDTO,
|
||||
actor: OrgServiceActor
|
||||
) => {
|
||||
const skipFields = SECRET_SYNC_SKIP_FIELDS_MAP[destination];
|
||||
const { permission } = await permissionService.getProjectPermission({
|
||||
actor: actor.type,
|
||||
actorId: actor.id,
|
||||
actorAuthMethod: actor.authMethod,
|
||||
actorOrgId: actor.orgId,
|
||||
actionProjectType: ActionProjectType.SecretManager,
|
||||
projectId
|
||||
});
|
||||
|
||||
ForbiddenError.from(permission).throwUnlessCan(
|
||||
ProjectPermissionSecretSyncActions.Read,
|
||||
ProjectPermissionSub.SecretSyncs
|
||||
);
|
||||
|
||||
if (!destinationConfig || Object.keys(destinationConfig).length === 0) {
|
||||
return { hasDuplicate: false, duplicateProjectId: undefined };
|
||||
}
|
||||
|
||||
try {
|
||||
const existingSyncs = await secretSyncDAL.findByDestinationAndOrgId(destination, actor.orgId);
|
||||
|
||||
const duplicates = existingSyncs.filter((sync) => {
|
||||
if (sync.id === excludeSyncId) {
|
||||
return false;
|
||||
}
|
||||
|
||||
try {
|
||||
const baseFieldsMatch = deepEqualSkipFields(sync.destinationConfig, destinationConfig, skipFields);
|
||||
if (baseFieldsMatch) {
|
||||
return DESTINATION_DUPLICATE_CHECK_MAP[destination](
|
||||
sync.destinationConfig as Record<string, unknown>,
|
||||
destinationConfig
|
||||
);
|
||||
}
|
||||
return false;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
});
|
||||
|
||||
const hasDuplicate = duplicates.length > 0;
|
||||
return {
|
||||
hasDuplicate,
|
||||
duplicateProjectId: hasDuplicate ? duplicates[0].projectId : undefined
|
||||
};
|
||||
} catch (error) {
|
||||
return { hasDuplicate: false, duplicateProjectId: undefined };
|
||||
}
|
||||
};
|
||||
|
||||
const createSecretSync = async (
|
||||
{ projectId, secretPath, environment, ...params }: TCreateSecretSyncDTO,
|
||||
actor: OrgServiceActor
|
||||
@@ -271,6 +332,30 @@ export const secretSyncServiceFactory = ({
|
||||
message: `Could not find folder with path "${secretPath}" in environment "${environment}" for project with ID "${projectId}"`
|
||||
});
|
||||
|
||||
const project = await projectDAL.findById(projectId);
|
||||
if (!project) {
|
||||
throw new NotFoundError({ message: "Project not found" });
|
||||
}
|
||||
|
||||
const organization = await orgDAL.findById(project.orgId);
|
||||
if (organization?.blockDuplicateSecretSyncDestinations) {
|
||||
const duplicateCheck = await checkDuplicateDestination(
|
||||
{
|
||||
destination: params.destination,
|
||||
destinationConfig: params.destinationConfig,
|
||||
projectId
|
||||
},
|
||||
actor
|
||||
);
|
||||
if (duplicateCheck.hasDuplicate) {
|
||||
throw new BadRequestError({
|
||||
message: `A secret sync with this destination already exists${
|
||||
duplicateCheck.duplicateProjectId ? ` in project ${duplicateCheck.duplicateProjectId}` : ""
|
||||
}.`
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
const destinationApp = SECRET_SYNC_CONNECTION_MAP[params.destination];
|
||||
|
||||
// validates permission to connect and app is valid for sync destination
|
||||
@@ -703,61 +788,6 @@ export const secretSyncServiceFactory = ({
|
||||
return updatedSecretSync as TSecretSync;
|
||||
};
|
||||
|
||||
const checkDuplicateDestination = async (
|
||||
{ destination, destinationConfig, excludeSyncId, projectId }: TCheckDuplicateDestinationDTO,
|
||||
actor: OrgServiceActor
|
||||
) => {
|
||||
const skipFields = SECRET_SYNC_SKIP_FIELDS_MAP[destination];
|
||||
const { permission } = await permissionService.getProjectPermission({
|
||||
actor: actor.type,
|
||||
actorId: actor.id,
|
||||
actorAuthMethod: actor.authMethod,
|
||||
actorOrgId: actor.orgId,
|
||||
actionProjectType: ActionProjectType.SecretManager,
|
||||
projectId
|
||||
});
|
||||
|
||||
ForbiddenError.from(permission).throwUnlessCan(
|
||||
ProjectPermissionSecretSyncActions.Read,
|
||||
ProjectPermissionSub.SecretSyncs
|
||||
);
|
||||
|
||||
if (!destinationConfig || Object.keys(destinationConfig).length === 0) {
|
||||
return { hasDuplicate: false, duplicateProjectId: undefined };
|
||||
}
|
||||
|
||||
try {
|
||||
const existingSyncs = await secretSyncDAL.findByDestinationAndOrgId(destination, actor.orgId);
|
||||
|
||||
const duplicates = existingSyncs.filter((sync) => {
|
||||
if (sync.id === excludeSyncId) {
|
||||
return false;
|
||||
}
|
||||
|
||||
try {
|
||||
const baseFieldsMatch = deepEqualSkipFields(sync.destinationConfig, destinationConfig, skipFields);
|
||||
if (baseFieldsMatch) {
|
||||
return DESTINATION_DUPLICATE_CHECK_MAP[destination](
|
||||
sync.destinationConfig as Record<string, unknown>,
|
||||
destinationConfig
|
||||
);
|
||||
}
|
||||
return false;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
});
|
||||
|
||||
const hasDuplicate = duplicates.length > 0;
|
||||
return {
|
||||
hasDuplicate,
|
||||
duplicateProjectId: hasDuplicate ? duplicates[0].projectId : undefined
|
||||
};
|
||||
} catch (error) {
|
||||
return { hasDuplicate: false, duplicateProjectId: undefined };
|
||||
}
|
||||
};
|
||||
|
||||
return {
|
||||
listSecretSyncOptions,
|
||||
listSecretSyncsByProjectId,
|
||||
|
||||
Reference in New Issue
Block a user