Merge pull request #2384 from akhilmhdh/feat/org-project-invite

Manager users without waiting for confirmation of mail
This commit is contained in:
Daniel Hougaard
2024-09-11 13:06:28 +04:00
committed by GitHub
17 changed files with 543 additions and 361 deletions
@@ -41,10 +41,9 @@ export const userGroupMembershipDALFactory = (db: TDbClient) => {
}; };
// special query // special query
const findUserGroupMembershipsInProject = async (usernames: string[], projectId: string) => { const findUserGroupMembershipsInProject = async (usernames: string[], projectId: string, tx?: Knex) => {
try { try {
const usernameDocs: string[] = await db const usernameDocs: string[] = await (tx || db.replicaNode())(TableName.UserGroupMembership)
.replicaNode()(TableName.UserGroupMembership)
.join( .join(
TableName.GroupProjectMembership, TableName.GroupProjectMembership,
`${TableName.UserGroupMembership}.groupId`, `${TableName.UserGroupMembership}.groupId`,
+15 -5
View File
@@ -5,6 +5,7 @@ import nacl from "tweetnacl";
import tweetnacl from "tweetnacl-util"; import tweetnacl from "tweetnacl-util";
import { TUserEncryptionKeys } from "@app/db/schemas"; import { TUserEncryptionKeys } from "@app/db/schemas";
import { UserEncryption } from "@app/services/user/user-types";
import { decryptSymmetric128BitHexKeyUTF8, encryptAsymmetric, encryptSymmetric } from "./encryption"; import { decryptSymmetric128BitHexKeyUTF8, encryptAsymmetric, encryptSymmetric } from "./encryption";
@@ -36,12 +37,16 @@ export const srpCheckClientProof = async (
// Ghost user related: // Ghost user related:
// This functionality is intended for ghost user logic. This happens on the frontend when a user is being created. // This functionality is intended for ghost user logic. This happens on the frontend when a user is being created.
// We replicate the same functionality on the backend when creating a ghost user. // We replicate the same functionality on the backend when creating a ghost user.
export const generateUserSrpKeys = async (email: string, password: string) => { export const generateUserSrpKeys = async (
email: string,
password: string,
customKeys?: { publicKey: string; privateKey: string }
) => {
const pair = nacl.box.keyPair(); const pair = nacl.box.keyPair();
const secretKeyUint8Array = pair.secretKey; const secretKeyUint8Array = pair.secretKey;
const publicKeyUint8Array = pair.publicKey; const publicKeyUint8Array = pair.publicKey;
const privateKey = tweetnacl.encodeBase64(secretKeyUint8Array); const privateKey = customKeys?.privateKey || tweetnacl.encodeBase64(secretKeyUint8Array);
const publicKey = tweetnacl.encodeBase64(publicKeyUint8Array); const publicKey = customKeys?.publicKey || tweetnacl.encodeBase64(publicKeyUint8Array);
// eslint-disable-next-line // eslint-disable-next-line
const client = new jsrp.client(); const client = new jsrp.client();
@@ -111,7 +116,7 @@ export const getUserPrivateKey = async (
| "encryptionVersion" | "encryptionVersion"
> >
) => { ) => {
if (user.encryptionVersion === 1) { if (user.encryptionVersion === UserEncryption.V1) {
return decryptSymmetric128BitHexKeyUTF8({ return decryptSymmetric128BitHexKeyUTF8({
ciphertext: user.encryptedPrivateKey, ciphertext: user.encryptedPrivateKey,
iv: user.iv, iv: user.iv,
@@ -119,7 +124,12 @@ export const getUserPrivateKey = async (
key: password.slice(0, 32).padStart(32 + (password.slice(0, 32).length - new Blob([password]).size), "0") key: password.slice(0, 32).padStart(32 + (password.slice(0, 32).length - new Blob([password]).size), "0")
}); });
} }
if (user.encryptionVersion === 2 && user.protectedKey && user.protectedKeyIV && user.protectedKeyTag) { if (
user.encryptionVersion === UserEncryption.V2 &&
user.protectedKey &&
user.protectedKeyIV &&
user.protectedKeyTag
) {
const derivedKey = await argon2.hash(password, { const derivedKey = await argon2.hash(password, {
salt: Buffer.from(user.salt), salt: Buffer.from(user.salt),
memoryCost: 65536, memoryCost: 65536,
+8 -2
View File
@@ -1,10 +1,16 @@
import fs from "fs/promises"; import fs from "fs/promises";
import path from "path"; import path from "path";
export const isDisposableEmail = async (email: string) => { export const isDisposableEmail = async (emails: string | string[]) => {
const emailDomain = email.split("@")[1];
const disposableEmails = await fs.readFile(path.join(__dirname, "disposable_emails.txt"), "utf8"); const disposableEmails = await fs.readFile(path.join(__dirname, "disposable_emails.txt"), "utf8");
if (Array.isArray(emails)) {
return emails.some((email) => {
const emailDomain = email.split("@")[1];
return disposableEmails.split("\n").includes(emailDomain);
});
}
const emailDomain = emails.split("@")[1];
if (disposableEmails.split("\n").includes(emailDomain)) return true; if (disposableEmails.split("\n").includes(emailDomain)) return true;
return false; return false;
}; };
+2 -1
View File
@@ -495,7 +495,8 @@ export const registerRoutes = async (
smtpService, smtpService,
userDAL, userDAL,
groupDAL, groupDAL,
orgBotDAL orgBotDAL,
projectRoleDAL
}); });
const signupService = authSignupServiceFactory({ const signupService = authSignupServiceFactory({
tokenService, tokenService,
@@ -18,9 +18,14 @@ export const registerInviteOrgRouter = async (server: FastifyZodProvider) => {
body: z.object({ body: z.object({
inviteeEmails: z.array(z.string().trim().email()), inviteeEmails: z.array(z.string().trim().email()),
organizationId: z.string().trim(), organizationId: z.string().trim(),
projectIds: z.array(z.string().trim()).optional(), projects: z
projectRoleSlug: z.nativeEnum(ProjectMembershipRole).optional(), .object({
organizationRoleSlug: z.nativeEnum(OrgMembershipRole) id: z.string(),
projectRoleSlug: z.string().array().default([ProjectMembershipRole.Member])
})
.array()
.optional(),
organizationRoleSlug: z.string().default(OrgMembershipRole.Member)
}), }),
response: { response: {
200: z.object({ 200: z.object({
@@ -40,12 +45,12 @@ export const registerInviteOrgRouter = async (server: FastifyZodProvider) => {
handler: async (req) => { handler: async (req) => {
if (req.auth.actor !== ActorType.USER) return; if (req.auth.actor !== ActorType.USER) return;
const completeInviteLinks = await server.services.org.inviteUserToOrganization({ const { signupTokens: completeInviteLinks } = await server.services.org.inviteUserToOrganization({
orgId: req.body.organizationId, orgId: req.body.organizationId,
userId: req.permission.id, actor: req.permission.type,
actorId: req.permission.id,
inviteeEmails: req.body.inviteeEmails, inviteeEmails: req.body.inviteeEmails,
projectIds: req.body.projectIds, projects: req.body.projects,
projectRoleSlug: req.body.projectRoleSlug,
organizationRoleSlug: req.body.organizationRoleSlug, organizationRoleSlug: req.body.organizationRoleSlug,
actorAuthMethod: req.permission.authMethod, actorAuthMethod: req.permission.authMethod,
actorOrgId: req.permission.orgId actorOrgId: req.permission.orgId
@@ -1,6 +1,6 @@
import { z } from "zod"; import { z } from "zod";
import { ProjectMembershipsSchema } from "@app/db/schemas"; import { OrgMembershipRole, ProjectMembershipRole, ProjectMembershipsSchema } from "@app/db/schemas";
import { EventType } from "@app/ee/services/audit-log/audit-log-types"; import { EventType } from "@app/ee/services/audit-log/audit-log-types";
import { PROJECT_USERS } from "@app/lib/api-docs"; import { PROJECT_USERS } from "@app/lib/api-docs";
import { writeLimit } from "@app/server/config/rateLimiter"; import { writeLimit } from "@app/server/config/rateLimiter";
@@ -36,14 +36,21 @@ export const registerProjectMembershipRouter = async (server: FastifyZodProvider
}, },
onRequest: verifyAuth([AuthMode.JWT, AuthMode.API_KEY, AuthMode.IDENTITY_ACCESS_TOKEN]), onRequest: verifyAuth([AuthMode.JWT, AuthMode.API_KEY, AuthMode.IDENTITY_ACCESS_TOKEN]),
handler: async (req) => { handler: async (req) => {
const memberships = await server.services.projectMembership.addUsersToProjectNonE2EE({ const usernamesAndEmails = [...req.body.emails, ...req.body.usernames];
projectId: req.params.projectId, const { projectMemberships: memberships } = await server.services.org.inviteUserToOrganization({
actorAuthMethod: req.permission.authMethod, actorAuthMethod: req.permission.authMethod,
actorId: req.permission.id, actorId: req.permission.id,
actorOrgId: req.permission.orgId, actorOrgId: req.permission.orgId,
actor: req.permission.type, actor: req.permission.type,
emails: req.body.emails, inviteeEmails: usernamesAndEmails,
usernames: req.body.usernames orgId: req.permission.orgId,
organizationRoleSlug: OrgMembershipRole.NoAccess,
projects: [
{
id: req.params.projectId,
projectRoleSlug: [ProjectMembershipRole.Member]
}
]
}); });
await server.services.auditLog.createAuditLog({ await server.services.auditLog.createAuditLog({
+154 -90
View File
@@ -1,15 +1,15 @@
import bcrypt from "bcrypt"; import bcrypt from "bcrypt";
import jwt from "jsonwebtoken"; import jwt from "jsonwebtoken";
import { OrgMembershipStatus, TableName } from "@app/db/schemas"; import { OrgMembershipStatus, SecretKeyEncoding, TableName } from "@app/db/schemas";
import { convertPendingGroupAdditionsToGroupMemberships } from "@app/ee/services/group/group-fns"; import { convertPendingGroupAdditionsToGroupMemberships } from "@app/ee/services/group/group-fns";
import { TUserGroupMembershipDALFactory } from "@app/ee/services/group/user-group-membership-dal"; import { TUserGroupMembershipDALFactory } from "@app/ee/services/group/user-group-membership-dal";
import { TLicenseServiceFactory } from "@app/ee/services/license/license-service"; import { TLicenseServiceFactory } from "@app/ee/services/license/license-service";
import { isAuthMethodSaml } from "@app/ee/services/permission/permission-fns"; import { isAuthMethodSaml } from "@app/ee/services/permission/permission-fns";
import { getConfig } from "@app/lib/config/env"; import { getConfig } from "@app/lib/config/env";
import { infisicalSymmetricEncypt } from "@app/lib/crypto/encryption"; import { infisicalSymmetricDecrypt, infisicalSymmetricEncypt } from "@app/lib/crypto/encryption";
import { getUserPrivateKey } from "@app/lib/crypto/srp"; import { generateUserSrpKeys, getUserPrivateKey } from "@app/lib/crypto/srp";
import { BadRequestError, UnauthorizedError } from "@app/lib/errors"; import { BadRequestError } from "@app/lib/errors";
import { isDisposableEmail } from "@app/lib/validator"; import { isDisposableEmail } from "@app/lib/validator";
import { TGroupProjectDALFactory } from "@app/services/group-project/group-project-dal"; import { TGroupProjectDALFactory } from "@app/services/group-project/group-project-dal";
import { TProjectDALFactory } from "@app/services/project/project-dal"; import { TProjectDALFactory } from "@app/services/project/project-dal";
@@ -17,14 +17,14 @@ import { TProjectBotDALFactory } from "@app/services/project-bot/project-bot-dal
import { TProjectKeyDALFactory } from "@app/services/project-key/project-key-dal"; import { TProjectKeyDALFactory } from "@app/services/project-key/project-key-dal";
import { TAuthTokenServiceFactory } from "../auth-token/auth-token-service"; import { TAuthTokenServiceFactory } from "../auth-token/auth-token-service";
import { TokenMetadataType, TokenType, TTokenMetadata } from "../auth-token/auth-token-types"; import { TokenType } from "../auth-token/auth-token-types";
import { TOrgDALFactory } from "../org/org-dal"; import { TOrgDALFactory } from "../org/org-dal";
import { TOrgServiceFactory } from "../org/org-service"; import { TOrgServiceFactory } from "../org/org-service";
import { TProjectMembershipDALFactory } from "../project-membership/project-membership-dal"; import { TProjectMembershipDALFactory } from "../project-membership/project-membership-dal";
import { addMembersToProject } from "../project-membership/project-membership-fns";
import { TProjectUserMembershipRoleDALFactory } from "../project-membership/project-user-membership-role-dal"; import { TProjectUserMembershipRoleDALFactory } from "../project-membership/project-user-membership-role-dal";
import { SmtpTemplates, TSmtpService } from "../smtp/smtp-service"; import { SmtpTemplates, TSmtpService } from "../smtp/smtp-service";
import { TUserDALFactory } from "../user/user-dal"; import { TUserDALFactory } from "../user/user-dal";
import { UserEncryption } from "../user/user-types";
import { TAuthDALFactory } from "./auth-dal"; import { TAuthDALFactory } from "./auth-dal";
import { validateProviderAuthToken, validateSignUpAuthorization } from "./auth-fns"; import { validateProviderAuthToken, validateSignUpAuthorization } from "./auth-fns";
import { TCompleteAccountInviteDTO, TCompleteAccountSignupDTO } from "./auth-signup-type"; import { TCompleteAccountInviteDTO, TCompleteAccountSignupDTO } from "./auth-signup-type";
@@ -67,8 +67,6 @@ export const authSignupServiceFactory = ({
smtpService, smtpService,
orgService, orgService,
orgDAL, orgDAL,
projectMembershipDAL,
projectUserMembershipRoleDAL,
licenseService licenseService
}: TAuthSignupDep) => { }: TAuthSignupDep) => {
// first step of signup. create user and send email // first step of signup. create user and send email
@@ -177,32 +175,88 @@ export const authSignupServiceFactory = ({
encryptedPrivateKey, encryptedPrivateKey,
iv: encryptedPrivateKeyIV, iv: encryptedPrivateKeyIV,
tag: encryptedPrivateKeyTag, tag: encryptedPrivateKeyTag,
encryptionVersion: 2 encryptionVersion: UserEncryption.V2
}); });
const { tag, encoding, ciphertext, iv } = infisicalSymmetricEncypt(privateKey); const { tag, encoding, ciphertext, iv } = infisicalSymmetricEncypt(privateKey);
const updateduser = await authDAL.transaction(async (tx) => { const updateduser = await authDAL.transaction(async (tx) => {
const us = await userDAL.updateById(user.id, { firstName, lastName, isAccepted: true }, tx); const us = await userDAL.updateById(user.id, { firstName, lastName, isAccepted: true }, tx);
if (!us) throw new Error("User not found"); if (!us) throw new Error("User not found");
const userEncKey = await userDAL.upsertUserEncryptionKey( const systemGeneratedUserEncryptionKey = await userDAL.findUserEncKeyByUserId(us.id, tx);
us.id, let userEncKey;
{
salt, // below condition is true means this is system generated credentials
verifier, // the private key is actually system generated password
publicKey, // thus we will re-encrypt the system generated private key with the new password
protectedKey, // akhilmhdh: you may find this like why? The reason is simple we are moving away from e2ee and these are pieces of it
protectedKeyIV, // without a dummy key in place some things will break and backward compatiability too. 2025 we will be removing all these things
protectedKeyTag, if (
encryptedPrivateKey, systemGeneratedUserEncryptionKey &&
iv: encryptedPrivateKeyIV, !systemGeneratedUserEncryptionKey.hashedPassword &&
tag: encryptedPrivateKeyTag, systemGeneratedUserEncryptionKey.serverEncryptedPrivateKey &&
hashedPassword, systemGeneratedUserEncryptionKey.serverEncryptedPrivateKeyTag &&
serverEncryptedPrivateKeyEncoding: encoding, systemGeneratedUserEncryptionKey.serverEncryptedPrivateKeyIV &&
serverEncryptedPrivateKeyTag: tag, systemGeneratedUserEncryptionKey.serverEncryptedPrivateKeyEncoding
serverEncryptedPrivateKeyIV: iv, ) {
serverEncryptedPrivateKey: ciphertext // get server generated password
}, const serverGeneratedPassword = infisicalSymmetricDecrypt({
tx iv: systemGeneratedUserEncryptionKey.serverEncryptedPrivateKeyIV,
); tag: systemGeneratedUserEncryptionKey.serverEncryptedPrivateKeyTag,
ciphertext: systemGeneratedUserEncryptionKey.serverEncryptedPrivateKey,
keyEncoding: systemGeneratedUserEncryptionKey.serverEncryptedPrivateKeyEncoding as SecretKeyEncoding
});
const serverGeneratedPrivateKey = await getUserPrivateKey(serverGeneratedPassword, {
...systemGeneratedUserEncryptionKey
});
const encKeys = await generateUserSrpKeys(email, password, {
publicKey: systemGeneratedUserEncryptionKey.publicKey,
privateKey: serverGeneratedPrivateKey
});
// now reencrypt server generated key with user provided password
userEncKey = await userDAL.upsertUserEncryptionKey(
us.id,
{
encryptionVersion: UserEncryption.V2,
protectedKey: encKeys.protectedKey,
protectedKeyIV: encKeys.protectedKeyIV,
protectedKeyTag: encKeys.protectedKeyTag,
publicKey: encKeys.publicKey,
encryptedPrivateKey: encKeys.encryptedPrivateKey,
iv: encKeys.encryptedPrivateKeyIV,
tag: encKeys.encryptedPrivateKeyTag,
salt: encKeys.salt,
verifier: encKeys.verifier,
hashedPassword,
serverEncryptedPrivateKeyEncoding: encoding,
serverEncryptedPrivateKeyTag: tag,
serverEncryptedPrivateKeyIV: iv,
serverEncryptedPrivateKey: ciphertext
},
tx
);
} else {
userEncKey = await userDAL.upsertUserEncryptionKey(
us.id,
{
encryptionVersion: UserEncryption.V2,
salt,
verifier,
publicKey,
protectedKey,
protectedKeyIV,
protectedKeyTag,
encryptedPrivateKey,
iv: encryptedPrivateKeyIV,
tag: encryptedPrivateKeyTag,
hashedPassword,
serverEncryptedPrivateKeyEncoding: encoding,
serverEncryptedPrivateKeyTag: tag,
serverEncryptedPrivateKeyIV: iv,
serverEncryptedPrivateKey: ciphertext
},
tx
);
}
// If it's SAML Auth and the organization ID is present, we should check if the user has a pending invite for this org, and accept it // If it's SAML Auth and the organization ID is present, we should check if the user has a pending invite for this org, and accept it
if ( if (
(isAuthMethodSaml(authMethod) || [AuthMethod.LDAP, AuthMethod.OIDC].includes(authMethod as AuthMethod)) && (isAuthMethodSaml(authMethod) || [AuthMethod.LDAP, AuthMethod.OIDC].includes(authMethod as AuthMethod)) &&
@@ -312,8 +366,7 @@ export const authSignupServiceFactory = ({
encryptedPrivateKey, encryptedPrivateKey,
encryptedPrivateKeyIV, encryptedPrivateKeyIV,
encryptedPrivateKeyTag, encryptedPrivateKeyTag,
authorization, authorization
tokenMetadata
}: TCompleteAccountInviteDTO) => { }: TCompleteAccountInviteDTO) => {
const user = await userDAL.findUserByUsername(email); const user = await userDAL.findUserByUsername(email);
if (!user || (user && user.isAccepted)) { if (!user || (user && user.isAccepted)) {
@@ -348,65 +401,76 @@ export const authSignupServiceFactory = ({
const updateduser = await authDAL.transaction(async (tx) => { const updateduser = await authDAL.transaction(async (tx) => {
const us = await userDAL.updateById(user.id, { firstName, lastName, isAccepted: true }, tx); const us = await userDAL.updateById(user.id, { firstName, lastName, isAccepted: true }, tx);
if (!us) throw new Error("User not found"); if (!us) throw new Error("User not found");
const userEncKey = await userDAL.upsertUserEncryptionKey( const systemGeneratedUserEncryptionKey = await userDAL.findUserEncKeyByUserId(us.id, tx);
us.id, let userEncKey;
{ // this means this is system generated credentials
salt, // now replace the private key
encryptionVersion: 2, if (
verifier, systemGeneratedUserEncryptionKey &&
publicKey, !systemGeneratedUserEncryptionKey.hashedPassword &&
protectedKey, systemGeneratedUserEncryptionKey.serverEncryptedPrivateKey &&
protectedKeyIV, systemGeneratedUserEncryptionKey.serverEncryptedPrivateKeyTag &&
protectedKeyTag, systemGeneratedUserEncryptionKey.serverEncryptedPrivateKeyIV &&
encryptedPrivateKey, systemGeneratedUserEncryptionKey.serverEncryptedPrivateKeyEncoding
iv: encryptedPrivateKeyIV, ) {
tag: encryptedPrivateKeyTag, // get server generated password
hashedPassword, const serverGeneratedPassword = infisicalSymmetricDecrypt({
serverEncryptedPrivateKeyEncoding: encoding, iv: systemGeneratedUserEncryptionKey.serverEncryptedPrivateKeyIV,
serverEncryptedPrivateKeyTag: tag, tag: systemGeneratedUserEncryptionKey.serverEncryptedPrivateKeyTag,
serverEncryptedPrivateKeyIV: iv, ciphertext: systemGeneratedUserEncryptionKey.serverEncryptedPrivateKey,
serverEncryptedPrivateKey: ciphertext keyEncoding: systemGeneratedUserEncryptionKey.serverEncryptedPrivateKeyEncoding as SecretKeyEncoding
}, });
tx const serverGeneratedPrivateKey = await getUserPrivateKey(serverGeneratedPassword, {
); ...systemGeneratedUserEncryptionKey
});
if (tokenMetadata) { const encKeys = await generateUserSrpKeys(email, password, {
const metadataObj = jwt.verify(tokenMetadata, appCfg.AUTH_SECRET) as TTokenMetadata; publicKey: systemGeneratedUserEncryptionKey.publicKey,
privateKey: serverGeneratedPrivateKey
if ( });
metadataObj?.payload?.userId !== user.id || // now reencrypt server generated key with user provided password
metadataObj?.payload?.orgId !== orgMembership.orgId || userEncKey = await userDAL.upsertUserEncryptionKey(
metadataObj?.type !== TokenMetadataType.InviteToProjects us.id,
) { {
throw new UnauthorizedError({ encryptionVersion: 2,
message: "Malformed or invalid metadata token" protectedKey: encKeys.protectedKey,
}); protectedKeyIV: encKeys.protectedKeyIV,
} protectedKeyTag: encKeys.protectedKeyTag,
publicKey: encKeys.publicKey,
for await (const projectId of metadataObj.payload.projectIds) { encryptedPrivateKey: encKeys.encryptedPrivateKey,
await addMembersToProject({ iv: encKeys.encryptedPrivateKeyIV,
orgDAL, tag: encKeys.encryptedPrivateKeyTag,
projectDAL, salt: encKeys.salt,
projectMembershipDAL, verifier: encKeys.verifier,
projectKeyDAL, hashedPassword,
userGroupMembershipDAL, serverEncryptedPrivateKeyEncoding: encoding,
projectBotDAL, serverEncryptedPrivateKeyTag: tag,
projectUserMembershipRoleDAL, serverEncryptedPrivateKeyIV: iv,
smtpService serverEncryptedPrivateKey: ciphertext
}).addMembersToNonE2EEProject( },
{ tx
emails: [user.email!], );
usernames: [], } else {
projectId, userEncKey = await userDAL.upsertUserEncryptionKey(
projectMembershipRole: metadataObj.payload.projectRoleSlug, us.id,
sendEmails: false {
}, encryptionVersion: UserEncryption.V2,
{ salt,
tx, verifier,
throwOnProjectNotFound: false publicKey,
} protectedKey,
); protectedKeyIV,
} protectedKeyTag,
encryptedPrivateKey,
iv: encryptedPrivateKeyIV,
tag: encryptedPrivateKeyTag,
hashedPassword,
serverEncryptedPrivateKeyEncoding: encoding,
serverEncryptedPrivateKeyTag: tag,
serverEncryptedPrivateKeyIV: iv,
serverEncryptedPrivateKey: ciphertext
},
tx
);
} }
const updatedMembersips = await orgDAL.updateMembership( const updatedMembersips = await orgDAL.updateMembership(
+309 -169
View File
@@ -9,7 +9,10 @@ import {
OrgMembershipStatus, OrgMembershipStatus,
ProjectMembershipRole, ProjectMembershipRole,
ProjectVersion, ProjectVersion,
SecretKeyEncoding,
TableName, TableName,
TProjectMemberships,
TProjectUserMembershipRolesInsert,
TUsers TUsers
} from "@app/db/schemas"; } from "@app/db/schemas";
import { TProjects } from "@app/db/schemas/projects"; import { TProjects } from "@app/db/schemas/projects";
@@ -18,13 +21,15 @@ import { TUserGroupMembershipDALFactory } from "@app/ee/services/group/user-grou
import { TLicenseServiceFactory } from "@app/ee/services/license/license-service"; import { TLicenseServiceFactory } from "@app/ee/services/license/license-service";
import { OrgPermissionActions, OrgPermissionSubjects } from "@app/ee/services/permission/org-permission"; import { OrgPermissionActions, OrgPermissionSubjects } from "@app/ee/services/permission/org-permission";
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service"; import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service";
import { ProjectPermissionActions, ProjectPermissionSub } from "@app/ee/services/permission/project-permission";
import { TProjectUserAdditionalPrivilegeDALFactory } from "@app/ee/services/project-user-additional-privilege/project-user-additional-privilege-dal"; import { TProjectUserAdditionalPrivilegeDALFactory } from "@app/ee/services/project-user-additional-privilege/project-user-additional-privilege-dal";
import { TSamlConfigDALFactory } from "@app/ee/services/saml-config/saml-config-dal"; import { TSamlConfigDALFactory } from "@app/ee/services/saml-config/saml-config-dal";
import { getConfig } from "@app/lib/config/env"; import { getConfig } from "@app/lib/config/env";
import { generateAsymmetricKeyPair } from "@app/lib/crypto"; import { generateAsymmetricKeyPair } from "@app/lib/crypto";
import { generateSymmetricKey, infisicalSymmetricEncypt } from "@app/lib/crypto/encryption"; import { generateSymmetricKey, infisicalSymmetricDecrypt, infisicalSymmetricEncypt } from "@app/lib/crypto/encryption";
import { generateUserSrpKeys } from "@app/lib/crypto/srp"; import { generateUserSrpKeys } from "@app/lib/crypto/srp";
import { BadRequestError, NotFoundError, UnauthorizedError } from "@app/lib/errors"; import { BadRequestError, NotFoundError, UnauthorizedError } from "@app/lib/errors";
import { groupBy } from "@app/lib/fn";
import { alphaNumericNanoId } from "@app/lib/nanoid"; import { alphaNumericNanoId } from "@app/lib/nanoid";
import { isDisposableEmail } from "@app/lib/validator"; import { isDisposableEmail } from "@app/lib/validator";
import { TOrgMembershipDALFactory } from "@app/services/org-membership/org-membership-dal"; import { TOrgMembershipDALFactory } from "@app/services/org-membership/org-membership-dal";
@@ -32,14 +37,14 @@ import { TUserAliasDALFactory } from "@app/services/user-alias/user-alias-dal";
import { ActorAuthMethod, ActorType, AuthMethod, AuthTokenType } from "../auth/auth-type"; import { ActorAuthMethod, ActorType, AuthMethod, AuthTokenType } from "../auth/auth-type";
import { TAuthTokenServiceFactory } from "../auth-token/auth-token-service"; import { TAuthTokenServiceFactory } from "../auth-token/auth-token-service";
import { TokenMetadataType, TokenType, TTokenMetadata } from "../auth-token/auth-token-types"; import { TokenType } from "../auth-token/auth-token-types";
import { TProjectDALFactory } from "../project/project-dal"; import { TProjectDALFactory } from "../project/project-dal";
import { verifyProjectVersions } from "../project/project-fns"; import { assignWorkspaceKeysToMembers } from "../project/project-fns";
import { TProjectBotDALFactory } from "../project-bot/project-bot-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 { TProjectMembershipDALFactory } from "../project-membership/project-membership-dal"; import { TProjectMembershipDALFactory } from "../project-membership/project-membership-dal";
import { addMembersToProject } from "../project-membership/project-membership-fns";
import { TProjectUserMembershipRoleDALFactory } from "../project-membership/project-user-membership-role-dal"; import { TProjectUserMembershipRoleDALFactory } from "../project-membership/project-user-membership-role-dal";
import { TProjectRoleDALFactory } from "../project-role/project-role-dal";
import { SmtpTemplates, TSmtpService } from "../smtp/smtp-service"; import { SmtpTemplates, TSmtpService } from "../smtp/smtp-service";
import { TUserDALFactory } from "../user/user-dal"; import { TUserDALFactory } from "../user/user-dal";
import { TIncidentContactsDALFactory } from "./incident-contacts-dal"; import { TIncidentContactsDALFactory } from "./incident-contacts-dal";
@@ -84,6 +89,7 @@ type TOrgServiceFactoryDep = {
"getPlan" | "updateSubscriptionOrgMemberCount" | "generateOrgCustomerId" | "removeOrgCustomer" "getPlan" | "updateSubscriptionOrgMemberCount" | "generateOrgCustomerId" | "removeOrgCustomer"
>; >;
projectUserAdditionalPrivilegeDAL: Pick<TProjectUserAdditionalPrivilegeDALFactory, "delete">; projectUserAdditionalPrivilegeDAL: Pick<TProjectUserAdditionalPrivilegeDALFactory, "delete">;
projectRoleDAL: Pick<TProjectRoleDALFactory, "find">;
userGroupMembershipDAL: Pick<TUserGroupMembershipDALFactory, "findUserGroupMembershipsInProject">; userGroupMembershipDAL: Pick<TUserGroupMembershipDALFactory, "findUserGroupMembershipsInProject">;
projectBotDAL: Pick<TProjectBotDALFactory, "findOne">; projectBotDAL: Pick<TProjectBotDALFactory, "findOne">;
projectUserMembershipRoleDAL: Pick<TProjectUserMembershipRoleDALFactory, "insertMany">; projectUserMembershipRoleDAL: Pick<TProjectUserMembershipRoleDALFactory, "insertMany">;
@@ -111,7 +117,8 @@ export const orgServiceFactory = ({
samlConfigDAL, samlConfigDAL,
userGroupMembershipDAL, userGroupMembershipDAL,
projectBotDAL, projectBotDAL,
projectUserMembershipRoleDAL projectUserMembershipRoleDAL,
projectRoleDAL
}: TOrgServiceFactoryDep) => { }: TOrgServiceFactoryDep) => {
/* /*
* Get organization details by the organization id * Get organization details by the organization id
@@ -440,17 +447,17 @@ export const orgServiceFactory = ({
*/ */
const inviteUserToOrganization = async ({ const inviteUserToOrganization = async ({
orgId, orgId,
userId, actorId,
actor,
inviteeEmails, inviteeEmails,
organizationRoleSlug, organizationRoleSlug,
projectRoleSlug, projects: invitedProjects,
projectIds,
actorAuthMethod, actorAuthMethod,
actorOrgId actorOrgId
}: TInviteUserToOrgDTO) => { }: TInviteUserToOrgDTO) => {
const appCfg = getConfig(); const appCfg = getConfig();
const { permission } = await permissionService.getUserOrgPermission(userId, orgId, actorAuthMethod, actorOrgId); const { permission } = await permissionService.getOrgPermission(actor, actorId, orgId, actorAuthMethod, actorOrgId);
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Create, OrgPermissionSubjects.Member); ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Create, OrgPermissionSubjects.Member);
const org = await orgDAL.findOrgById(orgId); const org = await orgDAL.findOrgById(orgId);
@@ -461,6 +468,13 @@ export const orgServiceFactory = ({
}); });
} }
const isEmailInvalid = await isDisposableEmail(inviteeEmails);
if (isEmailInvalid) {
throw new BadRequestError({
message: "Provided a disposable email",
name: "Org invite"
});
}
const plan = await licenseService.getPlan(orgId); const plan = await licenseService.getPlan(orgId);
if (plan?.memberLimit && plan.membersUsed >= plan.memberLimit) { if (plan?.memberLimit && plan.membersUsed >= plan.memberLimit) {
// limit imposed on number of members allowed / number of members used exceeds the number of members allowed // limit imposed on number of members allowed / number of members used exceeds the number of members allowed
@@ -475,205 +489,331 @@ export const orgServiceFactory = ({
message: "Failed to invite member due to member limit reached. Upgrade plan to invite more members." message: "Failed to invite member due to member limit reached. Upgrade plan to invite more members."
}); });
} }
const isCustomOrgRole = !Object.values(OrgMembershipRole).includes(organizationRoleSlug as OrgMembershipRole);
if (projectIds?.length) { if (isCustomOrgRole) {
const projects = await projectDAL.find({ if (!plan?.rbac)
orgId,
$in: {
id: projectIds
}
});
// if its not v3, throw an error
if (!verifyProjectVersions(projects, ProjectVersion.V3)) {
throw new BadRequestError({ throw new BadRequestError({
message: "One or more selected projects are not compatible with this operation. Please upgrade your projects." message: "Failed to assign custom role due to RBAC restriction. Upgrade plan to assign custom role to member."
}); });
}
} }
const inviteeUsers = await orgDAL.transaction(async (tx) => { const projectsToInvite = invitedProjects?.length
const users: Pick< ? await projectDAL.find({
TUsers & { orgId: string }, orgId,
"id" | "firstName" | "lastName" | "email" | "orgId" | "username" $in: {
>[] = []; id: invitedProjects?.map(({ id }) => id)
}
})
: [];
if (projectsToInvite.length !== invitedProjects?.length) {
throw new UnauthorizedError({
message: "One or more project doesn't have access to"
});
}
if (projectsToInvite.some((el) => el.version !== ProjectVersion.V3)) {
throw new BadRequestError({
message: "One or more selected projects are not compatible with this operation. Please upgrade your projects."
});
}
const mailsForOrgInvitation: { email: string; userId: string; firstName: string; lastName: string }[] = [];
const mailsForProjectInvitaion: { email: string[]; projectName: string }[] = [];
const newProjectMemberships: TProjectMemberships[] = [];
await orgDAL.transaction(async (tx) => {
const users: Pick<TUsers, "id" | "firstName" | "lastName" | "email" | "username">[] = [];
for await (const inviteeEmail of inviteeEmails) { for await (const inviteeEmail of inviteeEmails) {
const inviteeUser = await userDAL.findUserByUsername(inviteeEmail, tx); let inviteeUser = await userDAL.findUserByUsername(inviteeEmail, tx);
if (inviteeUser) { // if the user doesn't exist we create the user with the email
// if user already exist means its already part of infisical if (!inviteeUser) {
// Thus the signup flow is not needed anymore inviteeUser = await userDAL.create(
const [inviteeMembership] = await orgDAL.findMembership(
{ {
[`${TableName.OrgMembership}.orgId` as "orgId"]: orgId, isAccepted: false,
[`${TableName.OrgMembership}.userId` as "userId"]: inviteeUser.id email: inviteeEmail,
username: inviteeEmail,
authMethods: [AuthMethod.EMAIL],
isGhost: false
}, },
{ tx } tx
); );
if (inviteeMembership && inviteeMembership.status === OrgMembershipStatus.Accepted) {
throw new BadRequestError({
message: `Failed to invite members because ${inviteeEmail} is already part of the organization`,
name: "Invite user to org"
});
}
if (!inviteeMembership) {
await orgDAL.createMembership(
{
userId: inviteeUser.id,
inviteEmail: inviteeEmail,
orgId,
role: OrgMembershipRole.Member,
status: OrgMembershipStatus.Invited,
isActive: true
},
tx
);
if (projectIds?.length) {
if (
organizationRoleSlug === OrgMembershipRole.Custom ||
projectRoleSlug === ProjectMembershipRole.Custom
) {
throw new BadRequestError({
message: "Custom roles are not supported for inviting users to projects and organizations"
});
}
if (!projectRoleSlug) {
throw new BadRequestError({
message: "Selecting a project role is required to invite users to projects"
});
}
await projectMembershipDAL.insertMany(
projectIds.map((id) => ({ projectId: id, userId: inviteeUser.id })),
tx
);
for await (const projectId of projectIds) {
await addMembersToProject({
orgDAL,
projectDAL,
projectMembershipDAL,
projectKeyDAL,
userGroupMembershipDAL,
projectBotDAL,
projectUserMembershipRoleDAL,
smtpService
}).addMembersToNonE2EEProject(
{
emails: [inviteeEmail],
usernames: [],
projectId,
projectMembershipRole: projectRoleSlug,
sendEmails: false
},
{
tx
}
);
}
}
}
return [{ ...inviteeUser, orgId }];
} }
const isEmailInvalid = await isDisposableEmail(inviteeEmail);
if (isEmailInvalid) { const inviteeUserId = inviteeUser?.id;
throw new BadRequestError({ const existingEncrytionKey = await userDAL.findUserEncKeyByUserId(inviteeUserId, tx);
message: "Provided a disposable email",
name: "Org invite" // when user is missing the encrytion keys
// this could happen either if user doesn't exist or user didn't find step 3 of generating the encryption keys of srp
// So what we do is we generate a random secure password and then encrypt it with a random pub-private key
// Then when user sign in (as login is not possible as isAccepted is false) we rencrypt the private key with the user password
if (!inviteeUser || (inviteeUser && !inviteeUser?.isAccepted && !existingEncrytionKey)) {
const serverGeneratedPassword = crypto.randomBytes(32).toString("hex");
const { tag, encoding, ciphertext, iv } = infisicalSymmetricEncypt(serverGeneratedPassword);
const encKeys = await generateUserSrpKeys(inviteeEmail, serverGeneratedPassword);
await userDAL.createUserEncryption(
{
userId: inviteeUserId,
encryptionVersion: 2,
protectedKey: encKeys.protectedKey,
protectedKeyIV: encKeys.protectedKeyIV,
protectedKeyTag: encKeys.protectedKeyTag,
publicKey: encKeys.publicKey,
encryptedPrivateKey: encKeys.encryptedPrivateKey,
iv: encKeys.encryptedPrivateKeyIV,
tag: encKeys.encryptedPrivateKeyTag,
salt: encKeys.salt,
verifier: encKeys.verifier,
serverEncryptedPrivateKeyEncoding: encoding,
serverEncryptedPrivateKeyTag: tag,
serverEncryptedPrivateKeyIV: iv,
serverEncryptedPrivateKey: ciphertext
},
tx
);
}
const [inviteeMembership] = await orgDAL.findMembership(
{
[`${TableName.OrgMembership}.orgId` as "orgId"]: orgId,
[`${TableName.OrgMembership}.userId` as "userId"]: inviteeUserId
},
{ tx }
);
// if there exist no org membership we set is as given by the request
if (!inviteeMembership) {
let roleId;
const orgRole = isCustomOrgRole ? OrgMembershipRole.Custom : organizationRoleSlug;
if (isCustomOrgRole) {
const customRole = await orgRoleDAL.findOne({ slug: organizationRoleSlug, orgId });
if (!customRole)
throw new BadRequestError({ name: "Invite membership", message: "Organization role not found" });
roleId = customRole.id;
}
await orgDAL.createMembership(
{
userId: inviteeUser.id,
inviteEmail: inviteeEmail,
orgId,
role: orgRole,
status: OrgMembershipStatus.Invited,
isActive: true,
roleId
},
tx
);
mailsForOrgInvitation.push({
email: inviteeEmail,
userId: inviteeUser.id,
firstName: inviteeUser?.firstName || "",
lastName: inviteeUser.lastName || ""
}); });
} }
// not invited before
const user = await userDAL.create(
{
username: inviteeEmail,
email: inviteeEmail,
isAccepted: false,
authMethods: [AuthMethod.EMAIL],
isGhost: false
},
tx
);
await orgDAL.createMembership(
{
inviteEmail: inviteeEmail,
orgId,
userId: user.id,
role: organizationRoleSlug,
status: OrgMembershipStatus.Invited,
isActive: true
},
tx
);
users.push({ users.push(inviteeUser);
...user, }
orgId
const userIds = users.map(({ id }) => id);
const usernames = users.map((el) => el.username);
const userEncryptionKeys = await userDAL.findUserEncKeyByUserIdsBatch({ userIds }, tx);
// we don't need to spam with email. Thus org invitation doesn't need project invitation again
const userIdsWithOrgInvitation = new Set(mailsForOrgInvitation.map((el) => el.userId));
// if there exist no project membership we set is as given by the request
for await (const project of projectsToInvite) {
const projectId = project.id;
const { permission: projectPermission } = await permissionService.getProjectPermission(
actor,
actorId,
projectId,
actorAuthMethod,
actorOrgId
);
ForbiddenError.from(projectPermission).throwUnlessCan(
ProjectPermissionActions.Create,
ProjectPermissionSub.Member
);
const existingMembers = await projectMembershipDAL.find(
{
projectId: project.id,
$in: { userId: userIds }
},
{ tx }
);
const existingMembersGroupByUserId = groupBy(existingMembers, (i) => i.userId);
const userIdsToExcludeAsPartOfGroup = new Set(
await userGroupMembershipDAL.findUserGroupMembershipsInProject(usernames, projectId, tx)
);
const userWithEncryptionKeyInvitedToProject = userEncryptionKeys.filter(
(user) => !existingMembersGroupByUserId?.[user.userId] && !userIdsToExcludeAsPartOfGroup.has(user.userId)
);
// eslint-disable-next-line no-continue
if (!userWithEncryptionKeyInvitedToProject.length) continue;
// validate custom project role
const invitedProjectRoles = invitedProjects.find((el) => el.id === project.id)?.projectRoleSlug || [
ProjectMembershipRole.Member
];
const customProjectRoles = invitedProjectRoles.filter(
(role) => !Object.values(ProjectMembershipRole).includes(role as ProjectMembershipRole)
);
const hasCustomRole = Boolean(customProjectRoles.length);
if (hasCustomRole) {
if (!plan?.rbac)
throw new BadRequestError({
message:
"Failed to assign custom role due to RBAC restriction. Upgrade plan to assign custom role to member."
});
}
const customRoles = hasCustomRole
? await projectRoleDAL.find({
projectId,
$in: { slug: customProjectRoles.map((role) => role) }
})
: [];
if (customRoles.length !== customProjectRoles.length)
throw new BadRequestError({ message: "Custom role not found" });
const customRolesGroupBySlug = groupBy(customRoles, ({ slug }) => slug);
const ghostUser = await projectDAL.findProjectGhostUser(projectId, tx);
if (!ghostUser) {
throw new BadRequestError({
message: "Failed to find sudo user"
});
}
const ghostUserLatestKey = await projectKeyDAL.findLatestProjectKey(ghostUser.id, projectId, tx);
if (!ghostUserLatestKey) {
throw new BadRequestError({
message: "Failed to find sudo user latest key"
});
}
const bot = await projectBotDAL.findOne({ projectId }, tx);
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 = assignWorkspaceKeysToMembers({
decryptKey: ghostUserLatestKey,
userPrivateKey: botPrivateKey,
members: userWithEncryptionKeyInvitedToProject.map((userEnc) => ({
orgMembershipId: userEnc.userId,
projectMembershipRole: ProjectMembershipRole.Admin,
userPublicKey: userEnc.publicKey
}))
});
const projectMemberships = await projectMembershipDAL.insertMany(
userWithEncryptionKeyInvitedToProject.map((userEnc) => ({
projectId,
userId: userEnc.userId
})),
tx
);
newProjectMemberships.push(...projectMemberships);
const sanitizedProjectMembershipRoles: TProjectUserMembershipRolesInsert[] = [];
invitedProjectRoles.forEach((projectRole) => {
const isCustomRole = Boolean(customRolesGroupBySlug?.[projectRole]?.[0]);
projectMemberships.forEach((membership) => {
sanitizedProjectMembershipRoles.push({
projectMembershipId: membership.id,
role: isCustomRole ? ProjectMembershipRole.Custom : projectRole,
customRoleId: customRolesGroupBySlug[projectRole] ? customRolesGroupBySlug[projectRole][0].id : null
});
});
});
await projectUserMembershipRoleDAL.insertMany(sanitizedProjectMembershipRoles, tx);
await projectKeyDAL.insertMany(
newWsMembers.map((el) => ({
encryptedKey: el.workspaceEncryptedKey,
nonce: el.workspaceEncryptedNonce,
senderId: ghostUser.id,
receiverId: el.orgMembershipId,
projectId
})),
tx
);
mailsForProjectInvitaion.push({
email: userWithEncryptionKeyInvitedToProject
.filter((el) => !userIdsWithOrgInvitation.has(el.userId))
.map((el) => el.email || el.username),
projectName: project.name
}); });
} }
return users; return users;
}); });
const user = await userDAL.findById(userId); await licenseService.updateSubscriptionOrgMemberCount(orgId);
const signupTokens: { email: string; link: string }[] = []; const signupTokens: { email: string; link: string }[] = [];
if (inviteeUsers) { // send org invite mail
for await (const invitee of inviteeUsers) { await Promise.allSettled(
mailsForOrgInvitation.map(async (el) => {
const token = await tokenService.createTokenForUser({ const token = await tokenService.createTokenForUser({
type: TokenType.TOKEN_EMAIL_ORG_INVITATION, type: TokenType.TOKEN_EMAIL_ORG_INVITATION,
userId: invitee.id, userId: el.userId,
orgId orgId
}); });
let inviteMetadata: string = "";
if (projectIds && projectIds?.length > 0) {
inviteMetadata = jwt.sign(
{
type: TokenMetadataType.InviteToProjects,
payload: {
projectIds,
projectRoleSlug: projectRoleSlug!, // Implicitly checked inside transaction if projectRoleSlug is undefined
userId: invitee.id,
orgId
}
} satisfies TTokenMetadata,
appCfg.AUTH_SECRET,
{
expiresIn: appCfg.JWT_INVITE_LIFETIME
}
);
}
signupTokens.push({ signupTokens.push({
email: invitee.email || invitee.username, email: el.email,
link: `${appCfg.SITE_URL}/signupinvite?token=${token}${ link: `${appCfg.SITE_URL}/signupinvite?token=${token}&to=${el.email}&organization_id=${org?.id}`
inviteMetadata ? `&metadata=${inviteMetadata}` : ""
}&to=${invitee.email || invitee.username}&organization_id=${org?.id}`
}); });
await smtpService.sendMail({ return smtpService.sendMail({
template: SmtpTemplates.OrgInvite, template: SmtpTemplates.OrgInvite,
subjectLine: "Infisical organization invitation", subjectLine: "Infisical organization invitation",
recipients: [invitee.email || invitee.username], recipients: [el.email],
substitutions: { substitutions: {
metadata: inviteMetadata, inviterFirstName: el.firstName,
inviterFirstName: user.firstName, inviterUsername: el.email,
inviterUsername: user.username,
organizationName: org?.name, organizationName: org?.name,
email: invitee.email || invitee.username, email: el.email,
organizationId: org?.id.toString(), organizationId: org?.id.toString(),
token, token,
callback_url: `${appCfg.SITE_URL}/signupinvite` callback_url: `${appCfg.SITE_URL}/signupinvite`
} }
}); });
} })
} );
await licenseService.updateSubscriptionOrgMemberCount(orgId);
await Promise.allSettled(
mailsForProjectInvitaion
.filter((el) => Boolean(el.email.length))
.map(async (el) => {
return smtpService.sendMail({
template: SmtpTemplates.WorkspaceInvite,
subjectLine: "Infisical project invitation",
recipients: el.email,
substitutions: {
workspaceName: el.projectName,
callback_url: `${appCfg.SITE_URL}/login`
}
});
})
);
if (!appCfg.isSmtpConfigured) { if (!appCfg.isSmtpConfigured) {
return signupTokens; return { signupTokens, projectMemberships: newProjectMemberships };
} }
return { signupTokens: undefined, projectMemberships: newProjectMemberships };
}; };
/** /**
+7 -5
View File
@@ -1,4 +1,3 @@
import { OrgMembershipRole, ProjectMembershipRole } from "@app/db/schemas";
import { TOrgPermission } from "@app/lib/types"; import { TOrgPermission } from "@app/lib/types";
import { ActorAuthMethod, ActorType } from "../auth/auth-type"; import { ActorAuthMethod, ActorType } from "../auth/auth-type";
@@ -26,14 +25,17 @@ export type TDeleteOrgMembershipDTO = {
}; };
export type TInviteUserToOrgDTO = { export type TInviteUserToOrgDTO = {
userId: string; actorId: string;
actor: ActorType;
orgId: string; orgId: string;
actorOrgId: string | undefined; actorOrgId: string | undefined;
actorAuthMethod: ActorAuthMethod; actorAuthMethod: ActorAuthMethod;
inviteeEmails: string[]; inviteeEmails: string[];
organizationRoleSlug: OrgMembershipRole; organizationRoleSlug: string;
projectIds?: string[]; projects?: {
projectRoleSlug?: ProjectMembershipRole; id: string;
projectRoleSlug?: string[];
}[];
}; };
export type TVerifyUserToOrgDTO = { export type TVerifyUserToOrgDTO = {
@@ -22,11 +22,9 @@ import { TProjectRoleDALFactory } from "../project-role/project-role-dal";
import { SmtpTemplates, TSmtpService } from "../smtp/smtp-service"; import { SmtpTemplates, TSmtpService } from "../smtp/smtp-service";
import { TUserDALFactory } from "../user/user-dal"; import { TUserDALFactory } from "../user/user-dal";
import { TProjectMembershipDALFactory } from "./project-membership-dal"; import { TProjectMembershipDALFactory } from "./project-membership-dal";
import { addMembersToProject } from "./project-membership-fns";
import { import {
ProjectUserMembershipTemporaryMode, ProjectUserMembershipTemporaryMode,
TAddUsersToWorkspaceDTO, TAddUsersToWorkspaceDTO,
TAddUsersToWorkspaceNonE2EEDTO,
TDeleteProjectMembershipOldDTO, TDeleteProjectMembershipOldDTO,
TDeleteProjectMembershipsDTO, TDeleteProjectMembershipsDTO,
TGetProjectMembershipByUsernameDTO, TGetProjectMembershipByUsernameDTO,
@@ -61,7 +59,6 @@ export const projectMembershipServiceFactory = ({
projectUserMembershipRoleDAL, projectUserMembershipRoleDAL,
smtpService, smtpService,
projectRoleDAL, projectRoleDAL,
projectBotDAL,
orgDAL, orgDAL,
projectUserAdditionalPrivilegeDAL, projectUserAdditionalPrivilegeDAL,
userDAL, userDAL,
@@ -214,52 +211,6 @@ export const projectMembershipServiceFactory = ({
return orgMembers; return orgMembers;
}; };
const addUsersToProjectNonE2EE = async ({
projectId,
actorId,
actorAuthMethod,
actor,
actorOrgId,
emails,
usernames,
sendEmails = true
}: TAddUsersToWorkspaceNonE2EEDTO) => {
const project = await projectDAL.findById(projectId);
if (!project) throw new BadRequestError({ message: "Project not found" });
if (project.version === ProjectVersion.V1) {
throw new BadRequestError({ message: "Please upgrade your project on your dashboard" });
}
const { permission } = await permissionService.getProjectPermission(
actor,
actorId,
projectId,
actorAuthMethod,
actorOrgId
);
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Create, ProjectPermissionSub.Member);
const members = await addMembersToProject({
orgDAL,
projectDAL,
projectMembershipDAL,
projectKeyDAL,
userGroupMembershipDAL,
projectBotDAL,
projectUserMembershipRoleDAL,
smtpService
}).addMembersToNonE2EEProject({
emails,
usernames,
projectId,
projectMembershipRole: ProjectMembershipRole.Member,
sendEmails
});
return members;
};
const updateProjectMembership = async ({ const updateProjectMembership = async ({
actorId, actorId,
actor, actor,
@@ -530,7 +481,6 @@ export const projectMembershipServiceFactory = ({
getProjectMemberships, getProjectMemberships,
getProjectMembershipByUsername, getProjectMembershipByUsername,
updateProjectMembership, updateProjectMembership,
addUsersToProjectNonE2EE,
deleteProjectMemberships, deleteProjectMemberships,
deleteProjectMembership, // TODO: Remove this deleteProjectMembership, // TODO: Remove this
addUsersToProject, addUsersToProject,
+2 -3
View File
@@ -82,10 +82,9 @@ export const userDALFactory = (db: TDbClient) => {
} }
}; };
const findUserEncKeyByUserId = async (userId: string) => { const findUserEncKeyByUserId = async (userId: string, tx?: Knex) => {
try { try {
const user = await db const user = await (tx || db.replicaNode())(TableName.Users)
.replicaNode()(TableName.Users)
.where(`${TableName.Users}.id`, userId) .where(`${TableName.Users}.id`, userId)
.join(TableName.UserEncryptionKey, `${TableName.Users}.id`, `${TableName.UserEncryptionKey}.userId`) .join(TableName.UserEncryptionKey, `${TableName.Users}.id`, `${TableName.UserEncryptionKey}.userId`)
.first(); .first();
+5 -9
View File
@@ -4,18 +4,14 @@ import { alphaNumericNanoId } from "@app/lib/nanoid";
import { TUserDALFactory } from "@app/services/user/user-dal"; import { TUserDALFactory } from "@app/services/user/user-dal";
export const normalizeUsername = async (username: string, userDAL: Pick<TUserDALFactory, "findOne">) => { export const normalizeUsername = async (username: string, userDAL: Pick<TUserDALFactory, "findOne">) => {
let attempt = slugify(`${username}-${alphaNumericNanoId(4)}`); let attempt: string;
let user;
let user = await userDAL.findOne({ username: attempt }); do {
if (!user) return attempt;
while (true) {
attempt = slugify(`${username}-${alphaNumericNanoId(4)}`); attempt = slugify(`${username}-${alphaNumericNanoId(4)}`);
// eslint-disable-next-line no-await-in-loop // eslint-disable-next-line no-await-in-loop
user = await userDAL.findOne({ username: attempt }); user = await userDAL.findOne({ username: attempt });
} while (user);
if (!user) { return attempt;
return attempt;
}
}
}; };
+4
View File
@@ -0,0 +1,4 @@
export enum UserEncryption {
V1 = 1,
V2 = 2
}
+1 -2
View File
@@ -150,8 +150,7 @@ export type DeletOrgMembershipDTO = {
export type AddUserToOrgDTO = { export type AddUserToOrgDTO = {
inviteeEmails: string[]; inviteeEmails: string[];
projectIds?: string[]; projects?: { id: string; projectRoleSlug: string[] }[];
projectRoleSlug?: string;
organizationRoleSlug: string; organizationRoleSlug: string;
organizationId: string; organizationId: string;
}; };
@@ -129,8 +129,7 @@ export const AddOrgMemberModal = ({
organizationId: currentOrg?.id, organizationId: currentOrg?.id,
inviteeEmails: emails.split(",").map((email) => email.trim()), inviteeEmails: emails.split(",").map((email) => email.trim()),
organizationRoleSlug, organizationRoleSlug,
projectIds, projects: projectIds.map((id) => ({ id, projectRoleSlug: [projectRoleSlug] }))
projectRoleSlug
}); });
setCompleteInviteLinks(data?.completeInviteLinks ?? null); setCompleteInviteLinks(data?.completeInviteLinks ?? null);
@@ -101,9 +101,7 @@ export const AddMemberModal = ({ popUp, handlePopUpToggle }: Props) => {
members?.forEach((member) => { members?.forEach((member) => {
wsUserUsernames.set(member.user.username, true); wsUserUsernames.set(member.user.username, true);
}); });
return (orgUsers || []).filter( return (orgUsers || []).filter(({ user: u }) => !wsUserUsernames.has(u.username));
({ status, user: u }) => status === "accepted" && !wsUserUsernames.has(u.username)
);
}, [orgUsers, members]); }, [orgUsers, members]);
return ( return (
@@ -1,10 +1,11 @@
import { useMemo,useState } from "react"; import { useMemo, useState } from "react";
import { import {
faClock, faClock,
faEdit, faEdit,
faMagnifyingGlass, faMagnifyingGlass,
faTrash, faTrash,
faUsers} from "@fortawesome/free-solid-svg-icons"; faUsers
} from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { twMerge } from "tailwind-merge"; import { twMerge } from "tailwind-merge";
@@ -27,12 +28,14 @@ import {
Th, Th,
THead, THead,
Tooltip, Tooltip,
Tr} from "@app/components/v2"; Tr
} from "@app/components/v2";
import { import {
ProjectPermissionActions, ProjectPermissionActions,
ProjectPermissionSub, ProjectPermissionSub,
useUser, useUser,
useWorkspace} from "@app/context"; useWorkspace
} from "@app/context";
import { useGetWorkspaceUsers } from "@app/hooks/api"; import { useGetWorkspaceUsers } from "@app/hooks/api";
import { ProjectMembershipRole } from "@app/hooks/api/roles/types"; import { ProjectMembershipRole } from "@app/hooks/api/roles/types";
import { TWorkspaceUser } from "@app/hooks/api/types"; import { TWorkspaceUser } from "@app/hooks/api/types";
@@ -104,7 +107,7 @@ export const MembersTable = ({ popUp, handlePopUpOpen, handlePopUpToggle }: Prop
{!isMembersLoading && {!isMembersLoading &&
filterdUsers?.map((projectMember, index) => { filterdUsers?.map((projectMember, index) => {
const { user: u, inviteEmail, id: membershipId, roles } = projectMember; const { user: u, inviteEmail, id: membershipId, roles } = projectMember;
const name = u ? `${u.firstName} ${u.lastName}` : "-"; const name = u.firstName || u.lastName ? `${u.firstName} ${u.lastName || ""}` : "-";
const email = u?.email || inviteEmail; const email = u?.email || inviteEmail;
return ( return (