mirror of
https://github.com/awatertrevi/infisical.git
synced 2026-09-22 13:39:35 +00:00
fix: improvements
This commit is contained in:
@@ -18,14 +18,11 @@ export const registerExternalMigrationRouter = async (server: FastifyZodProvider
|
||||
nonce: z.string().trim().min(1),
|
||||
data: z.string().trim().min(1)
|
||||
})
|
||||
}),
|
||||
response: {
|
||||
200: z.object({})
|
||||
}
|
||||
})
|
||||
},
|
||||
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
||||
handler: async (req) => {
|
||||
await server.services.migration.importEnvnKeyData({
|
||||
await server.services.migration.importEnvKeyData({
|
||||
decryptionKey: req.body.decryptionKey,
|
||||
encryptedJson: req.body.encryptedJson,
|
||||
actorId: req.permission.id,
|
||||
|
||||
@@ -1,16 +1,33 @@
|
||||
import slugify from "@sindresorhus/slugify";
|
||||
import { randomUUID } from "crypto";
|
||||
import sjcl from "sjcl";
|
||||
import tweetnacl from "tweetnacl";
|
||||
import tweetnaclUtil from "tweetnacl-util";
|
||||
|
||||
import { OrgMembershipRole, ProjectMembershipRole, SecretType } from "@app/db/schemas";
|
||||
import { BadRequestError } from "@app/lib/errors";
|
||||
import { logger } from "@app/lib/logger";
|
||||
import { alphaNumericNanoId } from "@app/lib/nanoid";
|
||||
|
||||
import { InfisicalImportData, TEnvKeyExportJSON } from "./external-migration-types";
|
||||
import { TOrgServiceFactory } from "../org/org-service";
|
||||
import { TProjectServiceFactory } from "../project/project-service";
|
||||
import { TProjectEnvServiceFactory } from "../project-env/project-env-service";
|
||||
import { TSecretServiceFactory } from "../secret/secret-service";
|
||||
import { InfisicalImportData, TEnvKeyExportJSON, TImportInfisicalDataCreate } from "./external-migration-types";
|
||||
|
||||
export type TImportDataIntoInfisicalDTO = {
|
||||
projectService: TProjectServiceFactory;
|
||||
orgService: TOrgServiceFactory;
|
||||
projectEnvService: TProjectEnvServiceFactory;
|
||||
secretService: TSecretServiceFactory;
|
||||
|
||||
input: TImportInfisicalDataCreate;
|
||||
};
|
||||
|
||||
const { codec, hash } = sjcl;
|
||||
const { secretbox } = tweetnacl;
|
||||
|
||||
export const decryptEnvKeyData = async (decryptionKey: string, encryptedJson: { nonce: string; data: string }) => {
|
||||
export const decryptEnvKeyDataFn = async (decryptionKey: string, encryptedJson: { nonce: string; data: string }) => {
|
||||
const key = tweetnaclUtil.decodeBase64(codec.base64.fromBits(hash.sha256.hash(decryptionKey)));
|
||||
const nonce = tweetnaclUtil.decodeBase64(encryptedJson.nonce);
|
||||
const encryptedData = tweetnaclUtil.decodeBase64(encryptedJson.data);
|
||||
@@ -25,7 +42,7 @@ export const decryptEnvKeyData = async (decryptionKey: string, encryptedJson: {
|
||||
return decryptedJson;
|
||||
};
|
||||
|
||||
export const parseEnvKeyData = async (decryptedJson: string): Promise<InfisicalImportData> => {
|
||||
export const parseEnvKeyDataFn = async (decryptedJson: string): Promise<InfisicalImportData> => {
|
||||
const parsedJson: TEnvKeyExportJSON = JSON.parse(decryptedJson) as TEnvKeyExportJSON;
|
||||
|
||||
const infisicalImportData: InfisicalImportData = {
|
||||
@@ -71,3 +88,110 @@ export const parseEnvKeyData = async (decryptedJson: string): Promise<InfisicalI
|
||||
|
||||
return infisicalImportData;
|
||||
};
|
||||
|
||||
export const importDataIntoInfisicalFn = async ({
|
||||
projectService,
|
||||
orgService,
|
||||
projectEnvService,
|
||||
secretService,
|
||||
input: { data, actor, actorId, actorOrgId, actorAuthMethod }
|
||||
}: TImportDataIntoInfisicalDTO) => {
|
||||
// Import data to infisical
|
||||
if (!data || !data.projects) {
|
||||
throw new BadRequestError({ message: "No projects found in data" });
|
||||
}
|
||||
|
||||
const originalToNewProjectId = new Map<string, string>();
|
||||
const originalToNewEnvironmentId = new Map<string, string>();
|
||||
|
||||
for await (const [id, project] of data.projects) {
|
||||
const newProject = await projectService
|
||||
.createProject({
|
||||
actor,
|
||||
actorId,
|
||||
actorOrgId,
|
||||
actorAuthMethod,
|
||||
workspaceName: project.name,
|
||||
createDefaultEnvs: false
|
||||
})
|
||||
.catch(() => {
|
||||
throw new BadRequestError({ message: `Failed to import to project [name:${project.name}] [id:${id}]` });
|
||||
});
|
||||
|
||||
originalToNewProjectId.set(project.id, newProject.id);
|
||||
}
|
||||
|
||||
// Invite user importing projects
|
||||
const invites = await orgService.inviteUserToOrganization({
|
||||
actorAuthMethod,
|
||||
actorId,
|
||||
actorOrgId,
|
||||
actor,
|
||||
inviteeEmails: [],
|
||||
orgId: actorOrgId,
|
||||
organizationRoleSlug: OrgMembershipRole.NoAccess,
|
||||
projects: Array.from(originalToNewProjectId.values()).map((project) => ({
|
||||
id: project,
|
||||
projectRoleSlug: [ProjectMembershipRole.Member]
|
||||
}))
|
||||
});
|
||||
if (!invites) {
|
||||
throw new BadRequestError({ message: `Failed to invite user to projects: [userId:${actorId}]` });
|
||||
}
|
||||
|
||||
// Import environments
|
||||
if (data.environments) {
|
||||
for await (const [id, environment] of data.environments) {
|
||||
try {
|
||||
const newEnvironment = await projectEnvService.createEnvironment({
|
||||
actor,
|
||||
actorId,
|
||||
actorOrgId,
|
||||
actorAuthMethod,
|
||||
name: environment.name,
|
||||
projectId: originalToNewProjectId.get(environment.projectId)!,
|
||||
slug: slugify(`${environment.name}-${alphaNumericNanoId(4)}`)
|
||||
});
|
||||
|
||||
if (!newEnvironment) {
|
||||
logger.error(`Failed to import environment: [name:${environment.name}] [id:${id}]`);
|
||||
throw new BadRequestError({
|
||||
message: `Failed to import environment: [name:${environment.name}] [id:${id}]`
|
||||
});
|
||||
}
|
||||
originalToNewEnvironmentId.set(id, newEnvironment.slug);
|
||||
} catch (error) {
|
||||
throw new BadRequestError({
|
||||
message: `Failed to import environment: ${environment.name}]`,
|
||||
name: "EnvKeyMigrationImportEnvironment"
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Import secrets
|
||||
if (data.secrets) {
|
||||
for await (const [id, secret] of data.secrets) {
|
||||
const dataProjectId = data.environments?.get(secret.environmentId)?.projectId;
|
||||
if (!dataProjectId) {
|
||||
throw new BadRequestError({ message: `Failed to import secret "${secret.name}", project not found` });
|
||||
}
|
||||
const projectId = originalToNewProjectId.get(dataProjectId);
|
||||
const newSecret = await secretService.createSecretRaw({
|
||||
actorId,
|
||||
actor,
|
||||
actorOrgId,
|
||||
environment: originalToNewEnvironmentId.get(secret.environmentId)!,
|
||||
actorAuthMethod,
|
||||
projectId: projectId!,
|
||||
secretPath: "/",
|
||||
secretName: secret.name,
|
||||
type: SecretType.Shared,
|
||||
secretValue: secret.value
|
||||
});
|
||||
if (!newSecret) {
|
||||
throw new BadRequestError({ message: `Failed to import secret: [name:${secret.name}] [id:${id}]` });
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
@@ -1,16 +1,9 @@
|
||||
import slugify from "@sindresorhus/slugify";
|
||||
|
||||
import { OrgMembershipRole, ProjectMembershipRole, SecretType } from "@app/db/schemas";
|
||||
import { BadRequestError } from "@app/lib/errors";
|
||||
import { logger } from "@app/lib/logger";
|
||||
import { alphaNumericNanoId } from "@app/lib/nanoid";
|
||||
|
||||
import { TOrgServiceFactory } from "../org/org-service";
|
||||
import { TProjectServiceFactory } from "../project/project-service";
|
||||
import { TProjectEnvServiceFactory } from "../project-env/project-env-service";
|
||||
import { TSecretServiceFactory } from "../secret/secret-service";
|
||||
import { decryptEnvKeyData, parseEnvKeyData } from "./external-migration-fns";
|
||||
import { TImportEnvKeyDataCreate, TImportInfisicalDataCreate } from "./external-migration-types";
|
||||
import { decryptEnvKeyDataFn, importDataIntoInfisicalFn, parseEnvKeyDataFn } from "./external-migration-fns";
|
||||
import { TImportEnvKeyDataCreate } from "./external-migration-types";
|
||||
|
||||
type TExternalMigrationServiceFactoryDep = {
|
||||
projectService: TProjectServiceFactory;
|
||||
@@ -27,128 +20,7 @@ export const externalMigrationServiceFactory = ({
|
||||
projectEnvService,
|
||||
secretService
|
||||
}: TExternalMigrationServiceFactoryDep) => {
|
||||
const importInfisicalData = async ({
|
||||
data,
|
||||
actor,
|
||||
actorId,
|
||||
actorOrgId,
|
||||
actorAuthMethod
|
||||
}: TImportInfisicalDataCreate) => {
|
||||
// Import data to infisical
|
||||
if (!data || !data.projects) {
|
||||
throw new BadRequestError({ message: "No projects found in data" });
|
||||
}
|
||||
|
||||
const orginalToNewProjectId = new Map<string, string>();
|
||||
const orginalToNewEnvironmentId = new Map<string, string>();
|
||||
|
||||
// Import projects
|
||||
const projectPromises = [];
|
||||
for (const [id, project] of data.projects) {
|
||||
const projectPromise = projectService
|
||||
.createProject({
|
||||
actor,
|
||||
actorId,
|
||||
actorOrgId,
|
||||
actorAuthMethod,
|
||||
workspaceName: project.name,
|
||||
createDefaultEnvs: false
|
||||
})
|
||||
.then((projectResponse) => {
|
||||
if (!projectResponse) {
|
||||
throw new BadRequestError({ message: `Failed to import to project [name:${project.name}] [id:${id}]` });
|
||||
}
|
||||
orginalToNewProjectId.set(project.id, projectResponse.id);
|
||||
});
|
||||
projectPromises.push(projectPromise);
|
||||
}
|
||||
await Promise.all(projectPromises);
|
||||
|
||||
// Invite user importing projects
|
||||
const response = await orgService.inviteUserToOrganization({
|
||||
actorAuthMethod,
|
||||
actorId,
|
||||
actorOrgId,
|
||||
actor,
|
||||
inviteeEmails: [],
|
||||
orgId: actorOrgId,
|
||||
organizationRoleSlug: OrgMembershipRole.NoAccess,
|
||||
projects: Array.from(orginalToNewProjectId.values()).map((project) => {
|
||||
return {
|
||||
id: project,
|
||||
projectRoleSlug: [ProjectMembershipRole.Member]
|
||||
};
|
||||
})
|
||||
});
|
||||
if (!response) {
|
||||
throw new BadRequestError({ message: `Failed to invite user to projects: [userId:${actorId}]` });
|
||||
}
|
||||
|
||||
// Import environments
|
||||
if (data.environments) {
|
||||
for await (const [id, environment] of data.environments) {
|
||||
try {
|
||||
// TODO: we can create envs parallely once the position constraint is handled differently
|
||||
const newEnvironment = await projectEnvService.createEnvironment({
|
||||
actor,
|
||||
actorId,
|
||||
actorOrgId,
|
||||
actorAuthMethod,
|
||||
name: environment.name,
|
||||
projectId: orginalToNewProjectId.get(environment.projectId)!,
|
||||
slug: slugify(`${environment.name}-${alphaNumericNanoId(4)}`)
|
||||
});
|
||||
|
||||
if (!newEnvironment) {
|
||||
logger.error(`Failed to import environment: [name:${environment.name}] [id:${id}]`);
|
||||
throw new BadRequestError({
|
||||
message: `Failed to import environment: [name:${environment.name}] [id:${id}]`
|
||||
});
|
||||
}
|
||||
orginalToNewEnvironmentId.set(id, newEnvironment.slug);
|
||||
} catch (error) {
|
||||
return {
|
||||
success: false
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Import secrets
|
||||
if (data.secrets) {
|
||||
for await (const [id, secret] of data.secrets) {
|
||||
const dataProjectId = data.environments?.get(secret.environmentId)?.projectId;
|
||||
if (!dataProjectId) {
|
||||
logger.error(`Failed to import secret: [name:${secret.name}] [id:${id}], project not found`);
|
||||
return {
|
||||
success: false,
|
||||
message: `Failed to import secret: [name:${secret.name}] [id:${id}], project not found`
|
||||
};
|
||||
}
|
||||
const projectId = orginalToNewProjectId.get(dataProjectId);
|
||||
// TODO: we can create secrets parallely once the KMS ID bug on create is fixed
|
||||
const newSecret = await secretService.createSecretRaw({
|
||||
actorId,
|
||||
actor,
|
||||
actorOrgId,
|
||||
environment: orginalToNewEnvironmentId.get(secret.environmentId)!,
|
||||
actorAuthMethod,
|
||||
projectId: projectId!,
|
||||
secretPath: "/",
|
||||
secretName: secret.name,
|
||||
type: SecretType.Shared,
|
||||
secretValue: secret.value
|
||||
});
|
||||
if (!newSecret) {
|
||||
throw new BadRequestError({ message: `Failed to import secret: [name:${secret.name}] [id:${id}]` });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
};
|
||||
|
||||
const importEnvnKeyData = async ({
|
||||
const importEnvKeyData = async ({
|
||||
decryptionKey,
|
||||
encryptedJson,
|
||||
actor,
|
||||
@@ -156,13 +28,19 @@ export const externalMigrationServiceFactory = ({
|
||||
actorOrgId,
|
||||
actorAuthMethod
|
||||
}: TImportEnvKeyDataCreate) => {
|
||||
const json = await decryptEnvKeyData(decryptionKey, encryptedJson);
|
||||
const envKeyData = await parseEnvKeyData(json);
|
||||
const response = await importInfisicalData({ data: envKeyData, actor, actorId, actorOrgId, actorAuthMethod });
|
||||
const json = await decryptEnvKeyDataFn(decryptionKey, encryptedJson);
|
||||
const envKeyData = await parseEnvKeyDataFn(json);
|
||||
const response = await importDataIntoInfisicalFn({
|
||||
input: { data: envKeyData, actor, actorId, actorOrgId, actorAuthMethod },
|
||||
projectService,
|
||||
orgService,
|
||||
projectEnvService,
|
||||
secretService
|
||||
});
|
||||
return response;
|
||||
};
|
||||
|
||||
return {
|
||||
importEnvnKeyData
|
||||
importEnvKeyData
|
||||
};
|
||||
};
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { ForbiddenError } from "@casl/ability";
|
||||
import slugify from "@sindresorhus/slugify";
|
||||
|
||||
import { OrgMembershipRole, ProjectMembershipRole, ProjectVersion } from "@app/db/schemas";
|
||||
import { OrgMembershipRole, ProjectMembershipRole, ProjectVersion, TProjectEnvironments } from "@app/db/schemas";
|
||||
import { TLicenseServiceFactory } from "@app/ee/services/license/license-service";
|
||||
import { OrgPermissionActions, OrgPermissionSubjects } from "@app/ee/services/permission/org-permission";
|
||||
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service";
|
||||
@@ -208,15 +208,7 @@ export const projectServiceFactory = ({
|
||||
);
|
||||
|
||||
// set default environments and root folder for provided environments
|
||||
let envs: {
|
||||
id: string;
|
||||
createdAt: Date;
|
||||
updatedAt: Date;
|
||||
projectId: string;
|
||||
name: string;
|
||||
slug: string;
|
||||
position: number;
|
||||
}[] = [];
|
||||
let envs: TProjectEnvironments[] = [];
|
||||
if (createDefaultEnvs) {
|
||||
envs = await projectEnvDAL.insertMany(
|
||||
DEFAULT_PROJECT_ENVS.map((el, i) => ({ ...el, projectId: project.id, position: i + 1 })),
|
||||
|
||||
@@ -24,8 +24,7 @@ type TForm = z.infer<typeof formSchema>;
|
||||
export const ImportTab = () => {
|
||||
const fileUploadRef = useRef<HTMLInputElement>(null);
|
||||
|
||||
const { mutateAsync: importEnvKey
|
||||
} = useImportEnvKey();
|
||||
const { mutateAsync: importEnvKey } = useImportEnvKey();
|
||||
|
||||
const {
|
||||
handleSubmit,
|
||||
@@ -50,11 +49,14 @@ export const ImportTab = () => {
|
||||
|
||||
const parseJson = (src: ArrayBuffer) => {
|
||||
const file = src.toString();
|
||||
const formatedData: Record<string, string> = JSON.parse(file);
|
||||
if (Object.keys(formatedData).includes("nonce") && Object.keys(formatedData).includes("data")) {
|
||||
const formattedData: Record<string, string> = JSON.parse(file);
|
||||
if (
|
||||
Object.keys(formattedData).includes("nonce") &&
|
||||
Object.keys(formattedData).includes("data")
|
||||
) {
|
||||
const data = {
|
||||
nonce: formatedData.nonce,
|
||||
data: formatedData.data
|
||||
nonce: formattedData.nonce,
|
||||
data: formattedData.data
|
||||
};
|
||||
setValue("encryptedJson", data);
|
||||
trigger("encryptedJson");
|
||||
@@ -86,9 +88,9 @@ export const ImportTab = () => {
|
||||
if (!event?.target?.result) return;
|
||||
// parse function's argument looks like to be ArrayBuffer
|
||||
parseJson(event.target.result as ArrayBuffer);
|
||||
}
|
||||
};
|
||||
reader.readAsText(file);
|
||||
}
|
||||
};
|
||||
|
||||
const submitExport = async (data: TForm) => {
|
||||
if (!data.encryptedJson) {
|
||||
@@ -99,7 +101,7 @@ export const ImportTab = () => {
|
||||
return;
|
||||
}
|
||||
|
||||
try{
|
||||
try {
|
||||
await importEnvKey({ encryptedJson: data.encryptedJson, decryptionKey: data.encryptionKey });
|
||||
createNotification({
|
||||
text: "Data imported successfully.",
|
||||
@@ -109,11 +111,10 @@ export const ImportTab = () => {
|
||||
if (fileUploadRef.current) {
|
||||
fileUploadRef.current.value = "";
|
||||
}
|
||||
} catch (error) {
|
||||
} catch {
|
||||
reset();
|
||||
}
|
||||
|
||||
}
|
||||
};
|
||||
|
||||
const watchEncryptedJsonFile: any = watch("file");
|
||||
useEffect(() => {
|
||||
@@ -124,13 +125,13 @@ export const ImportTab = () => {
|
||||
|
||||
return (
|
||||
<div className="rounded-lg border border-mineshaft-600 bg-mineshaft-900 p-6">
|
||||
<h2 className="text-lg font-medium text-white mb-4">Import from external source</h2>
|
||||
<h2 className="mb-4 text-lg font-medium text-white">Import from external source</h2>
|
||||
<p className="text-sm text-mineshaft-400">
|
||||
Import data from another secret manager to Infisical.
|
||||
</p>
|
||||
<div className="border-b border-mineshaft-800 my-6" />
|
||||
<div className="flex justify-left">
|
||||
<h3 className="text-lg font-medium text-white mb-4">Import from EnvKey</h3>
|
||||
<div className="my-6 border-b border-mineshaft-800" />
|
||||
<div className="justify-left flex">
|
||||
<h3 className="mb-4 text-lg font-medium text-white">Import from EnvKey</h3>
|
||||
<Link href="https://infisical.com/docs/documentation/guides/migrating-from-envkey" passHref>
|
||||
<a target="_blank" rel="noopener noreferrer">
|
||||
<div className="ml-2 mb-1 inline-block rounded-md bg-yellow/20 px-1.5 pb-[0.03rem] pt-[0.04rem] text-sm text-yellow opacity-80 hover:opacity-100">
|
||||
@@ -148,12 +149,16 @@ export const ImportTab = () => {
|
||||
<form onSubmit={handleSubmit(submitExport)}>
|
||||
<Controller
|
||||
render={({ field: { onChange, ...field }, fieldState: { error } }) => (
|
||||
<FormControl errorText={error?.message} isError={Boolean(error)} label="Encryption Key">
|
||||
<FormControl
|
||||
errorText={error?.message}
|
||||
isError={Boolean(error)}
|
||||
label="Encryption Key"
|
||||
>
|
||||
<input
|
||||
{...field}
|
||||
onChange={onChange}
|
||||
type="password"
|
||||
className="w-full bg-mineshaft-800 text-white rounded-lg py-2 px-4"
|
||||
className="w-full rounded-lg bg-mineshaft-800 py-2 px-4 text-white"
|
||||
placeholder="Enter encryption key"
|
||||
/>
|
||||
</FormControl>
|
||||
@@ -161,14 +166,18 @@ export const ImportTab = () => {
|
||||
name="encryptionKey"
|
||||
control={control}
|
||||
/>
|
||||
<div className="flex justify-left">
|
||||
<div className="justify-left flex">
|
||||
<Controller
|
||||
name="file"
|
||||
control={control}
|
||||
defaultValue={null}
|
||||
rules={{ required: "File is required" }}
|
||||
render={({ field, fieldState: { error } }) => (
|
||||
<FormControl errorText={error?.message} isError={Boolean(error)} label="Export file from EnvKey">
|
||||
<FormControl
|
||||
errorText={error?.message}
|
||||
isError={Boolean(error)}
|
||||
label="Export file from EnvKey"
|
||||
>
|
||||
<>
|
||||
<input
|
||||
id="fileSelect"
|
||||
@@ -185,22 +194,22 @@ export const ImportTab = () => {
|
||||
onClick={() => {
|
||||
fileUploadRef?.current?.click();
|
||||
}}
|
||||
>
|
||||
>
|
||||
{fileUploadRef?.current?.value ? (
|
||||
<span className="text-green text-sm">
|
||||
<span className="text-sm text-green">
|
||||
{fileUploadRef?.current?.value.split("\\").pop()}
|
||||
</span>
|
||||
) : (
|
||||
<>
|
||||
Upload export file
|
||||
<FontAwesomeIcon icon={faUpload} size="xs" />
|
||||
</>
|
||||
Upload export file
|
||||
<FontAwesomeIcon icon={faUpload} size="xs" />
|
||||
</>
|
||||
)}
|
||||
</IconButton>
|
||||
</>
|
||||
</FormControl>
|
||||
)} />
|
||||
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
<div className="mt-6">
|
||||
<Button
|
||||
|
||||
Reference in New Issue
Block a user