mirror of
https://github.com/awatertrevi/infisical.git
synced 2026-10-08 00:27:35 +00:00
Finished migration
This commit is contained in:
@@ -15,6 +15,7 @@ export async function up(knex: Knex): Promise<void> {
|
|||||||
if (!hasProjectVersionColumn) {
|
if (!hasProjectVersionColumn) {
|
||||||
await knex.schema.alterTable(TableName.Project, (t) => {
|
await knex.schema.alterTable(TableName.Project, (t) => {
|
||||||
t.string("version").defaultTo(ProjectVersion.V1).notNullable();
|
t.string("version").defaultTo(ProjectVersion.V1).notNullable();
|
||||||
|
t.text("upgradeStatus").nullable();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -32,6 +33,7 @@ export async function down(knex: Knex): Promise<void> {
|
|||||||
if (hasProjectVersionColumn) {
|
if (hasProjectVersionColumn) {
|
||||||
await knex.schema.alterTable(TableName.Project, (t) => {
|
await knex.schema.alterTable(TableName.Project, (t) => {
|
||||||
t.dropColumn("version");
|
t.dropColumn("version");
|
||||||
|
t.dropColumn("upgradeStatus");
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -117,6 +117,24 @@ export enum ProjectVersion {
|
|||||||
V2 = "v2"
|
V2 = "v2"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Case study:
|
||||||
|
/*
|
||||||
|
|
||||||
|
Q: If the upgrade status is null and the project version is V1:
|
||||||
|
A: It would mean that the project has not been attempted to be upgraded
|
||||||
|
|
||||||
|
Q: If the upgrade status is not null (FAILED or IN_PROGRESS), we a status
|
||||||
|
|
||||||
|
Q: if the project status is null, and the project version is v2, we should display nothing cuz its upgraded!
|
||||||
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
export enum ProjectUpgradeStatus {
|
||||||
|
InProgress = "IN_PROGRESS",
|
||||||
|
Completed = "Project upgrade completed.", // Will be null if completed. So a completed status is not needed
|
||||||
|
Failed = "FAILED"
|
||||||
|
}
|
||||||
|
|
||||||
export enum IdentityAuthMethod {
|
export enum IdentityAuthMethod {
|
||||||
Univeral = "universal-auth"
|
Univeral = "universal-auth"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -15,7 +15,8 @@ export const ProjectsSchema = z.object({
|
|||||||
orgId: z.string().uuid(),
|
orgId: z.string().uuid(),
|
||||||
createdAt: z.date(),
|
createdAt: z.date(),
|
||||||
updatedAt: z.date(),
|
updatedAt: z.date(),
|
||||||
version: z.string().default("v1")
|
version: z.string().default("v1"),
|
||||||
|
upgradeStatus: z.string().nullable().optional()
|
||||||
});
|
});
|
||||||
|
|
||||||
export type TProjects = z.infer<typeof ProjectsSchema>;
|
export type TProjects = z.infer<typeof ProjectsSchema>;
|
||||||
|
|||||||
@@ -338,7 +338,8 @@ export const samlConfigServiceFactory = ({
|
|||||||
email,
|
email,
|
||||||
firstName,
|
firstName,
|
||||||
lastName,
|
lastName,
|
||||||
authMethods: [AuthMethod.EMAIL]
|
authMethods: [AuthMethod.EMAIL],
|
||||||
|
ghost: false
|
||||||
},
|
},
|
||||||
tx
|
tx
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { Job, JobsOptions, Queue, QueueOptions, RepeatOptions, Worker, WorkerListener } from "bullmq";
|
import { Job, JobsOptions, Queue, QueueOptions, RepeatOptions, Worker, WorkerListener } from "bullmq";
|
||||||
import Redis from "ioredis";
|
import Redis from "ioredis";
|
||||||
|
|
||||||
|
import { SecretKeyEncoding } from "@app/db/schemas";
|
||||||
import { TCreateAuditLogDTO } from "@app/ee/services/audit-log/audit-log-types";
|
import { TCreateAuditLogDTO } from "@app/ee/services/audit-log/audit-log-types";
|
||||||
import {
|
import {
|
||||||
TScanFullRepoEventPayload,
|
TScanFullRepoEventPayload,
|
||||||
@@ -15,7 +16,8 @@ export enum QueueName {
|
|||||||
IntegrationSync = "sync-integrations",
|
IntegrationSync = "sync-integrations",
|
||||||
SecretWebhook = "secret-webhook",
|
SecretWebhook = "secret-webhook",
|
||||||
SecretFullRepoScan = "secret-full-repo-scan",
|
SecretFullRepoScan = "secret-full-repo-scan",
|
||||||
SecretPushEventScan = "secret-push-event-scan"
|
SecretPushEventScan = "secret-push-event-scan",
|
||||||
|
UpgradeProjectToGhost = "upgrade-project-to-ghost"
|
||||||
}
|
}
|
||||||
|
|
||||||
export enum QueueJobs {
|
export enum QueueJobs {
|
||||||
@@ -25,7 +27,8 @@ export enum QueueJobs {
|
|||||||
AuditLogPrune = "audit-log-prune-job",
|
AuditLogPrune = "audit-log-prune-job",
|
||||||
SecWebhook = "secret-webhook-trigger",
|
SecWebhook = "secret-webhook-trigger",
|
||||||
IntegrationSync = "secret-integration-pull",
|
IntegrationSync = "secret-integration-pull",
|
||||||
SecretScan = "secret-scan"
|
SecretScan = "secret-scan",
|
||||||
|
UpgradeProjectToGhost = "upgrade-project-to-ghost-job"
|
||||||
}
|
}
|
||||||
|
|
||||||
export type TQueueJobTypes = {
|
export type TQueueJobTypes = {
|
||||||
@@ -64,6 +67,20 @@ export type TQueueJobTypes = {
|
|||||||
payload: TScanFullRepoEventPayload;
|
payload: TScanFullRepoEventPayload;
|
||||||
};
|
};
|
||||||
[QueueName.SecretPushEventScan]: { name: QueueJobs.SecretScan; payload: TScanPushEventPayload };
|
[QueueName.SecretPushEventScan]: { name: QueueJobs.SecretScan; payload: TScanPushEventPayload };
|
||||||
|
|
||||||
|
[QueueName.UpgradeProjectToGhost]: {
|
||||||
|
name: QueueJobs.UpgradeProjectToGhost;
|
||||||
|
payload: {
|
||||||
|
projectId: string;
|
||||||
|
startedByUserId: string;
|
||||||
|
encryptedPrivateKey: {
|
||||||
|
encryptedKey: string;
|
||||||
|
encryptedKeyIv: string;
|
||||||
|
encryptedKeyTag: string;
|
||||||
|
keyEncoding: SecretKeyEncoding;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
export type TQueueServiceFactory = ReturnType<typeof queueServiceFactory>;
|
export type TQueueServiceFactory = ReturnType<typeof queueServiceFactory>;
|
||||||
|
|||||||
@@ -65,6 +65,7 @@ import { orgRoleDALFactory } from "@app/services/org/org-role-dal";
|
|||||||
import { orgRoleServiceFactory } from "@app/services/org/org-role-service";
|
import { orgRoleServiceFactory } from "@app/services/org/org-role-service";
|
||||||
import { orgServiceFactory } from "@app/services/org/org-service";
|
import { orgServiceFactory } from "@app/services/org/org-service";
|
||||||
import { projectDALFactory } from "@app/services/project/project-dal";
|
import { projectDALFactory } from "@app/services/project/project-dal";
|
||||||
|
import { projectQueueFactory } from "@app/services/project/project-queue";
|
||||||
import { projectServiceFactory } from "@app/services/project/project-service";
|
import { projectServiceFactory } from "@app/services/project/project-service";
|
||||||
import { projectBotDALFactory } from "@app/services/project-bot/project-bot-dal";
|
import { projectBotDALFactory } from "@app/services/project-bot/project-bot-dal";
|
||||||
import { projectBotServiceFactory } from "@app/services/project-bot/project-bot-service";
|
import { projectBotServiceFactory } from "@app/services/project-bot/project-bot-service";
|
||||||
@@ -299,19 +300,33 @@ export const registerRoutes = async (
|
|||||||
projectKeyDAL,
|
projectKeyDAL,
|
||||||
projectMembershipDAL
|
projectMembershipDAL
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const projectQueueService = projectQueueFactory({
|
||||||
|
queueService,
|
||||||
|
secretDAL,
|
||||||
|
folderDAL,
|
||||||
|
projectDAL,
|
||||||
|
orgDAL,
|
||||||
|
orgService,
|
||||||
|
projectEnvDAL,
|
||||||
|
userDAL,
|
||||||
|
secretVersionDAL,
|
||||||
|
projectKeyDAL,
|
||||||
|
projectBotDAL,
|
||||||
|
projectMembershipDAL,
|
||||||
|
secretApprovalRequestDAL,
|
||||||
|
secretApprovalSecretDAL: sarSecretDAL
|
||||||
|
});
|
||||||
|
|
||||||
const projectService = projectServiceFactory({
|
const projectService = projectServiceFactory({
|
||||||
permissionService,
|
permissionService,
|
||||||
projectDAL,
|
projectDAL,
|
||||||
|
projectQueue: projectQueueService,
|
||||||
secretBlindIndexDAL,
|
secretBlindIndexDAL,
|
||||||
identityProjectDAL,
|
identityProjectDAL,
|
||||||
identityOrgMembershipDAL,
|
identityOrgMembershipDAL,
|
||||||
projectBotDAL,
|
projectBotDAL,
|
||||||
secretDAL,
|
|
||||||
orgDAL,
|
|
||||||
secretApprovalRequestDAL,
|
|
||||||
secretApprovalSecretDAL: sarSecretDAL,
|
|
||||||
projectKeyDAL,
|
projectKeyDAL,
|
||||||
secretVersionDAL,
|
|
||||||
userDAL,
|
userDAL,
|
||||||
projectEnvDAL,
|
projectEnvDAL,
|
||||||
orgService,
|
orgService,
|
||||||
|
|||||||
@@ -74,7 +74,7 @@ export const registerProjectRouter = async (server: FastifyZodProvider) => {
|
|||||||
userPrivateKey: z.string().trim()
|
userPrivateKey: z.string().trim()
|
||||||
}),
|
}),
|
||||||
response: {
|
response: {
|
||||||
200: z.object({})
|
200: z.void()
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
onRequest: verifyAuth([AuthMode.JWT, AuthMode.API_KEY]),
|
onRequest: verifyAuth([AuthMode.JWT, AuthMode.API_KEY]),
|
||||||
@@ -88,6 +88,31 @@ export const registerProjectRouter = async (server: FastifyZodProvider) => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
server.route({
|
||||||
|
url: "/:projectId/upgrade/status",
|
||||||
|
method: "GET",
|
||||||
|
schema: {
|
||||||
|
params: z.object({
|
||||||
|
projectId: z.string().trim()
|
||||||
|
}),
|
||||||
|
response: {
|
||||||
|
200: z.object({
|
||||||
|
status: z.string().nullable()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onRequest: verifyAuth([AuthMode.JWT, AuthMode.API_KEY]),
|
||||||
|
handler: async (req) => {
|
||||||
|
const status = await server.services.project.getProjectUpgradeStatus({
|
||||||
|
projectId: req.params.projectId,
|
||||||
|
actor: req.permission.type,
|
||||||
|
actorId: req.permission.id
|
||||||
|
});
|
||||||
|
|
||||||
|
return { status };
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
/* Create new project */
|
/* Create new project */
|
||||||
server.route({
|
server.route({
|
||||||
method: "POST",
|
method: "POST",
|
||||||
|
|||||||
@@ -50,7 +50,7 @@ export const authSignupServiceFactory = ({
|
|||||||
throw new Error("Failed to send verification code for complete account");
|
throw new Error("Failed to send verification code for complete account");
|
||||||
}
|
}
|
||||||
if (!user) {
|
if (!user) {
|
||||||
user = await userDAL.create({ authMethods: [AuthMethod.EMAIL], email });
|
user = await userDAL.create({ authMethods: [AuthMethod.EMAIL], email, ghost: false });
|
||||||
}
|
}
|
||||||
if (!user) throw new Error("Failed to create user");
|
if (!user) throw new Error("Failed to create user");
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { TDbClient } from "@app/db";
|
import { TDbClient } from "@app/db";
|
||||||
import { ProjectsSchema, TableName } from "@app/db/schemas";
|
import { ProjectsSchema, ProjectUpgradeStatus, TableName, TProjectsUpdate } from "@app/db/schemas";
|
||||||
import { DatabaseError } from "@app/lib/errors";
|
import { DatabaseError } from "@app/lib/errors";
|
||||||
import { ormify, selectAllTableCols, sqlNestRelationships } from "@app/lib/knex";
|
import { ormify, selectAllTableCols, sqlNestRelationships } from "@app/lib/knex";
|
||||||
|
|
||||||
@@ -66,6 +66,18 @@ export const projectDALFactory = (db: TDbClient) => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const setProjectUpgradeStatus = async (projectId: string, status: ProjectUpgradeStatus) => {
|
||||||
|
try {
|
||||||
|
const data: TProjectsUpdate = {
|
||||||
|
upgradeStatus: status
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
await db(TableName.Project).where({ id: projectId }).update(data);
|
||||||
|
} catch (error) {
|
||||||
|
throw new DatabaseError({ error, name: "Set project upgrade status" });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const findAllProjectsByIdentity = async (identityId: string) => {
|
const findAllProjectsByIdentity = async (identityId: string) => {
|
||||||
try {
|
try {
|
||||||
const workspaces = await db(TableName.IdentityProjectMembership)
|
const workspaces = await db(TableName.IdentityProjectMembership)
|
||||||
@@ -149,6 +161,7 @@ export const projectDALFactory = (db: TDbClient) => {
|
|||||||
return {
|
return {
|
||||||
...projectOrm,
|
...projectOrm,
|
||||||
findAllProjects,
|
findAllProjects,
|
||||||
|
setProjectUpgradeStatus,
|
||||||
findAllProjectsByIdentity,
|
findAllProjectsByIdentity,
|
||||||
findProjectGhostUser,
|
findProjectGhostUser,
|
||||||
findProjectById
|
findProjectById
|
||||||
|
|||||||
@@ -0,0 +1,410 @@
|
|||||||
|
/* eslint-disable no-await-in-loop */
|
||||||
|
import {
|
||||||
|
ProjectMembershipRole,
|
||||||
|
ProjectUpgradeStatus,
|
||||||
|
ProjectVersion,
|
||||||
|
SecretKeyEncoding,
|
||||||
|
TSecrets
|
||||||
|
} from "@app/db/schemas";
|
||||||
|
import { TSecretApprovalRequestDALFactory } from "@app/ee/services/secret-approval-request/secret-approval-request-dal";
|
||||||
|
import { TSecretApprovalRequestSecretDALFactory } from "@app/ee/services/secret-approval-request/secret-approval-request-secret-dal";
|
||||||
|
import { RequestState } from "@app/ee/services/secret-approval-request/secret-approval-request-types";
|
||||||
|
import {
|
||||||
|
decryptAsymmetric,
|
||||||
|
encryptSymmetric128BitHexKeyUTF8,
|
||||||
|
infisicalSymmetricDecrypt,
|
||||||
|
infisicalSymmetricEncypt
|
||||||
|
} from "@app/lib/crypto/encryption";
|
||||||
|
import { BadRequestError } from "@app/lib/errors";
|
||||||
|
import { logger } from "@app/lib/logger";
|
||||||
|
import { createProjectKey, createWsMembers } from "@app/lib/project";
|
||||||
|
import { decryptSecrets, SecretDocType, TPartialSecret } from "@app/lib/secret";
|
||||||
|
import { QueueJobs, QueueName, TQueueJobTypes, TQueueServiceFactory } from "@app/queue";
|
||||||
|
|
||||||
|
import { TOrgDALFactory } from "../org/org-dal";
|
||||||
|
import { TOrgServiceFactory } from "../org/org-service";
|
||||||
|
import { TProjectBotDALFactory } from "../project-bot/project-bot-dal";
|
||||||
|
import { TProjectEnvDALFactory } from "../project-env/project-env-dal";
|
||||||
|
import { TProjectKeyDALFactory } from "../project-key/project-key-dal";
|
||||||
|
import { TProjectMembershipDALFactory } from "../project-membership/project-membership-dal";
|
||||||
|
import { TSecretDALFactory } from "../secret/secret-dal";
|
||||||
|
import { TSecretVersionDALFactory } from "../secret/secret-version-dal";
|
||||||
|
import { TSecretFolderDALFactory } from "../secret-folder/secret-folder-dal";
|
||||||
|
import { TUserDALFactory } from "../user/user-dal";
|
||||||
|
import { TProjectDALFactory } from "./project-dal";
|
||||||
|
|
||||||
|
export type TProjectQueueFactory = ReturnType<typeof projectQueueFactory>;
|
||||||
|
|
||||||
|
type TProjectQueueFactoryDep = {
|
||||||
|
queueService: TQueueServiceFactory;
|
||||||
|
secretVersionDAL: Pick<TSecretVersionDALFactory, "find" | "bulkUpdateNoVersionIncrement">;
|
||||||
|
folderDAL: Pick<TSecretFolderDALFactory, "find">;
|
||||||
|
secretDAL: Pick<TSecretDALFactory, "find" | "bulkUpdateNoVersionIncrement">;
|
||||||
|
projectKeyDAL: Pick<TProjectKeyDALFactory, "findLatestProjectKey" | "find" | "create" | "delete" | "insertMany">;
|
||||||
|
secretApprovalRequestDAL: Pick<TSecretApprovalRequestDALFactory, "find">;
|
||||||
|
secretApprovalSecretDAL: Pick<TSecretApprovalRequestSecretDALFactory, "find" | "bulkUpdateNoVersionIncrement">;
|
||||||
|
projectBotDAL: Pick<TProjectBotDALFactory, "findOne" | "delete" | "create">;
|
||||||
|
orgService: Pick<TOrgServiceFactory, "addGhostUser">;
|
||||||
|
projectMembershipDAL: Pick<TProjectMembershipDALFactory, "create">;
|
||||||
|
userDAL: Pick<TUserDALFactory, "findUserEncKeyByUserId">;
|
||||||
|
|
||||||
|
projectEnvDAL: Pick<TProjectEnvDALFactory, "find">;
|
||||||
|
projectDAL: Pick<TProjectDALFactory, "findOne" | "transaction" | "updateById" | "setProjectUpgradeStatus" | "find">;
|
||||||
|
orgDAL: Pick<TOrgDALFactory, "findMembership">;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const projectQueueFactory = ({
|
||||||
|
queueService,
|
||||||
|
secretDAL,
|
||||||
|
folderDAL,
|
||||||
|
userDAL,
|
||||||
|
secretVersionDAL,
|
||||||
|
secretApprovalRequestDAL,
|
||||||
|
secretApprovalSecretDAL,
|
||||||
|
projectKeyDAL,
|
||||||
|
projectBotDAL,
|
||||||
|
projectEnvDAL,
|
||||||
|
orgDAL,
|
||||||
|
projectDAL,
|
||||||
|
orgService,
|
||||||
|
projectMembershipDAL
|
||||||
|
}: TProjectQueueFactoryDep) => {
|
||||||
|
const upgradeProject = async (dto: TQueueJobTypes["upgrade-project-to-ghost"]["payload"]) => {
|
||||||
|
await queueService.queue(QueueName.UpgradeProjectToGhost, QueueJobs.UpgradeProjectToGhost, dto, {
|
||||||
|
attempts: 1,
|
||||||
|
removeOnComplete: true,
|
||||||
|
removeOnFail: {
|
||||||
|
count: 5 // keep the most recent jobs
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
queueService.start(QueueName.UpgradeProjectToGhost, async ({ data }) => {
|
||||||
|
try {
|
||||||
|
const [project] = await projectDAL.find({
|
||||||
|
id: data.projectId,
|
||||||
|
version: ProjectVersion.V1,
|
||||||
|
$in: {
|
||||||
|
upgradeStatus: [ProjectUpgradeStatus.Failed, null]
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const oldProjectKey = await projectKeyDAL.findLatestProjectKey(data.startedByUserId, data.projectId);
|
||||||
|
|
||||||
|
if (!project || !oldProjectKey) {
|
||||||
|
throw new BadRequestError({
|
||||||
|
message: "Project or project key not found"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
await projectDAL.setProjectUpgradeStatus(data.projectId, ProjectUpgradeStatus.InProgress);
|
||||||
|
const userPrivateKey = infisicalSymmetricDecrypt({
|
||||||
|
keyEncoding: data.encryptedPrivateKey.keyEncoding,
|
||||||
|
ciphertext: data.encryptedPrivateKey.encryptedKey,
|
||||||
|
iv: data.encryptedPrivateKey.encryptedKeyIv,
|
||||||
|
tag: data.encryptedPrivateKey.encryptedKeyTag
|
||||||
|
});
|
||||||
|
|
||||||
|
const projectEnvs = await projectEnvDAL.find({
|
||||||
|
projectId: project.id
|
||||||
|
});
|
||||||
|
|
||||||
|
const projectFolders = await folderDAL.find({
|
||||||
|
$in: {
|
||||||
|
envId: projectEnvs.map((env) => env.id)
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Get all the secrets within the project (as encrypted)
|
||||||
|
const secrets: TPartialSecret[] = [];
|
||||||
|
for (const folder of projectFolders) {
|
||||||
|
const folderSecrets = await secretDAL.find({ folderId: folder.id });
|
||||||
|
|
||||||
|
const folderSecretVersions = await secretVersionDAL.find(
|
||||||
|
{
|
||||||
|
folderId: folder.id
|
||||||
|
},
|
||||||
|
// Only get the latest 100 secret versions for each folder.
|
||||||
|
{
|
||||||
|
limit: 100
|
||||||
|
}
|
||||||
|
);
|
||||||
|
const approvalRequests = await secretApprovalRequestDAL.find({
|
||||||
|
status: RequestState.Open,
|
||||||
|
folderId: folder.id
|
||||||
|
});
|
||||||
|
const approvalSecrets = await secretApprovalSecretDAL.find({
|
||||||
|
$in: {
|
||||||
|
requestId: approvalRequests.map((el) => el.id)
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
secrets.push(...folderSecrets.map((el) => ({ ...el, docType: SecretDocType.Secret })));
|
||||||
|
secrets.push(...folderSecretVersions.map((el) => ({ ...el, docType: SecretDocType.SecretVersion })));
|
||||||
|
secrets.push(...approvalSecrets.map((el) => ({ ...el, docType: SecretDocType.ApprovalSecret })));
|
||||||
|
}
|
||||||
|
|
||||||
|
const decryptedSecrets = decryptSecrets(secrets, userPrivateKey, oldProjectKey);
|
||||||
|
|
||||||
|
if (secrets.length !== decryptedSecrets.length) {
|
||||||
|
throw new Error("Failed to decrypt some secret versions");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get the existing bot and the existing project keys for the members of the project
|
||||||
|
const existingBot = await projectBotDAL.findOne({ projectId: project.id }).catch(() => null);
|
||||||
|
const existingProjectKeys = await projectKeyDAL.find({ projectId: project.id });
|
||||||
|
|
||||||
|
// TRANSACTION START
|
||||||
|
await projectDAL.transaction(async (tx) => {
|
||||||
|
await projectDAL.updateById(project.id, { version: ProjectVersion.V2 }, tx);
|
||||||
|
|
||||||
|
// Create a ghost user
|
||||||
|
const ghostUser = await orgService.addGhostUser(project.orgId, tx);
|
||||||
|
|
||||||
|
// Create a project key
|
||||||
|
const { key: newEncryptedProjectKey, iv: newEncryptedProjectKeyIv } = createProjectKey({
|
||||||
|
publicKey: ghostUser.keys.publicKey,
|
||||||
|
privateKey: ghostUser.keys.plainPrivateKey
|
||||||
|
});
|
||||||
|
|
||||||
|
// Create a new project key for the GHOST
|
||||||
|
await projectKeyDAL.create(
|
||||||
|
{
|
||||||
|
projectId: project.id,
|
||||||
|
receiverId: ghostUser.user.id,
|
||||||
|
encryptedKey: newEncryptedProjectKey,
|
||||||
|
nonce: newEncryptedProjectKeyIv,
|
||||||
|
senderId: ghostUser.user.id
|
||||||
|
},
|
||||||
|
tx
|
||||||
|
);
|
||||||
|
|
||||||
|
// Create a membership for the ghost user
|
||||||
|
await projectMembershipDAL.create(
|
||||||
|
{
|
||||||
|
projectId: project.id,
|
||||||
|
userId: ghostUser.user.id,
|
||||||
|
role: ProjectMembershipRole.Admin
|
||||||
|
},
|
||||||
|
tx
|
||||||
|
);
|
||||||
|
|
||||||
|
// If a bot already exists, delete it
|
||||||
|
if (existingBot) {
|
||||||
|
await projectBotDAL.delete({ id: existingBot.id }, tx);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Delete all the existing project keys
|
||||||
|
await projectKeyDAL.delete(
|
||||||
|
{
|
||||||
|
projectId: project.id,
|
||||||
|
$in: {
|
||||||
|
id: existingProjectKeys.map((key) => key.id)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
tx
|
||||||
|
);
|
||||||
|
|
||||||
|
const ghostUserLatestKey = await projectKeyDAL.findLatestProjectKey(ghostUser.user.id, project.id, tx);
|
||||||
|
|
||||||
|
if (!ghostUserLatestKey) {
|
||||||
|
throw new Error("User latest key not found (V2 Upgrade)");
|
||||||
|
}
|
||||||
|
|
||||||
|
const newProjectMembers: {
|
||||||
|
encryptedKey: string;
|
||||||
|
nonce: string;
|
||||||
|
senderId: string;
|
||||||
|
receiverId: string;
|
||||||
|
projectId: string;
|
||||||
|
}[] = [];
|
||||||
|
|
||||||
|
for (const key of existingProjectKeys) {
|
||||||
|
const user = await userDAL.findUserEncKeyByUserId(key.receiverId);
|
||||||
|
const [orgMembership] = await orgDAL.findMembership({ userId: key.receiverId, orgId: project.orgId });
|
||||||
|
|
||||||
|
if (!user || !orgMembership) {
|
||||||
|
throw new Error(`User with ID ${key.receiverId} was not found during upgrade, or user is not in org.`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const [newMember] = createWsMembers({
|
||||||
|
decryptKey: ghostUserLatestKey,
|
||||||
|
userPrivateKey: ghostUser.keys.plainPrivateKey,
|
||||||
|
members: [
|
||||||
|
{
|
||||||
|
userPublicKey: user.publicKey,
|
||||||
|
orgMembershipId: orgMembership.id,
|
||||||
|
projectMembershipRole: ProjectMembershipRole.Admin
|
||||||
|
}
|
||||||
|
]
|
||||||
|
});
|
||||||
|
|
||||||
|
newProjectMembers.push({
|
||||||
|
encryptedKey: newMember.workspaceEncryptedKey,
|
||||||
|
nonce: newMember.workspaceEncryptedNonce,
|
||||||
|
senderId: ghostUser.user.id,
|
||||||
|
receiverId: user.id,
|
||||||
|
projectId: project.id
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create project keys for all the old members
|
||||||
|
await projectKeyDAL.insertMany(newProjectMembers, tx);
|
||||||
|
|
||||||
|
// Encrypt the bot private key (which is the same as the ghost user)
|
||||||
|
const { iv, tag, ciphertext, encoding, algorithm } = infisicalSymmetricEncypt(ghostUser.keys.plainPrivateKey);
|
||||||
|
|
||||||
|
// 5. Create a bot for the project
|
||||||
|
const newBot = await projectBotDAL.create(
|
||||||
|
{
|
||||||
|
name: "Infisical Bot (Ghost)",
|
||||||
|
projectId: project.id,
|
||||||
|
tag,
|
||||||
|
iv,
|
||||||
|
encryptedPrivateKey: ciphertext,
|
||||||
|
isActive: true,
|
||||||
|
publicKey: ghostUser.keys.publicKey,
|
||||||
|
senderId: ghostUser.user.id,
|
||||||
|
encryptedProjectKey: newEncryptedProjectKey,
|
||||||
|
encryptedProjectKeyNonce: newEncryptedProjectKeyIv,
|
||||||
|
algorithm,
|
||||||
|
keyEncoding: encoding
|
||||||
|
},
|
||||||
|
tx
|
||||||
|
);
|
||||||
|
|
||||||
|
const botPrivateKey = infisicalSymmetricDecrypt({
|
||||||
|
keyEncoding: newBot.keyEncoding as SecretKeyEncoding,
|
||||||
|
iv: newBot.iv,
|
||||||
|
tag: newBot.tag,
|
||||||
|
ciphertext: newBot.encryptedPrivateKey
|
||||||
|
});
|
||||||
|
|
||||||
|
const botKey = decryptAsymmetric({
|
||||||
|
ciphertext: newBot.encryptedProjectKey!,
|
||||||
|
privateKey: botPrivateKey,
|
||||||
|
nonce: newBot.encryptedProjectKeyNonce!,
|
||||||
|
publicKey: ghostUser.keys.publicKey
|
||||||
|
});
|
||||||
|
|
||||||
|
type TPartialSecret = Pick<
|
||||||
|
TSecrets,
|
||||||
|
| "id"
|
||||||
|
| "secretKeyCiphertext"
|
||||||
|
| "secretKeyIV"
|
||||||
|
| "secretKeyTag"
|
||||||
|
| "secretValueCiphertext"
|
||||||
|
| "secretValueIV"
|
||||||
|
| "secretValueTag"
|
||||||
|
| "secretCommentCiphertext"
|
||||||
|
| "secretCommentIV"
|
||||||
|
| "secretCommentTag"
|
||||||
|
>;
|
||||||
|
|
||||||
|
const updatedSecrets: TPartialSecret[] = [];
|
||||||
|
const updatedSecretVersions: TPartialSecret[] = [];
|
||||||
|
const updatedSecretApprovals: TPartialSecret[] = [];
|
||||||
|
for (const rawSecret of decryptedSecrets) {
|
||||||
|
const secretKeyEncrypted = encryptSymmetric128BitHexKeyUTF8(rawSecret.secretKey, botKey);
|
||||||
|
const secretValueEncrypted = encryptSymmetric128BitHexKeyUTF8(rawSecret.secretValue || "", botKey);
|
||||||
|
const secretCommentEncrypted = encryptSymmetric128BitHexKeyUTF8(rawSecret.secretComment || "", botKey);
|
||||||
|
|
||||||
|
const payload = {
|
||||||
|
id: rawSecret.id,
|
||||||
|
secretKeyCiphertext: secretKeyEncrypted.ciphertext,
|
||||||
|
secretKeyIV: secretKeyEncrypted.iv,
|
||||||
|
secretKeyTag: secretKeyEncrypted.tag,
|
||||||
|
secretValueCiphertext: secretValueEncrypted.ciphertext,
|
||||||
|
secretValueIV: secretValueEncrypted.iv,
|
||||||
|
secretValueTag: secretValueEncrypted.tag,
|
||||||
|
secretCommentCiphertext: secretCommentEncrypted.ciphertext,
|
||||||
|
secretCommentIV: secretCommentEncrypted.iv,
|
||||||
|
secretCommentTag: secretCommentEncrypted.tag
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
if (rawSecret.docType === SecretDocType.Secret) {
|
||||||
|
updatedSecrets.push(payload);
|
||||||
|
} else if (rawSecret.docType === SecretDocType.SecretVersion) {
|
||||||
|
updatedSecretVersions.push(payload);
|
||||||
|
} else if (rawSecret.docType === SecretDocType.ApprovalSecret) {
|
||||||
|
updatedSecretApprovals.push(payload);
|
||||||
|
} else {
|
||||||
|
throw new Error("Unknown secret type");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const secretUpdates = await secretDAL.bulkUpdateNoVersionIncrement(
|
||||||
|
[
|
||||||
|
...updatedSecrets.map((secret) => ({
|
||||||
|
filter: { id: secret.id },
|
||||||
|
data: {
|
||||||
|
...secret,
|
||||||
|
id: undefined
|
||||||
|
}
|
||||||
|
}))
|
||||||
|
],
|
||||||
|
tx
|
||||||
|
);
|
||||||
|
|
||||||
|
const secretVersionUpdates = await secretVersionDAL.bulkUpdateNoVersionIncrement(
|
||||||
|
[
|
||||||
|
...updatedSecretVersions.map((version) => ({
|
||||||
|
filter: { id: version.id },
|
||||||
|
data: {
|
||||||
|
...version,
|
||||||
|
id: undefined
|
||||||
|
}
|
||||||
|
}))
|
||||||
|
],
|
||||||
|
tx
|
||||||
|
);
|
||||||
|
|
||||||
|
const secretApprovalUpdates = await secretApprovalSecretDAL.bulkUpdateNoVersionIncrement(
|
||||||
|
[
|
||||||
|
...updatedSecretApprovals.map((approval) => ({
|
||||||
|
filter: {
|
||||||
|
id: approval.id
|
||||||
|
},
|
||||||
|
data: {
|
||||||
|
...approval,
|
||||||
|
id: undefined
|
||||||
|
}
|
||||||
|
}))
|
||||||
|
],
|
||||||
|
tx
|
||||||
|
);
|
||||||
|
|
||||||
|
if (secretUpdates.length !== updatedSecrets.length) {
|
||||||
|
throw new Error("Failed to update some secrets");
|
||||||
|
}
|
||||||
|
if (secretVersionUpdates.length !== updatedSecretVersions.length) {
|
||||||
|
throw new Error("Failed to update some secret versions");
|
||||||
|
}
|
||||||
|
if (secretApprovalUpdates.length !== updatedSecretApprovals.length) {
|
||||||
|
throw new Error("Failed to update some secret approvals");
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new Error("Transaction was successful!");
|
||||||
|
});
|
||||||
|
} catch (err) {
|
||||||
|
const project = await projectDAL.findOne({ id: data.projectId, version: ProjectVersion.V1 }).catch(() => null);
|
||||||
|
|
||||||
|
if (!project) {
|
||||||
|
logger.error("Failed to upgrade project, because no project was found", data);
|
||||||
|
} else {
|
||||||
|
await projectDAL.setProjectUpgradeStatus(data.projectId, ProjectUpgradeStatus.Failed);
|
||||||
|
logger.error("Failed to upgrade project", data, err);
|
||||||
|
}
|
||||||
|
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
queueService.listen(QueueName.UpgradeProjectToGhost, "failed", (job, err) => {
|
||||||
|
logger.error("Upgrade project failed", job?.data, err);
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
upgradeProject
|
||||||
|
};
|
||||||
|
};
|
||||||
@@ -1,45 +1,36 @@
|
|||||||
|
/* eslint-disable @typescript-eslint/no-redeclare */
|
||||||
/* eslint-disable no-console */
|
/* eslint-disable no-console */
|
||||||
/* eslint-disable no-await-in-loop */
|
/* eslint-disable no-await-in-loop */
|
||||||
import { ForbiddenError } from "@casl/ability";
|
import { ForbiddenError } from "@casl/ability";
|
||||||
import slugify from "@sindresorhus/slugify";
|
import slugify from "@sindresorhus/slugify";
|
||||||
|
|
||||||
import { ProjectMembershipRole, ProjectVersion, SecretKeyEncoding, TSecrets } from "@app/db/schemas";
|
import { ProjectMembershipRole, ProjectVersion } 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";
|
||||||
import { ProjectPermissionActions, ProjectPermissionSub } from "@app/ee/services/permission/project-permission";
|
import { ProjectPermissionActions, ProjectPermissionSub } from "@app/ee/services/permission/project-permission";
|
||||||
import { TSecretApprovalRequestDALFactory } from "@app/ee/services/secret-approval-request/secret-approval-request-dal";
|
|
||||||
import { TSecretApprovalRequestSecretDALFactory } from "@app/ee/services/secret-approval-request/secret-approval-request-secret-dal";
|
|
||||||
import { RequestState } from "@app/ee/services/secret-approval-request/secret-approval-request-types";
|
|
||||||
import { isAtLeastAsPrivileged } from "@app/lib/casl";
|
import { isAtLeastAsPrivileged } from "@app/lib/casl";
|
||||||
import { getConfig } from "@app/lib/config/env";
|
import { getConfig } from "@app/lib/config/env";
|
||||||
import { createSecretBlindIndex } from "@app/lib/crypto";
|
import { createSecretBlindIndex } from "@app/lib/crypto";
|
||||||
import {
|
import { infisicalSymmetricEncypt } from "@app/lib/crypto/encryption";
|
||||||
decryptAsymmetric,
|
|
||||||
encryptSymmetric128BitHexKeyUTF8,
|
|
||||||
infisicalSymmetricDecrypt,
|
|
||||||
infisicalSymmetricEncypt
|
|
||||||
} from "@app/lib/crypto/encryption";
|
|
||||||
import { BadRequestError, ForbiddenRequestError } from "@app/lib/errors";
|
import { BadRequestError, ForbiddenRequestError } from "@app/lib/errors";
|
||||||
import { alphaNumericNanoId } from "@app/lib/nanoid";
|
import { alphaNumericNanoId } from "@app/lib/nanoid";
|
||||||
import { createProjectKey, createWsMembers } from "@app/lib/project";
|
import { createProjectKey, createWsMembers } from "@app/lib/project";
|
||||||
import { decryptSecrets, SecretDocType, TPartialSecret } from "@app/lib/secret";
|
import { TProjectPermission } from "@app/lib/types";
|
||||||
|
|
||||||
import { ActorType } from "../auth/auth-type";
|
import { ActorType } from "../auth/auth-type";
|
||||||
import { TIdentityOrgDALFactory } from "../identity/identity-org-dal";
|
import { TIdentityOrgDALFactory } from "../identity/identity-org-dal";
|
||||||
import { TIdentityProjectDALFactory } from "../identity-project/identity-project-dal";
|
import { TIdentityProjectDALFactory } from "../identity-project/identity-project-dal";
|
||||||
import { TOrgDALFactory } from "../org/org-dal";
|
|
||||||
import { TOrgServiceFactory } from "../org/org-service";
|
import { TOrgServiceFactory } from "../org/org-service";
|
||||||
import { TProjectBotDALFactory } from "../project-bot/project-bot-dal";
|
import { TProjectBotDALFactory } from "../project-bot/project-bot-dal";
|
||||||
import { TProjectEnvDALFactory } from "../project-env/project-env-dal";
|
import { TProjectEnvDALFactory } from "../project-env/project-env-dal";
|
||||||
import { TProjectKeyDALFactory } from "../project-key/project-key-dal";
|
import { TProjectKeyDALFactory } from "../project-key/project-key-dal";
|
||||||
import { TProjectMembershipDALFactory } from "../project-membership/project-membership-dal";
|
import { TProjectMembershipDALFactory } from "../project-membership/project-membership-dal";
|
||||||
import { TSecretDALFactory } from "../secret/secret-dal";
|
|
||||||
import { TSecretVersionDALFactory } from "../secret/secret-version-dal";
|
|
||||||
import { TSecretBlindIndexDALFactory } from "../secret-blind-index/secret-blind-index-dal";
|
import { TSecretBlindIndexDALFactory } from "../secret-blind-index/secret-blind-index-dal";
|
||||||
import { ROOT_FOLDER_NAME, TSecretFolderDALFactory } from "../secret-folder/secret-folder-dal";
|
import { ROOT_FOLDER_NAME, TSecretFolderDALFactory } from "../secret-folder/secret-folder-dal";
|
||||||
import { TUserDALFactory } from "../user/user-dal";
|
import { TUserDALFactory } from "../user/user-dal";
|
||||||
import { TProjectDALFactory } from "./project-dal";
|
import { TProjectDALFactory } from "./project-dal";
|
||||||
|
import { TProjectQueueFactory } from "./project-queue";
|
||||||
import { TCreateProjectDTO, TDeleteProjectDTO, TGetProjectDTO, TUpgradeProjectDTO } from "./project-types";
|
import { TCreateProjectDTO, TDeleteProjectDTO, TGetProjectDTO, TUpgradeProjectDTO } from "./project-types";
|
||||||
|
|
||||||
export const DEFAULT_PROJECT_ENVS = [
|
export const DEFAULT_PROJECT_ENVS = [
|
||||||
@@ -50,22 +41,18 @@ export const DEFAULT_PROJECT_ENVS = [
|
|||||||
|
|
||||||
type TProjectServiceFactoryDep = {
|
type TProjectServiceFactoryDep = {
|
||||||
projectDAL: TProjectDALFactory;
|
projectDAL: TProjectDALFactory;
|
||||||
|
projectQueue: TProjectQueueFactory;
|
||||||
userDAL: TUserDALFactory;
|
userDAL: TUserDALFactory;
|
||||||
folderDAL: TSecretFolderDALFactory;
|
folderDAL: TSecretFolderDALFactory;
|
||||||
projectEnvDAL: Pick<TProjectEnvDALFactory, "insertMany" | "find">;
|
projectEnvDAL: Pick<TProjectEnvDALFactory, "insertMany" | "find">;
|
||||||
secretVersionDAL: TSecretVersionDALFactory;
|
|
||||||
identityOrgMembershipDAL: TIdentityOrgDALFactory;
|
identityOrgMembershipDAL: TIdentityOrgDALFactory;
|
||||||
identityProjectDAL: TIdentityProjectDALFactory;
|
identityProjectDAL: TIdentityProjectDALFactory;
|
||||||
projectKeyDAL: Pick<TProjectKeyDALFactory, "create" | "findLatestProjectKey" | "delete" | "find" | "insertMany">;
|
projectKeyDAL: Pick<TProjectKeyDALFactory, "create" | "findLatestProjectKey" | "delete" | "find" | "insertMany">;
|
||||||
projectBotDAL: Pick<TProjectBotDALFactory, "create" | "findById" | "delete" | "findOne">;
|
projectBotDAL: Pick<TProjectBotDALFactory, "create" | "findById" | "delete" | "findOne">;
|
||||||
projectMembershipDAL: Pick<TProjectMembershipDALFactory, "create" | "findProjectGhostUser" | "findOne">;
|
projectMembershipDAL: Pick<TProjectMembershipDALFactory, "create" | "findProjectGhostUser" | "findOne">;
|
||||||
orgDAL: TOrgDALFactory;
|
|
||||||
secretApprovalRequestDAL: TSecretApprovalRequestDALFactory;
|
|
||||||
secretApprovalSecretDAL: TSecretApprovalRequestSecretDALFactory;
|
|
||||||
secretBlindIndexDAL: Pick<TSecretBlindIndexDALFactory, "create">;
|
secretBlindIndexDAL: Pick<TSecretBlindIndexDALFactory, "create">;
|
||||||
permissionService: TPermissionServiceFactory;
|
permissionService: TPermissionServiceFactory;
|
||||||
orgService: Pick<TOrgServiceFactory, "addGhostUser">;
|
orgService: Pick<TOrgServiceFactory, "addGhostUser">;
|
||||||
secretDAL: TSecretDALFactory;
|
|
||||||
licenseService: Pick<TLicenseServiceFactory, "getPlan">;
|
licenseService: Pick<TLicenseServiceFactory, "getPlan">;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -73,19 +60,15 @@ export type TProjectServiceFactory = ReturnType<typeof projectServiceFactory>;
|
|||||||
|
|
||||||
export const projectServiceFactory = ({
|
export const projectServiceFactory = ({
|
||||||
projectDAL,
|
projectDAL,
|
||||||
|
projectQueue,
|
||||||
projectKeyDAL,
|
projectKeyDAL,
|
||||||
secretApprovalRequestDAL,
|
|
||||||
secretApprovalSecretDAL,
|
|
||||||
permissionService,
|
permissionService,
|
||||||
userDAL,
|
userDAL,
|
||||||
folderDAL,
|
folderDAL,
|
||||||
orgService,
|
orgService,
|
||||||
orgDAL,
|
|
||||||
identityProjectDAL,
|
identityProjectDAL,
|
||||||
secretVersionDAL,
|
|
||||||
projectBotDAL,
|
projectBotDAL,
|
||||||
identityOrgMembershipDAL,
|
identityOrgMembershipDAL,
|
||||||
secretDAL,
|
|
||||||
secretBlindIndexDAL,
|
secretBlindIndexDAL,
|
||||||
projectMembershipDAL,
|
projectMembershipDAL,
|
||||||
projectEnvDAL,
|
projectEnvDAL,
|
||||||
@@ -396,303 +379,33 @@ export const projectServiceFactory = ({
|
|||||||
25. API route returns 200 OK.
|
25. API route returns 200 OK.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
const project = await projectDAL.findOne({ id: projectId, version: ProjectVersion.V1 });
|
const encryptedPrivateKey = infisicalSymmetricEncypt(userPrivateKey);
|
||||||
const oldProjectKey = await projectKeyDAL.findLatestProjectKey(actorId, projectId);
|
|
||||||
if (!project || !oldProjectKey) {
|
await projectQueue.upgradeProject({
|
||||||
|
projectId,
|
||||||
|
startedByUserId: actorId,
|
||||||
|
encryptedPrivateKey: {
|
||||||
|
encryptedKey: encryptedPrivateKey.ciphertext,
|
||||||
|
encryptedKeyIv: encryptedPrivateKey.iv,
|
||||||
|
encryptedKeyTag: encryptedPrivateKey.tag,
|
||||||
|
keyEncoding: encryptedPrivateKey.encoding
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const getProjectUpgradeStatus = async ({ projectId, actor, actorId }: TProjectPermission) => {
|
||||||
|
const { permission } = await permissionService.getProjectPermission(actor, actorId, projectId);
|
||||||
|
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.Secrets);
|
||||||
|
|
||||||
|
const project = await projectDAL.findProjectById(projectId);
|
||||||
|
|
||||||
|
if (!project) {
|
||||||
throw new BadRequestError({
|
throw new BadRequestError({
|
||||||
message: "Project or project key not found"
|
message: `Project with id ${projectId} not found`
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
const projectEnvs = await projectEnvDAL.find({
|
return project.upgradeStatus || null;
|
||||||
projectId: project.id
|
|
||||||
});
|
|
||||||
|
|
||||||
console.log(
|
|
||||||
"projectEnvs",
|
|
||||||
projectEnvs.map((e) => e.name)
|
|
||||||
);
|
|
||||||
|
|
||||||
const projectFolders = await folderDAL.find({
|
|
||||||
$in: {
|
|
||||||
envId: projectEnvs.map((env) => env.id)
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Get all the secrets within the project (as encrypted)
|
|
||||||
const secrets: TPartialSecret[] = [];
|
|
||||||
for (const folder of projectFolders) {
|
|
||||||
const folderSecrets = await secretDAL.find({ folderId: folder.id });
|
|
||||||
|
|
||||||
const folderSecretVersions = await secretVersionDAL.find({
|
|
||||||
folderId: folder.id
|
|
||||||
});
|
|
||||||
const approvalRequests = await secretApprovalRequestDAL.find({
|
|
||||||
status: RequestState.Open,
|
|
||||||
folderId: folder.id
|
|
||||||
});
|
|
||||||
const approvalSecrets = await secretApprovalSecretDAL.find({
|
|
||||||
$in: {
|
|
||||||
requestId: approvalRequests.map((el) => el.id)
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
secrets.push(...folderSecrets.map((el) => ({ ...el, docType: SecretDocType.Secret })));
|
|
||||||
secrets.push(...folderSecretVersions.map((el) => ({ ...el, docType: SecretDocType.SecretVersion })));
|
|
||||||
secrets.push(...approvalSecrets.map((el) => ({ ...el, docType: SecretDocType.ApprovalSecret })));
|
|
||||||
}
|
|
||||||
|
|
||||||
const decryptedSecrets = decryptSecrets(secrets, userPrivateKey, oldProjectKey);
|
|
||||||
|
|
||||||
if (secrets.length !== decryptedSecrets.length) {
|
|
||||||
throw new Error("Failed to decrypt some secret versions");
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get the existing bot and the existing project keys for the members of the project
|
|
||||||
const existingBot = await projectBotDAL.findOne({ projectId: project.id }).catch(() => null);
|
|
||||||
const existingProjectKeys = await projectKeyDAL.find({ projectId: project.id });
|
|
||||||
|
|
||||||
// TRANSACTION START
|
|
||||||
await projectDAL.transaction(async (tx) => {
|
|
||||||
await projectDAL.updateById(project.id, { version: ProjectVersion.V2 }, tx);
|
|
||||||
|
|
||||||
// Create a ghost user
|
|
||||||
const ghostUser = await orgService.addGhostUser(project.orgId, tx);
|
|
||||||
|
|
||||||
// Create a project key
|
|
||||||
const { key: newEncryptedProjectKey, iv: newEncryptedProjectKeyIv } = createProjectKey({
|
|
||||||
publicKey: ghostUser.keys.publicKey,
|
|
||||||
privateKey: ghostUser.keys.plainPrivateKey
|
|
||||||
});
|
|
||||||
|
|
||||||
console.log("Creating new project key for ghost user");
|
|
||||||
// Create a new project key for the GHOST
|
|
||||||
await projectKeyDAL.create(
|
|
||||||
{
|
|
||||||
projectId: project.id,
|
|
||||||
receiverId: ghostUser.user.id,
|
|
||||||
encryptedKey: newEncryptedProjectKey,
|
|
||||||
nonce: newEncryptedProjectKeyIv,
|
|
||||||
senderId: ghostUser.user.id
|
|
||||||
},
|
|
||||||
tx
|
|
||||||
);
|
|
||||||
|
|
||||||
// Create a membership for the ghost user
|
|
||||||
await projectMembershipDAL.create(
|
|
||||||
{
|
|
||||||
projectId: project.id,
|
|
||||||
userId: ghostUser.user.id,
|
|
||||||
role: ProjectMembershipRole.Admin
|
|
||||||
},
|
|
||||||
tx
|
|
||||||
);
|
|
||||||
|
|
||||||
// If a bot already exists, delete it
|
|
||||||
if (existingBot) {
|
|
||||||
console.log("Deleting existing bot");
|
|
||||||
await projectBotDAL.delete({ id: existingBot.id }, tx);
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log("Deleting old project keys");
|
|
||||||
// Delete all the existing project keys
|
|
||||||
await projectKeyDAL.delete(
|
|
||||||
{
|
|
||||||
projectId: project.id,
|
|
||||||
$in: {
|
|
||||||
id: existingProjectKeys.map((key) => key.id)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
tx
|
|
||||||
);
|
|
||||||
|
|
||||||
console.log("Finding latest key for ghost user");
|
|
||||||
const ghostUserLatestKey = await projectKeyDAL.findLatestProjectKey(ghostUser.user.id, project.id, tx);
|
|
||||||
|
|
||||||
if (!ghostUserLatestKey) {
|
|
||||||
throw new Error("User latest key not found (V2 Upgrade)");
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log("Creating new project keys for old members");
|
|
||||||
|
|
||||||
const newProjectMembers: {
|
|
||||||
encryptedKey: string;
|
|
||||||
nonce: string;
|
|
||||||
senderId: string;
|
|
||||||
receiverId: string;
|
|
||||||
projectId: string;
|
|
||||||
}[] = [];
|
|
||||||
|
|
||||||
for (const key of existingProjectKeys) {
|
|
||||||
const user = await userDAL.findUserEncKeyByUserId(key.receiverId);
|
|
||||||
const [orgMembership] = await orgDAL.findMembership({ userId: key.receiverId, orgId: project.orgId });
|
|
||||||
|
|
||||||
if (!user || !orgMembership) {
|
|
||||||
throw new Error(`User with ID ${key.receiverId} was not found during upgrade, or user is not in org.`);
|
|
||||||
}
|
|
||||||
|
|
||||||
const [newMember] = createWsMembers({
|
|
||||||
decryptKey: ghostUserLatestKey,
|
|
||||||
userPrivateKey: ghostUser.keys.plainPrivateKey,
|
|
||||||
members: [
|
|
||||||
{
|
|
||||||
userPublicKey: user.publicKey,
|
|
||||||
orgMembershipId: orgMembership.id,
|
|
||||||
projectMembershipRole: ProjectMembershipRole.Admin
|
|
||||||
}
|
|
||||||
]
|
|
||||||
});
|
|
||||||
|
|
||||||
newProjectMembers.push({
|
|
||||||
encryptedKey: newMember.workspaceEncryptedKey,
|
|
||||||
nonce: newMember.workspaceEncryptedNonce,
|
|
||||||
senderId: ghostUser.user.id,
|
|
||||||
receiverId: user.id,
|
|
||||||
projectId: project.id
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// Create project keys for all the old members
|
|
||||||
await projectKeyDAL.insertMany(newProjectMembers, tx);
|
|
||||||
|
|
||||||
// Encrypt the bot private key (which is the same as the ghost user)
|
|
||||||
const { iv, tag, ciphertext, encoding, algorithm } = infisicalSymmetricEncypt(ghostUser.keys.plainPrivateKey);
|
|
||||||
|
|
||||||
// 5. Create a bot for the project
|
|
||||||
const newBot = await projectBotDAL.create(
|
|
||||||
{
|
|
||||||
name: "Infisical Bot (Ghost)",
|
|
||||||
projectId: project.id,
|
|
||||||
tag,
|
|
||||||
iv,
|
|
||||||
encryptedPrivateKey: ciphertext,
|
|
||||||
isActive: true,
|
|
||||||
publicKey: ghostUser.keys.publicKey,
|
|
||||||
senderId: ghostUser.user.id,
|
|
||||||
encryptedProjectKey: newEncryptedProjectKey,
|
|
||||||
encryptedProjectKeyNonce: newEncryptedProjectKeyIv,
|
|
||||||
algorithm,
|
|
||||||
keyEncoding: encoding
|
|
||||||
},
|
|
||||||
tx
|
|
||||||
);
|
|
||||||
|
|
||||||
console.log("Updating secrets with new project key");
|
|
||||||
console.log("Got decrypted secrets");
|
|
||||||
|
|
||||||
const botPrivateKey = infisicalSymmetricDecrypt({
|
|
||||||
keyEncoding: newBot.keyEncoding as SecretKeyEncoding,
|
|
||||||
iv: newBot.iv,
|
|
||||||
tag: newBot.tag,
|
|
||||||
ciphertext: newBot.encryptedPrivateKey
|
|
||||||
});
|
|
||||||
|
|
||||||
const botKey = decryptAsymmetric({
|
|
||||||
ciphertext: newBot.encryptedProjectKey!,
|
|
||||||
privateKey: botPrivateKey,
|
|
||||||
nonce: newBot.encryptedProjectKeyNonce!,
|
|
||||||
publicKey: ghostUser.keys.publicKey
|
|
||||||
});
|
|
||||||
|
|
||||||
type TPartialSecret = Pick<
|
|
||||||
TSecrets,
|
|
||||||
| "id"
|
|
||||||
| "secretKeyCiphertext"
|
|
||||||
| "secretKeyIV"
|
|
||||||
| "secretKeyTag"
|
|
||||||
| "secretValueCiphertext"
|
|
||||||
| "secretValueIV"
|
|
||||||
| "secretValueTag"
|
|
||||||
| "secretCommentCiphertext"
|
|
||||||
| "secretCommentIV"
|
|
||||||
| "secretCommentTag"
|
|
||||||
>;
|
|
||||||
|
|
||||||
const updatedSecrets: TPartialSecret[] = [];
|
|
||||||
const updatedSecretVersions: TPartialSecret[] = [];
|
|
||||||
const updatedSecretApprovals: TPartialSecret[] = [];
|
|
||||||
for (const rawSecret of decryptedSecrets) {
|
|
||||||
const secretKeyEncrypted = encryptSymmetric128BitHexKeyUTF8(rawSecret.secretKey, botKey);
|
|
||||||
const secretValueEncrypted = encryptSymmetric128BitHexKeyUTF8(rawSecret.secretValue || "", botKey);
|
|
||||||
const secretCommentEncrypted = encryptSymmetric128BitHexKeyUTF8(rawSecret.secretComment || "", botKey);
|
|
||||||
|
|
||||||
const payload = {
|
|
||||||
id: rawSecret.id,
|
|
||||||
secretKeyCiphertext: secretKeyEncrypted.ciphertext,
|
|
||||||
secretKeyIV: secretKeyEncrypted.iv,
|
|
||||||
secretKeyTag: secretKeyEncrypted.tag,
|
|
||||||
secretValueCiphertext: secretValueEncrypted.ciphertext,
|
|
||||||
secretValueIV: secretValueEncrypted.iv,
|
|
||||||
secretValueTag: secretValueEncrypted.tag,
|
|
||||||
secretCommentCiphertext: secretCommentEncrypted.ciphertext,
|
|
||||||
secretCommentIV: secretCommentEncrypted.iv,
|
|
||||||
secretCommentTag: secretCommentEncrypted.tag
|
|
||||||
} as const;
|
|
||||||
|
|
||||||
if (rawSecret.docType === SecretDocType.Secret) {
|
|
||||||
updatedSecrets.push(payload);
|
|
||||||
} else if (rawSecret.docType === SecretDocType.SecretVersion) {
|
|
||||||
updatedSecretVersions.push(payload);
|
|
||||||
} else if (rawSecret.docType === SecretDocType.ApprovalSecret) {
|
|
||||||
updatedSecretApprovals.push(payload);
|
|
||||||
} else {
|
|
||||||
throw new Error("Unknown secret type");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const secretUpdates = await secretDAL.bulkUpdateNoVersionIncrement(
|
|
||||||
[
|
|
||||||
...updatedSecrets.map((secret) => ({
|
|
||||||
filter: { id: secret.id },
|
|
||||||
data: {
|
|
||||||
...secret,
|
|
||||||
id: undefined
|
|
||||||
}
|
|
||||||
}))
|
|
||||||
],
|
|
||||||
tx
|
|
||||||
);
|
|
||||||
|
|
||||||
const secretVersionUpdates = await secretVersionDAL.bulkUpdateNoVersionIncrement(
|
|
||||||
[
|
|
||||||
...updatedSecretVersions.map((version) => ({
|
|
||||||
filter: { id: version.id },
|
|
||||||
data: {
|
|
||||||
...version,
|
|
||||||
id: undefined
|
|
||||||
}
|
|
||||||
}))
|
|
||||||
],
|
|
||||||
tx
|
|
||||||
);
|
|
||||||
|
|
||||||
const secretApprovalUpdates = await secretApprovalSecretDAL.bulkUpdateNoVersionIncrement(
|
|
||||||
[
|
|
||||||
...updatedSecretApprovals.map((approval) => ({
|
|
||||||
filter: {
|
|
||||||
id: approval.id
|
|
||||||
},
|
|
||||||
data: {
|
|
||||||
...approval,
|
|
||||||
id: undefined
|
|
||||||
}
|
|
||||||
}))
|
|
||||||
],
|
|
||||||
tx
|
|
||||||
);
|
|
||||||
|
|
||||||
if (secretUpdates.length !== updatedSecrets.length) {
|
|
||||||
throw new Error("Failed to update some secrets");
|
|
||||||
}
|
|
||||||
if (secretVersionUpdates.length !== updatedSecretVersions.length) {
|
|
||||||
throw new Error("Failed to update some secret versions");
|
|
||||||
}
|
|
||||||
if (secretApprovalUpdates.length !== updatedSecretApprovals.length) {
|
|
||||||
throw new Error("Failed to update some secret approvals");
|
|
||||||
}
|
|
||||||
|
|
||||||
throw new Error("Transaction was successful");
|
|
||||||
});
|
|
||||||
};
|
};
|
||||||
|
|
||||||
return {
|
return {
|
||||||
@@ -700,6 +413,7 @@ export const projectServiceFactory = ({
|
|||||||
deleteProject,
|
deleteProject,
|
||||||
getProjects,
|
getProjects,
|
||||||
findProjectGhostUser,
|
findProjectGhostUser,
|
||||||
|
getProjectUpgradeStatus,
|
||||||
getAProject,
|
getAProject,
|
||||||
toggleAutoCapitalization,
|
toggleAutoCapitalization,
|
||||||
updateName,
|
updateName,
|
||||||
|
|||||||
@@ -70,6 +70,7 @@ export const superAdminServiceFactory = ({
|
|||||||
lastName,
|
lastName,
|
||||||
email,
|
email,
|
||||||
superAdmin: true,
|
superAdmin: true,
|
||||||
|
ghost: false,
|
||||||
isAccepted: true,
|
isAccepted: true,
|
||||||
authMethods: [AuthMethod.EMAIL]
|
authMethods: [AuthMethod.EMAIL]
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -0,0 +1,90 @@
|
|||||||
|
import { useCallback, useEffect, useState } from "react";
|
||||||
|
|
||||||
|
import { useNotificationContext } from "@app/components/context/Notifications/NotificationProvider";
|
||||||
|
import { useGetUpgradeProjectStatus, useUpgradeProject } from "@app/hooks/api";
|
||||||
|
import { Workspace } from "@app/hooks/api/types";
|
||||||
|
|
||||||
|
import { Alert } from "../Alert";
|
||||||
|
import { Button } from "../Button";
|
||||||
|
|
||||||
|
export type UpgradeProjectAlertProps = {
|
||||||
|
project: Workspace;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const UpgradeProjectAlert = ({ project }: UpgradeProjectAlertProps): JSX.Element | null => {
|
||||||
|
const { createNotification } = useNotificationContext();
|
||||||
|
const upgradeProject = useUpgradeProject();
|
||||||
|
const [currentStatus, setCurrentStatus] = useState<string | null>(null);
|
||||||
|
const {
|
||||||
|
data: projectStatus,
|
||||||
|
refetch: getLatestProjectStatus,
|
||||||
|
isLoading: statusIsLoading
|
||||||
|
} = useGetUpgradeProjectStatus(project.id);
|
||||||
|
|
||||||
|
const onUpgradeProject = useCallback(async () => {
|
||||||
|
const PRIVATE_KEY = localStorage.getItem("PRIVATE_KEY");
|
||||||
|
|
||||||
|
if (!PRIVATE_KEY) {
|
||||||
|
createNotification({
|
||||||
|
type: "error",
|
||||||
|
text: "Private key not found"
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
await upgradeProject.mutateAsync({
|
||||||
|
projectId: project.id,
|
||||||
|
privateKey: PRIVATE_KEY
|
||||||
|
});
|
||||||
|
|
||||||
|
await getLatestProjectStatus();
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (project.version === "v1") {
|
||||||
|
getLatestProjectStatus();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (projectStatus && projectStatus?.status !== null) {
|
||||||
|
if (projectStatus.status === "IN_PROGRESS") {
|
||||||
|
setCurrentStatus("Your upgrade is being processed.");
|
||||||
|
} else if (projectStatus.status === "FAILED") {
|
||||||
|
setCurrentStatus("Upgrade failed, please try again.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const interval = setInterval(() => {
|
||||||
|
if (project.version === "v1") {
|
||||||
|
getLatestProjectStatus();
|
||||||
|
}
|
||||||
|
}, 5_000);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
clearInterval(interval);
|
||||||
|
};
|
||||||
|
}, [projectStatus]);
|
||||||
|
|
||||||
|
const isLoading =
|
||||||
|
(upgradeProject.isLoading ||
|
||||||
|
currentStatus !== null ||
|
||||||
|
(currentStatus === null && statusIsLoading)) &&
|
||||||
|
projectStatus?.status !== "FAILED";
|
||||||
|
|
||||||
|
if (project.version !== "v1") return null;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="my-8">
|
||||||
|
<Alert title="Upgrade your project" variant="warning">
|
||||||
|
<div className="max-w-md">
|
||||||
|
Upgrade your project version to continue receiving the latest improvements and patches.
|
||||||
|
{currentStatus && <p className="mt-2 opacity-80">Status: {currentStatus}</p>}
|
||||||
|
</div>
|
||||||
|
<div className="mt-2">
|
||||||
|
<Button isLoading={isLoading} isDisabled={isLoading} onClick={onUpgradeProject}>
|
||||||
|
Upgrade
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</Alert>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
export { UpgradeProjectAlert } from "./UpgradeProjectAlert";
|
||||||
@@ -7,6 +7,7 @@ export {
|
|||||||
useDeleteUserFromWorkspace,
|
useDeleteUserFromWorkspace,
|
||||||
useDeleteWorkspace,
|
useDeleteWorkspace,
|
||||||
useDeleteWsEnvironment,
|
useDeleteWsEnvironment,
|
||||||
|
useGetUpgradeProjectStatus,
|
||||||
useGetUserWorkspaceMemberships,
|
useGetUserWorkspaceMemberships,
|
||||||
useGetUserWorkspaces,
|
useGetUserWorkspaces,
|
||||||
useGetWorkspaceAuthorizations,
|
useGetWorkspaceAuthorizations,
|
||||||
|
|||||||
@@ -24,6 +24,7 @@ export const workspaceKeys = {
|
|||||||
getWorkspaceSecrets: (workspaceId: string) => [{ workspaceId }, "workspace-secrets"] as const,
|
getWorkspaceSecrets: (workspaceId: string) => [{ workspaceId }, "workspace-secrets"] as const,
|
||||||
getWorkspaceIndexStatus: (workspaceId: string) =>
|
getWorkspaceIndexStatus: (workspaceId: string) =>
|
||||||
[{ workspaceId }, "workspace-index-status"] as const,
|
[{ workspaceId }, "workspace-index-status"] as const,
|
||||||
|
getProjectUpgradeStatus: (workspaceId: string) => [{ workspaceId }, "workspace-upgrade-status"],
|
||||||
getWorkspaceMemberships: (orgId: string) => [{ orgId }, "workspace-memberships"],
|
getWorkspaceMemberships: (orgId: string) => [{ orgId }, "workspace-memberships"],
|
||||||
getWorkspaceAuthorization: (workspaceId: string) => [{ workspaceId }, "workspace-authorizations"],
|
getWorkspaceAuthorization: (workspaceId: string) => [{ workspaceId }, "workspace-authorizations"],
|
||||||
getWorkspaceIntegrations: (workspaceId: string) => [{ workspaceId }, "workspace-integrations"],
|
getWorkspaceIntegrations: (workspaceId: string) => [{ workspaceId }, "workspace-integrations"],
|
||||||
@@ -51,6 +52,14 @@ const fetchWorkspaceIndexStatus = async (workspaceId: string) => {
|
|||||||
return data;
|
return data;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const fetchProjectUpgradeStatus = async (projectId: string) => {
|
||||||
|
const { data } = await apiRequest.get<{ status: string }>(
|
||||||
|
`/api/v2/workspace/${projectId}/upgrade/status`
|
||||||
|
);
|
||||||
|
|
||||||
|
return data;
|
||||||
|
};
|
||||||
|
|
||||||
export const fetchWorkspaceSecrets = async (workspaceId: string) => {
|
export const fetchWorkspaceSecrets = async (workspaceId: string) => {
|
||||||
const {
|
const {
|
||||||
data: { secrets }
|
data: { secrets }
|
||||||
@@ -76,6 +85,14 @@ export const useUpgradeProject = () => {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const useGetUpgradeProjectStatus = (projectId: string) => {
|
||||||
|
return useQuery({
|
||||||
|
queryKey: workspaceKeys.getProjectUpgradeStatus(projectId),
|
||||||
|
queryFn: () => fetchProjectUpgradeStatus(projectId),
|
||||||
|
enabled: true
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
const fetchUserWorkspaces = async () => {
|
const fetchUserWorkspaces = async () => {
|
||||||
const { data } = await apiRequest.get<{ workspaces: Workspace[] }>("/api/v1/workspace");
|
const { data } = await apiRequest.get<{ workspaces: Workspace[] }>("/api/v1/workspace");
|
||||||
return data.workspaces;
|
return data.workspaces;
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ export type Workspace = {
|
|||||||
name: string;
|
name: string;
|
||||||
orgId: string;
|
orgId: string;
|
||||||
version: "v1" | "v2";
|
version: "v1" | "v2";
|
||||||
|
upgradeStatus: string | null;
|
||||||
autoCapitalization: boolean;
|
autoCapitalization: boolean;
|
||||||
environments: WorkspaceEnv[];
|
environments: WorkspaceEnv[];
|
||||||
slug: string;
|
slug: string;
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { useCallback, useEffect, useRef, useState } from "react";
|
import { useEffect, useRef, useState } from "react";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import { useRouter } from "next/router";
|
import { useRouter } from "next/router";
|
||||||
@@ -14,8 +14,6 @@ import { useNotificationContext } from "@app/components/context/Notifications/No
|
|||||||
import NavHeader from "@app/components/navigation/NavHeader";
|
import NavHeader from "@app/components/navigation/NavHeader";
|
||||||
import { PermissionDeniedBanner } from "@app/components/permissions";
|
import { PermissionDeniedBanner } from "@app/components/permissions";
|
||||||
import {
|
import {
|
||||||
Alert,
|
|
||||||
AlertDescription,
|
|
||||||
Button,
|
Button,
|
||||||
EmptyState,
|
EmptyState,
|
||||||
IconButton,
|
IconButton,
|
||||||
@@ -31,6 +29,7 @@ import {
|
|||||||
Tooltip,
|
Tooltip,
|
||||||
Tr
|
Tr
|
||||||
} from "@app/components/v2";
|
} from "@app/components/v2";
|
||||||
|
import { UpgradeProjectAlert } from "@app/components/v2/UpgradeProjectAlert";
|
||||||
import { useOrganization, useWorkspace } from "@app/context";
|
import { useOrganization, useWorkspace } from "@app/context";
|
||||||
import {
|
import {
|
||||||
useCreateFolder,
|
useCreateFolder,
|
||||||
@@ -39,8 +38,7 @@ import {
|
|||||||
useGetFoldersByEnv,
|
useGetFoldersByEnv,
|
||||||
useGetProjectSecretsAllEnv,
|
useGetProjectSecretsAllEnv,
|
||||||
useGetUserWsKey,
|
useGetUserWsKey,
|
||||||
useUpdateSecretV3,
|
useUpdateSecretV3
|
||||||
useUpgradeProject
|
|
||||||
} from "@app/hooks/api";
|
} from "@app/hooks/api";
|
||||||
|
|
||||||
import { FolderBreadCrumbs } from "./components/FolderBreadCrumbs";
|
import { FolderBreadCrumbs } from "./components/FolderBreadCrumbs";
|
||||||
@@ -107,7 +105,6 @@ export const SecretOverviewPage = () => {
|
|||||||
environments: userAvailableEnvs.map(({ slug }) => slug)
|
environments: userAvailableEnvs.map(({ slug }) => slug)
|
||||||
});
|
});
|
||||||
|
|
||||||
const upgradeProject = useUpgradeProject();
|
|
||||||
const { mutateAsync: createSecretV3 } = useCreateSecretV3();
|
const { mutateAsync: createSecretV3 } = useCreateSecretV3();
|
||||||
const { mutateAsync: updateSecretV3 } = useUpdateSecretV3();
|
const { mutateAsync: updateSecretV3 } = useUpdateSecretV3();
|
||||||
const { mutateAsync: deleteSecretV3 } = useDeleteSecretV3();
|
const { mutateAsync: deleteSecretV3 } = useDeleteSecretV3();
|
||||||
@@ -201,24 +198,6 @@ export const SecretOverviewPage = () => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const onUpgradeProject = useCallback(async () => {
|
|
||||||
const PRIVATE_KEY = localStorage.getItem("PRIVATE_KEY");
|
|
||||||
|
|
||||||
if (!PRIVATE_KEY) {
|
|
||||||
createNotification({
|
|
||||||
type: "error",
|
|
||||||
text: "Private key not found"
|
|
||||||
});
|
|
||||||
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
await upgradeProject.mutateAsync({
|
|
||||||
projectId: workspaceId,
|
|
||||||
privateKey: PRIVATE_KEY
|
|
||||||
});
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const handleResetSearch = () => setSearchFilter("");
|
const handleResetSearch = () => setSearchFilter("");
|
||||||
|
|
||||||
const handleFolderClick = (path: string) => {
|
const handleFolderClick = (path: string) => {
|
||||||
@@ -338,22 +317,7 @@ export const SecretOverviewPage = () => {
|
|||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{currentWorkspace?.version === "v1" && (
|
{currentWorkspace?.version === "v1" && <UpgradeProjectAlert project={currentWorkspace} />}
|
||||||
<div className="mt-8">
|
|
||||||
<Alert variant="danger">
|
|
||||||
<AlertDescription className="prose">
|
|
||||||
Upgrade your project. More filler text More filler text More filler text More filler
|
|
||||||
text More filler text More filler text More filler text More filler text More filler
|
|
||||||
text More filler text More filler text More filler text{" "}
|
|
||||||
</AlertDescription>
|
|
||||||
<div className="mt-2">
|
|
||||||
<Button isLoading={upgradeProject.isLoading} onClick={onUpgradeProject}>
|
|
||||||
Upgrade
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</Alert>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
<div className="mt-8 flex items-center justify-between">
|
<div className="mt-8 flex items-center justify-between">
|
||||||
<FolderBreadCrumbs secretPath={secretPath} onResetSearch={handleResetSearch} />
|
<FolderBreadCrumbs secretPath={secretPath} onResetSearch={handleResetSearch} />
|
||||||
<div className="w-80">
|
<div className="w-80">
|
||||||
|
|||||||
Reference in New Issue
Block a user