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{" "} + +
+ +
+
+
+ )}