Skip to content

Commit 1d1b93e

Browse files
committed
WIP for #649
1 parent 4527213 commit 1d1b93e

File tree

8 files changed

+81
-10
lines changed

8 files changed

+81
-10
lines changed

client/src/api/index.js

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -95,6 +95,10 @@ export function other(id) {
9595
return fetchJson(`/api/v1/users/other/${id}`, {}, {}, false);
9696
}
9797

98+
export function institutionAdminsbyRole(roleId) {
99+
return fetchJson(`/api/v1/users/institution-admins/${roleId}`, {}, {}, false);
100+
}
101+
98102
export function searchUsers(pagination = {}) {
99103
const queryPart = paginationQueryParams(pagination, {})
100104
return fetchJson(`/api/v1/users/search?${queryPart}`);

client/src/locale/en.js

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -134,7 +134,9 @@ const en = {
134134
userInfo: "{{nbr}} member(s) & invalid {{period}}",
135135
roleInfo: "Role valid for <strong>{{days}} days</strong>",
136136
roleInfoNoEndDate: "Role has <strong>no end date</strong>",
137-
contactAdmin: "Contact role manager(s)"
137+
contactAdmin: "Contact role manager(s)",
138+
institutionAdmin: "Institution admins: {{names}}",
139+
noInstitutionAdmin: "There is not institution admins"
138140
},
139141
roles: {
140142
title: "Access Roles",

client/src/locale/nl.js

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -134,7 +134,10 @@ const nl = {
134134
userInfo: "{{nbr}} leden & verloopt {{period}}",
135135
roleInfo: "Rol geldig voor <strong>{{days}} dagen</strong>",
136136
roleInfoNoEndDate: "Rol heeft <strong>geen einddatum</strong>",
137-
contactAdmin: "Contact rolmanager(s)"
137+
contactAdmin: "Contact rolmanager(s)",
138+
institutionAdmin: "Institution admins: {{names}}",
139+
noInstitutionAdmin: "There is not institution admins"
140+
138141
},
139142
roles: {
140143
title: "Toegangsrollen",

client/src/locale/pt.js

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -134,7 +134,9 @@ const pt = {
134134
userInfo: "{{nbr}} member(s) & invalid {{period}}",
135135
roleInfo: "Role valid for <strong>{{days}} days</strong>",
136136
roleInfoNoEndDate: "Role has <strong>no end date</strong>",
137-
contactAdmin: "Contact role manager(s)"
137+
contactAdmin: "Contact role manager(s)",
138+
institutionAdmin: "Institution admins: {{names}}",
139+
noInstitutionAdmin: "There is not institution admins"
138140
},
139141
roles: {
140142
title: "Access Roles",

client/src/pages/Role.jsx

Lines changed: 19 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import React, {useEffect, useState} from "react";
2-
import {managersByRoleId, roleByID} from "../api";
2+
import {institutionAdminsbyRole, managersByRoleId, roleByID} from "../api";
33
import I18n from "../locale/I18n";
44
import "./Role.scss";
55
import {ButtonType, Loader, Tooltip} from "@surfnet/sds";
@@ -8,6 +8,7 @@ import {useAppStore} from "../stores/AppStore";
88
import {UnitHeader} from "../components/UnitHeader";
99
import WebsiteIcon from "../icons/network-information.svg";
1010
import PersonIcon from "../icons/persons.svg";
11+
import InstitutionAdminIcon from "@surfnet/sds/icons/illustrative-icons/presentation-amphitheater.svg";
1112
import {allowedToEditRole, AUTHORITIES, highestAuthority, isUserAllowed, urnFromRole} from "../utils/UserRole";
1213
import Tabs from "../components/Tabs";
1314
import {Page} from "../components/Page";
@@ -17,7 +18,7 @@ import ClipBoardCopy from "../components/ClipBoardCopy";
1718
import {deriveApplicationAttributes} from "../utils/Manage";
1819
import DOMPurify from "dompurify";
1920
import {UnitHeaderInviter} from "../components/UnitHeaderInviter";
20-
import {isEmpty} from "../utils/Utils";
21+
import {isEmpty, splitListSemantically} from "../utils/Utils";
2122
import {displayExpiryDate, futureDate} from "../utils/Date";
2223

2324
export const Role = () => {
@@ -30,6 +31,7 @@ export const Role = () => {
3031
const [currentTab, setCurrentTab] = useState(tab);
3132
const [tabs, setTabs] = useState([]);
3233
const [managerEmails, setManagerEmails] = useState([]);
34+
const [institutionAdmins, setInstitutionAdmins] = useState([]);
3335

3436
useEffect(() => {
3537
const isInviter = highestAuthority(user) === AUTHORITIES.INVITER;
@@ -54,10 +56,12 @@ export const Role = () => {
5456
navigate("/404");
5557
return;
5658
}
57-
roleByID(id, false)
58-
.then(res => {
59+
Promise.all([roleByID(id, false), institutionAdminsbyRole(id)])
60+
.then(results => {
61+
const res = results[0];
5962
deriveApplicationAttributes(res, I18n.locale, I18n.t("roles.multiple"), I18n.t("forms.and"))
6063
setRole(res);
64+
setInstitutionAdmins(results[1]);
6165
const isInviter = highestAuthority(user) === AUTHORITIES.INVITER;
6266
if (isInviter) {
6367
managersByRoleId(id).then(emails => setManagerEmails(emails));
@@ -173,6 +177,17 @@ export const Role = () => {
173177
<ClipBoardCopy txt={urn} transparentBackground={true}/>
174178
</div>
175179
<div className={"meta-data"}>
180+
<div className={"meta-data-row"}>
181+
<InstitutionAdminIcon/>
182+
{isEmpty(institutionAdmins) && <span>
183+
{I18n.t("role.noInstitutionAdmin")}
184+
</span>}
185+
{!isEmpty(institutionAdmins) && <span dangerouslySetInnerHTML={{
186+
__html: DOMPurify.sanitize(I18n.t("role.institutionAdmin", {
187+
names: splitListSemantically(institutionAdmins.map(u => u.name), I18n.t("forms.and"))
188+
}))
189+
}}/>}
190+
</div>
176191
<div className={"meta-data-row"}>
177192
<PersonIcon/>
178193
<span dangerouslySetInnerHTML={{

server/src/main/java/invite/api/UserController.java

Lines changed: 18 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,6 @@
3232
import org.springframework.data.domain.Sort;
3333
import org.springframework.http.MediaType;
3434
import org.springframework.http.ResponseEntity;
35-
import org.springframework.security.core.Authentication;
3635
import org.springframework.security.core.context.SecurityContextHolder;
3736
import org.springframework.transaction.annotation.Transactional;
3837
import org.springframework.util.StringUtils;
@@ -66,6 +65,7 @@ public class UserController {
6665
private final Config config;
6766
private final UserRepository userRepository;
6867
private final InvitationRepository invitationRepository;
68+
private final RoleRepository roleRepository;
6969
private final Manage manage;
7070
private final ObjectMapper objectMapper;
7171
private final RemoteProvisionedUserRepository remoteProvisionedUserRepository;
@@ -75,7 +75,7 @@ public class UserController {
7575
@Autowired
7676
public UserController(Config config,
7777
UserRepository userRepository,
78-
InvitationRepository invitationRepository,
78+
InvitationRepository invitationRepository, RoleRepository roleRepository,
7979
Manage manage,
8080
ObjectMapper objectMapper,
8181
RemoteProvisionedUserRepository remoteProvisionedUserRepository,
@@ -85,6 +85,7 @@ public UserController(Config config,
8585
@Value("${voot.group_urn_domain}") String groupUrnPrefix,
8686
ProvisioningService provisioningService) {
8787
this.invitationRepository = invitationRepository;
88+
this.roleRepository = roleRepository;
8889
this.provisioningService = provisioningService;
8990
this.config = config.withGroupUrnPrefix(groupUrnPrefix);
9091
this.userRepository = userRepository;
@@ -251,8 +252,22 @@ public ResponseEntity<Void> delete(@PathVariable("userId") Long userId, @Paramet
251252
return Results.deleteResult();
252253
}
253254

255+
@GetMapping("/institution-admins/{roleId}")
256+
@Transactional(readOnly = true)
257+
public ResponseEntity<List<User>> institutionAdminsbyRole(@PathVariable Long roleId,
258+
@Parameter(hidden = true) User user) {
259+
LOG.debug(String.format("GET institution-admins/%s for user %s", roleId, user.getEduPersonPrincipalName()));
260+
261+
Role role = roleRepository.findById(roleId).orElseThrow(() -> new NotFoundException("Role not found"));
262+
263+
UserPermissions.assertRoleAccess(user, role, Authority.INVITER);
264+
265+
List<User> users = userRepository.findInstitutionAdminsPerRole(role.getId());
266+
267+
return ResponseEntity.ok(users);
268+
}
254269

255-
@PostMapping("error")
270+
@PostMapping("error")
256271
public ResponseEntity<Map<String, Integer>> error(@RequestBody Map<String, Object> payload,
257272
@Parameter(hidden = true) User user) throws
258273
JsonProcessingException, UnknownHostException {

server/src/main/java/invite/repository/UserRepository.java

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -117,6 +117,14 @@ SELECT distinct(u.id), u.email, u.name, u.schac_home_organization, u.created_at,
117117
nativeQuery = true)
118118
List<User> findNonSuperUserWithoutUserRoles();
119119

120+
@Query(value = """
121+
SELECT u.* FROM users u
122+
INNER JOIN roles r on r.organization_guid = u.organization_guid
123+
WHERE r.id = ?1 AND u.institution_admin = 1
124+
""",
125+
nativeQuery = true)
126+
List<User> findInstitutionAdminsPerRole(Long roleId);
127+
120128
@Override
121129
default String rewrite(String query, Sort sort) {
122130
Sort.Order authoritySort = sort.getOrderFor("authority");

server/src/test/java/invite/api/UserControllerTest.java

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
import invite.manage.EntityType;
99
import invite.model.Authority;
1010
import invite.model.RemoteProvisionedUser;
11+
import invite.model.Role;
1112
import invite.model.User;
1213
import invite.model.UserRole;
1314
import io.restassured.common.mapper.TypeRef;
@@ -19,6 +20,7 @@
1920
import org.springframework.web.util.UriComponentsBuilder;
2021

2122
import java.io.IOException;
23+
import java.lang.reflect.Type;
2224
import java.net.URLDecoder;
2325
import java.nio.charset.StandardCharsets;
2426
import java.util.*;
@@ -551,6 +553,26 @@ void other() throws Exception {
551553
assertEquals(List.of("5", "5"), userRoles.stream().map(userRole -> userRole.getRole().getApplicationMaps().get(0).get("id")).toList());
552554
}
553555

556+
@Test
557+
void institutionAdminsbyRole() throws Exception {
558+
AccessCookieFilter accessCookieFilter = openIDConnectFlow("/api/v1/users/login", INVITER_WIKI_SUB);
559+
Role role = roleRepository.findByName("Wiki").get();
560+
561+
List<User> users = given()
562+
.when()
563+
.filter(accessCookieFilter.cookieFilter())
564+
.accept(ContentType.JSON)
565+
.contentType(ContentType.JSON)
566+
.pathParams("roleId", role.getId())
567+
.get("/api/v1/users/institution-admins/{roleId}")
568+
.as(new TypeRef<>() {
569+
});
570+
assertEquals(1, users.size());
571+
User user = users.getFirst();
572+
assertTrue(user.isInstitutionAdmin());
573+
assertEquals(user.getOrganizationGUID(), role.getOrganizationGUID());
574+
}
575+
554576
@Test
555577
void msAcceptReturn() throws Exception {
556578
super.stubForUpdateGraphUser(GUEST_SUB);

0 commit comments

Comments
 (0)