mirror of
https://github.com/OneUptime/oneuptime.git
synced 2026-04-06 00:32:12 +02:00
feat: Add SCIM Schemas and ResourceTypes endpoints for project and status page
This commit is contained in:
@@ -31,6 +31,8 @@ import {
|
||||
generateServiceProviderConfig,
|
||||
generateUsersListResponse,
|
||||
parseSCIMQueryParams,
|
||||
generateSchemasResponse,
|
||||
generateResourceTypesResponse,
|
||||
} from "../Utils/SCIMUtils";
|
||||
import {
|
||||
AppApiClientUrl,
|
||||
@@ -212,6 +214,64 @@ router.get(
|
||||
},
|
||||
);
|
||||
|
||||
// SCIM Schemas endpoint - GET /scim/v2/Schemas
|
||||
router.get(
|
||||
"/scim/v2/:projectScimId/Schemas",
|
||||
SCIMMiddleware.isAuthorizedSCIMRequest,
|
||||
async (
|
||||
req: ExpressRequest,
|
||||
res: ExpressResponse,
|
||||
next: NextFunction,
|
||||
): Promise<void> => {
|
||||
try {
|
||||
logger.debug(
|
||||
`Project SCIM Schemas - scimId: ${req.params["projectScimId"]!}`,
|
||||
);
|
||||
|
||||
const schemasResponse: JSONObject = generateSchemasResponse(
|
||||
req,
|
||||
req.params["projectScimId"]!,
|
||||
"project",
|
||||
);
|
||||
|
||||
logger.debug("Project SCIM Schemas response prepared successfully");
|
||||
return Response.sendJsonObjectResponse(req, res, schemasResponse);
|
||||
} catch (err) {
|
||||
logger.error(err);
|
||||
return next(err);
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
// SCIM ResourceTypes endpoint - GET /scim/v2/ResourceTypes
|
||||
router.get(
|
||||
"/scim/v2/:projectScimId/ResourceTypes",
|
||||
SCIMMiddleware.isAuthorizedSCIMRequest,
|
||||
async (
|
||||
req: ExpressRequest,
|
||||
res: ExpressResponse,
|
||||
next: NextFunction,
|
||||
): Promise<void> => {
|
||||
try {
|
||||
logger.debug(
|
||||
`Project SCIM ResourceTypes - scimId: ${req.params["projectScimId"]!}`,
|
||||
);
|
||||
|
||||
const resourceTypesResponse: JSONObject = generateResourceTypesResponse(
|
||||
req,
|
||||
req.params["projectScimId"]!,
|
||||
"project",
|
||||
);
|
||||
|
||||
logger.debug("Project SCIM ResourceTypes response prepared successfully");
|
||||
return Response.sendJsonObjectResponse(req, res, resourceTypesResponse);
|
||||
} catch (err) {
|
||||
logger.error(err);
|
||||
return next(err);
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
// Basic Users endpoint - GET /scim/v2/Users
|
||||
router.get(
|
||||
"/scim/v2/:projectScimId/Users",
|
||||
@@ -378,10 +438,10 @@ router.get(
|
||||
},
|
||||
);
|
||||
|
||||
// now paginate the results
|
||||
// now paginate the results (startIndex is 1-based in SCIM)
|
||||
const paginatedUsers: Array<JSONObject> = users.slice(
|
||||
(startIndex - 1) * count,
|
||||
startIndex * count,
|
||||
startIndex - 1,
|
||||
startIndex - 1 + count,
|
||||
);
|
||||
|
||||
logger.debug(`SCIM Users response prepared with ${users.length} users`);
|
||||
@@ -716,10 +776,10 @@ router.get(
|
||||
|
||||
const groups: Array<JSONObject> = await Promise.all(groupsPromises);
|
||||
|
||||
// Paginate results
|
||||
// Paginate results (startIndex is 1-based in SCIM)
|
||||
const paginatedGroups: Array<JSONObject> = groups.slice(
|
||||
(startIndex - 1) * count,
|
||||
startIndex * count,
|
||||
startIndex - 1,
|
||||
startIndex - 1 + count,
|
||||
);
|
||||
|
||||
logger.debug(
|
||||
|
||||
@@ -20,6 +20,8 @@ import LIMIT_MAX, { LIMIT_PER_PROJECT } from "Common/Types/Database/LimitMax";
|
||||
import {
|
||||
formatUserForSCIM,
|
||||
generateServiceProviderConfig,
|
||||
generateSchemasResponse,
|
||||
generateResourceTypesResponse,
|
||||
} from "../Utils/SCIMUtils";
|
||||
import Text from "Common/Types/Text";
|
||||
import HashedString from "Common/Types/HashedString";
|
||||
@@ -54,6 +56,66 @@ router.get(
|
||||
},
|
||||
);
|
||||
|
||||
// SCIM Schemas endpoint - GET /status-page-scim/v2/Schemas
|
||||
router.get(
|
||||
"/status-page-scim/v2/:statusPageScimId/Schemas",
|
||||
SCIMMiddleware.isAuthorizedSCIMRequest,
|
||||
async (
|
||||
req: ExpressRequest,
|
||||
res: ExpressResponse,
|
||||
next: NextFunction,
|
||||
): Promise<void> => {
|
||||
try {
|
||||
logger.debug(
|
||||
`Status Page SCIM Schemas - scimId: ${req.params["statusPageScimId"]!}`,
|
||||
);
|
||||
|
||||
const schemasResponse: JSONObject = generateSchemasResponse(
|
||||
req,
|
||||
req.params["statusPageScimId"]!,
|
||||
"status-page",
|
||||
);
|
||||
|
||||
logger.debug("Status Page SCIM Schemas response prepared successfully");
|
||||
return Response.sendJsonObjectResponse(req, res, schemasResponse);
|
||||
} catch (err) {
|
||||
logger.error(err);
|
||||
return next(err);
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
// SCIM ResourceTypes endpoint - GET /status-page-scim/v2/ResourceTypes
|
||||
router.get(
|
||||
"/status-page-scim/v2/:statusPageScimId/ResourceTypes",
|
||||
SCIMMiddleware.isAuthorizedSCIMRequest,
|
||||
async (
|
||||
req: ExpressRequest,
|
||||
res: ExpressResponse,
|
||||
next: NextFunction,
|
||||
): Promise<void> => {
|
||||
try {
|
||||
logger.debug(
|
||||
`Status Page SCIM ResourceTypes - scimId: ${req.params["statusPageScimId"]!}`,
|
||||
);
|
||||
|
||||
const resourceTypesResponse: JSONObject = generateResourceTypesResponse(
|
||||
req,
|
||||
req.params["statusPageScimId"]!,
|
||||
"status-page",
|
||||
);
|
||||
|
||||
logger.debug(
|
||||
"Status Page SCIM ResourceTypes response prepared successfully",
|
||||
);
|
||||
return Response.sendJsonObjectResponse(req, res, resourceTypesResponse);
|
||||
} catch (err) {
|
||||
logger.error(err);
|
||||
return next(err);
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
// Status Page Users endpoint - GET /status-page-scim/v2/Users
|
||||
router.get(
|
||||
"/status-page-scim/v2/:statusPageScimId/Users",
|
||||
@@ -154,10 +216,10 @@ router.get(
|
||||
},
|
||||
);
|
||||
|
||||
// Paginate the results
|
||||
// Paginate the results (startIndex is 1-based in SCIM)
|
||||
const paginatedUsers: Array<JSONObject> = users.slice(
|
||||
(startIndex - 1) * count,
|
||||
startIndex * count,
|
||||
startIndex - 1,
|
||||
startIndex - 1 + count,
|
||||
);
|
||||
|
||||
logger.debug(
|
||||
|
||||
@@ -4,6 +4,23 @@ import { JSONObject } from "Common/Types/JSON";
|
||||
import Email from "Common/Types/Email";
|
||||
import Name from "Common/Types/Name";
|
||||
import ObjectID from "Common/Types/ObjectID";
|
||||
import Exception from "Common/Types/Exception/Exception";
|
||||
|
||||
/**
|
||||
* SCIM Error types as defined in RFC 7644
|
||||
*/
|
||||
export enum SCIMErrorType {
|
||||
InvalidFilter = "invalidFilter",
|
||||
TooMany = "tooMany",
|
||||
Uniqueness = "uniqueness",
|
||||
Mutability = "mutability",
|
||||
InvalidSyntax = "invalidSyntax",
|
||||
InvalidPath = "invalidPath",
|
||||
NoTarget = "noTarget",
|
||||
InvalidValue = "invalidValue",
|
||||
InvalidVers = "invalidVers",
|
||||
Sensitive = "sensitive",
|
||||
}
|
||||
|
||||
/**
|
||||
* Shared SCIM utility functions for both Project SCIM and Status Page SCIM
|
||||
@@ -172,9 +189,9 @@ export const generateServiceProviderConfig: (
|
||||
supported: true,
|
||||
},
|
||||
bulk: {
|
||||
supported: true,
|
||||
maxOperations: 1000,
|
||||
maxPayloadSize: 1048576,
|
||||
supported: false,
|
||||
maxOperations: 0,
|
||||
maxPayloadSize: 0,
|
||||
},
|
||||
filter: {
|
||||
supported: true,
|
||||
@@ -263,3 +280,343 @@ export const parseSCIMQueryParams: (req: ExpressRequest) => {
|
||||
|
||||
return { startIndex, count };
|
||||
};
|
||||
|
||||
/**
|
||||
* Generate SCIM-compliant error response as per RFC 7644
|
||||
*/
|
||||
export const generateSCIMErrorResponse: (
|
||||
status: number,
|
||||
detail: string,
|
||||
scimType?: SCIMErrorType,
|
||||
) => JSONObject = (
|
||||
status: number,
|
||||
detail: string,
|
||||
scimType?: SCIMErrorType,
|
||||
): JSONObject => {
|
||||
const errorResponse: JSONObject = {
|
||||
schemas: ["urn:ietf:params:scim:api:messages:2.0:Error"],
|
||||
status: status.toString(),
|
||||
detail: detail,
|
||||
};
|
||||
|
||||
if (scimType) {
|
||||
errorResponse["scimType"] = scimType;
|
||||
}
|
||||
|
||||
return errorResponse;
|
||||
};
|
||||
|
||||
/**
|
||||
* Generate SCIM Schemas endpoint response
|
||||
*/
|
||||
export const generateSchemasResponse: (
|
||||
req: ExpressRequest,
|
||||
scimId: string,
|
||||
scimType: "project" | "status-page",
|
||||
) => JSONObject = (
|
||||
req: ExpressRequest,
|
||||
scimId: string,
|
||||
scimType: "project" | "status-page",
|
||||
): JSONObject => {
|
||||
const baseUrl: string = `${req.protocol}://${req.get("host")}`;
|
||||
const endpointPath: string =
|
||||
scimType === "project"
|
||||
? `/scim/v2/${scimId}`
|
||||
: `/status-page-scim/v2/${scimId}`;
|
||||
|
||||
const schemas: JSONObject[] = [
|
||||
{
|
||||
id: "urn:ietf:params:scim:schemas:core:2.0:User",
|
||||
name: "User",
|
||||
description: "User Schema",
|
||||
attributes: [
|
||||
{
|
||||
name: "userName",
|
||||
type: "string",
|
||||
multiValued: false,
|
||||
description: "Unique identifier for the User, typically email address",
|
||||
required: true,
|
||||
caseExact: false,
|
||||
mutability: "readWrite",
|
||||
returned: "default",
|
||||
uniqueness: "server",
|
||||
},
|
||||
{
|
||||
name: "name",
|
||||
type: "complex",
|
||||
multiValued: false,
|
||||
description: "The components of the user's name",
|
||||
required: false,
|
||||
subAttributes: [
|
||||
{
|
||||
name: "formatted",
|
||||
type: "string",
|
||||
multiValued: false,
|
||||
description: "The full name",
|
||||
required: false,
|
||||
mutability: "readWrite",
|
||||
returned: "default",
|
||||
},
|
||||
{
|
||||
name: "familyName",
|
||||
type: "string",
|
||||
multiValued: false,
|
||||
description: "The family name or last name",
|
||||
required: false,
|
||||
mutability: "readWrite",
|
||||
returned: "default",
|
||||
},
|
||||
{
|
||||
name: "givenName",
|
||||
type: "string",
|
||||
multiValued: false,
|
||||
description: "The given name or first name",
|
||||
required: false,
|
||||
mutability: "readWrite",
|
||||
returned: "default",
|
||||
},
|
||||
],
|
||||
mutability: "readWrite",
|
||||
returned: "default",
|
||||
},
|
||||
{
|
||||
name: "displayName",
|
||||
type: "string",
|
||||
multiValued: false,
|
||||
description: "The name of the User suitable for display",
|
||||
required: false,
|
||||
mutability: "readWrite",
|
||||
returned: "default",
|
||||
},
|
||||
{
|
||||
name: "emails",
|
||||
type: "complex",
|
||||
multiValued: true,
|
||||
description: "Email addresses for the user",
|
||||
required: false,
|
||||
subAttributes: [
|
||||
{
|
||||
name: "value",
|
||||
type: "string",
|
||||
multiValued: false,
|
||||
description: "Email address value",
|
||||
required: false,
|
||||
mutability: "readWrite",
|
||||
returned: "default",
|
||||
},
|
||||
{
|
||||
name: "type",
|
||||
type: "string",
|
||||
multiValued: false,
|
||||
description: "Type of email (work, home, other)",
|
||||
required: false,
|
||||
canonicalValues: ["work", "home", "other"],
|
||||
mutability: "readWrite",
|
||||
returned: "default",
|
||||
},
|
||||
{
|
||||
name: "primary",
|
||||
type: "boolean",
|
||||
multiValued: false,
|
||||
description: "Indicates if this is the primary email",
|
||||
required: false,
|
||||
mutability: "readWrite",
|
||||
returned: "default",
|
||||
},
|
||||
],
|
||||
mutability: "readWrite",
|
||||
returned: "default",
|
||||
},
|
||||
{
|
||||
name: "active",
|
||||
type: "boolean",
|
||||
multiValued: false,
|
||||
description: "Indicates whether the user is active",
|
||||
required: false,
|
||||
mutability: "readWrite",
|
||||
returned: "default",
|
||||
},
|
||||
],
|
||||
meta: {
|
||||
resourceType: "Schema",
|
||||
location: `${baseUrl}${endpointPath}/Schemas/urn:ietf:params:scim:schemas:core:2.0:User`,
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
// Add Group schema only for project SCIM
|
||||
if (scimType === "project") {
|
||||
schemas.push({
|
||||
id: "urn:ietf:params:scim:schemas:core:2.0:Group",
|
||||
name: "Group",
|
||||
description: "Group Schema (Teams in OneUptime)",
|
||||
attributes: [
|
||||
{
|
||||
name: "displayName",
|
||||
type: "string",
|
||||
multiValued: false,
|
||||
description: "Human-readable name for the Group/Team",
|
||||
required: true,
|
||||
mutability: "readWrite",
|
||||
returned: "default",
|
||||
uniqueness: "server",
|
||||
},
|
||||
{
|
||||
name: "members",
|
||||
type: "complex",
|
||||
multiValued: true,
|
||||
description: "A list of members of the Group",
|
||||
required: false,
|
||||
subAttributes: [
|
||||
{
|
||||
name: "value",
|
||||
type: "string",
|
||||
multiValued: false,
|
||||
description: "Identifier of the member",
|
||||
required: false,
|
||||
mutability: "immutable",
|
||||
returned: "default",
|
||||
},
|
||||
{
|
||||
name: "$ref",
|
||||
type: "reference",
|
||||
referenceTypes: ["User"],
|
||||
multiValued: false,
|
||||
description: "URI of the member resource",
|
||||
required: false,
|
||||
mutability: "immutable",
|
||||
returned: "default",
|
||||
},
|
||||
{
|
||||
name: "display",
|
||||
type: "string",
|
||||
multiValued: false,
|
||||
description: "Display name of the member",
|
||||
required: false,
|
||||
mutability: "immutable",
|
||||
returned: "default",
|
||||
},
|
||||
],
|
||||
mutability: "readWrite",
|
||||
returned: "default",
|
||||
},
|
||||
],
|
||||
meta: {
|
||||
resourceType: "Schema",
|
||||
location: `${baseUrl}${endpointPath}/Schemas/urn:ietf:params:scim:schemas:core:2.0:Group`,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
return {
|
||||
schemas: ["urn:ietf:params:scim:api:messages:2.0:ListResponse"],
|
||||
totalResults: schemas.length,
|
||||
itemsPerPage: schemas.length,
|
||||
startIndex: 1,
|
||||
Resources: schemas,
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Generate SCIM ResourceTypes endpoint response
|
||||
*/
|
||||
export const generateResourceTypesResponse: (
|
||||
req: ExpressRequest,
|
||||
scimId: string,
|
||||
scimType: "project" | "status-page",
|
||||
) => JSONObject = (
|
||||
req: ExpressRequest,
|
||||
scimId: string,
|
||||
scimType: "project" | "status-page",
|
||||
): JSONObject => {
|
||||
const baseUrl: string = `${req.protocol}://${req.get("host")}`;
|
||||
const endpointPath: string =
|
||||
scimType === "project"
|
||||
? `/scim/v2/${scimId}`
|
||||
: `/status-page-scim/v2/${scimId}`;
|
||||
|
||||
const resourceTypes: JSONObject[] = [
|
||||
{
|
||||
schemas: ["urn:ietf:params:scim:schemas:core:2.0:ResourceType"],
|
||||
id: "User",
|
||||
name: "User",
|
||||
endpoint: "/Users",
|
||||
description: "User Account",
|
||||
schema: "urn:ietf:params:scim:schemas:core:2.0:User",
|
||||
schemaExtensions: [],
|
||||
meta: {
|
||||
resourceType: "ResourceType",
|
||||
location: `${baseUrl}${endpointPath}/ResourceTypes/User`,
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
// Add Group resource type only for project SCIM
|
||||
if (scimType === "project") {
|
||||
resourceTypes.push({
|
||||
schemas: ["urn:ietf:params:scim:schemas:core:2.0:ResourceType"],
|
||||
id: "Group",
|
||||
name: "Group",
|
||||
endpoint: "/Groups",
|
||||
description: "Group (Team in OneUptime)",
|
||||
schema: "urn:ietf:params:scim:schemas:core:2.0:Group",
|
||||
schemaExtensions: [],
|
||||
meta: {
|
||||
resourceType: "ResourceType",
|
||||
location: `${baseUrl}${endpointPath}/ResourceTypes/Group`,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
return {
|
||||
schemas: ["urn:ietf:params:scim:api:messages:2.0:ListResponse"],
|
||||
totalResults: resourceTypes.length,
|
||||
itemsPerPage: resourceTypes.length,
|
||||
startIndex: 1,
|
||||
Resources: resourceTypes,
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Map HTTP status codes to SCIM error types
|
||||
*/
|
||||
export const getScimErrorTypeFromException: (
|
||||
err: Exception,
|
||||
) => SCIMErrorType | undefined = (err: Exception): SCIMErrorType | undefined => {
|
||||
const errorName: string = err.constructor.name;
|
||||
|
||||
switch (errorName) {
|
||||
case "BadRequestException":
|
||||
return SCIMErrorType.InvalidValue;
|
||||
case "NotFoundException":
|
||||
return SCIMErrorType.NoTarget;
|
||||
case "NotAuthorizedException":
|
||||
return undefined; // No specific SCIM type for auth errors
|
||||
default:
|
||||
return undefined;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Get HTTP status code from exception
|
||||
*/
|
||||
export const getHttpStatusFromException: (err: Exception) => number = (
|
||||
err: Exception,
|
||||
): number => {
|
||||
const errorName: string = err.constructor.name;
|
||||
|
||||
switch (errorName) {
|
||||
case "BadRequestException":
|
||||
return 400;
|
||||
case "NotAuthorizedException":
|
||||
return 401;
|
||||
case "PaymentRequiredException":
|
||||
return 402;
|
||||
case "NotFoundException":
|
||||
return 404;
|
||||
case "NotImplementedException":
|
||||
return 501;
|
||||
default:
|
||||
return 500;
|
||||
}
|
||||
};
|
||||
|
||||
@@ -35,11 +35,18 @@ Project SCIM allows identity providers to manage team members within OneUptime p
|
||||
### Project SCIM Endpoints
|
||||
|
||||
- **Service Provider Config**: `GET /scim/v2/{scimId}/ServiceProviderConfig`
|
||||
- **Schemas**: `GET /scim/v2/{scimId}/Schemas`
|
||||
- **Resource Types**: `GET /scim/v2/{scimId}/ResourceTypes`
|
||||
- **List Users**: `GET /scim/v2/{scimId}/Users`
|
||||
- **Get User**: `GET /scim/v2/{scimId}/Users/{userId}`
|
||||
- **Create User**: `POST /scim/v2/{scimId}/Users`
|
||||
- **Update User**: `PUT /scim/v2/{scimId}/Users/{userId}`
|
||||
- **Update User**: `PUT /scim/v2/{scimId}/Users/{userId}` or `PATCH /scim/v2/{scimId}/Users/{userId}`
|
||||
- **Delete User**: `DELETE /scim/v2/{scimId}/Users/{userId}`
|
||||
- **List Groups**: `GET /scim/v2/{scimId}/Groups`
|
||||
- **Get Group**: `GET /scim/v2/{scimId}/Groups/{groupId}`
|
||||
- **Create Group**: `POST /scim/v2/{scimId}/Groups`
|
||||
- **Update Group**: `PUT /scim/v2/{scimId}/Groups/{groupId}` or `PATCH /scim/v2/{scimId}/Groups/{groupId}`
|
||||
- **Delete Group**: `DELETE /scim/v2/{scimId}/Groups/{groupId}`
|
||||
|
||||
### Project SCIM User Lifecycle
|
||||
|
||||
@@ -74,10 +81,12 @@ Status Page SCIM allows identity providers to manage subscribers to private stat
|
||||
### Status Page SCIM Endpoints
|
||||
|
||||
- **Service Provider Config**: `GET /status-page-scim/v2/{scimId}/ServiceProviderConfig`
|
||||
- **Schemas**: `GET /status-page-scim/v2/{scimId}/Schemas`
|
||||
- **Resource Types**: `GET /status-page-scim/v2/{scimId}/ResourceTypes`
|
||||
- **List Users**: `GET /status-page-scim/v2/{scimId}/Users`
|
||||
- **Get User**: `GET /status-page-scim/v2/{scimId}/Users/{userId}`
|
||||
- **Create User**: `POST /status-page-scim/v2/{scimId}/Users`
|
||||
- **Update User**: `PUT /status-page-scim/v2/{scimId}/Users/{userId}`
|
||||
- **Update User**: `PUT /status-page-scim/v2/{scimId}/Users/{userId}` or `PATCH /status-page-scim/v2/{scimId}/Users/{userId}`
|
||||
- **Delete User**: `DELETE /status-page-scim/v2/{scimId}/Users/{userId}`
|
||||
|
||||
### Status Page SCIM User Lifecycle
|
||||
@@ -91,45 +100,317 @@ Status Page SCIM allows identity providers to manage subscribers to private stat
|
||||
|
||||
## Identity Provider Configuration
|
||||
|
||||
### Azure Active Directory (Azure AD)
|
||||
### Microsoft Entra ID (formerly Azure AD)
|
||||
|
||||
1. **Add OneUptime from Azure AD Gallery**
|
||||
- In Azure AD, go to **Enterprise Applications** > **New Application**
|
||||
- Add a **Non-gallery application**
|
||||
Microsoft Entra ID provides enterprise-grade identity management with robust SCIM provisioning capabilities. Follow these detailed steps to configure SCIM provisioning with OneUptime.
|
||||
|
||||
2. **Configure SCIM Settings**
|
||||
- In the OneUptime application, go to **Provisioning**
|
||||
- Set **Provisioning Mode** to **Automatic**
|
||||
- Enter the **Tenant URL** (SCIM Base URL from OneUptime)
|
||||
- Enter the **Secret Token** (Bearer Token from OneUptime)
|
||||
- Test the connection and save
|
||||
#### Prerequisites
|
||||
|
||||
3. **Configure Attribute Mappings**
|
||||
- Map Azure AD attributes to OneUptime SCIM attributes
|
||||
- Ensure `userPrincipalName` or `mail` is mapped to `userName`
|
||||
- Configure any additional attribute mappings as needed
|
||||
- Microsoft Entra ID tenant with Premium P1 or P2 license (required for automatic provisioning)
|
||||
- OneUptime account with Scale plan or higher
|
||||
- Admin access to both Microsoft Entra ID and OneUptime
|
||||
|
||||
4. **Assign Users**
|
||||
- Go to **Users and groups** and assign users to the OneUptime application
|
||||
- Users will be automatically provisioned to OneUptime
|
||||
#### Step 1: Get SCIM Configuration from OneUptime
|
||||
|
||||
1. Log in to your OneUptime dashboard
|
||||
2. Navigate to **Project Settings** > **Team** > **SCIM**
|
||||
3. Click **Create SCIM Configuration**
|
||||
4. Enter a friendly name (e.g., "Microsoft Entra ID Provisioning")
|
||||
5. Configure the following options:
|
||||
- **Auto Provision Users**: Enable to automatically create users
|
||||
- **Auto Deprovision Users**: Enable to automatically remove users
|
||||
- **Default Teams**: Select teams that new users should be added to
|
||||
- **Enable Push Groups**: Enable if you want to manage team membership via Entra ID groups
|
||||
6. Save the configuration
|
||||
7. Copy the **SCIM Base URL** and **Bearer Token** - you'll need these for Entra ID
|
||||
|
||||
#### Step 2: Create Enterprise Application in Microsoft Entra ID
|
||||
|
||||
1. Sign in to the [Microsoft Entra admin center](https://entra.microsoft.com)
|
||||
2. Navigate to **Identity** > **Applications** > **Enterprise applications**
|
||||
3. Click **+ New application**
|
||||
4. Click **+ Create your own application**
|
||||
5. Enter a name (e.g., "OneUptime")
|
||||
6. Select **Integrate any other application you don't find in the gallery (Non-gallery)**
|
||||
7. Click **Create**
|
||||
|
||||
#### Step 3: Configure SCIM Provisioning
|
||||
|
||||
1. In your OneUptime enterprise application, go to **Provisioning**
|
||||
2. Click **Get started**
|
||||
3. Set **Provisioning Mode** to **Automatic**
|
||||
4. Under **Admin Credentials**:
|
||||
- **Tenant URL**: Enter the SCIM Base URL from OneUptime (e.g., `https://oneuptime.com/api/identity/scim/v2/{your-scim-id}`)
|
||||
- **Secret Token**: Enter the Bearer Token from OneUptime
|
||||
5. Click **Test Connection** to verify the configuration
|
||||
6. Click **Save**
|
||||
|
||||
#### Step 4: Configure Attribute Mappings
|
||||
|
||||
1. In the Provisioning section, click **Mappings**
|
||||
2. Click **Provision Azure Active Directory Users**
|
||||
3. Configure the following attribute mappings:
|
||||
|
||||
| Azure AD Attribute | OneUptime SCIM Attribute | Required |
|
||||
|-------------------|-------------------------|----------|
|
||||
| `userPrincipalName` | `userName` | Yes |
|
||||
| `mail` | `emails[type eq "work"].value` | Recommended |
|
||||
| `displayName` | `displayName` | Recommended |
|
||||
| `givenName` | `name.givenName` | Optional |
|
||||
| `surname` | `name.familyName` | Optional |
|
||||
| `Switch([IsSoftDeleted], , "False", "True", "True", "False")` | `active` | Recommended |
|
||||
|
||||
4. Remove any mappings that are not needed to simplify provisioning
|
||||
5. Click **Save**
|
||||
|
||||
#### Step 5: Configure Group Provisioning (Optional)
|
||||
|
||||
If you enabled **Push Groups** in OneUptime:
|
||||
|
||||
1. Go back to **Mappings**
|
||||
2. Click **Provision Azure Active Directory Groups**
|
||||
3. Enable group provisioning by setting **Enabled** to **Yes**
|
||||
4. Configure the following attribute mappings:
|
||||
|
||||
| Azure AD Attribute | OneUptime SCIM Attribute |
|
||||
|-------------------|-------------------------|
|
||||
| `displayName` | `displayName` |
|
||||
| `members` | `members` |
|
||||
|
||||
5. Click **Save**
|
||||
|
||||
#### Step 6: Assign Users and Groups
|
||||
|
||||
1. In your OneUptime enterprise application, go to **Users and groups**
|
||||
2. Click **+ Add user/group**
|
||||
3. Select the users and/or groups you want to provision to OneUptime
|
||||
4. Click **Assign**
|
||||
|
||||
#### Step 7: Start Provisioning
|
||||
|
||||
1. Go to **Provisioning** > **Overview**
|
||||
2. Click **Start provisioning**
|
||||
3. The initial provisioning cycle will begin (this may take up to 40 minutes for the first sync)
|
||||
4. Monitor the **Provisioning logs** for any errors
|
||||
|
||||
#### Troubleshooting Microsoft Entra ID
|
||||
|
||||
- **Test Connection Fails**: Verify the SCIM Base URL includes `/api/identity` prefix and the Bearer Token is correct
|
||||
- **Users Not Provisioning**: Check that users are assigned to the application and attribute mappings are correct
|
||||
- **Provisioning Errors**: Review the Provisioning logs in Entra ID for specific error messages
|
||||
- **Sync Delays**: Initial provisioning can take up to 40 minutes; subsequent syncs occur every 40 minutes
|
||||
|
||||
---
|
||||
|
||||
### Okta
|
||||
|
||||
1. **SSO Application**
|
||||
- You should already have the Okta application from the SSO integration you might have completed. If you do not, then please check out SSO Readme to create a new Okta App.
|
||||
Okta provides flexible identity management with excellent SCIM support. Follow these detailed steps to configure SCIM provisioning with OneUptime.
|
||||
|
||||
2. **Configure SCIM Settings**
|
||||
- In the application settings (General Tab), go to **Provisioning**, select SAML and click on Save. **Proviosning** tab should now be enabled.
|
||||
- Set **SCIM connector base URL** to the OneUptime SCIM Base URL
|
||||
- Set **Unique identifier field for users** to `userName`
|
||||
- Enter the **Bearer Token** in the authentication header
|
||||
#### Prerequisites
|
||||
|
||||
3. **Configure Attribute Mappings**
|
||||
- Map Okta user attributes to SCIM attributes
|
||||
- Ensure `email` is mapped to `userName`
|
||||
- Configure additional mappings as needed
|
||||
- Okta tenant with provisioning capabilities (Lifecycle Management feature)
|
||||
- OneUptime account with Scale plan or higher
|
||||
- Admin access to both Okta and OneUptime
|
||||
|
||||
4. **Assign Users**
|
||||
- Assign users to the OneUptime application
|
||||
- Users will be automatically provisioned to OneUptime
|
||||
#### Step 1: Get SCIM Configuration from OneUptime
|
||||
|
||||
1. Log in to your OneUptime dashboard
|
||||
2. Navigate to **Project Settings** > **Team** > **SCIM**
|
||||
3. Click **Create SCIM Configuration**
|
||||
4. Enter a friendly name (e.g., "Okta Provisioning")
|
||||
5. Configure the following options:
|
||||
- **Auto Provision Users**: Enable to automatically create users
|
||||
- **Auto Deprovision Users**: Enable to automatically remove users
|
||||
- **Default Teams**: Select teams that new users should be added to
|
||||
- **Enable Push Groups**: Enable if you want to manage team membership via Okta groups
|
||||
6. Save the configuration
|
||||
7. Copy the **SCIM Base URL** and **Bearer Token** - you'll need these for Okta
|
||||
|
||||
#### Step 2: Create or Configure Okta Application
|
||||
|
||||
**If you have an existing SSO application:**
|
||||
1. Sign in to your Okta Admin Console
|
||||
2. Navigate to **Applications** > **Applications**
|
||||
3. Find and select your existing OneUptime application
|
||||
|
||||
**If creating a new application:**
|
||||
1. Sign in to your Okta Admin Console
|
||||
2. Navigate to **Applications** > **Applications**
|
||||
3. Click **Create App Integration**
|
||||
4. Select **SAML 2.0** and click **Next**
|
||||
5. Enter "OneUptime" as the App name
|
||||
6. Complete the SAML configuration (refer to SSO documentation)
|
||||
7. Click **Finish**
|
||||
|
||||
#### Step 3: Enable SCIM Provisioning
|
||||
|
||||
1. In your OneUptime application, go to the **General** tab
|
||||
2. In the **App Settings** section, click **Edit**
|
||||
3. Under **Provisioning**, select **SCIM**
|
||||
4. Click **Save**
|
||||
5. A new **Provisioning** tab will appear
|
||||
|
||||
#### Step 4: Configure SCIM Connection
|
||||
|
||||
1. Go to the **Provisioning** tab
|
||||
2. Click **Integration** in the left sidebar
|
||||
3. Click **Configure API Integration**
|
||||
4. Check **Enable API integration**
|
||||
5. Configure the following:
|
||||
- **SCIM connector base URL**: Enter the SCIM Base URL from OneUptime (e.g., `https://oneuptime.com/api/identity/scim/v2/{your-scim-id}`)
|
||||
- **Unique identifier field for users**: Enter `userName`
|
||||
- **Supported provisioning actions**: Select the actions you want to enable:
|
||||
- Import New Users and Profile Updates
|
||||
- Push New Users
|
||||
- Push Profile Updates
|
||||
- Push Groups (if using group-based provisioning)
|
||||
- **Authentication Mode**: Select **HTTP Header**
|
||||
- **Authorization**: Enter `Bearer {your-bearer-token}` (replace with actual token)
|
||||
6. Click **Test API Credentials** to verify the connection
|
||||
7. Click **Save**
|
||||
|
||||
#### Step 5: Configure Provisioning to App
|
||||
|
||||
1. In the **Provisioning** tab, click **To App** in the left sidebar
|
||||
2. Click **Edit**
|
||||
3. Enable the following options:
|
||||
- **Create Users**: Enable to provision new users
|
||||
- **Update User Attributes**: Enable to sync attribute changes
|
||||
- **Deactivate Users**: Enable to deprovision users when unassigned
|
||||
4. Click **Save**
|
||||
|
||||
#### Step 6: Configure Attribute Mappings
|
||||
|
||||
1. Scroll down to **Attribute Mappings**
|
||||
2. Verify or configure the following mappings:
|
||||
|
||||
| Okta Attribute | OneUptime SCIM Attribute | Direction |
|
||||
|---------------|-------------------------|-----------|
|
||||
| `userName` | `userName` | Okta to App |
|
||||
| `user.email` | `emails[primary eq true].value` | Okta to App |
|
||||
| `user.firstName` | `name.givenName` | Okta to App |
|
||||
| `user.lastName` | `name.familyName` | Okta to App |
|
||||
| `user.displayName` | `displayName` | Okta to App |
|
||||
|
||||
3. Remove any unnecessary mappings
|
||||
4. Click **Save** if you made changes
|
||||
|
||||
#### Step 7: Configure Push Groups (Optional)
|
||||
|
||||
If you enabled **Push Groups** in OneUptime:
|
||||
|
||||
1. Go to the **Push Groups** tab
|
||||
2. Click **+ Push Groups**
|
||||
3. Select **Find groups by name** or **Find groups by rule**
|
||||
4. Search for and select the groups you want to push
|
||||
5. Click **Save**
|
||||
|
||||
#### Step 8: Assign Users
|
||||
|
||||
1. Go to the **Assignments** tab
|
||||
2. Click **Assign** > **Assign to People** or **Assign to Groups**
|
||||
3. Select the users or groups you want to provision
|
||||
4. Click **Assign** for each selection
|
||||
5. Click **Done**
|
||||
|
||||
#### Step 9: Verify Provisioning
|
||||
|
||||
1. Go to **Reports** > **System Log** in the Okta Admin Console
|
||||
2. Filter for events related to your OneUptime application
|
||||
3. Verify that provisioning events are successful
|
||||
4. Check OneUptime to confirm users have been created
|
||||
|
||||
#### Troubleshooting Okta
|
||||
|
||||
- **API Credentials Test Fails**: Verify the SCIM Base URL and Bearer Token are correct
|
||||
- **Users Not Provisioning**: Ensure users are assigned to the application and provisioning is enabled
|
||||
- **Duplicate Users**: Ensure the `userName` attribute is unique and maps correctly to email
|
||||
- **Group Push Failures**: Verify groups exist and have the correct membership
|
||||
- **Error: 401 Unauthorized**: Regenerate the Bearer Token in OneUptime and update Okta
|
||||
|
||||
---
|
||||
|
||||
### Other Identity Providers
|
||||
|
||||
OneUptime's SCIM implementation follows the SCIM v2.0 specification and should work with any compliant identity provider. General configuration steps:
|
||||
|
||||
1. **SCIM Base URL**: `https://oneuptime.com/api/identity/scim/v2/{scim-id}` (for projects) or `https://oneuptime.com/api/identity/status-page-scim/v2/{scim-id}` (for status pages)
|
||||
2. **Authentication**: HTTP Bearer Token
|
||||
3. **Required User Attribute**: `userName` (must be a valid email address)
|
||||
4. **Supported Operations**: GET, POST, PUT, PATCH, DELETE for Users and Groups
|
||||
|
||||
#### Supported SCIM Endpoints
|
||||
|
||||
| Endpoint | Methods | Description |
|
||||
|----------|---------|-------------|
|
||||
| `/ServiceProviderConfig` | GET | SCIM server capabilities |
|
||||
| `/Schemas` | GET | Available resource schemas |
|
||||
| `/ResourceTypes` | GET | Available resource types |
|
||||
| `/Users` | GET, POST | List and create users |
|
||||
| `/Users/{id}` | GET, PUT, PATCH, DELETE | Manage individual users |
|
||||
| `/Groups` | GET, POST | List and create groups/teams (Project SCIM only) |
|
||||
| `/Groups/{id}` | GET, PUT, PATCH, DELETE | Manage individual groups (Project SCIM only) |
|
||||
|
||||
#### SCIM User Schema
|
||||
|
||||
```json
|
||||
{
|
||||
"schemas": ["urn:ietf:params:scim:schemas:core:2.0:User"],
|
||||
"userName": "user@example.com",
|
||||
"name": {
|
||||
"givenName": "John",
|
||||
"familyName": "Doe",
|
||||
"formatted": "John Doe"
|
||||
},
|
||||
"displayName": "John Doe",
|
||||
"emails": [
|
||||
{
|
||||
"value": "user@example.com",
|
||||
"type": "work",
|
||||
"primary": true
|
||||
}
|
||||
],
|
||||
"active": true
|
||||
}
|
||||
```
|
||||
|
||||
#### SCIM Group Schema
|
||||
|
||||
```json
|
||||
{
|
||||
"schemas": ["urn:ietf:params:scim:schemas:core:2.0:Group"],
|
||||
"displayName": "Engineering Team",
|
||||
"members": [
|
||||
{
|
||||
"value": "user-id-here",
|
||||
"display": "user@example.com"
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
## Frequently Asked Questions
|
||||
|
||||
### What happens when a user is deprovisioned?
|
||||
|
||||
When a user is deprovisioned (either by DELETE request or by setting `active: false`), they are removed from the teams configured in the SCIM settings. The user account itself remains in OneUptime but loses access to the project.
|
||||
|
||||
### Can I use SCIM without SSO?
|
||||
|
||||
Yes, SCIM and SSO are independent features. You can use SCIM for user provisioning while allowing users to log in with their OneUptime passwords or any other authentication method.
|
||||
|
||||
### How do I handle users who already exist in OneUptime?
|
||||
|
||||
When SCIM tries to create a user who already exists (matching by email), OneUptime will simply add them to the configured default teams rather than creating a duplicate user.
|
||||
|
||||
### What is the difference between default teams and push groups?
|
||||
|
||||
- **Default Teams**: All users provisioned via SCIM are added to the same predefined teams
|
||||
- **Push Groups**: Team membership is managed by your identity provider, allowing different users to be in different teams based on IdP group membership
|
||||
|
||||
### How often does provisioning sync occur?
|
||||
|
||||
This depends on your identity provider:
|
||||
- **Microsoft Entra ID**: Initial sync can take up to 40 minutes, subsequent syncs every 40 minutes
|
||||
- **Okta**: Near real-time for most operations, with periodic full syncs
|
||||
|
||||
|
||||
Reference in New Issue
Block a user