mirror of
https://github.com/awatertrevi/infisical.git
synced 2026-10-15 10:35:17 +00:00
Fix merge conflicts
This commit is contained in:
@@ -1,15 +1,15 @@
|
||||
import bcrypt from "bcrypt";
|
||||
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 { TUserGroupMembershipDALFactory } from "@app/ee/services/group/user-group-membership-dal";
|
||||
import { TLicenseServiceFactory } from "@app/ee/services/license/license-service";
|
||||
import { isAuthMethodSaml } from "@app/ee/services/permission/permission-fns";
|
||||
import { getConfig } from "@app/lib/config/env";
|
||||
import { infisicalSymmetricEncypt } from "@app/lib/crypto/encryption";
|
||||
import { getUserPrivateKey } from "@app/lib/crypto/srp";
|
||||
import { BadRequestError, UnauthorizedError } from "@app/lib/errors";
|
||||
import { infisicalSymmetricDecrypt, infisicalSymmetricEncypt } from "@app/lib/crypto/encryption";
|
||||
import { generateUserSrpKeys, getUserPrivateKey } from "@app/lib/crypto/srp";
|
||||
import { BadRequestError } from "@app/lib/errors";
|
||||
import { isDisposableEmail } from "@app/lib/validator";
|
||||
import { TGroupProjectDALFactory } from "@app/services/group-project/group-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 { 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 { TOrgServiceFactory } from "../org/org-service";
|
||||
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 { SmtpTemplates, TSmtpService } from "../smtp/smtp-service";
|
||||
import { TUserDALFactory } from "../user/user-dal";
|
||||
import { UserEncryption } from "../user/user-types";
|
||||
import { TAuthDALFactory } from "./auth-dal";
|
||||
import { validateProviderAuthToken, validateSignUpAuthorization } from "./auth-fns";
|
||||
import { TCompleteAccountInviteDTO, TCompleteAccountSignupDTO } from "./auth-signup-type";
|
||||
@@ -67,8 +67,6 @@ export const authSignupServiceFactory = ({
|
||||
smtpService,
|
||||
orgService,
|
||||
orgDAL,
|
||||
projectMembershipDAL,
|
||||
projectUserMembershipRoleDAL,
|
||||
licenseService
|
||||
}: TAuthSignupDep) => {
|
||||
// first step of signup. create user and send email
|
||||
@@ -177,32 +175,88 @@ export const authSignupServiceFactory = ({
|
||||
encryptedPrivateKey,
|
||||
iv: encryptedPrivateKeyIV,
|
||||
tag: encryptedPrivateKeyTag,
|
||||
encryptionVersion: 2
|
||||
encryptionVersion: UserEncryption.V2
|
||||
});
|
||||
const { tag, encoding, ciphertext, iv } = infisicalSymmetricEncypt(privateKey);
|
||||
const updateduser = await authDAL.transaction(async (tx) => {
|
||||
const us = await userDAL.updateById(user.id, { firstName, lastName, isAccepted: true }, tx);
|
||||
if (!us) throw new Error("User not found");
|
||||
const userEncKey = await userDAL.upsertUserEncryptionKey(
|
||||
us.id,
|
||||
{
|
||||
salt,
|
||||
verifier,
|
||||
publicKey,
|
||||
protectedKey,
|
||||
protectedKeyIV,
|
||||
protectedKeyTag,
|
||||
encryptedPrivateKey,
|
||||
iv: encryptedPrivateKeyIV,
|
||||
tag: encryptedPrivateKeyTag,
|
||||
hashedPassword,
|
||||
serverEncryptedPrivateKeyEncoding: encoding,
|
||||
serverEncryptedPrivateKeyTag: tag,
|
||||
serverEncryptedPrivateKeyIV: iv,
|
||||
serverEncryptedPrivateKey: ciphertext
|
||||
},
|
||||
tx
|
||||
);
|
||||
const systemGeneratedUserEncryptionKey = await userDAL.findUserEncKeyByUserId(us.id, tx);
|
||||
let userEncKey;
|
||||
|
||||
// below condition is true means this is system generated credentials
|
||||
// the private key is actually system generated password
|
||||
// thus we will re-encrypt the system generated private key with the new password
|
||||
// akhilmhdh: you may find this like why? The reason is simple we are moving away from e2ee and these are pieces of it
|
||||
// without a dummy key in place some things will break and backward compatiability too. 2025 we will be removing all these things
|
||||
if (
|
||||
systemGeneratedUserEncryptionKey &&
|
||||
!systemGeneratedUserEncryptionKey.hashedPassword &&
|
||||
systemGeneratedUserEncryptionKey.serverEncryptedPrivateKey &&
|
||||
systemGeneratedUserEncryptionKey.serverEncryptedPrivateKeyTag &&
|
||||
systemGeneratedUserEncryptionKey.serverEncryptedPrivateKeyIV &&
|
||||
systemGeneratedUserEncryptionKey.serverEncryptedPrivateKeyEncoding
|
||||
) {
|
||||
// get server generated password
|
||||
const serverGeneratedPassword = infisicalSymmetricDecrypt({
|
||||
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 (
|
||||
(isAuthMethodSaml(authMethod) || [AuthMethod.LDAP, AuthMethod.OIDC].includes(authMethod as AuthMethod)) &&
|
||||
@@ -312,8 +366,7 @@ export const authSignupServiceFactory = ({
|
||||
encryptedPrivateKey,
|
||||
encryptedPrivateKeyIV,
|
||||
encryptedPrivateKeyTag,
|
||||
authorization,
|
||||
tokenMetadata
|
||||
authorization
|
||||
}: TCompleteAccountInviteDTO) => {
|
||||
const user = await userDAL.findUserByUsername(email);
|
||||
if (!user || (user && user.isAccepted)) {
|
||||
@@ -348,65 +401,76 @@ export const authSignupServiceFactory = ({
|
||||
const updateduser = await authDAL.transaction(async (tx) => {
|
||||
const us = await userDAL.updateById(user.id, { firstName, lastName, isAccepted: true }, tx);
|
||||
if (!us) throw new Error("User not found");
|
||||
const userEncKey = await userDAL.upsertUserEncryptionKey(
|
||||
us.id,
|
||||
{
|
||||
salt,
|
||||
encryptionVersion: 2,
|
||||
verifier,
|
||||
publicKey,
|
||||
protectedKey,
|
||||
protectedKeyIV,
|
||||
protectedKeyTag,
|
||||
encryptedPrivateKey,
|
||||
iv: encryptedPrivateKeyIV,
|
||||
tag: encryptedPrivateKeyTag,
|
||||
hashedPassword,
|
||||
serverEncryptedPrivateKeyEncoding: encoding,
|
||||
serverEncryptedPrivateKeyTag: tag,
|
||||
serverEncryptedPrivateKeyIV: iv,
|
||||
serverEncryptedPrivateKey: ciphertext
|
||||
},
|
||||
tx
|
||||
);
|
||||
|
||||
if (tokenMetadata) {
|
||||
const metadataObj = jwt.verify(tokenMetadata, appCfg.AUTH_SECRET) as TTokenMetadata;
|
||||
|
||||
if (
|
||||
metadataObj?.payload?.userId !== user.id ||
|
||||
metadataObj?.payload?.orgId !== orgMembership.orgId ||
|
||||
metadataObj?.type !== TokenMetadataType.InviteToProjects
|
||||
) {
|
||||
throw new UnauthorizedError({
|
||||
message: "Malformed or invalid metadata token"
|
||||
});
|
||||
}
|
||||
|
||||
for await (const projectId of metadataObj.payload.projectIds) {
|
||||
await addMembersToProject({
|
||||
orgDAL,
|
||||
projectDAL,
|
||||
projectMembershipDAL,
|
||||
projectKeyDAL,
|
||||
userGroupMembershipDAL,
|
||||
projectBotDAL,
|
||||
projectUserMembershipRoleDAL,
|
||||
smtpService
|
||||
}).addMembersToNonE2EEProject(
|
||||
{
|
||||
emails: [user.email!],
|
||||
usernames: [],
|
||||
projectId,
|
||||
projectMembershipRole: metadataObj.payload.projectRoleSlug,
|
||||
sendEmails: false
|
||||
},
|
||||
{
|
||||
tx,
|
||||
throwOnProjectNotFound: false
|
||||
}
|
||||
);
|
||||
}
|
||||
const systemGeneratedUserEncryptionKey = await userDAL.findUserEncKeyByUserId(us.id, tx);
|
||||
let userEncKey;
|
||||
// this means this is system generated credentials
|
||||
// now replace the private key
|
||||
if (
|
||||
systemGeneratedUserEncryptionKey &&
|
||||
!systemGeneratedUserEncryptionKey.hashedPassword &&
|
||||
systemGeneratedUserEncryptionKey.serverEncryptedPrivateKey &&
|
||||
systemGeneratedUserEncryptionKey.serverEncryptedPrivateKeyTag &&
|
||||
systemGeneratedUserEncryptionKey.serverEncryptedPrivateKeyIV &&
|
||||
systemGeneratedUserEncryptionKey.serverEncryptedPrivateKeyEncoding
|
||||
) {
|
||||
// get server generated password
|
||||
const serverGeneratedPassword = infisicalSymmetricDecrypt({
|
||||
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: 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,
|
||||
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
|
||||
);
|
||||
}
|
||||
|
||||
const updatedMembersips = await orgDAL.updateMembership(
|
||||
|
||||
@@ -208,6 +208,23 @@ export const kmsServiceFactory = ({
|
||||
return org.kmsDefaultKeyId;
|
||||
};
|
||||
|
||||
const encryptWithRootKey = async () => {
|
||||
const cipher = symmetricCipherService(SymmetricEncryption.AES_GCM_256);
|
||||
return ({ plainText }: { plainText: Buffer }) => {
|
||||
const encryptedPlainTextBlob = cipher.encrypt(plainText, ROOT_ENCRYPTION_KEY);
|
||||
|
||||
return Promise.resolve({ cipherTextBlob: encryptedPlainTextBlob });
|
||||
};
|
||||
};
|
||||
|
||||
const decryptWithRootKey = async () => {
|
||||
const cipher = symmetricCipherService(SymmetricEncryption.AES_GCM_256);
|
||||
return ({ cipherTextBlob }: { cipherTextBlob: Buffer }) => {
|
||||
const decryptedBlob = cipher.decrypt(cipherTextBlob, ROOT_ENCRYPTION_KEY);
|
||||
return Promise.resolve(decryptedBlob);
|
||||
};
|
||||
};
|
||||
|
||||
const decryptWithKmsKey = async ({
|
||||
kmsId,
|
||||
depth = 0
|
||||
@@ -808,6 +825,8 @@ export const kmsServiceFactory = ({
|
||||
decryptWithKmsKey,
|
||||
encryptWithInputKey,
|
||||
decryptWithInputKey,
|
||||
encryptWithRootKey,
|
||||
decryptWithRootKey,
|
||||
getOrgKmsKeyId,
|
||||
getProjectSecretManagerKmsKeyId,
|
||||
updateProjectSecretManagerKmsKey,
|
||||
|
||||
@@ -9,7 +9,10 @@ import {
|
||||
OrgMembershipStatus,
|
||||
ProjectMembershipRole,
|
||||
ProjectVersion,
|
||||
SecretKeyEncoding,
|
||||
TableName,
|
||||
TProjectMemberships,
|
||||
TProjectUserMembershipRolesInsert,
|
||||
TUsers
|
||||
} from "@app/db/schemas";
|
||||
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 { OrgPermissionActions, OrgPermissionSubjects } from "@app/ee/services/permission/org-permission";
|
||||
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service";
|
||||
import { ProjectPermissionActions, ProjectPermissionSub } from "@app/ee/services/permission/project-permission";
|
||||
import { 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 { getConfig } from "@app/lib/config/env";
|
||||
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 { BadRequestError, NotFoundError, UnauthorizedError } from "@app/lib/errors";
|
||||
import { groupBy } from "@app/lib/fn";
|
||||
import { alphaNumericNanoId } from "@app/lib/nanoid";
|
||||
import { isDisposableEmail } from "@app/lib/validator";
|
||||
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 { 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 { verifyProjectVersions } from "../project/project-fns";
|
||||
import { assignWorkspaceKeysToMembers } from "../project/project-fns";
|
||||
import { TProjectBotDALFactory } from "../project-bot/project-bot-dal";
|
||||
import { TProjectKeyDALFactory } from "../project-key/project-key-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 { TProjectRoleDALFactory } from "../project-role/project-role-dal";
|
||||
import { SmtpTemplates, TSmtpService } from "../smtp/smtp-service";
|
||||
import { TUserDALFactory } from "../user/user-dal";
|
||||
import { TIncidentContactsDALFactory } from "./incident-contacts-dal";
|
||||
@@ -84,6 +89,7 @@ type TOrgServiceFactoryDep = {
|
||||
"getPlan" | "updateSubscriptionOrgMemberCount" | "generateOrgCustomerId" | "removeOrgCustomer"
|
||||
>;
|
||||
projectUserAdditionalPrivilegeDAL: Pick<TProjectUserAdditionalPrivilegeDALFactory, "delete">;
|
||||
projectRoleDAL: Pick<TProjectRoleDALFactory, "find">;
|
||||
userGroupMembershipDAL: Pick<TUserGroupMembershipDALFactory, "findUserGroupMembershipsInProject">;
|
||||
projectBotDAL: Pick<TProjectBotDALFactory, "findOne">;
|
||||
projectUserMembershipRoleDAL: Pick<TProjectUserMembershipRoleDALFactory, "insertMany">;
|
||||
@@ -111,7 +117,8 @@ export const orgServiceFactory = ({
|
||||
samlConfigDAL,
|
||||
userGroupMembershipDAL,
|
||||
projectBotDAL,
|
||||
projectUserMembershipRoleDAL
|
||||
projectUserMembershipRoleDAL,
|
||||
projectRoleDAL
|
||||
}: TOrgServiceFactoryDep) => {
|
||||
/*
|
||||
* Get organization details by the organization id
|
||||
@@ -440,17 +447,17 @@ export const orgServiceFactory = ({
|
||||
*/
|
||||
const inviteUserToOrganization = async ({
|
||||
orgId,
|
||||
userId,
|
||||
actorId,
|
||||
actor,
|
||||
inviteeEmails,
|
||||
organizationRoleSlug,
|
||||
projectRoleSlug,
|
||||
projectIds,
|
||||
projects: invitedProjects,
|
||||
actorAuthMethod,
|
||||
actorOrgId
|
||||
}: TInviteUserToOrgDTO) => {
|
||||
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);
|
||||
|
||||
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);
|
||||
if (plan?.memberLimit && plan.membersUsed >= plan.memberLimit) {
|
||||
// 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."
|
||||
});
|
||||
}
|
||||
|
||||
if (projectIds?.length) {
|
||||
const projects = await projectDAL.find({
|
||||
orgId,
|
||||
$in: {
|
||||
id: projectIds
|
||||
}
|
||||
});
|
||||
|
||||
// if its not v3, throw an error
|
||||
if (!verifyProjectVersions(projects, ProjectVersion.V3)) {
|
||||
const isCustomOrgRole = !Object.values(OrgMembershipRole).includes(organizationRoleSlug as OrgMembershipRole);
|
||||
if (isCustomOrgRole) {
|
||||
if (!plan?.rbac)
|
||||
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 users: Pick<
|
||||
TUsers & { orgId: string },
|
||||
"id" | "firstName" | "lastName" | "email" | "orgId" | "username"
|
||||
>[] = [];
|
||||
const projectsToInvite = invitedProjects?.length
|
||||
? await projectDAL.find({
|
||||
orgId,
|
||||
$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) {
|
||||
const inviteeUser = await userDAL.findUserByUsername(inviteeEmail, tx);
|
||||
let inviteeUser = await userDAL.findUserByUsername(inviteeEmail, tx);
|
||||
|
||||
if (inviteeUser) {
|
||||
// if user already exist means its already part of infisical
|
||||
// Thus the signup flow is not needed anymore
|
||||
const [inviteeMembership] = await orgDAL.findMembership(
|
||||
// if the user doesn't exist we create the user with the email
|
||||
if (!inviteeUser) {
|
||||
inviteeUser = await userDAL.create(
|
||||
{
|
||||
[`${TableName.OrgMembership}.orgId` as "orgId"]: orgId,
|
||||
[`${TableName.OrgMembership}.userId` as "userId"]: inviteeUser.id
|
||||
isAccepted: false,
|
||||
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) {
|
||||
throw new BadRequestError({
|
||||
message: "Provided a disposable email",
|
||||
name: "Org invite"
|
||||
|
||||
const inviteeUserId = inviteeUser?.id;
|
||||
const existingEncrytionKey = await userDAL.findUserEncKeyByUserId(inviteeUserId, tx);
|
||||
|
||||
// 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({
|
||||
...user,
|
||||
orgId
|
||||
users.push(inviteeUser);
|
||||
}
|
||||
|
||||
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;
|
||||
});
|
||||
|
||||
const user = await userDAL.findById(userId);
|
||||
|
||||
await licenseService.updateSubscriptionOrgMemberCount(orgId);
|
||||
const signupTokens: { email: string; link: string }[] = [];
|
||||
if (inviteeUsers) {
|
||||
for await (const invitee of inviteeUsers) {
|
||||
// send org invite mail
|
||||
await Promise.allSettled(
|
||||
mailsForOrgInvitation.map(async (el) => {
|
||||
const token = await tokenService.createTokenForUser({
|
||||
type: TokenType.TOKEN_EMAIL_ORG_INVITATION,
|
||||
userId: invitee.id,
|
||||
userId: el.userId,
|
||||
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({
|
||||
email: invitee.email || invitee.username,
|
||||
link: `${appCfg.SITE_URL}/signupinvite?token=${token}${
|
||||
inviteMetadata ? `&metadata=${inviteMetadata}` : ""
|
||||
}&to=${invitee.email || invitee.username}&organization_id=${org?.id}`
|
||||
email: el.email,
|
||||
link: `${appCfg.SITE_URL}/signupinvite?token=${token}&to=${el.email}&organization_id=${org?.id}`
|
||||
});
|
||||
|
||||
await smtpService.sendMail({
|
||||
return smtpService.sendMail({
|
||||
template: SmtpTemplates.OrgInvite,
|
||||
subjectLine: "Infisical organization invitation",
|
||||
recipients: [invitee.email || invitee.username],
|
||||
recipients: [el.email],
|
||||
substitutions: {
|
||||
metadata: inviteMetadata,
|
||||
inviterFirstName: user.firstName,
|
||||
inviterUsername: user.username,
|
||||
inviterFirstName: el.firstName,
|
||||
inviterUsername: el.email,
|
||||
organizationName: org?.name,
|
||||
email: invitee.email || invitee.username,
|
||||
email: el.email,
|
||||
organizationId: org?.id.toString(),
|
||||
token,
|
||||
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) {
|
||||
return signupTokens;
|
||||
return { signupTokens, projectMemberships: newProjectMemberships };
|
||||
}
|
||||
|
||||
return { signupTokens: undefined, projectMemberships: newProjectMemberships };
|
||||
};
|
||||
|
||||
/**
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import { OrgMembershipRole, ProjectMembershipRole } from "@app/db/schemas";
|
||||
import { TOrgPermission } from "@app/lib/types";
|
||||
|
||||
import { ActorAuthMethod, ActorType } from "../auth/auth-type";
|
||||
@@ -26,14 +25,17 @@ export type TDeleteOrgMembershipDTO = {
|
||||
};
|
||||
|
||||
export type TInviteUserToOrgDTO = {
|
||||
userId: string;
|
||||
actorId: string;
|
||||
actor: ActorType;
|
||||
orgId: string;
|
||||
actorOrgId: string | undefined;
|
||||
actorAuthMethod: ActorAuthMethod;
|
||||
inviteeEmails: string[];
|
||||
organizationRoleSlug: OrgMembershipRole;
|
||||
projectIds?: string[];
|
||||
projectRoleSlug?: ProjectMembershipRole;
|
||||
organizationRoleSlug: string;
|
||||
projects?: {
|
||||
id: string;
|
||||
projectRoleSlug?: string[];
|
||||
}[];
|
||||
};
|
||||
|
||||
export type TVerifyUserToOrgDTO = {
|
||||
|
||||
@@ -22,11 +22,9 @@ import { TProjectRoleDALFactory } from "../project-role/project-role-dal";
|
||||
import { SmtpTemplates, TSmtpService } from "../smtp/smtp-service";
|
||||
import { TUserDALFactory } from "../user/user-dal";
|
||||
import { TProjectMembershipDALFactory } from "./project-membership-dal";
|
||||
import { addMembersToProject } from "./project-membership-fns";
|
||||
import {
|
||||
ProjectUserMembershipTemporaryMode,
|
||||
TAddUsersToWorkspaceDTO,
|
||||
TAddUsersToWorkspaceNonE2EEDTO,
|
||||
TDeleteProjectMembershipOldDTO,
|
||||
TDeleteProjectMembershipsDTO,
|
||||
TGetProjectMembershipByUsernameDTO,
|
||||
@@ -61,7 +59,6 @@ export const projectMembershipServiceFactory = ({
|
||||
projectUserMembershipRoleDAL,
|
||||
smtpService,
|
||||
projectRoleDAL,
|
||||
projectBotDAL,
|
||||
orgDAL,
|
||||
projectUserAdditionalPrivilegeDAL,
|
||||
userDAL,
|
||||
@@ -214,52 +211,6 @@ export const projectMembershipServiceFactory = ({
|
||||
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 ({
|
||||
actorId,
|
||||
actor,
|
||||
@@ -530,7 +481,6 @@ export const projectMembershipServiceFactory = ({
|
||||
getProjectMemberships,
|
||||
getProjectMembershipByUsername,
|
||||
updateProjectMembership,
|
||||
addUsersToProjectNonE2EE,
|
||||
deleteProjectMemberships,
|
||||
deleteProjectMembership, // TODO: Remove this
|
||||
addUsersToProject,
|
||||
|
||||
@@ -34,6 +34,8 @@ import { TProjectUserMembershipRoleDALFactory } from "../project-membership/proj
|
||||
import { TProjectRoleDALFactory } from "../project-role/project-role-dal";
|
||||
import { getPredefinedRoles } from "../project-role/project-role-fns";
|
||||
import { ROOT_FOLDER_NAME, TSecretFolderDALFactory } from "../secret-folder/secret-folder-dal";
|
||||
import { TProjectSlackConfigDALFactory } from "../slack/project-slack-config-dal";
|
||||
import { TSlackIntegrationDALFactory } from "../slack/slack-integration-dal";
|
||||
import { TUserDALFactory } from "../user/user-dal";
|
||||
import { TProjectDALFactory } from "./project-dal";
|
||||
import { assignWorkspaceKeysToMembers, createProjectKey } from "./project-fns";
|
||||
@@ -43,6 +45,7 @@ import {
|
||||
TDeleteProjectDTO,
|
||||
TGetProjectDTO,
|
||||
TGetProjectKmsKey,
|
||||
TGetProjectSlackConfig,
|
||||
TListProjectAlertsDTO,
|
||||
TListProjectCasDTO,
|
||||
TListProjectCertificateTemplatesDTO,
|
||||
@@ -54,6 +57,7 @@ import {
|
||||
TUpdateProjectDTO,
|
||||
TUpdateProjectKmsDTO,
|
||||
TUpdateProjectNameDTO,
|
||||
TUpdateProjectSlackConfig,
|
||||
TUpdateProjectVersionLimitDTO,
|
||||
TUpgradeProjectDTO
|
||||
} from "./project-types";
|
||||
@@ -76,6 +80,8 @@ type TProjectServiceFactoryDep = {
|
||||
identityProjectMembershipRoleDAL: Pick<TIdentityProjectMembershipRoleDALFactory, "create">;
|
||||
projectKeyDAL: Pick<TProjectKeyDALFactory, "create" | "findLatestProjectKey" | "delete" | "find" | "insertMany">;
|
||||
projectMembershipDAL: Pick<TProjectMembershipDALFactory, "create" | "findProjectGhostUser" | "findOne">;
|
||||
projectSlackConfigDAL: Pick<TProjectSlackConfigDALFactory, "findOne" | "transaction" | "updateById" | "create">;
|
||||
slackIntegrationDAL: Pick<TSlackIntegrationDALFactory, "findById" | "findByIdWithWorkflowIntegrationDetails">;
|
||||
projectUserMembershipRoleDAL: Pick<TProjectUserMembershipRoleDALFactory, "create">;
|
||||
certificateAuthorityDAL: Pick<TCertificateAuthorityDALFactory, "find">;
|
||||
certificateDAL: Pick<TCertificateDALFactory, "find" | "countCertificatesInProject">;
|
||||
@@ -126,7 +132,9 @@ export const projectServiceFactory = ({
|
||||
pkiAlertDAL,
|
||||
keyStore,
|
||||
kmsService,
|
||||
projectBotDAL
|
||||
projectBotDAL,
|
||||
projectSlackConfigDAL,
|
||||
slackIntegrationDAL
|
||||
}: TProjectServiceFactoryDep) => {
|
||||
/*
|
||||
* Create workspace. Make user the admin
|
||||
@@ -909,6 +917,113 @@ export const projectServiceFactory = ({
|
||||
return { secretManagerKmsKey: kmsKey };
|
||||
};
|
||||
|
||||
const getProjectSlackConfig = async ({
|
||||
actorId,
|
||||
actor,
|
||||
actorOrgId,
|
||||
actorAuthMethod,
|
||||
projectId
|
||||
}: TGetProjectSlackConfig) => {
|
||||
const project = await projectDAL.findById(projectId);
|
||||
if (!project) {
|
||||
throw new NotFoundError({
|
||||
message: "Project not found"
|
||||
});
|
||||
}
|
||||
|
||||
const { permission } = await permissionService.getProjectPermission(
|
||||
actor,
|
||||
actorId,
|
||||
projectId,
|
||||
actorAuthMethod,
|
||||
actorOrgId
|
||||
);
|
||||
|
||||
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.Settings);
|
||||
|
||||
return projectSlackConfigDAL.findOne({
|
||||
projectId: project.id
|
||||
});
|
||||
};
|
||||
|
||||
const updateProjectSlackConfig = async ({
|
||||
actorId,
|
||||
actor,
|
||||
actorOrgId,
|
||||
actorAuthMethod,
|
||||
projectId,
|
||||
slackIntegrationId,
|
||||
isAccessRequestNotificationEnabled,
|
||||
accessRequestChannels,
|
||||
isSecretRequestNotificationEnabled,
|
||||
secretRequestChannels
|
||||
}: TUpdateProjectSlackConfig) => {
|
||||
const project = await projectDAL.findById(projectId);
|
||||
if (!project) {
|
||||
throw new NotFoundError({
|
||||
message: "Project not found"
|
||||
});
|
||||
}
|
||||
|
||||
const slackIntegration = await slackIntegrationDAL.findByIdWithWorkflowIntegrationDetails(slackIntegrationId);
|
||||
if (!slackIntegration) {
|
||||
throw new NotFoundError({
|
||||
message: "Slack integration not found"
|
||||
});
|
||||
}
|
||||
|
||||
const { permission } = await permissionService.getProjectPermission(
|
||||
actor,
|
||||
actorId,
|
||||
projectId,
|
||||
actorAuthMethod,
|
||||
actorOrgId
|
||||
);
|
||||
|
||||
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Edit, ProjectPermissionSub.Settings);
|
||||
|
||||
if (slackIntegration.orgId !== project.orgId) {
|
||||
throw new BadRequestError({
|
||||
message: "Selected slack integration is not in the same organization"
|
||||
});
|
||||
}
|
||||
|
||||
return projectSlackConfigDAL.transaction(async (tx) => {
|
||||
const slackConfig = await projectSlackConfigDAL.findOne(
|
||||
{
|
||||
projectId
|
||||
},
|
||||
tx
|
||||
);
|
||||
|
||||
if (slackConfig) {
|
||||
return projectSlackConfigDAL.updateById(
|
||||
slackConfig.id,
|
||||
{
|
||||
slackIntegrationId,
|
||||
isAccessRequestNotificationEnabled,
|
||||
accessRequestChannels,
|
||||
isSecretRequestNotificationEnabled,
|
||||
secretRequestChannels
|
||||
},
|
||||
tx
|
||||
);
|
||||
}
|
||||
|
||||
return projectSlackConfigDAL.create(
|
||||
{
|
||||
projectId,
|
||||
slackIntegrationId,
|
||||
isAccessRequestNotificationEnabled,
|
||||
accessRequestChannels,
|
||||
isSecretRequestNotificationEnabled,
|
||||
secretRequestChannels
|
||||
},
|
||||
tx
|
||||
);
|
||||
});
|
||||
};
|
||||
|
||||
return {
|
||||
createProject,
|
||||
deleteProject,
|
||||
@@ -929,6 +1044,8 @@ export const projectServiceFactory = ({
|
||||
updateProjectKmsKey,
|
||||
getProjectKmsBackup,
|
||||
loadProjectKmsBackup,
|
||||
getProjectKmsKeys
|
||||
getProjectKmsKeys,
|
||||
getProjectSlackConfig,
|
||||
updateProjectSlackConfig
|
||||
};
|
||||
};
|
||||
|
||||
@@ -123,3 +123,13 @@ export type TLoadProjectKmsBackupDTO = {
|
||||
export type TGetProjectKmsKey = TProjectPermission;
|
||||
|
||||
export type TListProjectCertificateTemplatesDTO = TProjectPermission;
|
||||
|
||||
export type TGetProjectSlackConfig = TProjectPermission;
|
||||
|
||||
export type TUpdateProjectSlackConfig = {
|
||||
slackIntegrationId: string;
|
||||
isAccessRequestNotificationEnabled: boolean;
|
||||
accessRequestChannels: string;
|
||||
isSecretRequestNotificationEnabled: boolean;
|
||||
secretRequestChannels: string;
|
||||
} & TProjectPermission;
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
import { Knex } from "knex";
|
||||
|
||||
import { TDbClient } from "@app/db";
|
||||
import { TableName } from "@app/db/schemas";
|
||||
import { ormify, selectAllTableCols } from "@app/lib/knex";
|
||||
|
||||
export type TProjectSlackConfigDALFactory = ReturnType<typeof projectSlackConfigDALFactory>;
|
||||
|
||||
export const projectSlackConfigDALFactory = (db: TDbClient) => {
|
||||
const projectSlackConfigOrm = ormify(db, TableName.ProjectSlackConfigs);
|
||||
|
||||
const getIntegrationDetailsByProject = (projectId: string, tx?: Knex) => {
|
||||
return (tx || db.replicaNode())(TableName.ProjectSlackConfigs)
|
||||
.join(
|
||||
TableName.SlackIntegrations,
|
||||
`${TableName.ProjectSlackConfigs}.slackIntegrationId`,
|
||||
`${TableName.SlackIntegrations}.id`
|
||||
)
|
||||
.where("projectId", "=", projectId)
|
||||
.select(selectAllTableCols(TableName.ProjectSlackConfigs), selectAllTableCols(TableName.SlackIntegrations))
|
||||
.first();
|
||||
};
|
||||
|
||||
return { ...projectSlackConfigOrm, getIntegrationDetailsByProject };
|
||||
};
|
||||
@@ -0,0 +1,16 @@
|
||||
import z from "zod";
|
||||
|
||||
export const validateSlackChannelsField = z
|
||||
.string()
|
||||
.trim()
|
||||
.default("")
|
||||
.transform((data) => {
|
||||
if (data === "") return "";
|
||||
return data
|
||||
.split(",")
|
||||
.map((id) => id.trim())
|
||||
.join(", ");
|
||||
})
|
||||
.refine((data) => data.split(",").length <= 20, {
|
||||
message: "You can only select up to 20 slack channels"
|
||||
});
|
||||
@@ -0,0 +1,177 @@
|
||||
import { WebClient } from "@slack/web-api";
|
||||
|
||||
import { getConfig } from "@app/lib/config/env";
|
||||
import { BadRequestError } from "@app/lib/errors";
|
||||
import { logger } from "@app/lib/logger";
|
||||
|
||||
import { TKmsServiceFactory } from "../kms/kms-service";
|
||||
import { KmsDataKey } from "../kms/kms-types";
|
||||
import { TProjectDALFactory } from "../project/project-dal";
|
||||
import { TProjectSlackConfigDALFactory } from "./project-slack-config-dal";
|
||||
import { SlackTriggerFeature, TSlackNotification } from "./slack-types";
|
||||
|
||||
export const fetchSlackChannels = async (botKey: string) => {
|
||||
const slackChannels: {
|
||||
name: string;
|
||||
id: string;
|
||||
}[] = [];
|
||||
|
||||
const slackWebClient = new WebClient(botKey);
|
||||
let cursor;
|
||||
|
||||
do {
|
||||
// eslint-disable-next-line no-await-in-loop
|
||||
const response = await slackWebClient.conversations.list({
|
||||
cursor,
|
||||
limit: 1000,
|
||||
types: "public_channel,private_channel"
|
||||
});
|
||||
|
||||
response.channels?.forEach((channel) =>
|
||||
slackChannels.push({
|
||||
name: channel.name_normalized as string,
|
||||
id: channel.id as string
|
||||
})
|
||||
);
|
||||
|
||||
// Set the cursor for the next page
|
||||
cursor = response.response_metadata?.next_cursor;
|
||||
} while (cursor); // Continue while there is a cursor
|
||||
|
||||
return slackChannels;
|
||||
};
|
||||
|
||||
const buildSlackPayload = (notification: TSlackNotification) => {
|
||||
const appCfg = getConfig();
|
||||
|
||||
switch (notification.type) {
|
||||
case SlackTriggerFeature.SECRET_APPROVAL: {
|
||||
const { payload } = notification;
|
||||
const messageBody = `A secret approval request has been opened by ${payload.userEmail}.
|
||||
*Environment*: ${payload.environment}
|
||||
*Secret path*: ${payload.secretPath || "/"}
|
||||
|
||||
View the complete details <${appCfg.SITE_URL}/project/${payload.projectId}/approval?requestId=${
|
||||
payload.requestId
|
||||
}|here>.`;
|
||||
|
||||
const payloadBlocks = [
|
||||
{
|
||||
type: "header",
|
||||
text: {
|
||||
type: "plain_text",
|
||||
text: "Secret approval request",
|
||||
emoji: true
|
||||
}
|
||||
},
|
||||
{
|
||||
type: "section",
|
||||
text: {
|
||||
type: "mrkdwn",
|
||||
text: messageBody
|
||||
}
|
||||
}
|
||||
];
|
||||
|
||||
return {
|
||||
payloadMessage: messageBody,
|
||||
payloadBlocks
|
||||
};
|
||||
}
|
||||
case SlackTriggerFeature.ACCESS_REQUEST: {
|
||||
const { payload } = notification;
|
||||
const messageBody = `${payload.requesterFullName} (${payload.requesterEmail}) has requested ${
|
||||
payload.isTemporary ? "temporary" : "permanent"
|
||||
} access to ${payload.secretPath} in the ${payload.environment} environment of ${payload.projectName}.
|
||||
|
||||
The following permissions are requested: ${payload.permissions.join(", ")}
|
||||
|
||||
View the request and approve or deny it <${payload.approvalUrl}|here>.`;
|
||||
|
||||
const payloadBlocks = [
|
||||
{
|
||||
type: "header",
|
||||
text: {
|
||||
type: "plain_text",
|
||||
text: "New access approval request pending for review",
|
||||
emoji: true
|
||||
}
|
||||
},
|
||||
{
|
||||
type: "section",
|
||||
text: {
|
||||
type: "mrkdwn",
|
||||
text: messageBody
|
||||
}
|
||||
}
|
||||
];
|
||||
|
||||
return {
|
||||
payloadMessage: messageBody,
|
||||
payloadBlocks
|
||||
};
|
||||
}
|
||||
default: {
|
||||
throw new BadRequestError({
|
||||
message: "Slack notification type not supported."
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
export const triggerSlackNotification = async ({
|
||||
projectId,
|
||||
notification,
|
||||
projectSlackConfigDAL,
|
||||
projectDAL,
|
||||
kmsService
|
||||
}: {
|
||||
projectId: string;
|
||||
notification: TSlackNotification;
|
||||
projectSlackConfigDAL: Pick<TProjectSlackConfigDALFactory, "getIntegrationDetailsByProject">;
|
||||
projectDAL: Pick<TProjectDALFactory, "findById">;
|
||||
kmsService: Pick<TKmsServiceFactory, "createCipherPairWithDataKey">;
|
||||
}) => {
|
||||
const { payloadMessage, payloadBlocks } = buildSlackPayload(notification);
|
||||
const project = await projectDAL.findById(projectId);
|
||||
const slackIntegration = await projectSlackConfigDAL.getIntegrationDetailsByProject(project.id);
|
||||
|
||||
if (!slackIntegration) {
|
||||
return;
|
||||
}
|
||||
|
||||
let targetChannelIds: string[] = [];
|
||||
if (notification.type === SlackTriggerFeature.ACCESS_REQUEST) {
|
||||
targetChannelIds = slackIntegration.accessRequestChannels?.split(", ") || [];
|
||||
if (!targetChannelIds.length || !slackIntegration.isAccessRequestNotificationEnabled) {
|
||||
return;
|
||||
}
|
||||
} else if (notification.type === SlackTriggerFeature.SECRET_APPROVAL) {
|
||||
targetChannelIds = slackIntegration.secretRequestChannels?.split(", ") || [];
|
||||
if (!targetChannelIds.length || !slackIntegration.isSecretRequestNotificationEnabled) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
const { decryptor: orgDataKeyDecryptor } = await kmsService.createCipherPairWithDataKey({
|
||||
type: KmsDataKey.Organization,
|
||||
orgId: project.orgId
|
||||
});
|
||||
|
||||
const botKey = orgDataKeyDecryptor({
|
||||
cipherTextBlob: slackIntegration.encryptedBotAccessToken
|
||||
}).toString("utf8");
|
||||
|
||||
const slackWebClient = new WebClient(botKey);
|
||||
|
||||
for await (const conversationId of targetChannelIds) {
|
||||
// we send both text and blocks for compatibility with barebone clients
|
||||
await slackWebClient.chat
|
||||
.postMessage({
|
||||
channel: conversationId,
|
||||
text: payloadMessage,
|
||||
blocks: payloadBlocks
|
||||
})
|
||||
.catch((err) => logger.error(err));
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,56 @@
|
||||
import { Knex } from "knex";
|
||||
|
||||
import { TDbClient } from "@app/db";
|
||||
import { TableName, TSlackIntegrations, TWorkflowIntegrations } from "@app/db/schemas";
|
||||
import { DatabaseError } from "@app/lib/errors";
|
||||
import { ormify, selectAllTableCols } from "@app/lib/knex";
|
||||
|
||||
export type TSlackIntegrationDALFactory = ReturnType<typeof slackIntegrationDALFactory>;
|
||||
|
||||
export const slackIntegrationDALFactory = (db: TDbClient) => {
|
||||
const slackIntegrationOrm = ormify(db, TableName.SlackIntegrations);
|
||||
|
||||
const findByIdWithWorkflowIntegrationDetails = async (id: string, tx?: Knex) => {
|
||||
try {
|
||||
return await (tx || db.replicaNode())(TableName.SlackIntegrations)
|
||||
.join(
|
||||
TableName.WorkflowIntegrations,
|
||||
`${TableName.SlackIntegrations}.id`,
|
||||
`${TableName.WorkflowIntegrations}.id`
|
||||
)
|
||||
.select(selectAllTableCols(TableName.SlackIntegrations))
|
||||
.select(db.ref("orgId").withSchema(TableName.WorkflowIntegrations))
|
||||
.select(db.ref("description").withSchema(TableName.WorkflowIntegrations))
|
||||
.select(db.ref("integration").withSchema(TableName.WorkflowIntegrations))
|
||||
.select(db.ref("slug").withSchema(TableName.WorkflowIntegrations))
|
||||
.where(`${TableName.WorkflowIntegrations}.id`, "=", id)
|
||||
.first();
|
||||
} catch (error) {
|
||||
throw new DatabaseError({ error, name: "Find by ID with Workflow integration details" });
|
||||
}
|
||||
};
|
||||
|
||||
const findWithWorkflowIntegrationDetails = async (
|
||||
filter: Partial<TSlackIntegrations> & Partial<TWorkflowIntegrations>,
|
||||
tx?: Knex
|
||||
) => {
|
||||
try {
|
||||
return await (tx || db.replicaNode())(TableName.SlackIntegrations)
|
||||
.join(
|
||||
TableName.WorkflowIntegrations,
|
||||
`${TableName.SlackIntegrations}.id`,
|
||||
`${TableName.WorkflowIntegrations}.id`
|
||||
)
|
||||
.select(selectAllTableCols(TableName.SlackIntegrations))
|
||||
.select(db.ref("orgId").withSchema(TableName.WorkflowIntegrations))
|
||||
.select(db.ref("description").withSchema(TableName.WorkflowIntegrations))
|
||||
.select(db.ref("integration").withSchema(TableName.WorkflowIntegrations))
|
||||
.select(db.ref("slug").withSchema(TableName.WorkflowIntegrations))
|
||||
.where(filter);
|
||||
} catch (error) {
|
||||
throw new DatabaseError({ error, name: "Find with Workflow integration details" });
|
||||
}
|
||||
};
|
||||
|
||||
return { ...slackIntegrationOrm, findByIdWithWorkflowIntegrationDetails, findWithWorkflowIntegrationDetails };
|
||||
};
|
||||
@@ -0,0 +1,463 @@
|
||||
import { ForbiddenError } from "@casl/ability";
|
||||
import { InstallProvider } from "@slack/oauth";
|
||||
|
||||
import { OrgPermissionActions, OrgPermissionSubjects } from "@app/ee/services/permission/org-permission";
|
||||
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service";
|
||||
import { getConfig } from "@app/lib/config/env";
|
||||
import { BadRequestError, NotFoundError } from "@app/lib/errors";
|
||||
|
||||
import { TKmsServiceFactory } from "../kms/kms-service";
|
||||
import { KmsDataKey } from "../kms/kms-types";
|
||||
import { getServerCfg } from "../super-admin/super-admin-service";
|
||||
import { TWorkflowIntegrationDALFactory } from "../workflow-integration/workflow-integration-dal";
|
||||
import { WorkflowIntegration } from "../workflow-integration/workflow-integration-types";
|
||||
import { fetchSlackChannels } from "./slack-fns";
|
||||
import { TSlackIntegrationDALFactory } from "./slack-integration-dal";
|
||||
import {
|
||||
TCompleteSlackIntegrationDTO,
|
||||
TDeleteSlackIntegrationDTO,
|
||||
TGetReinstallUrlDTO,
|
||||
TGetSlackInstallUrlDTO,
|
||||
TGetSlackIntegrationByIdDTO,
|
||||
TGetSlackIntegrationByOrgDTO,
|
||||
TGetSlackIntegrationChannelsDTO,
|
||||
TReinstallSlackIntegrationDTO,
|
||||
TUpdateSlackIntegrationDTO
|
||||
} from "./slack-types";
|
||||
|
||||
type TSlackServiceFactoryDep = {
|
||||
slackIntegrationDAL: Pick<
|
||||
TSlackIntegrationDALFactory,
|
||||
| "deleteById"
|
||||
| "updateById"
|
||||
| "create"
|
||||
| "findByIdWithWorkflowIntegrationDetails"
|
||||
| "findWithWorkflowIntegrationDetails"
|
||||
>;
|
||||
permissionService: Pick<TPermissionServiceFactory, "getProjectPermission" | "getOrgPermission">;
|
||||
kmsService: Pick<TKmsServiceFactory, "createCipherPairWithDataKey" | "encryptWithRootKey" | "decryptWithRootKey">;
|
||||
workflowIntegrationDAL: Pick<TWorkflowIntegrationDALFactory, "transaction" | "create" | "updateById" | "deleteById">;
|
||||
};
|
||||
|
||||
export type TSlackServiceFactory = ReturnType<typeof slackServiceFactory>;
|
||||
|
||||
export const slackServiceFactory = ({
|
||||
permissionService,
|
||||
slackIntegrationDAL,
|
||||
kmsService,
|
||||
workflowIntegrationDAL
|
||||
}: TSlackServiceFactoryDep) => {
|
||||
const completeSlackIntegration = async ({
|
||||
orgId,
|
||||
slug,
|
||||
description,
|
||||
teamId,
|
||||
teamName,
|
||||
slackUserId,
|
||||
slackAppId,
|
||||
botAccessToken,
|
||||
slackBotId,
|
||||
slackBotUserId
|
||||
}: TCompleteSlackIntegrationDTO) => {
|
||||
const { encryptor: orgDataKeyEncryptor } = await kmsService.createCipherPairWithDataKey({
|
||||
orgId,
|
||||
type: KmsDataKey.Organization
|
||||
});
|
||||
|
||||
const { cipherTextBlob: encryptedBotAccessToken } = orgDataKeyEncryptor({
|
||||
plainText: Buffer.from(botAccessToken, "utf8")
|
||||
});
|
||||
|
||||
await workflowIntegrationDAL.transaction(async (tx) => {
|
||||
const workflowIntegration = await workflowIntegrationDAL.create(
|
||||
{
|
||||
description,
|
||||
orgId,
|
||||
slug,
|
||||
integration: WorkflowIntegration.SLACK
|
||||
},
|
||||
tx
|
||||
);
|
||||
|
||||
await slackIntegrationDAL.create(
|
||||
{
|
||||
// @ts-expect-error id is kept as fixed because it is always equal to the workflow integration ID
|
||||
id: workflowIntegration.id,
|
||||
teamId,
|
||||
teamName,
|
||||
slackUserId,
|
||||
slackAppId,
|
||||
slackBotId,
|
||||
slackBotUserId,
|
||||
encryptedBotAccessToken
|
||||
},
|
||||
tx
|
||||
);
|
||||
});
|
||||
};
|
||||
|
||||
const reinstallSlackIntegration = async ({
|
||||
id,
|
||||
teamId,
|
||||
teamName,
|
||||
slackUserId,
|
||||
slackAppId,
|
||||
botAccessToken,
|
||||
slackBotId,
|
||||
slackBotUserId
|
||||
}: TReinstallSlackIntegrationDTO) => {
|
||||
const slackIntegration = await slackIntegrationDAL.findByIdWithWorkflowIntegrationDetails(id);
|
||||
|
||||
if (!slackIntegration) {
|
||||
throw new NotFoundError({
|
||||
message: "Slack integration not found"
|
||||
});
|
||||
}
|
||||
|
||||
const { encryptor: orgDataKeyEncryptor } = await kmsService.createCipherPairWithDataKey({
|
||||
orgId: slackIntegration.orgId,
|
||||
type: KmsDataKey.Organization
|
||||
});
|
||||
|
||||
const { cipherTextBlob: encryptedBotAccessToken } = orgDataKeyEncryptor({
|
||||
plainText: Buffer.from(botAccessToken, "utf8")
|
||||
});
|
||||
|
||||
await slackIntegrationDAL.updateById(id, {
|
||||
teamId,
|
||||
teamName,
|
||||
slackUserId,
|
||||
slackAppId,
|
||||
slackBotId,
|
||||
slackBotUserId,
|
||||
encryptedBotAccessToken
|
||||
});
|
||||
};
|
||||
|
||||
const getSlackInstaller = async () => {
|
||||
const appCfg = getConfig();
|
||||
const serverCfg = await getServerCfg();
|
||||
|
||||
let slackClientId = appCfg.WORKFLOW_SLACK_CLIENT_ID as string;
|
||||
let slackClientSecret = appCfg.WORKFLOW_SLACK_CLIENT_SECRET as string;
|
||||
|
||||
const decrypt = await kmsService.decryptWithRootKey();
|
||||
|
||||
if (serverCfg.encryptedSlackClientId) {
|
||||
slackClientId = (await decrypt({ cipherTextBlob: Buffer.from(serverCfg.encryptedSlackClientId) })).toString();
|
||||
}
|
||||
|
||||
if (serverCfg.encryptedSlackClientSecret) {
|
||||
slackClientSecret = (
|
||||
await decrypt({ cipherTextBlob: Buffer.from(serverCfg.encryptedSlackClientSecret) })
|
||||
).toString();
|
||||
}
|
||||
|
||||
if (!slackClientId || !slackClientSecret) {
|
||||
throw new BadRequestError({
|
||||
message: `Invalid Slack configuration. ${
|
||||
appCfg.isCloud
|
||||
? "Please contact the Infisical team."
|
||||
: "Contact your instance admin to setup Slack integration in the Admin settings. Your configuration is missing Slack client ID and secret."
|
||||
}`
|
||||
});
|
||||
}
|
||||
|
||||
return new InstallProvider({
|
||||
clientId: slackClientId,
|
||||
clientSecret: slackClientSecret,
|
||||
stateSecret: appCfg.AUTH_SECRET,
|
||||
legacyStateVerification: true,
|
||||
installationStore: {
|
||||
storeInstallation: async (installation) => {
|
||||
if (installation.isEnterpriseInstall && installation.enterprise?.id) {
|
||||
throw new BadRequestError({
|
||||
message: "Enterprise not yet supported"
|
||||
});
|
||||
}
|
||||
|
||||
const metadata = JSON.parse(installation.metadata || "") as {
|
||||
id?: string;
|
||||
orgId: string;
|
||||
slug: string;
|
||||
description?: string;
|
||||
};
|
||||
|
||||
if (metadata.id) {
|
||||
return reinstallSlackIntegration({
|
||||
id: metadata.id,
|
||||
teamId: installation.team?.id || "",
|
||||
teamName: installation.team?.name || "",
|
||||
slackUserId: installation.user.id,
|
||||
slackAppId: installation.appId || "",
|
||||
botAccessToken: installation.bot?.token || "",
|
||||
slackBotId: installation.bot?.id || "",
|
||||
slackBotUserId: installation.bot?.userId || ""
|
||||
});
|
||||
}
|
||||
|
||||
return completeSlackIntegration({
|
||||
orgId: metadata.orgId,
|
||||
slug: metadata.slug,
|
||||
description: metadata.description,
|
||||
teamId: installation.team?.id || "",
|
||||
teamName: installation.team?.name || "",
|
||||
slackUserId: installation.user.id,
|
||||
slackAppId: installation.appId || "",
|
||||
botAccessToken: installation.bot?.token || "",
|
||||
slackBotId: installation.bot?.id || "",
|
||||
slackBotUserId: installation.bot?.userId || ""
|
||||
});
|
||||
},
|
||||
// for our use-case we don't need to implement this because this will only be used
|
||||
// when listening for events from slack
|
||||
fetchInstallation: () => {
|
||||
return {} as never;
|
||||
},
|
||||
// for our use-case we don't need to implement this yet
|
||||
deleteInstallation: () => {
|
||||
return {} as never;
|
||||
}
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const getInstallUrl = async ({
|
||||
actorId,
|
||||
actor,
|
||||
actorOrgId,
|
||||
actorAuthMethod,
|
||||
slug,
|
||||
description
|
||||
}: TGetSlackInstallUrlDTO) => {
|
||||
const appCfg = getConfig();
|
||||
|
||||
const { permission } = await permissionService.getOrgPermission(
|
||||
actor,
|
||||
actorId,
|
||||
actorOrgId,
|
||||
actorAuthMethod,
|
||||
actorOrgId
|
||||
);
|
||||
|
||||
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Create, OrgPermissionSubjects.Settings);
|
||||
|
||||
const installer = await getSlackInstaller();
|
||||
const url = await installer.generateInstallUrl({
|
||||
scopes: ["chat:write.public", "chat:write", "channels:read", "groups:read"],
|
||||
metadata: JSON.stringify({
|
||||
slug,
|
||||
description,
|
||||
orgId: actorOrgId
|
||||
}),
|
||||
redirectUri: `${appCfg.SITE_URL}/api/v1/workflow-integrations/slack/oauth_redirect`
|
||||
});
|
||||
|
||||
return url;
|
||||
};
|
||||
|
||||
const getReinstallUrl = async ({ actorId, actor, actorOrgId, actorAuthMethod, id }: TGetReinstallUrlDTO) => {
|
||||
const appCfg = getConfig();
|
||||
const slackIntegration = await slackIntegrationDAL.findByIdWithWorkflowIntegrationDetails(id);
|
||||
|
||||
if (!slackIntegration) {
|
||||
throw new NotFoundError({
|
||||
message: "Slack integration not found"
|
||||
});
|
||||
}
|
||||
|
||||
const { permission } = await permissionService.getOrgPermission(
|
||||
actor,
|
||||
actorId,
|
||||
slackIntegration.orgId,
|
||||
actorAuthMethod,
|
||||
actorOrgId
|
||||
);
|
||||
|
||||
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Create, OrgPermissionSubjects.Settings);
|
||||
|
||||
const installer = await getSlackInstaller();
|
||||
const url = await installer.generateInstallUrl({
|
||||
scopes: ["chat:write.public", "chat:write", "channels:read", "groups:read"],
|
||||
metadata: JSON.stringify({
|
||||
id,
|
||||
orgId: slackIntegration.orgId
|
||||
}),
|
||||
redirectUri: `${appCfg.SITE_URL}/api/v1/workflow-integrations/slack/oauth_redirect`
|
||||
});
|
||||
|
||||
return url;
|
||||
};
|
||||
|
||||
const getSlackIntegrationsByOrg = async ({
|
||||
actorId,
|
||||
actor,
|
||||
actorOrgId,
|
||||
actorAuthMethod
|
||||
}: TGetSlackIntegrationByOrgDTO) => {
|
||||
const { permission } = await permissionService.getOrgPermission(
|
||||
actor,
|
||||
actorId,
|
||||
actorOrgId,
|
||||
actorAuthMethod,
|
||||
actorOrgId
|
||||
);
|
||||
|
||||
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Create, OrgPermissionSubjects.Settings);
|
||||
|
||||
const slackIntegrations = await slackIntegrationDAL.findWithWorkflowIntegrationDetails({
|
||||
orgId: actorOrgId
|
||||
});
|
||||
|
||||
return slackIntegrations;
|
||||
};
|
||||
|
||||
const getSlackIntegrationById = async ({
|
||||
actorId,
|
||||
actor,
|
||||
actorOrgId,
|
||||
actorAuthMethod,
|
||||
id
|
||||
}: TGetSlackIntegrationByIdDTO) => {
|
||||
const slackIntegration = await slackIntegrationDAL.findByIdWithWorkflowIntegrationDetails(id);
|
||||
if (!slackIntegration) {
|
||||
throw new NotFoundError({
|
||||
message: "Slack integration not found."
|
||||
});
|
||||
}
|
||||
|
||||
const { permission } = await permissionService.getOrgPermission(
|
||||
actor,
|
||||
actorId,
|
||||
slackIntegration.orgId,
|
||||
actorAuthMethod,
|
||||
actorOrgId
|
||||
);
|
||||
|
||||
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Read, OrgPermissionSubjects.Settings);
|
||||
|
||||
return slackIntegration;
|
||||
};
|
||||
|
||||
const getSlackIntegrationChannels = async ({
|
||||
actorId,
|
||||
actor,
|
||||
actorOrgId,
|
||||
actorAuthMethod,
|
||||
id
|
||||
}: TGetSlackIntegrationChannelsDTO) => {
|
||||
const slackIntegration = await slackIntegrationDAL.findByIdWithWorkflowIntegrationDetails(id);
|
||||
if (!slackIntegration) {
|
||||
throw new NotFoundError({
|
||||
message: "Slack integration not found."
|
||||
});
|
||||
}
|
||||
|
||||
const { permission } = await permissionService.getOrgPermission(
|
||||
actor,
|
||||
actorId,
|
||||
slackIntegration.orgId,
|
||||
actorAuthMethod,
|
||||
actorOrgId
|
||||
);
|
||||
|
||||
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Read, OrgPermissionSubjects.Settings);
|
||||
|
||||
const { decryptor: orgDataKeyDecryptor } = await kmsService.createCipherPairWithDataKey({
|
||||
orgId: slackIntegration.orgId,
|
||||
type: KmsDataKey.Organization
|
||||
});
|
||||
|
||||
const botKey = orgDataKeyDecryptor({
|
||||
cipherTextBlob: slackIntegration.encryptedBotAccessToken
|
||||
}).toString("utf8");
|
||||
|
||||
return fetchSlackChannels(botKey);
|
||||
};
|
||||
|
||||
const updateSlackIntegration = async ({
|
||||
actorId,
|
||||
actor,
|
||||
actorOrgId,
|
||||
actorAuthMethod,
|
||||
id,
|
||||
slug,
|
||||
description
|
||||
}: TUpdateSlackIntegrationDTO) => {
|
||||
const slackIntegration = await slackIntegrationDAL.findByIdWithWorkflowIntegrationDetails(id);
|
||||
if (!slackIntegration) {
|
||||
throw new NotFoundError({
|
||||
message: "Slack integration not found"
|
||||
});
|
||||
}
|
||||
|
||||
const { permission } = await permissionService.getOrgPermission(
|
||||
actor,
|
||||
actorId,
|
||||
slackIntegration.orgId,
|
||||
actorAuthMethod,
|
||||
actorOrgId
|
||||
);
|
||||
|
||||
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Edit, OrgPermissionSubjects.Settings);
|
||||
|
||||
return workflowIntegrationDAL.transaction(async (tx) => {
|
||||
await workflowIntegrationDAL.updateById(
|
||||
slackIntegration.id,
|
||||
{
|
||||
slug,
|
||||
description
|
||||
},
|
||||
tx
|
||||
);
|
||||
|
||||
const updatedIntegration = await slackIntegrationDAL.findByIdWithWorkflowIntegrationDetails(
|
||||
slackIntegration.id,
|
||||
tx
|
||||
);
|
||||
|
||||
return updatedIntegration!;
|
||||
});
|
||||
};
|
||||
|
||||
const deleteSlackIntegration = async ({
|
||||
actorId,
|
||||
actor,
|
||||
actorOrgId,
|
||||
actorAuthMethod,
|
||||
id
|
||||
}: TDeleteSlackIntegrationDTO) => {
|
||||
const slackIntegration = await slackIntegrationDAL.findByIdWithWorkflowIntegrationDetails(id);
|
||||
if (!slackIntegration) {
|
||||
throw new NotFoundError({
|
||||
message: "Slack integration not found"
|
||||
});
|
||||
}
|
||||
|
||||
const { permission } = await permissionService.getOrgPermission(
|
||||
actor,
|
||||
actorId,
|
||||
slackIntegration.orgId,
|
||||
actorAuthMethod,
|
||||
actorOrgId
|
||||
);
|
||||
|
||||
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Delete, OrgPermissionSubjects.Settings);
|
||||
|
||||
await workflowIntegrationDAL.deleteById(id);
|
||||
|
||||
return slackIntegration;
|
||||
};
|
||||
|
||||
return {
|
||||
getInstallUrl,
|
||||
getReinstallUrl,
|
||||
getSlackIntegrationsByOrg,
|
||||
getSlackIntegrationById,
|
||||
completeSlackIntegration,
|
||||
getSlackInstaller,
|
||||
updateSlackIntegration,
|
||||
deleteSlackIntegration,
|
||||
getSlackIntegrationChannels
|
||||
};
|
||||
};
|
||||
@@ -0,0 +1,79 @@
|
||||
import { TOrgPermission } from "@app/lib/types";
|
||||
|
||||
export type TGetSlackInstallUrlDTO = {
|
||||
slug: string;
|
||||
description?: string;
|
||||
} & Omit<TOrgPermission, "orgId">;
|
||||
|
||||
export type TGetReinstallUrlDTO = {
|
||||
id: string;
|
||||
} & Omit<TOrgPermission, "orgId">;
|
||||
|
||||
export type TGetSlackIntegrationByOrgDTO = Omit<TOrgPermission, "orgId">;
|
||||
|
||||
export type TGetSlackIntegrationByIdDTO = { id: string } & Omit<TOrgPermission, "orgId">;
|
||||
|
||||
export type TGetSlackIntegrationChannelsDTO = { id: string } & Omit<TOrgPermission, "orgId">;
|
||||
|
||||
export type TUpdateSlackIntegrationDTO = { id: string; slug?: string; description?: string } & Omit<
|
||||
TOrgPermission,
|
||||
"orgId"
|
||||
>;
|
||||
|
||||
export type TDeleteSlackIntegrationDTO = {
|
||||
id: string;
|
||||
} & Omit<TOrgPermission, "orgId">;
|
||||
|
||||
export type TCompleteSlackIntegrationDTO = {
|
||||
orgId: string;
|
||||
slug: string;
|
||||
description?: string;
|
||||
teamId: string;
|
||||
teamName: string;
|
||||
slackUserId: string;
|
||||
slackAppId: string;
|
||||
botAccessToken: string;
|
||||
slackBotId: string;
|
||||
slackBotUserId: string;
|
||||
};
|
||||
|
||||
export type TReinstallSlackIntegrationDTO = {
|
||||
id: string;
|
||||
teamId: string;
|
||||
teamName: string;
|
||||
slackUserId: string;
|
||||
slackAppId: string;
|
||||
botAccessToken: string;
|
||||
slackBotId: string;
|
||||
slackBotUserId: string;
|
||||
};
|
||||
|
||||
export enum SlackTriggerFeature {
|
||||
SECRET_APPROVAL = "secret-approval",
|
||||
ACCESS_REQUEST = "access-request"
|
||||
}
|
||||
|
||||
export type TSlackNotification =
|
||||
| {
|
||||
type: SlackTriggerFeature.SECRET_APPROVAL;
|
||||
payload: {
|
||||
userEmail: string;
|
||||
environment: string;
|
||||
secretPath: string;
|
||||
requestId: string;
|
||||
projectId: string;
|
||||
};
|
||||
}
|
||||
| {
|
||||
type: SlackTriggerFeature.ACCESS_REQUEST;
|
||||
payload: {
|
||||
requesterFullName: string;
|
||||
requesterEmail: string;
|
||||
isTemporary: boolean;
|
||||
secretPath: string;
|
||||
environment: string;
|
||||
projectName: string;
|
||||
permissions: string[];
|
||||
approvalUrl: string;
|
||||
};
|
||||
};
|
||||
@@ -6,10 +6,11 @@ import { TKeyStoreFactory } from "@app/keystore/keystore";
|
||||
import { getConfig } from "@app/lib/config/env";
|
||||
import { infisicalSymmetricEncypt } from "@app/lib/crypto/encryption";
|
||||
import { getUserPrivateKey } from "@app/lib/crypto/srp";
|
||||
import { BadRequestError } from "@app/lib/errors";
|
||||
import { BadRequestError, NotFoundError } from "@app/lib/errors";
|
||||
|
||||
import { TAuthLoginFactory } from "../auth/auth-login-service";
|
||||
import { AuthMethod } from "../auth/auth-type";
|
||||
import { TKmsServiceFactory } from "../kms/kms-service";
|
||||
import { TOrgServiceFactory } from "../org/org-service";
|
||||
import { TUserDALFactory } from "../user/user-dal";
|
||||
import { TSuperAdminDALFactory } from "./super-admin-dal";
|
||||
@@ -19,6 +20,7 @@ type TSuperAdminServiceFactoryDep = {
|
||||
serverCfgDAL: TSuperAdminDALFactory;
|
||||
userDAL: TUserDALFactory;
|
||||
authService: Pick<TAuthLoginFactory, "generateUserTokens">;
|
||||
kmsService: Pick<TKmsServiceFactory, "encryptWithRootKey" | "decryptWithRootKey">;
|
||||
orgService: Pick<TOrgServiceFactory, "createOrganization">;
|
||||
keyStore: Pick<TKeyStoreFactory, "getItem" | "setItemWithExpiry" | "deleteItem">;
|
||||
licenseService: Pick<TLicenseServiceFactory, "onPremFeatures">;
|
||||
@@ -39,6 +41,7 @@ export const superAdminServiceFactory = ({
|
||||
authService,
|
||||
orgService,
|
||||
keyStore,
|
||||
kmsService,
|
||||
licenseService
|
||||
}: TSuperAdminServiceFactoryDep) => {
|
||||
const initServerCfg = async () => {
|
||||
@@ -82,7 +85,12 @@ export const superAdminServiceFactory = ({
|
||||
return newCfg;
|
||||
};
|
||||
|
||||
const updateServerCfg = async (data: TSuperAdminUpdate, userId: string) => {
|
||||
const updateServerCfg = async (
|
||||
data: TSuperAdminUpdate & { slackClientId?: string; slackClientSecret?: string },
|
||||
userId: string
|
||||
) => {
|
||||
const updatedData = data;
|
||||
|
||||
if (data.enabledLoginMethods) {
|
||||
const superAdminUser = await userDAL.findById(userId);
|
||||
const loginMethodToAuthMethod = {
|
||||
@@ -113,7 +121,27 @@ export const superAdminServiceFactory = ({
|
||||
});
|
||||
}
|
||||
}
|
||||
const updatedServerCfg = await serverCfgDAL.updateById(ADMIN_CONFIG_DB_UUID, data);
|
||||
|
||||
const encryptWithRoot = await kmsService.encryptWithRootKey();
|
||||
if (data.slackClientId) {
|
||||
const { cipherTextBlob: encryptedClientId } = await encryptWithRoot({
|
||||
plainText: Buffer.from(data.slackClientId)
|
||||
});
|
||||
|
||||
updatedData.encryptedSlackClientId = encryptedClientId;
|
||||
updatedData.slackClientId = undefined;
|
||||
}
|
||||
|
||||
if (data.slackClientSecret) {
|
||||
const { cipherTextBlob: encryptedClientSecret } = await encryptWithRoot({
|
||||
plainText: Buffer.from(data.slackClientSecret)
|
||||
});
|
||||
|
||||
updatedData.encryptedSlackClientSecret = encryptedClientSecret;
|
||||
updatedData.slackClientSecret = undefined;
|
||||
}
|
||||
|
||||
const updatedServerCfg = await serverCfgDAL.updateById(ADMIN_CONFIG_DB_UUID, updatedData);
|
||||
|
||||
await keyStore.setItemWithExpiry(ADMIN_CONFIG_KEY, ADMIN_CONFIG_KEY_EXP, JSON.stringify(updatedServerCfg));
|
||||
|
||||
@@ -232,11 +260,38 @@ export const superAdminServiceFactory = ({
|
||||
return user;
|
||||
};
|
||||
|
||||
const getAdminSlackConfig = async () => {
|
||||
const serverCfg = await serverCfgDAL.findById(ADMIN_CONFIG_DB_UUID);
|
||||
|
||||
if (!serverCfg) {
|
||||
throw new NotFoundError({ name: "Admin config", message: "Admin config not found" });
|
||||
}
|
||||
|
||||
let clientId = "";
|
||||
let clientSecret = "";
|
||||
|
||||
const decrypt = await kmsService.decryptWithRootKey();
|
||||
|
||||
if (serverCfg.encryptedSlackClientId) {
|
||||
clientId = (await decrypt({ cipherTextBlob: serverCfg.encryptedSlackClientId })).toString();
|
||||
}
|
||||
|
||||
if (serverCfg.encryptedSlackClientSecret) {
|
||||
clientSecret = (await decrypt({ cipherTextBlob: serverCfg.encryptedSlackClientSecret })).toString();
|
||||
}
|
||||
|
||||
return {
|
||||
clientId,
|
||||
clientSecret
|
||||
};
|
||||
};
|
||||
|
||||
return {
|
||||
initServerCfg,
|
||||
updateServerCfg,
|
||||
adminSignUp,
|
||||
getUsers,
|
||||
deleteUser
|
||||
deleteUser,
|
||||
getAdminSlackConfig
|
||||
};
|
||||
};
|
||||
|
||||
@@ -82,10 +82,9 @@ export const userDALFactory = (db: TDbClient) => {
|
||||
}
|
||||
};
|
||||
|
||||
const findUserEncKeyByUserId = async (userId: string) => {
|
||||
const findUserEncKeyByUserId = async (userId: string, tx?: Knex) => {
|
||||
try {
|
||||
const user = await db
|
||||
.replicaNode()(TableName.Users)
|
||||
const user = await (tx || db.replicaNode())(TableName.Users)
|
||||
.where(`${TableName.Users}.id`, userId)
|
||||
.join(TableName.UserEncryptionKey, `${TableName.Users}.id`, `${TableName.UserEncryptionKey}.userId`)
|
||||
.first();
|
||||
|
||||
@@ -4,18 +4,14 @@ import { alphaNumericNanoId } from "@app/lib/nanoid";
|
||||
import { TUserDALFactory } from "@app/services/user/user-dal";
|
||||
|
||||
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 });
|
||||
if (!user) return attempt;
|
||||
|
||||
while (true) {
|
||||
do {
|
||||
attempt = slugify(`${username}-${alphaNumericNanoId(4)}`);
|
||||
// eslint-disable-next-line no-await-in-loop
|
||||
user = await userDAL.findOne({ username: attempt });
|
||||
} while (user);
|
||||
|
||||
if (!user) {
|
||||
return attempt;
|
||||
}
|
||||
}
|
||||
return attempt;
|
||||
};
|
||||
|
||||
@@ -3,3 +3,8 @@ import { TOrgPermission } from "@app/lib/types";
|
||||
export type TListUserGroupsDTO = {
|
||||
username: string;
|
||||
} & Omit<TOrgPermission, "orgId">;
|
||||
|
||||
export enum UserEncryption {
|
||||
V1 = 1,
|
||||
V2 = 2
|
||||
}
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
import { TDbClient } from "@app/db";
|
||||
import { TableName } from "@app/db/schemas";
|
||||
import { ormify } from "@app/lib/knex";
|
||||
|
||||
export type TWorkflowIntegrationDALFactory = ReturnType<typeof workflowIntegrationDALFactory>;
|
||||
|
||||
export const workflowIntegrationDALFactory = (db: TDbClient) => {
|
||||
const workflowIntegrationOrm = ormify(db, TableName.WorkflowIntegrations);
|
||||
|
||||
return workflowIntegrationOrm;
|
||||
};
|
||||
@@ -0,0 +1,43 @@
|
||||
import { ForbiddenError } from "@casl/ability";
|
||||
|
||||
import { OrgPermissionActions, OrgPermissionSubjects } from "@app/ee/services/permission/org-permission";
|
||||
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service";
|
||||
|
||||
import { TWorkflowIntegrationDALFactory } from "./workflow-integration-dal";
|
||||
import { TGetWorkflowIntegrationsByOrg } from "./workflow-integration-types";
|
||||
|
||||
type TWorkflowIntegrationServiceFactoryDep = {
|
||||
workflowIntegrationDAL: Pick<TWorkflowIntegrationDALFactory, "find">;
|
||||
permissionService: Pick<TPermissionServiceFactory, "getProjectPermission" | "getOrgPermission">;
|
||||
};
|
||||
|
||||
export type TWorkflowIntegrationServiceFactory = ReturnType<typeof workflowIntegrationServiceFactory>;
|
||||
|
||||
export const workflowIntegrationServiceFactory = ({
|
||||
workflowIntegrationDAL,
|
||||
permissionService
|
||||
}: TWorkflowIntegrationServiceFactoryDep) => {
|
||||
const getIntegrationsByOrg = async ({
|
||||
actorId,
|
||||
actor,
|
||||
actorOrgId,
|
||||
actorAuthMethod
|
||||
}: TGetWorkflowIntegrationsByOrg) => {
|
||||
const { permission } = await permissionService.getOrgPermission(
|
||||
actor,
|
||||
actorId,
|
||||
actorOrgId,
|
||||
actorAuthMethod,
|
||||
actorOrgId
|
||||
);
|
||||
|
||||
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Read, OrgPermissionSubjects.Settings);
|
||||
|
||||
return workflowIntegrationDAL.find({
|
||||
orgId: actorOrgId
|
||||
});
|
||||
};
|
||||
return {
|
||||
getIntegrationsByOrg
|
||||
};
|
||||
};
|
||||
@@ -0,0 +1,7 @@
|
||||
import { TOrgPermission } from "@app/lib/types";
|
||||
|
||||
export enum WorkflowIntegration {
|
||||
SLACK = "slack"
|
||||
}
|
||||
|
||||
export type TGetWorkflowIntegrationsByOrg = Omit<TOrgPermission, "orgId">;
|
||||
Reference in New Issue
Block a user