fix: improvements

This commit is contained in:
Daniel Hougaard
2024-10-01 03:51:55 +04:00
parent 22b6e0afcd
commit 0485b56e8d
5 changed files with 180 additions and 180 deletions
@@ -18,14 +18,11 @@ export const registerExternalMigrationRouter = async (server: FastifyZodProvider
nonce: z.string().trim().min(1), nonce: z.string().trim().min(1),
data: z.string().trim().min(1) data: z.string().trim().min(1)
}) })
}), })
response: {
200: z.object({})
}
}, },
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
handler: async (req) => { handler: async (req) => {
await server.services.migration.importEnvnKeyData({ await server.services.migration.importEnvKeyData({
decryptionKey: req.body.decryptionKey, decryptionKey: req.body.decryptionKey,
encryptedJson: req.body.encryptedJson, encryptedJson: req.body.encryptedJson,
actorId: req.permission.id, actorId: req.permission.id,
@@ -1,16 +1,33 @@
import slugify from "@sindresorhus/slugify";
import { randomUUID } from "crypto"; import { randomUUID } from "crypto";
import sjcl from "sjcl"; import sjcl from "sjcl";
import tweetnacl from "tweetnacl"; import tweetnacl from "tweetnacl";
import tweetnaclUtil from "tweetnacl-util"; import tweetnaclUtil from "tweetnacl-util";
import { OrgMembershipRole, ProjectMembershipRole, SecretType } from "@app/db/schemas";
import { BadRequestError } from "@app/lib/errors"; 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 { codec, hash } = sjcl;
const { secretbox } = tweetnacl; 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 key = tweetnaclUtil.decodeBase64(codec.base64.fromBits(hash.sha256.hash(decryptionKey)));
const nonce = tweetnaclUtil.decodeBase64(encryptedJson.nonce); const nonce = tweetnaclUtil.decodeBase64(encryptedJson.nonce);
const encryptedData = tweetnaclUtil.decodeBase64(encryptedJson.data); const encryptedData = tweetnaclUtil.decodeBase64(encryptedJson.data);
@@ -25,7 +42,7 @@ export const decryptEnvKeyData = async (decryptionKey: string, encryptedJson: {
return decryptedJson; 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 parsedJson: TEnvKeyExportJSON = JSON.parse(decryptedJson) as TEnvKeyExportJSON;
const infisicalImportData: InfisicalImportData = { const infisicalImportData: InfisicalImportData = {
@@ -71,3 +88,110 @@ export const parseEnvKeyData = async (decryptedJson: string): Promise<InfisicalI
return infisicalImportData; 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 { TOrgServiceFactory } from "../org/org-service";
import { TProjectServiceFactory } from "../project/project-service"; import { TProjectServiceFactory } from "../project/project-service";
import { TProjectEnvServiceFactory } from "../project-env/project-env-service"; import { TProjectEnvServiceFactory } from "../project-env/project-env-service";
import { TSecretServiceFactory } from "../secret/secret-service"; import { TSecretServiceFactory } from "../secret/secret-service";
import { decryptEnvKeyData, parseEnvKeyData } from "./external-migration-fns"; import { decryptEnvKeyDataFn, importDataIntoInfisicalFn, parseEnvKeyDataFn } from "./external-migration-fns";
import { TImportEnvKeyDataCreate, TImportInfisicalDataCreate } from "./external-migration-types"; import { TImportEnvKeyDataCreate } from "./external-migration-types";
type TExternalMigrationServiceFactoryDep = { type TExternalMigrationServiceFactoryDep = {
projectService: TProjectServiceFactory; projectService: TProjectServiceFactory;
@@ -27,128 +20,7 @@ export const externalMigrationServiceFactory = ({
projectEnvService, projectEnvService,
secretService secretService
}: TExternalMigrationServiceFactoryDep) => { }: TExternalMigrationServiceFactoryDep) => {
const importInfisicalData = async ({ const importEnvKeyData = 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 ({
decryptionKey, decryptionKey,
encryptedJson, encryptedJson,
actor, actor,
@@ -156,13 +28,19 @@ export const externalMigrationServiceFactory = ({
actorOrgId, actorOrgId,
actorAuthMethod actorAuthMethod
}: TImportEnvKeyDataCreate) => { }: TImportEnvKeyDataCreate) => {
const json = await decryptEnvKeyData(decryptionKey, encryptedJson); const json = await decryptEnvKeyDataFn(decryptionKey, encryptedJson);
const envKeyData = await parseEnvKeyData(json); const envKeyData = await parseEnvKeyDataFn(json);
const response = await importInfisicalData({ data: envKeyData, actor, actorId, actorOrgId, actorAuthMethod }); const response = await importDataIntoInfisicalFn({
input: { data: envKeyData, actor, actorId, actorOrgId, actorAuthMethod },
projectService,
orgService,
projectEnvService,
secretService
});
return response; return response;
}; };
return { return {
importEnvnKeyData importEnvKeyData
}; };
}; };
@@ -1,7 +1,7 @@
import { ForbiddenError } from "@casl/ability"; import { ForbiddenError } from "@casl/ability";
import slugify from "@sindresorhus/slugify"; 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 { TLicenseServiceFactory } from "@app/ee/services/license/license-service";
import { OrgPermissionActions, OrgPermissionSubjects } from "@app/ee/services/permission/org-permission"; import { OrgPermissionActions, OrgPermissionSubjects } from "@app/ee/services/permission/org-permission";
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service"; 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 // set default environments and root folder for provided environments
let envs: { let envs: TProjectEnvironments[] = [];
id: string;
createdAt: Date;
updatedAt: Date;
projectId: string;
name: string;
slug: string;
position: number;
}[] = [];
if (createDefaultEnvs) { if (createDefaultEnvs) {
envs = await projectEnvDAL.insertMany( envs = await projectEnvDAL.insertMany(
DEFAULT_PROJECT_ENVS.map((el, i) => ({ ...el, projectId: project.id, position: i + 1 })), 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 = () => { export const ImportTab = () => {
const fileUploadRef = useRef<HTMLInputElement>(null); const fileUploadRef = useRef<HTMLInputElement>(null);
const { mutateAsync: importEnvKey const { mutateAsync: importEnvKey } = useImportEnvKey();
} = useImportEnvKey();
const { const {
handleSubmit, handleSubmit,
@@ -50,11 +49,14 @@ export const ImportTab = () => {
const parseJson = (src: ArrayBuffer) => { const parseJson = (src: ArrayBuffer) => {
const file = src.toString(); const file = src.toString();
const formatedData: Record<string, string> = JSON.parse(file); const formattedData: Record<string, string> = JSON.parse(file);
if (Object.keys(formatedData).includes("nonce") && Object.keys(formatedData).includes("data")) { if (
Object.keys(formattedData).includes("nonce") &&
Object.keys(formattedData).includes("data")
) {
const data = { const data = {
nonce: formatedData.nonce, nonce: formattedData.nonce,
data: formatedData.data data: formattedData.data
}; };
setValue("encryptedJson", data); setValue("encryptedJson", data);
trigger("encryptedJson"); trigger("encryptedJson");
@@ -86,9 +88,9 @@ export const ImportTab = () => {
if (!event?.target?.result) return; if (!event?.target?.result) return;
// parse function's argument looks like to be ArrayBuffer // parse function's argument looks like to be ArrayBuffer
parseJson(event.target.result as ArrayBuffer); parseJson(event.target.result as ArrayBuffer);
} };
reader.readAsText(file); reader.readAsText(file);
} };
const submitExport = async (data: TForm) => { const submitExport = async (data: TForm) => {
if (!data.encryptedJson) { if (!data.encryptedJson) {
@@ -99,7 +101,7 @@ export const ImportTab = () => {
return; return;
} }
try{ try {
await importEnvKey({ encryptedJson: data.encryptedJson, decryptionKey: data.encryptionKey }); await importEnvKey({ encryptedJson: data.encryptedJson, decryptionKey: data.encryptionKey });
createNotification({ createNotification({
text: "Data imported successfully.", text: "Data imported successfully.",
@@ -109,11 +111,10 @@ export const ImportTab = () => {
if (fileUploadRef.current) { if (fileUploadRef.current) {
fileUploadRef.current.value = ""; fileUploadRef.current.value = "";
} }
} catch (error) { } catch {
reset(); reset();
} }
};
}
const watchEncryptedJsonFile: any = watch("file"); const watchEncryptedJsonFile: any = watch("file");
useEffect(() => { useEffect(() => {
@@ -124,13 +125,13 @@ export const ImportTab = () => {
return ( return (
<div className="rounded-lg border border-mineshaft-600 bg-mineshaft-900 p-6"> <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"> <p className="text-sm text-mineshaft-400">
Import data from another secret manager to Infisical. Import data from another secret manager to Infisical.
</p> </p>
<div className="border-b border-mineshaft-800 my-6" /> <div className="my-6 border-b border-mineshaft-800" />
<div className="flex justify-left"> <div className="justify-left flex">
<h3 className="text-lg font-medium text-white mb-4">Import from EnvKey</h3> <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> <Link href="https://infisical.com/docs/documentation/guides/migrating-from-envkey" passHref>
<a target="_blank" rel="noopener noreferrer"> <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"> <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)}> <form onSubmit={handleSubmit(submitExport)}>
<Controller <Controller
render={({ field: { onChange, ...field }, fieldState: { error } }) => ( 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 <input
{...field} {...field}
onChange={onChange} onChange={onChange}
type="password" 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" placeholder="Enter encryption key"
/> />
</FormControl> </FormControl>
@@ -161,14 +166,18 @@ export const ImportTab = () => {
name="encryptionKey" name="encryptionKey"
control={control} control={control}
/> />
<div className="flex justify-left"> <div className="justify-left flex">
<Controller <Controller
name="file" name="file"
control={control} control={control}
defaultValue={null} defaultValue={null}
rules={{ required: "File is required" }} rules={{ required: "File is required" }}
render={({ field, fieldState: { error } }) => ( 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 <input
id="fileSelect" id="fileSelect"
@@ -185,22 +194,22 @@ export const ImportTab = () => {
onClick={() => { onClick={() => {
fileUploadRef?.current?.click(); fileUploadRef?.current?.click();
}} }}
> >
{fileUploadRef?.current?.value ? ( {fileUploadRef?.current?.value ? (
<span className="text-green text-sm"> <span className="text-sm text-green">
{fileUploadRef?.current?.value.split("\\").pop()} {fileUploadRef?.current?.value.split("\\").pop()}
</span> </span>
) : ( ) : (
<> <>
Upload export file&nbsp; Upload export file&nbsp;
<FontAwesomeIcon icon={faUpload} size="xs" /> <FontAwesomeIcon icon={faUpload} size="xs" />
</> </>
)} )}
</IconButton> </IconButton>
</> </>
</FormControl> </FormControl>
)} /> )}
/>
</div> </div>
<div className="mt-6"> <div className="mt-6">
<Button <Button