mirror of
https://github.com/OneUptime/oneuptime.git
synced 2026-04-06 08:42:13 +02:00
Compare commits
11 Commits
probe-prox
...
scim-updat
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7c17328ee3 | ||
|
|
3eeb2a9eca | ||
|
|
51fa5705b1 | ||
|
|
6f952d0a5b | ||
|
|
ade98cf1ed | ||
|
|
a65e480bb6 | ||
|
|
c777a935c3 | ||
|
|
8ec9d2a930 | ||
|
|
224c225789 | ||
|
|
85dae7a307 | ||
|
|
332a479c22 |
@@ -1,6 +1,7 @@
|
||||
import SCIMMiddleware from "Common/Server/Middleware/SCIMAuthorization";
|
||||
import UserService from "Common/Server/Services/UserService";
|
||||
import TeamMemberService from "Common/Server/Services/TeamMemberService";
|
||||
import SCIMUserService from "Common/Server/Services/SCIMUserService";
|
||||
import Express, {
|
||||
ExpressRequest,
|
||||
ExpressResponse,
|
||||
@@ -15,6 +16,7 @@ import Name from "Common/Types/Name";
|
||||
import { JSONObject } from "Common/Types/JSON";
|
||||
import TeamMember from "Common/Models/DatabaseModels/TeamMember";
|
||||
import ProjectSCIM from "Common/Models/DatabaseModels/ProjectSCIM";
|
||||
import SCIMUser from "Common/Models/DatabaseModels/SCIMUser";
|
||||
import BadRequestException from "Common/Types/Exception/BadRequestException";
|
||||
import NotFoundException from "Common/Types/Exception/NotFoundException";
|
||||
import OneUptimeDate from "Common/Types/Date";
|
||||
@@ -29,11 +31,240 @@ import {
|
||||
generateUsersListResponse,
|
||||
parseSCIMQueryParams,
|
||||
logSCIMOperation,
|
||||
extractEmailFromSCIM,
|
||||
extractExternalIdFromSCIM,
|
||||
isUserNameEmail,
|
||||
} from "../Utils/SCIMUtils";
|
||||
import { DocsClientUrl } from "Common/Server/EnvironmentConfig";
|
||||
|
||||
const router: ExpressRouter = Express.getRouter();
|
||||
|
||||
// Helper function to find user by external ID or email
|
||||
const findUserByExternalIdOrEmail: (
|
||||
userName: string,
|
||||
projectId: ObjectID,
|
||||
scimConfigId: ObjectID,
|
||||
) => Promise<User | null> = async (
|
||||
userName: string,
|
||||
projectId: ObjectID,
|
||||
scimConfigId: ObjectID,
|
||||
): Promise<User | null> => {
|
||||
// First check if userName is an external ID (not an email)
|
||||
if (!isUserNameEmail(userName)) {
|
||||
logSCIMOperation(
|
||||
"User lookup",
|
||||
"project",
|
||||
scimConfigId.toString(),
|
||||
`Looking for external ID: ${userName}`,
|
||||
);
|
||||
|
||||
// Look up by external ID
|
||||
const scimUser: SCIMUser | null = await SCIMUserService.findOneBy({
|
||||
query: {
|
||||
externalId: userName,
|
||||
projectId: projectId,
|
||||
scimConfigId: scimConfigId,
|
||||
},
|
||||
select: {
|
||||
userId: true,
|
||||
user: {
|
||||
_id: true,
|
||||
email: true,
|
||||
name: true,
|
||||
createdAt: true,
|
||||
updatedAt: true,
|
||||
},
|
||||
},
|
||||
props: { isRoot: true },
|
||||
});
|
||||
|
||||
if (scimUser && scimUser.user) {
|
||||
logSCIMOperation(
|
||||
"User lookup",
|
||||
"project",
|
||||
scimConfigId.toString(),
|
||||
`Found user by external ID: ${scimUser.user.id}`,
|
||||
);
|
||||
return scimUser.user;
|
||||
}
|
||||
}
|
||||
|
||||
// Fall back to email lookup
|
||||
try {
|
||||
logSCIMOperation(
|
||||
"User lookup",
|
||||
"project",
|
||||
scimConfigId.toString(),
|
||||
`Looking for email: ${userName}`,
|
||||
);
|
||||
|
||||
const user: User | null = await UserService.findOneBy({
|
||||
query: { email: new Email(userName) },
|
||||
select: {
|
||||
_id: true,
|
||||
email: true,
|
||||
name: true,
|
||||
createdAt: true,
|
||||
updatedAt: true,
|
||||
},
|
||||
props: { isRoot: true },
|
||||
});
|
||||
|
||||
if (user) {
|
||||
logSCIMOperation(
|
||||
"User lookup",
|
||||
"project",
|
||||
scimConfigId.toString(),
|
||||
`Found user by email: ${user.id}`,
|
||||
);
|
||||
}
|
||||
|
||||
return user;
|
||||
} catch (error) {
|
||||
// If email validation fails, userName is likely an external ID but no mapping exists
|
||||
logSCIMOperation(
|
||||
"User lookup",
|
||||
"project",
|
||||
scimConfigId.toString(),
|
||||
`Email validation failed for: ${userName}, treating as external ID with no mapping`,
|
||||
);
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
// Helper function to create or update SCIM user mapping
|
||||
const createOrUpdateSCIMUserMapping: (
|
||||
user: User,
|
||||
externalId: string,
|
||||
projectId: ObjectID,
|
||||
scimConfigId: ObjectID,
|
||||
) => Promise<void> = async (
|
||||
user: User,
|
||||
externalId: string,
|
||||
projectId: ObjectID,
|
||||
scimConfigId: ObjectID,
|
||||
): Promise<void> => {
|
||||
// Check if mapping already exists
|
||||
const existingMapping: SCIMUser | null = await SCIMUserService.findOneBy({
|
||||
query: {
|
||||
userId: user.id!,
|
||||
projectId: projectId,
|
||||
scimConfigId: scimConfigId,
|
||||
},
|
||||
select: { _id: true, externalId: true },
|
||||
props: { isRoot: true },
|
||||
});
|
||||
|
||||
if (existingMapping) {
|
||||
// Update existing mapping if external ID changed
|
||||
if (existingMapping.externalId !== externalId) {
|
||||
await SCIMUserService.updateOneById({
|
||||
id: existingMapping.id!,
|
||||
data: { externalId: externalId },
|
||||
props: { isRoot: true },
|
||||
});
|
||||
|
||||
logSCIMOperation(
|
||||
"SCIM mapping",
|
||||
"project",
|
||||
scimConfigId.toString(),
|
||||
`Updated external ID mapping for user ${user.id} from ${existingMapping.externalId} to ${externalId}`,
|
||||
);
|
||||
}
|
||||
} else {
|
||||
// Create new mapping
|
||||
const scimUser: SCIMUser = new SCIMUser();
|
||||
scimUser.projectId = projectId;
|
||||
scimUser.scimConfigId = scimConfigId;
|
||||
scimUser.userId = user.id!;
|
||||
scimUser.externalId = externalId;
|
||||
|
||||
await SCIMUserService.create({
|
||||
data: scimUser,
|
||||
props: { isRoot: true },
|
||||
});
|
||||
|
||||
logSCIMOperation(
|
||||
"SCIM mapping",
|
||||
"project",
|
||||
scimConfigId.toString(),
|
||||
`Created external ID mapping for user ${user.id} with external ID ${externalId}`,
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
// Helper function to resolve user ID (could be internal ID or external ID)
|
||||
const resolveUserId: (
|
||||
userIdParam: string,
|
||||
projectId: ObjectID,
|
||||
scimConfigId: ObjectID,
|
||||
) => Promise<ObjectID | null> = async (
|
||||
userIdParam: string,
|
||||
projectId: ObjectID,
|
||||
scimConfigId: ObjectID,
|
||||
): Promise<ObjectID | null> => {
|
||||
// First try to parse as ObjectID (internal user ID)
|
||||
try {
|
||||
const objectId: ObjectID = new ObjectID(userIdParam);
|
||||
|
||||
// Verify this user exists in the project
|
||||
const teamMember: TeamMember | null = await TeamMemberService.findOneBy({
|
||||
query: {
|
||||
projectId: projectId,
|
||||
userId: objectId,
|
||||
},
|
||||
select: { userId: true },
|
||||
props: { isRoot: true },
|
||||
});
|
||||
|
||||
if (teamMember) {
|
||||
return objectId;
|
||||
}
|
||||
} catch (error) {
|
||||
// Not a valid ObjectID, continue to external ID lookup
|
||||
}
|
||||
|
||||
// Try to find by external ID
|
||||
const scimUser: SCIMUser | null = await SCIMUserService.findOneBy({
|
||||
query: {
|
||||
externalId: userIdParam,
|
||||
projectId: projectId,
|
||||
scimConfigId: scimConfigId,
|
||||
},
|
||||
select: { userId: true },
|
||||
props: { isRoot: true },
|
||||
});
|
||||
|
||||
if (scimUser && scimUser.userId) {
|
||||
return scimUser.userId;
|
||||
}
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
// Helper function to get external ID for a user
|
||||
const getExternalIdForUser: (
|
||||
userId: ObjectID,
|
||||
projectId: ObjectID,
|
||||
scimConfigId: ObjectID,
|
||||
) => Promise<string | null> = async (
|
||||
userId: ObjectID,
|
||||
projectId: ObjectID,
|
||||
scimConfigId: ObjectID,
|
||||
): Promise<string | null> => {
|
||||
const scimUser: SCIMUser | null = await SCIMUserService.findOneBy({
|
||||
query: {
|
||||
userId: userId,
|
||||
projectId: projectId,
|
||||
scimConfigId: scimConfigId,
|
||||
},
|
||||
select: { externalId: true },
|
||||
props: { isRoot: true },
|
||||
});
|
||||
|
||||
return scimUser?.externalId || null;
|
||||
};
|
||||
|
||||
const handleUserTeamOperations: (
|
||||
operation: "add" | "remove",
|
||||
projectId: ObjectID,
|
||||
@@ -120,6 +351,10 @@ router.get(
|
||||
SCIMMiddleware.isAuthorizedSCIMRequest,
|
||||
async (req: ExpressRequest, res: ExpressResponse): Promise<void> => {
|
||||
try {
|
||||
logger.debug(
|
||||
`🔗 PROJECT SCIM API HIT: GET ServiceProviderConfig - projectScimId: ${req.params["projectScimId"]}`,
|
||||
);
|
||||
|
||||
logSCIMOperation(
|
||||
"ServiceProviderConfig",
|
||||
"project",
|
||||
@@ -150,6 +385,10 @@ router.get(
|
||||
SCIMMiddleware.isAuthorizedSCIMRequest,
|
||||
async (req: ExpressRequest, res: ExpressResponse): Promise<void> => {
|
||||
try {
|
||||
logger.debug(
|
||||
`🔗 PROJECT SCIM API HIT: GET Users List - projectScimId: ${req.params["projectScimId"]}, query: ${JSON.stringify(req.query)}`,
|
||||
);
|
||||
|
||||
logSCIMOperation("Users list", "project", req.params["projectScimId"]!);
|
||||
|
||||
const oneuptimeRequest: OneUptimeRequest = req as OneUptimeRequest;
|
||||
@@ -179,20 +418,21 @@ router.get(
|
||||
/userName eq "([^"]+)"/i,
|
||||
);
|
||||
if (emailMatch) {
|
||||
const email: string = emailMatch[1]!;
|
||||
const userName: string = emailMatch[1]!;
|
||||
logSCIMOperation(
|
||||
"Users list",
|
||||
"project",
|
||||
req.params["projectScimId"]!,
|
||||
`filter by email: ${email}`,
|
||||
`filter by userName: ${userName}`,
|
||||
);
|
||||
|
||||
if (email) {
|
||||
const user: User | null = await UserService.findOneBy({
|
||||
query: { email: new Email(email) },
|
||||
select: { _id: true },
|
||||
props: { isRoot: true },
|
||||
});
|
||||
if (userName) {
|
||||
const user: User | null = await findUserByExternalIdOrEmail(
|
||||
userName,
|
||||
projectId,
|
||||
new ObjectID(req.params["projectScimId"]!),
|
||||
);
|
||||
|
||||
if (user && user.id) {
|
||||
query.userId = user.id;
|
||||
logSCIMOperation(
|
||||
@@ -206,7 +446,7 @@ router.get(
|
||||
"Users list",
|
||||
"project",
|
||||
req.params["projectScimId"]!,
|
||||
`user not found for email: ${email}`,
|
||||
`user not found for userName: ${userName}`,
|
||||
);
|
||||
return Response.sendJsonObjectResponse(
|
||||
req,
|
||||
@@ -244,18 +484,28 @@ router.get(
|
||||
});
|
||||
|
||||
// now get unique users.
|
||||
const usersInProjects: Array<JSONObject> = teamMembers
|
||||
.filter((tm: TeamMember) => {
|
||||
return tm.user && tm.user.id;
|
||||
})
|
||||
.map((tm: TeamMember) => {
|
||||
return formatUserForSCIM(
|
||||
tm.user!,
|
||||
const usersInProjects: Array<JSONObject> = [];
|
||||
|
||||
for (const tm of teamMembers) {
|
||||
if (tm.user && tm.user.id) {
|
||||
// Get external ID for this user if it exists
|
||||
const externalId: string | null = await getExternalIdForUser(
|
||||
tm.user.id,
|
||||
projectId,
|
||||
new ObjectID(req.params["projectScimId"]!),
|
||||
);
|
||||
|
||||
const userFormatted: JSONObject = formatUserForSCIM(
|
||||
tm.user,
|
||||
req,
|
||||
req.params["projectScimId"]!,
|
||||
"project",
|
||||
externalId,
|
||||
);
|
||||
});
|
||||
|
||||
usersInProjects.push(userFormatted);
|
||||
}
|
||||
}
|
||||
|
||||
// remove duplicates
|
||||
const uniqueUserIds: Set<string> = new Set<string>();
|
||||
@@ -295,6 +545,10 @@ router.get(
|
||||
SCIMMiddleware.isAuthorizedSCIMRequest,
|
||||
async (req: ExpressRequest, res: ExpressResponse): Promise<void> => {
|
||||
try {
|
||||
logger.debug(
|
||||
`🔗 PROJECT SCIM API HIT: GET Individual User - projectScimId: ${req.params["projectScimId"]}, userId: ${req.params["userId"]}`,
|
||||
);
|
||||
|
||||
logger.debug(
|
||||
`SCIM Get individual user request for userId: ${req.params["userId"]}, projectScimId: ${req.params["projectScimId"]}`,
|
||||
);
|
||||
@@ -302,21 +556,38 @@ router.get(
|
||||
const bearerData: JSONObject =
|
||||
oneuptimeRequest.bearerTokenData as JSONObject;
|
||||
const projectId: ObjectID = bearerData["projectId"] as ObjectID;
|
||||
const userId: string = req.params["userId"]!;
|
||||
const scimConfig: ProjectSCIM = bearerData["scimConfig"] as ProjectSCIM;
|
||||
const userIdParam: string = req.params["userId"]!;
|
||||
|
||||
logger.debug(
|
||||
`SCIM Get user - projectId: ${projectId}, userId: ${userId}`,
|
||||
`SCIM Get user - projectId: ${projectId}, userIdParam: ${userIdParam}`,
|
||||
);
|
||||
|
||||
if (!userIdParam) {
|
||||
throw new BadRequestException("User ID is required");
|
||||
}
|
||||
|
||||
// Resolve user ID (could be internal ID or external ID)
|
||||
const userId: ObjectID | null = await resolveUserId(
|
||||
userIdParam,
|
||||
projectId,
|
||||
scimConfig.id!,
|
||||
);
|
||||
|
||||
if (!userId) {
|
||||
throw new BadRequestException("User ID is required");
|
||||
logger.debug(
|
||||
`SCIM Get user - could not resolve user ID for param: ${userIdParam}`,
|
||||
);
|
||||
throw new NotFoundException(
|
||||
"User not found or not part of this project",
|
||||
);
|
||||
}
|
||||
|
||||
// Check if user exists and is part of the project
|
||||
const projectUser: TeamMember | null = await TeamMemberService.findOneBy({
|
||||
query: {
|
||||
projectId: projectId,
|
||||
userId: new ObjectID(userId),
|
||||
userId: userId,
|
||||
},
|
||||
select: {
|
||||
userId: true,
|
||||
@@ -333,7 +604,7 @@ router.get(
|
||||
|
||||
if (!projectUser || !projectUser.user) {
|
||||
logger.debug(
|
||||
`SCIM Get user - user not found or not part of project for userId: ${userId}`,
|
||||
`SCIM Get user - user not found or not part of project for resolved userId: ${userId}`,
|
||||
);
|
||||
throw new NotFoundException(
|
||||
"User not found or not part of this project",
|
||||
@@ -342,11 +613,19 @@ router.get(
|
||||
|
||||
logger.debug(`SCIM Get user - found user: ${projectUser.user.id}`);
|
||||
|
||||
// Get external ID for this user if it exists
|
||||
const externalId: string | null = await getExternalIdForUser(
|
||||
projectUser.user.id!,
|
||||
projectId,
|
||||
scimConfig.id!,
|
||||
);
|
||||
|
||||
const user: JSONObject = formatUserForSCIM(
|
||||
projectUser.user,
|
||||
req,
|
||||
req.params["projectScimId"]!,
|
||||
"project",
|
||||
externalId,
|
||||
);
|
||||
|
||||
return Response.sendJsonObjectResponse(req, res, user);
|
||||
@@ -363,6 +642,10 @@ router.put(
|
||||
SCIMMiddleware.isAuthorizedSCIMRequest,
|
||||
async (req: ExpressRequest, res: ExpressResponse): Promise<void> => {
|
||||
try {
|
||||
logger.debug(
|
||||
`🔗 PROJECT SCIM API HIT: PUT Update User - projectScimId: ${req.params["projectScimId"]}, userId: ${req.params["userId"]}`,
|
||||
);
|
||||
|
||||
logger.debug(
|
||||
`SCIM Update user request for userId: ${req.params["userId"]}, projectScimId: ${req.params["projectScimId"]}`,
|
||||
);
|
||||
@@ -370,70 +653,167 @@ router.put(
|
||||
const bearerData: JSONObject =
|
||||
oneuptimeRequest.bearerTokenData as JSONObject;
|
||||
const projectId: ObjectID = bearerData["projectId"] as ObjectID;
|
||||
const userId: string = req.params["userId"]!;
|
||||
const scimConfig: ProjectSCIM = bearerData["scimConfig"] as ProjectSCIM;
|
||||
const userIdParam: string = req.params["userId"]!;
|
||||
const scimUser: JSONObject = req.body;
|
||||
|
||||
logger.debug(
|
||||
`SCIM Update user - projectId: ${projectId}, userId: ${userId}`,
|
||||
`SCIM Update user - projectId: ${projectId}, userIdParam: ${userIdParam}`,
|
||||
);
|
||||
|
||||
logger.debug(
|
||||
`Request body for SCIM Update user: ${JSON.stringify(scimUser, null, 2)}`,
|
||||
);
|
||||
|
||||
if (!userId) {
|
||||
if (!userIdParam) {
|
||||
throw new BadRequestException("User ID is required");
|
||||
}
|
||||
|
||||
// Check if user exists and is part of the project
|
||||
const projectUser: TeamMember | null = await TeamMemberService.findOneBy({
|
||||
query: {
|
||||
projectId: projectId,
|
||||
userId: new ObjectID(userId),
|
||||
},
|
||||
select: {
|
||||
userId: true,
|
||||
user: {
|
||||
_id: true,
|
||||
email: true,
|
||||
name: true,
|
||||
createdAt: true,
|
||||
updatedAt: true,
|
||||
},
|
||||
},
|
||||
props: { isRoot: true },
|
||||
});
|
||||
|
||||
if (!projectUser || !projectUser.user) {
|
||||
logger.debug(
|
||||
`SCIM Update user - user not found or not part of project for userId: ${userId}`,
|
||||
);
|
||||
throw new NotFoundException(
|
||||
"User not found or not part of this project",
|
||||
);
|
||||
}
|
||||
|
||||
// Update user information
|
||||
const email: string =
|
||||
(scimUser["userName"] as string) ||
|
||||
((scimUser["emails"] as JSONObject[])?.[0]?.["value"] as string);
|
||||
// Extract user data from SCIM request
|
||||
const userName: string = extractEmailFromSCIM(scimUser);
|
||||
const externalId: string | null = extractExternalIdFromSCIM(scimUser);
|
||||
const name: string = parseNameFromSCIM(scimUser);
|
||||
const active: boolean = scimUser["active"] as boolean;
|
||||
|
||||
logger.debug(
|
||||
`SCIM Update user - email: ${email}, name: ${name}, active: ${active}`,
|
||||
`SCIM Update user - userName: ${userName}, externalId: ${externalId}, name: ${name}, active: ${active}`,
|
||||
);
|
||||
|
||||
// Extract email from emails array if userName is not an email
|
||||
let email: string = "";
|
||||
if (isUserNameEmail(userName)) {
|
||||
email = userName;
|
||||
} else {
|
||||
// Look for email in the emails array
|
||||
const emailsArray: JSONObject[] = scimUser["emails"] as JSONObject[];
|
||||
if (emailsArray && emailsArray.length > 0) {
|
||||
email = emailsArray[0]?.["value"] as string;
|
||||
}
|
||||
}
|
||||
|
||||
// Resolve user ID (could be internal ID or external ID)
|
||||
const userId: ObjectID | null = await resolveUserId(
|
||||
userIdParam,
|
||||
projectId,
|
||||
scimConfig.id!,
|
||||
);
|
||||
|
||||
let projectUser: TeamMember | null = null;
|
||||
let user: User | null = null;
|
||||
let isNewUser: boolean = false;
|
||||
|
||||
if (userId) {
|
||||
// Check if user exists and is part of the project
|
||||
projectUser = await TeamMemberService.findOneBy({
|
||||
query: {
|
||||
projectId: projectId,
|
||||
userId: userId,
|
||||
},
|
||||
select: {
|
||||
userId: true,
|
||||
user: {
|
||||
_id: true,
|
||||
email: true,
|
||||
name: true,
|
||||
createdAt: true,
|
||||
updatedAt: true,
|
||||
},
|
||||
},
|
||||
props: { isRoot: true },
|
||||
});
|
||||
|
||||
if (projectUser && projectUser.user) {
|
||||
user = projectUser.user;
|
||||
}
|
||||
}
|
||||
|
||||
// If user not found, create a new user (SCIM PUT should create if not exists)
|
||||
if (!user) {
|
||||
logger.debug(
|
||||
`SCIM Update user - user not found for param: ${userIdParam}, creating new user`,
|
||||
);
|
||||
|
||||
if (!scimConfig.autoProvisionUsers) {
|
||||
throw new BadRequestException(
|
||||
"Auto-provisioning is disabled for this project and user not found",
|
||||
);
|
||||
}
|
||||
|
||||
if (!email && !externalId) {
|
||||
throw new BadRequestException(
|
||||
"Either a valid email address or external ID is required to create user",
|
||||
);
|
||||
}
|
||||
|
||||
// Try to find existing user by email if we have one
|
||||
if (email) {
|
||||
try {
|
||||
user = await UserService.findOneBy({
|
||||
query: { email: new Email(email) },
|
||||
select: {
|
||||
_id: true,
|
||||
email: true,
|
||||
name: true,
|
||||
createdAt: true,
|
||||
updatedAt: true,
|
||||
},
|
||||
props: { isRoot: true },
|
||||
});
|
||||
} catch (error) {
|
||||
// Email validation failed, continue without email lookup
|
||||
logger.debug(`SCIM Update user - email validation failed for: ${email}`);
|
||||
}
|
||||
}
|
||||
|
||||
// Create new user if still not found
|
||||
if (!user) {
|
||||
if (!email) {
|
||||
throw new BadRequestException(
|
||||
"A valid email address is required to create a new user",
|
||||
);
|
||||
}
|
||||
|
||||
logger.debug(
|
||||
`SCIM Update user - creating new user for email: ${email}`,
|
||||
);
|
||||
user = await UserService.createByEmail({
|
||||
email: new Email(email),
|
||||
name: name ? new Name(name) : new Name("Unknown"),
|
||||
isEmailVerified: true,
|
||||
generateRandomPassword: true,
|
||||
props: { isRoot: true },
|
||||
});
|
||||
isNewUser = true;
|
||||
}
|
||||
|
||||
// Add user to default teams if configured
|
||||
if (scimConfig.teams && scimConfig.teams.length > 0) {
|
||||
logger.debug(
|
||||
`SCIM Update user - adding new user to ${scimConfig.teams.length} configured teams`,
|
||||
);
|
||||
await handleUserTeamOperations("add", projectId, user.id!, scimConfig);
|
||||
}
|
||||
}
|
||||
|
||||
// Create or update SCIM user mapping if we have an external ID
|
||||
if (externalId && user && user.id) {
|
||||
await createOrUpdateSCIMUserMapping(
|
||||
user,
|
||||
externalId,
|
||||
projectId,
|
||||
scimConfig.id!,
|
||||
);
|
||||
}
|
||||
|
||||
// Handle user deactivation by removing from teams
|
||||
if (active === false) {
|
||||
if (active === false && user && user.id) {
|
||||
logger.debug(
|
||||
`SCIM Update user - user marked as inactive, removing from teams`,
|
||||
);
|
||||
const scimConfig: ProjectSCIM = bearerData["scimConfig"] as ProjectSCIM;
|
||||
await handleUserTeamOperations(
|
||||
"remove",
|
||||
projectId,
|
||||
new ObjectID(userId),
|
||||
user.id,
|
||||
scimConfig,
|
||||
);
|
||||
logger.debug(
|
||||
@@ -442,15 +822,14 @@ router.put(
|
||||
}
|
||||
|
||||
// Handle user activation by adding to teams
|
||||
if (active === true) {
|
||||
if (active === true && user && user.id) {
|
||||
logger.debug(
|
||||
`SCIM Update user - user marked as active, adding to teams`,
|
||||
);
|
||||
const scimConfig: ProjectSCIM = bearerData["scimConfig"] as ProjectSCIM;
|
||||
await handleUserTeamOperations(
|
||||
"add",
|
||||
projectId,
|
||||
new ObjectID(userId),
|
||||
user.id,
|
||||
scimConfig,
|
||||
);
|
||||
logger.debug(
|
||||
@@ -458,7 +837,8 @@ router.put(
|
||||
);
|
||||
}
|
||||
|
||||
if (email || name) {
|
||||
// Update user information if needed and not a new user
|
||||
if (!isNewUser && user && user.id && (email || name)) {
|
||||
const updateData: any = {};
|
||||
if (email) {
|
||||
updateData.email = new Email(email);
|
||||
@@ -468,11 +848,11 @@ router.put(
|
||||
}
|
||||
|
||||
logger.debug(
|
||||
`SCIM Update user - updating user with data: ${JSON.stringify(updateData)}`,
|
||||
`SCIM Update user - updating existing user with data: ${JSON.stringify(updateData)}`,
|
||||
);
|
||||
|
||||
await UserService.updateOneById({
|
||||
id: new ObjectID(userId),
|
||||
id: user.id,
|
||||
data: updateData,
|
||||
props: { isRoot: true },
|
||||
});
|
||||
@@ -481,7 +861,7 @@ router.put(
|
||||
|
||||
// Fetch updated user
|
||||
const updatedUser: User | null = await UserService.findOneById({
|
||||
id: new ObjectID(userId),
|
||||
id: user.id,
|
||||
select: {
|
||||
_id: true,
|
||||
email: true,
|
||||
@@ -493,29 +873,32 @@ router.put(
|
||||
});
|
||||
|
||||
if (updatedUser) {
|
||||
const user: JSONObject = formatUserForSCIM(
|
||||
updatedUser,
|
||||
req,
|
||||
req.params["projectScimId"]!,
|
||||
"project",
|
||||
);
|
||||
return Response.sendJsonObjectResponse(req, res, user);
|
||||
user = updatedUser;
|
||||
}
|
||||
}
|
||||
|
||||
logger.debug(
|
||||
`SCIM Update user - no updates made, returning existing user`,
|
||||
);
|
||||
|
||||
// If no updates were made, return the existing user
|
||||
const user: JSONObject = formatUserForSCIM(
|
||||
projectUser.user,
|
||||
// Get external ID for response
|
||||
const userExternalId: string | null = user && user.id ?
|
||||
await getExternalIdForUser(user.id, projectId, scimConfig.id!) :
|
||||
externalId;
|
||||
|
||||
const responseUser: JSONObject = formatUserForSCIM(
|
||||
user!,
|
||||
req,
|
||||
req.params["projectScimId"]!,
|
||||
"project",
|
||||
userExternalId,
|
||||
);
|
||||
|
||||
return Response.sendJsonObjectResponse(req, res, user);
|
||||
// Set status code based on whether user was created or updated
|
||||
if (isNewUser) {
|
||||
res.status(201);
|
||||
logger.debug(`SCIM Update user - returning newly created user with id: ${user!.id}`);
|
||||
} else {
|
||||
logger.debug(`SCIM Update user - returning updated user with id: ${user!.id}`);
|
||||
}
|
||||
|
||||
return Response.sendJsonObjectResponse(req, res, responseUser);
|
||||
} catch (err) {
|
||||
logger.error(err);
|
||||
return Response.sendErrorResponse(req, res, err as BadRequestException);
|
||||
@@ -529,6 +912,10 @@ router.get(
|
||||
SCIMMiddleware.isAuthorizedSCIMRequest,
|
||||
async (req: ExpressRequest, res: ExpressResponse): Promise<void> => {
|
||||
try {
|
||||
logger.debug(
|
||||
`🔗 PROJECT SCIM API HIT: GET Groups - projectScimId: ${req.params["projectScimId"]}`,
|
||||
);
|
||||
|
||||
logger.debug(
|
||||
`SCIM Groups list request for projectScimId: ${req.params["projectScimId"]}`,
|
||||
);
|
||||
@@ -575,6 +962,10 @@ router.post(
|
||||
SCIMMiddleware.isAuthorizedSCIMRequest,
|
||||
async (req: ExpressRequest, res: ExpressResponse): Promise<void> => {
|
||||
try {
|
||||
logger.debug(
|
||||
`🔗 PROJECT SCIM API HIT: POST Create User - projectScimId: ${req.params["projectScimId"]}`,
|
||||
);
|
||||
|
||||
logger.debug(
|
||||
`SCIM Create user request for projectScimId: ${req.params["projectScimId"]}`,
|
||||
);
|
||||
@@ -591,32 +982,68 @@ router.post(
|
||||
}
|
||||
|
||||
const scimUser: JSONObject = req.body;
|
||||
const email: string =
|
||||
(scimUser["userName"] as string) ||
|
||||
((scimUser["emails"] as JSONObject[])?.[0]?.["value"] as string);
|
||||
const userName: string = extractEmailFromSCIM(scimUser);
|
||||
const externalId: string | null = extractExternalIdFromSCIM(scimUser);
|
||||
const name: string = parseNameFromSCIM(scimUser);
|
||||
|
||||
logger.debug(`SCIM Create user - email: ${email}, name: ${name}`);
|
||||
logger.debug(`SCIM Create user - userName: ${userName}, externalId: ${externalId}, name: ${name}`);
|
||||
|
||||
if (!email) {
|
||||
throw new BadRequestException("userName or email is required");
|
||||
// Extract email from emails array if userName is not an email
|
||||
let email: string = "";
|
||||
if (isUserNameEmail(userName)) {
|
||||
email = userName;
|
||||
} else {
|
||||
// Look for email in the emails array
|
||||
const emailsArray: JSONObject[] = scimUser["emails"] as JSONObject[];
|
||||
if (emailsArray && emailsArray.length > 0) {
|
||||
email = emailsArray[0]?.["value"] as string;
|
||||
}
|
||||
}
|
||||
|
||||
// Check if user already exists
|
||||
let user: User | null = await UserService.findOneBy({
|
||||
query: { email: new Email(email) },
|
||||
select: {
|
||||
_id: true,
|
||||
email: true,
|
||||
name: true,
|
||||
createdAt: true,
|
||||
updatedAt: true,
|
||||
},
|
||||
props: { isRoot: true },
|
||||
});
|
||||
if (!email && !externalId) {
|
||||
throw new BadRequestException(
|
||||
"Either a valid email address or external ID is required",
|
||||
);
|
||||
}
|
||||
|
||||
// Check if user already exists (by external ID first, then email)
|
||||
let user: User | null = null;
|
||||
|
||||
if (externalId) {
|
||||
user = await findUserByExternalIdOrEmail(
|
||||
externalId,
|
||||
projectId,
|
||||
scimConfig.id!,
|
||||
);
|
||||
}
|
||||
|
||||
if (!user && email) {
|
||||
try {
|
||||
user = await UserService.findOneBy({
|
||||
query: { email: new Email(email) },
|
||||
select: {
|
||||
_id: true,
|
||||
email: true,
|
||||
name: true,
|
||||
createdAt: true,
|
||||
updatedAt: true,
|
||||
},
|
||||
props: { isRoot: true },
|
||||
});
|
||||
} catch (error) {
|
||||
// Email validation failed, continue without email lookup
|
||||
logger.debug(`SCIM Create user - email validation failed for: ${email}`);
|
||||
}
|
||||
}
|
||||
|
||||
// Create user if doesn't exist
|
||||
if (!user) {
|
||||
if (!email) {
|
||||
throw new BadRequestException(
|
||||
"A valid email address is required to create a new user",
|
||||
);
|
||||
}
|
||||
|
||||
logger.debug(
|
||||
`SCIM Create user - creating new user for email: ${email}`,
|
||||
);
|
||||
@@ -633,6 +1060,16 @@ router.post(
|
||||
);
|
||||
}
|
||||
|
||||
// Create or update SCIM user mapping if we have an external ID
|
||||
if (externalId && user.id) {
|
||||
await createOrUpdateSCIMUserMapping(
|
||||
user,
|
||||
externalId,
|
||||
projectId,
|
||||
scimConfig.id!,
|
||||
);
|
||||
}
|
||||
|
||||
// Add user to default teams if configured
|
||||
if (scimConfig.teams && scimConfig.teams.length > 0) {
|
||||
logger.debug(
|
||||
@@ -646,6 +1083,7 @@ router.post(
|
||||
req,
|
||||
req.params["projectScimId"]!,
|
||||
"project",
|
||||
externalId,
|
||||
);
|
||||
|
||||
logger.debug(
|
||||
@@ -667,6 +1105,10 @@ router.delete(
|
||||
SCIMMiddleware.isAuthorizedSCIMRequest,
|
||||
async (req: ExpressRequest, res: ExpressResponse): Promise<void> => {
|
||||
try {
|
||||
logger.debug(
|
||||
`🔗 PROJECT SCIM API HIT: DELETE User - projectScimId: ${req.params["projectScimId"]}, userId: ${req.params["userId"]}`,
|
||||
);
|
||||
|
||||
logger.debug(
|
||||
`SCIM Delete user request for userId: ${req.params["userId"]}, projectScimId: ${req.params["projectScimId"]}`,
|
||||
);
|
||||
@@ -675,7 +1117,7 @@ router.delete(
|
||||
oneuptimeRequest.bearerTokenData as JSONObject;
|
||||
const projectId: ObjectID = bearerData["projectId"] as ObjectID;
|
||||
const scimConfig: ProjectSCIM = bearerData["scimConfig"] as ProjectSCIM;
|
||||
const userId: string = req.params["userId"]!;
|
||||
const userIdParam: string = req.params["userId"]!;
|
||||
|
||||
if (!scimConfig.autoDeprovisionUsers) {
|
||||
logger.debug("SCIM Delete user - auto-deprovisioning is disabled");
|
||||
@@ -684,10 +1126,26 @@ router.delete(
|
||||
);
|
||||
}
|
||||
|
||||
if (!userId) {
|
||||
if (!userIdParam) {
|
||||
throw new BadRequestException("User ID is required");
|
||||
}
|
||||
|
||||
// Resolve user ID (could be internal ID or external ID)
|
||||
const userId: ObjectID | null = await resolveUserId(
|
||||
userIdParam,
|
||||
projectId,
|
||||
scimConfig.id!,
|
||||
);
|
||||
|
||||
if (!userId) {
|
||||
logger.debug(
|
||||
`SCIM Delete user - could not resolve user ID for param: ${userIdParam}`,
|
||||
);
|
||||
throw new NotFoundException(
|
||||
"User not found or not part of this project",
|
||||
);
|
||||
}
|
||||
|
||||
logger.debug(
|
||||
`SCIM Delete user - removing user from all teams in project: ${projectId}`,
|
||||
);
|
||||
@@ -701,7 +1159,7 @@ router.delete(
|
||||
await handleUserTeamOperations(
|
||||
"remove",
|
||||
projectId,
|
||||
new ObjectID(userId),
|
||||
userId,
|
||||
scimConfig,
|
||||
);
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import SCIMMiddleware from "Common/Server/Middleware/SCIMAuthorization";
|
||||
import StatusPagePrivateUserService from "Common/Server/Services/StatusPagePrivateUserService";
|
||||
import StatusPageSCIMUserService from "Common/Server/Services/StatusPageSCIMUserService";
|
||||
import Express, {
|
||||
ExpressRequest,
|
||||
ExpressResponse,
|
||||
@@ -13,25 +14,276 @@ import Email from "Common/Types/Email";
|
||||
import { JSONObject } from "Common/Types/JSON";
|
||||
import StatusPagePrivateUser from "Common/Models/DatabaseModels/StatusPagePrivateUser";
|
||||
import StatusPageSCIM from "Common/Models/DatabaseModels/StatusPageSCIM";
|
||||
import StatusPageSCIMUser from "Common/Models/DatabaseModels/StatusPageSCIMUser";
|
||||
import BadRequestException from "Common/Types/Exception/BadRequestException";
|
||||
import NotFoundException from "Common/Types/Exception/NotFoundException";
|
||||
import LIMIT_MAX, { LIMIT_PER_PROJECT } from "Common/Types/Database/LimitMax";
|
||||
import LIMIT_MAX from "Common/Types/Database/LimitMax";
|
||||
import {
|
||||
parseNameFromSCIM,
|
||||
formatUserForSCIM,
|
||||
generateServiceProviderConfig,
|
||||
generateUsersListResponse,
|
||||
parseSCIMQueryParams,
|
||||
logSCIMOperation,
|
||||
extractEmailFromSCIM,
|
||||
extractExternalIdFromSCIM,
|
||||
isUserNameEmail,
|
||||
} from "../Utils/SCIMUtils";
|
||||
import Text from "Common/Types/Text";
|
||||
import HashedString from "Common/Types/HashedString";
|
||||
|
||||
const router: ExpressRouter = Express.getRouter();
|
||||
|
||||
// Helper function to find user by external ID or email
|
||||
const findUserByExternalIdOrEmail: (
|
||||
userName: string,
|
||||
statusPageId: ObjectID,
|
||||
projectId: ObjectID,
|
||||
scimConfigId: ObjectID,
|
||||
) => Promise<StatusPagePrivateUser | null> = async (
|
||||
userName: string,
|
||||
statusPageId: ObjectID,
|
||||
projectId: ObjectID,
|
||||
scimConfigId: ObjectID,
|
||||
): Promise<StatusPagePrivateUser | null> => {
|
||||
// First check if userName is an external ID (not an email)
|
||||
if (!isUserNameEmail(userName)) {
|
||||
logSCIMOperation(
|
||||
"User lookup",
|
||||
"status-page",
|
||||
scimConfigId.toString(),
|
||||
`Looking for external ID: ${userName}`,
|
||||
);
|
||||
|
||||
// Look up by external ID
|
||||
const scimUser: StatusPageSCIMUser | null = await StatusPageSCIMUserService.findOneBy({
|
||||
query: {
|
||||
externalId: userName,
|
||||
statusPageId: statusPageId,
|
||||
projectId: projectId,
|
||||
scimConfigId: scimConfigId,
|
||||
},
|
||||
select: {
|
||||
statusPagePrivateUserId: true,
|
||||
statusPagePrivateUser: {
|
||||
_id: true,
|
||||
email: true,
|
||||
createdAt: true,
|
||||
updatedAt: true,
|
||||
},
|
||||
},
|
||||
props: { isRoot: true },
|
||||
});
|
||||
|
||||
if (scimUser && scimUser.statusPagePrivateUser) {
|
||||
logSCIMOperation(
|
||||
"User lookup",
|
||||
"status-page",
|
||||
scimConfigId.toString(),
|
||||
`Found user by external ID: ${scimUser.statusPagePrivateUser.id}`,
|
||||
);
|
||||
return scimUser.statusPagePrivateUser;
|
||||
}
|
||||
}
|
||||
|
||||
// Fall back to email lookup
|
||||
try {
|
||||
logSCIMOperation(
|
||||
"User lookup",
|
||||
"status-page",
|
||||
scimConfigId.toString(),
|
||||
`Looking for email: ${userName}`,
|
||||
);
|
||||
|
||||
const user: StatusPagePrivateUser | null = await StatusPagePrivateUserService.findOneBy({
|
||||
query: {
|
||||
email: new Email(userName),
|
||||
statusPageId: statusPageId,
|
||||
},
|
||||
select: {
|
||||
_id: true,
|
||||
email: true,
|
||||
createdAt: true,
|
||||
updatedAt: true,
|
||||
},
|
||||
props: { isRoot: true },
|
||||
});
|
||||
|
||||
if (user) {
|
||||
logSCIMOperation(
|
||||
"User lookup",
|
||||
"status-page",
|
||||
scimConfigId.toString(),
|
||||
`Found user by email: ${user.id}`,
|
||||
);
|
||||
}
|
||||
|
||||
return user;
|
||||
} catch (error) {
|
||||
// If email validation fails, userName is likely an external ID but no mapping exists
|
||||
logSCIMOperation(
|
||||
"User lookup",
|
||||
"status-page",
|
||||
scimConfigId.toString(),
|
||||
`Email validation failed for: ${userName}, treating as external ID with no mapping`,
|
||||
);
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
// Helper function to create or update SCIM user mapping
|
||||
const createOrUpdateSCIMUserMapping: (
|
||||
user: StatusPagePrivateUser,
|
||||
externalId: string,
|
||||
statusPageId: ObjectID,
|
||||
projectId: ObjectID,
|
||||
scimConfigId: ObjectID,
|
||||
) => Promise<void> = async (
|
||||
user: StatusPagePrivateUser,
|
||||
externalId: string,
|
||||
statusPageId: ObjectID,
|
||||
projectId: ObjectID,
|
||||
scimConfigId: ObjectID,
|
||||
): Promise<void> => {
|
||||
// Check if mapping already exists
|
||||
const existingMapping: StatusPageSCIMUser | null = await StatusPageSCIMUserService.findOneBy({
|
||||
query: {
|
||||
statusPagePrivateUserId: user.id!,
|
||||
statusPageId: statusPageId,
|
||||
projectId: projectId,
|
||||
scimConfigId: scimConfigId,
|
||||
},
|
||||
select: { _id: true, externalId: true },
|
||||
props: { isRoot: true },
|
||||
});
|
||||
|
||||
if (existingMapping) {
|
||||
// Update existing mapping if external ID changed
|
||||
if (existingMapping.externalId !== externalId) {
|
||||
await StatusPageSCIMUserService.updateOneById({
|
||||
id: existingMapping.id!,
|
||||
data: { externalId: externalId },
|
||||
props: { isRoot: true },
|
||||
});
|
||||
|
||||
logSCIMOperation(
|
||||
"SCIM mapping",
|
||||
"status-page",
|
||||
scimConfigId.toString(),
|
||||
`Updated external ID mapping for user ${user.id} from ${existingMapping.externalId} to ${externalId}`,
|
||||
);
|
||||
}
|
||||
} else {
|
||||
// Create new mapping
|
||||
const scimUser: StatusPageSCIMUser = new StatusPageSCIMUser();
|
||||
scimUser.statusPageId = statusPageId;
|
||||
scimUser.projectId = projectId;
|
||||
scimUser.scimConfigId = scimConfigId;
|
||||
scimUser.statusPagePrivateUserId = user.id!;
|
||||
scimUser.externalId = externalId;
|
||||
|
||||
await StatusPageSCIMUserService.create({
|
||||
data: scimUser,
|
||||
props: { isRoot: true },
|
||||
});
|
||||
|
||||
logSCIMOperation(
|
||||
"SCIM mapping",
|
||||
"status-page",
|
||||
scimConfigId.toString(),
|
||||
`Created external ID mapping for user ${user.id} with external ID ${externalId}`,
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
// Helper function to resolve user ID (could be internal ID or external ID)
|
||||
const resolveUserId: (
|
||||
userIdParam: string,
|
||||
statusPageId: ObjectID,
|
||||
projectId: ObjectID,
|
||||
scimConfigId: ObjectID,
|
||||
) => Promise<ObjectID | null> = async (
|
||||
userIdParam: string,
|
||||
statusPageId: ObjectID,
|
||||
projectId: ObjectID,
|
||||
scimConfigId: ObjectID,
|
||||
): Promise<ObjectID | null> => {
|
||||
// First try to parse as ObjectID (internal user ID)
|
||||
try {
|
||||
const objectId: ObjectID = new ObjectID(userIdParam);
|
||||
|
||||
// Verify this user exists in the status page
|
||||
const statusPageUser: StatusPagePrivateUser | null = await StatusPagePrivateUserService.findOneBy({
|
||||
query: {
|
||||
_id: objectId,
|
||||
statusPageId: statusPageId,
|
||||
},
|
||||
select: { _id: true },
|
||||
props: { isRoot: true },
|
||||
});
|
||||
|
||||
if (statusPageUser) {
|
||||
return objectId;
|
||||
}
|
||||
} catch (error) {
|
||||
// Not a valid ObjectID, continue to external ID lookup
|
||||
}
|
||||
|
||||
// Try to find by external ID
|
||||
const scimUser: StatusPageSCIMUser | null = await StatusPageSCIMUserService.findOneBy({
|
||||
query: {
|
||||
externalId: userIdParam,
|
||||
statusPageId: statusPageId,
|
||||
projectId: projectId,
|
||||
scimConfigId: scimConfigId,
|
||||
},
|
||||
select: { statusPagePrivateUserId: true },
|
||||
props: { isRoot: true },
|
||||
});
|
||||
|
||||
if (scimUser && scimUser.statusPagePrivateUserId) {
|
||||
return scimUser.statusPagePrivateUserId;
|
||||
}
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
// Helper function to get external ID for a user
|
||||
const getExternalIdForUser: (
|
||||
userId: ObjectID,
|
||||
statusPageId: ObjectID,
|
||||
projectId: ObjectID,
|
||||
scimConfigId: ObjectID,
|
||||
) => Promise<string | null> = async (
|
||||
userId: ObjectID,
|
||||
statusPageId: ObjectID,
|
||||
projectId: ObjectID,
|
||||
scimConfigId: ObjectID,
|
||||
): Promise<string | null> => {
|
||||
const scimUser: StatusPageSCIMUser | null = await StatusPageSCIMUserService.findOneBy({
|
||||
query: {
|
||||
statusPagePrivateUserId: userId,
|
||||
statusPageId: statusPageId,
|
||||
projectId: projectId,
|
||||
scimConfigId: scimConfigId,
|
||||
},
|
||||
select: { externalId: true },
|
||||
props: { isRoot: true },
|
||||
});
|
||||
|
||||
return scimUser?.externalId || null;
|
||||
};
|
||||
|
||||
// SCIM Service Provider Configuration - GET /status-page-scim/v2/ServiceProviderConfig
|
||||
router.get(
|
||||
"/status-page-scim/v2/:statusPageScimId/ServiceProviderConfig",
|
||||
SCIMMiddleware.isAuthorizedSCIMRequest,
|
||||
async (req: ExpressRequest, res: ExpressResponse): Promise<void> => {
|
||||
try {
|
||||
logger.debug(
|
||||
`🔗 STATUS PAGE SCIM API HIT: GET ServiceProviderConfig - statusPageScimId: ${req.params["statusPageScimId"]}`,
|
||||
);
|
||||
|
||||
logSCIMOperation(
|
||||
"ServiceProviderConfig",
|
||||
"status-page",
|
||||
@@ -58,6 +310,10 @@ router.get(
|
||||
SCIMMiddleware.isAuthorizedSCIMRequest,
|
||||
async (req: ExpressRequest, res: ExpressResponse): Promise<void> => {
|
||||
try {
|
||||
logger.debug(
|
||||
`🔗 STATUS PAGE SCIM API HIT: GET Users List - statusPageScimId: ${req.params["statusPageScimId"]}, query: ${JSON.stringify(req.query)}`,
|
||||
);
|
||||
|
||||
logger.debug(
|
||||
`Status Page SCIM Users list request for statusPageScimId: ${req.params["statusPageScimId"]}`,
|
||||
);
|
||||
@@ -65,22 +321,66 @@ router.get(
|
||||
const bearerData: JSONObject =
|
||||
oneuptimeRequest.bearerTokenData as JSONObject;
|
||||
const statusPageId: ObjectID = bearerData["statusPageId"] as ObjectID;
|
||||
const projectId: ObjectID = bearerData["projectId"] as ObjectID;
|
||||
const scimConfig: StatusPageSCIM = bearerData["scimConfig"] as StatusPageSCIM;
|
||||
|
||||
// Parse query parameters
|
||||
const startIndex: number =
|
||||
parseInt(req.query["startIndex"] as string) || 1;
|
||||
const count: number = Math.min(
|
||||
parseInt(req.query["count"] as string) || 100,
|
||||
LIMIT_PER_PROJECT,
|
||||
const { startIndex, count } = parseSCIMQueryParams(req);
|
||||
const filter: string = req.query["filter"] as string;
|
||||
|
||||
logSCIMOperation(
|
||||
"Users list",
|
||||
"status-page",
|
||||
req.params["statusPageScimId"]!,
|
||||
`statusPageId: ${statusPageId}, startIndex: ${startIndex}, count: ${count}, filter: ${filter || "none"}`,
|
||||
);
|
||||
|
||||
logger.debug(
|
||||
`Status Page SCIM Users - statusPageId: ${statusPageId}, startIndex: ${startIndex}, count: ${count}`,
|
||||
);
|
||||
let users: Array<StatusPagePrivateUser> = [];
|
||||
|
||||
// Get all private users for this status page
|
||||
const statusPageUsers: Array<StatusPagePrivateUser> =
|
||||
await StatusPagePrivateUserService.findBy({
|
||||
// Handle SCIM filter for userName
|
||||
if (filter) {
|
||||
const emailMatch: RegExpMatchArray | null = filter.match(
|
||||
/userName eq "([^"]+)"/i,
|
||||
);
|
||||
if (emailMatch) {
|
||||
const userName: string = emailMatch[1]!;
|
||||
logSCIMOperation(
|
||||
"Users list",
|
||||
"status-page",
|
||||
req.params["statusPageScimId"]!,
|
||||
`filter by userName: ${userName}`,
|
||||
);
|
||||
|
||||
if (userName) {
|
||||
const user: StatusPagePrivateUser | null = await findUserByExternalIdOrEmail(
|
||||
userName,
|
||||
statusPageId,
|
||||
projectId,
|
||||
scimConfig.id!,
|
||||
);
|
||||
|
||||
if (user) {
|
||||
users = [user];
|
||||
logSCIMOperation(
|
||||
"Users list",
|
||||
"status-page",
|
||||
req.params["statusPageScimId"]!,
|
||||
`found user with id: ${user.id}`,
|
||||
);
|
||||
} else {
|
||||
logSCIMOperation(
|
||||
"Users list",
|
||||
"status-page",
|
||||
req.params["statusPageScimId"]!,
|
||||
`user not found for userName: ${userName}`,
|
||||
);
|
||||
users = [];
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Get all private users for this status page
|
||||
users = await StatusPagePrivateUserService.findBy({
|
||||
query: {
|
||||
statusPageId: statusPageId,
|
||||
},
|
||||
@@ -94,40 +394,50 @@ router.get(
|
||||
limit: LIMIT_MAX,
|
||||
props: { isRoot: true },
|
||||
});
|
||||
}
|
||||
|
||||
logger.debug(
|
||||
`Status Page SCIM Users - found ${statusPageUsers.length} users`,
|
||||
`Status Page SCIM Users - found ${users.length} users`,
|
||||
);
|
||||
|
||||
// Format users for SCIM
|
||||
const users: Array<JSONObject> = statusPageUsers.map(
|
||||
(user: StatusPagePrivateUser) => {
|
||||
return formatUserForSCIM(
|
||||
user,
|
||||
req,
|
||||
req.params["statusPageScimId"]!,
|
||||
"status-page",
|
||||
);
|
||||
},
|
||||
);
|
||||
// Format users for SCIM with external IDs
|
||||
const formattedUsers: Array<JSONObject> = [];
|
||||
|
||||
for (const user of users) {
|
||||
// Get external ID for this user if it exists
|
||||
const externalId: string | null = await getExternalIdForUser(
|
||||
user.id!,
|
||||
statusPageId,
|
||||
projectId,
|
||||
scimConfig.id!,
|
||||
);
|
||||
|
||||
const userFormatted: JSONObject = formatUserForSCIM(
|
||||
user,
|
||||
req,
|
||||
req.params["statusPageScimId"]!,
|
||||
"status-page",
|
||||
externalId,
|
||||
);
|
||||
|
||||
formattedUsers.push(userFormatted);
|
||||
}
|
||||
|
||||
// Paginate the results
|
||||
const paginatedUsers: Array<JSONObject> = users.slice(
|
||||
const paginatedUsers: Array<JSONObject> = formattedUsers.slice(
|
||||
(startIndex - 1) * count,
|
||||
startIndex * count,
|
||||
);
|
||||
|
||||
logger.debug(
|
||||
`Status Page SCIM Users response prepared with ${users.length} users`,
|
||||
`Status Page SCIM Users response prepared with ${formattedUsers.length} users`,
|
||||
);
|
||||
|
||||
return Response.sendJsonObjectResponse(req, res, {
|
||||
schemas: ["urn:ietf:params:scim:api:messages:2.0:ListResponse"],
|
||||
totalResults: users.length,
|
||||
startIndex: startIndex,
|
||||
itemsPerPage: paginatedUsers.length,
|
||||
Resources: paginatedUsers,
|
||||
});
|
||||
return Response.sendJsonObjectResponse(
|
||||
req,
|
||||
res,
|
||||
generateUsersListResponse(paginatedUsers, startIndex, formattedUsers.length),
|
||||
);
|
||||
} catch (err) {
|
||||
logger.error(err);
|
||||
return Response.sendErrorResponse(req, res, err as BadRequestException);
|
||||
@@ -141,6 +451,10 @@ router.get(
|
||||
SCIMMiddleware.isAuthorizedSCIMRequest,
|
||||
async (req: ExpressRequest, res: ExpressResponse): Promise<void> => {
|
||||
try {
|
||||
logger.debug(
|
||||
`🔗 STATUS PAGE SCIM API HIT: GET Individual User - statusPageScimId: ${req.params["statusPageScimId"]}, userId: ${req.params["userId"]}`,
|
||||
);
|
||||
|
||||
logger.debug(
|
||||
`Status Page SCIM Get individual user request for userId: ${req.params["userId"]}, statusPageScimId: ${req.params["statusPageScimId"]}`,
|
||||
);
|
||||
@@ -148,14 +462,33 @@ router.get(
|
||||
const bearerData: JSONObject =
|
||||
oneuptimeRequest.bearerTokenData as JSONObject;
|
||||
const statusPageId: ObjectID = bearerData["statusPageId"] as ObjectID;
|
||||
const userId: string = req.params["userId"]!;
|
||||
const projectId: ObjectID = bearerData["projectId"] as ObjectID;
|
||||
const scimConfig: StatusPageSCIM = bearerData["scimConfig"] as StatusPageSCIM;
|
||||
const userIdParam: string = req.params["userId"]!;
|
||||
|
||||
logger.debug(
|
||||
`Status Page SCIM Get user - statusPageId: ${statusPageId}, userId: ${userId}`,
|
||||
`Status Page SCIM Get user - statusPageId: ${statusPageId}, userIdParam: ${userIdParam}`,
|
||||
);
|
||||
|
||||
if (!userIdParam) {
|
||||
throw new BadRequestException("User ID is required");
|
||||
}
|
||||
|
||||
// Resolve user ID (could be internal ID or external ID)
|
||||
const userId: ObjectID | null = await resolveUserId(
|
||||
userIdParam,
|
||||
statusPageId,
|
||||
projectId,
|
||||
scimConfig.id!,
|
||||
);
|
||||
|
||||
if (!userId) {
|
||||
throw new BadRequestException("User ID is required");
|
||||
logger.debug(
|
||||
`Status Page SCIM Get user - could not resolve user ID for param: ${userIdParam}`,
|
||||
);
|
||||
throw new NotFoundException(
|
||||
"User not found or not part of this status page",
|
||||
);
|
||||
}
|
||||
|
||||
// Check if user exists and belongs to this status page
|
||||
@@ -163,7 +496,7 @@ router.get(
|
||||
await StatusPagePrivateUserService.findOneBy({
|
||||
query: {
|
||||
statusPageId: statusPageId,
|
||||
_id: new ObjectID(userId),
|
||||
_id: userId,
|
||||
},
|
||||
select: {
|
||||
_id: true,
|
||||
@@ -176,18 +509,27 @@ router.get(
|
||||
|
||||
if (!statusPageUser) {
|
||||
logger.debug(
|
||||
`Status Page SCIM Get user - user not found for userId: ${userId}`,
|
||||
`Status Page SCIM Get user - user not found for resolved userId: ${userId}`,
|
||||
);
|
||||
throw new NotFoundException(
|
||||
"User not found or not part of this status page",
|
||||
);
|
||||
}
|
||||
|
||||
// Get external ID for this user if it exists
|
||||
const externalId: string | null = await getExternalIdForUser(
|
||||
statusPageUser.id!,
|
||||
statusPageId,
|
||||
projectId,
|
||||
scimConfig.id!,
|
||||
);
|
||||
|
||||
const user: JSONObject = formatUserForSCIM(
|
||||
statusPageUser,
|
||||
req,
|
||||
req.params["statusPageScimId"]!,
|
||||
"status-page",
|
||||
externalId,
|
||||
);
|
||||
|
||||
logger.debug(
|
||||
@@ -208,6 +550,10 @@ router.post(
|
||||
SCIMMiddleware.isAuthorizedSCIMRequest,
|
||||
async (req: ExpressRequest, res: ExpressResponse): Promise<void> => {
|
||||
try {
|
||||
logger.debug(
|
||||
`🔗 STATUS PAGE SCIM API HIT: POST Create User - statusPageScimId: ${req.params["statusPageScimId"]}`,
|
||||
);
|
||||
|
||||
logger.debug(
|
||||
`Status Page SCIM Create user request for statusPageScimId: ${req.params["statusPageScimId"]}`,
|
||||
);
|
||||
@@ -215,6 +561,7 @@ router.post(
|
||||
const bearerData: JSONObject =
|
||||
oneuptimeRequest.bearerTokenData as JSONObject;
|
||||
const statusPageId: ObjectID = bearerData["statusPageId"] as ObjectID;
|
||||
const projectId: ObjectID = bearerData["projectId"] as ObjectID;
|
||||
const scimConfig: StatusPageSCIM = bearerData[
|
||||
"scimConfig"
|
||||
] as StatusPageSCIM;
|
||||
@@ -226,6 +573,9 @@ router.post(
|
||||
}
|
||||
|
||||
const scimUser: JSONObject = req.body;
|
||||
const userName: string = extractEmailFromSCIM(scimUser);
|
||||
const externalId: string | null = extractExternalIdFromSCIM(scimUser);
|
||||
const name: string = parseNameFromSCIM(scimUser);
|
||||
|
||||
logger.debug(
|
||||
`Status Page SCIM Create user - statusPageId: ${statusPageId}`,
|
||||
@@ -235,34 +585,67 @@ router.post(
|
||||
`Request body for Status Page SCIM Create user: ${JSON.stringify(scimUser, null, 2)}`,
|
||||
);
|
||||
|
||||
// Extract user data from SCIM payload
|
||||
const email: string =
|
||||
(scimUser["userName"] as string) ||
|
||||
((scimUser["emails"] as JSONObject[])?.[0]?.["value"] as string);
|
||||
logger.debug(`Status Page SCIM Create user - userName: ${userName}, externalId: ${externalId}, name: ${name}`);
|
||||
|
||||
if (!email) {
|
||||
throw new BadRequestException("Email is required for user creation");
|
||||
// Extract email from emails array if userName is not an email
|
||||
let email: string = "";
|
||||
if (isUserNameEmail(userName)) {
|
||||
email = userName;
|
||||
} else {
|
||||
// Look for email in the emails array
|
||||
const emailsArray: JSONObject[] = scimUser["emails"] as JSONObject[];
|
||||
if (emailsArray && emailsArray.length > 0) {
|
||||
email = emailsArray[0]?.["value"] as string;
|
||||
}
|
||||
}
|
||||
|
||||
logger.debug(`Status Page SCIM Create user - email: ${email}`);
|
||||
if (!email && !externalId) {
|
||||
throw new BadRequestException(
|
||||
"Either a valid email address or external ID is required",
|
||||
);
|
||||
}
|
||||
|
||||
// Check if user already exists for this status page
|
||||
let user: StatusPagePrivateUser | null =
|
||||
await StatusPagePrivateUserService.findOneBy({
|
||||
query: {
|
||||
statusPageId: statusPageId,
|
||||
email: new Email(email),
|
||||
},
|
||||
select: {
|
||||
_id: true,
|
||||
email: true,
|
||||
createdAt: true,
|
||||
updatedAt: true,
|
||||
},
|
||||
props: { isRoot: true },
|
||||
});
|
||||
// Check if user already exists (by external ID first, then email)
|
||||
let user: StatusPagePrivateUser | null = null;
|
||||
|
||||
if (externalId) {
|
||||
user = await findUserByExternalIdOrEmail(
|
||||
externalId,
|
||||
statusPageId,
|
||||
projectId,
|
||||
scimConfig.id!,
|
||||
);
|
||||
}
|
||||
|
||||
if (!user && email) {
|
||||
try {
|
||||
user = await StatusPagePrivateUserService.findOneBy({
|
||||
query: {
|
||||
email: new Email(email),
|
||||
statusPageId: statusPageId,
|
||||
},
|
||||
select: {
|
||||
_id: true,
|
||||
email: true,
|
||||
createdAt: true,
|
||||
updatedAt: true,
|
||||
},
|
||||
props: { isRoot: true },
|
||||
});
|
||||
} catch (error) {
|
||||
// Email validation failed, continue without email lookup
|
||||
logger.debug(`Status Page SCIM Create user - email validation failed for: ${email}`);
|
||||
}
|
||||
}
|
||||
|
||||
// Create user if doesn't exist
|
||||
if (!user) {
|
||||
if (!email) {
|
||||
throw new BadRequestException(
|
||||
"A valid email address is required to create a new user",
|
||||
);
|
||||
}
|
||||
|
||||
logger.debug(
|
||||
`Status Page SCIM Create user - creating new user with email: ${email}`,
|
||||
);
|
||||
@@ -271,7 +654,7 @@ router.post(
|
||||
privateUser.statusPageId = statusPageId;
|
||||
privateUser.email = new Email(email);
|
||||
privateUser.password = new HashedString(Text.generateRandomText(32));
|
||||
privateUser.projectId = bearerData["projectId"] as ObjectID;
|
||||
privateUser.projectId = projectId;
|
||||
|
||||
// Create new status page private user
|
||||
user = await StatusPagePrivateUserService.create({
|
||||
@@ -284,11 +667,23 @@ router.post(
|
||||
);
|
||||
}
|
||||
|
||||
// Create or update SCIM user mapping if we have an external ID
|
||||
if (externalId && user.id) {
|
||||
await createOrUpdateSCIMUserMapping(
|
||||
user,
|
||||
externalId,
|
||||
statusPageId,
|
||||
projectId,
|
||||
scimConfig.id!,
|
||||
);
|
||||
}
|
||||
|
||||
const createdUser: JSONObject = formatUserForSCIM(
|
||||
user,
|
||||
req,
|
||||
req.params["statusPageScimId"]!,
|
||||
"status-page",
|
||||
externalId,
|
||||
);
|
||||
|
||||
logger.debug(
|
||||
@@ -310,6 +705,10 @@ router.put(
|
||||
SCIMMiddleware.isAuthorizedSCIMRequest,
|
||||
async (req: ExpressRequest, res: ExpressResponse): Promise<void> => {
|
||||
try {
|
||||
logger.debug(
|
||||
`🔗 STATUS PAGE SCIM API HIT: PUT Update User - statusPageScimId: ${req.params["statusPageScimId"]}, userId: ${req.params["userId"]}`,
|
||||
);
|
||||
|
||||
logger.debug(
|
||||
`Status Page SCIM Update user request for userId: ${req.params["userId"]}, statusPageScimId: ${req.params["statusPageScimId"]}`,
|
||||
);
|
||||
@@ -317,27 +716,46 @@ router.put(
|
||||
const bearerData: JSONObject =
|
||||
oneuptimeRequest.bearerTokenData as JSONObject;
|
||||
const statusPageId: ObjectID = bearerData["statusPageId"] as ObjectID;
|
||||
const userId: string = req.params["userId"]!;
|
||||
const projectId: ObjectID = bearerData["projectId"] as ObjectID;
|
||||
const scimConfig: StatusPageSCIM = bearerData["scimConfig"] as StatusPageSCIM;
|
||||
const userIdParam: string = req.params["userId"]!;
|
||||
const scimUser: JSONObject = req.body;
|
||||
|
||||
logger.debug(
|
||||
`Status Page SCIM Update user - statusPageId: ${statusPageId}, userId: ${userId}`,
|
||||
`Status Page SCIM Update user - statusPageId: ${statusPageId}, userIdParam: ${userIdParam}`,
|
||||
);
|
||||
|
||||
logger.debug(
|
||||
`Request body for Status Page SCIM Update user: ${JSON.stringify(scimUser, null, 2)}`,
|
||||
);
|
||||
|
||||
if (!userId) {
|
||||
if (!userIdParam) {
|
||||
throw new BadRequestException("User ID is required");
|
||||
}
|
||||
|
||||
// Resolve user ID (could be internal ID or external ID)
|
||||
const userId: ObjectID | null = await resolveUserId(
|
||||
userIdParam,
|
||||
statusPageId,
|
||||
projectId,
|
||||
scimConfig.id!,
|
||||
);
|
||||
|
||||
if (!userId) {
|
||||
logger.debug(
|
||||
`Status Page SCIM Update user - could not resolve user ID for param: ${userIdParam}`,
|
||||
);
|
||||
throw new NotFoundException(
|
||||
"User not found or not part of this status page",
|
||||
);
|
||||
}
|
||||
|
||||
// Check if user exists and belongs to this status page
|
||||
const statusPageUser: StatusPagePrivateUser | null =
|
||||
await StatusPagePrivateUserService.findOneBy({
|
||||
query: {
|
||||
statusPageId: statusPageId,
|
||||
_id: new ObjectID(userId),
|
||||
_id: userId,
|
||||
},
|
||||
select: {
|
||||
_id: true,
|
||||
@@ -350,7 +768,7 @@ router.put(
|
||||
|
||||
if (!statusPageUser) {
|
||||
logger.debug(
|
||||
`Status Page SCIM Update user - user not found for userId: ${userId}`,
|
||||
`Status Page SCIM Update user - user not found for resolved userId: ${userId}`,
|
||||
);
|
||||
throw new NotFoundException(
|
||||
"User not found or not part of this status page",
|
||||
@@ -358,27 +776,46 @@ router.put(
|
||||
}
|
||||
|
||||
// Update user information
|
||||
const email: string =
|
||||
(scimUser["userName"] as string) ||
|
||||
((scimUser["emails"] as JSONObject[])?.[0]?.["value"] as string);
|
||||
const userName: string = extractEmailFromSCIM(scimUser);
|
||||
const externalId: string | null = extractExternalIdFromSCIM(scimUser);
|
||||
const active: boolean = scimUser["active"] as boolean;
|
||||
|
||||
logger.debug(
|
||||
`Status Page SCIM Update user - email: ${email}, active: ${active}`,
|
||||
`Status Page SCIM Update user - userName: ${userName}, externalId: ${externalId}, active: ${active}`,
|
||||
);
|
||||
|
||||
// Extract email from emails array if userName is not an email
|
||||
let email: string = "";
|
||||
if (isUserNameEmail(userName)) {
|
||||
email = userName;
|
||||
} else {
|
||||
// Look for email in the emails array
|
||||
const emailsArray: JSONObject[] = scimUser["emails"] as JSONObject[];
|
||||
if (emailsArray && emailsArray.length > 0) {
|
||||
email = emailsArray[0]?.["value"] as string;
|
||||
}
|
||||
}
|
||||
|
||||
// Create or update SCIM user mapping if we have an external ID
|
||||
if (externalId) {
|
||||
await createOrUpdateSCIMUserMapping(
|
||||
statusPageUser,
|
||||
externalId,
|
||||
statusPageId,
|
||||
projectId,
|
||||
scimConfig.id!,
|
||||
);
|
||||
}
|
||||
|
||||
// Handle user deactivation by deleting from status page
|
||||
if (active === false) {
|
||||
logger.debug(
|
||||
`Status Page SCIM Update user - user marked as inactive, removing from status page`,
|
||||
);
|
||||
|
||||
const scimConfig: StatusPageSCIM = bearerData[
|
||||
"scimConfig"
|
||||
] as StatusPageSCIM;
|
||||
if (scimConfig.autoDeprovisionUsers) {
|
||||
await StatusPagePrivateUserService.deleteOneById({
|
||||
id: new ObjectID(userId),
|
||||
id: userId,
|
||||
props: { isRoot: true },
|
||||
});
|
||||
|
||||
@@ -387,54 +824,51 @@ router.put(
|
||||
);
|
||||
|
||||
// Return empty response for deleted user
|
||||
return Response.sendJsonObjectResponse(req, res, {});
|
||||
res.status(204);
|
||||
return Response.sendJsonObjectResponse(req, res, {
|
||||
message: "User deprovisioned",
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Prepare update data
|
||||
const updateData: {
|
||||
email?: Email;
|
||||
} = {};
|
||||
|
||||
// Update email if provided and changed
|
||||
if (email && email !== statusPageUser.email?.toString()) {
|
||||
updateData.email = new Email(email);
|
||||
}
|
||||
|
||||
// Only update if there are changes
|
||||
if (Object.keys(updateData).length > 0) {
|
||||
logger.debug(
|
||||
`Status Page SCIM Update user - updating user with data: ${JSON.stringify(updateData)}`,
|
||||
`Status Page SCIM Update user - updating email from ${statusPageUser.email?.toString()} to ${email}`,
|
||||
);
|
||||
|
||||
await StatusPagePrivateUserService.updateOneById({
|
||||
id: new ObjectID(userId),
|
||||
data: updateData,
|
||||
id: userId,
|
||||
data: { email: new Email(email) },
|
||||
props: { isRoot: true },
|
||||
});
|
||||
|
||||
logger.debug(
|
||||
`Status Page SCIM Update user - user updated successfully`,
|
||||
);
|
||||
|
||||
// Fetch updated user
|
||||
const updatedUser: StatusPagePrivateUser | null =
|
||||
await StatusPagePrivateUserService.findOneById({
|
||||
id: new ObjectID(userId),
|
||||
select: {
|
||||
_id: true,
|
||||
email: true,
|
||||
createdAt: true,
|
||||
updatedAt: true,
|
||||
},
|
||||
props: { isRoot: true },
|
||||
});
|
||||
const updatedUser: StatusPagePrivateUser | null = await StatusPagePrivateUserService.findOneById({
|
||||
id: userId,
|
||||
select: {
|
||||
_id: true,
|
||||
email: true,
|
||||
createdAt: true,
|
||||
updatedAt: true,
|
||||
},
|
||||
props: { isRoot: true },
|
||||
});
|
||||
|
||||
if (updatedUser) {
|
||||
const userExternalId: string | null = await getExternalIdForUser(
|
||||
updatedUser.id!,
|
||||
statusPageId,
|
||||
projectId,
|
||||
scimConfig.id!,
|
||||
);
|
||||
|
||||
const user: JSONObject = formatUserForSCIM(
|
||||
updatedUser,
|
||||
req,
|
||||
req.params["statusPageScimId"]!,
|
||||
"status-page",
|
||||
userExternalId,
|
||||
);
|
||||
return Response.sendJsonObjectResponse(req, res, user);
|
||||
}
|
||||
@@ -445,11 +879,19 @@ router.put(
|
||||
);
|
||||
|
||||
// If no updates were made, return the existing user
|
||||
const userExternalId: string | null = await getExternalIdForUser(
|
||||
statusPageUser.id!,
|
||||
statusPageId,
|
||||
projectId,
|
||||
scimConfig.id!,
|
||||
);
|
||||
|
||||
const user: JSONObject = formatUserForSCIM(
|
||||
statusPageUser,
|
||||
req,
|
||||
req.params["statusPageScimId"]!,
|
||||
"status-page",
|
||||
userExternalId,
|
||||
);
|
||||
|
||||
return Response.sendJsonObjectResponse(req, res, user);
|
||||
@@ -466,6 +908,10 @@ router.delete(
|
||||
SCIMMiddleware.isAuthorizedSCIMRequest,
|
||||
async (req: ExpressRequest, res: ExpressResponse): Promise<void> => {
|
||||
try {
|
||||
logger.debug(
|
||||
`🔗 STATUS PAGE SCIM API HIT: DELETE User - statusPageScimId: ${req.params["statusPageScimId"]}, userId: ${req.params["userId"]}`,
|
||||
);
|
||||
|
||||
logger.debug(
|
||||
`Status Page SCIM Delete user request for userId: ${req.params["userId"]}, statusPageScimId: ${req.params["statusPageScimId"]}`,
|
||||
);
|
||||
@@ -473,10 +919,9 @@ router.delete(
|
||||
const bearerData: JSONObject =
|
||||
oneuptimeRequest.bearerTokenData as JSONObject;
|
||||
const statusPageId: ObjectID = bearerData["statusPageId"] as ObjectID;
|
||||
const scimConfig: StatusPageSCIM = bearerData[
|
||||
"scimConfig"
|
||||
] as StatusPageSCIM;
|
||||
const userId: string = req.params["userId"]!;
|
||||
const projectId: ObjectID = bearerData["projectId"] as ObjectID;
|
||||
const scimConfig: StatusPageSCIM = bearerData["scimConfig"] as StatusPageSCIM;
|
||||
const userIdParam: string = req.params["userId"]!;
|
||||
|
||||
if (!scimConfig.autoDeprovisionUsers) {
|
||||
throw new BadRequestException(
|
||||
@@ -485,11 +930,26 @@ router.delete(
|
||||
}
|
||||
|
||||
logger.debug(
|
||||
`Status Page SCIM Delete user - statusPageId: ${statusPageId}, userId: ${userId}`,
|
||||
`Status Page SCIM Delete user - statusPageId: ${statusPageId}, userIdParam: ${userIdParam}`,
|
||||
);
|
||||
|
||||
if (!userIdParam) {
|
||||
throw new BadRequestException("User ID is required");
|
||||
}
|
||||
|
||||
// Resolve user ID (could be internal ID or external ID)
|
||||
const userId: ObjectID | null = await resolveUserId(
|
||||
userIdParam,
|
||||
statusPageId,
|
||||
projectId,
|
||||
scimConfig.id!,
|
||||
);
|
||||
|
||||
if (!userId) {
|
||||
throw new BadRequestException("User ID is required");
|
||||
logger.debug(
|
||||
`Status Page SCIM Delete user - could not resolve user ID for param: ${userIdParam}`,
|
||||
);
|
||||
throw new NotFoundException("User not found");
|
||||
}
|
||||
|
||||
// Check if user exists and belongs to this status page
|
||||
@@ -497,7 +957,7 @@ router.delete(
|
||||
await StatusPagePrivateUserService.findOneBy({
|
||||
query: {
|
||||
statusPageId: statusPageId,
|
||||
_id: new ObjectID(userId),
|
||||
_id: userId,
|
||||
},
|
||||
select: {
|
||||
_id: true,
|
||||
@@ -507,7 +967,7 @@ router.delete(
|
||||
|
||||
if (!statusPageUser) {
|
||||
logger.debug(
|
||||
`Status Page SCIM Delete user - user not found for userId: ${userId}`,
|
||||
`Status Page SCIM Delete user - user not found for resolved userId: ${userId}`,
|
||||
);
|
||||
// SCIM spec says to return 404 for non-existent resources
|
||||
throw new NotFoundException("User not found");
|
||||
@@ -515,7 +975,7 @@ router.delete(
|
||||
|
||||
// Delete the user from status page
|
||||
await StatusPagePrivateUserService.deleteOneById({
|
||||
id: new ObjectID(userId),
|
||||
id: userId,
|
||||
props: { isRoot: true },
|
||||
});
|
||||
|
||||
|
||||
@@ -76,16 +76,19 @@ export const formatUserForSCIM: (
|
||||
req: ExpressRequest,
|
||||
scimId: string,
|
||||
scimType: "project" | "status-page",
|
||||
externalId?: string | null,
|
||||
) => JSONObject = (
|
||||
user: SCIMUser,
|
||||
req: ExpressRequest,
|
||||
scimId: string,
|
||||
scimType: "project" | "status-page",
|
||||
externalId?: string | null,
|
||||
): JSONObject => {
|
||||
const baseUrl: string = `${req.protocol}://${req.get("host")}`;
|
||||
const userName: string = user.email?.toString() || "";
|
||||
const userName: string = externalId || user.email?.toString() || "";
|
||||
const email: string = user.email?.toString() || "";
|
||||
const fullName: string =
|
||||
user.name?.toString() || userName.split("@")[0] || "Unknown User";
|
||||
user.name?.toString() || email.split("@")[0] || "Unknown User";
|
||||
|
||||
const nameData: { givenName: string; familyName: string; formatted: string } =
|
||||
parseNameToSCIMFormat(fullName);
|
||||
@@ -108,7 +111,7 @@ export const formatUserForSCIM: (
|
||||
},
|
||||
emails: [
|
||||
{
|
||||
value: userName,
|
||||
value: email,
|
||||
type: "work",
|
||||
primary: true,
|
||||
},
|
||||
@@ -136,6 +139,40 @@ export const extractEmailFromSCIM: (scimUser: JSONObject) => string = (
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* Extract external ID from SCIM user payload (for non-email userNames)
|
||||
*/
|
||||
export const extractExternalIdFromSCIM: (scimUser: JSONObject) => string | null = (
|
||||
scimUser: JSONObject,
|
||||
): string | null => {
|
||||
const userName: string = scimUser["userName"] as string;
|
||||
if (!userName) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Check if userName is not an email - if it's not a valid email format, treat it as external ID
|
||||
const emailRegex: RegExp = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
||||
if (!emailRegex.test(userName)) {
|
||||
return userName;
|
||||
}
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
/**
|
||||
* Check if a userName field contains an email or external ID
|
||||
*/
|
||||
export const isUserNameEmail: (userName: string) => boolean = (
|
||||
userName: string,
|
||||
): boolean => {
|
||||
if (!userName) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const emailRegex: RegExp = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
||||
return emailRegex.test(userName);
|
||||
};
|
||||
|
||||
/**
|
||||
* Extract active status from SCIM user payload
|
||||
*/
|
||||
|
||||
@@ -183,6 +183,8 @@ import OnCallDutyPolicyUserOverride from "./OnCallDutyPolicyUserOverride";
|
||||
import MonitorFeed from "./MonitorFeed";
|
||||
import MetricType from "./MetricType";
|
||||
import ProjectSCIM from "./ProjectSCIM";
|
||||
import SCIMUser from "./SCIMUser";
|
||||
import StatusPageSCIMUser from "./StatusPageSCIMUser";
|
||||
|
||||
const AllModelTypes: Array<{
|
||||
new (): BaseModel;
|
||||
@@ -387,6 +389,9 @@ const AllModelTypes: Array<{
|
||||
OnCallDutyPolicyTimeLog,
|
||||
|
||||
ProjectSCIM,
|
||||
SCIMUser,
|
||||
|
||||
StatusPageSCIMUser
|
||||
];
|
||||
|
||||
const modelTypeMap: { [key: string]: { new (): BaseModel } } = {};
|
||||
|
||||
287
Common/Models/DatabaseModels/SCIMUser.ts
Normal file
287
Common/Models/DatabaseModels/SCIMUser.ts
Normal file
@@ -0,0 +1,287 @@
|
||||
import Project from "./Project";
|
||||
import ProjectSCIM from "./ProjectSCIM";
|
||||
import User from "./User";
|
||||
import BaseModel from "./DatabaseBaseModel/DatabaseBaseModel";
|
||||
import Route from "../../Types/API/Route";
|
||||
import { PlanType } from "../../Types/Billing/SubscriptionPlan";
|
||||
import ColumnAccessControl from "../../Types/Database/AccessControl/ColumnAccessControl";
|
||||
import TableAccessControl from "../../Types/Database/AccessControl/TableAccessControl";
|
||||
import TableBillingAccessControl from "../../Types/Database/AccessControl/TableBillingAccessControl";
|
||||
import ColumnLength from "../../Types/Database/ColumnLength";
|
||||
import ColumnType from "../../Types/Database/ColumnType";
|
||||
import CrudApiEndpoint from "../../Types/Database/CrudApiEndpoint";
|
||||
import TableColumn from "../../Types/Database/TableColumn";
|
||||
import TableColumnType from "../../Types/Database/TableColumnType";
|
||||
import TableMetadata from "../../Types/Database/TableMetadata";
|
||||
import TenantColumn from "../../Types/Database/TenantColumn";
|
||||
import IconProp from "../../Types/Icon/IconProp";
|
||||
import ObjectID from "../../Types/ObjectID";
|
||||
import Permission from "../../Types/Permission";
|
||||
import {
|
||||
Column,
|
||||
Entity,
|
||||
Index,
|
||||
JoinColumn,
|
||||
ManyToOne,
|
||||
} from "typeorm";
|
||||
|
||||
@TableBillingAccessControl({
|
||||
create: PlanType.Scale,
|
||||
read: PlanType.Scale,
|
||||
update: PlanType.Scale,
|
||||
delete: PlanType.Scale,
|
||||
})
|
||||
@TenantColumn("projectId")
|
||||
@TableAccessControl({
|
||||
create: [
|
||||
Permission.ProjectOwner,
|
||||
Permission.ProjectAdmin,
|
||||
Permission.CreateProjectSSO,
|
||||
],
|
||||
read: [
|
||||
Permission.ProjectOwner,
|
||||
Permission.ProjectAdmin,
|
||||
Permission.ProjectMember,
|
||||
Permission.ReadProjectSSO,
|
||||
],
|
||||
delete: [
|
||||
Permission.ProjectOwner,
|
||||
Permission.ProjectAdmin,
|
||||
Permission.DeleteProjectSSO,
|
||||
],
|
||||
update: [
|
||||
Permission.ProjectOwner,
|
||||
Permission.ProjectAdmin,
|
||||
Permission.EditProjectSSO,
|
||||
],
|
||||
})
|
||||
@CrudApiEndpoint(new Route("/scim-user"))
|
||||
@Entity({
|
||||
name: "SCIMUser",
|
||||
})
|
||||
@TableMetadata({
|
||||
tableName: "SCIMUser",
|
||||
singularName: "SCIM User",
|
||||
pluralName: "SCIM Users",
|
||||
icon: IconProp.User,
|
||||
tableDescription: "SCIM User mapping to store external provider user IDs",
|
||||
})
|
||||
export default class SCIMUser extends BaseModel {
|
||||
@ColumnAccessControl({
|
||||
create: [
|
||||
Permission.ProjectOwner,
|
||||
Permission.ProjectAdmin,
|
||||
Permission.CreateProjectSSO,
|
||||
],
|
||||
read: [
|
||||
Permission.ProjectOwner,
|
||||
Permission.ProjectAdmin,
|
||||
Permission.ProjectMember,
|
||||
Permission.ReadProjectSSO,
|
||||
],
|
||||
update: [],
|
||||
})
|
||||
@TableColumn({
|
||||
manyToOneRelationColumn: "projectId",
|
||||
type: TableColumnType.Entity,
|
||||
modelType: Project,
|
||||
title: "Project",
|
||||
description: "Relation to Project Resource in which this object belongs",
|
||||
})
|
||||
@ManyToOne(
|
||||
() => {
|
||||
return Project;
|
||||
},
|
||||
{
|
||||
eager: false,
|
||||
nullable: false,
|
||||
onDelete: "CASCADE",
|
||||
orphanedRowAction: "delete",
|
||||
},
|
||||
)
|
||||
@JoinColumn({ name: "projectId" })
|
||||
public project?: Project = undefined;
|
||||
|
||||
@ColumnAccessControl({
|
||||
create: [
|
||||
Permission.ProjectOwner,
|
||||
Permission.ProjectAdmin,
|
||||
Permission.CreateProjectSSO,
|
||||
],
|
||||
read: [
|
||||
Permission.ProjectOwner,
|
||||
Permission.ProjectAdmin,
|
||||
Permission.ProjectMember,
|
||||
Permission.ReadProjectSSO,
|
||||
],
|
||||
update: [],
|
||||
})
|
||||
@Index()
|
||||
@TableColumn({
|
||||
type: TableColumnType.ObjectID,
|
||||
required: true,
|
||||
canReadOnRelationQuery: true,
|
||||
title: "Project ID",
|
||||
description: "ID of your OneUptime Project in which this object belongs",
|
||||
})
|
||||
@Column({
|
||||
type: ColumnType.ObjectID,
|
||||
nullable: false,
|
||||
transformer: ObjectID.getDatabaseTransformer(),
|
||||
})
|
||||
public projectId?: ObjectID = undefined;
|
||||
|
||||
@ColumnAccessControl({
|
||||
create: [
|
||||
Permission.ProjectOwner,
|
||||
Permission.ProjectAdmin,
|
||||
Permission.CreateProjectSSO,
|
||||
],
|
||||
read: [
|
||||
Permission.ProjectOwner,
|
||||
Permission.ProjectAdmin,
|
||||
Permission.ProjectMember,
|
||||
Permission.ReadProjectSSO,
|
||||
],
|
||||
update: [],
|
||||
})
|
||||
@TableColumn({
|
||||
manyToOneRelationColumn: "scimConfigId",
|
||||
type: TableColumnType.Entity,
|
||||
modelType: ProjectSCIM,
|
||||
title: "SCIM Config",
|
||||
description: "Relation to Project SCIM Config Resource",
|
||||
})
|
||||
@ManyToOne(
|
||||
() => {
|
||||
return ProjectSCIM;
|
||||
},
|
||||
{
|
||||
eager: false,
|
||||
nullable: false,
|
||||
onDelete: "CASCADE",
|
||||
orphanedRowAction: "delete",
|
||||
},
|
||||
)
|
||||
@JoinColumn({ name: "scimConfigId" })
|
||||
public scimConfig?: ProjectSCIM = undefined;
|
||||
|
||||
@ColumnAccessControl({
|
||||
create: [
|
||||
Permission.ProjectOwner,
|
||||
Permission.ProjectAdmin,
|
||||
Permission.CreateProjectSSO,
|
||||
],
|
||||
read: [
|
||||
Permission.ProjectOwner,
|
||||
Permission.ProjectAdmin,
|
||||
Permission.ProjectMember,
|
||||
Permission.ReadProjectSSO,
|
||||
],
|
||||
update: [],
|
||||
})
|
||||
@Index()
|
||||
@TableColumn({
|
||||
type: TableColumnType.ObjectID,
|
||||
required: true,
|
||||
title: "SCIM Config ID",
|
||||
description: "ID of the SCIM Config this user mapping belongs to",
|
||||
})
|
||||
@Column({
|
||||
type: ColumnType.ObjectID,
|
||||
nullable: false,
|
||||
transformer: ObjectID.getDatabaseTransformer(),
|
||||
})
|
||||
public scimConfigId?: ObjectID = undefined;
|
||||
|
||||
@ColumnAccessControl({
|
||||
create: [
|
||||
Permission.ProjectOwner,
|
||||
Permission.ProjectAdmin,
|
||||
Permission.CreateProjectSSO,
|
||||
],
|
||||
read: [
|
||||
Permission.ProjectOwner,
|
||||
Permission.ProjectAdmin,
|
||||
Permission.ProjectMember,
|
||||
Permission.ReadProjectSSO,
|
||||
],
|
||||
update: [],
|
||||
})
|
||||
@TableColumn({
|
||||
manyToOneRelationColumn: "userId",
|
||||
type: TableColumnType.Entity,
|
||||
modelType: User,
|
||||
title: "User",
|
||||
description: "Relation to User Resource",
|
||||
})
|
||||
@ManyToOne(
|
||||
() => {
|
||||
return User;
|
||||
},
|
||||
{
|
||||
eager: false,
|
||||
nullable: false,
|
||||
onDelete: "CASCADE",
|
||||
orphanedRowAction: "delete",
|
||||
},
|
||||
)
|
||||
@JoinColumn({ name: "userId" })
|
||||
public user?: User = undefined;
|
||||
|
||||
@ColumnAccessControl({
|
||||
create: [
|
||||
Permission.ProjectOwner,
|
||||
Permission.ProjectAdmin,
|
||||
Permission.CreateProjectSSO,
|
||||
],
|
||||
read: [
|
||||
Permission.ProjectOwner,
|
||||
Permission.ProjectAdmin,
|
||||
Permission.ProjectMember,
|
||||
Permission.ReadProjectSSO,
|
||||
],
|
||||
update: [],
|
||||
})
|
||||
@Index()
|
||||
@TableColumn({
|
||||
type: TableColumnType.ObjectID,
|
||||
required: true,
|
||||
title: "User ID",
|
||||
description: "ID of the OneUptime User this external ID maps to",
|
||||
})
|
||||
@Column({
|
||||
type: ColumnType.ObjectID,
|
||||
nullable: false,
|
||||
transformer: ObjectID.getDatabaseTransformer(),
|
||||
})
|
||||
public userId?: ObjectID = undefined;
|
||||
|
||||
@ColumnAccessControl({
|
||||
create: [
|
||||
Permission.ProjectOwner,
|
||||
Permission.ProjectAdmin,
|
||||
Permission.CreateProjectSSO,
|
||||
],
|
||||
read: [
|
||||
Permission.ProjectOwner,
|
||||
Permission.ProjectAdmin,
|
||||
Permission.ProjectMember,
|
||||
Permission.ReadProjectSSO,
|
||||
],
|
||||
update: [],
|
||||
})
|
||||
@Index()
|
||||
@TableColumn({
|
||||
type: TableColumnType.LongText,
|
||||
required: true,
|
||||
title: "External ID",
|
||||
description: "External user ID from SCIM provider (e.g., Azure AD user ID)",
|
||||
})
|
||||
@Column({
|
||||
type: ColumnType.LongText,
|
||||
nullable: false,
|
||||
length: ColumnLength.LongText,
|
||||
})
|
||||
public externalId?: string = undefined;
|
||||
}
|
||||
352
Common/Models/DatabaseModels/StatusPageSCIMUser.ts
Normal file
352
Common/Models/DatabaseModels/StatusPageSCIMUser.ts
Normal file
@@ -0,0 +1,352 @@
|
||||
import Project from "./Project";
|
||||
import StatusPage from "./StatusPage";
|
||||
import StatusPageSCIM from "./StatusPageSCIM";
|
||||
import StatusPagePrivateUser from "./StatusPagePrivateUser";
|
||||
import BaseModel from "./DatabaseBaseModel/DatabaseBaseModel";
|
||||
import Route from "../../Types/API/Route";
|
||||
import { PlanType } from "../../Types/Billing/SubscriptionPlan";
|
||||
import ColumnAccessControl from "../../Types/Database/AccessControl/ColumnAccessControl";
|
||||
import TableAccessControl from "../../Types/Database/AccessControl/TableAccessControl";
|
||||
import TableBillingAccessControl from "../../Types/Database/AccessControl/TableBillingAccessControl";
|
||||
import ColumnLength from "../../Types/Database/ColumnLength";
|
||||
import ColumnType from "../../Types/Database/ColumnType";
|
||||
import CrudApiEndpoint from "../../Types/Database/CrudApiEndpoint";
|
||||
import TableColumn from "../../Types/Database/TableColumn";
|
||||
import TableColumnType from "../../Types/Database/TableColumnType";
|
||||
import TableMetadata from "../../Types/Database/TableMetadata";
|
||||
import TenantColumn from "../../Types/Database/TenantColumn";
|
||||
import IconProp from "../../Types/Icon/IconProp";
|
||||
import ObjectID from "../../Types/ObjectID";
|
||||
import Permission from "../../Types/Permission";
|
||||
import {
|
||||
Column,
|
||||
Entity,
|
||||
Index,
|
||||
JoinColumn,
|
||||
ManyToOne,
|
||||
} from "typeorm";
|
||||
|
||||
@TableBillingAccessControl({
|
||||
create: PlanType.Scale,
|
||||
read: PlanType.Scale,
|
||||
update: PlanType.Scale,
|
||||
delete: PlanType.Scale,
|
||||
})
|
||||
@TenantColumn("projectId")
|
||||
@TableAccessControl({
|
||||
create: [
|
||||
Permission.ProjectOwner,
|
||||
Permission.ProjectAdmin,
|
||||
Permission.CreateStatusPagePrivateUser,
|
||||
],
|
||||
read: [
|
||||
Permission.ProjectOwner,
|
||||
Permission.ProjectAdmin,
|
||||
Permission.ProjectMember,
|
||||
Permission.ReadStatusPagePrivateUser,
|
||||
],
|
||||
delete: [
|
||||
Permission.ProjectOwner,
|
||||
Permission.ProjectAdmin,
|
||||
Permission.DeleteStatusPagePrivateUser,
|
||||
],
|
||||
update: [
|
||||
Permission.ProjectOwner,
|
||||
Permission.ProjectAdmin,
|
||||
Permission.EditStatusPagePrivateUser,
|
||||
],
|
||||
})
|
||||
@CrudApiEndpoint(new Route("/status-page-scim-user"))
|
||||
@Entity({
|
||||
name: "StatusPageSCIMUser",
|
||||
})
|
||||
@TableMetadata({
|
||||
tableName: "StatusPageSCIMUser",
|
||||
singularName: "Status Page SCIM User",
|
||||
pluralName: "Status Page SCIM Users",
|
||||
icon: IconProp.User,
|
||||
tableDescription: "Status Page SCIM User mapping to store external provider user IDs",
|
||||
})
|
||||
export default class StatusPageSCIMUser extends BaseModel {
|
||||
@ColumnAccessControl({
|
||||
create: [
|
||||
Permission.ProjectOwner,
|
||||
Permission.ProjectAdmin,
|
||||
Permission.CreateStatusPagePrivateUser,
|
||||
],
|
||||
read: [
|
||||
Permission.ProjectOwner,
|
||||
Permission.ProjectAdmin,
|
||||
Permission.ProjectMember,
|
||||
Permission.ReadStatusPagePrivateUser,
|
||||
],
|
||||
update: [],
|
||||
})
|
||||
@TableColumn({
|
||||
manyToOneRelationColumn: "projectId",
|
||||
type: TableColumnType.Entity,
|
||||
modelType: Project,
|
||||
title: "Project",
|
||||
description: "Relation to Project Resource in which this object belongs",
|
||||
})
|
||||
@ManyToOne(
|
||||
() => {
|
||||
return Project;
|
||||
},
|
||||
{
|
||||
eager: false,
|
||||
nullable: false,
|
||||
onDelete: "CASCADE",
|
||||
orphanedRowAction: "delete",
|
||||
},
|
||||
)
|
||||
@JoinColumn({ name: "projectId" })
|
||||
public project?: Project = undefined;
|
||||
|
||||
@ColumnAccessControl({
|
||||
create: [
|
||||
Permission.ProjectOwner,
|
||||
Permission.ProjectAdmin,
|
||||
Permission.CreateStatusPagePrivateUser,
|
||||
],
|
||||
read: [
|
||||
Permission.ProjectOwner,
|
||||
Permission.ProjectAdmin,
|
||||
Permission.ProjectMember,
|
||||
Permission.ReadStatusPagePrivateUser,
|
||||
],
|
||||
update: [],
|
||||
})
|
||||
@Index()
|
||||
@TableColumn({
|
||||
type: TableColumnType.ObjectID,
|
||||
required: true,
|
||||
canReadOnRelationQuery: true,
|
||||
title: "Project ID",
|
||||
description: "ID of your OneUptime Project in which this object belongs",
|
||||
})
|
||||
@Column({
|
||||
type: ColumnType.ObjectID,
|
||||
nullable: false,
|
||||
transformer: ObjectID.getDatabaseTransformer(),
|
||||
})
|
||||
public projectId?: ObjectID = undefined;
|
||||
|
||||
@ColumnAccessControl({
|
||||
create: [
|
||||
Permission.ProjectOwner,
|
||||
Permission.ProjectAdmin,
|
||||
Permission.CreateStatusPagePrivateUser,
|
||||
],
|
||||
read: [
|
||||
Permission.ProjectOwner,
|
||||
Permission.ProjectAdmin,
|
||||
Permission.ProjectMember,
|
||||
Permission.ReadStatusPagePrivateUser,
|
||||
],
|
||||
update: [],
|
||||
})
|
||||
@TableColumn({
|
||||
manyToOneRelationColumn: "statusPageId",
|
||||
type: TableColumnType.Entity,
|
||||
modelType: StatusPage,
|
||||
title: "Status Page",
|
||||
description: "Relation to Status Page Resource in which this object belongs",
|
||||
})
|
||||
@ManyToOne(
|
||||
() => {
|
||||
return StatusPage;
|
||||
},
|
||||
{
|
||||
eager: false,
|
||||
nullable: false,
|
||||
onDelete: "CASCADE",
|
||||
orphanedRowAction: "delete",
|
||||
},
|
||||
)
|
||||
@JoinColumn({ name: "statusPageId" })
|
||||
public statusPage?: StatusPage = undefined;
|
||||
|
||||
@ColumnAccessControl({
|
||||
create: [
|
||||
Permission.ProjectOwner,
|
||||
Permission.ProjectAdmin,
|
||||
Permission.CreateStatusPagePrivateUser,
|
||||
],
|
||||
read: [
|
||||
Permission.ProjectOwner,
|
||||
Permission.ProjectAdmin,
|
||||
Permission.ProjectMember,
|
||||
Permission.ReadStatusPagePrivateUser,
|
||||
],
|
||||
update: [],
|
||||
})
|
||||
@Index()
|
||||
@TableColumn({
|
||||
type: TableColumnType.ObjectID,
|
||||
required: true,
|
||||
canReadOnRelationQuery: true,
|
||||
title: "Status Page ID",
|
||||
description: "ID of your Status Page in which this object belongs",
|
||||
})
|
||||
@Column({
|
||||
type: ColumnType.ObjectID,
|
||||
nullable: false,
|
||||
transformer: ObjectID.getDatabaseTransformer(),
|
||||
})
|
||||
public statusPageId?: ObjectID = undefined;
|
||||
|
||||
@ColumnAccessControl({
|
||||
create: [
|
||||
Permission.ProjectOwner,
|
||||
Permission.ProjectAdmin,
|
||||
Permission.CreateStatusPagePrivateUser,
|
||||
],
|
||||
read: [
|
||||
Permission.ProjectOwner,
|
||||
Permission.ProjectAdmin,
|
||||
Permission.ProjectMember,
|
||||
Permission.ReadStatusPagePrivateUser,
|
||||
],
|
||||
update: [],
|
||||
})
|
||||
@TableColumn({
|
||||
manyToOneRelationColumn: "scimConfigId",
|
||||
type: TableColumnType.Entity,
|
||||
modelType: StatusPageSCIM,
|
||||
title: "SCIM Config",
|
||||
description: "Relation to Status Page SCIM Config Resource",
|
||||
})
|
||||
@ManyToOne(
|
||||
() => {
|
||||
return StatusPageSCIM;
|
||||
},
|
||||
{
|
||||
eager: false,
|
||||
nullable: false,
|
||||
onDelete: "CASCADE",
|
||||
orphanedRowAction: "delete",
|
||||
},
|
||||
)
|
||||
@JoinColumn({ name: "scimConfigId" })
|
||||
public scimConfig?: StatusPageSCIM = undefined;
|
||||
|
||||
@ColumnAccessControl({
|
||||
create: [
|
||||
Permission.ProjectOwner,
|
||||
Permission.ProjectAdmin,
|
||||
Permission.CreateStatusPagePrivateUser,
|
||||
],
|
||||
read: [
|
||||
Permission.ProjectOwner,
|
||||
Permission.ProjectAdmin,
|
||||
Permission.ProjectMember,
|
||||
Permission.ReadStatusPagePrivateUser,
|
||||
],
|
||||
update: [],
|
||||
})
|
||||
@Index()
|
||||
@TableColumn({
|
||||
type: TableColumnType.ObjectID,
|
||||
required: true,
|
||||
title: "SCIM Config ID",
|
||||
description: "ID of the Status Page SCIM Config this user mapping belongs to",
|
||||
})
|
||||
@Column({
|
||||
type: ColumnType.ObjectID,
|
||||
nullable: false,
|
||||
transformer: ObjectID.getDatabaseTransformer(),
|
||||
})
|
||||
public scimConfigId?: ObjectID = undefined;
|
||||
|
||||
@ColumnAccessControl({
|
||||
create: [
|
||||
Permission.ProjectOwner,
|
||||
Permission.ProjectAdmin,
|
||||
Permission.CreateStatusPagePrivateUser,
|
||||
],
|
||||
read: [
|
||||
Permission.ProjectOwner,
|
||||
Permission.ProjectAdmin,
|
||||
Permission.ProjectMember,
|
||||
Permission.ReadStatusPagePrivateUser,
|
||||
],
|
||||
update: [],
|
||||
})
|
||||
@TableColumn({
|
||||
manyToOneRelationColumn: "statusPagePrivateUserId",
|
||||
type: TableColumnType.Entity,
|
||||
modelType: StatusPagePrivateUser,
|
||||
title: "Status Page Private User",
|
||||
description: "Relation to Status Page Private User Resource",
|
||||
})
|
||||
@ManyToOne(
|
||||
() => {
|
||||
return StatusPagePrivateUser;
|
||||
},
|
||||
{
|
||||
eager: false,
|
||||
nullable: false,
|
||||
onDelete: "CASCADE",
|
||||
orphanedRowAction: "delete",
|
||||
},
|
||||
)
|
||||
@JoinColumn({ name: "statusPagePrivateUserId" })
|
||||
public statusPagePrivateUser?: StatusPagePrivateUser = undefined;
|
||||
|
||||
@ColumnAccessControl({
|
||||
create: [
|
||||
Permission.ProjectOwner,
|
||||
Permission.ProjectAdmin,
|
||||
Permission.CreateStatusPagePrivateUser,
|
||||
],
|
||||
read: [
|
||||
Permission.ProjectOwner,
|
||||
Permission.ProjectAdmin,
|
||||
Permission.ProjectMember,
|
||||
Permission.ReadStatusPagePrivateUser,
|
||||
],
|
||||
update: [],
|
||||
})
|
||||
@Index()
|
||||
@TableColumn({
|
||||
type: TableColumnType.ObjectID,
|
||||
required: true,
|
||||
title: "Status Page Private User ID",
|
||||
description: "ID of the Status Page Private User this external ID maps to",
|
||||
})
|
||||
@Column({
|
||||
type: ColumnType.ObjectID,
|
||||
nullable: false,
|
||||
transformer: ObjectID.getDatabaseTransformer(),
|
||||
})
|
||||
public statusPagePrivateUserId?: ObjectID = undefined;
|
||||
|
||||
@ColumnAccessControl({
|
||||
create: [
|
||||
Permission.ProjectOwner,
|
||||
Permission.ProjectAdmin,
|
||||
Permission.CreateStatusPagePrivateUser,
|
||||
],
|
||||
read: [
|
||||
Permission.ProjectOwner,
|
||||
Permission.ProjectAdmin,
|
||||
Permission.ProjectMember,
|
||||
Permission.ReadStatusPagePrivateUser,
|
||||
],
|
||||
update: [],
|
||||
})
|
||||
@Index()
|
||||
@TableColumn({
|
||||
type: TableColumnType.LongText,
|
||||
required: true,
|
||||
title: "External ID",
|
||||
description: "External user ID from SCIM provider (e.g., Azure AD user ID)",
|
||||
})
|
||||
@Column({
|
||||
type: ColumnType.LongText,
|
||||
nullable: false,
|
||||
length: ColumnLength.LongText,
|
||||
})
|
||||
public externalId?: string = undefined;
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
import { MigrationInterface, QueryRunner } from "typeorm";
|
||||
|
||||
export class MigrationName1756740910798 implements MigrationInterface {
|
||||
public name = 'MigrationName1756740910798'
|
||||
|
||||
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query(`CREATE TABLE "SCIMUser" ("_id" uuid NOT NULL DEFAULT uuid_generate_v4(), "createdAt" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(), "updatedAt" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(), "deletedAt" TIMESTAMP WITH TIME ZONE, "version" integer NOT NULL, "projectId" uuid NOT NULL, "scimConfigId" uuid NOT NULL, "userId" uuid NOT NULL, "externalId" character varying(500) NOT NULL, CONSTRAINT "PK_161711d359ba1935520b5aa313e" PRIMARY KEY ("_id"))`);
|
||||
await queryRunner.query(`CREATE INDEX "IDX_7561dd17a97f143cdffe341184" ON "SCIMUser" ("projectId") `);
|
||||
await queryRunner.query(`CREATE INDEX "IDX_ca31718fa40f6a1ac4aa63b5d8" ON "SCIMUser" ("scimConfigId") `);
|
||||
await queryRunner.query(`CREATE INDEX "IDX_c0bebe6a5b38293c297a6e2b1c" ON "SCIMUser" ("userId") `);
|
||||
await queryRunner.query(`CREATE INDEX "IDX_3593cbfcd05e591bfe131bf58a" ON "SCIMUser" ("externalId") `);
|
||||
await queryRunner.query(`CREATE TABLE "StatusPageSCIMUser" ("_id" uuid NOT NULL DEFAULT uuid_generate_v4(), "createdAt" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(), "updatedAt" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(), "deletedAt" TIMESTAMP WITH TIME ZONE, "version" integer NOT NULL, "projectId" uuid NOT NULL, "statusPageId" uuid NOT NULL, "scimConfigId" uuid NOT NULL, "statusPagePrivateUserId" uuid NOT NULL, "externalId" character varying(500) NOT NULL, CONSTRAINT "PK_e2fb21d6da5fc881f7adf2310f6" PRIMARY KEY ("_id"))`);
|
||||
await queryRunner.query(`CREATE INDEX "IDX_e0f38e455921c08948b9402e8f" ON "StatusPageSCIMUser" ("projectId") `);
|
||||
await queryRunner.query(`CREATE INDEX "IDX_4282ed65830c3301d7b91297b3" ON "StatusPageSCIMUser" ("statusPageId") `);
|
||||
await queryRunner.query(`CREATE INDEX "IDX_43712b2bba1e0f13970353bee6" ON "StatusPageSCIMUser" ("scimConfigId") `);
|
||||
await queryRunner.query(`CREATE INDEX "IDX_8e7127bd5155fd551b218076e0" ON "StatusPageSCIMUser" ("statusPagePrivateUserId") `);
|
||||
await queryRunner.query(`CREATE INDEX "IDX_3cbb3996ed387428369f45b3cb" ON "StatusPageSCIMUser" ("externalId") `);
|
||||
await queryRunner.query(`ALTER TABLE "OnCallDutyPolicyScheduleLayer" ALTER COLUMN "rotation" SET DEFAULT '{"_type":"Recurring","value":{"intervalType":"Day","intervalCount":{"_type":"PositiveNumber","value":1}}}'`);
|
||||
await queryRunner.query(`ALTER TABLE "OnCallDutyPolicyScheduleLayer" ALTER COLUMN "restrictionTimes" SET DEFAULT '{"_type":"RestrictionTimes","value":{"restictionType":"None","dayRestrictionTimes":null,"weeklyRestrictionTimes":[]}}'`);
|
||||
await queryRunner.query(`ALTER TABLE "SCIMUser" ADD CONSTRAINT "FK_7561dd17a97f143cdffe341184f" FOREIGN KEY ("projectId") REFERENCES "Project"("_id") ON DELETE CASCADE ON UPDATE NO ACTION`);
|
||||
await queryRunner.query(`ALTER TABLE "SCIMUser" ADD CONSTRAINT "FK_ca31718fa40f6a1ac4aa63b5d8f" FOREIGN KEY ("scimConfigId") REFERENCES "ProjectSCIM"("_id") ON DELETE CASCADE ON UPDATE NO ACTION`);
|
||||
await queryRunner.query(`ALTER TABLE "SCIMUser" ADD CONSTRAINT "FK_c0bebe6a5b38293c297a6e2b1c7" FOREIGN KEY ("userId") REFERENCES "User"("_id") ON DELETE CASCADE ON UPDATE NO ACTION`);
|
||||
await queryRunner.query(`ALTER TABLE "StatusPageSCIMUser" ADD CONSTRAINT "FK_e0f38e455921c08948b9402e8ff" FOREIGN KEY ("projectId") REFERENCES "Project"("_id") ON DELETE CASCADE ON UPDATE NO ACTION`);
|
||||
await queryRunner.query(`ALTER TABLE "StatusPageSCIMUser" ADD CONSTRAINT "FK_4282ed65830c3301d7b91297b3f" FOREIGN KEY ("statusPageId") REFERENCES "StatusPage"("_id") ON DELETE CASCADE ON UPDATE NO ACTION`);
|
||||
await queryRunner.query(`ALTER TABLE "StatusPageSCIMUser" ADD CONSTRAINT "FK_43712b2bba1e0f13970353bee64" FOREIGN KEY ("scimConfigId") REFERENCES "StatusPageSCIM"("_id") ON DELETE CASCADE ON UPDATE NO ACTION`);
|
||||
await queryRunner.query(`ALTER TABLE "StatusPageSCIMUser" ADD CONSTRAINT "FK_8e7127bd5155fd551b218076e0e" FOREIGN KEY ("statusPagePrivateUserId") REFERENCES "StatusPagePrivateUser"("_id") ON DELETE CASCADE ON UPDATE NO ACTION`);
|
||||
}
|
||||
|
||||
public async down(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query(`ALTER TABLE "StatusPageSCIMUser" DROP CONSTRAINT "FK_8e7127bd5155fd551b218076e0e"`);
|
||||
await queryRunner.query(`ALTER TABLE "StatusPageSCIMUser" DROP CONSTRAINT "FK_43712b2bba1e0f13970353bee64"`);
|
||||
await queryRunner.query(`ALTER TABLE "StatusPageSCIMUser" DROP CONSTRAINT "FK_4282ed65830c3301d7b91297b3f"`);
|
||||
await queryRunner.query(`ALTER TABLE "StatusPageSCIMUser" DROP CONSTRAINT "FK_e0f38e455921c08948b9402e8ff"`);
|
||||
await queryRunner.query(`ALTER TABLE "SCIMUser" DROP CONSTRAINT "FK_c0bebe6a5b38293c297a6e2b1c7"`);
|
||||
await queryRunner.query(`ALTER TABLE "SCIMUser" DROP CONSTRAINT "FK_ca31718fa40f6a1ac4aa63b5d8f"`);
|
||||
await queryRunner.query(`ALTER TABLE "SCIMUser" DROP CONSTRAINT "FK_7561dd17a97f143cdffe341184f"`);
|
||||
await queryRunner.query(`ALTER TABLE "OnCallDutyPolicyScheduleLayer" ALTER COLUMN "restrictionTimes" SET DEFAULT '{"_type": "RestrictionTimes", "value": {"restictionType": "None", "dayRestrictionTimes": null, "weeklyRestrictionTimes": []}}'`);
|
||||
await queryRunner.query(`ALTER TABLE "OnCallDutyPolicyScheduleLayer" ALTER COLUMN "rotation" SET DEFAULT '{"_type": "Recurring", "value": {"intervalType": "Day", "intervalCount": {"_type": "PositiveNumber", "value": 1}}}'`);
|
||||
await queryRunner.query(`DROP INDEX "public"."IDX_3cbb3996ed387428369f45b3cb"`);
|
||||
await queryRunner.query(`DROP INDEX "public"."IDX_8e7127bd5155fd551b218076e0"`);
|
||||
await queryRunner.query(`DROP INDEX "public"."IDX_43712b2bba1e0f13970353bee6"`);
|
||||
await queryRunner.query(`DROP INDEX "public"."IDX_4282ed65830c3301d7b91297b3"`);
|
||||
await queryRunner.query(`DROP INDEX "public"."IDX_e0f38e455921c08948b9402e8f"`);
|
||||
await queryRunner.query(`DROP TABLE "StatusPageSCIMUser"`);
|
||||
await queryRunner.query(`DROP INDEX "public"."IDX_3593cbfcd05e591bfe131bf58a"`);
|
||||
await queryRunner.query(`DROP INDEX "public"."IDX_c0bebe6a5b38293c297a6e2b1c"`);
|
||||
await queryRunner.query(`DROP INDEX "public"."IDX_ca31718fa40f6a1ac4aa63b5d8"`);
|
||||
await queryRunner.query(`DROP INDEX "public"."IDX_7561dd17a97f143cdffe341184"`);
|
||||
await queryRunner.query(`DROP TABLE "SCIMUser"`);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -164,6 +164,7 @@ import { MigrationName1755778934927 } from "./1755778934927-MigrationName";
|
||||
import { MigrationName1756293325324 } from "./1756293325324-MigrationName";
|
||||
import { MigrationName1756296282627 } from "./1756296282627-MigrationName";
|
||||
import { MigrationName1756300358095 } from "./1756300358095-MigrationName";
|
||||
import { MigrationName1756740910798 } from "./1756740910798-MigrationName";
|
||||
|
||||
export default [
|
||||
InitialMigration,
|
||||
@@ -332,4 +333,5 @@ export default [
|
||||
MigrationName1756293325324,
|
||||
MigrationName1756296282627,
|
||||
MigrationName1756300358095,
|
||||
MigrationName1756740910798
|
||||
];
|
||||
|
||||
10
Common/Server/Services/SCIMUserService.ts
Normal file
10
Common/Server/Services/SCIMUserService.ts
Normal file
@@ -0,0 +1,10 @@
|
||||
import Model from "../../Models/DatabaseModels/SCIMUser";
|
||||
import DatabaseService from "./DatabaseService";
|
||||
|
||||
export class Service extends DatabaseService<Model> {
|
||||
public constructor() {
|
||||
super(Model);
|
||||
}
|
||||
}
|
||||
|
||||
export default new Service();
|
||||
10
Common/Server/Services/StatusPageSCIMUserService.ts
Normal file
10
Common/Server/Services/StatusPageSCIMUserService.ts
Normal file
@@ -0,0 +1,10 @@
|
||||
import Model from "../../Models/DatabaseModels/StatusPageSCIMUser";
|
||||
import DatabaseService from "./DatabaseService";
|
||||
|
||||
export class Service extends DatabaseService<Model> {
|
||||
public constructor() {
|
||||
super(Model);
|
||||
}
|
||||
}
|
||||
|
||||
export default new Service();
|
||||
@@ -103,6 +103,8 @@ The following table lists the configurable parameters of the OneUptime chart and
|
||||
| `probes.<key>.monitorFetchLimit` | Number of resources to be monitored in parallel | `10` | |
|
||||
| `probes.<key>.syntheticMonitorScriptTimeoutInMs` | Timeout for synthetic monitor script | `60000` | |
|
||||
| `probes.<key>.customCodeMonitorScriptTimeoutInMs` | Timeout for custom code monitor script | `60000` | |
|
||||
| `probes.<key>.proxy.httpProxyUrl` | HTTP proxy URL for HTTP requests made by the probe (optional) | `nil` | |
|
||||
| `probes.<key>.proxy.httpsProxyUrl` | HTTPS proxy URL for HTTPS requests made by the probe (optional) | `nil` | |
|
||||
| `probes.<key>.additionalContainers` | Additional containers to add to the probe pod | `nil` | |
|
||||
| `probes.<key>.resources` | Pod resources (limits, requests) | `nil` | |
|
||||
| `statusPage.cnameRecord` | CNAME record for the status page | `nil` | |
|
||||
|
||||
@@ -103,6 +103,14 @@ spec:
|
||||
- name: DISABLE_TELEMETRY
|
||||
value: {{ $val.disableTelemetryCollection | quote }}
|
||||
{{- end }}
|
||||
{{- if and $val.proxy $val.proxy.httpProxyUrl }}
|
||||
- name: HTTP_PROXY_URL
|
||||
value: {{ $val.proxy.httpProxyUrl | squote }}
|
||||
{{- end }}
|
||||
{{- if and $val.proxy $val.proxy.httpsProxyUrl }}
|
||||
- name: HTTPS_PROXY_URL
|
||||
value: {{ $val.proxy.httpsProxyUrl | squote }}
|
||||
{{- end }}
|
||||
{{- include "oneuptime.env.oneuptimeSecret" $ | nindent 12 }}
|
||||
ports:
|
||||
- containerPort: {{ if and $val.ports $val.ports.http }}{{ $val.ports.http }}{{ else }}3874{{ end }}
|
||||
|
||||
@@ -208,6 +208,18 @@ probes:
|
||||
disableAutoscaler: false
|
||||
ports:
|
||||
http: 3874
|
||||
# Proxy configuration for probe connections
|
||||
proxy:
|
||||
# HTTP proxy URL for HTTP requests (optional)
|
||||
# Format: http://[username:password@]proxy.server.com:port
|
||||
# Example: http://proxy.example.com:8080
|
||||
# Example with auth: http://username:password@proxy.example.com:8080
|
||||
httpProxyUrl:
|
||||
# HTTPS proxy URL for HTTPS requests (optional)
|
||||
# Format: http://[username:password@]proxy.server.com:port
|
||||
# Example: http://proxy.example.com:8080
|
||||
# Example with auth: http://username:password@proxy.example.com:8080
|
||||
httpsProxyUrl:
|
||||
# KEDA autoscaling configuration based on monitor queue metrics
|
||||
keda:
|
||||
enabled: false
|
||||
@@ -234,6 +246,12 @@ probes:
|
||||
# customCodeMonitorScriptTimeoutInMs: 60000
|
||||
# disableTelemetryCollection: false
|
||||
# disableAutoscaler: false
|
||||
# # Proxy configuration for probe connections
|
||||
# proxy:
|
||||
# # HTTP proxy URL for HTTP requests (optional)
|
||||
# httpProxyUrl:
|
||||
# # HTTPS proxy URL for HTTPS requests (optional)
|
||||
# httpsProxyUrl:
|
||||
# resources:
|
||||
# additionalContainers:
|
||||
# KEDA autoscaling configuration based on monitor queue metrics
|
||||
|
||||
@@ -90,10 +90,12 @@ export const PORT: Port = new Port(
|
||||
// Format: http://[username:password@]proxy.example.com:port
|
||||
// Example: http://proxy.example.com:8080
|
||||
// Example with auth: http://user:pass@proxy.example.com:8080
|
||||
export const HTTP_PROXY_URL: string | null = process.env["HTTP_PROXY_URL"] || process.env["http_proxy"] || null;
|
||||
export const HTTP_PROXY_URL: string | null =
|
||||
process.env["HTTP_PROXY_URL"] || process.env["http_proxy"] || null;
|
||||
|
||||
// HTTPS_PROXY_URL: Proxy for HTTPS requests
|
||||
// Format: http://[username:password@]proxy.example.com:port
|
||||
// Example: http://proxy.example.com:8080
|
||||
// Example with auth: http://user:pass@proxy.example.com:8080
|
||||
export const HTTPS_PROXY_URL: string | null = process.env["HTTPS_PROXY_URL"] || process.env["https_proxy"] || null;
|
||||
export const HTTPS_PROXY_URL: string | null =
|
||||
process.env["HTTPS_PROXY_URL"] || process.env["https_proxy"] || null;
|
||||
|
||||
@@ -29,20 +29,20 @@ const init: PromiseVoidFunction = async (): Promise<void> => {
|
||||
// Log proxy status
|
||||
if (ProxyConfig.isProxyConfigured()) {
|
||||
logger.info("Proxy configuration:");
|
||||
|
||||
const httpProxy = ProxyConfig.getHttpProxyUrl();
|
||||
const httpsProxy = ProxyConfig.getHttpsProxyUrl();
|
||||
|
||||
const httpProxy: string | null = ProxyConfig.getHttpProxyUrl();
|
||||
const httpsProxy: string | null = ProxyConfig.getHttpsProxyUrl();
|
||||
|
||||
if (httpProxy) {
|
||||
logger.info(` HTTP proxy: ${httpProxy}`);
|
||||
}
|
||||
|
||||
|
||||
if (httpsProxy) {
|
||||
logger.info(` HTTPS proxy: ${httpsProxy}`);
|
||||
}
|
||||
|
||||
logger.info("Proxy will be used for all HTTP/HTTPS requests");
|
||||
}
|
||||
}
|
||||
|
||||
// Initialize telemetry
|
||||
Telemetry.init({
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
import OnlineCheck from "../../OnlineCheck";
|
||||
import ProxyConfig from "../../ProxyConfig";
|
||||
import URL from "Common/Types/API/URL";
|
||||
import type { HttpsProxyAgent } from "https-proxy-agent";
|
||||
import type { HttpProxyAgent } from "http-proxy-agent";
|
||||
import OneUptimeDate from "Common/Types/Date";
|
||||
import BadDataException from "Common/Types/Exception/BadDataException";
|
||||
import SslMonitorResponse from "Common/Types/Monitor/SSLMonitor/SslMonitorResponse";
|
||||
@@ -276,21 +278,25 @@ export default class SSLMonitor {
|
||||
|
||||
// Use proxy agent if proxy is configured
|
||||
if (ProxyConfig.isProxyConfigured()) {
|
||||
const httpsProxyAgent = ProxyConfig.getHttpsProxyAgent();
|
||||
const httpProxyAgent = ProxyConfig.getHttpProxyAgent();
|
||||
|
||||
const httpsProxyAgent: HttpsProxyAgent<string> | null =
|
||||
ProxyConfig.getHttpsProxyAgent();
|
||||
const httpProxyAgent: HttpProxyAgent<string> | null =
|
||||
ProxyConfig.getHttpProxyAgent();
|
||||
|
||||
// Prefer HTTPS proxy agent, fall back to HTTP proxy agent
|
||||
const proxyAgent = httpsProxyAgent || httpProxyAgent;
|
||||
|
||||
const proxyAgent:
|
||||
| (HttpsProxyAgent<string> | HttpProxyAgent<string>)
|
||||
| null = httpsProxyAgent || httpProxyAgent;
|
||||
|
||||
if (proxyAgent) {
|
||||
options.agent = proxyAgent;
|
||||
|
||||
const httpsProxyUrl = ProxyConfig.getHttpsProxyUrl();
|
||||
const httpProxyUrl = ProxyConfig.getHttpProxyUrl();
|
||||
const proxyUrl = httpsProxyUrl || httpProxyUrl;
|
||||
|
||||
|
||||
const httpsProxyUrl: string | null = ProxyConfig.getHttpsProxyUrl();
|
||||
const httpProxyUrl: string | null = ProxyConfig.getHttpProxyUrl();
|
||||
const proxyUrl: string | null = httpsProxyUrl || httpProxyUrl;
|
||||
|
||||
logger.debug(
|
||||
`SSL Monitor using proxy: ${proxyUrl} (HTTPS: ${!!httpsProxyUrl}, HTTP: ${!!httpProxyUrl})`,
|
||||
`SSL Monitor using proxy: ${proxyUrl} (HTTPS: ${Boolean(httpsProxyUrl)}, HTTP: ${Boolean(httpProxyUrl)})`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -146,8 +146,11 @@ export default class SyntheticMonitor {
|
||||
continue;
|
||||
}
|
||||
|
||||
const screenshotBuffer = result.returnValue.screenshots[screenshotName] as Buffer;
|
||||
scriptResult.screenshots[screenshotName] = screenshotBuffer.toString("base64"); // convert screenshots to base 64
|
||||
const screenshotBuffer: Buffer = result.returnValue.screenshots[
|
||||
screenshotName
|
||||
] as Buffer;
|
||||
scriptResult.screenshots[screenshotName] =
|
||||
screenshotBuffer.toString("base64"); // convert screenshots to base 64
|
||||
}
|
||||
}
|
||||
|
||||
@@ -284,23 +287,23 @@ export default class SyntheticMonitor {
|
||||
|
||||
// Prepare browser launch options with proxy support
|
||||
const baseOptions: BrowserLaunchOptions = {};
|
||||
|
||||
|
||||
// Configure proxy if available
|
||||
if (ProxyConfig.isProxyConfigured()) {
|
||||
const httpsProxyUrl = ProxyConfig.getHttpsProxyUrl();
|
||||
const httpProxyUrl = ProxyConfig.getHttpProxyUrl();
|
||||
|
||||
const httpsProxyUrl: string | null = ProxyConfig.getHttpsProxyUrl();
|
||||
const httpProxyUrl: string | null = ProxyConfig.getHttpProxyUrl();
|
||||
|
||||
// Prefer HTTPS proxy, fall back to HTTP proxy
|
||||
const proxyUrl = httpsProxyUrl || httpProxyUrl;
|
||||
|
||||
const proxyUrl: string | null = httpsProxyUrl || httpProxyUrl;
|
||||
|
||||
if (proxyUrl) {
|
||||
baseOptions.proxy = {
|
||||
server: proxyUrl,
|
||||
};
|
||||
|
||||
|
||||
// Extract username and password if present in proxy URL
|
||||
try {
|
||||
const parsedUrl = new URL(proxyUrl);
|
||||
const parsedUrl: globalThis.URL = new URL(proxyUrl);
|
||||
if (parsedUrl.username && parsedUrl.password) {
|
||||
baseOptions.proxy.username = parsedUrl.username;
|
||||
baseOptions.proxy.password = parsedUrl.password;
|
||||
@@ -308,9 +311,9 @@ export default class SyntheticMonitor {
|
||||
} catch (error) {
|
||||
logger.warn(`Failed to parse proxy URL for authentication: ${error}`);
|
||||
}
|
||||
|
||||
|
||||
logger.debug(
|
||||
`Synthetic Monitor using proxy: ${proxyUrl} (HTTPS: ${!!httpsProxyUrl}, HTTP: ${!!httpProxyUrl})`,
|
||||
`Synthetic Monitor using proxy: ${proxyUrl} (HTTPS: ${Boolean(httpsProxyUrl)}, HTTP: ${Boolean(httpProxyUrl)})`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -32,7 +32,7 @@ export default class ProxyConfig {
|
||||
if (HTTP_PROXY_URL) {
|
||||
this.httpProxyAgent = new HttpProxyAgent(HTTP_PROXY_URL);
|
||||
}
|
||||
|
||||
|
||||
if (HTTPS_PROXY_URL) {
|
||||
this.httpsProxyAgent = new HttpsProxyAgent(HTTPS_PROXY_URL);
|
||||
}
|
||||
@@ -41,7 +41,7 @@ export default class ProxyConfig {
|
||||
if (this.httpProxyAgent) {
|
||||
axios.defaults.httpAgent = this.httpProxyAgent;
|
||||
}
|
||||
|
||||
|
||||
if (this.httpsProxyAgent) {
|
||||
axios.defaults.httpsAgent = this.httpsProxyAgent;
|
||||
}
|
||||
@@ -59,7 +59,9 @@ export default class ProxyConfig {
|
||||
}
|
||||
|
||||
public static isProxyConfigured(): boolean {
|
||||
return this.isConfigured && (!!HTTP_PROXY_URL || !!HTTPS_PROXY_URL);
|
||||
return (
|
||||
this.isConfigured && (Boolean(HTTP_PROXY_URL) || Boolean(HTTPS_PROXY_URL))
|
||||
);
|
||||
}
|
||||
|
||||
public static getHttpProxyUrl(): string | null {
|
||||
@@ -95,12 +97,16 @@ export default class ProxyConfig {
|
||||
|
||||
try {
|
||||
if (HTTP_PROXY_URL) {
|
||||
const httpProxyAgent = new HttpProxyAgent(HTTP_PROXY_URL);
|
||||
const httpProxyAgent: HttpProxyAgent<string> = new HttpProxyAgent(
|
||||
HTTP_PROXY_URL,
|
||||
);
|
||||
instance.defaults.httpAgent = httpProxyAgent;
|
||||
}
|
||||
|
||||
if (HTTPS_PROXY_URL) {
|
||||
const httpsProxyAgent = new HttpsProxyAgent(HTTPS_PROXY_URL);
|
||||
const httpsProxyAgent: HttpsProxyAgent<string> = new HttpsProxyAgent(
|
||||
HTTPS_PROXY_URL,
|
||||
);
|
||||
instance.defaults.httpsAgent = httpsProxyAgent;
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user