feat: updated ui to reflect secret import and approval

This commit is contained in:
Akhil Mohan
2024-05-31 23:10:01 +05:30
committed by =
parent c9d6c5c5f7
commit 51c1487ed1
17 changed files with 103 additions and 47 deletions
+3 -1
View File
@@ -677,7 +677,9 @@ export const registerRoutes = async (
secretApprovalPolicyService, secretApprovalPolicyService,
secretBlindIndexDAL, secretBlindIndexDAL,
secretApprovalRequestDAL, secretApprovalRequestDAL,
secretApprovalRequestSecretDAL secretApprovalRequestSecretDAL,
secretQueueService,
snapshotService
}); });
const secretRotationQueue = secretRotationQueueFactory({ const secretRotationQueue = secretRotationQueueFactory({
telemetryService, telemetryService,
@@ -79,7 +79,7 @@ export const fnSecretsFromImports = async ({
let secretsFromDeeperImports: TSecretImportSecrets[] = []; let secretsFromDeeperImports: TSecretImportSecrets[] = [];
if (deeperImports.length) { if (deeperImports.length) {
secretsFromDeeperImports = await fnSecretsFromImports({ secretsFromDeeperImports = await fnSecretsFromImports({
allowedImports: deeperImports, allowedImports: deeperImports.filter(({ isReplication }) => !isReplication),
secretImportDAL, secretImportDAL,
folderDAL, folderDAL,
secretDAL, secretDAL,
@@ -8,6 +8,7 @@ import { TProjectDALFactory } from "../project/project-dal";
import { TProjectEnvDALFactory } from "../project-env/project-env-dal"; import { TProjectEnvDALFactory } from "../project-env/project-env-dal";
import { TSecretDALFactory } from "../secret/secret-dal"; import { TSecretDALFactory } from "../secret/secret-dal";
import { TSecretQueueFactory } from "../secret/secret-queue"; import { TSecretQueueFactory } from "../secret/secret-queue";
import { SecretOperations } from "../secret/secret-types";
import { TSecretFolderDALFactory } from "../secret-folder/secret-folder-dal"; import { TSecretFolderDALFactory } from "../secret-folder/secret-folder-dal";
import { TSecretImportDALFactory } from "./secret-import-dal"; import { TSecretImportDALFactory } from "./secret-import-dal";
import { fnSecretsFromImports } from "./secret-import-fns"; import { fnSecretsFromImports } from "./secret-import-fns";
@@ -26,7 +27,7 @@ type TSecretImportServiceFactoryDep = {
projectDAL: Pick<TProjectDALFactory, "checkProjectUpgradeStatus">; projectDAL: Pick<TProjectDALFactory, "checkProjectUpgradeStatus">;
projectEnvDAL: TProjectEnvDALFactory; projectEnvDAL: TProjectEnvDALFactory;
permissionService: Pick<TPermissionServiceFactory, "getProjectPermission">; permissionService: Pick<TPermissionServiceFactory, "getProjectPermission">;
secretQueueService: Pick<TSecretQueueFactory, "syncSecrets">; secretQueueService: Pick<TSecretQueueFactory, "syncSecrets" | "replicateSecrets">;
}; };
const ERR_SEC_IMP_NOT_FOUND = new BadRequestError({ message: "Secret import not found" }); const ERR_SEC_IMP_NOT_FOUND = new BadRequestError({ message: "Secret import not found" });
@@ -53,7 +54,7 @@ export const secretImportServiceFactory = ({
isReplication, isReplication,
path path
}: TCreateSecretImportDTO) => { }: TCreateSecretImportDTO) => {
const { permission } = await permissionService.getProjectPermission( const { permission, membership } = await permissionService.getProjectPermission(
actor, actor,
actorId, actorId,
projectId, projectId,
@@ -108,12 +109,27 @@ export const secretImportServiceFactory = ({
); );
}); });
await secretQueueService.syncSecrets({ if (secImport.isReplication) {
secretPath: secImport.importPath, const importedSecrets = await secretDAL.find({ folderId: sourceFolder?.id });
projectId, await secretQueueService.replicateSecrets({
environmentSlug: importEnv.slug, secretPath: secImport.importPath,
excludeReplication: true projectId,
}); environmentSlug: importEnv.slug,
pickOnlyImportIds: [secImport.id],
folderId: sourceFolder?.id as string,
secrets: importedSecrets.map(({ id, version }) => ({ operation: SecretOperations.Create, version, id })),
// TODO(akhilmhdh): approval based replication this will fail for identity
membershipId: membership?.id as string,
environmentId: importEnv.id
});
} else {
await secretQueueService.syncSecrets({
secretPath: secImport.importPath,
projectId,
environmentSlug: importEnv.slug,
excludeReplication: true
});
}
return { ...secImport, importEnv }; return { ...secImport, importEnv };
}; };
@@ -283,7 +299,7 @@ export const secretImportServiceFactory = ({
if (!folder) return []; if (!folder) return [];
// this will already order by position // this will already order by position
// so anything based on this order will also be in right position // so anything based on this order will also be in right position
const secretImports = await secretImportDAL.find({ folderId: folder.id }); const secretImports = await secretImportDAL.find({ folderId: folder.id, isReplication: false });
const allowedImports = secretImports.filter(({ importEnv, importPath }) => const allowedImports = secretImports.filter(({ importEnv, importPath }) =>
permission.can( permission.can(
@@ -64,12 +64,15 @@ export const secretReplicationServiceFactory = ({
}: TSecretReplicationServiceFactoryDep) => { }: TSecretReplicationServiceFactoryDep) => {
queueService.start(QueueName.SecretReplication, async (job) => { queueService.start(QueueName.SecretReplication, async (job) => {
logger.info(job.data, "Replication started"); logger.info(job.data, "Replication started");
const { secrets, folderId, secretPath, environmentId, projectId, membershipId } = job.data; const { secrets, folderId, secretPath, environmentId, projectId, membershipId, pickOnlyImportIds } = job.data;
const secretImports = await secretImportDAL.find({ let secretImports = await secretImportDAL.find({
importPath: secretPath, importPath: secretPath,
importEnv: environmentId, importEnv: environmentId,
isReplication: true isReplication: true
}); });
secretImports = pickOnlyImportIds
? secretImports.filter(({ id }) => pickOnlyImportIds?.includes(id))
: secretImports;
if (!secretImports.length || !secrets.length) return; if (!secretImports.length || !secrets.length) return;
// unfiltered secrets to be replicated // unfiltered secrets to be replicated
+27 -18
View File
@@ -143,6 +143,18 @@ export const secretQueueFactory = ({
}); });
}; };
const replicateSecrets = async (dto: TSyncSecretsDTO) => {
await queueService.queue(QueueName.SecretReplication, QueueJobs.SecretReplication, dto, {
attempts: 3,
backoff: {
type: "exponential",
delay: 2000
},
removeOnComplete: true,
removeOnFail: true
});
};
const removeSecretReminder = async (dto: TRemoveSecretReminderDTO) => { const removeSecretReminder = async (dto: TRemoveSecretReminderDTO) => {
const appCfg = getConfig(); const appCfg = getConfig();
await queueService.stopRepeatableJob( await queueService.stopRepeatableJob(
@@ -361,22 +373,17 @@ export const secretQueueFactory = ({
} }
); );
await syncIntegrations({ secretPath, projectId, environment }); await syncIntegrations({ secretPath, projectId, environment });
if (!excludeReplication) { if (!excludeReplication)
await queueService.queue( await replicateSecrets({
QueueName.SecretReplication, environmentId,
QueueJobs.SecretReplication, projectId,
{ environmentId, projectId, secretPath, folderId, secrets, membershipId }, secretPath,
{ folderId,
attempts: 3, secrets,
backoff: { membershipId,
type: "exponential", excludeReplication,
delay: 2000 environmentSlug: environment
}, });
removeOnComplete: true,
removeOnFail: true
}
);
}
}); });
queueService.start(QueueName.IntegrationSync, async (job) => { queueService.start(QueueName.IntegrationSync, async (job) => {
@@ -394,7 +401,8 @@ export const secretQueueFactory = ({
const linkSourceDto = { const linkSourceDto = {
projectId, projectId,
importEnv: folder.environment.id, importEnv: folder.environment.id,
importPath: secretPath importPath: secretPath,
isReplication: false
}; };
const imports = await secretImportDAL.find(linkSourceDto); const imports = await secretImportDAL.find(linkSourceDto);
@@ -598,6 +606,7 @@ export const secretQueueFactory = ({
syncIntegrations, syncIntegrations,
addSecretReminder, addSecretReminder,
removeSecretReminder, removeSecretReminder,
handleSecretReminder handleSecretReminder,
replicateSecrets
}; };
}; };
@@ -550,7 +550,8 @@ export const secretServiceFactory = ({
if (includeImports) { if (includeImports) {
const secretImports = await secretImportDAL.findByFolderIds(paths.map((p) => p.folderId)); const secretImports = await secretImportDAL.findByFolderIds(paths.map((p) => p.folderId));
const allowedImports = secretImports.filter(({ importEnv, importPath }) => const allowedImports = secretImports.filter(({ importEnv, importPath, isReplication }) =>
!isReplication &&
// if its service token allow full access over imported one // if its service token allow full access over imported one
actor === ActorType.SERVICE actor === ActorType.SERVICE
? true ? true
@@ -655,7 +656,7 @@ export const secretServiceFactory = ({
// then search for imported secrets // then search for imported secrets
// here we consider the import order also thus starting from bottom // here we consider the import order also thus starting from bottom
if (!secret && includeImports) { if (!secret && includeImports) {
const secretImports = await secretImportDAL.find({ folderId }); const secretImports = await secretImportDAL.find({ folderId, isReplication: false });
const allowedImports = secretImports.filter(({ importEnv, importPath }) => const allowedImports = secretImports.filter(({ importEnv, importPath }) =>
// if its service token allow full access over imported one // if its service token allow full access over imported one
actor === ActorType.SERVICE actor === ActorType.SERVICE
@@ -389,6 +389,8 @@ export type TSyncSecretsDTO<T extends boolean = false> = {
environmentId: string; environmentId: string;
folderId: string; folderId: string;
membershipId: string; membershipId: string;
// used for import creation to trigger replication
pickOnlyImportIds?: string[];
secrets: { secrets: {
operation: SecretOperations; operation: SecretOperations;
id: string; id: string;
@@ -220,6 +220,7 @@ export const useGetSecretApprovalRequestCount = ({
}) => }) =>
useQuery({ useQuery({
queryKey: secretApprovalRequestKeys.count({ workspaceId }), queryKey: secretApprovalRequestKeys.count({ workspaceId }),
refetchInterval: 5000,
queryFn: () => fetchSecretApprovalRequestCount({ workspaceId }), queryFn: () => fetchSecretApprovalRequestCount({ workspaceId }),
enabled: Boolean(workspaceId) && (options?.enabled ?? true) enabled: Boolean(workspaceId) && (options?.enabled ?? true)
}); });
@@ -44,6 +44,7 @@ export type TSecretApprovalSecChange = {
export type TSecretApprovalRequest<J extends unknown = EncryptedSecret> = { export type TSecretApprovalRequest<J extends unknown = EncryptedSecret> = {
id: string; id: string;
isReplication?: boolean;
slug: string; slug: string;
createdAt: string; createdAt: string;
committerId: string; committerId: string;
@@ -10,6 +10,7 @@ export type TSecretImport = {
position: string; position: string;
createdAt: string; createdAt: string;
updatedAt: string; updatedAt: string;
isReplication?: boolean;
}; };
export type TGetImportedFoldersByEnvDTO = { export type TGetImportedFoldersByEnvDTO = {
@@ -67,6 +67,7 @@ export const decryptSecrets = (
env: encSecret.environment, env: encSecret.environment,
key: secretKey, key: secretKey,
value: secretValue, value: secretValue,
isReplicated: encSecret.isReplicated,
tags: encSecret.tags, tags: encSecret.tags,
comment: secretComment, comment: secretComment,
reminderRepeatDays: encSecret.secretReminderRepeatDays, reminderRepeatDays: encSecret.secretReminderRepeatDays,
+2
View File
@@ -14,6 +14,7 @@ export type EncryptedSecret = {
secretValueIV: string; secretValueIV: string;
secretValueTag: string; secretValueTag: string;
__v: number; __v: number;
isReplicated?: boolean;
createdAt: string; createdAt: string;
updatedAt: string; updatedAt: string;
skipMultilineEncoding?: boolean; skipMultilineEncoding?: boolean;
@@ -31,6 +32,7 @@ export type DecryptedSecret = {
key: string; key: string;
value: string; value: string;
comment: string; comment: string;
isReplicated?: boolean;
reminderRepeatDays?: number | null; reminderRepeatDays?: number | null;
reminderNote?: string | null; reminderNote?: string | null;
tags: WsTag[]; tags: WsTag[];
@@ -212,7 +212,8 @@ export const SecretApprovalRequest = () => {
createdAt, createdAt,
policy, policy,
reviewers, reviewers,
status status,
isReplication
} = secretApproval; } = secretApproval;
const isApprover = policy?.approvers?.indexOf(myMembershipId || "") !== -1; const isApprover = policy?.approvers?.indexOf(myMembershipId || "") !== -1;
const isReviewed = const isReviewed =
@@ -240,8 +241,9 @@ export const SecretApprovalRequest = () => {
Opened {formatDistance(new Date(createdAt), new Date())} ago by{" "} Opened {formatDistance(new Date(createdAt), new Date())} ago by{" "}
{membersGroupById?.[committerId]?.user?.firstName}{" "} {membersGroupById?.[committerId]?.user?.firstName}{" "}
{membersGroupById?.[committerId]?.user?.lastName} ( {membersGroupById?.[committerId]?.user?.lastName} (
{membersGroupById?.[committerId]?.user?.email}){" "} {membersGroupById?.[committerId]?.user?.email})
{isApprover && !isReviewed && status === "open" && "- Review required"} {isReplication && "via replication"}
{isApprover && !isReviewed && status === "open" && " - Review required"}
</span> </span>
</div> </div>
); );
@@ -185,6 +185,7 @@ export const SecretApprovalRequestChanges = ({
<div className="flex flex-grow flex-col"> <div className="flex flex-grow flex-col">
<div className="mb-1 text-lg"> <div className="mb-1 text-lg">
{generateCommitText(secretApprovalRequestDetails.commits)} {generateCommitText(secretApprovalRequestDetails.commits)}
{secretApprovalRequestDetails.isReplication && <span className="text-sm text-bunker-300"> (replication)</span>}
</div> </div>
<div className="flex items-center text-sm text-bunker-300"> <div className="flex items-center text-sm text-bunker-300">
{committer?.user?.firstName} {committer?.user?.firstName}
@@ -11,7 +11,7 @@ import {
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { ProjectPermissionCan } from "@app/components/permissions"; import { ProjectPermissionCan } from "@app/components/permissions";
import { EmptyState, IconButton, SecretInput, TableContainer } from "@app/components/v2"; import { EmptyState, IconButton, SecretInput, TableContainer, Tag } from "@app/components/v2";
import { ProjectPermissionActions, ProjectPermissionSub } from "@app/context"; import { ProjectPermissionActions, ProjectPermissionSub } from "@app/context";
import { useToggle } from "@app/hooks"; import { useToggle } from "@app/hooks";
@@ -19,6 +19,7 @@ type Props = {
onDelete: () => void; onDelete: () => void;
environment: string; environment: string;
secretPath?: string; secretPath?: string;
isReplication?: boolean;
importEnvName: string; importEnvName: string;
importEnvPath: string; importEnvPath: string;
importedSecrets: { key: string; value: string; overriden: { env: string; secretPath: string } }[]; importedSecrets: { key: string; value: string; overriden: { env: string; secretPath: string } }[];
@@ -27,11 +28,20 @@ type Props = {
}; };
// to show the environment and folder icon // to show the environment and folder icon
export const EnvFolderIcon = ({ env, secretPath }: { env: string; secretPath: string }) => ( export const EnvFolderIcon = ({
env,
secretPath,
isReplication
}: {
env: string;
secretPath: string;
isReplication?: boolean;
}) => (
<div className="inline-flex items-center space-x-2"> <div className="inline-flex items-center space-x-2">
<div style={{ minWidth: "96px" }}>{env || "-"}</div> <div style={{ minWidth: "96px" }}>{env || "-"}</div>
{secretPath && ( {secretPath && (
<div className="inline-flex items-center space-x-2 border-l border-mineshaft-600 pl-2"> <div className="inline-flex items-center space-x-2 border-l border-mineshaft-600 pl-2">
{isReplication && <Tag size="xs">Replication Mode</Tag>}
<FontAwesomeIcon icon={faFolder} className="text-md text-green-700" /> <FontAwesomeIcon icon={faFolder} className="text-md text-green-700" />
<span>{secretPath}</span> <span>{secretPath}</span>
</div> </div>
@@ -44,6 +54,7 @@ export const SecretImportItem = ({
id, id,
importEnvName, importEnvName,
importEnvPath, importEnvPath,
isReplication,
importedSecrets = [], importedSecrets = [],
searchTerm = "", searchTerm = "",
secretPath, secretPath,
@@ -92,7 +103,11 @@ export const SecretImportItem = ({
<FontAwesomeIcon icon={faFileImport} /> <FontAwesomeIcon icon={faFileImport} />
</div> </div>
<div className="flex flex-grow items-center px-4 py-2"> <div className="flex flex-grow items-center px-4 py-2">
<EnvFolderIcon env={importEnvName || ""} secretPath={importEnvPath} /> <EnvFolderIcon
env={importEnvName || ""}
secretPath={importEnvPath}
isReplication={isReplication}
/>
</div> </div>
<div className="flex items-center space-x-4 border-l border-mineshaft-600 px-4 py-2"> <div className="flex items-center space-x-4 border-l border-mineshaft-600 px-4 py-2">
<ProjectPermissionCan <ProjectPermissionCan
@@ -90,7 +90,7 @@ export const SecretImportListView = ({
const { popUp, handlePopUpOpen, handlePopUpClose, handlePopUpToggle } = usePopUp([ const { popUp, handlePopUpOpen, handlePopUpClose, handlePopUpToggle } = usePopUp([
"deleteSecretImport" "deleteSecretImport"
] as const); ] as const);
const sensors = useSensors( const sensors = useSensors(
useSensor(MouseSensor, {}), useSensor(MouseSensor, {}),
useSensor(TouchSensor, {}), useSensor(TouchSensor, {}),
@@ -159,12 +159,13 @@ export const SecretImportListView = ({
> >
<SortableContext items={items} strategy={verticalListSortingStrategy}> <SortableContext items={items} strategy={verticalListSortingStrategy}>
{items?.map((item) => { {items?.map((item) => {
const { importPath, importEnv, id } = item; const { importPath, importEnv, id, isReplication } = item;
return ( return (
<SecretImportItem <SecretImportItem
searchTerm={searchTerm} searchTerm={searchTerm}
key={`imported-env-${id}`} key={`imported-env-${id}`}
id={id} id={id}
isReplication={isReplication}
importEnvPath={importPath} importEnvPath={importPath}
importEnvName={importEnv.name} importEnvName={importEnv.name}
importedSecrets={computeImportedSecretRows( importedSecrets={computeImportedSecretRows(
@@ -185,9 +186,8 @@ export const SecretImportListView = ({
isOpen={popUp.deleteSecretImport.isOpen} isOpen={popUp.deleteSecretImport.isOpen}
deleteKey="unlink" deleteKey="unlink"
title="Do you want to remove this secret import?" title="Do you want to remove this secret import?"
subTitle={`This will unlink secrets from environment ${ subTitle={`This will unlink secrets from environment ${(popUp.deleteSecretImport?.data as TSecretImport)?.importEnv
(popUp.deleteSecretImport?.data as TSecretImport)?.importEnv } of path ${(popUp.deleteSecretImport?.data as TSecretImport)?.importPath}?`}
} of path ${(popUp.deleteSecretImport?.data as TSecretImport)?.importPath}?`}
onChange={(isOpen) => handlePopUpToggle("deleteSecretImport", isOpen)} onChange={(isOpen) => handlePopUpToggle("deleteSecretImport", isOpen)}
onDeleteApproved={handleSecretImportDelete} onDeleteApproved={handleSecretImportDelete}
/> />
@@ -420,9 +420,8 @@ export const SecretItem = memo(
<Tooltip <Tooltip
content={ content={
secretReminderRepeatDays && secretReminderRepeatDays > 0 secretReminderRepeatDays && secretReminderRepeatDays > 0
? `Every ${secretReminderRepeatDays} day${ ? `Every ${secretReminderRepeatDays} day${Number(secretReminderRepeatDays) > 1 ? "s" : ""
Number(secretReminderRepeatDays) > 1 ? "s" : "" }
}
` `
: "Reminder" : "Reminder"
} }