From aa09bab7c9b12634fcff444f4f8ddb3265948d19 Mon Sep 17 00:00:00 2001 From: Nawaz Dhandala Date: Mon, 4 Aug 2025 21:36:11 +0100 Subject: [PATCH] Refactor SCIM migrations and models; update formatting and improve readability - Added missing comma in AllModelTypes array in Index.ts. - Refactored MigrationName1754304193228 to improve query formatting and readability. - Refactored MigrationName1754315774827 for consistency in formatting. - Updated migration index file to include new migration. - Standardized string quotes in Queue.ts for consistency. - Cleaned up SCIMAuthorization.ts by removing unnecessary whitespace and improving log formatting. - Refactored StartServer.ts to standardize content-type header handling. - Improved formatting in SCIM.tsx for better readability and consistency. - Refactored Metrics.ts to standardize queueSize extraction and type checking. - Enhanced Probe.ts logging for clarity and consistency. --- App/FeatureSet/BaseAPI/Index.ts | 2 - App/FeatureSet/Identity/API/SCIM.ts | 248 +++++++++++------- Common/Models/DatabaseModels/Index.ts | 2 +- .../1754304193228-MigrationName.ts | 87 ++++-- .../1754315774827-MigrationName.ts | 19 +- .../Postgres/SchemaMigrations/Index.ts | 2 +- Common/Server/Infrastructure/Queue.ts | 2 +- Common/Server/Middleware/SCIMAuthorization.ts | 44 ++-- Common/Server/Utils/StartServer.ts | 8 +- Dashboard/src/Pages/Settings/SCIM.tsx | 75 ++++-- Probe/API/Metrics.ts | 4 +- ProbeIngest/API/Probe.ts | 6 +- 12 files changed, 307 insertions(+), 192 deletions(-) diff --git a/App/FeatureSet/BaseAPI/Index.ts b/App/FeatureSet/BaseAPI/Index.ts index d67d3c8917..672795e777 100644 --- a/App/FeatureSet/BaseAPI/Index.ts +++ b/App/FeatureSet/BaseAPI/Index.ts @@ -583,14 +583,12 @@ import StatusPageAnnouncementTemplateService, { Service as StatusPageAnnouncementTemplateServiceType, } from "Common/Server/Services/StatusPageAnnouncementTemplateService"; - // ProjectSCIM import ProjectSCIM from "Common/Models/DatabaseModels/ProjectSCIM"; import ProjectSCIMService, { Service as ProjectSCIMServiceType, } from "Common/Server/Services/ProjectSCIMService"; - // Open API Spec import OpenAPI from "Common/Server/API/OpenAPI"; diff --git a/App/FeatureSet/Identity/API/SCIM.ts b/App/FeatureSet/Identity/API/SCIM.ts index 4647fceea9..0f90c21a3c 100644 --- a/App/FeatureSet/Identity/API/SCIM.ts +++ b/App/FeatureSet/Identity/API/SCIM.ts @@ -27,13 +27,18 @@ const router = Express.getRouter(); // Utility functions const parseNameFromSCIM = (scimUser: JSONObject): string => { + logger.debug( + `Parsing name from SCIM user: ${JSON.stringify(scimUser, null, 2)}`, + ); - logger.debug(`Parsing name from SCIM user: ${JSON.stringify(scimUser, null, 2)}`); + const givenName = + ((scimUser["name"] as JSONObject)?.["givenName"] as string) || ""; + const familyName = + ((scimUser["name"] as JSONObject)?.["familyName"] as string) || ""; + const formattedName = (scimUser["name"] as JSONObject)?.[ + "formatted" + ] as string; - const givenName = (scimUser["name"] as JSONObject)?.["givenName"] as string || ""; - const familyName = (scimUser["name"] as JSONObject)?.["familyName"] as string || ""; - const formattedName = (scimUser["name"] as JSONObject)?.["formatted"] as string; - // Construct full name: prefer formatted, then combine given+family, then fallback to displayName if (formattedName) { return formattedName; @@ -45,22 +50,24 @@ const parseNameFromSCIM = (scimUser: JSONObject): string => { return ""; }; -const parseNameToSCIMFormat = (fullName: string): { givenName: string; familyName: string; formatted: string } => { +const parseNameToSCIMFormat = ( + fullName: string, +): { givenName: string; familyName: string; formatted: string } => { const nameParts = fullName.trim().split(/\s+/); const givenName = nameParts[0] || ""; const familyName = nameParts.slice(1).join(" ") || ""; - + return { givenName, familyName, - formatted: fullName + formatted: fullName, }; }; const formatUserForSCIM = (user: User, req: ExpressRequest): JSONObject => { const baseUrl = `${req.protocol}://${req.get("host")}`; const nameData = parseNameToSCIMFormat(user.name?.toString() || ""); - + return { schemas: ["urn:ietf:params:scim:schemas:core:2.0:User"], id: user.id?.toString(), @@ -88,21 +95,26 @@ const formatUserForSCIM = (user: User, req: ExpressRequest): JSONObject => { }; const handleUserTeamOperations = async ( - operation: 'add' | 'remove', + operation: "add" | "remove", projectId: ObjectID, userId: ObjectID, - scimConfig: ProjectSCIM + scimConfig: ProjectSCIM, ): Promise => { - const teamsIds: Array = scimConfig.teams?.map((team: any) => team.id) || []; - + const teamsIds: Array = + scimConfig.teams?.map((team: any) => { + return team.id; + }) || []; + if (teamsIds.length === 0) { logger.debug(`SCIM Team operations - no teams configured for SCIM`); return; } - if (operation === 'add') { - logger.debug(`SCIM Team operations - adding user to ${teamsIds.length} configured teams`); - + if (operation === "add") { + logger.debug( + `SCIM Team operations - adding user to ${teamsIds.length} configured teams`, + ); + for (const team of scimConfig.teams || []) { const existingMember = await TeamMemberService.findOneBy({ query: { @@ -116,7 +128,7 @@ const handleUserTeamOperations = async ( if (!existingMember) { logger.debug(`SCIM Team operations - adding user to team: ${team.id}`); - let teamMember: TeamMember = new TeamMember(); + const teamMember: TeamMember = new TeamMember(); teamMember.projectId = projectId; teamMember.userId = userId; teamMember.teamId = team.id!; @@ -131,12 +143,16 @@ const handleUserTeamOperations = async ( }, }); } else { - logger.debug(`SCIM Team operations - user already member of team: ${team.id}`); + logger.debug( + `SCIM Team operations - user already member of team: ${team.id}`, + ); } } - } else if (operation === 'remove') { - logger.debug(`SCIM Team operations - removing user from ${teamsIds.length} configured teams`); - + } else if (operation === "remove") { + logger.debug( + `SCIM Team operations - removing user from ${teamsIds.length} configured teams`, + ); + await TeamMemberService.deleteBy({ query: { projectId: projectId, @@ -157,7 +173,7 @@ router.get( async (req: ExpressRequest, res: ExpressResponse): Promise => { try { logger.debug( - `SCIM ServiceProviderConfig request for projectScimId: ${req.params["projectScimId"]}` + `SCIM ServiceProviderConfig request for projectScimId: ${req.params["projectScimId"]}`, ); const serviceProviderConfig = { schemas: [ @@ -207,7 +223,7 @@ router.get( logger.error(err); return Response.sendErrorResponse(req, res, err as BadRequestException); } - } + }, ); // Basic Users endpoint - GET /scim/v2/Users @@ -217,7 +233,7 @@ router.get( async (req: ExpressRequest, res: ExpressResponse): Promise => { try { logger.debug( - `SCIM Users list request for projectScimId: ${req.params["projectScimId"]}` + `SCIM Users list request for projectScimId: ${req.params["projectScimId"]}`, ); const oneuptimeRequest = req as OneUptimeRequest; const bearerData = oneuptimeRequest.bearerTokenData as JSONObject; @@ -229,11 +245,11 @@ router.get( const filter = req.query["filter"] as string; logger.debug( - `SCIM Users query params - startIndex: ${startIndex}, count: ${count}, filter: ${filter || "none"}` + `SCIM Users query params - startIndex: ${startIndex}, count: ${count}, filter: ${filter || "none"}`, ); // Build query for team members in this project - let query: Query = { + const query: Query = { projectId: projectId, }; @@ -252,11 +268,11 @@ router.get( if (user && user.id) { query.userId = user.id; logger.debug( - `SCIM Users filter - found user with id: ${user.id}` + `SCIM Users filter - found user with id: ${user.id}`, ); } else { logger.debug( - `SCIM Users filter - user not found for email: ${email}` + `SCIM Users filter - user not found for email: ${email}`, ); return Response.sendJsonObjectResponse(req, res, { schemas: ["urn:ietf:params:scim:api:messages:2.0:ListResponse"], @@ -290,28 +306,33 @@ router.get( }, }); - // now get unique users. + // now get unique users. const usersInProjects: Array = teamMembers - .filter((tm: TeamMember) => tm.user && tm.user.id) - .map((tm: TeamMember) => formatUserForSCIM(tm.user!, req)); + .filter((tm: TeamMember) => { + return tm.user && tm.user.id; + }) + .map((tm: TeamMember) => { + return formatUserForSCIM(tm.user!, req); + }); - // remove duplicates + // remove duplicates const uniqueUserIds = new Set(); - const users: Array = usersInProjects.filter((user: JSONObject) => { - if (uniqueUserIds.has(user['id']?.toString() || "")) { - return false; - } - uniqueUserIds.add(user['id']?.toString() || ""); - return true; - }); - + const users: Array = usersInProjects.filter( + (user: JSONObject) => { + if (uniqueUserIds.has(user["id"]?.toString() || "")) { + return false; + } + uniqueUserIds.add(user["id"]?.toString() || ""); + return true; + }, + ); // now paginate the results const paginatedUsers = users.slice( (startIndex - 1) * count, - startIndex * count + startIndex * count, ); - + logger.debug(`SCIM Users response prepared with ${users.length} users`); return Response.sendJsonObjectResponse(req, res, { @@ -325,7 +346,7 @@ router.get( logger.error(err); return Response.sendErrorResponse(req, res, err as BadRequestException); } - } + }, ); // Get Individual User - GET /scim/v2/Users/{id} @@ -335,15 +356,16 @@ router.get( async (req: ExpressRequest, res: ExpressResponse): Promise => { try { logger.debug( - `SCIM Get individual user request for userId: ${req.params["userId"]}, projectScimId: ${req.params["projectScimId"]}` + `SCIM Get individual user request for userId: ${req.params["userId"]}, projectScimId: ${req.params["projectScimId"]}`, ); const oneuptimeRequest = req as OneUptimeRequest; const bearerData = oneuptimeRequest.bearerTokenData as JSONObject; const projectId = bearerData["projectId"] as ObjectID; const userId = req.params["userId"]; - - logger.debug(`SCIM Get user - projectId: ${projectId}, userId: ${userId}`); + logger.debug( + `SCIM Get user - projectId: ${projectId}, userId: ${userId}`, + ); if (!userId) { throw new BadRequestException("User ID is required"); @@ -370,10 +392,10 @@ 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 userId: ${userId}`, ); throw new NotFoundException( - "User not found or not part of this project" + "User not found or not part of this project", ); } @@ -386,7 +408,7 @@ router.get( logger.error(err); return Response.sendErrorResponse(req, res, err as BadRequestException); } - } + }, ); // Update User - PUT /scim/v2/Users/{id} @@ -396,7 +418,7 @@ router.put( async (req: ExpressRequest, res: ExpressResponse): Promise => { try { logger.debug( - `SCIM Update user request for userId: ${req.params["userId"]}, projectScimId: ${req.params["projectScimId"]}` + `SCIM Update user request for userId: ${req.params["userId"]}, projectScimId: ${req.params["projectScimId"]}`, ); const oneuptimeRequest = req as OneUptimeRequest; const bearerData = oneuptimeRequest.bearerTokenData as JSONObject; @@ -405,12 +427,11 @@ router.put( const scimUser = req.body; logger.debug( - `SCIM Update user - projectId: ${projectId}, userId: ${userId}` + `SCIM Update user - projectId: ${projectId}, userId: ${userId}`, ); - logger.debug( - `Request body for SCIM Update user: ${JSON.stringify(scimUser, null, 2)}` + `Request body for SCIM Update user: ${JSON.stringify(scimUser, null, 2)}`, ); if (!userId) { @@ -438,43 +459,69 @@ router.put( if (!projectUser || !projectUser.user) { logger.debug( - `SCIM Update user - user not found or not part of project for userId: ${userId}` + `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" + "User not found or not part of this project", ); } // Update user information - const email = scimUser["userName"] as string || (scimUser["emails"] as JSONObject[])?.[0]?.["value"] as string; + const email = + (scimUser["userName"] as string) || + ((scimUser["emails"] as JSONObject[])?.[0]?.["value"] as string); const name = parseNameFromSCIM(scimUser); const active = scimUser["active"] as boolean; - logger.debug(`SCIM Update user - email: ${email}, name: ${name}, active: ${active}`); + logger.debug( + `SCIM Update user - email: ${email}, name: ${name}, active: ${active}`, + ); // Handle user deactivation by removing from teams if (active === false) { - logger.debug(`SCIM Update user - user marked as inactive, removing from teams`); + logger.debug( + `SCIM Update user - user marked as inactive, removing from teams`, + ); const scimConfig = bearerData["scimConfig"] as ProjectSCIM; - await handleUserTeamOperations('remove', projectId, new ObjectID(userId), scimConfig); - logger.debug(`SCIM Update user - user successfully removed from teams due to deactivation`); + await handleUserTeamOperations( + "remove", + projectId, + new ObjectID(userId), + scimConfig, + ); + logger.debug( + `SCIM Update user - user successfully removed from teams due to deactivation`, + ); } // Handle user activation by adding to teams if (active === true) { - logger.debug(`SCIM Update user - user marked as active, adding to teams`); + logger.debug( + `SCIM Update user - user marked as active, adding to teams`, + ); const scimConfig = bearerData["scimConfig"] as ProjectSCIM; - await handleUserTeamOperations('add', projectId, new ObjectID(userId), scimConfig); - logger.debug(`SCIM Update user - user successfully added to teams due to activation`); + await handleUserTeamOperations( + "add", + projectId, + new ObjectID(userId), + scimConfig, + ); + logger.debug( + `SCIM Update user - user successfully added to teams due to activation`, + ); } if (email || name) { const updateData: any = {}; - if (email) updateData.email = new Email(email); - if (name) updateData.name = new Name(name); + if (email) { + updateData.email = new Email(email); + } + if (name) { + updateData.name = new Name(name); + } logger.debug( - `SCIM Update user - updating user with data: ${JSON.stringify(updateData)}` + `SCIM Update user - updating user with data: ${JSON.stringify(updateData)}`, ); await UserService.updateOneById({ @@ -505,7 +552,7 @@ router.put( } logger.debug( - `SCIM Update user - no updates made, returning existing user` + `SCIM Update user - no updates made, returning existing user`, ); // If no updates were made, return the existing user @@ -516,7 +563,7 @@ router.put( logger.error(err); return Response.sendErrorResponse(req, res, err as BadRequestException); } - } + }, ); // Groups endpoint - GET /scim/v2/Groups @@ -526,27 +573,29 @@ router.get( async (req: ExpressRequest, res: ExpressResponse): Promise => { try { logger.debug( - `SCIM Groups list request for projectScimId: ${req.params["projectScimId"]}` + `SCIM Groups list request for projectScimId: ${req.params["projectScimId"]}`, ); const oneuptimeRequest = req as OneUptimeRequest; const bearerData = oneuptimeRequest.bearerTokenData as JSONObject; const scimConfig = bearerData["scimConfig"] as ProjectSCIM; logger.debug( - `SCIM Groups - found ${scimConfig.teams?.length || 0} configured teams` + `SCIM Groups - found ${scimConfig.teams?.length || 0} configured teams`, ); // Return configured teams as groups - const groups = (scimConfig.teams || []).map((team: any) => ({ - schemas: ["urn:ietf:params:scim:schemas:core:2.0:Group"], - id: team.id?.toString(), - displayName: team.name?.toString(), - members: [], - meta: { - resourceType: "Group", - location: `${req.protocol}://${req.get("host")}/scim/v2/${req.params["projectScimId"]}/Groups/${team.id?.toString()}`, - }, - })); + const groups = (scimConfig.teams || []).map((team: any) => { + return { + schemas: ["urn:ietf:params:scim:schemas:core:2.0:Group"], + id: team.id?.toString(), + displayName: team.name?.toString(), + members: [], + meta: { + resourceType: "Group", + location: `${req.protocol}://${req.get("host")}/scim/v2/${req.params["projectScimId"]}/Groups/${team.id?.toString()}`, + }, + }; + }); return Response.sendJsonObjectResponse(req, res, { schemas: ["urn:ietf:params:scim:api:messages:2.0:ListResponse"], @@ -559,7 +608,7 @@ router.get( logger.error(err); return Response.sendErrorResponse(req, res, err as BadRequestException); } - } + }, ); // Create User - POST /scim/v2/Users @@ -569,7 +618,7 @@ router.post( async (req: ExpressRequest, res: ExpressResponse): Promise => { try { logger.debug( - `SCIM Create user request for projectScimId: ${req.params["projectScimId"]}` + `SCIM Create user request for projectScimId: ${req.params["projectScimId"]}`, ); const oneuptimeRequest = req as OneUptimeRequest; const bearerData = oneuptimeRequest.bearerTokenData as JSONObject; @@ -578,12 +627,14 @@ router.post( if (!scimConfig.autoProvisionUsers) { throw new BadRequestException( - "Auto-provisioning is disabled for this project" + "Auto-provisioning is disabled for this project", ); } const scimUser = req.body; - const email = scimUser["userName"] as string || (scimUser["emails"] as JSONObject[])?.[0]?.["value"] as string; + const email = + (scimUser["userName"] as string) || + ((scimUser["emails"] as JSONObject[])?.[0]?.["value"] as string); const name = parseNameFromSCIM(scimUser); logger.debug(`SCIM Create user - email: ${email}, name: ${name}`); @@ -608,7 +659,7 @@ router.post( // Create user if doesn't exist if (!user) { logger.debug( - `SCIM Create user - creating new user for email: ${email}` + `SCIM Create user - creating new user for email: ${email}`, ); user = await UserService.createByEmail({ email: new Email(email), @@ -619,22 +670,22 @@ router.post( }); } else { logger.debug( - `SCIM Create user - user already exists with id: ${user.id}` + `SCIM Create user - user already exists with id: ${user.id}`, ); } // Add user to default teams if configured if (scimConfig.teams && scimConfig.teams.length > 0) { logger.debug( - `SCIM Create user - adding user to ${scimConfig.teams.length} configured teams` + `SCIM Create user - adding user to ${scimConfig.teams.length} configured teams`, ); - await handleUserTeamOperations('add', projectId, user.id!, scimConfig); + await handleUserTeamOperations("add", projectId, user.id!, scimConfig); } const createdUser = formatUserForSCIM(user, req); logger.debug( - `SCIM Create user - returning created user with id: ${user.id}` + `SCIM Create user - returning created user with id: ${user.id}`, ); res.status(201); @@ -643,7 +694,7 @@ router.post( logger.error(err); return Response.sendErrorResponse(req, res, err as BadRequestException); } - } + }, ); // Delete User - DELETE /scim/v2/Users/{id} @@ -653,7 +704,7 @@ router.delete( async (req: ExpressRequest, res: ExpressResponse): Promise => { try { logger.debug( - `SCIM Delete user request for userId: ${req.params["userId"]}, projectScimId: ${req.params["projectScimId"]}` + `SCIM Delete user request for userId: ${req.params["userId"]}, projectScimId: ${req.params["projectScimId"]}`, ); const oneuptimeRequest = req as OneUptimeRequest; const bearerData = oneuptimeRequest.bearerTokenData as JSONObject; @@ -664,7 +715,7 @@ router.delete( if (!scimConfig.autoDeprovisionUsers) { logger.debug("SCIM Delete user - auto-deprovisioning is disabled"); throw new BadRequestException( - "Auto-deprovisioning is disabled for this project" + "Auto-deprovisioning is disabled for this project", ); } @@ -673,7 +724,7 @@ router.delete( } logger.debug( - `SCIM Delete user - removing user from all teams in project: ${projectId}` + `SCIM Delete user - removing user from all teams in project: ${projectId}`, ); // Remove user from teams the SCIM configured @@ -682,10 +733,15 @@ router.delete( throw new BadRequestException("No teams configured for SCIM"); } - await handleUserTeamOperations('remove', projectId, new ObjectID(userId), scimConfig); + await handleUserTeamOperations( + "remove", + projectId, + new ObjectID(userId), + scimConfig, + ); logger.debug( - `SCIM Delete user - user successfully deprovisioned from project` + `SCIM Delete user - user successfully deprovisioned from project`, ); res.status(204); @@ -696,7 +752,7 @@ router.delete( logger.error(err); return Response.sendErrorResponse(req, res, err as BadRequestException); } - } + }, ); export default router; diff --git a/Common/Models/DatabaseModels/Index.ts b/Common/Models/DatabaseModels/Index.ts index da21b0cf78..3ccfa9f89b 100644 --- a/Common/Models/DatabaseModels/Index.ts +++ b/Common/Models/DatabaseModels/Index.ts @@ -382,7 +382,7 @@ const AllModelTypes: Array<{ OnCallDutyPolicyTimeLog, - ProjectSCIM + ProjectSCIM, ]; const modelTypeMap: { [key: string]: { new (): BaseModel } } = {}; diff --git a/Common/Server/Infrastructure/Postgres/SchemaMigrations/1754304193228-MigrationName.ts b/Common/Server/Infrastructure/Postgres/SchemaMigrations/1754304193228-MigrationName.ts index 6e4c1ab29c..856ad2522f 100644 --- a/Common/Server/Infrastructure/Postgres/SchemaMigrations/1754304193228-MigrationName.ts +++ b/Common/Server/Infrastructure/Postgres/SchemaMigrations/1754304193228-MigrationName.ts @@ -1,32 +1,67 @@ import { MigrationInterface, QueryRunner } from "typeorm"; export class MigrationName1754304193228 implements MigrationInterface { - name = 'MigrationName1754304193228' + name = "MigrationName1754304193228"; - public async up(queryRunner: QueryRunner): Promise { - await queryRunner.query(`CREATE TABLE "ProjectSCIM" ("_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, "name" character varying(100) NOT NULL, "description" character varying(500), "bearerToken" character varying(500) NOT NULL, "autoProvisionUsers" boolean NOT NULL DEFAULT true, "autoDeprovisionUsers" boolean NOT NULL DEFAULT true, "isEnabled" boolean NOT NULL DEFAULT false, "createdByUserId" uuid, "deletedByUserId" uuid, CONSTRAINT "PK_51e71d70211675a5c918aee4e68" PRIMARY KEY ("_id"))`); - await queryRunner.query(`CREATE INDEX "IDX_f916360335859c26c4d7051239" ON "ProjectSCIM" ("projectId") `); - await queryRunner.query(`CREATE TABLE "ProjectScimTeam" ("projectScimId" uuid NOT NULL, "teamId" uuid NOT NULL, CONSTRAINT "PK_db724b66b4fa8c880ce5ccf820b" PRIMARY KEY ("projectScimId", "teamId"))`); - await queryRunner.query(`CREATE INDEX "IDX_b9a28efd66600267f0e9de0731" ON "ProjectScimTeam" ("projectScimId") `); - await queryRunner.query(`CREATE INDEX "IDX_bb0eda2ef0c773f975e9ad8448" ON "ProjectScimTeam" ("teamId") `); - await queryRunner.query(`ALTER TABLE "ProjectSCIM" ADD CONSTRAINT "FK_f916360335859c26c4d7051239b" FOREIGN KEY ("projectId") REFERENCES "Project"("_id") ON DELETE CASCADE ON UPDATE NO ACTION`); - await queryRunner.query(`ALTER TABLE "ProjectSCIM" ADD CONSTRAINT "FK_5d5d587984f156e5215d51daff7" FOREIGN KEY ("createdByUserId") REFERENCES "User"("_id") ON DELETE SET NULL ON UPDATE NO ACTION`); - await queryRunner.query(`ALTER TABLE "ProjectSCIM" ADD CONSTRAINT "FK_9cadda4fc2af268b5670d02bf76" FOREIGN KEY ("deletedByUserId") REFERENCES "User"("_id") ON DELETE SET NULL ON UPDATE NO ACTION`); - await queryRunner.query(`ALTER TABLE "ProjectScimTeam" ADD CONSTRAINT "FK_b9a28efd66600267f0e9de0731b" FOREIGN KEY ("projectScimId") REFERENCES "ProjectSCIM"("_id") ON DELETE CASCADE ON UPDATE CASCADE`); - await queryRunner.query(`ALTER TABLE "ProjectScimTeam" ADD CONSTRAINT "FK_bb0eda2ef0c773f975e9ad8448a" FOREIGN KEY ("teamId") REFERENCES "Team"("_id") ON DELETE CASCADE ON UPDATE CASCADE`); - } - - public async down(queryRunner: QueryRunner): Promise { - await queryRunner.query(`ALTER TABLE "ProjectScimTeam" DROP CONSTRAINT "FK_bb0eda2ef0c773f975e9ad8448a"`); - await queryRunner.query(`ALTER TABLE "ProjectScimTeam" DROP CONSTRAINT "FK_b9a28efd66600267f0e9de0731b"`); - await queryRunner.query(`ALTER TABLE "ProjectSCIM" DROP CONSTRAINT "FK_9cadda4fc2af268b5670d02bf76"`); - await queryRunner.query(`ALTER TABLE "ProjectSCIM" DROP CONSTRAINT "FK_5d5d587984f156e5215d51daff7"`); - await queryRunner.query(`ALTER TABLE "ProjectSCIM" DROP CONSTRAINT "FK_f916360335859c26c4d7051239b"`); - await queryRunner.query(`DROP INDEX "public"."IDX_bb0eda2ef0c773f975e9ad8448"`); - await queryRunner.query(`DROP INDEX "public"."IDX_b9a28efd66600267f0e9de0731"`); - await queryRunner.query(`DROP TABLE "ProjectScimTeam"`); - await queryRunner.query(`DROP INDEX "public"."IDX_f916360335859c26c4d7051239"`); - await queryRunner.query(`DROP TABLE "ProjectSCIM"`); - } + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query( + `CREATE TABLE "ProjectSCIM" ("_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, "name" character varying(100) NOT NULL, "description" character varying(500), "bearerToken" character varying(500) NOT NULL, "autoProvisionUsers" boolean NOT NULL DEFAULT true, "autoDeprovisionUsers" boolean NOT NULL DEFAULT true, "isEnabled" boolean NOT NULL DEFAULT false, "createdByUserId" uuid, "deletedByUserId" uuid, CONSTRAINT "PK_51e71d70211675a5c918aee4e68" PRIMARY KEY ("_id"))`, + ); + await queryRunner.query( + `CREATE INDEX "IDX_f916360335859c26c4d7051239" ON "ProjectSCIM" ("projectId") `, + ); + await queryRunner.query( + `CREATE TABLE "ProjectScimTeam" ("projectScimId" uuid NOT NULL, "teamId" uuid NOT NULL, CONSTRAINT "PK_db724b66b4fa8c880ce5ccf820b" PRIMARY KEY ("projectScimId", "teamId"))`, + ); + await queryRunner.query( + `CREATE INDEX "IDX_b9a28efd66600267f0e9de0731" ON "ProjectScimTeam" ("projectScimId") `, + ); + await queryRunner.query( + `CREATE INDEX "IDX_bb0eda2ef0c773f975e9ad8448" ON "ProjectScimTeam" ("teamId") `, + ); + await queryRunner.query( + `ALTER TABLE "ProjectSCIM" ADD CONSTRAINT "FK_f916360335859c26c4d7051239b" FOREIGN KEY ("projectId") REFERENCES "Project"("_id") ON DELETE CASCADE ON UPDATE NO ACTION`, + ); + await queryRunner.query( + `ALTER TABLE "ProjectSCIM" ADD CONSTRAINT "FK_5d5d587984f156e5215d51daff7" FOREIGN KEY ("createdByUserId") REFERENCES "User"("_id") ON DELETE SET NULL ON UPDATE NO ACTION`, + ); + await queryRunner.query( + `ALTER TABLE "ProjectSCIM" ADD CONSTRAINT "FK_9cadda4fc2af268b5670d02bf76" FOREIGN KEY ("deletedByUserId") REFERENCES "User"("_id") ON DELETE SET NULL ON UPDATE NO ACTION`, + ); + await queryRunner.query( + `ALTER TABLE "ProjectScimTeam" ADD CONSTRAINT "FK_b9a28efd66600267f0e9de0731b" FOREIGN KEY ("projectScimId") REFERENCES "ProjectSCIM"("_id") ON DELETE CASCADE ON UPDATE CASCADE`, + ); + await queryRunner.query( + `ALTER TABLE "ProjectScimTeam" ADD CONSTRAINT "FK_bb0eda2ef0c773f975e9ad8448a" FOREIGN KEY ("teamId") REFERENCES "Team"("_id") ON DELETE CASCADE ON UPDATE CASCADE`, + ); + } + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query( + `ALTER TABLE "ProjectScimTeam" DROP CONSTRAINT "FK_bb0eda2ef0c773f975e9ad8448a"`, + ); + await queryRunner.query( + `ALTER TABLE "ProjectScimTeam" DROP CONSTRAINT "FK_b9a28efd66600267f0e9de0731b"`, + ); + await queryRunner.query( + `ALTER TABLE "ProjectSCIM" DROP CONSTRAINT "FK_9cadda4fc2af268b5670d02bf76"`, + ); + await queryRunner.query( + `ALTER TABLE "ProjectSCIM" DROP CONSTRAINT "FK_5d5d587984f156e5215d51daff7"`, + ); + await queryRunner.query( + `ALTER TABLE "ProjectSCIM" DROP CONSTRAINT "FK_f916360335859c26c4d7051239b"`, + ); + await queryRunner.query( + `DROP INDEX "public"."IDX_bb0eda2ef0c773f975e9ad8448"`, + ); + await queryRunner.query( + `DROP INDEX "public"."IDX_b9a28efd66600267f0e9de0731"`, + ); + await queryRunner.query(`DROP TABLE "ProjectScimTeam"`); + await queryRunner.query( + `DROP INDEX "public"."IDX_f916360335859c26c4d7051239"`, + ); + await queryRunner.query(`DROP TABLE "ProjectSCIM"`); + } } diff --git a/Common/Server/Infrastructure/Postgres/SchemaMigrations/1754315774827-MigrationName.ts b/Common/Server/Infrastructure/Postgres/SchemaMigrations/1754315774827-MigrationName.ts index 117b7ea27d..31af21b038 100644 --- a/Common/Server/Infrastructure/Postgres/SchemaMigrations/1754315774827-MigrationName.ts +++ b/Common/Server/Infrastructure/Postgres/SchemaMigrations/1754315774827-MigrationName.ts @@ -1,14 +1,17 @@ import { MigrationInterface, QueryRunner } from "typeorm"; export class MigrationName1754315774827 implements MigrationInterface { - name = 'MigrationName1754315774827' + name = "MigrationName1754315774827"; - public async up(queryRunner: QueryRunner): Promise { - await queryRunner.query(`ALTER TABLE "ProjectSCIM" DROP COLUMN "isEnabled"`); - } - - public async down(queryRunner: QueryRunner): Promise { - await queryRunner.query(`ALTER TABLE "ProjectSCIM" ADD "isEnabled" boolean NOT NULL DEFAULT false`); - } + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query( + `ALTER TABLE "ProjectSCIM" DROP COLUMN "isEnabled"`, + ); + } + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query( + `ALTER TABLE "ProjectSCIM" ADD "isEnabled" boolean NOT NULL DEFAULT false`, + ); + } } diff --git a/Common/Server/Infrastructure/Postgres/SchemaMigrations/Index.ts b/Common/Server/Infrastructure/Postgres/SchemaMigrations/Index.ts index 8dcfe1c92b..6af8e95ae3 100644 --- a/Common/Server/Infrastructure/Postgres/SchemaMigrations/Index.ts +++ b/Common/Server/Infrastructure/Postgres/SchemaMigrations/Index.ts @@ -299,5 +299,5 @@ export default [ AddPerformanceIndexes1753378524062, MigrationName1753383711511, MigrationName1754304193228, - MigrationName1754315774827 + MigrationName1754315774827, ]; diff --git a/Common/Server/Infrastructure/Queue.ts b/Common/Server/Infrastructure/Queue.ts index bba38bd6d2..340004037b 100644 --- a/Common/Server/Infrastructure/Queue.ts +++ b/Common/Server/Infrastructure/Queue.ts @@ -225,7 +225,7 @@ export default class Queue { }; if (job.stacktrace && job.stacktrace.length > 0) { - result.stackTrace = job.stacktrace.join('\n'); + result.stackTrace = job.stacktrace.join("\n"); } return result; diff --git a/Common/Server/Middleware/SCIMAuthorization.ts b/Common/Server/Middleware/SCIMAuthorization.ts index 586b7df041..dbac11ed6b 100644 --- a/Common/Server/Middleware/SCIMAuthorization.ts +++ b/Common/Server/Middleware/SCIMAuthorization.ts @@ -13,7 +13,6 @@ import CaptureSpan from "../Utils/Telemetry/CaptureSpan"; import logger from "../Utils/Logger"; export default class SCIMMiddleware { - @CaptureSpan() public static async isAuthorizedSCIMRequest( req: ExpressRequest, @@ -40,39 +39,42 @@ export default class SCIMMiddleware { logger.debug( `SCIM Authorization: projectScimId=${projectScimId}, bearerToken=${ - bearerToken - }`); + bearerToken + }`, + ); if (!bearerToken) { throw new NotAuthorizedException( - "Bearer token is required for SCIM authentication" + "Bearer token is required for SCIM authentication", ); } // Find SCIM configuration by SCIM ID and bearer token - const scimConfig: ProjectSCIM | null = await ProjectSCIMService.findOneBy({ - query: { - _id: new ObjectID(projectScimId), - bearerToken: bearerToken, - }, - select: { - _id: true, - projectId: true, - autoProvisionUsers: true, - autoDeprovisionUsers: true, - teams: { + const scimConfig: ProjectSCIM | null = await ProjectSCIMService.findOneBy( + { + query: { + _id: new ObjectID(projectScimId), + bearerToken: bearerToken, + }, + select: { _id: true, - name: true, + projectId: true, + autoProvisionUsers: true, + autoDeprovisionUsers: true, + teams: { + _id: true, + name: true, + }, + }, + props: { + isRoot: true, }, }, - props: { - isRoot: true, - }, - }); + ); if (!scimConfig) { throw new NotAuthorizedException( - "Invalid bearer token or SCIM configuration not found" + "Invalid bearer token or SCIM configuration not found", ); } diff --git a/Common/Server/Utils/StartServer.ts b/Common/Server/Utils/StartServer.ts index 48f93c5da4..5eb7e67882 100644 --- a/Common/Server/Utils/StartServer.ts +++ b/Common/Server/Utils/StartServer.ts @@ -42,8 +42,6 @@ app.set("port", process.env["PORT"]); app.set("view engine", "ejs"); app.use(CookieParser()); - - const jsonBodyParserMiddleware: RequestHandler = ExpressJson({ limit: "50mb", extended: true, @@ -93,10 +91,10 @@ app.set("view engine", "ejs"); // Handle SCIM content type before JSON middleware app.use((req: ExpressRequest, _res: ExpressResponse, next: NextFunction) => { - const contentType = req.headers['content-type']; - if (contentType && contentType.includes('application/scim+json')) { + const contentType = req.headers["content-type"]; + if (contentType && contentType.includes("application/scim+json")) { // Set content type to application/json so express.json() can parse it - req.headers['content-type'] = 'application/json'; + req.headers["content-type"] = "application/json"; } next(); }); diff --git a/Dashboard/src/Pages/Settings/SCIM.tsx b/Dashboard/src/Pages/Settings/SCIM.tsx index a0d78122aa..e08c059d0f 100644 --- a/Dashboard/src/Pages/Settings/SCIM.tsx +++ b/Dashboard/src/Pages/Settings/SCIM.tsx @@ -10,9 +10,7 @@ import FieldType from "Common/UI/Components/Types/FieldType"; import HiddenText from "Common/UI/Components/HiddenText/HiddenText"; import ModelAPI from "Common/UI/Utils/ModelAPI/ModelAPI"; import API from "Common/UI/Utils/API/API"; -import { - IDENTITY_URL, -} from "Common/UI/Config"; +import { IDENTITY_URL } from "Common/UI/Config"; import Navigation from "Common/UI/Utils/Navigation"; import ProjectSCIM from "Common/Models/DatabaseModels/ProjectSCIM"; import Team from "Common/Models/DatabaseModels/Team"; @@ -29,14 +27,17 @@ const SCIMPage: FunctionComponent = ( _props: PageComponentProps, ): ReactElement => { const [showSCIMUrlId, setShowSCIMUrlId] = useState(""); - const [currentSCIMConfig, setCurrentSCIMConfig] = useState(null); + const [currentSCIMConfig, setCurrentSCIMConfig] = + useState(null); const [refresher, setRefresher] = useState(false); const [resetSCIMId, setResetSCIMId] = useState(""); const [showResetModal, setShowResetModal] = useState(false); const [isResetLoading, setIsResetLoading] = useState(false); const [resetError, setResetError] = useState(""); - const [showResetErrorModal, setShowResetErrorModal] = useState(false); - const [showResetSuccessModal, setShowResetSuccessModal] = useState(false); + const [showResetErrorModal, setShowResetErrorModal] = + useState(false); + const [showResetSuccessModal, setShowResetSuccessModal] = + useState(false); const [newBearerToken, setNewBearerToken] = useState(""); const resetBearerToken = async (): Promise => { @@ -114,7 +115,8 @@ const SCIMPage: FunctionComponent = ( title: "Name", fieldType: FormFieldSchemaType.Text, required: true, - description: "Friendly name to help you remember this SCIM configuration.", + description: + "Friendly name to help you remember this SCIM configuration.", placeholder: "Okta SCIM", validation: { minLength: 2, @@ -129,7 +131,8 @@ const SCIMPage: FunctionComponent = ( fieldType: FormFieldSchemaType.LongText, required: false, description: "Optional description for this SCIM configuration.", - placeholder: "SCIM configuration for automatic user provisioning from Okta", + placeholder: + "SCIM configuration for automatic user provisioning from Okta", stepId: "basic", }, { @@ -139,7 +142,8 @@ const SCIMPage: FunctionComponent = ( title: "Auto Provision Users", fieldType: FormFieldSchemaType.Checkbox, required: false, - description: "Automatically create users when they are added in your identity provider.", + description: + "Automatically create users when they are added in your identity provider.", stepId: "configuration", }, { @@ -149,7 +153,8 @@ const SCIMPage: FunctionComponent = ( title: "Auto Deprovision Users", fieldType: FormFieldSchemaType.Checkbox, required: false, - description: "Automatically remove users from teams when they are removed from your identity provider.", + description: + "Automatically remove users from teams when they are removed from your identity provider.", stepId: "configuration", }, { @@ -164,7 +169,8 @@ const SCIMPage: FunctionComponent = ( valueField: "_id", }, required: false, - description: "New users will be automatically added to these teams.", + description: + "New users will be automatically added to these teams.", stepId: "teams", }, ]} @@ -248,51 +254,66 @@ const SCIMPage: FunctionComponent = (

Use these URLs to configure SCIM in your identity provider:

- +
-

SCIM Base URL:

+

+ SCIM Base URL: +

{IDENTITY_URL.toString()}/scim/v2/{showSCIMUrlId}

- Use this as the SCIM endpoint URL in your identity provider + Use this as the SCIM endpoint URL in your identity + provider

-

Service Provider Config URL:

+

+ Service Provider Config URL: +

- {IDENTITY_URL.toString()}/scim/v2/{showSCIMUrlId}/ServiceProviderConfig + {IDENTITY_URL.toString()}/scim/v2/{showSCIMUrlId} + /ServiceProviderConfig
-

Users Endpoint:

+

+ Users Endpoint: +

{IDENTITY_URL.toString()}/scim/v2/{showSCIMUrlId}/Users
-

Groups Endpoint:

+

+ Groups Endpoint: +

{IDENTITY_URL.toString()}/scim/v2/{showSCIMUrlId}/Groups
-

Unique identifier field for users:

+

+ Unique identifier field for users: +

userName

- Use this field as the unique identifier for users in your identity provider SCIM configuration + Use this field as the unique identifier for users in your + identity provider SCIM configuration

-

Bearer Token:

+

+ Bearer Token: +

= ( />

- Use this bearer token for authentication in your identity provider SCIM configuration. + Use this bearer token for authentication in your identity + provider SCIM configuration.

@@ -352,12 +374,11 @@ const SCIMPage: FunctionComponent = ( title="New Bearer Token" description={
-

Your new Bearer Token has been generated:

+

+ Your new Bearer Token has been generated: +

- +

Please update your identity provider with this new token. diff --git a/Probe/API/Metrics.ts b/Probe/API/Metrics.ts index c21cac5b54..9a3f43152b 100644 --- a/Probe/API/Metrics.ts +++ b/Probe/API/Metrics.ts @@ -54,11 +54,11 @@ router.get( logger.debug(result.data); // Extract queueSize from the response - let queueSize: number = result.data['queueSize'] as number || 0; + let queueSize: number = (result.data["queueSize"] as number) || 0; // if string then convert to number - if (typeof queueSize === 'string') { + if (typeof queueSize === "string") { const parsedQueueSize = parseInt(queueSize, 10); if (!isNaN(parsedQueueSize)) { queueSize = parsedQueueSize; diff --git a/ProbeIngest/API/Probe.ts b/ProbeIngest/API/Probe.ts index b38bde05ed..58ae7fb4ad 100644 --- a/ProbeIngest/API/Probe.ts +++ b/ProbeIngest/API/Probe.ts @@ -231,7 +231,9 @@ router.post( }); } } else { - logger.debug("Billing is enabled, skipping probe offline email notification"); + logger.debug( + "Billing is enabled, skipping probe offline email notification", + ); } } @@ -405,7 +407,7 @@ router.post( probeId: probeId, isEnabled: true, nextPingAt: QueryHelper.lessThanEqualToOrNull( - OneUptimeDate.getSomeMinutesAgo(2) + OneUptimeDate.getSomeMinutesAgo(2), ), monitor: { ...MonitorService.getEnabledMonitorQuery(),