diff --git a/server/src/main/java/access/api/UserController.java b/server/src/main/java/access/api/UserController.java index 300f2c66..bfa01bc3 100644 --- a/server/src/main/java/access/api/UserController.java +++ b/server/src/main/java/access/api/UserController.java @@ -76,7 +76,7 @@ public UserController(Config config, this.objectMapper = objectMapper; this.manage = manage; this.remoteProvisionedUserRepository = remoteProvisionedUserRepository; - this.graphClient = new GraphClient(serverBaseURL, eduidIdpSchacHomeOrganization, keyStore); + this.graphClient = new GraphClient(serverBaseURL, eduidIdpSchacHomeOrganization, keyStore, objectMapper); } @GetMapping("config") diff --git a/server/src/main/java/access/model/User.java b/server/src/main/java/access/model/User.java index 3213f5a7..17cf2eb4 100644 --- a/server/src/main/java/access/model/User.java +++ b/server/src/main/java/access/model/User.java @@ -178,7 +178,7 @@ public void removeUserRole(UserRole role) { @JsonIgnore public Set manageIdentifierSet() { return userRoles.stream() - .filter(userRole -> userRole.getAuthority().equals(Authority.GUEST)) + .filter(userRole -> userRole.getAuthority().equals(Authority.GUEST) || userRole.isGuestRoleIncluded()) .map(userRole -> userRole.getRole().getApplicationUsages()) .flatMap(Collection::stream) .map(applicationUsage -> new ManageIdentifier(applicationUsage.getApplication().getManageId(),applicationUsage.getApplication().getManageType())) @@ -199,16 +199,41 @@ public Map asMap() { } @JsonIgnore - public void updateAttributes(Map attributes) { - this.eduPersonPrincipalName = (String) attributes.get("eduperson_principal_name"); - this.schacHomeOrganization = (String) attributes.get("schac_home_organization"); - this.givenName = (String) attributes.get("given_name"); - this.familyName = (String) attributes.get("family_name"); - this.email = (String) attributes.get("email"); + public boolean updateAttributes(Map attributes) { + boolean changed = false; + String newEdupersonPrincipalName = (String) attributes.get("eduperson_principal_name"); + changed = changed || !Objects.equals(this.eduPersonPrincipalName, newEdupersonPrincipalName); + this.eduPersonPrincipalName = newEdupersonPrincipalName; + + String newSchacHomeOrganization = (String) attributes.get("schac_home_organization"); + changed = changed || !Objects.equals(this.schacHomeOrganization, newSchacHomeOrganization); + this.schacHomeOrganization = newSchacHomeOrganization; + + String newGivenName = (String) attributes.get("given_name"); + changed = changed || !Objects.equals(this.givenName, newGivenName); + this.givenName = newGivenName; + + String newFamilyName = (String) attributes.get("family_name"); + changed = changed || !Objects.equals(this.familyName, newFamilyName); + this.familyName = newFamilyName; + + String newEmail = (String) attributes.get("email"); + changed = changed || !Objects.equals(this.email, newEmail); + this.email = newEmail; + this.lastActivity = Instant.now(); + String currentName = this.name; + String currentGivenName = this.givenName; + String currentFamilyName = this.familyName; + this.nameInvariant(attributes); + + changed = changed || !Objects.equals(this.name, currentName) || !Objects.equals(this.givenName, currentGivenName) + || !Objects.equals(this.familyName, currentFamilyName); + this.updateRemoteAttributes(attributes); + return changed; } @JsonIgnore diff --git a/server/src/main/java/access/provision/ProvisioningService.java b/server/src/main/java/access/provision/ProvisioningService.java index 6ba17af0..c9ed2042 100644 --- a/server/src/main/java/access/provision/ProvisioningService.java +++ b/server/src/main/java/access/provision/ProvisioningService.java @@ -11,6 +11,8 @@ public interface ProvisioningService { Optional newUserRequest(User user); + void updateUserRequest(User user); + void deleteUserRequest(User user); void newGroupRequest(Role role); diff --git a/server/src/main/java/access/provision/ProvisioningServiceDefault.java b/server/src/main/java/access/provision/ProvisioningServiceDefault.java index fbb7ca2c..7a854425 100644 --- a/server/src/main/java/access/provision/ProvisioningServiceDefault.java +++ b/server/src/main/java/access/provision/ProvisioningServiceDefault.java @@ -76,7 +76,7 @@ public ProvisioningServiceDefault(UserRoleRepository userRoleRepository, this.objectMapper = objectMapper; this.keyStore = keyStore; this.groupUrnPrefix = groupUrnPrefix; - this.graphClient = new GraphClient(serverBaseURL, eduidIdpSchacHomeOrganization, keyStore); + this.graphClient = new GraphClient(serverBaseURL, eduidIdpSchacHomeOrganization, keyStore, objectMapper); this.evaClient = new EvaClient(keyStore); // Otherwise, we can't use method PATCH OkHttpClient.Builder builder = new OkHttpClient.Builder(); @@ -86,7 +86,6 @@ public ProvisioningServiceDefault(UserRoleRepository userRoleRepository, } @Override - @SneakyThrows public Optional newUserRequest(User user) { List provisionings = getProvisionings(user); AtomicReference graphResponseReference = new AtomicReference<>(); @@ -109,7 +108,23 @@ public Optional newUserRequest(User user) { } @Override - @SneakyThrows + public void updateUserRequest(User user) { + List userProvisionings = getProvisionings(user); + List provisionings = userProvisionings.stream() + .filter(provisioning -> provisioning.getProvisioningType().equals(ProvisioningType.scim)) + .toList(); + //Provision the user to all provisionings in Manage where the user is known + provisionings.forEach(provisioning -> { + Optional provisionedUserOptional = + this.remoteProvisionedUserRepository.findByManageProvisioningIdAndUser(provisioning.getId(), user); + provisionedUserOptional.ifPresent(remoteProvisionedUser -> { + String userRequest = prettyJson(new UserRequest(user, remoteProvisionedUser.getRemoteIdentifier())); + this.updateRequest(provisioning, userRequest, USER_API, remoteProvisionedUser.getRemoteIdentifier(), HttpMethod.PUT); + }); + }); + } + + @Override public void deleteUserRequest(User user) { //First send update role requests user.getUserRoles() @@ -306,7 +321,6 @@ private String patchGroupRequest(Role role, return prettyJson(request); } - @SneakyThrows private Optional newRequest(Provisioning provisioning, String request, Provisionable provisionable) { boolean isUser = provisionable instanceof User; String apiType = isUser ? USER_API : GROUP_API; @@ -334,7 +348,6 @@ private Optional newRequest(Provisioning provisioning, Str } - @SneakyThrows private void updateRequest(Provisioning provisioning, String request, String apiType, @@ -362,7 +375,6 @@ private List getManageIdentifiers(Role role) { return role.applicationsUsed().stream().map(Application::getManageId).distinct().sorted().toList(); } - @SneakyThrows private void deleteRequest(Provisioning provisioning, String request, Provisionable provisionable, diff --git a/server/src/main/java/access/provision/graph/GraphClient.java b/server/src/main/java/access/provision/graph/GraphClient.java index f3001660..e41c2b2e 100644 --- a/server/src/main/java/access/provision/graph/GraphClient.java +++ b/server/src/main/java/access/provision/graph/GraphClient.java @@ -5,6 +5,7 @@ import access.provision.Provisioning; import com.azure.identity.ClientSecretCredential; import com.azure.identity.ClientSecretCredentialBuilder; +import com.fasterxml.jackson.databind.ObjectMapper; import com.microsoft.graph.authentication.TokenCredentialAuthProvider; import com.microsoft.graph.core.ClientException; import com.microsoft.graph.http.BaseRequest; @@ -19,6 +20,7 @@ import org.springframework.util.ReflectionUtils; import org.springframework.util.StringUtils; +import java.io.IOException; import java.lang.reflect.Field; public class GraphClient { @@ -28,11 +30,13 @@ public class GraphClient { private final String serverUrl; private final String eduidIdpSchacHomeOrganization; private final KeyStore keyStore; + private final ObjectMapper objectMapper; - public GraphClient(String serverUrl, String eduidIdpSchacHomeOrganization, KeyStore keyStore) { + public GraphClient(String serverUrl, String eduidIdpSchacHomeOrganization, KeyStore keyStore, ObjectMapper objectMapper) { this.serverUrl = serverUrl; this.eduidIdpSchacHomeOrganization = eduidIdpSchacHomeOrganization; this.keyStore= keyStore; + this.objectMapper = objectMapper; } @SuppressWarnings("unchecked") @@ -59,12 +63,15 @@ public GraphResponse newUserRequest(Provisioning provisioning, User user) { try { com.microsoft.graph.models.Invitation newInvitation = buildRequest.post(invitation); - LOG.info(String.format("Response from graph endpoint for user %s, inviteRedeemUrl: %s", + String invitationJson = objectMapper.writeValueAsString(newInvitation); + + LOG.info(String.format("Response from graph endpoint for user %s, inviteRedeemUrl: %s, json: %s", user.getEmail(), - newInvitation.inviteRedeemUrl + newInvitation.inviteRedeemUrl, + invitationJson )); return new GraphResponse(newInvitation.invitedUser.id, newInvitation.inviteRedeemUrl); - } catch (ClientException e) { + } catch (ClientException | IOException e) { String errorMessage = String.format("Error Graph request (entityID %s) to %s for user %s", provisioning.getEntityId(), graphUrl, diff --git a/server/src/main/java/access/security/CustomOidcUserService.java b/server/src/main/java/access/security/CustomOidcUserService.java index 741b6fd9..5bdeb9d8 100644 --- a/server/src/main/java/access/security/CustomOidcUserService.java +++ b/server/src/main/java/access/security/CustomOidcUserService.java @@ -2,6 +2,7 @@ import access.manage.Manage; import access.model.User; +import access.provision.ProvisioningService; import access.repository.UserRepository; import lombok.Getter; import org.springframework.security.oauth2.client.oidc.userinfo.OidcUserRequest; @@ -23,14 +24,20 @@ public class CustomOidcUserService implements OAuth2UserService { - user.updateAttributes(newClaims); + boolean changed = user.updateAttributes(newClaims); + if (changed) { + provisioningService.updateUserRequest(user); + } userRepository.save(user); }); OidcUserInfo oidcUserInfo = new OidcUserInfo(newClaims); diff --git a/server/src/main/java/access/security/SecurityConfig.java b/server/src/main/java/access/security/SecurityConfig.java index 1e0ee14d..58588267 100644 --- a/server/src/main/java/access/security/SecurityConfig.java +++ b/server/src/main/java/access/security/SecurityConfig.java @@ -2,6 +2,7 @@ import access.exception.ExtendedErrorAttributes; import access.manage.Manage; +import access.provision.ProvisioningService; import access.repository.APITokenRepository; import access.repository.InvitationRepository; import access.repository.UserRepository; @@ -55,6 +56,7 @@ public class SecurityConfig { private final String secret; private final ClientRegistrationRepository clientRegistrationRepository; private final InvitationRepository invitationRepository; + private final ProvisioningService provisioningService; private final String vootUser; private final String vootPassword; private final String attributeAggregationUser; @@ -67,6 +69,7 @@ public class SecurityConfig { @Autowired public SecurityConfig(ClientRegistrationRepository clientRegistrationRepository, InvitationRepository invitationRepository, + ProvisioningService provisioningService, @Value("${config.eduid-entity-id}") String eduidEntityId, @Value("${oidcng.introspect-url}") String introspectionUri, @Value("${oidcng.resource-server-id}") String clientId, @@ -81,6 +84,7 @@ public SecurityConfig(ClientRegistrationRepository clientRegistrationRepository, @Value("${attribute-aggregation.password}") String attributeAggregationPassword) { this.clientRegistrationRepository = clientRegistrationRepository; this.invitationRepository = invitationRepository; + this.provisioningService = provisioningService; this.eduidEntityId = eduidEntityId; this.introspectionUri = introspectionUri; this.clientId = clientId; @@ -165,7 +169,7 @@ SecurityFilterChain sessionSecurityFilterChain(HttpSecurity http, authorizationRequestResolver(this.clientRegistrationRepository) ) ).userInfoEndpoint(userInfoEndpointConfigurer -> userInfoEndpointConfigurer.oidcUserService( - new CustomOidcUserService(manage, userRepository, entitlement, organizationGuidPrefix))) + new CustomOidcUserService(manage, userRepository, provisioningService, entitlement, organizationGuidPrefix))) ) //We need a reference to the securityContextRepository to update the authentication after an InstitutionAdmin invitation accept .securityContext(securityContextConfigurer -> diff --git a/server/src/test/java/access/AbstractTest.java b/server/src/test/java/access/AbstractTest.java index 66867f0b..e52216bd 100644 --- a/server/src/test/java/access/AbstractTest.java +++ b/server/src/test/java/access/AbstractTest.java @@ -397,13 +397,13 @@ protected void stubForDeleteScimUser() { } protected void stubForDeleteEvaUser() { - stubFor(post(urlPathMatching(String.format("/eva/api/v1/guest/disable/(.*)"))) + stubFor(post(urlPathMatching("/eva/api/v1/guest/disable/(.*)")) .willReturn(aResponse() .withStatus(201))); } protected void stubForDeleteGraphUser() { - stubFor(delete(urlPathMatching(String.format("/graph/users"))) + stubFor(delete(urlPathMatching("/graph/users")) .willReturn(aResponse() .withStatus(201))); } @@ -417,7 +417,7 @@ protected void stubForDeleteScimRole() { protected String stubForCreateScimRole() throws JsonProcessingException { String value = UUID.randomUUID().toString(); String body = objectMapper.writeValueAsString(Map.of("id", value)); - stubFor(post(urlPathMatching(String.format("/api/scim/v2/groups"))) + stubFor(post(urlPathMatching("/api/scim/v2/groups")) .willReturn(aResponse() .withHeader("Content-Type", "application/json") .withBody(body))); @@ -427,7 +427,7 @@ protected String stubForCreateScimRole() throws JsonProcessingException { protected String stubForCreateScimUser() throws JsonProcessingException { String value = UUID.randomUUID().toString(); String body = objectMapper.writeValueAsString(Map.of("id", value)); - stubFor(post(urlPathMatching(String.format("/api/scim/v2/users"))) + stubFor(post(urlPathMatching("/api/scim/v2/users")) .willReturn(aResponse() .withHeader("Content-Type", "application/json") .withBody(body))); @@ -437,7 +437,7 @@ protected String stubForCreateScimUser() throws JsonProcessingException { protected String stubForCreateEvaUser() throws JsonProcessingException { String value = UUID.randomUUID().toString(); String body = objectMapper.writeValueAsString(Map.of("id", value)); - stubFor(post(urlPathMatching(String.format("/eva/api/v1/guest/create"))) + stubFor(post(urlPathMatching("/eva/api/v1/guest/create")) .willReturn(aResponse() .withHeader("Content-Type", "application/json") .withBody(body))); @@ -452,13 +452,20 @@ protected String stubForCreateGraphUser() throws JsonProcessingException { "inviteRedeemUrl", "https://www.google.com" )); - stubFor(post(urlPathMatching(String.format("/graph/users"))) + stubFor(post(urlPathMatching("/graph/users")) .willReturn(aResponse() .withHeader("Content-Type", "application/json") .withBody(body))); return value; } + protected void stubForUpdateScimUser() { + stubFor(put(urlPathMatching("/api/scim/v2/users/(.*)")) + .willReturn(aResponse() + .withHeader("Content-Type", "application/json") + )); + } + protected void stubForUpdateGraphUser(String sub) throws JsonProcessingException { User user = userRepository.findBySubIgnoreCase(sub).get(); String remoteIdentifier = UUID.randomUUID().toString(); @@ -466,24 +473,24 @@ protected void stubForUpdateGraphUser(String sub) throws JsonProcessingException String body = objectMapper.writeValueAsString(Map.of( "id", remoteIdentifier )); - stubFor(get(urlPathMatching(String.format("/graph/users"))) + stubFor(get(urlPathMatching("/graph/users")) .willReturn(aResponse() .withHeader("Content-Type", "application/json") .withBody(body))); - stubFor(patch(urlPathMatching(String.format("/graph/users"))) + stubFor(patch(urlPathMatching("/graph/users")) .willReturn(aResponse() .withHeader("Content-Type", "application/json") .withBody(body))); } - protected void stubForUpdateScimRole() throws JsonProcessingException { - stubFor(put(urlPathMatching(String.format("/api/scim/v2/groups/(.*)"))) + protected void stubForUpdateScimRole() { + stubFor(put(urlPathMatching("/api/scim/v2/groups/(.*)")) .willReturn(aResponse() .withHeader("Content-Type", "application/json") )); } - protected void stubForUpdateScimRolePatch() throws JsonProcessingException { + protected void stubForUpdateScimRolePatch() { stubFor(patch(urlPathMatching(String.format("/api/scim/v2/groups/(.*)"))) .willReturn(aResponse() .withHeader("Content-Type", "application/json") diff --git a/server/src/test/java/access/api/InvitationControllerTest.java b/server/src/test/java/access/api/InvitationControllerTest.java index e529aa43..842e9cd2 100644 --- a/server/src/test/java/access/api/InvitationControllerTest.java +++ b/server/src/test/java/access/api/InvitationControllerTest.java @@ -59,6 +59,8 @@ void getInvitationAlreadyAccepted() { @Test void newInvitation() throws Exception { + //Because the user is changed and provisionings are queried + stubForManageProvisioning(List.of()); AccessCookieFilter accessCookieFilter = openIDConnectFlow("/api/v1/users/login", MANAGE_SUB); stubForManageProviderById(EntityType.SAML20_SP, "1"); @@ -92,6 +94,8 @@ void newInvitation() throws Exception { @Test void newInvitationEmptyRoles() throws Exception { + //Because the user is changed and provisionings are queried + stubForManageProvisioning(List.of()); AccessCookieFilter accessCookieFilter = openIDConnectFlow("/api/v1/users/login", MANAGE_SUB); InvitationRequest invitationRequest = new InvitationRequest( @@ -185,13 +189,14 @@ void acceptForUpgradingExistingUserRole() throws Exception { .findFirst().get().getAuthority(); assertEquals(Authority.GUEST, authority); + //Because the user is changed and provisionings are queried + stubForManageProvisioning(List.of()); AccessCookieFilter accessCookieFilter = openIDConnectFlow("/api/v1/users/login", GUEST_SUB); String hash = Authority.MANAGER.name(); Invitation invitation = invitationRepository.findByHash(hash).get(); stubForManageProvisioning(List.of("5")); stubForCreateScimUser(); - AcceptInvitation acceptInvitation = new AcceptInvitation(hash, invitation.getId()); given() .when() diff --git a/server/src/test/java/access/api/RoleControllerTest.java b/server/src/test/java/access/api/RoleControllerTest.java index 0850ce0c..7732bb0d 100644 --- a/server/src/test/java/access/api/RoleControllerTest.java +++ b/server/src/test/java/access/api/RoleControllerTest.java @@ -25,6 +25,8 @@ class RoleControllerTest extends AbstractTest { @Test void create() throws Exception { + //Because the user is changed and provisionings are queried + stubForManageProvisioning(List.of()); AccessCookieFilter accessCookieFilter = openIDConnectFlow("/api/v1/users/login", MANAGE_SUB); Role role = new Role("New", "New desc", application("1", EntityType.SAML20_SP), 365, false, false); @@ -46,6 +48,8 @@ void create() throws Exception { @Test void createInvalidLandingPage() throws Exception { + //Because the user is changed and provisionings are queried + stubForManageProvisioning(List.of()); AccessCookieFilter accessCookieFilter = openIDConnectFlow("/api/v1/users/login", MANAGE_SUB); Role role = new Role("New", "New desc", application("1", EntityType.SAML20_SP), 365, false, false); @@ -63,6 +67,8 @@ void createInvalidLandingPage() throws Exception { @Test void createProvisionException() throws Exception { + //Because the user is changed and provisionings are queried + stubForManageProvisioning(List.of()); AccessCookieFilter accessCookieFilter = openIDConnectFlow("/api/v1/users/login", MANAGE_SUB); Role role = new Role("New", "New desc", application("1", EntityType.SAML20_SP), 365, false, false); super.stubForManagerProvidersByIdIn(EntityType.SAML20_SP, List.of("1")); @@ -82,7 +88,10 @@ void createProvisionException() throws Exception { @Test void update() throws Exception { + //Because the user is changed and provisionings are queried + stubForManageProvisioning(List.of()); AccessCookieFilter accessCookieFilter = openIDConnectFlow("/api/v1/users/login", MANAGE_SUB); + super.stubForManagerProvidersByIdIn(EntityType.SAML20_SP, List.of("1")); super.stubForManageProvisioning(List.of("1")); Role roleDB = roleRepository.search("wiki", 1).get(0); @@ -104,6 +113,8 @@ void update() throws Exception { @Test void updateApplications() throws Exception { + //Because the user is changed and provisionings are queried + stubForManageProvisioning(List.of()); AccessCookieFilter accessCookieFilter = openIDConnectFlow("/api/v1/users/login", MANAGE_SUB); super.stubForManagerProvidersByIdIn(EntityType.SAML20_SP, List.of("1", "2", "4")); @@ -131,7 +142,10 @@ void updateApplications() throws Exception { @Test void rolesByApplication() throws Exception { + //Because the user is changed and provisionings are queried + stubForManageProvisioning(List.of()); AccessCookieFilter accessCookieFilter = openIDConnectFlow("/api/v1/users/login", MANAGE_SUB); + super.stubForManagerProvidersByIdIn(EntityType.SAML20_SP, List.of("1")); List roles = given() @@ -188,7 +202,10 @@ void rolesByApplicationSuperUser() throws Exception { @Test void roleById() throws Exception { + //Because the user is changed and provisionings are queried + stubForManageProvisioning(List.of()); AccessCookieFilter accessCookieFilter = openIDConnectFlow("/api/v1/users/login", MANAGE_SUB); + Role roleDB = roleRepository.search("wiki", 1).get(0); super.stubForManagerProvidersByIdIn(EntityType.SAML20_SP, List.of("1")); Role role = given() @@ -207,7 +224,10 @@ void roleById() throws Exception { @Test void deleteRole() throws Exception { + //Because the user is changed and provisionings are queried + stubForManageProvisioning(List.of()); AccessCookieFilter accessCookieFilter = openIDConnectFlow("/api/v1/users/login", MANAGE_SUB); + Role role = roleRepository.search("wiki", 1).get(0); //Ensure delete provisioning is done remoteProvisionedGroupRepository.save(new RemoteProvisionedGroup(role, UUID.randomUUID().toString(), "7")); @@ -250,6 +270,8 @@ void search() throws Exception { @Test void roleByIdForbidden() throws Exception { + //Because the user is changed and provisionings are queried + stubForManageProvisioning(List.of()); AccessCookieFilter accessCookieFilter = openIDConnectFlow("/api/v1/users/login", MANAGE_SUB); Role roleDB = roleRepository.search("research", 1).get(0); super.stubForManageProviderById(EntityType.SAML20_SP, "4"); @@ -263,7 +285,6 @@ void roleByIdForbidden() throws Exception { .get("/api/v1/roles/{id}") .then() .statusCode(403); - } @Test diff --git a/server/src/test/java/access/api/UserControllerTest.java b/server/src/test/java/access/api/UserControllerTest.java index 4545da35..56031f98 100644 --- a/server/src/test/java/access/api/UserControllerTest.java +++ b/server/src/test/java/access/api/UserControllerTest.java @@ -5,8 +5,10 @@ import access.exception.NotFoundException; import access.manage.EntityType; import access.model.Authority; +import access.model.RemoteProvisionedUser; import access.model.User; import access.model.UserRole; +import com.github.tomakehurst.wiremock.verification.LoggedRequest; import io.restassured.common.mapper.TypeRef; import io.restassured.http.ContentType; import org.junit.jupiter.api.Test; @@ -19,6 +21,7 @@ import java.util.List; import java.util.Map; import java.util.Set; +import java.util.UUID; import java.util.stream.Stream; import static access.AbstractTest.*; @@ -217,6 +220,8 @@ void meWithImpersonationInstitutionAdmin() throws Exception { @Test void meWithNotAllowedImpersonation() throws Exception { + super.stubForManageProvisioning(List.of("1", "5")); + AccessCookieFilter accessCookieFilter = openIDConnectFlow("/api/v1/users/login", MANAGE_SUB); User guest = userRepository.findBySubIgnoreCase(GUEST_SUB).get(); @@ -378,4 +383,25 @@ void error() throws Exception { .then() .statusCode(201); } + + @Test + void meUpdateScim() throws Exception { + User user = userRepository.findBySubIgnoreCase(GUEST_SUB).get(); + String remoteScimIdentifier = UUID.randomUUID().toString(); + RemoteProvisionedUser remoteProvisionedUser = new RemoteProvisionedUser(user, remoteScimIdentifier,"7"); + remoteProvisionedUserRepository.save(remoteProvisionedUser); + + super.stubForManageProvisioning(List.of("1", "4", "5")); + super.stubForUpdateScimUser(); + + //This will trigger the SCIM update request, see CustomOidcUserService#loadUser + openIDConnectFlow("/api/v1/users/login", GUEST_SUB); + + List loggedRequests = findAll(putRequestedFor(urlPathMatching("/api/scim/v2/users/(.*)"))); + + assertEquals(1, loggedRequests.size()); + Map userRequest = objectMapper.readValue(loggedRequests.get(0).getBodyAsString(), Map.class); + assertEquals(remoteScimIdentifier, userRequest.get("id")); + } + } \ No newline at end of file diff --git a/server/src/test/java/access/api/UserRoleControllerTest.java b/server/src/test/java/access/api/UserRoleControllerTest.java index b9607b3b..ff79646c 100644 --- a/server/src/test/java/access/api/UserRoleControllerTest.java +++ b/server/src/test/java/access/api/UserRoleControllerTest.java @@ -22,7 +22,10 @@ class UserRoleControllerTest extends AbstractTest { @Test void byRole() throws Exception { + //Because the user is changed and provisionings are queried + stubForManageProvisioning(List.of()); AccessCookieFilter accessCookieFilter = openIDConnectFlow("/api/v1/users/login", MANAGE_SUB); + Role role = roleRepository.search("wiki", 1).get(0); List userRoles = given() .when() @@ -39,7 +42,10 @@ void byRole() throws Exception { @Test void updateEndDate() throws Exception { + //Because the user is changed and provisionings are queried + stubForManageProvisioning(List.of()); AccessCookieFilter accessCookieFilter = openIDConnectFlow("/api/v1/users/login", MANAGE_SUB); + UserRole userRole = userRoleRepository.findByRoleName("Wiki").stream() .filter(userRole1 -> userRole1.getAuthority().equals(Authority.GUEST)) .findFirst() @@ -66,7 +72,10 @@ void updateEndDate() throws Exception { @Test void updateEndDateInThePast() throws Exception { + //Because the user is changed and provisionings are queried + stubForManageProvisioning(List.of()); AccessCookieFilter accessCookieFilter = openIDConnectFlow("/api/v1/users/login", MANAGE_SUB); + UserRole userRole = userRoleRepository.findByRoleName("Wiki").stream() .filter(userRole1 -> userRole1.getAuthority().equals(Authority.GUEST)) .findFirst() @@ -85,7 +94,10 @@ void updateEndDateInThePast() throws Exception { @Test void deleteUserRole() throws Exception { + //Because the user is changed and provisionings are queried + stubForManageProvisioning(List.of()); AccessCookieFilter accessCookieFilter = openIDConnectFlow("/api/v1/users/login", MANAGE_SUB); + List userRoles = userRoleRepository.findByRoleName("Wiki"); UserRole guestUserRole = userRoles.stream().filter(userRole -> userRole.getAuthority().equals(Authority.GUEST)).findFirst().get(); given() diff --git a/server/src/test/java/access/provision/ProvisioningServiceDefaultTest.java b/server/src/test/java/access/provision/ProvisioningServiceDefaultTest.java index b0311d68..5fadc125 100644 --- a/server/src/test/java/access/provision/ProvisioningServiceDefaultTest.java +++ b/server/src/test/java/access/provision/ProvisioningServiceDefaultTest.java @@ -3,11 +3,14 @@ import access.AbstractTest; import access.model.*; import access.provision.scim.OperationType; +import access.provision.scim.UserRequest; import com.fasterxml.jackson.core.JsonProcessingException; +import com.github.tomakehurst.wiremock.verification.LoggedRequest; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; - +import static com.github.tomakehurst.wiremock.client.WireMock.*; import java.util.List; +import java.util.Map; import java.util.UUID; import static access.AbstractTest.GUEST_SUB; @@ -30,6 +33,23 @@ void newUserRequest() throws JsonProcessingException { assertEquals(remoteScimIdentifier, remoteProvisionedUsers.get(0).getRemoteIdentifier()); } + @Test + void updateUserRequest() throws JsonProcessingException { + User user = userRepository.findBySubIgnoreCase(GUEST_SUB).get(); + //Need to ensure the user is updated therefore the remote needs to exists and provisioning is scimn + String remoteScimIdentifier = UUID.randomUUID().toString(); + RemoteProvisionedUser remoteProvisionedUser = new RemoteProvisionedUser(user, remoteScimIdentifier,"7"); + remoteProvisionedUserRepository.save(remoteProvisionedUser); + this.stubForManageProvisioning(List.of("1", "4", "5")); + this.stubForUpdateScimUser(); + provisioningService.updateUserRequest(user); + List loggedRequests = findAll(putRequestedFor(urlPathMatching(String.format("/api/scim/v2/users/(.*)")))); + + assertEquals(1, loggedRequests.size()); + Map userRequest = objectMapper.readValue(loggedRequests.get(0).getBodyAsString(), Map.class); + assertEquals(remoteScimIdentifier, userRequest.get("id")); + } + @Test void deleteUserRequest() throws JsonProcessingException { User user = userRepository.findBySubIgnoreCase(GUEST_SUB).get(); diff --git a/server/src/test/java/access/provision/graph/GraphClientTest.java b/server/src/test/java/access/provision/graph/GraphClientTest.java index a6f48409..79b92625 100644 --- a/server/src/test/java/access/provision/graph/GraphClientTest.java +++ b/server/src/test/java/access/provision/graph/GraphClientTest.java @@ -6,11 +6,10 @@ import access.manage.LocalManage; import access.model.User; import access.provision.Provisioning; -import crypto.KeyStore; +import com.fasterxml.jackson.databind.ObjectMapper; import crypto.RSAKeyStore; import org.junit.jupiter.api.Test; -import java.security.NoSuchAlgorithmException; import java.util.Map; import static org.junit.jupiter.api.Assertions.assertThrows; @@ -18,7 +17,10 @@ //We test the non-happy paths here and the happy-paths through the controllers class GraphClientTest { - final GraphClient graphClient = new GraphClient("http://localhost:8888", "test.eduid.nl", new RSAKeyStore()); + final GraphClient graphClient = new GraphClient("http://localhost:8888", + "test.eduid.nl", + new RSAKeyStore(), + new ObjectMapper()); final LocalManage localManage = new LocalManage( ObjectMapperHolder.objectMapper, false); @Test diff --git a/server/src/test/java/access/teams/TeamsControllerTest.java b/server/src/test/java/access/teams/TeamsControllerTest.java index 9ea7b98a..838d278a 100644 --- a/server/src/test/java/access/teams/TeamsControllerTest.java +++ b/server/src/test/java/access/teams/TeamsControllerTest.java @@ -81,12 +81,13 @@ void migrateTeam() throws JsonProcessingException { }); //Now check if we get the correct URN from the Voot interface + Membership harryDoe = memberships.stream().filter(m -> m.getPerson().getName().equals("Harry Doe")).findFirst().get(); List> groups = given() .when() .auth().basic("voot", "secret") .accept(ContentType.JSON) .contentType(ContentType.JSON) - .pathParam("sub", memberships.get(0).getPerson().getUrn()) + .pathParam("sub", harryDoe.getPerson().getUrn()) .get("/api/voot/{sub}") .as(new TypeRef<>() { });