mirror of
https://github.com/awatertrevi/infisical.git
synced 2026-09-22 13:39:35 +00:00
Merge pull request #4535 from Infisical/expand-notification-coverage
feat(notifications): expand notification coverage
This commit is contained in:
@@ -777,6 +777,20 @@ export const accessApprovalRequestServiceFactory = ({
|
||||
.map((appUser) => appUser.email)
|
||||
.filter((email): email is string => !!email);
|
||||
|
||||
const approvalPath = `/projects/secret-management/${project.id}/approval`;
|
||||
const approvalUrl = `${cfg.SITE_URL}${approvalPath}`;
|
||||
|
||||
await notificationService.createUserNotifications(
|
||||
approverUsersForEmail.map((approver) => ({
|
||||
userId: approver.id,
|
||||
orgId: actorOrgId,
|
||||
type: NotificationType.ACCESS_POLICY_BYPASSED,
|
||||
title: "Secret Access Policy Bypassed",
|
||||
body: `**${actingUser.firstName} ${actingUser.lastName}** (${actingUser.email}) has accessed a secret in **${policy.secretPath || "/"}** in the **${environment?.name || permissionEnvironment}** environment for project **${project.name}** without obtaining the required approval.`,
|
||||
link: approvalPath
|
||||
}))
|
||||
);
|
||||
|
||||
if (recipientEmails.length > 0) {
|
||||
await smtpService.sendMail({
|
||||
recipients: recipientEmails,
|
||||
@@ -788,7 +802,7 @@ export const accessApprovalRequestServiceFactory = ({
|
||||
bypassReason: bypassReason || "No reason provided",
|
||||
secretPath: policy.secretPath || "/",
|
||||
environment: environment?.name || permissionEnvironment,
|
||||
approvalUrl: `${cfg.SITE_URL}/projects/secret-management/${project.id}/approval`,
|
||||
approvalUrl,
|
||||
requestType: "access"
|
||||
},
|
||||
template: SmtpTemplates.AccessSecretRequestBypassed
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
import { TSecretApprovalRequests } from "@app/db/schemas";
|
||||
import { getConfig } from "@app/lib/config/env";
|
||||
import { TNotificationServiceFactory } from "@app/services/notification/notification-service";
|
||||
import { NotificationType } from "@app/services/notification/notification-types";
|
||||
import { TProjectDALFactory } from "@app/services/project/project-dal";
|
||||
import { SmtpTemplates, TSmtpService } from "@app/services/smtp/smtp-service";
|
||||
|
||||
@@ -11,6 +13,7 @@ type TSendApprovalEmails = {
|
||||
smtpService: Pick<TSmtpService, "sendMail">;
|
||||
projectId: string;
|
||||
secretApprovalRequest: TSecretApprovalRequests;
|
||||
notificationService: Pick<TNotificationServiceFactory, "createUserNotifications">;
|
||||
};
|
||||
|
||||
export const sendApprovalEmailsFn = async ({
|
||||
@@ -18,7 +21,8 @@ export const sendApprovalEmailsFn = async ({
|
||||
projectDAL,
|
||||
smtpService,
|
||||
projectId,
|
||||
secretApprovalRequest
|
||||
secretApprovalRequest,
|
||||
notificationService
|
||||
}: TSendApprovalEmails) => {
|
||||
const cfg = getConfig();
|
||||
|
||||
@@ -26,6 +30,17 @@ export const sendApprovalEmailsFn = async ({
|
||||
|
||||
const project = await projectDAL.findProjectWithOrg(projectId);
|
||||
|
||||
await notificationService.createUserNotifications(
|
||||
policy.userApprovers.map((approver) => ({
|
||||
userId: approver.userId,
|
||||
orgId: project.orgId,
|
||||
type: NotificationType.SECRET_CHANGE_REQUEST,
|
||||
title: "Secret Change Request",
|
||||
body: `You have a new secret change request pending your review for the project **${project.name}** in the organization **${project.organization.name}**.`,
|
||||
link: `/projects/secret-management/${project.id}/approval?requestId=${secretApprovalRequest.id}`
|
||||
}))
|
||||
);
|
||||
|
||||
// now we need to go through each of the reviewers and print out all the commits that they need to approve
|
||||
for await (const reviewerUser of policy.userApprovers) {
|
||||
await smtpService.sendMail({
|
||||
|
||||
@@ -28,6 +28,8 @@ import { TKmsServiceFactory } from "@app/services/kms/kms-service";
|
||||
import { KmsDataKey } from "@app/services/kms/kms-types";
|
||||
import { TMicrosoftTeamsServiceFactory } from "@app/services/microsoft-teams/microsoft-teams-service";
|
||||
import { TProjectMicrosoftTeamsConfigDALFactory } from "@app/services/microsoft-teams/project-microsoft-teams-config-dal";
|
||||
import { TNotificationServiceFactory } from "@app/services/notification/notification-service";
|
||||
import { NotificationType } from "@app/services/notification/notification-types";
|
||||
import { TProjectDALFactory } from "@app/services/project/project-dal";
|
||||
import { TProjectBotServiceFactory } from "@app/services/project-bot/project-bot-service";
|
||||
import { TProjectEnvDALFactory } from "@app/services/project-env/project-env-dal";
|
||||
@@ -140,6 +142,7 @@ type TSecretApprovalRequestServiceFactoryDep = {
|
||||
projectMicrosoftTeamsConfigDAL: Pick<TProjectMicrosoftTeamsConfigDALFactory, "getIntegrationDetailsByProject">;
|
||||
microsoftTeamsService: Pick<TMicrosoftTeamsServiceFactory, "sendNotification">;
|
||||
folderCommitService: Pick<TFolderCommitServiceFactory, "createCommit">;
|
||||
notificationService: Pick<TNotificationServiceFactory, "createUserNotifications">;
|
||||
};
|
||||
|
||||
export type TSecretApprovalRequestServiceFactory = ReturnType<typeof secretApprovalRequestServiceFactory>;
|
||||
@@ -172,7 +175,8 @@ export const secretApprovalRequestServiceFactory = ({
|
||||
resourceMetadataDAL,
|
||||
projectMicrosoftTeamsConfigDAL,
|
||||
microsoftTeamsService,
|
||||
folderCommitService
|
||||
folderCommitService,
|
||||
notificationService
|
||||
}: TSecretApprovalRequestServiceFactoryDep) => {
|
||||
const requestCount = async ({
|
||||
projectId,
|
||||
@@ -1035,6 +1039,17 @@ export const secretApprovalRequestServiceFactory = ({
|
||||
}
|
||||
});
|
||||
|
||||
await notificationService.createUserNotifications(
|
||||
approverUsers.map((approver) => ({
|
||||
userId: approver.id,
|
||||
orgId: project.orgId,
|
||||
type: NotificationType.SECRET_CHANGE_POLICY_BYPASSED,
|
||||
title: "Secret Change Policy Bypassed",
|
||||
body: `**${requestedByUser.firstName} ${requestedByUser.lastName}** (${requestedByUser.email}) has merged a secret to **${policy.secretPath}** in the **${env.name}** environment for project **${project.name}** without obtaining the required approval.`,
|
||||
link: `/projects/secret-management/${project.id}/approval`
|
||||
}))
|
||||
);
|
||||
|
||||
await smtpService.sendMail({
|
||||
recipients: approverUsers.filter((approver) => approver.email).map((approver) => approver.email!),
|
||||
subjectLine: "Infisical Secret Change Policy Bypassed",
|
||||
@@ -1446,7 +1461,8 @@ export const secretApprovalRequestServiceFactory = ({
|
||||
secretApprovalPolicyDAL,
|
||||
secretApprovalRequest,
|
||||
smtpService,
|
||||
projectId
|
||||
projectId,
|
||||
notificationService
|
||||
});
|
||||
|
||||
return secretApprovalRequest;
|
||||
@@ -1813,7 +1829,8 @@ export const secretApprovalRequestServiceFactory = ({
|
||||
secretApprovalPolicyDAL,
|
||||
secretApprovalRequest,
|
||||
smtpService,
|
||||
projectId
|
||||
projectId,
|
||||
notificationService
|
||||
});
|
||||
return secretApprovalRequest;
|
||||
};
|
||||
|
||||
@@ -17,6 +17,8 @@ import {
|
||||
import { getConfig } from "@app/lib/config/env";
|
||||
import { logger } from "@app/lib/logger";
|
||||
import { QueueJobs, QueueName, TQueueServiceFactory } from "@app/queue";
|
||||
import { TNotificationServiceFactory } from "@app/services/notification/notification-service";
|
||||
import { NotificationType } from "@app/services/notification/notification-types";
|
||||
import { TProjectDALFactory } from "@app/services/project/project-dal";
|
||||
import { TProjectMembershipDALFactory } from "@app/services/project-membership/project-membership-dal";
|
||||
import { SmtpTemplates, TSmtpService } from "@app/services/smtp/smtp-service";
|
||||
@@ -28,6 +30,7 @@ type TSecretRotationV2QueueServiceFactoryDep = {
|
||||
smtpService: Pick<TSmtpService, "sendMail">;
|
||||
projectMembershipDAL: Pick<TProjectMembershipDALFactory, "findAllProjectMembers">;
|
||||
projectDAL: Pick<TProjectDALFactory, "findById">;
|
||||
notificationService: Pick<TNotificationServiceFactory, "createUserNotifications">;
|
||||
};
|
||||
|
||||
export const secretRotationV2QueueServiceFactory = async ({
|
||||
@@ -36,7 +39,8 @@ export const secretRotationV2QueueServiceFactory = async ({
|
||||
secretRotationV2Service,
|
||||
projectMembershipDAL,
|
||||
projectDAL,
|
||||
smtpService
|
||||
smtpService,
|
||||
notificationService
|
||||
}: TSecretRotationV2QueueServiceFactoryDep) => {
|
||||
const appCfg = getConfig();
|
||||
|
||||
@@ -152,6 +156,19 @@ export const secretRotationV2QueueServiceFactory = async ({
|
||||
|
||||
const rotationType = SECRET_ROTATION_NAME_MAP[type as SecretRotation];
|
||||
|
||||
const rotationPath = `/projects/secret-management/${projectId}/secrets/${environment.slug}`;
|
||||
|
||||
await notificationService.createUserNotifications(
|
||||
projectAdmins.map((admin) => ({
|
||||
userId: admin.userId,
|
||||
orgId: project.orgId,
|
||||
type: NotificationType.SECRET_ROTATION_FAILED,
|
||||
title: "Secret Rotation Failed",
|
||||
body: `Your **${rotationType}** rotation **${rotationName}** failed to rotate.`,
|
||||
link: rotationPath
|
||||
}))
|
||||
);
|
||||
|
||||
await smtpService.sendMail({
|
||||
recipients: projectAdmins.map((member) => member.user.email!).filter(Boolean),
|
||||
template: SmtpTemplates.SecretRotationFailed,
|
||||
@@ -165,9 +182,7 @@ export const secretRotationV2QueueServiceFactory = async ({
|
||||
secretPath: folder.path,
|
||||
environment: environment.name,
|
||||
projectName: project.name,
|
||||
rotationUrl: encodeURI(
|
||||
`${appCfg.SITE_URL}/projects/secret-management/${projectId}/secrets/${environment.slug}`
|
||||
)
|
||||
rotationUrl: encodeURI(`${appCfg.SITE_URL}${rotationPath}`)
|
||||
}
|
||||
});
|
||||
} catch (error) {
|
||||
|
||||
@@ -21,6 +21,8 @@ import { decryptAppConnection } from "@app/services/app-connection/app-connectio
|
||||
import { TAppConnection } from "@app/services/app-connection/app-connection-types";
|
||||
import { ActorType } from "@app/services/auth/auth-type";
|
||||
import { TKmsServiceFactory } from "@app/services/kms/kms-service";
|
||||
import { TNotificationServiceFactory } from "@app/services/notification/notification-service";
|
||||
import { NotificationType } from "@app/services/notification/notification-types";
|
||||
import { TProjectDALFactory } from "@app/services/project/project-dal";
|
||||
import { TProjectMembershipDALFactory } from "@app/services/project-membership/project-membership-dal";
|
||||
import { SmtpTemplates, TSmtpService } from "@app/services/smtp/smtp-service";
|
||||
@@ -52,6 +54,7 @@ type TSecretRotationV2QueueServiceFactoryDep = {
|
||||
appConnectionDAL: Pick<TAppConnectionDALFactory, "updateById">;
|
||||
auditLogService: Pick<TAuditLogServiceFactory, "createAuditLog">;
|
||||
keyStore: Pick<TKeyStoreFactory, "acquireLock" | "getItem">;
|
||||
notificationService: Pick<TNotificationServiceFactory, "createUserNotifications">;
|
||||
};
|
||||
|
||||
export type TSecretScanningV2QueueServiceFactory = Awaited<ReturnType<typeof secretScanningV2QueueServiceFactory>>;
|
||||
@@ -65,7 +68,8 @@ export const secretScanningV2QueueServiceFactory = async ({
|
||||
kmsService,
|
||||
auditLogService,
|
||||
keyStore,
|
||||
appConnectionDAL
|
||||
appConnectionDAL,
|
||||
notificationService
|
||||
}: TSecretRotationV2QueueServiceFactoryDep) => {
|
||||
const queueDataSourceFullScan = async (
|
||||
dataSource: TSecretScanningDataSourceWithConnection,
|
||||
@@ -592,16 +596,38 @@ export const secretScanningV2QueueServiceFactory = async ({
|
||||
|
||||
const timestamp = new Date().toISOString();
|
||||
|
||||
const subjectLine =
|
||||
payload.status === SecretScanningScanStatus.Completed
|
||||
? "Incident Alert: Secret(s) Leaked"
|
||||
: `Secret Scanning Failed`;
|
||||
|
||||
await notificationService.createUserNotifications(
|
||||
recipients.map((member) => ({
|
||||
userId: member.userId,
|
||||
orgId: project.orgId,
|
||||
type:
|
||||
payload.status === SecretScanningScanStatus.Completed
|
||||
? NotificationType.SECRET_SCANNING_SECRETS_DETECTED
|
||||
: NotificationType.SECRET_SCANNING_SCAN_FAILED,
|
||||
title: subjectLine,
|
||||
body:
|
||||
payload.status === SecretScanningScanStatus.Completed
|
||||
? `Uncovered **${payload.numberOfSecrets}** secret(s) ${payload.isDiffScan ? " from a recent commit to" : " in"} **${resourceName}**.`
|
||||
: `Encountered an error while attempting to scan the resource **${resourceName}**: ${payload.errorMessage}`,
|
||||
link:
|
||||
payload.status === SecretScanningScanStatus.Completed
|
||||
? `/projects/secret-scanning/${projectId}/findings?search=scanId:${payload.scanId}`
|
||||
: `/projects/secret-scanning/${projectId}/data-sources/${dataSource.type}/${dataSource.id}`
|
||||
}))
|
||||
);
|
||||
|
||||
await smtpService.sendMail({
|
||||
recipients: recipients.map((member) => member.user.email!).filter(Boolean),
|
||||
template:
|
||||
payload.status === SecretScanningScanStatus.Completed
|
||||
? SmtpTemplates.SecretScanningV2SecretsDetected
|
||||
: SmtpTemplates.SecretScanningV2ScanFailed,
|
||||
subjectLine:
|
||||
payload.status === SecretScanningScanStatus.Completed
|
||||
? "Incident Alert: Secret(s) Leaked"
|
||||
: `Secret Scanning Failed`,
|
||||
subjectLine,
|
||||
substitutions:
|
||||
payload.status === SecretScanningScanStatus.Completed
|
||||
? {
|
||||
|
||||
@@ -231,6 +231,8 @@ export type TQueueJobTypes = {
|
||||
[QueueName.ImportSecretsFromExternalSource]: {
|
||||
name: QueueJobs.ImportSecretsFromExternalSource;
|
||||
payload: {
|
||||
orgId: string;
|
||||
actorId: string;
|
||||
actorEmail: string;
|
||||
importType: ExternalPlatforms;
|
||||
data: {
|
||||
|
||||
@@ -773,7 +773,8 @@ export const registerRoutes = async (
|
||||
orgDAL,
|
||||
totpService,
|
||||
orgMembershipDAL,
|
||||
auditLogService
|
||||
auditLogService,
|
||||
notificationService
|
||||
});
|
||||
const passwordService = authPaswordServiceFactory({
|
||||
tokenService,
|
||||
@@ -887,7 +888,8 @@ export const registerRoutes = async (
|
||||
projectDAL,
|
||||
permissionService,
|
||||
projectUserMembershipRoleDAL,
|
||||
projectMembershipDAL
|
||||
projectMembershipDAL,
|
||||
notificationService
|
||||
});
|
||||
|
||||
const rateLimitService = rateLimitServiceFactory({
|
||||
@@ -926,7 +928,8 @@ export const registerRoutes = async (
|
||||
projectRoleDAL,
|
||||
groupProjectDAL,
|
||||
secretReminderRecipientsDAL,
|
||||
licenseService
|
||||
licenseService,
|
||||
notificationService
|
||||
});
|
||||
const projectUserAdditionalPrivilegeService = projectUserAdditionalPrivilegeServiceFactory({
|
||||
permissionService,
|
||||
@@ -1145,7 +1148,8 @@ export const registerRoutes = async (
|
||||
appConnectionDAL,
|
||||
licenseService,
|
||||
gatewayService,
|
||||
gatewayV2Service
|
||||
gatewayV2Service,
|
||||
notificationService
|
||||
});
|
||||
|
||||
const secretQueueService = secretQueueFactory({
|
||||
@@ -1229,7 +1233,8 @@ export const registerRoutes = async (
|
||||
projectTemplateService,
|
||||
groupProjectDAL,
|
||||
smtpService,
|
||||
reminderService
|
||||
reminderService,
|
||||
notificationService
|
||||
});
|
||||
|
||||
const projectEnvService = projectEnvServiceFactory({
|
||||
@@ -1363,7 +1368,8 @@ export const registerRoutes = async (
|
||||
resourceMetadataDAL,
|
||||
projectMicrosoftTeamsConfigDAL,
|
||||
microsoftTeamsService,
|
||||
folderCommitService
|
||||
folderCommitService,
|
||||
notificationService
|
||||
});
|
||||
|
||||
const secretService = secretServiceFactory({
|
||||
@@ -1814,7 +1820,8 @@ export const registerRoutes = async (
|
||||
secretV2BridgeService,
|
||||
resourceMetadataDAL,
|
||||
folderCommitService,
|
||||
folderVersionDAL
|
||||
folderVersionDAL,
|
||||
notificationService
|
||||
});
|
||||
|
||||
const migrationService = externalMigrationServiceFactory({
|
||||
@@ -2017,7 +2024,8 @@ export const registerRoutes = async (
|
||||
queueService,
|
||||
projectDAL,
|
||||
projectMembershipDAL,
|
||||
smtpService
|
||||
smtpService,
|
||||
notificationService
|
||||
});
|
||||
|
||||
const secretScanningV2Queue = await secretScanningV2QueueServiceFactory({
|
||||
@@ -2029,7 +2037,8 @@ export const registerRoutes = async (
|
||||
smtpService,
|
||||
kmsService,
|
||||
keyStore,
|
||||
appConnectionDAL
|
||||
appConnectionDAL,
|
||||
notificationService
|
||||
});
|
||||
|
||||
const secretScanningV2Service = secretScanningV2ServiceFactory({
|
||||
|
||||
@@ -3,8 +3,10 @@ import { z } from "zod";
|
||||
import { UserNotificationsSchema } from "@app/db/schemas/user-notifications";
|
||||
import { UnauthorizedError } from "@app/lib/errors";
|
||||
import { readLimit, writeLimit } from "@app/server/config/rateLimiter";
|
||||
import { getTelemetryDistinctId } from "@app/server/lib/telemetry";
|
||||
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
|
||||
import { AuthMode } from "@app/services/auth/auth-type";
|
||||
import { PostHogEventTypes } from "@app/services/telemetry/telemetry-types";
|
||||
|
||||
export const registerNotificationRouter = async (server: FastifyZodProvider) => {
|
||||
server.route({
|
||||
@@ -97,6 +99,16 @@ export const registerNotificationRouter = async (server: FastifyZodProvider) =>
|
||||
...req.body
|
||||
});
|
||||
|
||||
await server.services.telemetry.sendPostHogEvents({
|
||||
event: PostHogEventTypes.NotificationUpdated,
|
||||
distinctId: getTelemetryDistinctId(req),
|
||||
organizationId: req.permission.orgId,
|
||||
properties: {
|
||||
notificationId: req.params.notificationId,
|
||||
...req.body
|
||||
}
|
||||
});
|
||||
|
||||
return { notification };
|
||||
}
|
||||
});
|
||||
|
||||
@@ -14,6 +14,8 @@ import { getServerCfg } from "@app/services/super-admin/super-admin-service";
|
||||
|
||||
import { TAuthTokenServiceFactory } from "../auth-token/auth-token-service";
|
||||
import { TokenType } from "../auth-token/auth-token-types";
|
||||
import { TNotificationServiceFactory } from "../notification/notification-service";
|
||||
import { NotificationType } from "../notification/notification-types";
|
||||
import { TOrgDALFactory } from "../org/org-dal";
|
||||
import { getDefaultOrgMembershipRole } from "../org/org-role-fns";
|
||||
import { TOrgMembershipDALFactory } from "../org-membership/org-membership-dal";
|
||||
@@ -47,6 +49,7 @@ type TAuthLoginServiceFactoryDep = {
|
||||
totpService: Pick<TTotpServiceFactory, "verifyUserTotp" | "verifyWithUserRecoveryCode">;
|
||||
auditLogService: Pick<TAuditLogServiceFactory, "createAuditLog">;
|
||||
orgMembershipDAL: TOrgMembershipDALFactory;
|
||||
notificationService: Pick<TNotificationServiceFactory, "createUserNotifications">;
|
||||
};
|
||||
|
||||
export type TAuthLoginFactory = ReturnType<typeof authLoginServiceFactory>;
|
||||
@@ -57,7 +60,8 @@ export const authLoginServiceFactory = ({
|
||||
orgDAL,
|
||||
orgMembershipDAL,
|
||||
totpService,
|
||||
auditLogService
|
||||
auditLogService,
|
||||
notificationService
|
||||
}: TAuthLoginServiceFactoryDep) => {
|
||||
/*
|
||||
* Private
|
||||
@@ -71,6 +75,16 @@ export const authLoginServiceFactory = ({
|
||||
if (!isDeviceSeen) {
|
||||
const newDeviceList = devices.concat([{ ip, userAgent }]);
|
||||
await userDAL.updateById(user.id, { devices: JSON.stringify(newDeviceList) }, tx);
|
||||
|
||||
await notificationService.createUserNotifications([
|
||||
{
|
||||
userId: user.id,
|
||||
type: NotificationType.LOGIN_FROM_NEW_DEVICE,
|
||||
title: "Login From New Device",
|
||||
body: `A new device with IP **${ip}** and User Agent **${userAgent}** has logged into your account.`
|
||||
}
|
||||
]);
|
||||
|
||||
if (user.email) {
|
||||
await smtpService.sendMail({
|
||||
template: SmtpTemplates.NewDeviceJoin,
|
||||
@@ -563,6 +577,18 @@ export const authLoginServiceFactory = ({
|
||||
.filter(Boolean) as string[];
|
||||
|
||||
if (adminEmails.length > 0) {
|
||||
await notificationService.createUserNotifications(
|
||||
orgAdmins
|
||||
.filter((admin) => admin.user.id !== user.id)
|
||||
.map((admin) => ({
|
||||
userId: admin.user.id,
|
||||
orgId: organizationId,
|
||||
type: NotificationType.ADMIN_SSO_BYPASS,
|
||||
title: "Security Alert: Admin SSO Bypass",
|
||||
body: `The org admin **${user.email}** has bypassed enforced SSO login.`
|
||||
}))
|
||||
);
|
||||
|
||||
await smtpService.sendMail({
|
||||
recipients: adminEmails,
|
||||
subjectLine: "Security Alert: Admin SSO Bypass",
|
||||
|
||||
@@ -5,6 +5,8 @@ import { QueueJobs, QueueName, TQueueServiceFactory } from "@app/queue";
|
||||
|
||||
import { TFolderCommitServiceFactory } from "../folder-commit/folder-commit-service";
|
||||
import { TKmsServiceFactory } from "../kms/kms-service";
|
||||
import { TNotificationServiceFactory } from "../notification/notification-service";
|
||||
import { NotificationType } from "../notification/notification-types";
|
||||
import { TProjectDALFactory } from "../project/project-dal";
|
||||
import { TProjectServiceFactory } from "../project/project-service";
|
||||
import { TProjectEnvDALFactory } from "../project-env/project-env-dal";
|
||||
@@ -42,6 +44,7 @@ export type TExternalMigrationQueueFactoryDep = {
|
||||
folderVersionDAL: Pick<TSecretFolderVersionDALFactory, "create">;
|
||||
|
||||
resourceMetadataDAL: Pick<TResourceMetadataDALFactory, "insertMany" | "delete">;
|
||||
notificationService: Pick<TNotificationServiceFactory, "createUserNotifications">;
|
||||
};
|
||||
|
||||
export type TExternalMigrationQueueFactory = ReturnType<typeof externalMigrationQueueFactory>;
|
||||
@@ -62,9 +65,12 @@ export const externalMigrationQueueFactory = ({
|
||||
folderDAL,
|
||||
folderCommitService,
|
||||
folderVersionDAL,
|
||||
resourceMetadataDAL
|
||||
resourceMetadataDAL,
|
||||
notificationService
|
||||
}: TExternalMigrationQueueFactoryDep) => {
|
||||
const startImport = async (dto: {
|
||||
orgId: string;
|
||||
actorId: string;
|
||||
actorEmail: string;
|
||||
importType: ExternalPlatforms;
|
||||
data: {
|
||||
@@ -87,9 +93,19 @@ export const externalMigrationQueueFactory = ({
|
||||
};
|
||||
|
||||
queueService.start(QueueName.ImportSecretsFromExternalSource, async (job) => {
|
||||
const { data, actorEmail, importType } = job.data;
|
||||
const { data, actorEmail, importType, actorId, orgId } = job.data;
|
||||
|
||||
try {
|
||||
await notificationService.createUserNotifications([
|
||||
{
|
||||
userId: actorId,
|
||||
orgId,
|
||||
type: NotificationType.IMPORT_STARTED,
|
||||
title: "Import Started",
|
||||
body: `An import from **${importType}** to Infisical has been started.`
|
||||
}
|
||||
]);
|
||||
|
||||
await smtpService.sendMail({
|
||||
recipients: [actorEmail],
|
||||
subjectLine: "Infisical import started",
|
||||
@@ -137,6 +153,16 @@ export const externalMigrationQueueFactory = ({
|
||||
);
|
||||
}
|
||||
|
||||
await notificationService.createUserNotifications([
|
||||
{
|
||||
userId: actorId,
|
||||
orgId,
|
||||
type: NotificationType.IMPORT_SUCCESSFUL,
|
||||
title: "Import Successful",
|
||||
body: `An import from **${importType}** to Infisical has successfully completed.`
|
||||
}
|
||||
]);
|
||||
|
||||
await smtpService.sendMail({
|
||||
recipients: [actorEmail],
|
||||
subjectLine: "Infisical import successful",
|
||||
@@ -146,6 +172,17 @@ export const externalMigrationQueueFactory = ({
|
||||
template: SmtpTemplates.ExternalImportSuccessful
|
||||
});
|
||||
} catch (err) {
|
||||
await notificationService.createUserNotifications([
|
||||
{
|
||||
userId: actorId,
|
||||
orgId,
|
||||
type: NotificationType.IMPORT_FAILED,
|
||||
title: "Import Failed",
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-member-access
|
||||
body: `An import from **${importType}** to Infisical has failed: ${(err as any)?.message || "Unknown error"}.`
|
||||
}
|
||||
]);
|
||||
|
||||
await smtpService.sendMail({
|
||||
recipients: [job.data.actorEmail],
|
||||
subjectLine: "Infisical import failed",
|
||||
|
||||
@@ -73,6 +73,8 @@ export const externalMigrationServiceFactory = ({
|
||||
const encrypted = crypto.encryption().symmetric().encryptWithRootEncryptionKey(stringifiedJson);
|
||||
|
||||
await externalMigrationQueue.startImport({
|
||||
actorId: user.id,
|
||||
orgId: actorOrgId,
|
||||
actorEmail: user.email!,
|
||||
importType: ExternalPlatforms.EnvKey,
|
||||
data: {
|
||||
@@ -131,6 +133,8 @@ export const externalMigrationServiceFactory = ({
|
||||
const encrypted = crypto.encryption().symmetric().encryptWithRootEncryptionKey(stringifiedJson);
|
||||
|
||||
await externalMigrationQueue.startImport({
|
||||
actorId: user.id,
|
||||
orgId: actorOrgId,
|
||||
actorEmail: user.email!,
|
||||
importType: ExternalPlatforms.Vault,
|
||||
data: {
|
||||
|
||||
@@ -1,6 +1,21 @@
|
||||
export enum NotificationType {
|
||||
ACCESS_APPROVAL_REQUEST = "access-approval-request",
|
||||
ACCESS_APPROVAL_REQUEST_UPDATED = "access-approval-request-updated"
|
||||
ACCESS_APPROVAL_REQUEST_UPDATED = "access-approval-request-updated",
|
||||
ACCESS_POLICY_BYPASSED = "access-policy-bypassed",
|
||||
SECRET_CHANGE_REQUEST = "secret-change-request",
|
||||
SECRET_CHANGE_POLICY_BYPASSED = "secret-change-policy-bypassed",
|
||||
SECRET_ROTATION_FAILED = "secret-rotation-failed",
|
||||
SECRET_SCANNING_SECRETS_DETECTED = "secret-scanning-secrets-detected",
|
||||
SECRET_SCANNING_SCAN_FAILED = "secret-scanning-scan-failed",
|
||||
LOGIN_FROM_NEW_DEVICE = "login-from-new-device",
|
||||
ADMIN_SSO_BYPASS = "admin-sso-bypass",
|
||||
IMPORT_STARTED = "import-started",
|
||||
IMPORT_SUCCESSFUL = "import-successful",
|
||||
IMPORT_FAILED = "import-failed",
|
||||
DIRECT_PROJECT_ACCESS_ISSUED_TO_ADMIN = "direct-project-access-issued-to-admin",
|
||||
PROJECT_ACCESS_REQUEST = "project-access-request",
|
||||
PROJECT_INVITATION = "project-invitation",
|
||||
SECRET_SYNC_FAILED = "secret-sync-failed"
|
||||
}
|
||||
|
||||
export interface TCreateUserNotificationDTO {
|
||||
|
||||
@@ -5,6 +5,8 @@ import { OrgPermissionAdminConsoleAction, OrgPermissionSubjects } from "@app/ee/
|
||||
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service-types";
|
||||
import { BadRequestError, NotFoundError } from "@app/lib/errors";
|
||||
|
||||
import { TNotificationServiceFactory } from "../notification/notification-service";
|
||||
import { NotificationType } from "../notification/notification-types";
|
||||
import { TProjectDALFactory } from "../project/project-dal";
|
||||
import { TProjectMembershipDALFactory } from "../project-membership/project-membership-dal";
|
||||
import { TProjectUserMembershipRoleDALFactory } from "../project-membership/project-user-membership-role-dal";
|
||||
@@ -20,6 +22,7 @@ type TOrgAdminServiceFactoryDep = {
|
||||
>;
|
||||
projectUserMembershipRoleDAL: Pick<TProjectUserMembershipRoleDALFactory, "create" | "delete">;
|
||||
smtpService: Pick<TSmtpService, "sendMail">;
|
||||
notificationService: Pick<TNotificationServiceFactory, "createUserNotifications">;
|
||||
};
|
||||
|
||||
export type TOrgAdminServiceFactory = ReturnType<typeof orgAdminServiceFactory>;
|
||||
@@ -29,7 +32,8 @@ export const orgAdminServiceFactory = ({
|
||||
projectDAL,
|
||||
projectMembershipDAL,
|
||||
projectUserMembershipRoleDAL,
|
||||
smtpService
|
||||
smtpService,
|
||||
notificationService
|
||||
}: TOrgAdminServiceFactoryDep) => {
|
||||
const listOrgProjects = async ({
|
||||
actor,
|
||||
@@ -130,23 +134,34 @@ export const orgAdminServiceFactory = ({
|
||||
});
|
||||
|
||||
const projectMembers = await projectMembershipDAL.findAllProjectMembers(projectId);
|
||||
const filteredProjectMembers = projectMembers
|
||||
.filter(
|
||||
(member) => member.roles.some((role) => role.role === ProjectMembershipRole.Admin) && member.userId !== actorId
|
||||
)
|
||||
.map((el) => el.user.email!)
|
||||
.filter(Boolean);
|
||||
const projectAdmins = projectMembers.filter(
|
||||
(member) => member.roles.some((role) => role.role === ProjectMembershipRole.Admin) && member.userId !== actorId
|
||||
);
|
||||
const mappedProjectAdmins = projectAdmins.map((el) => el.user.email!).filter(Boolean);
|
||||
const actorEmail = projectMembers.find((el) => el.userId === actorId)?.user?.username;
|
||||
|
||||
if (filteredProjectMembers.length) {
|
||||
await smtpService.sendMail({
|
||||
template: SmtpTemplates.OrgAdminProjectDirectAccess,
|
||||
recipients: filteredProjectMembers,
|
||||
subjectLine: "Organization Admin Project Direct Access Issued",
|
||||
substitutions: {
|
||||
projectName: project.name,
|
||||
email: projectMembers.find((el) => el.userId === actorId)?.user?.username
|
||||
}
|
||||
});
|
||||
if (actorEmail) {
|
||||
await notificationService.createUserNotifications(
|
||||
projectAdmins.map((member) => ({
|
||||
userId: member.userId,
|
||||
orgId: project.orgId,
|
||||
type: NotificationType.DIRECT_PROJECT_ACCESS_ISSUED_TO_ADMIN,
|
||||
title: "Direct Project Access Issued",
|
||||
body: `The organization admin **${actorEmail}** has self-issued direct access to the project **${project.name}**.`
|
||||
}))
|
||||
);
|
||||
|
||||
if (mappedProjectAdmins.length) {
|
||||
await smtpService.sendMail({
|
||||
template: SmtpTemplates.OrgAdminProjectDirectAccess,
|
||||
recipients: mappedProjectAdmins,
|
||||
subjectLine: "Organization Admin Project Direct Access Issued",
|
||||
substitutions: {
|
||||
projectName: project.name,
|
||||
email: actorEmail
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
return { isExistingMember: false, membership: updatedMembership };
|
||||
};
|
||||
|
||||
@@ -18,6 +18,8 @@ import { ms } from "@app/lib/ms";
|
||||
import { TUserGroupMembershipDALFactory } from "../../ee/services/group/user-group-membership-dal";
|
||||
import { ActorType } from "../auth/auth-type";
|
||||
import { TGroupProjectDALFactory } from "../group-project/group-project-dal";
|
||||
import { TNotificationServiceFactory } from "../notification/notification-service";
|
||||
import { NotificationType } from "../notification/notification-types";
|
||||
import { TOrgDALFactory } from "../org/org-dal";
|
||||
import { TProjectDALFactory } from "../project/project-dal";
|
||||
import { TProjectBotDALFactory } from "../project-bot/project-bot-dal";
|
||||
@@ -56,6 +58,7 @@ type TProjectMembershipServiceFactoryDep = {
|
||||
projectUserAdditionalPrivilegeDAL: Pick<TProjectUserAdditionalPrivilegeDALFactory, "delete">;
|
||||
secretReminderRecipientsDAL: Pick<TSecretReminderRecipientsDALFactory, "delete">;
|
||||
groupProjectDAL: TGroupProjectDALFactory;
|
||||
notificationService: Pick<TNotificationServiceFactory, "createUserNotifications">;
|
||||
};
|
||||
|
||||
export type TProjectMembershipServiceFactory = ReturnType<typeof projectMembershipServiceFactory>;
|
||||
@@ -74,7 +77,8 @@ export const projectMembershipServiceFactory = ({
|
||||
projectDAL,
|
||||
projectKeyDAL,
|
||||
secretReminderRecipientsDAL,
|
||||
licenseService
|
||||
licenseService,
|
||||
notificationService
|
||||
}: TProjectMembershipServiceFactoryDep) => {
|
||||
const getProjectMemberships = async ({
|
||||
actorId,
|
||||
@@ -236,6 +240,16 @@ export const projectMembershipServiceFactory = ({
|
||||
});
|
||||
|
||||
if (sendEmails) {
|
||||
await notificationService.createUserNotifications(
|
||||
orgMembers.map((member) => ({
|
||||
userId: member.userId,
|
||||
orgId: project.orgId,
|
||||
type: NotificationType.PROJECT_INVITATION,
|
||||
title: "Project Invitation",
|
||||
body: `You've been invited to join the project **${project.name}**.`
|
||||
}))
|
||||
);
|
||||
|
||||
const appCfg = getConfig();
|
||||
await smtpService.sendMail({
|
||||
template: SmtpTemplates.WorkspaceInvite,
|
||||
|
||||
@@ -57,6 +57,8 @@ import { TKmsServiceFactory } from "../kms/kms-service";
|
||||
import { validateMicrosoftTeamsChannelsSchema } from "../microsoft-teams/microsoft-teams-fns";
|
||||
import { TMicrosoftTeamsIntegrationDALFactory } from "../microsoft-teams/microsoft-teams-integration-dal";
|
||||
import { TProjectMicrosoftTeamsConfigDALFactory } from "../microsoft-teams/project-microsoft-teams-config-dal";
|
||||
import { TNotificationServiceFactory } from "../notification/notification-service";
|
||||
import { NotificationType } from "../notification/notification-types";
|
||||
import { TOrgDALFactory } from "../org/org-dal";
|
||||
import { TPkiAlertDALFactory } from "../pki-alert/pki-alert-dal";
|
||||
import { TPkiCollectionDALFactory } from "../pki-collection/pki-collection-dal";
|
||||
@@ -183,6 +185,7 @@ type TProjectServiceFactoryDep = {
|
||||
>;
|
||||
projectTemplateService: TProjectTemplateServiceFactory;
|
||||
reminderService: Pick<TReminderServiceFactory, "deleteReminderBySecretId">;
|
||||
notificationService: Pick<TNotificationServiceFactory, "createUserNotifications">;
|
||||
};
|
||||
|
||||
export type TProjectServiceFactory = ReturnType<typeof projectServiceFactory>;
|
||||
@@ -227,7 +230,8 @@ export const projectServiceFactory = ({
|
||||
projectTemplateService,
|
||||
groupProjectDAL,
|
||||
smtpService,
|
||||
reminderService
|
||||
reminderService,
|
||||
notificationService
|
||||
}: TProjectServiceFactoryDep) => {
|
||||
/*
|
||||
* Create workspace. Make user the admin
|
||||
@@ -1924,6 +1928,21 @@ export const projectServiceFactory = ({
|
||||
projectTypeUrl = "cert-management";
|
||||
}
|
||||
|
||||
const callbackPath = `/projects/${projectTypeUrl}/${project.id}/access-management?selectedTab=members&requesterEmail=${userDetails.email}`;
|
||||
|
||||
await notificationService.createUserNotifications(
|
||||
projectMembers
|
||||
.filter((member) => member.roles.some((role) => role.role === ProjectMembershipRole.Admin))
|
||||
.map((member) => ({
|
||||
userId: member.userId,
|
||||
orgId: project.orgId,
|
||||
type: NotificationType.PROJECT_ACCESS_REQUEST,
|
||||
title: "Project Access Request",
|
||||
body: `**${userDetails.firstName} ${userDetails.lastName}** (${userDetails.email}) has requested access to the project **${project.name}**.`,
|
||||
link: callbackPath
|
||||
}))
|
||||
);
|
||||
|
||||
await smtpService.sendMail({
|
||||
template: SmtpTemplates.ProjectAccessRequest,
|
||||
recipients: filteredProjectMembers,
|
||||
@@ -1934,7 +1953,7 @@ export const projectServiceFactory = ({
|
||||
projectName: project?.name,
|
||||
orgName: org?.name,
|
||||
note: comment,
|
||||
callback_url: `${appCfg.SITE_URL}/projects/${projectTypeUrl}/${project.id}/access-management?selectedTab=members&requesterEmail=${userDetails.email}`
|
||||
callback_url: `${appCfg.SITE_URL}${callbackPath}`
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
@@ -61,6 +61,8 @@ import { SmtpTemplates, TSmtpService } from "@app/services/smtp/smtp-service";
|
||||
|
||||
import { TAppConnectionDALFactory } from "../app-connection/app-connection-dal";
|
||||
import { TFolderCommitServiceFactory } from "../folder-commit/folder-commit-service";
|
||||
import { TNotificationServiceFactory } from "../notification/notification-service";
|
||||
import { NotificationType } from "../notification/notification-types";
|
||||
|
||||
export type TSecretSyncQueueFactory = ReturnType<typeof secretSyncQueueFactory>;
|
||||
|
||||
@@ -100,6 +102,7 @@ type TSecretSyncQueueFactoryDep = {
|
||||
licenseService: Pick<TLicenseServiceFactory, "getPlan">;
|
||||
gatewayService: Pick<TGatewayServiceFactory, "fnGetGatewayClientTlsByGatewayId">;
|
||||
gatewayV2Service: Pick<TGatewayV2ServiceFactory, "getPlatformConnectionDetailsByGatewayId">;
|
||||
notificationService: Pick<TNotificationServiceFactory, "createUserNotifications">;
|
||||
};
|
||||
|
||||
type SecretSyncActionJob = Job<
|
||||
@@ -142,7 +145,8 @@ export const secretSyncQueueFactory = ({
|
||||
folderCommitService,
|
||||
licenseService,
|
||||
gatewayService,
|
||||
gatewayV2Service
|
||||
gatewayV2Service,
|
||||
notificationService
|
||||
}: TSecretSyncQueueFactoryDep) => {
|
||||
const appCfg = getConfig();
|
||||
|
||||
@@ -898,6 +902,19 @@ export const secretSyncQueueFactory = ({
|
||||
break;
|
||||
}
|
||||
|
||||
const syncPath = `/projects/secret-management/${projectId}/integrations/secret-syncs/${destination}/${secretSync.id}`;
|
||||
|
||||
await notificationService.createUserNotifications(
|
||||
projectAdmins.map((admin) => ({
|
||||
userId: admin.userId,
|
||||
orgId: project.orgId,
|
||||
type: NotificationType.SECRET_SYNC_FAILED,
|
||||
title: `Secret Sync Failed to ${actionLabel} Secrets`,
|
||||
body: `Your **${syncDestination}** sync **${name}** failed to complete${failureMessage ? `: \`${failureMessage}\`` : ""}`,
|
||||
link: syncPath
|
||||
}))
|
||||
);
|
||||
|
||||
await smtpService.sendMail({
|
||||
recipients: projectAdmins.map((member) => member.user.email!).filter(Boolean),
|
||||
template: SmtpTemplates.SecretSyncFailed,
|
||||
@@ -910,7 +927,7 @@ export const secretSyncQueueFactory = ({
|
||||
secretPath: folder?.path,
|
||||
environment: environment?.name,
|
||||
projectName: project.name,
|
||||
syncUrl: `${appCfg.SITE_URL}/projects/secret-management/${projectId}/integrations/secret-syncs/${destination}/${secretSync.id}`
|
||||
syncUrl: `${appCfg.SITE_URL}${syncPath}`
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
@@ -32,7 +32,8 @@ export enum PostHogEventTypes {
|
||||
IssueSshHostHostCert = "Issue SSH Host Host Certificate",
|
||||
SignCert = "Sign PKI Certificate",
|
||||
IssueCert = "Issue PKI Certificate",
|
||||
InvalidateCache = "Invalidate Cache"
|
||||
InvalidateCache = "Invalidate Cache",
|
||||
NotificationUpdated = "Notification Updated"
|
||||
}
|
||||
|
||||
export type TSecretModifiedEvent = {
|
||||
@@ -232,6 +233,14 @@ export type TInvalidateCacheEvent = {
|
||||
};
|
||||
};
|
||||
|
||||
export type TNotificationUpdatedEvent = {
|
||||
event: PostHogEventTypes.NotificationUpdated;
|
||||
properties: {
|
||||
notificationId: string;
|
||||
isRead?: boolean;
|
||||
};
|
||||
};
|
||||
|
||||
export type TPostHogEvent = { distinctId: string; organizationId?: string } & (
|
||||
| TSecretModifiedEvent
|
||||
| TAdminInitEvent
|
||||
@@ -251,4 +260,5 @@ export type TPostHogEvent = { distinctId: string; organizationId?: string } & (
|
||||
| TSignCertificateEvent
|
||||
| TIssueCertificateEvent
|
||||
| TInvalidateCacheEvent
|
||||
| TNotificationUpdatedEvent
|
||||
);
|
||||
|
||||
@@ -26,7 +26,11 @@ export const Notification = ({ notification, onDelete }: Props) => {
|
||||
{!notification.isRead && (
|
||||
<FontAwesomeIcon icon={faCircle} className="mt-1.5 size-2 text-yellow-400" />
|
||||
)}
|
||||
<Tooltip content={<Markdown>{notification.title}</Markdown>} delayDuration={300}>
|
||||
<Tooltip
|
||||
content={<Markdown>{notification.title}</Markdown>}
|
||||
delayDuration={300}
|
||||
className="z-[1000]"
|
||||
>
|
||||
<span className="overflow-hidden text-ellipsis whitespace-nowrap text-sm font-medium leading-5 text-mineshaft-100">
|
||||
<Markdown components={{ p: "span" }}>{notification.title}</Markdown>
|
||||
</span>
|
||||
|
||||
@@ -46,7 +46,7 @@ export const NotificationDropdown = () => {
|
||||
<DropdownMenuContent
|
||||
align="end"
|
||||
side="bottom"
|
||||
className="mt-3 flex h-[550px] w-[400px] overflow-hidden rounded-lg"
|
||||
className="z-[999] mt-3 flex h-[550px] w-[400px] overflow-hidden rounded-lg"
|
||||
>
|
||||
<div className="flex w-full flex-col">
|
||||
<div className="flex items-center justify-between border-b border-mineshaft-500 px-3 py-2">
|
||||
|
||||
Reference in New Issue
Block a user