ConnectionProviderConverter.java
package access.manage;
import access.model.Application;
import access.model.ConnectOptions;
import access.model.Connection;
import access.model.EntityType;
import access.model.State;
import access.model.Visibility;
import access.repository.UserRepository;
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.util.StringUtils;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.Set;
import java.util.stream.Collectors;
import java.util.stream.IntStream;
import static access.api.ConnectionController.SECRET_LENGTH;
import static access.manage.ManageData.*;
@SuppressWarnings("unchecked")
public class ConnectionProviderConverter {
private final List<Map<String, Object>> privacyInfo;
private final List<String> excludedARPAttributes;
private final List<String> excludedAttributes = List.of("revisionnote");
private final List<String> excludedMergeAttributesPaths = List.of("arp.attributes");
private final State defaultState;
private final ObjectMapper objectMapper;
@SneakyThrows
public ConnectionProviderConverter(ObjectMapper objectMapper, State defaultState) {
this.defaultState = defaultState;
this.objectMapper = objectMapper;
InputStream inputStream = new ClassPathResource("/metadata/Privacy.json").getInputStream();
this.privacyInfo = objectMapper.readValue(inputStream, new TypeReference<>() {
});
Map<String, List<Map<String, Object>>> arpInfo = objectMapper.readValue(new ClassPathResource("/metadata/ARP.json").getInputStream(), new TypeReference<>() {
});
Set<String> profileAttributes = arpInfo.get("profiles").stream()
.map(m -> {
List<String> attributes = (List) m.get("attributes");
List<String> optionalAttributes = (List) m.get("optionalAttributes");
attributes.addAll(optionalAttributes);
return attributes;
})
.flatMap(List::stream)
.collect(Collectors.toSet());
excludedARPAttributes = arpInfo.get("attributes").stream()
.filter(m -> !profileAttributes.contains((String) m.get("name")))
.map(m -> (String) m.get("urn"))
.toList();
}
public Map<String, Object> convert(Connection connection,
Map<String, Object> remoteProvider,
boolean changeRequestRequired) {
Application application = connection.getApplication();
//We need data both from the connection and the application
Map<String, Object> connectionMetaData = connection.getMetaData();
Map<String, Object> applicationMetaData = application.getMetaData();
Map<String, Object> information = (Map<String, Object>) applicationMetaData.getOrDefault("information", Map.of());
//Base structure
Map<String, Object> data = getData(remoteProvider);
Map<String, Object> metaDataFields = getMetaDataFields(data);
//Now copy all information from the connection to the data / metadata
putIf(remoteProvider, "id", connection.getManageIdentifier());
remoteProvider.put("type", connection.getProtocol().name());
putIf(remoteProvider, "eid", connection.getManageEid());
data.put("entityid", connectionMetaData.get("entityID"));
//Use connection's state if set, otherwise fall back to defaultState
State connectionState = connection.getState();
data.put("state", connectionState != null ? connectionState.name() : defaultState.name());
metaDataFields.put("name:en", connection.getName());
metaDataFields.put("name:nl", connection.getName());
putIf(metaDataFields, "logo:0:url", application.getLogoUrl());
putIf(metaDataFields, "coin:application_name", application.getName());
putIf(metaDataFields, "description:en", information.get("descriptionEN"));
putIf(metaDataFields, "description:nl", information.get("descriptionNL"));
putIf(metaDataFields, "coin:application_url", information.get("webSite"));
List<String> tags = (List<String>) information.getOrDefault("tags", List.of());
putIf(metaDataFields, "application_tags", tags);
convertContactPersons(applicationMetaData, metaDataFields);
Map<String, String> privacy = (Map<String, String>) applicationMetaData.getOrDefault("privacy", Map.of());
privacyInfo.forEach(item -> putIf(metaDataFields, (String) item.get("manage"), privacy.get(item.get("name"))));
metaDataFields.put("OrganizationName:en", application.getOrganization().getName());
data.put("allowedall", false);
data.put("revisionnote", "SURF Access update with remote API");
//We have merged everything from the application now, stop if changeRequestRequired
if (changeRequestRequired) {
return remoteProvider;
}
mergeAttributeReleasePolicies(connectionMetaData, data);
mergeAllowedEntities(data, connectionMetaData);
if (EntityType.oidc10_rp.equals(connection.getProtocol())) {
List<String> grantTypes = (List<String>) connectionMetaData.get("grantTypes");
putIf(metaDataFields, "grants", grantTypes);
putIf(metaDataFields, "redirectUrls", connectionMetaData.get("redirectUrls"));
metaDataFields.put("isPublicClient", connectionMetaData.getOrDefault("pkce", false));
metaDataFields.put("oidc:claims_in_id_token", connectionMetaData.getOrDefault("claimsInIdToken", false));
metaDataFields.put("accessTokenValidity", 3600);
if (grantTypes.contains("refresh_token")) {
metaDataFields.put("refreshTokenValidity", connectionMetaData.getOrDefault("refreshTokenValidity", 3600));
}
String secret = (String) connectionMetaData.get("secret");
//Might be an initial secret or a deliberate reset
if (StringUtils.hasText(secret) && secret.length() == SECRET_LENGTH) {
putIf(metaDataFields, "secret", connectionMetaData.get("secret"));
} else {
putIf(metaDataFields, "secret", metaDataFields.get("secret"));
}
}
if (EntityType.saml20_sp.equals(connection.getProtocol())) {
List<String> acsLocations = (List<String>) connectionMetaData.getOrDefault("acsLocations", Collections.emptyList());
IntStream.range(0, acsLocations.size()).forEach(i -> {
metaDataFields.put("AssertionConsumerService:" + i + ":Location", acsLocations.get(i));
metaDataFields.put("AssertionConsumerService:" + i + ":Binding", "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST");
});
}
String visibility = (String) connectionMetaData.get("visibility");
metaDataFields.put("coin:ss:idp_visible_only", Visibility.visible_to_idp_only.name().equals(visibility));
metaDataFields.put("coin:ss:hidden", Visibility.visible_to_none.name().equals(visibility));
String loginUrl = (String) connectionMetaData.getOrDefault("loginUrl", information.get("webSite"));
metaDataFields.put("coin:login_url", loginUrl);
String connectOption = (String) connectionMetaData.getOrDefault("connectOption", ConnectOptions.connect_with_interaction.name());
metaDataFields.put("coin:dashboard_connect_option", connectOption);
return remoteProvider;
}
public void convertContactPersons(Map<String, Object> applicationMetaData, Map<String, Object> metaDataFields) {
List<Map<String, String>> contactPersons = ((List<Map<String, String>>) applicationMetaData
.getOrDefault("contactPersons", Collections.emptyList()))
.stream()
.filter(m -> StringUtils.hasText(m.get("email")))
.toList();
//First, delete all contact persons before adding them again
metaDataFields.keySet().removeIf(key -> key.startsWith("contacts:"));
IntStream.range(0, contactPersons.size()).forEach(i -> {
Map<String, String> contactPerson = contactPersons.get(i);
Map.of("type", "contactType",
"email", "emailAddress",
"givenName", "givenName",
"surName", "surName")
.forEach((k, v) -> putIf(metaDataFields, "contacts:" + i + ":" + v, contactPerson.get(k)));
});
}
//For all attributes that have been changed, we create a single ChangeRequest
public Optional<ChangeRequest> deduceChangeRequests(Connection connection,
Map<String, Object> currentProvider) {
//We need to compare maps, the current data in Manage (e.g. currentProvider) and the new Data in Connection
//So we need to convert the connection in to the new Map, without modifying the originalMap
Map clonedProvider = deepClone(currentProvider);
Map<String, Object> newProvider = this.convert(connection, clonedProvider, false);
Map<String, Object> newData = getData(newProvider);
Map<String, Object> currentData = getData(currentProvider);
Map<String, Object> pathUpdates = new LinkedHashMap<>();
diffChangeRequestRecursive("", currentData, newData, pathUpdates);
if (pathUpdatesWarrantChangeRequest(pathUpdates)) {
ChangeRequest changeRequest = new ChangeRequest(
connection.getManageIdentifier(),
connection.getProtocol(),
pathUpdates,
false,
null,
RequestType.Change);
return Optional.of(changeRequest);
}
return Optional.empty();
}
private boolean pathUpdatesWarrantChangeRequest(Map<String, Object> pathUpdates) {
return !pathUpdates.isEmpty() &&
(pathUpdates.size() != 1 || !pathUpdates.containsKey("revisionnote"));
}
public HashMap<String, Object> convertProviderToApplicationMetaData(Map<String, Object> provider) {
Map<String, Object> metaDataFields = getMetaDataFields(getData(provider));
HashMap<String, Object> updatedMetaData = new HashMap<>();
updatedMetaData.put("privacy", this.privacyInfo.stream().collect(Collectors.toMap(
m -> m.get("name"),
m -> metaDataFields.getOrDefault(m.get("manage"), "")
)));
Map<String, Object> information = new HashMap<>();
information.put("tags", metaDataFields.getOrDefault("application_tags", new ArrayList<>()));
information.put("webSite", metaDataFields.get("coin:application_url"));
information.put("descriptionEN", metaDataFields.get("description:en"));
information.put("descriptionNL", metaDataFields.get("description:nl"));
updatedMetaData.put("information", information);
List<String> contactTypes = List.of("administrative", "technical", "support");
List<Map<String, Object>> contactPersons = metaDataFields
.entrySet()
.stream()
.filter(entry -> entry.getKey().endsWith(":contactType") &&
contactTypes.contains((String) entry.getValue()))
.collect(Collectors.toMap(
entry -> entry.getKey().split(":")[1],
entry -> (String) entry.getValue()
))
.entrySet().stream()
.map(entry -> Map.of(
"type", entry.getValue(),
"email", metaDataFields.getOrDefault("contacts:" + entry.getKey() + ":emailAddress", ""),
"surName", metaDataFields.getOrDefault("contacts:" + entry.getKey() + ":surName", ""),
"givenName", metaDataFields.getOrDefault("contacts:" + entry.getKey() + ":givenName", "")
))
.toList();
updatedMetaData.put("contactPersons", contactPersons);
return updatedMetaData;
}
@SneakyThrows
@SuppressWarnings("unchecked")
private Map deepClone(Map original) {
return this.objectMapper.convertValue(original, Map.class);
}
@SuppressWarnings("unchecked")
private void diffChangeRequestRecursive(String path,
Object oldVal,
Object newVal,
Map<String, Object> pathUpdates) {
if (Objects.equals(oldVal, newVal) || excludedAttributes.contains(path.toLowerCase())) {
return;
}
// both maps - recurse on union of keys if the name is not excluded
if (oldVal instanceof Map && newVal instanceof Map) {
Map<String, Object> oldMap = (Map<String, Object>) oldVal;
Map<String, Object> newMap = (Map<String, Object>) newVal;
Set<String> allKeys = new HashSet<>();
allKeys.addAll(oldMap.keySet());
allKeys.addAll(newMap.keySet());
for (String key : allKeys) {
String newPath = path.isEmpty() ? key : path + "." + key;
if (excludedMergeAttributesPaths.contains(newPath)) {
pathUpdates.put(path, newVal);
return;
}
diffChangeRequestRecursive(newPath, oldMap.get(key), newMap.get(key), pathUpdates);
}
return;
}
// from both lists take the new value
if (oldVal instanceof List && newVal instanceof List) {
pathUpdates.put(path, newVal);
return;
}
// if we get here, either type differs or one side is missing
if (oldVal == null && newVal != null) {
// added
pathUpdates.put(path, newVal);
} else if (oldVal != null && newVal == null) {
// removed
pathUpdates.put(path, null);
} else {
// changed
pathUpdates.put(path, newVal);
}
}
private void mergeAllowedEntities(Map<String, Object> data, Map<String, Object> connectionMetaData) {
List<String> allowedEntities = (List<String>) connectionMetaData.getOrDefault("allowedEntities", List.of());
data.put("allowedEntities", allowedEntities.stream().map(entity -> Map.of("name", entity)).toList());
}
private void mergeAttributeReleasePolicies(Map<String, Object> connectionMetaData, Map<String, Object> data) {
Map<String, Object> newArp = (Map<String, Object>) connectionMetaData.getOrDefault("arp", new HashMap<>());
Map<String, Object> arpFromManage = (Map<String, Object>) data.getOrDefault("arp", Map.of());
//Merge the two ARP's, ensuring no existing attributes are overridden which are in the excludedARPAttributes
Map<String, List<Map<String, String>>> existingArpAttributes = (Map<String, List<Map<String, String>>>) arpFromManage.getOrDefault("attributes", Map.of());
Map<String, List<Map<String, String>>> newArpAttributes = (Map<String, List<Map<String, String>>>) newArp.getOrDefault("attributes", Map.of());
existingArpAttributes.entrySet().stream()
.filter(entry -> excludedARPAttributes.contains(entry.getKey()))
.forEach(entry -> {
if (!newArpAttributes.containsKey(entry.getKey())) {
newArpAttributes.put(entry.getKey(), entry.getValue());
}
});
putIf(data, "arp", newArp);
}
private void putIf(Map<String, Object> result, String key, Object value) {
if (!isEmpty(value)) {
result.put(key, value);
}
}
}