mirror of
https://github.com/awatertrevi/infisical.git
synced 2026-10-05 14:26:38 +00:00
More gohst user
This commit is contained in:
@@ -58,6 +58,7 @@ export const auditLogServiceFactory = ({
|
|||||||
if (data.event.type !== EventType.LOGIN_IDENTITY_UNIVERSAL_AUTH) {
|
if (data.event.type !== EventType.LOGIN_IDENTITY_UNIVERSAL_AUTH) {
|
||||||
if (!data.projectId && !data.orgId) throw new BadRequestError({ message: "Must either project id or org id" });
|
if (!data.projectId && !data.orgId) throw new BadRequestError({ message: "Must either project id or org id" });
|
||||||
}
|
}
|
||||||
|
|
||||||
return auditLogQueue.pushToLog(data);
|
return auditLogQueue.pushToLog(data);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -9,11 +9,11 @@ export const getDefaultOnPremFeatures = (): TFeatureSet => ({
|
|||||||
_id: null,
|
_id: null,
|
||||||
slug: null,
|
slug: null,
|
||||||
tier: -1,
|
tier: -1,
|
||||||
workspaceLimit: null,
|
workspaceLimit: 5000,
|
||||||
workspacesUsed: 0,
|
workspacesUsed: 0,
|
||||||
memberLimit: null,
|
memberLimit: 5000,
|
||||||
membersUsed: 0,
|
membersUsed: 0,
|
||||||
environmentLimit: null,
|
environmentLimit: 5000,
|
||||||
environmentsUsed: 0,
|
environmentsUsed: 0,
|
||||||
secretVersioning: true,
|
secretVersioning: true,
|
||||||
pitRecovery: false,
|
pitRecovery: false,
|
||||||
@@ -28,7 +28,7 @@ export const getDefaultOnPremFeatures = (): TFeatureSet => ({
|
|||||||
status: null,
|
status: null,
|
||||||
trial_end: null,
|
trial_end: null,
|
||||||
has_used_trial: true,
|
has_used_trial: true,
|
||||||
secretApproval: false,
|
secretApproval: true,
|
||||||
secretRotation: true
|
secretRotation: true
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
import crypto from "crypto";
|
||||||
|
|
||||||
import { ProjectMembershipRole, TProjectKeys } from "@app/db/schemas";
|
import { ProjectMembershipRole, TProjectKeys } from "@app/db/schemas";
|
||||||
|
|
||||||
import { decryptAsymmetric, encryptAsymmetric } from "../crypto";
|
import { decryptAsymmetric, encryptAsymmetric } from "../crypto";
|
||||||
@@ -37,3 +39,22 @@ export const createWsMembers = ({ members, decryptKey, userPrivateKey }: AddUser
|
|||||||
|
|
||||||
return newWsMembers;
|
return newWsMembers;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
type TCreateWorkspaceKeyDTO = {
|
||||||
|
publicKey: string;
|
||||||
|
privateKey: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const createWorkspaceKey = ({ publicKey, privateKey }: TCreateWorkspaceKeyDTO) => {
|
||||||
|
// 3. Create a random key that we'll use as the project key.
|
||||||
|
const randomBytes = crypto.randomBytes(16).toString("hex");
|
||||||
|
|
||||||
|
// 4. Encrypt the project key with the users key pair.
|
||||||
|
const { ciphertext: encryptedProjectKey, nonce: encryptedProjectKeyIv } = encryptAsymmetric(
|
||||||
|
randomBytes,
|
||||||
|
publicKey,
|
||||||
|
privateKey
|
||||||
|
);
|
||||||
|
|
||||||
|
return { key: encryptedProjectKey, iv: encryptedProjectKeyIv };
|
||||||
|
};
|
||||||
|
|||||||
@@ -286,6 +286,7 @@ export const registerRoutes = async (
|
|||||||
projectMembershipDAL,
|
projectMembershipDAL,
|
||||||
projectDAL,
|
projectDAL,
|
||||||
permissionService,
|
permissionService,
|
||||||
|
projectBotDAL,
|
||||||
orgDAL,
|
orgDAL,
|
||||||
userDAL,
|
userDAL,
|
||||||
smtpService,
|
smtpService,
|
||||||
@@ -302,6 +303,11 @@ export const registerRoutes = async (
|
|||||||
permissionService,
|
permissionService,
|
||||||
projectDAL,
|
projectDAL,
|
||||||
secretBlindIndexDAL,
|
secretBlindIndexDAL,
|
||||||
|
identityProjectDAL,
|
||||||
|
identityOrgMembershipDAL,
|
||||||
|
projectBotDAL,
|
||||||
|
projectKeyDAL,
|
||||||
|
userDAL,
|
||||||
projectEnvDAL,
|
projectEnvDAL,
|
||||||
orgService,
|
orgService,
|
||||||
projectMembershipDAL,
|
projectMembershipDAL,
|
||||||
|
|||||||
@@ -249,6 +249,7 @@ export const registerProjectRouter = async (server: FastifyZodProvider) => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Is this actually used..?
|
||||||
server.route({
|
server.route({
|
||||||
url: "/:workspaceId/invite-signup",
|
url: "/:workspaceId/invite-signup",
|
||||||
method: "POST",
|
method: "POST",
|
||||||
|
|||||||
@@ -1,15 +1,8 @@
|
|||||||
import { ForbiddenError } from "@casl/ability";
|
|
||||||
import crypto from "crypto";
|
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
|
|
||||||
import { ProjectMembershipRole, ProjectsSchema } from "@app/db/schemas";
|
import { ProjectMembershipsSchema, ProjectsSchema } from "@app/db/schemas";
|
||||||
import { OrgPermissionActions, OrgPermissionSubjects } from "@app/ee/services/permission/org-permission";
|
import { EventType } from "@app/ee/services/audit-log/audit-log-types";
|
||||||
import { ProjectPermissionActions, ProjectPermissionSub } from "@app/ee/services/permission/project-permission";
|
|
||||||
import { encryptAsymmetric } from "@app/lib/crypto";
|
|
||||||
import { BadRequestError } from "@app/lib/errors";
|
|
||||||
import { createWsMembers } from "@app/lib/project";
|
|
||||||
import { authRateLimit } from "@app/server/config/rateLimiter";
|
import { authRateLimit } from "@app/server/config/rateLimiter";
|
||||||
import { ActorType } from "@app/services/auth/auth-type";
|
|
||||||
|
|
||||||
const projectWithEnv = ProjectsSchema.merge(
|
const projectWithEnv = ProjectsSchema.merge(
|
||||||
z.object({
|
z.object({
|
||||||
@@ -29,7 +22,6 @@ export const registerProjectRouter = async (server: FastifyZodProvider) => {
|
|||||||
schema: {
|
schema: {
|
||||||
body: z.object({
|
body: z.object({
|
||||||
projectName: z.string().trim(),
|
projectName: z.string().trim(),
|
||||||
inviteMemberEmails: z.array(z.string().email()).optional(),
|
|
||||||
organizationId: z.string().trim()
|
organizationId: z.string().trim()
|
||||||
}),
|
}),
|
||||||
response: {
|
response: {
|
||||||
@@ -39,110 +31,13 @@ export const registerProjectRouter = async (server: FastifyZodProvider) => {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
handler: async (req) => {
|
handler: async (req) => {
|
||||||
const { permission } = await server.services.permission.getOrgPermission(
|
const project = await server.services.project.createProject({
|
||||||
req.permission.type,
|
|
||||||
req.permission.id,
|
|
||||||
req.body.organizationId
|
|
||||||
);
|
|
||||||
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Create, OrgPermissionSubjects.Workspace);
|
|
||||||
|
|
||||||
// 2. Create a new project (will set the e2ee db field to false).
|
|
||||||
const { project, ghostUser } = await server.services.project.createProject({
|
|
||||||
actorId: req.permission.id,
|
actorId: req.permission.id,
|
||||||
actor: req.permission.type,
|
actor: req.permission.type,
|
||||||
orgId: req.body.organizationId,
|
orgId: req.body.organizationId,
|
||||||
workspaceName: req.body.projectName
|
workspaceName: req.body.projectName
|
||||||
});
|
});
|
||||||
|
|
||||||
// 3. Create a random key that we'll use as the project key.
|
|
||||||
const randomBytes = crypto.randomBytes(16).toString("hex");
|
|
||||||
|
|
||||||
// 4. Encrypt the project key with the users key pair.
|
|
||||||
const { ciphertext: encryptedProjectKey, nonce: encryptedProjectKeyIv } = encryptAsymmetric(
|
|
||||||
randomBytes,
|
|
||||||
ghostUser.keys.publicKey,
|
|
||||||
ghostUser.keys.plainPrivateKey
|
|
||||||
);
|
|
||||||
|
|
||||||
// 4. Save the project key for the ghost user.
|
|
||||||
await server.services.projectKey.uploadProjectKeys({
|
|
||||||
projectId: project.id,
|
|
||||||
actor: req.permission.type,
|
|
||||||
actorId: ghostUser.user.id,
|
|
||||||
nonce: encryptedProjectKeyIv,
|
|
||||||
receiverId: ghostUser.user.id,
|
|
||||||
encryptedKey: encryptedProjectKey
|
|
||||||
});
|
|
||||||
|
|
||||||
// 5. Create a bot for the project.
|
|
||||||
const bot = await server.services.projectBot.findBotByProjectId({
|
|
||||||
actorId: ghostUser.user.id,
|
|
||||||
actor: req.permission.type,
|
|
||||||
projectId: project.id,
|
|
||||||
|
|
||||||
// We set the publicKey and privateKey of the bot to the same as the ghost user.
|
|
||||||
// We do this because we'll need to access the private key again later, when adding new members to the project.
|
|
||||||
publicKey: ghostUser.keys.publicKey,
|
|
||||||
privateKey: ghostUser.keys.plainPrivateKey
|
|
||||||
});
|
|
||||||
|
|
||||||
// 6. Activate the bot.
|
|
||||||
await server.services.projectBot.setBotActiveState({
|
|
||||||
botKey: {
|
|
||||||
encryptedKey: encryptedProjectKey,
|
|
||||||
nonce: encryptedProjectKeyIv
|
|
||||||
},
|
|
||||||
actorId: ghostUser.user.id,
|
|
||||||
isActive: true,
|
|
||||||
actor: req.permission.type,
|
|
||||||
botId: bot.id
|
|
||||||
});
|
|
||||||
|
|
||||||
// 7. get the current user & org membership
|
|
||||||
const user = await server.services.user.getMe(req.permission.id);
|
|
||||||
const userOrgMembership = await server.services.permission.getUserOrgPermission(user.id, req.body.organizationId);
|
|
||||||
|
|
||||||
// 7. Get the latest key from the ghost!
|
|
||||||
const latestKey = await server.services.projectKey.getLatestProjectKey({
|
|
||||||
actorId: ghostUser.user.id,
|
|
||||||
actor: req.permission.type,
|
|
||||||
projectId: project.id
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!latestKey) throw new Error("Failed to get latest key");
|
|
||||||
|
|
||||||
// If the project is being created by a user, add the user to the project as an admin
|
|
||||||
if (req.permission.type === ActorType.USER) {
|
|
||||||
const projectAdmin = createWsMembers({
|
|
||||||
decryptKey: latestKey,
|
|
||||||
members: [
|
|
||||||
{
|
|
||||||
userPublicKey: user.publicKey,
|
|
||||||
orgMembershipId: userOrgMembership.membership.id,
|
|
||||||
projectMembershipRole: ProjectMembershipRole.Admin // <-- Make the first user an admin
|
|
||||||
}
|
|
||||||
],
|
|
||||||
userPrivateKey: ghostUser.keys.plainPrivateKey
|
|
||||||
});
|
|
||||||
|
|
||||||
await server.services.projectMembership.addUsersToProject({
|
|
||||||
projectId: project.id,
|
|
||||||
actorId: ghostUser.user.id,
|
|
||||||
actor: req.permission.type,
|
|
||||||
members: projectAdmin
|
|
||||||
});
|
|
||||||
}
|
|
||||||
// If the project is being created by an identity, add the identity to the project as an admin
|
|
||||||
else if (req.permission.type === ActorType.IDENTITY) {
|
|
||||||
await server.services.identityProject.createProjectIdentity({
|
|
||||||
actor: ActorType.IDENTITY,
|
|
||||||
actorId: ghostUser.user.id,
|
|
||||||
identityId: req.permission.id,
|
|
||||||
projectId: project.id,
|
|
||||||
role: ProjectMembershipRole.Admin
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
return { project };
|
return { project };
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -159,82 +54,35 @@ export const registerProjectRouter = async (server: FastifyZodProvider) => {
|
|||||||
}),
|
}),
|
||||||
body: z.object({
|
body: z.object({
|
||||||
emails: z.string().email().array()
|
emails: z.string().email().array()
|
||||||
})
|
}),
|
||||||
|
response: {
|
||||||
|
200: z.object({
|
||||||
|
memberships: ProjectMembershipsSchema.array()
|
||||||
|
})
|
||||||
|
}
|
||||||
},
|
},
|
||||||
handler: async (req) => {
|
handler: async (req) => {
|
||||||
const { permission } = await server.services.permission.getProjectPermission(
|
const memberships = await server.services.projectMembership.addUsersToProjectNonE2EE({
|
||||||
req.permission.type,
|
projectId: req.params.projectId,
|
||||||
req.permission.id,
|
|
||||||
req.params.projectId
|
|
||||||
);
|
|
||||||
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Create, ProjectPermissionSub.Member);
|
|
||||||
|
|
||||||
const project = await server.services.project.getAProject({
|
|
||||||
actorId: req.permission.id,
|
actorId: req.permission.id,
|
||||||
actor: req.permission.type,
|
actor: req.permission.type,
|
||||||
projectId: req.params.projectId
|
|
||||||
});
|
|
||||||
|
|
||||||
const ghostUser = await server.services.project.findProjectGhostUser(req.params.projectId);
|
|
||||||
|
|
||||||
if (!ghostUser) {
|
|
||||||
throw new BadRequestError({
|
|
||||||
message: "Failed" // TODO: Add a message
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
const latestKey = await server.services.projectKey.getLatestProjectKey({
|
|
||||||
actorId: ghostUser.id,
|
|
||||||
actor: ActorType.USER,
|
|
||||||
projectId: req.params.projectId
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!latestKey) {
|
|
||||||
throw new BadRequestError({
|
|
||||||
message: "Failed to find project key"
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
const bot = await server.services.projectBot.findBotByProjectId({
|
|
||||||
actor: req.permission.type,
|
|
||||||
actorId: req.permission.id,
|
|
||||||
projectId: req.params.projectId
|
|
||||||
});
|
|
||||||
|
|
||||||
// We get the bot private key, because the bot private key is the same as the ghost user's private key.
|
|
||||||
const botPrivateKey = server.services.projectBot.getBotPrivateKey({ bot });
|
|
||||||
|
|
||||||
const members = await server.services.org.findOrgMembersByEmail({
|
|
||||||
actor: req.permission.type,
|
|
||||||
actorId: req.permission.id,
|
|
||||||
orgId: project.orgId,
|
|
||||||
emails: req.body.emails
|
emails: req.body.emails
|
||||||
});
|
});
|
||||||
|
|
||||||
if (members.length !== req.body.emails.length) {
|
await server.services.auditLog.createAuditLog({
|
||||||
throw new BadRequestError({
|
|
||||||
message: "Some users are not part of the organization"
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
const wsMembers = createWsMembers({
|
|
||||||
members: members.map((membership) => ({
|
|
||||||
orgMembershipId: membership.id,
|
|
||||||
projectMembershipRole: ProjectMembershipRole.Member,
|
|
||||||
userPublicKey: membership.user.publicKey
|
|
||||||
})),
|
|
||||||
decryptKey: latestKey,
|
|
||||||
userPrivateKey: botPrivateKey
|
|
||||||
});
|
|
||||||
|
|
||||||
await server.services.projectMembership.addUsersToProject({
|
|
||||||
projectId: req.params.projectId,
|
projectId: req.params.projectId,
|
||||||
actorId: ghostUser.id, // We set the actor ID to the ghost user, because this is used as senderId in the project key sharing
|
...req.auditLogInfo,
|
||||||
actor: ActorType.USER,
|
event: {
|
||||||
members: wsMembers
|
type: EventType.ADD_BATCH_WORKSPACE_MEMBER,
|
||||||
|
metadata: memberships.map(({ userId, id }) => ({
|
||||||
|
userId: userId || "",
|
||||||
|
membershipId: id,
|
||||||
|
email: ""
|
||||||
|
}))
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
return {};
|
return { memberships };
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,17 +1,11 @@
|
|||||||
import { ForbiddenError } from "@casl/ability";
|
import { ForbiddenError } from "@casl/ability";
|
||||||
import { Knex } from "knex";
|
import { Knex } from "knex";
|
||||||
|
|
||||||
import { SecretEncryptionAlgo, SecretKeyEncoding } from "@app/db/schemas";
|
import { SecretKeyEncoding } from "@app/db/schemas";
|
||||||
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service";
|
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service";
|
||||||
import { ProjectPermissionActions, ProjectPermissionSub } from "@app/ee/services/permission/project-permission";
|
import { ProjectPermissionActions, ProjectPermissionSub } from "@app/ee/services/permission/project-permission";
|
||||||
import { getConfig } from "@app/lib/config/env";
|
import { decryptAsymmetric, generateAsymmetricKeyPair } from "@app/lib/crypto";
|
||||||
import {
|
import { infisicalSymmetricDecrypt, infisicalSymmetricEncypt } from "@app/lib/crypto/encryption";
|
||||||
decryptAsymmetric,
|
|
||||||
encryptSymmetric,
|
|
||||||
encryptSymmetric128BitHexKeyUTF8,
|
|
||||||
generateAsymmetricKeyPair
|
|
||||||
} from "@app/lib/crypto";
|
|
||||||
import { infisicalSymmetricDecrypt } from "@app/lib/crypto/encryption";
|
|
||||||
import { BadRequestError } from "@app/lib/errors";
|
import { BadRequestError } from "@app/lib/errors";
|
||||||
|
|
||||||
import { TProjectBotDALFactory } from "./project-bot-dal";
|
import { TProjectBotDALFactory } from "./project-bot-dal";
|
||||||
@@ -60,7 +54,6 @@ export const projectBotServiceFactory = ({ projectBotDAL, permissionService }: T
|
|||||||
}: TFindBotByProjectIdDTO) => {
|
}: TFindBotByProjectIdDTO) => {
|
||||||
const { permission } = await permissionService.getProjectPermission(actor, actorId, projectId, actorOrgId);
|
const { permission } = await permissionService.getProjectPermission(actor, actorId, projectId, actorOrgId);
|
||||||
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.Integrations);
|
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.Integrations);
|
||||||
const appCfg = getConfig();
|
|
||||||
|
|
||||||
const bot = await projectBotDAL.transaction(async (tx) => {
|
const bot = await projectBotDAL.transaction(async (tx) => {
|
||||||
const doc = await projectBotDAL.findOne({ projectId }, tx);
|
const doc = await projectBotDAL.findOne({ projectId }, tx);
|
||||||
@@ -68,41 +61,22 @@ export const projectBotServiceFactory = ({ projectBotDAL, permissionService }: T
|
|||||||
|
|
||||||
const keys = privateKey && publicKey ? { privateKey, publicKey } : generateAsymmetricKeyPair();
|
const keys = privateKey && publicKey ? { privateKey, publicKey } : generateAsymmetricKeyPair();
|
||||||
|
|
||||||
if (appCfg.ROOT_ENCRYPTION_KEY) {
|
const { iv, tag, ciphertext, encoding, algorithm } = infisicalSymmetricEncypt(keys.privateKey);
|
||||||
const { iv, tag, ciphertext } = encryptSymmetric(keys.privateKey, appCfg.ROOT_ENCRYPTION_KEY);
|
|
||||||
return projectBotDAL.create(
|
return projectBotDAL.create(
|
||||||
{
|
{
|
||||||
name: "Infisical Bot",
|
name: "Infisical Bot",
|
||||||
projectId,
|
projectId,
|
||||||
tag,
|
tag,
|
||||||
iv,
|
iv,
|
||||||
encryptedPrivateKey: ciphertext,
|
encryptedPrivateKey: ciphertext,
|
||||||
isActive: false,
|
isActive: false,
|
||||||
publicKey: keys.publicKey,
|
publicKey: keys.publicKey,
|
||||||
algorithm: SecretEncryptionAlgo.AES_256_GCM,
|
algorithm,
|
||||||
keyEncoding: SecretKeyEncoding.BASE64
|
keyEncoding: encoding
|
||||||
},
|
},
|
||||||
tx
|
tx
|
||||||
);
|
);
|
||||||
}
|
|
||||||
if (appCfg.ENCRYPTION_KEY) {
|
|
||||||
const { iv, tag, ciphertext } = encryptSymmetric128BitHexKeyUTF8(keys.privateKey, appCfg.ENCRYPTION_KEY);
|
|
||||||
return projectBotDAL.create(
|
|
||||||
{
|
|
||||||
name: "Infisical Bot",
|
|
||||||
projectId,
|
|
||||||
tag,
|
|
||||||
iv,
|
|
||||||
encryptedPrivateKey: ciphertext,
|
|
||||||
isActive: false,
|
|
||||||
publicKey: keys.publicKey,
|
|
||||||
algorithm: SecretEncryptionAlgo.AES_256_GCM,
|
|
||||||
keyEncoding: SecretKeyEncoding.UTF8
|
|
||||||
},
|
|
||||||
tx
|
|
||||||
);
|
|
||||||
}
|
|
||||||
throw new BadRequestError({ message: "Failed to create bot due to missing encryption key" });
|
|
||||||
});
|
});
|
||||||
return bot;
|
return bot;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
import { Knex } from "knex";
|
||||||
|
|
||||||
import { TDbClient } from "@app/db";
|
import { TDbClient } from "@app/db";
|
||||||
import { TableName, TProjectKeys } from "@app/db/schemas";
|
import { TableName, TProjectKeys } from "@app/db/schemas";
|
||||||
import { DatabaseError } from "@app/lib/errors";
|
import { DatabaseError } from "@app/lib/errors";
|
||||||
@@ -10,10 +12,11 @@ export const projectKeyDALFactory = (db: TDbClient) => {
|
|||||||
|
|
||||||
const findLatestProjectKey = async (
|
const findLatestProjectKey = async (
|
||||||
userId: string,
|
userId: string,
|
||||||
projectId: string
|
projectId: string,
|
||||||
|
tx?: Knex
|
||||||
): Promise<(TProjectKeys & { sender: { publicKey: string } }) | undefined> => {
|
): Promise<(TProjectKeys & { sender: { publicKey: string } }) | undefined> => {
|
||||||
try {
|
try {
|
||||||
const projectKey = await db(TableName.ProjectKeys)
|
const projectKey = await (tx || db)(TableName.ProjectKeys)
|
||||||
.join(TableName.Users, `${TableName.ProjectKeys}.senderId`, `${TableName.Users}.id`)
|
.join(TableName.Users, `${TableName.ProjectKeys}.senderId`, `${TableName.Users}.id`)
|
||||||
.join(TableName.UserEncryptionKey, `${TableName.UserEncryptionKey}.userId`, `${TableName.Users}.id`)
|
.join(TableName.UserEncryptionKey, `${TableName.UserEncryptionKey}.userId`, `${TableName.Users}.id`)
|
||||||
.where({ projectId, receiverId: userId })
|
.where({ projectId, receiverId: userId })
|
||||||
@@ -29,9 +32,9 @@ export const projectKeyDALFactory = (db: TDbClient) => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const findAllProjectUserPubKeys = async (projectId: string) => {
|
const findAllProjectUserPubKeys = async (projectId: string, tx?: Knex) => {
|
||||||
try {
|
try {
|
||||||
const pubKeys = await db(TableName.ProjectMembership)
|
const pubKeys = await (tx || db)(TableName.ProjectMembership)
|
||||||
.where({ projectId })
|
.where({ projectId })
|
||||||
.join(TableName.Users, `${TableName.ProjectMembership}.userId`, `${TableName.Users}.id`)
|
.join(TableName.Users, `${TableName.ProjectMembership}.userId`, `${TableName.Users}.id`)
|
||||||
.join(TableName.UserEncryptionKey, `${TableName.Users}.id`, `${TableName.UserEncryptionKey}.userId`)
|
.join(TableName.UserEncryptionKey, `${TableName.Users}.id`, `${TableName.UserEncryptionKey}.userId`)
|
||||||
|
|||||||
@@ -1,16 +1,26 @@
|
|||||||
/* eslint-disable no-await-in-loop */
|
/* eslint-disable no-await-in-loop */
|
||||||
import { ForbiddenError } from "@casl/ability";
|
import { ForbiddenError } from "@casl/ability";
|
||||||
|
|
||||||
import { OrgMembershipStatus, ProjectMembershipRole, TableName, TUsers } from "@app/db/schemas";
|
import {
|
||||||
|
OrgMembershipStatus,
|
||||||
|
ProjectMembershipRole,
|
||||||
|
SecretKeyEncoding,
|
||||||
|
TableName,
|
||||||
|
TProjectMemberships,
|
||||||
|
TUsers
|
||||||
|
} from "@app/db/schemas";
|
||||||
import { TLicenseServiceFactory } from "@app/ee/services/license/license-service";
|
import { TLicenseServiceFactory } from "@app/ee/services/license/license-service";
|
||||||
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service";
|
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service";
|
||||||
import { ProjectPermissionActions, ProjectPermissionSub } from "@app/ee/services/permission/project-permission";
|
import { ProjectPermissionActions, ProjectPermissionSub } from "@app/ee/services/permission/project-permission";
|
||||||
import { getConfig } from "@app/lib/config/env";
|
import { getConfig } from "@app/lib/config/env";
|
||||||
|
import { infisicalSymmetricDecrypt } from "@app/lib/crypto/encryption";
|
||||||
import { BadRequestError } from "@app/lib/errors";
|
import { BadRequestError } from "@app/lib/errors";
|
||||||
import { groupBy } from "@app/lib/fn";
|
import { groupBy } from "@app/lib/fn";
|
||||||
|
import { createWsMembers } from "@app/lib/project";
|
||||||
|
|
||||||
import { TOrgDALFactory } from "../org/org-dal";
|
import { TOrgDALFactory } from "../org/org-dal";
|
||||||
import { TProjectDALFactory } from "../project/project-dal";
|
import { TProjectDALFactory } from "../project/project-dal";
|
||||||
|
import { TProjectBotDALFactory } from "../project-bot/project-bot-dal";
|
||||||
import { TProjectKeyDALFactory } from "../project-key/project-key-dal";
|
import { TProjectKeyDALFactory } from "../project-key/project-key-dal";
|
||||||
import { TProjectRoleDALFactory } from "../project-role/project-role-dal";
|
import { TProjectRoleDALFactory } from "../project-role/project-role-dal";
|
||||||
import { SmtpTemplates, TSmtpService } from "../smtp/smtp-service";
|
import { SmtpTemplates, TSmtpService } from "../smtp/smtp-service";
|
||||||
@@ -18,6 +28,7 @@ import { TUserDALFactory } from "../user/user-dal";
|
|||||||
import { TProjectMembershipDALFactory } from "./project-membership-dal";
|
import { TProjectMembershipDALFactory } from "./project-membership-dal";
|
||||||
import {
|
import {
|
||||||
TAddUsersToWorkspaceDTO,
|
TAddUsersToWorkspaceDTO,
|
||||||
|
TAddUsersToWorkspaceNonE2EEDTO,
|
||||||
TDeleteProjectMembershipDTO,
|
TDeleteProjectMembershipDTO,
|
||||||
TGetProjectMembershipDTO,
|
TGetProjectMembershipDTO,
|
||||||
TInviteUserToProjectDTO,
|
TInviteUserToProjectDTO,
|
||||||
@@ -27,11 +38,12 @@ import {
|
|||||||
type TProjectMembershipServiceFactoryDep = {
|
type TProjectMembershipServiceFactoryDep = {
|
||||||
permissionService: Pick<TPermissionServiceFactory, "getProjectPermission">;
|
permissionService: Pick<TPermissionServiceFactory, "getProjectPermission">;
|
||||||
smtpService: TSmtpService;
|
smtpService: TSmtpService;
|
||||||
|
projectBotDAL: TProjectBotDALFactory;
|
||||||
projectMembershipDAL: TProjectMembershipDALFactory;
|
projectMembershipDAL: TProjectMembershipDALFactory;
|
||||||
userDAL: Pick<TUserDALFactory, "findById" | "findOne" | "findUserByProjectMembershipId">;
|
userDAL: Pick<TUserDALFactory, "findById" | "findOne" | "findUserByProjectMembershipId">;
|
||||||
projectRoleDAL: Pick<TProjectRoleDALFactory, "findOne">;
|
projectRoleDAL: Pick<TProjectRoleDALFactory, "findOne">;
|
||||||
orgDAL: Pick<TOrgDALFactory, "findMembership">;
|
orgDAL: Pick<TOrgDALFactory, "findMembership" | "findOrgMembersByEmail">;
|
||||||
projectDAL: Pick<TProjectDALFactory, "findById">;
|
projectDAL: Pick<TProjectDALFactory, "findById" | "findProjectGhostUser">;
|
||||||
projectKeyDAL: Pick<TProjectKeyDALFactory, "findLatestProjectKey" | "delete" | "insertMany">;
|
projectKeyDAL: Pick<TProjectKeyDALFactory, "findLatestProjectKey" | "delete" | "insertMany">;
|
||||||
licenseService: Pick<TLicenseServiceFactory, "getPlan">;
|
licenseService: Pick<TLicenseServiceFactory, "getPlan">;
|
||||||
};
|
};
|
||||||
@@ -43,6 +55,7 @@ export const projectMembershipServiceFactory = ({
|
|||||||
projectMembershipDAL,
|
projectMembershipDAL,
|
||||||
smtpService,
|
smtpService,
|
||||||
projectRoleDAL,
|
projectRoleDAL,
|
||||||
|
projectBotDAL,
|
||||||
orgDAL,
|
orgDAL,
|
||||||
userDAL,
|
userDAL,
|
||||||
projectDAL,
|
projectDAL,
|
||||||
@@ -98,15 +111,12 @@ export const projectMembershipServiceFactory = ({
|
|||||||
role: ProjectMembershipRole.Member
|
role: ProjectMembershipRole.Member
|
||||||
});
|
});
|
||||||
|
|
||||||
const sender = await userDAL.findById(actorId);
|
|
||||||
const appCfg = getConfig();
|
const appCfg = getConfig();
|
||||||
await smtpService.sendMail({
|
await smtpService.sendMail({
|
||||||
template: SmtpTemplates.WorkspaceInvite,
|
template: SmtpTemplates.WorkspaceInvite,
|
||||||
subjectLine: "Infisical workspace invitation",
|
subjectLine: "Infisical workspace invitation",
|
||||||
recipients: [invitee.email],
|
recipients: [invitee.email],
|
||||||
substitutions: {
|
substitutions: {
|
||||||
inviterFirstName: sender.firstName,
|
|
||||||
inviterEmail: sender.email,
|
|
||||||
workspaceName: project.name,
|
workspaceName: project.name,
|
||||||
callback_url: `${appCfg.SITE_URL}/login`
|
callback_url: `${appCfg.SITE_URL}/login`
|
||||||
}
|
}
|
||||||
@@ -175,15 +185,12 @@ export const projectMembershipServiceFactory = ({
|
|||||||
});
|
});
|
||||||
|
|
||||||
if (sendEmails) {
|
if (sendEmails) {
|
||||||
const sender = await userDAL.findById(actorId);
|
|
||||||
const appCfg = getConfig();
|
const appCfg = getConfig();
|
||||||
await smtpService.sendMail({
|
await smtpService.sendMail({
|
||||||
template: SmtpTemplates.WorkspaceInvite,
|
template: SmtpTemplates.WorkspaceInvite,
|
||||||
subjectLine: "Infisical workspace invitation",
|
subjectLine: "Infisical workspace invitation",
|
||||||
recipients: orgMembers.map(({ email }) => email).filter(Boolean),
|
recipients: orgMembers.map(({ email }) => email).filter(Boolean),
|
||||||
substitutions: {
|
substitutions: {
|
||||||
inviterFirstName: sender.firstName,
|
|
||||||
inviterEmail: sender.email,
|
|
||||||
workspaceName: project.name,
|
workspaceName: project.name,
|
||||||
callback_url: `${appCfg.SITE_URL}/login`
|
callback_url: `${appCfg.SITE_URL}/login`
|
||||||
}
|
}
|
||||||
@@ -192,6 +199,117 @@ export const projectMembershipServiceFactory = ({
|
|||||||
return orgMembers;
|
return orgMembers;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const addUsersToProjectNonE2EE = async ({
|
||||||
|
projectId,
|
||||||
|
actorId,
|
||||||
|
actor,
|
||||||
|
emails,
|
||||||
|
sendEmails = true
|
||||||
|
}: TAddUsersToWorkspaceNonE2EEDTO) => {
|
||||||
|
const project = await projectDAL.findById(projectId);
|
||||||
|
if (!project) throw new BadRequestError({ message: "Project not found" });
|
||||||
|
|
||||||
|
const { permission } = await permissionService.getProjectPermission(actor, actorId, projectId);
|
||||||
|
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Create, ProjectPermissionSub.Member);
|
||||||
|
|
||||||
|
const orgMembers = await orgDAL.findOrgMembersByEmail(project.orgId, emails);
|
||||||
|
|
||||||
|
if (orgMembers.length !== emails.length) throw new BadRequestError({ message: "Some users are not part of org" });
|
||||||
|
|
||||||
|
const existingMembers = await projectMembershipDAL.find({
|
||||||
|
projectId,
|
||||||
|
$in: { userId: orgMembers.map(({ user }) => user.id).filter(Boolean) }
|
||||||
|
});
|
||||||
|
if (existingMembers.length) throw new BadRequestError({ message: "Some users are already part of project" });
|
||||||
|
|
||||||
|
const ghostUser = await projectDAL.findProjectGhostUser(projectId);
|
||||||
|
|
||||||
|
if (!ghostUser) {
|
||||||
|
throw new BadRequestError({
|
||||||
|
message: "Failed to find ghost user"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const ghostUserLatestKey = await projectKeyDAL.findLatestProjectKey(ghostUser.id, projectId);
|
||||||
|
|
||||||
|
if (!ghostUserLatestKey) {
|
||||||
|
throw new BadRequestError({
|
||||||
|
message: "Failed to find ghost user latest key"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const bot = await projectBotDAL.findOne({ projectId });
|
||||||
|
|
||||||
|
if (!bot) {
|
||||||
|
throw new BadRequestError({
|
||||||
|
message: "Failed to find bot"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const botPrivateKey = infisicalSymmetricDecrypt({
|
||||||
|
keyEncoding: bot.keyEncoding as SecretKeyEncoding,
|
||||||
|
iv: bot.iv,
|
||||||
|
tag: bot.tag,
|
||||||
|
ciphertext: bot.encryptedPrivateKey
|
||||||
|
});
|
||||||
|
|
||||||
|
const newWsMembers = createWsMembers({
|
||||||
|
decryptKey: ghostUserLatestKey,
|
||||||
|
userPrivateKey: botPrivateKey,
|
||||||
|
members: orgMembers.map((membership) => ({
|
||||||
|
orgMembershipId: membership.id,
|
||||||
|
projectMembershipRole: ProjectMembershipRole.Member,
|
||||||
|
userPublicKey: membership.user.publicKey
|
||||||
|
}))
|
||||||
|
});
|
||||||
|
|
||||||
|
const members: TProjectMemberships[] = [];
|
||||||
|
|
||||||
|
await projectMembershipDAL.transaction(async (tx) => {
|
||||||
|
const result = await projectMembershipDAL.insertMany(
|
||||||
|
orgMembers.map(({ user, id: membershipId }) => {
|
||||||
|
const role =
|
||||||
|
orgMembers.find((membership) => membership.id === membershipId)?.role || ProjectMembershipRole.Member;
|
||||||
|
|
||||||
|
return {
|
||||||
|
projectId,
|
||||||
|
userId: user.id,
|
||||||
|
role
|
||||||
|
};
|
||||||
|
}),
|
||||||
|
tx
|
||||||
|
);
|
||||||
|
|
||||||
|
members.push(...result);
|
||||||
|
|
||||||
|
const encKeyGroupByOrgMembId = groupBy(newWsMembers, (i) => i.orgMembershipId);
|
||||||
|
await projectKeyDAL.insertMany(
|
||||||
|
orgMembers.map(({ user, id }) => ({
|
||||||
|
encryptedKey: encKeyGroupByOrgMembId[id][0].workspaceEncryptedKey,
|
||||||
|
nonce: encKeyGroupByOrgMembId[id][0].workspaceEncryptedNonce,
|
||||||
|
senderId: ghostUser.id,
|
||||||
|
receiverId: user.id,
|
||||||
|
projectId
|
||||||
|
})),
|
||||||
|
tx
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
if (sendEmails) {
|
||||||
|
const appCfg = getConfig();
|
||||||
|
await smtpService.sendMail({
|
||||||
|
template: SmtpTemplates.WorkspaceInvite,
|
||||||
|
subjectLine: "Infisical workspace invitation",
|
||||||
|
recipients: orgMembers.map(({ user }) => user.email).filter(Boolean),
|
||||||
|
substitutions: {
|
||||||
|
workspaceName: project.name,
|
||||||
|
callback_url: `${appCfg.SITE_URL}/login`
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return members;
|
||||||
|
};
|
||||||
|
|
||||||
const updateProjectMembership = async ({
|
const updateProjectMembership = async ({
|
||||||
actorId,
|
actorId,
|
||||||
actor,
|
actor,
|
||||||
@@ -268,6 +386,7 @@ export const projectMembershipServiceFactory = ({
|
|||||||
getProjectMemberships,
|
getProjectMemberships,
|
||||||
inviteUserToProject,
|
inviteUserToProject,
|
||||||
updateProjectMembership,
|
updateProjectMembership,
|
||||||
|
addUsersToProjectNonE2EE,
|
||||||
deleteProjectMembership,
|
deleteProjectMembership,
|
||||||
addUsersToProject
|
addUsersToProject
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -25,3 +25,8 @@ export type TAddUsersToWorkspaceDTO = {
|
|||||||
projectRole: ProjectMembershipRole;
|
projectRole: ProjectMembershipRole;
|
||||||
}[];
|
}[];
|
||||||
} & TProjectPermission;
|
} & TProjectPermission;
|
||||||
|
|
||||||
|
export type TAddUsersToWorkspaceNonE2EEDTO = {
|
||||||
|
sendEmails?: boolean;
|
||||||
|
emails: string[];
|
||||||
|
} & TProjectPermission;
|
||||||
|
|||||||
@@ -52,6 +52,20 @@ export const projectDALFactory = (db: TDbClient) => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const findProjectGhostUser = async (projectId: string) => {
|
||||||
|
try {
|
||||||
|
const ghostUser = await db(TableName.ProjectMembership)
|
||||||
|
.where({ projectId })
|
||||||
|
.join(TableName.Users, `${TableName.ProjectMembership}.userId`, `${TableName.Users}.id`)
|
||||||
|
.select(selectAllTableCols(TableName.Users))
|
||||||
|
.where({ ghost: true })
|
||||||
|
.first();
|
||||||
|
return ghostUser;
|
||||||
|
} catch (error) {
|
||||||
|
throw new DatabaseError({ error, name: "Find project ghost user" });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const findAllProjectsByIdentity = async (identityId: string) => {
|
const findAllProjectsByIdentity = async (identityId: string) => {
|
||||||
try {
|
try {
|
||||||
const workspaces = await db(TableName.IdentityProjectMembership)
|
const workspaces = await db(TableName.IdentityProjectMembership)
|
||||||
@@ -136,6 +150,7 @@ export const projectDALFactory = (db: TDbClient) => {
|
|||||||
...projectOrm,
|
...projectOrm,
|
||||||
findAllProjects,
|
findAllProjects,
|
||||||
findAllProjectsByIdentity,
|
findAllProjectsByIdentity,
|
||||||
|
findProjectGhostUser,
|
||||||
findProjectById
|
findProjectById
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -6,16 +6,25 @@ import { TLicenseServiceFactory } from "@app/ee/services/license/license-service
|
|||||||
import { OrgPermissionActions, OrgPermissionSubjects } from "@app/ee/services/permission/org-permission";
|
import { OrgPermissionActions, OrgPermissionSubjects } from "@app/ee/services/permission/org-permission";
|
||||||
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service";
|
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service";
|
||||||
import { ProjectPermissionActions, ProjectPermissionSub } from "@app/ee/services/permission/project-permission";
|
import { ProjectPermissionActions, ProjectPermissionSub } from "@app/ee/services/permission/project-permission";
|
||||||
|
import { isAtLeastAsPrivileged } from "@app/lib/casl";
|
||||||
import { getConfig } from "@app/lib/config/env";
|
import { getConfig } from "@app/lib/config/env";
|
||||||
import { createSecretBlindIndex } from "@app/lib/crypto";
|
import { createSecretBlindIndex } from "@app/lib/crypto";
|
||||||
import { BadRequestError } from "@app/lib/errors";
|
import { infisicalSymmetricEncypt } from "@app/lib/crypto/encryption";
|
||||||
|
import { BadRequestError, ForbiddenRequestError } from "@app/lib/errors";
|
||||||
import { alphaNumericNanoId } from "@app/lib/nanoid";
|
import { alphaNumericNanoId } from "@app/lib/nanoid";
|
||||||
|
import { createWorkspaceKey, createWsMembers } from "@app/lib/project";
|
||||||
|
|
||||||
|
import { ActorType } from "../auth/auth-type";
|
||||||
|
import { TIdentityOrgDALFactory } from "../identity/identity-org-dal";
|
||||||
|
import { TIdentityProjectDALFactory } from "../identity-project/identity-project-dal";
|
||||||
import { TOrgServiceFactory } from "../org/org-service";
|
import { TOrgServiceFactory } from "../org/org-service";
|
||||||
|
import { TProjectBotDALFactory } from "../project-bot/project-bot-dal";
|
||||||
import { TProjectEnvDALFactory } from "../project-env/project-env-dal";
|
import { TProjectEnvDALFactory } from "../project-env/project-env-dal";
|
||||||
|
import { TProjectKeyDALFactory } from "../project-key/project-key-dal";
|
||||||
import { TProjectMembershipDALFactory } from "../project-membership/project-membership-dal";
|
import { TProjectMembershipDALFactory } from "../project-membership/project-membership-dal";
|
||||||
import { TSecretBlindIndexDALFactory } from "../secret-blind-index/secret-blind-index-dal";
|
import { TSecretBlindIndexDALFactory } from "../secret-blind-index/secret-blind-index-dal";
|
||||||
import { ROOT_FOLDER_NAME, TSecretFolderDALFactory } from "../secret-folder/secret-folder-dal";
|
import { ROOT_FOLDER_NAME, TSecretFolderDALFactory } from "../secret-folder/secret-folder-dal";
|
||||||
|
import { TUserDALFactory } from "../user/user-dal";
|
||||||
import { TProjectDALFactory } from "./project-dal";
|
import { TProjectDALFactory } from "./project-dal";
|
||||||
import { TCreateProjectDTO, TDeleteProjectDTO, TGetProjectDTO } from "./project-types";
|
import { TCreateProjectDTO, TDeleteProjectDTO, TGetProjectDTO } from "./project-types";
|
||||||
|
|
||||||
@@ -27,8 +36,13 @@ export const DEFAULT_PROJECT_ENVS = [
|
|||||||
|
|
||||||
type TProjectServiceFactoryDep = {
|
type TProjectServiceFactoryDep = {
|
||||||
projectDAL: TProjectDALFactory;
|
projectDAL: TProjectDALFactory;
|
||||||
|
userDAL: TUserDALFactory;
|
||||||
folderDAL: Pick<TSecretFolderDALFactory, "insertMany">;
|
folderDAL: Pick<TSecretFolderDALFactory, "insertMany">;
|
||||||
projectEnvDAL: Pick<TProjectEnvDALFactory, "insertMany">;
|
projectEnvDAL: Pick<TProjectEnvDALFactory, "insertMany">;
|
||||||
|
identityOrgMembershipDAL: TIdentityOrgDALFactory;
|
||||||
|
identityProjectDAL: TIdentityProjectDALFactory;
|
||||||
|
projectKeyDAL: Pick<TProjectKeyDALFactory, "create" | "findLatestProjectKey">;
|
||||||
|
projectBotDAL: Pick<TProjectBotDALFactory, "create">;
|
||||||
projectMembershipDAL: Pick<TProjectMembershipDALFactory, "create" | "findProjectGhostUser">;
|
projectMembershipDAL: Pick<TProjectMembershipDALFactory, "create" | "findProjectGhostUser">;
|
||||||
orgService: Pick<TOrgServiceFactory, "addGhostUser">;
|
orgService: Pick<TOrgServiceFactory, "addGhostUser">;
|
||||||
secretBlindIndexDAL: Pick<TSecretBlindIndexDALFactory, "create">;
|
secretBlindIndexDAL: Pick<TSecretBlindIndexDALFactory, "create">;
|
||||||
@@ -40,9 +54,14 @@ export type TProjectServiceFactory = ReturnType<typeof projectServiceFactory>;
|
|||||||
|
|
||||||
export const projectServiceFactory = ({
|
export const projectServiceFactory = ({
|
||||||
projectDAL,
|
projectDAL,
|
||||||
|
projectKeyDAL,
|
||||||
permissionService,
|
permissionService,
|
||||||
|
userDAL,
|
||||||
folderDAL,
|
folderDAL,
|
||||||
orgService,
|
orgService,
|
||||||
|
identityProjectDAL,
|
||||||
|
projectBotDAL,
|
||||||
|
identityOrgMembershipDAL,
|
||||||
secretBlindIndexDAL,
|
secretBlindIndexDAL,
|
||||||
projectMembershipDAL,
|
projectMembershipDAL,
|
||||||
projectEnvDAL,
|
projectEnvDAL,
|
||||||
@@ -52,7 +71,12 @@ export const projectServiceFactory = ({
|
|||||||
* Create workspace. Make user the admin
|
* Create workspace. Make user the admin
|
||||||
* */
|
* */
|
||||||
const createProject = async ({ orgId, actor, actorId, actorOrgId, workspaceName }: TCreateProjectDTO) => {
|
const createProject = async ({ orgId, actor, actorId, actorOrgId, workspaceName }: TCreateProjectDTO) => {
|
||||||
const { permission } = await permissionService.getOrgPermission(actor, actorId, orgId, actorOrgId);
|
const { permission, membership: orgMembership } = await permissionService.getOrgPermission(
|
||||||
|
actor,
|
||||||
|
actorId,
|
||||||
|
orgId,
|
||||||
|
actorOrgId
|
||||||
|
);
|
||||||
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Create, OrgPermissionSubjects.Workspace);
|
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Create, OrgPermissionSubjects.Workspace);
|
||||||
|
|
||||||
const appCfg = getConfig();
|
const appCfg = getConfig();
|
||||||
@@ -110,14 +134,141 @@ export const projectServiceFactory = ({
|
|||||||
envs.map(({ id }) => ({ name: ROOT_FOLDER_NAME, envId: id, version: 1 })),
|
envs.map(({ id }) => ({ name: ROOT_FOLDER_NAME, envId: id, version: 1 })),
|
||||||
tx
|
tx
|
||||||
);
|
);
|
||||||
// _id for backward compat
|
|
||||||
return {
|
// 3. Create a random key that we'll use as the project key.
|
||||||
project: {
|
const { key: encryptedProjectKey, iv: encryptedProjectKeyIv } = createWorkspaceKey({
|
||||||
...project,
|
publicKey: ghostUser.keys.publicKey,
|
||||||
environments: envs,
|
privateKey: ghostUser.keys.plainPrivateKey
|
||||||
_id: project.id
|
});
|
||||||
|
|
||||||
|
// 4. Save the project key for the ghost user.
|
||||||
|
await projectKeyDAL.create(
|
||||||
|
{
|
||||||
|
projectId: project.id,
|
||||||
|
receiverId: ghostUser.user.id,
|
||||||
|
encryptedKey: encryptedProjectKey,
|
||||||
|
nonce: encryptedProjectKeyIv,
|
||||||
|
senderId: ghostUser.user.id
|
||||||
},
|
},
|
||||||
ghostUser
|
tx
|
||||||
|
);
|
||||||
|
|
||||||
|
const { iv, tag, ciphertext, encoding, algorithm } = infisicalSymmetricEncypt(ghostUser.keys.plainPrivateKey);
|
||||||
|
|
||||||
|
// 5. Create & a bot for the project
|
||||||
|
await projectBotDAL.create(
|
||||||
|
{
|
||||||
|
name: "Infisical Bot (Ghost)",
|
||||||
|
projectId: project.id,
|
||||||
|
tag,
|
||||||
|
iv,
|
||||||
|
encryptedPrivateKey: ciphertext,
|
||||||
|
isActive: true,
|
||||||
|
publicKey: ghostUser.keys.publicKey,
|
||||||
|
algorithm,
|
||||||
|
keyEncoding: encoding
|
||||||
|
},
|
||||||
|
tx
|
||||||
|
);
|
||||||
|
|
||||||
|
// Find the ghost users latest key
|
||||||
|
const latestKey = await projectKeyDAL.findLatestProjectKey(ghostUser.user.id, project.id, tx);
|
||||||
|
|
||||||
|
if (!latestKey) {
|
||||||
|
throw new Error("Latest key not found for user");
|
||||||
|
}
|
||||||
|
|
||||||
|
// If the project is being created by a user, add the user to the project as an admin
|
||||||
|
if (actor === ActorType.USER) {
|
||||||
|
// Find public key of user
|
||||||
|
const user = await userDAL.findUserEncKeyByUserId(actorId);
|
||||||
|
|
||||||
|
if (!user) {
|
||||||
|
throw new Error("User not found");
|
||||||
|
}
|
||||||
|
|
||||||
|
const [projectAdmin] = createWsMembers({
|
||||||
|
decryptKey: latestKey,
|
||||||
|
userPrivateKey: ghostUser.keys.plainPrivateKey,
|
||||||
|
members: [
|
||||||
|
{
|
||||||
|
userPublicKey: user.publicKey,
|
||||||
|
orgMembershipId: orgMembership.id,
|
||||||
|
projectMembershipRole: ProjectMembershipRole.Admin
|
||||||
|
}
|
||||||
|
]
|
||||||
|
});
|
||||||
|
|
||||||
|
// Create a membership for the user
|
||||||
|
await projectMembershipDAL.create(
|
||||||
|
{
|
||||||
|
projectId: project.id,
|
||||||
|
userId: user.id,
|
||||||
|
role: projectAdmin.projectRole
|
||||||
|
},
|
||||||
|
tx
|
||||||
|
);
|
||||||
|
|
||||||
|
// Create a project key for the user
|
||||||
|
await projectKeyDAL.create(
|
||||||
|
{
|
||||||
|
encryptedKey: projectAdmin.workspaceEncryptedKey,
|
||||||
|
nonce: projectAdmin.workspaceEncryptedNonce,
|
||||||
|
senderId: ghostUser.user.id,
|
||||||
|
receiverId: user.id,
|
||||||
|
projectId: project.id
|
||||||
|
},
|
||||||
|
tx
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// If the project is being created by an identity, add the identity to the project as an admin
|
||||||
|
else if (actor === ActorType.IDENTITY) {
|
||||||
|
// Find identity org membership
|
||||||
|
const identityOrgMembership = await identityOrgMembershipDAL.findOne(
|
||||||
|
{
|
||||||
|
identityId: actorId,
|
||||||
|
orgId: project.orgId
|
||||||
|
},
|
||||||
|
tx
|
||||||
|
);
|
||||||
|
|
||||||
|
// If identity org membership not found, throw error
|
||||||
|
if (!identityOrgMembership) {
|
||||||
|
throw new BadRequestError({
|
||||||
|
message: `Failed to find identity with id ${actorId}`
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get the role permission for the identity
|
||||||
|
// IS THIS CORRECT?
|
||||||
|
const { permission: rolePermission, role: customRole } = await permissionService.getOrgPermissionByRole(
|
||||||
|
ProjectMembershipRole.Admin,
|
||||||
|
orgId
|
||||||
|
);
|
||||||
|
|
||||||
|
const hasPrivilege = isAtLeastAsPrivileged(permission, rolePermission);
|
||||||
|
if (!hasPrivilege)
|
||||||
|
throw new ForbiddenRequestError({
|
||||||
|
message: "Failed to add identity to project with more privileged role"
|
||||||
|
});
|
||||||
|
const isCustomRole = Boolean(customRole);
|
||||||
|
|
||||||
|
await identityProjectDAL.create(
|
||||||
|
{
|
||||||
|
identityId: actorId,
|
||||||
|
projectId: project.id,
|
||||||
|
role: isCustomRole ? ProjectMembershipRole.Custom : ProjectMembershipRole.Admin,
|
||||||
|
roleId: customRole?.id
|
||||||
|
},
|
||||||
|
tx
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
...project,
|
||||||
|
environments: envs,
|
||||||
|
_id: project.id
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -1,15 +1,15 @@
|
|||||||
<!DOCTYPE html>
|
|
||||||
<html>
|
<html>
|
||||||
<head>
|
<head>
|
||||||
<meta charset="utf-8">
|
<meta charset="utf-8" />
|
||||||
<meta http-equiv="x-ua-compatible" content="ie=edge">
|
<meta http-equiv="x-ua-compatible" content="ie=edge" />
|
||||||
<title>Project Invitation</title>
|
<title>Project Invitation</title>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<h2>Join your team on Infisical</h2>
|
<h2>Join your team on Infisical</h2>
|
||||||
<p>{{inviterFirstName}} ({{inviterEmail}}) has invited you to their Infisical project — {{workspaceName}}</p>
|
<p>You have been invited to a new Infisical project — {{workspaceName}}</p>
|
||||||
<a href="{{callback_url}}">Join now</a>
|
<a href="{{callback_url}}">Join now</a>
|
||||||
<h3>What is Infisical?</h3>
|
<h3>What is Infisical?</h3>
|
||||||
<p>Infisical is an easy-to-use end-to-end encrypted tool that enables developers to sync and manage their secrets and configs.</p>
|
<p>Infisical is an easy-to-use end-to-end encrypted tool that enables developers to sync and manage their secrets
|
||||||
</body>
|
and configs.</p>
|
||||||
|
</body>
|
||||||
</html>
|
</html>
|
||||||
@@ -6,6 +6,7 @@ export type Workspace = {
|
|||||||
e2ee: boolean;
|
e2ee: boolean;
|
||||||
autoCapitalization: boolean;
|
autoCapitalization: boolean;
|
||||||
environments: WorkspaceEnv[];
|
environments: WorkspaceEnv[];
|
||||||
|
slug: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type WorkspaceEnv = {
|
export type WorkspaceEnv = {
|
||||||
|
|||||||
@@ -50,8 +50,8 @@ import {
|
|||||||
} from "@app/context";
|
} from "@app/context";
|
||||||
import { withPermission } from "@app/hoc";
|
import { withPermission } from "@app/hoc";
|
||||||
import {
|
import {
|
||||||
// fetchOrgUsers,
|
fetchOrgUsers,
|
||||||
// useAddUserToWs,
|
useAddUserToWsNonE2EE,
|
||||||
useCreateWorkspace,
|
useCreateWorkspace,
|
||||||
useRegisterUserAction
|
useRegisterUserAction
|
||||||
} from "@app/hooks/api";
|
} from "@app/hooks/api";
|
||||||
@@ -471,7 +471,7 @@ const OrganizationPage = withPermission(
|
|||||||
const currentOrg = String(router.query.id);
|
const currentOrg = String(router.query.id);
|
||||||
const orgWorkspaces = workspaces?.filter((workspace) => workspace.orgId === currentOrg) || [];
|
const orgWorkspaces = workspaces?.filter((workspace) => workspace.orgId === currentOrg) || [];
|
||||||
const { createNotification } = useNotificationContext();
|
const { createNotification } = useNotificationContext();
|
||||||
// const addWsUser = useAddUserToWs();
|
const addUsersToProject = useAddUserToWsNonE2EE();
|
||||||
|
|
||||||
const { popUp, handlePopUpOpen, handlePopUpClose, handlePopUpToggle } = usePopUp([
|
const { popUp, handlePopUpOpen, handlePopUpClose, handlePopUpToggle } = usePopUp([
|
||||||
"addNewWs",
|
"addNewWs",
|
||||||
@@ -498,10 +498,11 @@ const OrganizationPage = withPermission(
|
|||||||
const onCreateProject = async ({ name, addMembers }: TAddProjectFormData) => {
|
const onCreateProject = async ({ name, addMembers }: TAddProjectFormData) => {
|
||||||
// type check
|
// type check
|
||||||
if (!currentOrg) return;
|
if (!currentOrg) return;
|
||||||
|
if (!user) return;
|
||||||
try {
|
try {
|
||||||
const {
|
const {
|
||||||
data: {
|
data: {
|
||||||
project: { id: newWorkspaceId }
|
project: { id: newProjectId }
|
||||||
}
|
}
|
||||||
} = await createWs.mutateAsync({
|
} = await createWs.mutateAsync({
|
||||||
organizationId: currentOrg,
|
organizationId: currentOrg,
|
||||||
@@ -509,27 +510,19 @@ const OrganizationPage = withPermission(
|
|||||||
});
|
});
|
||||||
|
|
||||||
if (addMembers) {
|
if (addMembers) {
|
||||||
// not using hooks because need at this point only
|
const orgUsers = await fetchOrgUsers(currentOrg);
|
||||||
// const orgUsers = await fetchOrgUsers(currentOrg);
|
|
||||||
// const decryptKey = await fetchUserWsKey(newWorkspaceId);
|
await addUsersToProject.mutateAsync({
|
||||||
// await addWsUser.mutateAsync({
|
emails: orgUsers
|
||||||
// workspaceId: newWorkspaceId,
|
.map((member) => member.user.email)
|
||||||
// decryptKey,
|
.filter((email) => email !== user.email),
|
||||||
// userPrivateKey: PRIVATE_KEY,
|
projectId: newProjectId
|
||||||
// members: orgUsers
|
});
|
||||||
// .filter(
|
|
||||||
// ({ status, user: orgUser }) => status === "accepted" && user.email !== orgUser.email
|
|
||||||
// )
|
|
||||||
// .map(({ user: orgUser, id: orgMembershipId }) => ({
|
|
||||||
// userPublicKey: orgUser.publicKey,
|
|
||||||
// orgMembershipId
|
|
||||||
// }))
|
|
||||||
// });
|
|
||||||
}
|
}
|
||||||
|
|
||||||
createNotification({ text: "Workspace created", type: "success" });
|
createNotification({ text: "Workspace created", type: "success" });
|
||||||
handlePopUpClose("addNewWs");
|
handlePopUpClose("addNewWs");
|
||||||
router.push(`/project/${newWorkspaceId}/secrets/overview`);
|
router.push(`/project/${newProjectId}/secrets/overview`);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error(err);
|
console.error(err);
|
||||||
createNotification({ text: "Failed to create workspace", type: "error" });
|
createNotification({ text: "Failed to create workspace", type: "error" });
|
||||||
|
|||||||
Reference in New Issue
Block a user