Skip to content

Commit

Permalink
Merge pull request #2776 from Vojtech-Sassmann/syncGroupsToResources
Browse files Browse the repository at this point in the history
Group structure sync - resources
  • Loading branch information
stavamichal authored and zlamalp committed Jul 9, 2020
1 parent 7ec10ed commit 75c6d7d
Show file tree
Hide file tree
Showing 6 changed files with 778 additions and 24 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -334,6 +334,49 @@ public static Map<String, String> deserializeStringToMap(String text) {
return map;
}

/**
* From the given list, parses all values separated by the comma ',' char.
* The value has to end with the comma ',' char.
*
* All escaped commas '{backslash},' will not be used to split the value. If there is a
* backslash character:
* * it must be either follow by the comma ',' meaning that this
* comma is not used for split; or
* * it must be paired with another backslash '{backslash}{backslash}'. In that case, this double
* backslash in the result value would be replaced by a single back slash.
*
* Example:
* input: 'value1,val{backslash}ue2,val{backslash},ue3,'
* result: ['value1', 'val{backslash}ue2', val,ue3']
*
* @param value value to be parsed
* @return list of parsed values
*/
public static List<String> parseEscapedListValue(String value) {
String[] array = value.split(Character.toString(LIST_DELIMITER), -1);
List<String> listValue = new ArrayList<String>();
//join items which was splited on escaped LIST_DELIMITER
for(int i = 0; i < array.length -1; i++) { //itarate to lenght -1 ... last array item is always empty
String item = array[i];
while(item.matches("^(.*[^\\\\])?(\\\\\\\\)*\\\\$")) { //item last char is '\' . Next item start with ',', so we need to concat this items.
item = item.substring(0, item.length()-1); //cut off last char ('\')
try {
item = item.concat(Character.toString(LIST_DELIMITER)).concat(array[i+1]);
i++;
} catch(ArrayIndexOutOfBoundsException ex) {
throw new ConsistencyErrorException("Bad format in attribute value", ex);
}
}
//unescape
item = item.replaceAll("\\\\([\\\\" + Character.toString(LIST_DELIMITER) + "])", "$1");
if(item.equals("\\0")) item = null;

//return updated item back to list
listValue.add(item);
}
return listValue;
}

/**
* Converts string representation of an attribute value to correct java object
*
Expand Down Expand Up @@ -370,30 +413,7 @@ public static Object stringToAttributeValue(String stringValue, String type) {
} else if(attributeClass.equals(Boolean.class)) {
return Boolean.parseBoolean(stringValue);
} else if(attributeClass.equals(ArrayList.class)) {
String[] array = stringValue.split(Character.toString(LIST_DELIMITER), -1);
List<String> attributeValue = new ArrayList<String>();

//join items which was splited on escaped LIST_DELIMITER
for(int i = 0; i < array.length -1; i++) { //itarate to lenght -1 ... last array item is always empty
String item = array[i];
while(item.matches("^(.*[^\\\\])?(\\\\\\\\)*\\\\$")) { //item last char is '\' . Next item start with ',', so we need to concat this items.
item = item.substring(0, item.length()-1); //cut off last char ('\')
try {
item = item.concat(Character.toString(LIST_DELIMITER)).concat(array[i+1]);
i++;
} catch(ArrayIndexOutOfBoundsException ex) {
throw new ConsistencyErrorException("Bad format in attribute value", ex);
}
}
//unescape
item = item.replaceAll("\\\\([\\\\" + Character.toString(LIST_DELIMITER) + "])", "$1");
if(item.equals("\\0")) item = null;

//return updated item back to list
attributeValue.add(item);
}

return attributeValue;
return parseEscapedListValue(stringValue);
} else if(attributeClass.equals(LinkedHashMap.class)) {
String[] array = stringValue.split(Character.toString(LIST_DELIMITER), -1);
Map<String, String> attributeValue = new LinkedHashMap<String, String>();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,7 @@
import cz.metacentrum.perun.core.impl.Utils;
import cz.metacentrum.perun.core.impl.modules.attributes.urn_perun_entityless_attribute_def_def_namespace_GIDRanges;
import cz.metacentrum.perun.core.impl.modules.attributes.urn_perun_facility_attribute_def_virt_GIDRanges;
import cz.metacentrum.perun.core.impl.modules.attributes.urn_perun_group_attribute_def_def_groupStructureResources;
import cz.metacentrum.perun.core.impl.modules.attributes.urn_perun_member_attribute_def_def_suspensionInfo;
import cz.metacentrum.perun.core.implApi.AttributesManagerImplApi;
import cz.metacentrum.perun.core.implApi.modules.attributes.AttributesModuleImplApi;
Expand Down Expand Up @@ -7489,6 +7490,14 @@ protected void initialize() throws InternalErrorException {
rights.add(new AttributeRights(-1, Role.GROUPADMIN, Collections.singletonList(ActionType.READ)));
attributes.put(attr, rights);

//urn:perun:group:attribute-def:def:groupStructureResources
attr = new urn_perun_group_attribute_def_def_groupStructureResources().getAttributeDefinition();
//set attribute rights (with dummy id of attribute - not known yet)
rights = new ArrayList<>();
rights.add(new AttributeRights(-1, Role.VOADMIN, Arrays.asList(ActionType.READ, ActionType.WRITE)));
rights.add(new AttributeRights(-1, Role.GROUPADMIN, Collections.singletonList(ActionType.READ)));
attributes.put(attr, rights);

//urn:perun:facility:attribute-def:def:login-namespace
attr = new AttributeDefinition();
attr.setNamespace(AttributesManager.NS_FACILITY_ATTR_DEF);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,7 @@
import cz.metacentrum.perun.core.api.exceptions.ExtSourceNotExistsException;
import cz.metacentrum.perun.core.api.exceptions.ExtSourceUnsupportedOperationException;
import cz.metacentrum.perun.core.api.exceptions.ExtendMembershipException;
import cz.metacentrum.perun.core.api.exceptions.GroupAlreadyAssignedException;
import cz.metacentrum.perun.core.api.exceptions.GroupAlreadyRemovedException;
import cz.metacentrum.perun.core.api.exceptions.GroupAlreadyRemovedFromResourceException;
import cz.metacentrum.perun.core.api.exceptions.GroupExistsException;
Expand Down Expand Up @@ -93,6 +94,7 @@
import cz.metacentrum.perun.core.api.exceptions.PasswordDeletionFailedException;
import cz.metacentrum.perun.core.api.exceptions.PasswordOperationTimeoutException;
import cz.metacentrum.perun.core.api.exceptions.RelationExistsException;
import cz.metacentrum.perun.core.api.exceptions.ResourceNotExistsException;
import cz.metacentrum.perun.core.api.exceptions.UserExtSourceExistsException;
import cz.metacentrum.perun.core.api.exceptions.UserExtSourceNotExistsException;
import cz.metacentrum.perun.core.api.exceptions.UserNotAdminException;
Expand Down Expand Up @@ -122,6 +124,7 @@
import java.time.temporal.TemporalUnit;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.Comparator;
import java.util.Date;
Expand Down Expand Up @@ -161,6 +164,7 @@ public class GroupsManagerBlImpl implements GroupsManagerBl {
private final ArrayList<GroupStructureSynchronizerThread> groupStructureSynchronizerThreads;
private static final String A_G_D_AUTHORITATIVE_GROUP = AttributesManager.NS_GROUP_ATTR_DEF + ":authoritativeGroup";
private static final String A_G_D_EXPIRATION_RULES = AttributesManager.NS_GROUP_ATTR_DEF + ":groupMembershipExpirationRules";
private static final String A_G_D_GROUP_STRUCTURE_RESOURCES = AttributesManager.NS_GROUP_ATTR_DEF + ":groupStructureResources";
private static final String A_MG_D_MEMBERSHIP_EXPIRATION = AttributesManager.NS_MEMBER_GROUP_ATTR_DEF + ":groupMembershipExpiration";
private static final String A_M_V_LOA = AttributesManager.NS_MEMBER_ATTR_VIRT + ":loa";
private static final List<Status> statusesAffectedBySynchronization = Arrays.asList(Status.DISABLED, Status.EXPIRED, Status.INVALID);
Expand Down Expand Up @@ -1742,11 +1746,144 @@ public List<String> synchronizeGroupStructure(PerunSession sess, Group baseGroup

setUpSynchronizationAttributesForAllSubGroups(sess, baseGroup, source, loginAttributeDefinition, loginPrefix);

syncResourcesForSynchronization(sess, baseGroup, loginAttributeDefinition, skippedGroups);

log.info("Group structure synchronization {}: ended.", baseGroup);

return skippedGroups;
}

/**
* Sync resources from groupStructureResources attribute to the group
* tree with the root in the given base group. If some resources are not found
* or the assignment failed, there is a new message added to the skippedMessages list.
*
* @param sess perun session
* @param baseGroup group structure sync base group
* @param skippedMessages list where are added messages about skipped operations
*/
private void syncResourcesForSynchronization(PerunSession sess, Group baseGroup, AttributeDefinition loginAttr,
List<String> skippedMessages) {
Attribute syncedResourcesAttr;

try {
syncedResourcesAttr = perunBl.getAttributesManagerBl()
.getAttribute(sess, baseGroup, A_G_D_GROUP_STRUCTURE_RESOURCES);
} catch (WrongAttributeAssignmentException | AttributeNotExistsException e) {
throw new InternalErrorException("Failed to obtain the groupStructureResources attribute for structure synchronization.", e);
}

if (syncedResourcesAttr.getValue() == null) {
// no resources should be synced
return;
}

// load all subgroups with logins once, so it can be reused in other methods
Map<String, Group> groupsByLogins = getAllSubGroupsWithLogins(sess, baseGroup, loginAttr);

syncedResourcesAttr.valueAsMap().forEach((resourceId, groupLogins) ->
syncResourceInStructure(sess, resourceId, groupLogins, baseGroup, groupsByLogins, skippedMessages));
}

/**
* Assign resource with given id to groups with logins defined in parameter 'rawGroupLogins'.
*
* If the rawGroupLogins value is empty, then the resource is assigned to the whole structure.
* The rawGroupLogins are in format: 'login1,login2,login3,'. If the login contains a '\' or ','
* characters, they must be escaped by the '\' character.
*
* @param sess perun session
* @param rawResourceId id of a resource which should be set
* @param rawGroupLogins group login separated with a comma ','
* @param baseGroup base group which is used if the rawGroupLogins is empty
* @param groupsByLogins map containing groups with by their logins
* @param skippedMessages a list with skipped messages, other messages are added to this list
*/
private void syncResourceInStructure(PerunSession sess, String rawResourceId, String rawGroupLogins,
Group baseGroup, Map<String, Group> groupsByLogins,
List<String> skippedMessages) {
int resourceId = Integer.parseInt(rawResourceId);
try {
Resource resource = perunBl.getResourcesManagerBl().getResourceById(sess, resourceId);
List<String> groupLogins;
if (rawGroupLogins == null || rawGroupLogins.isEmpty()) {
// if no group logins are specified, assign the resource to the whole tree
groupLogins = Collections.singletonList(rawGroupLogins);
} else {
groupLogins = BeansUtils.parseEscapedListValue(rawGroupLogins);
}
groupLogins.forEach(login ->
syncResourceInStructure(sess, resource, baseGroup, login, groupsByLogins, skippedMessages));
} catch (ResourceNotExistsException e) {
log.error("Assigning groups to a resource was skipped during group structure synchronization, because the resource wasn't found.", e);
skippedMessages.add("Assigning groups to resource with id'" + resourceId + "' skipped because it was not found.");
}
}

/**
* Assign given resource to a group with given login, and to all of
* its subgroups.
*
* If the login value is empty, then the resource is assigned to
* the whole structure, except for the base group.
*
* @param sess perun session
* @param resource a resource which should be set
* @param login group login
* @param baseGroup base group which is used if the rawGroupLogins is empty
* @param groupsByLogins map containing groups with by their logins
* @param skippedMessages a list with skipped messages, other messages are added to this list
*/
private void syncResourceInStructure(PerunSession sess, Resource resource, Group baseGroup, String login,
Map<String, Group> groupsByLogins, List<String> skippedMessages) {
Group rootGroup;
boolean assigningToTheBaseGroup = login == null || login.isEmpty();

if (assigningToTheBaseGroup) {
rootGroup = baseGroup;
} else {
rootGroup = groupsByLogins.get(login);
if (rootGroup == null) {
skippedMessages.add("Resource with id '" + resource.getId() + "' was skipped for group with login '" +
login + "' no group with this login was found.");
return;
}
}

List<Group> groupsToAssign = perunBl.getGroupsManagerBl().getAllSubGroups(sess, rootGroup);

if (!assigningToTheBaseGroup) {
groupsToAssign.add(rootGroup);
}

assignGroupsToResource(sess, groupsToAssign, resource, skippedMessages);
}

/**
* Assign resource to the given groups. If any of the assignments fails,
* information is added to the given skippedMessages. If some of the groups
* is already assigned, the group is skipped silently.
*
* @param sess perun session
* @param groups groups which should be assigned to the given resource
* @param resource resource
* @param skippedMessages list where are added messages about skipped operations
*/
private void assignGroupsToResource(PerunSession sess, Collection<Group> groups, Resource resource,
List<String> skippedMessages) {
Set<Group> groupsToAssign = new HashSet<>(groups);
groupsToAssign.removeAll(perunBl.getResourcesManagerBl().getAssignedGroups(sess, resource));

groupsToAssign.forEach(group -> {
try {
perunBl.getResourcesManagerBl().assignGroupToResource(sess, group, resource);
} catch (WrongAttributeValueException | WrongReferenceAttributeValueException | GroupAlreadyAssignedException | GroupResourceMismatchException e) {
log.error("Failed to assign group during group structure synchronization. Group {}, resource {}", group, resource);
skippedMessages.add("Skipped assignment of a resource to a group. Group id: " + group.getId() + ", resource id: " + resource.getId());
}
});
}

@Override
public void forceGroupSynchronization(PerunSession sess, Group group) throws GroupSynchronizationAlreadyRunningException, GroupSynchronizationNotEnabledException {
//Check if the group should be synchronized (attribute synchronizationEnabled is set to 'true')
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
package cz.metacentrum.perun.core.impl.modules.attributes;

import cz.metacentrum.perun.core.api.Attribute;
import cz.metacentrum.perun.core.api.AttributeDefinition;
import cz.metacentrum.perun.core.api.AttributesManager;
import cz.metacentrum.perun.core.api.Group;
import cz.metacentrum.perun.core.api.Resource;
import cz.metacentrum.perun.core.api.Vo;
import cz.metacentrum.perun.core.api.exceptions.InternalErrorException;
import cz.metacentrum.perun.core.api.exceptions.VoNotExistsException;
import cz.metacentrum.perun.core.api.exceptions.WrongAttributeAssignmentException;
import cz.metacentrum.perun.core.api.exceptions.WrongAttributeValueException;
import cz.metacentrum.perun.core.api.exceptions.WrongReferenceAttributeValueException;
import cz.metacentrum.perun.core.impl.PerunSessionImpl;
import cz.metacentrum.perun.core.implApi.modules.attributes.GroupAttributesModuleAbstract;
import cz.metacentrum.perun.core.implApi.modules.attributes.GroupAttributesModuleImplApi;

import java.util.LinkedHashMap;
import java.util.List;
import java.util.Set;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.stream.Collectors;

/**
* @author Vojtech Sassmann <vojtech.sassmann@gmail.com>
*/
public class urn_perun_group_attribute_def_def_groupStructureResources extends GroupAttributesModuleAbstract implements GroupAttributesModuleImplApi {

private static final Pattern resourceIdPattern = Pattern.compile("^[1-9][0-9]*$");
private static final Pattern invalidEscapePattern =
Pattern.compile("(" + "([^\\\\]|^)(\\\\\\\\)*)\\\\([^,\\\\]|$)");

@Override
public void checkAttributeSyntax(PerunSessionImpl sess, Group group, Attribute attribute) throws WrongAttributeValueException {
//Null value is ok, means no settings for group
if(attribute.getValue() == null) return;

LinkedHashMap<String, String> attrValues = attribute.valueAsMap();

boolean hasInvalidResourceName = attrValues.keySet().stream()
.anyMatch(this::invalidResourceName);

if (hasInvalidResourceName) {
throw new WrongAttributeValueException(attribute, group, "Some of the specified resource ids has an invalid format.");
}
for (String rawGroupLogins : attrValues.values()) {
// null or empty value means the whole group tree
if (rawGroupLogins == null || rawGroupLogins.isEmpty()) {
continue;
}
Matcher m = invalidEscapePattern.matcher(rawGroupLogins);
if (m.find()) {
throw new WrongAttributeValueException(attribute, group, "Group logins format contains invalid escape sequence.");
}
if (!rawGroupLogins.endsWith(",")) {
throw new WrongAttributeValueException(attribute, group, "Each group login has to end with a comma ','.");
}
}
}

@Override
public void checkAttributeSemantics(PerunSessionImpl sess, Group group, Attribute attribute) throws WrongReferenceAttributeValueException, WrongAttributeAssignmentException {
//Null value is ok, means no settings for group
if(attribute.getValue() == null) return;

LinkedHashMap<String, String> attrValues = attribute.valueAsMap();
Vo vo;
try {
vo = sess.getPerunBl().getVosManagerBl().getVoById(sess, group.getVoId());
} catch (VoNotExistsException e) {
throw new InternalErrorException("Failed to find group's vo.", e);
}
List<Resource> voResources = sess.getPerunBl().getResourcesManagerBl().getResources(sess, vo);
Set<Integer> voResourceIds = voResources.stream()
.map(Resource::getId)
.collect(Collectors.toSet());

for (String rawId : attrValues.keySet()) {
int id = Integer.parseInt(rawId);
if (!voResourceIds.contains(id)) {
throw new WrongReferenceAttributeValueException(attribute, "There is no resource with id '" + id + "' assigned to the groups vo: " + vo);
}
}
}

private boolean invalidResourceName(String value) {
return !resourceIdPattern.matcher(value).matches();
}

@Override
public AttributeDefinition getAttributeDefinition() {
AttributeDefinition attr = new AttributeDefinition();
attr.setNamespace(AttributesManager.NS_GROUP_ATTR_DEF);
attr.setType(LinkedHashMap.class.getName());
attr.setFriendlyName("groupStructureResources");
attr.setDisplayName("Group structure synchronization resources");
attr.setDescription("Defines, which resources (map keys) should be auto assigned, and to which groups " +
"(map values). Each group login should end with the `,` character (even the last one, eg: " +
"`login1,login2,`). If some of the group logins contains a comma ',' or backslash '\\', you have to " +
"escape it with the backslash '\\' character.");
return attr;
}
}
Loading

0 comments on commit 75c6d7d

Please sign in to comment.