Skip to content
Snippets Groups Projects
Verified Commit adae8542 authored by Dominik Frantisek Bucik's avatar Dominik Frantisek Bucik
Browse files

feat: :guitar: VoBasedEdupersonScopedAffiliationsClaimSource impl

Implementation of a claim source to generate EPSA values based on vo
membership. ClaimSource requires congiruation option 'valueMap', in the
format of 'voId:aff,aff|voId:aff,aff'. If the user is a valid member of
a VO with id 'voId', all affiliation values are added into the claim
value.
parent 709abd4f
Branches
Tags
1 merge request!394feat: 🎸 VoBasedEdupersonScopedAffiliationsClaimSource impl
Pipeline #432292 passed
......@@ -341,4 +341,6 @@ public interface PerunAdapterMethods {
PerunUser getPerunUser(Long userId);
Set<Long> getUserVoIds(Long userId);
}
......@@ -414,4 +414,17 @@ public class PerunAdapterImpl extends PerunAdapter {
}
}
@Override
public Set<Long> getUserVoIds(Long userId) {
try {
return this.getAdapterPrimary().getUserVoIds(userId);
} catch (UnsupportedOperationException e) {
if (this.isCallFallback()) {
return this.getAdapterFallback().getUserVoIds(userId);
} else {
throw e;
}
}
}
}
......@@ -531,6 +531,28 @@ public class PerunAdapterLdap extends PerunAdapterWithMappingServices implements
return getPerunUser(filter);
}
@Override
public Set<Long> getUserVoIds(Long userId) {
if (userId == null) {
throw new IllegalArgumentException("No userId");
}
SearchScope scope = SearchScope.ONELEVEL;
final String[] attributes = {PERUN_VO_ID};
String uniqueMember = getDnPrefixForUserId(userId) + ',' + this.connectorLdap.getBaseDN();
FilterBuilder filter = and(equal(UNIQUE_MEMBER, uniqueMember), equal(OBJECT_CLASS, PERUN_VO));
EntryMapper<Long> mapper = e -> {
if (!checkHasAttributes(e, attributes)) {
return null;
}
return Long.valueOf(e.get(PERUN_VO_ID).getString());
};
List<Long> voIds = connectorLdap.search(null, filter, scope, attributes, mapper);
return voIds.stream().filter(Objects::nonNull).collect(Collectors.toSet());
}
private PerunUser getPerunUser(FilterBuilder filter) {
SearchScope scope = SearchScope.ONELEVEL;
String[] attributes = new String[]{PERUN_USER_ID, GIVEN_NAME, SN};
......
......@@ -950,6 +950,23 @@ public class PerunAdapterRpc extends PerunAdapterWithMappingServices implements
return RpcMapper.mapPerunUser(response);
}
@Override
public Set<Long> getUserVoIds(Long userId) {
if (!this.connectorRpc.isEnabled()) {
return Collections.emptySet();
} else if (userId == null) {
throw new IllegalArgumentException("No userId");
}
List<Member> members = getMembersByUser(userId);
Set<Long> voIds = new HashSet<>();
for (Member member: members) {
if (VALID == member.getStatus()) {
voIds.add(member.getVoId());
}
}
return voIds;
}
private Member getMemberByUser(Long userId, Long voId) {
if (!this.connectorRpc.isEnabled()) {
return null;
......
......@@ -4,55 +4,59 @@ import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.node.ArrayNode;
import com.fasterxml.jackson.databind.node.JsonNodeFactory;
import com.fasterxml.jackson.dataformat.yaml.YAMLMapper;
import cz.muni.ics.oidc.exceptions.ConfigurationException;
import cz.muni.ics.oidc.server.claims.ClaimSource;
import cz.muni.ics.oidc.server.claims.ClaimSourceInitContext;
import cz.muni.ics.oidc.server.claims.ClaimSourceProduceContext;
import lombok.extern.slf4j.Slf4j;
import org.springframework.util.StringUtils;
import java.io.File;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
/**
* Claim source for eduperson_scoped_affiliations MUNI.
* Claim source for generating affiliation values based on VO membership(s).
*
* Configuration (replace [claimName] with the name of the claim):
* <ul>
* <li>
* <b>custom.claim.[claimName].source.config_file</b> - path to the YML config file, see
* 'eduperson_scoped_affiliations_mu_source.yml' for example configuration
* <b>custom.claim.[claimName].source.valueMap</b> - Mapping of voIds to affiliation values. Has to be specified
* in a format 'voId:aff,aff|voId:aff,aff', where 'voId' is an ID of the VO and 'aff' is the value of an
* affiliation to be added to the output if the user is a valid member of the respective VO with the specified
* identifier.
* </li>
* </ul>
*
* @author Dominik Baránek <baranek@ics.muni.cz>
* @author Dominik Frantisek Bucik <bucik@ics.muni.cz>
*/
@Slf4j
public class EdupersonScopedAffiliationsMUSource extends ClaimSource {
public class VoBasedEdupersonScopedAffiliationsClaimSource extends ClaimSource {
private static final String CONFIG_FILE = "config_file";
private static final String KEY_SCOPE = "scope";
private static final String KEY_VO_ID = "voId";
private static final String KEY_AFFILIATIONS = "affiliations";
private static final String KEY_VALUE = "value";
private static final String KEY_GROUPS = "groups";
private final Pattern epsaPattern = Pattern.compile(
"(member|student|faculty|staff|alum|affiliate|unknown|library-walk-in)@.+"
);
private static final String DEFAULT_PATH = "/etc/perun/eduperson_scoped_affiliations_mu_source.yml";
private static final String KEY_VALUE_MAP = "valueMap";
private final Map<List<Long>, String> affiliations = new HashMap<>();
private Long voId = 363L;
private String valueScope = "muni.cz";
private final Map<Long, Set<String>> voIdValuesMap = new HashMap<>();
public EdupersonScopedAffiliationsMUSource(ClaimSourceInitContext ctx) {
public VoBasedEdupersonScopedAffiliationsClaimSource(ClaimSourceInitContext ctx) {
super(ctx);
parseConfigFile(ctx.getProperty(CONFIG_FILE, DEFAULT_PATH));
log.debug("{} - affiliations: '{}', voId: '{}', valueScope: '{}'",
getClaimName(), affiliations, voId, valueScope);
String valueMapProp = ctx.getProperty(KEY_VALUE_MAP, null);
if (!StringUtils.hasText(valueMapProp)) {
throw new ConfigurationException("Invalid configuration for claim " + getClaimName() + ": valueMap must be provided");
}
voIdValuesMap.putAll(parseValueMap(valueMapProp));
log.debug("{} - voIdAffiliationsMap: '{}'", getClaimName(), voIdValuesMap);
}
@Override
......@@ -63,24 +67,18 @@ public class EdupersonScopedAffiliationsMUSource extends ClaimSource {
@Override
public JsonNode produceValue(ClaimSourceProduceContext pctx) {
Long userId = pctx.getPerunUserId();
ArrayNode result = JsonNodeFactory.instance.arrayNode();
Set<Long> groups = pctx.getPerunAdapter().getUserGroupsIds(userId, voId);
for (Map.Entry<List<Long>, String> entry : affiliations.entrySet()) {
for (Long id: entry.getKey()) {
if (groups.contains(id)) {
String affiliation = entry.getValue() + '@' + valueScope;
log.trace("{} - added affiliation '{}' due to membership in group '{}'",
getClaimName(), affiliation, id);
result.add(affiliation);
break;
}
Set<String> userAffiliations = new HashSet<>();
Set<Long> userVoIds = pctx.getPerunAdapter().getUserVoIds(userId);
for (Long userVoId: userVoIds) {
Set<String> affiliationsToBeAdded = voIdValuesMap.getOrDefault(userVoId, new HashSet<>());
if (!affiliationsToBeAdded.isEmpty()) {
log.trace("{} - added affiliations '{}' due to membership in vo '{}'",
getClaimName(), affiliationsToBeAdded, userVoId);
userAffiliations.addAll(affiliationsToBeAdded);
}
}
if (result.size() == 0) {
String affiliation = "affiliate@" + valueScope;
log.trace("{} - user is not a member in any special groups, added default affiliation: '{}'",
getClaimName(), affiliation);
ArrayNode result = JsonNodeFactory.instance.arrayNode();
for (String affiliation : userAffiliations) {
result.add(affiliation);
}
......@@ -88,23 +86,69 @@ public class EdupersonScopedAffiliationsMUSource extends ClaimSource {
return result;
}
private void parseConfigFile(String file) {
log.trace("{} - Loading config file {}", getClaimName(), file);
YAMLMapper mapper = new YAMLMapper();
try {
JsonNode root = mapper.readValue(new File(file), JsonNode.class);
valueScope = root.get(KEY_SCOPE).asText();
voId = root.get(KEY_VO_ID).longValue();
for (JsonNode affiliationMapping : root.path(KEY_AFFILIATIONS)) {
String value = affiliationMapping.path(KEY_VALUE).asText();
List<Long> gids = new ArrayList<>();
for (JsonNode gid : affiliationMapping.path(KEY_GROUPS)) {
gids.add(gid.asLong());
}
affiliations.put(gids, value);
private Map<Long, Set<String>> parseValueMap(String valueMapProp) {
String[] valueMapParts = valueMapProp.split("\\|");
if (valueMapParts.length == 0) {
throw getConfigurationException(
"Could not parse valueMap property. Needs to be in format voId1:aff1,aff2|voId2:aff3"
);
}
for (String idValue: valueMapParts) {
if (!StringUtils.hasText(idValue)) {
throw getConfigurationException(
"Could not parse id and affiliations mapping, empty String encountered"
);
}
} catch (IOException ex) {
log.warn("{} - cannot read claim configuration file: '{}'", getClaimName(), file);
String[] idValueParts = idValue.split(":");
if (idValueParts.length != 2) {
throw getConfigurationException(
"Could not parse id and affiliations mapping. Needs to be in format voId:aff1,aff2"
);
}
long voId;
try {
voId = Long.parseLong(idValueParts[0]);
} catch (NumberFormatException ex) {
throw getConfigurationException("Could not parse VO id out of subcomponent " + idValue, ex);
}
Set<String> voAffiliations = parseAffiliations(idValueParts[1]);
if (voAffiliations.isEmpty()) {
throw getConfigurationException("No affiliation values found for voId " + voId);
}
voIdValuesMap.put(voId, voAffiliations);
}
return voIdValuesMap;
}
private Set<String> parseAffiliations(String idValuePart) {
String[] affiliations = idValuePart.split(",");
Set<String> resolvedAffiliations = new HashSet<>();
for (String affiliation : affiliations) {
if (!epsaPattern.matcher(affiliation).matches()) {
throw getConfigurationException(
"Value '" + affiliation + "' is not a valid eduPersonScopedAffiliation value"
);
}
resolvedAffiliations.add(affiliation);
}
return resolvedAffiliations;
}
private ConfigurationException getConfigurationException(String message) {
return getConfigurationException(message, null);
}
private ConfigurationException getConfigurationException(String message, Throwable cause) {
StringBuilder fullMessage = new StringBuilder();
fullMessage.append("Invalid configuration for claim ").append(getClaimName());
if (StringUtils.hasText(message)) {
fullMessage.append(": ").append(message);
}
if (cause != null) {
throw new ConfigurationException(fullMessage.toString(), cause);
} else {
throw new ConfigurationException(fullMessage.toString());
}
}
......
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please register or to comment