You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@syncope.apache.org by sk...@apache.org on 2018/04/12 13:22:13 UTC

syncope git commit: [SYNCOPE-1302] Added new expression model in mapping for internal attributes to access user relationships

Repository: syncope
Updated Branches:
  refs/heads/2_0_X 73fab3dee -> 02b6d4eb6


[SYNCOPE-1302] Added new expression model in mapping for internal attributes to access user relationships


Project: http://git-wip-us.apache.org/repos/asf/syncope/repo
Commit: http://git-wip-us.apache.org/repos/asf/syncope/commit/02b6d4eb
Tree: http://git-wip-us.apache.org/repos/asf/syncope/tree/02b6d4eb
Diff: http://git-wip-us.apache.org/repos/asf/syncope/diff/02b6d4eb

Branch: refs/heads/2_0_X
Commit: 02b6d4eb6f6d0eaa993e00ef6d86c9f7c4411850
Parents: 73fab3d
Author: skylark17 <ma...@tirasa.net>
Authored: Thu Apr 12 15:21:30 2018 +0200
Committer: skylark17 <ma...@tirasa.net>
Committed: Thu Apr 12 15:21:58 2018 +0200

----------------------------------------------------------------------
 .../NotificationWizardBuilder.java              |   4 +-
 .../console/wizards/AbstractMappingPanel.java   |   4 +-
 .../core/provisioning/api/IntAttrName.java      |  30 ++
 .../provisioning/java/IntAttrNameParser.java    |  24 +-
 .../provisioning/java/MappingManagerImpl.java   | 306 +++++++++++--------
 .../java/data/ResourceDataBinderImpl.java       |  15 +
 .../java/IntAttrNameParserTest.java             |  59 ++++
 .../concepts/externalresources.adoc             |   4 +
 8 files changed, 316 insertions(+), 130 deletions(-)
----------------------------------------------------------------------


http://git-wip-us.apache.org/repos/asf/syncope/blob/02b6d4eb/client/console/src/main/java/org/apache/syncope/client/console/notifications/NotificationWizardBuilder.java
----------------------------------------------------------------------
diff --git a/client/console/src/main/java/org/apache/syncope/client/console/notifications/NotificationWizardBuilder.java b/client/console/src/main/java/org/apache/syncope/client/console/notifications/NotificationWizardBuilder.java
index 3ccfab0..bd9c503 100644
--- a/client/console/src/main/java/org/apache/syncope/client/console/notifications/NotificationWizardBuilder.java
+++ b/client/console/src/main/java/org/apache/syncope/client/console/notifications/NotificationWizardBuilder.java
@@ -358,7 +358,9 @@ public class NotificationWizardBuilder extends AjaxWizardBuilder<NotificationWra
             recipientAttrName.addRequiredLabel();
             recipientAttrName.setTitle(getString("intAttrNameInfo.help")
                     + "<code>groups[groupName].attribute</code>, "
-                    + "<code>anyObjects[anyObjectName].attribute</code> or "
+                    + "<code>users[userName].attribute</code>, "
+                    + "<code>anyObjects[anyObjectName].attribute</code>, "
+                    + "<code>relationships[relationshipType][anyType].attribute</code> or "
                     + "<code>memberships[groupName].attribute</code>", true);
             add(recipientAttrName);
 

http://git-wip-us.apache.org/repos/asf/syncope/blob/02b6d4eb/client/console/src/main/java/org/apache/syncope/client/console/wizards/AbstractMappingPanel.java
----------------------------------------------------------------------
diff --git a/client/console/src/main/java/org/apache/syncope/client/console/wizards/AbstractMappingPanel.java b/client/console/src/main/java/org/apache/syncope/client/console/wizards/AbstractMappingPanel.java
index 42475c0..38e5579 100644
--- a/client/console/src/main/java/org/apache/syncope/client/console/wizards/AbstractMappingPanel.java
+++ b/client/console/src/main/java/org/apache/syncope/client/console/wizards/AbstractMappingPanel.java
@@ -153,7 +153,9 @@ public abstract class AbstractMappingPanel extends Panel {
                 Model.<String>of(),
                 Model.of(getString("intAttrNameInfo.help")
                         + "<code>groups[groupName].attribute</code>, "
-                        + "<code>anyObjects[anyObjectName].attribute</code> or "
+                        + "<code>users[userName].attribute</code>, "
+                        + "<code>anyObjects[anyObjectName].attribute</code>, "
+                        + "<code>relationships[relationshipType][anyType].attribute</code> or "
                         + "<code>memberships[groupName].attribute</code>"),
                 new PopoverConfig().withHtml(true).withPlacement(TooltipConfig.Placement.right)) {
 

http://git-wip-us.apache.org/repos/asf/syncope/blob/02b6d4eb/core/provisioning-api/src/main/java/org/apache/syncope/core/provisioning/api/IntAttrName.java
----------------------------------------------------------------------
diff --git a/core/provisioning-api/src/main/java/org/apache/syncope/core/provisioning/api/IntAttrName.java b/core/provisioning-api/src/main/java/org/apache/syncope/core/provisioning/api/IntAttrName.java
index f46384e..a243bad 100644
--- a/core/provisioning-api/src/main/java/org/apache/syncope/core/provisioning/api/IntAttrName.java
+++ b/core/provisioning-api/src/main/java/org/apache/syncope/core/provisioning/api/IntAttrName.java
@@ -35,10 +35,16 @@ public class IntAttrName {
 
     private String enclosingGroup;
 
+    private String relatedUser;
+
     private String relatedAnyObject;
 
     private String membershipOfGroup;
 
+    private String relationshipType;
+
+    private String relationshipAnyType;
+
     public AnyTypeKind getAnyTypeKind() {
         return anyTypeKind;
     }
@@ -79,6 +85,14 @@ public class IntAttrName {
         this.enclosingGroup = enclosingGroup;
     }
 
+    public String getRelatedUser() {
+        return relatedUser;
+    }
+
+    public void setRelatedUser(final String relatedUser) {
+        this.relatedUser = relatedUser;
+    }
+
     public String getRelatedAnyObject() {
         return relatedAnyObject;
     }
@@ -95,6 +109,22 @@ public class IntAttrName {
         this.membershipOfGroup = membershipOfGroup;
     }
 
+    public String getRelationshipType() {
+        return relationshipType;
+    }
+
+    public void setRelationshipType(final String relationshipType) {
+        this.relationshipType = relationshipType;
+    }
+
+    public String getRelationshipAnyType() {
+        return relationshipAnyType;
+    }
+
+    public void setRelationshipAnyType(final String relationshipAnyType) {
+        this.relationshipAnyType = relationshipAnyType;
+    }
+
     @Override
     public String toString() {
         return ToStringBuilder.reflectionToString(this, ToStringStyle.JSON_STYLE);

http://git-wip-us.apache.org/repos/asf/syncope/blob/02b6d4eb/core/provisioning-java/src/main/java/org/apache/syncope/core/provisioning/java/IntAttrNameParser.java
----------------------------------------------------------------------
diff --git a/core/provisioning-java/src/main/java/org/apache/syncope/core/provisioning/java/IntAttrNameParser.java b/core/provisioning-java/src/main/java/org/apache/syncope/core/provisioning/java/IntAttrNameParser.java
index 2b77a61..27dd988 100644
--- a/core/provisioning-java/src/main/java/org/apache/syncope/core/provisioning/java/IntAttrNameParser.java
+++ b/core/provisioning-java/src/main/java/org/apache/syncope/core/provisioning/java/IntAttrNameParser.java
@@ -38,12 +38,19 @@ public class IntAttrNameParser {
     private static final Pattern ENCLOSING_GROUP_PATTERN = Pattern.compile(
             "^groups\\[(" + SyncopeConstants.NAME_PATTERN + ")\\]\\.(.+)");
 
+    private static final Pattern RELATED_USER_PATTERN = Pattern.compile(
+            "^users\\[(" + SyncopeConstants.NAME_PATTERN + ")\\]\\.(.+)");
+
     private static final Pattern RELATED_ANY_OBJECT_PATTERN = Pattern.compile(
             "^anyObjects\\[(" + SyncopeConstants.NAME_PATTERN + ")\\]\\.(.+)");
 
     private static final Pattern MEMBERSHIP_PATTERN = Pattern.compile(
             "^memberships\\[(" + SyncopeConstants.NAME_PATTERN + ")\\]\\.(.+)");
 
+    private static final Pattern RELATIONSHIP_PATTERN = Pattern.compile(
+            "^relationships\\[(" + SyncopeConstants.NAME_PATTERN + ")\\]"
+            + "\\[(" + SyncopeConstants.NAME_PATTERN + ")\\]\\.(.+)");
+
     @Autowired
     private PlainSchemaDAO plainSchemaDAO;
 
@@ -114,7 +121,22 @@ public class IntAttrNameParser {
                         result.setMembershipOfGroup(matcher.group(1));
                         setFieldOrSchemaName(matcher.group(2), result.getAnyTypeKind(), result);
                     } else {
-                        throw new ParseException("Unparsable expression: " + intAttrName, 0);
+                        matcher = RELATED_USER_PATTERN.matcher(intAttrName);
+                        if (matcher.matches()) {
+                            result.setAnyTypeKind(AnyTypeKind.USER);
+                            result.setRelatedUser(matcher.group(1));
+                            setFieldOrSchemaName(matcher.group(2), result.getAnyTypeKind(), result);
+                        } else {
+                            matcher = RELATIONSHIP_PATTERN.matcher(intAttrName);
+                            if (matcher.matches()) {
+                                result.setAnyTypeKind(AnyTypeKind.ANY_OBJECT);
+                                result.setRelationshipType(matcher.group(1));
+                                result.setRelationshipAnyType(matcher.group(2));
+                                setFieldOrSchemaName(matcher.group(3), result.getAnyTypeKind(), result);
+                            } else {
+                                throw new ParseException("Unparsable expression: " + intAttrName, 0);
+                            }
+                        }
                     }
                 }
             }

http://git-wip-us.apache.org/repos/asf/syncope/blob/02b6d4eb/core/provisioning-java/src/main/java/org/apache/syncope/core/provisioning/java/MappingManagerImpl.java
----------------------------------------------------------------------
diff --git a/core/provisioning-java/src/main/java/org/apache/syncope/core/provisioning/java/MappingManagerImpl.java b/core/provisioning-java/src/main/java/org/apache/syncope/core/provisioning/java/MappingManagerImpl.java
index ac63ad7..66c1652 100644
--- a/core/provisioning-java/src/main/java/org/apache/syncope/core/provisioning/java/MappingManagerImpl.java
+++ b/core/provisioning-java/src/main/java/org/apache/syncope/core/provisioning/java/MappingManagerImpl.java
@@ -27,6 +27,7 @@ import java.util.List;
 import java.util.Set;
 import org.apache.commons.collections4.CollectionUtils;
 import org.apache.commons.collections4.ListUtils;
+import org.apache.commons.collections4.Predicate;
 import org.apache.commons.lang3.BooleanUtils;
 import org.apache.commons.lang3.StringUtils;
 import org.apache.commons.lang3.reflect.FieldUtils;
@@ -40,6 +41,7 @@ import org.apache.syncope.common.lib.to.GroupableRelatableTO;
 import org.apache.syncope.common.lib.to.MembershipTO;
 import org.apache.syncope.common.lib.to.RealmTO;
 import org.apache.syncope.common.lib.to.UserTO;
+import org.apache.syncope.common.lib.types.AnyTypeKind;
 import org.apache.syncope.common.lib.types.AttrSchemaType;
 import org.apache.syncope.core.persistence.api.attrvalue.validation.ParsingValidationException;
 import org.apache.syncope.core.persistence.api.dao.AnyObjectDAO;
@@ -48,8 +50,11 @@ import org.apache.syncope.core.persistence.api.dao.DerSchemaDAO;
 import org.apache.syncope.core.persistence.api.dao.GroupDAO;
 import org.apache.syncope.core.persistence.api.dao.PlainSchemaDAO;
 import org.apache.syncope.core.persistence.api.dao.RealmDAO;
+import org.apache.syncope.core.persistence.api.dao.RelationshipTypeDAO;
+import org.apache.syncope.core.persistence.api.dao.UserDAO;
 import org.apache.syncope.core.persistence.api.dao.VirSchemaDAO;
 import org.apache.syncope.core.persistence.api.entity.Any;
+import org.apache.syncope.core.persistence.api.entity.AnyType;
 import org.apache.syncope.core.persistence.api.entity.AnyUtils;
 import org.apache.syncope.core.persistence.api.entity.AnyUtilsFactory;
 import org.apache.syncope.core.persistence.api.entity.DerSchema;
@@ -59,6 +64,8 @@ import org.apache.syncope.core.persistence.api.entity.PlainAttr;
 import org.apache.syncope.core.persistence.api.entity.PlainAttrValue;
 import org.apache.syncope.core.persistence.api.entity.PlainSchema;
 import org.apache.syncope.core.persistence.api.entity.Realm;
+import org.apache.syncope.core.persistence.api.entity.Relationship;
+import org.apache.syncope.core.persistence.api.entity.RelationshipType;
 import org.apache.syncope.core.persistence.api.entity.Schema;
 import org.apache.syncope.core.persistence.api.entity.VirSchema;
 import org.apache.syncope.core.persistence.api.entity.anyobject.AnyObject;
@@ -119,6 +126,12 @@ public class MappingManagerImpl implements MappingManager {
     private GroupDAO groupDAO;
 
     @Autowired
+    private UserDAO userDAO;
+
+    @Autowired
+    private RelationshipTypeDAO relationshipTypeDAO;
+
+    @Autowired
     private RealmDAO realmDAO;
 
     @Autowired
@@ -401,10 +414,14 @@ public class MappingManagerImpl implements MappingManager {
 
         LOG.debug("Get internal values for {} as '{}' on {}", any, mapItem.getIntAttrName(), provision.getResource());
 
-        Any<?> reference = null;
+        List<Any<?>> references = new ArrayList<>();
         Membership<?> membership = null;
-        if (intAttrName.getEnclosingGroup() == null && intAttrName.getRelatedAnyObject() == null) {
-            reference = any;
+        if (intAttrName.getEnclosingGroup() == null
+                && intAttrName.getRelatedAnyObject() == null
+                && intAttrName.getRelationshipAnyType() == null
+                && intAttrName.getRelationshipType() == null
+                && intAttrName.getRelatedUser() == null) {
+            references.add(any);
         }
         if (any instanceof GroupableRelatable) {
             GroupableRelatable<?, ?, ?, ?, ?> groupableRelatable = (GroupableRelatable<?, ?, ?, ?, ?>) any;
@@ -415,7 +432,17 @@ public class MappingManagerImpl implements MappingManager {
                     LOG.warn("No membership for {} in {}, ignoring",
                             intAttrName.getEnclosingGroup(), groupableRelatable);
                 } else {
-                    reference = group;
+                    references.add(group);
+                }
+            } else if (intAttrName.getRelatedUser() != null) {
+                User user = userDAO.findByUsername(intAttrName.getRelatedUser());
+                if (user == null || user.getRelationships(groupableRelatable.getKey()).isEmpty()) {
+                    LOG.warn("No relationship for {} in {}, ignoring",
+                            intAttrName.getRelatedUser(), groupableRelatable);
+                } else if (groupableRelatable.getType().getKind() == AnyTypeKind.USER) {
+                    LOG.warn("Users cannot have relationship with other users, ignoring");
+                } else {
+                    references.add(user);
                 }
             } else if (intAttrName.getRelatedAnyObject() != null) {
                 AnyObject anyObject = anyObjectDAO.findByName(intAttrName.getRelatedAnyObject());
@@ -423,14 +450,37 @@ public class MappingManagerImpl implements MappingManager {
                     LOG.warn("No relationship for {} in {}, ignoring",
                             intAttrName.getRelatedAnyObject(), groupableRelatable);
                 } else {
-                    reference = anyObject;
+                    references.add(anyObject);
+                }
+            } else if (intAttrName.getRelationshipAnyType() != null && intAttrName.getRelationshipType() != null) {
+                RelationshipType relationshipType = relationshipTypeDAO.find(intAttrName.getRelationshipType());
+                final AnyType anyType = anyTypeDAO.find(intAttrName.getRelationshipAnyType());
+                if (relationshipType == null || groupableRelatable.getRelationships(relationshipType).isEmpty()) {
+                    LOG.warn("No relationship for type {} in {}, ignoring",
+                            intAttrName.getRelationshipType(), groupableRelatable);
+                } else if (anyType == null) {
+                    LOG.warn("No anyType {}, ignoring", intAttrName.getRelationshipAnyType());
+                } else {
+                    @SuppressWarnings("unchecked")
+                    List<Relationship<?, ?>> results = (List<Relationship<?, ?>>) ListUtils.select(
+                            groupableRelatable.getRelationships(relationshipType),
+                            new Predicate<Relationship<?, ?>>() {
+
+                        @Override
+                        public boolean evaluate(final Relationship<?, ?> relationship) {
+                            return anyType.equals(relationship.getRightEnd().getType());
+                        }
+                    });
+                    for (Relationship<?, ?> result : results) {
+                        references.add(result.getRightEnd());
+                    }
                 }
             } else if (intAttrName.getMembershipOfGroup() != null) {
                 Group group = groupDAO.findByName(intAttrName.getMembershipOfGroup());
                 membership = groupableRelatable.getMembership(group.getKey());
             }
         }
-        if (reference == null) {
+        if (references.isEmpty()) {
             LOG.warn("Could not determine the reference instance for {}", mapItem.getIntAttrName());
             return Collections.emptyList();
         }
@@ -438,143 +488,145 @@ public class MappingManagerImpl implements MappingManager {
         List<PlainAttrValue> values = new ArrayList<>();
         boolean transform = true;
 
-        AnyUtils anyUtils = anyUtilsFactory.getInstance(reference);
-        if (intAttrName.getField() != null) {
-            PlainAttrValue attrValue = anyUtils.newPlainAttrValue();
-
-            switch (intAttrName.getField()) {
-                case "key":
-                    attrValue.setStringValue(reference.getKey());
-                    values.add(attrValue);
-                    break;
+        for (Any<?> reference : references) {
+            AnyUtils anyUtils = anyUtilsFactory.getInstance(reference);
+            if (intAttrName.getField() != null) {
+                PlainAttrValue attrValue = anyUtils.newPlainAttrValue();
 
-                case "realm":
-                    attrValue.setStringValue(reference.getRealm().getFullPath());
-                    values.add(attrValue);
-                    break;
+                switch (intAttrName.getField()) {
+                    case "key":
+                        attrValue.setStringValue(reference.getKey());
+                        values.add(attrValue);
+                        break;
 
-                case "password":
-                    // ignore
-                    break;
+                    case "realm":
+                        attrValue.setStringValue(reference.getRealm().getFullPath());
+                        values.add(attrValue);
+                        break;
+
+                    case "password":
+                        // ignore
+                        break;
+
+                    case "userOwner":
+                    case "groupOwner":
+                        Mapping uMapping = provision.getAnyType().equals(anyTypeDAO.findUser())
+                                ? provision.getMapping()
+                                : null;
+                        Mapping gMapping = provision.getAnyType().equals(anyTypeDAO.findGroup())
+                                ? provision.getMapping()
+                                : null;
+
+                        if (reference instanceof Group) {
+                            Group group = (Group) reference;
+                            String groupOwnerValue = null;
+                            if (group.getUserOwner() != null && uMapping != null) {
+                                groupOwnerValue = getGroupOwnerValue(provision, group.getUserOwner());
+                            }
+                            if (group.getGroupOwner() != null && gMapping != null) {
+                                groupOwnerValue = getGroupOwnerValue(provision, group.getGroupOwner());
+                            }
 
-                case "userOwner":
-                case "groupOwner":
-                    Mapping uMapping = provision.getAnyType().equals(anyTypeDAO.findUser())
-                            ? provision.getMapping()
-                            : null;
-                    Mapping gMapping = provision.getAnyType().equals(anyTypeDAO.findGroup())
-                            ? provision.getMapping()
-                            : null;
-
-                    if (reference instanceof Group) {
-                        Group group = (Group) reference;
-                        String groupOwnerValue = null;
-                        if (group.getUserOwner() != null && uMapping != null) {
-                            groupOwnerValue = getGroupOwnerValue(provision, group.getUserOwner());
-                        }
-                        if (group.getGroupOwner() != null && gMapping != null) {
-                            groupOwnerValue = getGroupOwnerValue(provision, group.getGroupOwner());
+                            if (StringUtils.isNotBlank(groupOwnerValue)) {
+                                attrValue.setStringValue(groupOwnerValue);
+                                values.add(attrValue);
+                            }
                         }
+                        break;
 
-                        if (StringUtils.isNotBlank(groupOwnerValue)) {
-                            attrValue.setStringValue(groupOwnerValue);
+                    case "suspended":
+                        if (reference instanceof User) {
+                            attrValue.setBooleanValue(((User) reference).isSuspended());
                             values.add(attrValue);
                         }
-                    }
-                    break;
+                        break;
 
-                case "suspended":
-                    if (reference instanceof User) {
-                        attrValue.setBooleanValue(((User) reference).isSuspended());
-                        values.add(attrValue);
-                    }
-                    break;
-
-                case "mustChangePassword":
-                    if (reference instanceof User) {
-                        attrValue.setBooleanValue(((User) reference).isMustChangePassword());
-                        values.add(attrValue);
-                    }
-                    break;
+                    case "mustChangePassword":
+                        if (reference instanceof User) {
+                            attrValue.setBooleanValue(((User) reference).isMustChangePassword());
+                            values.add(attrValue);
+                        }
+                        break;
 
-                default:
-                    try {
-                        Object fieldValue = FieldUtils.readField(reference, intAttrName.getField(), true);
-                        if (fieldValue instanceof Date) {
-                            // needed because ConnId does not natively supports the Date type
-                            attrValue.setStringValue(DateFormatUtils.ISO_8601_EXTENDED_DATETIME_TIME_ZONE_FORMAT.
-                                    format((Date) fieldValue));
-                        } else if (Boolean.TYPE.isInstance(fieldValue)) {
-                            attrValue.setBooleanValue((Boolean) fieldValue);
-                        } else if (Double.TYPE.isInstance(fieldValue) || Float.TYPE.isInstance(fieldValue)) {
-                            attrValue.setDoubleValue((Double) fieldValue);
-                        } else if (Long.TYPE.isInstance(fieldValue) || Integer.TYPE.isInstance(fieldValue)) {
-                            attrValue.setLongValue((Long) fieldValue);
+                    default:
+                        try {
+                            Object fieldValue = FieldUtils.readField(reference, intAttrName.getField(), true);
+                            if (fieldValue instanceof Date) {
+                                // needed because ConnId does not natively supports the Date type
+                                attrValue.setStringValue(DateFormatUtils.ISO_8601_EXTENDED_DATETIME_TIME_ZONE_FORMAT.
+                                        format((Date) fieldValue));
+                            } else if (Boolean.TYPE.isInstance(fieldValue)) {
+                                attrValue.setBooleanValue((Boolean) fieldValue);
+                            } else if (Double.TYPE.isInstance(fieldValue) || Float.TYPE.isInstance(fieldValue)) {
+                                attrValue.setDoubleValue((Double) fieldValue);
+                            } else if (Long.TYPE.isInstance(fieldValue) || Integer.TYPE.isInstance(fieldValue)) {
+                                attrValue.setLongValue((Long) fieldValue);
+                            } else {
+                                attrValue.setStringValue(fieldValue.toString());
+                            }
+                            values.add(attrValue);
+                        } catch (Exception e) {
+                            LOG.error("Could not read value of '{}' from {}", intAttrName.getField(), reference, e);
+                        }
+                }
+            } else if (intAttrName.getSchemaType() != null) {
+                switch (intAttrName.getSchemaType()) {
+                    case PLAIN:
+                        PlainAttr<?> attr;
+                        if (membership == null) {
+                            attr = reference.getPlainAttr(intAttrName.getSchemaName());
                         } else {
-                            attrValue.setStringValue(fieldValue.toString());
+                            attr = ((GroupableRelatable<?, ?, ?, ?, ?>) reference).getPlainAttr(
+                                    intAttrName.getSchemaName(), membership);
                         }
-                        values.add(attrValue);
-                    } catch (Exception e) {
-                        LOG.error("Could not read value of '{}' from {}", intAttrName.getField(), reference, e);
-                    }
-            }
-        } else if (intAttrName.getSchemaType() != null) {
-            switch (intAttrName.getSchemaType()) {
-                case PLAIN:
-                    PlainAttr<?> attr;
-                    if (membership == null) {
-                        attr = reference.getPlainAttr(intAttrName.getSchemaName());
-                    } else {
-                        attr = ((GroupableRelatable<?, ?, ?, ?, ?>) reference).getPlainAttr(
-                                intAttrName.getSchemaName(), membership);
-                    }
-                    if (attr != null) {
-                        if (attr.getUniqueValue() != null) {
-                            values.add(anyUtils.clonePlainAttrValue(attr.getUniqueValue()));
-                        } else if (attr.getValues() != null) {
-                            for (PlainAttrValue value : attr.getValues()) {
-                                values.add(anyUtils.clonePlainAttrValue(value));
+                        if (attr != null) {
+                            if (attr.getUniqueValue() != null) {
+                                values.add(anyUtils.clonePlainAttrValue(attr.getUniqueValue()));
+                            } else if (attr.getValues() != null) {
+                                for (PlainAttrValue value : attr.getValues()) {
+                                    values.add(anyUtils.clonePlainAttrValue(value));
+                                }
                             }
                         }
-                    }
-                    break;
-
-                case DERIVED:
-                    DerSchema derSchema = derSchemaDAO.find(intAttrName.getSchemaName());
-                    if (derSchema != null) {
-                        String value = membership == null
-                                ? derAttrHandler.getValue(reference, derSchema)
-                                : derAttrHandler.getValue(reference, membership, derSchema);
-                        if (value != null) {
-                            PlainAttrValue attrValue = anyUtils.newPlainAttrValue();
-                            attrValue.setStringValue(value);
-                            values.add(attrValue);
+                        break;
+
+                    case DERIVED:
+                        DerSchema derSchema = derSchemaDAO.find(intAttrName.getSchemaName());
+                        if (derSchema != null) {
+                            String value = membership == null
+                                    ? derAttrHandler.getValue(reference, derSchema)
+                                    : derAttrHandler.getValue(reference, membership, derSchema);
+                            if (value != null) {
+                                PlainAttrValue attrValue = anyUtils.newPlainAttrValue();
+                                attrValue.setStringValue(value);
+                                values.add(attrValue);
+                            }
                         }
-                    }
-                    break;
-
-                case VIRTUAL:
-                    // virtual attributes don't get transformed
-                    transform = false;
-
-                    VirSchema virSchema = virSchemaDAO.find(intAttrName.getSchemaName());
-                    if (virSchema != null) {
-                        LOG.debug("Expire entry cache {}-{}", reference, intAttrName.getSchemaName());
-                        virAttrCache.expire(
-                                reference.getType().getKey(), reference.getKey(), intAttrName.getSchemaName());
-
-                        List<String> virValues = membership == null
-                                ? virAttrHandler.getValues(reference, virSchema)
-                                : virAttrHandler.getValues(reference, membership, virSchema);
-                        for (String value : virValues) {
-                            PlainAttrValue attrValue = anyUtils.newPlainAttrValue();
-                            attrValue.setStringValue(value);
-                            values.add(attrValue);
+                        break;
+
+                    case VIRTUAL:
+                        // virtual attributes don't get transformed
+                        transform = false;
+
+                        VirSchema virSchema = virSchemaDAO.find(intAttrName.getSchemaName());
+                        if (virSchema != null) {
+                            LOG.debug("Expire entry cache {}-{}", reference, intAttrName.getSchemaName());
+                            virAttrCache.expire(
+                                    reference.getType().getKey(), reference.getKey(), intAttrName.getSchemaName());
+
+                            List<String> virValues = membership == null
+                                    ? virAttrHandler.getValues(reference, virSchema)
+                                    : virAttrHandler.getValues(reference, membership, virSchema);
+                            for (String value : virValues) {
+                                PlainAttrValue attrValue = anyUtils.newPlainAttrValue();
+                                attrValue.setStringValue(value);
+                                values.add(attrValue);
+                            }
                         }
-                    }
-                    break;
+                        break;
 
-                default:
+                    default:
+                }
             }
         }
 

http://git-wip-us.apache.org/repos/asf/syncope/blob/02b6d4eb/core/provisioning-java/src/main/java/org/apache/syncope/core/provisioning/java/data/ResourceDataBinderImpl.java
----------------------------------------------------------------------
diff --git a/core/provisioning-java/src/main/java/org/apache/syncope/core/provisioning/java/data/ResourceDataBinderImpl.java b/core/provisioning-java/src/main/java/org/apache/syncope/core/provisioning/java/data/ResourceDataBinderImpl.java
index d4c4a76..294dbc1 100644
--- a/core/provisioning-java/src/main/java/org/apache/syncope/core/provisioning/java/data/ResourceDataBinderImpl.java
+++ b/core/provisioning-java/src/main/java/org/apache/syncope/core/provisioning/java/data/ResourceDataBinderImpl.java
@@ -480,6 +480,21 @@ public class ResourceDataBinderImpl implements ResourceDataBinder {
                                         "No need to map virtual schema on linking resource");
                             }
                         }
+                        if (intAttrName.getRelatedUser() != null
+                                && item.getPurpose() != MappingPurpose.PROPAGATION) {
+
+                            invalidMapping.getElements().add(
+                                    "Only " + MappingPurpose.PROPAGATION.name()
+                                    + " allowed when referring to users");
+                        }
+                        if ((intAttrName.getRelationshipType() != null
+                                || intAttrName.getRelationshipAnyType() != null)
+                                && item.getPurpose() != MappingPurpose.PROPAGATION) {
+
+                            invalidMapping.getElements().add(
+                                    "Only " + MappingPurpose.PROPAGATION.name()
+                                    + " allowed when referring to relationships");
+                        }
                     } else {
                         LOG.error("'{}' not allowed", itemTO.getIntAttrName());
                         invalidMapping.getElements().add("'" + itemTO.getIntAttrName() + "' not allowed");

http://git-wip-us.apache.org/repos/asf/syncope/blob/02b6d4eb/core/provisioning-java/src/test/java/org/apache/syncope/core/provisioning/java/IntAttrNameParserTest.java
----------------------------------------------------------------------
diff --git a/core/provisioning-java/src/test/java/org/apache/syncope/core/provisioning/java/IntAttrNameParserTest.java b/core/provisioning-java/src/test/java/org/apache/syncope/core/provisioning/java/IntAttrNameParserTest.java
index adf2871..334a162 100644
--- a/core/provisioning-java/src/test/java/org/apache/syncope/core/provisioning/java/IntAttrNameParserTest.java
+++ b/core/provisioning-java/src/test/java/org/apache/syncope/core/provisioning/java/IntAttrNameParserTest.java
@@ -49,6 +49,9 @@ public class IntAttrNameParserTest extends AbstractTest {
         assertNull(intAttrName.getEnclosingGroup());
         assertNull(intAttrName.getMembershipOfGroup());
         assertNull(intAttrName.getRelatedAnyObject());
+        assertNull(intAttrName.getRelationshipAnyType());
+        assertNull(intAttrName.getRelationshipType());
+        assertNull(intAttrName.getRelatedUser());
 
         intAttrName = intAttrNameParser.parse("name", AnyTypeKind.GROUP);
         assertNotNull(intAttrName);
@@ -60,6 +63,9 @@ public class IntAttrNameParserTest extends AbstractTest {
         assertNull(intAttrName.getEnclosingGroup());
         assertNull(intAttrName.getMembershipOfGroup());
         assertNull(intAttrName.getRelatedAnyObject());
+        assertNull(intAttrName.getRelationshipAnyType());
+        assertNull(intAttrName.getRelationshipType());
+        assertNull(intAttrName.getRelatedUser());
 
         intAttrName = intAttrNameParser.parse("userOwner", AnyTypeKind.GROUP);
         assertNotNull(intAttrName);
@@ -71,6 +77,9 @@ public class IntAttrNameParserTest extends AbstractTest {
         assertNull(intAttrName.getEnclosingGroup());
         assertNull(intAttrName.getMembershipOfGroup());
         assertNull(intAttrName.getRelatedAnyObject());
+        assertNull(intAttrName.getRelationshipAnyType());
+        assertNull(intAttrName.getRelationshipType());
+        assertNull(intAttrName.getRelatedUser());
 
         intAttrName = intAttrNameParser.parse("name", AnyTypeKind.USER);
         assertNotNull(intAttrName);
@@ -89,6 +98,9 @@ public class IntAttrNameParserTest extends AbstractTest {
         assertNull(intAttrName.getEnclosingGroup());
         assertNull(intAttrName.getMembershipOfGroup());
         assertNull(intAttrName.getRelatedAnyObject());
+        assertNull(intAttrName.getRelationshipAnyType());
+        assertNull(intAttrName.getRelationshipType());
+        assertNull(intAttrName.getRelatedUser());
 
         intAttrName = intAttrNameParser.parse("cn", AnyTypeKind.ANY_OBJECT);
         assertNotNull(intAttrName);
@@ -99,6 +111,9 @@ public class IntAttrNameParserTest extends AbstractTest {
         assertNull(intAttrName.getEnclosingGroup());
         assertNull(intAttrName.getMembershipOfGroup());
         assertNull(intAttrName.getRelatedAnyObject());
+        assertNull(intAttrName.getRelationshipAnyType());
+        assertNull(intAttrName.getRelationshipType());
+        assertNull(intAttrName.getRelatedUser());
 
         intAttrName = intAttrNameParser.parse("rvirtualdata", AnyTypeKind.ANY_OBJECT);
         assertNotNull(intAttrName);
@@ -109,6 +124,9 @@ public class IntAttrNameParserTest extends AbstractTest {
         assertNull(intAttrName.getEnclosingGroup());
         assertNull(intAttrName.getMembershipOfGroup());
         assertNull(intAttrName.getRelatedAnyObject());
+        assertNull(intAttrName.getRelationshipAnyType());
+        assertNull(intAttrName.getRelationshipType());
+        assertNull(intAttrName.getRelatedUser());
     }
 
     @Test
@@ -122,6 +140,25 @@ public class IntAttrNameParserTest extends AbstractTest {
         assertEquals("readers", intAttrName.getEnclosingGroup());
         assertNull(intAttrName.getMembershipOfGroup());
         assertNull(intAttrName.getRelatedAnyObject());
+        assertNull(intAttrName.getRelationshipAnyType());
+        assertNull(intAttrName.getRelationshipType());
+        assertNull(intAttrName.getRelatedUser());
+    }
+
+    @Test
+    public void relatedUser() throws ParseException {
+        IntAttrName intAttrName = intAttrNameParser.parse("users[bellini].firstname", AnyTypeKind.USER);
+        assertNotNull(intAttrName);
+        assertEquals(AnyTypeKind.USER, intAttrName.getAnyTypeKind());
+        assertNull(intAttrName.getField());
+        assertEquals("firstname", intAttrName.getSchemaName());
+        assertEquals(SchemaType.PLAIN, intAttrName.getSchemaType());
+        assertEquals("bellini", intAttrName.getRelatedUser());
+        assertNull(intAttrName.getEnclosingGroup());
+        assertNull(intAttrName.getMembershipOfGroup());
+        assertNull(intAttrName.getRelatedAnyObject());
+        assertNull(intAttrName.getRelationshipAnyType());
+        assertNull(intAttrName.getRelationshipType());
     }
 
     @Test
@@ -135,6 +172,9 @@ public class IntAttrNameParserTest extends AbstractTest {
         assertNull(intAttrName.getEnclosingGroup());
         assertEquals("hp", intAttrName.getRelatedAnyObject());
         assertNull(intAttrName.getMembershipOfGroup());
+        assertNull(intAttrName.getRelationshipAnyType());
+        assertNull(intAttrName.getRelationshipType());
+        assertNull(intAttrName.getRelatedUser());
     }
 
     @Test
@@ -148,6 +188,25 @@ public class IntAttrNameParserTest extends AbstractTest {
         assertNull(intAttrName.getEnclosingGroup());
         assertEquals("top", intAttrName.getMembershipOfGroup());
         assertNull(intAttrName.getRelatedAnyObject());
+        assertNull(intAttrName.getRelationshipAnyType());
+        assertNull(intAttrName.getRelationshipType());
+        assertNull(intAttrName.getRelatedUser());
+    }
+
+    @Test
+    public void relationship() throws ParseException {
+        IntAttrName intAttrName = intAttrNameParser.parse("relationships[inclusion][PRINTER].location",
+                AnyTypeKind.USER);
+        assertNotNull(intAttrName);
+        assertEquals(AnyTypeKind.ANY_OBJECT, intAttrName.getAnyTypeKind());
+        assertNull(intAttrName.getField());
+        assertEquals("location", intAttrName.getSchemaName());
+        assertEquals(SchemaType.PLAIN, intAttrName.getSchemaType());
+        assertEquals("inclusion", intAttrName.getRelationshipType());
+        assertEquals("PRINTER", intAttrName.getRelationshipAnyType());
+        assertNull(intAttrName.getEnclosingGroup());
+        assertNull(intAttrName.getRelatedAnyObject());
+        assertNull(intAttrName.getRelatedUser());
     }
 
     @Test

http://git-wip-us.apache.org/repos/asf/syncope/blob/02b6d4eb/src/main/asciidoc/reference-guide/concepts/externalresources.adoc
----------------------------------------------------------------------
diff --git a/src/main/asciidoc/reference-guide/concepts/externalresources.adoc b/src/main/asciidoc/reference-guide/concepts/externalresources.adoc
index d886191..b22eb9a 100644
--- a/src/main/asciidoc/reference-guide/concepts/externalresources.adoc
+++ b/src/main/asciidoc/reference-guide/concepts/externalresources.adoc
@@ -122,8 +122,12 @@ specified by an expression matching one of the following models:
 ** `schema` - resolves to the attribute for the given `schema`, owned by the mapped entity (user, group, any object)
 ** `groups[groupName].schema` - resolves to the attribute for the given `schema`, owned by the group with name
 `groupName`, if a membership for the mapped entity exists
+** `users[userName].schema` - resolves to the attribute for the given `schema`, owned by the user with name
+`userName`, if a relationship with the mapped entity exists
 ** `anyObjects[anyObjectName].schema` - resolves to the attribute for the given `schema`, owned by the any object with
 name `anyObjectName`, if a relationship with the mapped entity exists
+** `relationships[relationshipType][relationshipAnyType].schema` - resolves to the attribute for the given `schema`, 
+owned by the any object of type `relationshipAnyType`, if a relationship with the mapped entity and type `relationshipType` exists
 ** `memberships[groupName].schema` - resolves to the attribute for the given `schema`, owned by the membership for group
 `groupName` of the mapped entity (user, any object), if such a membership exists
 * external attribute - the name of the attribute on the Identity Store