Skip to content

Commit b764caa

Browse files
initial changes to show elevated roles
1 parent 75af6d6 commit b764caa

File tree

5 files changed

+91
-9
lines changed

5 files changed

+91
-9
lines changed

src/api/routers/managementRouter.ts

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,10 @@
11
import express, { Response } from 'express';
22
import { z } from 'zod';
33

4+
import { getKcAdminClient } from '../keycloakAdminClient';
45
import { isSuperUserCheck } from '../middleware/userRoleMiddleware';
56
import { GetUserAuditTrail } from '../services/auditTrailService';
7+
import { getElevatedRoleByEmail } from '../services/kcUsersService';
68
import { getAllUsersList, getUserById, updateUserLock } from '../services/managementService';
79
import { getUserParticipants, ParticipantRequest } from '../services/participantsService';
810

@@ -20,7 +22,22 @@ const handleGetUserAuditTrail = async (req: ParticipantRequest, res: Response) =
2022
const handleGetUserParticipants = async (req: ParticipantRequest, res: Response) => {
2123
const { userId } = z.object({ userId: z.coerce.number() }).parse(req.params);
2224
const participants = await getUserParticipants(userId);
23-
return res.status(200).json(participants ?? []);
25+
const list = participants ?? [];
26+
27+
let elevatedRole: 'SuperUser' | 'UID2 Support' | null = null;
28+
if (list.length === 0) {
29+
const user = await getUserById(userId);
30+
if (user?.email) {
31+
try {
32+
const kcAdminClient = await getKcAdminClient();
33+
elevatedRole = await getElevatedRoleByEmail(kcAdminClient, user.email);
34+
} catch {
35+
// Keycloak unavailable or user not found; keep elevatedRole null
36+
}
37+
}
38+
}
39+
40+
return res.status(200).json({ participants: list, elevatedRole });
2441
};
2542

2643
const handleChangeUserLock = async (req: ParticipantRequest, res: Response) => {

src/api/services/kcUsersService.ts

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,42 @@ import { SSP_KK_API_CLIENT_ID, SSP_KK_SSL_RESOURCE, SSP_WEB_BASE_URL } from '../
66

77
export const API_PARTICIPANT_MEMBER_ROLE_NAME = 'api-participant-member';
88

9+
// Same group names as userRoleMiddleware (from JWT payload / Keycloak attributes.groups)
10+
const developerElevatedRole = 'developer-elevated';
11+
const developerRole = 'developer';
12+
const uid2SupportRole = 'prod-uid2.0-support';
13+
14+
export type ElevatedRole = 'SuperUser' | 'UID2 Support';
15+
16+
/**
17+
* Resolves elevated role from Keycloak user attributes (key "groups"), not realm Groups.
18+
* Used when the viewed user has no portal participants but may have SuperUser/UID2 Support in IdP.
19+
*/
20+
export const getElevatedRoleByEmail = async (
21+
kcAdminClient: KeycloakAdminClient,
22+
email: string
23+
): Promise<ElevatedRole | null> => {
24+
const users = await queryKeycloakUsersByEmail(kcAdminClient, email);
25+
if (!users.length) return null;
26+
27+
const attrs = users[0].attributes;
28+
const groupsRaw = attrs?.groups;
29+
const groups: string[] = Array.isArray(groupsRaw)
30+
? groupsRaw
31+
: typeof groupsRaw === 'string'
32+
? [groupsRaw]
33+
: [];
34+
35+
if (groups.includes(developerElevatedRole)) return 'SuperUser';
36+
if (
37+
groups.includes(developerRole) ||
38+
groups.includes(uid2SupportRole)
39+
) {
40+
return 'UID2 Support';
41+
}
42+
return null;
43+
};
44+
945
export const queryKeycloakUsersByEmail = async (
1046
kcAdminClient: KeycloakAdminClient,
1147
email: string

src/web/components/UserManagement/UserPartcipantsTable.tsx

Lines changed: 19 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { ParticipantDTO } from '../../../api/entities/Participant';
22
import { UserDTO } from '../../../api/entities/User';
33
import { UserRoleId } from '../../../api/entities/UserRole';
4+
import { ElevatedRole } from '../../services/participant';
45
import { SortableProvider } from '../../contexts/SortableTableProvider';
56
import { TableNoDataPlaceholder } from '../Core/Tables/TableNoDataPlaceholder';
67

@@ -20,12 +21,27 @@ function UserParticipantRow({ participantName, roleName }: UserParticipantRowPro
2021
);
2122
}
2223

23-
type UserParticipantsTableProps = Readonly<{
24+
export type UserParticipantsTableProps = Readonly<{
2425
user: UserDTO;
2526
userParticipants: ParticipantDTO[];
27+
elevatedRole?: ElevatedRole | null;
2628
}>;
2729

28-
function UserParticipantsTableComponent({ user, userParticipants }: UserParticipantsTableProps) {
30+
function getEmptyParticipantsMessage(elevatedRole: ElevatedRole | null | undefined): string {
31+
if (elevatedRole === 'SuperUser') {
32+
return 'This user has SuperUser role and has access to all participants.';
33+
}
34+
if (elevatedRole === 'UID2 Support') {
35+
return 'This user has UID2 Support role and has access to all participants.';
36+
}
37+
return 'This user does not belong to any participant.';
38+
}
39+
40+
function UserParticipantsTableComponent({
41+
user,
42+
userParticipants,
43+
elevatedRole,
44+
}: UserParticipantsTableProps) {
2945
return (
3046
<div className='users-participants-table-container'>
3147
<table className='users-participants-table'>
@@ -52,7 +68,7 @@ function UserParticipantsTableComponent({ user, userParticipants }: UserParticip
5268
icon={<img src='/document.svg' alt='email-icon' />}
5369
title='No Participants'
5470
>
55-
<span>This user does not belong to any participant.</span>
71+
<span>{getEmptyParticipantsMessage(elevatedRole)}</span>
5672
</TableNoDataPlaceholder>
5773
)}
5874
</div>

src/web/components/UserManagement/UserParticipantsDialog.tsx

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import { useEffect, useState } from 'react';
22

33
import { ParticipantDTO } from '../../../api/entities/Participant';
44
import { UserDTO } from '../../../api/entities/User';
5-
import { GetUserParticipants } from '../../services/participant';
5+
import { ElevatedRole, GetUserParticipants } from '../../services/participant';
66
import { Dialog } from '../Core/Dialog/Dialog';
77
import { Loading } from '../Core/Loading/Loading';
88
import UserParticipantsTable from './UserPartcipantsTable';
@@ -14,12 +14,14 @@ type UserParticipantsDialogProps = Readonly<{
1414

1515
function UserParticipantsDialog({ user, onOpenChange }: UserParticipantsDialogProps) {
1616
const [userParticipants, setUserParticipants] = useState<ParticipantDTO[]>();
17+
const [elevatedRole, setElevatedRole] = useState<ElevatedRole | null>(null);
1718
const [isLoading, setIsLoading] = useState<boolean>(true);
1819

1920
useEffect(() => {
2021
const getParticipants = async () => {
21-
const participants = await GetUserParticipants(user.id);
22+
const { participants, elevatedRole: role } = await GetUserParticipants(user.id);
2223
setUserParticipants(participants);
24+
setElevatedRole(role);
2325
setIsLoading(false);
2426
};
2527
getParticipants();
@@ -35,7 +37,11 @@ function UserParticipantsDialog({ user, onOpenChange }: UserParticipantsDialogPr
3537
<Loading message='Loading participants...' />
3638
) : (
3739
<div>
38-
<UserParticipantsTable user={user} userParticipants={userParticipants ?? []} />
40+
<UserParticipantsTable
41+
user={user}
42+
userParticipants={userParticipants ?? []}
43+
elevatedRole={elevatedRole}
44+
/>
3945
</div>
4046
)}
4147
</Dialog>

src/web/services/participant.ts

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -44,9 +44,16 @@ export async function GetAllParticipants() {
4444
}
4545
}
4646

47-
export async function GetUserParticipants(userId: number) {
47+
export type ElevatedRole = 'SuperUser' | 'UID2 Support';
48+
49+
export type GetUserParticipantsResponse = {
50+
participants: ParticipantDTO[];
51+
elevatedRole: ElevatedRole | null;
52+
};
53+
54+
export async function GetUserParticipants(userId: number): Promise<GetUserParticipantsResponse> {
4855
try {
49-
const result = await axios.get<ParticipantDTO[]>(`/manage/${userId}/participants`, {
56+
const result = await axios.get<GetUserParticipantsResponse>(`/manage/${userId}/participants`, {
5057
validateStatus: (status) => status === 200,
5158
});
5259
return result.data;

0 commit comments

Comments
 (0)