Skip to content

Commit

Permalink
SCIM updates
Browse files Browse the repository at this point in the history
  • Loading branch information
oharsta committed Apr 10, 2024
1 parent c58879f commit 441edb6
Show file tree
Hide file tree
Showing 15 changed files with 193 additions and 39 deletions.
2 changes: 1 addition & 1 deletion server/src/main/java/access/api/UserController.java
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand Down
39 changes: 32 additions & 7 deletions server/src/main/java/access/model/User.java
Original file line number Diff line number Diff line change
Expand Up @@ -178,7 +178,7 @@ public void removeUserRole(UserRole role) {
@JsonIgnore
public Set<ManageIdentifier> 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()))
Expand All @@ -199,16 +199,41 @@ public Map<String, Object> asMap() {
}

@JsonIgnore
public void updateAttributes(Map<String, Object> 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<String, Object> 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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@ public interface ProvisioningService {

Optional<GraphResponse> newUserRequest(User user);

void updateUserRequest(User user);

void deleteUserRequest(User user);

void newGroupRequest(Role role);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand All @@ -86,7 +86,6 @@ public ProvisioningServiceDefault(UserRoleRepository userRoleRepository,
}

@Override
@SneakyThrows
public Optional<GraphResponse> newUserRequest(User user) {
List<Provisioning> provisionings = getProvisionings(user);
AtomicReference<GraphResponse> graphResponseReference = new AtomicReference<>();
Expand All @@ -109,7 +108,23 @@ public Optional<GraphResponse> newUserRequest(User user) {
}

@Override
@SneakyThrows
public void updateUserRequest(User user) {
List<Provisioning> userProvisionings = getProvisionings(user);
List<Provisioning> 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<RemoteProvisionedUser> 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()
Expand Down Expand Up @@ -306,7 +321,6 @@ private String patchGroupRequest(Role role,
return prettyJson(request);
}

@SneakyThrows
private Optional<ProvisioningResponse> newRequest(Provisioning provisioning, String request, Provisionable provisionable) {
boolean isUser = provisionable instanceof User;
String apiType = isUser ? USER_API : GROUP_API;
Expand Down Expand Up @@ -334,7 +348,6 @@ private Optional<ProvisioningResponse> newRequest(Provisioning provisioning, Str

}

@SneakyThrows
private void updateRequest(Provisioning provisioning,
String request,
String apiType,
Expand Down Expand Up @@ -362,7 +375,6 @@ private List<String> getManageIdentifiers(Role role) {
return role.applicationsUsed().stream().map(Application::getManageId).distinct().sorted().toList();
}

@SneakyThrows
private void deleteRequest(Provisioning provisioning,
String request,
Provisionable provisionable,
Expand Down
15 changes: 11 additions & 4 deletions server/src/main/java/access/provision/graph/GraphClient.java
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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 {
Expand All @@ -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")
Expand All @@ -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,
Expand Down
14 changes: 12 additions & 2 deletions server/src/main/java/access/security/CustomOidcUserService.java
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -23,14 +24,20 @@ public class CustomOidcUserService implements OAuth2UserService<OidcUserRequest,

@Getter
private final Manage manage;
private final ProvisioningService provisioningService;
private final UserRepository userRepository;
private final String entitlement;
private final String organizationGuidPrefix;
private final OidcUserService delegate;

public CustomOidcUserService(Manage manage, UserRepository userRepository, String entitlement, String organizationGuidPrefix) {
public CustomOidcUserService(Manage manage,
UserRepository userRepository,
ProvisioningService provisioningService,
String entitlement,
String organizationGuidPrefix) {
this.manage = manage;
this.userRepository = userRepository;
this.provisioningService = provisioningService;
this.entitlement = entitlement;
this.organizationGuidPrefix = organizationGuidPrefix;
delegate = new OidcUserService();
Expand Down Expand Up @@ -59,7 +66,10 @@ public OidcUser loadUser(OidcUserRequest userRequest) throws OAuth2Authenticatio
newClaims.putAll(manageClaims);
}
optionalUser.ifPresent(user -> {
user.updateAttributes(newClaims);
boolean changed = user.updateAttributes(newClaims);
if (changed) {
provisioningService.updateUserRequest(user);
}
userRepository.save(user);
});
OidcUserInfo oidcUserInfo = new OidcUserInfo(newClaims);
Expand Down
6 changes: 5 additions & 1 deletion server/src/main/java/access/security/SecurityConfig.java
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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;
Expand All @@ -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,
Expand All @@ -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;
Expand Down Expand Up @@ -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 ->
Expand Down
29 changes: 18 additions & 11 deletions server/src/test/java/access/AbstractTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -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)));
}
Expand All @@ -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)));
Expand All @@ -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)));
Expand All @@ -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)));
Expand All @@ -452,38 +452,45 @@ 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();
remoteProvisionedUserRepository.save(new RemoteProvisionedUser(user, remoteIdentifier, "9"));
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")
Expand Down
Loading

0 comments on commit 441edb6

Please sign in to comment.