From 1fc9794d7d4e6c5dd6ef6edf2c74183bf04f8846 Mon Sep 17 00:00:00 2001 From: Okke Harsta Date: Mon, 25 Sep 2023 16:01:25 +0200 Subject: [PATCH] WIP for institution admin provisioning --- client/src/utils/UserRole.js | 12 ++-- .../main/java/access/api/UserController.java | 4 ++ .../UserHandlerMethodArgumentResolver.java | 56 ++++++++++++++++++- .../main/java/access/manage/LocalManage.java | 18 +++++- .../src/main/java/access/manage/Manage.java | 4 +- .../main/java/access/manage/RemoteManage.java | 29 +++++++--- .../src/main/java/access/model/Authority.java | 4 +- server/src/main/java/access/model/User.java | 15 +++-- .../access/security/InstitutionAdmin.java | 17 ++++++ .../java/access/security/SecurityConfig.java | 8 ++- server/src/main/resources/application.yml | 4 ++ .../V5_0__user_institution_admin.sql | 4 ++ .../src/main/resources/manage/oidc10_rp.json | 3 +- .../src/main/resources/manage/saml20_sp.json | 6 +- server/src/test/java/access/AbstractTest.java | 29 +++++++++- server/src/test/java/access/Seed.java | 9 ++- .../java/access/api/UserControllerTest.java | 46 +++++++++++++-- 17 files changed, 231 insertions(+), 37 deletions(-) create mode 100644 server/src/main/java/access/security/InstitutionAdmin.java create mode 100644 server/src/main/resources/db/mysql/migration/V5_0__user_institution_admin.sql diff --git a/client/src/utils/UserRole.js b/client/src/utils/UserRole.js index ded980de..cd205e1b 100644 --- a/client/src/utils/UserRole.js +++ b/client/src/utils/UserRole.js @@ -10,6 +10,7 @@ export const INVITATION_STATUS = { export const AUTHORITIES = { SUPER_USER: "SUPER_USER", + INSTITUTION_ADMIN: "INSTITUTION_ADMIN", MANAGER: "MANAGER", INVITER: "INVITER", GUEST: "GUEST" @@ -17,15 +18,19 @@ export const AUTHORITIES = { const AUTHORITIES_HIERARCHY = { [AUTHORITIES.SUPER_USER]: 1, - [AUTHORITIES.MANAGER]: 2, - [AUTHORITIES.INVITER]: 3, - [AUTHORITIES.GUEST]: 4 + [AUTHORITIES.INSTITUTION_ADMIN]: 2, + [AUTHORITIES.MANAGER]: 3, + [AUTHORITIES.INVITER]: 4, + [AUTHORITIES.GUEST]: 5 } export const highestAuthority = user => { if (user.superUser) { return AUTHORITIES.SUPER_USER; } + if (user.institutionAdmin) { + return AUTHORITIES.INSTITUTION_ADMIN; + } return (user.userRoles || []).reduce((acc, u) => { if (AUTHORITIES_HIERARCHY[acc] > AUTHORITIES_HIERARCHY[AUTHORITIES[u.authority]]) { return u.authority @@ -130,7 +135,6 @@ export const allowedAuthoritiesForInvitation = (user, selectedRoles) => { } //Return only the AUTHORITIES where the user has the correct authority per selectedRole const userRolesForSelectedRoles = selectedRoles - //TODO Remove this hack and require only really roles .map(role => role.isUserRole ? role.role : role) .map(role => user.userRoles.find(userRole => userRole.role.manageId === role.manageId || userRole.role.id === role.id)) .filter(userRole => !isEmpty(userRole)); diff --git a/server/src/main/java/access/api/UserController.java b/server/src/main/java/access/api/UserController.java index a9cbb9fa..8d564eca 100644 --- a/server/src/main/java/access/api/UserController.java +++ b/server/src/main/java/access/api/UserController.java @@ -89,6 +89,10 @@ public ResponseEntity me(@Parameter(hidden = true) User user) { LOG.debug("/me"); List roles = user.getUserRoles().stream().map(UserRole::getRole).toList(); manage.deriveRemoteApplications(roles); + if (user.isInstitutionAdmin() && StringUtils.hasText(user.getOrganizationGUID())) { + List> applications = manage.providersByInstitutionalGUID(user.getOrganizationGUID()); + user.setApplications(applications); + } return ResponseEntity.ok(user); } diff --git a/server/src/main/java/access/config/UserHandlerMethodArgumentResolver.java b/server/src/main/java/access/config/UserHandlerMethodArgumentResolver.java index bd356c86..a9b8a37e 100755 --- a/server/src/main/java/access/config/UserHandlerMethodArgumentResolver.java +++ b/server/src/main/java/access/config/UserHandlerMethodArgumentResolver.java @@ -3,6 +3,7 @@ import access.exception.UserRestrictionException; import access.model.User; import access.repository.UserRepository; +import access.security.InstitutionAdmin; import access.security.SuperAdmin; import org.springframework.core.MethodParameter; import org.springframework.security.oauth2.client.authentication.OAuth2AuthenticationToken; @@ -15,7 +16,7 @@ import org.springframework.web.method.support.ModelAndViewContainer; import java.security.Principal; -import java.time.Instant; +import java.util.List; import java.util.Map; import java.util.Optional; @@ -23,10 +24,12 @@ public class UserHandlerMethodArgumentResolver implements HandlerMethodArgumentR private final UserRepository userRepository; private final SuperAdmin superAdmin; + private final InstitutionAdmin institutionAdmin; - public UserHandlerMethodArgumentResolver(UserRepository userRepository, SuperAdmin superAdmin) { + public UserHandlerMethodArgumentResolver(UserRepository userRepository, SuperAdmin superAdmin, InstitutionAdmin institutionAdmin) { this.userRepository = userRepository; this.superAdmin = superAdmin; + this.institutionAdmin = institutionAdmin; } public boolean supportsParameter(MethodParameter methodParameter) { @@ -54,7 +57,17 @@ public User resolveArgument(MethodParameter methodParameter, superAdmin.getUsers().stream().filter(adminSub -> adminSub.equals(sub)) .findFirst() .map(adminSub -> userRepository.save(new User(true, attributes))) - ).map(user -> { + ) + .or(() -> { + if (this.isInstitutionAdmin(attributes)) { + User user = new User(attributes); + userRepository.save(user); + return Optional.of(user); + } else { + return Optional.empty(); + } + }) + .map(user -> { String impersonateId = webRequest.getHeader("X-IMPERSONATE-ID"); if (StringUtils.hasText(impersonateId) && user.isSuperUser()) { return userRepository.findById(Long.valueOf(impersonateId)) @@ -69,10 +82,47 @@ public User resolveArgument(MethodParameter methodParameter, return optionalUser.map(user -> { if (user.getId() != null) { user.updateAttributes(attributes); + this.updateUser(user, attributes); userRepository.save(user); } return user; }).orElseThrow(UserRestrictionException::new); } + + private boolean isInstitutionAdmin(Map attributes) { + if (attributes.containsKey("eduperson_entitlement")) { + List entitlements = ((List) attributes.get("eduperson_entitlement")) + .stream().map(String::toLowerCase).toList(); + if (entitlements.contains(this.institutionAdmin.getEntitlement().toLowerCase())) { + return true; + } + } + return false; + } + + private User updateUser(User user, Map attributes) { + if (attributes.containsKey("eduperson_entitlement")) { + List entitlements = ((List) attributes.get("eduperson_entitlement")) + .stream().map(String::toLowerCase).toList(); + user.setInstitutionAdmin(entitlements.contains(this.institutionAdmin.getEntitlement().toLowerCase())); + String organizationGUIPrefix = this.institutionAdmin.getOrganizationGuidPrefix().toLowerCase(); + boolean hasOrganizationPrefix = false; + //lambda requires final variables + for (String entitlement : entitlements) { + if (entitlement.startsWith(organizationGUIPrefix)) { + user.setOrganizationGUID(entitlement.substring(this.institutionAdmin.getOrganizationGuidPrefix().length())); + hasOrganizationPrefix = true; + break; + } + } + if (!hasOrganizationPrefix) { + user.setOrganizationGUID(null); + } + } else { + user.setInstitutionAdmin(false); + user.setOrganizationGUID(null); + } + return user; + } } \ No newline at end of file diff --git a/server/src/main/java/access/manage/LocalManage.java b/server/src/main/java/access/manage/LocalManage.java index 5fe85b49..1c2d432a 100644 --- a/server/src/main/java/access/manage/LocalManage.java +++ b/server/src/main/java/access/manage/LocalManage.java @@ -8,6 +8,7 @@ import java.util.List; import java.util.Map; +import java.util.Objects; import java.util.Optional; import java.util.stream.Collectors; import java.util.stream.Stream; @@ -30,9 +31,11 @@ private List> initialize(ObjectMapper objectMapper, EntityTy } @Override - public List> providers(EntityType entityType) { + public List> providers(EntityType... entityTypes) { //Ensure it is immutable - return addIdentifierAlias(this.allProviders.get(entityType).stream().toList()); + return addIdentifierAlias(Stream.of(entityTypes).map(entityType -> this.allProviders.get(entityType).stream().toList()) + .flatMap(List::stream) + .toList()); } @Override @@ -82,4 +85,15 @@ public List> allowedEntries(EntityType entityType, String id }) .collect(Collectors.toList())); } + + @Override + public List> providersByInstitutionalGUID(String organisationGUID) { + List> providers = providers(EntityType.SAML20_SP, EntityType.OIDC10_RP); + return providers + .stream() + .filter(provider -> Objects.equals(((Map) ((Map) provider.get("data")) + .get("metaDataFields")) + .get("coin:institution_guid"), organisationGUID)) + .toList(); + } } diff --git a/server/src/main/java/access/manage/Manage.java b/server/src/main/java/access/manage/Manage.java index 7c490081..51a2485f 100644 --- a/server/src/main/java/access/manage/Manage.java +++ b/server/src/main/java/access/manage/Manage.java @@ -8,7 +8,7 @@ public interface Manage { - List> providers(EntityType entityType); + List> providers(EntityType... entityTypes); Map providerById(EntityType entityType, String id); @@ -20,6 +20,8 @@ public interface Manage { List> allowedEntries(EntityType entityType, String id); + List> providersByInstitutionalGUID(String organisationGUID); + //Due to the different API's we are using, the result sometimes contains an "_id" and sometimes an "id" default List> addIdentifierAlias(List> providers) { providers.forEach(this::addIdentifierAlias); diff --git a/server/src/main/java/access/manage/RemoteManage.java b/server/src/main/java/access/manage/RemoteManage.java index 6122643d..8d05e0b0 100644 --- a/server/src/main/java/access/manage/RemoteManage.java +++ b/server/src/main/java/access/manage/RemoteManage.java @@ -2,7 +2,6 @@ import com.fasterxml.jackson.core.type.TypeReference; import com.fasterxml.jackson.databind.ObjectMapper; -import lombok.SneakyThrows; import org.springframework.core.io.ClassPathResource; import org.springframework.http.client.support.BasicAuthenticationInterceptor; import org.springframework.web.client.RestTemplate; @@ -14,6 +13,7 @@ import java.util.Map; import java.util.Optional; import java.util.stream.Collectors; +import java.util.stream.Stream; @SuppressWarnings("unchecked") public class RemoteManage implements Manage { @@ -30,8 +30,10 @@ public RemoteManage(String url, String user, String password, ObjectMapper objec } @Override - public List> providers(EntityType entityType) { - return getRemoteMetaData(entityType.collectionName()); + public List> providers(EntityType... entityTypes) { + return addIdentifierAlias(Stream.of(entityTypes).map(entityType -> this.getRemoteMetaData(entityType.collectionName())) + .flatMap(List::stream) + .toList()); } @Override @@ -46,7 +48,7 @@ public List> providersByIdIn(EntityType entityType, List> providerByEntityID(EntityType entityType, String entityID) { String query = URLEncoder.encode(String.format("{\"data.entityid\":\"%s\"}", entityID), Charset.defaultCharset()); String queryUrl = String.format("%s/manage/api/internal/rawSearch/%s?query=%s", url, entityType.collectionName(), query); - List> providers = addIdentifierAlias(restTemplate.getForEntity(queryUrl, List.class).getBody()); + List> providers = addIdentifierAlias(restTemplate.getForEntity(queryUrl, List.class).getBody()); return providers.isEmpty() ? Optional.empty() : Optional.of(providers.get(0)); } @@ -57,7 +59,6 @@ public Map providerById(EntityType entityType, String id) { } - @Override public List> provisioning(List ids) { String queryUrl = String.format("%s/manage/api/internal/provisioning", url); @@ -66,14 +67,28 @@ public List> provisioning(List ids) { @Override public List> allowedEntries(EntityType entityType, String id) { - String queryUrl = String.format("%s/manage/api/internal/allowedEntities/%s/%s", url, entityType, id); + String queryUrl = String.format("%s/manage/api/internal/allowedEntities/%s/%s", url, entityType.collectionName(), id); return addIdentifierAlias(restTemplate.getForEntity(queryUrl, List.class).getBody()); } + @Override + public List> providersByInstitutionalGUID(String organisationGUID) { + Map baseQuery = (Map) this.queries.get("base_query"); + baseQuery.put("coin:institution_guid", organisationGUID); + List serviceProviders = restTemplate.postForObject( + String.format("%s/manage/api/internal/search/%s", this.url, EntityType.SAML20_SP.collectionName()), + baseQuery, List.class); + List relyingParties = restTemplate.postForObject( + String.format("%s/manage/api/internal/search/%s", this.url, EntityType.OIDC10_RP.collectionName()), + baseQuery, List.class); + serviceProviders.addAll(relyingParties); + return addIdentifierAlias(serviceProviders); + } + private List> getRemoteMetaData(String type) { Object baseQuery = this.queries.get("base_query"); String url = String.format("%s/manage/api/internal/search/%s", this.url, type); return addIdentifierAlias(restTemplate.postForObject(url, baseQuery, List.class)); - } + } diff --git a/server/src/main/java/access/model/Authority.java b/server/src/main/java/access/model/Authority.java index 21cb7a57..99fcd8d8 100644 --- a/server/src/main/java/access/model/Authority.java +++ b/server/src/main/java/access/model/Authority.java @@ -4,10 +4,11 @@ public enum Authority { - SUPER_USER(3), MANAGER(2), INVITER(1), GUEST(0); + SUPER_USER(4), INSTITUTION_ADMIN(3), MANAGER(2), INVITER(1), GUEST(0); private Map> translations = Map.of( "SUPER_USER", Map.of("en", "Super user", "nl", "Super user"), + "INSTITUTION_ADMIN", Map.of("en", "Institution admin", "nl", "Instellings-admin"), "MANAGER", Map.of("en", "Manager", "nl", "Beheerder"), "INVITER", Map.of("en", "Inviter", "nl", "Uitnodiger"), "GUEST", Map.of("en", "Guest", "nl", "Gast") @@ -31,6 +32,7 @@ public Authority nextAuthorityInHierarchy() { return switch (this.rights) { case 0 -> INVITER; case 1 -> MANAGER; + case 2 -> INSTITUTION_ADMIN; default -> SUPER_USER; }; } diff --git a/server/src/main/java/access/model/User.java b/server/src/main/java/access/model/User.java index be0ba525..c25a1cdc 100644 --- a/server/src/main/java/access/model/User.java +++ b/server/src/main/java/access/model/User.java @@ -11,10 +11,7 @@ import java.io.Serializable; import java.time.Instant; -import java.util.HashSet; -import java.util.List; -import java.util.Map; -import java.util.Set; +import java.util.*; import java.util.stream.Collectors; import java.util.stream.Stream; @@ -52,6 +49,13 @@ public class User implements Serializable, Provisionable { @Column(name = "schac_home_organization") private String schacHomeOrganization; + @Column(name = "organization_guid") + private String organizationGUID; + + @Column(name = "institution_admin") + @NotNull + private boolean institutionAdmin; + @Column private String email; @@ -68,6 +72,9 @@ public class User implements Serializable, Provisionable { @JsonIgnore private Set remoteProvisionedUsers = new HashSet<>(); + @Transient + private List> applications = Collections.emptyList(); + public User(Map attributes) { this(false, attributes); } diff --git a/server/src/main/java/access/security/InstitutionAdmin.java b/server/src/main/java/access/security/InstitutionAdmin.java new file mode 100644 index 00000000..20090e1f --- /dev/null +++ b/server/src/main/java/access/security/InstitutionAdmin.java @@ -0,0 +1,17 @@ +package access.security; + +import lombok.Getter; +import lombok.Setter; +import org.springframework.boot.context.properties.ConfigurationProperties; + +import java.util.List; + +@ConfigurationProperties(prefix = "institution-admin") +@Getter +@Setter +public class InstitutionAdmin { + + private String entitlement; + private String organizationGuidPrefix; + +} diff --git a/server/src/main/java/access/security/SecurityConfig.java b/server/src/main/java/access/security/SecurityConfig.java index 5220b448..0e60fd75 100644 --- a/server/src/main/java/access/security/SecurityConfig.java +++ b/server/src/main/java/access/security/SecurityConfig.java @@ -93,21 +93,23 @@ public SecurityConfig(ClientRegistrationRepository clientRegistrationRepository, } @Configuration - @EnableConfigurationProperties(SuperAdmin.class) + @EnableConfigurationProperties({SuperAdmin.class, InstitutionAdmin.class}) public static class MvcConfig implements WebMvcConfigurer { private final UserRepository userRepository; private final SuperAdmin superAdmin; + private final InstitutionAdmin institutionAdmin; @Autowired - public MvcConfig(UserRepository userRepository, SuperAdmin superAdmin) { + public MvcConfig(UserRepository userRepository, SuperAdmin superAdmin, InstitutionAdmin institutionAdmin) { this.userRepository = userRepository; this.superAdmin = superAdmin; + this.institutionAdmin = institutionAdmin; } @Override public void addArgumentResolvers(List argumentResolvers) { - argumentResolvers.add(new UserHandlerMethodArgumentResolver(userRepository, superAdmin)); + argumentResolvers.add(new UserHandlerMethodArgumentResolver(userRepository, superAdmin, institutionAdmin)); } @Override diff --git a/server/src/main/resources/application.yml b/server/src/main/resources/application.yml index 4ade1eb9..59565cd2 100644 --- a/server/src/main/resources/application.yml +++ b/server/src/main/resources/application.yml @@ -78,6 +78,10 @@ super-admin: users: - "urn:collab:person:example.com:admin" +institution-admin: + entitlement: "urn:mace:surfnet.nl:surfnet.nl:sab:role:SURFconextverantwoordelijke" + organization-guid-prefix: "urn:mace:surfnet.nl:surfnet.nl:sab:organizationGUID:" + config: client-url: "http://localhost:3000" welcome-url: "http://localhost:4000" diff --git a/server/src/main/resources/db/mysql/migration/V5_0__user_institution_admin.sql b/server/src/main/resources/db/mysql/migration/V5_0__user_institution_admin.sql new file mode 100644 index 00000000..28adb49f --- /dev/null +++ b/server/src/main/resources/db/mysql/migration/V5_0__user_institution_admin.sql @@ -0,0 +1,4 @@ +ALTER TABLE `users` + add `institution_admin` bool DEFAULT 0; +ALTER TABLE `users` + add `organization_guid` varchar(255) DEFAULT NULL; diff --git a/server/src/main/resources/manage/oidc10_rp.json b/server/src/main/resources/manage/oidc10_rp.json index 3a8ce434..68155ed5 100644 --- a/server/src/main/resources/manage/oidc10_rp.json +++ b/server/src/main/resources/manage/oidc10_rp.json @@ -9,7 +9,8 @@ "name:en": "Calendar EN", "name:nl": "Calendar NL", "OrganizationName:en": "SURF bv", - "logo:0:url": "https://static.surfconext.nl/media/idp/surfconext.png" + "logo:0:url": "https://static.surfconext.nl/media/idp/surfconext.png", + "coin:institution_guid": "ad93daef-0911-e511-80d0-005056956c1a" } } }, diff --git a/server/src/main/resources/manage/saml20_sp.json b/server/src/main/resources/manage/saml20_sp.json index 2f760e86..1c2a8cf2 100644 --- a/server/src/main/resources/manage/saml20_sp.json +++ b/server/src/main/resources/manage/saml20_sp.json @@ -8,7 +8,8 @@ "metaDataFields": { "name:en": "Wiki EN", "name:nl": "Wiki NL", - "OrganizationName:en": "SURF bv" + "OrganizationName:en": "SURF bv", + "coin:institution_guid": "ad93daef-0911-e511-80d0-005056956c1a" } } }, @@ -22,7 +23,8 @@ "name:en": "Network EN", "name:nl": "Network NL", "OrganizationName:en": "SURF bv", - "logo:0:url": "https://static.surfconext.nl/media/idp/surfconext.png" + "logo:0:url": "https://static.surfconext.nl/media/idp/surfconext.png", + "coin:institution_guid": "ad93daef-0911-e511-80d0-005056956c1a" } } }, diff --git a/server/src/test/java/access/AbstractTest.java b/server/src/test/java/access/AbstractTest.java index 3a0effbb..f369ae52 100644 --- a/server/src/test/java/access/AbstractTest.java +++ b/server/src/test/java/access/AbstractTest.java @@ -52,6 +52,8 @@ import java.time.temporal.ChronoUnit; import java.util.*; import java.util.function.Consumer; +import java.util.function.UnaryOperator; +import java.util.stream.Stream; import static com.github.tomakehurst.wiremock.client.WireMock.*; import static io.restassured.RestAssured.given; @@ -160,11 +162,11 @@ protected String opaqueAccessToken(String sub, String responseJsonFileName, Stri } protected AccessCookieFilter openIDConnectFlow(String path, String sub) throws Exception { - return this.openIDConnectFlow(path, sub, s -> { - }); + return this.openIDConnectFlow(path, sub, s -> {}, m -> m); } - protected AccessCookieFilter openIDConnectFlow(String path, String sub, Consumer authorizationConsumer) throws Exception { + protected AccessCookieFilter openIDConnectFlow(String path, String sub, Consumer authorizationConsumer, + UnaryOperator> userInfoEnhancer) throws Exception { CookieFilter cookieFilter = new CookieFilter(); Headers headers = given() .redirects() @@ -220,6 +222,7 @@ protected AccessCookieFilter openIDConnectFlow(String path, String sub, Consumer userInfo.put("sub", StringUtils.hasText(sub) ? sub : "sub"); userInfo.put("email", sub); userInfo.put("eduperson_principal_name", sub); + userInfo = userInfoEnhancer.apply(userInfo); String userInfoResult = objectMapper.writeValueAsString(userInfo); stubFor(get(urlPathMatching("/user-info")).willReturn(aResponse() .withHeader("Content-Type", "application/json") @@ -311,6 +314,26 @@ protected void stubForManageProvisioning(List identifiers) throws JsonPr } + protected void stubForManageProviderByOrganisationGUID(String organisationGUID) throws JsonProcessingException { + String path = "/manage/api/internal/search/%s"; + List> providers = localManage.providersByInstitutionalGUID(organisationGUID); + Map>> providersMap = Map.of( + EntityType.SAML20_SP, + providers.stream().filter(m -> m.get("type").equals(EntityType.SAML20_SP.collectionName())).toList(), + EntityType.OIDC10_RP, + providers.stream().filter(m -> m.get("type").equals(EntityType.OIDC10_RP.collectionName())).toList() + ); + //Lambda can't do exception handling + for (Map.Entry>> entry : providersMap.entrySet()) { + EntityType entityType = entry.getKey(); + List> providerList = entry.getValue(); + stubFor(post(urlPathMatching(String.format(path, entityType.collectionName()))).willReturn(aResponse() + .withHeader("Content-Type", "application/json") + .withBody(objectMapper.writeValueAsString(providerList)))); + } + + } + protected void stubForManageProviderById(EntityType entityType, String id) throws JsonProcessingException { String path = String.format("/manage/api/internal/metadata/%s/%s", entityType.name().toLowerCase(), id); String body = objectMapper.writeValueAsString(localManage.providerById(entityType, id)); diff --git a/server/src/test/java/access/Seed.java b/server/src/test/java/access/Seed.java index 43b97ad7..97f28842 100644 --- a/server/src/test/java/access/Seed.java +++ b/server/src/test/java/access/Seed.java @@ -20,9 +20,11 @@ public record Seed(InvitationRepository invitationRepository, public static final String SUPER_SUB = "urn:collab:person:example.com:super"; public static final String MANAGE_SUB = "urn:collab:person:example.com:manager"; + public static final String INSTITUTION_ADMIN = "urn:collab:person:example.com:institution_admin"; public static final String INVITER_SUB = "urn:collab:person:example.com:inviter"; public static final String GUEST_SUB = "urn:collab:person:example.com:guest"; public static final String GRAPH_INVITATION_HASH = "graph_invitation_hash"; + public static final String ORGANISATION_GUID = "ad93daef-0911-e511-80d0-005056956c1a"; public void doSeed() { this.invitationRepository.deleteAllInBatch(); @@ -35,13 +37,18 @@ public void doSeed() { User superUser = new User(true, SUPER_SUB, SUPER_SUB, "example.com", "David", "Doe", "david.doe@examole.com"); + User institutionAdmin = + new User(false, INSTITUTION_ADMIN, INSTITUTION_ADMIN, "example.com", "Carl", "Doe", "carl.doe@examole.com"); + institutionAdmin.setInstitutionAdmin(true); + institutionAdmin.setOrganizationGUID(ORGANISATION_GUID); + User manager = new User(false, MANAGE_SUB, MANAGE_SUB, "example.com", "Mary", "Doe", "mary.doe@examole.com"); User inviter = new User(false, INVITER_SUB, INVITER_SUB, "example.com", "Paul", "Doe", "paul.doe@examole.com"); User guest = new User(false, GUEST_SUB, GUEST_SUB, "example.com", "Ann", "Doe", "ann.doe@examole.com"); - doSave(this.userRepository, superUser, manager, inviter, guest); + doSave(this.userRepository, superUser, institutionAdmin, manager, inviter, guest); Role wiki = new Role("Wiki", "Wiki desc", "https://landingpage.com", "1", EntityType.SAML20_SP, 365, false, false); diff --git a/server/src/test/java/access/api/UserControllerTest.java b/server/src/test/java/access/api/UserControllerTest.java index bd2a1a16..b529505d 100644 --- a/server/src/test/java/access/api/UserControllerTest.java +++ b/server/src/test/java/access/api/UserControllerTest.java @@ -4,12 +4,10 @@ import access.AccessCookieFilter; import access.manage.EntityType; import access.model.Authority; -import access.model.Role; import access.model.User; import access.model.UserRole; import io.restassured.common.mapper.TypeRef; import io.restassured.http.ContentType; -import io.restassured.http.Header; import org.junit.jupiter.api.Test; import org.springframework.util.MultiValueMap; import org.springframework.web.util.UriComponentsBuilder; @@ -23,7 +21,6 @@ import static access.Seed.*; import static com.github.tomakehurst.wiremock.client.WireMock.*; -import static com.github.tomakehurst.wiremock.client.WireMock.aResponse; import static io.restassured.RestAssured.given; import static org.junit.jupiter.api.Assertions.*; @@ -67,7 +64,7 @@ void configMissingAttributes() throws Exception { .get("/api/v1/users/config") .as(Map.class); assertFalse((Boolean) res.get("authenticated")); - assertEquals(2, ((List)res.get("missingAttributes")).size()); + assertEquals(2, ((List) res.get("missingAttributes")).size()); } @Test @@ -93,6 +90,44 @@ void meWithOauth2Login() throws Exception { assertTrue((Boolean) res.get("authenticated")); } + @Test + void institutionAdminProvision() throws Exception { + AccessCookieFilter accessCookieFilter = openIDConnectFlow("/api/v1/users/me", "new_institution_admin", + s -> { + }, m -> { + m.put("eduperson_entitlement", + List.of( + "urn:mace:surfnet.nl:surfnet.nl:sab:role:SURFconextverantwoordelijke", + "urn:mace:surfnet.nl:surfnet.nl:sab:organizationGUID:" + ORGANISATION_GUID + )); + return m; + }); + super.stubForManageProviderByOrganisationGUID(ORGANISATION_GUID); + + User user = given() + .when() + .filter(accessCookieFilter.cookieFilter()) + .accept(ContentType.JSON) + .contentType(ContentType.JSON) + .get(accessCookieFilter.apiURL()) + .as(User.class); + assertNotNull(user.getId()); + assertTrue(user.isInstitutionAdmin()); + assertEquals(ORGANISATION_GUID, user.getOrganizationGUID()); + assertEquals(3, user.getApplications().size()); + user.getApplications().stream().forEach(application -> assertEquals(ORGANISATION_GUID, + ((Map)((Map)application.get("data")).get("metaDataFields")).get("coin:institution_guid"))); + + Map res = given() + .when() + .filter(accessCookieFilter.cookieFilter()) + .accept(ContentType.JSON) + .contentType(ContentType.JSON) + .get("/api/v1/users/config") + .as(Map.class); + assertTrue((Boolean) res.get("authenticated")); + } + @Test void meWithRoles() throws Exception { AccessCookieFilter accessCookieFilter = openIDConnectFlow("/api/v1/users/me", INVITER_SUB); @@ -128,7 +163,8 @@ void loginWithOauth2Login() throws Exception { assertEquals("login", prompt); String loginHint = URLDecoder.decode(queryParams.getFirst("login_hint"), StandardCharsets.UTF_8); assertEquals("https://login.test2.eduid.nl", loginHint); - }); + }, + m -> m); String location = given() .redirects()