mirror of
https://github.com/OneUptime/oneuptime.git
synced 2026-04-06 00:32:12 +02:00
feat: update SCIM integration to manage team members with Push Groups
This commit is contained in:
@@ -44,10 +44,13 @@ export class TeamMemberService extends DatabaseService<TeamMember> {
|
||||
}
|
||||
|
||||
@CaptureSpan()
|
||||
private async isSCIMEnabled(projectId: ObjectID): Promise<boolean> {
|
||||
private async isSCIMPushGroupsEnabled(
|
||||
projectId: ObjectID,
|
||||
): Promise<boolean> {
|
||||
const count: PositiveNumber = await ProjectSCIMService.countBy({
|
||||
query: {
|
||||
projectId: projectId,
|
||||
enablePushGroups: true,
|
||||
},
|
||||
props: {
|
||||
isRoot: true,
|
||||
@@ -63,12 +66,12 @@ export class TeamMemberService extends DatabaseService<TeamMember> {
|
||||
// Check if SCIM is enabled for the project
|
||||
if (
|
||||
!createBy.props.isRoot &&
|
||||
(await this.isSCIMEnabled(
|
||||
(await this.isSCIMPushGroupsEnabled(
|
||||
createBy.data.projectId! || createBy.props.tenantId,
|
||||
))
|
||||
) {
|
||||
throw new BadDataException(
|
||||
"Cannot invite team members when SCIM is enabled for this project.",
|
||||
"Cannot invite team members while SCIM Push Groups is enabled for this project. Disable Push Groups to manage members from OneUptime.",
|
||||
);
|
||||
}
|
||||
|
||||
@@ -311,10 +314,10 @@ export class TeamMemberService extends DatabaseService<TeamMember> {
|
||||
!deleteBy.props.isRoot &&
|
||||
members.length > 0 &&
|
||||
members[0]?.projectId &&
|
||||
(await this.isSCIMEnabled(members[0].projectId))
|
||||
(await this.isSCIMPushGroupsEnabled(members[0].projectId))
|
||||
) {
|
||||
throw new BadDataException(
|
||||
"Cannot delete team members when SCIM is enabled for this project.",
|
||||
"Cannot delete team members while SCIM Push Groups is enabled for this project. Disable Push Groups to manage members from OneUptime.",
|
||||
);
|
||||
}
|
||||
|
||||
@@ -346,11 +349,10 @@ export class TeamMemberService extends DatabaseService<TeamMember> {
|
||||
});
|
||||
|
||||
// Skip the one-member guard when SCIM manages membership for the project.
|
||||
const isSCIMEnabled: boolean = await this.isSCIMEnabled(
|
||||
member.projectId!,
|
||||
);
|
||||
const isPushGroupsManaged: boolean =
|
||||
await this.isSCIMPushGroupsEnabled(member.projectId!);
|
||||
|
||||
if (!isSCIMEnabled && membersInTeam.toNumber() <= 1) {
|
||||
if (!isPushGroupsManaged && membersInTeam.toNumber() <= 1) {
|
||||
throw new BadDataException(
|
||||
Errors.TeamMemberService.ONE_MEMBER_REQUIRED,
|
||||
);
|
||||
|
||||
@@ -71,6 +71,7 @@ export class Service extends DatabaseService<Model> {
|
||||
const scimCount: PositiveNumber = await ProjectSCIMService.countBy({
|
||||
query: {
|
||||
projectId: projectId,
|
||||
enablePushGroups: true,
|
||||
},
|
||||
skip: new PositiveNumber(0),
|
||||
limit: new PositiveNumber(1),
|
||||
@@ -82,7 +83,7 @@ export class Service extends DatabaseService<Model> {
|
||||
|
||||
if (scimCount.toNumber() > 0) {
|
||||
throw new BadDataException(
|
||||
`Cannot ${data.action} teams when SCIM is enabled for this project.`,
|
||||
`Cannot ${data.action} teams while SCIM Push Groups is enabled for this project. Disable Push Groups to manage teams from OneUptime.`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@ import MailService from "../../../Server/Services/MailService";
|
||||
import TeamMemberService from "../../../Server/Services/TeamMemberService";
|
||||
import UserNotificationRuleService from "../../../Server/Services/UserNotificationRuleService";
|
||||
import UserNotificationSettingService from "../../../Server/Services/UserNotificationSettingService";
|
||||
import ProjectSCIMService from "../../../Server/Services/ProjectSCIMService";
|
||||
import Errors from "../../../Server/Utils/Errors";
|
||||
import "../TestingUtils/Init";
|
||||
import ProjectServiceHelper from "../TestingUtils/Services/ProjectServiceHelper";
|
||||
@@ -334,6 +335,119 @@ describe("TeamMemberService", () => {
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
it("should block inviting users when SCIM push groups is enabled", async () => {
|
||||
const owner: User = await UserServiceHelper.genrateAndSaveRandomUser(
|
||||
null,
|
||||
{
|
||||
isRoot: true,
|
||||
},
|
||||
);
|
||||
|
||||
const project: Project =
|
||||
await ProjectServiceHelper.generateAndSaveRandomProject(null, {
|
||||
isRoot: true,
|
||||
userId: owner.id!,
|
||||
});
|
||||
|
||||
const team: Team = await TeamServiceHelper.generateAndSaveRandomTeam(
|
||||
{
|
||||
projectId: new ObjectID(project.id!),
|
||||
},
|
||||
{
|
||||
isRoot: true,
|
||||
},
|
||||
);
|
||||
|
||||
const memberUser: User =
|
||||
await UserServiceHelper.genrateAndSaveRandomUser(null, {
|
||||
isRoot: true,
|
||||
});
|
||||
|
||||
await ProjectSCIMService.create({
|
||||
data: {
|
||||
projectId: new ObjectID(project._id!),
|
||||
name: "Test SCIM Push Groups",
|
||||
bearerToken: ObjectID.generate().toString(),
|
||||
enablePushGroups: true,
|
||||
},
|
||||
props: {
|
||||
isRoot: true,
|
||||
},
|
||||
});
|
||||
|
||||
const tm: TeamMember = TeamMemberServiceHelper.generateRandomTeamMember(
|
||||
{
|
||||
projectId: new ObjectID(project._id!),
|
||||
userId: new ObjectID(memberUser._id!),
|
||||
teamId: new ObjectID(team._id!),
|
||||
},
|
||||
);
|
||||
|
||||
await expect(
|
||||
TeamMemberService.create({
|
||||
data: tm,
|
||||
props: { isRoot: false, tenantId: project.id! },
|
||||
}),
|
||||
).rejects.toThrow(/SCIM Push Groups/i);
|
||||
});
|
||||
|
||||
it("should allow inviting users when SCIM push groups is disabled", async () => {
|
||||
const owner: User = await UserServiceHelper.genrateAndSaveRandomUser(
|
||||
null,
|
||||
{
|
||||
isRoot: true,
|
||||
},
|
||||
);
|
||||
|
||||
const project: Project =
|
||||
await ProjectServiceHelper.generateAndSaveRandomProject(null, {
|
||||
isRoot: true,
|
||||
userId: owner.id!,
|
||||
});
|
||||
|
||||
const team: Team = await TeamServiceHelper.generateAndSaveRandomTeam(
|
||||
{
|
||||
projectId: new ObjectID(project.id!),
|
||||
},
|
||||
{
|
||||
isRoot: true,
|
||||
},
|
||||
);
|
||||
|
||||
const memberUser: User =
|
||||
await UserServiceHelper.genrateAndSaveRandomUser(null, {
|
||||
isRoot: true,
|
||||
});
|
||||
|
||||
await ProjectSCIMService.create({
|
||||
data: {
|
||||
projectId: new ObjectID(project._id!),
|
||||
name: "Test SCIM without Push Groups",
|
||||
bearerToken: ObjectID.generate().toString(),
|
||||
enablePushGroups: false,
|
||||
},
|
||||
props: {
|
||||
isRoot: true,
|
||||
},
|
||||
});
|
||||
|
||||
const tm: TeamMember = TeamMemberServiceHelper.generateRandomTeamMember(
|
||||
{
|
||||
projectId: new ObjectID(project._id!),
|
||||
userId: new ObjectID(memberUser._id!),
|
||||
teamId: new ObjectID(team._id!),
|
||||
},
|
||||
);
|
||||
|
||||
const teamMember: TeamMember = await TeamMemberService.create({
|
||||
data: tm,
|
||||
props: { isRoot: false, tenantId: project.id! },
|
||||
});
|
||||
|
||||
expect(teamMember).toBeDefined();
|
||||
expect(teamMember.projectId?.toString()).toEqual(project._id?.toString());
|
||||
});
|
||||
});
|
||||
|
||||
describe("onCreateSuccess", () => {
|
||||
|
||||
@@ -53,7 +53,8 @@ const TeamView: FunctionComponent<PageComponentProps> = (
|
||||
const complianceStatusTableRef: React.Ref<TeamComplianceStatusTableRef> =
|
||||
React.useRef<TeamComplianceStatusTableRef>(null);
|
||||
|
||||
const [isScimEnabled, setIsScimEnabled] = React.useState<boolean>(false);
|
||||
const [isPushGroupsManaged, setIsPushGroupsManaged] =
|
||||
React.useState<boolean>(false);
|
||||
|
||||
React.useEffect(() => {
|
||||
const checkScim: () => Promise<void> = async () => {
|
||||
@@ -65,9 +66,10 @@ const TeamView: FunctionComponent<PageComponentProps> = (
|
||||
modelType: ProjectSCIM,
|
||||
query: {
|
||||
projectId: props.currentProject._id,
|
||||
enablePushGroups: true,
|
||||
},
|
||||
});
|
||||
setIsScimEnabled(scimCount > 0);
|
||||
setIsPushGroupsManaged(scimCount > 0);
|
||||
} catch {
|
||||
// ignore
|
||||
}
|
||||
@@ -327,23 +329,30 @@ const TeamView: FunctionComponent<PageComponentProps> = (
|
||||
|
||||
{/* Team Members Table */}
|
||||
|
||||
{isPushGroupsManaged && (
|
||||
<Banner
|
||||
title="Team membership is managed by SCIM Push Groups"
|
||||
description="Manage team members from your identity provider or disable Push Groups in Settings > SCIM to make changes here."
|
||||
/>
|
||||
)}
|
||||
|
||||
<ModelTable<TeamMember>
|
||||
modelType={TeamMember}
|
||||
id="table-team-member"
|
||||
userPreferencesKey="team-member-table"
|
||||
isDeleteable={true}
|
||||
isDeleteable={!isPushGroupsManaged}
|
||||
name="Settings > Team > Member"
|
||||
createVerb={"Invite"}
|
||||
isCreateable={true}
|
||||
isCreateable={!isPushGroupsManaged}
|
||||
isViewable={false}
|
||||
query={{
|
||||
teamId: modelId,
|
||||
projectId: ProjectUtil.getCurrentProjectId()!,
|
||||
}}
|
||||
onBeforeCreate={(item: TeamMember): Promise<TeamMember> => {
|
||||
if (isScimEnabled) {
|
||||
if (isPushGroupsManaged) {
|
||||
throw new BadDataException(
|
||||
"Cannot invite users when SCIM is enabled for this project.",
|
||||
"Cannot invite users while SCIM Push Groups is enabled for this project. Disable Push Groups to manage members from OneUptime.",
|
||||
);
|
||||
}
|
||||
if (!props.currentProject || !props.currentProject._id) {
|
||||
@@ -354,9 +363,9 @@ const TeamView: FunctionComponent<PageComponentProps> = (
|
||||
return Promise.resolve(item);
|
||||
}}
|
||||
onBeforeDelete={async (item: TeamMember): Promise<TeamMember> => {
|
||||
if (isScimEnabled) {
|
||||
if (isPushGroupsManaged) {
|
||||
throw new BadDataException(
|
||||
"Cannot remove team members when SCIM is enabled for this project.",
|
||||
"Cannot remove team members while SCIM Push Groups is enabled for this project. Disable Push Groups to manage members from OneUptime.",
|
||||
);
|
||||
}
|
||||
return item;
|
||||
|
||||
@@ -21,6 +21,7 @@ import BadDataException from "Common/Types/Exception/BadDataException";
|
||||
import ModelAPI from "Common/UI/Utils/ModelAPI/ModelAPI";
|
||||
import ProjectSCIM from "Common/Models/DatabaseModels/ProjectSCIM";
|
||||
import ConfirmModal from "Common/UI/Components/Modal/ConfirmModal";
|
||||
import Banner from "Common/UI/Components/Banner/Banner";
|
||||
|
||||
const Teams: FunctionComponent<PageComponentProps> = (
|
||||
props: PageComponentProps,
|
||||
@@ -30,7 +31,8 @@ const Teams: FunctionComponent<PageComponentProps> = (
|
||||
const [showScimErrorModal, setShowScimErrorModal] =
|
||||
React.useState<boolean>(false);
|
||||
const [isFilterApplied, setIsFilterApplied] = React.useState<boolean>(false);
|
||||
const [isScimEnabled, setIsScimEnabled] = React.useState<boolean>(false);
|
||||
const [isPushGroupsManaged, setIsPushGroupsManaged] =
|
||||
React.useState<boolean>(false);
|
||||
|
||||
React.useEffect(() => {
|
||||
const checkScim: () => Promise<void> = async () => {
|
||||
@@ -42,9 +44,10 @@ const Teams: FunctionComponent<PageComponentProps> = (
|
||||
modelType: ProjectSCIM,
|
||||
query: {
|
||||
projectId: props.currentProject._id,
|
||||
enablePushGroups: true,
|
||||
},
|
||||
});
|
||||
setIsScimEnabled(scimCount > 0);
|
||||
setIsPushGroupsManaged(scimCount > 0);
|
||||
} catch {
|
||||
// ignore
|
||||
}
|
||||
@@ -54,12 +57,19 @@ const Teams: FunctionComponent<PageComponentProps> = (
|
||||
|
||||
return (
|
||||
<Fragment>
|
||||
{isPushGroupsManaged && (
|
||||
<Banner
|
||||
title="Users are managed by SCIM Push Groups"
|
||||
description="Invite or remove users from your identity provider or disable Push Groups in Settings > SCIM to manage them here."
|
||||
/>
|
||||
)}
|
||||
|
||||
<ModelTable<TeamMember>
|
||||
modelType={TeamMember}
|
||||
id="teams-table"
|
||||
name="Settings > Users"
|
||||
userPreferencesKey="users-table"
|
||||
isDeleteable={!isScimEnabled}
|
||||
isDeleteable={!isPushGroupsManaged}
|
||||
isEditable={false}
|
||||
isCreateable={false}
|
||||
onFilterApplied={(isApplied: boolean) => {
|
||||
@@ -67,9 +77,9 @@ const Teams: FunctionComponent<PageComponentProps> = (
|
||||
}}
|
||||
isViewable={true}
|
||||
onBeforeDelete={async (item: TeamMember): Promise<TeamMember> => {
|
||||
if (isScimEnabled) {
|
||||
if (isPushGroupsManaged) {
|
||||
throw new BadDataException(
|
||||
"Cannot remove team members when SCIM is enabled for this project.",
|
||||
"Cannot remove team members while SCIM Push Groups is enabled for this project. Disable Push Groups to manage members from OneUptime.",
|
||||
);
|
||||
}
|
||||
return item;
|
||||
@@ -84,7 +94,7 @@ const Teams: FunctionComponent<PageComponentProps> = (
|
||||
buttonStyle: ButtonStyleType.NORMAL,
|
||||
icon: IconProp.Add,
|
||||
onClick: () => {
|
||||
if (isScimEnabled) {
|
||||
if (isPushGroupsManaged) {
|
||||
setShowScimErrorModal(true);
|
||||
} else {
|
||||
setShowInviteUserModal(true);
|
||||
@@ -185,7 +195,7 @@ const Teams: FunctionComponent<PageComponentProps> = (
|
||||
},
|
||||
]}
|
||||
/>
|
||||
{showInviteUserModal && !isScimEnabled && (
|
||||
{showInviteUserModal && !isPushGroupsManaged && (
|
||||
<ModelFormModal<TeamMember>
|
||||
modelType={TeamMember}
|
||||
name="Invite New User"
|
||||
@@ -250,8 +260,8 @@ const Teams: FunctionComponent<PageComponentProps> = (
|
||||
)}
|
||||
{showScimErrorModal && (
|
||||
<ConfirmModal
|
||||
title="Users are managed by SCIM"
|
||||
description="Cannot invite users when SCIM is enabled for this project. User management is handled by your identity provider."
|
||||
title="Users are managed by SCIM Push Groups"
|
||||
description="Team membership is being managed by your identity provider. Disable Push Groups in Settings > SCIM if you need to invite or promote users from OneUptime."
|
||||
onSubmit={() => {
|
||||
setShowScimErrorModal(false);
|
||||
}}
|
||||
|
||||
Reference in New Issue
Block a user