Add org option to block duplicate destinations on secret syncs

This commit is contained in:
Carlos Monastyrski
2025-10-24 20:12:35 -03:00
parent 85dfdcc1bc
commit 1722f85e67
18 changed files with 339 additions and 73 deletions
@@ -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");
});
}
}
+2 -1
View File
@@ -40,7 +40,8 @@ export const OrganizationsSchema = z.object({
googleSsoAuthEnforced: z.boolean().default(false), googleSsoAuthEnforced: z.boolean().default(false),
googleSsoAuthLastUsed: z.date().nullable().optional(), googleSsoAuthLastUsed: z.date().nullable().optional(),
parentOrgId: z.string().uuid().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>; export type TOrganizations = z.infer<typeof OrganizationsSchema>;
+2
View File
@@ -1960,6 +1960,8 @@ export const registerRoutes = async (
secretImportDAL, secretImportDAL,
permissionService, permissionService,
appConnectionService, appConnectionService,
projectDAL,
orgDAL,
folderDAL, folderDAL,
secretSyncQueue, secretSyncQueue,
projectBotService, projectBotService,
@@ -323,7 +323,11 @@ export const registerOrgRouter = async (server: FastifyZodProvider) => {
.min(1, "Max Shared Secret view count cannot be lower than 1") .min(1, "Max Shared Secret view count cannot be lower than 1")
.max(1000, "Max Shared Secret view count cannot exceed 1000") .max(1000, "Max Shared Secret view count cannot exceed 1000")
.nullable() .nullable()
.optional(),
blockDuplicateSecretSyncDestinations: z
.boolean()
.optional() .optional()
.describe("Block duplicate secret sync destinations across the organization")
}), }),
response: { response: {
200: z.object({ 200: z.object({
+2 -1
View File
@@ -27,5 +27,6 @@ export const sanitizedOrganizationSchema = OrganizationsSchema.pick({
scannerProductEnabled: true, scannerProductEnabled: true,
shareSecretsProductEnabled: true, shareSecretsProductEnabled: true,
maxSharedSecretLifetime: true, maxSharedSecretLifetime: true,
maxSharedSecretViewLimit: true maxSharedSecretViewLimit: true,
blockDuplicateSecretSyncDestinations: true
}); });
+4 -2
View File
@@ -408,7 +408,8 @@ export const orgServiceFactory = ({
scannerProductEnabled, scannerProductEnabled,
shareSecretsProductEnabled, shareSecretsProductEnabled,
maxSharedSecretLifetime, maxSharedSecretLifetime,
maxSharedSecretViewLimit maxSharedSecretViewLimit,
blockDuplicateSecretSyncDestinations
} }
}: TUpdateOrgDTO) => { }: TUpdateOrgDTO) => {
const appCfg = getConfig(); const appCfg = getConfig();
@@ -592,7 +593,8 @@ export const orgServiceFactory = ({
scannerProductEnabled, scannerProductEnabled,
shareSecretsProductEnabled, shareSecretsProductEnabled,
maxSharedSecretLifetime, maxSharedSecretLifetime,
maxSharedSecretViewLimit maxSharedSecretViewLimit,
blockDuplicateSecretSyncDestinations
}); });
if (!org) throw new NotFoundError({ message: `Organization with ID '${orgId}' not found` }); if (!org) throw new NotFoundError({ message: `Organization with ID '${orgId}' not found` });
return org; return org;
+1
View File
@@ -90,6 +90,7 @@ export type TUpdateOrgDTO = {
shareSecretsProductEnabled: boolean; shareSecretsProductEnabled: boolean;
maxSharedSecretLifetime: number; maxSharedSecretLifetime: number;
maxSharedSecretViewLimit: number | null; maxSharedSecretViewLimit: number | null;
blockDuplicateSecretSyncDestinations: boolean;
}>; }>;
} & TOrgPermission; } & TOrgPermission;
@@ -15,6 +15,8 @@ import { BadRequestError, DatabaseError, NotFoundError } from "@app/lib/errors";
import { deepEqualSkipFields } from "@app/lib/fn/object"; import { deepEqualSkipFields } from "@app/lib/fn/object";
import { OrgServiceActor } from "@app/lib/types"; import { OrgServiceActor } from "@app/lib/types";
import { TAppConnectionServiceFactory } from "@app/services/app-connection/app-connection-service"; 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 { TProjectBotServiceFactory } from "@app/services/project-bot/project-bot-service";
import { TSecretFolderDALFactory } from "@app/services/secret-folder/secret-folder-dal"; import { TSecretFolderDALFactory } from "@app/services/secret-folder/secret-folder-dal";
import { SecretSync } from "@app/services/secret-sync/secret-sync-enums"; import { SecretSync } from "@app/services/secret-sync/secret-sync-enums";
@@ -50,6 +52,8 @@ type TSecretSyncServiceFactoryDep = {
secretImportDAL: TSecretImportDALFactory; secretImportDAL: TSecretImportDALFactory;
appConnectionService: Pick<TAppConnectionServiceFactory, "validateAppConnectionUsageById">; appConnectionService: Pick<TAppConnectionServiceFactory, "validateAppConnectionUsageById">;
permissionService: Pick<TPermissionServiceFactory, "getProjectPermission" | "getOrgPermission">; permissionService: Pick<TPermissionServiceFactory, "getProjectPermission" | "getOrgPermission">;
projectDAL: Pick<TProjectDALFactory, "findById">;
orgDAL: Pick<TOrgDALFactory, "findById">;
projectBotService: Pick<TProjectBotServiceFactory, "getBotKey">; projectBotService: Pick<TProjectBotServiceFactory, "getBotKey">;
folderDAL: Pick<TSecretFolderDALFactory, "findByProjectId" | "findById" | "findBySecretPath">; folderDAL: Pick<TSecretFolderDALFactory, "findByProjectId" | "findById" | "findBySecretPath">;
keyStore: Pick<TKeyStoreFactory, "getItem">; keyStore: Pick<TKeyStoreFactory, "getItem">;
@@ -68,6 +72,8 @@ export const secretSyncServiceFactory = ({
secretImportDAL, secretImportDAL,
permissionService, permissionService,
appConnectionService, appConnectionService,
projectDAL,
orgDAL,
projectBotService, projectBotService,
secretSyncQueue, secretSyncQueue,
keyStore, keyStore,
@@ -225,6 +231,61 @@ export const secretSyncServiceFactory = ({
return secretSync as TSecretSync; 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 ( const createSecretSync = async (
{ projectId, secretPath, environment, ...params }: TCreateSecretSyncDTO, { projectId, secretPath, environment, ...params }: TCreateSecretSyncDTO,
actor: OrgServiceActor 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}"` 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]; const destinationApp = SECRET_SYNC_CONNECTION_MAP[params.destination];
// validates permission to connect and app is valid for sync destination // validates permission to connect and app is valid for sync destination
@@ -703,61 +788,6 @@ export const secretSyncServiceFactory = ({
return updatedSecretSync as TSecretSync; 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 { return {
listSecretSyncOptions, listSecretSyncOptions,
listSecretSyncsByProjectId, listSecretSyncsByProjectId,
@@ -8,13 +8,14 @@ import { twMerge } from "tailwind-merge";
import { createNotification } from "@app/components/notifications"; import { createNotification } from "@app/components/notifications";
import { Button, FormControl, Switch } from "@app/components/v2"; import { Button, FormControl, Switch } from "@app/components/v2";
import { useProject } from "@app/context"; import { useOrganization, useProject } from "@app/context";
import { SECRET_SYNC_MAP } from "@app/helpers/secretSyncs"; import { SECRET_SYNC_MAP } from "@app/helpers/secretSyncs";
import { import {
SecretSync, SecretSync,
SecretSyncInitialSyncBehavior, SecretSyncInitialSyncBehavior,
TSecretSync, TSecretSync,
useCreateSecretSync, useCreateSecretSync,
useDuplicateDestinationCheck,
useSecretSyncOption useSecretSyncOption
} from "@app/hooks/api/secretSyncs"; } from "@app/hooks/api/secretSyncs";
@@ -48,6 +49,7 @@ export const CreateSecretSyncForm = ({
}: Props) => { }: Props) => {
const createSecretSync = useCreateSecretSync(); const createSecretSync = useCreateSecretSync();
const { currentProject } = useProject(); const { currentProject } = useProject();
const { currentOrg } = useOrganization();
const { name: destinationName } = SECRET_SYNC_MAP[destination]; const { name: destinationName } = SECRET_SYNC_MAP[destination];
const [showConfirmation, setShowConfirmation] = useState(false); const [showConfirmation, setShowConfirmation] = useState(false);
@@ -106,11 +108,20 @@ export const CreateSecretSyncForm = ({
setSelectedTabIndex((prev) => prev - 1); setSelectedTabIndex((prev) => prev - 1);
}; };
const { handleSubmit, trigger, control } = formMethods; const { handleSubmit, trigger, control, watch } = formMethods;
const { hasDuplicate } = useDuplicateDestinationCheck({
destination,
projectId: currentProject?.id || "",
enabled: true,
destinationConfig: watch("destinationConfig")
});
const isStepValid = async (index: number) => trigger(FORM_TABS[index].fields); const isStepValid = async (index: number) => trigger(FORM_TABS[index].fields);
const isFinalStep = selectedTabIndex === FORM_TABS.length - 1; const isFinalStep = selectedTabIndex === FORM_TABS.length - 1;
const isCreateButtonDisabled =
isFinalStep && hasDuplicate && currentOrg?.blockDuplicateSecretSyncDestinations;
const handleNext = async () => { const handleNext = async () => {
if (isFinalStep) { if (isFinalStep) {
@@ -245,7 +256,11 @@ export const CreateSecretSyncForm = ({
</FormProvider> </FormProvider>
<div className="flex w-full flex-row-reverse justify-between gap-4 pt-4"> <div className="flex w-full flex-row-reverse justify-between gap-4 pt-4">
<Button onClick={handleNext} colorSchema="secondary"> <Button
onClick={handleNext}
colorSchema="secondary"
isDisabled={isCreateButtonDisabled || (isFinalStep && hasDuplicate)}
>
{isFinalStep ? "Create Sync" : "Next"} {isFinalStep ? "Create Sync" : "Next"}
</Button> </Button>
{selectedTabIndex > 0 && ( {selectedTabIndex > 0 && (
@@ -6,7 +6,7 @@ import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { GenericFieldLabel } from "@app/components/secret-syncs"; import { GenericFieldLabel } from "@app/components/secret-syncs";
import { TSecretSyncForm } from "@app/components/secret-syncs/forms/schemas"; import { TSecretSyncForm } from "@app/components/secret-syncs/forms/schemas";
import { Badge } from "@app/components/v2"; import { Badge } from "@app/components/v2";
import { useProject } from "@app/context"; import { useOrganization, useProject } from "@app/context";
import { SECRET_SYNC_INITIAL_SYNC_BEHAVIOR_MAP, SECRET_SYNC_MAP } from "@app/helpers/secretSyncs"; import { SECRET_SYNC_INITIAL_SYNC_BEHAVIOR_MAP, SECRET_SYNC_MAP } from "@app/helpers/secretSyncs";
import { SecretSync, useDuplicateDestinationCheck } from "@app/hooks/api/secretSyncs"; import { SecretSync, useDuplicateDestinationCheck } from "@app/hooks/api/secretSyncs";
@@ -51,6 +51,7 @@ import { ZabbixSyncReviewFields } from "./ZabbixSyncReviewFields";
export const SecretSyncReviewFields = () => { export const SecretSyncReviewFields = () => {
const { watch } = useFormContext<TSecretSyncForm>(); const { watch } = useFormContext<TSecretSyncForm>();
const { currentProject } = useProject(); const { currentProject } = useProject();
const { currentOrg } = useOrganization();
let DestinationFieldsComponent: ReactNode; let DestinationFieldsComponent: ReactNode;
let AdditionalSyncOptionsFieldsComponent: ReactNode; let AdditionalSyncOptionsFieldsComponent: ReactNode;
@@ -193,18 +194,50 @@ export const SecretSyncReviewFields = () => {
{isChecking && <span className="text-xs text-mineshaft-400">Checking...</span>} {isChecking && <span className="text-xs text-mineshaft-400">Checking...</span>}
</div> </div>
{hasDuplicate && ( {hasDuplicate && (
<div className="mb-2 flex items-start rounded-md border border-yellow-600 bg-yellow-900/20 px-3 py-2"> <div
<div className="flex text-sm text-yellow-100"> className={`mb-2 flex items-start rounded-md border px-3 py-2 ${
<FontAwesomeIcon icon={faWarning} className="mt-1 mr-2 text-yellow-600" /> currentOrg?.blockDuplicateSecretSyncDestinations
? "border-red-600 bg-red-900/20"
: "border-yellow-600 bg-yellow-900/20"
}`}
>
<div
className={`flex text-sm ${
currentOrg?.blockDuplicateSecretSyncDestinations
? "text-red-100"
: "text-yellow-100"
}`}
>
<FontAwesomeIcon
icon={faWarning}
className={`mt-1 mr-2 ${
currentOrg?.blockDuplicateSecretSyncDestinations
? "text-red-600"
: "text-yellow-600"
}`}
/>
<div> <div>
<p> <p>
Another secret sync in your organization is already configured with the same {currentOrg?.blockDuplicateSecretSyncDestinations
destination. This may lead to conflicts or unexpected behavior. ? "Another secret sync in your organization is already configured with the same destination. This organization has blocking duplicate destinations enabled."
: "Another secret sync in your organization is already configured with the same destination. This may lead to conflicts or unexpected behavior."}
</p> </p>
{duplicateProjectId && ( {duplicateProjectId && (
<p className="mt-1 text-xs text-yellow-200"> <p
className={`mt-1 text-xs ${
currentOrg?.blockDuplicateSecretSyncDestinations
? "text-red-200"
: "text-yellow-200"
}`}
>
Duplicate found in project ID:{" "} Duplicate found in project ID:{" "}
<code className="rounded-sm bg-yellow-800/50 px-1 py-0.5"> <code
className={`rounded-sm px-1 py-0.5 ${
currentOrg?.blockDuplicateSecretSyncDestinations
? "bg-red-800/50"
: "bg-yellow-800/50"
}`}
>
{duplicateProjectId} {duplicateProjectId}
</code> </code>
</p> </p>
@@ -125,7 +125,8 @@ export const useUpdateOrg = () => {
scannerProductEnabled, scannerProductEnabled,
shareSecretsProductEnabled, shareSecretsProductEnabled,
maxSharedSecretLifetime, maxSharedSecretLifetime,
maxSharedSecretViewLimit maxSharedSecretViewLimit,
blockDuplicateSecretSyncDestinations
}) => { }) => {
return apiRequest.patch(`/api/v1/organization/${orgId}`, { return apiRequest.patch(`/api/v1/organization/${orgId}`, {
name, name,
@@ -146,7 +147,8 @@ export const useUpdateOrg = () => {
scannerProductEnabled, scannerProductEnabled,
shareSecretsProductEnabled, shareSecretsProductEnabled,
maxSharedSecretLifetime, maxSharedSecretLifetime,
maxSharedSecretViewLimit maxSharedSecretViewLimit,
blockDuplicateSecretSyncDestinations
}); });
}, },
onSuccess: () => { onSuccess: () => {
@@ -29,6 +29,7 @@ export type Organization = {
shareSecretsProductEnabled: boolean; shareSecretsProductEnabled: boolean;
maxSharedSecretLifetime: number; maxSharedSecretLifetime: number;
maxSharedSecretViewLimit: number | null; maxSharedSecretViewLimit: number | null;
blockDuplicateSecretSyncDestinations: boolean;
}; };
export type UpdateOrgDTO = { export type UpdateOrgDTO = {
@@ -52,6 +53,7 @@ export type UpdateOrgDTO = {
shareSecretsProductEnabled?: boolean; shareSecretsProductEnabled?: boolean;
maxSharedSecretViewLimit?: number | null; maxSharedSecretViewLimit?: number | null;
maxSharedSecretLifetime?: number; maxSharedSecretLifetime?: number;
blockDuplicateSecretSyncDestinations?: boolean;
}; };
export type BillingDetails = { export type BillingDetails = {
@@ -0,0 +1,64 @@
import { useState } from "react";
import { createNotification } from "@app/components/notifications";
import { OrgPermissionCan } from "@app/components/permissions";
import { Checkbox } from "@app/components/v2";
import { OrgPermissionActions, OrgPermissionSubjects, useOrganization } from "@app/context";
import { useUpdateOrg } from "@app/hooks/api/organization/queries";
export const BlockDuplicateSecretSyncDestinationsSection = () => {
const { currentOrg } = useOrganization();
const { mutateAsync: updateOrg } = useUpdateOrg();
const [isLoading, setIsLoading] = useState(false);
const handleToggle = async (state: boolean) => {
setIsLoading(true);
try {
if (!currentOrg?.id) {
setIsLoading(false);
return;
}
await updateOrg({
orgId: currentOrg.id,
blockDuplicateSecretSyncDestinations: state
});
createNotification({
text: `Successfully ${state ? "enabled" : "disabled"} blocking duplicate secret sync destinations for this organization`,
type: "success"
});
} catch (err) {
console.error(err);
createNotification({
text: "Failed to update blocking duplicate secret sync destinations setting for this organization",
type: "error"
});
} finally {
setIsLoading(false);
}
};
return (
<div className="mb-6 rounded-lg border border-mineshaft-600 bg-mineshaft-900 p-4">
<p className="mb-3 text-xl font-medium">Block Duplicate Secret Sync Destinations</p>
<OrgPermissionCan I={OrgPermissionActions.Edit} a={OrgPermissionSubjects.Settings}>
{(isAllowed) => (
<div className="w-max">
<Checkbox
id="blockDuplicateSecretSyncDestinations"
isDisabled={!isAllowed || isLoading}
isChecked={currentOrg?.blockDuplicateSecretSyncDestinations ?? false}
onCheckedChange={(state) => handleToggle(state as boolean)}
>
This feature prevents creating secret syncs with destinations that are already in use
by other syncs in your organization.
</Checkbox>
</div>
)}
</OrgPermissionCan>
</div>
);
};
@@ -0,0 +1 @@
export { BlockDuplicateSecretSyncDestinationsSection } from "./BlockDuplicateSecretSyncDestinationsSection";
@@ -0,0 +1,72 @@
import { useState } from "react";
import { createNotification } from "@app/components/notifications";
import { OrgPermissionCan } from "@app/components/permissions";
import { Switch } from "@app/components/v2";
import { OrgPermissionActions, OrgPermissionSubjects, useOrganization } from "@app/context";
import { useUpdateOrg } from "@app/hooks/api/organization/queries";
export const OrgProductSettingsTab = () => {
const { currentOrg } = useOrganization();
const { mutateAsync: updateOrg } = useUpdateOrg();
const [isLoading, setIsLoading] = useState(false);
const handleToggle = async (state: boolean) => {
setIsLoading(true);
try {
if (!currentOrg?.id) {
setIsLoading(false);
return;
}
await updateOrg({
orgId: currentOrg.id,
blockDuplicateSecretSyncDestinations: state
});
createNotification({
text: `Successfully ${state ? "enabled" : "disabled"} blocking duplicate secret sync destinations for this organization`,
type: "success"
});
} catch (err) {
console.error(err);
createNotification({
text: "Failed to update blocking duplicate secret sync destinations setting for this organization",
type: "error"
});
} finally {
setIsLoading(false);
}
};
return (
<div className="mb-6 rounded-lg border border-mineshaft-600 bg-mineshaft-900 p-6">
<div className="mb-6">
<h2 className="text-xl font-medium text-mineshaft-100">Secrets Management</h2>
</div>
<div className="flex items-center justify-between">
<div>
<h3 className="mb-2 text-lg font-medium text-mineshaft-100">
Block Duplicate Secret Sync Destinations
</h3>
<p className="text-sm text-mineshaft-400">
When enabled, this setting prevents the creation of multiple sync configurations
pointing to the same destination.
</p>
</div>
<OrgPermissionCan I={OrgPermissionActions.Edit} a={OrgPermissionSubjects.Settings}>
{(isAllowed) => (
<Switch
id="blockDuplicateSecretSyncDestinations"
isDisabled={!isAllowed || isLoading}
isChecked={currentOrg?.blockDuplicateSecretSyncDestinations ?? false}
onCheckedChange={(state) => handleToggle(state as boolean)}
/>
)}
</OrgPermissionCan>
</div>
</div>
);
};
@@ -0,0 +1 @@
export { OrgProductSettingsTab } from "./OrgProductSettingsTab";
@@ -10,6 +10,7 @@ import { ExternalMigrationsTab } from "../ExternalMigrationsTab";
import { KmipTab } from "../KmipTab/OrgKmipTab"; import { KmipTab } from "../KmipTab/OrgKmipTab";
import { OrgEncryptionTab } from "../OrgEncryptionTab"; import { OrgEncryptionTab } from "../OrgEncryptionTab";
import { OrgGeneralTab } from "../OrgGeneralTab"; import { OrgGeneralTab } from "../OrgGeneralTab";
import { OrgProductSettingsTab } from "../OrgProductSettingsTab";
import { OrgProvisioningTab } from "../OrgProvisioningTab"; import { OrgProvisioningTab } from "../OrgProvisioningTab";
import { OrgSecurityTab } from "../OrgSecurityTab"; import { OrgSecurityTab } from "../OrgSecurityTab";
import { OrgSsoTab } from "../OrgSsoTab"; import { OrgSsoTab } from "../OrgSsoTab";
@@ -63,7 +64,12 @@ export const OrgTabGroup = () => {
key: "project-templates", key: "project-templates",
component: ProjectTemplatesTab component: ProjectTemplatesTab
}, },
{ name: "KMIP", key: "kmip", component: KmipTab } { name: "KMIP", key: "kmip", component: KmipTab },
{
name: "Product Enforcements",
key: "product-enforcements",
component: OrgProductSettingsTab
}
]; ];
const [selectedTab, setSelectedTab] = useState(search.selectedTab || tabs[0].key); const [selectedTab, setSelectedTab] = useState(search.selectedTab || tabs[0].key);
@@ -1 +1,3 @@
export { BlockDuplicateSecretSyncDestinationsSection } from "./BlockDuplicateSecretSyncDestinationsSection";
export { OrgProductSettingsTab } from "./OrgProductSettingsTab";
export { OrgTabGroup } from "./OrgTabGroup"; export { OrgTabGroup } from "./OrgTabGroup";