From 8333250b0b5a8a1e8869c332079df7092af2ef90 Mon Sep 17 00:00:00 2001
From: Daniel Hougaard <62331820+DanielHougaard@users.noreply.github.com>
Date: Fri, 9 Feb 2024 22:28:37 +0400
Subject: [PATCH] Ghost user and migration finished!
---
.../secret-approval-request-secret-dal.ts | 32 +-
backend/src/lib/secret/index.ts | 126 ++++++
backend/src/server/routes/index.ts | 5 +
backend/src/server/routes/v1/bot-router.ts | 4 +-
backend/src/server/routes/v1/index.ts | 1 +
backend/src/server/routes/v2/index.ts | 2 +
.../project-membership-router.ts} | 41 +-
.../src/server/routes/v2/project-router.ts | 77 +++-
backend/src/server/routes/v3/index.ts | 2 -
.../project-bot/project-bot-service.ts | 7 +-
.../services/project-bot/project-bot-types.ts | 4 +
.../src/services/project/project-service.ts | 401 +++++++++++++++++-
backend/src/services/project/project-types.ts | 6 +
backend/src/services/secret/secret-dal.ts | 34 +-
.../src/services/secret/secret-version-dal.ts | 52 ++-
.../utilities/secrets/encryptSecrets.ts | 2 -
frontend/src/hooks/api/keys/queries.tsx | 4 +-
frontend/src/hooks/api/users/mutation.tsx | 2 +-
frontend/src/hooks/api/workspace/index.tsx | 3 +-
frontend/src/hooks/api/workspace/queries.tsx | 17 +-
frontend/src/hooks/api/workspace/types.ts | 2 +-
frontend/src/layouts/AppLayout/AppLayout.tsx | 57 +--
.../MemberListTab/MemberListTab.tsx | 9 +-
.../SecretOverviewPage/SecretOverviewPage.tsx | 43 +-
24 files changed, 810 insertions(+), 123 deletions(-)
create mode 100644 backend/src/lib/secret/index.ts
rename backend/src/server/routes/{v3/project-router.ts => v2/project-membership-router.ts} (55%)
diff --git a/backend/src/ee/services/secret-approval-request/secret-approval-request-secret-dal.ts b/backend/src/ee/services/secret-approval-request/secret-approval-request-secret-dal.ts
index 9b4742255..93e5a2538 100644
--- a/backend/src/ee/services/secret-approval-request/secret-approval-request-secret-dal.ts
+++ b/backend/src/ee/services/secret-approval-request/secret-approval-request-secret-dal.ts
@@ -1,8 +1,14 @@
import { Knex } from "knex";
import { TDbClient } from "@app/db";
-import { SecretApprovalRequestsSecretsSchema, TableName, TSecretTags } from "@app/db/schemas";
-import { DatabaseError } from "@app/lib/errors";
+import {
+ SecretApprovalRequestsSecretsSchema,
+ TableName,
+ TSecretApprovalRequestsSecrets,
+ TSecretApprovalRequestsSecretsUpdate,
+ TSecretTags
+} from "@app/db/schemas";
+import { BadRequestError, DatabaseError } from "@app/lib/errors";
import { ormify, selectAllTableCols, sqlNestRelationships } from "@app/lib/knex";
export type TSecretApprovalRequestSecretDALFactory = ReturnType;
@@ -11,6 +17,27 @@ export const secretApprovalRequestSecretDALFactory = (db: TDbClient) => {
const secretApprovalRequestSecretOrm = ormify(db, TableName.SecretApprovalRequestSecret);
const secretApprovalRequestSecretTagOrm = ormify(db, TableName.SecretApprovalRequestSecretTag);
+ const bulkUpdateNoVersionIncrement = async (
+ data: Array<{ filter: Partial; data: TSecretApprovalRequestsSecretsUpdate }>,
+ tx?: Knex
+ ) => {
+ try {
+ const secs = await Promise.all(
+ data.map(async ({ filter, data: updateData }) => {
+ const [doc] = await (tx || db)(TableName.SecretApprovalRequestSecret)
+ .where(filter)
+ .update(updateData)
+ .returning("*");
+ if (!doc) throw new BadRequestError({ message: "Failed to update document" });
+ return doc;
+ })
+ );
+ return secs;
+ } catch (error) {
+ throw new DatabaseError({ error, name: "bulk update secret" });
+ }
+ };
+
const findByRequestId = async (requestId: string, tx?: Knex) => {
try {
const doc = await (tx || db)({
@@ -190,6 +217,7 @@ export const secretApprovalRequestSecretDALFactory = (db: TDbClient) => {
return {
...secretApprovalRequestSecretOrm,
findByRequestId,
+ bulkUpdateNoVersionIncrement,
insertApprovalSecretTags: secretApprovalRequestSecretTagOrm.insertMany
};
};
diff --git a/backend/src/lib/secret/index.ts b/backend/src/lib/secret/index.ts
new file mode 100644
index 000000000..08cb0163d
--- /dev/null
+++ b/backend/src/lib/secret/index.ts
@@ -0,0 +1,126 @@
+import { z } from "zod";
+
+import { SecretKeyEncoding, TProjectKeys } from "@app/db/schemas";
+
+import { decryptAsymmetric, decryptSymmetric } from "../crypto";
+import { decryptSymmetric128BitHexKeyUTF8, TDecryptSymmetricInput } from "../crypto/encryption";
+
+export enum SecretDocType {
+ Secret = "secret",
+ SecretVersion = "secretVersion",
+ ApprovalSecret = "approvalSecret"
+}
+
+const PartialSecretSchema = z.object({
+ id: z.string(),
+ secretKeyCiphertext: z.string(),
+ secretKeyIV: z.string(),
+ secretKeyTag: z.string(),
+
+ secretValueCiphertext: z.string(),
+ secretValueIV: z.string(),
+ secretValueTag: z.string(),
+
+ secretCommentCiphertext: z.string().nullish(),
+ secretCommentIV: z.string().nullish(),
+ secretCommentTag: z.string().nullish(),
+
+ docType: z.nativeEnum(SecretDocType),
+
+ keyEncoding: z.string()
+});
+
+const PartialDecryptedSecretSchema = z.object({
+ id: z.string(),
+ secretKey: z.string(),
+ secretValue: z.string(),
+ secretComment: z.string().optional(),
+
+ docType: z.nativeEnum(SecretDocType)
+});
+
+export type TPartialSecret = z.infer;
+export type TPartialDecryptedSecret = z.infer;
+
+const symmetricDecrypt = ({
+ keyEncoding,
+ ciphertext,
+ tag,
+ iv,
+ key,
+ isApprovalSecret
+}: TDecryptSymmetricInput & { keyEncoding: SecretKeyEncoding; isApprovalSecret: boolean }) => {
+ if (keyEncoding === SecretKeyEncoding.UTF8 || isApprovalSecret) {
+ const data = decryptSymmetric128BitHexKeyUTF8({ key, iv, tag, ciphertext });
+ return data;
+ }
+ if (keyEncoding === SecretKeyEncoding.BASE64) {
+ const data = decryptSymmetric({ key, iv, tag, ciphertext });
+ return data;
+ }
+
+ throw new Error("Missing both encryption keys");
+};
+
+export const decryptSecrets = (
+ encryptedSecrets: TPartialSecret[],
+ privateKey: string,
+ latestKey: TProjectKeys & {
+ sender: {
+ publicKey: string;
+ };
+ }
+) => {
+ const key = decryptAsymmetric({
+ ciphertext: latestKey.encryptedKey,
+ nonce: latestKey.nonce,
+ publicKey: latestKey.sender.publicKey,
+ privateKey
+ });
+
+ const secrets: TPartialDecryptedSecret[] = [];
+
+ encryptedSecrets.forEach((encSecret) => {
+ const secretKey = symmetricDecrypt({
+ ciphertext: encSecret.secretKeyCiphertext,
+ iv: encSecret.secretKeyIV,
+ tag: encSecret.secretKeyTag,
+ key,
+ keyEncoding: encSecret.keyEncoding as SecretKeyEncoding,
+ isApprovalSecret: encSecret.docType === SecretDocType.ApprovalSecret
+ });
+
+ const secretValue = symmetricDecrypt({
+ ciphertext: encSecret.secretValueCiphertext,
+ iv: encSecret.secretValueIV,
+ tag: encSecret.secretValueTag,
+ key,
+ keyEncoding: encSecret.keyEncoding as SecretKeyEncoding,
+ isApprovalSecret: encSecret.docType === SecretDocType.ApprovalSecret
+ });
+
+ const secretComment =
+ encSecret.secretCommentCiphertext && encSecret.secretCommentIV && encSecret.secretCommentTag
+ ? symmetricDecrypt({
+ ciphertext: encSecret.secretCommentCiphertext,
+ iv: encSecret.secretCommentIV,
+ tag: encSecret.secretCommentTag,
+ key,
+ keyEncoding: encSecret.keyEncoding as SecretKeyEncoding,
+ isApprovalSecret: encSecret.docType === SecretDocType.ApprovalSecret
+ })
+ : "";
+
+ const decryptedSecret: TPartialDecryptedSecret = {
+ id: encSecret.id,
+ secretKey,
+ secretValue,
+ secretComment,
+ docType: encSecret.docType
+ };
+
+ secrets.push(decryptedSecret);
+ });
+
+ return secrets;
+};
diff --git a/backend/src/server/routes/index.ts b/backend/src/server/routes/index.ts
index e793d2133..068142ab8 100644
--- a/backend/src/server/routes/index.ts
+++ b/backend/src/server/routes/index.ts
@@ -306,7 +306,12 @@ export const registerRoutes = async (
identityProjectDAL,
identityOrgMembershipDAL,
projectBotDAL,
+ secretDAL,
+ orgDAL,
+ secretApprovalRequestDAL,
+ secretApprovalSecretDAL: sarSecretDAL,
projectKeyDAL,
+ secretVersionDAL,
userDAL,
projectEnvDAL,
orgService,
diff --git a/backend/src/server/routes/v1/bot-router.ts b/backend/src/server/routes/v1/bot-router.ts
index ff451a17e..20acf95ec 100644
--- a/backend/src/server/routes/v1/bot-router.ts
+++ b/backend/src/server/routes/v1/bot-router.ts
@@ -33,7 +33,7 @@ export const registerProjectBotRouter = async (server: FastifyZodProvider) => {
projectId: req.params.projectId
});
- if (!project.e2ee) {
+ if (project.version === "v2") {
throw new BadRequestError({ message: "Failed to find bot, project has E2EE disabled" });
}
@@ -79,7 +79,7 @@ export const registerProjectBotRouter = async (server: FastifyZodProvider) => {
handler: async (req) => {
const project = await server.services.projectBot.findProjectByBotId(req.params.botId);
- if (project?.e2ee === false) {
+ if (project?.version === "v2") {
throw new BadRequestError({ message: "Failed to set bot active, project has E2EE disabled" });
}
diff --git a/backend/src/server/routes/v1/index.ts b/backend/src/server/routes/v1/index.ts
index 744ba6ebf..fbc68d974 100644
--- a/backend/src/server/routes/v1/index.ts
+++ b/backend/src/server/routes/v1/index.ts
@@ -48,6 +48,7 @@ export const registerV1Routes = async (server: FastifyZodProvider) => {
await projectRouter.register(registerProjectMembershipRouter);
await projectRouter.register(registerSecretTagRouter);
},
+
{ prefix: "/workspace" }
);
diff --git a/backend/src/server/routes/v2/index.ts b/backend/src/server/routes/v2/index.ts
index 1f423c084..deebbc981 100644
--- a/backend/src/server/routes/v2/index.ts
+++ b/backend/src/server/routes/v2/index.ts
@@ -2,6 +2,7 @@ import { registerIdentityOrgRouter } from "./identity-org-router";
import { registerIdentityProjectRouter } from "./identity-project-router";
import { registerMfaRouter } from "./mfa-router";
import { registerOrgRouter } from "./organization-router";
+import { registerProjectMembershipRouter } from "./project-membership-router";
import { registerProjectRouter } from "./project-router";
import { registerServiceTokenRouter } from "./service-token-router";
import { registerUserRouter } from "./user-router";
@@ -21,6 +22,7 @@ export const registerV2Routes = async (server: FastifyZodProvider) => {
async (projectServer) => {
await projectServer.register(registerProjectRouter);
await projectServer.register(registerIdentityProjectRouter);
+ await projectServer.register(registerProjectMembershipRouter);
},
{ prefix: "/workspace" }
);
diff --git a/backend/src/server/routes/v3/project-router.ts b/backend/src/server/routes/v2/project-membership-router.ts
similarity index 55%
rename from backend/src/server/routes/v3/project-router.ts
rename to backend/src/server/routes/v2/project-membership-router.ts
index c1759f718..733147b00 100644
--- a/backend/src/server/routes/v3/project-router.ts
+++ b/backend/src/server/routes/v2/project-membership-router.ts
@@ -1,47 +1,10 @@
import { z } from "zod";
-import { ProjectMembershipsSchema, ProjectsSchema } from "@app/db/schemas";
+import { ProjectMembershipsSchema } from "@app/db/schemas";
import { EventType } from "@app/ee/services/audit-log/audit-log-types";
import { authRateLimit } from "@app/server/config/rateLimiter";
-const projectWithEnv = ProjectsSchema.merge(
- z.object({
- _id: z.string(),
- environments: z.object({ name: z.string(), slug: z.string(), id: z.string() }).array()
- })
-);
-
-export const registerProjectRouter = async (server: FastifyZodProvider) => {
- /* Create new project */
- server.route({
- method: "POST",
- url: "/",
- config: {
- rateLimit: authRateLimit
- },
- schema: {
- body: z.object({
- projectName: z.string().trim(),
- organizationId: z.string().trim()
- }),
- response: {
- 200: z.object({
- project: projectWithEnv
- })
- }
- },
- handler: async (req) => {
- const project = await server.services.project.createProject({
- actorId: req.permission.id,
- actor: req.permission.type,
- orgId: req.body.organizationId,
- workspaceName: req.body.projectName
- });
-
- return { project };
- }
- });
-
+export const registerProjectMembershipRouter = async (server: FastifyZodProvider) => {
server.route({
method: "POST",
url: "/:projectId/memberships",
diff --git a/backend/src/server/routes/v2/project-router.ts b/backend/src/server/routes/v2/project-router.ts
index d38efdbb9..bc0282850 100644
--- a/backend/src/server/routes/v2/project-router.ts
+++ b/backend/src/server/routes/v2/project-router.ts
@@ -1,13 +1,22 @@
import { z } from "zod";
-import { ProjectKeysSchema } from "@app/db/schemas";
+import { ProjectKeysSchema, ProjectsSchema } from "@app/db/schemas";
import { EventType } from "@app/ee/services/audit-log/audit-log-types";
+import { authRateLimit } from "@app/server/config/rateLimiter";
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
import { AuthMode } from "@app/services/auth/auth-type";
+const projectWithEnv = ProjectsSchema.merge(
+ z.object({
+ _id: z.string(),
+ environments: z.object({ name: z.string(), slug: z.string(), id: z.string() }).array()
+ })
+);
+
export const registerProjectRouter = async (server: FastifyZodProvider) => {
+ /* Get project key */
server.route({
- url: "/:workspaceId/encrypted-key",
+ url: "/:projectId/encrypted-key",
method: "GET",
schema: {
description: "Return encrypted project key",
@@ -17,7 +26,7 @@ export const registerProjectRouter = async (server: FastifyZodProvider) => {
}
],
params: z.object({
- workspaceId: z.string().trim()
+ projectId: z.string().trim()
}),
response: {
200: ProjectKeysSchema.merge(
@@ -34,13 +43,13 @@ export const registerProjectRouter = async (server: FastifyZodProvider) => {
const key = await server.services.projectKey.getLatestProjectKey({
actor: req.permission.type,
actorId: req.permission.id,
- projectId: req.params.workspaceId,
- actorOrgId: req.permission.orgId
+ actorOrgId: req.permission.orgId,
+ projectId: req.params.projectId
});
await server.services.auditLog.createAuditLog({
...req.auditLogInfo,
- projectId: req.params.workspaceId,
+ projectId: req.params.projectId,
event: {
type: EventType.GET_WORKSPACE_KEY,
metadata: {
@@ -52,4 +61,60 @@ export const registerProjectRouter = async (server: FastifyZodProvider) => {
return key;
}
});
+
+ server.route({
+ url: "/:projectId/upgrade",
+ method: "POST",
+ schema: {
+ params: z.object({
+ projectId: z.string().trim()
+ }),
+
+ body: z.object({
+ userPrivateKey: z.string().trim()
+ }),
+ response: {
+ 200: z.object({})
+ }
+ },
+ onRequest: verifyAuth([AuthMode.JWT, AuthMode.API_KEY]),
+ handler: async (req) => {
+ await server.services.project.upgradeProject({
+ actorId: req.permission.id,
+ actor: req.permission.type,
+ projectId: req.params.projectId,
+ userPrivateKey: req.body.userPrivateKey
+ });
+ }
+ });
+
+ /* Create new project */
+ server.route({
+ method: "POST",
+ url: "/",
+ config: {
+ rateLimit: authRateLimit
+ },
+ schema: {
+ body: z.object({
+ projectName: z.string().trim(),
+ organizationId: z.string().trim()
+ }),
+ response: {
+ 200: z.object({
+ project: projectWithEnv
+ })
+ }
+ },
+ handler: async (req) => {
+ const project = await server.services.project.createProject({
+ actorId: req.permission.id,
+ actor: req.permission.type,
+ orgId: req.body.organizationId,
+ workspaceName: req.body.projectName
+ });
+
+ return { project };
+ }
+ });
};
diff --git a/backend/src/server/routes/v3/index.ts b/backend/src/server/routes/v3/index.ts
index 21d57d359..6a061a635 100644
--- a/backend/src/server/routes/v3/index.ts
+++ b/backend/src/server/routes/v3/index.ts
@@ -1,5 +1,4 @@
import { registerLoginRouter } from "./login-router";
-import { registerProjectRouter } from "./project-router";
import { registerSecretBlindIndexRouter } from "./secret-blind-index-router";
import { registerSecretRouter } from "./secret-router";
import { registerSignupRouter } from "./signup-router";
@@ -11,5 +10,4 @@ export const registerV3Routes = async (server: FastifyZodProvider) => {
await server.register(registerUserRouter, { prefix: "/users" });
await server.register(registerSecretRouter, { prefix: "/secrets" });
await server.register(registerSecretBlindIndexRouter, { prefix: "/workspaces" });
- await server.register(registerProjectRouter, { prefix: "/projects" });
};
diff --git a/backend/src/services/project-bot/project-bot-service.ts b/backend/src/services/project-bot/project-bot-service.ts
index 476fba829..e95ab1610 100644
--- a/backend/src/services/project-bot/project-bot-service.ts
+++ b/backend/src/services/project-bot/project-bot-service.ts
@@ -50,6 +50,7 @@ export const projectBotServiceFactory = ({ projectBotDAL, permissionService }: T
projectId,
actorOrgId,
privateKey,
+ botKey,
publicKey
}: TFindBotByProjectIdDTO) => {
const { permission } = await permissionService.getProjectPermission(actor, actorId, projectId, actorOrgId);
@@ -73,7 +74,11 @@ export const projectBotServiceFactory = ({ projectBotDAL, permissionService }: T
isActive: false,
publicKey: keys.publicKey,
algorithm,
- keyEncoding: encoding
+ keyEncoding: encoding,
+ ...(botKey && {
+ encryptedProjectKey: botKey.encryptedKey,
+ encryptedProjectKeyNonce: botKey.nonce
+ })
},
tx
);
diff --git a/backend/src/services/project-bot/project-bot-types.ts b/backend/src/services/project-bot/project-bot-types.ts
index 4431c6686..47e3885c7 100644
--- a/backend/src/services/project-bot/project-bot-types.ts
+++ b/backend/src/services/project-bot/project-bot-types.ts
@@ -14,6 +14,10 @@ export type TSetActiveStateDTO = {
export type TFindBotByProjectIdDTO = {
privateKey?: string;
publicKey?: string;
+ botKey?: {
+ nonce: string;
+ encryptedKey: string;
+ };
} & TProjectPermission;
export type TGetPrivateKeyDTO = {
diff --git a/backend/src/services/project/project-service.ts b/backend/src/services/project/project-service.ts
index 26fb5056f..9b079576b 100644
--- a/backend/src/services/project/project-service.ts
+++ b/backend/src/services/project/project-service.ts
@@ -1,32 +1,46 @@
+/* eslint-disable no-console */
+/* eslint-disable no-await-in-loop */
import { ForbiddenError } from "@casl/ability";
import slugify from "@sindresorhus/slugify";
-import { ProjectMembershipRole } from "@app/db/schemas";
+import { ProjectMembershipRole, ProjectVersion, SecretKeyEncoding, TSecrets } from "@app/db/schemas";
import { TLicenseServiceFactory } from "@app/ee/services/license/license-service";
import { OrgPermissionActions, OrgPermissionSubjects } from "@app/ee/services/permission/org-permission";
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service";
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 { getConfig } from "@app/lib/config/env";
import { createSecretBlindIndex } from "@app/lib/crypto";
-import { infisicalSymmetricEncypt } from "@app/lib/crypto/encryption";
+import {
+ decryptAsymmetric,
+ encryptSymmetric128BitHexKeyUTF8,
+ infisicalSymmetricDecrypt,
+ infisicalSymmetricEncypt
+} from "@app/lib/crypto/encryption";
import { BadRequestError, ForbiddenRequestError } from "@app/lib/errors";
import { alphaNumericNanoId } from "@app/lib/nanoid";
-import { createWorkspaceKey, createWsMembers } from "@app/lib/project";
+import { createProjectKey, createWsMembers } from "@app/lib/project";
+import { decryptSecrets, SecretDocType, TPartialSecret } from "@app/lib/secret";
import { ActorType } from "../auth/auth-type";
import { TIdentityOrgDALFactory } from "../identity/identity-org-dal";
import { TIdentityProjectDALFactory } from "../identity-project/identity-project-dal";
+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 { TSecretBlindIndexDALFactory } from "../secret-blind-index/secret-blind-index-dal";
import { ROOT_FOLDER_NAME, TSecretFolderDALFactory } from "../secret-folder/secret-folder-dal";
import { TUserDALFactory } from "../user/user-dal";
import { TProjectDALFactory } from "./project-dal";
-import { TCreateProjectDTO, TDeleteProjectDTO, TGetProjectDTO } from "./project-types";
+import { TCreateProjectDTO, TDeleteProjectDTO, TGetProjectDTO, TUpgradeProjectDTO } from "./project-types";
export const DEFAULT_PROJECT_ENVS = [
{ name: "Development", slug: "dev" },
@@ -37,16 +51,21 @@ export const DEFAULT_PROJECT_ENVS = [
type TProjectServiceFactoryDep = {
projectDAL: TProjectDALFactory;
userDAL: TUserDALFactory;
- folderDAL: Pick;
- projectEnvDAL: Pick;
+ folderDAL: TSecretFolderDALFactory;
+ projectEnvDAL: Pick;
+ secretVersionDAL: TSecretVersionDALFactory;
identityOrgMembershipDAL: TIdentityOrgDALFactory;
identityProjectDAL: TIdentityProjectDALFactory;
- projectKeyDAL: Pick;
- projectBotDAL: Pick;
- projectMembershipDAL: Pick;
- orgService: Pick;
+ projectKeyDAL: Pick;
+ projectBotDAL: Pick;
+ projectMembershipDAL: Pick;
+ orgDAL: TOrgDALFactory;
+ secretApprovalRequestDAL: TSecretApprovalRequestDALFactory;
+ secretApprovalSecretDAL: TSecretApprovalRequestSecretDALFactory;
secretBlindIndexDAL: Pick;
permissionService: TPermissionServiceFactory;
+ orgService: Pick;
+ secretDAL: TSecretDALFactory;
licenseService: Pick;
};
@@ -55,13 +74,18 @@ export type TProjectServiceFactory = ReturnType;
export const projectServiceFactory = ({
projectDAL,
projectKeyDAL,
+ secretApprovalRequestDAL,
+ secretApprovalSecretDAL,
permissionService,
userDAL,
folderDAL,
orgService,
+ orgDAL,
identityProjectDAL,
+ secretVersionDAL,
projectBotDAL,
identityOrgMembershipDAL,
+ secretDAL,
secretBlindIndexDAL,
projectMembershipDAL,
projectEnvDAL,
@@ -99,7 +123,7 @@ export const projectServiceFactory = ({
name: workspaceName,
orgId,
slug: slugify(`${workspaceName}-${alphaNumericNanoId(4)}`),
- e2ee: false
+ version: ProjectVersion.V2
},
tx
);
@@ -136,7 +160,7 @@ export const projectServiceFactory = ({
);
// 3. Create a random key that we'll use as the project key.
- const { key: encryptedProjectKey, iv: encryptedProjectKeyIv } = createWorkspaceKey({
+ const { key: encryptedProjectKey, iv: encryptedProjectKeyIv } = createProjectKey({
publicKey: ghostUser.keys.publicKey,
privateKey: ghostUser.keys.plainPrivateKey
});
@@ -162,9 +186,12 @@ export const projectServiceFactory = ({
projectId: project.id,
tag,
iv,
+ encryptedProjectKey,
+ encryptedProjectKeyNonce: encryptedProjectKeyIv,
encryptedPrivateKey: ciphertext,
isActive: true,
publicKey: ghostUser.keys.publicKey,
+ senderId: ghostUser.user.id,
algorithm,
keyEncoding: encoding
},
@@ -321,6 +348,353 @@ export const projectServiceFactory = ({
return updatedProject;
};
+ const upgradeProject = async ({ projectId, actor, actorId, userPrivateKey }: TUpgradeProjectDTO) => {
+ const { permission, membership } = await permissionService.getProjectPermission(actor, actorId, projectId);
+
+ ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Delete, ProjectPermissionSub.Project);
+
+ if (membership?.role !== ProjectMembershipRole.Admin) {
+ throw new ForbiddenRequestError({
+ message: "User must be admin"
+ });
+ }
+
+ /*
+ 1. Get the existing project
+ 2. Get the existing project keys
+ 4. Get all the project envs & folders
+ 5. Get ALL secrets within the project
+ 6. Create a new ghost user
+ 7. Create a project membership for the ghost user
+ 8. Get the existing bot, and the existing project keys for the members of the project
+ 9. IF a bot already exists for the project, delete it!
+ 10. Delete all the existing project keys
+ 11. Create a project key for the ghost user
+ 12. Find the newly created ghost user's latest key
+
+
+ FOR EACH OF THE OLD PROJECT KEYS (loop):
+ 13. Find the user based on the key.receiverId.
+ 14. Find the org membership for the user.
+ 15. Create a new project key for the user.
+
+
+ 16. Encrypt the ghost user's private key
+ 17. Create a new bot, and set the public/private key of the bot, to the ghost user's public/private key.
+ 18. Add the workspace key to the bot
+ 19. Decrypt the secrets with the old project key
+ 20. Get the newly created bot's private key, and workspace key (we do it this way to test as many steps of the bot process as possible)
+ 21. Get the workspace key from the bot
+
+
+ FOR EACH DECRYPTED SECRET (loop):
+ 22. Re-encrypt the secret value, secret key, and secret comment with the NEW project key from the bot.
+ 23. Update the secret in the database with the new encrypted values.
+
+
+ 24. Transaction ends. If there were no errors. All changes are applied.
+ 25. API route returns 200 OK.
+ */
+
+ const project = await projectDAL.findOne({ id: projectId, version: ProjectVersion.V1 });
+ const oldProjectKey = await projectKeyDAL.findLatestProjectKey(actorId, projectId);
+ if (!project || !oldProjectKey) {
+ throw new BadRequestError({
+ message: "Project or project key not found"
+ });
+ }
+
+ const projectEnvs = await projectEnvDAL.find({
+ 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 {
createProject,
deleteProject,
@@ -328,6 +702,7 @@ export const projectServiceFactory = ({
findProjectGhostUser,
getAProject,
toggleAutoCapitalization,
- updateName
+ updateName,
+ upgradeProject
};
};
diff --git a/backend/src/services/project/project-types.ts b/backend/src/services/project/project-types.ts
index 2ffea1117..d4bbd9af1 100644
--- a/backend/src/services/project/project-types.ts
+++ b/backend/src/services/project/project-types.ts
@@ -1,3 +1,5 @@
+import { TProjectPermission } from "@app/lib/types";
+
import { ActorType } from "../auth/auth-type";
export type TCreateProjectDTO = {
@@ -21,3 +23,7 @@ export type TGetProjectDTO = {
actorOrgId?: string;
projectId: string;
};
+
+export type TUpgradeProjectDTO = {
+ userPrivateKey: string;
+} & TProjectPermission;
diff --git a/backend/src/services/secret/secret-dal.ts b/backend/src/services/secret/secret-dal.ts
index c4b74eef8..b3b538168 100644
--- a/backend/src/services/secret/secret-dal.ts
+++ b/backend/src/services/secret/secret-dal.ts
@@ -22,7 +22,11 @@ export const secretDALFactory = (db: TDbClient) => {
// the idea is to use postgres specific function
// insert with id this will cause a conflict then merge the data
- const bulkUpdate = async (data: Array<{ filter: Partial; data: TSecretsUpdate }>, tx?: Knex) => {
+ const bulkUpdate = async (
+ data: Array<{ filter: Partial; data: TSecretsUpdate }>,
+
+ tx?: Knex
+ ) => {
try {
const secs = await Promise.all(
data.map(async ({ filter, data: updateData }) => {
@@ -41,6 +45,24 @@ export const secretDALFactory = (db: TDbClient) => {
}
};
+ const bulkUpdateNoVersionIncrement = async (
+ data: Array<{ filter: Partial; data: TSecretsUpdate }>,
+ tx?: Knex
+ ) => {
+ try {
+ const secs = await Promise.all(
+ data.map(async ({ filter, data: updateData }) => {
+ const [doc] = await (tx || db)(TableName.Secret).where(filter).update(updateData).returning("*");
+ if (!doc) throw new BadRequestError({ message: "Failed to update document" });
+ return doc;
+ })
+ );
+ return secs;
+ } catch (error) {
+ throw new DatabaseError({ error, name: "bulk update secret" });
+ }
+ };
+
const deleteMany = async (
data: Array<{ blindIndex: string; type: SecretType }>,
folderId: string,
@@ -145,5 +167,13 @@ export const secretDALFactory = (db: TDbClient) => {
}
};
- return { ...secretOrm, update, bulkUpdate, deleteMany, findByFolderId, findByBlindIndexes };
+ return {
+ ...secretOrm,
+ update,
+ bulkUpdate,
+ deleteMany,
+ bulkUpdateNoVersionIncrement,
+ findByFolderId,
+ findByBlindIndexes
+ };
};
diff --git a/backend/src/services/secret/secret-version-dal.ts b/backend/src/services/secret/secret-version-dal.ts
index 7a6695e18..516a82a77 100644
--- a/backend/src/services/secret/secret-version-dal.ts
+++ b/backend/src/services/secret/secret-version-dal.ts
@@ -1,8 +1,8 @@
import { Knex } from "knex";
import { TDbClient } from "@app/db";
-import { TableName, TSecretVersions } from "@app/db/schemas";
-import { DatabaseError } from "@app/lib/errors";
+import { TableName, TSecretVersions, TSecretVersionsUpdate } from "@app/db/schemas";
+import { BadRequestError, DatabaseError } from "@app/lib/errors";
import { ormify, selectAllTableCols } from "@app/lib/knex";
export type TSecretVersionDALFactory = ReturnType;
@@ -36,6 +36,46 @@ export const secretVersionDALFactory = (db: TDbClient) => {
}
};
+ const bulkUpdate = async (
+ data: Array<{ filter: Partial; data: TSecretVersionsUpdate }>,
+ tx?: Knex
+ ) => {
+ try {
+ const secs = await Promise.all(
+ data.map(async ({ filter, data: updateData }) => {
+ const [doc] = await (tx || db)(TableName.SecretVersion)
+ .where(filter)
+ .update(updateData)
+ // .increment("version", 1) // TODO: Is this really needed?
+ .returning("*");
+ if (!doc) throw new BadRequestError({ message: "Failed to update document" });
+ return doc;
+ })
+ );
+ return secs;
+ } catch (error) {
+ throw new DatabaseError({ error, name: "bulk update secret" });
+ }
+ };
+
+ const bulkUpdateNoVersionIncrement = async (
+ data: Array<{ filter: Partial; data: TSecretVersionsUpdate }>,
+ tx?: Knex
+ ) => {
+ try {
+ const secs = await Promise.all(
+ data.map(async ({ filter, data: updateData }) => {
+ const [doc] = await (tx || db)(TableName.SecretVersion).where(filter).update(updateData).returning("*");
+ if (!doc) throw new BadRequestError({ message: "Failed to update document" });
+ return doc;
+ })
+ );
+ return secs;
+ } catch (error) {
+ throw new DatabaseError({ error, name: "bulk update secret" });
+ }
+ };
+
const findLatestVersionMany = async (folderId: string, secretIds: string[], tx?: Knex) => {
try {
const docs: Array = await (tx || db)(TableName.SecretVersion)
@@ -59,5 +99,11 @@ export const secretVersionDALFactory = (db: TDbClient) => {
}
};
- return { ...secretVersionOrm, findLatestVersionMany, findLatestVersionByFolderId };
+ return {
+ ...secretVersionOrm,
+ findLatestVersionMany,
+ bulkUpdate,
+ findLatestVersionByFolderId,
+ bulkUpdateNoVersionIncrement
+ };
};
diff --git a/frontend/src/components/utilities/secrets/encryptSecrets.ts b/frontend/src/components/utilities/secrets/encryptSecrets.ts
index 610e2a6f1..943502cdf 100644
--- a/frontend/src/components/utilities/secrets/encryptSecrets.ts
+++ b/frontend/src/components/utilities/secrets/encryptSecrets.ts
@@ -56,7 +56,6 @@ const encryptSecrets = async ({
publicKey: wsKey.sender.publicKey,
privateKey: PRIVATE_KEY
});
-
} else {
// case: a (shared) key does not exist for the workspace
randomBytes = crypto.randomBytes(16).toString("hex");
@@ -116,7 +115,6 @@ const encryptSecrets = async ({
return result;
});
-
} catch (error) {
console.log("Error while encrypting secrets");
}
diff --git a/frontend/src/hooks/api/keys/queries.tsx b/frontend/src/hooks/api/keys/queries.tsx
index 3cf11e189..7e59c0840 100644
--- a/frontend/src/hooks/api/keys/queries.tsx
+++ b/frontend/src/hooks/api/keys/queries.tsx
@@ -8,9 +8,9 @@ const encKeyKeys = {
getUserWorkspaceKey: (workspaceID: string) => ["workspace-key-pair", { workspaceID }] as const
};
-export const fetchUserWsKey = async (workspaceID: string) => {
+export const fetchUserWsKey = async (projectId: string) => {
const { data } = await apiRequest.get(
- `/api/v2/workspace/${workspaceID}/encrypted-key`
+ `/api/v2/workspace/${projectId}/encrypted-key`
);
return data;
diff --git a/frontend/src/hooks/api/users/mutation.tsx b/frontend/src/hooks/api/users/mutation.tsx
index f7c0e1558..7d982cea1 100644
--- a/frontend/src/hooks/api/users/mutation.tsx
+++ b/frontend/src/hooks/api/users/mutation.tsx
@@ -51,7 +51,7 @@ export const useAddUserToWsNonE2EE = () => {
return useMutation<{}, {}, AddUserToWsDTONonE2EE>({
mutationFn: async ({ projectId, emails }) => {
- const { data } = await apiRequest.post(`/api/v3/projects/${projectId}/memberships`, {
+ const { data } = await apiRequest.post(`/api/v2/workspace/${projectId}/memberships`, {
emails
});
return data;
diff --git a/frontend/src/hooks/api/workspace/index.tsx b/frontend/src/hooks/api/workspace/index.tsx
index 63be0f7ad..1ad051bc0 100644
--- a/frontend/src/hooks/api/workspace/index.tsx
+++ b/frontend/src/hooks/api/workspace/index.tsx
@@ -21,5 +21,6 @@ export {
useToggleAutoCapitalization,
useUpdateIdentityWorkspaceRole,
useUpdateUserWorkspaceRole,
- useUpdateWsEnvironment
+ useUpdateWsEnvironment,
+ useUpgradeProject
} from "./queries";
diff --git a/frontend/src/hooks/api/workspace/queries.tsx b/frontend/src/hooks/api/workspace/queries.tsx
index 38879de9d..7ec85c442 100644
--- a/frontend/src/hooks/api/workspace/queries.tsx
+++ b/frontend/src/hooks/api/workspace/queries.tsx
@@ -61,6 +61,21 @@ export const fetchWorkspaceSecrets = async (workspaceId: string) => {
return secrets;
};
+export const useUpgradeProject = () => {
+ const queryClient = useQueryClient();
+
+ return useMutation<{}, {}, { projectId: string; privateKey: string }>({
+ mutationFn: ({ projectId, privateKey }) => {
+ return apiRequest.post(`/api/v2/workspace/${projectId}/upgrade`, {
+ userPrivateKey: privateKey
+ });
+ },
+ onSuccess: () => {
+ queryClient.invalidateQueries(workspaceKeys.getAllUserWorkspace);
+ }
+ });
+};
+
const fetchUserWorkspaces = async () => {
const { data } = await apiRequest.get<{ workspaces: Workspace[] }>("/api/v1/workspace");
return data.workspaces;
@@ -160,7 +175,7 @@ export const createWorkspace = ({
organizationId,
projectName
}: CreateWorkspaceDTO): Promise<{ data: { project: Workspace } }> => {
- return apiRequest.post("/api/v3/projects", { projectName, organizationId });
+ return apiRequest.post("/api/v2/workspace", { projectName, organizationId });
};
export const useCreateWorkspace = () => {
diff --git a/frontend/src/hooks/api/workspace/types.ts b/frontend/src/hooks/api/workspace/types.ts
index 240a9e858..5fd3898f8 100644
--- a/frontend/src/hooks/api/workspace/types.ts
+++ b/frontend/src/hooks/api/workspace/types.ts
@@ -3,7 +3,7 @@ export type Workspace = {
id: string;
name: string;
orgId: string;
- e2ee: boolean;
+ version: "v1" | "v2";
autoCapitalization: boolean;
environments: WorkspaceEnv[];
slug: string;
diff --git a/frontend/src/layouts/AppLayout/AppLayout.tsx b/frontend/src/layouts/AppLayout/AppLayout.tsx
index 6f9d80e6b..1cda2c6d7 100644
--- a/frontend/src/layouts/AppLayout/AppLayout.tsx
+++ b/frontend/src/layouts/AppLayout/AppLayout.tsx
@@ -4,7 +4,6 @@
/* eslint-disable vars-on-top */
/* eslint-disable no-var */
/* eslint-disable func-names */
-import crypto from "crypto";
import { useEffect } from "react";
import { Controller, useForm } from "react-hook-form";
@@ -34,7 +33,6 @@ import * as yup from "yup";
import { useNotificationContext } from "@app/components/context/Notifications/NotificationProvider";
import { OrgPermissionCan } from "@app/components/permissions";
import { tempLocalStorage } from "@app/components/utilities/checks/tempLocalStorage";
-import { encryptAssymmetric } from "@app/components/utilities/cryptography/crypto";
import {
Button,
Checkbox,
@@ -62,16 +60,14 @@ import {
import { usePopUp } from "@app/hooks";
import {
fetchOrgUsers,
- useAddUserToWsE2EE,
+ useAddUserToWsNonE2EE,
useCreateWorkspace,
useGetOrgTrialUrl,
useGetSecretApprovalRequestCount,
useGetUserAction,
useLogoutUser,
- useRegisterUserAction,
- useUploadWsKey
+ useRegisterUserAction
} from "@app/hooks/api";
-import { fetchUserWsKey } from "@app/hooks/api/keys/queries";
import { CreateOrgModal } from "@app/views/Org/components";
interface LayoutProps {
@@ -129,8 +125,8 @@ export const AppLayout = ({ children }: LayoutProps) => {
: true;
const createWs = useCreateWorkspace();
- const uploadWsKey = useUploadWsKey();
- const addWsUser = useAddUserToWsE2EE();
+ const addUsersToProject = useAddUserToWsNonE2EE();
+
const infisicalPlatformVersion = process.env.NEXT_PUBLIC_INFISICAL_PLATFORM_VERSION;
const { popUp, handlePopUpOpen, handlePopUpClose, handlePopUpToggle } = usePopUp([
@@ -220,53 +216,32 @@ export const AppLayout = ({ children }: LayoutProps) => {
const onCreateProject = async ({ name, addMembers }: TAddProjectFormData) => {
// type check
- if (!currentOrg?.id) return;
+ if (!currentOrg) return;
+ if (!user) return;
try {
const {
data: {
- project: { id: newWorkspaceId }
+ project: { id: newProjectId }
}
} = await createWs.mutateAsync({
- organizationId: currentOrg?.id,
+ organizationId: currentOrg.id,
projectName: name
});
- const randomBytes = crypto.randomBytes(16).toString("hex");
- const PRIVATE_KEY = String(localStorage.getItem("PRIVATE_KEY"));
- const { ciphertext, nonce } = encryptAssymmetric({
- plaintext: randomBytes,
- publicKey: user.publicKey,
- privateKey: PRIVATE_KEY
- });
-
- await uploadWsKey.mutateAsync({
- encryptedKey: ciphertext,
- nonce,
- userId: user?.id,
- workspaceId: newWorkspaceId
- });
-
if (addMembers) {
- // not using hooks because need at this point only
const orgUsers = await fetchOrgUsers(currentOrg.id);
- const decryptKey = await fetchUserWsKey(newWorkspaceId);
- await addWsUser.mutateAsync({
- workspaceId: newWorkspaceId,
- decryptKey,
- userPrivateKey: PRIVATE_KEY,
- members: orgUsers
- .filter(
- ({ status, user: orgUser }) => status === "accepted" && user.email !== orgUser.email
- )
- .map(({ user: orgUser, id: orgMembershipId }) => ({
- userPublicKey: orgUser.publicKey,
- orgMembershipId
- }))
+
+ await addUsersToProject.mutateAsync({
+ emails: orgUsers
+ .map((member) => member.user.email)
+ .filter((email) => email !== user.email),
+ projectId: newProjectId
});
}
+
createNotification({ text: "Workspace created", type: "success" });
handlePopUpClose("addNewWs");
- router.push(`/project/${newWorkspaceId}/secrets/overview`);
+ router.push(`/project/${newProjectId}/secrets/overview`);
} catch (err) {
console.error(err);
createNotification({ text: "Failed to create workspace", type: "error" });
diff --git a/frontend/src/views/Project/MembersPage/components/MemberListTab/MemberListTab.tsx b/frontend/src/views/Project/MembersPage/components/MemberListTab/MemberListTab.tsx
index 838ccce19..222504a91 100644
--- a/frontend/src/views/Project/MembersPage/components/MemberListTab/MemberListTab.tsx
+++ b/frontend/src/views/Project/MembersPage/components/MemberListTab/MemberListTab.tsx
@@ -117,18 +117,23 @@ export const MemberListTab = () => {
if (!orgUser) return;
try {
- if (currentWorkspace.e2ee) {
+ if (currentWorkspace.version === "v1") {
await addUserToWorkspace({
workspaceId,
userPrivateKey,
decryptKey: wsKey,
members: [{ orgMembershipId, userPublicKey: orgUser.user.publicKey }]
});
- } else {
+ } else if (currentWorkspace.version === "v2") {
await addUserToWorkspaceNonE2EE({
projectId: workspaceId,
emails: [orgUser.user.email]
});
+ } else {
+ createNotification({
+ text: "Failed to add user to project, unknown project type",
+ type: "error"
+ });
}
createNotification({
text: "Successfully added user to the project",
diff --git a/frontend/src/views/SecretOverviewPage/SecretOverviewPage.tsx b/frontend/src/views/SecretOverviewPage/SecretOverviewPage.tsx
index 394914a00..b7af0e3a4 100644
--- a/frontend/src/views/SecretOverviewPage/SecretOverviewPage.tsx
+++ b/frontend/src/views/SecretOverviewPage/SecretOverviewPage.tsx
@@ -1,4 +1,4 @@
-import { useEffect, useRef, useState } from "react";
+import { useCallback, useEffect, useRef, useState } from "react";
import { useTranslation } from "react-i18next";
import Link from "next/link";
import { useRouter } from "next/router";
@@ -14,6 +14,8 @@ import { useNotificationContext } from "@app/components/context/Notifications/No
import NavHeader from "@app/components/navigation/NavHeader";
import { PermissionDeniedBanner } from "@app/components/permissions";
import {
+ Alert,
+ AlertDescription,
Button,
EmptyState,
IconButton,
@@ -37,7 +39,8 @@ import {
useGetFoldersByEnv,
useGetProjectSecretsAllEnv,
useGetUserWsKey,
- useUpdateSecretV3
+ useUpdateSecretV3,
+ useUpgradeProject
} from "@app/hooks/api";
import { FolderBreadCrumbs } from "./components/FolderBreadCrumbs";
@@ -104,6 +107,7 @@ export const SecretOverviewPage = () => {
environments: userAvailableEnvs.map(({ slug }) => slug)
});
+ const upgradeProject = useUpgradeProject();
const { mutateAsync: createSecretV3 } = useCreateSecretV3();
const { mutateAsync: updateSecretV3 } = useUpdateSecretV3();
const { mutateAsync: deleteSecretV3 } = useDeleteSecretV3();
@@ -197,6 +201,24 @@ 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 handleFolderClick = (path: string) => {
@@ -315,6 +337,23 @@ export const SecretOverviewPage = () => {
.
+
+ {currentWorkspace?.version === "v1" && (
+
+
+
+ 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{" "}
+
+
+
+ Upgrade
+
+
+
+
+ )}