mirror of
https://github.com/awatertrevi/infisical.git
synced 2026-10-07 16:27:40 +00:00
checkpoint
This commit is contained in:
@@ -349,4 +349,35 @@ export const registerProjectRouter = async (server: FastifyZodProvider) => {
|
|||||||
return backup;
|
return backup;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
server.route({
|
||||||
|
method: "POST",
|
||||||
|
url: "/:workspaceId/migrate-v3",
|
||||||
|
config: {
|
||||||
|
rateLimit: writeLimit
|
||||||
|
},
|
||||||
|
schema: {
|
||||||
|
params: z.object({
|
||||||
|
workspaceId: z.string().trim()
|
||||||
|
}),
|
||||||
|
|
||||||
|
response: {
|
||||||
|
200: z.object({
|
||||||
|
message: z.string()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onRequest: verifyAuth([AuthMode.JWT]),
|
||||||
|
handler: async (req) => {
|
||||||
|
const migration = await server.services.secret.startSecretV2Migration({
|
||||||
|
actor: req.permission.type,
|
||||||
|
actorId: req.permission.id,
|
||||||
|
actorAuthMethod: req.permission.authMethod,
|
||||||
|
actorOrgId: req.permission.orgId,
|
||||||
|
projectId: req.params.workspaceId
|
||||||
|
});
|
||||||
|
|
||||||
|
return migration;
|
||||||
|
}
|
||||||
|
});
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -25,7 +25,8 @@ export enum QueueName {
|
|||||||
DynamicSecretRevocation = "dynamic-secret-revocation",
|
DynamicSecretRevocation = "dynamic-secret-revocation",
|
||||||
CaCrlRotation = "ca-crl-rotation",
|
CaCrlRotation = "ca-crl-rotation",
|
||||||
SecretReplication = "secret-replication",
|
SecretReplication = "secret-replication",
|
||||||
SecretSync = "secret-sync" // parent queue to push integration sync, webhook, and secret replication
|
SecretSync = "secret-sync", // parent queue to push integration sync, webhook, and secret replication
|
||||||
|
ProjectV3Migration = "project-v3-migration"
|
||||||
}
|
}
|
||||||
|
|
||||||
export enum QueueJobs {
|
export enum QueueJobs {
|
||||||
@@ -44,7 +45,8 @@ export enum QueueJobs {
|
|||||||
DynamicSecretPruning = "dynamic-secret-pruning",
|
DynamicSecretPruning = "dynamic-secret-pruning",
|
||||||
CaCrlRotation = "ca-crl-rotation-job",
|
CaCrlRotation = "ca-crl-rotation-job",
|
||||||
SecretReplication = "secret-replication",
|
SecretReplication = "secret-replication",
|
||||||
SecretSync = "secret-sync" // parent queue to push integration sync, webhook, and secret replication
|
SecretSync = "secret-sync", // parent queue to push integration sync, webhook, and secret replication
|
||||||
|
ProjectV3Migration = "project-v3-migration"
|
||||||
}
|
}
|
||||||
|
|
||||||
export type TQueueJobTypes = {
|
export type TQueueJobTypes = {
|
||||||
@@ -136,6 +138,10 @@ export type TQueueJobTypes = {
|
|||||||
name: QueueJobs.SecretSync;
|
name: QueueJobs.SecretSync;
|
||||||
payload: TSyncSecretsDTO;
|
payload: TSyncSecretsDTO;
|
||||||
};
|
};
|
||||||
|
[QueueName.ProjectV3Migration]: {
|
||||||
|
name: QueueJobs.ProjectV3Migration;
|
||||||
|
payload: { projectId: string };
|
||||||
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
export type TQueueServiceFactory = ReturnType<typeof queueServiceFactory>;
|
export type TQueueServiceFactory = ReturnType<typeof queueServiceFactory>;
|
||||||
|
|||||||
@@ -331,6 +331,27 @@ export const secretFolderDALFactory = (db: TDbClient) => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// special query for project migration
|
||||||
|
const findByProjectId = async (projectId: string, tx?: Knex) => {
|
||||||
|
try {
|
||||||
|
const folders = await (tx || db.replicaNode())(TableName.SecretFolder)
|
||||||
|
.join(TableName.Environment, `${TableName.SecretFolder}.envId`, `${TableName.Environment}.id`)
|
||||||
|
.join(TableName.Project, `${TableName.Environment}.projectId`, `${TableName.Project}.id`)
|
||||||
|
.select(selectAllTableCols(TableName.SecretFolder))
|
||||||
|
.where({ projectId })
|
||||||
|
.select(
|
||||||
|
db.ref("id").withSchema(TableName.Environment).as("envId"),
|
||||||
|
db.ref("slug").withSchema(TableName.Environment).as("envSlug"),
|
||||||
|
db.ref("name").withSchema(TableName.Environment).as("envName"),
|
||||||
|
db.ref("projectId").withSchema(TableName.Environment),
|
||||||
|
db.ref("version").withSchema(TableName.Project).as("projectVersion")
|
||||||
|
);
|
||||||
|
return folders;
|
||||||
|
} catch (error) {
|
||||||
|
throw new DatabaseError({ error, name: "Find by id" });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
return {
|
return {
|
||||||
...secretFolderOrm,
|
...secretFolderOrm,
|
||||||
update,
|
update,
|
||||||
@@ -338,6 +359,7 @@ export const secretFolderDALFactory = (db: TDbClient) => {
|
|||||||
findById,
|
findById,
|
||||||
findByManySecretPath,
|
findByManySecretPath,
|
||||||
findSecretPathByFolderIds,
|
findSecretPathByFolderIds,
|
||||||
findClosestFolder
|
findClosestFolder,
|
||||||
|
findByProjectId
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -41,6 +41,7 @@ import {
|
|||||||
TRemoveSecretReminderDTO,
|
TRemoveSecretReminderDTO,
|
||||||
TSyncSecretsDTO
|
TSyncSecretsDTO
|
||||||
} from "./secret-types";
|
} from "./secret-types";
|
||||||
|
import { ProjectUpgradeStatus, ProjectVersion } from "@app/db/schemas";
|
||||||
|
|
||||||
export type TSecretQueueFactory = ReturnType<typeof secretQueueFactory>;
|
export type TSecretQueueFactory = ReturnType<typeof secretQueueFactory>;
|
||||||
type TSecretQueueFactoryDep = {
|
type TSecretQueueFactoryDep = {
|
||||||
@@ -52,7 +53,7 @@ type TSecretQueueFactoryDep = {
|
|||||||
secretDAL: TSecretDALFactory;
|
secretDAL: TSecretDALFactory;
|
||||||
secretImportDAL: Pick<TSecretImportDALFactory, "find">;
|
secretImportDAL: Pick<TSecretImportDALFactory, "find">;
|
||||||
webhookDAL: Pick<TWebhookDALFactory, "findAllWebhooks" | "transaction" | "update" | "bulkUpdate">;
|
webhookDAL: Pick<TWebhookDALFactory, "findAllWebhooks" | "transaction" | "update" | "bulkUpdate">;
|
||||||
projectEnvDAL: Pick<TProjectEnvDALFactory, "findOne">;
|
projectEnvDAL: Pick<TProjectEnvDALFactory, "findOne" | "find">;
|
||||||
projectDAL: TProjectDALFactory;
|
projectDAL: TProjectDALFactory;
|
||||||
projectBotDAL: TProjectBotDALFactory;
|
projectBotDAL: TProjectBotDALFactory;
|
||||||
projectMembershipDAL: Pick<TProjectMembershipDALFactory, "findAllProjectMembers">;
|
projectMembershipDAL: Pick<TProjectMembershipDALFactory, "findAllProjectMembers">;
|
||||||
@@ -761,6 +762,108 @@ export const secretQueueFactory = ({
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const startSecretV2Migration = async (projectId: string) => {
|
||||||
|
await queueService.queue(
|
||||||
|
QueueName.ProjectV3Migration,
|
||||||
|
QueueJobs.ProjectV3Migration,
|
||||||
|
{ projectId },
|
||||||
|
{
|
||||||
|
attempts: 2,
|
||||||
|
backoff: {
|
||||||
|
type: "exponential",
|
||||||
|
delay: 3000
|
||||||
|
},
|
||||||
|
removeOnComplete: true,
|
||||||
|
removeOnFail: true
|
||||||
|
}
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const MIGRATION_BATCH_SIZE = 10000;
|
||||||
|
queueService.start(QueueName.ProjectV3Migration, async (job) => {
|
||||||
|
const { projectId } = job.data;
|
||||||
|
const { botKey, shouldUseSecretV2Bridge: isProjectUpgradedToV3 } = await projectBotService.getBotKey(projectId);
|
||||||
|
if (isProjectUpgradedToV3) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!botKey) throw new BadRequestError({ message: "Bot not found" });
|
||||||
|
await projectDAL.updateById(projectId, { upgradeStatus: ProjectUpgradeStatus.InProgress });
|
||||||
|
const { encryptor: secretManagerEncryptor } = await kmsService.createCipherPairWithDataKey({
|
||||||
|
projectId,
|
||||||
|
type: KmsDataKey.SecretManager
|
||||||
|
});
|
||||||
|
|
||||||
|
const folders = await folderDAL.findByProjectId(projectId);
|
||||||
|
// except secret version and snapshot migrate rest of everything first in a transaction
|
||||||
|
await secretDAL.transaction(async (tx) => {
|
||||||
|
for (const folder of folders) {
|
||||||
|
const folderId = folder.id;
|
||||||
|
let projectV1Secrets;
|
||||||
|
do {
|
||||||
|
// eslint-disable-next-line no-await-in-loop
|
||||||
|
projectV1Secrets = await secretDAL.find({ folderId }, { limit: MIGRATION_BATCH_SIZE, tx });
|
||||||
|
await secretV2BridgeDAL.insertMany(
|
||||||
|
projectV1Secrets.map((el) => {
|
||||||
|
const key = decryptSymmetric128BitHexKeyUTF8({
|
||||||
|
ciphertext: el.secretKeyCiphertext,
|
||||||
|
iv: el.secretKeyIV,
|
||||||
|
tag: el.secretKeyTag,
|
||||||
|
key: botKey
|
||||||
|
});
|
||||||
|
const value = decryptSymmetric128BitHexKeyUTF8({
|
||||||
|
ciphertext: el.secretValueCiphertext,
|
||||||
|
iv: el.secretValueIV,
|
||||||
|
tag: el.secretValueTag,
|
||||||
|
key: botKey
|
||||||
|
});
|
||||||
|
const comment =
|
||||||
|
el.secretCommentCiphertext && el.secretCommentTag && el.secretCommentIV
|
||||||
|
? decryptSymmetric128BitHexKeyUTF8({
|
||||||
|
ciphertext: el.secretCommentCiphertext,
|
||||||
|
iv: el.secretCommentIV,
|
||||||
|
tag: el.secretCommentTag,
|
||||||
|
key: botKey
|
||||||
|
})
|
||||||
|
: "";
|
||||||
|
const encryptedValue = secretManagerEncryptor({ plainText: Buffer.from(value) }).cipherTextBlob;
|
||||||
|
const encryptedComment = comment
|
||||||
|
? secretManagerEncryptor({ plainText: Buffer.from(comment) }).cipherTextBlob
|
||||||
|
: null;
|
||||||
|
return {
|
||||||
|
id: el.id,
|
||||||
|
createdAt: el.createdAt,
|
||||||
|
updatedAt: el.updatedAt,
|
||||||
|
skipMultilineEncoding: el.skipMultilineEncoding,
|
||||||
|
encryptedComment,
|
||||||
|
encryptedValue,
|
||||||
|
key,
|
||||||
|
version: el.version,
|
||||||
|
type: el.type,
|
||||||
|
userId: el.userId,
|
||||||
|
folderId: el.folderId,
|
||||||
|
metadata: el.metadata,
|
||||||
|
reminderNote: el.secretReminderNote,
|
||||||
|
reminderRepeatDays: el.secretReminderRepeatDays
|
||||||
|
};
|
||||||
|
}),
|
||||||
|
tx
|
||||||
|
);
|
||||||
|
projectV1Secrets = await secretDAL.delete({ folderId, $in: { id: projectV1Secrets.map((el) => el.id) } }, tx);
|
||||||
|
} while (projectV1Secrets.length > 0);
|
||||||
|
}
|
||||||
|
await projectDAL.updateById(projectId, { upgradeStatus: null, version: ProjectVersion.V3 }, tx);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// eslint-disable-next-line
|
||||||
|
queueService.listen(QueueName.ProjectV3Migration, "failed", async (job, err) => {
|
||||||
|
if (job?.data) {
|
||||||
|
const { projectId } = job.data;
|
||||||
|
await projectDAL.updateById(projectId, { upgradeStatus: ProjectUpgradeStatus.Failed });
|
||||||
|
logger.error(err, `Failed to migrate project to v3: ${projectId}`);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
queueService.listen(QueueName.IntegrationSync, "failed", (job, err) => {
|
queueService.listen(QueueName.IntegrationSync, "failed", (job, err) => {
|
||||||
logger.error(err, "Failed to sync integration %s", job?.id);
|
logger.error(err, "Failed to sync integration %s", job?.id);
|
||||||
});
|
});
|
||||||
@@ -772,6 +875,7 @@ export const secretQueueFactory = ({
|
|||||||
return {
|
return {
|
||||||
// depth is internal only field thus no need to make it available outside
|
// depth is internal only field thus no need to make it available outside
|
||||||
syncSecrets,
|
syncSecrets,
|
||||||
|
startSecretV2Migration,
|
||||||
syncIntegrations,
|
syncIntegrations,
|
||||||
addSecretReminder,
|
addSecretReminder,
|
||||||
removeSecretReminder,
|
removeSecretReminder,
|
||||||
|
|||||||
@@ -68,6 +68,7 @@ import {
|
|||||||
TGetSecretsRawDTO,
|
TGetSecretsRawDTO,
|
||||||
TGetSecretVersionsDTO,
|
TGetSecretVersionsDTO,
|
||||||
TMoveSecretsDTO,
|
TMoveSecretsDTO,
|
||||||
|
TStartSecretsV2MigrationDTO,
|
||||||
TUpdateBulkSecretDTO,
|
TUpdateBulkSecretDTO,
|
||||||
TUpdateManySecretRawDTO,
|
TUpdateManySecretRawDTO,
|
||||||
TUpdateSecretDTO,
|
TUpdateSecretDTO,
|
||||||
@@ -90,7 +91,10 @@ type TSecretServiceFactoryDep = {
|
|||||||
secretBlindIndexDAL: TSecretBlindIndexDALFactory;
|
secretBlindIndexDAL: TSecretBlindIndexDALFactory;
|
||||||
permissionService: Pick<TPermissionServiceFactory, "getProjectPermission">;
|
permissionService: Pick<TPermissionServiceFactory, "getProjectPermission">;
|
||||||
snapshotService: Pick<TSecretSnapshotServiceFactory, "performSnapshot">;
|
snapshotService: Pick<TSecretSnapshotServiceFactory, "performSnapshot">;
|
||||||
secretQueueService: Pick<TSecretQueueFactory, "syncSecrets" | "handleSecretReminder" | "removeSecretReminder">;
|
secretQueueService: Pick<
|
||||||
|
TSecretQueueFactory,
|
||||||
|
"syncSecrets" | "handleSecretReminder" | "removeSecretReminder" | "startSecretV2Migration"
|
||||||
|
>;
|
||||||
projectBotService: Pick<TProjectBotServiceFactory, "getBotKey">;
|
projectBotService: Pick<TProjectBotServiceFactory, "getBotKey">;
|
||||||
secretImportDAL: Pick<TSecretImportDALFactory, "find" | "findByFolderIds">;
|
secretImportDAL: Pick<TSecretImportDALFactory, "find" | "findByFolderIds">;
|
||||||
secretVersionTagDAL: Pick<TSecretVersionTagDALFactory, "insertMany">;
|
secretVersionTagDAL: Pick<TSecretVersionTagDALFactory, "insertMany">;
|
||||||
@@ -2638,6 +2642,30 @@ export const secretServiceFactory = ({
|
|||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const startSecretV2Migration = async ({
|
||||||
|
projectId,
|
||||||
|
actor,
|
||||||
|
actorId,
|
||||||
|
actorOrgId,
|
||||||
|
actorAuthMethod
|
||||||
|
}: TStartSecretsV2MigrationDTO) => {
|
||||||
|
const { hasRole } = await permissionService.getProjectPermission(
|
||||||
|
actor,
|
||||||
|
actorId,
|
||||||
|
projectId,
|
||||||
|
actorAuthMethod,
|
||||||
|
actorOrgId
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!hasRole(ProjectMembershipRole.Admin))
|
||||||
|
throw new BadRequestError({ message: "Only admins are allowed to take this action" });
|
||||||
|
|
||||||
|
const { shouldUseSecretV2Bridge: isProjectV3 } = await projectBotService.getBotKey(projectId);
|
||||||
|
if (isProjectV3) throw new BadRequestError({ message: "project is already in v3" });
|
||||||
|
await secretQueueService.startSecretV2Migration(projectId);
|
||||||
|
return { message: "Migrating project to new KMS architecture" };
|
||||||
|
};
|
||||||
|
|
||||||
return {
|
return {
|
||||||
attachTags,
|
attachTags,
|
||||||
detachTags,
|
detachTags,
|
||||||
@@ -2659,6 +2687,7 @@ export const secretServiceFactory = ({
|
|||||||
deleteManySecretsRaw,
|
deleteManySecretsRaw,
|
||||||
getSecretVersions,
|
getSecretVersions,
|
||||||
backfillSecretReferences,
|
backfillSecretReferences,
|
||||||
moveSecrets
|
moveSecrets,
|
||||||
|
startSecretV2Migration
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -449,3 +449,5 @@ export enum SecretProtectionType {
|
|||||||
Approval = "approval",
|
Approval = "approval",
|
||||||
Direct = "direct"
|
Direct = "direct"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type TStartSecretsV2MigrationDTO = TProjectPermission;
|
||||||
|
|||||||
@@ -2,7 +2,8 @@ export {
|
|||||||
useAddGroupToWorkspace,
|
useAddGroupToWorkspace,
|
||||||
useDeleteGroupFromWorkspace,
|
useDeleteGroupFromWorkspace,
|
||||||
useLeaveProject,
|
useLeaveProject,
|
||||||
useUpdateGroupWorkspaceRole
|
useUpdateGroupWorkspaceRole,
|
||||||
|
useMigrateProjectToV3
|
||||||
} from "./mutations";
|
} from "./mutations";
|
||||||
export {
|
export {
|
||||||
useAddIdentityToWorkspace,
|
useAddIdentityToWorkspace,
|
||||||
|
|||||||
@@ -74,3 +74,15 @@ export const useLeaveProject = () => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const useMigrateProjectToV3 = () => {
|
||||||
|
const queryClient = useQueryClient();
|
||||||
|
return useMutation<{}, {}, { workspaceId: string }>({
|
||||||
|
mutationFn: ({ workspaceId }) => {
|
||||||
|
return apiRequest.delete(`/api/v1/workspace/${workspaceId}/migrate-v3`);
|
||||||
|
},
|
||||||
|
onSuccess: () => {
|
||||||
|
queryClient.invalidateQueries(workspaceKeys.getAllUserWorkspace);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|||||||
@@ -150,11 +150,15 @@ export const useGetWorkspaceSecrets = (workspaceId: string) => {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
export const useGetWorkspaceById = (workspaceId: string) => {
|
export const useGetWorkspaceById = (
|
||||||
|
workspaceId: string,
|
||||||
|
dto?: { refetchInterval?: number | false }
|
||||||
|
) => {
|
||||||
return useQuery({
|
return useQuery({
|
||||||
queryKey: workspaceKeys.getWorkspaceById(workspaceId),
|
queryKey: workspaceKeys.getWorkspaceById(workspaceId),
|
||||||
queryFn: () => fetchWorkspaceById(workspaceId),
|
queryFn: () => fetchWorkspaceById(workspaceId),
|
||||||
enabled: Boolean(workspaceId)
|
enabled: Boolean(workspaceId),
|
||||||
|
refetchInterval: dto?.refetchInterval
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
export enum ProjectVersion {
|
export enum ProjectVersion {
|
||||||
V1 = 1,
|
V1 = 1,
|
||||||
V2 = 2
|
V2 = 2,
|
||||||
|
V3 = 3
|
||||||
}
|
}
|
||||||
|
|
||||||
export enum ProjectUserMembershipTemporaryMode {
|
export enum ProjectUserMembershipTemporaryMode {
|
||||||
|
|||||||
@@ -55,7 +55,6 @@ import {
|
|||||||
SelectItem,
|
SelectItem,
|
||||||
UpgradePlanModal
|
UpgradePlanModal
|
||||||
} from "@app/components/v2";
|
} from "@app/components/v2";
|
||||||
import { UpgradeOverlay } from "@app/components/v2/UpgradeOverlay";
|
|
||||||
import {
|
import {
|
||||||
OrgPermissionActions,
|
OrgPermissionActions,
|
||||||
OrgPermissionSubjects,
|
OrgPermissionSubjects,
|
||||||
@@ -335,7 +334,6 @@ export const AppLayout = ({ children }: LayoutProps) => {
|
|||||||
<aside className="dark w-full border-r border-mineshaft-600 bg-gradient-to-tr from-mineshaft-700 via-mineshaft-800 to-mineshaft-900 md:w-60">
|
<aside className="dark w-full border-r border-mineshaft-600 bg-gradient-to-tr from-mineshaft-700 via-mineshaft-800 to-mineshaft-900 md:w-60">
|
||||||
<nav className="items-between flex h-full flex-col justify-between overflow-y-auto dark:[color-scheme:dark]">
|
<nav className="items-between flex h-full flex-col justify-between overflow-y-auto dark:[color-scheme:dark]">
|
||||||
<div>
|
<div>
|
||||||
<UpgradeOverlay />
|
|
||||||
{!router.asPath.includes("personal") && (
|
{!router.asPath.includes("personal") && (
|
||||||
<div className="flex h-12 cursor-default items-center px-3 pt-6">
|
<div className="flex h-12 cursor-default items-center px-3 pt-6">
|
||||||
{(router.asPath.includes("project") ||
|
{(router.asPath.includes("project") ||
|
||||||
|
|||||||
@@ -59,7 +59,6 @@ import {
|
|||||||
useGetFoldersByEnv,
|
useGetFoldersByEnv,
|
||||||
useGetImportedSecretsAllEnvs,
|
useGetImportedSecretsAllEnvs,
|
||||||
useGetProjectSecretsAllEnv,
|
useGetProjectSecretsAllEnv,
|
||||||
useGetUserWsKey,
|
|
||||||
useUpdateSecretV3
|
useUpdateSecretV3
|
||||||
} from "@app/hooks/api";
|
} from "@app/hooks/api";
|
||||||
import { useUpdateFolderBatch } from "@app/hooks/api/secretFolders/queries";
|
import { useUpdateFolderBatch } from "@app/hooks/api/secretFolders/queries";
|
||||||
@@ -70,7 +69,7 @@ import { ProjectVersion } from "@app/hooks/api/workspace/types";
|
|||||||
import { FolderForm } from "../SecretMainPage/components/ActionBar/FolderForm";
|
import { FolderForm } from "../SecretMainPage/components/ActionBar/FolderForm";
|
||||||
import { CreateSecretForm } from "./components/CreateSecretForm";
|
import { CreateSecretForm } from "./components/CreateSecretForm";
|
||||||
import { FolderBreadCrumbs } from "./components/FolderBreadCrumbs";
|
import { FolderBreadCrumbs } from "./components/FolderBreadCrumbs";
|
||||||
import { ProjectIndexSecretsSection } from "./components/ProjectIndexSecretsSection";
|
import { SecretV2MigrationSection } from "./components/SecretV2MigrationSection";
|
||||||
import { SecretOverviewDynamicSecretRow } from "./components/SecretOverviewDynamicSecretRow";
|
import { SecretOverviewDynamicSecretRow } from "./components/SecretOverviewDynamicSecretRow";
|
||||||
import { SecretOverviewFolderRow } from "./components/SecretOverviewFolderRow";
|
import { SecretOverviewFolderRow } from "./components/SecretOverviewFolderRow";
|
||||||
import { SecretOverviewTableRow } from "./components/SecretOverviewTableRow";
|
import { SecretOverviewTableRow } from "./components/SecretOverviewTableRow";
|
||||||
@@ -103,7 +102,6 @@ export const SecretOverviewPage = () => {
|
|||||||
const { currentOrg } = useOrganization();
|
const { currentOrg } = useOrganization();
|
||||||
const workspaceId = currentWorkspace?.id as string;
|
const workspaceId = currentWorkspace?.id as string;
|
||||||
const projectSlug = currentWorkspace?.slug as string;
|
const projectSlug = currentWorkspace?.slug as string;
|
||||||
const { data: latestFileKey } = useGetUserWsKey(workspaceId);
|
|
||||||
const [searchFilter, setSearchFilter] = useState("");
|
const [searchFilter, setSearchFilter] = useState("");
|
||||||
const secretPath = (router.query?.secretPath as string) || "/";
|
const secretPath = (router.query?.secretPath as string) || "/";
|
||||||
|
|
||||||
@@ -475,7 +473,7 @@ export const SecretOverviewPage = () => {
|
|||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div className="container mx-auto px-6 text-mineshaft-50 dark:[color-scheme:dark]">
|
<div className="container mx-auto px-6 text-mineshaft-50 dark:[color-scheme:dark]">
|
||||||
<ProjectIndexSecretsSection decryptFileKey={latestFileKey!} />
|
<SecretV2MigrationSection />
|
||||||
<div className="relative right-5 ml-4">
|
<div className="relative right-5 ml-4">
|
||||||
<NavHeader pageName={t("dashboard.title")} isProjectRelated />
|
<NavHeader pageName={t("dashboard.title")} isProjectRelated />
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
-109
@@ -1,109 +0,0 @@
|
|||||||
import { ProjectPermissionCan } from "@app/components/permissions";
|
|
||||||
import {
|
|
||||||
decryptAssymmetric,
|
|
||||||
decryptSymmetric
|
|
||||||
} from "@app/components/utilities/cryptography/crypto";
|
|
||||||
import { Button, Spinner } from "@app/components/v2";
|
|
||||||
import {
|
|
||||||
ProjectPermissionActions,
|
|
||||||
ProjectPermissionSub,
|
|
||||||
useProjectPermission,
|
|
||||||
useWorkspace
|
|
||||||
} from "@app/context";
|
|
||||||
import { useToggle } from "@app/hooks";
|
|
||||||
import { useGetWorkspaceIndexStatus, useNameWorkspaceSecrets } from "@app/hooks/api";
|
|
||||||
import { ProjectMembershipRole } from "@app/hooks/api/roles/types";
|
|
||||||
import { UserWsKeyPair } from "@app/hooks/api/types";
|
|
||||||
import { fetchWorkspaceSecrets } from "@app/hooks/api/workspace/queries";
|
|
||||||
|
|
||||||
// TODO: add check so that this only shows up if user is
|
|
||||||
// an admin in the workspace
|
|
||||||
type Props = {
|
|
||||||
decryptFileKey: UserWsKeyPair;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const ProjectIndexSecretsSection = ({ decryptFileKey }: Props) => {
|
|
||||||
const { currentWorkspace } = useWorkspace();
|
|
||||||
const { membership } = useProjectPermission();
|
|
||||||
const { data: isBlindIndexed, isLoading: isBlindIndexedLoading } = useGetWorkspaceIndexStatus(
|
|
||||||
currentWorkspace?.id ?? ""
|
|
||||||
);
|
|
||||||
const [isIndexing, setIsIndexing] = useToggle();
|
|
||||||
const nameWorkspaceSecrets = useNameWorkspaceSecrets();
|
|
||||||
|
|
||||||
const onEnableBlindIndices = async () => {
|
|
||||||
if (!currentWorkspace?.id) return;
|
|
||||||
setIsIndexing.on();
|
|
||||||
try {
|
|
||||||
const encryptedSecrets = await fetchWorkspaceSecrets(currentWorkspace.id);
|
|
||||||
|
|
||||||
const key = decryptAssymmetric({
|
|
||||||
ciphertext: decryptFileKey.encryptedKey,
|
|
||||||
nonce: decryptFileKey.nonce,
|
|
||||||
publicKey: decryptFileKey.sender.publicKey,
|
|
||||||
privateKey: localStorage.getItem("PRIVATE_KEY") as string
|
|
||||||
});
|
|
||||||
|
|
||||||
const secretsToUpdate = encryptedSecrets.map((encryptedSecret) => {
|
|
||||||
const secretName = decryptSymmetric({
|
|
||||||
ciphertext: encryptedSecret.secretKeyCiphertext,
|
|
||||||
iv: encryptedSecret.secretKeyIV,
|
|
||||||
tag: encryptedSecret.secretKeyTag,
|
|
||||||
key
|
|
||||||
});
|
|
||||||
|
|
||||||
return {
|
|
||||||
secretName,
|
|
||||||
secretId: encryptedSecret.id
|
|
||||||
};
|
|
||||||
});
|
|
||||||
await nameWorkspaceSecrets.mutateAsync({
|
|
||||||
workspaceId: currentWorkspace.id,
|
|
||||||
secretsToUpdate
|
|
||||||
});
|
|
||||||
} catch (err) {
|
|
||||||
console.log(err);
|
|
||||||
} finally {
|
|
||||||
setIsIndexing.off();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// for non admin this would throw an error
|
|
||||||
// so no need to render
|
|
||||||
return !isBlindIndexedLoading && typeof isBlindIndexed === "boolean" && !isBlindIndexed ? (
|
|
||||||
<div className="mt-4 rounded-lg border border-mineshaft-600 bg-mineshaft-900 p-4">
|
|
||||||
{isIndexing && (
|
|
||||||
<div className="absolute top-0 left-0 z-50 flex h-screen w-screen items-center justify-center bg-bunker-500 bg-opacity-80">
|
|
||||||
<Spinner size="lg" className="text-primary" />
|
|
||||||
<div className="ml-4 flex flex-col space-y-1">
|
|
||||||
<div className="text-3xl font-medium">Please wait</div>
|
|
||||||
<span className="inline-block">Re-indexing your secrets...</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
<p className="mb-2 text-lg font-semibold">Action Required</p>
|
|
||||||
<p className="mb-4 leading-7 text-gray-400">
|
|
||||||
Your project was created before the introduction of blind indexing. To continue accessing
|
|
||||||
secrets by name through the SDK, public API and web dashboard, please enable blind indexing.{" "}
|
|
||||||
<b>
|
|
||||||
{membership.role !== ProjectMembershipRole.Admin && "This is an admin only operation."}
|
|
||||||
</b>
|
|
||||||
</p>
|
|
||||||
<ProjectPermissionCan I={ProjectPermissionActions.Edit} a={ProjectPermissionSub.Settings}>
|
|
||||||
{(isAllowed) => (
|
|
||||||
<Button
|
|
||||||
onClick={onEnableBlindIndices}
|
|
||||||
isDisabled={!isAllowed || membership.role !== ProjectMembershipRole.Admin}
|
|
||||||
color="mineshaft"
|
|
||||||
type="submit"
|
|
||||||
isLoading={isIndexing}
|
|
||||||
>
|
|
||||||
Enable Blind Indexing
|
|
||||||
</Button>
|
|
||||||
)}
|
|
||||||
</ProjectPermissionCan>
|
|
||||||
</div>
|
|
||||||
) : (
|
|
||||||
<div />
|
|
||||||
);
|
|
||||||
};
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
export { ProjectIndexSecretsSection } from "./ProjectIndexSecretsSection";
|
|
||||||
+90
@@ -0,0 +1,90 @@
|
|||||||
|
import { createNotification } from "@app/components/notifications";
|
||||||
|
import { ProjectPermissionCan } from "@app/components/permissions";
|
||||||
|
|
||||||
|
import { Button, Spinner } from "@app/components/v2";
|
||||||
|
import {
|
||||||
|
ProjectPermissionActions,
|
||||||
|
ProjectPermissionSub,
|
||||||
|
useProjectPermission,
|
||||||
|
useWorkspace
|
||||||
|
} from "@app/context";
|
||||||
|
import { useGetWorkspaceById, useMigrateProjectToV3 } from "@app/hooks/api";
|
||||||
|
import { ProjectMembershipRole } from "@app/hooks/api/roles/types";
|
||||||
|
import { ProjectVersion } from "@app/hooks/api/workspace/types";
|
||||||
|
|
||||||
|
enum ProjectUpgradeStatus {
|
||||||
|
InProgress = "IN_PROGRESS",
|
||||||
|
// Completed -> Will be null if completed. So a completed status is not needed
|
||||||
|
Failed = "FAILED"
|
||||||
|
}
|
||||||
|
|
||||||
|
export const SecretV2MigrationSection = () => {
|
||||||
|
const { currentWorkspace } = useWorkspace();
|
||||||
|
const { data: workspaceDetails } = useGetWorkspaceById(
|
||||||
|
// if v3 no need to fetch
|
||||||
|
currentWorkspace?.version === ProjectVersion.V3 ? "" : currentWorkspace?.id || "",
|
||||||
|
{
|
||||||
|
refetchInterval:
|
||||||
|
currentWorkspace?.upgradeStatus === ProjectUpgradeStatus.InProgress ? 3000 : false
|
||||||
|
}
|
||||||
|
);
|
||||||
|
const { membership } = useProjectPermission();
|
||||||
|
const migrateProjectToV3 = useMigrateProjectToV3();
|
||||||
|
|
||||||
|
const isProjectUpgraded = workspaceDetails?.version === ProjectVersion.V3;
|
||||||
|
|
||||||
|
if (isProjectUpgraded || currentWorkspace?.version === ProjectVersion.V3) return null;
|
||||||
|
const isUpgrading = workspaceDetails?.upgradeStatus === ProjectUpgradeStatus.InProgress;
|
||||||
|
|
||||||
|
const handleMigrationSecretV2 = async () => {
|
||||||
|
try {
|
||||||
|
await migrateProjectToV3.mutateAsync({ workspaceId: currentWorkspace?.id || "" });
|
||||||
|
createNotification({
|
||||||
|
text: "Migrated project to new KMS",
|
||||||
|
type: "success"
|
||||||
|
});
|
||||||
|
} catch {
|
||||||
|
createNotification({
|
||||||
|
text: "Failed to upgrade project",
|
||||||
|
type: "error"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
// for non admin this would throw an error
|
||||||
|
// so no need to render
|
||||||
|
return (
|
||||||
|
<div className="mt-4 rounded-lg border border-mineshaft-600 bg-mineshaft-900 p-4">
|
||||||
|
{isUpgrading && (
|
||||||
|
<div className="absolute top-0 left-0 z-50 flex h-screen w-screen items-center justify-center bg-bunker-500 bg-opacity-80">
|
||||||
|
<Spinner size="lg" className="text-primary" />
|
||||||
|
<div className="ml-4 flex flex-col space-y-1">
|
||||||
|
<div className="text-3xl font-medium">Please wait</div>
|
||||||
|
<span className="inline-block">Upgrading your project...</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
<p className="mb-2 text-lg font-semibold">Action Required</p>
|
||||||
|
<p className="mb-4 leading-7 text-gray-400">
|
||||||
|
There is a new update for your project. Introducing Infisical KMS.
|
||||||
|
<b>
|
||||||
|
{membership.role !== ProjectMembershipRole.Admin && "This is an admin only operation."}
|
||||||
|
</b>
|
||||||
|
</p>
|
||||||
|
<ProjectPermissionCan I={ProjectPermissionActions.Edit} a={ProjectPermissionSub.Settings}>
|
||||||
|
{(isAllowed) => (
|
||||||
|
<Button
|
||||||
|
onClick={handleMigrationSecretV2}
|
||||||
|
isDisabled={
|
||||||
|
!isAllowed || membership.role !== ProjectMembershipRole.Admin || isUpgrading
|
||||||
|
}
|
||||||
|
color="mineshaft"
|
||||||
|
type="submit"
|
||||||
|
isLoading={migrateProjectToV3.isLoading}
|
||||||
|
>
|
||||||
|
Start Migration
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
</ProjectPermissionCan>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
export { SecretV2MigrationSection } from "./SecretV2MigrationSection";
|
||||||
Reference in New Issue
Block a user