pl.edu.icm.unity.engine.translation.in.InputTranslationEngineImpl Maven / Gradle / Ivy
/*
* Copyright (c) 2014 ICM Uniwersytet Warszawski All rights reserved.
* See LICENCE.txt file for licensing information.
*/
package pl.edu.icm.unity.engine.translation.in;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Deque;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import org.apache.logging.log4j.Logger;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.stereotype.Component;
import pl.edu.icm.unity.base.attribute.Attribute;
import pl.edu.icm.unity.base.attribute.AttributeExt;
import pl.edu.icm.unity.base.entity.Entity;
import pl.edu.icm.unity.base.entity.EntityParam;
import pl.edu.icm.unity.base.entity.EntityState;
import pl.edu.icm.unity.base.exceptions.EngineException;
import pl.edu.icm.unity.base.group.Group;
import pl.edu.icm.unity.base.group.GroupMembership;
import pl.edu.icm.unity.base.identity.Identity;
import pl.edu.icm.unity.base.identity.IdentityParam;
import pl.edu.icm.unity.base.utils.Log;
import pl.edu.icm.unity.engine.api.AttributesManagement;
import pl.edu.icm.unity.engine.api.EntityManagement;
import pl.edu.icm.unity.engine.api.GroupsManagement;
import pl.edu.icm.unity.engine.api.attributes.AttributeValueSyntax;
import pl.edu.icm.unity.engine.api.translation.in.EntityChange;
import pl.edu.icm.unity.engine.api.translation.in.GroupEffectMode;
import pl.edu.icm.unity.engine.api.translation.in.IdentityEffectMode;
import pl.edu.icm.unity.engine.api.translation.in.InputTranslationEngine;
import pl.edu.icm.unity.engine.api.translation.in.MappedAttribute;
import pl.edu.icm.unity.engine.api.translation.in.MappedGroup;
import pl.edu.icm.unity.engine.api.translation.in.MappedIdentity;
import pl.edu.icm.unity.engine.api.translation.in.MappingResult;
import pl.edu.icm.unity.engine.attribute.AttributeTypeHelper;
import pl.edu.icm.unity.engine.translation.ExecutionBreakException;
/**
* Applies all mappings which were recorded by profile's actions, taking into
* account the overall profile's settings.
*
* Important: the instance is running without authorization, the object can not
* be exposed to direct operation.
*
* @author K. Benedyczak
*/
@Component
public class InputTranslationEngineImpl implements InputTranslationEngine
{
private static final Logger log = Log.getLogger(Log.U_SERVER_TRANSLATION, InputTranslationEngineImpl.class);
private EntityManagement idsMan;
private AttributesManagement attrMan;
private GroupsManagement groupsMan;
private AttributeTypeHelper attrTypeHelper;
@Autowired
public InputTranslationEngineImpl(@Qualifier("insecure") EntityManagement idsMan,
@Qualifier("insecure") AttributesManagement attrMan,
@Qualifier("insecure") GroupsManagement groupsMan, AttributeTypeHelper attrTypeHelper)
{
this.idsMan = idsMan;
this.attrMan = attrMan;
this.groupsMan = groupsMan;
this.attrTypeHelper = attrTypeHelper;
}
@Override
public void process(MappingResult result) throws EngineException
{
Set processedAttributes = new HashSet<>();
EntityParam entity = processIdentities(result, processedAttributes);
result.setMappedToExistingEntity(entity);
if (entity == null)
{
log.info("The mapped identity does not exist in database and was not created. "
+ "The creation of groups and attributes is skipped, the mapped groups and attributes "
+ "will be available for the registration form (if any)");
return;
}
processGroups(result, entity, processedAttributes);
processAttributes(result, entity, processedAttributes);
processEntityChanges(result, entity);
}
/**
* Merges the information obtained after execution of an input
* translation profile with a manually specified entity.
*/
@Override
public void mergeWithExisting(MappingResult result, EntityParam baseEntity) throws EngineException
{
result.setCleanStaleAttributes(false);
result.setCleanStaleIdentities(false);
result.setCleanStaleGroups(false);
Set processedAttributes = new HashSet<>();
processIdentitiesForMerge(result, baseEntity);
processGroups(result, baseEntity, processedAttributes);
processAttributes(result, baseEntity, processedAttributes);
processEntityChanges(result, baseEntity);
}
/**
* @return true only if no one of mapped identities is present in db.
*/
@Override
public boolean identitiesNotPresentInDb(MappingResult result)
{
try
{
getIdentitiesToCreate(result);
return true;
} catch (EngineException e)
{
return false;
}
}
@Override
public Entity resolveMappedIdentity(MappedIdentity checked) throws EngineException
{
return idsMan.getEntity(new EntityParam(checked.getIdentity()));
}
@Override
public MappedIdentity getExistingIdentity(MappingResult result)
{
for (MappedIdentity checked : result.getIdentities())
{
try
{
idsMan.getEntity(new EntityParam(checked.getIdentity()));
return checked;
} catch (IllegalArgumentException e)
{
// OK
} catch (EngineException e)
{
log.error("Can't check the entity status, shouldn't happen", e);
}
}
return null;
}
/**
* performs identities mapping
*
* @return an mapped identity - previously existing or newly created; or
* null if identity mapping was not successful
*/
private EntityParam processIdentities(MappingResult result, Set processedAttributes)
throws EngineException
{
List mappedMissingIdentitiesToCreate = new ArrayList<>();
List mappedMissingIdentities = new ArrayList<>();
List mappedMissingCreateOrUpdateIdentities = new ArrayList<>();
Entity existing = null;
for (MappedIdentity checked : result.getIdentities())
{
try
{
Entity found = idsMan.getEntity(new EntityParam(checked.getIdentity()));
if (existing != null && !existing.getId().equals(found.getId()))
{
log.warn("Identity was mapped to two different entities: " + existing + " and "
+ found);
throw new ExecutionBreakException();
}
existing = found;
} catch (IllegalArgumentException e)
{
log.trace("Identity " + checked + " not found in DB, details of exception follows", e);
if (checked.getMode() == IdentityEffectMode.REQUIRE_MATCH)
{
log.info("The identity " + checked.getIdentity() + " doesn't exists "
+ "in local database, but the profile requires so.");
throw new ExecutionBreakException();
} else if (checked.getMode() == IdentityEffectMode.CREATE_OR_MATCH)
{
mappedMissingIdentitiesToCreate.add(checked);
} else if (checked.getMode() == IdentityEffectMode.MATCH)
{
mappedMissingIdentities.add(checked);
} else
{
mappedMissingCreateOrUpdateIdentities.add(checked);
}
}
}
if (existing != null)
{
mappedMissingIdentitiesToCreate.addAll(mappedMissingCreateOrUpdateIdentities);
if (result.isCleanStaleIdentities())
removeStaleIdentities(existing, result.getIdentities());
} else
{
mappedMissingIdentities.addAll(mappedMissingCreateOrUpdateIdentities);
}
if (mappedMissingIdentitiesToCreate.isEmpty() && mappedMissingIdentities.isEmpty() && existing == null)
{
log.info("The translation profile didn't return any identity of the principal. "
+ "We can't authenticate such anonymous principal.");
throw new ExecutionBreakException();
}
if (mappedMissingIdentitiesToCreate.isEmpty())
{
log.debug("No identity needs to be added");
return existing != null ? new EntityParam(existing.getId()) : null;
}
if (existing != null)
{
addEquivalents(mappedMissingIdentitiesToCreate, new EntityParam(existing.getId()), result);
return new EntityParam(existing.getId());
} else
{
Identity created = createNewEntity(result, mappedMissingIdentitiesToCreate,
processedAttributes);
return new EntityParam(created.getEntityId());
}
}
/**
* Identities created from this profile and idp which are not present
* are removed.
*/
private void removeStaleIdentities(Entity existing, List allMapped)
{
IdentityParam exampleMapped = allMapped.get(0).getIdentity();
String idp = exampleMapped.getRemoteIdp();
if (idp == null)
idp = "_____MISSING";
String profile = exampleMapped.getTranslationProfile();
if (profile == null)
profile = "_____MISSING";
for (Identity id : existing.getIdentities())
{
if (idp.equals(id.getRemoteIdp()) && profile.equals(id.getTranslationProfile()))
{
boolean has = allMapped.stream()
.anyMatch(mi -> id.getValue().equals(mi.getIdentity().getValue())
&& id.getTypeId().equals(mi.getIdentity().getTypeId()));
if (!has)
{
try
{
idsMan.removeIdentity(id);
} catch (EngineException e)
{
log.error("Can not remove stale identity " + id, e);
}
}
}
}
}
/**
* Besides returning the list of identities to be created also ensures
* that no one from mapped identities does exist in the db.
*
* @return list of mapped identities which should be created.
*/
private List getIdentitiesToCreate(MappingResult result) throws EngineException
{
List mappedMissingIdentitiesToCreate = new ArrayList<>();
for (MappedIdentity checked : result.getIdentities())
{
try
{
idsMan.getEntity(new EntityParam(checked.getIdentity()));
log.debug("Identity was mapped to existing identity.");
throw new ExecutionBreakException();
} catch (IllegalArgumentException e)
{
log.trace("Identity " + checked + " not found in DB, details of exception follows", e);
if (checked.getMode() == IdentityEffectMode.REQUIRE_MATCH)
{
log.info("The identity " + checked.getIdentity() + " doesn't exist "
+ "in local database, but the profile requires so.");
throw new ExecutionBreakException();
} else
{
mappedMissingIdentitiesToCreate.add(checked);
}
}
}
return mappedMissingIdentitiesToCreate;
}
private void processIdentitiesForMerge(MappingResult result, EntityParam baseEntity) throws EngineException
{
List mappedMissingIdentitiesToCreate = getIdentitiesToCreate(result);
if (mappedMissingIdentitiesToCreate.isEmpty())
{
log.info("The translation profile didn't return any identity of the principal. "
+ "We can't merge such anonymous principal.");
throw new ExecutionBreakException();
}
addEquivalents(mappedMissingIdentitiesToCreate, baseEntity, result);
}
private void addEquivalents(Collection toAdd, EntityParam parentEntity, MappingResult result)
throws EngineException
{
for (MappedIdentity mi : toAdd)
{
idsMan.addIdentity(mi.getIdentity(), parentEntity);
result.addAuthenticatedWith(mi.getIdentity().getValue());
}
}
private Identity createNewEntity(MappingResult result, List mappedMissingIdentities,
Set processedAttributes) throws EngineException
{
MappedIdentity first = mappedMissingIdentities.remove(0);
Identity added;
List attributes = getAttributesInGroup("/", result);
log.info("Adding entity " + first.getIdentity() + " to the local DB");
added = idsMan.addEntity(first.getIdentity(), first.getCredentialRequirement(), EntityState.valid,
attributes);
result.addAuthenticatedWith(first.getIdentity().getValue());
processedAttributes.addAll(attributes);
addEquivalents(mappedMissingIdentities, new EntityParam(added), result);
return added;
}
private Map> getAttributesByGroup(MappingResult result)
{
Map> ret = new HashMap<>();
ret.put("/", getAttributesInGroup("/", result));
for (MappedGroup mg : result.getGroups())
{
if ("/".equals(mg.getGroup()))
continue;
ret.put(mg.getGroup(), getAttributesInGroup(mg.getGroup(), result));
}
return ret;
}
private List getAttributesInGroup(String group, MappingResult result)
{
List ret = new ArrayList<>();
for (MappedAttribute mappedAttr : result.getAttributes())
{
if (mappedAttr.getAttribute().getGroupPath().equals(group))
ret.add(mappedAttr.getAttribute());
}
return ret;
}
private void processGroups(MappingResult result, EntityParam principal, Set processedAttributes)
throws EngineException
{
Map> attributesByGroup = getAttributesByGroup(result);
Map currentGroups = idsMan.getGroups(principal);
Set currentSimple = new HashSet<>(currentGroups.keySet());
for (MappedGroup gm : result.getGroups())
{
if (!currentGroups.containsKey(gm.getGroup()))
{
Deque missingGroups = Group.getMissingGroups(gm.getGroup(), currentSimple);
log.info("Adding to group " + gm);
addToGroupRecursive(principal, missingGroups, currentSimple, gm.getIdp(),
gm.getProfile(), gm.getCreateIfMissing(), attributesByGroup,
processedAttributes);
} else
{
log.debug("Entity already in the group " + gm + ", skipping");
}
}
if (result.isCleanStaleGroups())
removeStaleMemberships(currentGroups, result, principal);
}
private void removeStaleMemberships(Map currentGroups, MappingResult result,
EntityParam principal)
{
IdentityParam exampleMapped = result.getIdentities().get(0).getIdentity();
String idp = exampleMapped.getRemoteIdp();
if (idp == null)
idp = "_____MISSING";
String profile = exampleMapped.getTranslationProfile();
if (profile == null)
profile = "_____MISSING";
List mappedGroups = result.getGroups();
for (GroupMembership membership : currentGroups.values())
{
if (!membership.getGroup().equals("/") && idp.equals(membership.getRemoteIdp())
&& profile.equals(membership.getTranslationProfile()))
{
if (!mappedGroups.stream().anyMatch(gm -> membership.getGroup().equals(gm.getGroup())))
try
{
groupsMan.removeMember(membership.getGroup(), principal);
} catch (Exception e)
{
log.error("Can not remove stale group membership in "
+ membership.getGroup(), e);
}
}
}
}
private void addToGroupRecursive(EntityParam who, Deque missingGroups, Set currentGroups,
String idp, String profile, GroupEffectMode createMissingGroups,
Map> attributesByGroup, Set processedAttributes)
throws EngineException
{
String group = missingGroups.pollLast();
List attributes = attributesByGroup.get(group);
if (attributes == null)
attributes = new ArrayList<>();
boolean present = groupsMan.isPresent(group);
if (!present)
{
if (createMissingGroups == GroupEffectMode.CREATE_GROUP_IF_MISSING)
{
log.info("Group " + group + " doesn't exist, "
+ "will be created to fullfil translation profile rule");
groupsMan.addGroup(new Group(group));
groupsMan.addMemberFromParent(group, who, attributes, idp, profile);
processedAttributes.addAll(attributes);
} else if (createMissingGroups == GroupEffectMode.REQUIRE_EXISTING_GROUP)
{
log.info("Entity should be added to a group " + group + " which is missing, failing.");
throw new ExecutionBreakException();
} else
{
log.debug("Entity should be added to a group " + group
+ " which is missing, ignoring.");
return;
}
} else
{
groupsMan.addMemberFromParent(group, who, attributes, idp, profile);
processedAttributes.addAll(attributes);
}
currentGroups.add(group);
if (!missingGroups.isEmpty())
{
addToGroupRecursive(who, missingGroups, currentGroups, idp, profile, createMissingGroups,
attributesByGroup, processedAttributes);
}
}
private void processAttributes(MappingResult result, EntityParam principal,
Set alreadyProcessedAttributes) throws EngineException
{
Map existingAttributes = new HashMap<>();
Collection existingAttrs = attrMan.getAllAttributes(principal, false, (String)null, null, false);
for (AttributeExt a : existingAttrs)
existingAttributes.put(a.getGroupPath() + "///" + a.getName(), a);
List attrs = result.getAttributes();
for (MappedAttribute attr : attrs)
{
Attribute att = attr.getAttribute();
if (alreadyProcessedAttributes.contains(att))
continue;
AttributeExt existing = existingAttributes.get(att.getGroupPath() + "///" + att.getName());
switch (attr.getMode())
{
case CREATE_ONLY:
if (existing == null)
{
log.info("Creating attribute " + att);
attrMan.createAttribute(principal, att);
} else
{
log.debug("Skipping attribute which is already present: " + att);
}
break;
case CREATE_OR_UPDATE:
updateExistingAttribute(att, principal, existing);
break;
case UPDATE_ONLY:
if (existing != null)
{
updateExistingAttribute(att, principal, existing);
} else
{
log.debug("Skipping attribute to be updated as there is no one defined: "
+ att);
}
break;
}
}
if (result.isCleanStaleAttributes())
removeStaleAttributes(result, existingAttrs, principal);
}
private void updateExistingAttribute(Attribute att, EntityParam principal, AttributeExt existing)
throws EngineException
{
if (existing != null && attributesEqual(att, existing))
{
log.debug("Attribute {} in DB is up to date, skiping update", att);
return;
}
log.info("Updating attribute {}", att);
attrMan.setAttribute(principal, att);
}
private boolean attributesEqual(Attribute attribute, AttributeExt fromDB)
{
if (!attribute.getValueSyntax().equals(fromDB.getValueSyntax()))
return false;
List values = attribute.getValues();
if (values.size() != fromDB.getValues().size())
return false;
AttributeValueSyntax> attrSyntax = attrTypeHelper.getUnconfiguredSyntax(attribute.getValueSyntax());
for (int i = 0; i < values.size(); i++)
if (!attrSyntax.areEqualStringValue(values.get(i), fromDB.getValues().get(i)))
return false;
return true;
}
private void removeStaleAttributes(MappingResult result, Collection existingAttrs,
EntityParam principal)
{
IdentityParam exampleMapped = result.getIdentities().get(0).getIdentity();
String idp = exampleMapped.getRemoteIdp();
if (idp == null)
idp = "_____MISSING";
String profile = exampleMapped.getTranslationProfile();
if (profile == null)
profile = "_____MISSING";
List mappedAttributes = result.getAttributes();
for (AttributeExt a : existingAttrs)
{
if (idp.equals(a.getRemoteIdp()) && profile.equals(a.getTranslationProfile()))
{
if (!mappedAttributes.stream()
.anyMatch(ma -> a.getName().equals(ma.getAttribute().getName())
&& a.getGroupPath().equals(
ma.getAttribute().getGroupPath())))
try
{
attrMan.removeAttribute(principal, a.getGroupPath(), a.getName());
} catch (Exception e)
{
log.error("Can not remove stale attribute " + a, e);
}
}
}
}
private void processEntityChanges(MappingResult result, EntityParam principal) throws EngineException
{
List changes = result.getEntityChanges();
for (EntityChange change : changes)
{
if (change.getScheduledOperation() != null)
log.info("Changing entity scheduled operation to " + change.getScheduledOperation()
+ " on " + change.getScheduledTime());
else
log.info("Clearing entity scheduled change operation");
idsMan.scheduleEntityChange(principal, change.getScheduledTime(),
change.getScheduledOperation());
}
}
}