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

View File

@@ -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,

View File

@@ -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}]` });
}
}
}
};

View File

@@ -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
};
};

View File

@@ -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 })),

View File

@@ -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&nbsp;
<FontAwesomeIcon icon={faUpload} size="xs" />
</>
Upload export file&nbsp;
<FontAwesomeIcon icon={faUpload} size="xs" />
</>
)}
</IconButton>
</>
</FormControl>
)} />
)}
/>
</div>
<div className="mt-6">
<Button