mirror of
https://github.com/awatertrevi/infisical.git
synced 2026-10-08 07:27:43 +00:00
feat: added resync replication feature
This commit is contained in:
@@ -211,6 +211,49 @@ export const registerSecretImportRouter = async (server: FastifyZodProvider) =>
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
server.route({
|
||||||
|
method: "POST",
|
||||||
|
url: "/:secretImportId/replication-resync",
|
||||||
|
config: {
|
||||||
|
rateLimit: secretsLimit
|
||||||
|
},
|
||||||
|
schema: {
|
||||||
|
description: "Resync secret replication of secret imports",
|
||||||
|
security: [
|
||||||
|
{
|
||||||
|
bearerAuth: []
|
||||||
|
}
|
||||||
|
],
|
||||||
|
params: z.object({
|
||||||
|
secretImportId: z.string().trim().describe(SECRET_IMPORTS.UPDATE.secretImportId)
|
||||||
|
}),
|
||||||
|
body: z.object({
|
||||||
|
workspaceId: z.string().trim().describe(SECRET_IMPORTS.UPDATE.workspaceId),
|
||||||
|
environment: z.string().trim().describe(SECRET_IMPORTS.UPDATE.environment),
|
||||||
|
path: z.string().trim().default("/").transform(removeTrailingSlash).describe(SECRET_IMPORTS.UPDATE.path)
|
||||||
|
}),
|
||||||
|
response: {
|
||||||
|
200: z.object({
|
||||||
|
message: z.string()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onRequest: verifyAuth([AuthMode.JWT]),
|
||||||
|
handler: async (req) => {
|
||||||
|
const { message } = await server.services.secretImport.resyncSecretImportReplication({
|
||||||
|
actorId: req.permission.id,
|
||||||
|
actor: req.permission.type,
|
||||||
|
actorAuthMethod: req.permission.authMethod,
|
||||||
|
actorOrgId: req.permission.orgId,
|
||||||
|
id: req.params.secretImportId,
|
||||||
|
...req.body,
|
||||||
|
projectId: req.body.workspaceId
|
||||||
|
});
|
||||||
|
|
||||||
|
return { message };
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
server.route({
|
server.route({
|
||||||
method: "GET",
|
method: "GET",
|
||||||
url: "/",
|
url: "/",
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import { ForbiddenError, subject } from "@casl/ability";
|
import { ForbiddenError, subject } from "@casl/ability";
|
||||||
|
|
||||||
|
import { TableName } from "@app/db/schemas";
|
||||||
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service";
|
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service";
|
||||||
import { ProjectPermissionActions, ProjectPermissionSub } from "@app/ee/services/permission/project-permission";
|
import { ProjectPermissionActions, ProjectPermissionSub } from "@app/ee/services/permission/project-permission";
|
||||||
import { BadRequestError } from "@app/lib/errors";
|
import { BadRequestError } from "@app/lib/errors";
|
||||||
@@ -17,6 +18,7 @@ import {
|
|||||||
TDeleteSecretImportDTO,
|
TDeleteSecretImportDTO,
|
||||||
TGetSecretImportsDTO,
|
TGetSecretImportsDTO,
|
||||||
TGetSecretsFromImportDTO,
|
TGetSecretsFromImportDTO,
|
||||||
|
TResyncSecretImportReplicationDTO,
|
||||||
TUpdateSecretImportDTO
|
TUpdateSecretImportDTO
|
||||||
} from "./secret-import-types";
|
} from "./secret-import-types";
|
||||||
|
|
||||||
@@ -109,17 +111,17 @@ export const secretImportServiceFactory = ({
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
if (secImport.isReplication) {
|
if (secImport.isReplication && sourceFolder && membership) {
|
||||||
const importedSecrets = await secretDAL.find({ folderId: sourceFolder?.id });
|
const importedSecrets = await secretDAL.find({ folderId: sourceFolder?.id });
|
||||||
await secretQueueService.replicateSecrets({
|
await secretQueueService.replicateSecrets({
|
||||||
secretPath: secImport.importPath,
|
secretPath: secImport.importPath,
|
||||||
projectId,
|
projectId,
|
||||||
environmentSlug: importEnv.slug,
|
environmentSlug: importEnv.slug,
|
||||||
pickOnlyImportIds: [secImport.id],
|
pickOnlyImportIds: [secImport.id],
|
||||||
folderId: sourceFolder?.id as string,
|
folderId: sourceFolder.id,
|
||||||
secrets: importedSecrets.map(({ id, version }) => ({ operation: SecretOperations.Create, version, id })),
|
secrets: importedSecrets.map(({ id, version }) => ({ operation: SecretOperations.Create, version, id })),
|
||||||
// TODO(akhilmhdh): approval based replication this will fail for identity
|
// TODO(akhilmhdh): approval based replication this will fail for identity
|
||||||
membershipId: membership?.id as string,
|
membershipId: membership.id,
|
||||||
environmentId: importEnv.id
|
environmentId: importEnv.id
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
@@ -247,6 +249,76 @@ export const secretImportServiceFactory = ({
|
|||||||
return secImport;
|
return secImport;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const resyncSecretImportReplication = async ({
|
||||||
|
environment,
|
||||||
|
actor,
|
||||||
|
actorId,
|
||||||
|
actorOrgId,
|
||||||
|
actorAuthMethod,
|
||||||
|
projectId,
|
||||||
|
path,
|
||||||
|
id: secretImportDocId
|
||||||
|
}: TResyncSecretImportReplicationDTO) => {
|
||||||
|
const { permission, membership } = await permissionService.getProjectPermission(
|
||||||
|
actor,
|
||||||
|
actorId,
|
||||||
|
projectId,
|
||||||
|
actorAuthMethod,
|
||||||
|
actorOrgId
|
||||||
|
);
|
||||||
|
|
||||||
|
// check if user has permission to import into destination path
|
||||||
|
ForbiddenError.from(permission).throwUnlessCan(
|
||||||
|
ProjectPermissionActions.Create,
|
||||||
|
subject(ProjectPermissionSub.Secrets, { environment, secretPath: path })
|
||||||
|
);
|
||||||
|
|
||||||
|
const folder = await folderDAL.findBySecretPath(projectId, environment, path);
|
||||||
|
if (!folder) throw new BadRequestError({ message: "Folder not found", name: "Update import" });
|
||||||
|
|
||||||
|
const [secretImportDoc] = await secretImportDAL.find({
|
||||||
|
folderId: folder.id,
|
||||||
|
[`${TableName.SecretImport}.id` as "id"]: secretImportDocId
|
||||||
|
});
|
||||||
|
if (!secretImportDoc) throw new BadRequestError({ message: "Failed to find secret import" });
|
||||||
|
|
||||||
|
if (!secretImportDoc.isReplication) throw new BadRequestError({ message: "Import is not in replication mode" });
|
||||||
|
|
||||||
|
// check if user has permission to import from target path
|
||||||
|
ForbiddenError.from(permission).throwUnlessCan(
|
||||||
|
ProjectPermissionActions.Create,
|
||||||
|
subject(ProjectPermissionSub.Secrets, {
|
||||||
|
environment: secretImportDoc.importEnv.slug,
|
||||||
|
secretPath: secretImportDoc.importPath
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
await projectDAL.checkProjectUpgradeStatus(projectId);
|
||||||
|
|
||||||
|
const sourceFolder = await folderDAL.findBySecretPath(
|
||||||
|
projectId,
|
||||||
|
secretImportDoc.importEnv.slug,
|
||||||
|
secretImportDoc.importPath
|
||||||
|
);
|
||||||
|
|
||||||
|
const importedSecrets = await secretDAL.find({ folderId: sourceFolder?.id });
|
||||||
|
if (membership && sourceFolder) {
|
||||||
|
await secretQueueService.replicateSecrets({
|
||||||
|
secretPath: secretImportDoc.importPath,
|
||||||
|
projectId,
|
||||||
|
environmentSlug: secretImportDoc.importEnv.slug,
|
||||||
|
pickOnlyImportIds: [secretImportDoc.id],
|
||||||
|
folderId: sourceFolder.id,
|
||||||
|
secrets: importedSecrets.map(({ id, version }) => ({ operation: SecretOperations.Create, version, id })),
|
||||||
|
// TODO(akhilmhdh): approval based replication this will fail for identity
|
||||||
|
membershipId: membership.id,
|
||||||
|
environmentId: secretImportDoc.importEnv.id
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return { message: "replication started" };
|
||||||
|
};
|
||||||
|
|
||||||
const getImports = async ({
|
const getImports = async ({
|
||||||
path,
|
path,
|
||||||
environment,
|
environment,
|
||||||
@@ -319,6 +391,7 @@ export const secretImportServiceFactory = ({
|
|||||||
deleteImport,
|
deleteImport,
|
||||||
getImports,
|
getImports,
|
||||||
getSecretsFromImports,
|
getSecretsFromImports,
|
||||||
|
resyncSecretImportReplication,
|
||||||
fnSecretsFromImports
|
fnSecretsFromImports
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -17,6 +17,12 @@ export type TUpdateSecretImportDTO = {
|
|||||||
data: Partial<{ environment: string; path: string; position: number }>;
|
data: Partial<{ environment: string; path: string; position: number }>;
|
||||||
} & TProjectPermission;
|
} & TProjectPermission;
|
||||||
|
|
||||||
|
export type TResyncSecretImportReplicationDTO = {
|
||||||
|
environment: string;
|
||||||
|
path: string;
|
||||||
|
id: string;
|
||||||
|
} & TProjectPermission;
|
||||||
|
|
||||||
export type TDeleteSecretImportDTO = {
|
export type TDeleteSecretImportDTO = {
|
||||||
environment: string;
|
environment: string;
|
||||||
path: string;
|
path: string;
|
||||||
|
|||||||
@@ -1,4 +1,9 @@
|
|||||||
export { useCreateSecretImport, useDeleteSecretImport, useUpdateSecretImport } from "./mutation";
|
export {
|
||||||
|
useCreateSecretImport,
|
||||||
|
useDeleteSecretImport,
|
||||||
|
useResyncSecretReplication,
|
||||||
|
useUpdateSecretImport
|
||||||
|
} from "./mutation";
|
||||||
export {
|
export {
|
||||||
useGetImportedFoldersByEnv,
|
useGetImportedFoldersByEnv,
|
||||||
useGetImportedSecretsAllEnvs,
|
useGetImportedSecretsAllEnvs,
|
||||||
|
|||||||
@@ -3,7 +3,12 @@ import { useMutation, useQueryClient } from "@tanstack/react-query";
|
|||||||
import { apiRequest } from "@app/config/request";
|
import { apiRequest } from "@app/config/request";
|
||||||
|
|
||||||
import { secretImportKeys } from "./queries";
|
import { secretImportKeys } from "./queries";
|
||||||
import { TCreateSecretImportDTO, TDeleteSecretImportDTO, TUpdateSecretImportDTO } from "./types";
|
import {
|
||||||
|
TCreateSecretImportDTO,
|
||||||
|
TDeleteSecretImportDTO,
|
||||||
|
TResyncSecretReplicationDTO,
|
||||||
|
TUpdateSecretImportDTO
|
||||||
|
} from "./types";
|
||||||
|
|
||||||
export const useCreateSecretImport = () => {
|
export const useCreateSecretImport = () => {
|
||||||
const queryClient = useQueryClient();
|
const queryClient = useQueryClient();
|
||||||
@@ -54,6 +59,19 @@ export const useUpdateSecretImport = () => {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const useResyncSecretReplication = () => {
|
||||||
|
return useMutation<{}, {}, TResyncSecretReplicationDTO>({
|
||||||
|
mutationFn: async ({ environment, projectId, path, id }) => {
|
||||||
|
const { data } = await apiRequest.post(`/api/v1/secret-imports/${id}/replication-resync`, {
|
||||||
|
environment,
|
||||||
|
path,
|
||||||
|
workspaceId: projectId
|
||||||
|
});
|
||||||
|
return data;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
export const useDeleteSecretImport = () => {
|
export const useDeleteSecretImport = () => {
|
||||||
const queryClient = useQueryClient();
|
const queryClient = useQueryClient();
|
||||||
|
|
||||||
|
|||||||
@@ -76,6 +76,13 @@ export type TUpdateSecretImportDTO = {
|
|||||||
}>;
|
}>;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type TResyncSecretReplicationDTO = {
|
||||||
|
id: string;
|
||||||
|
projectId: string;
|
||||||
|
environment: string;
|
||||||
|
path?: string;
|
||||||
|
};
|
||||||
|
|
||||||
export type TDeleteSecretImportDTO = {
|
export type TDeleteSecretImportDTO = {
|
||||||
id: string;
|
id: string;
|
||||||
projectId: string;
|
projectId: string;
|
||||||
|
|||||||
@@ -81,7 +81,8 @@ export const CreateSecretImportForm = ({
|
|||||||
reset();
|
reset();
|
||||||
createNotification({
|
createNotification({
|
||||||
type: "success",
|
type: "success",
|
||||||
text: "Successfully linked"
|
text: `Successfully linked.${isReplication ? " Kindly refresh the board to see changes." : ""
|
||||||
|
}`
|
||||||
});
|
});
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error(err);
|
console.error(err);
|
||||||
|
|||||||
+54
-1
@@ -6,14 +6,18 @@ import {
|
|||||||
faFileImport,
|
faFileImport,
|
||||||
faFolder,
|
faFolder,
|
||||||
faKey,
|
faKey,
|
||||||
|
faRotate,
|
||||||
faUpDown
|
faUpDown
|
||||||
} from "@fortawesome/free-solid-svg-icons";
|
} from "@fortawesome/free-solid-svg-icons";
|
||||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||||
|
import { twMerge } from "tailwind-merge";
|
||||||
|
|
||||||
|
import { createNotification } from "@app/components/notifications";
|
||||||
import { ProjectPermissionCan } from "@app/components/permissions";
|
import { ProjectPermissionCan } from "@app/components/permissions";
|
||||||
import { EmptyState, IconButton, SecretInput, TableContainer, Tag } from "@app/components/v2";
|
import { EmptyState, IconButton, SecretInput, TableContainer, Tag } from "@app/components/v2";
|
||||||
import { ProjectPermissionActions, ProjectPermissionSub } from "@app/context";
|
import { ProjectPermissionActions, ProjectPermissionSub, useWorkspace } from "@app/context";
|
||||||
import { useToggle } from "@app/hooks";
|
import { useToggle } from "@app/hooks";
|
||||||
|
import { useResyncSecretReplication } from "@app/hooks/api";
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
onDelete: () => void;
|
onDelete: () => void;
|
||||||
@@ -60,10 +64,12 @@ export const SecretImportItem = ({
|
|||||||
secretPath,
|
secretPath,
|
||||||
environment
|
environment
|
||||||
}: Props) => {
|
}: Props) => {
|
||||||
|
const { currentWorkspace } = useWorkspace();
|
||||||
const [isExpanded, setIsExpanded] = useToggle();
|
const [isExpanded, setIsExpanded] = useToggle();
|
||||||
const { attributes, listeners, transform, transition, setNodeRef, isDragging } = useSortable({
|
const { attributes, listeners, transform, transition, setNodeRef, isDragging } = useSortable({
|
||||||
id
|
id
|
||||||
});
|
});
|
||||||
|
const resyncSecretReplication = useResyncSecretReplication();
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const filteredSecrets = importedSecrets.filter((secret) =>
|
const filteredSecrets = importedSecrets.filter((secret) =>
|
||||||
@@ -88,6 +94,28 @@ export const SecretImportItem = ({
|
|||||||
transition
|
transition
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleResyncSecretReplication = async () => {
|
||||||
|
if (resyncSecretReplication.isLoading) return;
|
||||||
|
try {
|
||||||
|
await resyncSecretReplication.mutateAsync({
|
||||||
|
id,
|
||||||
|
environment,
|
||||||
|
path: secretPath,
|
||||||
|
projectId: currentWorkspace?.id || ""
|
||||||
|
});
|
||||||
|
createNotification({
|
||||||
|
text: "Kindly refresh the board to see changes.",
|
||||||
|
type: "success"
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.error(error);
|
||||||
|
createNotification({
|
||||||
|
text: "Failed to resync replication",
|
||||||
|
type: "error"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div
|
<div
|
||||||
@@ -109,6 +137,31 @@ export const SecretImportItem = ({
|
|||||||
isReplication={isReplication}
|
isReplication={isReplication}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
<div className="flex items-center space-x-2 px-4 py-2">
|
||||||
|
<ProjectPermissionCan
|
||||||
|
I={ProjectPermissionActions.Edit}
|
||||||
|
a={subject(ProjectPermissionSub.Secrets, { environment, secretPath })}
|
||||||
|
renderTooltip
|
||||||
|
allowedLabel="Resync replicated secrets"
|
||||||
|
>
|
||||||
|
{(isAllowed) => (
|
||||||
|
<IconButton
|
||||||
|
size="md"
|
||||||
|
colorSchema="primary"
|
||||||
|
variant="plain"
|
||||||
|
ariaLabel="expand"
|
||||||
|
className={twMerge(
|
||||||
|
"p-0 opacity-0 group-hover:opacity-100",
|
||||||
|
resyncSecretReplication.isLoading && "animate-spin opacity-100"
|
||||||
|
)}
|
||||||
|
isDisabled={!isAllowed}
|
||||||
|
onClick={handleResyncSecretReplication}
|
||||||
|
>
|
||||||
|
<FontAwesomeIcon icon={faRotate} />
|
||||||
|
</IconButton>
|
||||||
|
)}
|
||||||
|
</ProjectPermissionCan>
|
||||||
|
</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
|
||||||
I={ProjectPermissionActions.Edit}
|
I={ProjectPermissionActions.Edit}
|
||||||
|
|||||||
Reference in New Issue
Block a user