feat(secrets-ui): improve warning message and logic for secret-sync on secret imports

This commit is contained in:
carlosmonastyrski
2025-04-29 10:15:53 -03:00
parent 4e4b1b689b
commit e959ed7fab
15 changed files with 71 additions and 36 deletions
+1
View File
@@ -1541,6 +1541,7 @@ export const registerRoutes = async (
const secretSyncService = secretSyncServiceFactory({ const secretSyncService = secretSyncServiceFactory({
secretSyncDAL, secretSyncDAL,
secretImportDAL,
permissionService, permissionService,
appConnectionService, appConnectionService,
folderDAL, folderDAL,
@@ -155,7 +155,7 @@ export const registerDashboardRouter = async (server: FastifyZodProvider) => {
.object({ .object({
secretId: z.string(), secretId: z.string(),
referencedSecretKey: z.string(), referencedSecretKey: z.string(),
referencedSecretId: z.string() referencedSecretEnv: z.string()
}) })
.array() .array()
.optional() .optional()
@@ -658,7 +658,7 @@ export const registerDashboardRouter = async (server: FastifyZodProvider) => {
.object({ .object({
secretId: z.string(), secretId: z.string(),
referencedSecretKey: z.string(), referencedSecretKey: z.string(),
referencedSecretId: z.string() referencedSecretEnv: z.string()
}) })
.array() .array()
.optional() .optional()
@@ -945,7 +945,7 @@ export const registerDashboardRouter = async (server: FastifyZodProvider) => {
const usedBySecretSyncs = secretSyncs.map((sync) => ({ const usedBySecretSyncs = secretSyncs.map((sync) => ({
name: sync.name, name: sync.name,
destination: sync.destination, destination: sync.destination,
environment environment: sync.environment?.name || environment
})); }));
if (secrets?.length || secretRotations?.length) { if (secrets?.length || secretRotations?.length) {
@@ -171,6 +171,19 @@ export const secretImportDALFactory = (db: TDbClient) => {
} }
}; };
const getFolderImports = async (secretPath: string, environmentId: string, tx?: Knex) => {
try {
const folderImports = await (tx || db.replicaNode())(TableName.SecretImport)
.where({ importPath: secretPath, importEnv: environmentId })
.join(TableName.SecretFolder, `${TableName.SecretImport}.folderId`, `${TableName.SecretFolder}.id`)
.join(TableName.Environment, `${TableName.SecretFolder}.envId`, `${TableName.Environment}.id`)
.select(db.ref("id").withSchema(TableName.SecretFolder).as("folderId"));
return folderImports;
} catch (error) {
throw new DatabaseError({ error, name: "get secret imports" });
}
};
const getFolderIsImportedBy = async ( const getFolderIsImportedBy = async (
secretPath: string, secretPath: string,
environmentId: string, environmentId: string,
@@ -204,7 +217,7 @@ export const secretImportDALFactory = (db: TDbClient) => {
db.ref("slug").withSchema(TableName.Environment).as("envSlug"), db.ref("slug").withSchema(TableName.Environment).as("envSlug"),
db.ref("id").withSchema(TableName.SecretFolder).as("folderId"), db.ref("id").withSchema(TableName.SecretFolder).as("folderId"),
db.ref("secretKey").withSchema(TableName.SecretReferenceV2).as("referencedSecretKey"), db.ref("secretKey").withSchema(TableName.SecretReferenceV2).as("referencedSecretKey"),
db.ref("id").withSchema(TableName.SecretV2).as("referencedSecretId") db.ref("environment").withSchema(TableName.SecretReferenceV2).as("referencedSecretEnv")
); );
const folderResults = folderImports.map(({ envName, envSlug, folderName, folderId }) => ({ const folderResults = folderImports.map(({ envName, envSlug, folderName, folderId }) => ({
@@ -215,14 +228,14 @@ export const secretImportDALFactory = (db: TDbClient) => {
})); }));
const secretResults = secretReferences.map( const secretResults = secretReferences.map(
({ envName, envSlug, secretId, folderName, folderId, referencedSecretKey, referencedSecretId }) => ({ ({ envName, envSlug, secretId, folderName, folderId, referencedSecretKey, referencedSecretEnv }) => ({
envName, envName,
envSlug, envSlug,
secretId, secretId,
folderName, folderName,
folderId, folderId,
referencedSecretKey, referencedSecretKey,
referencedSecretId referencedSecretEnv
}) })
); );
@@ -237,7 +250,7 @@ export const secretImportDALFactory = (db: TDbClient) => {
secrets: { secrets: {
secretId: string; secretId: string;
referencedSecretKey: string; referencedSecretKey: string;
referencedSecretId: string; referencedSecretEnv: string;
}[]; }[];
folderId: string; folderId: string;
folderImported: boolean; folderImported: boolean;
@@ -270,7 +283,7 @@ export const secretImportDALFactory = (db: TDbClient) => {
{ {
secretId: item.secretId, secretId: item.secretId,
referencedSecretKey: item.referencedSecretKey, referencedSecretKey: item.referencedSecretKey,
referencedSecretId: item.referencedSecretId referencedSecretEnv: item.referencedSecretEnv
} }
]; ];
} else { } else {
@@ -316,6 +329,7 @@ export const secretImportDALFactory = (db: TDbClient) => {
findLastImportPosition, findLastImportPosition,
updateAllPosition, updateAllPosition,
getProjectImportCount, getProjectImportCount,
getFolderIsImportedBy getFolderIsImportedBy,
getFolderImports
}; };
}; };
@@ -878,7 +878,7 @@ export const secretImportServiceFactory = ({
.map((otherSecret) => ({ .map((otherSecret) => ({
secretId: secret.secretKey, secretId: secret.secretKey,
referencedSecretKey: otherSecret.secretKey, referencedSecretKey: otherSecret.secretKey,
referencedSecretId: otherSecret.id referencedSecretEnv: environment
})); }));
}) || []; }) || [];
if (locallyReferenced.length > 0) { if (locallyReferenced.length > 0) {
@@ -56,12 +56,12 @@ export type FolderResult = {
export type SecretResult = { export type SecretResult = {
secretId: string; secretId: string;
referencedSecretKey: string; referencedSecretKey: string;
referencedSecretId: string; referencedSecretEnv: string;
} & FolderResult; } & FolderResult;
export type FolderInfo = { export type FolderInfo = {
folderName: string; folderName: string;
secrets?: { secretId: string; referencedSecretKey: string; referencedSecretId: string }[]; secrets?: { secretId: string; referencedSecretKey: string; referencedSecretEnv: string }[];
folderId: string; folderId: string;
folderImported: boolean; folderImported: boolean;
envSlug?: string; envSlug?: string;
@@ -32,12 +32,14 @@ import {
TUpdateSecretSyncDTO TUpdateSecretSyncDTO
} from "@app/services/secret-sync/secret-sync-types"; } from "@app/services/secret-sync/secret-sync-types";
import { TSecretImportDALFactory } from "../secret-import/secret-import-dal";
import { TSecretSyncDALFactory } from "./secret-sync-dal"; import { TSecretSyncDALFactory } from "./secret-sync-dal";
import { SECRET_SYNC_CONNECTION_MAP, SECRET_SYNC_NAME_MAP } from "./secret-sync-maps"; import { SECRET_SYNC_CONNECTION_MAP, SECRET_SYNC_NAME_MAP } from "./secret-sync-maps";
import { TSecretSyncQueueFactory } from "./secret-sync-queue"; import { TSecretSyncQueueFactory } from "./secret-sync-queue";
type TSecretSyncServiceFactoryDep = { type TSecretSyncServiceFactoryDep = {
secretSyncDAL: TSecretSyncDALFactory; secretSyncDAL: TSecretSyncDALFactory;
secretImportDAL: TSecretImportDALFactory;
appConnectionService: Pick<TAppConnectionServiceFactory, "connectAppConnectionById">; appConnectionService: Pick<TAppConnectionServiceFactory, "connectAppConnectionById">;
permissionService: Pick<TPermissionServiceFactory, "getProjectPermission" | "getOrgPermission">; permissionService: Pick<TPermissionServiceFactory, "getProjectPermission" | "getOrgPermission">;
projectBotService: Pick<TProjectBotServiceFactory, "getBotKey">; projectBotService: Pick<TProjectBotServiceFactory, "getBotKey">;
@@ -54,6 +56,7 @@ export type TSecretSyncServiceFactory = ReturnType<typeof secretSyncServiceFacto
export const secretSyncServiceFactory = ({ export const secretSyncServiceFactory = ({
secretSyncDAL, secretSyncDAL,
folderDAL, folderDAL,
secretImportDAL,
permissionService, permissionService,
appConnectionService, appConnectionService,
projectBotService, projectBotService,
@@ -87,7 +90,7 @@ export const secretSyncServiceFactory = ({
}; };
const listSecretSyncsBySecretPath = async ( const listSecretSyncsBySecretPath = async (
{ projectId, secretPath, environment, destination }: TListSecretSyncsByFolderId, { projectId, secretPath, environment }: TListSecretSyncsByFolderId,
actor: OrgServiceActor actor: OrgServiceActor
) => { ) => {
const { permission } = await permissionService.getProjectPermission({ const { permission } = await permissionService.getProjectPermission({
@@ -106,9 +109,12 @@ export const secretSyncServiceFactory = ({
const folder = await folderDAL.findBySecretPath(projectId, environment, secretPath); const folder = await folderDAL.findBySecretPath(projectId, environment, secretPath);
if (!folder) return []; if (!folder) return [];
const folderImports = await secretImportDAL.getFolderImports(secretPath, folder.envId);
const secretSyncs = await secretSyncDAL.find({ const secretSyncs = await secretSyncDAL.find({
...(destination && { destination }), $in: {
folderId: folder.id folderId: folderImports.map((folderImport) => folderImport.folderId).concat(folder.id)
}
}); });
return secretSyncs as TSecretSync[]; return secretSyncs as TSecretSync[];
@@ -33,7 +33,7 @@ export const DeleteActionModal = ({
buttonText = "Delete", buttonText = "Delete",
formContent, formContent,
deletionMessage, deletionMessage,
buttonColorSchema, buttonColorSchema = "danger",
children children
}: Props): JSX.Element => { }: Props): JSX.Element => {
const [inputData, setInputData] = useState(""); const [inputData, setInputData] = useState("");
@@ -69,7 +69,7 @@ export const DeleteActionModal = ({
<div className="mx-2 flex items-center"> <div className="mx-2 flex items-center">
<Button <Button
className="mr-4" className="mr-4"
colorSchema={buttonColorSchema || "danger"} colorSchema={buttonColorSchema}
isDisabled={!(deleteKey === inputData) || isLoading} isDisabled={!(deleteKey === inputData) || isLoading}
onClick={onDelete} onClick={onDelete}
isLoading={isLoading} isLoading={isLoading}
+1 -1
View File
@@ -51,7 +51,7 @@ export type ProjectSecretsImportedBy = {
environment: { name: string; slug: string }; environment: { name: string; slug: string };
folders: { folders: {
name: string; name: string;
secrets?: { secretId: string; referencedSecretKey: string; referencedSecretId: string }[]; secrets?: { secretId: string; referencedSecretKey: string; referencedSecretEnv: string }[];
isImported: boolean; isImported: boolean;
}[]; }[];
}; };
@@ -769,7 +769,7 @@ export const OverviewPage = () => {
const uniqueSecrets: { const uniqueSecrets: {
secretId: string; secretId: string;
referencedSecretKey: string; referencedSecretKey: string;
referencedSecretId: string; referencedSecretEnv: string;
}[] = []; }[] = [];
const secretIds = new Set<string>(); const secretIds = new Set<string>();
@@ -59,7 +59,7 @@ type Props = {
environment: { name: string; slug: string }; environment: { name: string; slug: string };
folders: { folders: {
name: string; name: string;
secrets?: { secretId: string; referencedSecretKey: string; referencedSecretId: string }[]; secrets?: { secretId: string; referencedSecretKey: string; referencedSecretEnv: string }[];
isImported: boolean; isImported: boolean;
}[]; }[];
}[]; }[];
@@ -133,7 +133,10 @@ export const SecretEditRow = ({
importedBy && importedBy &&
importedBy.some(({ folders }) => importedBy.some(({ folders }) =>
folders?.some(({ secrets }) => folders?.some(({ secrets }) =>
secrets?.some(({ referencedSecretId }) => referencedSecretId === secretId) secrets?.some(
({ referencedSecretKey, referencedSecretEnv }) =>
referencedSecretKey === secretName && referencedSecretEnv === environment
)
) )
) )
) { ) {
@@ -54,7 +54,7 @@ type Props = {
environment: { name: string; slug: string }; environment: { name: string; slug: string };
folders: { folders: {
name: string; name: string;
secrets?: { secretId: string; referencedSecretKey: string; referencedSecretId: string }[]; secrets?: { secretId: string; referencedSecretKey: string; referencedSecretEnv: string }[];
isImported: boolean; isImported: boolean;
}[]; }[];
}[]; }[];
@@ -118,7 +118,7 @@ type Props = {
environment: { name: string; slug: string }; environment: { name: string; slug: string };
folders: { folders: {
name: string; name: string;
secrets?: { secretId: string; referencedSecretKey: string; referencedSecretId: string }[]; secrets?: { secretId: string; referencedSecretKey: string; referencedSecretEnv: string }[];
isImported: boolean; isImported: boolean;
}[]; }[];
}[]; }[];
@@ -26,7 +26,7 @@ interface CollapsibleSecretImportsProps {
environment: { name: string; slug: string }; environment: { name: string; slug: string };
folders: { folders: {
name: string; name: string;
secrets?: { secretId: string; referencedSecretKey: string; referencedSecretId: string }[]; secrets?: { secretId: string; referencedSecretKey: string; referencedSecretEnv: string }[];
isImported: boolean; isImported: boolean;
}[]; }[];
}[]; }[];
@@ -179,15 +179,27 @@ export const CollapsibleSecretImports: React.FC<CollapsibleSecretImportsProps> =
return null; return null;
} }
const alertColors = onlyReferences
? {
border: "border-yellow-700/30",
bg: "bg-yellow-900/20",
text: "text-yellow-500"
}
: {
border: "border-red-700/30",
bg: "bg-red-900/20",
text: "text-red-500"
};
return ( return (
<div className="mb-4 w-full"> <div className="mb-4 w-full">
<div className="mb-4 rounded-md border border-red-700/30 bg-red-900/20"> <div className={`mb-4 rounded-md border ${alertColors.border} ${alertColors.bg}`}>
<div className="flex items-start gap-3 p-4"> <div className="flex items-start gap-3 p-4">
<div className="mt-0.5 flex-shrink-0 text-red-500"> <div className={`mt-0.5 flex-shrink-0 ${alertColors.text}`}>
<FontAwesomeIcon icon={faWarning} className="h-5 w-5" aria-hidden="true" /> <FontAwesomeIcon icon={faWarning} className="h-5 w-5" aria-hidden="true" />
</div> </div>
<div className="w-full"> <div className="w-full">
<p className="text-sm font-semibold text-red-500"> <p className={`text-sm font-semibold ${alertColors.text}`}>
The following resources will be affected by this change The following resources will be affected by this change
</p> </p>
</div> </div>
@@ -81,7 +81,7 @@ type Props = {
environment: { name: string; slug: string }; environment: { name: string; slug: string };
folders: { folders: {
name: string; name: string;
secrets?: { secretId: string; referencedSecretKey: string; referencedSecretId: string }[]; secrets?: { secretId: string; referencedSecretKey: string; referencedSecretEnv: string }[];
isImported: boolean; isImported: boolean;
}[]; }[];
}[]; }[];
@@ -227,13 +227,12 @@ export const SecretItem = memo(
}; };
const handleFormSubmit = async (data: TFormSchema) => { const handleFormSubmit = async (data: TFormSchema) => {
const hasDirectReferences = const hasDirectReferences = importedBy?.some(({ folders }) =>
importedBy && folders?.some(({ secrets }) =>
importedBy.some(({ folders }) => secrets?.some(({ referencedSecretKey }) => referencedSecretKey === secret.key)
folders?.some(({ secrets }) => )
secrets?.some(({ referencedSecretId }) => referencedSecretId === secret.id) );
)
);
if (hasDirectReferences) { if (hasDirectReferences) {
handlePopUpOpen("editSecret", data); handlePopUpOpen("editSecret", data);
return; return;
@@ -34,7 +34,7 @@ type Props = {
environment: { name: string; slug: string }; environment: { name: string; slug: string };
folders: { folders: {
name: string; name: string;
secrets?: { secretId: string; referencedSecretKey: string; referencedSecretId: string }[]; secrets?: { secretId: string; referencedSecretKey: string; referencedSecretEnv: string }[];
isImported: boolean; isImported: boolean;
}[]; }[];
}[]; }[];