Files
oneuptime/Common/Server/Services/TeamComplianceService.ts

557 lines
15 KiB
TypeScript

import TeamComplianceSettingService from "./TeamComplianceSettingService";
import TeamMemberService from "./TeamMemberService";
import UserEmailService from "./UserEmailService";
import UserSmsService from "./UserSmsService";
import UserCallService from "./UserCallService";
import UserPushService from "./UserPushService";
import IncidentSeverityService from "./IncidentSeverityService";
import AlertSeverityService from "./AlertSeverityService";
import UserNotificationRuleService from "./UserNotificationRuleService";
import UserService from "./UserService";
import TeamService from "./TeamService";
import ObjectID from "../../Types/ObjectID";
import ComplianceRuleType from "../../Types/Team/ComplianceRuleType";
import BadDataException from "../../Types/Exception/BadDataException";
import Includes from "../../Types/BaseDatabase/Includes";
import { LIMIT_PER_PROJECT } from "../../Types/Database/LimitMax";
import Team from "../../Models/DatabaseModels/Team";
import IncidentSeverity from "../../Models/DatabaseModels/IncidentSeverity";
import AlertSeverity from "../../Models/DatabaseModels/AlertSeverity";
export interface UserComplianceStatus {
userId: ObjectID;
userName: string;
userEmail: string;
userProfilePictureId?: ObjectID | undefined;
isCompliant: boolean;
nonCompliantRules: Array<{
ruleType: ComplianceRuleType;
reason: string;
}>;
}
export interface TeamComplianceStatus {
teamId: ObjectID;
teamName: string;
complianceSettings: Array<{
ruleType: ComplianceRuleType;
enabled: boolean;
}>;
userComplianceStatuses: Array<UserComplianceStatus>;
}
export default class TeamComplianceService {
public static async getTeamComplianceStatus(
teamId: ObjectID,
projectId: ObjectID,
): Promise<TeamComplianceStatus> {
// Get team details
const team: Partial<Team> | null = await TeamService.findOneById({
id: teamId,
select: {
name: true,
_id: true,
},
props: {
isRoot: true,
},
});
if (!team) {
throw new BadDataException("Team not found");
}
// Get compliance settings for this team
const complianceSettings: Array<{
ruleType?: ComplianceRuleType;
enabled?: boolean;
}> = await TeamComplianceSettingService.findBy({
query: {
teamId: teamId,
projectId: projectId,
},
select: {
ruleType: true,
enabled: true,
},
limit: 100,
skip: 0,
props: {
isRoot: true,
},
});
// Get team members
const teamMembers: Array<{ userId?: ObjectID; _id?: string }> =
await TeamMemberService.findBy({
query: {
teamId: teamId,
projectId: projectId,
},
select: {
userId: true,
_id: true,
},
limit: 100,
skip: 0,
props: {
isRoot: true,
},
});
const userIds: Array<ObjectID> = teamMembers
.map((member: { userId?: ObjectID; _id?: string }) => {
return member.userId!;
})
.filter(Boolean);
// Get user details
const users: any = await UserService.findBy({
query: {
_id: new Includes(userIds),
},
select: {
name: true,
email: true,
_id: true,
profilePictureId: true,
},
limit: 100,
skip: 0,
props: {
isRoot: true,
},
});
// Check compliance for each user
const userComplianceStatuses: Array<UserComplianceStatus> = [];
for (const user of users) {
const complianceStatus: {
isCompliant: boolean;
nonCompliantRules: Array<{
ruleType: ComplianceRuleType;
reason: string;
}>;
} = await this.checkUserCompliance(
user.id!,
projectId,
complianceSettings.map(
(setting: { ruleType?: ComplianceRuleType; enabled?: boolean }) => {
return {
ruleType: setting.ruleType!,
enabled: setting.enabled || false,
};
},
),
);
userComplianceStatuses.push({
userId: user.id!,
userName:
user.name?.toString() || user.email?.toString() || "Unknown User",
userEmail: user.email?.toString() || "",
userProfilePictureId: user.profilePictureId,
...complianceStatus,
});
}
return {
teamId: teamId,
teamName: team.name || "Unknown Team",
complianceSettings: complianceSettings.map(
(setting: { ruleType?: ComplianceRuleType; enabled?: boolean }) => {
return {
ruleType: setting.ruleType!,
enabled: setting.enabled || false,
};
},
),
userComplianceStatuses,
};
}
private static async checkUserCompliance(
userId: ObjectID,
projectId: ObjectID,
complianceSettings: Array<{
ruleType: ComplianceRuleType;
enabled: boolean;
}>,
): Promise<{
isCompliant: boolean;
nonCompliantRules: Array<{ ruleType: ComplianceRuleType; reason: string }>;
}> {
const nonCompliantRules: Array<{
ruleType: ComplianceRuleType;
reason: string;
}> = [];
// Check each enabled compliance rule
for (const setting of complianceSettings) {
if (!setting.enabled) {
continue;
}
const isCompliant: { compliant: boolean; reason: string } =
await this.checkRuleCompliance(userId, projectId, setting.ruleType);
if (!isCompliant.compliant) {
nonCompliantRules.push({
ruleType: setting.ruleType,
reason: isCompliant.reason,
});
}
}
return {
isCompliant: nonCompliantRules.length === 0,
nonCompliantRules,
};
}
private static async checkRuleCompliance(
userId: ObjectID,
projectId: ObjectID,
ruleType: ComplianceRuleType,
): Promise<{ compliant: boolean; reason: string }> {
switch (ruleType) {
case ComplianceRuleType.HasNotificationEmailMethod:
return await this.checkHasNotificationEmail(userId, projectId);
case ComplianceRuleType.HasNotificationSMSMethod:
return await this.checkHasNotificationSMS(userId, projectId);
case ComplianceRuleType.HasNotificationCallMethod:
return await this.checkHasNotificationCall(userId, projectId);
case ComplianceRuleType.HasNotificationPushMethod:
return await this.checkHasNotificationPush(userId, projectId);
case ComplianceRuleType.HasIncidentOnCallRules:
return await this.checkHasIncidentOnCallRules(userId, projectId);
case ComplianceRuleType.HasAlertOnCallRules:
return await this.checkHasAlertOnCallRules(userId, projectId);
default:
return { compliant: true, reason: "" };
}
}
private static async checkHasNotificationEmail(
userId: ObjectID,
projectId: ObjectID,
): Promise<{ compliant: boolean; reason: string }> {
const userEmails: Array<{ _id?: string }> = await UserEmailService.findBy({
query: {
userId: userId,
projectId: projectId,
isVerified: true,
},
select: {
_id: true,
},
props: {
isRoot: true,
},
limit: 1,
skip: 0,
});
const hasEmail: boolean = userEmails.length > 0;
return {
compliant: hasEmail,
reason: hasEmail
? ""
: "No verified email address configured for notifications",
};
}
private static async checkHasNotificationSMS(
userId: ObjectID,
projectId: ObjectID,
): Promise<{ compliant: boolean; reason: string }> {
const userSMS: Array<{ _id?: string }> = await UserSmsService.findBy({
query: {
userId: userId,
projectId: projectId,
isVerified: true,
},
select: {
_id: true,
},
props: {
isRoot: true,
},
limit: 1,
skip: 0,
});
const hasSMS: boolean = userSMS.length > 0;
return {
compliant: hasSMS,
reason: hasSMS
? ""
: "No verified phone number configured for SMS notifications",
};
}
private static async checkHasNotificationCall(
userId: ObjectID,
projectId: ObjectID,
): Promise<{ compliant: boolean; reason: string }> {
const userCalls: Array<{ _id?: string }> = await UserCallService.findBy({
query: {
userId: userId,
projectId: projectId,
isVerified: true,
},
select: {
_id: true,
},
props: {
isRoot: true,
},
limit: 1,
skip: 0,
});
const hasCall: boolean = userCalls.length > 0;
return {
compliant: hasCall,
reason: hasCall
? ""
: "No verified phone number configured for call notifications",
};
}
private static async checkHasNotificationPush(
userId: ObjectID,
projectId: ObjectID,
): Promise<{ compliant: boolean; reason: string }> {
const userPush: Array<{ _id?: string }> = await UserPushService.findBy({
query: {
userId: userId,
projectId: projectId,
isVerified: true,
},
select: {
_id: true,
},
props: {
isRoot: true,
},
limit: 1,
skip: 0,
});
const hasPush: boolean = userPush.length > 0;
return {
compliant: hasPush,
reason: hasPush ? "" : "No verified push notification device configured",
};
}
private static async checkHasIncidentOnCallRules(
userId: ObjectID,
projectId: ObjectID,
): Promise<{ compliant: boolean; reason: string }> {
// Get all incident severities for the project
const incidentSeverities: Array<Partial<IncidentSeverity>> =
await IncidentSeverityService.findBy({
query: {
projectId: projectId,
},
select: {
_id: true,
name: true,
},
props: {
isRoot: true,
},
limit: LIMIT_PER_PROJECT,
skip: 0,
});
if (incidentSeverities.length === 0) {
return { compliant: true, reason: "" }; // No incident severities configured
}
// Check if user has notification rules for all incident severities
const severityIds: Array<string> = incidentSeverities.map(
(severity: Partial<IncidentSeverity>) => {
return severity._id!;
},
);
const missingSeverities: Array<string> = [];
for (const severityId of severityIds) {
const notificationRules: Array<{
_id?: string;
userCallId?: ObjectID;
userSmsId?: ObjectID;
userEmailId?: ObjectID;
userPushId?: ObjectID;
}> = await UserNotificationRuleService.findBy({
query: {
userId: userId,
projectId: projectId,
incidentSeverityId: severityId,
},
select: {
_id: true,
userCallId: true,
userSmsId: true,
userEmailId: true,
userPushId: true,
},
props: {
isRoot: true,
},
limit: 1,
skip: 0,
});
// Check if user has at least one notification method configured for this severity
const hasNotificationMethod: boolean = notificationRules.some(
(rule: {
_id?: string;
userCallId?: ObjectID;
userSmsId?: ObjectID;
userEmailId?: ObjectID;
userPushId?: ObjectID;
}) => {
return (
rule.userCallId ||
rule.userSmsId ||
rule.userEmailId ||
rule.userPushId
);
},
);
if (!hasNotificationMethod) {
const severity: Partial<IncidentSeverity> | undefined =
incidentSeverities.find((s: Partial<IncidentSeverity>) => {
return s._id?.toString() === severityId.toString();
});
const severityName: string = severity?.name || severityId.toString();
missingSeverities.push(severityName);
}
}
if (missingSeverities.length > 0) {
return {
compliant: false,
reason: `Missing notification rules for incident severities: ${missingSeverities.join(", ")}`,
};
}
return {
compliant: true,
reason: "",
};
}
private static async checkHasAlertOnCallRules(
userId: ObjectID,
projectId: ObjectID,
): Promise<{ compliant: boolean; reason: string }> {
// Get all alert severities for the project
const alertSeverities: Array<Partial<AlertSeverity>> =
await AlertSeverityService.findBy({
query: {
projectId: projectId,
},
select: {
_id: true,
name: true,
},
props: {
isRoot: true,
},
limit: 100, // Assuming reasonable limit for severities
skip: 0,
});
if (alertSeverities.length === 0) {
return { compliant: true, reason: "" }; // No alert severities configured
}
// Check if user has notification rules for all alert severities
const severityIds: Array<string> = alertSeverities.map(
(severity: Partial<AlertSeverity>) => {
return severity._id!;
},
);
const missingSeverities: Array<string> = [];
for (const severityId of severityIds) {
const notificationRules: Array<{
_id?: string;
userCallId?: ObjectID;
userSmsId?: ObjectID;
userEmailId?: ObjectID;
userPushId?: ObjectID;
}> = await UserNotificationRuleService.findBy({
query: {
userId: userId,
projectId: projectId,
alertSeverityId: severityId,
},
select: {
_id: true,
userCallId: true,
userSmsId: true,
userEmailId: true,
userPushId: true,
},
props: {
isRoot: true,
},
limit: 1,
skip: 0,
});
// Check if user has at least one notification method configured for this severity
const hasNotificationMethod: boolean = notificationRules.some(
(rule: {
_id?: string;
userCallId?: ObjectID;
userSmsId?: ObjectID;
userEmailId?: ObjectID;
userPushId?: ObjectID;
}) => {
return (
rule.userCallId ||
rule.userSmsId ||
rule.userEmailId ||
rule.userPushId
);
},
);
if (!hasNotificationMethod) {
const severity: Partial<AlertSeverity> | undefined =
alertSeverities.find((s: Partial<AlertSeverity>) => {
return s._id?.toString() === severityId.toString();
});
const severityName: string = severity?.name || severityId.toString();
missingSeverities.push(severityName);
}
}
if (missingSeverities.length > 0) {
return {
compliant: false,
reason: `Missing notification rules for alert severities: ${missingSeverities.join(", ")}`,
};
}
return {
compliant: true,
reason: "",
};
}
}